feat: 新增NapCat运行态Profile持久化

This commit is contained in:
sunlei 2026-06-18 11:55:27 +08:00
parent d8d722e92a
commit 38cc5fb5bf
11 changed files with 942 additions and 1 deletions

View File

@ -141,6 +141,118 @@ CREATE TABLE IF NOT EXISTS `napcat_runtime_cleanup` (
KEY `idx_napcat_runtime_cleanup_session` (`session_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `napcat_runtime_profile` (
`id` bigint NOT NULL,
`account_id` bigint NOT NULL,
`container_id` bigint DEFAULT NULL,
`device_identity_id` bigint DEFAULT NULL,
`profile_version` varchar(64) NOT NULL,
`image_ref` varchar(255) NOT NULL,
`image_digest` varchar(255) DEFAULT NULL,
`base_image_digest` varchar(255) DEFAULT NULL,
`desktop_profile_version` varchar(64) DEFAULT NULL,
`locale_available` tinyint(1) NOT NULL DEFAULT 0,
`fontconfig_evidence` json DEFAULT NULL,
`timezone_evidence` json DEFAULT NULL,
`runtime_uid` int DEFAULT NULL,
`runtime_gid` int DEFAULT NULL,
`shm_size` varchar(32) DEFAULT NULL,
`locale` varchar(64) DEFAULT NULL,
`xdg_config_home` varchar(255) DEFAULT NULL,
`xdg_cache_home` varchar(255) DEFAULT NULL,
`xdg_data_home` varchar(255) DEFAULT NULL,
`persist_cache` tinyint(1) NOT NULL DEFAULT 1,
`persist_local_share` tinyint(1) NOT NULL DEFAULT 1,
`persist_logs` tinyint(1) NOT NULL DEFAULT 1,
`hostname_strategy` varchar(64) NOT NULL,
`mac_strategy` varchar(64) NOT NULL,
`migrate_device_identity` tinyint(1) NOT NULL DEFAULT 0,
`profile_status` varchar(32) NOT NULL,
`last_check_evidence` json DEFAULT NULL,
`last_checked_at` datetime DEFAULT NULL,
`create_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`update_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
PRIMARY KEY (`id`),
KEY `idx_napcat_runtime_profile_account` (`account_id`),
KEY `idx_napcat_runtime_profile_container` (`container_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `napcat_protocol_profile` (
`id` bigint NOT NULL,
`account_id` bigint NOT NULL,
`container_id` bigint DEFAULT NULL,
`profile_version` varchar(64) NOT NULL,
`packet_backend` varchar(64) NOT NULL,
`packet_server` varchar(255) NOT NULL DEFAULT '',
`o3_hook_mode` int NOT NULL DEFAULT 1,
`o3_hook_gray_enabled` tinyint(1) NOT NULL DEFAULT 0,
`onebot_config_hash` varchar(128) DEFAULT NULL,
`onebot_config_json` json DEFAULT NULL,
`napcat_config_hash` varchar(128) DEFAULT NULL,
`napcat_config_json` json DEFAULT NULL,
`profile_status` varchar(32) NOT NULL,
`last_check_evidence` json DEFAULT NULL,
`last_checked_at` datetime DEFAULT NULL,
`create_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`update_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
PRIMARY KEY (`id`),
KEY `idx_napcat_protocol_profile_account` (`account_id`),
KEY `idx_napcat_protocol_profile_container` (`container_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `napcat_session_behavior_profile` (
`id` bigint NOT NULL,
`account_id` bigint NOT NULL,
`profile_version` varchar(64) NOT NULL,
`enabled` tinyint(1) NOT NULL DEFAULT 1,
`cold_start_until` datetime DEFAULT NULL,
`housekeeping_enabled` tinyint(1) NOT NULL DEFAULT 0,
`housekeeping_interval_ms` int DEFAULT NULL,
`next_housekeeping_at` datetime DEFAULT NULL,
`last_housekeeping_at` datetime DEFAULT NULL,
`last_housekeeping_result` json DEFAULT NULL,
`presence_enabled` tinyint(1) NOT NULL DEFAULT 0,
`presence_strategy` varchar(64) DEFAULT NULL,
`last_presence_event_at` datetime DEFAULT NULL,
`next_presence_event_at` datetime DEFAULT NULL,
`auto_capability_stage` varchar(32) NOT NULL,
`last_behavior_evidence` json DEFAULT NULL,
`create_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`update_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
PRIMARY KEY (`id`),
KEY `idx_napcat_session_behavior_profile_account` (`account_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `napcat_login_event` (
`id` bigint NOT NULL,
`account_id` bigint NOT NULL,
`container_id` bigint DEFAULT NULL,
`event_kind` varchar(64) NOT NULL,
`event_source` varchar(32) NOT NULL,
`event_status` varchar(32) NOT NULL,
`evidence` json DEFAULT NULL,
`create_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`update_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
PRIMARY KEY (`id`),
KEY `idx_napcat_login_event_account` (`account_id`, `create_time`),
KEY `idx_napcat_login_event_container` (`container_id`, `create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `napcat_risk_mode` (
`id` bigint NOT NULL,
`account_id` bigint NOT NULL,
`risk_mode` varchar(32) NOT NULL,
`reason` varchar(255) DEFAULT NULL,
`source_event` varchar(64) DEFAULT NULL,
`expires_at` datetime DEFAULT NULL,
`last_evidence` json DEFAULT NULL,
`create_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`update_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
PRIMARY KEY (`id`),
UNIQUE KEY `uk_napcat_risk_mode_account` (`account_id`),
KEY `idx_napcat_risk_mode_mode` (`risk_mode`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `qqbot_config` (
`id` bigint NOT NULL,
`config_key` varchar(120) NOT NULL,

View File

@ -1016,3 +1016,110 @@ CREATE TABLE IF NOT EXISTS napcat_runtime_cleanup (
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
KEY idx_napcat_runtime_cleanup_session (session_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS napcat_runtime_profile (
id BIGINT NOT NULL PRIMARY KEY,
account_id BIGINT NOT NULL,
container_id BIGINT NULL,
device_identity_id BIGINT NULL,
profile_version VARCHAR(64) NOT NULL,
image_ref VARCHAR(255) NOT NULL,
image_digest VARCHAR(255) NULL,
base_image_digest VARCHAR(255) NULL,
desktop_profile_version VARCHAR(64) NULL,
locale_available TINYINT NOT NULL DEFAULT 0,
fontconfig_evidence JSON NULL,
timezone_evidence JSON NULL,
runtime_uid INT NULL,
runtime_gid INT NULL,
shm_size VARCHAR(32) NULL,
locale VARCHAR(64) NULL,
xdg_config_home VARCHAR(255) NULL,
xdg_cache_home VARCHAR(255) NULL,
xdg_data_home VARCHAR(255) NULL,
persist_cache TINYINT NOT NULL DEFAULT 1,
persist_local_share TINYINT NOT NULL DEFAULT 1,
persist_logs TINYINT NOT NULL DEFAULT 1,
hostname_strategy VARCHAR(64) NOT NULL,
mac_strategy VARCHAR(64) NOT NULL,
migrate_device_identity TINYINT NOT NULL DEFAULT 0,
profile_status VARCHAR(32) NOT NULL,
last_check_evidence JSON NULL,
last_checked_at DATETIME NULL,
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
KEY idx_napcat_runtime_profile_account (account_id),
KEY idx_napcat_runtime_profile_container (container_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS napcat_protocol_profile (
id BIGINT NOT NULL PRIMARY KEY,
account_id BIGINT NOT NULL,
container_id BIGINT NULL,
profile_version VARCHAR(64) NOT NULL,
packet_backend VARCHAR(64) NOT NULL,
packet_server VARCHAR(255) NOT NULL DEFAULT '',
o3_hook_mode INT NOT NULL DEFAULT 1,
o3_hook_gray_enabled TINYINT NOT NULL DEFAULT 0,
onebot_config_hash VARCHAR(128) NULL,
onebot_config_json JSON NULL,
napcat_config_hash VARCHAR(128) NULL,
napcat_config_json JSON NULL,
profile_status VARCHAR(32) NOT NULL,
last_check_evidence JSON NULL,
last_checked_at DATETIME NULL,
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
KEY idx_napcat_protocol_profile_account (account_id),
KEY idx_napcat_protocol_profile_container (container_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS napcat_session_behavior_profile (
id BIGINT NOT NULL PRIMARY KEY,
account_id BIGINT NOT NULL,
profile_version VARCHAR(64) NOT NULL,
enabled TINYINT NOT NULL DEFAULT 1,
cold_start_until DATETIME NULL,
housekeeping_enabled TINYINT NOT NULL DEFAULT 0,
housekeeping_interval_ms INT NULL,
next_housekeeping_at DATETIME NULL,
last_housekeeping_at DATETIME NULL,
last_housekeeping_result JSON NULL,
presence_enabled TINYINT NOT NULL DEFAULT 0,
presence_strategy VARCHAR(64) NULL,
last_presence_event_at DATETIME NULL,
next_presence_event_at DATETIME NULL,
auto_capability_stage VARCHAR(32) NOT NULL,
last_behavior_evidence JSON NULL,
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
KEY idx_napcat_session_behavior_profile_account (account_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS napcat_login_event (
id BIGINT NOT NULL PRIMARY KEY,
account_id BIGINT NOT NULL,
container_id BIGINT NULL,
event_kind VARCHAR(64) NOT NULL,
event_source VARCHAR(32) NOT NULL,
event_status VARCHAR(32) NOT NULL,
evidence JSON NULL,
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
KEY idx_napcat_login_event_account (account_id, create_time),
KEY idx_napcat_login_event_container (container_id, create_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS napcat_risk_mode (
id BIGINT NOT NULL PRIMARY KEY,
account_id BIGINT NOT NULL,
risk_mode VARCHAR(32) NOT NULL,
reason VARCHAR(255) NULL,
source_event VARCHAR(64) NULL,
expires_at DATETIME NULL,
last_evidence JSON NULL,
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_napcat_risk_mode_account (account_id),
KEY idx_napcat_risk_mode_mode (risk_mode)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

View File

@ -11,6 +11,11 @@ SELECT 'napcat_account_binding' AS table_name, COUNT(*) AS row_count FROM napcat
SELECT 'napcat_login_session' AS table_name, COUNT(*) AS row_count FROM napcat_login_session;
SELECT 'napcat_login_challenge' AS table_name, COUNT(*) AS row_count FROM napcat_login_challenge;
SELECT 'napcat_runtime_cleanup' AS table_name, COUNT(*) AS row_count FROM napcat_runtime_cleanup;
SELECT 'napcat_runtime_profile' AS table_name, COUNT(*) AS row_count FROM napcat_runtime_profile;
SELECT 'napcat_protocol_profile' AS table_name, COUNT(*) AS row_count FROM napcat_protocol_profile;
SELECT 'napcat_session_behavior_profile' AS table_name, COUNT(*) AS row_count FROM napcat_session_behavior_profile;
SELECT 'napcat_login_event' AS table_name, COUNT(*) AS row_count FROM napcat_login_event;
SELECT 'napcat_risk_mode' AS table_name, COUNT(*) AS row_count FROM napcat_risk_mode;
SELECT 'qqbot_plugin_task' AS table_name, COUNT(*) AS row_count FROM qqbot_plugin_task;
SELECT 'qqbot_plugin_task_run' AS table_name, COUNT(*) AS row_count FROM qqbot_plugin_task_run;
@ -111,6 +116,60 @@ WHERE table_schema = DATABASE()
AND table_name = 'napcat_runtime_cleanup'
AND index_name = 'idx_napcat_runtime_cleanup_session';
SELECT 'index_napcat_runtime_profile_account' AS check_name, COUNT(*) AS matched_rows
FROM information_schema.statistics
WHERE table_schema = DATABASE()
AND table_name = 'napcat_runtime_profile'
AND index_name = 'idx_napcat_runtime_profile_account';
SELECT 'index_napcat_runtime_profile_container' AS check_name, COUNT(*) AS matched_rows
FROM information_schema.statistics
WHERE table_schema = DATABASE()
AND table_name = 'napcat_runtime_profile'
AND index_name = 'idx_napcat_runtime_profile_container';
SELECT 'index_napcat_protocol_profile_account' AS check_name, COUNT(*) AS matched_rows
FROM information_schema.statistics
WHERE table_schema = DATABASE()
AND table_name = 'napcat_protocol_profile'
AND index_name = 'idx_napcat_protocol_profile_account';
SELECT 'index_napcat_protocol_profile_container' AS check_name, COUNT(*) AS matched_rows
FROM information_schema.statistics
WHERE table_schema = DATABASE()
AND table_name = 'napcat_protocol_profile'
AND index_name = 'idx_napcat_protocol_profile_container';
SELECT 'index_napcat_session_behavior_profile_account' AS check_name, COUNT(*) AS matched_rows
FROM information_schema.statistics
WHERE table_schema = DATABASE()
AND table_name = 'napcat_session_behavior_profile'
AND index_name = 'idx_napcat_session_behavior_profile_account';
SELECT 'index_napcat_login_event_account' AS check_name, COUNT(*) AS matched_rows
FROM information_schema.statistics
WHERE table_schema = DATABASE()
AND table_name = 'napcat_login_event'
AND index_name = 'idx_napcat_login_event_account';
SELECT 'index_napcat_login_event_container' AS check_name, COUNT(*) AS matched_rows
FROM information_schema.statistics
WHERE table_schema = DATABASE()
AND table_name = 'napcat_login_event'
AND index_name = 'idx_napcat_login_event_container';
SELECT 'index_napcat_risk_mode_account' AS check_name, COUNT(*) AS matched_rows
FROM information_schema.statistics
WHERE table_schema = DATABASE()
AND table_name = 'napcat_risk_mode'
AND index_name = 'uk_napcat_risk_mode_account';
SELECT 'index_napcat_risk_mode_mode' AS check_name, COUNT(*) AS matched_rows
FROM information_schema.statistics
WHERE table_schema = DATABASE()
AND table_name = 'napcat_risk_mode'
AND index_name = 'idx_napcat_risk_mode_mode';
SELECT 'column_napcat_login_challenge_session_id_varchar' AS check_name, COUNT(*) AS matched_rows
FROM information_schema.columns
WHERE table_schema = DATABASE()

View File

@ -2,8 +2,13 @@ import { NapcatAccountBinding } from './napcat-account-binding.entity';
import { NapcatContainer } from './napcat-container.entity';
import { NapcatDeviceIdentity } from './napcat-device-identity.entity';
import { NapcatLoginChallengeEntity } from './napcat-login-challenge.entity';
import { NapcatLoginEvent } from './napcat-login-event.entity';
import { NapcatLoginSession } from './napcat-login-session.entity';
import { NapcatProtocolProfile } from './napcat-protocol-profile.entity';
import { NapcatRiskMode } from './napcat-risk-mode.entity';
import { NapcatRuntimeCleanup } from './napcat-runtime-cleanup.entity';
import { NapcatRuntimeProfile } from './napcat-runtime-profile.entity';
import { NapcatSessionBehaviorProfile } from './napcat-session-behavior-profile.entity';
export const NAPCAT_RUNTIME_DOMAIN_CONTRACT = {
tables: [
@ -13,6 +18,11 @@ export const NAPCAT_RUNTIME_DOMAIN_CONTRACT = {
'napcat_login_session',
'napcat_login_challenge',
'napcat_runtime_cleanup',
'napcat_runtime_profile',
'napcat_protocol_profile',
'napcat_session_behavior_profile',
'napcat_login_event',
'napcat_risk_mode',
],
} as const;
@ -23,4 +33,17 @@ export const NAPCAT_RUNTIME_ENTITIES = [
NapcatLoginChallengeEntity,
NapcatLoginSession,
NapcatRuntimeCleanup,
NapcatRuntimeProfile,
NapcatProtocolProfile,
NapcatSessionBehaviorProfile,
NapcatLoginEvent,
NapcatRiskMode,
];
export {
NapcatLoginEvent,
NapcatProtocolProfile,
NapcatRiskMode,
NapcatRuntimeProfile,
NapcatSessionBehaviorProfile,
};

View File

@ -0,0 +1,76 @@
import { BeforeInsert, Column, Entity, Index, PrimaryColumn } from 'typeorm';
import {
ensureSnowflakeId,
KtCreateDateColumn,
KtDateTime,
KtUpdateDateColumn,
} from '@/common';
export type NapcatLoginEventKind =
| 'captcha_required'
| 'container_recreate'
| 'container_restart'
| 'manual_qr_created'
| 'manual_qr_scanned'
| 'new_device_required'
| 'password_attempt'
| 'quick_attempt'
| 'recovery_suspended';
export type NapcatLoginEventSource =
| 'admin'
| 'runtime'
| 'system'
| 'watchdog';
export type NapcatLoginEventStatus =
| 'blocked'
| 'failed'
| 'pending'
| 'skipped'
| 'success';
@Entity('napcat_login_event')
@Index('idx_napcat_login_event_account', ['accountId', 'createTime'])
@Index('idx_napcat_login_event_container', ['containerId', 'createTime'])
export class NapcatLoginEvent {
@PrimaryColumn({ type: 'bigint' })
id: string;
@Column({ name: 'account_id', type: 'bigint' })
accountId: string;
@Column({
default: null,
name: 'container_id',
nullable: true,
type: 'bigint',
})
containerId: null | string;
@Column({ length: 64, name: 'event_kind' })
eventKind: NapcatLoginEventKind;
@Column({ length: 32, name: 'event_source' })
eventSource: NapcatLoginEventSource;
@Column({ length: 32, name: 'event_status' })
eventStatus: NapcatLoginEventStatus;
@Column({ default: null, nullable: true, type: 'json' })
evidence: null | Record<string, unknown>;
@KtCreateDateColumn({ name: 'create_time' })
createTime: KtDateTime;
@KtUpdateDateColumn({ name: 'update_time' })
updateTime: KtDateTime;
/**
* Assigns a stable Snowflake id before persisting a login-side risk event.
*/
@BeforeInsert()
createId() {
ensureSnowflakeId(this);
}
}

View File

@ -0,0 +1,112 @@
import { BeforeInsert, Column, Entity, Index, PrimaryColumn } from 'typeorm';
import {
ensureSnowflakeId,
KtCreateDateColumn,
KtDateTime,
KtDateTimeColumn,
KtUpdateDateColumn,
} from '@/common';
export type NapcatProtocolProfileStatus =
| 'drifted'
| 'failed'
| 'pending'
| 'synced';
@Entity('napcat_protocol_profile')
@Index('idx_napcat_protocol_profile_account', ['accountId'])
@Index('idx_napcat_protocol_profile_container', ['containerId'])
export class NapcatProtocolProfile {
@PrimaryColumn({ type: 'bigint' })
id: string;
@Column({ name: 'account_id', type: 'bigint' })
accountId: string;
@Column({
default: null,
name: 'container_id',
nullable: true,
type: 'bigint',
})
containerId: null | string;
@Column({ length: 64, name: 'profile_version' })
profileVersion: string;
@Column({ length: 64, name: 'packet_backend' })
packetBackend: string;
@Column({ default: '', length: 255, name: 'packet_server' })
packetServer: string;
@Column({ default: 1, name: 'o3_hook_mode', type: 'int' })
o3HookMode: number;
@Column({ default: false, name: 'o3_hook_gray_enabled' })
o3HookGrayEnabled: boolean;
@Column({
default: null,
length: 128,
name: 'onebot_config_hash',
nullable: true,
})
onebotConfigHash: null | string;
@Column({
default: null,
name: 'onebot_config_json',
nullable: true,
type: 'json',
})
onebotConfigJson: null | Record<string, unknown>;
@Column({
default: null,
length: 128,
name: 'napcat_config_hash',
nullable: true,
})
napcatConfigHash: null | string;
@Column({
default: null,
name: 'napcat_config_json',
nullable: true,
type: 'json',
})
napcatConfigJson: null | Record<string, unknown>;
@Column({ length: 32, name: 'profile_status' })
profileStatus: NapcatProtocolProfileStatus;
@Column({
default: null,
name: 'last_check_evidence',
nullable: true,
type: 'json',
})
lastCheckEvidence: null | Record<string, unknown>;
@KtDateTimeColumn({
default: null,
name: 'last_checked_at',
nullable: true,
})
lastCheckedAt: KtDateTime | null;
@KtCreateDateColumn({ name: 'create_time' })
createTime: KtDateTime;
@KtUpdateDateColumn({ name: 'update_time' })
updateTime: KtDateTime;
/**
* Assigns a stable Snowflake id before persisting protocol-profile evidence.
*/
@BeforeInsert()
createId() {
ensureSnowflakeId(this);
}
}

View File

@ -0,0 +1,60 @@
import { BeforeInsert, Column, Entity, Index, PrimaryColumn } from 'typeorm';
import {
ensureSnowflakeId,
KtCreateDateColumn,
KtDateTime,
KtDateTimeColumn,
KtUpdateDateColumn,
} from '@/common';
export type NapcatRiskModeValue = 'cooldown' | 'manual_only' | 'normal';
@Entity('napcat_risk_mode')
@Index('uk_napcat_risk_mode_account', ['accountId'], { unique: true })
@Index('idx_napcat_risk_mode_mode', ['riskMode'])
export class NapcatRiskMode {
@PrimaryColumn({ type: 'bigint' })
id: string;
@Column({ name: 'account_id', type: 'bigint' })
accountId: string;
@Column({ length: 32, name: 'risk_mode' })
riskMode: NapcatRiskModeValue;
@Column({ default: null, length: 255, nullable: true })
reason: null | string;
@Column({
default: null,
length: 64,
name: 'source_event',
nullable: true,
})
sourceEvent: null | string;
@KtDateTimeColumn({ default: null, name: 'expires_at', nullable: true })
expiresAt: KtDateTime | null;
@Column({
default: null,
name: 'last_evidence',
nullable: true,
type: 'json',
})
lastEvidence: null | Record<string, unknown>;
@KtCreateDateColumn({ name: 'create_time' })
createTime: KtDateTime;
@KtUpdateDateColumn({ name: 'update_time' })
updateTime: KtDateTime;
/**
* Assigns a stable Snowflake id before persisting account risk-mode state.
*/
@BeforeInsert()
createId() {
ensureSnowflakeId(this);
}
}

View File

@ -0,0 +1,171 @@
import { BeforeInsert, Column, Entity, Index, PrimaryColumn } from 'typeorm';
import {
ensureSnowflakeId,
KtCreateDateColumn,
KtDateTime,
KtDateTimeColumn,
KtUpdateDateColumn,
} from '@/common';
export type NapcatRuntimeProfileStatus =
| 'drifted'
| 'failed'
| 'pending'
| 'synced';
@Entity('napcat_runtime_profile')
@Index('idx_napcat_runtime_profile_account', ['accountId'])
@Index('idx_napcat_runtime_profile_container', ['containerId'])
export class NapcatRuntimeProfile {
@PrimaryColumn({ type: 'bigint' })
id: string;
@Column({ name: 'account_id', type: 'bigint' })
accountId: string;
@Column({
default: null,
name: 'container_id',
nullable: true,
type: 'bigint',
})
containerId: null | string;
@Column({
default: null,
name: 'device_identity_id',
nullable: true,
type: 'bigint',
})
deviceIdentityId: null | string;
@Column({ length: 64, name: 'profile_version' })
profileVersion: string;
@Column({ length: 255, name: 'image_ref' })
imageRef: string;
@Column({ default: null, length: 255, name: 'image_digest', nullable: true })
imageDigest: null | string;
@Column({
default: null,
length: 255,
name: 'base_image_digest',
nullable: true,
})
baseImageDigest: null | string;
@Column({
default: null,
length: 64,
name: 'desktop_profile_version',
nullable: true,
})
desktopProfileVersion: null | string;
@Column({ default: false, name: 'locale_available' })
localeAvailable: boolean;
@Column({
default: null,
name: 'fontconfig_evidence',
nullable: true,
type: 'json',
})
fontconfigEvidence: null | Record<string, unknown>;
@Column({
default: null,
name: 'timezone_evidence',
nullable: true,
type: 'json',
})
timezoneEvidence: null | Record<string, unknown>;
@Column({ default: null, name: 'runtime_uid', nullable: true, type: 'int' })
runtimeUid: null | number;
@Column({ default: null, name: 'runtime_gid', nullable: true, type: 'int' })
runtimeGid: null | number;
@Column({ default: null, length: 32, name: 'shm_size', nullable: true })
shmSize: null | string;
@Column({ default: null, length: 64, nullable: true })
locale: null | string;
@Column({
default: null,
length: 255,
name: 'xdg_config_home',
nullable: true,
})
xdgConfigHome: null | string;
@Column({
default: null,
length: 255,
name: 'xdg_cache_home',
nullable: true,
})
xdgCacheHome: null | string;
@Column({
default: null,
length: 255,
name: 'xdg_data_home',
nullable: true,
})
xdgDataHome: null | string;
@Column({ default: true, name: 'persist_cache' })
persistCache: boolean;
@Column({ default: true, name: 'persist_local_share' })
persistLocalShare: boolean;
@Column({ default: true, name: 'persist_logs' })
persistLogs: boolean;
@Column({ length: 64, name: 'hostname_strategy' })
hostnameStrategy: string;
@Column({ length: 64, name: 'mac_strategy' })
macStrategy: string;
@Column({ default: false, name: 'migrate_device_identity' })
migrateDeviceIdentity: boolean;
@Column({ length: 32, name: 'profile_status' })
profileStatus: NapcatRuntimeProfileStatus;
@Column({
default: null,
name: 'last_check_evidence',
nullable: true,
type: 'json',
})
lastCheckEvidence: null | Record<string, unknown>;
@KtDateTimeColumn({
default: null,
name: 'last_checked_at',
nullable: true,
})
lastCheckedAt: KtDateTime | null;
@KtCreateDateColumn({ name: 'create_time' })
createTime: KtDateTime;
@KtUpdateDateColumn({ name: 'update_time' })
updateTime: KtDateTime;
/**
* Assigns a stable Snowflake id before persisting runtime-profile evidence.
*/
@BeforeInsert()
createId() {
ensureSnowflakeId(this);
}
}

View File

@ -0,0 +1,120 @@
import { BeforeInsert, Column, Entity, Index, PrimaryColumn } from 'typeorm';
import {
ensureSnowflakeId,
KtCreateDateColumn,
KtDateTime,
KtDateTimeColumn,
KtUpdateDateColumn,
} from '@/common';
export type NapcatAutoCapabilityStage =
| 'automatic'
| 'image_commands'
| 'manual_only'
| 'text_commands';
@Entity('napcat_session_behavior_profile')
@Index('idx_napcat_session_behavior_profile_account', ['accountId'])
export class NapcatSessionBehaviorProfile {
@PrimaryColumn({ type: 'bigint' })
id: string;
@Column({ name: 'account_id', type: 'bigint' })
accountId: string;
@Column({ length: 64, name: 'profile_version' })
profileVersion: string;
@Column({ default: true })
enabled: boolean;
@KtDateTimeColumn({
default: null,
name: 'cold_start_until',
nullable: true,
})
coldStartUntil: KtDateTime | null;
@Column({ default: false, name: 'housekeeping_enabled' })
housekeepingEnabled: boolean;
@Column({
default: null,
name: 'housekeeping_interval_ms',
nullable: true,
type: 'int',
})
housekeepingIntervalMs: null | number;
@KtDateTimeColumn({
default: null,
name: 'next_housekeeping_at',
nullable: true,
})
nextHousekeepingAt: KtDateTime | null;
@KtDateTimeColumn({
default: null,
name: 'last_housekeeping_at',
nullable: true,
})
lastHousekeepingAt: KtDateTime | null;
@Column({
default: null,
name: 'last_housekeeping_result',
nullable: true,
type: 'json',
})
lastHousekeepingResult: null | Record<string, unknown>;
@Column({ default: false, name: 'presence_enabled' })
presenceEnabled: boolean;
@Column({
default: null,
length: 64,
name: 'presence_strategy',
nullable: true,
})
presenceStrategy: null | string;
@KtDateTimeColumn({
default: null,
name: 'last_presence_event_at',
nullable: true,
})
lastPresenceEventAt: KtDateTime | null;
@KtDateTimeColumn({
default: null,
name: 'next_presence_event_at',
nullable: true,
})
nextPresenceEventAt: KtDateTime | null;
@Column({ length: 32, name: 'auto_capability_stage' })
autoCapabilityStage: NapcatAutoCapabilityStage;
@Column({
default: null,
name: 'last_behavior_evidence',
nullable: true,
type: 'json',
})
lastBehaviorEvidence: null | Record<string, unknown>;
@KtCreateDateColumn({ name: 'create_time' })
createTime: KtDateTime;
@KtUpdateDateColumn({ name: 'update_time' })
updateTime: KtDateTime;
/**
* Assigns a stable Snowflake id before persisting session-behavior profile state.
*/
@BeforeInsert()
createId() {
ensureSnowflakeId(this);
}
}

View File

@ -1,7 +1,8 @@
# QQBot NapCat Schema
NapCat owns container runtime, account binding, device identity, login session,
challenge, and cleanup persistence through the v3 `napcat_*` tables.
challenge, cleanup, runtime/profile evidence, login-event, and risk-mode
persistence through the v3 `napcat_*` tables.
- `napcat_container`
- `napcat_device_identity`
@ -9,6 +10,11 @@ challenge, and cleanup persistence through the v3 `napcat_*` tables.
- `napcat_login_session`
- `napcat_login_challenge`
- `napcat_runtime_cleanup`
- `napcat_runtime_profile`
- `napcat_protocol_profile`
- `napcat_session_behavior_profile`
- `napcat_login_event`
- `napcat_risk_mode`
Verification SQL:
@ -16,4 +22,9 @@ Verification SQL:
SELECT COUNT(*) FROM napcat_container WHERE is_deleted = 0;
SELECT COUNT(*) FROM napcat_device_identity;
SELECT COUNT(*) FROM napcat_account_binding WHERE is_deleted = 0;
SELECT COUNT(*) FROM napcat_runtime_profile;
SELECT COUNT(*) FROM napcat_protocol_profile;
SELECT COUNT(*) FROM napcat_session_behavior_profile;
SELECT COUNT(*) FROM napcat_login_event;
SELECT COUNT(*) FROM napcat_risk_mode;
```

View File

@ -0,0 +1,90 @@
import { getMetadataArgsStorage } from 'typeorm';
import {
NapcatLoginEvent,
NapcatProtocolProfile,
NapcatRiskMode,
NapcatRuntimeProfile,
NapcatSessionBehaviorProfile,
NAPCAT_RUNTIME_DOMAIN_CONTRACT,
NAPCAT_RUNTIME_ENTITIES,
} from '../../../../src/modules/qqbot/napcat';
import { readRefactorV3SqlSchema } from '../../../helpers/sql-schema.helper';
type EntityClass = new (...args: never[]) => unknown;
/**
* Reads a TypeORM entity table name from decorator metadata.
* @param entity - Entity class selected by the test to compare against SQL schema ownership.
*/
const getEntityTableName = (entity: EntityClass) =>
getMetadataArgsStorage().tables.find((table) => table.target === entity)
?.name;
/**
* Reads entity column names as they are persisted in MySQL.
* @param entity - Entity class whose decorator column metadata must match refactor-v3 SQL.
*/
const getEntityColumnNames = (entity: EntityClass) =>
getMetadataArgsStorage()
.columns.filter((column) => column.target === entity)
.map((column) => `${column.options.name || column.propertyName}`);
describe('NapCat runtime and protocol profile persistence', () => {
const schema = readRefactorV3SqlSchema();
it('declares runtime profile tables as NapCat-owned domain tables', () => {
expect(NAPCAT_RUNTIME_DOMAIN_CONTRACT.tables).toEqual(
expect.arrayContaining([
'napcat_runtime_profile',
'napcat_protocol_profile',
'napcat_session_behavior_profile',
'napcat_login_event',
'napcat_risk_mode',
]),
);
});
it.each([
[NapcatRuntimeProfile, 'napcat_runtime_profile'],
[NapcatProtocolProfile, 'napcat_protocol_profile'],
[NapcatSessionBehaviorProfile, 'napcat_session_behavior_profile'],
[NapcatLoginEvent, 'napcat_login_event'],
[NapcatRiskMode, 'napcat_risk_mode'],
])('maps %p to %s in the v3 SQL schema', (entity, tableName) => {
expect(NAPCAT_RUNTIME_ENTITIES).toContain(entity);
expect(getEntityTableName(entity)).toBe(tableName);
schema.expectTableColumns(tableName, getEntityColumnNames(entity));
});
it('keeps login-event fields separate from send budget fields', () => {
const loginEventColumns = getEntityColumnNames(NapcatLoginEvent);
expect(loginEventColumns).toEqual(
expect.arrayContaining([
'account_id',
'container_id',
'event_kind',
'event_source',
'event_status',
'evidence',
]),
);
expect(loginEventColumns.join(' ')).not.toMatch(
/hour|daily|quota|budget|limit_count/i,
);
});
it('keeps risk mode separate from account send budgets', () => {
const riskColumns = getEntityColumnNames(NapcatRiskMode);
expect(riskColumns).toEqual(
expect.arrayContaining([
'account_id',
'risk_mode',
'reason',
'source_event',
'expires_at',
'last_evidence',
]),
);
expect(riskColumns.join(' ')).not.toMatch(/daily|hour|budget|quota/i);
});
});