Refactor AI proxy error handling and remove deprecated upload and event routes

- Updated `getAiProxy` function to return a JSON response for missing objects when the user is the owner.
- Removed the `upload.ts`, `event.ts`, and related middleware files to streamline the codebase.
- Cleaned up `handle-request.ts` and `index.ts` by removing unused imports and routes.
- Deleted chunk upload handling and related utility functions to simplify resource management.
- Enhanced app manager list functionality to support app creation if not found.
This commit is contained in:
2026-02-02 18:06:31 +08:00
parent 82e3392b36
commit 158dd9e85c
14 changed files with 92 additions and 908 deletions

View File

@@ -98,10 +98,19 @@ const getAiProxy = async (req: IncomingMessage, res: ServerResponse, opts: Proxy
return true;
}
const stat = await oss.statObject(objectName);
if (!stat) {
createNotFoundPage('Invalid proxy url');
if (!stat && isOwner) {
// createNotFoundPage('文件不存在');
res.writeHead(200, { 'content-type': 'application/json' });
res.end(
JSON.stringify({
code: 404,
message: 'object not found',
}),
);
logger.debug('no stat', objectName, owner, req.url);
return true;
} else if (!stat && !isOwner) {
return createNotFoundPage('Invalid ai proxy url');
}
const permissionInstance = new UserPermission({ permission: stat.metaData as Permission, owner: owner });
const checkPermission = permissionInstance.checkPermissionSuccess({
@@ -112,6 +121,7 @@ const getAiProxy = async (req: IncomingMessage, res: ServerResponse, opts: Proxy
logger.info('no permission', checkPermission, loginUser, owner);
return createNotFoundPage('no permission');
}
if (showStat) {
res.writeHead(200, { 'content-type': 'application/json' });
res.end(

View File

@@ -1,171 +0,0 @@
import Busboy from 'busboy';
import { checkAuth } from '../middleware/auth.ts';
import { router, clients, writeEvents } from '../router.ts';
import { error } from '../middleware/auth.ts';
import fs from 'fs';
import { useFileStore } from '@kevisual/use-config';
import { app, oss } from '@/app.ts';
import { getContentType } from '@/utils/get-content-type.ts';
import path from 'path';
import { createWriteStream } from 'fs';
import crypto from 'crypto';
import { pipeBusboy } from '@/modules/fm-manager/index.ts';
const cacheFilePath = useFileStore('cache-file', { needExists: true });
router.post('/api/micro-app/upload', async (req, res) => {
if (res.headersSent) return; // 如果响应已发送,不再处理
res.writeHead(200, { 'Content-Type': 'application/json' });
const { tokenUser, token } = await checkAuth(req, res);
if (!tokenUser) return;
// 使用 busboy 解析 multipart/form-data
const busboy = Busboy({ headers: req.headers, preservePath: true, defCharset: 'utf-8' });
const fields: any = {};
let file: any = null;
let filePromise: Promise<void> | null = null;
let bytesReceived = 0;
let bytesExpected = parseInt(req.headers['content-length'] || '0');
busboy.on('field', (fieldname, value) => {
fields[fieldname] = value;
});
busboy.on('file', (fieldname, fileStream, info) => {
const { filename, encoding, mimeType } = info;
// 处理 UTF-8 文件名编码
const decodedFilename = typeof filename === 'string' ? Buffer.from(filename, 'latin1').toString('utf8') : filename;
const tempPath = path.join(cacheFilePath, `${Date.now()}-${Math.random().toString(36).substring(7)}`);
const writeStream = createWriteStream(tempPath);
const hash = crypto.createHash('md5');
let size = 0;
filePromise = new Promise<void>((resolve, reject) => {
fileStream.on('data', (chunk) => {
bytesReceived += chunk.length;
size += chunk.length;
hash.update(chunk);
if (bytesExpected > 0) {
const progress = (bytesReceived / bytesExpected) * 100;
console.log(`Upload progress: ${progress.toFixed(2)}%`);
const data = {
progress: progress.toFixed(2),
message: `Upload progress: ${progress.toFixed(2)}%`,
};
writeEvents(req, data);
}
});
fileStream.pipe(writeStream);
writeStream.on('finish', () => {
file = {
filepath: tempPath,
originalFilename: decodedFilename,
mimetype: mimeType,
hash: hash.digest('hex'),
size: size,
};
resolve();
});
writeStream.on('error', (err) => {
reject(err);
});
});
});
busboy.on('finish', async () => {
// 等待文件写入完成
if (filePromise) {
try {
await filePromise;
} catch (err) {
console.error(`File write error: ${err.message}`);
res.end(error(`File write error: ${err.message}`));
return;
}
}
const clearFiles = () => {
if (file?.filepath && fs.existsSync(file.filepath)) {
fs.unlinkSync(file.filepath);
}
};
if (!file) {
res.end(error('No file uploaded'));
return;
}
let appKey, collection;
const { appKey: _appKey, collection: _collecion } = fields;
if (Array.isArray(_appKey)) {
appKey = _appKey?.[0];
} else {
appKey = _appKey;
}
if (Array.isArray(_collecion)) {
collection = _collecion?.[0];
} else {
collection = _collecion;
}
collection = parseIfJson(collection);
appKey = appKey || 'micro-app';
console.log('Appkey', appKey);
console.log('collection', collection);
// 处理上传的文件
const uploadResults = [];
const tempPath = file.filepath; // 文件上传时的临时路径
const relativePath = file.originalFilename; // 保留表单中上传的文件名 (包含文件夹结构)
// 比如 child2/b.txt
const minioPath = `private/${tokenUser.username}/${appKey}/${relativePath}`;
// 上传到 MinIO 并保留文件夹结构
const isHTML = relativePath.endsWith('.html');
await oss.fPutObject(minioPath, tempPath, {
'Content-Type': getContentType(relativePath),
'app-source': 'user-micro-app',
'Cache-Control': isHTML ? 'no-cache' : 'max-age=31536000, immutable', // 缓存一年
});
uploadResults.push({
name: relativePath,
path: minioPath,
hash: file.hash,
size: file.size,
});
fs.unlinkSync(tempPath); // 删除临时文件
// 受控
const r = await app.call({
path: 'micro-app',
key: 'upload',
payload: {
token: token,
data: {
appKey,
collection,
files: uploadResults,
},
},
});
const data: any = {
code: r.code,
data: r.body,
};
if (r.message) {
data.message = r.message;
}
res.end(JSON.stringify(data));
});
pipeBusboy(req, res, busboy);
});
function parseIfJson(collection: any): any {
try {
return JSON.parse(collection);
} catch (e) {
return collection;
}
}

View File

@@ -1,49 +0,0 @@
import { router, error, checkAuth, clients, getTaskId, writeEvents, deleteOldClients } from './router.ts';
router.get('/api/events', async (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
});
const taskId = getTaskId(req);
if (!taskId) {
res.end(error('task-id is required'));
return;
}
// 将客户端连接推送到 clients 数组
clients.set(taskId, { client: res, createTime: Date.now() });
// 移除客户端连接
req.on('close', () => {
clients.delete(taskId);
});
});
router.get('/api/s1/events', async (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
});
const taskId = getTaskId(req);
if (!taskId) {
res.end(error('task-id is required'));
return;
}
// 将客户端连接推送到 clients 数组
clients.set(taskId, { client: res, createTime: Date.now() });
writeEvents(req, { progress: 0, message: 'start' });
// 不自动关闭连接
// res.end('ok');
});
router.get('/api/s1/events/close', async (req, res) => {
const taskId = getTaskId(req);
if (!taskId) {
res.end(error('task-id is required'));
return;
}
deleteOldClients();
clients.delete(taskId);
res.end('ok');
});

View File

@@ -1,20 +1,9 @@
import { useFileStore } from '@kevisual/use-config';
import http from 'node:http';
import fs from 'fs';
import Busboy from 'busboy';
import { app, oss } from '@/app.ts';
import { getContentType } from '@/utils/get-content-type.ts';
import { User } from '@/models/user.ts';
import { router, error, checkAuth, writeEvents } from './router.ts';
import { router } from './router.ts';
import './index.ts';
import { handleRequest as PageProxy } from './page-proxy.ts';
const simpleAppsPrefixs = [
"/api/micro-app/",
"/api/events",
"/api/s1/",
"/api/resource/",
"/api/wxmsg"
];

View File

@@ -1,6 +0,0 @@
// import './code/upload.ts';
import './event.ts';
import './resources/upload.ts';
import './resources/chunk.ts';
// import './resources/get-resources.ts';

View File

@@ -1,61 +0,0 @@
import { User } from '@/models/user.ts';
import http from 'http';
import { parse } from '@kevisual/router/src/server/cookie.ts';
export const error = (msg: string, code = 500) => {
return JSON.stringify({ code, message: msg });
};
export const checkAuth = async (req: http.IncomingMessage, res: http.ServerResponse) => {
let token = (req.headers?.['authorization'] as string) || (req.headers?.['Authorization'] as string) || '';
const url = new URL(req.url || '', 'http://localhost');
const resNoPermission = () => {
res.statusCode = 401;
res.end(error('Invalid authorization'));
return { tokenUser: null, token: null };
};
if (!token) {
token = url.searchParams.get('token') || '';
}
if (!token) {
const parsedCookies = parse(req.headers.cookie || '');
token = parsedCookies.token || '';
}
if (!token) {
return resNoPermission();
}
if (token) {
token = token.replace('Bearer ', '');
}
let tokenUser;
try {
tokenUser = await User.verifyToken(token);
} catch (e) {
console.log('checkAuth error', e);
res.statusCode = 401;
res.end(error('Invalid token'));
return { tokenUser: null, token: null };
}
return { tokenUser, token };
};
export const getLoginUser = async (req: http.IncomingMessage) => {
let token = (req.headers?.['authorization'] as string) || (req.headers?.['Authorization'] as string) || '';
const url = new URL(req.url || '', 'http://localhost');
if (!token) {
token = url.searchParams.get('token') || '';
}
if (!token) {
const parsedCookies = parse(req.headers.cookie || '');
token = parsedCookies.token || '';
}
if (token) {
token = token.replace('Bearer ', '');
}
let tokenUser;
try {
tokenUser = await User.verifyToken(token);
return { tokenUser, token };
} catch (e) {
return null;
}
};

View File

@@ -1 +0,0 @@
export * from './auth.ts'

View File

@@ -1,237 +0,0 @@
import { useFileStore } from '@kevisual/use-config';
import { checkAuth, error, router, writeEvents, getKey, getTaskId } from '../router.ts';
import Busboy from 'busboy';
import { app, oss } from '@/app.ts';
import { getContentType } from '@/utils/get-content-type.ts';
import { User } from '@/models/user.ts';
import fs from 'fs';
import { ConfigModel } from '@/routes/config/models/model.ts';
import { validateDirectory } from './util.ts';
import path from 'path';
import { createWriteStream } from 'fs';
import { pipeBusboy } from '@/modules/fm-manager/index.ts';
const cacheFilePath = useFileStore('cache-file', { needExists: true });
router.get('/api/s1/resources/upload/chunk', async (req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Upload API is ready');
});
// /api/s1/resources/upload
router.post('/api/s1/resources/upload/chunk', async (req, res) => {
const { tokenUser, token } = await checkAuth(req, res);
if (!tokenUser) return;
const url = new URL(req.url || '', 'http://localhost');
const share = !!url.searchParams.get('public');
const noCheckAppFiles = !!url.searchParams.get('noCheckAppFiles');
const taskId = getTaskId(req);
const finalFilePath = `${cacheFilePath}/${taskId}`;
if (!taskId) {
res.end(error('taskId is required'));
return;
}
// 使用 busboy 解析 multipart/form-data
const busboy = Busboy({ headers: req.headers, preservePath: true, defCharset: 'utf-8' });
const fields: any = {};
let file: any = null;
let tempPath = '';
let filePromise: Promise<void> | null = null;
busboy.on('field', (fieldname, value) => {
fields[fieldname] = value;
});
busboy.on('file', (fieldname, fileStream, info) => {
const { filename, encoding, mimeType } = info;
// 处理 UTF-8 文件名编码
const decodedFilename = typeof filename === 'string' ? Buffer.from(filename, 'latin1').toString('utf8') : filename;
tempPath = path.join(cacheFilePath, `${Date.now()}-${Math.random().toString(36).substring(7)}`);
const writeStream = createWriteStream(tempPath);
filePromise = new Promise<void>((resolve, reject) => {
fileStream.pipe(writeStream);
writeStream.on('finish', () => {
file = {
filepath: tempPath,
originalFilename: decodedFilename,
mimetype: mimeType,
};
resolve();
});
writeStream.on('error', (err) => {
reject(err);
});
});
});
busboy.on('finish', async () => {
// 等待文件写入完成
if (filePromise) {
try {
await filePromise;
} catch (err) {
console.error(`File write error: ${err.message}`);
res.end(error(`File write error: ${err.message}`));
return;
}
}
const clearFiles = () => {
if (tempPath && fs.existsSync(tempPath)) {
fs.unlinkSync(tempPath);
}
if (fs.existsSync(finalFilePath)) {
fs.unlinkSync(finalFilePath);
}
};
if (!file) {
res.end(error('No file uploaded'));
return;
}
// Handle chunked upload logic here
let { chunkIndex, totalChunks, appKey, version, username, directory } = getKey(fields, [
'chunkIndex',
'totalChunks',
'appKey',
'version',
'username',
'directory',
]);
if (!chunkIndex || !totalChunks) {
res.end(error('chunkIndex, totalChunks is required'));
clearFiles();
return;
}
const relativePath = file.originalFilename;
const writeStream = fs.createWriteStream(finalFilePath, { flags: 'a' });
const readStream = fs.createReadStream(tempPath);
readStream.pipe(writeStream);
writeStream.on('finish', async () => {
fs.unlinkSync(tempPath); // 删除临时文件
// Write event for progress tracking
const progress = ((parseInt(chunkIndex) + 1) / parseInt(totalChunks)) * 100;
writeEvents(req, {
progress,
message: `Upload progress: ${progress}%`,
});
if (parseInt(chunkIndex) + 1 === parseInt(totalChunks)) {
let uid = tokenUser.id;
if (username) {
const user = await User.getUserByToken(token);
const has = await user.hasUser(username, true);
if (!has) {
res.end(error('username is not found'));
clearFiles();
return;
}
const _user = await User.findOne({ where: { username } });
uid = _user?.id || '';
}
if (!appKey || !version) {
const config = await ConfigModel.getUploadConfig({ uid });
if (config) {
appKey = config.config?.data?.key || '';
version = config.config?.data?.version || '';
}
}
if (!appKey || !version) {
res.end(error('appKey or version is not found, please check the upload config.'));
clearFiles();
return;
}
const { code, message } = validateDirectory(directory);
if (code !== 200) {
res.end(error(message));
clearFiles();
return;
}
const minioPath = `${username || tokenUser.username}/${appKey}/${version}${directory ? `/${directory}` : ''}/${relativePath}`;
const metadata: any = {};
if (share) {
metadata.share = 'public';
}
// All chunks uploaded, now upload to MinIO
await oss.fPutObject(minioPath, finalFilePath, {
'Content-Type': getContentType(relativePath),
'app-source': 'user-app',
'Cache-Control': relativePath.endsWith('.html') ? 'no-cache' : 'max-age=31536000, immutable',
...metadata,
});
// Clean up the final file
fs.unlinkSync(finalFilePath);
const downloadBase = '/api/s1/share';
const uploadResult = {
name: relativePath,
path: `${downloadBase}/${minioPath}`,
appKey,
version,
username,
};
if (!noCheckAppFiles) {
// Notify the app
const r = await app.call({
path: 'app',
key: 'detectVersionList',
payload: {
token: token,
data: {
appKey,
version,
username,
},
},
});
const data: any = {
code: r.code,
data: {
app: r.body,
upload: [uploadResult],
},
};
if (r.message) {
data.message = r.message;
}
console.log('upload data', data);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(data));
} else {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(
JSON.stringify({
code: 200,
message: 'Chunk uploaded successfully',
data: { chunkIndex, totalChunks, upload: [uploadResult] },
}),
);
}
} else {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(
JSON.stringify({
code: 200,
message: 'Chunk uploaded successfully',
data: {
chunkIndex,
totalChunks,
},
}),
);
}
});
});
pipeBusboy(req, res, busboy);
});

