飞书AI机器人agent+MCP+Skill设计与实现

参考

Agent调用MCP服务

飞书聊天机器人事件回调测试

本文记录了从零搭建一个飞书 AI 机器人的完整过程,核心技术栈为 模型 API + MCP 工具调用 + Skill 。项目追求最小依赖、清晰分层、易于扩展。


整体思路

想做一件事:在飞书群里@机器人,它能用 AI 回复,还能调用外部工具(如查数据库、控制硬件)

拆解下来有三个核心问题:

  1. 怎么收发飞书消息? → 飞书开放平台 WebSocket 长连接
  2. 怎么让 AI 调用工具? → Claude 的 tool_use + MCP 协议
  3. 怎么切换 AI 的角色/行为? → Skill 文件系统(.md 文件定义人设)

最终形成这样一条数据流:

bash 复制代码
飞书消息 → FeishuClient → AgentRunner → 模型  API
                                ↕
                              McpClient → MCP Server(工具)

项目结构

复制代码
PS D:\workspace\gitee\0\ming_agent> tree /f
卷 新加卷 的文件夹 PATH 列表
卷序列号为 1E8A-2CFF
ming_agent/
│
│  .gitignore                # Git 忽略文件(node_modules 等)
│  package.json              # 项目依赖与启动脚本
│  index.html                # 项目说明页(可选)
│  init_readme.md            # 初始化说明
│  readme.md                 # 项目文档
│
├─extlib/                    # 底层 MCP 通信库(框架层,一般不需要改动)
│      BaseMcpServer.js      # MCP Server 基类
│      StdioMcpServer.js     # 基于 stdio 的 MCP Server 实现
│      StdioMcpClient.js     # 基于 stdio 的 MCP Client 实现
│      SseMcpServer.js       # 基于 SSE 的 MCP Server 实现
│      StreamableHttpMcpServer.js  # 基于 Streamable HTTP 的 MCP Server 实现
│      ming_node_init.js     # 初始化工具函数
│
├─mcpserver/                  # MCP 工具注册层(业务工具在这里扩展)
│      mcp_server.js           # 注册具体工具:加法、设置转速等
│      light_mcp_server.js  # 注册具体工具:开关灯
├─skills/                    # Skill 定义文件夹,每个 .md 文件是一个 AI 角色
│      main.md               # 默认 skill(通用助手)
│      joker.md              # 冷笑话测试 skill
│
├─src/                       # Agent 核心源码
│  │  index.js               # 程序入口:串联 Skill / MCP / Agent / 飞书四个模块
│  │  test-skill.js          # 本地测试脚本:不启动飞书,直接在终端跑 Agent
│  │
│  ├─agent/
│  │      runner.js          # agent  Loop:驱动"思考→调工具→再思考"循环
│  │
│  ├─config/
│  │      index.js           # 全局配置中心:所有参数集中在此,支持环境变量覆盖
│  │
│  ├─feishu/
│  │      client.js          # 飞书客户端:WebSocket 收消息 + 消息卡片回复
│  │
│  ├─mcp/
│  │      client.js          # MCP 客户端:连接 Server、拉取工具列表、代理工具调用
│  │
│  ├─skills/
│  │      manager.js         # Skill 加载器:读取 .md 文件,解析 frontmatter 和 system prompt
│  │
│  └─utils/
│         logger.js          # 分级日志:debug / info / warn / error,支持颜色和时间戳
│
└─test/                      # 参考示例(开发调试用,不进生产)
       anthropic_agent.js    # 原始 Claude Agent 示例
       feishu_client.js      # 原始飞书接入示例
       openai_agent.js       # OpenAI 兼容接口示例

依赖 package.json

json 复制代码
{
  "name": "ming_agent",
  "version": "1.0.0",
  "main": "feishu.js",
  "type": "module",
  "scripts": {
    "start": "node src/index.js",
    "test:skill": "node test-skill.js"
  },
  "author": "",
  "license": "ISC",
  "description": "",
  "dependencies": {
    "@anthropic-ai/sdk": "^0.78.0",
    "@larksuiteoapi/node-sdk": "^1.59.0",
    "@modelcontextprotocol/sdk": "^1.27.1",
    "ming_node": "^3.0.5",
    "openai": "^6.27.0",
    "zod": "^4.3.6"
  }
}

配置中心 src/config/index.js

js 复制代码
/**
 * ╔══════════════════════════════════════════════════════╗
 * ║                   全局配置中心                         ║
 * ║  所有可调参数集中在此,业务代码不再散落硬编码              ║
 * ╚══════════════════════════════════════════════════════╝
 *
 * 优先级:环境变量 > 此文件默认值
 * 敏感信息建议通过 .env 注入,不要提交到 Git
 */

import path from "path";

