feat(minapps): add proper region/language tags for all mini apps (#12636)

### What this PR does

为所有小程序添加了正确的地区(supportedRegions)和语言(locales)标签,并优化了过滤逻辑。

### Before this PR

- 部分小程序缺少 supportedRegions 和 locales 标签
- 用户手动固定的地区限制小程序在切换地区后会消失

### After this PR

- 所有小程序都配置了正确的 supportedRegions 和 locales 标签
- 用户主动固定的小程序不再受地区/语言过滤影响,始终显示

### Why we need it

1. 地区过滤: Global 用户不应该看到仅中国可用的小程序
2. 语言过滤: 用户应该看到支持其界面语言的小程序
3. 用户体验: 用户固定的小程序代表明确意图,不应被过滤

### Special notes for your reviewer

主要修改文件:
- src/renderer/src/config/minapps.ts: 添加/修正小程序标签
- src/renderer/src/hooks/useMinapps.ts: 优化过滤逻辑,pinned apps 不受过滤影响

### Release note

feat(minapps): 已添加所有小程序地区/语言标签,固定小程序不再受地区过滤影响

---------

Co-authored-by: SuYao <sy20010504@gmail.com>
Co-authored-by: Phantom <eurfelux@gmail.com>
This commit is contained in:
George·Dong
2026-02-25 10:03:44 +08:00
committed by GitHub
parent a030bd9895
commit 7e7277153a
22 changed files with 376 additions and 132 deletions

View File

@@ -42,6 +42,7 @@ export enum IpcChannel {
App_SetFullScreen = 'app:set-full-screen',
App_IsFullScreen = 'app:is-full-screen',
App_GetSystemFonts = 'app:get-system-fonts',
App_GetIpCountry = 'app:get-ip-country',
APP_CrashRenderProcess = 'app:crash-render-process',
App_MacIsProcessTrusted = 'app:mac-is-process-trusted',

View File

@@ -7,6 +7,7 @@ import { loggerService } from '@logger'
import { isLinux, isMac, isPortable, isWin } from '@main/constant'
import { generateSignature } from '@main/integration/cherryai'
import anthropicService from '@main/services/AnthropicService'
import { getIpCountry } from '@main/utils/ipService'
import {
autoDiscoverGitBash,
checkGitAvailable,
@@ -307,6 +308,11 @@ export async function registerIpc(mainWindow: BrowserWindow, app: Electron.App)
}
})
// Get IP Country
ipcMain.handle(IpcChannel.App_GetIpCountry, async () => {
return getIpCountry()
})
ipcMain.handle(IpcChannel.Config_Set, (_, key: string, value: any, isNotify: boolean = false) => {
configManager.set(key, value, isNotify)
})

View File

