diff --git a/.axon/settings.local.json b/.axon/settings.local.json index 19104a21..50a85cfe 100644 --- a/.axon/settings.local.json +++ b/.axon/settings.local.json @@ -79,7 +79,6 @@ "Bash(timeout 8 npx tsx:*)", "Bash(reg query \"HKCU\\\\Software\\\\Google\\\\Chrome\\\\NativeMessagingHosts\\\\com.anthropic.claude_browser_extension\")", "Bash(cmd /c \"echo test | more > \\\\.\\\\pipe\\\\claude-mcp-browser-bridge-wangbj\")", - "Bash(cmd /c \"start /b npx tsx F:/axon/src/cli.ts --chrome-native-host 2>&1\")", "Bash(timeout 3 echo:*)", "Bash(timeout 2 node:*)", "Bash(cmd /c \"tasklist /FI \"\"IMAGENAME eq node.exe\"\" 2>&1\")", diff --git a/.blueprint/95153ea4-aa71-42da-8736-4305c33930e9.json b/.blueprint/95153ea4-aa71-42da-8736-4305c33930e9.json deleted file mode 100644 index fe242880..00000000 --- a/.blueprint/95153ea4-aa71-42da-8736-4305c33930e9.json +++ /dev/null @@ -1,1495 +0,0 @@ -{ - "id": "95153ea4-aa71-42da-8736-4305c33930e9", - "name": "claude-code-open", - "description": "claude-code-open 是一个基于 React 框架的TypeScript 项目。包含 32 个主要模块:src/agents、src/background、src/blueprint、src/chrome-mcp、src/commands、src/config、src/context、src/core、src/env、src/git、src/hooks、src/map、src/mcp、src/media、src/memory、src/models、src/network、src/parser、src/permissions、src/plugins、src/prompt、src/sandbox、src/security、src/session、src/streaming、src/teleport、src/tools、src/types、src/ui、src/utils、src/web/client、src/web/server。", - "version": "1.0.0", - "status": "completed", - "projectPath": "F:\\claude-code-open", - "businessProcesses": [ - { - "id": "95ae546f-e732-4f31-97d9-acea5e6d97aa", - "name": "用户发起对话流程", - "description": "用户通过 CLI 或 Web UI 发送消息,系统处理并返回 AI 响应", - "type": "to-be", - "steps": [ - { - "id": "", - "order": 1, - "name": "用户在 UI(CLI 或 Web)输入消息", - "description": "用户在 UI(CLI 或 Web)输入消息", - "actor": "系统" - }, - { - "id": "", - "order": 2, - "name": "commands 模块解析命令和参数", - "description": "commands 模块解析命令和参数", - "actor": "系统" - }, - { - "id": "", - "order": 3, - "name": "permissions 模块检查所需权限", - "description": "permissions 模块检查所需权限", - "actor": "系统" - }, - { - "id": "", - "order": 4, - "name": "context 模块构建对话上下文", - "description": "context 模块构建对话上下文", - "actor": "系统" - }, - { - "id": "", - "order": 5, - "name": "memory 模块加载相关历史记忆", - "description": "memory 模块加载相关历史记忆", - "actor": "系统" - }, - { - "id": "", - "order": 6, - "name": "core 模块调用 Claude API", - "description": "core 模块调用 Claude API", - "actor": "系统" - }, - { - "id": "", - "order": 7, - "name": "streaming 模块处理流式响应", - "description": "streaming 模块处理流式响应", - "actor": "系统" - }, - { - "id": "", - "order": 8, - "name": "UI 模块实时显示响应", - "description": "UI 模块实时显示响应", - "actor": "系统" - }, - { - "id": "", - "order": 9, - "name": "memory 模块保存对话历史", - "description": "memory 模块保存对话历史", - "actor": "系统" - } - ], - "actors": [ - "系统", - "用户" - ], - "inputs": [], - "outputs": [] - }, - { - "id": "f0c7d0a5-832f-4215-aa69-c4e7264d05f3", - "name": "工具调用流程", - "description": "AI 在对话中调用工具执行特定任务", - "type": "to-be", - "steps": [ - { - "id": "", - "order": 1, - "name": "Claude API 返回工具调用请求", - "description": "Claude API 返回工具调用请求", - "actor": "系统" - }, - { - "id": "", - "order": 2, - "name": "tools 模块解析工具名称和参数", - "description": "tools 模块解析工具名称和参数", - "actor": "系统" - }, - { - "id": "", - "order": 3, - "name": "permissions 模块验证工具调用权限", - "description": "permissions 模块验证工具调用权限", - "actor": "系统" - }, - { - "id": "", - "order": 4, - "name": "如需授权,通过 UI 请求用户确认", - "description": "如需授权,通过 UI 请求用户确认", - "actor": "系统" - }, - { - "id": "", - "order": 5, - "name": "sandbox 模块创建隔离执行环境", - "description": "sandbox 模块创建隔离执行环境", - "actor": "系统" - }, - { - "id": "", - "order": 6, - "name": "工具在沙箱中执行并返回结果", - "description": "工具在沙箱中执行并返回结果", - "actor": "系统" - }, - { - "id": "", - "order": 7, - "name": "结果发送回 Claude API 继续对话", - "description": "结果发送回 Claude API 继续对话", - "actor": "系统" - }, - { - "id": "", - "order": 8, - "name": "清理沙箱资源", - "description": "清理沙箱资源", - "actor": "系统" - } - ], - "actors": [ - "系统", - "用户" - ], - "inputs": [], - "outputs": [] - }, - { - "id": "0d85cfe4-23c5-42d3-9621-d68c0b55047a", - "name": "项目蓝图生成流程", - "description": "扫描项目结构,生成语义蓝图文档", - "type": "to-be", - "steps": [ - { - "id": "", - "order": 1, - "name": "用户执行 blueprint 生成命令", - "description": "用户执行 blueprint 生成命令", - "actor": "系统" - }, - { - "id": "", - "order": 2, - "name": "map 模块扫描项目文件结构", - "description": "map 模块扫描项目文件结构", - "actor": "系统" - }, - { - "id": "", - "order": 3, - "name": "parser 模块解析源代码文件", - "description": "parser 模块解析源代码文件", - "actor": "系统" - }, - { - "id": "", - "order": 4, - "name": "blueprint 模块提取代码符号和依赖", - "description": "blueprint 模块提取代码符号和依赖", - "actor": "系统" - }, - { - "id": "", - "order": 5, - "name": "分析模块类型和职责", - "description": "分析模块类型和职责", - "actor": "系统" - }, - { - "id": "", - "order": 6, - "name": "生成蓝图 JSON 文档", - "description": "生成蓝图 JSON 文档", - "actor": "系统" - }, - { - "id": "", - "order": 7, - "name": "保存到 .blueprint 目录", - "description": "保存到 .blueprint 目录", - "actor": "系统" - }, - { - "id": "", - "order": 8, - "name": "可选:向 AI 发送蓝图以获取分析", - "description": "可选:向 AI 发送蓝图以获取分析", - "actor": "系统" - } - ], - "actors": [ - "系统", - "用户" - ], - "inputs": [], - "outputs": [] - }, - { - "id": "3e350dcc-65d2-4bb2-9701-53b660bb389b", - "name": "MCP 工具集成流程", - "description": "通过 MCP 协议集成外部工具服务器", - "type": "to-be", - "steps": [ - { - "id": "", - "order": 1, - "name": "配置 MCP 服务器连接信息", - "description": "配置 MCP 服务器连接信息", - "actor": "系统" - }, - { - "id": "", - "order": 2, - "name": "mcp 模块启动 MCP 客户端", - "description": "mcp 模块启动 MCP 客户端", - "actor": "系统" - }, - { - "id": "", - "order": 3, - "name": "连接到 MCP 服务器(如 Chrome MCP)", - "description": "连接到 MCP 服务器(如 Chrome MCP)", - "actor": "系统" - }, - { - "id": "", - "order": 4, - "name": "获取服务器提供的工具列表", - "description": "获取服务器提供的工具列表", - "actor": "系统" - }, - { - "id": "", - "order": 5, - "name": "将 MCP 工具注册到 tools 模块", - "description": "将 MCP 工具注册到 tools 模块", - "actor": "系统" - }, - { - "id": "", - "order": 6, - "name": "AI 调用 MCP 工具时通过协议转发", - "description": "AI 调用 MCP 工具时通过协议转发", - "actor": "系统" - }, - { - "id": "", - "order": 7, - "name": "接收工具执行结果并返回", - "description": "接收工具执行结果并返回", - "actor": "系统" - } - ], - "actors": [ - "系统", - "用户" - ], - "inputs": [], - "outputs": [] - }, - { - "id": "5a7238f8-87cc-44b0-94a3-fc9ccf1859d0", - "name": "多代理协作流程", - "description": "使用多个 AI 代理协作完成复杂任务", - "type": "to-be", - "steps": [ - { - "id": "", - "order": 1, - "name": "用户提交复杂任务", - "description": "用户提交复杂任务", - "actor": "系统" - }, - { - "id": "", - "order": 2, - "name": "agents 模块的 AgentCoordinator 分析任务", - "description": "agents 模块的 AgentCoordinator 分析任务", - "actor": "系统" - }, - { - "id": "", - "order": 3, - "name": "创建多个 Worker 代理", - "description": "创建多个 Worker 代理", - "actor": "系统" - }, - { - "id": "", - "order": 4, - "name": "分配子任务到各个代理(task:assigned 事件)", - "description": "分配子任务到各个代理(task:assigned 事件)", - "actor": "系统" - }, - { - "id": "", - "order": 5, - "name": "代理并行或串行执行任务", - "description": "代理并行或串行执行任务", - "actor": "系统" - }, - { - "id": "", - "order": 6, - "name": "监听 execution:completed 事件", - "description": "监听 execution:completed 事件", - "actor": "系统" - }, - { - "id": "", - "order": 7, - "name": "汇总各代理结果", - "description": "汇总各代理结果", - "actor": "系统" - }, - { - "id": "", - "order": 8, - "name": "返回最终结果给用户", - "description": "返回最终结果给用户", - "actor": "系统" - } - ], - "actors": [ - "系统", - "用户" - ], - "inputs": [], - "outputs": [] - }, - { - "id": "3b7a265a-7386-4060-8466-518215204e50", - "name": "Web UI 实时交互流程", - "description": "用户通过 Web 界面与 AI 实时对话", - "type": "to-be", - "steps": [ - { - "id": "", - "order": 1, - "name": "用户访问 Web UI", - "description": "用户访问 Web UI", - "actor": "系统" - }, - { - "id": "", - "order": 2, - "name": "web/server 提供静态页面", - "description": "web/server 提供静态页面", - "actor": "系统" - }, - { - "id": "", - "order": 3, - "name": "web/client 建立 WebSocket 连接", - "description": "web/client 建立 WebSocket 连接", - "actor": "系统" - }, - { - "id": "", - "order": 4, - "name": "用户发送消息通过 WebSocket", - "description": "用户发送消息通过 WebSocket", - "actor": "系统" - }, - { - "id": "", - "order": 5, - "name": "server 验证请求并调用 core 模块", - "description": "server 验证请求并调用 core 模块", - "actor": "系统" - }, - { - "id": "", - "order": 6, - "name": "通过 WebSocket 流式推送响应块", - "description": "通过 WebSocket 流式推送响应块", - "actor": "系统" - }, - { - "id": "", - "order": 7, - "name": "client 实时渲染响应内容", - "description": "client 实时渲染响应内容", - "actor": "系统" - }, - { - "id": "", - "order": 8, - "name": "支持代码高亮、文件上传等交互", - "description": "支持代码高亮、文件上传等交互", - "actor": "系统" - } - ], - "actors": [ - "系统", - "用户" - ], - "inputs": [], - "outputs": [] - } - ], - "modules": [ - { - "id": "526444b2-3547-4921-a471-04b7526fc3d8", - "name": "src/agents", - "description": "智能代理协调和任务分配系统", - "type": "service", - "responsibilities": [ - "通用服务提供", - "工具函数", - "管理多个 AI 代理的创建和生命周期", - "任务分配和执行协调", - "代理间通信和状态同步", - "工作流编排" - ], - "dependencies": [ - "9223a1ea-630d-47a9-a7fb-acd7b621c347", - "5f0e719f-2dae-4b7f-b401-4c1048874e2d" - ], - "interfaces": [], - "techStack": [ - "TypeScript", - "React" - ], - "rootPath": "src/agents" - }, - { - "id": "c7210ca1-09dd-4788-896f-333a8fb7ba1e", - "name": "src/background", - "description": "src/background 模块 - service", - "type": "service", - "responsibilities": [ - "通用服务提供", - "工具函数" - ], - "dependencies": [ - "07ba75b6-2489-43a4-bf3c-1b7d43d2962d" - ], - "interfaces": [], - "techStack": [ - "TypeScript", - "React" - ], - "rootPath": "src/background" - }, - { - "id": "3a0ef1ab-11fd-4d49-9433-f3a83f908ad0", - "name": "src/blueprint", - "description": "项目蓝图生成和代码语义分析", - "type": "service", - "responsibilities": [ - "通用服务提供", - "工具函数", - "扫描项目结构生成蓝图", - "提取代码符号和依赖关系", - "语义分析和模块识别", - "生成项目文档和架构图" - ], - "dependencies": [ - "4fe8d353-77d4-423b-8382-422d90aeeae8", - "2c454bf3-6508-4e5f-a511-1381b247a444" - ], - "interfaces": [], - "techStack": [ - "TypeScript", - "React" - ], - "rootPath": "src/blueprint" - }, - { - "id": "fbaec3be-2c25-48f7-8ec1-8ecf16ce3fcc", - "name": "src/chrome-mcp", - "description": "Model Context Protocol 实现和服务器管理", - "type": "service", - "responsibilities": [ - "通用服务提供", - "工具函数", - "实现 MCP 协议规范", - "管理 MCP 服务器连接", - "处理工具和资源注册", - "协调 MCP 客户端和服务端通信" - ], - "dependencies": [], - "interfaces": [], - "techStack": [ - "TypeScript", - "React" - ], - "rootPath": "src/chrome-mcp" - }, - { - "id": "af16b6ea-2d87-4e42-bcd7-55dc00ca6871", - "name": "src/commands", - "description": "CLI 命令定义和处理", - "type": "backend", - "responsibilities": [ - "业务逻辑处理", - "API 接口提供", - "认证授权", - "定义可用命令", - "解析命令行参数", - "执行命令逻辑", - "输出命令结果" - ], - "dependencies": [ - "1b8da626-fe5c-4e82-8509-05c11bd8efee", - "eb4bb4fb-5675-4c77-a279-e93bd9b5fd02", - "b759d6d3-9210-4497-8c7f-6b11bfd67ee2", - "e392adfa-3e97-4f1c-9c80-b583552cfd16", - "07ba75b6-2489-43a4-bf3c-1b7d43d2962d" - ], - "interfaces": [], - "techStack": [ - "TypeScript", - "React", - "Express" - ], - "rootPath": "src/commands" - }, - { - "id": "1b8da626-fe5c-4e82-8509-05c11bd8efee", - "name": "src/config", - "description": "配置管理和环境变量处理", - "type": "infrastructure", - "responsibilities": [ - "配置管理", - "部署脚本", - "加载和保存配置", - "管理环境变量", - "验证配置有效性", - "提供配置访问接口" - ], - "dependencies": [ - "9223a1ea-630d-47a9-a7fb-acd7b621c347", - "4a57b40c-d110-4288-ad2d-ae9b3d50f566" - ], - "interfaces": [], - "techStack": [ - "TypeScript", - "React" - ], - "rootPath": "src/config" - }, - { - "id": "b759d6d3-9210-4497-8c7f-6b11bfd67ee2", - "name": "src/context", - "description": "上下文管理和智能裁剪", - "type": "service", - "responsibilities": [ - "通用服务提供", - "工具函数", - "管理对话上下文窗口", - "智能裁剪历史消息", - "上下文大小估算", - "优先级排序" - ], - "dependencies": [ - "9223a1ea-630d-47a9-a7fb-acd7b621c347" - ], - "interfaces": [], - "techStack": [ - "TypeScript", - "React" - ], - "rootPath": "src/context" - }, - { - "id": "4fe8d353-77d4-423b-8382-422d90aeeae8", - "name": "src/core", - "description": "核心 AI 交互和 API 调用层", - "type": "backend", - "responsibilities": [ - "业务逻辑处理", - "API 接口提供", - "封装 Anthropic Claude API 调用", - "管理会话和消息流", - "处理流式响应", - "错误处理和重试逻辑" - ], - "dependencies": [ - "07ba75b6-2489-43a4-bf3c-1b7d43d2962d", - "9223a1ea-630d-47a9-a7fb-acd7b621c347", - "d67a951e-ab05-467f-9f3a-9462960501fc", - "5f0e719f-2dae-4b7f-b401-4c1048874e2d", - "0a37ae2a-3206-4088-8cfd-dd422f1347be", - "2abfd2ee-c4ed-40f3-99f6-95eeb6a25b89", - "1b8da626-fe5c-4e82-8509-05c11bd8efee", - "16ca5515-1ac5-402c-be01-dd30d8a15d03" - ], - "interfaces": [], - "techStack": [ - "TypeScript", - "React", - "Express" - ], - "rootPath": "src/core" - }, - { - "id": "4a57b40c-d110-4288-ad2d-ae9b3d50f566", - "name": "src/env", - "description": "src/env 模块 - service", - "type": "service", - "responsibilities": [ - "通用服务提供", - "工具函数" - ], - "dependencies": [], - "interfaces": [], - "techStack": [ - "TypeScript", - "React" - ], - "rootPath": "src/env" - }, - { - "id": "16ca5515-1ac5-402c-be01-dd30d8a15d03", - "name": "src/git", - "description": "Git 版本控制集成", - "type": "service", - "responsibilities": [ - "通用服务提供", - "工具函数", - "执行 Git 命令", - "获取仓库状态", - "提交和推送更改", - "分支管理" - ], - "dependencies": [ - "07ba75b6-2489-43a4-bf3c-1b7d43d2962d" - ], - "interfaces": [], - "techStack": [ - "TypeScript", - "React" - ], - "rootPath": "src/git" - }, - { - "id": "2abfd2ee-c4ed-40f3-99f6-95eeb6a25b89", - "name": "src/hooks", - "description": "src/hooks 模块 - service", - "type": "service", - "responsibilities": [ - "通用服务提供", - "工具函数" - ], - "dependencies": [ - "3a0ef1ab-11fd-4d49-9433-f3a83f908ad0" - ], - "interfaces": [], - "techStack": [ - "TypeScript", - "React" - ], - "rootPath": "src/hooks" - }, - { - "id": "eb4bb4fb-5675-4c77-a279-e93bd9b5fd02", - "name": "src/map", - "description": "项目文件映射和索引", - "type": "backend", - "responsibilities": [ - "业务逻辑处理", - "API 接口提供", - "构建项目文件索引", - "快速文件查找", - "依赖关系图构建", - "文件变更检测" - ], - "dependencies": [ - "4fe8d353-77d4-423b-8382-422d90aeeae8" - ], - "interfaces": [], - "techStack": [ - "TypeScript", - "React", - "Express" - ], - "rootPath": "src/map" - }, - { - "id": "3a7e3e57-8ffe-49ff-9416-117967c00575", - "name": "src/mcp", - "description": "Model Context Protocol 实现和服务器管理", - "type": "service", - "responsibilities": [ - "通用服务提供", - "工具函数", - "实现 MCP 协议规范", - "管理 MCP 服务器连接", - "处理工具和资源注册", - "协调 MCP 客户端和服务端通信" - ], - "dependencies": [ - "9223a1ea-630d-47a9-a7fb-acd7b621c347", - "5f0e719f-2dae-4b7f-b401-4c1048874e2d", - "4fe8d353-77d4-423b-8382-422d90aeeae8" - ], - "interfaces": [], - "techStack": [ - "TypeScript", - "React" - ], - "rootPath": "src/mcp" - }, - { - "id": "c074babc-4b2b-48cd-a380-45b6a27d3c03", - "name": "src/media", - "description": "多媒体文件处理", - "type": "service", - "responsibilities": [ - "通用服务提供", - "工具函数", - "处理图片上传和转换", - "文件类型检测", - "图片压缩和优化", - "多模态内容编码" - ], - "dependencies": [], - "interfaces": [], - "techStack": [ - "TypeScript", - "React" - ], - "rootPath": "src/media" - }, - { - "id": "be12691b-3844-4faa-baa2-37d60ded1d89", - "name": "src/memory", - "description": "对话历史和上下文记忆管理", - "type": "service", - "responsibilities": [ - "通用服务提供", - "工具函数", - "持久化对话历史", - "管理会话向量存储", - "实现语义搜索和相关记忆检索", - "优化上下文窗口使用" - ], - "dependencies": [ - "07ba75b6-2489-43a4-bf3c-1b7d43d2962d" - ], - "interfaces": [], - "techStack": [ - "TypeScript", - "React" - ], - "rootPath": "src/memory" - }, - { - "id": "0a37ae2a-3206-4088-8cfd-dd422f1347be", - "name": "src/models", - "description": "数据模型和持久化", - "type": "database", - "responsibilities": [ - "数据持久化", - "数据模型定义", - "数据库迁移", - "定义数据模型", - "数据库操作抽象", - "数据验证", - "模型关系管理" - ], - "dependencies": [], - "interfaces": [], - "techStack": [ - "TypeScript", - "React" - ], - "rootPath": "src/models" - }, - { - "id": "d67a951e-ab05-467f-9f3a-9462960501fc", - "name": "src/network", - "description": "网络请求和代理管理", - "type": "service", - "responsibilities": [ - "通用服务提供", - "工具函数", - "HTTP/HTTPS 请求封装", - "代理服务器配置", - "请求重试和超时", - "网络错误处理" - ], - "dependencies": [ - "4fe8d353-77d4-423b-8382-422d90aeeae8" - ], - "interfaces": [], - "techStack": [ - "TypeScript", - "React" - ], - "rootPath": "src/network" - }, - { - "id": "2c454bf3-6508-4e5f-a511-1381b247a444", - "name": "src/parser", - "description": "代码解析和语法分析", - "type": "service", - "responsibilities": [ - "通用服务提供", - "工具函数", - "解析不同语言的代码", - "提取代码结构和符号", - "生成抽象语法树(AST)", - "支持多语言" - ], - "dependencies": [], - "interfaces": [], - "techStack": [ - "TypeScript", - "React" - ], - "rootPath": "src/parser" - }, - { - "id": "2d0e1cc3-4731-4683-bee2-1db812002d2b", - "name": "src/permissions", - "description": "权限控制和安全策略管理", - "type": "frontend", - "responsibilities": [ - "用户界面渲染", - "用户交互处理", - "管理工具和操作权限", - "验证用户授权", - "持久化权限配置", - "提供权限 UI 界面" - ], - "dependencies": [ - "9223a1ea-630d-47a9-a7fb-acd7b621c347" - ], - "interfaces": [], - "techStack": [ - "TypeScript", - "React", - "React" - ], - "rootPath": "src/permissions" - }, - { - "id": "26fd42db-7579-4ed4-bede-f7e41f87623b", - "name": "src/plugins", - "description": "src/plugins 模块 - service", - "type": "service", - "responsibilities": [ - "通用服务提供", - "工具函数" - ], - "dependencies": [ - "07ba75b6-2489-43a4-bf3c-1b7d43d2962d", - "9223a1ea-630d-47a9-a7fb-acd7b621c347" - ], - "interfaces": [], - "techStack": [ - "TypeScript", - "React" - ], - "rootPath": "src/plugins" - }, - { - "id": "f76691d2-b9fd-434f-b465-f12380e06c24", - "name": "src/prompt", - "description": "src/prompt 模块 - service", - "type": "service", - "responsibilities": [ - "通用服务提供", - "工具函数" - ], - "dependencies": [ - "3a0ef1ab-11fd-4d49-9433-f3a83f908ad0", - "9223a1ea-630d-47a9-a7fb-acd7b621c347" - ], - "interfaces": [], - "techStack": [ - "TypeScript", - "React" - ], - "rootPath": "src/prompt" - }, - { - "id": "c1e92bdc-ca90-4d5f-b6ad-8fa35977b6f6", - "name": "src/sandbox", - "description": "沙箱环境和安全代码执行", - "type": "infrastructure", - "responsibilities": [ - "配置管理", - "部署脚本", - "创建隔离的执行环境", - "限制资源访问", - "监控代码执行", - "防止恶意代码执行" - ], - "dependencies": [ - "07ba75b6-2489-43a4-bf3c-1b7d43d2962d" - ], - "interfaces": [], - "techStack": [ - "TypeScript", - "React" - ], - "rootPath": "src/sandbox" - }, - { - "id": "54281756-e338-4c1c-9c8a-17c41b438dcd", - "name": "src/security", - "description": "安全策略和加密服务", - "type": "service", - "responsibilities": [ - "通用服务提供", - "工具函数", - "提供加密和解密服务", - "输入验证和清理", - "防止注入攻击", - "安全随机数生成" - ], - "dependencies": [ - "1b8da626-fe5c-4e82-8509-05c11bd8efee" - ], - "interfaces": [], - "techStack": [ - "TypeScript", - "React" - ], - "rootPath": "src/security" - }, - { - "id": "bcc0ee8e-4313-4602-82b4-41bb461dacde", - "name": "src/session", - "description": "src/session 模块 - service", - "type": "service", - "responsibilities": [ - "通用服务提供", - "工具函数" - ], - "dependencies": [ - "9223a1ea-630d-47a9-a7fb-acd7b621c347", - "1b8da626-fe5c-4e82-8509-05c11bd8efee", - "b759d6d3-9210-4497-8c7f-6b11bfd67ee2" - ], - "interfaces": [], - "techStack": [ - "TypeScript", - "React" - ], - "rootPath": "src/session" - }, - { - "id": "2695a3a7-b017-4a91-b38b-b5245acf6394", - "name": "src/streaming", - "description": "流式数据处理和事件流管理", - "type": "service", - "responsibilities": [ - "通用服务提供", - "工具函数", - "处理服务器发送事件(SSE)", - "管理流式响应", - "实现背压控制", - "处理流错误和重连" - ], - "dependencies": [], - "interfaces": [], - "techStack": [ - "TypeScript", - "React" - ], - "rootPath": "src/streaming" - }, - { - "id": "95d93ba8-99de-4003-a873-6fc432249f92", - "name": "src/teleport", - "description": "src/teleport 模块 - service", - "type": "service", - "responsibilities": [ - "通用服务提供", - "工具函数" - ], - "dependencies": [ - "3a7e3e57-8ffe-49ff-9416-117967c00575" - ], - "interfaces": [], - "techStack": [ - "TypeScript", - "React" - ], - "rootPath": "src/teleport" - }, - { - "id": "5f0e719f-2dae-4b7f-b401-4c1048874e2d", - "name": "src/tools", - "description": "工具系统和函数调用管理", - "type": "service", - "responsibilities": [ - "通用服务提供", - "工具函数", - "注册和管理可用工具", - "执行工具调用", - "参数验证和类型转换", - "工具执行结果处理" - ], - "dependencies": [ - "9223a1ea-630d-47a9-a7fb-acd7b621c347", - "07ba75b6-2489-43a4-bf3c-1b7d43d2962d", - "4fe8d353-77d4-423b-8382-422d90aeeae8", - "f76691d2-b9fd-434f-b465-f12380e06c24", - "2abfd2ee-c4ed-40f3-99f6-95eeb6a25b89", - "1b8da626-fe5c-4e82-8509-05c11bd8efee", - "3a0ef1ab-11fd-4d49-9433-f3a83f908ad0" - ], - "interfaces": [], - "techStack": [ - "TypeScript", - "React" - ], - "rootPath": "src/tools" - }, - { - "id": "9223a1ea-630d-47a9-a7fb-acd7b621c347", - "name": "src/types", - "description": "TypeScript 类型定义和接口", - "type": "infrastructure", - "responsibilities": [ - "配置管理", - "部署脚本", - "定义核心数据结构", - "提供类型安全", - "接口定义", - "类型工具函数" - ], - "dependencies": [], - "interfaces": [], - "techStack": [ - "TypeScript", - "React" - ], - "rootPath": "src/types" - }, - { - "id": "e392adfa-3e97-4f1c-9c80-b583552cfd16", - "name": "src/ui", - "description": "CLI 用户界面组件(基于 Ink)", - "type": "frontend", - "responsibilities": [ - "用户界面渲染", - "用户交互处理", - "渲染命令行界面", - "处理用户交互", - "显示进度和状态", - "格式化输出" - ], - "dependencies": [ - "4fe8d353-77d4-423b-8382-422d90aeeae8", - "af16b6ea-2d87-4e42-bcd7-55dc00ca6871", - "5f0e719f-2dae-4b7f-b401-4c1048874e2d", - "1b8da626-fe5c-4e82-8509-05c11bd8efee", - "0a37ae2a-3206-4088-8cfd-dd422f1347be", - "9223a1ea-630d-47a9-a7fb-acd7b621c347", - "07ba75b6-2489-43a4-bf3c-1b7d43d2962d" - ], - "interfaces": [], - "techStack": [ - "TypeScript", - "React", - "React" - ], - "rootPath": "src/ui" - }, - { - "id": "07ba75b6-2489-43a4-bf3c-1b7d43d2962d", - "name": "src/utils", - "description": "src/utils 模块 - service", - "type": "service", - "responsibilities": [ - "通用服务提供", - "工具函数" - ], - "dependencies": [ - "1b8da626-fe5c-4e82-8509-05c11bd8efee", - "9223a1ea-630d-47a9-a7fb-acd7b621c347" - ], - "interfaces": [], - "techStack": [ - "TypeScript", - "React" - ], - "rootPath": "src/utils" - }, - { - "id": "4d30a453-b09d-4bf9-a3e4-22af7d1d8e07", - "name": "src/web/client", - "description": "Web 前端用户界面", - "type": "frontend", - "responsibilities": [ - "用户界面渲染", - "用户交互处理", - "提供 Web UI 交互界面", - "处理用户输入和显示响应", - "管理前端状态", - "与后端 API 通信" - ], - "dependencies": [], - "interfaces": [], - "techStack": [ - "TypeScript", - "React", - "React" - ], - "rootPath": "src/web/client" - }, - { - "id": "1290c0f9-51f2-4184-881d-2f2b35d86797", - "name": "src/web/server", - "description": "Web 后端服务器和 API 路由", - "type": "backend", - "responsibilities": [ - "业务逻辑处理", - "API 接口提供", - "认证授权", - "提供 HTTP API 端点", - "处理 WebSocket 连接", - "代理 Claude API 请求", - "静态文件服务" - ], - "dependencies": [ - "4fe8d353-77d4-423b-8382-422d90aeeae8", - "1b8da626-fe5c-4e82-8509-05c11bd8efee", - "0a37ae2a-3206-4088-8cfd-dd422f1347be", - "5f0e719f-2dae-4b7f-b401-4c1048874e2d", - "f76691d2-b9fd-434f-b465-f12380e06c24", - "9223a1ea-630d-47a9-a7fb-acd7b621c347", - "bcc0ee8e-4313-4602-82b4-41bb461dacde", - "3a7e3e57-8ffe-49ff-9416-117967c00575", - "be12691b-3844-4faa-baa2-37d60ded1d89", - "07ba75b6-2489-43a4-bf3c-1b7d43d2962d", - "3a0ef1ab-11fd-4d49-9433-f3a83f908ad0", - "eb4bb4fb-5675-4c77-a279-e93bd9b5fd02" - ], - "interfaces": [], - "techStack": [ - "TypeScript", - "React", - "Express" - ], - "rootPath": "src/web/server" - } - ], - "nfrs": [ - { - "id": "5fe71b2b-7752-4dbf-a56b-966abc72d601", - "category": "maintainability", - "name": "代码可维护性", - "description": "保持代码清晰、有文档、有测试", - "priority": "must" - } - ], - "createdAt": "2026-01-24T12:32:12.213Z", - "updatedAt": "2026-02-08T14:14:20.256Z", - "changeHistory": [ - { - "id": "7d2fc820-b6a5-4c53-b0df-6da43b39d809", - "timestamp": "2026-01-24T12:32:12.213Z", - "type": "create", - "description": "蓝图创建:claude-code-open", - "author": "agent" - }, - { - "id": "b4718dfa-7940-4c78-a01c-c4fea14a34af", - "timestamp": "2026-01-24T12:32:12.217Z", - "type": "update", - "description": "添加系统模块:src/agents", - "author": "agent" - }, - { - "id": "48058b83-a841-433c-a60e-806fa1ffb28d", - "timestamp": "2026-01-24T12:32:12.235Z", - "type": "update", - "description": "添加系统模块:src/background", - "author": "agent" - }, - { - "id": "49d35053-c455-49d5-8d04-4ba59d8e1092", - "timestamp": "2026-01-24T12:32:12.249Z", - "type": "update", - "description": "添加系统模块:src/blueprint", - "author": "agent" - }, - { - "id": "cfbbe58a-6708-4133-98f4-6f6653c4dd80", - "timestamp": "2026-01-24T12:32:12.263Z", - "type": "update", - "description": "添加系统模块:src/chrome-mcp", - "author": "agent" - }, - { - "id": "b2edc5ea-21c4-4f9c-9ab0-1aa0934f89ea", - "timestamp": "2026-01-24T12:32:12.275Z", - "type": "update", - "description": "添加系统模块:src/commands", - "author": "agent" - }, - { - "id": "dfd588d8-2348-4dd4-8f9b-dfcd016617b6", - "timestamp": "2026-01-24T12:32:12.291Z", - "type": "update", - "description": "添加系统模块:src/config", - "author": "agent" - }, - { - "id": "bfad6c3f-fb72-4ebd-a20d-aef6c09410cf", - "timestamp": "2026-01-24T12:32:12.292Z", - "type": "update", - "description": "添加系统模块:src/context", - "author": "agent" - }, - { - "id": "7ec389b0-3684-4e21-b762-7d7a9787ab64", - "timestamp": "2026-01-24T12:32:12.307Z", - "type": "update", - "description": "添加系统模块:src/core", - "author": "agent" - }, - { - "id": "a02f4077-46c9-467a-89fd-5a9aaaf7349a", - "timestamp": "2026-01-24T12:32:12.321Z", - "type": "update", - "description": "添加系统模块:src/env", - "author": "agent" - }, - { - "id": "8cd0c597-6a38-4ec2-b5b3-69d96db49a87", - "timestamp": "2026-01-24T12:32:12.333Z", - "type": "update", - "description": "添加系统模块:src/git", - "author": "agent" - }, - { - "id": "3a6e61f2-123a-4d11-94e3-540d57101021", - "timestamp": "2026-01-24T12:32:12.342Z", - "type": "update", - "description": "添加系统模块:src/hooks", - "author": "agent" - }, - { - "id": "9cf79beb-8e1e-4c53-a585-ab86aa99a9a0", - "timestamp": "2026-01-24T12:32:12.350Z", - "type": "update", - "description": "添加系统模块:src/map", - "author": "agent" - }, - { - "id": "8aeeefdb-e833-4dc3-a672-6617634d8c96", - "timestamp": "2026-01-24T12:32:12.350Z", - "type": "update", - "description": "添加系统模块:src/mcp", - "author": "agent" - }, - { - "id": "f85745cb-790a-44ef-8844-e598703753ab", - "timestamp": "2026-01-24T12:32:12.352Z", - "type": "update", - "description": "添加系统模块:src/media", - "author": "agent" - }, - { - "id": "2bb6e4aa-718e-40b0-97b8-8aea8f6be286", - "timestamp": "2026-01-24T12:32:12.360Z", - "type": "update", - "description": "添加系统模块:src/memory", - "author": "agent" - }, - { - "id": "574ddc75-5ae1-4e8f-9110-d9da5cf2f173", - "timestamp": "2026-01-24T12:32:12.382Z", - "type": "update", - "description": "添加系统模块:src/models", - "author": "agent" - }, - { - "id": "8fde0581-79ec-40ba-a651-1923593c766e", - "timestamp": "2026-01-24T12:32:12.408Z", - "type": "update", - "description": "添加系统模块:src/network", - "author": "agent" - }, - { - "id": "04d27d22-a33b-4a38-b33a-78d5770837c7", - "timestamp": "2026-01-24T12:32:12.435Z", - "type": "update", - "description": "添加系统模块:src/parser", - "author": "agent" - }, - { - "id": "2bdfcc68-fff9-47b0-a962-06f15729f850", - "timestamp": "2026-01-24T12:32:12.463Z", - "type": "update", - "description": "添加系统模块:src/permissions", - "author": "agent" - }, - { - "id": "1dd56782-c8a9-4007-a0ae-97a2d44fcf6a", - "timestamp": "2026-01-24T12:32:12.491Z", - "type": "update", - "description": "添加系统模块:src/plugins", - "author": "agent" - }, - { - "id": "65fb9a0c-baa3-4950-900e-90d996eccd6a", - "timestamp": "2026-01-24T12:32:12.522Z", - "type": "update", - "description": "添加系统模块:src/prompt", - "author": "agent" - }, - { - "id": "7d350111-ba7c-43d2-ace2-08e9350d2597", - "timestamp": "2026-01-24T12:32:12.549Z", - "type": "update", - "description": "添加系统模块:src/sandbox", - "author": "agent" - }, - { - "id": "ddb343a2-4af5-42be-a448-c7dc36e683f4", - "timestamp": "2026-01-24T12:32:12.550Z", - "type": "update", - "description": "添加系统模块:src/security", - "author": "agent" - }, - { - "id": "fec63a36-925a-4870-9ce3-418bf1407f8b", - "timestamp": "2026-01-24T12:32:12.581Z", - "type": "update", - "description": "添加系统模块:src/session", - "author": "agent" - }, - { - "id": "8655006c-c5f6-41e4-beb9-20b3ef8658ef", - "timestamp": "2026-01-24T12:32:12.611Z", - "type": "update", - "description": "添加系统模块:src/streaming", - "author": "agent" - }, - { - "id": "ff683c2b-f16e-4a43-a645-154ce87137af", - "timestamp": "2026-01-24T12:32:12.638Z", - "type": "update", - "description": "添加系统模块:src/teleport", - "author": "agent" - }, - { - "id": "5bec3fb6-2c7b-44c9-8377-976daf6aa6e3", - "timestamp": "2026-01-24T12:32:12.667Z", - "type": "update", - "description": "添加系统模块:src/tools", - "author": "agent" - }, - { - "id": "718f8c78-8fd8-40bf-b49e-f334dd8b9784", - "timestamp": "2026-01-24T12:32:12.669Z", - "type": "update", - "description": "添加系统模块:src/types", - "author": "agent" - }, - { - "id": "df050dd5-3a59-47c9-b905-d0df41797547", - "timestamp": "2026-01-24T12:32:12.699Z", - "type": "update", - "description": "添加系统模块:src/ui", - "author": "agent" - }, - { - "id": "c6fb85d3-b821-43cf-81f2-74aed5efdeee", - "timestamp": "2026-01-24T12:32:12.724Z", - "type": "update", - "description": "添加系统模块:src/utils", - "author": "agent" - }, - { - "id": "a690446f-a8be-45da-8cb4-a4bc163730a6", - "timestamp": "2026-01-24T12:32:12.748Z", - "type": "update", - "description": "添加系统模块:src/web/client", - "author": "agent" - }, - { - "id": "1030eb47-cd2e-4ec0-8514-b1bc587eb0ce", - "timestamp": "2026-01-24T12:32:12.750Z", - "type": "update", - "description": "添加系统模块:src/web/server", - "author": "agent" - }, - { - "id": "7c65908f-8539-433b-b2e2-d6e45052b474", - "timestamp": "2026-01-24T12:32:12.811Z", - "type": "update", - "description": "添加业务流程:用户发起对话流程", - "author": "agent" - }, - { - "id": "6eefbd67-9750-4778-a4c8-bca0f4b06066", - "timestamp": "2026-01-24T12:32:12.813Z", - "type": "update", - "description": "添加业务流程:工具调用流程", - "author": "agent" - }, - { - "id": "c835b900-2a03-4ca0-9738-c637c3dc1126", - "timestamp": "2026-01-24T12:32:12.834Z", - "type": "update", - "description": "添加业务流程:项目蓝图生成流程", - "author": "agent" - }, - { - "id": "e8fa53ae-c35c-4b11-b49d-1c13d40ed93b", - "timestamp": "2026-01-24T12:32:12.847Z", - "type": "update", - "description": "添加业务流程:MCP 工具集成流程", - "author": "agent" - }, - { - "id": "caace7c2-cc2f-4ca7-81db-e513f9de3692", - "timestamp": "2026-01-24T12:32:12.865Z", - "type": "update", - "description": "添加业务流程:多代理协作流程", - "author": "agent" - }, - { - "id": "fa8daee0-ae05-41b3-88f8-d1112eb214b3", - "timestamp": "2026-01-24T12:32:12.887Z", - "type": "update", - "description": "添加业务流程:Web UI 实时交互流程", - "author": "agent" - }, - { - "id": "f7f02482-ae99-4aaa-8482-4e46b34374ce", - "timestamp": "2026-01-24T12:32:12.913Z", - "type": "update", - "description": "添加非功能性要求:代码可维护性", - "author": "agent" - } - ], - "approvedAt": "2026-01-24T12:32:12.938Z", - "approvedBy": "system", - "source": "codebase", - "constraints": [], - "designImages": [], - "lastExecutionPlan": { - "id": "plan-1770559843727-e2txno", - "blueprintId": "95153ea4-aa71-42da-8736-4305c33930e9", - "tasks": [], - "parallelGroups": [], - "estimatedCost": 0, - "estimatedMinutes": 0, - "autoDecisions": [], - "status": "ready", - "createdAt": "2026-02-08T14:10:43.727Z" - }, - "executionState": { - "currentGroupIndex": 0, - "completedTaskIds": [], - "failedTaskIds": [], - "skippedTaskIds": [], - "taskResults": [], - "currentCost": 0, - "startedAt": "2026-02-08T14:10:43.938Z", - "lastUpdatedAt": "2026-02-08T14:10:43.938Z", - "isPaused": false, - "isCancelled": false - } -} \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 2d18046e..78a1a658 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -23,7 +23,7 @@ body: label: Steps to reproduce description: How can we reproduce this issue? placeholder: | - 1. Run `npm run web` + 1. Run `npm run dev` 2. Open browser to localhost:3456 3. Click on '...' 4. See error diff --git a/.gitignore b/.gitignore index b33d12cf..b67b68f4 100644 --- a/.gitignore +++ b/.gitignore @@ -78,3 +78,4 @@ evals/reports/ # Manus 模板项目带的平台凭证(AWS session token / forge API key / OAuth secret) .project-config.json +docs/spec/roadmap/.milestone-verified.json diff --git a/CLAUDE.md b/CLAUDE.md index 02f50f76..fb5f08e8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,7 +1,6 @@ # CLAUDE.md -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - +你的工作搭子是个中国人 ## Project Overview **axon = Manus 复刻 + 编程护城河**(北极星 — 所有产品/架构决策的根): @@ -11,6 +10,8 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co - LLM 路由:当前唯一可用 backend = axon-cloud GPT 5.5(codex-client.ts),不是 Claude **Milestone 命名**:项目 refactor 用 `F<阶段>.<子任务>` 或 `M<阶段>.<子任务>` 命名(如 F5.b / M2.a / M4.b)。提交/记忆/对话都用这个 ID 引用历史决策。 +**Axon产品哲学** +- 秩序源于约束下的共振 ## 铁律(每条都是硬性约束,没有例外) @@ -49,11 +50,10 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ### 铁律10:要像一个专业的产品经理的角度去思考 - 因为你的用户都是程序员思维,很缺乏产品经理思维,你要承担起来产品经理的角色,主动给用户提出建议和方案,这样做出来的产品才有人买单,不要讨好用户,特别产品角度,要敢于和用户battle ### 铁律11:agent 项目评估和测试大于一切 -- 决定一个agent 项目能否成功的根本因素是建立评估集合和能反馈真实使用场景的测试案例 +- 决定一个agent 项目能否成功的根本因素是建立评估集合和能反馈真实使用场景的测试案例,任何功能必须有单元测试和集成测试,前后端的端到端测试 ## 项目性质 - 永远不要增加降级方案,遇到问题直接报错,不掩盖问题。 - docs/ 文档统一保存路径,tests/ 测试用例统一保存路径。 -- 用中文回复。 - 前端端口3456是唯一可用的端口,不要用其他端口启动项目 ## 行为红线 @@ -187,7 +187,7 @@ Full-stack web application: - 配置源:`src/web/server/modeRegistry.ts` — 每 mode 含 `systemPromptExtra` 追加到 system prompt 末尾 + 可选 `toolFilter`(目前仅 `chat` mode 禁所有工具) - 前端 chip:`src/web/client/src/pages/ManusWorkbench/ChatPane.tsx` — `PINNED_CHIPS`(4) + `POPOVER_CHIPS`(9),每 chip 的 `id` 对齐 modeRegistry mode id - 消息协议:前端发 `message_start { payload: { content, mode } }` → WebSocket handler 调 `conversationManager.setSessionMode(sessionId, mode)` → manus-prompt.ts 把 `modeCfg.systemPromptExtra` 拼到 prompt 末尾 -- 12 个 mode:`slides` / `website` / `desktop-app` / `design` / `video` / `app` / `schedule`(axon 加) / `wide-research` / `spreadsheet` / `visualization` / `audio` / `chat`(axon 加) +- 12 个 mode:`slides` / `website` / `desktop-app` / `design` / `video` / `app` / `schedule` / `wide-research` / `spreadsheet` / `visualization` / `audio` / `chat`(**全部对齐 Manus 内建**——★纠错 2026-06-04:早期标「schedule/chat 是 axon 加」是误认,实证 docs/manus-parity/screenshots/16-mode-more.png 中 Manus「More」浮窗含 Schedule tasks + Chat mode,二者 Manus 本有。Playbook=Manus 第 13 项外链,axon 不抄) - mode 决定**产品形态**而不只是 prompt:`website` mode LLM 第一调用 `propose_website_builder` blueprint 卡片,确认后才走 `apply_webpage_edits` 真改文件 ### Coding Agent Pipeline(M5-M9 — vision 五层,2026-05-24 全落地) diff --git a/axon-launch-deck-2026.pptx b/axon-launch-deck-2026.pptx deleted file mode 100644 index 3dd70140..00000000 Binary files a/axon-launch-deck-2026.pptx and /dev/null differ diff --git a/deploy/epay/Dockerfile b/deploy/epay/Dockerfile deleted file mode 100644 index 40498989..00000000 --- a/deploy/epay/Dockerfile +++ /dev/null @@ -1,45 +0,0 @@ -FROM php:7.4-apache - -# Install PHP extensions required by Epay -RUN apt-get update && apt-get install -y \ - libzip-dev \ - libpng-dev \ - libjpeg-dev \ - libfreetype6-dev \ - libcurl4-openssl-dev \ - unzip \ - git \ - && docker-php-ext-configure gd --with-freetype --with-jpeg \ - && docker-php-ext-install -j$(nproc) \ - pdo_mysql \ - mysqli \ - gd \ - curl \ - zip \ - bcmath \ - && apt-get clean && rm -rf /var/lib/apt/lists/* - -# Enable mod_rewrite (MPM fix done at runtime in start.sh) -RUN a2enmod rewrite - -# Set working directory -WORKDIR /var/www/html - -# Clone Epay source code -RUN git clone --depth 1 https://github.com/maajiko/Epay.git /tmp/epay \ - && cp -r /tmp/epay/* /tmp/epay/.* /var/www/html/ 2>/dev/null || true \ - && rm -rf /tmp/epay - -# Set permissions -RUN chown -R www-data:www-data /var/www/html \ - && chmod -R 755 /var/www/html - -# Apache config for .htaccess support -RUN printf '\n AllowOverride All\n Require all granted\n\n' > /etc/apache2/conf-available/epay.conf \ - && a2enconf epay - -# Copy startup script -COPY start.sh /usr/local/bin/start.sh -RUN chmod +x /usr/local/bin/start.sh - -CMD ["/usr/local/bin/start.sh"] diff --git a/deploy/epay/logs.txt b/deploy/epay/logs.txt deleted file mode 100644 index 64bf5bca..00000000 --- a/deploy/epay/logs.txt +++ /dev/null @@ -1,888 +0,0 @@ -[INFO] checking for cc option to produce PIC... -[INFO] -fPIC - -[INFO] checking if cc PIC flag -fPIC works... -[INFO] yes - -[INFO] checking if cc static flag -static works... -[INFO] yes -checking if cc supports -c -o file.o... -[INFO] yes - -[INFO] checking whether the cc linker (/usr/bin/ld -m elf_x86_64) supports shared libraries... -[INFO] yes - -[INFO] checking whether -lc should be explicitly linked in... -[INFO] no -checking dynamic linker characteristics... -[INFO] GNU/Linux ld.so - -[INFO] checking how to hardcode library paths into programs... -[INFO] immediate - -[INFO] checking whether stripping libraries is possible... -[INFO] yes - -[INFO] checking if libtool supports shared libraries... yes - -[INFO] checking whether to build shared libraries... -[INFO] yes -checking whether to build static libraries... -[INFO] no - -[INFO] -creating libtool - -[INFO] appending configuration tag "CXX" to libtool - -[INFO] configure: patching config.h.in - -[INFO] configure: creating ./config.status - -[INFO] config.status: creating config.h - -[INFO] /bin/bash /usr/src/php/ext/curl/libtool --mode=compile cc -I. -I/usr/src/php/ext/curl -DPHP_ATOM_INC -I/usr/src/php/ext/curl/include -I/usr/src/php/ext/curl/main -I/usr/src/php/ext/curl -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -I/usr/include/x86_64-linux-gnu -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/curl/interface.c -o interface.lo - -[INFO] /bin/bash /usr/src/php/ext/curl/libtool --mode=compile cc -I. -I/usr/src/php/ext/curl -DPHP_ATOM_INC -I/usr/src/php/ext/curl/include -I/usr/src/php/ext/curl/main -I/usr/src/php/ext/curl -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -I/usr/include/x86_64-linux-gnu -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/curl/multi.c -o multi.lo - -[INFO] /bin/bash /usr/src/php/ext/curl/libtool --mode=compile cc -I. -I/usr/src/php/ext/curl -DPHP_ATOM_INC -I/usr/src/php/ext/curl/include -I/usr/src/php/ext/curl/main -I/usr/src/php/ext/curl -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -I/usr/include/x86_64-linux-gnu -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/curl/share.c -o share.lo - -[INFO] /bin/bash /usr/src/php/ext/curl/libtool --mode=compile cc -I. -I/usr/src/php/ext/curl -DPHP_ATOM_INC -I/usr/src/php/ext/curl/include -I/usr/src/php/ext/curl/main -I/usr/src/php/ext/curl -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -I/usr/include/x86_64-linux-gnu -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/curl/curl_file.c -o curl_file.lo - -[INFO] mkdir .libs - -[INFO] mkdir .libs - -[INFO] mkdir .libs - -[INFO] mkdir: cannot create directory '.libs': File exists - -[INFO] mkdir: cannot create directory '.libs': File exists - -[INFO] cc -I. -I/usr/src/php/ext/curl -DPHP_ATOM_INC -I/usr/src/php/ext/curl/include -I/usr/src/php/ext/curl/main -I/usr/src/php/ext/curl -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -I/usr/include/x86_64-linux-gnu -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/curl/multi.c -fPIC -DPIC -o .libs/multi.o - -[INFO] cc -I. -I/usr/src/php/ext/curl -DPHP_ATOM_INC -I/usr/src/php/ext/curl/include -I/usr/src/php/ext/curl/main -I/usr/src/php/ext/curl -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -I/usr/include/x86_64-linux-gnu -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/curl/interface.c -fPIC -DPIC -o .libs/interface.o - -[INFO] cc -I. -I/usr/src/php/ext/curl -DPHP_ATOM_INC -I/usr/src/php/ext/curl/include -I/usr/src/php/ext/curl/main -I/usr/src/php/ext/curl -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -I/usr/include/x86_64-linux-gnu -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/curl/curl_file.c -fPIC -DPIC -o .libs/curl_file.o - -[INFO] cc -I. -I/usr/src/php/ext/curl -DPHP_ATOM_INC -I/usr/src/php/ext/curl/include -I/usr/src/php/ext/curl/main -I/usr/src/php/ext/curl -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -I/usr/include/x86_64-linux-gnu -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/curl/share.c -fPIC -DPIC -o .libs/share.o - -[INFO] /bin/bash /usr/src/php/ext/curl/libtool --mode=link cc -DPHP_ATOM_INC -I/usr/src/php/ext/curl/include -I/usr/src/php/ext/curl/main -I/usr/src/php/ext/curl -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -I/usr/include/x86_64-linux-gnu -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -Wl,-O1 -pie -o curl.la -export-dynamic -avoid-version -prefer-pic -module -rpath /usr/src/php/ext/curl/modules interface.lo multi.lo share.lo curl_file.lo -lcurl - -[INFO] cc -shared .libs/interface.o .libs/multi.o .libs/share.o .libs/curl_file.o -lcurl -Wl,-O1 -Wl,-soname -Wl,curl.so -o .libs/curl.so - -[INFO] creating curl.la - -[INFO] (cd .libs && rm -f curl.la && ln -s ../curl.la curl.la) - -[INFO] /bin/bash /usr/src/php/ext/curl/libtool --mode=install cp ./curl.la /usr/src/php/ext/curl/modules - -[INFO] cp ./.libs/curl.so /usr/src/php/ext/curl/modules/curl.so - -[INFO] cp ./.libs/curl.lai /usr/src/php/ext/curl/modules/curl.la - -[INFO] PATH="$PATH:/sbin" ldconfig -n /usr/src/php/ext/curl/modules - -[INFO] ---------------------------------------------------------------------- - -[INFO] Libraries have been installed in: - /usr/src/php/ext/curl/modules - -If you ever happen to want to link against installed libraries - -[INFO] in a given directory, LIBDIR, you must either use libtool, and -specify the full pathname of the library, or use the `-LLIBDIR' -flag during linking and do at least one of the following: - -[INFO] - add LIBDIR to the `LD_LIBRARY_PATH' environment variable - -[INFO] during execution - -[INFO] - add LIBDIR to the `LD_RUN_PATH' environment variable - -[INFO] during linking - -[INFO] - use the `-Wl,--rpath -Wl,LIBDIR' linker flag - - have your system administrator add LIBDIR to `/etc/ld.so.conf' - -[INFO] - -[INFO] See any operating system documentation about shared libraries for - -[INFO] more information, such as the ld(1) and ld.so(8) manual pages. - -[INFO] ---------------------------------------------------------------------- - -[INFO] - -[INFO] Build complete. - -[INFO] Don't forget to run 'make test'. - -[INFO] - -[INFO] + strip --strip-all modules/curl.so - -[INFO] Installing shared extensions: /usr/local/lib/php/extensions/no-debug-non-zts-20190902/ - -[INFO] -warning: curl (curl.so) is already loaded! - - -[INFO] find . -name \*.gcno -o -name \*.gcda | xargs rm -f - -[INFO] find . -name \*.lo -o -name \*.o | xargs rm -f - -[INFO] find . -name \*.la -o -name \*.a | xargs rm -f - -[INFO] find . -name \*.so | xargs rm -f - -[INFO] find . -name .libs -a -type d|xargs rm -rf - -[INFO] rm -f libphp.la modules/* libs/* - -[INFO] Configuring for: -PHP Api Version: 20190902 -Zend Module Api No: 20190902 -Zend Extension Api No: 320190902 - -[INFO] checking for grep that handles long lines and -e... -[INFO] /bin/grep - -[INFO] checking for egrep... -[INFO] /bin/grep -E - -[INFO] checking for a sed that does not truncate output... -[INFO] /bin/sed - -[INFO] checking for pkg-config... -[INFO] /usr/bin/pkg-config - -[INFO] checking pkg-config is at least version 0.9.0... -[INFO] yes - -[INFO] checking for cc... -[INFO] cc - -[INFO] checking whether the C compiler works... -[INFO] yes -checking for C compiler default output file name... -[INFO] a.out - -[INFO] checking for suffix of executables... -[INFO] - -[INFO] checking whether we are cross compiling... -[INFO] no - -[INFO] checking for suffix of object files... -[INFO] o -checking whether we are using the GNU C compiler... -[INFO] yes -checking whether cc accepts -g... -[INFO] yes - -[INFO] checking for cc option to accept ISO C89... -[INFO] none needed - -[INFO] checking how to run the C preprocessor... -[INFO] cc -E - -[INFO] checking for icc... -[INFO] no - -[INFO] checking for suncc... -[INFO] no - -[INFO] checking for system library directory... -[INFO] lib - -[INFO] checking if compiler supports -R... -[INFO] no - -[INFO] checking if compiler supports -Wl,-rpath,... -[INFO] yes - -[INFO] checking build system type... -[INFO] x86_64-pc-linux-gnu - -[INFO] checking host system type... -[INFO] x86_64-pc-linux-gnu - -[INFO] checking target system type... -[INFO] x86_64-pc-linux-gnu - -[INFO] checking for PHP prefix... /usr/local -checking for PHP includes... -[INFO] -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib - -[INFO] checking for PHP extension directory... -[INFO] /usr/local/lib/php/extensions/no-debug-non-zts-20190902 - -[INFO] checking for PHP installed headers prefix... -[INFO] /usr/local/include/php - -[INFO] checking if debug is enabled... -[INFO] no -checking if zts is enabled... -[INFO] no - -[INFO] checking for gawk... -[INFO] no - -[INFO] checking for nawk... -[INFO] nawk - -[INFO] checking if nawk is broken... -[INFO] no - -[INFO] checking for zip archive read/write support... -[INFO] yes, shared - -[INFO] checking for libzip >= 0.11 libzip != 1.3.1 libzip != 1.7.0... -[INFO] yes - -[INFO] checking for zip_file_set_encryption in -lzip... -[INFO] yes - -[INFO] checking for zip_libzip_version in -lzip... -[INFO] yes - -[INFO] checking for a sed that does not truncate output... -[INFO] /bin/sed - -[INFO] checking for ld used by cc... -[INFO] /usr/bin/ld - -[INFO] checking if the linker (/usr/bin/ld) is GNU ld... -[INFO] yes - -[INFO] checking for /usr/bin/ld option to reload object files... -[INFO] -r - -[INFO] checking for BSD-compatible nm... -[INFO] /usr/bin/nm -B - -[INFO] checking whether ln -s works... -[INFO] yes -checking how to recognize dependent libraries... -[INFO] pass_all - -[INFO] checking for ANSI C header files... -[INFO] yes - -[INFO] checking for sys/types.h... -[INFO] yes - -[INFO] checking for sys/stat.h... -[INFO] yes - -[INFO] checking for stdlib.h... -[INFO] yes - -[INFO] checking for string.h... -[INFO] yes - -[INFO] checking for memory.h... -[INFO] yes - -[INFO] checking for strings.h... -[INFO] yes - -[INFO] checking for inttypes.h... -[INFO] yes - -[INFO] checking for stdint.h... -[INFO] yes - -[INFO] checking for unistd.h... -[INFO] yes - -[INFO] checking dlfcn.h usability... -[INFO] yes -checking dlfcn.h presence... -[INFO] yes -checking for dlfcn.h... -[INFO] yes - -[INFO] checking the maximum length of command line arguments... -[INFO] 1572864 -checking command to parse /usr/bin/nm -B output from cc object... -[INFO] ok -checking for objdir... -[INFO] .libs - -[INFO] checking for ar... -[INFO] ar - -[INFO] checking for ranlib... -[INFO] ranlib - -[INFO] checking for strip... -[INFO] strip - -[INFO] checking if cc supports -fno-rtti -fno-exceptions... -[INFO] no - -[INFO] checking for cc option to produce PIC... -[INFO] -fPIC - -[INFO] checking if cc PIC flag -fPIC works... -[INFO] yes - -[INFO] checking if cc static flag -static works... -[INFO] yes - -[INFO] checking if cc supports -c -o file.o... -[INFO] yes - -[INFO] checking whether the cc linker (/usr/bin/ld -m elf_x86_64) supports shared libraries... -[INFO] yes - -[INFO] checking whether -lc should be explicitly linked in... -[INFO] no - -[INFO] checking dynamic linker characteristics... -[INFO] GNU/Linux ld.so - -[INFO] checking how to hardcode library paths into programs... -[INFO] immediate - -[INFO] checking whether stripping libraries is possible... -[INFO] yes - -[INFO] checking if libtool supports shared libraries... yes - -[INFO] checking whether to build shared libraries... -[INFO] yes - -[INFO] checking whether to build static libraries... -[INFO] no - -[INFO] -creating libtool - -[INFO] appending configuration tag "CXX" to libtool - -[INFO] configure: patching config.h.in - -[INFO] configure: creating ./config.status - -[INFO] config.status: creating config.h - -[INFO] /bin/bash /usr/src/php/ext/zip/libtool --mode=compile cc -I. -I/usr/src/php/ext/zip -DPHP_ATOM_INC -I/usr/src/php/ext/zip/include -I/usr/src/php/ext/zip/main -I/usr/src/php/ext/zip -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/zip/php_zip.c -o php_zip.lo - -[INFO] /bin/bash /usr/src/php/ext/zip/libtool --mode=compile cc -I. -I/usr/src/php/ext/zip -DPHP_ATOM_INC -I/usr/src/php/ext/zip/include -I/usr/src/php/ext/zip/main -I/usr/src/php/ext/zip -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/zip/zip_stream.c -o zip_stream.lo - -[INFO] mkdir .libs - -[INFO] cc -I. -I/usr/src/php/ext/zip -DPHP_ATOM_INC -I/usr/src/php/ext/zip/include -I/usr/src/php/ext/zip/main -I/usr/src/php/ext/zip -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/zip/zip_stream.c -fPIC -DPIC -o .libs/zip_stream.o - -[INFO] cc -I. -I/usr/src/php/ext/zip -DPHP_ATOM_INC -I/usr/src/php/ext/zip/include -I/usr/src/php/ext/zip/main -I/usr/src/php/ext/zip -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/zip/php_zip.c -fPIC -DPIC -o .libs/php_zip.o - -[INFO] /bin/bash /usr/src/php/ext/zip/libtool --mode=link cc -DPHP_ATOM_INC -I/usr/src/php/ext/zip/include -I/usr/src/php/ext/zip/main -I/usr/src/php/ext/zip -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -Wl,-O1 -pie -o zip.la -export-dynamic -avoid-version -prefer-pic -module -rpath /usr/src/php/ext/zip/modules php_zip.lo zip_stream.lo -lzip - -[INFO] cc -shared .libs/php_zip.o .libs/zip_stream.o -lzip -Wl,-O1 -Wl,-soname -Wl,zip.so -o .libs/zip.so - -[INFO] creating zip.la - -[INFO] (cd .libs && rm -f zip.la && ln -s ../zip.la zip.la) - -[INFO] /bin/bash /usr/src/php/ext/zip/libtool --mode=install cp ./zip.la /usr/src/php/ext/zip/modules - -[INFO] cp ./.libs/zip.so /usr/src/php/ext/zip/modules/zip.so - -[INFO] cp ./.libs/zip.lai /usr/src/php/ext/zip/modules/zip.la - -[INFO] PATH="$PATH:/sbin" ldconfig -n /usr/src/php/ext/zip/modules - -[INFO] ---------------------------------------------------------------------- - -[INFO] Libraries have been installed in: - /usr/src/php/ext/zip/modules - -If you ever happen to want to link against installed libraries -in a given directory, LIBDIR, you must either use libtool, and - -[INFO] specify the full pathname of the library, or use the `-LLIBDIR' -flag during linking and do at least one of the following: - -[INFO] - add LIBDIR to the `LD_LIBRARY_PATH' environment variable - during execution - -[INFO] - add LIBDIR to the `LD_RUN_PATH' environment variable - during linking - -[INFO] - use the `-Wl,--rpath -Wl,LIBDIR' linker flag - -[INFO] - have your system administrator add LIBDIR to `/etc/ld.so.conf' - -See any operating system documentation about shared libraries for -more information, such as the ld(1) and ld.so(8) manual pages. - -[INFO] ---------------------------------------------------------------------- - -[INFO] - -[INFO] Build complete. - -[INFO] Don't forget to run 'make test'. - -[INFO] - -[INFO] + strip --strip-all modules/zip.so - -[INFO] Installing shared extensions: /usr/local/lib/php/extensions/no-debug-non-zts-20190902/ - -[INFO] find . -name \*.gcno -o -name \*.gcda | xargs rm -f - -[INFO] find . -name \*.lo -o -name \*.o | xargs rm -f - -[INFO] find . -name \*.la -o -name \*.a | xargs rm -f - -[INFO] find . -name \*.so | xargs rm -f - -[INFO] find . -name .libs -a -type d|xargs rm -rf - -[INFO] rm -f libphp.la modules/* libs/* - -[INFO] Configuring for: -PHP Api Version: 20190902 -Zend Module Api No: 20190902 -Zend Extension Api No: 320190902 - -[INFO] checking for grep that handles long lines and -e... -[INFO] /bin/grep -checking for egrep... -[INFO] /bin/grep -E - -[INFO] checking for a sed that does not truncate output... -[INFO] /bin/sed - -[INFO] checking for pkg-config... -[INFO] /usr/bin/pkg-config - -[INFO] checking pkg-config is at least version 0.9.0... -[INFO] yes - -[INFO] checking for cc... -[INFO] cc - -[INFO] checking whether the C compiler works... -[INFO] yes -checking for C compiler default output file name... a.out - -[INFO] checking for suffix of executables... -[INFO] - -[INFO] checking whether we are cross compiling... -[INFO] no - -[INFO] checking for suffix of object files... -[INFO] o -checking whether we are using the GNU C compiler... -[INFO] yes - -[INFO] checking whether cc accepts -g... -[INFO] yes -checking for cc option to accept ISO C89... -[INFO] none needed - -[INFO] checking how to run the C preprocessor... -[INFO] cc -E - -[INFO] checking for icc... -[INFO] no - -[INFO] checking for suncc... -[INFO] no - -[INFO] checking for system library directory... -[INFO] lib - -[INFO] checking if compiler supports -R... -[INFO] no - -[INFO] checking if compiler supports -Wl,-rpath,... -[INFO] yes - -[INFO] checking build system type... -[INFO] x86_64-pc-linux-gnu - -[INFO] checking host system type... -[INFO] x86_64-pc-linux-gnu - -[INFO] checking target system type... -[INFO] x86_64-pc-linux-gnu - -[INFO] checking for PHP prefix... /usr/local - -[INFO] checking for PHP includes... -[INFO] -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -checking for PHP extension directory... /usr/local/lib/php/extensions/no-debug-non-zts-20190902 - -[INFO] checking for PHP installed headers prefix... -[INFO] /usr/local/include/php - -[INFO] checking if debug is enabled... -[INFO] no -checking if zts is enabled... -[INFO] no - -[INFO] checking for gawk... -[INFO] no -checking for nawk... -[INFO] nawk - -[INFO] checking if nawk is broken... -[INFO] no - -[INFO] checking whether to enable bc style precision math functions... -[INFO] yes, shared - -[INFO] checking for a sed that does not truncate output... -[INFO] /bin/sed - -[INFO] checking for ld used by cc... -[INFO] /usr/bin/ld - -[INFO] checking if the linker (/usr/bin/ld) is GNU ld... -[INFO] yes - -[INFO] checking for /usr/bin/ld option to reload object files... -[INFO] -r - -[INFO] checking for BSD-compatible nm... -[INFO] /usr/bin/nm -B -checking whether ln -s works... -[INFO] yes - -[INFO] checking how to recognize dependent libraries... -[INFO] pass_all - -[INFO] checking for ANSI C header files... -[INFO] yes - -[INFO] checking for sys/types.h... -[INFO] yes - -[INFO] checking for sys/stat.h... -[INFO] yes - -[INFO] checking for stdlib.h... -[INFO] yes - -[INFO] checking for string.h... -[INFO] yes - -[INFO] checking for memory.h... -[INFO] yes - -[INFO] checking for strings.h... -[INFO] yes - -[INFO] checking for inttypes.h... -[INFO] yes - -[INFO] checking for stdint.h... -[INFO] yes - -[INFO] checking for unistd.h... -[INFO] yes - -[INFO] checking dlfcn.h usability... -[INFO] yes -checking dlfcn.h presence... -[INFO] yes -checking for dlfcn.h... -[INFO] yes - -[INFO] checking the maximum length of command line arguments... -[INFO] 1572864 -checking command to parse /usr/bin/nm -B output from cc object... -[INFO] ok -checking for objdir... -[INFO] .libs - -[INFO] checking for ar... -[INFO] ar - -[INFO] checking for ranlib... -[INFO] ranlib - -[INFO] checking for strip... -[INFO] strip - -[INFO] checking if cc supports -fno-rtti -fno-exceptions... -[INFO] no - -[INFO] checking for cc option to produce PIC... -[INFO] -fPIC - -[INFO] checking if cc PIC flag -fPIC works... -[INFO] yes - -[INFO] checking if cc static flag -static works... -[INFO] yes -checking if cc supports -c -o file.o... -[INFO] yes - -[INFO] checking whether the cc linker (/usr/bin/ld -m elf_x86_64) supports shared libraries... -[INFO] yes - -[INFO] checking whether -lc should be explicitly linked in... -[INFO] no - -[INFO] checking dynamic linker characteristics... -[INFO] GNU/Linux ld.so - -[INFO] checking how to hardcode library paths into programs... -[INFO] immediate - -[INFO] checking whether stripping libraries is possible... -[INFO] yes - -[INFO] checking if libtool supports shared libraries... yes - -[INFO] checking whether to build shared libraries... -[INFO] yes - -[INFO] checking whether to build static libraries... -[INFO] no - -[INFO] -creating libtool - -[INFO] appending configuration tag "CXX" to libtool - -[INFO] configure: patching config.h.in - -[INFO] configure: creating ./config.status - -[INFO] config.status: creating config.h - -[INFO] /bin/bash /usr/src/php/ext/bcmath/libtool --mode=compile cc -I/usr/src/php/ext/bcmath/libbcmath/src -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 -I. -I/usr/src/php/ext/bcmath -DPHP_ATOM_INC -I/usr/src/php/ext/bcmath/include -I/usr/src/php/ext/bcmath/main -I/usr/src/php/ext/bcmath -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/bcmath/bcmath.c -o bcmath.lo - -[INFO] /bin/bash /usr/src/php/ext/bcmath/libtool --mode=compile cc -I/usr/src/php/ext/bcmath/libbcmath/src -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 -I. -I/usr/src/php/ext/bcmath -DPHP_ATOM_INC -I/usr/src/php/ext/bcmath/include -I/usr/src/php/ext/bcmath/main -I/usr/src/php/ext/bcmath -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/bcmath/libbcmath/src/add.c -o libbcmath/src/add.lo - -[INFO] /bin/bash /usr/src/php/ext/bcmath/libtool --mode=compile cc -I/usr/src/php/ext/bcmath/libbcmath/src -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 -I. -I/usr/src/php/ext/bcmath -DPHP_ATOM_INC -I/usr/src/php/ext/bcmath/include -I/usr/src/php/ext/bcmath/main -I/usr/src/php/ext/bcmath -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/bcmath/libbcmath/src/div.c -o libbcmath/src/div.lo - -[INFO] /bin/bash /usr/src/php/ext/bcmath/libtool --mode=compile cc -I/usr/src/php/ext/bcmath/libbcmath/src -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 -I. -I/usr/src/php/ext/bcmath -DPHP_ATOM_INC -I/usr/src/php/ext/bcmath/include -I/usr/src/php/ext/bcmath/main -I/usr/src/php/ext/bcmath -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/bcmath/libbcmath/src/init.c -o libbcmath/src/init.lo - -[INFO] /bin/bash /usr/src/php/ext/bcmath/libtool --mode=compile cc -I/usr/src/php/ext/bcmath/libbcmath/src -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 -I. -I/usr/src/php/ext/bcmath -DPHP_ATOM_INC -I/usr/src/php/ext/bcmath/include -I/usr/src/php/ext/bcmath/main -I/usr/src/php/ext/bcmath -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/bcmath/libbcmath/src/neg.c -o libbcmath/src/neg.lo - -[INFO] /bin/bash /usr/src/php/ext/bcmath/libtool --mode=compile cc -I/usr/src/php/ext/bcmath/libbcmath/src -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 -I. -I/usr/src/php/ext/bcmath -DPHP_ATOM_INC -I/usr/src/php/ext/bcmath/include -I/usr/src/php/ext/bcmath/main -I/usr/src/php/ext/bcmath -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/bcmath/libbcmath/src/outofmem.c -o libbcmath/src/outofmem.lo - -[INFO] /bin/bash /usr/src/php/ext/bcmath/libtool --mode=compile cc -I/usr/src/php/ext/bcmath/libbcmath/src -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 -I. -I/usr/src/php/ext/bcmath -DPHP_ATOM_INC -I/usr/src/php/ext/bcmath/include -I/usr/src/php/ext/bcmath/main -I/usr/src/php/ext/bcmath -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/bcmath/libbcmath/src/raisemod.c -o libbcmath/src/raisemod.lo - -[INFO] /bin/bash /usr/src/php/ext/bcmath/libtool --mode=compile cc -I/usr/src/php/ext/bcmath/libbcmath/src -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 -I. -I/usr/src/php/ext/bcmath -DPHP_ATOM_INC -I/usr/src/php/ext/bcmath/include -I/usr/src/php/ext/bcmath/main -I/usr/src/php/ext/bcmath -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/bcmath/libbcmath/src/sub.c -o libbcmath/src/sub.lo - -[INFO] /bin/bash /usr/src/php/ext/bcmath/libtool --mode=compile cc -I/usr/src/php/ext/bcmath/libbcmath/src -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 -I. -I/usr/src/php/ext/bcmath -DPHP_ATOM_INC -I/usr/src/php/ext/bcmath/include -I/usr/src/php/ext/bcmath/main -I/usr/src/php/ext/bcmath -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/bcmath/libbcmath/src/compare.c -o libbcmath/src/compare.lo - -[INFO] /bin/bash /usr/src/php/ext/bcmath/libtool --mode=compile cc -I/usr/src/php/ext/bcmath/libbcmath/src -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 -I. -I/usr/src/php/ext/bcmath -DPHP_ATOM_INC -I/usr/src/php/ext/bcmath/include -I/usr/src/php/ext/bcmath/main -I/usr/src/php/ext/bcmath -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/bcmath/libbcmath/src/divmod.c -o libbcmath/src/divmod.lo - -[INFO] /bin/bash /usr/src/php/ext/bcmath/libtool --mode=compile cc -I/usr/src/php/ext/bcmath/libbcmath/src -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 -I. -I/usr/src/php/ext/bcmath -DPHP_ATOM_INC -I/usr/src/php/ext/bcmath/include -I/usr/src/php/ext/bcmath/main -I/usr/src/php/ext/bcmath -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/bcmath/libbcmath/src/int2num.c -o libbcmath/src/int2num.lo - -[INFO] /bin/bash /usr/src/php/ext/bcmath/libtool --mode=compile cc -I/usr/src/php/ext/bcmath/libbcmath/src -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 -I. -I/usr/src/php/ext/bcmath -DPHP_ATOM_INC -I/usr/src/php/ext/bcmath/include -I/usr/src/php/ext/bcmath/main -I/usr/src/php/ext/bcmath -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/bcmath/libbcmath/src/num2long.c -o libbcmath/src/num2long.lo - -[INFO] /bin/bash /usr/src/php/ext/bcmath/libtool --mode=compile cc -I/usr/src/php/ext/bcmath/libbcmath/src -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 -I. -I/usr/src/php/ext/bcmath -DPHP_ATOM_INC -I/usr/src/php/ext/bcmath/include -I/usr/src/php/ext/bcmath/main -I/usr/src/php/ext/bcmath -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/bcmath/libbcmath/src/output.c -o libbcmath/src/output.lo - -[INFO] /bin/bash /usr/src/php/ext/bcmath/libtool --mode=compile cc -I/usr/src/php/ext/bcmath/libbcmath/src -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 -I. -I/usr/src/php/ext/bcmath -DPHP_ATOM_INC -I/usr/src/php/ext/bcmath/include -I/usr/src/php/ext/bcmath/main -I/usr/src/php/ext/bcmath -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/bcmath/libbcmath/src/recmul.c -o libbcmath/src/recmul.lo - -[INFO] /bin/bash /usr/src/php/ext/bcmath/libtool --mode=compile cc -I/usr/src/php/ext/bcmath/libbcmath/src -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 -I. -I/usr/src/php/ext/bcmath -DPHP_ATOM_INC -I/usr/src/php/ext/bcmath/include -I/usr/src/php/ext/bcmath/main -I/usr/src/php/ext/bcmath -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/bcmath/libbcmath/src/sqrt.c -o libbcmath/src/sqrt.lo - -[INFO] /bin/bash /usr/src/php/ext/bcmath/libtool --mode=compile cc -I/usr/src/php/ext/bcmath/libbcmath/src -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 -I. -I/usr/src/php/ext/bcmath -DPHP_ATOM_INC -I/usr/src/php/ext/bcmath/include -I/usr/src/php/ext/bcmath/main -I/usr/src/php/ext/bcmath -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/bcmath/libbcmath/src/zero.c -o libbcmath/src/zero.lo - -[INFO] /bin/bash /usr/src/php/ext/bcmath/libtool --mode=compile cc -I/usr/src/php/ext/bcmath/libbcmath/src -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 -I. -I/usr/src/php/ext/bcmath -DPHP_ATOM_INC -I/usr/src/php/ext/bcmath/include -I/usr/src/php/ext/bcmath/main -I/usr/src/php/ext/bcmath -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/bcmath/libbcmath/src/debug.c -o libbcmath/src/debug.lo - -[INFO] /bin/bash /usr/src/php/ext/bcmath/libtool --mode=compile cc -I/usr/src/php/ext/bcmath/libbcmath/src -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 -I. -I/usr/src/php/ext/bcmath -DPHP_ATOM_INC -I/usr/src/php/ext/bcmath/include -I/usr/src/php/ext/bcmath/main -I/usr/src/php/ext/bcmath -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/bcmath/libbcmath/src/doaddsub.c -o libbcmath/src/doaddsub.lo - -[INFO] /bin/bash /usr/src/php/ext/bcmath/libtool --mode=compile cc -I/usr/src/php/ext/bcmath/libbcmath/src -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 -I. -I/usr/src/php/ext/bcmath -DPHP_ATOM_INC -I/usr/src/php/ext/bcmath/include -I/usr/src/php/ext/bcmath/main -I/usr/src/php/ext/bcmath -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/bcmath/libbcmath/src/nearzero.c -o libbcmath/src/nearzero.lo - -[INFO] /bin/bash /usr/src/php/ext/bcmath/libtool --mode=compile cc -I/usr/src/php/ext/bcmath/libbcmath/src -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 -I. -I/usr/src/php/ext/bcmath -DPHP_ATOM_INC -I/usr/src/php/ext/bcmath/include -I/usr/src/php/ext/bcmath/main -I/usr/src/php/ext/bcmath -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/bcmath/libbcmath/src/num2str.c -o libbcmath/src/num2str.lo - -[INFO] /bin/bash /usr/src/php/ext/bcmath/libtool --mode=compile cc -I/usr/src/php/ext/bcmath/libbcmath/src -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 -I. -I/usr/src/php/ext/bcmath -DPHP_ATOM_INC -I/usr/src/php/ext/bcmath/include -I/usr/src/php/ext/bcmath/main -I/usr/src/php/ext/bcmath -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/bcmath/libbcmath/src/raise.c -o libbcmath/src/raise.lo - -[INFO] /bin/bash /usr/src/php/ext/bcmath/libtool --mode=compile cc -I/usr/src/php/ext/bcmath/libbcmath/src -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 -I. -I/usr/src/php/ext/bcmath -DPHP_ATOM_INC -I/usr/src/php/ext/bcmath/include -I/usr/src/php/ext/bcmath/main -I/usr/src/php/ext/bcmath -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/bcmath/libbcmath/src/rmzero.c -o libbcmath/src/rmzero.lo - -[INFO] /bin/bash /usr/src/php/ext/bcmath/libtool --mode=compile cc -I/usr/src/php/ext/bcmath/libbcmath/src -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 -I. -I/usr/src/php/ext/bcmath -DPHP_ATOM_INC -I/usr/src/php/ext/bcmath/include -I/usr/src/php/ext/bcmath/main -I/usr/src/php/ext/bcmath -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/bcmath/libbcmath/src/str2num.c -o libbcmath/src/str2num.lo - -[INFO] mkdir libbcmath/src/.libs -mkdir libbcmath/src/.libs - -[INFO] mkdir libbcmath/src/.libs - -[INFO] mkdir libbcmath/src/.libs - -[INFO] mkdir libbcmath/src/.libs - -[INFO] mkdir libbcmath/src/.libs - -[INFO] mkdir: -[INFO] cannot create directory 'libbcmath/src/.libs' -[INFO] : File exists -mkdir: cannot create directory 'libbcmath/src/.libs': File exists - -[INFO] mkdir: cannot create directory 'libbcmath/src/.libs': File exists - -[INFO] mkdir: cannot create directory 'libbcmath/src/.libs': File exists - -[INFO] mkdir: cannot create directory 'libbcmath/src/.libs': File exists - -[INFO] cc -I/usr/src/php/ext/bcmath/libbcmath/src -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 -I. -I/usr/src/php/ext/bcmath -DPHP_ATOM_INC -I/usr/src/php/ext/bcmath/include -I/usr/src/php/ext/bcmath/main -I/usr/src/php/ext/bcmath -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/bcmath/libbcmath/src/sqrt.c -fPIC -DPIC -o libbcmath/src/.libs/sqrt.o - cc -I/usr/src/php/ext/bcmath/libbcmath/src -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 -I. -I/usr/src/php/ext/bcmath -DPHP_ATOM_INC -I/usr/src/php/ext/bcmath/include -I/usr/src/php/ext/bcmath/main -I/usr/src/php/ext/bcmath -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/bcmath/libbcmath/src/num2long.c -fPIC -DPIC -o libbcmath/src/.libs/num2long.o - cc -I/usr/src/php/ext/bcmath/libbcmath/src -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 -I. -I/usr/src/php/ext/bcmath -DPHP_ATOM_INC -I/usr/src/php/ext/bcmath/include -I/usr/src/php/ext/bcmath/main -I/usr/src/php/ext/bcmath -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/bcmath/libbcmath/src/doaddsub.c -fPIC -DPIC -o libbcmath/src/.libs/doaddsub.o - cc -I/usr/src/php/ext/bcmath/libbcmath/src -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 -I. -I/usr/src/php/ext/bcmath -DPHP_ATOM_INC -I/usr/src/php/ext/bcmath/include -I/usr/src/php/ext/bcmath/main -I/usr/src/php/ext/bcmath -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/bcmath/libbcmath/src/recmul.c -fPIC -DPIC -o libbcmath/src/.libs/recmul.o - cc -I/usr/src/php/ext/bcmath/libbcmath/src -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 -I. -I/usr/src/php/ext/bcmath -DPHP_ATOM_INC -I/usr/src/php/ext/bcmath/include -I/usr/src/php/ext/bcmath/main -I/usr/src/php/ext/bcmath -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/bcmath/libbcmath/src/add.c -fPIC -DPIC -o libbcmath/src/.libs/add.o - -[INFO] cc -I/usr/src/php/ext/bcmath/libbcmath/src -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 -I. -I/usr/src/php/ext/bcmath -DPHP_ATOM_INC -I/usr/src/php/ext/bcmath/include -I/usr/src/php/ext/bcmath/main -I/usr/src/php/ext/bcmath -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/bcmath/libbcmath/src/div.c -fPIC -DPIC -o libbcmath/src/.libs/div.o - -[INFO] cc -I/usr/src/php/ext/bcmath/libbcmath/src -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 -I. -I/usr/src/php/ext/bcmath -DPHP_ATOM_INC -I/usr/src/php/ext/bcmath/include -I/usr/src/php/ext/bcmath/main -I/usr/src/php/ext/bcmath -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/bcmath/libbcmath/src/outofmem.c -fPIC -DPIC -o libbcmath/src/.libs/outofmem.o - cc -I/usr/src/php/ext/bcmath/libbcmath/src -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 -I. -I/usr/src/php/ext/bcmath -DPHP_ATOM_INC -I/usr/src/php/ext/bcmath/include -I/usr/src/php/ext/bcmath/main -I/usr/src/php/ext/bcmath -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/bcmath/libbcmath/src/init.c -fPIC -DPIC -o libbcmath/src/.libs/init.o - -[INFO] mkdir .libs - -[INFO] cc -I/usr/src/php/ext/bcmath/libbcmath/src -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 -I. -I/usr/src/php/ext/bcmath -DPHP_ATOM_INC -I/usr/src/php/ext/bcmath/include -I/usr/src/php/ext/bcmath/main -I/usr/src/php/ext/bcmath -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/bcmath/libbcmath/src/neg.c -fPIC -DPIC -o libbcmath/src/.libs/neg.o - -[INFO] cc -I/usr/src/php/ext/bcmath/libbcmath/src -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 -I. -I/usr/src/php/ext/bcmath -DPHP_ATOM_INC -I/usr/src/php/ext/bcmath/include -I/usr/src/php/ext/bcmath/main -I/usr/src/php/ext/bcmath -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/bcmath/libbcmath/src/raisemod.c -fPIC -DPIC -o libbcmath/src/.libs/raisemod.o - -[INFO] cc -I/usr/src/php/ext/bcmath/libbcmath/src -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 -I. -I/usr/src/php/ext/bcmath -DPHP_ATOM_INC -I/usr/src/php/ext/bcmath/include -I/usr/src/php/ext/bcmath/main -I/usr/src/php/ext/bcmath -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/bcmath/libbcmath/src/output.c -fPIC -DPIC -o libbcmath/src/.libs/output.o - -[INFO] cc -I/usr/src/php/ext/bcmath/libbcmath/src -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 -I. -I/usr/src/php/ext/bcmath -DPHP_ATOM_INC -I/usr/src/php/ext/bcmath/include -I/usr/src/php/ext/bcmath/main -I/usr/src/php/ext/bcmath -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/bcmath/libbcmath/src/compare.c -fPIC -DPIC -o libbcmath/src/.libs/compare.o - -[INFO] cc -I/usr/src/php/ext/bcmath/libbcmath/src -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 -I. -I/usr/src/php/ext/bcmath -DPHP_ATOM_INC -I/usr/src/php/ext/bcmath/include -I/usr/src/php/ext/bcmath/main -I/usr/src/php/ext/bcmath -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/bcmath/bcmath.c -fPIC -DPIC -o .libs/bcmath.o - -[INFO] cc -I/usr/src/php/ext/bcmath/libbcmath/src -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 -I. -I/usr/src/php/ext/bcmath -DPHP_ATOM_INC -I/usr/src/php/ext/bcmath/include -I/usr/src/php/ext/bcmath/main -I/usr/src/php/ext/bcmath -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/bcmath/libbcmath/src/str2num.c -fPIC -DPIC -o libbcmath/src/.libs/str2num.o - cc -I/usr/src/php/ext/bcmath/libbcmath/src -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 -I. -I/usr/src/php/ext/bcmath -DPHP_ATOM_INC -I/usr/src/php/ext/bcmath/include -I/usr/src/php/ext/bcmath/main -I/usr/src/php/ext/bcmath -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/bcmath/libbcmath/src/rmzero.c -fPIC -DPIC -o libbcmath/src/.libs/rmzero.o - -[INFO] cc -I/usr/src/php/ext/bcmath/libbcmath/src -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 -I. -I/usr/src/php/ext/bcmath -DPHP_ATOM_INC -I/usr/src/php/ext/bcmath/include -I/usr/src/php/ext/bcmath/main -I/usr/src/php/ext/bcmath -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/bcmath/libbcmath/src/debug.c -fPIC -DPIC -o libbcmath/src/.libs/debug.o - cc -I/usr/src/php/ext/bcmath/libbcmath/src -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 -I. -I/usr/src/php/ext/bcmath -DPHP_ATOM_INC -I/usr/src/php/ext/bcmath/include -I/usr/src/php/ext/bcmath/main -I/usr/src/php/ext/bcmath -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/bcmath/libbcmath/src/int2num.c -fPIC -DPIC -o libbcmath/src/.libs/int2num.o - -[INFO] cc -I/usr/src/php/ext/bcmath/libbcmath/src -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 -I. -I/usr/src/php/ext/bcmath -DPHP_ATOM_INC -I/usr/src/php/ext/bcmath/include -I/usr/src/php/ext/bcmath/main -I/usr/src/php/ext/bcmath -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/bcmath/libbcmath/src/nearzero.c -fPIC -DPIC -o libbcmath/src/.libs/nearzero.o - -[INFO] cc -I/usr/src/php/ext/bcmath/libbcmath/src -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 -I. -I/usr/src/php/ext/bcmath -DPHP_ATOM_INC -I/usr/src/php/ext/bcmath/include -I/usr/src/php/ext/bcmath/main -I/usr/src/php/ext/bcmath -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/bcmath/libbcmath/src/num2str.c -fPIC -DPIC -o libbcmath/src/.libs/num2str.o - -[INFO] cc -I/usr/src/php/ext/bcmath/libbcmath/src -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 -I. -I/usr/src/php/ext/bcmath -DPHP_ATOM_INC -I/usr/src/php/ext/bcmath/include -I/usr/src/php/ext/bcmath/main -I/usr/src/php/ext/bcmath -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/bcmath/libbcmath/src/zero.c -fPIC -DPIC -o libbcmath/src/.libs/zero.o - -[INFO] cc -I/usr/src/php/ext/bcmath/libbcmath/src -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 -I. -I/usr/src/php/ext/bcmath -DPHP_ATOM_INC -I/usr/src/php/ext/bcmath/include -I/usr/src/php/ext/bcmath/main -I/usr/src/php/ext/bcmath -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/bcmath/libbcmath/src/divmod.c -fPIC -DPIC -o libbcmath/src/.libs/divmod.o - -[INFO] cc -I/usr/src/php/ext/bcmath/libbcmath/src -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 -I. -I/usr/src/php/ext/bcmath -DPHP_ATOM_INC -I/usr/src/php/ext/bcmath/include -I/usr/src/php/ext/bcmath/main -I/usr/src/php/ext/bcmath -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/bcmath/libbcmath/src/sub.c -fPIC -DPIC -o libbcmath/src/.libs/sub.o - cc -I/usr/src/php/ext/bcmath/libbcmath/src -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 -I. -I/usr/src/php/ext/bcmath -DPHP_ATOM_INC -I/usr/src/php/ext/bcmath/include -I/usr/src/php/ext/bcmath/main -I/usr/src/php/ext/bcmath -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -c /usr/src/php/ext/bcmath/libbcmath/src/raise.c -fPIC -DPIC -o libbcmath/src/.libs/raise.o - -[INFO] /bin/bash /usr/src/php/ext/bcmath/libtool --mode=link cc -DPHP_ATOM_INC -I/usr/src/php/ext/bcmath/include -I/usr/src/php/ext/bcmath/main -I/usr/src/php/ext/bcmath -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DHAVE_CONFIG_H -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -Wl,-O1 -pie -o bcmath.la -export-dynamic -avoid-version -prefer-pic -module -rpath /usr/src/php/ext/bcmath/modules bcmath.lo libbcmath/src/add.lo libbcmath/src/div.lo libbcmath/src/init.lo libbcmath/src/neg.lo libbcmath/src/outofmem.lo libbcmath/src/raisemod.lo libbcmath/src/sub.lo libbcmath/src/compare.lo libbcmath/src/divmod.lo libbcmath/src/int2num.lo libbcmath/src/num2long.lo libbcmath/src/output.lo libbcmath/src/recmul.lo libbcmath/src/sqrt.lo libbcmath/src/zero.lo libbcmath/src/debug.lo libbcmath/src/doaddsub.lo libbcmath/src/nearzero.lo libbcmath/src/num2str.lo libbcmath/src/raise.lo libbcmath/src/rmzero.lo libbcmath/src/str2num.lo - -[INFO] cc -shared .libs/bcmath.o libbcmath/src/.libs/add.o libbcmath/src/.libs/div.o libbcmath/src/.libs/init.o libbcmath/src/.libs/neg.o libbcmath/src/.libs/outofmem.o libbcmath/src/.libs/raisemod.o libbcmath/src/.libs/sub.o libbcmath/src/.libs/compare.o libbcmath/src/.libs/divmod.o libbcmath/src/.libs/int2num.o libbcmath/src/.libs/num2long.o libbcmath/src/.libs/output.o libbcmath/src/.libs/recmul.o libbcmath/src/.libs/sqrt.o libbcmath/src/.libs/zero.o libbcmath/src/.libs/debug.o libbcmath/src/.libs/doaddsub.o libbcmath/src/.libs/nearzero.o libbcmath/src/.libs/num2str.o libbcmath/src/.libs/raise.o libbcmath/src/.libs/rmzero.o libbcmath/src/.libs/str2num.o -Wl,-O1 -Wl,-soname -Wl,bcmath.so -o .libs/bcmath.so - -[INFO] creating bcmath.la - -[INFO] (cd .libs && rm -f bcmath.la && ln -s ../bcmath.la bcmath.la) - -[INFO] /bin/bash /usr/src/php/ext/bcmath/libtool --mode=install cp ./bcmath.la /usr/src/php/ext/bcmath/modules - -[INFO] cp ./.libs/bcmath.so /usr/src/php/ext/bcmath/modules/bcmath.so - -[INFO] cp ./.libs/bcmath.lai /usr/src/php/ext/bcmath/modules/bcmath.la - -[INFO] PATH="$PATH:/sbin" ldconfig -n /usr/src/php/ext/bcmath/modules - -[INFO] ---------------------------------------------------------------------- - -[INFO] Libraries have been installed in: - /usr/src/php/ext/bcmath/modules - -If you ever happen to want to link against installed libraries - -[INFO] in a given directory, LIBDIR, you must either use libtool, and -specify the full pathname of the library, or use the `-LLIBDIR' -flag during linking and do at least one of the following: - -[INFO] - add LIBDIR to the `LD_LIBRARY_PATH' environment variable - during execution - - add LIBDIR to the `LD_RUN_PATH' environment variable - during linking - -[INFO] - use the `-Wl,--rpath -Wl,LIBDIR' linker flag - -[INFO] - have your system administrator add LIBDIR to `/etc/ld.so.conf' - - -[INFO] See any operating system documentation about shared libraries for - -[INFO] more information, such as the ld(1) and ld.so(8) manual pages. - -[INFO] ---------------------------------------------------------------------- - -[INFO] - -[INFO] Build complete. - -[INFO] Don't forget to run 'make test'. - -[INFO] - -[INFO] + strip --strip-all modules/bcmath.so - -[INFO] Installing shared extensions: /usr/local/lib/php/extensions/no-debug-non-zts-20190902/ - -[INFO] find . -name \*.gcno -o -name \*.gcda | xargs rm -f - -[INFO] find . -name \*.lo -o -name \*.o | xargs rm -f - -[INFO] find . -name \*.la -o -name \*.a | xargs rm -f - -[INFO] find . -name \*.so | xargs rm -f - -[INFO] find . -name .libs -a -type d|xargs rm -rf - -[INFO] rm -f libphp.la modules/* libs/* - -[INFO] [2/8] RUN apt-get update && apt-get install -y libzip-dev libpng-dev libjpeg-dev libfreetype6-dev libcurl4-openssl-dev unzip git && docker-php-ext-configure gd --with-freetype --with-jpeg && docker-php-ext-install -j$(nproc) pdo_mysql mysqli gd curl zip bcmath && apt-get clean && rm -rf /var/lib/apt/lists/* -[INFO] [3/8] RUN a2enmod rewrite -[INFO] Enabling module rewrite. - -[INFO] To activate the new configuration, you need to run: - service apache2 restart - -[INFO] [3/8] RUN a2enmod rewrite -[INFO] [4/8] RUN sed -i 's/80/${PORT}/g' /etc/apache2/sites-available/000-default.conf /etc/apache2/ports.conf -[INFO] [4/8] RUN sed -i 's/80/${PORT}/g' /etc/apache2/sites-available/000-default.conf /etc/apache2/ports.conf -[INFO] [5/8] WORKDIR /var/www/html -[INFO] [5/8] WORKDIR /var/www/html -[INFO] [6/8] RUN git clone --depth 1 https://github.com/maajiko/Epay.git /tmp/epay && cp -r /tmp/epay/* /tmp/epay/.* /var/www/html/ 2>/dev/null || true && rm -rf /tmp/epay -[INFO] Cloning into '/tmp/epay'... - -[INFO] [6/8] RUN git clone --depth 1 https://github.com/maajiko/Epay.git /tmp/epay && cp -r /tmp/epay/* /tmp/epay/.* /var/www/html/ 2>/dev/null || true && rm -rf /tmp/epay -[INFO] [7/8] RUN chown -R www-data:www-data /var/www/html && chmod -R 755 /var/www/html -[INFO] [7/8] RUN chown -R www-data:www-data /var/www/html && chmod -R 755 /var/www/html -[INFO] [8/8] RUN echo '\n AllowOverride All\n Require all granted\n' > /etc/apache2/conf-available/epay.conf && a2enconf epay -[INFO] Enabling conf epay. - -[INFO] To activate the new configuration, you need to run: - service apache2 reload - -[INFO] [8/8] RUN echo '\n AllowOverride All\n Require all granted\n' > /etc/apache2/conf-available/epay.conf && a2enconf epay -[INFO] [auth] sharing credentials for production-us-west2.railway-registry.com -[INFO] [auth] sharing credentials for production-us-west2.railway-registry.com -Build time: 72.78 seconds - -==================== -Starting Healthcheck -==================== - -Path: / -Retry window: 5m0s - -Attempt #1 failed with service unavailable. Continuing to retry for 4m49s -Attempt #2 failed with service unavailable. Continuing to retry for 4m38s -Attempt #3 failed with service unavailable. Continuing to retry for 4m26s -Attempt #4 failed with service unavailable. Continuing to retry for 4m12s -Attempt #5 failed with service unavailable. Continuing to retry for 3m54s -Attempt #6 failed with service unavailable. Continuing to retry for 3m28s -Attempt #7 failed with service unavailable. Continuing to retry for 2m48s -Attempt #8 failed with service unavailable. Continuing to retry for 2m8s -Attempt #9 failed with service unavailable. Continuing to retry for 1m28s -Attempt #10 failed with service unavailable. Continuing to retry for 48s -Attempt #11 failed with service unavailable. Continuing to retry for 8s - -1/1 replicas never became healthy! -Healthcheck failed! diff --git a/deploy/epay/logs2.txt b/deploy/epay/logs2.txt deleted file mode 100644 index e5e355e6..00000000 --- a/deploy/epay/logs2.txt +++ /dev/null @@ -1,74 +0,0 @@ - -[Region: us-west1] -========================= -Using Detected Dockerfile -========================= - -context: 130v-N7A5 -[INFO] [internal] load build definition from Dockerfile -[INFO] [internal] load build definition from Dockerfile -[INFO] [internal] load build definition from Dockerfile -[INFO] [internal] load build definition from Dockerfile -[INFO] [internal] load build definition from Dockerfile -[INFO] [internal] load metadata for docker.io/library/php:7.4-apache -[INFO] [auth] library/php:pull token for registry-1.docker.io -[INFO] [auth] library/php:pull token for registry-1.docker.io -[INFO] [internal] load metadata for docker.io/library/php:7.4-apache -[INFO] [internal] load .dockerignore -[INFO] [internal] load .dockerignore -[INFO] [internal] load .dockerignore -[INFO] [internal] load .dockerignore -[INFO] [8/8] RUN echo '#!/bin/bash\nPORT=${PORT:-80}\nsed -i "s/Listen 80/Listen ${PORT}/" /etc/apache2/ports.conf\nsed -i "s/:80/:${PORT}/" /etc/apache2/sites-available/000-default.conf\nexec apache2-foreground' > /usr/local/bin/start.sh && chmod +x /usr/local/bin/start.sh -[INFO] [7/8] RUN echo '\n AllowOverride All\n Require all granted\n' > /etc/apache2/conf-available/epay.conf && a2enconf epay -[INFO] [6/8] RUN chown -R www-data:www-data /var/www/html && chmod -R 755 /var/www/html -[INFO] [5/8] RUN git clone --depth 1 https://github.com/maajiko/Epay.git /tmp/epay && cp -r /tmp/epay/* /tmp/epay/.* /var/www/html/ 2>/dev/null || true && rm -rf /tmp/epay -[INFO] [4/8] WORKDIR /var/www/html -[INFO] [3/8] RUN a2enmod rewrite -[INFO] [2/8] RUN apt-get update && apt-get install -y libzip-dev libpng-dev libjpeg-dev libfreetype6-dev libcurl4-openssl-dev unzip git && docker-php-ext-configure gd --with-freetype --with-jpeg && docker-php-ext-install -j$(nproc) pdo_mysql mysqli gd curl zip bcmath && apt-get clean && rm -rf /var/lib/apt/lists/* -[INFO] [1/8] FROM docker.io/library/php:7.4-apache@sha256:c9d7e608f73832673479770d66aacc8100011ec751d1905ff63fae3fe2e0ca6d -[INFO] [1/8] FROM docker.io/library/php:7.4-apache@sha256:c9d7e608f73832673479770d66aacc8100011ec751d1905ff63fae3fe2e0ca6d -[INFO] [1/8] FROM docker.io/library/php:7.4-apache@sha256:c9d7e608f73832673479770d66aacc8100011ec751d1905ff63fae3fe2e0ca6d -[INFO] [2/8] RUN apt-get update && apt-get install -y libzip-dev libpng-dev libjpeg-dev libfreetype6-dev libcurl4-openssl-dev unzip git && docker-php-ext-configure gd --with-freetype --with-jpeg && docker-php-ext-install -j$(nproc) pdo_mysql mysqli gd curl zip bcmath && apt-get clean && rm -rf /var/lib/apt/lists/* -[INFO] [3/8] RUN a2enmod rewrite -[INFO] [4/8] WORKDIR /var/www/html -[INFO] [4/8] WORKDIR /var/www/html -[INFO] [5/8] RUN git clone --depth 1 https://github.com/maajiko/Epay.git /tmp/epay && cp -r /tmp/epay/* /tmp/epay/.* /var/www/html/ 2>/dev/null || true && rm -rf /tmp/epay -[INFO] Cloning into '/tmp/epay'... - -[INFO] [5/8] RUN git clone --depth 1 https://github.com/maajiko/Epay.git /tmp/epay && cp -r /tmp/epay/* /tmp/epay/.* /var/www/html/ 2>/dev/null || true && rm -rf /tmp/epay -[INFO] [6/8] RUN chown -R www-data:www-data /var/www/html && chmod -R 755 /var/www/html -[INFO] [6/8] RUN chown -R www-data:www-data /var/www/html && chmod -R 755 /var/www/html -[INFO] [7/8] RUN echo '\n AllowOverride All\n Require all granted\n' > /etc/apache2/conf-available/epay.conf && a2enconf epay -[INFO] Enabling conf epay. - -[INFO] To activate the new configuration, you need to run: - service apache2 reload - -[INFO] [7/8] RUN echo '\n AllowOverride All\n Require all granted\n' > /etc/apache2/conf-available/epay.conf && a2enconf epay -[INFO] [8/8] RUN echo '#!/bin/bash\nPORT=${PORT:-80}\nsed -i "s/Listen 80/Listen ${PORT}/" /etc/apache2/ports.conf\nsed -i "s/:80/:${PORT}/" /etc/apache2/sites-available/000-default.conf\nexec apache2-foreground' > /usr/local/bin/start.sh && chmod +x /usr/local/bin/start.sh -[INFO] [8/8] RUN echo '#!/bin/bash\nPORT=${PORT:-80}\nsed -i "s/Listen 80/Listen ${PORT}/" /etc/apache2/ports.conf\nsed -i "s/:80/:${PORT}/" /etc/apache2/sites-available/000-default.conf\nexec apache2-foreground' > /usr/local/bin/start.sh && chmod +x /usr/local/bin/start.sh -[INFO] [auth] sharing credentials for production-us-west2.railway-registry.com -[INFO] [auth] sharing credentials for production-us-west2.railway-registry.com -Build time: 26.56 seconds - -==================== -Starting Healthcheck -==================== - -Path: / -Retry window: 5m0s - -Attempt #1 failed with service unavailable. Continuing to retry for 4m49s -Attempt #2 failed with service unavailable. Continuing to retry for 4m38s -Attempt #3 failed with service unavailable. Continuing to retry for 4m26s -Attempt #4 failed with service unavailable. Continuing to retry for 4m12s -Attempt #5 failed with service unavailable. Continuing to retry for 3m54s -Attempt #6 failed with service unavailable. Continuing to retry for 3m28s -Attempt #7 failed with service unavailable. Continuing to retry for 2m48s -Attempt #8 failed with service unavailable. Continuing to retry for 2m8s -Attempt #9 failed with service unavailable. Continuing to retry for 1m28s -Attempt #10 failed with service unavailable. Continuing to retry for 48s -Attempt #11 failed with service unavailable. Continuing to retry for 8s - -1/1 replicas never became healthy! -Healthcheck failed! diff --git a/deploy/epay/logs3.txt b/deploy/epay/logs3.txt deleted file mode 100644 index c4c35dbb..00000000 --- a/deploy/epay/logs3.txt +++ /dev/null @@ -1,5 +0,0 @@ -Starting Container -AH00534: apache2: Configuration error: More than one MPM loaded. -AH00534: apache2: Configuration error: More than one MPM loaded. -AH00534: apache2: Configuration error: More than one MPM loaded. -AH00534: apache2: Configuration error: More than one MPM loaded. diff --git a/deploy/epay/logs4.txt b/deploy/epay/logs4.txt deleted file mode 100644 index 0f930edf..00000000 --- a/deploy/epay/logs4.txt +++ /dev/null @@ -1,5 +0,0 @@ -AH00534: apache2: Configuration error: More than one MPM loaded. -AH00534: apache2: Configuration error: More than one MPM loaded. -Starting Container -AH00534: apache2: Configuration error: More than one MPM loaded. -AH00534: apache2: Configuration error: More than one MPM loaded. diff --git a/deploy/epay/railway.toml b/deploy/epay/railway.toml deleted file mode 100644 index e2700859..00000000 --- a/deploy/epay/railway.toml +++ /dev/null @@ -1,6 +0,0 @@ -[build] -dockerfilePath = "Dockerfile" - -[deploy] -restartPolicyType = "ON_FAILURE" -restartPolicyMaxRetries = 3 diff --git a/deploy/epay/start.sh b/deploy/epay/start.sh deleted file mode 100644 index 7778eedd..00000000 --- a/deploy/epay/start.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash -set -e - -PORT=${PORT:-80} - -# Update Apache port configuration -sed -i "s/Listen 80/Listen ${PORT}/g" /etc/apache2/ports.conf -sed -i "s///g" /etc/apache2/sites-available/000-default.conf - -# Remove all MPM load configs, then only load prefork -rm -f /etc/apache2/mods-enabled/mpm_*.conf /etc/apache2/mods-enabled/mpm_*.load -ln -sf /etc/apache2/mods-available/mpm_prefork.conf /etc/apache2/mods-enabled/mpm_prefork.conf -ln -sf /etc/apache2/mods-available/mpm_prefork.load /etc/apache2/mods-enabled/mpm_prefork.load - -exec apache2-foreground diff --git a/docs/blog/swarm_mode/article.md b/docs/blog/swarm_mode/article.md deleted file mode 100644 index 3f54b706..00000000 --- a/docs/blog/swarm_mode/article.md +++ /dev/null @@ -1,81 +0,0 @@ -# Claude Code 上了 Dynamic Workflows,我们叫它蜂群 - - - - - -5 月 28 日 Claude Code 发了 Dynamic Workflows。我看完公告,第一反应不是「又出新功能了」,而是「这不就是我们叫蜂群的那套吗」。 - -简单说,它干的事是:把一个大任务拆开,多个子 agent 并行跑,跑完有人专门挑刺,最后汇总成一个结论。我们做 axon 时早就在这么干,只是名字不一样。 - ---- - - - -举个例子。你要审计一个开源仓库,里面有 7 个业务模块,每个模块都有自己的 spec 和真实测试。让单个 agent 从头读到尾,读到第三个模块就开始忘第一个模块的结论。最后它跟你说「基本完成」,你不敢信。 - -更靠谱的做法是:7 路并行审计,每路只盯一个模块;第一轮结束后,再派 7 个专门找茬的 agent,问「真的完成了吗?有没有 stub?测试是不是假的?」主 agent 收齐 14 份结果,再写最终报告。 - -axon 是开源 AI 编程助手,GitHub 地址:https://github.com/kill136/axon -自己 clone 下来就能跑,不用等邀请码。 - -## 一个人扛不动大仓库 - -问题不在 token 数字。200K 窗口也会满,关键是前面读过的东西,后面用不上。 - -大任务串行做也等不起。7 个模块一个一个查,光读代码就要好几轮。更麻烦的是没人复核:它自己读完、自己说查完了,漏掉的问题你事后才发现。 - - - -Claude Code 的 workflow 和 axon 的蜂群,干的是同一件事:拆任务、并行、复核。名字不同,拆法类似。 - -Claude 那边,你在 prompt 里提到 workflow,它会写一段编排脚本,子 agent 并行跑(官方说一次最多 16 路),有的 agent 专门反驳前面的结论。axon 这边分两层。 - - - -轻量用法:主对话里用 Task 工具直接扇出多个子 agent。每个子 agent 有独立上下文,干完只把摘要还给主对话。适合审计、调研、全库扫描这类「要结论、不改工程」的活。 - -工程用法:要从需求交付能跑的代码,走 Blueprint。先聊清楚要啥,生成蓝图当锚点;LeadAgent 接管后一直挂着,自己改关键文件,独立模块派 Worker 去干;Swarm Console 里能看到每个 agent 在干嘛。 - -Claude 在终端里确认一下就开跑。axon 多一个可视化控制台,代码在本地跑,编排逻辑开源能翻。 - -## 14 个 agent 怎么跑起来的 - -回到那个 7 模块审计。 - - - -主 agent 先把仓库摸清,列出 7 个域和对应的 spec、测试文件。然后同时派出 7 个审计 agent——各读各的代码,对照 spec 逐条核对,产出带证据的结论。 - -第一轮结束后,主 agent 不急着写报告。再派 7 个复核 agent,工作就一件事:质疑前一轮的「已完成」。有没有降级实现?eval 是不是空壳?接口是不是只写了类型没写逻辑? - -14 份结果回到主 agent,它做汇总,标出争议点,写最终报告。这一路走的是轻量蜂群,主对话里 Task 并行派发就够了,不用上 Blueprint。 - -如果只是查代码、出报告,到这儿就结了。如果要「从一句话需求到能跑的工程」,得上第二层。 - -SmartPlanner 跟你聊两三轮,把需求落成蓝图。LeadAgent 接手后探索代码库、拆任务、自己改核心文件,独立模块通过 DispatchWorker 派出去。Worker 改完要过 lint 和 typecheck,该跑 E2E 就开浏览器测。Swarm Console 里能点开每个 agent 的对话流——谁改了哪个文件、哪一步挂了,都能追。 - -axon 还有一条 Coding Pipeline:spec、蓝图、测试、交付前有硬门。蜂群负责拆和跑,硬门负责「真能交付」。这是后话,这篇不展开。 - -## 多出来的几件事 - -开源,编排逻辑在 `src/blueprint/` 里,能翻代码,不满意可以自己改。 - -本地跑,直接改磁盘上的工程,不用把整个仓库上传到云端沙盒。 - -Swarm Console 能回放每个 agent 的对话流。排查「谁改坏了」,比翻终端日志省事。 - -路线和 Claude Code 不同,各有取舍。Claude 背靠 Anthropic 的模型和额度;axon 适合想自己托管、想看清 agent 每一步的团队。 - -## 自己试 - -```bash -git clone https://github.com/kill136/axon -cd axon -npm install -npm run dev -``` - -浏览器打开 https://localhost:3456,扔同一个大任务进去——全库审计、安全扫描、多模块对照 spec,都行。有 Claude Code workflow 的话,同一个 prompt 两边各跑一遍,自己比。 - - diff --git a/docs/blog/swarm_mode/assets/preview.html b/docs/blog/swarm_mode/assets/preview.html deleted file mode 100644 index 045fc8e1..00000000 --- a/docs/blog/swarm_mode/assets/preview.html +++ /dev/null @@ -1,74 +0,0 @@ - - - - -蜂群软文配图预览 - - - -

蜂群模式公众号配图预览

-

- 每张图两套:透明底 name.png(浅/深背景各预览)· - 深色底 name-dark.png(真实导出)。 - 重新渲染:node docs/blog/swarm_mode/scripts/render-wechat-assets.mjs -

- -
- - - - diff --git a/docs/blog/swarm_mode/assets/swarm-audit-fanout-dark.png b/docs/blog/swarm_mode/assets/swarm-audit-fanout-dark.png deleted file mode 100644 index fa8bb299..00000000 Binary files a/docs/blog/swarm_mode/assets/swarm-audit-fanout-dark.png and /dev/null differ diff --git a/docs/blog/swarm_mode/assets/swarm-audit-fanout.html b/docs/blog/swarm_mode/assets/swarm-audit-fanout.html deleted file mode 100644 index 6f72dafe..00000000 --- a/docs/blog/swarm_mode/assets/swarm-audit-fanout.html +++ /dev/null @@ -1,96 +0,0 @@ - - - - - - - -
14 agents
-
-
7 模块审计 → 7 路复核 → 汇总
-
-
-
M1
M2
M3
-
M4
M5
M6
-
M7
-
-
-
-
-
-
-
审计
-
A1
A2
A3
-
A4
A5
A6
-
A7
-
-
-
-
-
-
-
复核
-
R1
R2
R3
-
R4
R5
R6
-
R7
-
-
汇总报告
-
- - diff --git a/docs/blog/swarm_mode/assets/swarm-audit-fanout.png b/docs/blog/swarm_mode/assets/swarm-audit-fanout.png deleted file mode 100644 index d35c93f7..00000000 Binary files a/docs/blog/swarm_mode/assets/swarm-audit-fanout.png and /dev/null differ diff --git a/docs/blog/swarm_mode/assets/swarm-cover-dark.png b/docs/blog/swarm_mode/assets/swarm-cover-dark.png deleted file mode 100644 index b20a3045..00000000 Binary files a/docs/blog/swarm_mode/assets/swarm-cover-dark.png and /dev/null differ diff --git a/docs/blog/swarm_mode/assets/swarm-cover.html b/docs/blog/swarm_mode/assets/swarm-cover.html deleted file mode 100644 index 517a56fb..00000000 --- a/docs/blog/swarm_mode/assets/swarm-cover.html +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - -
-
AXON
-
Claude Code 上了 Dynamic Workflows
我们叫它蜂群
-
拆任务 · 并行 · 复核
-
-
-
-
-
- - diff --git a/docs/blog/swarm_mode/assets/swarm-cover.png b/docs/blog/swarm_mode/assets/swarm-cover.png deleted file mode 100644 index ab7804ed..00000000 Binary files a/docs/blog/swarm_mode/assets/swarm-cover.png and /dev/null differ diff --git a/docs/blog/swarm_mode/assets/swarm-dual-layer-dark.png b/docs/blog/swarm_mode/assets/swarm-dual-layer-dark.png deleted file mode 100644 index 416ef003..00000000 Binary files a/docs/blog/swarm_mode/assets/swarm-dual-layer-dark.png and /dev/null differ diff --git a/docs/blog/swarm_mode/assets/swarm-dual-layer.html b/docs/blog/swarm_mode/assets/swarm-dual-layer.html deleted file mode 100644 index 308a6de3..00000000 --- a/docs/blog/swarm_mode/assets/swarm-dual-layer.html +++ /dev/null @@ -1,75 +0,0 @@ - - - - - - - -
-
两层蜂群,看你要干嘛
-
-
-
查代码 · 出报告
-
-
Task
-
主对话扇出子 agent
-
-
-
做工程 · 要交付
-
-
Blueprint
-
蓝图 + LeadAgent + Worker
-
-
-
- - diff --git a/docs/blog/swarm_mode/assets/swarm-dual-layer.png b/docs/blog/swarm_mode/assets/swarm-dual-layer.png deleted file mode 100644 index f4ad82c1..00000000 Binary files a/docs/blog/swarm_mode/assets/swarm-dual-layer.png and /dev/null differ diff --git a/docs/blog/swarm_mode/assets/swarm-same-idea-dark.png b/docs/blog/swarm_mode/assets/swarm-same-idea-dark.png deleted file mode 100644 index 92bc9166..00000000 Binary files a/docs/blog/swarm_mode/assets/swarm-same-idea-dark.png and /dev/null differ diff --git a/docs/blog/swarm_mode/assets/swarm-same-idea.html b/docs/blog/swarm_mode/assets/swarm-same-idea.html deleted file mode 100644 index e11b76b0..00000000 --- a/docs/blog/swarm_mode/assets/swarm-same-idea.html +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - -
-
-
Claude
workflow
-
-
-
-
- 拆任务·并行·复核 -
-
-
-
-
axon
蜂群
-
-
- - diff --git a/docs/blog/swarm_mode/assets/swarm-same-idea.png b/docs/blog/swarm_mode/assets/swarm-same-idea.png deleted file mode 100644 index 4a017b79..00000000 Binary files a/docs/blog/swarm_mode/assets/swarm-same-idea.png and /dev/null differ diff --git a/docs/blog/swarm_mode/assets/swarm-vs-single-dark.png b/docs/blog/swarm_mode/assets/swarm-vs-single-dark.png deleted file mode 100644 index d62aacbb..00000000 Binary files a/docs/blog/swarm_mode/assets/swarm-vs-single-dark.png and /dev/null differ diff --git a/docs/blog/swarm_mode/assets/swarm-vs-single.html b/docs/blog/swarm_mode/assets/swarm-vs-single.html deleted file mode 100644 index bf51bac0..00000000 --- a/docs/blog/swarm_mode/assets/swarm-vs-single.html +++ /dev/null @@ -1,118 +0,0 @@ - - - - - - - -
-
-
一个人
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 🥵 - -
读到第 3 个就忘了第 1 个
-
- -
- -
-
分开干
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
7 路并行,各盯一个模块
-
-
- - diff --git a/docs/blog/swarm_mode/assets/swarm-vs-single.png b/docs/blog/swarm_mode/assets/swarm-vs-single.png deleted file mode 100644 index f37868fe..00000000 Binary files a/docs/blog/swarm_mode/assets/swarm-vs-single.png and /dev/null differ diff --git a/docs/blog/swarm_mode/scripts/render-wechat-assets.mjs b/docs/blog/swarm_mode/scripts/render-wechat-assets.mjs deleted file mode 100644 index c6aa500f..00000000 --- a/docs/blog/swarm_mode/scripts/render-wechat-assets.mjs +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env node -/** - * 将 docs/blog/swarm_mode/assets/*.html 渲染为 PNG,供微信公众号插图。 - * 每张图渲染两份: - * - 透明底:.png(默认,适配浅色/任意背景) - * - 深色底:-dark.png(适配深色模板,背景 #0b1020) - * 用法:node docs/blog/swarm_mode/scripts/render-wechat-assets.mjs - */ -import { chromium } from 'playwright-core'; -import { readdir } from 'node:fs/promises'; -import { join, dirname } from 'node:path'; -import { fileURLToPath, pathToFileURL } from 'node:url'; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const assetsDir = join(__dirname, '../assets'); - -/** 各 HTML 对应的视口尺寸(与 HTML body 宽高一致) */ -const VIEWPORTS = { - 'swarm-cover.html': { width: 900, height: 383 }, -}; - -const DEFAULT_VIEWPORT = { width: 1080, height: 608 }; - -/** 深色底色,与公众号深色模板接近 */ -const DARK_BG = '#0b1020'; - -async function main() { - const files = (await readdir(assetsDir)) - .filter((f) => f.endsWith('.html') && f !== 'preview.html') - .sort(); - - if (files.length === 0) { - console.error('No HTML files found in', assetsDir); - process.exit(1); - } - - const browser = await chromium.launch({ headless: true }); - const context = await browser.newContext(); - let count = 0; - - for (const file of files) { - const viewport = VIEWPORTS[file] ?? DEFAULT_VIEWPORT; - const htmlPath = join(assetsDir, file); - const url = pathToFileURL(htmlPath).href; - - // 透明底 - const page = await context.newPage(); - await page.setViewportSize(viewport); - await page.goto(url, { waitUntil: 'networkidle' }); - const pngPath = htmlPath.replace(/\.html$/, '.png'); - await page.screenshot({ path: pngPath, omitBackground: true, type: 'png' }); - console.log(`✓ ${pngPath.replace(assetsDir + '/', '')}`); - await page.close(); - count++; - - // 深色底:注入背景色后正常截图(不 omitBackground) - const darkPage = await context.newPage(); - await darkPage.setViewportSize(viewport); - await darkPage.goto(url, { waitUntil: 'networkidle' }); - await darkPage.evaluate((bg) => { document.body.style.background = bg; }, DARK_BG); - const darkPath = htmlPath.replace(/\.html$/, '-dark.png'); - await darkPage.screenshot({ path: darkPath, type: 'png' }); - console.log(`✓ ${darkPath.replace(assetsDir + '/', '')}`); - await darkPage.close(); - count++; - } - - await browser.close(); - console.log(`\nRendered ${count} images (${files.length} × 透明 + 深色).`); -} - -main().catch((err) => { - console.error(err); - process.exit(1); -}); diff --git a/docs/manus-parity/manus-product-prd-2026-06-02.md b/docs/manus-parity/manus-product-prd-2026-06-02.md new file mode 100644 index 00000000..ce1c11ee --- /dev/null +++ b/docs/manus-parity/manus-product-prd-2026-06-02.md @@ -0,0 +1,334 @@ +# Manus 全功能 PRD(登录态实测版)— axon 对齐基线 + +> 调研日期:2026-06-02 | 方法:**真登录态实测**(非截图猜测) +> 账号:wang bingjie / bingjiew359@gmail.com(Free plan, isAlpha)| 版本:**Manus 1.6 Lite** +> 素材:[screenshots/](screenshots/)(登录态真截图 + [process/](screenshots/process/) 动态过程帧)| 高保真页:[mockups/home.html](mockups/home.html)(首页)+ [mockups/task-running.html](mockups/task-running.html)(**任务执行态**)| 录屏:[recordings/](recordings/)(slides + website 全程) +> +> ⚠️ **必读 §2.5「任务执行的动态过程 UE」** —— Manus 最核心的差异化交互(步骤实时流式 + Manus's Computer 工作台),用全新任务真录制反推,是 axon 最该对齐的一层。 + +本文是 [manus-playbook-coverage-analysis.md](../manus-playbook-coverage-analysis.md)(**playbook 任务维度**,100 个模板)的**正交补充**——本文盘的是**应用本体 UI / 信息架构 / 辅助配置 / 商业化模型**,即 playbook 分析覆盖不到的「产品骨架」。两者合起来才是 Manus 的完整全景。 + +--- + +## 0. 关键技术结论(登录态破解,写给做对齐的分身) + +| 项 | 实测结论 | +|---|---| +| **会话鉴权** | cookie `session_id` = 一个 HS256 JWT(claims: email/user_id/exp/jti/team_uid),域 `.manus.im`,**非 httpOnly**(前端 JS 可读),90 天有效,SameSite=Lax | +| **token 流向** | 前端 `getToken()`(webpack 模块 27755)从 `document.cookie` 取 `session_id` → 作 `Authorization: Bearer ` 打 `api.manus.im`(connect/gRPC-web) | +| **登录态门** | 另有 cookie `login_success=1`;`isLogin` 由 redux `authStatus` selector 从 `UserService.userInfo` 派生 | +| **App 路由** | SPA 在 `https://manus.im/app`;落地页 `manus.im/` 是独立 Next.js 营销站(bundle 在 `files.manuscdn.com` CDN)。登出访问 `/app` → 跳 `/login?redirectUrl=` | +| **Settings 路由** | **hash 路由** `/app#settings/`(不是 `/settings/`,独立导航会 404)。模态弹层形态 | +| **沙盒** | `*.sg1.manus.computer`(如 `3000-xxx.sg1.manus.computer`)= 每会话云电脑反代域名,印证 [[project_manus_sandbox_decision]](E2B Firecracker) | +| **本地存储** | `UserService.userInfo`(用户信息) / `UserService.userClientConfig`(客户端配置) / `manus-connectors`(连接器) / `persist:root`(只持久化 `configs`) / `im_bound_session_info` | +| **第三方** | 底部 "from ∞ Meta"(Meta 关联**属实**,但 [[feedback_no_propagate_unverified_realworld_claims]] 的「下架」是误传——产品在线运营);接入 GTM/Facebook Pixel/Amplitude/Intercom | + +**axon 复刻可直接抄的注入法**:Playwright `context.addCookies([{name:'session_id', value: JWT, domain:'.manus.im', ...}])` 即获得完整登录态(本次 30+ 截图全靠这条)。 + +--- + +## 1. 全局信息架构(IA 树) + +``` +manus.im/ +├─ (营销落地页, Next.js, /playbook 等) +└─ /app ← SPA 主入口(登录态) + ├─ 顶栏 + │ ├─ 模型选择器「Manus 1.6 Lite ▾」 + │ ├─ 🔔 通知中心 + │ ├─ 积分 pill「✦ 1,097」→ 弹层(Credits/Daily refresh/View usage/Upgrade) + │ └─ 头像「W」→ 菜单 + │ ├─ 账号切换(Personal) + │ ├─ Free / Upgrade + │ ├─ Credits 1,097 + │ ├─ Personalization / Account / Settings + │ ├─ Homepage↗ / Get help↗ / Docs↗ + │ └─ Sign out + ├─ 左栏导航 + │ ├─ manus logo + 搜索 + 折叠 + │ ├─ New task(默认) + │ ├─ Agent → /app/agents(部署营销 agent) + │ ├─ Plugins → /app/plugins(插件市场) + │ ├─ Scheduled[New] → /app/scheduled(定时任务) + │ ├─ Library → /app/library(交付物知识库) + │ ├─ Projects + New project + │ ├─ All tasks(任务历史,带状态图标 + 过滤) + │ └─ 底部:通知开关卡 / 工具图标 ×3 / "from ∞ Meta" + ├─ 主区(New task 态) + │ ├─ Free plan | Upgrade + │ ├─ H1「What can I do for you?」(serif) + │ ├─ Composer(输入 + +附件 + GitHub + Cloud computers[New] + 🎙 + ↑发送) + │ ├─ Mode chips:Create slides / Build website / Develop desktop apps / Design / More + │ │ └─ More 弹层:Video / Develop apps / Schedule tasks / Wide Research / + │ │ Spreadsheet / Visualization / Audio / Chat mode / Playbook↗ + │ └─ Download Manus 桌面端推广卡(轮播) + ├─ 主区(任务态)/app/:sessionId + │ ├─ 左:对话流(user/assistant + Task completed + Suggested follow-ups) + │ ├─ 右:Manus's Computer 面板(工具执行可视化,重活任务才出) + │ └─ 底部 composer + Cloud computers + └─ Settings 模态 /app#settings/* + ├─ Account 组:Account / General / Usage & Billing / Personalization + └─ Features 组:Mail Manus / Data controls / My Computer / Cloud browser / + My plugins / Integrations +``` + +--- + +## 2. 逐 Surface 规格 + axon 对齐状态 + +> 状态图例:🟢 axon 已对齐 | 🟡 部分对齐/形态有差 | 🔴 缺失 | ⚪ 战略不抄 + +### 2.1 主框架(三栏 workbench)— 🟢 +- 左栏 260px 导航 + 中间对话/主区 + 右侧 Computer/画布面板。 +- **axon**:[[project_manus_alignment_workbench]] F0-F2 已对齐三栏;ManusWorkbench 形态一致。 +- 截图:[02-app-home.png](screenshots/02-app-home.png) + +### 2.2 顶栏 — 🟡 +| 元素 | Manus | axon 状态 | +|---|---|---| +| 模型选择器 | 「Manus 1.6 Lite ▾」 | 🟡 axon 唯一 backend=GPT5.5,可不做切换但需占位 | +| 通知中心 🔔 | 任务完成提醒 + 红点 | 🔴 缺独立通知中心 | +| 积分 pill | ✦ 1,097 + 弹层 | 🟡 ApiUsageBar 已有余额(NewAPI),但非此交互形态 | +| 头像菜单 | 见 IA(10 项) | 🟡 有头像无完整菜单 | +- 截图:[17-model-selector.png](screenshots/17-model-selector.png) · [18-credits.png](screenshots/18-credits.png) · [19-avatar-menu.png](screenshots/19-avatar-menu.png) + +### 2.3 Composer + Mode Chips — 🟢(核心) +- 输入框 placeholder「Assign a task or ask anything」。 +- 工具排:**+**(附件) · **GitHub** 图标 · **Cloud computers[New]** chip · 🎙语音 · **↑** 发送。 +- Mode chips(pinned 4 + More 9 = **13 个**): + +| # | Manus chip | axon mode | 状态 | +|---|---|---|---| +| 1 | Create slides | slides | 🟢 | +| 2 | Build website | website | 🟢 | +| 3 | Develop desktop apps | desktop-app | 🟢 | +| 4 | Design | design | 🟢 | +| 5 | Video | video | 🟢 | +| 6 | Develop apps | app | 🟢 | +| 7 | Schedule tasks | schedule | 🟢(Manus More 浮窗本有的内建 mode,非 axon 新增——2026-06-04 ListScheduledTasks API 实证有真任务) | +| 8 | Wide Research | wide-research | 🟢 | +| 9 | Spreadsheet | spreadsheet | 🟢 | +| 10 | Visualization | visualization | 🟢 | +| 11 | Audio | audio | 🟢 | +| 12 | Chat mode | chat | 🟢(Manus More 浮窗本有的内建 mode,非 axon 新增——16-mode-more.png 实证) | +| 13 | **Playbook↗** | — | 🔴 **缺**(100 模板入口,跳独立页) | + +- **差异**:Manus 有 **Cloud computers** chip(每任务挂云电脑)+ **GitHub** 内联;axon 走 coding 流水线但无「云电脑 chip」显式开关。 +- **axon 对齐**:[[project_code_routing_removal_pivot]] 已把 12 mode 迁 skill(activate_skill),mode→skill 实质收官。**唯一硬缺口 = Playbook 模板库入口**(见 §3)。 +- 截图:[16-mode-more.png](screenshots/16-mode-more.png) · [20-composer-plus.png](screenshots/20-composer-plus.png) + +### 2.4 左栏「All tasks」任务历史 — 🟡 +- 每行:状态图标(虚线圈=进行中 / 实心=完成 / 不同图标=不同 mode)+ 任务标题截断。 +- 顶部「All tasks」+ 过滤图标(tabs: all/task/agents/library/scheduled/project)。 +- **axon**:有会话历史但无此「带 mode 图标 + 状态」的统一任务列表形态。🟡 + +### 2.5 ★任务执行的动态过程 UE(核心交互,全新任务真·实时录制)★ — 🟡 最该对齐 + +> **这是 Manus 最核心、差异化最强的交互层**,也是静态截图盘点漏掉的部分。本节用**两个全新任务的真实时录制**(slides + website,非历史回显,避免失真)反推。 +> 录屏:[recordings/slides-live-process.webm](recordings/slides-live-process.webm) · [recordings/website-live-process.webm](recordings/website-live-process.webm) +> 高保真页:[mockups/task-running.html](mockups/task-running.html)(任务执行态双栏,浏览器直开) + +#### 2.5.1 两种交付可视化模式(按 mode 分流) +| 模式 | 用哪些 mode | 交付物渲染在哪 | +|---|---|---| +| **A. 内联交付** | slides / design / doc / spreadsheet / 媒体类 | 交付物(幻灯片/图/表)直接渲染在**对话流里**,无右侧面板 | +| **B. Computer 工作台交付** | website / app / desktop-app / dashboard / 编程类 | 右侧弹出 **Manus's Computer** 面板(IDE 式),左对话 + 右工作台双栏 | + +#### 2.5.2 对话流动态时间线(共同骨架,逐帧实测) +一个任务从提交到完成,对话流按顺序**实时流式**出现以下元素: +1. **Mode badge**(右上角,如 `⌬ Website` / `📑 Slides`)——标明当前产品形态 +2. **用户 prompt 气泡**(右对齐) +3. **agent 计划叙述**(`manus [Lite]`:「好的,我将…先制定一个计划」)——先说要做什么 +4. **步骤列表**(带状态图标):`✓ 已完成` / `⟳ 进行中(spinner)`,如 `初始化项目脚手架` → `编写组件与样式` → `部署并向用户交付结果` +5. **脚手架/Blueprint 卡片**:`⬢ Todo App · Initializing…`(项目卡) +6. **工具执行卡**(tool-use card):`🔍 Search …` / `✎ Editing src/App.tsx`,带**时间戳 + 结果摘要**,可展开 +7. **✦ Thinking 活体状态**(脉冲点):`Thinking · 启动 dev server…` / `Optimizing slide height…`——实时告诉用户此刻在干什么 +8. **步骤进度卡**(底部常驻):`🖥 部署并向用户交付结果 3/3 · Building…`+缩略图,hover 提示 `View Manus's computer` +9. 运行时 composer 的发送键变 **■ 停止键** +10. 结束:**✓ Task completed** + `How was this result?` 星评 + **Suggested follow-ups**(3 条可点追问,如「添加过滤选项卡」「拖拽排序」)+ **交付物卡** + +#### 2.5.3 Manus's Computer 工作台(模式 B 右栏)= 一整套 BaaS,不只是 dev server +全栈任务实测,右栏 tab 远不止预览,是**围绕生成 app 的后端即服务工作台**: +| tab | 内容(实测) | axon | +|---|---|---| +| **Preview** | device 切换(桌面/手机)+URL 栏+`Edit`+全屏+**活的渲染 app**(待办 app 真跑出;构建中显示 "Manus is building…") | 🟡 iframe live preview 有 | +| **`` Code** | 左**文件树**(src/components/、ui/、lib、index.html、package.json、vite.config、tailwind.config…)+右**编辑器流式写代码**(真 Vite+React+TS 工程) | 🟡 有 Monaco 但未做成任务态右栏工作台 | +| **Backend Server** | 后端服务状态/日志 | 🔴 | +| **Dashboard** | **部署分析**(Analytics 流量折线图+汇总卡,"Publish to start collecting visitor data") | 🔴 | +| **Database** | **托管数据库表视图**(表列 `users`/`customers`/`appMetadata`,列编辑/Refresh/分页/Settings;`users` 即 **Manus Auth** 表,自动填入登录用户) | 🔴 **重要缺口** | +| **File storage** | 对象存储 | 🔴 | +| **⚙ Settings** | app 配置 | 🔴 | +- **右上**:`⤴ Share` / `⇪ Publish`;**右下角** `🍃 Made with Manus` 水印 +- **★关键发现**:Manus 给每个全栈 app **自动 provision 托管 DB + Manus Auth + 对象存储 + 后端 + 部署分析**(= 完整 BaaS)。这是 F 类「数据应用」playbook 能一键交付可用产品的底座,**axon 当前只有 dev server preview,缺这整层 BaaS**——而这恰是编程护城河里 axon 最该补、也最该超过 Manus 的地方(axon 是 IDE,DB/Auth/Storage 工具链更强)。 +- **★闭环实证(CRM 任务跑完)**:成品 CRM **真渲染出可用 dashboard**——左 app 导航(Dashboard/Customers) + 汇总卡 `Total 12 / Active 8 / Inactive 1 / Leads 2 / Churned 1` + 状态分布图;app 顶部 Features 标注它接了 **`Backend Server / Database / Manus Auth`**;**部署后有 Manus Auth 登录门**("Sign in to continue / Access requires authentication",本 session 自动登入)。即「从一句话 prompt → Blueprint 确认 → 托管全栈 app(带 DB/Auth/数据/图表)」整条链真闭环。截图:[process/web-12-crm-live-dashboard.png](screenshots/process/web-12-crm-live-dashboard.png)(活的 CRM+数据+图表)· [process/web-13-crm-baas-features.png](screenshots/process/web-13-crm-baas-features.png)(Features: Backend Server/Database/Manus Auth)· [process/web-11-crm-completed-authgate.png](screenshots/process/web-11-crm-completed-authgate.png)(Task completed + 登录门 + follow-ups) + - ⚠️ 注意 [[feedback_done_means_verified_in_real_product]] 同源坑:**回开已完成任务,右栏 dev preview 会显示 "Loading preview…"(dev server 已回收=失真)**——真·活预览只在任务运行中 or 已 Publish 的公网 URL 上;DB/Dashboard tab 数据则持久可看。 +- 截图:[process/web-02-computer-panel-code.png](screenshots/process/web-02-computer-panel-code.png)(文件树+流式代码)· [process/web-07-computer-database-auth.png](screenshots/process/web-07-computer-database-auth.png)(**托管 DB + Manus Auth 表**)· [process/web-08-computer-analytics.png](screenshots/process/web-08-computer-analytics.png)(部署分析)· [process/web-03-computer-panel-preview-done.png](screenshots/process/web-03-computer-panel-preview-done.png)(活的预览+Task completed+follow-ups)· [process/workbench-two-pane-computer.png](screenshots/process/workbench-two-pane-computer.png)(双栏全貌) + +#### 2.5.4 过程内的商业化触点(实测拦截) +- **模型升级 upsell 卡**:编码任务中弹 `✦ This task needs more firepower / 你的任务对 1.6 Lite 太复杂,切到 1.6 Max`(`Try Lite anyway` / `Build with 1.6 Max`)——**阻塞式**,停在 `Waiting for user…` +- **并发任务限流弹窗**:`Concurrent task limit reached / 你已达并发上限,升级解锁更多`(`Cancel` / `Upgrade now`)——Free 档单并发 +- 截图:[process/web-01-scaffold-upsell-gate.png](screenshots/process/web-01-scaffold-upsell-gate.png) + +#### 2.5.5 slides 内联交付实测(模式 A 范例) +prompt→`📑 Slides` badge→「I will create … HTML mode. Let me start by researching」→`🔍 Search` 工具卡带结果→`✦ Thinking`→**对话内逐页渲染 HTML 幻灯片**(封面「杭州西湖/人间天堂」中文零乱码、十景/美食卡)+ **CSS 代码块流式**→`Generate HTML presentation slides 3/3 · Optimizing…`。**= HTML deck 路线,与 axon slides 同源**([[project_slides_mode_redone_html_deck]] / [[feedback_slides_image_vs_code_false_dichotomy]])。 +- 截图:[process/slides-02-research-streaming.png](screenshots/process/slides-02-research-streaming.png) · [process/slides-03-deck-codegen.png](screenshots/process/slides-03-deck-codegen.png) · [process/slides-04-cover-rendered.png](screenshots/process/slides-04-cover-rendered.png) + +#### 2.5.6 axon 对齐差距(这一层最关键) +| 过程元素 | axon 现状 | +|---|---| +| 步骤列表实时流式 + 状态图标 | 🟡 CodingPipelinePanel 有流水线卡,但非 Manus 这种「叙述+步骤+工具卡」编织的对话流叙事 | +| **✦ Thinking 活体状态行**(此刻在干什么) | 🔴 缺——这是「不静默」的关键体验([[project_silent_hang_detection_landed]] 同源诉求) | +| 工具执行卡(带时间戳+可展开结果) | 🟡 有 tool_result 但非统一卡片叙事 | +| **Manus's Computer 多 tab 工作台**(Preview/Code/Database/Storage) | 🟡 [[project_coding_app_canvas_visibility_landed]] 右栏 iframe 通了 live preview,但**缺 Code/Database/Storage 多视图 tab + 文件树**(axon 是 IDE,这块反而最该超过 Manus)| +| **托管 BaaS**(自动 provision DB + Auth + 后端 + 对象存储 + 部署分析) | 🔴 **大缺口**——见 §2.5.3;F 类数据应用一键交付可用产品的底座,axon 仅 dev server,**编程护城河应优先补这层** | +| **Suggested follow-ups**(结束后 3 条追问) | 🔴 缺——留存/二次激活关键 | +| Task completed + 星评反馈 | 🔴 缺 | +| 交付物卡 + Share/Publish | 🟡 publish 链路有([[project_m15a_publish_tool_landed]]),无统一交付卡 UI | +| **过程内 upsell/限流**商业化触点 | ⚪ 形态可借鉴,axon 走 NewAPI quota | + +> **结论**:axon 的「能力」已对齐(真能跑出 slides/app),但「**过程呈现的叙事感**」差一截——用户看到的不是「Manus 在我眼前一步步工作」,而是相对静默的流水线。**这层动态 UE 是 Manus「产品力」的核心,优先级应高于再补静态配置页。** + +#### 2.5.7 ★Blueprint 功能计划确认卡(构建前 user-in-the-loop)★ — 🟢 axon 已有同构 +全栈/复杂任务在动手前先弹 **Blueprint 卡片**(实测 CRM dashboard 任务): +- 标题 `⬢ Blueprint · Customer Management Dashboard` + `Review feature plan` +- **可逐条编辑的特性清单**(每条 hover 出 ✏️ 编辑 / 🗑 删除):`客户数据库 schema(name/company/email/status/createdAt)` → `预置 8 条示例数据` → `带搜索过滤的客户表` → `modal 表单增/改(带校验)` → `删除确认弹窗` → `状态分布图(饼/柱)` → `汇总卡` → `+ Add feature` +- **`Skip all 59s` 自动跳过倒计时** + 进度 dots + **`Next`** 按钮;不回复则 `Manus will continue working after your reply` +- **axon 对齐**:这正是 axon **M6 propose_coding_spec 卡 + M7 generate_coding_blueprint** 的 Manus 原版([[project_manus_blueprint_card]] / [[project_m6_spec_card_landed]] / [[project_m7_blueprint_landed]])。axon 已有同构硬门,**形态可直接对齐**(补「可编辑特性 + 倒计时自动跳过」交互)。 +- 截图:[process/web-05-blueprint-feature-plan.png](screenshots/process/web-05-blueprint-feature-plan.png) + +#### 2.5.8 website/app mode 的 composer 增强(建任务前)— 🟡 +选 `Build website` mode 后,composer 下方额外出: +- **模板选择器**:`Landing Page / Dashboard / Portfolio / Corporate / SaaS …` +- **`Add website reference`**(给参考站)+ **`Import from Figma`** +- **`Powerful built-in Integrations`** 展示:`LLM / Stripe / Database / Image generation / Maps / Notification / File storage / Data API / Voice-to-Text`(= §2.5.3 BaaS 的入口宣告) +- 截图:[process/web-06-website-builder-picker.png](screenshots/process/web-06-website-builder-picker.png) + +#### 2.5.9 ★12 个 mode 动态过程全实测一览(每个都跑了真任务)★ +全部 12 mode 各发起真任务、录动态过程。归纳出 **5 类 UE 范式 + 3 个特殊门**: + +| # | mode | UE 范式 | 动态过程实测(真任务) | 截图 | axon | +|---|---|---|---|---|---| +| 1 | Create slides | **A 内联** | `📑Slides` badge→研究→对话内**逐页渲染 HTML 幻灯片**+CSS 流式 | [slides-04](screenshots/process/slides-04-cover-rendered.png) | 🟢 | +| 2 | Build website | **B 工作台** | 脚手架卡→Computer 面板文件树+流式代码+活预览 | [web-02](screenshots/process/web-02-computer-panel-code.png) | 🟡 | +| 3 | Develop apps | **B 工作台+BaaS** | Blueprint 卡→托管 DB/Auth/后端→可用 CRM(12 客户+图表) | [web-12](screenshots/process/web-12-crm-live-dashboard.png) | 🟡 | +| 4 | Develop desktop apps | **🔒 桌面门** | 点开即弹 `Local Folder Access Required`——**必须 Manus 桌面客户端**授权本地文件夹,Web 端做不了 | [mode-04](screenshots/process/mode-04-desktop-app-gated.png) | ⚪ axon 是 Electron 反而有优势 | +| 5 | Design | **C 媒体生成** | `🎨Design`→「entering generation mode」→工具卡 **`Generating image with GPT Image 2`**→落盘 png | [mode-05](screenshots/process/mode-05-design-imagegen.png) | 🟡 axon 有 image_generation | +| 6 | Video | **C 媒体生成**(free=💰付费门) | **Free**: 「video generation 是订阅专属」→降级备选+`Continue`;**Paid(实测)**: 真生成——`▶Video`→**GPT Image 2 逐帧生成 keyframe 图**(带详细 prompt)→组装视频→出 **Video editing mode**(Start editing) | [06](screenshots/process/mode-06-video-paywall.png)(free墙) · [06b](screenshots/process/mode-06b-video-paid-generating.png)(逐帧生成) · [06c](screenshots/process/mode-06c-video-paid-editmode.png)(编辑模式) · [06d](screenshots/process/mode-06d-video-paid-final-player.png)(**成品视频播放器:霓虹AI logo动画**) | 🟡 | +| 7 | Schedule tasks | **D 定时配置** | 生成**定时任务卡**(`Daily at 09:00` + AI 新闻早报),快速完成 | [mode-07](screenshots/process/mode-07-schedule-recurring.png) | 🟢 axon 有 schedule+Cron | +| 8 | Wide Research | **E 研究扇出** | 大量 `🔍Search` 来源卡聚合(EV 销量等多源数据)→边查边给结论 | [mode-08](screenshots/process/mode-08-wide-research.png) | 🟡 | +| 9 | Spreadsheet | **A 内联** | 快速产 **Excel 文件**(个人月度预算表)+`view all files`+交付卡 | [mode-09](screenshots/process/mode-09-spreadsheet-excel.png) | 🟡 | +| 10 | Visualization | **E→图表** | 研究数据(EV 销量 2024/2025)→准备图表可视化 | [mode-10](screenshots/process/mode-10-visualization.png) | 🟡 | +| 11 | Audio | **C 媒体生成** | **`Loading skill 'music-prompter'`**(skill 系统实战!)→工具卡 `Generating music`→落盘 wav | [mode-11](screenshots/process/mode-11-audio-musicgen.png) | 🟡 | +| 12 | Chat mode | **F 纯对话** | `Thinking process ⌄`(可折叠)→直接答→`Start agent`/`Create` 升级按钮+追问+星评 | [mode-12](screenshots/process/mode-12-chat-qa.png) | 🟢 | + +**5 类 UE 范式**:**A 内联交付**(slides/spreadsheet,渲染进对话流) · **B Computer 工作台**(website/app,IDE+BaaS) · **C 媒体生成**(design/audio,`Generating image/music` 工具卡 + skill 加载) · **D 定时配置**(schedule) · **E 研究扇出**(wide-research/visualization,多源检索) · **F 纯对话**(chat)。 +**3 个门**:desktop-app=🔒 桌面客户端门 · video=💰 订阅付费门 · 复杂全栈=模型升级 upsell 门(§2.5.4)。 +**新发现**:media 类(design/audio)实测**调用 Manus skill**(`Loading skill 'music-prompter'`)——印证 axon [[project_code_routing_removal_pivot]] 的 skill 组合层方向(Manus 也是 chip=skill)。 + +#### 2.5.10 ★Free vs Paid 实测对比(两个真账号都跑过)★ +用 Free 号(Lite) + Paid 号(Stripe 订阅)分别实测,差异: +| 维度 | Free | Paid(实测) | +|---|---|---| +| 模型 | **Manus 1.6 Lite** | **Manus 1.6**(满血,无 Lite 后缀) | +| Credits | ~1,097(每日刷新 300) | **3,852**(且无每日刷新限制提示) | +| 顶部 plan | `Free plan | Upgrade` 常驻 | 无(已订阅,`paymentPlatform:stripe` + `currentPeriodEnd`) | +| **Video 生成** | 💰 订阅墙拦截 | **✅ 真生成**:GPT Image 2 逐帧 keyframe → 组装视频 → **成品播放器**(霓虹 AI logo 动画) + **Video editing mode**(剪辑/加音乐/导出编辑器) | +| 复杂任务 upsell | `needs more firepower → 1.6 Max` | 1.6 起步,仍可升 Max(更复杂时) | +| 截图 | — | [paid-01-home](screenshots/paid-01-home-manus16-full.png)(满血首页) · [06d 成品视频](screenshots/process/mode-06d-video-paid-final-player.png) · 录屏 [video-paid-live-process.webm](recordings/video-paid-live-process.webm) | +- **对 axon 的意义**:① 商业化分层(Lite 免费/满血付费/媒体生成订阅墙)是 Manus 的核心变现结构,axon 走 NewAPI quota 可对齐分层;② **Video = 关键付费钩子**(成本高、感知强),axon 若做 video 应直接对标「逐帧图生成→组装→内置编辑器」这条链;③ media 生成已是付费护城河,axon 编程护城河是差异化主场,不必正面卷 video 模型。 + +### 2.6 Settings 模态(辅助配置全集)— 🔴 重点欠债 +模态左栏两组 + 右侧内容。**这是 axon 最大形态空白**([铁律] 用户专门点名「辅助配置功能」)。 + +| Tab | hash | 内容(实测) | axon | +|---|---|---|---| +| **Account** | `#settings/account` | 账号信息/绑定/注销 | 🔴 | +| **General** | `#settings/general` | Appearance(Language 下拉 / Theme: Light·Dark·Auto) + Communication preferences(Browser notifications / Receive product updates / Email me when queued task starts / Ads about Manus 四开关) | 🔴 | +| **Usage & Billing** | `#settings/usage` | 3 tab: **Tasks / Websites / Computers**;Free + Upgrade;Credits 797;Daily refresh 300(每日16:00);**Credits history 按任务扣费明细** | 🟡(NewAPI 有额度无此 UI) | +| **Personalization** | `#settings/personalization-center/profile` | 2 tab: Profile/Knowledge;**Import memory from another AI**;Nickname/Occupation/More about you/**Custom Instructions**;Import memory 按钮 | 🔴 | +| **Mail Manus** | — | 邮件触发 agent(给 agent 发邮件派活) | 🔴 | +| **Data controls** | `#settings/data-controls` | 数据留存/训练授权/导出/删除 | 🔴 | +| **My Computer** | `#settings/my-computer` | 2 tab: **Cloud computer**(持久 24/7 云工作区 + Create now) / **Local computer**(桌面端本地执行) | 🟡(E2B 已接,无配置 UI) | +| **Cloud browser** | — | 云浏览器会话配置 | 🟡(Browser 工具有,无配置面) | +| **My plugins** | `#settings/my-plugins` | 3 tab: **Connectors / Skills / Data sources** + Browse plugins + Create▾;已接 GitHub connector | 🟡 | +| **Integrations** | `#settings/integrations` | 第三方集成(Slack 等) | 🔴 | +- 截图:[30-settings-open.png](screenshots/30-settings-open.png) · [32-settings-general.png](screenshots/32-settings-general.png) · [33-set-usage.png](screenshots/33-set-usage.png) · [34-set-connectors.png](screenshots/34-set-connectors.png) · [36-set-my-computer.png](screenshots/36-set-my-computer.png) · [43-personalization-profile.png](screenshots/43-personalization-profile.png) + +### 2.7 Agent(部署营销 agent)— ⚪/🔴 +- 「Deploy your agent for marketing」:把品牌化 agent 部署到 Telegram/Messenger/Line/WhatsApp/Slack。 +- 4 卖点:Brand-consistent AI identity / Persistent memory & computer / Custom skills / Works in your messenger。 +- **判断**:这是 Manus 的 **B2B 获客护城河**(强依赖 connector 体系)。axon 短期 ⚪ 不抄([[project_axon_main_line_c_commercial]] 主打 C 端)。 +- 截图:[47-nav-agent.png](screenshots/47-nav-agent.png) + +### 2.8 Library(交付物知识库)— 🔴 +- 按日期分组的产物卡(生成的文档/图/文件)+ My Favorites + 搜索。 +- **axon**:交付物散在各会话,无统一 Library 聚合视图。🔴 +- 截图:[46-nav-library.png](screenshots/46-nav-library.png) + +### 2.9 Plugins 市场 — 🟡 +- **Connectors**:My Browser / Gmail / Instagram / Google Drive / Meta Ads Manager / Google Calendar / Notion / Instagram Creator Marketplace … +- **Skills**:github-pr / internet-skill-finder / html-slides-generator / marble-animator … +- **axon**:[[project_manus_grpc_architecture]] 有 src/skills+src/mcp 同构但 UI 没接;connector 体系是结构性差距。🟡 +- 截图:[45-nav-plugins.png](screenshots/45-nav-plugins.png) + +### 2.10 Scheduled(定时任务)— 🟢 +- /app/scheduled?tab=scheduled,定时任务列表。 +- **axon**:schedule mode + Cron 工具已有。🟢 +- 截图:[39-set-scheduled.png](screenshots/39-set-scheduled.png) + +--- + +## 3. 商业化模型(credits / 计费)— axon 对齐要点 + +| 维度 | Manus 实测 | axon 现状 | +|---|---|---| +| 计费单位 | **Credits**(按任务扣,复杂度定量:实测 9~277/任务) | quota(NewAPI,[[project_m16_commercialization_is_newapi]]) | +| 免费档 | Free credits 一次性 + **每日刷新 300**(16:00 重置) | NewAPI 后台配默认 quota | +| 计费三维 | **Tasks / Websites(托管) / Computers(云电脑时长)** | 仅按 LLM token,**缺 Websites/Computers 计费维度** | +| 升级 | Free → Upgrade(订阅档) | 充值/支付已由 NewAPI 实现 | +| 透明度 | **Credits history 逐任务明细** | 🔴 缺逐任务消耗视图 | + +**对齐建议**:商业化后端别自建(NewAPI 已全实现),但**前端缺一个 Usage&Billing 面**(按任务的 credits 明细 + 三维度),这是付费转化的信任组件。 + +--- + +## 4. axon 差距矩阵(按优先级,给开发对齐) + +| 优先级 | 缺口 | 类型 | 建议 | +|---|---|---|---| +| **P0 ★** | **任务执行的动态过程 UE**(步骤实时流式 + ✦Thinking 活体 + 工具卡 + Computer 多 tab 工作台 + Suggested follow-ups) | **核心产品力** | 见 §2.5;Manus 差异化最强的一层,「在我眼前一步步工作」的叙事感,**优先级最高** | +| **P0** | Settings 模态整套(10 子页) | 形态空白 | 用户点名「辅助配置」;至少先做 General/Personalization/Usage&Billing 三个高频面 | +| **P0** | Personalization(Custom Instructions + 记忆) | 产品力 | 直接影响输出质量,axon 有 memory 子系统但无用户可配 UI | +| **P1 ★护城河** | **托管 BaaS**(生成 app 自动配 DB + Auth + 后端 + 存储 + 部署分析) | 编程护城河 | 见 §2.5.3;F 类数据应用一键交付可用产品的底座,axon 是 IDE、工具链更强,**这块最该超过 Manus** | +| **P1** | Playbook 模板库入口 | 增长 | 100 模板是 Manus 获客主引擎;axon 有标杆模板路线([[project_m10_template_landmark_strategy]])但无聚合入口 | +| **P1** | Library 交付物知识库 | 留存 | 把散落产物聚合成可检索库 | +| **P1** | Usage&Billing 前端面 | 付费转化 | 后端 NewAPI 已就绪,缺逐任务 credits 明细 UI | +| **P2** | 顶栏通知中心 + Suggested follow-ups | 体验 | 任务完成闭环 + 追问引导 | +| **P2** | Connector 体系 + Plugins 市场 UI | 结构性 | 长期 B 端价值,C 端短期不卷 | +| ⚪ | Agent 营销部署(messenger) | B2B | 战略不抄(C 端聚焦) | + +> 与 [[project_reality_blood_bar_status]] 一致:mode/playbook 覆盖已 ~94%,**真血量在「应用骨架完整度 + 真人交付」**。本文揭示的 Settings/Personalization/Library/Usage 这一层「产品骨架」正是 axon 当前最薄的地方——比再加 mode 更值得做。 + +--- + +## 5. 交付物清单 + +- **本 PRD**:[manus-product-prd-2026-06-02.md](manus-product-prd-2026-06-02.md) +- **高保真静态页 ×7**(按 5 类 UE 范式,[mockups/index.html](mockups/index.html) 总览导航): + - [home.html](mockups/home.html)(主屏)· [task-running.html](mockups/task-running.html)(**B 工作台** website/app) + - [inline-delivery.html](mockups/inline-delivery.html)(**A 内联** slides)· [media-generation.html](mockups/media-generation.html)(**C 媒体** video 含播放器+编辑模式) + - [research-fanout.html](mockups/research-fanout.html)(**E 研究** wide-research)· [chat.html](mockups/chat.html)(**F 对话** chat) + - 共享设计系统 `mockups/_manus.css`(据真截图取样);D 定时(schedule)为轻量卡未单独出页 +- **动态过程录屏 ×3**:[recordings/slides-live-process.webm](recordings/slides-live-process.webm)(slides 内联交付)+ [recordings/website-live-process.webm](recordings/website-live-process.webm)(website Computer 编码)+ [recordings/dashboard-live-process.webm](recordings/dashboard-live-process.webm)(**CRM dashboard:Blueprint 卡 + 托管 DB/Auth BaaS**) +- **动态过程帧 15 张**:[screenshots/process/](screenshots/process/)(slides-* 内联 / web-01~10 Computer 工作台+Blueprint+DB+分析 / workbench 双栏) +- **真截图 26 张 + 复刻对照图**:[screenshots/](screenshots/)(`99-mockup-render.png` = 高保真页渲染,可与 `02-app-home.png` 真截图对比) +- **复刻脚本**(登录注入法,可复跑):[scripts/](scripts/)(见 [scripts/README.md](scripts/README.md),核心 = 注入 `session_id` cookie) + +## 6. 不确定项 / 待补 + +1. **access-tokens(OpenAPI)** route 存在但 Free 账号被降级到 General,未截到真面——需付费档复看。 +2. **Websites/Computers 计费 tab** 内容未深入(账号无数据)。 +3. ~~任务态 Computer 面板未截到~~ → **已补**:§2.5 用全新 slides + website 任务真录制覆盖(含 Computer 工作台 Code/Preview 实况)。剩 Database/File storage/Settings tab 内容未深入(待办 app 无后端数据)。 +4. **Mail Manus / Cloud browser / Integrations** 三个 Features 子页未逐一深截。 +5. 动态过程录的是 **Lite 模型**;1.6 Max 的过程/质量差异未测。 +5. 截图为 1440×900 桌面视口;**移动端响应式未覆盖**。 diff --git a/docs/manus-parity/mockups/_manus.css b/docs/manus-parity/mockups/_manus.css new file mode 100644 index 00000000..86664efb --- /dev/null +++ b/docs/manus-parity/mockups/_manus.css @@ -0,0 +1,77 @@ +/* Manus 高保真复刻 共享设计系统 (2026-06-02) — 据登录态真截图取样 + 被 inline-delivery / media-generation / research-fanout / chat 等页 link 复用 */ +:root{ + --bg:#FAFAF8; --side:#F4F3F0; --border:#EAE8E2; --border-soft:#EEEDE8; + --ink:#1F1E1C; --muted:#8C8A82; --muted2:#A8A69D; --pill:#ECEAE4; + --blue:#2563EB; --teal:#12A594; --gold:#C68A24; --green:#1A9E5F; --purple:#6E56CF; + --shadow:0 1px 2px rgba(0,0,0,.04), 0 10px 30px rgba(0,0,0,.05); +} +*{box-sizing:border-box; margin:0; padding:0} +body{font-family:-apple-system,"Segoe UI",Roboto,"PingFang SC","Microsoft YaHei",sans-serif; background:var(--bg); color:var(--ink); -webkit-font-smoothing:antialiased} +.app{display:flex; height:100vh; width:1440px; margin:0 auto} +/* sidebar */ +.side{width:262px; flex:0 0 262px; background:var(--side); border-right:1px solid var(--border); display:flex; flex-direction:column; padding:14px 12px} +.brand{display:flex; align-items:center; gap:8px; padding:6px 8px 12px} +.brand .logo{font-size:20px}.brand .name{font-weight:600; font-size:18px}.brand .sp{flex:1} +.nav{display:flex; flex-direction:column; gap:2px} +.navitem{display:flex; align-items:center; gap:11px; padding:8px 10px; border-radius:9px; font-size:14px; color:#3a3833; cursor:pointer} +.navitem:hover{background:#EDEBE5}.navitem.active{background:#E9E7E0} +.navitem .i{width:18px; text-align:center; color:#55534c} +.badge-new{font-size:11px; color:var(--blue); font-weight:500} +.sec{padding:15px 10px 5px; color:var(--muted); font-size:12.5px} +.tasks{flex:1; overflow:hidden; display:flex; flex-direction:column; gap:1px} +.task{display:flex; align-items:center; gap:9px; padding:7px 10px; border-radius:8px; font-size:13px; color:#46443f; white-space:nowrap; overflow:hidden; text-overflow:ellipsis} +.task:hover{background:#EDEBE5}.task .ti{flex:0 0 16px} +.foot{display:flex; align-items:center; padding:12px 8px 2px; color:var(--muted2); font-size:12.5px} +.foot .sp{flex:1} +/* main */ +.main{flex:1; display:flex; flex-direction:column; min-width:0} +.topbar{display:flex; align-items:center; padding:14px 20px; gap:10px; border-bottom:1px solid var(--border-soft)} +.topbar .model{font-size:15.5px; font-weight:500; color:#2a2924}.topbar .sp{flex:1} +.tbtn{display:flex; align-items:center; gap:6px; font-size:13px; color:#46443f; padding:6px 11px; border-radius:8px; cursor:pointer} +.tbtn:hover{background:var(--pill)} +.upg{color:var(--blue)} +/* conversation */ +.conv{flex:1; overflow:auto; padding:18px 26px 8px; max-width:980px; width:100%; margin:0 auto} +.mbadge{display:flex; justify-content:flex-end; margin-bottom:10px} +.mbadge span{display:inline-flex; align-items:center; gap:6px; font-size:13px; color:var(--muted)} +.ubub{background:#fff; border:1px solid var(--border); border-radius:14px; padding:12px 16px; font-size:14.5px; max-width:74%; margin-left:auto; line-height:1.5} +.arow{display:flex; align-items:center; gap:7px; margin:22px 0 9px} +.arow .lg{font-size:17px}.arow .nm{font-weight:600} +.lite{font-size:11px; color:var(--muted); border:1px solid var(--border); border-radius:6px; padding:1px 6px} +.atext{font-size:14.5px; line-height:1.65; color:#2c2b27} +.atext.b{font-weight:500} +.step{display:flex; align-items:flex-start; gap:9px; margin:13px 0; font-size:13.5px; color:#37352f} +.step .ic{flex:0 0 18px; height:18px; display:grid; place-items:center} +.ck svg{color:var(--green)} +.spin{width:14px; height:14px; border:2px solid #d8d6cf; border-top-color:var(--gold); border-radius:50%; animation:sp 1s linear infinite} +@keyframes sp{to{transform:rotate(360deg)}} +.toolcard{border:1px solid var(--border); border-radius:10px; margin:8px 0 8px 27px; max-width:560px; background:#fff; overflow:hidden} +.toolcard .th{display:flex; align-items:center; gap:8px; padding:8px 12px; font-size:12.5px; color:#46443f; background:#F7F6F2; border-bottom:1px solid var(--border)} +.toolcard .th .sp{flex:1}.toolcard .th .tm{color:var(--muted2); font-size:11.5px} +.toolcard .tb{padding:9px 12px; font-size:12.5px; color:#5b594f; line-height:1.55} +.think{display:flex; align-items:center; gap:8px; margin:13px 0 13px 27px; font-size:13.5px; color:#46443f} +.think .d{width:9px; height:9px; border-radius:50%; background:var(--blue); animation:pul 1.1s ease-in-out infinite} +@keyframes pul{0%,100%{opacity:.35; transform:scale(.8)}50%{opacity:1; transform:scale(1.15)}} +.progress{border:1px solid var(--border); border-radius:12px; background:#fff; display:flex; align-items:center; gap:11px; padding:10px 12px; margin:16px 0 6px; box-shadow:0 1px 2px rgba(0,0,0,.03); max-width:620px} +.progress .pv{width:54px; height:38px; border-radius:6px; border:1px solid var(--border); background:#F2F1ED; flex:0 0 54px; overflow:hidden} +.progress .pt{flex:1; font-size:13px}.progress .ps{font-size:11.5px; color:var(--muted); margin-top:2px} +.progress .pn{font-size:12px; color:var(--muted)} +.done{display:flex; align-items:center; gap:10px; margin:20px 0 6px} +.done .t{display:flex; align-items:center; gap:7px; color:var(--green); font-weight:500; font-size:14px} +.rate{display:flex; align-items:center; gap:8px; background:#F4F3EF; border-radius:10px; padding:8px 14px; font-size:13px; color:var(--muted)} +.followups{margin:14px 0} +.fu{display:flex; align-items:center; gap:10px; padding:13px 4px; border-top:1px solid var(--border-soft); font-size:13.5px; color:#3a3833; cursor:pointer} +.fu .ar{margin-left:auto; color:var(--muted2)} +.composer{margin:10px auto 16px; max-width:980px; width:100%; border:1px solid var(--border); border-radius:16px; background:#fff; padding:14px; box-shadow:var(--shadow)} +.composer .ph{color:#A9A79E; font-size:14px} +.composer .crow{display:flex; align-items:center; gap:9px; margin-top:12px} +.cbtn{width:30px; height:30px; border-radius:50%; border:1px solid var(--border); display:grid; place-items:center; color:#55534c; font-size:14px} +.cloud{display:flex; align-items:center; gap:6px; border:1px solid var(--border); border-radius:999px; padding:5px 11px; font-size:12.5px; color:#46443f} +.cloud .nw{color:var(--blue); font-weight:600; font-size:10px} +.crow .sp{flex:1} +.send{width:30px; height:30px; border-radius:50%; background:#EDEBE5; color:#9b988f; display:grid; place-items:center} +.stop{width:30px; height:30px; border-radius:50%; background:#1F1E1C; display:grid; place-items:center} +.stop i{width:10px; height:10px; background:#fff; border-radius:2px} +.hint{position:fixed; top:6px; left:50%; transform:translateX(-50%); background:#1F1E1C; color:#fff; font-size:11.5px; padding:4px 12px; border-radius:999px; opacity:.85; z-index:9} +/* 复用的迷你侧栏 markup 见各页 */ diff --git a/docs/manus-parity/mockups/chat.html b/docs/manus-parity/mockups/chat.html new file mode 100644 index 00000000..02fde1a9 --- /dev/null +++ b/docs/manus-parity/mockups/chat.html @@ -0,0 +1,58 @@ + + + +Manus 范式F · 纯对话(chat) + + + +
范式 F · 纯对话 — Thinking process + 答案 + Start agent/Create 升级 + 追问 + 星评 · 无 canvas
+
+ +
+
Manus 1.6 Lite ⌄
✦ Upgrade
⤴ Share
+
+
💬 Chat
+
用三句话解释什么是 RAG(检索增强生成)
+
🪴manusLite
+
Thinking process ⌄
+
+ RAG(检索增强生成)是一种结合了信息检索技术和人工智能大模型生成能力的技术框架。当用户提出问题时,系统会先从外部知识库或文档中检索出最相关的参考资料,再将这些资料连同问题一起交给大模型,由它生成准确、有依据的回答。这样大模型就能基于最新的、可溯源的外部知识来生成回答,从而有效减少模型"胡说八道"(幻觉)并提升回答的专业性和可靠性。 +
+
+
✦ Start agent
+
+ Create ⌄
+
+
+
💬 了解如何从需求出发搭建一个简单的 RAG 系统?
+
💬 对比一下 RAG 和微调 (Fine-tuning) 各自的优缺点?
+
💬 详细了解 RAG 中用来存储和检索知识的"向量数据库"是如何工作的?
+
+
How well did Manus answer your question? ☆ ☆ ☆ ☆ ☆
+
+
Message Manus
⌬ Cloud computers New
+
+
+ diff --git a/docs/manus-parity/mockups/home.html b/docs/manus-parity/mockups/home.html new file mode 100644 index 00000000..71f5e6c0 --- /dev/null +++ b/docs/manus-parity/mockups/home.html @@ -0,0 +1,212 @@ + + + + + + +Manus — high-fidelity home replica + + + +
高保真复刻 · 依据登录态真截图 · 点 More 看 13 mode
+
+ + + + +
+
+
Manus 1.6 Lite
+ + +
1,097
+
W
+
+ +
+
Free plan | Upgrade
+

What can I do for you?

+ +
+
Assign a task or ask anything
+
+ + + Cloud computers New + + + + +
+
+ +
+
Create slides
+
Build website
+
Develop desktop apps
+
Design
+
More
+
+
+ + +
+
▶ Video
+
▢ Develop apps
+
▦ Schedule tasks
+
◎ Wide Research
+
▦ Spreadsheet
+
📊 Visualization
+
≋ Audio
+
💬 Chat mode
+
▤ Playbook ↗
+
+ +
+
+
Download Manus for Windows or macOS
+
Access local files and work seamlessly with your desktop.
+
+
▦ preview
+
+
+
+
+ + diff --git a/docs/manus-parity/mockups/index.html b/docs/manus-parity/mockups/index.html new file mode 100644 index 00000000..b526183a --- /dev/null +++ b/docs/manus-parity/mockups/index.html @@ -0,0 +1,53 @@ + + + +Manus 高保真复刻 · 总览 + + +

Manus 高保真复刻 · 总览

+
据登录态真录制(slides/website/dashboard/video 等真任务)反推 · axon ManusWorkbench 对齐开发目标 · 2026-06-02
+ +

高保真页(按 5 类 UE 范式 + 主屏)

+ + +

12 个 mode → 5 类范式映射

+ + + + + + + + + + + + + + +
modeUE 范式对应高保真页
Create slidesA 内联inline-delivery.html
SpreadsheetA 内联inline-delivery.html
Build websiteB 工作台task-running.html
Develop appsB 工作台 + 托管 BaaStask-running.html
Develop desktop appsB 工作台(🔒 需桌面客户端)task-running.html
DesignC 媒体media-generation.html
AudioC 媒体(+ 加载 skill)media-generation.html
VideoC 媒体(💰 free 付费墙 / paid 真生成)media-generation.html
Wide ResearchE 研究扇出research-fanout.html
VisualizationE 研究 → 图表research-fanout.html
Schedule tasksD 定时配置(轻量卡,见 PRD §2.5.9)
Chat modeF 纯对话chat.html
+

5 类范式覆盖 11/12 mode;schedule(D 定时配置)是轻量配置卡,未单独出页。完整规格与真截图见 ../manus-product-prd-2026-06-02.md §2.5。所有页自包含,浏览器直接打开即可对比真截图(../screenshots/process/)。

+ diff --git a/docs/manus-parity/mockups/inline-delivery.html b/docs/manus-parity/mockups/inline-delivery.html new file mode 100644 index 00000000..26fe4748 --- /dev/null +++ b/docs/manus-parity/mockups/inline-delivery.html @@ -0,0 +1,88 @@ + + + +Manus 范式A · 内联交付(slides) + + + +
范式 A · 内联交付 — 交付物(幻灯片)逐页渲染进对话流 · 据 slides 真录制
+
+ +
+
Manus 1.6 Lite ⌄
✦ Upgrade
⤴ Share
+
+
📑 Slides
+
做一份介绍中国杭州西湖的简约风幻灯片,5 页,包含封面、历史、十景、美食、结尾
+
🪴manusLite
+
I will create a minimalist presentation about West Lake in Hangzhou with 8-12 slides using HTML mode. Let me start by researching the content.
+ +
Research West Lake content (history, Ten Scenes, cuisine)
+
🔍 Search for West Lake history, Ten Scenes & cuisine02:17
West Lake's history dates back over a thousand years, with the "Ten Scenes" established in 1699; local cuisine features West Lake Vinegar Fish…
+ +
Generate HTML presentation slides
+ + +
📑 封面1 / 10
+

杭州西湖

人间天堂 · 千年意象
+
+ +
📑 西湖概览2 / 10
+

西湖概览

+
+
📍
地理位置
浙江杭州城西,三面环山,面积约 6.5 km²
+
🌍
世界遗产
2011 年列入世界文化遗产名录
+
🏛
文化底蕴
白苏二堤、断桥残雪,千年诗画意象
+
+
+
+ +
</> 西湖概览 · style⧉ copy
+
.icon { font-size: 64px; color: #16A085 }
+h3 { font-family: 'Montserrat', sans-serif;
+     font-size: 28px; margin-bottom: 20px }
+
+ +
Thinking · Batch generating slides 8, 9, 10 and optimizing slide 1…
+
Generate HTML presentation slides
2:22 · Optimizing slide height…
3 / 3 ⌃
+
+
Message Manus
⌬ Cloud computers New
+
+
+ diff --git a/docs/manus-parity/mockups/media-generation.html b/docs/manus-parity/mockups/media-generation.html new file mode 100644 index 00000000..31543e3d --- /dev/null +++ b/docs/manus-parity/mockups/media-generation.html @@ -0,0 +1,79 @@ + + + +Manus 范式C · 媒体生成(video) + + + +
范式 C · 媒体生成 — Generating image/music 工具卡 → 成品媒体 + Video editing mode · 据 video 付费真录制
+
+ +
+
Manus 1.6 ⌄
⤴ Share
+
+
▶ Video
+
做一个 8 秒的科技公司 logo 开场动画短视频,简约科技风,深蓝霓虹光效
+
🪴manus1.6
+
我可以为你生成这个竖版科技公司 logo 开场动画。我先生成关键帧画面,再合成为视频。
+ +
Generate keyframe image with neon text style
+
🖼
Generating image with GPT Image 2
Minimalist neon deep-blue tech logo keyframe…
+ +
Compose keyframes into video & deliver
+ + +
+
+
AI
+
+
+
+ +
✓ Task completed
How was this result? ☆ ☆ ☆ ☆ ☆
+
+
💬 Make the video 8 seconds long.
+
💬 Change the video to horizontal format.
+
💬 Create a version with a different color scheme.
+
+
Deliver the final video to the user
tech_logo_intro.mp4 · 8s · 1080×1920
3 / 3 ⌃
+ + +
🎬 Video editing mode NEW

Click to enter the editor. Trim clips, add music, and export your final video — all in one place.

Start editing
+
+
Message Manus
⌬ Cloud computers New
+
+
+ diff --git a/docs/manus-parity/mockups/research-fanout.html b/docs/manus-parity/mockups/research-fanout.html new file mode 100644 index 00000000..c3e78856 --- /dev/null +++ b/docs/manus-parity/mockups/research-fanout.html @@ -0,0 +1,56 @@ + + + +Manus 范式E · 研究扇出 + + + +
范式 E · 研究扇出 — 多源 Search 来源卡密集聚合 + 边查边给结论 · 据 wide-research/visualization 真录制
+
+ +
+
Manus 1.6 Lite ⌄
✦ Upgrade
⤴ Share
+
+
📊 Data visualization
+
用图表可视化展示中国近 10 年新能源汽车销量的增长趋势
+
🪴manusLite
+
好的,我将为您搜集中国近 10 年新能源汽车的销量数据,并按照简约风格和配色方案制作高质量的可视化图表。
+ +
搜集中国近 10 年新能源汽车销量数据
+
🔍 搜索 中国新能源汽车销量数据 (2015–2025)04:56
+
🔍 搜索 中汽协 历年新能源乘用车销量 官方统计05:02
+
I found recent data indicating China's NEV sales reached over 1.3 million units monthly in 2025, with consistent growth over ten years, confirming a strong upward trend for visualization. Next, I'll compile detailed yearly figures.
+
🔍 补充 新能源汽车 2016–2023 年历年销量数据05:11
+
🔍 获取 2015 至 2023 年中国新能源汽车年度销量明细05:19
+
🗂 访问 中汽协 / 乘联会 权威网站获取 2015–2025 完整销量05:27
+ +
制作销量增长趋势可视化图表
+
Thinking · Preparing visualizations (line + bar, minimalist palette)…
+
搜集中国近 10 年新能源汽车销量数据
0:44 · Thinking
1 / 3 ⌃
+
+
Message Manus
⌬ Cloud computers New
+
+
+ diff --git a/docs/manus-parity/mockups/task-running.html b/docs/manus-parity/mockups/task-running.html new file mode 100644 index 00000000..7549d244 --- /dev/null +++ b/docs/manus-parity/mockups/task-running.html @@ -0,0 +1,200 @@ + + + + + + +Manus — 任务执行态 高保真复刻 + + + +
任务执行态高保真 · 左=步骤实时流式 右=Manus's Computer 编码工作台 · 据真实录制复刻
+
+ +
+ + + + + +
+ + +
+
+
Manus 1.6 Lite ⌄
+ + ⌬ Website +
+
+
做一个极简的待办事项 web 应用,深色主题,支持添加、勾选完成、删除任务,带本地存储
+ +
🪴manusLite
+
好的,我将为您创建一个极简的深色主题待办事项 Web 应用。我将先制定一个计划,然后初始化项目脚手架、编写组件与样式、部署交付。
+ +
初始化项目脚手架
+
Todo App
Vite + React + TypeScript + Tailwind · localStorage
+ +
编写组件与样式
+
+
✎ Editing src/App.tsx · 写入 TodoList / AddForm 组件
+
+ const [todos, setTodos] = useState<Todo[]>(() => load())
+ useEffect(() => localStorage.setItem(KEY, ...), [todos])
+
+ +
部署并向用户交付结果
+
Thinking · 启动 dev server,渲染预览…
+ +
+
+
部署并向用户交付结果
0:42 · Building & previewing…
+
3 / 3 ⌃
+
+
+
+
Message Manus
+
+ + + ⌬ Cloud computers New + + +
+
+
+ + +
+
+
Preview
+
Code
+
◔ Dashboard
+
Database
+
File storage
+
+ + +
⇪ Publish
+
+
+
+
▾ 📁 src
+
▾ 📁 components
+
📄 TodoItem.tsx
+
📄 AddTodoForm.tsx
+
▸ 📁 lib
+
📄 App.tsx
+
📄 main.tsx
+
📄 index.css
+
▸ 📁 public
+
📄 index.html
+
📄 package.json
+
📄 tailwind.config.ts
+
📄 vite.config.ts
+
📄 tsconfig.json
+
+
+
1import { useState, useEffect } from "react"
+
2import { TodoItem } from "./components/TodoItem"
+
3import { AddTodoForm } from "./components/AddTodoForm"
+
4
+
5// 待办类型 + 本地存储 key(深色主题)
+
6type Todo = { id: string; text: string; done: boolean }
+
7const KEY = "manus.todos"
+
8
+
9export default function App() {
+
10 const [todos, setTodos] = useState<Todo[]>(() => {
+
11 return JSON.parse(localStorage.getItem(KEY) ?? "[]")
+
12 })
+
13 useEffect(() => {
+
14 localStorage.setItem(KEY, JSON.stringify(todos))
+
15 }, [todos])
+
16
+
17 const add = (text: string) =>
+
18 setTodos([...todos, { id: crypto.randomUUID(), text, done: false }])
+
+
+
🍃 Made with Manus
+
+
+ + diff --git a/docs/manus-parity/recordings/dashboard-live-process.webm b/docs/manus-parity/recordings/dashboard-live-process.webm new file mode 100644 index 00000000..cee42fdc Binary files /dev/null and b/docs/manus-parity/recordings/dashboard-live-process.webm differ diff --git a/docs/manus-parity/recordings/slides-live-process.webm b/docs/manus-parity/recordings/slides-live-process.webm new file mode 100644 index 00000000..9e852787 Binary files /dev/null and b/docs/manus-parity/recordings/slides-live-process.webm differ diff --git a/docs/manus-parity/recordings/video-paid-live-process.webm b/docs/manus-parity/recordings/video-paid-live-process.webm new file mode 100644 index 00000000..13aaca81 Binary files /dev/null and b/docs/manus-parity/recordings/video-paid-live-process.webm differ diff --git a/docs/manus-parity/recordings/website-live-process.webm b/docs/manus-parity/recordings/website-live-process.webm new file mode 100644 index 00000000..16f26bac Binary files /dev/null and b/docs/manus-parity/recordings/website-live-process.webm differ diff --git a/docs/manus-parity/reference-manifest.md b/docs/manus-parity/reference-manifest.md new file mode 100644 index 00000000..26884e31 --- /dev/null +++ b/docs/manus-parity/reference-manifest.md @@ -0,0 +1,311 @@ +# Manus Parity — Reference Manifest(A/D 层「显式参照清单」) + +> 生成日期:2026-06-04 · 综合者:reference-freshness synthesizer +> 唯一新建文件,不改动 CLAUDE.md / modeRegistry / 记忆 / traceability-matrix / PRD 等任何已有文件。 + +## ① 这是什么 · 为什么存在 + +axon 的每个「对齐 oracle」(`*.manus-alignment.test.{ts,tsx}` 与 PRD/matrix 里的对齐断言)都把一组 **Manus 的事实**硬编进了断言里。这些事实不是凭空写的,背后是一张 capture(截图 / 录屏 / HAR)+ 一个年代(vintage)+ 一段已核 grounding。问题是: + +- **capture 会陈旧**——Manus 是在线高频迭代产品,截图里的 Credits 余额、mode 列表、Settings 分区、模型档名随时漂移。 +- **当一张新 capture 到位时**,没有一份「这张图当年编码了哪些 Manus 事实、哪些 oracle 依赖它」的索引,就无法回答「丢一张新截图 → diff → 哪些 oracle 要重验」。 + +本 manifest 就是这份索引。它把 A/D 层的「显式参照」固化成可操作的台账: + +- **每个 oracle 单元** ↔ 它引用的 capture + vintage + 编码的 Manus 事实清单 + 哪些是 deliberateSkip(战略不抄)。 +- **每张 capture** ↔ 它的陈旧度(driftRisk)+ 依赖它的 oracle。 +- **一份重抓清单**——给用户稀缺的「登录真 Manus 看一眼」预算,精准列出该拍什么。 +- **一套 drift-diff 流程**——新 capture 到位后如何逐条比对、标记失效 grounding。 +- **一张已确认的信念 bug 表**——VERIFIED_VERDICTS 里 `isRealBug=true` 的,留给主进程修。 + +> 关键纪律:**deliberateSkip ≠ 漏对齐**。它是 axon 看清了 Manus 的做法后*战略选择不抄*(商业化触点 / 死按钮 / 过度交互)。这些必须在 manifest 里显式标注,否则下一个分身会把「故意不做」当成「忘了做」去补,反而破坏护城河定位。 + +--- + +## ② 每单元参照表(oracle ⇄ Manus 参照) + +下面 6 个对齐 oracle,逐个列出 `oracleFile / groundingCaptures / captureVintage / 编码的 Manus 事实 / deliberateSkip 标注`。 +事实编号对应该 oracle 里的「契约 N」。**[SKIP]** = deliberateSkip(axon 战略不抄,断言验「不做」而非「做了」)。 + +### 2.1 unit `web-05-blueprint`(BlueprintCard ⇄ Manus `confirm_webdev_feature`) + +- **oracleFile**:`src/web/client/src/pages/ManusWorkbench/BlueprintCard.manus-alignment.test.tsx` +- **groundingCaptures**: + - `manus2.im62.har` — MathBuddy session 完整反推(8361 ws 帧) + - Blueprint 截图(步骤 3/3) + - 记忆 `project_manus_blueprint_card.md` / `project_manus_event_catalog.md` + - 协议事件 `confirm_webdev_feature.featureConfirm` +- **captureVintage**:2026-05-22(HAR 反推,**low drift**) + +| # | 契约 | 编码的 Manus 事实 | 标注 | +|---|---|---|---| +| 1 | 契约1 | 蓝图卡恰好 3 步,固定顺序 features → styleSelections → powerUps;「立即构建」按钮只在第 3 步出现 | — | +| 2 | 契约1/5 | 步骤 1 features 是 agent 提议的核心特性,view-only 不可编辑(MathBuddy 实拍:作业照片上传识题 / 分步骤解题 / 错题本追踪) | — | +| 3 | 契约2 | powerUps 项带 `abilities[]` 能力标签 chip(HAR 实拍「拍照识题」abilities=['LLM','File storage'])——蓝图卡标志特征 | — | +| 4 | 契约3 | `powerUps[].selected` 默认 true(默认推荐勾选)——「拍照识题」渲染即 checked | — | +| 5 | 契约4 | styleSelections 是 6 风格单选:inspire-me(默认) / playful / ethereal_gradient / pixel_art_digital / form_follows / spectrum;任意时刻恰好 1 个 selected | — | +| 6 | 契约5 | 点「立即构建」上行 `confirm_webdev_feature`,payload=修改后 featureConfirm(features 原样回传,powerUp/style 编辑被反映) | — | +| 7 | 契约6 | Manus 有「倒计时自动跳过」自动开始构建;**axon 不抄**——自动起 LLM 任务耗 credits,axon 选显式确认,渲染后 60s 任凭流逝绝不自动 onConfirm | **[SKIP]** | + +### 2.2 unit `M20.a AvatarMenu`(AvatarMenu ⇄ `19-avatar-menu.png`) + +- **oracleFile**:`src/web/client/src/pages/ManusWorkbench/AvatarMenu.manus-alignment.test.tsx` +- **groundingCaptures**:`docs/manus-parity/screenshots/19-avatar-menu.png`(主区右上角头像点开实拍) +- **captureVintage**:2026-06-02(Manus 1.6 Lite 截图实拍,**high drift**) + +| # | 契约 | 编码的 Manus 事实 | 标注 | +|---|---|---|---| +| 1 | 契约1 | 顶部账号身份块:圆形字母头像 W + wang bingjie + bingjiew359@gmail.com + 切换账号 ⇅ | — | +| 2 | 契约2 | 套餐行 Free + 黑色 Upgrade 按钮(商业化触点);付费态无升级按钮 | — | +| 3 | 契约3 | 额度行 ✦ Credits · 「1,097」(千分位) · ›;axon 用 NewAPI quota 对位 Credits | ⚠️ 见 C5:余额是 volatile 量,断言应验「千分位格式」非精确 1,097 | +| 4 | 契约4 | 分隔线下 Personalization / Account / Settings 三入口;axon 路由到 Settings 模态对应 tab | — | +| 5 | 契约5 | Settings 下方 Homepage / Get help / Docs 三条外链组(带 ↗);**axon 不抄**——无对应公开站,按「不留死按钮」暂不放 | **[SKIP]** | +| 6 | 契约6 | 最底部红色 danger 的 Sign out,与上方用分隔线隔开,固定结构性收尾项 | — | + +### 2.3 unit `M20.a SettingsModal`(SettingsModal ⇄ 设置模态 30/32/33/34) + +- **oracleFile**:`src/web/client/src/pages/ManusWorkbench/SettingsModal.manus-alignment.test.tsx` +- **groundingCaptures**:`30-settings-open.png` / `32-settings-general.png` / `33-set-usage.png` / `34-set-connectors.png` + 子页族 35–41(左栏导航各张一致) +- **captureVintage**:2026-06-02(Manus 1.6 Lite 设置模态实拍,**high drift**) + +| # | 契约 | 编码的 Manus 事实 | 标注 | +|---|---|---|---| +| 1 | 契约1 | 左栏两组:Account 组 4 项(Account/General/Usage & Billing/Personalization) + Features 组 6 项(Mail Manus/Data controls/My Computer/Cloud browser/My plugins/Integrations)=共 10 section;axon 本地化为 账号/功能 | — | +| 2 | 契约2 | General→Appearance:Language(下拉 English) + Theme(Light/Dark/Auto,Auto 高亮);axon 点主题真切 data-theme + localStorage | — | +| 3 | 契约3 | General→Communication preferences 恰好 4 条开关:Browser notifications(默认关) / Receive product updates(开) / Email me when queued task starts(开) / Ads about Manus(开) | — | +| 4 | 契约4 | 点不同 section 右内容区整体换页(Appearance ⇄ Usage 任务/网站/算力/Credits 块);33 有 Credits 块,axon 用 Usage「额度余额」对位 | — | +| 5 | 契约5 | sidebar 顶部有账号头块(W + wang bingjie + Personal + 切换账号 ⇅);**axon 不抄**——身份块已在顶栏 AvatarMenu,避免两处重复 | **[SKIP]** | +| 6 | 契约6 | 左栏底部有「Get help ↗」外链;**axon 不抄**——无对应帮助中心站,「不留死按钮」 | **[SKIP]** | + +### 2.4 unit `modeChips`(ModeChip 目录数据 ⇄ `16-mode-more.png`) + +- **oracleFile**:`src/web/client/src/pages/ManusWorkbench/modeChips.manus-alignment.test.ts` +- **groundingCaptures**:`16-mode-more.png`(More 展开浮窗) / `02-app-home.png`(pinned chips) / event-catalog HAR(suggestQualityMode) +- **captureVintage**:2026-06-02 截图实拍;商业化触点引 event-catalog HAR 2026-05-22(**截图 high drift / HAR low drift**) + +| # | 契约 | 编码的 Manus 事实 | 标注 | +|---|---|---|---| +| 1 | 契约1 | PINNED 常驻 chip 从左到右恰好 4 项:Create slides / Build website / Develop desktop apps / Design,之后是 More 按钮(非第 5 mode) | — | +| 2 | 契约2 | POPOVER 浮窗从上到下恰好 9 项:Video / Develop apps / Schedule tasks / Wide Research / Spreadsheet / Visualization / Audio / Chat mode / Playbook | — | +| 3 | 契约2/3 | **截图实读纠正以讹传讹**:modeRegistry.ts 与 CLAUDE.md 都写「schedule/chat 是 axon 新增」,但浮窗肉眼可见 Schedule tasks 和 Chat mode——它们是 Manus 真有的对齐项(见 C1 已确认 bug) | ⚠️ 见 C1 | +| 4 | 契约3 | 全部 mode = pinned 4 + popover 9 = 恰好 13 个(两组 id 并集双向无漏无溢) | — | +| 5 | 契约4/6 | 浮窗 9 项里只有 Playbook 右侧带 ↗ 外链箭头(跳 SEO playbook 站不发消息);其余 8 项普通 mode chip;axon 标 `playbook.isExternal=true` 且 prompt 为空 | — | +| 6 | 契约5 | Manus 在 mode/会话层有 `suggestQualityMode`「升级高质量模式(更多 credits)」商业化触点;**axon 不抄**——护城河是真实编程能力不是按质量档收 credits,chip 目录不含任何 quality/upgrade/premium/pro 升级 chip | **[SKIP]** | +| 7 | 契约6 | 每个可激活 chip 点击激活 mode 并预填任务 prompt(chip=激活 skill/mode);axon 非外链 chip 必须各自承载非空 prompt 模板,否则是死 chip | — | + +### 2.5 unit `web-06 website-builder-picker`(M10.e TemplatePickerCard ⇄ `web-06-website-builder-picker.png`) + +- **oracleFile**:`src/web/client/src/pages/ManusWorkbench/TemplatePickerCard.manus-alignment.test.tsx` +- **groundingCaptures**:`process/web-06-website-builder-picker.png`(Website chip 高亮入口实拍) + `traceability-matrix.md`(对齐缺口记录) +- **captureVintage**:2026-06-02 截图实拍(**high drift**) + +| # | 契约 | 编码的 Manus 事实 | 标注 | +|---|---|---|---| +| 1 | 契约1 | 模板网格(横排可滚动 chip)实拍 5 项:Landing Page / Dashboard / Portfolio / Corporate / SaaS(末尾 › 可滚更多) | — | +| 2 | 契约2/3 | 模板 chip 可点选的构建入口——单选语义(点一个就进构建);axon 用 `onSelect(id)` + `selectedId` | — | +| 3 | 契约4 | 选定后进构建流程,构建中不应再能点选;axon 用 busy 标志在 server 跑 initializeTemplate 时整卡 disabled 防 race 二次构建 | — | +| 4 | 契约5 | 网格上方有一行 2 入口:Add website reference + Import from Figma;**axon 当前未对齐(已知缺口, traceability 记录)**——只渲染模板网格,锁状态防加死按钮 | **[SKIP]** | +| 5 | 契约6 | 网格上方提示行 'What would you like to build?';axon 用 reason 段对位 | — | +| 6 | 版式注释 | 入口完整版式:标题 'What can I do for you?' + prompt 输入框(Website chip 高亮) + 提示行/2 入口 + 模板网格 + 底部 'Powerful built-in Integrations'(与本卡无关) | — | + +### 2.6 unit `ComposerSkills`(composer + 菜单/Skills 弹层 ⇄ `20-composer-plus.png`) + +- **oracleFile**:`src/web/client/src/pages/ManusWorkbench/ComposerSkills.manus-alignment.test.tsx` +- **groundingCaptures**:`20-composer-plus.png`(输入框左下角 + 点开实拍) + `COMPOSABLE_SKILL_NAMES` / `GET /api/skills`(axon 自有技能数据,非 Manus 观测项) +- **captureVintage**:2026-06-02 截图实拍(**high drift**) + +| # | 契约 | 编码的 Manus 事实 | 标注 | +|---|---|---|---| +| 1 | 契约1/3/6 | + 菜单从上到下只有三项:① Add from local files(📎) ② Use Skills(技能图标 + › 子菜单) ③ More(栅格 + › 子菜单) | — | +| 2 | 契约1/2 | 第 1 项「Add from local files」带 📎(上传附件),招牌首项且为动作项(点了进文件选择+收起);axon 本地化「添加本地文件」 | — | +| 3 | 契约3 | 第 2 项「Use Skills」带 › 子菜单箭头;axon 对位「使用技能」分段 | — | +| 4 | 契约4 | Add from local files 与 Use Skills 之间有分隔线,且前者在上后者在下(固定版式顺序) | — | +| 5 | 契约5 | 「Use Skills ›」是折叠二级子菜单要再点一次展开(截图里二级未展开);**axon 主动简化**——技能直接内联铺在一级可见可 toggle,省一次点击/一层导航 | **[SKIP]** | +| 6 | 契约6 | 第 3 项「More」(栅格+›)点开是更多杂项工具;**axon 不抄**——能力组合走「使用技能」段(COMPOSABLE_SKILL_NAMES 已涵盖),不另起语义模糊的 More 二级抽屉 | **[SKIP]** | + +> **deliberateSkip 汇总**(共 8 条战略不抄,按 unit):blueprint 倒计时自动跳过 · AvatarMenu 外链组(Homepage/Get help/Docs) · SettingsModal sidebar 账号头块 + Get help 外链 · modeChips suggestQualityMode 升级触点 · TemplatePicker Add reference/Figma 入口 · ComposerSkills 折叠二级子菜单 + More 抽屉。这些是 oracle 里**验「不做」**的断言,绝不当成待补缺口。 + +--- + +## ③ Capture 陈旧账(按 driftRisk 排序) + +vintage 2026-06-02 的渲染态截图全部 **high/medium**(Manus 在线产品随时迭代);两份 HAR 为 2026-05-22 但结构稳定 **low**。 + +### 3.1 high driftRisk(优先复核) + +| capture | vintage | 依赖 oracle / 文档 | +|---|---|---| +| `screenshots/02-app-home.png` | 2026-06-02 | modeChips(PINNED 4) · CLAUDE.md「三栏 workbench」信念 · PRD §1 IA/§2.2 顶栏 · matrix §1.1 | +| `screenshots/16-mode-more.png` | 2026-06-02 | modeChips(POPOVER 9/13 全集/Playbook 外链) · CLAUDE.md line 190 · PRD §2.3 · matrix §1.1 · **C1/C2/C4 全部需它裁决** | +| `screenshots/17-model-selector.png` | 2026-06-02 | matrix §1.1 line 82(Lite/Pro/Max) · PRD §2.2 · **C3 档位命名裁决** | +| `screenshots/18-credits.png` | 2026-06-02 | AvatarMenu 契约3(Credits) · PRD §2.2 pill/§3(每日刷新 300/16:00) · **C5 余额裁决** | +| `screenshots/19-avatar-menu.png` | 2026-06-02 | AvatarMenu 全 6 契约 · matrix §1.1 line 84 · PRD §2.2 头像菜单 | +| `screenshots/20-composer-plus.png` | 2026-06-02 | ComposerSkills(+菜单 3 项) · PRD §2.3 composer | +| `screenshots/30-settings-open.png` | 2026-06-02 | SettingsModal(10 section/4 开关/sidebar 头块) · PRD §2.6 · matrix §1.2 | +| `screenshots/31-settings-data-controls.png` | 2026-06-02 | SettingsModal(Data controls) · PRD §2.6 / matrix §1.2 | +| `screenshots/32-settings-general.png` | 2026-06-02 | SettingsModal 契约2/3(Language/Theme/4 通信开关) · PRD §2.6 | +| `screenshots/33-set-usage.png` | 2026-06-02 | SettingsModal 契约4(Usage Credits 块) · PRD §3 计费三维 · matrix §1.2 | +| `screenshots/34-set-connectors.png` | 2026-06-02 | SettingsModal(Connectors/Integrations) · PRD §2.9 Plugins 市场 | +| `screenshots/36-set-my-computer.png` | 2026-06-02 | PRD §2.6(My Computer Cloud/Local 2 tab) | +| `screenshots/37-set-my-plugins.png` | 2026-06-02 | PRD §2.6(My plugins Connectors/Skills/Data sources 3 tab) | +| `screenshots/38-set-skills.png` | 2026-06-02 | PRD §2.9 Skills 清单 · ComposerSkills(Use Skills 对位) | +| `screenshots/39-set-scheduled.png` | 2026-06-02 | PRD §1 左栏 Scheduled[New] · modeChips schedule mode | +| `screenshots/40-set-billing.png` | 2026-06-02 | PRD §3 商业化模型/Usage&Billing | +| `screenshots/43-personalization-profile.png` | 2026-06-02 | AvatarMenu 契约4 · SettingsModal(Personalization) · PRD §2.6(Profile/Knowledge/Custom Instructions) | +| `screenshots/44-account.png` | 2026-06-02 | AvatarMenu 契约4 · SettingsModal(Account) · PRD 调研元数据 | +| `screenshots/45-nav-plugins.png` | 2026-06-02 | PRD §2.9 Plugins 市场 Connectors/Skills · PRD §1 左栏 | +| `screenshots/46-nav-library.png` | 2026-06-02 | PRD §1 左栏导航 Library | +| `screenshots/47-nav-agent.png` | 2026-06-02 | PRD §2.7 Agent 页(Deploy/Telegram/Messenger/Line/WhatsApp/Slack) · PRD §1 左栏 | +| `screenshots/paid-01-home-manus16-full.png` | 2026-06-02 | PRD §2.5.10 line 230-237(付费档满血/~3,852 Credits/stripe) | +| `screenshots/process/mode-06-video-paywall.png` | 2026-06-02 | PRD §2.5.9 line 215-216(Video free 档订阅墙) | +| `screenshots/process/web-01-scaffold-upsell-gate.png` | 2026-06-02 | PRD §2.5.4 line 168-170(模型升级 upsell/并发限流) | +| `screenshots/process/web-06-website-builder-picker.png` | 2026-06-02 | TemplatePickerCard(6 契约+版式) · PRD §2.5.8 line 200-204(模板选择器/Figma/Integrations) | + +### 3.2 medium driftRisk + +| capture | vintage | 依赖 oracle / 文档 | +|---|---|---| +| `screenshots/35-set-access-tokens.png` | 2026-06-02 | PRD §2.6 / matrix §1.2(Access/OpenAPI tokens) | +| `screenshots/41-set-team-access.png` | 2026-06-02 | PRD §2.6 / matrix §1.2(Team settings) | +| `screenshots/42-set-team-usage.png` | 2026-06-02 | PRD §2.6 / matrix §1.2(Team usage) | +| `screenshots/99-mockup-render.png` | 2026-06-02 | (axon 自制 mockup,非 Manus 观测源,仅复刻校对) | +| `screenshots/process/_mockup-task-render.png` | 2026-06-02 | (axon 自制 task-running mockup,非 Manus 观测源) | +| `screenshots/process/mode-04-desktop-app-gated.png` | 2026-06-02 | PRD §2.5.9 line 214(desktop-app Local Folder Access 门) | +| `screenshots/process/mode-05-design-imagegen.png` | 2026-06-02 | PRD §2.5.9 line 215(Design Generating image with GPT Image 2) | +| `screenshots/process/mode-06b-video-paid-generating.png` | 2026-06-02 | PRD §2.5.9(paid video 真生成 keyframe) | +| `screenshots/process/mode-06c-video-paid-editmode.png` | 2026-06-02 | PRD §2.5.9(Video editing mode) | +| `screenshots/process/mode-06d-video-paid-final-player.png` | 2026-06-02 | PRD §2.5.9(Video final player) | +| `screenshots/process/mode-07-schedule-recurring.png` | 2026-06-02 | modeChips schedule mode · PRD §2.3 row 7 / matrix line 217 | +| `screenshots/process/mode-08-wide-research.png` | 2026-06-02 | modeChips wide-research mode | +| `screenshots/process/mode-09-spreadsheet-excel.png` | 2026-06-02 | modeChips spreadsheet · modeRegistry viz/spreadsheet 交付物信念 | +| `screenshots/process/mode-10-visualization.png` | 2026-06-02 | modeRegistry line 213(viz 交付物=interactive webpage) · modeChips viz | +| `screenshots/process/mode-11-audio-musicgen.png` | 2026-06-02 | modeChips audio · PRD §2.5.9 line 221/226(Loading skill music-prompter→chip=skill) | +| `screenshots/process/mode-12-chat-qa.png` | 2026-06-02 | modeRegistry line 245-246(chat=纯对话禁工具) · modeChips chat | +| `screenshots/process/slides-01..04` (compose/research/codegen/cover) | 2026-06-02 | PRD §2.5.5 slides 全程 · §2.5.1 line 132-135 A 内联交付 | +| `screenshots/process/web-02..04` (code/preview/building) | 2026-06-02 | PRD §2.5.1/2.5.2/2.5.3 Computer 工作台交付样例 | +| `screenshots/process/web-05-blueprint-feature-plan.png` | 2026-06-02 | BlueprintCard(6 契约) · PRD §2.5.7 line 192-196 · matrix §1.4 line 137 | +| `screenshots/process/web-07..13` (database-auth/analytics/fulltree/building/authgate/live-dashboard/baas) | 2026-06-02 | PRD §2.5.3 BaaS DB/Auth/Storage/Analytics · matrix §1.4 line 139-140 | +| `screenshots/process/workbench-two-pane-computer.png` | 2026-06-02 | PRD §2.5.2(Task completed/星评/follow-ups) · matrix §1.4 line 143 | +| `recordings/dashboard-live-process.webm` | 2026-06-02 | PRD §2.5.2 对话流时间线 · §2.5.7 Blueprint 卡全程 | +| `recordings/slides-live-process.webm` | 2026-06-02 | PRD §2.5.5 slides 全程 | +| `recordings/video-paid-live-process.webm` | 2026-06-02 | PRD §2.5.9 付费 video 全程 | +| `recordings/website-live-process.webm` | 2026-06-02 | PRD §2.5.2 时间线 · §2.5.8 website 建站全程 | + +### 3.3 low driftRisk(HAR,结构稳定) + +| capture | vintage | 依赖 oracle / 文档 | +|---|---|---| +| `/home/ec2-user/wbj/manus-research/har/manus2.im62.har` | 2026-05-22 | BlueprintCard(confirm_webdev_feature 6 契约) · event-catalog(20 类事件/suggestQualityMode/sandbox 生命周期) · ws_protocol(socket.io 握手/user_message 增量) · blueprint_card(featureConfirm payload) · matrix §1.4 line 137 | +| `/home/ec2-user/wbj/manus-research/har/manus2.im42.har` | 2026-05-22 | grpc_architecture(SkillService/SessionTemplate/Connector 三层) · canvas_architecture(CanvasService/tldraw) · CLAUDE.md「画布/image panel」信念 · modeRegistry line 8-10(chip=激活 mode 非 LLM 路由) | + +> ⚠️ HAR 虽 low drift,但 `sandboxUpdate` 帧的反代域名/端口(`sg1.manus.computer`/5901/8333/3000)属**基础设施层**,距今约 2 周可能随后端调整——见重抓清单 R12(可先从 HAR grep 复核当时值,不一定要重抓)。 + +--- + +## ④ 重抓清单(prioritized checklist) + +给用户稀缺的「登录真 Manus 看一眼」预算。把 §3.1 high driftRisk capture + STALE_ONLY 的 `captureNeeded` 合并,按「对 oracle/护城河决策的影响力」排序。每条写**拍什么界面/什么状态 → 验证哪条主张**。 + +> 抓取技巧通则:① 截图务必**带浏览器地址栏**(验 hash 路由用);② Credits/余额类**只对齐格式不对齐数值**,抓时记下抓取时刻;③ 市场/营销页(Plugins/Agent/Playbook)属高频增删,抓时连同「截图日期」一起记。 + +### P0 — 直接钉死 high-drift oracle 断言(最高优先) + +- [ ] **R1 · 首页 + More 浮窗全展开**(`16-mode-more.png` 重拍)→ 验 modeChips 全 6 契约:pinned 4 / popover 9 / 13 全集 / Playbook 仍带 ↗ / Schedule+Chat 仍在浮窗内。**这是 C1/C2/C4 三个 verdict 的裁决源,也是 mode 系统对齐的命门,第一优先。** +- [ ] **R2 · 模型选择器下拉展开**(`17-model-selector.png` 重拍)→ 验 C3:三档是否仍为 Manus 1.6 Max(Pro)/Manus 1.6(Pro)/Manus 1.6 Lite(免费默认),"Pro" 是否仍是付费徽章而非档名。 +- [ ] **R3 · 头像菜单点开**(`19-avatar-menu.png` 重拍)→ 验 AvatarMenu 全 6 契约:身份块/Free+Upgrade/Credits 千分位/三入口/Sign out。**抓时记下 Credits 值与时刻(验 C5 volatile,不当精确值)。** +- [ ] **R4 · Credits 弹层点开**(`18-credits.png` 重拍)→ 验 §3 商业化:每日刷新 300/16:00 是否仍在;记录 pill 值 vs 弹层值是否仍漂移(C5 铁证维持)。 +- [ ] **R5 · Settings 模态 + 逐 tab 切**(`30/32/33/34` + 子页族 35–41 重拍)→ 验 SettingsModal 契约1-4:左栏 10 section 是否仍为 Account 4 + Features 6;General 4 通信开关默认值;Usage Credits 块。 +- [ ] **R6 · composer + 菜单点开**(`20-composer-plus.png` 重拍)→ 验 ComposerSkills:+ 菜单是否仍恰好 3 项(Add from local files/Use Skills/More)+ 分隔线顺序。 +- [ ] **R7 · website builder 入口**(`web-06-website-builder-picker.png` 重拍)→ 验 TemplatePickerCard:模板网格 5 项是否变动;Add reference/Figma 2 入口是否仍在(决定 SKIP 缺口是否要补)。 + +### P1 — 验证「现实世界/基础设施」类主张(STALE_ONLY,现存 capture 证不了) + +- [ ] **R8 · DevTools Application→Cookies 抓 `session_id`** → 解 JWT header/payload,验「HS256 JWT(email/user_id/exp/jti/team_uid)/域 .manus.im/非 httpOnly/90 天/SameSite=Lax」(STALE: PRD §0 line 17)。**现存全是渲染态/网络帧,无 cookie 快照。** +- [ ] **R9 · Settings 各 tab 带地址栏截图 / 录屏看 URL hash** → 验「hash 路由 `/app#settings/` 而非 `/settings/`」(STALE: PRD §0 line 21)。 +- [ ] **R10 · 登出态访问 `manus.im/app` 跳转录屏 + 落地页加载 HAR** → 验「SPA 在 /app;落地页是独立 Next.js 营销站;bundle 在 `files.manuscdn.com`;登出跳 `/login?redirectUrl=`」(STALE: PRD §0 line 19-20)。 +- [ ] **R11 · `manus.im/app` 初始加载完整 HAR + 实时核实 Manus 运营状态** → 验第三方脚本域名(GTM/Facebook Pixel/Amplitude/Intercom) + Meta 关联/未下架。**⚠️ Meta 收购/下架属现实世界事件,触 [feedback_no_propagate_unverified_realworld_claims]——默认标「未核实」,必须实时源核实不靠记忆**(STALE: PRD §0 line 24)。 +- [ ] **R12 · 重触发全栈任务抓 `sandboxUpdate` 帧**(或先从 `manus2.im62.har` grep `sandboxUpdate` 复核当时值)→ 验云电脑反代域名/端口仍为 `sg1.manus.computer`/5901/8333/3000(STALE: PRD §0 line 22)。**可先 grep HAR 省一次重抓。** +- [ ] **R13 · DevTools Application→Local Storage 抓 `persist:root` / `UserService.*` 键值** → 验 localStorage 配置项清单(STALE: playbook_catalog + PRD §0 line 23)。 + +### P2 — 营销/生态高频增删页(即使 06-02 准确,逐项清单也易变) + +- [ ] **R14 · 付费态 Settings→Usage&Billing/Billing 截图** → 验付费档字段与 `paymentPlatform:stripe`;**Credits 按格式断言不按 ~3,852 具体数**(STALE: PRD §2.5.10/§3)。 +- [ ] **R15 · 复读/重抓 Agent 页**(`47-nav-agent.png`)→ 逐项核 4 卖点文案(Brand-consistent/Persistent memory/Custom skills/Works in your messenger) + Deploy 渠道(Telegram/Messenger/Line/WhatsApp/Slack)(STALE: PRD §2.7)。 +- [ ] **R16 · 复读/重抓 Plugins 市场 + Skills**(`45-nav-plugins.png` + `38-set-skills.png`)→ 逐项核 connector/skill 名(Gmail/Google Drive/Notion/… + github-pr/html-slides-generator/…);third-party 生态高漂移(STALE: PRD §2.9)。 +- [ ] **R17 · Manus Playbook 列表页 + 详情页截图 + CTA href Playwright 实测** → 验「65 个 SEO landing/7 段式模板/CTA 裸 URL 不传 playbookId」(STALE: playbook_catalog)。**SEO 营销页 Manus 高频增删,最易过期。** +- [ ] **R18 · 超长跨主题 session 的 `GetSession` 响应**(`manus2.im52.har` 或新抓长 session)→ 验后端是否返回摘要/裁剪后历史(STALE: feedback_manus_backend_context_optimization,作者自陈未直接观测)。 + +> 共 **18 条** checklist(P0×7 + P1×6 + P2×5)。P0 直接喂 oracle 单测,回抓即可 diff;P1/P2 是补现存 capture 的盲区(cookie/localStorage/第三方脚本/营销页/后端机制)。 + +### ④.5 ★Live-API 已解项(2026-06-04 用户给 Pro token 实证)★ + +> 用户给 Manus Pro token → 直查 `api.manus.im`(Connect-Web JSON + Bearer)做结构化 drift 检测,**把上面清单里「数据型」项从「人工浏览器」降级成「API 自动可查」**,大幅缩短人工重抓预算。方法+endpoint 清单见记忆 [[ops_manus_live_api_drift_detection]](token 不落盘,每次用户给)。 + +**API 直接解决/部分解决的 checklist 项**: +- **R14(付费档/stripe/credits)= 完全 API 解决**:`UserService/UserInfo` 实证 `membershipVersion=pro`(tier20) · `paymentPlatform=stripe` · subscriptionStatus Active · concurrentSessions 20;`GetAvailableCredits` total **3852**/proMonthly 4000/日刷 300。无需重抓 billing 截图。 +- **R8(session_id JWT 结构)= 部分解决**:token 本身即该 HS256 JWT,claims `email/user_id/exp/jti/team_uid/type` 全实证;仍缺 httpOnly/SameSite/90天(需 cookie 快照,那几项是 cookie 属性非 JWT 内容)。 +- **R11(Manus 运营状态)= 部分解决**:`api.manus.im` 全 200 + `manus.im/app` 加载正常 → **Manus 在线运营中(未下架)实时确认**(消解 [[feedback_no_propagate_unverified_realworld_claims]] 那条疑虑);第三方分析脚本域名仍需初始加载 HAR。 + +**API 顺带新查到的当前真值(2026-06-04)**: +- **`ListScheduledTasks` 返回真任务**「Weekly AI industry monitoring report」→ **schedule 是真 Manus 内建功能的 live 铁证**(在 16-mode-more.png 截图之外再加一层 API 实证,二次钉死 C1)。 +- `ListSlideTemplatesPublic`=**26** 个 slide 模板(Etching/Editorial/Pixel/Vellum/Dossier/Whiteboard/Sketch/Glamour…)。 +- `ListSkills`=**0**(此号无自建 skill;与 composer「Use Skills」非同源)。 +- `GetUserClientConfig`=`{}` · `GetGlobalSettings`=`{freeMapreduceCount:1}` → **证实 mode chip 列表不在任何 API,是客户端 CSR 硬编码**。 + +**★R1 已用浏览器复拍执行(2026-06-04,drift 环完整闭合一次)★**:playwright-core addCookies 注 session_id(=token) → goto manus.im/app → 点 More 浮窗 → 抽 mode 列表。结果 = pinned 4(Create slides/Build website/Develop desktop apps/Design) + popover 9(Video/Develop apps/Schedule tasks/Wide Research/Spreadsheet/Visualization/Audio/Chat mode/Playbook↗),**与 2026-06-02 的 16-mode-more.png 逐项完全一致 = 零 drift**。modeChips oracle 的 grounding **截至 2026-06-04 重新确认有效,drift 时钟重置**。附带:Pro 账号顶栏 pill 显示「Manus 1.6」(非 Lite),再次印证 C3 档名 Max/1.6/Lite。方法 = `playwright-core` + `~/.cache/ms-playwright/chromium-1223`,token 走 env 不落盘,账号截图用完即删。 + +**仍待人工/浏览器、当下未做的(API 取不到的 CSR 视觉版式)**:R2(模型档下拉)/R3·R5·R6·R7(头像菜单/Settings/composer/website-builder 纯 UI 版式)——但这批截图 2026-06-02 拍、距今仅 2 天 → drift 风险极低,且 R1 零 drift 间接佐证 app-shell 这两天没大改,可等老化再批量复拍(复用上面 R1 的浏览器法)。 + +**★R15(新增)· 交付卡复拍 → 抓出 chrome 偏差(2026-06-04 第二次浏览器复拍)★**:为解锁 DeliverableCard oracle(唯一有真 producer `website_delivered` 的活面但缺 grounding 截图),点开 Pro 账号「小游戏网站本地部署」任务复拍交付卡 → 存 `process/web-14-delivery-card-2026-06-04.png`。**复拍反而发现 axon DeliverableCard 偏离真 Manus**:真 Manus 交付 = 步骤汇总卡(✓+末步标题+「5/5」步数+折叠⌄+浮动缩略图)+右栏发布预览(Preview/Share/Published/Edit/Made with Manus),**无制品卡无⋯菜单无回滚**;axon 是 IDE 风制品卡(🌐+网站已更新+time·path+仪表盘+⋯[访问/复制路径/回滚])。组件注释自称 grounding 来自「2026-05-21 口述截图」实为掩盖偏差。处置 = 用户拍板**记分歧+锁护城河**:写窄·护城河型 oracle(`DeliverableCard.manus-alignment.test.tsx` 4 契约)只锁真共享(缩略图/点开)+真护城河(git 回滚,Manus 实拍证无),不锁 chrome。**E 层「回真 Manus 重观测」第二次跑出 grounding 价值**(R1=零 drift 确认;R15=抓出旧口述参照掩盖的真偏差)。token 走 env 不落盘,账号截图/视频/旧 token.txt 用完即清。 + +**★R15 续 · chrome 偏差混合收敛(2026-06-04 goal #2)★**:用户再拍板 AskUserQuestion「混合: Manus 外形+护城河抽屉」(关键新前提:goal #1 接通 plan_update 后「步骤汇总」外形才有数据基础)。DeliverableCard 重设:摘要行采 Manus 步骤汇总外形(✓完成勾+折叠 chevron);护城河真 git 回滚从 ⋯ 菜单提到**展开抽屉第一公民**(更可发现);删 ⋯ 菜单。oracle 从「窄·4 契约·不锁 chrome」升「混合·5 契约」——新增**契约5 正向锁采纳了的 Manus 外形**(✓+chevron,曾分歧今对齐),残留分歧(N/N 步数·结果右栏 vs 卡内缩略图)文档化不锁。变异认证恰红契约3(护城河)+5(外形)。机械测 `DeliverableCard.test.tsx` 同步 8→9(删⋯菜单断言、加 chevron 折叠断言)。**配套 producer 接活**(R15 把 DeliverableCard 列为「活面缺 grounding」,本次顺带把另两个「死面」也接活):M19.b plan_update(复用 TodoWrite 映射,驳 spec D2 新造 declare_plan)+M19.c suggested_followups(新 suggest_followups marker),`live-narrative` eval 跨边界实证 plan_update×5+suggested_followups×1 真到 ws 流 → FollowupSuggestions/PlanPanel 死面升活面。 + +> **C5 修正(铁律4,驳本 workflow 自己的建议)**:§6 的 C5 建议「AvatarMenu 契约3 去掉 hardcode 1,097 防回抓失配」**不成立**——该 oracle 是 jsdom 组件测,`credits:{remaining:1097}` 是 **fixture 常量**(测「给定值→渲染千分位格式」),**永不 query live Manus、永不漂移**。**不改 AvatarMenu oracle**。C5 真正有效的部分仅限「PRD 等描述性文档里写具体余额数(~3,852)要标 volatile」——而 API 实证当前恰为 3852(巧合仍准,但本质是 volatile 量)。 + +--- + +## ⑤ Drift-Diff 操作流程(新 capture 到位时怎么用本 manifest) + +当用户登录真 Manus 重抓了一张新 capture(覆盖某个旧 vintage),按此流程把「一张新图」翻译成「哪些 oracle 要重验」: + +1. **定位**:在 §3 capture 陈旧账里按文件名找到这张 capture,读出它的 `依赖 oracle / 文档` 列——这就是 blast radius(影响面)。 +2. **取旧事实**:去 §2 对应 unit 表,把该 capture grounding 的**每一条「编码的 Manus 事实」**逐条抄出来(含契约编号)。 +3. **逐条比对**:新图 vs 旧事实,对每条打三态: + - ✅ **仍成立** → grounding 有效,无需动 oracle。 + - 🔧 **变了** → grounding 失效。记录「旧事实 → 新事实」,标记该契约对应的 oracle 断言**需重写**(同步更新本 manifest §2 该行 + capture vintage)。 + - ⚠️ **新增/消失** → Manus 加了新元素或删了旧元素。若是新元素 → 评估是否要新增对齐契约(先判 deliberateSkip:是商业化触点/死按钮就标 SKIP 不抄);若消失 → 对应契约可能要删。 +4. **更 vintage**:把 §3 该 capture 行的 vintage 改成新抓取日期,driftRisk 时钟归零。 +5. **回写 oracle**:对每条 🔧,去 `oracleFile` 改断言 + fixture,跑 `npm test` 确认绿。**这步改的是 oracle 单测文件,不是本 manifest**——本 manifest 只是索引,单测才是 teeth。 +6. **VERIFIED_VERDICTS 复核**:若新图推翻了某个已确认 verdict(如 C1/C3)或激活了某个 false-positive(C2/C4/C5),同步更新 §6 表。 + +> **核心不变量**:本 manifest 是「capture ↔ 编码事实 ↔ oracle」的三向映射。**只要这张映射是全的,任何一张新截图都能在 30 秒内回答「我要重验哪几个单测」**。映射不全 = drift 盲区。 + +--- + +## ⑥ 已确认的信念 bug 表(来自 VERIFIED_VERDICTS · `isRealBug=true`) + +> ⚠️ 这些 bug 由**主进程审阅后修**,本 agent 不改任何共享文件(CLAUDE.md / modeRegistry / matrix / PRD / oracle 单测)。下表只做台账。 +> VERIFIED_VERDICTS 共 5 条,其中 **2 条 `isRealBug=true`**(C1/C3);C2/C4/C5 经核为 `isRealBug=false`(同截图两种合法计数口径 / 同文件详略并存 / Credits volatile 漂移风险但非事实型 bug),不在修复表,仅在下方备注。 + +### 确认 bug(需修,2 条) + +| id | claim(错误信念) | 正确表述 | fixTargets(主进程修) | +|---|---|---|---| +| **C1** | modeRegistry.ts 与 CLAUDE.md 注释称「schedule / chat 是 axon 新增,Manus 没有」 | Schedule tasks(schedule) 与 Chat mode(chat) 都是 **Manus 1.6 Lite 本身内建** mode,就在 Manus 自己的 More 浮窗里(`16-mode-more.png` 实证);axon 只是同名镜像对齐,非新增 | `docs/manus-parity/manus-product-prd-2026-06-02.md`:line 108 row 7 `🟢(axon 加的也叫 schedule)` → `🟢`;line 113 row 12 `🟢(axon 加)` → `🟢`。与 2026-06-04 已纠正的 modeRegistry.ts / CLAUDE.md / matrix §0.2 同步,消除文档残留 | +| **C3** | matrix §1.1 line 82 把模型选择器记为「Lite/Pro/Max」(把付费徽章 Pro 当模型档名,漏中间档) | 三档=`Manus 1.6 Max(Pro 徽章)` / `Manus 1.6(Pro 徽章)` / `Manus 1.6 Lite(免费默认选中)`;"Pro" 是付费徽章不是档名,"Max" 才是模型名,真实档位 Lite/1.6/Max | `docs/manus-parity/traceability-matrix.md`:§1.1 line 82(17-model-selector 行),Manus 功能列 `模型选择器 Lite/Pro/Max` → `模型选择器三档:Manus 1.6 Max(Pro)/Manus 1.6(Pro)/Manus 1.6 Lite(免费默认)`(视/UE/功能/判定列 ✅/➖/➖/➖ 不变,axon 仍单后端占位) | + +### 经核非 bug(false-positive,仅备注,不修) + +- **C2**(mode 数 12 vs 13):非矛盾,是同截图两种合法计数口径——「12 可激活 task mode + 1 Playbook 外链」。三处来源(CLAUDE.md/PRD/oracle)全与截图一致。建议全库统一句式「12 task mode + Playbook 外链」是**文风 polish 非正确性缺陷**。 +- **C4**(CLAUDE.md line7 vs line190 措辞):非冲突,两处都认同「12 内建 mode」,line7 是北极星结构性简写、line190 是详注带纠错,属同文件详略并存。真正要修的是 PRD line108/113(=C1)。 +- **C5**(Credits 余额 797 vs 1,097):非事实型 bug——claim 对各自引用的截图忠实(`19-avatar-menu.png` 头像菜单确为 1,097)。真问题是**漂移风险**:Credits 是按任务实时扣减的 volatile 量(`18-credits.png` 同图 pill 1,097/弹层 797 即铁证,daily refresh 300/16:00),把整数 1,097 硬编进会重抓的 oracle 必失配。**正确做法**:oracle 应断言「Credits 行存在 + 渲染千分位格式」而非「余额=1,097」;PRD 描述具体余额处统一标「volatile/抓取时刻快照」。**此项虽非 bug 但已在 §2.2 契约3 + 重抓清单 R3/R4 标注 ⚠️,建议主进程顺手硬化 oracle 断言(AvatarMenu 契约3 fixture 去掉 hardcode 1,097)防回抓失配。** + +--- + +## 附:本 manifest 覆盖统计 + +- oracle 单元:**6**(blueprint / AvatarMenu / SettingsModal / modeChips / TemplatePicker / ComposerSkills) +- 编码的 Manus 事实条目:**38**(含 8 条 deliberateSkip) +- capture 入账:**70**(high 25 · medium 28+ · low 2 HAR · 含录屏 4 + mockup 2 非观测源) +- 确认信念 bug:**2**(C1 / C3,留主进程修)· 经核非 bug:3(C2/C4/C5) +- 重抓 checklist:**18 条**(P0×7 / P1×6 / P2×5) diff --git a/docs/manus-parity/screenshots/02-app-home.png b/docs/manus-parity/screenshots/02-app-home.png new file mode 100644 index 00000000..fa5f0b42 Binary files /dev/null and b/docs/manus-parity/screenshots/02-app-home.png differ diff --git a/docs/manus-parity/screenshots/16-mode-more.png b/docs/manus-parity/screenshots/16-mode-more.png new file mode 100644 index 00000000..2201f43a Binary files /dev/null and b/docs/manus-parity/screenshots/16-mode-more.png differ diff --git a/docs/manus-parity/screenshots/17-model-selector.png b/docs/manus-parity/screenshots/17-model-selector.png new file mode 100644 index 00000000..43bd9a92 Binary files /dev/null and b/docs/manus-parity/screenshots/17-model-selector.png differ diff --git a/docs/manus-parity/screenshots/18-credits.png b/docs/manus-parity/screenshots/18-credits.png new file mode 100644 index 00000000..c53aeefb Binary files /dev/null and b/docs/manus-parity/screenshots/18-credits.png differ diff --git a/docs/manus-parity/screenshots/19-avatar-menu.png b/docs/manus-parity/screenshots/19-avatar-menu.png new file mode 100644 index 00000000..733d09d6 Binary files /dev/null and b/docs/manus-parity/screenshots/19-avatar-menu.png differ diff --git a/docs/manus-parity/screenshots/20-composer-plus.png b/docs/manus-parity/screenshots/20-composer-plus.png new file mode 100644 index 00000000..bd173892 Binary files /dev/null and b/docs/manus-parity/screenshots/20-composer-plus.png differ diff --git a/docs/manus-parity/screenshots/22-open-task.png b/docs/manus-parity/screenshots/22-open-task.png new file mode 100644 index 00000000..7ee40589 Binary files /dev/null and b/docs/manus-parity/screenshots/22-open-task.png differ diff --git a/docs/manus-parity/screenshots/23-task-workbench.png b/docs/manus-parity/screenshots/23-task-workbench.png new file mode 100644 index 00000000..7ee40589 Binary files /dev/null and b/docs/manus-parity/screenshots/23-task-workbench.png differ diff --git a/docs/manus-parity/screenshots/30-settings-open.png b/docs/manus-parity/screenshots/30-settings-open.png new file mode 100644 index 00000000..c4f3f39b Binary files /dev/null and b/docs/manus-parity/screenshots/30-settings-open.png differ diff --git a/docs/manus-parity/screenshots/31-settings-data-controls.png b/docs/manus-parity/screenshots/31-settings-data-controls.png new file mode 100644 index 00000000..1e90dee7 Binary files /dev/null and b/docs/manus-parity/screenshots/31-settings-data-controls.png differ diff --git a/docs/manus-parity/screenshots/32-settings-general.png b/docs/manus-parity/screenshots/32-settings-general.png new file mode 100644 index 00000000..90f272bf Binary files /dev/null and b/docs/manus-parity/screenshots/32-settings-general.png differ diff --git a/docs/manus-parity/screenshots/33-set-usage.png b/docs/manus-parity/screenshots/33-set-usage.png new file mode 100644 index 00000000..5dab952e Binary files /dev/null and b/docs/manus-parity/screenshots/33-set-usage.png differ diff --git a/docs/manus-parity/screenshots/34-set-connectors.png b/docs/manus-parity/screenshots/34-set-connectors.png new file mode 100644 index 00000000..5960fc58 Binary files /dev/null and b/docs/manus-parity/screenshots/34-set-connectors.png differ diff --git a/docs/manus-parity/screenshots/36-set-my-computer.png b/docs/manus-parity/screenshots/36-set-my-computer.png new file mode 100644 index 00000000..be4871a9 Binary files /dev/null and b/docs/manus-parity/screenshots/36-set-my-computer.png differ diff --git a/docs/manus-parity/screenshots/37-set-my-plugins.png b/docs/manus-parity/screenshots/37-set-my-plugins.png new file mode 100644 index 00000000..5960fc58 Binary files /dev/null and b/docs/manus-parity/screenshots/37-set-my-plugins.png differ diff --git a/docs/manus-parity/screenshots/38-set-skills.png b/docs/manus-parity/screenshots/38-set-skills.png new file mode 100644 index 00000000..c83dfc95 Binary files /dev/null and b/docs/manus-parity/screenshots/38-set-skills.png differ diff --git a/docs/manus-parity/screenshots/39-set-scheduled.png b/docs/manus-parity/screenshots/39-set-scheduled.png new file mode 100644 index 00000000..44225847 Binary files /dev/null and b/docs/manus-parity/screenshots/39-set-scheduled.png differ diff --git a/docs/manus-parity/screenshots/40-set-billing.png b/docs/manus-parity/screenshots/40-set-billing.png new file mode 100644 index 00000000..f4bde4df Binary files /dev/null and b/docs/manus-parity/screenshots/40-set-billing.png differ diff --git a/docs/manus-parity/screenshots/41-set-team-access.png b/docs/manus-parity/screenshots/41-set-team-access.png new file mode 100644 index 00000000..84213121 Binary files /dev/null and b/docs/manus-parity/screenshots/41-set-team-access.png differ diff --git a/docs/manus-parity/screenshots/42-set-team-usage.png b/docs/manus-parity/screenshots/42-set-team-usage.png new file mode 100644 index 00000000..fed80401 Binary files /dev/null and b/docs/manus-parity/screenshots/42-set-team-usage.png differ diff --git a/docs/manus-parity/screenshots/43-personalization-profile.png b/docs/manus-parity/screenshots/43-personalization-profile.png new file mode 100644 index 00000000..33bfb924 Binary files /dev/null and b/docs/manus-parity/screenshots/43-personalization-profile.png differ diff --git a/docs/manus-parity/screenshots/44-account.png b/docs/manus-parity/screenshots/44-account.png new file mode 100644 index 00000000..ec1feaac Binary files /dev/null and b/docs/manus-parity/screenshots/44-account.png differ diff --git a/docs/manus-parity/screenshots/45-nav-plugins.png b/docs/manus-parity/screenshots/45-nav-plugins.png new file mode 100644 index 00000000..b20b0fa4 Binary files /dev/null and b/docs/manus-parity/screenshots/45-nav-plugins.png differ diff --git a/docs/manus-parity/screenshots/46-nav-library.png b/docs/manus-parity/screenshots/46-nav-library.png new file mode 100644 index 00000000..56a0a7e9 Binary files /dev/null and b/docs/manus-parity/screenshots/46-nav-library.png differ diff --git a/docs/manus-parity/screenshots/47-nav-agent.png b/docs/manus-parity/screenshots/47-nav-agent.png new file mode 100644 index 00000000..5f107876 Binary files /dev/null and b/docs/manus-parity/screenshots/47-nav-agent.png differ diff --git a/docs/manus-parity/screenshots/99-mockup-render.png b/docs/manus-parity/screenshots/99-mockup-render.png new file mode 100644 index 00000000..4a0f44bd Binary files /dev/null and b/docs/manus-parity/screenshots/99-mockup-render.png differ diff --git a/docs/manus-parity/screenshots/paid-01-home-manus16-full.png b/docs/manus-parity/screenshots/paid-01-home-manus16-full.png new file mode 100644 index 00000000..bfc7aa5b Binary files /dev/null and b/docs/manus-parity/screenshots/paid-01-home-manus16-full.png differ diff --git a/docs/manus-parity/screenshots/process/_mockup-task-render.png b/docs/manus-parity/screenshots/process/_mockup-task-render.png new file mode 100644 index 00000000..58fd622b Binary files /dev/null and b/docs/manus-parity/screenshots/process/_mockup-task-render.png differ diff --git a/docs/manus-parity/screenshots/process/mode-04-desktop-app-gated.png b/docs/manus-parity/screenshots/process/mode-04-desktop-app-gated.png new file mode 100644 index 00000000..30eaffcd Binary files /dev/null and b/docs/manus-parity/screenshots/process/mode-04-desktop-app-gated.png differ diff --git a/docs/manus-parity/screenshots/process/mode-05-design-imagegen.png b/docs/manus-parity/screenshots/process/mode-05-design-imagegen.png new file mode 100644 index 00000000..005898f0 Binary files /dev/null and b/docs/manus-parity/screenshots/process/mode-05-design-imagegen.png differ diff --git a/docs/manus-parity/screenshots/process/mode-06-video-paywall.png b/docs/manus-parity/screenshots/process/mode-06-video-paywall.png new file mode 100644 index 00000000..8a1e43f2 Binary files /dev/null and b/docs/manus-parity/screenshots/process/mode-06-video-paywall.png differ diff --git a/docs/manus-parity/screenshots/process/mode-06b-video-paid-generating.png b/docs/manus-parity/screenshots/process/mode-06b-video-paid-generating.png new file mode 100644 index 00000000..2c956200 Binary files /dev/null and b/docs/manus-parity/screenshots/process/mode-06b-video-paid-generating.png differ diff --git a/docs/manus-parity/screenshots/process/mode-06c-video-paid-editmode.png b/docs/manus-parity/screenshots/process/mode-06c-video-paid-editmode.png new file mode 100644 index 00000000..177b192e Binary files /dev/null and b/docs/manus-parity/screenshots/process/mode-06c-video-paid-editmode.png differ diff --git a/docs/manus-parity/screenshots/process/mode-06d-video-paid-final-player.png b/docs/manus-parity/screenshots/process/mode-06d-video-paid-final-player.png new file mode 100644 index 00000000..bf85cc3f Binary files /dev/null and b/docs/manus-parity/screenshots/process/mode-06d-video-paid-final-player.png differ diff --git a/docs/manus-parity/screenshots/process/mode-07-schedule-recurring.png b/docs/manus-parity/screenshots/process/mode-07-schedule-recurring.png new file mode 100644 index 00000000..3a1c0a75 Binary files /dev/null and b/docs/manus-parity/screenshots/process/mode-07-schedule-recurring.png differ diff --git a/docs/manus-parity/screenshots/process/mode-08-wide-research.png b/docs/manus-parity/screenshots/process/mode-08-wide-research.png new file mode 100644 index 00000000..7e722639 Binary files /dev/null and b/docs/manus-parity/screenshots/process/mode-08-wide-research.png differ diff --git a/docs/manus-parity/screenshots/process/mode-09-spreadsheet-excel.png b/docs/manus-parity/screenshots/process/mode-09-spreadsheet-excel.png new file mode 100644 index 00000000..20ce48d7 Binary files /dev/null and b/docs/manus-parity/screenshots/process/mode-09-spreadsheet-excel.png differ diff --git a/docs/manus-parity/screenshots/process/mode-10-visualization.png b/docs/manus-parity/screenshots/process/mode-10-visualization.png new file mode 100644 index 00000000..0106d951 Binary files /dev/null and b/docs/manus-parity/screenshots/process/mode-10-visualization.png differ diff --git a/docs/manus-parity/screenshots/process/mode-11-audio-musicgen.png b/docs/manus-parity/screenshots/process/mode-11-audio-musicgen.png new file mode 100644 index 00000000..cdfa414d Binary files /dev/null and b/docs/manus-parity/screenshots/process/mode-11-audio-musicgen.png differ diff --git a/docs/manus-parity/screenshots/process/mode-12-chat-qa.png b/docs/manus-parity/screenshots/process/mode-12-chat-qa.png new file mode 100644 index 00000000..8e9f0976 Binary files /dev/null and b/docs/manus-parity/screenshots/process/mode-12-chat-qa.png differ diff --git a/docs/manus-parity/screenshots/process/slides-01-compose.png b/docs/manus-parity/screenshots/process/slides-01-compose.png new file mode 100644 index 00000000..79d4a50c Binary files /dev/null and b/docs/manus-parity/screenshots/process/slides-01-compose.png differ diff --git a/docs/manus-parity/screenshots/process/slides-02-research-streaming.png b/docs/manus-parity/screenshots/process/slides-02-research-streaming.png new file mode 100644 index 00000000..77cb4145 Binary files /dev/null and b/docs/manus-parity/screenshots/process/slides-02-research-streaming.png differ diff --git a/docs/manus-parity/screenshots/process/slides-03-deck-codegen.png b/docs/manus-parity/screenshots/process/slides-03-deck-codegen.png new file mode 100644 index 00000000..550f78a7 Binary files /dev/null and b/docs/manus-parity/screenshots/process/slides-03-deck-codegen.png differ diff --git a/docs/manus-parity/screenshots/process/slides-04-cover-rendered.png b/docs/manus-parity/screenshots/process/slides-04-cover-rendered.png new file mode 100644 index 00000000..e3c50465 Binary files /dev/null and b/docs/manus-parity/screenshots/process/slides-04-cover-rendered.png differ diff --git a/docs/manus-parity/screenshots/process/web-01-scaffold-upsell-gate.png b/docs/manus-parity/screenshots/process/web-01-scaffold-upsell-gate.png new file mode 100644 index 00000000..84d21073 Binary files /dev/null and b/docs/manus-parity/screenshots/process/web-01-scaffold-upsell-gate.png differ diff --git a/docs/manus-parity/screenshots/process/web-02-computer-panel-code.png b/docs/manus-parity/screenshots/process/web-02-computer-panel-code.png new file mode 100644 index 00000000..46ae42cf Binary files /dev/null and b/docs/manus-parity/screenshots/process/web-02-computer-panel-code.png differ diff --git a/docs/manus-parity/screenshots/process/web-03-computer-panel-preview-done.png b/docs/manus-parity/screenshots/process/web-03-computer-panel-preview-done.png new file mode 100644 index 00000000..0bf4468f Binary files /dev/null and b/docs/manus-parity/screenshots/process/web-03-computer-panel-preview-done.png differ diff --git a/docs/manus-parity/screenshots/process/web-04-building.png b/docs/manus-parity/screenshots/process/web-04-building.png new file mode 100644 index 00000000..0bf4468f Binary files /dev/null and b/docs/manus-parity/screenshots/process/web-04-building.png differ diff --git a/docs/manus-parity/screenshots/process/web-05-blueprint-feature-plan.png b/docs/manus-parity/screenshots/process/web-05-blueprint-feature-plan.png new file mode 100644 index 00000000..4b5efcd5 Binary files /dev/null and b/docs/manus-parity/screenshots/process/web-05-blueprint-feature-plan.png differ diff --git a/docs/manus-parity/screenshots/process/web-06-website-builder-picker.png b/docs/manus-parity/screenshots/process/web-06-website-builder-picker.png new file mode 100644 index 00000000..63735b67 Binary files /dev/null and b/docs/manus-parity/screenshots/process/web-06-website-builder-picker.png differ diff --git a/docs/manus-parity/screenshots/process/web-07-computer-database-auth.png b/docs/manus-parity/screenshots/process/web-07-computer-database-auth.png new file mode 100644 index 00000000..f7d3ab61 Binary files /dev/null and b/docs/manus-parity/screenshots/process/web-07-computer-database-auth.png differ diff --git a/docs/manus-parity/screenshots/process/web-08-computer-analytics.png b/docs/manus-parity/screenshots/process/web-08-computer-analytics.png new file mode 100644 index 00000000..bb1df33f Binary files /dev/null and b/docs/manus-parity/screenshots/process/web-08-computer-analytics.png differ diff --git a/docs/manus-parity/screenshots/process/web-09-computer-code-fulltree.png b/docs/manus-parity/screenshots/process/web-09-computer-code-fulltree.png new file mode 100644 index 00000000..fc6a1407 Binary files /dev/null and b/docs/manus-parity/screenshots/process/web-09-computer-code-fulltree.png differ diff --git a/docs/manus-parity/screenshots/process/web-10-computer-building.png b/docs/manus-parity/screenshots/process/web-10-computer-building.png new file mode 100644 index 00000000..a5edb9f1 Binary files /dev/null and b/docs/manus-parity/screenshots/process/web-10-computer-building.png differ diff --git a/docs/manus-parity/screenshots/process/web-11-crm-completed-authgate.png b/docs/manus-parity/screenshots/process/web-11-crm-completed-authgate.png new file mode 100644 index 00000000..2224244c Binary files /dev/null and b/docs/manus-parity/screenshots/process/web-11-crm-completed-authgate.png differ diff --git a/docs/manus-parity/screenshots/process/web-12-crm-live-dashboard.png b/docs/manus-parity/screenshots/process/web-12-crm-live-dashboard.png new file mode 100644 index 00000000..3d7e3019 Binary files /dev/null and b/docs/manus-parity/screenshots/process/web-12-crm-live-dashboard.png differ diff --git a/docs/manus-parity/screenshots/process/web-13-crm-baas-features.png b/docs/manus-parity/screenshots/process/web-13-crm-baas-features.png new file mode 100644 index 00000000..845ffd15 Binary files /dev/null and b/docs/manus-parity/screenshots/process/web-13-crm-baas-features.png differ diff --git a/docs/manus-parity/screenshots/process/web-14-delivery-card-2026-06-04.png b/docs/manus-parity/screenshots/process/web-14-delivery-card-2026-06-04.png new file mode 100644 index 00000000..7736a16e Binary files /dev/null and b/docs/manus-parity/screenshots/process/web-14-delivery-card-2026-06-04.png differ diff --git a/docs/manus-parity/screenshots/process/workbench-two-pane-computer.png b/docs/manus-parity/screenshots/process/workbench-two-pane-computer.png new file mode 100644 index 00000000..fb7eb116 Binary files /dev/null and b/docs/manus-parity/screenshots/process/workbench-two-pane-computer.png differ diff --git a/docs/manus-parity/scripts/README.md b/docs/manus-parity/scripts/README.md new file mode 100644 index 00000000..69552c3a --- /dev/null +++ b/docs/manus-parity/scripts/README.md @@ -0,0 +1,31 @@ +# Manus 登录态复刻脚本(可复跑) + +2026-06-02 用「真登录态」反推 Manus 全功能的脚本。**核心方法 = cookie 注入**,不是密码登录。 + +## 原理(破解结论) +Manus 会话鉴权 = cookie **`session_id`**(一个 HS256 JWT,域 `.manus.im`,非 httpOnly,90 天)。 +前端 `getToken()` 从 `document.cookie` 读它 → 作 `Authorization: Bearer` 打 `api.manus.im`。 +所以拿到这个 JWT,用 Playwright `addCookies` 注入 `session_id` + `login_success=1` 即获得完整登录态。 + +## 前置 +```bash +# 1) 把你的 Manus session_id JWT 写到 token 文件(脚本从这里读,不硬编码) +printf '%s' '<你的 session_id JWT>' > /tmp/manus-token.txt +# 2) 需要 playwright-core(仓库已装) + chromium(~/.cache/ms-playwright) +``` + +## 脚本 +| 文件 | 作用 | +|---|---| +| `manus-discover2.mjs` | 拦截 /app bundle 反推 token 存储机制(产出破解结论) | +| `manus-login.mjs` | 注入 cookie 验证登录 + dump userInfo | +| `manus-capture.mjs` | 全功能巡游截图(首页/mode/菜单/打开真实任务) | +| `manus-capture2.mjs` | Settings hash 路由直达 + 左栏导航页 | +| `manus-settings.mjs` | Settings 模态逐 tab 截图 | + +```bash +cd /home/ec2-user/wbj/axon && node docs/manus-parity/scripts/manus-login.mjs +# 截图落到 /tmp/manus-shots/,用 Read 工具看 +``` + +> ⚠️ token 是真凭证:用完去 Manus 重新登录可踢掉该 session。脚本本身不含 token(从 /tmp 读)。 diff --git a/docs/manus-parity/scripts/manus-capture.mjs b/docs/manus-parity/scripts/manus-capture.mjs new file mode 100644 index 00000000..4a014419 --- /dev/null +++ b/docs/manus-parity/scripts/manus-capture.mjs @@ -0,0 +1,92 @@ +// Manus 全功能巡游截图(M-parity 对齐素材)。 +// 复用破解结论:注入 session_id + login_success cookie 即登录。 +// 两阶段:① 直接导航各路由(最稳)② 回 /app 点 mode chips/菜单/开真实任务看 workbench。 +import fs from 'node:fs'; +import { createRequire } from 'node:module'; +const require = createRequire('/home/ec2-user/wbj/axon/package.json'); +const { chromium } = require('playwright-core'); + +const TOKEN = fs.readFileSync('/tmp/manus-token.txt', 'utf8').trim(); +const EXEC = '/home/ec2-user/.cache/ms-playwright/chromium-1223/chrome-linux64/chrome'; +const OUT = '/tmp/manus-shots'; +fs.mkdirSync(OUT, { recursive: true }); +const log = (...a) => console.log('[cap]', ...a); + +const browser = await chromium.launch({ + executablePath: EXEC, headless: true, + args: ['--no-sandbox', '--disable-blink-features=AutomationControlled', '--disable-dev-shm-usage'], +}); +const ctx = await browser.newContext({ + viewport: { width: 1440, height: 900 }, + userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36', + locale: 'en-US', +}); +await ctx.addCookies([ + { name: 'session_id', value: TOKEN, domain: '.manus.im', path: '/', secure: true, httpOnly: false, sameSite: 'Lax', expires: 1787205573 }, + { name: 'login_success', value: '1', domain: '.manus.im', path: '/', secure: true, httpOnly: false, sameSite: 'Lax', expires: 1787205573 }, +]); +const page = await ctx.newPage(); + +async function shot(name) { try { await page.screenshot({ path: `${OUT}/${name}.png` }); log('shot', name, '@', page.url()); } catch (e) { log('shot FAIL', name, e.message); } } +async function go(name, url) { + try { + await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 40000 }); + await page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {}); + await page.waitForTimeout(2500); + await shot(name); + } catch (e) { log('go FAIL', name, e.message); await shot(name + '-err'); } +} + +// ---- 阶段1:直接导航路由 ---- +const routes = [ + ['03-settings-usage', 'https://manus.im/settings/usage'], + ['04-settings-connectors', 'https://manus.im/settings/connectors'], + ['05-settings-access-tokens', 'https://manus.im/settings/access-tokens'], + ['06-settings-data-controls', 'https://manus.im/settings/data-controls'], + ['07-settings-my-computer', 'https://manus.im/settings/my-computer'], + ['08-settings-my-plugins', 'https://manus.im/settings/my-plugins'], + ['09-settings-skills', 'https://manus.im/settings/skills'], + ['10-settings-scheduled-tasks', 'https://manus.im/settings/scheduled-tasks'], + ['11-settings-billing', 'https://manus.im/settings/website-usage-billing'], + ['12-settings-team-access', 'https://manus.im/settings/team-access-controls'], + ['13-integrations', 'https://manus.im/integrations'], + ['14-scheduled-tasks', 'https://manus.im/scheduled-tasks'], +]; +for (const [n, u] of routes) await go(n, u); + +// ---- 阶段2:回 /app 交互 ---- +await go('15-app-home', 'https://manus.im/app'); + +async function clickTry(name, fn) { + try { await fn(); await page.waitForTimeout(1500); await shot(name); } + catch (e) { log('click FAIL', name, e.message); } + try { await page.keyboard.press('Escape'); await page.waitForTimeout(400); } catch {} +} +// mode chips 全集 (More) +await clickTry('16-mode-more', async () => { await page.getByText('More', { exact: true }).first().click({ timeout: 6000 }); }); +// 模型选择器 +await clickTry('17-model-selector', async () => { await page.getByText('Manus 1.6 Lite', { exact: false }).first().click({ timeout: 6000 }); }); +// 积分/升级 +await clickTry('18-credits', async () => { await page.getByText('1,097', { exact: false }).first().click({ timeout: 6000 }); }); +// 头像菜单(右上角坐标兜底) +await clickTry('19-avatar-menu', async () => { await page.mouse.click(1404, 27); }); +// composer + 附件菜单 +await clickTry('20-composer-plus', async () => { await page.locator('textarea, [contenteditable]').first().click({ timeout: 4000 }).catch(()=>{}); await page.mouse.click(515, 423); }); +// 打开一个真实历史任务看 workbench(读 .first 任务行) +await go('21-app-home2', 'https://manus.im/app'); +await clickTry('22-open-task', async () => { await page.getByText('你的能力与约束是什么', { exact: false }).first().click({ timeout: 8000 }); await page.waitForTimeout(3500); }); +await shot('23-task-workbench'); + +// 顺带 dump 一份 userInfo + connectors 配置(PRD 用) +try { + const meta = await page.evaluate(() => ({ + userInfo: localStorage.getItem('UserService.userInfo'), + userClientConfig: localStorage.getItem('UserService.userClientConfig'), + connectors: localStorage.getItem('manus-connectors'), + })); + fs.writeFileSync(`${OUT}/_meta.json`, JSON.stringify(meta, null, 2)); + log('meta dumped'); +} catch (e) { log('meta FAIL', e.message); } + +await browser.close(); +log('DONE'); diff --git a/docs/manus-parity/scripts/manus-capture2.mjs b/docs/manus-parity/scripts/manus-capture2.mjs new file mode 100644 index 00000000..db3cb0c5 --- /dev/null +++ b/docs/manus-parity/scripts/manus-capture2.mjs @@ -0,0 +1,62 @@ +// Manus settings hash 路由直达 + 左栏导航页截图。 +// 关键:settings 是 /app#settings/ hash 路由;每次 about:blank→hashUrl 强制重启读 hash。 +import fs from 'node:fs'; +import { createRequire } from 'node:module'; +const require = createRequire('/home/ec2-user/wbj/axon/package.json'); +const { chromium } = require('playwright-core'); +const TOKEN = fs.readFileSync('/tmp/manus-token.txt', 'utf8').trim(); +const EXEC = '/home/ec2-user/.cache/ms-playwright/chromium-1223/chrome-linux64/chrome'; +const OUT = '/tmp/manus-shots'; +const log = (...a) => console.log('[c2]', ...a); + +const browser = await chromium.launch({ executablePath: EXEC, headless: true, args: ['--no-sandbox', '--disable-dev-shm-usage'] }); +const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 }, userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36', locale: 'en-US' }); +await ctx.addCookies([ + { name: 'session_id', value: TOKEN, domain: '.manus.im', path: '/', secure: true, httpOnly: false, sameSite: 'Lax', expires: 1787205573 }, + { name: 'login_success', value: '1', domain: '.manus.im', path: '/', secure: true, httpOnly: false, sameSite: 'Lax', expires: 1787205573 }, +]); +const page = await ctx.newPage(); +const shot = async (n) => { try { await page.screenshot({ path: `${OUT}/${n}.png` }); log('shot', n, '@', page.url()); } catch (e) { log('shot FAIL', n, e.message); } }; +const goHash = async (n, hash) => { + try { + await page.goto('about:blank'); + await page.goto('https://manus.im/app' + hash, { waitUntil: 'domcontentloaded', timeout: 40000 }); + await page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {}); + await page.waitForTimeout(2800); + await shot(n); + } catch (e) { log('goHash FAIL', n, e.message); await shot(n + '-err'); } +}; + +const settings = [ + ['33-set-usage', '#settings/usage'], + ['34-set-connectors', '#settings/connectors'], + ['35-set-access-tokens', '#settings/access-tokens'], + ['36-set-my-computer', '#settings/my-computer'], + ['37-set-my-plugins', '#settings/my-plugins'], + ['38-set-skills', '#settings/skills'], + ['39-set-scheduled', '#settings/scheduled-tasks'], + ['40-set-billing', '#settings/website-usage-billing'], + ['41-set-team-access', '#settings/team-access-controls'], + ['42-set-team-usage', '#settings/team-usage'], + ['43-personalization-profile', '#settings/personalization-center/profile'], + ['44-account', '#settings/account'], +]; +for (const [n, h] of settings) await goHash(n, h); + +// 左栏导航页:click 文本 +async function clickNav(n, label) { + try { + await page.goto('https://manus.im/app', { waitUntil: 'domcontentloaded', timeout: 40000 }); + await page.waitForLoadState('networkidle', { timeout: 12000 }).catch(() => {}); + await page.waitForTimeout(2500); + await page.getByText(label, { exact: true }).first().click({ timeout: 6000 }); + await page.waitForTimeout(2500); + await shot(n); + } catch (e) { log('clickNav FAIL', n, e.message); await shot(n + '-err'); } +} +await clickNav('45-nav-plugins', 'Plugins'); +await clickNav('46-nav-library', 'Library'); +await clickNav('47-nav-agent', 'Agent'); + +await browser.close(); +log('DONE'); diff --git a/docs/manus-parity/scripts/manus-discover2.mjs b/docs/manus-parity/scripts/manus-discover2.mjs new file mode 100644 index 00000000..28706961 --- /dev/null +++ b/docs/manus-parity/scripts/manus-discover2.mjs @@ -0,0 +1,69 @@ +// 发现 v2:定位 Manus App 的 token 存储 key。 +// 思路:/app bundle 在 CDN 跨域,页内 fetch 被 CORS 挡 → 改用 response 拦截读 JS 正文(CDP 不受 CORS 限制)。 +import fs from 'node:fs'; +import { createRequire } from 'node:module'; +const require = createRequire('/home/ec2-user/wbj/axon/package.json'); +const { chromium } = require('playwright-core'); + +const EXEC = '/home/ec2-user/.cache/ms-playwright/chromium-1223/chrome-linux64/chrome'; +const OUT = '/tmp/manus-shots'; +fs.mkdirSync(OUT, { recursive: true }); + +const browser = await chromium.launch({ + executablePath: EXEC, headless: true, + args: ['--no-sandbox', '--disable-blink-features=AutomationControlled', '--disable-dev-shm-usage'], +}); +const ctx = await browser.newContext({ + viewport: { width: 1440, height: 900 }, + userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36', + locale: 'en-US', +}); +const page = await ctx.newPage(); +const jsBodies = []; +page.on('response', async (res) => { + try { + const ct = (res.headers()['content-type'] || ''); + if (res.url().endsWith('.js') || ct.includes('javascript')) { + const t = await res.text(); + if (t && t.length) jsBodies.push(t); + } + } catch { /* body 不可读跳过 */ } +}); + +const log = (...a) => console.log('[d2]', ...a); +try { + const resp = await page.goto('https://manus.im/app', { waitUntil: 'domcontentloaded', timeout: 45000 }); + log('initial status', resp && resp.status()); + await page.waitForLoadState('networkidle', { timeout: 20000 }).catch(() => {}); + await page.waitForTimeout(4000); + const url = page.url(); + let title = ''; try { title = await page.title(); } catch {} + log('landed url', url, 'title', JSON.stringify(title)); + try { await page.screenshot({ path: `${OUT}/01-app-loggedout.png` }); } catch {} + + // 聚合所有 JS 正文,抠 localStorage key + cookie 设置 + Bearer 用法 + const blob = jsBodies.join('\n'); + fs.writeFileSync('/tmp/manus-app-bundle.js', blob); + log('captured JS bytes:', blob.length, 'files:', jsBodies.length, '-> /tmp/manus-app-bundle.js'); + const grab = (re, n = 60) => { const s = new Set(); let m, c = 0; while ((m = re.exec(blob)) && c < n) { s.add(m[1]); c++; } return [...s]; }; + const lsKeys = grab(/localStorage\.(?:get|set|remove)Item\(\s*["'`]([^"'`]+)["'`]/g); + const lsIdx = grab(/localStorage\[\s*["'`]([^"'`]+)["'`]\s*\]/g); + const cookieSet = grab(/document\.cookie\s*=\s*["'`]?([A-Za-z0-9_\-]+)=/g, 20); + const authHdr = grab(/["'`]([Aa]uthorization)["'`]\s*[:,]/g, 10); + const bearerCtx = grab(/(.{0,40}Bearer\s*[$`'"+].{0,30})/g, 8); + const setItemTokenCtx = grab(/(setItem\([^)]{0,80}(?:token|Token|auth|Auth|jwt|JWT)[^)]{0,40})/g, 15); + log('result:\n' + JSON.stringify({ + localStorageKeys: lsKeys, + localStorageIdxKeys: lsIdx, + cookieSetNames: cookieSet, + authorizationHeaderRefs: authHdr, + bearerUsage: bearerCtx, + setItemTokenContexts: setItemTokenCtx, + currentLSafterLoad: await page.evaluate(() => Object.keys(localStorage)).catch(() => 'eval-failed'), + }, null, 2)); +} catch (e) { + log('ERROR', e.message); + try { await page.screenshot({ path: `${OUT}/01-error.png` }); } catch {} +} finally { + await browser.close(); +} diff --git a/docs/manus-parity/scripts/manus-login.mjs b/docs/manus-parity/scripts/manus-login.mjs new file mode 100644 index 00000000..e0823a47 --- /dev/null +++ b/docs/manus-parity/scripts/manus-login.mjs @@ -0,0 +1,50 @@ +// Manus 登录态注入 + 验证。 +// 破解结论:会话 token = cookie session_id(JWT,域 .manus.im,非 httpOnly), +// 前端 getToken() 读它作 Bearer 打 api.manus.im;login_success=1 是客户端登录态门。 +import fs from 'node:fs'; +import { createRequire } from 'node:module'; +const require = createRequire('/home/ec2-user/wbj/axon/package.json'); +const { chromium } = require('playwright-core'); + +const TOKEN = fs.readFileSync('/tmp/manus-token.txt', 'utf8').trim(); +const EXEC = '/home/ec2-user/.cache/ms-playwright/chromium-1223/chrome-linux64/chrome'; +const OUT = '/tmp/manus-shots'; +fs.mkdirSync(OUT, { recursive: true }); + +const browser = await chromium.launch({ + executablePath: EXEC, headless: true, + args: ['--no-sandbox', '--disable-blink-features=AutomationControlled', '--disable-dev-shm-usage'], +}); +const ctx = await browser.newContext({ + viewport: { width: 1440, height: 900 }, + userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36', + locale: 'en-US', +}); +// 核心:注入 session_id(JWT) + login_success cookie 到 .manus.im +await ctx.addCookies([ + { name: 'session_id', value: TOKEN, domain: '.manus.im', path: '/', secure: true, httpOnly: false, sameSite: 'Lax', expires: 1787205573 }, + { name: 'login_success', value: '1', domain: '.manus.im', path: '/', secure: true, httpOnly: false, sameSite: 'Lax', expires: 1787205573 }, +]); +const page = await ctx.newPage(); +const log = (...a) => console.log('[login]', ...a); +try { + await page.goto('https://manus.im/app', { waitUntil: 'domcontentloaded', timeout: 45000 }); + await page.waitForLoadState('networkidle', { timeout: 25000 }).catch(() => {}); + await page.waitForTimeout(4000); + const url = page.url(); + let title = ''; try { title = await page.title(); } catch {} + log('landed url:', url, 'title:', JSON.stringify(title)); + await page.screenshot({ path: `${OUT}/02-app-home.png`, fullPage: false }); + // 读登录态证据:userInfo + authStatus + const ev = await page.evaluate(() => { + let ui = null; try { ui = localStorage.getItem('UserService.userInfo'); } catch {} + return { lsKeys: Object.keys(localStorage), userInfo: ui ? ui.slice(0, 400) : null, hasSessionCookie: document.cookie.includes('session_id') }; + }).catch((e) => ({ err: e.message })); + log('evidence:', JSON.stringify(ev, null, 2)); + log(url.includes('/login') ? '❌ 仍在登录页,注入失败' : '✅ 已进入 App(非 /login)'); +} catch (e) { + log('ERROR', e.message); + try { await page.screenshot({ path: `${OUT}/02-error.png` }); } catch {} +} finally { + await browser.close(); +} diff --git a/docs/manus-parity/scripts/manus-settings.mjs b/docs/manus-parity/scripts/manus-settings.mjs new file mode 100644 index 00000000..5e042e8f --- /dev/null +++ b/docs/manus-parity/scripts/manus-settings.mjs @@ -0,0 +1,55 @@ +// Manus Settings/Personalization/Account 模态截图(settings 是 /app 内弹层,非独立路由)。 +import fs from 'node:fs'; +import { createRequire } from 'node:module'; +const require = createRequire('/home/ec2-user/wbj/axon/package.json'); +const { chromium } = require('playwright-core'); +const TOKEN = fs.readFileSync('/tmp/manus-token.txt', 'utf8').trim(); +const EXEC = '/home/ec2-user/.cache/ms-playwright/chromium-1223/chrome-linux64/chrome'; +const OUT = '/tmp/manus-shots'; +const log = (...a) => console.log('[set]', ...a); + +const browser = await chromium.launch({ executablePath: EXEC, headless: true, args: ['--no-sandbox', '--disable-dev-shm-usage'] }); +const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 }, userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36', locale: 'en-US' }); +await ctx.addCookies([ + { name: 'session_id', value: TOKEN, domain: '.manus.im', path: '/', secure: true, httpOnly: false, sameSite: 'Lax', expires: 1787205573 }, + { name: 'login_success', value: '1', domain: '.manus.im', path: '/', secure: true, httpOnly: false, sameSite: 'Lax', expires: 1787205573 }, +]); +const page = await ctx.newPage(); +const shot = async (n) => { try { await page.screenshot({ path: `${OUT}/${n}.png` }); log('shot', n, '@', page.url()); } catch (e) { log('shot FAIL', n, e.message); } }; +const openMenu = async () => { await page.mouse.click(1404, 27); await page.waitForTimeout(900); }; + +await page.goto('https://manus.im/app', { waitUntil: 'domcontentloaded', timeout: 40000 }); +await page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {}); +await page.waitForTimeout(3000); + +// 打开 Settings 模态 +await openMenu(); +try { await page.getByText('Settings', { exact: true }).first().click({ timeout: 6000 }); } catch (e) { log('open settings FAIL', e.message); } +await page.waitForTimeout(2000); +await shot('30-settings-open'); + +// 逐 tab 点击截图(modal 左侧 tab) +const tabs = ['Usage', 'Connectors', 'Access tokens', 'Data controls', 'My computer', 'Plugins', 'Skills', 'Scheduled tasks', 'Billing', 'Subscription', 'Team', 'Privacy', 'General']; +let idx = 31; +for (const t of tabs) { + try { + const loc = page.getByText(t, { exact: true }).first(); + if (await loc.count() > 0) { + await loc.click({ timeout: 4000 }); + await page.waitForTimeout(1400); + await shot(`${idx}-settings-${t.toLowerCase().replace(/\s+/g, '-')}`); + idx++; + } else { log('tab not found', t); } + } catch (e) { log('tab FAIL', t, e.message); } +} + +// 关闭 → Personalization +try { await page.keyboard.press('Escape'); await page.waitForTimeout(800); } catch {} +await openMenu(); +try { await page.getByText('Personalization', { exact: true }).first().click({ timeout: 6000 }); await page.waitForTimeout(1800); await shot('48-personalization'); } catch (e) { log('personalization FAIL', e.message); } +try { await page.keyboard.press('Escape'); await page.waitForTimeout(800); } catch {} +await openMenu(); +try { await page.getByText('Account', { exact: true }).first().click({ timeout: 6000 }); await page.waitForTimeout(1800); await shot('49-account'); } catch (e) { log('account FAIL', e.message); } + +await browser.close(); +log('DONE'); diff --git a/docs/manus-parity/traceability-matrix.md b/docs/manus-parity/traceability-matrix.md new file mode 100644 index 00000000..2e3d00d6 --- /dev/null +++ b/docs/manus-parity/traceability-matrix.md @@ -0,0 +1,319 @@ +# Manus 对齐 · 双向可追溯 + 功能实现矩阵(质量最后守门人) + +> 建立日期:2026-06-03 | 方法:4 个并行审计 agent 逐截图查到代码(grep handler/数据源/file:line),非凭记忆 +> 素材轴:[screenshots/](screenshots/)(28 主截图 + 31 process 帧)+ [mockups/](mockups/)(7 高保真)+ [recordings/](recordings/)(4 录屏) +> milestone 轴:[../spec/roadmap/](../spec/roadmap/)(M5–M20) + +## 0. 这份文档是什么(用户定的质量纪律) + +> **「视觉像」≠「功能实现」。** 截图/高保真对上只是第一关;截图里那个功能 axon 代码**真的实现并工作**了,才算通过。视觉像但功能是占位/假数据/缺失 = **假通过**,是 AI 编程最隐蔽的质量黑洞。 + +**★每功能点「完成」的唯一硬标准(2026-06-03 用户拍板,必须写进每个功能点的验收细节)★**: +1. **UI 对齐**:axon 长得像 Manus 截图/高保真。 +2. **UE 对齐**:点击/hover/弹层/切换/输入的交互行为对齐。 +3. **eval 模拟点击验功能效果对齐**:有一条 **eval 代码**(组件测试 `fireEvent` 或 Playwright 真点击)**模拟用户点击 → 断言功能的真实效果**(数据真出来/动作真发生/状态真变),且效果对齐 Manus。 +> 三条**全过**才算「完成」。只满足 1(UI 像)= 假通过;满足 1+2 但无 eval 验功能效果 = 未完成。**eval 是「功能真实现」的可机判证据**(铁律11:评估大于一切)——把「视觉像但功能空」这个 AI 编程质量黑洞用自动化点击门焊死。 + +**三级判定列**(每个功能点打分): +- **视觉(UI)** / **UE(交互)** / **功能(Function)**:功能列必须有 **eval 验收**(见 §6 每功能点 eval 验收表)——真接通 + 有 click-eval 断言效果才算 ✅。 + +**双向追溯**(参考系必须先双向补完整,再补功能——2026-06-03 流程纪律): +- 正向(§1):每张 Manus 截图 → 对应 milestone 功能点 + **eval 验收**。截图无 spec → 补 spec(§3.1 → [../spec/roadmap/M21-overview.md](../spec/roadmap/M21-overview.md))。 +- 反向(§3):每个 axon milestone → 有高保真/截图可验。无高保真 → 补(§3.2 护城河 → [../prototypes/](../prototypes/))。 + +**图例**:✅完整(UI+UE+eval 三过)| ⚠️像但功能缺/缺 eval | 🔴缺失 | ➖战略不做(架构不同) + +### 0.1 ★最新验证戳(2026-06-03,守门人纪律用在文档自己身上)★ + +> 「文档做完了吗」= 它声称的证据真实存在且真过,不是纸面声称。本次逐项实跑核验: + +- **§6.1 前端 click-eval 证据基座 = 实跑核验为真**:矩阵声称的 **34 个 click-eval 测试文件全部真实存在**;实跑 **36 文件 · 253 测试全绿**(jsdom,模拟点击断言功能效果,不依赖 LLM/渠道)。每个 ✅ 背后有可机判证据,非纸面。 +- **后端 mode 交付 eval(背书 §1.3 coding/mode ✅)= 本次未能干净复验,诚实标注**:回归门(10 套)实跑得 prompt-refactor 5/5 · **m7 3/3 · m8 5/5(最重套满分)· m9 1/1** 绿,但 m5 8/10 · m6 5/6 · m10-jsx 1/3 · m10-picker 0/1 · slides-deck 0/1 · silent-hang 1/2 低于基线。**隔离单跑判别**(slides-deck / m10-picker 各单独重跑)证实:失败均为 **`timedOut` 卡半途**(exit 124),非断言失败——= **gpt-5.5 渠道下线后刚恢复被限流的延迟 stall**,非代码退步(铁证:工作树干净无流水线改动 + 最重 m8 满分 + 失败是超时非逻辑错)。 +- **结论**:这些 mode ✅ 的背书暂依赖**前一基线(2026-05-25 实跑校准)+ 本次仍绿的 m7/m8/m9**,**非本次全绿复验**。待 gpt-5.5 渠道稳定后重跑回归门拿干净全绿,再把这几个 mode ✅ 钉死。**绝不因「应该是 flaky」就标绿**——这正是本文档要焊死的「假通过」。 + +### 0.2 ★对齐 oracle:把「对齐」从 vibe 变成 red/green 属性(2026-06-04 用户拍板,C 层落地)★ + +> **问题根**:「对齐 Manus」不是一个可完成的任务——它是个没有 spec 的移动靶。AI 能写出对齐代码(BlueprintCard 就是),但「对齐」只活在「某人某次认真读 HAR」的脑子里,没有门绑住。有人重构删掉 abilities chips,旧的 click-eval 照样全绿。 +> +> **区分两种 eval**(这是本文档第二层焊死纪律): +> - **普通 click-eval**(§6.1 的 253 个):用**临时编的** fixture 测「组件机械上能不能用」。守的是「功能不是空壳」。 +> - **对齐 oracle**(`*.manus-alignment.test.tsx`):fixture = **Manus HAR 实拍 capture**,每条断言 = **Manus 观测到的必对齐契约**且 cite 抓包来源。守的是「axon 没偏离 Manus-spec 行为」。绿=对齐,红=偏离。**故意不抄项**(如倒计时自动跳过)也编码成正向断言,把「主动的对齐决策」文档化而非静默省略。 + +**已落地(分波推进,含 F 层「agent 自动产出 + 独立双轴认证」实证)**: +| 单元 | oracle 文件 | 契约 | Manus 参照 | 产出方 | 状态(牙齿+grounding 双轴)| +|---|---|:-:|---|---|---| +| web-05-blueprint | `BlueprintCard.manus-alignment.test.tsx` | 6 | manus2.im62.har MathBuddy | 人手抽 | ✅ 6/6 | +| M20.a-avatar-menu | `AvatarMenu.manus-alignment.test.tsx` | 6 | 19-avatar-menu.png | agent 自动 | ✅ 6/6 + 变异认证 | +| M20.a-settings | `SettingsModal.manus-alignment.test.tsx` | 6 | 32-settings-general.png 等 | agent 自动 | ✅ 6/6 + 变异(注 5th COMM_KEY→契约3 红) + grounding 亲核(2 组 10 项·4 开关逐项吻合) | +| modeChips | `modeChips.manus-alignment.test.ts` | 6 | 16-mode-more.png | agent 自动 | ✅ 6/6 + 变异(id slidez→契约1/3 红) + **揪出文档 bug**(见下) | +| web-06-template-picker | `TemplatePickerCard.manus-alignment.test.tsx` | 6 | web-06-website-builder-picker.png | agent 自动 | ✅ 6/6 + 变异(已选 badge→契约3 红) + grounding 亲核(5 模板+Figma/ref 缺口属实) | +| composer-plus | `ComposerSkills.manus-alignment.test.tsx` | 6 | 20-composer-plus.png | agent 自动 | ✅ 6/6 + 变异(附件文案→契约1 红) + grounding 亲核 | +| M20.a-topbar | `WorkbenchTopBar.manus-alignment.test.tsx` | 5 | 02-app-home.png + 18-credits.png | agent 自动 | ✅ 5/5 + 变异(modelName axon→契约1 红) + grounding 亲核(顶栏版位/无模型档=C3 deliberateSkip) | +| M20.e-library | `LibraryView.manus-alignment.test.tsx` | 6 | 46-nav-library.png | agent 自动 | ✅ 6/6 + 变异(库标题→契约1 红) + grounding 亲核(标题/收藏/搜索/日期分组网格属实) | +| M20-database | `DatabaseTabBody.manus-alignment.test.tsx` | 6 | web-07-computer-database-auth.png | agent 自动 | ✅ 6/6 + 变异(rowMeta 列→契约4 红) + grounding 亲核(users/customers 表+列+真值+Manus Auth 子tab 逐项吻合,护城河真表非空壳) | +| M20-set-data-controls | `SettingsDataControls.manus-alignment.test.tsx` | 6 | 31-settings-data-controls.png | agent 自动 | ✅ 6/6 + 变异(会话数 个→项→契约2 红) + grounding 亲核(Manus 6 卡网格非 toggle,axon 真删会话/api/sessions DELETE) | +| M20-set-usage | `SettingsUsage.manus-alignment.test.tsx` | 6 | 33-set-usage.png + 40-set-billing.png | agent 自动 | ✅ 6/6 + 变异(额度余额→契约2 红) + grounding 亲核(Tasks/Websites/Computers tab+Credits 797+日刷300,真接 NewAPI quota 三态诚实/不抄 Stripe) | +| M20-set-personalization | `SettingsPersonalization.manus-alignment.test.tsx` | 6 | 43-personalization-profile.png | agent 自动 | ✅ 6/6 + 变异(profile key→契约5 红) + grounding 亲核(Profile 真表单→localStorage→manus-prompt 注入闭环,Knowledge 诚实占位) | +| F0-tasklist-nav | `TaskListPane.manus-alignment.test.tsx` | 6 | 02-app-home.png + 45/46/47-nav-*.png | agent 自动 | ✅ 6/6 + 变异(删「新」徽章→契约2 红 / footer 抄回 Meta→契约5 红) + grounding 亲核(左栏 5 项序/Scheduled「New」徽章/Projects+/All tasks/**footer 真为「from ∞ Meta」**证 axon 去 Meta 化是真决策) | +| computer-tabbar | `codeTabs.manus-alignment.test.tsx` | 6 | workbench-two-pane-computer.png | agent 自动 | ✅ 6/6 + 变异(deploy→files→契约5 红) + grounding 亲核(Computer 条**真 6 tab** Preview/`<>`/pie/database/**files(folder)**/gear,**第5位真是 files**证 axon deploy📦 替换是真护城河决策) | +| web-01-session-row | `SessionListItem.manus-alignment.test.tsx` | 5 | 02-app-home.png All tasks 行 | agent 自动 | ✅ 5/5 + 变异(状态点合并→契约2 红 / 删 confirm→契约5 红) + grounding 亲核(任务行真为状态点+标题,running 虚线圈 vs idle 实心点两态可分) | +| web-14-deliverable(混合·护城河型)| `DeliverableCard.manus-alignment.test.tsx` | 5 | **web-14-delivery-card-2026-06-04.png(2026-06-04 浏览器复拍真 Manus 交付卡)** | 人手抽 | ✅ 5/5 + 变异(✓完成勾改掉→契约5 红 / 回滚守卫去掉→契约3 红,1/2/4 绿) + grounding 亲核。**chrome 偏差已混合重设收敛**(用户拍板「Manus 外形+护城河抽屉」):①缩略图预览 ②点卡开预览 ③**真 git 回滚护城河**(Manus 实拍无,提到抽屉第一公民)④完成标题双类 ⑤**采纳 Manus 步骤汇总外形**(✓完成勾+折叠 chevron,曾分歧今对齐正向锁)。残留分歧(N/N 步数·结果右栏 vs 卡内)已文档化不锁 | + +> **★F 层规模化跑通(2026-06-04)★**:1 message 并行派 4 个 agent,各自读截图→抽契约→写 oracle→自跑绿。**不信 agent 嘴**——主进程**双轴独立认证**:①**牙齿**=对每个组件注 1 变异,4 oracle 各精确变红其绑定契约(SettingsModal 5th 通信项→契约3 / modeChips id 改坏→契约1+3 / TemplatePicker 改「已选」→契约3 / ComposerSkills 改附件文案→契约1),`git checkout` 还原→24/24 回绿。②**grounding**=主进程亲自看 4 张源截图核 agent 没看错(全部读对)。 +> +> **★对齐系统揪出的第一个真 bug(grounding 轴的价值)★**:modeChips agent + 主进程亲核 16-mode-more.png 一致确认 Manus「More」浮窗**有 Schedule tasks + Chat mode**——但 `CLAUDE.md` 和 `modeRegistry.ts` 注释都写「schedule/chat 是 axon 加、Manus 没有」=**以讹传讹的错误信念**。对齐 oracle 把契约强制 grounding 到真截图,逼出了团队文档与真 Manus 的偏差。**已全修(2026-06-04)**:CLAUDE.md + modeRegistry.ts + PRD line 108/113 残留全纠正;且 **live-API 二次实证**(`ListScheduledTasks` 返回真定时任务=schedule 是真 Manus 内建的 API 铁证)。另 A/D 审计揪出第二个 grounding bug **C3**(模型选择器把付费徽章 Pro 误当档名、漏中间档 Manus 1.6,亲核 17-model-selector.png 已修 matrix line 82)。 +> +> **★oracle 正确性有两条独立轴(本波新增纪律)★**:**牙齿**(绑代码不空过)=变异测试可认证;**grounding**(契约忠实反映真 Manus)=变异测**不了**,必须独立核实 agent 没看错 capture。⚠️**deliberate-skip 契约最危险**——agent 可能把「没建的功能」粉饰成「战略不抄」骗绿;这类契约必须有**文档化理由**(如 Blueprint auto-skip=耗 credits / AvatarMenu 死外链=「不留死按钮待真站点」原则),不能只凭 agent 断言。 +> +> **D 层覆盖账**:`find -name '*.manus-alignment.test.{tsx,ts}'` = 已绑对齐门单元数。当前 **16**(2 人手 + 14 agent 自动,93 契约双轴认证;DeliverableCard 复拍后混合重设 4→5 契约)。系统全貌(A 参照固化/B 单元契约/C oracle/D 覆盖账/E 双向漂移/F agent 自产+双轴认证)分波推进。 +> +> **★复拍解锁 DeliverableCard → 反而抓出 chrome 偏差(E 层 grounding 的价值再实证,2026-06-04)★**:本想用 Pro token **浏览器复拍真 Manus 交付卡**解锁 DeliverableCard oracle(它是唯一有真 producer `website_delivered` 的活面但缺 grounding 截图)。复拍(web-14-delivery-card-2026-06-04.png)**反而发现 axon 制品卡 chrome 偏离真 Manus**:真 Manus 交付 = 步骤汇总卡(✓+末步标题+5/5+折叠⌄+浮动缩略图)+右栏发布预览(Preview/Share/Published/Edit/Made with Manus),**无制品卡无⋯菜单无回滚**;axon 是自成一套 IDE 风制品卡。组件注释自称 grounding 来自「2026-05-21 口述截图」却画的是 axon 自己那套——**旧口述参照掩盖 chrome 偏差,回真 Manus 复拍抓出来了**。用户拍板**记分歧+锁护城河**(非重设 chrome):写**窄·护城河型 oracle**只锁真共享(缩略图/点开)+真护城河(**git 回滚——Manus 实拍证没有**),header 明标 chrome 分歧不假装对齐。**纪律细分**:chrome 偏差单元 ≠ 一律不装 oracle——全偏差(SchedulePluginCards 管理卡 vs 配置卡)→不装;偏差但有真共享+真护城河(DeliverableCard)→装窄 oracle 锁差异化,防重构丢护城河。**安全**:token 走 env 全程不落盘,账号截图/视频/旧 token.txt 用完即清。 +> +> **★第 5 波 + 新增「死面/活面」grounding 子纪律(2026-06-04)★**:派 3 agent 给左栏导航(TaskListPane)/Computer tab 条(codeTabs)/任务行(SessionListItem)补 oracle,双轴认证全过(主进程注 3 变异恰红 3 契约·还原 17/17 回绿 + 亲核 02-app-home/workbench-two-pane 两图)。**本波最关键产出是 grounding 预筛挖出的新陷阱**:FollowupSuggestions/PlanPanel 这两个 grounding **最硬**(workbench-two-pane 截图清楚显示 Suggested follow-ups)的候选,`grep` 实测 server **零 producer**(`suggested_followups`/`plan_update` 无人 emit)=真实产品里**永不渲染的死面**。给死面装绿 oracle = **新版假覆盖**(测一个用户看不见的面)。**子纪律:oracle grounding 不止要「截图有」,还要「axon 真渲染(server 有 producer)」**——二者缺一即记账不装(FollowupSuggestions/PlanPanel 标「consumer 就绪·producer 未接·待升 A」)。这条把守门人「完成=真前端能用」纪律下沉进了 oracle 准入门。另:DeliverableCard 是活面(`website_delivered` 真 emit)但缺一张干净 Manus 交付卡截图 grounding→列入复拍清单。 +> +> **★死面接活 + DeliverableCard 混合收敛(2026-06-04 续,goal #1+#2)★**:把第 5 波标的两个**死面接活**——先 `curl /v1/responses` 真请求实证 gpt-5.5 渠道已恢复(流式真出 token,非 503),再码 producer:**M19.b plan_update = 复用 agent 原生 TodoWrite→conversation.ts 映射**(★驳 spec D2 新造 declare_plan:TodoWrite 已是 agent 维护步骤计划的原生工具且已被 :5293 拦截,再造 marker=重复造轮子违铁律2★);**M19.c suggested_followups = 新 marker `suggest_followups`**。映射逻辑抽 `narrative-producers.ts` 纯函数 9 单测焦死契约。**活体跨边界证**:`live-narrative` eval(M19.f)真任务跑出 **plan_update×5 + suggested_followups×1 真到 ws 流**(producer→consumer 边界,浏览器钩子抓二进制帧失败后改用 WS harness 拿到铁证)。**FollowupSuggestions/PlanPanel 从死面升活面**——死面子纪律的「待升 A」已落。**goal #2**:plan_update 接通后「步骤汇总」外形才有数据基础,用户拍板「混合: Manus 外形+护城河抽屉」→ DeliverableCard 重设(✓完成勾+折叠 chevron 采 Manus 外形 / 回滚提到展开抽屉第一公民不再藏 ⋯),oracle 4→5 契约(chrome 偏差从「记分歧不锁」收敛成「契约5 正向锁采纳了的 Manus 外形」),变异认证恰红契约3+5。 +> +> **★第 4 波给「已闭合红牌」装门防回退(2026-06-04)★**:§2 #4「Settings 6 占位壳」红牌 2026-06-03 已闭合(10 子页全真),但**只是文档声称、无门守**。这波给 3 个 Settings 子页(数据控制/用量账单/个性化)补对齐 oracle:变异认证证它们真绑后端(会话数 fetch/NewAPI quota/profile→prompt 注入闭环)——**谁把它们改回占位壳即红**。意义=把「已闭合」从一次性人审升级成**持续 red/green 门**(防回归);且 grounding 亲核 4 张截图证 agent 没看错(DataControls agent 还纠正了我 prompt 里假设的「数据训练 toggle」——截图实为 6 卡网格)。Settings 子页真没建的部分(Knowledge memory/消耗明细/Websites usage)是诚实「即将上线」占位,oracle 正向断言守住「占位但不伪造假数据」。 +> +> **★grounding 轴枪毙弱 oracle(质量>数量实证,2026-06-04 第 3 波)★**:第 3 波派 4 agent,**SchedulePluginCards 被弃**——亲核 `mode-07-schedule-recurring.png` 证实 agent 报的真分歧:Manus 是对话流里的**定时配置卡**(Repeat Daily 09:00/Next run/Skip confirmations),axon 的是左栏**任务管理卡**,频率/时间走 modeChips NL prompt 收集(AI-native 无 picker)=**两种形态**。该 oracle 契约1-4 把管理卡特性硬 cite 到没有管理卡的截图=grounding 不实,留着就是凑覆盖数=假覆盖,**删之**。**schedule 配置 = axon AI-native NL 收集 vs Manus 交互 picker 卡 = 文档化形态分歧(非缺口、非 UI oracle 单元)**。教训:grounding 轴的价值不止揪 bug,还在**拒绝弱 oracle 污染覆盖账**。 +> +> **★A/D 层已落地(2026-06-04 workflow + live-API)★**:[reference-manifest.md](reference-manifest.md) 固化「6 oracle ↔ 70 capture+年代 ↔ 38 编码事实(含 8 deliberateSkip) ↔ 18 条重抓清单 ↔ drift-diff 流程」三向映射——丢一张新截图 30 秒定位要重验哪些单测。**E 层双向漂移的「Manus 侧」原本是唯一不可机械化核心(需回真 Manus 重观测)**,用户给 Pro token 后**已部分机械化**:`api.manus.im` Connect-Web 直查([[ops_manus_live_api_drift_detection]])覆盖数据型表面(账号/额度/skill/模板/定时),只剩 mode chip 列表 + 纯 UI 版式需浏览器复拍(且当前截图仅 2 天新无 staleness)。A/D 审计回读截图揪出 C3 + 修净 C1 残留。 + +--- + +## 1. 正向矩阵:Manus 截图 → milestone 功能点 → 三级状态 + +**★覆盖核算(2026-06-03 守门人精确盘点,反「29/89=67% 未完成」的机械误读)★**:§1 正向矩阵共 **49 个映射点**(每点 = 一张 Manus 截图功能 → axon milestone)。判定分布: + +| 判定 | 数 | 含义 | 是否「待办」 | +|---|:-:|---|---| +| ✅ | **31** | UI+UE+功能三过(绝大多数已有 click-eval,见 §6.1;mode 交付走 m5–m11 后端 eval 套件) | 否,已完成 | +| ➖ | **7** | 战略不做(架构不同/商业模式不同:模型分级·Team·桌面门·邮件触发等,**诚实省略非缺口**) | 否,正确不做 | +| ⚠️ | **11** | 真但不完整(多数卡 gpt-5.5/真部署:usage 明细·mode 交付卡·building 进度;少数 UI 打磨:Figma 导入) | 部分,多数 blocked | +| 🔴 | **2** | 缺失(analytics·BaaS Auth/Storage——**都需真部署+真流量+gpt-5.5 跑真全栈**) | 是,硬卡外部依赖 | +| 📋 | **1** | spec 已补待实现(access-tokens,需 axon-cloud 登录态) | 是,待 | + +**真实结论**:**38/49(78%)已解决**(31 完成 + 7 正确不做)。剩 14 个里 **~10 个 blocked**(gpt-5.5 渠道下线 / 需真部署),**~4 个低价值打磨**。**不是「67% 没做」,而是「78% 已落定,剩余多数是渠道下线期硬做=造假壳的活体项」**。守门人原则:blocked 项诚实标注不造假壳,待外部依赖恢复再验证。 + +### 1.1 顶栏 / 左栏导航 / 主屏(M20.a + F 系列) + +| 截图 | Manus 功能 | axon 实现(file:line) | 视 | UE | 功能 | 判定 | +|---|---|---|:-:|:-:|:-:|---| +| 02-app-home | 三栏主屏 + 4 pinned chip | ChatPane PINNED_CHIPS | ✅ | ✅ | ✅ | ✅ | +| 16-mode-more | More 弹层 9 mode | ChatPane POPOVER_CHIPS + morePopover | ✅ | ✅ | ✅ | ✅ | +| 17-model-selector | 模型选择器三档 Manus 1.6 Max/1.6/Lite(**Pro 是付费徽章非档名**,2026-06-04 亲核 17-model-selector.png 纠正) | WorkbenchTopBar 占位(单后端) | ✅ | ➖ | ➖ | ➖ | +| 18-credits | 积分 pill 弹层 | WorkbenchTopBar creditsPill + AvatarMenu | ✅ | ✅ | ✅ | ✅ | +| 19-avatar-menu | 头像菜单 10 项 | AvatarMenu(菜单→Settings/登出) | ✅ | ✅ | ✅ | ✅ 核心项全真;Homepage/Get help/Docs 外链 axon 无公开站 → **诚实省略不放死按钮**(AvatarMenu.tsx:16 注释,➖ 非假阳性) | +| 20-composer-plus | composer + 菜单(添加文件 + 使用技能) | ComposerSkills(/api/skills 11 真技能 + 选中进 payload→state.activeSkills 注入) | ✅ | ✅ | ✅ | ✅ **已闭合(2026-06-03,三条全过)**:点 +→菜单→选技能→chip→注入;对齐 [prototypes/skill-composition.html](../prototypes/skill-composition.html);eval `ComposerSkills.test.tsx` 9/9 + curl 证 11 真技能 | +| 22/23-open-task | 点任务→工作台 | TaskListPane selectSession | ✅ | ✅ | ✅ | ✅ | +| 45-nav-plugins | Plugins:MCP 网格 toggle | TaskListPane plugins view + handleMcpToggle | ✅ | ✅ | ✅ | ✅ | +| 46-nav-library | Library:交付物网格 | LibraryView(/api/artifacts + 搜索/收藏) | ✅ | ✅ | ✅ | ✅ | +| 47-nav-agent | Agent:部署营销 agent | TaskListPane agents view(BUILTIN_AGENTS) | ⚠️ | ✅ | ⚠️ | ⚠️ 卡片在,部署/激活缺 | +| paid-01-home | 付费满血首页 | 同 02 | ✅ | ✅ | ✅ | ✅ | + +### 1.2 Settings 模态子页(M20.a/b/c/d/f) + +| 截图 | Manus 功能 | axon(file:line) | 视 | UE | 功能 | 判定 | +|---|---|---|:-:|:-:|:-:|---| +| 30-settings-open | 模态框架 + 两组导航 | SettingsModal NAV 两组 | ✅ | ✅ | ✅ | ✅ | +| 32-settings-general | 语言+主题+4 通信开关 | GeneralPane(i18n changeLanguage + data-theme + localStorage) | ✅ | ✅ | ✅ | ✅ 语言真生效/主题真存/开关真持久 | +| 43-personalization | 资料 + Custom Instructions | PersonalizationPane + **readProfile→manus-prompt 注入** | ✅ | ✅ | ✅ | ✅ **存储+注入闭环**(注入测试钉死) | +| 33-set-usage | 三 tab + 余额 + 逐任务明细 | UsagePane(/api/axon-cloud/quota) | ✅ | ✅ | ⚠️ | ⚠️ 余额真,**逐任务明细+网站/算力 tab 占位** | +| 44-account | 账号信息 + 登出 + 删号 | AccountPane(/api/auth/oauth/status+logout) | ✅ | ✅ | ⚠️ | ⚠️ 登录/登出真,**缺 Delete account** | +| 31-data-controls | 数据留存/导出/删除 | DataControlsPane(GET /api/sessions 真会话数 + 删除全部) | ✅ | ✅ | ✅ | ✅ **已接通**(2026-06-03,真 101 会话数+30天留存+删除动作) | +| 34/37-my-plugins | Connectors/Skills/Data sources | MyPluginsPane(/api/connectors MCP 能力视图,诚实空态) | ✅ | ✅ | ✅ | ✅ **已接通**(已连接 MCP 显工具数;无连接诚实空态不造假) | +| 36-my-computer | Cloud/Local computer | MyComputerPane(/api/auth/oauth/status 真 runtimeBackend) | ✅ | ✅ | ✅ | ✅ **已接通**(运行后端 axon-cloud + 本地 VFS + E2B 说明) | +| 38-skills | Skills 列表 | MyPluginsPane(归我的插件) | ✅ | ✅ | ✅ | ✅ 并入 my-plugins | +| 集成 | 第三方服务连接 | IntegrationsPane(/api/connectors 10 真 provider + connect/disconnect) | ✅ | ✅ | ✅ | ✅ **已接通**(GitHub/Gmail/Notion/Slack…真 OAuth 流) | +| 邮件触发/云浏览器 | Features 子页 | InfoPane(诚实架构说明:飞书 Bot/webhook/定时 触发;agent 端 Playwright) | ✅ | ✅ | ➖ | ➖ axon 架构不同,**诚实说明非假占位**(非 false-pass) | +| 35-access-tokens | OpenAPI tokens | 📋 M21.a spec(复用 src/proxy) | 📋 | 📋 | 📋 | 📋 **spec 已补**(M21.a,待实现+eval:签发 token→断言能调通 proxy) | +| 41/42-team-* | Team settings | 📋 M21.b spec(⚪ 战略暂不做) | ➖ | ➖ | ➖ | ➖ **spec 记决策**(M21.b,B 端待 C 端跑通;留诚实入口) | +| 39-scheduled | 定时任务列表 | 独立左栏导航(非 Settings) | ✅ | ✅ | ✅ | ✅ axon 走独立 Scheduled 页 | +| 40-billing | Websites 计费 | UsagePane Websites tab | ✅ | ✅ | ⚠️ | ⚠️ 占位(依赖 M15 埋点) | + +### 1.3 12 个 mode 动态过程(M5–M11 coding + skill 系统 + M19 叙事) + +| mode / 截图 | Manus 功能 | axon(file:line) | 视 | UE | 功能 | 判定 | +|---|---|---|:-:|:-:|:-:|---| +| slides (slides-01~04) | 研究→逐页渲染 HTML deck | generate_slide_deck + conversation handler + deck 交付 | ✅ | ✅ | ✅ | ✅ 完整(图生成 deck) | +| website (web-02…) | 脚手架→工作台编码 | propose_website_builder + coding 流水线 | ✅ | ✅ | ✅ | ✅ | +| design (mode-05) | 进生成→GPT Image→落盘 | image_generation hosted tool | ✅ | ✅ | ⚠️ | ⚠️ 图生成真,**缺 deliver_design 交付卡** | +| video (mode-06~06d) | free 付费墙 / paid 逐帧→编辑器 | generate_video.ts **真自执行工具**(Veo 轮询下载 MP4,非 marker,无需 handler) | ✅ | ✅ | ✅ | ✅ 生成链路真(需付费 Veo tier);编辑器/player+付费墙 ➖ 战略不卷(媒体编辑非编程护城河,PRD §2.5.10) | +| schedule (mode-07) | 定时任务卡 | ScheduleTaskTool + Cron + daemon | ✅ | ✅ | ✅ | ✅ | +| wide-research (mode-08) | 多源 Search 聚合 | mode 注册 + web_search hosted | ✅ | ✅ | ⚠️ | ⚠️ **缺统一 marker + 研究交付卡** | +| spreadsheet (mode-09) | 产 Excel + 交付卡 | mode 注册→引导 coding;skill 内容未核实 | ⚠️ | ✅ | ⚠️ | ⚠️ **skill 实现待核实 + 缺交付卡** | +| visualization (mode-10) | 数据→图表 | mode 注册→引导 coding;skill 未核实 | ✅ | ✅ | ⚠️ | ⚠️ **skill 待核实 + 缺交付卡** | +| audio (mode-11) | Loading skill→生成 music→wav | generate_audio/generate_music marker 注册 | ✅ | ✅ | ✅ | ✅ | +| chat (mode-12) | 纯对话 + Thinking 折叠 | chat mode toolFilter 禁工具 | ✅ | ✅ | ✅ | ✅ | +| desktop-app (mode-04) | 桌面客户端门(Web 做不了) | axon Electron 反而有优势 | ➖ | ➖ | ➖ | ➖ 架构优势 | +| mode chips(13 个) | 16-mode-more / 02 | ChatPane 4 pinned + 9 popover,id 对齐 modeRegistry + activate_skill | ✅ | ✅ | ✅ | ✅ chip=skill 真激活 | + +### 1.4 Manus's Computer 工作台 + 托管 BaaS + Blueprint(M5–M11 + M13/M15) + +| 截图 | Manus 功能 | axon(file:line) | 视 | UE | 功能 | 判定 | +|---|---|---|:-:|:-:|:-:|---| +| web-01-upsell-gate | 模型升级阻塞门 | 走 NewAPI quota(非 upsell) | ✅ | ➖ | ➖ | ➖ 商业模式不同 | +| web-02/09-code | Code tab:文件树+流式写码 | CodePane + FileTree + CodeEditor(Monaco) | ✅ | ✅ | ✅ | ✅ **超越 Manus**(懒加载/拖拽/AI hooks) | +| web-03-preview | Preview:device+活预览 | WebsitePreviewPane directUrl + coding_app_preview | ✅ | ✅ | ✅ | ✅ | +| web-04/10-building | building 加载态 | iframe 加载态(无显式进度条) | ✅ | ⚠️ | ⚠️ | ⚠️ 缺显式 building 进度 UI | +| web-05-blueprint | Blueprint 卡:可编辑特性+倒计时跳过 | BlueprintCard 3 步 + confirm_webdev_feature | ✅ | ✅ | ✅ | ✅ **三条全过 + 对齐 oracle(2026-06-04)**:BlueprintCard.test 6/6(机械)+ **BlueprintCard.manus-alignment.test 6/6(绑 MathBuddy HAR 实拍契约,§0.2)**——abilities chip/默认勾选/inspire-me/协议保真都 red/green 焊死。倒计时自动跳过 = ⚪ 战略不抄(编码成契约6 正向断言:60s 不自动构建) | +| web-06-builder-picker | 模板选择+Add reference+Figma | TemplatePickerCard 网格 | ✅ | ✅ | ⚠️ | ⚠️ 网格完整,**缺 Add reference/Import Figma** | +| **web-07-database-auth** | **Database tab:托管 DB 表视图+Auth** | DatabaseTabBody + /api/project-db(SQLiteDriver introspect 项目本地 SQLite) | ✅ | ✅ | ✅ | ✅ **已闭合(2026-06-03)**:真表/列/行(后端 curl 打真 notessaas.sqlite 证 users/notes 真数据 + 注入/穿越 400;前端 4 组件测试)。Auth 表自动填登 = 待 publish-fullstack 接 Neon introspection(同范式换 PostgresDriver) | +| **web-08-analytics** | **Dashboard tab:部署分析折线图** | CodePane analytics tab = 占位;📋 M21.c spec | ✅ | 🔴 | 🔴 | 🔴 **spec 已补(M21.c)**;需真部署+真流量,无流量诚实空态不造假(eval 见 §6.2) | +| web-11/12-crm-live | 成品 CRM 活 dashboard+登录门 | deliver_app 真起 server + preview 渲染 | ✅ | ✅ | ✅ | ✅ 端到端真闭环 | +| **web-13-baas-features** | **BaaS features 管理(Backend/DB/Auth)** | 📋 M21.c spec(Database 已落地,Auth/Storage 待) | 🔴 | 🔴 | 🔴 | 🔴 **spec 已补(M21.c)**;Database tab ✅,Auth/Storage 待 gpt-5.5 跑真全栈(eval 见 §6.2) | +| workbench-two-pane | 双栏 6 tab 工作台 | CodePane 6 tab(Preview/Code/Deploy 真 + Data/Database/Settings 占位) | ✅ | ✅ | ⚠️ | ⚠️ 6 tab 中 **3 真 3 占位** | + +--- + +## 2. ★视觉像但功能缺(假通过红牌)★ —— 守门人最该盯的 + +按「视觉对上但功能没实现」严重度排序,这些是「看着像 Manus、点下去是空的」的隐蔽缺口: + +| # | 项 | 视觉 | 真实功能状态 | 严重度 | +|---|---|---|---|---| +| 1 | ~~Computer Database tab 占位空壳~~ | — | ✅ **已闭合(2026-06-03)**:DatabaseTabBody 接 /api/project-db introspect 项目本地 SQLite,真表/列/行(curl 打真 notessaas.sqlite 证 + 安全 + 4 组件测试)。云端 Neon 表视图待 publish 链路接(同范式) | ✅ 已解决 | +| 2 | **Computer Analytics/Dashboard tab** | 像 | **零数据接入**,纯占位 | 🔴 P2 | +| 3 | **BaaS 管理面板**(web-13) | 缺 | provision(DB/Auth/存储)后端在但**对用户完全透明** | 🔴 P1 护城河 | +| 4 | ~~Settings 6 子页占位壳~~ | — | ✅ **已闭合(2026-06-03)**:集成→/api/connectors、我的插件→MCP 能力、数据控制→/api/sessions、我的电脑→runtimeBackend 全接真后端 + 截图验证;邮件/云浏览器改诚实架构说明(➖ 非假壳) | ✅ 已解决 | +| 5 | ~~video mode 无 handler~~ | — | ✅ **假阴性已纠正**:generate_video.ts 是真自执行工具(Veo,非 marker,无需 handler),生成链路真(需付费 tier)。仅编辑器/player UI 缺 = ➖ 战略不卷(媒体非护城河) | ➖ 已澄清 | +| 6 | **Usage 逐任务明细 + 网站/算力 tab** | 像 | 余额真(NewAPI),明细**占位**(NewAPI 端点未确认 + M13/M15 埋点未建) | 🟡 P2 | +| 7 | design/research/spreadsheet/viz 交付卡 | mode 像 | **产物真且可见**(design 图 inline 渲染、website/slides 走 DeliverableCard、coding 走 CodingPipelineCard);仅 spreadsheet/research 的文件没包成「下载卡」= P3 打磨(非假阴性,产物不是看不见)。验证需 gpt-5.5 跑 mode | 🟢 P3 | +| 8 | composer + Skills菜单 / Agent 部署 | 像 | composer + 仅图片上传无 Skills 子菜单;Agent 部署占位(B2B,C 端主线暂不做)。头像外链 = ✅ 诚实省略(非缺口) | 🟢 P3 / ➖ | +| 9 | **Blueprint 倒计时自动跳过 / 模板 Figma·reference 导入** | 像 | 交互细节缺 | 🟢 P4 | + +> ✅ **已修正一个假阴性**(2026-06-03):SettingsModal 里 3 处注释说「Custom Instructions 注入待 M20.c 切片2 接后端」,但注入**已闭环并提交**(readProfile→manus-prompt,有注入测试)。注释已更新——反向提醒:**代码注释也会骗审计,文档与代码必须同步**。 + +--- + +## 3. 反向缺口(双向追溯的另一半) + +### 3.1 Manus 截图有,但 axon 无对应 milestone(漏规划) + +| 截图 | 缺口 | 说明 | +|---|---|---| +| 35-access-tokens | OpenAPI token 管理 | 无 milestone,Free 账号未截到全貌 | +| 41/42-team-* | Team settings/usage | 无 milestone(B 端协作,C 端主线暂不做) | +| web-07/08/13 | 托管 BaaS UI(DB/Auth/Analytics 表视图+管理面板) | 后端 provision 有,**UI 层无 milestone 规划** → 建议补 M20.g/M21 | + +### 3.2 axon 有实现,但无 Manus 截图验证(验证盲区 — 反而是护城河) + +| 功能 | milestone | 为何无截图 | +|---|---|---| +| Skill 激活系统(activate_skill 组合) | M17.p | axon 独有,Manus 未拍 | +| UI 契约四层确定性裁判(L1-L4 + baseline 校准) | M11 | coding flow 内部,Manus 无 | +| markdown_to_pdf / render_diagram / upload_file / extract_video_keyframes | M10.f | axon-native 工具 | +| Browser/Playwright 自动化 | — | 编程护城河 | +| Cloud sandbox(E2B Model A) | M13 | 后端基建 | +| Neon/Railway provision + M15 部署 | M15 | 后端,**缺前端可视(= §2 第 1/3 项)** | +| spec 确认(M6)/ blueprint 依赖拓扑(M7) | M6/M7 | 有 UI 卡但 Manus 未对应拍 | + +> **洞察**:反向盲区里大部分是 axon 的**编程护城河差异化能力**(Manus 没有所以没截图)——这些不是缺口,是优势。但 §3.1 的 BaaS UI 是真缺口(Manus 有、axon 后端有、就差 UI)。 + +--- + +## 4. 行动项(按守门人优先级) + +| 优先级 | 行动 | 依据 | +|---|---|---| +| **P1 护城河** | Computer **Database tab 接 Neon 真表视图** + **BaaS provision 进度/状态 UI** | §2 #1/#3,闭合 CRM 全栈闭环的「用户可见」最后一环 | +| **P1** | Analytics/Dashboard tab 接部署数据 | §2 #2 | +| **P2** | Settings 6 占位子页接已有后端(My Computer→E2B / My plugins→skill+connector / 数据控制→session 层) | §2 #4,多为暴露已有能力 | +| **P2** | video mode 补 conversation.ts handler + 交付(或明确战略不卷,标 ➖) | §2 #5 | +| **P3** | design/research/spreadsheet/viz 补 deliver_* 交付卡(复用 website_delivered 框架) | §2 #7 | +| **P3** | 补 milestone:access-tokens / team(或标战略不做) | §3.1 | +| 持续 | **每个新功能落地 = 必有截图/高保真对应 + 三级判定过**(本矩阵作为 CI 前的人工守门清单) | 用户定的质量纪律 | + +--- + +## 5. 汇总评分(功能实现对齐度,非视觉) + +| 组 | 视觉对齐 | 功能实现 | 主缺口 | +|---|---|---|---| +| 顶栏/导航/主屏 | ~95% | ~85% | 外链/composer+/Agent 部署 | +| Settings 子页 | ~90%(框架) | **~85%(10 子页全接真后端/诚实说明,2026-06-03)** | 仅余 access-tokens/Team 缺(B 端,无 milestone) | +| 12 mode 动态过程 | ~95% | ~75% | video handler + 4 mode 交付卡 | +| Computer 工作台 + BaaS | ~85% | ~60% | **Database/Analytics/BaaS 管理 UI(占位空壳)** | + +> **结论(2026-06-03 守门人推进后)**:视觉/UE 85-95%。已闭合:~~Settings Features 组占位壳~~(10 子页全真)、~~Computer Database tab 占位空壳~~(接真 SQLite introspection)、~~video 假阴性~~(生成真)。**剩余真短板 = Analytics/Dashboard tab(部署数据,需真 publish 流量)+ BaaS Auth/对象存储管理 UI + 云端 Neon 表视图(待 publish-fullstack 链路)**——这些依赖真部署/Neon provision,gpt-5.5 + 真 publish 后做,不造假壳。次为 4 mode 交付卡 + P3 外链/composer 打磨。 + + +--- + +## 6. ★每功能点 eval 验收表(模拟点击 → 断言功能效果对齐)★ + +> 2026-06-03 用户拍板:**每功能点「完成」必须有一条 eval 模拟点击、断言功能真实效果**(不是「后端存在」,是「点了真有效果且对齐 Manus」)。下表把功能点绑定到具体 click-eval(✅ 已有 = 指向真测试;📋 待写 = 给出 eval 契约,实现时按此写)。这是「完成」的可机判证据,CI 前的人工/自动守门清单。 + +### 6.1 已落地功能点(已有 click-eval,✅ 三条全过) + +| 功能点 | UI 对齐 | UE 对齐 | eval 模拟点击 → 断言效果 | 测试文件 | +|---|:-:|:-:|---|---| +| Settings 集成 | ✅ 32/connectors | ✅ | mock /api/connectors → 渲染 GitHub/Notion;GitHub 显「断开」、未配置点「连接」→ 断言提示需配置凭据 | `SettingsModal.test.tsx` | +| Settings 我的插件 | ✅ | ✅ | mock MCP 连接器 → 断言「3 工具」徽标;无连接 → 断言诚实空态文案 | `SettingsModal.test.tsx` | +| Settings 数据控制 | ✅ | ✅ | mock /api/sessions → 断言「2 个」会话数 + 留存说明 + 「删除全部」按钮在 | `SettingsModal.test.tsx` | +| Settings 账号 | ✅ 44 | ✅ | mock /oauth/status → 断言显示名/邮箱/退出登录;未登录 → 断言提示登录 | `SettingsModal.test.tsx` | +| Settings 用量 | ✅ 33 | ✅ | mock /quota → 断言额度余额「800」;切网站 tab → 断言占位 | `SettingsModal.test.tsx` | +| Settings 通用 | ✅ 32 | ✅ | 输入昵称→保存→断言落 localStorage;点主题→断言选中态;4 开关 role=switch | `SettingsModal.test.tsx` | +| Library 交付物 | ✅ 46 | ✅ | mock /api/artifacts → 断言卡片+日期分组;点卡片→断言 onOpenSession(id);搜索过滤;收藏过滤 | `LibraryView.test.tsx` | +| **Database tab(护城河)** | ✅ web-07 | ✅ | mock /api/project-db → 断言表列表+默认表真行;点切表→断言重拉该表行;空态。**+后端 curl 真 notessaas.sqlite 证真数据+注入/穿越 400** | `DatabaseTabBody.test.tsx` + curl | +| Custom Instructions 注入 | ✅ 43 | ✅ | build prompt 断言含「用户自定义指令」+全局含 coding+不传不污染+顺序 | `manus-prompt.test.ts` | +| 头像菜单 | ✅ 19 | ✅ | 点头像→断言菜单项;点个性化/账号/设置→断言 onOpenSettings(tab) | `AvatarMenu.test.tsx` | +| **主区顶栏(正向 web-01/02-app-home)** | ✅ 02 | ✅ | 左 model selector「axon ▾」;未登录点「登录」→onLogin;oauth 点头像→aria-expanded 展开+字母头像;cloud 积分 pill 真余额;builtin 不假装 cloud 身份 | `WorkbenchTopBar.test.tsx` 5 | +| **Plan 进度面板(正向:Manus 过程透明度 UE)** | ✅ | ✅ | 摘要 done/total+当前 doing 标题;展开列全 5 步+1-based 序号;点 header 折叠(aria-expanded+body 行隐现);空态不渲染;error→✗ 不掩盖失败 | `PlanPanel.test.tsx` 5 | +| **编程流水线信任叠加面板(护城河 M17.h/Slice-1~3)** | ✅ | ✅ | null/非 coding 不渲染;点 header 折叠;⚔工程/🏰王国 双镜片 aria-selected 切换;断裂接缝 🔴+onDispatch→「派活灭火」→onDispatch(指令);无 onDispatch→纯观测无死按钮 | `CodingPipelinePanel.test.tsx` 7 | +| **自建模板表单 modal(F5.h.3)** | ✅ | ✅ | open 才渲染;✕→onClose;类别变→模式候选联动重置(防失配);emoji 点选 data-selected;空必填→alert 拦截不发请求;填齐→点创建→POST /api/templates(body trim+emoji:前缀)→onCreated+onClose | `TemplateEditorModal.test.tsx` 6 | +| **website mode 需求挑选卡(护城河 propose_website_builder)** | ✅ | ✅ | 模板网格+intent+默认选 suggestedTemplate;切模板/改下拉/填额外要求→Generate 回传完整 selection(trim);Cancel→onCancel;disabled 冻结全卡 | `WebsiteRequirementsCard.test.tsx` 5 | +| **编程流水线 5 时刻状态卡(护城河 M7/M8,铁律不掩盖)** | ✅ | ✅ | 非法 data→null;type 路由;verify_task 未过→❌「带红不许往下做」;测试门未过→失败命令;验收<100%→amber「不到 100% 不交付」;交付被门卡→🚫卡点原因;交付成功→🎉+serverUrl 真链接(noopener) | `CodingPipelineCard.test.tsx` 7 | +| **右栏工作台 6-tab(正向 web-02/03)** | ✅ | ✅ | TAB_CONFIG 6 tab key/顺序精确对齐 Manus(预览/代码/数据/数据库/部署/设置),'more' 不平级;点 tab→onSelect(key) 右栏切 body 根契约;active 高亮 | `codeTabs.test.tsx` 5 | +| **左栏会话历史行(正向 web-01)** | ✅ | ✅ | 状态点(messageCount>0/=0)+标题(空→未命名任务)+active 高亮;点行→onSelect(id) 切 task;点 ✕→二次确认→onDelete(id) 且 stopPropagation 不穿透;confirm 取消→不删(防误删) | `SessionListItem.test.tsx` 6 | +| **左栏 MCP 插件卡(正向 F5.d.2)** | ✅ | ✅ | name+type/tools;启用 ●/禁用 ○+「已禁用」变灰;点 toggle→onToggle | `SchedulePluginCards.test.tsx` 3 | +| **左栏定时任务卡(正向「定时任务(新)」F5.d.3)** | ✅ | ✅ | type 图标/标签(⏱一次性/🔄周期/👁监听)+上次运行徽标(✓/✗/⌛)+禁用态;点 toggle→onToggle;点 ▶→onRunNow(立即触发) | `SchedulePluginCards.test.tsx` 4 | +| **左栏「库」模板卡(正向 F5.c/ListSessionTemplatesPublic)** | ✅ | ✅ | cover emoji 解析(emoji:→/其他→📋)+title/desc;official 无删除/用户模板有;点卡→onPick(tpl) 新建+预填;点 ✕→onDelete(event,tpl) 可 stopPropagation 阻穿透 | `TemplateCard.test.tsx` 5 | +| **composer 发送区(正向:Manus 输入框 UE)** | ✅ | ✅ | textarea+发送按钮+toolbarLeft slot;打字→onChange;点发送→onSubmit;Enter→onSubmit(+preventDefault)/Shift+Enter→换行不发;空白/sending→禁用(防空发/防重复发) | `ComposerSendArea.test.tsx` 7 | +| **Deploy tab 一键部署(正向 web-02 Deploy)** | ✅ | ✅ | 点部署→deploying(按钮禁用)→done(成功卡+真 URL+elapsed+打开/复制)/error(失败卡+错因不粉饰);POST body 带 projectPath;点复制→clipboard.writeText(完整URL);点下载 ZIP→建 a 打 export-zip | `DeployTabBody.test.tsx` 5 | +| **website 编辑进度气泡(护城河 M.3.b)** | ✅ | ✅ | phase/start/result/error 映射文案+颜色+role=status;enabled=false/null→unmount;result·error 3 秒淡出;error 标红不掩盖;部分成功不假装全绿 | `EditsProgressBubble.test.tsx` 7 | +| **编辑器语义地图(护城河 web-02-code 增强)** | ✅ | ✅ | 区块名+行号范围+描述;图标按类型(📦import/📐Props/🧩组件/⚡function);点区块→onNavigate(line) 跳转;currentLine 在范围→active 高亮;空态诚实 | `SemanticMap.test.tsx` 5 | +| **办公文档预览(正向 web-02 文件预览)** | ✅ | ✅ | image→img/pdf→iframe/未知→「Unknown file type」;下载 URL=/api/files/download?inline=1&path=&root=(正确 encode 防注入);无 projectPath 不带 root | `FilePreviewPanel.test.tsx` 5 | +| **文件树逻辑核心(正向 web-02 文件树)** | ✅ | ✅ | 6 纯 helper(路径父目录/拖拽安全 isDescendantOf/展开全部收目录/按路径查找/键盘导航 flatten 仅展开可见/懒加载 mergeSubtree 替换匹配路径)——递归 RENDER 是重 tier,但 bug 高发的逻辑核心已钉死 | `FileTree.helpers.test.tsx` 7 | +| **编辑器文件路由逻辑(正向 web-02 编辑器)** | ✅ | ✅ | getFileType(扩展名→走 Monaco vs 预览面板:image/pdf/excel/word,未知→code 不误判)/getLanguage(→Monaco 语言 id,未知→plaintext)/getFileName(兼容 / 和 \)——Monaco RENDER 测不了但路由逻辑核心抽 editorFileUtils 可测 | `CodeEditor.helpers.test.tsx` 5 | +| **卡片→agent 消息构造器(护城河 M6/website 契约)** | ✅ | ✅ | spec 确认/逃生/设计方向/网站需求 4 构造器的 marker(__coding_spec_confirmed__ 等)+JSON round-trip——后端据 marker 解析落 spec.json,拼错=点卡片没反应;id→name 查找+未知回落 | `composerMessages.test.ts` 6 | + +> **★守门人自查纠错(2026-06-03)★**:`SearchPanel`(项目级搜索)一度被当「活体 web-02 搜索」补 eval,**自查发现它仅被 `CodeView/index.tsx` 引用,而 index 仅被 App.tsx(display:none 遗留隐藏树)引用 → 不在活体 ManusWorkbench 树里**。活体 CodePane 的编辑器搜索 = Monaco 内置 Ctrl+F(CodeEditor),项目级 SearchPanel/CompactChatPanel/CompactMessage 都是遗留 CodeView/index 死代码。已删 SearchPanel.test(测死代码=假覆盖,[[feedback_root_hidden_app_panel_trap]] 同坑)。教训:**补 eval 前必先验组件被活体树触达,别只看「有引用」**。SemanticMap/FilePreviewPanel 经核查是真活体(CodeEditor 直接 import)。 +| Suggested follow-ups | ✅ chat | ✅ | mock 事件→断言渲染追问;点击→断言填 composer 发送 | `FollowupSuggestions.test.tsx` | +| coding 流水线信任面板 | ✅(护城河自有) | ✅ | 17 项 trust/lens 断言(盖章/灭火/接缝) | `CodingPipelinePanel.trust.test.tsx` | +| Blueprint 特性确认卡(web-05) | ✅ | ✅ | 逐步导航/风格单选/能力多选/立即构建回传 payload | `BlueprintCard.test.tsx` 6 | +| **★对齐 oracle:Blueprint ⇄ Manus(web-05,§0.2 C 层首单元)** | ✅ | ✅ | fixture=MathBuddy HAR 实拍;6 契约 cite 抓包:3 步顺序/abilities chip(LLM·File storage)/默认推荐勾选/inspire-me 单选/confirm_webdev_feature round-trip/无 auto-skip(⚪战略不抄) | `BlueprintCard.manus-alignment.test.tsx` 6 | +| **★对齐 oracle:AvatarMenu ⇄ Manus(M20.a,§0.2 F 层 agent 自动产出)** | ✅ | ✅ | fixture=19-avatar-menu.png 截图实读;6 契约 cite 截图:账号身份块/Free 露升级·付费隐藏/Credits 千分位·null 隐藏/三入口真路由/无死外链(⚪战略不抄)/Sign out 收尾。**agent 自产 + 主进程变异认证(注入3变异恰红3契约)** | `AvatarMenu.manus-alignment.test.tsx` 6 | +| 交付物卡(每次交付) | ✅ | ✅ | 点卡片/仪表盘→onClick(entryPath)、**折叠 chevron 切换抽屉**、抽屉直接露访问/复制路径/回滚(混合重设删 ⋯菜单)、slides/website 区分、回滚条件 | `DeliverableCard.test.tsx` 9 | +| 模板挑选卡(web-06 / M10.e) | ✅ | ✅ | 点模板→onSelect(id)、选中/busy 禁交互、错误/空态 | `TemplatePickerCard.test.tsx` 6 | +| **UI 契约 baseline 校准(护城河 M11.d 红线#8)** | ✅ | ✅ | 点校准→onStart、逐张✓/✗+批注+跳过像素、提交校准回传审批 payload(**用户拍板 baseline,AI 不当裁判**) | `UiContractCalibrationCard.test.tsx` 6 | +| **spec 确认卡(护城河 M6 硬门)** | ✅ | ✅ | 填齐 6 项→主按钮启用→确认回传正确 CodingSpec;逃生→onSkip;未填齐→禁用(全必填门) | `CodingSpecCard.test.tsx` 5 | +| 设计方向挑选卡(M6 V2) | ✅ | ✅ | 选方向→onSubmit(chosen)/disabled 禁选/空态 | `DesignDirectionPickerCard.test.tsx` 4 | +| user-in-the-loop 协作卡(M9 L4) | ✅ | ✅ | 展示契约:标题/步骤/外链 href+noopener/验证/兜底/不留空壳 | `UserActionCard.test.tsx` 4 | +| **mode chip 系统(正向 02-app-home/16-mode-more)** | ✅ | ✅ | 常驻4+浮窗9=13(Manus parity)/id 精确等于后端 mode 集合(防漂移)/id 唯一/非外链必有 prompt 模板(无假对话框)/playbook 外链占位/findChipById 反查 | `modeChips.test.tsx` 6 | + +### 6.2 待落地功能点(先写 eval 契约,按此实现+验收) + +| 功能点 | eval 契约(模拟点击 → 必须断言的效果) | 依赖 | +|---|---|---| +| ~~composer + Skills 菜单~~ | ✅ **已落地(2026-06-03)**:`ComposerSkills.test.tsx` 9/9(点 +→菜单/点技能→onToggle/点 ×→onRemove/添加文件回调)+ 后端 /api/skills curl 证 11 真技能 + payload→state.activeSkills 端到端注入 | ✅ 完成(移入 6.1) | +| 云端 Neon 表视图 | **确定性内核已落地(2026-06-03)**:parseDatabaseUrl + findEnvDatabases(project-db-api.test 8/8 + curl 证检测真 postgres/neon URL)+ Database tab 云横幅(DatabaseTabBody.test 2/2)。**剩:真连接浏览表** = createDriver(postgres) 走同一 introspection 范式,待真 Neon 可达(publish-fullstack + gpt-5.5) | 核心✅/真连接待 gpt-5.5 | +| BaaS Auth 管理 | 生成带 Auth 的 app → 断言 users 表可见 + 登录门状态 | gpt-5.5 跑全栈 | +| Analytics/Dashboard | publish 有流量后 → 断言真访问数据图;**无流量 → 断言诚实空态**(不造假图) | 真部署 + 真流量 | +| design/spreadsheet/**research/viz** 交付卡 | 跑 mode 产物 → 断言对话流出现可下载交付卡(点击→下载/打开);4 mode 各一条 | gpt-5.5 跑 mode | +| access-tokens(M21.a) | 签发 token → 断言列表出现 + 能调通 axon proxy;吊销 → 断言失效 | M21.a 实现 | +| **building 进度 UI(web-04/10)** | coding_pipeline_state 进行中 → preview pane 显式 building 进度条(非裸 iframe loading);done → 切真预览 + 进度条消失 | 前端可做(mock 事件 eval)+ 接 live loop 验 | +| **Websites 计费明细(40-billing)** | M15 publish 埋点后 → UsagePane Websites tab 断言真部署/带宽计费行;无数据→诚实空态 | M15 埋点 + 真部署 | +| **Delete account(44)** | 点删号→二次确认→调 NewAPI 销户端点→断言登出+清态 | ⚪ NewAPI 红线(账号是 NewAPI 域,需其销户 API;不自建) | +| **模板 Add reference / Import Figma(web-06)** | 点 Add reference→上传/URL→断言进 spec context;Figma→OAuth→拉设计 | Figma API + 产品决策 | + +### 6.3 反向(护城河)功能点的高保真参考 — 双向追溯闭环 + +| 护城河功能(§3.2) | 高保真/截图参考 | eval 验收 | +|---|---|---| +| Skill 组合(M17.p) | **新建 [../prototypes/skill-composition.html](../prototypes/skill-composition.html)**(agent 原生组合愿景) | 见 6.2 composer Skills | +| coding 流水线 + UI 契约裁判(M5-M9/M11) | 真 UI:CodingPipelinePanel / CodingSpecCard / BlueprintCard + [../prototypes/commander-*.html](../prototypes/) | `CodingPipelinePanel.trust.test.tsx`(17 项) | +| Database/Deploy/我的电脑(M13/M15/M20) | 真 axon UI(DatabaseTabBody/DeployTab/MyComputerPane) | `DatabaseTabBody.test.tsx` + curl | +| markdown_to_pdf/render_diagram/Browser(M10.f/护城河) | 后端工具,经对话流 tool 卡呈现(无独立 UI=无需高保真) | 工具单测(各 tool .test.ts) | + +> **闭环**:正向缺口(§3.1)→ M21 spec 补;反向缺口(§3.2)→ 真 UI 截图 / prototypes 高保真补 / 后端工具标无需 UI。参考系双向完备后,§6 的 eval 验收才是每功能点「完成」的唯一硬门。 diff --git a/docs/manus-playbook-coverage-analysis.md b/docs/manus-playbook-coverage-analysis.md index 15e45ab4..90ceb233 100644 --- a/docs/manus-playbook-coverage-analysis.md +++ b/docs/manus-playbook-coverage-analysis.md @@ -1,5 +1,21 @@ # Manus Playbook 覆盖分析 (2026-05-23) +> ## ⏱️ 2026-06-01 现状对账(本文以下正文是 2026-05-23 的 M5-M9 ROI **预测**,现已成现实,按下面对账读) +> +> **预测已兑现**:本文当时判断「M5 一旦落地,立即解锁 ~31 个 playbook,38%→69%;M7-M8 再解锁 ~22;M6 再 ~20;M9 再 ~8;只剩 ~6 connector 结构性差距」。**M5-M9 现已全部落地并实证闭环**([[project_m5_coding_agent_landed]]…[[project_m9_collab_landed]]、真闭环 e2e 见 [[project_coding_app_canvas_visibility_landed]])。即**当前可达覆盖 ≈ 94%**(100 − ~6 connector 结构性差距),不再是 38%。 +> +> **「编程护城河必赢战场」(E/F 类)实证状态**: +> - ✅ **ai-website-builder** — website mode 单模板路线已交付。 +> - ✅ **crm-dashboard / F 类 dashboard** — 真 e2e 实证:viz chip → coding agent 全 5 层 → deliver_app → 画布可见,1/1 PASS;产物经扒码验证为**竞品级质量**(数据驱动柱状图 + XSS 防护 + 可访问性 + 响应式 + supertest 真集成测试),非过门 gaming(直接补上本文「不确定项 #5:未验证子页产出质量」)。 +> - ✅ **code-generator / everything-calculator** — 同属「web app = React/Express 工程」,走已实证的同一条 coding 流水线(dashboard/TODO API 两个 e2e 已证)。 +> - ✅ **chrome-extension-builder(2026-06-01 接通)** — verify-first 实证:coding 流水线**完全能处理扩展**(真 e2e 全 5 层 PASS:含 countWords 纯函数单测过 test 门 + 文件存在性过 acceptance 门 + agent 自动建 dist/ 编译产物甚至自打 zip)。原担心「test/acceptance 门卡死无 server 的扩展」是错的;**真 gap 窄到只剩交付消息一处**:deliver_app 的 no-devScript 分支原只回泛泛「无 dev server」死胡同。已修:识别 manifest.json(优先 dist/ 可加载目录)→ 给 export-zip 下载链接 + chrome://extensions 加载已解压指引。4 点验证(流水线 e2e / 检测逻辑真产物 / typecheck / 真 zip 下载返回可装 V3 扩展)。改动仅 conversation.ts(避开 shared/types.ts 的 trust overlay 污染)。 +> +> **真实剩余缺口(按优先级)**:① ~6 connector 结构性差距(Instagram/Meta/Slack/Gmail,需 connector 体系,外部依赖);② 持续追踪类(M9 已落地但需真实用户基数才有意义)。**chrome-extension 已接通后,E 类(编程护城河必赢战场)5 个场景全部接通。** +> +> **mode 维度对齐**:12 mode 全体检收官(slides 重做 + viz/spreadsheet/app/desktop-app 路由 coding + video/design/audio AI-native)+ chip mode 首条丢失 bug 修复,见 `docs/spec/roadmap/mode-parity-overview.md`(commit c5280420)。 +> +> **下一前沿不在本文(playbook 覆盖已 ~94%)**:按 [[project_reality_blood_bar_status]],真血量在真人交付(=0)/ 付费(=0),milestone/覆盖率全绿 ≠ 通关。 + ## 一行摘要 axon 当前 12 mode 已能直接覆盖 **~38%** 的 Manus playbook(约 38/100);**~50%** 需要 M5-M9 落地后才能跑通;**~12%** 是结构性差距(连接器/外部 API),axon 在不接 Connector 之前永远做不到。**编程护城河场景**(code-generator、ai-website-builder、chrome-extension-builder、everything-calculator、crm-dashboard-generator 等)应作为 M5 后优先打的差异化战场——这一批 axon 比 Manus 强。 diff --git a/docs/prototypes/commander-cockpit.html b/docs/prototypes/commander-cockpit.html new file mode 100644 index 00000000..14514985 --- /dev/null +++ b/docs/prototypes/commander-cockpit.html @@ -0,0 +1,185 @@ + + + + + +指挥官驾驶舱 · axon + + + +
连接驾驶舱…
+
+ + + + + diff --git a/docs/prototypes/commander-game.html b/docs/prototypes/commander-game.html new file mode 100644 index 00000000..54f0e5ca --- /dev/null +++ b/docs/prototypes/commander-game.html @@ -0,0 +1,383 @@ + + + + + + +《指挥官》· axon + + + +
+ +
+
⟡ 指挥官
NotesSaaS
+
+
⏱ 回合 / 截止0 / 16
+
🔍 注意力(本回合)
+
🛡 稳定性
+
🐛 抽查捕获 bug0
+
⚔ 真 BOSS · 真人用户
+
+
+ +
+
+
+ +
+ 收起 + 怎么玩
+ · 点 🟡 据点 = 花注意力验证它 → 🟢
+ · 点 ▶ AI 推进 = AI 把能造的堆成 🟡(比你快)
+ · AI 在没验证的依赖上盖楼 → 🔴 断裂接缝,每回合蚀稳定性
+ · 看不见哪个 🟡 藏 bug——验证才知道
+ · 把 🚀交付 验证到 🟢 = 干净上线 = 赢
+ · 或 强行交付 赌一把(带病上线可能崩) +
+ +
+ +
+
+ 🟡 待验证(藏 bug?) + 🟢 硬门证过 + 🔴 断裂接缝 + ⬜ 迷雾(未解锁) +
+ + +
+ + + + + + diff --git a/docs/prototypes/commander-map.html b/docs/prototypes/commander-map.html new file mode 100644 index 00000000..b7d08ebd --- /dev/null +++ b/docs/prototypes/commander-map.html @@ -0,0 +1,377 @@ + + + + + + +指挥官战役地图 · axon + + + +
+
+ + +
+
+
⟡ 指挥官战役地图
+
PROJECT · loading…
+
+
+
+
⚔ 现实血条 (BOSS HP)40%
+
+
+ 代理: 4/10 形态 + 真BOSS: 真人 0 + 终极: 付费 0 +
+
+
+ + +
+
+ +
+
+ + +
+ +
QUEST
+

+ +
+
+
+
+ + +
+ 🟢 硬门证过(typecheck/测试真跑绿) + 🟡 模型自评·未硬验 + 🔴 断裂接缝(建在未完成依赖上) + ⬜ 战争迷雾(未抵达) +
+
点据点看情报 · 拖不动它,但你能指挥它
+
+ + +
+ + + + diff --git a/docs/prototypes/commander-play.html b/docs/prototypes/commander-play.html new file mode 100644 index 00000000..aff87198 --- /dev/null +++ b/docs/prototypes/commander-play.html @@ -0,0 +1,179 @@ + + + + + +码情值班 · axon + + + +
加载真实码情中… fetch ./code-situation.json
+ + + + + diff --git a/docs/prototypes/commander-real.html b/docs/prototypes/commander-real.html new file mode 100644 index 00000000..915c14a7 --- /dev/null +++ b/docs/prototypes/commander-real.html @@ -0,0 +1,190 @@ + + + + + +码情 · axon + + + +
+
加载真实码情中…
fetch ./code-situation.json
+ + + + + diff --git a/docs/prototypes/skill-composition.html b/docs/prototypes/skill-composition.html new file mode 100644 index 00000000..093365a9 --- /dev/null +++ b/docs/prototypes/skill-composition.html @@ -0,0 +1,141 @@ + + + + + + +axon — 可组合技能 高保真 + + + +
+

可组合技能 编程护城河 · Manus 无

+
对齐 Manus「chip = Skill」并超越:axon 让 agent 原生判断**组合多个**技能。用户在 composer + 选技能(可选),agent 据输入自主激活/组合。
+ +
+ +
+
帮我做一份西湖十景的演示文稿,并设个每周一早上的定时任务自动更新数据…
+
+ 📑 制作幻灯片 × + ⏰ 定时任务 × + 🪄 agent 自动组合 +
+
+ + 附件 · 技能 + +
+
+ + +
+
使用技能 · 选了 agent 必用,不选 agent 自判断
+
交付形态
+
+ 📑 +
制作幻灯片
HTML deck 逐页渲染 + 图片生成封面
+ 已选 +
+
+ 🌐 +
创建网站
真 Vite/React 工程 + 实时预览
+ 激活 +
+
+ 🎨 +
设计
GPT Image 生成图,落盘可编辑
+ 激活 +
+
+
自动化 / 研究
+
+ +
定时任务
Cron 调度,按计划自动跑
+ 已选 +
+
+ 🔍 +
Wide Research
多源检索聚合 + 子 agent 扇出
+ 激活 +
+
+ 📊 +
可视化
数据 → 图表 dashboard
+ 激活 +
+
+
…共 20+ 内建技能(algorithmic-art / mcp-builder / code-review / promo-video …)
+
+
+ + +
+

护城河差异点:agent 原生组合,不是代码路由

+

Manus 一次激活一个 chip(skill)。axon 把 12 mode 全迁成可组合 skill(COMPOSABLE_SKILL_NAMES),agent 据输入原生判断激活哪几个并组合执行——上例「做 deck + 每周定时更新」= 制作幻灯片 + 定时任务 两技能组合,由 agent 自调 activate_skill,不靠代码 if/else 路由([[project_code_routing_removal_pivot]])。

+
+ 用户输入 + agent 判断需要的技能 + activate_skill ×N(组合) + 按各技能流程交付 +
+
+
+ + diff --git a/docs/spec/README.md b/docs/spec/README.md index ecf68060..b01cd539 100644 --- a/docs/spec/README.md +++ b/docs/spec/README.md @@ -125,6 +125,9 @@ F.1-F.20 视频核验已有假设(含 F.4 金色 inline 状态条 / F.7 sandbo | [M9](roadmap/M9.md) | B.9.1-3 / F.4 | 待设计 | | [M10](roadmap/M10-overview.md) | B.2 / B.8 / C.3 / D.1 / D.5 | M10.h `m10-*` 套件(详见 M10-overview §下一步) | | [M11](roadmap/M11-overview.md) | C.3.4(UI 真验通过门)/ D.5.4(ui_contracts 自动裁判)/ 间接 B.2/B.8 | M11.g `m11-ui-probe` 套件(详见 M11-overview §Eval 计划) | +| [M20](roadmap/M20-overview.md) | C-product(应用骨架层:Settings/Personalization/Usage&Billing/Library) | 待落地(前端 jsdom 组件测试 + Browser 真走截图) | + +> ⚠️ **本表自 M12 起未维护**:M12–M19(+ M20)的完整 milestone 列表与状态见 [roadmap/](roadmap/) 目录各 `M*-overview.md` 文件 + 项目记忆 MEMORY.md。本 acceptance 映射表是 M5–M11 时期的历史快照,仅 M20 因新建一并登记,中间 M12–M19 索引以 roadmap/ 目录为准。 详细 eval-acceptance 映射见 [test-matrix.md](test-matrix.md)。 diff --git a/docs/spec/decisions-pending.md b/docs/spec/decisions-pending.md index a36e81c3..0b90a639 100644 --- a/docs/spec/decisions-pending.md +++ b/docs/spec/decisions-pending.md @@ -131,4 +131,16 @@ Manus V6 两条协议纪律,评估 axon 是否采纳: - **裁决**:**不引入 Manus 的 slides 工具 schema**。理由: 1. 与编程护城河一致:slides 当成代码工程交付(可改、可跑、可托管),比黑盒 generate 更强、更可控 —— 这是 axon 的差异化,不是缺口。 2. `slide_content_file_path/slide_count/generate_mode` 是 generate-tool 形态的入参,跟 axon 的 mode+真代码路线根本不同构,硬塞=倒退。 -- **状态**:✅ 已决(保留 mode + 真代码路线,不抄 slides 工具 schema)。导出能力(slides→PDF/PPT)属 M12.l,另议。 +- **状态**:⛔ **2026-06-01 此决策被用户推翻**(AskUserQuestion 明确拍板「图片生成主导,纯对齐 Manus」)。 + +**推翻复盘(重要,防再犯)**:用户实测合肥 deck 只得到一堆模板化 HTML(甚至 preview 报 `File does not exist`),指出 slides 的核心价值是**视觉表现力**,要对齐 Manus 的 `generate_mode=image`(图片生成 / Nano Banana 等价),不是纯代码工程。根因三条: + 1. 本条「编程护城河」决策是**前身 session(Opus 4.7)自己拍的板,没回来跟用户确认**;用户口头的「slides 用图片生成」偏好**从未进记忆/spec**,反向决策却进了 → 2026-05-31 的 slides 重做严格执行了错误方向。 + 2. **违反铁律2**:把 app/dashboard(要交互/要运行)的「真代码护城河」逻辑教条地套到 presentation(要好看)上,用一套重工程(模板+toolFilter+内容硬门+postprocess)**绕开了 AI 原生的图像生成表现力**。 + 3. slides 品类的价值函数 ≠ app/website 的价值函数,不能用同一把「护城河」尺子。 + +**新方向**:slides 重建为 **image-generation deck**(每页 image_generation 出整页视觉 + 可翻页 deck 查看器),纯对齐 Manus image mode。数据图表作为视觉元素接受不精确(用户已知并接受此取舍)。导出能力(slides→PDF/PPT)属 M12.l,另议。 + +**★2026-06-02 引擎已锁定 + 真图验证★**:用户指正「codex GPT 5.5 内置的是 gpt-image(GPT Image),中文很棒」——**实证确认用户对、我错**。强制 spike(真 server :3456,禁止写代码、只准内置 image_generation)生成合肥封面 `/tmp/spike-codex-img-1.png`:深蓝商务风 + 完整合肥天际线城市插画 + 大标题「合肥城市介绍」+ 副标题「区域经济与科创高地·2026」,**中文清晰准确零乱码、表现力强于代码渲染**。结论: +- **引擎 = codex 内置 gpt-image**(chatbi.site /responses 的 image_generation hosted tool)。已付费、已集成、中文优秀,**不需要 Gemini/Nano Banana 付费**(Nano Banana 那条作废,免费档 0 配额)。 +- 我此前「图片中文易糊」「推荐自由代码渲染」的判断**收回**(铁律4 双向:用户领域判断对)。 +- **真正待解的工程问题(非引擎)**:① base agent 被编程身份洗得不肯调 image_generation(spike 证明:必须强 prompt 禁代码才会调)→ slides mode prompt 要强制走图片生成;② 生成图是 base64 内联聊天、不落盘 → 要落盘成 slide-N.png + 拼可翻页 deck(复用 deliver_slides→website_delivered→画布 iframe,但要解决 iframe 加载同级 PNG 资源的服务问题);③ 交付硬门从「模板签名」改成「≥N 张图」。 diff --git a/docs/spec/roadmap/M17-overview.md b/docs/spec/roadmap/M17-overview.md new file mode 100644 index 00000000..9b8bf1eb --- /dev/null +++ b/docs/spec/roadmap/M17-overview.md @@ -0,0 +1,94 @@ +# M17 Overview — 指挥官全局地图:编程流水线信任叠加层(看得见的码情) + +> 📍 **父文档**: [../README](../README.md) | 🎯 **北极星**: [../vision.md](../vision.md)(强化**主线 A 编程护城河**的可观测性 + **红线 #8「LLM 永不当裁判」**的展示层)| 📊 **现状**: [../status.md](../status.md)(顶部「现实血条」即 M17.e) +> 🎯 **本文定位**: M5-M11 把编程流水线的**裁决**做成了硬门(typecheck/测试/验收/UI 契约),但那些门的真伪状态**没有变成人能一眼读懂的全局视图**。M17 把已有的硬门信号**渲染成指挥官战图**——区分「🟢 server 硬门盖章 vs 🟡 模型嘴上说的 vs 🔴 断裂接缝」,让被 vibe coding 淹没的人从「读代码的审查员」变回「读战图的指挥官」。**这是观测/UX 层,不新增裁决逻辑——裁决仍由 M7/M8/M11 的硬门负责。** +> 📂 **子任务**: M17.a 阶段信任配色 ✅ / M17.b 行为验证缺口 ✅ / M17.c 断裂接缝+依赖地图 ✅ / M17.d 高危改动 ⚠(押后,需埋点决策)/ M17.e 现实血条 ✅ / M17.f 真码情可视化+双镜片可玩原型 ✅ / M17.g 执行层(真灭火/盖章/发货,REST 原型)✅ / M17.h 派活手柄(编码动作长进 ManusWorkbench)✅ / M17.i 双镜片进 per-session 面板(⚔工程/🏰王国)✅ / M17.j 独立指挥官常驻面板(cockpit 毕业进产品:观测+执行+双镜片)✅ / M17.k 现实血条 BOSS HP HUD(三层不可刷真进度)✅ / M17.l axon 自蓝图 dogfood(roadmap 成真 blueprint + eval 硬门 + 全屏页 + 函数级下钻 + 里程碑 checklist)✅ / M17.m 双人格补齐(王国镜片产品楼层视图)✅ / M17.n ★蓝图变活验证状态机(门驱动状态,手敲 done 打回 🟡,跑门落盘 + 依赖门)★✅ +> 🧪 **验收映射**: 强化 [../acceptance/C-product.md](../acceptance/C-product.md)(交付物质量的**透明度维度**)+ [../acceptance/D-axon-unique.md](../acceptance/D-axon-unique.md)(护城河独特性:不只是能跑,还**让人看得见哪里没验证过**)。 +> ⛓️ **依赖**: M5-M11 全落地(M17 消费它们落盘的 spec.json / blueprint.json / test-report.json / acceptance.json / ui_contracts 信号)。不阻塞任何下游。 + +--- + +## 实施进度(2026-06-01 /loop,未提交,部分随 c5280420 被 chip-mode 工作连带提交) + +| 子任务 | 状态 | 备注 | +|---|---|---| +| **M17.a** 阶段信任配色 | ✅ | `CodingPipelinePanel` 从「完成/未完成」升级到「硬门证过 🟢 vs 模型自评 🟡」。验收门 `allPass` 改 `claimed`🟡「模型自评·非硬测」**绝不画绿**;ui_contracts `pending` 显 🟡「空跑」;`countHardDone` 不数 claimed(X/N 不被"模型说完了"灌水)。纯逻辑抽 [pipelineTrust.ts](../../../src/web/client/src/pages/ManusWorkbench/pipelineTrust.ts) | +| **M17.b** 行为验证缺口 | ✅ | 蓝图全 done 但测试门没绿 → 🟡「这些'完成'只到能编译,行为未验证」。零新埋点(现有 snapshot 派生) | +| **M17.c** 断裂接缝 + 全局地图 | ✅ | snapshot 带出 `dependsOn`;task 后渲染依赖邻接表("没全局地图"痛点);`seamRisk` 检测断裂接缝 🔴(下游 done 但上游没 done=绿勾撒谎,实读 `handleVerifyTask` 确认它只跑 typecheck 不查依赖→信号不空) | +| **M17.e** 现实血条 | ✅ | [../status.md](../status.md) 顶部钉「BOSS HP」,**脱钩 milestone**:代理 BOSS=真实端到端体检任务数(4/10 形态) → 真 BOSS=真人交付(0) → 终极=付费(0)。防 RPG 刷支线伪装主线进度 | +| **M17.f** 真码情可视化 + 双镜片可玩原型 | ✅(原型) | [scripts/gen-code-situation.mjs](../../../scripts/gen-code-situation.mjs) 采**真 git/真测试/真模块**→JSON;[commander-*.html](../../../docs/prototypes/) 4 个原型(地图/教学游戏/真仪表盘/真可玩值班),**双镜片**:⚔指挥官(中年程序员)/🏰王国(白领)。serve 走 Vite public 目录 → `http://localhost:3456/prototypes/` | +| **M17.d** 高危改动 ⚠ | ⏸️ 押后 | 需"每个 task 改了哪些文件"——task schema 无 `files`、不保证 git-init。**真架构 fork 待用户拍**:A=git-init+verify_task commit(E2B 沙盒下 git 复杂)/ B=拦 Write/Edit 累积(沙盒友好,推荐)。阈值/敏感路径清单(auth·支付·删除·.env·迁移)需用户定 | +| **M17.g** 执行层(真灭火/盖章/发货) | ✅ | `routes/code-situation.ts` 挂 `/api/code-situation`:GET 重采真码情 / POST run-tests 真跑 vitest / POST commit·push 真 git(复用 GitManager,硬 require confirm + 拒 main)。[commander-cockpit.html](../../../docs/prototypes/commander-cockpit.html) 真驾驶舱。验证:run-tests 真返 21✓/0✗、commit/push 无 confirm 一律 400。**观测→执行闭环接通** | +| **M17.h** 派活手柄(编码动作长进 ManusWorkbench) | ✅ | 用户「有测试动作,为什么没有编码动作」逼出的真缺口:观测/运维都缺**指挥官主动词「派活」**。`CodingPipelinePanel` 每处 🔴/🟡 火旁加「🔥 派活灭火」→ 经 ChatPane `handleDispatchFix` 把火翻成 coding-mode 指令注入**真 live loop**(`messagingRef.send` chat,复用 `handleSend` 同一条;session 已在 coding 态被守卫留在编码 agent)。agent 真改码 + 自己重跑 `run_blueprint_tests`,snapshot 刷新即「灭火」验证——**验证那一棒由 agent 自闭环,不造第二个按钮**(AI-native,铁律2)。**为什么落在 workbench 不落 REST 原型**:编码引擎=ws+`conversationManager.chat()` 会话态,REST 原型 host 真编码只有「造假」或「复制整套栈」两条死路(违铁律2)→ 派活逼原型毕业进真产品 | + +| **M17.i** 双镜片进 per-session 面板 | ✅ | 用户 /loop「你既是被 vibe coding 困扰的中年程序员、也是不懂开发的白领」逼出双人格内核搬进真产品。`CodingPipelinePanel` 加 ⚔工程/🏰王国 镜片切换:同一份真 snapshot,工程镜片说程序员话(spec/蓝图/task),王国镜片 `kingdomLens.ts` 翻成白领「王国建设」叙事(阶段=工程节点、task=城区、火=险情)。★诚实穿过翻译★:claimed🟡「工头自己说达标了(没有第三方验过)」绝不变绿「完工」。纯逻辑 [kingdomLens.ts](../../../src/web/client/src/pages/ManusWorkbench/kingdomLens.ts) | +| **M17.j** 独立指挥官常驻面板(cockpit 毕业进产品) | ✅ | 用户「把 cockpit 的 ops(灭火/盖章/发货)并进主面板」。★结构纠偏★:cockpit 盯 **axon 自身仓库**(`process.cwd`),`CodingPipelinePanel` 盯 **per-session app**(`state.session.cwd`)=两张不同战图,焊一起=脚枪(看 app 却推 axon)。故不焊接,而是 [CommanderPanel.tsx](../../../src/web/client/src/pages/ManusWorkbench/CommanderPanel.tsx) 独立常驻面板(用户选「悬浮🎖️按钮→右侧抽屉」),盯 axon 真码情 + 双镜片 + 执行层(🔥查火=GET `?tests=1` 真跑 / ✅盖章=POST commit / 🚀发货=POST push,内联二次确认 + 服务端硬 require confirm + 拒 main)。复用 M17.g 路由 + GitManager。纯翻译 [commanderLens.ts](../../../src/web/client/src/pages/ManusWorkbench/commanderLens.ts) | + +| **M17.k** 现实血条 BOSS HP HUD | ✅ | 用户「我全程在看、不知真进度、心慌」的正解 = 一个**不可刷**的诚实进度。`gen-code-situation` 从 status.md 真解析**三层**:代理 BOSS(forms/target 真实形态)+ 真 BOSS(realDelivery 真人非作者交付)+ 终极(paying 付费 + mrr)。CommanderPanel 顶部渲染 BOSS HP 块:代理进度条 + 真/终极 BOSS(=0 时红警「还没通关——代理满也只是热身,绿勾再多不算」)。★anti-gaming★:real/paying 照 status.md 真值 0 就显 0。纯逻辑 `bossHp()`,解析对真 status.md 验证过(4/10·0·0)| + +| **M17.l** axon 自蓝图 dogfood ★ | ✅ | 用户「spec 里程碑缺 checklist,搞笑的是 axon 没把蓝图用在自己身上」逼出。★架构纠偏★:字面跑 M7 `generate_coding_blueprint` 在 roadmap 上=类型错位(M7 verify=typecheck/task=代码单元,里程碑不是可 typecheck 的代码单元)→ 真 dogfood = 把蓝图灵魂(结构化 tasks+确定性硬门)套自己身上,里程碑硬门=它的 **eval 套件**(铁律11,验自己里程碑=跑它的 eval,正如 M7 验用户代码=跑 typecheck)。产物:①[blueprint.json](blueprint.json)=axon roadmap 用**真 M7 schema**(tasks+dependsOn)表达+每关卡挂真 eval 门(m5-coding-agent…m8-deliver/e2e-full-flow)+子任务 checklist;②`gatherMilestones` 读 blueprint.json 结构化(**不再 regex 猜 prose**——根因修复);③`POST /verify-milestone` **真跑硬门**(vitest 门活跑取绿/红、重型 eval 门诚实返命令不假跑),活体 M17→commanderLens.test.ts 8✓ green:true;④顺带满足前 3 条反馈:CommanderPanel 改**全屏独立页**(drawer→page)+模块**函数级下钻**(git diff 行号上溯定位函数)+火情滤 logs/.pptx 垃圾 | + +| **M17.n** 蓝图变活验证状态机 ★ | ✅ | 用户「蓝图你没用上啊,太简单了」点破 M17.l 两宗罪:①里程碑 status 是我**手敲 "done"**=正是信任叠加层在骂的「🟡 嘴说伪装 🟢」(自我打脸)②蓝图是死展示不是引擎。修:blueprint.json 的 status 只当「我声称的」(claim),**trust 由门裁决**——🟢 verified 只能来自 `verify-milestone` 真跑 gate 绿 + 落盘 `.milestone-verified.json`(gitignored) + 依赖也验过;手敲 done 一律打回 🟡 claimed;gate 绿但依赖没验=🔴 broken_seam(断裂)。activeable=下一个可验证(声称done+依赖验过+有门没跑)。活体实证:初始 0 verified/11 claimed→跑 M17 门(commanderLens.test.ts 9✓)→落盘→重算 M17=broken_seam(门绿但 M5-M11 没验,依赖门生效)。回归即扣血(红了撤销旧🟢)。commander 头部诚实报「🟢gate验过 N·🟡我声称的 M」。**这才是用蓝图驱动一场真验证战役** | + +**测试**: 全部 **51/51 全绿** + tsc 绿 + vite 构建绿(live bundle 含全部):`pipelineTrust.test.ts`(13) + `kingdomLens.test.ts`(8) + `commanderLens.test.ts`(8:含 BOSS HP 三层+win 路径) 纯逻辑(招牌 truth-negative:自评/着火/真BOSS=0 翻成白领话绝不粉饰);`CodingPipelinePanel.trust.test.tsx`(17:信任渲染+派活+双镜片) + `CommanderPanel.test.tsx`(5:fetch 渲染+镜片+盖章/发货带 confirm:true) jsdom。活体验证:GET /api/code-situation 真返(dirty 35/unpushed 3/10 模块 + 三层 reality 4/10·0·0)+ commit/push 无 confirm 一律 400。 + +**M17.h 运维红利**:纯客户端改动(ChatPane/CodingPipelinePanel/.css,零服务端)→ prod server `express.static` 每请求从磁盘读 → **只 `npm run build:web:client` 重建 bundle,刷新浏览器即生效,不重启 server 进程**(对比 M17.g 加服务端路由必须重启 :3456)。这正是「骑现有 ws+chat() 栈、零复制」的回报。 + +--- + +## 一句话目标 + +把编程流水线的**真伪状态**从「一片绿勾(分不清机器证过的还是模型嘴上说的)」升级到「**指挥官战图**:🟢 硬门盖章 / 🟡 模型自评 / 🔴 断裂接缝 + 战争迷雾」,让人不必读完每一行 diff,也能一眼看出**该把稀缺的注意力花在哪三处**。 + +--- + +## 起源与第一性 + +2026-06-01 用户(老程序员)连续抛出三层洞察,逼出这个里程碑: + +1. **"用 spec 推 milestone 像玩上古卷轴 RPG"** —— 拆大任务/大上下文成小任务/小上下文,正好解模型上下文不足;游戏化降低门槛,普通人也能像玩游戏一样交付软件。 +2. **"vibe coding 大项目时,我阅读速度完全跟不上模型生成速度…没有全局地图,看不到哪里可能有 bug,没法管理进度和质量,很心慌——因为游戏不是我玩的,我全程在看。"** +3. **"游戏是假的,我要真的,接 axon 自己的码情;你既是被 vibe coding 困扰的中年程序员,也是不懂开发的白领。"** + +**第一性诊断**:在 generation-speed 下,「读代码」这个角色已经死了——你不可能读完模型生成的每一行,正如你不会去读一个 RPG 引擎的 C++ 源码。出路不是读快点,是**换座位:从代码审查员(观众席)挪到指挥官(指挥席)**,读 HUD/战图而非源码。axon 已经有裁决(M7/M8/M11 硬门),缺的是**把裁决渲染成战图**的观测层——这就是 M17。 + +★**这层的铁律(违反则整层作废)**★:**🟢 done 只能由 server 端确定性硬门盖章**(`verify_task` 真跑 typecheck / 测试门真跑绿)。模型自评的验收门**永远顶天 🟡**。理由:信任叠加层是专家的**信任锚点**,自己绝不能撒谎;绿得到处都是只会让专家更慌,**标出战争迷雾**才能让人放松地抽查——把稀缺读码带宽只花在 🟡🔴 上。同源「[硬门 > prompt 祈祷](../../../)」全项目范式。 + +★**双镜片(M17.f 的想象力内核)**★:同一份真实码情,两副镜片——**⚔ 指挥官**(中年程序员:「我也读不完这 N 个 diff,替你把烧着的标红,你只看红的」)/ **🏰 王国**(不懂开发的白领:模块翻成城区、失败测试翻成"着火的楼"、现实血条翻成"几个真客户",零代码黑话)。一个工具同时服务"淹没的专家"和"看不懂代码的决策者"。 + +--- + +## 核心范式(改这套时必懂) + +- **观测层 ≠ 裁决层**:M17 只**读** M5-M11 落盘的信号(spec/blueprint/test-report/acceptance/ui_contracts)渲染,**不新增任何门**。改裁决去 M7/M8/M11,改展示来 M17。 +- **纯逻辑可纯函数 eval**:信任配色/接缝检测全抽 [pipelineTrust.ts](../../../src/web/client/src/pages/ManusWorkbench/pipelineTrust.ts)(零 react/css 依赖),诚实性被纯函数测试钉死(招牌:自评 100% 但测试红→断言绝不全绿,复刻 m8-harness-truth-negative)。React 接线另有 jsdom 渲染门(教训:纯测过≠接线没断,见 [[project_coding_app_canvas_visibility_landed]])。 +- **真数据,不编**(M17.f):原型吃 `gen-code-situation.mjs` 实采的真 git/真测试/真模块,点「调查」弹真失败测试文件名。**地图烧的红全是 axon 此刻真实的样子**。刷新=重跑采集器。 +- **运维约束**:服务器 dev 模式跑 **Vite 中间件**(非 express.static dist),原型放 `src/web/client/public/prototypes/` 才能被 :3456 serve(用户远程只能上 :3456,本地 file:// 没用);:3456 是 **http 不是 https**。 + +--- + +## 子任务拆解 + 下一步 + +- **M17.a/b/c/e/f/g/h/i/j 已落地**(见上表)。观测层已在 ManusWorkbench 里「长全了手」:①per-session 的 `CodingPipelinePanel` = 派活(M17.h) + 双镜片(M17.i);②独立常驻 `CommanderPanel`(M17.j) = 盯 axon 自身真码情 + 双镜片 + 灭火/盖章/发货。**两张互补战图**:per-session 盯「用户 app」、指挥官盯「axon 仓库本身」(作用对象 cwd 不同,刻意不合并——见 M17.j 结构纠偏)。cockpit/commander-*.html 原型已被 `CommanderPanel` 真产品取代(原型仍可留作设计靶子)。 + - **下一步可选**:M17.d 高危改动埋点(唯一真缺口,待拍);或把 M17.f 的「现实血条/真客户」做成 CommanderPanel 里更醒目的常驻血条;或给 CommanderPanel 的火接「派活」(指挥官面板里点 axon 自身的火 → 派 Claude Code/sub-agent 修,但 axon 自身开发走的是**我**这条 Claude Code 链路非 ws chat,需另接)。 +- **M17.d 高危改动 ⚠(待拍)**:唯一真缺口。需先定 changed-files 埋点架构(git-init vs 拦 Write/Edit)+ 敏感路径清单。这是产品决策点,不自动 barrel。 +- **M17.g 执行层 ✅ 已落地**(2026-06-01,用户拍板"游戏=观测+执行"):观测 → 执行的闭环接通。[routes/code-situation.ts](../../../src/web/server/routes/code-situation.ts)(挂 `/api/code-situation`):GET 重采真码情 / POST run-tests 真跑 vitest(只读)/ POST commit 真 stageAll+commit / POST push 真 push origin HEAD。复用 GitManager(铁律2)。**安全**:commit/push 硬 require `confirm:true`(前端再叠弹窗)+ push 物理拒绝 main/master(只许 feature 分支)。[commander-cockpit.html](../../../docs/prototypes/commander-cockpit.html) = 真驾驶舱(观测+真灭火/盖章/发货,动作完观测从真结果刷新)。**加上执行层那一刻它就不再是"游戏"——是真命令控制台**;下一步该长进 ManusWorkbench,不再是 docs 原型。 + - ★运维:执行层路由是**服务端代码**,dev 模式 tsx(非 watch)不热重载 → **加路由必须重启 :3456**(kill 旧 PID→`npm run dev` 后台→curl --retry-connrefused 等起来→验路由)。settings.json 存 key 故重启零配置丢失。 + +--- + +## 明确不做(边界) + +- **不让 LLM 当裁判**(红线 #8):M17 只渲染确定性硬门的结果,绝不引入"LLM 看一眼说像对的"作为信任来源。 +- **不替代 M11 校准**:ui_contracts 的 confirmed/pending 真伪来自 M11.d 的 baseline 校准,M17 只显示不改判。 +- **不在 M17 做高危 diff 埋点**:M17.d 的 changed-files 采集是新埋点 + 架构决策,独立拍板后再做,不混进观测层。 + +--- + +## 相关文档 / 产物 + +- [pipelineTrust.ts](../../../src/web/client/src/pages/ManusWorkbench/pipelineTrust.ts) + 测试 — M17.a/b/c 纯逻辑 + 诚实性门 +- [scripts/gen-code-situation.mjs](../../../scripts/gen-code-situation.mjs) — M17.f 真码情采集器 +- [docs/prototypes/commander-{map,game,real,play}.html](../../../docs/prototypes/) — M17.f 四原型(地图/教学游戏/真仪表盘/真可玩值班) +- [../status.md](../status.md) — 顶部「现实血条」= M17.e +- 记忆: `project_trust_overlay_slice1_landed` / `project_reality_blood_bar_status` diff --git a/docs/spec/roadmap/M18-overview.md b/docs/spec/roadmap/M18-overview.md new file mode 100644 index 00000000..4c2edbfd --- /dev/null +++ b/docs/spec/roadmap/M18-overview.md @@ -0,0 +1,106 @@ +# M18 Overview — 前后端契约硬化:让 ws 协议从「文档」变回「编译期强制」+ 统一 Artifact 渲染 + +> 📍 **父文档**: [../README](../README.md) | 🎯 **北极星**: [../vision.md](../vision.md)(直接服务**真 BOSS:真人交付** — 见 [../status.md](../status.md) 现实血条。"后端正常、前端空白"是真人弃用的头号杀手) +> 🎯 **本文定位**: 用户 2026-06-01 连说两次「同样的问题出现很多次,是架构问题」逼出的**根因 spec**。slides 产 .pptx、`tool_result.data` 被吞、`broadcastMessage` 死路、`envelope to:'iframe'` 写错、`data-empty` 不可达、chip mode 首条丢失……**不是 N 个独立 bug,是同一个架构缺陷的 N 个切片**:前后端契约是「隐式 + 裸字符串 + 手工镜像 + 零编译期强制」的接缝。M18 把它焊死。 +> 📂 **子任务**: M18.a 闭合 union / M18.b typed emit 收口 / M18.c 消费侧 typed+exhaustive / M18.d 统一 Artifact 渲染管线(对齐 Manus 内联页码缩略图)/ M18.e eval 跨边界 consumer 门 / M18.f(可选)根治 marker-tool data 流出 +> 🧪 **验收映射**: [../acceptance/C-product.md](../acceptance/C-product.md)(交付物**真到达用户眼睛**的维度)+ 强化 [[feedback_eval_blind_spot_frontend_wiring]](eval 必须跨 producer→consumer 边界) +> ⛓️ **依赖**: 无下游被阻塞。M18 是横切地基,越早做越省后面每个功能的「重新接一遍 + 偶尔接错」税。 + +--- + +## 1. 病根(代码实证 2026-06-01,非感觉) + +`ServerMessage`([../../../src/web/shared/types.ts](../../../src/web/shared/types.ts) L357+)是个写得很好的 discriminated union(50+ variants、typed payload、连 wiring 决策注释都有)。**但它已退化成文档**: + +### 致命证据 — 后端真 emit 的功能事件,全都不在 union 里 + +| 事件 | 在 union? | 被 emit? | 事件 | 在 union? | 被 emit? | +|---|---|---|---|---|---| +| `website_delivered` | 缺 ✗ | 是 | `coding_artifact` | 缺 ✗ | 是 | +| `coding_app_preview` | 缺 ✗ | 是 | `coding_pipeline_state` | 缺 ✗ | 是 | +| `mode_changed` | 缺 ✗ | 是 | `debug_system_prompt` | 缺 ✗ | 是 | +| `navigate_to_swarm` | 缺 ✗ | 是 | `debug_tools_for_turn` | 缺 ✗ | 是 | + +union 覆盖的是**老核心事件**(text_delta / tool_use / message_*);**每个后来加的功能事件——交付物 / coding 流水线 / mode——全部绕过 union 裸 `state.ws.send(JSON.stringify({...}))` 发**。这些恰恰是反复断的那些。 + +### 三个结构缺陷 + +- **缺陷① 协议无单一真相源 + 编译器管不到** + - 后端:`state.ws.send(JSON.stringify({ type: 'website_delivered', payload: {...} }))` — 裸字符串 + 裸对象,**conversation.ts 根本没 import ServerMessage**(grep 实证),TS 不校验。 + - 收口助手:**✗ 无** `emit()` / `sendTyped()` — 43 处 `type:'…'` 字面量散落,全裸 send。 + - 前端:`useMessageHandler` 入参 `send: (msg: any)`,`case 'website_delivered':` 裸接,**无 `default: never` exhaustive 检查** → 改个事件名/payload 字段,另一端**静默失联**,孤儿事件(如 `debug_system_prompt` 后端发、前端零 handler)不报错。 + +- **缺陷② 交付物 per-type 碎片,无统一抽象** + - slides / website / coding-app / picker / image / design **各有**自己的 ws 事件 + 卡片组件 + 渲染路径(画布 iframe / preview pane / inline)。无统一 `Artifact{kind,files,entry,renderMode}` + 一个 ``。每加一种交付物都重接一遍 producer→consumer,每次都可能接错。 + - **Manus 截图(2026-06-01 用户提供)印证解药**:Manus 把 slides 渲染成对话流内联、带页码的缩略图序列(1/12…12/12),且 outline 卡 + 页码缩略图对**任何 deck 是同一条管线** = 统一 artifact 渲染模型。"对齐 Manus UI" 与 "修缺陷②" 是同一件事。 + +- **缺陷③ eval 只测 producer 侧,consumer(真前端渲染)无门** + - 已在 [[feedback_eval_blind_spot_frontend_wiring]] 记录并起修(slides-deck 加了 `/api/files/preview` 接线门)。需推广到每种交付物。 + +### 为什么必然反复咬人 +错误接线的代价**在人看真 UI 之前完全不可见**:编译器抓不到(字符串)、eval 抓不到(测 producer/磁盘)→ 绿着上线、生产空白。这就是「频繁出现 = 架构问题」的机制。相关踩坑:[[feedback_tool_result_data_dropped]] [[feedback_broadcastmessage_is_dead_path]] [[feedback_envelope_protocol_bidirectional_spike]] [[feedback_image_gen_data_empty_trap]] [[feedback_chip_mode_lost_first_message]]。 + +--- + +## 2. 为什么这是真 BOSS 阻塞,不是刷支线 + +[../status.md](../status.md) 现实血条:milestone 全绿≠通关,真 BOSS = 真人(非作者)成功交付。**"agent 说做好了,画布/UI 空白"是真人弃用的头号杀手**——比缺功能更致命,因为它让"已完成"变成谎言。M18 不新增能力,它**移除真人交付路上一类系统性的隐形地雷**。这是对血条的直接攻击,不是点技能树。 + +--- + +## 3. 目标架构(增量收口,禁 big-bang 重写) + +核心洞察:**不要从零建 typed 协议——基建(ServerMessage union)已存在且优秀,只是被 producer 端绕过了。把它从"文档"升级回"强制"即可,改动比想象小得多。** + +### M18.a 闭合 union(让它成为真·单一真相源) +把所有正在被 emit 但缺失的事件补进 `ServerMessage`(website_delivered / coding_app_preview / coding_artifact / coding_pipeline_state / mode_changed / navigate_to_swarm / publish_progress 已在 / debug_* …)。payload 用现有 `CodingArtifactData` 等已存在类型。**纯增类型,零运行时改动,零回归。** + +### M18.b typed emit 收口(producer 端长牙齿) +新增 `emitToClient(ws: WebSocket, msg: ServerMessage): void`(一处,带 `readyState` 检查 — 见 [[feedback_broadcastmessage_is_dead_path]])。把后端 43 处裸 `state.ws.send(JSON.stringify({...}))` 逐步换成 `emitToClient(state.ws, {...})`。换完后**任何 emit 的对象都被 TS 按 union 校验**:事件名拼错、payload 字段缺/类型错 = 编译期红字。可分批迁移(先交付物族),不必一次性。 + +### M18.c 消费侧 typed + exhaustive(consumer 端长牙齿) +`useMessageHandler` 入参从 `msg: any` 改 `msg: ServerMessage`;switch 末尾加 `default: { const _exhaustive: never = msg; }`。**新增一个 ServerMessage variant 而前端没写 handler = 编译期红字** → 孤儿事件这一整类在 build 期死。 + +### M18.d 统一 Artifact 渲染管线(对齐 Manus + 消灭缺陷②) +- 协议层:定义 `Artifact = { kind: 'deck'|'webapp'|'image'|'doc'|…; entry: string; root: string; files?: number; renderMode: 'inline-pages'|'iframe'|'image' }`,一个 `artifact_delivered` 事件取代 per-type 交付事件(旧事件保留兼容期)。 +- 渲染层:一个 `` 按 `kind/renderMode` 分发。slides → `inline-pages`(对齐 Manus:对话流内联、带页码缩略图 1/N,复用 M18.e 的 `/api/files/preview` 逐页渲染)。 +- 新交付物只需声明 kind + renderMode,插进同一 typed channel + 同一 renderer,**不再手接接缝**。 + +### M18.e eval 跨边界 consumer 门(缺陷③ 推广) +每种交付物的 eval 至少一条 consumer 侧断言:GET 前端实际加载的 server 端点(slides/website 走 `/api/files/preview?path=&root=`)证明产物真能到前端。slides 已落地([../../../evals/suites/slides-deck/harness.ts](../../../evals/suites/slides-deck/harness.ts) `assertPreviewEndpointServes`),推广到 website / coding-app。 + +### M18.f(可选,根治 marker-tool 税) +`executeToolWithCancellation` 吞 `data` 这个底层 quirk 是「每个 marker 工具自造 ws 事件」的根源([[feedback_tool_result_data_dropped]])。评估是否让 marker 工具的结构化产出走**统一 typed artifact channel** 而非 N 个 bespoke 事件,从源头削减接缝数量。**先不做**——等 M18.a-d 把 typed 地基铺好再评估,避免过早抽象(铁律2/6)。 + +--- + +## 4. 试点 PoC(先证 pattern 再推全量 — 谋定后动) + +先只对**交付物事件族**(website_delivered / coding_app_preview / coding_artifact / coding_pipeline_state)跑通 M18.a+b+c 三步,证明: +1. 补进 union → typed emit 收口 → 这几个事件的 emit 处真被编译期校验; +2. 消费侧 exhaustive 后,故意删一个 handler 能触发编译错误(=孤儿事件这类真被抓)。 +PoC 绿了再机械推广到其余 39 处 emit + M18.d 统一渲染。 + +--- + +## 5. 三思 / 风险(铁律6 自我反驳) + +- **会不会过度工程化(铁律2)?** 不会——这不是新建抽象层,是**把已有但被绕过的契约接回去**。改动以「补类型 + 收口 send + 加 exhaustive」为主,机械、低风险、可分批。真正要克制的是 M18.f(统一 artifact channel)和 M18.d 的范围,故标可选 / 增量。 +- **big-bang 风险**:43 处 emit 一次性换 = 高风险。对策:M18.b/c 分批,先 PoC 交付物族,旧路径保留兼容期,逐步收敛。 +- **exhaustive 会不会逼着处理一堆其实不用渲染的事件(pong/debug_*)?** 会强制"显式声明不处理"(`case 'pong': break;`)——这正是要的:**显式 > 静默**,孤儿不再隐身。 +- **Manus 统一渲染会不会推翻现有画布/preview?** M18.d 用兼容期:`artifact_delivered` 与旧 `website_delivered` 并存,`` 内部仍可复用现有 `WebsitePreviewPane`/画布,只是收口入口。不是重写渲染,是统一**入口**。 + +--- + +## 6. 实施进度(2026-06-01 /loop,未提交) + +| 子任务 | 状态 | 备注 | +|---|---|---| +| **M18.a** 闭合 union | ✅ | shared/types.ts ServerMessage 补齐 10 个之前裸发缺失的功能事件(website_delivered/coding_app_preview/coding_artifact/coding_pipeline_state/mode_changed/agent_message/navigate_to_swarm/blueprint_created/debug_*)。纯增类型零回归。 | +| **M18.b** emitToClient 收口 | ✅(PoC+扩展) | conversation.ts 新增 `emitToClient(ws, msg: ServerMessage)`(带 readyState 检查)。**已收口 11 处**:交付物族 4(coding_artifact/coding_pipeline_state/website_delivered/coding_app_preview)+ mode_changed×2 + agent_message + navigate_to_swarm + blueprint_created + debug_system_prompt + debug_tools_for_turn。**门已长牙实证**:故意把 `serverUrl` 写成 `serverUrlXX` → `error TS2561 ... Did you mean 'serverUrl'?`,还原 0 error,30 单测绿,运行时等价(零回归)。**剩 7 处长尾**待完成 pass:3 处用 spread(ui_contract_calibration_ready/result、publish_progress —— 需先把 spread 源 typed)+ schedule_countdown/template_picker_proposal/ui_contracts_proposal(多行,事件已在 union)。 | +| **M18.c** 消费侧 typed+exhaustive | ⏳ 下一增量 | useMessageHandler `switch(msg.type)` 55 case **无 default**(未知事件静默吞)。但 `msg` 是 ServerMessage **超集**(含 session_switched/session_new_ready 等 inbound 生命周期事件,不在 union)→ 需先定义 `ClientInboundMessage = ServerMessage \| {生命周期事件}` 再 typed+`default:never`。是独立专注增量,不在 a/b 那轮仓促做。 | +| **M18.d** 统一 Artifact 渲染(对齐 Manus) | ⬜ | 待 a-c 地基。 | +| **M18.e** eval 跨边界 consumer 门 | ✅(slides) | slides-deck `assertPreviewEndpointServes` 已落地验证;推广 website/coding-app 待办。 | +| **M18.f** marker-tool data 统一 channel | ⬜ | 可选,待 a-d 后评估,防过早抽象。 | + +**slides 止血(本 session,M18.e 首样本 + 缺陷③ 局部修复)**:slides 硬门([../../../src/web/server/slides-guard.ts](../../../src/web/server/slides-guard.ts) + conversation.ts 执行前拦 Bash 产 office 二进制,17 单测)+ slides-deck eval 真实"PPT"措辞 + 负向断言 + `/api/files/preview` 接线门(e2e 1/1 PASS 实证 agent 被拦后真回退 deck 流程)。 diff --git a/docs/spec/roadmap/M19-overview.md b/docs/spec/roadmap/M19-overview.md new file mode 100644 index 00000000..c0331260 --- /dev/null +++ b/docs/spec/roadmap/M19-overview.md @@ -0,0 +1,121 @@ +# M19 Overview — 任务执行的动态过程叙事(Live Execution Narrative):让 axon「在用户眼前一步步工作」 + +> 📍 **父文档**: [../README](../README.md) | 🎯 **北极星**: [../vision.md](../vision.md)(直接服务 **真 BOSS:真人交付** — 见 [../status.md](../status.md) 现实血条) +> 🎯 **本文定位**: 2026-06-02 用登录态实测 Manus 全 12 mode 动态过程(证据库 [../../manus-parity/manus-product-prd-2026-06-02.md](../../manus-parity/manus-product-prd-2026-06-02.md) §2.5 + 录屏 `manus-parity/recordings/`)逼出的结论:**axon 的「能力」已对齐(真跑得出 slides/app),但「过程呈现的叙事感」差一截——用户看到的不是「agent 在我眼前一步步工作」,而是相对静默的流水线。** 这层动态 UE 是 Manus「产品力」的核心,优先级高于再补静态配置页。 +> 📂 **子任务**: M19.a live_status 接通(死线变活体)/ M19.b step_update 步骤流式(全 mode 计划叙事)/ M19.c Suggested follow-ups(结束追问)/ M19.d 右栏 Computer 多 tab 工作台 / M19.e 收尾态(Task completed + 星评 + 交付卡)/ M19.f eval 跨边界 consumer 门 +> 🧪 **验收映射**: [../acceptance/C-product.md](../acceptance/C-product.md)(交付过程**真到达用户眼睛**)+ [[feedback_done_means_verified_in_real_product]]("完成"=真前端走一遍)+ [[feedback_eval_blind_spot_frontend_wiring]](eval 必须跨 producer→consumer 边界) +> ⛓️ **依赖**: **M18(前后端契约硬化)是地基** — M19 新增的每个事件(live_status / step_update / suggested_followups)必须走 M18.a 进 `ServerMessage` union、M18.b `emitToClient` 发、M18.c exhaustive 消费。M19 不重建协议,是在已焊死的 typed channel 上「填进 Manus 有、axon 缺的叙事事件」。 + +--- + +## 1. 病根(代码实证 2026-06-02,非感觉) + +Manus 任务执行 = 一条**实时叙事流**:mode badge → 计划叙述 → **步骤列表(✓/⟳)** → 工具执行卡(带时间戳)→ **✦Thinking 活体状态(此刻在干啥)** → 步骤进度卡(底部常驻)→ **✓Task completed + 星评 + Suggested follow-ups**。axon 把这条叙事流的**承重件全做了一半就断了**:基建在、线没接。 + +### 致命证据 — 前端等着的活体事件,后端从不发 + +| 叙事件 | 前端现状 | 后端现状 | 判定 | +|---|---|---|---| +| `live_status`(此刻在干啥灰字) | ChatPane 有 handler([ChatPane.tsx:707](../../../src/web/client/src/pages/ManusWorkbench/ChatPane.tsx) + 渲染 :1815「axon 正在响应…」) | **grep 零发送** + 不在 `ServerMessage` union | 🔴 **死线**:UI 等着,后端永不喂 | +| `plan_update` / `new_plan_step`(步骤流式) | ChatPane 有 handler(:1021 / :1032) | **后端无规范发送** | 🔴 缺步骤叙事 | +| `status`(idle/thinking/tool_executing/streaming) | ChatPane:676 接 | 后端发(conversation.ts:8794+) | 🟡 **当终止信号用,不是进度** | +| Suggested follow-ups(结束 3 条追问) | **无 UI** | **无机制** | 🔴 完全缺失 | +| Task completed + 星评 | 散在卡片 | — | 🟡 无统一收尾态 | + +`live_status`/`plan_update` 正是 [M18](M18-overview.md) §1 点名的「后端发/前端零接 或 前端等/后端零发」的**孤儿事件**——只是方向相反(这次是前端等、后端缺)。同一个架构缺陷的另一半切片。 + +### 三个结构缺口 + +- **缺口① agent 不上报「此刻在干啥」** — 后端只在工具**完成**时发 `tool_result`;工具执行**中**、思考**中**、step 之间的空窗,用户只看到一个不动的 spinner「axon 正在响应…」([ChatPane.tsx:1818](../../../src/web/client/src/pages/ManusWorkbench/ChatPane.tsx))。Manus 全程有 `✦Thinking · Optimizing slide height…` 这种**人话状态行**。这不是看门狗([liveness-probe.ts](../../../src/web/server/liveness-probe.ts) 是检测静默挂起的容错,不是给用户看的进度),是**产品级的活体感**。同源诉求 [[project_silent_hang_detection_landed]](那次解的是「真挂起」,这次解「没挂起但看着像挂起」)。 + +- **缺口② 过程是「黑盒+结果」,不是「计划+逐步」** — Manus 一上来先 `I will create … Let me start by researching` 然后 `✓ Research → ⟳ Generate → ○ Deliver` 逐条点亮。axon 的步骤叙事只在 coding mode 有([CodingPipelinePanel.tsx](../../../src/web/client/src/pages/ManusWorkbench/CodingPipelinePanel.tsx),且是**侧栏面板形态**、`coding_pipeline_state` 驱动、**仅 website mode**)。**其余 11 个 mode(slides/design/research/…)零步骤叙事**——用户不知道 agent 打算干几步、到第几步。 + +- **缺口③ 右栏 Computer 只有两视图 + 没接通** — [WebsitePreviewPane.tsx](../../../src/web/client/src/pages/ManusWorkbench/WebsitePreviewPane.tsx) + [CodeView](../../../src/web/client/src/components/CodeView/index.tsx) 支持 Preview(iframe + live reload) + Code(Monaco)。但:(a) **多 tab 工作台缺**——无 Terminal(`SimpleTerminalTab.tsx` 存在但**未集成进 CodeView**)、无 Database;(b) `coding_app_preview` 事件后端已发([conversation.ts:7569](../../../src/web/server/conversation.ts))但 **ChatPane 未接**(grep 实证),coding-app 的 live serverUrl 进不了右栏。Manus 右栏是 `Preview / Code / Backend Server / Database / File storage` 一整套工作台(PRD §2.5.3)。 + +### 为什么必然「看着像没干活」 +后端真在干(工具在跑、LLM 在想),但**进度信息留在后端没往前端推**——编译器不报(事件没定义就没人要它)、eval 不抓(测 producer/磁盘,不看 spinner)。于是「能力真有、体验像死机」。这正是 [[feedback_done_means_verified_in_real_product]] 的根:我的「完成」bar(跑出结果)比用户的「完成」bar(**全程看着它活着、信得过**)低一截。 + +--- + +## 2. 为什么这是真 BOSS 阻塞,不是刷支线 + +[../status.md](../status.md) 现实血条:真 BOSS = 真人(非作者)成功交付。**一个相对静默、看不出在干啥的流水线,会在用户第一次用、转 30 秒圈时就流失**——比缺某个 mode 更致命,因为它发生在用户还没看到产物之前。Manus 的「活体叙事」是它让人**愿意等、信得过**的核心产品力。M19 不新增「能力」,它把已有能力**包装成用户感知得到、信得过的过程**——这是对血条(真人交付=0)的直接攻击。证据:登录态实测 Manus 一个 slides 任务全程有研究卡/Thinking/逐页渲染的叙事(录屏 `manus-parity/recordings/slides-live-process.webm`),axon 同等任务跑得出但过程相对黑盒。 + +--- + +## 3. 目标架构(增量收口,AI-native 优先,禁 big-bang) + +核心洞察:**承重件(emitToClient 收口 / ServerMessage union / ChatPane 的 live_status & plan handler / CodingPipeline 的 stage 模型)都在,缺的是「producer 端把进度喂出来」+「非 coding mode 也有步骤叙事」+「结束钩子」。** 改动比想象小:主要是后端在已有循环里多 emit 几类 typed 事件 + 前端补几个轻 UI。 + +### M19.a live_status 接通(死线变活体)— 最高杠杆、最低成本 【先做】 +- 协议:`live_status` 进 `ServerMessage`(走 M18.a),payload `{ text: string; phase?: 'thinking'|'tool'|'finalizing' }`。 +- 机制(**纯 AI-native,D1 已拍板**):新增 marker 工具 `report_progress(text)`(`BaseTool`,无副作用)。agent **主动调**它来告诉用户「此刻在干啥」;conversation.ts 按 `toolUse.name === 'report_progress'` 拦截(marker 范式,参考 M5-M10:真逻辑在拦截层、不碰 `result.data`),`emitToClient(state.ws, { type:'live_status', payload:{ text } })`。工具返回一个轻量 ack。**无 server 模板、无周期心跳**——状态行只在 agent 报时更新。 +- prompt:mode-agnostic 地在 base/loop prompt 加一条纪律「执行多步/长操作前,先调 report_progress 用一句话告诉用户你正要做什么」([manus-prompt.ts](../../../src/web/server/manus-prompt.ts) / [loop-prompt.ts](../../../src/core/loop-prompt.ts))。 +- consumer:ChatPane 已有 `liveStatus` 渲染([ChatPane.tsx:1815](../../../src/web/client/src/pages/ManusWorkbench/ChatPane.tsx))+ handler(:707),**核对真收到事件**;spinner 文案从死板「axon 正在响应…」换成实时 `live_status.text`。 +- 验收:跑任一真任务,前端灰字状态行**随 agent 自报变化≥3 次**(eval M19.f 钉死)。 + +### M19.b step_update 步骤流式(全 mode 计划叙事) +- 协议:`step_update` 进 union,payload `{ steps: Array<{ id; title; status: 'pending'|'running'|'done'|'failed' }>; current?: string }`(全量快照,对齐 `coding_pipeline_state` 的快照范式,避免增量乱序)。 +- producer:**复用而非新造**——coding mode 直接把 blueprint tasks 映射成 steps(CodingPipelinePanel 的数据已是这个形状);**非 coding mode(slides/research/design/…)让 agent 在开场自报一个 step 计划**(AI-native:模型输出 `` 或调一个轻量 `declare_plan` marker 工具,server 落成 `step_update`)。逐步推进时更新 status。见决策 D1/D2。 +- consumer:ChatPane 把 `step_update` 渲染成**对话流内联的步骤列表**(✓/⟳ 图标 + 当前步高亮),对齐 Manus(不是侧栏,是叙事流里)。coding mode 仍可保留 CodingPipelinePanel 侧栏作「指挥官全局图」([[project_trust_overlay_slice1_landed]]),二者数据同源。 +- 验收:slides + research + 一个 coding 任务各有**逐步点亮的步骤列表**出现在对话流。 + +### M19.c Suggested follow-ups(结束追问) 【consumer ✅ 落地 / producer ⏸ 待 gpt-5.5】 +> **2026-06-02 状态**:consumer 闭环已落地 + jsdom 组件测试 10/10 钉死(`suggested_followups` 进 +> `ServerMessage` union;`FollowupSuggestions.tsx` presentational 组件 + `parseFollowups` 纯函数; +> ChatPane 接线:state + handler + chat_start 清空 + `handleFollowupClick`(复用 handleDispatchFix 直发范式) + 渲染)。 +> **producer(agent 收尾产出 3 条)未接** —— 因 gpt-5.5 渠道下线(codex 503,验证不了)+ D3 待拍板暂缓; +> 在 producer 接通前本事件永不被 emit(已知临时状态)。恢复后建议走 **marker 工具 `suggest_followups`** +> (结构化、对齐 report_progress/[[feedback_tool_result_data_dropped]],同轮多调一次工具≠多起 LLM 调用, +> 同时满足 D3「省调用」),而非「同轮文本夹带 + server 解析」(文本解析脆)。 +- 协议:`suggested_followups` 进 union,payload `{ items: Array<{ label: string; prompt: string }> }`(3 条)。 +- producer:`message_complete` 后产出——**AI-native:同一轮 LLM 收尾时顺带产出 3 条**(prompt 里加「任务完成后给 3 条用户可能想继续的追问」),server 抽出发事件,**不额外起一次 LLM 调用**(铁律2,省 token + 延迟)。见决策 D3。 +- consumer:ChatPane 在 Task completed 下渲染 3 个可点行(点击=把 `prompt` 填进 composer 并发送)。复用 [chat.html](../../manus-parity/mockups/chat.html) / [inline-delivery.html](../../manus-parity/mockups/inline-delivery.html) 的 `.followups` 形态。 +- 验收:任务结束后出现 3 条**与任务相关**的追问,点击能续问。 + +### M19.d 右栏 Computer 多 tab 工作台 +- 接通 `coding_app_preview`:ChatPane 加 handler,把 serverUrl 喂给 WebsitePreviewPane `directUrl`([[project_coding_app_canvas_visibility_landed]] 已打通渲染,缺这一接线)。 +- 多 tab 化:CodeView 右栏从 {Preview, Code} 扩成 tabbed `{ Preview, Code, Terminal }`——**Terminal 复用已存在的 `SimpleTerminalTab.tsx`**(接进去,不新造)。tab 切换不重载 iframe。 +- **Database tab = 押后**(见决策 D4 + 非目标)——它依赖托管 BaaS,不属于「叙事层」,归后续护城河 milestone。M19 只做 Preview/Code/Terminal 的工作台壳 + coding_app_preview 接通。 +- 验收:coding 任务右栏可在 Preview/Code/Terminal 间切,且 live dev server 真出现在 Preview。 + +### M19.e 收尾态(Task completed + 星评 + 交付卡) +- 复用 [M18.d](M18-overview.md) 的统一 `Artifact` 渲染:任务结束渲染 `✓ Task completed` + `How was this result?` 星评(埋点回流到 eval/质量信号)+ 交付物卡(缩略图 + 打开/下载)。星评数据接 [[feedback_done_means_verified_in_real_product]] 的真实用户旅程信号。 +- 验收:每种交付物结束都有统一收尾态(不是各 mode 各搞)。 + +### M19.f eval 跨边界 consumer 门 +- 推广 [[feedback_eval_blind_spot_frontend_wiring]]:新增 suite 断言**叙事事件真到前端**——一个真任务跑完,harness 在 ws 流上断言收到 ≥3 次 `live_status`、≥1 个 `step_update`(steps 从 running→done)、1 个 `suggested_followups`(3 条)。**负向断言**:不得出现「全程零 live_status / spinner 孤儿」(接 liveness-probe 的 SPINNER_ORPHAN 判定)。 +- 接回归门 [`evals/regression.ts`](../../../evals/regression.ts)。 + +--- + +## 4. 非目标(scope 纪律,铁律2) + +- **托管 BaaS(DB/Auth/Backend/Storage provisioning)= 不在 M19**。那是编程护城河的 infra 大件(PRD §2.5.3 / §4 P1),归独立 milestone(建议 M20 或扩 [M13](M13-overview.md) 沙盒)。M19 是**叙事层(前端 + 协议)**,不碰 infra。Database tab 因此押后。 +- **不重做 mode 系统 / prompt**([modeRegistry.ts](../../../src/web/server/modeRegistry.ts) 已成熟)。 +- **不动 M18 的协议地基**——M19 是它的消费者,不是重写。 +- **video/audio 模型自研** = 不卷(PRD §2.5.10:媒体生成是 Manus 付费护城河,axon 主场是编程护城河)。 + +--- + +## 5. 决策待拍板(动手前需用户拍) + +- **D1 live_status / step 计划的文本来源** ✅ **已拍板 2026-06-02:纯 AI-native(agent 自报,无 server 兜底模板)**。理由:铁律2/7 + [[project_code_routing_removal_pivot]](全删兜底、完全信任 agent 原生判断力)。机制 = marker 工具 `report_progress(text)`(agent 主动调,server 拦截 emit `live_status`),不走文本解析、不做 server 模板补位。风险(agent 偶尔漏报→状态行偶尔空窗)**接受**,用 prompt 强约束「长操作前先报」+ eval 门兜底(M19.f 断言 ≥3 次)。 +- **D2 非 coding mode 的 step 怎么来** ✅ **已拍板:纯 AI-native**。agent 调轻量 `declare_plan(steps[])` marker 工具落 `step_update`(结构化、不靠文本解析,对齐 [[feedback_tool_result_data_dropped]]「marker 走专属事件」教训)。非 coding mode 由 agent 自己产 plan,不复用 blueprint、不 server 兜底。 +- **D3 follow-ups 生成**:同轮 LLM 顺带产出(省调用)❓ vs 独立轻量调用(更可控)。**倾向同轮**。 +- **D4 右栏 Database/BaaS tab**:押后到护城河 milestone(M19 只做叙事层)❓ vs 纳入 M19。**强烈倾向押后**(scope 纪律)。 +- **D5 星评数据落点**:回流 eval 质量信号 vs 仅 UI。**倾向回流**(铁律11,真实用户反馈是评估金矿)。 + +--- + +## 6. 落地顺序建议(按 ROI) + +``` +M19.a (live_status, 杠杆最高/成本最低,先证「不静默」) + → M19.c (follow-ups, 独立、留存价值、改动小) + → M19.b (step 流式, 最大叙事提升, 需 D1/D2 拍板) + → M19.d (右栏多 tab, 接通 coding_app_preview + Terminal) + → M19.e (收尾态, 复用 M18.d Artifact) + → M19.f (eval 门, 每步落地即补对应断言) +``` +每个子任务落地后**必用 Browser 开 :3456 真走一遍 + 截图**([[feedback_done_means_verified_in_real_product]]),并跑 [`evals/regression.ts`](../../../evals/regression.ts) 确认无真退步才提交。 diff --git a/docs/spec/roadmap/M20-overview.md b/docs/spec/roadmap/M20-overview.md new file mode 100644 index 00000000..f27ff97f --- /dev/null +++ b/docs/spec/roadmap/M20-overview.md @@ -0,0 +1,122 @@ +# M20 Overview — 应用骨架层(Product Shell):Settings / Personalization / Usage&Billing / Library + +> 📍 **父文档**: [../README](../README.md) | 🎯 **北极星**: [../vision.md](../vision.md)(直接服务 **真 BOSS:真人交付** — 见 [../status.md](../status.md) 现实血条) +> 🎯 **本文定位**: 2026-06-02 用登录态实测 Manus 全产品(证据库 [../../manus-parity/manus-product-prd-2026-06-02.md](../../manus-parity/manus-product-prd-2026-06-02.md) §2.6/§2.8/§3 + §4 差距矩阵 + 26 张 Settings/Personalization/Library 截图)+ 4 个并行 agent 代码实证逼出的结论:**axon 的「能力」已对齐(编程护城河五层真 ship、12 mode 全接),但「产品外壳」是空心的** —— Settings/Personalization/Library/计费明细这一层「用户面骨架」≈ 0% 实现,且**大半连 spec milestone 都没有**。这是 PRD §4 自评的 P0/P1,也是 [[project_manus_login_injection_and_full_prd]] 反复点名的「真差距=应用骨架层,比再加 mode 更值得做」。 +> 📂 **子任务**: M20.a Settings 模态框架 / M20.b General(语言·主题·通知) / M20.c Personalization(Custom Instructions+记忆) / M20.d Usage&Billing(NewAPI 额度明细+三维) / M20.e Library(交付物知识库) / M20.f 其余 Settings 子页(Data controls/My Computer/My plugins/Integrations) / M20.g 计费三维用量埋点 +> 🧪 **验收映射**: [../acceptance/C-product.md](../acceptance/C-product.md)(产品完整度)+ [[feedback_done_means_verified_in_real_product]]("完成"=真前端走一遍)+ 4 agent 核查基线(应用骨架层实测 ~5-15%) +> ⛓️ **依赖 / 边界**: **商业化后端不自建**(NewAPI/chatbi.site 已全实现账号·额度·充值·支付,见 [[project_m16_commercialization_is_newapi]])—— M20.d/g 是**前端展示 + 用量埋点**,绝不重造计费引擎。**不重造已有子系统**(memory/connector/skill/schedule/E2B/quota 都已存在,M20 多数子页 = 把它们暴露成 Manus 风 UI,不动后端逻辑——铁律2)。M19(动态过程叙事)正交并行,互不阻塞。 + +--- + +## 1. 病根(4 agent 代码实证 2026-06-02,非感觉) + +PRD §1「本文盘的是**应用本体 UI / 信息架构 / 辅助配置 / 商业化模型**,即 playbook 分析覆盖不到的『产品骨架』」。4 个并行 agent 把 PRD 的每条需求落到真实代码核验,结论:**能力层对齐、外壳层空心**。 + +### 致命证据 — 用户面骨架几乎不存在 + +| 需求(PRD) | 真实代码状态(agent 实证) | 判定 | +|---|---|---| +| Settings 模态整套(10 子页 §2.6) | `SettingsPanel.tsx` 是 **CLI 级配置壳**(general/model/api/embedding…),**无** Account/Usage&Billing/Personalization/Data controls/My Computer/My plugins/Integrations 用户面子页 | 🔴 ~5% | +| Personalization(Custom Instructions + 记忆 UI §2.6) | axon 有 memory 子系统(`src/memory/`),但**无任何用户可配 UI**;Custom Instructions 无输入口、无注入 | 🔴 0% | +| Usage & Billing 前端面(§3) | `ApiUsageBar` 有 API quota 条,但**非 Credits 概念、无逐任务明细、无 Tasks/Websites/Computers 三维** | 🔴 ~25% | +| Library 交付物知识库(§2.8) | `KnowledgePage` 是 Wiki(articles/search),**不是产物聚合库**;交付物散落各 session 无统一视图 | 🔴 0% | +| 顶栏(通知中心🔔 / 积分 pill 弹层 / 头像菜单 §2.2) | 仅 `useNotificationSound`(声音)、`AuthStatus`(登录态无菜单);无通知中心、无头像下拉菜单 | 🔴 ~15% | + +> **结论**:用户第一次打开 axon,**配置不了自己(没 Settings/Personalization)、找不回产物(没 Library)、看不清花了多少(没 Usage 明细)**。这不是「再加一个 mode」能补的,是产品骨架的结构性债。 + +### 一条贯穿全 milestone 的红线(写给动手的分身) +**这一层 80% 的工作量是「暴露已有能力」,不是「造新能力」**: +- Personalization 记忆 → 接 `src/memory/`(已有 embedding+BM25 混合检索),UI 只做增删查 + Custom Instructions 注入。 +- Usage&Billing → 读 NewAPI 的 `/quota` + 用量接口([[project_m16_commercialization_is_newapi]]),前端只渲染,**后端一行计费逻辑都别写**。 +- My plugins / Integrations → 接 `src/skills/` + connector 体系([[project_m12o_webhook_landed]] 已建 OAuth/webhook),UI 做浏览/启用。 +- My Computer → 接 E2B 云沙盒([[project_m13_sandbox_integration]]),UI 做状态/配置。 +- Scheduled → 已有(M20 不碰,[[project_axon_main_line_c_commercial]])。 + +--- + +## 2. 为什么这是真 BOSS 阻塞,不是刷支线 + +[../status.md](../status.md) 现实血条:真 BOSS = 真人(非作者)成功交付,当前 **0**。**一个没有 Settings/Personalization/Library 的产品,给真人用会立刻露馅**——他配不了语言/主题、写不了 Custom Instructions(直接影响输出质量)、产物用完就丢、不知道额度怎么扣的。这层「产品骨架」是「看起来像个正经产品 vs 像个 demo」的分水岭。与 M19(动态过程叙事,"看着它工作")互补:M19 解「过程可信」,M20 解「产品完整可托付」。两者都是真人留存的承重件,M19 偏"首次体验不流失"、M20 偏"持续使用可托付"。 + +--- + +## 3. 目标架构(子任务,AI-native 优先,禁 big-bang) + +### M20.a Settings 模态框架 【框架前置,先做】 +- 形态:对齐 Manus **hash 路由 `/app#settings/`**(PRD §0 实测:不是 `/settings/*`,独立导航会 404)+ 模态弹层 + 左栏两组(Account 组 / Features 组)。 +- 入口:头像菜单(顺带补 §2.2 头像下拉:账号/Upgrade/Credits/Personalization/Account/Settings/Sign out)。 +- 复用:axon 已有 `SettingsPanel.tsx`(CLI 级配置)**不丢**——并入 Features 组的「高级/开发者」分区;用户面新子页(b–f)挂在前面。 +- 验收:头像菜单 → 打开 Settings 模态 → 左栏可切各子页 → hash 路由可深链。 + +### M20.b General(Appearance + Communication) 【低成本】 +- Appearance:Language 下拉(接 `src/i18n/`,已有 en/zh)+ Theme(Light/Dark/Auto)。 +- Communication:Browser notifications / Receive product updates / Email when queued task starts / Ads 四开关(接 §2.2 通知中心,开关先落库后接事件)。 +- 验收:切语言/主题真生效(i18n 已有),开关状态持久。 + +### M20.c Personalization(Custom Instructions + 记忆) ★产品力,优先级最高★ +- Profile tab:Nickname / Occupation / More about you / **Custom Instructions**(多行)。 +- Knowledge tab:记忆条目增删查(接 `src/memory/`)+ **Import memory from another AI**(PRD §2.6)。 +- **AI-native 接线(关键)**:Custom Instructions 文本动态注入 system prompt——复用 [manus-prompt.ts](../../../src/web/server/manus-prompt.ts) 的动态块机制(同 mode extra / spec 注入范式),**不硬编码、按用户配置拼接**。见决策 D2(注入点 + 是否进 coding identity)。 +- 复用:memory 子系统已有混合检索,UI 只做 CRUD + 注入,**不重造检索**。 +- 验收:填 Custom Instructions → 下一轮对话 system prompt 真带上(debug_system_prompt ws 事件可验);记忆条目增删真落 `src/memory/`。 + +### M20.d Usage & Billing 前端面(守 NewAPI 红线) +- 3 tab(PRD §2.6/§3):**Tasks / Websites / Computers**;顶部 Free+Upgrade;Credits 余额 + Daily refresh;**Credits history 逐任务扣费明细**。 +- 数据源:**全部读 NewAPI**(`/quota` 已实测返真实余额,[[project_m16_commercialization_is_newapi]])+ axon 侧用量记录(usage-tracker)。**后端不自建计费**([[project_m16_commercialization_is_newapi]] 红线:M16.b 本地 CreditLedger 已证重复死代码、已回滚)。 +- 顶栏积分 pill:把 `ApiUsageBar` 升级成 Manus 风「✦ 余额」pill + 弹层(Credits/Daily refresh/View usage/Upgrade)。 +- 验收:逐任务消耗明细真渲染(数据来自后端用量记录);三维 tab 至少 Tasks 有真数据,Websites/Computers 见 M20.g。 + +### M20.e Library(交付物知识库) +- 左栏导航新增 **Library** 项(§1 IA / §2.8):按日期分组的产物卡(生成的文档/图/幻灯片/app)+ My Favorites + 搜索。 +- 数据源:**聚合现有各 session 的交付物**(website_delivered / deliver_app / deliver_slides 等已落盘产物 + session 元数据),**不新建产物存储**——做一个聚合索引视图。见决策 D4(扫盘聚合 vs 落索引表)。 +- 验收:跨 session 的交付物出现在统一 Library,可按日期/类型筛 + 搜索 + 收藏 + 点击打开。 + +### M20.f 其余 Settings 子页(暴露已有能力,可后置) +| 子页 | 接 axon 已有 | 工作量 | +|---|---|---| +| Data controls | 数据留存/导出/删除(session 持久化层已有) | 中 | +| My Computer | Cloud computer(E2B [[project_m13_sandbox_integration]])/ Local computer | 中 | +| My plugins | Connectors / Skills / Data sources(`src/skills/` + connector [[project_m12o_webhook_landed]]) | 中-大 | +| Integrations | 第三方集成(OAuth 体系已建) | 中 | +| Account | 账号信息/绑定(接 NewAPI AxonCloudAuth) | 小 | +- Mail Manus / Cloud browser = 押后(非高频,见非目标)。 + +### M20.g 计费三维用量埋点(守红线) +- Websites(托管时长)/ Computers(云电脑 vCPU·时长)两维用量**采集**——在 publish(M15 Railway/Vercel)+ E2B 沙盒(M13)链路埋点,把用量**喂给 NewAPI**(或本地用量记录供 M20.d 展示)。**不自建计费引擎**,只补「采集 + 上报」让 NewAPI 能按维度计费。见决策 D3。 +- 验收:跑一次 publish / 一次沙盒,Websites/Computers 用量被记录并能在 M20.d 展示。 + +--- + +## 4. 非目标(scope 纪律,铁律2) + +- **不自建商业化后端**:账号/额度/充值/支付由 NewAPI 完整实现([[project_m16_commercialization_is_newapi]])。M20.d/g 只做前端展示 + 用量采集上报。**违者 = 重复造轮子**(M16.b 已踩坑回滚)。 +- **不重造 memory/connector/skill/schedule/E2B 逻辑**:M20 是给它们「装上 Manus 风用户面 UI」,后端逻辑不动。 +- **Agent 营销部署(messenger)= ⚪ 战略不抄**(PRD §2.7,B2B 获客护城河,axon 主打 C 端 [[project_axon_main_line_c_commercial]])。 +- **Mail Manus / Cloud browser 深做 = 押后**(低频)。 +- **Playbook 100 模板库聚合页**:axon 是标杆模板路线([[project_m10_template_landmark_strategy]]),不做 100 模板库;左栏可留一个轻入口跳标杆模板,不在 M20 核心。 +- **不动 M19(动态叙事)/ 不动 mode 系统**。 + +--- + +## 5. 决策待拍板(动手前需用户拍) + +- **D1 用户配置数据落点**:Settings/Personalization 的用户配置存哪?候选:① `~/.axon/settings.json`(本地,community 版自然)② 后端 user profile(接 NewAPI 账号,cloud 版多端同步)。**倾向**:双轨——community 落本地、cloud 落 NewAPI profile(对齐双轨架构 [[project_m13a_sandbox_adapter_landed]])。 +- **D2 Custom Instructions 注入点**:注入 base/loop prompt?是否也进 CODING_AGENT_IDENTITY(coding mode 会替换 base identity,可能丢掉用户指令)?**倾向**:进 manus-prompt 动态块 + coding identity 也带(用户指令应全局生效)。 +- **D3 计费三维与 NewAPI 边界**:Websites/Computers 用量是只采集喂 NewAPI(NewAPI 计费),还是 axon 侧也算?**倾向**:只采集上报,计费归 NewAPI(守红线)。 +- **D4 Library 数据来源**:实时扫各 session 交付物聚合 vs 落一张 library 索引表(产物落盘时同步写)?**倾向**:落索引表(扫盘慢且不可搜)。 +- **D5 子任务优先级**:先做哪几个?**倾向**:M20.a 框架 → M20.c Personalization(产品力,直接影响输出质量)→ M20.b General → M20.d Usage&Billing → M20.e Library → M20.f/g。 + +--- + +## 6. 落地顺序建议(按 ROI,全部不依赖 LLM 即可验证 → gpt-5.5 下线期可推进) + +``` +M20.a (Settings 框架 + 头像菜单, 框架前置) + → M20.c (Personalization, 产品力最高 + Custom Instructions 注入可 debug_system_prompt 验) + → M20.b (General, 低成本, i18n/theme 已有) + → M20.d (Usage&Billing, 接 NewAPI 展示) + → M20.e (Library, 交付物聚合) + → M20.f (其余子页, 暴露已有能力) + → M20.g (计费三维埋点, 配合 M20.d) +``` +**每个子任务落地后必用 Browser 开 :3456 真走一遍 + 截图对照 Manus 截图**([[feedback_done_means_verified_in_real_product]]),前端逻辑配 jsdom 组件测试钉死([[project_coding_app_canvas_visibility_landed]]:重型 eval 覆盖不到 React 接线)。**这一层纯前端 + 接已有后端,几乎不依赖 LLM,是 gpt-5.5 渠道下线期间的最佳推进燃料**(见 [[ops_axon_runtime_blockers_gpt55_and_native]])。 diff --git a/docs/spec/roadmap/M21-overview.md b/docs/spec/roadmap/M21-overview.md new file mode 100644 index 00000000..ecd041fa --- /dev/null +++ b/docs/spec/roadmap/M21-overview.md @@ -0,0 +1,62 @@ +# M21 Overview — 双向追溯正向缺口补全:access-tokens / Team / BaaS 可视化管理层 + +> 📍 **父文档**: [../README](../README.md) | 🎯 **北极星**: [../vision.md](../vision.md) +> 🎯 **本文定位**: 2026-06-03 用户拍板的**流程纪律**(守门人):双向对比后**先补文档参考系再补功能**—— +> Manus 截图有、axon 无对应 spec 的,先把 spec 补上([../../manus-parity/traceability-matrix.md](../../manus-parity/traceability-matrix.md) §3.1)。 +> 本文补这三个「Manus 有截图、axon 无 milestone」的正向缺口,让可追溯矩阵的**正向方向闭合**。 +> 📂 **子任务**: M21.a access-tokens(OpenAPI token 管理)/ M21.b Team(团队设置/用量)/ M21.c BaaS 可视化管理层(Auth/对象存储/Analytics) +> ⛓️ **依赖**: M20(应用骨架层,Settings 框架已落地);Database tab 已在 2026-06-03 接真本地 SQLite(commit e2a3c05f),本文 M21.c 是其余 BaaS 面的规划。 + +--- + +## 1. 为什么先补 spec(流程纪律,2026-06-03 用户纠正) + +> 「双向对比时应**首先双向修改文档**:缺图片/高保真就补、缺 spec 就补,**不应该先补功能**。」 + +守门人(截图=质量最后守门人)只有在**参考系本身完备**时才成立:每张 Manus 截图都要有 axon spec/milestone 对应(正向),每个 axon milestone 都要有高保真/截图可验(反向)。此前直接补功能(Settings/Database)是本末倒置——参考系有洞,守门就有盲区。本文 + 反向高保真补全(见 traceability-matrix §3.2 → docs/prototypes/)一起把参考系焊死。 + +--- + +## 2. 子任务规格 + +### M21.a access-tokens(OpenAPI / 个人访问令牌)— 对齐 35-set-access-tokens +- **Manus 形态**:Settings 下 `Access tokens` 子页,签发/吊销个人 API token(给外部程序调 Manus OpenAPI)。Free 账号被降级到 General 未截到全貌(PRD §6 待补)。 +- **axon 对应**:axon 已有 `axon-proxy`(LAN key 共享)+ NewAPI key 体系。token 管理 = 在 Settings 加 `访问令牌` 子页:列出/签发/吊销本地 API token(供外部调 axon 的 `/v1/*` 代理)。 +- **数据源**:复用 `src/proxy/`(API proxy)+ NewAPI key;token 存 `~/.axon/settings.json` 或独立 token store。 +- **验收**:Settings 加「访问令牌」子页,签发一个 token → 能用它调通 axon proxy;吊销后失效。 +- **决策 D1**:是否暴露(安全面)—— 个人 token 能调 axon 全部能力,需权限范围(scope)。**倾向**:先只读 + 显式开关,scope 待定。 + +### M21.b Team(团队设置 / 团队用量)— 对齐 41-team-access / 42-team-usage +- **Manus 形态**:Settings 下 `Team access`(成员/角色/邀请)+ `Team usage`(团队额度聚合)。B 端协作。 +- **axon 判断**:⚪ **战略暂不做**([[project_axon_main_line_c_commercial]] 主线 C 打 C 端;团队协作是 B 端护城河,依赖账号/组织体系)。**但补 spec 记录决策**:team 不是漏做,是有意识 ⚪——待 C 端跑通、有 B 端需求再启。占位 Settings 子页可加「团队功能即将面向企业版开放」诚实说明(非死按钮)。 +- **决策 D2**:是否在 Settings 留团队入口占位。**倾向**:留诚实说明入口,不做功能。 + +### M21.c BaaS 可视化管理层(Auth / 对象存储 / Analytics)— 对齐 web-07/08/13 +- **背景**:Manus Computer 工作台右栏是一整套托管 BaaS(DB/Auth/后端/对象存储/部署分析)。axon 后端 provision 真有(Neon `neon-adapter.ts` / Railway / E2B),但**UI 层透明**(用户看不见)。Database tab 已 2026-06-03 接真本地 SQLite(e2a3c05f);本子任务规划其余面: + | 面 | 数据源 | 状态 | + |---|---|---| + | Database(本地 SQLite) | /api/project-db(SQLiteDriver) | ✅ 已落地 e2a3c05f | + | Database(云端 Neon) | publish-fullstack 后的 Neon 连接串 → PostgresDriver introspect | 📋 待 publish 链路接 | + | Auth(users 表 + 登录门) | 生成 app 的 users 表(同 DB introspection)+ Auth.js 配置 | 📋 规划 | + | 对象存储 | S3/Supabase Storage(M15 已给 token 思路) | 📋 规划 | + | Analytics/Dashboard | 真部署后的访问流量(需真 publish + analytics provider) | 🔴 阻塞(需真部署+流量) | +- **范式**:和 Database tab 同源——后端 introspection 端点 + 前端 tab 组件,**只读真数据、不造假壳**。 +- **★诚实边界★**:Analytics 需真部署 + 真流量,无真数据时**诚实空态**(「发布后才有访问数据」+ 真 publish 入口),绝不造假图表。云端 Neon 表视图需 publish-fullstack 真跑出 provisioned app(依赖 gpt-5.5)。 +- **验收**:生成带后端的 app → publish → 云端 Database tab 看见真表;本地 SQLite app → Database tab 已可看(done)。 + +--- + +## 3. 非目标 +- 不自建商业化(NewAPI 已实现,[[project_m16_commercialization_is_newapi]]);access-tokens 只签发本地代理 token,不碰计费。 +- Team B 端协作 = ⚪ 战略暂不做(记决策,非漏做)。 +- Analytics 不造假图表(无真流量就诚实空态)。 + +## 4. 决策待拍板 +- D1 access-tokens scope(全能力 vs 只读)+ 是否默认关闭。 +- D2 Team 是否留占位入口。 +- D3 BaaS Auth/Storage UI 优先级(vs 等 gpt-5.5 恢复后跑真全栈 app 再做)。 + +--- + +## 5. 与守门人矩阵的闭环 +本文落地后,traceability-matrix §3.1 的三个正向缺口从「无 milestone」→「有 spec」。反向缺口(§3.2,axon 护城河无高保真)由 `docs/prototypes/` 的 axon-moat 高保真补全。两者齐了,双向追溯参考系才完备,守门人才无盲区。 diff --git a/docs/spec/roadmap/blueprint.json b/docs/spec/roadmap/blueprint.json new file mode 100644 index 00000000..ec1d58cd --- /dev/null +++ b/docs/spec/roadmap/blueprint.json @@ -0,0 +1,78 @@ +{ + "_comment": "axon 自蓝图 (M17.l dogfood, 2026-06-01) — 用 axon 自己的 M7 blueprint schema 把 axon 的 roadmap 表达成结构化蓝图。缘起:用户『spec 里程碑缺 checklist,搞笑的是 axon 没有自己的蓝图系统(没把蓝图用在自己身上)』。★诚实硬门★:每个里程碑的 verify = 它的真 eval 套件/测试(铁律11,验自己里程碑=跑它的 eval,正如 M7 验用户代码=跑 typecheck);status 'done' 是可证伪的(任何人能跑 verify 复核),不是嘴上说的。dependsOn 反映 vision 五层 + 主线C 架构。commander 直接读本文件(结构化),不再 regex 猜 prose。", + "project": "axon", + "generatedBy": "axon self-blueprint (M17.l)", + "tasks": [ + { "id": "M5", "title": "Layer 1 coding agent 自动升级", "dependsOn": [], "verify": "evals/suites/m5-coding-agent", "status": "done", + "subtasks": [ + { "id": "M5.b", "title": "enter/exit_coding_mode 翻 mode", "done": true }, + { "id": "M5.d", "title": "coding agent 升级认证 10/10", "done": true }, + { "id": "M5.f", "title": "前端 coding 指示器", "done": true } + ] }, + { "id": "M6", "title": "Layer 2 spec 引导卡片 + 硬契约", "dependsOn": ["M5"], "verify": "evals/suites/m6-spec-card", "status": "done", + "subtasks": [ + { "id": "M6.a", "title": "propose_coding_spec → spec.json 落盘", "done": true }, + { "id": "M6.f", "title": "无 spec 物理屏蔽 Write/Edit", "done": true } + ] }, + { "id": "M7", "title": "Layer 3.a/b 蓝图生成 + 串行执行 + 严格门", "dependsOn": ["M6"], "verify": "evals/suites/m7-blueprint", "status": "done", + "subtasks": [ + { "id": "M7.b", "title": "generate_coding_blueprint → blueprint.json", "done": true }, + { "id": "M7.d", "title": "verify_task server 真跑 typecheck 绿才 done", "done": true } + ] }, + { "id": "M8", "title": "Layer 3.c/d/e 测试 + 评估 + 交付", "dependsOn": ["M7"], "verify": "evals/suites/m8-deliver", "status": "done", + "subtasks": [ + { "id": "M8.a", "title": "run_blueprint_tests 真跑 lint+typecheck+unit+e2e", "done": true }, + { "id": "M8.b", "title": "verify_against_spec 100% 验收门(唯一 LLM 自评门)", "done": true }, + { "id": "M8.c", "title": "deliver_app 双硬门交付", "done": true } + ] }, + { "id": "M9", "title": "Layer 4 user-in-the-loop 卡点协作", "dependsOn": ["M8"], "verify": "evals/suites/m9-collab", "status": "done", + "subtasks": [ { "id": "M9.a", "title": "request_user_action 阻塞卡点", "done": true } ] }, + { "id": "M10", "title": "Manus 沙箱借鉴:标杆模板 + AST 精准编辑", "dependsOn": ["M7"], "verify": "evals/suites/m10-jsx-editor", "status": "done", + "subtasks": [ + { "id": "M10.d", "title": "jsx-editor AST 精准改 (23/23)", "done": true }, + { "id": "M10.e", "title": "模板挑选卡片端到端", "done": true }, + { "id": "M10.f", "title": "render_diagram/upload/stt/keyframes 工具", "done": true } + ] }, + { "id": "M11", "title": "UI 验证框架:确定性裁判栈 L1-L4", "dependsOn": ["M8"], "verify": "evals/suites/ui-output", "status": "done", + "subtasks": [ + { "id": "M11.d", "title": "baseline 校准闭环(门3长牙齿)", "done": true }, + { "id": "M11.f", "title": "网关 + 门3 + codex 回归门", "done": true }, + { "id": "M11.h", "title": "语义锚点 marker", "done": false } + ] }, + { "id": "M12", "title": "Manus parity 补齐(消息/媒体/connector/webhook)", "dependsOn": ["M10"], "verify": null, "status": "done", + "subtasks": [ + { "id": "M12.i", "title": "媒体生成 4 类(video/speech/audio/music)", "done": true }, + { "id": "M12.o", "title": "webhook 接收端 + 验签", "done": true }, + { "id": "M12.k", "title": "全栈模板", "done": false } + ] }, + { "id": "M13", "title": "云沙盒接入 coding 执行 (E2B)", "dependsOn": ["M5"], "verify": "evals/suites/e2e-full-flow", "status": "done", + "subtasks": [ + { "id": "M13.a", "title": "SandboxAdapter 双轨地基", "done": true }, + { "id": "M13.b", "title": "Model A 文件活在 E2B + 编辑器/preview 沙盒感知", "done": true } + ] }, + { "id": "M14", "title": "LLM 代理网关 (用户自配 key 永不上后端)", "dependsOn": ["M13"], "verify": null, "status": "wip", + "subtasks": [ { "id": "M14.c", "title": "用户自配 key 隐私底线", "done": false } ] }, + { "id": "M15", "title": "托管 + 子域名部署 (Railway/Vercel/Neon)", "dependsOn": ["M13"], "verify": null, "status": "done", + "subtasks": [ + { "id": "M15.b", "title": "detectStack 分流静态/全栈", "done": true }, + { "id": "M15.a", "title": "publish 工具接电 + Vercel/Railway", "done": true } + ] }, + { "id": "M16", "title": "商业化:账号/额度/计费 (NewAPI 已实现)", "dependsOn": ["M15"], "verify": null, "status": "done", + "subtasks": [ { "id": "M16.x", "title": "由 NewAPI(chatbi.site) 完整实现", "done": true } ] }, + { "id": "M17", "title": "指挥官全局地图:信任叠加层 + 自蓝图", "dependsOn": ["M5", "M6", "M7", "M8", "M11"], "verify": "src/web/client/src/pages/ManusWorkbench/commanderLens.test.ts", "status": "wip", + "subtasks": [ + { "id": "M17.a", "title": "阶段信任配色 🟢/🟡", "done": true }, + { "id": "M17.b", "title": "行为验证缺口", "done": true }, + { "id": "M17.c", "title": "断裂接缝 + 依赖地图", "done": true }, + { "id": "M17.d", "title": "高危改动埋点", "done": false }, + { "id": "M17.e", "title": "现实血条 BOSS HP", "done": true }, + { "id": "M17.f", "title": "真码情可视化 + 双镜片原型", "done": true }, + { "id": "M17.g", "title": "执行层(灭火/盖章/发货)", "done": true }, + { "id": "M17.h", "title": "派活手柄(编码动作进面板)", "done": true }, + { "id": "M17.i", "title": "双镜片进 per-session 面板", "done": true }, + { "id": "M17.j", "title": "独立指挥官常驻面板", "done": true }, + { "id": "M17.k", "title": "现实血条 BOSS HP HUD", "done": true }, + { "id": "M17.l", "title": "axon 自蓝图(dogfood)", "done": false } + ] } + ] +} diff --git a/docs/spec/status.md b/docs/spec/status.md index 05345cf9..cb2254d6 100644 --- a/docs/spec/status.md +++ b/docs/spec/status.md @@ -5,6 +5,49 @@ --- +## 🪸 现实血条 (BOSS HP) — 唯一的「真通关」判定,与 milestone 完全脱钩 + +> 🎯 **为什么钉在最顶**:用 spec 推 M5→M16 像玩开放世界 RPG(主线/支线/技能树/MEMORY 当存档),推进感很爽。 +> 但 RPG 最毒的陷阱是 **「刷支线的爽感会伪装成主线进度」** —— 100 小时点满技能树,主线可能只推了 20%。 +> milestone 全绿、eval 全绿、parity 80% 都只是**经验值**,不是**击杀 BOSS**。软件的通关条件不在游戏里(不是「M16 落地」),在游戏外面:**真人愿意用、愿意付钱**。 +> 本条是唯一脱钩现实的锚点。**纪律:每次刷完一个 milestone,先抬头看这条血条有没有动;没动就是刷了小怪。** + +### 代理 BOSS:真实端到端体检任务(2026-06-01 选定为主血量 — 铁律 11) + +累计「自造真任务 → 真跑通全 5 层(spec→蓝图→执行→测试→验收 100%→交付**可运行**工程)」的**不同形态**数: + +``` +HP [████████████░░░░░░░░░░░░░░░░░░] 4 / 10 种真实形态 ≈ 40% +``` + +| # | 任务 / 项目 | 形态 | 真实变量 | 状态 | +|---|---|---|---|---| +| 1 | e2e-todo-api-full-flow / TodoApi | Express API | 五层闭环奠基 | ✅ | +| 2 | healthcheck-md-api / MarkdownRenderApi | API + 真 npm 依赖 | `marked` 真装真跑 | ✅ | +| 3 | healthcheck-frontend / UnitConverter | 前端 Vite SPA | 纯前端无后端门 | ✅ | +| 4 | healthcheck-fullstack / NotesSaaS | 全栈 SaaS | 前后端 + 持久化 | ✅ | + +> 下一刀目标:每多**一种没体检过的真实形态**就 +1(如:带 DB 迁移 / 带第三方 OAuth / 带定时任务 / 真部署上线 / Electron 桌面)。形态重复不计血——换个项目名跑同一种形态是刷小怪。 + +### 真 BOSS:真人(非作者)成功交付(当前 **0**) + +> 代理血条满 10 也**不算通关**——体检是你自己出题自己跑,必然高估。真 BOSS = 一个**不是你**的人,用 axon 交付了一个他**真的要用**的工程。第一滴血出现前,代理血条只是热身。 + +### 终极 BOSS:付费真人(当前 **0** · MRR **¥0**) + +> 商业化链路(NewAPI / chatbi.site)已接通、子弹上膛(见 [[project_m16_commercialization_is_newapi]]),BOSS 只是还没刷出来。通关条件不在「对齐 Manus」的经验值里,而在游戏外:**真人愿用、愿付费**。 +> +> ⚠️ **更正(2026-06-01 用户核实)**:本文档此前记「Manus 已被 Meta 收购下架」——**系误传,无可靠来源**。Manus 2026-06 仍是当红产品。「对齐 Manus」仍是有效北极星;但「真通关 = 真人付费」这条独立成立(与 Manus 存亡无关)。教训:现实世界事件(收购/下架/融资)不要从记忆/文档当核实事实传播,必须问用户或查实时来源。 + +### 记账规则(防止血条被刷成又一个虚荣指标 — 与全项目「硬门 > prompt 祈祷」同源) + +1. **只认真跑通,不认 milestone 落地**:某层 commit 了 ≠ HP+1;必须一条真任务从 prompt 一路到「交付物可 `npm start` / 可打开」。 +2. **形态去重**:换项目名跑同一形态不加血。 +3. **回归即扣血**:体检任务后来跑挂了,HP 立刻 -1,不许挂着旧战绩。 +4. **代理永远 ≤ 真实**:代理 BOSS 再好看,真 BOSS = 0 就还没通关,本段必须显示 0。 + +--- + ## ✅ 已有,本次重构不动 - **Layer 0 Mode 系统**:`src/web/server/modeRegistry.ts` 12 mode 全接线(M4 已清理 skipBlocks 死代码) @@ -140,4 +183,5 @@ - [roadmap/M10-overview.md](roadmap/M10-overview.md) — Manus 沙箱借鉴:30 模板池 + AST 精准编辑 + 沙箱工具矩阵(2026-05-25 加) - [roadmap/M11-overview.md](roadmap/M11-overview.md) — UI 验证框架:warm chromium daemon + L1-L4 确定性裁判栈(2026-05-25 加) - [roadmap/M12-overview.md](roadmap/M12-overview.md) — Manus parity 补齐:19 个子任务(2026-05-25 加) +- [roadmap/M17-overview.md](roadmap/M17-overview.md) — 指挥官全局地图:信任叠加层(🟢硬门/🟡自评/🔴接缝)+现实血条+双镜片真码情可玩原型(2026-06-01 加,本文顶部现实血条=M17.e) - [manus-parity-audit.md](manus-parity-audit.md) — 38 项能力对照矩阵(M12 单一真理源,2026-05-25 加) diff --git a/evals/regression.ts b/evals/regression.ts index 532ea9d1..8cb7e77b 100644 --- a/evals/regression.ts +++ b/evals/regression.ts @@ -25,6 +25,11 @@ interface SuiteSpec { } const SUITES: SuiteSpec[] = [ + // 真前端体检硬门 (2026-06-04 接入 / vibe-coding 根治):加载真 :3456 空状态主屏,断言「用户睁眼第一屏干净」—— + // 零 pageerror + 无野数字浮层(「222」类)+ 主屏骨架在 + 无新增 console/网络错(已知 2 个 404 透明 allowlist)+ 留截图。 + // ★把「打开真前端看一眼」从 prompt 自觉(~50% 遵守)变机器硬门(M5-M9「硬门>祈祷」用在元问题)★。 + // 快(无 LLM ~15s),放第一位先跑。自证有牙齿:注入假「222」浮层探测器精确变红(teeth-test 实证)。 + { name: 'frontend-health', expect: '5/5(用户第一屏干净)' }, { name: 'prompt-refactor', expect: '5/5' }, { name: 'm5-coding-agent', expect: '10/10' }, { name: 'm6-spec-card', expect: '6/6 (+1 skip)' }, @@ -42,6 +47,16 @@ const SUITES: SuiteSpec[] = [ // 1/1 哨兵:slides 任务 → agent initialize_project(deck) + 填内容 + deliver_slides // → website_delivered(kind=slides) + deck 结构门 + headless chromium 真渲染门(可翻页)。 { name: 'slides-deck', expect: '1/1' }, + // silent-hang 活体探测 (2026-06-01 接入):复刻「介绍合肥 ppt」截图的「卡住没反应」bug。 + // 2/2 哨兵:base 裸提问 + 健康基线,用 liveness-probe 判每轮是否静默挂起(spinner 永转)。 + // Layer A 确定性检测器单测在 src/web/server/liveness-probe.test.ts(vitest 常驻门),不在此 runner。 + { name: 'silent-hang', expect: '2/2', e2e: true }, + // M19.f 动态过程叙事跨边界门 (2026-06-02 接入):多步真任务跑完,断言 live_status 真到达前端 ws 流 ≥3 次 + // + 负向不得 SPINNER_ORPHAN(与 silent-hang 互补:那测「没卡死」,这测「过程看得见在干啥」)。 + // ★诚实基线:M19.a 靠 agent 主动调 report_progress(纯 prompt,遵守率 ~50%)→ 本门可能红 = 真信号, + // 不是 flaky。基线待实跑校准(server :3456 + codex 起后跑 evals/suites/live-narrative/run.ts)。★ + // 标 long:多步任务重 + 鲁棒性未知,仅 --with-e2e 跑,不污染默认快门。 + { name: 'live-narrative', expect: 'TBD(实跑校准 M19.a 鲁棒性)', e2e: true, long: true }, { name: 'e2e-full-flow', expect: '1/1 全链路闭环', e2e: true, long: true }, ]; diff --git a/evals/suites/e2e-full-flow/cases.ts b/evals/suites/e2e-full-flow/cases.ts index cd07a1e2..a0875b9a 100644 --- a/evals/suites/e2e-full-flow/cases.ts +++ b/evals/suites/e2e-full-flow/cases.ts @@ -103,9 +103,57 @@ E2E_CASES.push({ }, }); +// ============================================================================ +// case 3(2026-06-01 verify-first):chrome-extension-builder = 编程护城河 E 类唯一未接通的必赢场景。 +// 目的不是「保证过」,是**真跑看流水线卡在哪**:扩展无 dev server(test/acceptance/deliver 门都假设 +// 可跑 web app),先实证断点,再定最小修法(避免 speculative 建到末端)。spec 刻意框成可过门:含纯函数 +// countWords 可单测(喂 run_blueprint_tests)+ 文件存在性验收(不需浏览器)。base mode 隔离扩展问题。 +// ============================================================================ +const extDir = fs.mkdtempSync(path.join(os.tmpdir(), 'axon-e2e-ext-')); + +const wordCounterExtSpec = { + projectName: 'WordCounterExtension', + targetUsers: '浏览网页的用户', + coreFeatures: [ + 'manifest.json(manifest_version 3),声明 content script + popup', + 'src/wordCount.ts:纯函数 countWords(text: string): number(按空白分词,可单测)', + 'content script:调 countWords 统计当前页面文本词数,写入 chrome.storage', + 'popup.html + popup.ts:读取并显示词数', + ], + techPreferences: 'TypeScript + Chrome Manifest V3,vitest 单测 countWords,npm scripts 带 typecheck/test。注意:浏览器扩展不跑 HTTP server,没有 dev/start 脚本——交付物是「可加载已解压」的扩展目录/zip,不是 dev server URL。', + outOfScope: '不做后端、不打包发布到 Chrome 商店、不做复杂 UI', + acceptanceCriteria: [ + 'manifest.json 存在且 manifest_version 为 3', + 'countWords 单测通过:countWords("") === 0 且 countWords("a b c") === 3', + ], + expectedDeliverable: 'web-app', + createdAt: new Date().toISOString(), +}; + +E2E_CASES.push({ + id: 'e2e-chrome-extension-full-flow', + description: 'Chrome 扩展(Manifest V3)走全 5 层 — verify-first 看扩展(无 dev server)卡在哪个门', + input: '做一个 Chrome 扩展(Manifest V3):统计当前网页的英文单词数并在 popup 里显示。TypeScript,把分词逻辑抽成纯函数 countWords 并加一个 vitest 单测。注意扩展不跑 HTTP server。', + followUps: [ + [ + '确认这份 spec,自主开始。请自主完成整个流程,中途不要停下来问我:① generate_coding_blueprint ② 逐 task 实现 + verify_task 到绿 ③ run_blueprint_tests ④ verify_against_spec 逐条验收 ⑤ deliver_app。扩展没有 dev server,交付时如实处理(产出可加载的扩展目录)。一路走到交付。', + `__coding_spec_confirmed__ ${JSON.stringify(wordCounterExtSpec)}`, + ].join('\n'), + '继续,把剩下的 task 实现完并 verify_task,然后 run_blueprint_tests → verify_against_spec → deliver_app。', + '继续完成直到 deliver_app。', + '继续。', + ], + timeoutMs: 300_000, + contract: (events) => expectFullFlowArtifacts(extDir)(events), + setup: async (sendWs: (msg: any) => void): Promise => { + sendWs({ type: 'set_project_path', payload: { projectPath: extDir } }); + }, +}); + // 暴露 temp 目录给 run.ts 打印(便于失败时人工进去看 agent 到底建了啥)。 export const E2E_PROJECT_DIR = dir; export const E2E_DASH_DIR = dashDir; +export const E2E_EXT_DIR = extDir; // setup 注入:TODO API case set_project_path 到 temp 目录(与 contract 读盘同目录)。 E2E_CASES[0].setup = async (sendWs: (msg: any) => void): Promise => { diff --git a/evals/suites/frontend-health/run.ts b/evals/suites/frontend-health/run.ts new file mode 100644 index 00000000..13b6db38 --- /dev/null +++ b/evals/suites/frontend-health/run.ts @@ -0,0 +1,266 @@ +/** + * frontend-health/run.ts —— 真前端体检硬门(vibe-coding 根治 / 2026-06-04 用户拍板「建真前端体检硬门」) + * + * ── 为什么存在(根源诊断)────────────────────────────────────────────────────── + * 病根:axon 全套验证(16 对齐 oracle / M5-M19 eval / 回归门 / 38 单测 / 双轴变异认证)都在问 + * 「**我造的功能**对不对」,从来没有一个测试**加载组装后的真前端**、看「用户睁眼第一屏干不干净」。 + * 于是一个黑底「222」野盒子能长在空状态主屏(用户开门第一眼),而所有 milestone 灯还是绿的。 + * [[reality_blood_bar]]「milestone 全绿 ≠ 通关」+ [[feedback_done_means_verified_in_real_product]] + * 「完成=真前端走一遍」+ [[project_oneshot_root_cause_two_axes]]「real-ui-journey 成前置门」全指这。 + * + * 而「打开真前端看一眼」一直是一条 **prompt 自觉**(CLAUDE.md/记忆里写了),在堆 milestone 的冲刺下 + * ~50% 被跳过 —— 这正是 M5-M9 血泪教训「**结构化硬门 > prompt 祈祷**」。本套件把「打开看一眼」 + * 从 prompt 祈祷变**机器硬门**:每次回归都加载真 :3456、断言主屏干净、留截图给人过目。 + * + * ── 检查项(每个 state)───────────────────────────────────────────────────────── + * 1. pageerror(未捕获 JS 异常) → 硬失败(不可争辩的 bug) + * 2. 野浮层(content 区 fixed/absolute 元素 direct text=裸数字/junk,非 allowlist)→ 硬失败(「222」类) + * 3. 结构不变量(composer 在 / 左栏 5 项 nav 在)→ 硬失败(主屏骨架塌了) + * 4. console error / network 4xx-5xx → 报告;新增(非 KNOWN_*_ALLOWLIST)即硬失败 + * 5. 截图留档 → 强制产出 artifact 供人过目([[feedback_done_means_verified_in_real_product]]) + * + * ── 不做(铁律2 不过度工程化)────────────────────────────────────────────────── + * 像素 baseline diff([[project_m11_ui_probe_spec_drafted]] 教训:像素无 baseline 必 skip,过早优化)。 + * 本门只抓「确定性能抓的」(pageerror/野浮层/结构/网络错)+ 强制截图让人看「确定性抓不了的视觉垃圾」。 + * + * 跑:先有本地 axon-web 在 :3456(node dist/web-cli.js & 或 tsx src/web-cli.ts &)。 + * 单跑 `npx tsx evals/suites/frontend-health/run.ts`;回归门 evals/regression.ts 自动带它。 + */ +import { chromium, type Browser, type Page } from 'playwright-core'; +import { existsSync } from 'node:fs'; +import { homedir } from 'node:os'; +import { join } from 'node:path'; + +const BASE_URL = process.env.AXON_FE_HEALTH_URL || 'http://localhost:3456'; + +// vendored chromium(playwright-core 不自带浏览器,用 ms-playwright 缓存里的)。 +function resolveChromium(): string { + const cache = join(homedir(), '.cache', 'ms-playwright'); + for (const ver of ['chromium-1223', 'chromium-1208', 'chromium-1187']) { + const exe = join(cache, ver, 'chrome-linux64', 'chrome'); + if (existsSync(exe)) return exe; + } + throw new Error('找不到 vendored chromium(~/.cache/ms-playwright/chromium-*/chrome-linux64/chrome)。先 npx playwright install chromium'); +} + +/** + * 已知 console 错误 allowlist(URL/文本子串)。★不是隐藏,是透明追踪★:每个条目必带「为什么暂时容忍 + + * TODO」,门每跑都打印 allowlist 命中计数(让它们一直可见,而非沉默放行)。新增任何不在此列的 console/网络 + * 错误 = 硬失败。修掉后从这里删条目,门就对它长牙齿。 + */ +const KNOWN_NETWORK_ALLOWLIST: { pattern: string; why: string }[] = [ + { + pattern: '/live-history', + why: '空状态 load 时前端探 session live-history,session 尚不存在 → 404。TODO: 后端该返回空数组而非 404,或前端无 session 时不探。', + }, + { + pattern: '/api/files/read', + why: '空状态某处探幽灵项目的 client/index.html → 404。TODO: 查谁在无交付物时就 fire 这个 read(疑似 preview/DeliverableCard 预热)。', + }, +]; +function isAllowlisted(text: string): boolean { + return KNOWN_NETWORK_ALLOWLIST.some((a) => text.includes(a.pattern)); +} + +/** 已知合法浮层 allowlist(class 子串)—— 这些 fixed/absolute 元素是设计内的,不算野浮层。 */ +const KNOWN_FLOAT_ALLOWLIST = ['commanderFab', 'setup-wizard', 'imagePreview', 'modal', 'Modal', 'toast', 'Toast', 'tooltip', 'Tooltip', 'popover', 'Popover', 'dropdown', 'Dropdown']; + +interface CheckResult { name: string; pass: boolean; detail: string } + +async function dismissWizard(page: Page): Promise { + for (let i = 0; i < 6; i++) { + const has = await page.evaluate(() => !!document.querySelector('.setup-wizard-overlay')); + if (!has) return; + await page.evaluate(() => { + const ov = document.querySelector('.setup-wizard-overlay'); + if (!ov) return; + const btns = [...ov.querySelectorAll('button')] as HTMLButtonElement[]; + (btns.find((b) => /跳过|skip|完成|done|开始|进入/i.test(b.textContent || '')) || btns[btns.length - 1])?.click(); + }); + await page.waitForTimeout(800); + } +} + +/** + * 体检一个 state:导航 + 收 pageerror/console/网络错 + 跑 DOM 检查 + 截图。返回逐项 CheckResult。 + */ +async function inspectState( + browser: Browser, + stateName: string, + viewport: { width: number; height: number }, +): Promise { + const page = await browser.newPage({ viewport }); + const pageErrors: string[] = []; + const consoleErrors: string[] = []; + const networkErrors: string[] = []; + page.on('pageerror', (e) => pageErrors.push(String(e).slice(0, 200))); + page.on('console', (m) => { if (m.type() === 'error') consoleErrors.push(m.text().slice(0, 200)); }); + page.on('response', (r) => { if (r.status() >= 400) networkErrors.push(`${r.status()} ${r.url()}`); }); + page.on('requestfailed', (r) => networkErrors.push(`FAILED ${r.url()} ${r.failure()?.errorText || ''}`)); + + await page.goto(BASE_URL, { waitUntil: 'networkidle', timeout: 30000 }).catch(() => {}); + await page.waitForTimeout(3000); + await dismissWizard(page); + await page.waitForTimeout(2500); // 等 ws 连接 + 可能的延迟渲染 + + const results: CheckResult[] = []; + + // ① pageerror(未捕获 JS 异常)→ 硬失败 + results.push({ + name: `${stateName}/no-pageerror`, + pass: pageErrors.length === 0, + detail: pageErrors.length ? `${pageErrors.length} 个未捕获异常: ${pageErrors.join(' | ')}` : '无', + }); + + // ② 野浮层(content 区 fixed/absolute + direct text=裸数字/短 junk,非 allowlist)→ 「222」类 + const strayFloats = await page.evaluate((floatAllow) => { + const out: { tag: string; cls: string; text: string; rect: string }[] = []; + for (const el of Array.from(document.querySelectorAll('body *'))) { + const cs = getComputedStyle(el as Element); + if (cs.position !== 'fixed' && cs.position !== 'absolute') continue; + const r = (el as Element).getBoundingClientRect(); + if (r.width < 8 || r.height < 8) continue; + const cls = typeof (el as HTMLElement).className === 'string' ? (el as HTMLElement).className : ''; + if (floatAllow.some((a: string) => cls.includes(a))) continue; // 已知合法浮层跳过 + const direct = Array.from(el.childNodes).filter((n) => n.nodeType === 3).map((n) => (n.textContent || '').trim()).join(''); + // 野浮层特征:自己直接挂着「裸数字」或「极短无意义串」(222 / 调试值 / z-index 泄漏文本) + if (/^\d{1,6}$/.test(direct) || (/^[\d.]{1,8}$/.test(direct) && direct.length > 0)) { + out.push({ tag: (el as Element).tagName.toLowerCase(), cls: cls.slice(0, 50), text: direct, rect: `${Math.round(r.x)},${Math.round(r.y)} ${Math.round(r.width)}x${Math.round(r.height)}` }); + } + } + return out; + }, KNOWN_FLOAT_ALLOWLIST); + results.push({ + name: `${stateName}/no-stray-number-floats`, + pass: strayFloats.length === 0, + detail: strayFloats.length ? `野数字浮层(「222」类): ${JSON.stringify(strayFloats)}` : '无', + }); + + // ③ 结构不变量:主屏骨架在(composer textarea + 左栏 nav 有内容) + const skeleton = await page.evaluate(() => { + const composer = !!document.querySelector('textarea'); + // 左栏 5 项主 nav(新建任务/Agent/插件/定时任务/库)—— 至少有 4 个可见 nav 文本即认为骨架在 + const navTexts = ['新建任务', 'Agent', '插件', '定时任务', '库']; + const bodyText = document.body.innerText || ''; + const navHit = navTexts.filter((t) => bodyText.includes(t)).length; + return { composer, navHit }; + }); + results.push({ + name: `${stateName}/skeleton-intact`, + pass: skeleton.composer && skeleton.navHit >= 4, + detail: `composer=${skeleton.composer} 左栏nav命中=${skeleton.navHit}/5`, + }); + + // ④ console error / network 4xx-5xx —— 新增(非 allowlist)即硬失败;已知 allowlist 仅报告 + const newConsole = consoleErrors.filter((e) => !isAllowlisted(e) && !/Failed to load resource/.test(e)); + const newNetwork = networkErrors.filter((e) => !isAllowlisted(e)); + const allowlistedHits = networkErrors.filter((e) => isAllowlisted(e)).length + consoleErrors.filter((e) => isAllowlisted(e)).length; + results.push({ + name: `${stateName}/no-new-console-network-errors`, + pass: newConsole.length === 0 && newNetwork.length === 0, + detail: + (newConsole.length || newNetwork.length + ? `★新增错误(硬失败): console=${JSON.stringify(newConsole)} network=${JSON.stringify(newNetwork)}; ` + : '无新增错误; ') + `已知 allowlist 命中=${allowlistedHits}(透明追踪,见 KNOWN_NETWORK_ALLOWLIST)`, + }); + + // ⑤ 截图留档(强制 artifact 供人过目) + const shot = join('/tmp', `frontend-health-${stateName}.png`); + await page.screenshot({ path: shot }); + results.push({ name: `${stateName}/screenshot`, pass: true, detail: `已留档 ${shot}(人过目:有无视觉垃圾/错位)` }); + + await page.close(); + return results; +} + +/** + * 聊天往返硬门 —— 真发一条消息,断言助手回复**渲染到 UI**(不只是到达 ws)。 + * + * ★这正是 2026-06-05「助手回复一条都不渲染」3 个叠加 bug 漏网的检查★:当时所有 eval 都用 node ws-harness + * 直连后端、断言线上事件,**从不渲染真 React 前端**,更不走「temp→persistent 首条消息 session 切换 + 浏览器 + * 渲染」这条真实路径 → 后端/ws/LLM/落库全绿,但用户发消息助手回复在 UI 一条都不出现,没有任何门拦得住。 + * 本检查送一条确定性消息(1加1),等助手 chatBubbleAssistant 出现**非占位的真实文字**。LLM 依赖、较慢 + * (~5-30s,受 codex 负载影响),但这是「聊天到底能不能用」的唯一真前端证据,值得放进体检门。 + */ +async function chatRoundtrip(browser: Browser): Promise { + const page = await browser.newPage({ viewport: { width: 1500, height: 950 } }); + const pageErrors: string[] = []; + page.on('pageerror', (e) => pageErrors.push(String(e).slice(0, 200))); + await page.goto(BASE_URL, { waitUntil: 'domcontentloaded', timeout: 30000 }).catch(() => {}); + await page.waitForTimeout(3000); + await dismissWizard(page); + await page.waitForTimeout(1500); + const ta = await page.$('textarea'); + if (!ta) { + await page.close(); + return [{ name: 'chat-roundtrip/assistant-renders', pass: false, detail: '找不到输入框' }]; + } + await ta.click(); + await ta.fill('1加1等于几?只回答一个数字'); + await page.waitForTimeout(200); + await page.keyboard.press('Enter'); + // 等助手气泡出现「非占位的真实文字」(≤40s,给足 codex 负载下的响应时间)。 + let assistantText = ''; + const deadline = Date.now() + 40000; + while (Date.now() < deadline) { + await page.waitForTimeout(2000); + assistantText = await page.evaluate(() => { + const a = Array.from(document.querySelectorAll('[class*="chatBubbleAssistant"]')) + .map((e) => (e.textContent || '').trim()) + .filter((t) => t.length > 0 && !/正在思考|正在响应/.test(t)); + return a[0] || ''; + }); + if (assistantText.length > 0) break; + } + const shot = join('/tmp', 'frontend-health-chat-roundtrip.png'); + await page.screenshot({ path: shot }); + await page.close(); + return [{ + name: 'chat-roundtrip/assistant-renders', + pass: assistantText.length > 0, + detail: assistantText.length > 0 + ? `✓ 助手回复真渲染到 UI: "${assistantText.slice(0, 50)}"(截图 ${shot})` + : `★助手回复 40s 内未渲染到 UI —— 后端可能正常但前端断链(2026-06-05 3-bug 同类,或 codex 负载/渠道死)。pageErrors=${JSON.stringify(pageErrors.slice(0, 2))}(截图 ${shot})★`, + }]; +} + +async function main(): Promise { + console.log('# frontend-health 真前端体检硬门\n'); + console.log(`目标:${BASE_URL}`); + console.log(`运行时间:${process.env.AXON_HEALTH_TS || new Date().toISOString()}\n`); + + let browser: Browser; + try { + browser = await chromium.launch({ executablePath: resolveChromium(), headless: true, args: ['--no-sandbox'] }); + } catch (e) { + console.log(`[frontend-health] 浏览器启动失败:${(e as Error).message}`); + console.log('# 汇总:0/1 pass'); + process.exit(1); + } + + const allResults: CheckResult[] = []; + try { + // state: 空状态默认屏(用户开门第一眼)。用用户实拍的大视口 2559x1420 复刻其环境。 + // 未来可加更多 state(发消息后 / 各 mode / Settings 弹窗 / Computer 工作台)—— inspectState 已通用化。 + const r = await inspectState(browser, 'empty-default', { width: 2559, height: 1420 }); + allResults.push(...r); + // 聊天往返硬门:真发一条消息,断言助手回复渲染到 UI(2026-06-05 3-bug 的根治检查)。 + const chat = await chatRoundtrip(browser); + allResults.push(...chat); + } catch (e) { + console.log(`[frontend-health] 体检异常:${(e as Error).message}`); + allResults.push({ name: 'empty-default/run', pass: false, detail: (e as Error).message }); + } finally { + await browser.close(); + } + + for (const r of allResults) { + console.log(` ${r.pass ? '✅' : '❌'} ${r.name} — ${r.detail}`); + } + const passed = allResults.filter((r) => r.pass).length; + const total = allResults.length; + console.log(`\n# 汇总:${passed}/${total} pass`); + process.exit(passed === total ? 0 : 1); +} + +main(); diff --git a/evals/suites/live-narrative/cases.ts b/evals/suites/live-narrative/cases.ts new file mode 100644 index 00000000..27a75cae --- /dev/null +++ b/evals/suites/live-narrative/cases.ts @@ -0,0 +1,65 @@ +/* + * live-narrative cases —— M19.f:验证 live_status 叙事线真到前端(M19.a)。 + * + * 设计原则: + * · 必须是**真多步任务**(研究多对象→构建对比→总结)——单步任务本就没有过程可叙事, + * 数不到 ≥3 次 live_status 不能证明任何事。 + * · 主 case 用 base agent(mode=undefined):AXON_IDENTITY 的「保持节奏感→先调 report_progress」 + * 纪律在此生效,给 M19.a prompt-only 机制最佳发挥空间。coding mode 的盲区(CODING_AGENT_IDENTITY + * 替换掉该纪律)是另一条 finding,待 M19 把纪律补进 coding identity 后再加 coding case。 + * · cwd 隔离到 temp([[feedback_eval_must_isolate_cwd]]):跑真 agent 会真写文件,绝不污染仓库。 + * · 不为过测改写措辞([[feedback_eval_blind_spot_frontend_wiring]]):用真实多步诉求。 + */ +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import type { NarrativeCase } from './harness.js'; + +function tempProjectSetup(): (sendWs: (msg: any) => void) => Promise { + return async (sendWs) => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'axon-narrative-')); + sendWs({ type: 'set_project_path', payload: { projectPath: dir } }); + }; +} + +export const CASES: NarrativeCase[] = [ + { + // ★M19.a 核心断言★:一个明确多步的研究+整理任务。健康的 axon 应在过程中先后报「正在查 X」 + // 「正在查 Y」「正在汇总对比」等 ≥3 次 live_status(agent 按 AXON_IDENTITY 纪律主动调 report_progress)。 + // 数不到 ≥3 = M19.a 的 prompt-only 机制在真实负载下不够鲁棒(真信号,非 flaky)。 + id: 'multistep-research-base', + description: '多步研究任务(base agent):过程中 live_status 灰字应随 agent 自报变化 ≥3 次', + prompt: + '帮我对比北京、上海、深圳三座城市:分别查一下各自最新的常住人口和 2023 年 GDP,' + + '然后整理成一个简洁的对比表写进 compare.md,最后用两三句话总结哪座城市经济密度最高。', + mode: undefined, + deadlineMs: 180_000, + idleMs: 60_000, + minLiveStatus: 3, + setup: tempProjectSetup(), + }, + { + // ★M19.b(plan_update)+ M19.c(suggested_followups)producer 接线证明★(2026-06-04 落地)。 + // M19.b 复用 agent 原生 TodoWrite → conversation.ts 映射成 plan_update(非新造 declare_plan,驳 spec D2); + // M19.c 走 suggest_followups marker → suggested_followups。本 case **显式要求**用 TodoWrite 跟进步骤 + + // 收尾 suggest_followups —— 这是证 **producer 接线(wire)carries the event 真到达前端消费的 ws 流** + // (producer→consumer 跨边界,[[feedback_eval_blind_spot_frontend_wiring]]),不是测 naturalistic 遵守率。 + // · naturalistic 遵守率(不显式提工具、靠全局 prompt nudge 自发调)的软门由上一条 multistep-research-base + // 的 live_status≥3 同源覆盖(都靠 agent 主动调 marker);plan/follow 的 naturalistic 软门待 M19.f 扩。 + // · 自包含任务(规划行程→写文件→总结),不走 web_search → 比研究 case 快且稳,降 codex 负载 flaky。 + // 红 = producer 没接通 / agent 连显式要求都没调工具(真信号)。绿 = wire 通(event 真到 ws 流)。 + id: 'multistep-plan-followups-wire', + description: 'M19.b/c producer 接线:显式多步任务 → plan_update(TodoWrite 映射)≥1 + suggested_followups ≥1', + prompt: + '帮我规划一个 3 天的杭州深度游行程。请先用 TodoWrite 把你的步骤列成清单' + + '(第一天西湖、第二天灵隐与龙井、第三天宋城与运河、最后汇总成一张表写进 itinerary.md),一步步推进、每完成一步更新状态。' + + '全部做完后,用 suggest_followups 给我 2-3 条和这次行程强相关的后续追问建议。', + mode: undefined, + deadlineMs: 150_000, + idleMs: 60_000, + minLiveStatus: 1, // 本 case 焦点是 plan/follow,live_status 软阈值不与上条 ≥3 的 M19.a 门冲突 + expectStepUpdate: true, // M19.b 已落地 → 纳入硬断言(≥1 plan_update) + expectFollowups: true, // M19.c 已落地 → 纳入硬断言(≥1 suggested_followups) + setup: tempProjectSetup(), + }, +]; diff --git a/evals/suites/live-narrative/harness.ts b/evals/suites/live-narrative/harness.ts new file mode 100644 index 00000000..b9ab9fde --- /dev/null +++ b/evals/suites/live-narrative/harness.ts @@ -0,0 +1,220 @@ +/* + * live-narrative eval harness —— M19.f:任务执行「动态过程叙事」跨边界 consumer 门。 + * + * 病根(M19-overview §1):前端 ChatPane 早就备好 live_status / plan handler,但后端从不发 = 死线。 + * M19.a 接通了 report_progress → live_status。本 harness 验证这条线**真活着**: + * 对真 :3456 发一个多步任务,像真前端一样在 ws 流上**数 live_status 事件**—— + * 断言「过程中灰字状态行随 agent 自报变化 ≥N 次」(默认 3),且终止干净(不得 SPINNER_ORPHAN)。 + * + * ★为什么是「数 ws 流上的事件」而不是「磁盘/产物」★([[feedback_eval_blind_spot_frontend_wiring]]): + * M19 的本质是「过程真到达用户眼睛」。eval 必须跨 producer(conversation.ts emit) → consumer(前端 ws) + * 边界断言事件真到了前端消费的那条流,而不是停在后端日志/磁盘。live_status 不落盘、只走 ws, + * 所以只能在 ws 流上数。 + * + * ★诚实边界★:M19.a 靠 agent **主动调** report_progress(纯 AI-native,D1 拍板)。纯 prompt 遵守率 + * 实测 ~50%([[project_m7_blueprint_landed]] 教训),所以本门**可能红**——红 = M19.a 的 prompt-only + * 机制在真实 codex 负载下不够鲁棒,是真信号不是 flaky(已用自愈重试隔离 codex 冷启动假挂)。 + * + * 复用:probeLiveness(同 silent-hang)做负向「不得 SPINNER_ORPHAN/SILENT_HANG」断言。 + * + * M19.b(plan_update,复用 TodoWrite 映射)/ M19.c(suggested_followups,走 suggest_followups marker) + * **2026-06-04 已落地** → multistep-plan-followups-wire case 用 expectStepUpdate/expectFollowups 开关把这俩 + * 纳入硬断言(≥1)。stepUpdateCount 同时计 step_update(M19.b 备用增量名)与 plan_update(当前 TodoWrite 全量快照实走)。 + */ +import WebSocket from 'ws'; +import { probeLiveness, type LivenessEvent } from '../../../src/web/server/liveness-probe.js'; + +const DEFAULT_WS_URL = 'ws://localhost:3456/ws'; + +export interface NarrativeCase { + id: string; + description: string; + /** 用户输入。必须是**真多步任务**(研究→构建→总结这类),否则一步就答完、本就没有过程可叙事。 */ + prompt: string; + /** mode id;undefined = base agent(AXON_IDENTITY 的「保持节奏感」纪律在此生效)。 */ + mode?: string; + /** 观测窗(ms)。默认 180s(多步任务比单轮长)。 */ + deadlineMs?: number; + /** 多久零进展算挂起(ms)。默认 60s。 */ + idleMs?: number; + /** 要求至少几次 live_status(M19.a 核心断言)。默认 3。 */ + minLiveStatus?: number; + /** 是否断言 step_update(M19.b 落地后置 true)。默认 false(仅计数)。 */ + expectStepUpdate?: boolean; + /** 是否断言 suggested_followups(M19.c 落地后置 true)。默认 false(仅计数)。 */ + expectFollowups?: boolean; + /** setup:session_new 后、chat 前执行(如配 project_path 到 temp 隔离目录)。 */ + setup?: (sendWs: (msg: any) => void) => Promise; +} + +export interface NarrativeReport { + caseId: string; + description: string; + pass: boolean; + durationMs: number; + /** 首跑判挂起触发了自愈重试(codex 冷启动 stall 假挂)。 */ + retried?: boolean; + /** ws 流上数到的 live_status 事件数(M19.a 核心指标)。 */ + liveStatusCount: number; + /** 每条 live_status 的文本(核对真是「人话状态行」不是空/重复)。 */ + liveStatusTexts: string[]; + stepUpdateCount: number; + followupCount: number; + /** probeLiveness 判定(负向断言用)。 */ + livenessVerdict: string; + isOrphan: boolean; + details: string[]; +} + +interface RawWsEvent { + type: string; + payload?: any; + atMs: number; +} + +/** + * 跑一个叙事 case,自愈一次 codex 冷启动假挂(同 silent-hang 范式): + * 首跑若判 SILENT_HANG(codex 冷启动 stall,连带 live_status 也数不到)→ 重试一次; + * 重试仍 hang / 仍数不够 = 真信号(M19.a prompt-only 不够鲁棒,或真挂起)。 + */ +export async function runNarrativeCase(c: NarrativeCase, wsUrl = DEFAULT_WS_URL): Promise { + const first = await runNarrativeCaseOnce(c, wsUrl); + if (first.livenessVerdict === 'SILENT_HANG') { + console.log(` ⟳ ${c.id} 首跑判 SILENT_HANG → 自愈重试一次(排除 codex 冷启动 stall 假挂)`); + const retry = await runNarrativeCaseOnce(c, wsUrl); + retry.retried = true; + return retry; + } + return first; +} + +async function runNarrativeCaseOnce(c: NarrativeCase, wsUrl: string): Promise { + const deadlineMs = c.deadlineMs ?? 180_000; + const idleMs = c.idleMs ?? 60_000; + const minLiveStatus = c.minLiveStatus ?? 3; + const t0 = Date.now(); + const raw: RawWsEvent[] = []; + const ws = new WebSocket(wsUrl); + + await new Promise((resolve, reject) => { + const to = setTimeout(() => reject(new Error('ws connect timeout 10s')), 10_000); + ws.once('open', () => { clearTimeout(to); resolve(); }); + ws.once('error', (err: any) => { clearTimeout(to); reject(err); }); + }); + + ws.on('message', (data: any) => { + try { + const msg = JSON.parse(data.toString()); + raw.push({ + type: typeof msg?.type === 'string' ? msg.type : 'unknown', + payload: msg?.payload, + atMs: Date.now() - t0, + }); + } catch { + raw.push({ type: 'parse_error', atMs: Date.now() - t0 }); + } + }); + + const sendWs = (msg: any): void => ws.send(JSON.stringify(msg)); + + // 1. 新建 session + sendWs({ type: 'session_new', payload: {} }); + await sleep(300); + // 2. setup(如配 project_path 到 temp 隔离目录) + if (c.setup) { + await c.setup(sendWs); + await sleep(300); + } + + // 3. 发 chat —— 此刻起算该轮观测窗。 + const turnStart = Date.now(); + sendWs({ + type: 'chat', + payload: { + content: c.prompt, + messageId: `narrative-${c.id}-${Date.now()}`, + mode: c.mode, + }, + }); + + // 4. 像真前端一样等终止信号(message_complete / error / status:idle 任一),到 deadline 为止。 + const deadline = turnStart + deadlineMs; + while (Date.now() < deadline) { + if (raw.some((e) => isTerminal(e))) { + await sleep(400); // 拖尾收齐尾随事件(含可能的 suggested_followups) + break; + } + await sleep(200); + } + const windowEndMs = Date.now() - turnStart; + ws.close(); + + // 5. 只取 turnStart 之后的事件(session_new/setup 回执不算这一轮)。 + const turnEvents = raw.filter((e) => e.atMs >= (turnStart - t0) - 50); + + // 6. 数叙事事件(跨边界 consumer 断言的核心)。 + const liveStatusEvents = turnEvents.filter((e) => e.type === 'live_status'); + const liveStatusTexts = liveStatusEvents + .map((e) => (typeof e.payload?.text === 'string' ? e.payload.text.trim() : '')) + .filter((t) => t.length > 0); + const liveStatusCount = liveStatusTexts.length; // 只数有真文本的(空 text 不算「人话状态行」) + const stepUpdateCount = turnEvents.filter((e) => e.type === 'step_update' || e.type === 'plan_update').length; + const followupCount = turnEvents.filter((e) => e.type === 'suggested_followups').length; + + // 7. 负向断言:probeLiveness 判活性,不得 SPINNER_ORPHAN / SILENT_HANG。 + const livenessEvents: LivenessEvent[] = turnEvents + .map((e) => ({ type: normalizeType(e), atMs: Math.max(0, e.atMs - (turnStart - t0)) })); + const liveness = probeLiveness({ events: livenessEvents, windowEndMs, idleMs }); + const isOrphan = liveness.verdict === 'SPINNER_ORPHAN'; + + // 8. 判定:正向 live_status ≥ min + 负向不得 orphan/hang;step_update/followups 落地后才纳入硬断言。 + const passLiveStatus = liveStatusCount >= minLiveStatus; + const passLiveness = !liveness.isHang && !isOrphan; + const passStep = !c.expectStepUpdate || stepUpdateCount >= 1; + const passFollow = !c.expectFollowups || followupCount >= 1; + const pass = passLiveStatus && passLiveness && passStep && passFollow; + + const details: string[] = [ + `live_status:**${liveStatusCount}/${minLiveStatus}**(要求 ≥${minLiveStatus})${passLiveStatus ? '✅' : '❌'}` + + (liveStatusTexts.length ? ` 例:${liveStatusTexts.slice(0, 3).map((t) => `「${t}」`).join(' → ')}` : ' (一条都没数到)'), + `活性:**${liveness.verdict}**${passLiveness ? '✅' : '❌'}(${liveness.reason})`, + `plan_update=${stepUpdateCount}${c.expectStepUpdate ? `(要求≥1 ${passStep ? '✅' : '❌'})` : '(本 case 仅计数)'}` + + ` suggested_followups=${followupCount}${c.expectFollowups ? `(要求≥1 ${passFollow ? '✅' : '❌'})` : '(本 case 仅计数)'}`, + ]; + if (!passLiveStatus) { + details.push( + `诊断:live_status 不足 = agent 没(按 prompt 纪律)足量调 report_progress。可能根因:` + + `①该 mode toolFilter 屏蔽了 report_progress ②coding mode 丢了「保持节奏感」纪律(CODING_AGENT_IDENTITY 替换 AXON_IDENTITY)` + + `③纯 prompt 遵守率不足 → 需更强约束或重审 D1。`, + ); + } + + return { + caseId: c.id, + description: c.description, + pass, + durationMs: Date.now() - t0, + liveStatusCount, + liveStatusTexts, + stepUpdateCount, + followupCount, + livenessVerdict: liveness.verdict, + isOrphan, + details, + }; +} + +function isTerminal(e: RawWsEvent): boolean { + if (e.type === 'message_complete' || e.type === 'error') return true; + if (e.type === 'status' && e.payload?.status === 'idle') return true; + return false; +} + +function normalizeType(e: RawWsEvent): string { + if (e.type === 'status' && e.payload?.status === 'idle') return 'status_idle'; + return e.type; +} + +function sleep(ms: number): Promise { + return new Promise((r) => setTimeout(r, ms)); +} diff --git a/evals/suites/live-narrative/run.ts b/evals/suites/live-narrative/run.ts new file mode 100644 index 00000000..25382e62 --- /dev/null +++ b/evals/suites/live-narrative/run.ts @@ -0,0 +1,44 @@ +/* + * live-narrative —— 入口脚本(M19.f,对齐 silent-hang/run.ts 风格)。 + * + * 跑前:本地 axon-web(:3456)必须已起(建议 AXON_EVAL_DEBUG=1 npx tsx src/web-cli.ts &)+ codex backend key 可用。 + * CLI:npx tsx evals/suites/live-narrative/run.ts + * + * 打印「# 汇总:N/M pass」让 evals/regression.ts 抓汇总行。 + * + * 这是 M19 叙事层的「跨 producer→consumer 边界」门:断言 live_status 真到达前端消费的 ws 流。 + * 与 silent-hang 互补 —— silent-hang 测「没卡死」,本 suite 测「过程看得见在干啥」。 + */ +import { CASES } from './cases.js'; +import { runNarrativeCase } from './harness.js'; + +async function main(): Promise { + console.log('# live-narrative 动态过程叙事 evals(M19.f)\n'); + console.log(`运行时间:${new Date().toISOString()}`); + console.log(`AXON_EVAL_DEBUG:${process.env.AXON_EVAL_DEBUG ?? '(未设置)'}\n`); + + let pass = 0; + for (const c of CASES) { + process.stdout.write(`▶ ${c.id} … `); + try { + const r = await runNarrativeCase(c); + if (r.pass) pass++; + console.log( + `${r.pass ? '✅' : '❌'} live_status×${r.liveStatusCount} / ${r.livenessVerdict}` + + `${r.retried ? '(已自愈重试)' : ''} (${Math.round(r.durationMs / 1000)}s)`, + ); + for (const d of r.details) console.log(` ${d}`); + } catch (err) { + console.log(`❌ harness 异常:${err instanceof Error ? err.message : String(err)}`); + } + } + + console.log('\n---\n'); + console.log(`# 汇总:${pass}/${CASES.length} pass\n`); + process.exit(pass === CASES.length ? 0 : 1); +} + +main().catch((err) => { + console.error('runner crash:', err); + process.exit(2); +}); diff --git a/evals/suites/real-ui-journey/run.ts b/evals/suites/real-ui-journey/run.ts new file mode 100644 index 00000000..7dce29e1 --- /dev/null +++ b/evals/suites/real-ui-journey/run.ts @@ -0,0 +1,144 @@ +/** + * real-ui-journey —— 真浏览器用户旅程门(2026-06-01)。 + * + * 为什么存在(见 [[feedback_done_means_verified_in_real_product]]):所有现有 eval 都停在 ws 事件 + 磁盘 + + * file:// 渲染(producer 侧),**没有一个"像用户一样开真前端点一下"的 eval**。用户反复反馈「你说完成了, + * 我到前端用就发现没对齐」——根因就是缺这一层。本 suite 用 Playwright 驱动**真 localhost:3456**, + * 完整复现用户动作:点 slides chip → 打字 → Enter 发送 → 等右栏画布 → 断言**人眼可见结果**(画布真渲染了 + * 可翻页 deck,而不是「一条文本气泡 + .pptx 路径、画布空白」这个真实 bug)+ 每步截图存证。 + * + * 跑前:本地 axon-web(:3456)必须已起(含 slides 硬门那版代码)。 + * CLI:AXON_EVAL_DEBUG=1 npx tsx evals/suites/real-ui-journey/run.ts + * + * 设计要点: + * - 这是**消费者侧**断言:iframe[src*="/api/files/preview"] 真出现 = 用户真能在画布看到 deck。 + * - 失败画像精确对齐用户痛点:助手文本里出现 .pptx 路径 + 没有 preview iframe = 后端产物形态错/接线断。 + * - codex 在 agent 端吃 CPU,但 chromium.launch() 在发 prompt 之前完成,之后只 idle 轮询,不会被饿死。 + * - 截图落 /tmp/real-ui-journey-*.png 供人/agent 肉眼复核(眼见为实 > 口头"完成")。 + */ +import * as fs from 'node:fs'; + +const BASE = process.env.AXON_HTTP_URL || 'http://localhost:3456'; +const PROMPT = '请帮我制作一份介绍合肥的 PPT,6-8 页,包含城市概况、历史文化、产业经济、旅游和总结'; +const DEADLINE_MS = Number(process.env.AXON_UI_DEADLINE_MS || 240_000); // 等 codex 建 deck,给足 +const SHOT_DIR = '/tmp'; + +async function main(): Promise { + console.log('# real-ui-journey e2e(真浏览器用户旅程门)\n'); + console.log(`目标:${BASE} · prompt:${PROMPT}\n`); + + let chromium: any; + try { + ({ chromium } = await import('playwright-core')); + } catch { + console.log('⊘ skip:playwright-core 不可用'); + console.log('\n# 汇总:skip'); + process.exit(0); + } + + let browser: any; + try { + browser = await chromium.launch({ args: ['--no-sandbox'] }); + } catch (e) { + console.log(`⊘ skip:chromium 起不来:${(e as Error).message}`); + console.log('\n# 汇总:skip'); + process.exit(0); + } + + const shot = async (page: any, name: string): Promise => { + const p = `${SHOT_DIR}/real-ui-journey-${name}.png`; + try { await page.screenshot({ path: p, fullPage: false }); console.log(` 📸 ${p}`); } catch { /* ignore */ } + }; + + let pass = false; + let verdict = ''; + try { + const page = await browser.newPage({ viewport: { width: 1440, height: 900 } }); + page.setDefaultTimeout(20_000); + + // 预置 onboarding 完成标志(= 真用户配置过之后的常态;新会话才弹首次设置向导)。 + // addInitScript 在页面脚本执行前注入,SetupWizard 读 localStorage('axon_setup_done') 命中就不渲染。 + await page.addInitScript(() => { try { window.localStorage.setItem('axon_setup_done', 'true'); } catch { /* ignore */ } }); + + // 1. 开真前端 + console.log('[1] 打开真前端…'); + await page.goto(BASE, { waitUntil: 'domcontentloaded', timeout: 30_000 }); + // 等 composer 输入框出现 = SPA 真加载完 + const composer = page.locator('textarea[placeholder^="发送消息给 axon"]'); + await composer.waitFor({ state: 'visible', timeout: 30_000 }); + console.log(' ✓ workbench 已加载(composer 可见)'); + await shot(page, '01-loaded'); + + // 1.5 处理首次进入的 setup wizard 浮层(新会话/无 localStorage 必弹,setup-wizard-overlay 拦截所有点击)。 + // 真用户首次也得先过这关 —— eval 忠实复现。连点「跳过」直到浮层消失(最多 5 次防卡)。 + for (let i = 0; i < 5; i++) { + const overlay = page.locator('.setup-wizard-overlay'); + if (!(await overlay.count().catch(() => 0))) break; + const skip = page.getByText('跳过', { exact: false }).first(); + if (await skip.isVisible().catch(() => false)) { + await skip.click({ force: true }).catch(() => {}); + console.log(' ✓ 跳过首次设置向导'); + await page.waitForTimeout(600); + } else { break; } + } + + // 2. 点 slides chip(pinned,label='制作幻灯片') + console.log('[2] 点「制作幻灯片」chip…'); + const chip = page.getByText('制作幻灯片', { exact: false }).first(); + await chip.click({ timeout: 15_000 }); + console.log(' ✓ slides chip 已激活'); + await shot(page, '02-chip-active'); + + // 3. 覆盖预填的 prompt 模板,填真实用户措辞,Enter 发送 + console.log('[3] 输入真实 prompt 并 Enter 发送…'); + await composer.click(); + await composer.fill(PROMPT); + await shot(page, '03-typed'); + await composer.press('Enter'); + console.log(' ✓ 已发送,开始等 agent 建 deck…'); + + // 4. 轮询人眼可见结果,直到 deadline + const started = Date.now(); + let lastLog = 0; + while (Date.now() - started < DEADLINE_MS) { + const previewCount = await page.locator('iframe[src*="/api/files/preview"], iframe[src*="coding_app_preview"]').count().catch(() => 0); + const bodyText = (await page.textContent('body').catch(() => '')) || ''; + const hasPptxPath = /\.pptx/i.test(bodyText); + const hasDelivered = bodyText.includes('已交付') || bodyText.includes('交付') || previewCount > 0; + + const elapsed = Math.round((Date.now() - started) / 1000); + if (elapsed - lastLog >= 15) { console.log(` … 等待中 ${elapsed}s(preview iframe=${previewCount}, 文本含.pptx=${hasPptxPath})`); lastLog = elapsed; } + + // 成功:画布真出现 deck preview iframe + if (previewCount > 0) { + pass = true; + verdict = `✓ 画布真渲染了 deck(preview iframe 出现,${elapsed}s)= 用户能看到`; + break; + } + // 失败画像(用户原痛点):只有 .pptx 文本路径、画布无 preview + if (hasPptxPath && previewCount === 0 && hasDelivered) { + pass = false; + verdict = `✗ 复现 bug:助手给了 .pptx 路径但画布无 deck preview(${elapsed}s)= 用户看到空画布`; + break; + } + await page.waitForTimeout(3000); + } + if (!verdict) verdict = `✗ 超时 ${Math.round(DEADLINE_MS / 1000)}s 内画布未出现 deck preview(agent 可能还在建 / codex 慢 / 接线断)`; + + await shot(page, '04-result'); + console.log(`\n[结果] ${verdict}`); + } catch (e) { + verdict = `✗ 旅程异常:${(e as Error).message}`; + console.log(`\n[结果] ${verdict}`); + } finally { + try { await browser.close(); } catch { /* ignore */ } + } + + console.log(`\n# 汇总:${pass ? '1/1 pass' : '0/1 fail'}`); + // 列出截图供人肉眼复核 + const shots = fs.readdirSync(SHOT_DIR).filter((f) => f.startsWith('real-ui-journey-')).sort(); + console.log(`# 截图:${shots.map((s) => `${SHOT_DIR}/${s}`).join(' , ')}`); + process.exit(pass ? 0 : 1); +} + +main().catch((err) => { console.error('crash:', err); process.exit(2); }); diff --git a/evals/suites/silent-hang/cases.ts b/evals/suites/silent-hang/cases.ts new file mode 100644 index 00000000..b04a2611 --- /dev/null +++ b/evals/suites/silent-hang/cases.ts @@ -0,0 +1,46 @@ +/* + * silent-hang 活体探测 cases(2026-06-01)。 + * + * 设计原则(eval 盲区教训 [[feedback_eval_blind_spot_frontend_wiring]]): + * · 用**真实用户措辞**(直接抄 52.54.122.204 截图里那条),不为了过测而改写措辞。 + * · 复刻截图条件:用户**没点 chip** 直接发(mode=undefined,走 base agent)—— 这正是合肥翻车的入口。 + * · cwd 隔离到 temp([[feedback_eval_must_isolate_cwd]]):跑真 agent 会真写文件,绝不污染仓库。 + * + * 这些 case 断言的是**活性**(不挂起),不是产物正确性 —— 产物对错由 slides-deck suite 管。 + */ +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import type { LivenessCase } from './harness.js'; + +function tempProjectSetup(): (sendWs: (msg: any) => void) => Promise { + return async (sendWs) => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'axon-liveness-')); + sendWs({ type: 'set_project_path', payload: { projectPath: dir } }); + }; +} + +export const CASES: LivenessCase[] = [ + { + // ★截图复刻★:用户原话,base agent(没点 chip)。健康系统应在窗内开始响应(RESPONDED/SLOW_BUT_ALIVE), + // 绝不能 SILENT_HANG。若路由死胡同/codex stall 让它卡死 → 检测器判 hang,route-map 顺带指嫌疑行。 + id: 'hefei-slides-base-no-chip', + description: '复刻截图:「介绍合肥的演示文稿」base agent 裸提问,必须不静默挂起', + prompt: + '请帮我制作一份关于「介绍合肥的」的演示文稿,10-12 页,包含:封面 / 目录 / 核心论点 / 数据图表 / 结论与行动项。风格简洁专业。', + mode: undefined, + deadlineMs: 120_000, + idleMs: 60_000, + setup: tempProjectSetup(), + }, + { + // 健康基线:一句简单问候,几秒内必 RESPONDED。验证检测器对正常轮不误报挂起(防 false positive)。 + id: 'healthy-baseline-greeting', + description: '健康基线:简单问候应快速 RESPONDED(验证不误报挂起)', + prompt: '你好,用一句话介绍你自己。', + mode: 'chat', + deadlineMs: 60_000, + idleMs: 45_000, + strict: true, // 这条必须干净 RESPONDED + }, +]; diff --git a/evals/suites/silent-hang/harness.ts b/evals/suites/silent-hang/harness.ts new file mode 100644 index 00000000..d5c2fa94 --- /dev/null +++ b/evals/suites/silent-hang/harness.ts @@ -0,0 +1,214 @@ +/* + * silent-hang eval harness —— 「卡在哪里没反应」活体探测(Layer B,2026-06-01)。 + * + * 用户诉求(52.54.122.204 截图):发「介绍合肥的演示文稿」后 spinner 永远转,要一个机制能测出来。 + * + * 两层机制: + * · Layer A(确定性,永远可跑):src/web/server/liveness-probe.test.ts —— 用合成事件流钉死检测器 + * 对截图签名(SILENT_HANG / SPINNER_ORPHAN)的判定,不依赖 codex。 + * · Layer B(活体,本文件):对真 :3456 发真实用户措辞的请求,像真用户一样**等终止信号**,用 + * probeLiveness 判这一轮到底是正常响应了、还是卡死了。真 silent hang 是非确定性的(codex stall), + * 活体探测尽力复现 + 兜住「路由死胡同」这类确定性挂起回归。 + * + * ★终止信号必须复刻真前端 ChatPane★:前端停 spinner 靠 message_complete / status:idle / error + * 三者之一(2026-06-01 前端护栏修复后)。本 harness 等这三者中任一到达就停,没到 = 接着等到 deadline。 + * + * 复用:probeLiveness(检测器)+ route-map.diagnoseRoute(hang 落在 slides 意图时顺带指出嫌疑代码行)。 + * 注意 route-map 诊断的是「答了但走错路」,与本模块「根本没答」互补 —— 仅作 hang 时的额外定位线索。 + */ +import WebSocket from 'ws'; +import { + probeLiveness, + type LivenessEvent, + type LivenessResult, +} from '../../../src/web/server/liveness-probe.js'; +import { reconstructTrace, diagnoseRoute } from '../../../src/web/server/route-map.js'; + +const DEFAULT_WS_URL = 'ws://localhost:3456/ws'; + +export interface LivenessCase { + id: string; + description: string; + /** 用户输入(用真实措辞 —— 复刻截图,eval 盲区教训:避真实词会偏离真实用户分布)。 */ + prompt: string; + /** mode id;undefined = 默认 base agent(复刻截图:用户没点 chip 直接发)。 */ + mode?: string; + /** 观测窗(ms),到头还没终止信号就停止观测。默认 120s。 */ + deadlineMs?: number; + /** 多久零进展算挂起(ms)。默认 60s(> codex 冷启动首 token 最坏延迟,防误报)。 */ + idleMs?: number; + /** true = 要求 RESPONDED(不接受 ERRORED);默认 false(只要不是 hang 就算过)。 */ + strict?: boolean; + /** 可选 setup:session_new 后、chat 前执行(如配 project_path 到 temp 隔离目录)。 */ + setup?: (sendWs: (msg: any) => void) => Promise; +} + +export interface LivenessReport { + caseId: string; + description: string; + pass: boolean; + result: LivenessResult; + /** 失败/通过详情(markdown 友好),含 route-map 定位线索。 */ + details: string[]; + durationMs: number; + /** 首跑判挂起触发了自愈重试(codex 冷启动 stall 假挂)。 */ + retried?: boolean; +} + +interface RawWsEvent { + type: string; + payload?: any; + atMs: number; +} + +/** + * 跑一个活体探测 case,自愈一次 codex 冷启动假挂: + * 首跑若判 SILENT_HANG(可能是 codex 冷启动 stall 而非真 bug)→ 重试一次; + * 重试仍 hang = 确定性挂起(真 bug)。SPINNER_ORPHAN 不重试(它是确定性协议签名,重试无意义)。 + */ +export async function runLivenessCase(c: LivenessCase, wsUrl = DEFAULT_WS_URL): Promise { + const first = await runLivenessCaseOnce(c, wsUrl); + if (first.result.verdict === 'SILENT_HANG') { + console.log(` ⟳ ${c.id} 首跑判 SILENT_HANG → 自愈重试一次(排除 codex 冷启动 stall 假挂)`); + const retry = await runLivenessCaseOnce(c, wsUrl); + retry.retried = true; + // 重试若不再 hang,用重试结果;仍 hang 则确认是真挂起,返回重试结果(带 retried 标记)。 + return retry; + } + return first; +} + +async function runLivenessCaseOnce(c: LivenessCase, wsUrl: string): Promise { + const deadlineMs = c.deadlineMs ?? 120_000; + const idleMs = c.idleMs ?? 60_000; + const t0 = Date.now(); + const raw: RawWsEvent[] = []; + const ws = new WebSocket(wsUrl); + + await new Promise((resolve, reject) => { + const to = setTimeout(() => reject(new Error('ws connect timeout 10s')), 10_000); + ws.once('open', () => { clearTimeout(to); resolve(); }); + ws.once('error', (err: any) => { clearTimeout(to); reject(err); }); + }); + + ws.on('message', (data: any) => { + try { + const msg = JSON.parse(data.toString()); + raw.push({ + type: typeof msg?.type === 'string' ? msg.type : 'unknown', + payload: msg?.payload, + atMs: Date.now() - t0, + }); + } catch { + raw.push({ type: 'parse_error', atMs: Date.now() - t0 }); + } + }); + + const sendWs = (msg: any): void => ws.send(JSON.stringify(msg)); + + // 1. 新建 session + sendWs({ type: 'session_new', payload: {} }); + await sleep(300); + // 2. setup(如配 project_path) + if (c.setup) { + await c.setup(sendWs); + await sleep(300); + } + + // 3. 发 chat(带 messageId + 可选 mode)—— 此刻起算「该轮观测窗」。 + const turnStart = Date.now(); + sendWs({ + type: 'chat', + payload: { + content: c.prompt, + messageId: `liveness-${c.id}-${Date.now()}`, + mode: c.mode, + }, + }); + + // 4. 像真前端一样等终止信号(message_complete / error / status:idle 三者任一),到 deadline 为止。 + const deadline = turnStart + deadlineMs; + while (Date.now() < deadline) { + if (raw.some((e) => isTerminal(e))) { + await sleep(300); // 拖尾收齐尾随事件 + break; + } + await sleep(200); + } + const windowEndMs = Date.now() - turnStart; + ws.close(); + + // 5. 映射成 LivenessEvent(归一化 status:idle → 'status_idle'),喂检测器。 + // 只取 turnStart 之后的事件(session_new/setup 的回执不算这一轮)。 + const events: LivenessEvent[] = raw + .map((e) => ({ raw: e, rel: e.atMs - (turnStart - t0) })) + .filter((x) => x.rel >= -50) // 容忍轻微乱序 + .map(({ raw: e, rel }) => ({ type: normalizeType(e), atMs: Math.max(0, rel) })); + + const result = probeLiveness({ events, windowEndMs, idleMs }); + + const details: string[] = [ + `判定:**${result.verdict}** — ${result.reason}`, + `观测:${Math.round(windowEndMs / 1000)}s 窗 / idle 阈值 ${Math.round(idleMs / 1000)}s / ` + + `message_complete=${result.sawMessageComplete} error=${result.sawError} idle=${result.sawIdle} ` + + `最后进展@${result.lastProgressAtMs === null ? '无' : Math.round(result.lastProgressAtMs / 1000) + 's'}`, + ]; + + // 6. hang 时若属 slides 意图,用 route-map 顺带指出嫌疑代码行(额外定位线索)。 + if (result.isHang) { + const trace = reconstructTrace({ + userAsk: c.prompt, + resolvedMode: resolvedModeFromEvents(raw, c.mode), + toolCalls: raw + .filter((e) => e.type === 'tool_use_start') + .map((e) => ({ name: e.payload?.toolName, input: e.payload?.input })), + deliveredKinds: raw + .filter((e) => e.type === 'website_delivered' && typeof e.payload?.kind === 'string') + .map((e) => e.payload.kind), + }); + const diag = diagnoseRoute(trace); + if (diag && diag.suspect) { + details.push(`路由定位(route-map):${diag.plainSummary}`); + } + } + + // pass 判定:默认「只要不是 hang 就算过」(ERRORED/SLOW_BUT_ALIVE 都不是 spinner 永转); + // strict 模式要求 RESPONDED(干净成功)。 + const pass = c.strict ? result.verdict === 'RESPONDED' : !result.isHang; + if (!pass && c.strict && !result.isHang) { + details.push(`strict 模式要求 RESPONDED,实际 ${result.verdict}(非挂起但未干净成功)。`); + } + + return { + caseId: c.id, + description: c.description, + pass, + result, + details, + durationMs: Date.now() - t0, + }; +} + +function isTerminal(e: RawWsEvent): boolean { + if (e.type === 'message_complete' || e.type === 'error') return true; + if (e.type === 'status' && e.payload?.status === 'idle') return true; + return false; +} + +// ws 事件类型 → 检测器归一化类型(status:idle 单列)。 +function normalizeType(e: RawWsEvent): string { + if (e.type === 'status' && e.payload?.status === 'idle') return 'status_idle'; + return e.type; +} + +// 从事件重建 resolvedMode:优先 mode_changed(LLM 调 enter/exit_coding_mode 的真信号), +// 否则回退到 case 显式传的 mode chip,再否则 null(=从未进任何 mode,合肥 root cause)。 +function resolvedModeFromEvents(raw: RawWsEvent[], caseMode?: string): string | null { + const modeChanges = raw.filter((e) => e.type === 'mode_changed' && typeof e.payload?.mode === 'string'); + if (modeChanges.length > 0) return modeChanges[modeChanges.length - 1].payload.mode; + return caseMode ?? null; +} + +function sleep(ms: number): Promise { + return new Promise((r) => setTimeout(r, ms)); +} diff --git a/evals/suites/silent-hang/run.ts b/evals/suites/silent-hang/run.ts new file mode 100644 index 00000000..e767b5c0 --- /dev/null +++ b/evals/suites/silent-hang/run.ts @@ -0,0 +1,41 @@ +/* + * silent-hang —— 入口脚本(对齐 slides-deck/run.ts 风格)。 + * + * 跑前:本地 axon-web(:3456)必须已起(建议 AXON_EVAL_DEBUG=1 npx tsx src/web-cli.ts &)。 + * CLI:npx tsx evals/suites/silent-hang/run.ts + * + * 打印「# 汇总:N/M pass」让 evals/regression.ts 抓汇总行。 + * + * 注:Layer A 的确定性检测器单测在 src/web/server/liveness-probe.test.ts(走 vitest,npm test 常驻门)。 + * 本 run.ts 是 Layer B 活体探测,对真 :3456 像真用户一样探测「卡住没反应」。 + */ +import { CASES } from './cases.js'; +import { runLivenessCase } from './harness.js'; + +async function main(): Promise { + console.log('# silent-hang 活体探测 evals\n'); + console.log(`运行时间:${new Date().toISOString()}`); + console.log(`AXON_EVAL_DEBUG:${process.env.AXON_EVAL_DEBUG ?? '(未设置)'}\n`); + + let pass = 0; + for (const c of CASES) { + process.stdout.write(`▶ ${c.id} … `); + try { + const r = await runLivenessCase(c); + if (r.pass) pass++; + console.log(`${r.pass ? '✅' : '❌'} ${r.result.verdict}${r.retried ? '(已自愈重试)' : ''} (${Math.round(r.durationMs / 1000)}s)`); + for (const d of r.details) console.log(` ${d}`); + } catch (err) { + console.log(`❌ harness 异常:${err instanceof Error ? err.message : String(err)}`); + } + } + + console.log('\n---\n'); + console.log(`# 汇总:${pass}/${CASES.length} pass\n`); + process.exit(pass === CASES.length ? 0 : 1); +} + +main().catch((err) => { + console.error('runner crash:', err); + process.exit(2); +}); diff --git a/evals/suites/slides-deck/harness.ts b/evals/suites/slides-deck/harness.ts index 61cd36b8..0074bfad 100644 --- a/evals/suites/slides-deck/harness.ts +++ b/evals/suites/slides-deck/harness.ts @@ -21,22 +21,60 @@ import * as os from 'node:os'; import * as path from 'node:path'; const WS_URL = process.env.AXON_WS_URL || 'ws://localhost:3456/ws'; +const HTTP_URL = process.env.AXON_HTTP_URL || 'http://localhost:3456'; interface DeckCase { name: string; prompt: string; /** 期望最少页数(LLM 可多不可少) */ minSlides: number; + /** wave1 验证([[project_code_routing_removal_pivot]]):true=不发任何 chip mode,验 base agent + * 靠 prompt 原生知识**自己**路由到 deck(不靠 mode='slides' 门控)。这是「全系统拆代码路由」的招牌证据。 */ + noChip?: boolean; } +// ★prompt 必须用真实用户措辞(含 "PPT" 字样)★:旧 case 写"演示幻灯"刻意避开了 "ppt" 这个词, +// 稳稳触发 deck 流程 → eval 一直绿,但真实用户说"做个 ppt"会把 agent 拽向 Bash+python-pptx 后门 +// 产二进制 .pptx(2026-06-01 合肥 ppt 实锤:前端只收到纯文本+文件路径,画布空)。eval 的盲区不是 +// 断言不够,而是 prompt 分布偏离真实用户 → 这里用真实"PPT"措辞,让回归门真正覆盖危险路径。 const CASES: DeckCase[] = [ + // ★wave1 招牌验证★:不选任何 chip,用用户踩坑的**原话**。base agent 必须靠 AXON_HELPERS 里新加的 + // deck 知识自己路由到 HTML deck(而不是 base mode 下 freelance 一个 .pptx)。绿=「全系统拆代码路由、 + // agent 原生判断」在 slides 这一波成立;红=base prompt 知识不够强,得迭代(不是把 mode 门加回来)。 + { + name: 'hefei-deck-no-chip', + prompt: '请帮我制作一份介绍合肥ppt', + minSlides: 5, + noChip: true, + }, { name: 'hefei-deck', - prompt: '做一份关于合肥的演示幻灯,6-8 页,包含城市概况、历史文化、产业经济、旅游和总结', + prompt: '请帮我制作一份介绍合肥的 PPT,6-8 页,包含城市概况、历史文化、产业经济、旅游和总结', minSlides: 6, }, ]; +/** 递归找 office 二进制产物(.pptx/.ppt/.docx/.xlsx)—— slides 负向断言:deck 模式下不该出现任何二进制 office 文件 */ +function findOfficeBinaries(dir: string): string[] { + const hits: string[] = []; + const OFFICE_EXT = /\.(pptx?|docx?|xlsx?)$/i; + const walk = (d: string): void => { + let entries: fs.Dirent[]; + try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch { return; } + for (const e of entries) { + const full = path.join(d, e.name); + if (e.isDirectory()) { + if (e.name === 'node_modules' || e.name === '.git') continue; + walk(full); + } else if (OFFICE_EXT.test(e.name)) { + hits.push(full); + } + } + }; + walk(dir); + return hits; +} + /** 在 mutable events 数组里轮询直到 pred 命中或超时 */ function waitFor(events: any[], pred: (e: any) => boolean, timeoutMs: number, label: string): Promise { return new Promise((resolve, reject) => { @@ -88,6 +126,27 @@ async function assertDeckRenders(deckHtmlPath: string, minSlides: number): Promi } } +/** + * 真前端 render 端点门(前后端接线断言 — 2026-06-01):file:// 渲染门只证明「磁盘上的 HTML 自己能渲染」, + * 但真前端画布(WebsitePreviewPane)是经 `/api/files/preview?path=&root=` 这个 server 端点把 deck 喂进 + * iframe 的。这条链路断了(路径解析错 / base 注入坏 / 端点 500),file:// 测试照样绿、画布却空白 —— + * 这正是用户说的「前后端接线失败」。这里直接 GET 画布实际加载的端点,断言它真能把 deck 服务出来。 + */ +async function assertPreviewEndpointServes(projectPath: string, entryPath: string, minSlides: number): Promise<{ ok: boolean; reason: string }> { + const url = `${HTTP_URL}/api/files/preview?path=${encodeURIComponent(entryPath)}&root=${encodeURIComponent(projectPath)}`; + let res: Response; + try { + res = await fetch(url); + } catch (e) { + return { ok: false, reason: `端点请求失败(前端画布也会失败):${(e as Error).message}` }; + } + if (!res.ok) return { ok: false, reason: `端点 HTTP ${res.status}(画布 iframe 加载会白屏)` }; + const body = await res.text(); + const slideCount = (body.match(/class="slide/g) || []).length; + if (slideCount < minSlides) return { ok: false, reason: `端点返回的 HTML slide 数=${slideCount} < ${minSlides}(画布拿不到完整 deck)` }; + return { ok: true, reason: `画布 render 端点 OK:返回 ${slideCount} 张 slide 的 HTML` }; +} + export async function runSlidesDeckSuite(): Promise { const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'axon-slides-deck-')); console.log(`[slides-deck] temp project root: ${tmpRoot}`); @@ -115,9 +174,10 @@ export async function runSlidesDeckSuite(): Promise { await new Promise((r) => setTimeout(r, 300)); // 2. 发 slides 任务 + // noChip 用例**不发 mode** → base agent 必须靠原生判断自路由(wave1 招牌);普通用例仍预选 slides chip。 ws.send(JSON.stringify({ type: 'chat', - payload: { content: c.prompt, messageId: `eval-slides-${c.name}-${Date.now()}`, mode: 'slides' }, + payload: { content: c.prompt, messageId: `eval-slides-${c.name}-${Date.now()}`, ...(c.noChip ? {} : { mode: 'slides' }) }, })); // 3. 等 deliver_slides 的 website_delivered(kind='slides')。slides 是重型多轮 case,给足超时。 @@ -162,6 +222,20 @@ export async function runSlidesDeckSuite(): Promise { if (!hasNav) { console.error(`[${c.name}] ✗ 导航脚本丢失(agent 破坏了模板)`); allPass = false; } if (!noPlaceholder) { console.error(`[${c.name}] ✗ 占位符未替换`); allPass = false; } + // 负向断言(slides 招牌防线 — 2026-06-01 合肥 ppt 回归门):deck 模式下不该出现任何二进制 office 文件。 + // agent 若从 Bash+python-pptx 后门产了 .pptx(即便同时也产了 deck),这里直接判红。这是「后端产物形态」 + // 的硬断言:之前 eval 只正向查「deck 存在」,从不查「不该有 .pptx」→ 形态错也放过。 + // ★2026-06-01 wave1 修洞★:no-chip 用例里 agent 会**自选项目路径**(如 /tmp/hefei-introduction-deck), + // 落在 harness temp root 之外 → 只扫 tmpRoot 会漏掉 agent 真工作区里的 .pptx。这张网替代了被删的运行时 + // 代码 guard([[project_code_routing_removal_pivot]]),必须滴水不漏:tmpRoot + agent 真实交付路径都扫,去重。 + const officeBins = [...new Set([...findOfficeBinaries(tmpRoot), ...findOfficeBinaries(deckProjectPath)])]; + if (officeBins.length > 0) { + console.error(`[${c.name}] ✗ 检测到二进制 office 产物(slides 应只产 HTML deck):${officeBins.map((f) => path.basename(f)).join(', ')}`); + allPass = false; + } else { + console.log(`[${c.name}] ✓ 负向断言:无二进制 .pptx/.ppt 产物(形态正确,纯 HTML deck)`); + } + // 关 ws 释放资源 —— 必须在渲染门之前。agent 交付后常继续 stream,codex 持续吃 CPU 会把 // 后面 chromium.launch() 饿死(EXIT 124 根因)。拿到交付物后不再需要 agent,先断连。 ws.close(); @@ -176,6 +250,17 @@ export async function runSlidesDeckSuite(): Promise { console.error(`[${c.name}] ✗ 渲染门:${render.reason}`); allPass = false; } + + // 6. 真前端 render 端点门(前后端接线断言):GET 画布 iframe 实际加载的 /api/files/preview, + // 证明 deck 不只「磁盘上存在」,而是真能经 server 端点喂进前端画布。这层补上 file:// 渲染门 + // 覆盖不到的「后端→前端 render 链路」(用户痛点:后端正常但画布空 = 接线失败)。 + const ep = await assertPreviewEndpointServes(deckProjectPath, entryPath, c.minSlides); + if (ep.ok) { + console.log(`[${c.name}] ✓ 接线门:${ep.reason}`); + } else { + console.error(`[${c.name}] ✗ 接线门:${ep.reason}`); + allPass = false; + } } console.log(`\n[slides-deck] ${allPass ? 'PASS' : 'FAIL'}`); diff --git a/package.json b/package.json index 18905de4..aaef2081 100644 --- a/package.json +++ b/package.json @@ -27,32 +27,30 @@ "build": "tsc && node -e \"const{cpSync,mkdirSync}=require('fs');cpSync('src/browser/extension','dist/browser/extension',{recursive:true});cpSync('src/skills/builtin','dist/skills/builtin',{recursive:true});mkdirSync('dist/eye',{recursive:true});cpSync('src/eye/camera.py','dist/eye/camera.py')\"", "build:web:client": "npm --prefix src/web/client run build", "build:template-registry": "tsx scripts/build-template-registry.ts", + "typecheck": "tsc --noEmit", "start": "node dist/web-cli.js", "dev": "tsx src/web-cli.ts", "dev:watch": "tsx watch src/web-cli.ts --continue", - "web": "tsx src/web-cli.ts", - "web:debug": "node --inspect --import tsx src/web-cli.ts", - "web:start": "node dist/web-cli.js", - "web:dev": "tsx watch src/web-cli.ts", - "web:evolve": "tsx src/web-cli.ts --evolve -H 0.0.0.0", + "dev:debug": "node --inspect --import tsx src/web-cli.ts", + "dev:evolve": "tsx src/web-cli.ts --evolve -H 0.0.0.0", "proxy": "tsx src/proxy-cli.ts", "proxy:start": "node dist/proxy-cli.js", "feishu": "tsx src/feishu-cli.ts", "feishu:webui": "tsx src/feishu-cli.ts --webui", "feishu:start": "node dist/feishu-cli.js", - "install:playwright": "npm install -g @playwright/cli && npx playwright install chromium", + "install:playwright": "npx playwright install chromium", "test": "vitest", "test:ui": "vitest --ui", "test:integration": "vitest tests/integration", "test:unit": "vitest src", "test:coverage": "vitest --coverage", "test:watch": "vitest --watch", - "eval": "tsx evals/cli.ts curated", - "eval:smoke": "tsx evals/cli.ts smoke", + "eval": "tsx evals/cli.ts curated --model gpt-5.5", + "eval:smoke": "tsx evals/cli.ts smoke --model gpt-5.5", + "eval:regression": "tsx evals/regression.ts", "eval:ui-output": "tsx evals/cli.ts ui-output --model gpt-5.5", "eval:hosted-tools": "tsx evals/cli.ts hosted-tools --model gpt-5.5", "eval:website-builder-v3-llm-edit": "tsx evals/cli.ts website-builder-v3-llm-edit --model gpt-5.5", - "mirror:upload:cos": "node scripts/upload-tencent-cos-installer.cjs", "postinstall": "node scripts/postinstall.js || true", "prepublishOnly": "npm run build && npm run build:web:client", "pkg": "pkg dist/web-cli.js --compress GZip --target node18-win-x64 --output release/axon-web.exe", diff --git a/scripts/gen-code-situation.mjs b/scripts/gen-code-situation.mjs new file mode 100644 index 00000000..10a799c4 --- /dev/null +++ b/scripts/gen-code-situation.mjs @@ -0,0 +1,222 @@ +#!/usr/bin/env node +/* + * gen-code-situation.mjs — 采集 axon 真实「码情」→ JSON,喂给 commander-real.html 双镜片渲染。 + * + * 缘起(2026-06-01 /loop):用户说"游戏是假的(硬编码 NotesSaaS),我要真的、接 axon 自己的码情"。 + * 这个脚本把 axon 工作树的真实状态拉出来——真 git(脏/未推/最近提交) + 真测试(跑 client 套件取真 pass/fail) + * + 真模块结构(文件数) + 真现实血条(从 status.md 解析)——派生每个模块的信任态: + * 🔴 broken = 该模块下有失败测试 / 🟡 claimed = 有未提交改动 / 🟢 hard = 干净。 + * 这就是"接入码情":地图上烧的红、晃的黄,全是 axon 此刻真实的样子,不是编的。 + * + * 输出:src/web/client/public/prototypes/code-situation.json(page 同目录 fetch ./code-situation.json)。 + * 用法:node scripts/gen-code-situation.mjs [--no-tests](--no-tests 跳过慢的测试运行,只用 git+结构)。 + */ +import { execSync } from 'node:child_process'; +import { writeFileSync, readFileSync, existsSync, readdirSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import path from 'node:path'; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const OUT = path.join(ROOT, 'src/web/client/public/prototypes/code-situation.json'); +const runTests = !process.argv.includes('--no-tests'); +const sh = (cmd, timeout = 30000) => { + try { return execSync(cmd, { cwd: ROOT, encoding: 'utf-8', timeout, stdio: ['pipe', 'pipe', 'pipe'] }).trim(); } + catch (e) { return (e.stdout || '').toString().trim(); } +}; + +// ---- 1. 真 git 码情 ---- +function gatherGit() { + const branch = sh('git rev-parse --abbrev-ref HEAD') || '?'; + const porcelain = sh('git status --porcelain').split('\n').filter(Boolean); + const dirty = porcelain.filter((l) => !l.startsWith('??')); + const untracked = porcelain.filter((l) => l.startsWith('??')); + let unpushed = 0; + const up = sh('git rev-list --count @{u}..HEAD'); if (/^\d+$/.test(up)) unpushed = +up; + const recent = sh('git log --oneline -6').split('\n').filter(Boolean) + .map((l) => l.replace(/^(\w+)\s/, '').slice(0, 64)); + // 取脏文件路径(去掉前两列状态),用于按模块归因 + const dirtyPaths = dirty.map((l) => l.slice(3).trim()).filter(Boolean); + const untrackedPaths = untracked.map((l) => l.slice(3).trim()).filter(Boolean); + return { branch, dirtyCount: dirty.length, untrackedCount: untracked.length, unpushed, recent, dirtyPaths, untrackedPaths }; +} + +// ---- 2. 真测试码情(跑 client 套件取真 pass/fail) ---- +function gatherTests() { + if (!runTests) return { ran: false, passing: null, failing: null, failingFiles: [] }; + const outFile = '/tmp/cs-vitest.json'; + try { sh(`npx vitest run src/web/client --reporter=json --outputFile=${outFile} 2>/dev/null`, 120000); } catch { /* */ } + try { + const j = JSON.parse(readFileSync(outFile, 'utf-8')); + const failingFiles = (j.testResults || []).filter((r) => r.status === 'failed') + .map((r) => r.name.replace(ROOT + '/', '')).filter(Boolean); + return { ran: true, passing: j.numPassedTests ?? null, failing: j.numFailedTests ?? null, + totalFiles: (j.testResults || []).length, failingFiles }; + } catch { return { ran: true, passing: null, failing: null, failingFiles: [], error: 'parse failed' }; } +} + +// ---- 3. 真模块结构 + 派生信任态 ---- +const MODULES = [ + { id: 'web/server', title: '后端服务', icon: '🛰️', prefix: 'src/web/server' }, + { id: 'web/client', title: '前端界面', icon: '🖥️', prefix: 'src/web/client/src' }, + { id: 'tools', title: '工具系统', icon: '🧰', prefix: 'src/tools' }, + { id: 'coding', title: '编程护城河', icon: '🏰', prefix: 'src/coding' }, + { id: 'blueprint', title: '蓝图/多智能体', icon: '🗺️', prefix: 'src/blueprint' }, + { id: 'core', title: '核心引擎', icon: '⚙️', prefix: 'src/core' }, + { id: 'cloud-sandbox', title: '云沙盒', icon: '☁️', prefix: 'src/cloud-sandbox' }, + { id: 'deploy', title: '部署托管', icon: '🚀', prefix: 'src/deploy' }, + { id: 'memory', title: '长期记忆', icon: '🧠', prefix: 'src/memory' }, + { id: 'web/shared', title: '共享协议', icon: '🔗', prefix: 'src/web/shared' }, +]; +// 垃圾噪音文件(用户反馈:火情里全是 logs/.pptx/Dockerfile 等非源码,要的是真代码函数级信号)。 +const JUNK = /(^|\/)(logs?\d*\.txt|.*\.pptx|Dockerfile|.*\.log|.*\.lock)$|^deploy\/epay\//i; +const isSource = (p) => /\.(ts|tsx|js|jsx|mjs|css)$/.test(p) && !JUNK.test(p); + +// 从改动行往上找最近的外层声明 = 真函数级(比 git 自带 xfuncname 对 TS/TSX 准)。 +const SKIP_KW = new Set(['if', 'for', 'while', 'switch', 'catch', 'return', 'function', 'else', 'do', 'await', 'new']); +function enclosingDecl(lines, idx) { + for (let i = Math.min(idx, lines.length - 1); i >= 0 && i > idx - 500; i--) { + const l = lines[i]; + const m = + l.match(/^\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s*\*?\s*(\w+)/) || // function foo + l.match(/^\s*(?:export\s+)?(?:const|let|var)\s+(\w+)\s*[:=]/) || // const Foo = / : FC + l.match(/^\s*(?:export\s+)?(?:abstract\s+)?class\s+(\w+)/) || // class Foo + l.match(/^\s*(?:export\s+)?(?:interface|type|enum)\s+(\w+)/) || // interface/type Foo + l.match(/^\s*(?:public\s+|private\s+|protected\s+|static\s+|async\s+|get\s+|set\s+)*(\w+)\s*\([^)]*\)\s*[:{]/); // 方法 foo() { + if (m && !SKIP_KW.has(m[1])) return m[1]; + } + return null; +} +// 解析 `git diff HEAD -U0` 的 hunk 新文件行号 → 定位改动落在哪个函数(函数级颗粒度,用户明确要)。 +function changedFuncs(file) { + const out = sh(`git diff HEAD -U0 -- "${file}"`, 12000); + if (!out) return []; + let src = []; + try { src = readFileSync(path.join(ROOT, file), 'utf-8').split('\n'); } catch { return []; } + const funcs = new Set(); + for (const line of out.split('\n')) { + const h = line.match(/^@@\s+-\d+(?:,\d+)?\s+\+(\d+)(?:,(\d+))?\s+@@/); + if (!h) continue; + const start = +h[1], count = h[2] === undefined ? 1 : +h[2]; + for (let ln = start; ln < start + Math.max(count, 1); ln++) { + const name = enclosingDecl(src, ln - 1); // 行号 1-indexed → 数组 0-indexed + if (name) funcs.add(name); + } + } + return [...funcs].slice(0, 10); +} + +function gatherModules(git, tests) { + const failing = tests.failingFiles || []; + const dirtyStatus = new Map(); // path -> 'M'|'A'|'D' (从 porcelain 第二列粗判) + for (const p of git.dirtyPaths) dirtyStatus.set(p, 'M'); + for (const p of git.untrackedPaths || []) dirtyStatus.set(p, 'A'); + return MODULES.map((m) => { + let files = 0; + const c = sh(`find ${m.prefix} -type f \\( -name '*.ts' -o -name '*.tsx' \\) 2>/dev/null | grep -v node_modules | wc -l`); + if (/^\d+$/.test(c)) files = +c; + // 该模块下「真源码」改动(滤掉垃圾噪音)→ 每个文件下钻到函数级。 + const changedPaths = [...git.dirtyPaths, ...(git.untrackedPaths || [])] + .filter((p) => p.startsWith(m.prefix) && isSource(p)); + const changes = changedPaths.slice(0, 12).map((p) => ({ + file: p.slice(m.prefix.length).replace(/^\//, ''), + kind: dirtyStatus.get(p) === 'A' ? 'A' : 'M', + funcs: dirtyStatus.get(p) === 'A' ? [] : changedFuncs(p), // 新文件无 vs-HEAD diff,不列函数 + })); + const dirty = changedPaths.length; // 源码改动数(不含垃圾,颗粒度对齐) + const brokenTests = failing.filter((p) => p.startsWith(m.prefix)).length; + const trust = brokenTests > 0 ? 'broken' : dirty > 0 ? 'claimed' : 'hard'; + return { ...m, files, dirty, brokenTests, trust, changes }; + }).filter((m) => m.files > 0); +} + +// ---- 3b. 里程碑关卡(用户要:项目是 milestone 驱动的,面板得显示 M5→M17 关卡进度)---- +// 解析 docs/spec/roadmap/M{n}-overview.md:标题 + 子任务行的 ✅/⚠ 数 → 关卡进度。 +// 里程碑关卡 = axon 自蓝图 blueprint.json 驱动的**活验证状态机**(M17.n)。 +// ★铁律(违则整层作废)★:blueprint.json 里的 status 只是「我声称的」(claim),绝不当 🟢。 +// 🟢 verified 只能来自 verify-milestone 真跑 gate 绿 + 落盘 .milestone-verified.json + 依赖也验过(断裂检测)。 +// 手敲的 "done" 一律打回 🟡 claimed——这才是「门驱动状态」,不是嘴敲(修我自己 M17.l 的违规)。 +function gatherMilestones() { + const bpPath = path.join(ROOT, 'docs/spec/roadmap/blueprint.json'); + if (!existsSync(bpPath)) return []; + let bp; + try { bp = JSON.parse(readFileSync(bpPath, 'utf-8')); } catch { return []; } + // 验证日志:verify-milestone 真跑 gate 绿后写的硬证据(id → {green, at, passing})。 + const vlogPath = path.join(ROOT, 'docs/spec/roadmap/.milestone-verified.json'); + let vlog = {}; + if (existsSync(vlogPath)) { try { vlog = JSON.parse(readFileSync(vlogPath, 'utf-8')); } catch { /* */ } } + const gateGreen = (id) => !!(vlog[id] && vlog[id].green === true); + // git 出货佐证("feat(M15.a):"、"M8.c" 等引用数)——只做旁证,不当信任来源。 + const log = sh('git log --oneline -600', 15000) || ''; + const commitTally = {}; + for (const m of log.matchAll(/\bM(\d+)(?:[.)\s:a-z]|$)/g)) { const k = 'M' + m[1]; commitTally[k] = (commitTally[k] || 0) + 1; } + return (bp.tasks || []).map((tk) => { + const subtasks = Array.isArray(tk.subtasks) ? tk.subtasks : []; + const done = subtasks.filter((s) => s.done).length; + const claim = tk.status; // 我声称的(done/wip/planned)——不当真 + const deps = tk.dependsOn || []; + const green = gateGreen(tk.id); + const depsVerified = deps.every((d) => gateGreen(d)); // 依赖都 gate 验过了吗(断裂检测) + // ★信任裁决★:gate 绿+依赖验过=verified🟢;gate 绿但依赖没验=断裂(broken_seam); + // 声称 done 但没跑 gate=claimed🟡(我嘴说的);wip/planned 照声称。 + let trust; + if (green && depsVerified) trust = 'verified'; + else if (green && !depsVerified) trust = 'broken_seam'; + else if (claim === 'done') trust = 'claimed'; + else if (claim === 'wip') trust = 'wip'; + else trust = 'planned'; + // 下一个「可验证」= 声称 done、依赖都验过、自己还没跑 gate、且有 gate 可跑。 + const actionable = claim === 'done' && !green && depsVerified && !!tk.verify; + return { + id: tk.id, num: +String(tk.id).replace(/\D/g, ''), title: tk.title, + status: claim, trust, actionable, + verifiedAt: vlog[tk.id] ? vlog[tk.id].at : null, + dependsOn: deps, depsVerified, + verify: tk.verify || null, + commits: commitTally[tk.id] || 0, + done, total: subtasks.length, subtasks, + }; + }).sort((a, b) => a.num - b.num); +} + +// ---- 4. 真现实血条 BOSS HP(从 status.md 解析三层 — 与「现实血条」锚点同源) ---- +// 三层都从 status.md 顶部「现实血条 (BOSS HP)」真解析,不硬编码: +// 代理 BOSS = N/M 种真实形态 · 真 BOSS = 真人(非作者)交付数 · 终极 BOSS = 付费数 + MRR。 +// ★诚实★:真 BOSS / 付费 = 0 就老实返 0,不粉饰(anti-gaming,铁律11)。 +function gatherReality() { + let forms = 4, target = 10, realDelivery = 0, paying = 0, mrr = 0; + const sp = path.join(ROOT, 'docs/spec/status.md'); + if (existsSync(sp)) { + const t = readFileSync(sp, 'utf-8'); + const m = t.match(/(\d+)\s*\/\s*(\d+)\s*种真实形态/); if (m) { forms = +m[1]; target = +m[2]; } + // 真 BOSS:「真人(非作者)成功交付(当前 **0**)」 + const r = t.match(/成功交付(当前\s*\*{0,2}(\d+)/); if (r) realDelivery = +r[1]; + // 终极 BOSS:「付费真人(当前 **0** · MRR **¥0**)」(分隔符容差,匹配到 ¥ 数字) + const p = t.match(/付费真人(当前\s*\*{0,2}(\d+)\*{0,2}[^¥]*¥?([\d.]+)/); + if (p) { paying = +p[1]; mrr = +p[2]; } + } + return { forms, target, realDelivery, users: 0, paying, mrr }; +} + +// ---- 组装 ---- +const git = gatherGit(); +const tests = gatherTests(); +const modules = gatherModules(git, tests); +const milestones = gatherMilestones(); +const reality = gatherReality(); +const fires = []; +if (tests.failing) fires.push({ kind: 'test', n: tests.failing, label: `${tests.failing} 个测试在红` , detail: (tests.failingFiles || []).map((f) => f.split('/').pop()).join('、') }); +// 未提交火情:只算+只列「真源码」改动(滤掉 logs/.pptx 等垃圾噪音,对齐用户「要真代码函数级」反馈)。 +const srcDirty = [...git.dirtyPaths, ...git.untrackedPaths].filter(isSource); +if (srcDirty.length) fires.push({ kind: 'uncommitted', n: srcDirty.length, label: `${srcDirty.length} 处源码改动没盖章(未提交)`, detail: srcDirty.slice(0, 6).map((p) => p.split('/').pop()).join('、') }); +if (git.unpushed) fires.push({ kind: 'unpushed', n: git.unpushed, label: `${git.unpushed} 个提交没推上去`, detail: '本地领先远端' }); + +const data = { + // 注:生成时间由调用方/脚本注入避免不确定性问题——这里用 git 最近提交时间近似真实感 + generatedBy: 'gen-code-situation.mjs', branch: git.branch, + git: { dirty: git.dirtyCount, untracked: git.untrackedCount, unpushed: git.unpushed, recent: git.recent }, + tests, modules, milestones, reality, fires, +}; +writeFileSync(OUT, JSON.stringify(data, null, 2)); +console.log('✓ 真实码情已采集 →', OUT.replace(ROOT + '/', '')); +console.log(` 分支 ${git.branch} · 脏 ${git.dirtyCount} · 未推 ${git.unpushed} · 测试 ${tests.passing}✓/${tests.failing}✗ · 模块 ${modules.length}`); +console.log(' 信任态:', modules.map((m) => `${m.title}=${m.trust}`).join(' ')); diff --git a/scripts/m15-railway-fullstack-verify.ts b/scripts/m15-railway-fullstack-verify.ts new file mode 100644 index 00000000..a84b4c5d --- /dev/null +++ b/scripts/m15-railway-fullstack-verify.ts @@ -0,0 +1,168 @@ +/** + * M15 真 Railway 全栈 publish 验证(破「真人交付=0」的全栈那半)—— 2026-06-01 + * + * 完整链路(镜像 publish-fullstack.ts 生产路径): + * provision Neon Postgres → 写最小全栈 app(Express+pg 真读写库) → RailwayDeployAdapter.deploy + * (railway up 源码部署) → waitUntilReady(域名探活) → fetch 验证「后端在跑 + DB 连通 + 公网公开可访问」 + * + * 「用完即删」:try/finally 保证无论成败都拆干净 —— Railway 项目(按唯一名查 id→projectDelete) + * + Neon 库(deleteProject) + 临时目录。绝不留真实计费资源。 + * + * 跑法:AXON_EDITION=cloud npx tsx scripts/m15-railway-fullstack-verify.ts + */ + +process.env.AXON_EDITION = 'cloud'; + +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { ensureCloudEnvLoaded } from '../src/edition.js'; +import { RailwayDeployAdapter } from '../src/deploy/railway-adapter.js'; +import { NeonProvisioner } from '../src/deploy/neon-adapter.js'; + +const RAILWAY_API = 'https://backboard.railway.app/graphql/v2'; + +/** 直接打 Railway GraphQL(teardown 用,adapter 没暴露公开删项目)。 */ +async function railwayGql(token: string, query: string, variables?: Record): Promise { + const r = await fetch(RAILWAY_API, { + method: 'POST', + headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, + body: JSON.stringify({ query, ...(variables ? { variables } : {}) }), + }); + const j: any = await r.json().catch(() => ({})); + if (!r.ok || j.errors) throw new Error(`Railway GraphQL ${r.status}: ${j?.errors?.[0]?.message ?? ''}`); + return j.data; +} + +/** 按唯一项目名查 Railway projectId(teardown 定位)。 + * 用顶层 projects 查询而非 me{projects}:team/workspace token 对 me 无权(Not Authorized), + * 但顶层 projects 可列(2026-06-01 真探针实测)。 */ +async function findRailwayProjectId(token: string, name: string): Promise { + const d = await railwayGql(token, 'query { projects { edges { node { id name } } } }'); + const edges = d?.projects?.edges ?? []; + return edges.find((e: any) => e.node?.name === name)?.node?.id ?? null; +} + +async function main() { + await ensureCloudEnvLoaded(); + const railwayToken = process.env.RAILWAY_API_TOKEN ?? process.env.RAILWAY_TOKEN; + if (!railwayToken) throw new Error('未找到 RAILWAY_API_TOKEN(.env AXON_EDITION=cloud 应已注入)'); + if (!process.env.NEON_API_KEY) throw new Error('未找到 NEON_API_KEY'); + + const stamp = Date.now(); + const marker = `axon-railway-fullstack-${stamp}`; + const projName = `axon-fs-verify-${stamp}`; + const t0 = Date.now(); + + let neonProjectId: string | null = null; + let tmpDir: string | null = null; + let railwayProjectId: string | null = null; + let verdict = '未完成'; + + try { + // [1/5] provision Neon Postgres(真开库) + console.log('[1/5] provision Neon Postgres...'); + const neon = new NeonProvisioner(); + const prov = await neon.createProject(projName); + neonProjectId = prov.projectId; + console.log(` ✅ Neon 库已开 projectId=${neonProjectId}`); + + // [2/5] 写最小全栈 app(Express + pg 真连库读写,证「全栈」而非纯静态/纯服务) + console.log('[2/5] 生成最小全栈 app(Express + pg → Neon)...'); + tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'axon-railway-verify-')); + await fs.promises.writeFile(path.join(tmpDir, 'package.json'), JSON.stringify({ + name: projName, version: '1.0.0', private: true, + scripts: { start: 'node server.js' }, + dependencies: { express: '^4.19.2', pg: '^8.11.5' }, + }, null, 2)); + await fs.promises.writeFile(path.join(tmpDir, 'server.js'), ` +const express = require('express'); +const { Pool } = require('pg'); +const MARKER = ${JSON.stringify(marker)}; +const pool = new Pool({ connectionString: process.env.DATABASE_URL, ssl: { rejectUnauthorized: false } }); +const app = express(); +app.get('/', async (req, res) => { + try { + await pool.query('CREATE TABLE IF NOT EXISTS axon_proof(id serial primary key, marker text, at timestamptz default now())'); + await pool.query('INSERT INTO axon_proof(marker) VALUES($1)', [MARKER]); + const { rows } = await pool.query('SELECT count(*)::int AS n, now() AS now FROM axon_proof'); + res.set('content-type', 'text/html; charset=utf-8').send( + 'axon 全栈交付验证' + + '' + + '
' + + '

🎉 全栈 app 公网运行中

' + + '

后端 Express 在 Railway 跑,数据来自 Neon Postgres 真实读写。

' + + '

DB 行数:' + rows[0].n + ' · 服务器时间:' + rows[0].now + '

' + + '

' + MARKER + '

'); + } catch (e) { + res.status(500).send('DB error: ' + e.message); + } +}); +app.listen(process.env.PORT || 3000, () => console.log('up on ' + (process.env.PORT || 3000))); +`.trim()); + console.log(` ✅ app 写到 ${tmpDir}`); + + // [3/5] Railway 源码部署(railway up)→ 注入 DATABASE_URL + console.log('[3/5] Railway 源码部署(建 project→service→up→域名,可能 1-3 分钟)...'); + const adapter = new RailwayDeployAdapter({ token: railwayToken }); + const deployed = await adapter.deploy({ + name: projName, + sourceDir: tmpDir, + envVars: { DATABASE_URL: prov.databaseUrl }, + }); + console.log(` ✅ 部署创建,公网域名 url=${deployed.url}`); + + // [4/5] 探活直到 2xx/3xx + console.log('[4/5] 轮询域名直到就绪...'); + const status = await adapter.waitUntilReady(deployed.deploymentId, { timeoutMs: 300_000, pollMs: 6_000 }); + if (status.state !== 'ready') throw new Error(`Railway 未就绪:${status.state}`); + console.log(` ✅ state=ready url=${status.url}`); + + // [5/5] fetch 验证:后端在跑 + DB 连通 + 公开可访问 + console.log('[5/5] fetch 验证全栈链路...'); + const res = await fetch(status.url!, { redirect: 'follow' }); + const body = await res.text(); + const ok = res.status === 200 && body.includes(marker) && /DB 行数:\d+<\/b>/.test(body); + console.log(` status=${res.status} hasMarker=${body.includes(marker)} hasDbRows=${/DB 行数/.test(body)}`); + + console.log('\n========== 结论 =========='); + console.log(`部署 URL: ${status.url}`); + if (ok) { + verdict = 'PASS'; + console.log('🎉 全栈 last-mile 实证:后端 Express 在 Railway 公网跑 + Neon Postgres 真读写 + 无需登录公开访问'); + console.log(` 👉 ${status.url}(即将拆除,截图/访问要趁现在)`); + console.log(`\n真人交付=0 的「全栈那半」已补齐。总耗时 ${Math.round((Date.now() - t0) / 1000)}s`); + } else { + throw new Error(`全栈验证未通过:status=${res.status},页面=${body.slice(0, 300)}`); + } + } finally { + // ── 用完即删(无论成败):不留真实计费资源 ────────────────────────── + console.log('\n[teardown] 拆除真实资源(不留账单)...'); + try { + railwayProjectId = await findRailwayProjectId(railwayToken, projName); + if (railwayProjectId) { + await railwayGql(railwayToken, 'mutation($id:String!){ projectDelete(id:$id) }', { id: railwayProjectId }); + console.log(` ✅ Railway 项目已删 ${railwayProjectId}`); + } else { + console.log(' ℹ️ 未找到 Railway 项目(可能 deploy 失败已自动回滚删除)'); + } + } catch (e) { + console.warn(` ⚠️ Railway 拆除失败,需手动删项目 ${projName}:${(e as Error).message}`); + } + try { + if (neonProjectId) { + await new NeonProvisioner().deleteProject(neonProjectId); + console.log(` ✅ Neon 库已删 ${neonProjectId}`); + } + } catch (e) { + console.warn(` ⚠️ Neon 拆除失败,需手动删库 ${neonProjectId}:${(e as Error).message}`); + } + try { if (tmpDir) await fs.promises.rm(tmpDir, { recursive: true, force: true }); } catch { /* ignore */ } + console.log(`[teardown] 完成。验证结果:${verdict}`); + } +} + +main().catch((e) => { + console.error('\n❌ Railway 全栈验证 FAILED:', e?.message ?? e); + process.exit(1); +}); diff --git a/scripts/m15-real-publish-verify.ts b/scripts/m15-real-publish-verify.ts new file mode 100644 index 00000000..d627c5ac --- /dev/null +++ b/scripts/m15-real-publish-verify.ts @@ -0,0 +1,114 @@ +/** + * M15 真 publish 端到端验证(破「真人交付=0」)—— 2026-06-01 + * + * 完整链路:deploy → waitUntilReady → 【fetch 前:记录 protection 状态】→ makePublic + * → 【fetch 后:验证 URL 公开可访问(200+内容) 而非 401 需登录】 + * + * 这是 makePublic「最后一公里」的真实证据:用前后对比证明关 Deployment Protection + * 真的把「需登录的 401」变成了「能直接发给别人的公开 URL」。 + * + * 跑法:AXON_EDITION=cloud npx tsx scripts/m15-real-publish-verify.ts + * (AXON_EDITION 必须在进程启动时就设:getEdition 读 process.env 并缓存, + * 晚设会缓存成 community 导致 ensureCloudEnvLoaded no-op、.env 不加载、token 拿不到) + */ + +process.env.AXON_EDITION = 'cloud'; + +import { ensureCloudEnvLoaded } from '../src/edition.js'; +import { VercelDeployAdapter } from '../src/deploy/vercel-adapter.js'; + +/** fetch URL,返回 {status, isAuthWall, hasMarker}。不抛错(网络问题也照实记录)。 */ +async function probe(url: string, marker: string): Promise<{ status: number; isAuthWall: boolean; hasMarker: boolean }> { + try { + const res = await fetch(url, { redirect: 'manual' }); + const body = await res.text().catch(() => ''); + const isAuthWall = + res.status === 401 || + res.status === 403 || + // Vercel SSO 保护会 307/302 跳到 vercel.com/sso-api 或返回 Authentication Required 页 + ((res.status === 302 || res.status === 307) && /vercel\.com\/sso|authenticate/i.test(res.headers.get('location') ?? '')) || + /Authentication Required/i.test(body); + return { status: res.status, isAuthWall, hasMarker: body.includes(marker) }; + } catch (e) { + return { status: -1, isAuthWall: false, hasMarker: false }; + } +} + +async function main() { + await ensureCloudEnvLoaded(); + if (!process.env.VERCEL_TOKEN) throw new Error('未找到 VERCEL_TOKEN(.env AXON_EDITION=cloud 应已注入)'); + + const adapter = new VercelDeployAdapter(); + const stamp = Date.now(); + const marker = `axon-real-publish-${stamp}`; + // slug 同时作为 Vercel 项目名(inline-files deploy 自动建此项目)+ makePublic 的标识 + const slug = `axon-real-publish-${stamp}`; + const t0 = Date.now(); + + // 一个像样的可分享落地页(真人交付的产物应是「能发给别人看」的东西,不是 hello world) + const html = ` + +axon · 真人交付验证 + +
+

🎉 公开可访问

+

这个页面由 axon 一键发布到 Vercel,
并已自动关闭 Deployment Protection。

+

任何人无需登录即可打开此链接。

+

${marker}

+
`; + + console.log('[1/5] 创建真部署...'); + const result = await adapter.deploy({ name: slug, target: 'production', files: [{ path: 'index.html', content: html }] }); + console.log(` ✅ deploymentId=${result.deploymentId} url=${result.url}`); + + console.log('[2/5] 轮询直到 ready...'); + const status = await adapter.waitUntilReady(result.deploymentId, { timeoutMs: 120_000 }); + if (status.state !== 'ready') throw new Error(`部署未 ready: ${status.state}`); + const finalUrl = status.url ?? result.url; + console.log(` ✅ state=ready url=${finalUrl}`); + + console.log('[3/5] makePublic 前 probe(记录初始 protection 状态)...'); + const before = await probe(finalUrl, marker); + console.log(` before: status=${before.status} authWall=${before.isAuthWall} hasMarker=${before.hasMarker}`); + + console.log('[4/5] 调 makePublic(关 Deployment Protection)...'); + await adapter.makePublic(slug); + console.log(' ✅ makePublic 返回成功(PATCH /v9/projects 置 ssoProtection+passwordProtection=null)'); + + console.log('[5/5] makePublic 后 probe(验证变公开,带重试等 Vercel 生效)...'); + let after = await probe(finalUrl, marker); + for (let i = 0; i < 6 && !after.hasMarker; i++) { + await new Promise((r) => setTimeout(r, 2500)); + after = await probe(finalUrl, marker); + console.log(` retry#${i + 1}: status=${after.status} authWall=${after.isAuthWall} hasMarker=${after.hasMarker}`); + } + + console.log('\n========== 结论 =========='); + console.log(`部署 URL: ${finalUrl}`); + console.log(`before makePublic: ${before.isAuthWall ? '🔒 需登录(authWall)' : before.hasMarker ? '🌐 已公开' : `status=${before.status}`}`); + console.log(`after makePublic: ${after.hasMarker ? '🌐 公开可访问(内容命中 marker)' : after.isAuthWall ? '🔒 仍需登录' : `status=${after.status}`}`); + + if (after.hasMarker) { + const contrast = before.isAuthWall ? '(before 是 401/authWall → after 公开,最后一公里实证 ✅)' : '(该账号 deploy 默认即公开,makePublic 幂等无害)'; + console.log(`\n🎉 真人交付=0 已破:产出能直接发给任何人的公开 URL ${contrast}`); + console.log(` 👉 ${finalUrl}`); + console.log(`\n总耗时 ${Date.now() - t0}ms`); + } else { + throw new Error(`makePublic 后 URL 仍不可公开访问(status=${after.status} authWall=${after.isAuthWall})——最后一公里未真正打通,需排查`); + } +} + +main().catch((e) => { + console.error('\n❌ 真 publish 验证 FAILED:', e?.message ?? e); + process.exit(1); +}); diff --git a/src/agents/statusline.ts b/src/agents/statusline.ts deleted file mode 100644 index b630e14c..00000000 --- a/src/agents/statusline.ts +++ /dev/null @@ -1,687 +0,0 @@ -/** - * Statusline Setup Agent - * 配置 Axon 状态行设置 - */ - -import * as fs from 'fs'; -import * as path from 'path'; -import * as os from 'os'; -import { VERSION } from '../version.js'; - -// ============ 类型定义 ============ - -/** - * 状态行配置 - */ -export interface StatuslineConfig { - type: 'command' | 'template' | 'disabled'; - command?: string; - template?: string; - refreshInterval?: number; - position?: 'top' | 'bottom'; - style?: StatuslineStyle; -} - -/** - * 状态行样式配置 - */ -export interface StatuslineStyle { - backgroundColor?: string; - textColor?: string; - dimmed?: boolean; - bold?: boolean; - separatorChar?: string; -} - -/** - * 状态行显示元素 - */ -export interface StatuslineElement { - type: 'model' | 'tokens' | 'cost' | 'duration' | 'tools' | 'cwd' | - 'session' | 'git' | 'custom' | 'context_percentage'; - format?: string; - visible: boolean; - label?: string; -} - -/** - * 会话上下文数据(由 statusLine 命令接收) - */ -export interface StatuslineContext { - session_id: string; - transcript_path: string; - cwd: string; - model: { - id: string; - display_name: string; - }; - workspace: { - current_dir: string; - project_dir: string; - }; - version: string; - output_style: { - name: string; - }; - context_window: { - total_input_tokens: number; - total_output_tokens: number; - context_window_size: number; - current_usage: { - input_tokens: number; - output_tokens: number; - cache_creation_input_tokens: number; - cache_read_input_tokens: number; - } | null; - }; -} - -/** - * PS1 转义序列映射 - */ -export const PS1_ESCAPE_MAPPINGS: Record = { - '\\u': '$(whoami)', - '\\h': '$(hostname -s)', - '\\H': '$(hostname)', - '\\w': '$(pwd)', - '\\W': '$(basename "$(pwd)")', - '\\$': '$', - '\\n': '\\n', - '\\t': '$(date +%H:%M:%S)', - '\\d': '$(date "+%a %b %d")', - '\\@': '$(date +%I:%M%p)', - '\\#': '#', - '\\!': '!', -}; - -// ============ Statusline Agent 类 ============ - -/** - * Statusline 代理 - * 负责配置和管理 Axon 的状态行设置 - */ -export class StatuslineAgent { - private configDir: string; - private settingsFile: string; - private scriptsDir: string; - - constructor() { - // 初始化配置目录 - this.configDir = path.join(os.homedir(), '.axon'); - this.settingsFile = path.join(this.configDir, 'settings.json'); - this.scriptsDir = this.configDir; - - // 确保目录存在 - this.ensureDirectories(); - } - - /** - * 确保必要的目录存在 - */ - private ensureDirectories(): void { - if (!fs.existsSync(this.configDir)) { - fs.mkdirSync(this.configDir, { recursive: true }); - } - } - - /** - * 获取当前状态行配置 - */ - async getConfig(): Promise { - try { - const settings = this.readSettings(); - return settings?.statusLine || null; - } catch (error) { - console.error('Failed to read statusline config:', error); - return null; - } - } - - /** - * 配置状态行 - */ - async configure(config: Partial): Promise { - const settings = this.readSettings() || {}; - - settings.statusLine = { - ...settings.statusLine, - ...config, - }; - - this.writeSettings(settings); - } - - /** - * 从 PS1 环境变量导入配置 - */ - async importFromPS1(shellConfigPath?: string): Promise<{ - success: boolean; - ps1?: string; - command?: string; - error?: string; - }> { - try { - // 尝试从 shell 配置文件读取 PS1 - const shellFiles = shellConfigPath ? [shellConfigPath] : [ - path.join(os.homedir(), '.zshrc'), - path.join(os.homedir(), '.bashrc'), - path.join(os.homedir(), '.bash_profile'), - path.join(os.homedir(), '.profile'), - ]; - - let ps1Value: string | null = null; - let sourceFile: string | null = null; - - // 按优先级读取 - for (const file of shellFiles) { - if (fs.existsSync(file)) { - const content = fs.readFileSync(file, 'utf-8'); - const ps1Match = content.match(/(?:^|\n)\s*(?:export\s+)?PS1\s*=\s*["']([^"']+)["']/m); - - if (ps1Match) { - ps1Value = ps1Match[1]; - sourceFile = file; - break; - } - } - } - - if (!ps1Value) { - return { - success: false, - error: 'No PS1 configuration found in shell files. Please provide manual configuration.', - }; - } - - // 转换 PS1 为 shell 命令 - const command = this.convertPS1ToCommand(ps1Value); - - return { - success: true, - ps1: ps1Value, - command, - }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : String(error), - }; - } - } - - /** - * 转换 PS1 转义序列为 shell 命令 - */ - private convertPS1ToCommand(ps1: string): string { - let command = ps1; - - // 替换所有 PS1 转义序列 - for (const [escape, replacement] of Object.entries(PS1_ESCAPE_MAPPINGS)) { - command = command.replace(new RegExp(escape.replace(/\\/g, '\\\\'), 'g'), replacement); - } - - // 移除尾部的 $ 或 > 提示符 - command = command.replace(/\s*[\$>]\s*$/, ''); - - // 包装为 printf 以正确处理 ANSI 颜色代码 - if (command.includes('\\033') || command.includes('\\e[')) { - command = `printf '${command}'`; - } - - return command; - } - - /** - * 创建状态行脚本文件 - */ - async createScript( - scriptName: string, - scriptContent: string, - makeExecutable: boolean = true - ): Promise { - const scriptPath = path.join(this.scriptsDir, scriptName); - - // 写入脚本内容 - fs.writeFileSync(scriptPath, scriptContent, { mode: makeExecutable ? 0o755 : 0o644 }); - - return scriptPath; - } - - /** - * 创建预置模板脚本 - */ - async createTemplateScript(template: StatuslineTemplate): Promise { - const scriptContent = this.getTemplateScript(template); - const scriptName = `statusline-${template}.sh`; - - return this.createScript(scriptName, scriptContent, true); - } - - /** - * 获取模板脚本内容 - */ - private getTemplateScript(template: StatuslineTemplate): string { - const templates: Record = { - minimal: `#!/bin/bash -# Minimal statusline - model and directory only -input=$(cat) -model=$(echo "$input" | jq -r '.model.display_name') -dir=$(echo "$input" | jq -r '.workspace.current_dir' | sed "s|$HOME|~|") -printf "%s in %s" "$model" "$dir" -`, - standard: `#!/bin/bash -# Standard statusline - model, directory, and context usage -input=$(cat) -model=$(echo "$input" | jq -r '.model.display_name') -dir=$(echo "$input" | jq -r '.workspace.current_dir' | sed "s|$HOME|~|") -usage=$(echo "$input" | jq '.context_window.current_usage') - -if [ "$usage" != "null" ]; then - current=$(echo "$usage" | jq '.input_tokens + .cache_creation_input_tokens + .cache_read_input_tokens') - size=$(echo "$input" | jq '.context_window.context_window_size') - pct=$((current * 100 / size)) - printf "%s in %s (%d%% context)" "$model" "$dir" "$pct" -else - printf "%s in %s" "$model" "$dir" -fi -`, - detailed: `#!/bin/bash -# Detailed statusline - full information with git branch -input=$(cat) -model=$(echo "$input" | jq -r '.model.display_name') -dir=$(echo "$input" | jq -r '.workspace.current_dir' | sed "s|$HOME|~|") -style=$(echo "$input" | jq -r '.output_style.name') -usage=$(echo "$input" | jq '.context_window.current_usage') - -# Get git branch if in a git repo -git_branch="" -project_dir=$(echo "$input" | jq -r '.workspace.project_dir') -if [ -d "$project_dir/.git" ]; then - git_branch=$(cd "$project_dir" && git branch --show-current 2>/dev/null) - if [ -n "$git_branch" ]; then - git_branch=" [$git_branch]" - fi -fi - -# Calculate context percentage -context_info="" -if [ "$usage" != "null" ]; then - current=$(echo "$usage" | jq '.input_tokens + .cache_creation_input_tokens + .cache_read_input_tokens') - size=$(echo "$input" | jq '.context_window.context_window_size') - pct=$((current * 100 / size)) - context_info=" | \${pct}% ctx" -fi - -printf "%s in %s%s | %s%s" "$model" "$dir" "$git_branch" "$style" "$context_info" -`, - custom: `#!/bin/bash -# Custom statusline template - modify as needed -input=$(cat) - -# Extract available data -model=$(echo "$input" | jq -r '.model.display_name') -model_id=$(echo "$input" | jq -r '.model.id') -dir=$(echo "$input" | jq -r '.workspace.current_dir') -project=$(echo "$input" | jq -r '.workspace.project_dir') -session=$(echo "$input" | jq -r '.session_id') -version=$(echo "$input" | jq -r '.version') -style=$(echo "$input" | jq -r '.output_style.name') - -# Context window data -total_in=$(echo "$input" | jq '.context_window.total_input_tokens') -total_out=$(echo "$input" | jq '.context_window.total_output_tokens') -window_size=$(echo "$input" | jq '.context_window.context_window_size') -current_usage=$(echo "$input" | jq '.context_window.current_usage') - -# Build your custom statusline here -printf "Custom: %s in %s" "$model" "$dir" -`, - }; - - return templates[template]; - } - - /** - * 重置为默认配置 - */ - async resetToDefault(): Promise { - const settings = this.readSettings() || {}; - delete settings.statusLine; - this.writeSettings(settings); - } - - /** - * 预览状态行(使用模拟数据) - */ - async preview(command?: string): Promise { - const config = await this.getConfig(); - const cmdToTest = command || config?.command; - - if (!cmdToTest) { - return 'No statusline command configured'; - } - - // 创建模拟的上下文数据 - const mockContext: StatuslineContext = { - session_id: 'preview-session', - transcript_path: '/tmp/preview.json', - cwd: process.cwd(), - model: { - id: 'claude-sonnet-4-5-20250929', - display_name: 'Claude 3.5 Sonnet', - }, - workspace: { - current_dir: process.cwd(), - project_dir: process.cwd(), - }, - version: VERSION, - output_style: { - name: 'default', - }, - context_window: { - total_input_tokens: 15000, - total_output_tokens: 5000, - context_window_size: 200000, - current_usage: { - input_tokens: 12000, - output_tokens: 3000, - cache_creation_input_tokens: 2000, - cache_read_input_tokens: 1000, - }, - }, - }; - - try { - // 如果是脚本文件,执行它 - if (cmdToTest.startsWith('/') || cmdToTest.startsWith('~/')) { - const { execSync } = await import('child_process'); - const result = execSync(cmdToTest, { - input: JSON.stringify(mockContext), - encoding: 'utf-8', - timeout: 5000, - }); - return result.trim(); - } - - // 否则作为内联命令执行 - const { execSync } = await import('child_process'); - const result = execSync(`echo '${JSON.stringify(mockContext)}' | ${cmdToTest}`, { - encoding: 'utf-8', - shell: '/bin/bash', - timeout: 5000, - }); - return result.trim(); - } catch (error) { - return `Preview error: ${error instanceof Error ? error.message : String(error)}`; - } - } - - /** - * 读取设置文件 - */ - private readSettings(): Record | null { - try { - // 检查是否是符号链接 - let targetFile = this.settingsFile; - if (fs.lstatSync(this.settingsFile).isSymbolicLink()) { - targetFile = fs.readlinkSync(this.settingsFile); - if (!path.isAbsolute(targetFile)) { - targetFile = path.join(path.dirname(this.settingsFile), targetFile); - } - } - - if (!fs.existsSync(targetFile)) { - return null; - } - - const content = fs.readFileSync(targetFile, 'utf-8'); - return JSON.parse(content); - } catch (error) { - console.error('Failed to read settings:', error); - return null; - } - } - - /** - * 写入设置文件 - */ - private writeSettings(settings: Record): void { - try { - // 检查是否是符号链接 - let targetFile = this.settingsFile; - if (fs.existsSync(this.settingsFile) && fs.lstatSync(this.settingsFile).isSymbolicLink()) { - targetFile = fs.readlinkSync(this.settingsFile); - if (!path.isAbsolute(targetFile)) { - targetFile = path.join(path.dirname(this.settingsFile), targetFile); - } - } - - // 确保目录存在 - const dir = path.dirname(targetFile); - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }); - } - - fs.writeFileSync(targetFile, JSON.stringify(settings, null, 2), 'utf-8'); - } catch (error) { - throw new Error(`Failed to write settings: ${error instanceof Error ? error.message : String(error)}`); - } - } - - /** - * 验证命令是否安全 - */ - validateCommand(command: string): { valid: boolean; error?: string } { - // 基本安全检查 - const dangerousPatterns = [ - /rm\s+-rf\s+\//, - /:\(\)\{.*\};:/, // Fork bomb - /eval/, - /\$\(.*rm.*\)/, - ]; - - for (const pattern of dangerousPatterns) { - if (pattern.test(command)) { - return { - valid: false, - error: 'Command contains potentially dangerous patterns', - }; - } - } - - return { valid: true }; - } -} - -// ============ 预设模板类型 ============ - -export type StatuslineTemplate = 'minimal' | 'standard' | 'detailed' | 'custom'; - -// ============ Agent 系统提示词 ============ - -export const STATUSLINE_AGENT_SYSTEM_PROMPT = `You are a status line setup agent for Axon. Your job is to create or update the statusLine command in the user's Axon settings. - -When asked to convert the user's shell PS1 configuration, follow these steps: -1. Read the user's shell configuration files in this order of preference: - - ~/.zshrc - - ~/.bashrc - - ~/.bash_profile - - ~/.profile - -2. Extract the PS1 value using this regex pattern: /(?:^|\\n)\\s*(?:export\\s+)?PS1\\s*=\\s*["']([^"']+)["']/m - -3. Convert PS1 escape sequences to shell commands: - - \\u → $(whoami) - - \\h → $(hostname -s) - - \\H → $(hostname) - - \\w → $(pwd) - - \\W → $(basename "$(pwd)") - - \\$ → $ - - \\n → \\n - - \\t → $(date +%H:%M:%S) - - \\d → $(date "+%a %b %d") - - \\@ → $(date +%I:%M%p) - - \\# → # - - \\! → ! - -4. When using ANSI color codes, be sure to use \`printf\`. Do not remove colors. Note that the status line will be printed in a terminal using dimmed colors. - -5. If the imported PS1 would have trailing "$" or ">" characters in the output, you MUST remove them. - -6. If no PS1 is found and user did not provide other instructions, ask for further instructions. - -How to use the statusLine command: -1. The statusLine command will receive the following JSON input via stdin: - { - "session_id": "string", // Unique session ID - "transcript_path": "string", // Path to the conversation transcript - "cwd": "string", // Current working directory - "model": { - "id": "string", // Model ID (e.g., "claude-3-5-sonnet-20241022") - "display_name": "string" // Display name (e.g., "Claude 3.5 Sonnet") - }, - "workspace": { - "current_dir": "string", // Current working directory path - "project_dir": "string" // Project root directory path - }, - "version": "string", // Axon app version (e.g., "1.0.71") - "output_style": { - "name": "string", // Output style name (e.g., "default", "Explanatory", "Learning") - }, - "context_window": { - "total_input_tokens": number, // Total input tokens used in session (cumulative) - "total_output_tokens": number, // Total output tokens used in session (cumulative) - "context_window_size": number, // Context window size for current model (e.g., 200000) - "current_usage": { // Token usage from last API call (null if no messages yet) - "input_tokens": number, // Input tokens for current context - "output_tokens": number, // Output tokens generated - "cache_creation_input_tokens": number, // Tokens written to cache - "cache_read_input_tokens": number // Tokens read from cache - } | null - } - } - - You can use this JSON data in your command like: - - $(cat | jq -r '.model.display_name') - - $(cat | jq -r '.workspace.current_dir') - - $(cat | jq -r '.output_style.name') - - Or store it in a variable first: - - input=$(cat); echo "$(echo "$input" | jq -r '.model.display_name') in $(echo "$input" | jq -r '.workspace.current_dir')" - - To calculate context window percentage, use current_usage (current context) not the cumulative totals: - - input=$(cat); usage=$(echo "$input" | jq '.context_window.current_usage'); if [ "$usage" != "null" ]; then current=$(echo "$usage" | jq '.input_tokens + .cache_creation_input_tokens + .cache_read_input_tokens'); size=$(echo "$input" | jq '.context_window.context_window_size'); pct=$((current * 100 / size)); printf '%d%% context' "$pct"; fi - -2. For longer commands, you can save a new file in the user's ~/.axon directory, e.g.: - - ~/.axon/statusline-command.sh and reference that file in the settings. - -3. Update the user's ~/.axon/settings.json with: - { - "statusLine": { - "type": "command", - "command": "your_command_here" - } - } - -4. If ~/.axon/settings.json is a symlink, update the target file instead. - -Guidelines: -- Preserve existing settings when updating -- Return a summary of what was configured, including the name of the script file if used -- If the script includes git commands, they should skip optional locks -- IMPORTANT: At the end of your response, inform the parent agent that this "statusline-setup" agent must be used for further status line changes. - Also ensure that the user is informed that they can ask Claude to continue to make changes to the status line. -`; - -// ============ Agent 配置 ============ - -export const STATUSLINE_AGENT_CONFIG = { - agentType: 'statusline-setup', - whenToUse: 'Use this agent to configure the user\'s Axon status line setting.', - tools: ['Read', 'Edit'], - source: 'built-in', - baseDir: 'built-in', - model: 'sonnet', - color: 'orange', - systemPrompt: STATUSLINE_AGENT_SYSTEM_PROMPT, -}; - -// ============ 导出默认实例 ============ - -export const statuslineAgent = new StatuslineAgent(); - -// ============ 辅助函数 ============ - -/** - * 创建简单的内联状态行命令 - */ -export function createInlineCommand(elements: StatuslineElement[]): string { - const parts: string[] = []; - - for (const element of elements) { - if (!element.visible) continue; - - switch (element.type) { - case 'model': - parts.push('$(cat | jq -r \'.model.display_name\')'); - break; - case 'cwd': - parts.push('$(cat | jq -r \'.workspace.current_dir\' | sed "s|$HOME|~|")'); - break; - case 'session': - parts.push('$(cat | jq -r \'.session_id\' | cut -c1-8)'); - break; - case 'context_percentage': - parts.push('$(input=$(cat); usage=$(echo "$input" | jq \'.context_window.current_usage\'); if [ "$usage" != "null" ]; then current=$(echo "$usage" | jq \'.input_tokens + .cache_creation_input_tokens + .cache_read_input_tokens\'); size=$(echo "$input" | jq \'.context_window.context_window_size\'); pct=$((current * 100 / size)); printf \'%d%% ctx\' "$pct"; fi)'); - break; - case 'custom': - if (element.format) { - parts.push(element.format); - } - break; - } - } - - return parts.join(' | '); -} - -/** - * 解析并验证 statusline 命令 - */ -export function parseStatuslineCommand(command: string): { - valid: boolean; - uses_jq: boolean; - uses_git: boolean; - complexity: 'simple' | 'medium' | 'complex'; - warnings?: string[]; -} { - const warnings: string[] = []; - - const uses_jq = command.includes('jq'); - const uses_git = command.includes('git'); - - // 检测复杂度 - let complexity: 'simple' | 'medium' | 'complex' = 'simple'; - const pipeCount = (command.match(/\|/g) || []).length; - const commandSubstCount = (command.match(/\$\(/g) || []).length; - - if (pipeCount > 3 || commandSubstCount > 5) { - complexity = 'complex'; - warnings.push('Complex command may have performance impact'); - } else if (pipeCount > 1 || commandSubstCount > 2) { - complexity = 'medium'; - } - - // 检查 jq 依赖 - if (uses_jq) { - warnings.push('Requires jq to be installed'); - } - - return { - valid: true, - uses_jq, - uses_git, - complexity, - warnings: warnings.length > 0 ? warnings : undefined, - }; -} diff --git a/src/blueprint/browser-test-tools.ts b/src/blueprint/browser-test-tools.ts deleted file mode 100644 index ddf313ee..00000000 --- a/src/blueprint/browser-test-tools.ts +++ /dev/null @@ -1,859 +0,0 @@ -/** - * BrowserTestTools - 端到端测试专用浏览器工具 - * - * 封装 Chrome MCP 工具,提供更高级的测试 API: - * - 应用启动/停止 - * - 截图与设计图对比 - * - 表单操作与断言 - * - 等待与超时处理 - * - * 设计理念:让 E2E 测试 Agent 能够像人一样操作浏览器验收功能 - */ - -import { spawn, ChildProcess } from 'child_process'; -import * as fs from 'fs'; -import * as path from 'path'; -import { EventEmitter } from 'events'; - -// ============================================================================ -// 类型定义 -// ============================================================================ - -/** - * 浏览器测试配置 - */ -export interface BrowserTestConfig { - /** 项目路径 */ - projectPath: string; - /** 前端启动命令 */ - frontendCommand?: string; - /** 后端启动命令 */ - backendCommand?: string; - /** 前端端口 */ - frontendPort?: number; - /** 后端端口 */ - backendPort?: number; - /** 启动超时(毫秒) */ - startupTimeout?: number; - /** MCP 工具调用器 */ - mcpToolCaller: McpToolCaller; -} - -/** - * MCP 工具调用器接口 - */ -export interface McpToolCaller { - call(toolName: string, args: Record): Promise; -} - -/** - * MCP 工具调用结果 - */ -export interface McpToolResult { - success: boolean; - content?: string; - error?: string; - image?: { - data: string; // base64 - mimeType: string; - }; -} - -/** - * 页面元素 - */ -export interface PageElement { - refId: string; - role: string; - name?: string; - text?: string; - bounds?: { x: number; y: number; width: number; height: number }; -} - -/** - * 截图对比结果 - */ -export interface ScreenshotCompareResult { - /** 是否匹配 */ - matches: boolean; - /** 相似度分数 (0-100) */ - similarityScore: number; - /** 差异描述 */ - differences: string[]; - /** 截图路径 */ - screenshotPath: string; - /** 设计图路径 */ - designPath: string; -} - -/** - * 测试步骤结果 - */ -export interface TestStepResult { - step: string; - success: boolean; - duration: number; - error?: string; - screenshot?: string; - comparison?: ScreenshotCompareResult; -} - -/** - * 应用进程信息 - */ -interface AppProcess { - name: string; - process: ChildProcess; - port: number; - ready: boolean; -} - -// ============================================================================ -// BrowserTestTools 实现 -// ============================================================================ - -export class BrowserTestTools extends EventEmitter { - private config: BrowserTestConfig; - private processes: AppProcess[] = []; - private tabId: number | null = null; - private isRunning = false; - - constructor(config: BrowserTestConfig) { - super(); - this.config = { - frontendPort: 3000, - backendPort: 3001, - startupTimeout: 60000, - ...config, - }; - } - - // ========================================================================== - // 应用启动/停止 - // ========================================================================== - - /** - * 启动应用(前端+后端) - */ - async startApp(): Promise<{ success: boolean; error?: string }> { - this.log('启动应用...'); - - try { - // 检测项目类型并确定启动命令 - const commands = await this.detectStartCommands(); - - // 启动后端(如果有) - if (commands.backend) { - await this.startProcess('backend', commands.backend, this.config.backendPort!); - } - - // 启动前端 - if (commands.frontend) { - await this.startProcess('frontend', commands.frontend, this.config.frontendPort!); - } - - // 等待服务就绪 - await this.waitForServices(); - - this.isRunning = true; - this.log('应用启动成功'); - return { success: true }; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - this.log(`应用启动失败: ${message}`); - await this.stopApp(); - return { success: false, error: message }; - } - } - - /** - * 停止应用 - */ - async stopApp(): Promise { - this.log('停止应用...'); - - for (const proc of this.processes) { - try { - proc.process.kill('SIGTERM'); - // 给进程时间优雅退出 - await new Promise(resolve => setTimeout(resolve, 1000)); - if (!proc.process.killed) { - proc.process.kill('SIGKILL'); - } - } catch (e) { - // 忽略已退出的进程 - } - } - - this.processes = []; - this.isRunning = false; - this.log('应用已停止'); - } - - /** - * 检测项目启动命令 - */ - private async detectStartCommands(): Promise<{ frontend?: string; backend?: string }> { - const packageJsonPath = path.join(this.config.projectPath, 'package.json'); - - if (fs.existsSync(packageJsonPath)) { - const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); - const scripts = packageJson.scripts || {}; - - // 检测常见的启动脚本 - let frontend: string | undefined; - let backend: string | undefined; - - // 前端 - if (scripts['dev:client']) { - frontend = 'npm run dev:client'; - } else if (scripts['dev:frontend']) { - frontend = 'npm run dev:frontend'; - } else if (scripts['start:client']) { - frontend = 'npm run start:client'; - } else if (scripts.dev) { - frontend = 'npm run dev'; - } else if (scripts.start) { - frontend = 'npm start'; - } - - // 后端 - if (scripts['dev:server']) { - backend = 'npm run dev:server'; - } else if (scripts['dev:backend']) { - backend = 'npm run dev:backend'; - } else if (scripts['start:server']) { - backend = 'npm run start:server'; - } - - // 使用配置中指定的命令覆盖 - if (this.config.frontendCommand) { - frontend = this.config.frontendCommand; - } - if (this.config.backendCommand) { - backend = this.config.backendCommand; - } - - return { frontend, backend }; - } - - // Python 项目 - const pyprojectPath = path.join(this.config.projectPath, 'pyproject.toml'); - if (fs.existsSync(pyprojectPath)) { - return { - backend: 'python -m uvicorn main:app --reload', - }; - } - - throw new Error('Unable to detect project type and start commands'); - } - - /** - * 启动单个进程 - */ - private async startProcess(name: string, command: string, port: number): Promise { - this.log(`启动 ${name}: ${command}`); - - const [cmd, ...args] = command.split(' '); - const proc = spawn(cmd, args, { - cwd: this.config.projectPath, - shell: true, - env: { ...process.env, PORT: String(port) }, - }); - - const appProcess: AppProcess = { - name, - process: proc, - port, - ready: false, - }; - - proc.stdout?.on('data', (data) => { - const output = data.toString(); - this.emit('process:stdout', { name, output }); - - // 检测服务就绪 - if (this.isServiceReady(output)) { - appProcess.ready = true; - } - }); - - proc.stderr?.on('data', (data) => { - const output = data.toString(); - this.emit('process:stderr', { name, output }); - - // 某些框架用 stderr 输出启动信息 - if (this.isServiceReady(output)) { - appProcess.ready = true; - } - }); - - proc.on('error', (error) => { - this.emit('process:error', { name, error }); - }); - - proc.on('exit', (code) => { - this.emit('process:exit', { name, code }); - }); - - this.processes.push(appProcess); - } - - /** - * 检测服务是否就绪 - */ - private isServiceReady(output: string): boolean { - const readyPatterns = [ - /ready/i, - /listening on/i, - /started server/i, - /server running/i, - /localhost:\d+/i, - /Local:\s+http/i, - /compiled successfully/i, - ]; - - return readyPatterns.some(pattern => pattern.test(output)); - } - - /** - * 等待服务就绪 - */ - private async waitForServices(): Promise { - const timeout = this.config.startupTimeout!; - const startTime = Date.now(); - - while (Date.now() - startTime < timeout) { - const allReady = this.processes.every(p => p.ready); - if (allReady && this.processes.length > 0) { - // 额外等待一下确保服务完全就绪 - await new Promise(resolve => setTimeout(resolve, 2000)); - return; - } - await new Promise(resolve => setTimeout(resolve, 500)); - } - - throw new Error(`Service startup timeout (${timeout}ms)`); - } - - // ========================================================================== - // 浏览器操作 - // ========================================================================== - - /** - * 打开浏览器访问应用 - */ - async openBrowser(url?: string): Promise<{ success: boolean; tabId?: number; error?: string }> { - const targetUrl = url || `http://localhost:${this.config.frontendPort}`; - this.log(`打开浏览器: ${targetUrl}`); - - try { - // 创建新标签页 - const createResult = await this.config.mcpToolCaller.call('tabs_create_mcp', { - url: targetUrl, - }); - - if (!createResult.success) { - return { success: false, error: createResult.error || 'Failed to create tab' }; - } - - // 获取标签页信息 - const contextResult = await this.config.mcpToolCaller.call('tabs_context_mcp', {}); - - if (contextResult.success && contextResult.content) { - // 解析 tabId - const match = contextResult.content.match(/tabId[:\s]+(\d+)/i); - if (match) { - this.tabId = parseInt(match[1], 10); - } - } - - // 等待页面加载 - await this.waitForPageLoad(); - - return { success: true, tabId: this.tabId || undefined }; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - return { success: false, error: message }; - } - } - - /** - * 导航到指定 URL - */ - async navigate(url: string): Promise<{ success: boolean; error?: string }> { - if (!this.tabId) { - return { success: false, error: 'Browser not open' }; - } - - this.log(`导航到: ${url}`); - - const result = await this.config.mcpToolCaller.call('navigate', { - tabId: this.tabId, - url, - action: 'goto', - }); - - if (result.success) { - await this.waitForPageLoad(); - } - - return { success: result.success, error: result.error }; - } - - /** - * 等待页面加载完成 - */ - private async waitForPageLoad(timeout = 10000): Promise { - const startTime = Date.now(); - - while (Date.now() - startTime < timeout) { - // 简单地等待一段时间,让页面加载 - await new Promise(resolve => setTimeout(resolve, 1000)); - - // 可以通过 JavaScript 检查 document.readyState - if (this.tabId) { - const result = await this.config.mcpToolCaller.call('javascript_tool', { - action: 'javascript_exec', - tabId: this.tabId, - text: 'document.readyState', - }); - - if (result.success && result.content?.includes('complete')) { - return; - } - } - } - } - - /** - * 查找元素 - */ - async findElement(query: string): Promise { - if (!this.tabId) return null; - - const result = await this.config.mcpToolCaller.call('find', { - tabId: this.tabId, - query, - }); - - if (result.success && result.content) { - // 解析返回的元素信息 - const match = result.content.match(/ref_id[:\s]+"?([^"\s,]+)"?/i); - if (match) { - return { - refId: match[1], - role: 'element', - text: query, - }; - } - } - - return null; - } - - /** - * 等待元素出现 - */ - async waitForElement(query: string, timeout = 10000): Promise { - const startTime = Date.now(); - - while (Date.now() - startTime < timeout) { - const element = await this.findElement(query); - if (element) return element; - await new Promise(resolve => setTimeout(resolve, 500)); - } - - return null; - } - - /** - * 点击元素 - */ - async click(refIdOrQuery: string): Promise<{ success: boolean; error?: string }> { - if (!this.tabId) { - return { success: false, error: 'Browser not open' }; - } - - let refId = refIdOrQuery; - - // 如果不是 refId 格式,尝试查找元素 - if (!refIdOrQuery.match(/^[a-z0-9-]+$/i)) { - const element = await this.findElement(refIdOrQuery); - if (!element) { - return { success: false, error: `Element not found: ${refIdOrQuery}` }; - } - refId = element.refId; - } - - this.log(`点击元素: ${refIdOrQuery}`); - - const result = await this.config.mcpToolCaller.call('computer', { - action: 'click', - tabId: this.tabId, - ref_id: refId, - }); - - return { success: result.success, error: result.error }; - } - - /** - * 输入文本 - */ - async type(text: string): Promise<{ success: boolean; error?: string }> { - if (!this.tabId) { - return { success: false, error: 'Browser not open' }; - } - - this.log(`输入文本: ${text.substring(0, 50)}...`); - - const result = await this.config.mcpToolCaller.call('computer', { - action: 'type', - tabId: this.tabId, - text, - }); - - return { success: result.success, error: result.error }; - } - - /** - * 填写表单字段 - */ - async fillField(refIdOrQuery: string, value: string): Promise<{ success: boolean; error?: string }> { - if (!this.tabId) { - return { success: false, error: 'Browser not open' }; - } - - let refId = refIdOrQuery; - - if (!refIdOrQuery.match(/^[a-z0-9-]+$/i)) { - const element = await this.findElement(refIdOrQuery); - if (!element) { - return { success: false, error: `Element not found: ${refIdOrQuery}` }; - } - refId = element.refId; - } - - this.log(`填写字段: ${refIdOrQuery} = ${value.substring(0, 20)}...`); - - const result = await this.config.mcpToolCaller.call('form_input', { - tabId: this.tabId, - ref_id: refId, - value, - }); - - return { success: result.success, error: result.error }; - } - - /** - * 按键 - */ - async pressKey(key: string): Promise<{ success: boolean; error?: string }> { - if (!this.tabId) { - return { success: false, error: 'Browser not open' }; - } - - this.log(`按键: ${key}`); - - const result = await this.config.mcpToolCaller.call('computer', { - action: 'key', - tabId: this.tabId, - text: key, - }); - - return { success: result.success, error: result.error }; - } - - /** - * 滚动页面 - */ - async scroll(direction: 'up' | 'down' | 'left' | 'right', amount = 300): Promise<{ success: boolean; error?: string }> { - if (!this.tabId) { - return { success: false, error: 'Browser not open' }; - } - - const result = await this.config.mcpToolCaller.call('computer', { - action: 'scroll', - tabId: this.tabId, - direction, - amount, - }); - - return { success: result.success, error: result.error }; - } - - // ========================================================================== - // 截图与对比 - // ========================================================================== - - /** - * 截取当前页面截图 - */ - async takeScreenshot(savePath?: string): Promise<{ success: boolean; path?: string; base64?: string; error?: string }> { - if (!this.tabId) { - return { success: false, error: 'Browser not open' }; - } - - this.log('截取页面截图...'); - - // 使用 read_page 获取页面内容(包含截图) - // 或者使用 gif_creator capture 功能 - const result = await this.config.mcpToolCaller.call('gif_creator', { - action: 'capture', - tabId: this.tabId, - }); - - if (result.success && result.image) { - const base64 = result.image.data; - - if (savePath) { - const buffer = Buffer.from(base64, 'base64'); - fs.writeFileSync(savePath, buffer); - return { success: true, path: savePath, base64 }; - } - - return { success: true, base64 }; - } - - return { success: false, error: result.error || 'Screenshot failed' }; - } - - /** - * 与设计图对比(供 AI Agent 调用) - * - * 返回对比提示,让 AI 分析差异 - */ - async prepareScreenshotComparison( - screenshotBase64: string, - designImagePath: string - ): Promise<{ prompt: string; images: Array<{ type: 'base64'; data: string; mediaType: string }> }> { - // 读取设计图 - let designBase64: string; - - if (fs.existsSync(designImagePath)) { - const designBuffer = fs.readFileSync(designImagePath); - designBase64 = designBuffer.toString('base64'); - } else { - throw new Error(`Design image not found: ${designImagePath}`); - } - - return { - prompt: `请对比以下两张图片: - -**图1**: 当前页面截图(实际效果) -**图2**: 设计图(预期效果) - -请分析: -1. 布局是否一致?(元素位置、大小、间距) -2. 颜色是否一致?(背景色、文字色、按钮色) -3. 文字是否正确?(内容、字体、大小) -4. 是否有遗漏或多余的元素? -5. 整体视觉效果如何? - -请给出: -- 相似度评分 (0-100) -- 具体差异列表 -- 是否通过验收(相似度 >= 80 为通过)`, - images: [ - { type: 'base64', data: screenshotBase64, mediaType: 'image/png' }, - { type: 'base64', data: designBase64, mediaType: 'image/png' }, - ], - }; - } - - // ========================================================================== - // 断言 - // ========================================================================== - - /** - * 断言元素存在 - */ - async assertElementExists(query: string): Promise<{ success: boolean; error?: string }> { - const element = await this.waitForElement(query, 5000); - - if (element) { - return { success: true }; - } - - return { success: false, error: `Element not found: ${query}` }; - } - - /** - * 断言页面包含文本 - */ - async assertTextExists(text: string): Promise<{ success: boolean; error?: string }> { - if (!this.tabId) { - return { success: false, error: 'Browser not open' }; - } - - const result = await this.config.mcpToolCaller.call('get_page_text', { - tabId: this.tabId, - }); - - if (result.success && result.content?.includes(text)) { - return { success: true }; - } - - return { success: false, error: `Page does not contain text: ${text}` }; - } - - /** - * 断言 URL - */ - async assertUrl(expectedUrl: string | RegExp): Promise<{ success: boolean; actualUrl?: string; error?: string }> { - if (!this.tabId) { - return { success: false, error: 'Browser not open' }; - } - - const result = await this.config.mcpToolCaller.call('javascript_tool', { - action: 'javascript_exec', - tabId: this.tabId, - text: 'window.location.href', - }); - - if (result.success && result.content) { - const actualUrl = result.content.replace(/^["']|["']$/g, ''); - - if (typeof expectedUrl === 'string') { - if (actualUrl === expectedUrl || actualUrl.includes(expectedUrl)) { - return { success: true, actualUrl }; - } - } else if (expectedUrl.test(actualUrl)) { - return { success: true, actualUrl }; - } - - return { success: false, actualUrl, error: `URL mismatch: expected ${expectedUrl}, actual ${actualUrl}` }; - } - - return { success: false, error: 'Failed to get URL' }; - } - - /** - * 检查控制台错误 - */ - async checkConsoleErrors(): Promise<{ hasErrors: boolean; errors: string[] }> { - if (!this.tabId) { - return { hasErrors: false, errors: [] }; - } - - const result = await this.config.mcpToolCaller.call('read_console_messages', { - tabId: this.tabId, - pattern: 'error|Error|ERROR', - }); - - if (result.success && result.content) { - const errors = result.content.split('\n').filter(line => line.trim()); - return { hasErrors: errors.length > 0, errors }; - } - - return { hasErrors: false, errors: [] }; - } - - /** - * 检查网络错误 - */ - async checkNetworkErrors(): Promise<{ hasErrors: boolean; errors: string[] }> { - if (!this.tabId) { - return { hasErrors: false, errors: [] }; - } - - const result = await this.config.mcpToolCaller.call('read_network_requests', { - tabId: this.tabId, - }); - - if (result.success && result.content) { - // 查找 4xx 和 5xx 状态码 - const errorPattern = /status[:\s]+(4\d{2}|5\d{2})/gi; - const matches = result.content.match(errorPattern) || []; - - return { hasErrors: matches.length > 0, errors: matches }; - } - - return { hasErrors: false, errors: [] }; - } - - // ========================================================================== - // 辅助方法 - // ========================================================================== - - /** - * 获取页面文本内容 - */ - async getPageText(): Promise { - if (!this.tabId) return ''; - - const result = await this.config.mcpToolCaller.call('get_page_text', { - tabId: this.tabId, - }); - - return result.content || ''; - } - - /** - * 执行 JavaScript - */ - async executeScript(script: string): Promise<{ success: boolean; result?: string; error?: string }> { - if (!this.tabId) { - return { success: false, error: 'Browser not open' }; - } - - const result = await this.config.mcpToolCaller.call('javascript_tool', { - action: 'javascript_exec', - tabId: this.tabId, - text: script, - }); - - return { - success: result.success, - result: result.content, - error: result.error, - }; - } - - /** - * 读取页面结构(accessibility tree) - */ - async readPageStructure(filter: 'all' | 'interactive' = 'interactive'): Promise { - if (!this.tabId) return ''; - - const result = await this.config.mcpToolCaller.call('read_page', { - tabId: this.tabId, - filter, - }); - - return result.content || ''; - } - - /** - * 日志输出 - */ - private log(message: string): void { - console.log(`[BrowserTestTools] ${message}`); - this.emit('log', message); - } - - /** - * 获取运行状态 - */ - isAppRunning(): boolean { - return this.isRunning; - } - - /** - * 获取当前 Tab ID - */ - getTabId(): number | null { - return this.tabId; - } -} - -// ============================================================================ -// 工厂函数 -// ============================================================================ - -/** - * 创建浏览器测试工具实例 - */ -export function createBrowserTestTools(config: BrowserTestConfig): BrowserTestTools { - return new BrowserTestTools(config); -} diff --git a/src/blueprint/task-reviewer.ts b/src/blueprint/task-reviewer.ts deleted file mode 100644 index 83ec4e4d..00000000 --- a/src/blueprint/task-reviewer.ts +++ /dev/null @@ -1,769 +0,0 @@ -/** - * Task Reviewer Agent - 任务审查代理 - * - * 设计理念: - * - 分权制衡:执行者(Worker) ≠ 审核者(Reviewer) - * - 自然语言理解:用 AI 判断任务是否完成,而不是机械规则 - * - 使用 ConversationLoop,与 Worker 使用相同的认证方式 - * - * 工作流程: - * Worker 执行 → 收集材料 → Reviewer 审查 → 返回结论 - */ - -import { SmartTask, ModelType, Blueprint, TechStack } from './types.js'; -import { ConversationLoop } from '../core/loop.js'; -import { getAgentDecisionMaker } from './agent-decision-maker.js'; -import { SubmitReviewTool } from '../tools/submit-review.js'; - -// ============== 审查上下文 ============== - -/** - * v4.0: 审查上下文 - Reviewer 拥有的全局视角 - */ -export interface ReviewContext { - projectPath?: string; - isRetry?: boolean; - previousAttempts?: number; - /** v6.1: 上次失败的审查反馈(让 Reviewer 知道之前失败的原因) */ - lastReviewFeedback?: { - verdict: 'failed' | 'needs_revision'; - reasoning: string; - issues?: string[]; - suggestions?: string[]; - }; - - // v4.0: 全局上下文(类似 Queen 的视角) - /** v6.1: 使用 Pick 引用 Blueprint 类型,避免内联重复定义 */ - blueprint?: Pick; - - // 相关任务(上下文) - relatedTasks?: Array<{ - id: string; - name: string; - status: string; - }>; -} - -// ============== 类型定义 ============== - -/** - * 审查结论 - */ -export type ReviewVerdict = 'passed' | 'failed' | 'needs_revision'; - -/** - * 工具调用记录(用于审查) - */ -export interface ToolCallRecord { - name: string; - input?: Record; - output?: string; - error?: string; - timestamp?: number; -} - -/** - * 文件变更记录 - */ -export interface FileChangeRecord { - path: string; - type: 'created' | 'modified' | 'deleted'; - contentPreview?: string; // 变更内容预览(前 500 字符) -} - -/** - * Worker 执行结果(传给 Reviewer 的材料) - */ -export interface WorkerExecutionSummary { - // Worker 自我汇报 - selfReported: { - completed: boolean; - message?: string; - }; - - // 工具调用摘要 - toolCalls: ToolCallRecord[]; - - // 文件变更 - fileChanges: FileChangeRecord[]; - - // 测试状态(如果有) - testStatus?: { - ran: boolean; - passed: boolean; - output?: string; - }; - - // 执行耗时 - durationMs: number; - - // 错误信息(如果有) - error?: string; -} - -/** - * 审查结果 - */ -export interface ReviewResult { - verdict: ReviewVerdict; - confidence: 'high' | 'medium' | 'low'; - reasoning: string; - - // v4.0: Reviewer 实际验证过的内容 - verified?: string[]; - - // 如果失败,具体问题 - issues?: string[]; - - // 如果需要修改,建议 - suggestions?: string[]; - - // 审查耗时 - durationMs: number; - - // 使用的 token 数 - tokensUsed?: { - input: number; - output: number; - }; -} - -/** - * Reviewer 进度回调 - * v5.0: 新增进度反馈,让用户知道 Reviewer 在做什么 - */ -export type ReviewProgressCallback = (step: { - stage: 'checking_git' | 'verifying_files' | 'analyzing_quality' | 'completing'; - message: string; - details?: any; -}) => void; - -/** - * Reviewer 配置 - */ -export interface ReviewerConfig { - // 是否启用(默认 true) - enabled: boolean; - - // 模型选择(默认 haiku) - model: 'haiku' | 'sonnet' | 'opus'; - - // 审查严格程度 - strictness: 'lenient' | 'normal' | 'strict'; - - // 最大重试次数 - maxRetries: number; - - // 超时时间(毫秒) - timeoutMs: number; -} - -const DEFAULT_CONFIG: ReviewerConfig = { - enabled: true, - model: 'opus', // v4.0: Reviewer 和 Queen 必须用 opus(最强推理能力) - strictness: 'normal', - maxRetries: 2, - timeoutMs: 60000, // opus 需要更长时间 -}; - -// ============== 核心实现 ============== - -export class TaskReviewer { - private config: ReviewerConfig; - - constructor(config: Partial = {}) { - this.config = { ...DEFAULT_CONFIG, ...config }; - } - - /** - * 审查 Worker 的工作成果 - * v4.0: 支持全局上下文(Blueprint 信息) - * v5.0: 新增进度回调参数 - */ - async review( - task: SmartTask, - workerSummary: WorkerExecutionSummary, - context?: ReviewContext, - onProgress?: ReviewProgressCallback - ): Promise { - if (!this.config.enabled) { - // 审查被禁用,直接通过 - return { - verdict: 'passed', - confidence: 'low', - reasoning: 'Reviewer 已禁用,自动通过', - durationMs: 0, - }; - } - - const startTime = Date.now(); - - try { - // v5.0: 发送进度 - 开始审查 - onProgress?.({ - stage: 'checking_git', - message: '正在验证 Git 提交状态...', - details: { taskId: task.id }, - }); - - const prompt = this.buildReviewPrompt(task, workerSummary, context); - const result = await this.callReviewer(prompt, context?.projectPath, onProgress); - - // v5.0: 发送进度 - 完成审查 - onProgress?.({ - stage: 'completing', - message: `审查完成: ${result.verdict}`, - details: { verdict: result.verdict, confidence: result.confidence }, - }); - - return { - ...result, - durationMs: Date.now() - startTime, - }; - } catch (error) { - // 根据项目规则:禁止降级方案,直接抛出错误 - console.error('[TaskReviewer] Review failed:', error); - throw new Error(`Reviewer process error: ${error instanceof Error ? error.message : String(error)}`); - } - } - - /** - * v4.0: 构建 Reviewer 的 System Prompt - * v5.0: 优化 - 减少不必要的工具调用,聚焦改动验证 - * v6.0: 强制只返回 JSON,不要任何中间输出 - */ - private buildReviewerSystemPrompt(projectPath?: string): string { - return `你是一个高级任务审查员(Reviewer),负责审查 Worker 的工作成果。 - -## 你的能力 -- 你可以使用 Read、Glob、Grep、Bash 工具来**主动验证** Worker 的工作 -- 你能看到整个项目,可以检查代码是否真的被修改 -- 你是独立的第三方,不受 Worker 报告的影响 - -## 工作目录 -${projectPath || '未指定'} - -## 审查原则(v5.0 优化) -1. **优先验证 Git 提交**:最快最准确的方式是检查 git log 和 git status -2. **聚焦文件改动**:只验证 Worker 报告的改动文件,不要全量扫描 -3. **按需深入**:只在发现问题时才深入检查文件内容 -4. **理解意图**:理解任务的真正目标,而不是死板检查步骤 - -## 审查流程(精简版) -1. **第一步(必须)**:用 Bash 运行 \`git log -1 --oneline\` 验证最新提交 - - 如果有包含 "[Task]" 的新提交 → 继续第 2 步 - - 如果没有新提交 → 用 \`git status\` 检查是否有未提交改动 -2. **第二步(按需)**:如果报告了文件改动,抽查 1-2 个关键文件验证代码质量 - - 优先验证核心业务逻辑文件 - - 不需要验证所有文件 -3. **第三步(必须)**:返回 JSON 格式的审查结果 - -## 特殊情况 -- "无文件变更"不等于"任务失败",可能现有代码已满足要求 -- 如果 Worker 说"已存在,无需修改",验证文件是否确实满足要求 -- 重新执行的任务,检查之前的问题是否已解决 - -## ⚠️ 关键输出要求(v6.0 - 工具调用) -**完成验证后,必须调用 SubmitReview 工具提交审查结果!** -- ✅ 使用 SubmitReview 工具提交结论(100% 可靠的结构化输出) -- ❌ 不要返回 JSON 文本(已废弃,容易解析出错) -- 📝 你可以在调用工具前输出验证过程的文字说明(方便调试)`; - } - - /** - * 构建审查 Prompt - * v4.0: 包含 Blueprint 全局上下文 - */ - private buildReviewPrompt( - task: SmartTask, - summary: WorkerExecutionSummary, - context?: ReviewContext - ): string { - const strictnessGuide = { - lenient: '倾向于通过,只要核心目标达成即可', - normal: '平衡判断,任务目标应该基本完成', - strict: '严格审查,所有要求都必须满足', - }; - - // v4.0: 构建 Blueprint 全局上下文 - const blueprintContext = context?.blueprint ? ` -## 全局上下文(Blueprint - 你的全局视角) - -### 项目信息 -- **蓝图ID**: ${context.blueprint.id} -- **项目名称**: ${context.blueprint.name} -- **项目描述**: ${context.blueprint.description} - -${context.blueprint.requirements?.length ? `### 核心需求 -${context.blueprint.requirements.map((r, i) => `${i + 1}. ${r}`).join('\n')} -` : ''} -${context.blueprint.techStack ? `### 技术栈 -- **语言**: ${context.blueprint.techStack.language} -${context.blueprint.techStack.framework ? `- **框架**: ${context.blueprint.techStack.framework}` : ''} -` : ''} -${context.blueprint.constraints?.length ? `### 约束条件 -${context.blueprint.constraints.map(c => `- ${c}`).join('\n')} -` : ''} -` : ''; - - // v4.0: 相关任务上下文 - const relatedTasksContext = context?.relatedTasks?.length ? ` -### 相关任务状态 -${context.relatedTasks.map(t => `- ${t.name}: ${t.status}`).join('\n')} -` : ''; - - return `# 任务审查请求 - -你是一个独立的任务审查员(Reviewer)。你的职责是审查另一个 AI Agent(Worker)的工作成果,判断任务是否真正完成。 -${blueprintContext} -## 审查原则 -- ${strictnessGuide[this.config.strictness]} -- 理解任务的**意图**,而不是死板地检查每个步骤 -- 考虑上下文:如果是重新执行的任务,"无需修改"可能是正确的结论 -- 关注**结果**,而不是**过程** -- **结合全局上下文判断**:任务是否符合项目整体需求 - -## ⚠️ 环境问题判断规则(严格执行) - -### 核心原则 -**Worker 没有解决不了的问题!** Worker 遇到问题应该: -1. 先尝试自己解决 -2. 自己解决不了的,使用 AskUserQuestion 请求用户帮助 -3. 只有用户明确拒绝帮助时,才能标记为失败 - -### Worker 应该自己解决的问题 -- 缺少 npm/pip 包 → 运行 install 命令 -- 缺少配置文件 → 复制 .env.example 或创建配置 -- 需要构建 → 运行 build 命令 -- Docker 容器未启动 → docker-compose up -d -如果 Worker 没有尝试解决这些问题就放弃 → **needs_revision** - -### Worker 应该请求用户帮助的问题 -- 软件未安装 → 应使用 AskUserQuestion 询问用户 -- 需要 API 密钥 → 应使用 AskUserQuestion 询问用户 -- 需要数据库配置 → 应使用 AskUserQuestion 询问用户 -- 权限不足 → 应使用 AskUserQuestion 询问用户 -如果 Worker 没有请求用户帮助就放弃 → **needs_revision** -如果 Worker 请求了用户帮助,用户拒绝 → 可以 **passed**(在 issues 中注明) - -### 判断标准 -- 模糊的"环境问题"不可接受 → **failed** -- 必须有具体的错误信息和尝试记录 -- 检查 Worker 是否调用了 AskUserQuestion 请求用户帮助 -- 检查 Worker 的工具调用:是否真的运行了 npm install / docker-compose 等 - -## 任务信息 -${relatedTasksContext} - -### 任务描述 -- **ID**: ${task.id} -- **名称**: ${task.name} -- **类型**: ${task.type || 'feature'} -- **详细描述**: -${task.description} - -### 执行上下文 -- **项目路径**: ${context?.projectPath || '未知'} -- **是否重新执行**: ${context?.isRetry ? '是' : '否'} -${context?.previousAttempts ? `- **之前尝试次数**: ${context.previousAttempts}` : ''} - -## Worker 执行报告 - -### Worker 自我汇报 -- **声称完成**: ${summary.selfReported.completed ? '是' : '否'} -${summary.selfReported.message ? `- **汇报信息**: ${summary.selfReported.message}` : ''} - -### 文件变更 (共 ${summary.fileChanges.length} 个) -${this.formatFileChanges(summary.fileChanges)} - -### 测试状态 -${this.formatTestStatus(summary.testStatus)} - -### 执行耗时 -${Math.round(summary.durationMs / 1000)} 秒 - -${summary.error ? `### 错误信息\n${summary.error}` : ''} - -## 你的任务 - -**v5.0 优化:聚焦改动验证,减少不必要的工具调用** - -### 验证步骤(精简版) -1. **【最优先】检查 Git 提交**:用 Bash 运行 \`git log -1 --oneline\` 和 \`git status\` - - 有 "[Task]" 提交 → Worker 已完成并提交,继续验证质量 - - 无新提交但有改动 → **needs_revision**(Worker 写了代码但没提交) - - 无提交也无改动 → 检查现有代码是否已满足要求 -2. **【按需执行】验证改动文件**(仅当报告了文件改动时): - - **重点**:只验证上面"文件变更"列表中的文件 - - 抽查 1-2 个核心文件,用 Read 查看代码质量 - - 不需要验证所有文件,信任 Worker 的基本能力 -3. **【可选】深入检查**(仅当发现明显问题时): - - 用 Grep 搜索特定代码模式 - - 用 Glob 检查是否有遗漏的文件 - -### 判断标准 -- **【最重要】验证 Git 提交**: - 1. \`git log -1\` 显示包含 "[Task]" 的提交消息 → Worker 已完成提交,继续验证代码质量 - 2. \`git status\` 显示有未提交改动 → **needs_revision**(Worker 写了代码但没提交) - 3. 没有代码改动也没有新提交 → 检查现有代码是否满足要求 -- 如果 Worker 说完成了但你验证发现代码不存在 → **failed** -- 如果 Worker 没修改文件但现有代码已满足要求 → **passed** -- 如果代码存在但有明显问题需要修复 → **needs_revision** - -**关于 Git 提交失败**: -Worker 会自己用 Bash 提交 Git。如果提交失败,Worker 应该自己诊断并修复问题(如配置 user.email)。 -如果 Reviewer 发现有未提交的改动,判定 **needs_revision** 并建议 Worker 完成 Git 提交。 - -## ⚠️ 最终输出要求(v6.0 - 工具调用) - -**完成验证后,必须调用 SubmitReview 工具提交审查结果!** - -### 工具调用示例(passed) - -\`\`\` -SubmitReview({ - "verdict": "passed", - "confidence": "high", - "reasoning": "Git 提交已验证,健康检查服务实现正确", - "verified": ["Git 提交状态", "src/services/health.ts 代码质量"], - "issues": [], - "suggestions": [] -}) -\`\`\` - -### 工具调用示例(needs_revision) - -\`\`\` -SubmitReview({ - "verdict": "needs_revision", - "confidence": "high", - "reasoning": "代码已修改但未提交到 Git", - "verified": ["Git 提交状态", "文件改动检查"], - "issues": ["未提交 Git 改动"], - "suggestions": ["运行 git add . && git commit -m '[Task] 完成任务'"] -}) -\`\`\` - -**关键提醒**: -- ✅ 必须调用 SubmitReview 工具提交结论 -- 📝 你可以在调用工具前输出验证过程(如"正在检查 Git 提交...") -- ❌ 不要返回 JSON 文本(已废弃) -- 不要只看 Worker 的报告,必须自己验证 -- "无文件变更"不等于"任务失败",可能现有代码已满足要求`; - } - - /** - * 格式化文件变更 - */ - private formatFileChanges(changes: FileChangeRecord[]): string { - if (changes.length === 0) { - return '(无文件变更)'; - } - - return changes.slice(0, 10).map(change => { - const icon = change.type === 'created' ? '➕' : - change.type === 'modified' ? '📝' : '🗑️'; - return `- ${icon} ${change.path}`; - }).join('\n') + (changes.length > 10 ? `\n... 还有 ${changes.length - 10} 个文件` : ''); - } - - /** - * 格式化测试状态 - */ - private formatTestStatus(status?: WorkerExecutionSummary['testStatus']): string { - if (!status) { - return '(未运行测试)'; - } - if (!status.ran) { - return '未运行测试'; - } - if (status.passed) { - return '✅ 测试通过'; - } - return `❌ 测试失败${status.output ? `: ${status.output.substring(0, 200)}` : ''}`; - } - - /** - * 调用 Reviewer 模型(使用 ConversationLoop,与 Worker 相同的认证方式) - * v4.0: 支持只读工具,让 Reviewer 能主动验证代码 - * v5.0: 优化 - 降低 maxTurns,添加进度回调 - * v6.0: 添加 SubmitReview 工具,使用工具调用而非文本解析 - */ - private async callReviewer( - prompt: string, - projectPath?: string, - onProgress?: ReviewProgressCallback - ): Promise> { - // v4.0: Reviewer 现在拥有只读工具,可以主动验证 Worker 的工作 - // v5.5: 增加 Bash 工具,用于验证 Git 提交状态(git log, git status) - // v6.0: 添加 SubmitReview 工具,用于提交审查结果 - const REVIEWER_READ_ONLY_TOOLS = ['Read', 'Glob', 'Grep', 'LS', 'Bash', 'SubmitReview']; - - // v6.0: 清除之前的审查结果 - SubmitReviewTool.clearReviewResult(); - - // 使用 ConversationLoop,自动处理认证(支持 OAuth 和 API Key) - const loop = new ConversationLoop({ - model: this.config.model as ModelType, - maxTurns: 12, // v5.0: 优化 - 从 20 降低到 12(精简验证步骤后不需要这么多轮次) - verbose: false, - permissionMode: 'bypassPermissions', - workingDir: projectPath, // v4.0: 传递项目路径,让工具知道在哪里读文件 - isSubAgent: true, - systemPrompt: this.buildReviewerSystemPrompt(projectPath), - // 禁用 Extended Thinking,Reviewer 只需要简单的 JSON 输出 - thinking: { enabled: false }, - // v4.0: 允许只读工具,让 Reviewer 能主动验证 - allowedTools: REVIEWER_READ_ONLY_TOOLS, - }); - - let hasSeenBashTool = false; // v5.0: 追踪是否已执行 Git 验证 - let hasSeenReadTool = false; // v5.0: 追踪是否已开始读取文件 - let hasCalledSubmitReview = false; // v6.0: 追踪是否已调用 SubmitReview - - console.log(`[TaskReviewer] Starting model call: ${this.config.model}`); - - // 收集响应 - try { - for await (const event of loop.processMessageStream(prompt)) { - // v5.0: 根据工具调用发送进度反馈 - if (event.type === 'tool_start') { - const toolName = (event as any).toolName; - console.log(`[TaskReviewer] Using tool: ${toolName}`); - - // 发送不同的进度 - if (toolName === 'Bash' && !hasSeenBashTool) { - hasSeenBashTool = true; - onProgress?.({ - stage: 'checking_git', - message: '正在验证 Git 提交和文件状态...', - details: { tool: 'Bash' }, - }); - } else if ((toolName === 'Read' || toolName === 'Glob' || toolName === 'Grep') && !hasSeenReadTool) { - hasSeenReadTool = true; - onProgress?.({ - stage: 'verifying_files', - message: '正在验证文件内容和代码质量...', - details: { tool: toolName }, - }); - } else if (toolName === 'SubmitReview') { - hasCalledSubmitReview = true; - onProgress?.({ - stage: 'analyzing_quality', - message: '正在提交审查结果...', - details: { tool: 'SubmitReview' }, - }); - } - } - } - } catch (streamError) { - console.error('[TaskReviewer] Stream processing error:', streamError); - throw streamError; // 重新抛出,让上层处理 - } - - // v6.0: 从工具调用中读取审查结果 - const toolResult = SubmitReviewTool.getLastReviewResult(); - - if (toolResult) { - console.log(`[TaskReviewer] Retrieved result from SubmitReview tool: ${toolResult.verdict}`); - return { - verdict: toolResult.verdict, - confidence: toolResult.confidence, - reasoning: toolResult.reasoning, - verified: toolResult.verified, - issues: toolResult.issues, - suggestions: toolResult.suggestions, - }; - } - - // 如果没有调用 SubmitReview 工具,直接抛出异常(禁止降级) - console.error('[TaskReviewer] Reviewer did not call SubmitReview tool'); - throw new Error('Reviewer did not call SubmitReview tool, unable to complete review'); - } - - /** - * 解析 Reviewer 的响应 - * v4.1: 查找最后一个 JSON 块(因为 Reviewer 可能在验证过程中输出多段文本) - * v5.0: 当 JSON 解析失败时,使用 AI 重新解析,而不是脆弱的关键词匹配 - * - * @deprecated v6.0: 已废弃,现在使用 SubmitReview 工具调用,不再需要解析文本 - */ - private async parseReviewResponse(text: string): Promise> { - // v4.1: 查找所有 JSON 块,使用最后一个(Reviewer 验证过程中可能输出多段文本) - const jsonMatches = text.match(/```json\s*([\s\S]*?)\s*```/g); - if (jsonMatches && jsonMatches.length > 0) { - // 从最后一个开始尝试解析 - for (let i = jsonMatches.length - 1; i >= 0; i--) { - const match = jsonMatches[i].match(/```json\s*([\s\S]*?)\s*```/); - if (match) { - try { - const parsed = JSON.parse(match[1]); - // 验证必须有 verdict 字段 - if (parsed.verdict) { - console.log(`[TaskReviewer] Parse successful, using JSON block ${i + 1}/${jsonMatches.length}`); - return { - verdict: this.normalizeVerdict(parsed.verdict), - confidence: parsed.confidence || 'medium', - reasoning: parsed.reasoning || '无理由', - verified: parsed.verified, - issues: parsed.issues, - suggestions: parsed.suggestions, - }; - } - } catch (e) { - // 继续尝试上一个 - } - } - } - } - - // 尝试直接解析整个文本为 JSON(没有代码块) - try { - const parsed = JSON.parse(text); - if (parsed.verdict) { - return { - verdict: this.normalizeVerdict(parsed.verdict), - confidence: parsed.confidence || 'medium', - reasoning: parsed.reasoning || '无理由', - verified: parsed.verified, - issues: parsed.issues, - suggestions: parsed.suggestions, - }; - } - } catch (e) { - // 继续尝试 - } - - // v4.1: 尝试从文本中提取裸 JSON 对象(可能没有代码块包裹) - const bareJsonMatch = text.match(/\{[\s\S]*?"verdict"[\s\S]*?\}/); - if (bareJsonMatch) { - try { - const parsed = JSON.parse(bareJsonMatch[0]); - if (parsed.verdict) { - console.log('[TaskReviewer] Parse successful, using bare JSON object'); - return { - verdict: this.normalizeVerdict(parsed.verdict), - confidence: parsed.confidence || 'medium', - reasoning: parsed.reasoning || '无理由', - verified: parsed.verified, - issues: parsed.issues, - suggestions: parsed.suggestions, - }; - } - } catch (e) { - // 继续尝试 - } - } - - // v5.0: 无法解析 JSON 时,使用 AI 重新理解响应内容 - // 不再使用脆弱的关键词匹配(如 includes('passed')),而是让 AI 真正理解文本含义 - console.log('[TaskReviewer] JSON parsing failed, using AI to re-parse response...'); - - try { - const agentDecision = getAgentDecisionMaker(); - // 构造一个虚拟任务用于 AI 解析 - const parseResult = await agentDecision.askAgentForVerdict(text); - - if (parseResult) { - console.log('[TaskReviewer] AI re-parsing successful:', parseResult.verdict); - return { - verdict: parseResult.verdict, - confidence: parseResult.confidence, - reasoning: parseResult.reasoning, - issues: parseResult.issues, - suggestions: parseResult.suggestions, - }; - } - } catch (aiError) { - console.error('[TaskReviewer] AI re-parsing failed:', aiError); - } - - // v5.7: AI 也无法解析时,抛出异常让上层降级为信任 Worker - // 不再返回 needs_revision + "需要人工审核",因为系统设计为全自动化 - throw new Error(`Unable to parse review result, raw response: ${text.substring(0, 200)}`); - } - - /** - * 标准化 verdict - */ - private normalizeVerdict(verdict: string): ReviewVerdict { - const v = verdict?.toLowerCase(); - if (v === 'passed' || v === 'pass' || v === '通过') return 'passed'; - if (v === 'failed' || v === 'fail' || v === '失败') return 'failed'; - return 'needs_revision'; - } -} - -// ============== 辅助函数 ============== - -/** - * 从 Worker 事件流中收集执行摘要 - */ -export function collectWorkerSummary( - events: Array<{ - type: string; - toolName?: string; - toolInput?: any; - toolOutput?: string; - toolError?: string; - }>, - fileChanges: FileChangeRecord[], - durationMs: number, - error?: string -): WorkerExecutionSummary { - const toolCalls: ToolCallRecord[] = []; - let selfReportedCompleted = false; - let selfReportedMessage: string | undefined; - let testRan = false; - let testPassed = false; - let testOutput: string | undefined; - - for (const event of events) { - if (event.type === 'tool_end' && event.toolName) { - toolCalls.push({ - name: event.toolName, - input: event.toolInput, - output: event.toolOutput?.substring(0, 500), - error: event.toolError, - }); - - // 检测自我汇报 - if (event.toolName === 'UpdateTaskStatus') { - const input = event.toolInput as { status?: string; summary?: string; error?: string } | undefined; - if (input?.status === 'completed') { - selfReportedCompleted = true; - selfReportedMessage = input.summary; - } else if (input?.status === 'failed') { - selfReportedMessage = input.error; - } - } - - // 检测测试 - if (event.toolName === 'Bash') { - const input = event.toolInput as { command?: string } | undefined; - const command = input?.command || ''; - if (/\b(npm\s+test|vitest|jest|pytest|go\s+test|cargo\s+test)\b/i.test(command)) { - testRan = true; - testPassed = !event.toolError; - testOutput = event.toolOutput?.substring(0, 500); - } - } - } - } - - return { - selfReported: { - completed: selfReportedCompleted, - message: selfReportedMessage, - }, - toolCalls, - fileChanges, - testStatus: testRan ? { - ran: true, - passed: testPassed, - output: testOutput, - } : undefined, - durationMs, - error, - }; -} diff --git a/src/core/backgroundTasks.ts b/src/core/backgroundTasks.ts deleted file mode 100644 index 5455ef60..00000000 --- a/src/core/backgroundTasks.ts +++ /dev/null @@ -1,313 +0,0 @@ -/** - * 后台对话任务管理器 - * 用于实现 Ctrl+B 将当前对话转到后台运行的功能 - */ - -import { v4 as uuidv4 } from 'uuid'; -import * as fs from 'fs'; -import * as path from 'path'; -import { isBackgroundTasksDisabled } from '../utils/env-check.js'; - -// ============================================================================ -// 类型定义 -// ============================================================================ - -export interface BackgroundConversationTask { - id: string; - type: 'conversation'; - userInput: string; - status: 'running' | 'completed' | 'failed'; - startTime: number; - endTime?: number; - // 输出累积 - textOutput: string; - toolCalls: Array<{ - name: string; - input: unknown; - result?: string; - error?: string; - timestamp: number; - }>; - // 输出文件路径 - outputFile: string; - outputStream?: fs.WriteStream; - // 取消标志 - cancelled: boolean; - // 错误信息 - error?: string; -} - -export interface TaskSummary { - id: string; - type: string; - status: string; - userInput: string; - duration: number; - outputPreview: string; -} - -// ============================================================================ -// 后台任务存储 -// ============================================================================ - -const backgroundConversationTasks = new Map(); - -// 定时清理已完成的后台对话任务,防止 Map 无限增长 -const CONV_CLEANUP_INTERVAL_MS = 10 * 60 * 1000; // 10 分钟 -let _convCleanupTimer: ReturnType | null = null; - -function ensureConvCleanupTimer(): void { - if (_convCleanupTimer) return; - _convCleanupTimer = setInterval(() => { - cleanupCompletedTasks(); - if (backgroundConversationTasks.size === 0 && _convCleanupTimer) { - clearInterval(_convCleanupTimer); - _convCleanupTimer = null; - } - }, CONV_CLEANUP_INTERVAL_MS); - if (_convCleanupTimer && typeof _convCleanupTimer === 'object' && 'unref' in _convCleanupTimer) { - _convCleanupTimer.unref(); - } -} - -// 获取任务输出目录 -function getTasksDir(): string { - const homeDir = process.env.HOME || process.env.USERPROFILE || '/tmp'; - const tasksDir = path.join(homeDir, '.axon', 'tasks', 'conversations'); - - if (!fs.existsSync(tasksDir)) { - fs.mkdirSync(tasksDir, { recursive: true }); - } - - return tasksDir; -} - -// 获取任务输出文件路径 -function getTaskOutputPath(taskId: string): string { - return path.join(getTasksDir(), `${taskId}.log`); -} - -// ============================================================================ -// 后台任务管理 API -// ============================================================================ - -/** - * 创建新的后台对话任务 - */ -export function createBackgroundTask(userInput: string): BackgroundConversationTask | null { - // 检查环境变量:AXON_DISABLE_BACKGROUND_TASKS - if (isBackgroundTasksDisabled()) { - console.log('[BackgroundTasks] Background tasks disabled by environment variable'); - return null; - } - - const taskId = uuidv4(); - const outputFile = getTaskOutputPath(taskId); - const outputStream = fs.createWriteStream(outputFile, { flags: 'w' }); - - const task: BackgroundConversationTask = { - id: taskId, - type: 'conversation', - userInput, - status: 'running', - startTime: Date.now(), - textOutput: '', - toolCalls: [], - outputFile, - outputStream, - cancelled: false, - }; - - backgroundConversationTasks.set(taskId, task); - ensureConvCleanupTimer(); - - // 写入任务开始信息 - outputStream.write(`=== Background Conversation Task Started ===\n`); - outputStream.write(`Task ID: ${taskId}\n`); - outputStream.write(`User Input: ${userInput}\n`); - outputStream.write(`Start Time: ${new Date(task.startTime).toISOString()}\n`); - outputStream.write(`\n`); - - return task; -} - -/** - * 更新任务文本输出 - */ -export function appendTaskText(taskId: string, text: string): void { - const task = backgroundConversationTasks.get(taskId); - if (!task) return; - - task.textOutput += text; - task.outputStream?.write(text); -} - -/** - * 添加工具调用记录 - */ -export function addTaskToolCall( - taskId: string, - toolName: string, - input: unknown, - result?: string, - error?: string -): void { - const task = backgroundConversationTasks.get(taskId); - if (!task) return; - - const toolCall = { - name: toolName, - input, - result, - error, - timestamp: Date.now(), - }; - - task.toolCalls.push(toolCall); - - // 写入工具调用信息到文件 - task.outputStream?.write(`\n--- Tool: ${toolName} ---\n`); - task.outputStream?.write(`Input: ${JSON.stringify(input, null, 2)}\n`); - if (result) { - task.outputStream?.write(`Result: ${result.substring(0, 1000)}${result.length > 1000 ? '...' : ''}\n`); - } - if (error) { - task.outputStream?.write(`Error: ${error}\n`); - } - task.outputStream?.write(`\n`); -} - -/** - * 标记任务完成 - */ -export function completeTask(taskId: string, success: boolean = true, error?: string): void { - const task = backgroundConversationTasks.get(taskId); - if (!task) return; - - task.status = success ? 'completed' : 'failed'; - task.endTime = Date.now(); - task.error = error; - - // 写入任务结束信息 - task.outputStream?.write(`\n=== Task ${success ? 'Completed' : 'Failed'} ===\n`); - task.outputStream?.write(`End Time: ${new Date(task.endTime).toISOString()}\n`); - task.outputStream?.write(`Duration: ${task.endTime - task.startTime}ms\n`); - if (error) { - task.outputStream?.write(`Error: ${error}\n`); - } - - // 关闭输出流 - task.outputStream?.end(); - task.outputStream = undefined; -} - -/** - * 取消任务 - */ -export function cancelTask(taskId: string): boolean { - const task = backgroundConversationTasks.get(taskId); - if (!task) return false; - - task.cancelled = true; - completeTask(taskId, false, 'Task cancelled by user'); - - return true; -} - -/** - * 获取任务 - */ -export function getTask(taskId: string): BackgroundConversationTask | undefined { - return backgroundConversationTasks.get(taskId); -} - -/** - * 获取所有任务 - */ -export function getAllTasks(): BackgroundConversationTask[] { - return Array.from(backgroundConversationTasks.values()); -} - -/** - * 获取任务摘要列表 - */ -export function getTaskSummaries(): TaskSummary[] { - return Array.from(backgroundConversationTasks.values()).map((task) => ({ - id: task.id, - type: task.type, - status: task.status, - userInput: task.userInput.substring(0, 100) + (task.userInput.length > 100 ? '...' : ''), - duration: (task.endTime || Date.now()) - task.startTime, - outputPreview: task.textOutput.substring(0, 200) + (task.textOutput.length > 200 ? '...' : ''), - })); -} - -/** - * 删除任务 - */ -export function deleteTask(taskId: string): boolean { - const task = backgroundConversationTasks.get(taskId); - if (!task) return false; - - // 如果任务还在运行,先取消 - if (task.status === 'running') { - cancelTask(taskId); - } - - // 删除任务 - backgroundConversationTasks.delete(taskId); - - // 可选:删除输出文件 - try { - if (fs.existsSync(task.outputFile)) { - fs.unlinkSync(task.outputFile); - } - } catch (err) { - console.error(`Failed to delete task output file: ${err}`); - } - - return true; -} - -/** - * 清理已完成的任务 - */ -export function cleanupCompletedTasks(): number { - let cleaned = 0; - - Array.from(backgroundConversationTasks.entries()).forEach(([id, task]) => { - if (task.status !== 'running') { - deleteTask(id); - cleaned++; - } - }); - - return cleaned; -} - -/** - * 获取任务数量统计 - */ -export function getTaskStats(): { - total: number; - running: number; - completed: number; - failed: number; -} { - const tasks = Array.from(backgroundConversationTasks.values()); - - return { - total: tasks.length, - running: tasks.filter((t) => t.status === 'running').length, - completed: tasks.filter((t) => t.status === 'completed').length, - failed: tasks.filter((t) => t.status === 'failed').length, - }; -} - -/** - * 检查任务是否被取消 - */ -export function isTaskCancelled(taskId: string): boolean { - const task = backgroundConversationTasks.get(taskId); - return task?.cancelled || false; -} diff --git a/src/debugger/dap-client.ts b/src/debugger/dap-client.ts deleted file mode 100644 index 7af8e40e..00000000 --- a/src/debugger/dap-client.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { spawn, ChildProcess } from 'child_process'; -import { EventEmitter } from 'events'; - -export class DAPClient extends EventEmitter { - private process!: ChildProcess; - private buffer: Buffer = Buffer.alloc(0); - private seq: number = 1; - private pendingRequests: Map = new Map(); - - constructor(private adapterPath: string, private adapterArgs: string[]) { - super(); - } - - async start(): Promise { - this.process = spawn(this.adapterPath, this.adapterArgs, { - stdio: ['pipe', 'pipe', 'pipe'], - }); - this.process.stdout!.on('data', (chunk: Buffer) => this.handleData(chunk)); - } - - private handleData(chunk: Buffer): void { - this.buffer = Buffer.concat([this.buffer, chunk]); - while (true) { - const headerEnd = this.buffer.indexOf('\r\n\r\n'); - if (headerEnd === -1) break; - const header = this.buffer.slice(0, headerEnd).toString(); - const match = header.match(/Content-Length: (\d+)/); - if (!match) break; - const contentLength = parseInt(match[1]); - const messageStart = headerEnd + 4; - if (this.buffer.length < messageStart + contentLength) break; - const messageStr = this.buffer.slice(messageStart, messageStart + contentLength).toString(); - this.buffer = this.buffer.slice(messageStart + contentLength); - try { - const message = JSON.parse(messageStr); - this.handleMessage(message); - } catch (_) { - // 解析失败跳过 - } - } - } - - private handleMessage(message: any): void { - if (message.type === 'response') { - const pending = this.pendingRequests.get(message.request_seq); - if (pending) { - clearTimeout(pending.timer); - this.pendingRequests.delete(message.request_seq); - if (message.success) { - pending.resolve(message.body || {}); - } else { - pending.reject(new Error(message.message || 'DAP request failed')); - } - } - } else if (message.type === 'event') { - this.emit(message.event, message.body); - } - } - - async sendRequest(command: string, args?: any, timeout = 30000): Promise { - const seq = this.seq++; - const message = { seq, type: 'request', command, arguments: args }; - const content = JSON.stringify(message); - const header = `Content-Length: ${Buffer.byteLength(content)}\r\n\r\n`; - this.process.stdin!.write(header + content); - - return new Promise((resolve, reject) => { - const timer = setTimeout(() => { - this.pendingRequests.delete(seq); - reject(new Error(`DAP request ${command} timed out`)); - }, timeout); - this.pendingRequests.set(seq, { resolve, reject, timer }); - }); - } - - async initialize(): Promise { - return this.sendRequest('initialize', { - clientID: 'claude-code', - adapterID: 'generic', - pathFormat: 'path', - linesStartAt1: true, - columnsStartAt1: true, - supportsVariableType: true, - supportsRunInTerminalRequest: false, - }); - } - - async launch(args: any): Promise { await this.sendRequest('launch', args); } - async attach(args: any): Promise { await this.sendRequest('attach', args); } - async setBreakpoints(args: any): Promise { return this.sendRequest('setBreakpoints', args); } - async configurationDone(): Promise { await this.sendRequest('configurationDone'); } - async continue(args: any): Promise { return this.sendRequest('continue', args); } - async next(args: any): Promise { return this.sendRequest('next', args); } - async stepIn(args: any): Promise { return this.sendRequest('stepIn', args); } - async stepOut(args: any): Promise { return this.sendRequest('stepOut', args); } - async pause(args: any): Promise { return this.sendRequest('pause', args); } - async threads(): Promise { return this.sendRequest('threads'); } - async stackTrace(args: any): Promise { return this.sendRequest('stackTrace', args); } - async scopes(args: any): Promise { return this.sendRequest('scopes', args); } - async variables(args: any): Promise { return this.sendRequest('variables', args); } - async evaluate(args: any): Promise { return this.sendRequest('evaluate', args); } - async disconnect(args?: any): Promise { await this.sendRequest('disconnect', args || { terminateDebuggee: true }); } - - stop(): void { - this.process.kill(); - } -} diff --git a/src/deploy/railway-adapter.test.ts b/src/deploy/railway-adapter.test.ts index 72f77df8..2fd2c129 100644 --- a/src/deploy/railway-adapter.test.ts +++ b/src/deploy/railway-adapter.test.ts @@ -69,14 +69,43 @@ describe('M15 RailwayDeployAdapter cloud 守卫 + 入参校验', () => { }); }); -describe('M15 RailwayDeployAdapter token 权限', () => { - it('team/project token(me{} Not Authorized)→ 可操作错误(提示换账号 token 或配 WORKSPACE_ID)', async () => { - // 真探针实测(2026-05-31):team token 在 resolveWorkspaceId 的 me{} 查询上 Not Authorized - vi.stubGlobal('fetch', mockFetchSeq([{ errors: [{ message: 'Not Authorized' }] }])); +describe('M15 RailwayDeployAdapter token 权限(workspace token 自适应)', () => { + // 真探针实测(2026-05-31/06-01):workspace token 在 me{} Not Authorized,但可查顶层 projects。 + it('workspace token(me{} 无权)→ 自动 fallback 用 projects 反查 workspaceId → 部署成功', async () => { + const fetchMock = mockFetchSeq([ + { errors: [{ message: 'Not Authorized' }] }, // me{} 无权 + { data: { projects: { edges: [{ node: { id: 'prj_existing' } }] } } }, // fallback: 列顶层 projects + { data: { project: { workspace: { id: 'ws_discovered' } } } }, // 反查 project.workspace.id + { data: { projectCreate: { id: 'prj_new', environments: { edges: [{ node: { id: 'env_prod', name: 'production' } }] } } } }, + { data: { serviceCreate: { id: 'svc_1' } } }, + { data: { variableUpsert: true } }, + { data: { serviceDomainCreate: { domain: 'web.up.railway.app' } } }, + ]); + vi.stubGlobal('fetch', fetchMock); + queueUpResults([{ stdout: 'ok' }]); + const saved = process.env.RAILWAY_WORKSPACE_ID; + delete process.env.RAILWAY_WORKSPACE_ID; // 确保走 me{} → fallback 解析路径 + try { + const a = new RailwayDeployAdapter({ token: 'workspace-tok' }); // 不传 workspaceId + const r = await a.deploy({ name: 'site', sourceDir: '/tmp/site' }); + expect(r.url).toBe('https://web.up.railway.app'); + // projectCreate(第 4 次 fetch)用的是反查出的 ws_discovered + const createBody = JSON.parse((fetchMock.mock.calls[3][1] as { body: string }).body); + expect(createBody.variables.ws).toBe('ws_discovered'); + } finally { + if (saved !== undefined) process.env.RAILWAY_WORKSPACE_ID = saved; + } + }); + + it('workspace token 无权 + 零现有项目(projects 空)→ 才抛可操作错误', async () => { + vi.stubGlobal('fetch', mockFetchSeq([ + { errors: [{ message: 'Not Authorized' }] }, // me{} 无权 + { data: { projects: { edges: [] } } }, // 无项目可反查 → 返 null + ])); const saved = process.env.RAILWAY_WORKSPACE_ID; - delete process.env.RAILWAY_WORKSPACE_ID; // 确保走 me{} 解析路径 + delete process.env.RAILWAY_WORKSPACE_ID; try { - const a = new RailwayDeployAdapter({ token: 'team-tok' }); // 不传 workspaceId + const a = new RailwayDeployAdapter({ token: 'workspace-tok' }); await expect(a.deploy({ name: 'site', sourceDir: '/tmp/site' })).rejects.toThrow(/账号 token|RAILWAY_WORKSPACE_ID/); } finally { if (saved !== undefined) process.env.RAILWAY_WORKSPACE_ID = saved; diff --git a/src/deploy/railway-adapter.ts b/src/deploy/railway-adapter.ts index f39bc84f..25887d9e 100644 --- a/src/deploy/railway-adapter.ts +++ b/src/deploy/railway-adapter.ts @@ -95,17 +95,22 @@ export class RailwayDeployAdapter implements DeployAdapter { if (this.workspaceId) return this.workspaceId; const envWs = process.env.RAILWAY_WORKSPACE_ID; if (envWs) { this.workspaceId = envWs; return envWs; } - // me{} 查询需 user-scoped(账号/个人)token。team/project token 会 "Not Authorized"(2026-05-31 真探针实测) - // → 给可操作错误:换账号 token,或配 RAILWAY_WORKSPACE_ID 跳过 me 查询(team token 直接建 project)。 + // me{} 查询需 user-scoped(账号/个人)token。team/workspace token 会 "Not Authorized"。 + // 2026-06-01 真全栈 e2e 暴露此坑:.env 的 RAILWAY_API_TOKEN 就是 workspace token,me{} Not Authorized + // 导致全栈 publish 生产路径直接撞墙。治本(铁律3 不靠用户手配):me 无权时**自动 fallback** 用顶层 + // projects 查询反查 workspaceId(workspace token 实测可查 projects 但不可查 me)。两类 token 零配置自适应。 let d: Record; try { d = await this.gql(token, 'query { me { workspaces { id name } } }'); } catch (e) { if (/Not Authorized/i.test((e as Error).message)) { + const discovered = await this.discoverWorkspaceViaProjects(token); + if (discovered) { this.workspaceId = discovered; return discovered; } + // me 无权且无现有项目可反查 → 给可操作错误(这是唯一仍需手配的窄场景)。 throw new RailwayDeployError( - 'Railway token 无账号权限(me 查询 Not Authorized)——多半是 team/project token 而非账号 token。' + 'Railway token 无账号权限(me 查询 Not Authorized)且无现有项目可反查 workspace。' + '解法二选一:① 用 railway.app/account/tokens 建「账号 token」放 RAILWAY_API_TOKEN;' - + '② 若坚持用 team token,配 RAILWAY_WORKSPACE_ID=<团队/工作区 id> 跳过 me 查询。', + + '② 配 RAILWAY_WORKSPACE_ID=<工作区 id> 跳过自动解析。', ); } throw e; @@ -116,6 +121,23 @@ export class RailwayDeployAdapter implements DeployAdapter { return ws; } + /** + * workspace/team token 对 me{} 无权时的 fallback:列顶层 projects 取任一个,反查其 workspace.id。 + * (2026-06-01 真探针实测:workspace token 可查 `projects` 顶层字段但 `me` Not Authorized。) + * 返回 workspaceId;零现有项目 / 查询失败时返 null(让上层报「需手配 RAILWAY_WORKSPACE_ID」)。 + */ + private async discoverWorkspaceViaProjects(token: string): Promise { + try { + const pl = await this.gql(token, 'query { projects { edges { node { id } } } }'); + const pid = ((pl.projects as { edges?: Array<{ node: { id: string } }> })?.edges ?? [])[0]?.node?.id; + if (!pid) return null; + const pd = await this.gql(token, 'query($id:String!){ project(id:$id){ workspace { id } } }', { id: pid }); + return (pd.project as { workspace?: { id: string } })?.workspace?.id ?? null; + } catch { + return null; // 反查失败不掩盖主流程:返 null 让上层给可操作错误 + } + } + /** 建 project → projectId + 默认 production environmentId。 */ private async createProject(token: string, name: string, workspaceId: string): Promise<{ projectId: string; environmentId: string }> { const d = await this.gql( diff --git a/src/deploy/types.ts b/src/deploy/types.ts index b0eee47a..761fbce1 100644 --- a/src/deploy/types.ts +++ b/src/deploy/types.ts @@ -68,4 +68,10 @@ export interface DeployAdapter { deploymentId: string, opts?: { timeoutMs?: number; pollMs?: number }, ): Promise; + /** + * 可选:关部署保护让 URL 公开可分享(真人交付最后一公里)。仅 Vercel 有此概念(默认带 + * ssoProtection/passwordProtection);Railway 无(源码部署默认公开)故不实现。publish 流程在 + * provider==='vercel' 时调 `adapter.makePublic?.(name)`。 + */ + makePublic?(projectIdOrName: string): Promise; } diff --git a/src/deploy/vercel-adapter.test.ts b/src/deploy/vercel-adapter.test.ts index a5aa1472..c1e70b6e 100644 --- a/src/deploy/vercel-adapter.test.ts +++ b/src/deploy/vercel-adapter.test.ts @@ -77,6 +77,29 @@ describe('VercelDeployAdapter(mock fetch,不真部署)', () => { expect(s.state).toBe('error'); }); + it('makePublic PATCH /v9/projects/{name} 置 ssoProtection+passwordProtection=null(关部署保护)', async () => { + const fetchMock = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ id: 'prj_1' }) }); + vi.stubGlobal('fetch', fetchMock); + + const a = new VercelDeployAdapter({ token: 't', teamId: 'team_x' }); + await a.makePublic('axon-poc'); + + const [url, init] = fetchMock.mock.calls[0]; + expect(url).toContain('/v9/projects/axon-poc'); + expect(url).toContain('teamId=team_x'); + expect((init as any).method).toBe('PATCH'); + const body = JSON.parse((init as any).body); + expect(body).toEqual({ ssoProtection: null, passwordProtection: null }); + }); + + it('makePublic HTTP 错误原样抛出(铁律6 不掩盖)', async () => { + vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ + ok: false, status: 403, json: async () => ({ error: { message: 'insufficient scope' } }), + })); + const a = new VercelDeployAdapter({ token: 't' }); + await expect(a.makePublic('x')).rejects.toThrow(/403.*insufficient scope/); + }); + it('community edition → requireCloud 抛错(部署是 cloud 能力)', async () => { __resetEditionCacheForTest(); process.env.AXON_EDITION = 'community'; diff --git a/src/deploy/vercel-adapter.ts b/src/deploy/vercel-adapter.ts index 148ceae8..5bd00db6 100644 --- a/src/deploy/vercel-adapter.ts +++ b/src/deploy/vercel-adapter.ts @@ -111,6 +111,38 @@ export class VercelDeployAdapter implements DeployAdapter { }; } + /** + * 关 Vercel 部署保护 —— 真人交付「最后一公里」(2026-06-01 从 super-agent vercel-client.ts 移植)。 + * + * 问题:Vercel 项目默认带 deployment protection(ssoProtection / passwordProtection)→ 生产 URL 返回 + * 401 需登录 = **不可公开分享**,真人交付(拿到能发给别人的链接)就断在这。 + * 解:PATCH /v9/projects/{idOrName} 把两者置 null → URL 公开可访问。Vercel path 接受 project name 或 id; + * axon deploy 走 inline files 会自动建名为 opts.name 的项目,故可直接用 name 作标识。 + * + * ★这是「安全弱化」动作(把需鉴权的部署变公开)★:调用方(publish 流程)应在用户**明确要公开分享** + * 的语境下调(发布即分享是 publish 的本意)。community edition 由 ensureCreds 的 requireCloud 物理拦。 + */ + async makePublic(projectIdOrName: string): Promise { + const { token, teamId } = await this.ensureCreds(); + const res = await fetch( + `${VERCEL_API}/v9/projects/${encodeURIComponent(projectIdOrName)}${this.teamQuery(teamId)}`, + { + method: 'PATCH', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ ssoProtection: null, passwordProtection: null }), + }, + ); + if (!res.ok) { + const data: any = await res.json().catch(() => ({})); + throw new Error( + `关 Vercel 部署保护失败 HTTP ${res.status}: ${data?.error?.message ?? JSON.stringify(data)}`, + ); + } + } + async waitUntilReady( deploymentId: string, opts: { timeoutMs?: number; pollMs?: number } = {}, diff --git a/src/hooks/blueprint-hooks.ts b/src/hooks/blueprint-hooks.ts deleted file mode 100644 index bc03a345..00000000 --- a/src/hooks/blueprint-hooks.ts +++ /dev/null @@ -1,94 +0,0 @@ -/** - * 蓝图相关 Hooks(已简化) - * - * 新蜂群架构 v2.0 使用 SmartPlanner 进行需求规划, - * 不再需要复杂的边界检查和自动测试 hooks。 - * - * 保留此文件以保持向后兼容性,导出空函数。 - */ - -// ============================================================================ -// PreToolUse Hook:边界检查(已废弃) -// ============================================================================ - -/** - * PreToolUse 边界检查(已废弃) - * - * 新架构中,边界检查由 SmartPlanner 在任务规划阶段处理, - * 不再需要工具层面的运行时检查。 - * - * @deprecated 使用 SmartPlanner 进行需求规划 - */ -export async function preToolUseBoundaryCheck( - _toolName: string, - _toolInput: Record -): Promise<{ allowed: boolean; message?: string }> { - // 新架构不再需要边界检查,始终允许 - return { allowed: true }; -} - -// ============================================================================ -// PostToolUse Hook:自动测试(已废弃) -// ============================================================================ - -/** - * PostToolUse 自动测试(已废弃) - * - * 新架构中,测试由 AutonomousWorker 自主决策, - * 不再需要 hook 层面的自动测试。 - * - * @deprecated 使用 AutonomousWorker 的自主测试能力 - */ -export async function postToolUseTestRunner( - _toolName: string, - _toolInput: Record, - _toolResult: any -): Promise { - // 新架构不再需要自动测试 hook -} - -// ============================================================================ -// 注册 Hooks(已废弃) -// ============================================================================ - -/** - * 注册蓝图相关的 hooks(已废弃) - * - * @deprecated 新架构不再需要蓝图 hooks - */ -export function registerBlueprintHooks(): void { - // 新架构不再需要注册蓝图 hooks - // 保留空函数以保持向后兼容 -} - -// ============================================================================ -// 辅助函数(已简化) -// ============================================================================ - -/** - * 检查是否应该运行蓝图 hooks(已废弃) - * - * @deprecated 新架构不再需要此检查 - */ -export function shouldRunBlueprintHooks(): boolean { - // 新架构不再需要蓝图 hooks - return false; -} - -/** - * 获取蓝图边界检查状态(已废弃) - * - * @deprecated 使用 SmartPlanner 获取规划状态 - */ -export function getBoundaryCheckStatus(): { - enabled: boolean; - blueprintId?: string; - activeTaskCount: number; -} { - // 返回默认禁用状态 - return { - enabled: false, - blueprintId: undefined, - activeTaskCount: 0, - }; -} diff --git a/src/knowledge/curator.ts b/src/knowledge/curator.ts deleted file mode 100644 index a310e332..00000000 --- a/src/knowledge/curator.ts +++ /dev/null @@ -1,99 +0,0 @@ -/** - * Context Curator — AI 驱动的知识策展 - * 用主会话模型对候选知识做智能过滤与浓缩,替代盲目注入 - */ - -import { estimateTokens } from '../utils/token-estimate.js'; - -export interface CuratorInput { - /** 最近几轮对话的文本(用于理解当前话题) */ - conversationContext: string; - /** 候选知识库文章 */ - knowledgeArticles: Array<{ title: string; content: string; pinned?: boolean }>; -} - -export interface CuratorOutput { - /** 浓缩简报(null = 全部无关,不注入) */ - curated: string | null; - /** 简报是否包含知识内容 */ - fromKnowledge: boolean; -} - -const CURATOR_SYSTEM = `你是一个上下文策展助手。你的任务是从候选知识库文章中,筛选出与当前对话直接相关的信息,并浓缩为简洁的参考简报。 - -规则: -- 只保留与当前对话主题直接相关的内容 -- 完全无关的内容直接丢弃,不要提及 -- 保留关键事实、数据、决策,删除冗余描述 -- Pinned 知识文章视为用户认为重要的,倾向保留 -- 输出格式:直接输出简报文本,无需解释你的筛选过程 -- 如果所有候选内容都与对话无关,只输出 "NONE" -- 简报控制在 500 字以内`; - -/** - * 将候选内容组装为 Curator 的用户 prompt - */ -function buildCuratorPrompt(input: CuratorInput): string { - const parts: string[] = []; - - parts.push(`## 当前对话上下文\n${input.conversationContext}`); - - parts.push('## 候选知识库文章'); - for (const a of input.knowledgeArticles) { - const pin = a.pinned ? ' [pinned]' : ''; - // 截断过长的文章内容,只给 Curator 摘要 - const content = estimateTokens(a.content) > 800 - ? a.content.substring(0, 2400) + '\n...(truncated)' - : a.content; - parts.push(`### ${a.title}${pin}\n${content}`); - } - - parts.push('请根据当前对话上下文,从上述候选内容中筛选相关信息,输出浓缩简报。如果全部无关,只输出 NONE。'); - - return parts.join('\n\n'); -} - -/** - * 策展上下文:调用 LLM 对候选知识做智能筛选与浓缩 - * - * @param input 候选内容 - * @param llmCall LLM 调用函数(由调用方注入,复用主会话客户端) - * @param options 超时等配置 - */ -export async function curateContext( - input: CuratorInput, - llmCall: (systemPrompt: string, userPrompt: string) => Promise, - options?: { timeout?: number }, -): Promise { - const empty: CuratorOutput = { curated: null, fromKnowledge: false }; - - // 无候选内容,跳过 - if (input.knowledgeArticles.length === 0) { - return empty; - } - - const userPrompt = buildCuratorPrompt(input); - const timeout = options?.timeout ?? 5000; - - try { - // 带超时的 LLM 调用 - const result = await Promise.race([ - llmCall(CURATOR_SYSTEM, userPrompt), - new Promise((_, reject) => - setTimeout(() => reject(new Error('curator_timeout')), timeout), - ), - ]); - - if (!result || result.trim() === 'NONE' || result.trim() === '') { - return empty; - } - - return { - curated: result.trim(), - fromKnowledge: input.knowledgeArticles.length > 0, - }; - } catch { - // 超时或任何错误 → 返回 null,让调用方降级到直接注入 - return empty; - } -} diff --git a/src/mcp/auto-discovery.ts b/src/mcp/auto-discovery.ts deleted file mode 100644 index 2c9f4ae5..00000000 --- a/src/mcp/auto-discovery.ts +++ /dev/null @@ -1,1352 +0,0 @@ -/** - * MCP 自动发现和连接管理 - * - * 功能: - * 1. 从配置文件自动加载 MCP 服务器定义 - * 2. 支持三种传输: stdio, sse, http - * 3. 自动连接和重连机制 - * 4. MCP 工具自动注册到工具系统 - * 5. MCP 资源访问支持 - * 6. 连接失败优雅处理 - */ - -import { EventEmitter } from 'events'; -import * as fs from 'fs'; -import * as path from 'path'; -import { homedir } from 'os'; -import { spawn, ChildProcess } from 'child_process'; -import axios, { AxiosInstance } from 'axios'; -import { v4 as uuidv4 } from 'uuid'; -import { VERSION } from '../version.js'; - -// ============ 类型定义 ============ - -/** - * MCP 服务器配置 (支持多种格式) - */ -export interface McpServerDefinition { - /** 传输类型 */ - type?: 'stdio' | 'sse' | 'http'; - - /** stdio 传输的命令 */ - command?: string; - - /** 命令参数 */ - args?: string[]; - - /** 环境变量 */ - env?: Record; - - /** HTTP/SSE 传输的 URL */ - url?: string; - - /** 传输类型 (兼容格式) */ - transport?: 'stdio' | 'sse' | 'http'; - - /** HTTP 请求头 */ - headers?: Record; - - /** 是否启用 */ - enabled?: boolean; - - /** 超时时间 (毫秒) */ - timeout?: number; - - /** 重试次数 */ - retries?: number; - - /** 自动重连 */ - autoReconnect?: boolean; -} - -/** - * MCP 配置格式 (settings.json) - */ -export interface McpConfigFile { - mcpServers?: Record; -} - -/** - * 服务器状态 - */ -export type ServerStatus = - | 'disconnected' - | 'connecting' - | 'connected' - | 'error' - | 'reconnecting'; - -/** - * MCP 工具定义 - */ -export interface McpToolInfo { - name: string; - description?: string; - inputSchema: Record; -} - -/** - * MCP 资源定义 - */ -export interface McpResourceInfo { - uri: string; - name: string; - description?: string; - mimeType?: string; -} - -/** - * MCP 提示定义 - */ -export interface McpPromptInfo { - name: string; - description?: string; - arguments?: Array<{ - name: string; - description?: string; - required?: boolean; - }>; -} - -/** - * 服务器能力 - */ -export interface ServerCapabilities { - tools?: boolean; - resources?: boolean; - prompts?: boolean; - sampling?: boolean; -} - -/** - * 连接的服务器信息 - */ -export interface ConnectedServer { - name: string; - config: McpServerDefinition; - status: ServerStatus; - capabilities: ServerCapabilities; - tools: McpToolInfo[]; - resources: McpResourceInfo[]; - prompts: McpPromptInfo[]; - error?: string; - lastConnectTime?: number; - reconnectAttempts: number; - transport?: McpTransport; -} - -/** - * MCP 消息格式 - */ -export interface McpJsonRpcMessage { - jsonrpc: '2.0'; - id?: number | string; - method?: string; - params?: unknown; - result?: unknown; - error?: { - code: number; - message: string; - data?: unknown; - }; -} - -/** - * 自动发现选项 - */ -export interface AutoDiscoveryOptions { - /** 配置文件路径 (默认: ~/.axon/settings.json) */ - configPaths?: string[]; - - /** 启动时自动连接 */ - autoConnect?: boolean; - - /** 自动重连 */ - autoReconnect?: boolean; - - /** 连接超时 (毫秒) */ - connectionTimeout?: number; - - /** 最大重连次数 */ - maxReconnectAttempts?: number; - - /** 重连延迟基数 (毫秒) */ - reconnectDelayBase?: number; - - /** 健康检查间隔 (毫秒) */ - healthCheckInterval?: number; -} - -// ============ 传输层抽象 ============ - -/** - * MCP 传输接口 - */ -export interface McpTransport { - connect(): Promise; - disconnect(): Promise; - send(message: McpJsonRpcMessage): Promise; - isConnected(): boolean; - on(event: string, handler: (...args: any[]) => void): void; - off(event: string, handler: (...args: any[]) => void): void; -} - -/** - * Stdio 传输实现 - */ -class StdioTransport extends EventEmitter implements McpTransport { - private command: string; - private args: string[]; - private env: Record; - private process?: ChildProcess; - private buffer: string = ''; - private connected: boolean = false; - - constructor(command: string, args: string[] = [], env: Record = {}) { - super(); - this.command = command; - this.args = args; - this.env = { ...process.env as Record, ...env }; - } - - async connect(): Promise { - if (this.connected) return; - - return new Promise((resolve, reject) => { - try { - this.process = spawn(this.command, this.args, { - env: this.env, - stdio: ['pipe', 'pipe', 'pipe'], - shell: process.platform === 'win32', - }); - - this.process.on('error', (err) => { - this.connected = false; - this.emit('error', err); - reject(err); - }); - - this.process.on('exit', (code, signal) => { - this.connected = false; - this.emit('disconnect', { code, signal }); - }); - - this.process.stdout?.on('data', (data: Buffer) => { - this.buffer += data.toString(); - this.processBuffer(); - }); - - this.process.stderr?.on('data', (data: Buffer) => { - this.emit('stderr', data.toString()); - }); - - // 等待进程启动 - setTimeout(() => { - if (this.process && !this.process.killed) { - this.connected = true; - this.emit('connect'); - resolve(); - } - }, 100); - } catch (err) { - reject(err); - } - }); - } - - private processBuffer(): void { - const lines = this.buffer.split('\n'); - this.buffer = lines.pop() || ''; - - for (const line of lines) { - if (line.trim()) { - try { - const message: McpJsonRpcMessage = JSON.parse(line); - this.emit('message', message); - } catch (err) { - this.emit('parse-error', err, line); - } - } - } - } - - async send(message: McpJsonRpcMessage): Promise { - if (!this.process?.stdin) { - throw new Error('Transport not connected'); - } - - return new Promise((resolve, reject) => { - try { - const data = JSON.stringify(message) + '\n'; - this.process!.stdin!.write(data, (err) => { - if (err) reject(err); - else resolve(); - }); - } catch (err) { - reject(err); - } - }); - } - - async disconnect(): Promise { - if (this.process) { - this.process.kill(); - this.process = undefined; - } - this.connected = false; - this.emit('disconnect', { code: 0, signal: null }); - } - - isConnected(): boolean { - return this.connected && !!this.process && !this.process.killed; - } -} - -/** - * SSE 传输实现 - */ -class SseTransport extends EventEmitter implements McpTransport { - private url: string; - private headers: Record; - private httpClient: AxiosInstance; - private connected: boolean = false; - private pollingInterval?: NodeJS.Timeout; - private sessionId: string; - - constructor(url: string, headers: Record = {}) { - super(); - this.url = url; - this.headers = headers; - this.sessionId = uuidv4(); - this.httpClient = axios.create({ - baseURL: url, - headers: { - ...headers, - 'Accept': 'text/event-stream', - 'X-Session-ID': this.sessionId, - }, - timeout: 30000, - }); - } - - async connect(): Promise { - if (this.connected) return; - - try { - // 测试连接 - await this.httpClient.get('/health').catch(() => { - // 健康检查可能不存在,忽略 - }); - - this.connected = true; - this.startEventPolling(); - this.emit('connect'); - } catch (err) { - throw new Error(`Failed to connect to SSE server: ${err}`); - } - } - - private startEventPolling(): void { - // SSE 轮询模式 (Node.js 不原生支持 EventSource) - // 实际生产中应使用 eventsource 库 - this.pollingInterval = setInterval(async () => { - if (!this.connected) return; - - try { - const response = await this.httpClient.get('/events', { - headers: { - 'Accept': 'text/event-stream', - }, - responseType: 'text', - }); - - if (response.data) { - // 解析 SSE 数据 - const events = this.parseSSEData(response.data); - for (const event of events) { - if (event.data) { - try { - const message = JSON.parse(event.data); - this.emit('message', message); - } catch { - // 忽略解析错误 - } - } - } - } - } catch (err) { - // 静默处理轮询错误 - } - }, 1000); - } - - private parseSSEData(data: string): Array<{ event?: string; data?: string }> { - const events: Array<{ event?: string; data?: string }> = []; - const lines = data.split('\n'); - let currentEvent: { event?: string; data?: string } = {}; - - for (const line of lines) { - if (line.startsWith('event:')) { - currentEvent.event = line.slice(6).trim(); - } else if (line.startsWith('data:')) { - currentEvent.data = line.slice(5).trim(); - } else if (line === '' && currentEvent.data) { - events.push(currentEvent); - currentEvent = {}; - } - } - - return events; - } - - async send(message: McpJsonRpcMessage): Promise { - if (!this.connected) { - throw new Error('Transport not connected'); - } - - try { - const response = await this.httpClient.post('/messages', message, { - headers: { - 'Content-Type': 'application/json', - }, - }); - - if (response.data) { - this.emit('message', response.data); - } - } catch (err) { - throw new Error(`Failed to send SSE message: ${err}`); - } - } - - async disconnect(): Promise { - if (this.pollingInterval) { - clearInterval(this.pollingInterval); - this.pollingInterval = undefined; - } - this.connected = false; - this.emit('disconnect', { code: 0, signal: null }); - } - - isConnected(): boolean { - return this.connected; - } -} - -/** - * HTTP 传输实现 - */ -class HttpTransport extends EventEmitter implements McpTransport { - private url: string; - private headers: Record; - private httpClient: AxiosInstance; - private connected: boolean = false; - - constructor(url: string, headers: Record = {}) { - super(); - this.url = url; - this.headers = headers; - this.httpClient = axios.create({ - baseURL: url, - headers: { - ...headers, - 'Content-Type': 'application/json', - }, - timeout: 30000, - }); - } - - async connect(): Promise { - if (this.connected) return; - - try { - // 测试连接 - await this.httpClient.get('/health').catch(() => { - // 健康检查可能不存在 - }); - - this.connected = true; - this.emit('connect'); - } catch (err) { - throw new Error(`Failed to connect to HTTP server: ${err}`); - } - } - - async send(message: McpJsonRpcMessage): Promise { - if (!this.connected) { - throw new Error('Transport not connected'); - } - - try { - const response = await this.httpClient.post('/rpc', message); - if (response.data) { - this.emit('message', response.data); - } - } catch (err) { - throw new Error(`Failed to send HTTP message: ${err}`); - } - } - - async disconnect(): Promise { - this.connected = false; - this.emit('disconnect', { code: 0, signal: null }); - } - - isConnected(): boolean { - return this.connected; - } -} - -// ============ 默认配置 ============ - -const DEFAULT_OPTIONS: Required = { - configPaths: [ - path.join(homedir(), '.axon', 'settings.json'), - path.join(process.cwd(), '.axon', 'settings.json'), - ], - autoConnect: true, - autoReconnect: true, - connectionTimeout: 30000, - maxReconnectAttempts: 3, - reconnectDelayBase: 1000, - healthCheckInterval: 30000, -}; - -// ============ MCP 自动发现类 ============ - -/** - * MCP 自动发现和连接管理器 - * - * 使用示例: - * ```typescript - * const discovery = new McpAutoDiscovery(); - * await discovery.initialize(); - * - * // 获取所有已连接的服务器 - * const servers = discovery.getConnectedServers(); - * - * // 调用工具 - * const result = await discovery.callTool('my-server', 'toolName', { arg1: 'value' }); - * ``` - */ -export class McpAutoDiscovery extends EventEmitter { - private options: Required; - private servers: Map = new Map(); - private pendingRequests: Map void; - reject: (error: Error) => void; - timeout: NodeJS.Timeout; - }> = new Map(); - private messageId: number = 1; - private healthCheckTimers: Map = new Map(); - private initialized: boolean = false; - - constructor(options?: AutoDiscoveryOptions) { - super(); - this.options = { ...DEFAULT_OPTIONS, ...options }; - - // Windows 路径处理 - if (process.platform === 'win32') { - const userProfile = process.env.USERPROFILE || homedir(); - this.options.configPaths = [ - path.join(userProfile, '.axon', 'settings.json'), - path.join(process.cwd(), '.axon', 'settings.json'), - ]; - } - } - - // ============ 初始化 ============ - - /** - * 初始化自动发现系统 - */ - async initialize(): Promise { - if (this.initialized) { - return; - } - - // 1. 加载配置 - const configs = await this.loadConfigs(); - - // 2. 注册服务器 - for (const [name, config] of Object.entries(configs)) { - this.registerServer(name, config); - } - - // 3. 自动连接 - if (this.options.autoConnect) { - await this.connectAll(); - } - - this.initialized = true; - this.emit('initialized', { serverCount: this.servers.size }); - } - - /** - * 加载配置文件 - */ - private async loadConfigs(): Promise> { - const allConfigs: Record = {}; - - for (const configPath of this.options.configPaths) { - try { - if (!fs.existsSync(configPath)) { - continue; - } - - const content = fs.readFileSync(configPath, 'utf-8'); - const config: McpConfigFile = JSON.parse(content); - - if (config.mcpServers) { - for (const [name, serverConfig] of Object.entries(config.mcpServers)) { - // 后面的配置覆盖前面的 - allConfigs[name] = this.normalizeConfig(serverConfig); - } - } - } catch (err) { - console.warn(`Failed to load MCP config from ${configPath}:`, err); - this.emit('config:error', { path: configPath, error: err }); - } - } - - return allConfigs; - } - - /** - * 规范化配置 - */ - private normalizeConfig(config: McpServerDefinition): McpServerDefinition { - // 确定传输类型 - let type: 'stdio' | 'sse' | 'http' = config.type || config.transport || 'stdio'; - - // 如果有 URL 但没有指定类型,推断为 http 或 sse - if (!config.type && !config.transport && config.url) { - type = config.transport === 'sse' ? 'sse' : 'http'; - } - - return { - type, - command: config.command, - args: config.args || [], - env: config.env || {}, - url: config.url, - headers: config.headers || {}, - enabled: config.enabled !== false, - timeout: config.timeout || this.options.connectionTimeout, - retries: config.retries || this.options.maxReconnectAttempts, - autoReconnect: config.autoReconnect ?? this.options.autoReconnect, - }; - } - - // ============ 服务器管理 ============ - - /** - * 注册服务器 - */ - registerServer(name: string, config: McpServerDefinition): void { - const normalizedConfig = this.normalizeConfig(config); - - // 检查是否已启用 - if (normalizedConfig.enabled === false) { - this.emit('server:skipped', { name, reason: 'disabled' }); - return; - } - - const server: ConnectedServer = { - name, - config: normalizedConfig, - status: 'disconnected', - capabilities: {}, - tools: [], - resources: [], - prompts: [], - reconnectAttempts: 0, - }; - - this.servers.set(name, server); - this.emit('server:registered', { name, config: normalizedConfig }); - } - - /** - * 注销服务器 - */ - async unregisterServer(name: string): Promise { - const server = this.servers.get(name); - if (!server) return; - - // 断开连接 - await this.disconnect(name); - - // 移除服务器 - this.servers.delete(name); - this.emit('server:unregistered', { name }); - } - - // ============ 连接管理 ============ - - /** - * 连接到所有服务器 - */ - async connectAll(): Promise { - const results: Array<{ name: string; success: boolean; error?: string }> = []; - - for (const [name, server] of this.servers) { - try { - await this.connect(name); - results.push({ name, success: true }); - } catch (err) { - const errorMessage = err instanceof Error ? err.message : String(err); - results.push({ name, success: false, error: errorMessage }); - this.emit('server:connect-failed', { name, error: errorMessage }); - } - } - - this.emit('connect:all-complete', { results }); - } - - /** - * 连接到指定服务器 - */ - async connect(name: string): Promise { - const server = this.servers.get(name); - if (!server) { - throw new Error(`Server not found: ${name}`); - } - - if (server.status === 'connected') { - return; - } - - if (server.status === 'connecting') { - // 等待连接完成 - return new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - reject(new Error('Connection timeout while waiting')); - }, this.options.connectionTimeout); - - const checkStatus = setInterval(() => { - if (server.status === 'connected') { - clearTimeout(timeout); - clearInterval(checkStatus); - resolve(); - } else if (server.status === 'error' || server.status === 'disconnected') { - clearTimeout(timeout); - clearInterval(checkStatus); - reject(new Error(server.error || 'Connection failed')); - } - }, 100); - }); - } - - server.status = 'connecting'; - this.emit('server:connecting', { name }); - - try { - // 创建传输 - const transport = this.createTransport(server.config); - server.transport = transport; - - // 设置事件处理 - this.setupTransportHandlers(name, transport); - - // 连接 - await transport.connect(); - - // 初始化 MCP 协议 - await this.initializeMcp(name); - - // 获取能力 - await this.discoverCapabilities(name); - - server.status = 'connected'; - server.lastConnectTime = Date.now(); - server.reconnectAttempts = 0; - - // 启动健康检查 - this.startHealthCheck(name); - - this.emit('server:connected', { - name, - capabilities: server.capabilities, - toolCount: server.tools.length, - resourceCount: server.resources.length, - }); - } catch (err) { - server.status = 'error'; - server.error = err instanceof Error ? err.message : String(err); - - this.emit('server:error', { name, error: server.error }); - - // 尝试重连 - if (server.config.autoReconnect && server.reconnectAttempts < (server.config.retries || 3)) { - await this.scheduleReconnect(name); - } - - throw err; - } - } - - /** - * 断开服务器连接 - */ - async disconnect(name: string): Promise { - const server = this.servers.get(name); - if (!server) return; - - // 停止健康检查 - this.stopHealthCheck(name); - - // 断开传输 - if (server.transport) { - await server.transport.disconnect(); - server.transport = undefined; - } - - server.status = 'disconnected'; - this.emit('server:disconnected', { name }); - } - - /** - * 断开所有连接 - */ - async disconnectAll(): Promise { - for (const name of this.servers.keys()) { - await this.disconnect(name); - } - } - - /** - * 创建传输实例 - */ - private createTransport(config: McpServerDefinition): McpTransport { - switch (config.type) { - case 'stdio': - if (!config.command) { - throw new Error('Command required for stdio transport'); - } - return new StdioTransport(config.command, config.args, config.env); - - case 'sse': - if (!config.url) { - throw new Error('URL required for SSE transport'); - } - return new SseTransport(config.url, config.headers); - - case 'http': - if (!config.url) { - throw new Error('URL required for HTTP transport'); - } - return new HttpTransport(config.url, config.headers); - - default: - throw new Error(`Unsupported transport type: ${config.type}`); - } - } - - /** - * 设置传输事件处理 - */ - private setupTransportHandlers(name: string, transport: McpTransport): void { - transport.on('message', (message: McpJsonRpcMessage) => { - this.handleMessage(name, message); - }); - - transport.on('error', (error: Error) => { - const server = this.servers.get(name); - if (server) { - server.status = 'error'; - server.error = error.message; - this.emit('server:error', { name, error: error.message }); - } - }); - - transport.on('disconnect', () => { - const server = this.servers.get(name); - if (server && server.status !== 'disconnected') { - server.status = 'disconnected'; - this.emit('server:disconnected', { name }); - - // 尝试重连 - if (server.config.autoReconnect) { - this.scheduleReconnect(name); - } - } - }); - } - - // ============ MCP 协议 ============ - - /** - * 初始化 MCP 协议 - */ - private async initializeMcp(name: string): Promise { - const result = await this.sendRequest(name, 'initialize', { - protocolVersion: '2024-11-05', - capabilities: { - tools: {}, - resources: {}, - prompts: {}, - sampling: {}, - }, - clientInfo: { - name: 'axon', - version: VERSION, - }, - }); - - // 解析服务器能力 - const server = this.servers.get(name); - if (server && result) { - const initResult = result as { - capabilities?: { - tools?: unknown; - resources?: unknown; - prompts?: unknown; - sampling?: unknown; - }; - }; - - server.capabilities = { - tools: !!initResult.capabilities?.tools, - resources: !!initResult.capabilities?.resources, - prompts: !!initResult.capabilities?.prompts, - sampling: !!initResult.capabilities?.sampling, - }; - } - - // 发送 initialized 通知 - await this.sendNotification(name, 'notifications/initialized', {}); - } - - /** - * 发现服务器能力 - */ - private async discoverCapabilities(name: string): Promise { - const server = this.servers.get(name); - if (!server) return; - - // 获取工具列表 - if (server.capabilities.tools) { - try { - const result = await this.sendRequest(name, 'tools/list', {}); - const toolsResult = result as { tools?: McpToolInfo[] }; - server.tools = toolsResult.tools || []; - } catch (err) { - console.warn(`Failed to list tools for ${name}:`, err); - } - } - - // 获取资源列表 - if (server.capabilities.resources) { - try { - const result = await this.sendRequest(name, 'resources/list', {}); - const resourcesResult = result as { resources?: McpResourceInfo[] }; - server.resources = resourcesResult.resources || []; - } catch (err) { - console.warn(`Failed to list resources for ${name}:`, err); - } - } - - // 获取提示列表 - if (server.capabilities.prompts) { - try { - const result = await this.sendRequest(name, 'prompts/list', {}); - const promptsResult = result as { prompts?: McpPromptInfo[] }; - server.prompts = promptsResult.prompts || []; - } catch (err) { - console.warn(`Failed to list prompts for ${name}:`, err); - } - } - } - - // ============ 消息处理 ============ - - /** - * 发送请求并等待响应 - */ - async sendRequest(name: string, method: string, params: unknown): Promise { - const server = this.servers.get(name); - if (!server?.transport || !server.transport.isConnected()) { - throw new Error(`Server not connected: ${name}`); - } - - const id = this.messageId++; - const message: McpJsonRpcMessage = { - jsonrpc: '2.0', - id, - method, - params, - }; - - return new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - this.pendingRequests.delete(id); - reject(new Error(`Request timeout for ${method}`)); - }, server.config.timeout || this.options.connectionTimeout); - - this.pendingRequests.set(id, { resolve, reject, timeout }); - - server.transport!.send(message).catch((err) => { - clearTimeout(timeout); - this.pendingRequests.delete(id); - reject(err); - }); - }); - } - - /** - * 发送通知 (无响应) - */ - async sendNotification(name: string, method: string, params: unknown): Promise { - const server = this.servers.get(name); - if (!server?.transport || !server.transport.isConnected()) { - throw new Error(`Server not connected: ${name}`); - } - - const message: McpJsonRpcMessage = { - jsonrpc: '2.0', - method, - params, - }; - - await server.transport.send(message); - } - - /** - * 处理接收到的消息 - */ - private handleMessage(name: string, message: McpJsonRpcMessage): void { - // 响应消息 - if (message.id !== undefined) { - const pending = this.pendingRequests.get(message.id); - if (pending) { - clearTimeout(pending.timeout); - this.pendingRequests.delete(message.id); - - if (message.error) { - pending.reject(new Error(message.error.message)); - } else { - pending.resolve(message.result); - } - } - } - - // 通知消息 - if (message.method) { - this.emit('notification', { name, method: message.method, params: message.params }); - - // 处理特定通知 - if (message.method === 'notifications/tools/list_changed') { - this.discoverCapabilities(name); - } - } - } - - // ============ 工具调用 ============ - - /** - * 调用 MCP 工具 - */ - async callTool( - serverName: string, - toolName: string, - args: Record - ): Promise<{ success: boolean; output?: string; error?: string }> { - const server = this.servers.get(serverName); - if (!server) { - return { success: false, error: `Server not found: ${serverName}` }; - } - - if (server.status !== 'connected') { - // 尝试连接 - try { - await this.connect(serverName); - } catch { - return { success: false, error: `Failed to connect to server: ${serverName}` }; - } - } - - try { - const result = await this.sendRequest(serverName, 'tools/call', { - name: toolName, - arguments: args, - }); - - // 解析结果 - const toolResult = result as { - content?: Array<{ type: string; text?: string }>; - isError?: boolean; - }; - - if (toolResult.isError) { - const errorText = toolResult.content - ?.filter((c) => c.type === 'text') - .map((c) => c.text) - .join('\n') || 'Unknown error'; - return { success: false, error: errorText }; - } - - const outputText = toolResult.content - ?.filter((c) => c.type === 'text') - .map((c) => c.text) - .join('\n') || JSON.stringify(result); - - return { success: true, output: outputText }; - } catch (err) { - return { - success: false, - error: err instanceof Error ? err.message : String(err), - }; - } - } - - // ============ 资源访问 ============ - - /** - * 读取 MCP 资源 - */ - async readResource( - serverName: string, - uri: string - ): Promise<{ success: boolean; content?: string; mimeType?: string; error?: string }> { - const server = this.servers.get(serverName); - if (!server) { - return { success: false, error: `Server not found: ${serverName}` }; - } - - if (server.status !== 'connected') { - try { - await this.connect(serverName); - } catch { - return { success: false, error: `Failed to connect to server: ${serverName}` }; - } - } - - try { - const result = await this.sendRequest(serverName, 'resources/read', { uri }); - - const resourceResult = result as { - contents?: Array<{ - uri: string; - text?: string; - blob?: string; - mimeType?: string; - }>; - }; - - if (!resourceResult.contents || resourceResult.contents.length === 0) { - return { success: false, error: 'Resource not found' }; - } - - const content = resourceResult.contents[0]; - return { - success: true, - content: content.text || (content.blob ? `[Binary: ${content.blob.length} bytes]` : ''), - mimeType: content.mimeType, - }; - } catch (err) { - return { - success: false, - error: err instanceof Error ? err.message : String(err), - }; - } - } - - // ============ 重连逻辑 ============ - - /** - * 调度重连 - */ - private async scheduleReconnect(name: string): Promise { - const server = this.servers.get(name); - if (!server) return; - - const maxAttempts = server.config.retries || this.options.maxReconnectAttempts; - if (server.reconnectAttempts >= maxAttempts) { - this.emit('server:max-reconnects', { name, attempts: server.reconnectAttempts }); - return; - } - - server.status = 'reconnecting'; - server.reconnectAttempts++; - - const delay = this.options.reconnectDelayBase * Math.pow(2, server.reconnectAttempts - 1); - - this.emit('server:reconnecting', { - name, - attempt: server.reconnectAttempts, - maxAttempts, - delayMs: delay, - }); - - await new Promise((resolve) => setTimeout(resolve, delay)); - - try { - // 清理旧连接 - if (server.transport) { - await server.transport.disconnect(); - server.transport = undefined; - } - - // 重新连接 - await this.connect(name); - } catch (err) { - // connect 会处理后续重连 - } - } - - // ============ 健康检查 ============ - - /** - * 启动健康检查 - */ - private startHealthCheck(name: string): void { - this.stopHealthCheck(name); - - const timer = setInterval(async () => { - const server = this.servers.get(name); - if (!server || server.status !== 'connected') { - this.stopHealthCheck(name); - return; - } - - try { - await this.sendRequest(name, 'ping', {}); - } catch { - // 健康检查失败 - server.status = 'error'; - this.emit('health:failed', { name }); - this.stopHealthCheck(name); - - if (server.config.autoReconnect) { - await this.scheduleReconnect(name); - } - } - }, this.options.healthCheckInterval); - - this.healthCheckTimers.set(name, timer); - } - - /** - * 停止健康检查 - */ - private stopHealthCheck(name: string): void { - const timer = this.healthCheckTimers.get(name); - if (timer) { - clearInterval(timer); - this.healthCheckTimers.delete(name); - } - } - - // ============ 查询方法 ============ - - /** - * 获取所有服务器 - */ - getAllServers(): ConnectedServer[] { - return Array.from(this.servers.values()); - } - - /** - * 获取已连接的服务器 - */ - getConnectedServers(): ConnectedServer[] { - return Array.from(this.servers.values()).filter((s) => s.status === 'connected'); - } - - /** - * 获取服务器信息 - */ - getServer(name: string): ConnectedServer | undefined { - return this.servers.get(name); - } - - /** - * 获取所有工具 - */ - getAllTools(): Array { - const tools: Array = []; - - for (const server of this.servers.values()) { - if (server.status === 'connected') { - for (const tool of server.tools) { - tools.push({ ...tool, serverName: server.name }); - } - } - } - - return tools; - } - - /** - * 获取所有资源 - */ - getAllResources(): Array { - const resources: Array = []; - - for (const server of this.servers.values()) { - if (server.status === 'connected') { - for (const resource of server.resources) { - resources.push({ ...resource, serverName: server.name }); - } - } - } - - return resources; - } - - // ============ 清理 ============ - - /** - * 销毁实例 - */ - async destroy(): Promise { - // 停止所有健康检查 - for (const timer of this.healthCheckTimers.values()) { - clearInterval(timer); - } - this.healthCheckTimers.clear(); - - // 清除所有待处理请求 - for (const pending of this.pendingRequests.values()) { - clearTimeout(pending.timeout); - pending.reject(new Error('Discovery destroyed')); - } - this.pendingRequests.clear(); - - // 断开所有连接 - await this.disconnectAll(); - - // 清空服务器列表 - this.servers.clear(); - - // 移除所有监听器 - this.removeAllListeners(); - - this.initialized = false; - } -} - -// ============ 工厂函数 ============ - -/** - * 创建 MCP 自动发现实例 - */ -export function createAutoDiscovery(options?: AutoDiscoveryOptions): McpAutoDiscovery { - return new McpAutoDiscovery(options); -} - -/** - * 创建并初始化 MCP 自动发现实例 - */ -export async function initializeAutoDiscovery( - options?: AutoDiscoveryOptions -): Promise { - const discovery = new McpAutoDiscovery(options); - await discovery.initialize(); - return discovery; -} - -// ============ 默认导出 ============ - -export default McpAutoDiscovery; diff --git a/src/mcp/cancellation.ts b/src/mcp/cancellation.ts deleted file mode 100644 index bea9b8b9..00000000 --- a/src/mcp/cancellation.ts +++ /dev/null @@ -1,641 +0,0 @@ -/** - * MCP Cancellation Module - * - * Implements request cancellation for MCP operations. Provides: - * - Request tracking and cancellation - * - Timeout-based cancellation - * - Cancellation token pattern - * - Integration with AbortController - * - * Based on MCP Specification 2024-11-05 - */ - -import { EventEmitter } from 'events'; -import { CancelledNotification } from './protocol.js'; - -// ============ Type Definitions ============ - -/** - * Cancellation reason - */ -export enum CancellationReason { - UserCancelled = 'user_cancelled', - Timeout = 'timeout', - ServerRequest = 'server_request', - Shutdown = 'shutdown', - Error = 'error', -} - -/** - * Cancellable request - */ -export interface CancellableRequest { - id: string | number; - serverName: string; - method: string; - startTime: Date; - timeout?: number; - abortController?: AbortController; - onCancel?: (reason: CancellationReason) => void; -} - -/** - * Cancellation result - */ -export interface CancellationResult { - success: boolean; - reason: CancellationReason; - requestId: string | number; - serverName: string; - duration: number; -} - -// ============ Cancellation Token ============ - -/** - * Cancellation token for request tracking - * - * Provides a way to check if a request has been cancelled - * and to register callbacks for cancellation events. - */ -export class CancellationToken extends EventEmitter { - private _cancelled: boolean = false; - private _reason: CancellationReason | null = null; - private _timestamp: Date | null = null; - - /** - * Check if cancellation has been requested - */ - get isCancelled(): boolean { - return this._cancelled; - } - - /** - * Get cancellation reason - */ - get reason(): CancellationReason | null { - return this._reason; - } - - /** - * Get cancellation timestamp - */ - get timestamp(): Date | null { - return this._timestamp; - } - - /** - * Request cancellation - */ - cancel(reason: CancellationReason = CancellationReason.UserCancelled): void { - if (this._cancelled) { - return; - } - - this._cancelled = true; - this._reason = reason; - this._timestamp = new Date(); - - this.emit('cancelled', { reason, timestamp: this._timestamp }); - } - - /** - * Throw if cancelled - */ - throwIfCancelled(): void { - if (this._cancelled) { - throw new CancellationError(this._reason || CancellationReason.UserCancelled); - } - } - - /** - * Register a cancellation callback - */ - onCancelled(callback: (reason: CancellationReason) => void): void { - if (this._cancelled && this._reason) { - callback(this._reason); - } else { - this.once('cancelled', ({ reason }) => callback(reason)); - } - } -} - -/** - * Cancellation error - */ -export class CancellationError extends Error { - constructor(public reason: CancellationReason) { - super(`Request cancelled: ${reason}`); - this.name = 'CancellationError'; - } -} - -// ============ Cancellation Manager ============ - -/** - * Manages request cancellation for MCP operations - * - * Features: - * - Request registration and tracking - * - Manual and timeout-based cancellation - * - AbortController integration - * - Cancellation notification - * - Event emission for monitoring - */ -export class McpCancellationManager extends EventEmitter { - private requests: Map = new Map(); - private timeouts: Map = new Map(); - - // ============ Request Registration ============ - - /** - * Register a cancellable request - * - * Returns a cancellation token that can be used to track - * and trigger cancellation. - * - * Options: - * - timeout: Automatically cancel after this many ms - * - abortController: Link to an AbortController - * - onCancel: Callback when cancelled - * - onTimeout: Callback specifically for timeout (called in addition to onCancel) - */ - registerRequest( - id: string | number, - serverName: string, - method: string, - options?: { - timeout?: number; - abortController?: AbortController; - onCancel?: (reason: CancellationReason) => void; - onTimeout?: () => void; - } - ): CancellationToken { - const request: CancellableRequest = { - id, - serverName, - method, - startTime: new Date(), - timeout: options?.timeout, - abortController: options?.abortController, - onCancel: options?.onCancel, - }; - - this.requests.set(id, request); - - // Set up timeout if specified - if (options?.timeout) { - this.setupTimeout(id, options.timeout, options.onTimeout); - } - - // Create cancellation token - const token = new CancellationToken(); - - // Link AbortController if provided - if (options?.abortController) { - options.abortController.signal.addEventListener('abort', () => { - if (!token.isCancelled) { - token.cancel(CancellationReason.UserCancelled); - } - }); - } - - // When token is cancelled, cancel the request - token.onCancelled((reason) => { - this.cancelRequest(id, reason); - }); - - this.emit('request:registered', { id, serverName, method }); - - return token; - } - - /** - * Unregister a request (called when completed successfully) - */ - unregisterRequest(id: string | number): boolean { - const request = this.requests.get(id); - if (!request) { - return false; - } - - // Clear timeout - this.clearTimeout(id); - - // Remove request - this.requests.delete(id); - - this.emit('request:unregistered', { id, serverName: request.serverName }); - - return true; - } - - /** - * Check if a request is registered - */ - hasRequest(id: string | number): boolean { - return this.requests.has(id); - } - - /** - * Get a registered request - */ - getRequest(id: string | number): CancellableRequest | undefined { - return this.requests.get(id); - } - - /** - * Get all registered requests - */ - getAllRequests(): CancellableRequest[] { - return Array.from(this.requests.values()); - } - - /** - * Get requests for a specific server - */ - getServerRequests(serverName: string): CancellableRequest[] { - return this.getAllRequests().filter(r => r.serverName === serverName); - } - - // ============ Cancellation ============ - - /** - * Cancel a request - */ - cancelRequest( - id: string | number, - reason: CancellationReason = CancellationReason.UserCancelled - ): CancellationResult | null { - const request = this.requests.get(id); - if (!request) { - return null; - } - - const startTime = request.startTime.getTime(); - const duration = Date.now() - startTime; - - // Clear timeout - this.clearTimeout(id); - - // Abort if AbortController is available - if (request.abortController && !request.abortController.signal.aborted) { - request.abortController.abort(); - } - - // Call onCancel callback - if (request.onCancel) { - try { - request.onCancel(reason); - } catch (error) { - this.emit('cancel:error', { id, error }); - } - } - - // Remove request - this.requests.delete(id); - - const result: CancellationResult = { - success: true, - reason, - requestId: id, - serverName: request.serverName, - duration, - }; - - this.emit('request:cancelled', result); - - return result; - } - - /** - * Cancel all requests for a server - */ - cancelServerRequests( - serverName: string, - reason: CancellationReason = CancellationReason.Shutdown - ): CancellationResult[] { - const requests = this.getServerRequests(serverName); - const results: CancellationResult[] = []; - - for (const request of requests) { - const result = this.cancelRequest(request.id, reason); - if (result) { - results.push(result); - } - } - - this.emit('server:cancelled', { serverName, count: results.length }); - - return results; - } - - /** - * Cancel all requests - */ - cancelAll(reason: CancellationReason = CancellationReason.Shutdown): CancellationResult[] { - const requests = this.getAllRequests(); - const results: CancellationResult[] = []; - - for (const request of requests) { - const result = this.cancelRequest(request.id, reason); - if (result) { - results.push(result); - } - } - - this.emit('all:cancelled', { count: results.length }); - - return results; - } - - // ============ Timeout Management ============ - - /** - * Set up a timeout for a request - */ - private setupTimeout( - id: string | number, - timeout: number, - onTimeout?: () => void - ): void { - const timer = setTimeout(() => { - // Call timeout callback first if provided - if (onTimeout) { - try { - onTimeout(); - } catch (error) { - this.emit('timeout:error', { id, error }); - } - } - - // Then cancel the request - this.cancelRequest(id, CancellationReason.Timeout); - }, timeout); - - this.timeouts.set(id, timer); - } - - /** - * Clear a timeout - */ - private clearTimeout(id: string | number): void { - const timer = this.timeouts.get(id); - if (timer) { - clearTimeout(timer); - this.timeouts.delete(id); - } - } - - // ============ Notifications ============ - - /** - * Create a cancellation notification for a request - * - * This can be sent to the server to notify it of cancellation. - */ - createCancellationNotification( - requestId: string | number, - reason?: string - ): CancelledNotification { - return { - requestId, - ...(reason && { reason }), - }; - } - - /** - * Cancel a request and send cancellation notification - * - * This is the main method to cancel a request. It: - * 1. Cancels the request locally - * 2. Returns the notification that should be sent to the server - * - * The caller is responsible for actually sending the notification. - */ - cancelRequestWithNotification( - id: string | number, - reason: CancellationReason = CancellationReason.UserCancelled - ): { result: CancellationResult | null; notification: CancelledNotification | null } { - const result = this.cancelRequest(id, reason); - - if (!result) { - return { result: null, notification: null }; - } - - const notification = this.createCancellationNotification( - id, - this.getCancellationReasonString(reason) - ); - - return { result, notification }; - } - - /** - * Convert CancellationReason to string for notification - */ - private getCancellationReasonString(reason: CancellationReason): string { - switch (reason) { - case CancellationReason.UserCancelled: - return 'Request cancelled by user'; - case CancellationReason.Timeout: - return 'Request timed out'; - case CancellationReason.ServerRequest: - return 'Cancelled at server request'; - case CancellationReason.Shutdown: - return 'Cancelled due to shutdown'; - case CancellationReason.Error: - return 'Cancelled due to error'; - default: - return 'Request cancelled'; - } - } - - // ============ Statistics ============ - - /** - * Get statistics about cancellations - */ - getStats() { - const requests = this.getAllRequests(); - - const byServer: Record = {}; - requests.forEach(request => { - byServer[request.serverName] = (byServer[request.serverName] || 0) + 1; - }); - - const withTimeout = requests.filter(r => r.timeout !== undefined).length; - const withAbort = requests.filter(r => r.abortController !== undefined).length; - - return { - activeRequests: requests.length, - byServer, - withTimeout, - withAbort, - }; - } - - /** - * Get request durations - */ - getRequestDurations(): Array<{ - id: string | number; - serverName: string; - method: string; - duration: number; - }> { - const now = Date.now(); - - return this.getAllRequests().map(request => ({ - id: request.id, - serverName: request.serverName, - method: request.method, - duration: now - request.startTime.getTime(), - })); - } - - /** - * Find requests exceeding a duration threshold - */ - findLongRunningRequests(thresholdMs: number): CancellableRequest[] { - const now = Date.now(); - - return this.getAllRequests().filter(request => { - const duration = now - request.startTime.getTime(); - return duration > thresholdMs; - }); - } - - // ============ Cleanup ============ - - /** - * Clean up all requests and timeouts - */ - cleanup(): void { - // Clear all timeouts - this.timeouts.forEach((timer) => clearTimeout(timer)); - this.timeouts.clear(); - - // Clear all requests - this.requests.clear(); - - this.emit('cleanup'); - } - - // ============ Utility Methods ============ - - /** - * Create a managed cancellable request - * - * This is a high-level helper that: - * 1. Registers the request - * 2. Returns token and notification sender - * 3. Automatically handles cleanup - * - * Example usage: - * ```ts - * const { token, sendNotification } = manager.createCancellableRequest( - * requestId, - * 'myServer', - * 'tools/call', - * { timeout: 30000 } - * ); - * - * // Later, if cancelled: - * token.cancel(CancellationReason.UserCancelled); - * const notification = sendNotification(); - * // Send notification to server... - * ``` - */ - createCancellableRequest( - id: string | number, - serverName: string, - method: string, - options?: { - timeout?: number; - abortController?: AbortController; - onCancel?: (reason: CancellationReason) => void; - onTimeout?: () => void; - } - ): { - token: CancellationToken; - sendNotification: () => CancelledNotification | null; - cleanup: () => void; - } { - const token = this.registerRequest(id, serverName, method, options); - - return { - token, - sendNotification: () => { - if (!token.isCancelled || !token.reason) { - return null; - } - return this.createCancellationNotification( - id, - this.getCancellationReasonString(token.reason) - ); - }, - cleanup: () => { - this.unregisterRequest(id); - }, - }; - } -} - -// ============ Helper Functions ============ - -/** - * Create a cancellation token linked to an AbortController - */ -export function createTokenFromAbortController( - controller: AbortController -): CancellationToken { - const token = new CancellationToken(); - - controller.signal.addEventListener('abort', () => { - token.cancel(CancellationReason.UserCancelled); - }); - - return token; -} - -/** - * Create an AbortController that cancels after a timeout - */ -export function createTimeoutController(timeoutMs: number): AbortController { - const controller = new AbortController(); - - setTimeout(() => { - controller.abort(); - }, timeoutMs); - - return controller; -} - -/** - * Combine multiple cancellation tokens - */ -export function combineTokens(...tokens: CancellationToken[]): CancellationToken { - const combined = new CancellationToken(); - - for (const token of tokens) { - if (token.isCancelled) { - combined.cancel(token.reason || CancellationReason.UserCancelled); - break; - } - - token.onCancelled((reason) => { - combined.cancel(reason); - }); - } - - return combined; -} - -/** - * Check if an error is a cancellation error - */ -export function isCancellationError(error: unknown): error is CancellationError { - return error instanceof CancellationError || - (error instanceof Error && error.name === 'CancellationError'); -} diff --git a/src/mcp/notifications.ts b/src/mcp/notifications.ts deleted file mode 100644 index 4b969ac5..00000000 --- a/src/mcp/notifications.ts +++ /dev/null @@ -1,521 +0,0 @@ -/** - * MCP Notifications Module - * - * Handles notification messages from MCP servers. Notifications are one-way - * messages that don't require a response, used for: - * - Progress updates - * - Resource/tool/prompt list changes - * - Request cancellations - * - Custom server events - * - * Based on MCP Specification 2024-11-05 - */ - -import { EventEmitter } from 'events'; -import { McpMethod, ProgressNotification, CancelledNotification } from './protocol.js'; - -// ============ Type Definitions ============ - -/** - * Notification types - */ -export enum NotificationType { - Progress = 'progress', - Cancelled = 'cancelled', - ResourcesListChanged = 'resources/list_changed', - ResourcesUpdated = 'resources/updated', - ToolsListChanged = 'tools/list_changed', - PromptsListChanged = 'prompts/list_changed', - RootsListChanged = 'roots/list_changed', - Custom = 'custom', -} - -/** - * Base notification - */ -export interface Notification { - type: NotificationType; - serverName: string; - timestamp: Date; - method: string; - params?: unknown; -} - -/** - * Progress notification (extended) - */ -export interface ProgressNotificationExt extends Notification { - type: NotificationType.Progress; - params: ProgressNotification; -} - -/** - * Cancelled notification (extended) - */ -export interface CancelledNotificationExt extends Notification { - type: NotificationType.Cancelled; - params: CancelledNotification; -} - -/** - * List changed notification - */ -export interface ListChangedNotification extends Notification { - type: NotificationType.ResourcesListChanged | - NotificationType.ToolsListChanged | - NotificationType.PromptsListChanged | - NotificationType.RootsListChanged; -} - -/** - * Resource updated notification - */ -export interface ResourceUpdatedNotification extends Notification { - type: NotificationType.ResourcesUpdated; - params: { - uri: string; - }; -} - -/** - * Custom notification - */ -export interface CustomNotification extends Notification { - type: NotificationType.Custom; - params: unknown; -} - -/** - * Notification handler function - */ -export type NotificationHandler = (notification: Notification) => void | Promise; - -/** - * Progress handler function - */ -export type ProgressHandler = ( - serverName: string, - progress: ProgressNotification -) => void | Promise; - -// ============ Notification Manager ============ - -/** - * Manages notifications from MCP servers - * - * Features: - * - Notification routing and handling - * - Type-specific handlers - * - Progress tracking - * - Notification history - * - Event emission for monitoring - */ -export class McpNotificationManager extends EventEmitter { - private handlers: Map> = new Map(); - private progressHandlers: Map = new Map(); - private history: Notification[] = []; - private maxHistorySize: number; - - // Progress tracking - private progressStates: Map = new Map(); - - constructor(options?: { maxHistorySize?: number }) { - super(); - this.maxHistorySize = options?.maxHistorySize ?? 100; - } - - // ============ Handler Registration ============ - - /** - * Register a handler for a specific notification type - */ - on(type: string, handler: NotificationHandler): this { - if (!this.handlers.has(type)) { - this.handlers.set(type, new Set()); - } - - this.handlers.get(type)!.add(handler); - return this; - } - - /** - * Unregister a handler - */ - off(type: string, handler: NotificationHandler): this { - const handlers = this.handlers.get(type); - if (handlers) { - handlers.delete(handler); - if (handlers.size === 0) { - this.handlers.delete(type); - } - } - return this; - } - - /** - * Register a progress handler for a server - */ - onProgress(serverName: string, handler: ProgressHandler): void { - this.progressHandlers.set(serverName, handler); - } - - /** - * Unregister a progress handler - */ - offProgress(serverName: string): void { - this.progressHandlers.delete(serverName); - } - - // ============ Notification Handling ============ - - /** - * Handle a notification from a server - * - * This method should be called when receiving a notification message - * from an MCP server. - */ - async handleNotification( - serverName: string, - method: string, - params?: unknown - ): Promise { - // Determine notification type - const type = this.getNotificationType(method); - - // Create notification object - const notification: Notification = { - type, - serverName, - timestamp: new Date(), - method, - params, - }; - - // Add to history - this.addToHistory(notification); - - // Emit general event - this.emit('notification', notification); - - // Handle specific types - await this.handleSpecificType(notification); - - // Call registered handlers - await this.callHandlers(type, notification); - } - - /** - * Get notification type from method name - */ - private getNotificationType(method: string): NotificationType { - if (method === McpMethod.NotificationProgress) { - return NotificationType.Progress; - } else if (method === McpMethod.NotificationCancelled) { - return NotificationType.Cancelled; - } else if (method === McpMethod.NotificationResourcesListChanged) { - return NotificationType.ResourcesListChanged; - } else if (method === McpMethod.NotificationResourcesUpdated) { - return NotificationType.ResourcesUpdated; - } else if (method === McpMethod.NotificationToolsListChanged) { - return NotificationType.ToolsListChanged; - } else if (method === McpMethod.NotificationPromptsListChanged) { - return NotificationType.PromptsListChanged; - } else if (method.includes('roots/list_changed')) { - return NotificationType.RootsListChanged; - } - - return NotificationType.Custom; - } - - /** - * Handle specific notification types - */ - private async handleSpecificType(notification: Notification): Promise { - switch (notification.type) { - case NotificationType.Progress: - await this.handleProgress(notification as ProgressNotificationExt); - break; - - case NotificationType.Cancelled: - await this.handleCancelled(notification as CancelledNotificationExt); - break; - - case NotificationType.ResourcesListChanged: - case NotificationType.ToolsListChanged: - case NotificationType.PromptsListChanged: - case NotificationType.RootsListChanged: - await this.handleListChanged(notification as ListChangedNotification); - break; - - case NotificationType.ResourcesUpdated: - await this.handleResourceUpdated(notification as ResourceUpdatedNotification); - break; - } - } - - /** - * Handle progress notification - */ - private async handleProgress(notification: ProgressNotificationExt): Promise { - const { serverName, params } = notification; - const { progressToken, progress, total } = params; - - // Update progress state - const key = `${serverName}:${progressToken}`; - const existing = this.progressStates.get(key); - - this.progressStates.set(key, { - serverName, - token: progressToken, - progress, - total, - startTime: existing?.startTime ?? new Date(), - lastUpdate: new Date(), - }); - - // Emit progress event - this.emit('progress', { serverName, ...params }); - - // Call registered progress handler - const handler = this.progressHandlers.get(serverName); - if (handler) { - await handler(serverName, params); - } - - // If progress is complete (100% or progress === total) - const isComplete = (total !== undefined && progress >= total) || - (progress === 100); - - if (isComplete) { - this.emit('progress:complete', { serverName, token: progressToken }); - this.progressStates.delete(key); - } - } - - /** - * Handle cancelled notification - */ - private async handleCancelled(notification: CancelledNotificationExt): Promise { - const { serverName, params } = notification; - - this.emit('cancelled', { serverName, ...params }); - } - - /** - * Handle list changed notification - */ - private async handleListChanged(notification: ListChangedNotification): Promise { - const { serverName, type } = notification; - - this.emit('list:changed', { serverName, type }); - } - - /** - * Handle resource updated notification - */ - private async handleResourceUpdated(notification: ResourceUpdatedNotification): Promise { - const { serverName, params } = notification; - - this.emit('resource:updated', { serverName, uri: params.uri }); - } - - /** - * Call registered handlers for a notification type - */ - private async callHandlers(type: NotificationType, notification: Notification): Promise { - const handlers = this.handlers.get(type); - if (!handlers || handlers.size === 0) { - return; - } - - // Call all handlers (in parallel) - const promises = Array.from(handlers).map(handler => - Promise.resolve(handler(notification)).catch(error => { - this.emit('handler:error', { type, error, notification }); - }) - ); - - await Promise.all(promises); - } - - // ============ History Management ============ - - /** - * Add notification to history - */ - private addToHistory(notification: Notification): void { - this.history.push(notification); - - // Enforce size limit - if (this.history.length > this.maxHistorySize) { - this.history.shift(); - } - } - - /** - * Get notification history - */ - getHistory(filter?: { - serverName?: string; - type?: NotificationType; - since?: Date; - limit?: number; - }): Notification[] { - let filtered = [...this.history]; - - if (filter) { - if (filter.serverName) { - filtered = filtered.filter(n => n.serverName === filter.serverName); - } - - if (filter.type) { - filtered = filtered.filter(n => n.type === filter.type); - } - - if (filter.since) { - filtered = filtered.filter(n => n.timestamp >= filter.since!); - } - - if (filter.limit) { - filtered = filtered.slice(-filter.limit); - } - } - - return filtered; - } - - /** - * Clear history - */ - clearHistory(): void { - const count = this.history.length; - this.history = []; - this.emit('history:cleared', { count }); - } - - /** - * Clear history for a specific server - */ - clearServerHistory(serverName: string): number { - const before = this.history.length; - this.history = this.history.filter(n => n.serverName !== serverName); - return before - this.history.length; - } - - // ============ Progress Tracking ============ - - /** - * Get active progress operations - */ - getActiveProgress(): Array<{ - serverName: string; - token: string | number; - progress: number; - total?: number; - startTime: Date; - lastUpdate: Date; - duration: number; - }> { - const now = Date.now(); - - return Array.from(this.progressStates.values()).map(state => ({ - ...state, - duration: now - state.startTime.getTime(), - })); - } - - /** - * Get progress for a specific server - */ - getServerProgress(serverName: string): Array<{ - token: string | number; - progress: number; - total?: number; - }> { - return this.getActiveProgress() - .filter(p => p.serverName === serverName) - .map(({ token, progress, total }) => ({ token, progress, total })); - } - - /** - * Cancel progress tracking for a token - */ - cancelProgress(serverName: string, token: string | number): boolean { - const key = `${serverName}:${token}`; - return this.progressStates.delete(key); - } - - /** - * Clear all progress tracking - */ - clearProgress(): void { - const count = this.progressStates.size; - this.progressStates.clear(); - this.emit('progress:cleared', { count }); - } - - // ============ Statistics ============ - - /** - * Get statistics - */ - getStats() { - const byType: Record = {}; - - this.history.forEach(notification => { - byType[notification.type] = (byType[notification.type] || 0) + 1; - }); - - const byServer: Record = {}; - - this.history.forEach(notification => { - byServer[notification.serverName] = (byServer[notification.serverName] || 0) + 1; - }); - - return { - totalNotifications: this.history.length, - maxHistorySize: this.maxHistorySize, - handlerCount: this.handlers.size, - activeProgress: this.progressStates.size, - byType, - byServer, - }; - } -} - -// ============ Helper Functions ============ - -/** - * Create a progress notification parameters object - */ -export function createProgressParams( - token: string | number, - progress: number, - total?: number -): ProgressNotification { - return { - progressToken: token, - progress, - ...(total !== undefined && { total }), - }; -} - -/** - * Create a cancelled notification parameters object - */ -export function createCancelledParams( - requestId: string | number, - reason?: string -): CancelledNotification { - return { - requestId, - ...(reason && { reason }), - }; -} diff --git a/src/mcp/roots.ts b/src/mcp/roots.ts deleted file mode 100644 index d0da5074..00000000 --- a/src/mcp/roots.ts +++ /dev/null @@ -1,467 +0,0 @@ -/** - * MCP Roots Module - * - * Manages root directories for MCP servers. Roots define the base directories - * that servers can access, providing a sandboxing mechanism for file operations. - * - * Based on MCP Specification 2024-11-05 - */ - -import { EventEmitter } from 'events'; -import { Root } from './protocol.js'; -import path from 'path'; -import fs from 'fs'; - -// ============ Type Definitions ============ - -/** - * Root directory with metadata - */ -export interface RootInfo extends Root { - uri: string; - name?: string; - // Additional metadata - exists?: boolean; - absolutePath?: string; - permissions?: { - read: boolean; - write: boolean; - }; -} - -/** - * Roots configuration - */ -export interface RootsConfig { - roots: Root[]; - allowDynamicRoots?: boolean; - validatePaths?: boolean; -} - -// ============ Roots Manager ============ - -/** - * Manages root directories for MCP servers - * - * Features: - * - Root directory registration and management - * - Path validation and sandboxing - * - Dynamic root addition/removal - * - Event emission for root changes - */ -export class McpRootsManager extends EventEmitter { - private roots: Map = new Map(); - private config: { - allowDynamicRoots: boolean; - validatePaths: boolean; - }; - - constructor(config?: RootsConfig) { - super(); - - this.config = { - allowDynamicRoots: config?.allowDynamicRoots ?? true, - validatePaths: config?.validatePaths ?? true, - }; - - // Initialize with provided roots - if (config?.roots) { - config.roots.forEach(root => { - this.addRoot(root); - }); - } - } - - // ============ Root Management ============ - - /** - * Add a root directory - */ - addRoot(root: Root): RootInfo { - // Parse URI - const rootInfo = this.parseRoot(root); - - // Validate if enabled - if (this.config.validatePaths && rootInfo.absolutePath) { - this.validateRootPath(rootInfo.absolutePath); - } - - // Store root - this.roots.set(rootInfo.uri, rootInfo); - - this.emit('root:added', { root: rootInfo }); - - return rootInfo; - } - - /** - * Remove a root directory - */ - removeRoot(uri: string): boolean { - const root = this.roots.get(uri); - if (!root) { - return false; - } - - this.roots.delete(uri); - this.emit('root:removed', { root }); - - return true; - } - - /** - * Update a root directory - */ - updateRoot(uri: string, updates: Partial): RootInfo | null { - const existing = this.roots.get(uri); - if (!existing) { - return null; - } - - const updated = this.parseRoot({ - ...existing, - ...updates, - }); - - // Validate if path changed - if (updates.uri && this.config.validatePaths && updated.absolutePath) { - this.validateRootPath(updated.absolutePath); - } - - this.roots.set(uri, updated); - this.emit('root:updated', { root: updated, previous: existing }); - - return updated; - } - - /** - * Get a root by URI - */ - getRoot(uri: string): RootInfo | undefined { - return this.roots.get(uri); - } - - /** - * Get all roots - */ - getRoots(): RootInfo[] { - return Array.from(this.roots.values()); - } - - /** - * Get all roots as plain Root objects (for MCP protocol) - */ - getRootsForProtocol(): Root[] { - return this.getRoots().map(root => ({ - uri: root.uri, - ...(root.name && { name: root.name }), - })); - } - - /** - * Clear all roots - */ - clearRoots(): void { - const count = this.roots.size; - this.roots.clear(); - this.emit('roots:cleared', { count }); - } - - /** - * Check if a URI is registered as a root - */ - hasRoot(uri: string): boolean { - return this.roots.has(uri); - } - - // ============ Path Operations ============ - - /** - * Parse a root and extract information - */ - private parseRoot(root: Root): RootInfo { - let absolutePath: string | undefined; - let exists: boolean | undefined; - let permissions: { read: boolean; write: boolean } | undefined; - - // Parse file:// URIs - if (root.uri.startsWith('file://')) { - try { - absolutePath = this.uriToPath(root.uri); - - // Check if path exists - if (this.config.validatePaths) { - exists = fs.existsSync(absolutePath); - - // Check permissions - if (exists) { - try { - fs.accessSync(absolutePath, fs.constants.R_OK); - const read = true; - - let write = false; - try { - fs.accessSync(absolutePath, fs.constants.W_OK); - write = true; - } catch { - // Not writable - } - - permissions = { read, write }; - } catch { - permissions = { read: false, write: false }; - } - } - } - } catch (error) { - // Invalid file URI - } - } - - return { - ...root, - absolutePath, - exists, - permissions, - }; - } - - /** - * Convert file:// URI to local path - */ - private uriToPath(uri: string): string { - if (!uri.startsWith('file://')) { - throw new Error(`Invalid file URI: ${uri}`); - } - - // Remove file:// prefix - let path = uri.slice(7); - - // Handle Windows paths (file:///C:/...) - if (process.platform === 'win32') { - if (path.startsWith('/') && path[2] === ':') { - path = path.slice(1); - } - } - - // Decode URI components - path = decodeURIComponent(path); - - return path; - } - - /** - * Convert local path to file:// URI - */ - private pathToUri(filePath: string): string { - let uri = 'file://'; - - // Convert to absolute path - const absolute = path.resolve(filePath); - - // Handle Windows paths - if (process.platform === 'win32') { - // C:\path -> file:///C:/path - uri += '/' + absolute.replace(/\\/g, '/'); - } else { - // /path -> file:///path - uri += absolute; - } - - // Encode URI components - uri = encodeURI(uri); - - return uri; - } - - /** - * Validate a root path - */ - private validateRootPath(rootPath: string): void { - // Check if path exists - if (!fs.existsSync(rootPath)) { - throw new Error(`Root path does not exist: ${rootPath}`); - } - - // Check if it's a directory - const stats = fs.statSync(rootPath); - if (!stats.isDirectory()) { - throw new Error(`Root path is not a directory: ${rootPath}`); - } - - // Check if readable - try { - fs.accessSync(rootPath, fs.constants.R_OK); - } catch { - throw new Error(`Root path is not readable: ${rootPath}`); - } - } - - /** - * Check if a path is within any root - */ - isPathInRoots(filePath: string): boolean { - const absolutePath = path.resolve(filePath); - - for (const root of this.roots.values()) { - if (root.absolutePath && this.isPathInRoot(absolutePath, root.absolutePath)) { - return true; - } - } - - return false; - } - - /** - * Check if a path is within a specific root - */ - isPathInRoot(filePath: string, rootPath: string): boolean { - const absolutePath = path.resolve(filePath); - const absoluteRoot = path.resolve(rootPath); - - // Normalize paths - const normalizedPath = path.normalize(absolutePath); - const normalizedRoot = path.normalize(absoluteRoot); - - // Check if path starts with root - return normalizedPath.startsWith(normalizedRoot + path.sep) || - normalizedPath === normalizedRoot; - } - - /** - * Get the root that contains a path - */ - getRootForPath(filePath: string): RootInfo | null { - const absolutePath = path.resolve(filePath); - - for (const root of this.roots.values()) { - if (root.absolutePath && this.isPathInRoot(absolutePath, root.absolutePath)) { - return root; - } - } - - return null; - } - - /** - * Resolve a relative path against roots - */ - resolvePath(relativePath: string): string | null { - for (const root of this.roots.values()) { - if (root.absolutePath) { - const resolved = path.join(root.absolutePath, relativePath); - if (fs.existsSync(resolved)) { - return resolved; - } - } - } - - return null; - } - - // ============ Dynamic Roots ============ - - /** - * Add a root from a local path - */ - addRootFromPath(filePath: string, name?: string): RootInfo { - if (!this.config.allowDynamicRoots) { - throw new Error('Dynamic roots are not allowed'); - } - - const uri = this.pathToUri(filePath); - const root: Root = { - uri, - ...(name && { name }), - }; - - return this.addRoot(root); - } - - /** - * Add the current working directory as a root - */ - addCwdRoot(name: string = 'Current Directory'): RootInfo { - return this.addRootFromPath(process.cwd(), name); - } - - /** - * Add home directory as a root - */ - addHomeRoot(name: string = 'Home Directory'): RootInfo { - const home = process.env.HOME || process.env.USERPROFILE; - if (!home) { - throw new Error('Could not determine home directory'); - } - - return this.addRootFromPath(home, name); - } - - // ============ Statistics ============ - - /** - * Get statistics about roots - */ - getStats() { - const roots = this.getRoots(); - - return { - totalRoots: roots.length, - existingRoots: roots.filter(r => r.exists).length, - readableRoots: roots.filter(r => r.permissions?.read).length, - writableRoots: roots.filter(r => r.permissions?.write).length, - allowDynamicRoots: this.config.allowDynamicRoots, - validatePaths: this.config.validatePaths, - }; - } - - /** - * Refresh root information (check existence and permissions) - */ - refreshRoots(): void { - const roots = Array.from(this.roots.values()); - - for (const root of roots) { - const updated = this.parseRoot(root); - this.roots.set(root.uri, updated); - } - - this.emit('roots:refreshed', { count: roots.length }); - } -} - -// ============ Helper Functions ============ - -/** - * Create a root from a file path - */ -export function createRootFromPath(filePath: string, name?: string): Root { - const uri = path.isAbsolute(filePath) - ? `file://${filePath}` - : `file://${path.resolve(filePath)}`; - - return { - uri, - ...(name && { name }), - }; -} - -/** - * Create default roots configuration - */ -export function getDefaultRootsConfig(): RootsConfig { - return { - roots: [ - createRootFromPath(process.cwd(), 'Current Directory'), - ], - allowDynamicRoots: true, - validatePaths: true, - }; -} - -/** - * Normalize a file path for comparison - */ -export function normalizePath(filePath: string): string { - return path.normalize(path.resolve(filePath)); -} diff --git a/src/skills/builtin/app/SKILL.md b/src/skills/builtin/app/SKILL.md new file mode 100644 index 00000000..3a5a6302 --- /dev/null +++ b/src/skills/builtin/app/SKILL.md @@ -0,0 +1,18 @@ +--- +name: app +description: 开发真实可运行的应用(Web / 全栈 / 移动)——走编程流水线交付可跑工程。 +when-to-use: 用户要开发应用 / web app / 全栈应用 / SaaS / 移动 app / 小程序时 +allowed-tools: enter_coding_mode, Read, Write, Edit, Bash +--- + +# 开发应用 + +用户要一个真实可运行的应用(Web / 全栈 / 移动)—— 这是**编程任务**,用 `enter_coding_mode` 走编程流水线 +交付可运行工程(spec → 蓝图 → 测试门 → deliver_app,交付后自动在右栏画布预览),**不是**给代码片段。 +(进 coding 后这段 nuance 持续在你上下文。) + +- **Web / 全栈** → React + Vite(全栈加 Node/Express + 数据库),deliver_app 给可点开的运行链接。 +- **移动(React Native)** → 当前环境无模拟器,跑不出可点链接。动手前如实告诉用户:可生成并通过测试门交付完整 + RN 工程源码,但给不了运行链接,让他选「要 Web 版」还是「要 RN 工程」。 + +进度反馈(必须):动手前 1-2 句说清要做什么;关键节点各补一句。 diff --git a/src/skills/builtin/audio/SKILL.md b/src/skills/builtin/audio/SKILL.md new file mode 100644 index 00000000..0f45d07f --- /dev/null +++ b/src/skills/builtin/audio/SKILL.md @@ -0,0 +1,18 @@ +--- +name: audio +description: 音频生成——TTS 朗读 / 配音 / 音乐 / 音效。 +when-to-use: 用户要做 TTS 朗读 / 配音 / 音乐 / 音效生成时 +allowed-tools: generate_speech, generate_music, generate_audio, Read, Write +--- + +# 音频 + +你被激活来交付音频产物。 + +## 交付路径 + +- **TTS(朗读 / 配音)** → 用 `generate_speech`(若可用)生成音频文件;不可用就如实告诉用户并返回脚本 + 推荐工具。 +- **音乐 / 音效** → 用 `generate_music` / `generate_audio`(若可用);不可用同上如实降级。 + +★不掩盖问题★:hosted 音频工具不可用 / 缺 key 时,明确报错告诉用户,**不要**假装生成了音频。 +产物落到文件后,前端会用内联 `