diff --git a/.dockerignore b/.dockerignore index d5369692cb..c6167a25b2 100644 --- a/.dockerignore +++ b/.dockerignore @@ -49,6 +49,9 @@ coverage/ .env.* !.env.example +# 本地闭源插件目录可能包含发布签名私钥,绝不能进入 Docker 构建上下文。 +/plugins/ + # Local config config.yaml config.local.yaml diff --git a/.github/audit-exceptions.yml b/.github/audit-exceptions.yml index 2a89dd0761..6243f0fa7f 100644 --- a/.github/audit-exceptions.yml +++ b/.github/audit-exceptions.yml @@ -35,3 +35,10 @@ exceptions: mitigation: "Proxy configuration not user-controlled; upgrade when axios releases fix" expires_on: "2026-07-10" owner: "security@your-domain" + - package: nanoid + advisory: "GHSA-2v37-7h3g-55p8" + severity: high + reason: "Custom generator with size=0 not used; all nanoid calls use default size or positive values" + mitigation: "No zero-size custom generators in codebase; upgrade when nanoid releases fix" + expires_on: "2026-10-13" + owner: "security@your-domain" diff --git a/.github/workflows/backend-ci.yml b/.github/workflows/backend-ci.yml index 21a4fea79c..2bac7259a9 100644 --- a/.github/workflows/backend-ci.yml +++ b/.github/workflows/backend-ci.yml @@ -17,6 +17,7 @@ jobs: /bin/bash -n deploy/apple-container.sh /bin/bash deploy/tests/apple-container-test.sh /bin/sh deploy/tests/docker-compose-security-test.sh + /bin/sh deploy/tests/docker-compose-gateway-env-test.sh /bin/sh deploy/tests/docker-runtime-resources-test.sh /bin/sh deploy/test-caddyfile-cache.sh @@ -32,7 +33,7 @@ jobs: cache-dependency-path: backend/go.sum - name: Verify Go version run: | - go version | grep -q 'go1.26.5' + go version | grep -q 'go1.27.0' - name: Unit tests working-directory: backend run: make test-unit @@ -72,10 +73,10 @@ jobs: cache-dependency-path: backend/go.sum - name: Verify Go version run: | - go version | grep -q 'go1.26.5' + go version | grep -q 'go1.27.0' - name: golangci-lint uses: golangci/golangci-lint-action@v9 with: - version: v2.9 + version: v2.13 args: --timeout=30m working-directory: backend diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2ba01833c1..abdfc9c81d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -115,7 +115,7 @@ jobs: - name: Verify Go version run: | - go version | grep -q 'go1.26.5' + go version | grep -q 'go1.27.0' # Docker setup for GoReleaser - name: Set up QEMU diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index 96a7ae4edd..97357efde2 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -23,7 +23,7 @@ jobs: cache-dependency-path: backend/go.sum - name: Verify Go version run: | - go version | grep -q 'go1.26.5' + go version | grep -q 'go1.27.0' - name: Run govulncheck working-directory: backend run: | diff --git a/.gitignore b/.gitignore index 4ea6ff311b..20ed12275e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ -docs/claude-relay-service/ +# 本地杂项文档、测试数据和外部项目副本 +/docs-local/ .codex # =================== @@ -136,13 +137,23 @@ docs/* !docs/PAYMENT_CN.md !docs/ADMIN_PAYMENT_INTEGRATION_API.md !docs/ASYNC_IMAGE_TASKS.md +!docs/BATCH_IMAGE_MVP.md +!docs/COMPOSITE_GROUPS.md +!docs/PLUGIN_DEVELOPMENT.md +!docs/channel-monitor-v2-safe-defaults.md !docs/legal/ !docs/legal/*.md +!docs/screenshots/ +docs/screenshots/* +!docs/screenshots/mobile-account-actions-menu.png .serena/ .codex/ frontend/coverage/ aicodex output/ +.codegraph/ # Vitest / Vite cache at repo root .vite/ +# 本地闭源插件开发目录及构建产物不进入 Sub2API 仓库 +/plugins/ diff --git a/DEV_GUIDE.md b/DEV_GUIDE.md index 349c244491..2f440d0f62 100644 --- a/DEV_GUIDE.md +++ b/DEV_GUIDE.md @@ -34,8 +34,8 @@ ### 开发工具 ```bash -# golangci-lint(CI 用 v2.9,本地建议装同一版以免版本差异带来的噪音) -go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.9 +# golangci-lint(CI 用 v2.13,本地建议装同一版以免版本差异带来的噪音) +go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.13 # pnpm (前端包管理) npm install -g pnpm @@ -47,13 +47,13 @@ npm install -g pnpm | Workflow | 触发条件 | 检查内容 | |----------|----------|----------| -| **backend-ci.yml** | push, pull_request | 单元测试 + 集成测试 + golangci-lint v2.9 | +| **backend-ci.yml** | push, pull_request | 单元测试 + 集成测试 + golangci-lint v2.13 | | **security-scan.yml** | push, pull_request, 每周一 | govulncheck + gosec + pnpm audit | | **release.yml** | tag `v*` | 构建发布(PR 不触发) | ### CI 要求 -- Go 版本必须是 **1.26.5**:三个 workflow 都用 `go-version-file: backend/go.mod` 取版本,随后硬断言 `go version | grep -q 'go1.26.5'`。升级 Go 时要同时改 `backend/go.mod` 和 `backend-ci.yml`(两处)、`release.yml`、`security-scan.yml` 里的这句断言,否则 CI 会在版本校验步骤直接失败。 +- Go 版本必须是 **1.27.0**:三个 workflow 都用 `go-version-file: backend/go.mod` 取版本,随后硬断言 `go version | grep -q 'go1.27.0'`。升级 Go 时要同时改 `backend/go.mod`、`backend-ci.yml`(两处)、`release.yml`、`security-scan.yml` 里的这句断言,**以及三个 Dockerfile 里的 Go 构建镜像**(`Dockerfile` / `deploy/Dockerfile` 的 `ARG GOLANG_IMAGE`、`backend/Dockerfile` 的 `FROM golang:`)。前者漏了 CI 会在版本校验步骤直接失败;**后者漏了 CI 不会报,而是等到有人用这些 Dockerfile 构建时才失败**(`go.mod requires go >= X (running Y; GOTOOLCHAIN=local)`)。 - 前端使用 `pnpm install --frozen-lockfile`,必须提交 `pnpm-lock.yaml` ### 本地测试命令 @@ -203,7 +203,7 @@ go test -tags=integration ./... **解决**: ```bash cd backend -go generate ./ent # 重新生成 ent 代码 +go generate ./ent # 重新生成 ent 代码(json.RawMessage 字段会生成为同类型的 jsontext.Value,属预期) git add ent/ # 生成的文件也要提交 ``` diff --git a/Dockerfile b/Dockerfile index b2dfd7034f..adfa4d4702 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,7 @@ # ============================================================================= ARG NODE_IMAGE=node:24-alpine -ARG GOLANG_IMAGE=golang:1.26.5-alpine +ARG GOLANG_IMAGE=golang:1.27.0-alpine ARG ALPINE_IMAGE=alpine:3.21 ARG POSTGRES_IMAGE=postgres:18-alpine ARG GOPROXY=https://goproxy.cn,direct diff --git a/README.md b/README.md index f4ad2fa6b0..5d0c4f433a 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ # Sub2API -[](https://golang.org/) +[](https://golang.org/) [](https://vuejs.org/) [](https://www.postgresql.org/) [](https://redis.io/) @@ -58,6 +58,11 @@ Please read the following carefully before using this project:


















