Files
MoShiro/backend/src/MoShiro.Backend.Storage.Sqlite/Schema.cs
T
justme ad5efc62ff agent: unify listening and dialing with directed mutual measurements
Replace Probe with Agent across node runtime, Fleet contracts, backend state, API and storage. Report incoming sessions and outgoing link health through each Agent uplink, and retain directed link controls and retry.

Update protocol fixtures and end-to-end coverage for the unified model; accept requested cancellation during the Fleet timeout test cleanup.
2026-10-21 11:55:36 +08:00

312 lines
11 KiB
C#

using Microsoft.Data.Sqlite;
namespace MoShiro.Backend.Storage.Sqlite;
/// <summary>
/// 表结构。版本记录在 <c>PRAGMA user_version</c>。当前只有一个版本:不是这个版本的数据库(包括更早的构建留下的)不会被迁移,
/// 启动时报错,删除 <c>moshiro.db</c> 和 <c>raw/</c> 之后重新开始。以后的结构变更按版本号追加内置 SQL。
/// </summary>
internal static class Schema
{
public const int CurrentVersion = 1;
public static void Migrate(SqliteConnection connection)
{
int version = connection.Scalar<int>("PRAGMA user_version");
if (version == CurrentVersion)
{
return;
}
if (version != 0)
{
throw new InvalidOperationException($"database schema version {version} is not supported by this build (expected {CurrentVersion}); delete moshiro.db and the raw/ directory to start over");
}
using SqliteTransaction transaction = connection.BeginTransaction();
connection.Exec(V1);
connection.Exec("PRAGMA user_version = 1");
transaction.Commit();
}
/// <summary>
/// 新增 Kind 或指标都不需要改表结构:指标是 <c>metric_*.metrics</c> 里的一个键,Kind 只是 <c>raw_chunks.kind</c> 的一个取值。
/// 节点只有一张表(<c>agents</c>):监听的节点有地址和端口,不监听的节点只拨出。线路 <c>(agent_id, series)</c> 里的 agent_id 是监听方,series 里是拨号方。
/// </summary>
private const string V1 = """
CREATE TABLE agents (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
status TEXT NOT NULL,
pubkey BLOB NOT NULL,
tls_spki BLOB NOT NULL,
tls_pubkey BLOB NOT NULL,
listens INTEGER NOT NULL DEFAULT 0,
public_host TEXT NOT NULL DEFAULT '',
tcp_port INTEGER NOT NULL DEFAULT 0,
quic_ports TEXT NOT NULL DEFAULT '[]',
ipv6_host TEXT NOT NULL DEFAULT '',
ipv6_supported INTEGER NOT NULL DEFAULT 0,
ipv6_diagnostics TEXT,
public_host_manual INTEGER NOT NULL DEFAULT 0,
ipv6_host_manual INTEGER NOT NULL DEFAULT 0,
reported_addresses TEXT NOT NULL DEFAULT '[]',
labels TEXT NOT NULL DEFAULT '[]',
public_visible INTEGER NOT NULL DEFAULT 1,
kinds TEXT NOT NULL DEFAULT '[]',
agent_version TEXT,
local_limits TEXT,
task_version INTEGER NOT NULL DEFAULT 0,
applied_task_version INTEGER NOT NULL DEFAULT 0,
rejected TEXT,
task_hash TEXT,
meta TEXT,
added_by TEXT,
created_at INTEGER NOT NULL,
last_seen_at INTEGER,
row_version INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE analysis_queue (
agent_id TEXT NOT NULL,
series TEXT NOT NULL,
window_start INTEGER NOT NULL,
not_before INTEGER NOT NULL,
reason TEXT NOT NULL,
seq INTEGER NOT NULL DEFAULT 1,
PRIMARY KEY (agent_id, series, window_start)
) WITHOUT ROWID;
CREATE TABLE api_tokens (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
capabilities TEXT NOT NULL,
token_sha256 BLOB NOT NULL UNIQUE,
created_at INTEGER NOT NULL,
last_used_at INTEGER,
expires_at INTEGER,
revoked_at INTEGER
);
CREATE TABLE audit_log (
id INTEGER PRIMARY KEY,
at INTEGER NOT NULL,
user_id TEXT,
actor TEXT NOT NULL,
method TEXT NOT NULL,
action TEXT NOT NULL,
target TEXT,
detail TEXT,
remote TEXT,
success INTEGER NOT NULL
);
CREATE TABLE events (
id INTEGER PRIMARY KEY,
agent_id TEXT,
kind TEXT NOT NULL,
at INTEGER NOT NULL,
detail TEXT
);
CREATE TABLE host_samples (
agent_id TEXT NOT NULL,
at INTEGER NOT NULL,
cpu REAL,
mem_used INTEGER,
mem_total INTEGER,
disk_used INTEGER,
disk_total INTEGER,
net_rx_bps INTEGER,
net_tx_bps INTEGER,
uptime INTEGER,
PRIMARY KEY (agent_id, at)
) WITHOUT ROWID;
CREATE TABLE join_tokens (
id TEXT PRIMARY KEY,
token_sha256 BLOB NOT NULL UNIQUE,
note TEXT,
labels TEXT NOT NULL DEFAULT '[]',
auto_approve INTEGER NOT NULL,
max_uses INTEGER NOT NULL,
uses INTEGER NOT NULL DEFAULT 0,
expires_at INTEGER NOT NULL,
revoked_at INTEGER,
created_at INTEGER NOT NULL,
kind TEXT NOT NULL DEFAULT '',
created_by TEXT
);
CREATE TABLE metric_10s (agent_id TEXT NOT NULL, series TEXT NOT NULL, window_start INTEGER NOT NULL, partial INTEGER NOT NULL, metrics BLOB NOT NULL, PRIMARY KEY (agent_id, series, window_start)) WITHOUT ROWID;
CREATE TABLE metric_1h (agent_id TEXT NOT NULL, series TEXT NOT NULL, window_start INTEGER NOT NULL, partial INTEGER NOT NULL, metrics BLOB NOT NULL, PRIMARY KEY (agent_id, series, window_start)) WITHOUT ROWID;
CREATE TABLE metric_1m (agent_id TEXT NOT NULL, series TEXT NOT NULL, window_start INTEGER NOT NULL, partial INTEGER NOT NULL, metrics BLOB NOT NULL, PRIMARY KEY (agent_id, series, window_start)) WITHOUT ROWID;
CREATE TABLE metric_5m (agent_id TEXT NOT NULL, series TEXT NOT NULL, window_start INTEGER NOT NULL, partial INTEGER NOT NULL, metrics BLOB NOT NULL, PRIMARY KEY (agent_id, series, window_start)) WITHOUT ROWID;
CREATE TABLE metric_versions (
metric_id TEXT PRIMARY KEY,
fingerprint TEXT NOT NULL,
recompute_from INTEGER,
recompute_cursor INTEGER
);
CREATE TABLE node_meta (
node_id TEXT PRIMARY KEY,
os TEXT,
os_name TEXT,
arch TEXT,
country TEXT,
isp_key TEXT,
isp_name TEXT,
isp_manual INTEGER NOT NULL DEFAULT 0,
geo_host TEXT,
geo_at INTEGER NOT NULL DEFAULT 0,
version TEXT,
tag TEXT,
commit_id TEXT,
built_at TEXT,
updated_at INTEGER NOT NULL DEFAULT 0
) WITHOUT ROWID;
CREATE TABLE operator_sets (
version INTEGER PRIMARY KEY,
statement TEXT NOT NULL,
credential_id TEXT NOT NULL,
authenticator_data TEXT NOT NULL,
client_data_json TEXT NOT NULL,
signature TEXT NOT NULL,
created_at INTEGER NOT NULL
);
CREATE TABLE passkeys (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name TEXT NOT NULL,
public_key BLOB NOT NULL,
sign_count INTEGER NOT NULL DEFAULT 0,
transports TEXT NOT NULL DEFAULT '[]',
created_at INTEGER NOT NULL,
last_used_at INTEGER
);
CREATE TABLE raw_chunks (
chunk_id TEXT PRIMARY KEY,
segment_id INTEGER NOT NULL REFERENCES raw_segments(id),
offset INTEGER NOT NULL,
length INTEGER NOT NULL,
agent_id TEXT NOT NULL,
series TEXT NOT NULL,
kind TEXT NOT NULL,
task_id TEXT,
test_id TEXT,
phase INTEGER,
start_ts INTEGER NOT NULL,
duration_ms INTEGER NOT NULL,
partial INTEGER NOT NULL
) WITHOUT ROWID;
CREATE TABLE raw_segments (
id INTEGER PRIMARY KEY,
day TEXT NOT NULL,
path TEXT NOT NULL UNIQUE,
bytes INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE rollup_dirty (
level INTEGER NOT NULL,
agent_id TEXT NOT NULL,
series TEXT NOT NULL,
bucket_start INTEGER NOT NULL,
PRIMARY KEY (level, agent_id, series, bucket_start)
) WITHOUT ROWID;
CREATE TABLE sessions (
id_sha256 BLOB PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
csrf_token BLOB NOT NULL,
auth_method TEXT NOT NULL DEFAULT 'password',
created_at INTEGER NOT NULL,
expires_at INTEGER NOT NULL
);
CREATE TABLE settings (key TEXT PRIMARY KEY, value TEXT NOT NULL);
CREATE TABLE tasks (
id TEXT PRIMARY KEY,
scope TEXT NOT NULL,
agent_id TEXT REFERENCES agents(id) ON DELETE CASCADE,
base_task_id TEXT REFERENCES tasks(id) ON DELETE CASCADE,
kind TEXT NOT NULL,
target TEXT,
params TEXT NOT NULL DEFAULT '{}',
schedule TEXT NOT NULL,
enabled INTEGER NOT NULL DEFAULT 1,
updated_at INTEGER NOT NULL,
row_version INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE test_results (
test_id TEXT PRIMARY KEY,
metrics BLOB NOT NULL,
series BLOB NOT NULL
) WITHOUT ROWID;
CREATE TABLE tests (
id TEXT PRIMARY KEY,
agent_id TEXT NOT NULL,
dialer_id TEXT NOT NULL,
ip_version TEXT NOT NULL DEFAULT 'ipv4',
plan TEXT NOT NULL,
params TEXT NOT NULL,
trigger TEXT NOT NULL,
status TEXT NOT NULL,
error_code TEXT,
phase INTEGER,
phase_count INTEGER NOT NULL DEFAULT 0,
requested_at INTEGER NOT NULL,
started_at INTEGER,
finished_at INTEGER
);
CREATE TABLE users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
password_hash BLOB NOT NULL,
password_salt BLOB NOT NULL,
iterations INTEGER NOT NULL,
created_at INTEGER NOT NULL
);
CREATE INDEX ix_audit_time ON audit_log (at);
CREATE INDEX ix_events_agent ON events (agent_id, at);
CREATE INDEX ix_events_time ON events (at);
CREATE INDEX ix_metric_10s_time ON metric_10s (window_start);
CREATE INDEX ix_metric_1m_time ON metric_1m (window_start);
CREATE INDEX ix_metric_5m_time ON metric_5m (window_start);
CREATE INDEX ix_passkeys_user ON passkeys (user_id);
CREATE INDEX ix_queue_due ON analysis_queue (not_before);
CREATE INDEX ix_raw_segment ON raw_chunks (segment_id);
CREATE INDEX ix_raw_series ON raw_chunks (agent_id, series, start_ts);
CREATE INDEX ix_raw_test ON raw_chunks (test_id) WHERE test_id IS NOT NULL;
CREATE INDEX ix_tests_agent ON tests (agent_id, requested_at);
CREATE INDEX ix_tests_path ON tests (agent_id, dialer_id, ip_version, requested_at);
CREATE UNIQUE INDEX ux_agents_name ON agents (name) WHERE status <> 'pending';
""";
}