From 03837b0e487dbdafa4ac7700914e8e393d68de4d Mon Sep 17 00:00:00 2001 From: Rocky <72559939+hkgood@users.noreply.github.com> Date: Sun, 23 Aug 2026 20:26:34 +0800 Subject: [PATCH] Refine local AI hint fallbacks Keep clipboard actions in Skills and focus offline hints on distinct daily market briefings while preventing retired cached cards from resurfacing. --- .../Services/AIHintKeywordExtractor.swift | 2 +- .../Services/AIHintLocalCatalog.swift | 208 +++++++----------- OSGKeyboardShared/Services/AIHintPool.swift | 5 +- .../AIHintKeywordExtractorTests.swift | 24 +- OSGKeyboardTests/AIHintPoolTests.swift | 81 +++++-- 5 files changed, 163 insertions(+), 157 deletions(-) diff --git a/OSGKeyboardShared/Services/AIHintKeywordExtractor.swift b/OSGKeyboardShared/Services/AIHintKeywordExtractor.swift index cb8d98f..61de47d 100644 --- a/OSGKeyboardShared/Services/AIHintKeywordExtractor.swift +++ b/OSGKeyboardShared/Services/AIHintKeywordExtractor.swift @@ -53,7 +53,7 @@ public enum AIHintKeywordExtractor: Sendable { case .news: return locale == "zh" ? "今日早报" : "Today's briefing" case .stocks: - return locale == "zh" ? "今日大盘" : "Markets" + return card.displayText case .calendar: if locale != "zh", let name = trimmed(card.metadata?.name) { return name diff --git a/OSGKeyboardShared/Services/AIHintLocalCatalog.swift b/OSGKeyboardShared/Services/AIHintLocalCatalog.swift index 54b63df..857d418 100644 --- a/OSGKeyboardShared/Services/AIHintLocalCatalog.swift +++ b/OSGKeyboardShared/Services/AIHintLocalCatalog.swift @@ -1,8 +1,8 @@ // AIHintLocalCatalog.swift // OSGKeyboard · Shared // -// Built-in, non-time-sensitive AI idle hints (clipboard + evergreen). Always -// available as a fallback when the remote pack is missing or stale. +// Built-in current-information fallbacks for AI idle. Always available when +// the remote pack is missing or stale; clipboard actions live in Skills. import Foundation @@ -11,57 +11,13 @@ public enum AIHintLocalCatalog: Sendable { locale == "zh" ? zhCards : enCards } + /// Prevent removed built-ins from resurfacing from a ready pack written by + /// an older app version before the next successful cloud refresh. + public static func isRetired(cardID: String) -> Bool { + retiredCardIDs.contains(cardID) + } + private static let zhCards: [AIHintCard] = [ - AIHintCard( - id: "local-zh-clipboard-reply", - displayText: "帮我回复剪贴板", - prompt: "请根据剪贴板内容起草一段礼貌、简洁的回复,语气自然,可直接发送。", - category: "clipboard", - priority: 90, - source: "local", - locale: "zh", - conditions: ["clipboard_30s"] - ), - AIHintCard( - id: "local-zh-clipboard-translate", - displayText: "把剪贴板译成英文", - prompt: "请将剪贴板内容翻译成自然、地道的英文,保留原意与语气。", - category: "clipboard", - priority: 88, - source: "local", - locale: "zh", - conditions: ["clipboard_30s"] - ), - AIHintCard( - id: "local-zh-clipboard-summarize", - displayText: "帮我精简剪贴板", - prompt: "请将剪贴板内容精简为更短、更清晰的版本,保留关键信息与语气。", - category: "clipboard", - priority: 86, - source: "local", - locale: "zh", - conditions: ["clipboard_30s"] - ), - AIHintCard( - id: "local-zh-encyclopedia", - displayText: "有趣概念", - prompt: "用通俗易懂的中文解释一个有趣但常见的概念,并给一个生活里的例子(4-6 句)。", - category: "capability", - priority: 40, - source: "local", - locale: "zh" - ), - AIHintCard( - id: "local-zh-stocks", - displayText: "今日大盘", - prompt: "请用非专业口吻概括今天 A 股/港股/美股中至少一个市场的整体表现、" - + "可能驱动因素,并提醒这并非投资建议(4-6 句)。", - category: "economy", - priority: 42, - source: "local", - locale: "zh", - taskKind: .currentInformationQuestion - ), AIHintCard( id: "local-zh-daily-brief", displayText: "今日早报", @@ -74,76 +30,41 @@ public enum AIHintLocalCatalog: Sendable { taskKind: .currentInformationQuestion ), AIHintCard( - id: "local-zh-quote", - displayText: "今日金句", - prompt: "请给一句适合今天分享的中文金句,并附上一两句简短解释。", - category: "capability", - priority: 38, + id: "local-zh-stocks-cn", + displayText: "今日A股", + prompt: "请用非专业口吻概括今天 A 股的整体表现,说明上证指数、深证成指或创业板的主要变化、" + + "可能驱动因素,并提醒这并非投资建议(4-6 句)。", + category: "economy", + priority: 44, source: "local", - locale: "zh" + locale: "zh", + taskKind: .currentInformationQuestion ), AIHintCard( - id: "local-zh-howto", - displayText: "生活技巧", - prompt: "分享一个实用的生活或工作效率小技巧,用中文说清步骤与适用场景(4-6 句)。", - category: "capability", - priority: 36, + id: "local-zh-stocks-hk", + displayText: "今日港股", + prompt: "请用非专业口吻概括今天港股的整体表现,说明恒生指数、恒生科技指数的主要变化、" + + "可能驱动因素,并提醒这并非投资建议(4-6 句)。", + category: "economy", + priority: 43, source: "local", - locale: "zh" + locale: "zh", + taskKind: .currentInformationQuestion + ), + AIHintCard( + id: "local-zh-stocks-us", + displayText: "今日美股", + prompt: "请用非专业口吻概括今天或最近一个交易日美股的整体表现,说明道琼斯指数、标普 500、" + + "纳斯达克指数的主要变化、可能驱动因素,并提醒这并非投资建议(4-6 句)。", + category: "economy", + priority: 42, + source: "local", + locale: "zh", + taskKind: .currentInformationQuestion ) ] private static let enCards: [AIHintCard] = [ - AIHintCard( - id: "local-en-clipboard-reply", - displayText: "Reply to clipboard", - prompt: "Draft a concise, polite reply the user can send, based on the clipboard text.", - category: "clipboard", - priority: 90, - source: "local", - locale: "en", - conditions: ["clipboard_30s"] - ), - AIHintCard( - id: "local-en-clipboard-translate", - displayText: "Translate clipboard", - prompt: "Translate the clipboard text into natural English, preserving meaning and tone.", - category: "clipboard", - priority: 88, - source: "local", - locale: "en", - conditions: ["clipboard_30s"] - ), - AIHintCard( - id: "local-en-clipboard-summarize", - displayText: "Shorten clipboard", - prompt: "Shorten the clipboard text into a clearer, shorter version while keeping the key points.", - category: "clipboard", - priority: 86, - source: "local", - locale: "en", - conditions: ["clipboard_30s"] - ), - AIHintCard( - id: "local-en-encyclopedia", - displayText: "A concept", - prompt: "Explain an interesting everyday concept in plain English with one real-life example (4-6 sentences).", - category: "capability", - priority: 40, - source: "local", - locale: "en" - ), - AIHintCard( - id: "local-en-stocks", - displayText: "Markets", - prompt: "Summarize today's broad market mood (US or global) in plain English, " - + "note possible drivers, and add this is not financial advice (4-6 sentences).", - category: "economy", - priority: 42, - source: "local", - locale: "en", - taskKind: .currentInformationQuestion - ), AIHintCard( id: "local-en-daily-brief", displayText: "Today's briefing", @@ -156,22 +77,57 @@ public enum AIHintLocalCatalog: Sendable { taskKind: .currentInformationQuestion ), AIHintCard( - id: "local-en-quote", - displayText: "A quote", - prompt: "Share one short quote worth sending today, plus one or two sentences of context.", - category: "capability", - priority: 38, + id: "local-en-stocks-cn", + displayText: "China A-shares", + prompt: "Summarize today's China A-share market in plain English, including the main move in the " + + "Shanghai Composite, Shenzhen Component, or ChiNext, likely drivers, and a reminder that " + + "this is not financial advice (4-6 sentences).", + category: "economy", + priority: 44, source: "local", - locale: "en" + locale: "en", + taskKind: .currentInformationQuestion ), AIHintCard( - id: "local-en-howto", - displayText: "A tip", - prompt: "Share one practical life or productivity tip in English, with steps and when it helps (4-6 sentences).", - category: "capability", - priority: 36, + id: "local-en-stocks-hk", + displayText: "Hong Kong stocks", + prompt: "Summarize today's Hong Kong stock market in plain English, including the main moves in " + + "the Hang Seng Index and Hang Seng TECH Index, likely drivers, and a reminder that this is " + + "not financial advice (4-6 sentences).", + category: "economy", + priority: 43, source: "local", - locale: "en" + locale: "en", + taskKind: .currentInformationQuestion + ), + AIHintCard( + id: "local-en-stocks-us", + displayText: "US stocks", + prompt: "Summarize today's or the latest US stock-market session in plain English, including the " + + "main moves in the Dow, S&P 500, and Nasdaq, likely drivers, and a reminder that this is not " + + "financial advice (4-6 sentences).", + category: "economy", + priority: 42, + source: "local", + locale: "en", + taskKind: .currentInformationQuestion ) ] + + private static let retiredCardIDs: Set = [ + "local-zh-clipboard-reply", + "local-zh-clipboard-translate", + "local-zh-clipboard-summarize", + "local-zh-encyclopedia", + "local-zh-stocks", + "local-zh-quote", + "local-zh-howto", + "local-en-clipboard-reply", + "local-en-clipboard-translate", + "local-en-clipboard-summarize", + "local-en-encyclopedia", + "local-en-stocks", + "local-en-quote", + "local-en-howto" + ] } diff --git a/OSGKeyboardShared/Services/AIHintPool.swift b/OSGKeyboardShared/Services/AIHintPool.swift index 690104d..41f8f43 100644 --- a/OSGKeyboardShared/Services/AIHintPool.swift +++ b/OSGKeyboardShared/Services/AIHintPool.swift @@ -10,7 +10,10 @@ public enum AIHintPool: Sendable { public static func activeCards( pack: AIHintPack ) -> [AIHintCard] { - let regularCards = pack.cards.filter { !$0.requiresClipboard30s } + let regularCards = pack.cards + .filter { !AIHintLocalCatalog.isRetired(cardID: $0.id) } + .filter { !$0.requiresClipboard30s } + .filter { $0.metadata?.soul == nil } .filter { !isHistoricalToday($0) } // Clipboard-conditioned sentences no longer rotate in the carousel; diff --git a/OSGKeyboardTests/AIHintKeywordExtractorTests.swift b/OSGKeyboardTests/AIHintKeywordExtractorTests.swift index 7045e9b..909c116 100644 --- a/OSGKeyboardTests/AIHintKeywordExtractorTests.swift +++ b/OSGKeyboardTests/AIHintKeywordExtractorTests.swift @@ -100,18 +100,20 @@ final class AIHintKeywordExtractorTests: XCTestCase { XCTAssertEqual(soul.resolvedDisplayText, "今日金句") } - func testCapabilityMapsToSearch() { - let card = AIHintLocalCatalog.cards(locale: "zh") - .first { $0.id == "local-zh-encyclopedia" }! - XCTAssertEqual(card.visualKind, .search) - XCTAssertEqual(card.resolvedDisplayText, "有趣概念") - } + func testStockHintsKeepDistinctMarketLabelsAndChartIcon() { + let expectedLabels = [ + "local-zh-stocks-cn": "今日A股", + "local-zh-stocks-hk": "今日港股", + "local-zh-stocks-us": "今日美股" + ] + let cards = AIHintLocalCatalog.cards(locale: "zh") + .filter { expectedLabels[$0.id] != nil } - func testStocksMapsToChartIcon() { - let card = AIHintLocalCatalog.cards(locale: "zh") - .first { $0.id == "local-zh-stocks" }! - XCTAssertEqual(card.visualKind, .stocks) - XCTAssertEqual(card.resolvedDisplayText, "今日大盘") + XCTAssertEqual(cards.count, expectedLabels.count) + for card in cards { + XCTAssertEqual(card.visualKind, .stocks) + XCTAssertEqual(card.resolvedDisplayText, expectedLabels[card.id]) + } } } diff --git a/OSGKeyboardTests/AIHintPoolTests.swift b/OSGKeyboardTests/AIHintPoolTests.swift index 9abcbc2..4dc6051 100644 --- a/OSGKeyboardTests/AIHintPoolTests.swift +++ b/OSGKeyboardTests/AIHintPoolTests.swift @@ -13,9 +13,18 @@ final class AIHintPoolTests: XCTestCase { } func testClipboardWindowNeverPutsClipboardCardsInCarousel() { + let clipboardCard = AIHintCard( + id: "remote-clipboard-reply", + displayText: "回复剪贴板", + prompt: "请回复剪贴板内容", + category: "clipboard", + source: "remote", + locale: "zh", + conditions: ["clipboard_30s"] + ) let pack = AIHintPack( locale: "zh", - cards: AIHintLocalCatalog.cards(locale: "zh") + cards: AIHintLocalCatalog.cards(locale: "zh") + [clipboardCard] ) let recent = ClipboardHistoryEntry(text: "hello", createdAt: Date()) XCTAssertTrue( @@ -29,19 +38,52 @@ final class AIHintPoolTests: XCTestCase { XCTAssertTrue(cards.allSatisfy { !$0.requiresClipboard30s }) } - func testClipboardDisabledDropsClipboardCards() { - let pack = AIHintPack( - locale: "zh", - cards: AIHintLocalCatalog.cards(locale: "zh") + func testLocalCatalogDoesNotDuplicateClipboardSkills() { + for locale in ["zh", "en"] { + let cards = AIHintLocalCatalog.cards(locale: locale) + XCTAssertEqual(cards.count, 4) + XCTAssertTrue(cards.allSatisfy { !$0.requiresClipboard30s }) + XCTAssertFalse(cards.contains { $0.category == "capability" }) + } + } + + func testRetiredHintsDoNotResurfaceFromAnOlderReadyPack() { + let retired = AIHintCard( + id: "local-zh-quote", + displayText: "今日金句", + prompt: "旧版本缓存", + category: "capability", + source: "local", + locale: "zh" ) - let cards = AIHintPool.activeCards(pack: pack) - XCTAssertFalse(cards.isEmpty) - XCTAssertTrue(cards.allSatisfy { !$0.requiresClipboard30s }) + let retiredRemoteQuote = AIHintCard( + id: "tophub-daily-soul-old", + displayText: "今日一句", + prompt: "旧版本云端缓存", + category: "daily", + source: "tophub-daily", + locale: "zh", + metadata: AIHintMetadata(soul: "旧金句") + ) + let cards = AIHintPool.activeCards( + pack: AIHintPack(locale: "zh", cards: [retired, retiredRemoteQuote]) + ) + + XCTAssertFalse(cards.contains { $0.id == retired.id }) + XCTAssertFalse(cards.contains { $0.id == retiredRemoteQuote.id }) + XCTAssertEqual(Set(cards.map(\.id)), Set(AIHintLocalCatalog.cards(locale: "zh").map(\.id))) } func testResolvePromptEmbedsClipboardMaterialAsData() throws { - let card = AIHintLocalCatalog.cards(locale: "zh") - .first { $0.id == "local-zh-clipboard-reply" }! + let card = AIHintCard( + id: "remote-clipboard-reply", + displayText: "回复剪贴板", + prompt: "请回复剪贴板内容", + category: "clipboard", + source: "remote", + locale: "zh", + conditions: ["clipboard_30s"] + ) guard case .ready(let prompt) = AIHintPool.resolvePrompt( for: card, clipboardText: "你好" @@ -134,14 +176,17 @@ final class AIHintPoolTests: XCTestCase { XCTAssertEqual(card.taskKind, .currentInformationQuestion) } - func testLocalTimeSensitiveHintsRequireCurrentInformation() throws { + func testLocalTimeSensitiveHintsRequireCurrentInformation() { let cards = AIHintLocalCatalog.cards(locale: "zh") - let stocks = try XCTUnwrap(cards.first { $0.id == "local-zh-stocks" }) - let brief = try XCTUnwrap(cards.first { $0.id == "local-zh-daily-brief" }) - let concept = try XCTUnwrap(cards.first { $0.id == "local-zh-encyclopedia" }) - - XCTAssertEqual(stocks.taskKind, .currentInformationQuestion) - XCTAssertEqual(brief.taskKind, .currentInformationQuestion) - XCTAssertEqual(concept.taskKind, .aiQuestion) + XCTAssertEqual( + cards.map(\.id), + [ + "local-zh-daily-brief", + "local-zh-stocks-cn", + "local-zh-stocks-hk", + "local-zh-stocks-us" + ] + ) + XCTAssertTrue(cards.allSatisfy { $0.taskKind == .currentInformationQuestion }) } }