新增: 无人值守安全系列(H1-H3) + P5 审计日志与部署运维 + #14 异步委托
- H3 外部干预感知: run_agentic_loop 增 h3_watch_dir 参数, loop 启动取 git HEAD 基线, 每 5 轮探测, HEAD 被外部改写(切分支/reset/他话提交)即 emit_task_parked(stop_kind=external_change) + teardown 停靠; 4 调用方(发送/编辑/强制发送/审批续跑)从 allowed_dirs.persistent 排序取首目录, 修复初版 current_dir 恒空缺陷 - P5 审计日志: df-im-gateway audit.rs append-only SQLite(tunnel_audit 表, payload 4KB 截断, recent/by_request 查询), main.rs 8 打点点位(inbound/command/outbound), 写失败不阻断主链路, 17 tests passed - P5 部署运维: publish-im-gateway.ps1(musl 交叉编译→scp .new→systemd 原子切换→9191 验证→自动回滚) + systemd unit + nginx im.1216.top 反代 + 运维手册 - #14 异步委托: 受理/完成两段式 + 桌面任务卡(后台进行中徽章/轮次/切换) + 飞书双端同构, 关联缺陷 #8-#12 速清 验收: cargo check workspace EXIT 0, cargo test workspace 全绿(711+560+33+12...), vue-tsc EXIT 0
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
//! ```
|
||||
|
||||
mod adapters;
|
||||
mod audit;
|
||||
mod commander;
|
||||
mod config;
|
||||
mod feishu;
|
||||
@@ -43,6 +44,8 @@ async fn main() -> anyhow::Result<()> {
|
||||
.unwrap_or_else(|_| PathBuf::from("data"));
|
||||
std::fs::create_dir_all(&data_dir)?;
|
||||
let store = SessionStore::open(&data_dir.join("sessions.db"))?;
|
||||
// 审计日志(P5):IM 消息 ↔ TunnelCommand 全量记录,写失败不阻断主链路
|
||||
let audit = audit::AuditLog::open(&data_dir.join("audit.db"))?;
|
||||
|
||||
info_banner(&cfg);
|
||||
|
||||
@@ -153,11 +156,25 @@ async fn main() -> anyhow::Result<()> {
|
||||
let tail = throttle.finish(&conv_id);
|
||||
let full = tail.unwrap_or_default();
|
||||
let idx = platform_of(&conv_id);
|
||||
audit.record(audit::AuditEntry {
|
||||
direction: audit::Direction::Outbound,
|
||||
platform: String::new(), chat_id: conv_id.clone(),
|
||||
user_id: String::new(), conv_id: conv_id.clone(),
|
||||
action: "completed".into(), request_id: String::new(),
|
||||
payload: full.clone(), outcome: "ok".into(),
|
||||
});
|
||||
let _ = adapters[idx]
|
||||
.dispatch(OutboundAction::StreamText { chat_id: conv_id, text: full, done: true })
|
||||
.await;
|
||||
}
|
||||
df_tunnel::TunnelEvent::Approval { conv_id, approval_id, description } => {
|
||||
audit.record(audit::AuditEntry {
|
||||
direction: audit::Direction::Outbound,
|
||||
platform: String::new(), chat_id: conv_id.clone(),
|
||||
user_id: String::new(), conv_id: conv_id.clone(),
|
||||
action: "approval_card".into(), request_id: approval_id.clone(),
|
||||
payload: description.clone(), outcome: "ok".into(),
|
||||
});
|
||||
let idx = platform_of(&conv_id);
|
||||
let _ = adapters[idx]
|
||||
.dispatch(OutboundAction::ApprovalCard {
|
||||
@@ -182,12 +199,26 @@ async fn main() -> anyhow::Result<()> {
|
||||
// 回复适配器:按来源平台回路由
|
||||
let reply_idx = platform_of(&platform);
|
||||
if !cfg_loop.whitelist.is_empty() && !cfg_loop.whitelist.contains(&user_id) {
|
||||
audit.record(audit::AuditEntry {
|
||||
direction: audit::Direction::Inbound,
|
||||
platform: platform.clone(), chat_id: chat_id.clone(),
|
||||
user_id: user_id.clone(), conv_id: String::new(),
|
||||
action: "message".into(), request_id: String::new(),
|
||||
payload: text.clone(), outcome: "denied".into(),
|
||||
});
|
||||
let _ = adapters[reply_idx]
|
||||
.dispatch(OutboundAction::Notice { chat_id, text: "⛔ 你不在白名单内".into() })
|
||||
.await;
|
||||
continue;
|
||||
}
|
||||
let Some(channel_id) = cfg_loop.route(&text) else {
|
||||
audit.record(audit::AuditEntry {
|
||||
direction: audit::Direction::Inbound,
|
||||
platform: platform.clone(), chat_id: chat_id.clone(),
|
||||
user_id: user_id.clone(), conv_id: String::new(),
|
||||
action: "message".into(), request_id: String::new(),
|
||||
payload: text.clone(), outcome: "error".into(),
|
||||
});
|
||||
let _ = adapters[reply_idx]
|
||||
.dispatch(OutboundAction::Notice { chat_id, text: "⚠️ 无可用 channel".into() })
|
||||
.await;
|
||||
@@ -202,14 +233,45 @@ async fn main() -> anyhow::Result<()> {
|
||||
// 走查 #3:新建会话时清理流式残留(上轮异常中止无
|
||||
// Completed 事件,streams entry 永驻会错覆写旧消息)
|
||||
adapters[reply_idx].reset_stream(&chat_id).await;
|
||||
audit.record(audit::AuditEntry {
|
||||
direction: audit::Direction::Inbound,
|
||||
platform: platform.clone(), chat_id: chat_id.clone(),
|
||||
user_id: user_id.clone(), conv_id: conv.clone(),
|
||||
action: "new_session".into(), request_id: String::new(),
|
||||
payload: String::new(), outcome: "ok".into(),
|
||||
});
|
||||
conv
|
||||
}
|
||||
};
|
||||
// 入站消息审计:request_id 用 conv+毫秒派生,出站 Completed 事件无法
|
||||
// 回带,此处以 (direction=inbound,message) 为链头,command/outbound
|
||||
// 侧用同一 conv_id 关联(recent 按时间倒序即还原会话时间线)。
|
||||
audit.record(audit::AuditEntry {
|
||||
direction: audit::Direction::Inbound,
|
||||
platform: platform.clone(), chat_id: chat_id.clone(),
|
||||
user_id: user_id.clone(), conv_id: conv_id.clone(),
|
||||
action: "message".into(), request_id: String::new(),
|
||||
payload: text.clone(), outcome: "ok".into(),
|
||||
});
|
||||
audit.record(audit::AuditEntry {
|
||||
direction: audit::Direction::Command,
|
||||
platform: platform.clone(), chat_id: chat_id.clone(),
|
||||
user_id: user_id.clone(), conv_id: conv_id.clone(),
|
||||
action: "send_cmd".into(), request_id: String::new(),
|
||||
payload: text.clone(), outcome: "ok".into(),
|
||||
});
|
||||
let _ = cmd_tx
|
||||
.send(df_tunnel::TunnelCommand::Send { conv_id, content: text })
|
||||
.await;
|
||||
}
|
||||
InboundEvent::NewSession { platform, chat_id } => {
|
||||
audit.record(audit::AuditEntry {
|
||||
direction: audit::Direction::Inbound,
|
||||
platform: platform.clone(), chat_id: chat_id.clone(),
|
||||
user_id: String::new(), conv_id: String::new(),
|
||||
action: "new_session_reset".into(), request_id: String::new(),
|
||||
payload: String::new(), outcome: "ok".into(),
|
||||
});
|
||||
let _ = store.remove(&platform, &chat_id);
|
||||
let reply_idx = platform_of(&platform);
|
||||
let _ = adapters[reply_idx]
|
||||
@@ -227,6 +289,19 @@ async fn main() -> anyhow::Result<()> {
|
||||
.get_conv(&platform, &chat_id)
|
||||
.map(|m| m.conv_id)
|
||||
.unwrap_or_else(|| format!("{platform}:{chat_id}"));
|
||||
audit.record(audit::AuditEntry {
|
||||
direction: audit::Direction::Inbound,
|
||||
platform: platform.clone(), chat_id: chat_id.clone(),
|
||||
user_id: String::new(), conv_id: conv_id.clone(),
|
||||
action: "approval_click".into(), request_id: approval_id.clone(),
|
||||
payload: if approved { "approved".into() } else { "rejected".into() },
|
||||
outcome: "ok".into(),
|
||||
}); audit.record(audit::AuditEntry {
|
||||
direction: audit::Direction::Command,
|
||||
platform, chat_id, user_id: String::new(), conv_id: conv_id.clone(),
|
||||
action: "approve_cmd".into(), request_id: approval_id.clone(), payload: if approved { "approved".into() } else { "rejected".into() },
|
||||
outcome: "ok".into(),
|
||||
});
|
||||
let _ = cmd_tx
|
||||
.send(df_tunnel::TunnelCommand::Approve { conv_id, approval_id, accepted: approved })
|
||||
.await;
|
||||
|
||||
Reference in New Issue
Block a user