新增: df-im-gateway 网关骨架
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
//! 会话映射存储(IM chat_id ↔ DevFlow conv_id,SQLite + WAL)
|
||||
//!
|
||||
//! 建表即带 owner_user_id/platform 列(P0 预留项 ⑤,当前恒默认值,
|
||||
//! S2 多人场景免迁表)。
|
||||
//!
|
||||
//! 表结构:
|
||||
//! ```sql
|
||||
//! CREATE TABLE IF NOT EXISTS session_map (
|
||||
//! platform TEXT NOT NULL, -- feishu/wecom/dingtalk/web
|
||||
//! chat_id TEXT NOT NULL, -- IM 侧会话(单聊 user_id / 群聊 chat_id)
|
||||
//! conv_id TEXT NOT NULL, -- DevFlow 会话 ID
|
||||
//! owner_user_id TEXT NOT NULL DEFAULT '',-- 预留 S2 多人归属(当前恒 '')
|
||||
//! channel_id TEXT NOT NULL DEFAULT '',-- 路由到的桌面端 channel(预留项 ⑥)
|
||||
//! created_at INTEGER NOT NULL,
|
||||
//! updated_at INTEGER NOT NULL,
|
||||
//! PRIMARY KEY (platform, chat_id)
|
||||
//! );
|
||||
//! ```
|
||||
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use rusqlite::Connection;
|
||||
|
||||
/// 单条会话映射(字段预留 S2 多人/多桌面端,当前核心只读 conv_id)
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(dead_code)]
|
||||
pub struct SessionMap {
|
||||
pub platform: String,
|
||||
pub chat_id: String,
|
||||
pub conv_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub channel_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SessionStore {
|
||||
conn: Arc<std::sync::Mutex<Connection>>,
|
||||
}
|
||||
|
||||
impl SessionStore {
|
||||
/// 打开(或创建)库文件并确保表存在。WAL 模式(与桌面端一致)。
|
||||
pub fn open(path: &Path) -> anyhow::Result<Self> {
|
||||
let conn = Connection::open(path)?;
|
||||
conn.pragma_update(None, "journal_mode", "WAL")?;
|
||||
conn.execute_batch(
|
||||
"CREATE TABLE IF NOT EXISTS session_map (
|
||||
platform TEXT NOT NULL,
|
||||
chat_id TEXT NOT NULL,
|
||||
conv_id TEXT NOT NULL,
|
||||
owner_user_id TEXT NOT NULL DEFAULT '',
|
||||
channel_id TEXT NOT NULL DEFAULT '',
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
PRIMARY KEY (platform, chat_id)
|
||||
);",
|
||||
)?;
|
||||
Ok(Self { conn: Arc::new(std::sync::Mutex::new(conn)) })
|
||||
}
|
||||
|
||||
/// 内存库(测试/演示)
|
||||
#[allow(dead_code)]
|
||||
pub fn open_in_memory() -> anyhow::Result<Self> {
|
||||
let conn = Connection::open_in_memory()?;
|
||||
conn.execute_batch(
|
||||
"CREATE TABLE IF NOT EXISTS session_map (
|
||||
platform TEXT NOT NULL, chat_id TEXT NOT NULL, conv_id TEXT NOT NULL,
|
||||
owner_user_id TEXT NOT NULL DEFAULT '', channel_id TEXT NOT NULL DEFAULT '',
|
||||
created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL,
|
||||
PRIMARY KEY (platform, chat_id)
|
||||
);",
|
||||
)?;
|
||||
Ok(Self { conn: Arc::new(std::sync::Mutex::new(conn)) })
|
||||
}
|
||||
|
||||
/// 查 IM 会话映射的 conv_id(未映射返回 None)
|
||||
pub fn get_conv(&self, platform: &str, chat_id: &str) -> Option<SessionMap> {
|
||||
let conn = self.conn.lock().ok()?;
|
||||
conn.query_row(
|
||||
"SELECT platform, chat_id, conv_id, owner_user_id, channel_id FROM session_map \
|
||||
WHERE platform = ?1 AND chat_id = ?2",
|
||||
[platform, chat_id],
|
||||
|row| {
|
||||
Ok(SessionMap {
|
||||
platform: row.get(0)?,
|
||||
chat_id: row.get(1)?,
|
||||
conv_id: row.get(2)?,
|
||||
owner_user_id: row.get(3)?,
|
||||
channel_id: row.get(4)?,
|
||||
})
|
||||
},
|
||||
)
|
||||
.ok()
|
||||
}
|
||||
|
||||
/// 写/更新映射(upsert)
|
||||
pub fn set_conv(&self, platform: &str, chat_id: &str, conv_id: &str, channel_id: &str) -> anyhow::Result<()> {
|
||||
let conn = self.conn.lock().map_err(|e| anyhow::anyhow!("store 锁中毒: {e}"))?;
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis() as i64)
|
||||
.unwrap_or(0);
|
||||
conn.execute(
|
||||
"INSERT INTO session_map (platform, chat_id, conv_id, owner_user_id, channel_id, created_at, updated_at) \
|
||||
VALUES (?1, ?2, ?3, '', ?4, ?5, ?5) \
|
||||
ON CONFLICT(platform, chat_id) DO UPDATE SET conv_id = ?3, channel_id = ?4, updated_at = ?5",
|
||||
rusqlite::params![platform, chat_id, conv_id, channel_id, now],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 删除映射(桌面端会话已删,悬空清理)
|
||||
pub fn remove(&self, platform: &str, chat_id: &str) -> anyhow::Result<()> {
|
||||
let conn = self.conn.lock().map_err(|e| anyhow::anyhow!("store 锁中毒: {e}"))?;
|
||||
conn.execute("DELETE FROM session_map WHERE platform = ?1 AND chat_id = ?2", [platform, chat_id])?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn upsert_and_get() {
|
||||
let store = SessionStore::open_in_memory().unwrap();
|
||||
store.set_conv("feishu", "oc_123", "conv_abc", "chan-work").unwrap();
|
||||
let m = store.get_conv("feishu", "oc_123").unwrap();
|
||||
assert_eq!(m.conv_id, "conv_abc");
|
||||
assert_eq!(m.channel_id, "chan-work");
|
||||
assert_eq!(m.owner_user_id, "");
|
||||
|
||||
// upsert 覆盖
|
||||
store.set_conv("feishu", "oc_123", "conv_new", "chan-home").unwrap();
|
||||
let m = store.get_conv("feishu", "oc_123").unwrap();
|
||||
assert_eq!(m.conv_id, "conv_new");
|
||||
|
||||
// 删除后悬空
|
||||
store.remove("feishu", "oc_123").unwrap();
|
||||
assert!(store.get_conv("feishu", "oc_123").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn platform_isolated() {
|
||||
let store = SessionStore::open_in_memory().unwrap();
|
||||
store.set_conv("feishu", "c1", "conv1", "ch").unwrap();
|
||||
store.set_conv("wecom", "c1", "conv2", "ch").unwrap();
|
||||
assert_eq!(store.get_conv("feishu", "c1").unwrap().conv_id, "conv1");
|
||||
assert_eq!(store.get_conv("wecom", "c1").unwrap().conv_id, "conv2");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user