Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
8d30fbf
Add release manager workflow
JunchenMeteor Jun 6, 2026
7e2e411
Merge pull request #290 from JunchenMeteor/dev/feature/release-manager
JunchenMeteor Jun 6, 2026
482ac35
Refactor mobile session runtime
JunchenMeteor Jun 6, 2026
8e8c6fb
Add session runtime trace logging
JunchenMeteor Jun 6, 2026
89db4cc
Merge pull request #292 from JunchenMeteor/dev/refactor/session-runti…
JunchenMeteor Jun 6, 2026
b4bbfd3
Modularize mobile session runtime
JunchenMeteor Jun 6, 2026
250cc3b
Tighten mobile session resume selectors
JunchenMeteor Jun 6, 2026
be46723
Merge pull request #294 from JunchenMeteor/dev/refactor/mobile-sessio…
JunchenMeteor Jun 6, 2026
9a674cb
fix: Xunfei STT silent failure — root cause fixes
JunchenMeteor Jun 13, 2026
5c4cfef
Exclude e2e directory from vitest
JunchenMeteor Jun 13, 2026
649c84d
Merge pull request #296 from JunchenMeteor/dev/meteor/fixXunfeiSttSil…
JunchenMeteor Jun 13, 2026
14de6e6
Unify API route auth guards, add database indexes, extract shared uti…
JunchenMeteor Jun 19, 2026
8c5a7e2
Fix audio_clips index to use owner_type and owner_id columns
JunchenMeteor Jun 19, 2026
10dda3b
Restore requireEnv error labels, move sleep to shared/utils, fix E2E …
JunchenMeteor Jun 19, 2026
fe8e319
Merge pull request #298 from JunchenMeteor/dev/meteor/api-guardrails-…
JunchenMeteor Jun 19, 2026
5537ac2
Extract pure functions from VoiceSessionProvider into audio-engine, s…
JunchenMeteor Jun 19, 2026
87c81be
Extract playback engine into usePlaybackEngine hook (#302)
JunchenMeteor Jun 19, 2026
847c0cb
Extract listening engine into useListeningEngine hook (#304)
JunchenMeteor Jun 19, 2026
a0c0a85
Extract TTS engine into useTTSEngine hook (#306)
JunchenMeteor Jun 19, 2026
3e810a9
Add section comments and organize VoiceSessionProvider structure (#308)
JunchenMeteor Jun 19, 2026
4222cdc
refactor: split Mobile App.tsx God Component (2693→780 lines) into 7 …
JunchenMeteor Jun 20, 2026
e1fe547
fix: restore missing logic & pass lint/typecheck
JunchenMeteor Jun 20, 2026
c683e78
fix: restore native speech bridge, Xunfei prewarm, selectScenario, an…
JunchenMeteor Jun 20, 2026
42b4674
fix: wire Xunfei STT handlers via ref bridge + restore onMetric and c…
JunchenMeteor Jun 20, 2026
0a79b89
refactor: mobile Context architecture — eliminate God Component and p…
JunchenMeteor Jun 20, 2026
eb024c4
feat: expo-router migration — file-based routing with Tab navigator (…
JunchenMeteor Jun 20, 2026
18b3db8
fix: remove unused imports and context parameters (#316)
JunchenMeteor Jun 20, 2026
a1176ed
refactor: code style unification — imports, ref bridge, grouped Deps,…
JunchenMeteor Jun 20, 2026
bbd517b
Merge pull request #318 from JunchenMeteor/dev/refactor/mobile-code-s…
JunchenMeteor Jun 20, 2026
ff47c99
refactor: import ordering, type clustering, group separation per CODE…
JunchenMeteor Jun 21, 2026
4a19818
Merge pull request #319 from JunchenMeteor/dev/docs/code-style-guide
JunchenMeteor Jun 21, 2026
9ec25e4
fix mobile handler bridge stability
JunchenMeteor Jun 21, 2026
c163b0f
Merge pull request #322 from JunchenMeteor/dev/fix/mobile-handler-bri…
JunchenMeteor Jun 21, 2026
10e1583
test session smoke e2e flow
JunchenMeteor Jun 21, 2026
9fac386
Merge pull request #324 from JunchenMeteor/dev/test/session-smoke-e2e
JunchenMeteor Jun 21, 2026
9d0fac4
fix web accessibility labels
JunchenMeteor Jun 21, 2026
70e4db6
Merge pull request #326 from JunchenMeteor/dev/fix/web-accessibility-…
JunchenMeteor Jun 21, 2026
7b9f938
Add local cached TTS audio delivery
JunchenMeteor Jun 21, 2026
0cc3731
Merge pull request #328 from JunchenMeteor/dev/feature/tts-local-cach…
JunchenMeteor Jun 21, 2026
0617346
Add persistent API rate limiting
JunchenMeteor Jun 21, 2026
75fb052
Merge pull request #330 from JunchenMeteor/dev/feature/persistent-rat…
JunchenMeteor Jun 21, 2026
48da83d
Add Supabase database types (#332)
JunchenMeteor Jun 21, 2026
cc59890
Add i18n interpolation helper (#334)
JunchenMeteor Jun 21, 2026
6565412
Isolate app feedback store state (#336)
JunchenMeteor Jun 21, 2026
9acbe9c
Split product preferences from theme preferences (#338)
JunchenMeteor Jun 21, 2026
971600f
Use REST session delete route (#340)
JunchenMeteor Jun 21, 2026
9285c93
Compress Xunfei ASR stream state (#342)
JunchenMeteor Jun 21, 2026
ad15359
Refactor mobile shell and i18n pluralization (#344)
JunchenMeteor Jun 21, 2026
d8412ab
Refactor mobile screen state (#346)
JunchenMeteor Jun 21, 2026
bb32c82
Stabilize mobile runtime and server speech
JunchenMeteor Jul 11, 2026
a2320c4
Merge pull request #348 from JunchenMeteor/dev/fix/mobile-runtime-api…
JunchenMeteor Jul 11, 2026
205afd6
Prepare v1.4.0 release
github-actions[bot] Jul 11, 2026
82736c3
Merge pull request #350 from JunchenMeteor/dev/release/v1-4-0
JunchenMeteor Jul 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .github/workflows/release-manager.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: Release Manager

permissions:
contents: write
issues: write
pull-requests: write
actions: read
checks: read

on:
workflow_dispatch:
inputs:
version:
description: Release version, for example 1.3.1
required: true
type: string
action:
description: Release action
required: true
default: full
type: choice
options:
- full
- prepare
- promote
- verify

concurrency:
group: release-manager
cancel-in-progress: false

jobs:
release:
name: Release Manager
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.RELEASE_MANAGER_TOKEN || github.token }}

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 24

- name: Run release manager
env:
GH_TOKEN: ${{ secrets.RELEASE_MANAGER_TOKEN || github.token }}
run: node scripts/release-manager.mjs "${{ inputs.action }}" --version "${{ inputs.version }}"
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

# testing
/coverage
/test-results
/playwright-report

# next.js
/.next/
Expand Down
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,9 @@ ASR/TTS provider keys (all optional — mock/native fallback works without them)
ASR_PROVIDER=native
TTS_PROVIDER=mock
XUNFEI_APP_ID=
XUNFEI_API_PASSWORD= # preferred; sent as the server-side x-api-key header
XUNFEI_API_KEY=
XUNFEI_API_SECRET=
XUNFEI_API_SECRET= # APIKey + APISecret remain supported as a fallback
XUNFEI_ASR_PRODUCT=zh_iat
XUNFEI_TTS_VOICE= # default fallback vcn; coach voice is selectable in Settings
VOLCENGINE_ACCESS_KEY=
Expand Down Expand Up @@ -255,6 +256,18 @@ Set the API base URL in `apps/mobile/app.json`:
}
```

## Release Management

Production releases are managed by the GitHub Actions `Release Manager` workflow:

```text
GitHub -> Actions -> Release Manager -> Run workflow
```

Use `action=full` with a semantic version such as `1.3.1`. The workflow creates the release issue and PRs, waits for checks, promotes `main` to `release`, waits for Tencent deployment, verifies public URLs, and creates the GitHub Release.

Detailed operation and recovery steps live in `docs/release-manager.md`.

## TTS Providers

Domestic providers are the recommended real voice path:
Expand Down
15 changes: 14 additions & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,9 @@ ASR/TTS 服务商密钥(均可选,不填则使用 mock/native 兜底):
ASR_PROVIDER=native
TTS_PROVIDER=mock
XUNFEI_APP_ID=
XUNFEI_API_PASSWORD= # 推荐;仅由服务端通过 x-api-key 请求头发送
XUNFEI_API_KEY=
XUNFEI_API_SECRET=
XUNFEI_API_SECRET= # 仍兼容 APIKey + APISecret 作为兜底鉴权
XUNFEI_ASR_PRODUCT=zh_iat
XUNFEI_TTS_VOICE= # 默认兜底 vcn;具体教练声音在设置页选择
VOLCENGINE_ACCESS_KEY=
Expand Down Expand Up @@ -255,6 +256,18 @@ eas build --platform ios --profile preview
}
```

## 发布管理

生产发布通过 GitHub Actions 的 `Release Manager` workflow 管理:

```text
GitHub -> Actions -> Release Manager -> Run workflow
```

选择 `action=full`,输入类似 `1.3.1` 的语义化版本号。workflow 会自动创建 release issue 和 PR,等待检查通过,将 `main` 提升到 `release`,等待腾讯云部署,验证公网 URL,并创建 GitHub Release。

详细操作和中断恢复步骤见 `docs/release-manager.md`。

## TTS 服务商

国内用户推荐使用以下服务商:
Expand Down
9 changes: 5 additions & 4 deletions apps/mobile/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@
"name": "MeteorVoice",
"slug": "meteorvoice-mobile",
"owner": "junchenmeteors-team",
"version": "1.3.0",
"version": "1.4.0",
"orientation": "portrait",
"scheme": "meteorvoice",
"userInterfaceStyle": "automatic",
"icon": "./assets/icon.png",
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.tzcpa.meteorvoice",
"buildNumber": "2026060601",
"bundleIdentifier": "com.jcmeteor.meteorvoice",
"buildNumber": "2026071101",
"deploymentTarget": "18.0",
"infoPlist": {
"NSMicrophoneUsageDescription": "MeteorVoice uses the microphone for voice practice sessions.",
Expand All @@ -26,7 +26,7 @@
},
"android": {
"package": "com.jcmeteor.meteorvoice",
"versionCode": 2026060601,
"versionCode": 2026071101,
"permissions": [
"android.permission.RECORD_AUDIO",
"android.permission.MODIFY_AUDIO_SETTINGS",
Expand All @@ -37,6 +37,7 @@
]
},
"plugins": [
"expo-router",
[
"expo-speech-recognition",
{
Expand Down
169 changes: 169 additions & 0 deletions apps/mobile/app/(tabs)/_layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/**
* Four-tab layout using the original MeteorVoice visual language.
* The tab bar stays opaque and participates in layout so scroll content never
* shows through or underneath it.
*/
import { Tabs } from 'expo-router'
import {
Pressable,
StyleSheet,
Text,
View,
} from 'react-native'
import { SafeAreaView } from 'react-native-safe-area-context'

import { useSession } from '../../src/SessionContext'
import { useTheme } from '../../src/ThemeProvider'

const TAB_LABELS: Record<string, string> = {
home: 'nav.home',
session: 'nav.practice',
history: 'nav.history',
settings: 'nav.settings',
}

function TabIcon({ tab, color }: { color: string; tab: string }) {
if (tab === 'home') return (
<View style={styles.homeIcon}>
<View style={[styles.homeRoof, { borderBottomColor: color }]} />
<View style={[styles.homeBody, { backgroundColor: color }]} />
</View>
)

if (tab === 'session') return (
<View style={styles.sessionIcon}>
<View style={[styles.microphone, { borderColor: color }]} />
<View style={[styles.microphoneBase, { backgroundColor: color }]} />
</View>
)

if (tab === 'history') return (
<View style={styles.historyIcon}>
{[18, 14, 10].map(width => (
<View key={width} style={[styles.historyLine, { width, backgroundColor: color }]} />
))}
</View>
)

return (
<View style={styles.settingsIcon}>
<View style={[styles.settingsCenter, { borderColor: color }]} />
<View style={styles.settingsDots}>
{[0, 45, 90, 135].map(deg => (
<View
key={deg}
style={[
styles.settingsDot,
{ backgroundColor: color, transform: [{ rotate: `${deg}deg` }, { translateY: -8 }] },
]}
/>
))}
</View>
</View>
)
}

export default function TabLayout() {
const { C } = useTheme()
const { tr } = useSession()

return (
<SafeAreaView edges={['top']} style={[styles.shell, { backgroundColor: C.bg }]}>
<Tabs
screenOptions={{ headerShown: false }}
tabBar={({ state, navigation }) => (
<SafeAreaView edges={['bottom']} style={[styles.tabBarWrapper, { backgroundColor: C.bg }]}>
<View style={[styles.tabBar, { backgroundColor: C.surface, borderColor: C.border }]}>
{state.routes.map((route, index) => {
const focused = state.index === index
const labelKey = TAB_LABELS[route.name]

return (
<Pressable
key={route.key}
accessibilityRole="button"
accessibilityState={focused ? { selected: true } : {}}
accessibilityLabel={labelKey ? tr(labelKey) : route.name}
onLongPress={() => navigation.emit({ type: 'tabLongPress', target: route.key })}
onPress={() => {
const event = navigation.emit({
type: 'tabPress',
target: route.key,
canPreventDefault: true,
})
if (!focused && !event.defaultPrevented) navigation.navigate(route.name)
}}
style={[styles.tabItem, focused && { backgroundColor: C.accent }]}
>
<TabIcon tab={route.name} color={focused ? C.cream : C.textMuted} />
<Text style={[styles.tabLabel, { color: focused ? C.cream : C.textMuted }]}>
{labelKey ? tr(labelKey) : route.name}
</Text>
</Pressable>
)
})}
</View>
</SafeAreaView>
)}
>
<Tabs.Screen name="home" />
<Tabs.Screen name="session" />
<Tabs.Screen name="history" />
<Tabs.Screen name="settings" />
</Tabs>
</SafeAreaView>
)
}

const styles = StyleSheet.create({
shell: { flex: 1 },
tabBarWrapper: {
paddingHorizontal: 16,
paddingTop: 6,
paddingBottom: 8,
},
tabBar: {
flexDirection: 'row',
borderRadius: 24,
borderWidth: 1,
paddingVertical: 4,
paddingHorizontal: 4,
overflow: 'hidden',
},
tabItem: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 8,
gap: 2,
borderRadius: 20,
},
tabLabel: { fontSize: 10, fontWeight: '600' },
homeIcon: { width: 18, height: 18, alignItems: 'center', justifyContent: 'flex-end' },
homeRoof: {
width: 0,
height: 0,
borderLeftWidth: 9,
borderRightWidth: 9,
borderBottomWidth: 8,
borderLeftColor: 'transparent',
borderRightColor: 'transparent',
marginBottom: 1,
},
homeBody: { width: 12, height: 8, borderRadius: 1 },
sessionIcon: { width: 18, height: 18, alignItems: 'center', justifyContent: 'center', gap: 1 },
microphone: { width: 8, height: 11, borderRadius: 4, borderWidth: 2 },
microphoneBase: { width: 12, height: 2, borderRadius: 1 },
historyIcon: { width: 18, height: 18, justifyContent: 'center', gap: 3 },
historyLine: { height: 2, borderRadius: 1 },
settingsIcon: { width: 18, height: 18, alignItems: 'center', justifyContent: 'center' },
settingsCenter: { width: 10, height: 10, borderRadius: 5, borderWidth: 2 },
settingsDots: {
position: 'absolute',
width: 18,
height: 18,
alignItems: 'center',
justifyContent: 'center',
},
settingsDot: { position: 'absolute', width: 3, height: 3, borderRadius: 1.5 },
})
24 changes: 24 additions & 0 deletions apps/mobile/app/(tabs)/history.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* History tab — session history and review screen.
* 历史标签页 — 会话历史与回顾界面。
*/

import { useMobileAuth } from '../../src/mobileAuth'
import { HistoryScreen } from '../../src/screens/HistoryScreen'
import { useSession } from '../../src/SessionContext'

export default function HistoryTab() {
const { tr, locale, api } = useSession()
const auth = useMobileAuth()

return (
<HistoryScreen
tr={tr}
locale={locale}
api={api}
getAuthHeaders={auth.getAuthHeaders}
handleUnauthorized={() => auth.signOut(null)}
defaultApiBaseUrl=""
/>
)
}
26 changes: 26 additions & 0 deletions apps/mobile/app/(tabs)/home.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* Home tab — scenario selection screen.
* 首页标签页 — 场景选择界面。
*/

import { useRouter } from 'expo-router'

import { scenarios } from '@meteorvoice/shared'

import { HomeScreen } from '../../src/screens/HomeScreen'
import { useSession } from '../../src/SessionContext'

export default function HomeTab() {
const { tr, locale } = useSession()
const router = useRouter()

return (
<HomeScreen
tr={tr}
locale={locale}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
scenarios={scenarios as any}
onGoToSession={() => router.replace('/(tabs)/session')}
/>
)
}
Loading
Loading