export const CONFIG = {

    // ══════════════════════════════════════════
    //  日志
    // ══════════════════════════════════════════
    log: {
        /** debug < info < warn < error;生产建议 "info" */
        level:     process.env.LOG_LEVEL ?? "debug",
        color:     process.env.LOG_COLOR !== "false",
        timestamp: true,
    },

    // ══════════════════════════════════════════
    //  API 风格选择
    //  "anthropic" → 使用 Claude 原生 API
    //  "openai"    → 使用 OpenAI 兼容 API(GLM / DeepSeek / Qwen 等均可)
    // ══════════════════════════════════════════
    apiStyle: process.env.API_STYLE ?? "openai",

    // ══════════════════════════════════════════
    //  Anthropic / Claude 配置
    // ══════════════════════════════════════════
    anthropic: {
        apiKey:      process.env.ANTHROPIC_API_KEY  ?? "",
        baseURL:     process.env.ANTHROPIC_BASE_URL ?? "https://api.longcat.chat/anthropic",
        model:       process.env.ANTHROPIC_MODEL    ?? "LongCat-Flash-Chat",
        maxTokens:   1024,
        temperature: 1.0,
        // topP:        0.9,
        // toolChoice:  "auto",
    },

    // ══════════════════════════════════════════
    //  OpenAI 兼容配置(apiStyle = "openai" 时生效)
    //  兼容任何支持 OpenAI 格式的服务:GLM / DeepSeek / Qwen / Ollama ...
    // ══════════════════════════════════════════
    openai: {
        apiKey:      process.env.OPENAI_API_KEY  ?? "",
        baseURL:     process.env.OPENAI_BASE_URL ?? "https://api.edgefn.net/v1",
        model:       process.env.OPENAI_MODEL    ?? "GLM-4.7",
        maxTokens:   1024,
        temperature: 1.0,
        // topP:        0.9,
    },

    // ══════════════════════════════════════════
    //  MCP Server 配置(支持同时连接多个)
    //
    //  每个 server 独立配置 transport,三选一:
    //    "stdio"           本地子进程(默认,开发常用)
    //    "sse"             旧版 HTTP+SSE(兼容旧服务端)
    //    "streamable-http" 新版推荐远程协议(SDK v1.10+)
    //
    //  所有 server 的工具会自动合并,工具名重复时后者覆盖前者
    // ══════════════════════════════════════════
    mcpServers: [

        // ── 本地工具服务(stdio)────────────────
        {
            name:      "local",             // 标识名,仅用于日志
            transport: "stdio",
            command:   "node",
            args:    ["D:\\workspace\\gitee\\0\\ming_agent\\mcpserver\\mcp_server.js"],
        },
        // npx 启动的第三方 MCP 包
        {
            name:      "filesystem",
            transport: "stdio",
            command:   "npx",
            args:      ["-y", "@modelcontextprotocol/server-filesystem", "E:\\agent_test"],
        },
        // ── 联网(fetch 网页内容)────────────────
        // {
        //     name:      "fetch",
        //     transport: "stdio",
        //     command:   "npx",
        //     args:      ["-y", "fetch-mcp"],
        // },
        // ── 远程工具服务(streamable-http)──────
        // {
        //   name:      "remote",
        //   transport: "streamable-http",
        //   url:       "http://192.168.1.10:3000/mcp",
        //   headers:   { "Authorization": "Bearer your_token" },
        // },
        // ── 旧版 SSE 服务 ────────────────────────
        // {
        //   name:      "legacy",
        //   transport: "sse",
        //   url:       "http://192.168.1.10:3000/sse",
        // },

    ],
    // ══════════════════════════════════════════
    //  飞书
    // ══════════════════════════════════════════
    feishu: {
        appId:            process.env.FEISHU_APP_ID     ?? "",
        appSecret:        process.env.FEISHU_APP_SECRET ?? "",
        replyTitlePrefix: "🤖 助手回复",
        logIncoming:      true,
    },

    // ══════════════════════════════════════════
    //  Skill
    // ══════════════════════════════════════════
    skillsDir:   path.resolve(process.cwd(), "skills"),
    activeSkill: process.env.ACTIVE_SKILL ?? "main",
};

优先级:环境变量 > 配置文件默认值 。敏感信息(Key、Secret)通过 .env 注入,不提交到 Git。


日志工具 src/utils/logger.js

js 复制代码
/**
 * ══════════════════════════════════════════════════════
 *  Logger --- 轻量级日志工具
 * ══════════════════════════════════════════════════════
 *
 * 特性:
 *  - 四级日志:debug / info / warn / error
 *  - 可配置颜色、时间戳
 *  - 运行时动态修改级别:logger.setLevel("warn")
 *  - 通过 CONFIG.log 统一控制,无需散落 console.log
 */

import { CONFIG } from "../config/index.js";

// ANSI 终端颜色码
const COLORS = {
    reset:  "\x1b[0m",
    gray:   "\x1b[90m",
    cyan:   "\x1b[36m",
    green:  "\x1b[32m",
    yellow: "\x1b[33m",
    red:    "\x1b[31m",
};

// 日志级别权重(数字越大越严重)
const LEVEL_WEIGHT = { debug: 0, info: 1, warn: 2, error: 3 };

// 各级别对应样式
const LEVEL_STYLE = {
    debug: { label: "DEBUG", color: COLORS.gray   },
    info:  { label: "INFO ", color: COLORS.cyan   },
    warn:  { label: "WARN ", color: COLORS.yellow },
    error: { label: "ERROR", color: COLORS.red    },
};

class Logger {
    constructor(cfg = CONFIG.log) {
        this._level     = cfg.level     ?? "debug";
        this._color     = cfg.color     ?? true;
        this._timestamp = cfg.timestamp ?? true;
    }

    /** 运行时动态修改日志级别 */
    setLevel(level) {
        if (!(level in LEVEL_WEIGHT)) throw new Error(`无效日志级别: ${level}`);
        this._level = level;
    }

    _shouldPrint(level) {
        return LEVEL_WEIGHT[level] >= LEVEL_WEIGHT[this._level];
    }

    _format(level, parts) {
        const style = LEVEL_STYLE[level];
        const ts    = this._timestamp ? `${COLORS.gray}${new Date().toISOString()}${COLORS.reset} ` : "";
        const tag   = this._color
            ? `${style.color}[${style.label}]${COLORS.reset}`
            : `[${style.label}]`;

        return `${ts}${tag} ${parts.join(" ")}`;
    }

    _print(level, ...args) {
        if (!this._shouldPrint(level)) return;
        const line = this._format(level, args.map(a =>
            typeof a === "object" ? JSON.stringify(a, null, 2) : String(a)
        ));
        if (level === "error") {
            process.stderr.write(line + "\n");
        } else {
            process.stdout.write(line + "\n");
        }
    }

    debug(...args) { this._print("debug", ...args); }
    info (...args) { this._print("info",  ...args); }
    warn (...args) { this._print("warn",  ...args); }
    error(...args) { this._print("error", ...args); }

    /** 打印醒目的分隔线,方便区分对话轮次 */
    divider(char = "─", len = 52) {
        if (this._shouldPrint("debug")) {
            process.stdout.write(char.repeat(len) + "\n");
        }
    }
}

// 全局单例,所有模块共享同一个 logger
export const logger = new Logger();

日志分级控制

bash 复制代码
LOG_LEVEL=warn npm start   # 生产:只看警告和错误
LOG_LEVEL=debug npm start  # 开发:看所有细节

输出效果:

复制代码
2026-03-13T08:17:22.008Z [INFO ] [MCP] 连接成功,可用工具:加法, 设置转速
2026-03-13T08:17:22.009Z [INFO ] [Agent] 👤 用户: 帮我算 4+3  (skill: joker)
2026-03-13T08:17:22.010Z [DEBUG] [Agent] 请求 Claude(messages=1 条,skill=joker)

Skill 加载器 src/skills/manager.js

js 复制代码
/**
 * ══════════════════════════════════════════════════════
 *  SkillManager --- 从 .md 文件加载 Skill
 * ══════════════════════════════════════════════════════
 *
 * Skill 文件格式(标准 SKILL.md):
 *
 *   ---
 *   name: joker
 *   description: 冷笑话段子手
 *   temperature: 1.0
 *   maxTokens: 256
 *   ---
 *
 *   # 标题
 *   正文即 system prompt,支持完整 Markdown
 *
 * 加载规则:
 *   - YAML frontmatter 提供元数据(name/description/temperature/maxTokens 等)
 *   - frontmatter 之后的全部 Markdown 文本作为 system prompt
 *   - 未在 frontmatter 中声明的字段继承 CONFIG.anthropic 全局默认值
 */

