Expose image studio flow and related admin controls
Bundle the current backend and web changes into one publishable snapshot from the server worktree so the remote mirror reflects the deployed source. Constraint: Push the existing mixed worktree from /docker/new-api/src without reshaping files Rejected: Split into feature-specific commits | current worktree is already a single undifferentiated snapshot Confidence: medium Scope-risk: moderate Directive: Split future feature work before deployment to preserve clearer history Tested: git status inspection; git diff --stat review Not-tested: build, lint, unit tests, integration tests
This commit is contained in:
@@ -6,7 +6,7 @@ COPY web/bun.lock .
|
||||
RUN bun install
|
||||
COPY ./web .
|
||||
COPY ./VERSION .
|
||||
RUN DISABLE_ESLINT_PLUGIN='true' NODE_OPTIONS="--max-old-space-size=3072" VITE_REACT_APP_VERSION=$(cat VERSION) bun run build
|
||||
RUN DISABLE_ESLINT_PLUGIN='true' NODE_OPTIONS="--max-old-space-size=4096" VITE_REACT_APP_VERSION=$(cat VERSION) bun run build
|
||||
|
||||
FROM golang:1.26.1-alpine@sha256:2389ebfa5b7f43eeafbd6be0c3700cc46690ef842ad962f6c5bd6be49ed82039 AS builder2
|
||||
ENV GO111MODULE=on CGO_ENABLED=0
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
FROM oven/bun:latest AS builder
|
||||
|
||||
WORKDIR /build
|
||||
COPY web/package.json .
|
||||
COPY web/bun.lock .
|
||||
RUN bun install
|
||||
COPY ./web .
|
||||
COPY ./VERSION .
|
||||
RUN DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(cat VERSION) bun run build
|
||||
|
||||
FROM golang:alpine AS builder2
|
||||
ENV GO111MODULE=on CGO_ENABLED=0
|
||||
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
ENV GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH:-amd64}
|
||||
ENV GOEXPERIMENT=greenteagc
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
ADD go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
COPY --from=builder /build/dist ./web/dist
|
||||
RUN go build -ldflags "-s -w -X 'github.com/QuantumNous/new-api/common.Version=$(cat VERSION)'" -o new-api
|
||||
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends ca-certificates tzdata libasan8 wget \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& update-ca-certificates
|
||||
|
||||
COPY --from=builder2 /build/new-api /
|
||||
EXPOSE 3000
|
||||
WORKDIR /data
|
||||
ENTRYPOINT ["/new-api"]
|
||||
@@ -39,8 +39,10 @@ func GetEndpointTypesByChannelType(channelType int, modelName string) []constant
|
||||
}
|
||||
}
|
||||
if IsImageGenerationModel(modelName) {
|
||||
// add to first
|
||||
endpointTypes = append([]constant.EndpointType{constant.EndpointTypeImageGeneration}, endpointTypes...)
|
||||
}
|
||||
if IsImageEditModel(modelName) {
|
||||
endpointTypes = append([]constant.EndpointType{constant.EndpointTypeImageEdit}, endpointTypes...)
|
||||
}
|
||||
return endpointTypes
|
||||
}
|
||||
|
||||
@@ -13,10 +13,16 @@ var (
|
||||
"dall-e-3",
|
||||
"dall-e-2",
|
||||
"gpt-image-1",
|
||||
"gpt-image-2",
|
||||
"prefix:imagen-",
|
||||
"flux-",
|
||||
"flux.1-",
|
||||
}
|
||||
ImageEditModels = []string{
|
||||
"gpt-image-1",
|
||||
"gpt-image-2",
|
||||
"prefix:qwen-image-edit",
|
||||
}
|
||||
OpenAITextModels = []string{
|
||||
"gpt-",
|
||||
"o1",
|
||||
@@ -48,6 +54,19 @@ func IsImageGenerationModel(modelName string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func IsImageEditModel(modelName string) bool {
|
||||
modelName = strings.ToLower(modelName)
|
||||
for _, m := range ImageEditModels {
|
||||
if strings.Contains(modelName, m) {
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(m, "prefix:") && strings.HasPrefix(modelName, strings.TrimPrefix(m, "prefix:")) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func IsOpenAITextModel(modelName string) bool {
|
||||
modelName = strings.ToLower(modelName)
|
||||
for _, m := range OpenAITextModels {
|
||||
|
||||
1087
controller/image_studio.go
Normal file
1087
controller/image_studio.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -16,6 +16,10 @@ const (
|
||||
SecureVerificationSessionKey = "secure_verified_at"
|
||||
// PasskeyReadySessionKey means WebAuthn finished and /api/verify can finalize step-up verification.
|
||||
PasskeyReadySessionKey = "secure_passkey_ready_at"
|
||||
// secureVerificationMethodSessionKey stores the method that completed step-up verification.
|
||||
secureVerificationMethodSessionKey = "secure_verification_method"
|
||||
secureVerificationMethod2FA = "2fa"
|
||||
secureVerificationMethodPasskey = "passkey"
|
||||
// SecureVerificationTimeout 验证有效期(秒)
|
||||
SecureVerificationTimeout = 300 // 5分钟
|
||||
// PasskeyReadyTimeout passkey ready 标记有效期(秒)
|
||||
@@ -77,6 +81,7 @@ func UniversalVerify(c *gin.Context) {
|
||||
// 根据验证方式进行验证
|
||||
var verified bool
|
||||
var verifyMethod string
|
||||
var sessionMethod string
|
||||
var err error
|
||||
|
||||
switch req.Method {
|
||||
@@ -91,6 +96,7 @@ func UniversalVerify(c *gin.Context) {
|
||||
}
|
||||
verified = validateTwoFactorAuth(twoFA, req.Code)
|
||||
verifyMethod = "2FA"
|
||||
sessionMethod = secureVerificationMethod2FA
|
||||
|
||||
case "passkey":
|
||||
if !hasPasskey {
|
||||
@@ -108,6 +114,7 @@ func UniversalVerify(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
verifyMethod = "Passkey"
|
||||
sessionMethod = secureVerificationMethodPasskey
|
||||
|
||||
default:
|
||||
common.ApiError(c, fmt.Errorf("不支持的验证方式: %s", req.Method))
|
||||
@@ -120,7 +127,7 @@ func UniversalVerify(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 验证成功,在 session 中记录时间戳
|
||||
now, err := setSecureVerificationSession(c)
|
||||
now, err := setSecureVerificationSession(c, sessionMethod)
|
||||
if err != nil {
|
||||
common.ApiError(c, fmt.Errorf("保存验证状态失败: %v", err))
|
||||
return
|
||||
@@ -139,11 +146,16 @@ func UniversalVerify(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
func setSecureVerificationSession(c *gin.Context) (int64, error) {
|
||||
func setSecureVerificationSession(c *gin.Context, method string) (int64, error) {
|
||||
session := sessions.Default(c)
|
||||
session.Delete(PasskeyReadySessionKey)
|
||||
now := time.Now().Unix()
|
||||
session.Set(SecureVerificationSessionKey, now)
|
||||
if method != "" {
|
||||
session.Set(secureVerificationMethodSessionKey, method)
|
||||
} else {
|
||||
session.Delete(secureVerificationMethodSessionKey)
|
||||
}
|
||||
if err := session.Save(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
@@ -475,6 +475,12 @@ func generateDefaultSidebarConfig(userRole int) string {
|
||||
"personal": true,
|
||||
}
|
||||
|
||||
// 其他功能区域 - 所有用户都可以访问
|
||||
defaultConfig["other"] = map[string]interface{}{
|
||||
"enabled": true,
|
||||
"image_studio": true,
|
||||
}
|
||||
|
||||
// 管理员区域 - 根据角色决定
|
||||
if userRole == common.RoleAdminUser {
|
||||
// 管理员可以访问管理员区域,但不能访问系统设置
|
||||
|
||||
@@ -291,10 +291,7 @@ func getModelRequest(c *gin.Context) (*ModelRequest, bool, error) {
|
||||
modelRequest.Model = c.Param("model")
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(c.Request.URL.Path, "/v1/images/generations") {
|
||||
modelRequest.Model = common.GetStringIfEmpty(modelRequest.Model, "dall-e")
|
||||
} else if strings.HasPrefix(c.Request.URL.Path, "/v1/images/edits") {
|
||||
//modelRequest.Model = common.GetStringIfEmpty(c.PostForm("model"), "gpt-image-1")
|
||||
if strings.HasPrefix(c.Request.URL.Path, "/v1/images/generations") || strings.HasPrefix(c.Request.URL.Path, "/v1/images/edits") {
|
||||
contentType := c.ContentType()
|
||||
if slices.Contains([]string{gin.MIMEPOSTForm, gin.MIMEMultipartPOSTForm}, contentType) {
|
||||
req, err := getModelFromRequest(c)
|
||||
@@ -302,6 +299,9 @@ func getModelRequest(c *gin.Context) (*ModelRequest, bool, error) {
|
||||
modelRequest.Model = req.Model
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(c.Request.URL.Path, "/v1/images/generations") {
|
||||
modelRequest.Model = common.GetStringIfEmpty(modelRequest.Model, "dall-e")
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(c.Request.URL.Path, "/v1/audio") {
|
||||
relayMode := relayconstant.RelayModeAudioSpeech
|
||||
|
||||
@@ -160,6 +160,8 @@ func InitOptionMap() {
|
||||
common.OptionMap["CheckSensitiveOnPromptEnabled"] = strconv.FormatBool(setting.CheckSensitiveOnPromptEnabled)
|
||||
common.OptionMap["StopOnSensitiveEnabled"] = strconv.FormatBool(setting.StopOnSensitiveEnabled)
|
||||
common.OptionMap["SensitiveWords"] = setting.SensitiveWordsToString()
|
||||
common.OptionMap["ImageStudioSafetyPrompt"] = setting.ImageStudioSafetyPromptToString()
|
||||
common.OptionMap["ImageStudioBannedWords"] = setting.ImageStudioBannedWordsToString()
|
||||
common.OptionMap["StreamCacheQueueLength"] = strconv.Itoa(setting.StreamCacheQueueLength)
|
||||
common.OptionMap["AutomaticDisableKeywords"] = operation_setting.AutomaticDisableKeywordsToString()
|
||||
common.OptionMap["AutomaticDisableStatusCodes"] = operation_setting.AutomaticDisableStatusCodesToString()
|
||||
@@ -497,6 +499,10 @@ func updateOptionMap(key string, value string) (err error) {
|
||||
common.QuotaPerUnit, _ = strconv.ParseFloat(value, 64)
|
||||
case "SensitiveWords":
|
||||
setting.SensitiveWordsFromString(value)
|
||||
case "ImageStudioSafetyPrompt":
|
||||
setting.ImageStudioSafetyPromptFromString(value)
|
||||
case "ImageStudioBannedWords":
|
||||
setting.ImageStudioBannedWordsFromString(value)
|
||||
case "AutomaticDisableKeywords":
|
||||
operation_setting.AutomaticDisableKeywordsFromString(value)
|
||||
case "AutomaticDisableStatusCodes":
|
||||
|
||||
@@ -434,7 +434,10 @@ func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInf
|
||||
|
||||
func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.ImageRequest) (any, error) {
|
||||
switch info.RelayMode {
|
||||
case relayconstant.RelayModeImagesEdits:
|
||||
case relayconstant.RelayModeImagesEdits, relayconstant.RelayModeImagesGenerations:
|
||||
if info.RelayMode == relayconstant.RelayModeImagesGenerations && !strings.Contains(c.Request.Header.Get("Content-Type"), "multipart/form-data") {
|
||||
return request, nil
|
||||
}
|
||||
|
||||
var requestBody bytes.Buffer
|
||||
writer := multipart.NewWriter(&requestBody)
|
||||
@@ -602,7 +605,8 @@ func (a *Adaptor) ConvertOpenAIResponsesRequest(c *gin.Context, info *relaycommo
|
||||
func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) {
|
||||
if info.RelayMode == relayconstant.RelayModeAudioTranscription ||
|
||||
info.RelayMode == relayconstant.RelayModeAudioTranslation ||
|
||||
info.RelayMode == relayconstant.RelayModeImagesEdits {
|
||||
info.RelayMode == relayconstant.RelayModeImagesEdits ||
|
||||
(info.RelayMode == relayconstant.RelayModeImagesGenerations && strings.Contains(c.Request.Header.Get("Content-Type"), "multipart/form-data")) {
|
||||
return channel.DoFormRequest(a, c, info, requestBody)
|
||||
} else if info.RelayMode == relayconstant.RelayModeRealtime {
|
||||
return channel.DoWssRequest(a, c, info, requestBody)
|
||||
|
||||
@@ -143,11 +143,11 @@ func GetAndValidOpenAIImageRequest(c *gin.Context, relayMode int) (*dto.ImageReq
|
||||
imageRequest := &dto.ImageRequest{}
|
||||
|
||||
switch relayMode {
|
||||
case relayconstant.RelayModeImagesEdits:
|
||||
case relayconstant.RelayModeImagesEdits, relayconstant.RelayModeImagesGenerations:
|
||||
if strings.Contains(c.Request.Header.Get("Content-Type"), "multipart/form-data") {
|
||||
_, err := c.MultipartForm()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse image edit form request: %w", err)
|
||||
return nil, fmt.Errorf("failed to parse image form request: %w", err)
|
||||
}
|
||||
formData := c.Request.PostForm
|
||||
imageRequest.Prompt = formData.Get("prompt")
|
||||
|
||||
@@ -260,6 +260,19 @@ func SetApiRouter(router *gin.Engine) {
|
||||
tokenRoute.POST("/batch", controller.DeleteTokenBatch)
|
||||
}
|
||||
|
||||
imageStudioRoute := apiRouter.Group("/image-studio")
|
||||
imageStudioRoute.Use(middleware.UserAuth())
|
||||
{
|
||||
imageStudioRoute.POST("/generate", controller.SubmitImageStudioGenerate)
|
||||
imageStudioRoute.POST("/edit", controller.SubmitImageStudioEdit)
|
||||
imageStudioRoute.GET("/task/:id", controller.GetImageStudioTask)
|
||||
}
|
||||
imageStudioResultRoute := apiRouter.Group("/image-studio")
|
||||
imageStudioResultRoute.Use(middleware.TokenOrUserAuth())
|
||||
{
|
||||
imageStudioResultRoute.GET("/task/:id/result/:index", controller.GetImageStudioTaskResult)
|
||||
}
|
||||
|
||||
usageRoute := apiRouter.Group("/usage")
|
||||
usageRoute.Use(middleware.CORS(), middleware.CriticalRateLimit())
|
||||
{
|
||||
|
||||
@@ -17,6 +17,7 @@ func SetWebRouter(router *gin.Engine, buildFS embed.FS, indexPage []byte) {
|
||||
router.Use(gzip.Gzip(gzip.DefaultCompression))
|
||||
router.Use(middleware.GlobalWebRateLimit())
|
||||
router.Use(middleware.Cache())
|
||||
router.GET("/image-studio-results/*filepath", controller.GetImageStudioResultFile)
|
||||
router.Use(static.Serve("/", common.EmbedFolder(buildFS, "web/dist")))
|
||||
router.NoRoute(func(c *gin.Context) {
|
||||
c.Set(middleware.RouteTagKey, "web")
|
||||
|
||||
@@ -13,25 +13,136 @@ var StopOnSensitiveEnabled = true
|
||||
// StreamCacheQueueLength 流模式缓存队列长度,0表示无缓存
|
||||
var StreamCacheQueueLength = 0
|
||||
|
||||
const defaultImageStudioSafetyPrompt = `你是一个严格的图片生成安全审核器。
|
||||
如果用户提示词包含或试图引导生成以下内容,必须直接拒绝:
|
||||
1. 暴力、血腥、伤害、虐待、自残、自杀、恐怖主义、战争、武器、尸体、残肢、令人极度恶心的内容。
|
||||
2. 色情、裸露、性暗示、卖淫、约炮、成人视频、强奷、SM、性暴力、性服务相关内容。
|
||||
3. 赌博、博彩、赌场、下注、下注网站、洗钱、毒品、制毒、吸毒、贩毒、违禁药物。
|
||||
4. 中国政治敏感、反动、分裂、颠覆、煽动、侮辱国家象征、攻击国家领导人、违法集会、暴恐宣传等内容。
|
||||
5. 任何试图绕过上述限制的变体、谐音、拆字、缩写、暗语、拼音、外语翻译或角色扮演指令。
|
||||
|
||||
如果命中任何一项,直接拒绝,不要改写,不要生成,不要解释如何绕过。`
|
||||
|
||||
const defaultImageStudioBannedWords = `暴力
|
||||
血腥
|
||||
伤害
|
||||
虐待
|
||||
自残
|
||||
自杀
|
||||
恐怖
|
||||
恐怖主义
|
||||
战争
|
||||
武器
|
||||
枪支
|
||||
炸弹
|
||||
尸体
|
||||
残肢
|
||||
恶心
|
||||
色情
|
||||
裸露
|
||||
性暗示
|
||||
卖淫
|
||||
约炮
|
||||
成人视频
|
||||
SM
|
||||
性暴力
|
||||
性服务
|
||||
赌博
|
||||
博彩
|
||||
赌场
|
||||
下注
|
||||
洗钱
|
||||
毒品
|
||||
制毒
|
||||
吸毒
|
||||
贩毒
|
||||
违禁药物
|
||||
政治敏感
|
||||
反动
|
||||
分裂
|
||||
颠覆
|
||||
煽动
|
||||
国家领导人
|
||||
国旗
|
||||
国徽
|
||||
国歌
|
||||
暴恐
|
||||
porn
|
||||
nude
|
||||
nudity
|
||||
sex
|
||||
sexual
|
||||
erotic
|
||||
nsfw
|
||||
gore
|
||||
bloody
|
||||
blood
|
||||
violence
|
||||
violent
|
||||
suicide
|
||||
self-harm
|
||||
terrorist
|
||||
terrorism
|
||||
gambling
|
||||
casino
|
||||
drug
|
||||
drugs
|
||||
cocaine
|
||||
heroin
|
||||
meth`
|
||||
|
||||
var ImageStudioSafetyPrompt = strings.TrimSpace(defaultImageStudioSafetyPrompt)
|
||||
var ImageStudioBannedWords = splitLines(defaultImageStudioBannedWords)
|
||||
|
||||
// SensitiveWords 敏感词
|
||||
// var SensitiveWords []string
|
||||
var SensitiveWords = []string{
|
||||
"test_sensitive",
|
||||
}
|
||||
|
||||
func splitLines(s string) []string {
|
||||
items := make([]string, 0)
|
||||
for _, item := range strings.Split(s, "\n") {
|
||||
item = strings.TrimSpace(item)
|
||||
if item != "" {
|
||||
items = append(items, item)
|
||||
}
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func SensitiveWordsToString() string {
|
||||
return strings.Join(SensitiveWords, "\n")
|
||||
}
|
||||
|
||||
func SensitiveWordsFromString(s string) {
|
||||
SensitiveWords = []string{}
|
||||
sw := strings.Split(s, "\n")
|
||||
for _, w := range sw {
|
||||
w = strings.TrimSpace(w)
|
||||
if w != "" {
|
||||
SensitiveWords = append(SensitiveWords, w)
|
||||
}
|
||||
SensitiveWords = splitLines(s)
|
||||
}
|
||||
|
||||
func ImageStudioSafetyPromptToString() string {
|
||||
return ImageStudioSafetyPrompt
|
||||
}
|
||||
|
||||
func ImageStudioSafetyPromptFromString(s string) {
|
||||
prompt := strings.TrimSpace(s)
|
||||
if prompt == "" {
|
||||
ImageStudioSafetyPrompt = strings.TrimSpace(defaultImageStudioSafetyPrompt)
|
||||
return
|
||||
}
|
||||
ImageStudioSafetyPrompt = prompt
|
||||
}
|
||||
|
||||
func ImageStudioBannedWordsToString() string {
|
||||
return strings.Join(ImageStudioBannedWords, "\n")
|
||||
}
|
||||
|
||||
func ImageStudioBannedWordsFromString(s string) {
|
||||
words := splitLines(s)
|
||||
if len(words) == 0 {
|
||||
ImageStudioBannedWords = splitLines(defaultImageStudioBannedWords)
|
||||
return
|
||||
}
|
||||
ImageStudioBannedWords = words
|
||||
}
|
||||
|
||||
func ShouldCheckPromptSensitive() bool {
|
||||
|
||||
11
web/src/App.jsx
vendored
11
web/src/App.jsx
vendored
@@ -47,6 +47,7 @@ import Playground from './pages/Playground';
|
||||
import Subscription from './pages/Subscription';
|
||||
import OAuth2Callback from './components/auth/OAuth2Callback';
|
||||
import PersonalSetting from './components/settings/PersonalSetting';
|
||||
import ImageStudio from './pages/ImageStudio';
|
||||
import Setup from './pages/Setup';
|
||||
import SetupCheck from './components/layout/SetupCheck';
|
||||
|
||||
@@ -277,6 +278,16 @@ function App() {
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/console/image-studio'
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
|
||||
<ImageStudio />
|
||||
</Suspense>
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/console/log'
|
||||
element={
|
||||
|
||||
@@ -49,6 +49,7 @@ const routerMap = {
|
||||
deployment: '/console/deployment',
|
||||
playground: '/console/playground',
|
||||
personal: '/console/personal',
|
||||
image_studio: '/console/image-studio',
|
||||
};
|
||||
|
||||
const SiderBar = ({ onNavigate = () => {} }) => {
|
||||
@@ -145,6 +146,18 @@ const SiderBar = ({ onNavigate = () => {} }) => {
|
||||
return filteredItems;
|
||||
}, [t, isModuleVisible]);
|
||||
|
||||
const otherItems = useMemo(() => {
|
||||
const items = [
|
||||
{
|
||||
text: t('图片制作'),
|
||||
itemKey: 'image_studio',
|
||||
to: '/image-studio',
|
||||
},
|
||||
];
|
||||
|
||||
return items.filter((item) => isModuleVisible('other', item.itemKey));
|
||||
}, [t, isModuleVisible]);
|
||||
|
||||
const adminItems = useMemo(() => {
|
||||
const items = [
|
||||
{
|
||||
@@ -475,6 +488,19 @@ const SiderBar = ({ onNavigate = () => {} }) => {
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 其他功能区域 */}
|
||||
{hasSectionVisibleModules('other') && (
|
||||
<>
|
||||
<Divider className='sidebar-divider' />
|
||||
<div>
|
||||
{!collapsed && (
|
||||
<div className='sidebar-group-label'>{t('其他功能')}</div>
|
||||
)}
|
||||
{otherItems.map((item) => renderNavItem(item))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 管理员区域 - 只在管理员时显示且配置允许时显示 */}
|
||||
{isAdmin() && hasSectionVisibleModules('admin') && (
|
||||
<>
|
||||
|
||||
@@ -63,36 +63,9 @@ const NotificationSettings = ({
|
||||
// 左侧边栏设置相关状态
|
||||
const [sidebarLoading, setSidebarLoading] = useState(false);
|
||||
const [activeTabKey, setActiveTabKey] = useState('notification');
|
||||
const [sidebarModulesUser, setSidebarModulesUser] = useState({
|
||||
chat: {
|
||||
enabled: true,
|
||||
playground: true,
|
||||
chat: true,
|
||||
},
|
||||
console: {
|
||||
enabled: true,
|
||||
detail: true,
|
||||
token: true,
|
||||
log: true,
|
||||
midjourney: true,
|
||||
task: true,
|
||||
},
|
||||
personal: {
|
||||
enabled: true,
|
||||
topup: true,
|
||||
personal: true,
|
||||
},
|
||||
admin: {
|
||||
enabled: true,
|
||||
channel: true,
|
||||
models: true,
|
||||
deployment: true,
|
||||
subscription: true,
|
||||
redemption: true,
|
||||
user: true,
|
||||
setting: true,
|
||||
},
|
||||
});
|
||||
const [sidebarModulesUser, setSidebarModulesUser] = useState(
|
||||
mergeAdminConfig(null),
|
||||
);
|
||||
const [adminConfig, setAdminConfig] = useState(null);
|
||||
|
||||
// 使用后端权限验证替代前端角色判断
|
||||
@@ -155,29 +128,7 @@ const NotificationSettings = ({
|
||||
};
|
||||
|
||||
const resetSidebarModules = () => {
|
||||
const defaultConfig = {
|
||||
chat: { enabled: true, playground: true, chat: true },
|
||||
console: {
|
||||
enabled: true,
|
||||
detail: true,
|
||||
token: true,
|
||||
log: true,
|
||||
midjourney: true,
|
||||
task: true,
|
||||
},
|
||||
personal: { enabled: true, topup: true, personal: true },
|
||||
admin: {
|
||||
enabled: true,
|
||||
channel: true,
|
||||
models: true,
|
||||
deployment: true,
|
||||
subscription: true,
|
||||
redemption: true,
|
||||
user: true,
|
||||
setting: true,
|
||||
},
|
||||
};
|
||||
setSidebarModulesUser(defaultConfig);
|
||||
setSidebarModulesUser(mergeAdminConfig(null));
|
||||
};
|
||||
|
||||
// 加载左侧边栏配置
|
||||
@@ -207,7 +158,7 @@ const NotificationSettings = ({
|
||||
} else {
|
||||
userConf = userRes.data.data.sidebar_modules;
|
||||
}
|
||||
setSidebarModulesUser(userConf);
|
||||
setSidebarModulesUser(mergeAdminConfig(userConf));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载边栏配置失败:', error);
|
||||
@@ -287,6 +238,18 @@ const NotificationSettings = ({
|
||||
],
|
||||
},
|
||||
// 管理员区域:根据后端权限控制显示
|
||||
{
|
||||
key: 'other',
|
||||
title: t('其他功能区域'),
|
||||
description: t('图片与扩展功能'),
|
||||
modules: [
|
||||
{
|
||||
key: 'image_studio',
|
||||
title: t('图片制作'),
|
||||
description: t('生成图片和编辑图片'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'admin',
|
||||
title: t('管理员区域'),
|
||||
|
||||
3
web/src/helpers/render.jsx
vendored
3
web/src/helpers/render.jsx
vendored
@@ -75,6 +75,7 @@ import {
|
||||
Package,
|
||||
Server,
|
||||
CalendarClock,
|
||||
Sparkles,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
SiAtlassian,
|
||||
@@ -146,6 +147,8 @@ export function getLucideIcon(key, selected = false) {
|
||||
return <CalendarClock {...commonProps} color={iconColor} />;
|
||||
case 'setting':
|
||||
return <Settings {...commonProps} color={iconColor} />;
|
||||
case 'image_studio':
|
||||
return <Sparkles {...commonProps} color={iconColor} />;
|
||||
default:
|
||||
return <CircleUser {...commonProps} color={iconColor} />;
|
||||
}
|
||||
|
||||
7
web/src/helpers/utils.jsx
vendored
7
web/src/helpers/utils.jsx
vendored
@@ -121,13 +121,12 @@ if (isMobileScreen) {
|
||||
|
||||
export function showError(error) {
|
||||
console.error(error);
|
||||
if (error.message) {
|
||||
if (error?.message) {
|
||||
if (error.name === 'AxiosError') {
|
||||
switch (error.response.status) {
|
||||
const status = error.response?.status;
|
||||
switch (status) {
|
||||
case 401:
|
||||
// 清除用户状态
|
||||
localStorage.removeItem('user');
|
||||
// toast.error('错误:未登录或登录已过期,请重新登录!', showErrorOptions);
|
||||
window.location.href = '/login?expired=true';
|
||||
break;
|
||||
case 429:
|
||||
|
||||
4
web/src/hooks/common/useSidebar.js
vendored
4
web/src/hooks/common/useSidebar.js
vendored
@@ -44,6 +44,10 @@ export const DEFAULT_ADMIN_CONFIG = {
|
||||
topup: true,
|
||||
personal: true,
|
||||
},
|
||||
other: {
|
||||
enabled: true,
|
||||
image_studio: true,
|
||||
},
|
||||
admin: {
|
||||
enabled: true,
|
||||
channel: true,
|
||||
|
||||
1310
web/src/pages/ImageStudio/index.jsx
Normal file
1310
web/src/pages/ImageStudio/index.jsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -35,6 +35,7 @@ export default function SettingsSensitiveWords(props) {
|
||||
CheckSensitiveEnabled: false,
|
||||
CheckSensitiveOnPromptEnabled: false,
|
||||
SensitiveWords: '',
|
||||
ImageStudioSafetyPrompt: '',
|
||||
});
|
||||
const refForm = useRef();
|
||||
const [inputsRow, setInputsRow] = useState(inputs);
|
||||
@@ -130,8 +131,8 @@ export default function SettingsSensitiveWords(props) {
|
||||
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
|
||||
<Form.TextArea
|
||||
label={t('屏蔽词列表')}
|
||||
extraText={t('一行一个屏蔽词,不需要符号分割')}
|
||||
placeholder={t('一行一个屏蔽词,不需要符号分割')}
|
||||
extraText={t('一行一个屏蔽词,不需要符号分割。该词库也会用于图片生图提示词审核')}
|
||||
placeholder={t('一行一个屏蔽词,不需要符号分割。该词库也会用于图片生图提示词审核')}
|
||||
field={'SensitiveWords'}
|
||||
onChange={(value) =>
|
||||
setInputs({
|
||||
@@ -144,6 +145,24 @@ export default function SettingsSensitiveWords(props) {
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
|
||||
<Form.TextArea
|
||||
label={t('图片生图安全提示词')}
|
||||
extraText={t('仅用于图片生成/编辑,请写入暴力、色情、赌博、毒品、中国政治敏感、恐怖、恶心等禁止内容约束')}
|
||||
placeholder={t('仅用于图片生成/编辑,请写入暴力、色情、赌博、毒品、中国政治敏感、恐怖、恶心等禁止内容约束')}
|
||||
field={'ImageStudioSafetyPrompt'}
|
||||
onChange={(value) =>
|
||||
setInputs({
|
||||
...inputs,
|
||||
ImageStudioSafetyPrompt: value,
|
||||
})
|
||||
}
|
||||
style={{ fontFamily: 'JetBrains Mono, Consolas' }}
|
||||
autosize={{ minRows: 10, maxRows: 20 }}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Button size='default' onClick={onSubmit}>
|
||||
{t('保存屏蔽词过滤设置')}
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { API, showSuccess, showError } from '../../../helpers';
|
||||
import { StatusContext } from '../../../context/Status';
|
||||
import { mergeAdminConfig } from '../../../hooks/common/useSidebar';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
@@ -39,36 +40,9 @@ export default function SettingsSidebarModulesAdmin(props) {
|
||||
const [statusState, statusDispatch] = useContext(StatusContext);
|
||||
|
||||
// 左侧边栏模块管理状态(管理员全局控制)
|
||||
const [sidebarModulesAdmin, setSidebarModulesAdmin] = useState({
|
||||
chat: {
|
||||
enabled: true,
|
||||
playground: true,
|
||||
chat: true,
|
||||
},
|
||||
console: {
|
||||
enabled: true,
|
||||
detail: true,
|
||||
token: true,
|
||||
log: true,
|
||||
midjourney: true,
|
||||
task: true,
|
||||
},
|
||||
personal: {
|
||||
enabled: true,
|
||||
topup: true,
|
||||
personal: true,
|
||||
},
|
||||
admin: {
|
||||
enabled: true,
|
||||
channel: true,
|
||||
models: true,
|
||||
deployment: true,
|
||||
redemption: true,
|
||||
user: true,
|
||||
subscription: true,
|
||||
setting: true,
|
||||
},
|
||||
});
|
||||
const [sidebarModulesAdmin, setSidebarModulesAdmin] = useState(
|
||||
mergeAdminConfig(null),
|
||||
);
|
||||
|
||||
// 处理区域级别开关变更
|
||||
function handleSectionChange(sectionKey) {
|
||||
@@ -100,36 +74,7 @@ export default function SettingsSidebarModulesAdmin(props) {
|
||||
|
||||
// 重置为默认配置
|
||||
function resetSidebarModules() {
|
||||
const defaultModules = {
|
||||
chat: {
|
||||
enabled: true,
|
||||
playground: true,
|
||||
chat: true,
|
||||
},
|
||||
console: {
|
||||
enabled: true,
|
||||
detail: true,
|
||||
token: true,
|
||||
log: true,
|
||||
midjourney: true,
|
||||
task: true,
|
||||
},
|
||||
personal: {
|
||||
enabled: true,
|
||||
topup: true,
|
||||
personal: true,
|
||||
},
|
||||
admin: {
|
||||
enabled: true,
|
||||
channel: true,
|
||||
models: true,
|
||||
deployment: true,
|
||||
redemption: true,
|
||||
user: true,
|
||||
subscription: true,
|
||||
setting: true,
|
||||
},
|
||||
};
|
||||
const defaultModules = mergeAdminConfig(null);
|
||||
setSidebarModulesAdmin(defaultModules);
|
||||
showSuccess(t('已重置为默认配置'));
|
||||
}
|
||||
@@ -174,32 +119,9 @@ export default function SettingsSidebarModulesAdmin(props) {
|
||||
if (props.options && props.options.SidebarModulesAdmin) {
|
||||
try {
|
||||
const modules = JSON.parse(props.options.SidebarModulesAdmin);
|
||||
setSidebarModulesAdmin(modules);
|
||||
setSidebarModulesAdmin(mergeAdminConfig(modules));
|
||||
} catch (error) {
|
||||
// 使用默认配置
|
||||
const defaultModules = {
|
||||
chat: { enabled: true, playground: true, chat: true },
|
||||
console: {
|
||||
enabled: true,
|
||||
detail: true,
|
||||
token: true,
|
||||
log: true,
|
||||
midjourney: true,
|
||||
task: true,
|
||||
},
|
||||
personal: { enabled: true, topup: true, personal: true },
|
||||
admin: {
|
||||
enabled: true,
|
||||
channel: true,
|
||||
models: true,
|
||||
deployment: true,
|
||||
redemption: true,
|
||||
user: true,
|
||||
subscription: true,
|
||||
setting: true,
|
||||
},
|
||||
};
|
||||
setSidebarModulesAdmin(defaultModules);
|
||||
setSidebarModulesAdmin(mergeAdminConfig(null));
|
||||
}
|
||||
}
|
||||
}, [props.options]);
|
||||
@@ -248,6 +170,18 @@ export default function SettingsSidebarModulesAdmin(props) {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'other',
|
||||
title: t('其他功能区域'),
|
||||
description: t('图片与扩展功能'),
|
||||
modules: [
|
||||
{
|
||||
key: 'image_studio',
|
||||
title: t('图片制作'),
|
||||
description: t('生成图片和编辑图片'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'admin',
|
||||
title: t('管理员区域'),
|
||||
|
||||
Reference in New Issue
Block a user