让项目聊天读取原始 item 历史

DirectProject 聊天命令写入 Responses message item

聊天读取从原始 item 投影 user/assistant 内容

保留 Agent session 旧会话读写路径
This commit is contained in:
2026-09-04 17:20:33 +08:00
parent 3d44af0a7d
commit 60f17af905
2 changed files with 88 additions and 0 deletions
@@ -72,10 +72,41 @@ pub(crate) fn append_direct_project_history_item_at(
let path = history_path(root);
let lock = project_append_lock_for(&path)?;
let _append_guard = lock.lock("DirectProject 历史追加写")?;
if let Some(item_id) = item.get("id").and_then(Value::as_str) {
if read_direct_project_history_items_at(root)?.iter().any(|existing| {
existing.get("id").and_then(Value::as_str) == Some(item_id)
}) {
return Ok(());
}
}
let line = record(item)?;
append_jsonl_line_unlocked(&path, &line, "DirectProject 历史")
}
pub(crate) fn direct_project_local_message_item(
role: &str,
content: &str,
message_id: Option<&str>,
) -> Result<Value, String> {
let role = role.trim();
let content = content.trim();
if content.is_empty() || !matches!(role, "user" | "assistant") {
return Err("DirectProject 只接受非空 user/assistant 历史消息".to_string());
}
let mut item = serde_json::json!({
"type": "message",
"role": role,
"content": [{
"type": if role == "user" { "input_text" } else { "output_text" },
"text": content,
}],
});
if let Some(message_id) = message_id.map(str::trim).filter(|value| !value.is_empty()) {
item["id"] = Value::String(message_id.to_string());
}
Ok(item)
}
pub(crate) fn direct_project_user_message_item(prompt: &str) -> Value {
serde_json::json!({
"type": "message",
@@ -133,3 +164,47 @@ pub(crate) fn read_direct_project_history_items_at(
}
Ok(items)
}
pub(crate) fn read_direct_project_chat_history_at(
root: &Path,
) -> Result<LocalConversationResult, String> {
let path = history_path(root);
let items = read_direct_project_history_items_at(root)?;
let messages = items
.into_iter()
.filter_map(|item| {
let role = item.get("role").and_then(Value::as_str)?;
if !matches!(role, "user" | "assistant") {
return None;
}
let content = item
.get("content")
.and_then(Value::as_array)
.map(|parts| {
parts
.iter()
.filter_map(|part| part.get("text").and_then(Value::as_str))
.collect::<Vec<_>>()
.join("")
})
.unwrap_or_default();
(!content.is_empty()).then(|| LocalConversationMessageRecord {
schema_version: "agc-direct-project-context.v1".to_string(),
role: role.to_string(),
content,
agent_id: None,
message_id: item
.get("id")
.and_then(Value::as_str)
.map(str::to_string),
updated_at: 0,
})
})
.collect();
Ok(LocalConversationResult {
path: path.to_string_lossy().into_owned(),
agent_id: None,
session_id: None,
messages,
})
}
@@ -1,4 +1,5 @@
use super::*;
use crate::agent::read_direct_project_chat_history_at;
use crate::ui_editor::resource::font::FontAsset;
use sha2::{Digest, Sha256};
use std::collections::{BTreeMap, HashSet};
@@ -4707,6 +4708,9 @@ pub(crate) fn read_local_conversation(
) -> Result<LocalConversationResult, String> {
let root = Path::new(project_path.trim());
enforce_project_permission_policy(root, "conversation.read")?;
if agent_id.is_none() && session_id.is_none() {
return read_direct_project_chat_history_at(root);
}
read_local_conversation_for_session_at(root, agent_id.as_deref(), session_id.as_deref())
}
@@ -4720,6 +4724,15 @@ pub(crate) fn append_local_conversation_message(
) -> Result<LocalConversationResult, String> {
let root = Path::new(project_path.trim());
enforce_project_permission_policy(root, "conversation.write")?;
if agent_id.is_none() && session_id.is_none() {
let item = direct_project_local_message_item(
&message.role,
&message.content,
message_id.as_deref(),
)?;
append_direct_project_history_item_at(root, &item)?;
return read_direct_project_chat_history_at(root);
}
let _lock = acquire_project_write_lock(root, "conversation.write")?;
match message_id.as_deref() {
Some(message_id) => append_local_conversation_message_for_session_idempotent_at(