import fs   from "fs";
import path from "path";
import { CONFIG } from "../config/index.js";
import { logger  } from "../utils/logger.js";

export class SkillManager {
    /**
     * @param {string} [skillsDir]  skill 文件夹路径,默认读取 CONFIG.skillsDir
     */
    constructor(skillsDir) {
        this._dir    = skillsDir ?? CONFIG.skillsDir;
        this._active = this._load(CONFIG.activeSkill);
    }

    // ──────────────────────────────────────────
    //  公共接口
    // ──────────────────────────────────────────

    /** 返回当前激活的 skill(已合并全局默认值) */
    getActive() { return this._active; }

    /** 列出 skillsDir 下所有 .md 文件的 name + description */
    list() {
        return fs.readdirSync(this._dir)
            .filter(f => f.endsWith(".md"))
            .map(f => {
                try {
                    const s = this._load(path.basename(f, ".md"));
                    return { name: s.name, description: s.description, active: s.name === this._active.name };
                } catch { return null; }
            })
            .filter(Boolean);
    }

    // ──────────────────────────────────────────
    //  私有:加载并解析单个 skill 文件
    // ──────────────────────────────────────────

    _load(skillName) {
        const filePath = path.resolve(this._dir, `${skillName}.md`);

        if (!fs.existsSync(filePath)) {
            throw new Error(`[SkillManager] 找不到 skill 文件: ${filePath}`);
        }

        const raw              = fs.readFileSync(filePath, "utf-8");
        const { meta, system } = this._parse(raw);

        // frontmatter 字段优先,全局配置兜底
        const global   = CONFIG.anthropic;
        const resolved = {
            name:        meta.name        ?? skillName,
            description: meta.description ?? "(无描述)",
            system:      system.trim(),
            maxTokens:   meta.maxTokens   ?? global.maxTokens,
            temperature: meta.temperature ?? global.temperature,
            ...(meta.topP          !== undefined && { topP:          meta.topP          }),
            ...(meta.topK          !== undefined && { topK:          meta.topK          }),
            ...(meta.stopSequences !== undefined && { stopSequences: meta.stopSequences }),
            ...(meta.toolChoice    !== undefined && { toolChoice:    meta.toolChoice    }),
        };

        logger.info(
            `[Skill] 已加载 "${resolved.name}"(${resolved.description})` +
            ` temperature=${resolved.temperature} maxTokens=${resolved.maxTokens}`
        );
        return resolved;
    }

    /**
     * 解析 SKILL.md 文本
     * @returns {{ meta: object, system: string }}
     */
    _parse(raw) {
        // 匹配 --- frontmatter ---
        const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
        if (!fmMatch) {
            // 没有 frontmatter,整个文件当 system prompt
            return { meta: {}, system: raw };
        }

        const meta   = this._parseYaml(fmMatch[1]);
        const system = fmMatch[2];
        return { meta, system };
    }

    /**
     * 极简 YAML 解析(只支持 key: value 单层结构)
     * 如需数组/嵌套,可替换为 js-yaml 库
     */
    _parseYaml(text) {
        const meta = {};
        for (const line of text.split("\n")) {
            const m = line.match(/^(\w+):\s*(.+)$/);
            if (!m) continue;
            const [, key, val] = m;
            // 自动类型转换:数字 / 布尔 / 字符串
            if (!isNaN(val))               meta[key] = Number(val);
            else if (val === "true")        meta[key] = true;
            else if (val === "false")       meta[key] = false;
            else                            meta[key] = val.trim();
        }
        return meta;
    }
}

Skill 文件格式

采用标准 SKILL.md 格式:YAML frontmatter 放参数,正文是 system prompt。

markdown 复制代码
---
name: joker
description: 冷笑话段子手,专门讲让人沉默三秒的冷笑话
temperature: 1.0
maxTokens: 256
---

# 冷笑话段子手

你是一个专门讲冷笑话的段子手。

## 规则

- 每次只讲一个笑话,不超过 4 句话
- 笑话要足够冷,冷到让人沉默 3 秒才反应过来
- 结尾加一个 emoji 表情
- 用中文

frontmatter 支持的字段:

字段 类型 说明
name string skill 标识名
description string 描述(仅用于日志)
temperature number 随机性 0~1
maxTokens number 最大输出 token
topP number 核采样(可选)
toolChoice string 工具选择策略(可选)

未声明的字段自动继承 CONFIG.anthropic 全局默认值。

切换 Skill

bash 复制代码
# 方式一:改 config 里的 activeSkill 字段
activeSkill: "translator"

# 方式二:环境变量(临时切换,不改文件)
ACTIVE_SKILL=coder npm start

新增一个 skill 只需在 skills/ 目录下新建一个 .md 文件,零改动业务代码


MCP 工具调用 src/mcp/client.js

js 复制代码
/**
 * ══════════════════════════════════════════════════════
 *  MCP Client --- 同时连接多个 MCP Server
 * ══════════════════════════════════════════════════════
 *
 * 支持同时连接 CONFIG.mcpServers 中配置的所有服务:
 *   - 所有服务并行连接,加快启动速度
 *   - 工具列表自动合并,工具名重复时后者覆盖前者(配置顺序决定优先级)
 *   - 工具调用时自动路由到注册该工具的 server
 *   - 任意一个 server 断开不影响其他 server 的工具继续使用
 *
 * transport 三选一(每个 server 独立配置):
 *   "stdio"           本地子进程
 *   "sse"             旧版 HTTP+SSE
 *   "streamable-http" 新版推荐远程协议
 */

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport }          from "@modelcontextprotocol/sdk/client/stdio.js";
import { SSEClientTransport }            from "@modelcontextprotocol/sdk/client/sse.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { CONFIG } from "../config/index.js";
import { logger  } from "../utils/logger.js";

export class McpClient {
    constructor() {
        /** @type {Array<{ name: string, client: Client }>} 已连接的 server 列表 */
        this._connections = [];

        /** @type {Map<string, Client>} 工具名 → 所属 client 的路由表 */
        this._toolRouter  = new Map();

        /** @type {Array<ClaudeTool>} 合并后的工具列表 */
        this._tools       = [];
    }

    // ──────────────────────────────────────────
    //  连接所有 Server
    // ──────────────────────────────────────────

