mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-10-26 01:34:51 +08:00
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:
1 parent
ad8efc652b
commit
85ff0dd456
12 files changed
+187
-326
No files matched your search
@@ -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)],
|
||||
),
|
||||
];
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)],
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user