mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-14 02:42:04 +08:00
feat: 更新 SmartContentFormatter,确保格式化内容始终返回可显示的字符串
This commit is contained in:
@@ -794,8 +794,8 @@ async function execAction(positionalPrompt: string | undefined, options: CliExec
|
|||||||
// Always broadcast to dashboard for real-time viewing
|
// Always broadcast to dashboard for real-time viewing
|
||||||
// Note: /api/hook wraps extraData into payload, so send fields directly
|
// Note: /api/hook wraps extraData into payload, so send fields directly
|
||||||
// Maintain backward compatibility with frontend expecting { chunkType, data }
|
// Maintain backward compatibility with frontend expecting { chunkType, data }
|
||||||
// Use SmartContentFormatter for intelligent content formatting
|
// Use SmartContentFormatter for intelligent content formatting (never returns null)
|
||||||
const content = SmartContentFormatter.format(unit.content, unit.type) || JSON.stringify(unit.content);
|
const content = SmartContentFormatter.format(unit.content, unit.type);
|
||||||
broadcastStreamEvent('CLI_OUTPUT', {
|
broadcastStreamEvent('CLI_OUTPUT', {
|
||||||
executionId,
|
executionId,
|
||||||
chunkType: unit.type, // For backward compatibility
|
chunkType: unit.type, // For backward compatibility
|
||||||
|
|||||||
@@ -629,8 +629,8 @@ export async function handleClaudeRoutes(ctx: RouteContext): Promise<boolean> {
|
|||||||
category: 'internal',
|
category: 'internal',
|
||||||
id: syncId
|
id: syncId
|
||||||
}, (unit) => {
|
}, (unit) => {
|
||||||
// CliOutputUnit handler: use SmartContentFormatter for intelligent formatting
|
// CliOutputUnit handler: use SmartContentFormatter for intelligent formatting (never returns null)
|
||||||
const content = SmartContentFormatter.format(unit.content, unit.type) || JSON.stringify(unit.content);
|
const content = SmartContentFormatter.format(unit.content, unit.type);
|
||||||
broadcastToClients({
|
broadcastToClients({
|
||||||
type: 'CLI_OUTPUT',
|
type: 'CLI_OUTPUT',
|
||||||
payload: {
|
payload: {
|
||||||
|
|||||||
@@ -565,8 +565,8 @@ export async function handleCliRoutes(ctx: RouteContext): Promise<boolean> {
|
|||||||
parentExecutionId,
|
parentExecutionId,
|
||||||
stream: true
|
stream: true
|
||||||
}, (unit) => {
|
}, (unit) => {
|
||||||
// CliOutputUnit handler: use SmartContentFormatter for intelligent formatting
|
// CliOutputUnit handler: use SmartContentFormatter for intelligent formatting (never returns null)
|
||||||
const content = SmartContentFormatter.format(unit.content, unit.type) || JSON.stringify(unit.content);
|
const content = SmartContentFormatter.format(unit.content, unit.type);
|
||||||
|
|
||||||
// Append to active execution buffer
|
// Append to active execution buffer
|
||||||
const activeExec = activeExecutions.get(executionId);
|
const activeExec = activeExecutions.get(executionId);
|
||||||
|
|||||||
@@ -1009,8 +1009,8 @@ RULES: Be concise. Focus on practical understanding. Include function signatures
|
|||||||
category: 'internal',
|
category: 'internal',
|
||||||
id: syncId
|
id: syncId
|
||||||
}, (unit) => {
|
}, (unit) => {
|
||||||
// CliOutputUnit handler: use SmartContentFormatter for intelligent formatting
|
// CliOutputUnit handler: use SmartContentFormatter for intelligent formatting (never returns null)
|
||||||
const content = SmartContentFormatter.format(unit.content, unit.type) || JSON.stringify(unit.content);
|
const content = SmartContentFormatter.format(unit.content, unit.type);
|
||||||
broadcastToClients({
|
broadcastToClients({
|
||||||
type: 'CLI_OUTPUT',
|
type: 'CLI_OUTPUT',
|
||||||
payload: {
|
payload: {
|
||||||
|
|||||||
@@ -663,8 +663,8 @@ FILE NAME: ${fileName}`;
|
|||||||
// Create onOutput callback for real-time streaming
|
// Create onOutput callback for real-time streaming
|
||||||
const onOutput = broadcastToClients
|
const onOutput = broadcastToClients
|
||||||
? (unit: import('../../tools/cli-output-converter.js').CliOutputUnit) => {
|
? (unit: import('../../tools/cli-output-converter.js').CliOutputUnit) => {
|
||||||
// CliOutputUnit handler: use SmartContentFormatter for intelligent formatting
|
// CliOutputUnit handler: use SmartContentFormatter for intelligent formatting (never returns null)
|
||||||
const content = SmartContentFormatter.format(unit.content, unit.type) || JSON.stringify(unit.content);
|
const content = SmartContentFormatter.format(unit.content, unit.type);
|
||||||
broadcastToClients({
|
broadcastToClients({
|
||||||
type: 'CLI_OUTPUT',
|
type: 'CLI_OUTPUT',
|
||||||
payload: {
|
payload: {
|
||||||
@@ -750,8 +750,8 @@ FILE NAME: ${fileName}`;
|
|||||||
// Create onOutput callback for review step
|
// Create onOutput callback for review step
|
||||||
const reviewOnOutput = broadcastToClients
|
const reviewOnOutput = broadcastToClients
|
||||||
? (unit: import('../../tools/cli-output-converter.js').CliOutputUnit) => {
|
? (unit: import('../../tools/cli-output-converter.js').CliOutputUnit) => {
|
||||||
// CliOutputUnit handler: use SmartContentFormatter for intelligent formatting
|
// CliOutputUnit handler: use SmartContentFormatter for intelligent formatting (never returns null)
|
||||||
const content = SmartContentFormatter.format(unit.content, unit.type) || JSON.stringify(unit.content);
|
const content = SmartContentFormatter.format(unit.content, unit.type);
|
||||||
broadcastToClients({
|
broadcastToClients({
|
||||||
type: 'CLI_OUTPUT',
|
type: 'CLI_OUTPUT',
|
||||||
payload: {
|
payload: {
|
||||||
|
|||||||
@@ -581,8 +581,8 @@ Create a new Claude Code skill with the following specifications:
|
|||||||
// Create onOutput callback for real-time streaming
|
// Create onOutput callback for real-time streaming
|
||||||
const onOutput = broadcastToClients
|
const onOutput = broadcastToClients
|
||||||
? (unit: import('../../tools/cli-output-converter.js').CliOutputUnit) => {
|
? (unit: import('../../tools/cli-output-converter.js').CliOutputUnit) => {
|
||||||
// CliOutputUnit handler: use SmartContentFormatter for intelligent formatting
|
// CliOutputUnit handler: use SmartContentFormatter for intelligent formatting (never returns null)
|
||||||
const content = SmartContentFormatter.format(unit.content, unit.type) || JSON.stringify(unit.content);
|
const content = SmartContentFormatter.format(unit.content, unit.type);
|
||||||
broadcastToClients({
|
broadcastToClients({
|
||||||
type: 'CLI_OUTPUT',
|
type: 'CLI_OUTPUT',
|
||||||
payload: {
|
payload: {
|
||||||
|
|||||||
@@ -712,43 +712,94 @@ export class JsonLinesParser implements IOutputParser {
|
|||||||
export class SmartContentFormatter {
|
export class SmartContentFormatter {
|
||||||
/**
|
/**
|
||||||
* Format structured content into human-readable text
|
* Format structured content into human-readable text
|
||||||
* Returns formatted string or null if should use original content
|
* NEVER returns null - always returns displayable content to prevent data loss
|
||||||
*/
|
*/
|
||||||
static format(content: any, type: CliOutputUnitType): string | null {
|
static format(content: any, type: CliOutputUnitType): string {
|
||||||
|
// Handle null/undefined
|
||||||
|
if (content === null || content === undefined) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// String content - return as-is
|
||||||
if (typeof content === 'string') {
|
if (typeof content === 'string') {
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof content !== 'object' || content === null) {
|
// Primitive types - convert to string
|
||||||
|
if (typeof content !== 'object') {
|
||||||
return String(content);
|
return String(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Type-specific formatting
|
// Type-specific formatting with fallback chain
|
||||||
|
let result: string | null = null;
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'metadata':
|
case 'metadata':
|
||||||
return this.formatMetadata(content);
|
result = this.formatMetadata(content);
|
||||||
|
break;
|
||||||
case 'progress':
|
case 'progress':
|
||||||
return this.formatProgress(content);
|
result = this.formatProgress(content);
|
||||||
|
break;
|
||||||
case 'tool_call':
|
case 'tool_call':
|
||||||
return this.formatToolCall(content);
|
result = this.formatToolCall(content);
|
||||||
|
break;
|
||||||
case 'code':
|
case 'code':
|
||||||
return this.formatCode(content);
|
result = this.formatCode(content);
|
||||||
|
break;
|
||||||
case 'file_diff':
|
case 'file_diff':
|
||||||
return this.formatFileDiff(content);
|
result = this.formatFileDiff(content);
|
||||||
|
break;
|
||||||
case 'thought':
|
case 'thought':
|
||||||
return this.formatThought(content);
|
result = this.formatThought(content);
|
||||||
|
break;
|
||||||
case 'system':
|
case 'system':
|
||||||
return this.formatSystem(content);
|
result = this.formatSystem(content);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
// Try to extract text content from common fields
|
// Try to extract text content from common fields
|
||||||
return this.extractTextContent(content);
|
result = this.extractTextContent(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If type-specific formatting succeeded, return it
|
||||||
|
if (result && result.trim()) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: try to extract any text content regardless of type
|
||||||
|
const textContent = this.extractTextContent(content);
|
||||||
|
if (textContent && textContent.trim()) {
|
||||||
|
return textContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last resort: format as readable JSON with type hint
|
||||||
|
return this.formatAsReadableJson(content, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format object as readable JSON with type hint (fallback for unknown content)
|
||||||
|
* Ensures content is never lost
|
||||||
|
*/
|
||||||
|
private static formatAsReadableJson(content: any, type: CliOutputUnitType): string {
|
||||||
|
try {
|
||||||
|
const jsonStr = JSON.stringify(content, null, 0);
|
||||||
|
// For short content, show inline; for long content, indicate it's data
|
||||||
|
if (jsonStr.length <= 200) {
|
||||||
|
return `[${type}] ${jsonStr}`;
|
||||||
|
}
|
||||||
|
// For long content, show truncated with type indicator
|
||||||
|
return `[${type}] ${jsonStr.substring(0, 200)}...`;
|
||||||
|
} catch {
|
||||||
|
// If JSON.stringify fails, try to extract keys
|
||||||
|
const keys = Object.keys(content).slice(0, 5).join(', ');
|
||||||
|
return `[${type}] {${keys}${Object.keys(content).length > 5 ? ', ...' : ''}}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format metadata (session info, stats, etc.)
|
* Format metadata (session info, stats, etc.)
|
||||||
|
* Returns null if no meaningful metadata could be extracted
|
||||||
*/
|
*/
|
||||||
private static formatMetadata(content: any): string {
|
private static formatMetadata(content: any): string | null {
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
|
|
||||||
// Tool identifier
|
// Tool identifier
|
||||||
@@ -772,6 +823,11 @@ export class SmartContentFormatter {
|
|||||||
parts.push(`Status: ${content.status}`);
|
parts.push(`Status: ${content.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reason (for step_finish events)
|
||||||
|
if (content.reason) {
|
||||||
|
parts.push(`Reason: ${content.reason}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Duration
|
// Duration
|
||||||
if (content.durationMs || content.duration_ms) {
|
if (content.durationMs || content.duration_ms) {
|
||||||
const ms = content.durationMs || content.duration_ms;
|
const ms = content.durationMs || content.duration_ms;
|
||||||
@@ -785,8 +841,8 @@ export class SmartContentFormatter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cost
|
// Cost
|
||||||
if (content.totalCostUsd || content.total_cost_usd || content.cost) {
|
if (content.totalCostUsd !== undefined || content.total_cost_usd !== undefined || content.cost !== undefined) {
|
||||||
const cost = content.totalCostUsd || content.total_cost_usd || content.cost;
|
const cost = content.totalCostUsd ?? content.total_cost_usd ?? content.cost;
|
||||||
parts.push(`Cost: $${typeof cost === 'number' ? cost.toFixed(6) : cost}`);
|
parts.push(`Cost: $${typeof cost === 'number' ? cost.toFixed(6) : cost}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -795,13 +851,15 @@ export class SmartContentFormatter {
|
|||||||
parts.push(`Result: ${this.truncate(content.result, 100)}`);
|
parts.push(`Result: ${this.truncate(content.result, 100)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return parts.length > 0 ? parts.join(' | ') : JSON.stringify(content);
|
// Return null if no meaningful parts extracted (let fallback handle it)
|
||||||
|
return parts.length > 0 ? parts.join(' | ') : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format progress updates
|
* Format progress updates
|
||||||
|
* Returns null if no meaningful progress info could be extracted
|
||||||
*/
|
*/
|
||||||
private static formatProgress(content: any): string {
|
private static formatProgress(content: any): string | null {
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
|
|
||||||
// Tool identifier
|
// Tool identifier
|
||||||
@@ -824,13 +882,14 @@ export class SmartContentFormatter {
|
|||||||
parts.push(`[${content.progress}/${content.total}]`);
|
parts.push(`[${content.progress}/${content.total}]`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Session ID (brief)
|
// Session ID (brief) - only show if no message (avoid duplication)
|
||||||
const sessionId = content.sessionId || content.session_id;
|
const sessionId = content.sessionId || content.session_id;
|
||||||
if (sessionId && !content.message) {
|
if (sessionId && !content.message) {
|
||||||
parts.push(`Session: ${this.truncate(sessionId, 12)}`);
|
parts.push(`Session: ${this.truncate(sessionId, 12)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return parts.length > 0 ? parts.join(' ') : JSON.stringify(content);
|
// Return null if no meaningful parts extracted (let fallback handle it)
|
||||||
|
return parts.length > 0 ? parts.join(' ') : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -900,24 +959,26 @@ export class SmartContentFormatter {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Format thought/reasoning
|
* Format thought/reasoning
|
||||||
|
* Returns null if no text content could be extracted
|
||||||
*/
|
*/
|
||||||
private static formatThought(content: any): string {
|
private static formatThought(content: any): string | null {
|
||||||
if (typeof content === 'string') {
|
if (typeof content === 'string') {
|
||||||
return `💭 ${content}`;
|
return `💭 ${content}`;
|
||||||
}
|
}
|
||||||
const text = content.text || content.summary || content.content;
|
const text = content.text || content.summary || content.content || content.thinking;
|
||||||
return text ? `💭 ${text}` : JSON.stringify(content);
|
return text ? `💭 ${text}` : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format system message
|
* Format system message
|
||||||
|
* Returns null if no message content could be extracted
|
||||||
*/
|
*/
|
||||||
private static formatSystem(content: any): string {
|
private static formatSystem(content: any): string | null {
|
||||||
if (typeof content === 'string') {
|
if (typeof content === 'string') {
|
||||||
return `⚙️ ${content}`;
|
return `⚙️ ${content}`;
|
||||||
}
|
}
|
||||||
const message = content.message || content.content || content.event;
|
const message = content.message || content.content || content.event || content.info;
|
||||||
return message ? `⚙️ ${message}` : JSON.stringify(content);
|
return message ? `⚙️ ${message}` : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user