refactor(sessions): reuse shared block helpers across the session parsers

Behaviour-preserving cleanup; parsed output is byte-identical on 2,482
real local sessions (Codex, Claude, Hermes, OpenCode, Grok Build, Pi,
OpenClaw).

- Build Event blocks with SessionBlock::event instead of 15 hand-written
  literals.
- Drop codex_items' copies of count_diff_lines and saturating_u32.
- Move Hermes' OpenAI-shaped tool_calls parser into blocks and reuse it
  for Grok Build, which had a line-for-line copy.
- Call the OpenCode / Hermes parse_sqlite_source from content.rs instead
  of keeping verbatim copies there.
- Add CallSpec::new / ResultSpec::empty so the Codex call and result
  literals only spell out non-default fields, and remove the always-empty
  FileChangeInfo.output.
This commit is contained in:
Jason committed 2026-10-03 18:56:44 +08:00
1 parent ad8efc652b
commit 85ff0dd456
12 files changed
+187 -326

No files matched your search

+1 -6
View File
@@ -610,12 +610,7 @@ mod tests {
SessionMessage::from_blocks(
"assistant",
None,
vec![SessionBlock::Event {
kind: EventKind::Aborted,
text: None,
url: None,
full: None,
}],
vec![SessionBlock::event(EventKind::Aborted, None, None)],
),
];
+6 -23
View File
@@ -18,6 +18,7 @@ use serde::Serialize;
use serde_json::Value;
use super::model::{ContentRef, ImageRef, ImageSource};
use super::providers::{hermes, opencode};
use super::{canonicalize_existing_path, provider_roots};
/// 单次读取的全文上限
@@ -91,12 +92,12 @@ pub fn validate_source(provider_id: &str, source_path: &str) -> Result<Validated
}
"opencode" if source_path.starts_with("sqlite:") => validate_sqlite_source(
source_path,
parse_opencode_sqlite(source_path),
opencode::parse_sqlite_source(source_path),
&crate::opencode_config::get_opencode_db_path(),
)?,
"hermes" if source_path.starts_with("sqlite:") => validate_sqlite_source(
source_path,
parse_hermes_sqlite(source_path),
hermes::parse_sqlite_source(source_path),
&crate::hermes_config::get_hermes_dir().join("state.db"),
)?,
_ => {
@@ -149,24 +150,6 @@ pub(super) fn resolve_under_roots(
))
}
/// `sqlite:<db>:<ses_id>`(与 `providers::opencode::parse_sqlite_source` 同规则)
fn parse_opencode_sqlite(source: &str) -> Option<(PathBuf, String)> {
let rest = source.strip_prefix("sqlite:")?;
let sep = rest.rfind(":ses_")?;
Some((PathBuf::from(&rest[..sep]), rest[sep + 1..].to_string()))
}
/// `sqlite:<db>#<session_id>`(与 `providers::hermes::parse_sqlite_source` 同规则)
fn parse_hermes_sqlite(source: &str) -> Option<(PathBuf, String)> {
let rest = source.strip_prefix("sqlite:")?;
let hash = rest.rfind('#')?;
let session_id = &rest[hash + 1..];
if session_id.is_empty() {
return None;
}
Some((PathBuf::from(&rest[..hash]), session_id.to_string()))
}
fn validate_sqlite_source(
raw: &str,
parsed: Option<(PathBuf, String)>,
@@ -709,14 +692,14 @@ mod tests {
std::fs::write(&other, "").unwrap();
let raw = format!("sqlite:{}:ses_1", own.display());
let ok = validate_sqlite_source(&raw, parse_opencode_sqlite(&raw), &own).unwrap();
let ok = validate_sqlite_source(&raw, opencode::parse_sqlite_source(&raw), &own).unwrap();
assert!(
matches!(ok, SourceLocation::Sqlite { ref session_id, .. } if session_id == "ses_1")
);
let raw = format!("sqlite:{}:ses_1", other.display());
assert!(validate_sqlite_source(&raw, parse_opencode_sqlite(&raw), &own).is_err());
assert!(parse_hermes_sqlite("sqlite:/x/state.db#").is_none());
assert!(validate_sqlite_source(&raw, opencode::parse_sqlite_source(&raw), &own).is_err());
assert!(hermes::parse_sqlite_source("sqlite:/x/state.db#").is_none());
}
/// 超长注入文本与压缩摘要只下发预览:`full` 必须能取回原文(Claude 与 Codex 解析器)
@@ -251,7 +251,7 @@ pub fn preview_with(text: &str, max_lines: usize, max_chars: usize) -> Preview {
}
}
fn saturating_u32(n: usize) -> u32 {
pub(super) fn saturating_u32(n: usize) -> u32 {
u32::try_from(n).unwrap_or(u32::MAX)
}
@@ -567,6 +567,37 @@ fn str_field_allow_empty<'a>(input: &'a Value, keys: &[&str]) -> Option<&'a str>
.find_map(|key| input.get(*key).and_then(Value::as_str))
}
/// OpenAI 形状的 `tool_calls[]`(`{id, function{name, arguments}}`,也兼容扁平 `{id, name, arguments}`)
/// → ToolCall。`arguments_ref(pointer)` 为参数全文生成引用,`pointer` 是参数在 `tool_calls`
/// 数组内的 JSON Pointer(`/{i}/function/arguments` 或扁平形状的 `/{i}/arguments`)。
pub(super) fn openai_tool_calls(
calls: Option<&Value>,
arguments_ref: impl Fn(String) -> Option<ContentRef>,
) -> Vec<SessionBlock> {
calls
.and_then(Value::as_array)
.into_iter()
.flatten()
.enumerate()
.map(|(i, call)| {
let name = call
.pointer("/function/name")
.or_else(|| call.get("name"))
.and_then(Value::as_str)
.unwrap_or("unknown");
let id = call.get("id").and_then(Value::as_str).unwrap_or_default();
let (pointer, raw) = match call.pointer("/function/arguments") {
Some(raw) => (format!("/{i}/function/arguments"), Some(raw)),
None => (format!("/{i}/arguments"), call.get("arguments")),
};
let input = raw.map(parse_arguments).unwrap_or(Value::Null);
tool_call_block(ToolSource::Generic, id, name, &input, || {
arguments_ref(pointer)
})
})
.collect()
}
/// 由参数构造 `ToolCall`:归一化 kind、提炼标题、参数预览;参数超长时调用 `input_full` 生成引用。
/// Edit/Write 先按参数估算 diff,解析器拿到更准的数据再覆盖。
pub fn tool_call_block(
@@ -414,12 +414,7 @@ impl TranscriptBuilder {
"system",
ts,
None,
vec![SessionBlock::Event {
kind,
text,
url,
full: None,
}],
vec![SessionBlock::event(kind, text, url)],
);
}
@@ -681,19 +676,17 @@ impl TranscriptBuilder {
};
if let Some(first) = texts.iter().map(|(t, _)| t.trim()).find(|t| !t.is_empty()) {
if first.starts_with("[Request interrupted by user") {
blocks.push(SessionBlock::Event {
kind: EventKind::Aborted,
text: Some(first.to_string()),
url: None,
full: None,
});
blocks.push(SessionBlock::event(
EventKind::Aborted,
Some(first.to_string()),
None,
));
} else if let Some(command) = slash_command(first) {
blocks.push(SessionBlock::Event {
kind: EventKind::SlashCommand,
text: Some(command),
url: None,
full: None,
});
blocks.push(SessionBlock::event(
EventKind::SlashCommand,
Some(command),
None,
));
} else if first.starts_with("<bash-input>") {
blocks.extend(user_bash_blocks(first, uuid.as_deref()));
} else if is_injected_text(first) {
+57 -126
View File
@@ -797,12 +797,11 @@ impl RolloutParser {
"system",
ts,
None,
vec![SessionBlock::Event {
kind: EventKind::ModelChange,
text: Some(model),
url: None,
full: None,
}],
vec![SessionBlock::event(
EventKind::ModelChange,
Some(model),
None,
)],
false,
);
}
@@ -819,12 +818,7 @@ impl RolloutParser {
"system",
ts,
None,
vec![SessionBlock::Event {
kind: EventKind::Aborted,
text: Some(reason),
url: None,
full: None,
}],
vec![SessionBlock::event(EventKind::Aborted, Some(reason), None)],
false,
);
}
@@ -1066,12 +1060,7 @@ impl RolloutParser {
} else {
format!("{author}: {}", first.trim())
};
let mut blocks = vec![SessionBlock::Event {
kind: EventKind::SubAgent,
text: Some(event),
url: None,
full: None,
}];
let mut blocks = vec![SessionBlock::event(EventKind::SubAgent, Some(event), None)];
if !rest.trim().is_empty() {
blocks.push(SessionBlock::text(rest.trim().to_string()));
}
@@ -1094,27 +1083,14 @@ impl RolloutParser {
Some(ToolStatus::Interrupted) => ToolStatus::Interrupted,
_ => ToolStatus::Success,
};
let call = CallSpec {
id: id.clone(),
raw_name: "web_search".to_string(),
kind: ToolKind::Web,
title: web_action_title(&action),
detail: None,
server: None,
input: &input,
input_ref: None,
diff: None,
by_user: false,
};
let result = ResultSpec {
call_id: id.clone(),
status,
output: preview(""),
full: None,
exit_code: None,
duration_ms: None,
images: Vec::new(),
};
let call = CallSpec::new(
id.clone(),
"web_search",
ToolKind::Web,
web_action_title(&action),
&input,
);
let result = ResultSpec::empty(id.clone(), status);
self.push_assistant(ts, Some(id), vec![call.into_block(), result.into_block()]);
}
@@ -1244,16 +1220,10 @@ impl RolloutParser {
};
let block = CallSpec {
id: call_id.clone(),
raw_name: name.to_string(),
kind,
title,
detail,
server: normalized.server,
input: arguments,
input_ref: Some(span.content_ref("/payload/arguments")),
diff: None,
by_user: false,
..CallSpec::new(call_id.clone(), name, kind, title, arguments)
}
.into_block();
self.register_call(ts, owned(&p.id), call_id, block, flavor, cmds);
@@ -1267,16 +1237,14 @@ impl RolloutParser {
.unwrap_or_default();
let mut spec = CallSpec {
id: call_id.clone(),
raw_name: name.to_string(),
kind: ToolKind::Other,
title: title_other(name),
detail: None,
server: None,
input,
input_ref: Some(span.content_ref("/payload/input")),
diff: None,
by_user: false,
..CallSpec::new(
call_id.clone(),
name,
ToolKind::Other,
title_other(name),
input,
)
};
let (flavor, cmds) = match name {
"exec" => {
@@ -1321,19 +1289,18 @@ impl RolloutParser {
.or_else(|| owned(&p.id))
.unwrap_or_default();
let block = CallSpec {
id: call_id.clone(),
raw_name: "local_shell".to_string(),
kind: refine_shell_kind(&cmd),
title: title_shell(&cmd),
detail: action
.get("working_directory")
.and_then(Value::as_str)
.map(str::to_string),
server: None,
input: &input,
input_ref: Some(span.content_ref("/payload/action")),
diff: None,
by_user: false,
..CallSpec::new(
call_id.clone(),
"local_shell",
refine_shell_kind(&cmd),
title_shell(&cmd),
&input,
)
}
.into_block();
let cmds = if cmd.is_empty() {
@@ -1490,49 +1457,36 @@ impl RolloutParser {
}
ItemRecord::FileChange(info) => {
let call = CallSpec {
id: info.id.clone(),
raw_name: "FileChange".to_string(),
kind: diff_kind(&info.diff),
title: diff_title(&info.diff),
detail: None,
server: None,
input: "",
input_ref: None,
diff: Some(info.diff.clone()),
by_user: false,
};
let result = ResultSpec {
call_id: info.id.clone(),
status: info.status,
output: info.output.clone(),
full: None,
exit_code: None,
duration_ms: None,
images: Vec::new(),
..CallSpec::new(
info.id.clone(),
"FileChange",
diff_kind(&info.diff),
diff_title(&info.diff),
"",
)
};
let result = ResultSpec::empty(info.id.clone(), info.status);
(info.id, vec![call.into_block(), result.into_block()])
}
ItemRecord::Mcp(info) => {
let call = CallSpec {
id: info.id.clone(),
raw_name: info.tool.clone(),
kind: ToolKind::Mcp,
title: title_mcp(&info.server, &info.tool),
detail: info.detail.clone(),
server: Some(info.server.clone()),
input: &info.input,
input_ref: None,
diff: None,
by_user: false,
..CallSpec::new(
info.id.clone(),
info.tool.clone(),
ToolKind::Mcp,
title_mcp(&info.server, &info.tool),
&info.input,
)
};
let result = ResultSpec {
call_id: info.id.clone(),
status: info.status,
output: info.output.clone(),
full: info.output_ref.clone(),
exit_code: None,
duration_ms: info.duration_ms,
images: info.images.clone(),
..ResultSpec::empty(info.id.clone(), info.status)
};
let blocks = vec![call.into_block(), result.into_block()];
(info.id, blocks)
@@ -1544,27 +1498,14 @@ impl RolloutParser {
}],
),
ItemRecord::Web(info) => {
let call = CallSpec {
id: info.id.clone(),
raw_name: "web_search".to_string(),
kind: ToolKind::Web,
title: info.title.clone(),
detail: None,
server: None,
input: &info.input,
input_ref: None,
diff: None,
by_user: false,
};
let result = ResultSpec {
call_id: info.id.clone(),
status: ToolStatus::Success,
output: preview(""),
full: None,
exit_code: None,
duration_ms: None,
images: Vec::new(),
};
let call = CallSpec::new(
info.id.clone(),
"web_search",
ToolKind::Web,
info.title.clone(),
&info.input,
);
let result = ResultSpec::empty(info.id.clone(), ToolStatus::Success);
let blocks = vec![call.into_block(), result.into_block()];
(info.id, blocks)
}
@@ -1593,13 +1534,12 @@ impl RolloutParser {
None => {
let status = output_status(&out, None);
let result = ResultSpec {
call_id,
status,
output: preview(&out.text),
full: out.full,
exit_code: out.exit_code,
duration_ms: out.duration_ms,
images: out.images,
..ResultSpec::empty(call_id, status)
};
self.push("tool", ts, id, vec![result.into_block()], false);
}
@@ -1766,8 +1706,6 @@ impl RolloutParser {
images
};
ResultSpec {
call_id: last_id,
status,
output: preview(&out.text),
full: out.full,
exit_code: last_command.and_then(|c| c.exit_code).or(out.exit_code),
@@ -1776,6 +1714,7 @@ impl RolloutParser {
.or(out.duration_ms)
.or(mcp.as_ref().and_then(|m| m.duration_ms)),
images,
..ResultSpec::empty(last_id, status)
}
}
None => match last_command {
@@ -1786,15 +1725,7 @@ impl RolloutParser {
}
spec
}
None => ResultSpec {
call_id: last_id,
status: ToolStatus::Interrupted,
output: preview(""),
full: None,
exit_code: None,
duration_ms: None,
images: Vec::new(),
},
None => ResultSpec::empty(last_id, ToolStatus::Interrupted),
},
};
results.push(spec.into_block());
@@ -23,8 +23,9 @@ use crate::session_manager::model::{
};
use super::blocks::{
kind_from_codex_parsed_cmd, one_line_title, preview, preview_chars, refine_shell_kind,
title_mcp, title_path, title_shell, title_web, Preview, INPUT_PREVIEW_CHARS,
count_diff_lines, kind_from_codex_parsed_cmd, one_line_title, preview, preview_chars,
refine_shell_kind, saturating_u32, title_mcp, title_path, title_shell, title_web, Preview,
INPUT_PREVIEW_CHARS,
};
use super::utils::JsonlSpan;
@@ -294,7 +295,6 @@ pub(super) struct FileChangeInfo {
pub id: String,
pub diff: DiffSummary,
pub status: ToolStatus,
pub output: Preview,
}
#[derive(Debug, Clone)]
@@ -420,7 +420,7 @@ impl RawItem<'_> {
"/payload/item/changes/{}/unified_diff",
escape_pointer_token(path)
));
count_unified_diff(diff)
count_diff_lines(diff)
} else {
let lines = change
.get("content")
@@ -457,7 +457,6 @@ impl RawItem<'_> {
id,
diff: diff_summary(files, full),
status,
output: preview(""),
})
}
@@ -810,26 +809,6 @@ pub(super) fn parse_legacy_shell_output(text: &str) -> Option<(String, Option<i3
// ─── diff ────────────────────────────────────────────────────────────────
fn saturating_u32(n: usize) -> u32 {
u32::try_from(n).unwrap_or(u32::MAX)
}
/// unified diff 的增删行数(不计 `+++` / `---` 文件头)
pub(super) fn count_unified_diff(diff: &str) -> (u32, u32) {
let (mut added, mut removed) = (0u32, 0u32);
for line in diff.lines() {
if line.starts_with("+++") || line.starts_with("---") {
continue;
}
if line.starts_with('+') {
added += 1;
} else if line.starts_with('-') {
removed += 1;
}
}
(added, removed)
}
pub(super) fn diff_summary(files: Vec<DiffFile>, full: Option<ContentRef>) -> DiffSummary {
DiffSummary {
added: files.iter().map(|f| f.added).sum(),
@@ -968,7 +947,29 @@ pub(super) struct CallSpec<'s> {
pub by_user: bool,
}
impl CallSpec<'_> {
impl<'s> CallSpec<'s> {
/// 只填必需字段的调用,其余字段按需用 `..` 覆盖
pub fn new(
id: String,
raw_name: impl Into<String>,
kind: ToolKind,
title: String,
input: &'s str,
) -> Self {
Self {
id,
raw_name: raw_name.into(),
kind,
title,
detail: None,
server: None,
input,
input_ref: None,
diff: None,
by_user: false,
}
}
pub fn into_block(self) -> SessionBlock {
let input = preview_chars(self.input, INPUT_PREVIEW_CHARS);
SessionBlock::ToolCall {
@@ -1003,6 +1004,19 @@ pub(super) struct ResultSpec {
}
impl ResultSpec {
/// 没有输出的结果
pub fn empty(call_id: String, status: ToolStatus) -> Self {
Self {
call_id,
status,
output: preview(""),
full: None,
exit_code: None,
duration_ms: None,
images: Vec::new(),
}
}
pub fn into_block(self) -> SessionBlock {
let truncated = self.output.truncated;
SessionBlock::ToolResult {
@@ -1025,28 +1039,25 @@ impl CommandInfo {
/// 单独成块的 CommandExecution 工具调用(没有对应 exec 时)
pub fn call_spec(&self) -> CallSpec<'_> {
CallSpec {
id: self.id.clone(),
raw_name: "CommandExecution".to_string(),
kind: self.kind,
title: title_shell(&self.command),
detail: self.cwd.clone(),
server: None,
input: &self.command,
input_ref: None,
diff: None,
by_user: self.by_user,
..CallSpec::new(
self.id.clone(),
"CommandExecution",
self.kind,
title_shell(&self.command),
&self.command,
)
}
}
pub fn result_spec(&self, call_id: String) -> ResultSpec {
ResultSpec {
call_id,
status: self.status,
output: self.output.clone(),
full: self.output_ref.clone(),
exit_code: self.exit_code,
duration_ms: self.duration_ms,
images: Vec::new(),
..ResultSpec::empty(call_id, self.status)
}
}
}
@@ -1185,7 +1196,7 @@ mod tests {
assert_eq!(diff_title(&diff), "a.rs 等 3 个文件");
assert_eq!(
count_unified_diff("--- a\n+++ b\n@@\n-x\n+y\n+z\n ctx"),
count_diff_lines("--- a\n+++ b\n@@\n-x\n+y\n+z\n ctx"),
(2, 1)
);
}
@@ -111,12 +111,7 @@ pub fn load_messages(path: &Path) -> Result<Vec<SessionMessage>, String> {
let mut message = SessionMessage::from_blocks(
"system",
ts,
vec![SessionBlock::Event {
kind: event_kind,
text: Some(text),
url: None,
full: None,
}],
vec![SessionBlock::event(event_kind, Some(text), None)],
);
// info(登录、刷新等提示)默认折叠
message.injected = kind == "info";
@@ -6,9 +6,7 @@ use serde_json::Value;
use crate::session_manager::model::{SessionBlock, ToolStatus};
use crate::session_manager::{SessionMessage, SessionMeta};
use super::blocks::{
assign_turn_ids, parse_arguments, tool_call_block, tool_result_block, ToolSource,
};
use super::blocks::{assign_turn_ids, openai_tool_calls, tool_result_block};
use super::utils::{
extract_text, for_each_jsonl_value, parse_timestamp_to_ms, truncate_summary, TITLE_MAX_CHARS,
};
@@ -97,32 +95,9 @@ pub fn load_messages(path: &Path) -> Result<Vec<SessionMessage>, String> {
if !text.trim().is_empty() {
blocks.push(SessionBlock::text(text));
}
for (j, call) in value
.get("tool_calls")
.and_then(Value::as_array)
.into_iter()
.flatten()
.enumerate()
{
let name = call
.pointer("/function/name")
.or_else(|| call.get("name"))
.and_then(Value::as_str)
.unwrap_or("unknown");
let (input_pointer, raw_input) = match call.pointer("/function/arguments") {
Some(raw) => (format!("/tool_calls/{j}/function/arguments"), Some(raw)),
None => (format!("/tool_calls/{j}/arguments"), call.get("arguments")),
};
let input = raw_input.map(parse_arguments).unwrap_or(Value::Null);
let id = call.get("id").and_then(Value::as_str).unwrap_or_default();
blocks.push(tool_call_block(
ToolSource::Generic,
id,
name,
&input,
|| Some(span.content_ref(input_pointer)),
));
}
blocks.extend(openai_tool_calls(value.get("tool_calls"), |pointer| {
Some(span.content_ref(format!("/tool_calls{pointer}")))
}));
blocks
}
_ if text.trim().is_empty() => Vec::new(),
@@ -9,10 +9,7 @@ use crate::hermes_config::get_hermes_dir;
use crate::session_manager::model::{ContentRef, SessionBlock, ToolStatus};
use crate::session_manager::{SessionMessage, SessionMeta};
use super::blocks::{
assign_turn_ids, parse_arguments, thinking_block, tool_call_block, tool_result_block,
ToolSource,
};
use super::blocks::{assign_turn_ids, openai_tool_calls, thinking_block, tool_result_block};
use super::utils::for_each_jsonl_value;
use super::utils::{
extract_text, parse_timestamp_to_ms, read_head_tail_lines, truncate_summary, TITLE_MAX_CHARS,
@@ -488,37 +485,6 @@ fn load_messages_from_conn(
Ok(messages)
}
/// OpenAI 形状的 `tool_calls[]`(`{id, function{name, arguments}}`,也兼容扁平 `{id, name, arguments}`)
/// → ToolCall。`arguments_ref(pointer)` 为参数全文生成引用,`pointer` 是参数在 `tool_calls`
/// 数组内的 JSON Pointer(`/{i}/function/arguments` 或扁平形状的 `/{i}/arguments`)。
fn openai_tool_calls(
calls: Option<&Value>,
arguments_ref: impl Fn(String) -> Option<ContentRef>,
) -> Vec<SessionBlock> {
calls
.and_then(Value::as_array)
.into_iter()
.flatten()
.enumerate()
.map(|(i, call)| {
let name = call
.pointer("/function/name")
.or_else(|| call.get("name"))
.and_then(Value::as_str)
.unwrap_or("unknown");
let id = call.get("id").and_then(Value::as_str).unwrap_or_default();
let (pointer, raw) = match call.pointer("/function/arguments") {
Some(raw) => (format!("/{i}/function/arguments"), Some(raw)),
None => (format!("/{i}/arguments"), call.get("arguments")),
};
let input = raw.map(parse_arguments).unwrap_or(Value::Null);
tool_call_block(ToolSource::Generic, id, name, &input, || {
arguments_ref(pointer)
})
})
.collect()
}
/// `messages` 表某行某列的引用(`content::sqlite_allowed` 白名单内的列)
fn messages_cell(id: i64, column: &str, pointer: String) -> ContentRef {
ContentRef::Sqlite {
@@ -574,7 +540,7 @@ pub fn delete_session_sqlite(session_id: &str, source: &str) -> Result<bool, Str
Ok(deleted > 0)
}
fn parse_sqlite_source(source: &str) -> Option<(PathBuf, String)> {
pub(crate) fn parse_sqlite_source(source: &str) -> Option<(PathBuf, String)> {
let rest = source.strip_prefix("sqlite:")?;
let hash_pos = rest.rfind('#')?;
let db_path = PathBuf::from(&rest[..hash_pos]);
@@ -130,7 +130,7 @@ fn scan_sessions_json() -> Vec<SessionMeta> {
/// Uses `rfind(":ses_")` to split the path from the session ID because the
/// db path itself may contain colons (e.g. `C:\Users\...` on Windows).
/// This relies on the OpenCode convention that session IDs start with `ses_`.
fn parse_sqlite_source(source: &str) -> Option<(PathBuf, String)> {
pub(crate) fn parse_sqlite_source(source: &str) -> Option<(PathBuf, String)> {
let rest = source.strip_prefix("sqlite:")?;
let sep = rest.rfind(":ses_")?;
let db_path = PathBuf::from(&rest[..sep]);
@@ -113,12 +113,11 @@ fn push_part_blocks(blocks: &mut Vec<SessionBlock>, locator: &PartLocator, part:
Some("tool") => push_tool_blocks(blocks, locator, part),
Some("agent") => {
if let Some(name) = part.get("name").and_then(Value::as_str) {
blocks.push(SessionBlock::Event {
kind: EventKind::Other,
text: Some(format!("@{name}")),
url: None,
full: None,
});
blocks.push(SessionBlock::event(
EventKind::Other,
Some(format!("@{name}")),
None,
));
}
}
Some("patch") => push_patch_blocks(blocks, part),
@@ -327,12 +326,11 @@ fn push_file_block(blocks: &mut Vec<SessionBlock>, locator: &PartLocator, part:
}
}
if let Some(text) = filename.or_else(|| (!url.is_empty()).then_some(url)) {
blocks.push(SessionBlock::Event {
kind: EventKind::Other,
text: Some(text.to_string()),
url: None,
full: None,
});
blocks.push(SessionBlock::event(
EventKind::Other,
Some(text.to_string()),
None,
));
}
}
@@ -341,24 +339,18 @@ fn message_error_event(info: &Value) -> Option<SessionBlock> {
let error = info.get("error")?;
let name = error.get("name").and_then(Value::as_str).unwrap_or("");
if name == "MessageAbortedError" {
return Some(SessionBlock::Event {
kind: EventKind::Aborted,
text: None,
url: None,
full: None,
});
return Some(SessionBlock::event(EventKind::Aborted, None, None));
}
let text = error
.pointer("/data/message")
.and_then(Value::as_str)
.filter(|s| !s.is_empty())
.unwrap_or(name);
Some(SessionBlock::Event {
kind: EventKind::Error,
text: (!text.is_empty()).then(|| text.to_string()),
url: None,
full: None,
})
Some(SessionBlock::event(
EventKind::Error,
(!text.is_empty()).then(|| text.to_string()),
None,
))
}
/// 消息级 `cost/tokens/modelID/providerID/time/finish` → meta(0 视为缺省)。
@@ -170,18 +170,12 @@ impl PiTranscript {
// 中断 / 截断 / 出错追加事件
match msg.get("stopReason").and_then(Value::as_str) {
Some("aborted") => blocks.push(SessionBlock::Event {
kind: EventKind::Aborted,
text: None,
url: None,
full: None,
}),
Some("length") => blocks.push(SessionBlock::Event {
kind: EventKind::Other,
text: Some("truncated".into()),
url: None,
full: None,
}),
Some("aborted") => blocks.push(SessionBlock::event(EventKind::Aborted, None, None)),
Some("length") => blocks.push(SessionBlock::event(
EventKind::Other,
Some("truncated".into()),
None,
)),
Some("error") => blocks.push(SessionBlock::Event {
kind: EventKind::Error,
text: msg
@@ -430,12 +424,7 @@ fn event_message(kind: EventKind, text: String, ts: Option<i64>) -> Option<Sessi
SessionMessage::from_blocks(
"system",
ts,
vec![SessionBlock::Event {
kind,
text: Some(text),
url: None,
full: None,
}],
vec![SessionBlock::event(kind, Some(text), None)],
)
})
}