OpenClaw MCP 技能设计文档
2026年03月11日5 次阅读0 人喜欢
OpenClawMCP技能设计OAuth 2.0AI
OpenClaw MCP 技能设计文档
创建时间: 2026-03-11
目标: 为博客 MCP 服务构建 OpenClaw 技能,使 OpenClaw 用户能够通过 AI 助手管理博客内容
? 背景
OpenClaw 对 MCP 的支持情况
核心发现: OpenClaw 不原生支持远程 MCP 服务(HTTP/SSE/WebSocket),主要通过以下方式支持 MCP:
-
mcporter - 官方推荐的 MCP 桥接工具
- 支持本地 MCP 服务器(通过 stdio transport)
- 允许动态添加/修改 MCP 服务器
- 不支持远程 HTTP MCP 服务器
-
MCP 技能 - 社区解决方案
- mcp-integration skill
- 需要自定义实现远程 MCP 服务器连接
我们的博客 MCP 服务特点
- Transport: HTTP POST (Stateless Request-Response)
- 认证: OAuth 2.0 Bearer Token (LTK_ 前缀的长期 Token)
- 端点:
/api/mcp - 工具: 6 个工具(创建、更新、删除、获取、列出文章)
- 资源: 5+ 个资源(标签、合集、写作风格、参考文档)
? 设计目标
核心目标
为 OpenClaw 创建一个技能,使得:
- 透明集成: 用户可以通过自然语言指令管理博客
- 完整功能: 支持所有 MCP 工具和资源
- 无缝认证: 自动处理 OAuth 2.0 认证
- 错误处理: 提供清晰的错误提示和重试机制
- 可扩展: 易于添加新的 MCP 功能
技术约束
- OpenClaw 技能使用 TypeScript/JavaScript
- 需要 HTTP 客户端 来调用远程 MCP 服务
- 需要处理 JSON-RPC 2.0 协议
- 需要管理 认证 Token 的生命周期
?️ 架构设计
技能架构
┌─────────────────────────────────────────────────────────────┐
│ OpenClaw │
│ (AI Agent Runtime) │
└────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Blog Manager Skill │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Natural Language Processor │ │
│ │ (解析用户意图,提取参数,验证输入) │ │
│ └────────────────────┬─────────────────────────────────┘ │
│ │ │
│ ┌────────────────────▼─────────────────────────────────┐ │
│ │ MCP Client Adapter │ │
│ │ (JSON-RPC 2.0 协议封装,HTTP 请求管理) │ │
│ └────────────────────┬─────────────────────────────────┘ │
│ │ │
│ ┌────────────────────▼─────────────────────────────────┐ │
│ │ Authentication Manager │ │
│ │ (OAuth 2.0 Bearer Token 管理,自动刷新) │ │
│ └────────────────────┬─────────────────────────────────┘ │
│ │ │
└───────────────────────┼──────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Blog MCP Server (Remote) │
│ /api/mcp (HTTP POST) │
└─────────────────────────────────────────────────────────────┘
模块划分
1. Natural Language Processor (NLP)
- 解析用户自然语言指令
- 提取操作类型(创建/更新/删除/查询)
- 验证和清理输入参数
- 提供智能默认值
2. MCP Client Adapter
- 实现 JSON-RPC 2.0 协议
- 处理 HTTP 请求/响应
- 支持批量操作
- 错误处理和重试逻辑
3. Authentication Manager
- 管理 OAuth 2.0 Bearer Token
- Token 存储和检索
- 自动 Token 刷新(如需要)
- 认证失败处理
4. Resource Cache
- 缓存标签列表
- 缓存合集列表
- 缓存写作风格指南
- 智能缓存失效
? 技能功能设计
支持的操作
1. 文章管理
创建文章
用户指令示例:
- "帮我创建一篇关于 TypeScript 的文章"
- "写一篇博客,主题是 React 19 新特性"
- "发布一篇技术文章,介绍 Next.js 16"
参数提取:
- title: 从主题推断或请求用户输入
- content: 引导用户提供或草稿
- tags: 建议基于内容的标签
- category: 技术文章/随笔
- collections: 询问是否添加到合集
更新文章
用户指令示例:
- "更新第 123 篇文章的标题"
- "修改文章内容,添加新章节"
- "把这篇文章添加到 '小破站建设' 合集"
参数提取:
- id: 从上下文推断或请求
- 要更新的字段:动态提取
- add_to_collections: 解析合集名称
删除文章
用户指令示例:
- "删除文章 123"
- "移除这篇草稿"
安全确认:
- 二次确认
- 显示文章标题
查询文章
用户指令示例:
- "列出最近的文章"
- "搜索关于 MCP 的文章"
- "显示第 5 页的文章"
参数:
- pageNum, pageSize
- keyword
- hide (显示/隐藏)
2. 资源查询
用户指令示例:
- "显示所有标签"
- "有哪些合集?"
- "写作风格指南是什么?"
- "查看路由参考文档"
自动缓存资源,减少重复请求
交互流程
创建文章完整流程
1. 用户: "帮我创建一篇关于 MCP 的技术文章"
2. 技能响应流程:
a) 解析意图 → 创建文章
b) 检查资源 → 获取标签和合集列表
c) 读取写作风格指南
d) 生成草稿或引导用户输入
e) 展示预览
f) 确认后创建
3. 成功后:
- 显示文章链接
- 询问是否添加到合集
- 建议相关标签
智能建议
基于上下文提供智能建议:
1. 标签建议
- 从内容提取关键词
- 与现有标签匹配
- 推荐热门标签
2. 合集建议
- 基于分类推荐
- 显示合集描述
- 显示文章数量
3. 写作建议
- 风格一致性检查
- 结构建议
- 长度建议
? 认证设计
Token 管理
配置存储
typescript
// OpenClaw 技能配置
interface BlogSkillConfig {
mcpEndpoint: string; // "https://react.nnnnzs.cn/api/mcp"
authToken: string; // LTK_xxx
userId?: number; // 可选,用于日志
}
// 存储位置
// ~/.openclaw/skills/blog-manager/config.json
Token 获取流程
1. 首次使用时引导用户:
"请提供您的博客 MCP Token(从 /c/user/info 生成)"
2. 验证 Token:
- 调用 tools/list 测试
- 成功则存储
- 失败则提示重新输入
3. Token 存储:
- 本地加密存储
- 环境变量选项
- 不在日志中显示
认证失败处理
1. 捕获 401 错误
2. 提示用户:
"Token 已过期或无效,请重新生成"
3. 提供重新配置选项
4. 清除无效 Token
? 实现细节
技能目录结构
~/.openclaw/skills/blog-manager/
├── package.json
├── tsconfig.json
├── README.md
├── src/
│ ├── index.ts # 技能入口
│ ├── config.ts # 配置管理
│ ├── auth.ts # 认证管理
│ ├── mcp-client.ts # MCP 客户端
│ ├── nlp/ # 自然语言处理
│ │ ├── index.ts
│ │ ├── intent-parser.ts # 意图解析
│ │ └── param-extractor.ts # 参数提取
│ ├── handlers/ # 操作处理器
│ │ ├── article.ts # 文章操作
│ │ ├── resource.ts # 资源查询
│ │ └── cache.ts # 缓存管理
│ ├── prompts/ # 提示词模板
│ │ ├── create-article.md
│ │ ├── update-article.md
│ │ └── confirm-delete.md
│ └── types.ts # 类型定义
└── test/
├── mcp-client.test.ts
└── nlp.test.ts
核心:MCP 客户端实现
typescript
// src/mcp-client.ts
import axios from 'axios';
export class BlogMCPClient {
private endpoint: string;
private token: string;
constructor(endpoint: string, token: string) {
this.endpoint = endpoint;
this.token = token;
}
/**
* 调用 MCP 工具
*/
async callTool(toolName: string, args: Record<string, any>) {
const response = await axios.post(
this.endpoint,
{
jsonrpc: '2.0',
id: Date.now(),
method: 'tools/call',
params: {
name: toolName,
arguments: args
}
},
{
headers: {
'Authorization': `Bearer ${this.token}`,
'Content-Type': 'application/json'
}
}
);
if (response.data.error) {
throw new MCPError(response.data.error);
}
return response.data.result;
}
/**
* 读取 MCP 资源
*/
async readResource(uri: string) {
const response = await axios.post(
this.endpoint,
{
jsonrpc: '2.0',
id: Date.now(),
method: 'resources/read',
params: { uri }
},
{
headers: {
'Authorization': `Bearer ${this.token}`,
'Content-Type': 'application/json'
}
}
);
return response.data.result;
}
/**
* 列出可用工具
*/
async listTools() {
const response = await axios.post(
this.endpoint,
{
jsonrpc: '2.0',
id: Date.now(),
method: 'tools/list'
},
{
headers: {
'Authorization': `Bearer ${this.token}`,
'Content-Type': 'application/json'
}
}
);
return response.data.result;
}
/**
* 列出可用资源
*/
async listResources() {
const response = await axios.post(
this.endpoint,
{
jsonrpc: '2.0',
id: Date.now(),
method: 'resources/list'
},
{
headers: {
'Authorization': `Bearer ${this.token}`,
'Content-Type': 'application/json'
}
}
);
return response.data.result;
}
}
class MCPError extends Error {
constructor(public error: any) {
super(error.message || 'MCP Error');
this.name = 'MCPError';
}
}
核心:技能入口
typescript
// src/index.ts
import { PluginApi } from '@openclaw/sdk';
import { BlogMCPClient } from './mcp-client';
import { ConfigManager } from './config';
import { ArticleHandler } from './handlers/article';
import { ResourceHandler } from './handlers/resource';
import { IntentParser } from './nlp/intent-parser';
export default async function register(api: PluginApi) {
const config = await ConfigManager.load();
const client = new BlogMCPClient(config.endpoint, config.token);
// 注册技能
api.registerSkill({
id: 'blog-manager',
name: 'Blog Manager',
description: '管理博客文章、标签和合集',
version: '1.0.0',
// 配置处理
async configure(params) {
if (params.endpoint) config.endpoint = params.endpoint;
if (params.token) config.token = params.token;
await config.save();
return { success: true };
},
// 主处理函数
async handle(input, context) {
const parser = new IntentParser();
const intent = await parser.parse(input);
switch (intent.action) {
case 'create_article':
return await ArticleHandler.create(client, intent, context);
case 'update_article':
return await ArticleHandler.update(client, intent, context);
case 'delete_article':
return await ArticleHandler.delete(client, intent, context);
case 'list_articles':
return await ArticleHandler.list(client, intent, context);
case 'get_article':
return await ArticleHandler.get(client, intent, context);
case 'list_resources':
return await ResourceHandler.list(client, intent);
case 'read_resource':
return await ResourceHandler.read(client, intent);
default:
return {
text: `我不太理解您想做什么。可以尝试:
- 创建/更新/删除文章
- 查询文章列表
- 查看标签或合集`
};
}
}
});
// 注册命令
api.registerCommand({
name: 'blog',
description: '博客管理命令',
handler: async (args, context) => {
return await api.skills['blog-manager'].handle(args.join(' '), context);
}
});
}
意图解析
typescript
// src/nlp/intent-parser.ts
export interface Intent {
action: string;
params: Record<string, any>;
confidence: number;
}
export class IntentParser {
async parse(input: string): Promise<Intent> {
const lower = input.toLowerCase();
// 创建文章
if (this.matchesCreate(lower)) {
return {
action: 'create_article',
params: this.extractCreateParams(input),
confidence: 0.9
};
}
// 更新文章
if (this.matchesUpdate(lower)) {
return {
action: 'update_article',
params: this.extractUpdateParams(input),
confidence: 0.9
};
}
// 删除文章
if (this.matchesDelete(lower)) {
return {
action: 'delete_article',
params: this.extractDeleteParams(input),
confidence: 0.95
};
}
// 列出文章
if (this.matchesList(lower)) {
return {
action: 'list_articles',
params: this.extractListParams(input),
confidence: 0.9
};
}
// 获取文章
if (this.matchesGet(lower)) {
return {
action: 'get_article',
params: this.extractGetParams(input),
confidence: 0.9
};
}
// 资源查询
if (this.matchesResourceQuery(lower)) {
return {
action: this.extractResourceAction(lower),
params: {},
confidence: 0.85
};
}
// 默认:请求澄清
return {
action: 'clarify',
params: { originalInput: input },
confidence: 0.5
};
}
private matchesCreate(input: string): boolean {
return /^(创建|写|发布|新增|添加).*文章/.test(input) ||
/blog.*post/i.test(input) ||
/new.*article/i.test(input);
}
private matchesUpdate(input: string): boolean {
return /^(更新|修改|编辑).*文章/.test(input) ||
/update.*article/i.test(input);
}
private matchesDelete(input: string): boolean {
return /^(删除|移除).*文章/.test(input) ||
/delete.*article/i.test(input);
}
private matchesList(input: string): boolean {
return /^列出|显示|查看.*文章/.test(input) ||
/list.*articles?/i.test(input);
}
private matchesGet(input: string): boolean {
return /^获取|查询|搜索.*文章/.test(input) ||
/get.*article/i.test(input);
}
private matchesResourceQuery(input: string): boolean {
return /标签|合集|写作风格|参考文档/.test(input) ||
/tags?|collections?|writing.*style|reference/i.test(input);
}
private extractCreateParams(input: string): Record<string, any> {
const params: any = {};
// 提取主题/标题
const titleMatch = input.match(/(?:关于|主题是|标题是|名为)\s*[""]?(.+?)[""]?(?:\s|$|的)/);
if (titleMatch) {
params.title = titleMatch[1].trim();
}
// 提取分类
if (/技术|教程|指南/.test(input)) {
params.category = '技术文章';
} else if (/随笔|日记|感想/.test(input)) {
params.category = '随笔';
}
return params;
}
private extractUpdateParams(input: string): Record<string, any> {
const params: any = {};
// 提取文章 ID
const idMatch = input.match(/第\s*(\d+)\s*篇|文章\s*(\d+)|#(\d+)/);
if (idMatch) {
params.id = parseInt(idMatch[1] || idMatch[2] || idMatch[3]);
}
return params;
}
private extractDeleteParams(input: string): Record<string, any> {
const params: any = {};
// 提取文章 ID
const idMatch = input.match(/(\d+)/);
if (idMatch) {
params.id = parseInt(idMatch[1]);
}
return params;
}
private extractListParams(input: string): Record<string, any> {
const params: any = { pageNum: 1, pageSize: 10 };
// 提取页码
const pageMatch = input.match(/第\s*(\d+)\s*页/);
if (pageMatch) {
params.pageNum = parseInt(pageMatch[1]);
}
// 提取关键词
const keywordMatch = input.match(/(?:搜索|查找|关于)\s*(.+?)(?:\s|$|的)/);
if (keywordMatch) {
params.keyword = keywordMatch[1].trim();
}
return params;
}
private extractGetParams(input: string): Record<string, any> {
const params: any = {};
// 提取 ID 或标题
const idMatch = input.match(/(\d+)/);
if (idMatch) {
params.id = parseInt(idMatch[1]);
} else {
const titleMatch = input.match(/(?:标题|名为)\s*[""]?(.+?)[""]?(?:\s|$)/);
if (titleMatch) {
params.title = titleMatch[1].trim();
}
}
return params;
}
private extractResourceAction(input: string): string {
if (input.includes('标签') || input.includes('tag')) {
return 'list_tags';
}
if (input.includes('合集') || input.includes('collection')) {
return 'list_collections';
}
if (input.includes('写作风格') || input.includes('writing')) {
return 'read_writing_style';
}
return 'list_resources';
}
}
文章处理器
typescript
// src/handlers/article.ts
import { BlogMCPClient } from '../mcp-client';
import { Intent } from '../nlp/intent-parser';
export class ArticleHandler {
/**
* 创建文章
*/
static async create(client: BlogMCPClient, intent: Intent, context: any) {
// 1. 获取可用资源
const [tags, collections, styleGuide] = await Promise.all([
client.readResource('blog://tags'),
client.readResource('blog://collections'),
client.readResource('blog://writing_style')
]);
// 2. 如果没有提供完整参数,引导用户
if (!intent.params.title) {
return {
text: `请提供文章标题。当前可用的标签:${tags.contents[0].text}`,
suggestions: [
{ action: 'input', label: '输入标题' },
{ action: 'cancel', label: '取消' }
]
};
}
// 3. 检查写作风格
const style = styleGuide.contents[0].text;
const styleTips = this.extractStyleTips(style);
// 4. 引导输入内容或生成草稿
if (!intent.params.content) {
return {
text: `好的,我将创建文章《${intent.params.title}》。
${styleTips}
请提供文章内容,或者告诉我你想写什么,我可以帮你生成草稿。
当前可用的标签:${tags.contents[0].text}
当前可用的合集:${collections.contents[0].text}
`
};
}
// 5. 调用 MCP 工具创建
const result = await client.callTool('create_article', {
title: intent.params.title,
content: intent.params.content,
category: intent.params.category || '技术文章',
tags: intent.params.tags,
collections: intent.params.collections,
description: intent.params.description,
cover: intent.params.cover
});
// 6. 格式化响应
const article = JSON.parse(result.content[0].text);
return {
text: `✅ 文章创建成功!
标题:${article.title}
链接:${article.path}
ID:${article.id}
${article.path ? `? 查看文章: ${article.path}` : ''}
还需要做什么?
- 添加到合集?告诉我合集名称
- 添加标签?告诉我标签名称
- 修改内容?告诉我怎么改
`
};
}
/**
* 更新文章
*/
static async update(client: BlogMCPClient, intent: Intent, context: any) {
if (!intent.params.id) {
return {
text: '请提供要更新的文章 ID(例如:更新文章 123)'
};
}
// 获取文章当前状态
const current = await client.callTool('get_article', {
id: intent.params.id
});
if (current.isError) {
return {
text: `找不到文章 ID ${intent.params.id}`
};
}
const article = JSON.parse(current.content[0].text);
// 调用 MCP 工具更新
const result = await client.callTool('update_article', {
id: intent.params.id,
...intent.params
});
const updated = JSON.parse(result.content[0].text);
return {
text: `✅ 文章更新成功!
标题:${updated.title}
链接:${updated.path}
`
};
}
/**
* 删除文章
*/
static async delete(client: BlogMCPClient, intent: Intent, context: any) {
if (!intent.params.id) {
return {
text: '请提供要删除的文章 ID(例如:删除文章 123)'
};
}
// 二次确认
if (!context.confirmed) {
const current = await client.callTool('get_article', {
id: intent.params.id
});
if (current.isError) {
return {
text: `找不到文章 ID ${intent.params.id}`
};
}
const article = JSON.parse(current.content[0].text);
return {
text: `⚠️ 即将删除文章:
标题:${article.title}
ID:${article.id}
此操作不可撤销!确认删除吗?`,
confirm: true
};
}
// 执行删除
const result = await client.callTool('delete_article', {
id: intent.params.id
});
return {
text: `✅ 文章已删除(ID: ${intent.params.id})`
};
}
/**
* 列出文章
*/
static async list(client: BlogMCPClient, intent: Intent, context: any) {
const result = await client.callTool('list_articles', intent.params);
const data = JSON.parse(result.content[0].text);
let output = `? 文章列表 (第 ${data.pageNum} 页,共 ${data.total} 篇)\n\n`;
data.record.forEach((post: any, index: number) => {
output += `${index + 1}. ${post.title}\n`;
output += ` ID: ${post.id} | ${post.path || ''}\n`;
if (post.summary) output += ` ${post.summary}\n`;
output += '\n';
});
return {
text: output,
pagination: {
current: data.pageNum,
total: data.totalPages,
hasMore: data.pageNum < data.totalPages
}
};
}
/**
* 获取文章
*/
static async get(client: BlogMCPClient, intent: Intent, context: any) {
const result = await client.callTool('get_article', intent.params);
if (result.isError) {
return {
text: `获取文章失败:${result.content[0].text}`
};
}
const article = JSON.parse(result.content[0].text);
return {
text: `? ${article.title}
ID: ${article.id}
分类: ${article.category || '未分类'}
标签: ${article.tags || '无'}
创建时间: ${article.created_at}
${article.summary ? `摘要:${article.summary}\n\n` : ''}${article.content}
${article.path ? `\n? 链接: ${article.path}` : ''}`
};
}
private static extractStyleTips(styleGuide: string): string {
// 从写作风格指南中提取关键提示
return `? 写作提示:
- 使用第一人称("我")
- 保持对话式、非正式的语气
- 短句为主,避免过长段落
- 真诚、自然`;
}
}
资源处理器
typescript
// src/handlers/resource.ts
import { BlogMCPClient} from '../mcp-client';
import { Intent } from '../nlp/intent-parser';
export class ResourceHandler {
/**
* 列出资源
*/
static async list(client: BlogMCPClient, intent: Intent) {
const result = await client.listResources();
let output = '? 可用资源:\n\n';
result.resources.forEach((resource: any) => {
output += `- ${resource.name}\n`;
output += ` URI: ${resource.uri}\n`;
if (resource.description) {
output += ` ${resource.description}\n`;
}
output += '\n';
});
return {
text: output
};
}
/**
* 读取特定资源
*/
static async read(client: BlogMCPClient, intent: Intent) {
let uri: string;
switch (intent.action) {
case 'list_tags':
uri = 'blog://tags';
break;
case 'list_collections':
uri = 'blog://collections';
break;
case 'read_writing_style':
uri = 'blog://writing_style';
break;
default:
uri = intent.params.uri;
}
const result = await client.readResource(uri);
if (result.isError) {
return {
text: `读取资源失败:${result.content[0].text}`
};
}
const content = result.contents[0];
// 根据资源类型格式化输出
if (uri === 'blog://tags') {
const tags = content.text.split(',');
return {
text: `?️ 可用标签 (${tags.length} 个):\n\n${tags.join(', ')}`
};
}
if (uri === 'blog://collections') {
return {
text: `? 可用合集:\n\n${content.text}`
};
}
if (uri === 'blog://writing_style') {
return {
text: `? 写作风格指南:\n\n${content.text.substring(0, 500)}...\n\n(完整指南请参考博客文档)`
};
}
return {
text: content.text
};
}
}
? 使用文档
安装技能
bash
# 1. 克隆技能仓库
git clone https://github.com/NNNNzs/openclaw-blog-manager.git ~/.openclaw/skills/blog-manager
# 2. 安装依赖
cd ~/.openclaw/skills/blog-manager
pnpm install
# 3. 构建
pnpm build
# 4. 配置
openclaw skill configure blog-manager
配置技能
bash
# 方式 1: 通过命令行配置
openclaw skill config blog-manager --endpoint https://react.nnnnzs.cn/api/mcp
openclaw skill config blog-manager --token LTK_xxx
# 方式 2: 通过配置文件
# 编辑 ~/.openclaw/skills/blog-manager/config.json
{
"endpoint": "https://react.nnnnzs.cn/api/mcp",
"token": "LTK_xxx"
}
使用示例
在 OpenClaw Gateway 中
你: 帮我创建一篇关于 OpenClaw 的技术文章
OpenClaw: 好的,我将创建文章《OpenClaw》。
? 写作提示:
- 使用第一人称("我")
- 保持对话式、非正式的语气
- 短句为主,避免过长段落
- 真诚、自然
当前可用的标签:TypeScript,React,Next.js,MCP,OAuth
当前可用的合集:
ID: 1 | Slug: xiao-po-zhan-jian-she | Title: 小破站建设 | Articles: 10
请提供文章内容,或者告诉我你想写什么,我可以帮你生成草稿。
你: [提供文章内容或主题]
OpenClaw: ✅ 文章创建成功!
标题:OpenClaw 入门指南
链接:/2026/03/11/openclaw-getting-started
ID: 123
? 查看文章: /2026/03/11/openclaw-getting-started
还需要做什么?
- 添加到合集?告诉我合集名称
- 添加标签?告诉我标签名称
在频道中(WhatsApp/Telegram/Discord)
你: /blog 列出最近的文章
Bot: ? 文章列表 (第 1 页,共 25 篇)
1. MCP OAuth 2.0 认证升级总结
ID: 120 | /2026/01/15/mcp-oauth-upgrade
这篇文章记录了 MCP 认证从自定义 Headers 到 OAuth 2.0 的完整升级过程...
2. 向量检索系统设计
ID: 119 | /2026/01/10/vector-search-design
基于 Qdrant 的语义搜索系统设计文档...
[更多文章...]
---
输入页码查看更多,或使用 /blog 搜索 <关键词> 搜索文章
? 测试策略
单元测试
typescript
// test/mcp-client.test.ts
import { BlogMCPClient } from '../src/mcp-client';
describe('BlogMCPClient', () => {
let client: BlogMCPClient;
beforeEach(() => {
client = new BlogMCPClient(
'https://react.nnnnzs.cn/api/mcp',
'test_token'
);
});
test('应该成功调用工具', async () => {
const result = await client.callTool('list_articles', {
pageNum: 1,
pageSize: 10
});
expect(result).toHaveProperty('content');
});
test('应该处理认证错误', async () => {
const badClient = new BlogMCPClient(
'https://react.nnnnzs.cn/api/mcp',
'invalid_token'
);
await expect(
badClient.callTool('list_articles', {})
).rejects.toThrow('Authentication failed');
});
});
集成测试
typescript
// test/integration.test.ts
import { IntentParser } from '../src/nlp/intent-parser';
describe('意图解析', () => {
const parser = new IntentParser();
test('应该正确解析创建文章意图', async () => {
const intent = await parser.parse('创建一篇关于 React 的文章');
expect(intent.action).toBe('create_article');
expect(intent.params.title).toBe('React');
});
test('应该正确解析列出文章意图', async () => {
const intent = await parser.parse('列出第 2 页的文章');
expect(intent.action).toBe('list_articles');
expect(intent.params.pageNum).toBe(2);
});
});
? 部署与发布
发布到 LobeHub Skills Marketplace
bash
# 1. 准备发布
npm run build
npm run pack
# 2. 发布到 GitHub
git tag v1.0.0
git push --tags
# 3. 提交到 LobeHub
# 访问 https://lobehub.com/skills/publish
# 填写技能信息和仓库链接
技能元数据
json
{
"name": "blog-manager",
"displayName": "Blog Manager",
"description": "管理博客文章、标签和合集",
"version": "1.0.0",
"author": "NNNNzs",
"license": "MIT",
"homepage": "https://github.com/NNNNzs/openclaw-blog-manager",
"repository": "https://github.com/NNNNzs/openclaw-blog-manager",
"openclaw": {
"minVersion": "2026.3.0",
"permissions": [
"network",
"storage"
]
},
"keywords": [
"blog",
"cms",
"mcp",
"article"
]
}
? 参考资料
? 后续优化
短期优化
- 批量操作: 支持批量创建/更新文章
- 图片上传: 支持上传封面图
- 草稿保存: 保存未完成的文章草稿
- 快捷命令: 定义常用操作的快捷方式
长期优化
- 本地缓存: 实现本地文章缓存,离线查看
- 多用户支持: 支持切换不同的博客账户
- 统计分析: 文章阅读量、标签热度统计
- 自动发布: 定时发布、社交媒体同步
状态: ? 设计中
下一步: 实现核心 MCP 客户端和意图解析器
预计完成: 2026-03-20