这不是教程,是一份踩坑清单。所有代码和问题都来自真实项目,已开源。
Windows 的聚焦壁纸(Spotlight)有个毛病:锁屏上那张很好看,第二天就找不着了。必应每日一图也是,看完就没。
我想:能不能把它们攒起来,让电脑每天自己换?
换以前,这种念头只能想想——我不会写 Windows 桌面程序。这次我试着跟 WorkBuddy 说了一句。
三天后,我电脑上跑着一个 7 MB 的 exe:双击出中文菜单,后台每 30 分钟换一张,必应每日一图自动补,喜欢的按 [F] 收藏,开机自启,卸载就是把文件删掉。
下面正文是这三天的全过程;这一节是给想复现的人准备的速览。
我给 WorkBuddy 的东西一共三类,没有一样是"需求文档":
1)一句大白话的念头
Windows 聚焦壁纸第二天就找不着了,必应每日一图也是。能不能攒起来,让电脑每天自己换?
2)五条验收标准(说出来的是效果,不是实现)
3)之后每一轮的反馈,都是一句话 + 一条证据
我的反馈 | 附的证据 |
|---|---|
"我双击点开程序要大概十秒才会显示菜单" | 秒表计时 |
"F 盘明明有图片文件夹,它又强制建一个图片文件夹,它要造反吗?" | 文件夹截图 |
"每次更新的版本第一次使用,关闭菜单窗口就会跳出这个窗口" | 弹窗截图 |
"明明启动了,喊你启动?有误导的嫌疑" | 菜单截图 |
"客户第一次用的时候,程序要自动定位在 C 盘以外的图片文件夹,不用客户去猜" | — |
4)环境事实(这些不先说清楚,AI 就会写出跑不起来的代码)
python 命令是微软商店的占位符
在 WorkBuddy 里提需求:需求、反馈、证据都在同一个对话里,它读得到上下文,不用我反复交代背景。
说出来可能有点意外——没有配置。没有 MCP,没有自定义 Skill,没有写 System Prompt,就是开一个对话开始说人话。
真正需要"配置"的是它怎么用我这台机器:
项 | 实际做法 |
|---|---|
客户端 | WorkBuddy 桌面版(Windows) |
工作目录 |
|
给它的权限 | 直接读写本地文件、跑命令(PowerShell / Python / git) |
打包环境 | 在隔离的 venv 里装 PyInstaller 6.x,不动系统 Python(系统本来也没有) |
版本管理 | PortableGit(这台机器 git 的 https 走不通,是用 REST API 推的) |
自己攒的三个校验脚本 |
|
一句话总结:它是"能动手的同事",不是"会打字的搜索引擎"。能不能读写文件、能不能跑命令,差别是数量级的。
第 1 步:描述效果,不描述实现。
第一句话不是"帮我写个程序",而是"我要看到什么效果"。这一步是全程最关键的——一上来就说"你用 PowerShell 写个脚本",等于自己先替它做了架构决定。
第 2 步:让它先出一个最小能跑的版本。
第一版就两个文件:core.ps1(取图、换壁纸、记状态)、menu.ps1(中文菜单)。先跑起来,再谈完善。
第 3 步:打包成单文件 exe,然后自己双击试。
python build-exe.py 一键打包。这一步不能省——没双击过,就不算做完。
第 4 步:真用,然后提反馈。
每天用它换壁纸,不顺手就说,每条反馈带证据(截图、时间点、原话)。下面那六个坑,五个是这么挖出来的,没有一个是我"想"出来的。
第 5 步:改 → 打包 → 体检 → 提交,一次都不跳。

坑 1 的修法实拍:一处拿 C、一处拿 C:,两个永远不相等,于是系统盘从来没被排除过。

坑 6 的修法实拍:拉起子进程前把 PyInstaller 的环境变量剥干净。
第 6 步:晾一天再发布。
这是吃了亏之后定的规矩:新包打出来先自己用一天,不马上推。理由见正文最后。

