feat: 支持 Bun standalone 单文件打包
- core: 工具描述从文件系统加载改为编译时内联生成 - core: 添加 WASM 加载器支持嵌入式 WASM 数据 - core: bash-parser 使用动态导入 web-tree-sitter - server: 添加静态文件托管支持 (--static 参数) - server: 新增 standalone 入口点 (嵌入 Web UI + WASM) - scripts: 添加 build-standalone.ts 构建脚本 - 更新 .gitignore 忽略生成文件
This commit is contained in:
@@ -45,6 +45,11 @@ packages/desktop/src-tauri/Cargo.lock
|
||||
*.temp
|
||||
.cache/
|
||||
|
||||
# Generated files
|
||||
packages/core/src/tools/descriptions.generated.ts
|
||||
packages/server/src/embedded-assets.generated.ts
|
||||
packages/server/src/embedded-wasm.generated.ts
|
||||
|
||||
# AI Open reference code
|
||||
ai-open/
|
||||
|
||||
|
||||
+3
-1
@@ -41,7 +41,9 @@
|
||||
|
||||
"website:build": "pnpm --filter @ai-assistant/website build",
|
||||
"website:dev": "pnpm --filter @ai-assistant/website dev",
|
||||
"website:preview": "pnpm --filter @ai-assistant/website preview"
|
||||
"website:preview": "pnpm --filter @ai-assistant/website preview",
|
||||
|
||||
"build:standalone": "bun run scripts/build-standalone.ts"
|
||||
},
|
||||
"keywords": [
|
||||
"ai",
|
||||
|
||||
@@ -40,7 +40,8 @@
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc && cp -r src/tools/descriptions dist/tools/",
|
||||
"prebuild": "bun run scripts/generate-descriptions.ts",
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Generate Tool Descriptions
|
||||
*
|
||||
* 将 descriptions 目录下的 .txt 文件转换为 TypeScript 模块
|
||||
* 用于 Bun 打包时内联工具描述
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const DESCRIPTIONS_DIR = path.join(__dirname, '../src/tools/descriptions');
|
||||
const OUTPUT_FILE = path.join(__dirname, '../src/tools/descriptions.generated.ts');
|
||||
|
||||
interface DescriptionEntry {
|
||||
category: string;
|
||||
name: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
function collectDescriptions(): DescriptionEntry[] {
|
||||
const entries: DescriptionEntry[] = [];
|
||||
|
||||
const categories = fs.readdirSync(DESCRIPTIONS_DIR);
|
||||
for (const category of categories) {
|
||||
const categoryPath = path.join(DESCRIPTIONS_DIR, category);
|
||||
const stat = fs.statSync(categoryPath);
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
const files = fs.readdirSync(categoryPath);
|
||||
for (const file of files) {
|
||||
if (file.endsWith('.txt')) {
|
||||
const name = file.replace('.txt', '');
|
||||
const content = fs.readFileSync(path.join(categoryPath, file), 'utf-8').trim();
|
||||
entries.push({ category, name, content });
|
||||
}
|
||||
}
|
||||
} else if (category.endsWith('.txt')) {
|
||||
const name = category.replace('.txt', '');
|
||||
const content = fs.readFileSync(categoryPath, 'utf-8').trim();
|
||||
entries.push({ category: '', name, content });
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
function escapeString(str: string): string {
|
||||
return str
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/`/g, '\\`')
|
||||
.replace(/\$/g, '\\$');
|
||||
}
|
||||
|
||||
function generateCode(entries: DescriptionEntry[]): string {
|
||||
const lines: string[] = [
|
||||
'// Auto-generated by scripts/generate-descriptions.ts',
|
||||
'// Do not edit manually',
|
||||
'',
|
||||
'export const TOOL_DESCRIPTIONS: Record<string, string> = {',
|
||||
];
|
||||
|
||||
for (const entry of entries) {
|
||||
const escapedContent = escapeString(entry.content);
|
||||
lines.push(` '${entry.name}': \`${escapedContent}\`,`);
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
lines.push('};');
|
||||
lines.push('');
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function main() {
|
||||
console.log('Collecting tool descriptions...');
|
||||
const entries = collectDescriptions();
|
||||
console.log(`Found ${entries.length} descriptions`);
|
||||
|
||||
console.log('Generating TypeScript code...');
|
||||
const code = generateCode(entries);
|
||||
|
||||
console.log(`Writing to ${OUTPUT_FILE}...`);
|
||||
fs.writeFileSync(OUTPUT_FILE, code, 'utf-8');
|
||||
|
||||
console.log('Done!');
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -354,6 +354,9 @@ export type {
|
||||
FileSearchOptions,
|
||||
} from './file-index/index.js';
|
||||
|
||||
// RepoMap WASM 加载器(用于 standalone 构建)
|
||||
export { setEmbeddedWasm, hasEmbeddedWasm } from './repomap/index.js';
|
||||
|
||||
// Constants - 统一存储路径
|
||||
export {
|
||||
APP_DIR_NAME,
|
||||
|
||||
@@ -1,29 +1,41 @@
|
||||
import { Parser, Language, type Node } from 'web-tree-sitter';
|
||||
import * as path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { hasEmbeddedWasm, getEmbeddedWasm } from '../repomap/tags/wasm-loader.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// 解析后的命令结构
|
||||
export interface ParsedCommand {
|
||||
name: string; // 命令名,如 "git"
|
||||
name: string; // 命令名,如 "git"
|
||||
subcommand?: string; // 子命令,如 "push"
|
||||
args: string[]; // 参数列表
|
||||
text: string; // 原始命令文本
|
||||
args: string[]; // 参数列表
|
||||
text: string; // 原始命令文本
|
||||
}
|
||||
|
||||
// 解析结果
|
||||
export interface ParseResult {
|
||||
commands: ParsedCommand[]; // 所有解析出的命令
|
||||
commands: ParsedCommand[]; // 所有解析出的命令
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// 单例解析器
|
||||
let parserInstance: Parser | null = null;
|
||||
let bashLanguage: Language | null = null;
|
||||
// Tree-sitter 类型
|
||||
interface TreeSitterNode {
|
||||
type: string;
|
||||
text: string;
|
||||
childCount: number;
|
||||
child(index: number): TreeSitterNode | null;
|
||||
}
|
||||
|
||||
// 单例解析器 (使用 any 类型避免与 web-tree-sitter 的类型冲突)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let parserInstance: any = null;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let bashLanguage: any = null;
|
||||
let initPromise: Promise<void> | null = null;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let TreeSitter: any = null;
|
||||
|
||||
/**
|
||||
* 获取 wasm 文件路径
|
||||
@@ -55,19 +67,51 @@ async function initParser(): Promise<void> {
|
||||
|
||||
initPromise = (async () => {
|
||||
try {
|
||||
// 初始化 tree-sitter
|
||||
await Parser.init({
|
||||
locateFile: (scriptName: string) => {
|
||||
return getWasmPath(scriptName);
|
||||
},
|
||||
});
|
||||
// 动态导入 web-tree-sitter
|
||||
TreeSitter = await import('web-tree-sitter');
|
||||
|
||||
// 检查是否有嵌入的 WASM
|
||||
if (hasEmbeddedWasm()) {
|
||||
const wasmData = getEmbeddedWasm('tree-sitter.wasm');
|
||||
if (wasmData) {
|
||||
// 使用嵌入的 WASM 数据初始化
|
||||
await TreeSitter.Parser.init({
|
||||
wasmBinary: wasmData,
|
||||
});
|
||||
} else {
|
||||
// 回退到文件系统
|
||||
await TreeSitter.Parser.init({
|
||||
locateFile: (scriptName: string) => {
|
||||
return getWasmPath(scriptName);
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// 使用文件系统加载
|
||||
await TreeSitter.Parser.init({
|
||||
locateFile: (scriptName: string) => {
|
||||
return getWasmPath(scriptName);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 创建解析器实例
|
||||
parserInstance = new Parser();
|
||||
parserInstance = new TreeSitter.Parser();
|
||||
|
||||
// 加载 bash 语言
|
||||
const bashWasmPath = getWasmPath('tree-sitter-bash.wasm');
|
||||
bashLanguage = await Language.load(bashWasmPath);
|
||||
if (hasEmbeddedWasm()) {
|
||||
const bashWasmData = getEmbeddedWasm('tree-sitter-bash.wasm');
|
||||
if (bashWasmData) {
|
||||
bashLanguage = await TreeSitter.Parser.Language.load(bashWasmData);
|
||||
} else {
|
||||
const bashWasmPath = getWasmPath('tree-sitter-bash.wasm');
|
||||
bashLanguage = await TreeSitter.Parser.Language.load(bashWasmPath);
|
||||
}
|
||||
} else {
|
||||
const bashWasmPath = getWasmPath('tree-sitter-bash.wasm');
|
||||
bashLanguage = await TreeSitter.Parser.Language.load(bashWasmPath);
|
||||
}
|
||||
|
||||
parserInstance.setLanguage(bashLanguage);
|
||||
} catch (error) {
|
||||
initPromise = null;
|
||||
@@ -81,7 +125,7 @@ async function initParser(): Promise<void> {
|
||||
/**
|
||||
* 从语法树节点中提取命令信息
|
||||
*/
|
||||
function extractCommandFromNode(node: Node): ParsedCommand {
|
||||
function extractCommandFromNode(node: TreeSitterNode): ParsedCommand {
|
||||
const parts: string[] = [];
|
||||
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
@@ -101,8 +145,10 @@ function extractCommandFromNode(node: Node): ParsedCommand {
|
||||
// 对于字符串类型,提取内部文本(去掉引号)
|
||||
if (child.type === 'string' || child.type === 'raw_string') {
|
||||
const text = child.text;
|
||||
if ((text.startsWith('"') && text.endsWith('"')) ||
|
||||
(text.startsWith("'") && text.endsWith("'"))) {
|
||||
if (
|
||||
(text.startsWith('"') && text.endsWith('"')) ||
|
||||
(text.startsWith("'") && text.endsWith("'"))
|
||||
) {
|
||||
parts.push(text.slice(1, -1));
|
||||
} else {
|
||||
parts.push(text);
|
||||
@@ -138,8 +184,8 @@ function extractCommandFromNode(node: Node): ParsedCommand {
|
||||
/**
|
||||
* 递归查找所有命令节点
|
||||
*/
|
||||
function findCommandNodes(node: Node): Node[] {
|
||||
const commands: Node[] = [];
|
||||
function findCommandNodes(node: TreeSitterNode): TreeSitterNode[] {
|
||||
const commands: TreeSitterNode[] = [];
|
||||
|
||||
if (node.type === 'command') {
|
||||
commands.push(node);
|
||||
|
||||
@@ -11,6 +11,9 @@ export { RepoMap, createRepoMap } from './repomap.js';
|
||||
// Tag 提取
|
||||
export { TagExtractor } from './tags/index.js';
|
||||
|
||||
// WASM 加载器(用于 standalone 构建)
|
||||
export { setEmbeddedWasm, hasEmbeddedWasm } from './tags/wasm-loader.js';
|
||||
|
||||
// PageRank 排序
|
||||
export {
|
||||
Graph,
|
||||
|
||||
@@ -8,6 +8,7 @@ import * as path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import type { Tag } from '../types.js';
|
||||
import { getLanguageFromFilename } from '../types.js';
|
||||
import { hasEmbeddedWasm, getEmbeddedWasm } from './wasm-loader.js';
|
||||
|
||||
// Tree-sitter 类型(web-tree-sitter)
|
||||
interface TreeSitterParser {
|
||||
@@ -56,8 +57,25 @@ async function initTreeSitter(): Promise<void> {
|
||||
|
||||
try {
|
||||
const TreeSitter = await import('web-tree-sitter');
|
||||
// Parser 是命名导出的类,init 是其静态方法
|
||||
await TreeSitter.Parser.init();
|
||||
|
||||
// 检查是否有嵌入的 WASM
|
||||
if (hasEmbeddedWasm()) {
|
||||
const wasmData = getEmbeddedWasm('tree-sitter.wasm');
|
||||
if (wasmData) {
|
||||
// 使用嵌入的 WASM 数据直接初始化
|
||||
// web-tree-sitter 支持传入 WebAssembly.Module 或 ArrayBuffer
|
||||
await TreeSitter.Parser.init({
|
||||
// 传入 WASM 二进制数据
|
||||
wasmBinary: wasmData,
|
||||
});
|
||||
} else {
|
||||
await TreeSitter.Parser.init();
|
||||
}
|
||||
} else {
|
||||
// 使用默认行为(从 node_modules 加载)
|
||||
await TreeSitter.Parser.init();
|
||||
}
|
||||
|
||||
ParserClass = TreeSitter.Parser;
|
||||
treeSitterInitialized = true;
|
||||
} catch (error) {
|
||||
@@ -215,7 +233,19 @@ export class TagExtractor {
|
||||
}
|
||||
|
||||
try {
|
||||
// 尝试加载 WASM 语言文件
|
||||
// 首先检查是否有嵌入的语言 WASM
|
||||
if (hasEmbeddedWasm()) {
|
||||
const wasmName = `tree-sitter-${lang}.wasm`;
|
||||
const wasmData = getEmbeddedWasm(wasmName);
|
||||
if (wasmData) {
|
||||
// 使用嵌入的 WASM 数据加载语言
|
||||
const language = await ParserClass.Language.load(wasmData);
|
||||
this.languages.set(lang, language);
|
||||
return language;
|
||||
}
|
||||
}
|
||||
|
||||
// 回退到从文件系统加载
|
||||
const wasmPath = this.getWasmPath(lang);
|
||||
const language = await ParserClass.Language.load(wasmPath);
|
||||
this.languages.set(lang, language);
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* WASM Loader for Tree-sitter
|
||||
*
|
||||
* 支持从文件系统或嵌入的 base64 数据加载 WASM
|
||||
*/
|
||||
|
||||
// 嵌入的 WASM 数据(构建时生成)
|
||||
let embeddedWasm: Map<string, string> | null = null;
|
||||
|
||||
/**
|
||||
* 设置嵌入的 WASM 数据
|
||||
*/
|
||||
export function setEmbeddedWasm(wasm: Map<string, string>): void {
|
||||
embeddedWasm = wasm;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有嵌入的 WASM
|
||||
*/
|
||||
export function hasEmbeddedWasm(): boolean {
|
||||
return embeddedWasm !== null && embeddedWasm.size > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取嵌入的 WASM 数据
|
||||
*/
|
||||
export function getEmbeddedWasm(name: string): Uint8Array | null {
|
||||
if (!embeddedWasm) return null;
|
||||
|
||||
const base64 = embeddedWasm.get(name);
|
||||
if (!base64) return null;
|
||||
|
||||
// 解码 base64
|
||||
const binaryString = atob(base64);
|
||||
const bytes = new Uint8Array(binaryString.length);
|
||||
for (let i = 0; i < binaryString.length; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 tree-sitter.wasm 的路径或数据
|
||||
*/
|
||||
export function getTreeSitterWasmPath(): string | undefined {
|
||||
// 如果没有嵌入数据,返回 undefined 让默认行为生效
|
||||
if (!hasEmbeddedWasm()) {
|
||||
return undefined;
|
||||
}
|
||||
// 返回一个特殊标记,在 init 时会被拦截
|
||||
return '__embedded__';
|
||||
}
|
||||
@@ -1,64 +1,16 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
/**
|
||||
* Tool Description Loader
|
||||
*
|
||||
* 从内联的 descriptions.generated.ts 加载工具描述
|
||||
* 支持 Bun 单文件打包
|
||||
*/
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// 工具名到子目录的映射
|
||||
const TOOL_CATEGORY_MAP: Record<string, string> = {
|
||||
// shell
|
||||
bash: 'shell',
|
||||
kill_shell: 'shell',
|
||||
// filesystem
|
||||
read_file: 'filesystem',
|
||||
write_file: 'filesystem',
|
||||
edit_file: 'filesystem',
|
||||
multi_edit: 'filesystem',
|
||||
glob: 'filesystem',
|
||||
grep: 'filesystem',
|
||||
// web
|
||||
web_search: 'web',
|
||||
web_extract: 'web',
|
||||
// git
|
||||
git_status: 'git',
|
||||
git_diff: 'git',
|
||||
git_log: 'git',
|
||||
git_branch: 'git',
|
||||
git_add: 'git',
|
||||
git_commit: 'git',
|
||||
git_push: 'git',
|
||||
git_pull: 'git',
|
||||
git_checkout: 'git',
|
||||
git_stash: 'git',
|
||||
// todo
|
||||
todo_write: 'todo',
|
||||
// plan
|
||||
ask_user_question: 'plan',
|
||||
enter_plan_mode: 'plan',
|
||||
exit_plan_mode: 'plan',
|
||||
// task
|
||||
task: 'task',
|
||||
task_output: 'task',
|
||||
// checkpoint
|
||||
checkpoint_create: 'checkpoint',
|
||||
checkpoint_list: 'checkpoint',
|
||||
checkpoint_diff: 'checkpoint',
|
||||
checkpoint_restore: 'checkpoint',
|
||||
undo: 'checkpoint',
|
||||
// repomap
|
||||
repo_map: 'repomap',
|
||||
};
|
||||
import { TOOL_DESCRIPTIONS } from './descriptions.generated.js';
|
||||
|
||||
export function loadDescription(toolName: string): string {
|
||||
const category = TOOL_CATEGORY_MAP[toolName];
|
||||
const filePath = category
|
||||
? path.join(__dirname, 'descriptions', category, `${toolName}.txt`)
|
||||
: path.join(__dirname, 'descriptions', `${toolName}.txt`);
|
||||
|
||||
try {
|
||||
return fs.readFileSync(filePath, 'utf-8').trim();
|
||||
} catch {
|
||||
throw new Error(`无法加载工具描述文件: ${filePath}`);
|
||||
const description = TOOL_DESCRIPTIONS[toolName];
|
||||
if (!description) {
|
||||
throw new Error(`工具描述未找到: ${toolName}`);
|
||||
}
|
||||
return description;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"build:bundle": "bun build src/bin/server.ts --target bun --outfile dist/server.bundle.js",
|
||||
"build:standalone": "bun build src/bin/server.ts --compile --outfile dist/ai-server",
|
||||
"dev": "tsc --watch",
|
||||
"start": "bun run src/bin/server.ts",
|
||||
"start:dev": "bun --watch run src/bin/server.ts",
|
||||
|
||||
@@ -14,12 +14,13 @@
|
||||
import { app, websocket, startServer } from '../index.js';
|
||||
|
||||
// 解析命令行参数
|
||||
function parseArgs(): { port: number; host: string; auth?: boolean; token?: string } {
|
||||
function parseArgs(): { port: number; host: string; auth?: boolean; token?: string; staticDir?: string } {
|
||||
const args = process.argv.slice(2);
|
||||
let port = 3000;
|
||||
let host = '127.0.0.1';
|
||||
let auth: boolean | undefined;
|
||||
let token: string | undefined;
|
||||
let staticDir: string | undefined;
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] === '--port' || args[i] === '-p') {
|
||||
@@ -35,44 +36,51 @@ function parseArgs(): { port: number; host: string; auth?: boolean; token?: stri
|
||||
} else if (args[i] === '--token' || args[i] === '-t') {
|
||||
token = args[i + 1];
|
||||
i++;
|
||||
} else if (args[i] === '--static' || args[i] === '-s') {
|
||||
staticDir = args[i + 1];
|
||||
i++;
|
||||
} else if (args[i] === '--help' || args[i] === '-h') {
|
||||
console.log(`
|
||||
AI Assistant Server
|
||||
|
||||
Usage:
|
||||
bun run server.ts [options]
|
||||
ai-server [options]
|
||||
|
||||
Options:
|
||||
-p, --port <port> Port to listen on (default: 3000)
|
||||
-H, --host <host> Host to bind to (default: 127.0.0.1)
|
||||
--auth Enable authentication
|
||||
--no-auth Disable authentication
|
||||
-t, --token <token> Set authentication token
|
||||
-h, --help Show this help message
|
||||
-p, --port <port> Port to listen on (default: 3000)
|
||||
-H, --host <host> Host to bind to (default: 127.0.0.1)
|
||||
-s, --static <dir> Serve static files from directory (Web UI)
|
||||
--auth Enable authentication
|
||||
--no-auth Disable authentication
|
||||
-t, --token <token> Set authentication token
|
||||
-h, --help Show this help message
|
||||
|
||||
Examples:
|
||||
# Local development (no auth)
|
||||
bun run server.ts
|
||||
ai-server
|
||||
|
||||
# Serve with Web UI
|
||||
ai-server --static ./web-dist
|
||||
|
||||
# Remote server with auth
|
||||
bun run server.ts --host 0.0.0.0 --auth
|
||||
ai-server --host 0.0.0.0 --auth
|
||||
|
||||
# Custom token
|
||||
bun run server.ts --host 0.0.0.0 --token mysecrettoken
|
||||
# Full production setup
|
||||
ai-server --host 0.0.0.0 --static ./web-dist --token mysecrettoken
|
||||
`);
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
return { port, host, auth, token };
|
||||
return { port, host, auth, token, staticDir };
|
||||
}
|
||||
|
||||
// 主函数
|
||||
async function main() {
|
||||
const { port, host, auth, token } = parseArgs();
|
||||
const { port, host, auth, token, staticDir } = parseArgs();
|
||||
|
||||
// 初始化并打印启动信息
|
||||
await startServer({ port, host, auth, token });
|
||||
await startServer({ port, host, auth, token, staticDir });
|
||||
|
||||
// 启动 Bun 服务器
|
||||
const server = Bun.serve({
|
||||
|
||||
@@ -0,0 +1,320 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* AI Assistant Standalone Server
|
||||
*
|
||||
* 包含嵌入式 Web UI 的独立服务器
|
||||
* 由 scripts/build-standalone.ts 构建
|
||||
*/
|
||||
|
||||
import { Hono } from 'hono';
|
||||
import { cors } from 'hono/cors';
|
||||
import { logger } from 'hono/logger';
|
||||
import { createBunWebSocket } from 'hono/bun';
|
||||
|
||||
import {
|
||||
sessionsRouter,
|
||||
toolsRouter,
|
||||
configRouter,
|
||||
filesRouter,
|
||||
commandsRouter,
|
||||
mcpRouter,
|
||||
hooksRouter,
|
||||
agentsRouter,
|
||||
checkpointsRouter,
|
||||
providersRouter,
|
||||
servicesRouter,
|
||||
contextRouter,
|
||||
lspRouter,
|
||||
systemCommandsRouter,
|
||||
statsRouter,
|
||||
} from '../routes/index.js';
|
||||
import {
|
||||
handleWebSocket,
|
||||
handleWebSocketMessage,
|
||||
handleWebSocketClose,
|
||||
getConnectionStats,
|
||||
} from '../ws.js';
|
||||
import { handleSSE, getSSEStats } from '../sse.js';
|
||||
import { getSessionManager } from '../session/manager.js';
|
||||
import { initCore, isCoreAvailable } from '../agent/index.js';
|
||||
import {
|
||||
authMiddleware,
|
||||
initAuth,
|
||||
getAuthConfig,
|
||||
generateToken,
|
||||
addToken,
|
||||
} from '../auth/index.js';
|
||||
|
||||
// 嵌入的 Web 资源(构建时生成)
|
||||
import { EMBEDDED_ASSETS, decodeAsset } from '../embedded-assets.generated.js';
|
||||
// 嵌入的 WASM 文件(构建时生成)
|
||||
import { EMBEDDED_WASM } from '../embedded-wasm.generated.js';
|
||||
|
||||
// 创建 Hono 应用
|
||||
const app = new Hono();
|
||||
|
||||
// WebSocket 升级
|
||||
const { upgradeWebSocket, websocket } = createBunWebSocket();
|
||||
|
||||
// 中间件
|
||||
app.use('*', logger());
|
||||
app.use(
|
||||
'*',
|
||||
cors({
|
||||
origin: '*',
|
||||
allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
||||
allowHeaders: ['Content-Type', 'Authorization'],
|
||||
})
|
||||
);
|
||||
|
||||
// 认证中间件
|
||||
app.use('*', authMiddleware);
|
||||
|
||||
// 健康检查
|
||||
app.get('/health', (c) => {
|
||||
const sessionManager = getSessionManager();
|
||||
const wsStats = getConnectionStats();
|
||||
const sseStats = getSSEStats();
|
||||
const authConfig = getAuthConfig();
|
||||
|
||||
return c.json({
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
agent: {
|
||||
coreAvailable: isCoreAvailable(),
|
||||
},
|
||||
auth: {
|
||||
enabled: authConfig.enabled,
|
||||
tokenCount: authConfig.tokens.length,
|
||||
},
|
||||
stats: {
|
||||
sessions: sessionManager.count(),
|
||||
websocket: wsStats,
|
||||
sse: sseStats,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// API 路由
|
||||
const api = new Hono();
|
||||
api.route('/sessions', sessionsRouter);
|
||||
api.route('/tools', toolsRouter);
|
||||
api.route('/config', configRouter);
|
||||
api.route('/files', filesRouter);
|
||||
api.route('/commands', commandsRouter);
|
||||
api.route('/mcp', mcpRouter);
|
||||
api.route('/hooks', hooksRouter);
|
||||
api.route('/agents', agentsRouter);
|
||||
api.route('/checkpoints', checkpointsRouter);
|
||||
api.route('/providers', providersRouter);
|
||||
api.route('/services', servicesRouter);
|
||||
api.route('/lsp', lspRouter);
|
||||
api.route('/system-commands', systemCommandsRouter);
|
||||
api.route('/stats', statsRouter);
|
||||
api.route('/', contextRouter);
|
||||
|
||||
// SSE 事件流
|
||||
api.get('/sessions/:id/events', handleSSE);
|
||||
|
||||
// WebSocket 端点
|
||||
api.get(
|
||||
'/ws/:sessionId',
|
||||
upgradeWebSocket((c) => {
|
||||
const sessionId = c.req.param('sessionId');
|
||||
return {
|
||||
onOpen(_event, ws) {
|
||||
handleWebSocket(ws, sessionId);
|
||||
},
|
||||
onMessage(event, ws) {
|
||||
handleWebSocketMessage(ws, sessionId, event.data);
|
||||
},
|
||||
onClose(_event, ws) {
|
||||
handleWebSocketClose(ws, sessionId);
|
||||
},
|
||||
onError(event, ws) {
|
||||
console.error('[WS] Error:', event);
|
||||
handleWebSocketClose(ws, sessionId);
|
||||
},
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
app.route('/api', api);
|
||||
|
||||
// 嵌入式 Web UI 静态文件服务
|
||||
app.get('*', (c) => {
|
||||
let reqPath = c.req.path;
|
||||
|
||||
// 尝试查找精确匹配
|
||||
let asset = EMBEDDED_ASSETS.get(reqPath);
|
||||
|
||||
// 如果没找到且不是文件路径,尝试 index.html (SPA 路由)
|
||||
if (!asset && !reqPath.includes('.')) {
|
||||
asset = EMBEDDED_ASSETS.get('/index.html');
|
||||
}
|
||||
|
||||
if (asset) {
|
||||
const body = decodeAsset(asset);
|
||||
return new Response(body, {
|
||||
headers: {
|
||||
'Content-Type': asset.mimeType,
|
||||
'Cache-Control': reqPath.includes('/assets/') ? 'public, max-age=31536000' : 'no-cache',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return c.json({ success: false, error: 'Not found' }, 404);
|
||||
});
|
||||
|
||||
// 解析命令行参数
|
||||
function parseArgs(): { port: number; host: string; auth?: boolean; token?: string } {
|
||||
const args = process.argv.slice(2);
|
||||
let port = 3000;
|
||||
let host = '127.0.0.1';
|
||||
let auth: boolean | undefined;
|
||||
let token: string | undefined;
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] === '--port' || args[i] === '-p') {
|
||||
port = parseInt(args[i + 1], 10) || 3000;
|
||||
i++;
|
||||
} else if (args[i] === '--host' || args[i] === '-H') {
|
||||
host = args[i + 1] || '127.0.0.1';
|
||||
i++;
|
||||
} else if (args[i] === '--auth') {
|
||||
auth = true;
|
||||
} else if (args[i] === '--no-auth') {
|
||||
auth = false;
|
||||
} else if (args[i] === '--token' || args[i] === '-t') {
|
||||
token = args[i + 1];
|
||||
i++;
|
||||
} else if (args[i] === '--help' || args[i] === '-h') {
|
||||
console.log(`
|
||||
AI Assistant (Standalone)
|
||||
|
||||
A terminal-based AI coding assistant with embedded Web UI.
|
||||
|
||||
Usage:
|
||||
ai-assistant [options]
|
||||
|
||||
Options:
|
||||
-p, --port <port> Port to listen on (default: 3000)
|
||||
-H, --host <host> Host to bind to (default: 127.0.0.1)
|
||||
--auth Enable authentication
|
||||
--no-auth Disable authentication
|
||||
-t, --token <token> Set authentication token
|
||||
-h, --help Show this help message
|
||||
|
||||
Examples:
|
||||
# Local development
|
||||
ai-assistant
|
||||
|
||||
# Listen on all interfaces with auth
|
||||
ai-assistant --host 0.0.0.0 --auth
|
||||
|
||||
# Custom port and token
|
||||
ai-assistant --port 8080 --token mysecrettoken
|
||||
|
||||
After starting, open http://localhost:<port> in your browser.
|
||||
`);
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
return { port, host, auth, token };
|
||||
}
|
||||
|
||||
// 主函数
|
||||
async function main() {
|
||||
const { port, host, auth, token } = parseArgs();
|
||||
|
||||
// 初始化嵌入的 WASM(在 Core 初始化之前)
|
||||
if (EMBEDDED_WASM.size > 0) {
|
||||
try {
|
||||
// 动态导入 core 模块来设置 WASM
|
||||
const core = await import('@ai-assistant/core');
|
||||
if (typeof core.setEmbeddedWasm === 'function') {
|
||||
core.setEmbeddedWasm(EMBEDDED_WASM);
|
||||
console.log(`[Server] Embedded WASM initialized (${EMBEDDED_WASM.size} files)`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[Server] Failed to initialize embedded WASM:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化 SessionManager
|
||||
const sessionManager = getSessionManager();
|
||||
await sessionManager.init();
|
||||
|
||||
// 初始化 Core
|
||||
const coreLoaded = await initCore();
|
||||
if (coreLoaded) {
|
||||
console.log('[Server] Core module initialized');
|
||||
} else {
|
||||
console.warn('[Server] Core module not available, running in limited mode');
|
||||
}
|
||||
|
||||
// 初始化认证
|
||||
const isRemote = host !== '127.0.0.1' && host !== 'localhost';
|
||||
const authEnabled = auth !== undefined ? auth : isRemote;
|
||||
|
||||
initAuth({
|
||||
enabled: authEnabled,
|
||||
tokens: [],
|
||||
skipPaths: ['/health', '/api/health'],
|
||||
});
|
||||
|
||||
if (authEnabled) {
|
||||
const serverToken = token || generateToken();
|
||||
addToken(serverToken);
|
||||
console.log(`[Auth] Authentication enabled`);
|
||||
console.log(`[Auth] Token: ${serverToken}`);
|
||||
}
|
||||
|
||||
const coreStatus = isCoreAvailable() ? '✅ Core loaded' : '⚠️ Core not available';
|
||||
const authConfig = getAuthConfig();
|
||||
const authStatus = authConfig.enabled ? '🔐 Enabled' : '🔓 Disabled';
|
||||
|
||||
console.log(`
|
||||
╔════════════════════════════════════════════╗
|
||||
║ AI Assistant (Standalone) ║
|
||||
╠════════════════════════════════════════════╣
|
||||
║ Web UI: http://${host}:${port}
|
||||
║ REST API: http://${host}:${port}/api
|
||||
║ WebSocket: ws://${host}:${port}/api/ws/:sessionId
|
||||
║ Health: http://${host}:${port}/health
|
||||
║ Agent: ${coreStatus}
|
||||
║ Auth: ${authStatus}
|
||||
║ Web UI: 📦 Embedded
|
||||
╚════════════════════════════════════════════╝
|
||||
`);
|
||||
|
||||
// 启动服务器
|
||||
const server = Bun.serve({
|
||||
port,
|
||||
hostname: host,
|
||||
fetch: app.fetch,
|
||||
websocket,
|
||||
});
|
||||
|
||||
console.log(`Server running at http://${host}:${port}`);
|
||||
console.log(`Open http://${host === '0.0.0.0' ? 'localhost' : host}:${port} in your browser\n`);
|
||||
|
||||
// 优雅关闭
|
||||
process.on('SIGINT', () => {
|
||||
console.log('\nShutting down server...');
|
||||
server.stop();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
console.log('\nShutting down server...');
|
||||
server.stop();
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('Failed to start server:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -7,6 +7,7 @@
|
||||
import { Hono } from 'hono';
|
||||
import { cors } from 'hono/cors';
|
||||
import { logger } from 'hono/logger';
|
||||
import { serveStatic } from 'hono/bun';
|
||||
import { createBunWebSocket } from 'hono/bun';
|
||||
|
||||
import { sessionsRouter, toolsRouter, configRouter, filesRouter, commandsRouter, mcpRouter, hooksRouter, agentsRouter, checkpointsRouter, providersRouter, servicesRouter, contextRouter, lspRouter, systemCommandsRouter, statsRouter } from './routes/index.js';
|
||||
@@ -158,6 +159,8 @@ export interface ServerOptions {
|
||||
auth?: boolean;
|
||||
/** 预设的 token */
|
||||
token?: string;
|
||||
/** 静态文件目录 (托管 Web UI) */
|
||||
staticDir?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -210,18 +213,63 @@ export async function initServer(options: ServerOptions = {}): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置静态文件托管
|
||||
*/
|
||||
export function configureStaticFiles(staticDir: string): void {
|
||||
// 静态文件中间件
|
||||
app.use(
|
||||
'/*',
|
||||
serveStatic({
|
||||
root: staticDir,
|
||||
rewriteRequestPath: (path) => {
|
||||
// API 和健康检查路径不走静态文件
|
||||
if (path.startsWith('/api') || path === '/health') {
|
||||
return path;
|
||||
}
|
||||
return path;
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
// SPA 路由回退 - 所有非 API/静态文件请求返回 index.html
|
||||
app.get('*', async (c) => {
|
||||
const path = c.req.path;
|
||||
// 跳过 API 和健康检查
|
||||
if (path.startsWith('/api') || path === '/health') {
|
||||
return c.notFound();
|
||||
}
|
||||
|
||||
// 返回 index.html
|
||||
const indexPath = `${staticDir}/index.html`;
|
||||
const file = Bun.file(indexPath);
|
||||
if (await file.exists()) {
|
||||
return new Response(file, {
|
||||
headers: { 'Content-Type': 'text/html' },
|
||||
});
|
||||
}
|
||||
return c.notFound();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动服务器 (Bun 环境)
|
||||
*/
|
||||
export async function startServer(options: ServerOptions = {}): Promise<void> {
|
||||
const { port = 3000, host = '127.0.0.1' } = options;
|
||||
const { port = 3000, host = '127.0.0.1', staticDir } = options;
|
||||
|
||||
// 初始化
|
||||
await initServer(options);
|
||||
|
||||
// 配置静态文件托管
|
||||
if (staticDir) {
|
||||
configureStaticFiles(staticDir);
|
||||
}
|
||||
|
||||
const coreStatus = isCoreAvailable() ? '✅ Core loaded' : '⚠️ Core not available';
|
||||
const authConfig = getAuthConfig();
|
||||
const authStatus = authConfig.enabled ? '🔐 Enabled' : '🔓 Disabled';
|
||||
const staticStatus = staticDir ? `📁 ${staticDir}` : '❌ Disabled';
|
||||
|
||||
console.log(`
|
||||
╔════════════════════════════════════════════╗
|
||||
@@ -233,6 +281,7 @@ export async function startServer(options: ServerOptions = {}): Promise<void> {
|
||||
║ Health: http://${host}:${port}/health
|
||||
║ Agent: ${coreStatus}
|
||||
║ Auth: ${authStatus}
|
||||
║ Static: ${staticStatus}
|
||||
╚════════════════════════════════════════════╝
|
||||
`);
|
||||
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Build Standalone Server with Embedded Web UI and WASM
|
||||
*
|
||||
* 构建包含 Web UI 和 WASM 文件的独立服务器可执行文件
|
||||
*
|
||||
* Usage:
|
||||
* bun run scripts/build-standalone.ts
|
||||
* bun run scripts/build-standalone.ts --output ./dist/ai-assistant
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { $ } from 'bun';
|
||||
|
||||
const ROOT_DIR = path.join(import.meta.dir, '..');
|
||||
const WEB_DIR = path.join(ROOT_DIR, 'packages/web');
|
||||
const SERVER_DIR = path.join(ROOT_DIR, 'packages/server');
|
||||
const NODE_MODULES = path.join(ROOT_DIR, 'node_modules');
|
||||
const CORE_DIR = path.join(ROOT_DIR, 'packages/core');
|
||||
|
||||
// 解析命令行参数
|
||||
function parseArgs(): { output: string } {
|
||||
const args = process.argv.slice(2);
|
||||
let output = path.join(ROOT_DIR, 'dist/ai-assistant');
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] === '--output' || args[i] === '-o') {
|
||||
output = args[i + 1];
|
||||
i++;
|
||||
} else if (args[i] === '--help' || args[i] === '-h') {
|
||||
console.log(`
|
||||
Build Standalone Server with Embedded Web UI
|
||||
|
||||
Usage:
|
||||
bun run scripts/build-standalone.ts [options]
|
||||
|
||||
Options:
|
||||
-o, --output <path> Output executable path (default: dist/ai-assistant)
|
||||
-h, --help Show this help message
|
||||
`);
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
return { output };
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const { output } = parseArgs();
|
||||
const outputDir = path.dirname(output);
|
||||
|
||||
console.log('🚀 Building standalone AI Assistant...\n');
|
||||
|
||||
// 1. 构建 Core
|
||||
console.log('📦 Building @ai-assistant/core...');
|
||||
await $`pnpm --filter @ai-assistant/core build`.quiet();
|
||||
console.log(' ✅ Core built\n');
|
||||
|
||||
// 2. 构建 Web
|
||||
console.log('📦 Building @ai-assistant/web...');
|
||||
await $`pnpm --filter @ai-assistant/web build`.quiet();
|
||||
console.log(' ✅ Web built\n');
|
||||
|
||||
// 3. 生成嵌入式 Web 资源
|
||||
console.log('📝 Embedding Web UI assets...');
|
||||
const webDistDir = path.join(WEB_DIR, 'dist');
|
||||
const embeddedAssetsFile = path.join(SERVER_DIR, 'src/embedded-assets.generated.ts');
|
||||
await generateEmbeddedAssets(webDistDir, embeddedAssetsFile);
|
||||
console.log(' ✅ Assets embedded\n');
|
||||
|
||||
// 3.5. 生成嵌入式 WASM 资源
|
||||
console.log('📝 Embedding WASM files...');
|
||||
const embeddedWasmFile = path.join(SERVER_DIR, 'src/embedded-wasm.generated.ts');
|
||||
await generateEmbeddedWasm(embeddedWasmFile);
|
||||
console.log(' ✅ WASM embedded\n');
|
||||
|
||||
// 4. 构建 Server(使用嵌入式入口)
|
||||
console.log('📦 Building standalone server...');
|
||||
|
||||
// 确保输出目录存在
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
|
||||
await $`bun build ${SERVER_DIR}/src/bin/standalone.ts --compile --outfile ${output}`.quiet();
|
||||
console.log(' ✅ Server built\n');
|
||||
|
||||
// 5. 清理生成的文件
|
||||
if (fs.existsSync(embeddedAssetsFile)) {
|
||||
fs.unlinkSync(embeddedAssetsFile);
|
||||
}
|
||||
if (fs.existsSync(embeddedWasmFile)) {
|
||||
fs.unlinkSync(embeddedWasmFile);
|
||||
}
|
||||
|
||||
// 6. 显示结果
|
||||
const stats = fs.statSync(output);
|
||||
const sizeMB = (stats.size / 1024 / 1024).toFixed(1);
|
||||
|
||||
console.log('═══════════════════════════════════════════');
|
||||
console.log('✅ Build complete!');
|
||||
console.log('═══════════════════════════════════════════');
|
||||
console.log(` Output: ${output}`);
|
||||
console.log(` Size: ${sizeMB} MB`);
|
||||
console.log('');
|
||||
console.log('Usage:');
|
||||
console.log(` ${output} --help`);
|
||||
console.log(` ${output} --port 3000`);
|
||||
console.log('═══════════════════════════════════════════');
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Web dist 目录的所有文件转换为嵌入式 TypeScript 模块
|
||||
*/
|
||||
async function generateEmbeddedAssets(webDistDir: string, outputFile: string): Promise<void> {
|
||||
const assets: Map<string, { content: string; mimeType: string }> = new Map();
|
||||
|
||||
// 递归收集所有文件
|
||||
function collectFiles(dir: string, basePath: string = '') {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
const relativePath = basePath ? `${basePath}/${entry.name}` : entry.name;
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
collectFiles(fullPath, relativePath);
|
||||
} else {
|
||||
const content = fs.readFileSync(fullPath);
|
||||
const base64 = content.toString('base64');
|
||||
const mimeType = getMimeType(entry.name);
|
||||
assets.set('/' + relativePath, { content: base64, mimeType });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
collectFiles(webDistDir);
|
||||
|
||||
// 生成 TypeScript 代码
|
||||
const lines: string[] = [
|
||||
'// Auto-generated by scripts/build-standalone.ts',
|
||||
'// Do not edit manually',
|
||||
'',
|
||||
'export interface EmbeddedAsset {',
|
||||
' content: string; // base64 encoded',
|
||||
' mimeType: string;',
|
||||
'}',
|
||||
'',
|
||||
'export const EMBEDDED_ASSETS: Map<string, EmbeddedAsset> = new Map([',
|
||||
];
|
||||
|
||||
for (const [path, asset] of assets) {
|
||||
// 转义特殊字符
|
||||
const escapedContent = asset.content;
|
||||
lines.push(` ['${path}', { content: '${escapedContent}', mimeType: '${asset.mimeType}' }],`);
|
||||
}
|
||||
|
||||
lines.push(']);');
|
||||
lines.push('');
|
||||
lines.push('export function getAsset(path: string): EmbeddedAsset | undefined {');
|
||||
lines.push(' return EMBEDDED_ASSETS.get(path);');
|
||||
lines.push('}');
|
||||
lines.push('');
|
||||
lines.push('export function decodeAsset(asset: EmbeddedAsset): Uint8Array {');
|
||||
lines.push(' const binaryString = atob(asset.content);');
|
||||
lines.push(' const bytes = new Uint8Array(binaryString.length);');
|
||||
lines.push(' for (let i = 0; i < binaryString.length; i++) {');
|
||||
lines.push(' bytes[i] = binaryString.charCodeAt(i);');
|
||||
lines.push(' }');
|
||||
lines.push(' return bytes;');
|
||||
lines.push('}');
|
||||
lines.push('');
|
||||
|
||||
fs.writeFileSync(outputFile, lines.join('\n'), 'utf-8');
|
||||
console.log(` Generated ${assets.size} embedded assets`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据文件扩展名获取 MIME 类型
|
||||
*/
|
||||
function getMimeType(filename: string): string {
|
||||
const ext = path.extname(filename).toLowerCase();
|
||||
const mimeTypes: Record<string, string> = {
|
||||
'.html': 'text/html',
|
||||
'.css': 'text/css',
|
||||
'.js': 'application/javascript',
|
||||
'.json': 'application/json',
|
||||
'.svg': 'image/svg+xml',
|
||||
'.png': 'image/png',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.jpeg': 'image/jpeg',
|
||||
'.gif': 'image/gif',
|
||||
'.ico': 'image/x-icon',
|
||||
'.woff': 'font/woff',
|
||||
'.woff2': 'font/woff2',
|
||||
'.ttf': 'font/ttf',
|
||||
'.eot': 'application/vnd.ms-fontobject',
|
||||
'.webmanifest': 'application/manifest+json',
|
||||
'.txt': 'text/plain',
|
||||
'.xml': 'application/xml',
|
||||
};
|
||||
return mimeTypes[ext] || 'application/octet-stream';
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成嵌入式 WASM 模块
|
||||
* 包含 tree-sitter.wasm 和可用的语言 WASM 文件
|
||||
*/
|
||||
async function generateEmbeddedWasm(outputFile: string): Promise<void> {
|
||||
const wasmFiles: Map<string, string> = new Map();
|
||||
|
||||
// 查找 web-tree-sitter 的 tree-sitter.wasm
|
||||
const treeSitterWasmPaths = [
|
||||
path.join(NODE_MODULES, '.pnpm/web-tree-sitter@0.25.10/node_modules/web-tree-sitter/tree-sitter.wasm'),
|
||||
path.join(NODE_MODULES, 'web-tree-sitter/tree-sitter.wasm'),
|
||||
];
|
||||
|
||||
for (const wasmPath of treeSitterWasmPaths) {
|
||||
if (fs.existsSync(wasmPath)) {
|
||||
const content = fs.readFileSync(wasmPath);
|
||||
wasmFiles.set('tree-sitter.wasm', content.toString('base64'));
|
||||
console.log(` Found tree-sitter.wasm (${(content.length / 1024).toFixed(1)} KB)`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 查找语言特定的 WASM 文件
|
||||
const languageWasmPatterns = [
|
||||
// pnpm 格式
|
||||
{ dir: path.join(NODE_MODULES, '.pnpm'), pattern: /tree-sitter-(\w+)@.*\/tree-sitter-\1\.wasm$/ },
|
||||
];
|
||||
|
||||
// 扫描 node_modules/.pnpm 查找语言 WASM
|
||||
const pnpmDir = path.join(NODE_MODULES, '.pnpm');
|
||||
if (fs.existsSync(pnpmDir)) {
|
||||
const entries = fs.readdirSync(pnpmDir);
|
||||
for (const entry of entries) {
|
||||
if (entry.startsWith('tree-sitter-') && !entry.startsWith('tree-sitter-wasms')) {
|
||||
// 提取语言名称
|
||||
const match = entry.match(/^tree-sitter-(\w+)@/);
|
||||
if (match) {
|
||||
const lang = match[1];
|
||||
const wasmPath = path.join(pnpmDir, entry, 'node_modules', `tree-sitter-${lang}`, `tree-sitter-${lang}.wasm`);
|
||||
if (fs.existsSync(wasmPath)) {
|
||||
const content = fs.readFileSync(wasmPath);
|
||||
wasmFiles.set(`tree-sitter-${lang}.wasm`, content.toString('base64'));
|
||||
console.log(` Found tree-sitter-${lang}.wasm (${(content.length / 1024).toFixed(1)} KB)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (wasmFiles.size === 0) {
|
||||
console.warn(' ⚠️ No WASM files found');
|
||||
}
|
||||
|
||||
// 生成 TypeScript 代码
|
||||
const lines: string[] = [
|
||||
'// Auto-generated by scripts/build-standalone.ts',
|
||||
'// Do not edit manually',
|
||||
'',
|
||||
'/**',
|
||||
' * Embedded WASM files for tree-sitter',
|
||||
' * Keys: wasm filename, Values: base64 encoded content',
|
||||
' */',
|
||||
'export const EMBEDDED_WASM: Map<string, string> = new Map([',
|
||||
];
|
||||
|
||||
for (const [name, base64] of wasmFiles) {
|
||||
lines.push(` ['${name}', '${base64}'],`);
|
||||
}
|
||||
|
||||
lines.push(']);');
|
||||
lines.push('');
|
||||
|
||||
fs.writeFileSync(outputFile, lines.join('\n'), 'utf-8');
|
||||
console.log(` Generated ${wasmFiles.size} embedded WASM files`);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('❌ Build failed:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user