@@ -146,6 +146,7 @@ const api = {
setFullScreen: (value: boolean): Promise<void> => ipcRenderer.invoke(IpcChannel.App_SetFullScreen, value),
isFullScreen: (): Promise<boolean> => ipcRenderer.invoke(IpcChannel.App_IsFullScreen),
getSystemFonts: (): Promise<string[]> => ipcRenderer.invoke(IpcChannel.App_GetSystemFonts),
getIpCountry: (): Promise<string> => ipcRenderer.invoke(IpcChannel.App_GetIpCountry),
mockCrashRenderProcess: () => ipcRenderer.invoke(IpcChannel.APP_CrashRenderProcess),
mac: {
isProcessTrusted: (): Promise<boolean> => ipcRenderer.invoke(IpcChannel.App_MacIsProcessTrusted),

View File

@@ -34,6 +34,8 @@ const MinApp: FC<Props> = ({ app, onClick, size = 60, isLast }) => {
const navigate = useNavigate()
const isPinned = pinned.some((p) => p.id === app.id)
const isVisible = minapps.some((m) => m.id === app.id)
// Pinned apps should always be visible regardless of region/locale filtering
const shouldShow = isVisible || isPinned
const isActive = minappShow && currentMinappId === app.id
const isOpened = openedKeepAliveMinapps.some((item) => item.id === app.id)
const { isTopNavbar } = useNavbarPosition()
@@ -107,7 +109,7 @@ const MinApp: FC<Props> = ({ app, onClick, size = 60, isLast }) => {
: [])
]
if (!isVisible) {
if (!shouldShow) {
return null
}

View File

@@ -79,7 +79,8 @@ const loadCustomMiniApp = async (): Promise<MinAppType[]> => {
...app,
type: 'Custom',
logo: app.logo && app.logo !== '' ? app.logo : ApplicationLogo,
addTime: app.addTime || now
addTime: app.addTime || now,
supportedRegions: ['CN', 'Global'] // Custom mini apps should always be visible for all regions
}))
} catch (error) {
logger.error('Failed to load custom mini apps:', error as Error)
@@ -94,111 +95,118 @@ const ORIGIN_DEFAULT_MIN_APPS: MinAppType[] = [
name: 'ChatGPT',
url: 'https://chatgpt.com/',
logo: OpenAiProviderLogo,
bodered: true
bodered: true,
supportedRegions: ['CN', 'Global']
},
{
id: 'gemini',
name: 'Gemini',
url: 'https://gemini.google.com/',
logo: GeminiAppLogo,
bodered: true
bodered: true,
supportedRegions: ['CN', 'Global']
},
{
id: 'silicon',
name: 'SiliconFlow',
url: 'https://cloud.siliconflow.cn/playground/chat',
logo: SiliconFlowProviderLogo
logo: SiliconFlowProviderLogo,
supportedRegions: ['CN', 'Global']
},
{
id: 'deepseek',
name: 'DeepSeek',
url: 'https://chat.deepseek.com/',
logo: DeepSeekProviderLogo
logo: DeepSeekProviderLogo,
supportedRegions: ['CN', 'Global']
},
{
id: 'yi',
name: 'Wanzhi',
nameKey: 'minapps.wanzhi',
locales: ['zh-CN', 'zh-TW'],
url: 'https://www.wanzhi.com/',
logo: WanZhiAppLogo,
bodered: true
bodered: true,
supportedRegions: ['CN']
},
{
id: 'zhipu',
name: 'ChatGLM',
nameKey: 'minapps.chatglm',
locales: ['zh-CN', 'zh-TW'],
url: 'https://chatglm.cn/main/alltoolsdetail',
logo: ZhipuProviderLogo,
bodered: true
bodered: true,
supportedRegions: ['CN']
},
{
id: 'moonshot',
name: 'Kimi',
locales: ['zh-CN', 'zh-TW'],
url: 'https://kimi.moonshot.cn/',
logo: KimiAppLogo
logo: KimiAppLogo,
supportedRegions: ['CN', 'Global']
},
{
id: 'baichuan',
name: 'Baichuan',
nameKey: 'minapps.baichuan',
locales: ['zh-CN', 'zh-TW'],
url: 'https://ying.baichuan-ai.com/chat',
logo: BaicuanAppLogo
logo: BaicuanAppLogo,
supportedRegions: ['CN']
},
{
id: 'dashscope',
name: 'Qwen',
nameKey: 'minapps.qwen',
locales: ['zh-CN', 'zh-TW'],
url: 'https://www.qianwen.com',
logo: QwenModelLogo
logo: QwenModelLogo,
supportedRegions: ['CN']
},
{
id: 'stepfun',
name: 'Stepfun',
nameKey: 'minapps.stepfun',
locales: ['zh-CN', 'zh-TW'],
url: 'https://stepfun.com',
logo: StepfunAppLogo,
bodered: true
bodered: true,
supportedRegions: ['CN']
},
{
id: 'doubao',
name: 'Doubao',
nameKey: 'minapps.doubao',
locales: ['zh-CN', 'zh-TW'],
url: 'https://www.doubao.com/chat/',
logo: DoubaoAppLogo
logo: DoubaoAppLogo,
supportedRegions: ['CN']
},
{
id: 'cici',
name: 'Cici',
url: 'https://www.cici.com/chat/',
logo: CiciAppLogo
logo: CiciAppLogo,
supportedRegions: ['CN', 'Global']
},
{
id: 'minimax',
name: 'Hailuo',
nameKey: 'minapps.hailuo',
locales: ['zh-CN', 'zh-TW'],
url: 'https://chat.minimaxi.com/',
logo: HailuoModelLogo,
bodered: true
bodered: true,
supportedRegions: ['CN']
},
{
id: 'groq',
name: 'Groq',
url: 'https://chat.groq.com/',
logo: GroqProviderLogo
logo: GroqProviderLogo,
supportedRegions: ['CN', 'Global']
},
{
id: 'anthropic',
name: 'Claude',
url: 'https://claude.ai/',
logo: ClaudeAppLogo
logo: ClaudeAppLogo,
supportedRegions: ['CN', 'Global']
},
{
id: 'google',
@@ -208,116 +216,123 @@ const ORIGIN_DEFAULT_MIN_APPS: MinAppType[] = [
bodered: true,
style: {
padding: 5
}
},
supportedRegions: ['CN', 'Global']
},
{
id: 'baidu-ai-chat',
name: 'Wenxin',
nameKey: 'minapps.wenxin',
locales: ['zh-CN', 'zh-TW'],
logo: BaiduAiAppLogo,
url: 'https://yiyan.baidu.com/'
url: 'https://yiyan.baidu.com/',
supportedRegions: ['CN', 'Global']
},
{
id: 'baidu-ai-search',
name: 'Baidu AI Search',
nameKey: 'minapps.baidu-ai-search',
locales: ['zh-CN', 'zh-TW'],
logo: BaiduAiSearchLogo,
url: 'https://chat.baidu.com/',
bodered: true,
style: {
padding: 5
}
},
supportedRegions: ['CN']
},
{
id: 'tencent-yuanbao',
name: 'Tencent Yuanbao',
nameKey: 'minapps.tencent-yuanbao',
locales: ['zh-CN', 'zh-TW'],
logo: TencentYuanbaoAppLogo,
url: 'https://yuanbao.tencent.com/chat',
bodered: true
bodered: true,
supportedRegions: ['CN', 'Global']
},
{
id: 'sensetime-chat',
name: 'Sensechat',
nameKey: 'minapps.sensechat',
locales: ['zh-CN', 'zh-TW'],
logo: SensetimeAppLogo,
url: 'https://chat.sensetime.com/wb/chat',
bodered: true
bodered: true,
supportedRegions: ['CN']
},
{
id: 'spark-desk',
name: 'SparkDesk',
locales: ['zh-CN', 'zh-TW'],
logo: SparkDeskAppLogo,
url: 'https://xinghuo.xfyun.cn/desk'
url: 'https://xinghuo.xfyun.cn/desk',
supportedRegions: ['CN']
},
{
id: 'metaso',
name: 'Metaso',
nameKey: 'minapps.metaso',
locales: ['zh-CN', 'zh-TW'],
logo: MetasoAppLogo,
url: 'https://metaso.cn/'
url: 'https://metaso.cn/',
supportedRegions: ['CN', 'Global']
},
{
id: 'poe',
name: 'Poe',
logo: PoeAppLogo,
url: 'https://poe.com'
url: 'https://poe.com',
supportedRegions: ['CN', 'Global']
},
{
id: 'perplexity',
name: 'Perplexity',
logo: PerplexityAppLogo,
url: 'https://www.perplexity.ai/'
url: 'https://www.perplexity.ai/',
supportedRegions: ['CN', 'Global']
},
{
id: 'devv',
name: 'DEVV_',
logo: DevvAppLogo,
url: 'https://devv.ai/'
url: 'https://devv.ai/',
supportedRegions: ['CN', 'Global']
},
{
id: 'tiangong-ai',
name: 'Tiangong AI',
nameKey: 'minapps.tiangong-ai',
locales: ['zh-CN', 'zh-TW'],
logo: TiangongAiLogo,
url: 'https://www.tiangong.cn/',
bodered: true
bodered: true,
supportedRegions: ['CN']
},
{
id: 'Felo',
name: 'Felo',
logo: FeloAppLogo,
url: 'https://felo.ai/',
bodered: true
bodered: true,
supportedRegions: ['CN', 'Global']
},
{
id: 'duckduckgo',
name: 'DuckDuckGo',
logo: DuckDuckGoAppLogo,
url: 'https://duck.ai'
url: 'https://duck.ai',
supportedRegions: ['CN', 'Global']
},
{
id: 'bolt',
name: 'bolt',
logo: BoltAppLogo,
url: 'https://bolt.new/',
bodered: true
bodered: true,
supportedRegions: ['CN', 'Global']
},
{
id: 'nm',
name: 'Nami AI',
nameKey: 'minapps.nami-ai',
locales: ['zh-CN', 'zh-TW'],
logo: NamiAiLogo,
url: 'https://bot.n.cn/',
bodered: true
bodered: true,
supportedRegions: ['CN']
},
{
id: 'thinkany',
@@ -327,82 +342,92 @@ const ORIGIN_DEFAULT_MIN_APPS: MinAppType[] = [
bodered: true,
style: {
padding: 5
}
},
supportedRegions: ['CN', 'Global']
},
{
id: 'github-copilot',
name: 'GitHub Copilot',
logo: GithubCopilotLogo,
url: 'https://github.com/copilot'
url: 'https://github.com/copilot',
supportedRegions: ['CN', 'Global']
},
{
id: 'genspark',
name: 'Genspark',
logo: GensparkLogo,
url: 'https://www.genspark.ai/'
url: 'https://www.genspark.ai/',
supportedRegions: ['CN', 'Global']
},
{
id: 'grok',
name: 'Grok',
logo: GrokAppLogo,
url: 'https://grok.com',
bodered: true
bodered: true,
supportedRegions: ['CN', 'Global']
},
{
id: 'grok-x',
name: 'Grok / X',
logo: GrokXAppLogo,
url: 'https://x.com/i/grok',
bodered: true
bodered: true,
supportedRegions: ['CN', 'Global']
},
{
id: 'qwenlm',
name: 'QwenChat',
locales: ['zh-CN', 'zh-TW'],
logo: QwenlmAppLogo,
url: 'https://chat.qwen.ai'
url: 'https://chat.qwen.ai',
supportedRegions: ['CN', 'Global']
},
{
id: 'flowith',
name: 'Flowith',
logo: FlowithAppLogo,
url: 'https://www.flowith.io/',
bodered: true
bodered: true,
supportedRegions: ['CN', 'Global']
},
{
id: '3mintop',
name: '3MinTop',
logo: ThreeMinTopAppLogo,
url: 'https://3min.top',
bodered: false
bodered: false,
supportedRegions: ['CN', 'Global']
},
{
id: 'aistudio',
name: 'AI Studio',
logo: AIStudioLogo,
url: 'https://aistudio.google.com/'
url: 'https://aistudio.google.com/',
supportedRegions: ['CN', 'Global']
},
{
id: 'xiaoyi',
name: 'Xiaoyi',
nameKey: 'minapps.xiaoyi',
locales: ['zh-CN', 'zh-TW'],
logo: XiaoYiAppLogo,
url: 'https://xiaoyi.huawei.com/chat/',
bodered: true
bodered: true,
supportedRegions: ['CN']
},
{
id: 'notebooklm',
name: 'NotebookLM',
logo: NotebookLMAppLogo,
url: 'https://notebooklm.google.com/'
url: 'https://notebooklm.google.com/',
supportedRegions: ['CN', 'Global']
},
{
id: 'coze',
name: 'Coze',
logo: CozeAppLogo,
url: 'https://www.coze.com/space',
bodered: true
bodered: true,
supportedRegions: ['CN', 'Global']
},
{
id: 'dify',
@@ -412,68 +437,74 @@ const ORIGIN_DEFAULT_MIN_APPS: MinAppType[] = [
bodered: true,
style: {
padding: 5
}
},
supportedRegions: ['CN', 'Global']
},
{
id: 'wpslingxi',
name: 'WPS AI',
nameKey: 'minapps.wps-copilot',
locales: ['zh-CN', 'zh-TW'],
logo: WPSLingXiLogo,
url: 'https://copilot.wps.cn/',
bodered: true
bodered: true,
supportedRegions: ['CN']
},
{
id: 'lechat',
name: 'LeChat',
logo: LeChatLogo,
url: 'https://chat.mistral.ai/chat',
bodered: true
bodered: true,
supportedRegions: ['CN', 'Global']
},
{
id: 'abacus',
name: 'Abacus',
logo: AbacusLogo,
url: 'https://apps.abacus.ai/chatllm',
bodered: true
bodered: true,
supportedRegions: ['CN', 'Global']
},
{
id: 'lambdachat',
name: 'Lambda Chat',
logo: LambdaChatLogo,
url: 'https://lambda.chat/',
bodered: true
bodered: true,
supportedRegions: ['CN', 'Global']
},
{
id: 'monica',
name: 'Monica',
logo: MonicaLogo,
url: 'https://monica.im/home/',
bodered: true
bodered: true,
supportedRegions: ['CN', 'Global']
},
{
id: 'you',
name: 'You',
logo: YouLogo,
url: 'https://you.com/'
url: 'https://you.com/',
supportedRegions: ['CN', 'Global']
},
{
id: 'zhihu',
name: 'Zhihu Zhida',
nameKey: 'minapps.zhihu',
locales: ['zh-CN', 'zh-TW'],
logo: ZhihuAppLogo,
url: 'https://zhida.zhihu.com/',
bodered: true
bodered: true,
supportedRegions: ['CN']
},
{
id: 'dangbei',
name: 'Dangbei AI',
nameKey: 'minapps.dangbei',
locales: ['zh-CN', 'zh-TW'],
logo: DangbeiLogo,
url: 'https://ai.dangbei.com/',
bodered: true
bodered: true,
supportedRegions: ['CN', 'Global']
},
{
id: `zai`,
@@ -483,7 +514,8 @@ const ORIGIN_DEFAULT_MIN_APPS: MinAppType[] = [
bodered: true,
style: {
padding: 10
}
},
supportedRegions: ['CN', 'Global']
},
{
id: 'n8n',
@@ -493,27 +525,28 @@ const ORIGIN_DEFAULT_MIN_APPS: MinAppType[] = [
bodered: true,
style: {
padding: 5
}
},
supportedRegions: ['CN', 'Global']
},
{
id: 'longcat',
name: 'LongCat',
locales: ['zh-CN', 'zh-TW'],
logo: LongCatAppLogo,
url: 'https://longcat.chat/',
bodered: true
bodered: true,
supportedRegions: ['CN', 'Global']
},
{
id: 'ling',
name: 'Ant Ling',
nameKey: 'minapps.ant-ling',
locales: ['zh-CN', 'zh-TW'],
url: 'https://ling.tbox.cn/chat',
logo: LingAppLogo,
bodered: true,
style: {
padding: 6
}
},
supportedRegions: ['CN', 'Global']
},
{
id: 'huggingchat',
@@ -523,7 +556,8 @@ const ORIGIN_DEFAULT_MIN_APPS: MinAppType[] = [
bodered: true,
style: {
padding: 6
}
},
supportedRegions: ['CN', 'Global']
}
]

