diff --git a/AGENTS.md b/AGENTS.md index 4c53767..036276b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,7 +14,7 @@ writing commit messages that will ship to users. ### Version format -The current source-of-truth version is **2.1.0 (build 110)**. Releases use stable SemVer: +The current source-of-truth version is **2.1.6 (build 131)**. Releases use stable SemVer: | Field | File | Rule | |-------|------|------| diff --git a/CHANGELOG.md b/CHANGELOG.md index 46635a2..981c35f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,12 +7,64 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed +- **Auto-return to the previous app in every build**: host-app identification used to compile only in Debug, so TestFlight and App Store builds never knew which app you were typing in and could not jump back after starting a voice session. It now runs in Debug, TestFlight, and Release; if the system does not expose the host (iOS earlier than 26.4), OSG simply stays open as before. / **所有版本都能自动跳回原 App**:此前识别宿主 App 的逻辑只在 Debug 构建中编译,TestFlight 和 App Store 版本不知道你正在哪个 App 里输入,启动语音会话后无法自动跳回。现在 Debug、TestFlight、正式版都会识别;系统不提供宿主信息时(低于 iOS 26.4)与之前一样停留在 OSG。 + +## [2.1.6] - 2026-10-27 + +### Added +- **Translate to your target language**: when the copied text is in your device language and a translation target language is set, the skill bar shows a "Translate " button (for example "翻译英语" / "To Japanese") that translates the copy into that target. It is hidden when translation is off or the target is the same language as the text. / **翻译为目标语言**:复制的文字是设备语言、且已设置翻译目标语言时,技能栏会出现「翻译 XX」按钮(如「翻译英语」/「To Japanese」),点击后把内容译成目标语言;翻译关闭,或目标语言与原文相同时不显示。 +- **Polish foreign-language text**: when the copied text is in another language, the skill bar adds a "Polish" button that fixes grammar, improves wording, and polishes the text in one pass while keeping the original language. Automatic translation of foreign text keeps using the existing "auto translate on keyboard open" switch. / **优化外语文本**:复制的文字是其他语言时,技能栏新增「优化」按钮,一次完成语法纠错、措辞优化和润色,结果保持原语言。外语内容的自动翻译仍使用现有的「打开键盘自动翻译」开关。 + ### Changed -- **Messaging shift to AI voice keyboard / voice double / agentic skills**: README and the product page now lead with “Say it. It’s typed. Speaks like you. Acts for you.” — the personalization story (learned style, three-tone replies) moves ahead of typing, clipboard skills are framed as agentic actions, and typing becomes the fallback capability. / **宣传定位转向 AI 语音键盘 / 嘴替 / Agentic 技能**:README 与产品页改用「开口即文字。会说你的话,会办你的事。」定位;个人化卖点(专属风格、三语气回复)提前,剪贴板技能表述升级为 Agentic 行动,打字退为兜底能力。 -- **Product page redesign**: rebuild the GitHub Pages home as a dark product stage with the new Icon Composer app icon, the three-device hero visual, and real on-device videos and UI cards replacing hand-drawn mockups; add 2.1 selling points (personal style, three-tone replies, monthly usage calendar, EventKit skills export, guided trials and credit rewards), a TestFlight link, and self-hosted inline SVG icons; bump page references to 2.1.0. / **产品页重设计**:GitHub Pages 主页改为深色产品舞台,启用 Icon Composer 新图标与三端同框主视觉,以实机视频与真实界面卡片取代手绘示意;补齐 2.1 卖点(专属风格、三语气个性回复、月度用量日历、EventKit 技能写入、引导体验与积分奖励),新增 TestFlight 入口,图标改为自托管内联 SVG;页面版本号更新至 2.1.0。 -- **Host-only analytics recording**: the keyboard extension no longer opens the analytics or keyboard-usage SQLite queues. The host app is the only process that records and uploads events, so hiding the keyboard cannot hold WAL locks and get killed as `0xdead10cc`. Keyboard-activated and per-keystroke usage events are no longer collected. / **仅主 App 记埋点**:键盘扩展不再打开统计与键盘用量 SQLite 队列;开库、记账与上传只在主 App 进行,收起键盘时不会因 WAL 锁被系统以 `0xdead10cc` 终止。不再采集「键盘激活」和逐次按键用量。 +- **iPhone Duo keyboard height**: on the unfolded inner display the keyboard keeps the iPhone key layout but uses rows sized to the system keyboard, so it is 260 pt tall instead of 281 pt and sits close to the height of Apple's keyboard. iPad, other iPhones, and the folded outer display keep their current layout. / **iPhone Duo 键盘高度**:展开后的内屏上,键盘保留 iPhone 键位,但行高按系统键盘设定,高度由 281 pt 降为 260 pt,接近系统键盘。iPad、其他 iPhone 以及合上后的外屏保持原有布局。 +- **Voice keys on wide screens**: on iPad and the unfolded iPhone Duo, the voice keyboard replaces the round buttons with one row of system-style keys along the bottom, as tall as the typing keys: space, edit, send, undo, and delete (mirrored by the left/right-hand setting). Space, edit, undo, and delete share one width and send is twice as wide and centered, filling the row edge to edge. Edit and undo are always shown and grey out when unavailable, and while dictation or AI is running the row dims instead of disappearing. The voice surface now spans the same width as the typing keys, so the logo, tabs, and top-right buttons no longer shift when switching tabs. Other iPhones and the folded outer display keep the round buttons. The voice keyboard no longer shows a globe key on any device; switch keyboards from the typing tab. / **宽屏语音按键**:在 iPad 和展开的 iPhone Duo 上,语音键盘把圆形按钮换成底部一排与打字键同高的系统风格按键:空格、编辑、发送、撤销、删除(随左右手设置镜像)。空格、编辑、撤销、删除四键等宽,发送键为其两倍宽且居中,整排铺满;编辑和撤销常驻,不可用时变灰;听写或 AI 运行时整排变暗而不是消失。语音界面与打字键区同宽,切换标签时 Logo、标签栏和右上角按钮不再左右跳动。其他 iPhone 和合上后的外屏仍使用圆形按钮。语音键盘在所有设备上都不再显示地球键,切换键盘请在打字页进行。 +- **Shorter keyboard in landscape**: on any iPhone held sideways, including the folded iPhone Duo, the keyboard is 244 pt tall instead of 281 pt, so the app above keeps more of the screen. Typing keys are 40 pt tall with the system-style half-key indent on the second row, and the voice keyboard uses the same bottom row of keys as iPad and the unfolded Duo. Portrait keyboards are unchanged. / **横屏键盘更矮**:任意 iPhone 横放时(包括合上的 iPhone Duo),键盘高度从 281 pt 降到 244 pt,上方的 App 能多露出一些内容。打字键高 40 pt,第二排按系统键盘的方式缩进半个键;语音键盘改用与 iPad、展开的 Duo 相同的底部按键排布。竖屏键盘不变。 +- **Centred mic on the voice keyboard**: on every iPhone, the iPhone Duo, and iPad, the mic now sits centred between the top bar and the bottom of the keyboard — the space above it matches the gap plus the bottom row below it — instead of sitting low with extra space piled up above. The three-line live transcript above the mic always keeps its room. / **语音键盘麦克风居中**:所有 iPhone、iPhone Duo 和 iPad 上,麦克风改为在顶栏与键盘底部之间居中:上方空间与下方间距加底排的高度相等,不再把多余高度都堆在麦克风上方、让它偏低。麦克风上方始终保留三行实时听写的空间。 +- **Clipboard skills only in the top bar**: the circular skill buttons above the mic are gone on every iPhone and iPad, including onboarding practice. Clipboard skills now appear only as text buttons in the keyboard's top bar, beside the logo; during onboarding the skill being practised shows there and the practice hint stays in the middle of the voice keyboard. / **剪贴板技能只在顶栏**:麦克风上方的圆形技能按钮在所有 iPhone 和 iPad 上都已移除,新手引导练习也不例外。剪贴板技能只以文字按钮形式出现在键盘顶栏 logo 右侧;新手引导时,正在练习的技能显示在顶栏,语音键盘中间保留练习提示。 +- **Wide-layout top spacing follows the status bar**: the extra room above the sidebar logo and the Home title is now added only when no status bar sits above the content (the unfolded iPhone Duo held sideways) instead of on every iPhone, so the portrait inner display no longer stacks it on top of its status bar. iPad is unchanged. / **宽屏顶部间距跟随状态栏**:侧边栏 logo 和首页标题上方的额外间距,改为只在内容上方没有状态栏时添加(展开的 iPhone Duo 横放时),不再按“是不是 iPhone”判断;内屏竖放时不再在状态栏之下重复留白。iPad 不变。 +- **Clipboard empty state**: an empty clipboard panel now shows an icon and a line explaining that copied text appears there. / **剪贴板空状态**:剪贴板面板为空时显示图标,并说明复制的文字会出现在这里。 +- **Privacy policy URL**: the in-app privacy policy link now points to `https://osg.osglab.com/privacy/` on the mainland-hosted OSG website instead of the GitHub Pages address, so the China App Store storefront can reach a policy served from an ICP-filed domain. The policy text now names the correct site and notes that the in-app What's New page is served from `download.osglab.com` and does not run Google Analytics. / **隐私政策链接**:App 内隐私政策改指向境内官网的 `https://osg.osglab.com/privacy/`,不再使用 GitHub Pages 地址,使中国区 App Store 能访问备案域名下的政策页面。政策文本同步更正了站点名称,并说明 App 内「更新说明」页面由 `download.osglab.com` 提供、不运行 Google Analytics。 +- **ICP number in the site footer**: the product page now shows the MIIT filing number on its own centered line below the footer, linked to `beian.miit.gov.cn`, as required for sites hosted in mainland China. Its styling lives in a ` + + + + + + diff --git a/Scripts/appstore-creative/render.cjs b/Scripts/appstore-creative/render.cjs new file mode 100644 index 0000000..284d04e --- /dev/null +++ b/Scripts/appstore-creative/render.cjs @@ -0,0 +1,104 @@ +#!/usr/bin/env node +// Renders a deterministic canvas scene (window.render(t)) to PNG stills or +// a looping video piped straight into ffmpeg. +// +// NODE_PATH= node render.cjs \ +// --page header.html --query "w=3840&h=1646&lang=zh" \ +// --still 7.0 --out poster.png [--debug-safe] +// ... --video --fps 30 --out frames.mov (ProRes 4444 intermediate) +// +// Chromium comes from the Playwright cache; override with CHROMIUM_PATH. + +const { chromium } = require('playwright-core'); +const { spawn } = require('child_process'); +const path = require('path'); +const fs = require('fs'); +const os = require('os'); + +function arg(name, fallback) { + const i = process.argv.indexOf(`--${name}`); + if (i === -1) return fallback; + const v = process.argv[i + 1]; + return v === undefined || v.startsWith('--') ? true : v; +} + +function chromiumPath() { + if (process.env.CHROMIUM_PATH) return process.env.CHROMIUM_PATH; + const cache = path.join(os.homedir(), 'Library/Caches/ms-playwright'); + const dirs = fs.readdirSync(cache).filter((d) => /^chromium-\d+$/.test(d)).sort().reverse(); + for (const d of dirs) { + const p = path.join(cache, d, + 'chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing'); + if (fs.existsSync(p)) return p; + } + throw new Error('No Playwright Chromium found; set CHROMIUM_PATH'); +} + +(async () => { + const page = path.resolve(__dirname, arg('page', 'header.html')); + const query = arg('query', ''); + const out = path.resolve(arg('out')); + const params = new URLSearchParams(query); + const W = +params.get('w'); + const H = +params.get('h'); + + const browser = await chromium.launch({ + executablePath: chromiumPath(), + args: ['--force-color-profile=srgb', '--disable-gpu-vsync'], + }); + const ctx = await browser.newContext({ viewport: { width: W, height: H }, deviceScaleFactor: 1 }); + const tab = await ctx.newPage(); + tab.on('console', (m) => console.log('[page]', m.text())); + tab.on('pageerror', (e) => { console.error('[page error]', e); process.exit(1); }); + await tab.goto(`file://${page}?${query}`); + await tab.evaluate(() => window.READY); + + const grab = async (t, debugSafe) => { + const b64 = await tab.evaluate(async ({ t, debugSafe }) => { + await window.render(t); + if (debugSafe) window.SAFE_DEBUG(); + const c = document.getElementById('c'); + return c.toDataURL('image/png').split(',')[1]; + }, { t, debugSafe }); + return Buffer.from(b64, 'base64'); + }; + + if (arg('layout')) { + const layout = await tab.evaluate(() => window.LAYOUT); + fs.writeFileSync(path.resolve(arg('layout')), JSON.stringify(layout, null, 2)); + } + + if (arg('still') !== undefined) { + const t = parseFloat(arg('still')); + fs.writeFileSync(out, await grab(t, !!arg('debug-safe'))); + console.log(`wrote ${out}`); + } else if (arg('video')) { + const fps = +arg('fps', 30); + const duration = await tab.evaluate(() => window.DURATION); + const frames = Math.round(duration * fps); + // Lossless-ish intermediate; delivery encodes happen in a second pass. + const ff = spawn('ffmpeg', [ + '-y', '-hide_banner', '-loglevel', 'error', + '-f', 'image2pipe', '-framerate', String(fps), '-c:v', 'png', '-i', '-', + // Same matrix + tags as the simulator footage it gets composited with. + '-vf', 'scale=out_color_matrix=bt709:out_range=tv', + '-c:v', 'prores_ks', '-profile:v', '4', '-pix_fmt', 'yuva444p10le', + '-colorspace', 'bt709', '-color_primaries', 'bt709', + '-color_trc', 'iec61966-2-1', '-color_range', 'tv', + '-vendor', 'apl0', out, + ], { stdio: ['pipe', 'inherit', 'inherit'] }); + const started = Date.now(); + for (let i = 0; i < frames; i++) { + const buf = await grab(i / fps, false); + if (!ff.stdin.write(buf)) await new Promise((r) => ff.stdin.once('drain', r)); + if (i % 30 === 0) { + const s = ((Date.now() - started) / 1000).toFixed(0); + console.log(`frame ${i}/${frames} (${s}s)`); + } + } + ff.stdin.end(); + await new Promise((r, j) => ff.on('close', (c) => (c === 0 ? r() : j(new Error(`ffmpeg ${c}`))))); + console.log(`wrote ${out}`); + } + await browser.close(); +})().catch((e) => { console.error(e); process.exit(1); }); diff --git a/Scripts/appstore-creative/search.html b/Scripts/appstore-creative/search.html new file mode 100644 index 0000000..876ae4c --- /dev/null +++ b/Scripts/appstore-creative/search.html @@ -0,0 +1,365 @@ + + + + + + + + + + + + diff --git a/Scripts/record_feature_previews.sh b/Scripts/record_feature_previews.sh index 2930943..3f4e1fa 100755 --- a/Scripts/record_feature_previews.sh +++ b/Scripts/record_feature_previews.sh @@ -18,6 +18,8 @@ # # Usage: # Scripts/record_feature_previews.sh [slug ...] +# PREVIEW_LANG=en Scripts/record_feature_previews.sh [slug ...] +# -> English demo copy, footage in `.tmp/feature-previews/raw-en` set -euo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd)" @@ -27,17 +29,23 @@ UDID="${1:?usage: record_feature_previews.sh [slug ...]}" shift || true BUNDLE="com.osgkeyboard.ios" -RAW_DIR="$ROOT/.tmp/feature-previews/raw" +PREVIEW_LANG="${PREVIEW_LANG:-zh}" +if [[ "$PREVIEW_LANG" == "zh" ]]; then + RAW_DIR="$ROOT/.tmp/feature-previews/raw" +else + RAW_DIR="$ROOT/.tmp/feature-previews/raw-$PREVIEW_LANG" +fi mkdir -p "$RAW_DIR" +LANG_ARG="--whats-new-lang=$PREVIEW_LANG" # slug | seconds | launch args CLIPS=( - "voice-polish|23|--edit-demo --preview-fullscreen" + "voice-polish|23|--edit-demo --preview-fullscreen $LANG_ARG" # Static page — the motion is a post-production push-in, so this only needs # to be long enough to fill the App Store 15s minimum after trimming. "personal-style|22|--polish-styles-screenshot" - "clipboard-agent|46|--clipboard-demo --preview-fullscreen" - "ask-ai|36|--ai-demo --preview-fullscreen --whats-new-lang=zh" + "clipboard-agent|46|--clipboard-demo --preview-fullscreen $LANG_ARG" + "ask-ai|36|--ai-demo --preview-fullscreen $LANG_ARG" ) want() { diff --git a/Scripts/sim-import-account-session.sh b/Scripts/sim-import-account-session.sh new file mode 100755 index 0000000..730e8a1 --- /dev/null +++ b/Scripts/sim-import-account-session.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# Hand a signed-in account session from a physical device to an iOS Simulator. +# +# The Simulator cannot produce DeviceCheck / App Attest evidence, so the +# account server rejects its Apple sign-in (`integrity_evidence_missing`). +# Refresh and managed-gateway calls only need the session, so a DEBUG build on +# a device can relinquish its session instead: +# +# 1. Device (DEBUG build): Settings → Diagnostics → 把账号会话交给模拟器, +# then AirDrop the file to this Mac. +# 2. Mac: Scripts/sim-import-account-session.sh ~/Downloads/osg-account-handoff.json [udid] +# +# The DEBUG Simulator build must already be installed. The script places the +# file in the app's Documents, deletes the source, and relaunches the app, +# which adopts the session and deletes the file. The file holds a live refresh +# token — treat it like a password. + +set -euo pipefail + +usage() { + echo "usage: $0 [simulator-udid|booted]" >&2 + exit 64 +} + +[[ $# -ge 1 && $# -le 2 ]] || usage +source_file=$1 +device=${2:-booted} +bundle_id=com.osgkeyboard.ios +format=osg-debug-account-handoff-v1 + +[[ -f "$source_file" ]] || { echo "error: $source_file not found" >&2; exit 66; } +grep -q "\"$format\"" "$source_file" || { + echo "error: $source_file is not an OSG account handoff file" >&2 + exit 65 +} + +container=$(xcrun simctl get_app_container "$device" "$bundle_id" data) || { + echo "error: $bundle_id is not installed on simulator '$device' — run the DEBUG build first" >&2 + exit 69 +} + +mkdir -p "$container/Documents" +install -m 600 "$source_file" "$container/Documents/osg-account-handoff.json" +rm -f "$source_file" + +xcrun simctl terminate "$device" "$bundle_id" >/dev/null 2>&1 || true +xcrun simctl launch "$device" "$bundle_id" >/dev/null + +echo "Session handed to simulator '$device'. The app adopts it on launch and deletes the file." diff --git a/Tests/suite-manifest.json b/Tests/suite-manifest.json index d4cf34f..ca0a49b 100644 --- a/Tests/suite-manifest.json +++ b/Tests/suite-manifest.json @@ -26,13 +26,16 @@ "platform": "ios", "tests": [ "OSGKeyboardTests/AccountAPIClientTests", + "OSGKeyboardTests/DebugAccountSessionHandoffTests", "OSGKeyboardTests/AccountCenterViewModelTests", "OSGKeyboardTests/AccountCreditPurchaseManagerTests", "OSGKeyboardTests/AccountSecurityPrimitiveTests", "OSGKeyboardTests/AccountSignInCoordinatorTests", "OSGKeyboardTests/AccountSnapshotLoaderTests", "OSGKeyboardTests/DeviceIntegrityTests", - "OSGKeyboardTests/ReferralProfileTests" + "OSGKeyboardTests/ReferralProfileTests", + "OSGKeyboardTests/InvitePosterTests", + "OSGKeyboardTests/ScreenBrightnessBoostTests" ] }, "managed_gateway": { @@ -81,6 +84,8 @@ "OSGKeyboardTests/PolishStylePackTests", "OSGKeyboardTests/PolishStyleCorpusExportStoreTests", "OSGKeyboardTests/PolishStyleLearningServiceTests", + "OSGKeyboardTests/PersonalStyleLearningStateStoreTests", + "OSGKeyboardTests/PolishStylesL10nTests", "OSGKeyboardTests/LLMClientTests", "OSGKeyboardTests/TranscriptLanguageDetectorTests", "OSGKeyboardTests/AIModeLLMClientTests", @@ -148,6 +153,7 @@ "OSGKeyboardTests/FlowPiPLossReportGateTests", "OSGKeyboardTests/FlowSessionWarningReportGateTests", "OSGKeyboardTests/FlowHomePiPStatusPolicyTests", + "OSGKeyboardTests/HomeAccountSurfacePolicyTests", "OSGKeyboardTests/FlowPiPRecoveryPolicyTests", "OSGKeyboardTests/FlowAdaptiveDownsamplerTests", "OSGKeyboardTests/FlowSessionBridgeTests", @@ -173,6 +179,7 @@ "OSGKeyboardExtTests/CandidatePanelExpandTests", "OSGKeyboardExtTests/ClipboardSuggestionLifecycleTests", "OSGKeyboardExtTests/KeyboardStateTests", + "OSGKeyboardExtTests/DictationProgressTests", "OSGKeyboardExtTests/KeyboardUsageTypingTests", "OSGKeyboardExtTests/KeyboardSurfaceStateTests", "OSGKeyboardExtTests/KeyHitTestingTests", diff --git a/docs/CNAME b/docs/CNAME deleted file mode 100644 index 3a08408..0000000 --- a/docs/CNAME +++ /dev/null @@ -1 +0,0 @@ -osg.osglab.com \ No newline at end of file diff --git a/docs/FEATURE_PREVIEW_RECORDING.md b/docs/FEATURE_PREVIEW_RECORDING.md index 44b45a4..378e503 100644 --- a/docs/FEATURE_PREVIEW_RECORDING.md +++ b/docs/FEATURE_PREVIEW_RECORDING.md @@ -59,6 +59,8 @@ xcrun simctl install "$UDID" \ # 3) 录制(脚本会先校验 App Group 容器,缺失直接报错退出) Scripts/record_feature_previews.sh "$UDID" # 全部四段 Scripts/record_feature_previews.sh "$UDID" ask-ai # 只录一段 +# 英文文案(--whats-new-lang=en),素材落在 .tmp/feature-previews/raw-en +PREVIEW_LANG=en Scripts/record_feature_previews.sh "$UDID" voice-polish clipboard-agent ask-ai # 4) 合成。需要 Pillow —— 这版 ffmpeg 没有 libfreetype / libass, # 所有文字都由 PIL 渲染成 RGBA PNG 再 overlay。 @@ -137,6 +139,24 @@ app 里**没有**「点一次回复,弹出多条候选」这种交互:点一 `zoompan` 表达式同时做进和出——它的实际出帧数对不上 `duration * FPS`,片子会 停在半路,循环处肉眼可见地跳。现在的做法是只渲染推近的一半再 `reverse` 拼接。 +## App Store 创意素材(Header / Search Results) + +同一批原始素材(中英文各一套)还驱动 iOS 27 起的 App Store 创意素材,脚本在 +`Scripts/appstore-creative/`:`header.html` / `search.html` 是逐帧确定的 canvas +场景,`render.cjs` 用 Playwright 的 Chromium 渲染,`compose.py` 把录屏合进设备 +屏幕并按 App Store Connect 规格出片。 + +```bash +(cd .tmp/appstore-assets && npm install playwright-core) +NODE_PATH=$PWD/.tmp/appstore-assets/node_modules \ + python3 Scripts/appstore-creative/compose.py # header + stills + search +``` + +- 5244×2950 只接受 PNG;视频 Header 只能是 3840×1646。 +- 安全区取自 Apple 官方 Sketch 模板,`compose.py` 顶部有数值。 +- 3840×2560 的 H.264 需要 Level 6,另出一份 3072×2048 / Level 5.1 兼容版。 +- `compose.py` 的 `WINDOWS` 是照 0.5 秒联系表量出来的,重录后要重新对时。 + ## 改动了 demo 时间轴之后 `Scripts/feature_preview_clips.py` 里的裁切点和字幕时间是照着实际录像量出来的。 diff --git a/docs/assets/portfolio/osgkeyboard-portfolio-product.jpg b/docs/assets/portfolio/osgkeyboard-portfolio-product.jpg new file mode 100644 index 0000000..fd87ac0 Binary files /dev/null and b/docs/assets/portfolio/osgkeyboard-portfolio-product.jpg differ diff --git a/docs/assets/portfolio/osgkeyboard-portfolio-square.jpg b/docs/assets/portfolio/osgkeyboard-portfolio-square.jpg new file mode 100644 index 0000000..3ed4ab9 Binary files /dev/null and b/docs/assets/portfolio/osgkeyboard-portfolio-square.jpg differ diff --git a/docs/index.html b/docs/index.html index 4686739..c5b4f07 100644 --- a/docs/index.html +++ b/docs/index.html @@ -61,6 +61,30 @@ + diff --git a/docs/llms.txt b/docs/llms.txt index 7dead7d..481dc81 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -2,7 +2,7 @@ > OSGKeyboard is an agentic keyboard and AI voice keyboard (Chinese: AI 语音输入法 / 嘴替输入法) for iPhone, iPad, and Mac. The iOS keyboard combines on-device dictation, a unified AI assistant, a learned personal style that sounds like you, agentic clipboard skills, Chinese and English typing, and optional managed credits. On Mac, hold Option for global dictation. It is source available, not open source. -Current repository version: 2.1.0 (build 110). Later work is listed under CHANGELOG [Unreleased]. +Current repository version: 2.1.6 (build 131). Later work is listed under CHANGELOG [Unreleased]. ## Category definitions diff --git a/docs/osgkeyboardversion.html b/docs/osgkeyboardversion.html index 1d88e18..03ee5d7 100644 --- a/docs/osgkeyboardversion.html +++ b/docs/osgkeyboardversion.html @@ -3,15 +3,6 @@ - - - OSGKeyboard — What's New / 更新说明 @@ -198,6 +189,33 @@