    async connect() {
        const servers = CONFIG.mcpServers;
        if (!servers?.length) throw new Error("[MCP] mcpServers 配置为空");

        logger.info(`[MCP] 开始连接 ${servers.length} 个 Server...`);

        // 并行连接所有 server
        const results = await Promise.allSettled(
            servers.map(cfg => this._connectOne(cfg))
        );

        // 统计成功/失败
        results.forEach((r, i) => {
            if (r.status === "rejected") {
                logger.error(`[MCP] Server "${servers[i].name}" 连接失败:`, r.reason?.message);
            }
        });

        if (this._connections.length === 0) {
            throw new Error("[MCP] 所有 Server 均连接失败,无法继续");
        }

        logger.info(
            `[MCP] 已连接 ${this._connections.length}/${servers.length} 个 Server,` +
            `共 ${this._tools.length} 个工具:${this._tools.map(t => t.name).join(", ")}`
        );
    }

    // ──────────────────────────────────────────
    //  工具列表 & 调用
    // ──────────────────────────────────────────

    /** 返回合并后的工具列表(Claude API 格式) */
    getTools() { return this._tools; }

    /**
     * 调用工具,自动路由到注册该工具的 server
     * @param {string} name
     * @param {object} args
     * @returns {Promise<string>}
     */
    async callTool(name, args) {
        const client = this._toolRouter.get(name);
        if (!client) throw new Error(`[MCP] 未找到工具 "${name}",请检查 server 是否已注册`);

        logger.debug(`[MCP] → ${name}`, args);
        const result = await client.callTool({ name, arguments: args });
        const text   = result?.content?.[0]?.text ?? "";
        logger.debug(`[MCP] ← ${text}`);
        return text;
    }

    // ──────────────────────────────────────────
    //  关闭所有连接
    // ──────────────────────────────────────────

    async close() {
        await Promise.allSettled(
            this._connections.map(({ name, client }) =>
                client.close().then(() => logger.info(`[MCP] "${name}" 已关闭`))
            )
        );
        this._connections = [];
        this._toolRouter.clear();
        this._tools = [];
    }

    // ──────────────────────────────────────────
    //  私有:连接单个 Server 并注册其工具
    // ──────────────────────────────────────────

    async _connectOne(cfg) {
        const { name } = cfg;
        logger.info(`[MCP] 连接 "${name}"(${cfg.transport})...`);

        const transport = this._createTransport(cfg);
        const client    = new Client(
            { name: `feishu-claude-agent/${name}`, version: "1.0.0" },
            { capabilities: {} },
        );

        await client.connect(transport);

        // 拉取工具列表
        const { tools } = await client.listTools();
        logger.info(`[MCP] "${name}" 工具:${tools.map(t => t.name).join(", ")}`);

        // 注册到路由表(同名工具后者覆盖前者)
        for (const tool of tools) {
            if (this._toolRouter.has(tool.name)) {
                logger.warn(`[MCP] 工具 "${tool.name}" 已存在,由 "${name}" 覆盖`);
            }
            this._toolRouter.set(tool.name, client);
        }

        this._connections.push({ name, client });

        // 重新生成合并后的工具列表(保持与路由表一致)
        this._tools = [...this._toolRouter.entries()].map(([, c]) => {
            // 从对应 server 的工具原始数据重新构造
            return tools
                .filter(t => this._toolRouter.get(t.name) === c)
                .map(t => ({ name: t.name, description: t.description, input_schema: t.inputSchema }));
        }).flat();

        // 更完整的合并:重新从所有已连接 server 汇总
        await this._rebuildToolList();
    }

    /**
     * 根据路由表重建合并工具列表
     * 遍历所有已连接 server,按路由表中存活的工具重建
     */
    async _rebuildToolList() {
        const all = [];
        for (const { client } of this._connections) {
            const { tools } = await client.listTools();
            for (const t of tools) {
                // 只保留路由表中指向当前 client 的工具(避免被覆盖的重复项)
                if (this._toolRouter.get(t.name) === client) {
                    all.push({ name: t.name, description: t.description, input_schema: t.inputSchema });
                }
            }
        }
        this._tools = all;
    }

    // ──────────────────────────────────────────
    //  私有:按配置创建 Transport
    // ──────────────────────────────────────────

    _createTransport(cfg) {
        const { transport, command, args, url, headers = {} } = cfg;

        switch (transport) {
            case "stdio":
                return new StdioClientTransport({ command, args });

            case "sse":
                return new SSEClientTransport(new URL(url), { headers });

            case "streamable-http":
                return new StreamableHTTPClientTransport(new URL(url), { headers });

            default:
                throw new Error(
                    `[MCP] "${cfg.name}" 不支持的 transport: "${transport}",` +
                    `可选值:stdio | sse | streamable-http`
                );
        }
    }
}

MCP 服务 mcpserver/mcp_server.js

本文不展开可参考 express风格的mcpServer)注册mcp工具

js 复制代码
import { z } from 'zod';
import M from "ming_node";
import MyMcpServer from "../extlib/StdioMcpServer.js";
const app=new MyMcpServer("my_mcp_server");

app.listen(3000);
app.begin((req,res)=>{
    M.log("开始执行",req.mcpName,JSON.stringify(req.params));
})

app.end((req,res)=>{
    M.log("执行完成",req.mcpName,res.result);
})

app.get("加法",{
    a:z.number(),
    b:z.number(),
},async (req,res)=>{
    const {a,b}=req.params;
    const c=a+b+9;
    res.send(c);
});

app.get("设置转速",{ speed:z.number()},async (req,res)=>{
    const {speed}=req.params;
    M.log(`转速设置为 ${speed}`);
    res.send("转速设置完成");
});

MCP 服务 mcpserver/light_mcp_server.js

控制开关灯的mcp

js 复制代码
/**
 * 开关灯 MCP Server --- Streamable HTTP
 *
 * 依赖:
 *   npm install @modelcontextprotocol/sdk zod express
 *
 * 启动:
 *   node light_mcp_server.js
 *
 * Agent config 加入:
 *   { name: "light", transport: "streamable-http", url: "http://localhost:3100/mcp" }
 *
 * **飞书直接说**
 *
 * 打开客厅的灯
 * 关掉卧室灯
 * 所有房间灯的状态
 */

import express        from "express";
import { McpServer }  from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { z } from "zod";

const PORT = 3100;

// ── 灯的状态(内存,重启后重置)──────────────
const lights = {};  // { [room]: true/false }

// ── MCP Server 定义 ──────────────────────────
const server = new McpServer({
    name:    "light-server",
    version: "1.0.0",
});

// 开灯
server.tool(
    "turn_on_light",
    "开启指定房间的灯",
    { room: z.string().describe("房间名称,例如:客厅、卧室、厨房") },
    async ({ room }) => {
        lights[room] = true;
        console.log(`[灯控] ${room} 开灯`);
        return { content: [{ type: "text", text: `${room}的灯已开启 💡` }] };
    }
);

