diff --git a/apps/desktop/src/renderer/src/App.tsx b/apps/desktop/src/renderer/src/App.tsx index b81188e..89a9f7c 100644 --- a/apps/desktop/src/renderer/src/App.tsx +++ b/apps/desktop/src/renderer/src/App.tsx @@ -377,8 +377,6 @@ export default function App(): JSX.Element { onCloseTab={browser.closeTab} onCloseAllTabs={browser.closeBrowser} onAcknowledgeRisk={browser.acknowledgeRisk} - onDetectSources={browser.detectSources} - onImportProfile={browser.importProfile} /> ) } diff --git a/apps/desktop/src/renderer/src/components/SettingsModal.tsx b/apps/desktop/src/renderer/src/components/SettingsModal.tsx index 1a47515..6390779 100644 --- a/apps/desktop/src/renderer/src/components/SettingsModal.tsx +++ b/apps/desktop/src/renderer/src/components/SettingsModal.tsx @@ -37,6 +37,8 @@ import type { PluginSettingsSchema, PluginSettingsSchemas, PluginSettingValues, import { THEME_MODES, THEME_PRESETS, applyThemeColor } from '../lib/theme' import { usePluginList } from '../plugins/store' import { PluginIconValue } from '../plugins/plugin-icons' +import { BrowserProxyForm } from './browser/BrowserProxyForm' +import { BrowserImportForm } from './browser/BrowserImportForm' type PluginSettingsTab = `plugin:${string}` type Tab = 'general' | 'model' | 'prompt' | 'plugins' | 'skills' | 'commands' | PluginSettingsTab @@ -855,7 +857,8 @@ function PluginsPanel({ (a.manifest.order ?? 100) - (b.manifest.order ?? 100) || a.manifest.id.localeCompare(b.manifest.id) ) .map((p) => { - const openSettings = p.enabled && (p.hasSettings || Boolean(p.storageSchema)) + const openSettings = + p.enabled && (p.hasSettings || Boolean(p.storageSchema) || p.manifest.id === 'browser') return (
{ const pid = tab.slice(7) + // 内置浏览器:专属二级页(代理设置 + 登录态导入),不走向声明式设置表单 + if (pid === 'browser') { + return ( +
+
+
代理设置
+
+ +
+
+
+
导入本机浏览器登录态
+
+ +
+
+
+ ) + } const schema = settingSchemas[pid] const info = plugins.find((p) => p.manifest.id === pid) const hasForm = Boolean(schema && schema.items.length > 0) diff --git a/apps/desktop/src/renderer/src/components/browser/BrowserImportForm.tsx b/apps/desktop/src/renderer/src/components/browser/BrowserImportForm.tsx new file mode 100644 index 0000000..207ac07 --- /dev/null +++ b/apps/desktop/src/renderer/src/components/browser/BrowserImportForm.tsx @@ -0,0 +1,143 @@ +/** + * 本机浏览器登录态导入表单(面板浮层与设置页二级页共用)。 + * 选择本机浏览器(Chrome/Edge/Brave/Chromium)的一个 Profile, + * 把各站点登录态(Cookie / Local Storage)迁移到受管浏览器分区。 + * 自含数据源:直接调用 window.api(browserDetectSources / browserImportProfile)。 + */ +import { useEffect, useState, type JSX } from 'react' +import { LuChevronDown } from 'react-icons/lu' +import type { BrowserImportResult, BrowserSource, BrowserUiState } from '@shared/types' + +export function BrowserImportForm(): JSX.Element { + const [state, setState] = useState(null) + const [sources, setSources] = useState(null) + const [selected, setSelected] = useState<{ sourceId: string; profileDir: string } | null>(null) + const [busy, setBusy] = useState(false) + const [result, setResult] = useState(null) + const [error, setError] = useState(null) + + useEffect(() => { + let alive = true + window.api + .browserGetState() + .then((s) => { + if (alive) setState(s) + }) + .catch(() => undefined) + const off = window.api.onBrowserEvent((event) => { + if (event.type === 'state' && alive) setState(event.state) + }) + return () => { + alive = false + off() + } + }, []) + + useEffect(() => { + let alive = true + window.api + .browserDetectSources() + .then((list) => { + if (!alive) return + setSources(list) + const first = list[0] + if (first?.profiles[0]) setSelected({ sourceId: first.id, profileDir: first.profiles[0].dir }) + }) + .catch((err) => { + if (alive) setError(err instanceof Error ? err.message : String(err)) + }) + return () => { + alive = false + } + }, []) + + const runImport = (): void => { + if (!selected) return + setBusy(true) + setError(null) + setResult(null) + window.api + .browserImportProfile(selected.sourceId, selected.profileDir) + .then(setResult) + .catch((err) => setError(err instanceof Error ? err.message : String(err))) + .finally(() => setBusy(false)) + } + + return ( +
+

+ 选择本机浏览器的一个配置(Profile),把其中各网站的登录态(Cookie / Local Storage)导入到内置受管浏览器。 + 导入前请完全退出该浏览器,并关闭本面板的所有标签页。 +

+ + {state?.imported && ( +
+ 当前导入来源:{state.imported.browser} · {state.imported.profile} + {state.imported.cookies ? '(含 Cookie)' : ''} +
+ )} + + {sources === null && !error &&

正在检测本机浏览器…

} + {sources?.length === 0 && ( +

未检测到可导入的本机浏览器(支持 Chrome / Edge / Brave / Chromium)

+ )} + +
+ {sources?.map((source) => ( +
+
+ + {source.name} + ({source.profiles.length} 个配置) +
+
+ {source.profiles.map((profile) => { + const active = selected?.sourceId === source.id && selected.profileDir === profile.dir + return ( + + ) + })} +
+
+ ))} +
+ + {error && ( +
{error}
+ )} + {result && ( +
+ {result.message} +
+ )} + +
+ +
+
+ ) +} diff --git a/apps/desktop/src/renderer/src/components/browser/BrowserPanel.tsx b/apps/desktop/src/renderer/src/components/browser/BrowserPanel.tsx index 420c8ab..d18dddd 100644 --- a/apps/desktop/src/renderer/src/components/browser/BrowserPanel.tsx +++ b/apps/desktop/src/renderer/src/components/browser/BrowserPanel.tsx @@ -11,8 +11,10 @@ import { LuRotateCw, LuX } from 'react-icons/lu' -import type { BrowserImportResult, BrowserProxySettings, BrowserSource, BrowserUiState } from '@shared/types' +import type { BrowserUiState } from '@shared/types' import { BrowserSlot } from './BrowserSlot' +import { BrowserImportForm } from './BrowserImportForm' +import { BrowserProxyForm } from './BrowserProxyForm' /** * 内置受管浏览器的「chrome」部分(页签栏/地址栏/工具条/导入浮层)。 @@ -36,8 +38,6 @@ interface BrowserPanelProps { /** 关闭全部页面(销毁各标签的 WebContentsView;分区会话/登录态保留) */ onCloseAllTabs: () => void onAcknowledgeRisk: () => void - onDetectSources: () => Promise - onImportProfile: (sourceId: string, profileDir: string) => Promise } export function BrowserPanel(props: BrowserPanelProps): JSX.Element { @@ -423,60 +423,18 @@ export function BrowserPanel(props: BrowserPanelProps): JSX.Element { {/* 配置导入浮层(覆盖面板,出现时原生视图已隐藏) */} {importOpen && ( - setImportOpen(false)} - onDetectSources={props.onDetectSources} - onImportProfile={props.onImportProfile} - /> + setImportOpen(false)} /> )} {/* 代理设置浮层(覆盖面板,出现时原生视图已隐藏) */} - {proxyOpen && setProxyOpen(false)} />} + {proxyOpen && setProxyOpen(false)} />}
) } -// ---- 配置导入浮层 ---- - -function ImportOverlay({ - state, - onClose, - onDetectSources, - onImportProfile -}: { - state: BrowserUiState | null - onClose: () => void - onDetectSources: () => Promise - onImportProfile: (sourceId: string, profileDir: string) => Promise -}): JSX.Element { - const [sources, setSources] = useState(null) - const [selected, setSelected] = useState<{ sourceId: string; profileDir: string } | null>(null) - const [busy, setBusy] = useState(false) - const [result, setResult] = useState(null) - const [error, setError] = useState(null) - - useEffect(() => { - onDetectSources() - .then((list) => { - setSources(list) - const first = list[0] - if (first?.profiles[0]) setSelected({ sourceId: first.id, profileDir: first.profiles[0].dir }) - }) - .catch((err) => setError(err instanceof Error ? err.message : String(err))) - }, [onDetectSources]) - - const runImport = (): void => { - if (!selected) return - setBusy(true) - setError(null) - setResult(null) - onImportProfile(selected.sourceId, selected.profileDir) - .then(setResult) - .catch((err) => setError(err instanceof Error ? err.message : String(err))) - .finally(() => setBusy(false)) - } +// ---- 浮层薄壳:标题栏 + 关闭;表单本体在共用组件(设置页二级页同样复用) ---- +function ImportOverlay({ onClose }: { onClose: () => void }): JSX.Element { return (
@@ -490,134 +448,14 @@ function ImportOverlay({
- -
-

- 选择本机浏览器的一个配置(Profile),把其中各网站的登录态(Cookie / Local Storage)导入到内置受管浏览器。 - 导入前请完全退出该浏览器,并关闭本面板的所有标签页。 -

- - {state?.imported && ( -
- 当前导入来源:{state.imported.browser} · {state.imported.profile} - {state.imported.cookies ? '(含 Cookie)' : ''} -
- )} - - {sources === null && !error &&

正在检测本机浏览器…

} - {sources?.length === 0 &&

未检测到可导入的本机浏览器(支持 Chrome / Edge / Brave / Chromium)

} - -
- {sources?.map((source) => ( -
-
- - {source.name} - ({source.profiles.length} 个配置) -
-
- {source.profiles.map((profile) => { - const active = selected?.sourceId === source.id && selected.profileDir === profile.dir - return ( - - ) - })} -
-
- ))} -
- - {error && ( -
{error}
- )} - {result && ( -
- {result.message} -
- )} -
- -
- - +
+
) } -// ---- 代理设置浮层 ---- - -const PROXY_MODES: Array<{ id: BrowserProxySettings['mode']; label: string }> = [ - { id: 'system', label: '跟随系统' }, - { id: 'direct', label: '直连' }, - { id: 'fixed', label: '手动代理' } -] - -function ProxyOverlay({ state, onClose }: { state: BrowserUiState | null; onClose: () => void }): JSX.Element { - const current = state?.proxy ?? { mode: 'system' as const } - const [mode, setMode] = useState(current.mode) - const [rules, setRules] = useState(current.mode === 'fixed' ? (current.rules ?? '') : '') - const [bypass, setBypass] = useState(current.bypass ?? '') - const [busy, setBusy] = useState(false) - const [error, setError] = useState(null) - const [saved, setSaved] = useState(false) - - // 主进程广播回来(保存成功/其他入口改动)→ 表单跟随当前生效值 - useEffect(() => { - setMode(current.mode) - setRules(current.mode === 'fixed' ? (current.rules ?? '') : '') - setBypass(current.bypass ?? '') - }, [current.mode, current.rules, current.bypass]) - - const save = (): void => { - setBusy(true) - setError(null) - setSaved(false) - window.api - .browserSetProxy({ mode, rules: rules.trim(), bypass: bypass.trim() }) - .then((p) => { - setMode(p.mode) - setRules(p.rules ?? '') - setBypass(p.bypass ?? '') - setSaved(true) - }) - .catch((err) => setError(err instanceof Error ? err.message : String(err))) - .finally(() => setBusy(false)) - } - - const inputCls = - 'w-full rounded border border-line bg-inset px-2.5 py-1.5 text-[12px] text-ink outline-none placeholder:text-ink-3 focus:border-accent focus:bg-surface' - +function ProxyOverlay({ onClose }: { onClose: () => void }): JSX.Element { return (
@@ -631,81 +469,8 @@ function ProxyOverlay({ state, onClose }: { state: BrowserUiState | null; onClos
- -
-

- 代理只作用于内置受管浏览器(不影响应用其他部分与系统设置),保存后立即生效。 -

- -
- {PROXY_MODES.map((m) => ( - - ))} -
- - {mode === 'fixed' && ( -
- - -

- 规则语法与 Chromium 一致:host:port 应用于全部协议;也可按协议指定(http=…;https=…;socks=…)。 -

-
- )} - - {error && ( -
{error}
- )} - {saved && !error && ( -
- 已保存并立即生效(在途连接已重置)。 -
- )} -
- -
- - +
+
) diff --git a/apps/desktop/src/renderer/src/components/browser/BrowserProxyForm.tsx b/apps/desktop/src/renderer/src/components/browser/BrowserProxyForm.tsx new file mode 100644 index 0000000..63fa989 --- /dev/null +++ b/apps/desktop/src/renderer/src/components/browser/BrowserProxyForm.tsx @@ -0,0 +1,139 @@ +/** + * 受管浏览器代理设置表单(面板浮层与设置页二级页共用)。 + * 只作用于 persist:aso-browser 分区;保存即生效(主进程 setProxy + 断开在途连接)。 + * 自含数据源:自行订阅 browser 事件流获取当前生效设置。 + */ +import { useEffect, useState, type JSX } from 'react' +import type { BrowserProxySettings, BrowserUiState } from '@shared/types' + +const PROXY_MODES: Array<{ id: BrowserProxySettings['mode']; label: string }> = [ + { id: 'system', label: '跟随系统' }, + { id: 'direct', label: '直连' }, + { id: 'fixed', label: '手动代理' } +] + +export function BrowserProxyForm(): JSX.Element { + const [state, setState] = useState(null) + const [mode, setMode] = useState('system') + const [rules, setRules] = useState('') + const [bypass, setBypass] = useState('') + const [busy, setBusy] = useState(false) + const [error, setError] = useState(null) + const [saved, setSaved] = useState(false) + + // 当前生效设置:首查 + 事件跟随(主进程广播,保存成功后表单同步) + useEffect(() => { + let alive = true + window.api + .browserGetState() + .then((s) => { + if (alive) setState(s) + }) + .catch(() => undefined) + const off = window.api.onBrowserEvent((event) => { + if (event.type === 'state' && alive) setState(event.state) + }) + return () => { + alive = false + off() + } + }, []) + + const current = state?.proxy + useEffect(() => { + if (!current) return + setMode(current.mode) + setRules(current.mode === 'fixed' ? (current.rules ?? '') : '') + setBypass(current.bypass ?? '') + }, [current?.mode, current?.rules, current?.bypass]) + + const save = (): void => { + setBusy(true) + setError(null) + setSaved(false) + window.api + .browserSetProxy({ mode, rules: rules.trim(), bypass: bypass.trim() }) + .then((p) => { + setMode(p.mode) + setRules(p.rules ?? '') + setBypass(p.bypass ?? '') + setSaved(true) + }) + .catch((err) => setError(err instanceof Error ? err.message : String(err))) + .finally(() => setBusy(false)) + } + + const inputCls = + 'w-full rounded border border-line bg-inset px-2.5 py-1.5 text-[12px] text-ink outline-none placeholder:text-ink-3 focus:border-accent focus:bg-surface' + + return ( +
+

+ 代理只作用于内置受管浏览器(不影响应用其他部分与系统设置),保存后立即生效。 +

+ +
+ {PROXY_MODES.map((m) => ( + + ))} +
+ + {mode === 'fixed' && ( +
+ + +

+ 规则语法与 Chromium 一致:host:port 应用于全部协议;也可按协议指定(http=…;https=…;socks=…)。 +

+
+ )} + + {error && ( +
{error}
+ )} + {saved && !error && ( +
+ 已保存并立即生效(在途连接已重置)。 +
+ )} + +
+ +
+
+ ) +}