在 Mac 上优化锁屏体验
前言
工作日的时候,Codex 跑点东西电脑不能睡眠。如果一直用屏保其余时间就不会睡眠了,需要个方案能在工作日和非工作日之间切换
需求
工作日 12:00–20:00:手机离开 → 启动屏保并锁定
其他时间:无人操作 10 分钟关闭显示器
回来后,必须输入密码或 Touch ID,不让 BLEUnlock 自动解锁
配置
安装 BLEUnlock
1
brew install bleunlock
不要输密码,输了密码去钥匙串里面删掉。因为 BLEUnlock 的自动解锁设计会把登录密码保存到钥匙串。而且其 GitHub 项目在 2026 年出现了一个仍未关闭的“密码暴露”安全问题报告,同时也有人报告 macOS Tahoe 兼容问题
仅打开「用屏保来锁定它」

把「系统设置」 → 「锁定屏幕」 → 「启动屏幕保护程序或显示器关闭后需要输入密码」,设置成「立即」
新建一个脚本
workday-lock-installer.sh1
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
330
331
332
333
334
335
336
337
338
339
set -euo pipefail
BASE="$HOME/Library/Application Support/WorkdayLock"
SCRIPT="$BASE/workday-lock.sh"
PLIST="$HOME/Library/LaunchAgents/com.qiye.workday-lock.plist"
CAFFEINE_PLIST="$HOME/Library/LaunchAgents/com.qiye.workday-caffeinate.plist"
POLICY_LABEL="com.qiye.workday-lock"
CAFFEINE_LABEL="com.qiye.workday-caffeinate"
UID_NUM="$(/usr/bin/id -u)"
LAUNCH_DOMAIN="gui/$UID_NUM"
install_policy() {
if [[ ! -d /Applications/BLEUnlock.app && ! -d "$HOME/Applications/BLEUnlock.app" ]]; then
print -u2 "未找到 BLEUnlock.app。请先安装并配置 BLEUnlock,再运行此安装器。"
exit 1
fi
/bin/mkdir -p "$BASE" "$HOME/Library/LaunchAgents"
/bin/cat > "$SCRIPT" <<'POLICY'
#!/bin/zsh
# China-mainland workday + BLEUnlock + display-awake controller for macOS.
# Commands: apply (default), work, normal, status, refresh
set -u
BASE="$HOME/Library/Application Support/WorkdayLock"
CACHE="$BASE/cache"
STATE="$BASE/state"
LOG="$BASE/workday-lock.log"
MODEFILE="$STATE/mode"
CAFFEINE_LABEL="com.qiye.workday-caffeinate"
CAFFEINE_PLIST="$HOME/Library/LaunchAgents/$CAFFEINE_LABEL.plist"
LAUNCH_DOMAIN="gui/$(/usr/bin/id -u)"
/bin/mkdir -p "$CACHE" "$STATE"
log() {
print -r -- "$(/bin/date '+%F %T') $*" >> "$LOG"
}
caffeinate_is_running() {
/bin/launchctl print "$LAUNCH_DOMAIN/$CAFFEINE_LABEL" >/dev/null 2>&1
}
start_caffeinate() {
if caffeinate_is_running; then
return 0
fi
[[ -f "$CAFFEINE_PLIST" ]] || {
log "missing caffeinate plist: $CAFFEINE_PLIST"
return 1
}
/bin/launchctl bootstrap "$LAUNCH_DOMAIN" "$CAFFEINE_PLIST" >/dev/null 2>&1 || true
/bin/launchctl kickstart -k "$LAUNCH_DOMAIN/$CAFFEINE_LABEL" >/dev/null 2>&1 || true
if caffeinate_is_running; then
log "started caffeinate launch agent"
else
log "failed to start caffeinate launch agent"
return 1
fi
}
stop_caffeinate() {
if caffeinate_is_running; then
/bin/launchctl bootout "$LAUNCH_DOMAIN/$CAFFEINE_LABEL" >/dev/null 2>&1 || true
log "stopped caffeinate launch agent"
fi
}
start_bleunlock() {
if ! /usr/bin/pgrep -x BLEUnlock >/dev/null 2>&1; then
if /usr/bin/open -gj -a BLEUnlock; then
log "started BLEUnlock"
else
log "failed to start BLEUnlock"
return 1
fi
fi
}
stop_bleunlock() {
if /usr/bin/pgrep -x BLEUnlock >/dev/null 2>&1; then
/usr/bin/osascript -e 'tell application "BLEUnlock" to quit' >/dev/null 2>&1 || true
/bin/sleep 1
/usr/bin/pkill -x BLEUnlock >/dev/null 2>&1 || true
log "stopped BLEUnlock"
fi
}
cache_is_fresh() {
local file="$1"
[[ -f "$file" ]] || return 1
local mtime now
mtime="$(/usr/bin/stat -f %m "$file" 2>/dev/null || print 0)"
now="$(/bin/date +%s)"
(( now - mtime < 604800 ))
}
holiday_json_is_valid() {
/usr/bin/osascript -l JavaScript -e '
ObjC.import("Foundation");
function run(argv) {
const data = $.NSData.dataWithContentsOfFile(argv[0]);
if (!data) throw new Error("missing JSON file");
const text = ObjC.unwrap($.NSString.alloc.initWithDataEncoding(data, $.NSUTF8StringEncoding));
const obj = JSON.parse(text);
if (!obj || !Array.isArray(obj.days)) throw new Error("invalid holiday data");
return "ok";
}
' -- "$1" >/dev/null 2>&1
}
refresh_year() {
local year="$1"
local file="$CACHE/$year.json"
local tmp="$file.$$.tmp"
if cache_is_fresh "$file"; then
return 0
fi
local url="https://cdn.jsdelivr.net/gh/NateScarlet/holiday-cn@master/$year.json"
if /usr/bin/curl --fail --silent --show-error --location --max-time 15 \
"$url" -o "$tmp" \
&& holiday_json_is_valid "$tmp"; then
/bin/mv -f "$tmp" "$file"
log "refreshed holiday data year=$year"
else
/bin/rm -f "$tmp"
[[ -f "$file" ]] || log "holiday data unavailable year=$year; weekday fallback will be used"
fi
}
workday_status() {
local today="$1"
local year="${today[1,4]}"
local file="$CACHE/$year.json"
refresh_year "$year"
/usr/bin/osascript -l JavaScript - "$today" "$file" <<'JXA'
ObjC.import('Foundation');
function loadJSON(path) {
const data = $.NSData.dataWithContentsOfFile(path);
if (!data) return null;
const text = ObjC.unwrap($.NSString.alloc.initWithDataEncoding(data, $.NSUTF8StringEncoding));
try { return JSON.parse(text); } catch (_) { return null; }
}
function run(argv) {
const date = argv[0];
const obj = loadJSON(argv[1]);
if (obj && Array.isArray(obj.days)) {
const item = obj.days.find(x => x.date === date);
if (item) return item.isOffDay ? 'off' : 'work';
}
const p = date.split('-').map(Number);
const day = new Date(p[0], p[1] - 1, p[2], 12, 0, 0).getDay();
return day >= 1 && day <= 5 ? 'work' : 'off';
}
JXA
}
record_mode() {
local new_mode="$1"
local old_mode=""
[[ -f "$MODEFILE" ]] && old_mode="$(<"$MODEFILE")"
if [[ "$old_mode" != "$new_mode" ]]; then
print -r -- "$new_mode" > "$MODEFILE"
log "mode=$new_mode"
fi
}
enter_work_mode() {
start_caffeinate
start_bleunlock
record_mode work
}
enter_normal_mode() {
stop_caffeinate
stop_bleunlock
record_mode normal
}
apply_policy() {
local today hhmm day_status
today="$(/bin/date +%F)"
hhmm="$(/bin/date +%H%M)"
day_status="$(workday_status "$today" 2>/dev/null || print off)"
if [[ "$day_status" == "work" ]] && (( 10#$hhmm >= 1200 && 10#$hhmm < 2000 )); then
enter_work_mode
else
enter_normal_mode
fi
}
show_status() {
local today hhmm hhmm_number day_status mode ble cafe
today="$(/bin/date +%F)"
hhmm="$(/bin/date +%H:%M)"
day_status="$(workday_status "$today" 2>/dev/null || print unknown)"
mode="normal"
hhmm_number="${hhmm/:/}"
if [[ "$day_status" == "work" ]] && (( 10#$hhmm_number >= 1200 && 10#$hhmm_number < 2000 )); then
mode="work"
fi
ble="stopped"; /usr/bin/pgrep -x BLEUnlock >/dev/null 2>&1 && ble="running"
cafe="stopped"; caffeinate_is_running && cafe="running"
print "date=$today time=$hhmm china_day=$day_status expected_mode=$mode BLEUnlock=$ble caffeinate=$cafe"
}
case "${1:-apply}" in
apply) apply_policy ;;
work) enter_work_mode ;;
normal) enter_normal_mode ;;
status) show_status ;;
refresh)
y="$(/bin/date +%Y)"
/bin/rm -f "$CACHE/$y.json"
refresh_year "$y"
;;
*)
print -u2 "usage: $0 {apply|work|normal|status|refresh}"
exit 2
;;
esac
POLICY
/bin/chmod 755 "$SCRIPT"
/bin/cat > "$CAFFEINE_PLIST" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>$CAFFEINE_LABEL</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/caffeinate</string>
<string>-di</string>
</array>
<key>ProcessType</key>
<string>Background</string>
</dict>
</plist>
PLIST
/bin/cat > "$PLIST" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>$POLICY_LABEL</string>
<key>ProgramArguments</key>
<array>
<string>/bin/zsh</string>
<string>$SCRIPT</string>
<string>apply</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>StartInterval</key>
<integer>300</integer>
<key>StartCalendarInterval</key>
<array>
<dict>
<key>Hour</key><integer>12</integer>
<key>Minute</key><integer>0</integer>
</dict>
<dict>
<key>Hour</key><integer>20</integer>
<key>Minute</key><integer>0</integer>
</dict>
</array>
<key>ProcessType</key>
<string>Background</string>
</dict>
</plist>
PLIST
/bin/zsh -n "$SCRIPT"
/usr/bin/plutil -lint "$CAFFEINE_PLIST"
/usr/bin/plutil -lint "$PLIST"
/bin/launchctl bootout "$LAUNCH_DOMAIN/$POLICY_LABEL" 2>/dev/null || true
/bin/launchctl bootout "$LAUNCH_DOMAIN/$CAFFEINE_LABEL" 2>/dev/null || true
/bin/launchctl bootstrap "$LAUNCH_DOMAIN" "$PLIST"
print "安装完成。无需创建快捷指令或自动化。"
print "查看状态:$0 status"
print "刷新节假日:$0 refresh"
print "卸载:$0 uninstall"
}
show_status() {
if [[ ! -x "$SCRIPT" ]]; then
print -u2 "尚未安装。请先运行:$0 install"
exit 1
fi
"$SCRIPT" status
}
refresh_data() {
if [[ ! -x "$SCRIPT" ]]; then
print -u2 "尚未安装。请先运行:$0 install"
exit 1
fi
"$SCRIPT" refresh
"$SCRIPT" apply
"$SCRIPT" status
}
uninstall_policy() {
[[ -x "$SCRIPT" ]] && "$SCRIPT" normal || true
/bin/launchctl bootout "$LAUNCH_DOMAIN/$POLICY_LABEL" 2>/dev/null || true
/bin/launchctl bootout "$LAUNCH_DOMAIN/$CAFFEINE_LABEL" 2>/dev/null || true
/bin/rm -f "$PLIST" "$CAFFEINE_PLIST" "$SCRIPT"
print "已卸载。日志和节假日缓存保留在:$BASE"
}
case "${1:-install}" in
install) install_policy ;;
status) show_status ;;
refresh) refresh_data ;;
uninstall) uninstall_policy ;;
*)
print -u2 "用法:$0 {install|status|refresh|uninstall}"
exit 2
;;
esac假设文件下载到了“下载”文件夹,打开终端运行
1
2chmod +x "$HOME/Downloads/workday-lock-installer.sh"
"$HOME/Downloads/workday-lock-installer.sh" install如果 macOS 提示文件受隔离而无法执行,可以先运行:
1
xattr -d com.apple.quarantine "$HOME/Downloads/workday-lock-installer.sh"
管理命令
1
2
3
4
5
6
7
8# 查看状态
"$HOME/Downloads/workday-lock-installer.sh" status
# 重新下载节假日数据并应用策略
"$HOME/Downloads/workday-lock-installer.sh" refresh
# 卸载
"$HOME/Downloads/workday-lock-installer.sh" uninstall
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 七夜 の Blog!
评论











