feat: 重构为 Monorepo 架构并实现 HTTP Server

架构变更:
- 采用 pnpm workspaces 实现 Monorepo 结构
- 将现有代码迁移到 packages/core
- 新增 packages/server HTTP 服务层

Server 功能:
- REST API: 会话管理、工具管理、配置管理
- WebSocket: 实时双向通信支持
- SSE: 服务端事件推送
- Hono + Bun 作为运行时

API 端点:
- GET/POST /api/sessions - 会话 CRUD
- GET/POST /api/sessions/:id/messages - 消息管理
- GET /api/sessions/:id/events - SSE 事件流
- WS /api/ws/:sessionId - WebSocket 连接
- GET/POST /api/tools - 工具管理
- GET/PUT /api/config - 配置管理
This commit is contained in:
2025-12-12 10:42:20 +08:00
parent 59dbed926e
commit 5e32375f0e
301 changed files with 3281 additions and 43 deletions
@@ -0,0 +1,305 @@
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import type {
BashPermissionConfig,
PermissionContext,
PermissionCheckResult,
PermissionDecision,
PermissionRule,
} from '../types.js';
import type { PermissionChecker } from './base.js';
import { matchRulesAsync } from '../wildcard.js';
import { parseBashCommand } from '../bash-parser.js';
const CONFIG_DIR = path.join(os.homedir(), '.ai-terminal-assistant');
const PERMISSION_FILE = path.join(CONFIG_DIR, 'permissions.json');
// 默认权限配置
const DEFAULT_CONFIG: BashPermissionConfig = {
rules: [
// 默认允许的安全命令
{ pattern: 'ls *', action: 'allow' },
{ pattern: 'cat *', action: 'allow' },
{ pattern: 'head *', action: 'allow' },
{ pattern: 'tail *', action: 'allow' },
{ pattern: 'grep *', action: 'allow' },
{ pattern: 'find *', action: 'allow' },
{ pattern: 'echo *', action: 'allow' },
{ pattern: 'pwd', action: 'allow' },
{ pattern: 'which *', action: 'allow' },
{ pattern: 'type *', action: 'allow' },
{ pattern: 'git status', action: 'allow' },
{ pattern: 'git log *', action: 'allow' },
{ pattern: 'git diff *', action: 'allow' },
{ pattern: 'git branch *', action: 'allow' },
{ pattern: 'npm list *', action: 'allow' },
{ pattern: 'npm run *', action: 'ask' },
// 需要确认的命令
{ pattern: 'git push *', action: 'ask' },
{ pattern: 'git commit *', action: 'ask' },
{ pattern: 'git checkout *', action: 'ask' },
{ pattern: 'git reset *', action: 'ask' },
{ pattern: 'npm install *', action: 'ask' },
{ pattern: 'npm uninstall *', action: 'ask' },
{ pattern: 'yarn *', action: 'ask' },
{ pattern: 'pnpm *', action: 'ask' },
// 危险命令 - 默认拒绝
{ pattern: 'rm -rf *', action: 'deny' },
{ pattern: 'rm -r *', action: 'ask' },
{ pattern: 'sudo *', action: 'deny' },
{ pattern: 'chmod 777 *', action: 'deny' },
{ pattern: 'mkfs *', action: 'deny' },
{ pattern: 'dd *', action: 'deny' },
{ pattern: '> /dev/*', action: 'deny' },
],
externalDirectory: 'ask',
default: 'ask',
};
/**
* Bash 命令权限检查器
* 使用 tree-sitter 解析命令并检查权限
*/
export class BashPermissionChecker implements PermissionChecker {
readonly name = 'bash';
private config: BashPermissionConfig;
private projectRoot: string;
private askCallback?: (ctx: PermissionContext) => Promise<PermissionDecision>;
private sessionPermissions = new Map<string, 'allow' | 'deny'>();
constructor(projectRoot: string = process.cwd()) {
this.projectRoot = path.resolve(projectRoot);
this.config = this.loadConfig();
}
/**
* 设置权限询问回调
*/
setAskCallback(callback: (ctx: PermissionContext) => Promise<PermissionDecision>): void {
this.askCallback = callback;
}
/**
* 加载权限配置
*/
private loadConfig(): BashPermissionConfig {
try {
if (fs.existsSync(PERMISSION_FILE)) {
const content = fs.readFileSync(PERMISSION_FILE, 'utf-8');
const stored = JSON.parse(content) as Partial<BashPermissionConfig>;
return {
rules: stored.rules || DEFAULT_CONFIG.rules,
externalDirectory: stored.externalDirectory || DEFAULT_CONFIG.externalDirectory,
default: stored.default || DEFAULT_CONFIG.default,
};
}
} catch {
// 忽略加载错误
}
return { ...DEFAULT_CONFIG };
}
/**
* 保存权限配置
*/
saveConfig(): void {
if (!fs.existsSync(CONFIG_DIR)) {
fs.mkdirSync(CONFIG_DIR, { recursive: true });
}
fs.writeFileSync(PERMISSION_FILE, JSON.stringify(this.config, null, 2));
}
/**
* 添加权限规则
*/
addRule(rule: PermissionRule): void {
const existingIndex = this.config.rules.findIndex(r => r.pattern === rule.pattern);
if (existingIndex >= 0) {
this.config.rules[existingIndex] = rule;
} else {
this.config.rules.unshift(rule);
}
this.saveConfig();
}
/**
* 检查路径是否在项目目录内
*/
private isInProjectDirectory(targetPath: string): boolean {
const resolved = path.resolve(targetPath);
return resolved.startsWith(this.projectRoot + path.sep) || resolved === this.projectRoot;
}
/**
* 从解析后的命令中提取可能的外部路径
*/
private async extractPathsFromCommand(command: string, workdir: string): Promise<string[]> {
const externalPaths: string[] = [];
const parseResult = await parseBashCommand(command);
const pathCommands = new Set(['cd', 'rm', 'cp', 'mv', 'mkdir', 'touch', 'chmod', 'chown', 'cat', 'ls']);
for (const cmd of parseResult.commands) {
if (!pathCommands.has(cmd.name)) continue;
const pathsToCheck = [cmd.subcommand, ...cmd.args].filter(Boolean) as string[];
for (const arg of pathsToCheck) {
if (arg.startsWith('-')) continue;
let resolved: string | null = null;
if (arg.startsWith('/')) {
resolved = arg;
} else if (arg.startsWith('~')) {
resolved = arg.replace('~', os.homedir());
} else if (arg.includes('..') || arg.includes('/')) {
resolved = path.resolve(workdir, arg);
}
if (resolved && !this.isInProjectDirectory(resolved)) {
externalPaths.push(resolved);
}
}
}
return [...new Set(externalPaths)];
}
/**
* 检查命令权限
*/
async check(ctx: PermissionContext): Promise<PermissionCheckResult> {
const { command, workdir } = ctx;
// 1. 使用 tree-sitter 解析命令并检查权限
const parseResult = await matchRulesAsync(command, this.config.rules, this.config.default);
// 2. 检查会话级别的临时权限
for (const pattern of parseResult.askPatterns) {
const sessionPerm = this.sessionPermissions.get(pattern);
if (sessionPerm === 'deny') {
return {
allowed: false,
action: 'deny',
reason: `本次会话已拒绝此类命令: ${pattern}`,
};
}
if (sessionPerm === 'allow') {
const allAllowed = parseResult.askPatterns.every(p => this.sessionPermissions.get(p) === 'allow');
if (allAllowed && parseResult.action === 'ask') {
return {
allowed: true,
action: 'allow',
reason: '本次会话已允许此类命令',
};
}
}
}
// 3. 检查外部目录访问
const externalPaths = await this.extractPathsFromCommand(command, workdir);
if (externalPaths.length > 0) {
const extAction = this.config.externalDirectory;
if (extAction === 'deny') {
return {
allowed: false,
action: 'deny',
reason: `命令访问项目目录外的路径: ${externalPaths.join(', ')}`,
};
}
if (extAction === 'ask') {
if (!this.askCallback) {
return {
allowed: false,
action: 'ask',
needsConfirmation: true,
reason: `命令访问项目目录外的路径`,
patterns: externalPaths,
};
}
const decision = await this.askCallback({
...ctx,
externalPaths,
});
if (!decision.allow) {
return {
allowed: false,
action: 'deny',
reason: '用户拒绝访问外部目录',
};
}
}
}
// 4. 根据解析结果处理权限
const { action, matchedPattern, allCommands, askPatterns } = parseResult;
if (action === 'allow') {
return {
allowed: true,
action: 'allow',
reason: matchedPattern ? `匹配规则: ${matchedPattern}` : '默认允许',
};
}
if (action === 'deny') {
return {
allowed: false,
action: 'deny',
reason: matchedPattern
? `匹配规则: ${matchedPattern}`
: `包含被拒绝的命令: ${allCommands.map(c => c.name).join(', ')}`,
};
}
// action === 'ask'
if (!this.askCallback) {
return {
allowed: false,
action: 'ask',
needsConfirmation: true,
patterns: askPatterns,
};
}
const decision = await this.askCallback({
...ctx,
patterns: askPatterns,
});
if (decision.remember) {
for (const pattern of askPatterns) {
this.sessionPermissions.set(pattern, decision.allow ? 'allow' : 'deny');
}
}
return {
allowed: decision.allow,
action: decision.allow ? 'allow' : 'deny',
reason: decision.allow ? '用户允许' : '用户拒绝',
};
}
/**
* 清除会话权限
*/
clearSessionPermissions(): void {
this.sessionPermissions.clear();
}
/**
* 获取当前配置
*/
getConfig(): BashPermissionConfig {
return { ...this.config };
}
}