完全开源:密语 CipherTalk完全开源,但您一就要遵循本项目的开源协议进行使用。——一鲸落,万物生!

This commit is contained in:
ILoveBingLu
2026-01-25 07:11:13 +08:00
parent 33282a380e
commit 3a9d421f47
96 changed files with 25171 additions and 596 deletions
+336
View File
@@ -0,0 +1,336 @@
# 📦 自动发布脚本使用说明
## 🚀 快速开始
### 发布新版本(3 步完成)
```bash
# 1. 修改 package.json 中的版本号
# "version": "2.0.4"
# 2. 提交所有更改
git add .
git commit -m "release: v2.0.4"
# 3. 运行发布脚本
npm run tuisong
```
就这么简单!脚本会自动:
- ✅ 检查是否有未提交的更改(有则报错)
- ✅ 读取 `package.json` 中的版本号
- ✅ 推送到 GitHub
- ✅ 创建并推送版本标签(如 `v2.0.4`
- ✅ 触发自动构建和发布
---
## 📝 使用方法
```bash
npm run tuisong
```
**前提条件:**
- ✅ 所有更改已提交(`git commit`
-`package.json` 中的版本号已更新
**脚本会做什么:**
1. 检查是否有未提交的更改(有则退出)
2. 显示待推送的提交
3. 推送到 GitHub
4. 创建版本标签(如 `v2.0.4`
5. 推送标签到 GitHub
---
## 🎯 完整发布流程
### 步骤 1:修改版本号
编辑 `package.json`
```json
{
"name": "ciphertalk",
"version": "2.0.4", // 修改这里
...
}
```
**版本号规范:**
- **patch (x.y.Z)** - 修复 bug`2.0.3``2.0.4`
- **minor (x.Y.z)** - 新增功能:`2.0.3``2.1.0`
- **major (X.y.z)** - 重大更新:`2.0.3``3.0.0`
### 步骤 2:提交更改
```bash
git add .
git commit -m "release: v2.0.4"
```
### 步骤 3:推送发布
```bash
npm run tuisong
```
脚本会显示:
```
================================
密语 - 自动发布脚本
================================
📌 当前版本: v2.0.4
📝 待推送的提交:
abc1234 release: v2.0.4
[1/2] 🚀 推送到 GitHub...
✓ 推送成功
[2/2] 🏷️ 创建并推送标签 v2.0.4...
✓ 标签创建成功
================================
✅ 发布流程已启动!
================================
📦 版本: v2.0.4
🔗 查看构建进度:
https://github.com/JiQingzhe2004/ciphertalk/actions
🔗 发布完成后访问:
https://github.com/JiQingzhe2004/ciphertalk/releases/tag/v2.0.4
⏱️ 预计 10-15 分钟后构建完成
```
### 步骤 4:等待构建完成
GitHub Actions 会自动:
1. 安装依赖
2. 重新编译原生模块
3. 构建美化安装包
4. 创建 GitHub Release
5. 上传到 Cloudflare R2
6. 上传 `CipherTalk-2.0.4-Setup.exe`
---
## 🎨 使用场景示例
### 场景 1:修复 bug
```bash
# 1. 修改代码
# 2. 更新版本号: 2.0.3 → 2.0.4
# 3. 提交
git add .
git commit -m "fix: 修复表情包显示问题"
# 4. 发布
npm run tuisong
```
### 场景 2:添加新功能
```bash
# 1. 开发新功能
# 2. 更新版本号: 2.0.3 → 2.1.0
# 3. 提交
git add .
git commit -m "feat: 添加语音转文字功能"
# 4. 发布
npm run tuisong
```
### 场景 3:重大更新
```bash
# 1. 重构代码
# 2. 更新版本号: 2.0.3 → 3.0.0
# 3. 提交
git add .
git commit -m "feat!: 全新 UI 设计"
# 4. 发布
npm run tuisong
```
---
## 🔧 其他构建脚本
### 完整构建(生产环境)
```bash
npm run build:pro
```
包含:
- ✅ 更新 README 版本号
- ✅ TypeScript 编译
- ✅ Vite 构建前端
- ✅ Electron 打包
- ✅ 生成美化安装包
- ✅ 更新 latest.yml
### 仅构建外壳(测试用)
```bash
node scripts/build-shell-only.js
```
用于快速测试安装程序界面。
---
## 🤖 GitHub Actions 自动化
推送到 `main` 分支时自动触发:
1. 📦 安装依赖
2. 🔨 重新编译原生模块
3. 🏗️ 构建应用程序(`npm run build:pro`
4. 📊 获取版本号(从 `package.json`
5. 🎉 创建 GitHub Release(标签:`v2.0.4`
6. ☁️ 上传到 Cloudflare R2(自动删除旧版本)
7. 📤 上传构建产物到 GitHub
**查看构建状态:**
https://github.com/JiQingzhe2004/ciphertalk/actions
**查看发布版本:**
https://github.com/JiQingzhe2004/ciphertalk/releases
---
## ⚙️ GitHub Secrets 配置
需要在 GitHub 仓库设置中配置以下 Secrets:
### Cloudflare R2 配置
1. 进入 GitHub 仓库 → Settings → Secrets and variables → Actions
2. 点击 "New repository secret" 添加以下密钥:
| Secret 名称 | 说明 | 示例值 |
|------------|------|--------|
| `R2_ACCOUNT_ID` | R2 账户 ID | `bf9d655d15b24e8636ef9e61c137785b` |
| `R2_BUCKET_NAME` | R2 存储桶名称 | `miyu` |
| `R2_ACCESS_KEY_ID` | R2 访问密钥 ID | `3c49eaabd4b1a28f1d6a4eb642942ee7` |
| `R2_SECRET_ACCESS_KEY` | R2 桶密访问密钥 | `••••••••••••••••••••••••••••••••` |
### 邮件通知配置(可选)
如果需要在构建完成后收到邮件通知,添加以下 Secrets:
| Secret 名称 | 说明 | 示例值 |
|------------|------|--------|
| `MAIL_USERNAME` | 发件邮箱(Gmail | `your-email@gmail.com` |
| `MAIL_PASSWORD` | Gmail 应用专用密码 | `abcd efgh ijkl mnop` |
| `MAIL_TO` | 收件邮箱 | `your-email@gmail.com` |
**如何获取 Gmail 应用专用密码:**
1. 登录 [Google 账户](https://myaccount.google.com/)
2. 进入 **安全性****两步验证**(需要先启用)
3. 进入 **应用专用密码**
4. 选择 **邮件****Windows 计算机**
5. 点击 **生成**,复制 16 位密码(格式:`abcd efgh ijkl mnop`
6. 将密码添加到 GitHub Secrets 的 `MAIL_PASSWORD`
**邮件通知功能:**
- ✅ 构建成功时发送邮件(包含下载链接)
- ❌ 构建失败时发送邮件(包含错误日志链接)
- 📧 邮件发送到你的 GitHub 注册邮箱(或指定邮箱)
### 如何获取 R2 凭证
从你的截图中可以看到:
- **账户 ID**:在 Cloudflare R2 页面顶部显示
- **存储桶名称**:你创建的存储桶名称
- **访问密钥 ID**:在 R2 API 令牌页面显示
- **桶密访问密钥**:创建 API 令牌时显示(只显示一次,需要保存)
### R2 上传规则
- ✅ 自动上传 `CipherTalk-{版本号}-Setup.exe`
- ✅ 自动上传 `latest.yml`(用于自动更新)
- ✅ 自动删除旧版本的安装包(保留最新版本)
- ❌ 不上传 Core 版本(`*-Core-Setup.exe`
- ️ 如果存储桶为空,跳过删除步骤
---
## 🔧 故障排查
### 问题 1:检测到未提交的更改
**错误:**
```
❌ 检测到未提交的更改:
M package.json
M src/App.tsx
请先提交所有更改后再运行此脚本
```
**解决:**
```bash
# 提交所有更改
git add .
git commit -m "你的提交信息"
# 然后运行脚本
npm run tuisong
```
### 问题 2:推送失败
**错误:**
```
❌ 推送失败
请检查网络连接和 Git 配置
```
**解决:**
- 检查网络连接
- 检查 Git 配置
- 确认 GitHub 账号已登录
### 问题 3:标签已存在
**提示:**
```
⚠️ 标签 v2.0.4 已存在,跳过创建
```
**说明:**
- 这是正常提示,不影响推送
- 如果需要重新创建标签,先删除远程标签:
```bash
git push origin :refs/tags/v2.0.4
git tag -d v2.0.4
```
---
## 💡 提示
1. **推送前先测试** - 确保代码可以正常运行
2. **遵循版本号规范** - 便于版本管理([语义化版本](https://semver.org/lang/zh-CN/)
3. **写清楚提交信息** - 方便用户了解更新内容
4. **等待构建完成** - 大约 10-15 分钟
---
## 🔗 相关链接
- [GitHub Actions 工作流](../.github/workflows/build-release.yml)
- [语义化版本规范](https://semver.org/lang/zh-CN/)
- [Git 提交规范](https://www.conventionalcommits.org/zh-hans/)
+61
View File
@@ -0,0 +1,61 @@
const fs = require('fs')
const path = require('path')
const releaseDir = path.join(__dirname, '../release')
const ymlPath = path.join(releaseDir, 'latest.yml')
if (!fs.existsSync(ymlPath)) {
console.log('latest.yml 不存在,跳过')
process.exit(0)
}
// 读取 yml 内容
let content = fs.readFileSync(ymlPath, 'utf-8')
const lines = content.split('\n')
// 从 yml 中提取文件名
const match = content.match(/path:\s*(.+\.exe)/)
if (!match) {
console.log('未找到安装包文件名')
process.exit(0)
}
const exeName = match[1].trim()
const exePath = path.join(releaseDir, exeName)
if (!fs.existsSync(exePath)) {
console.log(`安装包不存在: ${exeName}`)
process.exit(0)
}
// 获取文件大小
const stats = fs.statSync(exePath)
const size = stats.size
// 找到 files 块内第一个 sha512 行,在其后插入 size
const newLines = []
let inFiles = false
let sizeAdded = false
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
newLines.push(line)
if (line.startsWith('files:')) {
inFiles = true
}
// 在 files 块内的第一个 sha512 后添加 size
if (inFiles && !sizeAdded && line.trim().startsWith('sha512:')) {
newLines.push(` size: ${size}`)
sizeAdded = true
inFiles = false
}
}
if (sizeAdded) {
fs.writeFileSync(ymlPath, newLines.join('\n'))
console.log(`已添加 size: ${size} 到 latest.yml`)
} else {
console.log('未找到合适位置插入 size')
}
+156
View File
@@ -0,0 +1,156 @@
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
//配置区
const PROJECT_ROOT = path.join(__dirname, '..');
const RELEASE_DIR = path.join(PROJECT_ROOT, 'release');
const INSTALLER_PRJ_DIR = path.join(PROJECT_ROOT, 'MyCoolInstaller');
const EMBEDDED_NAME = 'EmbeddedInstaller.exe';
function log(msg) {
console.log(`\n\x1b[36m[Build-Full]\x1b[0m ${msg}`);
}
function error(msg) {
console.error(`\n\x1b[31m[Error]\x1b[0m ${msg}`);
process.exit(1);
}
try {
// 1. 构建核心 Electron 应用 (包含 NSIS 打包 + UPX 优化)
log('🚀 Step 1: 构建核心 Electron 应用...');
execSync('npm run build', { stdio: 'inherit', cwd: PROJECT_ROOT });
// 2. 找到生成的 NSIS 安装包 (必须匹配当前版本)
log('🔍 Step 2: 寻找生成的 NSIS 安装包...');
if (!fs.existsSync(RELEASE_DIR)) error('Release 目录不存在,构建可能失败');
// 读取项目版本
const pkgPath = path.join(PROJECT_ROOT, 'package.json');
const pkgVersion = require(pkgPath).version;
const expectedName = `CipherTalk-${pkgVersion}-Setup.exe`;
const nsisPath = path.join(RELEASE_DIR, expectedName);
if (!fs.existsSync(nsisPath)) {
// 尝试模糊搜索作为备选(有时候 electron-builder 不带版本号?)
error(`未找到目标版本安装包: ${expectedName}\n请检查 package.json 版本号是否与生成产物一致。`);
}
const version = pkgVersion;
log(`✅ 找到安装包: ${expectedName} (v${version})`);
// 3. 复制到 WPF 工程目录准备嵌入
log('🚚 Step 3: 注入到安装器工程...');
const targetPayloadPath = path.join(INSTALLER_PRJ_DIR, EMBEDDED_NAME);
fs.copyFileSync(nsisPath, targetPayloadPath);
// 4. 编译 WPF 外壳 (需要系统中装有 .NET SDK)
log('🔨 Step 4: 编译 WPF 高颜值外壳...');
// 动态同步版本号:将 package.json 的 version 同步到 CSPROJ
// .NET 版本号遵循 Major.Minor.Build.Revision (4位),所以补个 .0
const netVersion = version.split('.').length === 3 ? `${version}.0` : version;
const csprojPath = path.join(INSTALLER_PRJ_DIR, 'MyCoolInstaller.csproj');
let csprojContent = fs.readFileSync(csprojPath, 'utf8');
csprojContent = csprojContent.replace(/<AssemblyVersion>.*<\/AssemblyVersion>/g, `<AssemblyVersion>${netVersion}</AssemblyVersion>`);
csprojContent = csprojContent.replace(/<FileVersion>.*<\/FileVersion>/g, `<FileVersion>${netVersion}</FileVersion>`);
fs.writeFileSync(csprojPath, csprojContent);
log(`️ 已更新安装器元数据版本为: ${netVersion}`);
// 指向具体的 csproj,避免多项目时的歧义
// 不使用 -o 参数,规避 Solution 构建时的路径冲突
const publishCmd = `dotnet publish "${csprojPath}" -c Release -r win-x64 --self-contained false -p:PublishSingleFile=true`;
try {
execSync(publishCmd, { stdio: 'inherit' });
} catch (e) {
error('WPF 编译失败。请确保安装了 .NET 8 SDK。');
}
// 5. 将最终产物移回 release 目录
log('🎁 Step 5: 输出最终产物...');
// 默认发布路径
const wpfOutput = path.join(INSTALLER_PRJ_DIR, 'bin', 'Release', 'net8.0-windows', 'win-x64', 'publish', 'MyCoolInstaller.exe');
if (!fs.existsSync(wpfOutput)) error(`WPF 产物未找到: ${wpfOutput}`);
// 记录原始大小用于日志
const originalSize = (fs.statSync(nsisPath).size / 1024 / 1024).toFixed(2);
// A. 备份原版 (改名为 Core-Setup)
const coreName = `CipherTalk-${version}-Core-Setup.exe`;
const corePath = path.join(RELEASE_DIR, coreName);
if (fs.existsSync(corePath)) fs.unlinkSync(corePath); // 覆盖旧备份
fs.renameSync(nsisPath, corePath);
log(`️ 原版安装包已重命名备份为: ${coreName}`);
// B. WPF 外壳上位 (使用标准 Setup 名字)
const finalName = `CipherTalk-${version}-Setup.exe`;
const finalPath = path.join(RELEASE_DIR, finalName);
// 复制前先检查占用
try {
if (fs.existsSync(finalPath)) fs.unlinkSync(finalPath);
fs.copyFileSync(wpfOutput, finalPath);
} catch (e) {
if (e.code === 'EBUSY') error(`目标文件被占用: ${finalPath}\n请关闭文件夹或程序后重试。`);
throw e;
}
// 清理临时文件
fs.unlinkSync(targetPayloadPath);
log(`🎉🎉🎉 全流程构建完成!`);
log(`📂 最终安装包: ${finalPath}`);
log(`📏 原始大小: ${originalSize} MB`);
const finalSize = fs.statSync(finalPath).size;
log(`📏 最终大小: ${(finalSize / 1024 / 1024).toFixed(2)} MB`);
// 6. 关键步骤:更新 latest.yml 以匹配新的安装包
// 否则自动更新会因为 SHA512 不匹配而失败
log('📝 Step 6: 修正 latest.yml 校验信息...');
const yamlPath = path.join(RELEASE_DIR, 'latest.yml');
// A. 必须删除 .blockmap 文件!
// 因为我们的 Setup.exe 已经被替换,原有的 blockmap 是针对旧 EXE 的。
// 如果不删,Updater 会尝试差分更新,导致校验失败。
const blockMapName = `${finalName}.blockmap`;
const blockMapPath = path.join(RELEASE_DIR, blockMapName);
if (fs.existsSync(blockMapPath)) {
fs.unlinkSync(blockMapPath);
log(`🗑️ 已删除无效的 BlockMap: ${blockMapName} (禁用差分更新)`);
}
if (fs.existsSync(yamlPath)) {
const crypto = require('crypto');
// 计算新的 SHA512 (Base64格式)
const buffer = fs.readFileSync(finalPath);
const hash = crypto.createHash('sha512').update(buffer).digest('base64');
let yamlContent = fs.readFileSync(yamlPath, 'utf8');
// 简单正则替换 (避免引入 yaml 库依赖)
// 1. 替换顶层 sha512
yamlContent = yamlContent.replace(/sha512: .+/g, `sha512: ${hash}`);
// 2. 替换顶层 size
yamlContent = yamlContent.replace(/size: \d+/g, `size: ${finalSize}`);
// 3. 确保 files 列表下的信息也更新 (如果有)
// 这比较复杂,通常 electron-updater 主要看顶层,或者 files 里的第一项
// 我们假设 electron-builder 生成的标准格式,暴力替换所有匹配的 checksum
// 但更安全的是只替换顶部的。标准 latest.yml 结构中 files 下也有 sha512。
// 重新写入
fs.writeFileSync(yamlPath, yamlContent);
log(`✅ latest.yml 已更新:\n SHA512: ${hash.substring(0, 20)}...\n Size: ${finalSize}`);
} else {
log('⚠️ 未找到 latest.yml,跳过元数据更新 (仅本地构建?)');
}
} catch (err) {
error(err.message);
}
+84
View File
@@ -0,0 +1,84 @@
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
//配置区
const PROJECT_ROOT = path.join(__dirname, '..');
const RELEASE_DIR = path.join(PROJECT_ROOT, 'release');
const INSTALLER_PRJ_DIR = path.join(PROJECT_ROOT, 'MyCoolInstaller');
const EMBEDDED_NAME = 'EmbeddedInstaller.exe';
function log(msg) { console.log(`\n\x1b[36m[Build-Shell]\x1b[0m ${msg}`); }
function error(msg) { console.error(`\n\x1b[31m[Error]\x1b[0m ${msg}`); process.exit(1); }
try {
// 0. 读取当前项目版本
const pkg = require(path.join(PROJECT_ROOT, 'package.json'));
const currentVersion = pkg.version;
log(`️ 当前项目版本: v${currentVersion}`);
// 1. 找到对应的 NSIS 安装包
log('🔍 Step 1: 寻找对应的 NSIS 安装包...');
if (!fs.existsSync(RELEASE_DIR)) error('Release 目录不存在');
// 精准匹配当前版本的安装包
const targetInstallerName = `CipherTalk-${currentVersion}-Setup.exe`;
const nsisPath = path.join(RELEASE_DIR, targetInstallerName);
if (!fs.existsSync(nsisPath)) {
error(`未找到对应版本的安装包: ${targetInstallerName}\n请先运行 npm run build 生成该版本的 Electron 安装包。`);
}
log(`✅ 找到安装包: ${targetInstallerName}`);
// 不需要正则匹配了,版本就是 currentVersion
const version = currentVersion;
// 2. 复制到 WPF 工程目录准备嵌入
log('🚚 Step 2: 注入到安装器工程...');
const targetPayloadPath = path.join(INSTALLER_PRJ_DIR, EMBEDDED_NAME);
fs.copyFileSync(nsisPath, targetPayloadPath);
// 3. 编译 WPF 外壳
log('🔨 Step 3: 快速编译 WPF 外壳...');
const csprojPath = path.join(INSTALLER_PRJ_DIR, 'MyCoolInstaller.csproj');
// 使用 PublishSingleFile 确保成单文件
const publishCmd = `dotnet publish "${csprojPath}" -c Release -r win-x64 --self-contained false -p:PublishSingleFile=true`;
try {
execSync(publishCmd, { stdio: 'inherit' });
} catch (e) {
error('WPF 编译失败');
}
// 4. 将最终产物移回 release 目录
log('🎁 Step 4: 输出最终产物...');
const wpfOutput = path.join(INSTALLER_PRJ_DIR, 'bin', 'Release', 'net8.0-windows', 'win-x64', 'publish', 'MyCoolInstaller.exe');
if (!fs.existsSync(wpfOutput)) error(`WPF 产物未找到: ${wpfOutput}`);
// 使用 Shell-Setup 后缀区分全量构建
const finalName = `CipherTalk-${version}-Shell-Setup.exe`;
const finalPath = path.join(RELEASE_DIR, finalName);
try {
if (fs.existsSync(finalPath)) {
fs.unlinkSync(finalPath); // 尝试先删除旧文件
}
fs.copyFileSync(wpfOutput, finalPath);
} catch (e) {
if (e.code === 'EBUSY' || e.code === 'EPERM') {
error(`目标文件被占用: ${finalPath}\n请关闭正在运行的安装程序或文件夹,然后重试。`);
} else {
throw e;
}
}
// 清理临时文件
fs.unlinkSync(targetPayloadPath);
log(`🎉🎉🎉 外壳构建完成!`);
log(`📂 最终安装包: ${finalPath}`);
} catch (err) {
error(err.message);
}
+27
View File
@@ -0,0 +1,27 @@
const fs = require('fs');
const path = require('path');
exports.default = async function (context) {
// context.appOutDir 是打包后的临时解压目录
const localesDir = path.join(context.appOutDir, 'locales');
if (fs.existsSync(localesDir)) {
console.log('🧹 正在清理多余的 Chromium 语言包...');
const files = fs.readdirSync(localesDir);
// 只保留中文(简体/繁体)和英文
const whitelist = [
'zh-CN.pak',
'en-US.pak'
];
let deletedCount = 0;
for (const file of files) {
if (file.endsWith('.pak') && !whitelist.includes(file)) {
fs.unlinkSync(path.join(localesDir, file));
deletedCount++;
}
}
console.log(`✅ 已删除 ${deletedCount} 个无关语言包,仅保留中英文。`);
}
};
+37
View File
@@ -0,0 +1,37 @@
const { execSync } = require('child_process');
const path = require('path');
const fs = require('fs');
const rootDir = path.join(__dirname, '..');
const upxPath = path.join(rootDir, 'upx', 'upx.exe');
// 需要压缩的依赖文件列表 (只针对 Windows 64位二进制)
const targets = [
// ⚠️ 暂停压缩:UPX 会破坏 .node 原生模块的完整性,导致 koffi/better-sqlite3 初始化失败
// 'node_modules/better-sqlite3/build/Release/better_sqlite3.node',
// 'node_modules/koffi/build/koffi/win32_x64/koffi.node',
];
console.log('🚀 开始自动化依赖包瘦身 (UPX)...');
if (!fs.existsSync(upxPath)) {
console.error('❌ 未找到 upx.exe,请确保 upx 目录在根目录下。');
process.exit(1);
}
targets.forEach(target => {
const fullPath = path.join(rootDir, target);
if (fs.existsSync(fullPath)) {
try {
console.log(`📦 正在压缩: ${target}`);
// --best 追求最高压缩比,--force 强制处理
execSync(`"${upxPath}" --best --force "${fullPath}"`, { stdio: 'inherit' });
} catch (err) {
console.warn(`⚠️ 无法压缩 ${target}:`, err.message);
}
} else {
console.log(`⏭️ 跳过 (未找到文件): ${target}`);
}
});
console.log('✅ 依赖包瘦身完成!\n');