first commit
This commit is contained in:
147
app/native-server/README.md
Normal file
147
app/native-server/README.md
Normal file
@@ -0,0 +1,147 @@
|
||||
# Fastify Chrome Native Messaging服务
|
||||
|
||||
这是一个基于Fastify的TypeScript项目,用于与Chrome扩展进行原生通信。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 通过Chrome Native Messaging协议与Chrome扩展进行双向通信
|
||||
- 提供RESTful API服务
|
||||
- 完全使用TypeScript开发
|
||||
- 包含完整的测试套件
|
||||
- 遵循代码质量最佳实践
|
||||
|
||||
## 开发环境设置
|
||||
|
||||
### 前置条件
|
||||
|
||||
- Node.js 14+
|
||||
- npm 6+
|
||||
|
||||
### 安装
|
||||
|
||||
```bash
|
||||
git clone https://github.com/your-username/fastify-chrome-native.git
|
||||
cd fastify-chrome-native
|
||||
npm install
|
||||
```
|
||||
|
||||
### 开发
|
||||
|
||||
1. 本地构建注册native server
|
||||
```bash
|
||||
cd app/native-server
|
||||
npm run dev
|
||||
```
|
||||
2. 启动chrome extension
|
||||
```bash
|
||||
cd app/chrome-extension
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### 构建
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
### 注册Native Messaging主机
|
||||
|
||||
全局安装后会自动注册
|
||||
```bash
|
||||
npm i -g mcp-chrome-bridge
|
||||
```
|
||||
|
||||
### 与Chrome扩展集成
|
||||
|
||||
以下是Chrome扩展中如何使用此服务的简单示例:
|
||||
|
||||
```javascript
|
||||
// background.js
|
||||
let nativePort = null;
|
||||
let serverRunning = false;
|
||||
|
||||
// 启动Native Messaging服务
|
||||
function startServer() {
|
||||
if (nativePort) {
|
||||
console.log('已连接到Native Messaging主机');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
nativePort = chrome.runtime.connectNative('com.yourcompany.fastify_native_host');
|
||||
|
||||
nativePort.onMessage.addListener(message => {
|
||||
console.log('收到Native消息:', message);
|
||||
|
||||
if (message.type === 'started') {
|
||||
serverRunning = true;
|
||||
console.log(`服务已启动,端口: ${message.payload.port}`);
|
||||
} else if (message.type === 'stopped') {
|
||||
serverRunning = false;
|
||||
console.log('服务已停止');
|
||||
} else if (message.type === 'error') {
|
||||
console.error('Native错误:', message.payload.message);
|
||||
}
|
||||
});
|
||||
|
||||
nativePort.onDisconnect.addListener(() => {
|
||||
console.log('Native连接断开:', chrome.runtime.lastError);
|
||||
nativePort = null;
|
||||
serverRunning = false;
|
||||
});
|
||||
|
||||
// 启动服务器
|
||||
nativePort.postMessage({ type: 'start', payload: { port: 3000 } });
|
||||
|
||||
} catch (error) {
|
||||
console.error('启动Native Messaging时出错:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 停止服务器
|
||||
function stopServer() {
|
||||
if (nativePort && serverRunning) {
|
||||
nativePort.postMessage({ type: 'stop' });
|
||||
}
|
||||
}
|
||||
|
||||
// 测试与服务器的通信
|
||||
async function testPing() {
|
||||
try {
|
||||
const response = await fetch('http://localhost:3000/ping');
|
||||
const data = await response.json();
|
||||
console.log('Ping响应:', data);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Ping失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 在扩展启动时连接Native主机
|
||||
chrome.runtime.onStartup.addListener(startServer);
|
||||
|
||||
// 导出供popup或内容脚本使用的API
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (message.action === 'startServer') {
|
||||
startServer();
|
||||
sendResponse({ success: true });
|
||||
} else if (message.action === 'stopServer') {
|
||||
stopServer();
|
||||
sendResponse({ success: true });
|
||||
} else if (message.action === 'testPing') {
|
||||
testPing().then(sendResponse);
|
||||
return true; // 指示我们将异步发送响应
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 测试
|
||||
|
||||
```bash
|
||||
npm run test
|
||||
```
|
||||
|
||||
### 许可证
|
||||
|
||||
MIT
|
64
app/native-server/debug.sh
Normal file
64
app/native-server/debug.sh
Normal file
@@ -0,0 +1,64 @@
|
||||
#!/bin/bash
|
||||
# 获取脚本所在的绝对目录
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
LOG_DIR="/Users/hang/code/tencent/ai/chrome-mcp-server/app/native-server/dist/logs" # 或者你选择的、确定有写入权限的目录
|
||||
|
||||
# 获取当前时间戳用于日志文件名,避免覆盖
|
||||
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
|
||||
WRAPPER_LOG="${LOG_DIR}/native_host_wrapper_${TIMESTAMP}.log"
|
||||
|
||||
# Node.js 脚本的实际路径
|
||||
NODE_SCRIPT="${SCRIPT_DIR}/index.js"
|
||||
|
||||
# 确保日志目录存在
|
||||
mkdir -p "${LOG_DIR}"
|
||||
|
||||
# 记录 wrapper 脚本被调用的信息
|
||||
echo "Wrapper script called at $(date)" > "${WRAPPER_LOG}"
|
||||
echo "SCRIPT_DIR: ${SCRIPT_DIR}" >> "${WRAPPER_LOG}"
|
||||
echo "LOG_DIR: ${LOG_DIR}" >> "${WRAPPER_LOG}"
|
||||
echo "NODE_SCRIPT: ${NODE_SCRIPT}" >> "${WRAPPER_LOG}"
|
||||
echo "Initial PATH: ${PATH}" >> "${WRAPPER_LOG}"
|
||||
|
||||
# 动态查找 Node.js 可执行文件
|
||||
NODE_EXEC=""
|
||||
# 1. 尝试用 which (它会使用当前环境的 PATH, 但 Chrome 的 PATH 可能不完整)
|
||||
if command -v node &>/dev/null; then
|
||||
NODE_EXEC=$(command -v node)
|
||||
echo "Found node using 'command -v node': ${NODE_EXEC}" >> "${WRAPPER_LOG}"
|
||||
fi
|
||||
|
||||
# 2. 如果 which 找不到,尝试一些 macOS 上常见的 Node.js 安装路径
|
||||
if [ -z "${NODE_EXEC}" ]; then
|
||||
COMMON_NODE_PATHS=(
|
||||
"/usr/local/bin/node" # Homebrew on Intel Macs / direct install
|
||||
"/opt/homebrew/bin/node" # Homebrew on Apple Silicon
|
||||
"$HOME/.nvm/versions/node/$(ls -t $HOME/.nvm/versions/node | head -n 1)/bin/node" # NVM (latest installed)
|
||||
# 你可以根据需要添加更多你环境中可能存在的路径
|
||||
)
|
||||
for path_to_node in "${COMMON_NODE_PATHS[@]}"; do
|
||||
if [ -x "${path_to_node}" ]; then
|
||||
NODE_EXEC="${path_to_node}"
|
||||
echo "Found node at common path: ${NODE_EXEC}" >> "${WRAPPER_LOG}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# 3. 如果还是找不到,记录错误并退出
|
||||
if [ -z "${NODE_EXEC}" ]; then
|
||||
echo "ERROR: Node.js executable not found!" >> "${WRAPPER_LOG}"
|
||||
echo "Please ensure Node.js is installed and its path is accessible or configured in this script." >> "${WRAPPER_LOG}"
|
||||
# 对于 Native Host,它需要保持运行以接收消息,直接退出可能不是最佳
|
||||
# 但如果node都找不到,也无法执行目标脚本
|
||||
# 这里可以考虑输出一个符合 Native Messaging 协议的错误消息给扩展(如果可以的话)
|
||||
# 或者就让它失败,Chrome会报告 Native Host Exited.
|
||||
exit 1 # 必须退出,否则下面的 exec 会失败
|
||||
fi
|
||||
|
||||
echo "Using Node executable: ${NODE_EXEC}" >> "${WRAPPER_LOG}"
|
||||
echo "Node version found by script: $(${NODE_EXEC} -v)" >> "${WRAPPER_LOG}"
|
||||
echo "Executing: ${NODE_EXEC} ${NODE_SCRIPT}" >> "${WRAPPER_LOG}"
|
||||
echo "PWD: $(pwd)" >> "${WRAPPER_LOG}" # PWD 记录一下,有时有用
|
||||
|
||||
exec "${NODE_EXEC}" "${NODE_SCRIPT}" 2>> "${LOG_DIR}/native_host_stderr_${TIMESTAMP}.log"
|
325
app/native-server/install.md
Normal file
325
app/native-server/install.md
Normal file
@@ -0,0 +1,325 @@
|
||||
# Chrome MCP Bridge 安装指南
|
||||
|
||||
本文档详细说明了 Chrome MCP Bridge 的安装和注册流程。
|
||||
|
||||
## 安装流程概述
|
||||
|
||||
Chrome MCP Bridge 的安装和注册流程如下:
|
||||
|
||||
```
|
||||
npm install -g chrome-mcp-bridge
|
||||
└─ postinstall.js
|
||||
├─ 复制可执行文件到 npm_prefix/bin ← 总是可写(用户或root权限)
|
||||
├─ 尝试用户级别注册 ← 无需sudo,大多数情况下成功
|
||||
└─ 如果失败 ➜ 提示用户运行 chrome-mcp-bridge register --system
|
||||
└─ 使用sudo-prompt自动提权 → 写入系统级清单文件
|
||||
```
|
||||
|
||||
上面的流程图展示了从全局安装开始,到最终完成注册的完整过程。
|
||||
|
||||
## 详细安装步骤
|
||||
|
||||
### 1. 全局安装
|
||||
|
||||
```bash
|
||||
npm install -g chrome-mcp-bridge
|
||||
```
|
||||
|
||||
安装完成后,系统会自动尝试在用户目录中注册 Native Messaging 主机。这不需要管理员权限,是推荐的安装方式。
|
||||
|
||||
### 2. 用户级别注册
|
||||
|
||||
用户级别注册会在以下位置创建清单文件:
|
||||
|
||||
```
|
||||
清单文件位置
|
||||
├─ 用户级别(无需管理员权限)
|
||||
│ ├─ Windows: %APPDATA%\Google\Chrome\NativeMessagingHosts\
|
||||
│ ├─ macOS: ~/Library/Application Support/Google/Chrome/NativeMessagingHosts/
|
||||
│ └─ Linux: ~/.config/google-chrome/NativeMessagingHosts/
|
||||
│
|
||||
└─ 系统级别(需要管理员权限)
|
||||
├─ Windows: %ProgramFiles%\Google\Chrome\NativeMessagingHosts\
|
||||
├─ macOS: /Library/Google/Chrome/NativeMessagingHosts/
|
||||
└─ Linux: /etc/opt/chrome/native-messaging-hosts/
|
||||
```
|
||||
|
||||
如果自动注册失败,或者您想手动注册,可以运行:
|
||||
|
||||
```bash
|
||||
chrome-mcp-bridge register
|
||||
```
|
||||
|
||||
### 3. 系统级别注册
|
||||
|
||||
如果用户级别注册失败(例如,由于权限问题),您可以尝试系统级别注册。系统级别注册需要管理员权限,但我们提供了两种便捷的方式来完成这一过程。
|
||||
|
||||
系统级别注册有两种方式:
|
||||
|
||||
#### 方式一:使用 `--system` 参数(推荐)
|
||||
|
||||
```bash
|
||||
chrome-mcp-bridge register --system
|
||||
```
|
||||
|
||||
这将使用 `sudo-prompt` 自动提升权限,无需手动输入 `sudo` 命令。
|
||||
|
||||
#### 方式二:直接使用管理员权限
|
||||
|
||||
**Windows**:
|
||||
以管理员身份运行命令提示符或 PowerShell,然后执行:
|
||||
|
||||
```
|
||||
chrome-mcp-bridge register
|
||||
```
|
||||
|
||||
**macOS/Linux**:
|
||||
使用 sudo 命令:
|
||||
|
||||
```
|
||||
sudo chrome-mcp-bridge register
|
||||
```
|
||||
|
||||
## 注册流程详解
|
||||
|
||||
### 注册流程图
|
||||
|
||||
```
|
||||
注册流程
|
||||
├─ 用户级别注册 (chrome-mcp-bridge register)
|
||||
│ ├─ 获取用户级别清单路径
|
||||
│ ├─ 创建用户目录
|
||||
│ ├─ 生成清单内容
|
||||
│ ├─ 写入清单文件
|
||||
│ └─ Windows平台:创建用户级注册表项
|
||||
│
|
||||
└─ 系统级别注册 (chrome-mcp-bridge register --system)
|
||||
├─ 检查是否有管理员权限
|
||||
│ ├─ 有权限 → 直接创建系统目录和写入清单
|
||||
│ └─ 无权限 → 使用sudo-prompt提权
|
||||
│ ├─ 创建临时清单文件
|
||||
│ └─ 复制到系统目录
|
||||
└─ Windows平台:创建系统级注册表项
|
||||
```
|
||||
|
||||
### 清单文件结构
|
||||
|
||||
```
|
||||
manifest.json
|
||||
├─ name: "com.chrome-mcp.native-host"
|
||||
├─ description: "Node.js Host for Browser Bridge Extension"
|
||||
├─ path: "/path/to/node" ← Node.js可执行文件路径
|
||||
├─ type: "stdio" ← 通信类型
|
||||
├─ allowed_origins: [ ← 允许连接的扩展
|
||||
│ "chrome-extension://扩展ID/"
|
||||
└─ args: [ ← 启动参数
|
||||
"/path/to/chrome-mcp-bridge",
|
||||
"native"
|
||||
]
|
||||
```
|
||||
|
||||
### 用户级别注册流程
|
||||
|
||||
1. 确定用户级别清单文件路径
|
||||
2. 创建必要的目录
|
||||
3. 生成清单内容,包括:
|
||||
- 主机名称
|
||||
- 描述
|
||||
- Node.js 可执行文件路径
|
||||
- 通信类型(stdio)
|
||||
- 允许的扩展 ID
|
||||
- 启动参数
|
||||
4. 写入清单文件
|
||||
5. 在 Windows 上,还会创建相应的注册表项
|
||||
|
||||
### 系统级别注册流程
|
||||
|
||||
1. 检测是否已有管理员权限
|
||||
2. 如果已有管理员权限:
|
||||
- 直接创建系统级目录
|
||||
- 写入清单文件
|
||||
- 设置适当的权限
|
||||
- 在 Windows 上创建系统级注册表项
|
||||
3. 如果没有管理员权限:
|
||||
- 使用 `sudo-prompt` 提升权限
|
||||
- 创建临时清单文件
|
||||
- 复制到系统目录
|
||||
- 在 Windows 上创建系统级注册表项
|
||||
|
||||
## 验证安装
|
||||
|
||||
### 验证流程图
|
||||
|
||||
```
|
||||
验证安装
|
||||
├─ 检查清单文件
|
||||
│ ├─ 文件存在 → 检查内容是否正确
|
||||
│ └─ 文件不存在 → 重新安装
|
||||
│
|
||||
├─ 检查Chrome扩展
|
||||
│ ├─ 扩展已安装 → 检查扩展权限
|
||||
│ └─ 扩展未安装 → 安装扩展
|
||||
│
|
||||
└─ 测试连接
|
||||
├─ 连接成功 → 安装完成
|
||||
└─ 连接失败 → 检查错误日志 → 参考故障排除
|
||||
```
|
||||
|
||||
### 验证步骤
|
||||
|
||||
安装完成后,您可以通过以下方式验证安装是否成功:
|
||||
|
||||
1. 检查清单文件是否存在于相应目录
|
||||
|
||||
- 用户级别:检查用户目录下的清单文件
|
||||
- 系统级别:检查系统目录下的清单文件
|
||||
- 确认清单文件内容是否正确
|
||||
|
||||
2. 在 Chrome 中安装对应的扩展
|
||||
|
||||
- 确保扩展已正确安装
|
||||
- 确保扩展有 `nativeMessaging` 权限
|
||||
|
||||
3. 尝试通过扩展连接到本地服务
|
||||
- 使用扩展的测试功能尝试连接
|
||||
- 检查 Chrome 的扩展日志是否有错误信息
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 故障排除流程图
|
||||
|
||||
```
|
||||
故障排除
|
||||
├─ 权限问题
|
||||
│ ├─ 检查用户权限
|
||||
│ │ ├─ 有足够权限 → 检查目录权限
|
||||
│ │ └─ 无足够权限 → 尝试系统级别安装
|
||||
│ │
|
||||
│ ├─ 执行权限问题 (macOS/Linux)
|
||||
│ │ ├─ "Permission denied" 错误
|
||||
│ │ ├─ "Native host has exited" 错误
|
||||
│ │ └─ 运行 chrome-mcp-bridge fix-permissions
|
||||
│ │
|
||||
│ └─ 尝试 chrome-mcp-bridge register --system
|
||||
│
|
||||
├─ 路径问题
|
||||
│ ├─ 检查Node.js安装 (node -v)
|
||||
│ └─ 检查全局NPM路径 (npm root -g)
|
||||
│
|
||||
├─ 注册表问题 (Windows)
|
||||
│ ├─ 检查注册表访问权限
|
||||
│ └─ 尝试手动创建注册表项
|
||||
│
|
||||
└─ 其他问题
|
||||
├─ 检查控制台错误信息
|
||||
└─ 提交Issue到项目仓库
|
||||
```
|
||||
|
||||
### 常见问题解决步骤
|
||||
|
||||
如果安装过程中遇到问题,请尝试以下步骤:
|
||||
|
||||
1. 确保 Node.js 已正确安装
|
||||
|
||||
- 运行 `node -v` 和 `npm -v` 检查版本
|
||||
- 确保 Node.js 版本 >= 14.x
|
||||
|
||||
2. 检查是否有足够的权限创建文件和目录
|
||||
|
||||
- 用户级别安装需要对用户目录有写入权限
|
||||
- 系统级别安装需要管理员/root权限
|
||||
|
||||
3. **修复执行权限问题**
|
||||
|
||||
**macOS/Linux 平台**:
|
||||
|
||||
**问题描述**:
|
||||
|
||||
- npm 安装通常会保留文件权限,但 pnpm 可能不会
|
||||
- 可能遇到 "Permission denied" 或 "Native host has exited" 错误
|
||||
- Chrome 扩展无法启动 native host 进程
|
||||
|
||||
**解决方案**:
|
||||
|
||||
a) **使用内置修复命令(推荐)**:
|
||||
|
||||
```bash
|
||||
chrome-mcp-bridge fix-permissions
|
||||
```
|
||||
|
||||
b) **手动设置权限**:
|
||||
|
||||
```bash
|
||||
# 查找安装路径
|
||||
npm list -g chrome-mcp-bridge
|
||||
# 或者对于 pnpm
|
||||
pnpm list -g chrome-mcp-bridge
|
||||
|
||||
# 设置执行权限(替换为实际路径)
|
||||
chmod +x /path/to/node_modules/chrome-mcp-bridge/run_host.sh
|
||||
chmod +x /path/to/node_modules/chrome-mcp-bridge/index.js
|
||||
chmod +x /path/to/node_modules/chrome-mcp-bridge/cli.js
|
||||
```
|
||||
|
||||
**Windows 平台**:
|
||||
|
||||
**问题描述**:
|
||||
|
||||
- Windows 上 `.bat` 文件通常不需要执行权限,但可能遇到其他问题
|
||||
- 文件可能被标记为只读
|
||||
- 可能遇到 "Access denied" 或文件无法执行的错误
|
||||
|
||||
**解决方案**:
|
||||
|
||||
a) **使用内置修复命令(推荐)**:
|
||||
|
||||
```cmd
|
||||
chrome-mcp-bridge fix-permissions
|
||||
```
|
||||
|
||||
b) **手动检查文件属性**:
|
||||
|
||||
```cmd
|
||||
# 查找安装路径
|
||||
npm list -g chrome-mcp-bridge
|
||||
|
||||
# 检查文件属性(在文件资源管理器中右键 -> 属性)
|
||||
# 确保 run_host.bat 不是只读文件
|
||||
```
|
||||
|
||||
c) **重新安装并强制权限**:
|
||||
|
||||
```bash
|
||||
# 卸载
|
||||
npm uninstall -g chrome-mcp-bridge
|
||||
# 或 pnpm uninstall -g chrome-mcp-bridge
|
||||
|
||||
# 重新安装
|
||||
npm install -g chrome-mcp-bridge
|
||||
# 或 pnpm install -g chrome-mcp-bridge
|
||||
|
||||
# 如果仍有问题,运行权限修复
|
||||
chrome-mcp-bridge fix-permissions
|
||||
```
|
||||
|
||||
4. 在 Windows 上,确保注册表访问没有被限制
|
||||
|
||||
- 检查是否可以访问 `HKCU\Software\Google\Chrome\NativeMessagingHosts\`
|
||||
- 对于系统级别,检查 `HKLM\Software\Google\Chrome\NativeMessagingHosts\`
|
||||
|
||||
5. 尝试使用系统级别安装
|
||||
|
||||
- 使用 `chrome-mcp-bridge register --system` 命令
|
||||
- 或直接使用管理员权限运行
|
||||
|
||||
6. 检查控制台输出的错误信息
|
||||
- 详细的错误信息通常会指出问题所在
|
||||
- 可以添加 `--verbose` 参数获取更多日志信息
|
||||
|
||||
如果问题仍然存在,请提交 issue 到项目仓库,并附上以下信息:
|
||||
|
||||
- 操作系统版本
|
||||
- Node.js 版本
|
||||
- 安装命令
|
||||
- 错误信息
|
||||
- 尝试过的解决方法
|
17
app/native-server/jest.config.js
Normal file
17
app/native-server/jest.config.js
Normal file
@@ -0,0 +1,17 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
roots: ['<rootDir>/src'],
|
||||
testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
|
||||
collectCoverage: true,
|
||||
collectCoverageFrom: ['src/**/*.ts', '!src/**/*.d.ts', '!src/scripts/**/*'],
|
||||
coverageDirectory: 'coverage',
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
branches: 70,
|
||||
functions: 80,
|
||||
lines: 80,
|
||||
statements: 80,
|
||||
},
|
||||
},
|
||||
};
|
78
app/native-server/package.json
Normal file
78
app/native-server/package.json
Normal file
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"name": "mcp-chrome-bridge",
|
||||
"version": "1.0.29",
|
||||
"description": "Chrome Native-Messaging host (Node)",
|
||||
"main": "dist/index.js",
|
||||
"bin": {
|
||||
"mcp-chrome-bridge": "./dist/cli.js",
|
||||
"mcp-chrome-stdio": "./dist/mcp/mcp-server-stdio.js"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "nodemon --watch src --ext ts,js,json --ignore dist/ --exec \"npm run build && npm run register:dev\"",
|
||||
"build": "ts-node src/scripts/build.ts",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"lint": "eslint 'src/**/*.{js,ts}'",
|
||||
"lint:fix": "eslint 'src/**/*.{js,ts}' --fix",
|
||||
"format": "prettier --write 'src/**/*.{js,ts,json}'",
|
||||
"register:dev": "node dist/scripts/register-dev.js",
|
||||
"postinstall": "node dist/scripts/postinstall.js"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"preferGlobal": true,
|
||||
"keywords": [
|
||||
"mcp",
|
||||
"chrome",
|
||||
"browser"
|
||||
],
|
||||
"author": "hangye",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fastify/cors": "^11.0.1",
|
||||
"@modelcontextprotocol/sdk": "^1.11.0",
|
||||
"chalk": "^5.4.1",
|
||||
"chrome-mcp-shared": "workspace:*",
|
||||
"commander": "^13.1.0",
|
||||
"fastify": "^5.3.2",
|
||||
"is-admin": "^4.0.0",
|
||||
"pino": "^9.6.0",
|
||||
"uuid": "^11.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "^29.7.0",
|
||||
"@types/chrome": "^0.0.318",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^22.15.3",
|
||||
"@types/supertest": "^6.0.3",
|
||||
"@typescript-eslint/parser": "^8.31.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"husky": "^9.1.7",
|
||||
"jest": "^29.7.0",
|
||||
"lint-staged": "^15.5.1",
|
||||
"nodemon": "^3.1.10",
|
||||
"pino-pretty": "^13.0.0",
|
||||
"rimraf": "^6.0.1",
|
||||
"supertest": "^7.1.0",
|
||||
"ts-jest": "^29.3.2",
|
||||
"ts-node": "^10.9.2"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,ts}": [
|
||||
"eslint --fix",
|
||||
"prettier --write"
|
||||
],
|
||||
"*.{json,md}": [
|
||||
"prettier --write"
|
||||
]
|
||||
}
|
||||
}
|
158
app/native-server/src/cli.ts
Normal file
158
app/native-server/src/cli.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { program } from 'commander';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import {
|
||||
tryRegisterUserLevelHost,
|
||||
colorText,
|
||||
registerWithElevatedPermissions,
|
||||
ensureExecutionPermissions,
|
||||
} from './scripts/utils';
|
||||
|
||||
// Import writeNodePath from postinstall
|
||||
async function writeNodePath(): Promise<void> {
|
||||
try {
|
||||
const nodePath = process.execPath;
|
||||
const nodePathFile = path.join(__dirname, 'node_path.txt');
|
||||
|
||||
console.log(colorText(`Writing Node.js path: ${nodePath}`, 'blue'));
|
||||
fs.writeFileSync(nodePathFile, nodePath, 'utf8');
|
||||
console.log(colorText('✓ Node.js path written for run_host scripts', 'green'));
|
||||
} catch (error: any) {
|
||||
console.warn(colorText(`⚠️ Failed to write Node.js path: ${error.message}`, 'yellow'));
|
||||
}
|
||||
}
|
||||
|
||||
program
|
||||
.version(require('../package.json').version)
|
||||
.description('Mcp Chrome Bridge - Local service for communicating with Chrome extension');
|
||||
|
||||
// Register Native Messaging host
|
||||
program
|
||||
.command('register')
|
||||
.description('Register Native Messaging host')
|
||||
.option('-f, --force', 'Force re-registration')
|
||||
.option('-s, --system', 'Use system-level installation (requires administrator/sudo privileges)')
|
||||
.action(async (options) => {
|
||||
try {
|
||||
// Write Node.js path for run_host scripts
|
||||
await writeNodePath();
|
||||
|
||||
// Detect if running with root/administrator privileges
|
||||
const isRoot = process.getuid && process.getuid() === 0; // Unix/Linux/Mac
|
||||
|
||||
let isAdmin = false;
|
||||
if (process.platform === 'win32') {
|
||||
try {
|
||||
isAdmin = require('is-admin')(); // Windows requires additional package
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
colorText('Warning: Unable to detect administrator privileges on Windows', 'yellow'),
|
||||
);
|
||||
isAdmin = false;
|
||||
}
|
||||
}
|
||||
|
||||
const hasElevatedPermissions = isRoot || isAdmin;
|
||||
|
||||
// If --system option is specified or running with root/administrator privileges
|
||||
if (options.system || hasElevatedPermissions) {
|
||||
await registerWithElevatedPermissions();
|
||||
console.log(
|
||||
colorText('System-level Native Messaging host registered successfully!', 'green'),
|
||||
);
|
||||
console.log(
|
||||
colorText(
|
||||
'You can now use connectNative in Chrome extension to connect to this service.',
|
||||
'blue',
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// Regular user-level installation
|
||||
console.log(colorText('Registering user-level Native Messaging host...', 'blue'));
|
||||
const success = await tryRegisterUserLevelHost();
|
||||
|
||||
if (success) {
|
||||
console.log(colorText('Native Messaging host registered successfully!', 'green'));
|
||||
console.log(
|
||||
colorText(
|
||||
'You can now use connectNative in Chrome extension to connect to this service.',
|
||||
'blue',
|
||||
),
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
colorText(
|
||||
'User-level registration failed, please try the following methods:',
|
||||
'yellow',
|
||||
),
|
||||
);
|
||||
console.log(colorText(' 1. sudo mcp-chrome-bridge register', 'yellow'));
|
||||
console.log(colorText(' 2. mcp-chrome-bridge register --system', 'yellow'));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(colorText(`Registration failed: ${error.message}`, 'red'));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
// Fix execution permissions
|
||||
program
|
||||
.command('fix-permissions')
|
||||
.description('Fix execution permissions for native host files')
|
||||
.action(async () => {
|
||||
try {
|
||||
console.log(colorText('Fixing execution permissions...', 'blue'));
|
||||
await ensureExecutionPermissions();
|
||||
console.log(colorText('✓ Execution permissions fixed successfully!', 'green'));
|
||||
} catch (error: any) {
|
||||
console.error(colorText(`Failed to fix permissions: ${error.message}`, 'red'));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
// Update port in stdio-config.json
|
||||
program
|
||||
.command('update-port <port>')
|
||||
.description('Update the port number in stdio-config.json')
|
||||
.action(async (port: string) => {
|
||||
try {
|
||||
const portNumber = parseInt(port, 10);
|
||||
if (isNaN(portNumber) || portNumber < 1 || portNumber > 65535) {
|
||||
console.error(colorText('Error: Port must be a valid number between 1 and 65535', 'red'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const configPath = path.join(__dirname, 'mcp', 'stdio-config.json');
|
||||
|
||||
if (!fs.existsSync(configPath)) {
|
||||
console.error(colorText(`Error: Configuration file not found at ${configPath}`, 'red'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const configData = fs.readFileSync(configPath, 'utf8');
|
||||
const config = JSON.parse(configData);
|
||||
|
||||
const currentUrl = new URL(config.url);
|
||||
currentUrl.port = portNumber.toString();
|
||||
config.url = currentUrl.toString();
|
||||
|
||||
fs.writeFileSync(configPath, JSON.stringify(config, null, 4));
|
||||
|
||||
console.log(colorText(`✓ Port updated successfully to ${portNumber}`, 'green'));
|
||||
console.log(colorText(`Updated URL: ${config.url}`, 'blue'));
|
||||
} catch (error: any) {
|
||||
console.error(colorText(`Failed to update port: ${error.message}`, 'red'));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
program.parse(process.argv);
|
||||
|
||||
// If no command provided, show help
|
||||
if (!process.argv.slice(2).length) {
|
||||
program.outputHelp();
|
||||
}
|
47
app/native-server/src/constant/index.ts
Normal file
47
app/native-server/src/constant/index.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
export enum NATIVE_MESSAGE_TYPE {
|
||||
START = 'start',
|
||||
STARTED = 'started',
|
||||
STOP = 'stop',
|
||||
STOPPED = 'stopped',
|
||||
PING = 'ping',
|
||||
PONG = 'pong',
|
||||
ERROR = 'error',
|
||||
}
|
||||
|
||||
export const NATIVE_SERVER_PORT = 56889;
|
||||
|
||||
// Timeout constants (in milliseconds)
|
||||
export const TIMEOUTS = {
|
||||
DEFAULT_REQUEST_TIMEOUT: 15000,
|
||||
EXTENSION_REQUEST_TIMEOUT: 20000,
|
||||
PROCESS_DATA_TIMEOUT: 20000,
|
||||
} as const;
|
||||
|
||||
// Server configuration
|
||||
export const SERVER_CONFIG = {
|
||||
HOST: '127.0.0.1',
|
||||
CORS_ORIGIN: true,
|
||||
LOGGER_ENABLED: false,
|
||||
} as const;
|
||||
|
||||
// HTTP Status codes
|
||||
export const HTTP_STATUS = {
|
||||
OK: 200,
|
||||
NO_CONTENT: 204,
|
||||
BAD_REQUEST: 400,
|
||||
INTERNAL_SERVER_ERROR: 500,
|
||||
GATEWAY_TIMEOUT: 504,
|
||||
} as const;
|
||||
|
||||
// Error messages
|
||||
export const ERROR_MESSAGES = {
|
||||
NATIVE_HOST_NOT_AVAILABLE: 'Native host connection not established.',
|
||||
SERVER_NOT_RUNNING: 'Server is not actively running.',
|
||||
REQUEST_TIMEOUT: 'Request to extension timed out.',
|
||||
INVALID_MCP_REQUEST: 'Invalid MCP request or session.',
|
||||
INVALID_SESSION_ID: 'Invalid or missing MCP session ID.',
|
||||
INTERNAL_SERVER_ERROR: 'Internal Server Error',
|
||||
MCP_SESSION_DELETION_ERROR: 'Internal server error during MCP session deletion.',
|
||||
MCP_REQUEST_PROCESSING_ERROR: 'Internal server error during MCP request processing.',
|
||||
INVALID_SSE_SESSION: 'Invalid or missing MCP session ID for SSE.',
|
||||
} as const;
|
35
app/native-server/src/index.ts
Normal file
35
app/native-server/src/index.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env node
|
||||
import serverInstance from './server';
|
||||
import nativeMessagingHostInstance from './native-messaging-host';
|
||||
|
||||
try {
|
||||
serverInstance.setNativeHost(nativeMessagingHostInstance); // Server needs setNativeHost method
|
||||
nativeMessagingHostInstance.setServer(serverInstance); // NativeHost needs setServer method
|
||||
nativeMessagingHostInstance.start();
|
||||
} catch (error) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
process.on('error', (error) => {
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Handle process signals and uncaught exceptions
|
||||
process.on('SIGINT', () => {
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('exit', (code) => {
|
||||
});
|
||||
|
||||
process.on('uncaughtException', (error) => {
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', (reason) => {
|
||||
// Don't exit immediately, let the program continue running
|
||||
});
|
113
app/native-server/src/mcp/mcp-server-stdio.ts
Normal file
113
app/native-server/src/mcp/mcp-server-stdio.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
CallToolResult,
|
||||
ListToolsRequestSchema,
|
||||
} from '@modelcontextprotocol/sdk/types.js';
|
||||
import { TOOL_SCHEMAS } from 'chrome-mcp-shared';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
let stdioMcpServer: Server | null = null;
|
||||
let mcpClient: Client | null = null;
|
||||
|
||||
// Read configuration from stdio-config.json
|
||||
const loadConfig = () => {
|
||||
try {
|
||||
const configPath = path.join(__dirname, 'stdio-config.json');
|
||||
const configData = fs.readFileSync(configPath, 'utf8');
|
||||
return JSON.parse(configData);
|
||||
} catch (error) {
|
||||
console.error('Failed to load stdio-config.json:', error);
|
||||
throw new Error('Configuration file stdio-config.json not found or invalid');
|
||||
}
|
||||
};
|
||||
|
||||
export const getStdioMcpServer = () => {
|
||||
if (stdioMcpServer) {
|
||||
return stdioMcpServer;
|
||||
}
|
||||
stdioMcpServer = new Server(
|
||||
{
|
||||
name: 'StdioChromeMcpServer',
|
||||
version: '1.0.0',
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
setupTools(stdioMcpServer);
|
||||
return stdioMcpServer;
|
||||
};
|
||||
|
||||
export const ensureMcpClient = async () => {
|
||||
try {
|
||||
if (mcpClient) {
|
||||
const pingResult = await mcpClient.ping();
|
||||
if (pingResult) {
|
||||
return mcpClient;
|
||||
}
|
||||
}
|
||||
|
||||
const config = loadConfig();
|
||||
mcpClient = new Client({ name: 'Mcp Chrome Proxy', version: '1.0.0' }, { capabilities: {} });
|
||||
const transport = new StreamableHTTPClientTransport(new URL(config.url), {});
|
||||
await mcpClient.connect(transport);
|
||||
return mcpClient;
|
||||
} catch (error) {
|
||||
mcpClient?.close();
|
||||
mcpClient = null;
|
||||
console.error('Failed to connect to MCP server:', error);
|
||||
}
|
||||
};
|
||||
|
||||
export const setupTools = (server: Server) => {
|
||||
// List tools handler
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOL_SCHEMAS }));
|
||||
|
||||
// Call tool handler
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) =>
|
||||
handleToolCall(request.params.name, request.params.arguments || {}),
|
||||
);
|
||||
};
|
||||
|
||||
const handleToolCall = async (name: string, args: any): Promise<CallToolResult> => {
|
||||
try {
|
||||
const client = await ensureMcpClient();
|
||||
if (!client) {
|
||||
throw new Error('Failed to connect to MCP server');
|
||||
}
|
||||
const result = await client.callTool({ name, arguments: args }, undefined, {
|
||||
timeout: 2 * 6 * 1000, // Default timeout of 2 minute
|
||||
});
|
||||
return result as CallToolResult;
|
||||
} catch (error: any) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Error calling tool: ${error.message}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
async function main() {
|
||||
const transport = new StdioServerTransport();
|
||||
await getStdioMcpServer().connect(transport);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('Fatal error Chrome MCP Server main():', error);
|
||||
process.exit(1);
|
||||
});
|
24
app/native-server/src/mcp/mcp-server.ts
Normal file
24
app/native-server/src/mcp/mcp-server.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
import { setupTools } from './register-tools';
|
||||
|
||||
export let mcpServer: Server | null = null;
|
||||
|
||||
export const getMcpServer = () => {
|
||||
if (mcpServer) {
|
||||
return mcpServer;
|
||||
}
|
||||
mcpServer = new Server(
|
||||
{
|
||||
name: 'ChromeMcpServer',
|
||||
version: '1.0.0',
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
setupTools(mcpServer);
|
||||
return mcpServer;
|
||||
};
|
55
app/native-server/src/mcp/register-tools.ts
Normal file
55
app/native-server/src/mcp/register-tools.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
CallToolResult,
|
||||
ListToolsRequestSchema,
|
||||
} from '@modelcontextprotocol/sdk/types.js';
|
||||
import nativeMessagingHostInstance from '../native-messaging-host';
|
||||
import { NativeMessageType, TOOL_SCHEMAS } from 'chrome-mcp-shared';
|
||||
|
||||
export const setupTools = (server: Server) => {
|
||||
// List tools handler
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOL_SCHEMAS }));
|
||||
|
||||
// Call tool handler
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) =>
|
||||
handleToolCall(request.params.name, request.params.arguments || {}),
|
||||
);
|
||||
};
|
||||
|
||||
const handleToolCall = async (name: string, args: any): Promise<CallToolResult> => {
|
||||
try {
|
||||
// 发送请求到Chrome扩展并等待响应
|
||||
const response = await nativeMessagingHostInstance.sendRequestToExtensionAndWait(
|
||||
{
|
||||
name,
|
||||
args,
|
||||
},
|
||||
NativeMessageType.CALL_TOOL,
|
||||
30000, // 30秒超时
|
||||
);
|
||||
if (response.status === 'success') {
|
||||
return response.data;
|
||||
} else {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Error calling tool: ${response.error}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
} catch (error: any) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Error calling tool: ${error.message}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
};
|
3
app/native-server/src/mcp/stdio-config.json
Normal file
3
app/native-server/src/mcp/stdio-config.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"url": "http://127.0.0.1:12306/mcp"
|
||||
}
|
268
app/native-server/src/native-messaging-host.ts
Normal file
268
app/native-server/src/native-messaging-host.ts
Normal file
@@ -0,0 +1,268 @@
|
||||
import { stdin, stdout } from 'process';
|
||||
import { Server } from './server';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { NativeMessageType } from 'chrome-mcp-shared';
|
||||
import { TIMEOUTS } from './constant';
|
||||
|
||||
interface PendingRequest {
|
||||
resolve: (value: any) => void;
|
||||
reject: (reason?: any) => void;
|
||||
timeoutId: NodeJS.Timeout;
|
||||
}
|
||||
|
||||
export class NativeMessagingHost {
|
||||
private associatedServer: Server | null = null;
|
||||
private pendingRequests: Map<string, PendingRequest> = new Map();
|
||||
|
||||
public setServer(serverInstance: Server): void {
|
||||
this.associatedServer = serverInstance;
|
||||
}
|
||||
|
||||
// add message handler to wait for start server
|
||||
public start(): void {
|
||||
try {
|
||||
this.setupMessageHandling();
|
||||
} catch (error: any) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
private setupMessageHandling(): void {
|
||||
let buffer = Buffer.alloc(0);
|
||||
let expectedLength = -1;
|
||||
|
||||
stdin.on('readable', () => {
|
||||
let chunk;
|
||||
while ((chunk = stdin.read()) !== null) {
|
||||
buffer = Buffer.concat([buffer, chunk]);
|
||||
|
||||
if (expectedLength === -1 && buffer.length >= 4) {
|
||||
expectedLength = buffer.readUInt32LE(0);
|
||||
buffer = buffer.slice(4);
|
||||
}
|
||||
|
||||
if (expectedLength !== -1 && buffer.length >= expectedLength) {
|
||||
const messageBuffer = buffer.slice(0, expectedLength);
|
||||
buffer = buffer.slice(expectedLength);
|
||||
|
||||
try {
|
||||
const message = JSON.parse(messageBuffer.toString());
|
||||
this.handleMessage(message);
|
||||
} catch (error: any) {
|
||||
this.sendError(`Failed to parse message: ${error.message}`);
|
||||
}
|
||||
expectedLength = -1; // reset to get next data
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
stdin.on('end', () => {
|
||||
this.cleanup();
|
||||
});
|
||||
|
||||
stdin.on('error', () => {
|
||||
this.cleanup();
|
||||
});
|
||||
}
|
||||
|
||||
private async handleMessage(message: any): Promise<void> {
|
||||
if (!message || typeof message !== 'object') {
|
||||
this.sendError('Invalid message format');
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.responseToRequestId) {
|
||||
const requestId = message.responseToRequestId;
|
||||
const pending = this.pendingRequests.get(requestId);
|
||||
|
||||
if (pending) {
|
||||
clearTimeout(pending.timeoutId);
|
||||
if (message.error) {
|
||||
pending.reject(new Error(message.error));
|
||||
} else {
|
||||
pending.resolve(message.payload);
|
||||
}
|
||||
this.pendingRequests.delete(requestId);
|
||||
} else {
|
||||
// just ignore
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle directive messages from Chrome
|
||||
try {
|
||||
switch (message.type) {
|
||||
case NativeMessageType.START:
|
||||
await this.startServer(message.payload?.port || 3000);
|
||||
break;
|
||||
case NativeMessageType.STOP:
|
||||
await this.stopServer();
|
||||
break;
|
||||
// Keep ping/pong for simple liveness detection, but this differs from request-response pattern
|
||||
case 'ping_from_extension':
|
||||
this.sendMessage({ type: 'pong_to_extension' });
|
||||
break;
|
||||
default:
|
||||
// Double check when message type is not supported
|
||||
if (!message.responseToRequestId) {
|
||||
this.sendError(
|
||||
`Unknown message type or non-response message: ${message.type || 'no type'}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
this.sendError(`Failed to handle directive message: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send request to Chrome and wait for response
|
||||
* @param messagePayload Data to send to Chrome
|
||||
* @param timeoutMs Timeout for waiting response (milliseconds)
|
||||
* @returns Promise, resolves to Chrome's returned payload on success, rejects on failure
|
||||
*/
|
||||
public sendRequestToExtensionAndWait(
|
||||
messagePayload: any,
|
||||
messageType: string = 'request_data',
|
||||
timeoutMs: number = TIMEOUTS.DEFAULT_REQUEST_TIMEOUT,
|
||||
): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const requestId = uuidv4(); // Generate unique request ID
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
this.pendingRequests.delete(requestId); // Remove from Map after timeout
|
||||
reject(new Error(`Request timed out after ${timeoutMs}ms`));
|
||||
}, timeoutMs);
|
||||
|
||||
// Store request's resolve/reject functions and timeout ID
|
||||
this.pendingRequests.set(requestId, { resolve, reject, timeoutId });
|
||||
|
||||
// Send message with requestId to Chrome
|
||||
this.sendMessage({
|
||||
type: messageType, // Define a request type, e.g. 'request_data'
|
||||
payload: messagePayload,
|
||||
requestId: requestId, // <--- Key: include request ID
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start Fastify server (now accepts Server instance)
|
||||
*/
|
||||
private async startServer(port: number): Promise<void> {
|
||||
if (!this.associatedServer) {
|
||||
this.sendError('Internal error: server instance not set');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (this.associatedServer.isRunning) {
|
||||
this.sendMessage({
|
||||
type: NativeMessageType.ERROR,
|
||||
payload: { message: 'Server is already running' },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await this.associatedServer.start(port, this);
|
||||
|
||||
this.sendMessage({
|
||||
type: NativeMessageType.SERVER_STARTED,
|
||||
payload: { port },
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
this.sendError(`Failed to start server: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop Fastify server
|
||||
*/
|
||||
private async stopServer(): Promise<void> {
|
||||
if (!this.associatedServer) {
|
||||
this.sendError('Internal error: server instance not set');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// Check status through associatedServer
|
||||
if (!this.associatedServer.isRunning) {
|
||||
this.sendMessage({
|
||||
type: NativeMessageType.ERROR,
|
||||
payload: { message: 'Server is not running' },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await this.associatedServer.stop();
|
||||
// this.serverStarted = false; // Server should update its own status after successful stop
|
||||
|
||||
this.sendMessage({ type: NativeMessageType.SERVER_STOPPED }); // Distinguish from previous 'stopped'
|
||||
} catch (error: any) {
|
||||
this.sendError(`Failed to stop server: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send message to Chrome extension
|
||||
*/
|
||||
public sendMessage(message: any): void {
|
||||
try {
|
||||
const messageString = JSON.stringify(message);
|
||||
const messageBuffer = Buffer.from(messageString);
|
||||
const headerBuffer = Buffer.alloc(4);
|
||||
headerBuffer.writeUInt32LE(messageBuffer.length, 0);
|
||||
// Ensure atomic write
|
||||
stdout.write(Buffer.concat([headerBuffer, messageBuffer]), (err) => {
|
||||
if (err) {
|
||||
// Consider how to handle write failure, may affect request completion
|
||||
} else {
|
||||
// Message sent successfully, no action needed
|
||||
}
|
||||
});
|
||||
} catch (error: any) {
|
||||
// Catch JSON.stringify or Buffer operation errors
|
||||
// If preparation stage fails, associated request may never be sent
|
||||
// Need to consider whether to reject corresponding Promise (if called within sendRequestToExtensionAndWait)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send error message to Chrome extension (mainly for sending non-request-response type errors)
|
||||
*/
|
||||
private sendError(errorMessage: string): void {
|
||||
this.sendMessage({
|
||||
type: NativeMessageType.ERROR_FROM_NATIVE_HOST, // Use more explicit type
|
||||
payload: { message: errorMessage },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Clean up resources
|
||||
*/
|
||||
private cleanup(): void {
|
||||
// Reject all pending requests
|
||||
this.pendingRequests.forEach((pending) => {
|
||||
clearTimeout(pending.timeoutId);
|
||||
pending.reject(new Error('Native host is shutting down or Chrome disconnected.'));
|
||||
});
|
||||
this.pendingRequests.clear();
|
||||
|
||||
if (this.associatedServer && this.associatedServer.isRunning) {
|
||||
this.associatedServer
|
||||
.stop()
|
||||
.then(() => {
|
||||
process.exit(0);
|
||||
})
|
||||
.catch(() => {
|
||||
process.exit(1);
|
||||
});
|
||||
} else {
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const nativeMessagingHostInstance = new NativeMessagingHost();
|
||||
export default nativeMessagingHostInstance;
|
121
app/native-server/src/scripts/build.ts
Normal file
121
app/native-server/src/scripts/build.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { execSync } from 'child_process';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const distDir = path.join(__dirname, '..', '..', 'dist');
|
||||
// 清理上次构建
|
||||
console.log('清理上次构建...');
|
||||
try {
|
||||
fs.rmSync(distDir, { recursive: true, force: true });
|
||||
} catch (err) {
|
||||
// 忽略目录不存在的错误
|
||||
console.log(err);
|
||||
}
|
||||
|
||||
// 创建dist目录
|
||||
fs.mkdirSync(distDir, { recursive: true });
|
||||
fs.mkdirSync(path.join(distDir, 'logs'), { recursive: true }); // 创建logs目录
|
||||
console.log('dist 和 dist/logs 目录已创建/确认存在');
|
||||
|
||||
// 编译TypeScript
|
||||
console.log('编译TypeScript...');
|
||||
execSync('tsc', { stdio: 'inherit' });
|
||||
|
||||
// 复制配置文件
|
||||
console.log('复制配置文件...');
|
||||
const configSourcePath = path.join(__dirname, '..', 'mcp', 'stdio-config.json');
|
||||
const configDestPath = path.join(distDir, 'mcp', 'stdio-config.json');
|
||||
|
||||
try {
|
||||
// 确保目标目录存在
|
||||
fs.mkdirSync(path.dirname(configDestPath), { recursive: true });
|
||||
|
||||
if (fs.existsSync(configSourcePath)) {
|
||||
fs.copyFileSync(configSourcePath, configDestPath);
|
||||
console.log(`已将 stdio-config.json 复制到 ${configDestPath}`);
|
||||
} else {
|
||||
console.error(`错误: 配置文件未找到: ${configSourcePath}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('复制配置文件时出错:', error);
|
||||
}
|
||||
|
||||
// 复制package.json并更新其内容
|
||||
console.log('准备package.json...');
|
||||
const packageJson = require('../../package.json');
|
||||
|
||||
// 创建安装说明
|
||||
const readmeContent = `# ${packageJson.name}
|
||||
|
||||
本程序为Chrome扩展的Native Messaging主机端。
|
||||
|
||||
## 安装说明
|
||||
|
||||
1. 确保已安装Node.js
|
||||
2. 全局安装本程序:
|
||||
\`\`\`
|
||||
npm install -g ${packageJson.name}
|
||||
\`\`\`
|
||||
3. 注册Native Messaging主机:
|
||||
\`\`\`
|
||||
# 用户级别安装(推荐)
|
||||
${packageJson.name} register
|
||||
|
||||
# 如果用户级别安装失败,可以尝试系统级别安装
|
||||
${packageJson.name} register --system
|
||||
# 或者使用管理员权限
|
||||
sudo ${packageJson.name} register
|
||||
\`\`\`
|
||||
|
||||
## 使用方法
|
||||
|
||||
此应用程序由Chrome扩展自动启动,无需手动运行。
|
||||
`;
|
||||
|
||||
fs.writeFileSync(path.join(distDir, 'README.md'), readmeContent);
|
||||
|
||||
console.log('复制包装脚本...');
|
||||
const scriptsSourceDir = path.join(__dirname, '.');
|
||||
const macOsWrapperSourcePath = path.join(scriptsSourceDir, 'run_host.sh');
|
||||
const windowsWrapperSourcePath = path.join(scriptsSourceDir, 'run_host.bat');
|
||||
|
||||
const macOsWrapperDestPath = path.join(distDir, 'run_host.sh');
|
||||
const windowsWrapperDestPath = path.join(distDir, 'run_host.bat');
|
||||
|
||||
try {
|
||||
if (fs.existsSync(macOsWrapperSourcePath)) {
|
||||
fs.copyFileSync(macOsWrapperSourcePath, macOsWrapperDestPath);
|
||||
console.log(`已将 ${macOsWrapperSourcePath} 复制到 ${macOsWrapperDestPath}`);
|
||||
} else {
|
||||
console.error(`错误: macOS 包装脚本源文件未找到: ${macOsWrapperSourcePath}`);
|
||||
}
|
||||
|
||||
if (fs.existsSync(windowsWrapperSourcePath)) {
|
||||
fs.copyFileSync(windowsWrapperSourcePath, windowsWrapperDestPath);
|
||||
console.log(`已将 ${windowsWrapperSourcePath} 复制到 ${windowsWrapperDestPath}`);
|
||||
} else {
|
||||
console.error(`错误: Windows 包装脚本源文件未找到: ${windowsWrapperSourcePath}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('复制包装脚本时出错:', error);
|
||||
}
|
||||
|
||||
// 为关键JavaScript文件和macOS包装脚本添加可执行权限
|
||||
console.log('添加可执行权限...');
|
||||
const filesToMakeExecutable = ['index.js', 'cli.js', 'run_host.sh']; // cli.js 假设在 dist 根目录
|
||||
|
||||
filesToMakeExecutable.forEach((file) => {
|
||||
const filePath = path.join(distDir, file); // filePath 现在是目标路径
|
||||
try {
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.chmodSync(filePath, '755');
|
||||
console.log(`已为 ${file} 添加可执行权限 (755)`);
|
||||
} else {
|
||||
console.warn(`警告: ${filePath} 不存在,无法添加可执行权限`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`为 ${file} 添加可执行权限时出错:`, error);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('✅ 构建完成');
|
4
app/native-server/src/scripts/constant.ts
Normal file
4
app/native-server/src/scripts/constant.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export const COMMAND_NAME = 'chrome-mcp-bridge';
|
||||
export const EXTENSION_ID = 'hbdgbgagpkpjffpklnamcljpakneikee';
|
||||
export const HOST_NAME = 'com.chromemcp.nativehost';
|
||||
export const DESCRIPTION = 'Node.js Host for Browser Bridge Extension';
|
295
app/native-server/src/scripts/postinstall.ts
Normal file
295
app/native-server/src/scripts/postinstall.ts
Normal file
@@ -0,0 +1,295 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { COMMAND_NAME } from './constant';
|
||||
import { colorText, tryRegisterUserLevelHost } from './utils';
|
||||
|
||||
// Check if this script is run directly
|
||||
const isDirectRun = require.main === module;
|
||||
|
||||
// Detect global installation for both npm and pnpm
|
||||
function detectGlobalInstall(): boolean {
|
||||
// npm uses npm_config_global
|
||||
if (process.env.npm_config_global === 'true') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// pnpm detection methods
|
||||
// Method 1: Check if PNPM_HOME is set and current path contains it
|
||||
if (process.env.PNPM_HOME && __dirname.includes(process.env.PNPM_HOME)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Method 2: Check if we're in a global pnpm directory structure
|
||||
// pnpm global packages are typically installed in ~/.local/share/pnpm/global/5/node_modules
|
||||
// Windows: %APPDATA%\pnpm\global\5\node_modules
|
||||
const globalPnpmPatterns =
|
||||
process.platform === 'win32'
|
||||
? ['\\pnpm\\global\\', '\\pnpm-global\\', '\\AppData\\Roaming\\pnpm\\']
|
||||
: ['/pnpm/global/', '/.local/share/pnpm/', '/pnpm-global/'];
|
||||
|
||||
if (globalPnpmPatterns.some((pattern) => __dirname.includes(pattern))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Method 3: Check npm_config_prefix for pnpm
|
||||
if (process.env.npm_config_prefix && __dirname.includes(process.env.npm_config_prefix)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Method 4: Windows-specific global installation paths
|
||||
if (process.platform === 'win32') {
|
||||
const windowsGlobalPatterns = [
|
||||
'\\npm\\node_modules\\',
|
||||
'\\AppData\\Roaming\\npm\\node_modules\\',
|
||||
'\\Program Files\\nodejs\\node_modules\\',
|
||||
'\\nodejs\\node_modules\\',
|
||||
];
|
||||
|
||||
if (windowsGlobalPatterns.some((pattern) => __dirname.includes(pattern))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const isGlobalInstall = detectGlobalInstall();
|
||||
|
||||
/**
|
||||
* Write Node.js path for run_host scripts to avoid fragile relative paths
|
||||
*/
|
||||
async function writeNodePath(): Promise<void> {
|
||||
try {
|
||||
const nodePath = process.execPath;
|
||||
const nodePathFile = path.join(__dirname, '..', 'node_path.txt');
|
||||
|
||||
console.log(colorText(`Writing Node.js path: ${nodePath}`, 'blue'));
|
||||
fs.writeFileSync(nodePathFile, nodePath, 'utf8');
|
||||
console.log(colorText('✓ Node.js path written for run_host scripts', 'green'));
|
||||
} catch (error: any) {
|
||||
console.warn(colorText(`⚠️ Failed to write Node.js path: ${error.message}`, 'yellow'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保执行权限(无论是否为全局安装)
|
||||
*/
|
||||
async function ensureExecutionPermissions(): Promise<void> {
|
||||
if (process.platform === 'win32') {
|
||||
// Windows 平台处理
|
||||
await ensureWindowsFilePermissions();
|
||||
return;
|
||||
}
|
||||
|
||||
// Unix/Linux 平台处理
|
||||
const filesToCheck = [
|
||||
path.join(__dirname, '..', 'index.js'),
|
||||
path.join(__dirname, '..', 'run_host.sh'),
|
||||
path.join(__dirname, '..', 'cli.js'),
|
||||
];
|
||||
|
||||
for (const filePath of filesToCheck) {
|
||||
if (fs.existsSync(filePath)) {
|
||||
try {
|
||||
fs.chmodSync(filePath, '755');
|
||||
console.log(
|
||||
colorText(`✓ Set execution permissions for ${path.basename(filePath)}`, 'green'),
|
||||
);
|
||||
} catch (err: any) {
|
||||
console.warn(
|
||||
colorText(
|
||||
`⚠️ Unable to set execution permissions for ${path.basename(filePath)}: ${err.message}`,
|
||||
'yellow',
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.warn(colorText(`⚠️ File not found: ${filePath}`, 'yellow'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Windows 平台文件权限处理
|
||||
*/
|
||||
async function ensureWindowsFilePermissions(): Promise<void> {
|
||||
const filesToCheck = [
|
||||
path.join(__dirname, '..', 'index.js'),
|
||||
path.join(__dirname, '..', 'run_host.bat'),
|
||||
path.join(__dirname, '..', 'cli.js'),
|
||||
];
|
||||
|
||||
for (const filePath of filesToCheck) {
|
||||
if (fs.existsSync(filePath)) {
|
||||
try {
|
||||
// 检查文件是否为只读,如果是则移除只读属性
|
||||
const stats = fs.statSync(filePath);
|
||||
if (!(stats.mode & parseInt('200', 8))) {
|
||||
// 检查写权限
|
||||
// 尝试移除只读属性
|
||||
fs.chmodSync(filePath, stats.mode | parseInt('200', 8));
|
||||
console.log(
|
||||
colorText(`✓ Removed read-only attribute from ${path.basename(filePath)}`, 'green'),
|
||||
);
|
||||
}
|
||||
|
||||
// 验证文件可读性
|
||||
fs.accessSync(filePath, fs.constants.R_OK);
|
||||
console.log(
|
||||
colorText(`✓ Verified file accessibility for ${path.basename(filePath)}`, 'green'),
|
||||
);
|
||||
} catch (err: any) {
|
||||
console.warn(
|
||||
colorText(
|
||||
`⚠️ Unable to verify file permissions for ${path.basename(filePath)}: ${err.message}`,
|
||||
'yellow',
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.warn(colorText(`⚠️ File not found: ${filePath}`, 'yellow'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function tryRegisterNativeHost(): Promise<void> {
|
||||
try {
|
||||
console.log(colorText('Attempting to register Chrome Native Messaging host...', 'blue'));
|
||||
|
||||
// Always ensure execution permissions, regardless of installation type
|
||||
await ensureExecutionPermissions();
|
||||
|
||||
if (isGlobalInstall) {
|
||||
// First try user-level installation (no elevated permissions required)
|
||||
const userLevelSuccess = await tryRegisterUserLevelHost();
|
||||
|
||||
if (!userLevelSuccess) {
|
||||
// User-level installation failed, suggest using register command
|
||||
console.log(
|
||||
colorText(
|
||||
'User-level installation failed, system-level installation may be needed',
|
||||
'yellow',
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
colorText('Please run the following command for system-level installation:', 'blue'),
|
||||
);
|
||||
console.log(` ${COMMAND_NAME} register --system`);
|
||||
printManualInstructions();
|
||||
}
|
||||
} else {
|
||||
// Local installation mode, don't attempt automatic registration
|
||||
console.log(
|
||||
colorText('Local installation detected, skipping automatic registration', 'yellow'),
|
||||
);
|
||||
printManualInstructions();
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(
|
||||
colorText(
|
||||
`注册过程中出现错误: ${error instanceof Error ? error.message : String(error)}`,
|
||||
'red',
|
||||
),
|
||||
);
|
||||
printManualInstructions();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打印手动安装指南
|
||||
*/
|
||||
function printManualInstructions(): void {
|
||||
console.log('\n' + colorText('===== Manual Registration Guide =====', 'blue'));
|
||||
|
||||
console.log(colorText('1. Try user-level installation (recommended):', 'yellow'));
|
||||
if (isGlobalInstall) {
|
||||
console.log(` ${COMMAND_NAME} register`);
|
||||
} else {
|
||||
console.log(` npx ${COMMAND_NAME} register`);
|
||||
}
|
||||
|
||||
console.log(
|
||||
colorText('\n2. If user-level installation fails, try system-level installation:', 'yellow'),
|
||||
);
|
||||
|
||||
console.log(colorText(' Use --system parameter (auto-elevate permissions):', 'yellow'));
|
||||
if (isGlobalInstall) {
|
||||
console.log(` ${COMMAND_NAME} register --system`);
|
||||
} else {
|
||||
console.log(` npx ${COMMAND_NAME} register --system`);
|
||||
}
|
||||
|
||||
console.log(colorText('\n Or use administrator privileges directly:', 'yellow'));
|
||||
if (os.platform() === 'win32') {
|
||||
console.log(
|
||||
colorText(
|
||||
' Please run Command Prompt or PowerShell as administrator and execute:',
|
||||
'yellow',
|
||||
),
|
||||
);
|
||||
if (isGlobalInstall) {
|
||||
console.log(` ${COMMAND_NAME} register`);
|
||||
} else {
|
||||
console.log(` npx ${COMMAND_NAME} register`);
|
||||
}
|
||||
} else {
|
||||
console.log(colorText(' Please run the following command in terminal:', 'yellow'));
|
||||
if (isGlobalInstall) {
|
||||
console.log(` sudo ${COMMAND_NAME} register`);
|
||||
} else {
|
||||
console.log(` sudo npx ${COMMAND_NAME} register`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
'\n' +
|
||||
colorText(
|
||||
'Ensure Chrome extension is installed and refresh the extension to connect to local service.',
|
||||
'blue',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 主函数
|
||||
*/
|
||||
async function main(): Promise<void> {
|
||||
console.log(colorText(`Installing ${COMMAND_NAME}...`, 'green'));
|
||||
|
||||
// Debug information
|
||||
console.log(colorText('Installation environment debug info:', 'blue'));
|
||||
console.log(` __dirname: ${__dirname}`);
|
||||
console.log(` npm_config_global: ${process.env.npm_config_global}`);
|
||||
console.log(` PNPM_HOME: ${process.env.PNPM_HOME}`);
|
||||
console.log(` npm_config_prefix: ${process.env.npm_config_prefix}`);
|
||||
console.log(` isGlobalInstall: ${isGlobalInstall}`);
|
||||
|
||||
// Always ensure execution permissions first
|
||||
await ensureExecutionPermissions();
|
||||
|
||||
// Write Node.js path for run_host scripts to use
|
||||
await writeNodePath();
|
||||
|
||||
// If global installation, try automatic registration
|
||||
if (isGlobalInstall) {
|
||||
await tryRegisterNativeHost();
|
||||
} else {
|
||||
console.log(colorText('Local installation detected', 'yellow'));
|
||||
printManualInstructions();
|
||||
}
|
||||
}
|
||||
|
||||
// Only execute main function when running this script directly
|
||||
if (isDirectRun) {
|
||||
main().catch((error) => {
|
||||
console.error(
|
||||
colorText(
|
||||
`Installation script error: ${error instanceof Error ? error.message : String(error)}`,
|
||||
'red',
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
3
app/native-server/src/scripts/register-dev.ts
Normal file
3
app/native-server/src/scripts/register-dev.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { tryRegisterUserLevelHost } from './utils';
|
||||
|
||||
tryRegisterUserLevelHost();
|
23
app/native-server/src/scripts/register.ts
Normal file
23
app/native-server/src/scripts/register.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env node
|
||||
import { COMMAND_NAME } from './constant';
|
||||
import { colorText, registerWithElevatedPermissions } from './utils';
|
||||
|
||||
/**
|
||||
* 主函数
|
||||
*/
|
||||
async function main(): Promise<void> {
|
||||
console.log(colorText(`正在注册 ${COMMAND_NAME} Native Messaging主机...`, 'blue'));
|
||||
|
||||
try {
|
||||
await registerWithElevatedPermissions();
|
||||
console.log(
|
||||
colorText('注册成功!现在Chrome扩展可以通过Native Messaging与本地服务通信。', 'green'),
|
||||
);
|
||||
} catch (error: any) {
|
||||
console.error(colorText(`注册失败: ${error.message}`, 'red'));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 执行主函数
|
||||
main();
|
95
app/native-server/src/scripts/run_host.bat
Normal file
95
app/native-server/src/scripts/run_host.bat
Normal file
@@ -0,0 +1,95 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
REM Setup paths
|
||||
set "SCRIPT_DIR=%~dp0"
|
||||
if "%SCRIPT_DIR:~-1%"=="\" set "SCRIPT_DIR=%SCRIPT_DIR:~0,-1%"
|
||||
set "LOG_DIR=%SCRIPT_DIR%\logs"
|
||||
set "NODE_SCRIPT=%SCRIPT_DIR%\index.js"
|
||||
|
||||
if not exist "%LOG_DIR%" md "%LOG_DIR%"
|
||||
|
||||
REM Generate timestamp
|
||||
for /f %%i in ('powershell -NoProfile -Command "Get-Date -Format 'yyyyMMdd_HHmmss'"') do set "TIMESTAMP=%%i"
|
||||
set "WRAPPER_LOG=%LOG_DIR%\native_host_wrapper_windows_%TIMESTAMP%.log"
|
||||
set "STDERR_LOG=%LOG_DIR%\native_host_stderr_windows_%TIMESTAMP%.log"
|
||||
|
||||
REM Initial logging
|
||||
echo Wrapper script called at %DATE% %TIME% > "%WRAPPER_LOG%"
|
||||
echo SCRIPT_DIR: %SCRIPT_DIR% >> "%WRAPPER_LOG%"
|
||||
echo LOG_DIR: %LOG_DIR% >> "%WRAPPER_LOG%"
|
||||
echo NODE_SCRIPT: %NODE_SCRIPT% >> "%WRAPPER_LOG%"
|
||||
echo Initial PATH: %PATH% >> "%WRAPPER_LOG%"
|
||||
echo User: %USERNAME% >> "%WRAPPER_LOG%"
|
||||
echo Current PWD: %CD% >> "%WRAPPER_LOG%"
|
||||
|
||||
REM Node.js discovery
|
||||
set "NODE_EXEC="
|
||||
|
||||
REM Priority 1: Installation-time node path
|
||||
set "NODE_PATH_FILE=%SCRIPT_DIR%\node_path.txt"
|
||||
echo Checking installation-time node path >> "%WRAPPER_LOG%"
|
||||
if exist "%NODE_PATH_FILE%" (
|
||||
set /p EXPECTED_NODE=<"%NODE_PATH_FILE%"
|
||||
if exist "!EXPECTED_NODE!" (
|
||||
set "NODE_EXEC=!EXPECTED_NODE!"
|
||||
echo Found installation-time node at !NODE_EXEC! >> "%WRAPPER_LOG%"
|
||||
)
|
||||
)
|
||||
|
||||
REM Priority 1.5: Fallback to relative path
|
||||
if not defined NODE_EXEC (
|
||||
set "EXPECTED_NODE=%SCRIPT_DIR%\..\..\..\node.exe"
|
||||
echo Checking relative path >> "%WRAPPER_LOG%"
|
||||
if exist "%EXPECTED_NODE%" (
|
||||
set "NODE_EXEC=%EXPECTED_NODE%"
|
||||
echo Found node at relative path: !NODE_EXEC! >> "%WRAPPER_LOG%"
|
||||
)
|
||||
)
|
||||
|
||||
REM Priority 2: where command
|
||||
if not defined NODE_EXEC (
|
||||
echo Trying 'where node.exe' >> "%WRAPPER_LOG%"
|
||||
for /f "delims=" %%i in ('where node.exe 2^>nul') do (
|
||||
if not defined NODE_EXEC (
|
||||
set "NODE_EXEC=%%i"
|
||||
echo Found node using 'where': !NODE_EXEC! >> "%WRAPPER_LOG%"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
REM Priority 3: Common paths
|
||||
if not defined NODE_EXEC (
|
||||
if exist "%ProgramFiles%\nodejs\node.exe" (
|
||||
set "NODE_EXEC=%ProgramFiles%\nodejs\node.exe"
|
||||
echo Found node at !NODE_EXEC! >> "%WRAPPER_LOG%"
|
||||
) else if exist "%ProgramFiles(x86)%\nodejs\node.exe" (
|
||||
set "NODE_EXEC=%ProgramFiles(x86)%\nodejs\node.exe"
|
||||
echo Found node at !NODE_EXEC! >> "%WRAPPER_LOG%"
|
||||
) else if exist "%LOCALAPPDATA%\Programs\nodejs\node.exe" (
|
||||
set "NODE_EXEC=%LOCALAPPDATA%\Programs\nodejs\node.exe"
|
||||
echo Found node at !NODE_EXEC! >> "%WRAPPER_LOG%"
|
||||
)
|
||||
)
|
||||
|
||||
REM Validation
|
||||
if not defined NODE_EXEC (
|
||||
echo ERROR: Node.js executable not found! >> "%WRAPPER_LOG%"
|
||||
exit /B 1
|
||||
)
|
||||
|
||||
echo Using Node executable: %NODE_EXEC% >> "%WRAPPER_LOG%"
|
||||
call "%NODE_EXEC%" -v >> "%WRAPPER_LOG%" 2>>&1
|
||||
|
||||
if not exist "%NODE_SCRIPT%" (
|
||||
echo ERROR: Node.js script not found at %NODE_SCRIPT% >> "%WRAPPER_LOG%"
|
||||
exit /B 1
|
||||
)
|
||||
|
||||
echo Executing: "%NODE_EXEC%" "%NODE_SCRIPT%" >> "%WRAPPER_LOG%"
|
||||
call "%NODE_EXEC%" "%NODE_SCRIPT%" 2>> "%STDERR_LOG%"
|
||||
set "EXIT_CODE=%ERRORLEVEL%"
|
||||
|
||||
echo Exit code: %EXIT_CODE% >> "%WRAPPER_LOG%"
|
||||
endlocal
|
||||
exit /B %EXIT_CODE%
|
141
app/native-server/src/scripts/run_host.sh
Normal file
141
app/native-server/src/scripts/run_host.sh
Normal file
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Configuration
|
||||
ENABLE_LOG_ROTATION="true"
|
||||
LOG_RETENTION_COUNT=5
|
||||
|
||||
# Setup paths
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
LOG_DIR="${SCRIPT_DIR}/logs"
|
||||
mkdir -p "${LOG_DIR}"
|
||||
|
||||
# Log rotation
|
||||
if [ "${ENABLE_LOG_ROTATION}" = "true" ]; then
|
||||
ls -tp "${LOG_DIR}/native_host_wrapper_macos_"* 2>/dev/null | tail -n +$((LOG_RETENTION_COUNT + 1)) | xargs -I {} rm -- {}
|
||||
ls -tp "${LOG_DIR}/native_host_stderr_macos_"* 2>/dev/null | tail -n +$((LOG_RETENTION_COUNT + 1)) | xargs -I {} rm -- {}
|
||||
fi
|
||||
|
||||
# Logging setup
|
||||
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
|
||||
WRAPPER_LOG="${LOG_DIR}/native_host_wrapper_macos_${TIMESTAMP}.log"
|
||||
STDERR_LOG="${LOG_DIR}/native_host_stderr_macos_${TIMESTAMP}.log"
|
||||
NODE_SCRIPT="${SCRIPT_DIR}/index.js"
|
||||
|
||||
# Initial logging
|
||||
{
|
||||
echo "--- Wrapper script called at $(date) ---"
|
||||
echo "SCRIPT_DIR: ${SCRIPT_DIR}"
|
||||
echo "LOG_DIR: ${LOG_DIR}"
|
||||
echo "NODE_SCRIPT: ${NODE_SCRIPT}"
|
||||
echo "Initial PATH: ${PATH}"
|
||||
echo "User: $(whoami)"
|
||||
echo "Current PWD: $(pwd)"
|
||||
} > "${WRAPPER_LOG}"
|
||||
|
||||
# Node.js discovery
|
||||
NODE_EXEC=""
|
||||
|
||||
# Priority 1: Installation-time node path
|
||||
NODE_PATH_FILE="${SCRIPT_DIR}/node_path.txt"
|
||||
echo "Searching for Node.js..." >> "${WRAPPER_LOG}"
|
||||
echo "[Priority 1] Checking installation-time node path" >> "${WRAPPER_LOG}"
|
||||
if [ -f "${NODE_PATH_FILE}" ]; then
|
||||
EXPECTED_NODE=$(cat "${NODE_PATH_FILE}" 2>/dev/null | tr -d '\n\r')
|
||||
if [ -n "${EXPECTED_NODE}" ] && [ -x "${EXPECTED_NODE}" ]; then
|
||||
NODE_EXEC="${EXPECTED_NODE}"
|
||||
echo "Found installation-time node at ${NODE_EXEC}" >> "${WRAPPER_LOG}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Priority 1.5: Fallback to relative path
|
||||
if [ -z "${NODE_EXEC}" ]; then
|
||||
EXPECTED_NODE="${SCRIPT_DIR}/../../../bin/node"
|
||||
echo "[Priority 1.5] Checking relative path" >> "${WRAPPER_LOG}"
|
||||
if [ -x "${EXPECTED_NODE}" ]; then
|
||||
NODE_EXEC="${EXPECTED_NODE}"
|
||||
echo "Found node at relative path: ${NODE_EXEC}" >> "${WRAPPER_LOG}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Priority 2: NVM
|
||||
if [ -z "${NODE_EXEC}" ]; then
|
||||
echo "[Priority 2] Checking NVM" >> "${WRAPPER_LOG}"
|
||||
NVM_DIR="$HOME/.nvm"
|
||||
if [ -d "${NVM_DIR}" ]; then
|
||||
# Try default version first
|
||||
if [ -L "${NVM_DIR}/alias/default" ]; then
|
||||
NVM_DEFAULT_VERSION=$(readlink "${NVM_DIR}/alias/default")
|
||||
NVM_DEFAULT_NODE="${NVM_DIR}/versions/node/${NVM_DEFAULT_VERSION}/bin/node"
|
||||
if [ -x "${NVM_DEFAULT_NODE}" ]; then
|
||||
NODE_EXEC="${NVM_DEFAULT_NODE}"
|
||||
echo "Found NVM default node: ${NODE_EXEC}" >> "${WRAPPER_LOG}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Fallback to latest version
|
||||
if [ -z "${NODE_EXEC}" ]; then
|
||||
LATEST_NVM_VERSION_PATH=$(ls -d ${NVM_DIR}/versions/node/v* 2>/dev/null | sort -V | tail -n 1)
|
||||
if [ -n "${LATEST_NVM_VERSION_PATH}" ] && [ -x "${LATEST_NVM_VERSION_PATH}/bin/node" ]; then
|
||||
NODE_EXEC="${LATEST_NVM_VERSION_PATH}/bin/node"
|
||||
echo "Found NVM latest node: ${NODE_EXEC}" >> "${WRAPPER_LOG}"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Priority 3: Common paths
|
||||
if [ -z "${NODE_EXEC}" ]; then
|
||||
echo "[Priority 3] Checking common paths" >> "${WRAPPER_LOG}"
|
||||
COMMON_NODE_PATHS=(
|
||||
"/opt/homebrew/bin/node"
|
||||
"/usr/local/bin/node"
|
||||
)
|
||||
for path_to_node in "${COMMON_NODE_PATHS[@]}"; do
|
||||
if [ -x "${path_to_node}" ]; then
|
||||
NODE_EXEC="${path_to_node}"
|
||||
echo "Found node at: ${NODE_EXEC}" >> "${WRAPPER_LOG}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Priority 4: command -v
|
||||
if [ -z "${NODE_EXEC}" ]; then
|
||||
echo "[Priority 4] Trying 'command -v node'" >> "${WRAPPER_LOG}"
|
||||
if command -v node &>/dev/null; then
|
||||
NODE_EXEC=$(command -v node)
|
||||
echo "Found node using 'command -v': ${NODE_EXEC}" >> "${WRAPPER_LOG}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Priority 5: PATH search
|
||||
if [ -z "${NODE_EXEC}" ]; then
|
||||
echo "[Priority 5] Searching PATH" >> "${WRAPPER_LOG}"
|
||||
OLD_IFS=$IFS
|
||||
IFS=:
|
||||
for path_in_env in $PATH; do
|
||||
if [ -x "${path_in_env}/node" ]; then
|
||||
NODE_EXEC="${path_in_env}/node"
|
||||
echo "Found node in PATH: ${NODE_EXEC}" >> "${WRAPPER_LOG}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
IFS=$OLD_IFS
|
||||
fi
|
||||
|
||||
# Execution
|
||||
if [ -z "${NODE_EXEC}" ]; then
|
||||
{
|
||||
echo "ERROR: Node.js executable not found!"
|
||||
echo "Searched: installation path, relative path, NVM, common paths, command -v, PATH"
|
||||
} >> "${WRAPPER_LOG}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
{
|
||||
echo "Using Node executable: ${NODE_EXEC}"
|
||||
echo "Node version: $(${NODE_EXEC} -v)"
|
||||
echo "Executing: ${NODE_EXEC} ${NODE_SCRIPT}"
|
||||
} >> "${WRAPPER_LOG}"
|
||||
|
||||
exec "${NODE_EXEC}" "${NODE_SCRIPT}" 2>> "${STDERR_LOG}"
|
433
app/native-server/src/scripts/utils.ts
Normal file
433
app/native-server/src/scripts/utils.ts
Normal file
@@ -0,0 +1,433 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import { execSync } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import { COMMAND_NAME, DESCRIPTION, EXTENSION_ID, HOST_NAME } from './constant';
|
||||
|
||||
export const access = promisify(fs.access);
|
||||
export const mkdir = promisify(fs.mkdir);
|
||||
export const writeFile = promisify(fs.writeFile);
|
||||
|
||||
/**
|
||||
* 打印彩色文本
|
||||
*/
|
||||
export function colorText(text: string, color: string): string {
|
||||
const colors: Record<string, string> = {
|
||||
red: '\x1b[31m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
blue: '\x1b[34m',
|
||||
reset: '\x1b[0m',
|
||||
};
|
||||
|
||||
return colors[color] + text + colors.reset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user-level manifest file path
|
||||
*/
|
||||
export function getUserManifestPath(): string {
|
||||
if (os.platform() === 'win32') {
|
||||
// Windows: %APPDATA%\Google\Chrome\NativeMessagingHosts\
|
||||
return path.join(
|
||||
process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'),
|
||||
'Google',
|
||||
'Chrome',
|
||||
'NativeMessagingHosts',
|
||||
`${HOST_NAME}.json`,
|
||||
);
|
||||
} else if (os.platform() === 'darwin') {
|
||||
// macOS: ~/Library/Application Support/Google/Chrome/NativeMessagingHosts/
|
||||
return path.join(
|
||||
os.homedir(),
|
||||
'Library',
|
||||
'Application Support',
|
||||
'Google',
|
||||
'Chrome',
|
||||
'NativeMessagingHosts',
|
||||
`${HOST_NAME}.json`,
|
||||
);
|
||||
} else {
|
||||
// Linux: ~/.config/google-chrome/NativeMessagingHosts/
|
||||
return path.join(
|
||||
os.homedir(),
|
||||
'.config',
|
||||
'google-chrome',
|
||||
'NativeMessagingHosts',
|
||||
`${HOST_NAME}.json`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get system-level manifest file path
|
||||
*/
|
||||
export function getSystemManifestPath(): string {
|
||||
if (os.platform() === 'win32') {
|
||||
// Windows: %ProgramFiles%\Google\Chrome\NativeMessagingHosts\
|
||||
return path.join(
|
||||
process.env.ProgramFiles || 'C:\\Program Files',
|
||||
'Google',
|
||||
'Chrome',
|
||||
'NativeMessagingHosts',
|
||||
`${HOST_NAME}.json`,
|
||||
);
|
||||
} else if (os.platform() === 'darwin') {
|
||||
// macOS: /Library/Google/Chrome/NativeMessagingHosts/
|
||||
return path.join('/Library', 'Google', 'Chrome', 'NativeMessagingHosts', `${HOST_NAME}.json`);
|
||||
} else {
|
||||
// Linux: /etc/opt/chrome/native-messaging-hosts/
|
||||
return path.join('/etc', 'opt', 'chrome', 'native-messaging-hosts', `${HOST_NAME}.json`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get native host startup script file path
|
||||
*/
|
||||
export async function getMainPath(): Promise<string> {
|
||||
try {
|
||||
const packageDistDir = path.join(__dirname, '..');
|
||||
const wrapperScriptName = process.platform === 'win32' ? 'run_host.bat' : 'run_host.sh';
|
||||
const absoluteWrapperPath = path.resolve(packageDistDir, wrapperScriptName);
|
||||
return absoluteWrapperPath;
|
||||
} catch (error) {
|
||||
console.log(colorText('Cannot find global package path, using current directory', 'yellow'));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保关键文件具有执行权限
|
||||
*/
|
||||
export async function ensureExecutionPermissions(): Promise<void> {
|
||||
try {
|
||||
const packageDistDir = path.join(__dirname, '..');
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
// Windows 平台处理
|
||||
await ensureWindowsFilePermissions(packageDistDir);
|
||||
return;
|
||||
}
|
||||
|
||||
// Unix/Linux 平台处理
|
||||
const filesToCheck = [
|
||||
path.join(packageDistDir, 'index.js'),
|
||||
path.join(packageDistDir, 'run_host.sh'),
|
||||
path.join(packageDistDir, 'cli.js'),
|
||||
];
|
||||
|
||||
for (const filePath of filesToCheck) {
|
||||
if (fs.existsSync(filePath)) {
|
||||
try {
|
||||
fs.chmodSync(filePath, '755');
|
||||
console.log(
|
||||
colorText(`✓ Set execution permissions for ${path.basename(filePath)}`, 'green'),
|
||||
);
|
||||
} catch (err: any) {
|
||||
console.warn(
|
||||
colorText(
|
||||
`⚠️ Unable to set execution permissions for ${path.basename(filePath)}: ${err.message}`,
|
||||
'yellow',
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.warn(colorText(`⚠️ File not found: ${filePath}`, 'yellow'));
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.warn(colorText(`⚠️ Error ensuring execution permissions: ${error.message}`, 'yellow'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Windows 平台文件权限处理
|
||||
*/
|
||||
async function ensureWindowsFilePermissions(packageDistDir: string): Promise<void> {
|
||||
const filesToCheck = [
|
||||
path.join(packageDistDir, 'index.js'),
|
||||
path.join(packageDistDir, 'run_host.bat'),
|
||||
path.join(packageDistDir, 'cli.js'),
|
||||
];
|
||||
|
||||
for (const filePath of filesToCheck) {
|
||||
if (fs.existsSync(filePath)) {
|
||||
try {
|
||||
// 检查文件是否为只读,如果是则移除只读属性
|
||||
const stats = fs.statSync(filePath);
|
||||
if (!(stats.mode & parseInt('200', 8))) {
|
||||
// 检查写权限
|
||||
// 尝试移除只读属性
|
||||
fs.chmodSync(filePath, stats.mode | parseInt('200', 8));
|
||||
console.log(
|
||||
colorText(`✓ Removed read-only attribute from ${path.basename(filePath)}`, 'green'),
|
||||
);
|
||||
}
|
||||
|
||||
// 验证文件可读性
|
||||
fs.accessSync(filePath, fs.constants.R_OK);
|
||||
console.log(
|
||||
colorText(`✓ Verified file accessibility for ${path.basename(filePath)}`, 'green'),
|
||||
);
|
||||
} catch (err: any) {
|
||||
console.warn(
|
||||
colorText(
|
||||
`⚠️ Unable to verify file permissions for ${path.basename(filePath)}: ${err.message}`,
|
||||
'yellow',
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.warn(colorText(`⚠️ File not found: ${filePath}`, 'yellow'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Native Messaging host manifest content
|
||||
*/
|
||||
export async function createManifestContent(): Promise<any> {
|
||||
const mainPath = await getMainPath();
|
||||
|
||||
return {
|
||||
name: HOST_NAME,
|
||||
description: DESCRIPTION,
|
||||
path: mainPath, // Node.js可执行文件路径
|
||||
type: 'stdio',
|
||||
allowed_origins: [`chrome-extension://${EXTENSION_ID}/`],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证Windows注册表项是否存在
|
||||
*/
|
||||
function verifyWindowsRegistryEntry(registryKey: string, expectedPath: string): boolean {
|
||||
if (os.platform() !== 'win32') {
|
||||
return true; // 非Windows平台跳过验证
|
||||
}
|
||||
|
||||
try {
|
||||
const result = execSync(`reg query "${registryKey}" /ve`, { encoding: 'utf8', stdio: 'pipe' });
|
||||
const lines = result.split('\n');
|
||||
for (const line of lines) {
|
||||
if (line.includes('REG_SZ') && line.includes(expectedPath.replace(/\\/g, '\\\\'))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试注册用户级别的Native Messaging主机
|
||||
*/
|
||||
export async function tryRegisterUserLevelHost(): Promise<boolean> {
|
||||
try {
|
||||
console.log(colorText('Attempting to register user-level Native Messaging host...', 'blue'));
|
||||
|
||||
// 1. 确保执行权限
|
||||
await ensureExecutionPermissions();
|
||||
|
||||
// 2. 确定清单文件路径
|
||||
const manifestPath = getUserManifestPath();
|
||||
|
||||
// 3. 确保目录存在
|
||||
await mkdir(path.dirname(manifestPath), { recursive: true });
|
||||
|
||||
// 4. 创建清单内容
|
||||
const manifest = await createManifestContent();
|
||||
|
||||
console.log('manifest path==>', manifest, manifestPath);
|
||||
|
||||
// 5. 写入清单文件
|
||||
await writeFile(manifestPath, JSON.stringify(manifest, null, 2));
|
||||
|
||||
if (os.platform() === 'win32') {
|
||||
const registryKey = `HKCU\\Software\\Google\\Chrome\\NativeMessagingHosts\\${HOST_NAME}`;
|
||||
try {
|
||||
// 确保路径使用正确的转义格式
|
||||
const escapedPath = manifestPath.replace(/\\/g, '\\\\');
|
||||
const regCommand = `reg add "${registryKey}" /ve /t REG_SZ /d "${escapedPath}" /f`;
|
||||
|
||||
console.log(colorText(`Executing registry command: ${regCommand}`, 'blue'));
|
||||
execSync(regCommand, { stdio: 'pipe' });
|
||||
|
||||
// 验证注册表项是否创建成功
|
||||
if (verifyWindowsRegistryEntry(registryKey, manifestPath)) {
|
||||
console.log(colorText('✓ Successfully created Windows registry entry', 'green'));
|
||||
} else {
|
||||
console.log(colorText('⚠️ Registry entry created but verification failed', 'yellow'));
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.log(
|
||||
colorText(`⚠️ Unable to create Windows registry entry: ${error.message}`, 'yellow'),
|
||||
);
|
||||
console.log(colorText(`Registry key: ${registryKey}`, 'yellow'));
|
||||
console.log(colorText(`Manifest path: ${manifestPath}`, 'yellow'));
|
||||
return false; // Windows上如果注册表项创建失败,整个注册过程应该视为失败
|
||||
}
|
||||
}
|
||||
|
||||
console.log(colorText('Successfully registered user-level Native Messaging host!', 'green'));
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log(
|
||||
colorText(
|
||||
`User-level registration failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
'yellow',
|
||||
),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 导入is-admin包(仅在Windows平台使用)
|
||||
let isAdmin: () => boolean = () => false;
|
||||
if (process.platform === 'win32') {
|
||||
try {
|
||||
isAdmin = require('is-admin');
|
||||
} catch (error) {
|
||||
console.warn('缺少is-admin依赖,Windows平台下可能无法正确检测管理员权限');
|
||||
console.warn(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用提升权限注册系统级清单
|
||||
*/
|
||||
export async function registerWithElevatedPermissions(): Promise<void> {
|
||||
try {
|
||||
console.log(colorText('Attempting to register system-level manifest...', 'blue'));
|
||||
|
||||
// 1. 确保执行权限
|
||||
await ensureExecutionPermissions();
|
||||
|
||||
// 2. 准备清单内容
|
||||
const manifest = await createManifestContent();
|
||||
|
||||
// 3. 获取系统级清单路径
|
||||
const manifestPath = getSystemManifestPath();
|
||||
|
||||
// 4. 创建临时清单文件
|
||||
const tempManifestPath = path.join(os.tmpdir(), `${HOST_NAME}.json`);
|
||||
await writeFile(tempManifestPath, JSON.stringify(manifest, null, 2));
|
||||
|
||||
// 5. 检测是否已经有管理员权限
|
||||
const isRoot = process.getuid && process.getuid() === 0; // Unix/Linux/Mac
|
||||
const hasAdminRights = process.platform === 'win32' ? isAdmin() : false; // Windows平台检测管理员权限
|
||||
const hasElevatedPermissions = isRoot || hasAdminRights;
|
||||
|
||||
// 准备命令
|
||||
const command =
|
||||
os.platform() === 'win32'
|
||||
? `if not exist "${path.dirname(manifestPath)}" mkdir "${path.dirname(manifestPath)}" && copy "${tempManifestPath}" "${manifestPath}"`
|
||||
: `mkdir -p "${path.dirname(manifestPath)}" && cp "${tempManifestPath}" "${manifestPath}" && chmod 644 "${manifestPath}"`;
|
||||
|
||||
if (hasElevatedPermissions) {
|
||||
// 已经有管理员权限,直接执行命令
|
||||
try {
|
||||
// 创建目录
|
||||
if (!fs.existsSync(path.dirname(manifestPath))) {
|
||||
fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
|
||||
}
|
||||
|
||||
// 复制文件
|
||||
fs.copyFileSync(tempManifestPath, manifestPath);
|
||||
|
||||
// 设置权限(非Windows平台)
|
||||
if (os.platform() !== 'win32') {
|
||||
fs.chmodSync(manifestPath, '644');
|
||||
}
|
||||
|
||||
console.log(colorText('System-level manifest registration successful!', 'green'));
|
||||
} catch (error: any) {
|
||||
console.error(
|
||||
colorText(`System-level manifest installation failed: ${error.message}`, 'red'),
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
// 没有管理员权限,打印手动操作提示
|
||||
console.log(
|
||||
colorText('⚠️ Administrator privileges required for system-level installation', 'yellow'),
|
||||
);
|
||||
console.log(
|
||||
colorText(
|
||||
'Please run one of the following commands with administrator privileges:',
|
||||
'blue',
|
||||
),
|
||||
);
|
||||
|
||||
if (os.platform() === 'win32') {
|
||||
console.log(colorText(' 1. Open Command Prompt as Administrator and run:', 'blue'));
|
||||
console.log(colorText(` ${command}`, 'cyan'));
|
||||
} else {
|
||||
console.log(colorText(' 1. Run with sudo:', 'blue'));
|
||||
console.log(colorText(` sudo ${command}`, 'cyan'));
|
||||
}
|
||||
|
||||
console.log(
|
||||
colorText(' 2. Or run the registration command with elevated privileges:', 'blue'),
|
||||
);
|
||||
console.log(colorText(` sudo ${COMMAND_NAME} register --system`, 'cyan'));
|
||||
|
||||
throw new Error('Administrator privileges required for system-level installation');
|
||||
}
|
||||
|
||||
// 6. Windows特殊处理 - 设置系统级注册表
|
||||
if (os.platform() === 'win32') {
|
||||
const registryKey = `HKLM\\Software\\Google\\Chrome\\NativeMessagingHosts\\${HOST_NAME}`;
|
||||
// 确保路径使用正确的转义格式
|
||||
const escapedPath = manifestPath.replace(/\\/g, '\\\\');
|
||||
const regCommand = `reg add "${registryKey}" /ve /t REG_SZ /d "${escapedPath}" /f`;
|
||||
|
||||
console.log(colorText(`Creating system registry entry: ${registryKey}`, 'blue'));
|
||||
console.log(colorText(`Manifest path: ${manifestPath}`, 'blue'));
|
||||
|
||||
if (hasElevatedPermissions) {
|
||||
// 已经有管理员权限,直接执行注册表命令
|
||||
try {
|
||||
execSync(regCommand, { stdio: 'pipe' });
|
||||
|
||||
// 验证注册表项是否创建成功
|
||||
if (verifyWindowsRegistryEntry(registryKey, manifestPath)) {
|
||||
console.log(colorText('Windows registry entry created successfully!', 'green'));
|
||||
} else {
|
||||
console.log(colorText('⚠️ Registry entry created but verification failed', 'yellow'));
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(
|
||||
colorText(`Windows registry entry creation failed: ${error.message}`, 'red'),
|
||||
);
|
||||
console.error(colorText(`Command: ${regCommand}`, 'red'));
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
// 没有管理员权限,打印手动操作提示
|
||||
console.log(
|
||||
colorText(
|
||||
'⚠️ Administrator privileges required for Windows registry modification',
|
||||
'yellow',
|
||||
),
|
||||
);
|
||||
console.log(colorText('Please run the following command as Administrator:', 'blue'));
|
||||
console.log(colorText(` ${regCommand}`, 'cyan'));
|
||||
console.log(colorText('Or run the registration command with elevated privileges:', 'blue'));
|
||||
console.log(
|
||||
colorText(
|
||||
` Run Command Prompt as Administrator and execute: ${COMMAND_NAME} register --system`,
|
||||
'cyan',
|
||||
),
|
||||
);
|
||||
|
||||
throw new Error('Administrator privileges required for Windows registry modification');
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(colorText(`注册失败: ${error.message}`, 'red'));
|
||||
throw error;
|
||||
}
|
||||
}
|
282
app/native-server/src/server/index.ts
Normal file
282
app/native-server/src/server/index.ts
Normal file
@@ -0,0 +1,282 @@
|
||||
import Fastify, { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
||||
import cors from '@fastify/cors';
|
||||
import {
|
||||
NATIVE_SERVER_PORT,
|
||||
TIMEOUTS,
|
||||
SERVER_CONFIG,
|
||||
HTTP_STATUS,
|
||||
ERROR_MESSAGES,
|
||||
} from '../constant';
|
||||
import { NativeMessagingHost } from '../native-messaging-host';
|
||||
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
||||
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
|
||||
import { getMcpServer } from '../mcp/mcp-server';
|
||||
|
||||
// Define request body type (if data needs to be retrieved from HTTP requests)
|
||||
interface ExtensionRequestPayload {
|
||||
data?: any; // Data you want to pass to the extension
|
||||
}
|
||||
|
||||
export class Server {
|
||||
private fastify: FastifyInstance;
|
||||
public isRunning = false; // Changed to public or provide a getter
|
||||
private nativeHost: NativeMessagingHost | null = null;
|
||||
private transportsMap: Map<string, StreamableHTTPServerTransport | SSEServerTransport> =
|
||||
new Map();
|
||||
|
||||
constructor() {
|
||||
this.fastify = Fastify({ logger: SERVER_CONFIG.LOGGER_ENABLED });
|
||||
this.setupPlugins();
|
||||
this.setupRoutes();
|
||||
}
|
||||
/**
|
||||
* Associate NativeMessagingHost instance
|
||||
*/
|
||||
public setNativeHost(nativeHost: NativeMessagingHost): void {
|
||||
this.nativeHost = nativeHost;
|
||||
}
|
||||
|
||||
private async setupPlugins(): Promise<void> {
|
||||
await this.fastify.register(cors, {
|
||||
origin: SERVER_CONFIG.CORS_ORIGIN,
|
||||
});
|
||||
}
|
||||
|
||||
private setupRoutes(): void {
|
||||
// for ping
|
||||
this.fastify.get(
|
||||
'/ask-extension',
|
||||
async (request: FastifyRequest<{ Body: ExtensionRequestPayload }>, reply: FastifyReply) => {
|
||||
|
||||
if (!this.nativeHost) {
|
||||
return reply
|
||||
.status(HTTP_STATUS.INTERNAL_SERVER_ERROR)
|
||||
.send({ error: ERROR_MESSAGES.NATIVE_HOST_NOT_AVAILABLE });
|
||||
}
|
||||
if (!this.isRunning) {
|
||||
return reply
|
||||
.status(HTTP_STATUS.INTERNAL_SERVER_ERROR)
|
||||
.send({ error: ERROR_MESSAGES.SERVER_NOT_RUNNING });
|
||||
}
|
||||
|
||||
try {
|
||||
// wait from extension message
|
||||
const extensionResponse = await this.nativeHost.sendRequestToExtensionAndWait(
|
||||
request.query,
|
||||
'process_data',
|
||||
TIMEOUTS.EXTENSION_REQUEST_TIMEOUT,
|
||||
);
|
||||
return reply.status(HTTP_STATUS.OK).send({ status: 'success', data: extensionResponse });
|
||||
} catch (error: any) {
|
||||
if (error.message.includes('timed out')) {
|
||||
return reply
|
||||
.status(HTTP_STATUS.GATEWAY_TIMEOUT)
|
||||
.send({ status: 'error', message: ERROR_MESSAGES.REQUEST_TIMEOUT });
|
||||
} else {
|
||||
return reply.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).send({
|
||||
status: 'error',
|
||||
message: `Failed to get response from extension: ${error.message}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Compatible with SSE
|
||||
this.fastify.get('/sse', async (_, reply) => {
|
||||
try {
|
||||
// Set SSE headers
|
||||
reply.raw.writeHead(HTTP_STATUS.OK, {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
Connection: 'keep-alive',
|
||||
});
|
||||
|
||||
// Create SSE transport
|
||||
const transport = new SSEServerTransport('/messages', reply.raw);
|
||||
this.transportsMap.set(transport.sessionId, transport);
|
||||
|
||||
reply.raw.on('close', () => {
|
||||
this.transportsMap.delete(transport.sessionId);
|
||||
});
|
||||
|
||||
const server = getMcpServer();
|
||||
await server.connect(transport);
|
||||
|
||||
// Keep connection open
|
||||
reply.raw.write(':\n\n');
|
||||
} catch (error) {
|
||||
if (!reply.sent) {
|
||||
reply.code(HTTP_STATUS.INTERNAL_SERVER_ERROR).send(ERROR_MESSAGES.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Compatible with SSE
|
||||
this.fastify.post('/messages', async (req, reply) => {
|
||||
try {
|
||||
const { sessionId } = req.query as any;
|
||||
const transport = this.transportsMap.get(sessionId) as SSEServerTransport;
|
||||
if (!sessionId || !transport) {
|
||||
reply.code(HTTP_STATUS.BAD_REQUEST).send('No transport found for sessionId');
|
||||
return;
|
||||
}
|
||||
|
||||
await transport.handlePostMessage(req.raw, reply.raw, req.body);
|
||||
} catch (error) {
|
||||
if (!reply.sent) {
|
||||
reply.code(HTTP_STATUS.INTERNAL_SERVER_ERROR).send(ERROR_MESSAGES.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// POST /mcp: Handle client-to-server messages
|
||||
this.fastify.post('/mcp', async (request, reply) => {
|
||||
const sessionId = request.headers['mcp-session-id'] as string | undefined;
|
||||
let transport: StreamableHTTPServerTransport | undefined = this.transportsMap.get(
|
||||
sessionId || '',
|
||||
) as StreamableHTTPServerTransport;
|
||||
if (transport) {
|
||||
// transport found, do nothing
|
||||
} else if (!sessionId && isInitializeRequest(request.body)) {
|
||||
const newSessionId = randomUUID(); // Generate session ID
|
||||
transport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: () => newSessionId, // Use pre-generated ID
|
||||
onsessioninitialized: (initializedSessionId) => {
|
||||
// Ensure transport instance exists and session ID matches
|
||||
if (transport && initializedSessionId === newSessionId) {
|
||||
this.transportsMap.set(initializedSessionId, transport);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
transport.onclose = () => {
|
||||
if (transport?.sessionId && this.transportsMap.get(transport.sessionId)) {
|
||||
this.transportsMap.delete(transport.sessionId);
|
||||
}
|
||||
};
|
||||
await getMcpServer().connect(transport);
|
||||
} else {
|
||||
reply.code(HTTP_STATUS.BAD_REQUEST).send({ error: ERROR_MESSAGES.INVALID_MCP_REQUEST });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await transport.handleRequest(request.raw, reply.raw, request.body);
|
||||
} catch (error) {
|
||||
if (!reply.sent) {
|
||||
reply
|
||||
.code(HTTP_STATUS.INTERNAL_SERVER_ERROR)
|
||||
.send({ error: ERROR_MESSAGES.MCP_REQUEST_PROCESSING_ERROR });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.fastify.get('/mcp', async (request, reply) => {
|
||||
const sessionId = request.headers['mcp-session-id'] as string | undefined;
|
||||
const transport = sessionId
|
||||
? (this.transportsMap.get(sessionId) as StreamableHTTPServerTransport)
|
||||
: undefined;
|
||||
if (!transport) {
|
||||
reply.code(HTTP_STATUS.BAD_REQUEST).send({ error: ERROR_MESSAGES.INVALID_SSE_SESSION });
|
||||
return;
|
||||
}
|
||||
|
||||
reply.raw.setHeader('Content-Type', 'text/event-stream');
|
||||
reply.raw.setHeader('Cache-Control', 'no-cache');
|
||||
reply.raw.setHeader('Connection', 'keep-alive');
|
||||
reply.raw.flushHeaders(); // Ensure headers are sent immediately
|
||||
|
||||
try {
|
||||
// transport.handleRequest will take over the response stream
|
||||
await transport.handleRequest(request.raw, reply.raw);
|
||||
if (!reply.sent) {
|
||||
// If transport didn't send anything (unlikely for SSE initial handshake)
|
||||
reply.hijack(); // Prevent Fastify from automatically sending response
|
||||
}
|
||||
} catch (error) {
|
||||
if (!reply.raw.writableEnded) {
|
||||
reply.raw.end();
|
||||
}
|
||||
}
|
||||
|
||||
request.socket.on('close', () => {
|
||||
request.log.info(`SSE client disconnected for session: ${sessionId}`);
|
||||
// transport's onclose should handle its own cleanup
|
||||
});
|
||||
});
|
||||
|
||||
this.fastify.delete('/mcp', async (request, reply) => {
|
||||
const sessionId = request.headers['mcp-session-id'] as string | undefined;
|
||||
const transport = sessionId
|
||||
? (this.transportsMap.get(sessionId) as StreamableHTTPServerTransport)
|
||||
: undefined;
|
||||
|
||||
if (!transport) {
|
||||
reply.code(HTTP_STATUS.BAD_REQUEST).send({ error: ERROR_MESSAGES.INVALID_SESSION_ID });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await transport.handleRequest(request.raw, reply.raw);
|
||||
// Assume transport.handleRequest will send response or transport.onclose will cleanup
|
||||
if (!reply.sent) {
|
||||
reply.code(HTTP_STATUS.NO_CONTENT).send();
|
||||
}
|
||||
} catch (error) {
|
||||
if (!reply.sent) {
|
||||
reply
|
||||
.code(HTTP_STATUS.INTERNAL_SERVER_ERROR)
|
||||
.send({ error: ERROR_MESSAGES.MCP_SESSION_DELETION_ERROR });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async start(port = NATIVE_SERVER_PORT, nativeHost: NativeMessagingHost): Promise<void> {
|
||||
if (!this.nativeHost) {
|
||||
this.nativeHost = nativeHost; // Ensure nativeHost is set
|
||||
} else if (this.nativeHost !== nativeHost) {
|
||||
this.nativeHost = nativeHost; // Update to the passed instance
|
||||
}
|
||||
|
||||
if (this.isRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.fastify.listen({ port, host: SERVER_CONFIG.HOST });
|
||||
this.isRunning = true; // Update running status
|
||||
// No need to return, Promise resolves void by default
|
||||
} catch (err) {
|
||||
this.isRunning = false; // Startup failed, reset status
|
||||
// Throw error instead of exiting directly, let caller (possibly NativeHost) handle
|
||||
throw err; // or return Promise.reject(err);
|
||||
// process.exit(1); // Not recommended to exit directly here
|
||||
}
|
||||
}
|
||||
|
||||
public async stop(): Promise<void> {
|
||||
if (!this.isRunning) {
|
||||
return;
|
||||
}
|
||||
// this.nativeHost = null; // Not recommended to nullify here, association relationship may still be needed
|
||||
try {
|
||||
await this.fastify.close();
|
||||
this.isRunning = false; // Update running status
|
||||
} catch (err) {
|
||||
// Even if closing fails, mark as not running, but log the error
|
||||
this.isRunning = false;
|
||||
throw err; // Throw error
|
||||
}
|
||||
}
|
||||
|
||||
public getInstance(): FastifyInstance {
|
||||
return this.fastify;
|
||||
}
|
||||
}
|
||||
|
||||
const serverInstance = new Server();
|
||||
export default serverInstance;
|
27
app/native-server/src/server/server.test.ts
Normal file
27
app/native-server/src/server/server.test.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { describe, expect, test, afterAll, beforeAll } from '@jest/globals';
|
||||
import supertest from 'supertest';
|
||||
import Server from './index';
|
||||
|
||||
describe('服务器测试', () => {
|
||||
// 启动服务器测试实例
|
||||
beforeAll(async () => {
|
||||
await Server.getInstance().ready();
|
||||
});
|
||||
|
||||
// 关闭服务器
|
||||
afterAll(async () => {
|
||||
await Server.stop();
|
||||
});
|
||||
|
||||
test('GET /ping 应返回正确响应', async () => {
|
||||
const response = await supertest(Server.getInstance().server)
|
||||
.get('/ping')
|
||||
.expect(200)
|
||||
.expect('Content-Type', /json/);
|
||||
|
||||
expect(response.body).toEqual({
|
||||
status: 'ok',
|
||||
message: 'pong',
|
||||
});
|
||||
});
|
||||
});
|
45
app/native-server/src/util/logger.ts
Normal file
45
app/native-server/src/util/logger.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
// import { stderr } from 'process';
|
||||
// import * as fs from 'fs';
|
||||
// import * as path from 'path';
|
||||
|
||||
// // 设置日志文件路径
|
||||
// const LOG_DIR = path.join(
|
||||
// '/Users/hang/code/ai/chrome-mcp-server/app/native-server/dist/',
|
||||
// '.debug-log',
|
||||
// ); // 使用不同目录区分
|
||||
// const LOG_FILE = path.join(
|
||||
// LOG_DIR,
|
||||
// `native-host-${new Date().toISOString().replace(/:/g, '-')}.log`,
|
||||
// );
|
||||
// // 确保日志目录存在
|
||||
// if (!fs.existsSync(LOG_DIR)) {
|
||||
// try {
|
||||
// fs.mkdirSync(LOG_DIR, { recursive: true });
|
||||
// } catch (err) {
|
||||
// stderr.write(`[ERROR] 创建日志目录失败: ${err}\n`);
|
||||
// }
|
||||
// }
|
||||
|
||||
// // 日志函数
|
||||
// function writeLog(level: string, message: string): void {
|
||||
// const timestamp = new Date().toISOString();
|
||||
// const logMessage = `[${timestamp}] [${level}] ${message}\n`;
|
||||
|
||||
// // 写入到文件
|
||||
// try {
|
||||
// fs.appendFileSync(LOG_FILE, logMessage);
|
||||
// } catch (err) {
|
||||
// stderr.write(`[ERROR] 写入日志失败: ${err}\n`);
|
||||
// }
|
||||
|
||||
// // 同时输出到stderr(不影响native messaging协议)
|
||||
// stderr.write(logMessage);
|
||||
// }
|
||||
|
||||
// // 日志级别函数
|
||||
// export const logger = {
|
||||
// debug: (message: string) => writeLog('DEBUG', message),
|
||||
// info: (message: string) => writeLog('INFO', message),
|
||||
// warn: (message: string) => writeLog('WARN', message),
|
||||
// error: (message: string) => writeLog('ERROR', message),
|
||||
// };
|
19
app/native-server/tsconfig.json
Normal file
19
app/native-server/tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2018",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"lib": ["ES2018", "DOM"],
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"skipLibCheck": true,
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "**/*.spec.ts", "**/*.test.ts"]
|
||||
}
|
Reference in New Issue
Block a user