feat(copilot): move provider configuration to settings

This commit is contained in:
2026-07-12 22:27:29 +08:00
parent f39943629f
commit df23caf7d9
29 changed files with 916 additions and 258 deletions
@@ -645,12 +645,12 @@ const menuItems = computed(() => {
icon: 'i-lucide-briefcase',
to: accountScopedRoute('general_settings_index'),
},
// {
// name: 'Settings Captain',
// label: t('SIDEBAR.CAPTAIN_AI'),
// icon: 'i-woot-captain',
// to: accountScopedRoute('captain_settings_index'),
// },
{
name: 'Settings Captain',
label: t('SIDEBAR.CAPTAIN_AI'),
icon: 'i-woot-captain',
to: accountScopedRoute('captain_settings_index'),
},
{
name: 'Settings Agents',
label: t('SIDEBAR.AGENTS'),
@@ -382,15 +382,28 @@
},
"DOCS": "Read docs",
"SECURITY": "Security",
"CAPTAIN_AI": "Captain",
"CAPTAIN_AI": "Copilot Configuration",
"CONVERSATION_WORKFLOW": "Conversation Workflow"
},
"CAPTAIN_SETTINGS": {
"TITLE": "Captain Settings",
"DESCRIPTION": "Configure your AI models and features for Captain. Captain follows a credit based billing, you will be charged credits for every action Captain takes based on the model selected.",
"TITLE": "Copilot Configuration",
"DESCRIPTION": "Configure the AI provider and models used by Copilot. Configuration is stored securely and takes effect without restarting the service.",
"LOADING": "Loading Captain configuration...",
"LINK_TEXT": "Learn more about Captain Credits",
"NOT_ENABLED": "Captain is not enabled for your account. Please upgrade your plan to access Captain features.",
"PROVIDER": {
"TITLE": "Provider Configuration",
"DESCRIPTION": "Configure the provider connection used by all Copilot and Captain AI requests.",
"PROVIDER": "Provider",
"MODEL": "Default model",
"BASE_URL": "Base URL",
"API_KEY": "API key",
"API_KEY_PLACEHOLDER": "Enter API key",
"API_KEY_CONFIGURED": "Configured: {masked}. Leave blank to keep it unchanged.",
"API_KEY_HELP": "The API key is encrypted at rest and is never returned by the API.",
"SAVE": "Save provider configuration",
"SAVING": "Saving..."
},
"MODEL_CONFIG": {
"TITLE": "Model Configuration",
"DESCRIPTION": "Select AI models for different features.",
@@ -382,15 +382,28 @@
},
"DOCS": "阅读文档",
"SECURITY": "安全",
"CAPTAIN_AI": "Captain",
"CAPTAIN_AI": "Copilot 配置",
"CONVERSATION_WORKFLOW": "会话工作流"
},
"CAPTAIN_SETTINGS": {
"TITLE": "Captain Settings",
"DESCRIPTION": "Configure your AI models and features for Captain. Captain follows a credit based billing, you will be charged credits for every action Captain takes based on the model selected.",
"LOADING": "Loading Captain configuration...",
"LINK_TEXT": "Learn more about Captain Credits",
"NOT_ENABLED": "Captain is not enabled for your account. Please upgrade your plan to access Captain features.",
"TITLE": "Copilot 配置",
"DESCRIPTION": "配置 Copilot 使用的 AI 服务商和模型。保存后立即生效,无需重启服务。",
"LOADING": "正在加载 Copilot 配置...",
"LINK_TEXT": "了解 Copilot",
"NOT_ENABLED": "Copilot 配置暂不可用。",
"PROVIDER": {
"TITLE": "服务商配置",
"DESCRIPTION": "配置所有 Copilot 与 Captain AI 请求使用的模型服务。",
"PROVIDER": "服务商",
"MODEL": "默认模型",
"BASE_URL": "接口地址",
"API_KEY": "API Key",
"API_KEY_PLACEHOLDER": "请输入 API Key",
"API_KEY_CONFIGURED": "已配置:{masked}。留空表示保持不变。",
"API_KEY_HELP": "API Key 加密保存,接口不会返回明文。",
"SAVE": "保存服务商配置",
"SAVING": "保存中..."
},
"MODEL_CONFIG": {
"TITLE": "Model Configuration",
"DESCRIPTION": "Select AI models for different features.",
@@ -1,9 +1,8 @@
<script setup>
import { computed, onMounted } from 'vue';
import { computed, onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia';
import { useAlert } from 'dashboard/composables';
import { useCaptain } from 'dashboard/composables/useCaptain';
import { useCaptainConfigStore } from 'dashboard/store/captain/preferences';
import SettingsLayout from '../SettingsLayout.vue';
@@ -11,13 +10,12 @@ import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
import SectionLayout from '../account/components/SectionLayout.vue';
import ModelSelector from './components/ModelSelector.vue';
import FeatureToggle from './components/FeatureToggle.vue';
import CaptainPaywall from 'next/captain/pageComponents/Paywall.vue';
import ProviderConfiguration from './components/ProviderConfiguration.vue';
const { t } = useI18n();
const { captainEnabled } = useCaptain();
const captainConfigStore = useCaptainConfigStore();
const { uiFlags } = storeToRefs(captainConfigStore);
const { uiFlags, providerConfig } = storeToRefs(captainConfigStore);
const isSavingProvider = ref(false);
const isLoading = computed(() => uiFlags.value.isFetching);
@@ -75,6 +73,18 @@ async function handleModelChange({ feature, model }) {
}
}
async function handleProviderSave(config) {
isSavingProvider.value = true;
try {
await captainConfigStore.updatePreferences({ provider_config: config });
useAlert(t('CAPTAIN_SETTINGS.API.SUCCESS'));
} catch (error) {
useAlert(error.response?.data?.error || t('CAPTAIN_SETTINGS.API.ERROR'));
} finally {
isSavingProvider.value = false;
}
}
onMounted(() => {
captainConfigStore.fetch();
});
@@ -90,13 +100,22 @@ onMounted(() => {
<BaseSettingsHeader
:title="t('CAPTAIN_SETTINGS.TITLE')"
:description="t('CAPTAIN_SETTINGS.DESCRIPTION')"
:link-text="t('CAPTAIN_SETTINGS.LINK_TEXT')"
icon-name="captain"
feature-name="captain_billing"
/>
</template>
<template #body>
<div v-if="captainEnabled" class="flex flex-col gap-1">
<div class="flex flex-col gap-1">
<SectionLayout
:title="t('CAPTAIN_SETTINGS.PROVIDER.TITLE')"
:description="t('CAPTAIN_SETTINGS.PROVIDER.DESCRIPTION')"
>
<ProviderConfiguration
:config="providerConfig"
:is-saving="isSavingProvider"
@save="handleProviderSave"
/>
</SectionLayout>
<!-- Model Configuration Section -->
<SectionLayout
:title="t('CAPTAIN_SETTINGS.MODEL_CONFIG.TITLE')"
@@ -133,9 +152,6 @@ onMounted(() => {
</div>
</SectionLayout>
</div>
<div v-else>
<CaptainPaywall />
</div>
</template>
</SettingsLayout>
</template>
@@ -1,6 +1,4 @@
import { frontendURL } from '../../../../helper/URLHelper';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import { INSTALLATION_TYPES } from 'dashboard/constants/installationTypes';
import SettingsWrapper from '../SettingsWrapper.vue';
import Index from './Index.vue';
@@ -10,7 +8,6 @@ export default {
path: frontendURL('accounts/:accountId/settings/captain'),
meta: {
permissions: ['administrator'],
featureFlag: FEATURE_FLAGS.CAPTAIN,
},
component: SettingsWrapper,
props: {
@@ -25,11 +22,6 @@ export default {
component: Index,
meta: {
permissions: ['administrator'],
featureFlag: FEATURE_FLAGS.CAPTAIN,
installationTypes: [
INSTALLATION_TYPES.ENTERPRISE,
INSTALLATION_TYPES.CLOUD,
],
},
},
],
@@ -0,0 +1,140 @@
<script setup>
import { reactive, watch } from 'vue';
import { useI18n } from 'vue-i18n';
const props = defineProps({
config: {
type: Object,
default: () => ({}),
},
isSaving: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['save']);
const { t } = useI18n();
const form = reactive({
provider: 'openai',
baseUrl: 'https://api.openai.com/v1',
model: 'gpt-4o-mini',
apiKey: '',
});
const providerDefaults = {
openai: 'https://api.openai.com/v1',
anthropic: 'https://api.anthropic.com',
openai_compatible: '',
};
watch(
() => props.config,
config => {
form.provider = config?.provider || 'openai';
form.baseUrl =
config?.base_url || providerDefaults[form.provider] || '';
form.model = config?.model || 'gpt-4o-mini';
form.apiKey = '';
},
{ immediate: true, deep: true }
);
const handleProviderChange = () => {
form.baseUrl = providerDefaults[form.provider] || '';
};
const save = () => {
emit('save', {
provider: form.provider,
base_url: form.baseUrl,
model: form.model,
api_key: form.apiKey,
});
};
</script>
<template>
<div class="grid gap-4 p-4 rounded-xl border border-n-weak bg-n-solid-1">
<div class="grid gap-4 md:grid-cols-2">
<label class="grid gap-1.5 text-sm text-n-slate-12">
<span class="font-medium">{{
t('CAPTAIN_SETTINGS.PROVIDER.PROVIDER')
}}</span>
<select
v-model="form.provider"
class="w-full px-3 py-2 border rounded-lg border-n-weak bg-n-solid-2"
@change="handleProviderChange"
>
<option value="openai">OpenAI</option>
<option value="openai_compatible">OpenAI Compatible</option>
<option value="anthropic">Anthropic</option>
</select>
</label>
<label class="grid gap-1.5 text-sm text-n-slate-12">
<span class="font-medium">{{
t('CAPTAIN_SETTINGS.PROVIDER.MODEL')
}}</span>
<input
v-model.trim="form.model"
type="text"
class="w-full px-3 py-2 border rounded-lg border-n-weak bg-n-solid-2"
placeholder="gpt-4o-mini"
/>
</label>
</div>
<label class="grid gap-1.5 text-sm text-n-slate-12">
<span class="font-medium">{{
t('CAPTAIN_SETTINGS.PROVIDER.BASE_URL')
}}</span>
<input
v-model.trim="form.baseUrl"
type="url"
class="w-full px-3 py-2 border rounded-lg border-n-weak bg-n-solid-2"
placeholder="https://api.openai.com/v1"
/>
</label>
<label class="grid gap-1.5 text-sm text-n-slate-12">
<span class="font-medium">{{
t('CAPTAIN_SETTINGS.PROVIDER.API_KEY')
}}</span>
<input
v-model="form.apiKey"
type="password"
autocomplete="new-password"
class="w-full px-3 py-2 border rounded-lg border-n-weak bg-n-solid-2"
:placeholder="
config.api_key_configured
? t('CAPTAIN_SETTINGS.PROVIDER.API_KEY_CONFIGURED', {
masked: config.api_key_masked,
})
: t('CAPTAIN_SETTINGS.PROVIDER.API_KEY_PLACEHOLDER')
"
/>
<span class="text-xs text-n-slate-10">
{{ t('CAPTAIN_SETTINGS.PROVIDER.API_KEY_HELP') }}
</span>
</label>
<div class="flex justify-end">
<button
type="button"
class="px-4 py-2 text-sm font-medium text-white rounded-lg bg-n-brand hover:bg-n-brand/90 disabled:opacity-60"
:disabled="
isSaving || !form.provider || !form.baseUrl || !form.model
"
@click="save"
>
{{
isSaving
? t('CAPTAIN_SETTINGS.PROVIDER.SAVING')
: t('CAPTAIN_SETTINGS.PROVIDER.SAVE')
}}
</button>
</div>
</div>
</template>
@@ -6,6 +6,7 @@ export const useCaptainConfigStore = defineStore('captainConfig', {
providers: {},
models: {},
features: {},
providerConfig: {},
uiFlags: {
isFetching: false,
},
@@ -54,6 +55,7 @@ export const useCaptainConfigStore = defineStore('captainConfig', {
this.providers = response.data.providers || {};
this.models = response.data.models || {};
this.features = response.data.features || {};
this.providerConfig = response.data.provider_config || {};
} catch (error) {
// Ignore error
} finally {
@@ -66,6 +68,7 @@ export const useCaptainConfigStore = defineStore('captainConfig', {
this.providers = response.data.providers || {};
this.models = response.data.models || {};
this.features = response.data.features || {};
this.providerConfig = response.data.provider_config || {};
},
},
});