+ {{ t('admin.accounts.openai.longContextBillingDesc') }} +
++ {{ t('admin.accounts.bulkEdit.longContextShadowHint') }} +
++ {{ t('admin.accounts.openai.endpointCapabilitiesDesc') }} +
++ {{ t('admin.accounts.openai.responsesModeDesc') }} +
++ {{ t('admin.accounts.openai.responsesModeTextDisabledHint') }} +
+{{ baseUrlHint }}
+ {{ t('admin.accounts.cnProviders.apiProtocol.responsesFallbackDesc') }} +
{{ apiKeyHint }}
{{ t('admin.accounts.selectedModels', { count: allowedModels.length }) }} {{ @@ -1532,7 +1685,7 @@
{{ t('admin.accounts.selectedModels', { count: allowedModels.length }) }} {{ t('admin.accounts.supportsAllModels') }} @@ -2072,7 +2230,12 @@
{{ t('admin.accounts.selectedModels', { count: allowedModels.length }) }}
{{
@@ -2829,6 +2992,7 @@
+ {{ t('admin.accounts.cnProviders.apiProtocol.responsesFallbackDesc') }}
+ {{ t(`admin.accounts.cnProviders.accountMode.${editAccountMode}Desc`) }} {{ t(`admin.accounts.cnProviders.apiProtocol.${cnProtocolDescKey}Desc`) }}
+ {{ t('admin.accounts.autoResetCredit.hint') }}
+ {{ t('admin.accounts.autoResetCredit.thresholdHint') }} {{ t('admin.channelMonitor.form.linkedAccountHint') }}
+ {{ t('admin.channelMonitor.form.openAIQuotaProbeHint') }}
+
+ {{ t('admin.channelMonitor.form.linkedAccountMissing') }}
+
+ {{ t('admin.channelMonitor.form.linkedAccountEmpty') }}
+ {{ editing.api_key_masked }} {{ t('admin.users.form.concurrencyHint') }}
+ {{ t('keys.useKeyModal.codexModelCatalog.description') }}
+
+ {{ codexModelCatalogPath }}
+
+ {{ t('keys.useKeyModal.codexModelCatalog.modelsCount', { count: codexModelManifestModelCount }) }}
+
+ {{ t('keys.useKeyModal.codexModelCatalog.errorDescription') }}
+ {{ t('dashboard.todayTokens') }} {{ formatTokens(stats?.today_tokens || 0) }} {{ t('dashboard.input') }}: {{ formatTokens(stats?.today_input_tokens || 0) }} / {{ t('dashboard.output') }}: {{ formatTokens(stats?.today_output_tokens || 0) }} {{ t('dashboard.input') }}: {{ formatTokens(stats?.today_input_tokens || 0) }} / {{ t('dashboard.output') }}: {{ formatTokens(stats?.today_output_tokens || 0) }} / {{ t('dashboard.cache') }}: {{ formatTokens((stats?.today_cache_creation_tokens || 0) + (stats?.today_cache_read_tokens || 0)) }} {{ t('dashboard.totalTokens') }} {{ formatTokens(stats?.total_tokens || 0) }} {{ t('dashboard.input') }}: {{ formatTokens(stats?.total_input_tokens || 0) }} / {{ t('dashboard.output') }}: {{ formatTokens(stats?.total_output_tokens || 0) }} {{ t('dashboard.input') }}: {{ formatTokens(stats?.total_input_tokens || 0) }} / {{ t('dashboard.output') }}: {{ formatTokens(stats?.total_output_tokens || 0) }} / {{ t('dashboard.cache') }}: {{ formatTokens((stats?.total_cache_creation_tokens || 0) + (stats?.total_cache_read_tokens || 0)) }}
{{ t('admin.channelMonitor.advanced.section') }}
@@ -197,6 +245,7 @@ import type {
ChannelMonitor,
CreateParams,
APIMode,
+ CheckMode,
Provider,
UpdateParams,
} from '@/api/admin/channelMonitor'
@@ -216,10 +265,20 @@ import {
PROVIDER_ANTHROPIC,
PROVIDER_GEMINI,
PROVIDER_GROK,
+ PROVIDER_ANTIGRAVITY,
+ PROVIDER_KIMI,
+ PROVIDER_ZHIPU,
+ PROVIDER_DEEPSEEK,
API_MODE_CHAT_COMPLETIONS,
API_MODE_RESPONSES,
+ CHECK_MODE_PROBE,
+ CHECK_MODE_QUOTA,
+ CHECK_MODE_QUOTA_PROBE,
DEFAULT_GROK_ENDPOINT,
DEFAULT_GROK_MODEL,
+ DEFAULT_KIMI_ENDPOINT,
+ DEFAULT_ZHIPU_ENDPOINT,
+ DEFAULT_DEEPSEEK_ENDPOINT,
DEFAULT_INTERVAL_SECONDS,
} from '@/constants/channelMonitor'
@@ -259,6 +318,8 @@ interface MonitorForm {
name: string
provider: Provider
api_mode: APIMode
+ check_mode: CheckMode
+ account_id: number | null
endpoint: string
api_key: string
primary_model: string
@@ -278,6 +339,8 @@ const form = reactive
+ {{ t('keys.useKeyModal.codexModelCatalog.title') }}
+
+
+
-
+
-
+
@@ -93,11 +111,11 @@
:key="idx"
class="whitespace-nowrap text-xs leading-5"
>
- {{ tierLabel(iv) }}
- {{ paidPerMillion(iv.input_price) }}
+ {{ tierLabel(iv) }}
+ {{ paidPerMillion(iv.input_price, period) }}
- {{ paidPerMillion(m.pricing?.input_price) }}
+ {{ paidPerMillion(m.pricing?.input_price, period) }}
@@ -105,25 +123,41 @@
v-for="(iv, idx) in tokenIntervals(m)"
:key="idx"
class="whitespace-nowrap text-xs leading-5"
+ :title="tierHint(m)"
>
- {{ tierLabel(iv) }}
- {{ paidPerMillion(iv.output_price) }}
+ {{ paidPerMillion(iv.output_price, period) }}
- {{ paidPerMillion(m.pricing?.output_price) }}
+ {{ paidPerMillion(m.pricing?.output_price, period) }}
+
+
-
+
- {{ official(m.official_pricing?.input_price) }}
+
+
- {{ official(m.official_pricing?.output_price) }}
+
+
+
+
-
+
{{ periodRate(period) }}x
+ {{ requestRate(m) }}x
@@ -218,7 +294,7 @@ import {
BILLING_MODE_IMAGE,
type BillingMode
} from '@/constants/channel'
-import type { PlazaModel } from '@/api/modelPlaza'
+import type { PlazaModel, PlazaTimePricingPeriod } from '@/api/modelPlaza'
import type { UserPricingInterval } from '@/api/channels'
const props = defineProps<{
@@ -232,6 +308,13 @@ const props = defineProps<{
/** 生图独立倍率:true 时图片计费模型的实付倍率取 imageRateMultiplier,不取分组/专属倍率。 */
imageRateIndependent?: boolean
imageRateMultiplier?: number | null
+ /**
+ * 高峰窗口描述(含倍率与服务器时区标注),空串/缺省 = 分组未启用高峰。
+ * 表格所有价格均为不含高峰因子的口径,该窗口仅用于分时时段行的 tooltip 披露:
+ * 与高峰重叠的部分实付还会再乘高峰倍率。
+ */
+ peakWindow?: string
+ peakRateMultiplier?: number | null
}>()
const { t } = useI18n()
@@ -279,10 +362,35 @@ function billingModeLabel(m: PlazaModel): string {
/** 价格统一保底 2 位小数,更长的有效小数原样保留。 */
const MIN_DECIMALS = 2
-/** 实付价 = 渠道单价 × 生效倍率,按 $/1M token 展示。 */
-function paidPerMillion(value: number | null | undefined): string {
+/** 表格行:每个模型一行标准价;配置了分时倍率的模型再按时段各加一行。 */
+interface PlazaRow {
+ model: PlazaModel
+ period: PlazaTimePricingPeriod | null
+ key: string
+}
+
+const rows = computed {{ m.model }}
+ {{ formatMonitorModel(m.model) }}
(null)
const loading = ref(false)
diff --git a/frontend/src/components/user/__tests__/MonitorCard.quota.spec.ts b/frontend/src/components/user/__tests__/MonitorCard.quota.spec.ts
new file mode 100644
index 0000000000..8bc680c64c
--- /dev/null
+++ b/frontend/src/components/user/__tests__/MonitorCard.quota.spec.ts
@@ -0,0 +1,103 @@
+import { mount } from '@vue/test-utils'
+import { describe, expect, it, vi } from 'vitest'
+
+import type { UserMonitorView } from '@/api/channelMonitor'
+import MonitorCard from '../monitor/MonitorCard.vue'
+
+const { isQuotaVisible } = vi.hoisted(() => ({
+ isQuotaVisible: vi.fn(() => false),
+}))
+
+vi.mock('@/utils/featureFlags', () => ({
+ isChannelMonitorQuotaVisible: () => isQuotaVisible(),
+}))
+
+vi.mock('vue-i18n', async () => {
+ const actual = await vi.importActual