View File

@@ -1,290 +0,0 @@
import { useFileStore } from '@kevisual/use-config';
import { checkAuth, error, router, writeEvents, getKey } from '../router.ts';
import Busboy from 'busboy';
import { app } from '@/app.ts';
import { getContentType } from '@/utils/get-content-type.ts';
import { User } from '@/models/user.ts';
import fs from 'fs';
import path from 'path';
import { createWriteStream } from 'fs';
import { pipeBusboy } from '@/modules/fm-manager/pipe-busboy.ts';
import { ConfigModel } from '@/routes/config/models/model.ts';
import { validateDirectory } from './util.ts';
import { pick } from 'es-toolkit';
import { getFileStat } from '@/routes/file/index.ts';
import { logger } from '@/modules/logger.ts';
import { oss } from '@/modules/s3.ts';
const cacheFilePath = useFileStore('cache-file', { needExists: true });
router.get('/api/s1/resources/upload', async (req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Upload API is ready');
});
export const parseIfJson = (data = '{}') => {
try {
const _data = JSON.parse(data);
if (typeof _data === 'object') return _data;
return {};
} catch (error) {
return {};
}
};
router.post('/api/s1/resources/upload/check', async (req, res) => {
const { tokenUser, token } = await checkAuth(req, res);
if (!tokenUser) {
res.end(error('Token is invalid.'));
return;
}
console.log('data', req.url);
res.writeHead(200, { 'Content-Type': 'application/json' });
const data = await router.getBody(req);
type Data = {
appKey: string;
version: string;
username: string;
directory: string;
files: { path: string; hash: string }[];
};
let { appKey, version, username, directory, files } = pick(data, ['appKey', 'version', 'username', 'directory', 'files']) as Data;
let uid = tokenUser.id;
if (username) {
const user = await User.getUserByToken(token);
const has = await user.hasUser(username, true);
if (!has) {
res.end(error('username is not found'));
return;
}
const _user = await User.findOne({ where: { username } });
uid = _user?.id || '';
}
if (!appKey || !version) {
res.end(error('appKey and version is required'));
}
const { code, message } = validateDirectory(directory);
if (code !== 200) {
res.end(error(message));
return;
}
type CheckResult = {
path: string;
stat: any;
resourcePath: string;
hash: string;
uploadHash: string;
isUpload?: boolean;
};
const checkResult: CheckResult[] = [];
for (let i = 0; i < files.length; i++) {
const file = files[i];
const relativePath = file.path;
const minioPath = `${username || tokenUser.username}/${appKey}/${version}${directory ? `/${directory}` : ''}/${relativePath}`;
let stat = await getFileStat(minioPath, true);
const statHash = stat?.etag || '';
checkResult.push({
path: relativePath,
uploadHash: file.hash,
resourcePath: minioPath,
isUpload: statHash === file.hash,
stat,
hash: statHash,
});
}
res.end(JSON.stringify({ code: 200, data: checkResult }));
});
// /api/s1/resources/upload
router.post('/api/s1/resources/upload', async (req, res) => {
const { tokenUser, token } = await checkAuth(req, res);
if (!tokenUser) {
res.end(error('Token is invalid.'));
return;
}
const url = new URL(req.url || '', 'http://localhost');
const share = !!url.searchParams.get('public');
const meta = parseIfJson(url.searchParams.get('meta'));
const noCheckAppFiles = !!url.searchParams.get('noCheckAppFiles');
// 使用 busboy 解析 multipart/form-data
const busboy = Busboy({ headers: req.headers, preservePath: true, defCharset: 'utf-8' });
const fields: any = {};
const files: any[] = [];
const filePromises: Promise<void>[] = [];
let bytesReceived = 0;
let bytesExpected = parseInt(req.headers['content-length'] || '0');
busboy.on('field', (fieldname, value) => {
fields[fieldname] = value;
});
busboy.on('file', (fieldname, fileStream, info) => {
const { filename, encoding, mimeType } = info;
// 处理 UTF-8 文件名编码busboy 可能返回 Latin-1 编码的缓冲区)
const decodedFilename = typeof filename === 'string' ? Buffer.from(filename, 'latin1').toString('utf8') : filename;
const tempPath = path.join(cacheFilePath, `${Date.now()}-${Math.random().toString(36).substring(7)}`);
const writeStream = createWriteStream(tempPath);
const filePromise = new Promise<void>((resolve, reject) => {
fileStream.on('data', (chunk) => {
bytesReceived += chunk.length;
if (bytesExpected > 0) {
const progress = (bytesReceived / bytesExpected) * 100;
const data = {
progress: progress.toFixed(2),
message: `Upload progress: ${progress.toFixed(2)}%`,
};
console.log('progress-upload', data);
writeEvents(req, data);
}
});
fileStream.pipe(writeStream);
writeStream.on('finish', () => {
files.push({
filepath: tempPath,
originalFilename: decodedFilename,
mimetype: mimeType,
});
resolve();
});
writeStream.on('error', (err) => {
reject(err);
});
});
filePromises.push(filePromise);
});
busboy.on('finish', async () => {
// 等待所有文件写入完成
try {
await Promise.all(filePromises);
} catch (err) {
logger.error(`File write error: ${err.message}`);
res.end(error(`File write error: ${err.message}`));
return;
}
const clearFiles = () => {
files.forEach((file) => {
if (file?.filepath && fs.existsSync(file.filepath)) {
fs.unlinkSync(file.filepath);
}
});
};
// 检查是否有文件上传
if (files.length === 0) {
res.end(error('files is required'));
return;
}
let { appKey, version, username, directory, description } = getKey(fields, ['appKey', 'version', 'username', 'directory', 'description']);
let uid = tokenUser.id;
if (username) {
const user = await User.getUserByToken(token);
const has = await user.hasUser(username, true);
if (!has) {
res.end(error('username is not found'));
clearFiles();
return;
}
const _user = await User.findOne({ where: { username } });
uid = _user?.id || '';
}
if (!appKey || !version) {
const config = await ConfigModel.getUploadConfig({ uid });
if (config) {
appKey = config.config?.data?.key || '';
version = config.config?.data?.version || '';
}
}
if (!appKey || !version) {
res.end(error('appKey or version is not found, please check the upload config.'));
clearFiles();
return;
}
const { code, message } = validateDirectory(directory);
if (code !== 200) {
res.end(error(message));
clearFiles();
return;
}
// 逐个处理每个上传的文件
const uploadedFiles = files;
logger.info(
'upload files',
uploadedFiles.map((item) => {
return pick(item, ['filepath', 'originalFilename']);
}),
);
const uploadResults = [];
for (let i = 0; i < uploadedFiles.length; i++) {
const file = uploadedFiles[i];
// @ts-ignore
const tempPath = file.filepath; // 文件上传时的临时路径
const relativePath = file.originalFilename; // 保留表单中上传的文件名 (包含文件夹结构)
// 比如 child2/b.txt
const minioPath = `${username || tokenUser.username}/${appKey}/${version}${directory ? `/${directory}` : ''}/${relativePath}`;
// 上传到 MinIO 并保留文件夹结构
const isHTML = relativePath.endsWith('.html');
const metadata: any = {};
if (share) {
metadata.share = 'public';
}
Object.assign(metadata, meta);
await oss.fPutObject(minioPath, tempPath, {
'Content-Type': getContentType(relativePath),
'app-source': 'user-app',
'Cache-Control': isHTML ? 'no-cache' : 'max-age=31536000, immutable', // 缓存一年
...metadata,
});
uploadResults.push({
name: relativePath,
path: minioPath,
});
fs.unlinkSync(tempPath); // 删除临时文件
}
if (!noCheckAppFiles) {
const _data = { appKey, version, username, files: uploadResults, description, }
if (_data.description) {
delete _data.description;
}
// 受控
const r = await app.call({
path: 'app',
key: 'uploadFiles',
payload: {
token: token,
data: _data,
},
});
const data: any = {
code: r.code,
data: {
app: r.body,
upload: uploadResults,
},
};
if (r.message) {
data.message = r.message;
}
console.log('upload data', data);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(data));
} else {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(
JSON.stringify({
code: 200,
data: {
detect: [],
upload: uploadResults,
},
}),
);
}
});
pipeBusboy(req, res, busboy);
});

