diff --git a/.helloagents/CHANGELOG.md b/.helloagents/CHANGELOG.md index faafde2..4664e65 100644 --- a/.helloagents/CHANGELOG.md +++ b/.helloagents/CHANGELOG.md @@ -1,5 +1,12 @@ # CHANGELOG +## [0.6.0] - 2026-04-27 + +### 新增 +- **[node-gfw-check]**: 新增节点墙状态检测闭环,管理端可对父节点发起检测并在节点列表展示、搜索和筛选正常 / 疑似被墙 / 部分异常 / 检测失败 / 未检测状态;子节点不单独检测并继承父节点状态,mi-node 支持 `gfw.check` WS 触发、REST 兜底领取和三网 ping 结果上报 — by yinjianm + - 方案: [202604272325_node-gfw-check](archive/2026-04/202604272325_node-gfw-check/) + - 决策: node-gfw-check#D001(使用 WS 触发 + REST 兜底), node-gfw-check#D002(子节点继承父节点墙状态) + ## [0.5.19] - 2026-04-27 ### 新增 diff --git a/.helloagents/INDEX.md b/.helloagents/INDEX.md index a289d0b..c15ddde 100644 --- a/.helloagents/INDEX.md +++ b/.helloagents/INDEX.md @@ -10,12 +10,13 @@ active_package: 无 ## 项目概览 - 类型: PHP Laravel 主仓 + `admin-frontend` Vue3 管理端前端 -- 当前重点模块: `admin-frontend`、`order-payment`、`subscription-protocols` -- 最新归档: `202604272310_ticket-chat-image-dnd-paste-upload` +- 当前重点模块: `admin-frontend`、`node-gfw-check`、`order-payment`、`subscription-protocols` +- 最新归档: `202604272325_node-gfw-check` ## 活跃模块 - [admin-frontend](modules/admin-frontend.md): 管理端登录、主布局、仪表盘、用户/节点/订阅/系统管理与管理 API 前端封装 +- [node-gfw-check](modules/node-gfw-check.md): 节点墙状态检测任务、父/子节点继承规则、mi-node 检测上报链路 - [order-payment](modules/order-payment.md): 订单支付成功快照、第三方回调元信息透传与后台支付成功信息展示 - [subscription-protocols](modules/subscription-protocols.md): 客户端订阅导出入口、协议适配器与版本兼容过滤 diff --git a/.helloagents/archive/2026-04/202604272325_node-gfw-check/proposal.md b/.helloagents/archive/2026-04/202604272325_node-gfw-check/proposal.md new file mode 100644 index 0000000..537a7f7 --- /dev/null +++ b/.helloagents/archive/2026-04/202604272325_node-gfw-check/proposal.md @@ -0,0 +1,223 @@ +# 变更提案: node-gfw-check + +## 元信息 +```yaml +类型: 新功能 +方案类型: implementation +优先级: P1 +状态: 已选方案 +创建: 2026-04-27 +``` + +--- + +## 1. 需求 + +### 背景 +节点管理页目前无法判断节点公网 IP 是否疑似被中国防火墙拦截。用户确认墙是双向影响,但当前暂时没有墙内探测 IP,因此第一阶段只让节点服务器主动 ping 国内三网目标,参考 `E:\code\shell\shell-script\network-tools\test_delay.sh` 的检测思路,不复用其中 Telegram 通知与自动安装依赖逻辑。 + +### 目标 +- 在 Xboard 管理端支持对父节点发起墙状态检测,并在节点列表中显示检测结果。 +- 子节点默认不单独检测,展示与筛选继承父节点最新墙状态,并标记为随父节点。 +- 节点端 `E:\code\go\mi-node` 支持接收检测任务、执行国内三网 ping、结构化上报结果。 +- 搜索与筛选支持区分被墙、正常、异常、未检测、随父节点。 + +### 约束条件 +```yaml +时间约束: 本轮落地基础闭环,不等待后续墙内检测 IP。 +性能约束: 检测任务低频触发,节点端并发 ping 要设置超时与并发上限,避免阻塞主服务循环。 +兼容性约束: 现有节点同步、用户同步、设备同步、REST fallback 不可回归;旧节点端未支持 gfw.check 时管理端应保留 pending/failed 可见状态。 +业务约束: 只对父节点创建实际检测任务;子节点继承父节点结果,不作为独立检测目标。 +安全约束: 不引入 Telegram token/chat_id;不在生产节点自动安装系统依赖;不执行破坏性命令。 +``` + +### 验收标准 +- [ ] 管理端节点列表可以发起单个/批量墙状态检测,子节点不会被独立下发检测。 +- [ ] `getNodes` 返回 `gfw_check` 字段;子节点返回父节点结果并带 `inherited=true` 与 `source_node_id`。 +- [ ] mi-node 收到 `gfw.check` WS 事件后执行检测并上报;WS 不可用时可通过 REST 任务接口兜底。 +- [ ] 前端节点旁显示墙状态标签,筛选和搜索可过滤被墙/正常/异常/未检测/随父节点。 +- [ ] PHP 语法检查、前端 build、mi-node Go 测试通过或明确记录无法执行原因。 + +--- + +## 2. 方案 + +### 技术方案 +采用已确认的方案 A:`WS 触发 + REST 兜底 + 子节点继承父节点状态`。 + +- 后端新增 `server_gfw_checks` 表与 `ServerGfwCheck` 模型,记录每次父节点检测任务、状态、摘要、原始结果和错误。 +- 后端新增 `ServerGfwCheckService`,负责发起检测、过滤子节点、推送 `gfw.check`、提供 REST 兜底任务读取、接收节点端报告、计算最终状态。 +- 管理端新增 `POST /server/manage/checkGfw`,支持单个/批量节点 ID。输入中子节点不下发任务,按父节点继承规则返回 skipped/inherited。 +- 节点端新增 `GET /server/gfw/task` 与 `POST /server/gfw/report`,由 `ServerV2` 鉴权保护。 +- mi-node 新增 `internal/gfwcheck` 包,内置三网目标,使用系统 `ping` 并发检测。Docker runtime 安装 `iputils`。 +- 前端新增墙状态类型、筛选器、标签和动作入口。关键词搜索覆盖中文状态词。 + +### 影响范围 +```yaml +涉及模块: + - Xboard 后端: 新表、新模型、新服务、管理端接口、节点端接口、节点列表返回字段、WS 推送事件。 + - admin-frontend: 节点页筛选、搜索、状态标签、单行/批量检测动作、API 类型。 + - mi-node: WS 事件解析、控制面事件转发、服务层检测执行、REST 兜底轮询与上报、Docker 运行依赖。 +预计变更文件: 20 个左右,覆盖 PHP、Vue/TypeScript、Go、Dockerfile 与方案包。 +``` + +### 风险评估 +| 风险 | 等级 | 应对 | +|------|------|------| +| 部分节点环境没有 `ping` 或缺 ICMP 权限 | 中 | 节点端检测失败时上报 `failed` 与错误;Docker 镜像补 `iputils`,不自动安装依赖 | +| 国内目标临时不可达导致误判 | 中 | 按运营商分组统计,区分 `blocked` 与 `partial`,保存 raw_result 供人工核对 | +| 旧 mi-node 不支持新事件 | 中 | 后端保留 REST task 与 checking/pending 状态,前端清楚显示未完成/失败 | +| 子节点继承状态造成误解 | 低 | `gfw_check.inherited=true`,前端显示“随父节点”并在 tooltip 中说明来源 | +| 检测任务阻塞主服务循环 | 中 | mi-node 在 goroutine 中执行,服务层避免同一 check 并发重复执行 | + +### 方案取舍 +```yaml +唯一方案理由: WS 触发能让在线节点立即执行检测,REST 兜底能覆盖 WS 不可用或旧链路;只检测父节点符合当前子节点中转落地到父节点的业务模型。 +放弃的替代路径: + - 只用管理端远程 ping 节点: 无法判断节点主动访问国内目标是否被墙,且与用户确认的双向墙逻辑不匹配。 + - 立即引入墙内探测节点: 当前没有墙内检测 IP,无法落地;后续可在同一数据模型中扩展为双向检测。 + - 子节点逐个检测: 子节点一般是中转入口,实际落地到父节点;逐个检测会制造噪音并增加不必要任务。 +回滚边界: 可回滚新增接口、服务、前端入口和 mi-node 事件处理;数据库新增表独立,不改动 v2_server 结构。 +``` + +--- + +## 3. 技术设计 + +### 架构设计 +```mermaid +flowchart TD + A[Admin NodesView] -->|POST checkGfw| B[ManageController] + B --> C[ServerGfwCheckService] + C --> D[(server_gfw_checks)] + C -->|gfw.check| E[NodeSyncService / WS] + C -->|pending task| F[GET /server/gfw/task] + E --> G[mi-node service] + F --> G + G --> H[internal/gfwcheck ping runner] + H -->|structured result| I[POST /server/gfw/report] + I --> C + C --> D + A -->|fetchNodes| J[getNodes + latest gfw_check] +``` + +### API 设计 +#### POST `/api/v2/{secure_path}/server/manage/checkGfw` +- **请求**: `{ "ids": [1, 2, 3] }` +- **响应**: `{ "started": [...], "skipped": [...], "total": 3 }` + +#### GET `/api/v2/server/gfw/task` +- **请求**: `token/node_id` 鉴权参数 +- **响应**: `{ "data": { "check_id": 123, "targets": {...}, "ping_count": 2, "timeout_seconds": 2, "parallel": 12 } }` 或 `null` + +#### POST `/api/v2/server/gfw/report` +- **请求**: `{ "check_id": 123, "status": "normal|blocked|partial|failed", "summary": {...}, "raw_result": {...}, "error_message": null }` +- **响应**: `{ "data": true }` + +### 数据模型 +| 字段 | 类型 | 说明 | +|------|------|------| +| id | big integer | 检测记录 ID | +| server_id | big integer | 父节点 ID | +| status | string | pending/checking/normal/blocked/partial/failed/skipped | +| triggered_by | unsigned big integer nullable | 发起检测的管理员 ID | +| summary | json nullable | 后端/前端摘要 | +| operator_summary | json nullable | 三网聚合结果 | +| raw_result | json nullable | 节点端原始结构化结果 | +| error_message | text nullable | 失败原因 | +| checked_at | unsigned integer nullable | 完成检测时间戳 | +| timestamps | timestamps | 创建/更新时间 | + +--- + +## 4. 核心场景 + +### 场景: 管理员检测父节点 +**模块**: Xboard 后端 + admin-frontend +**条件**: 管理员在节点列表选择父节点,点击检测墙状态。 +**行为**: 后端创建 checking 记录并推送 `gfw.check`,前端刷新显示“检测中”。 +**结果**: mi-node 上报后节点旁显示正常/疑似被墙/部分异常/检测失败。 + +### 场景: 管理员选择子节点 +**模块**: Xboard 后端 + admin-frontend +**条件**: 管理员选择子节点发起检测。 +**行为**: 后端不对该子节点创建独立任务,返回 skipped/inherited。 +**结果**: 前端提示子节点随父节点,列表显示父节点检测结果来源。 + +### 场景: WS 不可用 +**模块**: Xboard 后端 + mi-node +**条件**: 节点端没有收到 `gfw.check` WS 事件。 +**行为**: mi-node 定期查询 `/server/gfw/task` 获取未完成任务。 +**结果**: 任务仍可被执行并上报。 + +--- + +## 5. 技术决策 + +### node-gfw-check#D001: 使用 WS 触发 + REST 兜底 +**日期**: 2026-04-27 +**状态**: ✅采纳 +**背景**: 管理端需要尽快触发在线节点检测,但不能依赖 WS 一定可用。 +**选项分析**: +| 选项 | 优点 | 缺点 | +|------|------|------| +| A: WS 触发 + REST 兜底 | 在线响应快,离线/断连可兜底,符合现有节点同步架构 | 需要同时改两条链路 | +| B: 仅 REST 轮询 | 实现较少,行为稳定 | 检测响应慢,用户点击后等待不确定 | +| C: 仅 WS | 响应最快 | WS 不可用时任务丢失 | +**决策**: 选择方案 A。 +**理由**: 与现有 `NodeSyncService`、`NodeWorker`、mi-node 控制面抽象兼容,兼顾及时性与可靠性。 +**影响**: 后端新增任务接口与推送事件;mi-node 新增 WS 事件和 REST 轮询。 + +### node-gfw-check#D002: 子节点继承父节点墙状态 +**日期**: 2026-04-27 +**状态**: ✅采纳 +**背景**: 用户明确说明子节点通常作为中转,实际落地到父节点。 +**选项分析**: +| 选项 | 优点 | 缺点 | +|------|------|------| +| A: 子节点继承父节点 | 符合业务模型,减少噪音 | 需要 UI 明确说明来源 | +| B: 子节点也独立检测 | 看似更完整 | 结果不代表真实落地路径,任务成本高 | +**决策**: 选择方案 A。 +**理由**: 检测目标应代表落地 IP,父节点更接近真实出口。 +**影响**: 后端 `getNodes` 拼接继承状态;前端筛选按有效状态处理。 + +--- + +## 6. 验证策略 + +```yaml +verifyMode: review-first +reviewerFocus: + - app/Services/ServerGfwCheckService.php 的状态判定和子节点过滤 + - app/Http/Controllers/V2/Server/ServerController.php 的节点端报告鉴权与 check_id 归属校验 + - E:/code/go/mi-node/internal/gfwcheck 的 ping 超时、并发和错误处理 +testerFocus: + - php -l 新增/修改 PHP 文件 + - cd admin-frontend && npm run build + - cd E:/code/go/mi-node && go test ./... +uiValidation: required +riskBoundary: + - 不执行数据库迁移到生产库 + - 不复用参考脚本中的 Telegram token/chat_id + - 不自动安装节点系统依赖 +``` + +--- + +## 7. 成果设计 + +### 设计方向 +- **美学基调**: Apple 式后台精致极简,延续当前节点页黑色 hero 与白色工作台,不新增抢眼装饰色。 +- **记忆点**: 节点名旁新增一个轻量“连通性信号”状态胶囊,正常、检测中、疑似被墙、随父节点可以一眼区分但不压过节点名。 +- **参考**: `apple/DESIGN.md` 与现有 `NodesView.vue`。 + +### 视觉要素 +- **配色**: 继续使用页面黑白主节奏;交互强调使用 Apple Blue `#0071e3`;风险态沿用 Element Plus 语义色,避免新增复杂色盘。 +- **字体**: 继承项目现有字体栈与 Element Plus 表格字号,节点状态使用较小胶囊标签保持信息密度。 +- **布局**: 筛选栏增加“墙状态”下拉;节点单元格内在在线状态下方并列墙状态标签,不新增大面积卡片。 +- **动效**: 检测中使用按钮 loading 与标签文案反馈,不加额外动画。 +- **氛围**: 维持白色工作台与细分隔的管理后台质感,不使用渐变、纹理或装饰背景。 + +### 技术约束 +- **可访问性**: 状态不只靠颜色区分,标签文本必须明确;操作按钮 loading/disabled 状态可见。 +- **响应式**: 复用现有 toolbar wrap;新增筛选宽度与现有 select 一致,移动端自然换行。 diff --git a/.helloagents/archive/2026-04/202604272325_node-gfw-check/tasks.md b/.helloagents/archive/2026-04/202604272325_node-gfw-check/tasks.md new file mode 100644 index 0000000..25b8b99 --- /dev/null +++ b/.helloagents/archive/2026-04/202604272325_node-gfw-check/tasks.md @@ -0,0 +1,135 @@ +# 任务清单: node-gfw-check + +> **@status:** completed | 2026-04-27 23:40 + +```yaml +@feature: node-gfw-check +@created: 2026-04-27 +@status: completed +@mode: R2 +``` + +## LIVE_STATUS + +```json +{"status":"completed","completed":15,"failed":0,"pending":0,"total":15,"percent":100,"current":"实现与验证完成,准备归档方案包","updated_at":"2026-04-27 23:58:00"} +``` + +## 进度概览 + +| 完成 | 失败 | 跳过 | 总数 | +|------|------|------|------| +| 15 | 0 | 0 | 15 | + +--- + +## 任务列表 + +### 1. Xboard 后端数据与服务 + +- [√] 1.1 新增 `database/migrations/*_create_server_gfw_checks_table.php` + - 预期变更: 创建 `server_gfw_checks` 表,保存父节点检测任务、状态、摘要、原始结果、错误和完成时间。 + - 完成标准: migration 结构完整,down 可删除表,索引覆盖 `server_id/status/created_at` 查询。 + - 验证方式: `php -l database/migrations/*_create_server_gfw_checks_table.php`(当前环境无 PHP CLI,已做代码审查) + - depends_on: [] +- [√] 1.2 新增 `app/Models/ServerGfwCheck.php` 并扩展 `app/Models/Server.php` + - 预期变更: 增加检测记录模型、JSON casts、状态常量、`Server::gfwChecks()` 关系。 + - 完成标准: 模型可被服务层创建、查询、更新,PHP 语法通过。 + - 验证方式: `php -l app/Models/ServerGfwCheck.php app/Models/Server.php`(当前环境无 PHP CLI,已做代码审查) + - depends_on: [1.1] +- [√] 1.3 新增 `app/Services/ServerGfwCheckService.php` + - 预期变更: 实现 `startChecks`、`getPendingTaskForNode`、`reportResult`、`decorateServers`、状态判定与子节点继承。 + - 完成标准: 父节点下发任务;子节点返回 skipped/inherited;报告 check_id 必须归属当前节点。 + - 验证方式: `php -l app/Services/ServerGfwCheckService.php`(当前环境无 PHP CLI,已做代码审查) + - depends_on: [1.2] + +### 2. Xboard 后端接口与 WS + +- [√] 2.1 修改 `app/Http/Controllers/V2/Admin/Server/ManageController.php` 与 `app/Http/Routes/V2/AdminRoute.php` + - 预期变更: `getNodes` 附加 `gfw_check`;新增 `checkGfw` 管理接口。 + - 完成标准: 管理端可以按 ids 发起检测,响应包含 started/skipped/total。 + - 验证方式: `php -l app/Http/Controllers/V2/Admin/Server/ManageController.php app/Http/Routes/V2/AdminRoute.php`(当前环境无 PHP CLI,已做代码审查) + - depends_on: [1.3] +- [√] 2.2 修改 `app/Http/Controllers/V2/Server/ServerController.php` 与 `app/Http/Routes/V2/ServerRoute.php` + - 预期变更: 新增 `gfwTask` 和 `gfwReport` 节点端接口。 + - 完成标准: 通过 `server.v2` 鉴权读取节点;节点只能领取/上报自己的父节点检测任务。 + - 验证方式: `php -l app/Http/Controllers/V2/Server/ServerController.php app/Http/Routes/V2/ServerRoute.php`(当前环境无 PHP CLI,已做代码审查) + - depends_on: [1.3] +- [√] 2.3 修改 `app/WebSocket/NodeWorker.php` 或相关事件处理 + - 预期变更: 确保 `gfw.check` 作为 panel->node 推送事件能经现有 Redis/NodeRegistry 发送到节点端。 + - 完成标准: 不影响现有 `sync.*` 和 `report.devices` 事件。 + - 验证方式: 代码审查确认现有 Redis/NodeRegistry 已支持任意 panel->node event,无需修改 `NodeWorker.php` + - depends_on: [2.1] + +### 3. admin-frontend 节点管理 UI + +- [√] 3.1 修改 `admin-frontend/src/types/api.d.ts` 与 `admin-frontend/src/api/admin.ts` + - 预期变更: 增加 `AdminNodeGfwCheck`、`AdminNodeGfwStatus`、`checkNodeGfw` API。 + - 完成标准: TypeScript 类型覆盖节点列表字段和检测接口响应。 + - 验证方式: `cd admin-frontend && npm run build` + - depends_on: [2.1] +- [√] 3.2 修改 `admin-frontend/src/utils/nodes.ts` + - 预期变更: 增加墙状态 meta、tooltip、状态筛选类型,搜索文本包含被墙/正常/异常/未检测/随父节点。 + - 完成标准: `filterNodes` 可按墙状态过滤,关键词可命中墙状态中文词。 + - 验证方式: `cd admin-frontend && npm run build` + - depends_on: [3.1] +- [√] 3.3 修改 `admin-frontend/src/views/nodes/NodesView.vue` + - 预期变更: 增加墙状态筛选、节点旁状态标签、单行检测、批量检测与 loading 状态。 + - 完成标准: 父节点可检测;子节点操作提示随父节点;UI 保持现有节点页风格。 + - 验证方式: `cd admin-frontend && npm run build` + - depends_on: [3.2] + +### 4. mi-node 检测执行与上报 + +- [√] 4.1 新增 `E:/code/go/mi-node/internal/gfwcheck` + - 预期变更: 实现三网目标定义、并发 ping runner、结果汇总与状态判定。 + - 完成标准: 不依赖 shell 脚本,不包含 Telegram/自动安装逻辑;超时和并发可控。 + - 验证方式: `cd E:/code/go/mi-node && go test ./internal/gfwcheck` + - depends_on: [] +- [√] 4.2 修改 `E:/code/go/mi-node/internal/panel` 与 `E:/code/go/mi-node/internal/controlplane` + - 预期变更: 支持 `gfw.check` WS 事件、REST 获取任务、上报检测结果。 + - 完成标准: 事件可转为 service 层事件;REST payload 自动携带 token/node_id。 + - 验证方式: `cd E:/code/go/mi-node && go test ./internal/panel ./internal/controlplane` + - depends_on: [4.1] +- [√] 4.3 修改 `E:/code/go/mi-node/internal/service/service.go` 与 `E:/code/go/mi-node/internal/config/config.go` + - 预期变更: 服务层处理 WS 检测事件,增加低频 REST 兜底轮询,避免重复执行同一 check。 + - 完成标准: 检测在后台执行,不阻塞主 select 循环;支持配置默认轮询间隔。 + - 验证方式: `cd E:/code/go/mi-node && go test ./internal/service ./internal/config` + - depends_on: [4.2] +- [√] 4.4 修改 `E:/code/go/mi-node/Dockerfile` + - 预期变更: runtime 镜像安装 `iputils` 以提供 `ping`。 + - 完成标准: 不新增无关依赖,不改变入口。 + - 验证方式: 文件审查 + - depends_on: [4.1] + +### 5. 验证与知识库同步 + +- [√] 5.1 执行端到端验证命令 + - 预期变更: 运行 PHP 语法检查、前端 build、mi-node Go 测试,记录结果。 + - 完成标准: 可执行验证通过;不可执行项记录原因与残余风险。 + - 验证方式: `php -l ...`、`cd admin-frontend && npm run build`、`cd E:/code/go/mi-node && go test ./...` + - depends_on: [1.1,1.2,1.3,2.1,2.2,2.3,3.1,3.2,3.3,4.1,4.2,4.3,4.4] +- [√] 5.2 更新 `.helloagents/CHANGELOG.md` 与方案包状态 + - 预期变更: 记录本次新增墙状态检测闭环,更新任务状态、LIVE_STATUS 和执行日志。 + - 完成标准: CHANGELOG 与 tasks.md 反映实际完成内容和验证结果。 + - 验证方式: 文件审查 + - depends_on: [5.1] + +--- + +## 执行日志 + +| 时间 | 任务 | 状态 | 备注 | +|------|------|------|------| +| 2026-04-27 23:25 | DESIGN | in_progress | 创建方案包并固化方案 A | +| 2026-04-27 23:42 | backend/frontend/mi-node | completed | 完成后端接口、前端节点页、mi-node 检测与上报链路 | +| 2026-04-27 23:50 | verification | completed | `npm run build` 通过,`go test ./...` 通过;PHP CLI 不在 PATH | +| 2026-04-27 23:55 | knowledge | completed | 更新 CHANGELOG 与模块知识库 | + +--- + +## 执行备注 + +- 当前没有墙内检测 IP,本轮只做节点主动 ping 国内三网目标。 +- 子节点默认不独立检测,显示和筛选继承父节点墙状态。 +- 参考脚本中的 Telegram 通知和自动安装依赖不进入项目实现。 diff --git a/.helloagents/archive/_index.md b/.helloagents/archive/_index.md index 0ada825..3a7ef24 100644 --- a/.helloagents/archive/_index.md +++ b/.helloagents/archive/_index.md @@ -7,6 +7,7 @@ | 时间戳 | 名称 | 类型 | 涉及模块 | 决策 | 结果 | |--------|------|------|---------|------|------| +| 202604272325 | node-gfw-check | implementation | node-gfw-check,admin-frontend,mi-node | node-gfw-check#D001,#D002 | ✅完成 | | 202604272310 | ticket-chat-image-dnd-paste-upload | implementation | admin-frontend | ticket-chat-image-dnd-paste-upload#D001 | ✅完成 | | 202604250018 | admin-frontend-user-activity-status-filter | implementation | admin-frontend,backend | admin-frontend-user-activity-status-filter#D001,#D002,#D003 | ✅完成 | | 202604250002 | order-payment-snapshot | implementation | admin-frontend,order-payment | order-payment-snapshot#D001,#D002 | ✅完成 | @@ -37,6 +38,7 @@ ## 按月归档 ### 2026-04 +- [202604272325_node-gfw-check](./2026-04/202604272325_node-gfw-check/) - 新增节点墙状态检测闭环,支持父节点检测、子节点继承、管理端展示筛选,以及 mi-node WS/REST 检测上报 - [202604272310_ticket-chat-image-dnd-paste-upload](./2026-04/202604272310_ticket-chat-image-dnd-paste-upload/) - 为工单工作台回复区补齐图片拖拽上传与剪贴板粘贴上传,并将上传逻辑与样式从超大 SFC 中拆分 - [202604250018_admin-frontend-user-activity-status-filter](./2026-04/202604250018_admin-frontend-user-activity-status-filter/) - 为用户管理高级筛选新增“活跃状态”条件,并在后端补齐 `activity_status` 复合过滤规则,支持按活跃 / 非活跃筛选用户 - [202604250002_order-payment-snapshot](./2026-04/202604250002_order-payment-snapshot/) - 补齐订单支付成功快照保存链路,并在后台订单详情中集中展示支付渠道、支付方法、平台订单号、商户订单号、实付金额与支付 IP diff --git a/.helloagents/modules/_index.md b/.helloagents/modules/_index.md index e65b6b7..fdcf5d0 100644 --- a/.helloagents/modules/_index.md +++ b/.helloagents/modules/_index.md @@ -3,5 +3,6 @@ | 模块名 | 说明 | 最近更新 | |--------|------|----------| | [admin-frontend](admin-frontend.md) | 管理端前端登录、布局、仪表盘、用户管理、节点管理与管理 API 封装 | 2026-04-25 | +| [node-gfw-check](node-gfw-check.md) | 节点墙状态检测任务、父/子节点继承规则、mi-node 检测上报链路 | 2026-04-27 | | [order-payment](order-payment.md) | 订单支付成功快照、第三方回调元信息透传与后台支付成功信息展示 | 2026-04-25 | | [subscription-protocols](subscription-protocols.md) | 用户订阅导出入口、协议适配器与 Stash / Clash 系列兼容过滤 | 2026-04-24 | diff --git a/.helloagents/modules/admin-frontend.md b/.helloagents/modules/admin-frontend.md index 27f8ac8..1d8601a 100644 --- a/.helloagents/modules/admin-frontend.md +++ b/.helloagents/modules/admin-frontend.md @@ -41,6 +41,7 @@ - TUIC 表单默认以 V5 / V4 版本选择、`h3 / h2 / http/1.1` ALPN 选项和 `native / quic` UDP Relay Mode 对齐后端协议模板;AnyTLS Padding Scheme 默认值与 `Server` 模型完整模板保持一致 - 节点排序采用本地草稿 + 上移 / 下移模式,保存时向 `server/manage/sort` 提交 `{ id, order }[]` 顺序 payload - 节点列表现支持本地分页、在线 / 离线筛选、父/子节点筛选,以及跨分页稳定勾选;批量修改 / 批量删除仅作用于已勾选节点,其中批量修改可统一更新 `host / group_ids / rate` +- 节点管理页现支持墙状态展示、墙状态筛选与关键词搜索;父节点可通过行级或批量操作发起检测,子节点不单独检测并显示“随父节点”的继承状态 - 节点行级菜单现已补齐“置顶节点”,会复用当前排序结果生成新的顺序 payload 并提交到 `server/manage/sort` - 权限组管理页使用真实后端 `server/group/fetch`、`server/group/save` 与 `server/group/drop`,支持关键字搜索、新增/编辑中央弹窗、删除确认,以及从节点数量列跳转到 `#/nodes?group={id}` 的筛选联动 - 路由管理页使用真实后端 `server/route/fetch`、`server/route/save` 与 `server/route/drop`,支持路由列表、关键词搜索、新增/编辑中央弹窗、删除与动作值展示 @@ -95,6 +96,7 @@ - 依赖 `src/utils/notices.ts` 负责公告表单转换、内容摘要、排序与显示字段归一化 - 依赖 `src/utils/systemConfig.ts` 负责系统配置字段元信息、默认值、回填与保存序列化 - 依赖 `src/utils/routes.ts` 负责路由动作映射、匹配规则序列化、节点引用摘要与搜索过滤 +- 依赖 `src/utils/nodes.ts` 负责节点在线状态、父/子节点、墙状态 meta、搜索文本和筛选逻辑 - 依赖 `src/views/tickets/useTicketReplyImages.ts` 收敛工单回复区图片点击上传、拖拽上传、粘贴上传、文件校验和 Markdown 插入 - 依赖 Laravel 后端 `TicketService::reply()` 提供工单“再次回复自动重开”的统一业务语义 - 依赖 Laravel 注入的 `window.settings` diff --git a/.helloagents/modules/node-gfw-check.md b/.helloagents/modules/node-gfw-check.md new file mode 100644 index 0000000..9cb97c7 --- /dev/null +++ b/.helloagents/modules/node-gfw-check.md @@ -0,0 +1,30 @@ +# node-gfw-check + +## 职责 + +- 在 Xboard 后端维护节点墙状态检测任务和检测结果 +- 通过管理端接口触发父节点墙状态检测,并通过节点端接口提供 REST 兜底领取与结果上报 +- 通过 `NodeSyncService` 发送 `gfw.check` WS 事件,让在线 mi-node 可即时执行检测 +- 在节点列表返回 `gfw_check` 字段,并对子节点应用父节点检测结果继承规则 + +## 行为规范 + +- 检测任务只对父节点创建;带 `parent_id` 的子节点不单独下发任务 +- 子节点列表展示继承父节点最新 `gfw_check`,并返回 `inherited=true` 与 `source_node_id` +- `server_gfw_checks.status` 使用 `pending / checking / normal / blocked / partial / failed / skipped` +- 管理端 `POST server/manage/checkGfw` 接收 `{ ids: number[] }`,响应中区分 `started` 与 `skipped` +- 节点端 `GET server/gfw/task` 只向父节点返回待执行任务;节点端 `POST server/gfw/report` 必须校验 `check_id` 归属当前节点 +- 当前检测方向只做节点服务器主动 ping 国内三网目标;后续墙内探测 IP 可在同一任务模型中扩展 +- 参考脚本中的 Telegram 通知、chat_id、bot token 和自动安装依赖逻辑不得进入项目实现 +- mi-node 使用 Go 原生 runner 调用系统 `ping`,按三网目标并发检测并结构化上报 `summary / operator_summary / raw_result` +- Docker runtime 镜像需要提供 `ping`,当前通过 Alpine `iputils` 满足 + +## 依赖关系 + +- 依赖 `app/Services/ServerGfwCheckService.php` 统一处理任务创建、状态判定、结果装饰和子节点继承 +- 依赖 `app/Models/ServerGfwCheck.php` 与 `server_gfw_checks` 表保存检测记录 +- 依赖 `app/Http/Controllers/V2/Admin/Server/ManageController.php` 暴露管理端触发接口 +- 依赖 `app/Http/Controllers/V2/Server/ServerController.php` 暴露节点端任务领取和上报接口 +- 依赖 `app/Services/NodeSyncService.php` 与 Workerman WS 通道向在线节点推送 `gfw.check` +- 依赖 `E:/code/go/mi-node/internal/gfwcheck` 执行 ping 检测和结果判定 +- 依赖 `E:/code/go/mi-node/internal/panel`、`internal/controlplane` 与 `internal/service` 接收任务、轮询兜底并上报结果 diff --git a/.helloagents/plan/202604272338_admin-frontend-node-auto-online/.status.json b/.helloagents/plan/202604272338_admin-frontend-node-auto-online/.status.json new file mode 100644 index 0000000..640b86c --- /dev/null +++ b/.helloagents/plan/202604272338_admin-frontend-node-auto-online/.status.json @@ -0,0 +1,11 @@ +{ + "status": "in_progress", + "completed": 0, + "failed": 0, + "pending": 8, + "total": 8, + "done": 0, + "percent": 0, + "current": "方案包已创建,准备进入开发实施", + "updated_at": "2026-04-27 23:38:00" +} diff --git a/.helloagents/plan/202604272338_admin-frontend-node-auto-online/proposal.md b/.helloagents/plan/202604272338_admin-frontend-node-auto-online/proposal.md new file mode 100644 index 0000000..10dcb9f --- /dev/null +++ b/.helloagents/plan/202604272338_admin-frontend-node-auto-online/proposal.md @@ -0,0 +1,197 @@ +# 变更提案: admin-frontend-node-auto-online + +## 元信息 +```yaml +类型: 新功能 +方案类型: implementation +优先级: P1 +状态: 已选定方案 +创建: 2026-04-27 +``` + +--- + +## 1. 需求 + +### 背景 +节点管理页当前已经展示在线、离线、待同步和显隐状态,但 `show` 仍完全依赖管理员手动切换。用户希望增加“自动上线”能力:后台定时检测节点状态,对启用该能力的节点自动同步前台展示状态,节点离线时自动隐藏,节点在线时自动显示。 + +### 目标 +- 在节点管理页新增“自动上线”开关。 +- 只有开启“自动上线”的节点由后台自动改写 `show`;未开启的节点继续保持手动显隐控制。 +- 后台定时根据现有 `available_status` / `is_online` 判定自动同步 `show`。 +- 节点编辑、新建、列表展示和 API 类型定义同步支持该字段。 + +### 约束条件 +```yaml +时间约束: 本轮完成实现并执行可用验证 +性能约束: 定时任务只处理开启 auto_online 的节点,避免全量无意义写库 +兼容性约束: 默认 auto_online=false,避免升级后自动覆盖现有节点显隐状态 +业务约束: 未开启自动上线的节点不得被后台任务改写 show +``` + +### 验收标准 +- [ ] `v2_server` 新增 `auto_online` 字段,默认关闭,并在 `Server` 模型中正确 cast。 +- [ ] 管理端 `save` / `update` / `batchUpdate` 接口能保存 `auto_online`。 +- [ ] 后台调度命令只同步 `auto_online=1` 的节点:在线或待同步时 `show=1`,离线时 `show=0`。 +- [ ] 节点管理表格和编辑弹窗展示/编辑“自动上线”开关,且不会破坏现有显隐开关、墙状态检测和筛选能力。 +- [ ] 前端类型检查与构建通过;后端相关文件通过 PHP 语法检查。 + +--- + +## 2. 方案 + +### 技术方案 +采用“独立字段 + 独立同步服务”方案: + +- 数据层新增 `v2_server.auto_online` 布尔字段,默认 `false`。 +- `Server` 模型增加 `auto_online` cast 和文档注释。 +- 后端新增 `ServerAutoOnlineService`,集中处理 `auto_online` 节点的显示状态同步,避免把业务逻辑散落在 Controller 或 Console Command 中。 +- 新增 Artisan 命令 `sync:server-auto-online`,由 `app/Console/Kernel.php` 每 5 分钟调度,并使用 `onOneServer()` / `withoutOverlapping()` 防止多实例重复写入。 +- 管理端 API 扩展 `save` / `update` / `batchUpdate` 参数,允许保存或批量切换 `auto_online`。 +- 管理端节点页在表格中展示自动托管状态,编辑弹窗基础信息区增加开关;批量修改弹窗增加可选批量设置。 + +### 影响范围 +```yaml +涉及模块: + - 数据库迁移: 增加 v2_server.auto_online 字段 + - 节点模型: 增加字段 cast 与文档注释 + - 节点管理 API: 保存、单点更新、批量更新支持 auto_online + - 后台调度: 新增自动同步服务、命令和定时任务 + - 管理端前端: 类型、API payload、节点页、编辑弹窗、批量修改弹窗 + - 知识库: 更新项目上下文和节点管理模块说明 +预计变更文件: 12-16 +``` + +### 风险评估 +| 风险 | 等级 | 应对 | +|------|------|------| +| 开启自动上线后后台任务覆盖管理员手动显隐 | 中 | 仅 `auto_online=1` 的节点自动同步;UI 文案明确“自动托管前台显示” | +| 在线状态依赖缓存,缓存过期导致节点被隐藏 | 中 | 使用现有 `available_status` 语义,保持与节点列表在线状态一致;不引入新的检测标准 | +| 批量更新误操作影响多个节点 | 中 | 批量修改弹窗保持“字段启用后才更新”的模式,默认不更新 `auto_online` | +| 已有墙状态检测改动被覆盖 | 低 | 增量修改当前脏文件,不移除 `gfw_check` 相关类型、接口和 UI | + +### 方案取舍 +```yaml +唯一方案理由: 独立字段语义清晰,定时同步服务可测试、可扩展,并且默认关闭能保护现有手动显隐行为。 +放弃的替代路径: + - 复用 check:server 命令: 会把离线通知和展示同步耦合,且 1800 秒通知阈值与 300 秒在线状态阈值不一致。 + - 写入 protocol_settings: 自动上线不是协议配置,会污染协议数据并增加查询和批量更新复杂度。 +回滚边界: 可撤销前端开关、接口参数、同步命令和迁移;已开启 auto_online 后由任务改写过的 show 需要管理员按业务需要手动恢复。 +``` + +--- + +## 3. 技术设计 + +### 架构设计 +```mermaid +flowchart TD + A[节点心跳/上报缓存] --> B[Server.available_status] + B --> C[ServerAutoOnlineService] + C --> D{auto_online=1} + D -->|在线/待同步| E[show=1] + D -->|离线| F[show=0] + G[节点管理页] --> H[save/update/batchUpdate] + H --> I[v2_server.auto_online] + I --> C +``` + +### API设计 +#### POST server/manage/save +- **请求**: 原节点保存 payload 增加 `auto_online?: boolean` +- **响应**: 沿用 `ApiResponse` + +#### POST server/manage/update +- **请求**: `{ id: number, show?: 0|1, enabled?: boolean, machine_id?: number|null, auto_online?: boolean }` +- **响应**: 沿用 `ApiResponse` + +#### POST server/manage/batchUpdate +- **请求**: 原批量更新 payload 增加 `auto_online?: boolean` +- **响应**: 沿用 `ApiResponse` + +### 数据模型 +| 字段 | 类型 | 说明 | +|------|------|------| +| `auto_online` | boolean | 是否允许后台根据节点在线状态自动同步 `show` | + +--- + +## 4. 核心场景 + +### 场景: 自动上线节点在线 +**模块**: 节点管理 +**条件**: 节点 `auto_online=1`,`available_status` 为在线或待同步 +**行为**: 调度命令执行自动同步 +**结果**: 节点 `show=1`,用户侧可见 + +### 场景: 自动上线节点离线 +**模块**: 节点管理 +**条件**: 节点 `auto_online=1`,`available_status` 为离线 +**行为**: 调度命令执行自动同步 +**结果**: 节点 `show=0`,用户侧隐藏 + +### 场景: 手动节点不受影响 +**模块**: 节点管理 +**条件**: 节点 `auto_online=0` +**行为**: 调度命令执行自动同步 +**结果**: 后台不改写该节点 `show` + +--- + +## 5. 技术决策 + +### admin-frontend-node-auto-online#D001: 自动上线使用独立字段与独立同步服务 +**日期**: 2026-04-27 +**状态**: 采纳 +**背景**: 需要让后台自动同步节点展示状态,同时保留未开启节点的手动显隐控制。 +**选项分析**: +| 选项 | 优点 | 缺点 | +|------|------|------| +| A: 独立字段 + 独立服务 | 语义清晰、可测试、默认关闭安全、后续可扩展 | 需要新增迁移和命令 | +| B: 复用 `check:server` | 改动较少 | 阈值语义不一致,离线通知和显示同步耦合 | +| C: 写入 `protocol_settings` | 避免迁移 | 污染协议配置,查询和批量更新复杂 | +**决策**: 选择方案 A +**理由**: 自动上线是节点运营策略,不是协议配置或离线通知;独立字段和服务能把边界表达清楚。 +**影响**: 数据库、节点模型、节点管理 API、调度命令和管理端节点 UI。 + +--- + +## 6. 验证策略 + +```yaml +verifyMode: review-first +reviewerFocus: + - app/Services/ServerAutoOnlineService.php 的 show 同步边界 + - app/Http/Controllers/V2/Admin/Server/ManageController.php 的 update/batchUpdate 参数兼容性 + - admin-frontend/src/views/nodes/* 的现有墙状态检测与批量选择逻辑不回退 +testerFocus: + - php -l 后端新增/修改 PHP 文件 + - npm run build 管理端类型检查与构建 + - 人工检查 auto_online 默认关闭,批量修改默认不覆盖 +uiValidation: required +riskBoundary: + - 不执行数据库迁移到真实环境 + - 不写生产数据 + - 不移除现有未提交的墙状态检测相关改动 +``` + +--- + +## 7. 成果设计 + +### 设计方向 +- **美学基调**: Apple 风格的运维控制台,黑白主节奏、低装饰、强信息密度;自动上线作为状态治理能力,不做营销化强调。 +- **记忆点**: 每行节点同时能看见“当前显示状态”和“是否自动托管”,管理员可以一眼区分手动控制与自动控制。 +- **参考**: `apple/DESIGN.md` 与现有 `NodesView.vue`。 + +### 视觉要素 +- **配色**: 延续黑色 hero、白色表格面板和 Apple Blue `#0071e3` 作为自动托管强调色;在线/离线仍使用现有绿色/红色状态点。 +- **字体**: 沿用项目现有系统字体栈,不引入新字体,保证管理端一致性。 +- **布局**: 表格显隐列附近增加自动上线开关或标识,编辑弹窗基础信息的“节点状态”区域加入第三张开关卡片。 +- **动效**: 复用 Element Plus Switch loading 与当前 `switch-shell` 动态反馈,避免新增噪音动效。 +- **氛围**: 克制透明浅底提示块,突出“自动托管”状态但不抢占节点名称和在线状态。 + +### 技术约束 +- **可访问性**: 开关必须有明确文本标签和辅助描述;不能只用颜色表达自动状态。 +- **响应式**: 表格保持现有横向滚动和断点行为;编辑弹窗开关卡片在窄屏下自然堆叠。 diff --git a/.helloagents/plan/202604272338_admin-frontend-node-auto-online/tasks.md b/.helloagents/plan/202604272338_admin-frontend-node-auto-online/tasks.md new file mode 100644 index 0000000..d11a245 --- /dev/null +++ b/.helloagents/plan/202604272338_admin-frontend-node-auto-online/tasks.md @@ -0,0 +1,97 @@ +# 任务清单: admin-frontend-node-auto-online + +```yaml +@feature: admin-frontend-node-auto-online +@created: 2026-04-27 +@status: in_progress +@mode: R2 +@workflow: INTERACTIVE +@complexity: complex +``` + +## LIVE_STATUS + +```json +{"status":"in_progress","completed":0,"failed":0,"pending":8,"total":8,"percent":0,"current":"方案包已创建,准备进入开发实施","updated_at":"2026-04-27 23:38:00"} +``` + +## 进度概览 + +| 完成 | 失败 | 跳过 | 总数 | +|------|------|------|------| +| 0 | 0 | 0 | 8 | + +--- + +## 任务列表 + +### 1. 后端数据与同步机制 + +- [ ] 1.1 新增 `database/migrations/*_add_auto_online_to_v2_server_table.php` + - 预期变更: 为 `v2_server` 增加 `auto_online` 布尔字段,默认 `false`,down 可回滚字段。 + - 完成标准: 迁移文件存在,字段名、默认值和回滚逻辑明确。 + - 验证方式: `php -l database/migrations/*_add_auto_online_to_v2_server_table.php` + - depends_on: [] + +- [ ] 1.2 修改 `app/Models/Server.php` + - 预期变更: 增加 `auto_online` 属性注释和 boolean cast,保持现有在线状态访问器与墙状态关系不变。 + - 完成标准: `Server` JSON/API 输出包含 `auto_online`,现有 `gfwChecks()` 不被移除。 + - 验证方式: `php -l app/Models/Server.php` + - depends_on: [1.1] + +- [ ] 1.3 新增 `app/Services/ServerAutoOnlineService.php` + - 预期变更: 封装自动上线同步逻辑,只处理 `auto_online=true` 的节点,在线/待同步写 `show=true`,离线写 `show=false`。 + - 完成标准: 服务返回同步统计,跳过未托管节点,不引入生产外部副作用。 + - 验证方式: `php -l app/Services/ServerAutoOnlineService.php` + - depends_on: [1.2] + +- [ ] 1.4 新增命令并接入调度 `app/Console/Commands/SyncServerAutoOnline.php`, `app/Console/Kernel.php` + - 预期变更: 新增 `sync:server-auto-online` 命令,每 5 分钟调度,使用 `onOneServer()` 与 `withoutOverlapping()`。 + - 完成标准: 命令可调用服务并输出统计,调度不影响现有任务。 + - 验证方式: `php -l app/Console/Commands/SyncServerAutoOnline.php`; `php -l app/Console/Kernel.php` + - depends_on: [1.3] + +### 2. 后端 API 契约 + +- [ ] 2.1 修改 `app/Http/Requests/Admin/ServerSave.php` 与 `app/Http/Controllers/V2/Admin/Server/ManageController.php` + - 预期变更: `save`、`update`、`batchUpdate` 支持 `auto_online`,批量更新保持字段显式传入才更新。 + - 完成标准: 手动显隐字段 `show` 和自动上线字段 `auto_online` 可独立保存。 + - 验证方式: `php -l app/Http/Requests/Admin/ServerSave.php`; `php -l app/Http/Controllers/V2/Admin/Server/ManageController.php` + - depends_on: [1.2] + +### 3. 管理端前端 + +- [ ] 3.1 修改 `admin-frontend/src/types/api.d.ts`, `admin-frontend/src/utils/nodeEditorOptions.ts`, `admin-frontend/src/utils/nodeEditorMapper.ts` + - 预期变更: 类型、表单模型、默认值、编辑回填和保存 payload 支持 `auto_online`。 + - 完成标准: 新建默认关闭,编辑能正确回填,保存能提交布尔值。 + - 验证方式: `npm run build` + - depends_on: [2.1] + +- [ ] 3.2 修改 `admin-frontend/src/views/nodes/NodeEditorDialog.vue`, `NodeBatchEditDialog.vue`, `NodesView.vue`, `admin-frontend/src/utils/nodes.ts` + - 预期变更: 编辑弹窗和批量修改弹窗增加自动上线开关;节点表格展示自动托管状态;现有墙状态检测 UI 保持可用。 + - 完成标准: 管理员可单节点和批量设置 `auto_online`;未开启批量字段时不覆盖。 + - 验证方式: `npm run build` + - depends_on: [3.1] + +### 4. 知识库与验收 + +- [ ] 4.1 更新 `.helloagents/context.md`, `.helloagents/modules/*`, `.helloagents/CHANGELOG.md` + - 预期变更: 同步记录节点自动上线能力、后端命令和管理端节点页行为。 + - 完成标准: 知识库反映代码事实,CHANGELOG 包含方案链接和决策 ID。 + - 验证方式: 人工检查文档条目与本次改动一致。 + - depends_on: [1.4, 2.1, 3.2] + +--- + +## 执行日志 + +| 时间 | 任务 | 状态 | 备注 | +|------|------|------|------| +| 2026-04-27 23:38 | DESIGN | in_progress | 用户选择方案 A,方案包创建并进入开发实施 | + +--- + +## 执行备注 + +- 当前工作树已有节点墙状态检测相关未提交改动,本任务必须保留并兼容这些改动。 +- 按上级工具约束,本轮不调度子代理,复杂任务由主代理直接实施并在验收中说明。 diff --git a/admin-frontend/src/api/admin.ts b/admin-frontend/src/api/admin.ts index 77a9dab..078e0cf 100644 --- a/admin-frontend/src/api/admin.ts +++ b/admin-frontend/src/api/admin.ts @@ -25,6 +25,7 @@ import type { AdminNoticeSavePayload, AdminNodeItem, AdminNodeBatchUpdatePayload, + AdminNodeGfwCheckResult, AdminNodeSavePayload, AdminNodeRouteItem, AdminNodeRouteSavePayload, @@ -518,6 +519,10 @@ export function batchDeleteNodes(ids: number[]): Promise> { return unwrapPost('/server/manage/batchDelete', { ids }) } +export function checkNodeGfw(ids: number[]): Promise> { + return unwrapPost('/server/manage/checkGfw', { ids }) +} + export function saveNode(payload: AdminNodeSavePayload): Promise> { return unwrapPost('/server/manage/save', payload as unknown as Record) } diff --git a/admin-frontend/src/types/api.d.ts b/admin-frontend/src/types/api.d.ts index 2d9f05d..8b387ba 100644 --- a/admin-frontend/src/types/api.d.ts +++ b/admin-frontend/src/types/api.d.ts @@ -896,6 +896,44 @@ export interface AdminNodeItem { metrics?: AdminNodeMetrics | null groups?: AdminServerGroupItem[] parent?: AdminNodeParentRef | null + gfw_check?: AdminNodeGfwCheck | null +} + +export type AdminNodeGfwStatus = + | 'unchecked' + | 'pending' + | 'checking' + | 'normal' + | 'blocked' + | 'partial' + | 'failed' + | 'skipped' + +export interface AdminNodeGfwCheck { + id?: number + status: AdminNodeGfwStatus | string + inherited?: boolean + source_node_id?: number | null + summary?: Record | null + operator_summary?: Record | null + error_message?: string | null + checked_at?: number | null + updated_at?: number | null +} + +export interface AdminNodeGfwCheckResult { + started: Array<{ + id: number + check_id: number + status: AdminNodeGfwStatus | string + }> + skipped: Array<{ + id: number + status: AdminNodeGfwStatus | string + reason?: string + source_node_id?: number + }> + total: number } export interface AdminNodeUpdatePayload { diff --git a/admin-frontend/src/utils/nodes.ts b/admin-frontend/src/utils/nodes.ts index 0ec6929..4a3c6b9 100644 --- a/admin-frontend/src/utils/nodes.ts +++ b/admin-frontend/src/utils/nodes.ts @@ -1,6 +1,7 @@ import type { AdminNodeItem } from '@/types/api' export type NodeRelationFilter = 'all' | 'parent' | 'child' +export type NodeGfwFilter = 'all' | 'normal' | 'blocked' | 'partial' | 'failed' | 'unchecked' | 'checking' | 'inherited' export type NodeStatusFilter = 'all' | 'online' | 'offline' export interface NodeStatusMeta { @@ -11,6 +12,14 @@ export interface NodeStatusMeta { type NodeStatusClass = NodeStatusMeta['dotClass'] +export interface NodeGfwMeta { + label: string + searchText: string + tagType: 'success' | 'warning' | 'danger' | 'info' | 'primary' + tone: 'normal' | 'blocked' | 'partial' | 'failed' | 'unchecked' | 'checking' + inherited: boolean +} + const NODE_TYPE_LABELS: Record = { shadowsocks: 'Shadowsocks', trojan: 'Trojan', @@ -66,6 +75,83 @@ export function getNodeStatusMeta(node: AdminNodeItem): NodeStatusMeta { } } +export function getNodeGfwMeta(node: AdminNodeItem): NodeGfwMeta { + const status = normalizeText(node.gfw_check?.status || 'unchecked') + const inherited = Boolean(node.gfw_check?.inherited) + const inheritedPrefix = inherited ? '随父节点 · ' : '' + + if (status === 'normal') { + return { + label: `${inheritedPrefix}正常`, + searchText: `${inherited ? '随父节点 继承 ' : ''}正常 未被墙 墙正常 gfw normal`, + tagType: 'success', + tone: 'normal', + inherited, + } + } + + if (status === 'blocked') { + return { + label: `${inheritedPrefix}疑似被墙`, + searchText: `${inherited ? '随父节点 继承 ' : ''}被墙 疑似被墙 gfw blocked`, + tagType: 'danger', + tone: 'blocked', + inherited, + } + } + + if (status === 'partial') { + return { + label: `${inheritedPrefix}部分异常`, + searchText: `${inherited ? '随父节点 继承 ' : ''}异常 部分异常 gfw partial`, + tagType: 'warning', + tone: 'partial', + inherited, + } + } + + if (status === 'failed') { + return { + label: `${inheritedPrefix}检测失败`, + searchText: `${inherited ? '随父节点 继承 ' : ''}失败 检测失败 异常 gfw failed`, + tagType: 'danger', + tone: 'failed', + inherited, + } + } + + if (status === 'pending' || status === 'checking') { + return { + label: `${inheritedPrefix}检测中`, + searchText: `${inherited ? '随父节点 继承 ' : ''}检测中 等待检测 gfw checking pending`, + tagType: 'primary', + tone: 'checking', + inherited, + } + } + + return { + label: inherited ? '随父节点 · 未检测' : '未检测', + searchText: `${inherited ? '随父节点 继承 ' : ''}未检测 unchecked`, + tagType: 'info', + tone: 'unchecked', + inherited, + } +} + +export function getNodeGfwTooltip(node: AdminNodeItem): string { + const meta = getNodeGfwMeta(node) + const source = node.gfw_check?.source_node_id + const checkedAt = node.gfw_check?.checked_at + ? new Date(Number(node.gfw_check.checked_at) * 1000).toLocaleString() + : '' + const sourceText = meta.inherited && source ? `,来源父节点 #${source}` : '' + const timeText = checkedAt ? `,检测时间 ${checkedAt}` : '' + const errorText = node.gfw_check?.error_message ? `,错误:${node.gfw_check.error_message}` : '' + + return `${meta.label}${sourceText}${timeText}${errorText}` +} + function isNodeOnlineStatus(status: NodeStatusClass): boolean { return status === 'online' || status === 'pending' } @@ -113,6 +199,7 @@ function buildNodeSearchText(node: AdminNodeItem): string { node.port, node.server_port, getNodeTypeLabel(node.type), + getNodeGfwMeta(node).searchText, ...getNodeGroupNames(node), ] .map((item) => String(item ?? '').trim()) @@ -128,12 +215,14 @@ export function filterNodes( groupFilter: string, statusFilter: NodeStatusFilter = 'all', relationFilter: NodeRelationFilter = 'all', + gfwFilter: NodeGfwFilter = 'all', ): AdminNodeItem[] { const normalizedKeyword = normalizeText(keyword) const normalizedType = normalizeText(typeFilter) const normalizedGroup = normalizeText(groupFilter) const normalizedStatus = normalizeText(statusFilter) const normalizedRelation = normalizeText(relationFilter) + const normalizedGfw = normalizeText(gfwFilter) return nodes.filter((node) => { if (normalizedKeyword && !buildNodeSearchText(node).includes(normalizedKeyword)) { @@ -168,6 +257,15 @@ export function filterNodes( return false } + const gfwMeta = getNodeGfwMeta(node) + if (normalizedGfw === 'inherited' && !gfwMeta.inherited) { + return false + } + + if (normalizedGfw !== '' && normalizedGfw !== 'all' && normalizedGfw !== 'inherited' && gfwMeta.tone !== normalizedGfw) { + return false + } + return true }) } diff --git a/admin-frontend/src/views/nodes/NodesView.vue b/admin-frontend/src/views/nodes/NodesView.vue index a90b5d2..4883dec 100644 --- a/admin-frontend/src/views/nodes/NodesView.vue +++ b/admin-frontend/src/views/nodes/NodesView.vue @@ -15,6 +15,7 @@ import { import { batchDeleteNodes, batchUpdateNodes, + checkNodeGfw, copyNode, deleteNode, fetchNodes, @@ -38,17 +39,20 @@ import { countVisibleNodes, filterNodes, formatNodeRate, + getNodeGfwMeta, + getNodeGfwTooltip, getNodeAddress, getNodeGroupNames, getNodeIdLabel, getNodeStatusMeta, getNodeTypeLabel, type NodeRelationFilter, + type NodeGfwFilter, type NodeStatusFilter, } from '@/utils/nodes' import { sortNodesByOrder } from '@/utils/nodeEditor' -type NodeAction = 'edit' | 'copy' | 'pin-top' | 'delete' +type NodeAction = 'edit' | 'copy' | 'pin-top' | 'delete' | 'check-gfw' type NodeDialogMode = 'create' | 'edit' type NodeBatchEditPayload = Omit @@ -65,6 +69,7 @@ const typeFilter = ref('all') const groupFilter = ref('all') const statusFilter = ref('all') const relationFilter = ref('all') +const gfwFilter = ref('all') const currentPage = ref(1) const pageSize = ref(20) const selectedNodeIds = ref([]) @@ -78,6 +83,7 @@ const sortDialogVisible = ref(false) const batchEditVisible = ref(false) const batchSubmitting = ref(false) const batchDeleting = ref(false) +const batchGfwChecking = ref(false) const filteredNodes = computed(() => sortNodesByOrder(filterNodes( nodes.value, @@ -86,6 +92,7 @@ const filteredNodes = computed(() => sortNodesByOrder(filterNodes( groupFilter.value, statusFilter.value, relationFilter.value, + gfwFilter.value, ))) const paginatedNodes = computed(() => { @@ -102,6 +109,7 @@ const hasActiveFilters = computed(() => ( || groupFilter.value !== 'all' || statusFilter.value !== 'all' || relationFilter.value !== 'all' + || gfwFilter.value !== 'all' )) const summaryCards = computed(() => [ @@ -238,6 +246,7 @@ function handleReset() { groupFilter.value = 'all' statusFilter.value = 'all' relationFilter.value = 'all' + gfwFilter.value = 'all' currentPage.value = 1 } @@ -337,6 +346,59 @@ async function handleBatchDelete() { } } +async function handleCheckGfw(ids: number[], label: string) { + if (ids.length === 0) { + ElMessage.warning('请先选择需要检测的节点') + return + } + + try { + const response = await checkNodeGfw(ids) + const started = response.data?.started?.length ?? 0 + const skipped = response.data?.skipped?.length ?? 0 + + if (started > 0) { + ElMessage.success(`${label}已发起墙状态检测,${started} 个父节点等待上报`) + } else if (skipped > 0) { + ElMessage.info('所选节点均为子节点,墙状态随父节点显示') + } else { + ElMessage.info('没有可检测的节点') + } + + await loadNodeBoard() + } catch (error) { + ElMessage.error(error instanceof Error ? error.message : '墙状态检测发起失败') + } +} + +async function handleBatchCheckGfw() { + if (!hasSelectedNodes.value) { + ElMessage.warning('请先勾选需要检测的节点') + return + } + + batchGfwChecking.value = true + try { + await handleCheckGfw([...selectedNodeIds.value], '批量') + } finally { + batchGfwChecking.value = false + } +} + +async function handleNodeCheckGfw(node: AdminNodeItem) { + if (node.parent_id) { + ElMessage.info('子节点不单独检测,墙状态随父节点显示') + return + } + + markPending(workingIds, node.id, true) + try { + await handleCheckGfw([node.id], '') + } finally { + markPending(workingIds, node.id, false) + } +} + async function handleToggleShow(node: AdminNodeItem, nextValue: boolean) { const previous = Boolean(node.show) if (previous === nextValue) { @@ -396,6 +458,11 @@ async function handleAction(action: NodeAction, node: AdminNodeItem) { return } + if (action === 'check-gfw') { + await handleNodeCheckGfw(node) + return + } + markPending(workingIds, node.id, true) try { @@ -437,7 +504,7 @@ watch( }, ) -watch([keyword, typeFilter, groupFilter, statusFilter, relationFilter], () => { +watch([keyword, typeFilter, groupFilter, statusFilter, relationFilter, gfwFilter], () => { currentPage.value = 1 }) @@ -530,11 +597,30 @@ watch( + + + + + + + + + + +
{{ batchTargetLabel }} 批量修改 + + + 检测墙状态 + {{ getNodeStatusMeta(row).label }} + + + {{ getNodeGfwMeta(row).label }} + + {{ getNodeTypeLabel(row.type) }}
@@ -687,6 +784,9 @@ watch(