feat: 改进api生成方式

This commit is contained in:
SikongJueluo 2025-07-11 14:32:26 +08:00
parent bdffba7576
commit d88c710606
No known key found for this signature in database
4 changed files with 720 additions and 99 deletions

300
package-lock.json generated
View File

@ -40,10 +40,13 @@
"@vue/tsconfig": "^0.7.0",
"autoprefixer": "^10.4.20",
"daisyui": "^5.0.0",
"node-fetch": "^3.3.2",
"npm-run-all2": "^7.0.2",
"nswag": "^14.3.0",
"postcss": "^8.5.3",
"tailwindcss": "^4.0.12",
"ts-node": "^10.9.2",
"tsx": "^4.20.3",
"typescript": "~5.7.3",
"unplugin-vue-components": "^28.8.0",
"vite": "^6.1.0",
@ -542,6 +545,30 @@
"node": ">=6.9.0"
}
},
"node_modules/@cspotcode/source-map-support": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/trace-mapping": "0.3.9"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": {
"version": "0.3.9",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.0.3",
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz",
@ -1742,6 +1769,34 @@
"vue": "^2.7.0 || ^3.0.0"
}
},
"node_modules/@tsconfig/node10": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
"integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==",
"dev": true,
"license": "MIT"
},
"node_modules/@tsconfig/node12": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
"dev": true,
"license": "MIT"
},
"node_modules/@tsconfig/node14": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
"dev": true,
"license": "MIT"
},
"node_modules/@tsconfig/node16": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
"dev": true,
"license": "MIT"
},
"node_modules/@tsconfig/node22": {
"version": "22.0.1",
"resolved": "https://registry.npmjs.org/@tsconfig/node22/-/node22-22.0.1.tgz",
@ -2181,6 +2236,19 @@
"node": ">=0.4.0"
}
},
"node_modules/acorn-walk": {
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
"integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
"dev": true,
"license": "MIT",
"dependencies": {
"acorn": "^8.11.0"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/alien-signals": {
"version": "1.0.13",
"resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz",
@ -2228,6 +2296,13 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/arg": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
"dev": true,
"license": "MIT"
},
"node_modules/aria-hidden": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
@ -2476,6 +2551,13 @@
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/create-require": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
"dev": true,
"license": "MIT"
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@ -2530,6 +2612,16 @@
"url": "https://github.com/saadeghi/daisyui?sponsor=1"
}
},
"node_modules/data-uri-to-buffer": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 12"
}
},
"node_modules/de-indent": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
@ -2620,6 +2712,16 @@
"node": ">=8"
}
},
"node_modules/diff": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.3.1"
}
},
"node_modules/echarts": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz",
@ -2791,6 +2893,30 @@
}
}
},
"node_modules/fetch-blob": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "paypal",
"url": "https://paypal.me/jimmywarting"
}
],
"license": "MIT",
"dependencies": {
"node-domexception": "^1.0.0",
"web-streams-polyfill": "^3.0.3"
},
"engines": {
"node": "^12.20 || >= 14.13"
}
},
"node_modules/figures": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz",
@ -2820,6 +2946,19 @@
"node": ">=8"
}
},
"node_modules/formdata-polyfill": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
"dev": true,
"license": "MIT",
"dependencies": {
"fetch-blob": "^3.1.2"
},
"engines": {
"node": ">=12.20.0"
}
},
"node_modules/fraction.js": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
@ -2891,6 +3030,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/get-tsconfig": {
"version": "4.10.1",
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz",
"integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"resolve-pkg-maps": "^1.0.0"
},
"funding": {
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
}
},
"node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
@ -3519,6 +3671,13 @@
"@jridgewell/sourcemap-codec": "^1.5.0"
}
},
"node_modules/make-error": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
"dev": true,
"license": "ISC"
},
"node_modules/marked": {
"version": "12.0.2",
"resolved": "https://registry.npmjs.org/marked/-/marked-12.0.2.tgz",
@ -3672,6 +3831,46 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
"deprecated": "Use your platform's native DOMException instead",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "github",
"url": "https://paypal.me/jimmywarting"
}
],
"license": "MIT",
"engines": {
"node": ">=10.5.0"
}
},
"node_modules/node-fetch": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
"dev": true,
"license": "MIT",
"dependencies": {
"data-uri-to-buffer": "^4.0.0",
"fetch-blob": "^3.1.4",
"formdata-polyfill": "^4.0.10"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/node-fetch"
}
},
"node_modules/node-releases": {
"version": "2.0.19",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
@ -4065,6 +4264,16 @@
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/resolve-pkg-maps": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
}
},
"node_modules/rfdc": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
@ -4316,6 +4525,50 @@
"integrity": "sha512-320x5Ggei84AxzlXp91QkIGSw5wgaLT6GeAH0KsqDmRZdVWW2OiSeVvElVoatk3f7nicwXlElXsoFkARiGE2yg==",
"license": "MIT"
},
"node_modules/ts-node": {
"version": "10.9.2",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@cspotcode/source-map-support": "^0.8.0",
"@tsconfig/node10": "^1.0.7",
"@tsconfig/node12": "^1.0.7",
"@tsconfig/node14": "^1.0.0",
"@tsconfig/node16": "^1.0.2",
"acorn": "^8.4.1",
"acorn-walk": "^8.1.1",
"arg": "^4.1.0",
"create-require": "^1.1.0",
"diff": "^4.0.1",
"make-error": "^1.1.1",
"v8-compile-cache-lib": "^3.0.1",
"yn": "3.1.1"
},
"bin": {
"ts-node": "dist/bin.js",
"ts-node-cwd": "dist/bin-cwd.js",
"ts-node-esm": "dist/bin-esm.js",
"ts-node-script": "dist/bin-script.js",
"ts-node-transpile-only": "dist/bin-transpile.js",
"ts-script": "dist/bin-script-deprecated.js"
},
"peerDependencies": {
"@swc/core": ">=1.2.50",
"@swc/wasm": ">=1.2.50",
"@types/node": "*",
"typescript": ">=2.7"
},
"peerDependenciesMeta": {
"@swc/core": {
"optional": true
},
"@swc/wasm": {
"optional": true
}
}
},
"node_modules/ts-results-es": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ts-results-es/-/ts-results-es-5.0.1.tgz",
@ -4328,6 +4581,26 @@
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/tsx": {
"version": "4.20.3",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.3.tgz",
"integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "~0.25.0",
"get-tsconfig": "^4.7.5"
},
"bin": {
"tsx": "dist/cli.mjs"
},
"engines": {
"node": ">=18.0.0"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
}
},
"node_modules/typed-function": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/typed-function/-/typed-function-4.2.1.tgz",
@ -4487,6 +4760,13 @@
"browserslist": ">= 4.21.0"
}
},
"node_modules/v8-compile-cache-lib": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
"dev": true,
"license": "MIT"
},
"node_modules/vite": {
"version": "6.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
@ -4789,6 +5069,16 @@
"typescript": ">=5.0.0"
}
},
"node_modules/web-streams-polyfill": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 8"
}
},
"node_modules/webpack-virtual-modules": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
@ -4819,6 +5109,16 @@
"dev": true,
"license": "ISC"
},
"node_modules/yn": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/yocto-queue": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz",

