feat(keyboard): add custom skills plus events and navigate Shortcuts
Ship user-defined Shortcut skills, companion Events/Navigate recipes, shared skill/style card chrome, and bump the build to 69.
This commit is contained in:
@@ -70,6 +70,10 @@
|
||||
<string>skype</string>
|
||||
<string>zoomus</string>
|
||||
<string>shortcuts</string>
|
||||
<string>iosamap</string>
|
||||
<string>amapuri</string>
|
||||
<string>baidumap</string>
|
||||
<string>maps</string>
|
||||
<string>things</string>
|
||||
<string>todoist</string>
|
||||
<string>evernote</string>
|
||||
|
||||
@@ -36,6 +36,8 @@ struct OSGKeyboardApp: App {
|
||||
EditDemoView()
|
||||
} else if ProcessInfo.processInfo.arguments.contains("--ai-demo") {
|
||||
AIKeyboardDemoView()
|
||||
} else if ProcessInfo.processInfo.arguments.contains("--ai-skills-demo") {
|
||||
AIClipboardSkillLayoutDemoView()
|
||||
} else if ProcessInfo.processInfo.arguments.contains("--clipboard-demo") {
|
||||
ClipboardHistoryDemoView()
|
||||
} else if ProcessInfo.processInfo.arguments.contains("--edit-pager-ui-test") {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -10,8 +10,6 @@ import UIKit
|
||||
import OSGKeyboardShared
|
||||
|
||||
enum AIAgentShortcutInstaller {
|
||||
static let bundledResourceName = "OSGExtractTodos"
|
||||
|
||||
@MainActor
|
||||
static func openInstallPage(for skill: AIClipboardSkill) {
|
||||
if let shareURL = skill.shortcutICloudURL,
|
||||
@@ -19,17 +17,19 @@ enum AIAgentShortcutInstaller {
|
||||
UIApplication.shared.open(installURL)
|
||||
return
|
||||
}
|
||||
openBundledShortcut()
|
||||
openBundledShortcut(for: skill)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private static func openBundledShortcut() {
|
||||
guard let bundled = Bundle.main.url(
|
||||
forResource: bundledResourceName,
|
||||
withExtension: "shortcut"
|
||||
) else { return }
|
||||
private static func openBundledShortcut(for skill: AIClipboardSkill) {
|
||||
guard let resource = skill.shortcutResourceName,
|
||||
let bundled = Bundle.main.url(
|
||||
forResource: resource,
|
||||
withExtension: "shortcut"
|
||||
) else { return }
|
||||
let fileName = skill.shortcutName ?? resource
|
||||
let tmp = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent("\(AIClipboardSkillCatalog.extractTodosShortcutName).shortcut")
|
||||
.appendingPathComponent("\(fileName).shortcut")
|
||||
try? FileManager.default.removeItem(at: tmp)
|
||||
do {
|
||||
try FileManager.default.copyItem(at: bundled, to: tmp)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// AIAgentShortcutRunner.swift
|
||||
// OSGKeyboard · Main App
|
||||
//
|
||||
// Consumes the keyboard's pending extract-todos payload and opens the
|
||||
// Consumes the keyboard's pending export-skill payload and opens the
|
||||
// companion Shortcut. Release builds stay in Shortcuts. DEBUG builds add
|
||||
// x-callback URLs so Console can record success / error / cancel.
|
||||
|
||||
@@ -13,15 +13,22 @@ enum AIAgentShortcutRunner {
|
||||
static func runPendingIfNeeded() {
|
||||
AIAgentShortcutRun.trace("host.runPending begin")
|
||||
guard let payload = AppGroupStore().consumePendingShortcutRun() else { return }
|
||||
guard let skill = AIClipboardSkillCatalog.skill(id: payload.skillID),
|
||||
let catalog = AppGroupStore().agentUserSkillCatalog
|
||||
guard let skill = AIClipboardSkillCatalog.skill(id: payload.skillID, userCatalog: catalog),
|
||||
let name = skill.shortcutName else {
|
||||
AIAgentShortcutRun.trace(
|
||||
"host.runPending skip unknownSkill=\(payload.skillID)"
|
||||
)
|
||||
return
|
||||
}
|
||||
AIAgentShortcutRun.traceBody("host.titlesToShortcut", payload.joinedTitles)
|
||||
guard let url = shortcutsURL(name: name, text: payload.joinedTitles) else {
|
||||
guard let text = shortcutText(for: skill, payload: payload) else {
|
||||
AIAgentShortcutRun.trace(
|
||||
"host.runPending skip textBuildFailed skill=\(payload.skillID)"
|
||||
)
|
||||
return
|
||||
}
|
||||
AIAgentShortcutRun.traceBody("host.titlesToShortcut", text)
|
||||
guard let url = shortcutsURL(name: name, text: text) else {
|
||||
AIAgentShortcutRun.trace("host.runPending skip URLBuildFailed name=\(name)")
|
||||
return
|
||||
}
|
||||
@@ -55,6 +62,21 @@ enum AIAgentShortcutRunner {
|
||||
)
|
||||
}
|
||||
|
||||
/// Navigate: pick 高德 → 百度 → Apple Maps, pass that URL to the Shortcut.
|
||||
/// Other export skills send the parsed lines unchanged.
|
||||
private static func shortcutText(
|
||||
for skill: AIClipboardSkill,
|
||||
payload: AIAgentShortcutRunPayload
|
||||
) -> String? {
|
||||
if skill.id == AIClipboardSkillCatalog.navigateID {
|
||||
return AIMapNavigation.shortcutInput(
|
||||
from: payload.joinedTitles,
|
||||
canOpen: { UIApplication.shared.canOpenURL($0) }
|
||||
)
|
||||
}
|
||||
return payload.joinedTitles
|
||||
}
|
||||
|
||||
private static func shortcutsURL(name: String, text: String) -> URL? {
|
||||
#if DEBUG
|
||||
return AIAgentShortcutRun.shortcutsRunURL(
|
||||
|
||||
@@ -1358,13 +1358,15 @@ final class FlowSessionManager: ObservableObject {
|
||||
let utteranceId = command.utteranceId
|
||||
let commandSeq = command.commandSeq
|
||||
let sessionId = command.sessionId
|
||||
let thinkingEnabled = command.aiThinkingEnabled ?? true
|
||||
Task { @MainActor [weak self] in
|
||||
await self?.answerPrefilledAIQuestion(
|
||||
question: question,
|
||||
conversationID: conversationID,
|
||||
sessionId: sessionId,
|
||||
utteranceId: utteranceId,
|
||||
commandSeq: commandSeq
|
||||
commandSeq: commandSeq,
|
||||
thinkingEnabled: thinkingEnabled
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1385,7 +1387,8 @@ final class FlowSessionManager: ObservableObject {
|
||||
conversationID: UUID?,
|
||||
sessionId: UUID,
|
||||
utteranceId: UUID,
|
||||
commandSeq: Int64
|
||||
commandSeq: Int64,
|
||||
thinkingEnabled: Bool
|
||||
) async {
|
||||
// Hint-card / prefilled questions never go through `finalizeUtterance`,
|
||||
// so this path must drop the processing gate itself. Leaving it set
|
||||
@@ -1415,7 +1418,8 @@ final class FlowSessionManager: ObservableObject {
|
||||
do {
|
||||
let service = try AIQuestionService.configured(
|
||||
store: pipelineStore,
|
||||
conversations: aiConversations
|
||||
conversations: aiConversations,
|
||||
thinkingEnabled: thinkingEnabled
|
||||
)
|
||||
aiAnswerStreamThrottle = AIAnswerStreamThrottle()
|
||||
let answer = try await service.answer(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,136 @@
|
||||
// AIClipboardSkillLayoutDemoView.swift
|
||||
// OSGKeyboard · Main App (DEBUG-only)
|
||||
//
|
||||
// Interactive layout preview of AI Agent clipboard skills on the real
|
||||
// `AIKeyboardView`. Launch with `--ai-skills-demo` and optional
|
||||
// `--skills-count=N` (1...8).
|
||||
|
||||
#if DEBUG
|
||||
import SwiftUI
|
||||
import OSGKeyboardShared
|
||||
|
||||
struct AIClipboardSkillLayoutDemoView: View {
|
||||
@StateObject private var state = KeyboardState()
|
||||
@StateObject private var typing = TypingSessionController()
|
||||
@State private var count: Int = Self.initialCount
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
controls
|
||||
Spacer(minLength: 0)
|
||||
AIKeyboardView(
|
||||
state: state,
|
||||
typing: typing,
|
||||
onInsert: { _ in }
|
||||
)
|
||||
.background(Palette.light.background)
|
||||
}
|
||||
.background(Color(red: 0.06, green: 0.06, blue: 0.07).ignoresSafeArea())
|
||||
.environment(\.locale, Locale(identifier: "zh-Hans"))
|
||||
.preferredColorScheme(.light)
|
||||
.onAppear { apply(count) }
|
||||
.onChange(of: count) { _, newValue in apply(newValue) }
|
||||
}
|
||||
|
||||
private var controls: some View {
|
||||
VStack(spacing: 10) {
|
||||
Text("技能布局预览")
|
||||
.font(.headline)
|
||||
.foregroundStyle(.white)
|
||||
HStack(spacing: 16) {
|
||||
Button {
|
||||
count = max(1, count - 1)
|
||||
} label: {
|
||||
Image(systemName: "minus.circle.fill")
|
||||
.font(.system(size: 28))
|
||||
}
|
||||
Text("\(count) 个")
|
||||
.font(.title2.monospacedDigit().weight(.semibold))
|
||||
.foregroundStyle(.white)
|
||||
.frame(minWidth: 72)
|
||||
Button {
|
||||
count = min(8, count + 1)
|
||||
} label: {
|
||||
Image(systemName: "plus.circle.fill")
|
||||
.font(.system(size: 28))
|
||||
}
|
||||
}
|
||||
.foregroundStyle(.green)
|
||||
HStack(spacing: 8) {
|
||||
ForEach([3, 5, 8], id: \.self) { n in
|
||||
Button("\(n)") {
|
||||
count = n
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(count == n ? .green : .gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.top, 56)
|
||||
.padding(.bottom, 16)
|
||||
}
|
||||
|
||||
private func apply(_ count: Int) {
|
||||
let clamped = min(max(count, 1), 8)
|
||||
AIKeyboardView.debugPreviewSkills = Self.previewSkills(count: clamped)
|
||||
state.surface = .ai
|
||||
state.aiServiceAvailable = true
|
||||
state.micDisabled = false
|
||||
state.layoutWidth = 390
|
||||
state.usesIPadLayoutMetrics = false
|
||||
state.clipboardHistoryEnabled = true
|
||||
state.enabledClipboardSkillIDs = Array(
|
||||
AIClipboardSkillCatalog.catalog.map(\.id).prefix(clamped)
|
||||
)
|
||||
state.aiSession.enter()
|
||||
}
|
||||
|
||||
private static var initialCount: Int {
|
||||
let prefix = "--skills-count="
|
||||
if let arg = ProcessInfo.processInfo.arguments.first(where: { $0.hasPrefix(prefix) }),
|
||||
let value = Int(arg.dropFirst(prefix.count)),
|
||||
(1...8).contains(value) {
|
||||
return value
|
||||
}
|
||||
return 5
|
||||
}
|
||||
|
||||
private static func previewSkills(count: Int) -> [AIClipboardSkill] {
|
||||
let catalog = AIClipboardSkillCatalog.catalog
|
||||
if count <= catalog.count {
|
||||
return Array(catalog.prefix(count))
|
||||
}
|
||||
var extras: [AIClipboardSkill] = [
|
||||
AIClipboardSkill(
|
||||
id: "preview-polish",
|
||||
systemImage: "wand.and.stars",
|
||||
titleKey: "keyboard.ai.skill.previewPolish",
|
||||
cardTitleKey: "skills.reply.name",
|
||||
descriptionKey: "skills.reply.description",
|
||||
kind: .transform,
|
||||
isDefault: false
|
||||
),
|
||||
AIClipboardSkill(
|
||||
id: "preview-ideas",
|
||||
systemImage: "lightbulb.fill",
|
||||
titleKey: "keyboard.ai.skill.previewIdeas",
|
||||
cardTitleKey: "skills.summarize.name",
|
||||
descriptionKey: "skills.summarize.description",
|
||||
kind: .transform,
|
||||
isDefault: false
|
||||
),
|
||||
AIClipboardSkill(
|
||||
id: "preview-tone",
|
||||
systemImage: "theatermasks.fill",
|
||||
titleKey: "keyboard.ai.skill.previewTone",
|
||||
cardTitleKey: "skills.translate.name",
|
||||
descriptionKey: "skills.translate.description",
|
||||
kind: .transform,
|
||||
isDefault: false
|
||||
),
|
||||
]
|
||||
extras = Array(extras.prefix(count - catalog.count))
|
||||
return catalog + extras
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,31 @@
|
||||
// CatalogCardChrome.swift
|
||||
// OSGKeyboard · Main App
|
||||
//
|
||||
// Shared edit / selected badges for style and skill cards.
|
||||
|
||||
import SwiftUI
|
||||
import OSGKeyboardShared
|
||||
|
||||
enum CatalogCardChrome {
|
||||
static let badgeSize: CGFloat = 30
|
||||
static let editIconSize: CGFloat = 15
|
||||
static let checkIconSize: CGFloat = 21
|
||||
static let editHitSize: CGFloat = 44
|
||||
|
||||
static func editIcon(palette: ThemePalette) -> some View {
|
||||
Image(systemName: "pencil")
|
||||
.font(.system(size: editIconSize, weight: .semibold))
|
||||
.foregroundStyle(palette.textSecondary)
|
||||
.frame(width: badgeSize, height: badgeSize)
|
||||
.background(palette.background.opacity(0.75), in: Circle())
|
||||
}
|
||||
|
||||
static func checkIcon(palette: ThemePalette) -> some View {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.font(.system(size: checkIconSize, weight: .semibold))
|
||||
.foregroundStyle(palette.accent)
|
||||
.padding(Spacing.sm)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomTrailing)
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
}
|
||||
@@ -142,26 +142,14 @@ struct PolishStylesView: View {
|
||||
editingPack = pack
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: pack.kind == .builtin ? "eye" : "pencil")
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
.foregroundStyle(palette.textSecondary)
|
||||
.frame(width: 30, height: 30)
|
||||
.background(palette.background.opacity(0.75), in: Circle())
|
||||
CatalogCardChrome.editIcon(palette: palette)
|
||||
}
|
||||
.padding(Spacing.sm)
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel(
|
||||
Text(pack.kind == .builtin ? "polishStyles.viewPrompt" : "polishStyles.edit")
|
||||
)
|
||||
.accessibilityLabel(Text("polishStyles.edit"))
|
||||
|
||||
if isSelected {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.font(.system(size: 21, weight: .semibold))
|
||||
.foregroundStyle(palette.accent)
|
||||
.background(Color.white, in: Circle())
|
||||
.padding(Spacing.sm)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomTrailing)
|
||||
.allowsHitTesting(false)
|
||||
CatalogCardChrome.checkIcon(palette: palette)
|
||||
}
|
||||
}
|
||||
.background(
|
||||
|
||||
@@ -435,7 +435,10 @@
|
||||
"skills.enabled.section" = "In use (%d/%d)";
|
||||
"skills.enabled.empty" = "No skills on the keyboard. Turn one on from the list below.";
|
||||
"skills.available.section" = "Available";
|
||||
"skills.clipboardHistory.banner" = "Turn on Clipboard History in Settings to use these skills after you copy text.";
|
||||
"skills.clipboard.guide.title" = "Clipboard access needed";
|
||||
"skills.clipboard.guide.body" = "Turn on Clipboard History in the app, then allow paste access in iOS Settings, so these skills can run after you copy text.";
|
||||
"skills.clipboard.guide.openAppSettings" = "Open Clipboard settings";
|
||||
"skills.clipboard.guide.openSystemSettings" = "Open iOS Settings";
|
||||
"skills.detail.title" = "Skill";
|
||||
"skills.badge.default" = "Default skill";
|
||||
"skills.action.turnOff" = "Turn off";
|
||||
@@ -452,9 +455,43 @@
|
||||
"skills.translate.description" = "Translate the copied text using your keyboard language setting.";
|
||||
"skills.extractTodos.name" = "Extract tasks";
|
||||
"skills.extractTodos.description" = "Extract to-dos from copied text into Reminders.";
|
||||
"skills.extractEvents.name" = "Extract events";
|
||||
"skills.extractEvents.description" = "Extract events from copied text into Calendar.";
|
||||
"skills.navigate.name" = "Navigate";
|
||||
"skills.navigate.description" = "Find an address in the copied text and start driving directions. Uses Amap if installed, then Baidu Maps, then Apple Maps.";
|
||||
"skills.install.lead" = "Add it, then confirm. Don’t rename it.";
|
||||
"skills.install.openShortcuts" = "Add Shortcut";
|
||||
"skills.install.confirmAdded" = "I've added it";
|
||||
"skills.add" = "Add skill";
|
||||
"skills.edit" = "Edit skill";
|
||||
"skills.custom.description" = "Custom clipboard skill";
|
||||
"skills.editor.name" = "Name";
|
||||
"skills.editor.namePlaceholder" = "Skill name";
|
||||
"skills.editor.summary" = "About";
|
||||
"skills.editor.summaryPlaceholder" = "Shown on the skill card";
|
||||
"skills.editor.icon" = "Icon";
|
||||
"skills.editor.iconChoose" = "Choose SF Symbol";
|
||||
"skills.editor.iconSearch" = "Search symbols";
|
||||
"skills.editor.prompt" = "Processing prompt";
|
||||
"skills.editor.thinking" = "Thinking";
|
||||
"skills.editor.thinkingHint" = "Off by default. Turn on only when you want slower, deeper reasoning for this skill.";
|
||||
"skills.editor.shortcut" = "Shortcut";
|
||||
"skills.editor.linkPlaceholder" = "https://www.icloud.com/shortcuts/…";
|
||||
"skills.editor.shortcutNamePlaceholder" = "Shortcut name (can differ from the skill name)";
|
||||
"skills.editor.shortcutHint" = "Paste an iCloud share link. The published Shortcut name is filled in automatically and you can change it. Don’t rename it in the Shortcuts app after adding.";
|
||||
"skills.editor.lookingUp" = "Looking up Shortcut name…";
|
||||
"skills.editor.resolvedName" = "Will run: %@";
|
||||
"skills.editor.lookupFailed" = "Couldn’t read the Shortcut name. Check the link, or type the name yourself.";
|
||||
"skills.delete.title" = "Delete this skill?";
|
||||
"skills.delete.message" = "It will be removed from this device. The Shortcut in the Shortcuts app is not deleted.";
|
||||
"skills.error.title" = "Couldn’t save skill";
|
||||
"skills.error.emptyName" = "Enter a skill name.";
|
||||
"skills.error.emptyPrompt" = "The prompt cannot be empty.";
|
||||
"skills.error.emptyShortcutName" = "Enter the Shortcut name used to run it.";
|
||||
"skills.error.invalidLink" = "Enter a valid iCloud Shortcuts link.";
|
||||
"skills.error.emptyIcon" = "Choose an icon.";
|
||||
"skills.error.promptTooLong" = "The prompt can contain up to 6,000 characters.";
|
||||
"skills.error.generic" = "Try again.";
|
||||
|
||||
/* Polish style packs */
|
||||
"polishStyles.title" = "Polish styles";
|
||||
|
||||
@@ -434,7 +434,10 @@
|
||||
"skills.enabled.section" = "使用中(%d/%d)";
|
||||
"skills.enabled.empty" = "键盘上还没有技能。从下方列表打开一个即可。";
|
||||
"skills.available.section" = "可添加";
|
||||
"skills.clipboardHistory.banner" = "请先在设置中打开剪贴板历史,复制文字后才能在键盘上使用这些技能。";
|
||||
"skills.clipboard.guide.title" = "需要剪贴板权限";
|
||||
"skills.clipboard.guide.body" = "请先在 App 里打开剪贴板历史,再到系统设置中允许粘贴授权,复制后才能使用这些技能。";
|
||||
"skills.clipboard.guide.openAppSettings" = "打开剪贴板设置";
|
||||
"skills.clipboard.guide.openSystemSettings" = "打开系统设置";
|
||||
"skills.detail.title" = "技能";
|
||||
"skills.badge.default" = "默认技能";
|
||||
"skills.action.turnOff" = "关闭";
|
||||
@@ -451,9 +454,43 @@
|
||||
"skills.translate.description" = "按键盘目标语言翻译复制的内容。";
|
||||
"skills.extractTodos.name" = "提取待办";
|
||||
"skills.extractTodos.description" = "从复制内容提取待办,写入提醒事项。";
|
||||
"skills.extractEvents.name" = "提取日程";
|
||||
"skills.extractEvents.description" = "从复制内容提取日程,写入日历。";
|
||||
"skills.navigate.name" = "导航";
|
||||
"skills.navigate.description" = "从复制内容识别地址并开始驾车导航。优先高德,其次百度,最后 Apple 地图。";
|
||||
"skills.install.lead" = "添加后点「我已添加」,请勿改名。";
|
||||
"skills.install.openShortcuts" = "添加捷径";
|
||||
"skills.install.confirmAdded" = "我已添加";
|
||||
"skills.add" = "添加技能";
|
||||
"skills.edit" = "编辑技能";
|
||||
"skills.custom.description" = "自定义剪贴板技能";
|
||||
"skills.editor.name" = "名称";
|
||||
"skills.editor.namePlaceholder" = "技能名称";
|
||||
"skills.editor.summary" = "介绍";
|
||||
"skills.editor.summaryPlaceholder" = "显示在技能卡片上";
|
||||
"skills.editor.icon" = "图标";
|
||||
"skills.editor.iconChoose" = "选择 SF Symbol";
|
||||
"skills.editor.iconSearch" = "搜索符号";
|
||||
"skills.editor.prompt" = "文本处理提示词";
|
||||
"skills.editor.thinking" = "思考";
|
||||
"skills.editor.thinkingHint" = "默认关闭。仅在需要该技能更慢、更深的推理时开启。";
|
||||
"skills.editor.shortcut" = "捷径";
|
||||
"skills.editor.linkPlaceholder" = "https://www.icloud.com/shortcuts/…";
|
||||
"skills.editor.shortcutNamePlaceholder" = "捷径名称(可与技能名称不同)";
|
||||
"skills.editor.shortcutHint" = "粘贴 iCloud 分享链接。发布名称会自动填入,也可以自行修改。添加到「快捷指令」后请勿改名。";
|
||||
"skills.editor.lookingUp" = "正在读取捷径名称…";
|
||||
"skills.editor.resolvedName" = "将运行:%@";
|
||||
"skills.editor.lookupFailed" = "无法读取捷径名称。请检查链接,或手动填写名称。";
|
||||
"skills.delete.title" = "删除此技能?";
|
||||
"skills.delete.message" = "会从本机移除。快捷指令 App 里的捷径不会被删除。";
|
||||
"skills.error.title" = "无法保存技能";
|
||||
"skills.error.emptyName" = "请输入技能名称。";
|
||||
"skills.error.emptyPrompt" = "提示词不能为空。";
|
||||
"skills.error.emptyShortcutName" = "请填写用于运行的捷径名称。";
|
||||
"skills.error.invalidLink" = "请输入有效的 iCloud 捷径链接。";
|
||||
"skills.error.emptyIcon" = "请选择图标。";
|
||||
"skills.error.promptTooLong" = "提示词最多 6,000 字。";
|
||||
"skills.error.generic" = "请再试一次。";
|
||||
|
||||
/* 润色风格包 */
|
||||
"polishStyles.title" = "润色风格";
|
||||
|
||||
Reference in New Issue
Block a user