u
This commit is contained in:
@@ -50,6 +50,9 @@ export function createResetVfsTool({ googleAuthId }) {
|
||||
// touching disk so a store failure cannot leave the disk wiped while the database
|
||||
// still claims the files exist.
|
||||
for (const file of existing) {
|
||||
if (file.name === '.vfs_initialized') {
|
||||
continue;
|
||||
}
|
||||
if (keepSkills && isSkillPath(file.name)) {
|
||||
keptSkills.push(file.name);
|
||||
continue;
|
||||
@@ -63,6 +66,9 @@ export function createResetVfsTool({ googleAuthId }) {
|
||||
let deletedCount = 0;
|
||||
if (fs.existsSync(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
|
||||
// to decide here — nested directories are always disposable.
|
||||
if (keepSkills && isSkillPath(entry)) {
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import fs from 'fs';
|
||||
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.
|
||||
@@ -185,4 +193,32 @@ export function listSkillFiles(vfsRoot) {
|
||||
|
||||
out.sort((a, b) => a.path.localeCompare(b.path));
|
||||
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 { getStore } from '../db/store.js';
|
||||
import env from '../config/env.js';
|
||||
import { isSkillPath, getDefaultSkills } from './skill.js';
|
||||
|
||||
const FULL_HASH_REGEX = /^[a-fA-F0-9]{64}$/;
|
||||
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));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* If the company has no initialized VFS and no skills yet, loads default skills automatically.
|
||||
*
|
||||
* @param {{ googleAuthId: number }} ctx
|
||||
* @returns {Promise<string>} vfsRoot directory path
|
||||
@@ -108,8 +173,25 @@ export async function prepareVfs({ googleAuthId }) {
|
||||
fs.mkdirSync(vfsRoot, { recursive: true });
|
||||
|
||||
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) {
|
||||
if (file.name.startsWith('.')) continue;
|
||||
const diskPath = path.resolve(vfsRoot, file.name);
|
||||
if (!diskPath.startsWith(vfsRoot)) continue;
|
||||
if (!fs.existsSync(diskPath)) {
|
||||
@@ -127,6 +209,7 @@ export async function prepareVfs({ googleAuthId }) {
|
||||
return vfsRoot;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Syncs any modified or newly written files from the disk vfsRoot back to the modelvfs table.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user