feat(ollama): 支持 Cloud 官方用量自动刷新

This commit is contained in:
alfadb
2026-07-23 15:50:44 +08:00
parent 2c76506e07
commit 5ac4a9fac2
47 changed files with 5571 additions and 85 deletions
+1 -1
View File
@@ -1407,8 +1407,8 @@ const allColumns = computed(() => {
if (!authStore.isSimpleMode) {
c.push({ key: 'groups', label: t('admin.accounts.columns.groups'), sortable: false })
}
c.push({ key: 'usage', label: t('admin.accounts.columns.usageWindows'), sortable: false })
c.push(
{ key: 'usage', label: t('admin.accounts.columns.usageWindows'), sortable: false },
{ key: 'proxy', label: t('admin.accounts.columns.proxy'), sortable: false },
{ key: 'priority', label: t('admin.accounts.columns.priority'), sortable: true },
{ key: 'scheduler_score', label: t('admin.accounts.columns.schedulerScore'), sortable: false },
+103
View File
@@ -4189,6 +4189,70 @@
</div>
</div>
<!-- Ollama Cloud Usage Settings -->
<div class="card" data-testid="ollama-cloud-usage-global-settings">
<div class="border-b border-gray-100 px-6 py-4 dark:border-dark-700">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
{{ t("admin.settings.ollamaCloudUsage.title") }}
</h2>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{{ t("admin.settings.ollamaCloudUsage.description") }}
</p>
</div>
<div class="space-y-5 p-6">
<div v-if="ollamaCloudUsageLoading" class="flex items-center gap-2 text-gray-500">
<div class="h-4 w-4 animate-spin rounded-full border-b-2 border-primary-600"></div>
{{ t("common.loading") }}
</div>
<template v-else>
<div class="flex items-center justify-between gap-4">
<div>
<label class="font-medium text-gray-900 dark:text-white">
{{ t("admin.settings.ollamaCloudUsage.enabled") }}
</label>
<p class="text-sm text-gray-500 dark:text-gray-400">
{{ t("admin.settings.ollamaCloudUsage.enabledHint") }}
</p>
</div>
<Toggle
v-model="ollamaCloudUsageForm.enabled"
:aria-label="t('admin.settings.ollamaCloudUsage.enabled')"
data-testid="ollama-cloud-usage-global-enabled"
/>
</div>
<div v-if="ollamaCloudUsageForm.enabled" class="border-t border-gray-100 pt-4 dark:border-dark-700">
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300" for="ollama-cloud-usage-interval">
{{ t("admin.settings.ollamaCloudUsage.intervalMinutes") }}
</label>
<input
id="ollama-cloud-usage-interval"
v-model.number="ollamaCloudUsageForm.interval_minutes"
type="number"
min="15"
max="1440"
class="input w-32"
data-testid="ollama-cloud-usage-global-interval"
@keydown.enter.prevent="saveOllamaCloudUsageSettings"
/>
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
{{ t("admin.settings.ollamaCloudUsage.intervalHint") }}
</p>
</div>
<div class="flex justify-end border-t border-gray-100 pt-4 dark:border-dark-700">
<button
type="button"
class="btn btn-primary btn-sm"
:disabled="ollamaCloudUsageSaving"
data-testid="ollama-cloud-usage-global-save"
@click="saveOllamaCloudUsageSettings"
>
{{ ollamaCloudUsageSaving ? t("common.saving") : t("common.save") }}
</button>
</div>
</template>
</div>
</div>
<!-- Gateway Scheduling Settings -->
<div class="card">
<div
@@ -7865,6 +7929,13 @@ const upstreamBillingProbeForm = reactive({
interval_minutes: 30,
});
const ollamaCloudUsageLoading = ref(true);
const ollamaCloudUsageSaving = ref(false);
const ollamaCloudUsageForm = reactive({
enabled: false,
interval_minutes: 60,
});
// Overload Cooldown (529) 状态
const overloadCooldownLoading = ref(true);
const overloadCooldownSaving = ref(false);
@@ -10481,6 +10552,37 @@ async function saveUpstreamBillingProbeSettings() {
}
}
async function loadOllamaCloudUsageSettings() {
ollamaCloudUsageLoading.value = true;
try {
Object.assign(
ollamaCloudUsageForm,
await adminAPI.accounts.getOllamaCloudUsageSettings(),
);
} catch (_error: unknown) {
// Keep the fail-safe disabled defaults when this optional setting cannot be loaded.
} finally {
ollamaCloudUsageLoading.value = false;
}
}
async function saveOllamaCloudUsageSettings() {
ollamaCloudUsageSaving.value = true;
try {
const updated = await adminAPI.accounts.updateOllamaCloudUsageSettings({
...ollamaCloudUsageForm,
});
Object.assign(ollamaCloudUsageForm, updated);
appStore.showSuccess(t("admin.settings.ollamaCloudUsage.saved"));
} catch (error: unknown) {
appStore.showError(
extractApiErrorMessage(error, t("admin.settings.ollamaCloudUsage.saveFailed")),
);
} finally {
ollamaCloudUsageSaving.value = false;
}
}
// Overload Cooldown 方法
async function loadOverloadCooldownSettings() {
overloadCooldownLoading.value = true;
@@ -11178,6 +11280,7 @@ onMounted(() => {
loadSubscriptionGroups();
loadAdminApiKey();
loadUpstreamBillingProbeSettings();
loadOllamaCloudUsageSettings();
loadOverloadCooldownSettings();
loadRateLimit429CooldownSettings();
loadStreamTimeoutSettings();
@@ -166,6 +166,16 @@ describe('admin AccountsView usage windows hint', () => {
expect(hint.text()).toBe('admin.accounts.usageWindowsHint')
})
it('keeps Ollama Cloud in the single usage column and ignores legacy column preferences', async () => {
localStorage.setItem('account-hidden-columns', JSON.stringify(['ollama_cloud_usage']))
const wrapper = mountView()
await flushPromises()
const columns = wrapper.getComponent(DataTableStub).props('columns') as Array<{ key: string }>
expect(columns.filter(column => column.key === 'usage')).toHaveLength(1)
expect(columns.some(column => column.key === 'ollama_cloud_usage')).toBe(false)
})
it('renders the upstream billing trust warning next to the declared-rate column', async () => {
const wrapper = mountView()
await flushPromises()
@@ -18,6 +18,8 @@ const {
getBetaPolicySettings,
getUpstreamBillingProbeSettings,
updateUpstreamBillingProbeSettings,
getOllamaCloudUsageSettings,
updateOllamaCloudUsageSettings,
getGroups,
listProxies,
getProviders,
@@ -45,6 +47,11 @@ const {
interval_minutes: 30,
}),
updateUpstreamBillingProbeSettings: vi.fn().mockImplementation(async (payload) => payload),
getOllamaCloudUsageSettings: vi.fn().mockResolvedValue({
enabled: false,
interval_minutes: 60,
}),
updateOllamaCloudUsageSettings: vi.fn().mockImplementation(async (payload) => payload),
getGroups: vi.fn(),
listProxies: vi.fn(),
getProviders: vi.fn(),
@@ -77,6 +84,8 @@ vi.mock("@/api", () => ({
accounts: {
getUpstreamBillingProbeSettings,
updateUpstreamBillingProbeSettings,
getOllamaCloudUsageSettings,
updateOllamaCloudUsageSettings,
},
groups: {
getAll: getGroups,
@@ -573,6 +582,8 @@ describe("admin SettingsView payment visible method controls", () => {
getBetaPolicySettings.mockReset();
getUpstreamBillingProbeSettings.mockReset();
updateUpstreamBillingProbeSettings.mockReset();
getOllamaCloudUsageSettings.mockReset();
updateOllamaCloudUsageSettings.mockReset();
getGroups.mockReset();
listProxies.mockReset();
getProviders.mockReset();
@@ -633,6 +644,11 @@ describe("admin SettingsView payment visible method controls", () => {
interval_minutes: 30,
});
updateUpstreamBillingProbeSettings.mockImplementation(async (payload) => payload);
getOllamaCloudUsageSettings.mockResolvedValue({
enabled: false,
interval_minutes: 60,
});
updateOllamaCloudUsageSettings.mockImplementation(async (payload) => payload);
getGroups.mockResolvedValue([]);
listProxies.mockResolvedValue({
items: [],
@@ -976,6 +992,31 @@ describe("admin SettingsView payment visible method controls", () => {
expect(showSuccess).toHaveBeenCalledWith("上游倍率自动探测设置已保存");
});
it("loads fail-safe-off Ollama Cloud usage refresh settings and saves an explicit opt-in", async () => {
const wrapper = mountView();
await flushPromises();
await openGatewayTab(wrapper);
const card = wrapper.get('[data-testid="ollama-cloud-usage-global-settings"]');
expect(card.isVisible()).toBe(true);
expect(
(card.get('[data-testid="ollama-cloud-usage-global-enabled"]').element as HTMLInputElement)
.checked,
).toBe(false);
expect(card.find('[data-testid="ollama-cloud-usage-global-interval"]').exists()).toBe(false);
await card.get('[data-testid="ollama-cloud-usage-global-enabled"]').setValue(true);
await card.get('[data-testid="ollama-cloud-usage-global-interval"]').setValue(90);
await card.get('[data-testid="ollama-cloud-usage-global-save"]').trigger("click");
await flushPromises();
expect(updateOllamaCloudUsageSettings).toHaveBeenCalledWith({
enabled: true,
interval_minutes: 90,
});
});
it("places and explains rate controls for both scheduling modes", async () => {
const wrapper = mountView();