Skip to content

Commit 4163209

Browse files
author
iammm0
committed
feat: TUI 模型配置、启动构建与 LLM 厂商注册表
- 后端:新增 llm-provider-registry,listProviders 返回全量厂商;createLLM 使用注册表默认 Base URL - TUI:解包 Nest 响应信封;SetApiKey 使用 apiKey/baseUrl;厂商列表 merge 兜底与类型拆分 - 启动:根目录 prestart 与 start:stack 每次构建后端;TUI pretui 后运行 dist/cli.js;无 TTY 时 npm run tui - chore:start:latest 避免与 start:stack 重复编译;根目录 /dist/ 加入 .gitignore - docs:LLM_PROVIDERS 说明以 llm-provider-registry 为准 Made-with: Cursor
1 parent bd12195 commit 4163209

131 files changed

Lines changed: 1389 additions & 879 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ yarn-error.log*
77
# TypeScript 构建产物
88
server/dist/
99
terminal-ui/dist/
10+
/dist/
1011
desktop/dist/
1112
*.tsbuildinfo
1213

docs/LLM_PROVIDERS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,4 +121,4 @@ https://<resource-name>.openai.azure.com/openai/v1
121121
## 注意事项
122122

123123
- 厂商侧模型与 Base URL 可能会变化,文档只保证与当前代码的配置逻辑一致
124-
- 如果要看默认模型建议值,请以 `server/src/modules/system/system.service.ts` 中的 `PROVIDER_REGISTRY` 为准
124+
- 厂商清单与默认 OpenAI 兼容网关以 `server/src/modules/system/llm-provider-registry.ts` 中的 `LLM_PROVIDER_REGISTRY` 为准`listProviders` 与此同步)

package-lock.json

Lines changed: 0 additions & 67 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"clean": "node -e \"require('node:fs').rmSync('server/dist',{recursive:true,force:true})\"",
2222
"build": "tsc -p server/tsconfig.json",
2323
"dev": "tsx watch server/src/main.ts",
24+
"prestart": "npm run build",
2425
"start": "node server/dist/main.js",
2526
"start:stack": "node scripts/start_stack.js",
2627
"start:latest": "node scripts/start_latest.js",

scripts/start_latest.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -158,18 +158,17 @@ async function main() {
158158
log('Skipping npm install due to --no-install');
159159
}
160160

161-
log('Building backend...');
162-
await runNpm(['run', 'build']);
163-
164161
log(`Stopping old backend on port ${DEFAULT_PORT} (if any)...`);
165162
await stopListeningProcessOnPort(DEFAULT_PORT);
166163

167164
if (PREPARE_ONLY) {
165+
log('Building backend...');
166+
await runNpm(['run', 'build']);
168167
log('Prepare-only completed.');
169168
return;
170169
}
171170

172-
log('Launching full stack...');
171+
log('Launching full stack (start:stack will build backend then start TUI)...');
173172
await runNpm(['run', 'start:stack']);
174173
}
175174

scripts/start_stack.js

Lines changed: 68 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
'use strict';
44

55
const fs = require('node:fs');
6+
const os = require('node:os');
67
const path = require('node:path');
78
const { spawn } = require('node:child_process');
89

@@ -73,9 +74,9 @@ function runNpm(args, options = {}) {
7374
});
7475
}
7576

76-
async function ensureBackendBuild() {
77-
if (fs.existsSync(BACKEND_ENTRY)) return;
78-
log('Backend dist not found, building TypeScript backend...');
77+
/** 每次启动均编译后端,避免沿用旧的 server/dist */
78+
async function buildBackendLatest() {
79+
log('Building TypeScript backend (latest sources)...');
7980
await runNpm(['run', 'build']);
8081
}
8182

@@ -133,12 +134,34 @@ async function stopProcess(proc) {
133134
}
134135
}
135136

