🔍 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? ✅