# NetaClaw Phase 1: 清理旧代码 + 核心迁移 实施计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 移除 LangChain/Tauri/旧 Skill,从 OpenClaw 迁移 Agent 核心运行时,使系统能进行基本的 Agent 对话。 **Architecture:** Midway.js 后端保留业务模块(base/dict/task/user),新增 netaclaw 模块承载从 OpenClaw 迁移的 Agent 引擎。Agent 运行时采用 ReAct 循环(Think→Act→Observe),工具系统基于 TypeBox Schema,通过 WebSocket 与前端通信。 **Tech Stack:** Midway.js 3.20 / TypeScript 5.9 / TypeBox / @anthropic-ai/sdk / WebSocket (ws) **OpenClaw 源码位置:** `c:/Users/lixin/Desktop/RZYX_ZT/openclaw-main` --- ## 文件结构 ### 删除的文件/目录 - `packages/desktop/` — 整个 Tauri 桌面端 - `packages/skills/` — 旧 Python/Node skill 目录 - `packages/backend/src/modules/agent/` — 旧 LangChain Agent 模块 - `packages/backend/src/modules/audit/` — 旧审核模块(依赖 agent) - `packages/backend/src/modules/flow/` — 旧流程引擎(依赖 LangChain) - `packages/backend/src/modules/know/` — 旧知识库(依赖 LangChain) - `packages/backend/src/modules/ontology/` — 旧本体论模块 ### 新建的文件 | 文件 | 职责 | |------|------| | `packages/backend/src/modules/netaclaw/module.ts` | Midway.js 模块注册 | | `packages/backend/src/modules/netaclaw/config.ts` | NetaClaw 配置定义 | | `packages/backend/src/modules/netaclaw/tools/common.ts` | AgentTool 接口 + 工具结果构建器 | | `packages/backend/src/modules/netaclaw/tools/builtin/bash.ts` | Bash 命令执行工具 | | `packages/backend/src/modules/netaclaw/tools/builtin/file.ts` | 文件读写工具 | | `packages/backend/src/modules/netaclaw/tools/builtin/web_search.ts` | 网页搜索工具 | | `packages/backend/src/modules/netaclaw/runtime/agent.ts` | Agent ReAct 核心循环 | | `packages/backend/src/modules/netaclaw/runtime/attempt.ts` | 单次执行尝试 | | `packages/backend/src/modules/netaclaw/runtime/model_selection.ts` | 模型选择与故障转移 | | `packages/backend/src/modules/netaclaw/runtime/thinking.ts` | 思考级别控制 | | `packages/backend/src/modules/netaclaw/gateway/server.ts` | WebSocket 服务 | | `packages/backend/src/modules/netaclaw/gateway/session.ts` | 会话管理 | | `packages/backend/src/modules/netaclaw/gateway/protocol.ts` | 消息协议定义 | | `packages/backend/src/modules/netaclaw/plugins/plugin_entry.ts` | 插件定义接口 | | `packages/backend/src/modules/netaclaw/plugins/plugin_api.ts` | 插件 API | | `packages/backend/src/modules/netaclaw/plugins/llm_providers/openai.ts` | OpenAI 提供商 | | `packages/backend/src/modules/netaclaw/plugins/llm_providers/anthropic.ts` | Anthropic 提供商 | | `packages/backend/src/modules/netaclaw/plugins/llm_providers/deepseek.ts` | DeepSeek 提供商 | | `packages/backend/src/modules/netaclaw/service/chat.ts` | 对话服务(HTTP/SSE 入口) | | `packages/backend/src/modules/netaclaw/controller/chat.ts` | 对话控制器 | | `packages/backend/src/modules/netaclaw/entity/session.ts` | 会话实体 | | `packages/backend/src/modules/netaclaw/entity/message.ts` | 消息实体 | | `skills/hello-world/SKILL.md` | 示例 Skill(验证 Skill 加载机制) | ### 修改的文件 | 文件 | 改动 | |------|------| | `packages/backend/package.json` | 移除 @langchain/* 依赖,新增 @sinclair/typebox、@anthropic-ai/sdk、ws | | `packages/backend/src/config/config.default.ts` | 新增 netaclaw 配置段 | | `packages/backend/src/configuration.ts` | 注册 netaclaw 模块,移除旧模块引用 | | `pnpm-workspace.yaml` | 移除 desktop、skills 包路径 | | `package.json` (root) | 移除 desktop 相关脚本 | --- ## Task 1: 清理旧代码和依赖 **Files:** - Delete: `packages/desktop/` (整个目录) - Delete: `packages/skills/` (整个目录) - Delete: `packages/backend/src/modules/agent/` (整个目录) - Delete: `packages/backend/src/modules/audit/` (整个目录) - Delete: `packages/backend/src/modules/flow/` (整个目录) - Delete: `packages/backend/src/modules/know/` (整个目录) - Delete: `packages/backend/src/modules/ontology/` (整个目录) - Modify: `pnpm-workspace.yaml` - Modify: `package.json` (root) - Modify: `packages/backend/package.json` - Modify: `packages/backend/src/configuration.ts` - [ ] **Step 1: 删除 Tauri 桌面端和旧 Skills 目录** ```bash rm -rf packages/desktop packages/skills ``` - [ ] **Step 2: 删除旧的 LangChain Agent 及依赖模块** ```bash rm -rf packages/backend/src/modules/agent rm -rf packages/backend/src/modules/audit rm -rf packages/backend/src/modules/flow rm -rf packages/backend/src/modules/know rm -rf packages/backend/src/modules/ontology ``` - [ ] **Step 3: 更新 pnpm-workspace.yaml,移除 desktop 和 skills** ```yaml packages: - 'packages/*' ``` - [ ] **Step 4: 更新根 package.json scripts,移除 desktop 相关命令** 移除 `dev:all` 中的 desktop 引用,保留 backend 和 frontend: ```json "scripts": { "dev": "pnpm --parallel --filter @neta/backend --filter @neta/frontend dev", "build": "pnpm --filter './packages/*' build", "test": "pnpm --filter './packages/*' test", "lint": "pnpm --filter './packages/*' lint", "clean": "pnpm --filter './packages/*' clean && rm -rf node_modules" } ``` - [ ] **Step 5: 从 backend/package.json 移除 LangChain 及旧依赖** 移除以下依赖: ``` @langchain/classic @langchain/cohere @langchain/community @langchain/core @langchain/deepseek @langchain/langgraph @langchain/langgraph-checkpoint @langchain/mcp-adapters @langchain/ollama @langchain/openai @langchain/textsplitters langchain chromadb crawlee puppeteer-core ``` 新增以下依赖: ``` @sinclair/typebox: ^0.34.49 @anthropic-ai/sdk: ^0.81.0 ws: ^8.18.0 openai: ^4.73.0 ``` 运行:`pnpm install` - [ ] **Step 6: 更新 configuration.ts,移除旧模块引用** 从 `packages/backend/src/configuration.ts` 中移除对 agent/audit/flow/know/ontology 模块的所有 import 和引用。如果这些模块是通过 Cool Admin 自动扫描加载的(`src/modules/` 目录扫描),则删除目录后无需额外修改 configuration.ts。 验证:检查 configuration.ts 中是否有显式 import 这些模块。 - [ ] **Step 7: 验证后端能正常启动** ```bash cd packages/backend && pnpm dev ``` 预期:后端在 8003 端口启动,无 import 错误。可能有数据库表缺失警告(旧表),这是正常的。 - [ ] **Step 8: 提交** ```bash git add -A git commit -m "chore: remove Tauri, LangChain, old agent/audit/flow/know/ontology modules" ``` --- ## Task 2: NetaClaw 工具系统基础 **Files:** - Create: `packages/backend/src/modules/netaclaw/tools/common.ts` - [ ] **Step 1: 创建 netaclaw 模块目录结构** ```bash mkdir -p packages/backend/src/modules/netaclaw/{tools/builtin,runtime,gateway,plugins/llm_providers,service,controller,entity,platforms} ``` - [ ] **Step 2: 创建 AgentTool 接口和工具结果构建器** 参考 OpenClaw `src/agents/tools/common.ts`,创建 `packages/backend/src/modules/netaclaw/tools/common.ts`: ```typescript import { Type, Static, TSchema } from '@sinclair/typebox'; // --- 工具接口 --- export type AgentTool = { name: string; label: string; description: string; parameters: TParams; execute(id: string, params: Static): Promise; }; export type AgentToolWithMeta = AgentTool & { ownerOnly?: boolean; displaySummary?: string; }; export type AnyAgentTool = AgentToolWithMeta; // --- 工具结果 --- export type ToolResultContent = | { type: 'text'; text: string } | { type: 'json'; data: unknown } | { type: 'image'; url: string; mimeType?: string }; export function textResult(text: string): ToolResultContent { return { type: 'text', text }; } export function jsonResult(data: unknown): ToolResultContent { return { type: 'json', data }; } export function imageResult(url: string, mimeType?: string): ToolResultContent { return { type: 'image', url, mimeType }; } // --- 工具错误 --- export class ToolInputError extends Error { constructor(message: string) { super(message); this.name = 'ToolInputError'; } } export class ToolAuthorizationError extends Error { constructor(message: string) { super(message); this.name = 'ToolAuthorizationError'; } } // --- 参数读取辅助 --- export function readStringParam(params: Record, key: string): string { const val = params[key]; if (typeof val !== 'string') throw new ToolInputError(`参数 "${key}" 必须是字符串`); return val; } export function readOptionalStringParam(params: Record, key: string): string | undefined { const val = params[key]; if (val === undefined || val === null) return undefined; if (typeof val !== 'string') throw new ToolInputError(`参数 "${key}" 必须是字符串`); return val; } ``` - [ ] **Step 3: 提交** ```bash git add packages/backend/src/modules/netaclaw/ git commit -m "feat(netaclaw): add AgentTool interface and tool result builders" ``` --- ## Task 3: LLM 提供商插件 **Files:** - Create: `packages/backend/src/modules/netaclaw/plugins/plugin_entry.ts` - Create: `packages/backend/src/modules/netaclaw/plugins/llm_providers/anthropic.ts` - Create: `packages/backend/src/modules/netaclaw/plugins/llm_providers/openai.ts` - Create: `packages/backend/src/modules/netaclaw/plugins/llm_providers/deepseek.ts` - [ ] **Step 1: 创建插件定义接口** 创建 `packages/backend/src/modules/netaclaw/plugins/plugin_entry.ts`: ```typescript import { AnyAgentTool } from '../tools/common.js'; // --- 插件接口 --- export interface NetaClawPluginDefinition { id: string; name: string; version: string; tools?: () => AnyAgentTool[]; } // --- LLM 提供商接口 --- export interface LLMMessage { role: 'system' | 'user' | 'assistant' | 'tool'; content: string; toolCalls?: ToolCall[]; toolCallId?: string; } export interface ToolCall { id: string; name: string; arguments: string; // JSON string } export interface LLMResponse { content: string; toolCalls?: ToolCall[]; thinking?: string; usage?: { inputTokens: number; outputTokens: number }; stopReason?: 'end_turn' | 'tool_use' | 'max_tokens'; } export type ThinkLevel = 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'adaptive'; export interface LLMProviderConfig { provider: string; model: string; apiKey: string; baseUrl?: string; temperature?: number; maxTokens?: number; thinkLevel?: ThinkLevel; } export interface LLMProvider { id: string; name: string; chat(messages: LLMMessage[], tools: AnyAgentTool[], config: LLMProviderConfig): Promise; } ``` - [ ] **Step 2: 创建 Anthropic 提供商** 创建 `packages/backend/src/modules/netaclaw/plugins/llm_providers/anthropic.ts`: ```typescript import Anthropic from '@anthropic-ai/sdk'; import { LLMProvider, LLMMessage, LLMResponse, LLMProviderConfig, ToolCall } from '../plugin_entry.js'; import { AnyAgentTool } from '../../tools/common.js'; export class AnthropicProvider implements LLMProvider { id = 'anthropic'; name = 'Anthropic Claude'; async chat(messages: LLMMessage[], tools: AnyAgentTool[], config: LLMProviderConfig): Promise { const client = new Anthropic({ apiKey: config.apiKey, baseURL: config.baseUrl }); const systemMsg = messages.find(m => m.role === 'system'); const chatMessages = messages .filter(m => m.role !== 'system') .map(m => this.toAnthropicMessage(m)); const anthropicTools = tools.map(t => ({ name: t.name, description: t.description, input_schema: t.parameters as Record, })); const response = await client.messages.create({ model: config.model, max_tokens: config.maxTokens ?? 4096, system: systemMsg?.content ?? '', messages: chatMessages, tools: anthropicTools.length > 0 ? anthropicTools : undefined, temperature: config.temperature, }); return this.parseResponse(response); } private toAnthropicMessage(msg: LLMMessage): Anthropic.MessageParam { if (msg.role === 'assistant' && msg.toolCalls?.length) { return { role: 'assistant', content: [ ...(msg.content ? [{ type: 'text' as const, text: msg.content }] : []), ...msg.toolCalls.map(tc => ({ type: 'tool_use' as const, id: tc.id, name: tc.name, input: JSON.parse(tc.arguments), })), ], }; } if (msg.role === 'tool') { return { role: 'user', content: [{ type: 'tool_result' as const, tool_use_id: msg.toolCallId!, content: msg.content }], }; } return { role: msg.role as 'user' | 'assistant', content: msg.content }; } private parseResponse(response: Anthropic.Message): LLMResponse { let content = ''; const toolCalls: ToolCall[] = []; for (const block of response.content) { if (block.type === 'text') content += block.text; if (block.type === 'tool_use') { toolCalls.push({ id: block.id, name: block.name, arguments: JSON.stringify(block.input) }); } } return { content, toolCalls: toolCalls.length > 0 ? toolCalls : undefined, usage: { inputTokens: response.usage.input_tokens, outputTokens: response.usage.output_tokens }, stopReason: response.stop_reason === 'tool_use' ? 'tool_use' : 'end_turn', }; } } ``` - [ ] **Step 3: 创建 OpenAI 提供商** 创建 `packages/backend/src/modules/netaclaw/plugins/llm_providers/openai.ts`: ```typescript import OpenAI from 'openai'; import { LLMProvider, LLMMessage, LLMResponse, LLMProviderConfig, ToolCall } from '../plugin_entry.js'; import { AnyAgentTool } from '../../tools/common.js'; export class OpenAIProvider implements LLMProvider { id = 'openai'; name = 'OpenAI'; async chat(messages: LLMMessage[], tools: AnyAgentTool[], config: LLMProviderConfig): Promise { const client = new OpenAI({ apiKey: config.apiKey, baseURL: config.baseUrl }); const openaiMessages = messages.map(m => this.toOpenAIMessage(m)); const openaiTools = tools.map(t => ({ type: 'function' as const, function: { name: t.name, description: t.description, parameters: t.parameters as Record }, })); const response = await client.chat.completions.create({ model: config.model, messages: openaiMessages, tools: openaiTools.length > 0 ? openaiTools : undefined, max_tokens: config.maxTokens ?? 4096, temperature: config.temperature, }); const choice = response.choices[0]; const toolCalls: ToolCall[] = (choice.message.tool_calls ?? []).map(tc => ({ id: tc.id, name: tc.function.name, arguments: tc.function.arguments, })); return { content: choice.message.content ?? '', toolCalls: toolCalls.length > 0 ? toolCalls : undefined, usage: response.usage ? { inputTokens: response.usage.prompt_tokens, outputTokens: response.usage.completion_tokens } : undefined, stopReason: choice.finish_reason === 'tool_calls' ? 'tool_use' : 'end_turn', }; } private toOpenAIMessage(msg: LLMMessage): OpenAI.ChatCompletionMessageParam { if (msg.role === 'tool') { return { role: 'tool', content: msg.content, tool_call_id: msg.toolCallId! }; } if (msg.role === 'assistant' && msg.toolCalls?.length) { return { role: 'assistant', content: msg.content || null, tool_calls: msg.toolCalls.map(tc => ({ id: tc.id, type: 'function' as const, function: { name: tc.name, arguments: tc.arguments }, })), }; } return { role: msg.role, content: msg.content } as OpenAI.ChatCompletionMessageParam; } } ``` - [ ] **Step 4: 创建 DeepSeek 提供商(复用 OpenAI 兼容接口)** 创建 `packages/backend/src/modules/netaclaw/plugins/llm_providers/deepseek.ts`: ```typescript import { OpenAIProvider } from './openai.js'; export class DeepSeekProvider extends OpenAIProvider { id = 'deepseek'; name = 'DeepSeek'; // DeepSeek 使用 OpenAI 兼容 API,仅需覆盖 baseUrl 默认值 // 调用时传入 config.baseUrl = 'https://api.deepseek.com' } ``` - [ ] **Step 5: 提交** ```bash git add packages/backend/src/modules/netaclaw/plugins/ git commit -m "feat(netaclaw): add plugin interface and LLM providers (Anthropic, OpenAI, DeepSeek)" ``` --- ## Task 4: Agent 运行时核心(ReAct 循环) **Files:** - Create: `packages/backend/src/modules/netaclaw/runtime/agent.ts` - Create: `packages/backend/src/modules/netaclaw/runtime/attempt.ts` - Create: `packages/backend/src/modules/netaclaw/runtime/model_selection.ts` - Create: `packages/backend/src/modules/netaclaw/runtime/thinking.ts` - [ ] **Step 1: 创建思考级别控制** 创建 `packages/backend/src/modules/netaclaw/runtime/thinking.ts`: ```typescript import { ThinkLevel } from '../plugins/plugin_entry.js'; export function resolveThinkingDefault(model: string): ThinkLevel { if (model.includes('claude-3-5') || model.includes('claude-4')) return 'medium'; if (model.includes('deepseek-r1') || model.includes('o1') || model.includes('o3')) return 'high'; return 'off'; } export function isThinkingSupported(model: string): boolean { const thinkingModels = ['claude-3-5-sonnet', 'claude-4', 'deepseek-r1', 'o1', 'o3', 'qwq']; return thinkingModels.some(m => model.includes(m)); } ``` - [ ] **Step 2: 创建模型选择与故障转移** 创建 `packages/backend/src/modules/netaclaw/runtime/model_selection.ts`: ```typescript import { LLMProviderConfig } from '../plugins/plugin_entry.js'; import { LLMProvider } from '../plugins/plugin_entry.js'; import { AnthropicProvider } from '../plugins/llm_providers/anthropic.js'; import { OpenAIProvider } from '../plugins/llm_providers/openai.js'; import { DeepSeekProvider } from '../plugins/llm_providers/deepseek.js'; const providers: Map = new Map(); export function registerProvider(provider: LLMProvider): void { providers.set(provider.id, provider); } export function getProvider(id: string): LLMProvider { const p = providers.get(id); if (!p) throw new Error(`LLM 提供商 "${id}" 未注册`); return p; } export function initDefaultProviders(): void { registerProvider(new AnthropicProvider()); registerProvider(new OpenAIProvider()); registerProvider(new DeepSeekProvider()); } export interface ModelRef { provider: string; model: string; } export function parseModelRef(modelStr: string): ModelRef { // 格式: "provider:model" 或 "model"(默认 openai) const parts = modelStr.split(':'); if (parts.length === 2) return { provider: parts[0], model: parts[1] }; // 根据模型名推断提供商 if (modelStr.startsWith('claude')) return { provider: 'anthropic', model: modelStr }; if (modelStr.startsWith('deepseek')) return { provider: 'deepseek', model: modelStr }; return { provider: 'openai', model: modelStr }; } ``` - [ ] **Step 3: 创建单次执行尝试** 创建 `packages/backend/src/modules/netaclaw/runtime/attempt.ts`: ```typescript import { LLMMessage, LLMResponse, LLMProviderConfig } from '../plugins/plugin_entry.js'; import { AnyAgentTool } from '../tools/common.js'; import { getProvider } from './model_selection.js'; export interface AttemptParams { messages: LLMMessage[]; tools: AnyAgentTool[]; config: LLMProviderConfig; maxToolRounds?: number; onToken?: (text: string) => void; onThinking?: (text: string) => void; onToolCall?: (name: string, args: Record) => void; onToolResult?: (name: string, result: string) => void; } export interface AttemptResult { messages: LLMMessage[]; finalContent: string; usage: { inputTokens: number; outputTokens: number }; toolCallCount: number; } export async function runAttempt(params: AttemptParams): Promise { const { messages, tools, config, maxToolRounds = 20, onToken, onThinking, onToolCall, onToolResult } = params; const provider = getProvider(config.provider); const conversation = [...messages]; let totalInput = 0; let totalOutput = 0; let toolCallCount = 0; for (let round = 0; round < maxToolRounds; round++) { const response: LLMResponse = await provider.chat(conversation, tools, config); totalInput += response.usage?.inputTokens ?? 0; totalOutput += response.usage?.outputTokens ?? 0; if (response.thinking) onThinking?.(response.thinking); if (response.content) onToken?.(response.content); // 无工具调用 → 结束 if (!response.toolCalls?.length) { conversation.push({ role: 'assistant', content: response.content }); return { messages: conversation, finalContent: response.content, usage: { inputTokens: totalInput, outputTokens: totalOutput }, toolCallCount, }; } // 有工具调用 → 执行工具 conversation.push({ role: 'assistant', content: response.content, toolCalls: response.toolCalls, }); for (const tc of response.toolCalls) { toolCallCount++; onToolCall?.(tc.name, JSON.parse(tc.arguments)); const tool = tools.find(t => t.name === tc.name); let resultText: string; if (!tool) { resultText = `错误: 工具 "${tc.name}" 不存在`; } else { try { const result = await tool.execute(tc.id, JSON.parse(tc.arguments)); resultText = typeof result === 'string' ? result : JSON.stringify(result); } catch (err: any) { resultText = `工具执行错误: ${err.message}`; } } onToolResult?.(tc.name, resultText); conversation.push({ role: 'tool', content: resultText, toolCallId: tc.id }); } } // 超过最大轮次 const lastAssistant = conversation.filter(m => m.role === 'assistant').pop(); return { messages: conversation, finalContent: lastAssistant?.content ?? '达到最大工具调用轮次', usage: { inputTokens: totalInput, outputTokens: totalOutput }, toolCallCount, }; } ``` - [ ] **Step 4: 创建 Agent 核心循环** 创建 `packages/backend/src/modules/netaclaw/runtime/agent.ts`: ```typescript import { LLMMessage, LLMProviderConfig } from '../plugins/plugin_entry.js'; import { AnyAgentTool } from '../tools/common.js'; import { runAttempt, AttemptResult } from './attempt.js'; import { parseModelRef } from './model_selection.js'; import { resolveThinkingDefault } from './thinking.js'; export interface AgentConfig { name: string; systemPrompt: string; model: string; // "provider:model" 或 "model" apiKey: string; baseUrl?: string; temperature?: number; maxTokens?: number; maxToolRounds?: number; skills?: string[]; // 加载的 SKILL.md 名称列表 } export interface AgentRunParams { agentConfig: AgentConfig; tools: AnyAgentTool[]; userMessage: string; history?: LLMMessage[]; onToken?: (text: string) => void; onThinking?: (text: string) => void; onToolCall?: (name: string, args: Record) => void; onToolResult?: (name: string, result: string) => void; } export async function runAgent(params: AgentRunParams): Promise { const { agentConfig, tools, userMessage, history = [], onToken, onThinking, onToolCall, onToolResult } = params; const modelRef = parseModelRef(agentConfig.model); const thinkLevel = resolveThinkingDefault(modelRef.model); const llmConfig: LLMProviderConfig = { provider: modelRef.provider, model: modelRef.model, apiKey: agentConfig.apiKey, baseUrl: agentConfig.baseUrl, temperature: agentConfig.temperature, maxTokens: agentConfig.maxTokens, thinkLevel, }; const messages: LLMMessage[] = [ { role: 'system', content: agentConfig.systemPrompt }, ...history, { role: 'user', content: userMessage }, ]; return runAttempt({ messages, tools, config: llmConfig, maxToolRounds: agentConfig.maxToolRounds, onToken, onThinking, onToolCall, onToolResult, }); } ``` - [ ] **Step 5: 提交** ```bash git add packages/backend/src/modules/netaclaw/runtime/ git commit -m "feat(netaclaw): add Agent ReAct runtime with model selection and tool execution loop" ``` --- ## Task 5: 内置工具(Bash + File) **Files:** - Create: `packages/backend/src/modules/netaclaw/tools/builtin/bash.ts` - Create: `packages/backend/src/modules/netaclaw/tools/builtin/file.ts` - [ ] **Step 1: 创建 Bash 命令执行工具** 创建 `packages/backend/src/modules/netaclaw/tools/builtin/bash.ts`: ```typescript import { Type } from '@sinclair/typebox'; import { exec } from 'child_process'; import { promisify } from 'util'; import { AgentToolWithMeta, textResult } from '../common.js'; const execAsync = promisify(exec); const BashParams = Type.Object({ command: Type.String({ description: '要执行的 shell 命令' }), cwd: Type.Optional(Type.String({ description: '工作目录' })), timeout: Type.Optional(Type.Number({ description: '超时时间(毫秒),默认 30000' })), }); export const bashTool: AgentToolWithMeta = { name: 'bash', label: '执行命令', description: '在 shell 中执行命令并返回输出', parameters: BashParams, async execute(_id, params) { const timeout = params.timeout ?? 30000; try { const { stdout, stderr } = await execAsync(params.command, { cwd: params.cwd, timeout, maxBuffer: 1024 * 1024 * 10, // 10MB }); const output = stdout + (stderr ? `\n[stderr]: ${stderr}` : ''); return textResult(output || '(无输出)'); } catch (err: any) { return textResult(`命令执行失败: ${err.message}\n${err.stderr ?? ''}`); } }, }; ``` - [ ] **Step 2: 创建文件读写工具** 创建 `packages/backend/src/modules/netaclaw/tools/builtin/file.ts`: ```typescript import { Type } from '@sinclair/typebox'; import * as fs from 'fs/promises'; import * as path from 'path'; import { AgentToolWithMeta, textResult, ToolInputError } from '../common.js'; const ReadFileParams = Type.Object({ path: Type.String({ description: '文件绝对路径' }), encoding: Type.Optional(Type.String({ description: '编码,默认 utf-8' })), }); export const readFileTool: AgentToolWithMeta = { name: 'read_file', label: '读取文件', description: '读取指定路径的文件内容', parameters: ReadFileParams, async execute(_id, params) { try { const content = await fs.readFile(params.path, { encoding: (params.encoding ?? 'utf-8') as BufferEncoding }); return textResult(content); } catch (err: any) { return textResult(`读取文件失败: ${err.message}`); } }, }; const WriteFileParams = Type.Object({ path: Type.String({ description: '文件绝对路径' }), content: Type.String({ description: '要写入的内容' }), }); export const writeFileTool: AgentToolWithMeta = { name: 'write_file', label: '写入文件', description: '将内容写入指定路径的文件(自动创建目录)', parameters: WriteFileParams, async execute(_id, params) { try { await fs.mkdir(path.dirname(params.path), { recursive: true }); await fs.writeFile(params.path, params.content, 'utf-8'); return textResult(`文件已写入: ${params.path}`); } catch (err: any) { return textResult(`写入文件失败: ${err.message}`); } }, }; const ListDirParams = Type.Object({ path: Type.String({ description: '目录路径' }), }); export const listDirTool: AgentToolWithMeta = { name: 'list_dir', label: '列出目录', description: '列出指定目录下的文件和子目录', parameters: ListDirParams, async execute(_id, params) { try { const entries = await fs.readdir(params.path, { withFileTypes: true }); const lines = entries.map(e => `${e.isDirectory() ? '[DIR]' : '[FILE]'} ${e.name}`); return textResult(lines.join('\n') || '(空目录)'); } catch (err: any) { return textResult(`列出目录失败: ${err.message}`); } }, }; ``` - [ ] **Step 3: 提交** ```bash git add packages/backend/src/modules/netaclaw/tools/builtin/ git commit -m "feat(netaclaw): add builtin tools (bash, read_file, write_file, list_dir)" ``` --- ## Task 6: 数据库实体 + 会话管理 + WebSocket Gateway **Files:** - Create: `packages/backend/src/modules/netaclaw/entity/session.ts` - Create: `packages/backend/src/modules/netaclaw/entity/message.ts` - Create: `packages/backend/src/modules/netaclaw/gateway/protocol.ts` - Create: `packages/backend/src/modules/netaclaw/gateway/session.ts` - Create: `packages/backend/src/modules/netaclaw/gateway/server.ts` - [ ] **Step 1: 创建会话实体** 创建 `packages/backend/src/modules/netaclaw/entity/session.ts`: ```typescript import { BaseEntity } from '@cool-midway/core'; import { Column, Entity } from 'typeorm'; @Entity('netaclaw_session') export class NetaClawSessionEntity extends BaseEntity { @Column({ comment: '会话ID', unique: true }) sessionId: string; @Column({ comment: '用户ID', nullable: true }) userId: string; @Column({ comment: 'Agent名称' }) agentName: string; @Column({ comment: '会话标题', nullable: true }) title: string; @Column({ comment: '使用的模型', nullable: true }) model: string; @Column({ comment: '状态: active/archived', default: 'active' }) status: string; @Column({ type: 'json', comment: '元数据', nullable: true }) metadata: Record; } ``` - [ ] **Step 2: 创建消息实体** 创建 `packages/backend/src/modules/netaclaw/entity/message.ts`: ```typescript import { BaseEntity } from '@cool-midway/core'; import { Column, Entity, Index } from 'typeorm'; @Entity('netaclaw_message') export class NetaClawMessageEntity extends BaseEntity { @Index() @Column({ comment: '会话ID' }) sessionId: string; @Column({ comment: '角色: system/user/assistant/tool' }) role: string; @Column({ type: 'longtext', comment: '消息内容' }) content: string; @Column({ type: 'longtext', comment: '思考内容', nullable: true }) thinking: string; @Column({ type: 'json', comment: '工具调用', nullable: true }) toolCalls: unknown; @Column({ comment: '工具调用ID', nullable: true }) toolCallId: string; @Column({ comment: 'Skill名称', nullable: true }) skillName: string; @Column({ type: 'json', comment: '元数据', nullable: true }) metadata: Record; } ``` - [ ] **Step 3: 创建消息协议定义** 创建 `packages/backend/src/modules/netaclaw/gateway/protocol.ts`: ```typescript // --- 客户端 → 服务端 --- export interface ClientChatMessage { type: 'chat'; sessionId: string; content: string; agentName?: string; } export interface ClientPingMessage { type: 'ping'; } export type ClientMessage = ClientChatMessage | ClientPingMessage; // --- 服务端 → 客户端 --- export interface ServerTokenEvent { type: 'token'; sessionId: string; content: string; } export interface ServerThinkingEvent { type: 'thinking'; sessionId: string; content: string; } export interface ServerToolCallEvent { type: 'tool_call'; sessionId: string; toolName: string; args: Record; } export interface ServerToolResultEvent { type: 'tool_result'; sessionId: string; toolName: string; result: string; } export interface ServerDoneEvent { type: 'done'; sessionId: string; usage?: { inputTokens: number; outputTokens: number }; } export interface ServerErrorEvent { type: 'error'; sessionId: string; message: string; } export interface ServerPongEvent { type: 'pong'; } export type ServerEvent = | ServerTokenEvent | ServerThinkingEvent | ServerToolCallEvent | ServerToolResultEvent | ServerDoneEvent | ServerErrorEvent | ServerPongEvent; ``` - [ ] **Step 4: 创建会话管理服务** 创建 `packages/backend/src/modules/netaclaw/gateway/session.ts`: ```typescript import { Provide, Inject, Scope, ScopeEnum } from '@midwayjs/core'; import { InjectEntityModel } from '@midwayjs/typeorm'; import { Repository } from 'typeorm'; import { v4 as uuid } from 'uuid'; import { NetaClawSessionEntity } from '../entity/session.js'; import { NetaClawMessageEntity } from '../entity/message.js'; import { LLMMessage } from '../plugins/plugin_entry.js'; @Provide() @Scope(ScopeEnum.Singleton) export class NetaClawSessionService { @InjectEntityModel(NetaClawSessionEntity) sessionRepo: Repository; @InjectEntityModel(NetaClawMessageEntity) messageRepo: Repository; async createSession(agentName: string, userId?: string): Promise { const sessionId = uuid(); await this.sessionRepo.save({ sessionId, agentName, userId, status: 'active', }); return sessionId; } async getOrCreateSession(sessionId: string | undefined, agentName: string): Promise { if (sessionId) { const existing = await this.sessionRepo.findOneBy({ sessionId }); if (existing) return sessionId; } return this.createSession(agentName); } async loadHistory(sessionId: string): Promise { const messages = await this.messageRepo.find({ where: { sessionId }, order: { createTime: 'ASC' }, }); return messages.map(m => ({ role: m.role as LLMMessage['role'], content: m.content, toolCalls: m.toolCalls as any, toolCallId: m.toolCallId, })); } async saveMessage(sessionId: string, msg: LLMMessage & { thinking?: string; skillName?: string }): Promise { await this.messageRepo.save({ sessionId, role: msg.role, content: msg.content, thinking: msg.thinking, toolCalls: msg.toolCalls, toolCallId: msg.toolCallId, skillName: msg.skillName, }); } async updateTitle(sessionId: string, title: string): Promise { await this.sessionRepo.update({ sessionId }, { title }); } } ``` - [ ] **Step 5: 创建 WebSocket Gateway 服务** 创建 `packages/backend/src/modules/netaclaw/gateway/server.ts`: ```typescript import { Provide, Inject, Scope, ScopeEnum, Init, Logger } from '@midwayjs/core'; import { ILogger } from '@midwayjs/logger'; import { Context } from '@midwayjs/socketio'; import { OnWSConnection, OnWSMessage, WSController } from '@midwayjs/socketio'; import { NetaClawSessionService } from './session.js'; import { ClientMessage, ServerEvent } from './protocol.js'; import { runAgent, AgentConfig, AgentRunParams } from '../runtime/agent.js'; import { bashTool } from '../tools/builtin/bash.js'; import { readFileTool, writeFileTool, listDirTool } from '../tools/builtin/file.js'; import { AnyAgentTool } from '../tools/common.js'; import { initDefaultProviders } from '../runtime/model_selection.js'; @WSController('/netaclaw') export class NetaClawGateway { @Inject() ctx: Context; @Logger() logger: ILogger; @Inject() sessionService: NetaClawSessionService; private defaultTools: AnyAgentTool[] = [bashTool, readFileTool, writeFileTool, listDirTool]; @Init() async init() { initDefaultProviders(); } @OnWSConnection() async onConnect() { this.logger.info('[NetaClaw] 客户端已连接: %s', this.ctx.id); } @OnWSMessage('message') async onMessage(data: string) { let msg: ClientMessage; try { msg = JSON.parse(data); } catch { this.send({ type: 'error', sessionId: '', message: '无效的 JSON 消息' }); return; } if (msg.type === 'ping') { this.send({ type: 'pong' }); return; } if (msg.type === 'chat') { await this.handleChat(msg.sessionId, msg.content, msg.agentName); } } private async handleChat(sessionId: string, content: string, agentName?: string) { const agent = agentName ?? 'default'; const sid = await this.sessionService.getOrCreateSession(sessionId, agent); // 保存用户消息 await this.sessionService.saveMessage(sid, { role: 'user', content }); // 加载历史 const history = await this.sessionService.loadHistory(sid); // TODO: 从数据库加载 AgentConfig,当前使用默认配置 const agentConfig: AgentConfig = { name: agent, systemPrompt: '你是 NetaClaw 电商运营助手,帮助用户管理电商平台的商品、订单和营销活动。', model: process.env.NETACLAW_MODEL ?? 'anthropic:claude-sonnet-4-20250514', apiKey: process.env.NETACLAW_API_KEY ?? '', maxToolRounds: 20, }; try { const result = await runAgent({ agentConfig, tools: this.defaultTools, userMessage: content, history: history.slice(0, -1), // 排除刚保存的用户消息(已在 messages 中) onToken: text => this.send({ type: 'token', sessionId: sid, content: text }), onThinking: text => this.send({ type: 'thinking', sessionId: sid, content: text }), onToolCall: (name, args) => this.send({ type: 'tool_call', sessionId: sid, toolName: name, args }), onToolResult: (name, res) => this.send({ type: 'tool_result', sessionId: sid, toolName: name, result: res }), }); // 保存助手回复 await this.sessionService.saveMessage(sid, { role: 'assistant', content: result.finalContent }); this.send({ type: 'done', sessionId: sid, usage: result.usage }); } catch (err: any) { this.logger.error('[NetaClaw] Agent 执行错误: %s', err.message); this.send({ type: 'error', sessionId: sid, message: err.message }); } } private send(event: ServerEvent) { this.ctx.emit('message', JSON.stringify(event)); } } ``` - [ ] **Step 6: 提交** ```bash git add packages/backend/src/modules/netaclaw/entity/ packages/backend/src/modules/netaclaw/gateway/ git commit -m "feat(netaclaw): add session/message entities, WS gateway, and session management" ``` --- ## Task 7: Midway.js 模块注册 + 配置 + Skill 加载器 **Files:** - Create: `packages/backend/src/modules/netaclaw/config.ts` - Create: `packages/backend/src/modules/netaclaw/module.ts` - Create: `packages/backend/src/modules/netaclaw/service/skill_loader.ts` - Create: `packages/backend/src/modules/netaclaw/controller/chat.ts` - Create: `skills/hello-world/SKILL.md` - Modify: `packages/backend/src/config/config.default.ts` - [ ] **Step 1: 创建 NetaClaw 配置定义** 创建 `packages/backend/src/modules/netaclaw/config.ts`: ```typescript export interface NetaClawConfig { // 默认模型(格式: "provider:model") defaultModel: string; // API Keys(按提供商) apiKeys: Record; // Skill 目录路径 skillsDir: string; // 本地数据目录 dataDir: string; // 最大工具调用轮次 maxToolRounds: number; } export const defaultNetaClawConfig: NetaClawConfig = { defaultModel: 'anthropic:claude-sonnet-4-20250514', apiKeys: {}, skillsDir: './skills', dataDir: '~/.neta', maxToolRounds: 20, }; ``` - [ ] **Step 2: 创建 Skill 加载器(SKILL.md 格式)** 创建 `packages/backend/src/modules/netaclaw/service/skill_loader.ts`: ```typescript import { Provide, Scope, ScopeEnum, Config, Logger, Init } from '@midwayjs/core'; import { ILogger } from '@midwayjs/logger'; import * as fs from 'fs/promises'; import * as path from 'path'; export interface SkillMeta { name: string; description: string; metadata?: Record; content: string; // SKILL.md 正文(去掉 frontmatter) } @Provide() @Scope(ScopeEnum.Singleton) export class SkillLoaderService { @Logger() logger: ILogger; private skills: Map = new Map(); private skillsDir: string; @Init() async init() { this.skillsDir = path.resolve(process.cwd(), 'skills'); await this.scanSkills(); } async scanSkills(): Promise { try { const entries = await fs.readdir(this.skillsDir, { withFileTypes: true }); for (const entry of entries) { if (!entry.isDirectory()) continue; const skillMdPath = path.join(this.skillsDir, entry.name, 'SKILL.md'); try { const raw = await fs.readFile(skillMdPath, 'utf-8'); const skill = this.parseSkillMd(raw); if (skill) { this.skills.set(skill.name, skill); this.logger.info('[SkillLoader] 已加载 Skill: %s', skill.name); } } catch { // SKILL.md 不存在,跳过 } } this.logger.info('[SkillLoader] 共加载 %d 个 Skill', this.skills.size); } catch { this.logger.warn('[SkillLoader] Skills 目录不存在: %s', this.skillsDir); } } private parseSkillMd(raw: string): SkillMeta | null { const fmMatch = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/); if (!fmMatch) return null; const frontmatter = fmMatch[1]; const content = fmMatch[2].trim(); // 简单 YAML 解析(name 和 description) const nameMatch = frontmatter.match(/^name:\s*(.+)$/m); const descMatch = frontmatter.match(/^description:\s*(.+)$/m); if (!nameMatch) return null; return { name: nameMatch[1].trim(), description: descMatch?.[1]?.trim() ?? '', content, }; } getSkill(name: string): SkillMeta | undefined { return this.skills.get(name); } getAllSkills(): SkillMeta[] { return Array.from(this.skills.values()); } getSkillPrompt(names: string[]): string { const parts: string[] = []; for (const name of names) { const skill = this.skills.get(name); if (skill) { parts.push(`## Skill: ${skill.name}\n${skill.content}`); } } return parts.length > 0 ? `\n\n# 可用技能\n\n${parts.join('\n\n')}` : ''; } } ``` - [ ] **Step 3: 创建 HTTP 对话控制器(SSE 备选方案)** 创建 `packages/backend/src/modules/netaclaw/controller/chat.ts`: ```typescript import { Provide, Inject, Post, Body, Controller, Logger } from '@midwayjs/core'; import { ILogger } from '@midwayjs/logger'; import { Context } from '@midwayjs/koa'; import { NetaClawSessionService } from '../gateway/session.js'; import { SkillLoaderService } from '../service/skill_loader.js'; import { runAgent, AgentConfig } from '../runtime/agent.js'; import { bashTool } from '../tools/builtin/bash.js'; import { readFileTool, writeFileTool, listDirTool } from '../tools/builtin/file.js'; import { initDefaultProviders } from '../runtime/model_selection.js'; @Provide() @Controller('/open/netaclaw') export class NetaClawChatController { @Inject() ctx: Context; @Logger() logger: ILogger; @Inject() sessionService: NetaClawSessionService; @Inject() skillLoader: SkillLoaderService; @Post('/chat') async chat(@Body() body: { sessionId?: string; message: string; agentName?: string }) { initDefaultProviders(); const agentName = body.agentName ?? 'default'; const sessionId = await this.sessionService.getOrCreateSession(body.sessionId, agentName); await this.sessionService.saveMessage(sessionId, { role: 'user', content: body.message }); const history = await this.sessionService.loadHistory(sessionId); // 构建系统提示(含 Skill 文档) const skillPrompt = this.skillLoader.getSkillPrompt([]); const systemPrompt = `你是 NetaClaw 电商运营助手,帮助用户管理电商平台的商品、订单和营销活动。${skillPrompt}`; const agentConfig: AgentConfig = { name: agentName, systemPrompt, model: process.env.NETACLAW_MODEL ?? 'anthropic:claude-sonnet-4-20250514', apiKey: process.env.NETACLAW_API_KEY ?? '', maxToolRounds: 20, }; const tools = [bashTool, readFileTool, writeFileTool, listDirTool]; const result = await runAgent({ agentConfig, tools, userMessage: body.message, history: history.slice(0, -1), }); await this.sessionService.saveMessage(sessionId, { role: 'assistant', content: result.finalContent }); return { sessionId, content: result.finalContent, usage: result.usage, toolCallCount: result.toolCallCount, }; } @Post('/skills') async listSkills() { return this.skillLoader.getAllSkills().map(s => ({ name: s.name, description: s.description })); } } ``` - [ ] **Step 4: 创建 Midway.js 模块注册文件** 创建 `packages/backend/src/modules/netaclaw/module.ts`: ```typescript // NetaClaw 模块 - Midway.js 自动扫描加载 // 本文件确保模块被 Cool Admin 框架识别 export default { name: 'netaclaw', description: 'NetaClaw 电商浏览器自动化 Agent 引擎', }; ``` - [ ] **Step 5: 创建示例 Skill** 创建 `skills/hello-world/SKILL.md`: ```markdown --- name: hello-world description: 示例 Skill,用于验证 Skill 加载机制 --- # Hello World Skill ## 触发条件 用户说"你好"或要求测试 Skill 系统 ## 工作流程 1. 向用户问好 2. 列出当前可用的工具 3. 展示一个简单的文件操作示例 ## 规则约束 - 这是一个演示 Skill,不执行任何危险操作 ``` - [ ] **Step 6: 更新 config.default.ts 添加 netaclaw 配置段** 在 `packages/backend/src/config/config.default.ts` 中添加: ```typescript // 在 export default 对象中添加: netaclaw: { defaultModel: 'anthropic:claude-sonnet-4-20250514', apiKeys: { anthropic: process.env.NETACLAW_ANTHROPIC_KEY ?? '', openai: process.env.NETACLAW_OPENAI_KEY ?? '', deepseek: process.env.NETACLAW_DEEPSEEK_KEY ?? '', }, skillsDir: './skills', dataDir: '~/.neta', maxToolRounds: 20, }, ``` - [ ] **Step 7: 验证后端启动** ```bash cd packages/backend && pnpm dev ``` 预期:后端在 8003 端口启动,日志中可见 `[SkillLoader] 已加载 Skill: hello-world`。 测试 HTTP 接口(不需要真实 API Key 也能验证路由注册): ```bash curl -X POST http://localhost:8003/open/netaclaw/skills ``` 预期返回:`[{"name":"hello-world","description":"示例 Skill,用于验证 Skill 加载机制"}]` - [ ] **Step 8: 提交** ```bash git add packages/backend/src/modules/netaclaw/ skills/ packages/backend/src/config/ git commit -m "feat(netaclaw): add module registration, skill loader, HTTP chat controller, and hello-world skill" ``` --- ## Task 8: 端到端验证 **Files:** 无新建文件 - [ ] **Step 1: 设置环境变量** 在项目根目录 `.env` 中添加(使用你的真实 API Key): ```env NETACLAW_MODEL=anthropic:claude-sonnet-4-20250514 NETACLAW_API_KEY=your-anthropic-api-key NETACLAW_ANTHROPIC_KEY=your-anthropic-api-key ``` - [ ] **Step 2: 启动后端并测试对话** ```bash cd packages/backend && pnpm dev ``` 测试基本对话: ```bash curl -X POST http://localhost:8003/open/netaclaw/chat \ -H "Content-Type: application/json" \ -d '{"message": "你好,请列出当前目录的文件"}' ``` 预期:返回 JSON,包含 `sessionId`、`content`(助手回复,应包含目录列表)、`usage`、`toolCallCount`(应 >= 1,因为调用了 list_dir 工具)。 - [ ] **Step 3: 测试会话连续性** 使用上一步返回的 sessionId: ```bash curl -X POST http://localhost:8003/open/netaclaw/chat \ -H "Content-Type: application/json" \ -d '{"sessionId": "<上一步的sessionId>", "message": "刚才列出了哪些文件?"}' ``` 预期:助手能回忆上一轮对话内容。 - [ ] **Step 4: 提交最终状态** ```bash git add -A git commit -m "feat(netaclaw): Phase 1 complete - core agent runtime with tool execution and session management" ```