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 { os } from '@orpc/server';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { isProcessRunning, closeAntigravity, startAntigravity } from './process/handler';
|
import { isProcessRunning, closeAntigravity, startAntigravity } from './process/handler';
|
||||||
|
import { systemHandler } from './system/handler';
|
||||||
|
|
||||||
// Log middleware setup
|
// Log middleware setup
|
||||||
const logMiddleware = os.middleware(async (opts: any) => {
|
const logMiddleware = os.middleware(async (opts: any) => {
|
||||||
@@ -51,4 +52,5 @@ export const router = os.use(logMiddleware).router({
|
|||||||
cloud: cloudRouter,
|
cloud: cloudRouter,
|
||||||
config: configRouter,
|
config: configRouter,
|
||||||
gateway: gatewayRouter,
|
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',
|
default: 'Default',
|
||||||
restoreDefaults: 'Restore Defaults',
|
restoreDefaults: 'Restore Defaults',
|
||||||
},
|
},
|
||||||
|
|
||||||
examples: {
|
examples: {
|
||||||
title: 'Usage Examples',
|
title: 'Usage Examples',
|
||||||
description: 'Example commands to call the local API proxy.',
|
description: 'Example commands to call the local API proxy.',
|
||||||
@@ -378,6 +379,7 @@ i18n
|
|||||||
default: '默认',
|
default: '默认',
|
||||||
restoreDefaults: '恢复默认配置',
|
restoreDefaults: '恢复默认配置',
|
||||||
},
|
},
|
||||||
|
|
||||||
examples: {
|
examples: {
|
||||||
title: '使用示例',
|
title: '使用示例',
|
||||||
description: '调用本地 API 代理的示例命令。',
|
description: '调用本地 API 代理的示例命令。',
|
||||||
@@ -544,6 +546,10 @@ i18n
|
|||||||
api_key: 'API 密钥',
|
api_key: 'API 密钥',
|
||||||
auto_start: '随应用启动',
|
auto_start: '随应用启动',
|
||||||
auto_start_desc: '应用启动时自动启动代理服务',
|
auto_start_desc: '应用启动时自动启动代理服务',
|
||||||
|
local_access: '局域网访问:',
|
||||||
|
no_token_warning: '⚠️ 未设置 API 密钥。服务对公网开放!',
|
||||||
|
show_key: '显示密钥',
|
||||||
|
hide_key: '隐藏密钥',
|
||||||
},
|
},
|
||||||
mapping: {
|
mapping: {
|
||||||
title: '模型映射',
|
title: '模型映射',
|
||||||
|
|||||||
+4
-1
@@ -222,7 +222,10 @@ app
|
|||||||
const config = ConfigManager.loadConfig();
|
const config = ConfigManager.loadConfig();
|
||||||
if (config.proxy?.auto_start) {
|
if (config.proxy?.auto_start) {
|
||||||
const port = config.proxy?.port || 8045;
|
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}`);
|
logger.info(`NestJS Proxy: Auto-started on port ${port}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+57
-2
@@ -4,6 +4,8 @@
|
|||||||
*/
|
*/
|
||||||
import { createFileRoute } from '@tanstack/react-router';
|
import { createFileRoute } from '@tanstack/react-router';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { ipc } from '@/ipc/manager';
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useAppConfig } from '@/hooks/useAppConfig';
|
import { useAppConfig } from '@/hooks/useAppConfig';
|
||||||
import { ProxyConfig } from '@/types/config';
|
import { ProxyConfig } from '@/types/config';
|
||||||
@@ -29,6 +31,8 @@ import {
|
|||||||
BrainCircuit,
|
BrainCircuit,
|
||||||
Code,
|
Code,
|
||||||
Terminal,
|
Terminal,
|
||||||
|
Eye,
|
||||||
|
EyeOff,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -43,9 +47,19 @@ function ProxyPage() {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { config, isLoading, saveConfig } = useAppConfig();
|
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
|
// Local state for proxyConfig editing
|
||||||
const [proxyConfig, setProxyConfig] = useState<ProxyConfig | undefined>(undefined);
|
const [proxyConfig, setProxyConfig] = useState<ProxyConfig | undefined>(undefined);
|
||||||
const [isRegenerateDialogOpen, setIsRegenerateDialogOpen] = useState(false);
|
const [isRegenerateDialogOpen, setIsRegenerateDialogOpen] = useState(false);
|
||||||
|
const [showKey, setShowKey] = useState(false);
|
||||||
|
|
||||||
// Sync config.proxy to local state when loaded
|
// Sync config.proxy to local state when loaded
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -153,6 +167,27 @@ print(response.choices[0].message.content)`;
|
|||||||
<p className="text-muted-foreground mt-1">
|
<p className="text-muted-foreground mt-1">
|
||||||
{t('proxy.description', 'Manage the local API proxy service.')}
|
{t('proxy.description', 'Manage the local API proxy service.')}
|
||||||
</p>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* Service Control Card */}
|
{/* Service Control Card */}
|
||||||
@@ -235,10 +270,30 @@ print(response.choices[0].message.content)`;
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>{t('proxy.config.api_key', 'API Key')}</Label>
|
<Label>{t('proxy.config.api_key', 'API Key')}</Label>
|
||||||
<div className="flex gap-2">
|
<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
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="icon"
|
||||||
onClick={() => navigator.clipboard.writeText(proxyConfig.api_key || '')}
|
onClick={() => navigator.clipboard.writeText(proxyConfig.api_key || '')}
|
||||||
>
|
>
|
||||||
<Copy size={14} className="mr-1" />
|
<Copy size={14} className="mr-1" />
|
||||||
|
|||||||
+7
-1
@@ -4,15 +4,21 @@ import { AppModule } from './app.module';
|
|||||||
import { logger } from '../utils/logger';
|
import { logger } from '../utils/logger';
|
||||||
import { TokenManagerService } from './modules/proxy/token-manager.service';
|
import { TokenManagerService } from './modules/proxy/token-manager.service';
|
||||||
|
|
||||||
|
import { ProxyConfig } from '../types/config';
|
||||||
|
import { setServerConfig } from './server-config';
|
||||||
|
|
||||||
let app: NestFastifyApplication | null = null;
|
let app: NestFastifyApplication | null = null;
|
||||||
let currentPort: number = 0;
|
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) {
|
if (app) {
|
||||||
logger.info('NestJS server already running.');
|
logger.info('NestJS server already running.');
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setServerConfig(config);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
app = await NestFactory.create<NestFastifyApplication>(AppModule, new FastifyAdapter(), {
|
app = await NestFactory.create<NestFastifyApplication>(AppModule, new FastifyAdapter(), {
|
||||||
logger: ['error', 'warn', 'log'],
|
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 { FastifyReply } from 'fastify';
|
||||||
import { ProxyService } from './proxy.service';
|
import { ProxyService } from './proxy.service';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { OpenAIChatRequest, AnthropicChatRequest } from './interfaces/request-interfaces';
|
import { OpenAIChatRequest, AnthropicChatRequest } from './interfaces/request-interfaces';
|
||||||
|
import { ProxyGuard } from './proxy.guard';
|
||||||
|
|
||||||
@Controller('v1')
|
@Controller('v1')
|
||||||
|
@UseGuards(ProxyGuard)
|
||||||
export class ProxyController {
|
export class ProxyController {
|
||||||
constructor(private readonly proxyService: ProxyService) {}
|
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({
|
export const ProxyConfigSchema = z.object({
|
||||||
enabled: z.boolean(), // 是否启用
|
enabled: z.boolean(), // 是否启用
|
||||||
port: z.number(), // 监听端口
|
port: z.number(), // 监听端口
|
||||||
api_key: z.string(), // API 密钥
|
api_key: z.string(), // API 密钥 (Also used for Admin Mode auth)
|
||||||
auto_start: z.boolean(), // 是否自动启动
|
auto_start: z.boolean(), // 是否自动启动
|
||||||
anthropic_mapping: z.record(z.string(), z.string()), // 映射表
|
anthropic_mapping: z.record(z.string(), z.string()), // 映射表
|
||||||
request_timeout: z.number().default(120), // 超时秒数
|
request_timeout: z.number().default(120), // 超时秒数
|
||||||
|
|||||||
Reference in new issue
Block a user