refactor: eliminate code duplication with shared utilities

Created 5 new utility modules in apps/server/src/lib/ to eliminate ~320 lines of duplicated code:
- image-handler.ts: Centralized image processing (MIME types, base64, content blocks)
- prompt-builder.ts: Standardized prompt building with image attachments
- model-resolver.ts: Model alias resolution and provider routing
- conversation-utils.ts: Conversation history processing for providers
- error-handler.ts: Error classification and user-friendly messages

Updated services and providers to use shared utilities:
- agent-service.ts: -51 lines (removed duplicate image handling, model logic)
- auto-mode-service.ts: -75 lines (removed MODEL_MAP, duplicate utilities)
- claude-provider.ts: -10 lines (uses conversation-utils)
- codex-provider.ts: -5 lines (uses conversation-utils)

Added comprehensive documentation:
- docs/server/utilities.md: Complete reference for all 9 lib utilities
- docs/server/providers.md: Provider architecture guide with examples

Benefits:
- Single source of truth for critical business logic
- Improved maintainability and testability
- Consistent behavior across services and providers
- Better documentation for future development

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Kacper
2025-12-13 04:26:58 +01:00
parent 0519aba820
commit 7cbdb3db73
11 changed files with 2008 additions and 188 deletions

View File

@@ -9,6 +9,12 @@ import fs from "fs/promises";
import type { EventEmitter } from "../lib/events.js";
import { ProviderFactory } from "../providers/provider-factory.js";
import type { ExecuteOptions } from "../providers/types.js";
import {
readImageAsBase64,
} from "../lib/image-handler.js";
import { buildPromptWithImages } from "../lib/prompt-builder.js";
import { getEffectiveModel } from "../lib/model-resolver.js";
import { isAbortError } from "../lib/error-handler.js";
interface Message {
id: string;
@@ -123,22 +129,11 @@ export class AgentService {
if (imagePaths && imagePaths.length > 0) {
for (const imagePath of imagePaths) {
try {
const imageBuffer = await fs.readFile(imagePath);
const base64Data = imageBuffer.toString("base64");
const ext = path.extname(imagePath).toLowerCase();
const mimeTypeMap: Record<string, string> = {
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".gif": "image/gif",
".webp": "image/webp",
};
const mediaType = mimeTypeMap[ext] || "image/png";
const imageData = await readImageAsBase64(imagePath);
images.push({
data: base64Data,
mimeType: mediaType,
filename: path.basename(imagePath),
data: imageData.base64,
mimeType: imageData.mimeType,
filename: imageData.filename,
});
} catch (error) {
console.error(`[AgentService] Failed to load image ${imagePath}:`, error);
@@ -175,7 +170,7 @@ export class AgentService {
try {
// Use session model, parameter model, or default
const effectiveModel = model || session.model || "claude-opus-4-5-20251101";
const effectiveModel = getEffectiveModel(model, session.model);
// Get provider for this model
const provider = ProviderFactory.getProviderForModel(effectiveModel);
@@ -205,59 +200,13 @@ export class AgentService {
conversationHistory: conversationHistory.length > 0 ? conversationHistory : undefined,
};
// Build prompt content
let promptContent: string | Array<{ type: string; text?: string; source?: object }> =
message;
// Append image paths to prompt text (like old implementation)
if (imagePaths && imagePaths.length > 0) {
let enhancedMessage = message;
// Append image file paths to the message text
enhancedMessage += "\n\nAttached images:\n";
for (const imagePath of imagePaths) {
enhancedMessage += `- ${imagePath}\n`;
}
const contentBlocks: Array<{ type: string; text?: string; source?: object }> = [];
if (enhancedMessage && enhancedMessage.trim()) {
contentBlocks.push({ type: "text", text: enhancedMessage });
}
for (const imagePath of imagePaths) {
try {
const imageBuffer = await fs.readFile(imagePath);
const base64Data = imageBuffer.toString("base64");
const ext = path.extname(imagePath).toLowerCase();
const mimeTypeMap: Record<string, string> = {
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".gif": "image/gif",
".webp": "image/webp",
};
const mediaType = mimeTypeMap[ext] || "image/png";
contentBlocks.push({
type: "image",
source: {
type: "base64",
media_type: mediaType,
data: base64Data,
},
});
} catch (error) {
console.error(`[AgentService] Failed to load image ${imagePath}:`, error);
}
}
if (contentBlocks.length > 1 || contentBlocks[0]?.type === "image") {
promptContent = contentBlocks;
} else {
promptContent = enhancedMessage;
}
}
// Build prompt content with images
const { content: promptContent } = await buildPromptWithImages(
message,
imagePaths,
undefined, // no workDir for agent service
true // include image paths in text
);
// Set the prompt in options
options.prompt = promptContent;
@@ -335,7 +284,7 @@ export class AgentService {
message: currentAssistantMessage,
};
} catch (error) {
if (error instanceof AbortError || (error as Error)?.name === "AbortError") {
if (isAbortError(error)) {
session.isRunning = false;
session.abortController = null;
return { success: false, aborted: true };