fix: 非Claude Code客户端system prompt迁移至messages以绕过第三方应用检测

Anthropic近期引入基于system参数内容的第三方应用检测机制,原有的前置追加
Claude Code提示词策略无法通过检测(后续内容仍为非Claude Code格式触发429)。

新策略:对非Claude Code客户端的OAuth/SetupToken账号请求,将system字段
完整替换为Claude Code标识提示词,原始system内容作为user/assistant消息对
注入messages开头,模型仍接收完整指令。

仅影响/v1/messages路径,chat_completions和responses路径保持原有逻辑不变。
真正的Claude Code客户端请求完全不受影响(原样透传)。
This commit is contained in:
shaw
2026-04-07 17:06:47 +08:00
parent 9ab2fd7f9e
commit f3aa54b770
3 changed files with 219 additions and 3 deletions
@@ -278,3 +278,141 @@ func TestInjectClaudeCodePrompt(t *testing.T) {
})
}
}
func TestRewriteSystemForNonClaudeCode(t *testing.T) {
tests := []struct {
name string
body string
system any
wantSystemStr string // system 应为纯字符串
wantMessagesLen int // messages 数组长度
wantFirstMsgRole string // 第一条消息的 role
wantFirstMsgText string // 第一条消息的 content[0].text
wantAckMsgText string // 第二条消息的 content[0].text
}{
{
name: "nil system - no messages injected",
body: `{"model":"claude-3","messages":[{"role":"user","content":"hello"}]}`,
system: nil,
wantSystemStr: claudeCodeSystemPrompt,
wantMessagesLen: 1, // 原始 1 条消息,不注入
},
{
name: "empty string system - no messages injected",
body: `{"model":"claude-3","messages":[{"role":"user","content":"hello"}]}`,
system: "",
wantSystemStr: claudeCodeSystemPrompt,
wantMessagesLen: 1,
},
{
name: "custom string system - migrated to messages",
body: `{"model":"claude-3","messages":[{"role":"user","content":"hello"}]}`,
system: "You are a personal assistant running inside OpenClaw.",
wantSystemStr: claudeCodeSystemPrompt,
wantMessagesLen: 3, // instruction + ack + original
wantFirstMsgRole: "user",
wantFirstMsgText: "[System Instructions]\nYou are a personal assistant running inside OpenClaw.",
wantAckMsgText: "Understood. I will follow these instructions.",
},
{
name: "system equals Claude Code prompt - no messages injected",
body: `{"model":"claude-3","messages":[{"role":"user","content":"hello"}]}`,
system: claudeCodeSystemPrompt,
wantSystemStr: claudeCodeSystemPrompt,
wantMessagesLen: 1,
},
{
name: "array system with custom blocks - text joined and migrated",
body: `{"model":"claude-3","messages":[{"role":"user","content":"hello"}]}`,
system: []any{
map[string]any{"type": "text", "text": "First instruction"},
map[string]any{"type": "text", "text": "Second instruction"},
},
wantSystemStr: claudeCodeSystemPrompt,
wantMessagesLen: 3,
wantFirstMsgRole: "user",
wantFirstMsgText: "[System Instructions]\nFirst instruction\n\nSecond instruction",
wantAckMsgText: "Understood. I will follow these instructions.",
},
{
name: "empty array system - no messages injected",
body: `{"model":"claude-3","messages":[{"role":"user","content":"hello"}]}`,
system: []any{},
wantSystemStr: claudeCodeSystemPrompt,
wantMessagesLen: 1,
},
{
name: "json.RawMessage string system",
body: `{"model":"claude-3","system":"Custom prompt","messages":[{"role":"user","content":"hello"}]}`,
system: json.RawMessage(`"Custom prompt"`),
wantSystemStr: claudeCodeSystemPrompt,
wantMessagesLen: 3,
wantFirstMsgRole: "user",
wantFirstMsgText: "[System Instructions]\nCustom prompt",
wantAckMsgText: "Understood. I will follow these instructions.",
},
{
name: "json.RawMessage nil system",
body: `{"model":"claude-3","messages":[{"role":"user","content":"hello"}]}`,
system: json.RawMessage(nil),
wantSystemStr: claudeCodeSystemPrompt,
wantMessagesLen: 1,
},
{
name: "multiple original messages preserved",
body: `{"model":"claude-3","messages":[{"role":"user","content":"msg1"},{"role":"assistant","content":"resp1"},{"role":"user","content":"msg2"}]}`,
system: "Be helpful",
wantSystemStr: claudeCodeSystemPrompt,
wantMessagesLen: 5, // 2 injected + 3 original
wantFirstMsgRole: "user",
wantFirstMsgText: "[System Instructions]\nBe helpful",
wantAckMsgText: "Understood. I will follow these instructions.",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := rewriteSystemForNonClaudeCode([]byte(tt.body), tt.system)
var parsed map[string]any
err := json.Unmarshal(result, &parsed)
require.NoError(t, err)
// system 应为纯字符串
systemVal, ok := parsed["system"].(string)
require.True(t, ok, "system should be a string, got %T", parsed["system"])
require.Equal(t, tt.wantSystemStr, systemVal)
// 检查 messages
messages, ok := parsed["messages"].([]any)
require.True(t, ok, "messages should be an array")
require.Len(t, messages, tt.wantMessagesLen)
if tt.wantFirstMsgRole != "" && len(messages) >= 2 {
// 检查注入的 instruction 消息
firstMsg, ok := messages[0].(map[string]any)
require.True(t, ok)
require.Equal(t, tt.wantFirstMsgRole, firstMsg["role"])
firstContent, ok := firstMsg["content"].([]any)
require.True(t, ok)
require.Len(t, firstContent, 1)
firstBlock, ok := firstContent[0].(map[string]any)
require.True(t, ok)
require.Equal(t, tt.wantFirstMsgText, firstBlock["text"])
// 检查注入的 ack 消息
ackMsg, ok := messages[1].(map[string]any)
require.True(t, ok)
require.Equal(t, "assistant", ackMsg["role"])
ackContent, ok := ackMsg["content"].([]any)
require.True(t, ok)
require.Len(t, ackContent, 1)
ackBlock, ok := ackContent[0].(map[string]any)
require.True(t, ok)
require.Equal(t, tt.wantAckMsgText, ackBlock["text"])
}
})
}
}