This commit is contained in:
seb
2026-10-07 19:16:42 +02:00
parent 1743cef11c
commit 857963b1c3
20 changed files with 892 additions and 182 deletions

View File

@@ -4,7 +4,8 @@ import sharp from 'sharp';
import jwt from 'jsonwebtoken';
import { defineTool, ToolError, OpenRouterProvider } from 'libllmorbiter';
import env from '../config/env.js';
import { prepareVfs } from './vfs.js';
import { getImageByHash } from '../db/pic.js';
import { prepareVfs, extractFullHash } from './vfs.js';
import { resolveOpenRouterKey, resolveDefaultModel } from './llm-key.js';
/**
@@ -234,20 +235,34 @@ function extractImageInputs(args = {}) {
}
/**
* Fetches a database image by its content hash, authenticated as the calling user.
* Fetches a database image by its content hash using store or authenticated local endpoint.
* @param {string} hash 64-char SHA-256 hex
* @param {number} googleAuthId
* @returns {Promise<Buffer>}
*/
async function fetchDatabaseImage(hash, googleAuthId) {
const url = `http://127.0.0.1:${env.port}/image/${hash}`;
const token = jwt.sign({ id: googleAuthId }, env.jwtSecret, { expiresIn: '5m' });
const res = await fetch(url, {
headers: { Cookie: `token=${token}` },
signal: AbortSignal.timeout(IMAGE_TIMEOUT_MS),
});
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
return Buffer.from(await res.arrayBuffer());
try {
const image = await getImageByHash(googleAuthId, hash);
if (image?.buffer) {
return image.buffer;
}
} catch (dbErr) {
try {
const url = `http://127.0.0.1:${env.port}/api/image/${hash}`;
const token = jwt.sign({ id: googleAuthId }, env.jwtSecret, { expiresIn: '5m' });
const res = await fetch(url, {
headers: { Cookie: `token=${token}` },
signal: AbortSignal.timeout(IMAGE_TIMEOUT_MS),
});
if (res.ok) {
return Buffer.from(await res.arrayBuffer());
}
} catch {
// fallback error ignored
}
throw dbErr;
}
throw new Error('Image not found');
}
/**
@@ -269,6 +284,10 @@ export function createAnalyzeImageTool({ googleAuthId, model }) {
additionalProperties: false,
required: ['paths'],
properties: {
note: {
type: ['string', 'null'],
description: 'A note explaining what this image inspection is doing (or null). This note will be displayed to the user while the images are being analyzed.',
},
paths: {
type: 'array',
description: '1–9 VFS paths or DB paths ("image/<hash>").',
@@ -277,17 +296,17 @@ export function createAnalyzeImageTool({ googleAuthId, model }) {
maxItems: MAX_IMAGES,
},
prompt: {
type: 'string',
description: 'Inspection criteria or question.',
type: ['string', 'null'],
description: 'Inspection criteria or question, or null.',
},
grid: {
type: 'string',
enum: Object.keys(GRID_CONFIGS),
description: 'Grid layout. Omit for auto.',
type: ['string', 'null'],
enum: [...Object.keys(GRID_CONFIGS), null],
description: 'Grid layout or null for auto.',
},
},
},
invoke: async ({ paths, prompt, grid } = {}) => {
invoke: async ({ paths, prompt, grid, note } = {}) => {
const inputs = extractImageInputs({ paths });
if (inputs.length === 0) {
throw new ToolError(
@@ -325,6 +344,10 @@ export function createAnalyzeImageTool({ googleAuthId, model }) {
buf = fs.readFileSync(cached);
} else {
buf = await fetchDatabaseImage(hash, googleAuthId);
try {
fs.mkdirSync(path.join(vfsRoot, 'images'), { recursive: true });
fs.writeFileSync(cached, buf);
} catch { /* ignore cache write failure */ }
}
}
@@ -397,19 +420,3 @@ export function createAnalyzeImageTool({ googleAuthId, model }) {
},
});
}
/**
* Extracts a 64-char hex SHA-256 hash from a VFS image path, if present.
* Mirrors the sandbox's `extractFullHash` so `image/<hash>` resolves identically here.
* @param {string} p
* @returns {string|null}
*/
function extractFullHash(p) {
if (typeof p !== 'string') return null;
const parts = p.trim().replace(/^([a-zA-Z]:|\/)+/, '').split('/');
for (const part of parts) {
const seg = part.replace(/\.[a-zA-Z0-9]+$/, '');
if (/^[a-fA-F0-9]{64}$/.test(seg)) return seg.toLowerCase();
}
return null;
}