u
This commit is contained in:
108
src/tools/vfs.js
108
src/tools/vfs.js
@@ -32,6 +32,10 @@ export function getMimeType(filePath) {
|
||||
case '.log': return 'text/plain; charset=utf-8';
|
||||
case '.csv': return 'text/csv; charset=utf-8';
|
||||
case '.tsv': return 'text/tab-separated-values; charset=utf-8';
|
||||
case '.xlsx': return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
||||
case '.xls': return 'application/vnd.ms-excel';
|
||||
case '.xlsm': return 'application/vnd.ms-excel.sheet.macroEnabled.12';
|
||||
case '.xltx': return 'application/vnd.openxmlformats-officedocument.spreadsheetml.template';
|
||||
case '.md':
|
||||
case '.skill': return 'text/markdown; charset=utf-8';
|
||||
case '.xml': return 'application/xml; charset=utf-8';
|
||||
@@ -68,7 +72,7 @@ export function extractFullHash(p) {
|
||||
|
||||
export function isReadOnlyDatabasePath(filePath) {
|
||||
if (typeof filePath !== 'string' || !filePath.trim()) return false;
|
||||
const clean = filePath.trim().replace(/^([a-zA-Z]:|\/)+/, '').replace(/^\.\/+/, '');
|
||||
const clean = filePath.trim().replace(/^vfs:\/+/i, '').replace(/^([a-zA-Z]:|\/)+/, '').replace(/^\.\/+/, '');
|
||||
const lower = clean.toLowerCase();
|
||||
if (lower === 'image' || lower.startsWith('image/')) return true;
|
||||
if (extractFullHash(clean)) return true;
|
||||
@@ -186,6 +190,23 @@ export async function prepareVfs({ googleAuthId }) {
|
||||
|
||||
try {
|
||||
const files = await store.modelvfs.list(googleAuthId);
|
||||
const existingNames = new Set(files.map((f) => f.name));
|
||||
const defaultSkills = getDefaultSkills();
|
||||
for (const skill of defaultSkills) {
|
||||
if (!existingNames.has(skill.name)) {
|
||||
try {
|
||||
await store.modelvfs.write(googleAuthId, {
|
||||
name: skill.name,
|
||||
mime: 'text/markdown',
|
||||
content: Buffer.from(skill.content, 'utf8'),
|
||||
});
|
||||
files.push({ name: skill.name });
|
||||
existingNames.add(skill.name);
|
||||
} catch (err) {
|
||||
console.warn(`[vfs] failed to write default skill "${skill.name}" to modelvfs:`, err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const file of files) {
|
||||
const diskPath = path.resolve(vfsRoot, file.name);
|
||||
if (!diskPath.startsWith(vfsRoot)) continue;
|
||||
@@ -276,10 +297,10 @@ export function createVfs(vfsRoot, options = {}) {
|
||||
};
|
||||
|
||||
const safeResolve = (p) => {
|
||||
if (typeof p !== 'string' || !p.trim()) {
|
||||
throw new Error('Path must be a non-empty string');
|
||||
if (typeof p !== 'string' || !p.trim()) {
|
||||
throw new Error('Path must be a non-empty string');
|
||||
}
|
||||
const cleanVirtual = p.trim().replace(/^([a-zA-Z]:|\/)+/, '');
|
||||
const cleanVirtual = p.trim().replace(/^vfs:\/+/i, '').replace(/^([a-zA-Z]:|\/)+/, '');
|
||||
const resolved = path.resolve(vfsRoot, cleanVirtual);
|
||||
const relative = path.relative(vfsRoot, resolved);
|
||||
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
||||
@@ -302,20 +323,44 @@ export function createVfs(vfsRoot, options = {}) {
|
||||
|
||||
const vfs = {
|
||||
readFileSync: (p, opt) => {
|
||||
const hash = extractFullHash(p);
|
||||
if (hash) {
|
||||
const buf = syncFetchImage(hash);
|
||||
if (buf) {
|
||||
const encoding = typeof opt === 'string' ? opt : opt?.encoding;
|
||||
return encoding ? buf.toString(encoding) : buf;
|
||||
const hash = extractFullHash(p);
|
||||
if (hash) {
|
||||
const buf = syncFetchImage(hash);
|
||||
if (buf) {
|
||||
const encoding = typeof opt === 'string' ? opt : opt?.encoding;
|
||||
return encoding ? buf.toString(encoding) : buf;
|
||||
}
|
||||
}
|
||||
const diskPath = safeResolve(p);
|
||||
try {
|
||||
}
|
||||
const diskPath = safeResolve(p);
|
||||
try {
|
||||
return fs.readFileSync(diskPath, opt);
|
||||
} catch (err) {
|
||||
throw sanitizeFsError(err, p);
|
||||
}
|
||||
} catch (err) {
|
||||
throw sanitizeFsError(err, p);
|
||||
}
|
||||
},
|
||||
|
||||
readFile: (p, opt, cb) => {
|
||||
const callback = typeof opt === 'function' ? opt : cb;
|
||||
const options = typeof opt === 'function' ? undefined : opt;
|
||||
try {
|
||||
const data = vfs.readFileSync(p, options);
|
||||
if (typeof callback === 'function') process.nextTick(callback, null, data);
|
||||
} catch (err) {
|
||||
if (typeof callback === 'function') process.nextTick(callback, err);
|
||||
else throw err;
|
||||
}
|
||||
},
|
||||
|
||||
createReadStream: (p, opt) => {
|
||||
const diskPath = safeResolve(p);
|
||||
if (!fs.existsSync(diskPath)) {
|
||||
const err = new Error(`ENOENT: no such file or directory, open '${String(p)}'`);
|
||||
err.code = 'ENOENT';
|
||||
err.path = String(p);
|
||||
err.syscall = 'open';
|
||||
throw sanitizeFsError(err, p);
|
||||
}
|
||||
return fs.createReadStream(diskPath, opt);
|
||||
},
|
||||
|
||||
writeFileSync: (p, data, opt) => {
|
||||
@@ -329,6 +374,25 @@ export function createVfs(vfsRoot, options = {}) {
|
||||
}
|
||||
},
|
||||
|
||||
writeFile: (p, data, opt, cb) => {
|
||||
const callback = typeof opt === 'function' ? opt : cb;
|
||||
const options = typeof opt === 'function' ? undefined : opt;
|
||||
try {
|
||||
vfs.writeFileSync(p, data, options);
|
||||
if (typeof callback === 'function') process.nextTick(callback, null);
|
||||
} catch (err) {
|
||||
if (typeof callback === 'function') process.nextTick(callback, err);
|
||||
else throw err;
|
||||
}
|
||||
},
|
||||
|
||||
createWriteStream: (p, opt) => {
|
||||
assertNotReadOnlyDatabasePath(p);
|
||||
const diskPath = safeResolve(p);
|
||||
fs.mkdirSync(path.dirname(diskPath), { recursive: true });
|
||||
return fs.createWriteStream(diskPath, opt);
|
||||
},
|
||||
|
||||
appendFileSync: (p, data, opt) => {
|
||||
assertNotReadOnlyDatabasePath(p);
|
||||
const diskPath = safeResolve(p);
|
||||
@@ -436,6 +500,18 @@ export function createVfs(vfsRoot, options = {}) {
|
||||
assertNotReadOnlyDatabasePath(p);
|
||||
return safeResolve(p);
|
||||
},
|
||||
|
||||
/**
|
||||
* Resolve a virtual path to an absolute on-disk path for reading, applying containment checks.
|
||||
*/
|
||||
resolveReadPath: (p) => {
|
||||
return safeResolve(p);
|
||||
},
|
||||
|
||||
/**
|
||||
* Sanitize on-disk error paths back to virtual paths for display in sandbox transcripts.
|
||||
*/
|
||||
sanitizeError: (err, virtualPath) => sanitizeFsError(err, virtualPath),
|
||||
};
|
||||
|
||||
return vfs;
|
||||
|
||||
Reference in New Issue
Block a user