Spaces:
Paused
Paused
Upload 3 files
Browse files- Dockerfile +84 -0
- EditChannel.js +1208 -0
- README.md +35 -10
Dockerfile
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Stage 0: Fetcher - Get latest code and replace file
|
| 2 |
+
# Use an image with git installed
|
| 3 |
+
FROM alpine/git:latest AS fetcher
|
| 4 |
+
|
| 5 |
+
# Define a build argument to bust the cache
|
| 6 |
+
# ARG CACHE_BUSTER -- Removed as it cannot be passed dynamically on HF
|
| 7 |
+
|
| 8 |
+
# Set working directory
|
| 9 |
+
WORKDIR /app
|
| 10 |
+
|
| 11 |
+
# Clone the main repository
|
| 12 |
+
# Add date command output to a file to bust cache *before* cloning
|
| 13 |
+
RUN date +%s > /tmp/build_timestamp.txt && \
|
| 14 |
+
git clone --depth 1 https://github.com/QuantumNous/new-api.git .
|
| 15 |
+
|
| 16 |
+
# Copy the custom EditChannel.js from the local build context
|
| 17 |
+
COPY EditChannel.js ./web/src/pages/Channel/EditChannel.js
|
| 18 |
+
RUN echo "EditChannel.js copied from local context successfully."
|
| 19 |
+
|
| 20 |
+
# Stage 1: Frontend Builder - Build the web UI using bun
|
| 21 |
+
FROM oven/bun:latest AS frontend-builder
|
| 22 |
+
|
| 23 |
+
WORKDIR /build
|
| 24 |
+
|
| 25 |
+
# Copy package.json first to leverage Docker cache for dependencies
|
| 26 |
+
COPY --from=fetcher /app/web/package.json .
|
| 27 |
+
RUN bun install
|
| 28 |
+
|
| 29 |
+
# Copy the rest of the web source code and the VERSION file
|
| 30 |
+
COPY --from=fetcher /app/web .
|
| 31 |
+
COPY --from=fetcher /app/VERSION .
|
| 32 |
+
|
| 33 |
+
# Build the frontend, reading version from the VERSION file
|
| 34 |
+
RUN DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(cat VERSION) bun run build
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
# Stage 2: Backend Builder - Build the Go application
|
| 38 |
+
FROM golang:alpine AS backend-builder
|
| 39 |
+
|
| 40 |
+
ENV GO111MODULE=on \
|
| 41 |
+
CGO_ENABLED=0 \
|
| 42 |
+
GOOS=linux
|
| 43 |
+
|
| 44 |
+
WORKDIR /build
|
| 45 |
+
|
| 46 |
+
# Copy Go module files first for dependency caching
|
| 47 |
+
COPY --from=fetcher /app/go.mod /app/go.sum ./
|
| 48 |
+
RUN go mod download
|
| 49 |
+
|
| 50 |
+
# Copy the entire application source code from the fetcher stage
|
| 51 |
+
COPY --from=fetcher /app .
|
| 52 |
+
|
| 53 |
+
# Copy the built frontend assets from the frontend-builder stage
|
| 54 |
+
COPY --from=frontend-builder /build/dist ./web/dist
|
| 55 |
+
|
| 56 |
+
# Build the Go application, embedding the version
|
| 57 |
+
RUN go build -ldflags "-s -w -X 'one-api/common.Version=$(cat VERSION)'" -o one-api
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
# Stage 3: Final Stage - Create the final runtime image
|
| 61 |
+
FROM alpine
|
| 62 |
+
|
| 63 |
+
# Update, upgrade, install runtime dependencies and clean up
|
| 64 |
+
RUN apk update && \
|
| 65 |
+
apk upgrade && \
|
| 66 |
+
apk add --no-cache ca-certificates tzdata ffmpeg && \
|
| 67 |
+
update-ca-certificates && \
|
| 68 |
+
rm -rf /var/cache/apk/*
|
| 69 |
+
|
| 70 |
+
# Copy the built Go application from the backend-builder stage
|
| 71 |
+
COPY --from=backend-builder /build/one-api /one-api
|
| 72 |
+
|
| 73 |
+
# Create the logs directory and ensure /data is writable by the application user
|
| 74 |
+
RUN mkdir -p /data/logs && \
|
| 75 |
+
chmod -R a+w /data
|
| 76 |
+
|
| 77 |
+
# Expose the application port
|
| 78 |
+
EXPOSE 3000
|
| 79 |
+
|
| 80 |
+
# Set the working directory for the running container
|
| 81 |
+
WORKDIR /data
|
| 82 |
+
|
| 83 |
+
# Define the entry point for the container
|
| 84 |
+
ENTRYPOINT ["/one-api"]
|
EditChannel.js
ADDED
|
@@ -0,0 +1,1208 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useEffect, useState } from 'react';
|
| 2 |
+
import { useNavigate } from 'react-router-dom';
|
| 3 |
+
import { useTranslation } from 'react-i18next';
|
| 4 |
+
import {
|
| 5 |
+
API,
|
| 6 |
+
isMobile,
|
| 7 |
+
showError,
|
| 8 |
+
showInfo,
|
| 9 |
+
showSuccess,
|
| 10 |
+
verifyJSON,
|
| 11 |
+
} from '../../helpers';
|
| 12 |
+
import { CHANNEL_OPTIONS } from '../../constants';
|
| 13 |
+
import {
|
| 14 |
+
SideSheet,
|
| 15 |
+
Space,
|
| 16 |
+
Spin,
|
| 17 |
+
Button,
|
| 18 |
+
Input,
|
| 19 |
+
Typography,
|
| 20 |
+
Select,
|
| 21 |
+
TextArea,
|
| 22 |
+
Checkbox,
|
| 23 |
+
Banner,
|
| 24 |
+
Modal,
|
| 25 |
+
ImagePreview,
|
| 26 |
+
Card,
|
| 27 |
+
Tag,
|
| 28 |
+
} from '@douyinfe/semi-ui';
|
| 29 |
+
import { getChannelModels } from '../../helpers';
|
| 30 |
+
import {
|
| 31 |
+
IconSave,
|
| 32 |
+
IconClose,
|
| 33 |
+
IconServer,
|
| 34 |
+
IconSetting,
|
| 35 |
+
IconCode,
|
| 36 |
+
IconGlobe,
|
| 37 |
+
} from '@douyinfe/semi-icons';
|
| 38 |
+
|
| 39 |
+
const { Text, Title } = Typography;
|
| 40 |
+
|
| 41 |
+
const MODEL_MAPPING_EXAMPLE = {
|
| 42 |
+
'gpt-3.5-turbo': 'gpt-3.5-turbo-0125',
|
| 43 |
+
};
|
| 44 |
+
|
| 45 |
+
const STATUS_CODE_MAPPING_EXAMPLE = {
|
| 46 |
+
400: '500',
|
| 47 |
+
};
|
| 48 |
+
|
| 49 |
+
const REGION_EXAMPLE = {
|
| 50 |
+
default: 'us-central1',
|
| 51 |
+
'claude-3-5-sonnet-20240620': 'europe-west1',
|
| 52 |
+
};
|
| 53 |
+
|
| 54 |
+
function type2secretPrompt(type) {
|
| 55 |
+
// inputs.type === 15 ? '按照如下格式输入:APIKey|SecretKey' : (inputs.type === 18 ? '按照如下格式输入:APPID|APISecret|APIKey' : '请输入渠道对应的鉴权密钥')
|
| 56 |
+
switch (type) {
|
| 57 |
+
case 15:
|
| 58 |
+
return '按照如下格式输入:APIKey|SecretKey';
|
| 59 |
+
case 18:
|
| 60 |
+
return '按照如下格式输入:APPID|APISecret|APIKey';
|
| 61 |
+
case 22:
|
| 62 |
+
return '按照如下格式输入:APIKey-AppId,例如:fastgpt-0sp2gtvfdgyi4k30jwlgwf1i-64f335d84283f05518e9e041';
|
| 63 |
+
case 23:
|
| 64 |
+
return '按照如下格式输入:AppId|SecretId|SecretKey';
|
| 65 |
+
case 33:
|
| 66 |
+
return '按照如下格式输入:Ak|Sk|Region';
|
| 67 |
+
default:
|
| 68 |
+
return '请输入渠道对应的鉴权密钥';
|
| 69 |
+
}
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
const EditChannel = (props) => {
|
| 73 |
+
const { t } = useTranslation();
|
| 74 |
+
const navigate = useNavigate();
|
| 75 |
+
const channelId = props.editingChannel.id;
|
| 76 |
+
const isEdit = channelId !== undefined;
|
| 77 |
+
const [loading, setLoading] = useState(isEdit);
|
| 78 |
+
const handleCancel = () => {
|
| 79 |
+
props.handleClose();
|
| 80 |
+
};
|
| 81 |
+
const originInputs = {
|
| 82 |
+
name: '',
|
| 83 |
+
type: 1,
|
| 84 |
+
key: '',
|
| 85 |
+
openai_organization: '',
|
| 86 |
+
max_input_tokens: 0,
|
| 87 |
+
base_url: '',
|
| 88 |
+
other: '',
|
| 89 |
+
model_mapping: '',
|
| 90 |
+
status_code_mapping: '',
|
| 91 |
+
models: [],
|
| 92 |
+
auto_ban: 1,
|
| 93 |
+
test_model: '',
|
| 94 |
+
groups: ['default'],
|
| 95 |
+
priority: 0,
|
| 96 |
+
weight: 0,
|
| 97 |
+
tag: '',
|
| 98 |
+
};
|
| 99 |
+
const [batch, setBatch] = useState(false);
|
| 100 |
+
const [autoBan, setAutoBan] = useState(true);
|
| 101 |
+
// const [autoBan, setAutoBan] = useState(true);
|
| 102 |
+
const [inputs, setInputs] = useState(originInputs);
|
| 103 |
+
const [originModelOptions, setOriginModelOptions] = useState([]);
|
| 104 |
+
const [modelOptions, setModelOptions] = useState([]);
|
| 105 |
+
const [groupOptions, setGroupOptions] = useState([]);
|
| 106 |
+
const [basicModels, setBasicModels] = useState([]);
|
| 107 |
+
const [fullModels, setFullModels] = useState([]);
|
| 108 |
+
const [customModel, setCustomModel] = useState('');
|
| 109 |
+
const [modalImageUrl, setModalImageUrl] = useState('');
|
| 110 |
+
const [isModalOpenurl, setIsModalOpenurl] = useState(false);
|
| 111 |
+
const handleInputChange = (name, value) => {
|
| 112 |
+
if (name === 'base_url' && value.endsWith('/v1')) {
|
| 113 |
+
Modal.confirm({
|
| 114 |
+
title: '警告',
|
| 115 |
+
content:
|
| 116 |
+
'不需要在末尾加/v1,New API会自动处理,添加后可能导致请求失败,是否继续?',
|
| 117 |
+
onOk: () => {
|
| 118 |
+
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
| 119 |
+
},
|
| 120 |
+
});
|
| 121 |
+
return;
|
| 122 |
+
}
|
| 123 |
+
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
| 124 |
+
if (name === 'type') {
|
| 125 |
+
let localModels = [];
|
| 126 |
+
switch (value) {
|
| 127 |
+
case 2:
|
| 128 |
+
localModels = [
|
| 129 |
+
'mj_imagine',
|
| 130 |
+
'mj_variation',
|
| 131 |
+
'mj_reroll',
|
| 132 |
+
'mj_blend',
|
| 133 |
+
'mj_upscale',
|
| 134 |
+
'mj_describe',
|
| 135 |
+
'mj_uploads',
|
| 136 |
+
];
|
| 137 |
+
break;
|
| 138 |
+
case 5:
|
| 139 |
+
localModels = [
|
| 140 |
+
'swap_face',
|
| 141 |
+
'mj_imagine',
|
| 142 |
+
'mj_variation',
|
| 143 |
+
'mj_reroll',
|
| 144 |
+
'mj_blend',
|
| 145 |
+
'mj_upscale',
|
| 146 |
+
'mj_describe',
|
| 147 |
+
'mj_zoom',
|
| 148 |
+
'mj_shorten',
|
| 149 |
+
'mj_modal',
|
| 150 |
+
'mj_inpaint',
|
| 151 |
+
'mj_custom_zoom',
|
| 152 |
+
'mj_high_variation',
|
| 153 |
+
'mj_low_variation',
|
| 154 |
+
'mj_pan',
|
| 155 |
+
'mj_uploads',
|
| 156 |
+
];
|
| 157 |
+
break;
|
| 158 |
+
case 36:
|
| 159 |
+
localModels = ['suno_music', 'suno_lyrics'];
|
| 160 |
+
break;
|
| 161 |
+
default:
|
| 162 |
+
localModels = getChannelModels(value);
|
| 163 |
+
break;
|
| 164 |
+
}
|
| 165 |
+
if (inputs.models.length === 0) {
|
| 166 |
+
setInputs((inputs) => ({ ...inputs, models: localModels }));
|
| 167 |
+
}
|
| 168 |
+
setBasicModels(localModels);
|
| 169 |
+
}
|
| 170 |
+
//setAutoBan
|
| 171 |
+
};
|
| 172 |
+
|
| 173 |
+
const loadChannel = async () => {
|
| 174 |
+
setLoading(true);
|
| 175 |
+
let res = await API.get(`/api/channel/${channelId}`);
|
| 176 |
+
if (res === undefined) {
|
| 177 |
+
return;
|
| 178 |
+
}
|
| 179 |
+
const { success, message, data } = res.data;
|
| 180 |
+
if (success) {
|
| 181 |
+
if (data.models === '') {
|
| 182 |
+
data.models = [];
|
| 183 |
+
} else {
|
| 184 |
+
data.models = data.models.split(',');
|
| 185 |
+
}
|
| 186 |
+
if (data.group === '') {
|
| 187 |
+
data.groups = [];
|
| 188 |
+
} else {
|
| 189 |
+
data.groups = data.group.split(',');
|
| 190 |
+
}
|
| 191 |
+
if (data.model_mapping !== '') {
|
| 192 |
+
data.model_mapping = JSON.stringify(
|
| 193 |
+
JSON.parse(data.model_mapping),
|
| 194 |
+
null,
|
| 195 |
+
2,
|
| 196 |
+
);
|
| 197 |
+
}
|
| 198 |
+
setInputs(data);
|
| 199 |
+
if (data.auto_ban === 0) {
|
| 200 |
+
setAutoBan(false);
|
| 201 |
+
} else {
|
| 202 |
+
setAutoBan(true);
|
| 203 |
+
}
|
| 204 |
+
setBasicModels(getChannelModels(data.type));
|
| 205 |
+
// console.log(data);
|
| 206 |
+
} else {
|
| 207 |
+
showError(message);
|
| 208 |
+
}
|
| 209 |
+
setLoading(false);
|
| 210 |
+
};
|
| 211 |
+
|
| 212 |
+
const fetchUpstreamModelList = async (name) => {
|
| 213 |
+
// if (inputs['type'] !== 1) {
|
| 214 |
+
// showError(t('仅支持 OpenAI 接口格式'));
|
| 215 |
+
// return;
|
| 216 |
+
// }
|
| 217 |
+
setLoading(true);
|
| 218 |
+
const models = inputs['models'] || [];
|
| 219 |
+
let err = false;
|
| 220 |
+
|
| 221 |
+
if (isEdit) {
|
| 222 |
+
// 如果是编辑模式,使用已有的channel id获取模型列表
|
| 223 |
+
const res = await API.get('/api/channel/fetch_models/' + channelId);
|
| 224 |
+
if (res.data && res.data?.success) {
|
| 225 |
+
models.push(...res.data.data);
|
| 226 |
+
} else {
|
| 227 |
+
err = true;
|
| 228 |
+
}
|
| 229 |
+
} else {
|
| 230 |
+
// 如果是新建模式,通过后端代理获取模型列表
|
| 231 |
+
if (!inputs?.['key']) {
|
| 232 |
+
showError(t('请填写密钥'));
|
| 233 |
+
err = true;
|
| 234 |
+
} else {
|
| 235 |
+
try {
|
| 236 |
+
const res = await API.post('/api/channel/fetch_models', {
|
| 237 |
+
base_url: inputs['base_url'],
|
| 238 |
+
type: inputs['type'],
|
| 239 |
+
key: inputs['key'],
|
| 240 |
+
});
|
| 241 |
+
|
| 242 |
+
if (res.data && res.data.success) {
|
| 243 |
+
models.push(...res.data.data);
|
| 244 |
+
} else {
|
| 245 |
+
err = true;
|
| 246 |
+
}
|
| 247 |
+
} catch (error) {
|
| 248 |
+
console.error('Error fetching models:', error);
|
| 249 |
+
err = true;
|
| 250 |
+
}
|
| 251 |
+
}
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
if (!err) {
|
| 255 |
+
handleInputChange(name, Array.from(new Set(models)));
|
| 256 |
+
showSuccess(t('获取模型列表成功'));
|
| 257 |
+
} else {
|
| 258 |
+
showError(t('获取模型列表失败'));
|
| 259 |
+
}
|
| 260 |
+
setLoading(false);
|
| 261 |
+
};
|
| 262 |
+
|
| 263 |
+
const fetchModels = async () => {
|
| 264 |
+
try {
|
| 265 |
+
let res = await API.get(`/api/channel/models`);
|
| 266 |
+
let localModelOptions = res.data.data.map((model) => ({
|
| 267 |
+
label: model.id,
|
| 268 |
+
value: model.id,
|
| 269 |
+
}));
|
| 270 |
+
setOriginModelOptions(localModelOptions);
|
| 271 |
+
setFullModels(res.data.data.map((model) => model.id));
|
| 272 |
+
setBasicModels(
|
| 273 |
+
res.data.data
|
| 274 |
+
.filter((model) => {
|
| 275 |
+
return model.id.startsWith('gpt-') || model.id.startsWith('text-');
|
| 276 |
+
})
|
| 277 |
+
.map((model) => model.id),
|
| 278 |
+
);
|
| 279 |
+
} catch (error) {
|
| 280 |
+
showError(error.message);
|
| 281 |
+
}
|
| 282 |
+
};
|
| 283 |
+
|
| 284 |
+
const fetchGroups = async () => {
|
| 285 |
+
try {
|
| 286 |
+
let res = await API.get(`/api/group/`);
|
| 287 |
+
if (res === undefined) {
|
| 288 |
+
return;
|
| 289 |
+
}
|
| 290 |
+
setGroupOptions(
|
| 291 |
+
res.data.data.map((group) => ({
|
| 292 |
+
label: group,
|
| 293 |
+
value: group,
|
| 294 |
+
})),
|
| 295 |
+
);
|
| 296 |
+
} catch (error) {
|
| 297 |
+
showError(error.message);
|
| 298 |
+
}
|
| 299 |
+
};
|
| 300 |
+
|
| 301 |
+
useEffect(() => {
|
| 302 |
+
let localModelOptions = [...originModelOptions];
|
| 303 |
+
inputs.models.forEach((model) => {
|
| 304 |
+
if (!localModelOptions.find((option) => option.label === model)) {
|
| 305 |
+
localModelOptions.push({
|
| 306 |
+
label: model,
|
| 307 |
+
value: model,
|
| 308 |
+
});
|
| 309 |
+
}
|
| 310 |
+
});
|
| 311 |
+
setModelOptions(localModelOptions);
|
| 312 |
+
}, [originModelOptions, inputs.models]);
|
| 313 |
+
|
| 314 |
+
useEffect(() => {
|
| 315 |
+
fetchModels().then();
|
| 316 |
+
fetchGroups().then();
|
| 317 |
+
if (isEdit) {
|
| 318 |
+
loadChannel().then(() => { });
|
| 319 |
+
} else {
|
| 320 |
+
setInputs(originInputs);
|
| 321 |
+
let localModels = getChannelModels(inputs.type);
|
| 322 |
+
setBasicModels(localModels);
|
| 323 |
+
setInputs((inputs) => ({ ...inputs, models: localModels }));
|
| 324 |
+
}
|
| 325 |
+
}, [props.editingChannel.id]);
|
| 326 |
+
|
| 327 |
+
const submit = async () => {
|
| 328 |
+
if (!isEdit && (inputs.name === '' || inputs.key === '')) {
|
| 329 |
+
showInfo(t('请填写渠道名称和渠道密钥!'));
|
| 330 |
+
return;
|
| 331 |
+
}
|
| 332 |
+
if (inputs.models.length === 0) {
|
| 333 |
+
showInfo(t('请至少选择一个模型!'));
|
| 334 |
+
return;
|
| 335 |
+
}
|
| 336 |
+
if (inputs.model_mapping !== '' && !verifyJSON(inputs.model_mapping)) {
|
| 337 |
+
showInfo(t('模型映射必须是合法的 JSON 格式!'));
|
| 338 |
+
return;
|
| 339 |
+
}
|
| 340 |
+
let localInputs = { ...inputs };
|
| 341 |
+
if (localInputs.base_url && localInputs.base_url.endsWith('/')) {
|
| 342 |
+
localInputs.base_url = localInputs.base_url.slice(
|
| 343 |
+
0,
|
| 344 |
+
localInputs.base_url.length - 1,
|
| 345 |
+
);
|
| 346 |
+
}
|
| 347 |
+
if (localInputs.type === 18 && localInputs.other === '') {
|
| 348 |
+
localInputs.other = 'v2.1';
|
| 349 |
+
}
|
| 350 |
+
let res;
|
| 351 |
+
if (!Array.isArray(localInputs.models)) {
|
| 352 |
+
showError(t('提交失败,请勿重复提交!'));
|
| 353 |
+
handleCancel();
|
| 354 |
+
return;
|
| 355 |
+
}
|
| 356 |
+
localInputs.auto_ban = autoBan ? 1 : 0;
|
| 357 |
+
localInputs.models = localInputs.models.join(',');
|
| 358 |
+
localInputs.group = localInputs.groups.join(',');
|
| 359 |
+
if (isEdit) {
|
| 360 |
+
res = await API.put(`/api/channel/`, {
|
| 361 |
+
...localInputs,
|
| 362 |
+
id: parseInt(channelId),
|
| 363 |
+
});
|
| 364 |
+
} else {
|
| 365 |
+
res = await API.post(`/api/channel/`, localInputs);
|
| 366 |
+
}
|
| 367 |
+
const { success, message } = res.data;
|
| 368 |
+
if (success) {
|
| 369 |
+
if (isEdit) {
|
| 370 |
+
showSuccess(t('渠道更新成功!'));
|
| 371 |
+
} else {
|
| 372 |
+
showSuccess(t('渠道创建成功!'));
|
| 373 |
+
setInputs(originInputs);
|
| 374 |
+
}
|
| 375 |
+
props.refresh();
|
| 376 |
+
props.handleClose();
|
| 377 |
+
} else {
|
| 378 |
+
showError(message);
|
| 379 |
+
}
|
| 380 |
+
};
|
| 381 |
+
|
| 382 |
+
const addCustomModels = () => {
|
| 383 |
+
if (customModel.trim() === '') return;
|
| 384 |
+
const modelArray = customModel.split(',').map((model) => model.trim());
|
| 385 |
+
|
| 386 |
+
let localModels = [...inputs.models];
|
| 387 |
+
let localModelOptions = [...modelOptions];
|
| 388 |
+
let hasError = false;
|
| 389 |
+
|
| 390 |
+
modelArray.forEach((model) => {
|
| 391 |
+
if (model && !localModels.includes(model)) {
|
| 392 |
+
localModels.push(model);
|
| 393 |
+
localModelOptions.push({
|
| 394 |
+
key: model,
|
| 395 |
+
text: model,
|
| 396 |
+
value: model,
|
| 397 |
+
});
|
| 398 |
+
} else if (model) {
|
| 399 |
+
showError(t('某些模型已存在!'));
|
| 400 |
+
hasError = true;
|
| 401 |
+
}
|
| 402 |
+
});
|
| 403 |
+
|
| 404 |
+
if (hasError) return;
|
| 405 |
+
|
| 406 |
+
setModelOptions(localModelOptions);
|
| 407 |
+
setCustomModel('');
|
| 408 |
+
handleInputChange('models', localModels);
|
| 409 |
+
};
|
| 410 |
+
|
| 411 |
+
const handleAddPrefix = () => {
|
| 412 |
+
const channelName = inputs.name;
|
| 413 |
+
const currentModels = inputs.models; // This is an array of strings
|
| 414 |
+
|
| 415 |
+
if (!channelName || !currentModels || currentModels.length === 0) {
|
| 416 |
+
showError(t('请先填写渠道名称并选择模型!'));
|
| 417 |
+
return;
|
| 418 |
+
}
|
| 419 |
+
|
| 420 |
+
const prefixedModels = currentModels.map(
|
| 421 |
+
(model) => `${channelName}/${model}`,
|
| 422 |
+
);
|
| 423 |
+
|
| 424 |
+
const mapping = {};
|
| 425 |
+
prefixedModels.forEach((prefixedModel, index) => {
|
| 426 |
+
mapping[prefixedModel] = currentModels[index];
|
| 427 |
+
});
|
| 428 |
+
|
| 429 |
+
const modelMappingJson = JSON.stringify(mapping, null, 2); // Pretty print JSON
|
| 430 |
+
|
| 431 |
+
handleInputChange('models', prefixedModels);
|
| 432 |
+
handleInputChange('model_mapping', modelMappingJson);
|
| 433 |
+
showSuccess(t('模型前缀添加成功!'));
|
| 434 |
+
};
|
| 435 |
+
|
| 436 |
+
return (
|
| 437 |
+
<>
|
| 438 |
+
<SideSheet
|
| 439 |
+
placement={isEdit ? 'right' : 'left'}
|
| 440 |
+
title={
|
| 441 |
+
<Space>
|
| 442 |
+
<Tag color="blue" shape="circle">{isEdit ? t('编辑') : t('新建')}</Tag>
|
| 443 |
+
<Title heading={4} className="m-0">
|
| 444 |
+
{isEdit ? t('更新渠道信息') : t('创建新的渠道')}
|
| 445 |
+
</Title>
|
| 446 |
+
</Space>
|
| 447 |
+
}
|
| 448 |
+
headerStyle={{
|
| 449 |
+
borderBottom: '1px solid var(--semi-color-border)',
|
| 450 |
+
padding: '24px'
|
| 451 |
+
}}
|
| 452 |
+
bodyStyle={{
|
| 453 |
+
backgroundColor: 'var(--semi-color-bg-0)',
|
| 454 |
+
padding: '0'
|
| 455 |
+
}}
|
| 456 |
+
visible={props.visible}
|
| 457 |
+
width={isMobile() ? '100%' : 600}
|
| 458 |
+
footer={
|
| 459 |
+
<div className="flex justify-end bg-white">
|
| 460 |
+
<Space>
|
| 461 |
+
<Button
|
| 462 |
+
theme="solid"
|
| 463 |
+
size="large"
|
| 464 |
+
className="!rounded-full"
|
| 465 |
+
onClick={submit}
|
| 466 |
+
icon={<IconSave />}
|
| 467 |
+
>
|
| 468 |
+
{t('提交')}
|
| 469 |
+
</Button>
|
| 470 |
+
<Button
|
| 471 |
+
theme="light"
|
| 472 |
+
size="large"
|
| 473 |
+
className="!rounded-full"
|
| 474 |
+
type="primary"
|
| 475 |
+
onClick={handleCancel}
|
| 476 |
+
icon={<IconClose />}
|
| 477 |
+
>
|
| 478 |
+
{t('取消')}
|
| 479 |
+
</Button>
|
| 480 |
+
</Space>
|
| 481 |
+
</div>
|
| 482 |
+
}
|
| 483 |
+
closeIcon={null}
|
| 484 |
+
onCancel={() => handleCancel()}
|
| 485 |
+
>
|
| 486 |
+
<Spin spinning={loading}>
|
| 487 |
+
<div className="p-6">
|
| 488 |
+
<Card className="!rounded-2xl shadow-sm border-0 mb-6">
|
| 489 |
+
<div className="flex items-center mb-4 p-6 rounded-xl" style={{
|
| 490 |
+
background: 'linear-gradient(135deg, #1e3a8a 0%, #2563eb 50%, #3b82f6 100%)',
|
| 491 |
+
position: 'relative'
|
| 492 |
+
}}>
|
| 493 |
+
<div className="absolute inset-0 overflow-hidden">
|
| 494 |
+
<div className="absolute -top-10 -right-10 w-40 h-40 bg-white opacity-5 rounded-full"></div>
|
| 495 |
+
<div className="absolute -bottom-8 -left-8 w-24 h-24 bg-white opacity-10 rounded-full"></div>
|
| 496 |
+
</div>
|
| 497 |
+
<div className="w-10 h-10 rounded-full bg-white/20 flex items-center justify-center mr-4 relative">
|
| 498 |
+
<IconServer size="large" style={{ color: '#ffffff' }} />
|
| 499 |
+
</div>
|
| 500 |
+
<div className="relative">
|
| 501 |
+
<Text style={{ color: '#ffffff' }} className="text-lg font-medium">{t('基本信息')}</Text>
|
| 502 |
+
<div style={{ color: '#ffffff' }} className="text-sm opacity-80">{t('渠道的基本配置信息')}</div>
|
| 503 |
+
</div>
|
| 504 |
+
</div>
|
| 505 |
+
|
| 506 |
+
<div className="space-y-4">
|
| 507 |
+
<div>
|
| 508 |
+
<Text strong className="block mb-2">{t('类型')}</Text>
|
| 509 |
+
<Select
|
| 510 |
+
name='type'
|
| 511 |
+
required
|
| 512 |
+
optionList={CHANNEL_OPTIONS}
|
| 513 |
+
value={inputs.type}
|
| 514 |
+
onChange={(value) => handleInputChange('type', value)}
|
| 515 |
+
style={{ width: '100%' }}
|
| 516 |
+
filter
|
| 517 |
+
searchPosition='dropdown'
|
| 518 |
+
placeholder={t('请选择渠道类型')}
|
| 519 |
+
size="large"
|
| 520 |
+
className="!rounded-lg"
|
| 521 |
+
/>
|
| 522 |
+
</div>
|
| 523 |
+
|
| 524 |
+
<div>
|
| 525 |
+
<Text strong className="block mb-2">{t('名称')}</Text>
|
| 526 |
+
<Input
|
| 527 |
+
required
|
| 528 |
+
name='name'
|
| 529 |
+
placeholder={t('请为渠道命名')}
|
| 530 |
+
onChange={(value) => {
|
| 531 |
+
handleInputChange('name', value);
|
| 532 |
+
}}
|
| 533 |
+
value={inputs.name}
|
| 534 |
+
autoComplete='new-password'
|
| 535 |
+
size="large"
|
| 536 |
+
className="!rounded-lg"
|
| 537 |
+
/>
|
| 538 |
+
</div>
|
| 539 |
+
|
| 540 |
+
<div>
|
| 541 |
+
<Text strong className="block mb-2">{t('密钥')}</Text>
|
| 542 |
+
{batch ? (
|
| 543 |
+
<TextArea
|
| 544 |
+
name='key'
|
| 545 |
+
required
|
| 546 |
+
placeholder={t('请输入密钥,一行一个')}
|
| 547 |
+
onChange={(value) => {
|
| 548 |
+
handleInputChange('key', value);
|
| 549 |
+
}}
|
| 550 |
+
value={inputs.key}
|
| 551 |
+
style={{ minHeight: 150, fontFamily: 'JetBrains Mono, Consolas' }}
|
| 552 |
+
autoComplete='new-password'
|
| 553 |
+
className="!rounded-lg"
|
| 554 |
+
/>
|
| 555 |
+
) : (
|
| 556 |
+
<>
|
| 557 |
+
{inputs.type === 41 ? (
|
| 558 |
+
<TextArea
|
| 559 |
+
name='key'
|
| 560 |
+
required
|
| 561 |
+
placeholder={
|
| 562 |
+
'{\n' +
|
| 563 |
+
' "type": "service_account",\n' +
|
| 564 |
+
' "project_id": "abc-bcd-123-456",\n' +
|
| 565 |
+
' "private_key_id": "123xxxxx456",\n' +
|
| 566 |
+
' "private_key": "-----BEGIN PRIVATE KEY-----xxxx\n' +
|
| 567 |
+
' "client_email": "xxx@developer.gserviceaccount.com",\n' +
|
| 568 |
+
' "client_id": "111222333",\n' +
|
| 569 |
+
' "auth_uri": "https://accounts.google.com/o/oauth2/auth",\n' +
|
| 570 |
+
' "token_uri": "https://oauth2.googleapis.com/token",\n' +
|
| 571 |
+
' "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",\n' +
|
| 572 |
+
' "client_x509_cert_url": "https://xxxxx.gserviceaccount.com",\n' +
|
| 573 |
+
' "universe_domain": "googleapis.com"\n' +
|
| 574 |
+
'}'
|
| 575 |
+
}
|
| 576 |
+
onChange={(value) => {
|
| 577 |
+
handleInputChange('key', value);
|
| 578 |
+
}}
|
| 579 |
+
autosize={{ minRows: 10 }}
|
| 580 |
+
value={inputs.key}
|
| 581 |
+
autoComplete='new-password'
|
| 582 |
+
className="!rounded-lg font-mono"
|
| 583 |
+
/>
|
| 584 |
+
) : (
|
| 585 |
+
<Input
|
| 586 |
+
name='key'
|
| 587 |
+
required
|
| 588 |
+
placeholder={t(type2secretPrompt(inputs.type))}
|
| 589 |
+
onChange={(value) => {
|
| 590 |
+
handleInputChange('key', value);
|
| 591 |
+
}}
|
| 592 |
+
value={inputs.key}
|
| 593 |
+
autoComplete='new-password'
|
| 594 |
+
size="large"
|
| 595 |
+
className="!rounded-lg"
|
| 596 |
+
/>
|
| 597 |
+
)}
|
| 598 |
+
</>
|
| 599 |
+
)}
|
| 600 |
+
</div>
|
| 601 |
+
|
| 602 |
+
{!isEdit && (
|
| 603 |
+
<div className="flex items-center">
|
| 604 |
+
<Checkbox
|
| 605 |
+
checked={batch}
|
| 606 |
+
onChange={() => setBatch(!batch)}
|
| 607 |
+
/>
|
| 608 |
+
<Text strong className="ml-2">{t('批量创建')}</Text>
|
| 609 |
+
</div>
|
| 610 |
+
)}
|
| 611 |
+
</div>
|
| 612 |
+
</Card>
|
| 613 |
+
|
| 614 |
+
{/* API Configuration Card */}
|
| 615 |
+
<Card className="!rounded-2xl shadow-sm border-0 mb-6">
|
| 616 |
+
<div className="flex items-center mb-4 p-6 rounded-xl" style={{
|
| 617 |
+
background: 'linear-gradient(135deg, #065f46 0%, #059669 50%, #10b981 100%)',
|
| 618 |
+
position: 'relative'
|
| 619 |
+
}}>
|
| 620 |
+
<div className="absolute inset-0 overflow-hidden">
|
| 621 |
+
<div className="absolute -top-10 -right-10 w-40 h-40 bg-white opacity-5 rounded-full"></div>
|
| 622 |
+
<div className="absolute -bottom-8 -left-8 w-24 h-24 bg-white opacity-10 rounded-full"></div>
|
| 623 |
+
</div>
|
| 624 |
+
<div className="w-10 h-10 rounded-full bg-white/20 flex items-center justify-center mr-4 relative">
|
| 625 |
+
<IconGlobe size="large" style={{ color: '#ffffff' }} />
|
| 626 |
+
</div>
|
| 627 |
+
<div className="relative">
|
| 628 |
+
<Text style={{ color: '#ffffff' }} className="text-lg font-medium">{t('API 配置')}</Text>
|
| 629 |
+
<div style={{ color: '#ffffff' }} className="text-sm opacity-80">{t('API 地址和相关配置')}</div>
|
| 630 |
+
</div>
|
| 631 |
+
</div>
|
| 632 |
+
|
| 633 |
+
<div className="space-y-4">
|
| 634 |
+
{inputs.type === 40 && (
|
| 635 |
+
<Banner
|
| 636 |
+
type='info'
|
| 637 |
+
description={
|
| 638 |
+
<div>
|
| 639 |
+
<Text strong>{t('邀请链接')}:</Text>
|
| 640 |
+
<Text
|
| 641 |
+
link
|
| 642 |
+
underline
|
| 643 |
+
className="ml-2 cursor-pointer"
|
| 644 |
+
onClick={() => window.open('https://cloud.siliconflow.cn/i/hij0YNTZ')}
|
| 645 |
+
>
|
| 646 |
+
https://cloud.siliconflow.cn/i/hij0YNTZ
|
| 647 |
+
</Text>
|
| 648 |
+
</div>
|
| 649 |
+
}
|
| 650 |
+
className='!rounded-lg'
|
| 651 |
+
/>
|
| 652 |
+
)}
|
| 653 |
+
|
| 654 |
+
{inputs.type === 3 && (
|
| 655 |
+
<>
|
| 656 |
+
<Banner
|
| 657 |
+
type='warning'
|
| 658 |
+
description={t('2025年5月10日后添加的渠道,不需要再在部署的时候移除模型名称中的"."')}
|
| 659 |
+
className='!rounded-lg'
|
| 660 |
+
/>
|
| 661 |
+
<div>
|
| 662 |
+
<Text strong className="block mb-2">AZURE_OPENAI_ENDPOINT</Text>
|
| 663 |
+
<Input
|
| 664 |
+
name='azure_base_url'
|
| 665 |
+
placeholder={t('请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com')}
|
| 666 |
+
onChange={(value) => handleInputChange('base_url', value)}
|
| 667 |
+
value={inputs.base_url}
|
| 668 |
+
autoComplete='new-password'
|
| 669 |
+
size="large"
|
| 670 |
+
className="!rounded-lg"
|
| 671 |
+
/>
|
| 672 |
+
</div>
|
| 673 |
+
<div>
|
| 674 |
+
<Text strong className="block mb-2">{t('默认 API 版本')}</Text>
|
| 675 |
+
<Input
|
| 676 |
+
name='azure_other'
|
| 677 |
+
placeholder={t('请输入默认 API 版本,例如:2025-04-01-preview')}
|
| 678 |
+
onChange={(value) => handleInputChange('other', value)}
|
| 679 |
+
value={inputs.other}
|
| 680 |
+
autoComplete='new-password'
|
| 681 |
+
size="large"
|
| 682 |
+
className="!rounded-lg"
|
| 683 |
+
/>
|
| 684 |
+
</div>
|
| 685 |
+
</>
|
| 686 |
+
)}
|
| 687 |
+
|
| 688 |
+
{inputs.type === 8 && (
|
| 689 |
+
<>
|
| 690 |
+
<Banner
|
| 691 |
+
type='warning'
|
| 692 |
+
description={t('如果你对接的是上游One API或者New API等转发项目,请使用OpenAI类型,不要使用此类型,除非你知道你在做什么。')}
|
| 693 |
+
className='!rounded-lg'
|
| 694 |
+
/>
|
| 695 |
+
<div>
|
| 696 |
+
<Text strong className="block mb-2">{t('完整的 Base URL,支持变量{model}')}</Text>
|
| 697 |
+
<Input
|
| 698 |
+
name='base_url'
|
| 699 |
+
placeholder={t('请输入完整的URL,例如:https://api.openai.com/v1/chat/completions')}
|
| 700 |
+
onChange={(value) => handleInputChange('base_url', value)}
|
| 701 |
+
value={inputs.base_url}
|
| 702 |
+
autoComplete='new-password'
|
| 703 |
+
size="large"
|
| 704 |
+
className="!rounded-lg"
|
| 705 |
+
/>
|
| 706 |
+
</div>
|
| 707 |
+
</>
|
| 708 |
+
)}
|
| 709 |
+
|
| 710 |
+
{inputs.type === 37 && (
|
| 711 |
+
<Banner
|
| 712 |
+
type='warning'
|
| 713 |
+
description={t('Dify渠道只适配chatflow和agent,并且agent不支持图片!')}
|
| 714 |
+
className='!rounded-lg'
|
| 715 |
+
/>
|
| 716 |
+
)}
|
| 717 |
+
|
| 718 |
+
{inputs.type !== 3 && inputs.type !== 8 && inputs.type !== 22 && inputs.type !== 36 && inputs.type !== 45 && (
|
| 719 |
+
<div>
|
| 720 |
+
<Text strong className="block mb-2">{t('API地址')}</Text>
|
| 721 |
+
<Input
|
| 722 |
+
name='base_url'
|
| 723 |
+
placeholder={t('此项可选,用于通过自定义API地址来进行 API 调用,末尾不要带/v1和/')}
|
| 724 |
+
onChange={(value) => handleInputChange('base_url', value)}
|
| 725 |
+
value={inputs.base_url}
|
| 726 |
+
autoComplete='new-password'
|
| 727 |
+
size="large"
|
| 728 |
+
className="!rounded-lg"
|
| 729 |
+
/>
|
| 730 |
+
<Text type="tertiary" className="mt-1 text-xs">
|
| 731 |
+
{t('对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写')}
|
| 732 |
+
</Text>
|
| 733 |
+
</div>
|
| 734 |
+
)}
|
| 735 |
+
|
| 736 |
+
{inputs.type === 22 && (
|
| 737 |
+
<div>
|
| 738 |
+
<Text strong className="block mb-2">{t('私有部署地址')}</Text>
|
| 739 |
+
<Input
|
| 740 |
+
name='base_url'
|
| 741 |
+
placeholder={t('请输入私有部署地址,格式为:https://fastgpt.run/api/openapi')}
|
| 742 |
+
onChange={(value) => handleInputChange('base_url', value)}
|
| 743 |
+
value={inputs.base_url}
|
| 744 |
+
autoComplete='new-password'
|
| 745 |
+
size="large"
|
| 746 |
+
className="!rounded-lg"
|
| 747 |
+
/>
|
| 748 |
+
</div>
|
| 749 |
+
)}
|
| 750 |
+
|
| 751 |
+
{inputs.type === 36 && (
|
| 752 |
+
<div>
|
| 753 |
+
<Text strong className="block mb-2">
|
| 754 |
+
{t('注意非Chat API,请务必填写正确的API地址,否则可能导致无法使用')}
|
| 755 |
+
</Text>
|
| 756 |
+
<Input
|
| 757 |
+
name='base_url'
|
| 758 |
+
placeholder={t('请输入到 /suno 前的路径,通常就是域名,例如:https://api.example.com')}
|
| 759 |
+
onChange={(value) => handleInputChange('base_url', value)}
|
| 760 |
+
value={inputs.base_url}
|
| 761 |
+
autoComplete='new-password'
|
| 762 |
+
size="large"
|
| 763 |
+
className="!rounded-lg"
|
| 764 |
+
/>
|
| 765 |
+
</div>
|
| 766 |
+
)}
|
| 767 |
+
</div>
|
| 768 |
+
</Card>
|
| 769 |
+
|
| 770 |
+
{/* Model Configuration Card */}
|
| 771 |
+
<Card className="!rounded-2xl shadow-sm border-0 mb-6">
|
| 772 |
+
<div className="flex items-center mb-4 p-6 rounded-xl" style={{
|
| 773 |
+
background: 'linear-gradient(135deg, #4c1d95 0%, #6d28d9 50%, #7c3aed 100%)',
|
| 774 |
+
position: 'relative'
|
| 775 |
+
}}>
|
| 776 |
+
<div className="absolute inset-0 overflow-hidden">
|
| 777 |
+
<div className="absolute -top-10 -right-10 w-40 h-40 bg-white opacity-5 rounded-full"></div>
|
| 778 |
+
<div className="absolute -bottom-8 -left-8 w-24 h-24 bg-white opacity-10 rounded-full"></div>
|
| 779 |
+
</div>
|
| 780 |
+
<div className="w-10 h-10 rounded-full bg-white/20 flex items-center justify-center mr-4 relative">
|
| 781 |
+
<IconCode size="large" style={{ color: '#ffffff' }} />
|
| 782 |
+
</div>
|
| 783 |
+
<div className="relative">
|
| 784 |
+
<Text style={{ color: '#ffffff' }} className="text-lg font-medium">{t('模型配置')}</Text>
|
| 785 |
+
<div style={{ color: '#ffffff' }} className="text-sm opacity-80">{t('模型选择和映射设置')}</div>
|
| 786 |
+
</div>
|
| 787 |
+
</div>
|
| 788 |
+
|
| 789 |
+
<div className="space-y-4">
|
| 790 |
+
<div>
|
| 791 |
+
<Text strong className="block mb-2">{t('模型')}</Text>
|
| 792 |
+
<Select
|
| 793 |
+
placeholder={t('请选择该渠道所支持的模型')}
|
| 794 |
+
name='models'
|
| 795 |
+
required
|
| 796 |
+
multiple
|
| 797 |
+
selection
|
| 798 |
+
filter
|
| 799 |
+
searchPosition='dropdown'
|
| 800 |
+
onChange={(value) => handleInputChange('models', value)}
|
| 801 |
+
value={inputs.models}
|
| 802 |
+
autoComplete='new-password'
|
| 803 |
+
optionList={modelOptions}
|
| 804 |
+
size="large"
|
| 805 |
+
className="!rounded-lg"
|
| 806 |
+
/>
|
| 807 |
+
</div>
|
| 808 |
+
|
| 809 |
+
<div className="flex flex-wrap gap-2">
|
| 810 |
+
<Button
|
| 811 |
+
type='primary'
|
| 812 |
+
onClick={() => handleInputChange('models', basicModels)}
|
| 813 |
+
size="large"
|
| 814 |
+
className="!rounded-lg"
|
| 815 |
+
>
|
| 816 |
+
{t('填入相关模型')}
|
| 817 |
+
</Button>
|
| 818 |
+
<Button
|
| 819 |
+
type='secondary'
|
| 820 |
+
onClick={() => handleInputChange('models', fullModels)}
|
| 821 |
+
size="large"
|
| 822 |
+
className="!rounded-lg"
|
| 823 |
+
>
|
| 824 |
+
{t('填入所有模型')}
|
| 825 |
+
</Button>
|
| 826 |
+
<Button
|
| 827 |
+
type='tertiary'
|
| 828 |
+
onClick={() => fetchUpstreamModelList('models')}
|
| 829 |
+
size="large"
|
| 830 |
+
className="!rounded-lg"
|
| 831 |
+
>
|
| 832 |
+
{t('获取模型列表')}
|
| 833 |
+
</Button>
|
| 834 |
+
<Button
|
| 835 |
+
theme='light'
|
| 836 |
+
type='primary'
|
| 837 |
+
onClick={handleAddPrefix}
|
| 838 |
+
size='large'
|
| 839 |
+
className='!rounded-lg'
|
| 840 |
+
>
|
| 841 |
+
{t('一键添加前缀')}
|
| 842 |
+
</Button>
|
| 843 |
+
<Button
|
| 844 |
+
type='warning'
|
| 845 |
+
onClick={() => handleInputChange('models', [])}
|
| 846 |
+
size="large"
|
| 847 |
+
className="!rounded-lg"
|
| 848 |
+
>
|
| 849 |
+
{t('清除所有模型')}
|
| 850 |
+
</Button>
|
| 851 |
+
</div>
|
| 852 |
+
|
| 853 |
+
<div>
|
| 854 |
+
<Input
|
| 855 |
+
addonAfter={
|
| 856 |
+
<Button type='primary' onClick={addCustomModels} className="!rounded-r-lg">
|
| 857 |
+
{t('填入')}
|
| 858 |
+
</Button>
|
| 859 |
+
}
|
| 860 |
+
placeholder={t('输入自定义模型名称')}
|
| 861 |
+
value={customModel}
|
| 862 |
+
onChange={(value) => setCustomModel(value.trim())}
|
| 863 |
+
size="large"
|
| 864 |
+
className="!rounded-lg"
|
| 865 |
+
/>
|
| 866 |
+
</div>
|
| 867 |
+
|
| 868 |
+
<div>
|
| 869 |
+
<Text strong className="block mb-2">{t('模型重定向')}</Text>
|
| 870 |
+
<TextArea
|
| 871 |
+
placeholder={
|
| 872 |
+
t('此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:') +
|
| 873 |
+
`\n${JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)}`
|
| 874 |
+
}
|
| 875 |
+
name='model_mapping'
|
| 876 |
+
onChange={(value) => handleInputChange('model_mapping', value)}
|
| 877 |
+
autosize
|
| 878 |
+
value={inputs.model_mapping}
|
| 879 |
+
autoComplete='new-password'
|
| 880 |
+
className="!rounded-lg font-mono"
|
| 881 |
+
/>
|
| 882 |
+
<Text
|
| 883 |
+
className="!text-semi-color-primary cursor-pointer mt-1 block"
|
| 884 |
+
onClick={() => handleInputChange('model_mapping', JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2))}
|
| 885 |
+
>
|
| 886 |
+
{t('填入模板')}
|
| 887 |
+
</Text>
|
| 888 |
+
</div>
|
| 889 |
+
|
| 890 |
+
<div>
|
| 891 |
+
<Text strong className="block mb-2">{t('默认测试模型')}</Text>
|
| 892 |
+
<Input
|
| 893 |
+
name='test_model'
|
| 894 |
+
placeholder={t('不填则为模型列表第一个')}
|
| 895 |
+
onChange={(value) => handleInputChange('test_model', value)}
|
| 896 |
+
value={inputs.test_model}
|
| 897 |
+
size="large"
|
| 898 |
+
className="!rounded-lg"
|
| 899 |
+
/>
|
| 900 |
+
</div>
|
| 901 |
+
</div>
|
| 902 |
+
</Card>
|
| 903 |
+
|
| 904 |
+
{/* Advanced Settings Card */}
|
| 905 |
+
<Card className="!rounded-2xl shadow-sm border-0 mb-6">
|
| 906 |
+
<div className="flex items-center mb-4 p-6 rounded-xl" style={{
|
| 907 |
+
background: 'linear-gradient(135deg, #92400e 0%, #d97706 50%, #f59e0b 100%)',
|
| 908 |
+
position: 'relative'
|
| 909 |
+
}}>
|
| 910 |
+
<div className="absolute inset-0 overflow-hidden">
|
| 911 |
+
<div className="absolute -top-10 -right-10 w-40 h-40 bg-white opacity-5 rounded-full"></div>
|
| 912 |
+
<div className="absolute -bottom-8 -left-8 w-24 h-24 bg-white opacity-10 rounded-full"></div>
|
| 913 |
+
</div>
|
| 914 |
+
<div className="w-10 h-10 rounded-full bg-white/20 flex items-center justify-center mr-4 relative">
|
| 915 |
+
<IconSetting size="large" style={{ color: '#ffffff' }} />
|
| 916 |
+
</div>
|
| 917 |
+
<div className="relative">
|
| 918 |
+
<Text style={{ color: '#ffffff' }} className="text-lg font-medium">{t('高级设置')}</Text>
|
| 919 |
+
<div style={{ color: '#ffffff' }} className="text-sm opacity-80">{t('渠道的高级配置选项')}</div>
|
| 920 |
+
</div>
|
| 921 |
+
</div>
|
| 922 |
+
|
| 923 |
+
<div className="space-y-4">
|
| 924 |
+
<div>
|
| 925 |
+
<Text strong className="block mb-2">{t('分组')}</Text>
|
| 926 |
+
<Select
|
| 927 |
+
placeholder={t('请选择可以使用该渠道的分组')}
|
| 928 |
+
name='groups'
|
| 929 |
+
required
|
| 930 |
+
multiple
|
| 931 |
+
selection
|
| 932 |
+
allowAdditions
|
| 933 |
+
additionLabel={t('请在系统设置页面编辑分组倍率以添加新的分组:')}
|
| 934 |
+
onChange={(value) => handleInputChange('groups', value)}
|
| 935 |
+
value={inputs.groups}
|
| 936 |
+
autoComplete='new-password'
|
| 937 |
+
optionList={groupOptions}
|
| 938 |
+
size="large"
|
| 939 |
+
className="!rounded-lg"
|
| 940 |
+
/>
|
| 941 |
+
</div>
|
| 942 |
+
|
| 943 |
+
{inputs.type === 18 && (
|
| 944 |
+
<div>
|
| 945 |
+
<Text strong className="block mb-2">{t('模型版本')}</Text>
|
| 946 |
+
<Input
|
| 947 |
+
name='other'
|
| 948 |
+
placeholder={'请输入星火大模型版本,注意是接口地址中的版本号,例如:v2.1'}
|
| 949 |
+
onChange={(value) => handleInputChange('other', value)}
|
| 950 |
+
value={inputs.other}
|
| 951 |
+
autoComplete='new-password'
|
| 952 |
+
size="large"
|
| 953 |
+
className="!rounded-lg"
|
| 954 |
+
/>
|
| 955 |
+
</div>
|
| 956 |
+
)}
|
| 957 |
+
|
| 958 |
+
{inputs.type === 41 && (
|
| 959 |
+
<div>
|
| 960 |
+
<Text strong className="block mb-2">{t('部署地区')}</Text>
|
| 961 |
+
<TextArea
|
| 962 |
+
name='other'
|
| 963 |
+
placeholder={t(
|
| 964 |
+
'请输入部署地区,例如:us-central1\n支持使用模型映射格式\n' +
|
| 965 |
+
'{\n' +
|
| 966 |
+
' "default": "us-central1",\n' +
|
| 967 |
+
' "claude-3-5-sonnet-20240620": "europe-west1"\n' +
|
| 968 |
+
'}'
|
| 969 |
+
)}
|
| 970 |
+
autosize={{ minRows: 2 }}
|
| 971 |
+
onChange={(value) => handleInputChange('other', value)}
|
| 972 |
+
value={inputs.other}
|
| 973 |
+
autoComplete='new-password'
|
| 974 |
+
className="!rounded-lg font-mono"
|
| 975 |
+
/>
|
| 976 |
+
<Text
|
| 977 |
+
className="!text-semi-color-primary cursor-pointer mt-1 block"
|
| 978 |
+
onClick={() => handleInputChange('other', JSON.stringify(REGION_EXAMPLE, null, 2))}
|
| 979 |
+
>
|
| 980 |
+
{t('填入模板')}
|
| 981 |
+
</Text>
|
| 982 |
+
</div>
|
| 983 |
+
)}
|
| 984 |
+
|
| 985 |
+
{inputs.type === 21 && (
|
| 986 |
+
<div>
|
| 987 |
+
<Text strong className="block mb-2">{t('知识库 ID')}</Text>
|
| 988 |
+
<Input
|
| 989 |
+
name='other'
|
| 990 |
+
placeholder={'请输入知识库 ID,例如:123456'}
|
| 991 |
+
onChange={(value) => handleInputChange('other', value)}
|
| 992 |
+
value={inputs.other}
|
| 993 |
+
autoComplete='new-password'
|
| 994 |
+
size="large"
|
| 995 |
+
className="!rounded-lg"
|
| 996 |
+
/>
|
| 997 |
+
</div>
|
| 998 |
+
)}
|
| 999 |
+
|
| 1000 |
+
{inputs.type === 39 && (
|
| 1001 |
+
<div>
|
| 1002 |
+
<Text strong className="block mb-2">Account ID</Text>
|
| 1003 |
+
<Input
|
| 1004 |
+
name='other'
|
| 1005 |
+
placeholder={'请输入Account ID,例如:d6b5da8hk1awo8nap34ube6gh'}
|
| 1006 |
+
onChange={(value) => handleInputChange('other', value)}
|
| 1007 |
+
value={inputs.other}
|
| 1008 |
+
autoComplete='new-password'
|
| 1009 |
+
size="large"
|
| 1010 |
+
className="!rounded-lg"
|
| 1011 |
+
/>
|
| 1012 |
+
</div>
|
| 1013 |
+
)}
|
| 1014 |
+
|
| 1015 |
+
{inputs.type === 49 && (
|
| 1016 |
+
<div>
|
| 1017 |
+
<Text strong className="block mb-2">{t('智能体ID')}</Text>
|
| 1018 |
+
<Input
|
| 1019 |
+
name='other'
|
| 1020 |
+
placeholder={'请输入智能体ID,例如:7342866812345'}
|
| 1021 |
+
onChange={(value) => handleInputChange('other', value)}
|
| 1022 |
+
value={inputs.other}
|
| 1023 |
+
autoComplete='new-password'
|
| 1024 |
+
size="large"
|
| 1025 |
+
className="!rounded-lg"
|
| 1026 |
+
/>
|
| 1027 |
+
</div>
|
| 1028 |
+
)}
|
| 1029 |
+
|
| 1030 |
+
<div>
|
| 1031 |
+
<Text strong className="block mb-2">{t('渠道标签')}</Text>
|
| 1032 |
+
<Input
|
| 1033 |
+
name='tag'
|
| 1034 |
+
placeholder={t('渠道标签')}
|
| 1035 |
+
onChange={(value) => handleInputChange('tag', value)}
|
| 1036 |
+
value={inputs.tag}
|
| 1037 |
+
autoComplete='new-password'
|
| 1038 |
+
size="large"
|
| 1039 |
+
className="!rounded-lg"
|
| 1040 |
+
/>
|
| 1041 |
+
</div>
|
| 1042 |
+
|
| 1043 |
+
<div>
|
| 1044 |
+
<Text strong className="block mb-2">{t('渠道优先级')}</Text>
|
| 1045 |
+
<Input
|
| 1046 |
+
name='priority'
|
| 1047 |
+
placeholder={t('渠道优先级')}
|
| 1048 |
+
onChange={(value) => {
|
| 1049 |
+
const number = parseInt(value);
|
| 1050 |
+
if (isNaN(number)) {
|
| 1051 |
+
handleInputChange('priority', value);
|
| 1052 |
+
} else {
|
| 1053 |
+
handleInputChange('priority', number);
|
| 1054 |
+
}
|
| 1055 |
+
}}
|
| 1056 |
+
value={inputs.priority}
|
| 1057 |
+
autoComplete='new-password'
|
| 1058 |
+
size="large"
|
| 1059 |
+
className="!rounded-lg"
|
| 1060 |
+
/>
|
| 1061 |
+
</div>
|
| 1062 |
+
|
| 1063 |
+
<div>
|
| 1064 |
+
<Text strong className="block mb-2">{t('渠道权重')}</Text>
|
| 1065 |
+
<Input
|
| 1066 |
+
name='weight'
|
| 1067 |
+
placeholder={t('渠道权重')}
|
| 1068 |
+
onChange={(value) => {
|
| 1069 |
+
const number = parseInt(value);
|
| 1070 |
+
if (isNaN(number)) {
|
| 1071 |
+
handleInputChange('weight', value);
|
| 1072 |
+
} else {
|
| 1073 |
+
handleInputChange('weight', number);
|
| 1074 |
+
}
|
| 1075 |
+
}}
|
| 1076 |
+
value={inputs.weight}
|
| 1077 |
+
autoComplete='new-password'
|
| 1078 |
+
size="large"
|
| 1079 |
+
className="!rounded-lg"
|
| 1080 |
+
/>
|
| 1081 |
+
</div>
|
| 1082 |
+
|
| 1083 |
+
<div>
|
| 1084 |
+
<Text strong className="block mb-2">{t('渠道额外设置')}</Text>
|
| 1085 |
+
<TextArea
|
| 1086 |
+
placeholder={
|
| 1087 |
+
t('此项可选,用于配置渠道特定设置,为一个 JSON 字符串,例如:') +
|
| 1088 |
+
'\n{\n "force_format": true\n}'
|
| 1089 |
+
}
|
| 1090 |
+
name='setting'
|
| 1091 |
+
onChange={(value) => handleInputChange('setting', value)}
|
| 1092 |
+
autosize
|
| 1093 |
+
value={inputs.setting}
|
| 1094 |
+
autoComplete='new-password'
|
| 1095 |
+
className="!rounded-lg font-mono"
|
| 1096 |
+
/>
|
| 1097 |
+
<div className="flex gap-2 mt-1">
|
| 1098 |
+
<Text
|
| 1099 |
+
className="!text-semi-color-primary cursor-pointer"
|
| 1100 |
+
onClick={() => {
|
| 1101 |
+
handleInputChange(
|
| 1102 |
+
'setting',
|
| 1103 |
+
JSON.stringify({ force_format: true }, null, 2),
|
| 1104 |
+
);
|
| 1105 |
+
}}
|
| 1106 |
+
>
|
| 1107 |
+
{t('填入模板')}
|
| 1108 |
+
</Text>
|
| 1109 |
+
<Text
|
| 1110 |
+
className="!text-semi-color-primary cursor-pointer"
|
| 1111 |
+
onClick={() => {
|
| 1112 |
+
window.open(
|
| 1113 |
+
'https://github.com/QuantumNous/new-api/blob/main/docs/channel/other_setting.md',
|
| 1114 |
+
);
|
| 1115 |
+
}}
|
| 1116 |
+
>
|
| 1117 |
+
{t('设置说明')}
|
| 1118 |
+
</Text>
|
| 1119 |
+
</div>
|
| 1120 |
+
</div>
|
| 1121 |
+
|
| 1122 |
+
<div>
|
| 1123 |
+
<Text strong className="block mb-2">{t('参数覆盖')}</Text>
|
| 1124 |
+
<TextArea
|
| 1125 |
+
placeholder={
|
| 1126 |
+
t('此项可选,用于覆盖请求参数。不支持覆盖 stream 参数。为一个 JSON 字符串,例如:') +
|
| 1127 |
+
'\n{\n "temperature": 0\n}'
|
| 1128 |
+
}
|
| 1129 |
+
name='param_override'
|
| 1130 |
+
onChange={(value) => handleInputChange('param_override', value)}
|
| 1131 |
+
autosize
|
| 1132 |
+
value={inputs.param_override}
|
| 1133 |
+
autoComplete='new-password'
|
| 1134 |
+
className="!rounded-lg font-mono"
|
| 1135 |
+
/>
|
| 1136 |
+
</div>
|
| 1137 |
+
|
| 1138 |
+
{inputs.type === 1 && (
|
| 1139 |
+
<div>
|
| 1140 |
+
<Text strong className="block mb-2">{t('组织')}</Text>
|
| 1141 |
+
<Input
|
| 1142 |
+
name='openai_organization'
|
| 1143 |
+
placeholder={t('请输入组织org-xxx')}
|
| 1144 |
+
onChange={(value) => handleInputChange('openai_organization', value)}
|
| 1145 |
+
value={inputs.openai_organization}
|
| 1146 |
+
size="large"
|
| 1147 |
+
className="!rounded-lg"
|
| 1148 |
+
/>
|
| 1149 |
+
<Text type="tertiary" className="mt-1 text-xs">
|
| 1150 |
+
{t('组织,可选,不填则为默认组织')}
|
| 1151 |
+
</Text>
|
| 1152 |
+
</div>
|
| 1153 |
+
)}
|
| 1154 |
+
|
| 1155 |
+
<div className="flex items-center">
|
| 1156 |
+
<Checkbox
|
| 1157 |
+
checked={autoBan}
|
| 1158 |
+
onChange={() => setAutoBan(!autoBan)}
|
| 1159 |
+
/>
|
| 1160 |
+
<Text strong className="ml-2">
|
| 1161 |
+
{t('是否自动禁用(仅当自动禁用开启时有效),关闭后不会自动禁用该渠道')}
|
| 1162 |
+
</Text>
|
| 1163 |
+
</div>
|
| 1164 |
+
|
| 1165 |
+
<div>
|
| 1166 |
+
<Text strong className="block mb-2">
|
| 1167 |
+
{t('状态码复写(仅影响本地判断,不修改返回到上游的状态码)')}
|
| 1168 |
+
</Text>
|
| 1169 |
+
<TextArea
|
| 1170 |
+
placeholder={
|
| 1171 |
+
t('此项可选,用于复写返回的状态码,比如将claude渠道的400错误复写为500(用于重试),请勿滥用该功能,例如:') +
|
| 1172 |
+
'\n' +
|
| 1173 |
+
JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2)
|
| 1174 |
+
}
|
| 1175 |
+
name='status_code_mapping'
|
| 1176 |
+
onChange={(value) => handleInputChange('status_code_mapping', value)}
|
| 1177 |
+
autosize
|
| 1178 |
+
value={inputs.status_code_mapping}
|
| 1179 |
+
autoComplete='new-password'
|
| 1180 |
+
className="!rounded-lg font-mono"
|
| 1181 |
+
/>
|
| 1182 |
+
<Text
|
| 1183 |
+
className="!text-semi-color-primary cursor-pointer mt-1 block"
|
| 1184 |
+
onClick={() => {
|
| 1185 |
+
handleInputChange(
|
| 1186 |
+
'status_code_mapping',
|
| 1187 |
+
JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2),
|
| 1188 |
+
);
|
| 1189 |
+
}}
|
| 1190 |
+
>
|
| 1191 |
+
{t('填入模板')}
|
| 1192 |
+
</Text>
|
| 1193 |
+
</div>
|
| 1194 |
+
</div>
|
| 1195 |
+
</Card>
|
| 1196 |
+
</div>
|
| 1197 |
+
</Spin>
|
| 1198 |
+
<ImagePreview
|
| 1199 |
+
src={modalImageUrl}
|
| 1200 |
+
visible={isModalOpenurl}
|
| 1201 |
+
onVisibleChange={(visible) => setIsModalOpenurl(visible)}
|
| 1202 |
+
/>
|
| 1203 |
+
</SideSheet>
|
| 1204 |
+
</>
|
| 1205 |
+
);
|
| 1206 |
+
};
|
| 1207 |
+
|
| 1208 |
+
export default EditChannel;
|
README.md
CHANGED
|
@@ -1,10 +1,35 @@
|
|
| 1 |
-
---
|
| 2 |
-
title: New
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
-
sdk: docker
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Custom New-API Instance
|
| 3 |
+
emoji: 🚀
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: green
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 3000
|
| 8 |
+
license: apache-2.0
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# Custom New-API Instance with Model Prefix Feature
|
| 12 |
+
|
| 13 |
+
This Hugging Face Space runs a customized instance of the [New-API](https://github.com/QuantumNous/new-api) project.
|
| 14 |
+
|
| 15 |
+
## Key Features
|
| 16 |
+
|
| 17 |
+
* **Always Up-to-Date:** Automatically clones the latest code from the `QuantumNous/new-api` repository on each build, ensuring you have recent updates.
|
| 18 |
+
* **Custom UI Enhancement:** Includes a modified `EditChannel.js` from [timigogo/new-api-edit-channel](https://github.com/timigogo/new-api-edit-channel) which re-implements the "一键添加模型前缀" (Add Prefix with One Click) button on the channel editing page. This allows easy prefixing of model names with the channel name and generates the corresponding model mapping.
|
| 19 |
+
* **Dockerized Deployment:** Uses the provided multi-stage `Dockerfile` for a clean and reproducible build process. The Dockerfile handles fetching code, replacing the file, and building both frontend and backend components.
|
| 20 |
+
|
| 21 |
+
## Usage
|
| 22 |
+
|
| 23 |
+
Once the Space is running, you can access the New-API web interface via the public URL assigned by Hugging Face. The application listens on port 3000 internally.
|
| 24 |
+
|
| 25 |
+
Use this instance as you would a standard New-API deployment. The primary difference is the added button functionality on the channel editing form.
|
| 26 |
+
|
| 27 |
+
## Technical Details
|
| 28 |
+
|
| 29 |
+
* **Base Project:** [QuantumNous/new-API](https://github.com/QuantumNous/new-api) (Cloned automatically)
|
| 30 |
+
* **Customization Source:** [timigogo/new-api-edit-channel](https://github.com/timigogo/new-api-edit-channel) (`EditChannel.js`) (Cloned automatically)
|
| 31 |
+
* **Build Environment:** Defined by `Dockerfile`. The build process includes cache-busting techniques to ensure the latest code is fetched when the Space is rebuilt.
|
| 32 |
+
|
| 33 |
+
## License
|
| 34 |
+
|
| 35 |
+
This customized deployment is based on the New-API project, which uses the Apache 2.0 License. The license terms apply accordingly.
|