View File

@@ -2,42 +2,101 @@ import { allMinApps } from '@renderer/config/minapps'
import type { RootState } from '@renderer/store'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import { setDisabledMinApps, setMinApps, setPinnedMinApps } from '@renderer/store/minapps'
import type { LanguageVarious, MinAppType } from '@renderer/types'
import { useCallback, useMemo } from 'react'
import { setDetectedRegion } from '@renderer/store/runtime'
import type { MinAppRegion, MinAppType } from '@renderer/types'
import { useCallback, useEffect, useMemo, useRef } from 'react'
/**
* Data Flow Design:
*
* PRINCIPLE: Locale filtering is a VIEW concern, not a DATA concern.
* PRINCIPLE: Region filtering is a VIEW concern, not a DATA concern.
*
* - Redux stores ALL apps (including locale-restricted ones) to preserve user preferences
* - allMinApps is the template data source containing locale definitions
* - This hook applies locale filtering only when READING for UI display
* - When WRITING, locale-hidden apps are merged back to prevent data loss
* - Redux stores ALL apps (including region-restricted ones) to preserve user preferences
* - allMinApps is the template data source containing region definitions
* - This hook applies region filtering only when READING for UI display
* - When WRITING, hidden apps are merged back to prevent data loss
*/
// Check if app should be visible for the given locale
const isVisibleForLocale = (app: MinAppType, language: LanguageVarious): boolean => {
if (!app.locales) return true
return app.locales.includes(language)
/**
* Check if app should be visible for the given region.
*
* Region-based visibility rules:
* 1. CN users see everything
* 2. Global users: only show apps with supportedRegions including 'Global'
* (apps without supportedRegions field are treated as CN-only)
*/
const isVisibleForRegion = (app: MinAppType, region: MinAppRegion): boolean => {
// CN users see everything
if (region === 'CN') return true
// Global users: check if app supports international
// If no supportedRegions field, treat as CN-only (hidden from Global users)
if (!app.supportedRegions || app.supportedRegions.length === 0) {
return false
}
return app.supportedRegions.includes('Global')
}
// Filter apps by locale - only show apps that match current language
const filterByLocale = (apps: MinAppType[], language: LanguageVarious): MinAppType[] => {
return apps.filter((app) => isVisibleForLocale(app, language))
// Filter apps by region
const filterByRegion = (apps: MinAppType[], region: MinAppRegion): MinAppType[] => {
return apps.filter((app) => isVisibleForRegion(app, region))
}
// Get locale-hidden apps from allMinApps for the current language
// This uses allMinApps as source of truth for locale definitions
const getLocaleHiddenApps = (language: LanguageVarious): MinAppType[] => {
return allMinApps.filter((app) => !isVisibleForLocale(app, language))
// Get region-hidden apps from allMinApps for the current region
const getRegionHiddenApps = (region: MinAppRegion): MinAppType[] => {
return allMinApps.filter((app) => !isVisibleForRegion(app, region))
}
// Module-level promise to ensure only one IP detection request is made
let regionDetectionPromise: Promise<MinAppRegion> | null = null
// Detect user region via IPC call to main process (cached at module level)
const detectUserRegion = async (): Promise<MinAppRegion> => {
// Return existing promise if detection is already in progress
if (regionDetectionPromise) {
return regionDetectionPromise
}
regionDetectionPromise = (async () => {
try {
const country = await window.api.getIpCountry()
return country.toUpperCase() === 'CN' ? 'CN' : 'Global'
} catch {
// If detection fails, assume CN to show all apps (conservative approach)
return 'CN'
}
})()
return regionDetectionPromise
}
export const useMinapps = () => {
const { enabled, disabled, pinned } = useAppSelector((state: RootState) => state.minapps)
const language = useAppSelector((state: RootState) => state.settings.language)
const minAppRegionSetting = useAppSelector((state: RootState) => state.settings.minAppRegion)
const detectedRegion = useAppSelector((state: RootState) => state.runtime.detectedRegion)
const dispatch = useAppDispatch()
// Track if this hook instance has initiated detection to avoid duplicate requests
const hasInitiatedDetection = useRef(false)
// Compute effective region: use cached detection result or manual setting
const effectiveRegion: MinAppRegion = minAppRegionSetting === 'auto' ? (detectedRegion ?? 'CN') : minAppRegionSetting
// Only detect region once globally when in 'auto' mode and not yet detected
useEffect(() => {
const initRegion = async () => {
// Skip if not in auto mode, already detected, or this instance already initiated
if (minAppRegionSetting !== 'auto' || detectedRegion !== null || hasInitiatedDetection.current) {
return
}
hasInitiatedDetection.current = true
const detected = await detectUserRegion()
dispatch(setDetectedRegion(detected))
}
initRegion()
}, [minAppRegionSetting, detectedRegion, dispatch])
const mapApps = useCallback(
(apps: MinAppType[]) => apps.map((app) => allMinApps.find((item) => item.id === app.id) || app),
[]
@@ -54,30 +113,39 @@ export const useMinapps = () => {
[mapApps]
)
// READ: Get apps filtered by locale for UI display
// READ: Get apps filtered by region for UI display
const minapps = useMemo(() => {
const allApps = getAllApps(enabled, disabled)
const disabledIds = new Set(disabled.map((app) => app.id))
const withoutDisabled = allApps.filter((app) => !disabledIds.has(app.id))
return filterByLocale(withoutDisabled, language)
}, [enabled, disabled, language, getAllApps])
return filterByRegion(withoutDisabled, effectiveRegion)
}, [enabled, disabled, effectiveRegion, getAllApps])
const disabledApps = useMemo(() => filterByLocale(mapApps(disabled), language), [disabled, language, mapApps])
const pinnedApps = useMemo(() => filterByLocale(mapApps(pinned), language), [pinned, language, mapApps])
const disabledApps = useMemo(
() => filterByRegion(mapApps(disabled), effectiveRegion),
[disabled, effectiveRegion, mapApps]
)
// Pinned apps are always visible regardless of region/language
// User explicitly pinned apps should not be hidden
const pinnedApps = useMemo(() => mapApps(pinned), [pinned, mapApps])
// Get hidden apps for preserving user preferences when writing
const getHiddenApps = useCallback((region: MinAppRegion) => {
const regionHidden = getRegionHiddenApps(region)
const hiddenIds = new Set(regionHidden.map((app) => app.id))
return hiddenIds
}, [])
const updateMinapps = useCallback(
(visibleApps: MinAppType[]) => {
const disabledIds = new Set(disabled.map((app) => app.id))
const withoutDisabled = visibleApps.filter((app) => !disabledIds.has(app.id))
const localeHiddenApps = getLocaleHiddenApps(language)
const localeHiddenIds = new Set(localeHiddenApps.map((app) => app.id))
const preservedLocaleHidden = enabled.filter((app) => localeHiddenIds.has(app.id) && !disabledIds.has(app.id))
const hiddenIds = getHiddenApps(effectiveRegion)
const preservedHidden = enabled.filter((app) => hiddenIds.has(app.id) && !disabledIds.has(app.id))
const visibleIds = new Set(withoutDisabled.map((app) => app.id))
const toAppend = preservedLocaleHidden.filter((app) => !visibleIds.has(app.id))
const toAppend = preservedHidden.filter((app) => !visibleIds.has(app.id))
const merged = [...withoutDisabled, ...toAppend]
const existingIds = new Set(merged.map((app) => app.id))
@@ -85,37 +153,35 @@ export const useMinapps = () => {
dispatch(setMinApps([...merged, ...missingApps]))
},
[dispatch, enabled, disabled, language]
[dispatch, enabled, disabled, effectiveRegion, getHiddenApps]
)
// WRITE: Update disabled apps, preserving locale-hidden disabled apps
// WRITE: Update disabled apps, preserving hidden disabled apps
const updateDisabledMinapps = useCallback(
(visibleDisabledApps: MinAppType[]) => {
const localeHiddenApps = getLocaleHiddenApps(language)
const localeHiddenIds = new Set(localeHiddenApps.map((app) => app.id))
const preservedLocaleHidden = disabled.filter((app) => localeHiddenIds.has(app.id))
const hiddenIds = getHiddenApps(effectiveRegion)
const preservedHidden = disabled.filter((app) => hiddenIds.has(app.id))
const visibleIds = new Set(visibleDisabledApps.map((app) => app.id))
const toAppend = preservedLocaleHidden.filter((app) => !visibleIds.has(app.id))
const toAppend = preservedHidden.filter((app) => !visibleIds.has(app.id))
dispatch(setDisabledMinApps([...visibleDisabledApps, ...toAppend]))
},
[dispatch, disabled, language]
[dispatch, disabled, effectiveRegion, getHiddenApps]
)
// WRITE: Update pinned apps, preserving locale-hidden pinned apps
// WRITE: Update pinned apps, preserving hidden pinned apps
const updatePinnedMinapps = useCallback(
(visiblePinnedApps: MinAppType[]) => {
const localeHiddenApps = getLocaleHiddenApps(language)
const localeHiddenIds = new Set(localeHiddenApps.map((app) => app.id))
const preservedLocaleHidden = pinned.filter((app) => localeHiddenIds.has(app.id))
const hiddenIds = getHiddenApps(effectiveRegion)
const preservedHidden = pinned.filter((app) => hiddenIds.has(app.id))
const visibleIds = new Set(visiblePinnedApps.map((app) => app.id))
const toAppend = preservedLocaleHidden.filter((app) => !visibleIds.has(app.id))
const toAppend = preservedHidden.filter((app) => !visibleIds.has(app.id))
dispatch(setPinnedMinApps([...visiblePinnedApps, ...toAppend]))
},
[dispatch, pinned, language]
[dispatch, pinned, effectiveRegion, getHiddenApps]
)
return {

View File

@@ -4651,6 +4651,13 @@
"open_link_external": {
"title": "Open new-window links in browser"
},
"region": {
"auto": "Auto detect",
"cn": "China",
"description": "Filtering unsupported mini-programs based on the region",
"global": "Global",
"title": "Mini Program filter"
},
"reset_tooltip": "Reset to default",
"sidebar_description": "Show active mini apps in the sidebar",
"sidebar_title": "Sidebar Active Mini Apps Display",

View File

@@ -4651,6 +4651,13 @@
"open_link_external": {
"title": "在浏览器中打开新窗口链接"
},
"region": {
"auto": "自动检测",
"cn": "中国",
"description": "根据所在地区过滤不支持的小程序",
"global": "全球",
"title": "小程序区域筛选"
},
"reset_tooltip": "重置为默认值",
"sidebar_description": "设置侧边栏是否显示活跃的小程序",
"sidebar_title": "侧边栏活跃小程序显示设置",

View File

@@ -4651,6 +4651,13 @@
"open_link_external": {
"title": "在瀏覽器中開啟新視窗連結"
},
"region": {
"auto": "自動檢測",
"cn": "中國",
"description": "根據所在地區篩選不支援的小程式",
"global": "全球",
"title": "小程式區域篩選"
},
"reset_tooltip": "重設為預設值",
"sidebar_description": "設定側邊欄是否顯示活躍的小程式",
"sidebar_title": "側邊欄活躍小程式顯示設定",

View File

@@ -4651,6 +4651,13 @@
"open_link_external": {
"title": "Neue Fensterlinks im Browser öffnen"
},
"region": {
"auto": "Automatische Erkennung",
"cn": "China",
"description": "Nach Region gefilterte Mini-Programme werden nicht unterstützt",
"global": "global",
"title": "Bereichsfilterung für Mini-Programme"
},
"reset_tooltip": "Zurücksetzen auf Standardwert",
"sidebar_description": "Festlegen ob Seitenleiste aktive Mini-Apps anzeigt",
"sidebar_title": "Einstellungen für aktive Mini-Apps in Seitenleiste",

View File

@@ -4651,6 +4651,13 @@
"open_link_external": {
"title": "Άνοιγμα νέου παραθύρου σύνδεσης στον περιηγητή"
},
"region": {
"auto": "Αυτόματη ανίχνευση",
"cn": "Κίνα",
"description": "Η φιλτράρισμα των μικροπρογραμμάτων που δεν υποστηρίζονται βάσει της περιοχής σας",
"global": "παγκοσμίως",
"title": "Φίλτραρισμα περιοχής για μικρές εφαρμογές"
},
"reset_tooltip": "Επαναφορά στις προεπιλεγμένες τιμές",
"sidebar_description": "Καθορίστε εάν το ενεργό μικροπρόγραμμα θα εμφανίζεται στην πλευρική γραμμή",
"sidebar_title": "Ρυθμίσεις Εμφάνισης Ενεργού Μικροπρογράμματος στην Πλευρική Γραμμή",

View File

@@ -4651,6 +4651,13 @@
"open_link_external": {
"title": "Abrir enlace en nueva ventana del navegador"
},
"region": {
"auto": "Detección automática",
"cn": "China",
"description": "Los pequeños programas que no son compatibles se filtran según la región.",
"global": "global",
"title": "Filtro de área de la miniaplicación"
},
"reset_tooltip": "Restablecer a los valores predeterminados",
"sidebar_description": "Configura si se muestra o no en la barra lateral la miniaplicación activa",
"sidebar_title": "Visualización de miniaplicaciones activas en la barra lateral",

View File

@@ -4651,6 +4651,13 @@
"open_link_external": {
"title": "Ouvrir un nouveau lien dans une fenêtre du navigateur"
},
"region": {
"auto": "Détection automatique",
"cn": "Chine",
"description": "La filtration des mini-programmes non pris en charge selon la région n'est pas disponible",
"global": "mondial",
"title": "Filtrage par zone de la mini-application"
},
"reset_tooltip": "Réinitialiser aux valeurs par défaut",
"sidebar_description": "Définir si les applications actives doivent s'afficher dans la barre latérale",
"sidebar_title": "Affichage des applications actives dans la barre latérale",

View File

@@ -4651,6 +4651,13 @@
"open_link_external": {
"title": "新視窗のリンクをブラウザで開く"
},
"region": {
"auto": "自動検出",
"cn": "中国",
"description": "地域に基づいてサポートされていないミニアプリをフィルタリングします",
"global": "世界中",
"title": "ミニアプリ地域絞り込み"
},
"reset_tooltip": "デフォルト値にリセット",
"sidebar_description": "サイドバーにアクティブなミニアプリを表示するかどうかを設定します",
"sidebar_title": "サイドバーのアクティブなミニアプリ表示",

View File

@@ -4651,6 +4651,13 @@
"open_link_external": {
"title": "Abrir link em nova janela do navegador"
},
"region": {
"auto": "Detecção automática",
"cn": "China",
"description": "A filtragem de mini programas não suportados com base na região não é suportada",
"global": "global",
"title": "Filtro de área do mini programa"
},
"reset_tooltip": "Redefinir para os valores padrão",
"sidebar_description": "Defina se os mini aplicativos ativos serão exibidos na barra lateral",
"sidebar_title": "Exibição de Mini Aplicativos Ativos na Barra Lateral",

View File

@@ -4651,6 +4651,13 @@
"open_link_external": {
"title": "Deschide în browser linkurile care deschid ferestre noi"
},
"region": {
"auto": "Detectare automată",
"cn": "China",
"description": "Filtrarea aplicațiilor mini care nu sunt acceptate în funcție de regiunea în care vă aflați",
"global": "global",
"title": "Filtru zonă aplicație miniatură"
},
"reset_tooltip": "Resetează la implicit",
"sidebar_description": "Afișează mini-aplicațiile active în bara laterală",
"sidebar_title": "Afișare mini-aplicații active în bara laterală",

View File

@@ -4651,6 +4651,13 @@
"open_link_external": {
"title": "Открывать новые окна в браузере"
},
"region": {
"auto": "Автоматическое определение",
"cn": "Китай",
"description": "Фильтрация неподдерживаемых мини-программ в зависимости от региона",
"global": "Глобально",
"title": "Фильтрация по регионам в мини-программе"
},
"reset_tooltip": "Сбросить до значения по умолчанию",
"sidebar_description": "Настройка отображения активных мини-приложений в боковой панели",
"sidebar_title": "Отображение активных мини-приложений в боковой панели",

View File

@@ -1,15 +1,19 @@
import { UndoOutlined } from '@ant-design/icons' // 导入重置图标
import { InfoCircleOutlined, UndoOutlined } from '@ant-design/icons' // 导入重置图标和Info图标
import Selector from '@renderer/components/Selector'
import { allMinApps } from '@renderer/config/minapps'
import { useMinapps } from '@renderer/hooks/useMinapps'
import { useSettings } from '@renderer/hooks/useSettings'
import { SettingDescription, SettingDivider, SettingRowTitle, SettingTitle } from '@renderer/pages/settings'
import { useAppDispatch } from '@renderer/store'
import type { RootState } from '@renderer/store'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import {
setMaxKeepAliveMinapps,
setMinAppRegion,
setMinappsOpenLinkExternal,
setShowOpenedMinappsInSidebar
} from '@renderer/store/settings'
import { Button, message, Slider, Switch, Tooltip } from 'antd'
import type { MinAppRegionFilter } from '@renderer/types'
import { Button, Flex, message, Slider, Switch, Tooltip } from 'antd'
import type { FC } from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -20,6 +24,25 @@ import MiniAppIconsManager from './MiniAppIconsManager'
// 默认小程序缓存数量
const DEFAULT_MAX_KEEPALIVE = 3
// Region selector component with defensive default value
const RegionSelector: FC = () => {
const { t } = useTranslation()
const dispatch = useAppDispatch()
const minAppRegion = useAppSelector((state: RootState) => state.settings.minAppRegion) ?? 'auto'
const onMinAppRegionChange = (value: MinAppRegionFilter) => {
dispatch(setMinAppRegion(value))
}
const minAppRegionOptions: { value: MinAppRegionFilter; label: string }[] = [
{ value: 'auto', label: t('settings.miniapps.region.auto') },
{ value: 'CN', label: t('settings.miniapps.region.cn') },
{ value: 'Global', label: t('settings.miniapps.region.global') }
]
return <Selector size={14} value={minAppRegion} onChange={onMinAppRegionChange} options={minAppRegionOptions} />
}
const MiniAppSettings: FC = () => {
const { t } = useTranslation()
const dispatch = useAppDispatch()
@@ -94,6 +117,17 @@ const MiniAppSettings: FC = () => {
/>
</BorderedContainer>
<SettingDivider />
{/* 小程序地区设置 */}
<SettingRow style={{ height: 40, alignItems: 'center' }}>
<Flex align="center" gap={4}>
<SettingRowTitle>{t('settings.miniapps.region.title')}</SettingRowTitle>
<Tooltip title={t('settings.miniapps.region.description')} placement="right">
<InfoCircleOutlined style={{ cursor: 'pointer' }} />
</Tooltip>
</Flex>
<RegionSelector />
</SettingRow>
<SettingDivider />
<SettingRow style={{ height: 40, alignItems: 'center' }}>
<SettingLabelGroup>
<SettingRowTitle>{t('settings.miniapps.open_link_external.title')}</SettingRowTitle>

View File

@@ -3185,6 +3185,8 @@ const migrateConfig = {
assistant.defaultModel = qwen3Next80BModel
}
})
// Initialize mini app region filter setting
state.settings.minAppRegion ??= 'auto'
return state
} catch (error) {
logger.error('migrate 194 error', error as Error)

View File

@@ -17,7 +17,7 @@
import type { PayloadAction } from '@reduxjs/toolkit'
import { createSlice } from '@reduxjs/toolkit'
import { AppLogo, UserAvatar } from '@renderer/config/env'
import type { MinAppType, Topic, WebSearchStatus } from '@renderer/types'
import type { MinAppRegion, MinAppType, Topic, WebSearchStatus } from '@renderer/types'
import type { UpdateInfo } from 'builder-util-runtime'
export interface ChatState {
@@ -73,6 +73,8 @@ export interface RuntimeState {
export: ExportState
chat: ChatState
websearch: WebSearchState
/** Detected region from IP lookup (not persisted, re-detected on each app start) */
detectedRegion: MinAppRegion | null
}
export interface ExportState {
@@ -115,7 +117,8 @@ const initialState: RuntimeState = {
},
websearch: {
activeSearches: {}
}
},
detectedRegion: null
}
const runtimeSlice = createSlice({
@@ -205,6 +208,9 @@ const runtimeSlice = createSlice({
setSessionWaitingAction: (state, action: PayloadAction<{ id: string; value: boolean }>) => {
const { id, value } = action.payload
state.chat.sessionWaiting[id] = value
},
setDetectedRegion: (state, action: PayloadAction<MinAppRegion | null>) => {
state.detectedRegion = action.payload
}
}
})
@@ -235,7 +241,9 @@ export const {
setSessionWaitingAction,
// WebSearch related actions
setActiveSearches,
setWebSearchStatus
setWebSearchStatus,
// Region detection
setDetectedRegion
} = runtimeSlice.actions
export default runtimeSlice.reducer

View File

@@ -25,6 +25,7 @@ import type {
CodeStyleVarious,
LanguageVarious,
MathEngine,
MinAppRegionFilter,
OpenAIServiceTier,
PaintingProvider,
S3Config,
@@ -191,6 +192,8 @@ export interface SettingsState {
maxKeepAliveMinapps: number
showOpenedMinappsInSidebar: boolean
minappsOpenLinkExternal: boolean
/** Mini app region filter: 'auto' (detect from IP), 'CN', or 'Global' */
minAppRegion: MinAppRegionFilter
// 隐私设置
enableDataCollection: boolean
enableSpellCheck: boolean
@@ -376,6 +379,7 @@ export const initialState: SettingsState = {
maxKeepAliveMinapps: 3,
showOpenedMinappsInSidebar: true,
minappsOpenLinkExternal: false,
minAppRegion: 'auto',
enableDataCollection: false,
enableSpellCheck: false,
spellCheckLanguages: [],
@@ -798,6 +802,9 @@ const settingsSlice = createSlice({
setMinappsOpenLinkExternal: (state, action: PayloadAction<boolean>) => {
state.minappsOpenLinkExternal = action.payload
},
setMinAppRegion: (state, action: PayloadAction<MinAppRegionFilter>) => {
state.minAppRegion = action.payload
},
setEnableDataCollection: (state, action: PayloadAction<boolean>) => {
state.enableDataCollection = action.payload
},
@@ -997,6 +1004,7 @@ export const {
setMaxKeepAliveMinapps,
setShowOpenedMinappsInSidebar,
setMinappsOpenLinkExternal,
setMinAppRegion,
setEnableDataCollection,
setEnableSpellCheck,
setSpellCheckLanguages,

View File

@@ -507,8 +507,8 @@ export type MinAppType = {
name: string
/** i18n key for translatable names */
nameKey?: string
/** Locale codes where this app should be visible (e.g., ['zh-CN', 'zh-TW']) */
locales?: LanguageVarious[]
/** Regions where this app is available. If includes 'Global', shown to international users. */
supportedRegions?: MinAppRegion[]
logo?: string
url: string
// FIXME: It should be `bordered`
@@ -519,6 +519,11 @@ export type MinAppType = {
type?: 'Custom' | 'Default' // Added the 'type' property
}
/** Region types for miniapps visibility */
export type MinAppRegion = 'CN' | 'Global'
export type MinAppRegionFilter = 'auto' | MinAppRegion
export enum ThemeMode {
light = 'light',
dark = 'dark',