作者:mpoll.top 发布时间:2026-04-21 0 次浏览
在生产环境中,错误处理和日志记录是 Skill 质量的决定性因素。一个没有良好错误处理的 Skill 可能在遇到问题时直接崩溃,给用户带来糟糕的体验。本文将深入讲解 OpenClaw Skill 的错误处理策略、日志记录和最佳实践。
---
场景:天气查询 Skill
正常情况:
用户:"北京天气"
→ 调用天气 API
→ 返回:北京,晴,25°C
异常情况:
用户:"北京天气"
→ 调用天气 API
→ ❌ 网络超时
→ ❌ API 返回错误
→ ❌ 城市名称无效
→ 用户看到:❌ 无法获取天气信息(网络异常)
OpenClaw Skill 可能遇到的错误:
| 类型 | 示例 | 处理方式 |
|------|------|---------|
| 网络错误 | API 超时、连接失败 | 重试、降级 |
| 数据错误 | 无效输入、格式错误 | 验证、提示 |
| 权限错误 | 缺少 API 密钥、权限不足 | 配置检查 |
| 资源错误 | 文件不存在、磁盘满 | 预检查、清理 |
| 逻辑错误 | 业务规则违反 | 验证、回滚 |
在问题发生前预防:
async function execute(params, context) {
// ✅ 预检查:参数验证
if (!params.location) {
throw new Error('缺少必要参数:location');
}
// ✅ 预检查:配置验证
if (!context.config.apiKey) {
throw new Error('未配置 API 密钥,请在 TOOLS.md 中添加');
}
// ✅ 预检查:资源验证
if (params.file && !fs.existsSync(params.file)) {
throw new Error(`文件不存在:${params.file}`);
}
// 主要逻辑...
}
不同层级处理不同类型的错误:
async function execute(params, context) {
try {
// 层级 1:业务逻辑
const result = await fetchWeather(params.location);
return formatResult(result);
} catch (error) {
// 层级 2:错误分类处理
if (error.code === 'NETWORK_ERROR') {
context.logger.warn('网络错误,尝试重试...');
return await retryOperation(() => fetchWeather(params.location));
}
if (error.code === 'INVALID_LOCATION') {
// 用户错误,友好提示
return `❌ 找不到城市 "${params.location}",请检查城市名称`;
}
// 层级 3:未知错误,记录日志并返回通用错误
context.logger.error('未知错误:', error);
return '❌ 服务暂时不可用,请稍后重试';
}
}
定义清晰的错误码便于调试:
const ErrorCodes = {
// 网络错误 1xxx
NETWORK_TIMEOUT: 1001,
NETWORK_UNREACHABLE: 1002,
// 数据错误 2xxx
INVALID_INPUT: 2001,
MISSING_PARAMETER: 2002,
// 配置错误 3xxx
MISSING_CONFIG: 3001,
INVALID_CONFIG: 3002,
// 业务错误 4xxx
RESOURCE_NOT_FOUND: 4001,
PERMISSION_DENIED: 4002
};
class SkillError extends Error {
constructor(code, message, details = {}) {
super(message);
this.code = code;
this.details = details;
this.timestamp = new Date().toISOString();
}
}
// 使用示例
throw new SkillError(
ErrorCodes.NETWORK_TIMEOUT,
'天气 API 响应超时',
{ location: params.location, timeout: 5000 }
);
处理临时性故障:
async function retryOperation(operation, options = {}) {
const {
maxRetries = 3,
delayMs = 1000,
backoff = 2 // 指数退避
} = options;
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error;
// 只重试临时性错误
if (!isRetryableError(error)) {
throw error;
}
context.logger.warn(\`重试 \${attempt}/\${maxRetries}: \${error.message}\`);
// 等待后重试(指数退避)
if (attempt < maxRetries) {
await sleep(delayMs * Math.pow(backoff, attempt - 1));
}
}
}
throw lastError;
}
function isRetryableError(error) {
// 只有网络错误和超时错误值得重试
return [
'NETWORK_TIMEOUT',
'NETWORK_UNREACHABLE',
'RATE_LIMITED'
].includes(error.code);
}
OpenClaw 支持标准日志级别:
// DEBUG: 详细调试信息
context.logger.debug('API 请求参数:', params);
// INFO: 正常操作信息
context.logger.info('天气查询成功:', result);
// WARN: 警告信息(不影响功能)
context.logger.warn('API 响应较慢:', { duration: 3500 });
// ERROR: 错误信息
context.logger.error('API 调用失败:', error);
#### ✅ 好的日志
// 包含上下文信息
context.logger.info('查询天气', {
location: params.location,
units: params.units || 'celsius',
timestamp: new Date().toISOString()
});
// 结构化日志
context.logger.error('API 错误', {
code: error.code,
message: error.message,
url: error.url,
statusCode: error.statusCode,
duration: error.duration
});
// 敏感信息脱敏
context.logger.debug('API 密钥已加载', {
keyPrefix: context.config.apiKey?.substring(0, 4) + '*'
});
#### ❌ 差的日志
// 信息太少
context.logger.info('出错了');
// 泄露敏感信息
context.logger.debug('使用密钥:', context.config.apiKey);
// 日志过多造成噪音
for (let i = 0; i < 1000; i++) {
context.logger.debug('处理中...'); // 太多了!
}
在 Skill 中配置日志:
module.exports = {
name: 'weather',
description: '查询天气信息',
// 日志配置
logging: {
level: 'info', // debug, info, warn, error
format: 'json', // json, text
output: 'file' // file, console, both
},
async execute(params, context) {
// 使用 context.logger
}
};
// ❌ 技术术语,用户看不懂
throw new Error('HTTP 500: Internal Server Error at /api/weather');
// ✅ 用户友好
return \`
❌ 天气服务暂时不可用
可能的原因:
- 网络连接不稳定
- 天气服务器维护中
建议操作:
- 检查网络连接
- 稍后重试
- 如持续失败,联系管理员
\`;
const ErrorMessages = {
NETWORK_ERROR: \`
❌ 网络连接失败
无法连接到天气服务,请检查:
- 网络连接是否正常
- 防火墙设置
- 代理配置
\`,
INVALID_LOCATION: \`
❌ 找不到城市 "\${location}"
请检查:
- 城市名称是否正确
- 是否使用了标准城市名
- 中文城市请用中文名称
\`,
RATE_LIMITED: \`
⏱️ 请求过于频繁
请稍等 \${retryAfter}秒 后重试
提示:
- 免费版本限制:\${limit}次/小时
- 升级后可获得更高限额
\`
};
const https = require('https');
const ErrorCodes = {
NETWORK_TIMEOUT: 1001,
NETWORK_ERROR: 1002,
INVALID_LOCATION: 2001,
API_ERROR: 3001
};
class SkillError extends Error {
constructor(code, message, details = {}) {
super(message);
this.code = code;
this.details = details;
}
}
module.exports = {
name: 'weather',
description: '查询天气信息',
parameters: {
location: {
type: 'string',
description: '城市名称',
required: true
},
days: {
type: 'number',
description: '预报天数',
default: 3,
minimum: 1,
maximum: 7
}
},
async execute(params, context) {
const startTime = Date.now();
try {
// 1. 参数验证
validateParams(params);
// 2. 配置检查
const apiKey = context.config?.weatherApiKey;
if (!apiKey) {
throw new SkillError(
ErrorCodes.API_ERROR,
'未配置天气 API 密钥',
{ hint: '请在 TOOLS.md 中添加 weatherApiKey' }
);
}
// 3. 调用 API(带重试)
context.logger.info('查询天气', { location: params.location });
const weatherData = await retryOperation(
() => fetchWeather(params.location, apiKey),
{ maxRetries: 3, delayMs: 1000 }
);
// 4. 数据验证
if (!weatherData || !weatherData.current) {
throw new SkillError(
ErrorCodes.INVALID_LOCATION,
\`找不到城市:\${params.location}\`,
{ location: params.location }
);
}
// 5. 格式化结果
const duration = Date.now() - startTime;
context.logger.info('天气查询成功', {
location: params.location,
duration
});
return formatWeatherResult(weatherData, params.days);
} catch (error) {
// 6. 错误处理
context.logger.error('天气查询失败', {
location: params.location,
code: error.code,
message: error.message,
duration: Date.now() - startTime
});
// 7. 返回用户友好的错误消息
return getUserFriendlyError(error, params);
}
}
};
async function fetchWeather(location, apiKey) {
return new Promise((resolve, reject) => {
const url = \`https://api.weather.com/v1/current?location=\${encodeURIComponent(location)}&key=\${apiKey}\`;
const req = https.get(url, { timeout: 5000 }, (res) => {
if (res.statusCode === 404) {
reject(new SkillError(ErrorCodes.INVALID_LOCATION, '城市不存在'));
} else if (res.statusCode !== 200) {
reject(new SkillError(ErrorCodes.API_ERROR, \`API 错误:\${res.statusCode}\`));
} else {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => resolve(JSON.parse(data)));
}
});
req.on('error', (e) => {
if (e.code === 'ETIMEDOUT') {
reject(new SkillError(ErrorCodes.NETWORK_TIMEOUT, '请求超时'));
} else {
reject(new SkillError(ErrorCodes.NETWORK_ERROR, \`网络错误:\${e.message}\`));
}
});
});
}
function getUserFriendlyError(error, params) {
switch (error.code) {
case ErrorCodes.NETWORK_TIMEOUT:
return '⏱️ 请求超时,请检查网络连接后重试';
case ErrorCodes.INVALID_LOCATION:
return \`❌ 找不到城市 "\${params.location}",请检查城市名称\`;
case ErrorCodes.API_ERROR:
return '❌ 天气服务暂时不可用,请稍后重试';
default:
return '❌ 发生未知错误,请联系管理员';
}
}
const isDev = process.env.NODE_ENV === 'development';
if (isDev) {
context.logger.level = 'debug';
context.logger.debug('完整请求:', JSON.stringify(request, null, 2));
}
try {
// 可能出错的代码
} catch (error) {
context.logger.error('错误详情:', {
message: error.message,
stack: error.stack, // 完整堆栈
code: error.code,
details: error.details
});
throw error;
}
const timers = new Map();
function startTimer(name) {
timers.set(name, Date.now());
}
function endTimer(name, context) {
const duration = Date.now() - timers.get(name);
context?.logger.debug(\`计时:\${name} = \${duration}ms\`);
return duration;
}
// 使用
startTimer('api-call');
const result = await apiCall();
endTimer('api-call', context);
const assert = require('assert');
describe('Weather Skill 错误处理', () => {
it('应该处理网络超时', async () => {
const result = await weatherSkill.execute(
{ location: '北京' },
{
config: { weatherApiKey: 'test' },
logger: mockLogger
}
);
assert(result.includes('超时'));
});
it('应该处理无效城市', async () => {
const result = await weatherSkill.execute(
{ location: '无效城市' },
mockContext
);
assert(result.includes('找不到城市'));
});
it('应该处理缺少 API 密钥', async () => {
const result = await weatherSkill.execute(
{ location: '北京' },
{ config: {}, logger: mockLogger }
);
assert(result.includes('未配置'));
});
});
良好的错误处理和日志记录是高质量 Skill 的标志:
✅ 错误处理:
✅ 日志记录:
✅ 用户体验:
记住:用户不会看到你的代码,但会看到你的错误消息。让错误消息成为帮助用户解决问题的工具,而不是障碍。
发布分类:OpenClaw
标签:OpenClaw, 错误处理,日志,最佳实践,教程
字数:约 5,800 字
上一篇: Skill 参数设计:让用户用得爽
下一篇: Qwen3 Max 阿里通义千问旗舰版