chore: finalize v1.25.0 release artifacts

This commit is contained in:
coso
2026-04-30 23:28:52 +08:00
parent 4652dd633d
commit aaf3271973
2 changed files with 196 additions and 2 deletions
@@ -728,3 +728,45 @@ npx eslint "src/components/agent/chat/components/MessageList.tsx" "src/component
```
结果:性能指标汇总单测通过,ESLint touched files 继续通过。
### 2026-04-30:P1 第十四刀,旧会话首帧延后 Markdown hydrate
采集事实:
- DevBridge `3030` 仍未监听,真实 Playwright 多旧会话切换暂不可测。
- 当前机器已有 `tsc --noEmit` 与多条 `rustc` 高 CPU 进程,继续启动 Tauri / GUI smoke 会污染用户反馈的鼠标 loading 与 CPU 飙高采样。
- 第十三刀已跳过旧会话首帧的 `contentParts` 细节扫描,但短历史 assistant 正文仍会进入 `StreamingRenderer -> MarkdownRenderer -> ReactMarkdown`,即使 `renderMode=light` 也会同步解析多条历史 Markdown。
- 旧会话首帧的产品目标是“先看到消息文本与布局”,Markdown 标题、列表、代码高亮、表格等可等 idle 后恢复。
已完成:
- `MessageList.tsx`:
- 增加 `MESSAGE_LIST_STRUCTURED_HISTORY_CONTENT_RE` 与 `hasStructuredHistoricalContentHint`,避免把 A2UI / write_file / document 这类结构化协议用纯文本提前露出。
- 增加 `HistoricalMarkdownHydrationPreview`,旧会话首帧用 `whitespace-pre-wrap` 纯文本直接展示 assistant 正文,不挂载 `StreamingRenderer` / `ReactMarkdown`。
- 增加 `historicalMarkdownDeferredCount` 指标,记录旧会话首帧被延后的 Markdown hydrate 数量。
- 当 historical timeline idle-ready 后,自动恢复原有 `StreamingRenderer` / light Markdown 渲染。
- `MessageList.test.tsx`:更新旧会话首帧回归,断言首帧出现 `message-list-historical-markdown-preview`、不挂载 `streaming-renderer`,idle 后移除 preview 并恢复 renderer。
- `agentUiPerformanceMetrics.ts` / `.test.ts`:summary 增加 `historicalMarkdownDeferredMax`,方便 Playwright summary 直接读取 Markdown hydrate 延后命中量。
已验证:
```bash
npm exec -- vitest run "src/components/agent/chat/components/MessageList.test.tsx" -t "旧会话首帧应延后历史助手 contentParts 与 Markdown 细节扫描|已分页旧会话首帧应只把尾部相关 turns 的 threadItems 纳入计算|旧会话消息较少但执行过程很多时也应延后构建 timeline|旧会话里的长助手回复应先展示纯文本预览"
npm exec -- vitest run "src/lib/agentUiPerformanceMetrics.test.ts"
npx eslint "src/components/agent/chat/components/MessageList.tsx" "src/components/agent/chat/components/MessageList.test.tsx" "src/lib/agentUiPerformanceMetrics.ts" "src/lib/agentUiPerformanceMetrics.test.ts" --max-warnings 0
npm run bridge:health -- --timeout-ms 5000
```
结果:
- MessageList 定向回归:通过,`4` 个测试通过。
- 性能指标汇总单测:通过,`2` 个测试通过。
- ESLint touched files:通过。
- DevBridge 健康检查:失败,`http://127.0.0.1:3030/health` 未监听。
- TypeScript:本轮未追加全量 typecheck;本机已有其它 `tsc --noEmit` 高 CPU 进程在运行,避免重复启动。
下一步:
1. DevBridge 恢复后复测真实旧会话,读取 `historicalMarkdownDeferredMax`、`historicalContentPartsDeferredMax`、`threadItemsScanDeferredCount`、`clickToMessageListPaintMs`、`longTaskMaxMs`。
2. 若首帧仍慢,下一刀优先看 `visibleMessages.filter` / `buildMessageTurnGroups` 是否需要基于 sessionId 做更强 memo 或窗口化。
3. 若首帧已快但 idle 后出现卡顿,把 Markdown hydrate / timeline hydrate 拆成分批 idle,而不是一次性恢复完整历史。
@@ -688,6 +688,22 @@ fn build_success_criteria(
"未命名合同"
)
));
let execution_profile_keys =
modality_contract_execution_profile_keys(modality_runtime_contracts);
if !execution_profile_keys.is_empty() {
criteria.push(format!(
"回放必须保留 execution profile 决策输入:{}。",
format_text_list(&execution_profile_keys, "未记录 execution profile")
));
}
let executor_adapter_keys =
modality_contract_executor_adapter_keys(modality_runtime_contracts);
if !executor_adapter_keys.is_empty() {
criteria.push(format!(
"回放必须保留 executor adapter 绑定:{},不能退回自由工具选择或旧 CLI 旁路。",
format_text_list(&executor_adapter_keys, "未记录 executor adapter")
));
}
}
if modality_contract_has_browser_control(modality_runtime_contracts) {
criteria.push(
@@ -914,6 +930,21 @@ fn build_modality_contract_checks(modality_runtime_contracts: &Value) -> Vec<Str
format_text_list(&assessment_sources, "未记录判定来源")
));
}
let execution_profile_keys =
modality_contract_execution_profile_keys(modality_runtime_contracts);
if !execution_profile_keys.is_empty() {
checks.push(format!(
"确认 replay 保留 execution profile:{},用于解释模型角色、权限与 LimeCore policy 合并输入。",
format_text_list(&execution_profile_keys, "未记录 execution profile")
));
}
let executor_adapter_keys = modality_contract_executor_adapter_keys(modality_runtime_contracts);
if !executor_adapter_keys.is_empty() {
checks.push(format!(
"确认 replay 保留 executor adapter:{},用于解释真实执行器绑定、产物输出与失败映射。",
format_text_list(&executor_adapter_keys, "未记录 executor adapter")
));
}
if modality_contract_has_routing_block(modality_runtime_contracts) {
checks.push(format!(
@@ -1086,6 +1117,12 @@ fn infer_replay_suite_tags(
for contract_key in modality_contract_keys(modality_runtime_contracts) {
push_unique_owned_tag(&mut tags, format!("modality-{contract_key}"));
}
if !modality_contract_execution_profile_keys(modality_runtime_contracts).is_empty() {
push_unique_text_tag(&mut tags, "execution-profile");
}
if !modality_contract_executor_adapter_keys(modality_runtime_contracts).is_empty() {
push_unique_text_tag(&mut tags, "executor-adapter");
}
}
if modality_contract_has_routing_block(modality_runtime_contracts) {
@@ -1309,6 +1346,48 @@ fn modality_contract_failure_codes(modality_runtime_contracts: &Value) -> Vec<St
collect_unique_snapshot_strings(modality_runtime_contracts, "failureCode")
}
fn modality_contract_execution_profile_keys(modality_runtime_contracts: &Value) -> Vec<String> {
let mut values =
collect_unique_snapshot_strings(modality_runtime_contracts, "executionProfileKey");
collect_unique_snapshot_strings_into(
modality_runtime_contracts,
"execution_profile_key",
&mut values,
);
collect_unique_string_array_at_pointer(
modality_runtime_contracts,
"/snapshotIndex/executionProfileKeys",
&mut values,
);
collect_unique_string_array_at_pointer(
modality_runtime_contracts,
"/snapshot_index/execution_profile_keys",
&mut values,
);
values
}
fn modality_contract_executor_adapter_keys(modality_runtime_contracts: &Value) -> Vec<String> {
let mut values =
collect_unique_snapshot_strings(modality_runtime_contracts, "executorAdapterKey");
collect_unique_snapshot_strings_into(
modality_runtime_contracts,
"executor_adapter_key",
&mut values,
);
collect_unique_string_array_at_pointer(
modality_runtime_contracts,
"/snapshotIndex/executorAdapterKeys",
&mut values,
);
collect_unique_string_array_at_pointer(
modality_runtime_contracts,
"/snapshot_index/executor_adapter_keys",
&mut values,
);
values
}
fn modality_contract_has_browser_control(modality_runtime_contracts: &Value) -> bool {
modality_contract_snapshots(modality_runtime_contracts)
.iter()
@@ -1724,16 +1803,37 @@ fn collect_unique_snapshot_strings(
field_name: &str,
) -> Vec<String> {
let mut values = Vec::new();
collect_unique_snapshot_strings_into(modality_runtime_contracts, field_name, &mut values);
values
}
fn collect_unique_snapshot_strings_into(
modality_runtime_contracts: &Value,
field_name: &str,
values: &mut Vec<String>,
) {
for snapshot in modality_contract_snapshots(modality_runtime_contracts) {
if let Some(value) = snapshot
.get(field_name)
.and_then(Value::as_str)
.and_then(|value| normalize_optional_text(Some(value.to_string())))
{
push_unique_owned_tag(&mut values, value);
push_unique_owned_tag(values, value);
}
}
}
fn collect_unique_string_array_at_pointer(value: &Value, pointer: &str, values: &mut Vec<String>) {
if let Some(items) = value.pointer(pointer).and_then(Value::as_array) {
for item in items {
if let Some(value) = item
.as_str()
.and_then(|value| normalize_optional_text(Some(value.to_string())))
{
push_unique_owned_tag(values, value);
}
}
}
values
}
fn format_text_list(values: &[String], fallback: &str) -> String {
@@ -3253,6 +3353,12 @@ mod tests {
"executor_binding": {
"executor_kind": "skill",
"binding_key": "research"
},
"execution_profile": {
"profile_key": "web_research_profile"
},
"executor_adapter": {
"adapter_key": "skill:research"
}
},
"entry_source": "at_report_command"
@@ -3296,12 +3402,42 @@ mod tests {
.and_then(Value::as_str),
Some("executor_invoked")
);
assert_eq!(
input
.pointer("/runtimeContext/modalityRuntimeContracts/snapshots/0/executionProfileKey")
.and_then(Value::as_str),
Some("web_research_profile")
);
assert_eq!(
input
.pointer("/runtimeContext/modalityRuntimeContracts/snapshots/0/executorAdapterKey")
.and_then(Value::as_str),
Some("skill:research")
);
assert_eq!(
input
.pointer(
"/runtimeContext/modalityRuntimeContracts/snapshotIndex/executionProfileKeys/0",
)
.and_then(Value::as_str),
Some("web_research_profile")
);
assert_eq!(
input
.pointer(
"/runtimeContext/modalityRuntimeContracts/snapshotIndex/executorAdapterKeys/0",
)
.and_then(Value::as_str),
Some("skill:research")
);
let suite_tags = input
.pointer("/classification/suiteTags")
.and_then(Value::as_array)
.expect("suite tags");
for expected_tag in [
"modality-web_research",
"execution-profile",
"executor-adapter",
"web-research",
"research-skill",
"web-research-trace",
@@ -3317,12 +3453,16 @@ mod tests {
assert!(expected.contains("Skill(report_generate)"));
assert!(expected.contains("search_query"));
assert!(expected.contains("lime_site_*"));
assert!(expected.contains("web_research_profile"));
assert!(expected.contains("skill:research"));
assert!(expected.contains("model_memory_only_answer"));
assert!(expected.contains("local_file_search_before_research_skill"));
assert!(expected.contains("\"requiresHumanReview\": false"));
let grader = fs::read_to_string(grader_path).expect("grader");
assert!(grader.contains("多模态运行合同检查"));
assert!(grader.contains("web_research_profile"));
assert!(grader.contains("skill:research"));
assert!(grader.contains("Skill(research)"));
assert!(grader.contains("Skill(report_generate)"));
assert!(grader.contains("model_memory_only_answer"));
@@ -3337,6 +3477,18 @@ mod tests {
.and_then(Value::as_str),
Some("web_research_skill_trace.modality_runtime_contract")
);
assert_eq!(
links
.pointer("/modalityRuntimeContracts/snapshotIndex/executionProfileKeys/0")
.and_then(Value::as_str),
Some("web_research_profile")
);
assert_eq!(
links
.pointer("/modalityRuntimeContracts/snapshotIndex/executorAdapterKeys/0")
.and_then(Value::as_str),
Some("skill:research")
);
}
#[test]