feat(agents): add Pi CLIProxyAPI provider integration

- Add Pi detection and launch support
- Install the CLIProxyAPI provider extension through Pi
- Sync EasyCLIProxyAPI base URL and API key to Pi configuration
- Add Pi agent UI, translations, documentation, and tests
This commit is contained in:
lzt404 committed 2026-08-03 00:06:16 +08:00
1 parent 176cf87225
commit 035c77354b
6 files changed
+441 -7

No files matched your search

+16
View File
@@ -433,12 +433,15 @@ export const zhCN = {
'agents.description.opencode': '使用 CPA OpenAI Compatible provider',
'agents.description.openclaw': '使用 CPA 模型供应商和默认模型',
'agents.description.hermes': '使用 CPA custom provider',
'agents.description.pi': '使用 Pi CLI 和 CLIProxyAPI provider 动态加载模型',
'agents.list.detecting': '正在检测',
'agents.list.unsupported': '当前平台不支持',
'agents.list.notInstalled': '未检测到安装',
'agents.list.modified': '已修改 · {model}',
'agents.list.installedVersion': '已安装 · {version}',
'agents.list.installed': '已安装 · 保持原配置',
'agents.list.pluginNotInstalled': 'Pi provider 插件未安装',
'agents.list.configured': '已配置 · EasyCLIProxyAPI',
'agents.model.loading': '正在读取模型',
'agents.model.loadFailed': '获取模型失败',
'agents.model.select': '选择模型',
@@ -594,6 +597,11 @@ export const zhCN = {
'agents.launch.unavailable': '无法启动',
'agents.launch.closeChatgpt': '关闭 ChatGPT App',
'agents.launch.closingChatgpt': '关闭中',
'agents.pi.installTitle': 'Pi CLIProxyAPI provider',
'agents.pi.installDescription': '安装官方插件,并将当前 EasyCLIProxyAPI 的 baseUrl 和 API key 写入 Pi 配置。',
'agents.pi.install': '安装并配置 Pi 插件',
'agents.pi.update': '重新同步 Pi 配置',
'agents.pi.installing': '正在安装并配置',
'agents.default.title': '重置为默认配置',
'agents.default.description': '这会使用当前选中模型重写 {name} 的全部相关配置文件,并删除其中其他用户内容。该操作没有备份且无法恢复。',
'agents.default.confirm': '确认写入默认配置',
@@ -1361,12 +1369,15 @@ export const en: Record<MessageKey, string> = {
'agents.description.opencode': 'Uses the CPA OpenAI-compatible provider',
'agents.description.openclaw': 'Uses the CPA model provider and default model',
'agents.description.hermes': 'Uses the CPA custom provider',
'agents.description.pi': 'Loads models dynamically in Pi through the CLIProxyAPI provider',
'agents.list.detecting': 'Detecting',
'agents.list.unsupported': 'Unsupported on this platform',
'agents.list.notInstalled': 'Installation not detected',
'agents.list.modified': 'Modified · {model}',
'agents.list.installedVersion': 'Installed · {version}',
'agents.list.installed': 'Installed · original configuration',
'agents.list.pluginNotInstalled': 'Pi provider not installed',
'agents.list.configured': 'Configured · EasyCLIProxyAPI',
'agents.model.loading': 'Loading models',
'agents.model.loadFailed': 'Failed to load models',
'agents.model.select': 'Select a model',
@@ -1522,6 +1533,11 @@ export const en: Record<MessageKey, string> = {
'agents.launch.unavailable': 'Cannot Start',
'agents.launch.closeChatgpt': 'Close ChatGPT App',
'agents.launch.closingChatgpt': 'Closing',
'agents.pi.installTitle': 'Pi CLIProxyAPI provider',
'agents.pi.installDescription': 'Install the official plugin and write the current EasyCLIProxyAPI base URL and API key into Pi configuration.',
'agents.pi.install': 'Install and Configure Pi',
'agents.pi.update': 'Resync Pi Configuration',
'agents.pi.installing': 'Installing and configuring',
'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',
+74 -6
View File
@@ -51,7 +51,8 @@ type AgentClientId =
| 'codex'
| 'opencode'
| 'openclaw'
| 'hermes';
| 'hermes'
| 'pi';
type AgentModificationState = 'unconfigured' | 'applied' | 'invalid';
@@ -60,6 +61,7 @@ type AgentConfigStatus = {
name: string;
supportedPlatform: boolean;
installed: boolean;
pluginInstalled: boolean;
executablePath: string | null;
launchTargets: AgentLaunchTarget[];
version: string | null;
@@ -139,7 +141,7 @@ type AgentDefinition = {
name: string;
icon?: string;
Icon?: ComponentType<{ size?: number; 'aria-hidden'?: boolean }>;
descriptionKey: 'agents.description.claudeCode' | 'agents.description.claudeDesktop' | 'agents.description.codex' | 'agents.description.opencode' | 'agents.description.openclaw' | 'agents.description.hermes';
descriptionKey: 'agents.description.claudeCode' | 'agents.description.claudeDesktop' | 'agents.description.codex' | 'agents.description.opencode' | 'agents.description.openclaw' | 'agents.description.hermes' | 'agents.description.pi';
};
type AgentSubpageId = 'core' | 'sessions';
@@ -187,6 +189,12 @@ const agentDefinitions: AgentDefinition[] = [
icon: hermesIcon,
descriptionKey: 'agents.description.hermes',
},
{
id: 'pi',
name: 'Pi',
Icon: Bot,
descriptionKey: 'agents.description.pi',
},
];
const agentSubpages: AgentSubpageDefinition[] = [
@@ -268,6 +276,12 @@ 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.modificationState === 'invalid') return translate(locale, 'agents.status.invalid');
if (status.modificationState === 'applied') return translate(locale, 'agents.list.modified', { model: status.appliedModel ?? '—' });
return status.version
@@ -538,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' | 'launch' | 'launch-cli' | 'launch-app' | 'close-app' | 'oauth-check' | 'directory' | null
'apply' | 'close-config' | 'default' | 'clear' | 'install-pi' | 'launch' | 'launch-cli' | 'launch-app' | 'close-app' | 'oauth-check' | 'directory' | null
>(null);
const busy = busyAction !== null;
const [detectionError, setDetectionError] = useState('');
@@ -614,6 +628,12 @@ export function AgentsPage() {
}, [loadStatuses]);
useEffect(() => {
if (selected === 'pi') {
setModels([]);
setModelLoading(false);
setModelError('');
return;
}
void loadModels(selected);
}, [loadModels, selected]);
@@ -668,6 +688,7 @@ export function AgentsPage() {
const savedSelectedModel = modelByClient[selected] ?? '';
const selectedModelOption = findAgentModel(models, savedSelectedModel);
const selectedModel = selectedModelOption?.name ?? '';
const isPiClient = selected === 'pi';
const isClaudeModelMappingClient = selected === 'claude-code' || selected === 'claude-desktop';
const claudeMappingModels = useMemo(
() => filterAgentModelsByAlias(models, claudeCustomMapping),
@@ -738,9 +759,10 @@ export function AgentsPage() {
activeStatus?.supportedPlatform
&& activeStatus.installed
&& !modelLoading
&& (isClaudeModelMappingClient
&& (isPiClient
|| (isClaudeModelMappingClient
? claudeMappingsReady
: selectedModelOption),
: selectedModelOption)),
);
const launchEnabled = Boolean(
activeStatus?.supportedPlatform
@@ -901,6 +923,19 @@ export function AgentsPage() {
}
};
const installPiProvider = async () => {
setBusyAction('install-pi');
setConfigurationError('');
try {
await invoke<AgentConfigActionResult>('install_pi_provider');
await reloadStatusesAfterAction();
} catch (requestError) {
setConfigurationError(String(requestError));
} finally {
setBusyAction(null);
}
};
const closeConfigurationChanges = async () => {
setConfigurationError('');
setBusyAction('close-config');
@@ -1217,7 +1252,7 @@ export function AgentsPage() {
</div>
) : null}
{!isClaudeModelMappingClient ? (
{!isClaudeModelMappingClient && !isPiClient ? (
<section className="agent-core-setting-section agent-model-section">
<div className="agent-section-heading">
<div><strong>{t('agents.useModel')}</strong></div>
@@ -1293,6 +1328,38 @@ export function AgentsPage() {
</section>
) : null}
{isPiClient ? (
<section className={`agent-core-setting-section agent-modification-actions ${activeStatus?.configured ? 'enabled' : ''}`}>
<div className="agent-section-heading">
<div>
<strong>{t('agents.pi.installTitle')}</strong>
<span>{t('agents.pi.installDescription')}</span>
</div>
</div>
<div className="agent-modification-control">
<div className="agent-modification-buttons">
<button
type="button"
className="primary-button"
onClick={() => void 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')}
</button>
</div>
{configurationError ? (
<span className="agent-inline-message error" role="alert" aria-live="polite">
{configurationError}
</span>
) : null}
</div>
</section>
) : (
<section className={`agent-core-setting-section agent-modification-actions ${activeStatus?.modificationState === 'applied' ? 'enabled' : ''}`}>
<div className="agent-section-heading">
<div>
@@ -1377,6 +1444,7 @@ export function AgentsPage() {
) : null}
</div>
</section>
)}
<div className="agent-config-footer">
<div className="agent-launch-control">