Compare commits

..

17 Commits

Author SHA1 Message Date
xuncha
26d4751e80 Merge pull request #157 from hicccc77/dev
Dev
2026-01-31 18:37:19 +08:00
xuncha
b8120a5119 Merge pull request #156 from xunchahaha/dev
111
2026-01-31 18:36:50 +08:00
xuncha
68a13cefc3 111 2026-01-31 18:36:28 +08:00
xuncha
cd4b8f3702 Merge pull request #155 from hicccc77/main
同步一下
2026-01-31 18:16:11 +08:00
xuncha
c5956ba203 Merge pull request #154 from xunchahaha/main
xiufu
2026-01-31 18:15:10 +08:00
xuncha
f456357e01 Merge pull request #153 from xunchahaha/dev
Dev
2026-01-31 18:14:26 +08:00
xuncha
4ef821f45f 更新版本号 2026-01-31 18:12:57 +08:00
xuncha
912c78e9e9 Merge branch 'main' of https://github.com/xunchahaha/WeFlow 2026-01-31 18:11:58 +08:00
xuncha
bfcd154a25 wxid可以自己选择 2026-01-31 18:11:55 +08:00
xuncha
a1c8ba48b0 Merge pull request #1 from xunchahaha/main
11
2026-01-31 17:46:20 +08:00
xuncha
f93369489d Merge branch 'hicccc77:main' into main 2026-01-31 17:45:54 +08:00
xuncha
014f57f152 尝试修复秘钥获取失败 2026-01-31 17:44:52 +08:00
xuncha
3f1eb58af4 Merge pull request #151 from xunchahaha:main
Main
2026-01-31 16:14:26 +08:00
xuncha
3d9b1b0f8c Merge pull request #150 from xunchahaha:main
Main
2026-01-31 16:06:54 +08:00
xuncha
97f14030de Merge pull request #149 from xunchahaha:main
Main
2026-01-31 16:02:01 +08:00
xuncha
a760f45823 Merge pull request #148 from xunchahaha:main
Main
2026-01-31 15:56:53 +08:00
xuncha
c29bbab25f Merge pull request #147 from xunchahaha:main
Main
2026-01-31 15:51:21 +08:00
9 changed files with 243 additions and 23 deletions

View File

@@ -674,6 +674,10 @@ function registerIpcHandlers() {
return dbPathService.scanWxids(rootPath)
})
ipcMain.handle('dbpath:scanWxidCandidates', async (_, rootPath: string) => {
return dbPathService.scanWxidCandidates(rootPath)
})
ipcMain.handle('dbpath:getDefault', async () => {
return dbPathService.getDefaultPath()
})

View File

@@ -71,6 +71,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
dbPath: {
autoDetect: () => ipcRenderer.invoke('dbpath:autoDetect'),
scanWxids: (rootPath: string) => ipcRenderer.invoke('dbpath:scanWxids', rootPath),
scanWxidCandidates: (rootPath: string) => ipcRenderer.invoke('dbpath:scanWxidCandidates', rootPath),
getDefault: () => ipcRenderer.invoke('dbpath:getDefault')
},

View File

@@ -118,6 +118,48 @@ export class DbPathService {
}
}
/**
* 扫描目录名候选(仅包含下划线的文件夹,排除 all_users
*/
scanWxidCandidates(rootPath: string): WxidInfo[] {
const wxids: WxidInfo[] = []
try {
if (existsSync(rootPath)) {
const entries = readdirSync(rootPath)
for (const entry of entries) {
const entryPath = join(rootPath, entry)
let stat: ReturnType<typeof statSync>
try {
stat = statSync(entryPath)
} catch {
continue
}
if (!stat.isDirectory()) continue
const lower = entry.toLowerCase()
if (lower === 'all_users') continue
if (!entry.includes('_')) continue
wxids.push({ wxid: entry, modifiedTime: stat.mtimeMs })
}
}
if (wxids.length === 0) {
const rootName = basename(rootPath)
if (rootName.includes('_') && rootName.toLowerCase() !== 'all_users') {
const rootStat = statSync(rootPath)
wxids.push({ wxid: rootName, modifiedTime: rootStat.mtimeMs })
}
}
} catch { }
return wxids.sort((a, b) => {
if (b.modifiedTime !== a.modifiedTime) return b.modifiedTime - a.modifiedTime
return a.wxid.localeCompare(b.wxid)
})
}
/**
* 扫描 wxid 列表
*/

View File