// 关灯
server.tool(
    "turn_off_light",
    "关闭指定房间的灯",
    { room: z.string().describe("房间名称,例如:客厅、卧室、厨房") },
    async ({ room }) => {
        lights[room] = false;
        console.log(`[灯控] ${room} 关灯`);
        return { content: [{ type: "text", text: `${room}的灯已关闭 🌙` }] };
    }
);

// 查询状态
server.tool(
    "get_light_status",
    "查询灯的状态,不传 room 则返回所有房间",
    { room: z.string().describe("房间名称,不填则查询所有房间").optional() },
    async ({ room }) => {
        if (room) {
            const s = lights[room];
            const text = s === undefined
                ? `${room} 尚未操作过`
                : `${room}:${s ? "开启 💡" : "关闭 🌙"}`;
            return { content: [{ type: "text", text }] };
        }
        const all = Object.entries(lights);
        const text = all.length === 0
            ? "暂无灯控记录"
            : all.map(([r, s]) => `${r}:${s ? "开 💡" : "关 🌙"}`).join("\n");
        return { content: [{ type: "text", text }] };
    }
);

// ── HTTP Server ──────────────────────────────
const app = express();
app.use(express.json());

// 每个请求创建独立 transport(无状态模式,最简单)
app.post("/mcp", async (req, res) => {
    const transport = new StreamableHTTPServerTransport({
        sessionIdGenerator: undefined, // 无状态
    });
    res.on("close", () => transport.close());
    await server.connect(transport);
    await transport.handleRequest(req, res, req.body);
});

// GET /mcp 不支持
app.get("/mcp", (_req, res) => {
    res.status(405).json({ error: "Method Not Allowed" });
});

app.listen(PORT, () => {
    console.log(`[灯控 MCP] 已启动,监听 http://localhost:${PORT}/mcp`);
});

Agent 主循环 src/agent/runner.js

Agent 的核心是一个 while 循环:Claude 思考 → 决定调工具 → 拿到结果 → 继续思考 → 输出最终答案

js 复制代码
/**
 * ══════════════════════════════════════════════════════
 *  Agent Runner --- 支持 Anthropic / OpenAI 双风格
 * ══════════════════════════════════════════════════════
 *
 * 通过 CONFIG.apiStyle 选择后端:
 *   "anthropic" → Claude 原生 API(tool_use / tool_result)
 *   "openai"    → OpenAI 兼容 API(tool_calls / tool 消息)
 *                 兼容 GLM / DeepSeek / Qwen / Ollama 等任意兼容服务
 *
 * 对外接口保持不变:runner.run(userMessage) → Promise<string>
 * 切换风格只需改 CONFIG.apiStyle,上层代码零修改。
 */

import Anthropic from "@anthropic-ai/sdk";
import OpenAI    from "openai";
import { CONFIG } from "../config/index.js";
import { logger  } from "../utils/logger.js";

export class AgentRunner {
    /**
     * @param {import('../mcp/client.js').McpClient}        mcpClient
     * @param {import('../skills/manager.js').SkillManager} skillManager
     */
    constructor(mcpClient, skillManager) {
        this._mcp          = mcpClient;
        this._skillManager = skillManager;
        this._style        = CONFIG.apiStyle;   // "anthropic" | "openai"
        this._client       = this._createClient();

        logger.info(`[Agent] API 风格: ${this._style}`);
    }

    // ──────────────────────────────────────────
    //  公共接口
    // ──────────────────────────────────────────

    /**
     * 处理一条用户消息,返回最终纯文本回复
     * @param {string} userMessage
     * @returns {Promise<string>}
     */
    async run(userMessage) {
        const skill = this._skillManager.getActive();

        logger.divider();
        logger.info(`[Agent] 👤 用户: ${userMessage}  (skill: ${skill.name})`);
        logger.divider();

        return this._style === "openai"
            ? this._runOpenAI(userMessage, skill)
            : this._runAnthropic(userMessage, skill);
    }

    // ══════════════════════════════════════════
    //  Anthropic 风格 Agentic Loop
    // ══════════════════════════════════════════

    async _runAnthropic(userMessage, skill) {
        const messages = [{ role: "user", content: userMessage }];
        const tools    = this._mcp.getTools(); // { name, description, input_schema }

        while (true) {
            const cfg    = CONFIG.anthropic;
            const params = {
                model:       cfg.model,
                max_tokens:  skill.maxTokens,
                system:      skill.system,       // Anthropic 独有:顶层 system 字段
                temperature: skill.temperature,
                messages,
                tools,
                ...(skill.topP          !== undefined && { top_p:          skill.topP          }),
                ...(skill.topK          !== undefined && { top_k:          skill.topK          }),
                ...(skill.stopSequences !== undefined && { stop_sequences: skill.stopSequences }),
                ...(skill.toolChoice    !== undefined && { tool_choice:    skill.toolChoice    }),
            };

            logger.debug(`[Agent/Anthropic] 请求(messages=${messages.length} 条)`);
            const response = await this._client.messages.create(params);

            // ① 正常结束
            if (response.stop_reason === "end_turn") {
                const text = response.content.find(b => b.type === "text")?.text ?? "";
                logger.info(`[Agent] 🤖 ${text}`);
                return text;
            }

            // ② 调工具
            if (response.stop_reason === "tool_use") {
                messages.push({ role: "assistant", content: response.content });

                // 执行工具,结果格式:[{ type:"tool_result", tool_use_id, content }]
                const toolResults = await this._execToolsAnthropic(response.content);
                messages.push({ role: "user", content: toolResults });
                continue;
            }

            // ③ 兜底(max_tokens 等)
            const fallback = response.content.find(b => b.type === "text")?.text ?? "(无响应)";
            logger.warn(`[Agent/Anthropic] stop_reason=${response.stop_reason}`);
            return fallback;
        }
    }

    /**
     * 执行 Anthropic 风格的工具调用
     * 结果追加为 { type:"tool_result", tool_use_id, content } 块
     */
    async _execToolsAnthropic(contentBlocks) {
        const results = [];
        for (const block of contentBlocks.filter(b => b.type === "tool_use")) {
            const text = await this._callTool(block.name, block.input);
            results.push({ type: "tool_result", tool_use_id: block.id, content: text });
        }
        return results;
    }

    // ══════════════════════════════════════════
    //  OpenAI 风格 Agentic Loop
    // ══════════════════════════════════════════

