Local-first history
+Session history is stored in the browser. Fast and simple; not shared across devices.
+diff --git a/.github/workflows/backend-ci.yml b/.github/workflows/backend-ci.yml index f8b22ee7033..15ff97fe005 100644 --- a/.github/workflows/backend-ci.yml +++ b/.github/workflows/backend-ci.yml @@ -20,7 +20,7 @@ jobs: cache-dependency-path: backend/go.sum - name: Verify Go version run: | - go version | grep -q 'go1.26.2' + go version | grep -q 'go1.26.3' - name: Unit tests working-directory: backend run: make test-unit @@ -60,7 +60,7 @@ jobs: cache-dependency-path: backend/go.sum - name: Verify Go version run: | - go version | grep -q 'go1.26.2' + go version | grep -q 'go1.26.3' - name: golangci-lint uses: golangci/golangci-lint-action@v9 with: diff --git a/.github/workflows/docker-push.yml b/.github/workflows/docker-push.yml new file mode 100644 index 00000000000..4e192d96996 --- /dev/null +++ b/.github/workflows/docker-push.yml @@ -0,0 +1,39 @@ +name: Deploy Sub2api to GHCR + +on: + push: + branches: [ "main" ] # 只有推送到 main 分支时触发,如果是 master 请修改 + +env: + REGISTRY: ghcr.io + # 镜像名会自动设为:用户名/仓库名 + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-push: + runs-on: ubuntu-latest # 使用 GitHub 提供的 amd64 环境构建 + permissions: + contents: read + packages: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + push: true + # 明确指定 amd64,确保服务器能跑 + platforms: linux/amd64 + tags: | + ghcr.io/${{ env.IMAGE_NAME }}:latest + ghcr.io/${{ env.IMAGE_NAME }}:${{ github.sha }} \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 26ed8524141..80bc9850dae 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.2' + go version | grep -q 'go1.26.3' # Docker setup for GoReleaser - name: Set up QEMU diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index 600fd2faecc..ef8e59e54ac 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.2' + go version | grep -q 'go1.26.3' - name: Run govulncheck working-directory: backend run: | diff --git a/.gitignore b/.gitignore index cf251f0715a..cc338cb2ad7 100644 --- a/.gitignore +++ b/.gitignore @@ -131,8 +131,12 @@ docs/* !docs/PAYMENT.md !docs/PAYMENT_CN.md !docs/ADMIN_PAYMENT_INTEGRATION_API.md +!docs/CHAT_COMPLETION_FEATURE_DEVELOPMENT.md .serena/ .codex/ frontend/coverage/ aicodex output/ + +./docs/* +./docs diff --git a/.superpowers/brainstorm/13206-1778653382/.server-stopped b/.superpowers/brainstorm/13206-1778653382/.server-stopped new file mode 100644 index 00000000000..f72eadd2002 --- /dev/null +++ b/.superpowers/brainstorm/13206-1778653382/.server-stopped @@ -0,0 +1 @@ +{"reason":"idle timeout","timestamp":1778656022182} diff --git a/.superpowers/brainstorm/13206-1778653382/.server.pid b/.superpowers/brainstorm/13206-1778653382/.server.pid new file mode 100644 index 00000000000..ece240fb701 --- /dev/null +++ b/.superpowers/brainstorm/13206-1778653382/.server.pid @@ -0,0 +1 @@ +13206 diff --git a/.superpowers/brainstorm/13206-1778653382/chat-capabilities-layout-v2.html b/.superpowers/brainstorm/13206-1778653382/chat-capabilities-layout-v2.html new file mode 100644 index 00000000000..a2a0d4e1460 --- /dev/null +++ b/.superpowers/brainstorm/13206-1778653382/chat-capabilities-layout-v2.html @@ -0,0 +1,73 @@ +
Right-side balance panel removed. Usage and cost are kept lightweight inside the conversation, while detailed balance remains in Usage Records.
+ +A proposed way to fit streaming, Markdown/code tools, history, costs, and mobile behavior into one usable chat workspace.
+ +Continuing in terminal...
+





