feat: 优化 macOS 微信发送消息能力

参考 wechat_chatter PR #36:
https://github.com/yincongcyincong/wechat_chatter/pull/36

补充 WeChat 4.1.11.53 的 GetService / CdnManager 地址,
并适配绑定后的发送能力检测逻辑。
This commit is contained in:
Wxw-Gu
2026-09-14 15:59:08 +08:00
parent 5efae86fcf
commit cf9ea8a83b
14 changed files with 659 additions and 171 deletions
@@ -19,10 +19,7 @@ import {
Switch
} from '../ui'
import { isMac, isWindows } from '../../utils/runtime-environment'
import {
PersonalWechatChatComposer,
type ChatMessage
} from './PersonalWechatChatComposer'
import { PersonalWechatChatComposer, type ChatMessage } from './PersonalWechatChatComposer'
import { PersonalWechatSetupGuide } from './PersonalWechatSetupGuide'
import { PersonalWechatVoiceDiagnosticDialog } from './PersonalWechatVoiceDiagnosticDialog'
import { PersonalWechatWindowsSendDialog } from './PersonalWechatWindowsSendDialog'
@@ -86,7 +83,6 @@ function PersonalWechatMacSendDialog({
const [binding, setBinding] = useState(false)
const [runtimeBusy, setRuntimeBusy] = useState(false)
const [sendBusy, setSendBusy] = useState(false)
const [detectionAttempted, setDetectionAttempted] = useState(false)
// 状态可能来自之前的 OneBot 进程或日志;发送入口只信任当前语音能力状态。
const [sessionBound, setSessionBound] = useState(false)
const [messages, setMessages] = useState<ChatMessage[]>([])
@@ -144,6 +140,21 @@ function PersonalWechatMacSendDialog({
return unsubscribe
}, [refreshStatus])
useEffect(() => {
if (!senderStatus || senderStatus.canSend || senderStatus.state === 'error') return undefined
const bound =
senderStatus.state === 'online' ||
Boolean(
senderStatus.wechatPid &&
senderStatus.boundWechatPid === senderStatus.wechatPid &&
senderStatus.attachReady &&
senderStatus.baseAddressReady
)
if (!bound) return undefined
const timer = window.setInterval(() => void refreshStatus(), 1_000)
return () => window.clearInterval(timer)
}, [refreshStatus, senderStatus])
useEffect(() => {
if (!isMac) return undefined
let active = true
@@ -193,7 +204,6 @@ function PersonalWechatMacSendDialog({
const handleBind = async (): Promise<void> => {
if (binding) return
setBinding(true)
setDetectionAttempted(false)
setSendError(null)
try {
const nextStatus = await window.api.rebindPersonalWechatSender()
@@ -207,7 +217,7 @@ function PersonalWechatMacSendDialog({
nextStatus.baseAddressReady
)
)
if (nextStatus.state !== 'online' && nextStatus.message) setSendError(nextStatus.message)
if (nextStatus.state === 'error' && nextStatus.message) setSendError(nextStatus.message)
} catch (error) {
setSendError(error instanceof Error ? error.message : String(error))
} finally {
@@ -215,11 +225,6 @@ function PersonalWechatMacSendDialog({
}
}
const handleDetect = async (): Promise<void> => {
setDetectionAttempted(true)
await refreshStatus()
}
const handleClose = (): void => {
if (isBusy || closingRef.current) return
closingRef.current = true
@@ -235,9 +240,7 @@ function PersonalWechatMacSendDialog({
onOpenTextToSpeechSettings()
}
const handleSend = async (
filePath: string
): Promise<{ success: boolean; error?: string }> => {
const handleSend = async (filePath: string): Promise<{ success: boolean; error?: string }> => {
setSendBusy(true)
setSendError(null)
try {
@@ -364,8 +367,6 @@ function PersonalWechatMacSendDialog({
sessionBound={sessionBound}
onDownloadRuntime={() => void handleDownloadRuntime()}
onBind={() => void handleBind()}
detectionAttempted={detectionAttempted}
onDetect={() => void handleDetect()}
onStartSending={() => undefined}
onOpenTextToSpeechSettings={handleOpenSettings}
/>
@@ -15,18 +15,16 @@ interface PersonalWechatSetupGuideProps {
runtimeBusy: boolean
binding: boolean
detecting: boolean
detectionAttempted: boolean
sessionBound: boolean
onDownloadRuntime: () => void
onBind: () => void
onDetect: () => void
onStartSending: () => void
onOpenTextToSpeechSettings?: () => void
}
function capabilityLabel(ready: boolean, detectionAttempted: boolean): string {
function capabilityLabel(ready: boolean, initializing: boolean): string {
if (ready) return '已就绪'
return detectionAttempted ? '未检测' : '待检测'
return initializing ? '初始化中' : '未就绪'
}
function diagnosticValue(value: unknown): string {
@@ -47,9 +45,7 @@ function bindingHint(status: PersonalWechatSenderStatus): string {
}
function isWechatBound(status: PersonalWechatSenderStatus): boolean {
if (status.state === 'online') return true
return Boolean(
status.state === 'hook_not_ready' &&
status.wechatPid &&
status.boundWechatPid === status.wechatPid &&
status.attachReady &&
@@ -64,11 +60,9 @@ export function PersonalWechatSetupGuide({
runtimeBusy,
binding,
detecting,
detectionAttempted,
sessionBound,
onDownloadRuntime,
onBind,
onDetect,
onStartSending,
onOpenTextToSpeechSettings
}: PersonalWechatSetupGuideProps): React.ReactElement {
@@ -78,9 +72,8 @@ export function PersonalWechatSetupGuide({
const runtimeDownloading = runtimeBusy || runtimeStatus?.state === 'downloading'
const connected = sessionBound && (senderStatus ? isWechatBound(senderStatus) : false)
const canSendVoice = Boolean(senderStatus?.canSendVoice)
const detectedVoiceReady = detectionAttempted && canSendVoice
const verificationComplete = connected && detectedVoiceReady
const allReady = verificationComplete
const initializing = connected && !canSendVoice && senderStatus?.state !== 'error'
const allReady = connected && Boolean(senderStatus?.canSend)
const progress = runtimeProgress || runtimeStatus
const progressPercent = Math.max(0, Math.min(100, Math.round((progress?.progress || 0) * 100)))
@@ -181,44 +174,38 @@ export function PersonalWechatSetupGuide({
</div>
</li>
<li
className={verificationComplete ? 'is-complete' : connected ? 'is-current' : 'is-pending'}
>
<span className="personal-wechat-step-number">{verificationComplete ? '✓' : '3'}</span>
<li className={allReady ? 'is-complete' : connected ? 'is-current' : 'is-pending'}>
<span className="personal-wechat-step-number">{allReady ? '✓' : '3'}</span>
<div className="personal-wechat-step-content">
<strong>验证消息能力</strong>
<p>请打开微信,可以给任意好友发送一张图片,完成一次能力初始化。</p>
<p className="personal-wechat-step-hint">完成后点击“重新检测”。</p>
{connected && !verificationComplete && (
<Button
size="sm"
variant="outline"
onClick={onDetect}
disabled={detecting || binding}
>
{detecting ? '正在检测…' : '重新检测'}
</Button>
)}
{detectionAttempted && !detecting && !verificationComplete && (
<strong>初始化发送能力</strong>
<p>绑定微信后,TraceMemo 会自动完成文字、图片和语音发送所需的初始化。</p>
{initializing && (
<p className="personal-wechat-step-hint" role="status">
暂未检测到语音发送能力,请确认微信已完成初始化后再次检测。
{detecting ? '正在初始化发送能力…' : '正在等待发送运行时完成初始化…'}
</p>
)}
{senderStatus?.state === 'error' && (
<p className="personal-wechat-step-hint" role="status">
{senderStatus.message || '发送能力初始化失败,请重新绑定。'}
</p>
)}
</div>
</li>
<li
className={allReady ? 'is-complete' : verificationComplete ? 'is-current' : 'is-pending'}
>
<li className={allReady ? 'is-complete' : initializing ? 'is-current' : 'is-pending'}>
<span className="personal-wechat-step-number">{allReady ? '✓' : '4'}</span>
<div className="personal-wechat-step-content">
<strong>能力检测</strong>
<div className="personal-wechat-capabilities" aria-label="微信消息能力">
{[['语音消息', detectedVoiceReady]].map(([label, ready]) => (
{[
['文字消息', Boolean(senderStatus?.canSendText)],
['图片消息', Boolean(senderStatus?.canSendImage)],
['语音消息', canSendVoice]
].map(([label, ready]) => (
<span key={String(label)} className={ready ? 'is-ready' : ''}>
<b aria-hidden>{ready ? '✓' : '−'}</b>
{label}
<small>{capabilityLabel(Boolean(ready), detectionAttempted)}</small>
<small>{capabilityLabel(Boolean(ready), initializing)}</small>
</span>
))}
</div>
@@ -229,15 +216,10 @@ export function PersonalWechatSetupGuide({
开始发送
</Button>
</>
) : verificationComplete ? (
<>
<p className="personal-wechat-step-hint">
语音能力还未就绪,请在微信中完成一次媒体消息初始化后重新检测。
</p>
<Button size="sm" variant="outline" onClick={onDetect} disabled={detecting}>
{detecting ? '正在检测…' : '重新检测能力'}
</Button>
</>
) : initializing ? (
<p className="personal-wechat-step-hint" role="status">
发送能力准备完成后会自动进入已就绪状态。
</p>
) : null}
</div>
</li>
@@ -113,8 +113,8 @@ const capabilityCopy = (capability: PersonalWechatSendCapability | null): string
case 'needs_binding':
case 'unconfigured':
return '请先绑定个人微信'
case 'needs_verification':
return '请先完成微信消息能力检测'
case 'initializing':
return '微信发送能力正在初始化'
default:
return '微信发送能力异常'
}
@@ -598,8 +598,9 @@ export function ScheduledReportsWorkspace({
const [editingTask, setEditingTask] = React.useState<ScheduledReportTask | null>(null)
const [saving, setSaving] = React.useState(false)
const [deletingTask, setDeletingTask] = React.useState<ScheduledReportTask | null>(null)
const [retryingExecution, setRetryingExecution] =
React.useState<ScheduledReportExecution | null>(null)
const [retryingExecution, setRetryingExecution] = React.useState<ScheduledReportExecution | null>(
null
)
const [busyTaskId, setBusyTaskId] = React.useState<string | null>(null)
const load = React.useCallback(async (): Promise<void> => {
@@ -917,11 +918,11 @@ export function ScheduledReportsWorkspace({
</span>
{(capability?.status === 'needs_binding' ||
capability?.status === 'unconfigured' ||
capability?.status === 'needs_verification' ||
capability?.status === 'initializing' ||
capability?.status === 'error' ||
capabilityError) && (
<Button variant="link" size="sm" className="ml-auto" onClick={onOpenWechatSettings}>
{capability?.status === 'needs_verification' ? '去检测' : '去配置'}
去配置
</Button>
)}
</div>
@@ -39,7 +39,7 @@ const capabilityLabel: Record<PersonalWechatSendCapability['status'], string> =
unsupported: '暂不支持',
unconfigured: '尚未配置',
needs_binding: '需要绑定',
needs_verification: '需要检测',
initializing: '初始化中',
ready: '已就绪',
error: '异常'
}
@@ -81,7 +81,6 @@ export function PersonalWechatSendPage({
const [runtimeBusy, setRuntimeBusy] = useState(false)
const [binding, setBinding] = useState(false)
const [detecting, setDetecting] = useState(false)
const [detectionAttempted, setDetectionAttempted] = useState(false)
const [error, setError] = useState('')
const [windowsSenderStatus, setWindowsSenderStatus] = useState<PersonalWechatSenderStatus | null>(
null
@@ -129,6 +128,15 @@ export function PersonalWechatSendPage({
return () => unsubscribe?.()
}, [refresh])
useEffect(() => {
if (isWindows || !senderStatus || senderStatus.canSend || senderStatus.state === 'error') {
return undefined
}
if (!boundToCurrentWechat(senderStatus)) return undefined
const timer = window.setInterval(() => void refresh(), 1_000)
return () => window.clearInterval(timer)
}, [refresh, senderStatus])
useEffect(() => {
if (!isWindows) return
let active = true
@@ -215,13 +223,12 @@ export function PersonalWechatSendPage({
if (binding) return
setBinding(true)
setError('')
setDetectionAttempted(false)
try {
const nextStatus = await window.api.rebindPersonalWechatSender()
setSenderStatus(nextStatus)
const nextCapability = await window.api.getPersonalWechatSendCapability()
setCapability(nextCapability)
if (nextStatus.state !== 'online' && nextStatus.message) setError(nextStatus.message)
if (nextStatus.state === 'error' && nextStatus.message) setError(nextStatus.message)
} catch (reason) {
setError(reason instanceof Error ? reason.message : '个人微信绑定失败')
} finally {
@@ -236,7 +243,6 @@ export function PersonalWechatSendPage({
}
if (detecting) return
setDetecting(true)
setDetectionAttempted(true)
try {
await refresh()
} finally {
@@ -324,7 +330,7 @@ export function PersonalWechatSendPage({
: windowsSenderStatus?.state === 'error' && windowsSenderStatus.endpointReady
? 'error'
: windowsPortInput.trim()
? 'needs_verification'
? 'initializing'
: 'unconfigured'
: status
const pageReady = isWindows ? Boolean(windowsSenderStatus?.canSend) : ready
@@ -374,7 +380,7 @@ export function PersonalWechatSendPage({
</p>
</div>
<Button variant="outline" size="sm" onClick={() => void detectCapability()}>
{detecting ? '检测中…' : '重新检测'}
{detecting ? '刷新中…' : isWindows ? '重新检测' : '刷新状态'}
</Button>
</div>
<div className="mt-5 grid grid-cols-3 gap-2" aria-label="微信发送能力明细">
@@ -574,7 +580,7 @@ export function PersonalWechatSendPage({
) : null}
</section>
<h2 className="settings-section-heading">配置与检测</h2>
<h2 className="settings-section-heading">配置状态</h2>
<PersonalWechatSetupGuide
runtimeStatus={runtimeStatus}
senderStatus={senderStatus}
@@ -582,11 +588,9 @@ export function PersonalWechatSendPage({
runtimeBusy={runtimeBusy}
binding={binding}
detecting={detecting}
detectionAttempted={detectionAttempted}
sessionBound={boundToCurrentWechat(senderStatus)}
onDownloadRuntime={() => void downloadRuntime()}
onBind={() => void bindWechat()}
onDetect={() => void detectCapability()}
onStartSending={() =>
onNotice('微信消息发送能力已就绪,请在档案中选择会话开始发送。')
}