官方 API 接入
围绕 Codex API 做服务,不把用户绕进一堆无关功能。
- 先选方案
- 再拿密钥
- 直接调用
From e09542d26b9874bdffd18575d83ea2ec20c21e89 Mon Sep 17 00:00:00 2001 From: dofastted <278058887+dofastted@users.noreply.github.com> Date: Thu, 18 Jun 2026 16:23:04 +0800 Subject: [PATCH 1/6] feat: route auto group by request path --- middleware/distributor.go | 15 ++++++++++++++ middleware/distributor_test.go | 36 ++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 middleware/distributor_test.go diff --git a/middleware/distributor.go b/middleware/distributor.go index 258aebb57037..c03edeb675d9 100644 --- a/middleware/distributor.go +++ b/middleware/distributor.go @@ -101,6 +101,11 @@ func Distribute() func(c *gin.Context) { } } + if routedGroup, ok := autoGroupForRequestPath(usingGroup, c.Request.URL.Path); ok { + usingGroup = routedGroup + common.SetContextKey(c, constant.ContextKeyUsingGroup, usingGroup) + } + if preferredChannelID, found := service.GetPreferredChannelByAffinity(c, modelRequest.Model, usingGroup); found { affinityUsable := false preferred, err := model.CacheGetChannel(preferredChannelID) @@ -167,6 +172,16 @@ func Distribute() func(c *gin.Context) { } } +func autoGroupForRequestPath(usingGroup string, requestPath string) (string, bool) { + if usingGroup != "auto" { + return usingGroup, false + } + if strings.Contains(requestPath, "/v1/chat/completions") { + return "codex-completions", true + } + return usingGroup, false +} + // getModelFromRequest 从请求中读取模型信息 // 根据 Content-Type 自动处理: // - application/json diff --git a/middleware/distributor_test.go b/middleware/distributor_test.go new file mode 100644 index 000000000000..4059cfbe67ea --- /dev/null +++ b/middleware/distributor_test.go @@ -0,0 +1,36 @@ +package middleware + +import "testing" + +func TestAutoGroupForRequestPathRoutesChatCompletions(t *testing.T) { + got, changed := autoGroupForRequestPath("auto", "/v1/chat/completions") + + if got != "codex-completions" { + t.Fatalf("expected codex-completions, got %q", got) + } + if !changed { + t.Fatal("expected chat completions path to change auto group") + } +} + +func TestAutoGroupForRequestPathKeepsResponsesAuto(t *testing.T) { + got, changed := autoGroupForRequestPath("auto", "/v1/responses") + + if got != "auto" { + t.Fatalf("expected auto, got %q", got) + } + if changed { + t.Fatal("expected responses path to keep auto group") + } +} + +func TestAutoGroupForRequestPathKeepsExplicitGroup(t *testing.T) { + got, changed := autoGroupForRequestPath("codex", "/v1/chat/completions") + + if got != "codex" { + t.Fatalf("expected explicit group, got %q", got) + } + if changed { + t.Fatal("expected explicit group to stay unchanged") + } +} From 003d7c0089a887d2e52247cfcf75c003bd579d54 Mon Sep 17 00:00:00 2001 From: dofastted <278058887+dofastted@users.noreply.github.com> Date: Thu, 18 Jun 2026 16:32:44 +0800 Subject: [PATCH 2/6] fix: address auto route review feedback --- middleware/distributor.go | 2 +- middleware/distributor_test.go | 70 +++++++++++++++++++++------------- 2 files changed, 44 insertions(+), 28 deletions(-) diff --git a/middleware/distributor.go b/middleware/distributor.go index c03edeb675d9..44a21e66f696 100644 --- a/middleware/distributor.go +++ b/middleware/distributor.go @@ -176,7 +176,7 @@ func autoGroupForRequestPath(usingGroup string, requestPath string) (string, boo if usingGroup != "auto" { return usingGroup, false } - if strings.Contains(requestPath, "/v1/chat/completions") { + if strings.HasPrefix(requestPath, "/v1/chat/completions") { return "codex-completions", true } return usingGroup, false diff --git a/middleware/distributor_test.go b/middleware/distributor_test.go index 4059cfbe67ea..fe3e20cc3bfc 100644 --- a/middleware/distributor_test.go +++ b/middleware/distributor_test.go @@ -1,36 +1,52 @@ package middleware -import "testing" +import ( + "testing" -func TestAutoGroupForRequestPathRoutesChatCompletions(t *testing.T) { - got, changed := autoGroupForRequestPath("auto", "/v1/chat/completions") + "github.com/stretchr/testify/require" +) - if got != "codex-completions" { - t.Fatalf("expected codex-completions, got %q", got) +func TestAutoGroupForRequestPath(t *testing.T) { + tests := []struct { + name string + usingGroup string + requestPath string + expectedGroup string + expectedChanged bool + }{ + { + name: "routes chat completions", + usingGroup: "auto", + requestPath: "/v1/chat/completions", + expectedGroup: "codex-completions", + expectedChanged: true, + }, + { + name: "keeps responses auto", + usingGroup: "auto", + requestPath: "/v1/responses", + expectedGroup: "auto", + }, + { + name: "keeps explicit group", + usingGroup: "codex", + requestPath: "/v1/chat/completions", + expectedGroup: "codex", + }, + { + name: "ignores embedded chat completions fragment", + usingGroup: "auto", + requestPath: "/proxy/v1/chat/completions", + expectedGroup: "auto", + }, } - if !changed { - t.Fatal("expected chat completions path to change auto group") - } -} - -func TestAutoGroupForRequestPathKeepsResponsesAuto(t *testing.T) { - got, changed := autoGroupForRequestPath("auto", "/v1/responses") - - if got != "auto" { - t.Fatalf("expected auto, got %q", got) - } - if changed { - t.Fatal("expected responses path to keep auto group") - } -} -func TestAutoGroupForRequestPathKeepsExplicitGroup(t *testing.T) { - got, changed := autoGroupForRequestPath("codex", "/v1/chat/completions") + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, changed := autoGroupForRequestPath(tt.usingGroup, tt.requestPath) - if got != "codex" { - t.Fatalf("expected explicit group, got %q", got) - } - if changed { - t.Fatal("expected explicit group to stay unchanged") + require.Equal(t, tt.expectedGroup, got) + require.Equal(t, tt.expectedChanged, changed) + }) } } From 87e52e831afeda07a320943a7cc03e26ca32ccba Mon Sep 17 00:00:00 2001 From: dofastted <278058887+dofastted@users.noreply.github.com> Date: Thu, 18 Jun 2026 16:41:39 +0800 Subject: [PATCH 3/6] fix: tighten auto route endpoint matching --- middleware/distributor.go | 2 +- middleware/distributor_test.go | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/middleware/distributor.go b/middleware/distributor.go index 44a21e66f696..73ce03e9041c 100644 --- a/middleware/distributor.go +++ b/middleware/distributor.go @@ -176,7 +176,7 @@ func autoGroupForRequestPath(usingGroup string, requestPath string) (string, boo if usingGroup != "auto" { return usingGroup, false } - if strings.HasPrefix(requestPath, "/v1/chat/completions") { + if requestPath == "/v1/chat/completions" || strings.HasPrefix(requestPath, "/v1/chat/completions/") { return "codex-completions", true } return usingGroup, false diff --git a/middleware/distributor_test.go b/middleware/distributor_test.go index fe3e20cc3bfc..6ef52e848492 100644 --- a/middleware/distributor_test.go +++ b/middleware/distributor_test.go @@ -3,7 +3,7 @@ package middleware import ( "testing" - "github.com/stretchr/testify/require" + "github.com/stretchr/testify/assert" ) func TestAutoGroupForRequestPath(t *testing.T) { @@ -39,14 +39,20 @@ func TestAutoGroupForRequestPath(t *testing.T) { requestPath: "/proxy/v1/chat/completions", expectedGroup: "auto", }, + { + name: "ignores similar chat completions prefix", + usingGroup: "auto", + requestPath: "/v1/chat/completions-extra", + expectedGroup: "auto", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, changed := autoGroupForRequestPath(tt.usingGroup, tt.requestPath) - require.Equal(t, tt.expectedGroup, got) - require.Equal(t, tt.expectedChanged, changed) + assert.Equal(t, tt.expectedGroup, got) + assert.Equal(t, tt.expectedChanged, changed) }) } } From 35e302a569c5b1bd6ad014f4c478630a6da0bdb3 Mon Sep 17 00:00:00 2001 From: dofastted <278058887+dofastted@users.noreply.github.com> Date: Thu, 18 Jun 2026 17:13:02 +0800 Subject: [PATCH 4/6] feat: add custom landing page and safety docs --- docs/input_safety_handoff.md | 549 +++++++++++++++++ docs/input_safety_rules.md | 1106 ++++++++++++++++++++++++++++++++++ main.go | 6 + router/web-router.go | 6 + web/custom/home.html | 204 +++++++ 5 files changed, 1871 insertions(+) create mode 100644 docs/input_safety_handoff.md create mode 100644 docs/input_safety_rules.md create mode 100644 web/custom/home.html diff --git a/docs/input_safety_handoff.md b/docs/input_safety_handoff.md new file mode 100644 index 000000000000..f6aac6877a04 --- /dev/null +++ b/docs/input_safety_handoff.md @@ -0,0 +1,549 @@ +# new-api 输入安全拦截配置与 Codex 交接说明 + +本文档给 VPS 上运行的 Codex 使用,用于在 new-api 中实现本地输入安全审查。目标是:不依赖 OpenAI moderation,直接在网关侧拦截用户请求中的 cyber abuse、NSFW、隐私窃取、诈骗等明显不合规输入。 + +相关规则详见:`docs/input_safety_rules.md`。 + +## 1. 是否可以由 VPS 上的 Codex 直接完成 + +可以。该功能是纯 Go 后端改动,不需要外部 SaaS 权限,也不需要 OpenAI 审查模型。 + +VPS Codex 需要具备: + +```text +仓库源码读写权限 +Go 工具链 +能运行目标 Go 测试 +能编辑环境变量或 docker-compose 配置 +``` + +不需要: + +```text +OpenAI API Key +数据库迁移 +前端改动 +第三方模型部署 +``` + +## 2. 目标行为 + +只审查用户可控输入: + +```text +/v1/chat/completions messages 中 role=user 的 content +/v1/completions prompt +/v1/responses input 中用户文本 +/v1/images/generations prompt +/v1/images/edits multipart form 的 prompt +/v1/messages Claude messages 中 role=user 的 content +/v1beta/models/* Gemini contents 中 role=user 或空 role 的 parts[].text +/v1/models/* Gemini contents 中 role=user 或空 role 的 parts[].text +``` + +不审查: + +```text +system prompt +developer prompt +assistant 历史输出 +tool 输出 +平台内部拼接内容 +模型响应内容 +``` + +命中高风险规则后,统一阻断请求并返回 OpenAI 兼容错误。 + +## 3. 对外返回格式 + +建议 HTTP 状态码:`400`。 + +```json +{ + "error": { + "message": "请求内容不符合输入安全规则,请修改 prompt 后重试。", + "type": "invalid_request_error", + "param": "messages[0].content", + "code": "input_safety_blocked" + } +} +``` + +说明: + +```text +message: 固定通用提示,不暴露关键词 +param: 被拦截的用户输入字段路径 +code: 固定 input_safety_blocked +type: 使用 invalid_request_error,保持 OpenAI 兼容风格 +``` + +不要对外返回: + +```text +rule_id +命中关键词 +score +正则表达式 +内部分类细节 +``` + +内部日志可以记录: + +```text +request_id +user_id +ip_hash +endpoint +model +param +category +rule_id +score +action +request_hash +``` + +## 4. 推荐配置项 + +使用环境变量,避免新增数据库迁移。 + +```text +INPUT_REVIEW_ENABLED=false +INPUT_REVIEW_MODE=log +INPUT_REVIEW_BLOCK_SCORE=40 +INPUT_REVIEW_REVIEW_SCORE=20 +INPUT_REVIEW_MAX_CHARS=8000 +INPUT_REVIEW_RETURN_MESSAGE=请求内容不符合输入安全规则,请修改 prompt 后重试。 +``` + +配置含义: + +| 变量 | 默认值 | 说明 | +| --- | --- | --- | +| `INPUT_REVIEW_ENABLED` | `false` | 是否启用输入安全审查 | +| `INPUT_REVIEW_MODE` | `log` | `log` 只记录不阻断;`block` 阻断高风险请求 | +| `INPUT_REVIEW_BLOCK_SCORE` | `40` | 达到该分数阻断 | +| `INPUT_REVIEW_REVIEW_SCORE` | `20` | 达到该分数记录为中风险 | +| `INPUT_REVIEW_MAX_CHARS` | `8000` | 单段输入最多审查字符数,超出按高风险处理或截断后审查 | +| `INPUT_REVIEW_RETURN_MESSAGE` | 中文默认提示 | 对外返回 message | + +上线建议: + +```text +第一阶段:INPUT_REVIEW_ENABLED=true, INPUT_REVIEW_MODE=log +第二阶段:观察误杀后切 INPUT_REVIEW_MODE=block +``` + +## 5. 推荐代码结构 + +新增文件: + +```text +middleware/input_safety.go +middleware/input_safety_rules.go +middleware/input_safety_test.go +``` + +可选新增文件: + +```text +middleware/input_safety_extract.go +``` + +不要新增前端文件。 +不要新增数据库表。 +不要修改规则文档中的项目保护信息。 + +## 6. 接入点 + +当前 relay 路由入口:`router/relay-router.go`。 + +现有链路: + +```go +relayV1Router := router.Group("/v1") +relayV1Router.Use(middleware.RouteTag("relay")) +relayV1Router.Use(middleware.SystemPerformanceCheck()) +relayV1Router.Use(middleware.TokenAuth()) +relayV1Router.Use(middleware.ModelRequestRateLimit()) + +httpRouter := relayV1Router.Group("") +httpRouter.Use(middleware.Distribute()) +``` + +建议把输入审查放在 `Distribute()` 之前: + +```go +httpRouter := relayV1Router.Group("") +httpRouter.Use(middleware.InputSafetyReview()) +httpRouter.Use(middleware.Distribute()) +``` + +原因: + +```text +先拦截,再分发渠道,避免为违规请求做渠道选择和上游准备 +TokenAuth 已完成,可记录 user_id +ModelRequestRateLimit 已完成,可保留现有限流语义 +common.GetRequestBody / common.UnmarshalBodyReusable 会缓存并复位请求体,后续 Distribute 和 Relay 仍可读取 +``` + +注意:`/v1/realtime` 是 WebSocket,不在本次范围。 + +## 7. 请求体读取要求 + +必须复用项目现有工具: + +```go +common.UnmarshalBodyReusable(c, &request) +common.ParseMultipartFormReusable(c) +``` + +不要直接使用: + +```go +encoding/json.Unmarshal +json.NewDecoder +io.ReadAll(c.Request.Body) 后不复位 +``` + +项目规则要求 JSON marshal/unmarshal 使用 `common/json.go` 包装函数。 + +## 8. 提取策略 + +### 8.1 OpenAI Chat Completions + +最小结构: + +```go +type inputSafetyOpenAIRequest struct { + Messages []struct { + Role string `json:"role"` + Content any `json:"content"` + } `json:"messages"` + Prompt any `json:"prompt"` + Input any `json:"input"` +} +``` + +提取: + +```text +messages[i].role == "user" -> content +prompt -> prompt +input -> input +``` + +Content 可能是: + +```text +string +[]object,其中 type == "text" 的 text 字段 +``` + +### 8.2 Responses API + +`input` 可能是: + +```text +string +array,包含 role/content +array,包含 type=input_text 的 text +``` + +只提取用户输入文本。 + +### 8.3 Images + +JSON: + +```text +prompt +``` + +multipart: + +```text +form.Value["prompt"] +``` + +### 8.4 Claude + +提取: + +```text +messages[i].role == "user" 的 content +``` + +content 可能是 string 或 blocks。只取 text block。 + +### 8.5 Gemini + +提取: + +```text +contents[i].role == "user" 或 role == "" 的 parts[j].text +``` + +不提取: + +```text +systemInstruction +functionCall +functionResponse +inlineData +``` + +## 9. 规则引擎要求 + +第一版用内置规则,不读取外部文件,降低部署复杂度。 + +建议结构: + +```go +type inputSafetyRule struct { + ID string + Category string + Score int + Any []string + All [][]string +} + +type inputSafetyFinding struct { + Param string + Category string + RuleID string + Score int +} +``` + +匹配方式: + +```text +Any: 任一关键词命中即加分 +All: 每个组合组都至少命中一个词才加分 +``` + +例如: + +```text +[窃取|盗取|steal|dump] + [cookie|token|密码|凭据] +``` + +强制 block 规则: + +```text +NSFW_SEXUAL_MINORS_001 +NSFW_NON_CONSENSUAL_001 +CYBER_CREDENTIAL_THEFT_001 +CYBER_MALWARE_001 +CYBER_EVASION_001 +CYBER_PHISHING_001 +CYBER_PAYMENT_FRAUD_001 +``` + +规则来源:`docs/input_safety_rules.md`。 + +## 10. 文本归一化 + +实现函数: + +```go +func normalizeInputSafetyText(s string) string +``` + +至少处理: + +```text +strings.ToLower +strings.TrimSpace +连续空白合并 +移除零宽字符:\u200b、\u200c、\u200d、\ufeff +全角 ASCII 转半角 +URL QueryUnescape 一次,失败则保留原文 +``` + +不要做昂贵或不确定的深度解码。 +不要递归 base64 解码。 + +## 11. 错误构造 + +项目已有: + +```go +types.OpenAIError +``` + +字段: + +```go +type OpenAIError struct { + Message string `json:"message"` + Type string `json:"type"` + Param string `json:"param"` + Code any `json:"code"` +} +``` + +中间件里可直接返回: + +```go +c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{ + "error": types.OpenAIError{ + Message: inputSafetyReturnMessage(), + Type: "invalid_request_error", + Param: finding.Param, + Code: "input_safety_blocked", + }, +}) +``` + +如果后续希望纳入 `types.ErrorCode`,可新增: + +```go +ErrorCodeInputSafetyBlocked ErrorCode = "input_safety_blocked" +``` + +但第一版中间件直接返回即可,改动更小。 + +## 12. 日志要求 + +使用项目 logger。日志不要包含完整原文。 + +建议记录: + +```text +input safety blocked param=messages[0].content category=cyber_abuse rule=CYBER_MALWARE_001 score=100 path=/v1/chat/completions +``` + +不要记录: + +```text +完整 prompt +完整 token +完整 cookie +完整 URL query 中的密钥 +``` + +如需要 request_hash,使用 SHA-256 对原始片段求 hash。 + +## 13. 测试要求 + +新增 `middleware/input_safety_test.go`。 + +必须覆盖: + +1. `INPUT_REVIEW_ENABLED=false` 时放行。 +2. `INPUT_REVIEW_MODE=log` 命中规则也放行。 +3. Chat Completions 中 `role=user` 命中 malware 组合时 400。 +4. Chat Completions 中 `system` 命中但 user 正常时放行。 +5. `prompt` 命中 NSFW 图片生成规则时 400。 +6. Claude user message 命中凭据窃取时 400。 +7. Gemini user part 命中钓鱼规则时 400。 +8. 返回体包含: + - `error.message` + - `error.type == "invalid_request_error"` + - `error.param` + - `error.code == "input_safety_blocked"` +9. 返回体不包含: + - `rule_id` + - `score` + - 具体关键词 + +测试只跑新增或相关测试即可。 + +## 14. 验证命令 + +在仓库根目录执行: + +```bash +go test ./middleware +``` + +如果改动触及 `router/relay-router.go`,再执行: + +```bash +go test ./router ./middleware +``` + +最终建议执行: + +```bash +go test ./relay/helper ./middleware +``` + +不要跑前端构建;本任务不涉及前端。 + +## 15. VPS 环境变量示例 + +Docker Compose 可添加: + +```yaml +environment: + - INPUT_REVIEW_ENABLED=true + - INPUT_REVIEW_MODE=block + - INPUT_REVIEW_BLOCK_SCORE=40 + - INPUT_REVIEW_REVIEW_SCORE=20 + - INPUT_REVIEW_MAX_CHARS=8000 + - INPUT_REVIEW_RETURN_MESSAGE=请求内容不符合输入安全规则,请修改 prompt 后重试。 +``` + +灰度期建议: + +```yaml +environment: + - INPUT_REVIEW_ENABLED=true + - INPUT_REVIEW_MODE=log +``` + +## 16. 给 VPS Codex 的执行提示词 + +可直接复制给服务器上的 Codex: + +```text +你在 new-api 仓库中工作。请实现本地输入安全审查,不依赖 OpenAI moderation。 + +必须先阅读: +- AGENTS.md +- docs/input_safety_rules.md +- docs/input_safety_handoff.md + +目标: +- 只审查用户输入字段:chat messages role=user content、prompt、responses input、image prompt、Claude role=user content、Gemini user contents parts text。 +- 不审查 system/developer/assistant/tool/model output。 +- 命中高风险 cyber abuse、NSFW、privacy、fraud 规则时,在 relay 前阻断。 +- 返回 OpenAI 兼容错误:type=invalid_request_error, code=input_safety_blocked, param=被拦截字段路径, message=请求内容不符合输入安全规则,请修改 prompt 后重试。 +- 不向用户返回 rule_id、score、关键词。 + +实现要求: +- 新增 middleware/input_safety.go、middleware/input_safety_rules.go、middleware/input_safety_test.go。 +- 在 router/relay-router.go 中把 middleware.InputSafetyReview() 加到 httpRouter 的 Distribute() 之前。 +- 使用 common.UnmarshalBodyReusable / common.ParseMultipartFormReusable 读取请求体,不能破坏后续读取。 +- JSON 解析必须使用 common 包装函数,不要直接调用 encoding/json 的 Marshal/Unmarshal。 +- 配置使用环境变量:INPUT_REVIEW_ENABLED、INPUT_REVIEW_MODE、INPUT_REVIEW_BLOCK_SCORE、INPUT_REVIEW_REVIEW_SCORE、INPUT_REVIEW_MAX_CHARS、INPUT_REVIEW_RETURN_MESSAGE。 +- 默认 INPUT_REVIEW_ENABLED=false,避免未配置时改变现有行为。 +- 日志不记录完整 prompt,只记录 param、category、rule_id、score、path。 + +测试: +- 添加 middleware/input_safety_test.go。 +- 覆盖 disabled 放行、log 模式放行、block 模式拦截、system 命中不拦截、OpenAI/Claude/Gemini/Image 提取、错误返回不泄露规则细节。 +- 运行 go test ./middleware。 +- 如果修改 router,运行 go test ./router ./middleware。 + +不要修改前端。不要新增数据库迁移。不要提交 git,除非用户另行要求。 +``` + +## 17. 完成标准 + +Codex 完成后应提供: + +```text +修改文件列表 +新增配置项 +命中的测试用例 +测试命令和结果 +是否需要重启服务 +``` + +服务端启用时,只需重启 new-api 进程或容器,使环境变量生效。 diff --git a/docs/input_safety_rules.md b/docs/input_safety_rules.md new file mode 100644 index 000000000000..4750980b1d78 --- /dev/null +++ b/docs/input_safety_rules.md @@ -0,0 +1,1106 @@ +# 输入安全拦截规则建议 + +本文档用于在网关侧部署本地输入安全规则。目标是只审查用户提交的 `input` / `prompt` / `user message` 内容,拦截明显不合规的 cyber abuse、NSFW、隐私窃取和诈骗类请求。 + +> 说明:本规则是硬限制与关键词组合方案,不等同于模型审查或法律合规结论。上线前建议先使用 `log` 模式观察误杀,再切换高置信规则为 `block`。 + +## 1. 拦截返回样式 + +符合项目现有 OpenAI 兼容错误格式。建议 HTTP 状态码使用 `400`。 + +```json +{ + "error": { + "message": "请求内容不符合输入安全规则,请修改 prompt 后重试。", + "type": "invalid_request_error", + "param": "input", + "code": "input_safety_blocked" + } +} +``` + +英文部署可使用: + +```json +{ + "error": { + "message": "Your request was blocked by the input safety policy. Please revise your prompt and try again.", + "type": "invalid_request_error", + "param": "input", + "code": "input_safety_blocked" + } +} +``` + +对外不要返回命中的规则 ID、关键词或分类,避免用户按提示绕过规则。 + +## 2. 审查范围 + +只审查用户可控输入。 + +| 接口类型 | 需要审查 | 不审查 | +| --- | --- | --- | +| Chat Completions | `messages[].role == "user"` 的 `content` | `system`、`developer`、`assistant`、`tool` | +| Responses | `input` 中用户文本;`role == "user"` 的内容 | 系统指令、平台注入内容、模型输出 | +| Completions | `prompt` | 内部拼接模板 | +| Images | `prompt` | 生成结果 | +| Claude | `role == "user"` 的文本内容 | `system`、assistant 历史 | +| Gemini | 用户 role 的文本内容 | system instruction、model 历史 | + +## 3. 预处理规则 + +匹配前建议执行: + +1. 转小写。 +2. 全角转半角。 +3. 合并连续空白字符。 +4. URL decode 一次。 +5. 移除零宽字符。 +6. 仅保存原文 hash,不默认保存原文。 +7. 超长 base64 或不可读大块文本直接按可疑输入处理。 + +建议限制: + +```text +INPUT_REVIEW_MAX_CHARS=8000 +INPUT_REVIEW_BLOCK_SCORE=40 +INPUT_REVIEW_REVIEW_SCORE=20 +INPUT_REVIEW_MODE=log|block +``` + +## 4. 动作分级 + +| 分数 | 动作 | 说明 | +| --- | --- | --- | +| `>= 100` | `block` | 强违规,例如未成年人性内容、凭据窃取、恶意软件生成 | +| `>= 40` | `block` | 高置信组合命中 | +| `>= 20` | `review` / `log` | 中风险或上下文不足 | +| `< 20` | `allow` | 放行 | + +如果没有人工复核流程,建议只对 `>= 40` 的规则启用拦截,其余先记录。 + +## 5. Cyber Abuse 规则 + +### 5.1 凭据、令牌、会话窃取 + +- `rule_id`: `CYBER_CREDENTIAL_THEFT_001` +- `category`: `cyber_abuse.credential_theft` +- `score`: `100` +- `action`: `block` + +命中逻辑:`动词组 A` + `目标组 B`。 + +英文动词组 A: + +```text +steal +extract +dump +exfiltrate +grab +harvest +scrape +leak +bypass login +session hijack +account takeover +``` + +英文目标组 B: + +```text +password +credential +cookie +session +token +api key +secret key +ssh key +private key +jwt +oauth token +refresh token +access token +2fa code +otp +``` + +中文动词组 A: + +```text +窃取 +盗取 +偷取 +抓取 +导出 +提取 +泄露 +批量获取 +劫持 +接管账号 +绕过登录 +``` + +中文目标组 B: + +```text +密码 +凭据 +cookie +会话 +token +令牌 +api key +密钥 +私钥 +ssh key +jwt +oauth +刷新令牌 +访问令牌 +验证码 +动态码 +二次验证 +``` + +示例命中: + +```text +窃取 cookie +抓取用户 token +write a script to dump saved passwords +extract oauth token from browser +``` + +允许例外: + +```text +如何保护 cookie +token 泄露后如何轮换 +detect credential leakage in logs +``` + +### 5.2 恶意软件生成 + +- `rule_id`: `CYBER_MALWARE_001` +- `category`: `cyber_abuse.malware` +- `score`: `100` +- `action`: `block` + +命中逻辑:`生成动词` + `恶意软件词`。 + +英文生成动词: + +```text +write +create +build +generate +make +develop +implement +code +``` + +英文恶意软件词: + +```text +malware +ransomware +keylogger +trojan +backdoor +rootkit +worm +botnet +infostealer +stealer +cryptominer +reverse shell +persistence mechanism +process injection +dll injection +credential stealer +``` + +中文生成动词: + +```text +写 +生成 +制作 +开发 +实现 +编写 +构建 +``` + +中文恶意软件词: + +```text +恶意软件 +勒索软件 +键盘记录器 +木马 +后门 +rootkit +蠕虫 +僵尸网络 +盗号木马 +信息窃取器 +挖矿木马 +反弹 shell +持久化 +进程注入 +dll 注入 +凭据窃取 +``` + +允许例外: + +```text +恶意软件检测规则 +勒索软件应急响应 +keylogger 防护方案 +reverse shell 是什么以及如何检测 +``` + +### 5.3 规避检测、免杀、绕过风控 + +- `rule_id`: `CYBER_EVASION_001` +- `category`: `cyber_abuse.evasion` +- `score`: `100` +- `action`: `block` + +命中逻辑:`规避词 A` + `检测/风控目标 B`。 + +英文规避词 A: + +```text +bypass +evade +avoid detection +undetectable +obfuscate +hide from +disable +turn off +circumvent +``` + +英文目标 B: + +```text +antivirus +edr +xdr +firewall +waf +ids +ips +sandbox +defender +security tool +detection +rate limit +captcha +risk control +``` + +中文规避词 A: + +```text +绕过 +规避 +免杀 +隐藏 +躲避 +逃避检测 +关闭 +禁用 +对抗 +绕开 +``` + +中文目标 B: + +```text +杀毒 +防病毒 +edr +xdr +防火墙 +waf +入侵检测 +沙箱 +defender +安全软件 +检测 +风控 +验证码 +限流 +``` + +允许例外: + +```text +如何加强验证码防护 +WAF 绕过检测日志分析 +EDR 告警排查 +``` + +### 5.4 真实目标攻击 + +- `rule_id`: `CYBER_REAL_TARGET_ATTACK_001` +- `category`: `cyber_abuse.real_target_attack` +- `score`: `80` +- `action`: `block` + +命中逻辑:`攻击动词` + `真实目标特征`。 + +英文攻击动词: + +```text +exploit +attack +hack +breach +penetrate +gain access +unauthorized access +take over +scan +enumerate +bruteforce +brute force +``` + +中文攻击动词: + +```text +攻击 +入侵 +黑掉 +拿下 +渗透 +未授权访问 +接管 +扫描 +爆破 +撞库 +枚举 +``` + +真实目标特征: + +```regex +\b(?:\d{1,3}\.){3}\d{1,3}\b +https?://[^\s]+\b +\b[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\b +``` + +中文目标词: + +```text +真实网站 +公网 ip +公司网络 +生产服务器 +目标域名 +受害者 +后台 +管理面板 +数据库 +``` + +英文目标词: + +```text +real website +public ip +company network +production server +target domain +victim +admin panel +database +``` + +允许例外: + +```text +授权渗透测试报告模板 +CTF 靶场题目分析 +本地靶机漏洞复现 +``` + +建议白名单词: + +```text +ctf +靶场 +lab +localhost +127.0.0.1 +本地环境 +授权 +defensive +防御 +修复 +``` + +### 5.5 钓鱼、仿冒登录、诈骗脚本 + +- `rule_id`: `CYBER_PHISHING_001` +- `category`: `cyber_abuse.phishing` +- `score`: `100` +- `action`: `block` + +英文: + +```text +phishing page +fake login +credential harvesting +clone login page +spoof email +scam script +bank login clone +paypal login clone +microsoft login clone +google login clone +``` + +中文: + +```text +钓鱼页面 +仿冒登录 +假登录页 +克隆登录页 +盗号页面 +收集账号密码 +诈骗脚本 +仿冒银行 +仿冒支付 +仿冒微软登录 +仿冒谷歌登录 +``` + +组合增强: + +```text +create/build/generate/write/制作/生成/编写 + 上述词 => block +``` + +### 5.6 DDoS 和流量攻击 + +- `rule_id`: `CYBER_DDOS_001` +- `category`: `cyber_abuse.ddos` +- `score`: `100` +- `action`: `block` + +英文: + +```text +ddos +dos attack +booter +stresser +udp flood +tcp flood +syn flood +http flood +slowloris +amplification attack +stress a website +take down website +``` + +中文: + +```text +ddos +拒绝服务 +打流量 +流量攻击 +压垮网站 +打死网站 +洪水攻击 +udp flood +tcp flood +syn flood +http flood +慢速攻击 +放大攻击 +``` + +允许例外: + +```text +DDoS 防护 +限流设计 +高并发压测自己的服务 +流量攻击日志分析 +``` + +### 5.7 批量注册、垃圾信息、平台滥用 + +- `rule_id`: `CYBER_PLATFORM_ABUSE_001` +- `category`: `cyber_abuse.platform_abuse` +- `score`: `60` +- `action`: `block` + +英文组合: + +```text +mass signup + bypass captcha +bulk account creation + fake identity +spam bot + send messages +scrape users + send dm +auto register + proxy +``` + +中文组合: + +```text +批量注册 + 绕过验证码 +批量养号 + 代理 +自动注册 + 风控 +群发私信 + 爬取用户 +垃圾消息 + 机器人 +``` + +### 5.8 支付欺诈、卡密、盗刷 + +- `rule_id`: `CYBER_PAYMENT_FRAUD_001` +- `category`: `fraud.payment` +- `score`: `100` +- `action`: `block` + +英文: + +```text +carding +stolen credit card +credit card dump +bin attack +chargeback fraud +payment bypass +fake payment proof +``` + +中文: + +```text +盗刷 +黑卡 +信用卡料 +卡料 +跑卡 +撞库支付 +绕过支付 +伪造付款截图 +拒付欺诈 +``` + +## 6. NSFW 规则 + +### 6.1 未成年人性内容 + +- `rule_id`: `NSFW_SEXUAL_MINORS_001` +- `category`: `nsfw.sexual_minors` +- `score`: `200` +- `action`: `block` + +命中逻辑:`未成年人词` + `性内容词`。 + +英文未成年人词: + +```text +minor +underage +child +children +kid +teen +schoolgirl +schoolboy +preteen +lolita +loli +shota +``` + +中文未成年人词: + +```text +未成年 +儿童 +小孩 +孩子 +幼女 +幼童 +萝莉 +正太 +学生妹 +小学生 +初中生 +未满18 +未满十八 +``` + +英文性内容词: + +```text +sex +sexual +nude +naked +porn +erotic +explicit +lewd +strip +seduce +incest +``` + +中文性内容词: + +```text +性 +色情 +裸露 +裸体 +成人内容 +露骨 +淫秽 +挑逗 +脱衣 +乱伦 +``` + +任意组合命中即拦截。 + +### 6.2 露骨色情生成 + +- `rule_id`: `NSFW_EXPLICIT_SEXUAL_001` +- `category`: `nsfw.explicit_sexual` +- `score`: `80` +- `action`: `block` + +英文: + +```text +explicit sex +pornographic +hardcore porn +graphic sexual +sexual act +oral sex +anal sex +masturbation +fetish porn +``` + +中文: + +```text +露骨色情 +色情描写 +成人影片 +成人视频 +性交 +口交 +肛交 +自慰 +性癖色情 +``` + +组合增强: + +```text +write/generate/create/生成/写/制作 + 上述词 => block +``` + +允许例外: + +```text +性教育 +医学解释 +性健康咨询 +法律合规讨论 +``` + +### 6.3 非自愿、性暴力、胁迫性内容 + +- `rule_id`: `NSFW_NON_CONSENSUAL_001` +- `category`: `nsfw.non_consensual` +- `score`: `200` +- `action`: `block` + +英文: + +```text +non-consensual +without consent +rape +sexual assault +forced sex +coerced +blackmail sex +revenge porn +hidden camera +upskirt +``` + +中文: + +```text +非自愿 +未经同意 +强奸 +性侵 +强迫发生关系 +胁迫 +偷拍视频 +偷拍裙底 +报复性色情 +裸照威胁 +``` + +### 6.4 裸露或色情图片生成提示词 + +- `rule_id`: `NSFW_IMAGE_PROMPT_001` +- `category`: `nsfw.image_prompt` +- `score`: `80` +- `action`: `block` + +命中逻辑:`图片生成词` + `裸露/色情词`。 + +英文图片生成词: + +```text +generate image +create image +image prompt +stable diffusion +midjourney +photorealistic +render +``` + +中文图片生成词: + +```text +生成图片 +画一张 +图片提示词 +绘图提示词 +文生图 +图生图 +写真 +渲染 +``` + +英文裸露/色情词: + +```text +nude +naked + topless +see-through +lingerie +porn +explicit +spread legs +``` + +中文裸露/色情词: + +```text +裸体 +裸露 +裸照 +上空 +透视 +内衣 +色情 +露骨 +大尺度 +``` + +允许例外: + +```text +医学解剖图 +艺术史分析 +服装设计说明 +``` + +## 7. 隐私与人肉搜索规则 + +### 7.1 个人隐私泄露 + +- `rule_id`: `PRIVACY_DANGEROUS_DISCLOSURE_001` +- `category`: `privacy.dangerous_disclosure` +- `score`: `80` +- `action`: `block` + +英文动词: + +```text +find address +track location +dox +lookup personal info +find phone number +find real identity +``` + +中文动词: + +```text +查地址 +定位某人 +人肉 +开盒 +查手机号 +查身份证 +查真实身份 +查家庭住址 +``` + +敏感字段: + +```text +身份证 +手机号 +家庭住址 +住址 +银行卡 +护照 +社保号 +phone number +home address +id number +passport +bank account +ssn +``` + +允许例外: + +```text +隐私保护建议 +个人信息脱敏 +合规数据处理 +``` + +## 8. 自残与暴力规则 + +### 8.1 自残方法指导 + +- `rule_id`: `SELF_HARM_INSTRUCTIONS_001` +- `category`: `self_harm.instructions` +- `score`: `100` +- `action`: `block` + +英文: + +```text +how to kill myself +suicide method +self harm instructions +painless suicide +best way to die +``` + +中文: + +```text +怎么自杀 +自杀方法 +自残教程 +无痛自杀 +怎么死不痛苦 +``` + +允许并应放行的求助类表达: + +```text +我想自杀怎么办 +如何帮助有自杀想法的人 +心理危机求助 +``` + +说明:求助类不要拦截为违规,应该交给上游模型提供安全支持或转人工。 + +### 8.2 暴力伤害指导 + +- `rule_id`: `VIOLENCE_INSTRUCTIONS_001` +- `category`: `violence.instructions` +- `score`: `80` +- `action`: `block` + +英文: + +```text +how to hurt someone +how to kill someone +make a weapon +hide a weapon +violent attack plan +``` + +中文: + +```text +如何伤害别人 +如何杀人 +制作武器 +藏匿武器 +暴力袭击计划 +``` + +## 9. 组合规则建议 + +### 9.1 强制 block 组合 + +```text +未成年人词 + 性内容词 => block +生成动词 + 恶意软件词 => block +规避词 + 安全检测目标 => block +窃取动词 + 凭据目标 => block +攻击动词 + 真实目标特征 => block +图片生成词 + 裸露/色情词 => block +非自愿词 + 性内容词 => block +``` + +### 9.2 降低误杀白名单 + +出现以下词时,可降低 20 分,但不得覆盖强制 block 规则: + +英文: + +```text +defense +defensive +protect +mitigate +patch +fix +detect +monitor +incident response +ctf +lab +localhost +training +education +medical +legal compliance +``` + +中文: + +```text +防御 +保护 +缓解 +修复 +补丁 +检测 +监控 +应急响应 +靶场 +本地环境 +授权 +培训 +教育 +医学 +合规 +法律讨论 +``` + +不得白名单覆盖: + +```text +sexual_minors +credential_theft +malware_generation +non_consensual_sexual +payment_fraud +``` + +## 10. 建议配置结构 + +```json +{ + "enabled": true, + "mode": "block", + "block_score": 40, + "review_score": 20, + "max_input_chars": 8000, + "return_message": "请求内容不符合输入安全规则,请修改 prompt 后重试。", + "rules": [ + { + "id": "CYBER_CREDENTIAL_THEFT_001", + "category": "cyber_abuse.credential_theft", + "score": 100, + "action": "block" + }, + { + "id": "NSFW_SEXUAL_MINORS_001", + "category": "nsfw.sexual_minors", + "score": 200, + "action": "block" + } + ] +} +``` + +## 11. 日志字段 + +建议记录: + +```text +request_id +user_id +ip_hash +endpoint +model +category +rule_id +score +action +request_hash +created_at +``` + +不建议默认记录: + +```text +完整原文 +完整图片 URL +完整 token +完整 cookie +完整密钥 +``` + +## 12. 上线顺序 + +1. `log` 模式上线全部规则。 +2. 观察 1 到 3 天误杀。 +3. 先启用强制 block: + - `NSFW_SEXUAL_MINORS_001` + - `NSFW_NON_CONSENSUAL_001` + - `CYBER_CREDENTIAL_THEFT_001` + - `CYBER_MALWARE_001` + - `CYBER_EVASION_001` + - `CYBER_PHISHING_001` + - `CYBER_PAYMENT_FRAUD_001` +4. 对真实目标攻击、DDoS、平台滥用启用 `review` 或较高阈值 block。 +5. 后续如可用,再接入模型审查作为二级判断。 diff --git a/main.go b/main.go index 3361b8ce9338..532da5f7c9ba 100644 --- a/main.go +++ b/main.go @@ -47,6 +47,9 @@ var classicBuildFS embed.FS //go:embed web/classic/dist/index.html var classicIndexPage []byte +//go:embed web/custom/home.html +var customHomePage []byte + func main() { startTime := time.Now() @@ -195,6 +198,7 @@ func main() { DefaultIndexPage: indexPage, ClassicBuildFS: classicBuildFS, ClassicIndexPage: classicIndexPage, + CustomHomePage: customHomePage, }) var port = os.Getenv("PORT") if port == "" { @@ -229,6 +233,7 @@ func InjectUmamiAnalytics() { placeholder := []byte("\n") indexPage = bytes.ReplaceAll(indexPage, placeholder, analyticsInject) classicIndexPage = bytes.ReplaceAll(classicIndexPage, placeholder, analyticsInject) + customHomePage = bytes.ReplaceAll(customHomePage, placeholder, analyticsInject) } func InjectGoogleAnalytics() { @@ -253,6 +258,7 @@ func InjectGoogleAnalytics() { placeholder := []byte("\n") indexPage = bytes.ReplaceAll(indexPage, placeholder, analyticsInject) classicIndexPage = bytes.ReplaceAll(classicIndexPage, placeholder, analyticsInject) + customHomePage = bytes.ReplaceAll(customHomePage, placeholder, analyticsInject) } func InitResources() error { diff --git a/router/web-router.go b/router/web-router.go index 0d475e90d54d..b1a593d9dcaf 100644 --- a/router/web-router.go +++ b/router/web-router.go @@ -19,6 +19,7 @@ type ThemeAssets struct { DefaultIndexPage []byte ClassicBuildFS embed.FS ClassicIndexPage []byte + CustomHomePage []byte } func SetWebRouter(router *gin.Engine, assets ThemeAssets) { @@ -29,6 +30,11 @@ func SetWebRouter(router *gin.Engine, assets ThemeAssets) { router.Use(gzip.Gzip(gzip.DefaultCompression)) router.Use(middleware.GlobalWebRateLimit()) router.Use(middleware.Cache()) + router.GET("/", func(c *gin.Context) { + c.Set(middleware.RouteTagKey, "web") + c.Header("Cache-Control", "no-cache") + c.Data(http.StatusOK, "text/html; charset=utf-8", assets.CustomHomePage) + }) router.Use(static.Serve("/", themeFS)) router.NoRoute(func(c *gin.Context) { c.Set(middleware.RouteTagKey, "web") diff --git a/web/custom/home.html b/web/custom/home.html new file mode 100644 index 000000000000..fb23e46c8bf3 --- /dev/null +++ b/web/custom/home.html @@ -0,0 +1,204 @@ + + +
+ + + +fkcodex 只把关键事讲清楚:怎么买、怎么调、用了多少。首页保持轻量,控制台继续由 new-api 原前端承接。
围绕 Codex API 做服务,不把用户绕进一堆无关功能。
小用量先试,稳定调用再升级,特殊请求量再单独谈。
密钥、请求、Token 和剩余额度都放在控制台里看。
“这个页面只负责讲清楚 API 服务价值;用户进入控制台后,仍然使用 new-api 原生的登录、定价、模型和密钥管理流程。”