Repository navigation
feat: SQLite 临时目录、货币与周期放开、从 GitHub 装主题、说清拒绝安装的原因(上游 #78/#71/#75) - #63
Merged
Merged
Conversation
三件事,都是上游最近合入而我们没跟的。 ## 1. SQLite 的临时文件放到数据库所在目录(上游 #78) 回收空间时 `VACUUM` 要先把整个库重建到一个临时文件里。SQLite 依次找 `$SQLITE_TMPDIR`、`$TMPDIR`、`/var/tmp`、`/usr/tmp`、`/tmp` 和工作目录, 而我们的两种部署都没有可用的: - **Docker**:`Dockerfile` 是 `FROM scratch` + `USER 65534`,镜像里根本没有 `/tmp`(注释自己写着「Nothing in this image can mkdir」),`/` 属于 root 而进程是 65534 → 六个候选一个都不可写,回收必定失败 - **一键脚本**:`install-hub.sh` 的单元是 `PrivateTmp=yes` + `ProtectSystem=strict` + `MemoryMax=256M`,`/tmp` 是 tmpfs 的系统上临时副本 占内存并计入 256M,库一大就被 OOM 杀掉 启动时一次 `PRAGMA temp_store_directory`(用内存连接设,进程级、必须在打开 任何连接之前)把临时目录指到库所在的那块盘上;SQLite 打开临时文件后立即 删除,目录里看不到残留。设置失败只记一条警告,按默认目录继续跑。 ## 2. 货币可填任意三字母代码,付款周期可填任意月数(上游 #71) 原来的面板只给 5 种货币、6 种周期。现在货币是输入框(三个字母,大小写都行, 存大写),周期是「数字 + 月/年/一次性」,1 个月到 100 年。 hub 侧校验两者:货币必须是三个字母;周期存成 `<n>m`,其中 1、3、6、12、24、 36 个月仍存原来的名字(`monthly`/`quarterly`/…),**旧主题照常显示**。自动 续期因此也支持任意月数顺延(`cycle_months` 直接吃 `<n>m`)。 ## 3. 拒绝安装时说出是哪一半(我们自己 backlog 的一条) `provisioning_allowed` 原来只返回真假,四个拒绝分支各自 `debug!` 到日志里, 面板上只有一句笼统的提示——而看面板的人没有 journal。改成返回具名的 `ProvisionBlock`(`host` / `https` / `site` / `origin`):`/api/me` 多带一个 `provision_block` 字段给面板,403 的正文在原来那段通用说明后面接上具体那一半。 四个分支拆细了一点:原来 https 与 `--site` 合在一个条件里,现在分开报,因为 它们的修法完全不同(一个改反代,一个改启动参数)。
## 续费设置
货币从 5 项下拉改成输入框(三个字母,大小写都行;`uppercase` 用 CSS 而不是改
写输入值,否则中文输入法正在拼写时会被打断),下面一行提示识别出的中文名,
认不出的代码提示「未知代码,照原样显示」。付款周期改成「数字 + 月/年/一次性」,
整年按年填——五年套餐是这么卖的。存回去时与原来的长度按**月数**比较,所以
名字之间换来换去不会写一次无意义的更新。`CYCLES` 标签表随之删掉,只留
`cycleMonths()` 解析。
上游在货币标签旁放了一个悬停问号解释填法;我们面板还没有 Tooltip 原语
(`@radix-ui/react-tooltip` 未引入),所以那两句说明并进了 `Field` 已有的
hint 位置。要照上游做成悬停问号,得先引入 tooltip 原语。
## 为什么不能添加节点
节点页的两个按钮以前只是置灰,不说原因;更新页只有一句通用的「请通过 HTTPS
域名访问面板后生成升级命令」。现在两处都显示具体那一半:
- hub 侧拒绝时用 `/api/me` 新带的 `provision_block`,四种各有各的说法
(Host 不是域名 / 这一跳不是 https / `--site` 不是 https 域名 / Origin 不符)
- 面板自己加的两个条件也有各自的说法(当前地址不是 https 域名 / `--site`
不是 https 域名)——这两个 hub 看不到,而我在本地预览时正是被它们挡住的
## 版本信息
`useVersions` 原来只有一次请求、失败被 `catch(() => {})` 吞掉,于是那一次失败
会让更新页一直显示「查不到版本」、导航上的更新圆点也不出现,直到手动刷新。
现在失败**或答案里一个 latest 都没有**时重试两次(1.5 秒、4 秒);hub 拿到
答案后是六小时缓存,重试只多花一次请求。
主题页多一个仓库地址输入框,新接口 `POST /api/theme-install`:hub 读该仓库最新 release 里的 `theme.tar.gz` 安装,省掉「先下到本地再上传」。地址只取 `<owner>/<repo>` 两段,下载地址由 hub 自己拼,所以仍然只连 api.github.com 与 github.com;填了 GitHub 代理时下载经代理。大小上限、解包检查与上传完全相同。 `repo()` 现在容忍浏览器地址栏常带的 `?tab=readme-ov-file` 与 `#readme`——它们 在取段之前就被丢掉,进不了拼出来的地址(既有测试里 `?x=1` 原是被拒的敌对样例, 现在改为接受并补了断言)。`github.com/...` 这种省掉协议的粘贴形式也接受。 `latest()` 把 GitHub 的拒绝分类说出来:404 是「仓库不存在,或者还没有正式 release」(私有仓库回答一样),403/429 是这台机器被限流,另有连不上与接口出错。 **选错文件的提示**:release 页的 Source code zip、或已被浏览器解压过的 tar, 原来报「主题包损坏或不完整(可能没下载完),重新下载」——重新下载同一个文件 无济于事。现在先看头两个字节,不是 gzip 就直接说「到主题仓库的 Releases 下载 theme.tar.gz,不要选 Source code」;Source code (tar.gz) 开头的 `pax_global_header` 是不落盘的元数据,原先按不支持的条目拒绝,现在计入解压总量后跳过,包继续走到 缺少 `theme.json` 的检查,那句提示也补上同样的指引。`archive()` 里对下载到的 字节也验一次魔数:代理在 200 下返回自己的拦截页时,否则会先报「sha256 不对」, 把人引到「release 坏了」上去。 **与上游的一处差异(我们更严)**:我们的 `archive()` 还要求 release 里有 `theme.tar.gz.sha256` 并核对 sha256(上游没有这一步)。实测 `monitor-probe` 的 两个主题仓库都发布了它,所以第三方主题不受影响;真的没发布时,报错直接说清 该怎么做(让作者补上,或下载后手动上传——上传路径不做校验)。
安装主题那张卡片加上仓库地址输入框与「从 GitHub 安装」按钮,上传退为次要按钮 (`variant="outline"`);两种方式的说明并进卡片正文。 上游把这些说明放在标题旁的悬停问号里;我们面板没有 Tooltip 原语,所以写成可见 的四行(地址怎么填、别选 Source code、卡片上的 ⟳ 是检查更新、只安装可信来源)。 输入框用 `type="text"` + `inputMode="url"` 而不是 `type="url"`:浏览器的原生校验 会先用它自己的语言回话,hub 的中文提示就没机会出现。
`billing_error` 要求货币是三个字母,而面板的「添加节点」只发一个 name,靠的是 `Node.currency` 的 serde 默认值 `usd()`。这条链子是新接上的,但既有测试只断言了 `public` / `billing_cycle` / `traffic_reset_day` 三个默认值,没覆盖 currency—— 默认值若被改成空串,面板的新增节点会全部 400,而那处断言不会响。加一行。
`temp_files_beside` 的 `Ok` 分支被测到了,出错分支没有——而它正是设计里的兜底: SQLite 拒绝一个不可写的目录(我们 bundle 的 3.50.2 报 `not a writable directory`), `main` 于是打一条警告、按默认目录继续跑。同时断言一次「设不上不会破坏原有设置」, 因为「看起来设上了但用不了」比默认值更糟。
一次独立复核(把上游三份 diff 逐 hunk 与我们的树核对)找到一处真的漏移植,以及
三处被简化掉的东西:
1. `theme::archive` **读响应体**那一步漏了代理感知的说明——上游是
`response.bytes().await.with_context(unreachable)?`。过滤网络最典型的失败方式就是
传到一半断掉,那时面板显示的是 reqwest 自己的英文句子,而不是「换一个代理,或
清空代理让 hub 直连」。校验和那一步的 `.text()` 同样补上。
顺带修掉一处重复:`error_for_status()` 后面曾连着两层同样的 context,于是
「release 里没有 sha256」那条路(404)会把同一句话显示两遍。
2. 付款周期的数字框补 `step={1}` 与 `aria-label="周期长度"`:前者挡住小数(`1.5m`
会被 hub 拒成「要是整月」,用户看到的是自己的输入被拒),后者原本是个没有名字的
输入框。
3. 货币框补 `autoCapitalize="characters"` 与 `spellCheck={false}`(上游有):手机上
自动大写,也别给货币代码画红波浪线。
另补一条上游的断言:五年周期按自己的月数顺延(`60m` → 2031-03-10),以及具名长度
`36m` 走同一个函数。
独立复核(新上下文、只看仍未修的问题)报了一条中危与两条低危,逐条修掉并实测:
1. **中危:续费框会把认不出的已存周期静默改成 yearly。** `cycleMonths` 对 `weekly`/
`2y`/空串这类值返回 NaN,表单于是显示「1 年」,而保存时「按月数比较」对 NaN 恒为
false,就把 `12m` 发了出去、hub 归一成 `yearly` 落库——只改价格也会发生,且原值
无法再写回。port 之前我们的 `update_node` 不校验周期,老库确实可能有这种值。
改法:记住两个控件的**初始状态**,只有被动过才发送周期;认不出的值在提示里原样
显示(「现在存的是「weekly」,不动这里就保持原样」)。
**实测**(真界面 + 真库):塞入 `weekly`,只改价格保存 → 价格 128→999,周期**仍是
`weekly`**;再真去把数字改成 5 保存 → 周期变成 `5m`(两个方向都对)。
2. 顺手补同一处的另一个洞:把周期数字框**清空**再保存,原来会发 `0m`,hub 回 400,于是
价格/货币/到期一起没保存。现在被改动但填的不是 ≥1 的整数时直接提示,不提交。
3. `theme::latest` 解码失败那条:`.context("读不到 {owner}/{repo} …")` 传的是字面量,
面板会把花括号原样显示出来(同一函数另外两条路都是 `format!`)。改成 `with_context`。
4. `useVersions` 空答案重试的注释不实:hub 对空 `hub_latest` 负缓存 10 分钟、空
`agent_latest` 6 小时,所以「连不上 GitHub」时三次拿到的是同一个空答案。行为保留
(hub 刚启动那条路径确实能靠重试拿到答案),把注释改成实话。
上一轮修掉的那条中危 bug(认不出的已存周期被静默改成 yearly)埋在 React 组件里, 测不到——而它正是「面板改了用户没打算改的东西」这一类,值得有回归测试兜住。把判断 抽成四个纯函数放进 `lib/format.ts`(`cycleMonths` 旁边):`cycleFields`(控件初始 状态与可读性)、`cycleValue`(`<n>m` 拼写)、`cycleOk`(hub 会接受的边界)、 `cyclePatch`(该回传什么)。组件只负责调用。新增 `lib/format.test.ts` 并接进 `npm test`(面板 CI 会跑)。 测试覆盖三件事:认不出的值原样回传;没动过的控件按**存储的原拼写**回传(具名长度不 被换成 `<n>m`);边界(0、空、小数、超过 1200 个月都拒,1.5 年=18 个月算合法)。 **重构后真界面复验**(行为与重构前一致):塞入 `weekly` 只改价格 → 价格 777、 周期**仍是 `weekly`**;把数字填成 2000 → 提示「付款周期要在 1 个月到 100 年之间, 填整数」、对话框不关、库里没变。
之前因为没有 Tooltip 原语,`#71` 的货币说明与 `#75` 的地址说明都写成了可见文字。 现在补上原语并把说明收进悬停问号,与上游一致。 **没有新增依赖**:上游用的是 `@radix-ui/react-tooltip`,而我们面板依赖的是 `radix-ui@^1.6.7`(总包,现有 `select`/`dialog`/`switch` 都从它导入)——它本身就 再导出了 `@radix-ui/react-tooltip`,所以 `import { Tooltip } from "radix-ui"` 即可, 另加一个直接依赖只会多一份 Radix 来源。 - `components/ui/tooltip.tsx`:按我们的房屋风格写(`radix-ui` + `cn` + `data-slot` + `tw-animate-css` 的进出场动画) - `Help`:点按也打开(轻触没有悬停),并 `preventDefault` 掉触发器和**标签**的默认行为 ——问号在我们的 `Field` 里位于包住控件的 `<label>` 内,不拦就会被点成「聚焦输入框」 - `Field` 多一个 `help`,货币一栏的填法与例子回到 `help` 里(`hint` 只留动态识别出的中文名) - 主题页的四行说明收进标题旁的问号;「请只安装可信来源」按上游保持常显 **实测**(真浏览器):点击问号 → tooltip 内容与上游一致、`document.activeElement` 仍是 BODY(没被标签抢去聚焦输入框);真实指针悬停同样出 tooltip;主题页那个含五行 且安全提示仍常显。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
两批一起做,同属一次发版。
第 1 批
1. SQLite 的临时文件放到数据库所在目录(上游 #78)
回收空间时
VACUUM要把整个库重建到一个临时文件里。SQLite 依次找$SQLITE_TMPDIR、$TMPDIR、/var/tmp、/usr/tmp、/tmp和工作目录,而我们的两种部署都没有可用的:Dockerfile是FROM scratch+USER 65534,镜像里根本没有/tmp(注释自己写着「Nothing in this image can mkdir」),/属于 root 而进程是 65534 → 六个候选一个都不可写install-hub.sh:377-382是PrivateTmp=yes+ProtectSystem=strict+MemoryMax=256M,/tmp是 tmpfs 的系统上临时副本占内存并计入 256M,库一大就被 OOM 杀掉启动时一次
PRAGMA temp_store_directory(用内存连接设,进程级,必须在打开任何连接之前)把临时目录指到库所在那块盘。实测(直接观测到临时文件落在哪):把库造到 144 MB(180 万行,且都在保留期内,
prune不会先把它们删掉——第一版夹具的时间戳落在 1970 年,被prune删光后 VACUUM 只花了 21 ms,什么也测不到)。一边用os.listdir以约 2000 次/秒轮询四个候选目录,一边调POST /api/db/vacuum:耗时 0.558 s、回收 71.9 MB,抓到/tmp/vactest/etilqs_9e402aa64a254eee——就在库所在目录里。同时盯着的$TMPDIR(这台机器是/var/folders/…/T,SQLite 默认搜索里排第一)、/var/tmp、/tmp三处都没有;hub 的工作目录又是 monitor 仓库,不是那里。所以那个位置只可能是temp_store_directory指定的。(用lsof采样抓不到:临时文件的生命周期比它扫进程表还短。)纠正我先前提过的一句:我用
sqlite3CLI 做的「机制实验」并没有测到 VACUUM——PRAGMA temp_store_directory='/不存在的目录'在设置时就报not a writable directory,CLI 在第一条语句就中止,VACUUM 根本没跑。它证明的是「设不上会立刻报错」,不是「VACUUM 会用这个目录」;后者现在由上面那次观测直接证明。顺带量到:我们 bundled 的是 SQLite 3.50.2,temp_store = 0(文件临时表),temp_store_directory未设时查询返回 0 行。另有一个没深究的现象:hub 启动后把库所在目录改成只读,VACUUM 仍然成功——说明建临时文件时那个目录若写不进去,SQLite 会退回默认搜索(
/tmp等),而不是直接失败。这与修法一致:正常情况那个目录就是可写的(库存放在那里),而 scratch 镜像里默认搜索的候选全不可用,所以设过去的那个才是唯一能用的。没复现到的一点:这台机器没有 Docker,
sandbox-exec也被拒(Operation not permitted),所以「scratch 镜像里必失败」是静态结论(六个候选目录全部不存在或属于 root),加上上游在真 Docker 里的实测;我没有自己跑出那条VFS is unable to determine a suitable directory。2. 货币可填任意三字母代码,付款周期可填任意月数(上游 #71)
面板原来只给 5 种货币、6 种周期。现在货币是输入框(三个字母,大小写都行,存大写),周期是「数字 + 月/年/一次性」,1 个月到 100 年。hub 校验两者;周期存成
<n>m,其中 1/3/6/12/24/36 个月仍存原来的名字,所以为旧版 hub 写的主题照常显示。自动续期因此也支持任意月数。实测(预览库 + 真界面):
twd+60m→ 存成TWD/60m;USDT、港币、0m、1201m、weekly全部 400;编辑框读回货币=TWD(提示「新台币」)、付款周期=5 年;把 5 改成 3 保存 → 36 个月归一成具名triennial,主题公开页显示128.00 TWD / 三年。Intl.DisplayNames边界核过:XYZ原样回显 → 「未知代码」,US/空/USDT抛错 → 退回静态说明。与上游的差异:上游在货币标签旁放悬停问号;我们面板还没有 Tooltip 原语(
@radix-ui/react-tooltip未引入),那两句并进了Field已有的 hint 位置。3. 拒绝安装时说出是哪一半(我们 backlog 的一条)
provisioning_allowed原来只返回真假,四个拒绝分支各自debug!到日志,面板只有一句笼统提示——而看面板的人没有 journal。改成具名的ProvisionBlock(host/https/site/origin):/api/me多带provision_block,403 正文在通用说明后接上具体那一半;面板在节点页按钮旁(原来只置灰不解释)和更新页各显示一句。实测:明文 IP 入口下
GET /api/me→can_provision: false, provision_block: "https",节点页显示「这一跳没有说明自己是 https;反向代理要发 X-Forwarded-Proto: https」。4.
/api/version失败会重试(我们 backlog 的一条)原来一次请求、失败被
catch(() => {})吞掉,那一次失败会让更新页一直显示「查不到版本」、导航圆点也不出现,直到手动刷新。现在失败或答案里一个 latest 都没有时重试两次(1.5 秒、4 秒)。实测(前置代理数请求,可判定):把 hub 的
HTTPS_PROXY指到不可达地址(/api/version回空答案)→ 面板请求 3 次,时间戳…14.939 → …16.449 → …20.456(+1.51 s / +4.01 s);GitHub 可达时 1 次,页面显示「最新 v1.1.0 · 1 台待升级」。第 2 批:从 GitHub 仓库地址装主题(上游 #75)
主题页多一个仓库地址输入框,新接口
POST /api/theme-install:hub 读该仓库最新 release 里的theme.tar.gz安装。地址只取<owner>/<repo>两段,下载地址由 hub 自己拼,仍然只连 api.github.com 与 github.com;填了 GitHub 代理时下载经代理;大小上限、解包检查与上传完全相同。repo()容忍浏览器地址栏常带的?tab=readme-ov-file与#readme(取段之前就丢掉,进不了拼出来的地址——既有测试里?x=1原是被拒的敌对样例,现在改为接受并补了断言),也接受省掉协议的github.com/...latest()把 GitHub 的拒绝分类:404「仓库不存在,或者还没有正式 release」(私有仓库回答一样)、403/429 限流、连不上、接口出错pax_global_header是不落盘的元数据,原先按不支持的条目拒绝,现在计入解压总量后跳过,包继续走到缺少theme.json的检查,那句提示也补上同样指引archive()对下载到的字节也验一次魔数:代理在 200 下返回自己的拦截页时,否则会先报「sha256 不对」,把人引到「release 坏了」实测:
POST /api/theme-install {"url":"https://github.com/spot-probe/monitor-theme-default"}→ 下载(含.sha256校验)、解包、校验、发布全部走通,磁盘上theme.json变成 1.8.2;坏地址(空、非 GitHub、github.com/a)全部 400;spot-probe/monitor这种有 release 但没有主题包的仓库回「release v1.8.2 里没有 theme.tar.gz」。面板截图确认新的安装卡片。与上游的一处差异(我们更严):我们的
archive()还要求 release 里有theme.tar.gz.sha256并核对 sha256(上游没有这一步)。实测monitor-probe的两个主题仓库都发布了它,第三方主题不受影响;真没发布时报错直接说清该怎么做(让作者补上,或下载后手动上传——上传路径不做校验)。校验
cargo test135 通过(新增:临时目录、拒绝原因、货币与周期归一、从 GitHub 装的坏地址、gzip 魔数与 pax 头;被拒值表加了 5 条)·clippy --all-targets0 警告 ·fmt --check干净 · 面板lint/test/build通过。配套:monitor-theme-default#27(续费一栏显示任意周期)、monitor-document#12(文档)。