diff --git a/apps/electron/package.json b/apps/electron/package.json index fdd3dd3c5..38b615177 100644 --- a/apps/electron/package.json +++ b/apps/electron/package.json @@ -1,6 +1,6 @@ { "name": "@proma/electron", - "version": "0.17.15", + "version": "0.17.16", "description": "Proma next gen ai software with general agents - Electron App", "main": "dist/main.cjs", "author": { diff --git a/apps/electron/src/renderer/App.tsx b/apps/electron/src/renderer/App.tsx index f9e72d31f..4395f673e 100644 --- a/apps/electron/src/renderer/App.tsx +++ b/apps/electron/src/renderer/App.tsx @@ -1,6 +1,7 @@ import * as React from 'react' import { useAtom, useStore } from 'jotai' import { AppShell } from './components/app-shell/AppShell' +import { InitialModelSetupView } from './components/onboarding/InitialModelSetupView' import { OnboardingView } from './components/onboarding/OnboardingView' import { TutorialBanner } from './components/tutorial/TutorialBanner' import { EnvironmentCheckDialog } from './components/environment/EnvironmentCheckDialog' @@ -17,6 +18,7 @@ import { onboardingReplayRequestedAtom } from './atoms/onboarding' import { settingsOpenAtom, settingsTabAtom } from './atoms/settings-tab' import { tabsAtom, activeTabIdAtom, openTab, TUTORIAL_TAB_ID } from './atoms/tab-atoms' import { hasCompletedCurrentOnboarding } from '../types' +import { requiresInitialModelSetup } from './lib/model-setup' import hopperSeasideWhiteHouse from './assets/onboarding/hopper-seaside-white-house.png' import promaMarkWhite from './assets/onboarding/proma-mark-white.svg' import type { AppShellContextType } from './contexts/AppShellContext' @@ -27,6 +29,8 @@ export default function App(): React.ReactElement { const store = useStore() const [isLoading, setIsLoading] = React.useState(true) const [showOnboarding, setShowOnboarding] = React.useState(false) + const [showInitialModelSetup, setShowInitialModelSetup] = React.useState(false) + const [initialModelSetupCreatesWelcome, setInitialModelSetupCreatesWelcome] = React.useState(false) const [onboardingReplayRequested, setOnboardingReplayRequested] = useAtom(onboardingReplayRequestedAtom) const [isReplayingOnboarding, setIsReplayingOnboarding] = React.useState(false) const isWindows = React.useMemo(() => detectIsWindows(), []) @@ -37,9 +41,14 @@ export default function App(): React.ReactElement { React.useEffect(() => { const initialize = async () => { try { - const settings = await window.electronAPI.getSettings() + const [settings, channels] = await Promise.all([ + window.electronAPI.getSettings(), + window.electronAPI.listChannels(), + ]) if (!hasCompletedCurrentOnboarding(settings)) { setShowOnboarding(true) + } else if (requiresInitialModelSetup(channels)) { + setShowInitialModelSetup(true) } } catch (error) { console.error('[App] 初始化失败:', error) @@ -60,18 +69,7 @@ export default function App(): React.ReactElement { setOnboardingReplayRequested(false) }, [isLoading, onboardingReplayRequested, setOnboardingReplayRequested]) - // 完成 onboarding 回调:创建欢迎对话,可选打开教程 Tab - const handleOnboardingComplete = async (openTutorial?: boolean) => { - const replayingOnboarding = isReplayingOnboarding - setShowOnboarding(false) - setIsReplayingOnboarding(false) - - if (replayingOnboarding) { - store.set(settingsTabAtom, 'onboarding') - store.set(settingsOpenAtom, true) - return - } - + const enterProma = async (openTutorial?: boolean): Promise => { if (openTutorial) { const tabs = store.get(tabsAtom) const result = openTab(tabs, { type: 'tutorial', sessionId: TUTORIAL_TAB_ID, title: 'Proma 使用教程' }) @@ -100,6 +98,45 @@ export default function App(): React.ReactElement { } } + // 完成 onboarding 后,首次没有保存过渠道的用户必须先走完设置页的创建表单。 + const handleOnboardingComplete = async (openTutorial?: boolean): Promise => { + const replayingOnboarding = isReplayingOnboarding + setIsReplayingOnboarding(false) + + if (replayingOnboarding) { + setShowOnboarding(false) + store.set(settingsTabAtom, 'onboarding') + store.set(settingsOpenAtom, true) + return + } + + try { + const [settings, channels] = await Promise.all([ + window.electronAPI.getSettings(), + window.electronAPI.listChannels(), + ]) + if (requiresInitialModelSetup(channels)) { + setInitialModelSetupCreatesWelcome(true) + setShowInitialModelSetup(true) + setShowOnboarding(false) + return + } + } catch (error) { + // 无法读取渠道时不阻止用户进入应用,避免临时 IPC 错误造成无法启动。 + console.error('[App] 检查首次模型配置失败:', error) + } + + setShowOnboarding(false) + await enterProma(openTutorial) + } + + const handleInitialModelSetupComplete = async (): Promise => { + setShowInitialModelSetup(false) + if (!initialModelSetupCreatesWelcome) return + setInitialModelSetupCreatesWelcome(false) + await enterProma() + } + // 加载中状态 if (isLoading) { return @@ -140,6 +177,7 @@ export default function App(): React.ReactElement { + {showInitialModelSetup && } ) } diff --git a/apps/electron/src/renderer/assets/models/official/claude.png b/apps/electron/src/renderer/assets/models/official/claude.png new file mode 100644 index 000000000..560598ed1 Binary files /dev/null and b/apps/electron/src/renderer/assets/models/official/claude.png differ diff --git a/apps/electron/src/renderer/assets/models/official/deepseek.png b/apps/electron/src/renderer/assets/models/official/deepseek.png new file mode 100644 index 000000000..04b181f2a Binary files /dev/null and b/apps/electron/src/renderer/assets/models/official/deepseek.png differ diff --git a/apps/electron/src/renderer/assets/models/official/moonshot.png b/apps/electron/src/renderer/assets/models/official/moonshot.png new file mode 100644 index 000000000..c77e05726 Binary files /dev/null and b/apps/electron/src/renderer/assets/models/official/moonshot.png differ diff --git a/apps/electron/src/renderer/assets/models/official/openai.png b/apps/electron/src/renderer/assets/models/official/openai.png new file mode 100644 index 000000000..394ebc75b Binary files /dev/null and b/apps/electron/src/renderer/assets/models/official/openai.png differ diff --git a/apps/electron/src/renderer/assets/models/official/zhipu.png b/apps/electron/src/renderer/assets/models/official/zhipu.png new file mode 100644 index 000000000..1cbf1682d Binary files /dev/null and b/apps/electron/src/renderer/assets/models/official/zhipu.png differ diff --git a/apps/electron/src/renderer/components/onboarding/InitialModelSetupView.tsx b/apps/electron/src/renderer/components/onboarding/InitialModelSetupView.tsx new file mode 100644 index 000000000..731f1d27d --- /dev/null +++ b/apps/electron/src/renderer/components/onboarding/InitialModelSetupView.tsx @@ -0,0 +1,55 @@ +import type { Channel } from '@proma/shared' +import { CheckCircle2 } from 'lucide-react' +import { ChannelForm } from '@/components/settings/ChannelForm' +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' + +interface InitialModelSetupViewProps { + onComplete: () => void +} + +/** + * 首次使用时在已加载的主界面上覆盖渠道创建 Dialog。 + * 这让用户能看到 Proma 已经可用,同时要求先保存一个渠道配置再开始交互。 + */ +export function InitialModelSetupView({ onComplete }: InitialModelSetupViewProps): React.ReactElement { + const handleSaved = (channel?: Channel): void => { + if (channel) onComplete() + } + + return ( + undefined}> + event.preventDefault()} + onPointerDownOutside={(event) => event.preventDefault()} + > +
+ +
+
+ 添加模型渠道 + + 完成这一步后即可开始使用 Proma;模型可稍后再添加。 + +
+ + undefined} + /> +
+
+
+ ) +} diff --git a/apps/electron/src/renderer/components/settings/ChannelForm.tsx b/apps/electron/src/renderer/components/settings/ChannelForm.tsx index 5cfab4075..439794936 100644 --- a/apps/electron/src/renderer/components/settings/ChannelForm.tsx +++ b/apps/electron/src/renderer/components/settings/ChannelForm.tsx @@ -53,6 +53,11 @@ import { resolveOpenAIResponsesUrl, } from '@proma/core' import { getProviderLogo } from '@/lib/model-logo' +import openaiModelLogo from '@/assets/models/official/openai.png' +import claudeModelLogo from '@/assets/models/official/claude.png' +import deepseekModelLogo from '@/assets/models/official/deepseek.png' +import moonshotModelLogo from '@/assets/models/official/moonshot.png' +import zhipuModelLogo from '@/assets/models/official/zhipu.png' import { ScrollArea } from '@/components/ui/scroll-area' import { AlertDialog, @@ -75,6 +80,8 @@ import { interface ChannelFormProps { /** 编辑模式下传入已有渠道,创建模式传 null */ channel: Channel | null + /** 首次配置时隐藏返回操作,完成创建后才进入主工作区。 */ + initialSetup?: boolean onSaved: (channel?: Channel) => void onCancel: () => void } @@ -198,7 +205,65 @@ function buildZhipuTeamSecret(secret: ZhipuTeamSecretForm): string { /** auto-save 防抖延迟 */ const AUTO_SAVE_DELAY = 600 -export function ChannelForm({ channel, onSaved, onCancel }: ChannelFormProps): React.ReactElement { +const OFFICIAL_MODEL_CHANNELS = [ + { provider: 'OpenAI', model: 'GPT 5.6 最新模型', logo: openaiModelLogo }, + { provider: 'Anthropic', model: 'Claude 最新模型', logo: claudeModelLogo }, + { provider: 'DeepSeek', model: 'DeepSeek V4', logo: deepseekModelLogo }, + { provider: 'Moonshot', model: 'Kimi K3', logo: moonshotModelLogo }, + { provider: '智谱', model: 'GLM-5.2', logo: zhipuModelLogo }, +] as const + +function openPromaCommercialDownload(): void { + void window.electronAPI.openExternal('https://proma.cool/download') +} + +function OfficialCloudChannelNotice(): React.ReactElement { + return ( + +
+
+ Proma 商业版 +
+ +
+

当前 Proma 官方 Agent 渠道

+
    + {OFFICIAL_MODEL_CHANNELS.map(({ provider, model, logo }) => ( +
  • + + {provider} · {model} +
  • + ))} +
+
+ +
+

+ 官方托管链路提供 Agent 协议兼容与模型健康监控;精选模型享受 Proma Cloud 优惠,并包含 WebSearch、GPT Image 2 等内嵌能力。 +

+ +
+ +

+ 可用模型、价格和权益会随时间调整,以商业版应用内当期展示为准。 +

+
+
+ ) +} + +export function ChannelForm({ channel, initialSetup = false, onSaved, onCancel }: ChannelFormProps): React.ReactElement { const isEdit = channel !== null // 表单状态 @@ -783,9 +848,9 @@ export function ChannelForm({ channel, onSaved, onCancel }: ChannelFormProps): R } } - /** 创建渠道(仅新建模式) */ + /** 创建渠道(仅新建模式)。首次 onboarding 可先保存空模型渠道,后续再补充模型。 */ const handleCreate = async (): Promise => { - if (models.length === 0) { + if (!initialSetup && models.length === 0) { toast.warning('尚未配置模型,建议先从供应商获取或手动添加', { id: 'no-models-warn' }) return } @@ -863,16 +928,22 @@ export function ChannelForm({ channel, onSaved, onCancel }: ChannelFormProps): R return (
+ {!isEdit && } + {/* 标题栏 */}
- + {!initialSetup && ( + + )}

{isEdit ? '编辑模型配置' : '添加模型配置'}

diff --git a/apps/electron/src/renderer/lib/model-setup.ts b/apps/electron/src/renderer/lib/model-setup.ts new file mode 100644 index 000000000..83ccc9abb --- /dev/null +++ b/apps/electron/src/renderer/lib/model-setup.ts @@ -0,0 +1,9 @@ +import type { Channel } from '@proma/shared' + +/** + * 首次进入 Proma 前只要求用户至少保存过一个渠道。 + * 已有用户即使暂时停用渠道,也不应因为升级被重新拦截。 + */ +export function requiresInitialModelSetup(channels: readonly Channel[]): boolean { + return channels.length === 0 +}