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.
This commit is contained in:
Rocky
2026-08-22 16:46:37 +08:00
parent 386ba140f7
commit fe8f657c8d
8 changed files with 95 additions and 13 deletions
+1 -1
View File
@@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### 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、人名和组织名标签,全程不上传剪贴板原文。 - **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 附加风格,并在保存前强制检查。 - **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 ### Changed
@@ -402,7 +402,7 @@ final class AIKeyboardCoordinator {
} }
private func resolvedSkill(id: String) -> AIClipboardSkill? { 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. /// Parse an export skill. Empty in-keyboard tip, stay in the host app.
@@ -119,6 +119,11 @@ public struct AppGroupPersistor {
userCatalog: store.agentUserSkillCatalog, userCatalog: store.agentUserSkillCatalog,
uiLanguage: language 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 /// Cloud without ASR/LLM keys blocks the mic. Local ASR still works when
+7 -5
View File
@@ -34,6 +34,7 @@ struct AIKeyboardView: View {
static let skillCellSpacing: CGFloat = 10 static let skillCellSpacing: CGFloat = 10
static let pageDotSize: CGFloat = 5 static let pageDotSize: CGFloat = 5
static let skillPaginationBottomInset: CGFloat = 6 static let skillPaginationBottomInset: CGFloat = 6
static let maximumSemanticSkills = 5
} }
#if DEBUG #if DEBUG
@@ -109,7 +110,7 @@ struct AIKeyboardView: View {
selectedSkillPage = 0 selectedSkillPage = 0
resetCarousel() resetCarousel()
} }
.onChange(of: state.enabledClipboardSkills) { _, _ in .onChange(of: state.clipboardSkillCatalog) { _, _ in
selectedSkillPage = 0 selectedSkillPage = 0
resetCarousel() resetCarousel()
} }
@@ -583,13 +584,14 @@ struct AIKeyboardView: View {
guard let newest = clipboardHistory.newestEntry, guard let newest = clipboardHistory.newestEntry,
let snapshot = semanticRanking.snapshot, let snapshot = semanticRanking.snapshot,
snapshot.entryID == newest.id else { snapshot.entryID == newest.id else {
return state.enabledClipboardSkills return []
} }
return ClipboardSkillSemanticRanker.ranked( return ClipboardSkillSemanticRanker.recommended(
skills: state.enabledClipboardSkills, skills: state.clipboardSkillCatalog,
sourceText: newest.text, sourceText: newest.text,
analysis: snapshot.analysis, analysis: snapshot.analysis,
uiLanguage: state.uiLanguage uiLanguage: state.uiLanguage,
limit: Layout.maximumSemanticSkills
) )
} }
@@ -1,9 +1,9 @@
// ClipboardSkillSemanticRanker.swift // ClipboardSkillSemanticRanker.swift
// OSGKeyboard · Shared // OSGKeyboard · Shared
// //
// Keeps the user's saved skill order as the stable fallback, then temporarily // Selects skills from the complete catalog for the newest accepted clipboard
// promotes relevant skills for the newest accepted clipboard entry. Analysis // entry. Analysis is local and ephemeral; installation state, user-managed
// is local and ephemeral; neither labels nor reordered IDs are persisted. // ordering, labels, and recommendations are never persisted.
import Combine import Combine
import Foundation import Foundation
@@ -19,7 +19,40 @@ public enum ClipboardSkillSemanticRanker {
uiLanguage: AppUILanguage uiLanguage: AppUILanguage
) -> [AIClipboardSkill] { ) -> [AIClipboardSkill] {
guard skills.count > 1 else { return skills } 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] = [:] var scores: [String: Int] = [:]
func boost(_ id: String, _ value: Int) { func boost(_ id: String, _ value: Int) {
scores[id, default: 0] += value scores[id, default: 0] += value
@@ -71,6 +104,8 @@ public enum ClipboardSkillSemanticRanker {
if analysis.hasOrganizationName, if analysis.hasOrganizationName,
analysis.task.isDetected || analysis.question.isDetected || analysis.invitation.isDetected { analysis.task.isDetected || analysis.question.isDetected || analysis.invitation.isDetected {
boost(AIClipboardSkillCatalog.businessReplyID, 125) boost(AIClipboardSkillCatalog.businessReplyID, 125)
} else if analysis.hasOrganizationName {
boost(AIClipboardSkillCatalog.businessReplyID, 70)
} }
if isListLike(sourceText) { if isListLike(sourceText) {
@@ -84,7 +119,16 @@ public enum ClipboardSkillSemanticRanker {
boost(AIClipboardSkillCatalog.extractConclusionsID, 125) boost(AIClipboardSkillCatalog.extractConclusionsID, 125)
boost(AIClipboardSkillCatalog.saveToNotesID, 85) 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( let baseline = Dictionary(
uniqueKeysWithValues: skills.enumerated().map { ($0.element.id, $0.offset) } uniqueKeysWithValues: skills.enumerated().map { ($0.element.id, $0.offset) }
) )
@@ -150,6 +150,10 @@ public final class KeyboardState: ObservableObject {
/// ensures Darwin updates publish prompt/name changes even when IDs stay unchanged. /// ensures Darwin updates publish prompt/name changes even when IDs stay unchanged.
@Published public var enabledClipboardSkills: [AIClipboardSkill] = @Published public var enabledClipboardSkills: [AIClipboardSkill] =
AIClipboardSkillCatalog.visible() 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. /// Export skills whose companion Shortcut setup the user confirmed.
@Published public var confirmedClipboardShortcutIDs: [String] = [] @Published public var confirmedClipboardShortcutIDs: [String] = []
/// App language captured with the same App Group snapshot as skill copy. /// App language captured with the same App Group snapshot as skill copy.
@@ -119,6 +119,32 @@ final class ClipboardSkillSemanticRankerTests: XCTestCase {
XCTAssertEqual(ranked, baseline) 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( private func rank(
text: String, text: String,
analysis: ClipboardSemanticAnalysis analysis: ClipboardSemanticAnalysis
+4 -3
View File
@@ -6,6 +6,7 @@ VERSION="2.43.0"
SHA256="a4847ed77d3341a4d24049bc4424a3babca4c94ff1dcaaee923eaca2b32c678f" SHA256="a4847ed77d3341a4d24049bc4424a3babca4c94ff1dcaaee923eaca2b32c678f"
DESTINATION="${RUNNER_TEMP:?RUNNER_TEMP is required}/xcodegen-$VERSION" DESTINATION="${RUNNER_TEMP:?RUNNER_TEMP is required}/xcodegen-$VERSION"
ARCHIVE="$RUNNER_TEMP/xcodegen-$VERSION.zip" ARCHIVE="$RUNNER_TEMP/xcodegen-$VERSION.zip"
INSTALL_ROOT="$DESTINATION/xcodegen"
curl -fsSL --retry 3 --retry-all-errors --retry-delay 2 \ curl -fsSL --retry 3 --retry-all-errors --retry-delay 2 \
--connect-timeout 15 --max-time 120 \ --connect-timeout 15 --max-time 120 \
@@ -16,6 +17,6 @@ rm -rf "$DESTINATION"
mkdir -p "$DESTINATION" mkdir -p "$DESTINATION"
unzip -q "$ARCHIVE" -d "$DESTINATION" unzip -q "$ARCHIVE" -d "$DESTINATION"
test -x "$DESTINATION/bin/xcodegen" test -x "$INSTALL_ROOT/bin/xcodegen"
echo "$DESTINATION/bin" >> "$GITHUB_PATH" echo "$INSTALL_ROOT/bin" >> "$GITHUB_PATH"
"$DESTINATION/bin/xcodegen" --version "$INSTALL_ROOT/bin/xcodegen" --version