mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-07 01:45:27 +08:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
958bd124cc | ||
|
|
b89e154e43 | ||
|
|
01924f4a69 | ||
|
|
3725694bdf | ||
|
|
21b12f583a | ||
|
|
d97b86e0ee | ||
|
|
0869ea56cd | ||
|
|
4768440627 | ||
|
|
37b867c7ad | ||
|
|
25ea28a277 | ||
|
|
0ac49ab32b | ||
|
|
70c59eb71d | ||
|
|
962700f525 | ||
|
|
e143d13ff6 | ||
|
|
2f853d7364 | ||
|
|
36099a4ada | ||
|
|
73bdb55cee | ||
|
|
3f3198c959 | ||
|
|
6b8f7f8821 | ||
|
|
ac9a1ae742 | ||
|
|
bd4c2bacbc | ||
|
|
6cfc7051c4 | ||
|
|
22a2a97a76 |
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -13,8 +13,8 @@
|
|||||||
|
|
||||||
#### 🔀 变更说明 | Description of Change
|
#### 🔀 变更说明 | Description of Change
|
||||||
|
|
||||||
|
<!-- Thank you for your Pull Request. Please provide a description above. -->
|
||||||
|
|
||||||
#### 📝 补充信息 | Additional Information
|
#### 📝 补充信息 | Additional Information
|
||||||
|
|
||||||
|
<!-- Add any other context about the Pull Request here. -->
|
||||||
127
.github/workflows/release-dockerhub.yml
vendored
Normal file
127
.github/workflows/release-dockerhub.yml
vendored
Normal file
@@ -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
|
||||||
128
.github/workflows/release.yml
vendored
Normal file
128
.github/workflows/release.yml
vendored
Normal file
@@ -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
|
||||||
40
.gitignore
vendored
40
.gitignore
vendored
@@ -2,37 +2,6 @@
|
|||||||
config.json
|
config.json
|
||||||
.env
|
.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
|
# IDE
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
@@ -44,7 +13,6 @@ env/
|
|||||||
# Logs
|
# Logs
|
||||||
*.log
|
*.log
|
||||||
logs/
|
logs/
|
||||||
uvicorn.log
|
|
||||||
artifacts/
|
artifacts/
|
||||||
|
|
||||||
# Vercel
|
# Vercel
|
||||||
@@ -56,8 +24,6 @@ webui/node_modules/
|
|||||||
webui/dist/
|
webui/dist/
|
||||||
.npm
|
.npm
|
||||||
.pnpm-store/
|
.pnpm-store/
|
||||||
# 保留 webui/package-lock.json 用于 CI 缓存
|
|
||||||
# package-lock.json # 如果有根目录的可以忽略
|
|
||||||
yarn.lock
|
yarn.lock
|
||||||
pnpm-lock.yaml
|
pnpm-lock.yaml
|
||||||
|
|
||||||
@@ -86,7 +52,9 @@ coverage*.out
|
|||||||
cover/
|
cover/
|
||||||
|
|
||||||
# Misc
|
# Misc
|
||||||
*.pyc
|
|
||||||
*.pyo
|
|
||||||
.git/
|
.git/
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
|
# Claude Code
|
||||||
|
.claude/
|
||||||
|
CLAUDE.local.md
|
||||||
|
|||||||
@@ -8,12 +8,15 @@ RUN npm run build
|
|||||||
|
|
||||||
FROM golang:1.24 AS go-builder
|
FROM golang:1.24 AS go-builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ARG TARGETOS=linux
|
ARG TARGETOS
|
||||||
ARG TARGETARCH=amd64
|
ARG TARGETARCH
|
||||||
COPY go.mod go.sum* ./
|
COPY go.mod go.sum* ./
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
COPY . .
|
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
|
FROM busybox:1.36.1-musl AS busybox-tools
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,14 @@
|
|||||||
services:
|
services:
|
||||||
ds2api:
|
ds2api:
|
||||||
build: .
|
image: crpi-cnazxqmg4avmg4fq.cn-beijing.personal.cr.aliyuncs.com/ronghuaxueleng/ds2api:latest
|
||||||
image: ds2api:latest
|
|
||||||
container_name: ds2api
|
container_name: ds2api
|
||||||
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
- "${PORT:-5001}:${PORT:-5001}"
|
- "6011:5001"
|
||||||
env_file:
|
volumes:
|
||||||
- .env
|
- ./config.json:/app/config.json # 配置文件
|
||||||
|
- ./.env:/app/.env # 环境变量
|
||||||
environment:
|
environment:
|
||||||
- HOST=0.0.0.0
|
- TZ=Asia/Shanghai
|
||||||
restart: unless-stopped
|
- LOG_LEVEL=INFO
|
||||||
healthcheck:
|
- DS2API_ADMIN_KEY=${DS2API_ADMIN_KEY:-ds2api}
|
||||||
test: ["CMD", "/usr/local/bin/busybox", "wget", "-qO-", "http://localhost:${PORT:-5001}/healthz"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
start_period: 10s
|
|
||||||
|
|||||||
@@ -24,8 +24,21 @@ func (h *Handler) listAccounts(w http.ResponseWriter, r *http.Request) {
|
|||||||
pageSize = 100
|
pageSize = 100
|
||||||
}
|
}
|
||||||
accounts := h.Store.Snapshot().Accounts
|
accounts := h.Store.Snapshot().Accounts
|
||||||
total := len(accounts)
|
|
||||||
reverseAccounts(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
|
totalPages := 1
|
||||||
if total > 0 {
|
if total > 0 {
|
||||||
totalPages = (total + pageSize - 1) / pageSize
|
totalPages = (total + pageSize - 1) / pageSize
|
||||||
|
|||||||
@@ -125,10 +125,7 @@ func (h *Handler) testAccount(ctx context.Context, acc config.Account, model, me
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(message) == "" {
|
if strings.TrimSpace(message) == "" {
|
||||||
result["success"] = true
|
message = "你是谁?"
|
||||||
result["message"] = "API 测试成功(仅会话创建)"
|
|
||||||
result["response_time"] = int(time.Since(start).Milliseconds())
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
thinking, search, ok := config.GetModelConfig(model)
|
thinking, search, ok := config.GetModelConfig(model)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
566
start.mjs
Normal file
566
start.mjs
Normal file
@@ -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);
|
||||||
|
});
|
||||||
@@ -24,6 +24,8 @@ export default function AccountManagerContainer({ config, onRefresh, onMessage,
|
|||||||
fetchAccounts,
|
fetchAccounts,
|
||||||
changePageSize,
|
changePageSize,
|
||||||
resolveAccountIdentifier,
|
resolveAccountIdentifier,
|
||||||
|
searchQuery,
|
||||||
|
handleSearchChange,
|
||||||
} = useAccountsData({ apiFetch })
|
} = useAccountsData({ apiFetch })
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -91,6 +93,8 @@ export default function AccountManagerContainer({ config, onRefresh, onMessage,
|
|||||||
onPrevPage={() => fetchAccounts(page - 1)}
|
onPrevPage={() => fetchAccounts(page - 1)}
|
||||||
onNextPage={() => fetchAccounts(page + 1)}
|
onNextPage={() => fetchAccounts(page + 1)}
|
||||||
onPageSizeChange={changePageSize}
|
onPageSizeChange={changePageSize}
|
||||||
|
searchQuery={searchQuery}
|
||||||
|
onSearchChange={handleSearchChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AddKeyModal
|
<AddKeyModal
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ export default function AccountsTable({
|
|||||||
onPrevPage,
|
onPrevPage,
|
||||||
onNextPage,
|
onNextPage,
|
||||||
onPageSizeChange,
|
onPageSizeChange,
|
||||||
|
searchQuery,
|
||||||
|
onSearchChange,
|
||||||
}) {
|
}) {
|
||||||
const [copiedId, setCopiedId] = useState(null)
|
const [copiedId, setCopiedId] = useState(null)
|
||||||
|
|
||||||
@@ -38,6 +40,13 @@ export default function AccountsTable({
|
|||||||
<p className="text-sm text-muted-foreground">{t('accountManager.accountsDesc')}</p>
|
<p className="text-sm text-muted-foreground">{t('accountManager.accountsDesc')}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={e => 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"
|
||||||
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={onTestAll}
|
onClick={onTestAll}
|
||||||
disabled={testingAll || totalAccounts === 0}
|
disabled={testingAll || totalAccounts === 0}
|
||||||
@@ -138,7 +147,7 @@ export default function AccountsTable({
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
) : (
|
) : (
|
||||||
<div className="p-8 text-center text-muted-foreground">{t('accountManager.noAccounts')}</div>
|
<div className="p-8 text-center text-muted-foreground">{searchQuery ? t('accountManager.searchNoResults') : t('accountManager.noAccounts')}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -16,10 +16,14 @@ export function useAccountsData({ apiFetch }) {
|
|||||||
return String(acc.identifier || acc.email || acc.mobile || '').trim()
|
return String(acc.identifier || acc.email || acc.mobile || '').trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchAccounts = async (targetPage = page, targetPageSize = pageSize) => {
|
const [searchQuery, setSearchQuery] = useState('')
|
||||||
|
|
||||||
|
const fetchAccounts = async (targetPage = page, targetPageSize = pageSize, targetQuery = searchQuery) => {
|
||||||
setLoadingAccounts(true)
|
setLoadingAccounts(true)
|
||||||
try {
|
try {
|
||||||
const res = await apiFetch(`/admin/accounts?page=${targetPage}&page_size=${targetPageSize}`)
|
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) {
|
if (res.ok) {
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
setAccounts(data.items || [])
|
setAccounts(data.items || [])
|
||||||
@@ -39,6 +43,11 @@ export function useAccountsData({ apiFetch }) {
|
|||||||
fetchAccounts(1, newSize)
|
fetchAccounts(1, newSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleSearchChange = (query) => {
|
||||||
|
setSearchQuery(query)
|
||||||
|
fetchAccounts(1, pageSize, query)
|
||||||
|
}
|
||||||
|
|
||||||
const fetchQueueStatus = async () => {
|
const fetchQueueStatus = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await apiFetch('/admin/queue/status')
|
const res = await apiFetch('/admin/queue/status')
|
||||||
@@ -71,5 +80,7 @@ export function useAccountsData({ apiFetch }) {
|
|||||||
fetchAccounts,
|
fetchAccounts,
|
||||||
changePageSize,
|
changePageSize,
|
||||||
resolveAccountIdentifier,
|
resolveAccountIdentifier,
|
||||||
|
searchQuery,
|
||||||
|
handleSearchChange,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,7 +129,9 @@
|
|||||||
"passwordPlaceholder": "Account password",
|
"passwordPlaceholder": "Account password",
|
||||||
"addAccountLoading": "Adding...",
|
"addAccountLoading": "Adding...",
|
||||||
"addAccountAction": "Add account",
|
"addAccountAction": "Add account",
|
||||||
"pageInfo": "Page {current}/{total}, {count} accounts total"
|
"pageInfo": "Page {current}/{total}, {count} accounts total",
|
||||||
|
"searchPlaceholder": "Search accounts...",
|
||||||
|
"searchNoResults": "No accounts match your search"
|
||||||
},
|
},
|
||||||
"apiTester": {
|
"apiTester": {
|
||||||
"defaultMessage": "Hello, please introduce yourself in one sentence.",
|
"defaultMessage": "Hello, please introduce yourself in one sentence.",
|
||||||
|
|||||||
@@ -129,7 +129,9 @@
|
|||||||
"passwordPlaceholder": "账号密码",
|
"passwordPlaceholder": "账号密码",
|
||||||
"addAccountLoading": "添加中...",
|
"addAccountLoading": "添加中...",
|
||||||
"addAccountAction": "添加账号",
|
"addAccountAction": "添加账号",
|
||||||
"pageInfo": "第 {current}/{total} 页,共 {count} 个账号"
|
"pageInfo": "第 {current}/{total} 页,共 {count} 个账号",
|
||||||
|
"searchPlaceholder": "搜索账号...",
|
||||||
|
"searchNoResults": "未找到匹配的账号"
|
||||||
},
|
},
|
||||||
"apiTester": {
|
"apiTester": {
|
||||||
"defaultMessage": "你好,请用一句话介绍你自己。",
|
"defaultMessage": "你好,请用一句话介绍你自己。",
|
||||||
|
|||||||
Reference in New Issue
Block a user