View File

@@ -1,30 +0,0 @@
/**
* 校验directory是否合法, 合法返回200, 不合法返回500
*
* directory 不能以/开头,不能以/结尾。不能以.开头,不能以.结尾。
* 把directory的/替换掉后,只能包含数字、字母、下划线、中划线
* @param directory 目录
* @returns
*/
export const validateDirectory = (directory?: string) => {
// 对directory进行校验不能以/开头,不能以/结尾。不能以.开头,不能以.结尾。
if (directory && (directory.startsWith('/') || directory.endsWith('/') || directory.startsWith('..') || directory.endsWith('..'))) {
return {
code: 500,
message: 'directory is invalid',
};
}
// 把directory的/替换掉后,只能包含数字、字母、下划线、中划线
// 可以包含.
let _directory = directory?.replace(/\//g, '');
if (_directory && !/^[a-zA-Z0-9_.-]+$/.test(_directory)) {
return {
code: 500,
message: 'directory is invalid, only number, letter, underline and hyphen are allowed',
};
}
return {
code: 200,
message: 'directory is valid',
};
};

View File

@@ -1,14 +1,13 @@
import { router } from '@/app.ts';
import http from 'http';
import { useContextKey } from '@kevisual/context';
import { checkAuth, error } from './middleware/auth.ts';
export { router, checkAuth, error };
export { router, };
/**
* 事件客户端
*/
const eventClientsInit = () => {
const clients = new Map<string, { client?: http.ServerResponse; createTime?: number; [key: string]: any }>();
const clients = new Map<string, { client?: http.ServerResponse; createTime?: number;[key: string]: any }>();
return clients;
};
export const clients = useContextKey('event-clients', () => eventClientsInit());

View File

@@ -43,7 +43,7 @@ app
console.log('get app manager called');
const tokenUser = ctx.state.tokenUser;
const id = ctx.query.id;
const { key, version } = ctx.query?.data || {};
const { key, version, create = false } = ctx.query?.data || {};
if (!id && (!key || !version)) {
throw new CustomError('id is required');
}
@@ -59,8 +59,27 @@ app
},
});
}
if (!am && create) {
am = await AppListModel.create({
key,
version,
uid: tokenUser.id,
data: {},
});
const res = await app.run({ path: 'app', key: "detectVersionList", payload: { data: { appKey: key, version, username: tokenUser.username }, token: ctx.query.token } });
if (res.code !== 200) {
ctx.throw(res.message || 'detect version list error');
}
am = await AppListModel.findOne({
where: {
key,
version,
uid: tokenUser.id,
},
});
}
if (!am) {
throw new CustomError('app not found');
ctx.throw('app not found');
}
console.log('get app', am.id, am.key, am.version);
ctx.body = prefixFix(am, tokenUser.username);