feat: 完善 Server 层并添加 CLI 和 Web 前端
Server 层增强: - 添加 Agent 适配层,支持动态加载 core 模块 - 实现 Token 认证机制,支持本地/远程模式 - WebSocket 集成 Agent 实时对话 CLI 模块 (packages/cli): - serve 命令启动 HTTP Server - attach 命令连接远程 Server - API Client 封装 Web 前端 (packages/web): - React 18 + Vite + Tailwind CSS - 会话管理侧边栏 - WebSocket 实时聊天界面 - 流式消息显示
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Auth Module
|
||||
*
|
||||
* 导出认证相关功能
|
||||
*/
|
||||
|
||||
export {
|
||||
// 类型
|
||||
type AuthConfig,
|
||||
type AuthContext,
|
||||
// Token 操作
|
||||
generateToken,
|
||||
maskToken,
|
||||
validateToken,
|
||||
extractToken,
|
||||
// 配置
|
||||
initAuth,
|
||||
getAuthConfig,
|
||||
addToken,
|
||||
removeToken,
|
||||
setAuthEnabled,
|
||||
// 中间件
|
||||
authMiddleware,
|
||||
getAuthContext,
|
||||
} from './token.js';
|
||||
@@ -0,0 +1,263 @@
|
||||
/**
|
||||
* Token Authentication
|
||||
*
|
||||
* 简单的 Token 认证机制
|
||||
* - 本地模式 (127.0.0.1): 无需认证
|
||||
* - 远程模式: 需要 Bearer Token
|
||||
*/
|
||||
|
||||
import { createMiddleware } from 'hono/factory';
|
||||
import type { Context, Next } from 'hono';
|
||||
|
||||
// ============================================================================
|
||||
// 类型定义
|
||||
// ============================================================================
|
||||
|
||||
export interface AuthConfig {
|
||||
/** 是否启用认证 (远程模式下自动启用) */
|
||||
enabled: boolean;
|
||||
/** 有效的 token 列表 */
|
||||
tokens: string[];
|
||||
/** 跳过认证的路径 */
|
||||
skipPaths: string[];
|
||||
}
|
||||
|
||||
export interface AuthContext {
|
||||
/** 是否已认证 */
|
||||
authenticated: boolean;
|
||||
/** 使用的 token (脱敏) */
|
||||
tokenHint?: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 默认配置
|
||||
// ============================================================================
|
||||
|
||||
const defaultConfig: AuthConfig = {
|
||||
enabled: false,
|
||||
tokens: [],
|
||||
skipPaths: ['/health', '/api/health'],
|
||||
};
|
||||
|
||||
// 当前配置
|
||||
let authConfig: AuthConfig = { ...defaultConfig };
|
||||
|
||||
// ============================================================================
|
||||
// Token 生成
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* 生成随机 token
|
||||
*/
|
||||
export function generateToken(length: number = 32): string {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let token = '';
|
||||
const randomValues = new Uint8Array(length);
|
||||
crypto.getRandomValues(randomValues);
|
||||
for (let i = 0; i < length; i++) {
|
||||
token += chars[randomValues[i] % chars.length];
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* 脱敏 token (只显示前4位和后4位)
|
||||
*/
|
||||
export function maskToken(token: string): string {
|
||||
if (token.length <= 8) {
|
||||
return '****';
|
||||
}
|
||||
return `${token.slice(0, 4)}...${token.slice(-4)}`;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 配置管理
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* 初始化认证配置
|
||||
*/
|
||||
export function initAuth(config: Partial<AuthConfig> = {}): AuthConfig {
|
||||
authConfig = {
|
||||
...defaultConfig,
|
||||
...config,
|
||||
};
|
||||
return authConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取认证配置
|
||||
*/
|
||||
export function getAuthConfig(): AuthConfig {
|
||||
return { ...authConfig };
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加 token
|
||||
*/
|
||||
export function addToken(token: string): void {
|
||||
if (!authConfig.tokens.includes(token)) {
|
||||
authConfig.tokens.push(token);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除 token
|
||||
*/
|
||||
export function removeToken(token: string): void {
|
||||
const index = authConfig.tokens.indexOf(token);
|
||||
if (index !== -1) {
|
||||
authConfig.tokens.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用/禁用认证
|
||||
*/
|
||||
export function setAuthEnabled(enabled: boolean): void {
|
||||
authConfig.enabled = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为本地请求
|
||||
*/
|
||||
function isLocalRequest(c: Context): boolean {
|
||||
// 获取客户端 IP
|
||||
const forwarded = c.req.header('x-forwarded-for');
|
||||
const realIp = c.req.header('x-real-ip');
|
||||
|
||||
// 如果有代理头,检查原始 IP
|
||||
if (forwarded) {
|
||||
const clientIp = forwarded.split(',')[0].trim();
|
||||
return isLocalIP(clientIp);
|
||||
}
|
||||
|
||||
if (realIp) {
|
||||
return isLocalIP(realIp);
|
||||
}
|
||||
|
||||
// Bun/Hono 环境下获取连接 IP
|
||||
// 这需要 server 层传递,目前假设本地
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为本地 IP
|
||||
*/
|
||||
function isLocalIP(ip: string): boolean {
|
||||
return (
|
||||
ip === '127.0.0.1' ||
|
||||
ip === '::1' ||
|
||||
ip === 'localhost' ||
|
||||
ip.startsWith('192.168.') ||
|
||||
ip.startsWith('10.') ||
|
||||
ip.startsWith('172.16.') ||
|
||||
ip.startsWith('172.17.') ||
|
||||
ip.startsWith('172.18.') ||
|
||||
ip.startsWith('172.19.') ||
|
||||
ip.startsWith('172.2') ||
|
||||
ip.startsWith('172.30.') ||
|
||||
ip.startsWith('172.31.')
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 认证验证
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* 验证 token
|
||||
*/
|
||||
export function validateToken(token: string): boolean {
|
||||
return authConfig.tokens.includes(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求中提取 token
|
||||
*/
|
||||
export function extractToken(c: Context): string | null {
|
||||
// 1. 从 Authorization header 提取
|
||||
const authHeader = c.req.header('Authorization');
|
||||
if (authHeader?.startsWith('Bearer ')) {
|
||||
return authHeader.slice(7);
|
||||
}
|
||||
|
||||
// 2. 从 query parameter 提取 (用于 WebSocket)
|
||||
const queryToken = c.req.query('token');
|
||||
if (queryToken) {
|
||||
return queryToken;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Hono 中间件
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* 认证中间件
|
||||
*/
|
||||
export const authMiddleware = createMiddleware(async (c: Context, next: Next) => {
|
||||
// 检查是否跳过认证
|
||||
const path = c.req.path;
|
||||
if (authConfig.skipPaths.some((p) => path.startsWith(p))) {
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果认证未启用,跳过
|
||||
if (!authConfig.enabled) {
|
||||
c.set('auth', { authenticated: true } as AuthContext);
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
|
||||
// 本地请求跳过认证
|
||||
if (isLocalRequest(c)) {
|
||||
c.set('auth', { authenticated: true } as AuthContext);
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
|
||||
// 提取 token
|
||||
const token = extractToken(c);
|
||||
|
||||
if (!token) {
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Authentication required',
|
||||
message: 'Please provide a valid token in the Authorization header',
|
||||
},
|
||||
401
|
||||
);
|
||||
}
|
||||
|
||||
// 验证 token
|
||||
if (!validateToken(token)) {
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Invalid token',
|
||||
message: 'The provided token is not valid',
|
||||
},
|
||||
401
|
||||
);
|
||||
}
|
||||
|
||||
// 设置认证上下文
|
||||
c.set('auth', {
|
||||
authenticated: true,
|
||||
tokenHint: maskToken(token),
|
||||
} as AuthContext);
|
||||
|
||||
await next();
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取当前请求的认证上下文
|
||||
*/
|
||||
export function getAuthContext(c: Context): AuthContext {
|
||||
return c.get('auth') || { authenticated: false };
|
||||
}
|
||||
Reference in New Issue
Block a user