尊敬的用户 %s,您的 API 请求在内容审计中触发平台风控策略。详情如下。
+| 触发时间 | %s |
| 触发来源 | 内容审核 |
| 所属分组 | %s |
| 命中类别 | %s / %.3f |
| 累计触发次数 | %d 次(阈值 %d) |
此邮件由 %s 自动发送,请勿回复。
+尊敬的用户 %s,您的账户在计数周期内多次触发平台风控策略,系统已自动禁用该账户。详情如下。
+| 封禁时间 | %s |
| 触发来源 | 内容审核 |
| 所属分组 | %s |
| 命中类别 | %s / %.3f |
| 累计触发次数 | %d 次(阈值 %d) |
如需申诉或恢复账号,请联系平台管理员处理。
+此邮件由 %s 自动发送,请勿回复。
++ {{ t('admin.accounts.openai.codexImageGenerationBridgeDesc') }} +
++ {{ t('admin.accounts.oauth.openai.codexSessionDesc') }} +
+ ++ {{ t('admin.accounts.oauth.openai.codexSessionHint') }} +
++ {{ error }} +
+
+
+
+
继续登录前需要先同意最新条款。
++ 未同意前,账号密码输入和快捷登录会保持禁用。 +
++ 我们的服务条款已于 {{ updatedAt || '近期' }} 更新。在继续使用服务之前,请仔细阅读并同意以下条款。 +
+相关文档
+{{ JSON.stringify(data) }}',
+ },
+}))
+
+vi.mock('vue-i18n', () => ({
+ useI18n: () => ({
+ t: (key: string) =>
+ ({
+ 'dashboard.tokenInput': '输入 Token',
+ 'dashboard.tokenOutput': '输出 Token',
+ 'dashboard.tokenCacheWrite': '缓存写入 Token',
+ 'dashboard.tokenCacheRead': '缓存读取 Token',
+ 'dashboard.cacheHitRate': '缓存命中率',
+ 'dashboard.actualCost': '实际费用',
+ 'dashboard.standardCost': '标准费用',
+ 'admin.dashboard.tokenUsageTrend': 'Token 使用趋势',
+ 'admin.dashboard.noDataAvailable': '暂无数据',
+ })[key] || key,
+ }),
+}))
+
+describe('TokenUsageTrend', () => {
+ it('uses localized Chinese labels instead of English dataset names', () => {
+ const wrapper = mount(TokenUsageTrend, {
+ props: {
+ trendData: [
+ {
+ date: '2026-05-01',
+ input_tokens: 10,
+ output_tokens: 5,
+ cache_creation_tokens: 3,
+ cache_read_tokens: 2,
+ actual_cost: 0.1,
+ cost: 0.2,
+ },
+ ],
+ },
+ global: {
+ stubs: {
+ LoadingSpinner: true,
+ },
+ },
+ })
+
+ const text = wrapper.get('[data-testid="chart"]').text()
+ expect(text).toContain('输入 Token')
+ expect(text).toContain('输出 Token')
+ expect(text).toContain('缓存写入 Token')
+ expect(text).toContain('缓存读取 Token')
+ expect(text).toContain('缓存命中率')
+ expect(text).not.toContain('Input')
+ expect(text).not.toContain('Output')
+ expect(text).not.toContain('Cache Creation')
+ expect(text).not.toContain('Cache Read')
+ expect(text).not.toContain('Cache Hit Rate')
+ })
+})
diff --git a/frontend/src/components/chat/MarkdownMessage.vue b/frontend/src/components/chat/MarkdownMessage.vue
new file mode 100644
index 00000000000..b592c537efd
--- /dev/null
+++ b/frontend/src/components/chat/MarkdownMessage.vue
@@ -0,0 +1,107 @@
+
+ {{ part.code }}
+ + {{ t('admin.channels.form.codexImageGenerationBridgeHint') }} +
+{{ t("admin.groups.imagePricing.description") }}
++ {{ t("admin.groups.imagePricing.modeHint") }} +
+{{ t("admin.groups.imagePricing.description") }}
++ {{ t("admin.groups.imagePricing.modeHint") }} +
+{{ t('admin.riskControl.description') }}
+{{ item.label }}
+ + {{ item.badge }} + +{{ item.value }}
+{{ item.meta }}
+{{ t('admin.riskControl.workerStatusHint') }}
+{{ t('admin.riskControl.queueUsage') }}
++ {{ formatNumber(status?.queue_length ?? 0) }} / {{ formatNumber(status?.queue_size ?? configForm.queue_size) }} +
+{{ t('admin.riskControl.activeWorkers') }}
+{{ status?.active_workers ?? 0 }}
+{{ t('admin.riskControl.idleWorkers') }}
+{{ status?.idle_workers ?? configForm.worker_count }}
+{{ t('admin.riskControl.processed') }}
+{{ formatNumber(status?.processed ?? 0) }}
+{{ t('admin.riskControl.droppedErrors') }}
+{{ formatNumber((status?.dropped ?? 0) + (status?.errors ?? 0)) }}
+{{ t('admin.riskControl.workerPool') }}
++ {{ t('admin.riskControl.workerPoolMeta', { active: status?.active_workers ?? 0, idle: status?.idle_workers ?? configForm.worker_count, total: status?.worker_count ?? configForm.worker_count }) }} +
+{{ t('admin.riskControl.recordsHint') }}
+| {{ t('admin.riskControl.table.time') }} | +{{ t('admin.riskControl.table.group') }} | +{{ t('admin.riskControl.table.user') }} | +{{ t('admin.riskControl.table.apiKey') }} | +{{ t('admin.riskControl.table.endpoint') }} | +{{ t('admin.riskControl.table.result') }} | +{{ t('admin.riskControl.table.highest') }} | +{{ t('admin.riskControl.table.actionMeta') }} | +{{ t('admin.riskControl.table.latency') }} | +{{ t('admin.riskControl.table.input') }} | +
|---|---|---|---|---|---|---|---|---|---|
| {{ t('common.loading') }} | +|||||||||
| {{ t('admin.riskControl.emptyLogs') }} | +|||||||||
| {{ formatDateTime(row.created_at) }} | +{{ row.group_name || '-' }} | +
+ {{ row.user_email || '-' }}
+ UID {{ row.user_id }}
+ |
+ {{ row.api_key_name || '-' }} | +
+ {{ row.endpoint || '-' }}
+ {{ row.provider || '-' }} / {{ row.model || '-' }}
+ |
+ + + {{ resultLabel(row) }} + + | +
+ {{ row.highest_category || '-' }}
+ {{ percent(row.highest_score) }}
+ |
+
+ {{ violationCountText(row) }}
+
+ {{ row.email_sent ? t('admin.riskControl.emailSent') : t('admin.riskControl.emailNotSent') }}
+ / {{ t('admin.riskControl.autoBanned') }}
+
+
+ |
+
+ {{ latencyText(row.upstream_latency_ms) }}
+
+ {{ t('admin.riskControl.queueDelay', { ms: row.queue_delay_ms }) }}
+
+ |
+ + + | +
{{ t('admin.riskControl.enabled') }}
+{{ t('admin.riskControl.enabledHint') }}
+{{ modeDescription(configForm.mode) }}
++ {{ t('admin.riskControl.apiKeysHint', { count: configForm.api_key_count }) }} +
+{{ t('admin.riskControl.auditTestInput') }}
+{{ t('admin.riskControl.auditTestInputHint') }}
+{{ t('admin.riskControl.auditTestImages') }}
+{{ t('admin.riskControl.auditTestImagesHint') }}
+{{ t('admin.riskControl.apiKeyHealth') }}
+{{ t('admin.riskControl.apiKeyFreezeRule') }}
+{{ t('admin.riskControl.apiKeyHealthEmpty') }}
+{{ t('admin.riskControl.apiKeyHealthEmptyHint') }}
+{{ apiKeyStatusMeta(row) }}
++ {{ row.last_error }} +
+{{ t('admin.riskControl.auditTestResult') }}
++ {{ t('admin.riskControl.auditTestHighest', { category: moderationTestResult.highest_category || '-', score: percent(moderationTestResult.highest_score) }) }} +
+{{ t('admin.riskControl.groupScopeHint') }}
+{{ t('admin.riskControl.noGroups') }}
+{{ t('admin.riskControl.recordNonHits') }}
+{{ t('admin.riskControl.recordNonHitsHint') }}
+{{ t('admin.riskControl.preHashCheck') }}
+{{ t('admin.riskControl.preHashCheckHint') }}
++ {{ t('admin.riskControl.flaggedHashCount', { count: formatNumber(status?.flagged_hash_count ?? 0) }) }} +
+{{ t('admin.riskControl.flaggedHashHint') }}
+{{ t('admin.riskControl.emailOnHit') }}
+{{ t('admin.riskControl.emailOnHitHint') }}
+{{ t('admin.riskControl.autoBan') }}
+{{ t('admin.riskControl.autoBanHint') }}
+{{ t('admin.riskControl.table.time') }}
+{{ formatDateTime(inputDetailRow.created_at) }}
+{{ t('admin.riskControl.table.user') }}
+{{ inputDetailRow.user_email || '-' }}
+{{ t('admin.riskControl.table.result') }}
+ + {{ resultLabel(inputDetailRow) }} + +{{ t('admin.riskControl.table.highest') }}
++ {{ inputDetailRow.highest_category || '-' }} / {{ percent(inputDetailRow.highest_score) }} +
+{{ t('admin.riskControl.inputDetailContent') }}
++ {{ inputDetailRow.endpoint || '-' }} · {{ inputDetailRow.provider || '-' }} / {{ inputDetailRow.model || '-' }} +
+{{ inputDetailText }}
+ + {{ t("admin.settings.rateLimit429Cooldown.description") }} +
++ {{ t("admin.settings.rateLimit429Cooldown.enabledHint") }} +
++ {{ + t( + "admin.settings.rateLimit429Cooldown.cooldownSecondsHint", + ) + }} +
++ {{ + localText( + "开启 GitHub 或 Google 邮箱授权登录后,系统会读取已验证邮箱,存在则直接登录,不存在则自动注册。", + "After GitHub or Google email OAuth is enabled, the system reads a verified email, signs in matching users, and auto-registers missing users.", + ) + }} +
++ {{ + localText( + "GitHub OAuth App 需要 read:user user:email 权限,回调地址填写下方后端地址。", + "GitHub OAuth App needs read:user user:email scopes. Use the backend callback URL below.", + ) + }} +
+
+ {{ githubOAuthRedirectUrlSuggestion }}
+
+ + {{ + localText( + "Google OAuth 客户端需要 openid email profile 范围,并在凭据里登记后端回调地址。", + "Google OAuth client needs openid email profile scopes and the backend callback URL registered in credentials.", + ) + }} +
+
+ {{ googleOAuthRedirectUrlSuggestion }}
+
+ + {{ + localText( + "控制登录页是否要求用户先阅读并同意服务条款、隐私政策或其他 Markdown 文档。", + "Control whether the login page requires users to accept Markdown policy documents first.", + ) + }} +
++ {{ + form.login_agreement_mode === "checkbox" + ? localText("复选框会显示在登录按钮下方,未勾选前所有登录入口禁用。", "The checkbox appears below the login button and gates all login actions.") + : localText("弹窗会在登录页打开,用户拒绝后所有登录入口保持禁用。", "The modal opens on the login page and gates all login actions until accepted.") + }} +
++ {{ localText("日期或文档内容变化后,用户需要重新同意。", "Changing the date or content requires fresh consent.") }} +
++ {{ + localText( + "文档名称可自定义,内容按 Markdown 保存。可参考:服务条款、使用政策、支持的国家和地区、服务特定条款。", + "Document titles are customizable and content is saved as Markdown.", + ) + }} +
++ {{ doc.title || localText("未命名文档", "Untitled document") }} +
++ {{ loginAgreementRoutePath(doc, index) }} +
++ {{ t('admin.settings.features.imageGeneration.description') }} +
+
+
+ {{ t('admin.settings.features.imageGeneration.enabledHint') }} +
++ {{ t('admin.settings.features.chatCompletion.description') }} +
++ {{ t('admin.settings.features.chatCompletion.enabledHint') }} +
++ {{ t('admin.settings.features.riskControl.description') }} +
+
+
+ {{ t('admin.settings.features.riskControl.enabledHint') }} +
++ {{ t('auth.oauth.callbackHint') }} +
++ {{ registrationHint }} +
+ ++ {{ registrationError }} +
+ ++ {{ t('auth.oauth.invalidCallbackHint') }} +
+ ++ {{ t('chatCompletion.disabled') }} +
++ {{ t('chatCompletion.empty') }} +
+