    async _runOpenAI(userMessage, skill) {
        const cfg = CONFIG.openai;

        // OpenAI 的 system 通过消息数组第一条传入
        const messages = [
            { role: "system", content: skill.system },
            { role: "user",   content: userMessage  },
        ];

        // MCP 工具格式 → OpenAI function 格式
        const tools = this._mcp.getTools().map(t => ({
            type: "function",
            function: {
                name:        t.name,
                description: t.description,
                parameters:  t.input_schema,  // 字段名不同:input_schema → parameters
            },
        }));

        while (true) {
            const params = {
                model:       cfg.model,
                max_tokens:  skill.maxTokens,
                temperature: skill.temperature,
                messages,
                tools,
                tool_choice: skill.toolChoice ?? "auto",
                ...(skill.topP !== undefined && { top_p: skill.topP }),
            };

            logger.debug(`[Agent/OpenAI] 请求(messages=${messages.length} 条)`);
            const response = await this._client.chat.completions.create(params);

            const choice       = response.choices[0];
            const message      = choice.message;
            const finishReason = choice.finish_reason;

            // ① 正常结束
            if (finishReason === "stop") {
                const text = message.content ?? "";
                logger.info(`[Agent] 🤖 ${text}`);
                return text;
            }

            // ② 调工具
            if (finishReason === "tool_calls") {
                messages.push(message); // 追加 assistant 消息(含 tool_calls)

                // 执行工具,结果追加为 { role:"tool", tool_call_id, content } 消息
                const toolResults = await this._execToolsOpenAI(message.tool_calls);
                messages.push(...toolResults);
                continue;
            }

            // ③ 兜底(length 等)
            logger.warn(`[Agent/OpenAI] finish_reason=${finishReason}`);
            return message.content ?? "(无响应)";
        }
    }

    /**
     * 执行 OpenAI 风格的工具调用
     * 结果追加为 { role:"tool", tool_call_id, content } 消息
     */
    async _execToolsOpenAI(toolCalls) {
        const results = [];
        for (const call of toolCalls) {
            const name  = call.function.name;
            const input = JSON.parse(call.function.arguments);
            const text  = await this._callTool(name, input);
            results.push({ role: "tool", tool_call_id: call.id, content: text });
        }
        return results;
    }

    // ══════════════════════════════════════════
    //  公共:执行单次工具调用(两种风格共用)
    // ══════════════════════════════════════════

    async _callTool(name, input) {
        logger.info(`[Agent] 🔧 调用工具: ${name}`, input);
        let text;
        try {
            text = await this._mcp.callTool(name, input);
        } catch (err) {
            logger.error(`[Agent] 工具调用失败: ${name}`, err.message);
            text = `工具调用出错: ${err.message}`;
        }
        logger.info(`[Agent] ✅ 工具结果: ${text}`);
        return text;
    }

    // ══════════════════════════════════════════
    //  初始化客户端
    // ══════════════════════════════════════════

    _createClient() {
        if (this._style === "openai") {
            const { apiKey, baseURL } = CONFIG.openai;
            logger.debug(`[Agent] OpenAI 客户端 baseURL=${baseURL}`);
            return new OpenAI({ apiKey, baseURL });
        }

        // 默认 Anthropic
        const { apiKey, baseURL } = CONFIG.anthropic;
        logger.debug(`[Agent] Anthropic 客户端 baseURL=${baseURL}`);
        return new Anthropic({
            apiKey,
            baseURL,
            defaultHeaders: {
                "Authorization": `Bearer ${apiKey}`,
                "x-api-key":     apiKey,
            },
        });
    }
}

飞书接入 src/feishu/client.js

忽略历史消息,忽略重复触发的消息

js 复制代码
/**
 * ══════════════════════════════════════════════════════
 *  Feishu Client --- 飞书 WebSocket 长连接客户端
 * ══════════════════════════════════════════════════════
 *
 * 职责:
 *  1. 建立飞书 WebSocket 长连接,监听 im.message.receive_v1 事件
 *  2. 消息去重:同一条消息只处理一次(防飞书网络抖动重推)
 *  3. 解析消息,提取文本内容和 chat_id
 *  4. 将消息文本交给 onMessage 回调(由外部注入 Agent 逻辑)
 *  5. 将回复以消息卡片形式发回飞书
 *
 * 设计原则:
 *  - Feishu 层不感知 Agent 细节,只负责 I/O
 *  - 通过 onMessage 钩子解耦,方便单元测试和替换
 */

import * as Lark from "@larksuiteoapi/node-sdk";
import { CONFIG } from "../config/index.js";
import { logger } from "../utils/logger.js";

export class FeishuClient {
    /**
     * @param {(text: string, ctx: MessageContext) => Promise<string>} onMessage
     *   收到用户消息时的回调,接收消息文本,返回待发送的回复文本
     */
    constructor(onMessage) {
        this._onMessage = onMessage;

        const { appId, appSecret } = CONFIG.feishu;

        // HTTP 客户端(用于主动发消息)
        this._client = new Lark.Client({ appId, appSecret });

        // WebSocket 客户端(用于接收事件推送)
        this._wsClient = new Lark.WSClient({
            appId,
            appSecret,
            // 将 Lark SDK 自身日志映射到我们的 logger
            loggerLevel: this._larkLogLevel(),
        });

        /** 启动时间戳(ms),早于此时间的消息一律丢弃 */
        this._startTime = Date.now();

        /**
         * 去重层1:message_id 级别
         * 防止飞书重推同一条事件(message_id 相同)
         */
        this._handledIds = new Set();

        /**
         * 去重层2:内容级别 Map<chat_id, text>
         * 只记录每个会话上一条内容,连续发相同内容才过滤
         * 中间插了别的内容则不算重复,正常处理
         */
        this._lastText = new Map();
    }

    // ──────────────────────────────────────────
    //  启动监听
    // ──────────────────────────────────────────

    /**
     * 启动 WebSocket 长连接,开始接收飞书消息
     */
    start() {
        logger.info("[Feishu] 正在连接 WebSocket...");

        this._wsClient.start({
            eventDispatcher: new Lark.EventDispatcher({}).register({
                // 注册消息已读事件(空处理器,消除 SDK "no handler" 警告)
                "im.message.message_read_v1": (_data) => {},

                /**
                 * 监听「接收消息」事件
                 * 文档:https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/im-v1/message/events/receive
                 */
                "im.message.receive_v1": async (data) => {
                    await this._handleIncoming(data);
                },
            }),
        });

        logger.info("[Feishu] WebSocket 已启动,等待消息...");
    }

    // ──────────────────────────────────────────
    //  私有:处理收到的消息
    // ──────────────────────────────────────────

