diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index c4bd04b..22c8204 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,20 +1,20 @@
-#### 💻 变更类型 | Change Type
-
-
-
-- [ ] ✨ feat
-- [ ] 🐛 fix
-- [ ] ♻️ refactor
-- [ ] 💄 style
-- [ ] 👷 build
-- [ ] ⚡️ perf
-- [ ] 📝 docs
-- [ ] 🔨 chore
-
-#### 🔀 变更说明 | Description of Change
-
-
-
-#### 📝 补充信息 | Additional Information
-
-
+#### 💻 变更类型 | Change Type
+
+
+
+- [ ] ✨ feat
+- [ ] 🐛 fix
+- [ ] ♻️ refactor
+- [ ] 💄 style
+- [ ] 👷 build
+- [ ] ⚡️ perf
+- [ ] 📝 docs
+- [ ] 🔨 chore
+
+#### 🔀 变更说明 | Description of Change
+
+
+
+#### 📝 补充信息 | Additional Information
+
+
\ No newline at end of file
diff --git a/.github/workflows/release-dockerhub.yml b/.github/workflows/release-dockerhub.yml
new file mode 100644
index 0000000..de86912
--- /dev/null
+++ b/.github/workflows/release-dockerhub.yml
@@ -0,0 +1,127 @@
+name: Release to Docker Hub
+
+on:
+ workflow_dispatch:
+ inputs:
+ version_type:
+ description: '版本类型'
+ required: true
+ default: 'patch'
+ type: choice
+ options:
+ - patch
+ - minor
+ - major
+
+permissions:
+ contents: write
+
+jobs:
+ release:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v5
+ with:
+ fetch-depth: 0
+ token: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Get current version
+ id: get_version
+ run: |
+ LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
+ TAG_VERSION=${LATEST_TAG#v}
+
+ if [ -f VERSION ]; then
+ FILE_VERSION=$(cat VERSION | tr -d '[:space:]')
+ else
+ FILE_VERSION="0.0.0"
+ fi
+
+ function version_gt() { test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$1"; }
+
+ if version_gt "$FILE_VERSION" "$TAG_VERSION"; then
+ VERSION="$FILE_VERSION"
+ else
+ VERSION="$TAG_VERSION"
+ fi
+
+ echo "Current version: $VERSION"
+ echo "current_version=$VERSION" >> $GITHUB_OUTPUT
+
+ - name: Calculate next version
+ id: next_version
+ env:
+ VERSION_TYPE: ${{ github.event.inputs.version_type }}
+ run: |
+ VERSION="${{ steps.get_version.outputs.current_version }}"
+ BASE_VERSION=$(echo "$VERSION" | sed 's/-.*$//')
+
+ IFS='.' read -r -a version_parts <<< "$BASE_VERSION"
+ MAJOR="${version_parts[0]:-0}"
+ MINOR="${version_parts[1]:-0}"
+ PATCH="${version_parts[2]:-0}"
+
+ case "$VERSION_TYPE" in
+ major)
+ NEW_VERSION="$((MAJOR + 1)).0.0"
+ ;;
+ minor)
+ NEW_VERSION="${MAJOR}.$((MINOR + 1)).0"
+ ;;
+ *)
+ NEW_VERSION="${MAJOR}.${MINOR}.$((PATCH + 1))"
+ ;;
+ esac
+
+ echo "New version: $NEW_VERSION"
+ echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
+ echo "new_tag=v$NEW_VERSION" >> $GITHUB_OUTPUT
+
+ - name: Update VERSION file
+ run: |
+ echo "${{ steps.next_version.outputs.new_version }}" > VERSION
+
+ - name: Commit VERSION and create tag
+ run: |
+ git config user.name "github-actions[bot]"
+ git config user.email "github-actions[bot]@users.noreply.github.com"
+
+ git add VERSION
+ if ! git diff --cached --quiet; then
+ git commit -m "chore: bump version to ${{ steps.next_version.outputs.new_tag }} [skip ci]"
+ fi
+
+ NEW_TAG="${{ steps.next_version.outputs.new_tag }}"
+ git tag -a "$NEW_TAG" -m "Release $NEW_TAG"
+ git push origin HEAD:main "$NEW_TAG"
+
+ # Docker 构建并推送到 Docker Hub
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v3
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Log in to Docker Hub
+ uses: docker/login-action@v3
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+
+ - name: Build and push Docker image
+ uses: docker/build-push-action@v6
+ with:
+ context: .
+ file: ./docker/Dockerfile
+ platforms: linux/amd64,linux/arm64
+ push: true
+ tags: |
+ ${{ secrets.DOCKERHUB_USERNAME }}/ds2api:${{ steps.next_version.outputs.new_tag }}
+ ${{ secrets.DOCKERHUB_USERNAME }}/ds2api:${{ steps.next_version.outputs.new_version }}
+ ${{ secrets.DOCKERHUB_USERNAME }}/ds2api:latest
+ labels: |
+ org.opencontainers.image.version=${{ steps.next_version.outputs.new_version }}
+ org.opencontainers.image.revision=${{ github.sha }}
+ cache-from: type=gha
+ cache-to: type=gha,mode=max
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..8b7cbc1
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,128 @@
+name: Release to Aliyun CR
+
+on:
+ workflow_dispatch:
+ inputs:
+ version_type:
+ description: '版本类型'
+ required: true
+ default: 'patch'
+ type: choice
+ options:
+ - patch
+ - minor
+ - major
+
+permissions:
+ contents: write
+
+jobs:
+ release:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v5
+ with:
+ fetch-depth: 0
+ token: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Get current version
+ id: get_version
+ run: |
+ LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
+ TAG_VERSION=${LATEST_TAG#v}
+
+ if [ -f VERSION ]; then
+ FILE_VERSION=$(cat VERSION | tr -d '[:space:]')
+ else
+ FILE_VERSION="0.0.0"
+ fi
+
+ function version_gt() { test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$1"; }
+
+ if version_gt "$FILE_VERSION" "$TAG_VERSION"; then
+ VERSION="$FILE_VERSION"
+ else
+ VERSION="$TAG_VERSION"
+ fi
+
+ echo "Current version: $VERSION"
+ echo "current_version=$VERSION" >> $GITHUB_OUTPUT
+
+ - name: Calculate next version
+ id: next_version
+ env:
+ VERSION_TYPE: ${{ github.event.inputs.version_type }}
+ run: |
+ VERSION="${{ steps.get_version.outputs.current_version }}"
+ BASE_VERSION=$(echo "$VERSION" | sed 's/-.*$//')
+
+ IFS='.' read -r -a version_parts <<< "$BASE_VERSION"
+ MAJOR="${version_parts[0]:-0}"
+ MINOR="${version_parts[1]:-0}"
+ PATCH="${version_parts[2]:-0}"
+
+ case "$VERSION_TYPE" in
+ major)
+ NEW_VERSION="$((MAJOR + 1)).0.0"
+ ;;
+ minor)
+ NEW_VERSION="${MAJOR}.$((MINOR + 1)).0"
+ ;;
+ *)
+ NEW_VERSION="${MAJOR}.${MINOR}.$((PATCH + 1))"
+ ;;
+ esac
+
+ echo "New version: $NEW_VERSION"
+ echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
+ echo "new_tag=v$NEW_VERSION" >> $GITHUB_OUTPUT
+
+ - name: Update VERSION file
+ run: |
+ echo "${{ steps.next_version.outputs.new_version }}" > VERSION
+
+ - name: Commit VERSION and create tag
+ run: |
+ git config user.name "github-actions[bot]"
+ git config user.email "github-actions[bot]@users.noreply.github.com"
+
+ git add VERSION
+ if ! git diff --cached --quiet; then
+ git commit -m "chore: bump version to ${{ steps.next_version.outputs.new_tag }} [skip ci]"
+ fi
+
+ NEW_TAG="${{ steps.next_version.outputs.new_tag }}"
+ git tag -a "$NEW_TAG" -m "Release $NEW_TAG"
+ git push origin HEAD:main "$NEW_TAG"
+
+ # Docker 构建并推送到阿里云
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v3
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Log in to Aliyun Container Registry
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ secrets.ALIYUN_REGISTRY }}
+ username: ${{ secrets.ALIYUN_REGISTRY_USER }}
+ password: ${{ secrets.ALIYUN_REGISTRY_PASSWORD }}
+
+ - name: Build and push Docker image
+ uses: docker/build-push-action@v6
+ with:
+ context: .
+ file: ./docker/Dockerfile
+ platforms: linux/amd64,linux/arm64
+ push: true
+ tags: |
+ ${{ secrets.ALIYUN_REGISTRY }}/${{ secrets.ALIYUN_REGISTRY_NAMESPACE }}/ds2api:${{ steps.next_version.outputs.new_tag }}
+ ${{ secrets.ALIYUN_REGISTRY }}/${{ secrets.ALIYUN_REGISTRY_NAMESPACE }}/ds2api:${{ steps.next_version.outputs.new_version }}
+ ${{ secrets.ALIYUN_REGISTRY }}/${{ secrets.ALIYUN_REGISTRY_NAMESPACE }}/ds2api:latest
+ labels: |
+ org.opencontainers.image.version=${{ steps.next_version.outputs.new_version }}
+ org.opencontainers.image.revision=${{ github.sha }}
+ cache-from: type=gha
+ cache-to: type=gha,mode=max
diff --git a/.gitignore b/.gitignore
index 422c203..d096b58 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,37 +2,6 @@
config.json
.env
-# Python
-__pycache__/
-*.py[cod]
-*$py.class
-*.so
-.Python
-build/
-develop-eggs/
-dist/
-downloads/
-eggs/
-.eggs/
-lib/
-lib64/
-parts/
-sdist/
-var/
-wheels/
-pip-wheel-metadata/
-share/python-wheels/
-*.egg-info/
-.installed.cfg
-*.egg
-MANIFEST
-
-# Virtual environments
-venv/
-ENV/
-env/
-.venv
-
# IDE
.vscode/
.idea/
@@ -44,7 +13,6 @@ env/
# Logs
*.log
logs/
-uvicorn.log
artifacts/
# Vercel
@@ -56,8 +24,6 @@ webui/node_modules/
webui/dist/
.npm
.pnpm-store/
-# 保留 webui/package-lock.json 用于 CI 缓存
-# package-lock.json # 如果有根目录的可以忽略
yarn.lock
pnpm-lock.yaml
@@ -86,7 +52,9 @@ coverage*.out
cover/
# Misc
-*.pyc
-*.pyo
.git/
Thumbs.db
+
+# Claude Code
+.claude/
+CLAUDE.local.md
diff --git a/VERSION b/VERSION
new file mode 100644
index 0000000..7ecf123
--- /dev/null
+++ b/VERSION
@@ -0,0 +1 @@
+0.1.0
diff --git a/docker-compose.yml b/docker-compose.yml
index 2ac83e1..3e6b605 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,18 +1,14 @@
-services:
- ds2api:
- build: .
- image: ds2api:latest
- container_name: ds2api
- ports:
- - "${PORT:-5001}:${PORT:-5001}"
- env_file:
- - .env
- environment:
- - HOST=0.0.0.0
- restart: unless-stopped
- healthcheck:
- test: ["CMD", "/usr/local/bin/busybox", "wget", "-qO-", "http://localhost:${PORT:-5001}/healthz"]
- interval: 30s
- timeout: 10s
- retries: 3
- start_period: 10s
+services:
+ ds2api:
+ image: crpi-cnazxqmg4avmg4fq.cn-beijing.personal.cr.aliyuncs.com/ronghuaxueleng/ds2api:latest
+ container_name: ds2api
+ restart: always
+ ports:
+ - "6011:5001"
+ volumes:
+ - ./config.json:/app/config.json # 配置文件
+ - ./.env:/app/.env # 环境变量
+ environment:
+ - TZ=Asia/Shanghai
+ - LOG_LEVEL=INFO
+ - DS2API_ADMIN_KEY=${DS2API_ADMIN_KEY:-ds2api}
diff --git a/internal/admin/handler_accounts_crud.go b/internal/admin/handler_accounts_crud.go
index a0d64df..768e59e 100644
--- a/internal/admin/handler_accounts_crud.go
+++ b/internal/admin/handler_accounts_crud.go
@@ -1,115 +1,128 @@
-package admin
-
-import (
- "encoding/json"
- "fmt"
- "net/http"
- "strings"
-
- "github.com/go-chi/chi/v5"
-
- "ds2api/internal/config"
-)
-
-func (h *Handler) listAccounts(w http.ResponseWriter, r *http.Request) {
- page := intFromQuery(r, "page", 1)
- pageSize := intFromQuery(r, "page_size", 10)
- if page < 1 {
- page = 1
- }
- if pageSize < 1 {
- pageSize = 1
- }
- if pageSize > 100 {
- pageSize = 100
- }
- accounts := h.Store.Snapshot().Accounts
- total := len(accounts)
- reverseAccounts(accounts)
- totalPages := 1
- if total > 0 {
- totalPages = (total + pageSize - 1) / pageSize
- }
- start := (page - 1) * pageSize
- if start > total {
- start = total
- }
- end := start + pageSize
- if end > total {
- end = total
- }
- items := make([]map[string]any, 0, end-start)
- for _, acc := range accounts[start:end] {
- token := strings.TrimSpace(acc.Token)
- preview := ""
- if token != "" {
- if len(token) > 20 {
- preview = token[:20] + "..."
- } else {
- preview = token
- }
- }
- items = append(items, map[string]any{
- "identifier": acc.Identifier(),
- "email": acc.Email,
- "mobile": acc.Mobile,
- "has_password": acc.Password != "",
- "has_token": token != "",
- "token_preview": preview,
- "test_status": acc.TestStatus,
- })
- }
- writeJSON(w, http.StatusOK, map[string]any{"items": items, "total": total, "page": page, "page_size": pageSize, "total_pages": totalPages})
-}
-
-func (h *Handler) addAccount(w http.ResponseWriter, r *http.Request) {
- var req map[string]any
- _ = json.NewDecoder(r.Body).Decode(&req)
- acc := toAccount(req)
- if acc.Identifier() == "" {
- writeJSON(w, http.StatusBadRequest, map[string]any{"detail": "需要 email 或 mobile"})
- return
- }
- err := h.Store.Update(func(c *config.Config) error {
- for _, a := range c.Accounts {
- if acc.Email != "" && a.Email == acc.Email {
- return fmt.Errorf("邮箱已存在")
- }
- if acc.Mobile != "" && a.Mobile == acc.Mobile {
- return fmt.Errorf("手机号已存在")
- }
- }
- c.Accounts = append(c.Accounts, acc)
- return nil
- })
- if err != nil {
- writeJSON(w, http.StatusBadRequest, map[string]any{"detail": err.Error()})
- return
- }
- h.Pool.Reset()
- writeJSON(w, http.StatusOK, map[string]any{"success": true, "total_accounts": len(h.Store.Snapshot().Accounts)})
-}
-
-func (h *Handler) deleteAccount(w http.ResponseWriter, r *http.Request) {
- identifier := chi.URLParam(r, "identifier")
- err := h.Store.Update(func(c *config.Config) error {
- idx := -1
- for i, a := range c.Accounts {
- if accountMatchesIdentifier(a, identifier) {
- idx = i
- break
- }
- }
- if idx < 0 {
- return fmt.Errorf("账号不存在")
- }
- c.Accounts = append(c.Accounts[:idx], c.Accounts[idx+1:]...)
- return nil
- })
- if err != nil {
- writeJSON(w, http.StatusNotFound, map[string]any{"detail": err.Error()})
- return
- }
- h.Pool.Reset()
- writeJSON(w, http.StatusOK, map[string]any{"success": true, "total_accounts": len(h.Store.Snapshot().Accounts)})
-}
+package admin
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "strings"
+
+ "github.com/go-chi/chi/v5"
+
+ "ds2api/internal/config"
+)
+
+func (h *Handler) listAccounts(w http.ResponseWriter, r *http.Request) {
+ page := intFromQuery(r, "page", 1)
+ pageSize := intFromQuery(r, "page_size", 10)
+ if page < 1 {
+ page = 1
+ }
+ if pageSize < 1 {
+ pageSize = 1
+ }
+ if pageSize > 100 {
+ pageSize = 100
+ }
+ accounts := h.Store.Snapshot().Accounts
+ reverseAccounts(accounts)
+ q := strings.TrimSpace(strings.ToLower(r.URL.Query().Get("q")))
+ if q != "" {
+ filtered := make([]config.Account, 0, len(accounts))
+ for _, acc := range accounts {
+ id := strings.ToLower(acc.Identifier())
+ if strings.Contains(id, q) ||
+ strings.Contains(strings.ToLower(acc.Email), q) ||
+ strings.Contains(strings.ToLower(acc.Mobile), q) {
+ filtered = append(filtered, acc)
+ }
+ }
+ accounts = filtered
+ }
+ total := len(accounts)
+ totalPages := 1
+ if total > 0 {
+ totalPages = (total + pageSize - 1) / pageSize
+ }
+ start := (page - 1) * pageSize
+ if start > total {
+ start = total
+ }
+ end := start + pageSize
+ if end > total {
+ end = total
+ }
+ items := make([]map[string]any, 0, end-start)
+ for _, acc := range accounts[start:end] {
+ token := strings.TrimSpace(acc.Token)
+ preview := ""
+ if token != "" {
+ if len(token) > 20 {
+ preview = token[:20] + "..."
+ } else {
+ preview = token
+ }
+ }
+ items = append(items, map[string]any{
+ "identifier": acc.Identifier(),
+ "email": acc.Email,
+ "mobile": acc.Mobile,
+ "has_password": acc.Password != "",
+ "has_token": token != "",
+ "token_preview": preview,
+ "test_status": acc.TestStatus,
+ })
+ }
+ writeJSON(w, http.StatusOK, map[string]any{"items": items, "total": total, "page": page, "page_size": pageSize, "total_pages": totalPages})
+}
+
+func (h *Handler) addAccount(w http.ResponseWriter, r *http.Request) {
+ var req map[string]any
+ _ = json.NewDecoder(r.Body).Decode(&req)
+ acc := toAccount(req)
+ if acc.Identifier() == "" {
+ writeJSON(w, http.StatusBadRequest, map[string]any{"detail": "需要 email 或 mobile"})
+ return
+ }
+ err := h.Store.Update(func(c *config.Config) error {
+ for _, a := range c.Accounts {
+ if acc.Email != "" && a.Email == acc.Email {
+ return fmt.Errorf("邮箱已存在")
+ }
+ if acc.Mobile != "" && a.Mobile == acc.Mobile {
+ return fmt.Errorf("手机号已存在")
+ }
+ }
+ c.Accounts = append(c.Accounts, acc)
+ return nil
+ })
+ if err != nil {
+ writeJSON(w, http.StatusBadRequest, map[string]any{"detail": err.Error()})
+ return
+ }
+ h.Pool.Reset()
+ writeJSON(w, http.StatusOK, map[string]any{"success": true, "total_accounts": len(h.Store.Snapshot().Accounts)})
+}
+
+func (h *Handler) deleteAccount(w http.ResponseWriter, r *http.Request) {
+ identifier := chi.URLParam(r, "identifier")
+ err := h.Store.Update(func(c *config.Config) error {
+ idx := -1
+ for i, a := range c.Accounts {
+ if accountMatchesIdentifier(a, identifier) {
+ idx = i
+ break
+ }
+ }
+ if idx < 0 {
+ return fmt.Errorf("账号不存在")
+ }
+ c.Accounts = append(c.Accounts[:idx], c.Accounts[idx+1:]...)
+ return nil
+ })
+ if err != nil {
+ writeJSON(w, http.StatusNotFound, map[string]any{"detail": err.Error()})
+ return
+ }
+ h.Pool.Reset()
+ writeJSON(w, http.StatusOK, map[string]any{"success": true, "total_accounts": len(h.Store.Snapshot().Accounts)})
+}
diff --git a/internal/admin/handler_accounts_testing.go b/internal/admin/handler_accounts_testing.go
index 7a7430d..262c809 100644
--- a/internal/admin/handler_accounts_testing.go
+++ b/internal/admin/handler_accounts_testing.go
@@ -1,212 +1,209 @@
-package admin
-
-import (
- "bytes"
- "context"
- "encoding/json"
- "fmt"
- "io"
- "net/http"
- "strings"
- "sync"
- "time"
-
- authn "ds2api/internal/auth"
- "ds2api/internal/config"
- "ds2api/internal/sse"
-)
-
-func (h *Handler) testSingleAccount(w http.ResponseWriter, r *http.Request) {
- var req map[string]any
- _ = json.NewDecoder(r.Body).Decode(&req)
- identifier, _ := req["identifier"].(string)
- if strings.TrimSpace(identifier) == "" {
- writeJSON(w, http.StatusBadRequest, map[string]any{"detail": "需要账号标识(identifier / email / mobile)"})
- return
- }
- acc, ok := findAccountByIdentifier(h.Store, identifier)
- if !ok {
- writeJSON(w, http.StatusNotFound, map[string]any{"detail": "账号不存在"})
- return
- }
- model, _ := req["model"].(string)
- if model == "" {
- model = "deepseek-chat"
- }
- message, _ := req["message"].(string)
- result := h.testAccount(r.Context(), acc, model, message)
- writeJSON(w, http.StatusOK, result)
-}
-
-func (h *Handler) testAllAccounts(w http.ResponseWriter, r *http.Request) {
- var req map[string]any
- _ = json.NewDecoder(r.Body).Decode(&req)
- model, _ := req["model"].(string)
- if model == "" {
- model = "deepseek-chat"
- }
- accounts := h.Store.Snapshot().Accounts
- if len(accounts) == 0 {
- writeJSON(w, http.StatusOK, map[string]any{"total": 0, "success": 0, "failed": 0, "results": []any{}})
- return
- }
-
- // Concurrent testing with a semaphore to limit parallelism.
- const maxConcurrency = 5
- results := runAccountTestsConcurrently(accounts, maxConcurrency, func(_ int, account config.Account) map[string]any {
- return h.testAccount(r.Context(), account, model, "")
- })
-
- success := 0
- for _, res := range results {
- if ok, _ := res["success"].(bool); ok {
- success++
- }
- }
- writeJSON(w, http.StatusOK, map[string]any{"total": len(accounts), "success": success, "failed": len(accounts) - success, "results": results})
-}
-
-func runAccountTestsConcurrently(accounts []config.Account, maxConcurrency int, testFn func(int, config.Account) map[string]any) []map[string]any {
- if maxConcurrency <= 0 {
- maxConcurrency = 1
- }
- sem := make(chan struct{}, maxConcurrency)
- results := make([]map[string]any, len(accounts))
- var wg sync.WaitGroup
- for i, acc := range accounts {
- wg.Add(1)
- go func(idx int, account config.Account) {
- defer wg.Done()
- sem <- struct{}{} // acquire
- defer func() { <-sem }() // release
- results[idx] = testFn(idx, account)
- }(i, acc)
- }
- wg.Wait()
- return results
-}
-
-func (h *Handler) testAccount(ctx context.Context, acc config.Account, model, message string) map[string]any {
- start := time.Now()
- identifier := acc.Identifier()
- result := map[string]any{"account": identifier, "success": false, "response_time": 0, "message": "", "model": model}
- defer func() {
- status := "failed"
- if ok, _ := result["success"].(bool); ok {
- status = "ok"
- }
- _ = h.Store.UpdateAccountTestStatus(identifier, status)
- }()
- token := strings.TrimSpace(acc.Token)
- if token == "" {
- newToken, err := h.DS.Login(ctx, acc)
- if err != nil {
- result["message"] = "登录失败: " + err.Error()
- return result
- }
- token = newToken
- _ = h.Store.UpdateAccountToken(acc.Identifier(), token)
- }
- authCtx := &authn.RequestAuth{UseConfigToken: false, DeepSeekToken: token}
- sessionID, err := h.DS.CreateSession(ctx, authCtx, 1)
- if err != nil {
- newToken, loginErr := h.DS.Login(ctx, acc)
- if loginErr != nil {
- result["message"] = "创建会话失败: " + err.Error()
- return result
- }
- token = newToken
- authCtx.DeepSeekToken = token
- _ = h.Store.UpdateAccountToken(acc.Identifier(), token)
- sessionID, err = h.DS.CreateSession(ctx, authCtx, 1)
- if err != nil {
- result["message"] = "创建会话失败: " + err.Error()
- return result
- }
- }
- if strings.TrimSpace(message) == "" {
- result["success"] = true
- result["message"] = "API 测试成功(仅会话创建)"
- result["response_time"] = int(time.Since(start).Milliseconds())
- return result
- }
- thinking, search, ok := config.GetModelConfig(model)
- if !ok {
- thinking, search = false, false
- }
- _ = search
- pow, err := h.DS.GetPow(ctx, authCtx, 1)
- if err != nil {
- result["message"] = "获取 PoW 失败: " + err.Error()
- return result
- }
- payload := map[string]any{"chat_session_id": sessionID, "prompt": "<|User|>" + message, "ref_file_ids": []any{}, "thinking_enabled": thinking, "search_enabled": search}
- resp, err := h.DS.CallCompletion(ctx, authCtx, payload, pow, 1)
- if err != nil {
- result["message"] = "请求失败: " + err.Error()
- return result
- }
- if resp.StatusCode != http.StatusOK {
- defer resp.Body.Close()
- result["message"] = fmt.Sprintf("请求失败: HTTP %d", resp.StatusCode)
- return result
- }
- collected := sse.CollectStream(resp, thinking, true)
- result["success"] = true
- result["response_time"] = int(time.Since(start).Milliseconds())
- if collected.Text != "" {
- result["message"] = collected.Text
- } else {
- result["message"] = "(无回复内容)"
- }
- if collected.Thinking != "" {
- result["thinking"] = collected.Thinking
- }
- return result
-}
-
-func (h *Handler) testAPI(w http.ResponseWriter, r *http.Request) {
- var req map[string]any
- _ = json.NewDecoder(r.Body).Decode(&req)
- model, _ := req["model"].(string)
- message, _ := req["message"].(string)
- apiKey, _ := req["api_key"].(string)
- if model == "" {
- model = "deepseek-chat"
- }
- if message == "" {
- message = "你好"
- }
- if apiKey == "" {
- keys := h.Store.Snapshot().Keys
- if len(keys) == 0 {
- writeJSON(w, http.StatusBadRequest, map[string]any{"detail": "没有可用的 API Key"})
- return
- }
- apiKey = keys[0]
- }
- host := r.Host
- scheme := "http"
- if strings.Contains(strings.ToLower(host), "vercel") || strings.Contains(strings.ToLower(r.Header.Get("X-Forwarded-Proto")), "https") {
- scheme = "https"
- }
- payload := map[string]any{"model": model, "messages": []map[string]any{{"role": "user", "content": message}}, "stream": false}
- b, _ := json.Marshal(payload)
- request, _ := http.NewRequestWithContext(r.Context(), http.MethodPost, fmt.Sprintf("%s://%s/v1/chat/completions", scheme, host), bytes.NewReader(b))
- request.Header.Set("Authorization", "Bearer "+apiKey)
- request.Header.Set("Content-Type", "application/json")
- resp, err := (&http.Client{Timeout: 60 * time.Second}).Do(request)
- if err != nil {
- writeJSON(w, http.StatusOK, map[string]any{"success": false, "error": err.Error()})
- return
- }
- defer resp.Body.Close()
- body, _ := io.ReadAll(resp.Body)
- if resp.StatusCode == http.StatusOK {
- var parsed any
- _ = json.Unmarshal(body, &parsed)
- writeJSON(w, http.StatusOK, map[string]any{"success": true, "status_code": resp.StatusCode, "response": parsed})
- return
- }
- writeJSON(w, http.StatusOK, map[string]any{"success": false, "status_code": resp.StatusCode, "response": string(body)})
-}
+package admin
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+ "sync"
+ "time"
+
+ authn "ds2api/internal/auth"
+ "ds2api/internal/config"
+ "ds2api/internal/sse"
+)
+
+func (h *Handler) testSingleAccount(w http.ResponseWriter, r *http.Request) {
+ var req map[string]any
+ _ = json.NewDecoder(r.Body).Decode(&req)
+ identifier, _ := req["identifier"].(string)
+ if strings.TrimSpace(identifier) == "" {
+ writeJSON(w, http.StatusBadRequest, map[string]any{"detail": "需要账号标识(identifier / email / mobile)"})
+ return
+ }
+ acc, ok := findAccountByIdentifier(h.Store, identifier)
+ if !ok {
+ writeJSON(w, http.StatusNotFound, map[string]any{"detail": "账号不存在"})
+ return
+ }
+ model, _ := req["model"].(string)
+ if model == "" {
+ model = "deepseek-chat"
+ }
+ message, _ := req["message"].(string)
+ result := h.testAccount(r.Context(), acc, model, message)
+ writeJSON(w, http.StatusOK, result)
+}
+
+func (h *Handler) testAllAccounts(w http.ResponseWriter, r *http.Request) {
+ var req map[string]any
+ _ = json.NewDecoder(r.Body).Decode(&req)
+ model, _ := req["model"].(string)
+ if model == "" {
+ model = "deepseek-chat"
+ }
+ accounts := h.Store.Snapshot().Accounts
+ if len(accounts) == 0 {
+ writeJSON(w, http.StatusOK, map[string]any{"total": 0, "success": 0, "failed": 0, "results": []any{}})
+ return
+ }
+
+ // Concurrent testing with a semaphore to limit parallelism.
+ const maxConcurrency = 5
+ results := runAccountTestsConcurrently(accounts, maxConcurrency, func(_ int, account config.Account) map[string]any {
+ return h.testAccount(r.Context(), account, model, "")
+ })
+
+ success := 0
+ for _, res := range results {
+ if ok, _ := res["success"].(bool); ok {
+ success++
+ }
+ }
+ writeJSON(w, http.StatusOK, map[string]any{"total": len(accounts), "success": success, "failed": len(accounts) - success, "results": results})
+}
+
+func runAccountTestsConcurrently(accounts []config.Account, maxConcurrency int, testFn func(int, config.Account) map[string]any) []map[string]any {
+ if maxConcurrency <= 0 {
+ maxConcurrency = 1
+ }
+ sem := make(chan struct{}, maxConcurrency)
+ results := make([]map[string]any, len(accounts))
+ var wg sync.WaitGroup
+ for i, acc := range accounts {
+ wg.Add(1)
+ go func(idx int, account config.Account) {
+ defer wg.Done()
+ sem <- struct{}{} // acquire
+ defer func() { <-sem }() // release
+ results[idx] = testFn(idx, account)
+ }(i, acc)
+ }
+ wg.Wait()
+ return results
+}
+
+func (h *Handler) testAccount(ctx context.Context, acc config.Account, model, message string) map[string]any {
+ start := time.Now()
+ identifier := acc.Identifier()
+ result := map[string]any{"account": identifier, "success": false, "response_time": 0, "message": "", "model": model}
+ defer func() {
+ status := "failed"
+ if ok, _ := result["success"].(bool); ok {
+ status = "ok"
+ }
+ _ = h.Store.UpdateAccountTestStatus(identifier, status)
+ }()
+ token := strings.TrimSpace(acc.Token)
+ if token == "" {
+ newToken, err := h.DS.Login(ctx, acc)
+ if err != nil {
+ result["message"] = "登录失败: " + err.Error()
+ return result
+ }
+ token = newToken
+ _ = h.Store.UpdateAccountToken(acc.Identifier(), token)
+ }
+ authCtx := &authn.RequestAuth{UseConfigToken: false, DeepSeekToken: token}
+ sessionID, err := h.DS.CreateSession(ctx, authCtx, 1)
+ if err != nil {
+ newToken, loginErr := h.DS.Login(ctx, acc)
+ if loginErr != nil {
+ result["message"] = "创建会话失败: " + err.Error()
+ return result
+ }
+ token = newToken
+ authCtx.DeepSeekToken = token
+ _ = h.Store.UpdateAccountToken(acc.Identifier(), token)
+ sessionID, err = h.DS.CreateSession(ctx, authCtx, 1)
+ if err != nil {
+ result["message"] = "创建会话失败: " + err.Error()
+ return result
+ }
+ }
+ if strings.TrimSpace(message) == "" {
+ message = "你是谁?"
+ }
+ thinking, search, ok := config.GetModelConfig(model)
+ if !ok {
+ thinking, search = false, false
+ }
+ _ = search
+ pow, err := h.DS.GetPow(ctx, authCtx, 1)
+ if err != nil {
+ result["message"] = "获取 PoW 失败: " + err.Error()
+ return result
+ }
+ payload := map[string]any{"chat_session_id": sessionID, "prompt": "<|User|>" + message, "ref_file_ids": []any{}, "thinking_enabled": thinking, "search_enabled": search}
+ resp, err := h.DS.CallCompletion(ctx, authCtx, payload, pow, 1)
+ if err != nil {
+ result["message"] = "请求失败: " + err.Error()
+ return result
+ }
+ if resp.StatusCode != http.StatusOK {
+ defer resp.Body.Close()
+ result["message"] = fmt.Sprintf("请求失败: HTTP %d", resp.StatusCode)
+ return result
+ }
+ collected := sse.CollectStream(resp, thinking, true)
+ result["success"] = true
+ result["response_time"] = int(time.Since(start).Milliseconds())
+ if collected.Text != "" {
+ result["message"] = collected.Text
+ } else {
+ result["message"] = "(无回复内容)"
+ }
+ if collected.Thinking != "" {
+ result["thinking"] = collected.Thinking
+ }
+ return result
+}
+
+func (h *Handler) testAPI(w http.ResponseWriter, r *http.Request) {
+ var req map[string]any
+ _ = json.NewDecoder(r.Body).Decode(&req)
+ model, _ := req["model"].(string)
+ message, _ := req["message"].(string)
+ apiKey, _ := req["api_key"].(string)
+ if model == "" {
+ model = "deepseek-chat"
+ }
+ if message == "" {
+ message = "你好"
+ }
+ if apiKey == "" {
+ keys := h.Store.Snapshot().Keys
+ if len(keys) == 0 {
+ writeJSON(w, http.StatusBadRequest, map[string]any{"detail": "没有可用的 API Key"})
+ return
+ }
+ apiKey = keys[0]
+ }
+ host := r.Host
+ scheme := "http"
+ if strings.Contains(strings.ToLower(host), "vercel") || strings.Contains(strings.ToLower(r.Header.Get("X-Forwarded-Proto")), "https") {
+ scheme = "https"
+ }
+ payload := map[string]any{"model": model, "messages": []map[string]any{{"role": "user", "content": message}}, "stream": false}
+ b, _ := json.Marshal(payload)
+ request, _ := http.NewRequestWithContext(r.Context(), http.MethodPost, fmt.Sprintf("%s://%s/v1/chat/completions", scheme, host), bytes.NewReader(b))
+ request.Header.Set("Authorization", "Bearer "+apiKey)
+ request.Header.Set("Content-Type", "application/json")
+ resp, err := (&http.Client{Timeout: 60 * time.Second}).Do(request)
+ if err != nil {
+ writeJSON(w, http.StatusOK, map[string]any{"success": false, "error": err.Error()})
+ return
+ }
+ defer resp.Body.Close()
+ body, _ := io.ReadAll(resp.Body)
+ if resp.StatusCode == http.StatusOK {
+ var parsed any
+ _ = json.Unmarshal(body, &parsed)
+ writeJSON(w, http.StatusOK, map[string]any{"success": true, "status_code": resp.StatusCode, "response": parsed})
+ return
+ }
+ writeJSON(w, http.StatusOK, map[string]any{"success": false, "status_code": resp.StatusCode, "response": string(body)})
+}
diff --git a/start.mjs b/start.mjs
new file mode 100644
index 0000000..59c5101
--- /dev/null
+++ b/start.mjs
@@ -0,0 +1,565 @@
+#!/usr/bin/env node
+/**
+ * DS2API 启动脚本 - 交互式菜单
+ *
+ * 使用方法:
+ * node start.mjs # 显示交互式菜单
+ * node start.mjs dev # 开发模式(后端 + 前端热重载)
+ * node start.mjs prod # 生产模式(编译后运行)
+ * node start.mjs build # 编译后端二进制
+ * node start.mjs webui # 构建前端静态文件
+ * node start.mjs install # 安装前端依赖
+ * node start.mjs stop # 停止所有服务
+ * node start.mjs status # 查看服务状态
+ */
+
+import { spawn, execSync } from 'child_process';
+import { createInterface } from 'readline';
+import { existsSync } from 'fs';
+import { fileURLToPath } from 'url';
+import { dirname, join } from 'path';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+
+// 判断是否为 Windows
+const isWindows = process.platform === 'win32';
+
+// 编译产物路径
+const BINARY = join(__dirname, isWindows ? 'ds2api.exe' : 'ds2api');
+
+// 配置(从环境变量读取,与 Go 主程序保持一致)
+const CONFIG = {
+ port: process.env.PORT || '5001',
+ frontendPort: 5173,
+ logLevel: process.env.LOG_LEVEL || 'INFO',
+ adminKey: process.env.DS2API_ADMIN_KEY || 'admin',
+ webuiDir: join(__dirname, 'webui'),
+ staticAdminDir: process.env.DS2API_STATIC_ADMIN_DIR || join(__dirname, 'static', 'admin'),
+};
+
+// 国内镜像配置
+const MIRRORS = {
+ goproxy: process.env.GOPROXY || 'https://goproxy.cn,direct',
+ npm: process.env.NPM_REGISTRY || 'https://registry.npmmirror.com',
+};
+
+// 存储子进程
+const processes = [];
+
+// 颜色输出
+const colors = {
+ reset: '\x1b[0m',
+ bright: '\x1b[1m',
+ dim: '\x1b[2m',
+ red: '\x1b[31m',
+ green: '\x1b[32m',
+ yellow: '\x1b[33m',
+ blue: '\x1b[34m',
+ magenta: '\x1b[35m',
+ cyan: '\x1b[36m',
+};
+
+const log = {
+ info: (msg) => console.log(`${colors.cyan}[INFO]${colors.reset} ${msg}`),
+ success: (msg) => console.log(`${colors.green}[OK]${colors.reset} ${msg}`),
+ warn: (msg) => console.log(`${colors.yellow}[WARN]${colors.reset} ${msg}`),
+ error: (msg) => console.log(`${colors.red}[ERROR]${colors.reset} ${msg}`),
+ title: (msg) => console.log(`\n${colors.bright}${colors.magenta}${msg}${colors.reset}`),
+};
+
+// 清理并退出
+function cleanup() {
+ console.log('\n');
+ log.info('正在关闭所有服务...');
+ processes.forEach(proc => {
+ if (proc && !proc.killed) {
+ proc.kill('SIGTERM');
+ }
+ });
+ log.success('已退出');
+ process.exit(0);
+}
+
+process.on('SIGINT', cleanup);
+process.on('SIGTERM', cleanup);
+
+// 检查命令是否存在
+function commandExists(cmd) {
+ try {
+ execSync(`${isWindows ? 'where' : 'which'} ${cmd}`, { stdio: 'ignore' });
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+// 检查 Go 是否安装
+function checkGo() {
+ return commandExists('go');
+}
+
+// 获取 Go 版本
+function getGoVersion() {
+ try {
+ return execSync('go version', { encoding: 'utf-8' }).trim();
+ } catch {
+ return null;
+ }
+}
+
+// 检查前端依赖是否已安装
+function checkFrontendDeps() {
+ if (!existsSync(CONFIG.webuiDir)) return null;
+ return existsSync(join(CONFIG.webuiDir, 'node_modules'));
+}
+
+// 检查前端是否已构建
+function checkWebuiBuilt() {
+ return existsSync(join(CONFIG.staticAdminDir, 'index.html'));
+}
+
+// 检查后端二进制是否存在
+function binaryExists() {
+ return existsSync(BINARY);
+}
+
+// 查找占用端口的进程 PID
+function findPidByPort(port) {
+ try {
+ if (isWindows) {
+ const output = execSync(`netstat -ano | findstr :${port} | findstr LISTENING`, {
+ encoding: 'utf-8',
+ shell: true,
+ stdio: ['pipe', 'pipe', 'ignore'],
+ });
+ const pids = new Set();
+ for (const line of output.trim().split('\n')) {
+ const parts = line.trim().split(/\s+/);
+ const pid = parts[parts.length - 1];
+ if (pid && pid !== '0') pids.add(pid);
+ }
+ return [...pids];
+ } else {
+ const output = execSync(`lsof -ti :${port}`, {
+ encoding: 'utf-8',
+ stdio: ['pipe', 'pipe', 'ignore'],
+ });
+ return output.trim().split('\n').filter(Boolean);
+ }
+ } catch {
+ return [];
+ }
+}
+
+// 获取运行中的服务状态
+function getRunningStatus() {
+ const backendPids = findPidByPort(CONFIG.port);
+ const frontendPids = findPidByPort(CONFIG.frontendPort);
+ return {
+ backend: backendPids,
+ frontend: frontendPids,
+ isRunning: backendPids.length > 0 || frontendPids.length > 0,
+ };
+}
+
+// 停止服务
+async function stopServices() {
+ const running = getRunningStatus();
+
+ if (!running.isRunning) {
+ log.warn('没有检测到正在运行的服务');
+ return;
+ }
+
+ log.title('========== 停止服务 ==========');
+
+ const killProcess = async (pid) => {
+ try {
+ if (isWindows) {
+ try {
+ execSync(`taskkill /PID ${pid}`, { stdio: 'ignore', shell: true });
+ } catch {
+ execSync(`taskkill /F /T /PID ${pid}`, { stdio: 'ignore', shell: true });
+ }
+ } else {
+ execSync(`kill -15 ${pid}`, { stdio: 'ignore' });
+ await new Promise(r => setTimeout(r, 500));
+ try {
+ execSync(`kill -0 ${pid}`, { stdio: 'ignore' });
+ execSync(`kill -9 ${pid}`, { stdio: 'ignore' });
+ } catch { /* 进程已退出 */ }
+ }
+ } catch { /* 进程可能已退出 */ }
+ };
+
+ if (running.backend.length > 0) {
+ log.info(`停止后端服务 (端口 ${CONFIG.port}, PID: ${running.backend.join(', ')})...`);
+ for (const pid of running.backend) await killProcess(pid);
+ log.success('后端服务已停止');
+ }
+
+ if (running.frontend.length > 0) {
+ log.info(`停止前端服务 (端口 ${CONFIG.frontendPort}, PID: ${running.frontend.join(', ')})...`);
+ for (const pid of running.frontend) await killProcess(pid);
+ log.success('前端服务已停止');
+ }
+}
+
+// 安装前端依赖
+async function installFrontendDeps() {
+ if (!existsSync(CONFIG.webuiDir)) {
+ log.warn('webui 目录不存在,跳过前端依赖安装');
+ return;
+ }
+ log.info(`安装前端依赖 (npm ci, registry: ${MIRRORS.npm})...`);
+ return new Promise((resolve, reject) => {
+ const proc = spawn('npm', ['ci', '--registry', MIRRORS.npm], {
+ cwd: CONFIG.webuiDir,
+ stdio: 'inherit',
+ shell: true,
+ });
+ proc.on('close', code => code === 0 ? resolve() : reject(new Error('前端依赖安装失败')));
+ });
+}
+
+// 确保前端依赖已安装
+async function ensureFrontendDeps() {
+ if (checkFrontendDeps() === false) {
+ log.warn('检测到前端依赖未安装,正在安装...');
+ await installFrontendDeps();
+ }
+}
+
+// 编译后端二进制
+async function buildBackend() {
+ if (!checkGo()) throw new Error('未找到 Go,请先安装 Go (https://go.dev/dl/)');
+ log.info(`编译后端二进制 (GOPROXY: ${MIRRORS.goproxy})...`);
+ return new Promise((resolve, reject) => {
+ const proc = spawn('go', ['build', '-o', BINARY, './cmd/ds2api'], {
+ cwd: __dirname,
+ stdio: 'inherit',
+ shell: true,
+ env: { ...process.env, GOPROXY: MIRRORS.goproxy },
+ });
+ proc.on('close', code => code === 0 ? resolve() : reject(new Error('后端编译失败')));
+ });
+}
+
+// 构建前端静态文件
+async function buildWebui() {
+ if (!existsSync(CONFIG.webuiDir)) {
+ log.warn('webui 目录不存在');
+ return;
+ }
+ await ensureFrontendDeps();
+ log.info('构建前端静态文件...');
+ return new Promise((resolve, reject) => {
+ const proc = spawn(
+ 'npm', ['run', 'build', '--', '--outDir', CONFIG.staticAdminDir, '--emptyOutDir'],
+ { cwd: CONFIG.webuiDir, stdio: 'inherit', shell: true }
+ );
+ proc.on('close', code => code === 0 ? resolve() : reject(new Error('前端构建失败')));
+ });
+}
+
+// 启动后端(开发模式:go run,无需预编译)
+async function startBackendDev() {
+ if (!checkGo()) throw new Error('未找到 Go,请先安装 Go (https://go.dev/dl/)');
+ log.info(`启动后端(go run)... http://localhost:${CONFIG.port}`);
+ const proc = spawn('go', ['run', './cmd/ds2api'], {
+ cwd: __dirname,
+ stdio: 'inherit',
+ shell: true,
+ env: {
+ ...process.env,
+ PORT: CONFIG.port,
+ LOG_LEVEL: CONFIG.logLevel,
+ DS2API_ADMIN_KEY: CONFIG.adminKey,
+ GOPROXY: MIRRORS.goproxy,
+ },
+ });
+ processes.push(proc);
+ return proc;
+}
+
+// 启动后端(生产模式:运行编译好的二进制)
+async function startBackendProd() {
+ if (!binaryExists()) {
+ log.warn('未找到编译产物,正在编译...');
+ await buildBackend();
+ }
+ log.info(`启动后端(二进制)... http://localhost:${CONFIG.port}`);
+ const proc = spawn(BINARY, [], {
+ cwd: __dirname,
+ stdio: 'inherit',
+ shell: false,
+ env: {
+ ...process.env,
+ PORT: CONFIG.port,
+ LOG_LEVEL: CONFIG.logLevel,
+ DS2API_ADMIN_KEY: CONFIG.adminKey,
+ },
+ });
+ processes.push(proc);
+ return proc;
+}
+
+// 启动前端开发服务器
+async function startFrontend() {
+ if (!existsSync(CONFIG.webuiDir)) {
+ log.warn('webui 目录不存在,跳过前端启动');
+ return null;
+ }
+ await ensureFrontendDeps();
+ log.info(`启动前端开发服务器... http://localhost:${CONFIG.frontendPort}`);
+ const proc = spawn('npm', ['run', 'dev'], {
+ cwd: CONFIG.webuiDir,
+ stdio: 'inherit',
+ shell: true,
+ });
+ processes.push(proc);
+ return proc;
+}
+
+// 显示状态信息
+function showStatus() {
+ console.log('\n' + '─'.repeat(50));
+ log.success(`后端 API: http://localhost:${CONFIG.port}`);
+ log.success(`管理界面: http://localhost:${CONFIG.port}/admin`);
+ if (existsSync(CONFIG.webuiDir)) {
+ log.success(`前端 Dev: http://localhost:${CONFIG.frontendPort}`);
+ }
+ console.log('─'.repeat(50));
+ log.info('按 Ctrl+C 停止所有服务\n');
+}
+
+// 等待进程退出
+function waitForProcesses() {
+ return new Promise(resolve => {
+ const check = setInterval(() => {
+ if (processes.filter(p => !p.killed).length === 0) {
+ clearInterval(check);
+ resolve();
+ }
+ }, 1000);
+ });
+}
+
+// 交互式菜单
+async function showMenu() {
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
+ const question = (prompt) => new Promise(resolve => rl.question(prompt, resolve));
+
+ console.clear();
+ log.title('╔══════════════════════════════════════════╗');
+ log.title('║ DS2API 启动脚本 (Go) ║');
+ log.title('╚══════════════════════════════════════════╝');
+
+ // 环境状态
+ const goVersion = getGoVersion();
+ const frontendDeps = checkFrontendDeps();
+ const webuiBuilt = checkWebuiBuilt();
+ const hasBinary = binaryExists();
+ const running = getRunningStatus();
+
+ const ok = (v) => v ? `${colors.green}✓${colors.reset}` : `${colors.yellow}✗${colors.reset}`;
+
+ console.log(`\n${colors.bright}环境状态:${colors.reset}`);
+ console.log(` Go: ${goVersion ? `${colors.green}${goVersion}${colors.reset}` : `${colors.red}未安装${colors.reset}`}`);
+ console.log(` 前端依赖: ${frontendDeps === null ? `${colors.dim}N/A${colors.reset}` : frontendDeps ? `${colors.green}已安装${colors.reset}` : `${colors.yellow}未安装${colors.reset}`}`);
+ console.log(` 前端构建: ${ok(webuiBuilt)} ${webuiBuilt ? `(${CONFIG.staticAdminDir})` : '未构建'}`);
+ console.log(` 后端二进制: ${ok(hasBinary)} ${hasBinary ? BINARY : '未编译'}`);
+
+ console.log(`\n${colors.bright}服务状态:${colors.reset}`);
+ console.log(` 后端 (:${CONFIG.port}): ${running.backend.length > 0 ? `${colors.green}运行中${colors.reset} (PID: ${running.backend.join(', ')})` : `${colors.dim}未运行${colors.reset}`}`);
+ console.log(` 前端 (:${CONFIG.frontendPort}): ${running.frontend.length > 0 ? `${colors.green}运行中${colors.reset} (PID: ${running.frontend.join(', ')})` : `${colors.dim}未运行${colors.reset}`}`);
+
+ console.log(`\n${colors.bright}环境变量:${colors.reset}`);
+ console.log(` PORT: ${colors.cyan}${CONFIG.port}${colors.reset}`);
+ console.log(` LOG_LEVEL: ${colors.cyan}${CONFIG.logLevel}${colors.reset}`);
+ console.log(` DS2API_ADMIN_KEY: ${colors.cyan}${CONFIG.adminKey}${colors.reset}`);
+ console.log(` GOPROXY: ${colors.cyan}${MIRRORS.goproxy}${colors.reset}`);
+ console.log(` NPM_REGISTRY: ${colors.cyan}${MIRRORS.npm}${colors.reset}`);
+ console.log(`${colors.dim} 自定义: DS2API_ADMIN_KEY=密钥 PORT=5001 node start.mjs${colors.reset}`);
+
+ console.log(`
+${colors.bright}请选择操作:${colors.reset}
+
+ ${colors.cyan}1.${colors.reset} 开发模式 (go run + 前端热重载)
+ ${colors.cyan}2.${colors.reset} 仅后端 (go run,无需编译)
+ ${colors.cyan}3.${colors.reset} 仅前端 (npm dev)
+ ${colors.cyan}4.${colors.reset} 生产模式 (编译后运行,前端已嵌入)
+ ${colors.cyan}5.${colors.reset} 编译后端 (go build)
+ ${colors.cyan}6.${colors.reset} 构建前端 (npm build → static/admin)
+ ${colors.cyan}7.${colors.reset} 安装前端依赖 (npm ci)
+ ${colors.red}8.${colors.reset} 停止所有服务
+ ${colors.cyan}0.${colors.reset} 退出
+`);
+
+ const choice = await question(`${colors.yellow}请输入选项 [1]: ${colors.reset}`);
+ rl.close();
+
+ switch (choice.trim() || '1') {
+ case '1':
+ log.title('========== 开发模式 ==========');
+ await startBackendDev();
+ await new Promise(r => setTimeout(r, 1500));
+ await startFrontend();
+ showStatus();
+ await waitForProcesses();
+ break;
+
+ case '2':
+ log.title('========== 仅后端 (go run) ==========');
+ await startBackendDev();
+ showStatus();
+ await waitForProcesses();
+ break;
+
+ case '3':
+ log.title('========== 仅前端 ==========');
+ await startFrontend();
+ showStatus();
+ await waitForProcesses();
+ break;
+
+ case '4':
+ log.title('========== 生产模式 ==========');
+ await startBackendProd();
+ showStatus();
+ await waitForProcesses();
+ break;
+
+ case '5':
+ log.title('========== 编译后端 ==========');
+ await buildBackend();
+ log.success(`编译完成:${BINARY}`);
+ break;
+
+ case '6':
+ log.title('========== 构建前端 ==========');
+ await buildWebui();
+ log.success('前端构建完成!');
+ break;
+
+ case '7':
+ log.title('========== 安装前端依赖 ==========');
+ await installFrontendDeps();
+ log.success('前端依赖安装完成!');
+ break;
+
+ case '8':
+ await stopServices();
+ break;
+
+ case '0':
+ log.info('再见!');
+ process.exit(0);
+ break;
+
+ default:
+ log.warn('无效选项');
+ await showMenu();
+ }
+}
+
+// 命令行参数处理
+async function main() {
+ const cmd = process.argv[2];
+
+ if (!checkGo() && !['install', 'webui', 'stop', 'status', 'help', '-h', '--help'].includes(cmd)) {
+ log.error('未找到 Go,请先安装 Go: https://go.dev/dl/');
+ if (!cmd) {
+ // 无 Go 时仍允许进入菜单(可以只操作前端)
+ } else {
+ process.exit(1);
+ }
+ }
+
+ switch (cmd) {
+ case 'dev':
+ log.title('========== 开发模式 ==========');
+ await startBackendDev();
+ await new Promise(r => setTimeout(r, 1500));
+ await startFrontend();
+ showStatus();
+ await waitForProcesses();
+ break;
+
+ case 'prod':
+ log.title('========== 生产模式 ==========');
+ await startBackendProd();
+ showStatus();
+ await waitForProcesses();
+ break;
+
+ case 'build':
+ await buildBackend();
+ log.success(`编译完成:${BINARY}`);
+ break;
+
+ case 'webui':
+ await buildWebui();
+ log.success('前端构建完成!');
+ break;
+
+ case 'install':
+ await installFrontendDeps();
+ log.success('前端依赖安装完成!');
+ break;
+
+ case 'stop':
+ await stopServices();
+ break;
+
+ case 'status': {
+ const status = getRunningStatus();
+ const goVer = getGoVersion();
+ console.log(`\n${colors.bright}环境:${colors.reset}`);
+ console.log(` Go: ${goVer || `${colors.red}未安装${colors.reset}`}`);
+ console.log(`\n${colors.bright}服务状态:${colors.reset}`);
+ console.log(` 后端 (:${CONFIG.port}): ${status.backend.length > 0 ? `${colors.green}运行中${colors.reset} (PID: ${status.backend.join(', ')})` : `${colors.dim}未运行${colors.reset}`}`);
+ console.log(` 前端 (:${CONFIG.frontendPort}): ${status.frontend.length > 0 ? `${colors.green}运行中${colors.reset} (PID: ${status.frontend.join(', ')})` : `${colors.dim}未运行${colors.reset}`}\n`);
+ break;
+ }
+
+ case 'help':
+ case '-h':
+ case '--help':
+ console.log(`
+${colors.bright}DS2API 启动脚本 (Go)${colors.reset}
+
+${colors.cyan}使用方法:${colors.reset}
+ node start.mjs 显示交互式菜单
+ node start.mjs dev 开发模式 (go run + 前端热重载)
+ node start.mjs prod 生产模式 (编译产物,前端已嵌入)
+ node start.mjs build 编译后端二进制 (go build)
+ node start.mjs webui 构建前端静态文件
+ node start.mjs install 安装前端依赖 (npm ci)
+ node start.mjs stop 停止所有服务
+ node start.mjs status 查看服务状态
+
+${colors.cyan}常用环境变量:${colors.reset}
+ PORT 后端端口 (默认: 5001)
+ LOG_LEVEL 日志级别: DEBUG|INFO|WARN|ERROR (默认: INFO)
+ DS2API_ADMIN_KEY 管理员密钥 (默认: admin)
+ DS2API_CONFIG_PATH 配置文件路径 (默认: config.json)
+ GOPROXY Go 模块代理 (默认: https://goproxy.cn,direct)
+ NPM_REGISTRY npm 镜像源 (默认: https://registry.npmmirror.com)
+
+${colors.cyan}示例:${colors.reset}
+ DS2API_ADMIN_KEY=mykey PORT=8080 node start.mjs dev
+ GOPROXY=off NPM_REGISTRY=https://registry.npmjs.org node start.mjs dev
+`);
+ break;
+
+ default:
+ await showMenu();
+ }
+}
+
+main().catch(e => {
+ log.error(e.message);
+ process.exit(1);
+});
diff --git a/webui/src/features/account/AccountManagerContainer.jsx b/webui/src/features/account/AccountManagerContainer.jsx
index 558739d..9da3616 100644
--- a/webui/src/features/account/AccountManagerContainer.jsx
+++ b/webui/src/features/account/AccountManagerContainer.jsx
@@ -1,117 +1,121 @@
-import { useI18n } from '../../i18n'
-import { useAccountsData } from './useAccountsData'
-import { useAccountActions } from './useAccountActions'
-import QueueCards from './QueueCards'
-import ApiKeysPanel from './ApiKeysPanel'
-import AccountsTable from './AccountsTable'
-import AddKeyModal from './AddKeyModal'
-import AddAccountModal from './AddAccountModal'
-
-export default function AccountManagerContainer({ config, onRefresh, onMessage, authFetch }) {
- const { t } = useI18n()
- const apiFetch = authFetch || fetch
-
- const {
- queueStatus,
- keysExpanded,
- setKeysExpanded,
- accounts,
- page,
- pageSize,
- totalPages,
- totalAccounts,
- loadingAccounts,
- fetchAccounts,
- changePageSize,
- resolveAccountIdentifier,
- } = useAccountsData({ apiFetch })
-
- const {
- showAddKey,
- setShowAddKey,
- showAddAccount,
- setShowAddAccount,
- newKey,
- setNewKey,
- copiedKey,
- setCopiedKey,
- newAccount,
- setNewAccount,
- loading,
- testing,
- testingAll,
- batchProgress,
- addKey,
- deleteKey,
- addAccount,
- deleteAccount,
- testAccount,
- testAllAccounts,
- } = useAccountActions({
- apiFetch,
- t,
- onMessage,
- onRefresh,
- config,
- fetchAccounts,
- resolveAccountIdentifier,
- })
-
- return (
-
-
-
-
-
-
setShowAddAccount(true)}
- onTestAccount={testAccount}
- onDeleteAccount={deleteAccount}
- onPrevPage={() => fetchAccounts(page - 1)}
- onNextPage={() => fetchAccounts(page + 1)}
- onPageSizeChange={changePageSize}
- />
-
- setShowAddKey(false)}
- onAdd={addKey}
- />
-
- setShowAddAccount(false)}
- onAdd={addAccount}
- />
-
- )
-}
+import { useI18n } from '../../i18n'
+import { useAccountsData } from './useAccountsData'
+import { useAccountActions } from './useAccountActions'
+import QueueCards from './QueueCards'
+import ApiKeysPanel from './ApiKeysPanel'
+import AccountsTable from './AccountsTable'
+import AddKeyModal from './AddKeyModal'
+import AddAccountModal from './AddAccountModal'
+
+export default function AccountManagerContainer({ config, onRefresh, onMessage, authFetch }) {
+ const { t } = useI18n()
+ const apiFetch = authFetch || fetch
+
+ const {
+ queueStatus,
+ keysExpanded,
+ setKeysExpanded,
+ accounts,
+ page,
+ pageSize,
+ totalPages,
+ totalAccounts,
+ loadingAccounts,
+ fetchAccounts,
+ changePageSize,
+ resolveAccountIdentifier,
+ searchQuery,
+ handleSearchChange,
+ } = useAccountsData({ apiFetch })
+
+ const {
+ showAddKey,
+ setShowAddKey,
+ showAddAccount,
+ setShowAddAccount,
+ newKey,
+ setNewKey,
+ copiedKey,
+ setCopiedKey,
+ newAccount,
+ setNewAccount,
+ loading,
+ testing,
+ testingAll,
+ batchProgress,
+ addKey,
+ deleteKey,
+ addAccount,
+ deleteAccount,
+ testAccount,
+ testAllAccounts,
+ } = useAccountActions({
+ apiFetch,
+ t,
+ onMessage,
+ onRefresh,
+ config,
+ fetchAccounts,
+ resolveAccountIdentifier,
+ })
+
+ return (
+
+
+
+
+
+
setShowAddAccount(true)}
+ onTestAccount={testAccount}
+ onDeleteAccount={deleteAccount}
+ onPrevPage={() => fetchAccounts(page - 1)}
+ onNextPage={() => fetchAccounts(page + 1)}
+ onPageSizeChange={changePageSize}
+ searchQuery={searchQuery}
+ onSearchChange={handleSearchChange}
+ />
+
+ setShowAddKey(false)}
+ onAdd={addKey}
+ />
+
+ setShowAddAccount(false)}
+ onAdd={addAccount}
+ />
+
+ )
+}
diff --git a/webui/src/features/account/AccountsTable.jsx b/webui/src/features/account/AccountsTable.jsx
index 7f7c960..d7be383 100644
--- a/webui/src/features/account/AccountsTable.jsx
+++ b/webui/src/features/account/AccountsTable.jsx
@@ -1,182 +1,191 @@
-import { useState } from 'react'
-import { ChevronLeft, ChevronRight, Check, Copy, Play, Plus, Trash2 } from 'lucide-react'
-import clsx from 'clsx'
-
-export default function AccountsTable({
- t,
- accounts,
- loadingAccounts,
- testing,
- testingAll,
- batchProgress,
- totalAccounts,
- page,
- pageSize,
- totalPages,
- resolveAccountIdentifier,
- onTestAll,
- onShowAddAccount,
- onTestAccount,
- onDeleteAccount,
- onPrevPage,
- onNextPage,
- onPageSizeChange,
-}) {
- const [copiedId, setCopiedId] = useState(null)
-
- const copyId = (id) => {
- navigator.clipboard.writeText(id).then(() => {
- setCopiedId(id)
- setTimeout(() => setCopiedId(null), 1500)
- })
- }
- return (
-
-
-
-
{t('accountManager.accountsTitle')}
-
{t('accountManager.accountsDesc')}
-
-
-
-
-
-
-
- {testingAll && batchProgress.total > 0 && (
-
-
- {t('accountManager.testingAllAccounts')}
- {batchProgress.current} / {batchProgress.total}
-
-
- {batchProgress.results.length > 0 && (
-
- {batchProgress.results.map((r, i) => (
-
- {r.success ? '✓' : '✗'} {r.id}
-
- ))}
-
- )}
-
- )}
-
-
- {loadingAccounts ? (
-
{t('actions.loading')}
- ) : accounts.length > 0 ? (
- accounts.map((acc, i) => {
- const id = resolveAccountIdentifier(acc)
- return (
-
-
-
-
-
copyId(id)}
- >
- {id || '-'}
- {copiedId === id
- ?
- :
- }
-
-
- {acc.test_status === 'failed' ? t('accountManager.testStatusFailed') : (acc.test_status === 'ok' || acc.has_token) ? t('accountManager.sessionActive') : t('accountManager.reauthRequired')}
- {acc.token_preview && (
-
- {acc.token_preview}
-
- )}
-
-
-
-
-
-
-
-
- )
- })
- ) : (
-
{t('accountManager.noAccounts')}
- )}
-
-
- {totalPages > 1 && (
-
-
-
- {t('accountManager.pageInfo', { current: page, total: totalPages, count: totalAccounts })}
-
-
-
-
-
- {page} / {totalPages}
-
-
-
- )}
-
- )
-}
+import { useState } from 'react'
+import { ChevronLeft, ChevronRight, Check, Copy, Play, Plus, Trash2 } from 'lucide-react'
+import clsx from 'clsx'
+
+export default function AccountsTable({
+ t,
+ accounts,
+ loadingAccounts,
+ testing,
+ testingAll,
+ batchProgress,
+ totalAccounts,
+ page,
+ pageSize,
+ totalPages,
+ resolveAccountIdentifier,
+ onTestAll,
+ onShowAddAccount,
+ onTestAccount,
+ onDeleteAccount,
+ onPrevPage,
+ onNextPage,
+ onPageSizeChange,
+ searchQuery,
+ onSearchChange,
+}) {
+ const [copiedId, setCopiedId] = useState(null)
+
+ const copyId = (id) => {
+ navigator.clipboard.writeText(id).then(() => {
+ setCopiedId(id)
+ setTimeout(() => setCopiedId(null), 1500)
+ })
+ }
+ return (
+
+
+
+
{t('accountManager.accountsTitle')}
+
{t('accountManager.accountsDesc')}
+
+
+
onSearchChange(e.target.value)}
+ placeholder={t('accountManager.searchPlaceholder')}
+ className="px-3 py-1.5 text-sm bg-muted border border-border rounded-lg focus:outline-none focus:ring-1 focus:ring-ring placeholder:text-muted-foreground"
+ />
+
+
+
+
+
+ {testingAll && batchProgress.total > 0 && (
+
+
+ {t('accountManager.testingAllAccounts')}
+ {batchProgress.current} / {batchProgress.total}
+
+
+ {batchProgress.results.length > 0 && (
+
+ {batchProgress.results.map((r, i) => (
+
+ {r.success ? '✓' : '✗'} {r.id}
+
+ ))}
+
+ )}
+
+ )}
+
+
+ {loadingAccounts ? (
+
{t('actions.loading')}
+ ) : accounts.length > 0 ? (
+ accounts.map((acc, i) => {
+ const id = resolveAccountIdentifier(acc)
+ return (
+
+
+
+
+
copyId(id)}
+ >
+ {id || '-'}
+ {copiedId === id
+ ?
+ :
+ }
+
+
+ {acc.test_status === 'failed' ? t('accountManager.testStatusFailed') : (acc.test_status === 'ok' || acc.has_token) ? t('accountManager.sessionActive') : t('accountManager.reauthRequired')}
+ {acc.token_preview && (
+
+ {acc.token_preview}
+
+ )}
+
+
+
+
+
+
+
+
+ )
+ })
+ ) : (
+
{searchQuery ? t('accountManager.searchNoResults') : t('accountManager.noAccounts')}
+ )}
+
+
+ {totalPages > 1 && (
+
+
+
+ {t('accountManager.pageInfo', { current: page, total: totalPages, count: totalAccounts })}
+
+
+
+
+
+ {page} / {totalPages}
+
+
+
+ )}
+
+ )
+}
diff --git a/webui/src/features/account/useAccountsData.js b/webui/src/features/account/useAccountsData.js
index 90c56e0..3c904f3 100644
--- a/webui/src/features/account/useAccountsData.js
+++ b/webui/src/features/account/useAccountsData.js
@@ -1,75 +1,86 @@
-import { useEffect, useState } from 'react'
-
-export function useAccountsData({ apiFetch }) {
- const [queueStatus, setQueueStatus] = useState(null)
- const [keysExpanded, setKeysExpanded] = useState(false)
-
- const [accounts, setAccounts] = useState([])
- const [page, setPage] = useState(1)
- const [pageSize, setPageSize] = useState(10)
- const [totalPages, setTotalPages] = useState(1)
- const [totalAccounts, setTotalAccounts] = useState(0)
- const [loadingAccounts, setLoadingAccounts] = useState(false)
-
- const resolveAccountIdentifier = (acc) => {
- if (!acc || typeof acc !== 'object') return ''
- return String(acc.identifier || acc.email || acc.mobile || '').trim()
- }
-
- const fetchAccounts = async (targetPage = page, targetPageSize = pageSize) => {
- setLoadingAccounts(true)
- try {
- const res = await apiFetch(`/admin/accounts?page=${targetPage}&page_size=${targetPageSize}`)
- if (res.ok) {
- const data = await res.json()
- setAccounts(data.items || [])
- setTotalPages(data.total_pages || 1)
- setTotalAccounts(data.total || 0)
- setPage(data.page || 1)
- }
- } catch (e) {
- console.error('Failed to fetch accounts:', e)
- } finally {
- setLoadingAccounts(false)
- }
- }
-
- const changePageSize = (newSize) => {
- setPageSize(newSize)
- fetchAccounts(1, newSize)
- }
-
- const fetchQueueStatus = async () => {
- try {
- const res = await apiFetch('/admin/queue/status')
- if (res.ok) {
- const data = await res.json()
- setQueueStatus(data)
- }
- } catch (e) {
- console.error('Failed to fetch queue status:', e)
- }
- }
-
- useEffect(() => {
- fetchAccounts()
- fetchQueueStatus()
- const interval = setInterval(fetchQueueStatus, 5000)
- return () => clearInterval(interval)
- }, [])
-
- return {
- queueStatus,
- keysExpanded,
- setKeysExpanded,
- accounts,
- page,
- pageSize,
- totalPages,
- totalAccounts,
- loadingAccounts,
- fetchAccounts,
- changePageSize,
- resolveAccountIdentifier,
- }
-}
+import { useEffect, useState } from 'react'
+
+export function useAccountsData({ apiFetch }) {
+ const [queueStatus, setQueueStatus] = useState(null)
+ const [keysExpanded, setKeysExpanded] = useState(false)
+
+ const [accounts, setAccounts] = useState([])
+ const [page, setPage] = useState(1)
+ const [pageSize, setPageSize] = useState(10)
+ const [totalPages, setTotalPages] = useState(1)
+ const [totalAccounts, setTotalAccounts] = useState(0)
+ const [loadingAccounts, setLoadingAccounts] = useState(false)
+
+ const resolveAccountIdentifier = (acc) => {
+ if (!acc || typeof acc !== 'object') return ''
+ return String(acc.identifier || acc.email || acc.mobile || '').trim()
+ }
+
+ const [searchQuery, setSearchQuery] = useState('')
+
+ const fetchAccounts = async (targetPage = page, targetPageSize = pageSize, targetQuery = searchQuery) => {
+ setLoadingAccounts(true)
+ try {
+ let url = `/admin/accounts?page=${targetPage}&page_size=${targetPageSize}`
+ if (targetQuery.trim()) url += `&q=${encodeURIComponent(targetQuery.trim())}`
+ const res = await apiFetch(url)
+ if (res.ok) {
+ const data = await res.json()
+ setAccounts(data.items || [])
+ setTotalPages(data.total_pages || 1)
+ setTotalAccounts(data.total || 0)
+ setPage(data.page || 1)
+ }
+ } catch (e) {
+ console.error('Failed to fetch accounts:', e)
+ } finally {
+ setLoadingAccounts(false)
+ }
+ }
+
+ const changePageSize = (newSize) => {
+ setPageSize(newSize)
+ fetchAccounts(1, newSize)
+ }
+
+ const handleSearchChange = (query) => {
+ setSearchQuery(query)
+ fetchAccounts(1, pageSize, query)
+ }
+
+ const fetchQueueStatus = async () => {
+ try {
+ const res = await apiFetch('/admin/queue/status')
+ if (res.ok) {
+ const data = await res.json()
+ setQueueStatus(data)
+ }
+ } catch (e) {
+ console.error('Failed to fetch queue status:', e)
+ }
+ }
+
+ useEffect(() => {
+ fetchAccounts()
+ fetchQueueStatus()
+ const interval = setInterval(fetchQueueStatus, 5000)
+ return () => clearInterval(interval)
+ }, [])
+
+ return {
+ queueStatus,
+ keysExpanded,
+ setKeysExpanded,
+ accounts,
+ page,
+ pageSize,
+ totalPages,
+ totalAccounts,
+ loadingAccounts,
+ fetchAccounts,
+ changePageSize,
+ resolveAccountIdentifier,
+ searchQuery,
+ handleSearchChange,
+ }
+}
diff --git a/webui/src/locales/en.json b/webui/src/locales/en.json
index bd38fd0..499509e 100644
--- a/webui/src/locales/en.json
+++ b/webui/src/locales/en.json
@@ -1,295 +1,297 @@
-{
- "language": {
- "label": "Language",
- "english": "English",
- "chinese": "中文"
- },
- "nav": {
- "accounts": {
- "label": "Account Management",
- "desc": "Manage the DeepSeek account pool"
- },
- "test": {
- "label": "API Test",
- "desc": "Test API connectivity and responses"
- },
- "import": {
- "label": "Batch Import",
- "desc": "Bulk import account configuration"
- },
- "vercel": {
- "label": "Vercel Sync",
- "desc": "Sync configuration to Vercel"
- },
- "settings": {
- "label": "Settings",
- "desc": "Edit runtime and security settings online"
- }
- },
- "sidebar": {
- "onlineAdminConsole": "Online Admin Console",
- "systemStatus": "System Status",
- "statusOnline": "Online",
- "accounts": "Accounts",
- "keys": "Keys",
- "signOut": "Sign out"
- },
- "auth": {
- "expired": "Authentication expired. Please sign in again.",
- "checking": "Checking authentication status..."
- },
- "errors": {
- "fetchConfig": "Failed to fetch configuration: {error}"
- },
- "actions": {
- "cancel": "Cancel",
- "add": "Add",
- "delete": "Delete",
- "copy": "Copy",
- "generate": "Generate",
- "test": "Test",
- "testing": "Testing...",
- "loading": "Loading..."
- },
- "messages": {
- "deleted": "Deleted successfully",
- "deleteFailed": "Delete failed",
- "failedToAdd": "Failed to add",
- "networkError": "Network error.",
- "requestFailed": "Request failed.",
- "generationStopped": "Generation stopped.",
- "invalidJson": "Invalid JSON format.",
- "importFailed": "Import failed.",
- "copyFailed": "Copy failed."
- },
- "landing": {
- "adminConsole": "Admin Console",
- "apiStatus": "API Status",
- "features": {
- "compatibility": {
- "title": "Full Compatibility",
- "desc": "OpenAI & Claude format support"
- },
- "loadBalancing": {
- "title": "Load Balancing",
- "desc": "Smart rotation with stable throughput"
- },
- "reasoning": {
- "title": "Deep Reasoning",
- "desc": "Expose reasoning traces when enabled"
- },
- "search": {
- "title": "Web Search",
- "desc": "Integrated native web search"
- }
- }
- },
- "accountManager": {
- "addKeySuccess": "API key added successfully.",
- "addAccountSuccess": "Account added successfully.",
- "requiredFields": "Password and email/mobile are required.",
- "deleteKeyConfirm": "Are you sure you want to delete this API key?",
- "deleteAccountConfirm": "Are you sure you want to delete this account?",
- "invalidIdentifier": "Invalid account identifier. Operation aborted.",
- "testAllConfirm": "Test API connectivity for all accounts?",
- "testAllCompleted": "Completed: {success}/{total} available",
- "testFailed": "Test failed: {error}",
- "available": "Available",
- "inUse": "In use",
- "totalPool": "Total pool",
- "accountsUnit": "accounts",
- "threadsUnit": "threads",
- "apiKeysTitle": "API Keys",
- "apiKeysDesc": "Manage the API access key pool",
- "addKey": "Add key",
- "copied": "Copied",
- "copyKeyTitle": "Copy key",
- "deleteKeyTitle": "Delete key",
- "noApiKeys": "No API keys found.",
- "accountsTitle": "DeepSeek Accounts",
- "accountsDesc": "Manage the DeepSeek account pool",
- "testAll": "Test all",
- "addAccount": "Add account",
- "testingAllAccounts": "Testing all accounts...",
- "sessionActive": "Session active",
- "reauthRequired": "Re-auth required",
- "testStatusFailed": "Last test failed",
- "noAccounts": "No accounts found.",
- "modalAddKeyTitle": "Add API key",
- "newKeyLabel": "New key value",
- "newKeyPlaceholder": "Enter a custom API key",
- "generate": "Generate",
- "generateHint": "Click Generate to create a random key.",
- "addKeyLoading": "Adding...",
- "addKeyAction": "Add key",
- "modalAddAccountTitle": "Add DeepSeek account",
- "emailOptional": "Email (optional)",
- "mobileOptional": "Mobile (optional)",
- "passwordLabel": "Password",
- "passwordPlaceholder": "Account password",
- "addAccountLoading": "Adding...",
- "addAccountAction": "Add account",
- "pageInfo": "Page {current}/{total}, {count} accounts total"
- },
- "apiTester": {
- "defaultMessage": "Hello, please introduce yourself in one sentence.",
- "models": {
- "chat": "Non-reasoning model",
- "reasoner": "Reasoning model",
- "chatSearch": "Non-reasoning model (with search)",
- "reasonerSearch": "Reasoning model (with search)"
- },
- "missingApiKey": "Please provide an API key.",
- "requestFailed": "Request failed.",
- "networkError": "Network error: {error}",
- "testSuccess": "{account}: Test successful ({time}ms)",
- "config": "Configuration",
- "modelLabel": "Model",
- "streamMode": "Streaming",
- "accountSelector": "Account",
- "autoRandom": "🤖 Auto / Random",
- "apiKeyOptional": "API Key (optional)",
- "apiKeyDefault": "Default: ...{suffix}",
- "apiKeyPlaceholder": "Enter a custom key",
- "modeManaged": "Managed key mode (uses account pool).",
- "modeDirect": "Direct token mode (requires a valid DeepSeek token).",
- "statusError": "Error",
- "reasoningTrace": "Reasoning Trace",
- "generating": "Generating response...",
- "enterMessage": "Enter a message...",
- "adminConsoleLabel": "DeepSeek admin console"
- },
- "batchImport": {
- "templates": {
- "full": {
- "name": "Full configuration template",
- "desc": "Includes keys, accounts, and model mapping"
- },
- "emailOnly": {
- "name": "Email-only accounts",
- "desc": "Batch import accounts using email login"
- },
- "mobileOnly": {
- "name": "Mobile-only accounts",
- "desc": "Batch import accounts using mobile login"
- },
- "keysOnly": {
- "name": "API keys only",
- "desc": "Add API access keys only"
- }
- },
- "enterJson": "Please provide JSON configuration content.",
- "importSuccess": "Import successful: {keys} keys, {accounts} accounts",
- "templateLoaded": "Template loaded: {name}",
- "currentConfigLoaded": "Current configuration loaded.",
- "fetchConfigFailed": "Failed to fetch configuration.",
- "copySuccess": "Base64 configuration copied to clipboard.",
- "quickTemplates": "Quick Templates",
- "dataExport": "Data Export",
- "dataExportDesc": "Copy the Base64-encoded configuration for Vercel environment variables.",
- "copyBase64": "Copy Base64 config",
- "copied": "Copied",
- "variableName": "Variable name",
- "jsonEditor": "JSON Editor",
- "loadCurrentConfig": "Load current config",
- "applyConfig": "Apply config",
- "importing": "Importing...",
- "importComplete": "Import complete",
- "importSummary": "Imported {keys} API keys and updated {accounts} accounts."
- },
- "settings": {
- "loadFailed": "Failed to load settings.",
- "nonJsonResponse": "Unexpected non-JSON response from server (status: {status}).",
- "save": "Save settings",
- "saving": "Saving...",
- "saveSuccess": "Settings saved and hot reloaded.",
- "saveFailed": "Failed to save settings.",
- "securityTitle": "Security",
- "jwtExpireHours": "JWT expiry (hours)",
- "newPassword": "New admin password",
- "newPasswordPlaceholder": "Enter new password (min 4 chars)",
- "updatePassword": "Update password",
- "updating": "Updating...",
- "passwordTooShort": "Password must be at least 4 characters.",
- "passwordUpdated": "Password updated. Please sign in again.",
- "passwordUpdateFailed": "Failed to update password.",
- "runtimeTitle": "Concurrency & Queue",
- "accountMaxInflight": "Per-account max inflight",
- "accountMaxQueue": "Account max queue size",
- "globalMaxInflight": "Global max inflight",
- "behaviorTitle": "Behavior",
- "toolcallMode": "Toolcall mode",
- "earlyEmitConfidence": "Early emit confidence",
- "responsesTTL": "Responses store TTL (seconds)",
- "embeddingsProvider": "Embeddings provider",
- "modelTitle": "Model mapping",
- "claudeMapping": "Claude mapping (JSON)",
- "modelAliases": "Model aliases (JSON)",
- "backupTitle": "Backup & Restore",
- "loadExport": "Load current export",
- "importModeMerge": "Merge import (default)",
- "importModeReplace": "Replace all import",
- "importNow": "Import now",
- "importing": "Importing...",
- "importPlaceholder": "Paste config JSON to import",
- "importEmpty": "Please input import JSON.",
- "importInvalidJson": "Import JSON is invalid.",
- "importFailed": "Import failed.",
- "importSuccess": "Config imported (mode: {mode}).",
- "exportFailed": "Export failed.",
- "exportLoaded": "Current export loaded.",
- "exportJson": "Export JSON",
- "invalidJsonField": "{field} is not a valid JSON object.",
- "defaultPasswordWarning": "You are using the default admin password \"admin\". Please change it.",
- "vercelSyncHint": "Configuration changed. For Vercel deployments, sync manually in Vercel Sync and redeploy.",
- "autoFetchPaused": "Auto loading paused after {count} failures: {error}",
- "retryLoad": "Retry now"
- },
- "login": {
- "welcome": "Welcome back",
- "subtitle": "Enter your admin key to continue",
- "adminKeyLabel": "Admin key",
- "adminKeyPlaceholder": "Enter your admin key...",
- "rememberSession": "Remember this session",
- "signIn": "Sign in",
- "secureConnection": "Secure connection",
- "adminPortal": "DS2API admin portal",
- "signInFailed": "Sign-in failed.",
- "networkError": "Network error: {error}"
- },
- "vercel": {
- "tokenRequired": "Vercel access token is required.",
- "projectRequired": "Project ID is required.",
- "syncFailed": "Sync failed.",
- "networkError": "Network error.",
- "title": "Vercel Deployment",
- "description": "Sync the current keys and accounts directly to Vercel environment variables.",
- "tokenLabel": "Vercel Access Token",
- "getToken": "Get token",
- "tokenPlaceholderPreconfig": "Using preconfigured token",
- "tokenPlaceholder": "Enter Vercel access token",
- "projectIdLabel": "Project ID",
- "projectIdHint": "Find it in Project Settings → General.",
- "teamIdLabel": "Team ID",
- "optional": "optional",
- "syncing": "Syncing...",
- "syncRedeploy": "Sync & redeploy",
- "redeployHint": "This triggers a Vercel redeploy and usually takes 30–60 seconds.",
- "syncSucceeded": "Sync succeeded",
- "syncFailedLabel": "Sync failed",
- "openDeployment": "Open deployment",
- "statusSynced": "Synced",
- "statusNotSynced": "Not synced",
- "statusNeverSynced": "Never synced",
- "lastSyncTime": "Last sync: {time}",
- "pollPaused": "Status polling paused after {count} failures.",
- "manualRefresh": "Refresh manually",
- "howItWorks": "How it works",
- "steps": {
- "one": "The current configuration (keys and accounts) is exported as JSON.",
- "two": "The JSON is Base64-encoded for safe formatting.",
- "three": "Update the env var in Vercel:",
- "four": "Trigger a redeploy to apply the updated environment variables."
- }
- }
-}
+{
+ "language": {
+ "label": "Language",
+ "english": "English",
+ "chinese": "中文"
+ },
+ "nav": {
+ "accounts": {
+ "label": "Account Management",
+ "desc": "Manage the DeepSeek account pool"
+ },
+ "test": {
+ "label": "API Test",
+ "desc": "Test API connectivity and responses"
+ },
+ "import": {
+ "label": "Batch Import",
+ "desc": "Bulk import account configuration"
+ },
+ "vercel": {
+ "label": "Vercel Sync",
+ "desc": "Sync configuration to Vercel"
+ },
+ "settings": {
+ "label": "Settings",
+ "desc": "Edit runtime and security settings online"
+ }
+ },
+ "sidebar": {
+ "onlineAdminConsole": "Online Admin Console",
+ "systemStatus": "System Status",
+ "statusOnline": "Online",
+ "accounts": "Accounts",
+ "keys": "Keys",
+ "signOut": "Sign out"
+ },
+ "auth": {
+ "expired": "Authentication expired. Please sign in again.",
+ "checking": "Checking authentication status..."
+ },
+ "errors": {
+ "fetchConfig": "Failed to fetch configuration: {error}"
+ },
+ "actions": {
+ "cancel": "Cancel",
+ "add": "Add",
+ "delete": "Delete",
+ "copy": "Copy",
+ "generate": "Generate",
+ "test": "Test",
+ "testing": "Testing...",
+ "loading": "Loading..."
+ },
+ "messages": {
+ "deleted": "Deleted successfully",
+ "deleteFailed": "Delete failed",
+ "failedToAdd": "Failed to add",
+ "networkError": "Network error.",
+ "requestFailed": "Request failed.",
+ "generationStopped": "Generation stopped.",
+ "invalidJson": "Invalid JSON format.",
+ "importFailed": "Import failed.",
+ "copyFailed": "Copy failed."
+ },
+ "landing": {
+ "adminConsole": "Admin Console",
+ "apiStatus": "API Status",
+ "features": {
+ "compatibility": {
+ "title": "Full Compatibility",
+ "desc": "OpenAI & Claude format support"
+ },
+ "loadBalancing": {
+ "title": "Load Balancing",
+ "desc": "Smart rotation with stable throughput"
+ },
+ "reasoning": {
+ "title": "Deep Reasoning",
+ "desc": "Expose reasoning traces when enabled"
+ },
+ "search": {
+ "title": "Web Search",
+ "desc": "Integrated native web search"
+ }
+ }
+ },
+ "accountManager": {
+ "addKeySuccess": "API key added successfully.",
+ "addAccountSuccess": "Account added successfully.",
+ "requiredFields": "Password and email/mobile are required.",
+ "deleteKeyConfirm": "Are you sure you want to delete this API key?",
+ "deleteAccountConfirm": "Are you sure you want to delete this account?",
+ "invalidIdentifier": "Invalid account identifier. Operation aborted.",
+ "testAllConfirm": "Test API connectivity for all accounts?",
+ "testAllCompleted": "Completed: {success}/{total} available",
+ "testFailed": "Test failed: {error}",
+ "available": "Available",
+ "inUse": "In use",
+ "totalPool": "Total pool",
+ "accountsUnit": "accounts",
+ "threadsUnit": "threads",
+ "apiKeysTitle": "API Keys",
+ "apiKeysDesc": "Manage the API access key pool",
+ "addKey": "Add key",
+ "copied": "Copied",
+ "copyKeyTitle": "Copy key",
+ "deleteKeyTitle": "Delete key",
+ "noApiKeys": "No API keys found.",
+ "accountsTitle": "DeepSeek Accounts",
+ "accountsDesc": "Manage the DeepSeek account pool",
+ "testAll": "Test all",
+ "addAccount": "Add account",
+ "testingAllAccounts": "Testing all accounts...",
+ "sessionActive": "Session active",
+ "reauthRequired": "Re-auth required",
+ "testStatusFailed": "Last test failed",
+ "noAccounts": "No accounts found.",
+ "modalAddKeyTitle": "Add API key",
+ "newKeyLabel": "New key value",
+ "newKeyPlaceholder": "Enter a custom API key",
+ "generate": "Generate",
+ "generateHint": "Click Generate to create a random key.",
+ "addKeyLoading": "Adding...",
+ "addKeyAction": "Add key",
+ "modalAddAccountTitle": "Add DeepSeek account",
+ "emailOptional": "Email (optional)",
+ "mobileOptional": "Mobile (optional)",
+ "passwordLabel": "Password",
+ "passwordPlaceholder": "Account password",
+ "addAccountLoading": "Adding...",
+ "addAccountAction": "Add account",
+ "pageInfo": "Page {current}/{total}, {count} accounts total",
+ "searchPlaceholder": "Search accounts...",
+ "searchNoResults": "No accounts match your search"
+ },
+ "apiTester": {
+ "defaultMessage": "Hello, please introduce yourself in one sentence.",
+ "models": {
+ "chat": "Non-reasoning model",
+ "reasoner": "Reasoning model",
+ "chatSearch": "Non-reasoning model (with search)",
+ "reasonerSearch": "Reasoning model (with search)"
+ },
+ "missingApiKey": "Please provide an API key.",
+ "requestFailed": "Request failed.",
+ "networkError": "Network error: {error}",
+ "testSuccess": "{account}: Test successful ({time}ms)",
+ "config": "Configuration",
+ "modelLabel": "Model",
+ "streamMode": "Streaming",
+ "accountSelector": "Account",
+ "autoRandom": "🤖 Auto / Random",
+ "apiKeyOptional": "API Key (optional)",
+ "apiKeyDefault": "Default: ...{suffix}",
+ "apiKeyPlaceholder": "Enter a custom key",
+ "modeManaged": "Managed key mode (uses account pool).",
+ "modeDirect": "Direct token mode (requires a valid DeepSeek token).",
+ "statusError": "Error",
+ "reasoningTrace": "Reasoning Trace",
+ "generating": "Generating response...",
+ "enterMessage": "Enter a message...",
+ "adminConsoleLabel": "DeepSeek admin console"
+ },
+ "batchImport": {
+ "templates": {
+ "full": {
+ "name": "Full configuration template",
+ "desc": "Includes keys, accounts, and model mapping"
+ },
+ "emailOnly": {
+ "name": "Email-only accounts",
+ "desc": "Batch import accounts using email login"
+ },
+ "mobileOnly": {
+ "name": "Mobile-only accounts",
+ "desc": "Batch import accounts using mobile login"
+ },
+ "keysOnly": {
+ "name": "API keys only",
+ "desc": "Add API access keys only"
+ }
+ },
+ "enterJson": "Please provide JSON configuration content.",
+ "importSuccess": "Import successful: {keys} keys, {accounts} accounts",
+ "templateLoaded": "Template loaded: {name}",
+ "currentConfigLoaded": "Current configuration loaded.",
+ "fetchConfigFailed": "Failed to fetch configuration.",
+ "copySuccess": "Base64 configuration copied to clipboard.",
+ "quickTemplates": "Quick Templates",
+ "dataExport": "Data Export",
+ "dataExportDesc": "Copy the Base64-encoded configuration for Vercel environment variables.",
+ "copyBase64": "Copy Base64 config",
+ "copied": "Copied",
+ "variableName": "Variable name",
+ "jsonEditor": "JSON Editor",
+ "loadCurrentConfig": "Load current config",
+ "applyConfig": "Apply config",
+ "importing": "Importing...",
+ "importComplete": "Import complete",
+ "importSummary": "Imported {keys} API keys and updated {accounts} accounts."
+ },
+ "settings": {
+ "loadFailed": "Failed to load settings.",
+ "nonJsonResponse": "Unexpected non-JSON response from server (status: {status}).",
+ "save": "Save settings",
+ "saving": "Saving...",
+ "saveSuccess": "Settings saved and hot reloaded.",
+ "saveFailed": "Failed to save settings.",
+ "securityTitle": "Security",
+ "jwtExpireHours": "JWT expiry (hours)",
+ "newPassword": "New admin password",
+ "newPasswordPlaceholder": "Enter new password (min 4 chars)",
+ "updatePassword": "Update password",
+ "updating": "Updating...",
+ "passwordTooShort": "Password must be at least 4 characters.",
+ "passwordUpdated": "Password updated. Please sign in again.",
+ "passwordUpdateFailed": "Failed to update password.",
+ "runtimeTitle": "Concurrency & Queue",
+ "accountMaxInflight": "Per-account max inflight",
+ "accountMaxQueue": "Account max queue size",
+ "globalMaxInflight": "Global max inflight",
+ "behaviorTitle": "Behavior",
+ "toolcallMode": "Toolcall mode",
+ "earlyEmitConfidence": "Early emit confidence",
+ "responsesTTL": "Responses store TTL (seconds)",
+ "embeddingsProvider": "Embeddings provider",
+ "modelTitle": "Model mapping",
+ "claudeMapping": "Claude mapping (JSON)",
+ "modelAliases": "Model aliases (JSON)",
+ "backupTitle": "Backup & Restore",
+ "loadExport": "Load current export",
+ "importModeMerge": "Merge import (default)",
+ "importModeReplace": "Replace all import",
+ "importNow": "Import now",
+ "importing": "Importing...",
+ "importPlaceholder": "Paste config JSON to import",
+ "importEmpty": "Please input import JSON.",
+ "importInvalidJson": "Import JSON is invalid.",
+ "importFailed": "Import failed.",
+ "importSuccess": "Config imported (mode: {mode}).",
+ "exportFailed": "Export failed.",
+ "exportLoaded": "Current export loaded.",
+ "exportJson": "Export JSON",
+ "invalidJsonField": "{field} is not a valid JSON object.",
+ "defaultPasswordWarning": "You are using the default admin password \"admin\". Please change it.",
+ "vercelSyncHint": "Configuration changed. For Vercel deployments, sync manually in Vercel Sync and redeploy.",
+ "autoFetchPaused": "Auto loading paused after {count} failures: {error}",
+ "retryLoad": "Retry now"
+ },
+ "login": {
+ "welcome": "Welcome back",
+ "subtitle": "Enter your admin key to continue",
+ "adminKeyLabel": "Admin key",
+ "adminKeyPlaceholder": "Enter your admin key...",
+ "rememberSession": "Remember this session",
+ "signIn": "Sign in",
+ "secureConnection": "Secure connection",
+ "adminPortal": "DS2API admin portal",
+ "signInFailed": "Sign-in failed.",
+ "networkError": "Network error: {error}"
+ },
+ "vercel": {
+ "tokenRequired": "Vercel access token is required.",
+ "projectRequired": "Project ID is required.",
+ "syncFailed": "Sync failed.",
+ "networkError": "Network error.",
+ "title": "Vercel Deployment",
+ "description": "Sync the current keys and accounts directly to Vercel environment variables.",
+ "tokenLabel": "Vercel Access Token",
+ "getToken": "Get token",
+ "tokenPlaceholderPreconfig": "Using preconfigured token",
+ "tokenPlaceholder": "Enter Vercel access token",
+ "projectIdLabel": "Project ID",
+ "projectIdHint": "Find it in Project Settings → General.",
+ "teamIdLabel": "Team ID",
+ "optional": "optional",
+ "syncing": "Syncing...",
+ "syncRedeploy": "Sync & redeploy",
+ "redeployHint": "This triggers a Vercel redeploy and usually takes 30–60 seconds.",
+ "syncSucceeded": "Sync succeeded",
+ "syncFailedLabel": "Sync failed",
+ "openDeployment": "Open deployment",
+ "statusSynced": "Synced",
+ "statusNotSynced": "Not synced",
+ "statusNeverSynced": "Never synced",
+ "lastSyncTime": "Last sync: {time}",
+ "pollPaused": "Status polling paused after {count} failures.",
+ "manualRefresh": "Refresh manually",
+ "howItWorks": "How it works",
+ "steps": {
+ "one": "The current configuration (keys and accounts) is exported as JSON.",
+ "two": "The JSON is Base64-encoded for safe formatting.",
+ "three": "Update the env var in Vercel:",
+ "four": "Trigger a redeploy to apply the updated environment variables."
+ }
+ }
+}
diff --git a/webui/src/locales/zh.json b/webui/src/locales/zh.json
index 1206f89..27150b4 100644
--- a/webui/src/locales/zh.json
+++ b/webui/src/locales/zh.json
@@ -1,295 +1,297 @@
-{
- "language": {
- "label": "语言",
- "english": "English",
- "chinese": "中文"
- },
- "nav": {
- "accounts": {
- "label": "账号管理",
- "desc": "管理 DeepSeek 账号池"
- },
- "test": {
- "label": "API 测试",
- "desc": "测试 API 连接与响应"
- },
- "import": {
- "label": "批量导入",
- "desc": "批量导入账号配置"
- },
- "vercel": {
- "label": "Vercel 同步",
- "desc": "同步配置到 Vercel"
- },
- "settings": {
- "label": "设置中心",
- "desc": "在线修改系统设置与配置"
- }
- },
- "sidebar": {
- "onlineAdminConsole": "在线管理面板",
- "systemStatus": "系统状态",
- "statusOnline": "在线",
- "accounts": "账号",
- "keys": "密钥",
- "signOut": "退出登录"
- },
- "auth": {
- "expired": "认证已过期,请重新登录",
- "checking": "正在检查登录状态..."
- },
- "errors": {
- "fetchConfig": "获取配置失败: {error}"
- },
- "actions": {
- "cancel": "取消",
- "add": "添加",
- "delete": "删除",
- "copy": "复制",
- "generate": "生成",
- "test": "测试",
- "testing": "正在测试...",
- "loading": "加载中..."
- },
- "messages": {
- "deleted": "删除成功",
- "deleteFailed": "删除失败",
- "failedToAdd": "添加失败",
- "networkError": "网络错误",
- "requestFailed": "请求失败",
- "generationStopped": "已停止生成",
- "invalidJson": "无效的 JSON 格式",
- "importFailed": "导入失败",
- "copyFailed": "复制失败"
- },
- "landing": {
- "adminConsole": "管理面板",
- "apiStatus": "API 状态",
- "features": {
- "compatibility": {
- "title": "全面兼容",
- "desc": "适配 OpenAI 与 Claude 格式"
- },
- "loadBalancing": {
- "title": "负载均衡",
- "desc": "智能轮询,稳定高效"
- },
- "reasoning": {
- "title": "深度思考",
- "desc": "支持推理过程输出"
- },
- "search": {
- "title": "联网搜索",
- "desc": "集成原生网页搜索能力"
- }
- }
- },
- "accountManager": {
- "addKeySuccess": "API 密钥添加成功",
- "addAccountSuccess": "账号添加成功",
- "requiredFields": "需要填写密码以及邮箱或手机号",
- "deleteKeyConfirm": "确定要删除此 API 密钥吗?",
- "deleteAccountConfirm": "确定要删除此账号吗?",
- "invalidIdentifier": "账号标识无效,无法执行操作",
- "testAllConfirm": "测试所有账号的 API 连通性?",
- "testAllCompleted": "完成:{success}/{total} 可用",
- "testFailed": "测试失败: {error}",
- "available": "可用",
- "inUse": "正在使用",
- "totalPool": "账号池总数",
- "accountsUnit": "个账号",
- "threadsUnit": "线程",
- "apiKeysTitle": "API 密钥",
- "apiKeysDesc": "管理 API 访问密钥池",
- "addKey": "添加密钥",
- "copied": "已复制",
- "copyKeyTitle": "复制密钥",
- "deleteKeyTitle": "删除密钥",
- "noApiKeys": "未找到 API 密钥",
- "accountsTitle": "DeepSeek 账号",
- "accountsDesc": "管理 DeepSeek 账号池",
- "testAll": "测试全部",
- "addAccount": "添加账号",
- "testingAllAccounts": "正在测试所有账号...",
- "sessionActive": "已建立会话",
- "reauthRequired": "需重新登录",
- "testStatusFailed": "上次测试失败",
- "noAccounts": "未找到任何账号",
- "modalAddKeyTitle": "添加 API 密钥",
- "newKeyLabel": "新密钥值",
- "newKeyPlaceholder": "输入自定义 API 密钥",
- "generate": "生成",
- "generateHint": "点击「生成」自动创建随机密钥",
- "addKeyLoading": "添加中...",
- "addKeyAction": "添加密钥",
- "modalAddAccountTitle": "添加 DeepSeek 账号",
- "emailOptional": "邮箱 (可选)",
- "mobileOptional": "手机号 (可选)",
- "passwordLabel": "密码",
- "passwordPlaceholder": "账号密码",
- "addAccountLoading": "添加中...",
- "addAccountAction": "添加账号",
- "pageInfo": "第 {current}/{total} 页,共 {count} 个账号"
- },
- "apiTester": {
- "defaultMessage": "你好,请用一句话介绍你自己。",
- "models": {
- "chat": "非思考模型",
- "reasoner": "思考模型",
- "chatSearch": "非思考模型 (带搜索)",
- "reasonerSearch": "思考模型 (带搜索)"
- },
- "missingApiKey": "请提供 API 密钥",
- "requestFailed": "请求失败",
- "networkError": "网络错误: {error}",
- "testSuccess": "{account}: 测试成功 ({time}ms)",
- "config": "配置",
- "modelLabel": "模型",
- "streamMode": "流式模式",
- "accountSelector": "选择账号",
- "autoRandom": "🤖 自动 / 随机",
- "apiKeyOptional": "API 密钥 (可选)",
- "apiKeyDefault": "默认: ...{suffix}",
- "apiKeyPlaceholder": "输入自定义密钥",
- "modeManaged": "当前使用托管 key 模式(会走账号池)。",
- "modeDirect": "当前使用直通 token 模式(需填写有效 DeepSeek token)。",
- "statusError": "错误",
- "reasoningTrace": "思维链过程",
- "generating": "正在生成响应...",
- "enterMessage": "输入消息...",
- "adminConsoleLabel": "DeepSeek 管理员界面"
- },
- "batchImport": {
- "templates": {
- "full": {
- "name": "全量配置模板",
- "desc": "包含密钥、账号及模型映射"
- },
- "emailOnly": {
- "name": "仅邮箱账号",
- "desc": "批量导入邮箱格式账号"
- },
- "mobileOnly": {
- "name": "仅手机号账号",
- "desc": "批量导入手机号格式账号"
- },
- "keysOnly": {
- "name": "仅 API 密钥",
- "desc": "仅添加 API 访问密钥"
- }
- },
- "enterJson": "请输入 JSON 配置内容",
- "importSuccess": "导入成功: {keys} 个密钥, {accounts} 个账号",
- "templateLoaded": "已加载模板: {name}",
- "currentConfigLoaded": "当前配置已加载",
- "fetchConfigFailed": "获取配置失败",
- "copySuccess": "Base64 配置已复制到剪贴板",
- "quickTemplates": "快速模板",
- "dataExport": "数据导出",
- "dataExportDesc": "获取配置的 Base64 字符串,用于 Vercel 环境变量。",
- "copyBase64": "复制 Base64 配置",
- "copied": "已复制",
- "variableName": "变量名",
- "jsonEditor": "JSON 编辑器",
- "loadCurrentConfig": "加载当前配置",
- "applyConfig": "应用配置",
- "importing": "正在导入...",
- "importComplete": "导入操作已完成",
- "importSummary": "成功导入了 {keys} 个 API 密钥,并更新了 {accounts} 个账号。"
- },
- "settings": {
- "loadFailed": "加载设置失败",
- "nonJsonResponse": "服务端返回了非 JSON 响应(状态码:{status})",
- "save": "保存设置",
- "saving": "保存中...",
- "saveSuccess": "设置已保存并热更新生效",
- "saveFailed": "保存设置失败",
- "securityTitle": "安全设置",
- "jwtExpireHours": "JWT 有效期(小时)",
- "newPassword": "面板新密码",
- "newPasswordPlaceholder": "输入新密码(至少 4 位)",
- "updatePassword": "修改密码",
- "updating": "更新中...",
- "passwordTooShort": "新密码至少 4 位",
- "passwordUpdated": "密码已更新,需重新登录",
- "passwordUpdateFailed": "密码更新失败",
- "runtimeTitle": "并发与队列",
- "accountMaxInflight": "每账号并发上限",
- "accountMaxQueue": "账号等待队列上限",
- "globalMaxInflight": "全局并发上限",
- "behaviorTitle": "行为设置",
- "toolcallMode": "Toolcall 模式",
- "earlyEmitConfidence": "早发置信度",
- "responsesTTL": "Responses 缓存 TTL(秒)",
- "embeddingsProvider": "Embeddings Provider",
- "modelTitle": "模型映射",
- "claudeMapping": "Claude 映射(JSON)",
- "modelAliases": "模型别名(JSON)",
- "backupTitle": "备份与恢复",
- "loadExport": "加载当前导出",
- "importModeMerge": "合并导入(默认)",
- "importModeReplace": "全量覆盖导入",
- "importNow": "立即导入",
- "importing": "导入中...",
- "importPlaceholder": "粘贴要导入的 JSON 配置",
- "importEmpty": "请先输入导入 JSON",
- "importInvalidJson": "导入 JSON 格式无效",
- "importFailed": "导入失败",
- "importSuccess": "配置导入成功(模式:{mode})",
- "exportFailed": "导出失败",
- "exportLoaded": "已加载当前配置导出",
- "exportJson": "导出 JSON",
- "invalidJsonField": "{field} 不是有效 JSON 对象",
- "defaultPasswordWarning": "当前使用默认密码 admin,请尽快在此修改。",
- "vercelSyncHint": "当前配置已更新。Vercel 部署请到 Vercel 同步页面手动同步并重部署。",
- "autoFetchPaused": "自动加载已暂停:连续失败 {count} 次({error})",
- "retryLoad": "立即重试"
- },
- "login": {
- "welcome": "欢迎回来",
- "subtitle": "请输入管理员密钥以继续",
- "adminKeyLabel": "管理员密钥",
- "adminKeyPlaceholder": "输入您的管理员密钥...",
- "rememberSession": "记住登录状态",
- "signIn": "登录",
- "secureConnection": "安全连接",
- "adminPortal": "DS2API 管理员门户",
- "signInFailed": "登录失败",
- "networkError": "网络错误: {error}"
- },
- "vercel": {
- "tokenRequired": "需要 Vercel 访问令牌",
- "projectRequired": "需要项目 ID",
- "syncFailed": "同步失败",
- "networkError": "网络错误",
- "title": "Vercel 部署",
- "description": "将当前密钥和账号配置直接同步到 Vercel 环境变量中。",
- "tokenLabel": "Vercel 访问令牌",
- "getToken": "获取令牌",
- "tokenPlaceholderPreconfig": "正在使用预配置的令牌",
- "tokenPlaceholder": "输入 Vercel 访问令牌",
- "projectIdLabel": "项目 ID",
- "projectIdHint": "可在项目设置 (Project Settings) → 常规 (General) 中找到",
- "teamIdLabel": "团队 ID",
- "optional": "可选",
- "syncing": "正在同步...",
- "syncRedeploy": "同步并重新部署",
- "redeployHint": "这将触发 Vercel 的重新部署,大约需要 30-60 秒。",
- "syncSucceeded": "同步成功",
- "syncFailedLabel": "同步失败",
- "openDeployment": "访问部署地址",
- "statusSynced": "已同步",
- "statusNotSynced": "未同步",
- "statusNeverSynced": "从未同步",
- "lastSyncTime": "上次同步: {time}",
- "pollPaused": "状态轮询已暂停:连续失败 {count} 次。",
- "manualRefresh": "手动刷新",
- "howItWorks": "工作原理",
- "steps": {
- "one": "当前配置 (密钥和账号) 被导出为 JSON 字符串。",
- "two": "JSON 被编码为 Base64 以确保格式兼容性。",
- "three": "更新 Vercel 项目中的环境变量:",
- "four": "触发重新部署以应用新的环境变量。"
- }
- }
-}
+{
+ "language": {
+ "label": "语言",
+ "english": "English",
+ "chinese": "中文"
+ },
+ "nav": {
+ "accounts": {
+ "label": "账号管理",
+ "desc": "管理 DeepSeek 账号池"
+ },
+ "test": {
+ "label": "API 测试",
+ "desc": "测试 API 连接与响应"
+ },
+ "import": {
+ "label": "批量导入",
+ "desc": "批量导入账号配置"
+ },
+ "vercel": {
+ "label": "Vercel 同步",
+ "desc": "同步配置到 Vercel"
+ },
+ "settings": {
+ "label": "设置中心",
+ "desc": "在线修改系统设置与配置"
+ }
+ },
+ "sidebar": {
+ "onlineAdminConsole": "在线管理面板",
+ "systemStatus": "系统状态",
+ "statusOnline": "在线",
+ "accounts": "账号",
+ "keys": "密钥",
+ "signOut": "退出登录"
+ },
+ "auth": {
+ "expired": "认证已过期,请重新登录",
+ "checking": "正在检查登录状态..."
+ },
+ "errors": {
+ "fetchConfig": "获取配置失败: {error}"
+ },
+ "actions": {
+ "cancel": "取消",
+ "add": "添加",
+ "delete": "删除",
+ "copy": "复制",
+ "generate": "生成",
+ "test": "测试",
+ "testing": "正在测试...",
+ "loading": "加载中..."
+ },
+ "messages": {
+ "deleted": "删除成功",
+ "deleteFailed": "删除失败",
+ "failedToAdd": "添加失败",
+ "networkError": "网络错误",
+ "requestFailed": "请求失败",
+ "generationStopped": "已停止生成",
+ "invalidJson": "无效的 JSON 格式",
+ "importFailed": "导入失败",
+ "copyFailed": "复制失败"
+ },
+ "landing": {
+ "adminConsole": "管理面板",
+ "apiStatus": "API 状态",
+ "features": {
+ "compatibility": {
+ "title": "全面兼容",
+ "desc": "适配 OpenAI 与 Claude 格式"
+ },
+ "loadBalancing": {
+ "title": "负载均衡",
+ "desc": "智能轮询,稳定高效"
+ },
+ "reasoning": {
+ "title": "深度思考",
+ "desc": "支持推理过程输出"
+ },
+ "search": {
+ "title": "联网搜索",
+ "desc": "集成原生网页搜索能力"
+ }
+ }
+ },
+ "accountManager": {
+ "addKeySuccess": "API 密钥添加成功",
+ "addAccountSuccess": "账号添加成功",
+ "requiredFields": "需要填写密码以及邮箱或手机号",
+ "deleteKeyConfirm": "确定要删除此 API 密钥吗?",
+ "deleteAccountConfirm": "确定要删除此账号吗?",
+ "invalidIdentifier": "账号标识无效,无法执行操作",
+ "testAllConfirm": "测试所有账号的 API 连通性?",
+ "testAllCompleted": "完成:{success}/{total} 可用",
+ "testFailed": "测试失败: {error}",
+ "available": "可用",
+ "inUse": "正在使用",
+ "totalPool": "账号池总数",
+ "accountsUnit": "个账号",
+ "threadsUnit": "线程",
+ "apiKeysTitle": "API 密钥",
+ "apiKeysDesc": "管理 API 访问密钥池",
+ "addKey": "添加密钥",
+ "copied": "已复制",
+ "copyKeyTitle": "复制密钥",
+ "deleteKeyTitle": "删除密钥",
+ "noApiKeys": "未找到 API 密钥",
+ "accountsTitle": "DeepSeek 账号",
+ "accountsDesc": "管理 DeepSeek 账号池",
+ "testAll": "测试全部",
+ "addAccount": "添加账号",
+ "testingAllAccounts": "正在测试所有账号...",
+ "sessionActive": "已建立会话",
+ "reauthRequired": "需重新登录",
+ "testStatusFailed": "上次测试失败",
+ "noAccounts": "未找到任何账号",
+ "modalAddKeyTitle": "添加 API 密钥",
+ "newKeyLabel": "新密钥值",
+ "newKeyPlaceholder": "输入自定义 API 密钥",
+ "generate": "生成",
+ "generateHint": "点击「生成」自动创建随机密钥",
+ "addKeyLoading": "添加中...",
+ "addKeyAction": "添加密钥",
+ "modalAddAccountTitle": "添加 DeepSeek 账号",
+ "emailOptional": "邮箱 (可选)",
+ "mobileOptional": "手机号 (可选)",
+ "passwordLabel": "密码",
+ "passwordPlaceholder": "账号密码",
+ "addAccountLoading": "添加中...",
+ "addAccountAction": "添加账号",
+ "pageInfo": "第 {current}/{total} 页,共 {count} 个账号",
+ "searchPlaceholder": "搜索账号...",
+ "searchNoResults": "未找到匹配的账号"
+ },
+ "apiTester": {
+ "defaultMessage": "你好,请用一句话介绍你自己。",
+ "models": {
+ "chat": "非思考模型",
+ "reasoner": "思考模型",
+ "chatSearch": "非思考模型 (带搜索)",
+ "reasonerSearch": "思考模型 (带搜索)"
+ },
+ "missingApiKey": "请提供 API 密钥",
+ "requestFailed": "请求失败",
+ "networkError": "网络错误: {error}",
+ "testSuccess": "{account}: 测试成功 ({time}ms)",
+ "config": "配置",
+ "modelLabel": "模型",
+ "streamMode": "流式模式",
+ "accountSelector": "选择账号",
+ "autoRandom": "🤖 自动 / 随机",
+ "apiKeyOptional": "API 密钥 (可选)",
+ "apiKeyDefault": "默认: ...{suffix}",
+ "apiKeyPlaceholder": "输入自定义密钥",
+ "modeManaged": "当前使用托管 key 模式(会走账号池)。",
+ "modeDirect": "当前使用直通 token 模式(需填写有效 DeepSeek token)。",
+ "statusError": "错误",
+ "reasoningTrace": "思维链过程",
+ "generating": "正在生成响应...",
+ "enterMessage": "输入消息...",
+ "adminConsoleLabel": "DeepSeek 管理员界面"
+ },
+ "batchImport": {
+ "templates": {
+ "full": {
+ "name": "全量配置模板",
+ "desc": "包含密钥、账号及模型映射"
+ },
+ "emailOnly": {
+ "name": "仅邮箱账号",
+ "desc": "批量导入邮箱格式账号"
+ },
+ "mobileOnly": {
+ "name": "仅手机号账号",
+ "desc": "批量导入手机号格式账号"
+ },
+ "keysOnly": {
+ "name": "仅 API 密钥",
+ "desc": "仅添加 API 访问密钥"
+ }
+ },
+ "enterJson": "请输入 JSON 配置内容",
+ "importSuccess": "导入成功: {keys} 个密钥, {accounts} 个账号",
+ "templateLoaded": "已加载模板: {name}",
+ "currentConfigLoaded": "当前配置已加载",
+ "fetchConfigFailed": "获取配置失败",
+ "copySuccess": "Base64 配置已复制到剪贴板",
+ "quickTemplates": "快速模板",
+ "dataExport": "数据导出",
+ "dataExportDesc": "获取配置的 Base64 字符串,用于 Vercel 环境变量。",
+ "copyBase64": "复制 Base64 配置",
+ "copied": "已复制",
+ "variableName": "变量名",
+ "jsonEditor": "JSON 编辑器",
+ "loadCurrentConfig": "加载当前配置",
+ "applyConfig": "应用配置",
+ "importing": "正在导入...",
+ "importComplete": "导入操作已完成",
+ "importSummary": "成功导入了 {keys} 个 API 密钥,并更新了 {accounts} 个账号。"
+ },
+ "settings": {
+ "loadFailed": "加载设置失败",
+ "nonJsonResponse": "服务端返回了非 JSON 响应(状态码:{status})",
+ "save": "保存设置",
+ "saving": "保存中...",
+ "saveSuccess": "设置已保存并热更新生效",
+ "saveFailed": "保存设置失败",
+ "securityTitle": "安全设置",
+ "jwtExpireHours": "JWT 有效期(小时)",
+ "newPassword": "面板新密码",
+ "newPasswordPlaceholder": "输入新密码(至少 4 位)",
+ "updatePassword": "修改密码",
+ "updating": "更新中...",
+ "passwordTooShort": "新密码至少 4 位",
+ "passwordUpdated": "密码已更新,需重新登录",
+ "passwordUpdateFailed": "密码更新失败",
+ "runtimeTitle": "并发与队列",
+ "accountMaxInflight": "每账号并发上限",
+ "accountMaxQueue": "账号等待队列上限",
+ "globalMaxInflight": "全局并发上限",
+ "behaviorTitle": "行为设置",
+ "toolcallMode": "Toolcall 模式",
+ "earlyEmitConfidence": "早发置信度",
+ "responsesTTL": "Responses 缓存 TTL(秒)",
+ "embeddingsProvider": "Embeddings Provider",
+ "modelTitle": "模型映射",
+ "claudeMapping": "Claude 映射(JSON)",
+ "modelAliases": "模型别名(JSON)",
+ "backupTitle": "备份与恢复",
+ "loadExport": "加载当前导出",
+ "importModeMerge": "合并导入(默认)",
+ "importModeReplace": "全量覆盖导入",
+ "importNow": "立即导入",
+ "importing": "导入中...",
+ "importPlaceholder": "粘贴要导入的 JSON 配置",
+ "importEmpty": "请先输入导入 JSON",
+ "importInvalidJson": "导入 JSON 格式无效",
+ "importFailed": "导入失败",
+ "importSuccess": "配置导入成功(模式:{mode})",
+ "exportFailed": "导出失败",
+ "exportLoaded": "已加载当前配置导出",
+ "exportJson": "导出 JSON",
+ "invalidJsonField": "{field} 不是有效 JSON 对象",
+ "defaultPasswordWarning": "当前使用默认密码 admin,请尽快在此修改。",
+ "vercelSyncHint": "当前配置已更新。Vercel 部署请到 Vercel 同步页面手动同步并重部署。",
+ "autoFetchPaused": "自动加载已暂停:连续失败 {count} 次({error})",
+ "retryLoad": "立即重试"
+ },
+ "login": {
+ "welcome": "欢迎回来",
+ "subtitle": "请输入管理员密钥以继续",
+ "adminKeyLabel": "管理员密钥",
+ "adminKeyPlaceholder": "输入您的管理员密钥...",
+ "rememberSession": "记住登录状态",
+ "signIn": "登录",
+ "secureConnection": "安全连接",
+ "adminPortal": "DS2API 管理员门户",
+ "signInFailed": "登录失败",
+ "networkError": "网络错误: {error}"
+ },
+ "vercel": {
+ "tokenRequired": "需要 Vercel 访问令牌",
+ "projectRequired": "需要项目 ID",
+ "syncFailed": "同步失败",
+ "networkError": "网络错误",
+ "title": "Vercel 部署",
+ "description": "将当前密钥和账号配置直接同步到 Vercel 环境变量中。",
+ "tokenLabel": "Vercel 访问令牌",
+ "getToken": "获取令牌",
+ "tokenPlaceholderPreconfig": "正在使用预配置的令牌",
+ "tokenPlaceholder": "输入 Vercel 访问令牌",
+ "projectIdLabel": "项目 ID",
+ "projectIdHint": "可在项目设置 (Project Settings) → 常规 (General) 中找到",
+ "teamIdLabel": "团队 ID",
+ "optional": "可选",
+ "syncing": "正在同步...",
+ "syncRedeploy": "同步并重新部署",
+ "redeployHint": "这将触发 Vercel 的重新部署,大约需要 30-60 秒。",
+ "syncSucceeded": "同步成功",
+ "syncFailedLabel": "同步失败",
+ "openDeployment": "访问部署地址",
+ "statusSynced": "已同步",
+ "statusNotSynced": "未同步",
+ "statusNeverSynced": "从未同步",
+ "lastSyncTime": "上次同步: {time}",
+ "pollPaused": "状态轮询已暂停:连续失败 {count} 次。",
+ "manualRefresh": "手动刷新",
+ "howItWorks": "工作原理",
+ "steps": {
+ "one": "当前配置 (密钥和账号) 被导出为 JSON 字符串。",
+ "two": "JSON 被编码为 Base64 以确保格式兼容性。",
+ "three": "更新 Vercel 项目中的环境变量:",
+ "four": "触发重新部署以应用新的环境变量。"
+ }
+ }
+}