@kaitranntt/ccs 7.65.2 → 7.65.3-dev.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -0
- package/dist/api/services/profile-lifecycle-service.d.ts.map +1 -1
- package/dist/api/services/profile-lifecycle-service.js +4 -0
- package/dist/api/services/profile-lifecycle-service.js.map +1 -1
- package/dist/api/services/profile-writer.d.ts.map +1 -1
- package/dist/api/services/profile-writer.js +3 -0
- package/dist/api/services/profile-writer.js.map +1 -1
- package/dist/ccs.js +32 -3
- package/dist/ccs.js.map +1 -1
- package/dist/cliproxy/executor/env-resolver.d.ts +3 -0
- package/dist/cliproxy/executor/env-resolver.d.ts.map +1 -1
- package/dist/cliproxy/executor/env-resolver.js +19 -1
- package/dist/cliproxy/executor/env-resolver.js.map +1 -1
- package/dist/cliproxy/executor/index.d.ts.map +1 -1
- package/dist/cliproxy/executor/index.js +24 -5
- package/dist/cliproxy/executor/index.js.map +1 -1
- package/dist/cliproxy/services/variant-settings.d.ts.map +1 -1
- package/dist/cliproxy/services/variant-settings.js +11 -0
- package/dist/cliproxy/services/variant-settings.js.map +1 -1
- package/dist/commands/help-command.js +4 -4
- package/dist/commands/help-command.js.map +1 -1
- package/dist/commands/install-command.d.ts.map +1 -1
- package/dist/commands/install-command.js +16 -3
- package/dist/commands/install-command.js.map +1 -1
- package/dist/copilot/copilot-executor.d.ts +2 -0
- package/dist/copilot/copilot-executor.d.ts.map +1 -1
- package/dist/copilot/copilot-executor.js +36 -4
- package/dist/copilot/copilot-executor.js.map +1 -1
- package/dist/delegation/headless-executor.d.ts.map +1 -1
- package/dist/delegation/headless-executor.js +79 -2
- package/dist/delegation/headless-executor.js.map +1 -1
- package/dist/management/checks/image-analysis-check.d.ts.map +1 -1
- package/dist/management/checks/image-analysis-check.js +4 -5
- package/dist/management/checks/image-analysis-check.js.map +1 -1
- package/dist/management/instance-manager.js +1 -1
- package/dist/management/instance-manager.js.map +1 -1
- package/dist/ui/assets/{accounts-BHEYnq6b.js → accounts-Dh95PibK.js} +1 -1
- package/dist/ui/assets/{alert-dialog-D0EFRcfB.js → alert-dialog-C5RdUHi9.js} +1 -1
- package/dist/ui/assets/{api-DhM3BYXr.js → api-C0ROFLme.js} +1 -1
- package/dist/ui/assets/{auth-section-DVp8FQGm.js → auth-section-M2azTP3G.js} +1 -1
- package/dist/ui/assets/{backups-section-CRo0NZkA.js → backups-section-DIDUVa0t.js} +1 -1
- package/dist/ui/assets/{channels-uZ_9CBqO.js → channels-D_5uerEp.js} +1 -1
- package/dist/ui/assets/{checkbox-32DNqW_Q.js → checkbox-CgMg7fDH.js} +1 -1
- package/dist/ui/assets/{claude-extension-BfXlz5gV.js → claude-extension-DA9wMzPz.js} +1 -1
- package/dist/ui/assets/{cliproxy-DjNY9H-U.js → cliproxy-4yUL1fQw.js} +1 -1
- package/dist/ui/assets/{cliproxy-ai-providers-5SHLMHiy.js → cliproxy-ai-providers-DedMcdcc.js} +1 -1
- package/dist/ui/assets/{cliproxy-control-panel-Zax_m1AC.js → cliproxy-control-panel-B0kwxgNi.js} +1 -1
- package/dist/ui/assets/{codex-CRUSpjsu.js → codex-CAWw4ZNl.js} +1 -1
- package/dist/ui/assets/{confirm-dialog-DVf5ZmCZ.js → confirm-dialog-Ds0PYz2R.js} +1 -1
- package/dist/ui/assets/{copilot-BZrihl_Z.js → copilot-m6i00mFy.js} +1 -1
- package/dist/ui/assets/{cursor-BP4nbEk_.js → cursor-COeD0Dgq.js} +1 -1
- package/dist/ui/assets/{droid-BG92rdM2.js → droid-CznUyiRx.js} +1 -1
- package/dist/ui/assets/{globalenv-section-Cf6dKgSf.js → globalenv-section-FgK1eGWk.js} +1 -1
- package/dist/ui/assets/{health-BTy1UZs3.js → health-Cpu6bD6K.js} +1 -1
- package/dist/ui/assets/{index-DuRYaONg.js → index-Bhz6T039.js} +1 -1
- package/dist/ui/assets/{index-N2ZSJurX.js → index-C7sG68Mi.js} +1 -1
- package/dist/ui/assets/index-CcKb4PL_.js +69 -0
- package/dist/ui/assets/{index-wg7UtkFv.js → index-DampXntj.js} +1 -1
- package/dist/ui/assets/{index-BVeN0dIB.js → index-DgnxlKNk.js} +1 -1
- package/dist/ui/assets/{index-DHrTq-0n.js → index-rTSyskt3.js} +1 -1
- package/dist/ui/assets/{masked-input-DX9bedLy.js → masked-input-B_l4FMkE.js} +1 -1
- package/dist/ui/assets/{proxy-status-widget-DVDMuZK5.js → proxy-status-widget-C7wSbfPC.js} +1 -1
- package/dist/ui/assets/{raw-json-settings-editor-panel-Dkt5E6Z_.js → raw-json-settings-editor-panel-CViWFt6t.js} +1 -1
- package/dist/ui/assets/{searchable-select-BP3Q1-Yn.js → searchable-select-7-yJbbw2.js} +1 -1
- package/dist/ui/assets/{separator-BLGGUlh9.js → separator-DApM4Wa5.js} +1 -1
- package/dist/ui/assets/{shared-G0XRyLig.js → shared-Blmm7sMd.js} +1 -1
- package/dist/ui/assets/{table-B4lRrWC-.js → table-BwM4zncv.js} +1 -1
- package/dist/ui/assets/{updates--A2Sdo7N.js → updates-DJ0ofB67.js} +1 -1
- package/dist/ui/index.html +1 -1
- package/dist/utils/hooks/get-image-analysis-hook-env.d.ts +26 -0
- package/dist/utils/hooks/get-image-analysis-hook-env.d.ts.map +1 -1
- package/dist/utils/hooks/get-image-analysis-hook-env.js +79 -1
- package/dist/utils/hooks/get-image-analysis-hook-env.js.map +1 -1
- package/dist/utils/hooks/image-analysis-backend-resolver.js +5 -5
- package/dist/utils/hooks/image-analysis-backend-resolver.js.map +1 -1
- package/dist/utils/hooks/image-analysis-runtime-status.d.ts +2 -0
- package/dist/utils/hooks/image-analysis-runtime-status.d.ts.map +1 -1
- package/dist/utils/hooks/image-analysis-runtime-status.js +15 -11
- package/dist/utils/hooks/image-analysis-runtime-status.js.map +1 -1
- package/dist/utils/hooks/image-analyzer-hook-installer.d.ts.map +1 -1
- package/dist/utils/hooks/image-analyzer-hook-installer.js +60 -27
- package/dist/utils/hooks/image-analyzer-hook-installer.js.map +1 -1
- package/dist/utils/hooks/image-analyzer-profile-hook-injector.d.ts.map +1 -1
- package/dist/utils/hooks/image-analyzer-profile-hook-injector.js +3 -0
- package/dist/utils/hooks/image-analyzer-profile-hook-injector.js.map +1 -1
- package/dist/utils/hooks/index.d.ts +2 -1
- package/dist/utils/hooks/index.d.ts.map +1 -1
- package/dist/utils/hooks/index.js +14 -7
- package/dist/utils/hooks/index.js.map +1 -1
- package/dist/utils/image-analysis/claude-tool-args.d.ts +6 -0
- package/dist/utils/image-analysis/claude-tool-args.d.ts.map +1 -0
- package/dist/utils/image-analysis/claude-tool-args.js +65 -0
- package/dist/utils/image-analysis/claude-tool-args.js.map +1 -0
- package/dist/utils/image-analysis/index.d.ts +4 -0
- package/dist/utils/image-analysis/index.d.ts.map +1 -1
- package/dist/utils/image-analysis/index.js +20 -1
- package/dist/utils/image-analysis/index.js.map +1 -1
- package/dist/utils/image-analysis/mcp-installer.d.ts +18 -0
- package/dist/utils/image-analysis/mcp-installer.d.ts.map +1 -0
- package/dist/utils/image-analysis/mcp-installer.js +447 -0
- package/dist/utils/image-analysis/mcp-installer.js.map +1 -0
- package/dist/web-server/routes/image-analysis-routes.d.ts.map +1 -1
- package/dist/web-server/routes/image-analysis-routes.js +30 -5
- package/dist/web-server/routes/image-analysis-routes.js.map +1 -1
- package/lib/hooks/image-analysis-runtime.cjs +469 -0
- package/lib/hooks/image-analyzer-transformer.cjs +27 -418
- package/lib/mcp/ccs-image-analysis-server.cjs +440 -0
- package/package.json +1 -1
- package/scripts/github/normalize-ai-review-output.mjs +95 -33
- package/dist/ui/assets/index-Corv1lSo.js +0 -69
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const http = require('http');
|
|
3
|
+
const https = require('https');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.heic', '.bmp', '.tiff'];
|
|
7
|
+
const PDF_EXTENSIONS = ['.pdf'];
|
|
8
|
+
const DEFAULT_MODEL = 'gemini-2.5-flash';
|
|
9
|
+
const DEFAULT_TIMEOUT_SEC = 60;
|
|
10
|
+
const MAX_FILE_SIZE_MB = 10;
|
|
11
|
+
const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024;
|
|
12
|
+
const MAX_PROMPT_TEMPLATE_BYTES = 32 * 1024;
|
|
13
|
+
const SCREENSHOT_NAME_REGEX =
|
|
14
|
+
/(screen[-_ ]?shot|screen[-_ ]?capture|screencap|snapshot|snip|clip|capture)/i;
|
|
15
|
+
const TEMPLATE_FILE_NAMES = {
|
|
16
|
+
default: 'default.txt',
|
|
17
|
+
screenshot: 'screenshot.txt',
|
|
18
|
+
document: 'document.txt',
|
|
19
|
+
};
|
|
20
|
+
const FALLBACK_PROMPTS = {
|
|
21
|
+
default: `Analyze this image/document thoroughly and provide a detailed description.
|
|
22
|
+
|
|
23
|
+
Include:
|
|
24
|
+
1. Overall content and purpose
|
|
25
|
+
2. Text content (if any) - transcribe important text verbatim
|
|
26
|
+
3. Visual elements (diagrams, charts, UI components, icons)
|
|
27
|
+
4. Layout and structure (sections, hierarchy, flow)
|
|
28
|
+
5. Colors, styling, notable design elements
|
|
29
|
+
6. Any actionable information (buttons, links, code snippets)
|
|
30
|
+
|
|
31
|
+
Be comprehensive - this description replaces direct visual access.
|
|
32
|
+
The AI assistant reading this cannot see the original image.`,
|
|
33
|
+
screenshot: `Analyze this screenshot in detail for a developer who cannot see it.
|
|
34
|
+
|
|
35
|
+
Focus on:
|
|
36
|
+
1. Application/website type and state
|
|
37
|
+
2. UI elements visible (buttons, inputs, menus, modals)
|
|
38
|
+
3. All text content - transcribe exactly
|
|
39
|
+
4. Error messages or notifications (quote exactly)
|
|
40
|
+
5. Layout and component hierarchy
|
|
41
|
+
6. Interactive elements and their states
|
|
42
|
+
7. Console output or logs if visible
|
|
43
|
+
8. Any code snippets shown
|
|
44
|
+
|
|
45
|
+
Be precise - this enables the assistant to help debug or understand the UI.`,
|
|
46
|
+
document: `Analyze this document/PDF thoroughly for a developer.
|
|
47
|
+
|
|
48
|
+
Extract and provide:
|
|
49
|
+
1. Document title, type, and structure
|
|
50
|
+
2. All text content - transcribe in reading order
|
|
51
|
+
3. Tables - format as markdown tables
|
|
52
|
+
4. Lists and bullet points - preserve structure
|
|
53
|
+
5. Code blocks or technical content
|
|
54
|
+
6. Diagrams or flowcharts - describe in detail
|
|
55
|
+
7. Headers and section organization
|
|
56
|
+
8. Any important metadata visible
|
|
57
|
+
|
|
58
|
+
Accuracy in text extraction is critical.`,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
function debugLog(message, data = {}) {
|
|
62
|
+
if (!process.env.CCS_DEBUG) return;
|
|
63
|
+
|
|
64
|
+
const lines = [`[CCS Hook] ${message}`];
|
|
65
|
+
for (const [key, value] of Object.entries(data)) {
|
|
66
|
+
if (value !== undefined && value !== null && value !== '') {
|
|
67
|
+
lines.push(` ${key}: ${value}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
console.error(lines.join('\n'));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function parseProviderModels(envValue) {
|
|
74
|
+
if (!envValue) return {};
|
|
75
|
+
return envValue.split(',').reduce((acc, pair) => {
|
|
76
|
+
const [provider, ...modelParts] = pair.split(':');
|
|
77
|
+
const model = modelParts.join(':').trim();
|
|
78
|
+
if (provider && model) {
|
|
79
|
+
acc[provider.trim()] = model;
|
|
80
|
+
}
|
|
81
|
+
return acc;
|
|
82
|
+
}, {});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function normalizeTemplateName(value) {
|
|
86
|
+
if (typeof value !== 'string') return null;
|
|
87
|
+
const normalized = value.trim().toLowerCase();
|
|
88
|
+
return Object.prototype.hasOwnProperty.call(TEMPLATE_FILE_NAMES, normalized) ? normalized : null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function selectPromptTemplate(filePath, requestedTemplate) {
|
|
92
|
+
const explicitTemplate = normalizeTemplateName(requestedTemplate);
|
|
93
|
+
if (explicitTemplate) {
|
|
94
|
+
return explicitTemplate;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const extension = path.extname(filePath).toLowerCase();
|
|
98
|
+
if (PDF_EXTENSIONS.includes(extension)) {
|
|
99
|
+
return 'document';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return SCREENSHOT_NAME_REGEX.test(path.basename(filePath)) ? 'screenshot' : 'default';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function readPromptFile(filePath) {
|
|
106
|
+
try {
|
|
107
|
+
const stats = fs.statSync(filePath);
|
|
108
|
+
if (stats.size > MAX_PROMPT_TEMPLATE_BYTES) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
const content = fs.readFileSync(filePath, 'utf8').trim();
|
|
112
|
+
return content.length > 0 ? content : null;
|
|
113
|
+
} catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function loadPromptTemplate(filePath, requestedTemplate, focus) {
|
|
119
|
+
const template = selectPromptTemplate(filePath, requestedTemplate);
|
|
120
|
+
const promptsDir = process.env.CCS_IMAGE_ANALYSIS_PROMPTS_DIR || '';
|
|
121
|
+
const promptPath = promptsDir
|
|
122
|
+
? path.join(promptsDir, TEMPLATE_FILE_NAMES[template])
|
|
123
|
+
: null;
|
|
124
|
+
const promptText = (promptPath && readPromptFile(promptPath)) || FALLBACK_PROMPTS[template];
|
|
125
|
+
|
|
126
|
+
if (!focus || !focus.trim()) {
|
|
127
|
+
return {
|
|
128
|
+
template,
|
|
129
|
+
promptSource: promptPath ? 'installed-or-fallback' : 'bundled-fallback',
|
|
130
|
+
prompt: promptText,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
template,
|
|
136
|
+
promptSource: promptPath ? 'installed-or-fallback' : 'bundled-fallback',
|
|
137
|
+
prompt: `${promptText}\n\nSpecific focus:\n${focus.trim()}`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function getCurrentProvider() {
|
|
142
|
+
return process.env.CCS_CURRENT_PROVIDER || '';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function getConfiguredModel() {
|
|
146
|
+
const explicitModel = process.env.CCS_IMAGE_ANALYSIS_MODEL;
|
|
147
|
+
if (explicitModel && explicitModel.trim()) {
|
|
148
|
+
return explicitModel.trim();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const providerModels = parseProviderModels(process.env.CCS_IMAGE_ANALYSIS_PROVIDER_MODELS);
|
|
152
|
+
return providerModels[getCurrentProvider()] || DEFAULT_MODEL;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function getModelsToTry() {
|
|
156
|
+
const models = [];
|
|
157
|
+
const seen = new Set();
|
|
158
|
+
|
|
159
|
+
const explicitModel = process.env.CCS_IMAGE_ANALYSIS_MODEL;
|
|
160
|
+
if (explicitModel && explicitModel.trim()) {
|
|
161
|
+
models.push(explicitModel.trim());
|
|
162
|
+
seen.add(explicitModel.trim());
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const providerModels = parseProviderModels(process.env.CCS_IMAGE_ANALYSIS_PROVIDER_MODELS);
|
|
166
|
+
const providerModel = providerModels[getCurrentProvider()];
|
|
167
|
+
if (providerModel && !seen.has(providerModel)) {
|
|
168
|
+
models.push(providerModel);
|
|
169
|
+
seen.add(providerModel);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (models.length === 0) {
|
|
173
|
+
models.push(DEFAULT_MODEL);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return models;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function getRuntimeBaseUrl() {
|
|
180
|
+
const runtimePath = (process.env.CCS_IMAGE_ANALYSIS_RUNTIME_PATH || '')
|
|
181
|
+
.trim()
|
|
182
|
+
.replace(/\/+$/, '');
|
|
183
|
+
const explicitBaseUrl = process.env.CCS_IMAGE_ANALYSIS_RUNTIME_BASE_URL;
|
|
184
|
+
if (explicitBaseUrl && explicitBaseUrl.trim()) {
|
|
185
|
+
const normalizedBaseUrl = explicitBaseUrl.trim().replace(/\/+$/, '');
|
|
186
|
+
if (!runtimePath) {
|
|
187
|
+
return normalizedBaseUrl;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
const parsed = new URL(normalizedBaseUrl);
|
|
192
|
+
const normalizedPath = parsed.pathname.replace(/\/+$/, '');
|
|
193
|
+
if (normalizedPath === runtimePath) {
|
|
194
|
+
return normalizedBaseUrl;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
parsed.pathname = runtimePath;
|
|
198
|
+
return parsed.toString().replace(/\/+$/, '');
|
|
199
|
+
} catch {
|
|
200
|
+
return `${normalizedBaseUrl}${runtimePath}`;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const port = Number.parseInt(process.env.CCS_CLIPROXY_PORT || '8317', 10);
|
|
205
|
+
return `http://127.0.0.1:${port}${runtimePath}`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function getRuntimeEndpoint() {
|
|
209
|
+
return `${getRuntimeBaseUrl()}/v1/messages`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function getApiKey() {
|
|
213
|
+
if (Object.prototype.hasOwnProperty.call(process.env, 'CCS_IMAGE_ANALYSIS_RUNTIME_API_KEY')) {
|
|
214
|
+
const explicitApiKey = (process.env.CCS_IMAGE_ANALYSIS_RUNTIME_API_KEY || '').trim();
|
|
215
|
+
return explicitApiKey || 'ccs-internal-managed';
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return process.env.CCS_CLIPROXY_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN || 'ccs-internal-managed';
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function shouldAllowSelfSigned() {
|
|
222
|
+
const value = `${process.env.CCS_IMAGE_ANALYSIS_RUNTIME_ALLOW_SELF_SIGNED || ''}`.trim().toLowerCase();
|
|
223
|
+
return value === '1' || value === 'true' || value === 'yes';
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function getTimeoutMs(timeoutMs) {
|
|
227
|
+
if (typeof timeoutMs === 'number' && timeoutMs > 0) {
|
|
228
|
+
return timeoutMs;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const timeoutSec = Number.parseInt(
|
|
232
|
+
process.env.CCS_IMAGE_ANALYSIS_TIMEOUT || `${DEFAULT_TIMEOUT_SEC}`,
|
|
233
|
+
10
|
|
234
|
+
);
|
|
235
|
+
return Math.max(1, Math.min(600, timeoutSec)) * 1000;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function isAnalyzableFile(filePath) {
|
|
239
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
240
|
+
return IMAGE_EXTENSIONS.includes(ext) || PDF_EXTENSIONS.includes(ext);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function getMediaType(filePath) {
|
|
244
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
245
|
+
return (
|
|
246
|
+
{
|
|
247
|
+
'.jpg': 'image/jpeg',
|
|
248
|
+
'.jpeg': 'image/jpeg',
|
|
249
|
+
'.png': 'image/png',
|
|
250
|
+
'.gif': 'image/gif',
|
|
251
|
+
'.webp': 'image/webp',
|
|
252
|
+
'.heic': 'image/heic',
|
|
253
|
+
'.bmp': 'image/bmp',
|
|
254
|
+
'.tiff': 'image/tiff',
|
|
255
|
+
'.pdf': 'application/pdf',
|
|
256
|
+
}[ext] || 'application/octet-stream'
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function encodeFileToBase64(filePath) {
|
|
261
|
+
return fs.readFileSync(filePath).toString('base64');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function buildContentBlock(base64Data, mediaType) {
|
|
265
|
+
const source = {
|
|
266
|
+
type: 'base64',
|
|
267
|
+
media_type: mediaType,
|
|
268
|
+
data: base64Data,
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
if (mediaType === 'application/pdf') {
|
|
272
|
+
return {
|
|
273
|
+
type: 'document',
|
|
274
|
+
source,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
type: 'image',
|
|
280
|
+
source,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function extractTextContent(response) {
|
|
285
|
+
if (!response || !Array.isArray(response.content)) {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const textBlocks = response.content
|
|
290
|
+
.filter((block) => block && block.type === 'text' && typeof block.text === 'string')
|
|
291
|
+
.map((block) => block.text)
|
|
292
|
+
.filter((text) => text.trim());
|
|
293
|
+
|
|
294
|
+
return textBlocks.length > 0 ? textBlocks.join('\n\n') : null;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function parseCliProxyResponse(data) {
|
|
298
|
+
const response = JSON.parse(data);
|
|
299
|
+
const text = extractTextContent(response);
|
|
300
|
+
if (!text) {
|
|
301
|
+
throw new Error('No text content in response');
|
|
302
|
+
}
|
|
303
|
+
return text;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function analyzeViaCliProxy(base64Data, mediaType, model, prompt, timeoutMs) {
|
|
307
|
+
return new Promise((resolve, reject) => {
|
|
308
|
+
const endpoint = new URL(getRuntimeEndpoint());
|
|
309
|
+
const transport = endpoint.protocol === 'https:' ? https : http;
|
|
310
|
+
const apiKey = getApiKey();
|
|
311
|
+
const requestBody = JSON.stringify({
|
|
312
|
+
model,
|
|
313
|
+
max_tokens: 4096,
|
|
314
|
+
messages: [
|
|
315
|
+
{
|
|
316
|
+
role: 'user',
|
|
317
|
+
content: [
|
|
318
|
+
{ type: 'text', text: prompt },
|
|
319
|
+
buildContentBlock(base64Data, mediaType),
|
|
320
|
+
],
|
|
321
|
+
},
|
|
322
|
+
],
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
const req = transport.request(
|
|
326
|
+
{
|
|
327
|
+
protocol: endpoint.protocol,
|
|
328
|
+
hostname: endpoint.hostname,
|
|
329
|
+
port: endpoint.port,
|
|
330
|
+
path: `${endpoint.pathname}${endpoint.search}`,
|
|
331
|
+
method: 'POST',
|
|
332
|
+
headers: {
|
|
333
|
+
'Content-Type': 'application/json',
|
|
334
|
+
'Content-Length': Buffer.byteLength(requestBody),
|
|
335
|
+
'x-api-key': apiKey,
|
|
336
|
+
Authorization: `Bearer ${apiKey}`,
|
|
337
|
+
},
|
|
338
|
+
timeout: timeoutMs,
|
|
339
|
+
...(endpoint.protocol === 'https:' && shouldAllowSelfSigned()
|
|
340
|
+
? { rejectUnauthorized: false }
|
|
341
|
+
: {}),
|
|
342
|
+
},
|
|
343
|
+
(res) => {
|
|
344
|
+
let data = '';
|
|
345
|
+
|
|
346
|
+
res.on('data', (chunk) => {
|
|
347
|
+
data += chunk;
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
res.on('end', () => {
|
|
351
|
+
if (res.statusCode === 401 || res.statusCode === 403) {
|
|
352
|
+
reject(new Error(`AUTH_ERROR:${res.statusCode}`));
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (res.statusCode === 429) {
|
|
357
|
+
reject(new Error(`RATE_LIMIT:${res.headers['retry-after'] || ''}`));
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (res.statusCode !== 200) {
|
|
362
|
+
reject(new Error(`API_ERROR:${res.statusCode}:${data}`));
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
try {
|
|
367
|
+
resolve(parseCliProxyResponse(data));
|
|
368
|
+
} catch (error) {
|
|
369
|
+
reject(error);
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
req.on('error', (error) => reject(error));
|
|
376
|
+
req.on('timeout', () => {
|
|
377
|
+
req.destroy();
|
|
378
|
+
reject(new Error('TIMEOUT'));
|
|
379
|
+
});
|
|
380
|
+
req.write(requestBody);
|
|
381
|
+
req.end();
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async function analyzeWithRetry(base64Data, mediaType, prompt, timeoutMs) {
|
|
386
|
+
const models = getModelsToTry();
|
|
387
|
+
let lastError = null;
|
|
388
|
+
|
|
389
|
+
for (const [index, model] of models.entries()) {
|
|
390
|
+
try {
|
|
391
|
+
debugLog(`Trying model ${index + 1}/${models.length}`, { model });
|
|
392
|
+
const description = await analyzeViaCliProxy(base64Data, mediaType, model, prompt, timeoutMs);
|
|
393
|
+
return { description, model };
|
|
394
|
+
} catch (error) {
|
|
395
|
+
lastError = error;
|
|
396
|
+
const message = error.message || '';
|
|
397
|
+
if (
|
|
398
|
+
index === models.length - 1 ||
|
|
399
|
+
['AUTH_ERROR', 'RATE_LIMIT', 'TIMEOUT', 'EACCES', 'EPERM', 'ECONNREFUSED'].some((token) =>
|
|
400
|
+
message.includes(token)
|
|
401
|
+
)
|
|
402
|
+
) {
|
|
403
|
+
throw error;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
throw lastError || new Error('No models configured for image analysis');
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async function analyzeFile(filePath, options = {}) {
|
|
412
|
+
const stats = fs.statSync(filePath);
|
|
413
|
+
if (stats.size >= MAX_FILE_SIZE_BYTES) {
|
|
414
|
+
throw new Error(`FILE_TOO_LARGE:${stats.size}`);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const timeoutMs = getTimeoutMs(options.timeoutMs);
|
|
418
|
+
const { template, prompt, promptSource } = loadPromptTemplate(
|
|
419
|
+
filePath,
|
|
420
|
+
options.template,
|
|
421
|
+
options.focus
|
|
422
|
+
);
|
|
423
|
+
const model = getConfiguredModel();
|
|
424
|
+
|
|
425
|
+
debugLog('Starting image analysis', {
|
|
426
|
+
file: path.basename(filePath),
|
|
427
|
+
size: `${(stats.size / 1024).toFixed(1)} KB`,
|
|
428
|
+
provider: getCurrentProvider() || 'unknown',
|
|
429
|
+
model,
|
|
430
|
+
modelsToTry: getModelsToTry().join(' -> '),
|
|
431
|
+
timeout: `${timeoutMs / 1000}s`,
|
|
432
|
+
endpoint: getRuntimeEndpoint(),
|
|
433
|
+
template,
|
|
434
|
+
promptSource,
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
const base64Data = encodeFileToBase64(filePath);
|
|
438
|
+
const mediaType = getMediaType(filePath);
|
|
439
|
+
debugLog('File encoded', {
|
|
440
|
+
mediaType,
|
|
441
|
+
base64Length: `${(base64Data.length / 1024).toFixed(1)}KB`,
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
const result = await analyzeWithRetry(base64Data, mediaType, prompt, timeoutMs);
|
|
445
|
+
debugLog('Analysis complete', {
|
|
446
|
+
responseLength: `${result.description.length} chars`,
|
|
447
|
+
model: result.model,
|
|
448
|
+
template,
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
return {
|
|
452
|
+
description: result.description,
|
|
453
|
+
model: result.model,
|
|
454
|
+
fileSize: stats.size,
|
|
455
|
+
mediaType,
|
|
456
|
+
template,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
module.exports = {
|
|
461
|
+
DEFAULT_MODEL,
|
|
462
|
+
DEFAULT_TIMEOUT_SEC,
|
|
463
|
+
MAX_FILE_SIZE_BYTES,
|
|
464
|
+
analyzeFile,
|
|
465
|
+
getRuntimeEndpoint,
|
|
466
|
+
isAnalyzableFile,
|
|
467
|
+
parseProviderModels,
|
|
468
|
+
selectPromptTemplate,
|
|
469
|
+
};
|