feat: add boss key

This commit is contained in:
Draculabo authored and vin committed 2026-01-10 01:07:09 +08:00
1 parent e7087bc4b1
commit bc775d4fd6
10 files changed
+168 -6

No files matched your search

+2
View File
@@ -10,6 +10,7 @@ import { gatewayRouter } from './gateway/router';
import { os } from '@orpc/server';
import { z } from 'zod';
import { isProcessRunning, closeAntigravity, startAntigravity } from './process/handler';
import { systemHandler } from './system/handler';
// Log middleware setup
const logMiddleware = os.middleware(async (opts: any) => {
@@ -51,4 +52,5 @@ export const router = os.use(logMiddleware).router({
cloud: cloudRouter,
config: configRouter,
gateway: gatewayRouter,
system: systemHandler,
});
+20
View File
@@ -0,0 +1,20 @@
import { os } from '@orpc/server';
import { z } from 'zod';
import { networkInterfaces } from 'os';
export const systemHandler = os.route({
get_local_ip: os.handler(async () => {
const nets = networkInterfaces();
const results: string[] = [];
for (const name of Object.keys(nets)) {
for (const net of nets[name] || []) {
// Skip over non-IPv4 and internal (i.e. 127.0.0.1) addresses
if (net.family === 'IPv4' && !net.internal) {
results.push(net.address);
}
}
}
return results.length > 0 ? results[0] : '127.0.0.1';
}),
});
+6
View File
@@ -105,6 +105,7 @@ i18n
default: 'Default',
restoreDefaults: 'Restore Defaults',
},
examples: {
title: 'Usage Examples',
description: 'Example commands to call the local API proxy.',
@@ -378,6 +379,7 @@ i18n
default: '默认',
restoreDefaults: '恢复默认配置',
},
examples: {
title: '使用示例',
description: '调用本地 API 代理的示例命令。',
@@ -544,6 +546,10 @@ i18n
api_key: 'API 密钥',
auto_start: '随应用启动',
auto_start_desc: '应用启动时自动启动代理服务',
local_access: '局域网访问:',
no_token_warning: '⚠️ 未设置 API 密钥。服务对公网开放!',
show_key: '显示密钥',
hide_key: '隐藏密钥',
},
mapping: {
title: '模型映射',
+4 -1
View File
@@ -222,7 +222,10 @@ app
const config = ConfigManager.loadConfig();
if (config.proxy?.auto_start) {
const port = config.proxy?.port || 8045;
await bootstrapNestServer(port);
// Default to a valid ProxyConfig object if null, although loadConfig ensures defaults
if (config.proxy) {
await bootstrapNestServer(config.proxy);
}
logger.info(`NestJS Proxy: Auto-started on port ${port}`);
}
+57 -2
View File
@@ -4,6 +4,8 @@
*/
import { createFileRoute } from '@tanstack/react-router';
import { useTranslation } from 'react-i18next';
import { useQuery } from '@tanstack/react-query';
import { ipc } from '@/ipc/manager';
import { useState, useEffect } from 'react';
import { useAppConfig } from '@/hooks/useAppConfig';
import { ProxyConfig } from '@/types/config';
@@ -29,6 +31,8 @@ import {
BrainCircuit,
Code,
Terminal,
Eye,
EyeOff,
} from 'lucide-react';
import {
Dialog,
@@ -43,9 +47,19 @@ function ProxyPage() {
const { t } = useTranslation();
const { config, isLoading, saveConfig } = useAppConfig();
const { data: localIp } = useQuery({
queryKey: ['system', 'localIp'],
queryFn: async () => {
// IPC might not be ready on first render, handled by query retry or lazy loading implication
// But ipc is imported statically now.
return (await ipc.client.system.get_local_ip()) as string;
},
});
// Local state for proxyConfig editing
const [proxyConfig, setProxyConfig] = useState<ProxyConfig | undefined>(undefined);
const [isRegenerateDialogOpen, setIsRegenerateDialogOpen] = useState(false);
const [showKey, setShowKey] = useState(false);
// Sync config.proxy to local state when loaded
useEffect(() => {
@@ -153,6 +167,27 @@ print(response.choices[0].message.content)`;
<p className="text-muted-foreground mt-1">
{t('proxy.description', 'Manage the local API proxy service.')}
</p>
{/* Local Access Info Banner */}
{/* Local Access Info Banner */}
{proxyConfig?.enabled && (
<div className="mt-4 flex flex-col gap-2 rounded-md border border-blue-100 bg-blue-50 p-3 text-sm text-blue-900 dark:border-blue-900/50 dark:bg-blue-950/30 dark:text-blue-200">
<div className="flex items-center gap-2">
<div className="font-semibold">{t('proxy.config.local_access', 'Local Access:')}</div>
<code className="rounded bg-blue-100 px-1.5 py-0.5 font-mono select-all dark:bg-blue-900/50">
http://{localIp || 'localhost'}:{proxyConfig.port}/v1
</code>
</div>
{!proxyConfig.api_key && (
<div className="flex items-center gap-2 text-xs font-medium text-amber-600 dark:text-amber-400">
{t(
'proxy.config.no_token_warning',
'⚠️ No API Key set. Service is open to the public network!',
)}
</div>
)}
</div>
)}
</div>
{/* Service Control Card */}
@@ -235,10 +270,30 @@ print(response.choices[0].message.content)`;
<div className="space-y-2">
<Label>{t('proxy.config.api_key', 'API Key')}</Label>
<div className="flex gap-2">
<Input value={proxyConfig.api_key || ''} readOnly className="font-mono text-sm" />
<div className="relative flex-1">
<Input
value={proxyConfig.api_key || ''}
readOnly
type={showKey ? 'text' : 'password'}
className="pr-10 font-mono text-sm"
/>
<Button
variant="ghost"
size="icon"
className="absolute top-0 right-0 h-full px-3 py-2 hover:bg-transparent"
onClick={() => setShowKey(!showKey)}
title={showKey ? t('proxy.config.hide_key') : t('proxy.config.show_key')}
>
{showKey ? (
<EyeOff className="text-muted-foreground h-4 w-4" />
) : (
<Eye className="text-muted-foreground h-4 w-4" />
)}
</Button>
</div>
<Button
variant="outline"
size="sm"
size="icon"
onClick={() => navigator.clipboard.writeText(proxyConfig.api_key || '')}
>
<Copy size={14} className="mr-1" />
+7 -1
View File
@@ -4,15 +4,21 @@ import { AppModule } from './app.module';
import { logger } from '../utils/logger';
import { TokenManagerService } from './modules/proxy/token-manager.service';
import { ProxyConfig } from '../types/config';
import { setServerConfig } from './server-config';
let app: NestFastifyApplication | null = null;
let currentPort: number = 0;
export async function bootstrapNestServer(port: number = 8045): Promise<boolean> {
export async function bootstrapNestServer(config: ProxyConfig): Promise<boolean> {
const port = config.port || 8045;
if (app) {
logger.info('NestJS server already running.');
return true;
}
setServerConfig(config);
try {
app = await NestFactory.create<NestFastifyApplication>(AppModule, new FastifyAdapter(), {
logger: ['error', 'warn', 'log'],
+3 -1
View File
@@ -1,10 +1,12 @@
import { Controller, Get, Post, Body, Res, HttpStatus } from '@nestjs/common';
import { Controller, Get, Post, Body, Res, HttpStatus, UseGuards } from '@nestjs/common';
import { FastifyReply } from 'fastify';
import { ProxyService } from './proxy.service';
import { Observable } from 'rxjs';
import { OpenAIChatRequest, AnthropicChatRequest } from './interfaces/request-interfaces';
import { ProxyGuard } from './proxy.guard';
@Controller('v1')
@UseGuards(ProxyGuard)
export class ProxyController {
constructor(private readonly proxyService: ProxyService) {}
+57
View File
@@ -0,0 +1,57 @@
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
Logger,
} from '@nestjs/common';
import { getServerConfig } from '../../server-config';
@Injectable()
export class ProxyGuard implements CanActivate {
private readonly logger = new Logger(ProxyGuard.name);
canActivate(context: ExecutionContext): boolean {
const config = getServerConfig();
// 1. Check for API Key in config
const apiKey = config?.api_key;
// 2. Bypass if no api_key set (Open Mode) or config missing
if (!apiKey || apiKey.trim() === '') {
return true;
}
const request = context.switchToHttp().getRequest();
const headers = request.headers;
// 3. Extract Token from Headers
let clientToken: string | undefined;
// A. standard 'authorization: Bearer <token>'
if (headers['authorization']) {
const parts = headers['authorization'].split(' ');
if (parts.length === 2 && parts[0].toLowerCase() === 'bearer') {
clientToken = parts[1];
}
}
// B. 'x-api-key' (Anthropic)
if (!clientToken && headers['x-api-key']) {
clientToken = headers['x-api-key'] as string;
}
// C. 'x-goog-api-key' (Gemini)
if (!clientToken && headers['x-goog-api-key']) {
clientToken = headers['x-goog-api-key'] as string;
}
// 4. Validate
if (clientToken === apiKey) {
return true;
}
this.logger.warn(`Blocked unauthorized access from ${request.ip}`);
throw new UnauthorizedException('Invalid API Key');
}
}
+11
View File
@@ -0,0 +1,11 @@
import { ProxyConfig } from '../types/config';
let serverConfig: ProxyConfig | null = null;
export function setServerConfig(config: ProxyConfig) {
serverConfig = config;
}
export function getServerConfig(): ProxyConfig | null {
return serverConfig;
}
+1 -1
View File
@@ -8,7 +8,7 @@ export const UpstreamProxyConfigSchema = z.object({
export const ProxyConfigSchema = z.object({
enabled: z.boolean(), // 是否启用
port: z.number(), // 监听端口
api_key: z.string(), // API 密钥
api_key: z.string(), // API 密钥 (Also used for Admin Mode auth)
auto_start: z.boolean(), // 是否自动启动
anthropic_mapping: z.record(z.string(), z.string()), // 映射表
request_timeout: z.number().default(120), // 超时秒数