u
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import * as db from '../../db/company.js';
|
import * as db from '../../db/company.js';
|
||||||
import * as googleAuth from '../../db/googleAuth.js';
|
import * as googleAuth from '../../db/googleAuth.js';
|
||||||
import { clearCachedUser } from '../middleware/auth.js';
|
import { clearCachedUser } from '../middleware/auth.js';
|
||||||
|
import { initCompanyVfs } from '../../tools/vfs.js';
|
||||||
|
|
||||||
function userPayload(authRow) {
|
function userPayload(authRow) {
|
||||||
return {
|
return {
|
||||||
@@ -169,6 +170,11 @@ export function register(app, basePath = '') {
|
|||||||
const company = await db.createCompany(req.user.id, body);
|
const company = await db.createCompany(req.user.id, body);
|
||||||
await googleAuth.setSelectedCompany(req.user.id, company.id);
|
await googleAuth.setSelectedCompany(req.user.id, company.id);
|
||||||
clearCachedUser(req.user.id);
|
clearCachedUser(req.user.id);
|
||||||
|
try {
|
||||||
|
await initCompanyVfs({ googleAuthId: req.user.id });
|
||||||
|
} catch (vfsErr) {
|
||||||
|
console.error('Failed to initialize company VFS:', vfsErr);
|
||||||
|
}
|
||||||
const authRow = await googleAuth.getAllowedEmail(req.user.email);
|
const authRow = await googleAuth.getAllowedEmail(req.user.email);
|
||||||
res.status(201).json({ company, user: userPayload(authRow) });
|
res.status(201).json({ company, user: userPayload(authRow) });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
11
src/defaultSkills/image-search-inspect-present.SKILL
Normal file
11
src/defaultSkills/image-search-inspect-present.SKILL
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
Efficient image search, inspection, and presentation
|
||||||
|
|
||||||
|
# Goal
|
||||||
|
Find relevant online images, inspect the best candidates, and show the user the results with minimal turns and tool overhead.
|
||||||
|
|
||||||
|
# Procedure
|
||||||
|
1. Search with concise, specific queries. For Categories or brands photos down to 300x300 are ok, but prouct photos should be 800+
|
||||||
|
2. **Search in parallel.** For multiple distinct subjects, call `search_images` concurrently, requesting a few candidates per query.
|
||||||
|
3. Use the paths returned by `search_images`. If needed, check `find_files` to confirm paths.
|
||||||
|
4. **Inspect candidates in one call.** Pass up to nine candidate paths to one `analyze_image` call.
|
||||||
|
5. When you upload a photo using the API you also must show the photo to the user in markdown when summarizing. ``
|
||||||
@@ -11,7 +11,7 @@ import { createDashbackRequestTool } from '../tools/dashback-request.js';
|
|||||||
import { createAskUserTool } from '../tools/ask-user.js';
|
import { createAskUserTool } from '../tools/ask-user.js';
|
||||||
import { createJavascriptExecuteTool } from '../tools/javascript.js';
|
import { createJavascriptExecuteTool } from '../tools/javascript.js';
|
||||||
import { createResetVfsTool } from '../tools/reset-vfs.js';
|
import { createResetVfsTool } from '../tools/reset-vfs.js';
|
||||||
import { resolveVfsRoot } from '../tools/vfs.js';
|
import { prepareVfs, resolveVfsRoot } from '../tools/vfs.js';
|
||||||
import { listSkillFiles } from '../tools/skill.js';
|
import { listSkillFiles } from '../tools/skill.js';
|
||||||
import { createApiSuggestionsOrIssuesTool } from '../tools/apisuggestionsorissues.js';
|
import { createApiSuggestionsOrIssuesTool } from '../tools/apisuggestionsorissues.js';
|
||||||
import { createFindFilesTool } from '../tools/find-files.js';
|
import { createFindFilesTool } from '../tools/find-files.js';
|
||||||
@@ -446,13 +446,16 @@ export class ConversationManager {
|
|||||||
// Transcript comes from the database (includes user, assistant, tool, reasoning).
|
// Transcript comes from the database (includes user, assistant, tool, reasoning).
|
||||||
const history = await db.listMessages(googleAuthId, this.conversationId);
|
const history = await db.listMessages(googleAuthId, this.conversationId);
|
||||||
|
|
||||||
// Resolve the VFS root so the prompt can list the company's SKILL files. A failure
|
// Resolve and prepare the VFS root so the prompt can list the company's SKILL files.
|
||||||
// here must not abort the turn — the prompt simply falls back to "no skills".
|
// A failure here must not abort the turn — the prompt simply falls back to "no skills".
|
||||||
let vfsRoot = null;
|
let vfsRoot = null;
|
||||||
try {
|
try {
|
||||||
vfsRoot = await resolveVfsRoot(googleAuthId);
|
vfsRoot = await prepareVfs({ googleAuthId });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('conversation: could not resolve VFS root for skill index:', err.message);
|
console.warn('conversation: could not prepare VFS root for skill index:', err.message);
|
||||||
|
try {
|
||||||
|
vfsRoot = await resolveVfsRoot(googleAuthId);
|
||||||
|
} catch { /* ignore fallback error */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
const messages = [
|
const messages = [
|
||||||
|
|||||||
@@ -50,6 +50,9 @@ export function createResetVfsTool({ googleAuthId }) {
|
|||||||
// touching disk so a store failure cannot leave the disk wiped while the database
|
// touching disk so a store failure cannot leave the disk wiped while the database
|
||||||
// still claims the files exist.
|
// still claims the files exist.
|
||||||
for (const file of existing) {
|
for (const file of existing) {
|
||||||
|
if (file.name === '.vfs_initialized') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (keepSkills && isSkillPath(file.name)) {
|
if (keepSkills && isSkillPath(file.name)) {
|
||||||
keptSkills.push(file.name);
|
keptSkills.push(file.name);
|
||||||
continue;
|
continue;
|
||||||
@@ -63,6 +66,9 @@ export function createResetVfsTool({ googleAuthId }) {
|
|||||||
let deletedCount = 0;
|
let deletedCount = 0;
|
||||||
if (fs.existsSync(vfsRoot)) {
|
if (fs.existsSync(vfsRoot)) {
|
||||||
for (const entry of fs.readdirSync(vfsRoot)) {
|
for (const entry of fs.readdirSync(vfsRoot)) {
|
||||||
|
if (entry === '.vfs_initialized') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
// Only root-level entries can be SKILL files, so the bare name is enough
|
// Only root-level entries can be SKILL files, so the bare name is enough
|
||||||
// to decide here — nested directories are always disposable.
|
// to decide here — nested directories are always disposable.
|
||||||
if (keepSkills && isSkillPath(entry)) {
|
if (keepSkills && isSkillPath(entry)) {
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
export const DEFAULT_SKILLS_DIR = fs.existsSync(path.resolve(__dirname, '../defaultSkills'))
|
||||||
|
? path.resolve(__dirname, '../defaultSkills')
|
||||||
|
: path.resolve(process.cwd(), 'defaultSkills');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* skill.js — the `SKILL` VFS file type.
|
* skill.js — the `SKILL` VFS file type.
|
||||||
@@ -186,3 +194,31 @@ export function listSkillFiles(vfsRoot) {
|
|||||||
out.sort((a, b) => a.path.localeCompare(b.path));
|
out.sort((a, b) => a.path.localeCompare(b.path));
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads all default skill files from DEFAULT_SKILLS_DIR.
|
||||||
|
* @returns {Array<{ name: string, content: string }>}
|
||||||
|
*/
|
||||||
|
export function getDefaultSkills() {
|
||||||
|
if (!fs.existsSync(DEFAULT_SKILLS_DIR)) return [];
|
||||||
|
let dirents;
|
||||||
|
try {
|
||||||
|
dirents = fs.readdirSync(DEFAULT_SKILLS_DIR, { withFileTypes: true });
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const out = [];
|
||||||
|
for (const dirent of dirents) {
|
||||||
|
if (!dirent.isFile() || !isSkillName(dirent.name)) continue;
|
||||||
|
const absPath = path.join(DEFAULT_SKILLS_DIR, dirent.name);
|
||||||
|
try {
|
||||||
|
const content = fs.readFileSync(absPath, 'utf8');
|
||||||
|
out.push({ name: dirent.name, content });
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`[skill] failed to read default skill "${dirent.name}":`, err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import fs from 'fs';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { getStore } from '../db/store.js';
|
import { getStore } from '../db/store.js';
|
||||||
import env from '../config/env.js';
|
import env from '../config/env.js';
|
||||||
|
import { isSkillPath, getDefaultSkills } from './skill.js';
|
||||||
|
|
||||||
const FULL_HASH_REGEX = /^[a-fA-F0-9]{64}$/;
|
const FULL_HASH_REGEX = /^[a-fA-F0-9]{64}$/;
|
||||||
const VIRTUAL_IMAGE_FOLDERS = new Set(['image', 'images']);
|
const VIRTUAL_IMAGE_FOLDERS = new Set(['image', 'images']);
|
||||||
@@ -96,8 +97,72 @@ async function vfsRootFor(store, googleAuthId) {
|
|||||||
return path.resolve(process.cwd(), 'data/vfs', String(companyId));
|
return path.resolve(process.cwd(), 'data/vfs', String(companyId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads default SKILL files into the selected company's VFS (both modelvfs table and disk cache).
|
||||||
|
*
|
||||||
|
* @param {{ googleAuthId: number, store?: object, vfsRoot?: string }} ctx
|
||||||
|
* @returns {Promise<Array<string>>} List of loaded skill file names
|
||||||
|
*/
|
||||||
|
export async function loadDefaultSkills({ googleAuthId, store: passedStore, vfsRoot: passedVfsRoot } = {}) {
|
||||||
|
const store = passedStore || await getStore();
|
||||||
|
const vfsRoot = passedVfsRoot || await vfsRootFor(store, googleAuthId);
|
||||||
|
fs.mkdirSync(vfsRoot, { recursive: true });
|
||||||
|
|
||||||
|
const defaultSkills = getDefaultSkills();
|
||||||
|
const loaded = [];
|
||||||
|
|
||||||
|
for (const skill of defaultSkills) {
|
||||||
|
try {
|
||||||
|
await store.modelvfs.write(googleAuthId, {
|
||||||
|
name: skill.name,
|
||||||
|
mime: 'text/markdown',
|
||||||
|
content: Buffer.from(skill.content, 'utf8'),
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[vfs] failed to write default skill "${skill.name}" to modelvfs:`, err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const diskPath = path.resolve(vfsRoot, skill.name);
|
||||||
|
fs.writeFileSync(diskPath, skill.content, 'utf8');
|
||||||
|
loaded.push(skill.name);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[vfs] failed to write default skill "${skill.name}" to disk:`, err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return loaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the VFS for the selected company with default SKILL files and marks it initialized.
|
||||||
|
*
|
||||||
|
* @param {{ googleAuthId: number, store?: object, vfsRoot?: string }} ctx
|
||||||
|
* @returns {Promise<string>} vfsRoot directory path
|
||||||
|
*/
|
||||||
|
export async function initCompanyVfs({ googleAuthId, store: passedStore, vfsRoot: passedVfsRoot } = {}) {
|
||||||
|
const store = passedStore || await getStore();
|
||||||
|
const vfsRoot = passedVfsRoot || await vfsRootFor(store, googleAuthId);
|
||||||
|
fs.mkdirSync(vfsRoot, { recursive: true });
|
||||||
|
|
||||||
|
await loadDefaultSkills({ googleAuthId, store, vfsRoot });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await store.modelvfs.write(googleAuthId, {
|
||||||
|
name: '.vfs_initialized',
|
||||||
|
mime: 'text/plain',
|
||||||
|
content: Buffer.from('1', 'utf8'),
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[vfs] failed to mark .vfs_initialized in modelvfs:', err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return vfsRoot;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure the local directory for this company's VFS exists and is hydrated from modelvfs.
|
* Ensure the local directory for this company's VFS exists and is hydrated from modelvfs.
|
||||||
|
* If the company has no initialized VFS and no skills yet, loads default skills automatically.
|
||||||
*
|
*
|
||||||
* @param {{ googleAuthId: number }} ctx
|
* @param {{ googleAuthId: number }} ctx
|
||||||
* @returns {Promise<string>} vfsRoot directory path
|
* @returns {Promise<string>} vfsRoot directory path
|
||||||
@@ -108,8 +173,25 @@ export async function prepareVfs({ googleAuthId }) {
|
|||||||
fs.mkdirSync(vfsRoot, { recursive: true });
|
fs.mkdirSync(vfsRoot, { recursive: true });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const files = await store.modelvfs.list(googleAuthId);
|
let files = await store.modelvfs.list(googleAuthId);
|
||||||
|
const hasInitMarker = files.some((f) => f.name === '.vfs_initialized');
|
||||||
|
const hasAnySkills = files.some((f) => isSkillPath(f.name));
|
||||||
|
|
||||||
|
if (!hasInitMarker && !hasAnySkills) {
|
||||||
|
await initCompanyVfs({ googleAuthId, store, vfsRoot });
|
||||||
|
files = await store.modelvfs.list(googleAuthId);
|
||||||
|
} else if (!hasInitMarker) {
|
||||||
|
try {
|
||||||
|
await store.modelvfs.write(googleAuthId, {
|
||||||
|
name: '.vfs_initialized',
|
||||||
|
mime: 'text/plain',
|
||||||
|
content: Buffer.from('1', 'utf8'),
|
||||||
|
});
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
|
if (file.name.startsWith('.')) continue;
|
||||||
const diskPath = path.resolve(vfsRoot, file.name);
|
const diskPath = path.resolve(vfsRoot, file.name);
|
||||||
if (!diskPath.startsWith(vfsRoot)) continue;
|
if (!diskPath.startsWith(vfsRoot)) continue;
|
||||||
if (!fs.existsSync(diskPath)) {
|
if (!fs.existsSync(diskPath)) {
|
||||||
@@ -127,6 +209,7 @@ export async function prepareVfs({ googleAuthId }) {
|
|||||||
return vfsRoot;
|
return vfsRoot;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Syncs any modified or newly written files from the disk vfsRoot back to the modelvfs table.
|
* Syncs any modified or newly written files from the disk vfsRoot back to the modelvfs table.
|
||||||
*
|
*
|
||||||
|
|||||||
Reference in New Issue
Block a user