mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-25 13:45:03 +08:00
feat(omo): integrate Oh My OpenCode profile management (#972)
* feat(omo): integrate Oh My OpenCode profile management into Provider system Adds full-stack OMO support: backend config read/write/import, OMO-specific provider CRUD with exclusive switching, frontend profile editor with agent/category/model configuration, global config management, and i18n support. * feat(omo): add model/variant dropdowns from enabled providers Replace model text inputs with Select dropdowns sourced from enabled OpenCode providers, add thinking-level variant selection, and prevent auto-enabling newly added OMO providers. * fix(omo): use standard provider action styles for OMO switch button * fix(omo): replace hardcoded isZh strings with proper i18n t() calls
This commit is contained in:
+25
-62
@@ -8,7 +8,6 @@ import {
|
||||
Plus,
|
||||
Settings,
|
||||
ArrowLeft,
|
||||
// Bot, // TODO: Agents 功能开发中,暂时不需要
|
||||
Book,
|
||||
Wrench,
|
||||
RefreshCw,
|
||||
@@ -56,6 +55,7 @@ import { UniversalProviderPanel } from "@/components/universal";
|
||||
import { McpIcon } from "@/components/BrandIcons";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { SessionManagerPage } from "@/components/sessions/SessionManagerPage";
|
||||
import { useDisableCurrentOmo } from "@/lib/query/omo";
|
||||
|
||||
type View =
|
||||
| "providers"
|
||||
@@ -68,7 +68,6 @@ type View =
|
||||
| "universal"
|
||||
| "sessions";
|
||||
|
||||
// macOS Overlay mode needs space for traffic light buttons, Windows/Linux use native titlebar
|
||||
const DRAG_BAR_HEIGHT = isWindows() || isLinux() ? 0 : 28; // px
|
||||
const HEADER_HEIGHT = 64; // px
|
||||
const CONTENT_TOP_OFFSET = DRAG_BAR_HEIGHT + HEADER_HEIGHT;
|
||||
@@ -118,7 +117,6 @@ function App() {
|
||||
localStorage.setItem(VIEW_STORAGE_KEY, currentView);
|
||||
}, [currentView]);
|
||||
|
||||
// Get settings for visibleApps
|
||||
const { data: settingsData } = useSettingsQuery();
|
||||
const visibleApps: VisibleApps = settingsData?.visibleApps ?? {
|
||||
claude: true,
|
||||
@@ -127,7 +125,6 @@ function App() {
|
||||
opencode: true,
|
||||
};
|
||||
|
||||
// Get first visible app for fallback
|
||||
const getFirstVisibleApp = (): AppId => {
|
||||
if (visibleApps.claude) return "claude";
|
||||
if (visibleApps.codex) return "codex";
|
||||
@@ -136,7 +133,6 @@ function App() {
|
||||
return "claude"; // fallback
|
||||
};
|
||||
|
||||
// If current active app is hidden, switch to first visible app
|
||||
useEffect(() => {
|
||||
if (!visibleApps[activeApp]) {
|
||||
setActiveApp(getFirstVisibleApp());
|
||||
@@ -145,7 +141,6 @@ function App() {
|
||||
|
||||
const [editingProvider, setEditingProvider] = useState<Provider | null>(null);
|
||||
const [usageProvider, setUsageProvider] = useState<Provider | null>(null);
|
||||
// Confirm action state: 'remove' = remove from live config, 'delete' = delete from database
|
||||
const [confirmAction, setConfirmAction] = useState<{
|
||||
provider: Provider;
|
||||
action: "remove" | "delete";
|
||||
@@ -153,7 +148,6 @@ function App() {
|
||||
const [envConflicts, setEnvConflicts] = useState<EnvConflict[]>([]);
|
||||
const [showEnvBanner, setShowEnvBanner] = useState(false);
|
||||
|
||||
// 使用 Hook 保存最后有效值,用于动画退出期间保持内容显示
|
||||
const effectiveEditingProvider = useLastValidValue(editingProvider);
|
||||
const effectiveUsageProvider = useLastValidValue(usageProvider);
|
||||
|
||||
@@ -164,15 +158,12 @@ function App() {
|
||||
const addActionButtonClass =
|
||||
"bg-orange-500 hover:bg-orange-600 dark:bg-orange-500 dark:hover:bg-orange-600 text-white shadow-lg shadow-orange-500/30 dark:shadow-orange-500/40 rounded-full w-8 h-8";
|
||||
|
||||
// 获取代理服务状态
|
||||
const {
|
||||
isRunning: isProxyRunning,
|
||||
takeoverStatus,
|
||||
status: proxyStatus,
|
||||
} = useProxyStatus();
|
||||
// 当前应用的代理是否开启
|
||||
const isCurrentAppTakeoverActive = takeoverStatus?.[activeApp] || false;
|
||||
// 当前应用代理实际使用的供应商 ID(从 active_targets 中获取)
|
||||
const activeProviderId = useMemo(() => {
|
||||
const target = proxyStatus?.active_targets?.find(
|
||||
(t) => t.app_type === activeApp,
|
||||
@@ -180,7 +171,6 @@ function App() {
|
||||
return target?.provider_id;
|
||||
}, [proxyStatus?.active_targets, activeApp]);
|
||||
|
||||
// 获取供应商列表,当代理服务运行时自动刷新
|
||||
const { data, isLoading, refetch } = useProvidersQuery(activeApp, {
|
||||
isProxyRunning,
|
||||
});
|
||||
@@ -188,7 +178,6 @@ function App() {
|
||||
const currentProviderId = data?.currentProviderId ?? "";
|
||||
const hasSkillsSupport = true;
|
||||
|
||||
// 🎯 使用 useProviderActions Hook 统一管理所有 Provider 操作
|
||||
const {
|
||||
addProvider,
|
||||
updateProvider,
|
||||
@@ -197,7 +186,23 @@ function App() {
|
||||
saveUsageScript,
|
||||
} = useProviderActions(activeApp);
|
||||
|
||||
// 监听来自托盘菜单的切换事件
|
||||
const disableOmoMutation = useDisableCurrentOmo();
|
||||
const handleDisableOmo = () => {
|
||||
disableOmoMutation.mutate(undefined, {
|
||||
onSuccess: () => {
|
||||
toast.success(t("omo.disabled", { defaultValue: "OMO 已停用" }));
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(
|
||||
t("omo.disableFailed", {
|
||||
defaultValue: "停用 OMO 失败: {{error}}",
|
||||
error: extractErrorMessage(error),
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let unsubscribe: (() => void) | undefined;
|
||||
|
||||
@@ -221,7 +226,6 @@ function App() {
|
||||
};
|
||||
}, [activeApp, refetch]);
|
||||
|
||||
// 监听统一供应商同步事件,刷新所有应用的供应商列表
|
||||
useEffect(() => {
|
||||
let unsubscribe: (() => void) | undefined;
|
||||
|
||||
@@ -229,10 +233,7 @@ function App() {
|
||||
try {
|
||||
const { listen } = await import("@tauri-apps/api/event");
|
||||
unsubscribe = await listen("universal-provider-synced", async () => {
|
||||
// 统一供应商同步后刷新所有应用的供应商列表
|
||||
// 使用 invalidateQueries 使所有 providers 查询失效
|
||||
await queryClient.invalidateQueries({ queryKey: ["providers"] });
|
||||
// 同时更新托盘菜单
|
||||
try {
|
||||
await providersApi.updateTrayMenu();
|
||||
} catch (error) {
|
||||
@@ -253,7 +254,6 @@ function App() {
|
||||
};
|
||||
}, [queryClient]);
|
||||
|
||||
// 应用启动时检测所有应用的环境变量冲突
|
||||
useEffect(() => {
|
||||
const checkEnvOnStartup = async () => {
|
||||
try {
|
||||
@@ -278,7 +278,6 @@ function App() {
|
||||
checkEnvOnStartup();
|
||||
}, []);
|
||||
|
||||
// 应用启动时检查是否刚完成了配置迁移
|
||||
useEffect(() => {
|
||||
const checkMigration = async () => {
|
||||
try {
|
||||
@@ -297,7 +296,6 @@ function App() {
|
||||
checkMigration();
|
||||
}, [t]);
|
||||
|
||||
// 应用启动时检查是否刚完成了 Skills 自动导入(统一管理 SSOT)
|
||||
useEffect(() => {
|
||||
const checkSkillsMigration = async () => {
|
||||
try {
|
||||
@@ -326,14 +324,12 @@ function App() {
|
||||
checkSkillsMigration();
|
||||
}, [t, queryClient]);
|
||||
|
||||
// 切换应用时检测当前应用的环境变量冲突
|
||||
useEffect(() => {
|
||||
const checkEnvOnSwitch = async () => {
|
||||
try {
|
||||
const conflicts = await checkEnvConflicts(activeApp);
|
||||
|
||||
if (conflicts.length > 0) {
|
||||
// 合并新检测到的冲突
|
||||
setEnvConflicts((prev) => {
|
||||
const existingKeys = new Set(
|
||||
prev.map((c) => `${c.varName}:${c.sourcePath}`),
|
||||
@@ -359,7 +355,6 @@ function App() {
|
||||
checkEnvOnSwitch();
|
||||
}, [activeApp]);
|
||||
|
||||
// 全局键盘快捷键
|
||||
const currentViewRef = useRef(currentView);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -368,17 +363,14 @@ function App() {
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
// Cmd/Ctrl + , 打开设置
|
||||
if (event.key === "," && (event.metaKey || event.ctrlKey)) {
|
||||
event.preventDefault();
|
||||
setCurrentView("settings");
|
||||
return;
|
||||
}
|
||||
|
||||
// ESC 键返回
|
||||
if (event.key !== "Escape" || event.defaultPrevented) return;
|
||||
|
||||
// 如果有模态框打开(通过 overflow hidden 判断),则不处理全局 ESC,交给模态框处理
|
||||
if (document.body.style.overflow === "hidden") return;
|
||||
|
||||
const view = currentViewRef.current;
|
||||
@@ -396,7 +388,6 @@ function App() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 打开网站链接
|
||||
const handleOpenWebsite = async (url: string) => {
|
||||
try {
|
||||
await settingsApi.openExternal(url);
|
||||
@@ -410,22 +401,17 @@ function App() {
|
||||
}
|
||||
};
|
||||
|
||||
// 编辑供应商
|
||||
const handleEditProvider = async (provider: Provider) => {
|
||||
await updateProvider(provider);
|
||||
setEditingProvider(null);
|
||||
};
|
||||
|
||||
// 确认删除/移除供应商
|
||||
const handleConfirmAction = async () => {
|
||||
if (!confirmAction) return;
|
||||
const { provider, action } = confirmAction;
|
||||
|
||||
if (action === "remove") {
|
||||
// Remove from live config only (for additive mode apps like OpenCode)
|
||||
// Does NOT delete from database - provider remains in the list
|
||||
await providersApi.removeFromLiveConfig(provider.id, activeApp);
|
||||
// Invalidate queries to refresh the isInConfig state
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["opencodeLiveProviderIds"],
|
||||
});
|
||||
@@ -436,13 +422,11 @@ function App() {
|
||||
{ closeButton: true },
|
||||
);
|
||||
} else {
|
||||
// Delete from database
|
||||
await deleteProvider(provider.id);
|
||||
}
|
||||
setConfirmAction(null);
|
||||
};
|
||||
|
||||
// Generate a unique provider key for OpenCode duplication
|
||||
const generateUniqueOpencodeKey = (
|
||||
originalKey: string,
|
||||
existingKeys: string[],
|
||||
@@ -453,7 +437,6 @@ function App() {
|
||||
return baseKey;
|
||||
}
|
||||
|
||||
// If -copy already exists, try -copy-2, -copy-3, ...
|
||||
let counter = 2;
|
||||
while (existingKeys.includes(`${baseKey}-${counter}`)) {
|
||||
counter++;
|
||||
@@ -461,9 +444,7 @@ function App() {
|
||||
return `${baseKey}-${counter}`;
|
||||
};
|
||||
|
||||
// 复制供应商
|
||||
const handleDuplicateProvider = async (provider: Provider) => {
|
||||
// 1️⃣ 计算新的 sortIndex:如果原供应商有 sortIndex,则复制它
|
||||
const newSortIndex =
|
||||
provider.sortIndex !== undefined ? provider.sortIndex + 1 : undefined;
|
||||
|
||||
@@ -482,7 +463,6 @@ function App() {
|
||||
iconColor: provider.iconColor,
|
||||
};
|
||||
|
||||
// OpenCode: generate unique provider key (used as ID)
|
||||
if (activeApp === "opencode") {
|
||||
const existingKeys = Object.keys(providers);
|
||||
duplicatedProvider.providerKey = generateUniqueOpencodeKey(
|
||||
@@ -491,7 +471,6 @@ function App() {
|
||||
);
|
||||
}
|
||||
|
||||
// 2️⃣ 如果原供应商有 sortIndex,需要将后续所有供应商的 sortIndex +1
|
||||
if (provider.sortIndex !== undefined) {
|
||||
const updates = Object.values(providers)
|
||||
.filter(
|
||||
@@ -505,7 +484,6 @@ function App() {
|
||||
sortIndex: p.sortIndex! + 1,
|
||||
}));
|
||||
|
||||
// 先更新现有供应商的 sortIndex,为新供应商腾出位置
|
||||
if (updates.length > 0) {
|
||||
try {
|
||||
await providersApi.updateSortOrder(updates, activeApp);
|
||||
@@ -521,11 +499,9 @@ function App() {
|
||||
}
|
||||
}
|
||||
|
||||
// 3️⃣ 添加复制的供应商
|
||||
await addProvider(duplicatedProvider);
|
||||
};
|
||||
|
||||
// 打开提供商终端
|
||||
const handleOpenTerminal = async (provider: Provider) => {
|
||||
try {
|
||||
await providersApi.openTerminal(provider.id, activeApp);
|
||||
@@ -545,10 +521,8 @@ function App() {
|
||||
}
|
||||
};
|
||||
|
||||
// 导入配置成功后刷新
|
||||
const handleImportSuccess = async () => {
|
||||
try {
|
||||
// 导入会影响所有应用的供应商数据:刷新所有 providers 缓存
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["providers"],
|
||||
refetchType: "all",
|
||||
@@ -626,7 +600,6 @@ function App() {
|
||||
default:
|
||||
return (
|
||||
<div className="px-6 flex flex-col h-[calc(100vh-8rem)] overflow-hidden">
|
||||
{/* 独立滚动容器 - 解决 Linux/Ubuntu 下 DndContext 与滚轮事件冲突 */}
|
||||
<div className="flex-1 overflow-y-auto overflow-x-hidden pb-12 px-1">
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
@@ -648,7 +621,9 @@ function App() {
|
||||
}
|
||||
activeProviderId={activeProviderId}
|
||||
onSwitch={switchProvider}
|
||||
onEdit={setEditingProvider}
|
||||
onEdit={(provider) => {
|
||||
setEditingProvider(provider);
|
||||
}}
|
||||
onDelete={(provider) =>
|
||||
setConfirmAction({ provider, action: "delete" })
|
||||
}
|
||||
@@ -658,6 +633,9 @@ function App() {
|
||||
setConfirmAction({ provider, action: "remove" })
|
||||
: undefined
|
||||
}
|
||||
onDisableOmo={
|
||||
activeApp === "opencode" ? handleDisableOmo : undefined
|
||||
}
|
||||
onDuplicate={handleDuplicateProvider}
|
||||
onConfigureUsage={setUsageProvider}
|
||||
onOpenWebsite={handleOpenWebsite}
|
||||
@@ -695,13 +673,11 @@ function App() {
|
||||
className="flex flex-col h-screen overflow-hidden bg-background text-foreground selection:bg-primary/30"
|
||||
style={{ overflowX: "hidden", paddingTop: CONTENT_TOP_OFFSET }}
|
||||
>
|
||||
{/* 全局拖拽区域(顶部 28px),避免上边框无法拖动 */}
|
||||
<div
|
||||
className="fixed top-0 left-0 right-0 z-[60]"
|
||||
data-tauri-drag-region
|
||||
style={{ WebkitAppRegion: "drag", height: DRAG_BAR_HEIGHT } as any}
|
||||
/>
|
||||
{/* 环境变量警告横幅 */}
|
||||
{showEnvBanner && envConflicts.length > 0 && (
|
||||
<EnvWarningBanner
|
||||
conflicts={envConflicts}
|
||||
@@ -710,7 +686,6 @@ function App() {
|
||||
sessionStorage.setItem("env_banner_dismissed", "true");
|
||||
}}
|
||||
onDeleted={async () => {
|
||||
// 删除后重新检测
|
||||
try {
|
||||
const allConflicts = await checkAllEnvConflicts();
|
||||
const flatConflicts = Object.values(allConflicts).flat();
|
||||
@@ -822,7 +797,7 @@ function App() {
|
||||
setSettingsDefaultTab("usage");
|
||||
setCurrentView("settings");
|
||||
}}
|
||||
title={t("settings.usage.title", {
|
||||
title={t("usage.title", {
|
||||
defaultValue: "使用统计",
|
||||
})}
|
||||
className="hover:bg-black/5 dark:hover:bg-white/5"
|
||||
@@ -970,18 +945,6 @@ function App() {
|
||||
>
|
||||
<Wrench className="flex-shrink-0 w-4 h-4" />
|
||||
</Button>
|
||||
{/* TODO: Agents 功能开发中,暂时隐藏入口 */}
|
||||
{/* {isClaudeApp && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setCurrentView("agents")}
|
||||
className="text-muted-foreground hover:text-foreground hover:bg-black/5 dark:hover:bg-white/5"
|
||||
title="Agents"
|
||||
>
|
||||
<Bot className="w-4 h-4" />
|
||||
</Button>
|
||||
)} */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
|
||||
@@ -17,7 +17,6 @@ import { UniversalProviderPanel } from "@/components/universal";
|
||||
import { providerPresets } from "@/config/claudeProviderPresets";
|
||||
import { codexProviderPresets } from "@/config/codexProviderPresets";
|
||||
import { geminiProviderPresets } from "@/config/geminiProviderPresets";
|
||||
// Note: opencodeProviderPresets is loaded via ProviderForm, not needed here
|
||||
import type { UniversalProviderPreset } from "@/config/universalProviderPresets";
|
||||
|
||||
interface AddProviderDialogProps {
|
||||
@@ -36,7 +35,6 @@ export function AddProviderDialog({
|
||||
onSubmit,
|
||||
}: AddProviderDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
// OpenCode doesn't support universal providers
|
||||
const showUniversalTab = appId !== "opencode";
|
||||
const [activeTab, setActiveTab] = useState<"app-specific" | "universal">(
|
||||
"app-specific",
|
||||
@@ -45,7 +43,6 @@ export function AddProviderDialog({
|
||||
const [selectedUniversalPreset, setSelectedUniversalPreset] =
|
||||
useState<UniversalProviderPreset | null>(null);
|
||||
|
||||
// Handle universal provider save
|
||||
const handleUniversalProviderSave = useCallback(
|
||||
async (provider: UniversalProvider) => {
|
||||
try {
|
||||
@@ -73,7 +70,6 @@ export function AddProviderDialog({
|
||||
[t, onOpenChange],
|
||||
);
|
||||
|
||||
// Close universal form and return to main dialog
|
||||
const handleUniversalFormClose = useCallback(() => {
|
||||
setUniversalFormOpen(false);
|
||||
setSelectedUniversalPreset(null);
|
||||
@@ -86,7 +82,6 @@ export function AddProviderDialog({
|
||||
unknown
|
||||
>;
|
||||
|
||||
// 构造基础提交数据
|
||||
const providerData: Omit<Provider, "id"> & { providerKey?: string } = {
|
||||
name: values.name.trim(),
|
||||
notes: values.notes?.trim() || undefined,
|
||||
@@ -98,7 +93,6 @@ export function AddProviderDialog({
|
||||
...(values.meta ? { meta: values.meta } : {}),
|
||||
};
|
||||
|
||||
// OpenCode: pass providerKey for ID generation
|
||||
if (appId === "opencode" && values.providerKey) {
|
||||
providerData.providerKey = values.providerKey;
|
||||
}
|
||||
@@ -107,8 +101,7 @@ export function AddProviderDialog({
|
||||
providerData.meta?.custom_endpoints &&
|
||||
Object.keys(providerData.meta.custom_endpoints).length > 0;
|
||||
|
||||
if (!hasCustomEndpoints) {
|
||||
// 收集端点候选(仅在缺少自定义端点时兜底)
|
||||
if (!hasCustomEndpoints && values.presetCategory !== "omo") {
|
||||
const urlSet = new Set<string>();
|
||||
|
||||
const addUrl = (rawUrl?: string) => {
|
||||
@@ -163,7 +156,6 @@ export function AddProviderDialog({
|
||||
}
|
||||
}
|
||||
}
|
||||
// Note: OpenCode doesn't use endpointCandidates - it handles endpoints internally
|
||||
}
|
||||
|
||||
if (appId === "claude") {
|
||||
@@ -187,7 +179,6 @@ export function AddProviderDialog({
|
||||
addUrl(env.GOOGLE_GEMINI_BASE_URL);
|
||||
}
|
||||
} else if (appId === "opencode") {
|
||||
// OpenCode uses options.baseURL
|
||||
const options = parsedConfig.options as
|
||||
| Record<string, any>
|
||||
| undefined;
|
||||
@@ -221,7 +212,6 @@ export function AddProviderDialog({
|
||||
[appId, onSubmit, onOpenChange],
|
||||
);
|
||||
|
||||
// 动态 footer:根据当前 Tab 显示不同按钮
|
||||
const footer =
|
||||
!showUniversalTab || activeTab === "app-specific" ? (
|
||||
<>
|
||||
@@ -296,7 +286,6 @@ export function AddProviderDialog({
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
) : (
|
||||
// OpenCode: directly show form without tabs
|
||||
<ProviderForm
|
||||
appId={appId}
|
||||
submitLabel={t("common.add")}
|
||||
@@ -306,7 +295,6 @@ export function AddProviderDialog({
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Universal Provider Form Modal */}
|
||||
{showUniversalTab && (
|
||||
<UniversalProviderFormModal
|
||||
isOpen={universalFormOpen}
|
||||
|
||||
@@ -19,20 +19,20 @@ import type { AppId } from "@/lib/api";
|
||||
interface ProviderActionsProps {
|
||||
appId?: AppId;
|
||||
isCurrent: boolean;
|
||||
/** OpenCode: 是否已添加到配置 */
|
||||
isInConfig?: boolean;
|
||||
isTesting?: boolean;
|
||||
isProxyTakeover?: boolean;
|
||||
isOmo?: boolean;
|
||||
isLastOmo?: boolean;
|
||||
onSwitch: () => void;
|
||||
onEdit: () => void;
|
||||
onDuplicate: () => void;
|
||||
onTest?: () => void;
|
||||
onConfigureUsage: () => void;
|
||||
onDelete: () => void;
|
||||
/** OpenCode: remove from live config (not delete from database) */
|
||||
onRemoveFromConfig?: () => void;
|
||||
onDisableOmo?: () => void;
|
||||
onOpenTerminal?: () => void;
|
||||
// 故障转移相关
|
||||
isAutoFailoverEnabled?: boolean;
|
||||
isInFailoverQueue?: boolean;
|
||||
onToggleFailover?: (enabled: boolean) => void;
|
||||
@@ -44,6 +44,8 @@ export function ProviderActions({
|
||||
isInConfig = false,
|
||||
isTesting,
|
||||
isProxyTakeover = false,
|
||||
isOmo = false,
|
||||
isLastOmo = false,
|
||||
onSwitch,
|
||||
onEdit,
|
||||
onDuplicate,
|
||||
@@ -51,8 +53,8 @@ export function ProviderActions({
|
||||
onConfigureUsage,
|
||||
onDelete,
|
||||
onRemoveFromConfig,
|
||||
onDisableOmo,
|
||||
onOpenTerminal,
|
||||
// 故障转移相关
|
||||
isAutoFailoverEnabled = false,
|
||||
isInFailoverQueue = false,
|
||||
onToggleFailover,
|
||||
@@ -60,19 +62,20 @@ export function ProviderActions({
|
||||
const { t } = useTranslation();
|
||||
const iconButtonClass = "h-8 w-8 p-1";
|
||||
|
||||
// OpenCode 使用累加模式
|
||||
const isOpenCodeMode = appId === "opencode";
|
||||
const isOpenCodeMode = appId === "opencode" && !isOmo;
|
||||
|
||||
// 故障转移模式下的按钮逻辑(OpenCode 不支持故障转移)
|
||||
const isFailoverMode =
|
||||
!isOpenCodeMode && isAutoFailoverEnabled && onToggleFailover;
|
||||
!isOpenCodeMode && !isOmo && isAutoFailoverEnabled && onToggleFailover;
|
||||
|
||||
// 处理主按钮点击
|
||||
const handleMainButtonClick = () => {
|
||||
if (isOpenCodeMode) {
|
||||
// OpenCode 模式:切换配置状态(添加/移除)
|
||||
if (isOmo) {
|
||||
if (isCurrent) {
|
||||
onDisableOmo?.();
|
||||
} else {
|
||||
onSwitch();
|
||||
}
|
||||
} else if (isOpenCodeMode) {
|
||||
if (isInConfig) {
|
||||
// Use onRemoveFromConfig if available, otherwise fall back to onDelete
|
||||
if (onRemoveFromConfig) {
|
||||
onRemoveFromConfig();
|
||||
} else {
|
||||
@@ -82,17 +85,33 @@ export function ProviderActions({
|
||||
onSwitch(); // 添加到配置
|
||||
}
|
||||
} else if (isFailoverMode) {
|
||||
// 故障转移模式:切换队列状态
|
||||
onToggleFailover(!isInFailoverQueue);
|
||||
} else {
|
||||
// 普通模式:切换供应商
|
||||
onSwitch();
|
||||
}
|
||||
};
|
||||
|
||||
// 主按钮的状态和样式
|
||||
const getMainButtonState = () => {
|
||||
// OpenCode 累加模式
|
||||
if (isOmo) {
|
||||
if (isCurrent) {
|
||||
return {
|
||||
disabled: false,
|
||||
variant: "secondary" as const,
|
||||
className:
|
||||
"bg-gray-200 text-muted-foreground hover:bg-gray-200 hover:text-muted-foreground dark:bg-gray-700 dark:hover:bg-gray-700",
|
||||
icon: <Check className="h-4 w-4" />,
|
||||
text: t("provider.inUse"),
|
||||
};
|
||||
}
|
||||
return {
|
||||
disabled: false,
|
||||
variant: "default" as const,
|
||||
className: "",
|
||||
icon: <Play className="h-4 w-4" />,
|
||||
text: t("provider.enable"),
|
||||
};
|
||||
}
|
||||
|
||||
if (isOpenCodeMode) {
|
||||
if (isInConfig) {
|
||||
return {
|
||||
@@ -114,7 +133,6 @@ export function ProviderActions({
|
||||
};
|
||||
}
|
||||
|
||||
// 故障转移模式
|
||||
if (isFailoverMode) {
|
||||
if (isInFailoverQueue) {
|
||||
return {
|
||||
@@ -136,7 +154,6 @@ export function ProviderActions({
|
||||
};
|
||||
}
|
||||
|
||||
// 普通模式
|
||||
if (isCurrent) {
|
||||
return {
|
||||
disabled: true,
|
||||
@@ -161,8 +178,11 @@ export function ProviderActions({
|
||||
|
||||
const buttonState = getMainButtonState();
|
||||
|
||||
// OpenCode 模式下删除按钮始终可用(主按钮"移除"是从 live 配置移除,删除是从数据库删除)
|
||||
const canDelete = isOpenCodeMode ? true : !isCurrent;
|
||||
const canDelete = isOmo
|
||||
? !(isLastOmo && isCurrent)
|
||||
: isOpenCodeMode
|
||||
? true
|
||||
: !isCurrent;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1.5">
|
||||
|
||||
@@ -27,11 +27,13 @@ interface ProviderCardProps {
|
||||
isCurrent: boolean;
|
||||
appId: AppId;
|
||||
isInConfig?: boolean; // OpenCode: 是否已添加到 opencode.json
|
||||
isOmo?: boolean;
|
||||
isLastOmo?: boolean;
|
||||
onSwitch: (provider: Provider) => void;
|
||||
onEdit: (provider: Provider) => void;
|
||||
onDelete: (provider: Provider) => void;
|
||||
/** OpenCode: remove from live config (not delete from database) */
|
||||
onRemoveFromConfig?: (provider: Provider) => void;
|
||||
onDisableOmo?: () => void;
|
||||
onConfigureUsage: (provider: Provider) => void;
|
||||
onOpenWebsite: (url: string) => void;
|
||||
onDuplicate: (provider: Provider) => void;
|
||||
@@ -41,7 +43,6 @@ interface ProviderCardProps {
|
||||
isProxyRunning: boolean;
|
||||
isProxyTakeover?: boolean; // 代理接管模式(Live配置已被接管,切换为热切换)
|
||||
dragHandleProps?: DragHandleProps;
|
||||
// 故障转移相关
|
||||
isAutoFailoverEnabled?: boolean; // 是否开启自动故障转移
|
||||
failoverPriority?: number; // 故障转移优先级(1 = P1, 2 = P2, ...)
|
||||
isInFailoverQueue?: boolean; // 是否在故障转移队列中
|
||||
@@ -50,17 +51,14 @@ interface ProviderCardProps {
|
||||
}
|
||||
|
||||
const extractApiUrl = (provider: Provider, fallbackText: string) => {
|
||||
// 优先级 1: 备注
|
||||
if (provider.notes?.trim()) {
|
||||
return provider.notes.trim();
|
||||
}
|
||||
|
||||
// 优先级 2: 官网地址
|
||||
if (provider.websiteUrl) {
|
||||
return provider.websiteUrl;
|
||||
}
|
||||
|
||||
// 优先级 3: 从配置中提取请求地址
|
||||
const config = provider.settingsConfig;
|
||||
|
||||
if (config && typeof config === "object") {
|
||||
@@ -89,10 +87,13 @@ export function ProviderCard({
|
||||
isCurrent,
|
||||
appId,
|
||||
isInConfig = true,
|
||||
isOmo = false,
|
||||
isLastOmo = false,
|
||||
onSwitch,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onRemoveFromConfig,
|
||||
onDisableOmo,
|
||||
onConfigureUsage,
|
||||
onOpenWebsite,
|
||||
onDuplicate,
|
||||
@@ -102,7 +103,6 @@ export function ProviderCard({
|
||||
isProxyRunning,
|
||||
isProxyTakeover = false,
|
||||
dragHandleProps,
|
||||
// 故障转移相关
|
||||
isAutoFailoverEnabled = false,
|
||||
failoverPriority,
|
||||
isInFailoverQueue = false,
|
||||
@@ -111,7 +111,6 @@ export function ProviderCard({
|
||||
}: ProviderCardProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// 获取供应商健康状态
|
||||
const { data: health } = useProviderHealth(provider.id, appId);
|
||||
|
||||
const fallbackUrlText = t("provider.notConfigured", {
|
||||
@@ -122,24 +121,18 @@ export function ProviderCard({
|
||||
return extractApiUrl(provider, fallbackUrlText);
|
||||
}, [provider, fallbackUrlText]);
|
||||
|
||||
// 判断是否为可点击的 URL(备注不可点击)
|
||||
const isClickableUrl = useMemo(() => {
|
||||
// 如果有备注,则不可点击
|
||||
if (provider.notes?.trim()) {
|
||||
return false;
|
||||
}
|
||||
// 如果显示的是回退文本,也不可点击
|
||||
if (displayUrl === fallbackUrlText) {
|
||||
return false;
|
||||
}
|
||||
// 其他情况(官网地址或请求地址)可点击
|
||||
return true;
|
||||
}, [provider.notes, displayUrl, fallbackUrlText]);
|
||||
|
||||
const usageEnabled = provider.meta?.usage_script?.enabled ?? false;
|
||||
|
||||
// 获取用量数据以判断是否有多套餐
|
||||
// OpenCode(累加模式):使用 isInConfig 代替 isCurrent
|
||||
const shouldAutoQuery = appId === "opencode" ? isInConfig : isCurrent;
|
||||
const autoQueryInterval = shouldAutoQuery
|
||||
? provider.meta?.usage_script?.autoQueryInterval || 0
|
||||
@@ -153,21 +146,17 @@ export function ProviderCard({
|
||||
const hasMultiplePlans =
|
||||
usage?.success && usage.data && usage.data.length > 1;
|
||||
|
||||
// 多套餐默认展开
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
// 操作按钮容器 ref,用于动态计算宽度
|
||||
const actionsRef = useRef<HTMLDivElement>(null);
|
||||
const [actionsWidth, setActionsWidth] = useState(0);
|
||||
|
||||
// 当检测到多套餐时自动展开
|
||||
useEffect(() => {
|
||||
if (hasMultiplePlans) {
|
||||
setIsExpanded(true);
|
||||
}
|
||||
}, [hasMultiplePlans]);
|
||||
|
||||
// 动态获取操作按钮宽度
|
||||
useEffect(() => {
|
||||
if (actionsRef.current) {
|
||||
const updateWidth = () => {
|
||||
@@ -175,7 +164,6 @@ export function ProviderCard({
|
||||
setActionsWidth(width);
|
||||
};
|
||||
updateWidth();
|
||||
// 监听窗口大小变化
|
||||
window.addEventListener("resize", updateWidth);
|
||||
return () => window.removeEventListener("resize", updateWidth);
|
||||
}
|
||||
@@ -188,32 +176,27 @@ export function ProviderCard({
|
||||
onOpenWebsite(displayUrl);
|
||||
};
|
||||
|
||||
// 判断是否是"当前使用中"的供应商
|
||||
// - OpenCode(累加模式):不存在"当前"概念,始终返回 false
|
||||
// - 故障转移模式:代理实际使用的供应商(activeProviderId)
|
||||
// - 代理接管模式(非故障转移):isCurrent
|
||||
// - 普通模式:isCurrent
|
||||
const isActiveProvider =
|
||||
appId === "opencode"
|
||||
const isActiveProvider = isOmo
|
||||
? isCurrent
|
||||
: appId === "opencode"
|
||||
? false
|
||||
: isAutoFailoverEnabled
|
||||
? activeProviderId === provider.id
|
||||
: isCurrent;
|
||||
|
||||
// 判断是否使用绿色(代理接管模式)还是蓝色(普通模式)
|
||||
const shouldUseGreen = isProxyTakeover && isActiveProvider;
|
||||
const shouldUseBlue = !isProxyTakeover && isActiveProvider;
|
||||
const shouldUseGreen = !isOmo && isProxyTakeover && isActiveProvider;
|
||||
const shouldUseBlue =
|
||||
(isOmo && isActiveProvider) ||
|
||||
(!isOmo && !isProxyTakeover && isActiveProvider);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative overflow-hidden rounded-xl border border-border p-4 transition-all duration-300",
|
||||
"bg-card text-card-foreground group",
|
||||
// hover 时的边框效果
|
||||
isAutoFailoverEnabled || isProxyTakeover
|
||||
? "hover:border-emerald-500/50"
|
||||
: "hover:border-border-active",
|
||||
// 当前激活的供应商边框样式
|
||||
shouldUseGreen &&
|
||||
"border-emerald-500/60 shadow-sm shadow-emerald-500/10",
|
||||
shouldUseBlue && "border-blue-500/60 shadow-sm shadow-blue-500/10",
|
||||
@@ -225,7 +208,6 @@ export function ProviderCard({
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-0 bg-gradient-to-r to-transparent transition-opacity duration-500 pointer-events-none",
|
||||
// 代理接管模式使用绿色渐变,普通模式使用蓝色渐变
|
||||
shouldUseGreen && "from-emerald-500/10",
|
||||
shouldUseBlue && "from-blue-500/10",
|
||||
!isActiveProvider && "from-primary/10",
|
||||
@@ -248,7 +230,6 @@ export function ProviderCard({
|
||||
<GripVertical className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
{/* 供应商图标 */}
|
||||
<div className="h-8 w-8 rounded-lg bg-muted flex items-center justify-center border border-border group-hover:scale-105 transition-transform duration-300">
|
||||
<ProviderIcon
|
||||
icon={provider.icon}
|
||||
@@ -264,14 +245,18 @@ export function ProviderCard({
|
||||
{provider.name}
|
||||
</h3>
|
||||
|
||||
{/* 健康状态徽章 */}
|
||||
{isOmo && (
|
||||
<span className="inline-flex items-center rounded-md bg-violet-100 px-1.5 py-0.5 text-[10px] font-semibold text-violet-700 dark:bg-violet-900/40 dark:text-violet-300">
|
||||
OMO
|
||||
</span>
|
||||
)}
|
||||
|
||||
{isProxyRunning && isInFailoverQueue && health && (
|
||||
<ProviderHealthBadge
|
||||
consecutiveFailures={health.consecutive_failures}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 故障转移优先级徽章 */}
|
||||
{isAutoFailoverEnabled &&
|
||||
isInFailoverQueue &&
|
||||
failoverPriority && (
|
||||
@@ -318,10 +303,8 @@ export function ProviderCard({
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
{/* 用量信息区域 - hover 时向左移动,为操作按钮腾出空间 */}
|
||||
<div className="ml-auto">
|
||||
<div className="flex items-center gap-1 transition-transform duration-200 group-hover:-translate-x-[var(--actions-width)] group-focus-within:-translate-x-[var(--actions-width)]">
|
||||
{/* 多套餐时显示套餐数量,单套餐时显示详细信息 */}
|
||||
{hasMultiplePlans ? (
|
||||
<div className="flex items-center gap-2 text-xs text-gray-600 dark:text-gray-400">
|
||||
<span className="font-medium">
|
||||
@@ -342,7 +325,6 @@ export function ProviderCard({
|
||||
inline={true}
|
||||
/>
|
||||
)}
|
||||
{/* 展开/折叠按钮 - 仅在有多套餐时显示 */}
|
||||
{hasMultiplePlans && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
@@ -366,7 +348,6 @@ export function ProviderCard({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 操作按钮区域 - 绝对定位在右侧,hover 时滑入,与用量信息保持间距 */}
|
||||
<div
|
||||
ref={actionsRef}
|
||||
className="absolute right-0 top-1/2 -translate-y-1/2 flex items-center gap-1.5 pl-3 opacity-0 pointer-events-none group-hover:opacity-100 group-focus-within:opacity-100 group-hover:pointer-events-auto group-focus-within:pointer-events-auto transition-all duration-200 translate-x-2 group-hover:translate-x-0 group-focus-within:translate-x-0"
|
||||
@@ -377,6 +358,8 @@ export function ProviderCard({
|
||||
isInConfig={isInConfig}
|
||||
isTesting={isTesting}
|
||||
isProxyTakeover={isProxyTakeover}
|
||||
isOmo={isOmo}
|
||||
isLastOmo={isLastOmo}
|
||||
onSwitch={() => onSwitch(provider)}
|
||||
onEdit={() => onEdit(provider)}
|
||||
onDuplicate={() => onDuplicate(provider)}
|
||||
@@ -388,10 +371,10 @@ export function ProviderCard({
|
||||
? () => onRemoveFromConfig(provider)
|
||||
: undefined
|
||||
}
|
||||
onDisableOmo={onDisableOmo}
|
||||
onOpenTerminal={
|
||||
onOpenTerminal ? () => onOpenTerminal(provider) : undefined
|
||||
}
|
||||
// 故障转移相关
|
||||
isAutoFailoverEnabled={isAutoFailoverEnabled}
|
||||
isInFailoverQueue={isInFailoverQueue}
|
||||
onToggleFailover={onToggleFailover}
|
||||
@@ -400,7 +383,6 @@ export function ProviderCard({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 展开的完整套餐列表 */}
|
||||
{isExpanded && hasMultiplePlans && (
|
||||
<div className="mt-4 pt-4 border-t border-border-default">
|
||||
<UsageFooter
|
||||
|
||||
@@ -20,7 +20,6 @@ import type { Provider } from "@/types";
|
||||
import type { AppId } from "@/lib/api";
|
||||
import { providersApi } from "@/lib/api/providers";
|
||||
import { useDragSort } from "@/hooks/useDragSort";
|
||||
// import { useStreamCheck } from "@/hooks/useStreamCheck"; // 测试功能已隐藏
|
||||
import { ProviderCard } from "@/components/providers/ProviderCard";
|
||||
import { ProviderEmptyState } from "@/components/providers/ProviderEmptyState";
|
||||
import {
|
||||
@@ -29,6 +28,7 @@ import {
|
||||
useAddToFailoverQueue,
|
||||
useRemoveFromFailoverQueue,
|
||||
} from "@/lib/query/failover";
|
||||
import { useCurrentOmoProviderId, useOmoProviderCount } from "@/lib/query/omo";
|
||||
import { useCallback } from "react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -40,8 +40,8 @@ interface ProviderListProps {
|
||||
onSwitch: (provider: Provider) => void;
|
||||
onEdit: (provider: Provider) => void;
|
||||
onDelete: (provider: Provider) => void;
|
||||
/** OpenCode: remove from live config (not delete from database) */
|
||||
onRemoveFromConfig?: (provider: Provider) => void;
|
||||
onDisableOmo?: () => void;
|
||||
onDuplicate: (provider: Provider) => void;
|
||||
onConfigureUsage?: (provider: Provider) => void;
|
||||
onOpenWebsite: (url: string) => void;
|
||||
@@ -61,6 +61,7 @@ export function ProviderList({
|
||||
onEdit,
|
||||
onDelete,
|
||||
onRemoveFromConfig,
|
||||
onDisableOmo,
|
||||
onDuplicate,
|
||||
onConfigureUsage,
|
||||
onOpenWebsite,
|
||||
@@ -77,14 +78,12 @@ export function ProviderList({
|
||||
appId,
|
||||
);
|
||||
|
||||
// OpenCode: 查询 live 配置中的供应商 ID 列表,用于判断 isInConfig
|
||||
const { data: opencodeLiveIds } = useQuery({
|
||||
queryKey: ["opencodeLiveProviderIds"],
|
||||
queryFn: () => providersApi.getOpenCodeLiveProviderIds(),
|
||||
enabled: appId === "opencode",
|
||||
});
|
||||
|
||||
// OpenCode: 判断供应商是否已添加到 opencode.json
|
||||
const isProviderInConfig = useCallback(
|
||||
(providerId: string): boolean => {
|
||||
if (appId !== "opencode") return true; // 非 OpenCode 应用始终返回 true
|
||||
@@ -93,20 +92,18 @@ export function ProviderList({
|
||||
[appId, opencodeLiveIds],
|
||||
);
|
||||
|
||||
// 流式健康检查 - 功能已隐藏
|
||||
// const { checkProvider, isChecking } = useStreamCheck(appId);
|
||||
|
||||
// 故障转移相关
|
||||
const { data: isAutoFailoverEnabled } = useAutoFailoverEnabled(appId);
|
||||
const { data: failoverQueue } = useFailoverQueue(appId);
|
||||
const addToQueue = useAddToFailoverQueue();
|
||||
const removeFromQueue = useRemoveFromFailoverQueue();
|
||||
|
||||
// 联动状态:只有当前应用开启代理接管且故障转移开启时才启用故障转移模式
|
||||
const isFailoverModeActive =
|
||||
isProxyTakeover === true && isAutoFailoverEnabled === true;
|
||||
|
||||
// 计算供应商在故障转移队列中的优先级(基于 sortIndex 排序)
|
||||
const isOpenCode = appId === "opencode";
|
||||
const { data: currentOmoId } = useCurrentOmoProviderId(isOpenCode);
|
||||
const { data: omoProviderCount } = useOmoProviderCount(isOpenCode);
|
||||
|
||||
const getFailoverPriority = useCallback(
|
||||
(providerId: string): number | undefined => {
|
||||
if (!isFailoverModeActive || !failoverQueue) return undefined;
|
||||
@@ -118,7 +115,6 @@ export function ProviderList({
|
||||
[isFailoverModeActive, failoverQueue],
|
||||
);
|
||||
|
||||
// 判断供应商是否在故障转移队列中
|
||||
const isInFailoverQueue = useCallback(
|
||||
(providerId: string): boolean => {
|
||||
if (!isFailoverModeActive || !failoverQueue) return false;
|
||||
@@ -127,7 +123,6 @@ export function ProviderList({
|
||||
[isFailoverModeActive, failoverQueue],
|
||||
);
|
||||
|
||||
// 切换供应商的故障转移队列状态
|
||||
const handleToggleFailover = useCallback(
|
||||
(providerId: string, enabled: boolean) => {
|
||||
if (enabled) {
|
||||
@@ -139,11 +134,6 @@ export function ProviderList({
|
||||
[appId, addToQueue, removeFromQueue],
|
||||
);
|
||||
|
||||
// handleTest 功能已隐藏 - 供应商请求格式复杂难以统一测试
|
||||
// const handleTest = (provider: Provider) => {
|
||||
// checkProvider(provider.id, provider.name);
|
||||
// };
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
@@ -215,36 +205,44 @@ export function ProviderList({
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
{filteredProviders.map((provider) => (
|
||||
<SortableProviderCard
|
||||
key={provider.id}
|
||||
provider={provider}
|
||||
isCurrent={provider.id === currentProviderId}
|
||||
appId={appId}
|
||||
isInConfig={isProviderInConfig(provider.id)}
|
||||
onSwitch={onSwitch}
|
||||
onEdit={onEdit}
|
||||
onDelete={onDelete}
|
||||
onRemoveFromConfig={onRemoveFromConfig}
|
||||
onDuplicate={onDuplicate}
|
||||
onConfigureUsage={onConfigureUsage}
|
||||
onOpenWebsite={onOpenWebsite}
|
||||
onOpenTerminal={onOpenTerminal}
|
||||
// onTest 功能已隐藏 - 供应商请求格式复杂难以统一测试
|
||||
// onTest={appId !== "opencode" ? handleTest : undefined}
|
||||
isTesting={false} // isChecking(provider.id) - 测试功能已隐藏
|
||||
isProxyRunning={isProxyRunning}
|
||||
isProxyTakeover={isProxyTakeover}
|
||||
// 故障转移相关:联动状态
|
||||
isAutoFailoverEnabled={isFailoverModeActive}
|
||||
failoverPriority={getFailoverPriority(provider.id)}
|
||||
isInFailoverQueue={isInFailoverQueue(provider.id)}
|
||||
onToggleFailover={(enabled) =>
|
||||
handleToggleFailover(provider.id, enabled)
|
||||
}
|
||||
activeProviderId={activeProviderId}
|
||||
/>
|
||||
))}
|
||||
{filteredProviders.map((provider) => {
|
||||
const isOmo = provider.category === "omo";
|
||||
const isOmoCurrent = isOmo && provider.id === (currentOmoId || "");
|
||||
return (
|
||||
<SortableProviderCard
|
||||
key={provider.id}
|
||||
provider={provider}
|
||||
isCurrent={
|
||||
isOmo ? isOmoCurrent : provider.id === currentProviderId
|
||||
}
|
||||
appId={appId}
|
||||
isInConfig={isProviderInConfig(provider.id)}
|
||||
isOmo={isOmo}
|
||||
isLastOmo={
|
||||
isOmo && (omoProviderCount ?? 0) <= 1 && isOmoCurrent
|
||||
}
|
||||
onSwitch={onSwitch}
|
||||
onEdit={onEdit}
|
||||
onDelete={onDelete}
|
||||
onRemoveFromConfig={onRemoveFromConfig}
|
||||
onDisableOmo={onDisableOmo}
|
||||
onDuplicate={onDuplicate}
|
||||
onConfigureUsage={onConfigureUsage}
|
||||
onOpenWebsite={onOpenWebsite}
|
||||
onOpenTerminal={onOpenTerminal}
|
||||
isTesting={false} // isChecking(provider.id) - 测试功能已隐藏
|
||||
isProxyRunning={isProxyRunning}
|
||||
isProxyTakeover={isProxyTakeover}
|
||||
isAutoFailoverEnabled={isFailoverModeActive}
|
||||
failoverPriority={getFailoverPriority(provider.id)}
|
||||
isInFailoverQueue={isInFailoverQueue(provider.id)}
|
||||
onToggleFailover={(enabled) =>
|
||||
handleToggleFailover(provider.id, enabled)
|
||||
}
|
||||
activeProviderId={activeProviderId}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
@@ -334,11 +332,13 @@ interface SortableProviderCardProps {
|
||||
isCurrent: boolean;
|
||||
appId: AppId;
|
||||
isInConfig: boolean;
|
||||
isOmo: boolean;
|
||||
isLastOmo: boolean;
|
||||
onSwitch: (provider: Provider) => void;
|
||||
onEdit: (provider: Provider) => void;
|
||||
onDelete: (provider: Provider) => void;
|
||||
/** OpenCode: remove from live config (not delete from database) */
|
||||
onRemoveFromConfig?: (provider: Provider) => void;
|
||||
onDisableOmo?: () => void;
|
||||
onDuplicate: (provider: Provider) => void;
|
||||
onConfigureUsage?: (provider: Provider) => void;
|
||||
onOpenWebsite: (url: string) => void;
|
||||
@@ -347,7 +347,6 @@ interface SortableProviderCardProps {
|
||||
isTesting: boolean;
|
||||
isProxyRunning: boolean;
|
||||
isProxyTakeover: boolean;
|
||||
// 故障转移相关
|
||||
isAutoFailoverEnabled: boolean;
|
||||
failoverPriority?: number;
|
||||
isInFailoverQueue: boolean;
|
||||
@@ -360,10 +359,13 @@ function SortableProviderCard({
|
||||
isCurrent,
|
||||
appId,
|
||||
isInConfig,
|
||||
isOmo,
|
||||
isLastOmo,
|
||||
onSwitch,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onRemoveFromConfig,
|
||||
onDisableOmo,
|
||||
onDuplicate,
|
||||
onConfigureUsage,
|
||||
onOpenWebsite,
|
||||
@@ -399,10 +401,13 @@ function SortableProviderCard({
|
||||
isCurrent={isCurrent}
|
||||
appId={appId}
|
||||
isInConfig={isInConfig}
|
||||
isOmo={isOmo}
|
||||
isLastOmo={isLastOmo}
|
||||
onSwitch={onSwitch}
|
||||
onEdit={onEdit}
|
||||
onDelete={onDelete}
|
||||
onRemoveFromConfig={onRemoveFromConfig}
|
||||
onDisableOmo={onDisableOmo}
|
||||
onDuplicate={onDuplicate}
|
||||
onConfigureUsage={
|
||||
onConfigureUsage ? (item) => onConfigureUsage(item) : () => undefined
|
||||
@@ -418,7 +423,6 @@ function SortableProviderCard({
|
||||
listeners,
|
||||
isDragging,
|
||||
}}
|
||||
// 故障转移相关
|
||||
isAutoFailoverEnabled={isAutoFailoverEnabled}
|
||||
failoverPriority={failoverPriority}
|
||||
isInFailoverQueue={isInFailoverQueue}
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useEffect, useState } from "react";
|
||||
import { FullScreenPanel } from "@/components/common/FullScreenPanel";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Save, FolderInput, Loader2 } from "lucide-react";
|
||||
import JsonEditor from "@/components/JsonEditor";
|
||||
import {
|
||||
OmoGlobalConfigFields,
|
||||
type OmoGlobalConfigFieldsRef,
|
||||
} from "./OmoGlobalConfigFields";
|
||||
import type { OmoGlobalConfig } from "@/types/omo";
|
||||
|
||||
interface OmoCommonConfigEditorProps {
|
||||
previewValue: string;
|
||||
useCommonConfig: boolean;
|
||||
onCommonConfigToggle: (checked: boolean) => void;
|
||||
isModalOpen: boolean;
|
||||
onEditClick: () => void;
|
||||
onModalClose: () => void;
|
||||
onSave: () => Promise<void>;
|
||||
isSaving: boolean;
|
||||
onGlobalConfigStateChange: (config: OmoGlobalConfig) => void;
|
||||
globalConfigRef: React.RefObject<OmoGlobalConfigFieldsRef | null>;
|
||||
fieldsKey: number;
|
||||
}
|
||||
|
||||
export function OmoCommonConfigEditor({
|
||||
previewValue,
|
||||
useCommonConfig,
|
||||
onCommonConfigToggle,
|
||||
isModalOpen,
|
||||
onEditClick,
|
||||
onModalClose,
|
||||
onSave,
|
||||
isSaving,
|
||||
onGlobalConfigStateChange,
|
||||
globalConfigRef,
|
||||
fieldsKey,
|
||||
}: OmoCommonConfigEditorProps) {
|
||||
const { t } = useTranslation();
|
||||
const [isDarkMode, setIsDarkMode] = useState(false);
|
||||
const [isImporting, setIsImporting] = useState(false);
|
||||
useEffect(() => {
|
||||
const syncDarkMode = () =>
|
||||
setIsDarkMode(document.documentElement.classList.contains("dark"));
|
||||
syncDarkMode();
|
||||
const observer = new MutationObserver(syncDarkMode);
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ["class"],
|
||||
});
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
const handleImportLocal = async () => {
|
||||
if (!globalConfigRef.current) return;
|
||||
setIsImporting(true);
|
||||
try {
|
||||
await globalConfigRef.current.importFromLocal();
|
||||
} finally {
|
||||
setIsImporting(false);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>{t("provider.configJson")}</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="inline-flex items-center gap-2 text-sm text-muted-foreground cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={useCommonConfig}
|
||||
onChange={(e) => onCommonConfigToggle(e.target.checked)}
|
||||
className="w-4 h-4 text-blue-500 bg-white dark:bg-gray-800 border-border-default rounded focus:ring-blue-500 dark:focus:ring-blue-400 focus:ring-2"
|
||||
/>
|
||||
<span>
|
||||
{t("omo.writeCommonConfig", {
|
||||
defaultValue: "Write to common config",
|
||||
})}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-end">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onEditClick}
|
||||
className="text-xs text-blue-400 dark:text-blue-500 hover:text-blue-500 dark:hover:text-blue-400 transition-colors"
|
||||
>
|
||||
{t("omo.editCommonConfig", { defaultValue: "Edit common config" })}
|
||||
</button>
|
||||
</div>
|
||||
<JsonEditor
|
||||
value={previewValue}
|
||||
onChange={() => {}}
|
||||
darkMode={isDarkMode}
|
||||
rows={14}
|
||||
showValidation={false}
|
||||
language="json"
|
||||
/>
|
||||
</div>
|
||||
<FullScreenPanel
|
||||
isOpen={isModalOpen}
|
||||
title={t("omo.editCommonConfigTitle", {
|
||||
defaultValue: "Edit OMO Common Config",
|
||||
})}
|
||||
onClose={onModalClose}
|
||||
footer={
|
||||
<>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleImportLocal}
|
||||
disabled={isImporting}
|
||||
className="gap-2"
|
||||
>
|
||||
{isImporting ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<FolderInput className="w-4 h-4" />
|
||||
)}
|
||||
{t("common.import", { defaultValue: "Import" })}
|
||||
</Button>
|
||||
<Button type="button" variant="outline" onClick={onModalClose}>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onSave}
|
||||
disabled={isSaving}
|
||||
className="gap-2"
|
||||
>
|
||||
{isSaving ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<Save className="w-4 h-4" />
|
||||
)}
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("omo.commonConfigHint", {
|
||||
defaultValue:
|
||||
"OMO common config will be merged into all OMO configs that enable it",
|
||||
})}
|
||||
</p>
|
||||
<OmoGlobalConfigFields
|
||||
key={fieldsKey}
|
||||
ref={globalConfigRef as React.Ref<OmoGlobalConfigFieldsRef>}
|
||||
onStateChange={onGlobalConfigStateChange}
|
||||
hideSaveButtons
|
||||
/>
|
||||
</div>
|
||||
</FullScreenPanel>
|
||||
</>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,739 @@
|
||||
import {
|
||||
useState,
|
||||
useEffect,
|
||||
useCallback,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
} from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
Save,
|
||||
Loader2,
|
||||
X,
|
||||
FolderInput,
|
||||
RotateCcw,
|
||||
ChevronsUpDown,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { toast } from "sonner";
|
||||
import type { OmoGlobalConfig } from "@/types/omo";
|
||||
import {
|
||||
OMO_DISABLEABLE_AGENTS,
|
||||
OMO_DISABLEABLE_MCPS,
|
||||
OMO_DISABLEABLE_HOOKS,
|
||||
OMO_DISABLEABLE_SKILLS,
|
||||
OMO_DEFAULT_SCHEMA_URL,
|
||||
OMO_SISYPHUS_AGENT_PLACEHOLDER,
|
||||
OMO_LSP_PLACEHOLDER,
|
||||
OMO_EXPERIMENTAL_PLACEHOLDER,
|
||||
OMO_BACKGROUND_TASK_PLACEHOLDER,
|
||||
OMO_BROWSER_AUTOMATION_PLACEHOLDER,
|
||||
OMO_CLAUDE_CODE_PLACEHOLDER,
|
||||
} from "@/types/omo";
|
||||
import {
|
||||
useOmoGlobalConfig,
|
||||
useSaveOmoGlobalConfig,
|
||||
useReadOmoLocalFile,
|
||||
} from "@/lib/query/omo";
|
||||
|
||||
interface PresetOption {
|
||||
readonly value: string;
|
||||
readonly label: string;
|
||||
}
|
||||
|
||||
export interface OmoGlobalConfigFieldsRef {
|
||||
buildCurrentConfig: () => OmoGlobalConfig;
|
||||
buildCurrentConfigStrict: () => OmoGlobalConfig;
|
||||
importFromLocal: () => Promise<void>;
|
||||
}
|
||||
|
||||
interface OmoGlobalConfigFieldsProps {
|
||||
onStateChange?: (config: OmoGlobalConfig) => void;
|
||||
hideSaveButtons?: boolean;
|
||||
}
|
||||
|
||||
type OmoAdvancedFieldKey =
|
||||
| "lspStr"
|
||||
| "experimentalStr"
|
||||
| "backgroundTaskStr"
|
||||
| "browserStr"
|
||||
| "claudeCodeStr";
|
||||
|
||||
const OMO_ADVANCED_JSON_FIELDS: ReadonlyArray<{
|
||||
key: OmoAdvancedFieldKey;
|
||||
labelKey: string;
|
||||
defaultLabel: string;
|
||||
placeholder: string;
|
||||
minHeight: string;
|
||||
}> = [
|
||||
{
|
||||
key: "lspStr",
|
||||
labelKey: "omo.advancedLsp",
|
||||
defaultLabel: "LSP Config",
|
||||
placeholder: OMO_LSP_PLACEHOLDER,
|
||||
minHeight: "200px",
|
||||
},
|
||||
{
|
||||
key: "experimentalStr",
|
||||
labelKey: "omo.advancedExperimental",
|
||||
defaultLabel: "Experimental Features",
|
||||
placeholder: OMO_EXPERIMENTAL_PLACEHOLDER,
|
||||
minHeight: "120px",
|
||||
},
|
||||
{
|
||||
key: "backgroundTaskStr",
|
||||
labelKey: "omo.advancedBackgroundTask",
|
||||
defaultLabel: "Background Tasks",
|
||||
placeholder: OMO_BACKGROUND_TASK_PLACEHOLDER,
|
||||
minHeight: "250px",
|
||||
},
|
||||
{
|
||||
key: "browserStr",
|
||||
labelKey: "omo.advancedBrowserAutomation",
|
||||
defaultLabel: "Browser Automation",
|
||||
placeholder: OMO_BROWSER_AUTOMATION_PLACEHOLDER,
|
||||
minHeight: "80px",
|
||||
},
|
||||
{
|
||||
key: "claudeCodeStr",
|
||||
labelKey: "omo.advancedClaudeCode",
|
||||
defaultLabel: "Claude Code",
|
||||
placeholder: OMO_CLAUDE_CODE_PLACEHOLDER,
|
||||
minHeight: "180px",
|
||||
},
|
||||
];
|
||||
|
||||
function TagListEditor({
|
||||
label,
|
||||
values,
|
||||
onChange,
|
||||
placeholder,
|
||||
presets,
|
||||
}: {
|
||||
label: string;
|
||||
values: string[];
|
||||
onChange: (values: string[]) => void;
|
||||
placeholder?: string;
|
||||
presets?: readonly PresetOption[];
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const toggleValue = (v: string) => {
|
||||
if (values.includes(v)) {
|
||||
onChange(values.filter((x) => x !== v));
|
||||
} else {
|
||||
onChange([...values, v]);
|
||||
}
|
||||
};
|
||||
const customValue = search.trim();
|
||||
const canAddCustom = customValue.length > 0 && !values.includes(customValue);
|
||||
const triggerText =
|
||||
values.length === 0
|
||||
? placeholder || t("omo.selectPlaceholder", { defaultValue: "Select..." })
|
||||
: values.length === 1
|
||||
? values[0]
|
||||
: `${values[0]} +${values.length - 1}`;
|
||||
|
||||
const availablePresets = presets?.filter(
|
||||
(p) =>
|
||||
!search.trim() ||
|
||||
p.label.toLowerCase().includes(search.toLowerCase()) ||
|
||||
p.value.toLowerCase().includes(search.toLowerCase()),
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-sm">{label}</Label>
|
||||
{values.length > 0 && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 px-1.5 text-xs text-muted-foreground"
|
||||
onClick={() => onChange([])}
|
||||
>
|
||||
{t("omo.clear", { defaultValue: "Clear" })}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{values.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{values.map((v, i) => (
|
||||
<Badge
|
||||
key={`${v}-${i}`}
|
||||
variant="secondary"
|
||||
className="text-xs gap-1"
|
||||
>
|
||||
{v}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onChange(values.filter((_, idx) => idx !== i))}
|
||||
className="hover:text-destructive"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<DropdownMenu open={open} onOpenChange={setOpen} modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"flex items-center justify-between w-full h-8 px-3 rounded-md border border-input bg-background text-sm",
|
||||
"hover:bg-accent hover:text-accent-foreground transition-colors",
|
||||
open && "ring-2 ring-ring",
|
||||
)}
|
||||
aria-expanded={open}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"truncate",
|
||||
values.length > 0 ? "text-foreground" : "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{triggerText}
|
||||
</span>
|
||||
<ChevronsUpDown className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="start"
|
||||
sideOffset={6}
|
||||
className="w-[var(--radix-dropdown-menu-trigger-width)] p-0 z-[120]"
|
||||
>
|
||||
<div className="p-1.5 border-b border-border/30">
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
e.stopPropagation();
|
||||
if (e.key === "Enter" && canAddCustom) {
|
||||
e.preventDefault();
|
||||
onChange([...values, customValue]);
|
||||
setSearch("");
|
||||
}
|
||||
}}
|
||||
placeholder={
|
||||
placeholder ||
|
||||
t("omo.searchOrType", {
|
||||
defaultValue: "Search or type custom value...",
|
||||
})
|
||||
}
|
||||
className="h-7 text-sm"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
{canAddCustom && (
|
||||
<button
|
||||
type="button"
|
||||
className="w-full px-2.5 py-1.5 text-left text-sm border-b border-border/30 hover:bg-accent"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => {
|
||||
onChange([...values, customValue]);
|
||||
setSearch("");
|
||||
}}
|
||||
>
|
||||
+ {customValue}
|
||||
</button>
|
||||
)}
|
||||
<div className="max-h-48 overflow-auto py-1">
|
||||
{availablePresets && availablePresets.length > 0 ? (
|
||||
availablePresets.map((p) => {
|
||||
const checked = values.includes(p.value);
|
||||
return (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={p.value}
|
||||
checked={checked}
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
onCheckedChange={() => toggleValue(p.value)}
|
||||
className="text-sm"
|
||||
>
|
||||
{p.label}
|
||||
</DropdownMenuCheckboxItem>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="px-2.5 py-2 text-sm text-muted-foreground">
|
||||
{t("omo.noMatches", { defaultValue: "No matches" })}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function JsonTextareaField({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
minHeight,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
placeholder?: string;
|
||||
minHeight?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-sm">{label}</Label>
|
||||
<Textarea
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder={placeholder || "{}"}
|
||||
className="font-mono text-sm"
|
||||
style={{ minHeight: minHeight || "100px" }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const OmoGlobalConfigFields = forwardRef<
|
||||
OmoGlobalConfigFieldsRef,
|
||||
OmoGlobalConfigFieldsProps
|
||||
>(function OmoGlobalConfigFields({ onStateChange, hideSaveButtons }, ref) {
|
||||
const { t } = useTranslation();
|
||||
const { data: config } = useOmoGlobalConfig();
|
||||
const saveMutation = useSaveOmoGlobalConfig();
|
||||
|
||||
const [schemaUrl, setSchemaUrl] = useState(OMO_DEFAULT_SCHEMA_URL);
|
||||
const [sisyphusAgentStr, setSisyphusAgentStr] = useState("");
|
||||
const [disabledAgents, setDisabledAgents] = useState<string[]>([]);
|
||||
const [disabledMcps, setDisabledMcps] = useState<string[]>([]);
|
||||
const [disabledHooks, setDisabledHooks] = useState<string[]>([]);
|
||||
const [disabledSkills, setDisabledSkills] = useState<string[]>([]);
|
||||
const [lspStr, setLspStr] = useState("");
|
||||
const [experimentalStr, setExperimentalStr] = useState("");
|
||||
const [backgroundTaskStr, setBackgroundTaskStr] = useState("");
|
||||
const [browserStr, setBrowserStr] = useState("");
|
||||
const [claudeCodeStr, setClaudeCodeStr] = useState("");
|
||||
const [otherFieldsStr, setOtherFieldsStr] = useState("");
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
|
||||
const applyGlobalState = useCallback((global: OmoGlobalConfig) => {
|
||||
setSchemaUrl(global.schemaUrl || OMO_DEFAULT_SCHEMA_URL);
|
||||
setSisyphusAgentStr(
|
||||
global.sisyphusAgent ? JSON.stringify(global.sisyphusAgent, null, 2) : "",
|
||||
);
|
||||
setDisabledAgents(global.disabledAgents || []);
|
||||
setDisabledMcps(global.disabledMcps || []);
|
||||
setDisabledHooks(global.disabledHooks || []);
|
||||
setDisabledSkills(global.disabledSkills || []);
|
||||
setLspStr(global.lsp ? JSON.stringify(global.lsp, null, 2) : "");
|
||||
setExperimentalStr(
|
||||
global.experimental ? JSON.stringify(global.experimental, null, 2) : "",
|
||||
);
|
||||
setBackgroundTaskStr(
|
||||
global.backgroundTask
|
||||
? JSON.stringify(global.backgroundTask, null, 2)
|
||||
: "",
|
||||
);
|
||||
setBrowserStr(
|
||||
global.browserAutomationEngine
|
||||
? JSON.stringify(global.browserAutomationEngine, null, 2)
|
||||
: "",
|
||||
);
|
||||
setClaudeCodeStr(
|
||||
global.claudeCode ? JSON.stringify(global.claudeCode, null, 2) : "",
|
||||
);
|
||||
setOtherFieldsStr(
|
||||
global.otherFields ? JSON.stringify(global.otherFields, null, 2) : "",
|
||||
);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (config && !loaded) {
|
||||
applyGlobalState(config);
|
||||
setLoaded(true);
|
||||
}
|
||||
}, [config, loaded, applyGlobalState]);
|
||||
|
||||
const parseJsonField = useCallback(
|
||||
(
|
||||
fieldName: string,
|
||||
raw: string,
|
||||
strict: boolean,
|
||||
): Record<string, unknown> | undefined => {
|
||||
if (!raw.trim()) return undefined;
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(raw);
|
||||
if (
|
||||
typeof parsed !== "object" ||
|
||||
parsed === null ||
|
||||
Array.isArray(parsed)
|
||||
) {
|
||||
if (strict) {
|
||||
throw new Error(
|
||||
t("omo.jsonMustBeObject", {
|
||||
field: fieldName,
|
||||
defaultValue: "{{field}} must be a JSON object",
|
||||
}),
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
return parsed as Record<string, unknown>;
|
||||
} catch (error) {
|
||||
if (strict) {
|
||||
if (error instanceof Error) {
|
||||
throw error;
|
||||
}
|
||||
throw new Error(
|
||||
t("omo.jsonInvalid", {
|
||||
field: fieldName,
|
||||
defaultValue: "{{field}} contains invalid JSON",
|
||||
}),
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
[t],
|
||||
);
|
||||
|
||||
const buildCurrentConfigInternal = useCallback(
|
||||
(strict: boolean): OmoGlobalConfig => {
|
||||
return {
|
||||
id: "global",
|
||||
schemaUrl: schemaUrl || undefined,
|
||||
sisyphusAgent: parseJsonField(
|
||||
t("omo.sisyphusAgentConfig", {
|
||||
defaultValue: "Sisyphus Agent",
|
||||
}),
|
||||
sisyphusAgentStr,
|
||||
strict,
|
||||
),
|
||||
disabledAgents,
|
||||
disabledMcps,
|
||||
disabledHooks,
|
||||
disabledSkills,
|
||||
lsp: parseJsonField(
|
||||
t("omo.advancedLsp", { defaultValue: "LSP" }),
|
||||
lspStr,
|
||||
strict,
|
||||
),
|
||||
experimental: parseJsonField(
|
||||
t("omo.advancedExperimental", { defaultValue: "Experimental" }),
|
||||
experimentalStr,
|
||||
strict,
|
||||
),
|
||||
backgroundTask: parseJsonField(
|
||||
t("omo.advancedBackgroundTask", {
|
||||
defaultValue: "Background Task",
|
||||
}),
|
||||
backgroundTaskStr,
|
||||
strict,
|
||||
),
|
||||
browserAutomationEngine: parseJsonField(
|
||||
t("omo.advancedBrowserAutomation", {
|
||||
defaultValue: "Browser Automation",
|
||||
}),
|
||||
browserStr,
|
||||
strict,
|
||||
),
|
||||
claudeCode: parseJsonField(
|
||||
t("omo.advancedClaudeCode", { defaultValue: "Claude Code" }),
|
||||
claudeCodeStr,
|
||||
strict,
|
||||
),
|
||||
otherFields: parseJsonField(
|
||||
t("omo.otherFields", {
|
||||
defaultValue: "Other Config",
|
||||
}),
|
||||
otherFieldsStr,
|
||||
strict,
|
||||
),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
},
|
||||
[
|
||||
schemaUrl,
|
||||
sisyphusAgentStr,
|
||||
disabledAgents,
|
||||
disabledMcps,
|
||||
disabledHooks,
|
||||
disabledSkills,
|
||||
lspStr,
|
||||
experimentalStr,
|
||||
backgroundTaskStr,
|
||||
browserStr,
|
||||
claudeCodeStr,
|
||||
otherFieldsStr,
|
||||
parseJsonField,
|
||||
],
|
||||
);
|
||||
|
||||
const buildCurrentConfig = useCallback(
|
||||
() => buildCurrentConfigInternal(false),
|
||||
[buildCurrentConfigInternal],
|
||||
);
|
||||
|
||||
const buildCurrentConfigStrict = useCallback(
|
||||
() => buildCurrentConfigInternal(true),
|
||||
[buildCurrentConfigInternal],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (loaded && onStateChange) {
|
||||
onStateChange(buildCurrentConfig());
|
||||
}
|
||||
}, [loaded, onStateChange, buildCurrentConfig]);
|
||||
|
||||
const handleSaveGlobal = useCallback(async () => {
|
||||
try {
|
||||
const result = buildCurrentConfigStrict();
|
||||
await saveMutation.mutateAsync(result);
|
||||
toast.success(
|
||||
t("omo.globalConfigSaved", {
|
||||
defaultValue: "Global config saved",
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
toast.error(String(err));
|
||||
}
|
||||
}, [buildCurrentConfigStrict, saveMutation, t]);
|
||||
|
||||
const disabledCount =
|
||||
disabledAgents.length +
|
||||
disabledMcps.length +
|
||||
disabledHooks.length +
|
||||
disabledSkills.length;
|
||||
const advancedFieldValues: Record<OmoAdvancedFieldKey, string> = {
|
||||
lspStr,
|
||||
experimentalStr,
|
||||
backgroundTaskStr,
|
||||
browserStr,
|
||||
claudeCodeStr,
|
||||
};
|
||||
|
||||
const advancedFieldSetters: Record<
|
||||
OmoAdvancedFieldKey,
|
||||
(value: string) => void
|
||||
> = {
|
||||
lspStr: setLspStr,
|
||||
experimentalStr: setExperimentalStr,
|
||||
backgroundTaskStr: setBackgroundTaskStr,
|
||||
browserStr: setBrowserStr,
|
||||
claudeCodeStr: setClaudeCodeStr,
|
||||
};
|
||||
|
||||
const disabledEditorConfigs = [
|
||||
{
|
||||
key: "agents",
|
||||
label: t("omo.disabledAgents", { defaultValue: "Agents" }),
|
||||
values: disabledAgents,
|
||||
onChange: setDisabledAgents,
|
||||
placeholder: t("omo.disabledAgentsPlaceholder", {
|
||||
defaultValue: "Disabled Agents",
|
||||
}),
|
||||
presets: OMO_DISABLEABLE_AGENTS,
|
||||
},
|
||||
{
|
||||
key: "mcps",
|
||||
label: t("omo.disabledMcps", { defaultValue: "MCPs" }),
|
||||
values: disabledMcps,
|
||||
onChange: setDisabledMcps,
|
||||
placeholder: t("omo.disabledMcpsPlaceholder", {
|
||||
defaultValue: "Disabled MCPs",
|
||||
}),
|
||||
presets: OMO_DISABLEABLE_MCPS,
|
||||
},
|
||||
{
|
||||
key: "hooks",
|
||||
label: t("omo.disabledHooks", { defaultValue: "Hooks" }),
|
||||
values: disabledHooks,
|
||||
onChange: setDisabledHooks,
|
||||
placeholder: t("omo.disabledHooksPlaceholder", {
|
||||
defaultValue: "Disabled Hooks",
|
||||
}),
|
||||
presets: OMO_DISABLEABLE_HOOKS,
|
||||
},
|
||||
{
|
||||
key: "skills",
|
||||
label: t("omo.disabledSkills", { defaultValue: "Skills" }),
|
||||
values: disabledSkills,
|
||||
onChange: setDisabledSkills,
|
||||
placeholder: t("omo.disabledSkillsPlaceholder", {
|
||||
defaultValue: "Disabled Skills",
|
||||
}),
|
||||
presets: OMO_DISABLEABLE_SKILLS,
|
||||
},
|
||||
] as const;
|
||||
|
||||
const readLocalFile = useReadOmoLocalFile();
|
||||
|
||||
const handleImportGlobalFromLocal = useCallback(async () => {
|
||||
try {
|
||||
const data = await readLocalFile.mutateAsync();
|
||||
applyGlobalState(data.global);
|
||||
toast.success(
|
||||
t("omo.importGlobalSuccess", {
|
||||
defaultValue: "Imported global config from local file (unsaved)",
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
toast.error(
|
||||
t("omo.importGlobalFailed", {
|
||||
error: String(err),
|
||||
defaultValue: "Failed to read local file: {{error}}",
|
||||
}),
|
||||
);
|
||||
}
|
||||
}, [readLocalFile, applyGlobalState, t]);
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
buildCurrentConfig,
|
||||
buildCurrentConfigStrict,
|
||||
importFromLocal: handleImportGlobalFromLocal,
|
||||
}),
|
||||
[buildCurrentConfig, buildCurrentConfigStrict, handleImportGlobalFromLocal],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{!hideSaveButtons && (
|
||||
<div className="flex items-center justify-end gap-1.5">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 text-xs"
|
||||
disabled={readLocalFile.isPending}
|
||||
onClick={handleImportGlobalFromLocal}
|
||||
>
|
||||
{readLocalFile.isPending ? (
|
||||
<Loader2 className="h-3.5 w-3.5 mr-1 animate-spin" />
|
||||
) : (
|
||||
<FolderInput className="h-3.5 w-3.5 mr-1" />
|
||||
)}
|
||||
{t("omo.importLocal", { defaultValue: "Import Local" })}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-7 text-xs"
|
||||
disabled={saveMutation.isPending}
|
||||
onClick={handleSaveGlobal}
|
||||
>
|
||||
{saveMutation.isPending ? (
|
||||
<Loader2 className="h-3.5 w-3.5 mr-1 animate-spin" />
|
||||
) : (
|
||||
<Save className="h-3.5 w-3.5 mr-1" />
|
||||
)}
|
||||
{t("omo.saveGlobalConfig", { defaultValue: "Save Global Config" })}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-sm">
|
||||
{t("omo.schemaUrl", { defaultValue: "$schema" })}
|
||||
</Label>
|
||||
{schemaUrl !== OMO_DEFAULT_SCHEMA_URL && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 text-xs px-1.5"
|
||||
onClick={() => setSchemaUrl(OMO_DEFAULT_SCHEMA_URL)}
|
||||
>
|
||||
<RotateCcw className="h-3 w-3 mr-0.5" />
|
||||
{t("omo.resetDefault", { defaultValue: "Reset" })}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<Input
|
||||
value={schemaUrl}
|
||||
onChange={(e) => setSchemaUrl(e.target.value)}
|
||||
placeholder={OMO_DEFAULT_SCHEMA_URL}
|
||||
className="text-sm h-8"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border border-border/40 bg-muted/10 p-2 space-y-2">
|
||||
<Label className="text-sm font-semibold">
|
||||
{t("omo.sisyphusAgentConfig", {
|
||||
defaultValue: "Sisyphus Agent",
|
||||
})}
|
||||
</Label>
|
||||
<Textarea
|
||||
value={sisyphusAgentStr}
|
||||
onChange={(e) => setSisyphusAgentStr(e.target.value)}
|
||||
placeholder={OMO_SISYPHUS_AGENT_PLACEHOLDER}
|
||||
className="font-mono text-sm"
|
||||
style={{ minHeight: "140px" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border border-border/40 bg-muted/10 p-2 space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-sm font-semibold">
|
||||
{t("omo.disabledItems", { defaultValue: "Disabled Items" })}
|
||||
</Label>
|
||||
{disabledCount > 0 && (
|
||||
<Badge variant="secondary" className="text-xs h-5">
|
||||
{disabledCount}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{disabledEditorConfigs.map((editor) => (
|
||||
<TagListEditor
|
||||
key={editor.key}
|
||||
label={editor.label}
|
||||
values={editor.values}
|
||||
onChange={editor.onChange}
|
||||
placeholder={editor.placeholder}
|
||||
presets={editor.presets}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border border-border/40 bg-muted/10 p-2 space-y-2">
|
||||
<Label className="text-sm font-semibold">
|
||||
{t("omo.advanced", { defaultValue: "Advanced Settings" })}
|
||||
</Label>
|
||||
{OMO_ADVANCED_JSON_FIELDS.map((field) => (
|
||||
<JsonTextareaField
|
||||
key={field.key}
|
||||
label={t(field.labelKey, { defaultValue: field.defaultLabel })}
|
||||
value={advancedFieldValues[field.key]}
|
||||
onChange={advancedFieldSetters[field.key]}
|
||||
placeholder={field.placeholder}
|
||||
minHeight={field.minHeight}
|
||||
/>
|
||||
))}
|
||||
|
||||
<JsonTextareaField
|
||||
label={t("omo.otherFields", {
|
||||
defaultValue: "Other Config",
|
||||
})}
|
||||
value={otherFieldsStr}
|
||||
onChange={setOtherFieldsStr}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -40,7 +40,6 @@ export function ProviderPresetSelector({
|
||||
}: ProviderPresetSelectorProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// 根据分类获取提示文字
|
||||
const getCategoryHint = (): React.ReactNode => {
|
||||
switch (category) {
|
||||
case "official":
|
||||
@@ -63,6 +62,11 @@ export function ProviderPresetSelector({
|
||||
return t("providerForm.customApiKeyHint", {
|
||||
defaultValue: "💡 自定义配置需手动填写所有必要字段",
|
||||
});
|
||||
case "omo":
|
||||
return t("providerForm.omoHint", {
|
||||
defaultValue:
|
||||
"💡 OMO 配置管理 Agent 模型分配,写入 oh-my-opencode.jsonc",
|
||||
});
|
||||
default:
|
||||
return t("providerPreset.hint", {
|
||||
defaultValue: "选择预设后可继续调整下方字段。",
|
||||
@@ -70,7 +74,6 @@ export function ProviderPresetSelector({
|
||||
}
|
||||
};
|
||||
|
||||
// 渲染预设按钮的图标
|
||||
const renderPresetIcon = (
|
||||
preset: ProviderPreset | CodexProviderPreset | GeminiProviderPreset,
|
||||
) => {
|
||||
@@ -91,7 +94,6 @@ export function ProviderPresetSelector({
|
||||
}
|
||||
};
|
||||
|
||||
// 获取预设按钮的样式类名
|
||||
const getPresetButtonClass = (
|
||||
isSelected: boolean,
|
||||
preset: ProviderPreset | CodexProviderPreset | GeminiProviderPreset,
|
||||
@@ -100,18 +102,15 @@ export function ProviderPresetSelector({
|
||||
"inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors";
|
||||
|
||||
if (isSelected) {
|
||||
// 如果有自定义主题,使用自定义颜色
|
||||
if (preset.theme?.backgroundColor) {
|
||||
return `${baseClass} text-white`;
|
||||
}
|
||||
// 默认使用主题蓝色
|
||||
return `${baseClass} bg-blue-500 text-white dark:bg-blue-600`;
|
||||
}
|
||||
|
||||
return `${baseClass} bg-accent text-muted-foreground hover:bg-accent/80`;
|
||||
};
|
||||
|
||||
// 获取预设按钮的内联样式(用于自定义背景色)
|
||||
const getPresetButtonStyle = (
|
||||
isSelected: boolean,
|
||||
preset: ProviderPreset | CodexProviderPreset | GeminiProviderPreset,
|
||||
@@ -130,7 +129,6 @@ export function ProviderPresetSelector({
|
||||
<div className="space-y-3">
|
||||
<FormLabel>{t("providerPreset.label")}</FormLabel>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{/* 自定义按钮 */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onPresetChange("custom")}
|
||||
@@ -143,7 +141,6 @@ export function ProviderPresetSelector({
|
||||
{t("providerPreset.custom")}
|
||||
</button>
|
||||
|
||||
{/* 预设按钮 */}
|
||||
{categoryKeys.map((category) => {
|
||||
const entries = groupedPresets[category];
|
||||
if (!entries || entries.length === 0) return null;
|
||||
@@ -174,7 +171,6 @@ export function ProviderPresetSelector({
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* 统一供应商预设(新的一行) */}
|
||||
{onUniversalPresetSelect && universalProviderPresets.length > 0 && (
|
||||
<>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
@@ -196,7 +192,6 @@ export function ProviderPresetSelector({
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
{/* 管理统一供应商按钮 */}
|
||||
{onManageUniversalProviders && (
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
/**
|
||||
* OpenCode 预设供应商配置模板
|
||||
* OpenCode 使用 AI SDK npm 包,配置结构与其他应用不同
|
||||
*/
|
||||
import type { ProviderCategory, OpenCodeProviderConfig } from "../types";
|
||||
import type { PresetTheme, TemplateValueConfig } from "./claudeProviderPresets";
|
||||
|
||||
@@ -9,27 +5,18 @@ export interface OpenCodeProviderPreset {
|
||||
name: string;
|
||||
websiteUrl: string;
|
||||
apiKeyUrl?: string;
|
||||
/** OpenCode settings_config 结构 */
|
||||
settingsConfig: OpenCodeProviderConfig;
|
||||
isOfficial?: boolean;
|
||||
isPartner?: boolean;
|
||||
partnerPromotionKey?: string;
|
||||
category?: ProviderCategory;
|
||||
/** 模板变量定义 */
|
||||
templateValues?: Record<string, TemplateValueConfig>;
|
||||
/** 视觉主题配置 */
|
||||
theme?: PresetTheme;
|
||||
/** 图标名称 */
|
||||
icon?: string;
|
||||
/** 图标颜色 */
|
||||
iconColor?: string;
|
||||
/** 标记为自定义模板(用于 UI 区分) */
|
||||
isCustomTemplate?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenCode npm 包选项(AI SDK 生态)
|
||||
*/
|
||||
export const opencodeNpmPackages = [
|
||||
{ value: "@ai-sdk/openai", label: "OpenAI" },
|
||||
{ value: "@ai-sdk/openai-compatible", label: "OpenAI Compatible" },
|
||||
@@ -37,11 +24,7 @@ export const opencodeNpmPackages = [
|
||||
{ value: "@ai-sdk/google", label: "Google (Gemini)" },
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* OpenCode 供应商预设列表
|
||||
*/
|
||||
export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
|
||||
// ========== 国产官方 ==========
|
||||
{
|
||||
name: "DeepSeek",
|
||||
websiteUrl: "https://platform.deepseek.com",
|
||||
@@ -474,7 +457,6 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
|
||||
},
|
||||
},
|
||||
|
||||
// ========== 聚合网站 ==========
|
||||
{
|
||||
name: "AiHubMix",
|
||||
websiteUrl: "https://aihubmix.com",
|
||||
@@ -583,7 +565,6 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
|
||||
},
|
||||
},
|
||||
|
||||
// ========== 第三方合作伙伴 ==========
|
||||
{
|
||||
name: "PackyCode",
|
||||
websiteUrl: "https://www.packyapi.com",
|
||||
@@ -729,7 +710,6 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
|
||||
},
|
||||
},
|
||||
|
||||
// ========== 自定义模板 ==========
|
||||
{
|
||||
name: "OpenAI Compatible",
|
||||
websiteUrl: "",
|
||||
@@ -758,4 +738,18 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "Oh My OpenCode",
|
||||
websiteUrl: "https://github.com/code-yeongyu/oh-my-opencode",
|
||||
settingsConfig: {
|
||||
npm: "",
|
||||
options: {},
|
||||
models: {},
|
||||
},
|
||||
category: "omo" as ProviderCategory,
|
||||
icon: "opencode",
|
||||
iconColor: "#8B5CF6",
|
||||
isCustomTemplate: true,
|
||||
},
|
||||
];
|
||||
|
||||
+101
-1
@@ -32,6 +32,7 @@
|
||||
"back": "Back",
|
||||
"refresh": "Refresh",
|
||||
"refreshing": "Refreshing...",
|
||||
"import": "Import",
|
||||
"all": "All",
|
||||
"search": "Search",
|
||||
"reset": "Reset",
|
||||
@@ -106,6 +107,10 @@
|
||||
"duplicate": "Duplicate",
|
||||
"sortUpdateFailed": "Failed to update sort order",
|
||||
"configureUsage": "Configure usage query",
|
||||
"officialPartner": "Official Partner",
|
||||
"openTerminal": "Open Terminal",
|
||||
"terminalOpened": "Terminal opened",
|
||||
"terminalOpenFailed": "Failed to open terminal",
|
||||
"name": "Provider Name",
|
||||
"namePlaceholder": "e.g., Claude Official",
|
||||
"websiteUrl": "Website URL",
|
||||
@@ -156,7 +161,8 @@
|
||||
"deleteFailed": "Failed to delete provider: {{error}}",
|
||||
"settingsSaved": "Settings saved",
|
||||
"settingsSaveFailed": "Failed to save settings: {{error}}",
|
||||
"openAIChatFormatHint": "This provider uses OpenAI Chat format and requires the proxy service to be enabled"
|
||||
"openAIChatFormatHint": "This provider uses OpenAI Chat format and requires the proxy service to be enabled",
|
||||
"openLinkFailed": "Failed to open link"
|
||||
},
|
||||
"confirm": {
|
||||
"deleteProvider": "Delete Provider",
|
||||
@@ -478,6 +484,7 @@
|
||||
"aggregatorApiKeyHint": "💡 Only need to fill in API Key, endpoint is preset",
|
||||
"thirdPartyApiKeyHint": "💡 Only need to fill in API Key, endpoint is preset",
|
||||
"customApiKeyHint": "💡 Custom configuration requires manually filling all necessary fields",
|
||||
"omoHint": "💡 OMO config manages Agent model assignments and writes to oh-my-opencode.jsonc",
|
||||
"officialHint": "💡 Official provider uses browser login, no API Key needed",
|
||||
"getApiKey": "Get API Key",
|
||||
"partnerPromotion": {
|
||||
@@ -1267,6 +1274,9 @@
|
||||
"agents": {
|
||||
"title": "Agents"
|
||||
},
|
||||
"modelTest": {
|
||||
"testProvider": "Test model"
|
||||
},
|
||||
"health": {
|
||||
"operational": "Operational",
|
||||
"degraded": "Degraded",
|
||||
@@ -1505,6 +1515,7 @@
|
||||
"deleted": "Universal provider deleted",
|
||||
"addSuccess": "Universal provider added successfully",
|
||||
"addFailed": "Failed to add universal provider",
|
||||
"hint": "Cross-app unified config, auto-sync to Claude/Codex/Gemini",
|
||||
"manage": "Manage",
|
||||
"loadError": "Failed to load universal providers",
|
||||
"saveError": "Failed to save universal provider",
|
||||
@@ -1519,5 +1530,94 @@
|
||||
"saveAndSyncError": "Failed to save and sync",
|
||||
"configJsonPreview": "Config JSON Preview",
|
||||
"configJsonPreviewHint": "The following configurations will be synced to each app (only the displayed fields will be overwritten, other custom settings will be preserved)"
|
||||
},
|
||||
"omo": {
|
||||
"editProfile": "Edit OMO Config",
|
||||
"newProfile": "New OMO Config",
|
||||
"profileName": "Name",
|
||||
"mainAgents": "Main Agents",
|
||||
"subAgents": "Sub Agents",
|
||||
"categories": "Categories",
|
||||
"customAgents": "Custom Agents",
|
||||
"noCustomAgents": "No custom agents",
|
||||
"otherFields": "Other Config",
|
||||
"globalConfig": "OMO Global Config",
|
||||
"globalConfigShort": "OMO Config",
|
||||
"globalConfigSaved": "Global config saved",
|
||||
"addProfile": "Add OMO Provider",
|
||||
"disabledItems": "Disabled Items",
|
||||
"advanced": "Advanced Settings",
|
||||
"profileCreated": "OMO config created",
|
||||
"profileUpdated": "OMO config updated",
|
||||
"invalidJson": "Other Fields contains invalid JSON",
|
||||
"confirmDelete": "Delete Config",
|
||||
"confirmDeleteMsg": "Delete \"{{name}}\"?",
|
||||
"profileDeleted": "Config deleted",
|
||||
"imported": "Imported as \"{{name}}\"",
|
||||
"import": "Import",
|
||||
"global": "Global",
|
||||
"empty": "No OMO configs yet. Click + Add or Import from local.",
|
||||
"applied": "Applied",
|
||||
"apply": "Apply",
|
||||
"enable": "Enable",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "OMO disabled",
|
||||
"disableFailed": "Failed to disable OMO: {{error}}",
|
||||
"writeCommonConfig": "Write to common config",
|
||||
"editCommonConfig": "Edit common config",
|
||||
"editCommonConfigTitle": "Common Config",
|
||||
"commonConfigHint": "OMO common config will be merged into all OMO configs that enable it",
|
||||
"selectPlaceholder": "Select...",
|
||||
"clear": "Clear",
|
||||
"clearWrapped": "(Clear)",
|
||||
"defaultWrapped": "(Default)",
|
||||
"variantPlaceholder": "variant",
|
||||
"selectEnabledModel": "Select enabled model",
|
||||
"selectModelFirst": "Select model first",
|
||||
"noEnabledModels": "No enabled models",
|
||||
"noVariantsForModel": "No variants for model",
|
||||
"currentValueNotEnabled": "{{value}} (current value, not enabled)",
|
||||
"currentValueUnavailable": "{{value}} (current value, unavailable)",
|
||||
"advancedLabel": "Advanced",
|
||||
"advancedJsonInvalid": "Advanced JSON is invalid",
|
||||
"advancedJsonHint": "temperature, top_p, budgetTokens, prompt_append, permission, etc. Leave empty for defaults",
|
||||
"noEnabledModelsWarning": "No enabled models available. Configure and enable OpenCode models first.",
|
||||
"importLocalReplaceSuccess": "Imported local file and replaced Agents/Categories/Other Fields",
|
||||
"importLocalFailed": "Failed to read local file: {{error}}",
|
||||
"agentKeyPlaceholder": "agent key",
|
||||
"categoryKeyPlaceholder": "category key",
|
||||
"modelNamePlaceholder": "model-name",
|
||||
"custom": "Custom",
|
||||
"customCategories": "Custom Categories",
|
||||
"modelConfiguration": "Model Configuration",
|
||||
"fillRecommended": "Fill Recommended",
|
||||
"configSummary": "{{agents}} agents, {{categories}} categories configured · Click ⚙ for advanced params",
|
||||
"enabledModelsCount": "{{count}} enabled models available",
|
||||
"source": "from:",
|
||||
"otherFieldsJson": "Other Fields (JSON)",
|
||||
"searchOrType": "Search or type custom value...",
|
||||
"noMatches": "No matches",
|
||||
"jsonMustBeObject": "{{field}} must be a JSON object",
|
||||
"jsonInvalid": "{{field}} contains invalid JSON",
|
||||
"importGlobalSuccess": "Imported global config from local file (unsaved)",
|
||||
"importGlobalFailed": "Failed to read local file: {{error}}",
|
||||
"importLocal": "Import Local",
|
||||
"saveGlobalConfig": "Save Global Config",
|
||||
"schemaUrl": "$schema",
|
||||
"resetDefault": "Reset",
|
||||
"sisyphusAgentConfig": "Sisyphus Agent Config",
|
||||
"disabledAgents": "Agents",
|
||||
"disabledAgentsPlaceholder": "Disabled Agents",
|
||||
"disabledMcps": "MCPs",
|
||||
"disabledMcpsPlaceholder": "Disabled MCPs",
|
||||
"disabledHooks": "Hooks",
|
||||
"disabledHooksPlaceholder": "Disabled Hooks",
|
||||
"disabledSkills": "Skills",
|
||||
"disabledSkillsPlaceholder": "Disabled Skills",
|
||||
"advancedLsp": "LSP Config",
|
||||
"advancedExperimental": "Experimental Features",
|
||||
"advancedBackgroundTask": "Background Tasks",
|
||||
"advancedBrowserAutomation": "Browser Automation",
|
||||
"advancedClaudeCode": "Claude Code"
|
||||
}
|
||||
}
|
||||
|
||||
+108
-1
@@ -32,6 +32,7 @@
|
||||
"back": "戻る",
|
||||
"refresh": "更新",
|
||||
"refreshing": "更新中...",
|
||||
"import": "インポート",
|
||||
"all": "すべて",
|
||||
"search": "検索",
|
||||
"reset": "リセット",
|
||||
@@ -106,6 +107,10 @@
|
||||
"duplicate": "複製",
|
||||
"sortUpdateFailed": "並び順の更新に失敗しました",
|
||||
"configureUsage": "利用状況を設定",
|
||||
"officialPartner": "公式パートナー",
|
||||
"openTerminal": "ターミナルを開く",
|
||||
"terminalOpened": "ターミナルを開きました",
|
||||
"terminalOpenFailed": "ターミナルを開けませんでした",
|
||||
"name": "プロバイダー名",
|
||||
"namePlaceholder": "例: Claude Official",
|
||||
"websiteUrl": "Web サイト URL",
|
||||
@@ -156,7 +161,8 @@
|
||||
"deleteFailed": "プロバイダーの削除に失敗しました: {{error}}",
|
||||
"settingsSaved": "設定を保存しました",
|
||||
"settingsSaveFailed": "設定の保存に失敗しました: {{error}}",
|
||||
"openAIChatFormatHint": "このプロバイダーは OpenAI Chat フォーマットを使用しており、プロキシサービスの有効化が必要です"
|
||||
"openAIChatFormatHint": "このプロバイダーは OpenAI Chat フォーマットを使用しており、プロキシサービスの有効化が必要です",
|
||||
"openLinkFailed": "リンクを開けませんでした"
|
||||
},
|
||||
"confirm": {
|
||||
"deleteProvider": "プロバイダーを削除",
|
||||
@@ -478,6 +484,7 @@
|
||||
"aggregatorApiKeyHint": "💡 API Key のみ入力すれば OK。エンドポイントはプリセット済みです",
|
||||
"thirdPartyApiKeyHint": "💡 API Key のみ入力すれば OK。エンドポイントはプリセット済みです",
|
||||
"customApiKeyHint": "💡 カスタム設定では必要な項目をすべて手動で入力してください",
|
||||
"omoHint": "💡 OMO 設定は Agent のモデル割り当てを管理し、oh-my-opencode.jsonc に書き込みます",
|
||||
"officialHint": "💡 公式プロバイダーはブラウザログインで、API Key は不要です",
|
||||
"getApiKey": "API Key を取得",
|
||||
"partnerPromotion": {
|
||||
@@ -1161,6 +1168,13 @@
|
||||
"codex": "Codex",
|
||||
"gemini": "Gemini",
|
||||
"opencode": "OpenCode"
|
||||
},
|
||||
"installFromZip": {
|
||||
"button": "ZIP からインストール",
|
||||
"installing": "インストール中...",
|
||||
"successSingle": "スキル {{name}} をインストールしました",
|
||||
"successMultiple": "{{count}} 件のスキルをインストールしました",
|
||||
"noSkillsFound": "ZIP ファイルにスキルが見つかりません(SKILL.md が必要です)"
|
||||
}
|
||||
},
|
||||
"deeplink": {
|
||||
@@ -1258,6 +1272,9 @@
|
||||
"agents": {
|
||||
"title": "エージェント"
|
||||
},
|
||||
"modelTest": {
|
||||
"testProvider": "モデルテスト"
|
||||
},
|
||||
"health": {
|
||||
"operational": "正常",
|
||||
"degraded": "低下",
|
||||
@@ -1479,6 +1496,7 @@
|
||||
"deleted": "統合プロバイダーを削除しました",
|
||||
"addSuccess": "統合プロバイダーを追加しました",
|
||||
"addFailed": "統合プロバイダーの追加に失敗しました",
|
||||
"hint": "クロスアプリ統合設定。Claude/Codex/Gemini に自動同期します",
|
||||
"manage": "管理",
|
||||
"loadError": "統合プロバイダーの読み込みに失敗しました",
|
||||
"saveError": "統合プロバイダーの保存に失敗しました",
|
||||
@@ -1493,5 +1511,94 @@
|
||||
"saveAndSyncError": "保存と同期に失敗しました",
|
||||
"configJsonPreview": "設定 JSON プレビュー",
|
||||
"configJsonPreviewHint": "以下は各アプリに同期される設定内容です(表示されているフィールドのみ上書きされ、他のカスタム設定は保持されます)"
|
||||
},
|
||||
"omo": {
|
||||
"editProfile": "OMO 設定を編集",
|
||||
"newProfile": "新規 OMO 設定",
|
||||
"profileName": "名前",
|
||||
"mainAgents": "メインエージェント",
|
||||
"subAgents": "サブエージェント",
|
||||
"categories": "カテゴリ",
|
||||
"customAgents": "カスタムエージェント",
|
||||
"noCustomAgents": "カスタムエージェントなし",
|
||||
"otherFields": "その他の設定",
|
||||
"globalConfig": "OMO グローバル設定",
|
||||
"globalConfigShort": "OMO 設定",
|
||||
"globalConfigSaved": "グローバル設定を保存しました",
|
||||
"addProfile": "OMO プロバイダーを追加",
|
||||
"disabledItems": "無効項目設定",
|
||||
"advanced": "詳細設定",
|
||||
"profileCreated": "OMO 設定を作成しました",
|
||||
"profileUpdated": "OMO 設定を更新しました",
|
||||
"invalidJson": "その他のフィールドに無効なJSONが含まれています",
|
||||
"confirmDelete": "設定を削除",
|
||||
"confirmDeleteMsg": "「{{name}}」を削除しますか?",
|
||||
"profileDeleted": "設定を削除しました",
|
||||
"imported": "「{{name}}」としてインポートしました",
|
||||
"import": "インポート",
|
||||
"global": "グローバル",
|
||||
"empty": "OMO 設定がありません。+ 追加またはローカルからインポートしてください。",
|
||||
"applied": "適用済み",
|
||||
"apply": "適用",
|
||||
"enable": "有効化",
|
||||
"enabled": "有効中",
|
||||
"disabled": "OMO を無効化しました",
|
||||
"disableFailed": "OMO の無効化に失敗しました: {{error}}",
|
||||
"writeCommonConfig": "共通設定に書き込む",
|
||||
"editCommonConfig": "共通設定を編集",
|
||||
"editCommonConfigTitle": "共通設定",
|
||||
"commonConfigHint": "OMO 共通設定は有効にしたすべての OMO 設定に統合されます",
|
||||
"selectPlaceholder": "選択してください...",
|
||||
"clear": "クリア",
|
||||
"clearWrapped": "(クリア)",
|
||||
"defaultWrapped": "(デフォルト)",
|
||||
"variantPlaceholder": "variant",
|
||||
"selectEnabledModel": "有効なモデルを選択",
|
||||
"selectModelFirst": "先にモデルを選択",
|
||||
"noEnabledModels": "有効なモデルがありません",
|
||||
"noVariantsForModel": "このモデルには思考レベルがありません",
|
||||
"currentValueNotEnabled": "{{value}} (現在値・未有効)",
|
||||
"currentValueUnavailable": "{{value}} (現在値・利用不可)",
|
||||
"advancedLabel": "詳細",
|
||||
"advancedJsonInvalid": "詳細 JSON が不正です",
|
||||
"advancedJsonHint": "temperature, top_p, budgetTokens, prompt_append, permission など。空欄でデフォルトを使用します",
|
||||
"noEnabledModelsWarning": "利用可能な有効モデルがありません。先に OpenCode モデルを有効化してください。",
|
||||
"importLocalReplaceSuccess": "ローカルファイルから読み込み、Agents/Categories/Other Fields を置き換えました",
|
||||
"importLocalFailed": "ローカルファイルの読み込みに失敗しました: {{error}}",
|
||||
"agentKeyPlaceholder": "agent キー",
|
||||
"categoryKeyPlaceholder": "カテゴリキー",
|
||||
"modelNamePlaceholder": "model-name",
|
||||
"custom": "カスタム",
|
||||
"customCategories": "カスタムカテゴリ",
|
||||
"modelConfiguration": "モデル設定",
|
||||
"fillRecommended": "推奨を入力",
|
||||
"configSummary": "{{agents}} 個の Agent、{{categories}} 個の Category を設定済み · ⚙ で詳細を展開",
|
||||
"enabledModelsCount": "有効モデル {{count}} 件",
|
||||
"source": "出典:",
|
||||
"otherFieldsJson": "その他のフィールド (JSON)",
|
||||
"searchOrType": "検索またはカスタム値を入力...",
|
||||
"noMatches": "一致する項目がありません",
|
||||
"jsonMustBeObject": "{{field}} は JSON オブジェクトである必要があります",
|
||||
"jsonInvalid": "{{field}} に無効な JSON が含まれています",
|
||||
"importGlobalSuccess": "ローカルファイルからグローバル設定を読み込みました(未保存)",
|
||||
"importGlobalFailed": "ローカルファイルの読み込みに失敗しました: {{error}}",
|
||||
"importLocal": "ローカルからインポート",
|
||||
"saveGlobalConfig": "グローバル設定を保存",
|
||||
"schemaUrl": "$schema",
|
||||
"resetDefault": "デフォルトに戻す",
|
||||
"sisyphusAgentConfig": "Sisyphus Agent 設定",
|
||||
"disabledAgents": "Agents",
|
||||
"disabledAgentsPlaceholder": "無効化する Agents",
|
||||
"disabledMcps": "MCPs",
|
||||
"disabledMcpsPlaceholder": "無効化する MCPs",
|
||||
"disabledHooks": "Hooks",
|
||||
"disabledHooksPlaceholder": "無効化する Hooks",
|
||||
"disabledSkills": "Skills",
|
||||
"disabledSkillsPlaceholder": "無効化する Skills",
|
||||
"advancedLsp": "LSP 設定",
|
||||
"advancedExperimental": "実験的機能",
|
||||
"advancedBackgroundTask": "バックグラウンドタスク",
|
||||
"advancedBrowserAutomation": "ブラウザ自動化",
|
||||
"advancedClaudeCode": "Claude Code"
|
||||
}
|
||||
}
|
||||
|
||||
+101
-1
@@ -32,6 +32,7 @@
|
||||
"back": "返回",
|
||||
"refresh": "刷新",
|
||||
"refreshing": "刷新中...",
|
||||
"import": "导入",
|
||||
"all": "全部",
|
||||
"search": "查询",
|
||||
"reset": "重置",
|
||||
@@ -106,6 +107,10 @@
|
||||
"duplicate": "复制",
|
||||
"sortUpdateFailed": "排序更新失败",
|
||||
"configureUsage": "配置用量查询",
|
||||
"officialPartner": "官方合作伙伴",
|
||||
"openTerminal": "打开终端",
|
||||
"terminalOpened": "终端已打开",
|
||||
"terminalOpenFailed": "打开终端失败",
|
||||
"name": "供应商名称",
|
||||
"namePlaceholder": "例如:Claude 官方",
|
||||
"websiteUrl": "官网链接",
|
||||
@@ -156,7 +161,8 @@
|
||||
"deleteFailed": "删除供应商失败:{{error}}",
|
||||
"settingsSaved": "设置已保存",
|
||||
"settingsSaveFailed": "保存设置失败:{{error}}",
|
||||
"openAIChatFormatHint": "此供应商使用 OpenAI Chat 格式,需要开启代理服务才能正常使用"
|
||||
"openAIChatFormatHint": "此供应商使用 OpenAI Chat 格式,需要开启代理服务才能正常使用",
|
||||
"openLinkFailed": "链接打开失败"
|
||||
},
|
||||
"confirm": {
|
||||
"deleteProvider": "删除供应商",
|
||||
@@ -478,6 +484,7 @@
|
||||
"aggregatorApiKeyHint": "💡 只需填写 API Key,请求地址已预设",
|
||||
"thirdPartyApiKeyHint": "💡 只需填写 API Key,请求地址已预设",
|
||||
"customApiKeyHint": "💡 自定义配置需手动填写所有必要字段",
|
||||
"omoHint": "💡 OMO 配置管理 Agent 模型分配,写入 oh-my-opencode.jsonc",
|
||||
"officialHint": "💡 官方供应商使用浏览器登录,无需配置 API Key",
|
||||
"getApiKey": "获取 API Key",
|
||||
"partnerPromotion": {
|
||||
@@ -1267,6 +1274,9 @@
|
||||
"agents": {
|
||||
"title": "智能体"
|
||||
},
|
||||
"modelTest": {
|
||||
"testProvider": "测试模型"
|
||||
},
|
||||
"health": {
|
||||
"operational": "正常",
|
||||
"degraded": "降级",
|
||||
@@ -1505,6 +1515,7 @@
|
||||
"deleted": "统一供应商已删除",
|
||||
"addSuccess": "统一供应商添加成功",
|
||||
"addFailed": "统一供应商添加失败",
|
||||
"hint": "跨应用统一配置,自动同步到 Claude/Codex/Gemini",
|
||||
"manage": "管理",
|
||||
"loadError": "加载统一供应商失败",
|
||||
"saveError": "保存统一供应商失败",
|
||||
@@ -1519,5 +1530,94 @@
|
||||
"saveAndSyncError": "保存并同步失败",
|
||||
"configJsonPreview": "配置 JSON 预览",
|
||||
"configJsonPreviewHint": "以下是将要同步到各应用的配置内容(仅覆盖显示的字段,保留其他自定义配置)"
|
||||
},
|
||||
"omo": {
|
||||
"editProfile": "编辑 OMO 配置",
|
||||
"newProfile": "新建 OMO 配置",
|
||||
"profileName": "名称",
|
||||
"mainAgents": "主 Agent",
|
||||
"subAgents": "子 Agent",
|
||||
"categories": "分类",
|
||||
"customAgents": "自定义 Agent",
|
||||
"noCustomAgents": "暂无自定义 Agent",
|
||||
"otherFields": "其他配置",
|
||||
"globalConfig": "OMO 全局配置",
|
||||
"globalConfigShort": "OMO 配置",
|
||||
"globalConfigSaved": "全局配置已保存",
|
||||
"addProfile": "添加 OMO 配置",
|
||||
"disabledItems": "禁用项设置",
|
||||
"advanced": "高级设置",
|
||||
"profileCreated": "OMO 配置已创建",
|
||||
"profileUpdated": "OMO 配置已更新",
|
||||
"invalidJson": "其他字段包含无效 JSON",
|
||||
"confirmDelete": "删除配置",
|
||||
"confirmDeleteMsg": "确定删除 \"{{name}}\" 吗?",
|
||||
"profileDeleted": "配置已删除",
|
||||
"imported": "已导入为 \"{{name}}\"",
|
||||
"import": "导入",
|
||||
"global": "全局",
|
||||
"empty": "暂无配置。点击 + 添加或从本地导入。",
|
||||
"applied": "已应用",
|
||||
"apply": "应用",
|
||||
"enable": "启用",
|
||||
"enabled": "启用中",
|
||||
"disabled": "OMO 已停用",
|
||||
"disableFailed": "停用 OMO 失败: {{error}}",
|
||||
"writeCommonConfig": "写入通用配置",
|
||||
"editCommonConfig": "编辑通用配置",
|
||||
"editCommonConfigTitle": "通用配置",
|
||||
"commonConfigHint": "OMO 通用配置将合并到所有启用它的 OMO 配置中",
|
||||
"selectPlaceholder": "请选择...",
|
||||
"clear": "清空",
|
||||
"clearWrapped": "(清空)",
|
||||
"defaultWrapped": "(默认)",
|
||||
"variantPlaceholder": "思考等级",
|
||||
"selectEnabledModel": "选择已启用模型",
|
||||
"selectModelFirst": "先选择模型",
|
||||
"noEnabledModels": "暂无已启用模型",
|
||||
"noVariantsForModel": "该模型无思考等级",
|
||||
"currentValueNotEnabled": "{{value}}(当前值,未启用)",
|
||||
"currentValueUnavailable": "{{value}}(当前值,未启用)",
|
||||
"advancedLabel": "高级参数",
|
||||
"advancedJsonInvalid": "高级参数 JSON 无效",
|
||||
"advancedJsonHint": "temperature, top_p, budgetTokens, prompt_append, permission 等,留空使用默认值",
|
||||
"noEnabledModelsWarning": "当前没有可用的已启用模型,请先启用并配置 OpenCode 模型",
|
||||
"importLocalReplaceSuccess": "已从本地文件导入并覆盖 Agent/Category/Other Fields",
|
||||
"importLocalFailed": "读取本地文件失败: {{error}}",
|
||||
"agentKeyPlaceholder": "agent 键名",
|
||||
"categoryKeyPlaceholder": "分类键名",
|
||||
"modelNamePlaceholder": "模型名",
|
||||
"custom": "自定义",
|
||||
"customCategories": "自定义分类",
|
||||
"modelConfiguration": "模型配置",
|
||||
"fillRecommended": "填充推荐",
|
||||
"configSummary": "已配置 {{agents}} 个 Agent,{{categories}} 个 Category · 点击 ⚙ 展开高级参数",
|
||||
"enabledModelsCount": "可选已启用模型 {{count}} 个",
|
||||
"source": "来源:",
|
||||
"otherFieldsJson": "其他字段 (JSON)",
|
||||
"searchOrType": "搜索或输入自定义值...",
|
||||
"noMatches": "无匹配项",
|
||||
"jsonMustBeObject": "{{field}} 必须是 JSON 对象",
|
||||
"jsonInvalid": "{{field}} 包含无效 JSON",
|
||||
"importGlobalSuccess": "已从本地文件导入全局配置(未保存)",
|
||||
"importGlobalFailed": "读取本地文件失败: {{error}}",
|
||||
"importLocal": "从本地导入",
|
||||
"saveGlobalConfig": "保存全局配置",
|
||||
"schemaUrl": "$schema",
|
||||
"resetDefault": "重置默认",
|
||||
"sisyphusAgentConfig": "Sisyphus Agent 设置",
|
||||
"disabledAgents": "Agents",
|
||||
"disabledAgentsPlaceholder": "禁用的 Agents",
|
||||
"disabledMcps": "MCPs",
|
||||
"disabledMcpsPlaceholder": "禁用的 MCPs",
|
||||
"disabledHooks": "Hooks",
|
||||
"disabledHooksPlaceholder": "禁用的 Hooks",
|
||||
"disabledSkills": "Skills",
|
||||
"disabledSkillsPlaceholder": "禁用的 Skills",
|
||||
"advancedLsp": "LSP 配置",
|
||||
"advancedExperimental": "实验性功能",
|
||||
"advancedBackgroundTask": "后台任务",
|
||||
"advancedBrowserAutomation": "浏览器自动化",
|
||||
"advancedClaudeCode": "Claude Code"
|
||||
}
|
||||
}
|
||||
|
||||
+9
-34
@@ -1,9 +1,7 @@
|
||||
/* Tailwind CSS v3 指令 */
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* shadcn/ui 主题变量 - 蓝色主题 */
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
@@ -13,25 +11,20 @@
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 240 10% 3.9%;
|
||||
|
||||
/* 主色调:macOS 风格系统蓝 */
|
||||
--primary: 210 100% 56%;
|
||||
--primary-foreground: 0 0% 100%;
|
||||
|
||||
/* 次要色:淡蓝灰 */
|
||||
--secondary: 240 4.8% 95.9%;
|
||||
--secondary-foreground: 240 5.9% 10%;
|
||||
|
||||
/* 强调色 */
|
||||
--muted: 240 4.8% 95.9%;
|
||||
--muted-foreground: 240 3.8% 46.1%;
|
||||
--accent: 240 4.8% 95.9%;
|
||||
--accent-foreground: 240 5.9% 10%;
|
||||
|
||||
/* 危险色:红色 */
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
|
||||
/* 边框和输入框 */
|
||||
--border: 240 5.9% 90%;
|
||||
--input: 240 5.9% 90%;
|
||||
--ring: 210 100% 56%;
|
||||
@@ -40,7 +33,6 @@
|
||||
}
|
||||
|
||||
.dark {
|
||||
/* 背景与卡片:接近 macOS 深色 systemBackground / windowBackground */
|
||||
--background: 240 5% 12%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 240 5% 16%;
|
||||
@@ -48,7 +40,6 @@
|
||||
--popover: 240 5% 16%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
|
||||
/* 暗色模式主色调:macOS 风格系统蓝(略微降低亮度) */
|
||||
--primary: 210 100% 54%;
|
||||
--primary-foreground: 0 0% 100%;
|
||||
|
||||
@@ -60,7 +51,6 @@
|
||||
--accent: 240 5% 18%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
|
||||
/* 暗色模式危险色 */
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
|
||||
@@ -70,7 +60,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Glassmorphism Utilities */
|
||||
.glass {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
backdrop-filter: blur(10px);
|
||||
@@ -100,7 +89,6 @@
|
||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
|
||||
}
|
||||
|
||||
/* 供应商卡片选中状态 */
|
||||
.glass-card-active {
|
||||
background: rgba(59, 130, 246, 0.08);
|
||||
border: 1px solid rgba(59, 130, 246, 0.4);
|
||||
@@ -125,7 +113,6 @@
|
||||
border-top: 2px solid hsl(var(--border));
|
||||
}
|
||||
|
||||
/* Tauri 拖拽区域 */
|
||||
[data-tauri-drag-region] {
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
@@ -135,19 +122,16 @@
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
/* 全局基础样式 */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE 10+ */
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
html {
|
||||
@apply font-sans antialiased;
|
||||
line-height: 1.5;
|
||||
/* 让原生控件与滚动条随主题切换配色 */
|
||||
color-scheme: light;
|
||||
/* 禁用 overscroll 回弹效果,防止下拉时顶部边框被拉下来 */
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
@@ -157,24 +141,19 @@ body {
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
/* 暗色模式下启用暗色原生控件/滚动条配色 */
|
||||
html.dark {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
/* 滚动条样式 - 完全隐藏(支持所有浏览器) */
|
||||
::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 焦点样式 */
|
||||
*:focus-visible {
|
||||
@apply outline-2 outline-blue-500 outline-offset-2;
|
||||
}
|
||||
|
||||
/* 统一边框设计系统 - 使用工具类定义 */
|
||||
@layer utilities {
|
||||
/* 让滚动条悬浮于内容之上,避免出现/消失时挤压布局 */
|
||||
.scroll-overlay {
|
||||
scrollbar-gutter: stable both-edges;
|
||||
padding-right: 0.5rem;
|
||||
@@ -182,13 +161,11 @@ html.dark {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* 默认边框:1px,使用主题边框颜色 */
|
||||
.border-default {
|
||||
border-width: 1px;
|
||||
border-color: hsl(var(--border));
|
||||
}
|
||||
|
||||
/* 激活边框:2px,使用主色 */
|
||||
.border-active {
|
||||
border-width: 2px;
|
||||
}
|
||||
@@ -210,20 +187,17 @@ html.dark {
|
||||
}
|
||||
}
|
||||
|
||||
/* 禁用 Edge / IE 的密码显示按钮 */
|
||||
input[type="password"]::-ms-reveal,
|
||||
input[type="password"]::-ms-clear {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Theme transition animation using View Transitions API */
|
||||
::view-transition-old(root),
|
||||
::view-transition-new(root) {
|
||||
animation: none;
|
||||
mix-blend-mode: normal;
|
||||
}
|
||||
|
||||
/* Old snapshot stays behind, new snapshot animates on top */
|
||||
::view-transition-old(root) {
|
||||
z-index: 1;
|
||||
}
|
||||
@@ -232,25 +206,26 @@ input[type="password"]::-ms-clear {
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
/* Circular expand animation from click position */
|
||||
@keyframes theme-circle-expand {
|
||||
from {
|
||||
clip-path: circle(0% at var(--theme-transition-x, 50%) var(--theme-transition-y, 50%));
|
||||
clip-path: circle(
|
||||
0% at var(--theme-transition-x, 50%) var(--theme-transition-y, 50%)
|
||||
);
|
||||
}
|
||||
|
||||
to {
|
||||
clip-path: circle(150% at var(--theme-transition-x, 50%) var(--theme-transition-y, 50%));
|
||||
clip-path: circle(
|
||||
150% at var(--theme-transition-x, 50%) var(--theme-transition-y, 50%)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/* Apply animation to new snapshot - works for both light and dark transitions */
|
||||
::view-transition-new(root) {
|
||||
animation: theme-circle-expand 0.4s ease-out;
|
||||
}
|
||||
|
||||
/* Respect user preference for reduced motion */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
::view-transition-new(root) {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// 配置相关 API
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
export type AppType = "claude" | "codex" | "gemini";
|
||||
export type AppType = "claude" | "codex" | "gemini" | "omo";
|
||||
|
||||
/**
|
||||
* 获取 Claude 通用配置片段(已废弃,使用 getCommonConfigSnippet)
|
||||
@@ -63,7 +63,7 @@ export type ExtractCommonConfigSnippetOptions = {
|
||||
};
|
||||
|
||||
export async function extractCommonConfigSnippet(
|
||||
appType: AppType,
|
||||
appType: Exclude<AppType, "omo">,
|
||||
options?: ExtractCommonConfigSnippetOptions,
|
||||
): Promise<string> {
|
||||
const args: Record<string, unknown> = { appType };
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import type { OmoLocalFileData } from "@/types/omo";
|
||||
|
||||
export const omoApi = {
|
||||
readLocalFile: (): Promise<OmoLocalFileData> => invoke("read_omo_local_file"),
|
||||
getCurrentOmoProviderId: (): Promise<string> =>
|
||||
invoke("get_current_omo_provider_id"),
|
||||
getOmoProviderCount: (): Promise<number> => invoke("get_omo_provider_count"),
|
||||
disableCurrentOmo: (): Promise<void> => invoke("disable_current_omo"),
|
||||
};
|
||||
+28
-14
@@ -17,13 +17,15 @@ export const useAddProviderMutation = (appId: AppId) => {
|
||||
let id: string;
|
||||
|
||||
if (appId === "opencode") {
|
||||
// OpenCode: use user-provided providerKey as ID
|
||||
if (!providerInput.providerKey) {
|
||||
throw new Error("Provider key is required for OpenCode");
|
||||
if (providerInput.category === "omo") {
|
||||
id = `omo-${generateUUID()}`;
|
||||
} else {
|
||||
if (!providerInput.providerKey) {
|
||||
throw new Error("Provider key is required for OpenCode");
|
||||
}
|
||||
id = providerInput.providerKey;
|
||||
}
|
||||
id = providerInput.providerKey;
|
||||
} else {
|
||||
// Other apps: use random UUID
|
||||
id = generateUUID();
|
||||
}
|
||||
|
||||
@@ -32,7 +34,6 @@ export const useAddProviderMutation = (appId: AppId) => {
|
||||
id,
|
||||
createdAt: Date.now(),
|
||||
};
|
||||
// Remove providerKey from the provider object before saving
|
||||
delete (newProvider as any).providerKey;
|
||||
|
||||
await providersApi.add(newProvider, appId);
|
||||
@@ -41,7 +42,15 @@ export const useAddProviderMutation = (appId: AppId) => {
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ["providers", appId] });
|
||||
|
||||
// 更新托盘菜单(失败不影响主操作)
|
||||
if (appId === "opencode") {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["omo", "current-provider-id"],
|
||||
});
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["omo", "provider-count"],
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await providersApi.updateTrayMenu();
|
||||
} catch (trayError) {
|
||||
@@ -115,7 +124,15 @@ export const useDeleteProviderMutation = (appId: AppId) => {
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ["providers", appId] });
|
||||
|
||||
// 更新托盘菜单(失败不影响主操作)
|
||||
if (appId === "opencode") {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["omo", "current-provider-id"],
|
||||
});
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["omo", "provider-count"],
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await providersApi.updateTrayMenu();
|
||||
} catch (trayError) {
|
||||
@@ -157,14 +174,15 @@ export const useSwitchProviderMutation = (appId: AppId) => {
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ["providers", appId] });
|
||||
|
||||
// OpenCode: also invalidate live provider IDs cache to update button state
|
||||
if (appId === "opencode") {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["opencodeLiveProviderIds"],
|
||||
});
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["omo", "current-provider-id"],
|
||||
});
|
||||
}
|
||||
|
||||
// 更新托盘菜单(失败不影响主操作)
|
||||
try {
|
||||
await providersApi.updateTrayMenu();
|
||||
} catch (trayError) {
|
||||
@@ -173,14 +191,10 @@ export const useSwitchProviderMutation = (appId: AppId) => {
|
||||
trayError,
|
||||
);
|
||||
}
|
||||
|
||||
// Note: Success toast is handled by useProviderActions.switchProvider
|
||||
// to allow customization based on provider properties (e.g., apiFormat)
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
const detail = extractErrorMessage(error) || t("common.unknown");
|
||||
|
||||
// 标题与详情分离,便于扫描 + 一键复制
|
||||
toast.error(
|
||||
t("notifications.switchFailedTitle", { defaultValue: "切换失败" }),
|
||||
{
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { omoApi } from "@/lib/api/omo";
|
||||
import * as configApi from "@/lib/api/config";
|
||||
import type { OmoGlobalConfig } from "@/types/omo";
|
||||
|
||||
export const omoKeys = {
|
||||
all: ["omo"] as const,
|
||||
globalConfig: () => [...omoKeys.all, "global-config"] as const,
|
||||
currentProviderId: () => [...omoKeys.all, "current-provider-id"] as const,
|
||||
providerCount: () => [...omoKeys.all, "provider-count"] as const,
|
||||
};
|
||||
|
||||
function invalidateOmoQueries(queryClient: ReturnType<typeof useQueryClient>) {
|
||||
queryClient.invalidateQueries({ queryKey: omoKeys.globalConfig() });
|
||||
queryClient.invalidateQueries({ queryKey: ["providers"] });
|
||||
queryClient.invalidateQueries({ queryKey: omoKeys.currentProviderId() });
|
||||
queryClient.invalidateQueries({ queryKey: omoKeys.providerCount() });
|
||||
}
|
||||
|
||||
export function useOmoGlobalConfig(enabled = true) {
|
||||
return useQuery({
|
||||
queryKey: omoKeys.globalConfig(),
|
||||
enabled,
|
||||
queryFn: async (): Promise<OmoGlobalConfig> => {
|
||||
const raw = await configApi.getCommonConfigSnippet("omo");
|
||||
if (!raw) {
|
||||
return {
|
||||
id: "global",
|
||||
disabledAgents: [],
|
||||
disabledMcps: [],
|
||||
disabledHooks: [],
|
||||
disabledSkills: [],
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
try {
|
||||
return JSON.parse(raw) as OmoGlobalConfig;
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"[omo] invalid global config json, fallback to defaults",
|
||||
error,
|
||||
);
|
||||
return {
|
||||
id: "global",
|
||||
disabledAgents: [],
|
||||
disabledMcps: [],
|
||||
disabledHooks: [],
|
||||
disabledSkills: [],
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useCurrentOmoProviderId(enabled = true) {
|
||||
return useQuery({
|
||||
queryKey: omoKeys.currentProviderId(),
|
||||
queryFn: omoApi.getCurrentOmoProviderId,
|
||||
enabled,
|
||||
});
|
||||
}
|
||||
|
||||
export function useOmoProviderCount(enabled = true) {
|
||||
return useQuery({
|
||||
queryKey: omoKeys.providerCount(),
|
||||
queryFn: omoApi.getOmoProviderCount,
|
||||
enabled,
|
||||
});
|
||||
}
|
||||
|
||||
export function useSaveOmoGlobalConfig() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (input: OmoGlobalConfig) => {
|
||||
const jsonStr = JSON.stringify(input);
|
||||
await configApi.setCommonConfigSnippet("omo", jsonStr);
|
||||
},
|
||||
onSuccess: () => invalidateOmoQueries(queryClient),
|
||||
});
|
||||
}
|
||||
|
||||
export function useReadOmoLocalFile() {
|
||||
return useMutation({
|
||||
mutationFn: () => omoApi.readLocalFile(),
|
||||
});
|
||||
}
|
||||
|
||||
export function useDisableCurrentOmo() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: () => omoApi.disableCurrentOmo(),
|
||||
onSuccess: () => invalidateOmoQueries(queryClient),
|
||||
});
|
||||
}
|
||||
+2
-1
@@ -3,7 +3,8 @@ export type ProviderCategory =
|
||||
| "cn_official" // 开源官方(原"国产官方")
|
||||
| "aggregator" // 聚合网站
|
||||
| "third_party" // 第三方供应商
|
||||
| "custom"; // 自定义
|
||||
| "custom" // 自定义
|
||||
| "omo"; // Oh My OpenCode
|
||||
|
||||
export interface Provider {
|
||||
id: string;
|
||||
|
||||
@@ -0,0 +1,361 @@
|
||||
export interface OmoGlobalConfig {
|
||||
id: string;
|
||||
schemaUrl?: string;
|
||||
sisyphusAgent?: Record<string, unknown>;
|
||||
disabledAgents: string[];
|
||||
disabledMcps: string[];
|
||||
disabledHooks: string[];
|
||||
disabledSkills: string[];
|
||||
lsp?: Record<string, unknown>;
|
||||
experimental?: Record<string, unknown>;
|
||||
backgroundTask?: Record<string, unknown>;
|
||||
browserAutomationEngine?: Record<string, unknown>;
|
||||
claudeCode?: Record<string, unknown>;
|
||||
otherFields?: Record<string, unknown>;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface OmoLocalFileData {
|
||||
agents?: Record<string, Record<string, unknown>>;
|
||||
categories?: Record<string, Record<string, unknown>>;
|
||||
otherFields?: Record<string, unknown>;
|
||||
global: OmoGlobalConfig;
|
||||
filePath: string;
|
||||
lastModified?: string;
|
||||
}
|
||||
|
||||
export interface OmoAgentDef {
|
||||
key: string;
|
||||
display: string;
|
||||
descZh: string;
|
||||
descEn: string;
|
||||
recommended?: string;
|
||||
group: "main" | "sub";
|
||||
}
|
||||
|
||||
export interface OmoCategoryDef {
|
||||
key: string;
|
||||
display: string;
|
||||
descZh: string;
|
||||
descEn: string;
|
||||
recommended?: string;
|
||||
}
|
||||
|
||||
export const OMO_BUILTIN_AGENTS: OmoAgentDef[] = [
|
||||
{
|
||||
key: "Sisyphus",
|
||||
display: "Sisyphus",
|
||||
descZh: "主编排者",
|
||||
descEn: "Main orchestrator",
|
||||
recommended: "claude-opus-4-6",
|
||||
group: "main",
|
||||
},
|
||||
{
|
||||
key: "Hephaestus",
|
||||
display: "Hephaestus",
|
||||
descZh: "自主深度工作者",
|
||||
descEn: "Autonomous deep worker",
|
||||
recommended: "gpt-5.3-codex",
|
||||
group: "main",
|
||||
},
|
||||
{
|
||||
key: "Prometheus",
|
||||
display: "Prometheus",
|
||||
descZh: "战略规划者",
|
||||
descEn: "Strategic planner",
|
||||
recommended: "claude-opus-4-6",
|
||||
group: "main",
|
||||
},
|
||||
{
|
||||
key: "Atlas",
|
||||
display: "Atlas",
|
||||
descZh: "任务管理者",
|
||||
descEn: "Task manager",
|
||||
recommended: "kimi-k2.5",
|
||||
group: "main",
|
||||
},
|
||||
{
|
||||
key: "oracle",
|
||||
display: "Oracle",
|
||||
descZh: "战略顾问",
|
||||
descEn: "Strategic advisor",
|
||||
recommended: "gpt-5.3",
|
||||
group: "sub",
|
||||
},
|
||||
{
|
||||
key: "librarian",
|
||||
display: "Librarian",
|
||||
descZh: "多仓库研究员",
|
||||
descEn: "Multi-repo researcher",
|
||||
recommended: "glm-4.7",
|
||||
group: "sub",
|
||||
},
|
||||
{
|
||||
key: "explore",
|
||||
display: "Explore",
|
||||
descZh: "快速代码搜索",
|
||||
descEn: "Fast code search",
|
||||
recommended: "grok-code-fast-1",
|
||||
group: "sub",
|
||||
},
|
||||
{
|
||||
key: "multimodal-looker",
|
||||
display: "Multimodal-Looker",
|
||||
descZh: "媒体分析器",
|
||||
descEn: "Media analyzer",
|
||||
recommended: "gemini-3-flash",
|
||||
group: "sub",
|
||||
},
|
||||
{
|
||||
key: "Metis",
|
||||
display: "Metis",
|
||||
descZh: "规划前分析顾问",
|
||||
descEn: "Pre-plan analysis advisor",
|
||||
recommended: "claude-opus-4-6",
|
||||
group: "sub",
|
||||
},
|
||||
{
|
||||
key: "Momus",
|
||||
display: "Momus",
|
||||
descZh: "计划审查者",
|
||||
descEn: "Plan reviewer",
|
||||
recommended: "gpt-5.3",
|
||||
group: "sub",
|
||||
},
|
||||
{
|
||||
key: "Sisyphus-Junior",
|
||||
display: "Sisyphus-Junior",
|
||||
descZh: "委托任务执行器",
|
||||
descEn: "Delegated task executor",
|
||||
group: "sub",
|
||||
},
|
||||
];
|
||||
|
||||
export const OMO_BUILTIN_CATEGORIES: OmoCategoryDef[] = [
|
||||
{
|
||||
key: "visual-engineering",
|
||||
display: "Visual Engineering",
|
||||
descZh: "视觉/前端工程",
|
||||
descEn: "Visual/frontend engineering",
|
||||
recommended: "gemini-3-pro",
|
||||
},
|
||||
{
|
||||
key: "ultrabrain",
|
||||
display: "Ultrabrain",
|
||||
descZh: "超级思考",
|
||||
descEn: "Ultra thinking",
|
||||
recommended: "claude-opus-4-6",
|
||||
},
|
||||
{
|
||||
key: "deep",
|
||||
display: "Deep",
|
||||
descZh: "深度工作",
|
||||
descEn: "Deep work",
|
||||
recommended: "gpt-5.3-codex",
|
||||
},
|
||||
{
|
||||
key: "artistry",
|
||||
display: "Artistry",
|
||||
descZh: "创意/文艺",
|
||||
descEn: "Creative/artistic",
|
||||
recommended: "claude-opus-4-6",
|
||||
},
|
||||
{
|
||||
key: "quick",
|
||||
display: "Quick",
|
||||
descZh: "快速响应",
|
||||
descEn: "Quick response",
|
||||
recommended: "gemini-3-flash",
|
||||
},
|
||||
{
|
||||
key: "unspecified-low",
|
||||
display: "Unspecified Low",
|
||||
descZh: "通用低配",
|
||||
descEn: "General low tier",
|
||||
recommended: "gemini-3-flash",
|
||||
},
|
||||
{
|
||||
key: "unspecified-high",
|
||||
display: "Unspecified High",
|
||||
descZh: "通用高配",
|
||||
descEn: "General high tier",
|
||||
recommended: "gpt-5.3-codex",
|
||||
},
|
||||
{
|
||||
key: "writing",
|
||||
display: "Writing",
|
||||
descZh: "写作",
|
||||
descEn: "Writing",
|
||||
recommended: "claude-opus-4-6",
|
||||
},
|
||||
];
|
||||
|
||||
export const OMO_DISABLEABLE_AGENTS = [
|
||||
{ value: "Prometheus (Planner)", label: "Prometheus (Planner)" },
|
||||
{ value: "Atlas", label: "Atlas" },
|
||||
{ value: "oracle", label: "Oracle" },
|
||||
{ value: "librarian", label: "Librarian" },
|
||||
{ value: "explore", label: "Explore" },
|
||||
{ value: "multimodal-looker", label: "Multimodal Looker" },
|
||||
{ value: "frontend-ui-ux-engineer", label: "Frontend UI/UX Engineer" },
|
||||
{ value: "document-writer", label: "Document Writer" },
|
||||
{ value: "Sisyphus-Junior", label: "Sisyphus-Junior" },
|
||||
{ value: "Metis (Plan Consultant)", label: "Metis (Plan Consultant)" },
|
||||
{ value: "Momus (Plan Reviewer)", label: "Momus (Plan Reviewer)" },
|
||||
{ value: "OpenCode-Builder", label: "OpenCode-Builder" },
|
||||
] as const;
|
||||
|
||||
export const OMO_DISABLEABLE_MCPS = [
|
||||
{ value: "context7", label: "context7" },
|
||||
{ value: "grep_app", label: "grep_app" },
|
||||
{ value: "websearch", label: "websearch" },
|
||||
] as const;
|
||||
|
||||
export const OMO_DISABLEABLE_HOOKS = [
|
||||
{ value: "todo-continuation-enforcer", label: "todo-continuation-enforcer" },
|
||||
{ value: "context-window-monitor", label: "context-window-monitor" },
|
||||
{ value: "session-recovery", label: "session-recovery" },
|
||||
{ value: "session-notification", label: "session-notification" },
|
||||
{ value: "comment-checker", label: "comment-checker" },
|
||||
{ value: "grep-output-truncator", label: "grep-output-truncator" },
|
||||
{ value: "tool-output-truncator", label: "tool-output-truncator" },
|
||||
{
|
||||
value: "directory-agents-injector",
|
||||
label: "directory-agents-injector",
|
||||
},
|
||||
{
|
||||
value: "directory-readme-injector",
|
||||
label: "directory-readme-injector",
|
||||
},
|
||||
{
|
||||
value: "empty-task-response-detector",
|
||||
label: "empty-task-response-detector",
|
||||
},
|
||||
{ value: "think-mode", label: "think-mode" },
|
||||
{
|
||||
value: "anthropic-context-window-limit-recovery",
|
||||
label: "anthropic-context-window-limit-recovery",
|
||||
},
|
||||
{ value: "rules-injector", label: "rules-injector" },
|
||||
{ value: "background-notification", label: "background-notification" },
|
||||
{ value: "auto-update-checker", label: "auto-update-checker" },
|
||||
{ value: "startup-toast", label: "startup-toast" },
|
||||
{ value: "keyword-detector", label: "keyword-detector" },
|
||||
{ value: "agent-usage-reminder", label: "agent-usage-reminder" },
|
||||
{ value: "non-interactive-env", label: "non-interactive-env" },
|
||||
{ value: "interactive-bash-session", label: "interactive-bash-session" },
|
||||
{
|
||||
value: "compaction-context-injector",
|
||||
label: "compaction-context-injector",
|
||||
},
|
||||
{
|
||||
value: "thinking-block-validator",
|
||||
label: "thinking-block-validator",
|
||||
},
|
||||
{ value: "claude-code-hooks", label: "claude-code-hooks" },
|
||||
{ value: "ralph-loop", label: "ralph-loop" },
|
||||
{ value: "preemptive-compaction", label: "preemptive-compaction" },
|
||||
] as const;
|
||||
|
||||
export const OMO_DISABLEABLE_SKILLS = [
|
||||
{ value: "playwright", label: "playwright" },
|
||||
{ value: "agent-browser", label: "agent-browser" },
|
||||
{ value: "git-master", label: "git-master" },
|
||||
] as const;
|
||||
|
||||
export const OMO_DEFAULT_SCHEMA_URL =
|
||||
"https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json";
|
||||
|
||||
export const OMO_SISYPHUS_AGENT_PLACEHOLDER = `{
|
||||
"disabled": false,
|
||||
"default_builder_enabled": false,
|
||||
"planner_enabled": true,
|
||||
"replace_plan": true
|
||||
}`;
|
||||
|
||||
export const OMO_LSP_PLACEHOLDER = `{
|
||||
"typescript-language-server": {
|
||||
"command": ["typescript-language-server", "--stdio"],
|
||||
"extensions": [".ts", ".tsx"],
|
||||
"priority": 10
|
||||
},
|
||||
"pylsp": {
|
||||
"disabled": true
|
||||
}
|
||||
}`;
|
||||
|
||||
export const OMO_EXPERIMENTAL_PLACEHOLDER = `{
|
||||
"truncate_all_tool_outputs": true,
|
||||
"aggressive_truncation": true,
|
||||
"auto_resume": true
|
||||
}`;
|
||||
|
||||
export const OMO_BACKGROUND_TASK_PLACEHOLDER = `{
|
||||
"defaultConcurrency": 5,
|
||||
"providerConcurrency": {
|
||||
"anthropic": 3,
|
||||
"openai": 5,
|
||||
"google": 10
|
||||
},
|
||||
"modelConcurrency": {
|
||||
"anthropic/claude-opus-4-6": 2,
|
||||
"google/gemini-3-flash": 10
|
||||
}
|
||||
}`;
|
||||
|
||||
export const OMO_BROWSER_AUTOMATION_PLACEHOLDER = `{
|
||||
"provider": "playwright"
|
||||
}`;
|
||||
|
||||
export const OMO_CLAUDE_CODE_PLACEHOLDER = `{
|
||||
"mcp": true,
|
||||
"commands": true,
|
||||
"skills": true,
|
||||
"agents": true,
|
||||
"hooks": true,
|
||||
"plugins": true
|
||||
}`;
|
||||
|
||||
export function mergeOmoConfigPreview(
|
||||
global: OmoGlobalConfig,
|
||||
agents: Record<string, Record<string, unknown>>,
|
||||
categories: Record<string, Record<string, unknown>>,
|
||||
otherFieldsStr: string,
|
||||
): Record<string, unknown> {
|
||||
const result: Record<string, unknown> = {};
|
||||
|
||||
if (global.schemaUrl) result["$schema"] = global.schemaUrl;
|
||||
|
||||
if (global.sisyphusAgent) result["sisyphus_agent"] = global.sisyphusAgent;
|
||||
if (global.disabledAgents?.length)
|
||||
result["disabled_agents"] = global.disabledAgents;
|
||||
if (global.disabledMcps?.length)
|
||||
result["disabled_mcps"] = global.disabledMcps;
|
||||
if (global.disabledHooks?.length)
|
||||
result["disabled_hooks"] = global.disabledHooks;
|
||||
if (global.disabledSkills?.length)
|
||||
result["disabled_skills"] = global.disabledSkills;
|
||||
if (global.lsp) result["lsp"] = global.lsp;
|
||||
if (global.experimental) result["experimental"] = global.experimental;
|
||||
if (global.backgroundTask) result["background_task"] = global.backgroundTask;
|
||||
if (global.browserAutomationEngine)
|
||||
result["browser_automation_engine"] = global.browserAutomationEngine;
|
||||
if (global.claudeCode) result["claude_code"] = global.claudeCode;
|
||||
|
||||
if (global.otherFields) {
|
||||
for (const [k, v] of Object.entries(global.otherFields)) {
|
||||
result[k] = v;
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(agents).length > 0) result["agents"] = agents;
|
||||
if (Object.keys(categories).length > 0) result["categories"] = categories;
|
||||
try {
|
||||
const other = JSON.parse(otherFieldsStr || "{}");
|
||||
for (const [k, v] of Object.entries(other)) {
|
||||
result[k] = v;
|
||||
}
|
||||
} catch {}
|
||||
|
||||
return result;
|
||||
}
|
||||
Reference in New Issue
Block a user