Files
rond-with-you/.workbuddy/memory/MEMORY.md
T
tamakyi d4e4257a23 启动模板自检 + 站长导航放行 + 统计脚本关口收紧
排查:/admin/settings 渲染到一半就断
- 现象:页面在「统计脚本」那张卡片处截断,没有保存按钮、没有 </form></html>,而 HTTP 仍是 200。
- 根因:跑的是旧二进制(改动前编译),磁盘上的模板却是新的。debug=true 下 pageSet()
  每次渲染都从磁盘重解析模板,于是新 settings.html 引用了旧 setting.Settings 里没有的
  TrackEnabled —— 执行期报错,而 render 只把错误写进日志,客户端只看到半张页面。
- 判据:strings <二进制> | grep -c cleanTrackScript(旧 0 / 新 2)。别用 strings | grep
  TrackScript —— config 里本来就有这个名字,旧二进制也命中,是个假信号。

启动自检(防复发)
- template_smoke.go:pageProbe 给 24 个页面各配一份最小可用数据,New() 里全部渲染一遍,
  任一页失败就拒绝启动。pageProbe 与 pageFiles 双向校验,新加页面必须补探针,
  漏配按失败处理(不是省事)。
- smokeOne 用 recover 兜住方法里的 panic,报「哪一页坏了」而不是崩栈。
- 实测:往 settings.html 插一个不存在的字段 → 启动即报
  「模板自检未通过…admin/settings: can't evaluate field …」,退出码 1。
- 门禁 template_smoke_test.go:一条跑全量自检,一条变异验证(探针换成字段更少的类型
  必须报错且点名该页)。

站长导航被页面开关误伤
- guardPage 只拦访客(站长始终能访问全部公开页),但导航写的是 {{if .Settings.HasPage "x"}},
  没有 IsAdmin 分支 ⇒ 页面开关一关,站长自己也只剩「主页」一个入口 —— 能访问却点不到。
  改成 {{if or .IsAdmin (.Settings.HasPage "x")}}(map/places/timeline/stats/report 五处)。
- 门禁 nav_visibility_test.go:三向不变量(站长仍有全部入口 / 访客一个都没有 /
  全开放时访客也看得到),做过变异验证。
- 访客侧「只有主页」是按后台配置应有的状态,没有动数据。

统计脚本关口收紧(cleanTrackScript)
- src= 只在 <script …> 开标签内部找。原来对整段做 strings.Index,脚本体里一句
  `img.src=u` 就被命中、整段被判危险而清空 —— 页面照样提示「设置已保存」,站长看不出没挂上。
- 开闭标签从「只拒 shut > open」改成严格配平:末尾未闭合的 <script> 会把页面收尾整段吞进脚本。
- 不再静默:签名改成 (script, warn),说明码经 ?warn= 回显成人话;判为危险时整段不采纳、
  库里原脚本与开关都不动(把站长手上好用的脚本一并清掉更糟)。
- 门禁 TestCleanTrackScript 重写:逐条边界 + 定义性不变量(采纳了非空结果时,
  里面开闭标签数必定相等)+ TestTrackWarnMessage(未识别码不得回显)。变异验证做过。
- 文案订正:settings.html 的「conf/app.ini 的 track_script 会作为这里的默认值」有歧义 ——
  它不回填文本框,而是「这个框留空时、注入那一刻才回落到 ini」。

其余
- bin/ 目录删除(15 个 exe / 412MB),二进制只留仓库根 rond.exe;
  技能 rond-web-verify 与 memory details 十八里 bin/rond.exe 的写法同步改掉。
- 验收:./scripts/verify.sh 全绿且 0 skip(PASS=346 FAIL=0 SKIP=0)。
2026-10-09 11:06:41 +08:00

