feat: add React UI and AI page sharing tools

Move HTTP page rendering to the React app, fix multi-tool-call message handling, add API docs lookup, web page lookup, and Markdown preview page sharing.
This commit is contained in:
2026-07-07 22:27:14 +08:00
parent 3d77733e11
commit 640ba9e7d2
65 changed files with 8020 additions and 1732 deletions
+132 -2
View File
@@ -6,6 +6,7 @@ use ias_common::model::ai::{
};
use ias_common::queue::ChannelMessage;
use sqlx::PgPool;
use std::collections::HashSet;
use crate::model::{ContextMessageRow, ContextScope, ContextSession, LongTermMemory};
use crate::repository::ContextRepository;
@@ -112,6 +113,7 @@ impl ContextManager {
let session = self.active_or_new_session(&scope).await?;
let tool_calls = tool_calls
.as_ref()
.filter(|calls| !calls.is_empty())
.map(serde_json::to_string)
.transpose()
.context("序列化 assistant tool_calls 失败")?;
@@ -285,6 +287,7 @@ impl ContextManager {
messages.push(row_to_chat_message(row)?);
}
let messages = sanitize_tool_call_messages(messages);
Ok(ContextSnapshot { messages })
}
@@ -448,7 +451,8 @@ fn row_to_chat_message(row: ContextMessageRow) -> anyhow::Result<ChatCompletionR
.as_deref()
.map(serde_json::from_str::<Vec<ChatCompletionToolCall>>)
.transpose()
.context("解析历史 tool_calls 失败")?;
.context("解析历史 tool_calls 失败")?
.filter(|calls| !calls.is_empty());
Ok(ChatCompletionRequestMessage {
role: row.role,
@@ -458,6 +462,74 @@ fn row_to_chat_message(row: ContextMessageRow) -> anyhow::Result<ChatCompletionR
})
}
fn sanitize_tool_call_messages(
messages: Vec<ChatCompletionRequestMessage>,
) -> Vec<ChatCompletionRequestMessage> {
let mut sanitized = Vec::with_capacity(messages.len());
let mut index = 0;
while index < messages.len() {
let message = &messages[index];
let tool_calls = message
.tool_calls
.as_ref()
.filter(|calls| !calls.is_empty());
if message.role == "assistant"
&& let Some(tool_calls) = tool_calls
{
let expected_ids = tool_calls
.iter()
.map(|call| call.id.as_str())
.collect::<HashSet<_>>();
let mut seen_ids = HashSet::new();
let mut tool_messages = Vec::new();
let mut next = index + 1;
while next < messages.len() && messages[next].role == "tool" {
if let Some(tool_call_id) = messages[next].tool_call_id.as_deref()
&& expected_ids.contains(tool_call_id)
&& seen_ids.insert(tool_call_id)
{
tool_messages.push(messages[next].clone());
}
next += 1;
}
if seen_ids.len() == expected_ids.len() {
sanitized.push(message.clone());
sanitized.extend(tool_messages);
} else {
let content = message.content.trim();
if !content.is_empty() {
let mut assistant = message.clone();
assistant.tool_calls = None;
sanitized.push(assistant);
}
eprintln!(
"跳过不完整 assistant tool_calls 历史片段: expected={}, actual={}",
expected_ids.len(),
seen_ids.len()
);
}
index = next;
continue;
}
if message.role == "tool" {
eprintln!("跳过没有对应 assistant tool_calls 的历史 tool 消息");
index += 1;
continue;
}
sanitized.push(message.clone());
index += 1;
}
sanitized
}
fn build_session_summary(
session: &ContextSession,
messages: &[ContextMessageRow],
@@ -731,7 +803,16 @@ fn read_i64_env(name: &str, default: i64) -> i64 {
#[cfg(test)]
mod tests {
use super::detect_assistant_name_preference;
use super::{detect_assistant_name_preference, sanitize_tool_call_messages};
use ias_common::model::ai::{ChatCompletionRequestMessage, ChatCompletionToolCall};
fn tool_call(id: &str) -> ChatCompletionToolCall {
ChatCompletionToolCall {
function: None,
id: id.to_string(),
call_type: "function".to_string(),
}
}
#[test]
fn detects_assistant_name_preference() {
@@ -754,4 +835,53 @@ mod tests {
assert_eq!(detect_assistant_name_preference("不要叫知微"), None);
assert_eq!(detect_assistant_name_preference("你是谁?"), None);
}
#[test]
fn keeps_complete_multi_tool_call_sequence() {
let messages = sanitize_tool_call_messages(vec![
ChatCompletionRequestMessage::new("user".to_string(), "查一下".to_string()),
ChatCompletionRequestMessage::assistant(
"assistant".to_string(),
String::new(),
Some(vec![tool_call("call_1"), tool_call("call_2")]),
),
ChatCompletionRequestMessage::tool(
"tool".to_string(),
"结果 1".to_string(),
"call_1".to_string(),
),
ChatCompletionRequestMessage::tool(
"tool".to_string(),
"结果 2".to_string(),
"call_2".to_string(),
),
]);
assert_eq!(messages.len(), 4);
assert_eq!(messages[1].tool_calls.as_ref().unwrap().len(), 2);
assert_eq!(messages[2].tool_call_id.as_deref(), Some("call_1"));
assert_eq!(messages[3].tool_call_id.as_deref(), Some("call_2"));
}
#[test]
fn drops_incomplete_tool_call_sequence() {
let messages = sanitize_tool_call_messages(vec![
ChatCompletionRequestMessage::new("user".to_string(), "查一下".to_string()),
ChatCompletionRequestMessage::assistant(
"assistant".to_string(),
String::new(),
Some(vec![tool_call("call_1"), tool_call("call_2")]),
),
ChatCompletionRequestMessage::tool(
"tool".to_string(),
"结果 1".to_string(),
"call_1".to_string(),
),
ChatCompletionRequestMessage::new("user".to_string(), "继续".to_string()),
]);
assert_eq!(messages.len(), 2);
assert_eq!(messages[0].role, "user");
assert_eq!(messages[1].content, "继续");
}
}