feat: 添加 Web 前端及服务端 SSE 流式支持,扩展多模型兼容

后端:
  - server: 实现完整的 HTTP 会话管理(CRUD)+ SSE 事件流推送,
    支持双通道架构(POST 发消息 + GET SSE 接收流式响应)
  - runtime: ContentBlock 新增 Thinking / RedactedThinking 变体,
    支持思考过程和已编辑思考的序列化/反序列化
  - api: 注册 GLM 系列模型(glm-4/5 等)到模型注册表,
    扩展 XAI/OpenAI 兼容提供商的请求构建逻辑

  前端:
  - 基于 Ant Design X 构建完整聊天界面:Bubble.List 消息列表、
    Sender 输入框、Conversations 会话管理、Think 思考过程折叠、
    ThoughtChain 工具调用链展示
  - XMarkdown 集成:代码高亮、Mermaid 图表、LaTeX 公式、
    自定义脚注、流式渲染(incomplete 占位符)
  - SSE Hook 对接服务端事件流,手动管理 AssistantBuffer 累积 delta
  - 深色/浅色主题切换,会话侧边栏(新建/切换/删除)
This commit is contained in:
fengmengqi
2026-04-10 16:29:27 +08:00
parent 0782159ecd
commit 4a04faf926
56 changed files with 8611 additions and 172 deletions
+52 -7
View File
@@ -59,6 +59,7 @@ const INTERNAL_PROGRESS_HEARTBEAT_INTERVAL: Duration = Duration::from_secs(3);
type AllowedToolSet = BTreeSet<String>;
fn main() {
dotenvy::dotenv().ok();
if let Err(error) = run() {
eprintln!("{}", render_cli_error(&error.to_string()));
std::process::exit(1);
@@ -171,7 +172,7 @@ impl CliOutputFormat {
#[allow(clippy::too_many_lines)]
fn parse_args(args: &[String]) -> Result<CliAction, String> {
let mut model = DEFAULT_MODEL.to_string();
let mut model = env::var("CLAW_MODEL").unwrap_or_else(|_| DEFAULT_MODEL.to_string());
let mut output_format = CliOutputFormat::Text;
let mut permission_mode = default_permission_mode();
let mut wants_version = false;
@@ -2504,6 +2505,12 @@ fn render_export_text(session: &Session) -> String {
for block in &message.blocks {
match block {
ContentBlock::Text { text } => lines.push(text.clone()),
ContentBlock::Thinking { thinking, .. } => {
lines.push(format!("[thinking] {thinking}"));
}
ContentBlock::RedactedThinking { .. } => {
lines.push("[thinking] <redacted>".to_string());
}
ContentBlock::ToolUse { id, name, input } => {
lines.push(format!("[tool_use id={id} name={name}] {input}"));
}
@@ -3158,8 +3165,18 @@ impl ApiClient for DefaultRuntimeClient {
input.push_str(&partial_json);
}
}
ContentBlockDelta::ThinkingDelta { .. }
| ContentBlockDelta::SignatureDelta { .. } => {}
ContentBlockDelta::ThinkingDelta { thinking } => {
if !thinking.is_empty() {
if self.emit_output {
// Use a dimmed style for thinking blocks
write!(out, "\x1b[2m{thinking}\x1b[0m")
.and_then(|()| out.flush())
.map_err(|error| RuntimeError::new(error.to_string()))?;
}
events.push(AssistantEvent::ThinkingDelta(thinking));
}
}
ContentBlockDelta::SignatureDelta { .. } => {}
},
ApiStreamEvent::ContentBlockStop(_) => {
if let Some(rendered) = markdown_stream.flush(&renderer) {
@@ -3237,7 +3254,10 @@ fn final_assistant_text(summary: &runtime::TurnSummary) -> String {
.iter()
.filter_map(|block| match block {
ContentBlock::Text { text } => Some(text.as_str()),
_ => None,
ContentBlock::Thinking { thinking, .. } => Some(thinking.as_str()),
ContentBlock::RedactedThinking { .. }
| ContentBlock::ToolUse { .. }
| ContentBlock::ToolResult { .. } => None,
})
.collect::<Vec<_>>()
.join("")
@@ -3256,7 +3276,10 @@ fn collect_tool_uses(summary: &runtime::TurnSummary) -> Vec<serde_json::Value> {
"name": name,
"input": input,
})),
_ => None,
ContentBlock::Thinking { .. }
| ContentBlock::RedactedThinking { .. }
| ContentBlock::Text { .. }
| ContentBlock::ToolResult { .. } => None,
})
.collect()
}
@@ -3278,7 +3301,10 @@ fn collect_tool_results(summary: &runtime::TurnSummary) -> Vec<serde_json::Value
"output": output,
"is_error": is_error,
})),
_ => None,
ContentBlock::Thinking { .. }
| ContentBlock::RedactedThinking { .. }
| ContentBlock::Text { .. }
| ContentBlock::ToolUse { .. } => None,
})
.collect()
}
@@ -3819,7 +3845,16 @@ fn push_output_block(
};
*pending_tool = Some((id, name, initial_input));
}
OutputContentBlock::Thinking { .. } | OutputContentBlock::RedactedThinking { .. } => {}
OutputContentBlock::Thinking { thinking, .. } => {
if !thinking.is_empty() {
// Dimmed style for thinking
write!(out, "\x1b[2m{thinking}\x1b[0m")
.and_then(|()| out.flush())
.map_err(|error| RuntimeError::new(error.to_string()))?;
events.push(AssistantEvent::ThinkingDelta(thinking));
}
}
OutputContentBlock::RedactedThinking { .. } => {}
}
Ok(())
}
@@ -3928,6 +3963,16 @@ fn convert_messages(messages: &[ConversationMessage]) -> Vec<InputMessage> {
.iter()
.map(|block| match block {
ContentBlock::Text { text } => InputContentBlock::Text { text: text.clone() },
ContentBlock::Thinking {
thinking,
signature,
} => InputContentBlock::Thinking {
thinking: thinking.clone(),
signature: signature.clone(),
},
ContentBlock::RedactedThinking { data } => InputContentBlock::RedactedThinking {
data: serde_json::from_str(&data.render()).unwrap_or(serde_json::Value::Null),
},
ContentBlock::ToolUse { id, name, input } => InputContentBlock::ToolUse {
id: id.clone(),
name: name.clone(),