From 56936b786ad481bc5a3f42ed58b93d953bb7ecce Mon Sep 17 00:00:00 2001 From: takeshi <11970938+takeshi264@users.noreply.github.com> Date: Fri, 16 Oct 2026 11:25:16 +0900 Subject: [PATCH] fix(vocabulary): enable subscribed vocabularies for Ziranma Co-Authored-By: Codex --- Apps/AIMESettings/Views/LibraryViews.swift | 2 +- CHANGELOG.en.md | 4 +++ CHANGELOG.md | 4 +++ .../Sources/AIMECore/DoublePinyin.swift | 23 +++++++++--- .../AIMECoreTests/DoublePinyinTests.swift | 25 +++++++++++++ .../AIMECoreTests/SubscriptionTests.swift | 36 +++++++++++++++++-- 6 files changed, 86 insertions(+), 8 deletions(-) diff --git a/Apps/AIMESettings/Views/LibraryViews.swift b/Apps/AIMESettings/Views/LibraryViews.swift index 5d0e9d8..8fae30b 100644 --- a/Apps/AIMESettings/Views/LibraryViews.swift +++ b/Apps/AIMESettings/Views/LibraryViews.swift @@ -394,7 +394,7 @@ private struct SubscriptionsSection: View { Button("订阅", action: add).buttonStyle(.borderedProminent).disabled(url.isEmpty || !model.canAddSubscription) } .controlSize(.small) - Text("支持 RIME dict.yaml、「词条⇥编码⇥权重」表格或每行一个词;拼音自动生成,全拼与小鹤双拼均可打出。") + Text("支持 RIME dict.yaml、「词条⇥编码⇥权重」表格或每行一个词;拼音自动生成,全拼、小鹤双拼与自然码双拼均可打出。") .font(.caption).foregroundStyle(.secondary) } if let activity = model.subscriptionActivity { diff --git a/CHANGELOG.en.md b/CHANGELOG.en.md index 41f4cbd..827d4e5 100644 --- a/CHANGELOG.en.md +++ b/CHANGELOG.en.md @@ -12,6 +12,10 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and ## [Unreleased] +### Fixed + +- Added missing Ziranma double-pinyin support for subscribed vocabularies. The next deployment generates and mounts Ziranma tables for existing subscriptions. + ## [0.1.9] - 2026-10-13 > Stable release · Developer ID signed · notarized by Apple diff --git a/CHANGELOG.md b/CHANGELOG.md index 5429a0b..303d11d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ AIME 每个版本面向用户的变动都记在这里,官网「更新日志」 ## [Unreleased] +### 修复 + +- 补上自然码双拼的订阅词库支持,已有订阅在下次部署时自动生成自然码词表并接入候选。 + ## [0.1.9] - 2026-10-13 > 正式版 · Developer ID 签名 · 已通过 Apple 公证 diff --git a/Packages/AIMEKit/Sources/AIMECore/DoublePinyin.swift b/Packages/AIMEKit/Sources/AIMECore/DoublePinyin.swift index 42127f0..0b997d9 100644 --- a/Packages/AIMEKit/Sources/AIMECore/DoublePinyin.swift +++ b/Packages/AIMEKit/Sources/AIMECore/DoublePinyin.swift @@ -5,10 +5,15 @@ public import Foundation public enum DoublePinyin: String, Sendable, CaseIterable { /// 小鹤双拼 (double_pinyin_flypy) case flypy + /// 自然码双拼 (double_pinyin) + case ziranma /// Schema ids that use this layout. public var schemaIDs: [String] { - switch self { case .flypy: ["double_pinyin_flypy"] } + switch self { + case .flypy: ["double_pinyin_flypy"] + case .ziranma: ["double_pinyin"] + } } public static func layout(forSchema id: String) -> DoublePinyin? { @@ -16,7 +21,9 @@ public enum DoublePinyin: String, Sendable, CaseIterable { } private var initials: [String: String] { - switch self { case .flypy: ["zh": "v", "ch": "i", "sh": "u"] } + switch self { + case .flypy, .ziranma: ["zh": "v", "ch": "i", "sh": "u"] + } } private var finals: [String: String] { @@ -27,6 +34,13 @@ public enum DoublePinyin: String, Sendable, CaseIterable { "ing": "k", "uai": "k", "iang": "l", "uang": "l", "ou": "z", "ia": "x", "ua": "x", "ao": "c", "ui": "v", "v": "v", "in": "b", "iao": "n", "ian": "m", ] + case .ziranma: [ + "iu": "q", "ia": "w", "ua": "w", "uan": "r", "van": "r", "ue": "t", "ve": "t", + "ing": "y", "uai": "y", "uo": "o", "un": "p", "vn": "p", "ong": "s", "iong": "s", + "iang": "d", "uang": "d", "en": "f", "eng": "g", "ang": "h", "ian": "m", "an": "j", + "iao": "c", "ao": "k", "ai": "l", "ei": "z", "ie": "x", "ui": "v", "ou": "b", + "in": "n", "v": "v" + ] } } @@ -49,8 +63,9 @@ public enum DoublePinyin: String, Sendable, CaseIterable { } let initial = String(syllable.first!) let rest = String(syllable.dropFirst()) - // j/q/x/y + u is ü. - let normalized = ("jqxy".contains(initial) && rest.hasPrefix("u")) ? "v" + rest.dropFirst() : rest + // Flypy uses v for j/q/x/y + ü; Ziranma retains the written u (ju, qu, xu, yu). + let normalized = (self == .flypy && "jqxy".contains(initial) && rest.hasPrefix("u")) + ? "v" + rest.dropFirst() : rest if normalized.count == 1 { return initial + normalized } guard let final = finals[normalized] ?? finals[rest] else { return nil } return initial + final diff --git a/Packages/AIMEKit/Tests/AIMECoreTests/DoublePinyinTests.swift b/Packages/AIMEKit/Tests/AIMECoreTests/DoublePinyinTests.swift index ad4c9e6..2a1ade2 100644 --- a/Packages/AIMEKit/Tests/AIMECoreTests/DoublePinyinTests.swift +++ b/Packages/AIMEKit/Tests/AIMECoreTests/DoublePinyinTests.swift @@ -22,4 +22,29 @@ struct DoublePinyinTests { #expect(flypy.code(forPinyin: "not a syllable xq") == nil) #expect(DoublePinyin.layout(forSchema: "double_pinyin_flypy") == .flypy) } + + @Test func convertsSyllablesToZiranma() { + let ziranma = DoublePinyin.ziranma + let cases: [String: String] = [ + "ni": "ni", "hao": "hk", "jie": "jx", "chuang": "id", "xue": "xt", + "lv": "lv", "lve": "lt", "ju": "ju", "qu": "qu", "xu": "xu", "yu": "yu", + "a": "aa", "e": "ee", "ai": "ai", "an": "an", "ang": "ah", "eng": "eg", "er": "er", "ou": "ou", + "xiong": "xs", "guang": "gd", "zhuang": "vd", "sheng": "ug", "yuan": "yr", "yun": "yp", + "bin": "bn", "liu": "lq", "shuang": "ud", "jiao": "jc", "gou": "gb", "ying": "yy" + ] + for (pinyin, code) in cases { + #expect(ziranma.code(forSyllable: pinyin) == code, "\(pinyin)") + } + #expect(ziranma.code(forPinyin: "xiang liang shu ju ku") == "xdlduujuku") + #expect(ziranma.code(forPinyin: "ju shen zhi neng") == "juufving") + #expect(flypy.code(forPinyin: "ju shen zhi neng") == "jvufving") + } + + @Test func recognizesOnlyKnownDoublePinyinSchemas() { + #expect(DoublePinyin.layout(forSchema: "double_pinyin") == .ziranma) + #expect(DoublePinyin.layout(forSchema: "double_pinyin_flypy") == .flypy) + #expect(DoublePinyin.layout(forSchema: "double_pinyin_custom") == nil) + #expect(DoublePinyin.layout(forSchema: "double_pinyin_mspy") == nil) + #expect(DoublePinyin.layout(forSchema: "rime_ice") == nil) + } } diff --git a/Packages/AIMEKit/Tests/AIMECoreTests/SubscriptionTests.swift b/Packages/AIMEKit/Tests/AIMECoreTests/SubscriptionTests.swift index 796ba11..1d88bad 100644 --- a/Packages/AIMEKit/Tests/AIMECoreTests/SubscriptionTests.swift +++ b/Packages/AIMEKit/Tests/AIMECoreTests/SubscriptionTests.swift @@ -46,20 +46,50 @@ struct SubscriptionTests { @Test func rebuildWritesLayoutTablesAndMountsSchemas() async throws { let source = root.appendingPathComponent("feed.txt") - try "氛围编程\n".write(to: source, atomically: true, encoding: .utf8) + try "氛围编程\n向量数据库\txiang liang shu ju ku\t80\n".write(to: source, atomically: true, encoding: .utf8) let manager = SubscriptionManager(paths: paths) try await manager.add(url: source.absoluteString, name: nil) - let count = try manager.rebuild(schemas: ["rime_ice", "double_pinyin_flypy"]) - #expect(count == 3) + let count = try manager.rebuild(schemas: ["rime_ice", "double_pinyin_flypy", "double_pinyin", "double_pinyin_custom"]) + #expect(count == 4) let full = try String(contentsOf: paths.userDataDir.appendingPathComponent("aime_online.txt"), encoding: .utf8) let flypy = try String(contentsOf: paths.userDataDir.appendingPathComponent("aime_online_flypy.txt"), encoding: .utf8) + let ziranma = try String(contentsOf: paths.userDataDir.appendingPathComponent("aime_online_ziranma.txt"), encoding: .utf8) #expect(full.contains("智能体\tzhinengti") && full.contains("DeepSeek\tdeepseek")) #expect(flypy.contains("智能体\tvingti") && flypy.contains("氛围编程\tffwwbmig")) + #expect(ziranma.contains("向量数据库\txdlduujuku\t80") && ziranma.contains("DeepSeek\tdeepseek")) let layers = ConfigLayers(paths: paths) #expect(layers.generatedValue(.schema("double_pinyin_flypy"), keypath: "aime_online")?["user_dict"] == "aime_online_flypy") + #expect(layers.generatedValue(.schema("double_pinyin"), keypath: "aime_online")?["user_dict"] == "aime_online_ziranma") + #expect(layers.generatedValue(.schema("double_pinyin"), keypath: "engine/translators/+") == ["table_translator@aime_online"]) + #expect(layers.generatedValue(.schema("double_pinyin_custom"), keypath: "aime_online") == nil) #expect(layers.generatedValue(.schema("rime_ice"), keypath: "engine/translators/+") == ["table_translator@aime_online"]) } + @Test func existingSubscriptionCacheAddsMissingZiranmaTableWithoutDownloading() async throws { + try "schema_list:\n - schema: double_pinyin\n".write( + to: paths.sharedDataDir!.appendingPathComponent("default.yaml"), atomically: true, encoding: .utf8) + let source = root.appendingPathComponent("feed.txt") + try "具身智能\tju shen zhi neng\t80\n".write(to: source, atomically: true, encoding: .utf8) + let manager = SubscriptionManager(paths: paths) + let added = try await manager.add(url: source.absoluteString, name: nil) + let cached = try Data(contentsOf: manager.cacheURL(added.id)) + #expect(try manager.ensureTables()) + #expect(try manager.ensureTables() == false) + + // An older installation has the cache and signature, but no Ziranma table or mount. + let table = paths.userDataDir.appendingPathComponent("aime_online_ziranma.txt") + try FileManager.default.removeItem(at: table) + try FileManager.default.removeItem(at: paths.aimeDir.appendingPathComponent("generated/double_pinyin.yaml")) + try FileManager.default.removeItem(at: source) + + #expect(try manager.ensureTables()) + #expect(try String(contentsOf: table, encoding: .utf8).contains("具身智能\tjuufving\t80")) + #expect(ConfigLayers(paths: paths).generatedValue(.schema("double_pinyin"), keypath: "aime_online")?["user_dict"] + == "aime_online_ziranma") + #expect(try Data(contentsOf: manager.cacheURL(added.id)) == cached) + #expect(try manager.ensureTables() == false) + } + @Test func ensureTablesDedupesAndRebuildsOnlyWhenInputsChange() async throws { try "schema_list:\n - schema: rime_ice\n".write( to: paths.sharedDataDir!.appendingPathComponent("default.yaml"), atomically: true, encoding: .utf8)