暂无与当前版本匹配的更新说明。

No release notes match this app version.

+ +
+
2.1.6
+ +
+
+
    +
  • 适配 iPhone Duo:展开内屏自动使用宽屏布局,合上后切回手机布局
  • +
  • 优化横屏键盘:iPad 与 iPhone Duo 的语音键盘底部改为系统风格按键行,iPhone 横放时键盘更矮
  • +
  • 优化首页顶部:最多只显示一张卡片,缺少权限、未登录或积分用完时直接给出下一步入口
  • +
  • 新增剪贴板 AI 技能:其他语言的内容可一键“优化”,当前语言的内容可“翻译 XX”译成目标语言
  • +
+
+
+ +
+
+
    +
  • iPhone Duo support: the unfolded inner display uses the wide layout automatically and returns to the phone layout when folded
  • +
  • Better landscape keyboards: on iPad and iPhone Duo the voice keyboard ends with a row of system-style keys, and the keyboard is shorter on an iPhone held sideways
  • +
  • Cleaner top of Home: at most one card is shown, with a clear next step when permissions are missing, you are signed out, or credits run out
  • +
  • New clipboard AI skills: “Polish” for text in other languages, and “Translate to XX” for text in your current language
  • +
+
+
+
+ diff --git a/docs/privacy/index.html b/docs/privacy/index.html index f173e38..70912a1 100644 --- a/docs/privacy/index.html +++ b/docs/privacy/index.html @@ -88,7 +88,7 @@

