u
This commit is contained in:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user