feat: add boss key
This commit is contained in:
1 parent
e7087bc4b1
commit
bc775d4fd6
10 files changed
+168
-6
No files matched your search
@@ -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,
|
||||
});
|
||||
@@ -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';
|
||||
}),
|
||||
});
|
||||
@@ -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
@@ -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
@@ -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
@@ -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'],
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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), // 超时秒数
|
||||
|
||||
Reference in new issue
Block a user