Files
isphere-ai-bridge/native/MsgLibReadSidecar/Program.cs
2026-07-10 03:02:16 +08:00

508 lines
25 KiB
C#

using System;
using System.Collections.Generic;
using System.Data.Common;
using System.IO;
using System.Reflection;
namespace MsgLibReadSidecar
{
internal static class Program
{
private const string Version = "0.2.0";
private const string DefaultPassword = "123";
private static readonly HashSet<string> DefaultTargetTables = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"TD_Roster", "TD_CustomEffigy", "tblRecent", "tblChatLevel", "tblPersonMsg", "tblMsgGroupPersonMsg",
"TD_WorkGroupAuth", "TD_ReceiveFileRecord", "TD_SendFileRecord", "TD_SystemMessageRecord", "TD_MsgReadBurn"
};
[STAThread]
private static int Main(string[] args)
{
string requestText = Console.In.ReadToEnd();
Dictionary<string, object> request;
string requestId = "";
string op = "";
if (!SidecarProtocol.TryParseRequest(requestText, out request, out requestId, out op))
{
Console.WriteLine(SidecarProtocol.ToJson(SidecarProtocol.Failure(requestId, op, "INVALID_REQUEST", "request must be a valid isphere.msglib.v1 JSON object")));
return 0;
}
try
{
Dictionary<string, object> opArgs = SidecarProtocol.GetObject(request, "args");
Dictionary<string, object> response;
switch (op)
{
case "self_check":
response = SidecarProtocol.Success(requestId, op, SelfCheckData());
break;
case "schema_summary":
response = SidecarProtocol.Success(requestId, op, SchemaSummary(opArgs, false));
break;
case "display_sources":
response = SidecarProtocol.Success(requestId, op, SchemaSummary(opArgs, true));
break;
case "display_entities":
response = SidecarProtocol.Success(requestId, op, DisplayEntities(opArgs));
break;
default:
response = SidecarProtocol.Failure(requestId, op, "UNSUPPORTED_OP", "unsupported op: " + op);
break;
}
Console.WriteLine(SidecarProtocol.ToJson(response));
return 0;
}
catch (Exception ex)
{
Console.Error.WriteLine(ex.ToString());
Console.WriteLine(SidecarProtocol.ToJson(SidecarProtocol.Failure(requestId, op, "SIDECAR_FAILED", ex.Message)));
return 0;
}
}
private static Dictionary<string, object> SelfCheckData()
{
return new Dictionary<string, object>
{
{ "sidecar_name", "MsgLibReadSidecar" },
{ "sidecar_version", Version },
{ "protocol", SidecarProtocol.Protocol },
{ "runtime", ".NET Framework " + Environment.Version },
{ "process_bits", IntPtr.Size * 8 },
{ "requires_process_bits", 32 },
{ "read_only", true },
{ "mutates_db", false },
{ "db_mutation_allowed", false },
{ "message_body_reads_allowed", false }
};
}
private static Dictionary<string, object> SchemaSummary(Dictionary<string, object> args, bool displayOnly)
{
string sqliteDllPath = SidecarProtocol.GetString(args, "sqlite_dll_path", "");
string dbPath = SidecarProtocol.GetString(args, "db_path", "");
string password = SidecarProtocol.GetString(args, "password", DefaultPassword);
bool includeCounts = SidecarProtocol.GetBool(args, "include_counts", true);
int maxTables = Clamp(SidecarProtocol.GetInt(args, "max_tables", 128), 1, 256);
List<string> requestedTargets = SidecarProtocol.GetStringList(args, "target_tables");
HashSet<string> targetTables = requestedTargets.Count == 0 ? DefaultTargetTables : new HashSet<string>(requestedTargets, StringComparer.OrdinalIgnoreCase);
ValidateInputs(sqliteDllPath, dbPath);
var result = new Dictionary<string, object>
{
{ "metadata_only", true },
{ "read_only", true },
{ "message_body_values_returned", false },
{ "sqlite_dll_path", sqliteDllPath },
{ "db_path", dbPath },
{ "provider", "System.Data.SQLite" },
{ "process_bits", IntPtr.Size * 8 }
};
using (DbConnection connection = OpenSqliteConnection(sqliteDllPath, dbPath, password))
{
connection.Open();
TryExecuteNonQuery(connection, "PRAGMA query_only=ON");
result["sqlite_version"] = ExecuteScalar(connection, "select sqlite_version()");
List<Dictionary<string, object>> tables = ReadMasterTables(connection, maxTables);
List<Dictionary<string, object>> columns = ReadColumns(connection, tables, displayOnly ? targetTables : null);
result["tables"] = tables;
result["columns"] = columns;
result["target_row_counts"] = includeCounts ? ReadTargetCounts(connection, tables, targetTables) : new List<Dictionary<string, object>>();
if (displayOnly)
{
result["display_sources"] = BuildDisplaySources(tables, columns);
}
}
return result;
}
private static Dictionary<string, object> DisplayEntities(Dictionary<string, object> args)
{
string sqliteDllPath = SidecarProtocol.GetString(args, "sqlite_dll_path", "");
string dbPath = SidecarProtocol.GetString(args, "db_path", "");
string password = SidecarProtocol.GetString(args, "password", DefaultPassword);
string entityType = SidecarProtocol.GetString(args, "entity_type", "").Trim().ToLowerInvariant();
string query = SidecarProtocol.GetString(args, "query", "").Trim();
int limit = Clamp(SidecarProtocol.GetInt(args, "limit", 25), 1, 100);
if (!string.Equals(entityType, "contacts", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(entityType, "groups", StringComparison.OrdinalIgnoreCase))
{
throw new ArgumentException("entity_type must be contacts or groups");
}
ValidateInputs(sqliteDllPath, dbPath);
var result = new Dictionary<string, object>
{
{ "metadata_only", true },
{ "read_only", true },
{ "message_body_values_returned", false },
{ "raw_rows_returned", false },
{ "entity_type", entityType },
{ "limit", limit },
{ "provider", "System.Data.SQLite" },
{ "process_bits", IntPtr.Size * 8 }
};
using (DbConnection connection = OpenSqliteConnection(sqliteDllPath, dbPath, password))
{
connection.Open();
TryExecuteNonQuery(connection, "PRAGMA query_only=ON");
List<Dictionary<string, object>> tables = ReadMasterTables(connection, 256);
HashSet<string> entityTables = BuildDisplayEntityTableSet(entityType);
List<Dictionary<string, object>> columns = ReadColumns(connection, tables, entityTables);
Dictionary<string, HashSet<string>> columnMap = BuildColumnMap(columns);
List<Dictionary<string, object>> entities = ReadDisplayEntities(connection, entityType, query, limit, columnMap);
result["entities"] = entities;
result["result_count"] = entities.Count;
}
return result;
}
private static void ValidateInputs(string sqliteDllPath, string dbPath)
{
if (string.IsNullOrWhiteSpace(sqliteDllPath) || !File.Exists(sqliteDllPath))
{
throw new ArgumentException("sqlite_dll_path must point to an existing System.Data.SQLite.dll");
}
if (string.IsNullOrWhiteSpace(dbPath) || !File.Exists(dbPath))
{
throw new ArgumentException("db_path must point to an existing copied MsgLib.db");
}
if (!string.Equals(Path.GetFileName(dbPath), "MsgLib.db", StringComparison.OrdinalIgnoreCase))
{
throw new ArgumentException("db_path must point to a copied MsgLib.db file");
}
}
private static DbConnection OpenSqliteConnection(string sqliteDllPath, string dbPath, string password)
{
Assembly assembly = Assembly.LoadFrom(sqliteDllPath);
Type builderType = assembly.GetType("System.Data.SQLite.SQLiteConnectionStringBuilder", true);
object builder = Activator.CreateInstance(builderType);
SetPropertyIfExists(builderType, builder, "DataSource", dbPath);
SetPropertyIfExists(builderType, builder, "Password", password);
SetPropertyIfExists(builderType, builder, "ReadOnly", true);
SetPropertyIfExists(builderType, builder, "Pooling", false);
SetPropertyIfExists(builderType, builder, "FailIfMissing", true);
string connectionString = Convert.ToString(builderType.GetProperty("ConnectionString").GetValue(builder, null));
Type connectionType = assembly.GetType("System.Data.SQLite.SQLiteConnection", true);
object connection = Activator.CreateInstance(connectionType, new object[] { connectionString });
DbConnection dbConnection = connection as DbConnection;
if (dbConnection == null)
{
throw new InvalidOperationException("SQLiteConnection is not a DbConnection");
}
return dbConnection;
}
private static void SetPropertyIfExists(Type type, object target, string name, object value)
{
PropertyInfo property = type.GetProperty(name);
if (property != null && property.CanWrite)
{
property.SetValue(target, value, null);
}
}
private static List<Dictionary<string, object>> ReadMasterTables(DbConnection connection, int maxTables)
{
var rows = new List<Dictionary<string, object>>();
using (DbCommand command = connection.CreateCommand())
{
command.CommandText = "select type, name, tbl_name from sqlite_master where type in ('table','view') order by type, name";
using (DbDataReader reader = command.ExecuteReader())
{
while (reader.Read() && rows.Count < maxTables)
{
rows.Add(new Dictionary<string, object>
{
{ "type", Convert.ToString(reader["type"]) },
{ "name", Convert.ToString(reader["name"]) },
{ "tbl_name", Convert.ToString(reader["tbl_name"]) }
});
}
}
}
return rows;
}
private static List<Dictionary<string, object>> ReadColumns(DbConnection connection, List<Dictionary<string, object>> tables, HashSet<string> onlyTables)
{
var rows = new List<Dictionary<string, object>>();
foreach (Dictionary<string, object> table in tables)
{
string tableName = Convert.ToString(table["name"]);
string tableType = Convert.ToString(table["type"]);
if (!string.Equals(tableType, "table", StringComparison.OrdinalIgnoreCase)) continue;
if (onlyTables != null && !onlyTables.Contains(tableName)) continue;
using (DbCommand command = connection.CreateCommand())
{
command.CommandText = "PRAGMA table_info(" + QuoteIdent(tableName) + ")";
using (DbDataReader reader = command.ExecuteReader())
{
while (reader.Read())
{
rows.Add(new Dictionary<string, object>
{
{ "table", tableName },
{ "cid", Convert.ToString(reader["cid"]) },
{ "name", Convert.ToString(reader["name"]) },
{ "type", Convert.ToString(reader["type"]) },
{ "notnull", Convert.ToString(reader["notnull"]) },
{ "pk", Convert.ToString(reader["pk"]) }
});
}
}
}
}
return rows;
}
private static List<Dictionary<string, object>> ReadTargetCounts(DbConnection connection, List<Dictionary<string, object>> tables, HashSet<string> targetTables)
{
var rows = new List<Dictionary<string, object>>();
foreach (Dictionary<string, object> table in tables)
{
string tableName = Convert.ToString(table["name"]);
string tableType = Convert.ToString(table["type"]);
if (!string.Equals(tableType, "table", StringComparison.OrdinalIgnoreCase)) continue;
if (!targetTables.Contains(tableName)) continue;
rows.Add(new Dictionary<string, object>
{
{ "table", tableName },
{ "row_count", ExecuteScalar(connection, "select count(*) from " + QuoteIdent(tableName)) }
});
}
return rows;
}
private static List<Dictionary<string, object>> BuildDisplaySources(List<Dictionary<string, object>> tables, List<Dictionary<string, object>> columns)
{
var tableSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (Dictionary<string, object> table in tables)
{
if (string.Equals(Convert.ToString(table["type"]), "table", StringComparison.OrdinalIgnoreCase))
{
tableSet.Add(Convert.ToString(table["name"]));
}
}
var columnMap = new Dictionary<string, HashSet<string>>(StringComparer.OrdinalIgnoreCase);
foreach (Dictionary<string, object> column in columns)
{
string table = Convert.ToString(column["table"]);
string name = Convert.ToString(column["name"]);
if (!columnMap.ContainsKey(table)) columnMap[table] = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
columnMap[table].Add(name);
}
var sources = new List<Dictionary<string, object>>();
AddSource(sources, tableSet, columnMap, "contacts_roster", "TD_Roster", new string[] { "JId", "Nick", "GroupName" });
AddSource(sources, tableSet, columnMap, "contacts_effigy", "TD_CustomEffigy", new string[] { "PersonJid", "PersonName" });
AddSource(sources, tableSet, columnMap, "recent_display", "tblRecent", new string[] { "PersonId", "PersonName", "FType" });
AddSource(sources, tableSet, columnMap, "person_message_names", "tblPersonMsg", new string[] { "SndPerson", "SndPersonId", "RecvPerson", "RecvPersonId", "ObjPerson", "ObjPersonId" });
AddSource(sources, tableSet, columnMap, "group_message_names", "tblMsgGroupPersonMsg", new string[] { "MsgGroupId", "MsgGroupName", "SessionPersonId", "SessionPersonName", "GroupType" });
AddSource(sources, tableSet, columnMap, "work_group_auth", "TD_WorkGroupAuth", new string[] { "GroupJID", "GroupName", "ChildGroupJid" });
AddSource(sources, tableSet, columnMap, "received_file_metadata", "TD_ReceiveFileRecord", new string[] { "FileName", "SendPersonJid", "SendPersonName" });
AddSource(sources, tableSet, columnMap, "sent_file_metadata", "TD_SendFileRecord", new string[] { "FileName", "ReceivePersonJid", "ReceivePersonName" });
return sources;
}
private sealed class DisplayEntitySpec
{
public string EntityType;
public string SourceTable;
public string JidColumn;
public string NameColumn;
public double Confidence;
}
private static HashSet<string> BuildDisplayEntityTableSet(string entityType)
{
var tables = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (DisplayEntitySpec spec in BuildDisplayEntitySpecs(entityType))
{
tables.Add(spec.SourceTable);
}
return tables;
}
private static Dictionary<string, HashSet<string>> BuildColumnMap(List<Dictionary<string, object>> columns)
{
var columnMap = new Dictionary<string, HashSet<string>>(StringComparer.OrdinalIgnoreCase);
foreach (Dictionary<string, object> column in columns)
{
string table = Convert.ToString(column["table"]);
string name = Convert.ToString(column["name"]);
if (!columnMap.ContainsKey(table)) columnMap[table] = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
columnMap[table].Add(name);
}
return columnMap;
}
private static List<DisplayEntitySpec> BuildDisplayEntitySpecs(string entityType)
{
var specs = new List<DisplayEntitySpec>();
if (string.Equals(entityType, "contacts", StringComparison.OrdinalIgnoreCase))
{
AddEntitySpec(specs, entityType, "TD_Roster", "JId", "Nick", 0.95);
AddEntitySpec(specs, entityType, "TD_CustomEffigy", "PersonJid", "PersonName", 0.90);
AddEntitySpec(specs, entityType, "tblRecent", "PersonId", "PersonName", 0.70);
AddEntitySpec(specs, entityType, "tblChatLevel", "PersonId", "PersonName", 0.70);
AddEntitySpec(specs, entityType, "tblPersonMsg", "SndPersonId", "SndPerson", 0.55);
AddEntitySpec(specs, entityType, "tblPersonMsg", "RecvPersonId", "RecvPerson", 0.55);
AddEntitySpec(specs, entityType, "tblPersonMsg", "ObjPersonId", "ObjPerson", 0.55);
}
else if (string.Equals(entityType, "groups", StringComparison.OrdinalIgnoreCase))
{
AddEntitySpec(specs, entityType, "TD_WorkGroupAuth", "GroupJID", "GroupName", 0.90);
AddEntitySpec(specs, entityType, "TD_WorkGroupAuth", "ChildGroupJid", "GroupName", 0.80);
AddEntitySpec(specs, entityType, "tblMsgGroupPersonMsg", "MsgGroupId", "MsgGroupName", 0.70);
AddEntitySpec(specs, entityType, "tblMsgGroupPersonMsg", "SessionPersonId", "SessionPersonName", 0.60);
}
return specs;
}
private static void AddEntitySpec(List<DisplayEntitySpec> specs, string entityType, string table, string jidColumn, string nameColumn, double confidence)
{
specs.Add(new DisplayEntitySpec
{
EntityType = entityType,
SourceTable = table,
JidColumn = jidColumn,
NameColumn = nameColumn,
Confidence = confidence
});
}
private static List<Dictionary<string, object>> ReadDisplayEntities(DbConnection connection, string entityType, string query, int limit, Dictionary<string, HashSet<string>> columnMap)
{
var rows = new List<Dictionary<string, object>>();
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (DisplayEntitySpec spec in BuildDisplayEntitySpecs(entityType))
{
if (rows.Count >= limit) break;
if (!columnMap.ContainsKey(spec.SourceTable)) continue;
if (!columnMap[spec.SourceTable].Contains(spec.JidColumn)) continue;
if (!columnMap[spec.SourceTable].Contains(spec.NameColumn)) continue;
ReadDisplayEntitiesForSpec(connection, spec, query, limit - rows.Count, rows, seen);
}
return rows;
}
private static void ReadDisplayEntitiesForSpec(DbConnection connection, DisplayEntitySpec spec, string query, int limit, List<Dictionary<string, object>> rows, HashSet<string> seen)
{
if (limit <= 0) return;
using (DbCommand command = connection.CreateCommand())
{
string jid = QuoteIdent(spec.JidColumn);
string name = QuoteIdent(spec.NameColumn);
command.CommandText =
"select " + jid + " as jid, " + name + " as display_name from " + QuoteIdent(spec.SourceTable) +
" where (coalesce(" + jid + ", '') <> '' or coalesce(" + name + ", '') <> '')";
if (!string.IsNullOrWhiteSpace(query))
{
command.CommandText += " and (" + jid + " like @query or " + name + " like @query)";
DbParameter queryParam = command.CreateParameter();
queryParam.ParameterName = "@query";
queryParam.Value = "%" + query + "%";
command.Parameters.Add(queryParam);
}
command.CommandText += " limit " + Convert.ToString(limit);
using (DbDataReader reader = command.ExecuteReader())
{
while (reader.Read() && rows.Count < limit)
{
string entityJid = Convert.ToString(reader["jid"]);
string displayName = Convert.ToString(reader["display_name"]);
if (string.IsNullOrWhiteSpace(entityJid) && string.IsNullOrWhiteSpace(displayName)) continue;
string key = spec.EntityType + "|" + entityJid + "|" + displayName + "|" + spec.SourceTable;
if (seen.Contains(key)) continue;
seen.Add(key);
rows.Add(new Dictionary<string, object>
{
{ "entity_type", spec.EntityType },
{ "source_table", spec.SourceTable },
{ "jid", entityJid },
{ "display_name", displayName },
{ "confidence", spec.Confidence },
{ "matched_columns", new string[] { spec.JidColumn, spec.NameColumn } }
});
}
}
}
}
private static void AddSource(List<Dictionary<string, object>> sources, HashSet<string> tableSet, Dictionary<string, HashSet<string>> columnMap, string sourceName, string table, string[] columns)
{
var presentColumns = new List<string>();
if (columnMap.ContainsKey(table))
{
foreach (string column in columns)
{
if (columnMap[table].Contains(column)) presentColumns.Add(column);
}
}
sources.Add(new Dictionary<string, object>
{
{ "source", sourceName },
{ "table", table },
{ "available", tableSet.Contains(table) && presentColumns.Count > 0 },
{ "required_columns", columns },
{ "present_columns", presentColumns.ToArray() }
});
}
private static string ExecuteScalar(DbConnection connection, string sql)
{
using (DbCommand command = connection.CreateCommand())
{
command.CommandText = sql;
object value = command.ExecuteScalar();
return Convert.ToString(value);
}
}
private static void TryExecuteNonQuery(DbConnection connection, string sql)
{
try
{
using (DbCommand command = connection.CreateCommand())
{
command.CommandText = sql;
command.ExecuteNonQuery();
}
}
catch
{
}
}
private static string QuoteIdent(string value)
{
return "\"" + value.Replace("\"", "\"\"") + "\"";
}
private static int Clamp(int value, int min, int max)
{
if (value < min) return min;
if (value > max) return max;
return value;
}
}
}