    /**
     * @param {object} data  飞书事件原始数据
     */
    async _handleIncoming(data) {
        const { message } = data;
        const { message_id, chat_id, content, message_type } = message;

        // ── 过滤历史消息:只处理启动后收到的消息 ──
        // 飞书重连时会补推历史消息,一律丢弃
        const msgTimeMs = Number(message.create_time); // 飞书时间戳单位:毫秒
        if (msgTimeMs < this._startTime) {
            logger.debug(`[Feishu] 历史消息跳过,消息时间=${new Date(msgTimeMs).toISOString()}`);
            return;
        }

        // ── 去重层1:同一 message_id 只处理一次(防飞书重推同一事件)──
        if (this._handledIds.has(message_id)) {
            logger.debug(`[Feishu] 重复事件,跳过 message_id=${message_id}`);
            return;
        }
        this._handledIds.add(message_id);
        if (this._handledIds.size > 1000) this._handledIds.clear();

        // ── 去重层2:连续发送相同内容才算重复 ──────────────────────
        // 只对比同一 chat_id 的【上一条】内容,中间插了别的内容就不算重复
        const userTextRaw = (() => { try { return JSON.parse(content).text?.trim() ?? ""; } catch { return ""; } })();
        if (this._lastText.get(chat_id) === userTextRaw) {
            logger.debug(`[Feishu] 连续重复内容,跳过 [chat_id=${chat_id}]: ${userTextRaw}`);
            return;
        }
        this._lastText.set(chat_id, userTextRaw);

        // ── 只处理文本消息 ───────────────────────
        if (message_type !== "text") {
            logger.debug(`[Feishu] 忽略非文本消息,类型: ${message_type}`);
            return;
        }

        // content 格式:{"text": "用户输入内容"}
        let userText;
        try {
            userText = JSON.parse(content).text?.trim();
        } catch {
            logger.warn("[Feishu] 无法解析消息内容", content);
            return;
        }

        if (!userText) {
            logger.debug("[Feishu] 空消息,跳过");
            return;
        }

        if (CONFIG.feishu.logIncoming) {
            logger.info(`[Feishu] 收到消息 [chat_id=${chat_id}]: ${userText}`);
        }

        // ── 调用 Agent 处理 ──────────────────────
        let replyText;
        try {
            replyText = await this._onMessage(userText, { chat_id, raw: data });
        } catch (err) {
            logger.error("[Feishu] Agent 处理失败", err.message);
            replyText = `⚠️ 处理出错:${err.message}`;
        }

        await this._reply(chat_id, userText, replyText);
    }

    // ──────────────────────────────────────────
    //  私有:发送消息卡片回复
    // ──────────────────────────────────────────

    /**
     * @param {string} chatId       目标会话 ID
     * @param {string} userText     用户原文(用作卡片标题)
     * @param {string} replyText    回复正文
     */
    async _reply(chatId, userText, replyText) {
        const { replyTitlePrefix } = CONFIG.feishu;

        try {
            await this._client.im.v1.message.create({
                params: { receive_id_type: "chat_id" },
                data: {
                    receive_id: chatId,
                    msg_type:   "interactive",
                    content: Lark.messageCard.defaultCard({
                        title:   `${replyTitlePrefix}:${userText}`,
                        content: replyText,
                    }),
                },
            });

            logger.info(`[Feishu] ✅ 回复已发送 [chat_id=${chatId}]`);
        } catch (err) {
            logger.error("[Feishu] 发送回复失败", err.message);
        }
    }

    // ──────────────────────────────────────────
    //  私有:映射日志级别
    // ──────────────────────────────────────────

    /** 将我们的日志级别映射到 Lark SDK 的 LoggerLevel */
    _larkLogLevel() {
        const map = {
            debug: Lark.LoggerLevel.debug,
            info:  Lark.LoggerLevel.info,
            warn:  Lark.LoggerLevel.warn,
            error: Lark.LoggerLevel.error,
        };
        return map[CONFIG.log.level] ?? Lark.LoggerLevel.info;
    }
}

/**
 * @typedef {object} MessageContext
 * @property {string} chat_id   飞书会话 ID
 * @property {object} raw       飞书原始事件数据
 */

onMessage 是从外部注入的回调,FeishuClient 完全不依赖 Agent,两侧可以独立测试。


程序入口 src/index.js

js 复制代码
/**
 * ══════════════════════════════════════════════════════
 *  index.js --- 程序入口
 * ══════════════════════════════════════════════════════
 *
 * 启动顺序:
 *   1. 连接 MCP Server(获取工具列表)
 *   2. 创建 Claude AgentRunner(注入 MCP 客户端)
 *   3. 启动飞书 WebSocket(注入 Agent 作为消息处理器)
 *
 * 扩展建议:
 *   - 多用户并发:AgentRunner.run() 本身是无状态的,可直接并发调用
 *   - 多轮对话:将 messages 历史按 chat_id 维护在 Map 中
 *   - 限流/队列:在 onMessage 前包装一层 p-limit 或 Bull 队列
 */

import { McpClient    } from "./mcp/client.js";
import { AgentRunner  } from "./agent/runner.js";
import { FeishuClient } from "./feishu/client.js";
import { SkillManager } from "./skills/manager.js";
import { logger       } from "./utils/logger.js";

async function main() {
    logger.info("═".repeat(52));
    logger.info("  Feishu × Claude × MCP Agent 启动中...");
    logger.info("═".repeat(52));

    // ── Step 1: 加载 Skill ────────────────────
    // 从 CONFIG.activeSkill 读取,失败时立即抛错退出
    const skillManager = new SkillManager();
    const skill = skillManager.getActive();
    logger.info(`[Main] 当前 skill: ${skill.name} --- ${skill.description}`);

    // ── Step 2: 连接 MCP Server ───────────────
    const mcpClient = new McpClient();
    await mcpClient.connect();

    // ── Step 3: 创建 Agent(注入 MCP + Skill)──
    const agent = new AgentRunner(mcpClient, skillManager);

    // ── Step 4: 启动飞书,注入 Agent 处理逻辑 ──
    const feishu = new FeishuClient(
        /**
         * onMessage 钩子:每条飞书消息触发一次
         * @param {string} userText      用户输入文本
         * @param {MessageContext} ctx   飞书上下文(chat_id 等)
         * @returns {Promise<string>}    Claude 的最终回复文本
         */
        async (userText, ctx) => {
            logger.debug(`[Main] 开始处理消息,chat_id=${ctx.chat_id}`);
            const reply = await agent.run(userText);
            logger.debug(`[Main] 处理完成,chat_id=${ctx.chat_id}`);
            return reply;
        }
    );

    feishu.start();

    // ── 优雅退出 ──────────────────────────────
    const shutdown = async (signal) => {
        logger.warn(`[Main] 收到信号 ${signal},正在关闭...`);
        await mcpClient.close();
        process.exit(0);
    };

    process.on("SIGINT",  () => shutdown("SIGINT"));
    process.on("SIGTERM", () => shutdown("SIGTERM"));
}