User-configured cloud ASR and LLM requests go directly to the provider you configure. Managed-credit requests go through account.osglab.com to the managed ASR or LLM provider. Apple processes Sign in with Apple and StoreKit purchases. AI skills may also hand generated data to Apple Shortcuts or open a route in Apple Maps, Amap, or Baidu Maps. Public suggestions and update pages are served by key.osglab.com and download.osglab.com. Optional Mac models are downloaded from Hugging Face. Each service's privacy policy applies.

Website analytics

-

The OSGKeyboard website at hkgood.github.io/OSGKeyboard — including the in-app What's New page loaded from that site — uses Google Analytics 4 (gtag.js) to measure page visits, approximate region, and device or browser type. It runs only on these web pages: it is not an app SDK, never sees keyboard input, audio, transcripts, clipboard content, prompts, or API keys, and is not controlled by the in-app analytics toggle. Data processed by Google is governed by Google's privacy policy; you can block it with a browser content blocker or Google's opt-out add-on.

+

The OSGKeyboard website at osg.osglab.com uses Google Analytics 4 (gtag.js) to measure page visits, approximate region, and device or browser type. It runs only on these web pages: it is not an app SDK, never sees keyboard input, audio, transcripts, clipboard content, prompts, or API keys, and is not controlled by the in-app analytics toggle. The in-app What's New page is served from download.osglab.com and does not run Google Analytics. Data processed by Google is governed by Google's privacy policy; you can block it with a browser content blocker or Google's opt-out add-on.

