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..536c3dc --- /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: ./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..da3264f --- /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: ./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/Dockerfile b/Dockerfile index c86f82d..6996f43 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,12 +8,15 @@ RUN npm run build FROM golang:1.24 AS go-builder WORKDIR /app -ARG TARGETOS=linux -ARG TARGETARCH=amd64 +ARG TARGETOS +ARG TARGETARCH COPY go.mod go.sum* ./ RUN go mod download COPY . . -RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o /out/ds2api ./cmd/ds2api +RUN set -eux; \ + GOOS="${TARGETOS:-$(go env GOOS)}"; \ + GOARCH="${TARGETARCH:-$(go env GOARCH)}"; \ + CGO_ENABLED=0 GOOS="${GOOS}" GOARCH="${GOARCH}" go build -o /out/ds2api ./cmd/ds2api FROM busybox:1.36.1-musl AS busybox-tools 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..d168be3 --- /dev/null +++ b/start.mjs @@ -0,0 +1,566 @@ +#!/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(() => { + const activeCount = processes.filter(proc => proc.exitCode === null && proc.signalCode === null).length; + if (activeCount === 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": "触发重新部署以应用新的环境变量。" + } + } +}