Files
rond-with-you/.workbuddy/memory/2026-09-14.md
T
tamakyi 3f1b737a78 记忆:把 .workbuddy/memory 纳入版本控制
用户要求这份记忆跟着仓库走,而不是只留在本机。

.gitignore 里挡路的是 `.workbuddy/`。改成反忽略六行,并且**只放行 .md**:
别用 `!.workbuddy/memory/**`——它会把 memory 下任何文件都放行,本项目真发生过
`web.test.exe` 混进暂存。复核用 `git check-ignore -q --no-index` 逐条打靶:
memory/*.png、memory/sub/x.md、memory/MEMORY.md.bak、plans/a.md 全 IGNORED,
17 个 .md 全 included;`git add -An .workbuddy/` 恰好 16 个文件。
其余 .workbuddy 内容(计划、本地设置、临时产物)继续忽略。记忆里没有真凭据
(唯一命中是 2026-09-14.md 里的 `password=rond-admin`,那是项目默认口令)。

顺带做两件整理:

1. 取证了注入上限。以前按「字节数」判断 MEMORY.md 是否超限是错的:
   WorkBuddy 的 `getWorkingMemoryContent()` 用 `MAX_MEMORY_CHARS = 1e4` 比 JS
   `content.length`,即**字符数**(中文 1 字算 1);超了 substring 截断、尾部静默丢失。
   本 details 与每日日志都不参与注入,只有 MEMORY.md 受约束。
   现已从 6604 字符收到上限的 66%,并把这条上限写进索引抬头。
2. 把「备份合并」「导出/恢复三条铁律」两段长文迁进 MEMORY-details.md 的卅一/卅二节,
   索引只留关键词指针。行尾统一为 LF(仓库 .gitattributes 是 `* text=auto eol=lf`)。
2026-09-30 18:31:08 +08:00

239 lines
19 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.
# 2026-09-14
## rond-with-you:把 rond 备份变成足迹站点(从零建成可运行版本)
### 背景
用户提供 `data/20260914_151649.rondbackup`(3.4MB),要求做一个后台可上传该文件、
前台展示足迹地图 / 类型 / 日期筛选的站点。技术栈由用户指定为 PostgreSQL(本地测试环境
`postgres / 127.0.0.1:5432`),后端语言 Python 或 Go 自选。
### 备份结构(已实测确认)
`.rondbackup` 是 zip,内含 Core Data 库 `LifeEasy.sqlite` + `-wal` + `-shm`。
**关键点:数据几乎全在 WAL 里**(主库 348KB / WAL 3MB),必须三个文件一起解出再打开,
否则只能读到极少量记录。Core Data 时间戳基准为 2001-01-01 UTC。
主要表与本次数据量:`ZVISIT` 824、`ZLOCATION` 323、`ZRAWVISIT` 2246、`ZMOVEMENT` 791、
`ZACTIVITY` 30、`ZHOURLYWEATHER` 557、`ZTAG` 8、`ZTRANSPORT` 5、`ACHANGE` 4810(未导入)。
标签关联表命名规则为 `Z_<实体号>TAGS_`,如 `Z_11VISITS_` = Visit↔Tag(11=Tag,18=Visit)。
导入结果:823 条到访(跳过 1 条到达时间缺失的空记录)、323 地点、8 标签关联、791 位移、
557 天气;跨度 2025-02-10 ~ 2026-09-13,估算里程 16861.9km,城市 9 座 / 省份 2 个。
### 技术选型
- Go 1.25 + 标准库 net/http + html/template(未引入 Web 框架)
- PostgreSQL + `pgx/v5`;备份解析用纯 Go 的 `modernc.org/sqlite`(无 cgo)
- 前端手写 CSS,Leaflet + markercluster 本地内置(无 CDN 依赖)
- 口令 PBKDF2-SHA256(标准库 `crypto/pbkdf2`);会话为 HMAC 签名 Cookie,
签名混入口令摘要前缀,改密即让旧会话失效
### 踩过的坑(都已修复)
1. `//go:embed static` 的资源路径相对包目录,原放在仓库根的 `web/` 下会编译失败,
已移动到 `internal/web/{templates,static}`。
2. 地点查询用 `WHERE ... EXISTS (SELECT ... visits v` + `inner.SQL()[7:]` 拼接,
切片把 `WHERE` 关键字一起去掉了,导致 `SQLSTATE 42601`。改为 `inner.expr()` 并给
子查询占位符加偏移(`where.off`),避免与外层 `$1` 冲突。
3. 模板函数 `dur` 形参写死 int64,而 `VisitRow.DurationMin` 是 int,渲染时报
"wrong type for value",改成接受 `any`。
4. 页面模板若共用 `templates/*.html` 一次性 ParseFS,各页 `{{define "content"}}`
会互相覆盖;改为每个页面与 layout 单独组成模板集。
5. 月度图表只查有数据的月份,会把空档压缩掉;改为补齐成连续月份序列并标注年份。
6. 移动端导航会折行撑坏布局,窄屏改为隐藏站点名、导航横向滚动、按钮文案缩短。
地图缩放/图层控件在窄屏改挂右下角,避免与全宽筛选面板重叠。
### 环境备忘
- `GOPROXY=https://goproxy.cn` 可用;`proxy.golang.org` 不通
- jsdelivr 可下载前端库;高德瓦片 `webrd0X/webst0X.is.autonavi.com` 可直连
- playwright-cli 通过 `npx --yes @playwright/cli@latest` 调用;**浏览器进程会在两次
bash 调用之间被回收**,open / goto / screenshot 必须放在同一次调用里
---
## 第二轮:地图偏移 + 重复导入 + 隐私默认 + 底图配置(P0/P1 收尾与回归)
### 坐标结论(已实测,写进 README 了)
rond 入库的 `ZLOCATION` / `ZRAWVISIT.ZLATITUDE` **本来就是 GCJ-02**,不是 WGS-84。
证据两条:① 2246 个采样点「解析坐标 − 原始 GPS」中位差 lat −0.002823 / lon +0.004187,
与 GCJ-02 理论偏移(−0.002825 / +0.004187)残差 0 米;② 与世界迷雾覆蓋比对,
逆 GCJ→WGS 后命中率 92.3%,直接用原始值只有 32.8%。
→ 高德 / 天地图这类 GCJ-02 底图**直接画,不要再纠偏**;只有 WGS-84 底图才做 GCJ→WGS 逆变换。
旧代码对每个点都套了一次 `gcj()`,所以叠了约 1.1km 的双重偏移。
### 底图配置
`conf/app.ini`:`map_provider` = amap(默认免密钥)/ tianditu(需 `map_key`)/ custom
(需 `map_tile_url` + `map_crs`)。填了 `map_key` 后瓦片走服务端代理 `/tiles/{layer}/{z}/{x}/{y}`,
密钥不出现在页面源码;无密钥则浏览器直连。前端读 `/api/map-config` 拿 `crs` 决定要不要逆变换,
切换底图时重新投影已有标记。
### 本次修掉的三个 bug(都是实测复现过的)
1. **瓦片代理子域写死 0** → 高德子域只有 01~04,`webrd00` / `wprd00` 根本不解析。
改为按 `(x+y)%count` 轮转,高德 1..4、天地图 0..7。代理模式实测 4 张瓦片全部 200。
2. **`Stats.Skipped` 从未赋值**(`ingest.go` 构造 Stats 时漏了字段)→ 「已跳过 N 条」
与「内容完全重合」两个提示全是死代码。补上后实测提示正常。
3. **`movements` INSERT 不写 `user_id`** → 位移永远无法去重(重导入 791 条变 1582 条),
且下次启动时迁移的回填 UPDATE 会撞上 `idx_movements_dedupe`,**整个站点起不来**
(`SQLSTATE 23505`,实测复现)。已补 user_id + `ON CONFLICT DO NOTHING`,
并把迁移里三个回填 UPDATE 各自包进 `EXCEPTION WHEN unique_violation`,保证迁移再也不会拖垮启动。
### 数据集模型:已定「快照式」并落地(用户在两个方向里选了快照式)
「自然键跨数据集全局去重」与「一次上传 = 一份完整数据集」互相矛盾:全局去重下,更新备份里的
新记录只会落进未激活的数据集,页面上完全看不到(已实测:删 1 条后重导,新记录进了
`is_active=f` 的数据集 #2,页面仍显示 822 条)。
最终实现(快照式 B+):
- 一份备份 = 一份完整快照,行级唯一性只靠各表自带的 `UNIQUE (dataset_id, src_pk)`
- `schema.sql`:`DROP INDEX idx_visits/movements/weather_dedupe`(迁移里清理旧索引),
`datasets` 加 `content_hash` 并建 `(user_id, content_hash)` 索引
- `ingest.Import`:先按 `contentHash(b)`(到访自然键排序后 sha256)查重,命中就复用旧数据集
并返回 `Stats.Identical=true`;否则建新快照,最后 `UPDATE datasets SET is_active=(id=$1) WHERE user_id=$2`
让最新快照接管展示
- `handlers_admin.go`:同 SHA 重复上传改为「已导入过,未重复导入」并顺手删掉刚落盘的文件
(顺带修了删除数据集不删上传文件的 P2);提示语明确写「已切换为当前展示」
- 回归全绿(一次性库实测):首导 823 / 同文件复用 / 内容同字节不同的复用 / 含 1 条新到访的
备份 → 新快照 #2 824 条接管展示,页面上新记录可见
- 已知小尾巴:存量数据集的 `content_hash` 是 NULL,无法识别与它的内容重复,只影响第一次
重传;后续上传正常
- 交叉编译产物 `dist/rond-linux-amd64` 已同步重编
### 回归验证配方(可复用,别动真实库)
1. 建一次性库:`CREATE DATABASE rond_dedup_test`
2. 写 `conf/app.test.ini` 指向它、`listen = :8090`、独立 upload/data 目录
3. 首次启动自动建表并生成 `admin / rond-admin`
4. `curl -X POST -d "username=admin&password=rond-admin" .../admin/login` 拿 cookie
5. `curl -b <jar> -F "[email protected]" .../admin/upload`,提示语在 303 的
`Location` 头里(URL 编码,`urllib.parse.unquote` 解一下)
6. 造「内容相同但摘要不同」的备份:用 python `zipfile` 按原条目重打包即可换 SHA
7. 收尾:kill rond.exe → `DROP DATABASE` → 删 `conf/app.test.ini` 与测试目录
### 其他备忘
- `go build ... | head -20 && echo OK` 会**掩盖编译失败**(管道退出码来自 head),别这么判成功
- Windows 上用 python 写 `/tmp/x` 会落到 `C:\tmp`(和 Git Bash 的 /tmp 不是一处),
尽量写项目内路径
- 后台删除数据集只删 DB 行,`storage/uploads/` 里的原始文件会留在磁盘上(P2,待清理逻辑)
### 世界迷雾叠加(已全部落地并验证)
- `internal/fog`(fog.go 解码/编码、store.go 内存索引+Import(merge)、render.go 瓦片渲染)
- 端到端:Go 解码与 Python 原型一致(52 tile / 8701 块 / 575753 格),编码回环无损(299112B)
- 路由:`POST /admin/fog`(mode=overlay|cover)、`GET /admin/fog/export`、`GET /tiles/fog/{z}/{x}/{y}?crs=`
- 前端:mapConfig 下发 fog 统计,rond-map.js 图层控件加「世界迷雾」,切底图自动换 crs 重拉
- 渲染两坑:① 低缩放级按全球块范围双循环 = z0 卡死 43 亿次 → 改为遍历块表+粗裁剪
② `image.RGBA` 是预乘 alpha,直写颜色被 png 编码器除回 alpha 变绿 → 用 `image.NRGBA`
- 对齐验证:足迹点 WGS 格位图已点亮;gcj02 渲染像素与足迹点像素偏差 0.0px(数学);
z15 近景合成图迷雾沿道路分布、红点落在迷雾格上(视觉)
- 测试:overlay 删块恢复 ✓、cover 清空重导 ✓、导出→再导入格数不变 ✓
- 测试配置留在 `conf/app-test.ini`(已 gitignore);主站 8080 还是旧二进制,重启后才有迷雾功能
- 19:07 主站已用新二进制重启(后台任务),真实库迁移出 fog_blocks ✓,fog 瓦片/配置接口正常;
真实库尚未导入迷雾,等用户在后台传 .fwss
- 整体检查:build/vet 零问题;仓库历史文件是 CRLF 行尾(gofmt 全文件 diff),只有 internal/fog
做了 gofmt;fog.go 里多余的 filepath_Base 已换成 filepath.Base
### P1+P2 六个功能(19:18-19:38 全部落地并冒烟通过)
- 迷雾覆盖统计:fog.CellOf/AreaKm2/Index.CellLit + web/fogCoverage,stats 页新区块;
坑:SQL 少写一列但 Scan 五个目标 → 静默返回 nil(区块不显示),已修
- 行程轨迹:/api/track(movements+places 起终点,5000 段上限),map.html 行程轨迹开关,
rond-map.js setTrack,随底图坐标系重投影
- 到访热力:vendor leaflet-heat.js(unpkg 0.2.0),map.html 热力开关 + setHeat,权重=到访次数
- 年度报告:/report?y=,Nav 新增「报告」,report.html(KPI+城市榜+南北两端+交通)
- 数据导出:/admin/export/places.{geojson,gpx},全量地点含家/工作(站长本人数据),
GCJ-02 原样导出并在 properties 注明 crs
- PWA:manifest.webmanifest + PIL 生成的爪印图标 192/512 + layout head meta
- 验证:测试库(rond_exp_test,已删)验导出 323 features/323 wpt、迷雾覆盖 47.8km²、
河池 43/93 等;主站 6 条路由全 200
- 所有 Go 文件已 gofmt(含 LF 行尾归一,为将来 git init 首提交做准备)
### 三个增强功能(20:00-20:45 全部落地并验证,主站 8080 已用最终二进制重启)
- **OSM 免密钥兜底**:高德瓦片 tileerror≥8 次前端自动切 OSM + toast; clarification:Leaflet 是渲染库
不是瓦片源,高德本来就是免 key 直连默认底图
- **公开页面开关**:settings 保存 `pages`(map/places/timeline/stats/report),guardPage 中间件
对匿名访客 404,管理员放行,导航同步隐藏;实测 anon /stats=404、admin /stats=200
- **TOTP 两步验证**(internal/auth/totp.go + handlers_admin.go):RFC 6238 HMAC-SHA1 30s ±1 窗口;
两步登录中间令牌 `2fa|uid|exp|mac`(绑定口令摘要,改密失效,5 分钟);关闭 2FA 也要求新鲜验证码。
全流程实测通过:绑定→确认→登出→密码登录→动态码→/admin 200;负例(错码/篡改令牌)均被拒;
关闭后登录免第二步 ✓
- **两个坑**:① TOTP 计数器曾用 `t.Unix()/int64(30*time.Second)`(除以 300 亿),改 `unixSec/30`;
② login2FA 曾用 `SplitN(pending,"|",2)[0]` 取 uid(取到的是 "2fa" 前缀),改 `parts[1]`
- **测试方法坑(重要)**:html/template 会把属性值里的 `+` 转义成 `&#43;`(防 UTF-7),curl 从 HTML
抠隐藏字段直接回传会验签失败——浏览器会解码实体,这不是产品 bug。测试脚本要先 `sed 's/&#43;/+/g'`
- 验证用测试账号:首次启动自动生成 admin/rond-admin(README 有写)
- 清理:rond_2fa_test 库已 DROP、conf/app-2fa.ini 与 .tmp-test/2fa 已删;conf/app-test.ini 保留
(gitignored,回归配方用)
- venv 的 python.exe 在 `envs/<name>/Scripts/python.exe`(不在根目录),用户记忆里写的 sss 环境可用
### 口令重置(21:05)
- 主站真实库(rond_with_you)的 admin 口令早已改过,默认 rond-admin 只对空表新库生效
(EnsureDefaultUser 见 users 非空即跳过);用户忘了旧口令,用项目内 auth.HashPassword 写了一次性
重置程序(.tmp-test/resetpw,用后已删)把口令恢复为 rond-admin,curl 实测登录 303→/admin 200 ✓
- 改口令会同时让旧会话失效(session MAC 混入口令摘要前缀)——实测无异常
- 遗留建议(P2,未实施,等批准):默认账号首次登录时页面没有任何「请修改默认口令」的提示,
README 有写但站点不强制;可考虑 EnsureDefaultUser 建的账号加个首登 flash 提示
### 默认口令警示 + 迷雾后台管理增强(22:30-22:45,主站已重启生效)
- adminHome 检测 admin 口令仍为 rond-admin 时,后台首页顶部渲染警示条(含改密/2FA 链接);
实现:AdminData.DefaultPW = auth.VerifyPassword(p.User.Hash, "rond-admin"),仅后台首页每次渲染时验证一次
- 迷雾卡片增强:空态引导(尚未导入提示)、块/格/估算面积/最近更新时间(fog_blocks.max(updated_at),
sql.NullTime + 模板函数 dtt 接受 *time.Time)、「清空迷雾」按钮(POST /admin/fog/clear,confirm 后
DELETE FROM fog_blocks + fogResetIndex() 换掉内存单例索引)
- 模板函数注意:text/template 调函数时指针参数不会自动解引用,可空时间字段要么加 *T 版本函数,
要么模板里 if 判空后用 deref
- 安全全链路实测:导出(299112B)→清空→空态→cover 重导→8701 块/575753 格/47.8km² 完整还原,瓦片 200
- 用户真实库迷雾是用户 22:26 自己在后台导入的(说明迷雾 UI 其实可用,只是缺状态展示让人困惑)
### 迷雾可视化 A+C + 传输层性能优化(23:50-00:25,主站已重启生效)
- 迷雾配色改高对比:填充 #307ae0 a=132、边界 #103484 a=232;render.go 改为两遍渲染
(先填 mask,再按四邻域判定填充/描边),瓦片外邻格按「已探索」处理以免接缝处出现网格伪影。
实测贵阳 z12 瓦片:填充 5572px / 轮廓 663px ✓
- 前端加「迷雾浓度」滑杆(L.Control + localStorage rond.fogOpacity,默认沿用图层 opacity 0.9)
- 性能:新增 internal/web/gzip.go(gzip 池 + 首字节判 content-type,PNG 透传);
assets.go 计算静态文件哈希作 assetVersion,cacheStatic 对 ?v=匹配者给 immutable 一年;
/map 不再内联 points(58.6KB→12.5KB,gzip 3.1KB)改走 /api/points;leaflet-heat 改为
首次勾选热力图时动态加载(ensureHeat)
- 实测:/stats 38.3→5.4KB、/api/track 38.9→6.7KB、/report 6.3→2.0KB
- 坑:模板函数传 *time.Time 不会自动解引用(另见上条);server.go 里 gzip 是包级函数不是 Server 方法
### 访客可见区域白名单 + 地点别名/备注(01:00-01:30,主站已重启生效)
- 区域白名单:setting.Settings.Regions(省/市/区任意层级);stats.Filter.Regions 在 visitsWhere 里
生成三段 IN 条件(province/city/district 各一份占位符);filterOf 里仅 `!p.IsAdmin()` 时注入,
因此地图/地点库/时间线/统计/报告全站生效、管理员全量。
实测:设「贵阳市」→ 匿名 73 点全为贵阳,管理员 283 点 ✓(测完已清空)
- 地点备注:新表 place_notes(user_id, src_pk, alias, note),按 src_pk 而非 places.id 存,
重传备份不丢;internal/notes 包(All/Save,空则删记录)。Place 加 SrcPK/Alias/Note 三个字段,
placeCols 末尾追加 p.src_pk(scanPlace 同步)
- 显示约定:别名对外替代原名(applyNotes 里直接改 Name,各页面/地图气泡自动生效),
备注仅管理员可见(applyNotes 传 admin 标志)
- 坑:applyNotes 传切片,单地点场景必须 `ps := []stats.Place{place}; ...; place = ps[0]`,
直接传字面量切片改不到原值
- 后台设置页新增「访客可见区域」textarea(一行一个),保存时按行 trim 去空
### 备注可见性开关 + 后台备注管理页(01:25-01:35,主站已重启生效)
- setting.Settings.NotesPublic(notes_public,默认 false);applyNotes 参数由 admin 改为 showNote
= p.IsAdmin() || p.Settings.NotesPublic,别名始终对外、备注按开关
- 新增 GET /admin/notes(notesPage + templates/admin/notes.html):LEFT JOIN places 取地点名/城市/到访数,
支持 ?q= 搜索(别名/备注/地点名 ILIKE),每行就地保存/删除(删除=提交空值,Save 会自动删记录),
按 Referer 判断保存后回管理页还是后台首页
- 后台首页导航与地点详情页都已挂入口
- **事故与修复**:用 curl 单字段 POST /admin/settings 测区域白名单时,未提交 pages 字段导致
「公开页面」被清空(/map 等全部 404)。教训:settingsSubmit 是整体覆盖,任何脚本化测试都必须
把 pages/modules 等字段一并回传;已用全字段 POST 恢复
- Git Bash 里 `curl -o /dev/null` 偶发 exit 23(写入失败),改成 -o 文件更稳
### 四个增强功能 F1–F4(20:5x 收尾,主站已重启实测)
- F1 出行方式配色:apiTrack 返回 `modes` 图例(空方式归「步行/其他」灰色 #8e8e93),
rond-map.js 渲染图例;实测 `modes:[步行/其他 370, 电🐔 317, 地铁 22, 高铁 21, 公交 17, 火车 9]`
- F2 城市级聚合 + 首次到达里程碑:
- 地图 `/api/cities` 按城市聚合到访/时长/首次(last)日期,圈大小随到访量;已剔除家/工作
- 报告页「🏅 首次到达的城市」里程碑:firstArrivalCities 查「有史以来首次到访落在本年」的城市
(HAVING min(arrival) in [from,to)),按日期升序;区域白名单对访客同样生效
- **实测发现并修复的 bug**:apiCityAgg 原 SQL 写 `AND NOT p.is_home AND NOT p.is_work`,
但 is_home/is_work 是 visits 列、places 表没有 → `SQLSTATE 42703` 直接 500。
改为 `NOT EXISTS (SELECT 1 FROM visits vh WHERE vh.place_id=p.id AND (vh.is_home OR vh.is_work))`
- 报告页已验证 2025 年出现里程碑、/api/cities 返回 9 城带 first/last 日期
- F3 年度报告长图/PDF 分享:ReportData 加 Cities []CityMilestone;报告页加「复制链接」(`navigator.share`
优先、否则 clipboard) + 「保存为 PDF」(`window.print()`),app.css 加 `@media print` 隐藏导航/页脚/按钮、
卡片 break-inside:avoid。注:长图用浏览器打印生成 PDF(未接 html2canvas),README 已注明该取舍
- F4 从 GPX 补充轨迹:internal/gpx 解析 trk/trkseg/rte(WGS-84);adminGpxUpload 存 gpx_tracks,
apiGpx 按 crs=gcj02 把 WGS-84 转 GCJ-02 对齐高德底图;地图橙色叠加
- **修复**:gpx.Parse 原来 `if len(out)>0 return` 在已有 trk 时直接返回、丢掉 rte;改为 trk 与 rte 都追加
- 实测:登录 admin/rond-admin → 上传含 trk+rte 的测试 gpx → /api/gpx 返回两条轨迹(测试数据已删除)
- 验证手段:`go build` + `go vet` 全过;主站后台起、curl 实测各 API 与报告页;测试轨迹上传后清理
- README 已把 F1–F4 从「后续可以做的」挪到已实现章节,并补 GPX/城市聚合/报告分享说明