Data retention

Settings remain on your device until you delete the app or reset settings. When iCloud settings sync is enabled, API keys replicate through iCloud Keychain and eligible preferences, statistics, dictionary data, voice history, and saved custom styles may sync through your private iCloud account. Voice history is capped at 300 entries and can be cleared from History or by resetting settings; clearing all history also removes retained style-prompt snapshots. A generated style is stored only after you review and save it. Clipboard history remains device-local, is capped at 15 entries, has no fixed expiry, and remains when capture is turned off or settings are reset; use the separate confirmed clear action to delete it. Chinese and English typing-learning data stays local and can be cleared from typing settings without deleting the personal dictionary. Downloaded Mac speech models remain until you remove the model in the app or delete its local files.

@@ -156,7 +156,7 @@

用户自备的云端 ASR 与 LLM 请求直接发送到你配置的服务商;托管积分请求经 account.osglab.com 发送到托管 ASR 或 LLM 服务商。Apple 处理 Apple 登录与 StoreKit 购买。AI 技能也可能把生成数据交给 Apple 快捷指令,或在 Apple 地图、高德地图、百度地图中打开路线。公开建议与更新页面分别由 key.osglab.com、download.osglab.com 提供;可选 Mac 模型从 Hugging Face 下载。各服务的隐私政策适用。

