feat: 增加TCP NATMap菜单与消息模板
This commit is contained in:
parent
83b874322a
commit
1c35fc9dbd
4
API.md
4
API.md
@ -473,6 +473,8 @@ QQBot 运行态包括 NapCat 容器登录、OneBot v11 反向 WebSocket、MQTT
|
|||||||
|
|
||||||
TCP NATMap 独立消息源为 `network.tcp.natmap-endpoint-changed`、版本 `1`,订阅配置精确为 `{ tcpChannelId, ddnsRecordId }`,两个字段都必须是字符串 Snowflake ID,且 DDNS 必须是启用、未删除、绑定同一 TCP 通道的 A 记录。变量精确为 `endpoint`、`fqdn`、`publicIpv4`、`publicPort`、`previousPublicIpv4`、`previousPublicPort`、`portForwardName`;`endpoint` 使用事件端口冻结为 `FQDN:newPort`。DDNS 尚未同步到事件 IPv4 时为 `waiting_ddns`,当前 tuple 已变化或租约过期为 `superseded`,通道撤下、删除、停用 NATMap 或 DDNS 改绑为 `cancelled`。现有 STUN 来源定义与变量保持不变。
|
TCP NATMap 独立消息源为 `network.tcp.natmap-endpoint-changed`、版本 `1`,订阅配置精确为 `{ tcpChannelId, ddnsRecordId }`,两个字段都必须是字符串 Snowflake ID,且 DDNS 必须是启用、未删除、绑定同一 TCP 通道的 A 记录。变量精确为 `endpoint`、`fqdn`、`publicIpv4`、`publicPort`、`previousPublicIpv4`、`previousPublicPort`、`portForwardName`;`endpoint` 使用事件端口冻结为 `FQDN:newPort`。DDNS 尚未同步到事件 IPv4 时为 `waiting_ddns`,当前 tuple 已变化或租约过期为 `superseded`,通道撤下、删除、停用 NATMap 或 DDNS 改绑为 `cancelled`。现有 STUN 来源定义与变量保持不变。
|
||||||
|
|
||||||
|
初始化与增量 SQL 使用稳定 ID `2041700000000200602` 幂等创建 `TCP NATMap 端点变更默认模板`,正文固定为 `当前 TCP NATMap 端点已变更为 ${{endpoint}}`;既有 STUN 模板 ID、来源键和正文不得修改。TCP NATMap 独立操作权限为 `System:Network:PortForward:Natmap`,只授予活动 `super` 角色。
|
||||||
|
|
||||||
#### 内部事件与投递生命周期
|
#### 内部事件与投递生命周期
|
||||||
|
|
||||||
- UDP 只有一个既有有效端口直接变化为另一个有效端口的 `changed` 事件,才写既有 STUN Outbox;TCP 则在既有有效 tuple 的公网 IPv4 或端口任一变化时写独立 NATMap endpoint Outbox。首次 `published`、同 tuple 续期、`withdrawn`、同 tuple `restored`、重复事件和回滚事务都不产生 TCP 消息事件。事件使用生产者 `eventId` 幂等,matching report/event 的 history、current/baseline 与 Outbox 保持既有事务关联门禁,事务提交后才调用 `requestDrain()`。
|
- UDP 只有一个既有有效端口直接变化为另一个有效端口的 `changed` 事件,才写既有 STUN Outbox;TCP 则在既有有效 tuple 的公网 IPv4 或端口任一变化时写独立 NATMap endpoint Outbox。首次 `published`、同 tuple 续期、`withdrawn`、同 tuple `restored`、重复事件和回滚事务都不产生 TCP 消息事件。事件使用生产者 `eventId` 幂等,matching report/event 的 history、current/baseline 与 Outbox 保持既有事务关联门禁,事务提交后才调用 `requestDrain()`。
|
||||||
@ -482,7 +484,7 @@ TCP NATMap 独立消息源为 `network.tcp.natmap-endpoint-changed`、版本 `1`
|
|||||||
|
|
||||||
#### SQL、发布与回滚
|
#### SQL、发布与回滚
|
||||||
|
|
||||||
既有环境使用本功能幂等增量入口 `sql/qqbot-message-push-init.sql`,随后执行只读的 `sql/qqbot-message-push-verify.sql`;包含历史迁移的 `sql/qqbot-init.sql` 不能作为本功能生产迁移。只有一次性、可丢弃的全量初始化环境才依次使用 `sql/refactor-v3/00-full-schema.sql`、`01-seed-core.sql`、`99-verify.sql`。发布顺序是:备份六表及相关菜单/角色授权行 → 应用对应 SQL 入口 → 验证表、17 个精确索引、默认模板与权限 → 先验证 API 再发布 Admin → 创建订阅和逐账号绑定 → 使用授权非生产目标完成有界 A→B 验收。
|
既有环境使用本功能幂等增量入口 `sql/qqbot-message-push-init.sql`,随后执行只读的 `sql/qqbot-message-push-verify.sql`;包含历史迁移的 `sql/qqbot-init.sql` 不能作为本功能生产迁移。只有一次性、可丢弃的全量初始化环境才依次使用 `sql/refactor-v3/00-full-schema.sql`、`01-seed-core.sql`、`99-verify.sql`。发布顺序是:备份六表及相关菜单/角色授权行 → 应用对应 SQL 入口 → 验证表、17 个精确索引、STUN/TCP 默认模板与权限 → 先验证 API 再发布 Admin → 创建订阅和逐账号绑定 → 使用授权非生产目标完成有界 A→B 验收。
|
||||||
|
|
||||||
回滚先停用全部发布绑定,再回滚 Admin 和 API,并保留事件、投递及发送日志;Network Agent、端口转发、STUN Keeper 和 DDNS 继续运行。Jenkins/K8s 通过只证明版本已部署,不能替代真实 CRUD、页面或事件到消息的功能验收。每次发布必须分别记录生产 SQL、真实 API、页面、Outbox/DDNS 和授权 QQ 投递证据;没有明确授权的 QQ 群或 QQ 号时不得任选目标,并应把真实投递单独标记为未验证。
|
回滚先停用全部发布绑定,再回滚 Admin 和 API,并保留事件、投递及发送日志;Network Agent、端口转发、STUN Keeper 和 DDNS 继续运行。Jenkins/K8s 通过只证明版本已部署,不能替代真实 CRUD、页面或事件到消息的功能验收。每次发布必须分别记录生产 SQL、真实 API、页面、Outbox/DDNS 和授权 QQ 投递证据;没有明确授权的 QQ 群或 QQ 号时不得任选目标,并应把真实投递单独标记为未验证。
|
||||||
|
|
||||||
|
|||||||
@ -88,7 +88,7 @@ v2 endpoint event 通常先于 matching reported 到达,因此 event-first 先
|
|||||||
|
|
||||||
同一模块提供腾讯云云解析 DNS 的双栈自动 DDNS。A 记录可从合格 UDP Keeper 或 TCP NATMap 通道的有效公网 IPv4 取值,AAAA 记录只从在线 Agent 最近上报的全局 IPv6 取值;DNS 值始终不包含端口。A 来源以 `组名 / UDP Keeper` 或 `组名 / TCP NATMap` 展示;同步地址等于当前通道 IPv4 且当前公网端口有效时,管理响应额外返回 `accessEndpoint=FQDN:port`。仅端口变化只更新该派生端点,不调用腾讯云。协调器只修改已存在、已启用、默认线路且唯一的 A/AAAA 记录,保留 RecordId、线路和 TTL,并在写入后回读确认。删除 Admin 绑定只停止本地自动更新,不删除云端 DNS 记录。凭据只从 API 私有运行环境的 `NETWORK_DDNS_DNSPOD_SECRET_ID/SECRET_KEY` 读取,不进入 Admin、数据库、MQTT、Agent、日志或 Git;`DNSPOD` 是腾讯云官方 SDK 的技术服务名。
|
同一模块提供腾讯云云解析 DNS 的双栈自动 DDNS。A 记录可从合格 UDP Keeper 或 TCP NATMap 通道的有效公网 IPv4 取值,AAAA 记录只从在线 Agent 最近上报的全局 IPv6 取值;DNS 值始终不包含端口。A 来源以 `组名 / UDP Keeper` 或 `组名 / TCP NATMap` 展示;同步地址等于当前通道 IPv4 且当前公网端口有效时,管理响应额外返回 `accessEndpoint=FQDN:port`。仅端口变化只更新该派生端点,不调用腾讯云。协调器只修改已存在、已启用、默认线路且唯一的 A/AAAA 记录,保留 RecordId、线路和 TTL,并在写入后回读确认。删除 Admin 绑定只停止本地自动更新,不删除云端 DNS 记录。凭据只从 API 私有运行环境的 `NETWORK_DDNS_DNSPOD_SECRET_ID/SECRET_KEY` 读取,不进入 Admin、数据库、MQTT、Agent、日志或 Git;`DNSPOD` 是腾讯云官方 SDK 的技术服务名。
|
||||||
|
|
||||||
QQBot 系统消息推送由全局消息订阅和模板、账号范围发布绑定及耐久事件/投递共同管理;订阅表单字段、候选集合和资源匹配由所选系统消息源 adapter 声明,核心模块不写死 STUN 的 `portForwardId/ddnsRecordId` 或 TCP NATMap 的 `tcpChannelId/ddnsRecordId`。TCP 来源变量精确为 `endpoint/fqdn/publicIpv4/publicPort/previousPublicIpv4/previousPublicPort/portForwardName`,只有所选同通道 A 记录同步到事件 IPv4 后才能发送。账号接口严格使用路由 `selfId`,不会回退到其他机器人。群聊和私聊目标分别使用 `group` / `private`,Snowflake、QQ 账号和目标 ID 在 HTTP 与数据库边界始终保持字符串。系统事件只能通过内部 Outbox stager 暂存,不提供 publish/event/worker HTTP 路由;管理响应仅返回字段白名单,不暴露账号凭据、Provider/OneBot/MQTT 运行对象、原始事件载荷或内部持久化键。
|
QQBot 系统消息推送由全局消息订阅和模板、账号范围发布绑定及耐久事件/投递共同管理;订阅表单字段、候选集合和资源匹配由所选系统消息源 adapter 声明,核心模块不写死 STUN 的 `portForwardId/ddnsRecordId` 或 TCP NATMap 的 `tcpChannelId/ddnsRecordId`。TCP 来源变量精确为 `endpoint/fqdn/publicIpv4/publicPort/previousPublicIpv4/previousPublicPort/portForwardName`,只有所选同通道 A 记录同步到事件 IPv4 后才能发送;初始化与增量 SQL 幂等提供正文为 `当前 TCP NATMap 端点已变更为 ${{endpoint}}` 的默认模板,既有 STUN 模板保持不变。账号接口严格使用路由 `selfId`,不会回退到其他机器人。群聊和私聊目标分别使用 `group` / `private`,Snowflake、QQ 账号和目标 ID 在 HTTP 与数据库边界始终保持字符串。系统事件只能通过内部 Outbox stager 暂存,不提供 publish/event/worker HTTP 路由;管理响应仅返回字段白名单,不暴露账号凭据、Provider/OneBot/MQTT 运行对象、原始事件载荷或内部持久化键。
|
||||||
|
|
||||||
`qqbot_message_event` 同时承担事件 Outbox:扇出状态为 `accepted`、`processing`、`retry`、`completed`、`failed`;按目标冻结的 `qqbot_message_delivery` 状态为 `waiting_ddns`、`pending`、`processing`、`retry`、`success`、`failed`、`superseded`、`cancelled`。扇出和投递每次各领取最多 50 行,处理租约为 30 秒;进程启动后立即恢复,并每 5 秒扫描一次。`waiting_ddns` 每 60 秒持久化复检;临时错误从 10 秒开始指数退避,单次最长 15 分钟,并以事件发生后 24 小时为截止时间。
|
`qqbot_message_event` 同时承担事件 Outbox:扇出状态为 `accepted`、`processing`、`retry`、`completed`、`failed`;按目标冻结的 `qqbot_message_delivery` 状态为 `waiting_ddns`、`pending`、`processing`、`retry`、`success`、`failed`、`superseded`、`cancelled`。扇出和投递每次各领取最多 50 行,处理租约为 30 秒;进程启动后立即恢复,并每 5 秒扫描一次。`waiting_ddns` 每 60 秒持久化复检;临时错误从 10 秒开始指数退避,单次最长 15 分钟,并以事件发生后 24 小时为截止时间。
|
||||||
|
|
||||||
@ -237,7 +237,7 @@ bash scripts/bangdream-render-smoke.sh --operation-key bangdream.event.stage --t
|
|||||||
QQBot 系统消息推送按以下顺序发布和回滚:
|
QQBot 系统消息推送按以下顺序发布和回滚:
|
||||||
|
|
||||||
1. 备份 `qqbot_message_subscription`、`qqbot_message_template`、`qqbot_message_publish_binding`、`qqbot_message_publish_target`、`qqbot_message_event`、`qqbot_message_delivery`,以及本功能相关的 `admin_menu` / `admin_role_menu` 行。
|
1. 备份 `qqbot_message_subscription`、`qqbot_message_template`、`qqbot_message_publish_binding`、`qqbot_message_publish_target`、`qqbot_message_event`、`qqbot_message_delivery`,以及本功能相关的 `admin_menu` / `admin_role_menu` 行。
|
||||||
2. 既有环境只应用本功能的幂等增量入口 `sql/qqbot-message-push-init.sql`,随后执行只读的 `sql/qqbot-message-push-verify.sql`;不要把包含历史迁移的 `sql/qqbot-init.sql` 作为本功能生产迁移。仅一次性、可丢弃的全量初始化环境按顺序使用 `sql/refactor-v3/00-full-schema.sql`、`01-seed-core.sql`、`99-verify.sql`。
|
2. 既有环境只应用本功能的幂等增量入口 `sql/qqbot-message-push-init.sql`,随后执行只读的 `sql/qqbot-message-push-verify.sql`;不要把包含历史迁移的 `sql/qqbot-init.sql` 作为本功能生产迁移。仅一次性、可丢弃的全量初始化环境按顺序使用 `sql/refactor-v3/00-full-schema.sql`、`01-seed-core.sql`、`99-verify.sql`。发布前还需通过网络菜单 SQL 应用独立 `System:Network:PortForward:Natmap` 权限并确认只授予活动 `super`。
|
||||||
3. 验证六表、17 个精确索引、默认模板、18 个页面/按钮菜单和活动 `super/admin` 角色授权。
|
3. 验证六表、17 个精确索引、默认模板、18 个页面/按钮菜单和活动 `super/admin` 角色授权。
|
||||||
4. 先发布并验证 API 健康检查、旧 QQBot 发送能力和新只读接口,再发布并验证 Admin。
|
4. 先发布并验证 API 健康检查、旧 QQBot 发送能力和新只读接口,再发布并验证 Admin。
|
||||||
5. 管理员显式创建订阅,并在每个发布账号中选择模板、配置群聊/私聊目标和启用绑定;随后用授权的非生产目标做一次有界 A→B 端口变化、DDNS 门禁、发送日志和幂等验收。
|
5. 管理员显式创建订阅,并在每个发布账号中选择模板、配置群聊/私聊目标和启用绑定;随后用授权的非生产目标做一次有界 A→B 端口变化、DDNS 门禁、发送日志和幂等验收。
|
||||||
|
|||||||
@ -19,7 +19,8 @@ VALUES
|
|||||||
(2041700000000120223, 2041700000000100207, 'SystemNetworkDdnsCreate', NULL, NULL, NULL, 'System:Network:Ddns:Create', 'button', '{"title":"common.create"}', 1, 10),
|
(2041700000000120223, 2041700000000100207, 'SystemNetworkDdnsCreate', NULL, NULL, NULL, 'System:Network:Ddns:Create', 'button', '{"title":"common.create"}', 1, 10),
|
||||||
(2041700000000120224, 2041700000000100207, 'SystemNetworkDdnsUpdate', NULL, NULL, NULL, 'System:Network:Ddns:Update', 'button', '{"title":"common.edit"}', 1, 11),
|
(2041700000000120224, 2041700000000100207, 'SystemNetworkDdnsUpdate', NULL, NULL, NULL, 'System:Network:Ddns:Update', 'button', '{"title":"common.edit"}', 1, 11),
|
||||||
(2041700000000120225, 2041700000000100207, 'SystemNetworkDdnsDelete', NULL, NULL, NULL, 'System:Network:Ddns:Delete', 'button', '{"title":"common.delete"}', 1, 12),
|
(2041700000000120225, 2041700000000100207, 'SystemNetworkDdnsDelete', NULL, NULL, NULL, 'System:Network:Ddns:Delete', 'button', '{"title":"common.delete"}', 1, 12),
|
||||||
(2041700000000120226, 2041700000000100207, 'SystemNetworkDdnsRetry', NULL, NULL, NULL, 'System:Network:Ddns:Retry', 'button', '{"title":"common.retry"}', 1, 13)
|
(2041700000000120226, 2041700000000100207, 'SystemNetworkDdnsRetry', NULL, NULL, NULL, 'System:Network:Ddns:Retry', 'button', '{"title":"common.retry"}', 1, 13),
|
||||||
|
(2041700000000120227, 2041700000000100207, 'SystemNetworkPortForwardNatmap', NULL, NULL, NULL, 'System:Network:PortForward:Natmap', 'button', '{"title":"system.network.natmap"}', 1, 14)
|
||||||
ON DUPLICATE KEY UPDATE
|
ON DUPLICATE KEY UPDATE
|
||||||
`name` = VALUES(`name`),
|
`name` = VALUES(`name`),
|
||||||
`pid` = VALUES(`pid`),
|
`pid` = VALUES(`pid`),
|
||||||
@ -46,6 +47,7 @@ WHERE role.`role_code` <> 'super'
|
|||||||
'SystemNetworkPortForwardDelete',
|
'SystemNetworkPortForwardDelete',
|
||||||
'SystemNetworkPortForwardRetry',
|
'SystemNetworkPortForwardRetry',
|
||||||
'SystemNetworkPortForwardKeeper',
|
'SystemNetworkPortForwardKeeper',
|
||||||
|
'SystemNetworkPortForwardNatmap',
|
||||||
'SystemNetworkPortForwardProbe',
|
'SystemNetworkPortForwardProbe',
|
||||||
'SystemNetworkPortForwardHistory',
|
'SystemNetworkPortForwardHistory',
|
||||||
'SystemNetworkDdnsList',
|
'SystemNetworkDdnsList',
|
||||||
@ -66,6 +68,7 @@ JOIN `admin_menu` menu ON menu.`name` IN (
|
|||||||
'SystemNetworkPortForwardDelete',
|
'SystemNetworkPortForwardDelete',
|
||||||
'SystemNetworkPortForwardRetry',
|
'SystemNetworkPortForwardRetry',
|
||||||
'SystemNetworkPortForwardKeeper',
|
'SystemNetworkPortForwardKeeper',
|
||||||
|
'SystemNetworkPortForwardNatmap',
|
||||||
'SystemNetworkPortForwardProbe',
|
'SystemNetworkPortForwardProbe',
|
||||||
'SystemNetworkPortForwardHistory',
|
'SystemNetworkPortForwardHistory',
|
||||||
'SystemNetworkDdnsList',
|
'SystemNetworkDdnsList',
|
||||||
|
|||||||
@ -1162,6 +1162,13 @@ WHERE NOT EXISTS (
|
|||||||
WHERE `source_key` = 'network.stun.mapping-port-changed' AND `name` = 'STUN 映射端口变更默认模板' AND `is_deleted` = 0
|
WHERE `source_key` = 'network.stun.mapping-port-changed' AND `name` = 'STUN 映射端口变更默认模板' AND `is_deleted` = 0
|
||||||
);
|
);
|
||||||
|
|
||||||
|
INSERT INTO `qqbot_message_template` (`id`, `name`, `source_key`, `content`, `enabled`, `remark`, `is_deleted`)
|
||||||
|
SELECT 2041700000000200602, 'TCP NATMap 端点变更默认模板', 'network.tcp.natmap-endpoint-changed', '当前 TCP NATMap 端点已变更为 ${{endpoint}}', 1, '系统默认模板', 0
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM `qqbot_message_template`
|
||||||
|
WHERE `source_key` = 'network.tcp.natmap-endpoint-changed' AND `name` = 'TCP NATMap 端点变更默认模板' AND `is_deleted` = 0
|
||||||
|
);
|
||||||
|
|
||||||
INSERT INTO `admin_menu` (`id`, `pid`, `name`, `path`, `component`, `redirect`, `auth_code`, `type`, `meta`, `status`, `sort`) VALUES
|
INSERT INTO `admin_menu` (`id`, `pid`, `name`, `path`, `component`, `redirect`, `auth_code`, `type`, `meta`, `status`, `sort`) VALUES
|
||||||
(2041700000000100413,2041700000000100400,'QqBotMessageSubscription','/qqbot/message-subscription','/qqbot/message-subscription/list',NULL,'QqBot:MessageSubscription:List','menu','{"icon":"lucide:bell-ring","title":"消息订阅"}',1,10),
|
(2041700000000100413,2041700000000100400,'QqBotMessageSubscription','/qqbot/message-subscription','/qqbot/message-subscription/list',NULL,'QqBot:MessageSubscription:List','menu','{"icon":"lucide:bell-ring","title":"消息订阅"}',1,10),
|
||||||
(2041700000000100414,2041700000000100400,'QqBotMessageTemplate','/qqbot/message-template','/qqbot/message-template/list',NULL,'QqBot:MessageTemplate:List','menu','{"icon":"lucide:message-square-plus","title":"消息模板"}',1,11),
|
(2041700000000100414,2041700000000100400,'QqBotMessageTemplate','/qqbot/message-template','/qqbot/message-template/list',NULL,'QqBot:MessageTemplate:List','menu','{"icon":"lucide:message-square-plus","title":"消息模板"}',1,11),
|
||||||
|
|||||||
@ -123,6 +123,23 @@ WHERE NOT EXISTS (
|
|||||||
AND is_deleted = 0
|
AND is_deleted = 0
|
||||||
);
|
);
|
||||||
|
|
||||||
|
INSERT INTO qqbot_message_template (id, name, source_key, content, enabled, remark, is_deleted)
|
||||||
|
SELECT
|
||||||
|
2041700000000200602,
|
||||||
|
'TCP NATMap 端点变更默认模板',
|
||||||
|
'network.tcp.natmap-endpoint-changed',
|
||||||
|
'当前 TCP NATMap 端点已变更为 ${{endpoint}}',
|
||||||
|
1,
|
||||||
|
'系统默认模板',
|
||||||
|
0
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM qqbot_message_template
|
||||||
|
WHERE source_key = 'network.tcp.natmap-endpoint-changed'
|
||||||
|
AND name = 'TCP NATMap 端点变更默认模板'
|
||||||
|
AND is_deleted = 0
|
||||||
|
);
|
||||||
|
|
||||||
INSERT INTO admin_menu (
|
INSERT INTO admin_menu (
|
||||||
id,
|
id,
|
||||||
pid,
|
pid,
|
||||||
|
|||||||
@ -97,6 +97,15 @@ WHERE id = 2041700000000200601
|
|||||||
AND enabled = 1
|
AND enabled = 1
|
||||||
AND is_deleted = 0;
|
AND is_deleted = 0;
|
||||||
|
|
||||||
|
SELECT 'seed_qqbot_tcp_natmap_message_template' AS check_name, COUNT(*) AS matched_rows
|
||||||
|
FROM qqbot_message_template
|
||||||
|
WHERE id = 2041700000000200602
|
||||||
|
AND BINARY name = BINARY 'TCP NATMap 端点变更默认模板'
|
||||||
|
AND BINARY source_key = BINARY 'network.tcp.natmap-endpoint-changed'
|
||||||
|
AND BINARY content = BINARY '当前 TCP NATMap 端点已变更为 ${{endpoint}}'
|
||||||
|
AND enabled = 1
|
||||||
|
AND is_deleted = 0;
|
||||||
|
|
||||||
SELECT 'seed_qqbot_message_push_menu' AS check_name, COUNT(*) AS matched_rows
|
SELECT 'seed_qqbot_message_push_menu' AS check_name, COUNT(*) AS matched_rows
|
||||||
FROM admin_menu
|
FROM admin_menu
|
||||||
WHERE id IN (
|
WHERE id IN (
|
||||||
|
|||||||
@ -150,6 +150,7 @@ INSERT INTO admin_menu (
|
|||||||
(2041700000000120224, 2041700000000100207, 'SystemNetworkDdnsUpdate', NULL, NULL, NULL, 'System:Network:Ddns:Update', 'button', '{"title":"common.edit"}', 1, 11),
|
(2041700000000120224, 2041700000000100207, 'SystemNetworkDdnsUpdate', NULL, NULL, NULL, 'System:Network:Ddns:Update', 'button', '{"title":"common.edit"}', 1, 11),
|
||||||
(2041700000000120225, 2041700000000100207, 'SystemNetworkDdnsDelete', NULL, NULL, NULL, 'System:Network:Ddns:Delete', 'button', '{"title":"common.delete"}', 1, 12),
|
(2041700000000120225, 2041700000000100207, 'SystemNetworkDdnsDelete', NULL, NULL, NULL, 'System:Network:Ddns:Delete', 'button', '{"title":"common.delete"}', 1, 12),
|
||||||
(2041700000000120226, 2041700000000100207, 'SystemNetworkDdnsRetry', NULL, NULL, NULL, 'System:Network:Ddns:Retry', 'button', '{"title":"common.retry"}', 1, 13),
|
(2041700000000120226, 2041700000000100207, 'SystemNetworkDdnsRetry', NULL, NULL, NULL, 'System:Network:Ddns:Retry', 'button', '{"title":"common.retry"}', 1, 13),
|
||||||
|
(2041700000000120227, 2041700000000100207, 'SystemNetworkPortForwardNatmap', NULL, NULL, NULL, 'System:Network:PortForward:Natmap', 'button', '{"title":"system.network.natmap"}', 1, 14),
|
||||||
(
|
(
|
||||||
2041700000000100400,
|
2041700000000100400,
|
||||||
0,
|
0,
|
||||||
@ -220,6 +221,7 @@ WHERE role.role_code <> 'super'
|
|||||||
'SystemNetworkPortForwardDelete',
|
'SystemNetworkPortForwardDelete',
|
||||||
'SystemNetworkPortForwardRetry',
|
'SystemNetworkPortForwardRetry',
|
||||||
'SystemNetworkPortForwardKeeper',
|
'SystemNetworkPortForwardKeeper',
|
||||||
|
'SystemNetworkPortForwardNatmap',
|
||||||
'SystemNetworkPortForwardProbe',
|
'SystemNetworkPortForwardProbe',
|
||||||
'SystemNetworkPortForwardHistory',
|
'SystemNetworkPortForwardHistory',
|
||||||
'SystemNetworkDdnsList',
|
'SystemNetworkDdnsList',
|
||||||
@ -640,6 +642,15 @@ WHERE NOT EXISTS (
|
|||||||
AND is_deleted = 0
|
AND is_deleted = 0
|
||||||
);
|
);
|
||||||
|
|
||||||
|
INSERT INTO qqbot_message_template (id, name, source_key, content, enabled, remark, is_deleted)
|
||||||
|
SELECT 2041700000000200602, 'TCP NATMap 端点变更默认模板', 'network.tcp.natmap-endpoint-changed', '当前 TCP NATMap 端点已变更为 ${{endpoint}}', 1, '系统默认模板', 0
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM qqbot_message_template
|
||||||
|
WHERE source_key = 'network.tcp.natmap-endpoint-changed'
|
||||||
|
AND name = 'TCP NATMap 端点变更默认模板'
|
||||||
|
AND is_deleted = 0
|
||||||
|
);
|
||||||
|
|
||||||
INSERT INTO admin_menu (id, pid, name, path, component, redirect, auth_code, type, meta, status, sort) VALUES
|
INSERT INTO admin_menu (id, pid, name, path, component, redirect, auth_code, type, meta, status, sort) VALUES
|
||||||
(2041700000000100413,2041700000000100400,'QqBotMessageSubscription','/qqbot/message-subscription','/qqbot/message-subscription/list',NULL,'QqBot:MessageSubscription:List','menu','{"icon":"lucide:bell-ring","title":"消息订阅"}',1,10),
|
(2041700000000100413,2041700000000100400,'QqBotMessageSubscription','/qqbot/message-subscription','/qqbot/message-subscription/list',NULL,'QqBot:MessageSubscription:List','menu','{"icon":"lucide:bell-ring","title":"消息订阅"}',1,10),
|
||||||
(2041700000000100414,2041700000000100400,'QqBotMessageTemplate','/qqbot/message-template','/qqbot/message-template/list',NULL,'QqBot:MessageTemplate:List','menu','{"icon":"lucide:message-square-plus","title":"消息模板"}',1,11),
|
(2041700000000100414,2041700000000100400,'QqBotMessageTemplate','/qqbot/message-template','/qqbot/message-template/list',NULL,'QqBot:MessageTemplate:List','menu','{"icon":"lucide:message-square-plus","title":"消息模板"}',1,11),
|
||||||
|
|||||||
@ -2,6 +2,7 @@ SELECT 'admin_user' AS table_name, COUNT(*) AS row_count FROM admin_user;
|
|||||||
SELECT 'admin_role' AS table_name, COUNT(*) AS row_count FROM admin_role;
|
SELECT 'admin_role' AS table_name, COUNT(*) AS row_count FROM admin_role;
|
||||||
SELECT 'admin_menu' AS table_name, COUNT(*) AS row_count FROM admin_menu;
|
SELECT 'admin_menu' AS table_name, COUNT(*) AS row_count FROM admin_menu;
|
||||||
SELECT 'network_port_forward' AS table_name, COUNT(*) AS row_count FROM network_port_forward;
|
SELECT 'network_port_forward' AS table_name, COUNT(*) AS row_count FROM network_port_forward;
|
||||||
|
SELECT 'network_port_forward_group' AS table_name, COUNT(*) AS row_count FROM network_port_forward_group;
|
||||||
SELECT 'network_ddns_record' AS table_name, COUNT(*) AS row_count FROM network_ddns_record;
|
SELECT 'network_ddns_record' AS table_name, COUNT(*) AS row_count FROM network_ddns_record;
|
||||||
SELECT 'network_agent_state' AS table_name, COUNT(*) AS row_count FROM network_agent_state;
|
SELECT 'network_agent_state' AS table_name, COUNT(*) AS row_count FROM network_agent_state;
|
||||||
SELECT 'network_endpoint_history' AS table_name, COUNT(*) AS row_count FROM network_endpoint_history;
|
SELECT 'network_endpoint_history' AS table_name, COUNT(*) AS row_count FROM network_endpoint_history;
|
||||||
@ -38,6 +39,15 @@ WHERE id = 2041700000000200601
|
|||||||
AND enabled = 1
|
AND enabled = 1
|
||||||
AND is_deleted = 0;
|
AND is_deleted = 0;
|
||||||
|
|
||||||
|
SELECT 'seed_qqbot_tcp_natmap_message_template' AS check_name, COUNT(*) AS matched_rows
|
||||||
|
FROM qqbot_message_template
|
||||||
|
WHERE id = 2041700000000200602
|
||||||
|
AND BINARY name = BINARY 'TCP NATMap 端点变更默认模板'
|
||||||
|
AND BINARY source_key = BINARY 'network.tcp.natmap-endpoint-changed'
|
||||||
|
AND BINARY content = BINARY '当前 TCP NATMap 端点已变更为 ${{endpoint}}'
|
||||||
|
AND enabled = 1
|
||||||
|
AND is_deleted = 0;
|
||||||
|
|
||||||
WITH expected_menu AS (
|
WITH expected_menu AS (
|
||||||
SELECT 2041700000000100413 AS id, 2041700000000100400 AS pid, 'QqBotMessageSubscription' AS name, 'QqBot:MessageSubscription:List' AS auth_code, 10 AS sort
|
SELECT 2041700000000100413 AS id, 2041700000000100400 AS pid, 'QqBotMessageSubscription' AS name, 'QqBot:MessageSubscription:List' AS auth_code, 10 AS sort
|
||||||
UNION ALL SELECT 2041700000000100414, 2041700000000100400, 'QqBotMessageTemplate', 'QqBot:MessageTemplate:List', 11
|
UNION ALL SELECT 2041700000000100414, 2041700000000100400, 'QqBotMessageTemplate', 'QqBot:MessageTemplate:List', 11
|
||||||
@ -148,6 +158,26 @@ WHERE table_schema = DATABASE()
|
|||||||
AND table_name = 'network_port_forward'
|
AND table_name = 'network_port_forward'
|
||||||
AND index_name = 'uk_network_port_forward_active_key';
|
AND index_name = 'uk_network_port_forward_active_key';
|
||||||
|
|
||||||
|
SELECT 'index_network_port_forward_active_group_protocol_key' AS check_name, COUNT(*) AS matched_rows
|
||||||
|
FROM information_schema.statistics
|
||||||
|
WHERE table_schema = DATABASE()
|
||||||
|
AND table_name = 'network_port_forward'
|
||||||
|
AND index_name = 'uk_network_port_forward_active_group_protocol_key'
|
||||||
|
AND non_unique = 0;
|
||||||
|
|
||||||
|
SELECT 'network_port_forward_active_group_protocol_key_conflicts' AS check_name, COUNT(*) AS invalid_rows
|
||||||
|
FROM (
|
||||||
|
SELECT active_group_protocol_key
|
||||||
|
FROM network_port_forward
|
||||||
|
WHERE active_group_protocol_key IS NOT NULL
|
||||||
|
GROUP BY active_group_protocol_key
|
||||||
|
HAVING COUNT(*) > 1
|
||||||
|
) conflicts;
|
||||||
|
|
||||||
|
SELECT 'network_endpoint_history_mechanism_values' AS check_name, COUNT(*) AS invalid_rows
|
||||||
|
FROM network_endpoint_history
|
||||||
|
WHERE mechanism NOT IN ('udp_stun', 'tcp_natmap');
|
||||||
|
|
||||||
SELECT 'index_network_ddns_record_active_key' AS check_name, COUNT(*) AS matched_rows
|
SELECT 'index_network_ddns_record_active_key' AS check_name, COUNT(*) AS matched_rows
|
||||||
FROM information_schema.statistics
|
FROM information_schema.statistics
|
||||||
WHERE table_schema = DATABASE()
|
WHERE table_schema = DATABASE()
|
||||||
@ -271,6 +301,92 @@ WHERE role.role_code <> 'super'
|
|||||||
'System:Network:Ddns:Retry'
|
'System:Network:Ddns:Retry'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
SELECT 'seed_network_natmap_permission' AS check_name, COUNT(*) AS matched_rows
|
||||||
|
FROM admin_menu
|
||||||
|
WHERE id = 2041700000000120227
|
||||||
|
AND BINARY name = BINARY 'SystemNetworkPortForwardNatmap'
|
||||||
|
AND BINARY auth_code = BINARY 'System:Network:PortForward:Natmap'
|
||||||
|
AND status = 1
|
||||||
|
AND is_deleted = 0;
|
||||||
|
|
||||||
|
SELECT 'seed_network_natmap_super_permission' AS check_name, COUNT(*) AS matched_rows
|
||||||
|
FROM admin_role_menu role_menu
|
||||||
|
JOIN admin_role role ON role.id = role_menu.role_id
|
||||||
|
JOIN admin_menu menu ON menu.id = role_menu.menu_id
|
||||||
|
WHERE role.role_code = 'super'
|
||||||
|
AND role.status = 1
|
||||||
|
AND role.is_deleted = 0
|
||||||
|
AND menu.id = 2041700000000120227
|
||||||
|
AND BINARY menu.name = BINARY 'SystemNetworkPortForwardNatmap'
|
||||||
|
AND BINARY menu.auth_code = BINARY 'System:Network:PortForward:Natmap'
|
||||||
|
AND menu.status = 1
|
||||||
|
AND menu.is_deleted = 0;
|
||||||
|
|
||||||
|
SELECT 'network_natmap_non_super_permissions_should_be_zero' AS check_name, COUNT(*) AS invalid_rows
|
||||||
|
FROM admin_role_menu role_menu
|
||||||
|
JOIN admin_role role ON role.id = role_menu.role_id
|
||||||
|
JOIN admin_menu menu ON menu.id = role_menu.menu_id
|
||||||
|
WHERE role.role_code <> 'super'
|
||||||
|
AND role.is_deleted = 0
|
||||||
|
AND menu.id = 2041700000000120227
|
||||||
|
AND menu.is_deleted = 0;
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
'network_8213_udp_channel_state' AS check_name,
|
||||||
|
channel.id,
|
||||||
|
channel.group_id,
|
||||||
|
channel.name,
|
||||||
|
channel.external_port,
|
||||||
|
channel.internal_port,
|
||||||
|
channel.protocol,
|
||||||
|
channel.target_ipv4,
|
||||||
|
channel.desired_presence,
|
||||||
|
channel.keeper_desired_enabled,
|
||||||
|
channel.keeper_status,
|
||||||
|
channel.desired_revision,
|
||||||
|
channel.reported_revision,
|
||||||
|
channel.current_public_ipv4,
|
||||||
|
channel.current_public_port,
|
||||||
|
channel.last_observed_ipv4,
|
||||||
|
channel.last_observed_port,
|
||||||
|
channel.is_deleted,
|
||||||
|
forwarding.protocol_mode AS group_protocol_mode,
|
||||||
|
forwarding.external_port AS group_external_port,
|
||||||
|
forwarding.internal_port AS group_internal_port,
|
||||||
|
forwarding.target_ipv4 AS group_target_ipv4,
|
||||||
|
forwarding.is_deleted AS group_is_deleted
|
||||||
|
FROM network_port_forward channel
|
||||||
|
JOIN network_port_forward_group forwarding ON forwarding.id = channel.group_id
|
||||||
|
WHERE channel.external_port = 8213
|
||||||
|
AND channel.protocol = 'udp';
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
'network_8213_udp_ddns_state' AS check_name,
|
||||||
|
ddns.id,
|
||||||
|
ddns.port_forward_id,
|
||||||
|
ddns.name,
|
||||||
|
ddns.record_type,
|
||||||
|
ddns.source_type,
|
||||||
|
ddns.domain,
|
||||||
|
ddns.sub_domain,
|
||||||
|
ddns.enabled,
|
||||||
|
ddns.sync_status,
|
||||||
|
ddns.provider_record_id,
|
||||||
|
ddns.source_address,
|
||||||
|
ddns.applied_address,
|
||||||
|
ddns.retry_count,
|
||||||
|
ddns.next_retry_at,
|
||||||
|
ddns.last_attempt_at,
|
||||||
|
ddns.last_synced_at,
|
||||||
|
ddns.last_error_code,
|
||||||
|
ddns.last_error_message,
|
||||||
|
ddns.is_deleted
|
||||||
|
FROM network_ddns_record ddns
|
||||||
|
JOIN network_port_forward channel ON ddns.port_forward_id = channel.id
|
||||||
|
WHERE ddns.source_type = 'port_forward_ipv4'
|
||||||
|
AND channel.external_port = 8213
|
||||||
|
AND channel.protocol = 'udp';
|
||||||
|
|
||||||
SELECT 'index_admin_user_username' AS check_name, COUNT(*) AS matched_rows
|
SELECT 'index_admin_user_username' AS check_name, COUNT(*) AS matched_rows
|
||||||
FROM information_schema.statistics
|
FROM information_schema.statistics
|
||||||
WHERE table_schema = DATABASE()
|
WHERE table_schema = DATABASE()
|
||||||
|
|||||||
@ -253,6 +253,7 @@ VALUES
|
|||||||
(2041700000000120224, 2041700000000100207, 'SystemNetworkDdnsUpdate', NULL, NULL, NULL, 'System:Network:Ddns:Update', 'button', '{"title":"common.edit"}', 1, 11),
|
(2041700000000120224, 2041700000000100207, 'SystemNetworkDdnsUpdate', NULL, NULL, NULL, 'System:Network:Ddns:Update', 'button', '{"title":"common.edit"}', 1, 11),
|
||||||
(2041700000000120225, 2041700000000100207, 'SystemNetworkDdnsDelete', NULL, NULL, NULL, 'System:Network:Ddns:Delete', 'button', '{"title":"common.delete"}', 1, 12),
|
(2041700000000120225, 2041700000000100207, 'SystemNetworkDdnsDelete', NULL, NULL, NULL, 'System:Network:Ddns:Delete', 'button', '{"title":"common.delete"}', 1, 12),
|
||||||
(2041700000000120226, 2041700000000100207, 'SystemNetworkDdnsRetry', NULL, NULL, NULL, 'System:Network:Ddns:Retry', 'button', '{"title":"common.retry"}', 1, 13),
|
(2041700000000120226, 2041700000000100207, 'SystemNetworkDdnsRetry', NULL, NULL, NULL, 'System:Network:Ddns:Retry', 'button', '{"title":"common.retry"}', 1, 13),
|
||||||
|
(2041700000000120227, 2041700000000100207, 'SystemNetworkPortForwardNatmap', NULL, NULL, NULL, 'System:Network:PortForward:Natmap', 'button', '{"title":"system.network.natmap"}', 1, 14),
|
||||||
(2041700000000100300, 0, 'Blog', '/blog', NULL, '/blog/article', NULL, 'catalog', '{"icon":"lucide:newspaper","order":100,"title":"博客管理"}', 1, 100),
|
(2041700000000100300, 0, 'Blog', '/blog', NULL, '/blog/article', NULL, 'catalog', '{"icon":"lucide:newspaper","order":100,"title":"博客管理"}', 1, 100),
|
||||||
(2041700000000100301, 2041700000000100300, 'BlogArticle', '/blog/article', '/blog/article/list', NULL, 'Blog:Article:List', 'menu', '{"icon":"lucide:file-text","title":"文章管理"}', 1, 0),
|
(2041700000000100301, 2041700000000100300, 'BlogArticle', '/blog/article', '/blog/article/list', NULL, 'Blog:Article:List', 'menu', '{"icon":"lucide:file-text","title":"文章管理"}', 1, 0),
|
||||||
(2041700000000120301, 2041700000000100301, 'BlogArticleCreate', NULL, NULL, NULL, 'Blog:Article:Create', 'button', '{"title":"common.create"}', 1, 0),
|
(2041700000000120301, 2041700000000100301, 'BlogArticleCreate', NULL, NULL, NULL, 'Blog:Article:Create', 'button', '{"title":"common.create"}', 1, 0),
|
||||||
@ -379,6 +380,7 @@ WHERE `is_deleted` = 0
|
|||||||
'SystemNetworkPortForwardDelete',
|
'SystemNetworkPortForwardDelete',
|
||||||
'SystemNetworkPortForwardRetry',
|
'SystemNetworkPortForwardRetry',
|
||||||
'SystemNetworkPortForwardKeeper',
|
'SystemNetworkPortForwardKeeper',
|
||||||
|
'SystemNetworkPortForwardNatmap',
|
||||||
'SystemNetworkPortForwardProbe',
|
'SystemNetworkPortForwardProbe',
|
||||||
'SystemNetworkPortForwardHistory',
|
'SystemNetworkPortForwardHistory',
|
||||||
'SystemNetworkDdnsList',
|
'SystemNetworkDdnsList',
|
||||||
|
|||||||
@ -21,16 +21,18 @@ describe('System network-management SQL', () => {
|
|||||||
'Delete',
|
'Delete',
|
||||||
'Retry',
|
'Retry',
|
||||||
'Keeper',
|
'Keeper',
|
||||||
|
'Natmap',
|
||||||
'Probe',
|
'Probe',
|
||||||
'History',
|
'History',
|
||||||
];
|
];
|
||||||
for (const sql of [menuSql, initSql]) {
|
for (const sql of [menuSql, initSql, canonicalSeed]) {
|
||||||
expect(sql).toContain("'SystemNetwork'");
|
expect(sql).toContain("'SystemNetwork'");
|
||||||
expect(sql).toContain("'/system/network/list'");
|
expect(sql).toContain("'/system/network/list'");
|
||||||
for (const action of actions) {
|
for (const action of actions) {
|
||||||
expect(sql).toContain(`'SystemNetworkPortForward${action}'`);
|
expect(sql).toContain(`'SystemNetworkPortForward${action}'`);
|
||||||
expect(sql).toContain(`'System:Network:PortForward:${action}'`);
|
expect(sql).toContain(`'System:Network:PortForward:${action}'`);
|
||||||
}
|
}
|
||||||
|
expect(sql.match(/2041700000000120227/g)).toHaveLength(1);
|
||||||
expect(sql).not.toMatch(/PortForward(Check|Ensure)/);
|
expect(sql).not.toMatch(/PortForward(Check|Ensure)/);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -39,6 +41,13 @@ describe('System network-management SQL', () => {
|
|||||||
expect(menuSql).toContain("role.`role_code` <> 'super'");
|
expect(menuSql).toContain("role.`role_code` <> 'super'");
|
||||||
expect(menuSql).toContain("role.`role_code` = 'super'");
|
expect(menuSql).toContain("role.`role_code` = 'super'");
|
||||||
expect(menuSql).toContain('ON DUPLICATE KEY UPDATE');
|
expect(menuSql).toContain('ON DUPLICATE KEY UPDATE');
|
||||||
|
expect(initSql).toContain('SELECT 2041700000000010001, `id`');
|
||||||
|
expect(initSql).toMatch(
|
||||||
|
/AND `name` NOT IN \([\s\S]*?'SystemNetworkPortForwardNatmap'/,
|
||||||
|
);
|
||||||
|
expect(canonicalSeed).toMatch(
|
||||||
|
/role\.role_code <> 'super'[\s\S]*?SystemNetworkPortForwardNatmap[\s\S]*?SELECT 2041700000000010001, id/,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('defines and verifies all three control-plane tables and singleton seed', () => {
|
it('defines and verifies all three control-plane tables and singleton seed', () => {
|
||||||
@ -57,10 +66,70 @@ describe('System network-management SQL', () => {
|
|||||||
expect(canonicalSeed).toContain("'nas-main'");
|
expect(canonicalSeed).toContain("'nas-main'");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('verifies group/channel integrity, NATMap permission, and the exact 8213 UDP state', () => {
|
||||||
|
for (const checkName of [
|
||||||
|
'network_port_forward_group',
|
||||||
|
'network_port_forward_active_group_protocol_key_conflicts',
|
||||||
|
'network_endpoint_history_mechanism_values',
|
||||||
|
'seed_network_natmap_permission',
|
||||||
|
'seed_network_natmap_super_permission',
|
||||||
|
'network_natmap_non_super_permissions_should_be_zero',
|
||||||
|
'network_8213_udp_channel_state',
|
||||||
|
'network_8213_udp_ddns_state',
|
||||||
|
]) {
|
||||||
|
expect(canonicalVerify).toContain(`'${checkName}'`);
|
||||||
|
}
|
||||||
|
expect(canonicalVerify).toContain(
|
||||||
|
"'uk_network_port_forward_active_group_protocol_key'",
|
||||||
|
);
|
||||||
|
expect(canonicalVerify).toMatch(
|
||||||
|
/external_port\s*=\s*8213[\s\S]*?protocol\s*=\s*'udp'/,
|
||||||
|
);
|
||||||
|
expect(canonicalVerify).toMatch(
|
||||||
|
/port_forward_id\s*=\s*channel\.id[\s\S]*?channel\.external_port\s*=\s*8213/,
|
||||||
|
);
|
||||||
|
const ddnsDefinition = canonicalSchema.match(
|
||||||
|
/CREATE TABLE IF NOT EXISTS network_ddns_record \(([\s\S]*?)\) ENGINE=/,
|
||||||
|
)?.[1];
|
||||||
|
expect(ddnsDefinition).toBeDefined();
|
||||||
|
for (const column of [
|
||||||
|
'port_forward_id',
|
||||||
|
'record_type',
|
||||||
|
'source_type',
|
||||||
|
'domain',
|
||||||
|
'sub_domain',
|
||||||
|
'enabled',
|
||||||
|
'sync_status',
|
||||||
|
'provider_record_id',
|
||||||
|
'source_address',
|
||||||
|
'applied_address',
|
||||||
|
'retry_count',
|
||||||
|
'next_retry_at',
|
||||||
|
'last_attempt_at',
|
||||||
|
'last_synced_at',
|
||||||
|
'last_error_code',
|
||||||
|
'last_error_message',
|
||||||
|
'is_deleted',
|
||||||
|
]) {
|
||||||
|
expect(ddnsDefinition).toMatch(new RegExp(`\\b${column}\\b`));
|
||||||
|
expect(canonicalVerify).toContain(`ddns.${column}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('contains no router credential or Palworld-specific schema', () => {
|
it('contains no router credential or Palworld-specific schema', () => {
|
||||||
const allSql = [menuSql, schemaSql, canonicalSchema].join('\n');
|
const allSql = [
|
||||||
|
menuSql,
|
||||||
|
initSql,
|
||||||
|
schemaSql,
|
||||||
|
canonicalSchema,
|
||||||
|
canonicalSeed,
|
||||||
|
canonicalVerify,
|
||||||
|
].join('\n');
|
||||||
expect(allSql).not.toMatch(
|
expect(allSql).not.toMatch(
|
||||||
/router_password|xiaomi_password|palworld|PDC_/i,
|
/router_password|xiaomi_password|palworld|PDC_|-----BEGIN (?:CERTIFICATE|PRIVATE KEY)-----/i,
|
||||||
|
);
|
||||||
|
expect(allSql).not.toMatch(
|
||||||
|
/NETWORK_DDNS_DNSPOD_SECRET_(?:ID|KEY)\s*=\s*\S+/,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -405,6 +405,13 @@ const extractExpectedMenuIds = (statement: string) =>
|
|||||||
[...extractExpectedMenuCte(statement).matchAll(/select (\d+)(?: as id)?/g)].map(([, id]) => id);
|
[...extractExpectedMenuCte(statement).matchAll(/select (\d+)(?: as id)?/g)].map(([, id]) => id);
|
||||||
|
|
||||||
describe('QQBot message-push SQL contract', () => {
|
describe('QQBot message-push SQL contract', () => {
|
||||||
|
const tcpSourceAdapter = readFileSync(
|
||||||
|
resolve(
|
||||||
|
process.cwd(),
|
||||||
|
'src/modules/admin/platform-config/network-management/network-tcp-natmap-message-source.adapter.ts',
|
||||||
|
),
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
const bootstrapSql = readNormalizedSql('sql/qqbot-init.sql');
|
const bootstrapSql = readNormalizedSql('sql/qqbot-init.sql');
|
||||||
const migrationSql = readNormalizedSql('sql/qqbot-message-push-init.sql');
|
const migrationSql = readNormalizedSql('sql/qqbot-message-push-init.sql');
|
||||||
const migrationVerifySql = readNormalizedSql('sql/qqbot-message-push-verify.sql');
|
const migrationVerifySql = readNormalizedSql('sql/qqbot-message-push-verify.sql');
|
||||||
@ -485,6 +492,33 @@ describe('QQBot message-push SQL contract', () => {
|
|||||||
for (const sql of [bootstrapSql, seedSql, migrationSql]) expect(sql).toContain(expectedTemplate);
|
for (const sql of [bootstrapSql, seedSql, migrationSql]) expect(sql).toContain(expectedTemplate);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('adds the exact TCP NATMap template without changing the STUN template', () => {
|
||||||
|
const expectedTcpTemplate = normalizeSql(`
|
||||||
|
insert into qqbot_message_template (id, name, source_key, content, enabled, remark, is_deleted)
|
||||||
|
select 2041700000000200602, 'TCP NATMap 端点变更默认模板',
|
||||||
|
'network.tcp.natmap-endpoint-changed', '当前 TCP NATMap 端点已变更为 \${{endpoint}}', 1, '系统默认模板', 0
|
||||||
|
where not exists (
|
||||||
|
select 1 from qqbot_message_template
|
||||||
|
where source_key = 'network.tcp.natmap-endpoint-changed'
|
||||||
|
and name = 'TCP NATMap 端点变更默认模板' and is_deleted = 0
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
for (const sql of [bootstrapSql, seedSql, migrationSql]) {
|
||||||
|
expect(sql).toContain(expectedTcpTemplate);
|
||||||
|
expect(sql.match(/2041700000000200602/g)).toHaveLength(1);
|
||||||
|
expect(sql).toContain(
|
||||||
|
"'network.stun.mapping-port-changed', '当前STUN的端口已变更为${{endpoint}}'",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps the TCP template source key aligned with source version 1', () => {
|
||||||
|
expect(tcpSourceAdapter).toMatch(
|
||||||
|
/const SOURCE_KEY = 'network\.tcp\.natmap-endpoint-changed';/,
|
||||||
|
);
|
||||||
|
expect(tcpSourceAdapter).toMatch(/version:\s*1,/);
|
||||||
|
});
|
||||||
|
|
||||||
it('verifies the default template stable values with case-sensitive text comparisons', () => {
|
it('verifies the default template stable values with case-sensitive text comparisons', () => {
|
||||||
const defaultTemplateVerify = extractVerificationStatement(verifySql, 'seed_qqbot_message_template');
|
const defaultTemplateVerify = extractVerificationStatement(verifySql, 'seed_qqbot_message_template');
|
||||||
expect(defaultTemplateVerify).toBe(normalizeSql(`
|
expect(defaultTemplateVerify).toBe(normalizeSql(`
|
||||||
@ -499,6 +533,25 @@ describe('QQBot message-push SQL contract', () => {
|
|||||||
`));
|
`));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('verifies the TCP template stable values in current and migration summaries', () => {
|
||||||
|
for (const sql of [verifySql, migrationVerifySql]) {
|
||||||
|
const tcpTemplateVerify = extractVerificationStatement(
|
||||||
|
sql,
|
||||||
|
'seed_qqbot_tcp_natmap_message_template',
|
||||||
|
);
|
||||||
|
expect(tcpTemplateVerify).toBe(normalizeSql(`
|
||||||
|
select 'seed_qqbot_tcp_natmap_message_template' as check_name, count(*) as matched_rows
|
||||||
|
from qqbot_message_template
|
||||||
|
where id = 2041700000000200602
|
||||||
|
and binary name = binary 'TCP NATMap 端点变更默认模板'
|
||||||
|
and binary source_key = binary 'network.tcp.natmap-endpoint-changed'
|
||||||
|
and binary content = binary '当前 TCP NATMap 端点已变更为 \${{endpoint}}'
|
||||||
|
and enabled = 1
|
||||||
|
and is_deleted = 0;
|
||||||
|
`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('verifies every message-push table with its own complete row-count statement', () => {
|
it('verifies every message-push table with its own complete row-count statement', () => {
|
||||||
for (const table of Object.keys(messagePushTableDefinitions)) {
|
for (const table of Object.keys(messagePushTableDefinitions)) {
|
||||||
const statement = extractVerificationStatement(verifySql, table);
|
const statement = extractVerificationStatement(verifySql, table);
|
||||||
@ -543,6 +596,24 @@ describe('QQBot message-push SQL contract', () => {
|
|||||||
expect(migrationSql).toContain('insert ignore into admin_role_menu');
|
expect(migrationSql).toContain('insert ignore into admin_role_menu');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not seed real accounts, targets, provider credentials, or certificates', () => {
|
||||||
|
const sql = [
|
||||||
|
bootstrapSql,
|
||||||
|
migrationSql,
|
||||||
|
migrationVerifySql,
|
||||||
|
seedSql,
|
||||||
|
verifySql,
|
||||||
|
vbenSql,
|
||||||
|
].join('\n');
|
||||||
|
expect(sql).not.toMatch(
|
||||||
|
/insert into qqbot_message_(?:publish_binding|publish_target)\b/,
|
||||||
|
);
|
||||||
|
expect(sql).not.toMatch(/NETWORK_DDNS_DNSPOD_SECRET_(?:ID|KEY)\s*=\s*\S+/);
|
||||||
|
expect(sql).not.toMatch(
|
||||||
|
/router_password|xiaomi_password|-----BEGIN (?:CERTIFICATE|PRIVATE KEY)-----/i,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('keeps the production verification scoped to safe summaries', () => {
|
it('keeps the production verification scoped to safe summaries', () => {
|
||||||
for (const table of Object.keys(messagePushTableDefinitions)) {
|
for (const table of Object.keys(messagePushTableDefinitions)) {
|
||||||
expect(migrationVerifySql).toContain(`'${table}'`);
|
expect(migrationVerifySql).toContain(`'${table}'`);
|
||||||
@ -551,6 +622,10 @@ describe('QQBot message-push SQL contract', () => {
|
|||||||
migrationVerifySql,
|
migrationVerifySql,
|
||||||
'seed_qqbot_message_template',
|
'seed_qqbot_message_template',
|
||||||
)).toContain("binary content = binary '当前STUN的端口已变更为${{endpoint}}'");
|
)).toContain("binary content = binary '当前STUN的端口已变更为${{endpoint}}'");
|
||||||
|
expect(extractVerificationStatement(
|
||||||
|
migrationVerifySql,
|
||||||
|
'seed_qqbot_tcp_natmap_message_template',
|
||||||
|
)).toContain("binary content = binary '当前 TCP NATMap 端点已变更为 ${{endpoint}}'");
|
||||||
expect(extractVerificationStatement(
|
expect(extractVerificationStatement(
|
||||||
migrationVerifySql,
|
migrationVerifySql,
|
||||||
'seed_qqbot_message_push_menu',
|
'seed_qqbot_message_push_menu',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user