feat: 抽离保留进程功能到设置里

This commit is contained in:
Wxw-Gu
2026-09-14 16:44:53 +08:00
parent cf9ea8a83b
commit 550e3b104c
5 changed files with 83 additions and 64 deletions
@@ -9,15 +9,7 @@ import type {
PersonalWechatRuntimeProgressEvent, PersonalWechatRuntimeProgressEvent,
PersonalWechatRuntimeStatus PersonalWechatRuntimeStatus
} from '../../../../shared/personal-wechat-runtime' } from '../../../../shared/personal-wechat-runtime'
import { import { Button, Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '../ui'
Button,
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
Switch
} from '../ui'
import { isMac, isWindows } from '../../utils/runtime-environment' import { isMac, isWindows } from '../../utils/runtime-environment'
import { PersonalWechatChatComposer, type ChatMessage } from './PersonalWechatChatComposer' import { PersonalWechatChatComposer, type ChatMessage } from './PersonalWechatChatComposer'
import { PersonalWechatSetupGuide } from './PersonalWechatSetupGuide' import { PersonalWechatSetupGuide } from './PersonalWechatSetupGuide'
@@ -89,7 +81,6 @@ function PersonalWechatMacSendDialog({
const [sendError, setSendError] = useState<string | null>(null) const [sendError, setSendError] = useState<string | null>(null)
const [voiceDiagnostic, setVoiceDiagnostic] = useState<PersonalWechatVoiceDiagnostic | null>(null) const [voiceDiagnostic, setVoiceDiagnostic] = useState<PersonalWechatVoiceDiagnostic | null>(null)
const [voiceDiagnosticOpen, setVoiceDiagnosticOpen] = useState(false) const [voiceDiagnosticOpen, setVoiceDiagnosticOpen] = useState(false)
const [keepOneBotProcess, setKeepOneBotProcess] = useState(false)
const requestIdRef = useRef(0) const requestIdRef = useRef(0)
const restoreFocusRef = useRef<HTMLElement | null>(null) const restoreFocusRef = useRef<HTMLElement | null>(null)
const closingRef = useRef(false) const closingRef = useRef(false)
@@ -155,36 +146,6 @@ function PersonalWechatMacSendDialog({
return () => window.clearInterval(timer) return () => window.clearInterval(timer)
}, [refreshStatus, senderStatus]) }, [refreshStatus, senderStatus])
useEffect(() => {
if (!isMac) return undefined
let active = true
const readKeepProcess = window.api.getPersonalWechatKeepOneBotProcess
if (!readKeepProcess) return undefined
void readKeepProcess().then((keep) => {
if (active && typeof keep === 'boolean') setKeepOneBotProcess(keep)
})
return () => {
active = false
}
}, [])
const handleKeepOneBotProcessChange = async (keep: boolean): Promise<void> => {
if (!isMac) return
const saveKeepProcess = window.api.setPersonalWechatKeepOneBotProcess
if (!saveKeepProcess) {
setSendError('请重启 TraceMemo 后再使用“保留 OneBot 进程”')
return
}
setKeepOneBotProcess(keep)
try {
const saved = await saveKeepProcess(keep)
if (typeof saved === 'boolean') setKeepOneBotProcess(saved)
} catch (error) {
setKeepOneBotProcess(!keep)
setSendError(error instanceof Error ? error.message : String(error))
}
}
const handleDownloadRuntime = async (): Promise<void> => { const handleDownloadRuntime = async (): Promise<void> => {
if (runtimeBusy) return if (runtimeBusy) return
setRuntimeBusy(true) setRuntimeBusy(true)
@@ -399,34 +360,14 @@ function PersonalWechatMacSendDialog({
)} )}
</div> </div>
{setupReady && isMac && ( {setupReady && isMac && (
<div className="personal-wechat-chat-footer flex items-center justify-between"> <div className="personal-wechat-chat-footer flex items-center justify-end">
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<span>保留 OneBot 进程</span>
<Switch
checked={keepOneBotProcess}
onCheckedChange={(checked) => void handleKeepOneBotProcessChange(checked)}
aria-label="保留 OneBot 进程"
/>
</div>
<Button variant="link" size="sm" onClick={() => void handleOpenVoiceDiagnostic()}> <Button variant="link" size="sm" onClick={() => void handleOpenVoiceDiagnostic()}>
语音发送诊断 语音发送诊断
</Button> </Button>
</div> </div>
)} )}
{!setupReady && ( {!setupReady && (
<div <div className="personal-wechat-chat-footer flex items-center justify-end">
className={`personal-wechat-chat-footer flex items-center ${isMac ? 'justify-between' : 'justify-end'}`}
>
{isMac && (
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<span>保留 OneBot 进程</span>
<Switch
checked={keepOneBotProcess}
onCheckedChange={(checked) => void handleKeepOneBotProcessChange(checked)}
aria-label="保留 OneBot 进程"
/>
</div>
)}
<Button variant="outline" onClick={handleClose} disabled={isBusy}> <Button variant="outline" onClick={handleClose} disabled={isBusy}>
关闭 关闭
</Button> </Button>
@@ -15,7 +15,8 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
Input, Input,
Skeleton Skeleton,
Switch
} from '../../../components/ui' } from '../../../components/ui'
import { PersonalWechatSetupGuide } from '../../../components/chat/PersonalWechatSetupGuide' import { PersonalWechatSetupGuide } from '../../../components/chat/PersonalWechatSetupGuide'
import { PersonalWechatSupportedVersionsContent } from '../../../components/chat/PersonalWechatSupportedVersionsContent' import { PersonalWechatSupportedVersionsContent } from '../../../components/chat/PersonalWechatSupportedVersionsContent'
@@ -88,6 +89,8 @@ export function PersonalWechatSendPage({
const [windowsPortInput, setWindowsPortInput] = useState('') const [windowsPortInput, setWindowsPortInput] = useState('')
const [windowsDetectedPort, setWindowsDetectedPort] = useState('') const [windowsDetectedPort, setWindowsDetectedPort] = useState('')
const [windowsEndpointBusy, setWindowsEndpointBusy] = useState(false) const [windowsEndpointBusy, setWindowsEndpointBusy] = useState(false)
const [keepOneBotProcess, setKeepOneBotProcess] = useState(false)
const [keepProcessBusy, setKeepProcessBusy] = useState(false)
const [showWechatVersions, setShowWechatVersions] = useState(false) const [showWechatVersions, setShowWechatVersions] = useState(false)
const runtimeVersionsTriggerRef = useRef<HTMLButtonElement | null>(null) const runtimeVersionsTriggerRef = useRef<HTMLButtonElement | null>(null)
const personalWechatRuntimeSupported = isMac && Boolean(runtimeStatus?.supported) const personalWechatRuntimeSupported = isMac && Boolean(runtimeStatus?.supported)
@@ -128,6 +131,21 @@ export function PersonalWechatSendPage({
return () => unsubscribe?.() return () => unsubscribe?.()
}, [refresh]) }, [refresh])
useEffect(() => {
if (!isMac) return undefined
let active = true
const readKeepProcess = window.api.getPersonalWechatKeepOneBotProcess
if (typeof readKeepProcess !== 'function') return undefined
void readKeepProcess()
.then((keep) => {
if (active && typeof keep === 'boolean') setKeepOneBotProcess(keep)
})
.catch(() => undefined)
return () => {
active = false
}
}, [])
useEffect(() => { useEffect(() => {
if (isWindows || !senderStatus || senderStatus.canSend || senderStatus.state === 'error') { if (isWindows || !senderStatus || senderStatus.canSend || senderStatus.state === 'error') {
return undefined return undefined
@@ -219,6 +237,26 @@ export function PersonalWechatSendPage({
} }
} }
const handleKeepOneBotProcessChange = async (keep: boolean): Promise<void> => {
if (!isMac || keepProcessBusy) return
const saveKeepProcess = window.api.setPersonalWechatKeepOneBotProcess
if (typeof saveKeepProcess !== 'function') {
onNotice('请重启 TraceMemo 后再使用“保留 OneBot 进程”')
return
}
setKeepProcessBusy(true)
setKeepOneBotProcess(keep)
try {
const saved = await saveKeepProcess(keep)
if (typeof saved === 'boolean') setKeepOneBotProcess(saved)
} catch (reason) {
setKeepOneBotProcess(!keep)
onNotice(reason instanceof Error ? reason.message : String(reason))
} finally {
setKeepProcessBusy(false)
}
}
const bindWechat = async (): Promise<void> => { const bindWechat = async (): Promise<void> => {
if (binding) return if (binding) return
setBinding(true) setBinding(true)
@@ -578,6 +616,20 @@ export function PersonalWechatSendPage({
/> />
</div> </div>
) : null} ) : null}
<div className="col-span-full flex items-center justify-between gap-4 border-t border-border-subtle pt-4">
<div>
<strong className="block text-sm">保留 OneBot 进程</strong>
<p className="mt-1 text-xs text-muted-foreground">
退出 TraceMemo 后继续保留进程,重新打开时可以复用。
</p>
</div>
<Switch
checked={keepOneBotProcess}
disabled={keepProcessBusy}
onCheckedChange={(checked) => void handleKeepOneBotProcessChange(checked)}
aria-label="保留 OneBot 进程"
/>
</div>
</section> </section>
<h2 className="settings-section-heading">配置状态</h2> <h2 className="settings-section-heading">配置状态</h2>
@@ -143,6 +143,7 @@ describe('PersonalWechatSendDialog', () => {
renderDialog() renderDialog()
await startComposer() await startComposer()
expect(screen.getByRole('dialog')).toHaveTextContent('文字转语音') expect(screen.getByRole('dialog')).toHaveTextContent('文字转语音')
expect(screen.queryByRole('switch', { name: '保留 OneBot 进程' })).not.toBeInTheDocument()
expect(screen.queryByText('验证消息能力')).not.toBeInTheDocument() expect(screen.queryByText('验证消息能力')).not.toBeInTheDocument()
expect(screen.getByRole('button', { name: '生成语音' })).toBeDisabled() expect(screen.getByRole('button', { name: '生成语音' })).toBeDisabled()
}) })
@@ -19,6 +19,8 @@ const cancelRuntimeDownload = vi.fn()
const removeRuntime = vi.fn() const removeRuntime = vi.fn()
const openRuntimeDirectory = vi.fn() const openRuntimeDirectory = vi.fn()
const rebindSender = vi.fn() const rebindSender = vi.fn()
const getKeepOneBotProcess = vi.fn()
const setKeepOneBotProcess = vi.fn()
const senderStatus = { const senderStatus = {
state: 'online' as const, state: 'online' as const,
@@ -88,6 +90,8 @@ describe('PersonalWechatSendPage on macOS', () => {
}) })
openRuntimeDirectory.mockReset().mockResolvedValue({ success: true }) openRuntimeDirectory.mockReset().mockResolvedValue({ success: true })
rebindSender.mockReset().mockResolvedValue(senderStatus) rebindSender.mockReset().mockResolvedValue(senderStatus)
getKeepOneBotProcess.mockReset().mockResolvedValue(false)
setKeepOneBotProcess.mockReset().mockImplementation(async (keep: boolean) => keep)
vi.spyOn(window, 'confirm').mockReturnValue(true) vi.spyOn(window, 'confirm').mockReturnValue(true)
Object.defineProperty(window, 'api', { Object.defineProperty(window, 'api', {
configurable: true, configurable: true,
@@ -100,7 +104,9 @@ describe('PersonalWechatSendPage on macOS', () => {
cancelPersonalWechatRuntimeDownload: cancelRuntimeDownload, cancelPersonalWechatRuntimeDownload: cancelRuntimeDownload,
removePersonalWechatRuntime: removeRuntime, removePersonalWechatRuntime: removeRuntime,
openPersonalWechatRuntimeDirectory: openRuntimeDirectory, openPersonalWechatRuntimeDirectory: openRuntimeDirectory,
rebindPersonalWechatSender: rebindSender rebindPersonalWechatSender: rebindSender,
getPersonalWechatKeepOneBotProcess: getKeepOneBotProcess,
setPersonalWechatKeepOneBotProcess: setKeepOneBotProcess
} }
}) })
}) })
@@ -123,6 +129,18 @@ describe('PersonalWechatSendPage on macOS', () => {
expect(versionsDialog).toHaveTextContent('4.1.11.53') expect(versionsDialog).toHaveTextContent('4.1.11.53')
}) })
it('keeps the OneBot process setting on the macOS WeChat send page', async () => {
const user = userEvent.setup()
render(<PersonalWechatSendPage onNotice={vi.fn()} />)
const keepProcess = await screen.findByRole('switch', { name: '保留 OneBot 进程' })
expect(keepProcess).not.toBeChecked()
await user.click(keepProcess)
await waitFor(() => expect(setKeepOneBotProcess).toHaveBeenCalledWith(true))
expect(keepProcess).toBeChecked()
})
it('downloads a missing macOS runtime from the send page', async () => { it('downloads a missing macOS runtime from the send page', async () => {
getRuntimeStatus.mockResolvedValue({ getRuntimeStatus.mockResolvedValue({
...readyRuntime, ...readyRuntime,
@@ -191,4 +191,11 @@ describe('PersonalWechatSendPage on Windows', () => {
expect(screen.queryByText(/OneBot/i)).not.toBeInTheDocument() expect(screen.queryByText(/OneBot/i)).not.toBeInTheDocument()
expect(screen.queryByText('微信发送组件')).not.toBeInTheDocument() expect(screen.queryByText('微信发送组件')).not.toBeInTheDocument()
}) })
it('does not show the macOS process setting on the Windows WeChat send page', async () => {
render(<PersonalWechatSendPage onNotice={vi.fn()} />)
expect(await screen.findByRole('spinbutton', { name: '微信发送能力端口' })).toBeVisible()
expect(screen.queryByRole('switch', { name: '保留 OneBot 进程' })).not.toBeInTheDocument()
})
}) })