feat(build): 添加扩展打包脚本和文档

添加自动打包脚本 package.js,支持从 dist 目录生成发布包
新增 PACKAGING.md 文档说明打包流程
更新 .gitignore 忽略 releases 目录
优化 manifest.json 主机权限配置
新增 CHANGELOG-PERMISSIONS.md 记录权限变更
这个提交包含在:
Zain 2025-11-08 06:33:43 +08:00
父节点 c6b48167bb
当前提交 f32329ce19
共有 6 个文件被更改,包括 885 次插入 和 1 次删除

4
.gitignore vendored
查看文件

@ -99,6 +99,10 @@ temp/
extension-temp/
crx/
# Release packages (generated by npm run package)
releases/
*.zip
# Test files
test-results/
playwright-report/

236
CHANGELOG-PERMISSIONS.md 普通文件
查看文件

@ -0,0 +1,236 @@
# 🔒 权限配置优化记录
## 📅 优化时间
2025-11-07
## 🎯 优化目标
提高 Chrome Web Store 审核通过率,符合最小权限原则。
---
## ⚠️ 优化前配置(高风险)
### 问题:使用了广泛的主机权限
```json
"host_permissions": [
"<all_urls>"
]
```
### 风险分析
| 风险级别 | 说明 |
|---------|------|
| 🔴 **审核风险** | `<all_urls>` 是高敏感权限,会触发严格审核 |
| 🔴 **信任问题** | 用户看到"可访问所有网站"会担心隐私 |
| 🔴 **政策违规** | 不符合 Chrome Web Store 最小权限原则 |
---
## ✅ 优化后配置(低风险)
### 解决方案:明确列出需要的域名
```json
"host_permissions": [
"https://www.baidu.com/*",
"https://www.google.com/*",
"https://www.bing.com/*",
"https://www.sogou.com/*",
"https://www.so.com/*"
]
```
### 优势分析
| 优势 | 说明 |
|------|------|
| ✅ **审核友好** | 明确的权限范围,降低审核难度 |
| ✅ **用户信任** | 用户清楚知道扩展只在 5 个搜索引擎工作 |
| ✅ **政策合规** | 符合最小权限原则,透明化权限申请 |
| ✅ **功能无损** | 完全满足现有功能需求 |
---
## 🔍 技术验证
### 配置一致性检查
✅ **host_permissions 与 content_scripts.matches 完全一致**
```json
// host_permissions(主机权限)
"host_permissions": [
"https://www.baidu.com/*",
"https://www.google.com/*",
"https://www.bing.com/*",
"https://www.sogou.com/*",
"https://www.so.com/*"
]
// content_scripts.matches(内容脚本匹配)
"matches": [
"https://www.baidu.com/*",
"https://www.google.com/*",
"https://www.bing.com/*",
"https://www.sogou.com/*",
"https://www.so.com/*"
]
```
### 权限统计
| 配置项 | 数量 | 说明 |
|--------|------|------|
| 基础权限 (permissions) | 4 | storage, activeTab, scripting, contextMenus |
| 主机权限 (host_permissions) | 5 | 5 个搜索引擎域名 |
| Content Scripts 域名 | 5 | 与主机权限一致 ✅ |
---
## 📦 打包验证
### 新版本包信息
```
文件名: releases/ssp-v1.0.0.zip
版本号: v1.0.0
文件大小: 101.59 KB
```
### ZIP 包内容验证
✅ `manifest.json` 已包含优化后的权限配置
✅ 所有域名列表一致
✅ JSON 格式正确
✅ 符合 Manifest V3 规范
---
## 🎯 Chrome Web Store 审核材料
### 更新后的主机权限理由
```
本扩展请求主机权限的理由:
1. 多搜索引擎支持
- 在百度、谷歌、必应、搜狗、360搜索 5 个主流搜索引擎上提供功能
- 为每个搜索引擎提供适配的高级语法生成
2. 功能实现
- 在搜索结果页面注入内容脚本
- 读取搜索框内容以实现"优化当前搜索"功能
- 将生成的搜索语法自动填入搜索框
3. 权限范围
- 仅限于明确列出的 5 个搜索引擎域名
- 不访问其他网站,不收集用户浏览数据
- 符合最小权限原则
此权限是扩展核心功能的必要条件。
```
---
## 📊 影响分析
### ✅ 正面影响
1. **审核通过率提升**
- 从高风险配置改为低风险配置
- 减少审核人员的疑虑和问询
2. **用户信任度提升**
- 权限申请更透明
- 用户清楚扩展的工作范围
3. **政策合规性**
- 符合 Chrome Web Store 最佳实践
- 遵循最小权限原则
### ⚠️ 注意事项
1. **未来扩展搜索引擎支持**
- 如需支持新搜索引擎,需更新 `host_permissions`
- 同时更新 `content_scripts.matches`
- 重新发布版本并说明权限变更原因
2. **版本更新流程**
- 添加新域名前评估必要性
- 在更新说明中解释权限变更
- 提前准备审核材料
---
## 🚀 后续维护
### 添加新搜索引擎的流程
1. **评估必要性**
- 确认用户需求
- 评估搜索引擎市场份额
2. **更新配置**
```json
// 在两个地方同时添加
"host_permissions": [
// 现有域名...
"https://www.new-search-engine.com/*" // 新增
],
"content_scripts": [{
"matches": [
// 现有域名...
"https://www.new-search-engine.com/*" // 新增
]
}]
```
3. **更新审核材料**
- 在权限理由中说明新增搜索引擎
- 更新"支持的搜索引擎数量"
- 准备详细的功能说明
4. **版本发布**
- 更新 `package.json` 版本号
- 在 CHANGELOG.md 记录变更
- 重新打包发布
---
## ✅ 最佳实践总结
### Chrome Web Store 审核最佳实践
1. **最小权限原则**
- ✅ 只申请必要的权限
- ✅ 使用具体域名代替通配符
- ✅ 避免 `<all_urls>` 等广泛权限
2. **透明度原则**
- ✅ 清晰说明每个权限的用途
- ✅ 提供具体的使用场景
- ✅ 强调隐私保护措施
3. **一致性原则**
- ✅ 确保 `host_permissions` 与实际使用一致
- ✅ 与 `content_scripts.matches` 保持同步
- ✅ 权限理由与实际功能匹配
4. **用户信任原则**
- ✅ 让用户了解扩展的工作范围
- ✅ 说明数据处理方式(本地存储)
- ✅ 提供隐私政策链接
---
## 📝 相关文档
- [Chrome 扩展权限文档](https://developer.chrome.com/docs/extensions/mv3/declare_permissions/)
- [Chrome Web Store 政策](https://developer.chrome.com/docs/webstore/program_policies/)
- [最小权限原则](https://developer.chrome.com/docs/extensions/mv3/security/#permissions)
---
**SearchSyntax Pro** - 遵循最佳实践,保护用户隐私

258
PACKAGING.md 普通文件
查看文件

@ -0,0 +1,258 @@
# 📦 Chrome扩展打包指南
本文档说明如何将 SearchSyntax Pro 打包为可发布到 Chrome Web Store 的 ZIP 文件。
## 🚀 快速开始
### 方式一:完整打包流程(推荐)
```bash
npm run package
```
这个命令会:
1. 执行 TypeScript 编译
2. 运行 Vite 构建
3. 复制静态资源(manifest.json、icons、国际化文件)
4. 将 dist 目录打包为 ZIP 文件
### 方式二:仅打包(快速迭代)
如果你已经运行过 `npm run build`,可以直接打包:
```bash
npm run package:only
```
## 📁 输出文件
打包后会在项目根目录生成:
```
releases/
└── ssp-v{version}.zip
```
例如:`releases/ssp-v1.0.0.zip`
版本号自动从 `package.json` 读取。
## 🔍 打包内容
ZIP 包会包含 `dist/` 目录的所有必要文件:
- ✅ `manifest.json` - Chrome 扩展配置
- ✅ `background.js` - Service Worker(后台脚本)
- ✅ `popup.js` - 弹窗界面脚本
- ✅ `options.js` - 设置页面脚本
- ✅ `content.js` - 内容脚本
- ✅ `icons/` - 所有尺寸的图标(16x16, 32x32, 48x48, 128x128)
- ✅ `_locales/` - 国际化资源文件
- ✅ 其他构建产物(CSS、资源文件等)
### 自动排除的文件
打包脚本会自动排除以下文件:
- ❌ `.DS_Store` - macOS 系统文件
- ❌ `*.backup`, `*.bak` - 备份文件
- ❌ `*.tmp` - 临时文件
- ❌ `__MACOSX/` - macOS 压缩产物
- ❌ `.git/` - Git 版本控制文件
- ❌ `node_modules/` - 依赖文件
## 📊 打包信息
打包完成后,脚本会显示:
- ✅ 文件名称和路径
- ✅ 版本号
- ✅ 文件大小(压缩后)
- ✅ 文件质量评估(<5MB优秀)
## 🌐 上传到 Chrome Web Store
### 前提条件
1. **开发者账户**:注册 Chrome Web Store 开发者账户($5 一次性费用)
2. **准备材料**:
- 扩展 ZIP 包(通过 `npm run package` 生成)
- 至少 1 张截图(1280x800 或 640x400)
- 详细描述和简短描述
- 隐私政策 URL(如果使用权限)
### 上传步骤
1. **访问开发者控制台**
```
https://chrome.google.com/webstore/devconsole
```
2. **创建新项**
- 点击"新增项"
- 上传生成的 ZIP 文件(`releases/ssp-v1.0.0.zip`)
3. **填写商店列表信息**
- 详细描述(推荐 800-1000 字)
- 简短描述(<132 字符)
- 上传截图(至少 1 张,推荐 3-5 张)
- 选择分类:生产力工具
4. **配置隐私设置**
- 说明权限用途
- 提供隐私政策链接
- 说明数据处理方式
5. **提交审核**
- 检查所有必填项
- 点击"提交审核"
- 等待审核结果(通常 24-48 小时)
## 🔄 版本更新流程
### 1. 更新版本号
在 `package.json` 中更新版本号:
```json
{
"version": "1.1.0"
}
```
建议同步更新 `public/manifest.json` 的版本号(虽然打包脚本会使用 package.json 的版本)。
### 2. 构建和打包
```bash
npm run package
```
### 3. 上传新版本
- 在 Chrome Web Store 开发者控制台
- 选择现有扩展
- 上传新的 ZIP 包
- 填写更新说明
- 提交审核
## 🛠️ 故障排查
### 问题:找不到 zip 命令
**错误信息**:
```
❌ 未找到系统zip命令
```
**解决方案**:
- **macOS**:已预装 zip,无需操作
- **Linux**:
```bash
sudo apt-get install zip
```
- **Windows**:
- 安装 [Git for Windows](https://git-scm.com/download/win)(包含 Git Bash)
- 或使用 [WSL (Windows Subsystem for Linux)](https://docs.microsoft.com/en-us/windows/wsl/install)
### 问题:dist 目录不存在
**错误信息**:
```
❌ dist目录不存在
请先运行构建命令:npm run build
```
**解决方案**:
```bash
npm run build
```
### 问题:文件大小过大
**警告信息**:
```
⚠️ ZIP文件超过10MB,可能影响审核速度
```
**优化建议**:
1. 检查是否包含不必要的资源文件
2. 压缩图片资源
3. 移除未使用的依赖
4. 使用 Vite 构建优化
### 问题:版本号格式无效
**错误信息**:
```
❌ 无效的版本号格式
```
**解决方案**:
确保 `package.json` 中的版本号遵循语义化版本规范(SemVer):
- ✅ 正确:`1.0.0`, `1.2.3`, `2.0.0-beta.1`
- ❌ 错误:`v1.0`, `1.0`, `latest`
## 📝 最佳实践
### 1. 版本管理
- 遵循[语义化版本](https://semver.org/lang/zh-CN/)规范
- 主版本号(Major):不兼容的 API 修改
- 次版本号(Minor):向下兼容的功能新增
- 修订号(Patch):向下兼容的问题修正
### 2. 发布前检查清单
- [ ] 运行所有测试:`npm test`
- [ ] 代码质量检查:`npm run lint`
- [ ] 类型检查:`npm run type-check`
- [ ] 本地功能测试(加载 dist 到浏览器)
- [ ] 更新版本号
- [ ] 更新 CHANGELOG.md
- [ ] 创建 Git 标签
### 3. Git 工作流
```bash
# 1. 更新版本号
npm version patch # 或 minor, major
# 2. 提交更改
git add .
git commit -m "chore: release v1.0.1"
# 3. 创建标签
git tag v1.0.1
# 4. 推送到远程
git push origin main --tags
# 5. 打包发布
npm run package
```
### 4. 保留历史版本
`releases/` 目录已在 `.gitignore` 中排除,但建议:
- 在本地保留最近 3-5 个版本的 ZIP 包
- 使用 GitHub Releases 存档重要版本
- 记录每个版本的发布日期和审核通过日期
## 🔗 相关资源
- [Chrome 扩展开发者文档](https://developer.chrome.com/docs/extensions/)
- [Chrome Web Store 政策](https://developer.chrome.com/docs/webstore/program_policies/)
- [发布检查清单](https://developer.chrome.com/docs/webstore/publish/)
- [语义化版本规范](https://semver.org/lang/zh-CN/)
## 💡 提示
- 首次发布审核可能需要更长时间(1-7 天)
- 后续更新通常更快(24-48 小时)
- 保持良好的代码质量和文档可以加速审核
- 及时响应审核人员的问题和反馈
---
**SearchSyntax Pro** - 让高级搜索变得简单易用

查看文件

@ -6,6 +6,8 @@
"scripts": {
"dev": "vite",
"build": "tsc && vite build && node scripts/post-build.js",
"package": "npm run build && node scripts/package.js",
"package:only": "node scripts/package.js",
"preview": "vite preview",
"test": "jest",
"test:e2e": "playwright test",

查看文件

@ -13,7 +13,11 @@
],
"host_permissions": [
"<all_urls>"
"https://www.baidu.com/*",
"https://www.google.com/*",
"https://www.bing.com/*",
"https://www.sogou.com/*",
"https://www.so.com/*"
],
"background": {

380
scripts/package.js 普通文件
查看文件

@ -0,0 +1,380 @@
/**
* Chrome扩展自动打包脚本
*
* 功能:
* - 自动从package.json读取版本号
* - 将dist目录打包为ZIP格式
* - 生成带版本号的发布包:releases/ssp-v{version}.zip
* - 提供详细的错误提示和成功信息
*
* 架构设计:
* - 单一职责原则:每个函数只做一件事
* - 错误优先:完善的错误处理和用户提示
* - 零依赖:仅使用Node.js内置模块
*
* @author SearchSyntax Pro Team
*/
import { execSync } from 'child_process';
import { existsSync, mkdirSync, readFileSync, statSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
// ============================================================================
// 配置常量
// ============================================================================
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const rootDir = join(__dirname, '..');
const CONFIG = {
distDir: join(rootDir, 'dist'),
releasesDir: join(rootDir, 'releases'),
packageName: 'ssp',
packageJsonPath: join(rootDir, 'package.json'),
};
// 颜色输出(在终端中更友好)
const colors = {
reset: '\x1b[0m',
bright: '\x1b[1m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
cyan: '\x1b[36m',
};
// ============================================================================
// 工具函数
// ============================================================================
/**
* 格式化文件大小为人类可读格式
* @param {number} bytes - 字节数
* @returns {string} 格式化后的大小(如 "1.23 MB")
*/
function formatFileSize(bytes) {
const units = ['B', 'KB', 'MB', 'GB'];
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return `${size.toFixed(2)} ${units[unitIndex]}`;
}
/**
* 打印带颜色的消息
* @param {string} message - 消息内容
* @param {string} color - 颜色代码
*/
function log(message, color = colors.reset) {
console.log(`${color}${message}${colors.reset}`);
}
/**
* 打印成功消息
*/
function logSuccess(message) {
log(`✅ ${message}`, colors.green);
}
/**
* 打印错误消息
*/
function logError(message) {
log(`❌ ${message}`, colors.red);
}
/**
* 打印警告消息
*/
function logWarning(message) {
log(`⚠️ ${message}`, colors.yellow);
}
/**
* 打印信息消息
*/
function logInfo(message) {
log(`ℹ️ ${message}`, colors.cyan);
}
/**
* 打印标题
*/
function logHeader(message) {
log(`\n${'='.repeat(60)}`, colors.blue);
log(` ${message}`, colors.bright + colors.blue);
log(`${'='.repeat(60)}\n`, colors.blue);
}
// ============================================================================
// 环境验证层
// ============================================================================
/**
* 检查系统是否安装了zip命令
* @throws {Error} 如果zip命令不存在
*/
function checkZipCommand() {
try {
execSync('which zip', { stdio: 'ignore' });
logSuccess('检测到系统zip命令');
} catch (error) {
logError('未找到系统zip命令');
console.log('\n请安装zip工具:');
console.log(' macOS: 已预装');
console.log(' Linux: sudo apt-get install zip');
console.log(' Windows: 安装 Git Bash 或使用 WSL\n');
throw new Error('缺少zip命令');
}
}
/**
* 检查dist目录是否存在
* @throws {Error} 如果dist目录不存在
*/
function checkDistDirectory() {
if (!existsSync(CONFIG.distDir)) {
logError('dist目录不存在');
console.log('\n请先运行构建命令:');
console.log(' npm run build\n');
throw new Error('缺少dist目录');
}
logSuccess(`找到构建目录: ${CONFIG.distDir}`);
}
// ============================================================================
// 版本管理层
// ============================================================================
/**
* 从package.json读取版本号
* @returns {string} 版本号(如 "1.0.0")
* @throws {Error} 如果读取失败或版本号无效
*/
function getVersion() {
try {
const packageJson = JSON.parse(
readFileSync(CONFIG.packageJsonPath, 'utf-8')
);
const version = packageJson.version;
if (!version) {
throw new Error('package.json中未找到version字段');
}
// 验证版本号格式(语义化版本)
if (!/^\d+\.\d+\.\d+(-[\w.]+)?$/.test(version)) {
throw new Error(`无效的版本号格式: ${version}`);
}
logSuccess(`读取版本号: v${version}`);
return version;
} catch (error) {
logError(`读取版本号失败: ${error.message}`);
throw error;
}
}
// ============================================================================
// 文件系统层
// ============================================================================
/**
* 创建releases目录(如果不存在)
*/
function prepareReleasesDir() {
if (!existsSync(CONFIG.releasesDir)) {
mkdirSync(CONFIG.releasesDir, { recursive: true });
logSuccess(`创建输出目录: ${CONFIG.releasesDir}`);
} else {
logInfo(`使用现有输出目录: ${CONFIG.releasesDir}`);
}
}
/**
* 检查同名包文件是否已存在
* @param {string} zipFileName - ZIP文件名
* @returns {boolean} 是否存在
*/
function checkExistingPackage(zipFileName) {
const zipPath = join(CONFIG.releasesDir, zipFileName);
if (existsSync(zipPath)) {
const stats = statSync(zipPath);
const size = formatFileSize(stats.size);
logWarning(`发现同名文件: ${zipFileName} (${size})`);
logInfo('将覆盖现有文件');
return true;
}
return false;
}
// ============================================================================
// 打包执行层
// ============================================================================
/**
* 执行ZIP打包命令
* @param {string} version - 版本号
* @returns {string} 生成的ZIP文件路径
* @throws {Error} 如果打包失败
*/
function createZipPackage(version) {
const zipFileName = `${CONFIG.packageName}-v${version}.zip`;
const zipPath = join(CONFIG.releasesDir, zipFileName);
logInfo('开始打包...');
try {
// 切换到dist目录并打包所有内容
// -r: 递归打包目录
// -q: 安静模式(不显示进度)
// -9: 最高压缩率
// -x: 排除不必要的文件
const excludePatterns = [
'*.DS_Store', // macOS系统文件
'__MACOSX/*', // macOS压缩产物
'*.backup', // 备份文件
'*.bak', // 备份文件
'*.tmp', // 临时文件
'.git/*', // Git文件
'node_modules/*', // 依赖文件(不应该在dist中)
].map(p => `"${p}"`).join(' ');
const command = `cd "${CONFIG.distDir}" && zip -r -q -9 "${zipPath}" . -x ${excludePatterns}`;
execSync(command, { stdio: 'inherit' });
// 验证ZIP文件是否成功创建
if (!existsSync(zipPath)) {
throw new Error('ZIP文件创建失败');
}
return zipPath;
} catch (error) {
logError(`打包失败: ${error.message}`);
throw error;
}
}
/**
* 计算并返回文件大小信息
* @param {string} filePath - 文件路径
* @returns {Object} 包含size和formattedSize的对象
*/
function getFileInfo(filePath) {
const stats = statSync(filePath);
return {
size: stats.size,
formattedSize: formatFileSize(stats.size),
};
}
// ============================================================================
// 报告层
// ============================================================================
/**
* 显示打包成功信息
* @param {string} zipPath - ZIP文件路径
* @param {string} version - 版本号
*/
function displaySuccess(zipPath, version) {
const fileInfo = getFileInfo(zipPath);
const fileName = join('releases', `${CONFIG.packageName}-v${version}.zip`);
logHeader('打包成功!');
console.log(`${colors.bright}📦 扩展包信息${colors.reset}`);
console.log(` 文件名称: ${colors.cyan}${fileName}${colors.reset}`);
console.log(` 版本号: ${colors.green}v${version}${colors.reset}`);
console.log(` 文件大小: ${colors.yellow}${fileInfo.formattedSize}${colors.reset}`);
console.log(` 完整路径: ${colors.blue}${zipPath}${colors.reset}`);
console.log(`\n${colors.bright}🚀 下一步操作${colors.reset}`);
console.log(' 1. 访问 Chrome Web Store Developer Console');
console.log(' https://chrome.google.com/webstore/devconsole');
console.log(' 2. 上传生成的ZIP文件');
console.log(' 3. 填写商店列表信息');
console.log(' 4. 提交审核\n');
// 文件大小警告
if (fileInfo.size > 10 * 1024 * 1024) {
logWarning('ZIP文件超过10MB,可能影响审核速度');
logInfo('建议优化资源文件大小');
} else if (fileInfo.size > 5 * 1024 * 1024) {
logInfo('ZIP文件大小适中(5-10MB)');
} else {
logSuccess('ZIP文件大小优秀(<5MB)');
}
}
/**
* 显示错误信息并退出
* @param {Error} error - 错误对象
*/
function displayError(error) {
logHeader('打包失败');
logError(error.message);
if (error.stack) {
console.log(`\n${colors.yellow}详细错误信息:${colors.reset}`);
console.log(error.stack);
}
console.log(`\n${colors.cyan}需要帮助?${colors.reset}`);
console.log(' - 查看项目文档: README.md');
console.log(' - 提交Issue: https://github.com/lhly/search-syntax-pro/issues\n');
process.exit(1);
}
// ============================================================================
// 主流程
// ============================================================================
/**
* 主函数:执行完整的打包流程
*/
async function main() {
try {
logHeader('🎁 Chrome扩展自动打包工具');
// 第1步:环境验证
console.log(`${colors.bright}第1步: 环境验证${colors.reset}`);
checkZipCommand();
checkDistDirectory();
// 第2步:读取版本号
console.log(`\n${colors.bright}第2步: 版本管理${colors.reset}`);
const version = getVersion();
// 第3步:准备输出目录
console.log(`\n${colors.bright}第3步: 文件系统准备${colors.reset}`);
prepareReleasesDir();
const zipFileName = `${CONFIG.packageName}-v${version}.zip`;
checkExistingPackage(zipFileName);
// 第4步:执行打包
console.log(`\n${colors.bright}第4步: 执行打包${colors.reset}`);
const zipPath = createZipPackage(version);
// 第5步:显示结果
logSuccess('打包完成');
displaySuccess(zipPath, version);
} catch (error) {
displayError(error);
}
}
// 执行主流程
main();