feat: 统一 Commilitia Drop 全客户端命名与分发
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
# cdrop 桌面原生数据面(Option A)设计
|
||||
# Commilitia Drop 桌面原生数据面(Option A)设计
|
||||
|
||||
> 目标:把桌面 **P2P 数据面**(WebRTC DataChannel 收发)从 WebView 的 JS 移到 Go 进程(`pion/webrtc`),**留在 Wails、不上 Tauri**。
|
||||
> 关联:`desktop/PLAN.md`(桌面总计划)、`desktop/RESEARCH.md`(调研)、`web/src/features/transfer/{transfer,p2p,relay,incomingSink,source}.ts`、吞吐专项诊断(task #38)。
|
||||
@@ -62,7 +62,7 @@ selectBackend() → isDesktop() ? goBridgeBackend : jsWebrtcBackend // web / i
|
||||
```
|
||||
Sender(sessionId, peerName, filePath, iceServers):
|
||||
pc ← pion.NewPeerConnection(SettingEngine{ rwnd 大; host 候选真 IP; iceServers })
|
||||
dc ← pc.CreateDataChannel("cdrop-file", ordered=true)
|
||||
dc ← pc.CreateDataChannel("commilitia-drop-file", ordered=true)
|
||||
onOpen:
|
||||
send JSON {type:meta, name, size, sha256?} // 与 p2p.ts 同帧
|
||||
for chunk in readFileByPath(filePath, CHUNK): // Go 直接磁盘读,无桥、无 base64
|
||||
@@ -117,7 +117,7 @@ ICE creds:Go 拉 /api/calls/credentials(带 a.token),与 JS 同源
|
||||
> 双实现的唯一契约是**线协议**。任何一项漂移都会让桌面↔web/iOS 互通断裂。建议抽出共享测试向量(meta/chunk/done/ack 的字节级样例)双侧回归。
|
||||
|
||||
- **信令**:`POST /api/hub/signal {to, payload:{type:"offer"|"answer"|"ice", sdp?, candidate?}}`;入站经 SSE。
|
||||
- **DataChannel**:名 `"cdrop-file"`,`ordered:true`。
|
||||
- **DataChannel**:名 `"commilitia-drop-file"`,`ordered:true`。
|
||||
- **帧**:`meta` JSON `{type,name,size,sha256?}` / 二进制 chunk / `done` JSON / `ack` JSON `{type,bytes}`(接收端节流 200ms 回传累计已收字节,单调取大)。
|
||||
- **状态机**:`/api/transfer/{initiate,p2p,done,fail,cancel,fallback}`;**完成由接收端 POST `/done`**(权威),发送端不重复。
|
||||
- **中继回退语义**:JS 看门狗 30s 未 `connected` → `POST /fallback` 并对 Go `Cancel(session)`;Go 拆 pion,relay 由 JS 接管(数据面回到 JS relay,符合「relay 留 JS」)。
|
||||
@@ -130,7 +130,7 @@ ICE creds:Go 拉 /api/calls/credentials(带 a.token),与 JS 同源
|
||||
>
|
||||
> 实测证据:iOS 经 gomobile/pion 时 Go 的 raw BSD socket 不与 iOS Network framework 集成(WebKit/libwebrtc 才集成)——Mac→iOS 常连不上(~30s ICE 超时)、iOS→Mac 偶连上 host↔host 但 ~4.5MB 中途断流(`read/write on closed pipe`)、TURN CreatePermission 刷屏、时好时坏。接口 / 链路本地 / IPv4-only 过滤只去噪、治不了架构不兼容(且那些 iOS 定向过滤漏进桌面共享引擎致 IPv6 host 对被裁退中继、重大回归)。即下方 §137 标注的“LOW-MEDIUM 成熟度、无 pion+gomobile 生产先例”风险兑现。
|
||||
>
|
||||
> **U2 落地(现行)**:JS 路由 / 桥协议 / 线协议 / `IOS_NATIVE` 开关全不变,只把 Swift 侧原生引擎从 gomobile `EngineEngine` 换成 `ios/CDrop/Sources/Engine/LibWebRtcEngine.swift`(libwebrtc `RTCPeerConnection`,stasel/WebRTC M149 经 SPM、pin 精确版本 149.0.0)。该引擎是 `engine.go`/`session.go` 线协议的忠实 Swift 端口——DataChannel `cdrop-file`(ordered)、meta/分片(64KB)/done/ack 文本+二进制帧、16MB/4MB 水位背压、ack 追平完成、不冲突落盘,逐字节对齐桌面 pion 与 web JS 引擎,故 iOS↔桌面、iOS↔浏览器互通。桌面数据面仍走 pion(isDesktop 分支,不受影响)。libwebrtc 与 iOS 网络栈原生集成 + DcSCTP 默认 ~5MB rwnd(不受 WebKit 256KB 限),是吞吐与连通性的根治。
|
||||
> **U2 落地(现行)**:JS 路由 / 桥协议 / 线协议 / `IOS_NATIVE` 开关全不变,只把 Swift 侧原生引擎从 gomobile `EngineEngine` 换成 `ios/CDrop/Sources/Engine/LibWebRtcEngine.swift`(libwebrtc `RTCPeerConnection`,stasel/WebRTC M149 经 SPM、pin 精确版本 149.0.0)。该引擎是 `engine.go`/`session.go` 线协议的忠实 Swift 端口——DataChannel `commilitia-drop-file`(ordered)、meta/分片(64KB)/done/ack 文本+二进制帧、16MB/4MB 水位背压、ack 追平完成、不冲突落盘,逐字节对齐桌面 pion 与 web JS 引擎,故 iOS↔桌面、iOS↔浏览器互通。桌面数据面仍走 pion(isDesktop 分支,不受影响)。libwebrtc 与 iOS 网络栈原生集成 + DcSCTP 默认 ~5MB rwnd(不受 WebKit 256KB 限),是吞吐与连通性的根治。
|
||||
>
|
||||
> **验证(2026-06-28)**:模拟器全 app(含 Share/控件扩展)编译过 + web typecheck/build 过;**环回端到端测试**(两个 LibWebRtcEngine 同进程交叉连、20MB 文件经完整线协议传输后逐字节完整性比对)✅ 过(`ios/CDrop/Tests/LibWebRtcEngineTests.swift`,`just ios-sim-build` 同款免签名构建)。真机 / 真网吞吐为剩余闸(须 deploy prod 让设备拿到 `IOS_NATIVE=true` 的引擎 + 真机装机,二者 Touch ID 门控)。
|
||||
>
|
||||
|
||||
+22
-10
@@ -1,4 +1,4 @@
|
||||
# cdrop 桌面客户端实施计划
|
||||
# Commilitia Drop 桌面客户端实施计划
|
||||
|
||||
> Wails v2 · macOS 优先 + Windows 已落地
|
||||
> 状态:**macOS 与 Windows 双端均已实现并 RDP/真机实测通过**——A2 前端复用 + 托盘/菜单栏 + 剪贴板双向同步 + 桌面设置 + session 持久化 + 开机自启(含自启静默驻留)+ 设备类型登记。macOS 见 §10,Windows 落地(含「custom-scheme 不支持流式」核心坑与 127.0.0.1 本地代理解法)见 §11。计划已据原生平台研究 sweep(`desktop/RESEARCH.md`)校正。
|
||||
@@ -26,7 +26,9 @@
|
||||
|
||||
## 0. 范围、非范围与本轮关键决策
|
||||
|
||||
**范围**:常驻桌面客户端,补浏览器做不到的——后台常驻、剪贴板自动监听上传、文件落盘、系统通知、全局快捷键。平台 **macOS(Apple Silicon + Intel)优先**,**Windows(x64)保持兼容**(同源、同分支,平台差异用 Go build tag 隔离)。
|
||||
**范围**:常驻桌面客户端,补浏览器做不到的——后台常驻、剪贴板自动监听上传、文件落盘、
|
||||
系统通知、全局快捷键。平台 **macOS(仅 Apple Silicon)优先**,**Windows(x64)保持兼容**
|
||||
(同源、同分支,平台差异用 Go build tag 隔离)。
|
||||
|
||||
**非范围(推后)**:Linux 打包、iOS / Android 原生、自更新(仅版本检查提示)、产品化 / 多租户、Admin、Symmetric NAT 的 mDNS 直连、本地内容缓存 / 离线。
|
||||
|
||||
@@ -109,7 +111,7 @@ sweep 把 D1 从“最高风险未知”变成“路径已明、含一处后端
|
||||
|
||||
### 3.1 回调通道:loopback(已定,弃 scheme)
|
||||
|
||||
采用 **RFC 8252 loopback**:Go 起临时 `127.0.0.1:0`(必须 IPv4 字面量,不用 `localhost`)HTTP server 捕获 `code`。相对自定义 scheme `cdrop://` 的优势:免单实例锁、免 `Info.plist` / 注册表登记、免深链解析、跨平台代码一致。scheme 留作未来“浏览器点 cdrop 链接唤起桌面端”的另一用例。
|
||||
采用 **RFC 8252 loopback**:Go 起临时 `127.0.0.1:0`(必须 IPv4 字面量,不用 `localhost`)HTTP server 捕获 `code`。相对自定义 scheme `commilitia-drop://` 的优势:免单实例锁、免 `Info.plist` / 注册表登记、免深链解析、跨平台代码一致。scheme 留作未来“浏览器点 Commilitia Drop 链接唤起桌面端”的另一用例。
|
||||
|
||||
### 3.2 token 交换放 Go 侧(已定)
|
||||
|
||||
@@ -166,13 +168,20 @@ sweep 源码级确认:Casdoor 支持 public client(PKCE 时 `client_secret`
|
||||
- **Quick Send 浮窗(sweep 校正)**:**Wails v2 单原生窗口**,开不了第二个原生窗。v2 内只能“同窗 HTML 浮层 / 路由切换”近似;“失焦自隐”v2 无现成钩子、要自写 ObjC `NSWindowDelegate windowDidResignKey`(v2 路线最脏一块)。**这是上 v3 的最强理由**(v3 多窗口每窗一等对象)。
|
||||
- 产品化时建议热键可重绑(规避撞车)。
|
||||
|
||||
### D6 · 打包(无证书:仅 `.app`)
|
||||
- **macOS(当前路径)**:`wails build -platform darwin/universal` 产出 `build/bin/<App>.app`(`lipo` 合并 arm64 + amd64)。**不公证、不 DMG、无 Developer ID**。为满足 D3 通知与稳定身份,做 **ad-hoc 签名** `codesign -s - --force --deep <App>.app` + `Info.plist` 带 `CFBundleIdentifier`。分发靠手动(右键打开 / 去 quarantine)。
|
||||
- **entitlements(若 ad-hoc 也带)**:非沙箱只需 `com.apple.security.network.client`;purego dlopen 系统 framework **不需** `disable-library-validation`;Carbon 热键不需 entitlement。(不要列通知 / 剪贴板 / 热键 entitlement——过度声明。)
|
||||
### D6 · 打包与分发(2026-07-31 已验证)
|
||||
- **macOS(当前路径)**:`just desktop-dist-mac` 构建 Apple Silicon ARM64 Wails app,复用
|
||||
既有 Developer ID Application 证书,以 hardened runtime + secure timestamp 签名;先提交
|
||||
app 公证并 staple,再生成含 `/Applications` 链接的 DMG,对 DMG 签名、单独公证并 staple。
|
||||
app 与 DMG 均已获 Apple `Accepted`,Gatekeeper 评估为 `Notarized Developer ID`。
|
||||
- **entitlements**:当前非沙箱瘦客户端不额外声明通知、剪贴板或热键 entitlement;purego
|
||||
`dlopen` Apple 系统 framework 不需 `disable-library-validation`,Carbon 热键也不需要
|
||||
entitlement。后续引入第三方 dylib 或沙箱时重新评估。
|
||||
- **Windows**:`wails build -platform windows/amd64 [-nsis]` 产出 `.exe` / NSIS 安装器;当前**不签名**(SmartScreen 会提示),获代码签名证书后再 `signtool`。
|
||||
- **版本检查**:启动 / 定时拉版本端点,过期提示去下载,**不自更新**。
|
||||
- **将来获证书**:完整 codesign(Developer ID)+ notarytool + DMG + CI 流程见 `desktop/RESEARCH.md` D6 手册,届时直接套用。
|
||||
- **CI**:D6 后 GitHub Actions 双 runner(macos-latest / windows-latest)跑构建 + 防回归(无证书阶段仅构建 + ad-hoc,不跑公证)。
|
||||
- **操作入口**:完整构建、签名、公证、安装和验收流程见
|
||||
`docs/client-build-install.md`;底层命令与设计依据见 `desktop/RESEARCH.md` D6 手册。
|
||||
- **CI**:后续 GitHub Actions 双 runner(macos-latest / windows-latest)跑构建 + 防回归;
|
||||
macOS runner 导入 Developer ID 后跑公证,Windows 在获得 Authenticode 证书后加入签名。
|
||||
|
||||
---
|
||||
|
||||
@@ -296,7 +305,8 @@ A2 复用(地基,先让真 web UI 在桌面跑起来 + 登录走 Go 桥)
|
||||
|
||||
### 10.6 实现状态(2026-06-13 实现,本地全绿)
|
||||
|
||||
`go build ./... / go test -race ./... / web tsc / wails build` 全部通过,产出 `build/bin/cdrop-desktop.app`(内嵌真 web 应用,8 个绑定就绪)。
|
||||
`go build ./... / go test -race ./... / web tsc / wails build` 全部通过,产出
|
||||
`build/bin/Commilitia Drop.app`(内嵌真 web 应用,8 个绑定就绪)。
|
||||
|
||||
落地与对 §10.1–10.4 的关键决策:
|
||||
|
||||
@@ -389,7 +399,9 @@ Windows 侧无 cgo——darwin 的 NSStatusBar/NSPasteboard CGO 走 build tag
|
||||
|
||||
- **设备类型**:客户端发 `X-Device-Type`(桌面按 `runtime.GOOS` 注入 macos/windows),后端中间件白名单登记(替代写死 `browser`),前端 i18n 本地化展示(浏览器 / macOS 客户端 / Windows 客户端…)。
|
||||
- **设备名持久化**修复:`SaveSettings` 合并保留 `DeviceName`(设置页 `DesktopConfig` 无 device_name 字段,原先存设置会用空值覆盖)。
|
||||
- **统一软件名** `Commilitia Drop Desktop`(`.app` / `.exe`);品牌 `.icns`(macOS)/ `.ico`(Windows,无 ImageMagick 时用一次性 Go 工具封 PNG-in-ICO);bundle id `net.commilitia.cdrop`。
|
||||
- **统一软件名** `Commilitia Drop`(`.app` / `.exe`);品牌 `.icns`(macOS)/ `.ico`
|
||||
(Windows,无 ImageMagick 时用一次性 Go 工具封 PNG-in-ICO);稳定内部 bundle id
|
||||
`net.commilitia.cdrop`。
|
||||
- **Cmd+,(Ctrl+,)→ 设置**:`main.tsx` 全局 keydown,桌面与浏览器通用。
|
||||
|
||||
### 11.5 自启静默驻留(autostart → 不弹窗,直接驻留菜单栏 / 托盘)
|
||||
|
||||
+22
-12
@@ -1,19 +1,29 @@
|
||||
# README
|
||||
# Commilitia Drop(Wails)
|
||||
|
||||
## About
|
||||
这是 Windows 与 macOS 当前唯一的正式桌面客户端。两个平台都复用仓库根目录 `web/`
|
||||
的 React 前端;`frontend/src` 只保留为早期 Wails 桥接示例,不是可发布的第二套界面。
|
||||
|
||||
This is the official Wails Vanilla-TS template.
|
||||
正式构建必须从仓库根目录运行:
|
||||
|
||||
You can configure the project by editing `wails.json`. More information about the project settings can be found
|
||||
here: https://wails.io/docs/reference/project-config
|
||||
```sh
|
||||
just desktop-build-win
|
||||
just desktop-build-mac
|
||||
```
|
||||
|
||||
## Live Development
|
||||
macOS 正式分发使用既有 Developer ID Application 证书和 ASC API Key:
|
||||
|
||||
To run in live development mode, run `wails dev` in the project directory. This will run a Vite development
|
||||
server that will provide very fast hot reload of your frontend changes. If you want to develop in a browser
|
||||
and have access to your Go methods, there is also a dev server that runs on http://localhost:34115. Connect
|
||||
to this in your browser, and you can call your Go code from devtools.
|
||||
```sh
|
||||
just desktop-dist-mac
|
||||
```
|
||||
|
||||
## Building
|
||||
该任务会依次签名、公证并装订 `.app`,再生成、签名、公证和装订
|
||||
`desktop/build/bin/Commilitia Drop.dmg`。Windows 当前输出未做 Authenticode 签名。
|
||||
|
||||
To build a redistributable, production mode package, use `wails build`.
|
||||
macOS 构建仅支持 Apple Silicon(ARM64)。构建任务会先生成最新 Web 产物,再以
|
||||
`wails build -clean` 打包,避免将陈旧前端或另一平台的旧产物混入发布目录。
|
||||
|
||||
`ios/CDrop` 中的原生 macOS target 仍是迁移候选,只供本地验证;在功能对等、三语界面、
|
||||
签名扩展和真机验收全部完成前,不得代替或与本 Wails 客户端同时分发。
|
||||
|
||||
完整的全客户端构建、安装和验证步骤见
|
||||
[`../docs/client-build-install.md`](../docs/client-build-install.md)。
|
||||
|
||||
+45
-20
@@ -1,6 +1,8 @@
|
||||
# cdrop 桌面端 D1 前决策就绪简报
|
||||
# Commilitia Drop 桌面端 D1 前决策就绪简报
|
||||
|
||||
> 框架基线:Wails v2.12.0(Go 1.23 模块声明,本机 toolchain 1.26.3)+ 系统 WebView,瘦客户端,业务全走 `drop.commilitia.net`。平台优先级 macOS(Apple Silicon + Intel universal),Windows x64 兼容。
|
||||
> 框架基线:Wails v2.12.0(Go 1.23 模块声明,本机 toolchain 1.26.3)+ 系统 WebView,
|
||||
> 瘦客户端,业务全走 `drop.commilitia.net`。平台优先级 macOS(仅 Apple Silicon),
|
||||
> Windows x64 兼容。
|
||||
> 已对四项关键论断做对抗式核验:托盘能力 **refuted**、热键库可用性 **supported(附 pin 条件)**、Casdoor/R1 **mixed(需一处后端改动 + 版本前提)**、签名/公证 entitlement **mixed(核心子句成立,捆绑表述需修正)**。
|
||||
|
||||
---
|
||||
@@ -121,7 +123,7 @@
|
||||
|
||||
本节只覆盖 Wails/Go 客户端侧的实现机制。前提已确定:public client + PKCE,回调用 `http://127.0.0.1:<临时端口>/callback`,Casdoor 对 loopback 任意端口放行,后端 audience 改多值校验。Casdoor 端点:authorize=`/login/oauth/authorize`,token=`/api/login/oauth/access_token`。
|
||||
|
||||
### 1. loopback vs 自定义 scheme `cdrop://` 的取舍(Wails 语境)
|
||||
### 1. loopback vs 自定义 scheme `commilitia-drop://` 的取舍(Wails 语境)
|
||||
|
||||
两条路线都符合 RFC 8252 对原生应用的要求(§7.1 私有 scheme、§7.3 loopback),但在 Wails 下实现成本差异明显。
|
||||
|
||||
@@ -132,10 +134,15 @@
|
||||
- 跨平台零差异:macOS / Windows / Linux 代码一致,只依赖 `net/http`。
|
||||
- 必须用 IP 字面量 `127.0.0.1`(或 `[::1]`),不要用 `localhost`——RFC 8252 §8.3:用 `localhost` 可能因 DNS / hosts 解析意外监听到非回环接口。端口用 OS 分配的临时端口(`:0`),符合 RFC 8252 §7.3“服务器 MUST 允许请求时指定任意端口”。
|
||||
|
||||
**自定义 scheme(`cdrop://callback`):**
|
||||
**自定义 scheme(`commilitia-drop://callback`):**
|
||||
|
||||
- 需要 OS 级登记:macOS 在 `Info.plist` 写 `CFBundleURLTypes` → `CFBundleURLSchemes`(值 `cdrop`);Windows 要在注册表 `HKEY_CLASSES_ROOT\cdrop`(或 per-user `HKCU\Software\Classes\cdrop`)写 `URL Protocol` 键 + `shell\open\command` 指向 exe;Linux 走 `.desktop` 的 `x-scheme-handler/cdrop`。
|
||||
- 必须配单实例锁。点 `cdrop://` 时 OS 会重新拉起 app 的“第二个实例”,深链作为命令行参数传入。需要 `options.App.SingleInstanceLock` 把这个 deep link 转发给首实例,否则 token 落在一个马上要退出的临时进程里,拿不到。
|
||||
- 需要 OS 级登记:macOS 在 `Info.plist` 写 `CFBundleURLTypes` → `CFBundleURLSchemes`
|
||||
(值 `commilitia-drop`);Windows 要在注册表
|
||||
`HKEY_CLASSES_ROOT\commilitia-drop`(或 per-user
|
||||
`HKCU\Software\Classes\commilitia-drop`)写 `URL Protocol` 键 +
|
||||
`shell\open\command` 指向 exe;Linux 走
|
||||
`.desktop` 的 `x-scheme-handler/commilitia-drop`。
|
||||
- 必须配单实例锁。点 `commilitia-drop://` 时 OS 会重新拉起 app 的“第二个实例”,深链作为命令行参数传入。需要 `options.App.SingleInstanceLock` 把这个 deep link 转发给首实例,否则 token 落在一个马上要退出的临时进程里,拿不到。
|
||||
- macOS 上单实例锁与深链协作有已知坑(见 wails issue #5089:v3 的 single instance lock 与 `OpenedWithURL` 不兼容;v2 也需自己从 `Args` 解析 URL 并 `WindowUnminimise` + `Show`)。
|
||||
|
||||
scheme 路线的 `SingleInstanceLock` 形态(作为对比,**本项目不采用**):
|
||||
@@ -145,9 +152,9 @@ scheme 路线的 `SingleInstanceLock` 形态(作为对比,**本项目不采
|
||||
SingleInstanceLock: &options.SingleInstanceLock{
|
||||
UniqueId: "net.commilitia.cdrop",
|
||||
OnSecondInstanceLaunch: func(d options.SecondInstanceData) {
|
||||
// d.Args 里含被 OS 透传的 cdrop://callback?code=...&state=...
|
||||
// d.Args 里含被 OS 透传的 commilitia-drop://callback?code=...&state=...
|
||||
for _, arg := range d.Args {
|
||||
if strings.HasPrefix(arg, "cdrop://") {
|
||||
if strings.HasPrefix(arg, "commilitia-drop://") {
|
||||
runtime.WindowUnminimise(appCtx) // 回调不会自动聚焦窗口
|
||||
runtime.Show(appCtx)
|
||||
runtime.EventsEmit(appCtx, "oauth:callback", arg)
|
||||
@@ -157,7 +164,7 @@ SingleInstanceLock: &options.SingleInstanceLock{
|
||||
},
|
||||
```
|
||||
|
||||
**推荐:loopback。** 在 Wails 下它省掉了单实例锁、`Info.plist`/注册表登记、深链解析与跨平台分叉这一整套,授权流在单进程内自洽,代码量和真机调试面都小得多。`cdrop://` 的唯一优势是不占端口、回调 URL 更“原生”,但对瘦客户端不值这些成本。scheme 留作未来若需“浏览器里点 cdrop 链接唤起桌面端”的备选——那是另一个用例(深链唤起),与本次登录回调无关。
|
||||
**推荐:loopback。** 在 Wails 下它省掉了单实例锁、`Info.plist`/注册表登记、深链解析与跨平台分叉这一整套,授权流在单进程内自洽,代码量和真机调试面都小得多。`commilitia-drop://` 的唯一优势是不占端口、回调 URL 更“原生”,但对瘦客户端不值这些成本。scheme 留作未来若需“浏览器里点 Commilitia Drop 链接唤起桌面端”的备选——那是另一个用例(深链唤起),与本次登录回调无关。
|
||||
|
||||
### 2. `runtime.BrowserOpenURL` 用法与注意
|
||||
|
||||
@@ -397,13 +404,13 @@ export async function onLoginClick(): Promise<void>
|
||||
func writeClosePage(w http.ResponseWriter, ok bool)
|
||||
{
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
msg := "已登录,可关闭本页返回 cdrop。"
|
||||
msg := "已登录,可关闭本页返回 Commilitia Drop。"
|
||||
if !ok
|
||||
{
|
||||
msg = "登录未完成,可关闭本页返回 cdrop 重试。"
|
||||
msg = "登录未完成,可关闭本页返回 Commilitia Drop 重试。"
|
||||
}
|
||||
fmt.Fprintf(w, `<!doctype html><html lang="zh-Hans"><head><meta charset="utf-8">`+
|
||||
`<title>cdrop</title></head><body style="font-family:sans-serif;text-align:center;margin-top:20vh">`+
|
||||
`<title>Commilitia Drop</title></head><body style="font-family:sans-serif;text-align:center;margin-top:20vh">`+
|
||||
`<p>%s</p><script>setTimeout(function(){window.close();},800);</script>`+
|
||||
`</body></html>`, msg)
|
||||
}
|
||||
@@ -443,7 +450,8 @@ func writeClosePage(w http.ResponseWriter, ok bool)
|
||||
|
||||
## 打包·签名·公证操作手册(D6 补充)
|
||||
|
||||
本节只补**具体命令与 CI 配置**;entitlement/公证“会不会被拒”的判断已在前文完成。环境基准:Wails v2.12.0(Go),macOS universal 优先 + Windows x64,瘦客户端。
|
||||
本节只补**具体命令与 CI 配置**;entitlement/公证“会不会被拒”的判断已在前文完成。
|
||||
环境基准:Wails v2.12.0(Go),macOS arm64 + Windows x64,瘦客户端。
|
||||
|
||||
> 重要前提:Wails 官方 signing 指南至今仍推荐 `gon`,但该工具已停止维护、且依赖已废弃的 `altool` 语义。本手册一律改用 Apple 现行的 `codesign` + `notarytool` + `stapler` 链路,这是已确定的正确路径。
|
||||
|
||||
@@ -452,13 +460,15 @@ func writeClosePage(w http.ResponseWriter, ok bool)
|
||||
#### 1.1 构建产物与已知坑(已确定)
|
||||
|
||||
```bash
|
||||
wails build -platform darwin/universal -clean
|
||||
wails build -platform darwin/arm64 -clean
|
||||
```
|
||||
|
||||
- 产物:`build/bin/<AppName>.app`(universal `.app` bundle,`lipo` 已合并 arm64 + amd64 两份 Go 二进制)。`<AppName>` 来自 `wails.json` 的 `outputfilename`/项目名。
|
||||
- 产物:`build/bin/<AppName>.app`(Apple Silicon arm64)。`<AppName>` 来自
|
||||
`wails.json` 的 `outputfilename`/项目名。
|
||||
- 项目脚手架在 `build/darwin/` 下生成 `Info.plist`(模板 `Info.plist`/`Info.dev.plist`),这是 `.app` 内 `Contents/Info.plist` 的来源。改 bundle ID、版本号、`LSMinimumSystemVersion` 等都改这里。
|
||||
- 已知坑:
|
||||
- universal 构建要求**本机同时具备 arm64 与 amd64 的 CGO 工具链**。在 Apple Silicon 的 `macos-latest`(macos-14/15)runner 上原生满足;不要尝试从 Linux 交叉编译 darwin(CGO + macOS SDK 缺失,社区反复确认不可行)。
|
||||
- 当前只构建 arm64;不要尝试从 Linux 交叉编译 darwin(CGO + macOS SDK 缺失,
|
||||
社区反复确认不可行)。
|
||||
- `wails build` 本身**不签名、不公证 macOS 产物**(与 Windows 的 `-nsis` 不同,没有内建签名参数)。签名/公证完全是构建后的独立步骤。
|
||||
- Wails 没有内建 DMG 封装,需自己做(见 1.4)。
|
||||
|
||||
@@ -563,7 +573,22 @@ xcrun stapler staple "YourApp.dmg"
|
||||
|
||||
#### 1.6 Wails 对 darwin 签名的内建支持(已确定)
|
||||
|
||||
`wails build -platform darwin/universal` **无签名参数**,不做 codesign/notarize;签名、公证、DMG 全是构建后的外部步骤。`build/darwin/` 下有 `Info.plist`/`Info.dev.plist`;`entitlements.plist` 非自动生成,需手动放进该路径。universal 由 Wails 内部 `lipo` 合并,无需手动 `lipo`。
|
||||
`wails build -platform darwin/arm64` **无签名参数**,不做 codesign/notarize;签名、
|
||||
公证、DMG 全是构建后的外部步骤。`build/darwin/` 下有 `Info.plist`/`Info.dev.plist`;
|
||||
`entitlements.plist` 非自动生成,需手动放进该路径。
|
||||
|
||||
#### 1.7 本项目已验证的正式 recipe(2026-07-31)
|
||||
|
||||
仓库根目录的 `just desktop-dist-mac` 已把上述外部步骤固化:ARM64 构建 → 复用既有
|
||||
Developer ID Application 身份签名 → `ditto` ZIP 公证 → app staple → 生成 DMG → DMG 签名与
|
||||
单独公证 → DMG staple → Gatekeeper 评估。DMG 由 `desktop/scripts/make-dmg.sh` 使用纯系统工具
|
||||
创建,内容只有 `Commilitia Drop.app` 和 `/Applications` 链接,不依赖 `create-dmg`。
|
||||
|
||||
本次实测 app submission `0e2c80e1-e40d-4108-951e-64c810bd118e`、DMG submission
|
||||
`7a6aea7a-6c40-4bdb-96e3-46e6f8fbc813` 均为 `Accepted`;两者 staple/validate 成功,
|
||||
`spctl` 返回 `Notarized Developer ID`。这证明 Wails 应用本身满足 Apple 公证要求;后续发布应
|
||||
复用 recipe,不再使用 ad-hoc 签名或未公证 `.app` 作为正式分发物。面向操作者的完整流程见
|
||||
`docs/client-build-install.md`。
|
||||
|
||||
### 2. Windows
|
||||
|
||||
@@ -596,7 +621,7 @@ signtool sign /fd sha256 /tr http://ts.ssl.com /td sha256 /f certificate.pfx /p
|
||||
```yaml
|
||||
jobs:
|
||||
macos:
|
||||
runs-on: macos-latest # Apple Silicon,原生支持 universal CGO
|
||||
runs-on: macos-latest # Apple Silicon arm64
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with: { submodules: recursive } # cjk-autospace 等 submodule
|
||||
@@ -605,7 +630,7 @@ jobs:
|
||||
- uses: actions/setup-node@v4
|
||||
with: { node-version: '20' }
|
||||
- run: go install github.com/wailsapp/wails/v2/cmd/wails@v2.12.0
|
||||
- run: wails build -platform darwin/universal -clean
|
||||
- run: wails build -platform darwin/arm64 -clean
|
||||
- uses: apple-actions/import-codesign-certs@v7
|
||||
with:
|
||||
p12-file-base64: ${{ secrets.APPLE_CERT_P12_BASE64 }}
|
||||
@@ -685,7 +710,7 @@ jobs:
|
||||
- Wails Mac App Store 指南(entitlements/codesign --options=runtime、build/darwin 路径):https://wails.io/docs/guides/mac-appstore/
|
||||
- Wails NSIS installer 指南(build/windows/installer、-nsis):https://wails.io/docs/guides/windows-installer/
|
||||
- Wails NSIS 不签主 exe 的 issue #3716:https://github.com/wailsapp/wails/issues/3716
|
||||
- Wails Crossplatform build(GitHub Actions、darwin/universal 矩阵):https://wails.io/docs/guides/crossplatform-build/
|
||||
- Wails Crossplatform build(GitHub Actions):https://wails.io/docs/guides/crossplatform-build/
|
||||
- notarytool man page:https://keith.github.io/xcode-man-pages/notarytool.1.html
|
||||
- Apple TN3147 迁移到新公证工具:https://developer.apple.com/documentation/technotes/tn3147-migrating-to-the-latest-notarization-tool
|
||||
- Apple Disable Library Validation entitlement:https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_cs_disable-library-validation
|
||||
|
||||
+8
-5
@@ -62,6 +62,9 @@ func (a *App) startup(ctx context.Context) {
|
||||
if platform.IsLaunchAtLoginEnabled() {
|
||||
_ = platform.SetLaunchAtLogin(true)
|
||||
}
|
||||
// Migrate native notification metadata at startup so an old internal-name
|
||||
// entry does not remain visible until the first incoming notification.
|
||||
platform.InitializeNotifications()
|
||||
// 原生传输引擎:落地目录取当前配置(设置页改目录时经 SaveSettings 同步到引擎)。
|
||||
a.transfer = engine.New(engine.Config{DownloadDir: platform.ResolveDownloadDir()}, &transferEvents{app: a})
|
||||
a.startClipboardSync(ctx)
|
||||
@@ -70,10 +73,10 @@ func (a *App) startup(ctx context.Context) {
|
||||
platform.TriggerLocalNetwork()
|
||||
platform.InstallStatusBar(
|
||||
platform.StatusBarMenu{
|
||||
Title: "cdrop",
|
||||
Title: "Commilitia Drop",
|
||||
Show: "显示主窗口",
|
||||
Settings: "设置…",
|
||||
Quit: "退出 cdrop",
|
||||
Quit: "退出 Commilitia Drop",
|
||||
},
|
||||
// The native menu-action callbacks fire on the AppKit main thread; calling
|
||||
// Wails runtime methods synchronously there can re-enter the main run loop
|
||||
@@ -107,10 +110,10 @@ func (a *App) buildMenu() *menu.Menu {
|
||||
m := menu.NewMenu()
|
||||
|
||||
appSub := menu.NewMenu()
|
||||
appSub.Append(menu.Text("退出 cdrop", keys.CmdOrCtrl("q"), func(*menu.CallbackData) {
|
||||
appSub.Append(menu.Text("退出 Commilitia Drop", keys.CmdOrCtrl("q"), func(*menu.CallbackData) {
|
||||
a.requestQuit()
|
||||
}))
|
||||
m.Append(menu.SubMenu("cdrop", appSub)) // macOS renders the first menu as the app menu
|
||||
m.Append(menu.SubMenu("Commilitia Drop", appSub)) // macOS renders the first menu as the app menu
|
||||
|
||||
m.Append(menu.EditMenu()) // Undo/Redo/Cut/Copy/Paste/SelectAll
|
||||
|
||||
@@ -415,7 +418,7 @@ func (a *App) resolveOAuthConfig() (platform.OAuthConfig, error) {
|
||||
func oauthConfigFromEnv() platform.OAuthConfig {
|
||||
return platform.OAuthConfig{
|
||||
BrokerURL: os.Getenv("CDROP_BROKER_URL"),
|
||||
App: envOr("CDROP_BROKER_APP", "cdrop"),
|
||||
App: envOr("CDROP_BROKER_APP", "commilitia-drop"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<string>Commilitia Drop 使用本地网络发现同内网设备并建立点对点直连传输。</string>
|
||||
<key>NSBonjourServices</key>
|
||||
<array>
|
||||
<string>_cdrop._tcp</string>
|
||||
<string>_commilitia-drop._tcp</string>
|
||||
</array>
|
||||
{{if .Info.FileAssociations}}
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<string>Commilitia Drop 使用本地网络发现同内网设备并建立点对点直连传输。</string>
|
||||
<key>NSBonjourServices</key>
|
||||
<array>
|
||||
<string>_cdrop._tcp</string>
|
||||
<string>_commilitia-drop._tcp</string>
|
||||
</array>
|
||||
{{if .Info.FileAssociations}}
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
// 留给宿主(桌面 JS / iOS Swift),引擎只经回调收发不透明信令串——纯数据面。
|
||||
//
|
||||
// 线协议与 web 引擎 web/src/features/transfer/p2p.ts 逐字节一致,故 Go 端可与浏览器 /
|
||||
// iOS 的 JS 引擎互通:DataChannel "cdrop-file"(ordered),控制帧 meta/done/ack 走文本帧、
|
||||
// iOS 的 JS 引擎互通:DataChannel "commilitia-drop-file"(ordered),控制帧 meta/done/ack 走文本帧、
|
||||
// 文件分片走二进制帧;信令 payload 形如 {type, sdp?, candidate?}。
|
||||
package engine
|
||||
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
channelName = "cdrop-file"
|
||||
channelName = "commilitia-drop-file"
|
||||
chunkSize = 64 * 1024
|
||||
highWatermark = 16 * 1024 * 1024
|
||||
lowWatermark = 4 * 1024 * 1024
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>cdrop-desktop</title>
|
||||
<title>Commilitia Drop</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -8,9 +8,9 @@ import { EventsOn } from "../wailsjs/runtime/runtime";
|
||||
// Go 换 token → oauth:success / oauth:error 事件回到这里。
|
||||
document.querySelector("#app")!.innerHTML = `
|
||||
<main class="login-demo">
|
||||
<h1>cdrop 桌面端</h1>
|
||||
<h1>Commilitia Drop</h1>
|
||||
<p class="status" id="status">检查登录状态……</p>
|
||||
<button class="btn" id="loginBtn" disabled>登录 cdrop</button>
|
||||
<button class="btn" id="loginBtn" disabled>登录 Commilitia Drop</button>
|
||||
<p class="hint">登录会在系统浏览器中打开授权页,完成后自动返回。</p>
|
||||
</main>
|
||||
`;
|
||||
|
||||
+1
-1
@@ -67,7 +67,7 @@ func main() {
|
||||
|
||||
// Create application with options
|
||||
err = wails.Run(&options.App{
|
||||
Title: "cdrop",
|
||||
Title: "Commilitia Drop",
|
||||
Width: 1024,
|
||||
Height: 768,
|
||||
StartHidden: hidden,
|
||||
|
||||
@@ -68,7 +68,14 @@ func ResolveDeviceName() string {
|
||||
if h, err := os.Hostname(); err == nil && h != "" {
|
||||
return h
|
||||
}
|
||||
return "cdrop-desktop"
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
return "Commilitia Drop (macOS)"
|
||||
case "windows":
|
||||
return "Commilitia Drop (Windows)"
|
||||
default:
|
||||
return "Commilitia Drop"
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultConfig is what a fresh install gets: clipboard sync on, no autostart.
|
||||
|
||||
@@ -43,7 +43,7 @@ func FetchOAuthConfig(ctx context.Context, apiBase string) (OAuthConfig, error)
|
||||
|
||||
app := c.App
|
||||
if app == "" {
|
||||
app = "cdrop"
|
||||
app = "commilitia-drop"
|
||||
}
|
||||
cfg := OAuthConfig{BrokerURL: c.BrokerURL, App: app}
|
||||
if cfg.BrokerURL == "" {
|
||||
|
||||
@@ -17,7 +17,7 @@ func TestFetchOAuthConfig_Success(t *testing.T) {
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"auth_mode": "prod",
|
||||
"broker_url": "https://sso.example.net",
|
||||
"broker_app": "cdrop",
|
||||
"broker_app": "commilitia-drop",
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
@@ -30,13 +30,13 @@ func TestFetchOAuthConfig_Success(t *testing.T) {
|
||||
if cfg.BrokerURL != "https://sso.example.net" {
|
||||
t.Errorf("broker_url = %q", cfg.BrokerURL)
|
||||
}
|
||||
if cfg.App != "cdrop" {
|
||||
t.Errorf("app = %q, want cdrop", cfg.App)
|
||||
if cfg.App != "commilitia-drop" {
|
||||
t.Errorf("app = %q, want commilitia-drop", cfg.App)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchOAuthConfig_DefaultsApp(t *testing.T) {
|
||||
// broker_app omitted → defaults to "cdrop".
|
||||
// broker_app omitted → defaults to "commilitia-drop".
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"broker_url": "https://sso.example.net"})
|
||||
@@ -46,8 +46,8 @@ func TestFetchOAuthConfig_DefaultsApp(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("FetchOAuthConfig: %v", err)
|
||||
}
|
||||
if cfg.App != "cdrop" {
|
||||
t.Errorf("app = %q, want default cdrop", cfg.App)
|
||||
if cfg.App != "commilitia-drop" {
|
||||
t.Errorf("app = %q, want default commilitia-drop", cfg.App)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
)
|
||||
|
||||
// 代铸 (proxy-mint) on the desktop. The device-authorization flow yields a bootstrap
|
||||
// machine token (scope app:cdrop, no meta) that proves the user's identity but is not a
|
||||
// machine token (scope app:commilitia-drop, no meta) that proves the user's identity but is not a
|
||||
// cdrop-managed device. This call exchanges it for a managed device session bound to this
|
||||
// device's stable device_id, so the desktop joins cdrop's unified device list and is
|
||||
// managed exactly like a browser — the same model, not a separate native-only track.
|
||||
|
||||
@@ -46,7 +46,7 @@ func writeInto(dir, name string, data []byte) (string, error) {
|
||||
}
|
||||
safe := sanitizeFileName(name)
|
||||
if safe == "" {
|
||||
safe = "cdrop-download"
|
||||
safe = "Commilitia Drop Download"
|
||||
}
|
||||
target := uniquePath(dir, safe)
|
||||
if err := os.WriteFile(target, data, 0o644); err != nil {
|
||||
@@ -248,7 +248,7 @@ func FinalizeStreamingDownload(sessionId, name string) (string, error) {
|
||||
}
|
||||
safe := sanitizeFileName(name)
|
||||
if safe == "" {
|
||||
safe = "cdrop-download"
|
||||
safe = "Commilitia Drop Download"
|
||||
}
|
||||
target := uniquePath(d.dir, safe)
|
||||
if err := os.Rename(d.tmp, target); err != nil {
|
||||
|
||||
@@ -69,7 +69,7 @@ func HealIdentity(s *LoginResult, apiBase string) *LoginResult {
|
||||
}
|
||||
if changed {
|
||||
if err := SaveSession(*s); err != nil {
|
||||
slog.Warn("cdrop: heal identity save failed", "err", err)
|
||||
slog.Warn("Commilitia Drop: heal identity save failed", "err", err)
|
||||
}
|
||||
}
|
||||
return s
|
||||
|
||||
@@ -10,8 +10,8 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// launchAgentLabel is the reverse-DNS label + plist filename for the per-user
|
||||
// LaunchAgent that starts cdrop at login.
|
||||
// launchAgentLabel is a stable internal identifier. The product name shown to
|
||||
// users comes from the application metadata, not this LaunchAgent label.
|
||||
const launchAgentLabel = "net.commilitia.cdrop"
|
||||
|
||||
func launchAgentPath() (string, error) {
|
||||
@@ -71,7 +71,40 @@ func SetLaunchAtLogin(enabled bool) error {
|
||||
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(p, []byte(buildLaunchAgentPlist(launchAgentLabel, args)), 0o644)
|
||||
if err := writeLaunchAgentAtomically(
|
||||
p,
|
||||
[]byte(buildLaunchAgentPlist(launchAgentLabel, args)),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeLaunchAgentAtomically(path string, contents []byte) error {
|
||||
dir := filepath.Dir(path)
|
||||
f, err := os.CreateTemp(dir, "."+filepath.Base(path)+".tmp-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tempPath := f.Name()
|
||||
defer os.Remove(tempPath)
|
||||
|
||||
if err := f.Chmod(0o644); err != nil {
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
if _, err := f.Write(contents); err != nil {
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
if err := f.Sync(); err != nil {
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(tempPath, path)
|
||||
}
|
||||
|
||||
// IsLaunchAtLoginEnabled reports whether the LaunchAgent plist is present.
|
||||
|
||||
@@ -48,8 +48,8 @@ func TestSetLaunchAtLoginInstallsAndRemoves(t *testing.T) {
|
||||
|
||||
func TestAppBundlePath(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"/Apps/cdrop.app/Contents/MacOS/desktop": "/Apps/cdrop.app",
|
||||
"/usr/local/bin/desktop": "",
|
||||
"/Apps/Commilitia Drop.app/Contents/MacOS/Commilitia Drop": "/Apps/Commilitia Drop.app",
|
||||
"/usr/local/bin/Commilitia Drop": "",
|
||||
}
|
||||
for in, want := range cases {
|
||||
if got := appBundlePath(in); got != want {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#import <Network/Network.h>
|
||||
|
||||
// cdropTriggerLocalNetwork:起一个对 _cdrop._tcp 的 Bonjour 浏览,触发 macOS 本地网络权限弹窗。
|
||||
// cdropTriggerLocalNetwork:起一个对 _commilitia-drop._tcp 的 Bonjour 浏览,触发 macOS 本地网络权限弹窗。
|
||||
// WKWebView 自身不会触发该权限请求,须由宿主 App 主动发起一次本地网络访问;授权后本进程内的
|
||||
// WebRTC 才能收集 host / mDNS 候选实现同内网直连(见 localnetwork_darwin.go 注释)。浏览结果本身
|
||||
// 不关心——「发起访问」这一动作即触发授权。保活单个 browser(静态全局,ARC 下持有),幂等。
|
||||
@@ -11,7 +11,7 @@ void cdropTriggerLocalNetwork(void) {
|
||||
return;
|
||||
}
|
||||
nw_browse_descriptor_t descriptor =
|
||||
nw_browse_descriptor_create_bonjour_service("_cdrop._tcp", NULL);
|
||||
nw_browse_descriptor_create_bonjour_service("_commilitia-drop._tcp", NULL);
|
||||
nw_parameters_t parameters = nw_parameters_create();
|
||||
nw_parameters_set_include_peer_to_peer(parameters, true);
|
||||
|
||||
|
||||
@@ -13,6 +13,10 @@ import "C"
|
||||
|
||||
import "unsafe"
|
||||
|
||||
// InitializeNotifications is a no-op on macOS; the notification center is
|
||||
// initialized lazily by the native framework.
|
||||
func InitializeNotifications() {}
|
||||
|
||||
// Notify shows a native system notification.
|
||||
//
|
||||
// macOS uses UNUserNotificationCenter, which REQUIRES the app bundle to be
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
package platform
|
||||
|
||||
// InitializeNotifications is a no-op on unsupported desktop platforms.
|
||||
func InitializeNotifications() {}
|
||||
|
||||
// Notify is a no-op on platforms without a native notification backend wired up.
|
||||
// The desktop client targets macOS and Windows; this keeps the package building
|
||||
// on other GOOS (e.g. a Linux `go vet` / CI pass).
|
||||
|
||||
@@ -4,14 +4,18 @@ package platform
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
toast "git.sr.ht/~jackmordaunt/go-toast/v2"
|
||||
"golang.org/x/sys/windows/registry"
|
||||
)
|
||||
|
||||
const (
|
||||
// toastAppID 是 Windows Action Center 里显示的应用标识,与 macOS bundle id 对齐。
|
||||
toastAppID = "net.commilitia.cdrop"
|
||||
// toastAppID 是稳定的内部 AppUserModelID;Windows 通知设置中的用户可见名称由
|
||||
// toastDisplayName 单独写入 DisplayName,避免为了改显示名破坏系统身份。
|
||||
toastAppID = "net.commilitia.cdrop"
|
||||
toastDisplayName = "Commilitia Drop"
|
||||
// toastGUID 固定不变——它把通知归属到注册表里的本应用条目;更换会让既有通知
|
||||
// 失去归属。
|
||||
toastGUID = "{c4d8e2a1-6b3f-4e7a-9c2d-1f5b8a0e3d6c}"
|
||||
@@ -19,20 +23,41 @@ const (
|
||||
|
||||
var toastInit sync.Once
|
||||
|
||||
// Notify shows a native Windows toast. go-toast renders via the WinRT/COM path
|
||||
// (PowerShell fallback when the AppID isn't registered), so it works without code
|
||||
// signing — SmartScreen only gates the installer, not notifications. Best-effort:
|
||||
// errors are swallowed. SetAppData registers the app identity once so the toast
|
||||
// shows "cdrop" rather than the PowerShell host.
|
||||
func Notify(title, body string) {
|
||||
// InitializeNotifications registers the stable Windows notification identity
|
||||
// and corrects its user-visible name. It is safe to call repeatedly.
|
||||
func InitializeNotifications() {
|
||||
toastInit.Do(func() {
|
||||
data := toast.AppData{AppID: toastAppID, GUID: toastGUID}
|
||||
if exe, err := os.Executable(); err == nil {
|
||||
data.ActivationExe = exe
|
||||
}
|
||||
_ = toast.SetAppData(data)
|
||||
})
|
||||
|
||||
// go-toast v2.0.3 没有独立 DisplayName 字段,会把 AppID 写进
|
||||
// DisplayName,且已有值时不更新;在同一稳定键上显式覆盖显示名。
|
||||
appKey := filepath.Join(
|
||||
"SOFTWARE",
|
||||
"Classes",
|
||||
"AppUserModelId",
|
||||
toastAppID,
|
||||
)
|
||||
if key, err := registry.OpenKey(
|
||||
registry.CURRENT_USER,
|
||||
appKey,
|
||||
registry.SET_VALUE,
|
||||
); err == nil {
|
||||
_ = key.SetStringValue("DisplayName", toastDisplayName)
|
||||
_ = key.Close()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Notify shows a native Windows toast. go-toast renders via the WinRT/COM path
|
||||
// (PowerShell fallback when the AppID isn't registered), so it works without code
|
||||
// signing — SmartScreen only gates the installer, not notifications. Best-effort:
|
||||
// errors are swallowed.
|
||||
func Notify(title, body string) {
|
||||
InitializeNotifications()
|
||||
n := toast.Notification{
|
||||
AppID: toastAppID,
|
||||
Title: title,
|
||||
|
||||
@@ -26,7 +26,7 @@ import (
|
||||
|
||||
// OAuthConfig carries the Auth Broker coordinates. BrokerURL is the broker's PUBLIC
|
||||
// origin (e.g. https://sso.commilitia.net); App is this app's key in the broker apps
|
||||
// registry ("cdrop"). The app's loopback redirect must be registered in the broker's
|
||||
// registry ("commilitia-drop"). The app's loopback redirect must be registered in the broker's
|
||||
// apps.json (redirect_uris) for the device flow to accept it.
|
||||
type OAuthConfig struct {
|
||||
BrokerURL string
|
||||
@@ -138,7 +138,7 @@ func (f *Flow) Login(ctx context.Context) (*TokenResult, error) {
|
||||
"state": {state},
|
||||
"code_challenge": {challenge},
|
||||
"code_challenge_method": {"S256"},
|
||||
"description": {"cdrop 桌面客户端"},
|
||||
"description": {"Commilitia Drop"},
|
||||
}.Encode()
|
||||
f.openURL(authURL)
|
||||
|
||||
@@ -255,11 +255,11 @@ func randString(n int) (string, error) {
|
||||
// the real affordance; the close attempt is best-effort.
|
||||
func writeClosePage(w http.ResponseWriter, ok bool) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
msg := "已登录,可关闭本页返回 cdrop。"
|
||||
msg := "已登录,可关闭本页返回 Commilitia Drop。"
|
||||
if !ok {
|
||||
msg = "登录未完成,可关闭本页返回 cdrop 重试。"
|
||||
msg = "登录未完成,可关闭本页返回 Commilitia Drop 重试。"
|
||||
}
|
||||
fmt.Fprintf(w, `<!doctype html><html lang="zh-Hans"><head><meta charset="utf-8"><title>cdrop</title></head>`+
|
||||
fmt.Fprintf(w, `<!doctype html><html lang="zh-Hans"><head><meta charset="utf-8"><title>Commilitia Drop</title></head>`+
|
||||
`<body style="font-family:system-ui,sans-serif;text-align:center;margin-top:20vh">`+
|
||||
`<p>%s</p><script>setTimeout(function(){window.close();},800);</script></body></html>`, msg)
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ func TestLogin_Success(t *testing.T) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"id": "sid-1",
|
||||
"app": "cdrop",
|
||||
"app": "commilitia-drop",
|
||||
"access": "at-123",
|
||||
"refresh": "rtk-456",
|
||||
"access_expires": time.Now().Add(15 * time.Minute).Unix(),
|
||||
@@ -63,7 +63,7 @@ func TestLogin_Success(t *testing.T) {
|
||||
}))
|
||||
defer brokerSrv.Close()
|
||||
|
||||
cfg := OAuthConfig{BrokerURL: brokerSrv.URL, App: "cdrop"}
|
||||
cfg := OAuthConfig{BrokerURL: brokerSrv.URL, App: "commilitia-drop"}
|
||||
|
||||
// Fake browser: parse the /device/authorize URL, assert it carries app + PKCE,
|
||||
// then GET the loopback redirect with a code + the same state (approved).
|
||||
@@ -80,7 +80,7 @@ func TestLogin_Success(t *testing.T) {
|
||||
if q.Get("code_challenge") == "" || q.Get("code_challenge_method") != "S256" {
|
||||
t.Errorf("authorize request missing PKCE challenge: %v", q)
|
||||
}
|
||||
if q.Get("app") != "cdrop" {
|
||||
if q.Get("app") != "commilitia-drop" {
|
||||
t.Errorf("authorize app = %q", q.Get("app"))
|
||||
}
|
||||
cb := q.Get("redirect_uri") + "?code=auth-code-xyz&state=" + url.QueryEscape(q.Get("state"))
|
||||
@@ -119,7 +119,7 @@ func TestLogin_Success(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLogin_StateMismatch(t *testing.T) {
|
||||
cfg := OAuthConfig{BrokerURL: "https://sso.example.net", App: "cdrop"}
|
||||
cfg := OAuthConfig{BrokerURL: "https://sso.example.net", App: "commilitia-drop"}
|
||||
openURL := func(authURL string) {
|
||||
u, _ := url.Parse(authURL)
|
||||
redirect := u.Query().Get("redirect_uri")
|
||||
@@ -157,7 +157,7 @@ func TestRefresh_Success(t *testing.T) {
|
||||
}))
|
||||
defer brokerSrv.Close()
|
||||
|
||||
cfg := OAuthConfig{BrokerURL: brokerSrv.URL, App: "cdrop"}
|
||||
cfg := OAuthConfig{BrokerURL: brokerSrv.URL, App: "commilitia-drop"}
|
||||
tok, err := NewFlow(cfg, func(string) {}).Refresh(context.Background(), "old-rtk")
|
||||
if err != nil {
|
||||
t.Fatalf("Refresh: %v", err)
|
||||
@@ -174,7 +174,7 @@ func TestRefresh_Success(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRefresh_EmptyToken(t *testing.T) {
|
||||
cfg := OAuthConfig{BrokerURL: "https://sso.example.net", App: "cdrop"}
|
||||
cfg := OAuthConfig{BrokerURL: "https://sso.example.net", App: "commilitia-drop"}
|
||||
if _, err := NewFlow(cfg, func(string) {}).Refresh(context.Background(), ""); err == nil {
|
||||
t.Fatal("want error for empty refresh token")
|
||||
}
|
||||
|
||||
@@ -177,7 +177,11 @@ func SaveSession(res LoginResult) error {
|
||||
if enc, err := encryptToken(res.RefreshToken); err == nil {
|
||||
rec.RefreshTokenEnc = enc
|
||||
} else {
|
||||
slog.Warn("cdrop: refresh_token kept in session file; OS secret store unavailable", "err", err)
|
||||
slog.Warn(
|
||||
"Commilitia Drop: refresh_token kept in session file; OS secret store unavailable",
|
||||
"err",
|
||||
err,
|
||||
)
|
||||
rec.RefreshToken = res.RefreshToken
|
||||
}
|
||||
}
|
||||
@@ -226,7 +230,7 @@ func LoadSession() (*LoginResult, error) {
|
||||
// still unavailable, leaving the file as-is).
|
||||
res.RefreshToken = rec.RefreshToken
|
||||
if err := SaveSession(*res); err != nil {
|
||||
slog.Warn("cdrop: refresh_token migration to secret store failed", "err", err)
|
||||
slog.Warn("Commilitia Drop: refresh_token migration to secret store failed", "err", err)
|
||||
}
|
||||
case rec.RefreshTokenEnc != "":
|
||||
if rt, err := decryptToken(rec.RefreshTokenEnc); err == nil {
|
||||
@@ -234,7 +238,7 @@ func LoadSession() (*LoginResult, error) {
|
||||
} else {
|
||||
// Key gone / ciphertext corrupt: drop to an access-token-only session;
|
||||
// the app will require a fresh login once the access_token lapses.
|
||||
slog.Warn("cdrop: decrypt refresh_token failed; re-login will be required", "err", err)
|
||||
slog.Warn("Commilitia Drop: decrypt refresh_token failed; re-login will be required", "err", err)
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
@@ -252,7 +256,7 @@ func ClearSession() error {
|
||||
}
|
||||
if err := keyring.Delete(keyringService, keyringKeyAccount); err != nil &&
|
||||
!errors.Is(err, keyring.ErrNotFound) {
|
||||
slog.Warn("cdrop: clear session key from secret store failed", "err", err)
|
||||
slog.Warn("Commilitia Drop: clear session key from secret store failed", "err", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ package platform
|
||||
|
||||
/*
|
||||
#cgo darwin CFLAGS: -x objective-c -fobjc-arc
|
||||
#cgo darwin LDFLAGS: -framework Cocoa
|
||||
#cgo darwin LDFLAGS: -framework Cocoa -framework UniformTypeIdentifiers
|
||||
#include <stdlib.h>
|
||||
|
||||
void cdropStatusBarInstall(const void *iconPNG, int iconLen, const char *title,
|
||||
|
||||
Executable
+33
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -ne 2 ]]
|
||||
then
|
||||
echo "用法:$0 <Commilitia Drop.app> <Commilitia Drop.dmg>" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
appPath="$1"
|
||||
outputPath="$2"
|
||||
|
||||
if [[ ! -d "$appPath" ]]
|
||||
then
|
||||
echo "找不到应用:$appPath" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
stagingDirectory="$(mktemp -d /tmp/commilitia-drop-dmg.XXXXXX)"
|
||||
trap 'rm -rf "$stagingDirectory"' EXIT
|
||||
|
||||
ditto "$appPath" "$stagingDirectory/Commilitia Drop.app"
|
||||
ln -s /Applications "$stagingDirectory/Applications"
|
||||
mkdir -p "$(dirname "$outputPath")"
|
||||
rm -f "$outputPath"
|
||||
|
||||
hdiutil create \
|
||||
-volname "Commilitia Drop" \
|
||||
-srcfolder "$stagingDirectory" \
|
||||
-ov \
|
||||
-format UDZO \
|
||||
"$outputPath"
|
||||
+3
-3
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"$schema": "https://wails.io/schemas/config.v2.json",
|
||||
"name": "Commilitia Drop Desktop",
|
||||
"outputfilename": "Commilitia Drop Desktop",
|
||||
"name": "Commilitia Drop",
|
||||
"outputfilename": "Commilitia Drop",
|
||||
"frontend:install": "cd ../../web && npm install",
|
||||
"frontend:build": "cd ../../web && npm run build -- --outDir ../desktop/frontend/dist --emptyOutDir",
|
||||
"frontend:dev:watcher": "cd ../../web && npm run dev",
|
||||
"frontend:dev:serverUrl": "auto",
|
||||
"info": {
|
||||
"productName": "Commilitia Drop Desktop"
|
||||
"productName": "Commilitia Drop"
|
||||
},
|
||||
"author": {
|
||||
"name": "commilitia",
|
||||
|
||||
Reference in New Issue
Block a user