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:
Dex Miller
2026-02-09 12:18:20 +08:00
committed by GitHub
parent 5502c74a79
commit ed47480fc7
35 changed files with 4304 additions and 737 deletions
+28 -14
View File
@@ -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: "切换失败" }),
{
+95
View File
@@ -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),
});
}