diff --git a/.gitignore b/.gitignore
index 07bf2802..d9d537af 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,6 +7,7 @@
!/qiming-file-server/
!/qiming-mobile/
!/qiming-claude-code-acp-ts/
+!/qiming-dev-inject/
!/qimingclaw/
!/qimingcode/
diff --git a/qiming-dev-inject/.gitignore b/qiming-dev-inject/.gitignore
new file mode 100644
index 00000000..fde99533
--- /dev/null
+++ b/qiming-dev-inject/.gitignore
@@ -0,0 +1,76 @@
+# 依赖
+node_modules/
+.npm/
+.pnp
+.pnp.js
+
+# 日志
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+pnpm-debug.log*
+
+# 缓存
+.cache/
+.parcel-cache/
+.npm/
+.eslintcache
+.next/
+out/
+dist/
+build/
+
+# 操作系统
+.DS_Store
+.DS_Store?
+._*
+.Spotlight-V100
+.Trashes
+ehthumbs.db
+Thumbs.db
+
+# 编辑器
+.vscode/
+.idea/
+*.swp
+*.swo
+*~
+.cursor/
+
+# 环境变量
+.env
+.env.local
+.env*.local
+.env.development
+.env.production
+.env.test
+
+# 测试项目依赖和构建
+test-projects/*/node_modules/
+test-projects/*/.next/
+test-projects/*/dist/
+test-projects/*/build/
+test-projects/*/.env*
+
+# 临时文件
+*.tmp
+*.temp
+server.log
+
+# 打包文件
+*.tgz
+*.tar.gz
+
+# 覆盖率报告
+coverage/
+.nyc_output/
+
+# IDE 临时文件
+*.code-workspace
+
+# Lock files
+pnpm-lock.yaml
+yarn.lock
+package-lock.json
diff --git a/qiming-dev-inject/.npmignore b/qiming-dev-inject/.npmignore
new file mode 100644
index 00000000..46266b57
--- /dev/null
+++ b/qiming-dev-inject/.npmignore
@@ -0,0 +1,73 @@
+# 测试文件和目录
+test/
+test-*.js
+test.html
+test-simplified.js
+test-projects/
+*.test.js
+
+# 示例和演示文件
+# EXAMPLES.md (保留在发布包中,因为用户可能需要)
+react-iframe-test.html
+simple-react-app.html
+simplified-test.html
+server.js
+start-test.js
+test-cli.js
+test-framework.js
+test-framework-simple.js
+test-full-system.js
+
+# 日志文件
+*.log
+server.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# 依赖
+node_modules/
+.pnp
+.pnp.js
+
+# 构建输出
+dist/
+build/
+.next/
+out/
+
+# 缓存
+.cache/
+.npm/
+.parcel-cache/
+.eslintcache
+
+# 环境配置
+.env
+.env.local
+.env*.local
+
+# IDE 配置
+.vscode/
+.idea/
+*.swp
+*.swo
+*~
+
+# 系统文件
+.DS_Store
+Thumbs.db
+*.tmp
+
+# Git
+.git/
+.gitignore
+.gitattributes
+
+# CI/CD
+.github/
+.gitlab-ci.yml
+.travis.yml
+
+# 文档(除了 package.json 中 files 指定的)
+# 注意:README.md 等会自动包含
diff --git a/qiming-dev-inject/EXAMPLES.md b/qiming-dev-inject/EXAMPLES.md
new file mode 100644
index 00000000..2aa27ddb
--- /dev/null
+++ b/qiming-dev-inject/EXAMPLES.md
@@ -0,0 +1,149 @@
+# 使用示例
+
+## 安装方式
+
+```bash
+# 静默安装(推荐,自动确认,无需交互)
+npx -y @xagi/dev-inject install --framework
+
+# 使用默认脚本地址 (/sdk/dev-monitor.js)
+npx @xagi/dev-inject install --framework
+
+# 使用自定义地址
+npx @xagi/dev-inject install --remote=http://localhost:9000/dev-monitor.js --framework
+
+# 使用 pnpm dlx
+pnpm dlx @xagi/dev-inject install --framework
+```
+
+## 1. 本地开发环境监控
+
+将监控脚本复制到你的项目静态资源目录:
+
+```bash
+# 复制监控脚本到你的项目中
+cp dev-inject/scripts/dev-monitor.js public/scripts/
+```
+
+然后注入到 HTML:
+
+```bash
+# 注入本地监控脚本
+npx @xagi/dev-inject install --remote=/scripts/dev-monitor.js
+
+# 或者指定特定 HTML 文件
+npx @xagi/dev-inject install --remote=/scripts/dev-monitor.js --file=./public/index.html
+```
+
+## 2. 远程开发服务器监控
+
+如果你的监控脚本运行在开发服务器上:
+
+```bash
+# 注入远程脚本
+npx @xagi/dev-inject install --remote=http://localhost:9000/dev-monitor.js
+
+# 或使用 pnpm
+pnpm dlx @xagi/dev-inject install --remote=http://localhost:9000/dev-monitor.js
+```
+
+## 3. 在 React/Vite/Next.js 项目中使用(框架感知模式)
+
+```bash
+# Vite 项目 - 使用框架感知注入(推荐)
+npx @xagi/dev-inject install --remote=http://localhost:9000/dev-monitor.js --framework
+
+# Next.js App Router 项目
+npx @xagi/dev-inject install --remote=http://localhost:9000/dev-monitor.js --framework
+
+# Next.js Pages Router 项目
+npx @xagi/dev-inject install --remote=/scripts/dev-monitor.js --framework
+
+# Create React App 项目
+npx @xagi/dev-inject install --remote=/scripts/dev-monitor.js --framework
+```
+
+## 4. 开发完成后清理
+
+```bash
+# 移除框架注入
+npx @xagi/dev-inject uninstall --framework
+
+# 移除传统注入
+npx @xagi/dev-inject uninstall
+
+# 只移除特定文件的注入脚本
+pnpm dlx @xagi/dev-inject uninstall --file=./index.html
+```
+
+## 5. 预览模式
+
+在实际注入前,可以先预览将要执行的操作:
+
+```bash
+npx @xagi/dev-inject install --remote=/scripts/dev-monitor.js --dry-run --verbose
+```
+
+## 监控面板使用
+
+在浏览器控制台中:
+
+```javascript
+// 显示监控面板
+DevMonitor.showPanel()
+
+// 获取监控数据
+DevMonitor.getData()
+
+// 导出数据到文件
+DevMonitor.exportData()
+
+// 清除监控数据
+DevMonitor.clearData()
+
+// 设置日志级别
+DevMonitor.setLogLevel('debug') // debug, info, warn, error
+```
+
+## 完整工作流示例
+
+```bash
+# 1. 进入项目目录
+cd my-project
+
+# 2. 预览注入操作(框架感知模式)
+npx @xagi/dev-inject install --remote=http://localhost:9000/dev-monitor.js --framework --dry-run
+
+# 3. 执行注入(框架感知模式)
+# 方式一:静默安装(推荐,一键完成)
+npx -y @xagi/dev-inject install --framework
+
+# 方式二:标准安装
+npx @xagi/dev-inject install --remote=http://localhost:9000/dev-monitor.js --framework
+
+# 4. 启动开发服务器
+npm run dev
+
+# 5. 在浏览器中测试功能,打开控制台使用 DevMonitor API
+
+# 6. 开发完成后清理注入
+npx @xagi/dev-inject uninstall --framework
+```
+
+## 快速启动示例
+
+使用远程监控脚本(推荐):
+
+```bash
+# 静默安装(最推荐,一键完成)
+npx -y @xagi/dev-inject install --framework
+
+# 框架感知注入(自动检测最佳注入方式)
+npx @xagi/dev-inject install --remote=http://localhost:9000/dev-monitor.js --framework
+
+# 或使用 pnpm
+pnpm dlx @xagi/dev-inject install --remote=http://localhost:9000/dev-monitor.js --framework
+
+# 卸载
+npx @xagi/dev-inject uninstall --framework
+```
diff --git a/qiming-dev-inject/LICENSE b/qiming-dev-inject/LICENSE
new file mode 100644
index 00000000..35137f43
--- /dev/null
+++ b/qiming-dev-inject/LICENSE
@@ -0,0 +1,22 @@
+MIT License
+
+Copyright (c) 2024 dongdada29
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
diff --git a/qiming-dev-inject/README.md b/qiming-dev-inject/README.md
new file mode 100644
index 00000000..b8542e1f
--- /dev/null
+++ b/qiming-dev-inject/README.md
@@ -0,0 +1,257 @@
+# 网页应用智能脚本注入
+
+🚀 **框架感知的开发环境脚本注入工具**
+
+支持 Vite、Next.js、React 等现代框架的智能脚本注入,提供零侵入式的开发期监控方案。
+
+## 🌟 特性
+
+### 框架感知注入
+- 🟢 **Vite**: 自动注入 Vite 插件到 `vite.config.js`
+- 🟢 **Next.js App Router**: 注入到 `app/layout.tsx`
+- 🟢 **Next.js Pages Router**: 注入到 `pages/_document.tsx`
+- 🟢 **Create React App**: 注入到 `public/index.html`
+- 🟢 **传统 HTML**: 直接修改 HTML 文件
+
+### 核心功能
+- ✅ 支持远程 URL 和绝对路径注入
+- ✅ 自动检测项目类型和最佳注入方式
+- ✅ 智能去重,避免重复注入
+- ✅ 支持卸载功能
+- ✅ 预览模式(dry-run)
+- ✅ 详细日志输出
+- ✅ 零侵入式修改
+
+## 安装
+
+使用 npx(推荐):
+
+```bash
+# 查看帮助信息
+npx @xagi/dev-inject --help
+
+# 查看版本信息
+npx @xagi/dev-inject --version
+
+# 静默安装(自动确认,无需交互)-y 选项
+npx -y @xagi/dev-inject install --framework
+```
+
+或使用 pnpm dlx:
+
+```bash
+pnpm dlx @xagi/dev-inject --help
+```
+
+## 🚀 快速开始
+
+### 框架感知注入(推荐)
+
+```bash
+# 静默安装(推荐,无需确认)
+npx -y @xagi/dev-inject install --framework
+
+# 使用默认脚本地址 (/sdk/dev-monitor.js)
+npx @xagi/dev-inject install --framework
+
+# 或指定自定义地址
+npx @xagi/dev-inject install --remote=http://localhost:9000/dev-monitor.js --framework
+
+# 使用 pnpm
+pnpm dlx @xagi/dev-inject install --framework
+
+# 查看检测到的项目类型
+npx @xagi/dev-inject install --framework --verbose
+```
+
+### 传统 HTML 注入
+
+```bash
+# 注入远程脚本
+npx @xagi/dev-inject install --remote=http://localhost:9000/dev-monitor.js
+
+# 注入本地脚本
+npx @xagi/dev-inject install --remote=/scripts/dev-monitor.js
+
+# 指定特定文件
+npx @xagi/dev-inject install --remote=/scripts/monitor.js --file=./public/index.html
+```
+
+### 预览和卸载
+
+```bash
+# 预览将要执行的操作
+npx @xagi/dev-inject install --remote=/scripts/monitor.js --framework --dry-run
+
+# 移除框架注入
+npx @xagi/dev-inject uninstall --framework
+
+# 移除传统注入
+npx @xagi/dev-inject uninstall
+```
+
+## 📋 支持的脚本格式
+
+### 远程 URL
+```bash
+--remote=http://localhost:9000/dev-monitor.js
+--remote=https://cdn.example.com/monitor.js
+```
+
+### 绝对路径
+```bash
+--remote=/scripts/dev-monitor.js
+--remote=/assets/monitor.js
+```
+
+## 🎯 配置选项
+
+| 选项 | 描述 | 示例 |
+|------|------|------|
+| `--remote` | 脚本地址(可选,默认值: /sdk/dev-monitor.js) | `--remote=/scripts/dev-monitor.js` |
+| `--framework, -f` | 使用框架感知注入 | `--framework` |
+| `--file` | 指定 HTML 文件 | `--file=./public/index.html` |
+| `--dry-run` | 预览模式 | `--dry-run` |
+| `--verbose` | 详细输出 | `--verbose` |
+| `--help` | 显示帮助 | `--help` |
+| `--version` | 显示版本 | `--version` |
+
+## 监控脚本功能
+
+`dev-inject/scripts/dev-monitor.js` 提供完整的开发监控功能:
+
+### 错误监控
+- 全局 JavaScript 错误捕获
+- Promise Rejection 监控
+- 资源加载错误监控
+- iframe 通信错误
+
+### 性能监控
+- 页面加载时间分析
+- 网络请求时间统计
+- 长任务检测
+- DOM Ready 性能
+
+### 开发工具
+- 实时监控面板
+- 数据导出功能
+- 控制台日志拦截
+- iframe 通信支持
+
+### 使用监控面板
+```javascript
+// 在浏览器控制台执行
+DevMonitor.showPanel(); // 显示监控面板
+DevMonitor.getData(); // 获取监控数据
+DevMonitor.exportData(); // 导出监控数据
+DevMonitor.clearData(); // 清除监控数据
+```
+
+## 配置选项
+
+| 选项 | 描述 | 示例 |
+|------|------|------|
+| `--remote` | 脚本地址(必需) | `--remote=/scripts/dev-monitor.js` |
+| `--file` | 指定 HTML 文件 | `--file=./public/index.html` |
+| `--dry-run` | 预览模式 | `--dry-run` |
+| `--verbose` | 详细输出 | `--verbose` |
+| `--help` | 显示帮助 | `--help` |
+| `--version` | 显示版本 | `--version` |
+
+## 工作原理
+
+1. **查找 HTML 文件**:自动查找项目中的 HTML 文件(优先 index.html)
+2. **脚本注入**:在 `` 标签前注入脚本
+3. **去重处理**:移除之前注入的相同脚本,避免重复
+4. **智能检测**:支持多种项目结构(React、Vite、Next.js 等)
+
+## 开发和测试
+
+```bash
+# 进入项目目录
+cd dev-inject
+
+# 安装依赖
+npm install
+
+# 链接到全局(用于本地测试)
+npm link
+
+# 测试帮助信息
+dev-inject --help
+
+# 测试注入功能(预览模式)
+dev-inject install --remote=/scripts/dev-monitor.js --dry-run
+
+# 取消链接
+npm unlink
+```
+
+## 📦 发布到 npm
+
+```bash
+# 确保你已经登录 npm
+npm login
+
+# 发布到 npm
+npm publish
+
+# 发布后验证
+npx @xagi/dev-inject --version
+```
+
+## 项目结构
+
+```
+dev-inject/
+├── bin/
+│ └── index.js # CLI 入口文件
+├── lib/
+│ ├── args.js # 参数解析
+│ ├── inject.js # 核心注入功能
+│ ├── utils.js # 工具函数
+│ └── help.js # 帮助信息
+├── scripts/
+│ └── dev-monitor.js # 监控脚本
+├── package.json # 项目配置
+└── README.md # 说明文档
+```
+
+## 兼容性
+
+- ✅ Node.js >= 14.0.0
+- ✅ 所有现代浏览器
+- ✅ React、Vue、Angular、Next.js 等
+- ✅ Vite、Webpack、Create React App 等构建工具
+
+## 📦 发布配置
+
+本项目已配置 npm publish 时仅包含必要的文件:
+
+**包含的文件:**
+- ✅ `bin/` - CLI 可执行文件
+- ✅ `lib/` - 核心库文件
+- ✅ `scripts/` - 监控脚本
+- ✅ `*.md` - 文档文件
+- ✅ `LICENSE` - 许可证
+
+**排除的文件:**
+- ❌ `test/` - 测试文件
+- ❌ `test-projects/` - 测试项目
+- ❌ 所有 `*.test.js` 文件
+- ❌ 测试和演示 HTML 文件
+- ❌ 日志和缓存文件
+- ❌ 开发工具配置
+
+完整配置请查看 `package.json` 的 `files` 字段和 `.npmignore` 文件。
+
+## 注意事项
+
+1. **绝对路径**:确保静态文件服务器可以访问该路径
+2. **重复注入**:工具会自动处理重复注入问题
+3. **权限**:确保有 HTML 文件的写入权限
+4. **备份**:建议在操作前备份重要文件
+
+## 许可证
+
+MIT
diff --git a/qiming-dev-inject/bin/index.js b/qiming-dev-inject/bin/index.js
new file mode 100644
index 00000000..96eaeb7c
--- /dev/null
+++ b/qiming-dev-inject/bin/index.js
@@ -0,0 +1,54 @@
+#!/usr/bin/env node
+
+import { fileURLToPath } from 'url';
+import { dirname, join } from 'path';
+import { parseArgs } from '../lib/args.js';
+import { installScript } from '../lib/inject.js';
+import { uninstallScript } from '../lib/inject.js';
+import { showHelp, showVersion } from '../lib/help.js';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+
+async function main() {
+ try {
+ const args = process.argv.slice(2);
+
+ // 显示帮助信息
+ if (args.includes('--help') || args.includes('-h')) {
+ showHelp();
+ return;
+ }
+
+ // 显示版本信息
+ if (args.includes('--version') || args.includes('-v')) {
+ showVersion();
+ return;
+ }
+
+ const parsed = parseArgs(args);
+
+ if (!parsed.command) {
+ console.error('❌ 错误:请指定命令 (install 或 uninstall)');
+ console.log('使用 --help 查看帮助信息');
+ process.exit(1);
+ }
+
+ switch (parsed.command) {
+ case 'install':
+ await installScript(parsed.options);
+ break;
+ case 'uninstall':
+ await uninstallScript(parsed.options);
+ break;
+ default:
+ console.error(`❌ 未知命令: ${parsed.command}`);
+ process.exit(1);
+ }
+ } catch (error) {
+ console.error('❌ 执行失败:', error.message);
+ process.exit(1);
+ }
+}
+
+main();
diff --git a/qiming-dev-inject/lib/args.js b/qiming-dev-inject/lib/args.js
new file mode 100644
index 00000000..e71b2b07
--- /dev/null
+++ b/qiming-dev-inject/lib/args.js
@@ -0,0 +1,48 @@
+
+export function parseArgs(args) {
+ const result = {
+ command: null,
+ options: {},
+ };
+
+ // 解析命令
+ if (args[0] === 'install' || args[0] === 'uninstall') {
+ result.command = args[0];
+ args = args.slice(1);
+ } else if (args.length === 0) {
+ return result;
+ }
+
+ // 解析选项
+ for (let i = 0; i < args.length; i++) {
+ const arg = args[i];
+
+ if (arg.startsWith('--remote=')) {
+ result.options.remote = arg.split('=')[1];
+ } else if (arg === '--remote' && i + 1 < args.length) {
+ result.options.remote = args[i + 1];
+ i++; // 跳过下一个参数
+ } else if (arg.startsWith('--file=')) {
+ result.options.file = arg.split('=')[1];
+ } else if (arg === '--file' && i + 1 < args.length) {
+ result.options.file = args[i + 1];
+ i++; // 跳过下一个参数
+ } else if (arg === '--dry-run') {
+ result.options.dryRun = true;
+ } else if (arg === '--verbose') {
+ result.options.verbose = true;
+ } else if (arg === '--framework' || arg === '-f') {
+ result.options.framework = true;
+ } else if (!arg.startsWith('--')) {
+ // 未知参数,可能是错误输入
+ console.warn(`⚠️ 忽略未知参数: ${arg}`);
+ }
+ }
+
+ // 为 install 命令设置 remote 默认值
+ if (result.command === 'install' && !result.options.remote) {
+ result.options.remote = '/sdk/dev-monitor.js';
+ }
+
+ return result;
+}
diff --git a/qiming-dev-inject/lib/code-cleaner.js b/qiming-dev-inject/lib/code-cleaner.js
new file mode 100644
index 00000000..673ed8fa
--- /dev/null
+++ b/qiming-dev-inject/lib/code-cleaner.js
@@ -0,0 +1,112 @@
+/**
+ * 代码清理工具类 - 清理多余的空白字符和逗号
+ */
+export class CodeCleaner {
+ /**
+ * 清理多余的空白行
+ * @param {string} content - 要清理的内容
+ * @param {boolean} preserveDoubleNewline - 是否保留双换行(默认 true)
+ * @returns {string} 清理后的内容
+ */
+ static cleanBlankLines(content, preserveDoubleNewline = true) {
+ if (preserveDoubleNewline) {
+ // 保留单行空白,清理多余的连续空白行
+ return content.replace(/\n\s*\n\s*\n+/g, '\n\n');
+ } else {
+ // 清理所有多余空白行,只保留单个换行
+ return content.replace(/\n\s*\n+/g, '\n');
+ }
+ }
+
+ /**
+ * 清理行尾的空白字符
+ * @param {string} content - 要清理的内容
+ * @returns {string} 清理后的内容
+ */
+ static cleanTrailingSpaces(content) {
+ return content.replace(/[ \t]+$/gm, '');
+ }
+
+ /**
+ * 清理多余的逗号
+ * @param {string} content - 要清理的内容
+ * @returns {string} 清理后的内容
+ */
+ static cleanCommas(content) {
+ // 1. 清理单独的逗号行(整行都是逗号和空白)
+ content = content.replace(/^\s*,\s*\n/gm, '');
+ // 2. 清理行首的逗号和空白
+ content = content.replace(/^\s*,\s+/gm, '');
+ // 3. 清理连续的多个逗号
+ content = content.replace(/,\s*,\s*/g, ',');
+ return content;
+ }
+
+ /**
+ * 清理空的标签对
+ * @param {string} content - 要清理的内容
+ * @param {string} tagName - 标签名
+ * @returns {string} 清理后的内容
+ */
+ static cleanEmptyTags(content, tagName) {
+ const regex = new RegExp(`<${tagName}[^>]*>\\s*${tagName}>`, 'g');
+ return content.replace(regex, '');
+ }
+
+ /**
+ * 清理标签中间的空白行
+ * @param {string} content - 要清理的内容
+ * @param {string} tagName - 标签名(可以是正则表达式)
+ * @returns {string} 清理后的内容
+ */
+ static cleanTagWhitespace(content, tagName) {
+ // 清理如
\n\n\n -> \n
+ content = content.replace(
+ new RegExp(`(<${tagName}[^>]*>)\\s*\\n\\s*\\n(\\s*${tagName}>)`, 'g'),
+ '$1\n$2'
+ );
+ return content;
+ }
+
+ /**
+ * 综合清理:清理所有常见的代码格式问题
+ * @param {string} content - 要清理的内容
+ * @param {object} options - 清理选项
+ * @returns {string} 清理后的内容
+ */
+ static cleanAll(content, options = {}) {
+ const {
+ blankLines = true,
+ trailingSpaces = true,
+ commas = false,
+ emptyTags = true,
+ tagWhitespace = true
+ } = options;
+
+ if (blankLines) {
+ content = this.cleanBlankLines(content);
+ }
+
+ if (trailingSpaces) {
+ content = this.cleanTrailingSpaces(content);
+ }
+
+ if (commas) {
+ content = this.cleanCommas(content);
+ }
+
+ if (emptyTags) {
+ ['head', 'Head'].forEach(tag => {
+ content = this.cleanEmptyTags(content, tag);
+ });
+ }
+
+ if (tagWhitespace) {
+ ['Head', 'head'].forEach(tag => {
+ content = this.cleanTagWhitespace(content, tag);
+ });
+ }
+
+ return content;
+ }
+}
diff --git a/qiming-dev-inject/lib/file-finder.js b/qiming-dev-inject/lib/file-finder.js
new file mode 100644
index 00000000..f87bc5f1
--- /dev/null
+++ b/qiming-dev-inject/lib/file-finder.js
@@ -0,0 +1,133 @@
+import fs from 'fs';
+import path from 'path';
+import { lookupFiles } from './utils.js';
+
+/**
+ * 文件查找工具类 - 支持多种后缀名尝试机制
+ */
+export class FileFinder {
+ /**
+ * 查找具有多种后缀名的文件
+ * @param {string} basePath - 基础路径(不包含后缀)
+ * @param {string[]} extensions - 后缀名数组,按优先级排序
+ * @param {string} startDir - 起始目录
+ * @returns {string|null} 找到的文件路径或 null
+ */
+ static findWithExtensions(basePath, extensions = ['.tsx', '.ts', '.jsx', '.js'], startDir = process.cwd()) {
+ for (const ext of extensions) {
+ const fullPath = path.join(startDir, basePath + ext);
+ if (fs.existsSync(fullPath)) {
+ return basePath + ext;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * 查找多个候选文件
+ * @param {string[]} candidates - 候选文件路径数组
+ * @param {string} startDir - 起始目录
+ * @returns {string[]} 找到的文件路径数组
+ */
+ static findMultiple(candidates, startDir = process.cwd()) {
+ const foundFiles = [];
+ for (const candidate of candidates) {
+ if (candidate === '*.html') {
+ // 特殊处理:查找所有 HTML 文件
+ const htmlFiles = lookupFiles(startDir);
+ foundFiles.push(...htmlFiles);
+ } else if (fs.existsSync(path.join(startDir, candidate))) {
+ foundFiles.push(candidate);
+ }
+ }
+ return foundFiles;
+ }
+
+ /**
+ * 查找框架特定的入口文件
+ * @param {string} projectType - 项目类型
+ * @param {string} startDir - 起始目录
+ * @returns {string[]} 找到的文件路径数组
+ */
+ static findFrameworkEntry(projectType, startDir = process.cwd()) {
+ const foundFiles = [];
+
+ switch (projectType) {
+ case 'vite':
+ // 尝试查找 vite.config 文件
+ const viteConfig = this.findWithExtensions('vite.config', ['.js', '.ts', '.mjs'], startDir);
+ if (viteConfig) {
+ foundFiles.push(viteConfig);
+ }
+ break;
+
+ case 'next-pages':
+ // 尝试查找 _document 文件
+ const documentFile = this.findWithExtensions('pages/_document', ['.tsx', '.jsx', '.ts', '.js'], startDir);
+ if (documentFile) {
+ foundFiles.push(documentFile);
+ }
+
+ // 尝试查找 _app 文件
+ const appFile = this.findWithExtensions('pages/_app', ['.tsx', '.jsx', '.ts', '.js'], startDir);
+ if (appFile) {
+ foundFiles.push(appFile);
+ }
+ break;
+
+ case 'next-app':
+ // 尝试查找 layout 文件
+ const layoutFile = this.findWithExtensions('app/layout', ['.tsx', '.jsx', '.ts', '.js'], startDir);
+ if (layoutFile) {
+ foundFiles.push(layoutFile);
+ }
+
+ // 尝试查找 globals.css
+ const globalsCss = 'app/globals.css';
+ if (fs.existsSync(path.join(startDir, globalsCss))) {
+ foundFiles.push(globalsCss);
+ }
+ break;
+
+ case 'next-hybrid':
+ // 尝试查找 app/layout 文件
+ const appLayoutFile = this.findWithExtensions('app/layout', ['.tsx', '.jsx', '.ts', '.js'], startDir);
+ if (appLayoutFile) {
+ foundFiles.push(appLayoutFile);
+ }
+
+ // 尝试查找 pages/_document 文件
+ const pagesDocumentFile = this.findWithExtensions('pages/_document', ['.tsx', '.jsx', '.ts', '.js'], startDir);
+ if (pagesDocumentFile) {
+ foundFiles.push(pagesDocumentFile);
+ }
+ break;
+
+ case 'create-react-app':
+ // 尝试查找 HTML 文件
+ const htmlFiles = ['public/index.html', 'index.html'];
+ foundFiles.push(...this.findMultiple(htmlFiles, startDir));
+ break;
+
+ case 'html-static':
+ // 尝试查找 HTML 文件
+ const staticHtmlFiles = ['index.html', '*.html'];
+ foundFiles.push(...this.findMultiple(staticHtmlFiles, startDir));
+ break;
+
+ default:
+ // 默认查找 HTML 文件
+ const defaultHtmlFiles = ['index.html', '*.html'];
+ foundFiles.push(...this.findMultiple(defaultHtmlFiles, startDir));
+ break;
+ }
+
+ return foundFiles;
+ }
+}
+
+// 导出便捷方法
+export function findFrameworkEntry(projectType, startDir = process.cwd()) {
+ return FileFinder.findFrameworkEntry(projectType, startDir);
+}
+
diff --git a/qiming-dev-inject/lib/framework-inject.js b/qiming-dev-inject/lib/framework-inject.js
new file mode 100644
index 00000000..9f7b1c9c
--- /dev/null
+++ b/qiming-dev-inject/lib/framework-inject.js
@@ -0,0 +1,850 @@
+import {
+ readFile,
+ writeFile,
+ parseRemoteType,
+ generateScriptTag,
+ injectScriptToHtml,
+ removeInjectedScripts,
+ log,
+ logInfo,
+ logSuccess,
+ logError
+} from './utils.js';
+import { FileFinder } from './file-finder.js';
+import { CodeCleaner } from './code-cleaner.js';
+import fs from 'fs';
+import path from 'path';
+
+// 检测项目类型
+export function detectProjectType(startDir = process.cwd()) {
+ const indicators = {
+ 'vite': ['vite.config.js', 'vite.config.ts'],
+ 'next-pages': ['next.config.js', 'pages/'],
+ 'next-app': ['next.config.js', 'app/'],
+ 'create-react-app': ['public/index.html', 'src/'],
+ 'webpack': ['webpack.config.js'],
+ 'html-static': ['index.html', '.html']
+ };
+
+ try {
+ // 按优先级顺序检测
+ const priorityOrder = ['vite', 'next-app', 'next-pages', 'create-react-app', 'webpack', 'html-static'];
+
+ for (const type of priorityOrder) {
+ const files = indicators[type];
+ for (const file of files) {
+ if (fs.existsSync(path.join(startDir, file))) {
+ // 特殊处理 Next.js
+ if (type.startsWith('next-')) {
+ const hasPages = fs.existsSync(path.join(startDir, 'pages'));
+ const hasApp = fs.existsSync(path.join(startDir, 'app'));
+
+ if (hasApp && hasPages) {
+ return 'next-hybrid'; // 两个目录都存在
+ } else if (hasApp) {
+ return 'next-app';
+ } else if (hasPages) {
+ return 'next-pages';
+ }
+ }
+ return type;
+ }
+ }
+ }
+ } catch (error) {
+ logError(`项目类型检测失败: ${error.message}`);
+ }
+
+ return 'unknown';
+}
+
+// 查找框架特定的入口文件 - 支持多次尝试机制
+export function findFrameworkEntry(projectType, startDir = process.cwd()) {
+ return FileFinder.findFrameworkEntry(projectType, startDir);
+}
+
+// 为 Vite 项目注入脚本插件
+export function injectToViteConfig(filePath, remote, scriptTag) {
+ try {
+ // 检查文件是否存在
+ if (!fs.existsSync(path.join(process.cwd(), filePath))) {
+ log(`Vite 配置文件 ${filePath} 不存在,跳过`, true);
+ return false;
+ }
+
+ let content = readFile(filePath);
+
+ // 先移除之前的注入(如果存在)- 支持新旧两种格式
+ // 使用字符串查找方式,确保能移除所有注入块(包括多个重复的情况)
+ const startMarker = '// ';
+ const endMarker = '// ';
+
+ let startIndex = content.indexOf(startMarker);
+ while (startIndex !== -1) {
+ const endIndex = content.indexOf(endMarker, startIndex);
+ if (endIndex !== -1) {
+ // 找到结束标记,移除从开始标记到结束标记后的所有内容
+ // 注意:逗号在 })() 和注释结束之间,所以会被包含在移除范围内
+ let removeEnd = endIndex + endMarker.length;
+ // 移除结束标记后的空白和换行
+ while (removeEnd < content.length && (content[removeEnd] === ' ' || content[removeEnd] === '\t' || content[removeEnd] === '\n')) {
+ removeEnd++;
+ }
+
+ // 移除整个块:从开始标记(包括前面的换行和空白)到结束标记后
+ // 向前查找,移除开始标记前的换行和空白
+ let removeStart = startIndex;
+ while (removeStart > 0 && (content[removeStart - 1] === ' ' || content[removeStart - 1] === '\t' || content[removeStart - 1] === '\n')) {
+ removeStart--;
+ if (content[removeStart] === '\n') {
+ removeStart++;
+ break;
+ }
+ }
+
+ content = content.slice(0, removeStart) + content.slice(removeEnd);
+ } else {
+ // 没有找到结束标记,只移除开始标记
+ content = content.slice(0, startIndex) + content.slice(startIndex + startMarker.length);
+ }
+ // 继续查找下一个注入块
+ startIndex = content.indexOf(startMarker);
+ }
+
+ // 移除旧版格式(向后兼容)
+ content = content.replace(
+ /\/\/\s*dev-inject-plugin[\s\S]*?}\s*}\s*,?\s*/g,
+ ''
+ );
+
+ // 清理多余的逗号和空行
+ // 移除连续的逗号
+ content = content.replace(/,\s*,/g, ',');
+ // 移除数组开头的逗号
+ content = content.replace(/\[\s*,/g, '[');
+ // 移除数组元素之间多余的逗号
+ content = content.replace(/,\s*\)/g, ')');
+ content = content.replace(/,\s*\n\s*\]/g, '\n ]');
+ content = content.replace(/,\s*\n\s*\[/g, ' [');
+ // 清理多余的空行(保留最多一个空行)
+ content = content.replace(/\n\s*\n\s*\n+/g, '\n\n');
+
+ // 生成插件代码,传入原始 remote URL 而不是 scriptTag
+ const pluginCode = generateVitePlugin(remote, scriptTag);
+
+ // 查找 plugins 数组,并在其中插入插件
+ const pluginsMatch = content.match(/(plugins:\s*\[)([\s\S]*?)(\])/);
+ if (pluginsMatch) {
+ const beforePlugins = pluginsMatch[1];
+ const pluginsContent = pluginsMatch[2].trim();
+ const afterPlugins = pluginsMatch[3];
+
+ // 插入插件到 plugins 数组开头
+ const newPlugins = beforePlugins + '\n ' + pluginCode + '\n ' + pluginsContent + '\n ' + afterPlugins;
+ content = content.replace(pluginsMatch[0], newPlugins);
+ } else {
+ // 如果没有 plugins 数组,需要创建
+ log(`未找到 plugins 配置,Vite 配置可能不标准: ${filePath}`, true);
+ return false;
+ }
+
+ writeFile(filePath, content);
+ logSuccess(`已向 ${filePath} 注入 Vite 插件`);
+ return true;
+
+ } catch (error) {
+ log(`Vite 插件注入失败: ${error.message}`, true);
+ return false;
+ }
+}
+// 更规范与健壮的版本
+function generateVitePlugin(remote, { insertPosition = '' } = {}) {
+ const safeRemote = JSON.stringify(remote); // 避免注入问题
+ const pluginName = 'dev-inject';
+ const scriptId = 'dev-inject-monitor';
+
+ const scriptInjection = `
+
+ `;
+
+ return `
+ //
+ {
+ name: '${pluginName}',
+ enforce: 'post', // 确保在 HTML 注入阶段最后执行
+ transformIndexHtml(html) {
+ if (!html.includes('data-id="${scriptId}"')) {
+ return html.replace(${JSON.stringify(insertPosition)}, \`${scriptInjection}\\n${insertPosition}\`);
+ }
+ return html;
+ }
+ },
+ //
+ `;
+}
+
+
+// 为 Next.js 注入脚本到 _document
+export function injectToNextDocument(filePath, scriptTag, remote = null) {
+ try {
+ // 检查文件是否存在
+ if (!fs.existsSync(path.join(process.cwd(), filePath))) {
+ log(`Next.js 文件 ${filePath} 不存在,跳过`, true);
+ return false;
+ }
+
+ let content = readFile(filePath);
+
+ // 检查是否是 _document.tsx/_document.js
+ if (filePath.includes('_document')) {
+ return injectToDocumentComponent(content, scriptTag, filePath, remote);
+ }
+
+ // 如果是 _app.tsx/_app.js
+ if (filePath.includes('_app')) {
+ return injectToAppComponent(content, scriptTag, filePath, remote);
+ }
+
+ log(`无法识别的 Next.js 文件类型: ${filePath}`, true);
+ return false;
+
+ } catch (error) {
+ log(`Next.js 注入失败: ${error.message}`, true);
+ return false;
+ }
+}
+
+// 注入到 Next.js _document 组件
+function injectToDocumentComponent(content, scriptTag, filePath, remote = null) {
+ const isTypeScript = filePath.endsWith('.tsx');
+
+ // 先移除之前的注入
+ content = removeNextDocumentInjection(content);
+
+ // 生成脚本加载代码(优先使用动态方式)
+ let scriptInjection;
+ if (remote) {
+ // 使用动态脚本加载器(推荐方式,防止缓存)
+ const scriptLoader = generateDynamicScriptLoader(remote);
+ scriptInjection = `{/* DEV-INJECT-START */}
+ {typeof window !== 'undefined' && (
+
+ )}
+ {/* DEV-INJECT-END */}`;
+ } else {
+ // 向后兼容:使用旧的 scriptTag 方式
+ scriptInjection = `{/* DEV-INJECT-START */}
+ {typeof window !== 'undefined' && (
+ ${scriptTag}
+ )}
+ {/* DEV-INJECT-END */}`;
+ }
+
+ // 查找 组件
+ const headRegex = /]*>([\s\S]*?)<\/Head>/;
+ const headMatch = content.match(headRegex);
+
+ if (headMatch) {
+ // 在 组件内注入 - 使用 DEV-INJECT-START/END 标识
+ const beforeHead = headMatch[1];
+ const newHead = beforeHead + '\n ' + scriptInjection;
+ // 修复多余的 < 符号问题
+ const headAttributes = headMatch[0].match(/]*)>/);
+ const attributes = headAttributes ? headAttributes[1] : '';
+ content = content.replace(headRegex, `\n ${newHead}\n `);
+ } else {
+ // 如果没有 组件,需要添加
+ const importReactMatch = content.match(/import.*React.*from.*react/);
+ if (importReactMatch) {
+ const afterImport = content.indexOf(importReactMatch[0]) + importReactMatch[0].length;
+ const headComponent = isTypeScript ? `
+import Head from 'next/head';` : `
+import Head from 'next/head';`;
+
+ content = content.slice(0, afterImport) + headComponent + content.slice(afterImport);
+ }
+
+ // 在 Document 组件的 render 方法中添加 Head
+ const renderMatch = content.match(/render\(\)\s*{[\s\S]*?return\s*\(/);
+ if (renderMatch) {
+ const insertPoint = content.indexOf(renderMatch[0]) + renderMatch[0].length;
+ let headInsert;
+ if (remote) {
+ // 使用动态脚本加载器
+ const scriptLoader = generateDynamicScriptLoader(remote);
+ headInsert = `
+
+ {/* DEV-INJECT-START */}
+ {typeof window !== 'undefined' && (
+
+ )}
+ {/* DEV-INJECT-END */}
+ `;
+ } else {
+ // 向后兼容:使用旧的 scriptTag 方式
+ headInsert = `
+
+ {/* DEV-INJECT-START */}
+ {typeof window !== 'undefined' && ${scriptTag.replace(/`;
+ } else if (type === 'absolute-path') {
+ return ``;
+ } else {
+ throw new Error(`未知的远程类型: ${type}`);
+ }
+}
+
+/**
+ * 从 HTML 内容中移除注入的脚本 - 同时清理前后的空白行
+ */
+export function removeInjectedScripts(htmlContent, identifier = 'dev-inject') {
+ // 使用标准的开始/结束标识移除注入的代码
+ // 匹配包括前后换行符和空白在内的完整块
+ // 使用非贪婪匹配确保只匹配一个注入块
+ const injectBlockRegex = /(\r?\n)?\s*[\s\S]*?\s*(\r?\n)?/g;
+
+ let cleaned = htmlContent.replace(injectBlockRegex, (match, beforeNewline, afterNewline) => {
+ // 如果匹配块前后都有换行符,只保留一个换行符
+ if (beforeNewline && afterNewline) {
+ return '\n';
+ }
+ // 如果只有前面的换行符,保留它
+ if (beforeNewline) {
+ return beforeNewline;
+ }
+ // 如果只有后面的换行符,保留它
+ if (afterNewline) {
+ return afterNewline;
+ }
+ // 如果都没有,返回空字符串
+ return '';
+ });
+
+ // 清理移除后可能产生的多余空白行
+ // 清理 标签后的多余空白行(保留一个换行)
+ cleaned = cleaned.replace(/(]*>)\s*\n\s*\n+(\s*)/g, '$1\n$2');
+ // 清理 前的多余空白行(保留一个换行)
+ cleaned = cleaned.replace(/(\s*)\n\s*\n+(\s*<\/head>)/g, '$1\n$2');
+ // 清理连续的多个空白行(保留最多一个空行)
+ cleaned = cleaned.replace(/\n\s*\n\s*\n+/g, '\n\n');
+ // 清理空行(只包含空白字符的行)
+ cleaned = cleaned.replace(/^\s+$/gm, '');
+
+ return cleaned;
+}
+
+/**
+ * 向 HTML 中注入脚本 - 使用动态时间戳防止缓存
+ */
+export function injectScriptToHtml(
+ htmlContent,
+ scriptTag,
+ identifier = 'dev-inject',
+ remote = null
+) {
+ // 先清除之前的注入
+ htmlContent = removeInjectedScripts(htmlContent, identifier);
+
+ // 生成脚本内容 - 优先使用动态时间戳方式
+ // 注意:脚本内容前后不包含额外的换行符,由插入逻辑统一处理
+ let scriptContent;
+ if (remote) {
+ // 使用动态脚本加载器(推荐方式,防止缓存)
+ const safeRemote = JSON.stringify(remote);
+ const scriptId = `${identifier}-monitor`;
+ scriptContent = `
+
+`;
+ } else {
+ // 向后兼容:从 scriptTag 中提取 URL 并添加时间戳
+ const scriptSrc = scriptTag.match(/src="([^"]*)"/)?.[1] || '';
+ if (scriptSrc) {
+ // 提取基础 URL(移除可能已存在的时间戳参数)
+ const baseUrl = scriptSrc.split('?')[0];
+ const separator = scriptSrc.includes('?') ? '&' : '?';
+ scriptContent = `
+
+`;
+ } else {
+ // 如果无法提取 URL,使用旧的静态方式
+ scriptContent = `
+ ${scriptTag}
+`;
+ }
+ }
+
+ // 查找 标签
+ const headEndIndex = htmlContent.lastIndexOf('');
+
+ if (headEndIndex !== -1) {
+ // 在 之前注入
+ const beforeHead = htmlContent.substring(0, headEndIndex);
+ const afterHead = htmlContent.substring(headEndIndex);
+
+ // 检查 前是否有非空白内容
+ const beforeHeadTrimmed = beforeHead.trimEnd();
+ const needsNewlineBefore = beforeHeadTrimmed.length > 0 && !beforeHeadTrimmed.endsWith('\n');
+
+ // 构建注入内容,统一格式
+ let result;
+ if (needsNewlineBefore) {
+ // 如果前面有内容,添加换行符后插入脚本
+ result = `${beforeHeadTrimmed}\n${scriptContent}\n${afterHead}`;
+ } else {
+ // 如果前面是空白或换行,直接插入(脚本内容已有标识注释)
+ // 确保脚本前后有适当的换行
+ const trimmedBefore = beforeHead.trimEnd();
+ if (trimmedBefore.length === 0) {
+ // 如果前面完全是空白,直接插入
+ result = `${beforeHead}${scriptContent}\n${afterHead}`;
+ } else {
+ // 前面有内容但末尾是换行,直接插入
+ result = `${beforeHead}${scriptContent}\n${afterHead}`;
+ }
+ }
+
+ // 清理可能的多余空白行(保留最多一个空行)
+ // 清理 标签后的多余空白行
+ result = result.replace(/(]*>)\s*\n\s*\n+(\s*)/g, '$1\n$2');
+ // 清理 前的多余空白行
+ result = result.replace(/(\s*)\n\s*\n+(\s*<\/head>)/g, '$1\n$2');
+ // 清理连续的多个空白行
+ result = result.replace(/\n\s*\n\s*\n+/g, '\n\n');
+
+ return result;
+ } else {
+ // 如果没有找到 ,查找 标签
+ const bodyStartIndex = htmlContent.indexOf('', bodyStartIndex) + 1;
+ const beforeBody = htmlContent.substring(0, bodyEndIndex);
+ const afterBody = htmlContent.substring(bodyEndIndex);
+ return `${beforeBody}
+${scriptContent}
+${afterBody}`;
+ } else {
+ // 如果都没有找到,在文件末尾添加
+ return `${htmlContent}
+${scriptContent}`;
+ }
+ }
+}
+
+/**
+ * 日志输出函数
+ */
+export function log(message, verbose = false) {
+ if (verbose) {
+ console.log(`[dev-inject] ${message}`);
+ }
+}
+
+export function logError(message) {
+ console.error(`❌ [dev-inject] ${message}`);
+}
+
+export function logSuccess(message) {
+ console.log(`✅ [dev-inject] ${message}`);
+}
+
+export function logInfo(message) {
+ console.log(`ℹ️ [dev-inject] ${message}`);
+}
diff --git a/qiming-dev-inject/package.json b/qiming-dev-inject/package.json
new file mode 100644
index 00000000..02cf4dd2
--- /dev/null
+++ b/qiming-dev-inject/package.json
@@ -0,0 +1,50 @@
+{
+ "name": "@xagi/dev-inject",
+ "version": "1.0.4",
+ "description": "框架感知的开发环境脚本注入工具 - 支持 Vite、Next.js、React 等现代框架的智能脚本注入",
+ "type": "module",
+ "main": "lib/index.js",
+ "bin": {
+ "dev-inject": "bin/index.js"
+ },
+ "files": [
+ "bin/",
+ "lib/",
+ "scripts/",
+ "*.md",
+ "LICENSE"
+ ],
+ "scripts": {
+ "build": "echo 'No build needed for Node.js CLI'",
+ "prepare": "chmod +x bin/index.js",
+ "test": "node scripts/test-projects.js all",
+ "test:all": "node scripts/test-projects.js all",
+ "test:install": "node scripts/test-projects.js install",
+ "test:uninstall": "node scripts/test-projects.js uninstall",
+ "test:html": "cd test-projects/html-project && node ../../bin/index.js install --remote=/sdk/dev-monitor.js --framework && node ../../bin/index.js uninstall --framework",
+ "test:vite": "cd test-projects/vite-project && node ../../bin/index.js install --remote=/sdk/dev-monitor.js --framework && node ../../bin/index.js uninstall --framework",
+ "test:next-pages": "cd test-projects/next-pages-project && node ../../bin/index.js install --remote=/sdk/dev-monitor.js --framework && node ../../bin/index.js uninstall --framework",
+ "test:next-app": "cd test-projects/next-app-project && node ../../bin/index.js install --remote=/sdk/dev-monitor.js --framework && node ../../bin/index.js uninstall --framework"
+ },
+ "keywords": [
+ "development",
+ "script-injection",
+ "html",
+ "cli",
+ "remote-script",
+ "local-script"
+ ],
+ "author": "dongdada29",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/dongdada29/dev-inject.git"
+ },
+ "bugs": {
+ "url": "https://github.com/dongdada29/dev-inject/issues"
+ },
+ "homepage": "https://github.com/dongdada29/dev-inject#readme"
+}
diff --git a/qiming-dev-inject/scripts/dev-monitor.js b/qiming-dev-inject/scripts/dev-monitor.js
new file mode 100644
index 00000000..c59388a5
--- /dev/null
+++ b/qiming-dev-inject/scripts/dev-monitor.js
@@ -0,0 +1,223 @@
+/**
+ * 开发环境监控脚本
+ * 提供错误监控、性能监控、通信功能
+ */
+
+(function () {
+ 'use strict';
+
+ // 配置
+ const config = {
+ version: '1.0.0',
+ enabled: true,
+ logLevel: 'error', // 只记录错误级别日志
+ maxErrors: 10, // 减少存储量
+ maxLogs: 20, // 减少存储量
+ };
+
+ // 简化的监控数据存储
+ const monitorData = {
+ errors: [],
+ basicInfo: {
+ url: window.location.href,
+ userAgent: navigator.userAgent.split(' ')[0], // 只保留浏览器名称
+ },
+ ready: false,
+ };
+
+ // 简化的日志函数 - 只记录错误
+ const logger = {
+ error: (message, details = null) => {
+ console.error('[Dev-Monitor ERROR]', message, details || '');
+
+ const errorData = {
+ message: typeof message === 'string' ? message : message.toString(),
+ details: details ? JSON.stringify(details).substring(0, 200) : null, // 限制详细信息长度
+ timestamp: Date.now(),
+ };
+
+ monitorData.errors.push(errorData);
+
+ // 限制错误数量
+ if (monitorData.errors.length > config.maxErrors) {
+ monitorData.errors.shift();
+ }
+ },
+ };
+
+ // 简化的错误监控
+ function setupErrorMonitoring() {
+ // 全局错误捕获
+ window.addEventListener('error', function (event) {
+ const errorMsg = `${event.message} at ${event.filename}:${event.lineno}:${event.colno}`;
+ logger.error(errorMsg);
+ });
+
+ // Promise 错误捕获
+ window.addEventListener('unhandledrejection', function (event) {
+ const errorMsg = `Promise rejection: ${event.reason}`;
+ logger.error(errorMsg);
+ });
+
+ // 资源加载错误 - 只记录关键信息
+ window.addEventListener(
+ 'error',
+ function (event) {
+ if (event.target !== window) {
+ const source = event.target.src || event.target.href || 'unknown';
+ logger.error(`Resource failed: ${source}`);
+ }
+ },
+ true
+ );
+ }
+
+ // 移除复杂的性能监控和控制台拦截,专注于核心错误监控
+
+ // 简化的通信功能
+ function setupCommunication() {
+ // 向父窗口发送监控数据 - 只在有新错误时发送
+ function sendToParent() {
+ if (
+ window.parent &&
+ window.parent !== window &&
+ monitorData.errors.length > 0
+ ) {
+ try {
+ const summary = {
+ type: 'dev-monitor-summary',
+ errorCount: monitorData.errors.length,
+ latestError: monitorData.errors[monitorData.errors.length - 1],
+ url: monitorData.basicInfo.url,
+ timestamp: Date.now(),
+ };
+
+ window.parent.postMessage(summary, '*');
+ } catch (e) {
+ // 静默处理错误
+ }
+ }
+ }
+
+ // 监听来自父窗口的请求
+ window.addEventListener('message', function (event) {
+ if (event.data && event.data.type === 'dev-monitor-request') {
+ const summary = {
+ type: 'dev-monitor-data',
+ errorCount: monitorData.errors.length,
+ errors: monitorData.errors.slice(-3), // 只发送最近3个错误
+ url: monitorData.basicInfo.url,
+ };
+
+ try {
+ window.parent.postMessage(summary, '*');
+ } catch (e) {
+ // 静默处理错误
+ }
+ }
+ });
+
+ // 降低发送频率 - 每10秒检查一次
+ setInterval(sendToParent, 10000);
+ }
+
+ // 简化的全局 API
+ window.DevMonitor = {
+ // 获取错误统计
+ getStats: function () {
+ return {
+ errorCount: monitorData.errors.length,
+ url: monitorData.basicInfo.url,
+ userAgent: monitorData.basicInfo.userAgent,
+ latestError: monitorData.errors[monitorData.errors.length - 1] || null,
+ };
+ },
+
+ // 获取所有错误(限制数量)
+ getErrors: function () {
+ return monitorData.errors.slice(-5); // 只返回最近5个错误
+ },
+
+ // 清除错误
+ clearErrors: function () {
+ monitorData.errors = [];
+ console.log('✅ DevMonitor errors cleared');
+ },
+
+ // 显示简化监控面板
+ showPanel: function () {
+ // 移除现有面板
+ const existingPanel = document.getElementById('dev-monitor-panel');
+ if (existingPanel) {
+ existingPanel.remove();
+ }
+
+ const panel = document.createElement('div');
+ panel.id = 'dev-monitor-panel';
+ panel.style.cssText = `
+ position: fixed;
+ top: 10px;
+ right: 10px;
+ width: 280px;
+ background: #1a1a1a;
+ color: white;
+ border: 1px solid #333;
+ border-radius: 6px;
+ padding: 12px;
+ font-family: monospace;
+ font-size: 12px;
+ z-index: 999999;
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
+ `;
+
+ const errors = monitorData.errors.slice(-3); // 只显示最近3个错误
+
+ panel.innerHTML = `
+
+ DevMonitor
+
+
+
+ Errors (${monitorData.errors.length}):
+
+ ${
+ errors.length
+ ? errors
+ .map(
+ e => `
+
+ ${e.message}
+ ${e.details ? `
${e.details}` : ''}
+
+ `
+ )
+ .join('')
+ : 'No errors
'
+ }
+
+
+
+
+ `;
+
+ document.body.appendChild(panel);
+ },
+ };
+
+ // 简化的初始化
+ function init() {
+ setupErrorMonitoring();
+ setupCommunication();
+ monitorData.ready = true;
+
+ // 简化的控制台提示
+ console.log(
+ '%cDevMonitor v' + config.version,
+ 'color: #4CAF50; font-weight: bold;',
+ '- DevMonitor.showPanel()'
+ );
+ }
+
+ // 立即初始化
+ init();
+})();
diff --git a/qiming-dev-inject/scripts/test-projects.js b/qiming-dev-inject/scripts/test-projects.js
new file mode 100644
index 00000000..24944642
--- /dev/null
+++ b/qiming-dev-inject/scripts/test-projects.js
@@ -0,0 +1,534 @@
+#!/usr/bin/env node
+
+/**
+ * 测试脚本 - 运行所有 test-projects 的注入和卸载测试
+ */
+
+import { execSync } from 'child_process';
+import { fileURLToPath } from 'url';
+import { dirname, join } from 'path';
+import { existsSync, readFileSync } from 'fs';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+const rootDir = join(__dirname, '..');
+const testProjectsDir = join(rootDir, 'test-projects');
+
+// 测试项目列表
+const testProjects = [
+ { name: 'html-project', type: 'html-static' },
+ { name: 'vite-project', type: 'vite' },
+ { name: 'next-pages-project', type: 'next-pages' },
+ { name: 'next-app-project', type: 'next-app' },
+];
+
+// 测试配置
+const testConfig = {
+ remote: '/sdk/dev-monitor.js',
+ remoteUrl: 'https://testagent.xspaceagi.com/sdk/dev-monitor.js',
+};
+
+// 颜色输出
+const colors = {
+ reset: '\x1b[0m',
+ bright: '\x1b[1m',
+ green: '\x1b[32m',
+ red: '\x1b[31m',
+ yellow: '\x1b[33m',
+ blue: '\x1b[34m',
+ cyan: '\x1b[36m',
+};
+
+function log(message, color = 'reset') {
+ console.log(`${colors[color]}${message}${colors.reset}`);
+}
+
+function logSection(title) {
+ console.log('\n' + '='.repeat(60));
+ log(title, 'bright');
+ console.log('='.repeat(60));
+}
+
+function logTest(name) {
+ log(`\n📦 测试项目: ${name}`, 'cyan');
+}
+
+function runCommand(command, cwd) {
+ try {
+ const output = execSync(command, {
+ cwd,
+ encoding: 'utf8',
+ stdio: 'pipe',
+ });
+ return { success: true, output };
+ } catch (error) {
+ return {
+ success: false,
+ output: error.stdout || error.stderr || error.message,
+ error: error.message,
+ };
+ }
+}
+
+/**
+ * 测试单个项目的安装(注入)
+ */
+function testInstall(project, remote, testName) {
+ const projectPath = join(testProjectsDir, project.name);
+
+ if (!existsSync(projectPath)) {
+ log(` ❌ 项目目录不存在: ${projectPath}`, 'red');
+ return false;
+ }
+
+ logTest(`${project.name} (${testName})`);
+
+ // 1. 先清理之前的注入(可选)
+ log(' 🔄 步骤 1: 清理之前的注入...', 'yellow');
+ const uninstallResult = runCommand(
+ `node ${join(rootDir, 'bin/index.js')} uninstall --framework`,
+ projectPath
+ );
+
+ if (!uninstallResult.success && !uninstallResult.output.includes('未找到需要清理的注入脚本')) {
+ log(` ⚠️ 清理警告: ${uninstallResult.output}`, 'yellow');
+ }
+
+ // 2. 执行注入
+ log(' ➕ 步骤 2: 注入脚本...', 'yellow');
+ const installResult = runCommand(
+ `node ${join(rootDir, 'bin/index.js')} install --remote=${remote} --framework`,
+ projectPath
+ );
+
+ if (!installResult.success) {
+ log(` ❌ 注入失败: ${installResult.error}`, 'red');
+ console.log(installResult.output);
+ return false;
+ }
+
+ log(' ✅ 注入成功', 'green');
+
+ // 3. 验证注入结果
+ log(' 🔍 步骤 3: 验证注入结果...', 'yellow');
+ const verifyResult = verifyInjection(project, projectPath);
+
+ if (!verifyResult.success) {
+ log(` ❌ 验证失败: ${verifyResult.message}`, 'red');
+ return false;
+ }
+
+ log(' ✅ 验证通过', 'green');
+
+ return true;
+}
+
+/**
+ * 测试单个项目的卸载
+ */
+function testUninstall(project) {
+ const projectPath = join(testProjectsDir, project.name);
+
+ if (!existsSync(projectPath)) {
+ log(` ❌ 项目目录不存在: ${projectPath}`, 'red');
+ return false;
+ }
+
+ logTest(`${project.name}`);
+
+ // 1. 先确保有注入内容(如果没有,先注入)
+ log(' 🔍 步骤 1: 检查是否有注入内容...', 'yellow');
+ const hasInjection = checkInjection(project, projectPath);
+
+ if (!hasInjection) {
+ log(' ⚠️ 未找到注入内容,先执行注入...', 'yellow');
+ const installResult = runCommand(
+ `node ${join(rootDir, 'bin/index.js')} install --remote=/sdk/dev-monitor.js --framework`,
+ projectPath
+ );
+
+ if (!installResult.success) {
+ log(` ❌ 预注入失败: ${installResult.error}`, 'red');
+ return false;
+ }
+ log(' ✅ 预注入成功', 'green');
+ }
+
+ // 2. 执行卸载
+ log(' 🗑️ 步骤 2: 卸载注入...', 'yellow');
+ const uninstallResult = runCommand(
+ `node ${join(rootDir, 'bin/index.js')} uninstall --framework`,
+ projectPath
+ );
+
+ if (!uninstallResult.success) {
+ log(` ❌ 卸载失败: ${uninstallResult.error}`, 'red');
+ console.log(uninstallResult.output);
+ return false;
+ }
+
+ log(' ✅ 卸载成功', 'green');
+
+ // 3. 验证卸载结果
+ log(' 🔍 步骤 3: 验证卸载结果...', 'yellow');
+ const verifyResult = verifyUninstall(project, projectPath);
+
+ if (!verifyResult.success) {
+ log(` ❌ 验证失败: ${verifyResult.message}`, 'red');
+ return false;
+ }
+
+ log(' ✅ 验证通过', 'green');
+
+ return true;
+}
+
+/**
+ * 检查是否有注入内容
+ */
+function checkInjection(project, projectPath) {
+ try {
+ switch (project.type) {
+ case 'html-static': {
+ const htmlFile = join(projectPath, 'index.html');
+ if (existsSync(htmlFile)) {
+ const content = readFileSync(htmlFile, 'utf8');
+ return content.includes('DEV-INJECT-START');
+ }
+ return false;
+ }
+ case 'vite': {
+ const configFile = join(projectPath, 'vite.config.js');
+ if (existsSync(configFile)) {
+ const content = readFileSync(configFile, 'utf8');
+ return content.includes('DEV-INJECT-START');
+ }
+ return false;
+ }
+ case 'next-pages': {
+ const docFile = join(projectPath, 'pages/_document.tsx');
+ if (existsSync(docFile)) {
+ const content = readFileSync(docFile, 'utf8');
+ return content.includes('DEV-INJECT-START');
+ }
+ return false;
+ }
+ case 'next-app': {
+ const layoutFile = join(projectPath, 'app/layout.tsx');
+ if (existsSync(layoutFile)) {
+ const content = readFileSync(layoutFile, 'utf8');
+ return content.includes('DEV-INJECT-START');
+ }
+ return false;
+ }
+ }
+ return false;
+ } catch (error) {
+ return false;
+ }
+}
+
+/**
+ * 验证注入结果
+ */
+function verifyInjection(project, projectPath) {
+ try {
+ switch (project.type) {
+ case 'html-static': {
+ const htmlFile = join(projectPath, 'index.html');
+ const content = readFileSync(htmlFile, 'utf8');
+ if (!content.includes('DEV-INJECT-START')) {
+ return { success: false, message: 'HTML 文件中未找到注入标记' };
+ }
+ break;
+ }
+ case 'vite': {
+ const configFile = join(projectPath, 'vite.config.js');
+ const content = readFileSync(configFile, 'utf8');
+ if (!content.includes('DEV-INJECT-START')) {
+ return { success: false, message: 'Vite 配置文件中未找到注入标记' };
+ }
+ // 验证逗号位置(允许多种格式)
+ if (!content.includes('// ')) {
+ return { success: false, message: '未找到结束标记' };
+ }
+ // 验证 index.html 未被修改
+ const htmlFile = join(projectPath, 'index.html');
+ const htmlContent = readFileSync(htmlFile, 'utf8');
+ if (htmlContent.includes('DEV-INJECT-START')) {
+ return { success: false, message: 'index.html 不应包含注入标记' };
+ }
+ break;
+ }
+ case 'next-pages': {
+ const docFile = join(projectPath, 'pages/_document.tsx');
+ if (existsSync(docFile)) {
+ const content = readFileSync(docFile, 'utf8');
+ if (!content.includes('DEV-INJECT-START')) {
+ return { success: false, message: '_document.tsx 中未找到注入标记' };
+ }
+ }
+ break;
+ }
+ case 'next-app': {
+ const layoutFile = join(projectPath, 'app/layout.tsx');
+ if (existsSync(layoutFile)) {
+ const content = readFileSync(layoutFile, 'utf8');
+ if (!content.includes('DEV-INJECT-START')) {
+ return { success: false, message: 'layout.tsx 中未找到注入标记' };
+ }
+ }
+ break;
+ }
+ }
+
+ return { success: true };
+ } catch (error) {
+ return { success: false, message: error.message };
+ }
+}
+
+/**
+ * 验证卸载结果
+ */
+function verifyUninstall(project, projectPath) {
+ try {
+ switch (project.type) {
+ case 'html-static': {
+ const htmlFile = join(projectPath, 'index.html');
+ const content = readFileSync(htmlFile, 'utf8');
+ if (content.includes('DEV-INJECT-START')) {
+ return { success: false, message: 'HTML 文件中仍存在注入标记' };
+ }
+ break;
+ }
+ case 'vite': {
+ const configFile = join(projectPath, 'vite.config.js');
+ const content = readFileSync(configFile, 'utf8');
+ if (content.includes('DEV-INJECT-START')) {
+ return { success: false, message: 'Vite 配置文件中仍存在注入标记' };
+ }
+ break;
+ }
+ case 'next-pages': {
+ const docFile = join(projectPath, 'pages/_document.tsx');
+ if (existsSync(docFile)) {
+ const content = readFileSync(docFile, 'utf8');
+ if (content.includes('DEV-INJECT-START')) {
+ return { success: false, message: '_document.tsx 中仍存在注入标记' };
+ }
+ }
+ break;
+ }
+ case 'next-app': {
+ const layoutFile = join(projectPath, 'app/layout.tsx');
+ if (existsSync(layoutFile)) {
+ const content = readFileSync(layoutFile, 'utf8');
+ if (content.includes('DEV-INJECT-START')) {
+ return { success: false, message: 'layout.tsx 中仍存在注入标记' };
+ }
+ }
+ break;
+ }
+ }
+
+ return { success: true };
+ } catch (error) {
+ return { success: false, message: error.message };
+ }
+}
+
+/**
+ * 运行安装测试
+ */
+function runInstallTests() {
+ logSection('🚀 开始运行安装(注入)测试');
+ log(`测试目录: ${testProjectsDir}`, 'blue');
+ log(`测试项目数量: ${testProjects.length}`, 'blue');
+
+ const results = {
+ total: 0,
+ passed: 0,
+ failed: 0,
+ projects: [],
+ };
+
+ // 测试本地路径注入
+ logSection('📝 测试 1: 本地路径注入 (/sdk/dev-monitor.js)');
+
+ for (const project of testProjects) {
+ results.total++;
+ const success = testInstall(project, testConfig.remote, '本地路径');
+ results.projects.push({ project: project.name, test: '本地路径', success });
+
+ if (success) {
+ results.passed++;
+ } else {
+ results.failed++;
+ }
+ }
+
+ // 测试远程 URL 注入
+ logSection('🌐 测试 2: 远程 URL 注入 (https://testagent.xspaceagi.com/sdk/dev-monitor.js)');
+
+ for (const project of testProjects) {
+ results.total++;
+ const success = testInstall(project, testConfig.remoteUrl, '远程URL');
+ results.projects.push({ project: project.name, test: '远程URL', success });
+
+ if (success) {
+ results.passed++;
+ } else {
+ results.failed++;
+ }
+ }
+
+ // 输出测试结果
+ logSection('📊 安装测试结果汇总');
+ log(`总测试数: ${results.total}`, 'blue');
+ log(`通过: ${results.passed}`, 'green');
+ log(`失败: ${results.failed}`, results.failed > 0 ? 'red' : 'green');
+
+ console.log('\n详细结果:');
+ results.projects.forEach(({ project, test, success }) => {
+ const icon = success ? '✅' : '❌';
+ const color = success ? 'green' : 'red';
+ log(` ${icon} ${project} - ${test}`, color);
+ });
+
+ // 返回退出码
+ process.exit(results.failed > 0 ? 1 : 0);
+}
+
+/**
+ * 运行卸载测试
+ */
+function runUninstallTests() {
+ logSection('🚀 开始运行卸载测试');
+ log(`测试目录: ${testProjectsDir}`, 'blue');
+ log(`测试项目数量: ${testProjects.length}`, 'blue');
+
+ const results = {
+ total: 0,
+ passed: 0,
+ failed: 0,
+ projects: [],
+ };
+
+ for (const project of testProjects) {
+ results.total++;
+ const success = testUninstall(project);
+ results.projects.push({ project: project.name, success });
+
+ if (success) {
+ results.passed++;
+ } else {
+ results.failed++;
+ }
+ }
+
+ // 输出测试结果
+ logSection('📊 卸载测试结果汇总');
+ log(`总测试数: ${results.total}`, 'blue');
+ log(`通过: ${results.passed}`, 'green');
+ log(`失败: ${results.failed}`, results.failed > 0 ? 'red' : 'green');
+
+ console.log('\n详细结果:');
+ results.projects.forEach(({ project, success }) => {
+ const icon = success ? '✅' : '❌';
+ const color = success ? 'green' : 'red';
+ log(` ${icon} ${project}`, color);
+ });
+
+ // 返回退出码
+ process.exit(results.failed > 0 ? 1 : 0);
+}
+
+/**
+ * 运行所有测试(安装 + 卸载)
+ */
+function runAllTests() {
+ logSection('🚀 开始运行完整测试(安装 + 卸载)');
+ log(`测试目录: ${testProjectsDir}`, 'blue');
+ log(`测试项目数量: ${testProjects.length}`, 'blue');
+
+ const results = {
+ total: 0,
+ passed: 0,
+ failed: 0,
+ projects: [],
+ };
+
+ // 先运行安装测试
+ logSection('📝 第一部分: 安装测试');
+
+ for (const project of testProjects) {
+ results.total++;
+ const success = testInstall(project, testConfig.remote, '本地路径');
+ results.projects.push({ project: project.name, test: '安装', success });
+
+ if (success) {
+ results.passed++;
+ } else {
+ results.failed++;
+ }
+ }
+
+ // 再运行卸载测试
+ logSection('🗑️ 第二部分: 卸载测试');
+
+ for (const project of testProjects) {
+ results.total++;
+ const success = testUninstall(project);
+ results.projects.push({ project: project.name, test: '卸载', success });
+
+ if (success) {
+ results.passed++;
+ } else {
+ results.failed++;
+ }
+ }
+
+ // 输出测试结果
+ logSection('📊 完整测试结果汇总');
+ log(`总测试数: ${results.total}`, 'blue');
+ log(`通过: ${results.passed}`, 'green');
+ log(`失败: ${results.failed}`, results.failed > 0 ? 'red' : 'green');
+
+ console.log('\n详细结果:');
+ results.projects.forEach(({ project, test, success }) => {
+ const icon = success ? '✅' : '❌';
+ const color = success ? 'green' : 'red';
+ log(` ${icon} ${project} - ${test}`, color);
+ });
+
+ // 返回退出码
+ process.exit(results.failed > 0 ? 1 : 0);
+}
+
+// 解析命令行参数
+const args = process.argv.slice(2);
+const command = args[0] || 'all';
+
+// 运行测试
+try {
+ switch (command) {
+ case 'install':
+ case 'i':
+ runInstallTests();
+ break;
+ case 'uninstall':
+ case 'u':
+ runUninstallTests();
+ break;
+ case 'all':
+ default:
+ runAllTests();
+ break;
+ }
+} catch (error) {
+ log(`\n❌ 测试执行失败: ${error.message}`, 'red');
+ console.error(error);
+ process.exit(1);
+}
+
diff --git a/qiming-dev-inject/test-projects/README.md b/qiming-dev-inject/test-projects/README.md
new file mode 100644
index 00000000..71690d02
--- /dev/null
+++ b/qiming-dev-inject/test-projects/README.md
@@ -0,0 +1,124 @@
+# 测试项目集合
+
+这个目录包含了用于测试 dev-inject 在不同框架和项目类型中的注入功能的示例项目。
+
+## 项目列表
+
+### 1. HTML 静态项目 (html-project)
+- **类型**: 纯 HTML + JavaScript
+- **端口**: 3002
+- **启动**: `npm run serve` 或 `python3 -m http.server 3002`
+- **特点**: 基础的 HTML 项目,用于测试最简单场景下的脚本注入
+
+### 2. Vite 项目 (vite-project)
+- **类型**: Vite + React
+- **端口**: 3001
+- **启动**: `npm run dev`
+- **特点**: 现代化的 Vite 构建工具,单页应用
+
+### 3. Next.js App Router (next-app-project)
+- **类型**: Next.js 14 App Router
+- **端口**: 3000
+- **启动**: `npm run dev`
+- **特点**: 最新的 Next.js 应用路由架构,支持 React Server Components
+
+### 4. Next.js Pages Router (next-pages-project)
+- **类型**: Next.js Pages Router
+- **端口**: 3000
+- **启动**: `npm run dev`
+- **特点**: 传统的 Next.js Pages 路由架构
+
+## 使用指南
+
+### 快速开始
+
+1. **选择一个测试项目**
+ ```bash
+ cd test-projects/html-project # 或选择其他项目
+ ```
+
+2. **安装依赖**(如果有 package.json)
+ ```bash
+ npm install
+ ```
+
+3. **启动项目**
+ ```bash
+ npm run dev
+ # 或
+ npm run serve
+ # 或
+ python3 -m http.server 3002
+ ```
+
+4. **使用 dev-inject 注入监控脚本**
+ ```bash
+ # 在项目根目录运行
+ node ../bin/index.js install --remote http://localhost:9001/scripts/dev-monitor.js
+ ```
+
+5. **测试功能**
+ - 打开浏览器访问项目
+ - 点击页面上的按钮触发各种错误
+ - 在浏览器控制台运行 DevMonitor 命令查看监控信息
+
+### 测试命令
+
+所有测试项目都支持以下 DevMonitor 命令:
+
+```javascript
+// 显示监控面板
+DevMonitor.showPanel()
+
+// 获取统计信息
+DevMonitor.getStats()
+
+// 清除错误记录
+DevMonitor.clearErrors()
+
+// 检查注入状态
+window.__DEV_INJECT__
+```
+
+## 功能特性
+
+每个测试项目都包含以下功能:
+
+- ✅ 触发同步错误
+- ✅ 触发异步错误
+- ✅ 错误记录和显示
+- ✅ 清除错误功能
+- ✅ DevMonitor 控制台命令
+- ✅ 实时错误统计
+- ✅ 美观的 UI 界面
+
+## 项目对比
+
+| 项目类型 | 构建工具 | 路由方式 | 启动速度 | 适用场景 |
+|---------|---------|---------|---------|---------|
+| HTML | 无 | 静态文件 | 最快 | 传统网站 |
+| Vite | Vite | SPA | 快 | 现代 SPA 应用 |
+| Next.js App | Next.js | App Router | 中 | 全栈应用(推荐) |
+| Next.js Pages | Next.js | Pages Router | 中 | 传统 Next.js 应用 |
+
+## 注意事项
+
+1. **端口冲突**: 确保各个项目使用不同的端口
+2. **依赖安装**: 每个项目需要单独安装依赖
+3. **测试服务器**: 确保 dev-inject 的监控服务器 (localhost:9001) 正在运行
+4. **框架检测**: dev-inject 会自动检测项目类型并应用相应的注入策略
+
+## 问题排查
+
+如果遇到问题:
+
+1. 检查端口是否被占用
+2. 确认所有依赖已正确安装
+3. 查看浏览器控制台是否有错误
+4. 确认监控服务器是否正在运行
+5. 检查注入的脚本是否正确加载
+
+## 贡献
+
+欢迎添加更多测试项目或改进现有项目!
+
diff --git a/qiming-dev-inject/test-projects/html-project/.gitignore b/qiming-dev-inject/test-projects/html-project/.gitignore
new file mode 100644
index 00000000..d3074e17
--- /dev/null
+++ b/qiming-dev-inject/test-projects/html-project/.gitignore
@@ -0,0 +1,16 @@
+# OS
+.DS_Store
+Thumbs.db
+
+# Editor
+.vscode/
+.idea/
+*.swp
+*.swo
+
+# Node
+node_modules/
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
diff --git a/qiming-dev-inject/test-projects/html-project/README.md b/qiming-dev-inject/test-projects/html-project/README.md
new file mode 100644
index 00000000..5b6d99e8
--- /dev/null
+++ b/qiming-dev-inject/test-projects/html-project/README.md
@@ -0,0 +1,41 @@
+# HTML 静态项目测试
+
+这是一个用于测试 dev-inject 在纯 HTML 静态项目中的注入功能的示例项目。
+
+## 启动项目
+
+### 方式 1: 使用 Python
+```bash
+python3 -m http.server 3002
+```
+
+### 方式 2: 使用 serve
+```bash
+npx serve -s . -p 3002
+```
+
+项目将在 http://localhost:3002 运行。
+
+## 测试步骤
+
+1. 启动项目(任选上述方式之一)
+2. 使用 dev-inject 注入监控脚本
+3. 点击页面上的按钮触发各种错误类型
+4. 在浏览器控制台运行 DevMonitor 命令查看监控信息
+
+## DevMonitor 命令
+
+- `DevMonitor.showPanel()` - 显示监控面板
+- `DevMonitor.getStats()` - 获取统计信息
+- `DevMonitor.clearErrors()` - 清除错误记录
+- `window.__DEV_INJECT__` - 检查注入状态
+
+## 功能特性
+
+- 触发同步错误
+- 触发异步错误
+- 触发引用错误
+- 触发类型错误
+- 实时统计错误数据
+- 清除错误记录
+
diff --git a/qiming-dev-inject/test-projects/html-project/index.html b/qiming-dev-inject/test-projects/html-project/index.html
new file mode 100644
index 00000000..8378fcba
--- /dev/null
+++ b/qiming-dev-inject/test-projects/html-project/index.html
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+ HTML 静态项目 - dev-inject 测试
+
+
+
+
+
+
+ HTML 静态项目测试
+ 测试 dev-inject 在纯 HTML 项目中的注入功能
+
+
+
diff --git a/qiming-dev-inject/test-projects/html-project/package.json b/qiming-dev-inject/test-projects/html-project/package.json
new file mode 100644
index 00000000..bc5efacb
--- /dev/null
+++ b/qiming-dev-inject/test-projects/html-project/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "html-static-test",
+ "version": "1.0.0",
+ "description": "纯 HTML 静态项目测试 - dev-inject",
+ "type": "module",
+ "scripts": {
+ "start": "python3 -m http.server 3002",
+ "serve": "npx serve -s . -p 3002"
+ },
+ "keywords": [
+ "html",
+ "test",
+ "dev-inject"
+ ],
+ "author": "",
+ "license": "MIT"
+}
\ No newline at end of file
diff --git a/qiming-dev-inject/test-projects/next-app-project/.gitignore b/qiming-dev-inject/test-projects/next-app-project/.gitignore
new file mode 100644
index 00000000..1403e903
--- /dev/null
+++ b/qiming-dev-inject/test-projects/next-app-project/.gitignore
@@ -0,0 +1,36 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# local env files
+.env*.local
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
+
diff --git a/qiming-dev-inject/test-projects/next-app-project/README.md b/qiming-dev-inject/test-projects/next-app-project/README.md
new file mode 100644
index 00000000..20e904bf
--- /dev/null
+++ b/qiming-dev-inject/test-projects/next-app-project/README.md
@@ -0,0 +1,32 @@
+# Next.js App Router 测试项目
+
+这是一个用于测试 dev-inject 框架感知注入功能的 Next.js App Router 项目。
+
+## 安装依赖
+
+```bash
+npm install
+```
+
+## 启动开发服务器
+
+```bash
+npm run dev
+```
+
+项目将在 http://localhost:3000 运行。
+
+## 测试步骤
+
+1. 启动项目:`npm run dev`
+2. 使用 dev-inject 注入监控脚本
+3. 点击页面上的按钮触发各种错误
+4. 在浏览器控制台运行 DevMonitor 命令查看监控信息
+
+## DevMonitor 命令
+
+- `DevMonitor.showPanel()` - 显示监控面板
+- `DevMonitor.getStats()` - 获取统计信息
+- `DevMonitor.clearErrors()` - 清除错误记录
+- `window.__DEV_INJECT__` - 检查注入状态
+
diff --git a/qiming-dev-inject/test-projects/next-app-project/app/globals.css b/qiming-dev-inject/test-projects/next-app-project/app/globals.css
new file mode 100644
index 00000000..b5c61c95
--- /dev/null
+++ b/qiming-dev-inject/test-projects/next-app-project/app/globals.css
@@ -0,0 +1,3 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
diff --git a/qiming-dev-inject/test-projects/next-app-project/app/layout.tsx b/qiming-dev-inject/test-projects/next-app-project/app/layout.tsx
new file mode 100644
index 00000000..5ef05df1
--- /dev/null
+++ b/qiming-dev-inject/test-projects/next-app-project/app/layout.tsx
@@ -0,0 +1,32 @@
+import type { Metadata } from 'next'
+import { Inter } from 'next/font/google'
+import './globals.css'
+
+const inter = Inter({ subsets: ['latin'] })
+
+export const metadata: Metadata = {
+ title: 'Next.js App Test - dev-inject',
+ description: '测试 dev-inject 框架感知注入功能',
+}
+
+export default function RootLayout({
+ children,
+}: {
+ children: React.ReactNode
+}) {
+ return (
+
+
+ {/* DEV-INJECT-START */}
+ {process.env.NODE_ENV === 'development' && (
+
+ )}
+ {/* DEV-INJECT-END */}
+
+
+ {children}
+
+ )
+}
diff --git a/qiming-dev-inject/test-projects/next-app-project/app/page.tsx b/qiming-dev-inject/test-projects/next-app-project/app/page.tsx
new file mode 100644
index 00000000..f7799dae
--- /dev/null
+++ b/qiming-dev-inject/test-projects/next-app-project/app/page.tsx
@@ -0,0 +1,85 @@
+'use client'
+
+import { useState } from 'react'
+
+export default function Home() {
+ const [errors, setErrors] = useState([]);
+
+ const triggerError = () => {
+ try {
+ throw new Error('测试错误 - Next.js App Router');
+ } catch (error) {
+ setErrors(prev => [...prev, (error as Error).message]);
+ }
+ };
+
+ const triggerAsyncError = () => {
+ Promise.reject(new Error('异步错误测试 - Next.js App'));
+ };
+
+ const clearErrors = () => {
+ setErrors([]);
+ };
+
+ return (
+
+
+
+ 🟢 Next.js App Router Test
+ 测试 dev-inject 框架感知注入功能
+
+
+
+
+
🚀 测试功能
+
+
+
+
+
+
+
+
+
📊 错误记录
+ {errors.length === 0 ? (
+
暂无错误
+ ) : (
+
+ {errors.map((error, index) => (
+
+ {error}
+
+ ))}
+
+ )}
+
+
+
+
🔧 DevMonitor 检查
+
在浏览器控制台中检查:
+
+ DevMonitor.showPanel() - 显示监控面板
+ DevMonitor.getStats() - 获取统计信息
+ window.__DEV_INJECT__ - 检查注入状态
+
+
+
+
+
+ )
+}
diff --git a/qiming-dev-inject/test-projects/next-app-project/next.config.js b/qiming-dev-inject/test-projects/next-app-project/next.config.js
new file mode 100644
index 00000000..767719fc
--- /dev/null
+++ b/qiming-dev-inject/test-projects/next-app-project/next.config.js
@@ -0,0 +1,4 @@
+/** @type {import('next').NextConfig} */
+const nextConfig = {}
+
+module.exports = nextConfig
diff --git a/qiming-dev-inject/test-projects/next-app-project/package.json b/qiming-dev-inject/test-projects/next-app-project/package.json
new file mode 100644
index 00000000..23201aca
--- /dev/null
+++ b/qiming-dev-inject/test-projects/next-app-project/package.json
@@ -0,0 +1,25 @@
+{
+ "name": "next-app-test",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build",
+ "start": "next start",
+ "lint": "next lint"
+ },
+ "dependencies": {
+ "react": "^18",
+ "react-dom": "^18",
+ "next": "14.0.0"
+ },
+ "devDependencies": {
+ "typescript": "^5",
+ "@types/node": "^20",
+ "@types/react": "^18",
+ "@types/react-dom": "^18",
+ "autoprefixer": "^10.0.1",
+ "postcss": "^8",
+ "tailwindcss": "^3.3.0"
+ }
+}
diff --git a/qiming-dev-inject/test-projects/next-app-project/postcss.config.js b/qiming-dev-inject/test-projects/next-app-project/postcss.config.js
new file mode 100644
index 00000000..2ce518bb
--- /dev/null
+++ b/qiming-dev-inject/test-projects/next-app-project/postcss.config.js
@@ -0,0 +1,7 @@
+module.exports = {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
+
diff --git a/qiming-dev-inject/test-projects/next-app-project/tailwind.config.js b/qiming-dev-inject/test-projects/next-app-project/tailwind.config.js
new file mode 100644
index 00000000..0bf9d44e
--- /dev/null
+++ b/qiming-dev-inject/test-projects/next-app-project/tailwind.config.js
@@ -0,0 +1,12 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ content: [
+ './pages/**/*.{js,ts,jsx,tsx,mdx}',
+ './components/**/*.{js,ts,jsx,tsx,mdx}',
+ './app/**/*.{js,ts,jsx,tsx,mdx}',
+ ],
+ theme: {
+ extend: {},
+ },
+ plugins: [],
+}
diff --git a/qiming-dev-inject/test-projects/next-app-project/tsconfig.json b/qiming-dev-inject/test-projects/next-app-project/tsconfig.json
new file mode 100644
index 00000000..addc7c3e
--- /dev/null
+++ b/qiming-dev-inject/test-projects/next-app-project/tsconfig.json
@@ -0,0 +1,28 @@
+{
+ "compilerOptions": {
+ "target": "es5",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": ["./*"]
+ }
+ },
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
+ "exclude": ["node_modules"]
+}
+
diff --git a/qiming-dev-inject/test-projects/next-pages-project/.gitignore b/qiming-dev-inject/test-projects/next-pages-project/.gitignore
new file mode 100644
index 00000000..1403e903
--- /dev/null
+++ b/qiming-dev-inject/test-projects/next-pages-project/.gitignore
@@ -0,0 +1,36 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# local env files
+.env*.local
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
+
diff --git a/qiming-dev-inject/test-projects/next-pages-project/README.md b/qiming-dev-inject/test-projects/next-pages-project/README.md
new file mode 100644
index 00000000..21d651c7
--- /dev/null
+++ b/qiming-dev-inject/test-projects/next-pages-project/README.md
@@ -0,0 +1,32 @@
+# Next.js Pages Router 测试项目
+
+这是一个用于测试 dev-inject 框架感知注入功能的 Next.js Pages Router 项目。
+
+## 安装依赖
+
+```bash
+npm install
+```
+
+## 启动开发服务器
+
+```bash
+npm run dev
+```
+
+项目将在 http://localhost:3000 运行。
+
+## 测试步骤
+
+1. 启动项目:`npm run dev`
+2. 使用 dev-inject 注入监控脚本
+3. 点击页面上的按钮触发各种错误
+4. 在浏览器控制台运行 DevMonitor 命令查看监控信息
+
+## DevMonitor 命令
+
+- `DevMonitor.showPanel()` - 显示监控面板
+- `DevMonitor.getStats()` - 获取统计信息
+- `DevMonitor.clearErrors()` - 清除错误记录
+- `window.__DEV_INJECT__` - 检查注入状态
+
diff --git a/qiming-dev-inject/test-projects/next-pages-project/next.config.js b/qiming-dev-inject/test-projects/next-pages-project/next.config.js
new file mode 100644
index 00000000..ccf1eb20
--- /dev/null
+++ b/qiming-dev-inject/test-projects/next-pages-project/next.config.js
@@ -0,0 +1,5 @@
+/** @type {import('next').NextConfig} */
+const nextConfig = {}
+
+module.exports = nextConfig
+
diff --git a/qiming-dev-inject/test-projects/next-pages-project/package.json b/qiming-dev-inject/test-projects/next-pages-project/package.json
new file mode 100644
index 00000000..8a80e681
--- /dev/null
+++ b/qiming-dev-inject/test-projects/next-pages-project/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "next-pages-test",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build",
+ "start": "next start",
+ "lint": "next lint"
+ },
+ "dependencies": {
+ "react": "^18",
+ "react-dom": "^18",
+ "next": "14.0.0"
+ },
+ "devDependencies": {
+ "typescript": "^5",
+ "@types/node": "^20",
+ "@types/react": "^18",
+ "@types/react-dom": "^18"
+ }
+}
\ No newline at end of file
diff --git a/qiming-dev-inject/test-projects/next-pages-project/pages/_app.tsx b/qiming-dev-inject/test-projects/next-pages-project/pages/_app.tsx
new file mode 100644
index 00000000..179c18dc
--- /dev/null
+++ b/qiming-dev-inject/test-projects/next-pages-project/pages/_app.tsx
@@ -0,0 +1,97 @@
+import type { AppProps } from 'next/app'
+import '../styles/globals.css'
+
+
+ {/* DEV-INJECT-START */}
+
+ {/* DEV-INJECT-END */}
+
+
+ {/* DEV-INJECT-START */}
+
+ {/* DEV-INJECT-END */}
+
+
+ {/* DEV-INJECT-START */}
+
+ {/* DEV-INJECT-END */}
+
+
+ {/* DEV-INJECT-START */}
+
+ {/* DEV-INJECT-END */}
+
+
+ {/* DEV-INJECT-START */}
+
+ {/* DEV-INJECT-END */}
+
+
+ {/* DEV-INJECT-START */}
+
+ {/* DEV-INJECT-END */}
+
+
+ {/* DEV-INJECT-START */}
+
+ {/* DEV-INJECT-END */}
+
+
+ {/* DEV-INJECT-START */}
+
+ {/* DEV-INJECT-END */}
+
+
+ {/* DEV-INJECT-START */}
+
+ {/* DEV-INJECT-END */}
+
+
+ {/* DEV-INJECT-START */}
+
+ {/* DEV-INJECT-END */}
+
+
+ {/* DEV-INJECT-START */}
+
+ {/* DEV-INJECT-END */}
+
+
+ {/* DEV-INJECT-START */}
+
+ {/* DEV-INJECT-END */}
+
+
+ {/* DEV-INJECT-START */}
+
+ {/* DEV-INJECT-END */}
+
+
+ {/* DEV-INJECT-START */}
+
+ {/* DEV-INJECT-END */}
+
+
+ {/* DEV-INJECT-START */}
+ useEffect(() => {
+ if (typeof window !== 'undefined') {
+ (function() {
+ const remote = "https://testagent.xspaceagi.com/sdk/dev-monitor.js";
+ const separator = remote.includes('?') ? '&' : '?';
+ const script = document.createElement('script');
+ script.src = remote + separator + 't=' + Date.now();
+ script.dataset.id = 'dev-inject-monitor-script';
+ script.defer = true;
+ // 防止重复注入
+ if (!document.querySelector('[data-id="dev-inject-monitor-script"]')) {
+ document.head.appendChild(script);
+ }
+ })();
+ }
+ }, []);
+ {/* DEV-INJECT-END */}
+
+export default function App({ Component, pageProps }: AppProps) {
+ return
+}
+
diff --git a/qiming-dev-inject/test-projects/next-pages-project/pages/_document.tsx b/qiming-dev-inject/test-projects/next-pages-project/pages/_document.tsx
new file mode 100644
index 00000000..f84956b5
--- /dev/null
+++ b/qiming-dev-inject/test-projects/next-pages-project/pages/_document.tsx
@@ -0,0 +1,24 @@
+import { Html, Head, Main, NextScript } from 'next/document'
+
+export default function Document() {
+ return (
+
+
+
+
+
+ {/* DEV-INJECT-START */}
+ {typeof window !== 'undefined' && (
+
+ )}
+ {/* DEV-INJECT-END */}
+
+
+
+
+
+
+ )
+}
diff --git a/qiming-dev-inject/test-projects/next-pages-project/pages/index.tsx b/qiming-dev-inject/test-projects/next-pages-project/pages/index.tsx
new file mode 100644
index 00000000..b2862934
--- /dev/null
+++ b/qiming-dev-inject/test-projects/next-pages-project/pages/index.tsx
@@ -0,0 +1,92 @@
+import { useState } from 'react'
+import Head from 'next/head'
+
+export default function Home() {
+ const [errors, setErrors] = useState([]);
+
+ const triggerError = () => {
+ try {
+ throw new Error('测试错误 - Next.js Pages Router');
+ } catch (error) {
+ setErrors(prev => [...prev, (error as Error).message]);
+ }
+ };
+
+ const triggerAsyncError = () => {
+ Promise.reject(new Error('异步错误测试 - Next.js Pages'));
+ };
+
+ const clearErrors = () => {
+ setErrors([]);
+ };
+
+ return (
+ <>
+
+ Next.js Pages Test - dev-inject
+
+
+
+
+
+
+ 📄 Next.js Pages Router Test
+ 测试 dev-inject 框架感知注入功能
+
+
+
+
+
🚀 测试功能
+
+
+
+
+
+
+
+
+
📊 错误记录
+ {errors.length === 0 ? (
+
暂无错误
+ ) : (
+
+ {errors.map((error, index) => (
+
+ {error}
+
+ ))}
+
+ )}
+
+
+
+
🔧 DevMonitor 检查
+
在浏览器控制台中检查:
+
+ DevMonitor.showPanel() - 显示监控面板
+ DevMonitor.getStats() - 获取统计信息
+ window.__DEV_INJECT__ - 检查注入状态
+
+
+
+
+
+ >
+ )
+}
+
diff --git a/qiming-dev-inject/test-projects/next-pages-project/postcss.config.js b/qiming-dev-inject/test-projects/next-pages-project/postcss.config.js
new file mode 100644
index 00000000..2ce518bb
--- /dev/null
+++ b/qiming-dev-inject/test-projects/next-pages-project/postcss.config.js
@@ -0,0 +1,7 @@
+module.exports = {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
+
diff --git a/qiming-dev-inject/test-projects/next-pages-project/styles/globals.css b/qiming-dev-inject/test-projects/next-pages-project/styles/globals.css
new file mode 100644
index 00000000..b9b8c1f0
--- /dev/null
+++ b/qiming-dev-inject/test-projects/next-pages-project/styles/globals.css
@@ -0,0 +1,17 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+body {
+ margin: 0;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+ sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+code {
+ font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
+ monospace;
+}
\ No newline at end of file
diff --git a/qiming-dev-inject/test-projects/next-pages-project/tailwind.config.js b/qiming-dev-inject/test-projects/next-pages-project/tailwind.config.js
new file mode 100644
index 00000000..5d66275b
--- /dev/null
+++ b/qiming-dev-inject/test-projects/next-pages-project/tailwind.config.js
@@ -0,0 +1,12 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ content: [
+ './pages/**/*.{js,ts,jsx,tsx,mdx}',
+ './components/**/*.{js,ts,jsx,tsx,mdx}',
+ ],
+ theme: {
+ extend: {},
+ },
+ plugins: [],
+}
+
diff --git a/qiming-dev-inject/test-projects/next-pages-project/tsconfig.json b/qiming-dev-inject/test-projects/next-pages-project/tsconfig.json
new file mode 100644
index 00000000..46df0b1c
--- /dev/null
+++ b/qiming-dev-inject/test-projects/next-pages-project/tsconfig.json
@@ -0,0 +1,24 @@
+{
+ "compilerOptions": {
+ "target": "es5",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "paths": {
+ "@/*": ["./*"]
+ }
+ },
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
+ "exclude": ["node_modules"]
+}
+
diff --git a/qiming-dev-inject/test-projects/vite-project/.gitignore b/qiming-dev-inject/test-projects/vite-project/.gitignore
new file mode 100644
index 00000000..d600b6c7
--- /dev/null
+++ b/qiming-dev-inject/test-projects/vite-project/.gitignore
@@ -0,0 +1,25 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
diff --git a/qiming-dev-inject/test-projects/vite-project/README.md b/qiming-dev-inject/test-projects/vite-project/README.md
new file mode 100644
index 00000000..bb1275e3
--- /dev/null
+++ b/qiming-dev-inject/test-projects/vite-project/README.md
@@ -0,0 +1,32 @@
+# Vite React 测试项目
+
+这是一个用于测试 dev-inject 框架感知注入功能的 Vite + React 项目。
+
+## 安装依赖
+
+```bash
+npm install
+```
+
+## 启动开发服务器
+
+```bash
+npm run dev
+```
+
+项目将在 http://localhost:3001 运行。
+
+## 测试步骤
+
+1. 启动项目:`npm run dev`
+2. 使用 dev-inject 注入监控脚本
+3. 点击页面上的按钮触发各种错误
+4. 在浏览器控制台运行 DevMonitor 命令查看监控信息
+
+## DevMonitor 命令
+
+- `DevMonitor.showPanel()` - 显示监控面板
+- `DevMonitor.getStats()` - 获取统计信息
+- `DevMonitor.clearErrors()` - 清除错误记录
+- `window.__DEV_INJECT__` - 检查注入状态
+
diff --git a/qiming-dev-inject/test-projects/vite-project/index.html b/qiming-dev-inject/test-projects/vite-project/index.html
new file mode 100644
index 00000000..77054973
--- /dev/null
+++ b/qiming-dev-inject/test-projects/vite-project/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+ Vite Test - dev-inject
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/qiming-dev-inject/test-projects/vite-project/package.json b/qiming-dev-inject/test-projects/vite-project/package.json
new file mode 100644
index 00000000..d6eac0a1
--- /dev/null
+++ b/qiming-dev-inject/test-projects/vite-project/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "vite-test-project",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0"
+ },
+ "devDependencies": {
+ "@types/react": "^18.2.43",
+ "@types/react-dom": "^18.2.17",
+ "@vitejs/plugin-react": "^4.2.1",
+ "vite": "^5.0.8"
+ }
+}
diff --git a/qiming-dev-inject/test-projects/vite-project/postcss.config.js b/qiming-dev-inject/test-projects/vite-project/postcss.config.js
new file mode 100644
index 00000000..1054f2c2
--- /dev/null
+++ b/qiming-dev-inject/test-projects/vite-project/postcss.config.js
@@ -0,0 +1,12 @@
+// PostCSS 配置文件
+// 此文件用于防止 Vite 向上查找父目录的 PostCSS 配置
+// 本项目使用普通 CSS,不需要 PostCSS 插件
+
+export default {
+ plugins: {
+ // 如果需要 PostCSS 插件,可以在这里添加
+ // 例如:autoprefixer, tailwindcss 等
+ }
+}
+
+
diff --git a/qiming-dev-inject/test-projects/vite-project/src/App.css b/qiming-dev-inject/test-projects/vite-project/src/App.css
new file mode 100644
index 00000000..62db33e4
--- /dev/null
+++ b/qiming-dev-inject/test-projects/vite-project/src/App.css
@@ -0,0 +1,119 @@
+#root {
+ max-width: 1280px;
+ margin: 0 auto;
+ padding: 2rem;
+ text-align: center;
+}
+
+.app {
+ min-height: 100vh;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+}
+
+.container {
+ max-width: 800px;
+ margin: 0 auto;
+ padding: 20px;
+}
+
+header {
+ margin-bottom: 40px;
+}
+
+header h1 {
+ margin: 0;
+ font-size: 2.5rem;
+ margin-bottom: 10px;
+}
+
+header p {
+ margin: 0;
+ opacity: 0.8;
+ font-size: 1.2rem;
+}
+
+.section {
+ background: rgba(255, 255, 255, 0.1);
+ border-radius: 12px;
+ padding: 20px;
+ margin-bottom: 20px;
+ backdrop-filter: blur(10px);
+}
+
+.section h2 {
+ margin-top: 0;
+ margin-bottom: 15px;
+}
+
+.buttons {
+ display: flex;
+ gap: 10px;
+ justify-content: center;
+ flex-wrap: wrap;
+}
+
+button {
+ padding: 10px 20px;
+ border: none;
+ border-radius: 6px;
+ font-size: 14px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+button:hover {
+ transform: translateY(-2px);
+}
+
+button.danger {
+ background: #f56565;
+ color: white;
+}
+
+button.danger:hover {
+ background: #e53e3e;
+}
+
+button.secondary {
+ background: #718096;
+ color: white;
+}
+
+button.secondary:hover {
+ background: #4a5568;
+}
+
+.error-list {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+.error-item {
+ background: rgba(245, 101, 101, 0.2);
+ border-left: 3px solid #f56565;
+ padding: 10px;
+ margin: 5px 0;
+ border-radius: 4px;
+ text-align: left;
+ font-family: monospace;
+ font-size: 14px;
+}
+
+.no-errors {
+ color: #a0aec0;
+ font-style: italic;
+}
+
+ul {
+ text-align: left;
+}
+
+code {
+ background: rgba(0, 0, 0, 0.2);
+ padding: 2px 6px;
+ border-radius: 4px;
+ font-family: monospace;
+}
diff --git a/qiming-dev-inject/test-projects/vite-project/src/App.jsx b/qiming-dev-inject/test-projects/vite-project/src/App.jsx
new file mode 100644
index 00000000..b7e02174
--- /dev/null
+++ b/qiming-dev-inject/test-projects/vite-project/src/App.jsx
@@ -0,0 +1,73 @@
+import React, { useState } from 'react'
+import './App.css'
+
+function App() {
+ const [errors, setErrors] = useState([]);
+
+ const triggerError = () => {
+ try {
+ throw new Error('测试错误 - Vite 项目');
+ } catch (error) {
+ setErrors(prev => [...prev, error.message]);
+ }
+ };
+
+ const triggerAsyncError = () => {
+ Promise.reject(new Error('异步错误测试 - Vite'));
+ };
+
+ const clearErrors = () => {
+ setErrors([]);
+ };
+
+ return (
+
+
+
+ 🟣 Vite Test Project
+ 测试 dev-inject 框架感知注入功能
+
+
+
+
🚀 测试功能
+
+
+
+
+
+
+
+
+
📊 错误记录
+ {errors.length === 0 ? (
+
暂无错误
+ ) : (
+
+ {errors.map((error, index) => (
+ - {error}
+ ))}
+
+ )}
+
+
+
+
🔧 DevMonitor 检查
+
在浏览器控制台中检查:
+
+ DevMonitor.showPanel() - 显示监控面板
+ DevMonitor.getStats() - 获取统计信息
+ window.__DEV_INJECT__ - 检查注入状态
+
+
+
+
+ );
+}
+
+export default App;
diff --git a/qiming-dev-inject/test-projects/vite-project/src/index.css b/qiming-dev-inject/test-projects/vite-project/src/index.css
new file mode 100644
index 00000000..c9e6946a
--- /dev/null
+++ b/qiming-dev-inject/test-projects/vite-project/src/index.css
@@ -0,0 +1,66 @@
+:root {
+ font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
+ line-height: 1.5;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ display: flex;
+ place-items: center;
+ min-width: 320px;
+ min-height: 100vh;
+}
+
+#root {
+ width: 100%;
+ margin: 0 auto;
+ text-align: center;
+}
+
+button {
+ border-radius: 8px;
+ border: 1px solid transparent;
+ padding: 0.6em 1.2em;
+ font-size: 1em;
+ font-weight: 500;
+ font-family: inherit;
+ background-color: #1a1a1a;
+ cursor: pointer;
+ transition: border-color 0.25s;
+}
+
+button:hover {
+ border-color: #646cff;
+}
+
+button:focus,
+button:focus-visible {
+ outline: 4px auto -webkit-focus-ring-color;
+}
+
+@media (prefers-color-scheme: light) {
+ :root {
+ color: #213547;
+ background-color: #ffffff;
+ }
+
+ button {
+ background-color: #f9f9f9;
+ }
+}
\ No newline at end of file
diff --git a/qiming-dev-inject/test-projects/vite-project/src/main.jsx b/qiming-dev-inject/test-projects/vite-project/src/main.jsx
new file mode 100644
index 00000000..54b39dd1
--- /dev/null
+++ b/qiming-dev-inject/test-projects/vite-project/src/main.jsx
@@ -0,0 +1,10 @@
+import React from 'react'
+import ReactDOM from 'react-dom/client'
+import App from './App.jsx'
+import './index.css'
+
+ReactDOM.createRoot(document.getElementById('root')).render(
+
+
+ ,
+)
diff --git a/qiming-dev-inject/test-projects/vite-project/vite.config.js b/qiming-dev-inject/test-projects/vite-project/vite.config.js
new file mode 100644
index 00000000..6dbef817
--- /dev/null
+++ b/qiming-dev-inject/test-projects/vite-project/vite.config.js
@@ -0,0 +1,40 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+export default defineConfig({
+ plugins: [
+
+ //
+ {
+ name: 'dev-inject',
+ enforce: 'post', // 确保在 HTML 注入阶段最后执行
+ transformIndexHtml(html) {
+ if (!html.includes('data-id="dev-inject-monitor"')) {
+ return html.replace("", `
+
+ \n`);
+ }
+ return html;
+ }
+ },
+ //
+
+ react()
+ ],
+ server: {
+ port: 3001
+ }
+})
diff --git a/qiming-dev-inject/test/e2e/e2e.test.js b/qiming-dev-inject/test/e2e/e2e.test.js
new file mode 100644
index 00000000..7a193a33
--- /dev/null
+++ b/qiming-dev-inject/test/e2e/e2e.test.js
@@ -0,0 +1,388 @@
+import { test, describe } from 'node:test';
+import assert from 'node:assert';
+import { execSync, spawn } from 'child_process';
+import { readFileSync } from 'fs';
+import { join } from 'path';
+import { fileURLToPath } from 'url';
+import { dirname } from 'path';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+
+describe('端到端测试', () => {
+ const testDir = join(__dirname, 'fixtures');
+ const cliPath = join(__dirname, '..', '..', 'bin', 'index.js');
+ const projectRoot = join(__dirname, '..', '..');
+
+ // 创建测试项目的基本结构
+ const testProjects = {
+ vite: {
+ name: 'test-vite-project',
+ files: {
+ 'package.json': JSON.stringify({
+ "name": "test-vite-project",
+ "version": "1.0.0",
+ "type": "module",
+ "scripts": { "dev": "vite" },
+ "dependencies": { "react": "^18.2.0" }
+ }, null, 2),
+ 'vite.config.js': `import { defineConfig } from 'vite'
+export default defineConfig({
+ plugins: []
+})`,
+ 'index.html': `
+
+
+ Vite Test
+
+
+
+
+`,
+ 'src/main.js': `import './app.js'`,
+ 'src/app.js': `console.log('Vite app loaded');
+document.getElementById('app').innerHTML = 'Vite App
';`
+ }
+ },
+ 'next-app': {
+ name: 'test-next-app',
+ files: {
+ 'package.json': JSON.stringify({
+ "name": "test-next-app",
+ "version": "1.0.0",
+ "scripts": { "dev": "next dev" }
+ }, null, 2),
+ 'next.config.js': 'module.exports = {}',
+ 'app/layout.tsx': `import './globals.css'
+export default function RootLayout({ children }) {
+ return (
+
+ {children}
+
+ )}`,
+ 'app/page.tsx': `export default function Home() {
+ return Next.js App
;
+}`,
+ 'app/globals.css': 'body { font-family: Arial, sans-serif; }'
+ }
+ },
+ 'html-static': {
+ name: 'test-html-project',
+ files: {
+ 'index.html': `
+
+
+
+ Static HTML
+
+
+ Static HTML Project
+ This is a static HTML project.
+
+`
+ }
+ }
+ };
+
+ function createProject(projectName, files) {
+ const projectPath = join(testDir, projectName);
+ mkdirSync(projectPath, { recursive: true });
+
+ for (const [filePath, content] of Object.entries(files)) {
+ const fullPath = join(projectPath, filePath);
+ const dir = dirname(fullPath);
+
+ if (!existsSync(dir)) {
+ mkdirSync(dir, { recursive: true });
+ }
+
+ writeFileSync(fullPath, content);
+ }
+
+ return projectPath;
+ }
+
+ function cleanupProject(projectPath) {
+ if (existsSync(projectPath)) {
+ rmSync(projectPath, { recursive: true });
+ }
+ }
+
+ function runCliInProject(projectPath, args, options = {}) {
+ try {
+ const result = execSync(`node ${cliPath} ${args}`, {
+ encoding: 'utf8',
+ cwd: projectPath,
+ stdio: ['ignore', 'pipe', 'pipe'],
+ timeout: options.timeout || 10000
+ });
+ return { success: true, output: result };
+ } catch (error) {
+ return {
+ success: false,
+ output: error.stdout || error.message,
+ code: error.status,
+ signal: error.signal
+ };
+ }
+ }
+
+ test.before(() => {
+ if (!existsSync(testDir)) {
+ mkdirSync(testDir, { recursive: true });
+ }
+ });
+
+ test.after(() => {
+ // 清理所有测试项目
+ Object.keys(testProjects).forEach(projectName => {
+ const projectPath = join(testDir, testProjects[projectName].name);
+ cleanupProject(projectPath);
+ });
+ });
+
+ describe('Vite 项目完整流程', () => {
+ test('应该完成 Vite 项目的完整注入流程', () => {
+ const projectPath = createProject(testProjects.vite.name, testProjects.vite.files);
+
+ try {
+ // 1. 检测项目类型
+ const detectResult = runCliInProject(projectPath, 'install --remote=/scripts/monitor.js --framework --dry-run --verbose');
+ assert.strictEqual(detectResult.success, true);
+ assert.match(detectResult.output, /检测到项目类型: vite/);
+
+ // 2. 执行实际注入
+ const injectResult = runCliInProject(projectPath, 'install --remote=/scripts/monitor.js --framework');
+ assert.strictEqual(injectResult.success, true);
+ assert.match(injectResult.output, /框架注入成功/);
+
+ // 3. 验证配置文件被修改
+ const viteConfigPath = join(projectPath, 'vite.config.js');
+ assert(existsSync(viteConfigPath));
+ const configContent = readFileSync(viteConfigPath, 'utf8');
+ assert.match(configContent, /dev-inject-plugin/);
+ assert.match(configContent, /scripts\/monitor\.js/);
+
+ // 4. 验证原始配置保留
+ assert.match(configContent, /defineConfig/);
+
+ // 5. 执行卸载
+ const uninstallResult = runCliInProject(projectPath, 'uninstall --framework');
+ assert.strictEqual(uninstallResult.success, true);
+
+ // 6. 验证插件被移除
+ const cleanedContent = readFileSync(viteConfigPath, 'utf8');
+ assert(!cleanedContent.includes('dev-inject-plugin'));
+
+ } finally {
+ cleanupProject(projectPath);
+ }
+ });
+
+ test('应该处理不同的脚本类型', () => {
+ const projectPath = createProject(testProjects.vite.name, testProjects.vite.files);
+
+ try {
+ // 测试 HTTP URL
+ const httpResult = runCliInProject(projectPath, 'install --remote=https://cdn.example.com/monitor.js --framework --dry-run');
+ assert.strictEqual(httpResult.success, true);
+ assert.match(httpResult.output, /检测到项目类型: vite/);
+
+ // 测试本地路径
+ const localResult = runCliInProject(projectPath, 'install --remote=/assets/dev-monitor.js --framework --dry-run');
+ assert.strictEqual(localResult.success, true);
+ assert.match(localResult.output, /检测到项目类型: vite/);
+
+ } finally {
+ cleanupProject(projectPath);
+ }
+ });
+ });
+
+ describe('Next.js 项目完整流程', () => {
+ test('应该完成 Next.js App Router 的完整注入流程', () => {
+ const projectPath = createProject(testProjects['next-app'].name, testProjects['next-app'].files);
+
+ try {
+ // 1. 检测项目类型
+ const detectResult = runCliInProject(projectPath, 'install --remote=/scripts/monitor.js --framework --dry-run --verbose');
+ assert.strictEqual(detectResult.success, true);
+ assert.match(detectResult.output, /检测到项目类型: next-app/);
+
+ // 2. 执行实际注入
+ const injectResult = runCliInProject(projectPath, 'install --remote=/scripts/monitor.js --framework');
+ assert.strictEqual(injectResult.success, true);
+ assert.match(injectResult.output, /框架注入成功/);
+
+ // 3. 验证 layout 文件被修改
+ const layoutPath = join(projectPath, 'app/layout.tsx');
+ assert(existsSync(layoutPath));
+ const layoutContent = readFileSync(layoutPath, 'utf8');
+ assert.match(layoutContent, /process\.env\.NODE_ENV === 'development'/);
+ assert.match(layoutContent, /scripts\/monitor\.js/);
+
+ // 4. 验证原始内容保留
+ assert.match(layoutContent, /RootLayout/);
+ assert.match(layoutContent, /children/);
+
+ // 5. 执行卸载
+ const uninstallResult = runCliInProject(projectPath, 'uninstall --framework');
+ assert.strictEqual(uninstallResult.success, true);
+
+ // 6. 验证注入内容被移除
+ const cleanedContent = readFileSync(layoutPath, 'utf8');
+ assert(!cleanedContent.includes('process.env.NODE_ENV'));
+ assert(!cleanedContent.includes('scripts/monitor.js'));
+
+ } finally {
+ cleanupProject(projectPath);
+ }
+ });
+ });
+
+ describe('静态 HTML 项目完整流程', () => {
+ test('应该完成静态 HTML 项目的完整注入流程', () => {
+ const projectPath = createProject(testProjects['html-static'].name, testProjects['html-static'].files);
+
+ try {
+ // 1. 检测项目类型
+ const detectResult = runCliInProject(projectPath, 'install --remote=/scripts/monitor.js --framework --dry-run --verbose');
+ assert.strictEqual(detectResult.success, true);
+ assert.match(detectResult.output, /检测到项目类型: html-static/);
+
+ // 2. 执行实际注入
+ const injectResult = runCliInProject(projectPath, 'install --remote=/scripts/monitor.js --framework');
+ assert.strictEqual(injectResult.success, true);
+ assert.match(injectResult.output, /成功处理 \d+ 个文件/);
+
+ // 3. 验证 HTML 文件被修改
+ const htmlPath = join(projectPath, 'index.html');
+ assert(existsSync(htmlPath));
+ const htmlContent = readFileSync(htmlPath, 'utf8');
+ assert.match(htmlContent, /');
+ }
+ return html;
+ }
+ }
+ ],
+ server: {
+ port: 3000
+ }
+})`;
+ writeFileSync(join(testDir, 'vite.config.js'), viteConfigWithPlugin);
+
+ // 执行注入(应该检测到已存在)
+ const result = smartInject({
+ remote: 'http://localhost:9000/dev-monitor.js',
+ dryRun: true
+ }, testDir);
+
+ assert.strictEqual(result, true);
+ });
+
+ test('应该处理没有 plugins 数组的情况', () => {
+ // 创建没有 plugins 的配置
+ const simpleConfig = `export default {
+ server: {
+ port: 3000
+ }
+ }`;
+ writeFileSync(join(testDir, 'vite.config.js'), simpleConfig);
+
+ const result = smartInject({
+ remote: '/scripts/monitor.js',
+ dryRun: true
+ }, testDir);
+
+ assert.strictEqual(result, true);
+ });
+
+ test('应该处理 TypeScript 配置文件', () => {
+ const tsConfig = `import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+export default defineConfig({
+ plugins: [react()]
+})`;
+ writeFileSync(join(testDir, 'vite.config.ts'), tsConfig);
+
+ const result = smartInject({
+ remote: '/scripts/monitor.js',
+ dryRun: true
+ }, testDir);
+
+ assert.strictEqual(result, true);
+ });
+ });
+
+ describe('Next.js App Router 注入', () => {
+ test('应该成功注入到 app/layout.tsx', () => {
+ // 创建 Next.js App Router 结构
+ mkdirSync(join(testDir, 'app'), { recursive: true });
+ writeFileSync(join(testDir, 'next.config.js'), 'module.exports = {}');
+
+ const layoutContent = `import type { Metadata } from 'next'
+import { Inter } from 'next/font/google'
+import './globals.css'
+
+const inter = Inter({ subsets: ['latin'] })
+
+export const metadata: Metadata = {
+ title: 'Next.js App',
+ description: 'Generated by create next app',
+}
+
+export default function RootLayout({
+ children,
+}: {
+ children: React.ReactNode
+}) {
+ return (
+
+ {children}
+
+ )
+}`;
+ writeFileSync(join(testDir, 'app', 'layout.tsx'), layoutContent);
+
+ const result = smartInject({
+ remote: 'http://localhost:9000/dev-monitor.js',
+ dryRun: true
+ }, testDir);
+
+ assert.strictEqual(result, true);
+ });
+
+ test('应该处理 app/layout.js', () => {
+ mkdirSync(join(testDir, 'app'), { recursive: true });
+ writeFileSync(join(testDir, 'next.config.js'), 'module.exports = {}');
+
+ const jsLayout = `import './globals.css'
+
+export default function RootLayout({ children }) {
+ return (
+
+ {children}
+
+ )
+}`;
+ writeFileSync(join(testDir, 'app', 'layout.js'), jsLayout);
+
+ const result = smartInject({
+ remote: '/scripts/monitor.js',
+ dryRun: true
+ }, testDir);
+
+ assert.strictEqual(result, true);
+ });
+ });
+
+ describe('Next.js Pages Router 注入', () => {
+ test('应该成功注入到 pages/_document.tsx', () => {
+ mkdirSync(join(testDir, 'pages'), { recursive: true });
+ writeFileSync(join(testDir, 'next.config.js'), 'module.exports = {}');
+
+ const documentContent = `import { Html, Head, Main, NextScript } from 'next/document'
+
+export default function Document() {
+ return (
+
+
+
+
+
+
+
+
+
+
+ )
+}`;
+ writeFileSync(join(testDir, 'pages', '_document.tsx'), documentContent);
+
+ const result = smartInject({
+ remote: 'http://localhost:9000/dev-monitor.js',
+ dryRun: true
+ }, testDir);
+
+ assert.strictEqual(result, true);
+ });
+
+ test('应该处理 pages/_document.js', () => {
+ mkdirSync(join(testDir, 'pages'), { recursive: true });
+ writeFileSync(join(testDir, 'next.config.js'), 'module.exports = {}');
+
+ const jsDocument = `const { Html, Head, Main, NextScript } = require('next/document')
+
+export default function Document() {
+ return (
+
+
+ My App
+
+
+
+
+
+
+ )
+}`;
+ writeFileSync(join(testDir, 'pages', '_document.js'), jsDocument);
+
+ const result = smartInject({
+ remote: '/scripts/monitor.js',
+ dryRun: true
+ }, testDir);
+
+ assert.strictEqual(result, true);
+ });
+ });
+
+ describe('混合模式处理', () => {
+ test('应该优先注入到 app/layout', () => {
+ // 创建混合模式结构
+ mkdirSync(join(testDir, 'app'), { recursive: true });
+ mkdirSync(join(testDir, 'pages'), { recursive: true });
+ writeFileSync(join(testDir, 'next.config.js'), 'module.exports = {}');
+
+ writeFileSync(join(testDir, 'app', 'layout.tsx'), 'export default function Layout() { return App Layout
; }');
+ writeFileSync(join(testDir, 'pages', '_document.tsx'), 'export default function Document() { return Document
; }');
+
+ const result = smartInject({
+ remote: '/scripts/monitor.js',
+ dryRun: true
+ }, testDir);
+
+ assert.strictEqual(result, true);
+ });
+ });
+
+ describe('错误处理', () => {
+ test('应该处理不支持的框架类型', () => {
+ writeFileSync(join(testDir, 'index.html'), '');
+
+ try {
+ smartInject({
+ remote: '/scripts/monitor.js',
+ dryRun: false
+ }, testDir);
+ assert.fail('应该抛出错误');
+ } catch (error) {
+ assert.match(error.message, /不支持的项目类型/);
+ }
+ });
+
+ test('应该处理配置文件不存在的情况', () => {
+ writeFileSync(join(testDir, 'next.config.js'), 'module.exports = {}');
+ // 不创建 layout 文件
+
+ try {
+ smartInject({
+ remote: '/scripts/monitor.js',
+ dryRun: false
+ }, testDir);
+ assert.fail('应该抛出错误');
+ } catch (error) {
+ assert.match(error.message, /未找到/);
+ }
+ });
+
+ test('应该处理无效的配置文件', () => {
+ writeFileSync(join(testDir, 'vite.config.js'), 'invalid javascript content');
+
+ try {
+ smartInject({
+ remote: '/scripts/monitor.js',
+ dryRun: false
+ }, testDir);
+ assert.fail('应该抛出错误');
+ } catch (error) {
+ assert.ok(error.message.length > 0); // 应该有某种错误信息
+ }
+ });
+ });
+
+ describe('实际文件修改测试', () => {
+ test('应该实际修改 Vite 配置文件', () => {
+ const originalConfig = `import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+export default defineConfig({
+ plugins: [react()],
+ server: {
+ port: 3000
+ }
+})`;
+
+ writeFileSync(join(testDir, 'vite.config.js'), originalConfig);
+
+ // 执行实际注入
+ const result = smartInject({
+ remote: '/scripts/monitor.js',
+ dryRun: false,
+ verbose: false
+ }, testDir);
+
+ assert.strictEqual(result, true);
+
+ // 验证文件被修改
+ const modifiedContent = readFileSync(join(testDir, 'vite.config.js'), 'utf8');
+ assert.match(modifiedContent, /dev-inject-plugin/);
+ assert.match(modifiedContent, /scripts\/monitor\.js/);
+
+ // 验证原始内容保留
+ assert.match(modifiedContent, /defineConfig/);
+ assert.match(modifiedContent, /react\(\)/);
+ });
+
+ test('应该能够移除注入的内容', () => {
+ // 先注入
+ smartInject({
+ remote: '/scripts/monitor.js',
+ dryRun: false
+ }, testDir);
+
+ // 然后移除
+ const result = removeInjection({
+ dryRun: false,
+ verbose: false
+ }, testDir);
+
+ assert.strictEqual(result, true);
+
+ // 验证插件被移除
+ const cleanedContent = readFileSync(join(testDir, 'vite.config.js'), 'utf8');
+ assert(!cleanedContent.includes('dev-inject-plugin'));
+ });
+ });
+
+ describe('不同脚本类型', () => {
+ test('应该处理 HTTP URL', () => {
+ writeFileSync(join(testDir, 'vite.config.js'), 'export default { plugins: [] }');
+
+ const result = smartInject({
+ remote: 'https://cdn.example.com/monitor.js',
+ dryRun: true
+ }, testDir);
+
+ assert.strictEqual(result, true);
+ });
+
+ test('应该处理本地路径', () => {
+ writeFileSync(join(testDir, 'vite.config.js'), 'export default { plugins: [] }');
+
+ const result = smartInject({
+ remote: '/assets/dev-monitor.js',
+ dryRun: true
+ }, testDir);
+
+ assert.strictEqual(result, true);
+ });
+ });
+});
diff --git a/qiming-dev-inject/test/package.json b/qiming-dev-inject/test/package.json
new file mode 100644
index 00000000..b244b0ee
--- /dev/null
+++ b/qiming-dev-inject/test/package.json
@@ -0,0 +1,14 @@
+{
+ "type": "module",
+ "scripts": {
+ "test": "node --test",
+ "test:unit": "node --test test/unit/*.test.js",
+ "test:integration": "node --test test/integration/*.test.js",
+ "test:e2e": "node --test test/e2e/*.test.js",
+ "test:watch": "node --test --watch",
+ "test:coverage": "node --test --experimental-test-coverage"
+ },
+ "devDependencies": {
+ "node:test": "^1.0.0"
+ }
+}
diff --git a/qiming-dev-inject/test/test-runner.js b/qiming-dev-inject/test/test-runner.js
new file mode 100644
index 00000000..61814185
--- /dev/null
+++ b/qiming-dev-inject/test/test-runner.js
@@ -0,0 +1,180 @@
+#!/usr/bin/env node
+
+console.log('🧪 dev-inject 测试运行器\n');
+console.log('=' .repeat(50));
+
+import { execSync } from 'child_process';
+import { existsSync } from 'fs';
+import { join } from 'path';
+
+const testDir = new URL('.', import.meta.url).pathname;
+
+// 颜要在项目根目录运行
+const projectRoot = process.cwd();
+const cliPath = join(projectRoot, 'bin', 'index.js');
+const testPackagePath = join(projectRoot, 'test', 'package.json');
+
+// 检查测试环境
+function checkTestEnvironment() {
+ if (!existsSync(cliPath)) {
+ console.error('❌ CLI 路径不存在:', cliPath);
+ console.log('请确保在 dev-inject 项目根目录运行此脚本');
+ process.exit(1);
+ }
+
+ if (!existsSync(testPackagePath)) {
+ console.error('❌ 测试配置不存在:', testPackagePath);
+ console.log('正在创建测试配置...');
+ execSync('npm init -y', { cwd: join(projectRoot, 'test'), stdio: 'inherit' });
+ }
+}
+
+// 运行指定类型的测试
+function runTests(testType) {
+ console.log(`\n🚀 运行 ${testType.toUpperCase()} 测试...\n`);
+
+ try {
+ const testCommand = `node --test test/${testType}/*.test.js`;
+ execSync(testCommand, {
+ cwd: projectRoot,
+ stdio: 'inherit',
+ timeout: 60000 // 60秒超时
+ });
+ return true;
+ } catch (error) {
+ console.log(`\n❌ ${testType.toUpperCase()} 测试失败`);
+ if (error.signal === 'SIGTERM') {
+ console.log(' 原因: 测试超时');
+ }
+ return false;
+ }
+}
+
+// 运行特定测试
+function runSpecificTest(testPath) {
+ console.log(`\n🎯 运行特定测试: ${testPath}\n`);
+
+ try {
+ execSync(`node --test ${testPath}`, {
+ cwd: projectRoot,
+ stdio: 'inherit',
+ timeout: 30000
+ });
+ return true;
+ } catch (error) {
+ console.log(`\n❌ 测试 ${testPath} 失败`);
+ return false;
+ }
+}
+
+// 生成测试报告
+function generateTestReport(results) {
+ const totalTests = Object.keys(results).length;
+ const passedTests = Object.values(results).filter(Boolean).length;
+ const failedTests = totalTests - passedTests;
+
+ console.log('\n' + '='.repeat(50));
+ console.log('📊 测试报告汇总');
+ console.log('='.repeat(50));
+ console.log(`✅ 通过: ${passedTests}/${totalTests}`);
+ console.log(`❌ 失败: ${failedTests}/${totalTests}`);
+ console.log(`📈 成功率: ${Math.round((passedTests / totalTests) * 100)}%`);
+
+ if (passedTests === totalTests) {
+ console.log('\n🎉 所有测试都通过了!dev-inject 功能正常!');
+ } else {
+ console.log('\n⚠️ 有部分测试失败,请检查相关功能。');
+ }
+
+ console.log('\n🔍 测试覆盖范围:');
+ console.log(' 📦 单元测试 - 核心函数和逻辑');
+ console.log(' 🔧 集成测试 - 模块间协作');
+ console.log(' 🌐 端到端测试 - 完整工作流');
+ console.log(' 📱 框架支持 - Vite、Next.js、HTML');
+
+ return passedTests === totalTests;
+}
+
+// 主函数
+function main() {
+ const args = process.argv.slice(2);
+
+ // 检查测试环境
+ checkTestEnvironment();
+
+ // 解析命令行参数
+ if (args.includes('--help') || args.includes('-h')) {
+ console.log(`
+🧪 dev-inject 测试运行器
+
+用法:
+ node test/test-runner.js [选项] [测试类型/文件]
+
+选项:
+ --help, -h 显示帮助信息
+ --unit 运行单元测试
+ --integration 运行集成测试
+ --e2e 运行端到端测试
+ --all 运行所有测试
+ --verbose 显示详细输出
+ --watch 监听模式(需要 node --test 支持)
+
+示例:
+ node test/test-runner.js --unit # 只运行单元测试
+ node test/test-runner.js --integration # 只运行集成测试
+ node test/test-runner.js --e2e # 只运行端到端测试
+ node test/test-runner.js --all # 运行所有测试
+ node test/test-runner.js test/unit/parseArgs.test.js # 运行特定测试文件
+
+测试类型:
+ unit 单元测试(核心函数)
+ integration 集成测试(模块协作)
+ e2e 端到端测试(完整流程)
+`);
+ process.exit(0);
+ }
+
+ const results = {};
+
+ if (args.includes('--all')) {
+ // 运行所有测试
+ console.log('🎯 运行所有测试套件...');
+ results.unit = runTests('unit');
+ results.integration = runTests('integration');
+ results.e2e = runTests('e2e');
+ } else if (args.includes('--unit')) {
+ // 运行单元测试
+ results.unit = runTests('unit');
+ } else if (args.includes('--integration')) {
+ // 运行集成测试
+ results.integration = runTests('integration');
+ } else if (args.includes('--e2e')) {
+ // 运行端到端测试
+ results.e2e = runTests('e2e');
+ } else if (args.some(arg => arg.endsWith('.test.js'))) {
+ // 运行特定测试文件
+ const testFile = args.find(arg => arg.endsWith('.test.js'));
+ const success = runSpecificTest(testFile);
+ process.exit(success ? 0 : 1);
+ } else {
+ // 默认运行快速测试
+ console.log('🚀 运行快速测试套件...');
+ results.unit = runTests('unit');
+
+ if (results.unit) {
+ console.log('\n✅ 单元测试通过,继续运行集成测试...');
+ results.integration = runTests('integration');
+ }
+
+ if (results.unit && results.integration) {
+ console.log('\n✅ 基础测试通过,可以运行完整测试:');
+ console.log(' node test/test-runner.js --all');
+ }
+ }
+
+ // 生成报告
+ const allPassed = generateTestReport(results);
+ process.exit(allPassed ? 0 : 1);
+}
+
+main();
diff --git a/qiming-dev-inject/test/unit/detectProjectType.test.js b/qiming-dev-inject/test/unit/detectProjectType.test.js
new file mode 100644
index 00000000..1e8aa2b0
--- /dev/null
+++ b/qiming-dev-inject/test/unit/detectProjectType.test.js
@@ -0,0 +1,160 @@
+import { test, describe } from 'node:test';
+import assert from 'node:assert';
+import { existsSync, mkdirSync, writeFileSync, rmSync } from 'fs';
+import { join } from 'path';
+import { fileURLToPath } from 'url';
+import { dirname } from 'path';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+
+// 导入要测试的模块
+import { detectProjectType } from '../../lib/framework-inject.js';
+
+describe('框架检测功能', () => {
+ const testDir = join(__dirname, 'fixtures');
+
+ // 测试前设置
+ test.before(() => {
+ // 确保测试目录存在
+ if (!existsSync(testDir)) {
+ mkdirSync(testDir, { recursive: true });
+ }
+ });
+
+ // 测试后清理
+ test.after(() => {
+ // 清理测试文件
+ const filesToClean = [
+ 'vite.config.js',
+ 'vite.config.ts',
+ 'next.config.js',
+ 'app/layout.tsx',
+ 'pages/_document.tsx',
+ 'public/index.html',
+ 'index.html'
+ ];
+
+ filesToClean.forEach(file => {
+ const filePath = join(testDir, file);
+ if (existsSync(filePath)) {
+ rmSync(filePath);
+ }
+ });
+ });
+
+ describe('Vite 项目检测', () => {
+ test('应该检测到 Vite 项目 (js)', () => {
+ writeFileSync(join(testDir, 'vite.config.js'), 'module.exports = {};');
+ const type = detectProjectType(testDir);
+ assert.strictEqual(type, 'vite');
+ });
+
+ test('应该检测到 Vite 项目 (ts)', () => {
+ writeFileSync(join(testDir, 'vite.config.ts'), 'export default {};');
+ const type = detectProjectType(testDir);
+ assert.strictEqual(type, 'vite');
+ });
+
+ test('优先检测 Vite 而不是其他配置', () => {
+ writeFileSync(join(testDir, 'vite.config.js'), 'module.exports = {};');
+ writeFileSync(join(testDir, 'next.config.js'), 'module.exports = {};');
+ writeFileSync(join(testDir, 'public/index.html'), '');
+ const type = detectProjectType(testDir);
+ assert.strictEqual(type, 'vite');
+ });
+ });
+
+ describe('Next.js App Router 检测', () => {
+ test('应该检测到 Next.js App Router', () => {
+ writeFileSync(join(testDir, 'next.config.js'), 'module.exports = {};');
+ mkdirSync(join(testDir, 'app'), { recursive: true });
+ writeFileSync(join(testDir, 'app', 'layout.tsx'), 'export default function Layout() {}');
+ const type = detectProjectType(testDir);
+ assert.strictEqual(type, 'next-app');
+ });
+
+ test('应该检测到 Next.js App Router (js)', () => {
+ writeFileSync(join(testDir, 'next.config.js'), 'module.exports = {};');
+ mkdirSync(join(testDir, 'app'), { recursive: true });
+ writeFileSync(join(testDir, 'app', 'layout.js'), 'export default function Layout() {}');
+ const type = detectProjectType(testDir);
+ assert.strictEqual(type, 'next-app');
+ });
+ });
+
+ describe('Next.js Pages Router 检测', () => {
+ test('应该检测到 Next.js Pages Router', () => {
+ writeFileSync(join(testDir, 'next.config.js'), 'module.exports = {};');
+ mkdirSync(join(testDir, 'pages'), { recursive: true });
+ writeFileSync(join(testDir, 'pages', '_document.tsx'), 'export default function Document() {}');
+ const type = detectProjectType(testDir);
+ assert.strictEqual(type, 'next-pages');
+ });
+
+ test('应该检测到 Next.js Pages Router (js)', () => {
+ writeFileSync(join(testDir, 'next.config.js'), 'module.exports = {};');
+ mkdirSync(join(testDir, 'pages'), { recursive: true });
+ writeFileSync(join(testDir, 'pages', '_document.js'), 'export default function Document() {}');
+ const type = detectProjectType(testDir);
+ assert.strictEqual(type, 'next-pages');
+ });
+ });
+
+ describe('Next.js 混合模式检测', () => {
+ test('应该检测到 Next.js 混合模式', () => {
+ writeFileSync(join(testDir, 'next.config.js'), 'module.exports = {};');
+ mkdirSync(join(testDir, 'app'), { recursive: true });
+ mkdirSync(join(testDir, 'pages'), { recursive: true });
+ writeFileSync(join(testDir, 'app', 'layout.tsx'), 'export default function Layout() {}');
+ writeFileSync(join(testDir, 'pages', '_document.tsx'), 'export default function Document() {}');
+ const type = detectProjectType(testDir);
+ assert.strictEqual(type, 'next-hybrid');
+ });
+ });
+
+ describe('Create React App 检测', () => {
+ test('应该检测到 Create React App', () => {
+ mkdirSync(join(testDir, 'public'), { recursive: true });
+ writeFileSync(join(testDir, 'public', 'index.html'), '');
+ const type = detectProjectType(testDir);
+ assert.strictEqual(type, 'create-react-app');
+ });
+ });
+
+ describe('HTML 静态项目检测', () => {
+ test('应该检测到 HTML 静态项目', () => {
+ writeFileSync(join(testDir, 'index.html'), '');
+ const type = detectProjectType(testDir);
+ assert.strictEqual(type, 'html-static');
+ });
+ });
+
+ describe('未知项目检测', () => {
+ test('应该返回 unknown 对于空目录', () => {
+ const type = detectProjectType(testDir);
+ assert.strictEqual(type, 'unknown');
+ });
+
+ test('应该返回 unknown 对于不存在的目录', () => {
+ const type = detectProjectType('/non-existent-directory');
+ assert.strictEqual(type, 'unknown');
+ });
+ });
+
+ describe('优先级检测', () => {
+ test('Next.js 优先于 HTML', () => {
+ writeFileSync(join(testDir, 'next.config.js'), 'module.exports = {};');
+ writeFileSync(join(testDir, 'index.html'), '');
+ const type = detectProjectType(testDir);
+ assert.strictEqual(type, 'next-app'); // 默认是 app 模式
+ });
+
+ test('Vite 优先于 Next.js', () => {
+ writeFileSync(join(testDir, 'vite.config.js'), 'module.exports = {};');
+ writeFileSync(join(testDir, 'next.config.js'), 'module.exports = {};');
+ const type = detectProjectType(testDir);
+ assert.strictEqual(type, 'vite');
+ });
+ });
+});
diff --git a/qiming-dev-inject/test/unit/fixtures/app/layout.js b/qiming-dev-inject/test/unit/fixtures/app/layout.js
new file mode 100644
index 00000000..9d9d0186
--- /dev/null
+++ b/qiming-dev-inject/test/unit/fixtures/app/layout.js
@@ -0,0 +1 @@
+export default function Layout() {}
\ No newline at end of file
diff --git a/qiming-dev-inject/test/unit/fixtures/index.html b/qiming-dev-inject/test/unit/fixtures/index.html
new file mode 100644
index 00000000..6c70bcfe
--- /dev/null
+++ b/qiming-dev-inject/test/unit/fixtures/index.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/qiming-dev-inject/test/unit/fixtures/pages/_document.js b/qiming-dev-inject/test/unit/fixtures/pages/_document.js
new file mode 100644
index 00000000..8b7222cd
--- /dev/null
+++ b/qiming-dev-inject/test/unit/fixtures/pages/_document.js
@@ -0,0 +1 @@
+export default function Document() {}
\ No newline at end of file
diff --git a/qiming-dev-inject/test/unit/fixtures/test.html b/qiming-dev-inject/test/unit/fixtures/test.html
new file mode 100644
index 00000000..6c70bcfe
--- /dev/null
+++ b/qiming-dev-inject/test/unit/fixtures/test.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/qiming-dev-inject/test/unit/parseArgs.test.js b/qiming-dev-inject/test/unit/parseArgs.test.js
new file mode 100644
index 00000000..a0399922
--- /dev/null
+++ b/qiming-dev-inject/test/unit/parseArgs.test.js
@@ -0,0 +1,161 @@
+import { test, describe } from 'node:test';
+import assert from 'node:assert';
+
+// 导入要测试的模块
+import { parseArgs } from '../../lib/args.js';
+
+describe('CLI 参数解析', () => {
+ describe('基本命令解析', () => {
+ test('应该解析 install 命令', () => {
+ const result = parseArgs(['install', '--remote', '/scripts/test.js']);
+ assert.strictEqual(result.command, 'install');
+ assert.strictEqual(result.options.remote, '/scripts/test.js');
+ });
+
+ test('应该解析 uninstall 命令', () => {
+ const result = parseArgs(['uninstall']);
+ assert.strictEqual(result.command, 'uninstall');
+ });
+
+ test('应该处理空参数', () => {
+ const result = parseArgs([]);
+ assert.strictEqual(result.command, null);
+ assert.deepStrictEqual(result.options, {});
+ });
+ });
+
+ describe('--remote 参数解析', () => {
+ test('应该解析 --remote=value 格式', () => {
+ const result = parseArgs(['install', '--remote=http://localhost:9000/monitor.js']);
+ assert.strictEqual(result.options.remote, 'http://localhost:9000/monitor.js');
+ });
+
+ test('应该解析 --remote value 格式', () => {
+ const result = parseArgs(['install', '--remote', '/scripts/dev-monitor.js']);
+ assert.strictEqual(result.options.remote, '/scripts/dev-monitor.js');
+ });
+
+ test('应该处理多个 --remote 参数(使用最后一个)', () => {
+ const result = parseArgs(['install', '--remote', '/first.js', '--remote', '/second.js']);
+ assert.strictEqual(result.options.remote, '/second.js');
+ });
+ });
+
+ describe('--file 参数解析', () => {
+ test('应该解析 --file=value 格式', () => {
+ const result = parseArgs(['install', '--remote=/test.js', '--file=public/index.html']);
+ assert.strictEqual(result.options.file, 'public/index.html');
+ });
+
+ test('应该解析 --file value 格式', () => {
+ const result = parseArgs(['install', '--remote=/test.js', '--file', './public/index.html']);
+ assert.strictEqual(result.options.file, './public/index.html');
+ });
+ });
+
+ describe('--framework 参数解析', () => {
+ test('应该解析 --framework 选项', () => {
+ const result = parseArgs(['install', '--remote=/test.js', '--framework']);
+ assert.strictEqual(result.options.framework, true);
+ });
+
+ test('应该解析 -f 简写选项', () => {
+ const result = parseArgs(['install', '--remote=/test.js', '-f']);
+ assert.strictEqual(result.options.framework, true);
+ });
+ });
+
+ describe('--dry-run 参数解析', () => {
+ test('应该解析 --dry-run 选项', () => {
+ const result = parseArgs(['install', '--remote=/test.js', '--dry-run']);
+ assert.strictEqual(result.options.dryRun, true);
+ });
+ });
+
+ describe('--verbose 参数解析', () => {
+ test('应该解析 --verbose 选项', () => {
+ const result = parseArgs(['install', '--remote=/test.js', '--verbose']);
+ assert.strictEqual(result.options.verbose, true);
+ });
+ });
+
+ describe('复合选项解析', () => {
+ test('应该解析多个选项', () => {
+ const result = parseArgs([
+ 'install',
+ '--remote', 'http://localhost:9000/monitor.js',
+ '--framework',
+ '--dry-run',
+ '--verbose',
+ '--file', './test.html'
+ ]);
+
+ assert.strictEqual(result.command, 'install');
+ assert.strictEqual(result.options.remote, 'http://localhost:9000/monitor.js');
+ assert.strictEqual(result.options.framework, true);
+ assert.strictEqual(result.options.dryRun, true);
+ assert.strictEqual(result.options.verbose, true);
+ assert.strictEqual(result.options.file, './test.html');
+ });
+ });
+
+ describe('错误处理', () => {
+ test('install 命令缺少 --remote 参数应该抛出错误', () => {
+ assert.throws(() => {
+ parseArgs(['install']);
+ }, /install 命令需要 --remote 参数/);
+ });
+
+ test('uninstall 命令不需要 --remote 参数', () => {
+ const result = parseArgs(['uninstall']);
+ assert.strictEqual(result.command, 'uninstall');
+ assert.deepStrictEqual(result.options, {});
+ });
+ });
+
+ describe('无效参数处理', () => {
+ test('应该忽略未知参数', () => {
+ // 使用 mock console.warn 来捕获警告
+ const originalWarn = console.warn;
+ let warnCalled = false;
+ console.warn = () => { warnCalled = true; };
+
+ const result = parseArgs(['install', '--remote=/test.js', '--unknown-option']);
+
+ console.warn = originalWarn;
+
+ // 应该仍然正确解析已知参数
+ assert.strictEqual(result.options.remote, '/test.js');
+ // 应该标记未知参数被忽略(通过 console.warn)
+ assert.ok(warnCalled); // 如果有警告输出则测试通过
+ });
+ });
+
+ describe('参数顺序无关性', () => {
+ test('应该正确处理不同顺序的参数', () => {
+ const result1 = parseArgs(['install', '--remote=/test.js', '--framework', '--dry-run']);
+ const result2 = parseArgs(['install', '--dry-run', '--framework', '--remote=/test.js']);
+
+ assert.deepStrictEqual(result1.options, result2.options);
+ });
+ });
+
+ describe('边界情况', () => {
+ test('应该处理空的 --remote 值', () => {
+ const result = parseArgs(['install', '--remote', '']);
+ assert.strictEqual(result.options.remote, '');
+ });
+
+ test('应该处理包含特殊字符的路径', () => {
+ const complexPath = '/path with spaces/file.js';
+ const result = parseArgs(['install', '--remote', complexPath]);
+ assert.strictEqual(result.options.remote, complexPath);
+ });
+
+ test('应该处理长参数值', () => {
+ const longValue = 'a'.repeat(1000);
+ const result = parseArgs(['install', '--remote', longValue]);
+ assert.strictEqual(result.options.remote, longValue);
+ });
+ });
+});
diff --git a/qiming-dev-inject/test/unit/utils.test.js b/qiming-dev-inject/test/unit/utils.test.js
new file mode 100644
index 00000000..0e0f768d
--- /dev/null
+++ b/qiming-dev-inject/test/unit/utils.test.js
@@ -0,0 +1,304 @@
+import { test, describe } from 'node:test';
+import assert from 'node:assert';
+import { existsSync, mkdirSync, writeFileSync, rmSync } from 'fs';
+import { join } from 'path';
+import { fileURLToPath } from 'url';
+import { dirname } from 'path';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+
+// 导入要测试的模块
+import {
+ parseRemoteType,
+ generateScriptTag,
+ lookupFiles,
+ injectScriptToHtml,
+ removeInjectedScripts
+} from '../../lib/utils.js';
+
+describe('工具函数', () => {
+ const testDir = join(__dirname, 'fixtures');
+
+ test.before(() => {
+ if (!existsSync(testDir)) {
+ mkdirSync(testDir, { recursive: true });
+ }
+ });
+
+ test.after(() => {
+ const filesToClean = [
+ 'test1.html',
+ 'test2.html',
+ 'subdir/test3.html',
+ 'subdir/test4.html'
+ ];
+
+ filesToClean.forEach(file => {
+ const filePath = join(testDir, file);
+ if (existsSync(filePath)) {
+ rmSync(filePath);
+ }
+ });
+
+ const subDir = join(testDir, 'subdir');
+ if (existsSync(subDir)) {
+ rmSync(subDir, { recursive: true });
+ }
+ });
+
+ describe('parseRemoteType', () => {
+ test('应该识别 HTTP URL', () => {
+ assert.strictEqual(parseRemoteType('http://localhost:9000/monitor.js'), 'url');
+ assert.strictEqual(parseRemoteType('http://example.com/script.js'), 'url');
+ });
+
+ test('应该识别 HTTPS URL', () => {
+ assert.strictEqual(parseRemoteType('https://cdn.example.com/monitor.js'), 'url');
+ assert.strictEqual(parseRemoteType('https://localhost:3000/dev-monitor.js'), 'url');
+ });
+
+ test('应该识别绝对路径', () => {
+ assert.strictEqual(parseRemoteType('/scripts/monitor.js'), 'absolute-path');
+ assert.strictEqual(parseRemoteType('/assets/dev-monitor.js'), 'absolute-path');
+ assert.strictEqual(parseRemoteType('/path/to/script.js'), 'absolute-path');
+ });
+
+ test('应该处理相对路径', () => {
+ try {
+ parseRemoteType('./script.js');
+ assert.fail('应该抛出错误');
+ } catch (error) {
+ assert.match(error.message, /不支持的远程路径格式/);
+ }
+ });
+
+ test('应该处理空字符串', () => {
+ try {
+ parseRemoteType('');
+ assert.fail('应该抛出错误');
+ } catch (error) {
+ assert.match(error.message, /不支持的远程路径格式/);
+ }
+ });
+ });
+
+ describe('generateScriptTag', () => {
+ test('应该生成 URL 类型的脚本标签', () => {
+ const tag = generateScriptTag('http://localhost:9000/monitor.js', 'url');
+ assert.strictEqual(tag, '');
+ });
+
+ test('应该生成绝对路径类型的脚本标签', () => {
+ const tag = generateScriptTag('/scripts/monitor.js', 'absolute-path');
+ assert.strictEqual(tag, '');
+ });
+
+ test('应该处理不同的路径格式', () => {
+ const testCases = [
+ 'https://cdn.example.com/monitor.js',
+ 'http://localhost:3000/dev-monitor.js',
+ '/assets/scripts/dev-monitor.js',
+ '/scripts/monitor.js'
+ ];
+
+ testCases.forEach(path => {
+ const type = parseRemoteType(path);
+ const tag = generateScriptTag(path, type);
+ assert.match(tag, /^';
+ const result = injectScriptToHtml(html, scriptTag);
+
+ assert.match(result, /';
+ const result = injectScriptToHtml(html, scriptTag);
+
+ assert.match(result, //);
+ });
+
+ test('应该处理没有 标签的情况', () => {
+ const html = `Testcontent`;
+ const scriptTag = '';
+ const result = injectScriptToHtml(html, scriptTag);
+
+ assert.match(result, /';
+ const result = injectScriptToHtml(html, scriptTag);
+
+ assert.match(result, /';
+ const result = injectScriptToHtml(html, scriptTag);
+
+ assert.match(result, /
+
+
+ Hello World
+
+