Merge pull request #83 from router-for-me/dev

Dev
This commit is contained in:
lzt404 authored and GitHub committed 2026-08-03 14:39:48 +08:00
commit ea811ee0cb
8 files changed
+257 -54

No files matched your search

+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "cpa-gui",
"private": true,
"version": "0.2.10",
"version": "0.2.11",
"type": "module",
"packageManager": "bun@1.3.14",
"scripts": {
+1 -1
View File
@@ -582,7 +582,7 @@ checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636"
[[package]]
name = "cpa-gui"
version = "0.2.10"
version = "0.2.11"
dependencies = [
"base64 0.22.1",
"chrono",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "cpa-gui"
version = "0.2.10"
version = "0.2.11"
description = "CPA desktop management shell"
authors = ["lzt404"]
edition = "2021"
+127 -5
View File
@@ -1765,6 +1765,47 @@ async fn install_pi_provider(
Ok(result)
}
#[tauri::command]
async fn sync_pi_provider(
app: tauri::AppHandle,
gui_config_state: tauri::State<'_, GuiConfigState>,
cache: tauri::State<'_, AgentConfigStatusCache>,
) -> Result<AgentConfigActionResult, String> {
let home = app
.path()
.home_dir()
.map_err(|error| format!("鏃犳硶鑾峰彇鐢ㄦ埛鐩綍: {error}"))?;
let config = gui_config_state.snapshot()?;
let port = config.port;
let api_key = effective_agent_api_key(&config).to_string();
let result =
tauri::async_runtime::spawn_blocking(move || sync_pi_provider_inner(&home, port, &api_key))
.await
.map_err(|error| format!("鍚屾 Pi 閰嶇疆浠诲姟澶辫触: {error}"))??;
cache.clear()?;
Ok(result)
}
#[tauri::command]
async fn uninstall_pi_provider(
app: tauri::AppHandle,
cache: tauri::State<'_, AgentConfigStatusCache>,
) -> Result<AgentConfigActionResult, String> {
let home = app
.path()
.home_dir()
.map_err(|error| format!("鏃犳硶鑾峰彇鐢ㄦ埛鐩綍: {error}"))?;
let executable =
find_pi_executable(&home).ok_or_else(|| "鏈娴嬪埌 Pi CLI锛岃鍏堝畨瑁?Pi".to_string())?;
let result = tauri::async_runtime::spawn_blocking(move || {
uninstall_pi_provider_inner(&home, &executable)
})
.await
.map_err(|error| format!("鍗歌浇 Pi 鎻掍欢浠诲姟澶辫触: {error}"))??;
cache.clear()?;
Ok(result)
}
fn validate_claude_code_working_directory(value: &str) -> Result<PathBuf, String> {
let value = value.trim();
if value.is_empty() {
@@ -3257,7 +3298,28 @@ fn install_pi_provider_inner(
changed_files.push(path_to_string(&settings_path));
}
let mut result = sync_pi_provider_inner(home, port, api_key)?;
changed_files.extend(result.changed_files);
result.changed_files = changed_files;
Ok(result)
}
fn sync_pi_provider_inner(
home: &Path,
port: u16,
api_key: &str,
) -> Result<AgentConfigActionResult, String> {
if port == 0 {
return Err("Core port is invalid".to_string());
}
if api_key.trim().is_empty() {
return Err("EasyCLIProxyAPI has no usable API key".to_string());
}
let config_path = pi_provider_config_path(home);
let mut changed_files = Vec::new();
if !pi_provider_package_installed(home)? {
return Err("Pi CLIProxyAPI provider is not installed".to_string());
}
let existing = if config_path.is_file() {
Some(fs::read_to_string(&config_path).map_err(|error| {
format!(
@@ -3284,6 +3346,29 @@ fn install_pi_provider_inner(
))
}
fn uninstall_pi_provider_inner(
home: &Path,
executable: &Path,
) -> Result<AgentConfigActionResult, String> {
if !pi_provider_package_installed(home)? {
return Ok(action_result(
"not-installed",
false,
None,
Vec::new(),
Vec::new(),
));
}
remove_pi_package(executable, home)?;
Ok(action_result(
"removed",
false,
None,
vec![path_to_string(&pi_provider_settings_path(home))],
Vec::new(),
))
}
fn install_pi_package(executable: &Path, home: &Path) -> Result<(), String> {
#[cfg(target_os = "windows")]
let mut command = windows_command_for_executable(executable, false);
@@ -3315,6 +3400,37 @@ fn install_pi_package(executable: &Path, home: &Path) -> Result<(), String> {
))
}
fn remove_pi_package(executable: &Path, home: &Path) -> Result<(), String> {
#[cfg(target_os = "windows")]
let mut command = windows_command_for_executable(executable, false);
#[cfg(not(target_os = "windows"))]
let mut command = Command::new(executable);
command
.arg("remove")
.arg(PI_CLIPROXYAPI_PACKAGE)
.current_dir(home)
.stdin(Stdio::null());
configure_background_command(&mut command);
let output = command
.output()
.map_err(|error| format!("鎵ц Pi 鎻掍欢鍗歌浇澶辫触: {error}"))?;
if output.status.success() {
return Ok(());
}
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
let detail = if !stderr.is_empty() { stderr } else { stdout };
Err(format!(
"Pi CLIProxyAPI provider 鎻掍欢鍗歌浇澶辫触{}",
if detail.is_empty() {
String::new()
} else {
format!(": {detail}")
}
))
}
fn codex_configuration_directory(home: &Path) -> PathBuf {
env::var_os("CODEX_HOME")
.map(PathBuf::from)
@@ -17670,11 +17786,15 @@ fn ensure_configuration_watch_directories(
fn start_configuration_file_watcher(app: tauri::AppHandle) -> Result<(), String> {
let tracked_paths = tracked_configuration_paths(&app)?;
let (sender, receiver) = std::sync::mpsc::channel();
let mut watcher: RecommendedWatcher =
notify::recommended_watcher(move |event: Result<notify::Event, notify::Error>| {
let _ = sender.send(event);
})
.map_err(|error| format!("创建配置文件监控器失败: {error}"))?;
let mut watcher: RecommendedWatcher = notify::recommended_watcher(
move |event: Result<notify::Event, notify::Error>| match event {
Ok(event) if event.kind.is_access() => {}
event => {
let _ = sender.send(event);
}
},
)
.map_err(|error| format!("创建配置文件监控器失败: {error}"))?;
let mut watched_directories = Vec::new();
ensure_configuration_watch_directories(&mut watcher, &tracked_paths, &mut watched_directories)?;
@@ -17929,6 +18049,8 @@ fn main() {
refresh_agent_config_statuses,
get_agent_models,
install_pi_provider,
sync_pi_provider,
uninstall_pi_provider,
check_codex_oauth_login,
update_codex_model_catalog,
get_thinking_aliases,
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "EasyCLIProxyAPI",
"version": "0.2.10",
"version": "0.2.11",
"identifier": "com.cpa.gui",
"build": {
"beforeDevCommand": "bun run dev",
+18 -8
View File
@@ -441,6 +441,7 @@ export const zhCN = {
'agents.list.installedVersion': '已安装 · {version}',
'agents.list.installed': '已安装 · 保持原配置',
'agents.list.pluginNotInstalled': 'Pi provider 插件未安装',
'agents.list.piInstalled': 'Pi 插件已安装',
'agents.list.configured': '已配置 · EasyCLIProxyAPI',
'agents.model.loading': '正在读取模型',
'agents.model.loadFailed': '获取模型失败',
@@ -598,10 +599,14 @@ export const zhCN = {
'agents.launch.closeChatgpt': '关闭 ChatGPT App',
'agents.launch.closingChatgpt': '关闭中',
'agents.pi.installTitle': 'Pi CLIProxyAPI provider',
'agents.pi.installDescription': '安装插件,并自动配置 Pi 的 baseUrl 和 API key',
'agents.pi.install': '安装并配置 Pi 插件',
'agents.pi.update': '重新同步 Pi 配置',
'agents.pi.installing': '正在安装并配置',
'agents.pi.installDescription': '安装插件后自动配置',
'agents.pi.install': '安装 Pi 插件',
'agents.pi.update': '同步 Pi 配置',
'agents.pi.installing': '正在安装插件',
'agents.pi.sync': '同步 Pi 配置',
'agents.pi.syncing': '正在同步配置',
'agents.pi.uninstall': '卸载 Pi 插件',
'agents.pi.uninstalling': '正在卸载插件',
'agents.default.title': '重置为默认配置',
'agents.default.description': '这会使用当前选中模型重写 {name} 的全部相关配置文件,并删除其中其他用户内容。该操作没有备份且无法恢复。',
'agents.default.confirm': '确认写入默认配置',
@@ -1377,6 +1382,7 @@ export const en: Record<MessageKey, string> = {
'agents.list.installedVersion': 'Installed · {version}',
'agents.list.installed': 'Installed · original configuration',
'agents.list.pluginNotInstalled': 'Pi provider not installed',
'agents.list.piInstalled': 'Pi plugin installed',
'agents.list.configured': 'Configured · EasyCLIProxyAPI',
'agents.model.loading': 'Loading models',
'agents.model.loadFailed': 'Failed to load models',
@@ -1534,10 +1540,14 @@ export const en: Record<MessageKey, string> = {
'agents.launch.closeChatgpt': 'Close ChatGPT App',
'agents.launch.closingChatgpt': 'Closing',
'agents.pi.installTitle': 'Pi CLIProxyAPI provider',
'agents.pi.installDescription': "Install the plugin and automatically configure Pi's baseUrl and API key.",
'agents.pi.install': 'Install and Configure Pi',
'agents.pi.update': 'Resync Pi Configuration',
'agents.pi.installing': 'Installing and configuring',
'agents.pi.installDescription': 'Automatically configure after installing the plugin.',
'agents.pi.install': 'Install Pi Plugin',
'agents.pi.update': 'Sync Pi Configuration',
'agents.pi.installing': 'Installing plugin',
'agents.pi.sync': 'Sync Pi Configuration',
'agents.pi.syncing': 'Syncing configuration',
'agents.pi.uninstall': 'Uninstall Pi Plugin',
'agents.pi.uninstalling': 'Uninstalling plugin',
'agents.default.title': 'Reset to Default Configuration',
'agents.default.description': 'This rewrites every related {name} configuration file using the selected model and deletes all other user content in those files. No backup is created and the operation cannot be undone.',
'agents.default.confirm': 'Write Default Configuration',
+53 -17
View File
@@ -277,11 +277,10 @@ const listStatusText = (status: AgentConfigStatus | undefined) => {
if (!status) return translate(locale, 'agents.list.detecting');
if (!status.supportedPlatform) return translate(locale, 'agents.list.unsupported');
if (!status.installed) return translate(locale, 'agents.list.notInstalled');
if (status.id === 'pi' && !status.pluginInstalled) {
return translate(locale, 'agents.list.pluginNotInstalled');
}
if (status.id === 'pi' && status.configured) {
return translate(locale, 'agents.list.configured');
if (status.id === 'pi') {
return status.pluginInstalled
? translate(locale, 'agents.list.piInstalled')
: translate(locale, 'agents.list.pluginNotInstalled');
}
if (status.modificationState === 'invalid') return translate(locale, 'agents.status.invalid');
if (status.modificationState === 'applied') return translate(locale, 'agents.list.modified', { model: status.appliedModel ?? '—' });
@@ -553,7 +552,7 @@ export function AgentsPage() {
const [loading, setLoading] = useState(true);
const [modelLoading, setModelLoading] = useState(false);
const [busyAction, setBusyAction] = useState<
'apply' | 'close-config' | 'default' | 'clear' | 'install-pi' | 'launch' | 'launch-cli' | 'launch-app' | 'close-app' | 'oauth-check' | 'directory' | null
'apply' | 'close-config' | 'default' | 'clear' | 'install-pi' | 'sync-pi' | 'uninstall-pi' | 'launch' | 'launch-cli' | 'launch-app' | 'close-app' | 'oauth-check' | 'directory' | null
>(null);
const busy = busyAction !== null;
const [detectionError, setDetectionError] = useState('');
@@ -937,6 +936,32 @@ export function AgentsPage() {
}
};
const syncPiProvider = async () => {
setBusyAction('sync-pi');
setConfigurationError('');
try {
await invoke<AgentConfigActionResult>('sync_pi_provider');
await reloadStatusesAfterAction();
} catch (requestError) {
setConfigurationError(String(requestError));
} finally {
setBusyAction(null);
}
};
const uninstallPiProvider = async () => {
setBusyAction('uninstall-pi');
setConfigurationError('');
try {
await invoke<AgentConfigActionResult>('uninstall_pi_provider');
await reloadStatusesAfterAction();
} catch (requestError) {
setConfigurationError(String(requestError));
} finally {
setBusyAction(null);
}
};
const closeConfigurationChanges = async () => {
setConfigurationError('');
setBusyAction('close-config');
@@ -1197,7 +1222,9 @@ export function AgentsPage() {
<span className="agent-client-icon"><AgentMark definition={agent} /></span>
<span><strong>{agent.name}</strong><small>{listStatusText(status)}</small></span>
<i
className={status?.modificationEnabled ? 'configured' : status?.installed ? 'installed' : ''}
className={status?.id === 'pi'
? status?.installed ? 'installed' : ''
: status?.modificationEnabled ? 'configured' : status?.installed ? 'installed' : ''}
aria-hidden="true"
/>
</button>
@@ -1243,7 +1270,7 @@ export function AgentsPage() {
</div>
</div>
{activeStatus?.error || activeStatus?.warnings.length ? (
{activeStatus?.error || (!isPiClient && activeStatus?.warnings.length) ? (
<div className="agent-status-messages" aria-live="polite">
{activeStatus.error ? (
<span className="agent-inline-message error" role="alert">{activeStatus.error}</span>
@@ -1330,7 +1357,7 @@ export function AgentsPage() {
) : null}
{isPiClient ? (
<section className={`agent-core-setting-section agent-modification-actions ${activeStatus?.configured ? 'enabled' : ''}`}>
<section className="agent-core-setting-section agent-modification-actions">
<div className="agent-section-heading">
<div>
<strong>{t('agents.pi.installTitle')}</strong>
@@ -1341,16 +1368,25 @@ export function AgentsPage() {
<div className="agent-modification-buttons">
<button
type="button"
className="primary-button"
onClick={() => void installPiProvider()}
className={activeStatus?.pluginInstalled ? 'danger-button' : 'primary-button'}
onClick={() => void (activeStatus?.pluginInstalled ? uninstallPiProvider() : installPiProvider())}
disabled={busy || !activeStatus?.installed || !activeStatus.supportedPlatform}
>
{busyAction === 'install-pi' ? <LoaderCircle size={16} className="spin" /> : null}
{busyAction === 'install-pi'
? t('agents.pi.installing')
: activeStatus?.configured
? t('agents.pi.update')
: t('agents.pi.install')}
{busyAction === 'install-pi' || busyAction === 'uninstall-pi'
? <LoaderCircle size={16} className="spin" />
: null}
{activeStatus?.pluginInstalled
? busyAction === 'uninstall-pi' ? t('agents.pi.uninstalling') : t('agents.pi.uninstall')
: busyAction === 'install-pi' ? t('agents.pi.installing') : t('agents.pi.install')}
</button>
<button
type="button"
className="secondary-button"
onClick={() => void syncPiProvider()}
disabled={busy || !activeStatus?.installed || !activeStatus.supportedPlatform || !activeStatus.pluginInstalled}
>
{busyAction === 'sync-pi' ? <LoaderCircle size={16} className="spin" /> : <RefreshCw size={16} />}
{busyAction === 'sync-pi' ? t('agents.pi.syncing') : t('agents.pi.sync')}
</button>
</div>
{configurationError ? (
+55 -20
View File
@@ -51,6 +51,18 @@ type ConfigAction =
| null;
type NoticeTone = 'success' | 'error';
type ConfigSubpage = 'general' | 'network' | 'aliases';
type NetworkDraftField = 'port' | 'allowLan' | 'proxyUrl' | 'sessionAffinity' | 'sessionTtl';
type DraftRefreshMode = 'replace' | 'preserve';
type NetworkDraftDirty = Record<NetworkDraftField, boolean>;
const cleanNetworkDraft = (): NetworkDraftDirty => ({
port: false,
allowLan: false,
proxyUrl: false,
sessionAffinity: false,
sessionTtl: false,
});
const ROUTING_OPTIONS = [
{ value: 'round-robin', labelKey: 'config.routing.roundRobin' },
@@ -85,6 +97,7 @@ export function ConfigPanelPage() {
const [sessionTtlDraft, setSessionTtlDraft] = useState('');
const [portError, setPortError] = useState('');
const [savedStatusVisible, setSavedStatusVisible] = useState(false);
const networkDraftDirtyRef = useRef<NetworkDraftDirty>(cleanNetworkDraft());
const noticeTimerRef = useRef<number | null>(null);
const copyTimerRef = useRef<number | null>(null);
@@ -93,7 +106,7 @@ export function ConfigPanelPage() {
let stop: (() => void) | null = null;
void loadSettings();
void listen('config-files-changed', () => {
if (!disposed) void loadSettings();
if (!disposed) void loadSettings('preserve');
}).then((unlisten) => {
if (disposed) unlisten();
else stop = unlisten;
@@ -121,24 +134,41 @@ export function ConfigPanelPage() {
}, 3200);
};
const applySettings = (result: CoreConfigSettings, syncNetworkDrafts = true) => {
const applySettings = (result: CoreConfigSettings, mode: DraftRefreshMode = 'replace') => {
setSettings(result);
if (syncNetworkDrafts) {
setPortDraft(String(result.port));
setAllowLanDraft(result.allowLan);
setProxyUrlDraft(result.proxyUrl);
setSessionAffinityDraft(result.routingSessionAffinity);
setSessionTtlDraft(result.routingSessionAffinityTtl);
setPortError('');
if (mode === 'preserve') {
const dirty = networkDraftDirtyRef.current;
if (!dirty.port) setPortDraft(String(result.port));
if (!dirty.allowLan) setAllowLanDraft(result.allowLan);
if (!dirty.proxyUrl) setProxyUrlDraft(result.proxyUrl);
if (!dirty.sessionAffinity) setSessionAffinityDraft(result.routingSessionAffinity);
if (!dirty.sessionTtl) setSessionTtlDraft(result.routingSessionAffinityTtl);
return;
}
networkDraftDirtyRef.current = cleanNetworkDraft();
setPortDraft(String(result.port));
setAllowLanDraft(result.allowLan);
setProxyUrlDraft(result.proxyUrl);
setSessionAffinityDraft(result.routingSessionAffinity);
setSessionTtlDraft(result.routingSessionAffinityTtl);
setPortError('');
};
async function loadSettings(syncNetworkDrafts = true) {
const markDraftDirty = (field: NetworkDraftField) => {
networkDraftDirtyRef.current[field] = true;
setSavedStatusVisible(false);
};
const clearDraftDirty = (field: NetworkDraftField) => {
networkDraftDirtyRef.current[field] = false;
};
async function loadSettings(mode: DraftRefreshMode = 'replace') {
setLoading(true);
setLoadError('');
try {
const result = await invoke<CoreConfigSettings>('get_core_config_settings');
applySettings(result, syncNetworkDrafts);
applySettings(result, mode);
} catch (error) {
setSettings(null);
setLoadError(String(error));
@@ -156,14 +186,14 @@ export function ConfigPanelPage() {
setBusyAction(action);
try {
const result = await invoke<CoreConfigSettings>(command, args);
applySettings(result, false);
setSettings(result);
setLoadError('');
showNotice(successMessage, 'success');
return true;
} catch (error) {
if (settings) applySettings(settings, false);
if (settings) setSettings(settings);
showNotice(t('config.error.saveFailed', { error: String(error) }), 'error');
void loadSettings(false);
void loadSettings('preserve');
return false;
} finally {
setBusyAction(null);
@@ -325,8 +355,10 @@ export function ConfigPanelPage() {
const openWebUi = async () => {
try {
const latestSettings = await invoke<CoreConfigSettings>('get_core_config_settings');
applySettings(latestSettings, 'preserve');
await invoke('open_external_url', {
url: webUiManagementUrl(settings?.port ?? 8317),
url: webUiManagementUrl(latestSettings.port),
});
} catch (error) {
showNotice(t('config.webuiKey.error.openFailed', { error: String(error) }), 'error');
@@ -751,12 +783,13 @@ export function ConfigPanelPage() {
aria-invalid={Boolean(portError)}
title={portError || t('config.network.portHint')}
onChange={(event) => {
setSavedStatusVisible(false);
markDraftDirty('port');
setPortDraft(event.currentTarget.value.replace(/\D/g, '').slice(0, 5));
setPortError('');
}}
onKeyDown={(event) => {
if (event.key === 'Escape' && settings) {
clearDraftDirty('port');
setPortDraft(String(settings.port));
setPortError('');
event.currentTarget.blur();
@@ -778,7 +811,7 @@ export function ConfigPanelPage() {
checked={allowLanDraft}
disabled={controlsDisabled}
onChange={(event) => {
setSavedStatusVisible(false);
markDraftDirty('allowLan');
setAllowLanDraft(event.currentTarget.checked);
}}
/>
@@ -798,11 +831,12 @@ export function ConfigPanelPage() {
disabled={controlsDisabled}
placeholder={t('config.network.proxyPlaceholder')}
onChange={(event) => {
setSavedStatusVisible(false);
markDraftDirty('proxyUrl');
setProxyUrlDraft(event.currentTarget.value);
}}
onKeyDown={(event) => {
if (event.key === 'Escape' && settings) {
clearDraftDirty('proxyUrl');
setProxyUrlDraft(settings.proxyUrl);
event.currentTarget.blur();
}
@@ -831,7 +865,7 @@ export function ConfigPanelPage() {
checked={sessionAffinityDraft}
disabled={controlsDisabled}
onChange={(event) => {
setSavedStatusVisible(false);
markDraftDirty('sessionAffinity');
setSessionAffinityDraft(event.currentTarget.checked);
}}
/>
@@ -851,11 +885,12 @@ export function ConfigPanelPage() {
disabled={controlsDisabled}
placeholder="1h"
onChange={(event) => {
setSavedStatusVisible(false);
markDraftDirty('sessionTtl');
setSessionTtlDraft(event.currentTarget.value);
}}
onKeyDown={(event) => {
if (event.key === 'Escape' && settings) {
clearDraftDirty('sessionTtl');
setSessionTtlDraft(settings.routingSessionAffinityTtl);
event.currentTarget.blur();
}