构建与发布 / 单测与集成测试 (push) Waiting to run
构建与发布 / 打包 ${{ matrix.platform }} ${{ matrix.arch }} (arm64, linux, ubuntu-24.04-arm) (push) Blocked by required conditions
构建与发布 / 打包 ${{ matrix.platform }} ${{ matrix.arch }} (arm64, macos, macos-15) (push) Blocked by required conditions
构建与发布 / 打包 ${{ matrix.platform }} ${{ matrix.arch }} (x64, linux, ubuntu-24.04) (push) Blocked by required conditions
构建与发布 / 打包 ${{ matrix.platform }} ${{ matrix.arch }} (x64, windows, windows-2025) (push) Blocked by required conditions
构建与发布 / 发布 GitHub Release (push) Blocked by required conditions
200 lines
21 KiB
Markdown
200 lines
21 KiB
Markdown
# AGENTS
|
||
|
||
面向在本仓库工作的 AI 代理。约定之外的部分按代码里的既有写法照做。
|
||
|
||
## 项目性质
|
||
|
||
Electron 桌面应用:多源文献检索 + 本地书库 + 内置阅读器(PDF/EPUB/MOBI/AZW)+ 批注笔记 + AI 助手。
|
||
Windows x64 免安装版,用户数据在程序同级 `data/`。
|
||
|
||
## 语言与风格
|
||
|
||
- 界面文案、提交信息、注释一律中文;标识符英文。
|
||
- 默认不写注释。只在原因不显然时写:隐藏约束、易踩的坑、绕过某个具体 bug 的原因。不要复述代码在做什么。
|
||
- 不用 emoji。散文里避免破折号。
|
||
- 回复简洁,不主动扩展用户没要求的事。
|
||
|
||
## 硬性约束
|
||
|
||
### 安全边界不可绕过
|
||
|
||
- 渲染层永远不能传任意路径读盘。所有文件访问必须过 `main.js` 的 `resolveReadable()`,它只放行书库中真实登记的条目。
|
||
- 分段读取会话(`src/reader/range-sessions.js`)与 sender 绑定,句柄不透明,webContents 销毁即回收。新增 IPC 时必须校验 sender。
|
||
- 所有 `ipcMain.handle` 走 `wrap()`,同步抛出也要变成 `{ ok:false }`,否则渲染层的 `await` 无 catch,界面会永远卡在加载中。
|
||
- 不要把密钥、token 写进日志或错误信息。AI Key 经 `safeStorage` 加密后落盘。
|
||
|
||
### 依赖
|
||
|
||
- 前端第三方库全部 vendored 在 `src/ui/vendor/`,版本在 `package.json` 中锁死(不用 `^`)。不要引入新的运行时依赖,除非用户明确要求。
|
||
- `src/ui/vendor/pdf.worker.range.mjs` 是**手工 patch 过的** PDF.js worker,不要用上游文件覆盖。见下文。
|
||
|
||
## PDF 分段读取(易踩坑)
|
||
|
||
超大 PDF 不整文件读入内存,走 `reader:rangeOpen/rangeRead/rangeClose`。`reader:bytes` 直接拒绝 PDF。
|
||
|
||
worker 选择策略在 `pdf-adapter.mjs`:
|
||
|
||
- ≤256 MB:用**官方** worker(`pdf.worker.min.mjs`),即使是 range 模式。
|
||
- >256 MB:用稀疏 worker(`pdf.worker.range.mjs`)。
|
||
|
||
稀疏 worker 的基础缓冲区是空的(`new Uint8Array(0)`),因此**任何对 `stream.bytes.buffer` 直接建视图的代码都会抛 RangeError**。已知踩过的坑:`preEvaluateFont` 里的 `ToUnicode` 哈希,错误表现是字体静默降级成不可见的 `ErrorFont`(文本项数量正常,但画布上没有墨迹)。必须用 `stream.getByteRange(start, end)`。
|
||
|
||
改动这个 worker 后,用真实 PDF 对比三条路径的渲染结果(文本项数 + 非白像素数):官方 worker 全量数据 / 官方 worker range / 稀疏 worker range。三者应一致。
|
||
|
||
## PDF 渲染画质
|
||
|
||
- 底栏「画质」档位(1/2/3)经 `renderOpts` 下发为 `opts.renderQuality`,持久化在 `reader.pdfRenderQuality`。
|
||
- 生效倍率是 `max(devicePixelRatio, renderQuality)`,**HiDPI 屏上不叠乘**。所以断言时不能写「backing 翻倍」,只能断言 `backing / CSS 宽度 == 期望倍率`;开发机 dpr 常见 1.75,写死 2 会假失败。
|
||
- 画布尺寸必须过 `clampCanvasSize()`:Chromium 单边上限 16384px、面积上限 268435456px。超限时浏览器**静默给出不可用画布**,整页空白且不报错。大幅面图纸(2000pt 以上)在 scale 5 下必然踩中。
|
||
- 钳制后 backing 与 CSS 盒子的比例不再等于名义倍率,`pdfPage.render` 的 `transform` 必须用 `fit.scaleX` / `fit.scaleY`(两个方向分别算,`Math.floor` 的损失不同),否则画面错位或只画出一角。
|
||
- 倍率变化必须走 `epoch++` 整篇重建,只改新渲染的页会让同屏出现清晰度不一致。
|
||
- 集成测试里量画质前要先把视口滚回目标页:离屏页会被回收成空白占位,量到的是占位画布。画布尺寸在重建时立即变大,墨迹要等重绘完才落上去,断言墨迹必须轮询而不是只等比例。
|
||
|
||
## TXT 与 Markdown
|
||
|
||
- `text-adapter.mjs` 把 txt/md 转成内存 EPUB 再交给 `epub-adapter` 渲染,定位器 `kind` 为 `txt` / `md`,结构与 epub 同源(`chapter` + `offset`)。新增重排格式时记得同步 `REFLOW_KINDS`。
|
||
- 格式白名单有六处,改一处不够:`main.js` 的 `READABLE_EXT`、`library/store.js` 与 `library/local-import.js` 的 `BOOK_EXT`、`main.js` 里选文件对话框的 `extensions`,以及**渲染层的两份** `READABLE_RE`(`ui/views/library.js`、`ui/reader/shell.mjs`)。单测有正则锁死并逐项比对渲染层与 `READABLE_EXT` 是否一致。
|
||
- 渲染层那两份漏掉格式**不会报错**:IPC 照样能打开,但卡片上的「阅读」按钮和封面点击入口静默消失,阅读器的「从书库打开」列表里也少掉这些书。用户看到的现象是"内置阅读器打不开 txt"。集成测试里直接调 `reader.open` 是查不出来的,必须从书库界面点「阅读」。
|
||
- 书库卡片的「阅读」取的是**第一个可阅读文件**。同一条书目既有 txt 又有 pdf 时,加进白名单后打开的文件会从 pdf 变成 txt,断言里不要写死 PDF 画布。
|
||
- Markdown 走 markdown-it(`html: false`)+ DOMPurify 双保险。裸 HTML 会被转义成文本,所以**净化断言必须按 DOM 查**(`querySelectorAll('script')`、`on*` 属性、`a[href]` 协议),用 `innerHTML` 匹配 `onerror` 会把转义后的字面量误判成漏网。
|
||
- 编码探测支持 UTF-8 / UTF-16LE / UTF-16BE(含 BOM)与 GB18030。带 BOM 的 UTF-16LE 是记事本另存的默认之一,探测错会整本乱码且用户无从修正。
|
||
|
||
## AI 助手
|
||
|
||
- 上下文范围:`selection | page | document | page-image | region-image`。
|
||
- 只有 `selection` 需要选中文本;`page` 和 `document` 不依赖选区。
|
||
- `document` 无论多短都强制弹确认框,并提示可能超出模型上下文限制。
|
||
- 正文**完整发送,不做本地截断**。模型窗口够不够由接口自己判断,超限时把报错转成中文提示(`isContextOverflow` / `overflowHint`)。曾经按 `MAX_CHARS = 12000` 挖空中间,实测 40 页文档只发出 8 页,而界面仍显示全文字数,属于静默数据丢失,已移除。`clipContext` 保留给有明确预算上限的场合显式调用。
|
||
- 界面展示的字数必须等于真正外发的字数。任何"先按上限裁剪再发送"的改动都要同步改 `aiCost` 与确认框文案,否则用户会把答非所问归因于模型。
|
||
- 图像只用 JPEG 单一编码路径,1600px / 目标 400 KB。**不要加格式回退**(PNG 等),维护成本高于收益;视觉模型按像素图块计费,格式不影响费用。
|
||
- 三种协议:Anthropic `/messages`、OpenAI Responses `/responses`、兼容 Chat `/chat/completions`。图像负载格式各不相同,改动时三条都要验。
|
||
|
||
## AI 多轮会话
|
||
|
||
- 会话存在 `reader-ai-sessions/`,一个会话一个 JSON,外加可重建的 `index.json`。不进 `store.js`,避免整本阅读进度被聊天记录带着反复重写。
|
||
- `ai:run` 里顺序是硬约束:**先 `historyFor()` 再 `appendUser()`**。顺序反了,当前这轮提问会出现在自己的历史里,模型收到两遍同样的问题。
|
||
- 同一会话禁止并发生成(`ai:run` 里按 `sessionId` 查 `aiRuns`)。两轮同时写一个文件,后完成的那轮会把前一轮的消息覆盖掉。
|
||
- 消息正文存的是**用户看见的那句提问**(`aiTurnTitle`),整篇正文只在 `contextRef` 里留 scope / 字数 / 哈希。把正文当消息存会让重开后的气泡变成十几万字原文,还会被 `LIMITS.question` 截成一段无意义的残句。
|
||
- 失败和取消都要落盘,并且要把**已经流出来的残片**一起存(`streamed`)。只写空串的话,界面上明明显示着半截回答,一重开就消失。
|
||
- 历史只发文本:`stream()` 只取 `role` 与 `text`,图像一律不重发。历史里内联 base64 会让每轮费用随轮数线性上涨。
|
||
- 落盘失败不能把已经拿到的回答变成请求失败,`settleAiAssistant` 吞掉异常。
|
||
- 书籍删除、孤立对账都要带上会话(`aiSessions.forgetMany` / `orphanReport`),删完调 `collectAiImages()`。内容寻址的图片没有引用者就永远不会被回收。
|
||
- 集成测试里断言历史时**不能按固定下标取消息**:本轮提问固定在末尾,中间是历史。原来写死 `messages[1]` 的断言在多轮上线后会取到上一轮,表现为"图像尺寸无效"这种完全无关的报错。
|
||
|
||
## 笔记独立窗口
|
||
|
||
- `src/reader/note-window.js` 是**单窗口多标签**:一个笔记窗口,一条笔记一个标签,已开则切到该标签。同一条笔记两处编辑时 `reader:updateNote` 是整条覆盖、无版本校验,后保存者会把画布内容整块吃掉,所以「一条笔记只能有一个编辑器」是数据安全约束,不是体验优化。`openTab` 必须先 `tabOf()` 查重。
|
||
- 阅读器内的笔记模态**保留**,因此「模态 + 独立窗口」仍可能撞车。靠列表按钮避开:已开窗时「编辑」变成「在窗口中编辑」并转为聚焦窗口,不再开模态。
|
||
- 笔记消失时必须**关掉对应标签**(不是销毁整个窗口):`reader:removeNote` 调 `closeFor`,`library:removeMany` 与 `reader:purgeOrphans` 调 `closeForEntries`。留着标签,它下一次保存会把已删的笔记整条写回去。最后一个标签关掉后窗口才自行退场。
|
||
- 三处窗口广播(`notifyNotesChanged`、`applyWindowIcons`、`notifyUiThemeChanged`)都要带 `noteWindow.all()`,漏一处笔记窗口就收不到笔记变更或主题切换。单测有正则锁死。
|
||
- `notes:getOne` 要校验 `noteWindow.ownsNote(event.sender, noteId)`:笔记窗口只能读**自己已开标签**的那几条,否则这个通道就是遍历全部笔记的后门。开窗目标也必须过 `findNote()` 用 `listNotes()` 重新对账。
|
||
- `notes:tabsChanged`(`setTabs`)只能**收窄**标签集,即只允许 `openNotes.delete`,绝不能 `set`。允许渲染层往里加 ID 等于让它自己扩权:谎报持有某条笔记后 `notes:getOne` 立刻放行,授权集合就形同虚设。新增标签只能走 `open()`,那条路径过 `findNote()` 对账。
|
||
- 关窗拦截三件套必须配套:`close` 里 `preventDefault()` + `closePending` + 看门狗,渲染层处理完调 `notes:shutdownReady`,**用户取消时必须调 `notes:cancelClose`**。少了取消回报,`closePending` 一直为真会让之后每次点关闭都被当成「正在处理」静默忽略,而看门狗仍会在十秒后把带未保存内容的窗口直接销毁。
|
||
- 笔记没有自动保存。切换标签只留在内存,LRU 回收前要把未保存内容序列化进 `pendingContent`,否则回收即丢改动。
|
||
- 脏判定必须比对**序列化后的内容**(`contentKey` 与挂载后取的 `baselineKey`),不能用 `pointerdown`/`keydown` 之类的交互事件:只点选不改字也会被判脏,每个标签关闭时都弹一次无谓的确认。基线要在编辑器 `ready()` 之后取(画布有 version 1→2 归一化,拿磁盘原值当基线会让刚打开就显示已修改),保存成功后基线要跟着前移。
|
||
- 笔记窗口**不**纳入 `isReaderSender`,不获得 AI 会话等阅读器权限。
|
||
- PDF 底版草稿按 sender 隔离(`resolveDrafts` / `readDraft`),草稿不能跨窗口交接,笔记窗口必须自己 stage。
|
||
- 编辑区吃满整窗要逐层 `min-height: 0`,缺一层 flex 子项就被内容顶高、画布溢出窗口。
|
||
- 笔记本下拉框要 `max-width` + `min-width: 0`。只给 `max-width` 不够:`select` 的 min-content 以最长选项为准,长书名/长笔记本名照样把整行顶宽(实测取消限宽后从 260px 涨到 553px)。
|
||
- 标题栏只放品牌名「笔记」,不要副标题。`note.html` 用的是 `style.css`,那里的 `.titlebar-left` **不是** flex(只有 `reader.css` 才是),把 `brand-sub` 放成 `.brand` 的同级会掉到下一行,把左侧块顶成 46px 而标题栏只有 44px。要加副标题只能像 `index.html` 那样塞进 `.brand` 内部。
|
||
- `note.html` 不加载 `reader.css`,所以标签条样式必须在 `note-window.css` 里**自带一份**,也不能用只在 `reader.css` 里定义的变量(如 `--hover-bg-soft`),否则静默失效。
|
||
- 集成测试四个坑:窗口刚建好时 `getURL()` 还是空串,找窗口必须轮询;断言窗口数量只能数**笔记窗口**,用总窗口数当基线会被阅读器窗口的开关搅乱;笔记页同时有多张卡片,找按钮必须限定在 `.note-card[data-note-id=...]` 内,全局找「编辑」会命中别的卡片;多标签后表单是每标签一份,查询要限定在当前激活的 `.note-tab-view` 内,或直接按 `[data-note-id]` 定位,否则量到的是别的标签。
|
||
- 删除笔记要走真实 IPC(`reader:removeNote`),它内部已经调了 `closeFor`。测试里再手工补一次 `closeFor` 等于在验证自己造的假路径,还会因为重复处理而看到「标签没关掉」的假失败。
|
||
|
||
## 书库卡片封面
|
||
|
||
- 封面比例来源不一(内置生成 400x500,书源常见 0.65~0.75,还有方图和横图)。卡片盒子固定 `aspect-ratio: 3/4`,用 `background-size: cover` 会按各自比例裁掉不同的边,观感就是「预览大小不一致」,但**量盒子是量不出问题的**(每张都一样宽高),必须看截图或比对可见的封面内容。
|
||
- 现在用 `contain` 完整显示封面,留白由 `::before` 里同一张图放大模糊后垫底。注意 **`::before` 会盖在父元素自己的背景之上**,所以清晰的那层必须单独画在 `::after` 里,只调 `z-index` 是压不住父元素背景的;照这个顺序:`::before`(模糊,z-index 0)→ `::after`(清晰,z-index 1)→ `.card-cover > *`(角标文字,z-index 2)。两个伪元素都要 `pointer-events: none`,否则挡掉封面的阅读点击。
|
||
- 多选复选框不要加 `padding` + 背景色块。13px 的原生复选框套一圈衬底后看起来像加粗了边框,浅色封面上的可见性用 `filter: drop-shadow(...)` 解决。
|
||
|
||
## 笔记表单的选择器边界
|
||
|
||
`.note-edit-form` 里嵌着画布工具栏(`.canvas-note-root`)与富文本工具栏(`.ql-toolbar`),两者都有 `<select>`。因此 `.note-edit-form select { ... }` 这类**后代选择器会一路灌进工具栏**,必须显式 `:not(.canvas-note-root select):not(.ql-toolbar select)`。
|
||
|
||
踩过的坑:表单控件的 `margin-top: 5px` 落到工具栏的粗细/纸张下拉上,分组高度变成 28 与 33 两种,工具栏从一行变两行(42px → 78px)。表现像是「工具栏没横排、需要 flex-wrap 调整」,改 `.canvas-note-tool-group` 的 wrap/shrink 只能把 78px 压到 46px,剩下的 4px 差和分组错位依然在,因为根因不在布局属性而在这条越界的样式。
|
||
|
||
诊断方法:量到子项全是 28px 而父分组是 33px 时,不要继续猜 flex 属性,直接遍历 `document.styleSheets` 找出 `el.matches(rule.selectorText)` 的全部规则,越界的那条会立刻现形。
|
||
|
||
## 测试
|
||
|
||
```bash
|
||
npm test # Node 单测
|
||
npx electron src/_test/electron/<name>.integration.js # Electron 集成
|
||
```
|
||
|
||
集成套件:`ai-scope`、`reader-features`、`library-notes`、`annotation`、`download`、`cover`、`startup`。
|
||
|
||
要求:
|
||
|
||
- 完成任务前跑单测 + 相关集成套件。
|
||
- 单测里有对源码的正则断言(锁死关键约定)。改了被断言的代码要同步更新断言,不要为了让测试过而弱化断言。
|
||
- 集成测试断言"真正离开进程的内容"(真实本地 HTTP 服务收到的 body、真实渲染出的像素),不要 stub 渲染层。
|
||
- 不要为了测试往生产代码里加 `window.__test` 之类的全局钩子,通过真实 UI 断言。
|
||
- `library-notes` 偶发失败,重跑确认再判断。
|
||
|
||
## 构建
|
||
|
||
```bash
|
||
npm run build # Windows,输出 dist/PeopleLib-windows-x64/
|
||
npm run build:mac # macOS arm64,输出 .app 与 .dmg,只能在 macOS 上跑
|
||
npm run build:linux -- --arch x64 # Linux,输出 tar.gz,任意平台可构建
|
||
npm run release -- --platform linux --arch x64 # 发布件 + 校验和
|
||
```
|
||
|
||
Windows:
|
||
|
||
- 输出目录固定,不带版本号。改名会导致 `data/` 被遗留在旧目录。
|
||
- 构建保留 `data/`,但会清空其余内容。**构建前必须退出该目录下运行中的 `PeopleLib.exe`**,否则清理到一半失败,目录处于不完整状态。
|
||
- 涉及 `data/` 的操作前后做逐文件哈希比对,确认书库、笔记、批注未被改动。
|
||
|
||
macOS(`build-mac.js`,易踩坑):
|
||
|
||
- **不能交叉构建**。DMG 需要 `hdiutil`,且 Apple Silicon 内核直接拒绝执行未签名二进制;改名与改 plist 会让 Electron 原始签名失效,必须用 macOS 的 `codesign` 重签(ad-hoc `--sign -` 即可)。
|
||
- 解压官方 zip 必须用 `ditto`。`Electron Framework.framework` 内有符号链接,用 Node 或 `unzip` 解压会展开成副本,签名随即失效。
|
||
- 重打包后 `Info.plist` 里的 `ElectronAsarIntegrity` 必须删除,否则启动即报完整性错误。
|
||
- helper 的 plist **没有** `CFBundleExecutable`,靠 bundle 名推断可执行文件名。重命名 helper 后必须显式补上该字段,否则渲染进程起不来,界面一片空白。
|
||
- 签名严格由内向外:嵌套可执行文件 → helper → framework → 外层 `.app`。带版本的 framework 签 `Versions/A`;`Squirrel.framework` 的 `Resources/ShipIt` 是独立可执行文件,要单独签。
|
||
- 数据目录走 `~/Library/Application Support/PeopleLib`,**不要**沿用 Windows 的便携布局:`.app` 在 DMG 里只读,且升级覆盖会删掉用户书库。
|
||
- `.icns` 由 `npm run icons:icns` 生成,纯 Node 实现(icns 自 10.7 起内嵌 PNG),不依赖 macOS 的 `iconutil`。窗口图标在非 Windows 平台用 PNG,`.ico` 只有 Windows 认。
|
||
|
||
Linux(`build-linux.js`):
|
||
|
||
- 直接从官方 zip 的条目转写进 tar,**不落地中间目录**。可执行位存在 zip 的 external attributes 里,先解到 NTFS 再打包会全部丢掉,产物解压后主程序和 `chrome-sandbox` 都不可执行。因此这个脚本在 Windows 上也能构建。
|
||
- tar 头是手写的。路径超过 100 字节要走 PAX 扩展头:ustar 的 `prefix` 只能在斜杠处切分,`undici` 与 vendor 里的深层路径切不出合法组合。
|
||
|
||
发布件(`build-release.js`):
|
||
|
||
- 发布件的唯一出口,不要手工压缩构建目录上传。Windows 便携版的 `data/` 就在程序同级,手工压缩会把用户书库连同笔记打进公开发布件;脚本按前缀排除并在打包后回读压缩包确认。
|
||
- 打完包一定回读产物再签校验和:Linux 要确认可执行位还在,Windows 要确认没有 `data/` 与 `_test`。只算哈希不看内容,等于把「构建脚本改坏了」这类问题一路放到用户手上。
|
||
|
||
## 持续集成
|
||
|
||
- `.github/workflows/build.yml`:`validate`(单测 + 全部集成套件,`xvfb-run` 起 X server)→ `package`(四目标矩阵)→ `release`(仅 `v*` 标签)。
|
||
- 仓库 `.npmrc` 指向 npmmirror,GitHub runner 在境外拉不动,工作流用 `npm_config_registry` 与 `ELECTRON_MIRROR` 覆盖回官方源。新增构建步骤时别把这两个环境变量漏掉。
|
||
- 新增集成套件后要同步加进工作流的套件列表,单测里有断言按 `src/_test/electron/` 的实际文件逐个核对,漏加会直接失败。
|
||
- 标签名必须等于 `v` + `package.json` 的 `version`,且标签要指向被构建的那个提交。
|
||
- 下载完整日志要仓库管理员权限,所以失败详情都转成 `::error` 注解。改这几步时别把注解去掉,否则没有管理员权限的人只能看到「exit code 1」。
|
||
- runner 上 `chrome-sandbox` 拿不到 root:root 4755,集成测试必须带 `--no-sandbox`。这个开关只属于 CI,不要带进构建产物。
|
||
- runner 自带字体极少,工作流装了 Liberation 与 Noto CJK。界面文案是中文,缺 CJK 字体会渲染成豆腐块,版式测量也跟着偏。
|
||
|
||
Linux 上首次跑测试暴露过三类只在该平台成立的问题,新写代码时留意:
|
||
|
||
- `mtimeMs` 在 ext4/APFS 带亚毫秒小数,`Date.now()` 只到整毫秒。直接相减,刚落盘的文件年龄是负数,「超过宽限期就回收」的逻辑永远不触发。比较前先 `Math.floor`。
|
||
- 集成夹具要走网络时,代理只能读 `HTTPS_PROXY`,不能写死本机端口,否则 CI 上直接 ECONNREFUSED。
|
||
- 版式类断言的容差不要写死字符数,按实际渲染出的每行字数算。字体集不同,同一行的字数就不同,写死的数字换个平台就假失败。
|
||
|
||
## 仓库
|
||
|
||
两个远端,用途不同:
|
||
|
||
- `origin`(私有):完整源码,日常开发推这里。
|
||
- `github`(公开):源码与 CI。GitHub Actions 必须 checkout 到源码才能构建,所以公开仓不再只放 README。历史与本地无共同祖先,推送前先核对两边的差异范围。
|
||
|
||
推公开仓前必须确认:没有本地配置、账号凭据、`data/` 内容或诊断产物混进去。公开仓一旦推出去,删提交也留在别人的克隆里。
|
||
|
||
其他:
|
||
|
||
- 根目录 `dist/` 已 gitignore,规则写作 `/dist/`,**必须保留前导斜杠**:不加会连 `icons/dist/` 一起忽略,新克隆的仓库缺图标,构建直接失败。诊断产物(`*-diagnostic.png`、`probe*.json`)也已忽略,不要提交。
|
||
- 未跟踪文件视为用户资产,不要删除或覆盖。清理前先看 `git status --porcelain`。
|
||
- 提交前 `git diff --cached` 检查是否混入密钥。
|