feat: Enhance CLI output handling with structured Intermediate Representation (IR)

- Introduced `CliOutputUnit` and `IOutputParser` interfaces for unified output processing.
- Implemented `PlainTextParser` and `JsonLinesParser` for parsing raw CLI output into structured units.
- Updated `executeCliTool` to utilize output parsers and handle structured output.
- Added `flattenOutputUnits` utility for extracting clean output from structured data.
- Enhanced `ConversationTurn` and `ExecutionRecord` interfaces to include structured output.
- Created comprehensive documentation for CLI Output Converter usage and integration.
- Improved error handling and type mapping for various output formats.
This commit is contained in:
catlog22
2026-01-08 17:26:40 +08:00
parent b86cdd6644
commit d0523684e5
22 changed files with 1618 additions and 111 deletions

View File

@@ -118,8 +118,9 @@ export async function csrfValidation(ctx: CsrfMiddlewareContext): Promise<boolea
const method = (req.method || 'GET').toUpperCase();
if (!['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) return true;
// Always allow token acquisition routes.
// Always allow token acquisition routes and webhook endpoints.
if (pathname === '/api/auth/token') return true;
if (pathname === '/api/hook') return true;
// Requests authenticated via Authorization header do not require CSRF protection.
const authorization = getHeaderValue(req.headers.authorization);

View File

@@ -627,7 +627,18 @@ export async function handleClaudeRoutes(ctx: RouteContext): Promise<boolean> {
stream: false,
category: 'internal',
id: syncId
}, onOutput);
}, (unit) => {
// CliOutputUnit handler: convert to string content for broadcast
const content = typeof unit.content === 'string' ? unit.content : JSON.stringify(unit.content);
broadcastToClients({
type: 'CLI_OUTPUT',
payload: {
executionId: syncId,
chunkType: unit.type,
data: content
}
});
});
// Broadcast CLI_EXECUTION_COMPLETED event
broadcastToClients({

View File

@@ -195,7 +195,7 @@ export async function handleCliRoutes(ctx: RouteContext): Promise<boolean> {
}
// API: Get/Update Tool Config
const configMatch = pathname.match(/^\/api\/cli\/config\/(gemini|qwen|codex)$/);
const configMatch = pathname.match(/^\/api\/cli\/config\/(gemini|qwen|codex|claude|opencode)$/);
if (configMatch) {
const tool = configMatch[1];
@@ -216,7 +216,7 @@ export async function handleCliRoutes(ctx: RouteContext): Promise<boolean> {
if (req.method === 'PUT') {
handlePostRequest(req, res, async (body: unknown) => {
try {
const updates = body as { enabled?: boolean; primaryModel?: string; secondaryModel?: string };
const updates = body as { enabled?: boolean; primaryModel?: string; secondaryModel?: string; tags?: string[] };
const updated = updateToolConfig(initialPath, tool, updates);
// Broadcast config updated event
@@ -559,19 +559,22 @@ export async function handleCliRoutes(ctx: RouteContext): Promise<boolean> {
category: category || 'user',
parentExecutionId,
stream: true
}, (chunk) => {
// Append chunk to active execution buffer
}, (unit) => {
// CliOutputUnit handler: convert to string content
const content = typeof unit.content === 'string' ? unit.content : JSON.stringify(unit.content);
// Append to active execution buffer
const activeExec = activeExecutions.get(executionId);
if (activeExec) {
activeExec.output += chunk.data || '';
activeExec.output += content || '';
}
broadcastToClients({
type: 'CLI_OUTPUT',
payload: {
executionId,
chunkType: chunk.type,
data: chunk.data
chunkType: unit.type,
data: content
}
});
});

View File

@@ -1007,7 +1007,18 @@ RULES: Be concise. Focus on practical understanding. Include function signatures
stream: false,
category: 'internal',
id: syncId
}, onOutput);
}, (unit) => {
// CliOutputUnit handler: convert to string content for broadcast
const content = typeof unit.content === 'string' ? unit.content : JSON.stringify(unit.content);
broadcastToClients({
type: 'CLI_OUTPUT',
payload: {
executionId: syncId,
chunkType: unit.type,
data: content
}
});
});
// Broadcast CLI_EXECUTION_COMPLETED event
broadcastToClients({

View File

@@ -661,13 +661,15 @@ FILE NAME: ${fileName}`;
// Create onOutput callback for real-time streaming
const onOutput = broadcastToClients
? (chunk: { type: string; data: string }) => {
? (unit: import('../../tools/cli-output-converter.js').CliOutputUnit) => {
// CliOutputUnit handler: convert to string content for broadcast
const content = typeof unit.content === 'string' ? unit.content : JSON.stringify(unit.content);
broadcastToClients({
type: 'CLI_OUTPUT',
payload: {
executionId,
chunkType: chunk.type,
data: chunk.data
chunkType: unit.type,
data: content
}
});
}
@@ -746,13 +748,15 @@ FILE NAME: ${fileName}`;
// Create onOutput callback for review step
const reviewOnOutput = broadcastToClients
? (chunk: { type: string; data: string }) => {
? (unit: import('../../tools/cli-output-converter.js').CliOutputUnit) => {
// CliOutputUnit handler: convert to string content for broadcast
const content = typeof unit.content === 'string' ? unit.content : JSON.stringify(unit.content);
broadcastToClients({
type: 'CLI_OUTPUT',
payload: {
executionId: reviewExecutionId,
chunkType: chunk.type,
data: chunk.data
chunkType: unit.type,
data: content
}
});
}

View File

@@ -579,13 +579,15 @@ Create a new Claude Code skill with the following specifications:
// Create onOutput callback for real-time streaming
const onOutput = broadcastToClients
? (chunk: { type: string; data: string }) => {
? (unit: import('../../tools/cli-output-converter.js').CliOutputUnit) => {
// CliOutputUnit handler: convert to string content for broadcast
const content = typeof unit.content === 'string' ? unit.content : JSON.stringify(unit.content);
broadcastToClients({
type: 'CLI_OUTPUT',
payload: {
executionId,
chunkType: chunk.type,
data: chunk.data
chunkType: unit.type,
data: content
}
});
}

View File

@@ -357,7 +357,7 @@ export async function startServer(options: ServerOptions = {}): Promise<http.Ser
const tokenManager = getTokenManager();
const secretKey = tokenManager.getSecretKey();
tokenManager.getOrCreateAuthToken();
const unauthenticatedPaths = new Set<string>(['/api/auth/token', '/api/csrf-token']);
const unauthenticatedPaths = new Set<string>(['/api/auth/token', '/api/csrf-token', '/api/hook']);
const server = http.createServer(async (req, res) => {
const url = new URL(req.url ?? '/', `http://localhost:${serverPort}`);