网站分析

-

OSGKeyboard 网站(hkgood.github.io/OSGKeyboard,包括 App 内从该站点加载的「更新说明」页面)使用 Google Analytics 4(gtag.js)统计页面访问量、大致地区与设备或浏览器类型。它仅运行在这些网页中:不是 App 内 SDK,无法接触键盘输入、语音、转写文本、剪贴板内容、提示词或 API 密钥,也不受 App 内「产品分析」开关控制。Google 处理的数据适用其隐私政策;你可以通过浏览器内容拦截器或 Google 的停用插件将其屏蔽。

+

OSGKeyboard 网站(osg.osglab.com)使用 Google Analytics 4(gtag.js)统计页面访问量、大致地区与设备或浏览器类型。它仅运行在这些网页中:不是 App 内 SDK,无法接触键盘输入、语音、转写文本、剪贴板内容、提示词或 API 密钥,也不受 App 内「产品分析」开关控制。App 内「更新说明」页面由 download.osglab.com 提供,不运行 Google Analytics。Google 处理的数据适用其隐私政策;你可以通过浏览器内容拦截器或 Google 的停用插件将其屏蔽。

数据保留

设置保留在设备上,直至卸载或重置。开启 iCloud 设置同步后,API 密钥经 iCloud 钥匙串同步,可同步的偏好、统计、词库、语音历史和已保存的自定义风格可能经私有 iCloud 账户同步。语音历史上限 300 条,可在「历史」页清空或通过重置设置清除;清空全部历史也会删除保留的风格 Prompt 快照。生成的风格只有在你检查并保存后才会写入。剪贴板历史仅保存在本机,上限 15 条,没有固定过期时间;关闭采集或重置设置均会保留,需使用单独确认的清空操作删除。中英文输入学习数据仅留本机,可在输入设置中单独清除且不影响个性词库。下载的 Mac 语音模型会保留到你在应用中移除模型或删除本地文件。

