MacOS 睡眠 微信的自动控制(优雅退出 + 自动登录)
z 2026-03-20
随笔
一直有个挺烦人的问题:
👉 Mac 上登录微信后,手机设置免提醒。但电脑睡眠之后手机基本就收不到通知了。
尤其是合上电脑盖子或睡眠之后,人已经走了,结果手机也不提醒,容易错过消息。
折腾了一下,搞了个比较稳定的自动方案:Mac 睡眠自动退出微信,唤醒自动恢复(稳定方案),效果还不错,记录一下。
# 问题本质
WeChat 的机制是:
PC 在线 → 手机不推送
PC 离线 → 手机恢复推送
1
2
2
所以核心思路其实很简单:
Mac 睡眠 → 让微信下线
Mac 唤醒 → 再自动打开微信
1
2
2
# 为什么不用 sleepwatcher
一开始试的是 sleepwatcher:
brew install sleepwatcher
1
确实能用,但有个问题:官方好久不更新了,就怕万一哪天系统升级或更新等就失效了。
# 最终方案:Hammerspoon(推荐)
用的是:Hammerspoon
它是基于 macOS 系统事件的自动化工具,比 sleepwatcher 稳很多。
# 实现效果
最终效果:
Mac 合盖 / 睡眠 → 自动退出微信 → 手机恢复通知 ✅
Mac 唤醒 → 自动打开微信(带重试)✅
1
2
2
基本无感使用。
如果你不想使用了,就把 Hammerspoon 这个软件退出就行。想用了,再重新打开即可。
# 安装
brew install --cask hammerspoon
1
启动后,编辑配置文件:
vim ~/.hammerspoon/init.lua
1
# 核心脚本
-- =============================================================
-- Mac睡眠 WeChat 自动控制(优雅退出 + 自动登录)
-- =============================================================
-- 启用 IPC,便于通过命令行 `hs -c "..."` 调试/探测(可保留,无副作用)
require("hs.ipc")
local APP_NAME = "WeChat"
local WECHAT_BUNDLE_ID = "com.tencent.xinWeChat"
local LOG_FILE = "/tmp/wechat_sleep.log"
-- ================================
-- 日志系统
-- ================================
local function logger(level, msg)
local f = io.open(LOG_FILE, "a")
if f then
f:write(os.date("%Y-%m-%d %H:%M:%S") .. " [" .. level .. "] " .. msg .. "\n")
f:close()
end
end
-- ================================
-- 工具方法
-- ================================
local function isWeChatRunning()
-- 方法 1: 通过 Hammerspoon 应用实例
local apps = hs.application.applicationsForBundleID(WECHAT_BUNDLE_ID)
if #apps > 0 then
return true
end
-- 方法 2: 通过系统进程
local ok, result, status = hs.execute("pgrep -x WeChat")
if ok and result and result:match("%d+") then
return true
end
return false
end
-- 获取微信 app 实例(统一用 bundleID;微信显示名是「微信」,
-- hs.application.get("WeChat") 会返回 nil,甚至误匹配到「企业微信」)
local function getWeChatApp()
return hs.application.applicationsForBundleID(WECHAT_BUNDLE_ID)[1]
end
-- ================================
-- 方案 A: UI 菜单点击退出(优雅退出)
-- ================================
local function tryMenuQuit()
logger("INFO", "尝试方案 A: UI 菜单点击...")
local script = [[
tell application "System Events"
tell process "WeChat"
try
click menu item "退出微信" of menu "微信" of menu bar 1
return true
on error
try
click menu item "Quit WeChat" of menu "WeChat" of menu bar 1
return true
on error
return false
end try
end try
end tell
end tell
]]
local ok, result = hs.applescript.applescript(script)
if ok and result then
logger("ACTION", "方案 A 已触发菜单退出")
return true
else
logger("WARN", "方案 A 菜单点击失败")
return false
end
end
-- ================================
-- 方案 B: Cmd+Q 兜底
-- ================================
local function sendCmdQ()
local app = getWeChatApp()
if app then
app:activate()
app:unhide()
hs.timer.usleep(500000)
hs.eventtap.keyStroke({"cmd"}, "q", 0, app)
logger("ACTION", "方案 B 已发送 Cmd+Q")
else
logger("ERROR", "未获取到微信实例,Cmd+Q 失败")
end
end
-- ================================
-- 优雅退出流程
-- ================================
local function quitWeChatGracefully()
if not isWeChatRunning() then
return true
end
logger("ACTION", "开始优雅退出流程")
local app = getWeChatApp()
if app then
app:activate()
app:unhide()
hs.timer.usleep(500000)
end
local ok = tryMenuQuit()
if ok then
logger("ACTION", "方案 A 菜单点击已触发(退出成功)")
else
logger("WARN", "方案 A 菜单点击失败,尝试方案 B (Cmd+Q)...")
sendCmdQ()
end
-- 不再认为超时为 ERROR,改为 WARN 并保留实际状态
for i = 1, 10 do
if not isWeChatRunning() then
logger("INFO", "微信已退出")
return true
end
hs.timer.usleep(300000)
end
logger("WARN", "微信仍在运行,可能被后台拦截(无需强制退出)")
return false
end
-- ================================
-- 启动微信
-- ================================
local function startWeChat()
logger("INFO", "尝试启动微信...")
-- 尝试 launchOrFocus
hs.application.launchOrFocus(APP_NAME)
local maxWait = 5 -- 最多轮询 5 次
local i = 0
while i < maxWait do
if isWeChatRunning() then
logger("ACTION", "微信启动成功(launchOrFocus)")
return true
end
hs.timer.usleep(1000000) -- 每 0.5 秒检查一次
i = i + 1
end
-- fallback: open -a 命令
logger("WARN", "launchOrFocus 启动失败,尝试 open -a 命令")
hs.execute('open -a "WeChat"')
i = 0
while i < maxWait do
if isWeChatRunning() then
logger("ACTION", "微信启动成功(open 命令)")
return true
end
hs.timer.usleep(1000000)
i = i + 1
end
-- 最终仍未启动,才算失败
logger("ERROR", "微信启动失败")
return false
end
-- ================================
-- 自动登录:确认「进入微信」
-- ================================
-- 通过窗口尺寸特征查找登录确认窗口(竖向小窗 ~280x380,区别于主界面大窗)
-- 注意:app 实例统一用 getWeChatApp()(bundleID)。微信显示名是「微信」,
-- 用 hs.application.get("WeChat") 会返回 nil,甚至误匹配到「企业微信」。
local function findLoginWindow()
local app = getWeChatApp()
if not app then return nil end
for _, win in ipairs(app:allWindows()) do
local f = win:frame()
if win:isVisible() and f.w > 0 and f.w < 450 and f.h < 600 then
return win
end
end
return nil
end
-- 点击「进入微信」:实测微信登录窗是自绘 UI,合成的 AXPress 与 AppleScript
-- click button 都无效,只有真实鼠标点击有效。按钮中心实测位于窗口水平 50%、垂直 75.5%。
local function clickEnterButton(win)
local app = getWeChatApp()
if app then app:activate() end
hs.timer.usleep(400000)
local f = win:frame()
local x = f.x + f.w * 0.5
local y = f.y + f.h * 0.755
hs.eventtap.leftClick({ x = x, y = y })
logger("ACTION", string.format("自动登录:已点击「进入微信」坐标 (%.0f,%.0f)", x, y))
end
-- 主方案:模拟回车键(「进入微信」是默认按钮,回车即可确认;不依赖坐标,最鲁棒)
local function pressReturn()
local app = getWeChatApp()
if app then app:activate() end
hs.timer.usleep(300000)
hs.eventtap.keyStroke({}, "return", 0)
logger("ACTION", "自动登录:已发送回车键")
end
-- 自动确认登录主流程:检测登录窗 → 坐标点击 → 轮询等待登录成功(含网络延迟)
local function confirmWeChatLogin()
-- 1. 等待登录确认窗口出现(冷启动渲染需要时间),最多约 8 秒
local win = nil
for i = 1, 16 do
win = findLoginWindow()
if win then break end
hs.timer.usleep(500000)
end
if not win then
logger("INFO", "未检测到登录确认窗口(可能已自动登录或仍在加载),跳过自动点击")
return
end
logger("ACTION", "检测到登录确认窗口,开始自动「进入微信」")
-- 2. 主方案:回车键(不依赖坐标,最鲁棒),轮询等待登录窗消失(含网络延迟,最多约 10 秒)
pressReturn()
for i = 1, 20 do
hs.timer.usleep(500000)
if not findLoginWindow() then
logger("ACTION", "自动登录成功(回车键)")
return
end
end
-- 3. 兜底:坐标点击,再等约 6 秒
logger("WARN", "回车键后登录窗仍在,尝试坐标点击兜底")
clickEnterButton(win)
for i = 1, 12 do
hs.timer.usleep(500000)
if not findLoginWindow() then
logger("ACTION", "自动登录成功(坐标点击兜底)")
return
end
end
logger("WARN", "自动登录未成功,登录窗仍存在,请手动确认")
end
-- ================================
-- 网络检测
-- ================================
local function waitForNetwork(timeout)
local t = 0
while t < timeout do
local ok = hs.execute("ping -c 1 8.8.8.8 >/dev/null 2>&1")
if ok then
logger("INFO", "网络已恢复")
return true
end
hs.timer.usleep(1000000)
t = t + 1
end
logger("WARN", "网络检测超时")
return false
end
-- ================================
-- 停止微信流程(仅优雅退出,不强杀)
-- ================================
local function stopWeChat()
if not isWeChatRunning() then
logger("INFO", "微信未运行,无需处理")
return
end
logger("ACTION", "开始停止微信")
quitWeChatGracefully()
end
-- ================================
-- 系统睡眠/唤醒监听
-- ================================
local isSleeping = false
local watcher = hs.caffeinate.watcher.new(function(event)
if event == hs.caffeinate.watcher.systemWillSleep then
if isSleeping then return end
isSleeping = true
logger("SYSTEM", "=== 系统进入睡眠 ===")
stopWeChat()
elseif event == hs.caffeinate.watcher.systemDidWake then
logger("SYSTEM", "=== 系统已唤醒 ===")
hs.timer.doAfter(5, function()
waitForNetwork(10)
if not isWeChatRunning() then
logger("INFO", "尝试唤醒微信...")
if startWeChat() then
confirmWeChatLogin()
end
else
logger("INFO", "微信已在运行")
end
isSleeping = false
end)
end
end)
watcher:start()
logger("SYSTEM", "Hammerspoon: Mac睡眠 WeChat 自动控制插件加载成功(优雅退出 + 自动登录)")
-- ================================
-- 测试热键:Ctrl+Shift+L 一键「退出 → 重启 → 自动登录」
-- 用于即时验证自动登录效果,无需等待睡眠唤醒;验证稳定后可删除此段
-- ================================
-- hs.hotkey.bind({"ctrl", "shift"}, "L", function()
-- logger("SYSTEM", "=== 手动测试:退出 → 重启 → 自动登录 ===")
-- quitWeChatGracefully()
-- hs.timer.doAfter(2, function()
-- if startWeChat() then
-- confirmWeChatLogin()
-- end
-- end)
-- end)
-- logger("SYSTEM", "测试热键已绑定:Ctrl+Shift+L(退出→重启→自动登录)")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
# 如何生效
修改完后:
打开 APP 应用:Hammerspoon → Reload Config
1
或者:
hs -c "hs.reload()"
1
# 效果验证
可以看日志:
tail -f /tmp/wechat_sleep.log
1
能看到:
========================================
2026-03-20 18:09:38 系统准备进入睡眠
========================================
2026-03-20 18:09:38 尝试优雅退出微信(osascript)
2026-03-20 18:09:39 优雅退出成功
2026-03-20 18:09:39 微信已完全退出
========================================
2026-03-20 18:09:53 系统已唤醒
========================================
2026-03-20 18:09:58 网络已恢复
2026-03-20 18:09:58 尝试启动微信,第 1 次
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
# 一些细节优化
# 1 为什么要强杀(pkill)
因为:osascript quit 不一定成功有时候微信会残留进程。
# 2 为什么要延迟 5 秒
因为刚唤醒时:
- Wi-Fi 可能还没连上
- 系统还没完全恢复
# 3 为什么要检测网络
避免:微信启动 → 没网 → 登录失败
# 适合人群
- Mac 常驻微信用户
- 经常合盖离开
- 依赖手机通知
# 总结
👉 用 Hammerspoon 接管系统事件,比“脚本 + 运气”靠谱得多。
这个方案的核心是:
监听系统睡眠/唤醒事件
+ 自动控制微信进程
+ 加上重试 网络检测
1
2
3
2
3
优点:
- 稳定
- 有日志(可排查)
- 基本无感
赞赏一下

「真诚赞赏,手留余香」
# 打赏记录
| 打赏者 | 打助金额 (元) | 支付方式 | 时间 | 备注 |
|---|---|---|---|---|
| John | 12 | 微信 | 2020-06-09 | |
| 艾斯 | 32 | 支付宝 | 2020-07-11 | nice |
| HickSalmon | 15 | 微信 | 2020-09-21 | 有赏交流 |
- 03
- 未来 10 年,哪些工作会被替代?哪些更稳?我们到底该焦虑什么04-15