git log --oneline 的真实提交记录:每一步改动都留了痕迹,改坏了能退回去。
1)一个单文件 exe:7.27 MB,免安装、免管理员权限,双击就出中文菜单;卸载就是把文件删掉。
2)全部源码开源(MIT):Gitee 与 GitHub 双仓库,发行版里挂着打包好的 exe。


3)它现在在我机器上的真实运行状态(2026-09-20 实测,不是演示数据):
项 | 实际值 |
|---|---|
版本 | v1.6.2 |
壁纸库 | 必应 2 张 · 聚焦 51 张 |
累计下载 | 53 张(删掉的也算,是笔只增不减的流水账) |
累计换图 | 66 次 |
换图间隔 | 30 分钟 |
保存位置 |
|
后台进程 | 正在跑 |

程序主菜单(v1.6.2,本机实测)。第一行如实报告后台状态;「累计下载」这行是后来加的——旧版本下载的图也要算进去,否则数不准。
4)一份踩坑清单:六个坑,每一个都附了错在哪、怎么修、为什么这么修。
第一句话不是"帮我写个程序",而是描述我要看到什么效果:
这五条后来成了整个项目的验收标准。回头看,这一步是全程最关键的:描述效果而不是描述实现,AI 才有发挥余地;一上来就说"你用 PowerShell 写个脚本",等于自己先替它做了架构决定,而且往往是错的。
C 和 C: 不是一回事需求是"把壁纸存到系统盘以外",听起来一行判断的事。实际写出来的 bug 藏了三个版本:
# 一处给 'C'($env:SystemDrive 去掉冒号),另一处给 'C:'(路径解析出来带冒号)
# 口径不一致 -> 比较永远为真 -> 系统盘从来没被排除过
if ($picturesDrive -ne $sysDrive) { ... }后果很讽刺:恰恰是那些"图片文件夹还在 C 盘"的电脑(也就是大多数人的电脑),程序反而判定"它不在系统盘",然后往 C 盘里塞。
修法是先统一口径再比:
function Get-BwSysDriveLetter {
try { return (([string]$env:SystemDrive).TrimEnd('\').TrimEnd(':')).ToUpper() } catch { return '' }
}
function Get-BwDriveLetter([string]$p) {
if (-not $p) { return '' }
if ($p -match '^([A-Za-z]):') { return $Matches[1].ToUpper() }
return ''
}教训:单元测试抓不到这类 bug——逻辑没错,是数据口径错了。我是看见 F 盘根目录凭空多出一个空的「图片」文件夹才发现的。
@() 少包一层,数组会变成字符串收藏功能,收第一张正常,收第二张时收藏夹里只剩一个拼坏的文件名,两张都不生效。
# 错:单元素数组从函数返回时被展开成标量,'字符串 + 数组' 走的是字符串拼接
$s.favorites = @((Get-BwFav $s) + @($n))
# 对:两边都包成数组
$s.favorites = @(@(Get-BwFav $s) + @($n))[psobject] 不是 [PSCustomObject]"自动挑可用空间最大的非系统盘"——写了,也测了,但它其实从没生效过。
# 错:[psobject]@{} 实际生成的是 Hashtable,Sort-Object 取不到 .Free 属性
$item = [psobject]@{ Drive = 'E:'; Free = 256 }
# 对
$item = [PSCustomObject]@{ Drive = 'E:'; Free = 256 }Sort-Object Free -Descending 取不到属性时不报错,只是不排。实测退化成 F → E → D,于是"挑最大的盘"恒等于挑 F 盘。这种静默失败最要命:日志干净,功能看着也在跑,就是答案不对。
New-Item 没有 -LiteralPath我本机 PowerShell 5.1 的 New-Item 参数集里根本没有 -LiteralPath,传它直接抛"找不到与参数名称匹配的参数"。后果是:给一个还不存在的目录测可写性,必定抛异常返回 false——用户手输任何新路径,都会被判成"写不进去"。那是环境问题,不是权限问题。
改法是不依赖 cmdlet,直接用 .NET:
function New-BwDir([string]$dir) {
if (-not $dir) { return $false }
if (Test-Path -LiteralPath $dir) { return $true }
try { [void][System.IO.Directory]::CreateDirectory($dir); return $true } catch { return $false }
}[Screen]::PrimaryScreen.Bounds 给的是逻辑像素。4K 屏开 150% 缩放时它只报 2560×1440,按 Width -ge 3840 判断就下成 1080p——图是缩水的,而且你根本看不出为什么。
$ph = @(Get-BwScreenPhysical) # 乘上系统 DPI 还原成物理像素再判断现象很怪:每个新版本第一次使用之后,关掉菜单窗口就弹一个 Failed to remove temporary directory。以后再也不弹。
根因链条:PyInstaller 单文件 exe 运行时把自己解压到 Temp\_MEIxxx,退出时回收。而菜单 exe → PowerShell → 后台 exe 这条启动链把环境变量一路继承下去,里面带着 _PYI_* / _MEIPASS2 标记。后台 exe 拿到标记后误以为自己是菜单的子进程,不自建临时目录,直接复用菜单那个。于是关菜单时后台还占着 → 删不掉 → 弹框。
为什么只在"新版本第一次"出现?因为只有第一次才需要按 [A] 开自动换;以后后台由系统开机独立拉起,环境干净,各用各的目录。现象和推断严丝合缝。
修法只有几行(官方文档里有一节专门讲这个):
def clean_env():
drop_prefix = ('_PYI_', 'PYINSTALLER_')
drop_exact = {'_MEIPASS2'}
env = {}
for k, v in os.environ.items():
if k in drop_exact or k.startswith(drop_prefix):
continue
env[k] = v
return env这是三天里我最想说的一句。
AI 的测试其实很扎实——状态迁移、收藏增删、快捷方式的四种分支、打包后解包校验 MD5……全过。但它测的是"功能对不对"。
我测的是"拿去真用顺不顺手"。这两件事的交集比想象中窄得多:
[A] 开",可菜单里当时只摆了 [B] —— 功能没问题,人照着做做不到[1] 改保存位置",实际是 [3] —— 功能没问题,指错了路[B] 关掉的 —— 功能没问题,说了假话这几类错,AI 自己永远发现不了,因为它不会真的照着那行提示去按那个键。所以我给自己定了条规矩:新版本打完包,晾一天再发布。留出来的这一天,基本都是靠真实使用在兜底。v1.6.2 就是这么晾出来的,晾出了上面那个跟了好几个版本的弹窗。
一、只排序,不筛选。 程序探到某个盘根目录写不进去,是把它从列表里剔除,还是列出来并给一键修复?我一开始选了剔除,后来推翻——那等于拿我这台机器的权限替所有用户做决定,别人会以为"这程序不支持我的盘"。程序可以推荐,不该替人决定。
二、能自动找现成的,就别让人选。 "壁纸存哪"这件事,正确做法不是弹一个路径输入框,而是先扫一遍:C 盘以外有没有现成的图片文件夹?有就用它,没有再考虑建。把"让用户填"当最后手段,不是第一手段。
@(),不写清楚为什么,下次重构一定会被改回去。[F] 收藏,可设置"只在收藏里轮换",填充方式可自己选%LOCALAPPDATA%\微软壁纸助手,exe 旁边干干净净;删掉 exe 就是卸载项目开源在 Gitee(MIT 协议):https://gitee.com/kele551/ms-wallpaper-assistant
我不是专职程序员。这三天里 AI 写的每一行代码我都看得懂个大概,但我没写过一行。
真正的门槛从来不是"会不会写代码",而是你能不能把你要什么说清楚,以及你愿不愿意拿去真用、把不顺手的地方挑出来说。后半句比前半句难得多。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。