本机软件库¶
维护「重装系统程序清单」的操作手册。
流程:放安装包到 packages\ → 改 programs.json → 跑 python programs_server.py(起服务器)→ 本机点「运行」,局域网另有只读页面可点「本地下载」。
1. 目录结构¶
| Text Only | |
|---|---|
路径一律用相对路径,从 programs.json 所在目录算起:
- 安装包 →
"file": "packages/python-3.15.2-amd64.exe" - 程序找文件 =
json 所在目录 / file 字段,与盘符无关,整个文件夹搬到哪台机器都能用
2. 新增一个软件¶
第 1 步:安装包丢进 packages\¶
保持官方原文件名,别加空格和全角符号。
第 2 步:programs.json 的 programs 数组末尾加一条¶
| JSON | |
|---|---|
上一项记得补逗号。updated_at 填当前**毫秒级**时间戳(Date.now() 风格,13 位),命令行取:date +%s%3N。
第 3 步:启动服务器¶
| Text Only | |
|---|---|
浏览器会自动打开(--no-open 可关掉)。改完 JSON 存盘,按 F5 就会看到新内容,不用重启服务。
本机打开时每张卡片右侧有蓝色「运行」按钮,点了就用系统默认程序打开对应的文件——.exe 直接执行,.msi 交给 Windows Installer,.zip 交给解压软件;本地包存在时按钮区还有「本地下载」,点它即以附件形式下载该文件。
3. 字段说明¶
meta(只保留两项)¶
| 字段 | 说明 |
|---|---|
title |
页面大标题(当前为「本机软件库」) |
timestamp |
清单最后更新时刻,毫秒级时间戳(Date.now(),13 位),页面顶部「更新」显示它(格式化为 YYYY-MM-DD) |
程序数、总体积、分类列表全部由程序在渲染时从 programs 数组和磁盘文件算出,不写回 JSON。
programs[]¶
| 字段 | 必填 | 说明 |
|---|---|---|
name |
✅ | 英文/原名,灰色小字,参与搜索 |
name_cn |
✅ | 中文名,卡片主标题;留空则回退用 name |
file |
✅ | 相对路径 packages/xxx.exe,必须与磁盘完全一致。没有本地文件填 "" |
version |
建议 | 蓝色徽章 |
platform |
✅ | windows-x64 / macos-arm64 / macos-x64 / android |
category |
✅ | 决定筛选 chip,取值见下 |
source_site |
建议 | 来源网站,显示在「来源」 |
page_url |
✅ | 官方下载页,「官网下载」按钮;无来源填 "",按钮自动隐藏 |
size_bytes |
可选 | 本地无文件时的兜底体积 |
note |
可选 | 一行备注,留空不显示;当前全部为空 |
updated_at |
✅ | 该条最后修改时刻,毫秒级时间戳,页面显示为「更新 2026-11-12」 |
已废弃:
download_url(直链)字段已从 JSON 全部移除。需要分发安装包时直接点卡片上的文件路径下载,不再记录外链直链。
category 取值(新增尽量复用):
file 为空串:不显示文件标签,也没有「本地下载」和「运行」按钮,只剩「官网下载」(如 Google Chrome)。
4. 图标¶
卡片左侧的图标是**自动从安装包里提取的**,不用手工准备:
- 用 Windows 自带的
PrivateExtractIconsW取 256px 图标,再自己编码成 PNG(zlib + struct,标准库,不需要 Pillow) - 缓存到
icons\<sha1(file)前16位>.png,浏览器直接按/icons/xxx.png请求(静态页则走相对路径) - 增量:只有图标不存在、或源文件比图标新时才重新提取。24 个包首次约 0.3 秒,之后零开销
取不到图标的会显示首字母块,这属于正常情况:
| 情况 | 说明 |
|---|---|
.msi / .zip |
不是 PE 文件,本身没有图标资源(Node.js、RBTray 就是这种) |
file 为空 |
本地没包(如 Chrome) |
| 非 Windows 系统 | 提取依赖 Win32 API,直接全部回退成字母块 |
整个 icons\ 删掉也没关系,下次生成会自动重建。旧条目删掉后残留的图标不会自动清理(无害,手动删即可)。
5. 「本地下载」按钮¶
卡片右侧按钮区第一个就是「本地下载」,指向本地安装包:
| 模式 | href |
|---|---|
| 服务器 | /download?file=packages/xxx.exe(新标签页,附件流式回传) |
--generate 静态页 |
packages/xxx.exe(相对路径,file:// 下就是本地文件) |
只有 file 指向的文件**存在**时才出现这个按钮;不存在则不渲染(避免点了 404)。左侧的文件路径标签只是展示信息,不可点。
服务端 GET /download 的处理:
- 复用安装包的同一套校验(必须 resolve 到
packages\内、文件必须存在),穿越路径返回 404 Content-Type: application/octet-stream+Content-Length,256 KB 分块流式发送,大文件不占内存Content-Disposition: attachment,中文文件名用 RFC 5987 的filename*=UTF-8''编码,下载下来名字不会乱码- 不限制后缀——
.zip/.apk/.dmg也能下
所以想让某个包能被局域网下载,只要它躺在 packages\ 里并在 JSON 里写了正确的 file 就行。
6. 数组顺序 = 安装顺序¶
programs 下标即页面序号 1..N,不另设 install_order。重装时从上往下点「运行」即可。
7. 服务器模式(默认)¶
| 行为 | 说明 |
|---|---|
| 端口 | 默认自动挑选空闲端口;想固定用 --port 8080(被占用时会自动换) |
| 监听 | 默认 0.0.0.0,局域网其它机器可访问(只读);--host 127.0.0.1 则仅本机 |
| 刷新 | 只有 programs.json 变了才重新渲染:每次请求只 stat 一次文件(mtime_ns + size),指纹没变直接返回内存里的缓存 HTML。本机和局域网页面按钮不同,各存一份 |
| 打开浏览器 | 启动 0.6 秒后自动打开;--no-open 关闭 |
运行按钮(仅本机)¶
服务端不是靠前端隐藏来控制的,GET / 时就按客户端来源决定渲不渲染:
- 客户端 IP 必须是
127.0.0.1/::1 - 且
Host头必须是127.0.0.1/localhost(所以用本机局域网 IP 打开自己也是只读) - 两个条件都满足才输出「运行」按钮
POST /install 会**再校验一次**同样的条件,远程请求直接 403 {"ok":false,"message":"仅允许本机运行"}。绕过前端、直接 curl 也打不进来。
其余规则:
- 只对
file指向**存在**文件、且后缀为.exe/.msi/.bat/.cmd的条目显示 - 一律用系统默认程序打开(
os.startfile):.exe直接执行、.msi走 Windows Installer、.zip走解压软件 - 启动方式就是
os.startfile(等价双击),不做任何窗口监听、轮询或置前逻辑 - 不限制后缀,
packages\里放什么都能点「运行」 - 装系统类软件会弹 UAC,需要你点「是」
- 结果通过页面底部 toast 提示(「已启动 xxx」/「文件不存在」等)
安全约束(写在代码里,别绕过)¶
- 运行和下载都只允许
packages\内的文件,../路径穿越直接拒绝(404) - 运行能力绑定本机回环 +
Host白名单,局域网访问天生只读 - 局域网可下载 = 任何同网段的人都能拉走
packages\里的安装包,这是功能不是漏洞;不想这样就用--host 127.0.0.1
静态模式¶
| Bash | |
|---|---|
生成 programs.html,没有运行按钮(POST /install 是服务端接口),「本地下载」的 href 是相对路径,双击本地打开页面后也能点它下载安装包。
8. 页面是什么样¶
- 顶部:程序数 / 总体积 / 更新日期
- 搜索框 + 分类 chip 筛选
- 卡片:左侧**图标**(左上角小圆标是序号)、中文名、英文名、版本徽章、平台、分类标签、相对路径、来源、体积、更新日期
- 左侧:文件路径标签(纯展示)
- 右侧按钮,从左到右:本地下载 → 官网下载 → 运行(最后一个是蓝色主按钮,仅本机 + 服务器模式)
刻意不做的东西(别再加回来):快捷键监听、打印按钮、复制本地路径、复制直链、直链按钮、状态标记(本地就绪 / 需在线下载)。
9. 命令行¶
10. 排错¶
| 现象 | 处理 |
|---|---|
体积显示不对 / 走到兜底 size_bytes |
file 与磁盘文件名不一致,或不在 packages\ 下。ls packages 对一下 |
| 点「运行」提示「文件不存在」 | file 路径写错,或文件被移走 |
| 点「运行」后没反应 | 看服务端终端日志;UAC 弹窗可能被最小化或挡在其它窗口后 |
| 程序起来了但窗口在后台 | 已知行为,不处理。服务是后台进程,安装器窗口可能停在任务栏,点一下即可 |
| 服务启动后浏览器打不开 | 用终端里打印的 http://127.0.0.1:<端口>/ 手动访问,端口每次可能不同 |
| 局域网访问不了 | 本机防火墙拦了该端口;或启动时用了 --host 127.0.0.1 |
| 局域网页面没有「运行」按钮 | 正常设计:运行只在本机 127.0.0.1 / localhost 打开时出现 |
| 点文件路径报 404 | file 路径写错或文件被移走,刷新页面后该标签会变回不可点 |
| 图标显示成首字母块 | .msi / .zip 没有图标资源,属正常;.exe 也这样说明提取失败,删掉 icons\ 里对应文件重新刷新即可 |
| 图标全空 / 裂图 | 静态页被单独拷走而没带 icons\ 目录;把整个文件夹一起搬 |
| 下载的中文文件名乱码 | 用了很老的浏览器;接口已带 filename*=UTF-8'',Chrome/Edge/Firefox 都正常 |
| 端口固定失败 | --port 指定的端口被占用时会自动换一个,看启动输出确认实际端口 |
| 中文乱码 | JSON 存成 GBK 了,另存为 UTF-8 无 BOM |
| 「JSON 解析失败」 | 漏逗号或末尾多逗号,用 python -m json.tool programs.json 查 |
| 改了 JSON 页面没变 | 看终端是「重新生成」还是「缓存」;若显示缓存说明文件没真正写入(编辑器可能用了临时文件替换,试试重新保存)。报错会返回 500 页面并打印原因 |
| 分类 chip 顺序怪 | chip 按 category 首次出现顺序排,调整首条位置 |
11. 约束¶
- 单文件自包含:CSS/JS 全内联,无 CDN、无外链字体
- 只改
programs.json,programs.html是产物 - 零第三方依赖,Python 3.9+ 可跑
- 全流程相对路径(含
icons/),目录整体搬家不用改任何配置