View File

@ -9,9 +9,7 @@
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --build",
"pregen-api": "cd server && dotnet run --property:Configuration=Release &",
"gen-api": "npx nswag openapi2tsclient /input:http://localhost:5000/swagger/v1/swagger.json /output:src/APIClient.ts",
"postgen-api": "pkill server"
"gen-api": "npx tsx scripts/GenerateWebAPI.ts"
},
"dependencies": {
"@svgdotjs/svg.js": "^3.2.4",
@ -46,10 +44,13 @@
"@vue/tsconfig": "^0.7.0",
"autoprefixer": "^10.4.20",
"daisyui": "^5.0.0",
"node-fetch": "^3.3.2",
"npm-run-all2": "^7.0.2",
"nswag": "^14.3.0",
"postcss": "^8.5.3",
"tailwindcss": "^4.0.12",
"ts-node": "^10.9.2",
"tsx": "^4.20.3",
"typescript": "~5.7.3",
"unplugin-vue-components": "^28.8.0",
"vite": "^6.1.0",

335
scripts/GenerateWebAPI.ts Normal file
View File

@ -0,0 +1,335 @@
import { spawn, exec, ChildProcess } from 'child_process';
import { promisify } from 'util';
import fetch from 'node-fetch';
const execAsync = promisify(exec);
async function waitForServer(url: string, maxRetries: number = 30, interval: number = 1000): Promise<boolean> {
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url);
if (response.ok) {
console.log('✓ Server is ready');
return true;
}
} catch (error) {
// Server not ready yet
}
console.log(`Waiting for server... (${i + 1}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, interval));
}
return false;
}
// 改进全局变量类型
let serverProcess: ChildProcess | null = null;
let webProcess: ChildProcess | null = null;
async function startWeb(): Promise<ChildProcess> {
console.log('Starting Vite frontend...');
return new Promise((resolve, reject) => {
const process = spawn('npm', ['run', 'dev'], {
stdio: 'pipe'
});
let webStarted = false;
process.stdout?.on('data', (data) => {
const output = data.toString();
console.log(`Web: ${output}`);
// 检查 Vite 是否已启动
if ((output.includes('Local:') || output.includes('ready in')) && !webStarted) {
webStarted = true;
resolve(process);
}
});
process.stderr?.on('data', (data) => {
console.error(`Web Error: ${data}`);
});
process.on('error', (error) => {
reject(error);
});
process.on('exit', (code, signal) => {
console.log(`Web process exited with code ${code} and signal ${signal}`);
if (!webStarted) {
reject(new Error(`Web process exited unexpectedly with code ${code}`));
}
});
// 存储进程引用
webProcess = process;
// 超时处理
setTimeout(() => {
if (!webStarted) {
reject(new Error('Web server failed to start within timeout'));
}
}, 30000); // 30秒超时
});
}
async function startServer(): Promise<ChildProcess> {
console.log('Starting .NET server...');
return new Promise((resolve, reject) => {
const process = spawn('dotnet', ['run', '--property:Configuration=Release'], {
cwd: 'server',
stdio: 'pipe'
});
let serverStarted = false;
process.stdout?.on('data', (data) => {
const output = data.toString();
console.log(`Server: ${output}`);
// 检查服务器是否已启动
if (output.includes('Now listening on:') && !serverStarted) {
serverStarted = true;
resolve(process);
}
});
process.stderr?.on('data', (data) => {
console.error(`Server Error: ${data}`);
});
process.on('error', (error) => {
reject(error);
});
process.on('exit', (code, signal) => {
console.log(`Server process exited with code ${code} and signal ${signal}`);
if (!serverStarted) {
reject(new Error(`Server process exited unexpectedly with code ${code}`));
}
});
// 存储进程引用
serverProcess = process;
// 超时处理
setTimeout(() => {
if (!serverStarted) {
reject(new Error('Server failed to start within timeout'));
}
}, 30000); // 30秒超时
});
}
async function stopServer(): Promise<void> {
console.log('Stopping server...');
if (!serverProcess) {
console.log('No server process to stop');
return;
}
try {
// 检查进程是否还存在
if (serverProcess.killed || serverProcess.exitCode !== null) {
console.log('✓ Server process already terminated');
serverProcess = null;
return;
}
// 发送 SIGTERM 信号
const killed = serverProcess.kill('SIGTERM');
if (!killed) {
console.warn('Failed to send SIGTERM to server process');
return;
}
// 等待进程优雅退出
const exitPromise = new Promise<void>((resolve) => {
if (serverProcess) {
serverProcess.on('exit', () => {
console.log('✓ Server stopped gracefully');
resolve();
});
} else {
resolve();
}
});
// 设置超时,如果 5 秒内没有退出则强制终止
const timeoutPromise = new Promise<void>((resolve) => {
setTimeout(() => {
if (serverProcess && !serverProcess.killed && serverProcess.exitCode === null) {
console.log('Force killing server process...');
serverProcess.kill('SIGKILL');
}
resolve();
}, 5000);
});
await Promise.race([exitPromise, timeoutPromise]);
} catch (error) {
console.warn('Warning: Could not stop server process:', error);
} finally {
serverProcess = null;
// 额外清理:确保没有遗留的 dotnet 进程
try {
if (process.platform !== 'win32') {
// 只清理与我们项目相关的进程
await execAsync('pkill -f "dotnet.*run.*--property:Configuration=Release"').catch(() => {
// 忽略错误,可能没有匹配的进程
});
}
} catch (cleanupError) {
// 忽略清理错误
}
}
}
async function stopWeb(): Promise<void> {
console.log('Stopping web server...');
if (!webProcess) {
console.log('No web process to stop');
return;
}
try {
// 检查进程是否还存在
if (webProcess.killed || webProcess.exitCode !== null) {
console.log('✓ Web process already terminated');
webProcess = null;
return;
}
// 发送 SIGTERM 信号
const killed = webProcess.kill('SIGTERM');
if (!killed) {
console.warn('Failed to send SIGTERM to web process');
return;
}
// 等待进程优雅退出
const exitPromise = new Promise<void>((resolve) => {
if (webProcess) {
webProcess.on('exit', () => {
console.log('✓ Web server stopped gracefully');
resolve();
});
} else {
resolve();
}
});
// 设置超时,如果 5 秒内没有退出则强制终止
const timeoutPromise = new Promise<void>((resolve) => {
setTimeout(() => {
if (webProcess && !webProcess.killed && webProcess.exitCode === null) {
console.log('Force killing web process...');
webProcess.kill('SIGKILL');
}
resolve();
}, 5000);
});
await Promise.race([exitPromise, timeoutPromise]);
} catch (error) {
console.warn('Warning: Could not stop web process:', error);
} finally {
webProcess = null;
// 额外清理:确保没有遗留的 npm/node 进程
try {
if (process.platform !== 'win32') {
// 清理可能的 vite 进程
await execAsync('pkill -f "vite"').catch(() => {
// 忽略错误,可能没有匹配的进程
});
}
} catch (cleanupError) {
// 忽略清理错误
}
}
}
async function generateApiClient(): Promise<void> {
console.log('Generating API client...');
try {
await execAsync('npx nswag openapi2tsclient /input:http://localhost:5000/swagger/v1/swagger.json /output:src/APIClient.ts');
console.log('✓ API client generated successfully');
} catch (error) {
throw new Error(`Failed to generate API client: ${error}`);
}
}
async function main(): Promise<void> {
try {
// Start web frontend first
await startWeb();
console.log('✓ Frontend started');
// Wait a bit for frontend to fully initialize
await new Promise(resolve => setTimeout(resolve, 3000));
// Start server
await startServer();
console.log('✓ Backend started');
// Wait for server to be ready (给服务器额外时间完全启动)
await new Promise(resolve => setTimeout(resolve, 2000));
// Check if swagger endpoint is available
const serverReady = await waitForServer('http://localhost:5000/swagger/v1/swagger.json');
if (!serverReady) {
throw new Error('Server failed to start within the expected time');
}
// Generate API client
await generateApiClient();
console.log('✓ API generation completed successfully');
} catch (error) {
console.error('❌ Error:', error);
process.exit(1);
} finally {
// Always try to stop processes in order: server first, then web
await stopServer();
await stopWeb();
}
}
// 改进的进程终止处理
const cleanup = async (signal: string) => {
console.log(`\nReceived ${signal}, cleaning up...`);
await stopServer();
await stopWeb();
process.exit(0);
};
process.on('SIGINT', () => cleanup('SIGINT'));
process.on('SIGTERM', () => cleanup('SIGTERM'));
// 处理未捕获的异常
process.on('uncaughtException', async (error) => {
console.error('❌ Uncaught exception:', error);
await stopServer();
await stopWeb();
process.exit(1);
});
process.on('unhandledRejection', async (reason, promise) => {
console.error('❌ Unhandled rejection at:', promise, 'reason:', reason);
await stopServer();
await stopWeb();
process.exit(1);
});
main().catch(async (error) => {
console.error('❌ Unhandled error:', error);
await stopServer();
await stopWeb();
process.exit(1);
});

View File

@ -327,7 +327,7 @@ export class BsdlParserClient {
* [TODO:description]
* @return [TODO:return]
*/
getBoundaryLogicalPorts(): Promise<FileResponse> {
getBoundaryLogicalPorts(): Promise<FileResponse | null> {
let url_ = this.baseUrl + "/api/BsdlParser/GetBoundaryLogicalPorts";
url_ = url_.replace(/[?&]$/, "");
@ -343,7 +343,7 @@ export class BsdlParserClient {
});
}
protected processGetBoundaryLogicalPorts(response: Response): Promise<FileResponse> {
protected processGetBoundaryLogicalPorts(response: Response): Promise<FileResponse | null> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200 || status === 206) {
@ -362,7 +362,7 @@ export class BsdlParserClient {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<FileResponse>(null as any);
return Promise.resolve<FileResponse | null>(null as any);
}
}
@ -377,11 +377,21 @@ export class DataClient {
}
/**
*
* @return
* [TODO:description]
* @param name (optional) [TODO:parameter]
* @param password (optional) [TODO:parameter]
* @return [TODO:return]
*/
createTables(): Promise<FileResponse> {
let url_ = this.baseUrl + "/api/Data/CreateTable";
login(name: string | undefined, password: string | undefined): Promise<FileResponse | null> {
let url_ = this.baseUrl + "/api/Data/login?";
if (name === null)
throw new Error("The parameter 'name' cannot be null.");
else if (name !== undefined)
url_ += "name=" + encodeURIComponent("" + name) + "&";
if (password === null)
throw new Error("The parameter 'password' cannot be null.");
else if (password !== undefined)
url_ += "password=" + encodeURIComponent("" + password) + "&";
url_ = url_.replace(/[?&]$/, "");
let options_: RequestInit = {
@ -392,11 +402,11 @@ export class DataClient {
};
return this.http.fetch(url_, options_).then((_response: Response) => {
return this.processCreateTables(_response);
return this.processLogin(_response);
});
}
protected processCreateTables(response: Response): Promise<FileResponse> {
protected processLogin(response: Response): Promise<FileResponse | null> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200 || status === 206) {
@ -415,57 +425,15 @@ export class DataClient {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<FileResponse>(null as any);
return Promise.resolve<FileResponse | null>(null as any);
}
/**
*
* @return
* [TODO:description]
* @return [TODO:return]
*/
dropTables(): Promise<FileResponse> {
let url_ = this.baseUrl + "/api/Data/DropTables";
url_ = url_.replace(/[?&]$/, "");
let options_: RequestInit = {
method: "DELETE",
headers: {
"Accept": "application/octet-stream"
}
};
return this.http.fetch(url_, options_).then((_response: Response) => {
return this.processDropTables(_response);
});
}
protected processDropTables(response: Response): Promise<FileResponse> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200 || status === 206) {
const contentDisposition = response.headers ? response.headers.get("content-disposition") : undefined;
let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined;
let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined;
if (fileName) {
fileName = decodeURIComponent(fileName);
} else {
fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined;
fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined;
}
return response.blob().then(blob => { return { fileName: fileName, data: blob, status: status, headers: _headers }; });
} else if (status !== 200 && status !== 204) {
return response.text().then((_responseText) => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<FileResponse>(null as any);
}
/**
*
* @return
*/
allUsers(): Promise<FileResponse> {
let url_ = this.baseUrl + "/api/Data/AllUsers";
testAuth(): Promise<FileResponse | null> {
let url_ = this.baseUrl + "/api/Data/TestAuth";
url_ = url_.replace(/[?&]$/, "");
let options_: RequestInit = {
@ -476,11 +444,11 @@ export class DataClient {
};
return this.http.fetch(url_, options_).then((_response: Response) => {
return this.processAllUsers(_response);
return this.processTestAuth(_response);
});
}
protected processAllUsers(response: Response): Promise<FileResponse> {
protected processTestAuth(response: Response): Promise<FileResponse | null> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200 || status === 206) {
@ -499,20 +467,30 @@ export class DataClient {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<FileResponse>(null as any);
return Promise.resolve<FileResponse | null>(null as any);
}
/**
*
* @param name (optional)
* @param email (optional) [TODO:parameter]
* @param password (optional) [TODO:parameter]
* @return
*/
signUpUser(name: string | undefined): Promise<FileResponse> {
signUpUser(name: string | undefined, email: string | undefined, password: string | undefined): Promise<FileResponse | null> {
let url_ = this.baseUrl + "/api/Data/SignUpUser?";
if (name === null)
throw new Error("The parameter 'name' cannot be null.");
else if (name !== undefined)
url_ += "name=" + encodeURIComponent("" + name) + "&";
if (email === null)
throw new Error("The parameter 'email' cannot be null.");
else if (email !== undefined)
url_ += "email=" + encodeURIComponent("" + email) + "&";
if (password === null)
throw new Error("The parameter 'password' cannot be null.");
else if (password !== undefined)
url_ += "password=" + encodeURIComponent("" + password) + "&";
url_ = url_.replace(/[?&]$/, "");
let options_: RequestInit = {
@ -527,7 +505,7 @@ export class DataClient {
});
}
protected processSignUpUser(response: Response): Promise<FileResponse> {
protected processSignUpUser(response: Response): Promise<FileResponse | null> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200 || status === 206) {
@ -546,7 +524,7 @@ export class DataClient {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<FileResponse>(null as any);
return Promise.resolve<FileResponse | null>(null as any);
}
}
@ -943,9 +921,9 @@ export class JtagClient {
/**
*
* @param address (optional)
* @param file (optional)
* @param file (optional)
*/
uploadBitstream(address: string | undefined, file: FileParameter | null | undefined): Promise<boolean> {
uploadBitstream(address: string | undefined, file: FileParameter | undefined): Promise<boolean> {
let url_ = this.baseUrl + "/api/Jtag/UploadBitstream?";
if (address === null)
throw new Error("The parameter 'address' cannot be null.");
@ -954,7 +932,9 @@ export class JtagClient {
url_ = url_.replace(/[?&]$/, "");
const content_ = new FormData();
if (file !== null && file !== undefined)
if (file === null || file === undefined)
throw new Error("The parameter 'file' cannot be null.");
else
content_.append("file", file.data, file.fileName ? file.fileName : "file");
let options_: RequestInit = {
@ -1519,10 +1499,10 @@ export class RemoteUpdateClient {
/**
*
* @param address (optional)
* @param goldenBitream (optional)
* @param bitstream1 (optional)
* @param bitstream2 (optional)
* @param bitstream3 (optional)
* @param goldenBitream (optional)
* @param bitstream1 (optional) 1
* @param bitstream2 (optional) 2
* @param bitstream3 (optional) 3
* @return
*/
uploadBitstreams(address: string | undefined, goldenBitream: FileParameter | null | undefined, bitstream1: FileParameter | null | undefined, bitstream2: FileParameter | null | undefined, bitstream3: FileParameter | null | undefined): Promise<boolean> {
@ -2156,7 +2136,7 @@ export class UDPClient {
/**
* IP地址接收的数据列表
* @param address (optional) IP地址
* @param taskID (optional)
* @param taskID (optional) ID
*/
getRecvDataArray(address: string | undefined, taskID: number | undefined): Promise<UDPData[]> {
let url_ = this.baseUrl + "/api/UDP/GetRecvDataArray?";
@ -2213,7 +2193,7 @@ export class UDPClient {
}
export class Exception implements IException {
message?: string;
message!: string;
innerException?: Exception | undefined;
source?: string | undefined;
stackTrace?: string | undefined;
@ -2254,7 +2234,7 @@ export class Exception implements IException {
}
export interface IException {
message?: string;
message: string;
innerException?: Exception | undefined;
source?: string | undefined;
stackTrace?: string | undefined;
@ -2262,7 +2242,9 @@ export interface IException {
/** 摄像头配置请求模型 */
export class CameraConfigRequest implements ICameraConfigRequest {
/** 摄像头地址 */
address!: string;
/** 摄像头端口 */
port!: number;
constructor(data?: ICameraConfigRequest) {
@ -2298,7 +2280,9 @@ export class CameraConfigRequest implements ICameraConfigRequest {
/** 摄像头配置请求模型 */
export interface ICameraConfigRequest {
/** 摄像头地址 */
address: string;
/** 摄像头端口 */
port: number;
}
@ -2330,6 +2314,7 @@ export interface ISystemException extends IException {
}
export class ArgumentException extends SystemException implements IArgumentException {
declare message: string;
paramName?: string | undefined;
constructor(data?: IArgumentException) {
@ -2361,22 +2346,22 @@ export class ArgumentException extends SystemException implements IArgumentExcep
}
export interface IArgumentException extends ISystemException {
message?: string;
message: string;
paramName?: string | undefined;
}
/** Package options which to send address to read or write */
export class SendAddrPackOptions implements ISendAddrPackOptions {
/** 突发类型 */
burstType?: BurstType;
burstType!: BurstType;
/** 任务ID */
commandID?: number;
commandID!: number;
/** 标识写入还是读取 */
isWrite?: boolean;
isWrite!: boolean;
/** 突发长度0是32bits255是32bits x 256 */
burstLength?: number;
burstLength!: number;
/** 目标地址 */
address?: number;
address!: number;
constructor(data?: ISendAddrPackOptions) {
if (data) {
@ -2418,15 +2403,15 @@ export class SendAddrPackOptions implements ISendAddrPackOptions {
/** Package options which to send address to read or write */
export interface ISendAddrPackOptions {
/** 突发类型 */
burstType?: BurstType;
burstType: BurstType;
/** 任务ID */
commandID?: number;
commandID: number;
/** 标识写入还是读取 */
isWrite?: boolean;
isWrite: boolean;
/** 突发长度0是32bits255是32bits x 256 */
burstLength?: number;
burstLength: number;
/** 目标地址 */
address?: number;
address: number;
}
/** Package Burst Type */
@ -2438,17 +2423,17 @@ export enum BurstType {
/** UDP接受数据包格式 */
export class UDPData implements IUDPData {
/** 接受到的时间 */
dateTime?: Date;
dateTime!: Date;
/** 发送来源的IP地址 */
address?: string;
address!: string;
/** 发送来源的端口号 */
port?: number;
port!: number;
/** 任务ID */
taskID?: number;
taskID!: number;
/** 接受到的数据 */
data?: string;
data!: string;
/** 是否被读取过 */
hasRead?: boolean;
hasRead!: boolean;
constructor(data?: IUDPData) {
if (data) {
@ -2492,17 +2477,17 @@ export class UDPData implements IUDPData {
/** UDP接受数据包格式 */
export interface IUDPData {
/** 接受到的时间 */
dateTime?: Date;
dateTime: Date;
/** 发送来源的IP地址 */
address?: string;
address: string;
/** 发送来源的端口号 */
port?: number;
port: number;
/** 任务ID */
taskID?: number;
taskID: number;
/** 接受到的数据 */
data?: string;
data: string;
/** 是否被读取过 */
hasRead?: boolean;
hasRead: boolean;
}
export interface FileParameter {
@ -2546,4 +2531,4 @@ function throwException(message: string, status: number, response: string, heade
throw result;
else
throw new ApiException(message, status, response, headers, null);
}
}