diff --git a/AGENTS.md b/AGENTS.md index a29b349..dbb3702 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,8 +22,8 @@ ResearchThread 是一个**本地优先**的桌面工具(Tauri 2,macOS + Wind ## 当前状态 -- 更新日期:2026-09-11 -- 阶段:**M3 完成 + 种子发布加固**(M1/M2/M3 全部落地,实机验证)——发布前审阅确认的九项门槛已全部代码落地: +- 更新日期:2026-09-12 +- 阶段:**M3 完成 + 种子发布加固 + macOS 实机验证通过**(M1/M2/M3 全部落地,实机验证)——发布前审阅确认的九项门槛已全部代码落地: - **事务式写入**(`storage/atomicWrite.ts`):写 `.rt-tmp` → 原文件让位 `.rt-bak` → 新内容就位 → 清备份;任何一步失败或进程中断后,**原文件或备份必有一个可恢复**;启动时 `recoverInterruptedWrites()` 扫描恢复并记账。测试注入 rename 失败/持续故障/崩溃残留全覆盖 - **单实例**(`tauri-plugin-single-instance`):第二实例启动即聚焦已有主窗口后退出,杜绝双 watcher + 同名 tmp/bak 互踩 - **外部编辑并发策略**:storage 保存前比对磁盘内容与应用最后已知内容(`seen` 缓存,读/写后更新),不一致抛 `ExternalConflictError`,UI 弹「覆盖保存(acknowledge 后重试)/ 重新加载 / 取消」。`updateTaskMeta` / `appendEntry` / `updateEntry` / `updateProjectDescription` / `saveWeekly` **五路全部受保护**(appendEntry 同样检查冲突后整文件重写,比纯追加更安全) @@ -35,8 +35,10 @@ ResearchThread 是一个**本地优先**的桌面工具(Tauri 2,macOS + Wind - **周报正确性**:任务定位主键改完整相对路径(修跨项目同名任务归错),活跃工作线 ☆ 置顶稳定排序 - **watcher 路径归一化**:Windows 反斜杠事件先归一再剥数据目录前缀(此前 `.git` / `.activity` 过滤失配、活动账记绝对路径);`.rt-bak` 一并过滤 - **发布物料**:关于页读真实版本(`getVersion()`)标「种子测试版」;[docs/seed-manual.md](docs/seed-manual.md) 种子手册——隐私边界(**明确撤回「打包数据目录反馈」**,改为脱敏反馈模板)、SmartScreen 安装步骤、卸载数据保留说明、外部编辑并发规则 - - **发布包与安装提示**:`npm run make-release`([scripts/make-release.mjs](scripts/make-release.mjs))把 NSIS 安装包、SHA256SUMS、README、种子手册归拢到 `release/`(gitignore,脚本可重复生成);安装器中文化(`bundle.windows.nsis.languages` 简中+英文+语言选择器)并以 `bundle.license`(`src-tauri/INSTALL-NOTES.txt`)在安装前展示中文安装须知;应用首启弹欢迎卡(数据位置/快照警告含义/脱敏反馈,`settings.introSeen` 持久化开关) -- 下一步:干净 Windows 账户实测(首装 / 无 git / 无 CLI / 双开 / 升级 / 卸载重装 / 快照恢复演练)→ 全部过门后打 release tag 发 **Windows x64 受控种子版**;随后回到 **第二步阶梯 2a**(AgentRunner CLI 后端,见下条与 PART2.md) + - **发布包与安装提示**:`npm run make-release`([scripts/make-release.mjs](scripts/make-release.mjs))平台感知(win 收 NSIS exe / darwin 收 DMG),归拢安装包、SHA256SUMS、README、种子手册到 `release/`(gitignore,脚本可重复生成);安装器中文化(`bundle.windows.nsis.languages` 简中+英文+语言选择器)并以 `bundle.license`(`src-tauri/INSTALL-NOTES.txt`)在安装前展示中文安装须知;应用首启弹欢迎卡(数据位置/快照警告含义/脱敏反馈,`settings.introSeen` 持久化开关) +- **macOS 适配(v0.1.0,PR feat/v0.1-macos-support)**:Tauri 配置拆三文件(`tauri.conf.json` 公共 / `tauri.windows.conf.json` NSIS+WebView2Loader / `tauri.macos.conf.json` app+dmg+minimumSystemVersion 12.0,构建时自动合并);文件管理器改 `plugin-opener`(`openPath`,替代硬编码 explorer);**Finder/Dock 启动 PATH 修复**(`fix-path-env` crate,lib.rs setup 里 `fix()`,mac 验收 = Finder 双击启动后终端 git/claude/codex 可用);shell capability 拆平台(`shell-windows.json` / `shell-macos.json`——mac 只留 git/claude/codex/npm/node,explorer/cmd/powershell 不进 mac 白名单);universal 构建命令与 mac 测试矩阵见 docs/release.md §0/§3/§4-6。**签名两阶段**:内测不签名(**macOS 15+ 无「右键→打开」通道**,被拦后走「系统设置→隐私与安全性→仍要打开」,实机 2026-09-12 确认;macOS 12–14 才是右键→打开),面向陌生用户前必须 Developer ID + 公证 +- **macOS 实机验证(2026-09-12,issue #4 已回报)**:universal DMG(lipo x86_64+arm64)、Finder 启动 PATH 三件套(git/claude/codex 全通)、数据持久、快照、.trash、冲突链路、单实例、⌘⇧Space、DMG 安装/重装/卸载保留全过;**修复 P1**:tauri-plugin-fs 点文件开关只认 `plugins.fs` 插件配置(capabilities 同名字段被忽略),Unix 默认拒绝点路径导致 macOS 数据层初始化必失败(`3e1ab0b`);**确认 macOS 15+ 无「右键打开」Gatekeeper 通道**(真实下载实测),指引按系统版本分流(`51c45a8`)。待办 issue:DMG 安装须知文件、首启 quarantine 检测提示卡、Developer ID 签名+公证 +- 下一步:Windows 侧拉取分支回归(fs 修复对 Windows no-op,跑 vitest + NSIS 重归拢)→ 审阅合并 PR #3 → 干净 Windows 账户实测(首装 / 无 git / 无 CLI / 双开 / 升级 / 卸载重装 / 快照恢复演练)→ 全部过门后打 release tag 发 **Windows x64 受控种子版 + macOS universal DMG**;随后回到 **第二步阶梯 2a**(AgentRunner CLI 后端,见下条与 PART2.md) - 第二步技术报告见 [PART2.md](PART2.md):CLI 后端选型(claude 首选/codex 坑清单)、MCP 工具面与 will_write 归属协议、2c 解析器选型、风险登记册与排期——**实施 2a–2d 前必读** - **Windows 构建环境(本机)**:Rust stable-msvc 装了但**缺 Windows SDK**(提权安装 SDK 未获批准);当前用 `stable-x86_64-pc-windows-gnu` + `rust-lld` + Strawberry Perl 的 dlltool 编译通过。**启动命令**: ```bash @@ -46,11 +48,11 @@ ResearchThread 是一个**本地优先**的桌面工具(Tauri 2,macOS + Wind ``` `src-tauri/.cargo/config.toml` 固定了 rust-lld 链接器;`Cargo.toml` 的 lib crate-type 只保留 `["lib"]`(cdylib 会撞 65535 导出上限)。改 `capabilities/*.json` 后需 `touch build.rs && cargo build` 强制重嵌(dev watch 不监听 capabilities)。 - **打包安装器**:`npm run tauri build`(同上环境)→ NSIS 安装包在 `src-tauri/target/release/bundle/nsis/`,per-user 安装免管理员;随后 `npm run make-release` 归拢发布包到 `release/`(安装包 + SHA256SUMS + README + 种子手册)。**坑**:GNU 构建动态链接 `WebView2Loader.dll`(dev 时由构建目录提供,安装后缺失会导致启动即退 EXIT=127)——已通过 `bundle.resources` 打进安装器(`src-tauri/WebView2Loader.dll`,勿删);升级 webview2-com-sys 后需同步更新该 dll。安装器未签名,首次安装会过 SmartScreen 警告(安装须知页已说明「更多信息→仍要运行」)。 -- 终端面板 shell 白名单:`capabilities/default.json` 中 `shell:allow-spawn`/`shell:allow-execute` 的 allow 条目(name/cmd/args:true),新增可执行程序需同步加两处 +- 终端面板 shell 白名单:按平台拆分在 `capabilities/shell-windows.json` / `capabilities/shell-macos.json` 的 `shell:allow-spawn`/`shell:allow-execute` allow 条目(name/cmd/args:true),新增可执行程序需同步加两处(其余公共能力在 `capabilities/common.json`) ## 关键决策(改动前必须与用户确认) -1. **存储 = Markdown 文件树 + YAML frontmatter,不用数据库**。理由:第二步 agent 经 MCP 工具读写(零迁移);Obsidian 可打开同一目录;git 提供版本历史。数据目录(默认 `~/ResearchThread/`,可在设置中修改)与代码仓库分离。**文件树是项目地图的唯一事实源**,一切地图视图都是 `get_project_map()` 的派生渲染,绝不持久化为第二事实源。 +1. **存储 = Markdown 文件树 + YAML frontmatter,不用数据库**。理由:第二步 agent 经 MCP 工具读写(零迁移);Obsidian 可打开同一目录;git 提供版本历史。数据目录固定 `~/ResearchThread/`(当前不提供设置项修改)与代码仓库分离。**文件树是项目地图的唯一事实源**,一切地图视图都是 `get_project_map()` 的派生渲染,绝不持久化为第二事实源。 2. **storage 拆分为纯核心 + IO 适配器**(`core.ts` 无 Tauri API,`fsAdapter.ts` 注入 `tauri-plugin-fs`)。目的:2b 阶段同一核心配 `node:fs` 即可跑成 MCP server。**storage 写回契约**(宽松读 / 规范写 / 字段级合并保留未知字段 / 原子写 / 容忍 CRLF)见 PLAN.md 第三节,是数据完整性的关键,**违反契约的改动必须有测试覆盖**。 3. **起步零自定义 Rust**:文件 IO 走官方 `tauri-plugin-fs` 的 JS API。极简是第一阶段的起步策略而非长期约束——重大引入(如真 PTY)前与用户确认即可;**PTY 已降级为远期可选**(对话面板出现后终端退化为工具抽屉)。 4. **前端 = React 19 + Vite + Tailwind v4(2026-09-10 定)**:用户采纳外部按 UI.md 交付的 UI,并明确「可以扩大规模,只要匹配需求」。集成时剥离其 SSR(TanStack Start/Nitro)、登录(better-auth)、数据库(pglite/pg/kysely)——本地优先单机桌面应用不需要这些。**`DataAccess` 接口(UI.md 第 2 节)是 UI 与 storage 的唯一边界**,UI 组件不得绕过它触文件或自建数据。原「M1 末评估框架去留」的决策点提前完成。交付中超出 M1 范围的视图(搜索/今日/周报/图谱/agent 对话)按插件门控保留、默认关闭,不投入打磨(图谱与反目标冲突,保持禁用)。 diff --git a/README.md b/README.md index 72d9027..ab579e0 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # ResearchThread -本地优先的科研/工作流程管理桌面应用(Tauri 2)。当前种子构建:**Windows x64**;macOS 为计划目标、尚未构建验证。把手写「思路整理」工作流数字化:**空间 → 项目 → 任务**三级层级 + 双层上下文(项目整体上下文 / 任务上下文)+ 周文档。 +本地优先的科研/工作流程管理桌面应用(Tauri 2)。发布目标:**Windows x64 + macOS 12+(universal:Apple Silicon + Intel,DMG)**——macOS 适配代码已入库,DMG 需在 macOS 机构建(流程见 [docs/release.md](docs/release.md) §3)。把手写「思路整理」工作流数字化:**空间 → 项目 → 任务**三级层级 + 双层上下文(项目整体上下文 / 任务上下文)+ 周文档。 第二步(已规划、未开始):**人只说自然语言,agent 维护整个项目地图**——口述新任务/新项目/任务转折即可,agent 经 MCP 工具接口执行并定时产出日报/周报。 ## 状态 -**M3 完成 + 种子发布加固**(2026-09-11,M1–M3 全部落地并实机验证):数据落盘(Markdown 文件树)、活动日志、文件监听、收件箱、今日视角、终端面板、数据目录初始化、周文档一键生成、全文搜索、项目归档、markdown 预览之上,按发布前审阅补齐数据安全九项:事务式可恢复写入、单实例、外部编辑冲突防护、每日快照健康(git 缺失界面可见)、未保存草稿退出保护、周文档覆盖确认、周报正确性修正、watcher 路径归一化、种子手册(隐私边界 + 脱敏反馈模板)。下一步:干净 Windows 环境实测 → release tag → 受控种子版;随后第二步阶梯 2a。计划见 [PLAN.md](PLAN.md)。 +**M3 完成 + 种子发布加固**(2026-09-11,M1–M3 全部落地并实机验证):数据落盘(Markdown 文件树)、活动日志、文件监听、收件箱、今日视角、终端面板、数据目录初始化、周文档一键生成、全文搜索、项目归档、markdown 预览之上,按发布前审阅补齐数据安全九项:事务式可恢复写入、单实例、外部编辑冲突防护、每日快照健康(git 缺失界面可见)、未保存草稿退出保护、周文档覆盖确认、周报正确性修正、watcher 路径归一化、种子手册(隐私边界 + 脱敏反馈模板)。**macOS 实机验证完成**(2026-09-12,PR #3):universal DMG(x86_64+arm64)、Finder 启动 PATH、应用内终端三件套、冲突/快照/.trash/单实例全矩阵通过;修复 macOS 数据层初始化 bug(fs 插件点文件作用域);确认 macOS 15+ 无「右键打开」Gatekeeper 通道,安装指引已按系统版本分流。下一步:干净 Windows 环境实测 → release tag → 受控种子版;随后第二步阶梯 2a。计划见 [PLAN.md](PLAN.md)。 ## 文档导航 @@ -22,7 +22,7 @@ ## 技术栈 - React 19 + Vite + Tailwind v4(SPA;按 [UI.md](UI.md) 契约集成外部交付的 UI,剥离其 SSR/登录/数据库) -- Tauri 2 桌面壳(plugin-fs / plugin-shell / single-instance / global-shortcut;生产线启用 CSP,见 [docs/release.md](docs/release.md) §5) +- Tauri 2 桌面壳(plugin-fs / plugin-shell / plugin-opener / single-instance / global-shortcut;生产线启用 CSP,见 [docs/release.md](docs/release.md) §5) - 数据访问统一走 `src/dataAccess.ts` 接口([UI.md](UI.md) 第 2 节契约):浏览器 dev → MockDataAccess;Tauri 桌面 → StorageDataAccess(Markdown 文件树落盘) - 数据层 `src/storage/`:纯核心 `core.ts`(无 Tauri API)+ IO 适配器 `fsAdapter.ts`(plugin-fs)+ 事务式写入 `atomicWrite.ts` + 每日快照健康 `snapshot.ts`;同一核心配 `node:fs` 即成 2b 阶段 MCP server(`tests/storageNode.test.ts` 的适配器即雏形) - Vitest 守护写回契约与数据安全(宽松读/规范写/保留未知字段/外部冲突防护/软删除/中断恢复);CI 见 `.github/workflows/ci.yml` diff --git a/docs/release.md b/docs/release.md index 056c0ea..0903a67 100644 --- a/docs/release.md +++ b/docs/release.md @@ -1,31 +1,44 @@ # 发布流程与边界(Release Playbook) > **每次打包发版前必读。** 本文件是发版的唯一规范;与代码冲突时以本文件为准修复代码。 -> `release/`(仓库根)是**唯一**发布产物位置,由 `npm run make-release` 生成——本目录不手工编辑、不手工投放文件。 +> `release/`(仓库根)是**唯一**发布产物位置,由 `npm run make-release` 生成——**生成型文件(README.md / SHA256SUMS.txt / 种子手册副本)绝不手工编辑**;对侧平台的安装包可以作为汇总输入拷入 `release/`,但拷入后**必须重跑** `npm run make-release` 重新生成校验和(见 §1 汇总语义)。 ## 0. 一句话流程 ```bash +# —— Windows 构建机(本机 GNU 工具链)—— export PATH="/d/perl/c/x86_64-w64-mingw32/bin:/d/perl/c/bin:$HOME/.cargo/bin:$PATH" export RUSTUP_TOOLCHAIN=stable-x86_64-pc-windows-gnu npx vitest run # ① 测试全绿 npm run build # ② tsc + vite 构建通过 npm run tauri build # ③ NSIS 安装包(同上环境) npm run make-release # ④ 归拢 release/(含门槛自检) + +# —— macOS 构建机(Apple Silicon 亦出 universal)—— +npx vitest run +npm run build +npm run tauri build -- --target universal-apple-darwin --bundles app,dmg # ③ DMG +npm run make-release # ④ 同一脚本,darwin 下收 DMG ``` +两台机器各自运行 `make-release` 生成自家产物;**汇总**:把对侧安装包拷进本机 `release/` 后重跑一次 `npm run make-release`——脚本按目录内实际存在的当前版本安装包全量重算 `SHA256SUMS.txt` 与 `README.md`,得到覆盖双平台的统一发布物。 + ## 1. 位置与产物(不变量) - `release/` 在仓库根、**gitignore**,内容只能由 [scripts/make-release.mjs](../scripts/make-release.mjs) 生成;脚本是入库的,产物不入库。 -- 产物固定四件套,缺一不可: +- 产物固定集合,缺一不可: | 文件 | 内容 | 源头 | |---|---|---| - | `ResearchThread__x64-setup.exe` | NSIS 安装包 | `npm run tauri build` | - | `SHA256SUMS.txt` | 安装包与手册的 SHA-256 | 脚本计算 | + | `ResearchThread__x64-setup.exe` | Windows NSIS 安装包 | Windows 机 `npm run tauri build` | + | `ResearchThread__universal.dmg` | macOS 安装映像(Apple Silicon + Intel 切片;**Intel 硬件未单独实测**,发布页须按此口径说明) | macOS 机 `tauri build --target universal-apple-darwin` | + | `SHA256SUMS.txt` | 安装包与手册的 SHA-256 | 脚本按 release/ 内实际安装包**全量重算**(跨平台统一,见下方汇总语义) | | `README.md` | 下载/校验/安装说明 + 构建提交记录 | 脚本模板 | | `种子测试手册.md` | 种子用户手册副本 | `docs/seed-manual.md`(只改源头,不改副本) | -- 每次生成**整体覆盖**:`release/` 只保留最新一个版本,不堆历史;历史版本靠 git tag 重新构建复现,发布后的安装包应转存到发布渠道/网盘归档。 -- 手工改 `release/` 里任何文件 = 无效发布,下次生成会被覆盖。 +- **汇总语义(消解双平台覆盖矛盾)**:脚本只清理「旧版本」安装包;**当前版本**的安装包跨平台累积——Windows 跑完把 dmg 拷进来重跑、或在 macOS 侧反向操作,`SHA256SUMS.txt` 都会自动覆盖双平台,无需手工拼校验和文件。**规则**:安装包(exe/dmg)可以作为汇总输入拷入 `release/`;除此之外的一切(README.md、SHA256SUMS.txt、手册副本)只能由脚本生成,手改 = 无效发布;任何拷入之后都必须重跑 `npm run make-release`,让校验和覆盖最终产物集合。 +- macOS 侧**只认 universal DMG**(`target/universal-apple-darwin/...`),严格匹配 `productName + version` 文件名;native/ARM-only DMG 不是发布物,脚本直接失败、不 fallback。 +- 历史(旧版本)不堆在 `release/`:发布后的安装包转存到发布渠道/网盘归档,靠 git tag 重新构建复现。 +- 生成型 metadata(README / SHA256SUMS / 手册副本)手改 = 无效发布,下次生成会被覆盖;安装包拷入见「汇总语义」。 +- Tauri 配置按平台拆分(构建时自动合并):`tauri.conf.json`(公共:窗口/CSP/图标)+ `tauri.windows.conf.json`(NSIS/WebView2Loader/须知页)+ `tauri.macos.conf.json`(app+dmg/最低 macOS 12)。**不要把平台特有配置写回公共文件。** ## 2. 版本规则 @@ -33,14 +46,25 @@ npm run make-release # ④ 归拢 release/(含门槛自检) - 发版必须先在 git 打 tag `vX.Y.Z`,tag 指向的提交 = 构建提交;从脏工作区构建会被脚本警告。 - 0.x 为种子期:功能新增升 minor、修复升 patch;数据格式破坏性变更(见 §5)即便 0.x 也需用户确认。 -## 3. 打包环境(本机现状) +## 3. 打包环境 + +### Windows(本机现状) - 机器无 Windows SDK,用 GNU 工具链:`stable-x86_64-pc-windows-gnu` + rust-lld(`src-tauri/.cargo/config.toml` 固定),启动命令见 §0。 - `src-tauri/WebView2Loader.dll` **勿删**:GNU 构建动态链接它,已用 `bundle.resources` 打进安装器;升级 `webview2-com-sys` 后必须同步替换该 dll 并重测安装包启动。 -- 改过 `src-tauri/capabilities/*.json` 后需 `touch src-tauri/build.rs` 强制重嵌(dev watch 不监听 capabilities)。 -- 发布前 `package-lock.json` 与 `src-tauri/Cargo.lock` 必须已提交(可复现构建的前提)。 - 安装器形态固定:**per-user NSIS、免管理员、未签名**;SmartScreen 警告由安装须知页(`src-tauri/INSTALL-NOTES.txt`)解释,**不得**要求用户关闭 SmartScreen。 +### macOS(v0.1.0 起) + +- 前置只需 Xcode Command Line Tools(`xcode-select --install`)+ rustup + node;第一轮先 native ARM(`npm run tauri dev`),通过后再 universal。 +- universal 二进制:`rustup target add aarch64-apple-darwin x86_64-apple-darwin` 后 `npm run tauri build -- --target universal-apple-darwin --bundles app,dmg`;产物在 `src-tauri/target/universal-apple-darwin/release/bundle/dmg/`。**发布口径**:universal 含 Intel 切片(`lipo` 可验证),但实际硬件验证只在 Apple Silicon 上做过——release note 必须写「Tested on Apple Silicon; Intel hardware has not yet been separately validated」,不得声称 Intel 已全面验证。 +- **PATH 修复是硬依赖**:Finder/Dock 启动的 GUI 不继承 shell PATH,`src-tauri/src/lib.rs` 里的 `fix_path_env::fix()` 不可移除;验收标准是 **Finder 双击启动**后 git/claude/codex 在内置终端可用(Terminal 启动可用不算过)。 +- 文件管理器走 `plugin-opener`(`openPath`),不进 shell 白名单;shell capability 按平台拆分(`capabilities/shell-macos.json` 只含 git/claude/codex/node/npm)。 +- 签名分两阶段:内测期不签名(未签名包被 Gatekeeper 拦截后,macOS 15+ 走「系统设置 → 隐私与安全性 → 仍要打开」放行;macOS 12–14 才有「右键 → 打开」——**「右键打开」通道在 macOS 15 Sequoia 起已被移除**,2026-09-12 M5/macOS 26.6 实测确认,手册已按系统版本分开写);**面向陌生用户分发前必须 Developer ID 签名 + 公证**(Tauri 支持 signing identity 自动公证),届时更新手册与 `tauri.macos.conf.json` 的签名配置。 +- `bundle.icon` 已含 `icons/icon.icns`;`minimumSystemVersion: 12.0`。 + +通用:改过 `src-tauri/capabilities/*.json` 后需 `touch src-tauri/build.rs` 强制重嵌(dev watch 不监听 capabilities);发布前 `package-lock.json` 与 `src-tauri/Cargo.lock` 必须已提交。 + ## 4. 每次发布的门槛(顺序执行,全过才发) 1. `npx vitest run` 全绿(当前基线 60 个)。 @@ -48,7 +72,9 @@ npm run make-release # ④ 归拢 release/(含门槛自检) 3. `npm run tauri dev` 冒烟:应用可启动、数据目录正常读取。 4. `npm run tauri build` 成功出包。 5. `npm run make-release`:脚本自检通过(版本三处一致;安装包不旧于源码;脏工作区警告)。**记住 SHA-256,发布渠道必须原文公布。** -6. 干净(或隔离)Windows 环境验证——没有做过这步的包不发给用户: +6. 干净(或隔离)环境验证——没有做过这步的包不发给用户: + + **Windows:** - [ ] 全新安装 → 首启欢迎卡出现,数据目录创建; - [ ] 无 git 的机器:左下出现「快照保护未生效」持续警告(装 git 后重试可恢复); - [ ] 双击两次启动:第二实例只聚焦第一个窗口(单实例); @@ -56,6 +82,21 @@ npm run make-release # ④ 归拢 release/(含门槛自检) - [ ] 覆盖安装(升级)后数据完好; - [ ] 卸载 → 数据目录**仍在** → 重装后数据回来; - [ ] 快照恢复演练:改错/删错任务后用 `git checkout` 找回。 + + **macOS(v0.1.0 起,第一轮 native + 第二轮 universal 各过一遍核心项):** + - [ ] DMG → 应用程序安装,Finder 双击启动(**不**从 Terminal 启动); + - [ ] Finder/Dock 启动后,内置终端 `git --version` / `claude --version` / `codex --version` 可用(PATH 修复验收); + - [ ] 数据目录 `~/ResearchThread` 创建、读写、重启仍在; + - [ ] 创建 Space/Project/Task、写 context、重启不丢; + - [ ] 外部编辑(Obsidian/文本编辑器)→ 冲突提示出现; + - [ ] Force Quit 后重启:中断写入被恢复(`.rt-bak` 机制); + - [ ] 连续双击两次启动:单实例聚焦; + - [ ] `⌘⇧Space` 全局快捷键唤起收件箱; + - [ ] 「打开数据目录」在 Finder 中打开(opener); + - [ ] 删除任务 → `.trash/` 可找回; + - [ ] 无 git 的 Mac:快照警告出现;装 git 后重试恢复; + - [ ] 旧 DMG → 新 DMG 覆盖升级数据完好;删 .app 卸载不删数据; + - [ ] 未签名 DMG 下载后(真实浏览器下载带 quarantine):macOS 15+ 弹「移到废纸篓/完成」→ 系统设置 → 隐私与安全性 →「仍要打开」可放行(macOS 12–14:右键 → 打开)。 7. git commit 并打 tag `vX.Y.Z`;tag message 记录版本与 SHA-256。 8. 发布渠道:安装包 + 公布的 SHA-256 + `release/README.md` 内容 + 种子手册。 @@ -84,8 +125,9 @@ npm run make-release # ④ 归拢 release/(含门槛自检) ## 7. 范围边界(防蔓延,发布不夹带) -- 只发 **Windows x64 NSIS**。macOS、安装包签名、自动更新是后续里程碑,不 sneak 进普通发版。 -- 安装器保持现有形态(中文界面 + 安装须知页 + 语言选择器);不加自定义安装页面、不捆绑其他软件。 +- 发布目标:**Windows x64 NSIS + macOS 12+ universal DMG(未签名内测期)**。Mac App Store、自动更新是后续里程碑,不 sneak 进普通发版。 +- 安装器形态固定:Windows 中文 NSIS + 安装须知页;macOS 走标准 DMG 拖入 Applications。不加自定义安装页面、不捆绑其他软件。 +- 面向陌生用户的 macOS 公开分发前,**必须** Developer ID 签名 + 公证(熟人内测可用「系统设置 → 隐私与安全性 → 仍要打开」过渡,macOS 15+ 无「右键 → 打开」通道,手册已写)。 - 反目标照旧(AGENTS.md):不做项目管理软件、不做笔记软件、不做 chat-first。 ## 8. 发布记录 diff --git a/docs/seed-manual.md b/docs/seed-manual.md index 1b22ebe..4aa4202 100644 --- a/docs/seed-manual.md +++ b/docs/seed-manual.md @@ -1,4 +1,4 @@ -# ResearchThread 种子测试手册(Windows x64 · 0.1.0 Seed) +# ResearchThread 种子测试手册(Windows x64 + macOS · 0.1.0 Seed) 感谢参与种子测试。这份手册说明怎么装、数据放在哪、怎么保护数据、以及反馈时我们收集什么、**不**收集什么。 @@ -75,8 +75,18 @@ Windows 版本:如 Win10 22H2 / Win11 23H2 ## 7. 已知限制(种子版) -- 仅 Windows x64;macOS、自动更新、安装包签名在后续版本。 +- Windows x64 + macOS(universal:Apple Silicon + Intel 切片;实际硬件验证在 Apple Silicon 完成,Intel 未单独实测);自动更新、安装包签名在后续版本。 - 周文档为实验功能:同一周「重新生成」会覆盖手工修改(有二次确认,git 历史可找回)。 - 图谱视图与整体目标冲突,保持禁用;Agent 面板是占位,尚未接真实 CLI。 遇到任何「数据看起来不对」的情况,请先复制整个数据目录留证,再联系我们。谢谢! + +## 8. macOS 附录(Mac 种子用户) + +- **安装**:双击 DMG,把 ResearchThread 拖进「应用程序」。安装包未签名,首次启动会被 macOS 拦截,放行方式(只需一次,**不要**去系统设置里关 Gatekeeper): + - **macOS 15(Sequoia)及更高**:双击/右键打开都会弹「移到废纸篓 / 完成」——点「**完成**」关掉它,然后打开 **系统设置 → 隐私与安全性**,滚到页面底部,点「ResearchThread 已被阻止」旁的「**仍要打开**」,Touch ID/密码确认即可。 + - **macOS 12–14**:在「应用程序」里**右键 ResearchThread → 打开 → 再点「打开」**即可。 +- **数据**:与 Windows 完全一致,存放在 `/Users/<你>/ResearchThread/`(Finder 里按 `⌘⇧G` 输入 `~/ResearchThread` 可直达)。删除应用不会删除这个目录。 +- **快照**:需要本机安装 git(`xcode-select --install` 装的 Command Line Tools 就自带)。Finder 双击启动的应用拿不到终端里的 PATH——应用已内置修复;若左下角仍出现「快照保护未生效」,装好 git 后在「设置 → 数据 → 重试」。 +- **并发规则、隐私说明、反馈模板**:与上文 Windows 部分完全相同。 +- 测试重点(Mac 版特有):Finder 双击启动后在内置终端跑 `git --version` 应该成功;「打开数据目录」应该唤起 Finder。 diff --git a/package-lock.json b/package-lock.json index 75b78e4..82724c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@tauri-apps/api": "^2.11.1", "@tauri-apps/plugin-fs": "^2.5.2", "@tauri-apps/plugin-global-shortcut": "^2.3.2", + "@tauri-apps/plugin-opener": "^2.5.5", "@tauri-apps/plugin-shell": "^2.3.6", "@xterm/addon-fit": "^0.11.0", "@xterm/xterm": "^6.0.0", @@ -1199,6 +1200,15 @@ "@tauri-apps/api": "^2.11.0" } }, + "node_modules/@tauri-apps/plugin-opener": { + "version": "2.5.5", + "resolved": "https://registry.npmmirror.com/@tauri-apps/plugin-opener/-/plugin-opener-2.5.5.tgz", + "integrity": "sha512-xvzGai5aQds8j8R8RsUK/lW6pGG50YgOYIPLzvkqmkwAj7dfySOD7sGtejRzvVdMmv1EQfKVEFh1MvmDp8QR0g==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.11.0" + } + }, "node_modules/@tauri-apps/plugin-shell": { "version": "2.3.6", "resolved": "https://registry.npmmirror.com/@tauri-apps/plugin-shell/-/plugin-shell-2.3.6.tgz", diff --git a/package.json b/package.json index 3aec4f1..c0a09bc 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@tauri-apps/api": "^2.11.1", "@tauri-apps/plugin-fs": "^2.5.2", "@tauri-apps/plugin-global-shortcut": "^2.3.2", + "@tauri-apps/plugin-opener": "^2.5.5", "@tauri-apps/plugin-shell": "^2.3.6", "@xterm/addon-fit": "^0.11.0", "@xterm/xterm": "^6.0.0", diff --git a/scripts/make-release.mjs b/scripts/make-release.mjs index 665ce2a..3af63b0 100644 --- a/scripts/make-release.mjs +++ b/scripts/make-release.mjs @@ -1,21 +1,25 @@ -// scripts/make-release.mjs — 把发布产物归拢到 release/(可重复执行)。 -// 输入:src-tauri/target/release/bundle/nsis/ 下的安装包 + docs/seed-manual.md -// 输出:release/ResearchThread__x64-setup.exe、SHA256SUMS.txt、README.md、种子测试手册.md +// scripts/make-release.mjs — 把发布产物归拢到 release/(可重复执行,平台感知,跨平台可汇总)。 +// Windows(win32):收 NSIS 安装包 ResearchThread__x64-setup.exe +// macOS(darwin):只收 universal DMG(src-tauri/target/universal-apple-darwin/...), +// 严格匹配 productName+version,没有就失败——native/ARM-only DMG 不允许当发布物。 +// 汇总语义:release/ 内「当前版本」的安装包跨平台累积(本脚本只清理旧版本产物), +// SHA256SUMS.txt / README.md 每次按 release/ 内实际存在的安装包全量重算—— +// 把对侧安装包拷进 release/ 后重跑本脚本,即得覆盖双平台的统一校验和。 // 用法:先 npm run tauri build,再 npm run make-release。 // 发版要求与红线见 docs/release.md;本脚本强制:版本三处一致,其余为警告。 import { execFileSync } from "node:child_process"; import { createHash } from "node:crypto"; -import { copyFile, mkdir, readFile, stat, writeFile } from "node:fs/promises"; +import { copyFile, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const isMac = process.platform === "darwin"; const conf = JSON.parse(await readFile(join(root, "src-tauri/tauri.conf.json"), "utf8")); const version = conf.version; -const installerName = `ResearchThread_${version}_x64-setup.exe`; -const installerSrc = join(root, "src-tauri/target/release/bundle/nsis", installerName); +const productName = conf.productName; const sha256 = async (path) => { const buf = await readFile(path); @@ -43,11 +47,38 @@ if (pkg.version !== version || cargoVersion !== version) { process.exit(1); } -// 2) 安装包存在 -try { - await stat(installerSrc); -} catch { - console.error(`找不到安装包 ${installerSrc}——先运行 npm run tauri build`); +// 2) 定位本平台安装包(严格版本匹配;macOS 只认 universal) +const installerRe = new RegExp(`^${productName}_${version}_.+\\.(exe|dmg)$`); +async function findInstaller() { + if (!isMac) { + const name = `${productName}_${version}_x64-setup.exe`; + return { src: join(root, "src-tauri/target/release/bundle/nsis", name), name }; + } + // 只扫 universal 目录:native/ARM-only DMG 不是发布物,绝不 fallback + const dmgDir = join(root, "src-tauri/target/universal-apple-darwin/release/bundle/dmg"); + const found = []; + try { + for (const f of await readdir(dmgDir)) { + if (installerRe.test(f)) { + const full = join(dmgDir, f); + found.push({ src: full, name: f, mtime: (await stat(full)).mtimeMs }); + } + } + } catch { + // 目录不存在(未做 universal 构建) + } + if (found.length === 0) return null; + found.sort((a, b) => b.mtime - a.mtime); + return found[0]; +} + +const installer = await findInstaller(); +if (!installer) { + console.error( + isMac + ? `找不到 universal DMG(${productName}_${version}_*.dmg @ src-tauri/target/universal-apple-darwin/release/bundle/dmg/)——先运行 npm run tauri build -- --target universal-apple-darwin --bundles app,dmg。native/ARM-only DMG 不能作为发布物。` + : `找不到安装包 ${productName}_${version}_x64-setup.exe——先运行 npm run tauri build`, + ); process.exit(1); } @@ -57,10 +88,9 @@ const gitDirty = (git("status", "--porcelain") ?? "").length > 0; if (gitDirty) console.warn("⚠ 工作区有未提交改动——发布应从干净的提交 / tag 构建(docs/release.md §2)"); // 4) 安装包是否比源码旧(过期构建警告) -const installerMtime = (await stat(installerSrc)).mtimeMs; +const installerMtime = (await stat(installer.src)).mtimeMs; const staleSources = []; const scan = async (dir) => { - const { readdir } = await import("node:fs/promises"); for (const e of await readdir(dir, { withFileTypes: true })) { const full = join(dir, e.name); if (e.isDirectory()) await scan(full); @@ -70,69 +100,115 @@ const scan = async (dir) => { await scan(join(root, "src")); await scan(join(root, "src-tauri/src")); await scan(join(root, "src-tauri/capabilities")); -for (const f of ["src-tauri/tauri.conf.json", "src-tauri/INSTALL-NOTES.txt"]) { - if ((await stat(join(root, f))).mtimeMs > installerMtime) staleSources.push(f); +for (const f of [ + "src-tauri/tauri.conf.json", + "src-tauri/tauri.windows.conf.json", + "src-tauri/tauri.macos.conf.json", + "src-tauri/INSTALL-NOTES.txt", +]) { + try { + if ((await stat(join(root, f))).mtimeMs > installerMtime) staleSources.push(f); + } catch { + // 平台配置文件可以不存在 + } } if (staleSources.length > 0) { console.warn(`⚠ 安装包早于 ${staleSources.length} 个较新的源文件(如 ${staleSources[0]})——确认已重新 npm run tauri build`); } -// —— 生成 release/(整体覆盖,只保留最新版本) —— +// —— 生成 release/ —— const outDir = join(root, "release"); await mkdir(outDir, { recursive: true }); -const installerDest = join(outDir, installerName); -await copyFile(installerSrc, installerDest); +// 清理旧版本产物(当前版本的安装包跨平台累积,不删对侧文件) +const oldArtifactRe = new RegExp(`^${productName}_\\d+\\.\\d+\\.\\d+_.+\\.(exe|dmg)$`); +for (const f of await readdir(outDir)) { + if (oldArtifactRe.test(f) && !installerRe.test(f)) { + await rm(join(outDir, f), { force: true }); + console.log(`已清理旧版本产物:${f}`); + } +} -const installerHash = await sha256(installerDest); -await copyFile(join(root, "docs/seed-manual.md"), join(outDir, "种子测试手册.md")); +const installerDest = join(outDir, installer.name); +await copyFile(installer.src, installerDest); -const manualHash = await sha256(join(outDir, "种子测试手册.md")); +// 收集 release/ 内当前版本的全部安装包(含对侧拷入的),统一重算校验和 +const bundled = (await readdir(outDir)) + .filter((f) => installerRe.test(f)) + .sort(); +const manualName = "种子测试手册.md"; +await copyFile(join(root, "docs/seed-manual.md"), join(outDir, manualName)); +const hashes = {}; +for (const f of [...bundled, manualName]) { + hashes[f] = await sha256(join(outDir, f)); +} await writeFile( join(outDir, "SHA256SUMS.txt"), - `${installerHash} ${installerName}\n${manualHash} 种子测试手册.md\n`, + [...bundled, manualName].map((f) => `${hashes[f]} ${f}`).join("\n") + "\n", "utf8", ); const buildDate = new Date().toISOString().slice(0, 10); +const platformRows = bundled + .map((f) => + f.endsWith(".exe") + ? `| \`${f}\` | Windows x64 安装包(NSIS,per-user,免管理员;中文界面与安装须知页) |` + : `| \`${f}\` | macOS 安装映像(DMG,universal:Apple Silicon + Intel 切片;Intel 硬件未单独实测) |`, + ) + .join("\n"); +const verifyLines = bundled + .map((f) => `- \`${f}\`:\`${hashes[f]}\``) + .join("\n"); + await writeFile( join(outDir, "README.md"), - `# ResearchThread ${version} 种子发布包(Windows x64) + `# ResearchThread ${version} 种子发布包 本目录为受控种子测试版发布物(scripts/make-release.mjs 生成,可重复执行)。 发版要求与红线见仓库 docs/release.md。 | 文件 | 说明 | |---|---| -| \`${installerName}\` | NSIS 安装包(per-user,免管理员;安装器有中文界面与安装须知页) | -| \`SHA256SUMS.txt\` | 校验值(发布渠道需原文公布) | -| \`种子测试手册.md\` | 种子用户手册:数据/快照/并发/隐私与脱敏反馈模板 | +${platformRows} +| \`SHA256SUMS.txt\` | 校验值(发布渠道需原文公布,覆盖下方全部安装包) | +| \`${manualName}\` | 种子用户手册:数据/快照/并发/隐私与脱敏反馈模板 | + +## 校验值(SHA-256) + +${verifyLines} ## 安装步骤 -1. 核对校验值:PowerShell 执行 \`Get-FileHash .\\${installerName}\`,与 \`SHA256SUMS.txt\` 或发布页公布的值一致。 +**Windows:** +1. 核对校验值:PowerShell 执行 \`Get-FileHash .\\<安装包>\`,与 \`SHA256SUMS.txt\` 或发布页公布的值一致。 2. 双击安装包。未签名会过 SmartScreen:**「更多信息」→「仍要运行」**,不需要关闭 SmartScreen。 3. 首次启动应用会弹欢迎提示(数据位置 / 快照警告含义 / 反馈方式),照读一遍即可。 +**macOS:** +1. 核对校验值:终端执行 \`shasum -a 256 ./\`,与 \`SHA256SUMS.txt\` 或发布页公布的值一致。 +2. 双击 DMG,把 ResearchThread 拖入「应用程序」。 +3. 首次启动(未签名构建):macOS 15+ 会弹「移到废纸篓 / 完成」——点「完成」关掉,再打开 **系统设置 → 隐私与安全性**,页面底部点「ResearchThread 已被阻止」旁的「**仍要打开**」确认;macOS 12–14:右键 ResearchThread → 打开 → 打开。不要关闭 Gatekeeper。 +4. 应用为 universal 二进制(Apple Silicon + Intel 切片),实际硬件验证在 Apple Silicon 上完成,Intel 未单独实测——发布页按此口径说明。 + ## 快速核对 -- 程序装在 \`%LOCALAPPDATA%\\ResearchThread\`(约 10 MB)。 -- 数据目录 \`~/ResearchThread\` 由应用首次启动创建,卸载**不会**删除。 +- 数据目录 \`~/ResearchThread\` 由应用首次启动创建,卸载**不会**删除(Windows 卸载只删程序本体;macOS 删 .app 同理)。 - 每天首次启动自动 git 快照;无 git 机器会显示「快照保护未生效」持续警告(数据仍正常保存)。 ## 构建信息 - 版本:${version}(tauri.conf.json = package.json = Cargo.toml) +- 本次生成平台:${isMac ? "macOS" : "Windows"}(另一平台安装包由对侧构建后拷入本目录重跑本脚本汇总) - 构建提交:\`${gitHead}\`${gitDirty ? "(⚠ 工作区不干净)" : ""} - 构建日期:${buildDate} -- ${installerName} -- SHA-256:\`${installerHash}\` +- 本次归拢:${installer.name} `, "utf8", ); console.log(`release/ 已生成(构建提交 ${gitHead}${gitDirty ? ",工作区不干净" : ""}):`); -console.log(` ${installerName} (SHA-256 ${installerHash})`); -console.log(` SHA256SUMS.txt / README.md / 种子测试手册.md`); +console.log(` 本次归拢 ${installer.name} (SHA-256 ${hashes[installer.name]})`); +console.log(` 覆盖安装包:${bundled.join(", ") || "(无)"}`); +console.log(` SHA256SUMS.txt / README.md / ${manualName}`); diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 69a5283..44f2398 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1026,6 +1026,16 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" +[[package]] +name = "fix-path-env" +version = "0.0.0" +source = "git+https://github.com/tauri-apps/fix-path-env-rs#c4c45d503ea115a839aae718d02f79e7c7f0f673" +dependencies = [ + "home", + "strip-ansi-escapes", + "thiserror 1.0.69", +] + [[package]] name = "flate2" version = "1.1.10" @@ -1533,6 +1543,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "html5ever" version = "0.38.0" @@ -3010,12 +3029,14 @@ dependencies = [ name = "researchthread" version = "0.1.0" dependencies = [ + "fix-path-env", "serde", "serde_json", "tauri", "tauri-build", "tauri-plugin-fs", "tauri-plugin-global-shortcut", + "tauri-plugin-opener", "tauri-plugin-shell", "tauri-plugin-single-instance", ] @@ -3485,6 +3506,15 @@ dependencies = [ "quote", ] +[[package]] +name = "strip-ansi-escapes" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8f8038e7e7969abb3f1b7c2a811225e9296da208539e0f79c5251d6cac0025" +dependencies = [ + "vte", +] + [[package]] name = "strsim" version = "0.11.1" @@ -3794,6 +3824,28 @@ dependencies = [ "thiserror 2.0.20", ] +[[package]] +name = "tauri-plugin-opener" +version = "2.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60d60366174b745b4ef5824b8bbc1c457fd08f0ce101ff643c0a49181a9f4e91" +dependencies = [ + "dunce", + "glob", + "objc2-app-kit", + "objc2-foundation", + "open", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.20", + "url", + "windows", + "zbus", +] + [[package]] name = "tauri-plugin-shell" version = "2.3.6" @@ -4450,6 +4502,15 @@ dependencies = [ "libc", ] +[[package]] +name = "vte" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077" +dependencies = [ + "memchr", +] + [[package]] name = "walkdir" version = "2.5.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index ca79eee..6949f3f 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -16,8 +16,11 @@ tauri-build = { version = "2", features = [] } tauri = { version = "2", features = [] } tauri-plugin-fs = { version = "2", features = ["watch"] } tauri-plugin-shell = "2" +tauri-plugin-opener = "2" tauri-plugin-single-instance = "2" tauri-plugin-global-shortcut = "2" +# Finder/Dock 启动的 GUI 不继承 shell PATH(Tauri 官方修复,见 docs/release.md mac 章节) +fix-path-env = { git = "https://github.com/tauri-apps/fix-path-env-rs" } serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/src-tauri/capabilities/common.json b/src-tauri/capabilities/common.json new file mode 100644 index 0000000..dc53722 --- /dev/null +++ b/src-tauri/capabilities/common.json @@ -0,0 +1,43 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "common", + "description": "跨平台能力:数据目录文件 IO 与监听、目录打开(opener)、全局快捷键", + "windows": ["main"], + "permissions": [ + "core:default", + "core:window:allow-show", + "core:window:allow-hide", + "core:window:allow-set-focus", + "core:window:allow-is-visible", + "core:window:allow-close", + "core:window:allow-destroy", + "fs:default", + "fs:allow-read-text-file", + "fs:allow-write-text-file", + "fs:allow-read-dir", + "fs:allow-mkdir", + "fs:allow-exists", + "fs:allow-remove", + "fs:allow-rename", + "fs:allow-stat", + "fs:allow-watch", + { + "identifier": "fs:scope", + "allow": [ + { "path": "$HOME/ResearchThread" }, + { "path": "$HOME/ResearchThread/**" } + ] + }, + { + "identifier": "opener:allow-open-path", + "allow": [ + { "path": "$HOME/ResearchThread" }, + { "path": "$HOME/ResearchThread/**" } + ] + }, + "global-shortcut:allow-register", + "global-shortcut:allow-unregister", + "global-shortcut:allow-unregister-all", + "global-shortcut:allow-is-registered" + ] +} diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json deleted file mode 100644 index 5bde162..0000000 --- a/src-tauri/capabilities/default.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "$schema": "../gen/schemas/desktop-schema.json", - "identifier": "default", - "description": "ResearchThread 主窗口能力:数据目录文件 IO 与监听、终端 shell、全局快捷键", - "windows": ["main"], - "permissions": [ - "core:default", - "core:window:allow-show", - "core:window:allow-hide", - "core:window:allow-set-focus", - "core:window:allow-is-visible", - "core:window:allow-close", - "core:window:allow-destroy", - "fs:default", - "fs:allow-read-text-file", - "fs:allow-write-text-file", - "fs:allow-read-dir", - "fs:allow-mkdir", - "fs:allow-exists", - "fs:allow-remove", - "fs:allow-rename", - "fs:allow-stat", - "fs:allow-watch", - { - "identifier": "fs:scope", - "allow": [ - { "path": "$HOME/ResearchThread" }, - { "path": "$HOME/ResearchThread/**" } - ], - "requireLiteralLeadingDot": false - }, - { - "identifier": "shell:allow-execute", - "allow": [ - { "name": "git", "cmd": "git", "args": true }, - { "name": "claude", "cmd": "claude", "args": true }, - { "name": "codex", "cmd": "codex", "args": true }, - { "name": "cmd", "cmd": "cmd", "args": true }, - { "name": "powershell", "cmd": "powershell", "args": true }, - { "name": "explorer", "cmd": "explorer", "args": true }, - { "name": "npm", "cmd": "npm", "args": true }, - { "name": "node", "cmd": "node", "args": true } - ] - }, - { - "identifier": "shell:allow-spawn", - "allow": [ - { "name": "git", "cmd": "git", "args": true }, - { "name": "claude", "cmd": "claude", "args": true }, - { "name": "codex", "cmd": "codex", "args": true }, - { "name": "cmd", "cmd": "cmd", "args": true }, - { "name": "powershell", "cmd": "powershell", "args": true }, - { "name": "explorer", "cmd": "explorer", "args": true }, - { "name": "npm", "cmd": "npm", "args": true }, - { "name": "node", "cmd": "node", "args": true } - ] - }, - "shell:allow-kill", - "shell:allow-stdin-write", - "global-shortcut:allow-register", - "global-shortcut:allow-unregister", - "global-shortcut:allow-unregister-all", - "global-shortcut:allow-is-registered" - ] -} diff --git a/src-tauri/capabilities/shell-macos.json b/src-tauri/capabilities/shell-macos.json new file mode 100644 index 0000000..b3db5ba --- /dev/null +++ b/src-tauri/capabilities/shell-macos.json @@ -0,0 +1,31 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "shell-macos", + "description": "macOS 终端/快照 shell 白名单(高权限边界,见 docs/release.md §5-0):git/claude/codex/node/npm;文件管理器走 opener,不需要 shell", + "windows": ["main"], + "platforms": ["macOS"], + "permissions": [ + { + "identifier": "shell:allow-execute", + "allow": [ + { "name": "git", "cmd": "git", "args": true }, + { "name": "claude", "cmd": "claude", "args": true }, + { "name": "codex", "cmd": "codex", "args": true }, + { "name": "npm", "cmd": "npm", "args": true }, + { "name": "node", "cmd": "node", "args": true } + ] + }, + { + "identifier": "shell:allow-spawn", + "allow": [ + { "name": "git", "cmd": "git", "args": true }, + { "name": "claude", "cmd": "claude", "args": true }, + { "name": "codex", "cmd": "codex", "args": true }, + { "name": "npm", "cmd": "npm", "args": true }, + { "name": "node", "cmd": "node", "args": true } + ] + }, + "shell:allow-kill", + "shell:allow-stdin-write" + ] +} diff --git a/src-tauri/capabilities/shell-windows.json b/src-tauri/capabilities/shell-windows.json new file mode 100644 index 0000000..919db3a --- /dev/null +++ b/src-tauri/capabilities/shell-windows.json @@ -0,0 +1,35 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "shell-windows", + "description": "Windows 终端/快照 shell 白名单(高权限边界,见 docs/release.md §5-0):git/claude/codex/node/npm + cmd/powershell;explorer 已由 opener 取代", + "windows": ["main"], + "platforms": ["windows"], + "permissions": [ + { + "identifier": "shell:allow-execute", + "allow": [ + { "name": "git", "cmd": "git", "args": true }, + { "name": "claude", "cmd": "claude", "args": true }, + { "name": "codex", "cmd": "codex", "args": true }, + { "name": "cmd", "cmd": "cmd", "args": true }, + { "name": "powershell", "cmd": "powershell", "args": true }, + { "name": "npm", "cmd": "npm", "args": true }, + { "name": "node", "cmd": "node", "args": true } + ] + }, + { + "identifier": "shell:allow-spawn", + "allow": [ + { "name": "git", "cmd": "git", "args": true }, + { "name": "claude", "cmd": "claude", "args": true }, + { "name": "codex", "cmd": "codex", "args": true }, + { "name": "cmd", "cmd": "cmd", "args": true }, + { "name": "powershell", "cmd": "powershell", "args": true }, + { "name": "npm", "cmd": "npm", "args": true }, + { "name": "node", "cmd": "node", "args": true } + ] + }, + "shell:allow-kill", + "shell:allow-stdin-write" + ] +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 5a661db..93cbdb0 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -14,7 +14,16 @@ pub fn run() { })) .plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_shell::init()) + .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_global_shortcut::Builder::new().build()) + .setup(|_app| { + // macOS:Finder/Dock 启动不继承 .zshrc 的 PATH,git/claude/codex 会找不到; + // 官方 fix-path-env 恢复登录 shell 的 PATH(其他平台为安全 no-op) + if let Err(e) = fix_path_env::fix() { + eprintln!("fix_path_env failed: {e}"); + } + Ok(()) + }) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 39571f5..e2749d0 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -24,21 +24,18 @@ "devCsp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src ipc: http://ipc.localhost ws://localhost:1420 http://localhost:1420" } }, + "plugins": { + "fs": { + "requireLiteralLeadingDot": false + } + }, "bundle": { "active": true, - "targets": ["nsis"], - "resources": ["WebView2Loader.dll"], - "license": "INSTALL-NOTES.txt", - "windows": { - "nsis": { - "languages": ["SimpChinese", "English"], - "displayLanguageSelector": true - } - }, "icon": [ "icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", + "icons/icon.icns", "icons/icon.ico" ] } diff --git a/src-tauri/tauri.macos.conf.json b/src-tauri/tauri.macos.conf.json new file mode 100644 index 0000000..8173e9c --- /dev/null +++ b/src-tauri/tauri.macos.conf.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "bundle": { + "targets": ["app", "dmg"], + "macOS": { + "minimumSystemVersion": "12.0" + } + } +} diff --git a/src-tauri/tauri.windows.conf.json b/src-tauri/tauri.windows.conf.json new file mode 100644 index 0000000..a89ece3 --- /dev/null +++ b/src-tauri/tauri.windows.conf.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "bundle": { + "targets": ["nsis"], + "resources": ["WebView2Loader.dll"], + "license": "INSTALL-NOTES.txt", + "windows": { + "nsis": { + "languages": ["SimpChinese", "English"], + "displayLanguageSelector": true + } + } + } +} diff --git a/src/main.tsx b/src/main.tsx index fa22d99..c022092 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -44,12 +44,21 @@ async function boot(): Promise { initialTask = null; } } catch (e) { - root.innerHTML = ` -
-

ResearchThread 数据层初始化失败

-

${String(e)}

-

请检查数据目录(~/ResearchThread)权限后重启应用。

-
`; + // renderer 安全红线:错误文案一律 textContent,不经 innerHTML(docs/release.md §5-0) + const box = document.createElement("div"); + box.style.cssText = + "font-family:system-ui,sans-serif;padding:48px;max-width:560px;margin:0 auto;color:#1c1917"; + const h1 = document.createElement("h1"); + h1.style.cssText = "font-size:18px;margin-bottom:12px"; + h1.textContent = "ResearchThread 数据层初始化失败"; + const msg = document.createElement("p"); + msg.style.cssText = "color:#666;line-height:1.6;white-space:pre-wrap"; + msg.textContent = e instanceof Error ? e.message : String(e); + const hint = document.createElement("p"); + hint.style.cssText = "color:#999;margin-top:16px"; + hint.textContent = "请检查数据目录(~/ResearchThread)权限后重启应用。"; + box.append(h1, msg, hint); + root.replaceChildren(box); return; } diff --git a/src/storage/git.ts b/src/storage/git.ts index fca5f36..f5df95d 100644 --- a/src/storage/git.ts +++ b/src/storage/git.ts @@ -48,8 +48,8 @@ export async function gitSnapshot(dataDir: string, label?: string): Promise { - const cmd = Command.create("explorer", [dir]); - await cmd.spawn(); + const { openPath } = await import("@tauri-apps/plugin-opener"); + await openPath(dir); }