diff --git a/app/services/grok/services/chat.py b/app/services/grok/services/chat.py index eec1e8ae8..e7b92e9d3 100644 --- a/app/services/grok/services/chat.py +++ b/app/services/grok/services/chat.py @@ -537,8 +537,8 @@ def __init__( self.fingerprint: str = "" self.rollout_id: str = "" self.think_opened: bool = False - self.think_closed_once: bool = False self.image_think_active: bool = False + self._content_started: bool = False self.role_sent: bool = False self.filter_tags = get_config("app.filter_tags") self.tool_usage_enabled = ( @@ -816,7 +816,6 @@ async def process(self, response: AsyncIterable[bytes]) -> AsyncGenerator[str, N if self.image_think_active and self.think_opened: yield self._sse("\n\n") self.think_opened = False - self.think_closed_once = True self.image_think_active = False for url in proc_base._collect_images(mr): parts = url.split("/") @@ -860,15 +859,26 @@ async def process(self, response: AsyncIterable[bytes]) -> AsyncGenerator[str, N if (token := resp.get("token")) is not None: if not token: continue - if is_thinking and self.think_closed_once and not self.image_think_active: - continue filtered = self._filter_token(token) if not filtered: continue + # 判断是否在 Agent 思考/处理阶段: + # - isThinking=true → 归入 think + # - 有 messageStepId → Agent 处理中,归入 think + # - image_think_active → 图片生成中 + # 正式内容开始后,丢弃中途插入的思考(Grok 官网也隐藏了这部分) + has_step_id = bool(resp.get("messageStepId")) in_think = ( - (is_thinking and not self.think_closed_once) + is_thinking + or has_step_id or self.image_think_active ) + # 正式内容已开始后,丢弃中途插入的 Agent 思考(1-2 句内部注释,无用户价值) + if self._content_started and in_think and not self.image_think_active: + continue + # 空 token 不关闭 think 块(搜索结果间的空 token 不算正式内容) + if not in_think and not filtered.strip(): + continue if in_think: if not self.show_think: continue @@ -879,7 +889,7 @@ async def process(self, response: AsyncIterable[bytes]) -> AsyncGenerator[str, N if self.think_opened: yield self._sse("\n\n") self.think_opened = False - self.think_closed_once = True + self._content_started = True if in_think: self._record_content(filtered) @@ -901,7 +911,6 @@ async def process(self, response: AsyncIterable[bytes]) -> AsyncGenerator[str, N if self.think_opened: yield self._sse("\n") - self.think_closed_once = True if self._tool_stream_enabled: for kind, payload in self._flush_tool_stream(): @@ -1022,6 +1031,8 @@ async def process(self, response: AsyncIterable[bytes]) -> dict[str, Any]: response_id = "" fingerprint = "" content = "" + # 兜底收集非 thinking 且无 messageStepId 的最终内容 token + fallback_tokens: list[str] = [] idle_timeout = get_config("chat.stream_timeout") try: @@ -1041,6 +1052,13 @@ async def process(self, response: AsyncIterable[bytes]) -> dict[str, Any]: if (llm := resp.get("llmInfo")) and not fingerprint: fingerprint = llm.get("modelHash", "") + # 收集非 thinking 且无 messageStepId 的 token(最终内容兜底) + is_thinking = bool(resp.get("isThinking")) + has_step_id = bool(resp.get("messageStepId")) + if not is_thinking and not has_step_id: + if tok := resp.get("token"): + fallback_tokens.append(tok) + if mr := resp.get("modelResponse"): response_id = mr.get("responseId", "") content = mr.get("message", "") @@ -1140,6 +1158,10 @@ def _render_card(match: re.Match) -> str: finally: await self.close() + # modelResponse.message 为空时(多智能体模型),用兜底 token 拼接 + if not content and fallback_tokens: + content = "".join(fallback_tokens) + content = self._filter_content(content) # Parse for tool calls if tools were provided diff --git a/app/services/reverse/app_chat.py b/app/services/reverse/app_chat.py index 526059f0d..fdb486967 100644 --- a/app/services/reverse/app_chat.py +++ b/app/services/reverse/app_chat.py @@ -102,6 +102,19 @@ def _resolve_custom_personality() -> Optional[str]: return None return value + # modelMode → modeId 映射(Grok Web 新 API 格式) + # 基于浏览器前端 JS 逆向和 API 全量测试验证: + # 付费 SuperGrok 的多智能体模式需要 modeId 字段才能正常响应 + # 有 modeId 时不发 modelName/modelMode(浏览器前端逻辑) + _MODE_ID_MAP = { + "MODEL_MODE_FAST": "fast", + "MODEL_MODE_EXPERT": "expert", + "MODEL_MODE_HEAVY": "heavy", + "MODEL_MODE_GROK_420": "expert", + "MODEL_MODE_GROK_4_1_THINKING": "expert", + "MODEL_MODE_GROK_4_1_MINI_THINKING": "expert", + } + @staticmethod def build_payload( message: str, @@ -152,8 +165,13 @@ def build_payload( "toolOverrides": tool_overrides or {}, } - if model == "grok-420": - payload["enable420"] = True + # 优先使用 modeId(Grok 新 API 格式,付费号多智能体模式必需) + # 有 modeId 时移除 modelName/modelMode(浏览器前端逻辑) + mode_id = AppChatReverse._MODE_ID_MAP.get(mode) + if mode_id: + payload["modeId"] = mode_id + payload.pop("modelName", None) + payload.pop("modelMode", None) custom_personality = AppChatReverse._resolve_custom_personality() if custom_personality is not None: