修复: 飞书交互批: 推送汇总卡/伪受理/回查/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:
@@ -260,7 +260,7 @@ async fn handle_card_action(events: Sender<InboundEvent>, action: larksuite_oapi
|
||||
|
||||
/// 构造审批卡片(Card JSON 1.0):标题 + 详情 + 同意/拒绝按钮
|
||||
fn build_approval_card(approval_id: &str, title: &str, detail: &str) -> Card {
|
||||
let detail_md = format!("{}\n\n**审批 ID**: `{approval_id}`", escape_md(detail));
|
||||
let detail_md = format!("{}\n\n**审批 ID**: `{approval_id}`", md_to_lark_friendly(detail));
|
||||
let to_json_value = |v: serde_json::Value| -> larksuite_oapi_sdk_rs::JsonValue {
|
||||
serde_json::from_value(serde_json::to_value(v).expect("serde_json::Value 可逆"))
|
||||
.expect("按钮 value 结构兼容")
|
||||
@@ -285,19 +285,54 @@ fn build_approval_card(approval_id: &str, title: &str, detail: &str) -> Card {
|
||||
.element(Element::Action(actions))
|
||||
}
|
||||
|
||||
/// Markdown 转义(飞书卡片 lark_md 语法)
|
||||
fn escape_md(s: &str) -> String {
|
||||
s.replace('\\', "\\\\")
|
||||
.replace('*', "\\*")
|
||||
.replace('_', "\\_")
|
||||
.replace('`', "\\`")
|
||||
.replace('[', "\\[")
|
||||
.replace(']', "\\]")
|
||||
.replace('<', "\\<")
|
||||
.replace('>', "\\>")
|
||||
.replace('(', "\\(")
|
||||
.replace(')', "\\)")
|
||||
.replace('#', "\\#")
|
||||
/// 出站 Markdown 净化(飞书卡片 lark_md 语法适配)。
|
||||
///
|
||||
/// AI 产出常带标准 Markdown 结构,lark_md 不支持标题与表格语法,原样注入按字面量渲染。
|
||||
/// 规则(与 src-tauri feishu_channel.rs 侧 md_to_lark_friendly 同规则互引,双端出站口径一致):
|
||||
/// 标题行(行首一个以上 #)转粗体行;表格行(| 开头)转列表行,表头分隔行整行跳过;
|
||||
/// 代码块围栏内外均保留原样(转义或改写破坏代码展示);其余行原样透传(lark_md 原生
|
||||
/// 支持粗体/链接/行内代码,不额外转义)。
|
||||
fn md_to_lark_friendly(s: &str) -> String {
|
||||
let mut out = String::with_capacity(s.len());
|
||||
let mut in_code = false;
|
||||
for line in s.lines() {
|
||||
if line.trim_start().starts_with("```") {
|
||||
in_code = !in_code;
|
||||
out.push_str(line);
|
||||
out.push('\n');
|
||||
continue;
|
||||
}
|
||||
if in_code {
|
||||
out.push_str(line);
|
||||
out.push('\n');
|
||||
continue;
|
||||
}
|
||||
let trimmed = line.trim_start();
|
||||
if trimmed.starts_with('#') {
|
||||
let title = trimmed.trim_start_matches('#').trim();
|
||||
if !title.is_empty() {
|
||||
out.push_str(&format!("**{title}**\n"));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if trimmed.starts_with('|') {
|
||||
let cells: Vec<&str> = trimmed.trim_matches('|').split('|').map(str::trim).collect();
|
||||
// 表头分隔行(|---|:---|)无内容语义,整行跳过
|
||||
if cells.iter().all(|c| c.is_empty() || c.chars().all(|ch| ch == '-' || ch == ':')) {
|
||||
continue;
|
||||
}
|
||||
let shown: Vec<&str> = cells.into_iter().filter(|c| !c.is_empty()).collect();
|
||||
out.push_str(&format!("- {}\n", shown.join(" · ")));
|
||||
continue;
|
||||
}
|
||||
out.push_str(line);
|
||||
out.push('\n');
|
||||
}
|
||||
// lines() 逐行补换行,源文本不以换行结尾时尾部多出一个,与源对齐
|
||||
if !s.ends_with('\n') {
|
||||
out.pop();
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -349,11 +384,27 @@ mod tests {
|
||||
assert!(approved_flags.contains(&true) && approved_flags.contains(&false));
|
||||
}
|
||||
|
||||
/// Markdown 转义:卡片详情中的 markdown 语法字符被转义
|
||||
/// 出站净化:标题转粗体行,表格行转列表行,表头分隔行跳过
|
||||
#[test]
|
||||
fn escape_md_works() {
|
||||
assert_eq!(escape_md("a*b_c`d\\e"), "a\\*b\\_c\\`d\\\\e");
|
||||
// 走查 #7:链接/标题/引用语法字符也须转义,防用户内容注入卡片结构
|
||||
assert_eq!(escape_md("[x](http://e)#h <b>(y)"), "\\[x\\]\\(http://e\\)\\#h \\<b\\>\\(y\\)");
|
||||
fn lark_friendly_converts_headings_and_tables() {
|
||||
let md = "# 标题\n| a | b |\n|---|---|\n| 1 | 2 |";
|
||||
assert_eq!(md_to_lark_friendly(md), "**标题**\n- a · b\n- 1 · 2");
|
||||
}
|
||||
|
||||
/// 出站净化:代码围栏内外均保留原样,围栏外的标题照常改写
|
||||
#[test]
|
||||
fn lark_friendly_preserves_code_fence() {
|
||||
let md = "前文\n```\n# 不是标题\n| 不 | 是表格 |\n```\n## 后文";
|
||||
assert_eq!(
|
||||
md_to_lark_friendly(md),
|
||||
"前文\n```\n# 不是标题\n| 不 | 是表格 |\n```\n**后文**"
|
||||
);
|
||||
}
|
||||
|
||||
/// 出站净化:普通行原样透传(lark_md 原生支持粗体/链接,不额外转义)
|
||||
#[test]
|
||||
fn lark_friendly_passes_plain_lines() {
|
||||
let md = "**粗** 与 [链接](http://e)\n普通行";
|
||||
assert_eq!(md_to_lark_friendly(md), md);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user