@@ -43,6 +43,7 @@ export class KeyService {
private GetWindowThreadProcessId: any = null
private IsWindowVisible: any = null
private EnumChildWindows: any = null
private PostMessageW: any = null
private WNDENUMPROC_PTR: any = null
// Advapi32
@@ -57,6 +58,7 @@ export class KeyService {
private readonly HKEY_LOCAL_MACHINE = 0x80000002
private readonly HKEY_CURRENT_USER = 0x80000001
private readonly ERROR_SUCCESS = 0
private readonly WM_CLOSE = 0x0010
private getDllPath(): string {
const isPackaged = typeof app !== 'undefined' && app ? app.isPackaged : process.env.NODE_ENV === 'production'
@@ -224,6 +226,7 @@ export class KeyService {
this.EnumWindows = this.user32.func('EnumWindows', 'bool', [this.WNDENUMPROC_PTR, 'intptr_t'])
this.EnumChildWindows = this.user32.func('EnumChildWindows', 'bool', ['void*', this.WNDENUMPROC_PTR, 'intptr_t'])
this.PostMessageW = this.user32.func('PostMessageW', 'bool', ['void*', 'uint32', 'uintptr_t', 'intptr_t'])
this.GetWindowTextW = this.user32.func('GetWindowTextW', 'int', ['void*', this.koffi.out('uint16*'), 'int'])
this.GetWindowTextLengthW = this.user32.func('GetWindowTextLengthW', 'int', ['void*'])
@@ -437,16 +440,60 @@ export class KeyService {
return fallbackPid ?? null
}
private async killWeChatProcesses() {
private async waitForWeChatExit(timeoutMs = 8000): Promise<boolean> {
const start = Date.now()
while (Date.now() - start < timeoutMs) {
const weixinPid = await this.findPidByImageName('Weixin.exe')
const wechatPid = await this.findPidByImageName('WeChat.exe')
if (!weixinPid && !wechatPid) return true
await new Promise(r => setTimeout(r, 400))
}
return false
}
private async closeWeChatWindows(): Promise<boolean> {
if (!this.ensureUser32()) return false
let requested = false
const enumWindowsCallback = this.koffi.register((hWnd: any, lParam: any) => {
if (!this.IsWindowVisible(hWnd)) return true
const title = this.getWindowTitle(hWnd)
const className = this.getClassName(hWnd)
const classLower = (className || '').toLowerCase()
const isWeChatWindow = this.isWeChatWindowTitle(title) || classLower.includes('wechat') || classLower.includes('weixin')
if (!isWeChatWindow) return true
requested = true
try {
await execFileAsync('taskkill', ['/F', '/IM', 'Weixin.exe'])
await execFileAsync('taskkill', ['/F', '/IM', 'WeChat.exe'])
this.PostMessageW?.(hWnd, this.WM_CLOSE, 0, 0)
} catch { }
return true
}, this.WNDENUMPROC_PTR)
this.EnumWindows(enumWindowsCallback, 0)
this.koffi.unregister(enumWindowsCallback)
return requested
}
private async killWeChatProcesses(): Promise<boolean> {
const requested = await this.closeWeChatWindows()
if (requested) {
const gracefulOk = await this.waitForWeChatExit(1500)
if (gracefulOk) return true
}
try {
await execFileAsync('taskkill', ['/F', '/T', '/IM', 'Weixin.exe'])
await execFileAsync('taskkill', ['/F', '/T', '/IM', 'WeChat.exe'])
} catch (e) {
// Ignore if not found
}
await new Promise(r => setTimeout(r, 1000))
return await this.waitForWeChatExit(5000)
}
// --- Window Detection ---
private getWindowTitle(hWnd: any): string {
@@ -605,15 +652,24 @@ export class KeyService {
}
// 2. Restart WeChat
onStatus?.('正在重启微信以进行获取...', 0)
await this.killWeChatProcesses()
onStatus?.('正在关闭微信以进行获取...', 0)
const closed = await this.killWeChatProcesses()
if (!closed) {
const err = '无法自动关闭微信,请手动退出后重试'
onStatus?.(err, 2)
return { success: false, error: err }
}
// 3. Launch
// 3. Launch
onStatus?.('正在启动微信...', 0)
const sub = spawn(wechatPath, { detached: true, stdio: 'ignore' })
const sub = spawn(wechatPath, {
detached: true,
stdio: 'ignore',
cwd: dirname(wechatPath)
})
sub.unref()
// 4. Wait for Window & Get PID (Crucial change: discover PID from window)
// 4. Wait for Window & Get PID (Crucial change: discover PID from window)
onStatus?.('等待微信界面就绪...', 0)
const pid = await this.waitForWeChatWindow()
if (!pid) {

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "weflow",
"version": "1.4.3",
"version": "1.4.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "weflow",
"version": "1.4.2",
"version": "1.4.4",
"hasInstallScript": true,
"dependencies": {
"better-sqlite3": "^12.5.0",

View File

@@ -1,9 +1,13 @@
{
"name": "weflow",
"version": "1.4.3",
"version": "1.4.4",
"description": "WeFlow",
"main": "dist-electron/main.js",
"author": "cc",
"repository": {
"type": "git",
"url": "https://github.com/hicccc77/WeFlow"
},
"//": "二改不应改变此处的作者与应用信息",
"scripts": {
"postinstall": "echo 'No native modules to rebuild'",
@@ -55,6 +59,8 @@
"appId": "com.WeFlow.app",
"publish": {
"provider": "github",
"owner": "hicccc77",
"repo": "WeFlow",
"releaseType": "release"
},
"productName": "WeFlow",

View File

@@ -435,6 +435,58 @@
}
}
.wxid-select {
position: relative;
}
.wxid-dropdown {
position: absolute;
top: calc(100% + 6px);
left: 0;
right: 0;
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 6px;
max-height: 220px;
overflow: auto;
z-index: 20;
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.08);
}
.wxid-option {
width: 100%;
border: none;
background: transparent;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 10px 12px;
border-radius: 8px;
cursor: pointer;
color: var(--text-primary);
font-size: 13px;
&:hover {
background: var(--bg-hover);
}
&.active {
background: var(--primary-light);
}
}
.wxid-name {
font-weight: 600;
}
.wxid-time {
color: var(--text-tertiary);
font-size: 12px;
white-space: nowrap;
}
.field-with-toggle {
position: relative;
}

View File

@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react'
import { useState, useEffect, useRef } from 'react'
import { useNavigate } from 'react-router-dom'
import { useAppStore } from '../stores/appStore'
import { dialog } from '../services/ipc'
@@ -35,6 +35,8 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
const [cachePath, setCachePath] = useState('')
const [wxid, setWxid] = useState('')
const [wxidOptions, setWxidOptions] = useState<Array<{ wxid: string; modifiedTime: number }>>([])
const [showWxidSelect, setShowWxidSelect] = useState(false)
const wxidSelectRef = useRef<HTMLDivElement>(null)
const [error, setError] = useState('')
const [isConnecting, setIsConnecting] = useState(false)
const [isDetectingPath, setIsDetectingPath] = useState(false)
@@ -127,8 +129,22 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
useEffect(() => {
setWxidOptions([])
setWxid('')
setShowWxidSelect(false)
}, [dbPath])
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (!showWxidSelect) return
const target = event.target as Node
if (wxidSelectRef.current && !wxidSelectRef.current.contains(target)) {
setShowWxidSelect(false)
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [showWxidSelect])
const currentStep = steps[stepIndex]
const rootClassName = `welcome-page${isClosing ? ' is-closing' : ''}${standalone ? ' is-standalone' : ''}`
const showWindowControls = standalone
@@ -217,6 +233,28 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
}
}
const handleScanWxidCandidates = async () => {
if (!dbPath) {
setError('请先选择数据库目录')
return
}
if (isScanningWxid) return
setIsScanningWxid(true)
setError('')
try {
const wxids = await window.electronAPI.dbPath.scanWxidCandidates(dbPath)
setWxidOptions(wxids)
setShowWxidSelect(true)
if (!wxids.length) {
setError('未检测到可用的账号目录,请检查路径')
}
} catch (e) {
setError(`扫描失败: ${e}`)
} finally {
setIsScanningWxid(false)
}
}
const handleAutoGetDbKey = async () => {
if (isFetchingDbKey) return
setIsFetchingDbKey(true)
@@ -556,14 +594,35 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
{currentStep.id === 'key' && (
<div className="form-group">
<label className="field-label"> (Wxid)</label>
<div className="wxid-select" ref={wxidSelectRef}>
<input
type="text"
className="field-input"
placeholder="等待获取..."
placeholder="点击选择..."
value={wxid}
readOnly
onClick={handleScanWxidCandidates}
onChange={(e) => setWxid(e.target.value)}
/>
{showWxidSelect && wxidOptions.length > 0 && (
<div className="wxid-dropdown">
{wxidOptions.map((opt) => (
<button
key={opt.wxid}
type="button"
className={`wxid-option ${opt.wxid === wxid ? 'active' : ''}`}
onClick={() => {
setWxid(opt.wxid)
setShowWxidSelect(false)
}}
>
<span className="wxid-name">{opt.wxid}</span>
<span className="wxid-time">{formatModifiedTime(opt.modifiedTime)}</span>
</button>
))}
</div>
)}
</div>
<label className="field-label mt-4"></label>
<div className="field-with-toggle">
@@ -733,4 +792,3 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
}
export default WelcomePage

View File

@@ -42,6 +42,7 @@ export interface ElectronAPI {
dbPath: {
autoDetect: () => Promise<{ success: boolean; path?: string; error?: string }>
scanWxids: (rootPath: string) => Promise<WxidInfo[]>
scanWxidCandidates: (rootPath: string) => Promise<WxidInfo[]>
getDefault: () => Promise<string>
}
wcdb: {