mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-26 07:35:50 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1cef17174b | ||
|
|
73cabf2acd | ||
|
|
49770f9e8d | ||
|
|
e32261d274 |
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@@ -45,8 +45,6 @@ jobs:
|
|||||||
- name: Package and Publish macOS arm64 (unsigned DMG)
|
- name: Package and Publish macOS arm64 (unsigned DMG)
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
WF_SIGN_PRIVATE_KEY: ${{ secrets.WF_SIGN_PRIVATE_KEY }}
|
|
||||||
WF_SIGNING_REQUIRED: "1"
|
|
||||||
CSC_IDENTITY_AUTO_DISCOVERY: "false"
|
CSC_IDENTITY_AUTO_DISCOVERY: "false"
|
||||||
run: |
|
run: |
|
||||||
npx electron-builder --mac dmg --arm64 --publish always
|
npx electron-builder --mac dmg --arm64 --publish always
|
||||||
@@ -84,8 +82,6 @@ jobs:
|
|||||||
- name: Package and Publish Linux
|
- name: Package and Publish Linux
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
WF_SIGN_PRIVATE_KEY: ${{ secrets.WF_SIGN_PRIVATE_KEY }}
|
|
||||||
WF_SIGNING_REQUIRED: "1"
|
|
||||||
run: |
|
run: |
|
||||||
npx electron-builder --linux --publish always
|
npx electron-builder --linux --publish always
|
||||||
|
|
||||||
@@ -122,8 +118,6 @@ jobs:
|
|||||||
- name: Package and Publish
|
- name: Package and Publish
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
WF_SIGN_PRIVATE_KEY: ${{ secrets.WF_SIGN_PRIVATE_KEY }}
|
|
||||||
WF_SIGNING_REQUIRED: "1"
|
|
||||||
run: |
|
run: |
|
||||||
npx electron-builder --publish always
|
npx electron-builder --publish always
|
||||||
|
|
||||||
@@ -160,8 +154,6 @@ jobs:
|
|||||||
- name: Package and Publish Windows arm64
|
- name: Package and Publish Windows arm64
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
WF_SIGN_PRIVATE_KEY: ${{ secrets.WF_SIGN_PRIVATE_KEY }}
|
|
||||||
WF_SIGNING_REQUIRED: "1"
|
|
||||||
run: |
|
run: |
|
||||||
npx electron-builder --win nsis --arm64 --publish always '--config.artifactName=${productName}-${version}-arm64-Setup.${ext}'
|
npx electron-builder --win nsis --arm64 --publish always '--config.artifactName=${productName}-${version}-arm64-Setup.${ext}'
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import * as https from 'https'
|
|||||||
import * as http from 'http'
|
import * as http from 'http'
|
||||||
import * as fzstd from 'fzstd'
|
import * as fzstd from 'fzstd'
|
||||||
import * as crypto from 'crypto'
|
import * as crypto from 'crypto'
|
||||||
import { app, BrowserWindow } from 'electron'
|
import { app, BrowserWindow, dialog } from 'electron'
|
||||||
import { ConfigService } from './config'
|
import { ConfigService } from './config'
|
||||||
import { wcdbService } from './wcdbService'
|
import { wcdbService } from './wcdbService'
|
||||||
import { MessageCacheService } from './messageCacheService'
|
import { MessageCacheService } from './messageCacheService'
|
||||||
@@ -292,6 +292,7 @@ class ChatService {
|
|||||||
private readonly allGroupSessionIdsCacheTtlMs = 5 * 60 * 1000
|
private readonly allGroupSessionIdsCacheTtlMs = 5 * 60 * 1000
|
||||||
private groupMyMessageCountCacheScope = ''
|
private groupMyMessageCountCacheScope = ''
|
||||||
private groupMyMessageCountMemoryCache = new Map<string, GroupMyMessageCountCacheEntry>()
|
private groupMyMessageCountMemoryCache = new Map<string, GroupMyMessageCountCacheEntry>()
|
||||||
|
private initFailureDialogShown = false
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.configService = new ConfigService()
|
this.configService = new ConfigService()
|
||||||
@@ -338,6 +339,55 @@ class ChatService {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private extractErrorCode(message?: string): number | null {
|
||||||
|
const text = String(message || '').trim()
|
||||||
|
if (!text) return null
|
||||||
|
const match = text.match(/(?:错误码\s*[::]\s*|\()(-?\d{2,6})(?:\)|\b)/)
|
||||||
|
if (!match) return null
|
||||||
|
const parsed = Number(match[1])
|
||||||
|
return Number.isFinite(parsed) ? parsed : null
|
||||||
|
}
|
||||||
|
|
||||||
|
private toCodeOnlyMessage(rawMessage?: string, fallbackCode = -3999): string {
|
||||||
|
const code = this.extractErrorCode(rawMessage) ?? fallbackCode
|
||||||
|
return `错误码: ${code}`
|
||||||
|
}
|
||||||
|
|
||||||
|
private async maybeShowInitFailureDialog(errorMessage: string): Promise<void> {
|
||||||
|
if (!app.isPackaged) return
|
||||||
|
if (this.initFailureDialogShown) return
|
||||||
|
|
||||||
|
const code = this.extractErrorCode(errorMessage)
|
||||||
|
if (code === null) return
|
||||||
|
const isSecurityCode =
|
||||||
|
code === -101 ||
|
||||||
|
code === -102 ||
|
||||||
|
code === -2299 ||
|
||||||
|
code === -2301 ||
|
||||||
|
code === -2302 ||
|
||||||
|
code === -1006 ||
|
||||||
|
(code <= -2201 && code >= -2212)
|
||||||
|
if (!isSecurityCode) return
|
||||||
|
|
||||||
|
this.initFailureDialogShown = true
|
||||||
|
const detail = [
|
||||||
|
`错误码: ${code}`
|
||||||
|
].join('\n')
|
||||||
|
|
||||||
|
try {
|
||||||
|
await dialog.showMessageBox({
|
||||||
|
type: 'error',
|
||||||
|
title: 'WeFlow 启动失败',
|
||||||
|
message: '启动失败,请反馈错误码。',
|
||||||
|
detail,
|
||||||
|
buttons: ['确定'],
|
||||||
|
noLink: true
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
// 弹窗失败不阻断主流程
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 连接数据库
|
* 连接数据库
|
||||||
*/
|
*/
|
||||||
@@ -362,7 +412,9 @@ class ChatService {
|
|||||||
const cleanedWxid = this.cleanAccountDirName(wxid)
|
const cleanedWxid = this.cleanAccountDirName(wxid)
|
||||||
const openOk = await wcdbService.open(dbPath, decryptKey, cleanedWxid)
|
const openOk = await wcdbService.open(dbPath, decryptKey, cleanedWxid)
|
||||||
if (!openOk) {
|
if (!openOk) {
|
||||||
return { success: false, error: 'WCDB 打开失败,请检查路径和密钥' }
|
const detailedError = this.toCodeOnlyMessage(await wcdbService.getLastInitError())
|
||||||
|
await this.maybeShowInitFailureDialog(detailedError)
|
||||||
|
return { success: false, error: detailedError }
|
||||||
}
|
}
|
||||||
|
|
||||||
this.connected = true
|
this.connected = true
|
||||||
@@ -376,7 +428,7 @@ class ChatService {
|
|||||||
return { success: true }
|
return { success: true }
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('ChatService: 连接数据库失败:', e)
|
console.error('ChatService: 连接数据库失败:', e)
|
||||||
return { success: false, error: String(e) }
|
return { success: false, error: this.toCodeOnlyMessage(String(e), -3998) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -126,6 +126,10 @@ export class WcdbCore {
|
|||||||
this.writeLog(`[bootstrap] setPaths resourcesPath=${resourcesPath} userDataPath=${userDataPath}`, true)
|
this.writeLog(`[bootstrap] setPaths resourcesPath=${resourcesPath} userDataPath=${userDataPath}`, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getLastInitError(): string | null {
|
||||||
|
return lastDllInitError
|
||||||
|
}
|
||||||
|
|
||||||
setLogEnabled(enabled: boolean): void {
|
setLogEnabled(enabled: boolean): void {
|
||||||
this.logEnabled = enabled
|
this.logEnabled = enabled
|
||||||
this.writeLog(`[bootstrap] setLogEnabled=${enabled ? '1' : '0'} env.WCDB_LOG_ENABLED=${process.env.WCDB_LOG_ENABLED || ''}`, true)
|
this.writeLog(`[bootstrap] setLogEnabled=${enabled ? '1' : '0'} env.WCDB_LOG_ENABLED=${process.env.WCDB_LOG_ENABLED || ''}`, true)
|
||||||
@@ -300,23 +304,7 @@ export class WcdbCore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private formatInitProtectionError(code: number): string {
|
private formatInitProtectionError(code: number): string {
|
||||||
switch (code) {
|
return `错误码: ${code}`
|
||||||
case -101: return '安全校验失败:授权已过期(-101)'
|
|
||||||
case -102: return '安全校验失败:关键环境文件缺失(-102)'
|
|
||||||
case -2201: return '安全校验失败:未找到签名清单(-2201)'
|
|
||||||
case -2202: return '安全校验失败:缺少签名文件(-2202)'
|
|
||||||
case -2203: return '安全校验失败:读取签名清单失败(-2203)'
|
|
||||||
case -2204: return '安全校验失败:读取签名文件失败(-2204)'
|
|
||||||
case -2205: return '安全校验失败:签名内容格式无效(-2205)'
|
|
||||||
case -2206: return '安全校验失败:签名清单解析失败(-2206)'
|
|
||||||
case -2207: return '安全校验失败:清单平台与当前平台不匹配(-2207)'
|
|
||||||
case -2208: return '安全校验失败:目标文件哈希读取失败(-2208)'
|
|
||||||
case -2209: return '安全校验失败:目标文件哈希不匹配(-2209)'
|
|
||||||
case -2210: return '安全校验失败:签名无效(-2210)'
|
|
||||||
case -2211: return '安全校验失败:主程序 EXE 哈希不匹配(-2211)'
|
|
||||||
case -2212: return '安全校验失败:wcdb_api 模块哈希不匹配(-2212)'
|
|
||||||
default: return `安全校验失败(错误码: ${code})`
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private isLogEnabled(): boolean {
|
private isLogEnabled(): boolean {
|
||||||
@@ -640,7 +628,9 @@ export class WcdbCore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.writeLog(`[bootstrap] koffi.load begin path=${dllPath}`, true)
|
||||||
this.lib = this.koffi.load(dllPath)
|
this.lib = this.koffi.load(dllPath)
|
||||||
|
this.writeLog('[bootstrap] koffi.load ok', true)
|
||||||
|
|
||||||
// InitProtection (Added for security)
|
// InitProtection (Added for security)
|
||||||
try {
|
try {
|
||||||
@@ -666,6 +656,7 @@ export class WcdbCore {
|
|||||||
}
|
}
|
||||||
for (const resPath of resourcePaths) {
|
for (const resPath of resourcePaths) {
|
||||||
try {
|
try {
|
||||||
|
this.writeLog(`[bootstrap] InitProtection call path=${resPath}`, true)
|
||||||
protectionCode = Number(this.wcdbInitProtection(resPath))
|
protectionCode = Number(this.wcdbInitProtection(resPath))
|
||||||
if (protectionCode === 0) {
|
if (protectionCode === 0) {
|
||||||
protectionOk = true
|
protectionOk = true
|
||||||
@@ -687,7 +678,7 @@ export class WcdbCore {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
lastDllInitError = `InitProtection symbol not found: ${String(e)}`
|
lastDllInitError = this.formatInitProtectionError(-2301)
|
||||||
this.writeLog(`[bootstrap] InitProtection symbol load failed: ${String(e)}`, true)
|
this.writeLog(`[bootstrap] InitProtection symbol load failed: ${String(e)}`, true)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -1107,7 +1098,7 @@ export class WcdbCore {
|
|||||||
const initResult = this.wcdbInit()
|
const initResult = this.wcdbInit()
|
||||||
if (initResult !== 0) {
|
if (initResult !== 0) {
|
||||||
console.error('WCDB 初始化失败:', initResult)
|
console.error('WCDB 初始化失败:', initResult)
|
||||||
lastDllInitError = `初始化失败(错误码: ${initResult})`
|
lastDllInitError = this.formatInitProtectionError(initResult)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1118,14 +1109,7 @@ export class WcdbCore {
|
|||||||
const errorMsg = e instanceof Error ? e.message : String(e)
|
const errorMsg = e instanceof Error ? e.message : String(e)
|
||||||
console.error('WCDB 初始化异常:', errorMsg)
|
console.error('WCDB 初始化异常:', errorMsg)
|
||||||
this.writeLog(`WCDB 初始化异常: ${errorMsg}`, true)
|
this.writeLog(`WCDB 初始化异常: ${errorMsg}`, true)
|
||||||
lastDllInitError = errorMsg
|
lastDllInitError = this.formatInitProtectionError(-2302)
|
||||||
// 检查是否是常见的 VC++ 运行时缺失错误
|
|
||||||
if (errorMsg.includes('126') || errorMsg.includes('找不到指定的模块') ||
|
|
||||||
errorMsg.includes('The specified module could not be found')) {
|
|
||||||
lastDllInitError = '可能缺少 Visual C++ 运行时库。请安装 Microsoft Visual C++ Redistributable (x64)。'
|
|
||||||
} else if (errorMsg.includes('193') || errorMsg.includes('不是有效的 Win32 应用程序')) {
|
|
||||||
lastDllInitError = 'DLL 架构不匹配。请确保使用 64 位版本的应用程序。'
|
|
||||||
}
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1152,8 +1136,7 @@ export class WcdbCore {
|
|||||||
if (!this.initialized) {
|
if (!this.initialized) {
|
||||||
const initOk = await this.initialize()
|
const initOk = await this.initialize()
|
||||||
if (!initOk) {
|
if (!initOk) {
|
||||||
// 返回更详细的错误信息,帮助用户诊断问题
|
const detailedError = lastDllInitError || this.formatInitProtectionError(-2303)
|
||||||
const detailedError = lastDllInitError || 'WCDB 初始化失败'
|
|
||||||
return { success: false, error: detailedError }
|
return { success: false, error: detailedError }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1163,7 +1146,7 @@ export class WcdbCore {
|
|||||||
this.writeLog(`testConnection dbPath=${dbPath} wxid=${wxid} dbStorage=${dbStoragePath || 'null'}`)
|
this.writeLog(`testConnection dbPath=${dbPath} wxid=${wxid} dbStorage=${dbStoragePath || 'null'}`)
|
||||||
|
|
||||||
if (!dbStoragePath || !existsSync(dbStoragePath)) {
|
if (!dbStoragePath || !existsSync(dbStoragePath)) {
|
||||||
return { success: false, error: `数据库目录不存在: ${dbPath}` }
|
return { success: false, error: this.formatInitProtectionError(-3001) }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 递归查找 session.db
|
// 递归查找 session.db
|
||||||
@@ -1171,7 +1154,7 @@ export class WcdbCore {
|
|||||||
this.writeLog(`testConnection sessionDb=${sessionDbPath || 'null'}`)
|
this.writeLog(`testConnection sessionDb=${sessionDbPath || 'null'}`)
|
||||||
|
|
||||||
if (!sessionDbPath) {
|
if (!sessionDbPath) {
|
||||||
return { success: false, error: `未找到 session.db 文件` }
|
return { success: false, error: this.formatInitProtectionError(-3002) }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 分配输出参数内存
|
// 分配输出参数内存
|
||||||
@@ -1180,17 +1163,13 @@ export class WcdbCore {
|
|||||||
|
|
||||||
if (result !== 0) {
|
if (result !== 0) {
|
||||||
await this.printLogs()
|
await this.printLogs()
|
||||||
let errorMsg = '数据库打开失败'
|
|
||||||
if (result === -1) errorMsg = '参数错误'
|
|
||||||
else if (result === -2) errorMsg = '密钥错误'
|
|
||||||
else if (result === -3) errorMsg = '数据库打开失败'
|
|
||||||
this.writeLog(`testConnection openAccount failed code=${result}`)
|
this.writeLog(`testConnection openAccount failed code=${result}`)
|
||||||
return { success: false, error: `${errorMsg} (错误码: ${result})` }
|
return { success: false, error: this.formatInitProtectionError(result) }
|
||||||
}
|
}
|
||||||
|
|
||||||
const tempHandle = handleOut[0]
|
const tempHandle = handleOut[0]
|
||||||
if (tempHandle <= 0) {
|
if (tempHandle <= 0) {
|
||||||
return { success: false, error: '无效的数据库句柄' }
|
return { success: false, error: this.formatInitProtectionError(-3003) }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 测试成功:使用 shutdown 清理资源(包括测试句柄)
|
// 测试成功:使用 shutdown 清理资源(包括测试句柄)
|
||||||
@@ -1219,7 +1198,7 @@ export class WcdbCore {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('测试连接异常:', e)
|
console.error('测试连接异常:', e)
|
||||||
this.writeLog(`testConnection exception: ${String(e)}`)
|
this.writeLog(`testConnection exception: ${String(e)}`)
|
||||||
return { success: false, error: String(e) }
|
return { success: false, error: this.formatInitProtectionError(-3004) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1411,6 +1390,7 @@ export class WcdbCore {
|
|||||||
*/
|
*/
|
||||||
async open(dbPath: string, hexKey: string, wxid: string): Promise<boolean> {
|
async open(dbPath: string, hexKey: string, wxid: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
|
lastDllInitError = null
|
||||||
if (!this.initialized) {
|
if (!this.initialized) {
|
||||||
const initOk = await this.initialize()
|
const initOk = await this.initialize()
|
||||||
if (!initOk) return false
|
if (!initOk) return false
|
||||||
@@ -1438,6 +1418,7 @@ export class WcdbCore {
|
|||||||
if (!dbStoragePath || !existsSync(dbStoragePath)) {
|
if (!dbStoragePath || !existsSync(dbStoragePath)) {
|
||||||
console.error('数据库目录不存在:', dbPath)
|
console.error('数据库目录不存在:', dbPath)
|
||||||
this.writeLog(`open failed: dbStorage not found for ${dbPath}`)
|
this.writeLog(`open failed: dbStorage not found for ${dbPath}`)
|
||||||
|
lastDllInitError = this.formatInitProtectionError(-3001)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1446,6 +1427,7 @@ export class WcdbCore {
|
|||||||
if (!sessionDbPath) {
|
if (!sessionDbPath) {
|
||||||
console.error('未找到 session.db 文件')
|
console.error('未找到 session.db 文件')
|
||||||
this.writeLog('open failed: session.db not found')
|
this.writeLog('open failed: session.db not found')
|
||||||
|
lastDllInitError = this.formatInitProtectionError(-3002)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1456,11 +1438,13 @@ export class WcdbCore {
|
|||||||
console.error('打开数据库失败:', result)
|
console.error('打开数据库失败:', result)
|
||||||
await this.printLogs()
|
await this.printLogs()
|
||||||
this.writeLog(`open failed: openAccount code=${result}`)
|
this.writeLog(`open failed: openAccount code=${result}`)
|
||||||
|
lastDllInitError = this.formatInitProtectionError(result)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const handle = handleOut[0]
|
const handle = handleOut[0]
|
||||||
if (handle <= 0) {
|
if (handle <= 0) {
|
||||||
|
lastDllInitError = this.formatInitProtectionError(-3003)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1470,6 +1454,7 @@ export class WcdbCore {
|
|||||||
this.currentWxid = wxid
|
this.currentWxid = wxid
|
||||||
this.currentDbStoragePath = dbStoragePath
|
this.currentDbStoragePath = dbStoragePath
|
||||||
this.initialized = true
|
this.initialized = true
|
||||||
|
lastDllInitError = null
|
||||||
if (this.wcdbSetMyWxid && wxid) {
|
if (this.wcdbSetMyWxid && wxid) {
|
||||||
try {
|
try {
|
||||||
this.wcdbSetMyWxid(this.handle, wxid)
|
this.wcdbSetMyWxid(this.handle, wxid)
|
||||||
@@ -1487,6 +1472,7 @@ export class WcdbCore {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('打开数据库异常:', e)
|
console.error('打开数据库异常:', e)
|
||||||
this.writeLog(`open exception: ${String(e)}`)
|
this.writeLog(`open exception: ${String(e)}`)
|
||||||
|
lastDllInitError = this.formatInitProtectionError(-3004)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -164,6 +164,10 @@ export class WcdbService {
|
|||||||
return this.callWorker('open', { dbPath, hexKey, wxid })
|
return this.callWorker('open', { dbPath, hexKey, wxid })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getLastInitError(): Promise<string | null> {
|
||||||
|
return this.callWorker('getLastInitError')
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 关闭数据库连接
|
* 关闭数据库连接
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ if (parentPort) {
|
|||||||
case 'open':
|
case 'open':
|
||||||
result = await core.open(payload.dbPath, payload.hexKey, payload.wxid)
|
result = await core.open(payload.dbPath, payload.hexKey, payload.wxid)
|
||||||
break
|
break
|
||||||
|
case 'getLastInitError':
|
||||||
|
result = core.getLastInitError()
|
||||||
|
break
|
||||||
case 'close':
|
case 'close':
|
||||||
core.close()
|
core.close()
|
||||||
result = { success: true }
|
result = { success: true }
|
||||||
|
|||||||
@@ -63,8 +63,6 @@
|
|||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"appId": "com.WeFlow.app",
|
"appId": "com.WeFlow.app",
|
||||||
"afterPack": "scripts/afterPack-sign-manifest.cjs",
|
|
||||||
"afterSign": "scripts/afterPack-sign-manifest.cjs",
|
|
||||||
"publish": {
|
"publish": {
|
||||||
"provider": "github",
|
"provider": "github",
|
||||||
"owner": "hicccc77",
|
"owner": "hicccc77",
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,289 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
/* eslint-disable no-console */
|
|
||||||
const fs = require('node:fs');
|
|
||||||
const path = require('node:path');
|
|
||||||
const crypto = require('node:crypto');
|
|
||||||
|
|
||||||
const MANIFEST_NAME = '.wf_manifest.json';
|
|
||||||
const SIGNATURE_NAME = '.wf_manifest.sig';
|
|
||||||
const MODULE_FILENAME = {
|
|
||||||
win32: 'wcdb_api.dll',
|
|
||||||
darwin: 'libwcdb_api.dylib',
|
|
||||||
linux: 'libwcdb_api.so',
|
|
||||||
};
|
|
||||||
|
|
||||||
function readTextIfExists(filePath) {
|
|
||||||
try {
|
|
||||||
if (!fs.existsSync(filePath)) return null;
|
|
||||||
return fs.readFileSync(filePath, 'utf8');
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadEnvFile(projectDir, fileName) {
|
|
||||||
const envPath = path.join(projectDir, fileName);
|
|
||||||
const content = readTextIfExists(envPath);
|
|
||||||
if (!content) return false;
|
|
||||||
|
|
||||||
for (const line of content.split(/\r?\n/)) {
|
|
||||||
const trimmed = line.trim();
|
|
||||||
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
||||||
const eq = trimmed.indexOf('=');
|
|
||||||
if (eq <= 0) continue;
|
|
||||||
const key = trimmed.slice(0, eq).trim();
|
|
||||||
let value = trimmed.slice(eq + 1).trim();
|
|
||||||
if (!key || process.env[key] !== undefined) continue;
|
|
||||||
if (
|
|
||||||
(value.startsWith('"') && value.endsWith('"')) ||
|
|
||||||
(value.startsWith("'") && value.endsWith("'"))
|
|
||||||
) {
|
|
||||||
value = value.slice(1, -1);
|
|
||||||
}
|
|
||||||
process.env[key] = value;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureSigningEnv() {
|
|
||||||
const projectDir = process.cwd();
|
|
||||||
if (!process.env.WF_SIGN_PRIVATE_KEY) {
|
|
||||||
loadEnvFile(projectDir, '.env.local');
|
|
||||||
loadEnvFile(projectDir, '.env');
|
|
||||||
}
|
|
||||||
|
|
||||||
const keyB64 = (process.env.WF_SIGN_PRIVATE_KEY || '').trim();
|
|
||||||
const required = (process.env.WF_SIGNING_REQUIRED || '').trim() === '1';
|
|
||||||
if (!keyB64) {
|
|
||||||
if (required) {
|
|
||||||
throw new Error(
|
|
||||||
'WF_SIGN_PRIVATE_KEY is missing (WF_SIGNING_REQUIRED=1). ' +
|
|
||||||
'Set it in CI Secret or .env.local for local build.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return keyB64;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPlatform(context) {
|
|
||||||
const raw = (
|
|
||||||
context?.electronPlatformName ||
|
|
||||||
context?.packager?.platform?.name ||
|
|
||||||
process.platform
|
|
||||||
);
|
|
||||||
return normalizePlatformTag(raw);
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizePlatformTag(rawPlatform) {
|
|
||||||
const p = String(rawPlatform || '').toLowerCase();
|
|
||||||
if (p === 'darwin' || p === 'mac' || p === 'macos' || p === 'osx') return 'darwin';
|
|
||||||
if (p === 'win32' || p === 'win' || p === 'windows') return 'win32';
|
|
||||||
if (p === 'linux') return 'linux';
|
|
||||||
return p || process.platform;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getProductFilename(context) {
|
|
||||||
return (
|
|
||||||
context?.packager?.appInfo?.productFilename ||
|
|
||||||
context?.packager?.config?.productName ||
|
|
||||||
'WeFlow'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveMacAppBundle(appOutDir, productFilename) {
|
|
||||||
const candidates = [];
|
|
||||||
if (String(appOutDir).toLowerCase().endsWith('.app')) {
|
|
||||||
candidates.push(appOutDir);
|
|
||||||
} else {
|
|
||||||
candidates.push(path.join(appOutDir, `${productFilename}.app`));
|
|
||||||
if (fs.existsSync(appOutDir) && fs.statSync(appOutDir).isDirectory()) {
|
|
||||||
const appDirs = fs
|
|
||||||
.readdirSync(appOutDir, { withFileTypes: true })
|
|
||||||
.filter((e) => e.isDirectory() && e.name.toLowerCase().endsWith('.app'))
|
|
||||||
.map((e) => path.join(appOutDir, e.name));
|
|
||||||
candidates.push(...appDirs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const bundleDir of candidates) {
|
|
||||||
const resourcesPath = path.join(bundleDir, 'Contents', 'Resources');
|
|
||||||
if (fs.existsSync(resourcesPath) && fs.statSync(resourcesPath).isDirectory()) {
|
|
||||||
return bundleDir;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getResourcesDir(appOutDir, platform, productFilename) {
|
|
||||||
if (platform === 'darwin') {
|
|
||||||
const bundleDir = resolveMacAppBundle(appOutDir, productFilename);
|
|
||||||
if (!bundleDir) return path.join(appOutDir, 'resources');
|
|
||||||
return path.join(bundleDir, 'Contents', 'Resources');
|
|
||||||
}
|
|
||||||
return path.join(appOutDir, 'resources');
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeRel(baseDir, filePath) {
|
|
||||||
return path.relative(baseDir, filePath).split(path.sep).join('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
function sha256FileHex(filePath) {
|
|
||||||
const data = fs.readFileSync(filePath);
|
|
||||||
return crypto.createHash('sha256').update(data).digest('hex');
|
|
||||||
}
|
|
||||||
|
|
||||||
function findFirstExisting(paths) {
|
|
||||||
for (const p of paths) {
|
|
||||||
if (p && fs.existsSync(p) && fs.statSync(p).isFile()) return p;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function findExecutablePath({ appOutDir, platform, productFilename, executableName }) {
|
|
||||||
if (platform === 'win32') {
|
|
||||||
return findFirstExisting([
|
|
||||||
path.join(appOutDir, `${productFilename}.exe`),
|
|
||||||
path.join(appOutDir, `${executableName || ''}.exe`),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (platform === 'darwin') {
|
|
||||||
const bundleDir = resolveMacAppBundle(appOutDir, productFilename) || appOutDir;
|
|
||||||
const macOsDir = path.join(bundleDir, 'Contents', 'MacOS');
|
|
||||||
const preferred = findFirstExisting([path.join(macOsDir, productFilename)]);
|
|
||||||
if (preferred) return preferred;
|
|
||||||
if (!fs.existsSync(macOsDir)) return null;
|
|
||||||
const files = fs
|
|
||||||
.readdirSync(macOsDir)
|
|
||||||
.map((name) => path.join(macOsDir, name))
|
|
||||||
.filter((p) => fs.statSync(p).isFile());
|
|
||||||
return files[0] || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return findFirstExisting([
|
|
||||||
path.join(appOutDir, executableName || ''),
|
|
||||||
path.join(appOutDir, productFilename),
|
|
||||||
path.join(appOutDir, productFilename.toLowerCase()),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function findByBasenameRecursive(rootDir, basename) {
|
|
||||||
if (!fs.existsSync(rootDir)) return null;
|
|
||||||
const stack = [rootDir];
|
|
||||||
while (stack.length > 0) {
|
|
||||||
const dir = stack.pop();
|
|
||||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
||||||
for (const entry of entries) {
|
|
||||||
const full = path.join(dir, entry.name);
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
stack.push(full);
|
|
||||||
} else if (entry.isFile() && entry.name.toLowerCase() === basename.toLowerCase()) {
|
|
||||||
return full;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getModulePath(resourcesDir, appOutDir, platform) {
|
|
||||||
const filename = MODULE_FILENAME[platform] || MODULE_FILENAME[process.platform];
|
|
||||||
if (!filename) return null;
|
|
||||||
|
|
||||||
const direct = findFirstExisting([
|
|
||||||
path.join(resourcesDir, 'resources', filename),
|
|
||||||
path.join(resourcesDir, filename),
|
|
||||||
]);
|
|
||||||
if (direct) return direct;
|
|
||||||
|
|
||||||
const inResources = findByBasenameRecursive(resourcesDir, filename);
|
|
||||||
if (inResources) return inResources;
|
|
||||||
|
|
||||||
return findByBasenameRecursive(appOutDir, filename);
|
|
||||||
}
|
|
||||||
|
|
||||||
function signDetachedEd25519(payloadUtf8, privateKeyDerB64) {
|
|
||||||
const privateKeyDer = Buffer.from(privateKeyDerB64, 'base64');
|
|
||||||
const keyObject = crypto.createPrivateKey({
|
|
||||||
key: privateKeyDer,
|
|
||||||
format: 'der',
|
|
||||||
type: 'pkcs8',
|
|
||||||
});
|
|
||||||
return crypto.sign(null, Buffer.from(payloadUtf8, 'utf8'), keyObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = async function afterPack(context) {
|
|
||||||
const privateKeyDerB64 = ensureSigningEnv();
|
|
||||||
if (!privateKeyDerB64) {
|
|
||||||
console.log('[wf-sign] skip: WF_SIGN_PRIVATE_KEY not provided and signing not required.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const appOutDir = context?.appOutDir;
|
|
||||||
if (!appOutDir || !fs.existsSync(appOutDir)) {
|
|
||||||
throw new Error(`[wf-sign] invalid appOutDir: ${String(appOutDir)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const platform = String(getPlatform(context)).toLowerCase();
|
|
||||||
const productFilename = getProductFilename(context);
|
|
||||||
const executableName = context?.packager?.config?.linux?.executableName || '';
|
|
||||||
const resourcesDir = getResourcesDir(appOutDir, platform, productFilename);
|
|
||||||
if (!fs.existsSync(resourcesDir)) {
|
|
||||||
throw new Error(
|
|
||||||
`[wf-sign] resources directory not found: ${resourcesDir}; platform=${platform}; appOutDir=${appOutDir}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const exePath = findExecutablePath({
|
|
||||||
appOutDir,
|
|
||||||
platform,
|
|
||||||
productFilename,
|
|
||||||
executableName,
|
|
||||||
});
|
|
||||||
if (!exePath) {
|
|
||||||
throw new Error(
|
|
||||||
`[wf-sign] executable not found. platform=${platform}, appOutDir=${appOutDir}, productFilename=${productFilename}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const modulePath = getModulePath(resourcesDir, appOutDir, platform);
|
|
||||||
if (!modulePath) {
|
|
||||||
throw new Error(
|
|
||||||
`[wf-sign] ${MODULE_FILENAME[platform] || 'wcdb_api'} not found under resources: ${resourcesDir}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const manifest = {
|
|
||||||
schema: 1,
|
|
||||||
platform,
|
|
||||||
version: context?.packager?.appInfo?.version || '',
|
|
||||||
generatedAt: new Date().toISOString(),
|
|
||||||
targets: [
|
|
||||||
{
|
|
||||||
id: 'exe',
|
|
||||||
path: normalizeRel(resourcesDir, exePath),
|
|
||||||
sha256: sha256FileHex(exePath),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'module',
|
|
||||||
path: normalizeRel(resourcesDir, modulePath),
|
|
||||||
sha256: sha256FileHex(modulePath),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const payload = `${JSON.stringify(manifest, null, 2)}\n`;
|
|
||||||
const signature = signDetachedEd25519(payload, privateKeyDerB64).toString('base64');
|
|
||||||
|
|
||||||
const manifestPath = path.join(resourcesDir, MANIFEST_NAME);
|
|
||||||
const signaturePath = path.join(resourcesDir, SIGNATURE_NAME);
|
|
||||||
fs.writeFileSync(manifestPath, payload, 'utf8');
|
|
||||||
fs.writeFileSync(signaturePath, `${signature}\n`, 'utf8');
|
|
||||||
|
|
||||||
console.log(`[wf-sign] manifest: ${manifestPath}`);
|
|
||||||
console.log(`[wf-sign] signature: ${signaturePath}`);
|
|
||||||
console.log(`[wf-sign] exe: ${manifest.targets[0].path}`);
|
|
||||||
console.log(`[wf-sign] exe.sha256: ${manifest.targets[0].sha256}`);
|
|
||||||
console.log(`[wf-sign] module: ${manifest.targets[1].path}`);
|
|
||||||
console.log(`[wf-sign] module.sha256: ${manifest.targets[1].sha256}`);
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user