diff --git a/project.yml b/project.yml index b682c36..2efc080 100644 --- a/project.yml +++ b/project.yml @@ -48,8 +48,8 @@ settings: ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS: YES STRING_CATALOG_GENERATE_SYMBOLS: YES CLANG_CXX_LANGUAGE_STANDARD: c++17 - MARKETING_VERSION: "2.1.0" - CURRENT_PROJECT_VERSION: "110" + MARKETING_VERSION: "2.1.6" + CURRENT_PROJECT_VERSION: "131" # 签名配置来自 Signing.local.xcconfig(gitignored,不会被覆盖) # 项目级签名 xcconfig,适用于所有 target @@ -188,6 +188,7 @@ targets: NSContactsUsageDescription: "OSGKeyboard uses Contacts only when you choose Create Contact for a copied phone number." NSRemindersFullAccessUsageDescription: "OSGKeyboard adds reminders only when you choose Extract To-dos for copied text." NSCalendarsFullAccessUsageDescription: "OSGKeyboard adds calendar events only when you choose Extract Events for copied text." + NSPhotoLibraryAddUsageDescription: "OSGKeyboard saves your invitation poster to Photos only when you choose Save Image." UIBackgroundModes: - audio BGTaskSchedulerPermittedIdentifiers: @@ -352,6 +353,7 @@ targets: settings: base: PRODUCT_BUNDLE_IDENTIFIER: com.osgkeyboard.ios.keyboard + SWIFT_OBJC_BRIDGING_HEADER: OSGKeyboardExt/OSGKeyboardExt-Bridging-Header.h TARGETED_DEVICE_FAMILY: "1,2" SUPPORTS_MACCATALYST: NO SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD: NO @@ -397,6 +399,9 @@ targets: - "include/**" info: path: OSGKeyboardShared/Info.plist + properties: + CFBundleShortVersionString: "$(MARKETING_VERSION)" + CFBundleVersion: "$(CURRENT_PROJECT_VERSION)" settings: base: PRODUCT_BUNDLE_IDENTIFIER: com.osgkeyboard.ios.shared @@ -431,6 +436,9 @@ targets: - path: OSGKeyboardHostSupport info: path: OSGKeyboardHostSupport/Info.plist + properties: + CFBundleShortVersionString: "$(MARKETING_VERSION)" + CFBundleVersion: "$(CURRENT_PROJECT_VERSION)" settings: base: PRODUCT_BUNDLE_IDENTIFIER: com.osgkeyboard.ios.hostsupport @@ -463,6 +471,9 @@ targets: - path: Tests/ProviderToolRequestCoordinatorTests.swift info: path: OSGKeyboardTests/Info.plist + properties: + CFBundleShortVersionString: "$(MARKETING_VERSION)" + CFBundleVersion: "$(CURRENT_PROJECT_VERSION)" dependencies: - target: OSGKeyboard - target: OSGKeyboardHostSupport @@ -486,6 +497,9 @@ targets: buildPhase: resources info: path: OSGKeyboardExtTests/Info.plist + properties: + CFBundleShortVersionString: "$(MARKETING_VERSION)" + CFBundleVersion: "$(CURRENT_PROJECT_VERSION)" dependencies: - target: OSGKeyboardShared settings: @@ -505,6 +519,9 @@ targets: - path: OSGKeyboardUITests info: path: OSGKeyboardUITests/Info.plist + properties: + CFBundleShortVersionString: "$(MARKETING_VERSION)" + CFBundleVersion: "$(CURRENT_PROJECT_VERSION)" dependencies: - target: OSGKeyboard settings: @@ -529,6 +546,9 @@ targets: - path: Tests/ProviderToolRequestCoordinatorTests.swift info: path: OSGKeyboardMacTests/Info.plist + properties: + CFBundleShortVersionString: "$(MARKETING_VERSION)" + CFBundleVersion: "$(CURRENT_PROJECT_VERSION)" dependencies: - target: OSGKeyboardMac settings: @@ -578,7 +598,10 @@ targets: - path: OSGKeyboard/Views/Account/AccountCreditStore.swift - path: OSGKeyboard/Views/Account/AccountCreditPurchaseManager.swift - path: OSGKeyboard/Views/Account/LiveAccountServices.swift + - path: OSGKeyboard/Views/Account/DebugAccountSessionHandoff.swift - path: OSGKeyboard/Views/Account/ReferralProfileViewModel.swift + # Invitation QR poster: pure SwiftUI + Core Image, shared with iOS. + - path: OSGKeyboard/Views/Account/InvitePoster.swift - path: OSGKeyboard/Views/Account/AccountAppleAuthorizationFactories.swift - path: OSGKeyboard/Services/AccountAnalyticsBearerBridge.swift - path: OSGKeyboardHostSupport