Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,10 @@ REASONIX_TRANSCRIPT_DIR=./transcripts
# TELEGRAM_BOT_TOKEN=123456:botfather-token
# TELEGRAM_OWNER_USER_ID=123456789
# TELEGRAM_ALLOWLIST=123456789,987654321

# API 提供商选择(可选)
# 可选值: deepseek, siliconflow, aliyun, volcengine, tencent, baidu, openrouter, fireworks, together, replicate
# DEEPSEEK_API_PROVIDER=siliconflow

# 或者直接指定 Base URL(优先级高于 apiProvider)
# DEEPSEEK_BASE_URL=https://api.siliconflow.cn/v1
596 changes: 330 additions & 266 deletions desktop/package-lock.json

Large diffs are not rendered by default.

19 changes: 14 additions & 5 deletions desktop/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,12 @@ export type Settings = {
contextTokens?: Record<string, number>;
showSystemEvents?: boolean;
version: string;
apiProvider?: string;
apiKeys?: Record<string, string | undefined>;
customProviders?: Record<string, { baseUrl: string; name?: string }>;
availableProviders?: Array<{ id: string; name: string; baseUrl: string; custom: boolean }>;
/** Canonical → provider-specific model names for the current provider. */
availableModels?: Record<string, string>;
};

export type BalanceInfoItem = {
Expand Down Expand Up @@ -420,6 +426,9 @@ function sanitizeSettingsPatch(patch: SettingsPatch): Partial<Settings> {
exaApiKey: _exa,
ollamaApiKey: _ollama,
webSearchEndpoint,
apiProvider: _apiProvider,
apiKey: _apiKey,
customProviders: _customProviders,
...rest
} = patch;
const sanitized: Partial<Settings> = { ...rest };
Expand Down Expand Up @@ -1049,6 +1058,11 @@ function applyIncomingRaw(state: State, ev: IncomingEvent): State {
subagentModels: ev.subagentModels,
showSystemEvents: ev.showSystemEvents,
version: ev.version,
apiProvider: ev.apiProvider,
apiKeys: ev.apiKeys,
customProviders: ev.customProviders,
availableProviders: ev.availableProviders,
availableModels: ev.availableModels,
},
};
}
Expand Down Expand Up @@ -1598,10 +1612,6 @@ function TabRuntime({
sendRpc({ cmd: "qq_config_save", ...patch }),
[sendRpc],
);
const saveApiKey = useCallback(
(key: string) => sendRpc({ cmd: "setup_save_key", key }),
[sendRpc],
);
const addMcpSpec = useCallback(
(spec: string) => sendRpc({ cmd: "mcp_specs_add", spec }),
[sendRpc],
Expand Down Expand Up @@ -2877,7 +2887,6 @@ function TabRuntime({
qq={state.qq}
onClose={() => setSettingsOpen(false)}
onSave={saveSettings}
onSaveApiKey={saveApiKey}
onLoadQQ={loadQQSettings}
onConnectQQ={connectQQ}
onDisconnectQQ={disconnectQQ}
Expand Down
9 changes: 9 additions & 0 deletions desktop/src/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,15 @@ export const de: typeof en = {
baseUrl: "DeepSeek-Basis-URL",
baseUrlHint:
"Nur bei Verwendung eines Proxys überschreiben. Leer = offizieller Endpunkt. Neustart erforderlich.",
provider: "API-Anbieter",
providerHint: "Anbieter auswählen, um die Basis-URL automatisch zu konfigurieren.",
providerCustom: "Benutzerdefinierter Anbieter",
providerCustomAdd: "Hinzufügen",
providerCustomRemove: "Entfernen",
providerCustomId: "ID",
providerCustomUrl: "Basis-URL",
providerCustomName: "Anzeigename (optional)",
providerCustomSave: "Hinzufügen",
workspace: "Arbeitsbereich",
workspaceHint:
"Root-Verzeichnis, in dem Agent-Tools arbeiten. Wechseln speichert in der Konfiguration und lädt Tools neu.",
Expand Down
9 changes: 9 additions & 0 deletions desktop/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,15 @@ export const en = {
},
baseUrl: "DeepSeek base URL",
baseUrlHint: "Override only if using a proxy. Empty = official endpoint. Restart required.",
provider: "API provider",
providerHint: "Select a provider to auto-configure Base URL.",
providerCustom: "Custom provider",
providerCustomAdd: "Add",
providerCustomRemove: "Remove",
providerCustomId: "ID",
providerCustomUrl: "Base URL",
providerCustomName: "Display name (optional)",
providerCustomSave: "Add",
workspace: "Workspace",
workspaceHint:
"Root dir agent tools operate inside. Switching saves to config and reloads tools.",
Expand Down
9 changes: 9 additions & 0 deletions desktop/src/i18n/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,15 @@ export const ja: typeof en = {
},
baseUrl: "DeepSeek ベースURL",
baseUrlHint: "プロキシ使用時のみ上書き。空欄 = 公式エンドポイント。再起動が必要です。",
provider: "APIプロバイダー",
providerHint: "プロバイダーを選択するとベースURLが自動設定されます。",
providerCustom: "カスタムプロバイダー",
providerCustomAdd: "追加",
providerCustomRemove: "削除",
providerCustomId: "ID",
providerCustomUrl: "ベースURL",
providerCustomName: "表示名(任意)",
providerCustomSave: "追加",
workspace: "ワークスペース",
workspaceHint:
"エージェントツールが操作するルートディレクトリ。切り替えは設定に保存され、ツールが再読み込みされます。",
Expand Down
9 changes: 9 additions & 0 deletions desktop/src/i18n/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,15 @@ export const zhCN: typeof en = {
},
baseUrl: "DeepSeek base URL",
baseUrlHint: "走代理时再改。留空 = 官方地址。需重启。",
provider: "API 提供商",
providerHint: "选择 API 提供商后自动配置 Base URL。",
providerCustom: "自定义提供商",
providerCustomAdd: "添加",
providerCustomRemove: "删除",
providerCustomId: "标识 (ID)",
providerCustomUrl: "Base URL",
providerCustomName: "显示名称 (可选)",
providerCustomSave: "添加",
workspace: "工作目录",
workspaceHint: "agent 工具操作的根目录。切换会写入配置并重载工具。",
workspaceChange: "更换…",
Expand Down
9 changes: 9 additions & 0 deletions desktop/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,12 @@ export type SettingsEvent = {
subagentModels?: Record<string, "flash" | "pro">;
showSystemEvents?: boolean;
version: string;
apiProvider?: string;
apiKeys?: Record<string, string | undefined>;
customProviders?: Record<string, { baseUrl: string; name?: string }>;
availableProviders?: Array<{ id: string; name: string; baseUrl: string; custom: boolean }>;
/** Canonical → provider-specific model names for the current provider. */
availableModels?: Record<string, string>;
};

export type QQSettingsEvent = {
Expand Down Expand Up @@ -423,6 +429,9 @@ export type SettingsPatch = {
/** Per-model context-window override (tokens). Keys are model ids; values are the prompt-side token cap. */
contextTokens?: Record<string, number>;
showSystemEvents?: boolean;
apiProvider?: string;
apiKey?: string;
customProviders?: { add?: { id: string; baseUrl: string; name?: string }; remove?: string };
};

export type QQConfigPatch = {
Expand Down
142 changes: 127 additions & 15 deletions desktop/src/ui/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ export function SettingsModal({
qq,
onClose,
onSave,
onSaveApiKey,
onLoadQQ,
onConnectQQ,
onDisconnectQQ,
Expand Down Expand Up @@ -117,7 +116,6 @@ export function SettingsModal({
qq: QQDesktopSettingsState | null;
onClose: () => void;
onSave: (patch: SettingsPatch) => void;
onSaveApiKey: (key: string) => void;
onLoadQQ: () => void;
onConnectQQ: () => void;
onDisconnectQQ: () => void;
Expand Down Expand Up @@ -201,7 +199,19 @@ export function SettingsModal({
onPickWorkspace={onPickWorkspace}
/>
)}
{page === "models" && <PageModels settings={settings} onSave={onSave} />}
{page === "models" && (
<>
<ApiKeySection
baseUrl={settings.baseUrl}
apiKeyPrefix={settings.apiKeyPrefix}
availableProviders={settings.availableProviders}
apiProvider={settings.apiProvider}
apiKeys={settings.apiKeys}
onSave={onSave}
/>
<PageModels settings={settings} onSave={onSave} />
</>
)}
{page === "mcp" && (
<PageMCP
specs={mcpSpecs}
Expand Down Expand Up @@ -232,12 +242,6 @@ export function SettingsModal({
{page === "shortcuts" && <PageShortcuts />}
{page === "general" ? (
<>
<ApiKeySection
baseUrl={settings.baseUrl}
apiKeyPrefix={settings.apiKeyPrefix}
onSave={onSave}
onSaveApiKey={onSaveApiKey}
/>
<QQChannelSection
qq={qq}
configureOpen={qqConfigureOpen}
Expand Down Expand Up @@ -967,25 +971,62 @@ function WebSearchApiKeyRow({
function ApiKeySection({
baseUrl,
apiKeyPrefix,
availableProviders,
apiProvider,
apiKeys,
onSave,
onSaveApiKey,
}: {
baseUrl?: string;
apiKeyPrefix?: string;
availableProviders?: Array<{ id: string; name: string; baseUrl: string; custom: boolean }>;
apiProvider?: string;
apiKeys?: Record<string, string | undefined>;
onSave: (patch: SettingsPatch) => void;
onSaveApiKey: (key: string) => void;
}) {
const [key, setKey] = useState("");
const [urlDraft, setUrlDraft] = useState(baseUrl ?? "");
const [addOpen, setAddOpen] = useState(false);
const [addId, setAddId] = useState("");
const [addUrl, setAddUrl] = useState("");
const [addName, setAddName] = useState("");
// Reset key input when switching providers so the previous
// provider's key doesn't linger in the input field.
useEffect(() => { setKey(""); }, [apiProvider]);
useEffect(() => { setUrlDraft(baseUrl ?? ""); }, [baseUrl]);
const providers = availableProviders ?? [];
const current = providers.find((p) => p.id === apiProvider);
// Only show prefix if this specific provider has a saved key in apiKeys.
// No fallback to apiKeyPrefix — that would leak another provider's key.
const currentApiKeyPrefix = apiKeys?.[apiProvider ?? ""];
return (
<section className="section">
<div className="stitle">{t("settings.apiSection")}</div>
<div className="setting-row">
<div className="l">
<div className="n">{t("settings.provider")}</div>
<div className="h">{t("settings.providerHint")}</div>
</div>
<select
className="field"
value={apiProvider ?? ""}
onChange={(e) => {
const v = e.target.value;
if (v) onSave({ apiProvider: v });
}}
>
{providers.map((p) => (
<option key={p.id} value={p.id}>
{p.name} {p.custom ? "(custom)" : ""} — {p.baseUrl}
</option>
))}
</select>
</div>
<div className="setting-row">
<div className="l">
<div className="n">{t("settings.apiKey")}</div>
<div className="h">
{apiKeyPrefix
? t("settings.apiKeySet", { prefix: apiKeyPrefix })
{currentApiKeyPrefix
? t("settings.apiKeySet", { prefix: currentApiKeyPrefix })
: t("settings.apiKeyNotSet")}
</div>
</div>
Expand All @@ -1003,7 +1044,7 @@ function ApiKeySection({
disabled={!key}
onClick={() => {
if (!key) return;
onSaveApiKey(key);
onSave({ apiKey: key });
setKey("");
}}
>
Expand All @@ -1014,7 +1055,7 @@ function ApiKeySection({
<div className="setting-row">
<div className="l">
<div className="n">{t("settings.baseUrl")}</div>
<div className="h">{t("settings.baseUrlHint")}</div>
<div className="h">{current ? current.baseUrl : t("settings.baseUrlHint")}</div>
</div>
<input
className="field mono"
Expand All @@ -1023,6 +1064,77 @@ function ApiKeySection({
onBlur={() => onSave({ baseUrl: urlDraft.trim() })}
/>
</div>
<div className="setting-row">
<div className="l">
<div className="n">{t("settings.providerCustom")}</div>
</div>
<div style={{ display: "flex", gap: 6 }}>
{!addOpen ? (
<button type="button" className="btn" onClick={() => setAddOpen(true)}>
{t("settings.providerCustomAdd")}
</button>
) : null}
{current?.custom ? (
<button
type="button"
className="btn"
onClick={() => {
onSave({ customProviders: { remove: current.id } });
}}
>
{t("settings.providerCustomRemove")}
</button>
) : null}
</div>
</div>
{addOpen ? (
<div className="setting-row" style={{ flexDirection: "column", gap: 8, alignItems: "flex-start" }}>
<div style={{ display: "flex", gap: 6, width: "100%" }}>
<input
className="field mono"
value={addId}
onChange={(e) => setAddId(e.target.value)}
placeholder={t("settings.providerCustomId")}
style={{ flex: 1 }}
/>
<input
className="field mono"
value={addUrl}
onChange={(e) => setAddUrl(e.target.value)}
placeholder={t("settings.providerCustomUrl")}
style={{ flex: 2 }}
/>
<input
className="field mono"
value={addName}
onChange={(e) => setAddName(e.target.value)}
placeholder={t("settings.providerCustomName")}
style={{ flex: 1 }}
/>
<button
type="button"
className="btn primary"
disabled={!addId.trim() || !addUrl.trim()}
onClick={() => {
onSave({
customProviders: {
add: { id: addId.trim(), baseUrl: addUrl.trim(), name: addName.trim() || undefined },
},
});
setAddId("");
setAddUrl("");
setAddName("");
setAddOpen(false);
}}
>
{t("settings.providerCustomSave")}
</button>
<button type="button" className="btn" onClick={() => setAddOpen(false)}>
{t("settings.apiKeyCancel")}
</button>
</div>
</div>
) : null}
</section>
);
}
Expand Down
2 changes: 2 additions & 0 deletions src/cli/commands/acp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
DEFAULT_MODEL,
bridgeEndpointEnv,
loadApiKey,
loadApiProvider,
loadEditMode,
loadEndpoint,
loadMaxIterPerTurn,
Expand Down Expand Up @@ -184,6 +185,7 @@ async function buildSession(opts: {
prefix,
tools: toolset.tools,
model,
providerId: loadApiProvider(),
budgetUsd: opts.budgetUsd,
maxIterPerTurn: loadMaxIterPerTurn(),
session: `acp-${timestampSuffix()}`,
Expand Down
Loading