GPU_GUARD_MONOREPO/docs/superpowers/plans/2026-04-11-netaclaw-phase1-cleanup-core.md
2026-05-20 21:39:12 +08:00

47 KiB
Raw Blame History

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 目录

rm -rf packages/desktop packages/skills
  • Step 2: 删除旧的 LangChain Agent 及依赖模块
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
packages:
  - 'packages/*'
  • Step 4: 更新根 package.json scripts移除 desktop 相关命令

移除 dev:all 中的 desktop 引用,保留 backend 和 frontend

"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: 验证后端能正常启动
cd packages/backend && pnpm dev

预期:后端在 8003 端口启动,无 import 错误。可能有数据库表缺失警告(旧表),这是正常的。

  • Step 8: 提交
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 模块目录结构

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

import { Type, Static, TSchema } from '@sinclair/typebox';

// --- 工具接口 ---

export type AgentTool<TParams extends TSchema, TResult> = {
  name: string;
  label: string;
  description: string;
  parameters: TParams;
  execute(id: string, params: Static<TParams>): Promise<TResult>;
};

export type AgentToolWithMeta<TParams extends TSchema, TResult> =
  AgentTool<TParams, TResult> & {
    ownerOnly?: boolean;
    displaySummary?: string;
  };

export type AnyAgentTool = AgentToolWithMeta<any, unknown>;

// --- 工具结果 ---

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<string, unknown>, key: string): string {
  const val = params[key];
  if (typeof val !== 'string') throw new ToolInputError(`参数 "${key}" 必须是字符串`);
  return val;
}

export function readOptionalStringParam(params: Record<string, unknown>, 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: 提交
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

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<LLMResponse>;
}
  • Step 2: 创建 Anthropic 提供商

创建 packages/backend/src/modules/netaclaw/plugins/llm_providers/anthropic.ts

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<LLMResponse> {
    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<string, unknown>,
    }));

    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

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<LLMResponse> {
    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<string, unknown> },
    }));

    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

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: 提交
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

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

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<string, LLMProvider> = 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

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<string, unknown>) => 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<AttemptResult> {
  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

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<string, unknown>) => void;
  onToolResult?: (name: string, result: string) => void;
}

export async function runAgent(params: AgentRunParams): Promise<AttemptResult> {
  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: 提交
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

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<typeof BashParams, unknown> = {
  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

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<typeof ReadFileParams, unknown> = {
  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<typeof WriteFileParams, unknown> = {
  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<typeof ListDirParams, unknown> = {
  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: 提交
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

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<string, unknown>;
}
  • Step 2: 创建消息实体

创建 packages/backend/src/modules/netaclaw/entity/message.ts

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<string, unknown>;
}
  • Step 3: 创建消息协议定义

创建 packages/backend/src/modules/netaclaw/gateway/protocol.ts

// --- 客户端 → 服务端 ---

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<string, unknown>;
}

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

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<NetaClawSessionEntity>;

  @InjectEntityModel(NetaClawMessageEntity)
  messageRepo: Repository<NetaClawMessageEntity>;

  async createSession(agentName: string, userId?: string): Promise<string> {
    const sessionId = uuid();
    await this.sessionRepo.save({
      sessionId,
      agentName,
      userId,
      status: 'active',
    });
    return sessionId;
  }

  async getOrCreateSession(sessionId: string | undefined, agentName: string): Promise<string> {
    if (sessionId) {
      const existing = await this.sessionRepo.findOneBy({ sessionId });
      if (existing) return sessionId;
    }
    return this.createSession(agentName);
  }

  async loadHistory(sessionId: string): Promise<LLMMessage[]> {
    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<void> {
    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<void> {
    await this.sessionRepo.update({ sessionId }, { title });
  }
}
  • Step 5: 创建 WebSocket Gateway 服务

创建 packages/backend/src/modules/netaclaw/gateway/server.ts

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: 提交
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

export interface NetaClawConfig {
  // 默认模型(格式: "provider:model"
  defaultModel: string;
  // API Keys按提供商
  apiKeys: Record<string, string>;
  // 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

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<string, unknown>;
  content: string; // SKILL.md 正文(去掉 frontmatter
}

@Provide()
@Scope(ScopeEnum.Singleton)
export class SkillLoaderService {
  @Logger()
  logger: ILogger;

  private skills: Map<string, SkillMeta> = new Map();
  private skillsDir: string;

  @Init()
  async init() {
    this.skillsDir = path.resolve(process.cwd(), 'skills');
    await this.scanSkills();
  }

  async scanSkills(): Promise<void> {
    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

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

// NetaClaw 模块 - Midway.js 自动扫描加载
// 本文件确保模块被 Cool Admin 框架识别
export default {
  name: 'netaclaw',
  description: 'NetaClaw 电商浏览器自动化 Agent 引擎',
};
  • Step 5: 创建示例 Skill

创建 skills/hello-world/SKILL.md

---
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 中添加:

// 在 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: 验证后端启动
cd packages/backend && pnpm dev

预期:后端在 8003 端口启动,日志中可见 [SkillLoader] 已加载 Skill: hello-world

测试 HTTP 接口(不需要真实 API Key 也能验证路由注册):

curl -X POST http://localhost:8003/open/netaclaw/skills

预期返回:[{"name":"hello-world","description":"示例 Skill用于验证 Skill 加载机制"}]

  • Step 8: 提交
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

NETACLAW_MODEL=anthropic:claude-sonnet-4-20250514
NETACLAW_API_KEY=your-anthropic-api-key
NETACLAW_ANTHROPIC_KEY=your-anthropic-api-key
  • Step 2: 启动后端并测试对话
cd packages/backend && pnpm dev

测试基本对话:

curl -X POST http://localhost:8003/open/netaclaw/chat \
  -H "Content-Type: application/json" \
  -d '{"message": "你好,请列出当前目录的文件"}'

预期:返回 JSON包含 sessionIdcontent(助手回复,应包含目录列表)、usagetoolCallCount(应 >= 1因为调用了 list_dir 工具)。

  • Step 3: 测试会话连续性

使用上一步返回的 sessionId

curl -X POST http://localhost:8003/open/netaclaw/chat \
  -H "Content-Type: application/json" \
  -d '{"sessionId": "<上一步的sessionId>", "message": "刚才列出了哪些文件?"}'

预期:助手能回忆上一轮对话内容。

  • Step 4: 提交最终状态
git add -A
git commit -m "feat(netaclaw): Phase 1 complete - core agent runtime with tool execution and session management"