2026-09-09 10:08:45 +08:00
|
|
|
import fs from 'node:fs';
|
|
|
|
|
import path from 'node:path';
|
|
|
|
|
|
2026-09-09 00:52:52 +08:00
|
|
|
export const name = 'dsh-plugin-model-enhancer';
|
|
|
|
|
export const inject = ['webServer'];
|
|
|
|
|
|
|
|
|
|
let cachedData = null;
|
|
|
|
|
let lastFetchedAt = 0;
|
|
|
|
|
const CACHE_TTL_MS = 3600 * 1000; // 1 hour
|
|
|
|
|
|
|
|
|
|
async function getModelsDevData() {
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
if (cachedData && now - lastFetchedAt < CACHE_TTL_MS) {
|
|
|
|
|
return cachedData;
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch('https://models.dev/api.json', {
|
|
|
|
|
headers: {
|
|
|
|
|
'User-Agent': 'DSH-Model-Enhancer/1.0.0'
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
throw new Error(`Failed to fetch models.dev: HTTP ${res.status}`);
|
|
|
|
|
}
|
|
|
|
|
const json = await res.json();
|
|
|
|
|
cachedData = json;
|
|
|
|
|
lastFetchedAt = now;
|
|
|
|
|
return json;
|
|
|
|
|
} catch (err) {
|
|
|
|
|
if (cachedData) {
|
|
|
|
|
// Fallback to stale cache if network fails
|
|
|
|
|
return cachedData;
|
|
|
|
|
}
|
|
|
|
|
throw err;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function apply(ctx) {
|
2026-09-09 10:08:45 +08:00
|
|
|
// 1. Models.dev Cache Proxy
|
2026-09-09 00:52:52 +08:00
|
|
|
ctx.effect(() => {
|
|
|
|
|
return ctx.webServer.register({
|
|
|
|
|
kind: 'exact',
|
|
|
|
|
path: '/api/models-dev/models',
|
|
|
|
|
handler: async (req, res) => {
|
|
|
|
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
|
|
|
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
|
|
|
|
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
|
|
|
|
|
|
|
|
if (req.method === 'OPTIONS') {
|
|
|
|
|
res.writeHead(204);
|
|
|
|
|
res.end();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (req.method !== 'GET') {
|
|
|
|
|
res.writeHead(405, { 'Content-Type': 'application/json' });
|
|
|
|
|
res.end(JSON.stringify({ error: 'Method Not Allowed' }));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const data = await getModelsDevData();
|
|
|
|
|
res.writeHead(200, {
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
'Cache-Control': 'public, max-age=3600'
|
|
|
|
|
});
|
|
|
|
|
res.end(JSON.stringify(data));
|
|
|
|
|
} catch (err) {
|
|
|
|
|
ctx.logger?.error?.('[model-enhancer] Error fetching models.dev:', err);
|
|
|
|
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
|
|
|
res.end(JSON.stringify({ error: err.message || 'Internal Server Error' }));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}, 'dsh-plugin-model-enhancer: webServer proxy route');
|
2026-09-09 10:08:45 +08:00
|
|
|
|
|
|
|
|
// 2. Binary / Large Document File Upload to Workspace Endpoint
|
|
|
|
|
ctx.effect(() => {
|
|
|
|
|
return ctx.webServer.register({
|
|
|
|
|
kind: 'exact',
|
|
|
|
|
path: '/api/model-enhancer/upload',
|
|
|
|
|
handler: async (req, res) => {
|
|
|
|
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
|
|
|
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
|
|
|
|
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Filename');
|
|
|
|
|
|
|
|
|
|
if (req.method === 'OPTIONS') {
|
|
|
|
|
res.writeHead(204);
|
|
|
|
|
res.end();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (req.method !== 'POST') {
|
|
|
|
|
res.writeHead(405, { 'Content-Type': 'application/json' });
|
|
|
|
|
res.end(JSON.stringify({ error: 'Method Not Allowed' }));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const urlObj = new URL(req.url, 'http://127.0.0.1');
|
|
|
|
|
const rawName = req.headers['x-filename'] || urlObj.searchParams.get('filename') || `upload_${Date.now()}.bin`;
|
|
|
|
|
const decodedName = decodeURIComponent(rawName);
|
|
|
|
|
const safeName = path.basename(decodedName);
|
|
|
|
|
|
|
|
|
|
const uploadDir = path.join(process.cwd(), 'uploads');
|
|
|
|
|
fs.mkdirSync(uploadDir, { recursive: true });
|
|
|
|
|
|
|
|
|
|
const targetPath = path.join(uploadDir, safeName);
|
|
|
|
|
const writeStream = fs.createWriteStream(targetPath);
|
|
|
|
|
|
|
|
|
|
let totalBytes = 0;
|
|
|
|
|
req.on('data', (chunk) => {
|
|
|
|
|
totalBytes += chunk.length;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
req.pipe(writeStream);
|
|
|
|
|
|
|
|
|
|
writeStream.on('finish', () => {
|
|
|
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
|
|
|
res.end(JSON.stringify({
|
|
|
|
|
ok: true,
|
|
|
|
|
filename: safeName,
|
|
|
|
|
path: path.join('uploads', safeName),
|
|
|
|
|
size: totalBytes
|
|
|
|
|
}));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
writeStream.on('error', (err) => {
|
|
|
|
|
ctx.logger?.error?.('[model-enhancer] Upload stream error:', err);
|
|
|
|
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
|
|
|
res.end(JSON.stringify({ error: err.message || 'File write error' }));
|
|
|
|
|
});
|
|
|
|
|
} catch (err) {
|
|
|
|
|
ctx.logger?.error?.('[model-enhancer] Upload handler error:', err);
|
|
|
|
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
|
|
|
res.end(JSON.stringify({ error: err.message || 'Upload processing error' }));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}, 'dsh-plugin-model-enhancer: workspace file upload endpoint');
|
2026-09-09 00:52:52 +08:00
|
|
|
}
|