main().catch(err => {
    logger.error("[Main] 启动失败", err.message);
    process.exit(1);
});

启动顺序清晰,四步串联,任何一步失败都会 throw 让进程退出而不是静默失败。


本地测试 src/test-skill.js

开发阶段不想每次都打开飞书,用 test-skill.js 直接在终端跑 Agent:

js 复制代码
/**
 * ══════════════════════════════════════════════════════
 *  test-skill.js --- Skill 独立测试脚本
 * ══════════════════════════════════════════════════════
 *
 * 不启动飞书,直接在终端跑 Agent,快速验证 skill 效果。
 *
 * 用法:
 *   node src/test-skill.js                      # 使用 activeSkill
 *   ACTIVE_SKILL=translator node src/test-skill.js  # 临时切换
 */

import { McpClient    } from "./mcp/client.js";
import { AgentRunner  } from "./agent/runner.js";
import { SkillManager } from "./skills/manager.js";
import { logger       } from "./utils/logger.js";

// ── 测试用例:在这里添加你想测试的消息 ──────────────
const TEST_CASES = [
    "给我讲个笑话",
    "再来一个",
    "这个不够冷,更冷一点",
];

async function main() {
    logger.info("═".repeat(52));
    logger.info("  Skill 测试模式(无飞书)");
    logger.info("═".repeat(52));

    // 加载 skill
    const skillManager = new SkillManager();
    const skill = skillManager.getActive();
    logger.info(`当前 skill: "${skill.name}" --- ${skill.description}\n`);

    // 连接 MCP
    const mcpClient = new McpClient();
    await mcpClient.connect();

    const agent = new AgentRunner(mcpClient, skillManager);

    // 逐条跑测试用例
    for (const msg of TEST_CASES) {
        const reply = await agent.run(msg);
        // 用醒目格式打印最终回复
        console.log(`  💬 输入:${msg}`);
        console.log(`  🤖 回复:${reply}`);
    }

    await mcpClient.close();
    logger.info("测试完成 ✅");
}

main().catch(err => {
    logger.error("测试失败", err.message);
    process.exit(1);
});
bash 复制代码
npm run test:skill

# 临时测试其他 skill
ACTIVE_SKILL=translator npm run test:skill

运行效果

启动日志:

复制代码
════════════════════════════════════════════════════
  Feishu × Claude × MCP Agent 启动中...
════════════════════════════════════════════════════
[INFO ] [Skill] 已加载 "joker"(冷笑话段子手) temperature=1 maxTokens=256
[INFO ] [MCP] 连接成功,可用工具:加法, 设置转速
[INFO ] [Feishu] WebSocket 已启动,等待消息...

收到消息后:

复制代码
────────────────────────────────────────────────────
[INFO ] [Agent] 👤 用户: 帮我算 4+3  (skill: joker)
────────────────────────────────────────────────────
[DEBUG] [Agent] 请求 Claude(messages=1 条,skill=joker)
[INFO ] [Agent] 🔧 调用工具: 加法 {"a":4,"b":3}
[INFO ] [Agent] ✅ 工具结果: 7
[INFO ] [Agent] 🤖 Claude: 4+3=7,数学告诉我们加法不会骗人 🧮
[INFO ] [Feishu] ✅ 回复已发送 [chat_id=oc_xxx]

skill例子

main.md

bash 复制代码
---
name: main
description: 通用 MCP 执行助手,专注工具调用,默认操作 E:\agent_test 目录
temperature: 0.7
maxTokens: 1024
---

# MCP 执行助手

你是一个专注执行 MCP 工具的智能助手,忠实反馈工具结果,不加主观判断。

## 工作目录

默认工作目录为 `E:\agent_test`,涉及文件、目录相关操作时无需用户重复指定路径,自动补全。

## 行为规则

- 优先调用 MCP 工具完成任务,不用文字推测代替实际调用
- 忠实呈现工具返回结果,不质疑、不修正、不过度解释
- 工具调用失败时,如实告知错误信息,不自行编造结果
- 回复简洁,结尾附 2 个贴合场景的 emoji
- 全程使用中文

joker.md

bash 复制代码
---
name: joker
description: 冷笑话段子手,专门讲让人沉默三秒的冷笑话
temperature: 1.0
maxTokens: 256
---

# 冷笑话段子手

你是一个专门讲冷笑话的段子手。

## 规则

- 每次只讲一个笑话,不超过 4 句话
- 笑话要足够冷,冷到让人沉默 3 秒才反应过来
- 结尾加一个 emoji 表情
- 如果用户夸你,继续讲下一个,不要重复
- 用中文

小结

模块 核心设计
config/ 所有参数集中管理,支持环境变量覆盖
utils/logger 四级日志,运行时可动态修改级别
skills/ .md 文件定义 AI 角色,frontmatter 控制参数
mcp/ 工具协议封装,切换 Server 只改两行配置
agent/ Agentic Loop,驱动 Claude 多轮工具调用
feishu/ 纯 I/O 层,通过回调与 Agent 解耦
相关推荐
Deepoch2 小时前
Deepoc具身模型开发板:为机械臂清洁机器人注入“智慧灵魂”
大数据·科技·机器人·机械臂·清洁机器人·具身模型·deepoc
天空属于哈夫克32 小时前
私域自动回复机器人:构建 7x24 小时智能客户响应系统
机器人
韶关亿宏科技-光纤通信小易3 小时前
管道打磨机器人-300–700mm 内径打磨解决方案
机器人
晓纪同学4 小时前
ROS2 -03-工作空间与功能包
机器人
兮动人5 小时前
Linux 云服务器部署 OpenClaw 全攻略:从环境搭建到 QQ 机器人集成
linux·服务器·机器人·openclaw
河铃旅鹿6 小时前
在windows电脑上用虚拟机--ubuntu系统部署openclaw并在主机用飞书连接对话的一站式教程
windows·ubuntu·飞书
算.子7 小时前
使用OpenClaw飞书插件玩转飞书
ai·飞书·openclaw
剪刀石头布Cheers7 小时前
Windows系统 OpenClaw+Ollama+飞书/QQ
飞书·openclaw·小龙虾
广州赛远7 小时前
SRA166防静电防护服安装保养指南:避免机器人静电损伤的实操详解
人工智能·机器人