fix: 修复4.20模型对话错误和注入提示词以支持openclaw调用工具

This commit is contained in:
XianYuDaXian committed 2026-03-18 09:20:28 +08:00
1 parent 8c1a1b1068
commit d35f71721c
5 files changed
+285 -37

No files matched your search

+23 -2
View File
@@ -29,6 +29,13 @@ from app.services.grok.utils.response import (
)
try:
with open("app_traffic.log", "w", encoding="utf-8") as f:
import datetime
f.write(f"=== app_traffic.log Cleared on Uvicorn Loaded [{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] ===\n\n")
except Exception:
pass
class MessageItem(BaseModel):
"""消息项"""
@@ -393,7 +400,7 @@ def validate_request(request: ChatCompletionRequest):
# 字符串内容
if isinstance(content, str):
if not content.strip():
if not content.strip() and msg.role != "assistant":
raise ValidationException(
message="Message content cannot be empty",
param=f"messages.{idx}.content",
@@ -514,7 +521,7 @@ def validate_request(request: ChatCompletionRequest):
"file.file_data",
f"messages.{idx}.content.{block_idx}.file.file_data",
)
elif content is None and msg.role == "tool":
elif content is None and (msg.role == "tool" or msg.tool_calls):
pass
elif not isinstance(content, list):
raise ValidationException(
@@ -1034,6 +1041,20 @@ async def chat_completions(request: ChatCompletionRequest, raw_request: Request)
except Exception as e:
return _chat_error_as_success_response(request.model, _video_error_message(e))
else:
try:
with open("app_traffic.log", "a", encoding="utf-8") as f:
import datetime
f.write(f"--- Chat Request [{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] ---\n")
dump = {
"model": request.model,
"messages": [m.model_dump() for m in request.messages],
"stream": request.stream,
"tools": request.tools if request.tools else None,
}
f.write(orjson.dumps(dump, option=orjson.OPT_INDENT_2).decode('utf-8') + "\n\n")
except Exception:
pass
result = await ChatService.completions(
model=request.model,
messages=[msg.model_dump() for msg in request.messages],
+33 -7
View File
@@ -1,4 +1,4 @@
"""
"""
Grok Chat 服务
"""
@@ -130,7 +130,14 @@ class MessageExtractor:
if isinstance(content, str):
if content.strip():
parts.append(content)
text_to_add = content
if role == "tool" and len(text_to_add) > 3000:
text_to_add = (
text_to_add[:1500]
+ "\n\n... [Output Truncated for Conciseness] ...\n\n"
+ text_to_add[-1000:]
)
parts.append(text_to_add)
elif isinstance(content, dict):
content = [content]
for item in content:
@@ -209,11 +216,11 @@ class MessageExtractor:
role_label = role
if role == "tool":
name = msg.get("name")
call_id = msg.get("tool_call_id")
if isinstance(name, str) and name.strip():
role_label = f"tool[{name.strip()}]"
if isinstance(call_id, str) and call_id.strip():
role_label = f"{role_label}#{call_id.strip()}"
role_label = f"--- Action Result: {name.strip()} ---"
full_text = "\n".join(parts)
if "exited with code" in full_text and "code 0" not in full_text:
role_label += " [FAILED]"
extracted.append({"role": role_label, "text": "\n".join(parts)})
# 找到最后一条 user 消息
@@ -239,7 +246,26 @@ class MessageExtractor:
if tools:
tool_prompt = build_tool_prompt(tools, tool_choice, parallel_tool_calls)
if tool_prompt:
combined = f"{tool_prompt}\n\n{combined}"
# 注入 Few-Shot 引导以避免模型惯性,强化 Tool Calling 回语。
few_shot = (
"\n\n--- CRITICAL: TOOL CALL REQUIREMENT ---\n"
"Example:\n"
"User: test.txt 里面有什么内容?\n"
"Assistant: <tool_call>{\"name\": \"read\", \"arguments\": {\"path\": \"test.txt\"}}</tool_call>\n"
"User: 【System Notification: Tool Execution Result - read】\nhello world\n"
"Assistant: test.txt 的内容是 hello world。\n"
"--- END EXAMPLE ---\n\n"
"Important Guidelines:\n"
"1. **Task Complete**: If the information you have (including tool execution results) is SUFFICIENT to answer the user, you MUST answer directly in human language. DO NOT call any tools.\n"
"2. **More Tools Needed**: If it is INDISPENSABLE to use a tool, you MUST output ONLY the raw `<tool_call>` block without conversational narrative filler."
)
combined = f"{tool_prompt}{few_shot}\n\n{combined}"
# 尾插强化 (Tail Enforcement)
combined += (
"\n\n--- Important Note ---\n"
"- Only respond using the valid structural `<tool_call>` JSON tag if you need data from available tools.\n"
"- Provide only one single execution block and wait for response without dialogue."
)
return combined, file_attachments, image_attachments
+12 -10
View File
@@ -34,9 +34,13 @@ def build_tool_prompt(
return ""
lines = [
"Important Rule: Focus strictly on answering using tool output data and JSON.",
"To satisfy the task, leverage available tools in sequence without adding conversational filler.",
"Do not formulate standard chatbot dialogue or greet.",
"",
"# Available Tools",
"",
"You have access to the following tools. To call a tool, output a <tool_call> block with a JSON object containing \"name\" and \"arguments\".",
"To call a tool, you MUST output a <tool_call> block containing a JSON object with \"name\" and \"arguments\".",
"",
"Format:",
"<tool_call>",
@@ -77,10 +81,14 @@ def build_tool_prompt(
lines.append(f"IMPORTANT: You MUST call the tool \"{forced_name}\" in your response.")
else:
# "auto" or default
lines.append("Decide whether to call a tool based on the user's request. If you don't need a tool, respond normally with text only.")
lines.append("Decide whether to call a tool based on the user's request. IF YOU CALL A TOOL, DO NOT ADD ANY DIALOGUE. OUTPUT <tool_call> FAST AND ONLY.")
lines.append("")
lines.append("When you call a tool, you may include text before or after the <tool_call> blocks, but the tool call blocks must be valid JSON.")
lines.append("")
lines.append("(If the task is complete and no more tools are needed, provide a regular answer. Only use <tool_call> when you actually need to call a tool.)")
lines.append("")
lines.append("IMPORTANT (Tool Error Handling): If a tool (e.g., `read`) fails with `EISDIR: illegal operation on a directory`, it means the path is a directory. You MUST call `exec` with `dir` (on Windows) or `ls` (on Linux) to list its contents instead.")
return "\n".join(lines)
@@ -296,14 +304,8 @@ def format_tool_history(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
})
elif role == "tool":
# Convert tool result to text format
tool_name = name or "unknown"
call_id = tool_call_id or ""
content_str = content if isinstance(content, str) else json.dumps(content, ensure_ascii=False) if content else ""
result.append({
"role": "user",
"content": f"tool ({tool_name}, {call_id}): {content_str}",
})
# 保留 tool 角色,交给 MessageExtractor 统一压平或由后台处理
result.append(msg)
else:
result.append(msg)
+19
View File
@@ -39,6 +39,18 @@ def _is_transient_network_error(err: Exception) -> bool:
class AppChatReverse:
"""/rest/app-chat/conversations/new reverse interface."""
@staticmethod
def _resolve_custom_personality() -> Optional[str]:
"""Resolve optional custom personality from app config."""
value = get_config("app.custom_instruction", "")
if value is None:
return None
if not isinstance(value, str):
value = str(value)
if not value.strip():
return None
return value
@staticmethod
def build_payload(
message: str,
@@ -94,6 +106,13 @@ class AppChatReverse:
if model_config_override:
payload["responseMetadata"]["modelConfigOverride"] = model_config_override
if model == "grok-420":
payload["enable420"] = True
custom_personality = AppChatReverse._resolve_custom_personality()
if custom_personality is not None and "Greet the user" not in message[-1000:]:
payload["customPersonality"] = custom_personality
return payload
@staticmethod
+198 -18
View File
@@ -1,8 +1,8 @@
"""Shared header builders for reverse interfaces."""
import re
import uuid
import orjson
import random
from urllib.parse import urlparse
from typing import Dict, Optional
@@ -10,6 +10,52 @@ from app.core.logger import logger
from app.core.config import get_config
from app.services.reverse.utils.statsig import StatsigGenerator
_HEADER_CHAR_REPLACEMENTS = str.maketrans(
{
"\u2010": "-", # hyphen
"\u2011": "-", # non-breaking hyphen
"\u2012": "-", # figure dash
"\u2013": "-", # en dash
"\u2014": "-", # em dash
"\u2212": "-", # minus sign
"\u2018": "'", # left single quote
"\u2019": "'", # right single quote
"\u201c": '"', # left double quote
"\u201d": '"', # right double quote
"\u00a0": " ", # nbsp
"\u2007": " ", # figure space
"\u202f": " ", # narrow nbsp
"\u200b": "", # zero width space
"\u200c": "", # zero width non-joiner
"\u200d": "", # zero width joiner
"\ufeff": "", # bom
}
)
def _sanitize_header_value(
value: Optional[str],
*,
field_name: str,
remove_all_spaces: bool = False,
) -> str:
"""Normalize header values and make sure they are latin-1 safe."""
raw = "" if value is None else str(value)
normalized = raw.translate(_HEADER_CHAR_REPLACEMENTS)
if remove_all_spaces:
normalized = re.sub(r"\s+", "", normalized)
else:
normalized = normalized.strip()
# curl_cffi header encoding defaults to latin-1.
normalized = normalized.encode("latin-1", errors="ignore").decode("latin-1")
if normalized != raw:
logger.warning(
f"Sanitized header field '{field_name}' (len {len(raw)} -> {len(normalized)})"
)
return normalized
def build_sso_cookie(sso_token: str) -> str:
"""
@@ -23,15 +69,41 @@ def build_sso_cookie(sso_token: str) -> str:
"""
# Format
sso_token = sso_token[4:] if sso_token.startswith("sso=") else sso_token
sso_token = _sanitize_header_value(
sso_token, field_name="sso_token", remove_all_spaces=True
)
# SSO Cookie
cookie = f"sso={sso_token}; sso-rw={sso_token}"
# CF Cookies
cf_cookies = get_config("proxy.cf_cookies") or ""
if not cf_cookies:
cf_clearance = get_config("proxy.cf_clearance")
if cf_clearance:
cf_cookies = _sanitize_header_value(
get_config("proxy.cf_cookies") or "", field_name="proxy.cf_cookies"
)
cf_clearance = _sanitize_header_value(
get_config("proxy.cf_clearance") or "",
field_name="proxy.cf_clearance",
remove_all_spaces=True,
)
cf_refresh_enabled = bool(get_config("proxy.enabled"))
if cf_refresh_enabled:
if not cf_cookies and cf_clearance:
cf_cookies = f"cf_clearance={cf_clearance}"
elif cf_clearance:
if cf_cookies:
# Replace existing cf_clearance or append if missing.
if re.search(r"(?:^|;\\s*)cf_clearance=", cf_cookies):
cf_cookies = re.sub(
r"(^|;\\s*)cf_clearance=[^;]*",
r"\\1cf_clearance=" + cf_clearance,
cf_cookies,
count=1,
)
else:
cf_cookies = cf_cookies.rstrip("; ")
cf_cookies = f"{cf_cookies}; cf_clearance={cf_clearance}"
else:
cf_cookies = f"cf_clearance={cf_clearance}"
if cf_cookies:
if cookie and not cookie.endswith(";"):
@@ -41,6 +113,95 @@ def build_sso_cookie(sso_token: str) -> str:
return cookie
def _extract_major_version(browser: Optional[str], user_agent: Optional[str]) -> Optional[str]:
if browser:
match = re.search(r"(\d{2,3})", browser)
if match:
return match.group(1)
if user_agent:
for pattern in [r"Edg/(\d+)", r"Chrome/(\d+)", r"Chromium/(\d+)"]:
match = re.search(pattern, user_agent)
if match:
return match.group(1)
return None
def _detect_platform(user_agent: str) -> Optional[str]:
ua = user_agent.lower()
if "windows" in ua:
return "Windows"
if "mac os x" in ua or "macintosh" in ua:
return "macOS"
if "android" in ua:
return "Android"
if "iphone" in ua or "ipad" in ua:
return "iOS"
if "linux" in ua:
return "Linux"
return None
def _detect_arch(user_agent: str) -> Optional[str]:
ua = user_agent.lower()
if "aarch64" in ua or "arm" in ua:
return "arm"
if "x86_64" in ua or "x64" in ua or "win64" in ua or "intel" in ua:
return "x86"
return None
def _build_client_hints(browser: Optional[str], user_agent: Optional[str]) -> Dict[str, str]:
browser = (browser or "").strip().lower()
user_agent = user_agent or ""
ua = user_agent.lower()
is_edge = "edge" in browser or "edg" in ua
is_brave = "brave" in browser
is_chromium = any(key in browser for key in ["chrome", "chromium", "edge", "brave"]) or (
"chrome" in ua or "chromium" in ua or "edg" in ua
)
is_firefox = "firefox" in ua or "firefox" in browser
is_safari = ("safari" in ua and "chrome" not in ua and "chromium" not in ua and "edg" not in ua) or "safari" in browser
if not is_chromium or is_firefox or is_safari:
return {}
version = _extract_major_version(browser, user_agent)
if not version:
return {}
if is_edge:
brand = "Microsoft Edge"
elif "chromium" in browser:
brand = "Chromium"
elif is_brave:
brand = "Brave"
else:
brand = "Google Chrome"
sec_ch_ua = (
f"\"{brand}\";v=\"{version}\", "
f"\"Chromium\";v=\"{version}\", "
"\"Not(A:Brand\";v=\"24\""
)
platform = _detect_platform(user_agent)
arch = _detect_arch(user_agent)
mobile = "?1" if ("mobile" in ua or platform in ("Android", "iOS")) else "?0"
hints = {
"Sec-Ch-Ua": sec_ch_ua,
"Sec-Ch-Ua-Mobile": mobile,
}
if platform:
hints["Sec-Ch-Ua-Platform"] = f"\"{platform}\""
if arch:
hints["Sec-Ch-Ua-Arch"] = arch
hints["Sec-Ch-Ua-Bitness"] = "64"
hints["Sec-Ch-Ua-Model"] = "" if mobile == "?0" else ""
return hints
def build_ws_headers(token: Optional[str] = None, origin: Optional[str] = None, extra: Optional[Dict[str, str]] = None) -> Dict[str, str]:
"""
Build headers for WebSocket requests.
@@ -53,12 +214,24 @@ def build_ws_headers(token: Optional[str] = None, origin: Optional[str] = None,
Returns:
Dict[str, str]: The headers dictionary.
"""
user_agent = _sanitize_header_value(
get_config("proxy.user_agent"), field_name="proxy.user_agent"
)
if not user_agent:
user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36"
safe_origin = _sanitize_header_value(origin or "https://grok.com", field_name="origin")
headers = {
"Origin": origin or "https://grok.com",
"Origin": safe_origin,
"User-Agent": user_agent,
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
"Cache-Control": "no-cache",
"Pragma": "no-cache",
}
client_hints = _build_client_hints(get_config("proxy.browser"), user_agent)
if client_hints:
headers.update(client_hints)
if token:
headers["Cookie"] = build_sso_cookie(token)
@@ -81,22 +254,29 @@ def build_headers(cookie_token: str, content_type: Optional[str] = None, origin:
Returns:
Dict[str, str]: The headers dictionary.
"""
trace_id = uuid.uuid4().hex
span_id = uuid.uuid4().hex[:16]
user_agent = _sanitize_header_value(
get_config("proxy.user_agent"), field_name="proxy.user_agent"
)
if not user_agent:
user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36"
safe_origin = _sanitize_header_value(origin or "https://grok.com", field_name="origin")
safe_referer = _sanitize_header_value(
referer or "https://grok.com/", field_name="referer"
)
headers = {
"Baggage": f"sentry-environment=production,sentry-release=c43385ae21231a335971832ae5b5e8bdba69852d,sentry-public_key=b311e0f2690c81f25e2c4cf6d4f7ce1c,sentry-trace_id={trace_id},sentry-org_id=4508179396558848,sentry-sampled=false,sentry-sample_rand={random.random()},sentry-sample_rate=0",
"Origin": origin or "https://grok.com",
"Accept-Encoding": "gzip, deflate, br, zstd",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
"Baggage": "sentry-environment=production,sentry-release=d6add6fb0460641fd482d767a335ef72b9b6abb8,sentry-public_key=b311e0f2690c81f25e2c4cf6d4f7ce1c",
"Origin": safe_origin,
"Priority": "u=1, i",
"Referer": referer or "https://grok.com/",
"Referer": safe_referer,
"Sec-Fetch-Mode": "cors",
"Sentry-Trace": f"{trace_id}-{span_id}-0",
"Traceparent": f"00-{trace_id}-{span_id}-00",
"User-Agent": user_agent,
}
user_agent = get_config("proxy.user_agent")
if user_agent:
headers["User-Agent"] = user_agent
client_hints = _build_client_hints(get_config("proxy.browser"), user_agent)
if client_hints:
headers.update(client_hints)
# Cookie
headers["Cookie"] = build_sso_cookie(cookie_token)