137+
/**
138+
* IDE 集成终端通常无真实 TTY,Ink 无法在同一进程内运行。在 Windows 上通过「新控制台窗口」启动 TUI。
139+
*/
140+
function launchTuiInNewWindowsConsole(baseUrl) {
141+
const batPath = path.join(os.tmpdir(), `secbot-tui-${process.pid}-${Date.now()}.bat`);
142+
const safeUrl = baseUrl.replace(/"/g, '').replace(/\r?\n/g, '');
143+
const safeDir = TUI_DIR.replace(/"/g, '""');
144+
const body = [
145+
'@echo off',
146+
`set "SECBOT_API_URL=${safeUrl}"`,
147+
`cd /d "${safeDir}"`,
148+
'call npm.cmd run tui',
149+
].join('\r\n');
150+
fs.writeFileSync(batPath, `${body}\r\n`, 'utf8');
151+
152+
const child = spawn('cmd.exe', ['/c', 'start', 'Secbot TUI', batPath], {
153+
cwd: ROOT,
154+
stdio: 'ignore',
155+
windowsHide: false,
156+
detached: true,
157+
});
158+
child.unref();
159+
}
160+
136161
async function main() {
137-
if (!process.stdin.isTTY || !process.stdout.isTTY) {
138-
log('Current terminal has no real TTY; open CMD/PowerShell/Windows Terminal to run TUI.');
139-
}
162+
const hasTTY = Boolean(process.stdin.isTTY && process.stdout.isTTY);
140163

141-
await ensureBackendBuild();
164+
await buildBackendLatest();
142165
await ensureTuiDeps();
143166

144167
const port = String(process.env.PORT || 8000);
@@ -174,25 +197,43 @@ async function main() {
174197
log(`Using existing backend at ${baseUrl}`);
175198
}
176199

177-
log('Starting terminal TUI...');
178-
const tuiNpm = npmInvocation(['run', 'tui']);
179-
const tuiProc = spawn(tuiNpm.cmd, tuiNpm.argv, {
180-
cwd: TUI_DIR,
181-
env: tuiEnv,
182-
stdio: 'inherit',
183-
shell: tuiNpm.shell,
184-
windowsHide: true,
185-
});
186-
187200
const shutdown = async () => {
188201
if (startedBackend) {
189202
await stopProcess(backendProc);
190203
}
191204
};
192205

206+
let tuiProc = null;
207+
let tuiInSameTerminal = true;
208+
209+
if (!hasTTY && process.platform === 'win32') {
210+
log('当前无真实 TTY(常见于 Cursor/VS Code 集成终端),将在新控制台窗口中启动 TUI。');
211+
log('Starting terminal TUI in a new window...');
212+
launchTuiInNewWindowsConsole(baseUrl);
213+
log('TUI 已在新窗口启动;本终端将保持后端运行,按 Ctrl+C 可停止后端。');
214+
tuiInSameTerminal = false;
215+
} else if (!hasTTY) {
216+
log('当前终端无真实 TTY,无法在此进程内启动 Ink TUI。');
217+
log('请在系统终端中执行:npm run start:stack,或 Windows 下双击 scripts\\start-cli.bat');
218+
await shutdown();
219+
process.exit(1);
220+
} else {
221+
log('Starting terminal TUI...');
222+
const tuiNpm = npmInvocation(['run', 'tui']);
223+
tuiProc = spawn(tuiNpm.cmd, tuiNpm.argv, {
224+
cwd: TUI_DIR,
225+
env: tuiEnv,
226+
stdio: 'inherit',
227+
shell: tuiNpm.shell,
228+
windowsHide: true,
229+
});
230+
}
231+
193232
process.on('SIGINT', async () => {
194233
try {
195-
if (tuiProc.exitCode === null) tuiProc.kill('SIGINT');
234+
if (tuiInSameTerminal && tuiProc && tuiProc.exitCode === null) {
235+
tuiProc.kill('SIGINT');
236+
}
196237
await shutdown();
197238
} finally {
198239
process.exit(130);
@@ -201,13 +242,21 @@ async function main() {
201242

202243
process.on('SIGTERM', async () => {
203244
try {
204-
if (tuiProc.exitCode === null) tuiProc.kill('SIGTERM');
245+
if (tuiInSameTerminal && tuiProc && tuiProc.exitCode === null) {
246+
tuiProc.kill('SIGTERM');
247+
}
205248
await shutdown();
206249
} finally {
207250
process.exit(143);
208251
}
209252
});
210253

254+
if (!tuiInSameTerminal) {
255+
// 后端子进程已启动时,本进程需保持运行直至用户 Ctrl+C(见 SIGINT);否则仅新开 TUI 窗口时也可仅靠子进程存活
256+
await new Promise(() => {});
257+
return;
258+
}
259+
211260
const tuiCode = await new Promise((resolve, reject) => {
212261
tuiProc.once('error', reject);
213262
tuiProc.once('close', (code) => resolve(code ?? 0));

server/src/app.module.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,5 +35,3 @@ import { VulnDbModule } from './modules/vuln-db/vuln-db.module';
3535
],
3636
})
3737
export class AppModule {}
38-
39-

server/src/common/event-bus.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,7 @@ export class EventBus {
8484
}
8585
}
8686

87-
emitSimple(
88-
type: EventType,
89-
data: Record<string, unknown> = {},
90-
iteration = 0,
91-
): void {
87+
emitSimple(type: EventType, data: Record<string, unknown> = {}, iteration = 0): void {
9288
this.emit({
9389
type,
9490
data,
Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,4 @@
1-
import {
2-
ArgumentsHost,
3-
Catch,
4-
ExceptionFilter,
5-
HttpException,
6-
HttpStatus,
7-
} from '@nestjs/common';
1+
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus } from '@nestjs/common';
82
import { Request, Response } from 'express';
93

104
@Catch()
@@ -15,18 +9,11 @@ export class HttpExceptionFilter implements ExceptionFilter {
159
const request = ctx.getRequest<Request>();
1610

1711
const isHttpException = exception instanceof HttpException;
18-
const status = isHttpException
19-
? exception.getStatus()
20-
: HttpStatus.INTERNAL_SERVER_ERROR;
12+
const status = isHttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR;
2113

22-
const message = isHttpException
23-
? exception.getResponse()
24-
: 'Internal server error';
14+
const message = isHttpException ? exception.getResponse() : 'Internal server error';
2515

26-
const body =
27-
typeof message === 'string'
28-
? { message }
29-
: (message as Record<string, unknown>);
16+
const body = typeof message === 'string' ? { message } : (message as Record<string, unknown>);
3017

3118
response.status(status).json({
3219
success: false,
@@ -37,4 +24,3 @@ export class HttpExceptionFilter implements ExceptionFilter {
3724
});
3825
}
3926
}
40-

0 commit comments

Comments
 (0)