修复: 飞书交互批: 推送汇总卡/伪受理/回查/watcher弃守/status/md净化

- 34 push_to_chat 滚动汇总卡: 同 chat 在途事务卡原地追加, ✅/❌ 终态另起新卡, 更新失败回退重绑; L3 过程不再逐条新开卡片刷屏
- 20 伪受理断层: 发票前过 can_accept_request 守卫, 不可启动回实况话术; inflight 移交 watcher 持有, 运行期计数不随 handler 返回归零
- 22 兜底回查限定受理时间戳边界(纯函数+单测), 缺时间戳视为边界外, 禁投旧回答冒充
- 23 watcher 消费本会话 AiError 视同终局投失败卡, 不再空挂 70min
- 26 话术改实况: 未受理(暂不入队)可稍后重发, 删假排队承诺
- 27 快任务兜底改受理卡发出后 8s 宽限窗判定, 替换入场恒假死分支
- 21 /status 命令桌面入口: ai_chat_send 拦截, 零 LLM 零 spawn 返回进度摘要
- 38 出站 md 净化: 标题转粗体/表格转列表/围栏保留, feishu_channel 与 df-im-gateway 同规则互引
- 受理卡符号对齐规范(任务开始用🚀, ✅仅完成)
- 验收: cargo check 零 error + feishu 13/df-im-gateway 19/chat 17 passed + vue-tsc 0
This commit is contained in:
lxy
2026-10-06 23:48:29 +08:00
parent 9520c198a2
commit 1ce1360a0b
4 changed files with 666 additions and 77 deletions
@@ -654,6 +654,14 @@ pub async fn ai_chat_send(
// None/空 → 无 @ mention(向后兼容,仅 / 技能走 skill 参数);非空 → 按 kind 投影成 Augmentation。
mention_spans: Option<Vec<MentionSpanDto>>,
) -> Result<String, String> {
// /status 进度查询命令(2026-10-06):双端话术引用 /status 但命令此前不存在,
// 发送即被当任务烧一轮 LLM。置于技能解析之前拦截,零 LLM 零 spawn。飞书渠道在
// feishu_channel.rs 受理前有同命令拦截,本分支补齐桌面入口。
if message.trim() == "/status" {
return reply_status_summary(&app, &state, conversation_id.clone())
.await
.map(|_| "ok".to_string());
}
// H1 根修:前导 `/技能名` 解析。此前技能唯一触发路是前端浮层选中(skill 参数),
// 用户直接打字 `/name xxx` 时整串当普通消息,技能注入零发生。
// 此处对照 skills_cached() 已扫技能名(含 commands/*.md 来源)匹配前导斜杠令牌,
@@ -692,6 +700,87 @@ pub async fn ai_chat_send(
.map(|_| "ok".to_string())
}
/// /status 进度查询(2026-10-06):命令拦截应答,零 LLM 零 spawn。
///
/// 读 conv_states 生命周期态 + per_conv 轮次/最近输出,拼几行摘要作为 assistant 消息落库。
/// 事件序列与常规轮收尾同契约(AiAgentRound 建泡 → AiTextDelta 填文 → AiCompleted 复位),
/// 缺完成事件前端 doSend 已置的 streaming 无法复位即悬挂。不迁移 ConvState(只读诊断),
/// 不写 working_context(/status 是探针不是任务目标)。
///
/// 会话定位与 spawn_agentic_session 同序(入参优先 → active);无会话/会话已删时 Err 透出
/// (前端 toast),零落库零事件。轮次/目标快照短锁读即释,不与 loop 写交叉。
async fn reply_status_summary(
app: &AppHandle,
state: &AppState,
conversation_id: Option<String>,
) -> Result<(), String> {
let target = match conversation_id.filter(|s| !s.is_empty()) {
Some(id) => id,
None => state
.ai_session
.lock()
.await
.active_conversation_id
.clone()
.ok_or_else(|| "📭 当前无会话,发送消息即自动创建。".to_string())?,
};
let (state_line, pinned_goals) = {
let cs = state.conv_states.get(&target);
let session = state.ai_session.lock().await;
let conv = session
.conv_read(&target)
.ok_or_else(|| "📭 会话不存在或已删除。".to_string())?;
let line = match cs {
ConvState::Generating => {
format!("🚀 任务执行中(已 {} 轮),完成后自动回填本会话。", conv.iteration_used)
}
ConvState::Compressed => "⏳ 上下文压缩中,完成后自动续跑。".to_string(),
ConvState::Stopping => "⏹ 收到停止指令,正在收尾退出。".to_string(),
ConvState::Error => "❌ 上次任务出错,处理后重发消息即可重试。".to_string(),
ConvState::Idle => "✅ 会话空闲,无进行中任务。".to_string(),
};
(line, conv.pinned_goals.clone())
};
// 最近输出预览(复用 agentic 收敛的末条 assistant 取数;空=尚无产出,首 token/工具执行中)
let recent = super::super::agentic::last_active_assistant_text(&state.ai_session, &target).await;
let recent_line = if recent.trim().is_empty() {
"最近输出:暂无".to_string()
} else {
let snippet: String = recent.chars().take(80).collect();
format!("最近输出:{snippet}")
};
let summary = format!("{state_line}\n{recent_line}");
// 问答成对落库(重开可见,LLM 历史连贯);save 增量路径与 loop 同 per-conv 锁串行
{
let mut session = state.ai_session.lock().await;
let conv = session.conv(&target);
conv.messages.push(ChatMessage::user("/status"));
conv.messages.push(ChatMessage::assistant(summary.clone()));
}
save_conversation(&state.ai_session, &state.db, &target, None, None, true).await;
// pinned_goals 原样透传防前端目标区回退(与 agentic completed_event 同口径)
emit_ai_chat_event(app, AiChatEvent::AiAgentRound { round: 1, conversation_id: Some(target.clone()) });
emit_ai_chat_event(app, AiChatEvent::AiTextDelta { delta: summary, conversation_id: Some(target.clone()) });
emit_ai_chat_event(
app,
AiChatEvent::AiCompleted {
total_tokens: 0,
prompt_tokens: 0,
completion_tokens: 0,
prompt_cache_hit_tokens: 0,
prompt_cache_miss_tokens: 0,
reasoning_tokens: 0,
is_estimated: false,
incomplete: None,
incomplete_reason: None,
conversation_id: Some(target),
pinned_goals,
},
);
Ok(())
}
/// 发送消息并启动 agentic 会话的共用核心(纯搬迁自 ai_chat_send,行为逐字等价)。
///
/// 调用方:ai_chat_send IPC(前端对话,pin_active=true)/ 工作流 AiAgentNode 编排会话