From fe8f657c8d5a416ea7203e12f59cee56c4d3d1ca Mon Sep 17 00:00:00 2001 From: Rocky <72559939+hkgood@users.noreply.github.com> Date: Sat, 22 Aug 2026 16:46:37 +0800 Subject: [PATCH] Fix CI tooling and semantic skill recommendations Resolve the pinned XcodeGen archive layout and let the keyboard derive bounded clipboard recommendations from the full catalog independently of installation state. --- CHANGELOG.md | 2 +- .../Services/AIKeyboardCoordinator.swift | 2 +- .../Services/AppGroupPersistor.swift | 5 ++ OSGKeyboardExt/Views/AIKeyboardView.swift | 12 +++-- .../ClipboardSkillSemanticRanker.swift | 50 +++++++++++++++++-- .../Services/KeyboardState.swift | 4 ++ .../ClipboardSkillSemanticRankerTests.swift | 26 ++++++++++ Scripts/install-xcodegen-ci.sh | 7 +-- 8 files changed, 95 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97144ba..47340e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - **Local clipboard semantics**: add reproducible bilingual Create ML training, self-contained on-device classifiers for tasks, questions, invitations, complaints, and sentiment, plus deterministic language, date, address, phone, URL, person, and organization labels without uploading clipboard text. / **本地剪贴板语义**:新增可复现的中英语料与 Create ML 训练流程,以及任务、问句、邀约、投诉和情感的端侧自包含分类器,并结合确定性的语言、日期、地址、电话、URL、人名和组织名标签,全程不上传剪贴板原文。 -- **Contextual clipboard skills**: install the complete built-in skill catalog by default, add source-language, invitation, task, clarification, empathy, business, conclusion, and list actions, remove the eight-skill cap, and rank keyboard skills ephemerally from local clipboard labels while preserving the user's saved order. / **情境化剪贴板技能**:默认安装完整内置技能目录,新增原语言回复、邀约、任务、澄清、共情、商务、结论与清单操作,移除八个技能上限,并根据本地剪贴板标签临时智能排序,同时保留用户保存的长期顺序。 +- **Contextual clipboard skills**: add source-language, invitation, task, clarification, empathy, business, conclusion, and list actions, then select up to five relevant keyboard skills from the complete catalog using ephemeral local clipboard labels, independent of installation state or user-managed ordering. / **情境化剪贴板技能**:新增原语言回复、邀约、任务、澄清、共情、商务、结论与清单操作,并通过本地临时剪贴板标签从完整目录中自动选出最多五个相关技能,不再依赖安装状态或用户管理的固定顺序。 - **Learned speaking styles**: unlock user-initiated style generation after 5,000 effective dictation characters, prioritize recurring native speech and explicit user edits, use historical polish prompts only to subtract AI-added style, and require review before saving. / **学习说话风格**:累积 5,000 个有效听写字符后,可优先根据反复出现的原生口述与用户明确编辑生成个人风格,历史润色 Prompt 仅用于排除 AI 附加风格,并在保存前强制检查。 ### Changed diff --git a/OSGKeyboardExt/Services/AIKeyboardCoordinator.swift b/OSGKeyboardExt/Services/AIKeyboardCoordinator.swift index b79e800..f8d6ef3 100644 --- a/OSGKeyboardExt/Services/AIKeyboardCoordinator.swift +++ b/OSGKeyboardExt/Services/AIKeyboardCoordinator.swift @@ -402,7 +402,7 @@ final class AIKeyboardCoordinator { } private func resolvedSkill(id: String) -> AIClipboardSkill? { - state.enabledClipboardSkills.first { $0.id == id } + state.clipboardSkillCatalog.first { $0.id == id } } /// Parse an export skill. Empty → in-keyboard tip, stay in the host app. diff --git a/OSGKeyboardExt/Services/AppGroupPersistor.swift b/OSGKeyboardExt/Services/AppGroupPersistor.swift index b2dee96..9780093 100644 --- a/OSGKeyboardExt/Services/AppGroupPersistor.swift +++ b/OSGKeyboardExt/Services/AppGroupPersistor.swift @@ -119,6 +119,11 @@ public struct AppGroupPersistor { userCatalog: store.agentUserSkillCatalog, uiLanguage: language ) + state.clipboardSkillCatalog = AIClipboardSkillCatalog.all( + officialCatalog: store.officialSkillCatalog, + userCatalog: store.agentUserSkillCatalog, + uiLanguage: language + ) } /// Cloud without ASR/LLM keys blocks the mic. Local ASR still works when diff --git a/OSGKeyboardExt/Views/AIKeyboardView.swift b/OSGKeyboardExt/Views/AIKeyboardView.swift index b545b19..a4f14c4 100644 --- a/OSGKeyboardExt/Views/AIKeyboardView.swift +++ b/OSGKeyboardExt/Views/AIKeyboardView.swift @@ -34,6 +34,7 @@ struct AIKeyboardView: View { static let skillCellSpacing: CGFloat = 10 static let pageDotSize: CGFloat = 5 static let skillPaginationBottomInset: CGFloat = 6 + static let maximumSemanticSkills = 5 } #if DEBUG @@ -109,7 +110,7 @@ struct AIKeyboardView: View { selectedSkillPage = 0 resetCarousel() } - .onChange(of: state.enabledClipboardSkills) { _, _ in + .onChange(of: state.clipboardSkillCatalog) { _, _ in selectedSkillPage = 0 resetCarousel() } @@ -583,13 +584,14 @@ struct AIKeyboardView: View { guard let newest = clipboardHistory.newestEntry, let snapshot = semanticRanking.snapshot, snapshot.entryID == newest.id else { - return state.enabledClipboardSkills + return [] } - return ClipboardSkillSemanticRanker.ranked( - skills: state.enabledClipboardSkills, + return ClipboardSkillSemanticRanker.recommended( + skills: state.clipboardSkillCatalog, sourceText: newest.text, analysis: snapshot.analysis, - uiLanguage: state.uiLanguage + uiLanguage: state.uiLanguage, + limit: Layout.maximumSemanticSkills ) } diff --git a/OSGKeyboardShared/Services/ClipboardSkillSemanticRanker.swift b/OSGKeyboardShared/Services/ClipboardSkillSemanticRanker.swift index 7b71ea8..d42e0b1 100644 --- a/OSGKeyboardShared/Services/ClipboardSkillSemanticRanker.swift +++ b/OSGKeyboardShared/Services/ClipboardSkillSemanticRanker.swift @@ -1,9 +1,9 @@ // ClipboardSkillSemanticRanker.swift // OSGKeyboard · Shared // -// Keeps the user's saved skill order as the stable fallback, then temporarily -// promotes relevant skills for the newest accepted clipboard entry. Analysis -// is local and ephemeral; neither labels nor reordered IDs are persisted. +// Selects skills from the complete catalog for the newest accepted clipboard +// entry. Analysis is local and ephemeral; installation state, user-managed +// ordering, labels, and recommendations are never persisted. import Combine import Foundation @@ -19,7 +19,40 @@ public enum ClipboardSkillSemanticRanker { uiLanguage: AppUILanguage ) -> [AIClipboardSkill] { guard skills.count > 1 else { return skills } + return sorted( + skills, + scores: relevanceScores( + sourceText: sourceText, + analysis: analysis, + uiLanguage: uiLanguage + ) + ) + } + /// Returns only skills supported by current semantic evidence. No matching + /// label produces no recommendation instead of a fixed fallback row. + public static func recommended( + skills: [AIClipboardSkill], + sourceText: String, + analysis: ClipboardSemanticAnalysis, + uiLanguage: AppUILanguage, + limit: Int + ) -> [AIClipboardSkill] { + guard limit > 0 else { return [] } + let scores = relevanceScores( + sourceText: sourceText, + analysis: analysis, + uiLanguage: uiLanguage + ) + let relevant = skills.filter { scores[$0.id, default: 0] > 0 } + return Array(sorted(relevant, scores: scores).prefix(limit)) + } + + private static func relevanceScores( + sourceText: String, + analysis: ClipboardSemanticAnalysis, + uiLanguage: AppUILanguage + ) -> [String: Int] { var scores: [String: Int] = [:] func boost(_ id: String, _ value: Int) { scores[id, default: 0] += value @@ -71,6 +104,8 @@ public enum ClipboardSkillSemanticRanker { if analysis.hasOrganizationName, analysis.task.isDetected || analysis.question.isDetected || analysis.invitation.isDetected { boost(AIClipboardSkillCatalog.businessReplyID, 125) + } else if analysis.hasOrganizationName { + boost(AIClipboardSkillCatalog.businessReplyID, 70) } if isListLike(sourceText) { @@ -84,7 +119,16 @@ public enum ClipboardSkillSemanticRanker { boost(AIClipboardSkillCatalog.extractConclusionsID, 125) boost(AIClipboardSkillCatalog.saveToNotesID, 85) } + if analysis.sentiment == .positive { + boost(AIClipboardSkillCatalog.replyID, 45) + } + return scores + } + private static func sorted( + _ skills: [AIClipboardSkill], + scores: [String: Int] + ) -> [AIClipboardSkill] { let baseline = Dictionary( uniqueKeysWithValues: skills.enumerated().map { ($0.element.id, $0.offset) } ) diff --git a/OSGKeyboardShared/Services/KeyboardState.swift b/OSGKeyboardShared/Services/KeyboardState.swift index 0ba6415..2be45d8 100644 --- a/OSGKeyboardShared/Services/KeyboardState.swift +++ b/OSGKeyboardShared/Services/KeyboardState.swift @@ -150,6 +150,10 @@ public final class KeyboardState: ObservableObject { /// ensures Darwin updates publish prompt/name changes even when IDs stay unchanged. @Published public var enabledClipboardSkills: [AIClipboardSkill] = AIClipboardSkillCatalog.visible() + /// Complete resolved catalog used for semantic recommendations. Keyboard + /// visibility is independent from the Skills page installation state. + @Published public var clipboardSkillCatalog: [AIClipboardSkill] = + AIClipboardSkillCatalog.catalog /// Export skills whose companion Shortcut setup the user confirmed. @Published public var confirmedClipboardShortcutIDs: [String] = [] /// App language captured with the same App Group snapshot as skill copy. diff --git a/OSGKeyboardTests/ClipboardSkillSemanticRankerTests.swift b/OSGKeyboardTests/ClipboardSkillSemanticRankerTests.swift index 0a0cc77..e550814 100644 --- a/OSGKeyboardTests/ClipboardSkillSemanticRankerTests.swift +++ b/OSGKeyboardTests/ClipboardSkillSemanticRankerTests.swift @@ -119,6 +119,32 @@ final class ClipboardSkillSemanticRankerTests: XCTestCase { XCTAssertEqual(ranked, baseline) } + func testRecommendationsSelectOnlySemanticallyRelevantSkills() { + let recommendations = ClipboardSkillSemanticRanker.recommended( + skills: AIClipboardSkillCatalog.catalog, + sourceText: "北京市朝阳区望京街 10 号,到了给我电话。", + analysis: analysis(hasAddress: true), + uiLanguage: .chinese, + limit: 5 + ).map(\.id) + + XCTAssertEqual(recommendations, [AIClipboardSkillCatalog.navigateID]) + XCTAssertFalse(recommendations.contains(AIClipboardSkillCatalog.summarizeID)) + XCTAssertFalse(recommendations.contains(AIClipboardSkillCatalog.translateID)) + } + + func testRecommendationsStayEmptyWhenNoSemanticLabelMatches() { + let recommendations = ClipboardSkillSemanticRanker.recommended( + skills: AIClipboardSkillCatalog.catalog, + sourceText: "知道了", + analysis: analysis(), + uiLanguage: .chinese, + limit: 5 + ).map(\.id) + + XCTAssertTrue(recommendations.isEmpty) + } + private func rank( text: String, analysis: ClipboardSemanticAnalysis diff --git a/Scripts/install-xcodegen-ci.sh b/Scripts/install-xcodegen-ci.sh index 9adf1ed..657c9c6 100755 --- a/Scripts/install-xcodegen-ci.sh +++ b/Scripts/install-xcodegen-ci.sh @@ -6,6 +6,7 @@ VERSION="2.43.0" SHA256="a4847ed77d3341a4d24049bc4424a3babca4c94ff1dcaaee923eaca2b32c678f" DESTINATION="${RUNNER_TEMP:?RUNNER_TEMP is required}/xcodegen-$VERSION" ARCHIVE="$RUNNER_TEMP/xcodegen-$VERSION.zip" +INSTALL_ROOT="$DESTINATION/xcodegen" curl -fsSL --retry 3 --retry-all-errors --retry-delay 2 \ --connect-timeout 15 --max-time 120 \ @@ -16,6 +17,6 @@ rm -rf "$DESTINATION" mkdir -p "$DESTINATION" unzip -q "$ARCHIVE" -d "$DESTINATION" -test -x "$DESTINATION/bin/xcodegen" -echo "$DESTINATION/bin" >> "$GITHUB_PATH" -"$DESTINATION/bin/xcodegen" --version +test -x "$INSTALL_ROOT/bin/xcodegen" +echo "$INSTALL_ROOT/bin" >> "$GITHUB_PATH" +"$INSTALL_ROOT/bin/xcodegen" --version