110 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# rond-with-you 硬规矩(索引)
> 全文:`.workbuddy/memory/MEMORY-details.md`(38 节);验证机制:技能 `rond-fix-verify`;备份包解析/导出:技能 `rond-backup-parse`。本文件只做关键词索引——**新规矩写 details,别堆这里**。
>
> ⚠️ 本文件**按字符数**注入,硬上限 `MAX_MEMORY_CHARS = 1e4`(WorkBuddy `app.asar` 里 `getWorkingMemoryContent`;中文 1 字算 1,不是按字节)。超了会被 `substring(0,1e4)` 截断并写明「先整理再干活」——即整段尾部静默丢失。日常日志与本 details **不参与注入**,可以随便写。现值见 `wc -m`,留 20% 余量。
## 落库 / 精度(details 一、四、五、十六、十九、廿八A)
- 建地点+到访**同一事务**;新行号只走 `nextSrcPK`+`clearStaleRaw`,**禁 `MAX(src_pk)+1`**。`seq_test.go`
- 行程派生列同导入(`validateMovementForm`):`from/to_place_id`(留空=绕过区域过滤直露访客)、`distance_km`、`duration_min`。
- 表单精度低于真值 ⇒ 只改备注也**静默改时间/坐标**:`keepStoredTime`/`resolveMovementEnd`/`keepStoredCoord`;改到访时间须 `clampVisitMovements` 夹行程。
- `src_pk` 只单数据集内有效;跨备份键=`ZIDENTIFIER_`/`ZUID_`;`contentHash` 别用 `SrcPK|Arrival`。
- 坐标留档 `coord_source/coord_sys/src_lat/src_lon`;换算只走 geo 包与 `/api/coords`。
- 脏数据:`appleTime` 超范围浮点转 int64 **回绕非饱和**(先卡秒数);`validCoord` 非法归 `(0,0)`。`dirty_test.go`
- 聚合刷新(`saveVisitTags`/`refreshPlaceAgg`/`refreshDatasetAgg`/`activityFlags`)**必须返回 error**。`agg_error_test.go`
- 收藏切换走单语句 `UPDATE … RETURNING bookmarked`。`visit_bookmark_test.go`
## rond 导出(details 二、廿六)
- 铁律:**PG 覆盖管理列、`entity_raw` 还原其余列**;基线已有行保真机原值(`keepNilCols`+`known[pk]`)。
- 新行只补 `ZRAWLATITUDE/LONGITUDE`、`ZVISIT.ZIDENTIFIER_`/`ZACTIVITY.ZUID_`(确定性=表名+src_pk)、`ZICON_`、`ZLASTARRIVALDATE_` 哨兵 `-63114076800`。
- `ZMOVEMENT.ZTYPE_` 不能 NULL、**别当管理列**(会洗掉 type=4);`queryTransports` 必须 `GROUP BY transport_src`。
- rows 收尾必须 `drainRows`(先 `Err()` 再 `Close()`)。`parse_rows_test.go`
- **扫进裸 `bool` 的列必须先 `COALESCE`**:`weather.is_daylight` 漏写 ⇒ NULL ⇒ 整包拒收(details 卅C)。
- 核对导出包**别按留档 UUID 找行**:站点新建行会被补派生 UUID(`coreDataUUID`)。
- **换基线**(底座换代,`/admin/baseline`):站点数据一行不动,只换底稿。判据=到访 UUID 重合率(`matched==0` 硬拒)+ 干跑导出;记档 `datasets.baseline_*`,`ensureBaseline` **必须先看记档**;留档名**必须防重名**(秒级时间戳会让回滚覆盖掉刚生成的留档)。`baseline_swap_test.go`(details 卅三)
## 迷雾 / 遮罩(details 三、九、十、廿二)
- `.fwss`=zip、条目 zlib;`Model/*/`=头 32768B+每块 515B;`Model/~/`、`Model/#/` 原样写回;清空/替换都要连 `fog_raw_files`。
- 导入三段式(preview 暂存 → 差异页现算 → `?stash=` 落地);带 stash=urlencoded、无 stash=multipart 两条都留。
- `fog_masks` 圆心 GCJ-02+半径,判定前换 WGS-84 网格;遮罩只对访客;`stats.Filter` 有**两套 where**(`visitsWhere`+`movementsWhere`),漏一个=「到访藏了里程还在」。
- 瓦片须与底图同 `crs`;带地图页须 `p.HasMap=true`。
- `tileDigits` 补前导 0;`tileName` 尾缀 `len(s)-2` 单数字编号时为 -1 ⇒ panic;解码守卫 `len(name)<8`。
- `fogIndexV` 必须 `atomic.Pointer`(普通指针=真 data race)。`fog_race_test.go`(-race)
## 前端 / 模板 / CSS(details 六~八、十七、廿三、廿八D、卅七)
- ⚠️ **对访客公开的块,措辞必须按身份分支**(`{{if .IsAdmin}}我的…{{else}}站长…{{end}}`):
写模板时人站在站长视角下笔,「我的结论 / 你自己 / 我推荐的地点」露给访客就没人读得懂。
照 `place.html` 投票区与 `rond-map.js` 气泡的写法。门禁 `TestVisitorPagesUseThirdPersonCopy`
(以访客 ctx 渲染 `/`、`/stats`、`/report` 逐个断言禁用词,并确认站长那份仍说「我的」)。
- 改 Go 必须重启;模板/CSS/JS 只在 `debug=true` 热加载(`AssetV` 启动哈希)。⚠️ **新模板+旧二进制**=「渲染到一半仍 200」;启动自检 `smokeRender` 会拒启(四十一)。
- 拼查询串走返回 `template.URL` 的方法(`PlaceLink`/`EditPlaceLink`/`MapLink`/`EditLink`),勿手工转义。
- 窄屏覆盖写 `app.css` **末尾那块**;宽表堆叠类名**成对** `class="tb tb-stack"`;触控 44px。`tb_stack_test.go`
- 加表格列:`PlaceCols`/`scanPlace` **成对**、新列追加最后;CJK 挤换行只抬该表 `min-width`。
- ⚠️ 模板 JS 别用「heredoc+python」写 `\n`(`<script>` 语法错、交互全哑而模板测试照过);用 Edit,改完 CDP 验 JS 报错为空。`inline_script_test.go`
- 只在访客复现的 bug:先比「无 cookie 原始 HTML vs CDP 后 DOM」;别让 JS 从 `{{if .IsAdmin}}` 块读 id。
- **异步回包一律发序号作废**:`var seq=++xxxSeq;` 在 `if(!on) return;` **之前**,回包首行 `if(seq!==xxxSeq) return;`。`map_async_cancel_test.go`
- `window.localStorage` 一律经 `lsGet`/`lsSet`(隐私模式裸访问会抛);`previewQueue[id]` 须在 `if (live)` 之前排队、回调消费后删。
## 编辑页 / 重复到访(details 三十)
- 「重复到访」卡片:判定与 `tools/dedup_site_visits.py` 同语义、**UUID 是前提**、基线对不上就不给按钮;改指合并标记只动**本来就有**的键(`create_if_missing=false`)。`dup_visits_test.go`
- **已并入 / 子段(`ZMERGEDTO`/`ZPARENT`)只在读侧收口**,判据唯一一份 `stats.SoloVisitSQL`;天气与位移**不收口**;导出侧一字节不动。收口点清单见 details 卅G。
- 相对计数断言会**假绿**(变异让基准值一起膨胀),必须写定义性不变量;`render` 是流式直写,`code==200` 不等于渲染完。
- ⚠️ ingest 地点聚合回填必须排在 `insertEntityRaw` **之后**(判据读 entity_raw)。
## 活动 / 类别(details 廿四、廿五)
- **活动是表,类别不是**——类别只是 `places.poi_category` 字符串列;显示名**绝不写进该列**(`TestCategoryLabelNeverTouchesValue`),走 `categoryLabel(v,custom)`,Go 侧算好塞。
- 站内新建行 `entity_raw.raw IS NULL`;`ZCATEGORY_` 真机只有 `MKPOICategoryXxx` 枚举。
- 类别候选:编辑/照片页=库里在用 `GROUP BY`;补全页=在用 ∪ 静态表;下拉留「其它(自己写)」,默认「不动」。
## 归属口径 / 防复发(details 卅五)
- ⚠️ **铁律:这份记录能不能从备份重新导入?** 能 ⇒ 可挂 `dataset_id … ON DELETE CASCADE`
(places/visits/movements/weather/activities/tags/entity_raw);**不能 ⇒ 只留 dataset_id 当出处、不许挂外键**
(place_images/place_notes/place_votes/topics)。挂在备份里没有的表上 = 删数据集就永久丢失。
- 两道门禁都钉着它:运行时查 `pg_constraint`(表必须在白名单里,反向也查)+ **静态扫 `schema.sql`**
(新建的库走 CREATE TABLE,只看运行时会有盲点)。`dataset_ownership_test.go`
- **体检常驻后台首页**:库里 N 张 / 本地盘上 M 个 / 落单 K 个(`imageHealth`)——
事故的本质是「行与对象悄悄对不上、没人发现」,让它当场可见而不是等照片消失才追。
## 跨备份 / 附属数据(details 十九~廿一、卅四)
- 预演按 `VisitUUID`/`ActivityUUID` 对齐、只读;`raw #>> '{ZIDENTIFIER_,$blob}'` 是 base64 须转 hex 比;地点只认坐标(`PlaceCoordKey` 6 位)。
- 附属数据(备注/投票/图片)主键=`place_key` 坐标键(非 dataset+src_pk);Go 与 SQL 两套须逐位一致。
- **站长结论(`place_votes`)与访客票(`place_visitor_votes`)是两码事**:各存各表、各看各榜;
统计页拆成「我的结论」与「访客点赞」两块(`votes.TopVisitorVoted`,`HAVING` 只算有票的那个方向)。
⚠️ 票表也按坐标键跨数据集读 ⇒ 用例必须按 `place_key` 清历史票,否则票数越跑越多。(details 卅六)
- ⚠️ **附属数据不能挂数据集外键**:`place_images` 曾按 `dataset_id` CASCADE ⇒ 删旧数据集=删图片行、**文件却留在盘上**(读取按坐标键、行按数据集级联,两套口径打架)。已摘掉外键;恢复走后台「图片挂载恢复」,认地点靠旧数据集留下的 `baselines/<id>.sqlite`(**别清 baselines 目录**);⚠️ 来源必须**全部读进来合并**(places → entity_raw 留档 → 该 sqlite 及**每份 `.bak`**)——换基线会覆盖底座文件,只看当前那份会整片认不出(实测 40 张只挂回 7 张)。`image_recovery_test.go`(details 卅四)
- ⚠️ 恢复**同时扫对象存储**(R2/S3):`media.ListObjects`(ListObjectsV2 + SigV4,查询串先编码再签),
需 `s3:ListBucket` 权限;列不动时本地照救 + warning。**专题同理**:`topics` 已改成归人不归数据集
(`user_id` 过滤,内容仍照生效数据集算)。用户级数据靠 `Dataset.UserID` 定位。(details 卅四)
- ⚠️ `schema.sql` 迁移块必须收敛且排在依赖表之后(插在 `place_images` 前:开发库无症、**新库起不来**)。
- 合并 `backup_merge.go`:整个 `withTx`、不删除;新行 `*_visit_src` 写**库里的 src_pk**;必须 `writeRawRow`;删地点前先查有无到访(CASCADE)。
## 后端 / 性能(details 十、廿八C)
- `findSourceArchive` 先按 `size_bytes` 筛再算 sha256(`pickArchiveBySha`)。池 8/8 + `db.WarmUp(d,6)`;页面内查询 `runJobs`(≤6);别用 `fog.Index.All()`;改遮罩须 `masks.invalidate()`+`fogTiles.clear()`。
- `placeCols` 子查询必须带 `vN.dataset_id=p.dataset_id`。`stats/placecols_test.go`(按子查询分段判)
- multipart handler 直接 `ParseMultipartForm`(`r.ParseForm()` 毒化字段);**之前必须 `MaxBytesReader`**。五个上传口:备份/GPX/迷雾/照片/地点图片。
- 包了 `ResponseWriter` 必须实现 `Unwrap()`(`gzipWriter`/`headerSent`);`recover` 写错误页前须看 `headerSent`。`server_error_test.go`
- 「先落盘再解析」的上传 handler,落盘后每条失败路径都要删半成品。`upload_cleanup_test.go`
- 批量勾选走 `*Bulk` + `p.src_pk = ANY($3::bigint[])`(pgx 直接吃 `[]int64`);**全局内存缓存必须带上限**(`fillupCache` FIFO 淘汰最旧 1/4);**同一查询别在一个 handler 里查两遍**。`admin_notes_bulk_test.go`+`fillup_cache_test.go`+`stats_weekday_once_test.go`
- 加 `setting` 字段改三处:`Default()`/`Load()`/`settingsSubmit()`。
## 安全 / 跳转(details 廿八B)
- 跳转唯一判据 `safeInternalPath`(`view.go`);四处共用。`redirect_target_test.go`(含禁弱判据静态扫描)
- cookie `Secure` 按请求判断(`cookieSecure`),否则本机 http 与 CDP 都登不进。
- CSRF 只加同源 Origin/Referer 守卫(`csrf.go`):仅写方法、`Origin: null` 拒、两头都缺放行。
- `clientIP` 只认 XFF **最后一段**(须 `netip.ParseAddr`);⚠️ 投票匿名身份=`hash(cookie,IP)`⇒改取法即改身份。
- GPX 按属主收口:`/api/gpx` 取生效数据集 `user_id` 再 `WHERE user_id=$1`;`adminGpxDelete` 查 `RowsAffected()==0`。`gpx_owner_test.go`
- 下载名走 `downloadDisposition`(ASCII 兜底+`filename*=UTF-8''`);四出口都经此。`download_name_test.go`
- `writeRawRow` 必须上抛错误让 `withTx` 回滚。**两步验证两闸都要**:`loginGuard`(键 `"2fa:"+用户名`)+`CodeReplay.Consume`。`twofa_guard_test.go`+`code_replay_test.go`
## 口径 / 404 / 验收(details 十一~十三、廿七、廿八E、卅二)
- ⚠️ **跑全量验收前**:① `VACUUM (FULL, ANALYZE)`(死元组涨到 3.3GB 后级联 DELETE 变 IO 爬虫,看着像 hang);② 库内必须有 `is_active` 数据集,否则 `internal/stats` 6 用例**假 skip**。跑着时别并发跑定点测试。
- 区域判定**两端各过一遍再 AND**(同 `regionSQL`);`movementsWhere` 也必须两端各判,不能用 `EXISTS(... IN (from,to) ...)`;比较列一律 `COALESCE(col,'')=$n`。`region_consistency_test.go`
- 其余(累计停留口径、`Filter.Verdict` 下沉、404 分档、验收四件套、覆盖率 `-coverpkg`)见 details 卅二。
## 备份合并 / 导出链路(details 卅一 + 廿一)
- 对齐键:到访=`ZIDENTIFIER_`、地点=`PlaceCoordKey`(坐标 6 位,**不认名称**)、活动=`ZUID_`;合并前必须**重算差异**。
- 地点配对只有一份实现 `rond.MatchPlaces`(差异与合并共用);同名兜底必须有距离上限(`nameMatchMaxKm=1.0`)。
- 差异页默认 radio 是「**用实机**」;写合并测试要照这个提交(抓 `value="backup"`),否则测的不是真实点击行为。
- 自检不变量:合并后没有任何地点被搬动 >1km、也没有任何到访离它的实机地点 >1km。
- **三条铁律**:① app 只靠 `Z_PK` 解引用地点/活动 ⇒ 导出的编号必须与**数据集基线**同一套 ⇒ **基线必须是手机包**;② rond 导入是**整库替换** ⇒ 导入前先清空 app 数据;③ 站点数据是超集 ⇒ 用「手机导出作新数据集 + 旧站点导出当备份合并进去」对齐;`ZRAW` 这类从源包抄来的计数会越界,靠导出侧夹取兜住。