feat(zhipu): support team GLM Coding Plan usage query

Team GLM Coding Plan accounts fail the quota probe with
"当前用户不存在coding plan" because the personal-plan query path
does not carry team context.

- quota probe: when credentials contain zhipu_organization, switch the
  Zhipu quota endpoint to the team variant (?type=2 + bigmodel-organization
  / bigmodel-project headers); personal plans keep the existing path
- account create/edit modals: optional organization/project ID inputs for
  zhipu coding-plan accounts, persisted in credentials; edit modal
  backfills them and clears back to personal plan when emptied
- add a click-to-open tooltip tutorial (zh/en) explaining how to grab the
  IDs from the bigmodel.cn web console via the /api/biz/v1/organization
  network request, with a sample URL mapping org-/proj_ segments to fields
This commit is contained in:
wall-wxk
2026-08-28 16:36:30 +08:00
parent e866ff6ec4
commit c4e46c3be8
5 changed files with 136 additions and 0 deletions
@@ -547,6 +547,36 @@
</div>
</div>
<!-- Zhipu 团队版 Coding Plan:组织/项目 ID(可选,填写后额度探测走团队版端点) -->
<div v-if="form.platform === 'zhipu' && accountMode === 'coding'" class="mt-4">
<div class="flex items-center">
<label class="input-label">{{ t('admin.accounts.cnProviders.zhipuTeam.title') }}</label>
<HelpTooltip trigger="click" width-class="w-80">
<p class="mb-1 font-medium">{{ t('admin.accounts.cnProviders.zhipuTeam.help.title') }}</p>
<ol class="list-decimal space-y-1 pl-4">
<li>{{ t('admin.accounts.cnProviders.zhipuTeam.help.step1') }}</li>
<li>{{ t('admin.accounts.cnProviders.zhipuTeam.help.step2') }}</li>
<li>{{ t('admin.accounts.cnProviders.zhipuTeam.help.step3') }}</li>
<li>{{ t('admin.accounts.cnProviders.zhipuTeam.help.step4') }}</li>
</ol>
<p class="mt-2 break-all rounded bg-black/20 p-1.5 font-mono text-[11px] leading-relaxed">
{{ t('admin.accounts.cnProviders.zhipuTeam.help.example') }}
</p>
</HelpTooltip>
</div>
<div class="mt-2 grid gap-4 sm:grid-cols-2">
<div>
<label class="input-label">{{ t('admin.accounts.cnProviders.zhipuTeam.organization') }}</label>
<input v-model="zhipuOrganization" type="text" class="input" :placeholder="t('admin.accounts.cnProviders.zhipuTeam.organizationPlaceholder')" />
</div>
<div>
<label class="input-label">{{ t('admin.accounts.cnProviders.zhipuTeam.project') }}</label>
<input v-model="zhipuProject" type="text" class="input" :placeholder="t('admin.accounts.cnProviders.zhipuTeam.projectPlaceholder')" />
</div>
</div>
<p class="input-hint mt-2">{{ t('admin.accounts.cnProviders.zhipuTeam.hint') }}</p>
</div>
<!-- Account Type Selection (Gemini) -->
<div v-if="form.platform === 'gemini'">
<div class="flex items-center justify-between">
@@ -3764,6 +3794,7 @@ import type {
import BaseDialog from '@/components/common/BaseDialog.vue'
import ConfirmDialog from '@/components/common/ConfirmDialog.vue'
import Select from '@/components/common/Select.vue'
import HelpTooltip from '@/components/common/HelpTooltip.vue'
import PlatformIcon from '@/components/common/PlatformIcon.vue'
import Icon from '@/components/icons/Icon.vue'
import ProxySelector from '@/components/common/ProxySelector.vue'
@@ -3969,6 +4000,9 @@ const accountMode = ref<CnAccountMode>('payg')
// API 协议决定转发端点与格式:cc=现有转换链,anthropic=原生直通(Claude Code),
// responses=deepseek 原生 Responses 端点(Codex)。与账号类型正交。
const apiProtocol = ref<CnApiProtocol>('adaptive')
// 智谱团队版 Coding Plan:组织/项目 ID,写入 credentials 供额度探测切换团队端点
const zhipuOrganization = ref('')
const zhipuProject = ref('')
const adaptiveBaseUrls = ref<Record<CnNativeApiProtocol, string>>({
chat_completions: '',
anthropic: '',
@@ -5544,6 +5578,11 @@ const handleSubmit = async () => {
if (apiProtocol.value !== 'adaptive' && resolvedCNBase) {
credentials.base_url = resolvedCNBase
}
// 智谱团队版 Coding Plan:组织/项目 ID 写入凭据(非空才写)
if (form.platform === 'zhipu' && accountMode.value === 'coding') {
if (zhipuOrganization.value.trim()) credentials.zhipu_organization = zhipuOrganization.value.trim()
if (zhipuProject.value.trim()) credentials.zhipu_project = zhipuProject.value.trim()
}
}
// Add model mapping if configured(OpenAI 开启自动透传时不应用)
@@ -118,6 +118,35 @@
</div>
<p class="input-hint">{{ t(`admin.accounts.cnProviders.apiProtocol.${cnProtocolDescKey}Desc`) }}</p>
</div>
<!-- Zhipu 团队版 Coding Plan:组织/项目 ID(可选,填写后用量查询走团队版端点) -->
<div v-if="account.platform === 'zhipu' && editAccountMode === 'coding'">
<div class="flex items-center">
<label class="input-label">{{ t('admin.accounts.cnProviders.zhipuTeam.title') }}</label>
<HelpTooltip trigger="click" width-class="w-80">
<p class="mb-1 font-medium">{{ t('admin.accounts.cnProviders.zhipuTeam.help.title') }}</p>
<ol class="list-decimal space-y-1 pl-4">
<li>{{ t('admin.accounts.cnProviders.zhipuTeam.help.step1') }}</li>
<li>{{ t('admin.accounts.cnProviders.zhipuTeam.help.step2') }}</li>
<li>{{ t('admin.accounts.cnProviders.zhipuTeam.help.step3') }}</li>
<li>{{ t('admin.accounts.cnProviders.zhipuTeam.help.step4') }}</li>
</ol>
<p class="mt-2 break-all rounded bg-black/20 p-1.5 font-mono text-[11px] leading-relaxed">
{{ t('admin.accounts.cnProviders.zhipuTeam.help.example') }}
</p>
</HelpTooltip>
</div>
<div class="mt-2 grid gap-4 sm:grid-cols-2">
<div>
<label class="input-label">{{ t('admin.accounts.cnProviders.zhipuTeam.organization') }}</label>
<input v-model="editZhipuOrganization" type="text" class="input" :placeholder="t('admin.accounts.cnProviders.zhipuTeam.organizationPlaceholder')" />
</div>
<div>
<label class="input-label">{{ t('admin.accounts.cnProviders.zhipuTeam.project') }}</label>
<input v-model="editZhipuProject" type="text" class="input" :placeholder="t('admin.accounts.cnProviders.zhipuTeam.projectPlaceholder')" />
</div>
</div>
<p class="input-hint mt-2">{{ t('admin.accounts.cnProviders.zhipuTeam.hint') }}</p>
</div>
<div>
<label class="input-label">{{ t('admin.accounts.apiKey') }}</label>
<input
@@ -2854,6 +2883,7 @@ import type {
import BaseDialog from '@/components/common/BaseDialog.vue'
import ConfirmDialog from '@/components/common/ConfirmDialog.vue'
import Select from '@/components/common/Select.vue'
import HelpTooltip from '@/components/common/HelpTooltip.vue'
import Toggle from '@/components/common/Toggle.vue'
import Icon from '@/components/icons/Icon.vue'
import ProxySelector from '@/components/common/ProxySelector.vue'
@@ -2987,6 +3017,9 @@ const cnPresetPlatform = computed<'kimi' | 'zhipu' | 'deepseek'>(() => {
})
const editApiProtocol = ref<CnApiProtocol>('adaptive')
const editAccountMode = ref<CnAccountMode>('payg')
// 智谱团队版 Coding Plan:组织/项目 ID,写入 credentials 供额度探测切换团队端点
const editZhipuOrganization = ref('')
const editZhipuProject = ref('')
const editAdaptiveBaseUrls = ref<Record<CnNativeApiProtocol, string>>({
chat_completions: '',
anthropic: '',
@@ -3939,6 +3972,11 @@ const syncFormFromAccount = (newAccount: Account | null) => {
nextAdaptiveBaseUrls[legacyProtocol] = legacyBaseUrl
}
editAdaptiveBaseUrls.value = nextAdaptiveBaseUrls
// 智谱团队版 Coding Plan:回填组织/项目 ID
if (newAccount.platform === 'zhipu') {
editZhipuOrganization.value = typeof credentials.zhipu_organization === 'string' ? credentials.zhipu_organization : ''
editZhipuProject.value = typeof credentials.zhipu_project === 'string' ? credentials.zhipu_project : ''
}
}
const platformDefaultUrl =
newAccount.platform === 'openai'
@@ -4648,6 +4686,19 @@ const handleSubmit = async () => {
} else {
delete newCredentials.api_base_urls
}
// 智谱团队版 Coding Plan:组织/项目 ID 写入凭据(非空才写,清空即移除回落个人版路径)
if (props.account.platform === 'zhipu') {
const org = editZhipuOrganization.value.trim()
const project = editZhipuProject.value.trim()
if (org) {
newCredentials.zhipu_organization = org
if (project) newCredentials.zhipu_project = project
else delete newCredentials.zhipu_project
} else {
delete newCredentials.zhipu_organization
delete newCredentials.zhipu_project
}
}
}
// Handle API key