🔍 JSON输出问题根本原因分析
为什么机器人有时返回大量JSON格式 | Ultrathink分析
❌ 问题:用户体验差,看到大段代码/JSON
📋 问题描述
用户反馈:
飞书机器人有时会返回大量JSON格式的内容,例如:
receive_id: chatId,
content: JSON.stringify({ file_key: fileKey }),
msg_type: 'file'
}
});
if (res.code === 0) {
console.log('[FeishuClient] File message sent successfully');
return {
success: true,
message_id: res.data?.message_id
};
} else {
throw new Error(`Feishu API error: ${res.code} - ${res.msg}`);
这种长篇大段的JSON/代码输出严重影响客户体验。
🔬 根本原因分析
数据流追踪
Claude CLI 进程 (--output-format stream-json)
↓
[stdout] 输出多种类型的内容:
├─ JSON 格式的响应(type: assistant, system, result 等)
├─ 非 JSON 的调试信息
├─ 工具调用的输出
└─ 错误和警告消息
↓
claude-cli.js:154-244 处理 stdout
├─ 尝试 JSON.parse(line)
├─ 成功 → 转发为 'claude-response'
└─ 失败 → 转发为 'claude-output' (纯文本)
↓
feishu-message-writer.js
├─ handleClaudeResponse() - 提取 text 字段
└─ handleClaudeOutput() - 直接追加原始文本
↓
appendText() → buffer → flush()
↓
发送到飞书用户 ❌
关键代码位置
| 文件:行号 |
问题代码 |
说明 |
| claude-cli.js:235-241 |
catch (parseError) {
ws.send(JSON.stringify({
type: 'claude-output',
data: line
}));
}
|
❌ 将所有非JSON内容作为原始文本转发 |
| feishu-message-writer.js:137-140 |
handleClaudeOutput(data) {
if (data && typeof data === 'string') {
this.appendText(data);
}
}
|
❌ 直接追加所有非JSON输出,无过滤 |
| claude-cli.js:247-252 |
claudeProcess.stderr.on('data', (data) => {
ws.send(JSON.stringify({
type: 'claude-error',
error: data.toString()
}));
});
|
❌ 将stderr调试信息也发送给用户 |
典型触发场景
场景 1: Claude 返回代码建议
用户问:"如何发送飞书文件消息?"
Claude 回复包含完整代码片段 → 代码被当作普通文本全部发送
场景 2: Claude CLI 调试输出
Claude CLI 输出非JSON的状态信息 → 被当作 'claude-output' 转发
场景 3: 工具调用结果
Claude 使用工具(如Read/Write)的输出可能包含大量结构化数据 → 直接显示给用户
场景 4: 错误堆栈
发生错误时,完整的错误堆栈被发送 → 用户看到技术细节
📊 影响分析
| 影响维度 |
严重程度 |
描述 |
| 用户体验 |
🔴 高 |
大段JSON/代码淹没真正的回复,难以阅读 |
| 信息噪音 |
🔴 高 |
用户看到不应该看到的系统内部信息 |
| 专业性 |
🟡 中 |
暴露技术细节,降低产品专业度 |
| 安全风险 |
🟡 中 |
可能泄露内部实现细节或敏感路径 |
✅ 解决方案设计
方案 A: 智能输出过滤器(推荐)
核心思路: 在 feishu-message-writer.js 添加智能过滤层,识别并过滤掉非用户内容
过滤规则:
1. 检测系统输出 - 忽略以 `[`, `{`, `console.log`, `Error:` 等开头的行
2. 检测调试信息 - 过滤包含 `[FeishuClient]`, `[ContextManager]` 等标记的内容
3. 检测代码片段 - 识别代码块(连续的函数/JSON结构),可选压缩或隐藏
4. 保留用户内容 - 只保留真正的自然语言回复
5. 错误美化 - 将技术错误转换为用户友好的提示
方案 B: 改进 Claude CLI 输出解析
核心思路: 在 claude-cli.js 更严格地解析和分类输出
实施步骤:
1. 区分 stdout 中的不同内容类型
2. 只转发 type='assistant' 的消息
3. 丢弃或记录到日志(而非转发)其他类型的输出
4. stderr 只记录到服务器日志,不发送给用户
方案 C: 混合方案(最优)
核心思路: 结合方案A和B,双重过滤
第一层:claude-cli.js(源头过滤)
- 只转发 JSON 格式的 assistant 消息
- stderr 写入日志文件,不转发
第二层:feishu-message-writer.js(智能过滤)
- 过滤掉明显的系统输出
- 美化错误消息
- 检测并折叠长代码块
第三层:用户友好化
- 长代码块 → "📄 代码建议已生成,是否需要查看详情?"
- JSON数据 → 折叠为摘要
- 错误堆栈 → "操作失败,请稍后重试"
推荐实施顺序
| 阶段 |
任务 |
优先级 |
工作量 |
| Phase 1 |
实现智能输出过滤器(filter-claude-output.js) |
🔴 P0 |
2小时 |
| Phase 2 |
集成到 feishu-message-writer.js |
🔴 P0 |
1小时 |
| Phase 3 |
改进 claude-cli.js 源头过滤 |
🟡 P1 |
1小时 |
| Phase 4 |
添加代码块折叠/摘要功能 |
🟢 P2 |
3小时 |
| Phase 5 |
测试和调优过滤规则 |
🔴 P0 |
2小时 |
📝 实现细节预览
智能过滤器核心逻辑
export class ClaudeOutputFilter {
constructor() {
// 系统输出特征(应该被过滤)
this.systemPatterns = [
/^\[[\w]+\]/, // [ModuleName] 前缀
/^{[\s\S]*}$/, // 纯JSON对象
/^\s*(console\.|Error:|throw)/, // 调试语句
/^(async\s+)?function\s+\w+/, // 函数定义
/^(const|let|var|import|export)/, // 代码声明
];
// 错误消息映射(技术 → 用户友好)
this.errorMap = {
'ENOENT': '文件未找到',
'EACCES': '权限不足',
'Claude CLI exited with code 1': '处理失败,请重试'
};
}
/**
* 检测文本是否为系统输出(应该被过滤)
*/
isSystemOutput(text) {
return this.systemPatterns.some(pattern => pattern.test(text));
}
/**
* 检测是否为代码块
*/
isCodeBlock(text) {
const lines = text.split('\\n');
if (lines.length < 3) return false;
// 检测代码特征
const codeIndicators = [
/^\s*[\{\}\[\]]/, // 大括号/方括号开头
/^\s*(if|for|while|function|const|let)/, // 关键字
/[;{}]\s*$/, // 分号或大括号结尾
];
const codeLineCount = lines.filter(line =>
codeIndicators.some(pattern => pattern.test(line))
).length;
return codeLineCount / lines.length > 0.5; // 超过50%的行是代码
}
/**
* 过滤和美化输出
*/
filter(text) {
// 空文本直接返回
if (!text || !text.trim()) return '';
// 检测整体类型
if (this.isSystemOutput(text)) {
console.log('[Filter] 过滤系统输出:', text.substring(0, 50));
return ''; // 完全过滤掉
}
if (this.isCodeBlock(text)) {
console.log('[Filter] 检测到代码块,折叠处理');
return '📄 代码建议已生成\\n(内容较长已折叠)';
}
// 错误消息美化
for (const [technical, friendly] of Object.entries(this.errorMap)) {
if (text.includes(technical)) {
return `⚠️ ${friendly}`;
}
}
// 正常文本,直接返回
return text;
}
}
集成到消息写入器
// feishu-message-writer.js
import { ClaudeOutputFilter } from './filter-claude-output.js';
export class FeishuMessageWriter {
constructor(...) {
// ... 现有代码 ...
this.outputFilter = new ClaudeOutputFilter(); // 新增
}
handleClaudeOutput(data) {
if (data && typeof data === 'string') {
// 过滤后再追加
const filtered = this.outputFilter.filter(data);
if (filtered) {
this.appendText(filtered);
}
}
}
handleError(error) {
console.error('[FeishuWriter] Claude error:', error);
// 美化错误消息,而非直接显示
const friendlyError = this.outputFilter.filter(error);
this.appendText(`\\n${friendlyError}\\n`);
}
}
🎯 预期效果
| 场景 |
之前 |
之后 |
| 代码建议 |
显示完整代码(500+行) |
📄 代码建议已生成 (内容较长已折叠) |
| JSON数据 |
显示原始JSON结构 |
(完全过滤,不显示) |
| 错误堆栈 |
Error: ENOENT: no such file... at Function.access (node:fs:...) |
⚠️ 文件未找到 |
| 系统日志 |
[FeishuClient] File message sent... |
(完全过滤,不显示) |
| 正常对话 |
Hello! How can I help you? |
Hello! How can I help you? ✅ |