GPU_GUARD_MONOREPO/docs/superpowers/plans/2026-04-11-netaclaw-phase1-cleanup-core.md

1560 lines
47 KiB
Markdown
Raw Permalink Normal View History

2026-05-20 21:39:12 +08:00
# 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<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: 提交**
```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<LLMResponse>;
}
```
- [ ] **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<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`
```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<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`
```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<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`
```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<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`
```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<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: 提交**
```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<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`
```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<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: 提交**
```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<string, unknown>;
}
```
- [ ] **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<string, unknown>;
}
```
- [ ] **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<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`
```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<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`
```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<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`
```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<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`
```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"
```