feat: 拆分 worker 架构并增强 assistant 安全保护

- 新增 daemon/channel/assistant/function/scheduler/logging worker 架构和 JetStream 消息协议

- 修复邮件通知误入 assistant、迁移 checksum、日志 subject 和工具调用无限循环风险

- 新增 assistant 工具轮次熔断、工具结果截断和提示词软收敛规则

- 补充邮件推送关键字、可信发件人配置、升级日志和验证测试
This commit is contained in:
2026-07-10 00:41:36 +08:00
parent 7deae286ca
commit af6cfdaa83
70 changed files with 5953 additions and 1608 deletions
+16
View File
@@ -0,0 +1,16 @@
[package]
name = "ias-logging"
version = "0.1.0"
edition = "2024"
[dependencies]
ias-common = { path = "../ias-common" }
anyhow = "1"
serde_json = "1"
sqlx = { version = "0.9.0", features = [
"runtime-tokio",
"tls-rustls",
"postgres",
"chrono",
] }
tokio = { version = "1.0", features = ["rt-multi-thread", "macros", "time"] }
+87
View File
@@ -0,0 +1,87 @@
use std::time::Duration;
use ias_common::mq::{
ConsumerSpec, JetStreamBus, JetStreamConfig, LogEventPayload, StreamKind, StreamNames,
};
use sqlx::PgPool;
pub async fn run_worker(pool: PgPool) -> anyhow::Result<()> {
let config = JetStreamConfig::from_env();
loop {
match run_once(pool.clone(), config.clone()).await {
Ok(()) => return Ok(()),
Err(error) => {
eprintln!("Logging Worker 连接或消费失败: {error:#}");
tokio::time::sleep(Duration::from_secs(5)).await;
}
}
}
}
async fn run_once(pool: PgPool, config: JetStreamConfig) -> anyhow::Result<()> {
let bus = JetStreamBus::connect(config.clone()).await?;
bus.ensure_streams().await?;
let names = StreamNames::new(config.stream_prefix.clone());
let spec = ConsumerSpec {
stream: names.name(StreamKind::Logs),
durable_name: "logging-worker".to_string(),
filter_subject: "logs.>".to_string(),
ack_wait: config.ack_wait(),
max_deliver: config.max_deliver,
};
bus.consume_envelopes::<LogEventPayload, _, _>(spec, move |envelope| {
let pool = pool.clone();
async move {
let payload = envelope.payload;
let metadata = serde_json::json!({
"trace_id": payload.trace_id,
"message_id": payload.message_id,
"command_id": payload.command_id,
"call_id": payload.call_id,
"fire_id": payload.fire_id,
"fields": payload.fields,
});
sqlx::query(
r#"
INSERT INTO ias_operation_logs (level, module, message, metadata)
VALUES ($1, $2, $3, $4)
"#,
)
.bind(payload.level)
.bind(payload.source)
.bind(payload.message)
.bind(metadata)
.execute(&pool)
.await?;
Ok(())
}
})
.await
}
#[cfg(test)]
mod tests {
use ias_common::mq::{LogEventPayload, MessageEnvelope, log_subject};
#[test]
fn log_event_serialization_preserves_trace_id_without_full_body_requirement() {
let payload = LogEventPayload::new(
"info",
"worker.test",
"状态变化",
"trace-1",
serde_json::json!({"message_id": "msg-1"}),
);
let subject = log_subject("worker.test");
let envelope = MessageEnvelope::new(
subject.as_str(),
payload.event_id.clone(),
payload.trace_id.clone(),
payload,
);
let encoded = serde_json::to_string(&envelope).unwrap();
assert!(encoded.contains("trace-1"));
assert!(encoded.contains("worker.test"));
}
}