diff --git a/packages/client/src/api/hermes/copilot-auth.ts b/packages/client/src/api/hermes/copilot-auth.ts new file mode 100644 index 00000000..e2733f84 --- /dev/null +++ b/packages/client/src/api/hermes/copilot-auth.ts @@ -0,0 +1,42 @@ +import { request } from '../client' + +export type CopilotTokenSource = 'env' | 'gh-cli' | 'apps-json' | null + +export interface CopilotStartResult { + session_id: string + user_code: string + verification_url: string + expires_in: number + interval: number +} + +export interface CopilotPollResult { + status: 'pending' | 'approved' | 'denied' | 'expired' | 'error' + error: string | null +} + +export interface CopilotCheckTokenResult { + has_token: boolean + source: CopilotTokenSource + enabled: boolean +} + +export async function startCopilotLogin(): Promise { + return request('/api/hermes/auth/copilot/start', { method: 'POST' }) +} + +export async function pollCopilotLogin(sessionId: string): Promise { + return request(`/api/hermes/auth/copilot/poll/${sessionId}`) +} + +export async function checkCopilotToken(): Promise { + return request('/api/hermes/auth/copilot/check-token') +} + +export async function enableCopilot(): Promise<{ ok: boolean }> { + return request<{ ok: boolean }>('/api/hermes/auth/copilot/enable', { method: 'POST' }) +} + +export async function disableCopilot(): Promise<{ ok: boolean; cleared_env: boolean; cleared_default?: boolean }> { + return request<{ ok: boolean; cleared_env: boolean; cleared_default?: boolean }>('/api/hermes/auth/copilot/disable', { method: 'POST' }) +} diff --git a/packages/client/src/api/hermes/system.ts b/packages/client/src/api/hermes/system.ts index fc359be9..920c3488 100644 --- a/packages/client/src/api/hermes/system.ts +++ b/packages/client/src/api/hermes/system.ts @@ -31,6 +31,8 @@ export interface AvailableModelGroup { base_url: string models: string[] api_key: string + /** 可选:模型 ID -> 元数据(preview/disabled)。目前仅 Copilot 提供。 */ + model_meta?: Record } export interface AvailableModelsResponse { diff --git a/packages/client/src/components/hermes/models/CopilotLoginModal.vue b/packages/client/src/components/hermes/models/CopilotLoginModal.vue new file mode 100644 index 00000000..179106e1 --- /dev/null +++ b/packages/client/src/components/hermes/models/CopilotLoginModal.vue @@ -0,0 +1,243 @@ + + + + + diff --git a/packages/client/src/components/hermes/models/ProviderCard.vue b/packages/client/src/components/hermes/models/ProviderCard.vue index 9aad9448..14f1b793 100644 --- a/packages/client/src/components/hermes/models/ProviderCard.vue +++ b/packages/client/src/components/hermes/models/ProviderCard.vue @@ -3,29 +3,65 @@ import { ref, computed } from 'vue' import { NButton, useMessage, useDialog } from 'naive-ui' import type { AvailableModelGroup } from '@/api/hermes/system' import { useModelsStore } from '@/stores/hermes/models' +import { useAppStore } from '@/stores/hermes/app' +import { useChatStore } from '@/stores/hermes/chat' +import { checkCopilotToken, disableCopilot } from '@/api/hermes/copilot-auth' import { useI18n } from 'vue-i18n' const props = defineProps<{ provider: AvailableModelGroup }>() const { t } = useI18n() const modelsStore = useModelsStore() +const appStore = useAppStore() +const chatStore = useChatStore() const message = useMessage() const dialog = useDialog() const isCustom = computed(() => props.provider.provider.startsWith('custom:')) +const isCopilot = computed(() => props.provider.provider === 'copilot') const displayName = computed(() => props.provider.label) const deleting = ref(false) async function handleDelete() { + let copilotMsg = '' + if (isCopilot.value) { + // 提前查 source,让用户清楚移除会不会影响 VS Code/gh CLI 等其他工具的登录态 + try { + const status = await checkCopilotToken() + if (status.source === 'env') copilotMsg = t('models.copilotDeleteHintEnv') + else if (status.source === 'gh-cli') copilotMsg = t('models.copilotDeleteHintGhCli') + else if (status.source === 'apps-json') copilotMsg = t('models.copilotDeleteHintAppsJson') + } catch { /* ignore — fall back to generic confirm copy */ } + } dialog.warning({ title: t('models.deleteProvider'), - content: t('models.deleteConfirm', { name: displayName.value }), + content: isCopilot.value && copilotMsg + ? `${t('models.deleteConfirm', { name: displayName.value })}\n\n${copilotMsg}` + : t('models.deleteConfirm', { name: displayName.value }), positiveText: t('common.delete'), negativeText: t('common.cancel'), onPositiveClick: async () => { deleting.value = true try { - await modelsStore.removeProvider(props.provider.provider) + if (isCopilot.value) { + // Copilot 走显式 opt-in 模型:disable 把 enabled 置 false, + // 仅当 token 来自 ~/.hermes/.env 时才清掉,gh-cli / apps.json 不动。 + await disableCopilot() + // 服务端会在默认模型属于 copilot 时清掉 model.default,这里再清理本地 + // 会话级 model/provider,避免 Chat 页继续显示已下架的 copilot 模型。 + chatStore.clearProviderFromSessions('copilot') + await Promise.all([modelsStore.fetchProviders(), appStore.loadModels()]) + } else { + await modelsStore.removeProvider(props.provider.provider) + } + // 删完之后若已没有默认模型,自动从剩余 provider 里挑一个,避免 chat 页 + // "无默认模型"的尴尬态。与 hermes CLI `model` 子命令的隐含行为对齐。 + if (!appStore.selectedModel && appStore.modelGroups.length > 0) { + const first = appStore.modelGroups.find(g => g.models.length > 0) + if (first) { + await appStore.switchModel(first.models[0], first.provider) + } + } message.success(t('models.providerDeleted')) } catch (e: any) { message.error(e.message) diff --git a/packages/client/src/components/hermes/models/ProviderFormModal.vue b/packages/client/src/components/hermes/models/ProviderFormModal.vue index a574e5d1..997cefce 100644 --- a/packages/client/src/components/hermes/models/ProviderFormModal.vue +++ b/packages/client/src/components/hermes/models/ProviderFormModal.vue @@ -1,10 +1,12 @@