diff --git a/PRODUCT_CAPABILITIES.zh-CN.md b/PRODUCT_CAPABILITIES.zh-CN.md index 9535e22..f0b80cf 100644 --- a/PRODUCT_CAPABILITIES.zh-CN.md +++ b/PRODUCT_CAPABILITIES.zh-CN.md @@ -28,10 +28,10 @@ | 能力域 | 能力 | 用户结果 | 当前状态 | |---|---|---|---| -| 分享入口 | 在微信转发菜单增加十二个入口 | 无需打开微信流主窗口即可发起场景 | 稳定 | +| 分享入口 | 在微信转发菜单增加十三个入口 | 无需打开微信流主窗口即可发起场景 | 稳定 | | 分批收集 | 连续接收多批原始 ZIP,最后统一选择目标和场景 | 浮条首尾参考、记录页恢复、冻结交付和失败重试 | 开发版已实现,微信宿主分享链路待实测 | | 文件接入 | 接收微信导出的 ZIP 和文件型附件 | 原始文件和文件名进入本地批次 | 稳定 | -| 目标转发 | 发给 Codex、Claude、豆包、千问办公、WorkBuddy、WeSight 或自定义应用 | 激活目标 App 并自动粘贴 | 稳定,依赖辅助功能权限 | +| 目标转发 | 发给 Codex、Claude、豆包、千问办公、WorkBuddy、WeSight、DeepSeek Harness、Qoder CN 或自定义应用 | 激活目标 App 并自动粘贴 | 稳定,依赖辅助功能权限 | | Obsidian | 生成 Markdown 笔记并保存原始归档 | 聊天记录进入本地知识库 | 稳定 | | 剪贴板 | 文件写入剪贴板 | 用户自行在目标位置粘贴 | 稳定 | | 记录 | 历史批次、状态、重新发送和清理 | 可以确认文件是否送达 | 稳定 | @@ -178,7 +178,7 @@ ### 8.2 网络 - 除软件更新外不联网。 -- 十二个 Share Extension 均在沙盒中运行。 +- 十三个 Share Extension 均在沙盒中运行。 - 扩展没有网络权限。 ### 8.3 系统权限 @@ -219,7 +219,7 @@ | 能力 | 主要实现 | |---|---| -| 十二个分享入口 | `macos/Scripts/share-slots.sh` | +| 十三个分享入口 | `macos/Scripts/share-slots.sh` | | Share Extension 接入 | `macos/Sources/WeChatBridgeShare/ShareViewController.swift` | | 附件复制与命名 | `macos/Sources/WeChatBridgeShare/AttachmentImporter.swift` | | 目标应用定义 | `macos/Sources/WeChatBridgeCore/ShareAction.swift` | diff --git a/README.md b/README.md index 857093c..304f107 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ flowchart LR | 能力 | 使用体验 | | --- | --- | -| 十二个原生入口 | 在微信转发菜单直接选择目标,无需打开微信流主窗口 | +| 十三个原生入口 | 在微信转发菜单直接选择目标,无需打开微信流主窗口 | | AI Agent 转发 | 激活目标 App,附加场景指令并自动粘贴聊天归档 | | Obsidian 沉淀 | 生成 Markdown 笔记,保存原始 ZIP,并按聊天名组织内容 | | 自定义目标 | 添加任意 macOS 应用,终端类应用可只接收文件路径 | @@ -59,6 +59,7 @@ flowchart LR | 发给 WorkBuddy | 激活 WorkBuddy 并粘贴聊天归档 | | 发给 WeSight | 激活 WeSight 并粘贴聊天归档 | | 发给 DeepSeek Harness | 激活 DeepSeek Harness 并粘贴聊天归档 | +| 发给 Qoder CN | 激活 Qoder CN 并粘贴聊天归档 | | 沉淀到 Obsidian | 创建 Markdown 笔记并保存原始附件,通知可打开笔记 | | 沉淀到文件夹 | 写入选定目录中的 Markdown 与附件,通知可在访达中定位 | | 分批收集到微信流 | 连续分享多批原始 ZIP,收齐后一次选择目标和场景 | @@ -85,7 +86,7 @@ flowchart LR 场景管理页 - 十二个入口随时开关,未安装的应用直接标注 + 十三个入口随时开关,未安装的应用直接标注 场景保存提示词与适用 Agent,转发时挑一个 @@ -179,7 +180,7 @@ shared/ # 两端共用的资产与契约 site/ # Sparkle 更新源与版本说明 ``` -项目使用 Swift Package Manager 管理 macOS 源码和 Sparkle 依赖。`macos/Scripts/make-app.sh` 会把主程序与十二个 Share Extension 组装成完整的 `.app`。Windows 侧见 `windows/AGENTS.md` 与 `windows/docs/README.zh-CN.md`。 +项目使用 Swift Package Manager 管理 macOS 源码和 Sparkle 依赖。`macos/Scripts/make-app.sh` 会把主程序与十三个 Share Extension 组装成完整的 `.app`。Windows 侧见 `windows/AGENTS.md` 与 `windows/docs/README.zh-CN.md`。 ## 开发与验证 diff --git a/README_EN.md b/README_EN.md index 7854dba..a0aace6 100644 --- a/README_EN.md +++ b/README_EN.md @@ -39,7 +39,7 @@ flowchart LR | Capability | Experience | | --- | --- | -| Twelve native entries | Pick a destination in WeChat without opening the main app | +| Thirteen native entries | Pick a destination in WeChat without opening the main app | | AI agent hand-off | Activate the target app, attach a scene prompt, and paste the archive | | Obsidian archiving | Create a Markdown note, retain the original ZIP, and organize it by chat name | | Custom destinations | Add any macOS app; terminal-style apps can receive file paths only | @@ -59,6 +59,7 @@ flowchart LR | Send to WorkBuddy | Activate WorkBuddy and paste the conversation archive | | Send to WeSight | Activate WeSight and paste the conversation archive | | Send to DeepSeek Harness | Activate DeepSeek Harness and paste the conversation archive | +| Send to Qoder CN | Activate Qoder CN and paste the conversation archive | | Save to Obsidian | Create a Markdown note and preserve the original attachment | | Save to folder | Write Markdown and attachments to the selected folder; reveal files from the notification | | Collect in WeChatBridge | Receive multiple original ZIPs, then choose a target and scene once | @@ -83,7 +84,7 @@ Pick a destination straight from WeChat's “Forward to other apps” menu, with Scene management pane - Toggle any of the twelve entries; missing apps are flagged + Toggle any of the thirteen entries; missing apps are flagged Scenes store prompts and their agents, chosen at forward time @@ -156,7 +157,7 @@ shared/ # Agent logos, design assets, screenshots, and skills site/ # Sparkle update feed and release notes ``` -Swift Package Manager owns the source layout and the Sparkle dependency. `macos/Scripts/make-app.sh` assembles the host executable and twelve Share Extensions into a complete `.app` bundle. +Swift Package Manager owns the source layout and the Sparkle dependency. `macos/Scripts/make-app.sh` assembles the host executable and thirteen Share Extensions into a complete `.app` bundle. ## Development checks diff --git a/USAGE.zh-CN.md b/USAGE.zh-CN.md index dc7219a..1f5d014 100644 --- a/USAGE.zh-CN.md +++ b/USAGE.zh-CN.md @@ -19,7 +19,7 @@ ## 转发入口 -十二个入口覆盖主流 Agent、笔记软件和剪贴板,随时可以用开关收窄或放开。 +十三个入口覆盖主流 Agent、笔记软件和剪贴板,随时可以用开关收窄或放开。 ![入口设置页](shared/Screenshots/usage-entries.png) @@ -29,7 +29,7 @@ ![支持的 Agent](shared/Screenshots/usage-supported-agents.png) -- 发给 Codex / Claude / 豆包 / 千问办公 / WorkBuddy / WeSight:激活目标应用并粘贴聊天归档。 +- 发给 Codex / Claude / 豆包 / 千问办公 / WorkBuddy / WeSight / DeepSeek Harness / Qoder CN:激活目标应用并粘贴聊天归档。 - 沉淀到 Obsidian:生成 Markdown 笔记,同时保存微信导出的原始压缩包;通过通知打开笔记。 - 沉淀到文件夹:将 Markdown、原始 ZIP 和附件保存到选定目录,默认位置为下载目录下的「微信流」文件夹。 - 复制到剪贴板:只写文件,不自动粘贴,由你自己决定贴到哪里。 diff --git a/USAGE_EN.md b/USAGE_EN.md index c75cac7..d776ae5 100644 --- a/USAGE_EN.md +++ b/USAGE_EN.md @@ -19,7 +19,7 @@ These entries come from WeChatBridge and sit in the same menu as AirDrop, Messag ## Destinations -Twelve entries cover mainstream agents, note-taking apps, and the clipboard. You can narrow or widen the list at any time. +Thirteen entries cover mainstream agents, note-taking apps, and the clipboard. You can narrow or widen the list at any time. ![Entries settings](shared/Screenshots/usage-entries.png) @@ -29,7 +29,7 @@ Supported agents: ![Supported agents](shared/Screenshots/usage-supported-agents.png) -- Send to Codex / Claude / Doubao / QwenWork / WorkBuddy / WeSight: activate the target app and paste the archive. +- Send to Codex / Claude / Doubao / QwenWork / WorkBuddy / WeSight / DeepSeek Harness / Qoder CN: activate the target app and paste the archive. - Save to Obsidian: create a Markdown note and keep the original WeChat export. - Copy to Clipboard: write the files without pasting, so you decide where they land. - Send to Custom: add any macOS app; terminal-style apps can receive file paths only. diff --git a/macos/Resources/Localizations/en.lproj/Localizable.strings b/macos/Resources/Localizations/en.lproj/Localizable.strings index 02b1b15..21e47a6 100644 --- a/macos/Resources/Localizations/en.lproj/Localizable.strings +++ b/macos/Resources/Localizations/en.lproj/Localizable.strings @@ -32,6 +32,7 @@ "发给豆包" = "Send to Doubao"; "发给 WeSight" = "Send to WeSight"; "发给 DeepSeek Harness" = "Send to DeepSeek Harness"; +"发给 Qoder CN" = "Send to Qoder CN"; "沉淀到 Obsidian" = "Save to Obsidian"; "沉淀到文件夹" = "Save to Folder"; "添加应用…" = "Add an App…"; @@ -131,6 +132,7 @@ "激活豆包并直接粘贴到输入框。" = "Brings Doubao forward and pastes straight into its input."; "激活 WeSight 并直接粘贴到输入框。" = "Brings WeSight forward and pastes straight into its input."; "激活 DeepSeek Harness 并直接粘贴到输入框。" = "Brings DeepSeek Harness forward and pastes straight into its input."; +"激活 Qoder CN 并直接粘贴到输入框。" = "Brings Qoder CN forward and pastes straight into its input."; "把聊天记录转成 Markdown,写入选定的 Obsidian 知识库。" = "Turns the chat archive into Markdown and writes it into the selected Obsidian vault."; "把聊天记录转成 Markdown 和附件,写入选定的文件夹。" = "Turns the chat into Markdown and attachments in the folder you choose."; "只放进剪贴板,去哪儿按 ⌘V 由你决定。" = "Puts them on the clipboard and leaves the ⌘V to you."; diff --git a/macos/Resources/Localizations/zh-Hans.lproj/Localizable.strings b/macos/Resources/Localizations/zh-Hans.lproj/Localizable.strings index c05c775..81e3356 100644 --- a/macos/Resources/Localizations/zh-Hans.lproj/Localizable.strings +++ b/macos/Resources/Localizations/zh-Hans.lproj/Localizable.strings @@ -32,6 +32,7 @@ "发给豆包" = "发给豆包"; "发给 WeSight" = "发给 WeSight"; "发给 DeepSeek Harness" = "发给 DeepSeek Harness"; +"发给 Qoder CN" = "发给 Qoder CN"; "沉淀到 Obsidian" = "沉淀到 Obsidian"; "沉淀到文件夹" = "沉淀到文件夹"; "添加应用…" = "添加应用…"; @@ -131,6 +132,7 @@ "激活豆包并直接粘贴到输入框。" = "激活豆包并直接粘贴到输入框。"; "激活 WeSight 并直接粘贴到输入框。" = "激活 WeSight 并直接粘贴到输入框。"; "激活 DeepSeek Harness 并直接粘贴到输入框。" = "激活 DeepSeek Harness 并直接粘贴到输入框。"; +"激活 Qoder CN 并直接粘贴到输入框。" = "激活 Qoder CN 并直接粘贴到输入框。"; "把聊天记录转成 Markdown,写入选定的 Obsidian 知识库。" = "把聊天记录转成 Markdown,写入选定的 Obsidian 知识库。"; "把聊天记录转成 Markdown 和附件,写入选定的文件夹。" = "把聊天记录转成 Markdown 和附件,写入选定的文件夹。"; "只放进剪贴板,去哪儿按 ⌘V 由你决定。" = "只放进剪贴板,去哪儿按 ⌘V 由你决定。"; diff --git a/macos/Resources/ShareLocalizations/QoderCN/en.lproj/InfoPlist.strings b/macos/Resources/ShareLocalizations/QoderCN/en.lproj/InfoPlist.strings new file mode 100644 index 0000000..424ab0f --- /dev/null +++ b/macos/Resources/ShareLocalizations/QoderCN/en.lproj/InfoPlist.strings @@ -0,0 +1 @@ +"CFBundleDisplayName" = "Send to Qoder CN"; diff --git a/macos/Resources/ShareLocalizations/QoderCN/zh-Hans.lproj/InfoPlist.strings b/macos/Resources/ShareLocalizations/QoderCN/zh-Hans.lproj/InfoPlist.strings new file mode 100644 index 0000000..b6c0ab9 --- /dev/null +++ b/macos/Resources/ShareLocalizations/QoderCN/zh-Hans.lproj/InfoPlist.strings @@ -0,0 +1 @@ +"CFBundleDisplayName" = "发给 Qoder CN"; diff --git a/macos/Scripts/check-release-config.sh b/macos/Scripts/check-release-config.sh index 9bd49e6..62d32f4 100755 --- a/macos/Scripts/check-release-config.sh +++ b/macos/Scripts/check-release-config.sh @@ -23,8 +23,8 @@ plutil -lint "$APP_INFO" "$SHARE_INFO" "$APP_ENTITLEMENTS" "$SHARE_ENTITLEMENTS" plutil -lint "$ROOT"/Resources/Localizations/*/*.strings "$ROOT"/Resources/ShareLocalizations/*/*/*.strings >/dev/null python3 -m json.tool "$SHARED/Skills/catalog.json" >/dev/null -if [ "${#SHARE_SLOTS[@]}" -ne 12 ]; then - echo "expected 12 Share-menu entries, found ${#SHARE_SLOTS[@]}" >&2 +if [ "${#SHARE_SLOTS[@]}" -ne 13 ]; then + echo "expected 13 Share-menu entries, found ${#SHARE_SLOTS[@]}" >&2 exit 1 fi diff --git a/macos/Scripts/share-slots.sh b/macos/Scripts/share-slots.sh index ead305f..35c5381 100644 --- a/macos/Scripts/share-slots.sh +++ b/macos/Scripts/share-slots.sh @@ -6,7 +6,7 @@ # same pkd election System Settings → General → Login Items & Extensions → # Sharing does. # -# All twelve run the same executable and tell themselves apart by DKShareAction, +# All of them run the same executable and tell themselves apart by DKShareAction, # so adding an entry costs a row here and a pair of InfoPlist.strings — not a # second copy of the import code. # @@ -25,6 +25,7 @@ SHARE_SLOTS=( "WorkBuddy|WeChatBridgeShareWorkBuddy|ShareWorkBuddy|workBuddy|发给 WorkBuddy" "WeSight|WeChatBridgeShareWeSight|ShareWeSight|weSight|发给 WeSight" "DeepSeekHarness|WeChatBridgeShareDeepSeekHarness|ShareDeepSeekHarness|deepSeekHarness|发给 DeepSeek Harness" + "QoderCN|WeChatBridgeShareQoderCN|ShareQoderCN|qoderCN|发给 Qoder CN" "Obsidian|WeChatBridgeShareObsidian|ShareObsidian|obsidian|沉淀到 Obsidian" "Collection|WeChatBridgeShareCollection|ShareCollection|collect|分批收集到微信流" "Folder|WeChatBridgeShareFolder|ShareFolder|folder|沉淀到文件夹" @@ -42,6 +43,7 @@ share_logo_name() { QwenWork) printf '%s' "02-qwen.png" ;; WorkBuddy) printf '%s' "06-workbuddy.png" ;; WeSight) printf '%s' "07-wesight.png" ;; + QoderCN) printf '%s' "08-qodercn.png" ;; Obsidian) printf '%s' "05-obsidian.png" ;; esac } diff --git a/macos/Sources/WeChatBridgeApp/ActionRunner.swift b/macos/Sources/WeChatBridgeApp/ActionRunner.swift index 525bdb2..0a59c33 100644 --- a/macos/Sources/WeChatBridgeApp/ActionRunner.swift +++ b/macos/Sources/WeChatBridgeApp/ActionRunner.swift @@ -95,7 +95,7 @@ final class ActionRunner { // Nothing is shown: the share sheet said 「已复制到剪贴板」 a moment // ago and is still on screen. A second capsule saying it again is // WeChatBridge talking over the system. - case .codex, .claude, .doubao, .qwen, .workBuddy, .weSight, .deepSeekHarness, .obsidian, .folder, .custom: + case .codex, .claude, .doubao, .qwen, .workBuddy, .weSight, .deepSeekHarness, .qoderCN, .obsidian, .folder, .custom: // Shares and WeChat captures share the same clipboard queue. // // Read here rather than where the panel opens. This forward may wait diff --git a/macos/Sources/WeChatBridgeApp/AppModel.swift b/macos/Sources/WeChatBridgeApp/AppModel.swift index c981d16..a03d22a 100644 --- a/macos/Sources/WeChatBridgeApp/AppModel.swift +++ b/macos/Sources/WeChatBridgeApp/AppModel.swift @@ -220,7 +220,7 @@ final class AppModel: ObservableObject { // Already copied, by the extension, and already recorded as // such by `BatchState.initial`. Nothing left to do. return false - case .codex, .claude, .doubao, .qwen, .workBuddy, .weSight, .deepSeekHarness, .obsidian, .folder, .custom: + case .codex, .claude, .doubao, .qwen, .workBuddy, .weSight, .deepSeekHarness, .qoderCN, .obsidian, .folder, .custom: // A forward whose one-shot request is already gone: the file was // consumed by a run that then died before it could act, or the // extension never managed to write it. Either way nothing can be diff --git a/macos/Sources/WeChatBridgeApp/ForwardTargets.swift b/macos/Sources/WeChatBridgeApp/ForwardTargets.swift index 8c1a9a4..51f29bd 100644 --- a/macos/Sources/WeChatBridgeApp/ForwardTargets.swift +++ b/macos/Sources/WeChatBridgeApp/ForwardTargets.swift @@ -158,6 +158,7 @@ final class ForwardTargets: ObservableObject { ForwardDestination(action: .workBuddy, target: nil), ForwardDestination(action: .weSight, target: nil), ForwardDestination(action: .deepSeekHarness, target: nil), + ForwardDestination(action: .qoderCN, target: nil), ForwardDestination(action: .obsidian, target: nil), ] + targets.map { ForwardDestination(action: .custom, target: $0) } } diff --git a/macos/Sources/WeChatBridgeApp/ShareEntryProbe.swift b/macos/Sources/WeChatBridgeApp/ShareEntryProbe.swift index f1f47b5..f269b54 100644 --- a/macos/Sources/WeChatBridgeApp/ShareEntryProbe.swift +++ b/macos/Sources/WeChatBridgeApp/ShareEntryProbe.swift @@ -142,7 +142,7 @@ final class ShareEntryProbe: ObservableObject { /// /// `run` blocks on a `DispatchSemaphore`, and a blocked cooperative thread /// is gone until it unblocks — the runtime cannot reclaim it. `refresh` - /// probes nine entries in a row, so one cold `pkd` could park a pool thread + /// probes every entry in a row, so one cold `pkd` could park a pool thread /// for 15 s, and flipping several switches quickly parks more. On a /// four-core Mac that is most of the pool, and the `Task.sleep` loop /// `AutoPaste` uses to wait for the target app to come frontmost has a diff --git a/macos/Sources/WeChatBridgeApp/Views/Settings/ShareEntryList.swift b/macos/Sources/WeChatBridgeApp/Views/Settings/ShareEntryList.swift index 129a097..94f0a92 100644 --- a/macos/Sources/WeChatBridgeApp/Views/Settings/ShareEntryList.swift +++ b/macos/Sources/WeChatBridgeApp/Views/Settings/ShareEntryList.swift @@ -172,6 +172,7 @@ struct ShareEntryList: View { case .qwen: file = "02-qwen.png" case .workBuddy: file = "06-workbuddy.png" case .weSight: file = "07-wesight.png" + case .qoderCN: file = "08-qodercn.png" case .obsidian: file = "05-obsidian.png" case .deepSeekHarness, .collect, .folder, .clipboard, .custom: return nil } @@ -187,7 +188,7 @@ struct ShareEntryList: View { /// table of symbols would be a second answer to "which icon is this entry". static func symbol(for action: ShareAction) -> String { switch action { - case .codex, .claude, .doubao, .qwen, .workBuddy, .weSight, .deepSeekHarness: "paperplane" + case .codex, .claude, .doubao, .qwen, .workBuddy, .weSight, .deepSeekHarness, .qoderCN: "paperplane" case .obsidian: "book.closed" case .collect: "tray.and.arrow.down" case .folder: "folder" @@ -199,7 +200,7 @@ struct ShareEntryList: View { private func detail(for action: ShareAction) -> String { if compactDetails { switch action { - case .codex, .claude, .doubao, .qwen, .workBuddy, .weSight, .deepSeekHarness: + case .codex, .claude, .doubao, .qwen, .workBuddy, .weSight, .deepSeekHarness, .qoderCN: guard let bundleIdentifier = action.targetBundleIdentifier else { return L10n.text("未安装") } @@ -231,6 +232,7 @@ struct ShareEntryList: View { case .workBuddy: return L10n.text("激活 WorkBuddy 并直接粘贴到输入框。") case .weSight: return L10n.text("激活 WeSight 并直接粘贴到输入框。") case .deepSeekHarness: return L10n.text("激活 DeepSeek Harness 并直接粘贴到输入框。") + case .qoderCN: return L10n.text("激活 Qoder CN 并直接粘贴到输入框。") case .obsidian: return L10n.text("把聊天记录转成 Markdown,写入选定的 Obsidian 知识库。") case .collect: return L10n.text("多次分享,收齐后一起发送") case .folder: return L10n.text("把聊天记录转成 Markdown 和附件,写入选定的文件夹。") @@ -242,7 +244,7 @@ struct ShareEntryList: View { private func detailIsWarning(for action: ShareAction) -> Bool { guard compactDetails else { return false } switch action { - case .codex, .claude, .doubao, .qwen, .workBuddy, .weSight, .deepSeekHarness: + case .codex, .claude, .doubao, .qwen, .workBuddy, .weSight, .deepSeekHarness, .qoderCN: return action.targetBundleIdentifier.map { !InstalledApp.lookup($0).isInstalled } ?? true diff --git a/macos/Sources/WeChatBridgeCore/BatchState.swift b/macos/Sources/WeChatBridgeCore/BatchState.swift index 08d5057..22709aa 100644 --- a/macos/Sources/WeChatBridgeCore/BatchState.swift +++ b/macos/Sources/WeChatBridgeCore/BatchState.swift @@ -134,7 +134,7 @@ public struct BatchState: Codable, Sendable, Hashable { action: action, targetName: targetName ) - case .collect, .codex, .claude, .doubao, .qwen, .workBuddy, .weSight, .deepSeekHarness, .obsidian, .folder, .custom: + case .collect, .codex, .claude, .doubao, .qwen, .workBuddy, .weSight, .deepSeekHarness, .qoderCN, .obsidian, .folder, .custom: return BatchState(outcome: nil, action: action, targetName: targetName) } } diff --git a/macos/Sources/WeChatBridgeCore/ShareAction.swift b/macos/Sources/WeChatBridgeCore/ShareAction.swift index 98fc009..f058e7a 100644 --- a/macos/Sources/WeChatBridgeCore/ShareAction.swift +++ b/macos/Sources/WeChatBridgeCore/ShareAction.swift @@ -23,6 +23,8 @@ public enum ShareAction: String, Codable, Sendable, CaseIterable { case weSight /// Paste into DeepSeek Harness. case deepSeekHarness + /// Paste into Qoder CN. + case qoderCN /// Write a Markdown note and its source archive into the configured vault. case obsidian /// Write a Markdown note and its attachments into the configured folder, @@ -83,6 +85,7 @@ public enum ShareAction: String, Codable, Sendable, CaseIterable { case .workBuddy: return "com.tencent.workbuddy.mac" case .weSight: return "ai.wesight.app" case .deepSeekHarness: return "com.deepseek.dsh" + case .qoderCN: return "com.qodercn.app" case .obsidian: return "md.obsidian" // `.custom` has no fixed destination and no destination in its intent // either. `ActionRunner` resolves one from the user's own list, or takes @@ -102,7 +105,7 @@ public enum ShareAction: String, Codable, Sendable, CaseIterable { public var needsIntent: Bool { switch self { case .collect, .clipboard: return false - case .codex, .claude, .doubao, .qwen, .workBuddy, .weSight, .deepSeekHarness, .obsidian, .folder, .custom: return true + case .codex, .claude, .doubao, .qwen, .workBuddy, .weSight, .deepSeekHarness, .qoderCN, .obsidian, .folder, .custom: return true } } @@ -118,6 +121,7 @@ public enum ShareAction: String, Codable, Sendable, CaseIterable { case .workBuddy: return "WorkBuddy" case .weSight: return L10n.text("WeSight") case .deepSeekHarness: return "DeepSeek Harness" + case .qoderCN: return "Qoder CN" case .obsidian: return L10n.text("Obsidian") case .collect: return L10n.text("分批收集") case .folder: return L10n.text("文件夹") @@ -141,6 +145,7 @@ public enum ShareAction: String, Codable, Sendable, CaseIterable { case .workBuddy: return L10n.text("发给 WorkBuddy") case .weSight: return L10n.text("发给 WeSight") case .deepSeekHarness: return L10n.text("发给 DeepSeek Harness") + case .qoderCN: return L10n.text("发给 Qoder CN") case .obsidian: return L10n.text("沉淀到 Obsidian") case .collect: return L10n.text("分批收集到微信流") case .folder: return L10n.text("沉淀到文件夹") @@ -162,6 +167,7 @@ public enum ShareAction: String, Codable, Sendable, CaseIterable { case .workBuddy: return "ShareWorkBuddy" case .weSight: return "ShareWeSight" case .deepSeekHarness: return "ShareDeepSeekHarness" + case .qoderCN: return "ShareQoderCN" case .obsidian: return "ShareObsidian" case .collect: return "ShareCollection" case .folder: return "ShareFolder" diff --git a/macos/Sources/WeChatBridgeCore/ShareFailure.swift b/macos/Sources/WeChatBridgeCore/ShareFailure.swift index fad1892..8d1359e 100644 --- a/macos/Sources/WeChatBridgeCore/ShareFailure.swift +++ b/macos/Sources/WeChatBridgeCore/ShareFailure.swift @@ -17,7 +17,7 @@ public struct ShareFailure: Codable, Sendable, Hashable { public let at: Date /// Which Share-menu entry the user picked. Not shown anywhere yet — the /// message already names what went wrong — but a field report is worth - /// little without knowing which of the nine entries produced it. + /// little without knowing which entry produced it. public let action: ShareAction public let message: String diff --git a/macos/Tests/WeChatBridgeCoreTests/ShareActionTests.swift b/macos/Tests/WeChatBridgeCoreTests/ShareActionTests.swift index 7d23f4d..39f161e 100644 --- a/macos/Tests/WeChatBridgeCoreTests/ShareActionTests.swift +++ b/macos/Tests/WeChatBridgeCoreTests/ShareActionTests.swift @@ -16,7 +16,7 @@ final class ShareActionTests: XCTestCase { } func testOnlyForwardingActionsNameATargetApp() { - XCTAssertEqual(ShareAction.allCases.count, 12) + XCTAssertEqual(ShareAction.allCases.count, 13) XCTAssertEqual(ShareAction.codex.targetBundleIdentifier, "com.openai.codex") XCTAssertEqual(ShareAction.claude.targetBundleIdentifier, "com.anthropic.claudefordesktop") XCTAssertEqual(ShareAction.doubao.targetBundleIdentifier, "com.bot.pc.doubao") @@ -24,6 +24,7 @@ final class ShareActionTests: XCTestCase { XCTAssertEqual(ShareAction.workBuddy.targetBundleIdentifier, "com.tencent.workbuddy.mac") XCTAssertEqual(ShareAction.weSight.targetBundleIdentifier, "ai.wesight.app") XCTAssertEqual(ShareAction.deepSeekHarness.targetBundleIdentifier, "com.deepseek.dsh") + XCTAssertEqual(ShareAction.qoderCN.targetBundleIdentifier, "com.qodercn.app") XCTAssertEqual(ShareAction.obsidian.targetBundleIdentifier, "md.obsidian") XCTAssertNil(ShareAction.clipboard.targetBundleIdentifier) // 「沉淀到文件夹」 delivers to a path, not an app. @@ -51,6 +52,18 @@ final class ShareActionTests: XCTestCase { XCTAssertEqual(try BatchManifest.decoder().decode(BatchManifest.self, from: manifestData).action, .folder) } + /// The raw value is the wire contract: `Scripts/share-slots.sh` stamps it + /// into the appex's `DKShareAction`, and `ShareAction.declared` reads it + /// back. Renaming the case without the slot row breaks the entry silently, + /// so both halves are pinned together here. + func testQoderCNActionRoundTripsThroughCodable() throws { + let data = try JSONEncoder().encode(ShareAction.qoderCN) + XCTAssertEqual(String(data: data, encoding: .utf8), "\"qoderCN\"") + XCTAssertEqual(try JSONDecoder().decode(ShareAction.self, from: data), .qoderCN) + XCTAssertEqual(ShareAction.qoderCN.bundleIdentifierSuffix, "ShareQoderCN") + XCTAssertTrue(ShareAction.qoderCN.needsIntent) + } + func testAnIntentIsReadableExactlyOnce() throws { let batchID = try commitBatch(intent: BatchIntent(action: .codex, requestedAt: Date())) @@ -84,7 +97,7 @@ final class ShareActionTests: XCTestCase { XCTAssertFalse(ShareAction.clipboard.needsIntent) for action in [ ShareAction.codex, .claude, .doubao, .qwen, .workBuddy, - .weSight, .deepSeekHarness, .obsidian, .folder, .custom, + .weSight, .deepSeekHarness, .qoderCN, .obsidian, .folder, .custom, ] { XCTAssertTrue(action.needsIntent, "\(action)") } diff --git a/shared/AppLogos/08-qodercn.png b/shared/AppLogos/08-qodercn.png new file mode 100644 index 0000000..1fbf1ef Binary files /dev/null and b/shared/AppLogos/08-qodercn.png differ diff --git a/shared/AppLogos/README.md b/shared/AppLogos/README.md index a9c0936..2452cb1 100644 --- a/shared/AppLogos/README.md +++ b/shared/AppLogos/README.md @@ -9,6 +9,7 @@ Official 1024 x 1024 App Store artwork, ordered for the forwarding flow: 5. `05-obsidian.png` - [Obsidian](https://apps.apple.com/cn/app/id1557175442) 6. `06-workbuddy.png` - [WorkBuddy](https://apps.apple.com/cn/app/id6761374913) 7. `07-wesight.png` - WeSight +8. `08-qodercn.png` - Qoder CN (icon extracted from the local `/Applications/Qoder CN.app` bundle; no App Store listing) Doubao, Qwen, Obsidian, and WorkBuddy use the China App Store listing. Claude and ChatGPT have no China App Store listing, so their official