@clawhub-liuwei120-4b0a4523f3
5GC Web仪表自动化技能,支持AMF/UDM/AUSF/SMF/PGW-C/UPF/PGW-U/GNB/UE/PCF/NRF/QoS/TC/PCC/smpolicy的批量添加与编辑及PCF默认规则一键配置
---
name: 5gc-web-dotouch
version: 1.0.0
description: 5GC Web仪表自动化技能,支持AMF/UDM/AUSF/SMF/PGW-C/UPF/PGW-U/GNB/UE/PCF/NRF/QoS/TC/PCC/smpolicy的批量添加与编辑及PCF默认规则一键配置
author: liuwei120
tags: [5gc, automation, playwright, network]
---
# 5GC Web 仪表自动化技能
> 统一管理 AMF、UDM/AUSF、SMF/PGW-C、UPF/PGW-U、GNB、UE、PCF、NRF 八类网元的添加与编辑操作,以及 PCC 规则、QoS 模板、Traffic Control、SMPolicy 和 PCF 默认规则一键配置。
---
## 目录
- [快速开始](#快速开始)
- [统一 CLI 入口](#统一-cli-入口)
- [技能详细文档](#技能详细文档)
- [AMF](#amf)
- [UDM/AUSF](#udmausf)
- [SMF/PGW-C](#smfpgw-c)
- [UPF/PGW-U](#upfpgw-u)
- [GNB](#gnb)
- [UE](#ue)
- [PCF/PCRF](#pcfpcrf)
- [PCC 规则](#pcc-规则)
- [QoS 模板](#qos-模板)
- [Traffic Control](#traffic-control)
- [SMPolicy](#smpolicy)
- [UE Smpolicy](#smpolicy-ue-add-skilljs)
- [DNN Smpolicy](#smpolicy-dnn)
- [DNN Smpolicy](#smpolicy-dnn)
- [TAC Smpolicy](#smpolicy-tac)
- [Cell Smpolicy](#smpolicy-cell)
- [Cell Forbidden Smpolicy](#smpolicy-cell-forbidden)
- [NRF](#nrf)
- [全局参数参考](#全局参数参考)
- [字段参考](#字段参考)
---
## 快速开始
### 安装方法
技能目录位于 `skills/5gc/`,由统一入口 `5gc.js` 统一调度,无需额外安装:
```bash
# 克隆或复制到本机
git clone <repo> ~/.openclaw/workspace/skills/5gc
# 直接使用统一入口(推荐)
node skills/5gc/scripts/5gc.js <entity> <action> [options]
# 或直接调用各脚本
node skills/5gc/scripts/amf-add-skill.js <参数>
```
### 前置要求
- Node.js ≥ 14
- Playwright(`npm i playwright && npx playwright install chromium`)
- 5GC 仪表地址:`https://192.168.3.89`(默认)
- 登录凭证:`[email protected]` / `dotouch`
- 仪表上已创建对应工程(如 `XW_S5GC_1`)
### 会话缓存
所有脚本自动复用 Playwright 会话缓存(`.sessions/` 目录),首次登录后再次运行无需重复登录。
---
## 统一 CLI 入口
### 路径
```
node skills/5gc/scripts/5gc.js <entity> <action> [options]
```
### 支持的网元与操作
| entity | add | edit | 特殊操作 |
|--------|-----|------|---------|
| `amf` | ✅ | ✅ | |
| `udm` | ✅ | ✅ | |
| `smf` | ✅ | ✅ | |
| `upf` | ✅ | ✅ | |
| `gnb` | ✅ | ✅ | |
| `ue` | ✅ | ✅ | |
| `pcf` | ✅ | ✅ | `default-rule-add` |
| `pcc` | ✅ | ✅ | |
| `qos` | ✅ | | |
| `tc` | ✅ | | |
| `smpolicy` | | | `add-pcc`, `ue-add`, `ue-edit`, `dnn-add`, `dnn-edit` |
| `nrf` | ✅ | ✅ | |
### 全局选项
| 选项 | 说明 |
|------|------|
| `--url <地址>` | 5GC 仪表地址,默认 `https://192.168.3.89` |
| `--headed` | 打开可见浏览器窗口(调试用) |
### 三种使用模式
```bash
# 1. 添加网元
node 5gc.js amf add <名称> [参数...]
# 2. 批量编辑(当前工程下所有该类网元)
node 5gc.js amf edit --project <工程> --set-<字段> <值>
# 3. 单个编辑(按名称精确匹配)
node 5gc.js amf edit --name <名称> --project <工程> --set-<字段> <值>
```
---
## 技能详细文档
---
### AMF
#### amf-add-skill.js
**功能**:在指定工程下添加一个 AMF 实例。
**使用方式**:
```bash
node 5gc.js amf add <名称> [选项...]
# 或直接调用
node skills/5gc/scripts/amf-add-skill.js <名称> [选项...]
```
**参数**:
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `<名称>` | AMF 实例名称(位置参数) | **必填** |
| `--project <工程>` / `-p <工程>` | 目标工程名称 | `5G_basic_process` |
| `--url <地址>` | 5GC 仪表地址 | `https://192.168.3.89` |
| `--mcc <值>` | MCC(移动国家码) | `460` |
| `--mnc <值>` | MNC(移动网络码) | `01` |
| `--ngap_sip <IP>` | NGAP 信令面 IP | `200.20.20.1` |
| `--ngap_port <端口>` | NGAP 端口 | `38412` |
| `--http2_sip <IP>` | HTTP2 服务 IP | `200.20.20.5` |
| `--http2_port <端口>` | HTTP2 端口 | `8080` |
| `--stac <值>` | 起始 TAC | `101` |
| `--etac <值>` | 结束 TAC | `102` |
| `--region_id <值>` | 区域 ID | `1` |
| `--set_id <值>` | Set ID | `1` |
| `--pointer <值>` | 指针 | `1` |
| `--headed` | 打开可见浏览器 | false |
**示例**:
```bash
# 基本添加
node 5gc.js amf add AMF_TEST --project XW_S5GC_1
# 指定 NGAP IP 和端口
node 5gc.js amf add AMF_PROD --project XW_S5GC_1 --ngap_sip 10.200.1.50 --ngap_port 38412
# 使用不同 MCC/MNC
node 5gc.js amf add AMF_CMCC --project XW_S5GC_1 --mcc 460 --mnc 00
```
---
#### amf-edit-skill.js
**功能**:修改 AMF 配置参数。支持单个修改或批量修改工程下所有 AMF。
**使用方式**:
```bash
node 5gc.js amf edit [选项...]
```
**参数**:
| 参数 | 说明 |
|------|------|
| `--project <工程>` / `-p <工程>` | 目标工程,不带 `--name` 时批量修改该工程下所有 AMF |
| `--name <名称>` | 精确匹配要修改的 AMF 名称 |
| `--id <ID>` | 按 AMF ID 修改 |
| `--set-<字段> <值>` | 修改指定字段的值,支持多组 |
| `--url <地址>` | 5GC 仪表地址 |
| `--headed` | 打开可见浏览器 |
**可编辑字段**:`name`, `mcc`, `mnc`, `ngap_sip`, `ngap_port`, `http2_sip`, `http2_port`, `stac`, `etac`, `region_id`, `set_id`, `pointer`, `ea[NEA0]`, `ea[128-NEA1]`, `ea[128-NEA2]`, `ea[128-NEA3]`, `ia[NIA0]`, `ia[128-NIA1]`, `ia[128-NIA2]`, `ia[128-NIA3]`
> ⚠️ `ea[NEA0]` 等算法字段:实际向表单填入字段名 `ea[NEA0]`(input[name="ea[NEA0]"]),layui checkbox 点击基于索引而非字段名,详情见 SKILL.md 算法配置章节。
**示例**:
```bash
# 批量修改工程下所有 AMF 的 NGAP IP
node 5gc.js amf edit --project XW_S5GC_1 --set-ngap_sip 10.200.1.99
# 修改指定 AMF
node 5gc.js amf edit --name AMF_TEST --project XW_S5GC_1 --set-ngap_sip 10.200.1.50 --set-http2_sip 10.200.1.51
# 按 ID 修改
node 5gc.js amf edit --id 6633 --set-ngap_port 38413
```
---
### UDM/AUSF
#### ausf-udm-add-skill.js
**功能**:在指定工程下添加一个 UDM/AUSF 实例。
**使用方式**:
```bash
node 5gc.js udm add <名称> [选项...]
```
**参数**:
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `<名称>` | UDM 实例名称(位置参数) | **必填** |
| `--project <工程>` / `-p <工程>` | 目标工程名称 | `5G_basic_process` |
| `--url <地址>` | 5GC 仪表地址 | `https://192.168.3.89` |
| `--count <数量>` | 实例数量 | `1` |
| `--sip <IP>` | SIP 服务 IP | `192.168.20.30` |
| `--port <端口>` | SIP 端口 | `80` |
| `--auth_method <方法>` | 认证方法 | `5G_AKA` |
| `--scheme <协议>` | 协议类型 | `HTTP` |
| `--priority <优先级>` | 优先级 | `8` |
| `--headed` | 打开可见浏览器 | false |
**示例**:
```bash
# 基本添加
node 5gc.js udm add UDM_TEST --project XW_S5GC_1
# 指定 SIP IP 和端口
node 5gc.js udm add UDM_PROD --project XW_S5GC_1 --sip 10.0.0.100 --port 8080
# 批量添加 3 个实例
node 5gc.js udm add UDM_CLUSTER --project XW_S5GC_1 --count 3 --sip 10.0.0.50
```
---
#### ausf-udm-edit-skill.js
**功能**:修改 UDM/AUSF 配置参数。支持批量和单个修改。
**使用方式**:
```bash
node 5gc.js udm edit [选项...]
```
**参数**:
| 参数 | 说明 |
|------|------|
| `--project <工程>` | 目标工程,不带 `--name` 时批量修改 |
| `--name <名称>` | 精确匹配要修改的 UDM 名称 |
| `--set-sip <IP>` | 修改 SIP IP |
| `--set-port <端口>` | 修改端口 |
| `--set-auth_method <方法>` | 修改认证方法 |
| `--set-scheme <协议>` | 修改协议 |
| `--set-count <数量>` | 修改实例数量 |
| `--url <地址>` | 5GC 仪表地址 |
| `--headed` | 打开可见浏览器 |
**示例**:
```bash
# 批量修改工程下所有 UDM 的 SIP IP
node 5gc.js udm edit --project XW_S5GC_1 --set-sip 10.0.0.99
# 修改指定 UDM
node 5gc.js udm edit --name UDM_TEST --project XW_S5GC_1 --set-sip 10.0.0.88 --set-port 8080
```
---
### SMF/PGW-C
#### smf-pgwc-add-skill.js
**功能**:在指定工程下添加一个 SMF/PGW-C 实例。
**使用方式**:
```bash
node 5gc.js smf add --name <名称> [选项...]
```
> ⚠️ 通过 5gc.js 统一调度时必须使用 `--name <名称>` 形式(不是位置参数)。
**参数**:
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `--name <名称>` | SMF 实例名称 | **必填** |
| `--project <工程>` / `-p <工程>` | 目标工程名称 | `XW_S5GC_1` |
| `--url <地址>` | 5GC 仪表地址 | `https://192.168.3.89` |
| `--pfcp_sip <IP>` | PFCP 信令面 IP | `200.20.20.25` |
| `--http2_sip <IP>` | HTTP2 服务 IP | `200.20.20.25` |
| `--mcc <值>` | MCC | `460` |
| `--mnc <值>` | MNC | `01` |
| `--pdu_capacity <数量>` | PDU 会话容量 | `200000` |
| `--ue_min <IP>` | UE IP 池起始 | `30.30.30.20` |
| `--ue_max <IP>` | UE IP 池结束 | `30.31.30.20` |
| `--interest_tac <TAC列表>` | 关注 TAC 列表(逗号分隔) | `101,102` |
| `--headed` | 打开可见浏览器 | false |
> ✅ **NSSAI 自动配置**:脚本在 SMF 创建后会自动打开 NSSAI 配置弹窗,添加一条默认 NSSAI(SST=1, SD=000001, DNN Group=cscn2net)。如需自定义 NSSAI 参数,请直接修改脚本中的硬编码值。
>
> ⚠️ ue_sip6 / ue_eip6 为硬编码值,不支持 CLI 参数覆盖。
**示例**:
```bash
# 基本添加
node 5gc.js smf add --name SMF_TEST --project XW_S5GC_1
# 指定工程和 IP/MCC
node 5gc.js smf add --name SMF_PROD --project XW_S5GC_1 --pfcp_sip 10.10.10.50 --http2_sip 10.10.10.51 --mcc 460 --mnc 01
```
---
#### smf-pgwc-edit-skill.js
**功能**:修改 SMF/PGW-C 配置参数。支持批量和单个修改。
**使用方式**:
```bash
node 5gc.js smf edit [选项...]
```
**参数**:
| 参数 | 说明 |
|------|------|
| `--project <工程>` | 目标工程,不带 `--name` 时批量修改 |
| `--name <名称>` | 精确匹配要修改的 SMF 名称 |
| `--set-pfcp_sip <IP>` | 修改 PFCP 信令面 IP |
| `--set-http2_sip <IP>` | 修改 HTTP2 服务 IP |
| `--set-mcc <值>` | 修改 MCC |
| `--set-mnc <值>` | 修改 MNC |
| `--set-pdu_capacity <数量>` | 修改 PDU 会话容量 |
| `--set-ue_min <IP>` | 修改 UE IP 池起始 |
| `--set-ue_max <IP>` | 修改 UE IP 池结束 |
| `--set-interest_tac <TAC列表>` | 修改关注 TAC 列表(逗号分隔) |
> ⚠️ 以下字段不支持 `--set-` 修改:dnn、n3_ip、n6_ip、snssai_sst、snssai_sd。如需修改,请通过仪表 UI 手动完成。NSSAI 配置请在添加时自动完成(见上文)。
**示例**:
```bash
# 批量修改工程下所有 SMF 的 HTTP2 IP
node 5gc.js smf edit --project XW_S5GC_1 --set-http2_sip 10.10.10.99
# 修改指定 SMF 的 pfcp_sip 和 MCC/MNC
node 5gc.js smf edit --name SMF_TEST --project XW_S5GC_1 --set-pfcp_sip 10.10.10.88 --set-mcc 460 --set-mnc 01
```
---
### UPF/PGW-U
#### upf-add-skill.js
**功能**:在指定工程下添加一个 UPF/PGW-U 实例。
**使用方式**:
```bash
node 5gc.js upf add <名称> [选项...]
```
**参数**:
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `<名称>` | UPF 实例名称(位置参数) | **必填** |
| `--project <工程>` / `-p <工程>` | 目标工程名称 | `XW_S5GC_1` |
| `--url <地址>` | 5GC 仪表地址 | `https://192.168.3.89` |
| `--n4_ip <IP>` | N4 接口 IP | `192.168.20.30` |
| `--n3_ip <IP>` | N3 接口 IP | `192.168.20.30` |
| `--n6_ip <IP>` | N6 接口 IP | `192.168.20.31` |
| `--n4_port <端口>` | N4 端口 | `8805` |
| `--MCC <值>` | MCC(注意大写) | `460` |
| `--MNC <值>` | MNC(注意大写) | `01` |
| `--pdu_capacity <数量>` | PDU 会话容量 | `20000` |
| `--ue_min <IP>` | UE IP 池起始 | `20.20.20.20` |
| `--ue_max <IP>` | UE IP 池结束 | `20.20.60.20` |
| `--headed` | 打开可见浏览器 | false |
> ⚠️ DNN、TAC、NSSAI 在添加脚本中为硬编码默认值,不支持命令行覆盖。如需修改,请使用 `upf edit` 脚本。
**示例**:
```bash
# 基本添加
node 5gc.js upf add UPF_TEST --project XW_S5GC_1
# 指定 N4/N3/N6 IP 和 MCC/MNC
node 5gc.js upf add UPF_PROD --project XW_S5GC_1 --n4_ip 10.0.0.50 --n6_ip 10.0.0.51 --MCC 460 --MNC 01
```
---
#### upf-edit-skill.js
**功能**:修改 UPF/PGW-U 配置参数。支持批量和单个修改。
**使用方式**:
```bash
node 5gc.js upf edit [选项...]
```
**参数**:
| 参数 | 说明 |
|------|------|
| `--project <工程>` | 目标工程,不带 `--name` 时批量修改 |
| `--name <名称>` | 精确匹配要修改的 UPF 名称 |
| `--set-n3_ip <IP>` | 修改 N3 接口 IP |
| `--set-n4_ip <IP>` | 修改 N4 接口 IP |
| `--set-n4_port <端口>` | 修改 N4 端口 |
| `--set-n6_ip <IP>` | 修改 N6 接口 IP |
| `--set-MCC <值>` | 修改 MCC(注意大写) |
| `--set-MNC <值>` | 修改 MNC(注意大写) |
| `--set-pdu_capacity <数量>` | 修改 PDU 会话容量 |
| `--set-ue_min <IP>` | 修改 UE IP 池起始 |
| `--set-ue_max <IP>` | 修改 UE IP 池结束 |
| `--url <地址>` | 5GC 仪表地址 |
| `--headed` | 打开可见浏览器 |
> ⚠️ `dnn`(DNN)和 TAC/NSSAI 在 UPF 表单中存储在 jsgrid 配置行内,不支持简单的 `--set-` 修改。
**示例**:
```bash
# 批量修改工程下所有 UPF 的 N4 IP
node 5gc.js upf edit --project XW_S5GC_1 --set-n4_ip 99.99.99.99
# 修改指定 UPF 的 N4/N6 IP 和 MCC/MNC
node 5gc.js upf edit --name UPF_TEST --project XW_S5GC_1 --set-n4_ip 88.88.88.88 --set-n6_ip 88.88.88.89 --set-MCC 460 --set-MNC 01
```
---
### GNB
#### gnb-add-skill.js
**功能**:在指定工程下添加一个 GNB 实例。
**使用方式**:
```bash
node 5gc.js gnb add <名称> [选项...]
```
**参数**:
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `<名称>` | GNB 实例名称(位置参数) | **必填** |
| `--project <工程>` / `-p <工程>` | 目标工程名称 | `XW_S5GC_1` |
| `--url <地址>` | 5GC 仪表地址 | `https://192.168.3.89` |
| `--ngap_sip <IP>` | NGAP 信令面 IP | `200.20.20.50` |
| `--user_sip_ip_v4 <IP>` | 用户面 IPv4 | `2.2.2.2` |
| `--mcc <值>` | MCC | `460` |
| `--mnc <值>` | MNC | `60` |
| `--stac <值>` | 起始 TAC | `0` |
| `--etac <值>` | 结束 TAC | `0` |
| `--node_id <ID>` | 节点 ID | `70` |
| `--cell_count <数量>` | 小区数量 | `1` |
| `--headed` | 打开可见浏览器 | false |
> ⚠️ `stac`/`etac`/`node_id` 非默认值时可能触发表单验证失败,建议先使用默认值添加后再用 `gnb edit` 修改。
**示例**:
```bash
# 基本添加
node 5gc.js gnb add GNB_TEST --project XW_S5GC_1
# 指定 NGAP IP、用户面 IP 和 TAC
node 5gc.js gnb add GNB_PROD --project XW_S5GC_1 --ngap_sip 200.20.20.100 --user_sip_ip_v4 3.3.3.3 --mcc 460 --mnc 60 --stac 1 --etac 10
```
---
#### gnb-edit-skill.js
**功能**:修改 GNB 配置参数。支持批量和单个修改。
**使用方式**:
```bash
node 5gc.js gnb edit [选项...]
```
**参数**:
| 参数 | 说明 |
|------|------|
| `--project <工程>` | 目标工程,不带 `--name` 时批量修改 |
| `--name <名称>` | 精确匹配要修改的 GNB 名称 |
| `--set-ngap_sip <IP>` | 修改 NGAP 信令面 IP |
| `--set-user_sip_ip_v4 <IP>` | 修改用户面 IPv4 |
| `--set-user_sip_ip_v6 <IP>` | 修改用户面 IPv6 |
| `--set-mcc <值>` | 修改 MCC |
| `--set-mnc <值>` | 修改 MNC |
| `--set-stac <值>` | 修改起始 TAC |
| `--set-etac <值>` | 修改结束 TAC |
| `--set-node_id <ID>` | 修改节点 ID |
| `--set-cell_count <数量>` | 修改小区数量 |
| `--set-replay_ip <IP>` | 修改回放 IP |
| `--set-replay_port <端口>` | 修改回放端口 |
| `--url <地址>` | 5GC 仪表地址 |
| `--headed` | 打开可见浏览器 |
**示例**:
```bash
# 批量修改工程下所有 GNB 的用户面 IP
node 5gc.js gnb edit --project XW_S5GC_1 --set-user_sip_ip_v4 99.99.99.99
# 修改指定 GNB 的 NGAP IP 和 MCC/MNC
node 5gc.js gnb edit --name GNB_TEST --project XW_S5GC_1 --set-ngap_sip 200.20.20.88 --set-mcc 461 --set-mnc 22
```
---
### UE
#### ue-add-skill.js
**功能**:在指定工程下添加一个或多个 UE 实例。
**使用方式**:
```bash
node 5gc.js ue add --name <名称> [选项...]
```
**参数**:
| 参数 | 短名 | 说明 | 默认值 |
|------|------|------|--------|
| `--name <名称>` | `-n <名称>` | UE 名称(只支持字母/数字/下划线) | **必填** |
| `--project <工程>` | `-p <工程>` | 目标工程名称 | `XW_S5GC_1` |
| `--url <地址>` | `-u <地址>` | 5GC 仪表地址 | `https://192.168.3.89` |
| `--imsi <值>` | | 起始 IMSI(15位) | `460001234567890` |
| `--msisdn <值>` | | MSISDN(13-15位,以 86 开头) | `8611111111111` |
| `--mcc <值>` | | MCC | `460` |
| `--mnc <值>` | | MNC | `01` |
| `--key <值>` | | KI 密钥(32位 hex) | `1111...`(32个1) |
| `--opc <值>` | | OPc 密钥(32位 hex) | `1111...`(32个1) |
| `--imeisv <值>` | | IMEISV(偶数位) | `8611111111111111` |
| `--sst <值>` | | NSSAI SST | `1` |
| `--sd <值>` | | NSSAI SD | `111111` |
| `--count <数量>` | `-c <数量>` | 连续添加数量 | `1` |
| `--headed` | | 打开可见浏览器 | false |
> **命名约束**:UE 名称只能包含字母、数字、下划线(`_`),不能使用连字符(`-`)或其他特殊字符。
**示例**:
```bash
# 基本添加
node 5gc.js ue add --name UE_001 --project XW_S5GC_1
# 指定 IMSI 和 MSISDN
node 5gc.js ue add --name UE_TEST --imsi 460000000000001 --msisdn 8613888888888 --project XW_S5GC_1
# 批量添加 10 个连续 UE
node 5gc.js ue add --name UE_BATCH --count 10 --project XW_S5GC_1 --msisdn 8613900000000
# 指定认证密钥
node 5gc.js ue add --name UE_AUTH --project XW_S5GC_1 --key 00112233445566778899aabbccddeeff --opc 11223344556677889900aabbccddeeff
```
---
#### ue-edit-skill.js
**功能**:修改 UE 配置参数。支持批量和单个修改。
**使用方式**:
```bash
node 5gc.js ue edit [选项...]
```
**参数**:
| 参数 | 说明 |
|------|------|
| `--project <工程>` | 目标工程,不带 `--name` 时批量修改该工程下所有 UE |
| `--name <名称>` | 精确匹配要修改的 UE 名称(不支持批量时按名称过滤) |
| `--id <ID>` | 按 UE ID 修改 |
| `--set-msisdn <值>` | 修改 MSISDN |
| `--set-s_imsi <值>` | 修改 IMSI |
| `--set-mcc <值>` | 修改 MCC |
| `--set-mnc <值>` | 修改 MNC |
| `--set-key <值>` | 修改 KI 密钥 |
| `--set-opc <值>` | 修改 OPc 密钥 |
| `--set-imeisv <值>` | 修改 IMEISV |
| `--set-sst <值>` | 修改 NSSAI SST |
| `--set-sd <值>` | 修改 NSSAI SD |
| `--set-replay_ip <IP>` | 修改回放 IP |
| `--set-replay_port <端口>` | 修改回放端口 |
| `--set-count <数量>` | 修改数量 |
| `--url <地址>` | 5GC 仪表地址 |
| `--headed` | 打开可见浏览器 |
> ⚠️ `user_sip_ip_v4`、`user_sip_ip_v6` 在 UE 编辑表单中不存在此字段名,无需修改。
**示例**:
```bash
# 批量修改工程下所有 UE 的 MSISDN
node 5gc.js ue edit --project XW_S5GC_1 --set-msisdn 8613911111111
# 修改指定 UE
node 5gc.js ue edit --name UE_001 --project XW_S5GC_1 --set-msisdn 8613988888888 --set-sst 1 --set-sd 222222
# 按 ID 修改
node 5gc.js ue edit --id 10337 --set-opc aabbccddeeff00112233445566778899 --set-imeisv 8611111111111112
```
---
### PCF/PCRF
#### pcf-add-skill.js
**功能**:在指定工程下添加一个 PCF/PCRF 实例。
**使用方式**:
```bash
node 5gc.js pcf add <名称> [选项...]
node skills/5gc/scripts/pcf-add-skill.js <名称> [选项...]
```
**参数**:
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `<名称>` | PCF 实例名称(位置参数) | **必填** |
| `--project <工程>` / `-p <工程>` | 目标工程名称 | `XW_S5GC_1` |
| `--url <地址>` | 5GC 仪表地址 | `https://192.168.3.89` |
| `--http2_sip <IP>` | HTTP2 服务 IP | `192.168.20.90` |
| `--http2_port <端口>` | HTTP2 端口 | `80` |
| `--MCC <值>` | MCC(注意大写) | `460` |
| `--MNC <值>` | MNC(注意大写) | `01` |
| `--headed` | 打开可见浏览器 | false |
**示例**:
```bash
node 5gc.js pcf add PCF-TEST --project XW_S5GC_1
node 5gc.js pcf add PCF-PROD --project XW_S5GC_1 --http2_sip 10.0.0.50 --MCC 460 --MNC 01
```
#### pcf-edit-skill.js
**功能**:编辑指定工程下的 PCF/PCRF 实例(支持单条和批量)。
**使用方式**:
```bash
# 批量编辑:修改工程下所有 PCF 的字段
node 5gc.js pcf edit --project <工程> --set-<字段> <值>
# 单条编辑:修改指定名称的 PCF
node 5gc.js pcf edit --name <名称> --project <工程> --set-<字段> <值>
```
**可编辑字段**:
| 参数 | 说明 |
|------|------|
| `--set-http2_sip <IP>` | 修改 HTTP2 服务 IP |
| `--set-http2_port <端口>` | 修改 HTTP2 端口 |
| `--set-MCC <值>` | 修改 MCC(注意大写) |
| `--set-MNC <值>` | 修改 MNC(注意大写) |
**示例**:
```bash
# 批量修改工程下所有 PCF 的 HTTP2 IP
node 5gc.js pcf edit --project XW_S5GC_1 --set-http2_sip 10.10.10.99
# 修改指定 PCF 的 HTTP2 IP 和 MNC
node 5gc.js pcf edit --name pcc --project XW_S5GC_1 --set-http2_sip 10.10.10.88 --set-MNC 01
```
#### default-rule-add-skill.js(PCF 默认规则一键配置)
**功能**:为指定工程一键配置完整的 PCF 默认规则链路,包含 QoS 模板 → Traffic Control → PCC 规则 → sm_policy_default → PCF default_smpolicy 全五步。
**使用方式**:
```bash
node 5gc.js pcf default-rule-add --project <工程> [选项...]
node skills/5gc/scripts/default-rule-add-skill.js --project <工程> [选项...]
```
**参数**(全部可选,有默认值):
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `--project` | 工程名 | `XW_S5GC_1` |
| `--qos-id` | QoS 模板 ID | `qos_default_{时间戳}` |
| `--5qi` | 5QI 值(不指定则自动选择未使用的值) | 自动(优先 8/9/6/5...) |
| `--maxbr-ul` | 上行最大比特率 | `10000000` |
| `--maxbr-dl` | 下行最大比特率 | `20000000` |
| `--gbr-ul` | 上行保证比特率 | `5000000` |
| `--gbr-dl` | 下行保证比特率 | `5000000` |
| `--tc-id` | TC 规则 ID | `tc_default_{时间戳}` |
| `--flow-status` | TC 流状态 | `ENABLED` |
| `--pcc-id` | PCC 规则 ID | `pcc_default` |
| `--precedence` | PCC 优先级 | `63` |
| `--headed` | 显示浏览器窗口(调试用) | off |
**示例**:
```bash
# 最简用法(自动生成所有 ID)
node 5gc.js pcf default-rule-add --project XW_SUPF_5_1_2_4
# 指定 QoS 参数(高速率)
node 5gc.js pcf default-rule-add --project XW_SUPF_5_1_2_4 \
--qos-id qos_high_rate --5qi 8 \
--maxbr-ul 50000000 --maxbr-dl 100000000 \
--gbr-ul 20000000 --gbr-dl 40000000
# 指定 PCC 优先级
node 5gc.js pcf default-rule-add --project XW_SUPF_5_1_2_4 --pcc-id pcc_new --precedence 50
# 调试模式
node 5gc.js pcf default-rule-add --project XW_SUPF_5_1_2_4 --headed
```
> **注意**:同一工程多次运行会自动删除旧的同名资源并重建,不会污染配置。
### PCC 规则
#### pcc-add-skill.js
**功能**:在指定工程下添加一条 PCC 规则(PCC 规则用于绑定 QoS 模板和 Traffic Control)。
**使用方式**:
```bash
node 5gc.js pcc add --project <工程> --pcc-id <ID> --qos <QoS名称> --tc <TC名称> [选项...]
node skills/5gc/scripts/pcc-add-skill.js --project <工程> --pcc-id <ID> --qos <QoS名称> --tc <TC名称> [选项...]
```
**参数**:
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `--project` | 工程名 | `XW_S5GC_1` |
| `--pcc-id` | PCC 规则 ID(字母/数字/下划线) | **必填** |
| `--qos` | 引用的 QoS 模板名称 | **必填** |
| `--tc` | 引用的 Traffic Control 名称 | **必填** |
| `--precedence` | 优先级(0-255) | `63` |
| `--flow-desc` | 流描述(可选) | |
| `--headed` | 显示浏览器窗口 | off |
**示例**:
```bash
# 基本添加
node 5gc.js pcc add --project XW_SUPF_5_1_2_4 --pcc-id pcc_new --qos qos1 --tc tc1
# 指定优先级
node 5gc.js pcc add --project XW_SUPF_5_1_2_4 --pcc-id pcc_high --qos qos2 --tc tc1 --precedence 50
```
#### pcc-edit-skill.js
**功能**:编辑已有 PCC 规则的 QoS/TC 绑定(切换 PCC 引用的 QoS 模板或 Traffic Control)。
**使用方式**:
```bash
node 5gc.js pcc edit --project <工程> --name <PCC名称> --set-qos <新QoS> [--set-tc <新TC>]
```
**参数**:
| 参数 | 说明 |
|------|------|
| `--project` | 工程名 |
| `--name` | 要修改的 PCC 规则名称(精确匹配) |
| `--set-qos <名称>` | 切换到新的 QoS 模板 |
| `--set-tc <名称>` | 切换到新的 Traffic Control |
| `--headed` | 显示浏览器窗口 |
**示例**:
```bash
# 修改 PCC 引用的 QoS(用于修改上下行速率)
node 5gc.js pcc edit --project XW_SUPF_5_1_2_4 --name pcc_default --set-qos qos_high_rate
# 同时修改 QoS 和 TC
node 5gc.js pcc edit --project XW_SUPF_5_1_2_4 --name pcc_default --set-qos qos1 --set-tc tc2
```
> ⚠️ **重要**:PCC 的 `refQosData` 和 `refTcData` 存储在 xm-select 多选组件中。编辑时会自动切换选择,无需手动取消旧选项。
### NRF(网络存储功能)
#### nrf-add-skill.js
**功能**:在指定工程下添加一个 NRF 实例。
**使用方式**:
```bash
node 5gc.js nrf add <名称> [选项...]
node skills/5gc/scripts/nrf-add-skill.js <名称> [选项...]
```
**参数**:
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `<名称>` | NRF 实例名称(位置参数) | **必填** |
| `--project <工程>` / `-p <工程>` | 目标工程名称 | `XW_S5GC_1` |
| `--url <地址>` | 5GC 仪表地址 | `https://192.168.3.89` |
| `--http2_sip <IP>` | HTTP2 服务 IP | `192.168.20.100` |
| `--http2_port <端口>` | HTTP2 端口 | `80` |
| `--MCC <值>` | MCC(注意大写) | `460` |
| `--MNC <值>` | MNC(注意大写) | `01` |
| `--headed` | 打开可见浏览器 | false |
**示例**:
```bash
node 5gc.js nrf add NRF-TEST --project XW_S5GC_1
node 5gc.js nrf add NRF-PROD --project XW_S5GC_1 --http2_sip 10.0.0.50 --MCC 460 --MNC 01
```
#### nrf-edit-skill.js
**功能**:编辑指定工程下的 NRF 实例(支持单条和批量)。
**使用方式**:
```bash
# 批量编辑:修改工程下所有 NRF 的字段
node 5gc.js nrf edit --project <工程> --set-<字段> <值>
# 单条编辑:修改指定名称的 NRF
node 5gc.js nrf edit --name <名称> --project <工程> --set-<字段> <值>
```
**可编辑字段**:
| 参数 | 说明 |
|------|------|
| `--set-http2_sip <IP>` | 修改 HTTP2 服务 IP |
| `--set-http2_port <端口>` | 修改 HTTP2 端口 |
| `--set-MCC <值>` | 修改 MCC(注意大写) |
| `--set-MNC <值>` | 修改 MNC(注意大写) |
**示例**:
```bash
# 批量修改工程下所有 NRF 的 HTTP2 IP
node 5gc.js nrf edit --project XW_S5GC_1 --set-http2_sip 10.10.10.99
# 修改指定 NRF 的 HTTP2 IP 和 MNC
node 5gc.js nrf edit --name nrf1 --project XW_S5GC_1 --set-http2_sip 10.10.10.88 --set-MNC 01
```
### QoS 模板
#### qos-add-skill.js
**功能**:在指定工程下添加一个 QoS(服务质量)模板,定义 5QI、上下行最大比特率、保证比特率等参数。
**使用方式**:
```bash
node 5gc.js qos add --project <工程> --qos-id <ID> [选项...]
node skills/5gc/scripts/qos-add-skill.js --project <工程> --qos-id <ID> [选项...]
```
**参数**:
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `--project` | 工程名 | `XW_S5GC_1` |
| `--qos-id` | QoS 模板 ID(字母/数字/下划线) | **必填** |
| `--5qi` | 5QI 值(不指定则自动选择) | 自动选择未使用的值(优先 8/9/6/5...) |
| `--maxbr-ul` | 上行最大比特率(bps) | `10000000` |
| `--maxbr-dl` | 下行最大比特率(bps) | `20000000` |
| `--gbr-ul` | 上行保证比特率(bps) | `5000000` |
| `--gbr-dl` | 下行保证比特率(bps) | `5000000` |
| `--priority` | 优先级 | 空 |
| `--headed` | 显示浏览器窗口 | off |
**示例**:
```bash
# 基本添加
node 5gc.js qos add --project XW_SUPF_5_1_2_4 --qos-id qos1
# 高速率 QoS
node 5gc.js qos add --project XW_SUPF_5_1_2_4 --qos-id qos_high \
--5qi 8 --maxbr-ul 50000000 --maxbr-dl 100000000 \
--gbr-ul 20000000 --gbr-dl 40000000
# 批量创建不同 5qi 的 QoS 模板
node 5gc.js qos add --project XW_SUPF_5_1_2_4 --qos-id qos_6 --5qi 6
node 5gc.js qos add --project XW_SUPF_5_1_2_4 --qos-id qos_9 --5qi 9
```
---
### Traffic Control
#### tc-add-skill.js
**功能**:在指定工程下添加一条 Traffic Control 流量控制规则,控制 UE 流量的启用/禁用状态。
**使用方式**:
```bash
node 5gc.js tc add --project <工程> --tc-id <ID> [选项...]
node skills/5gc/scripts/tc-add-skill.js --project <工程> --tc-id <ID> [选项...]
```
**参数**:
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `--project` | 工程名 | `XW_S5GC_1` |
| `--tc-id` | TC 规则 ID(字母/数字/下划线) | **必填** |
| `--flow-status` | 流状态 | `ENABLED` |
| `--flow-desc` | 流描述(可选) | |
| `--headed` | 显示浏览器窗口 | off |
> **flow-status 选项**:`ENABLED`(启用)、`DISABLED`(禁用)、`ENABLED-UPLINK`(仅上行)等。
**示例**:
```bash
# 基本添加
node 5gc.js tc add --project XW_SUPF_5_1_2_4 --tc-id tc1
# 指定流状态
node 5gc.js tc add --project XW_SUPF_5_1_2_4 --tc-id tc_uplink --flow-status ENABLED-UPLINK
```
---
### SMPolicy
#### smpolicy_add_pcc.js {#smpolicy-default}
**功能**:将已有 PCC 规则添加到工程默认的 `sm_policy_default` 会话策略中(追加到 pccRules 列表)。
**使用方式**:
```bash
node 5gc.js smpolicy add-pcc --project <工程> --pcc-id <PCC名称>
node skills/5gc/scripts/smpolicy_add_pcc.js --project <工程> --pcc-id <PCC名称>
```
**参数**:
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `--project` | 工程名 | `XW_SUPF_5_1_2_4` |
| `--pcc-id` | 已存在的 PCC 规则 ID | **必填** |
| `--headed` | 显示浏览器窗口 | off |
> **链路**:`smpolicy/default/index` → 编辑 `sm_policy_default` 弹窗 → pccRules xm-select 中追加指定 PCC。
**示例**:
```bash
# 将 PCC 添加到 sm_policy_default
node 5gc.js smpolicy add-pcc --project XW_SUPF_5_1_2_4 --pcc-id pcc_high_rate
# 添加多个 PCC 规则
node 5gc.js smpolicy add-pcc --project XW_SUPF_5_1_2_4 --pcc-id pcc_default
node 5gc.js smpolicy add-pcc --project XW_SUPF_5_1_2_4 --pcc-id pcc_video
```
---
#### smpolicy-ue-add-skill.js {#smpolicy-ue-add-skilljs}
**功能**:在指定工程下添加一条 UE Smpolicy 规则,按 IMSI/DNN/sNssai 匹配 UE 并关联 PCC 规则。
**使用方式**:
```bash
node 5gc.js smpolicy ue-add --project <工程> --name <名称> --dnn <DNN> [选项...]
node skills/5gc/scripts/smpolicy-ue-add-skill.js --project <工程> --name <名称> --dnn <DNN> [选项...]
```
**参数**:
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `--project` | 工程名 | `XW_S5GC_1` |
| `--name` | UE策略名称(字母/数字/下划线) | **必填** |
| `--dnn` | DNN | **必填** |
| `--imsi` | IMSI 起始值(不填则自动生成) | 自动 |
| `--imsi-num` | IMSI 数量 | `1` |
| `--sst` | sNssai SST | `1` |
| `--sd` | sNssai SD | `111111` |
| `--sess-rules` | 会话规则(xm-select,多个逗号分隔) | |
| `--pcc-rules` | PCC规则(xm-select,多个逗号分隔) | |
| `--pra-rules` | PRA规则(xm-select,可选) | |
| `--ref-qos-timer` | reflectiveQoSTimer 值(秒) | |
| `--headed` | 显示浏览器窗口 | off |
**示例**:
```bash
# 基本添加
node 5gc.js smpolicy ue-add --project XW_SUPF_5_1_2_4 --name ue_policy1 --dnn internet
# 指定 IMSI 和 sNssai
node 5gc.js smpolicy ue-add --project XW_SUPF_5_1_2_4 --name ue_policy1 --dnn internet \
--imsi 460001234567890 --sst 1 --sd 111111
# 绑定 PCC 规则(多个逗号分隔)
node 5gc.js smpolicy ue-add --project XW_SUPF_5_1_2_4 --name ue_policy2 --dnn internet \
--pcc-rules "pcc2,pcc_default"
# 指定反射 QoS 定时器
node 5gc.js smpolicy ue-add --project XW_SUPF_5_1_2_4 --name ue_policy3 --dnn internet \
--pcc-rules pcc2 --ref-qos-timer 60
```
#### smpolicy-ue-edit-skill.js
**功能**:编辑已有 UE Smpolicy 规则的字段(DNN、sNssai、PCC 绑定等)。
**使用方式**:
```bash
node 5gc.js smpolicy ue-edit --project <工程> --name <名称> [--dnn <新DNN>] [--pcc-rules <规则>] [选项...]
```
**参数**:
| 参数 | 说明 |
|------|------|
| `--project` | 工程名 |
| `--name` | 要编辑的 UE 策略名称(精确匹配) |
| `--dnn` | 新 DNN |
| `--imsi` | 新 IMSI 起始值 |
| `--sst` | 新 sNssai SST |
| `--sd` | 新 sNssai SD |
| `--sess-rules` | 会话规则(xm-select,多个逗号分隔) |
| `--pcc-rules` | PCC规则(xm-select,多个逗号分隔) |
| `--pra-rules` | PRA规则(xm-select) |
| `--ref-qos-timer` | reflectiveQoSTimer |
| `--headed` | 显示浏览器窗口 |
> ⚠️ xm-select 为多选模式。指定 `--pcc-rules` 时会叠加选中已有规则;编辑时需注意 toggle 行为。
**示例**:
```bash
# 修改 DNN
node 5gc.js smpolicy ue-edit --project XW_SUPF_5_1_2_4 --name ue_policy1 --dnn internet_new
# 修改 PCC 绑定
node 5gc.js smpolicy ue-edit --project XW_SUPF_5_1_2_4 --name ue_policy1 --pcc-rules pcc2,pcc_reg_test
# 修改 sNssai
node 5gc.js smpolicy ue-edit --project XW_SUPF_5_1_2_4 --name ue_policy1 --sst 1 --sd 222222
```
#### smpolicy-dnn-add-skill.js {#smpolicy-dnn}
**功能**:在指定工程下添加一条 DNN Smpolicy 规则,按 DNN/sNssai 匹配会话并关联 PCC 规则。
**使用方式**:
```bash
node 5gc.js smpolicy dnn-add --project <工程> --name <名称> --dnn <DNN> [选项...]
```
**参数**:
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `--project` | 工程名 | `XW_S5GC_1` |
| `--name` | DNN策略名称(必填) | **必填** |
| `--dnn` | DNN值(必填) | **必填** |
| `--sst` | sNssai SST | `1` |
| `--sd` | sNssai SD | `111111` |
| `--sess-rules` | 会话规则(xm-select,多个逗号分隔) | |
| `--pcc-rules` | PCC规则(xm-select,多个逗号分隔) | |
| `--pra-rules` | PRA规则(xm-select,可选) | |
| `--ref-qos-timer` | reflectiveQoSTimer(秒) | |
| `--headed` | 显示浏览器窗口 | off |
**示例**:
```bash
# 基本添加
node 5gc.js smpolicy dnn-add --project XW_SUPF_5_1_2_4 --name dnn_policy1 --dnn internet
# 绑定 PCC 规则
node 5gc.js smpolicy dnn-add --project XW_SUPF_5_1_2_4 --name dnn_policy1 --dnn internet --pcc-rules pcc2
# 多个 PCC 规则
node 5gc.js smpolicy dnn-add --project XW_SUPF_5_1_2_4 --name dnn_policy2 --dnn internet --pcc-rules "pcc2,pcc_default"
```
#### smpolicy-dnn-edit-skill.js
**功能**:编辑已有 DNN Smpolicy 规则的字段(DNN、sNssai、PCC 绑定等)。
**使用方式**:
```bash
node 5gc.js smpolicy dnn-edit --project <工程> --name <名称> [--dnn <新DNN>] [--pcc-rules <规则>] [选项...]
```
**参数**:
| 参数 | 说明 |
|------|------|
| `--project` | 工程名 |
| `--name` | 要编辑的 DNN 策略名称(精确匹配) |
| `--dnn` | 新 DNN 值 |
| `--sst` | 新 sNssai SST |
| `--sd` | 新 sNssai SD |
| `--sess-rules` | 会话规则(xm-select,多个逗号分隔) |
| `--pcc-rules` | PCC规则(xm-select,多个逗号分隔) |
| `--pra-rules` | PRA规则(xm-select) |
| `--ref-qos-timer` | reflectiveQoSTimer |
| `--headed` | 显示浏览器窗口 |
> ⚠️ xm-select 为多选模式。指定 `--pcc-rules` 时会叠加选中已有规则;编辑时需注意 toggle 行为。
**示例**:
```bash
# 修改 DNN
node 5gc.js smpolicy dnn-edit --project XW_SUPF_5_1_2_4 --name dnn_policy1 --dnn internet_new
# 修改 PCC 绑定
node 5gc.js smpolicy dnn-edit --project XW_SUPF_5_1_2_4 --name dnn_policy1 --pcc-rules pcc2,pcc_default
```
---
## 全局参数参考
以下参数所有脚本均支持:
| 参数 | 说明 | 适用范围 |
|------|------|---------|
| `--url <地址>` | 5GC 仪表 URL | 所有脚本 |
| `--project <工程>` / `-p <工程>` | 目标工程名称 | 所有脚本 |
| `--headed` | 打开可见 Chromium 窗口(调试用) | 所有脚本 |
| `--set-<字段> <值>` | 修改指定字段值 | 所有 edit 脚本 |
| `--name <名称>` | 按名称精确匹配 | 所有 edit 脚本 |
| `--id <ID>` | 按 ID 直接定位 | 所有 edit 脚本 |
---
## 字段参考
### AMF 字段
| 字段名 | 说明 | 示例值 |
|--------|------|--------|
| `mcc` | 移动国家码 | `460` |
| `mnc` | 移动网络码 | `01` |
| `ngap_sip` | NGAP 信令面 IP | `10.200.1.50` |
| `ngap_port` | NGAP 端口 | `38412` |
| `http2_sip` | HTTP2 服务 IP | `10.200.1.51` |
| `http2_port` | HTTP2 端口 | `8080` |
| `stac` | 起始 TAC | `101` |
| `etac` | 结束 TAC | `102` |
| `region_id` | 区域 ID | `1` |
| `set_id` | Set ID | `1` |
| `pointer` | 指针 | `1` |
| `ea[NEA0]` ~ `ea[128-NEA3]` | 加密算法(默认全选) | `1` |
| `ia[NIA0]` ~ `ia[128-NIA3]` | 完整性保护算法(默认全选) | `1` |
### UDM/AUSF 字段
| 字段名 | 说明 | 示例值 |
|--------|------|--------|
| `count` | 实例数量 | `3` |
| `sip` | SIP 服务 IP | `10.0.0.100` |
| `port` | 端口 | `80` |
| `auth_method` | 认证方法 | `5G_AKA` |
| `scheme` | 协议类型 | `HTTP` |
| `priority` | 优先级 | `8` |
### SMF/PGW-C 字段
| 字段名 | 说明 | 示例值 |
|--------|------|--------|
| `pfcp_sip` | PFCP 信令面 IP | `10.10.10.50` |
| `n3_ip` | N3 接口 IP | `10.10.10.50` |
| `n6_ip` | N6 接口 IP | `10.10.10.51` |
| `http2_sip` | HTTP2 服务 IP | `10.10.10.50` |
| `dnn` | DNN(数据网络名) | `internet` |
| `snssai_sst` | NSSAI SST | `1` |
| `snssai_sd` | NSSAI SD | `ffffff` |
| `mcc` | MCC | `460` |
| `mnc` | MNC | `01` |
| `pdu_capacity` | PDU 会话容量 | `200000` |
### UPF/PGW-U 字段
| 字段名 | 说明 | 示例值 |
|--------|------|--------|
| `n3_ip` | N3 接口 IP | `192.168.20.30` |
| `n4_ip` | N4 接口 IP(PFCP) | `192.168.20.30` |
| `n6_ip` | N6 接口 IP | `192.168.20.31` |
| `n6_gw` | N6 网关 IP | `192.168.20.1` |
| `dnn` | DNN | `internet` |
| `static_arp` | 静态 ARP | `192.168.20.254` |
| `sst` | NSSAI SST | `1` |
| `sd` | NSSAI SD | `ffffff` |
| `stac` | 起始 TAC | `101` |
| `etac` | 结束 TAC | `102` |
### GNB 字段
| 字段名 | 说明 | 示例值 |
|--------|------|--------|
| `ngap_sip` | NGAP 信令面 IP | `200.20.20.50` |
| `user_sip_ip_v4` | 用户面 IPv4 | `2.2.2.2` |
| `user_sip_ip_v6` | 用户面 IPv6 | `::1` |
| `mcc` | MCC | `460` |
| `mnc` | MNC | `60` |
| `stac` | 起始 TAC | `0` |
| `etac` | 结束 TAC | `0` |
| `node_id` | 节点 ID | `70` |
| `cell_count` | 小区数量 | `1` |
| `replay_ip` | 回放 IP | `0.0.0.0` |
| `replay_port` | 回放端口 | `0` |
### UE 字段
| 字段名 | 说明 | 示例值 |
|--------|------|--------|
| `s_imsi` | 起始 IMSI(15位) | `460001234567890` |
| `msisdn` | MSISDN(13-15位,86开头) | `8613888888888` |
| `mcc` | MCC | `460` |
| `mnc` | MNC | `01` |
| `key` | KI 密钥(32位 hex) | `001122...` |
| `op_opc` | OPc 密钥(32位 hex) | `aabbcc...` |
| `imeisv` | IMEISV(15位,偶数) | `8611111111111111` |
| `nssai_sst` | NSSAI SST | `1` |
| `nssai_sd` | NSSAI SD | `111111` |
| `user_sip_ip_v4` | 用户面 IPv4 | `自动分配` |
| `user_sip_ip_v6` | 用户面 IPv6 | `自动分配` |
| `replay_ip` | 回放 IP | `0.0.0.0` |
| `replay_port` | 回放端口 | `0` |
#### default-rule-add-skill.js(PCF 默认规则一键配置)
**功能**:为指定工程一键配置完整的 PCF 默认规则链路,包含 QoS 模板 → Traffic Control → PCC 规则 → sm_policy_default → PCF default_smpolicy 全五步。
**使用方式**:
```bash
node 5gc.js pcf default-rule-add --project <工程> [选项...]
node skills/5gc/scripts/default-rule-add-skill.js --project <工程> [选项...]
```
**参数**(全部可选,有默认值):
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `--project` | 工程名 | `XW_S5GC_1` |
| `--pcf-name` | **PCF 实例名称**(必填,指定要为哪个 PCF 配置默认规则) | 无 |
| `--qos-id` | QoS 模板 ID | `qos_default_{时间戳}` |
| `--5qi` | 5QI 值(不指定则自动选择未使用的值) | 自动(优先 8/9/6/5...) |
| `--maxbr-ul` | 上行最大比特率 | `10000000` |
| `--maxbr-dl` | 下行最大比特率 | `20000000` |
| `--gbr-ul` | 上行保证比特率 | `5000000` |
| `--gbr-dl` | 下行保证比特率 | `5000000` |
| `--tc-id` | TC 规则 ID | `tc_default_{时间戳}` |
| `--flow-status` | TC 流状态 | `ENABLED` |
| `--pcc-id` | PCC 规则 ID | `pcc_default` |
| `--precedence` | PCC 优先级 | `63` |
| `--headed` | 显示浏览器窗口(调试用) | off |
**示例**:
```bash
# 最简用法(自动生成所有 ID)
node 5gc.js pcf default-rule-add --project XW_SUPF_5_1_2_4 --pcf-name pcc
# 指定 QoS 参数(高速率)
node 5gc.js pcf default-rule-add --project XW_SUPF_5_1_2_4 --pcf-name pcc \
--qos-id qos_high_rate --5qi 8 \
--maxbr-ul 50000000 --maxbr-dl 100000000 \
--gbr-ul 20000000 --gbr-dl 40000000
# 指定 PCC 优先级
node 5gc.js pcf default-rule-add --project XW_SUPF_5_1_2_4 --pcf-name pcc --pcc-id pcc_new --precedence 50
# 调试模式
node 5gc.js pcf default-rule-add --project XW_SUPF_5_1_2_4 --pcf-name pcc --headed
```
**完整链路**:
1. ✅ **QoS 模板创建**:自动选择未使用的 5QI,创建 QoS 模板
2. ✅ **Traffic Control 创建**:创建 ENABLED 状态的 TC 规则
3. ✅ **PCC 规则创建**:创建 PCC 规则,绑定 QoS 和 TC
4. ✅ **sm_policy_default 创建/更新**:创建或更新默认会话策略,绑定 PCC 规则
5. ✅ **PCF default_smpolicy 设置**:为指定 PCF 实例设置 default_smpolicy 为 sm_policy_default
**注意事项**:
- 同一工程多次运行会自动删除旧的同名资源并重建,不会污染配置
- 必须指定 `--pcf-name` 参数,明确要为哪个 PCF 实例配置默认规则
- 脚本会自动处理弹窗(iframe)和 CSRF token,无需手动操作
- 所有步骤都有验证检查,确保配置成功
**已测试工程**:
- ✅ XW_SUPF_5_1_11_2(PCF "qqq")
- ✅ XW_SUPF_5_1_8_1(PCF "pcc")
- ✅ XW_SUPF_5_1_4_1(PCF "pcc")
### PCF/PCRF 字段
| 字段名 | 说明 | 示例值 |
|--------|------|--------|
| `http2_sip` | HTTP2 服务 IP | `192.168.20.90` |
| `http2_port` | HTTP2 端口 | `80` |
| `MCC` | MCC(大写) | `460` |
| `MNC` | MNC(大写) | `01` |
| `count` | 实例数量 | `1` |
FILE:scripts/5gc.js
/**
* 5GC Web 仪表统一 CLI
*
* 用法: node 5gc.js <entity> <action> [options]
*
* entity (网元类型): amf | udm | smf | upf | gnb | ue | pcf | nrf | qos | tc | smpolicy
* action (操作类型): add | edit | default-rule-add | default-rule-edit
*
* 通用选项:
* --url <地址> 5GC 仪表地址(默认 https://192.168.3.89)
* --project <工程> 目标工程名称
* --name <名称> 网元名称(用于单条记录筛选)
* --id <id> 网元 ID(直接编辑指定 ID)
* --headed 以有头模式运行(显示浏览器窗口)
*
* 字段修改(edit 模式)--set-<field> <value>:
* AMF: name|sbi_ip|sbi_port|amf_name|guami|mcc|mnc|sst|sd|ap1|ap2|ap3|ap4|ap5
* UDM: name|auth_supi|auth_op_type|op_opc|aud_method|scheme|id|priority
* SMF: name|pfcp_ip|n3_ip|n6_ip|dnn|snssai|sliceamba_type
* UPF: name|n4_ip|n3_ip|n6_ip|dnn|snssai|count|static_arp|ue_ip_pool
* GNB: name|ngap_ip|user_sip_ip_v4|mcc|mnc|stac|etac|node_id|cell_count|replay_ip|replay_port
* UE: name|count|mcc|mnc|s_imsi|key|opc|imeisv|msisdn|user_sip_ip_v4|user_sip_ip_v6|replay_ip|replay_port
*
* 示例:
* node 5gc.js amf add --name AMF_TEST --project XW_S5GC_1 --sbi-ip 10.0.0.1
* node 5gc.js gnb add --name GNB_TEST --project XW_S5GC_1 --count 1 --mcc 460 --mnc 01 --stac 1 --etac 100
* node 5gc.js ue add --name UE_001 --imsi 460001234567890 --msisdn 8613888888888
* node 5gc.js ue edit --project XW_S5GC_1 --set-msisdn 8613888888888
* node 5gc.js ue edit --id 10337 --set-msisdn 8613888888888
* node 5gc.js gnb edit --project XW_S5GC_1 --set-user_sip_ip_v4 200.200.200.200
* node 5gc.js upf edit --project XW_S5GC_1 --set-n4_ip 10.0.0.5
* node 5gc.js pcf default-rule-add --project XW_SUPF_5_1_2_4 --pcf-name pcc
* node 5gc.js pcf default-rule-add --project XW_SUPF_5_1_2_4 --pcf-name pcc --qos-id qos1 --tc-id tc1 --pcc-id pcc_default --precedence 50
* node 5gc.js qos add --project XW_SUPF_5_1_2_4 --qos-id qos_new --5qi 8 --maxbr-ul 10000000 --maxbr-dl 20000000
* node 5gc.js tc add --project XW_SUPF_5_1_2_4 --tc-id tc_new --flow-status ENABLED
* node 5gc.js pcc add --project XW_SUPF_5_1_2_4 --pcc-id pcc_new --qos qos1 --tc tc1
* node 5gc.js smpolicy default-add-pcc --project XW_SUPF_5_1_2_4 --pcc-id pcc_new
*/
const { spawn } = require('child_process');
const path = require('path');
const fs = require('fs');
const SCRIPTS_DIR = __dirname;
const argv = process.argv.slice(2);
if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') {
printHelp();
process.exit(0);
}
const entity = argv[0].toLowerCase();
const action = (argv[1] || '').toLowerCase();
const VALID_ENTITIES = ['amf', 'udm', 'smf', 'upf', 'gnb', 'ue', 'pcf', 'pcc', 'nrf', 'qos', 'tc', 'smpolicy'];
const VALID_ACTIONS = ['add', 'edit', 'default-rule-add', 'add-pcc', 'ue-add', 'ue-edit', 'dnn-add', 'dnn-edit'];
if (!VALID_ENTITIES.includes(entity)) {
console.error(`\n❌ 未知网元类型: entity`);
console.error(' 可用: ' + VALID_ENTITIES.join(', '));
process.exit(1);
}
if (!action || !VALID_ACTIONS.includes(action)) {
console.error(`\n❌ 未知操作: action || '(空)'`);
console.error(' 用法: node 5gc.js <entity> <action> [options]');
console.error(' 示例: node 5gc.js amf add --help');
process.exit(1);
}
// 子脚本映射
// 所有 edit 均映射到 edit 脚本(单条 + 批量二合一)
const scriptMap = {
'amf:add': 'amf-add-skill.js',
'amf:edit': 'amf-edit-skill.js',
'udm:add': 'ausf-udm-add-skill.js',
'udm:edit': 'ausf-udm-edit-skill.js',
'smf:add': 'smf-pgwc-add-skill.js',
'smf:edit': 'smf-pgwc-edit-skill.js',
'upf:add': 'upf-add-skill.js',
'upf:edit': 'upf-edit-skill.js',
'gnb:add': 'gnb-add-skill.js',
'gnb:edit': 'gnb-edit-skill.js',
'ue:add': 'ue-add-skill.js',
'ue:edit': 'ue-edit-skill.js',
'pcf:add': 'pcf-add-skill.js',
'pcf:edit': 'pcf-edit-skill.js',
'pcf:default-rule-add': 'default-rule-add-skill.js',
'pcc:add': 'pcc-add-skill.js',
'pcc:edit': 'pcc-edit-skill.js',
'nrf:add': 'nrf-add-skill.js',
'nrf:edit': 'nrf-edit-skill.js',
'qos:add': 'qos-add-skill.js',
'tc:add': 'tc-add-skill.js',
'smpolicy:add-pcc': 'smpolicy_add_pcc.js',
'smpolicy:ue-add': 'smpolicy-ue-add-skill.js',
'smpolicy:ue-edit': 'smpolicy-ue-edit-skill.js',
'smpolicy:dnn-add': 'smpolicy-dnn-add-skill.js',
'smpolicy:dnn-edit': 'smpolicy-dnn-edit-skill.js',
};
const scriptFile = scriptMap[`entity:action`];
const scriptPath = path.join(SCRIPTS_DIR, scriptFile);
if (!fs.existsSync(scriptPath)) {
console.error(`\n❌ 脚本不存在: scriptPath`);
process.exit(1);
}
function normalizeChildArgs(entity, action, args) {
const out = [];
let positionalName = null;
for (let i = 0; i < args.length; i++) {
const arg = args[i];
const next = i + 1 < args.length ? args[i + 1] : undefined;
if (arg === '--name' && next !== undefined) {
if (entity === 'ue' && action === 'add') {
out.push('--name', next);
} else {
positionalName = next;
}
i++;
continue;
}
if ((entity === 'smf' || entity === 'upf' || entity === 'gnb') && arg === '--pfcp-ip' && next !== undefined) {
out.push('--pfcp_sip', next); i++; continue;
}
if (entity === 'smf' && arg === '--n3-ip' && next !== undefined) {
out.push('--http2_sip', next); i++; continue;
}
if (entity === 'upf' && arg === '--n4-ip' && next !== undefined) {
out.push('--n4_ip', next); i++; continue;
}
if (entity === 'upf' && arg === '--n3-ip' && next !== undefined) {
out.push('--n3_ip', next); i++; continue;
}
if (entity === 'upf' && arg === '--n6-ip' && next !== undefined) {
out.push('--n6_ip', next); i++; continue;
}
if (entity === 'gnb' && arg === '--ngap-ip' && next !== undefined) {
out.push('--ngap_sip', next); i++; continue;
}
if (entity === 'gnb' && arg === '--user-sip-ip-v4' && next !== undefined) {
out.push('--user_sip_ip_v4', next); i++; continue;
}
if (entity === 'gnb' && arg === '--node-id' && next !== undefined) {
out.push('--node_id', next); i++; continue;
}
if (entity === 'amf' && action === 'add') {
if (arg === '--sbi-ip' && next !== undefined) { out.push('--http2_sip', next); i++; continue; }
if (arg === '--sst' && next !== undefined) { i++; continue; }
if (arg === '--sd' && next !== undefined) { i++; continue; }
}
if (entity === 'udm' && action === 'add') {
if (arg === '--auth-supi' && next !== undefined) { i++; continue; }
if (arg === '--auth-op-type' && next !== undefined) { i++; continue; }
if (arg === '--opc' && next !== undefined) { out.push('--op_opc', next); i++; continue; }
}
out.push(arg);
}
if (positionalName) out.unshift(positionalName);
return out;
}
// 去掉 entity 和 action 后的参数传给子脚本
const childArgv = normalizeChildArgs(entity, action, argv.slice(2));
console.log(`\n▶ 5GC entity.toUpperCase() action`);
console.log(' → node ' + scriptFile + ' ' + childArgv.join(' ') + '\n');
// 用子进程调用,保持 CLI 参数隔离
const child = spawn('node', [scriptPath, ...childArgv], {
stdio: 'inherit',
shell: true,
cwd: SCRIPTS_DIR,
});
child.on('exit', (code) => process.exit(code || 0));
child.on('error', (err) => { console.error('启动失败:', err.message); process.exit(1); });
function printHelp() {
console.log(`
5GC Web 仪表自动化 - 统一 CLI
=============================
用法:
node 5gc.js <entity> <action> [options]
网元类型 (entity):
amf - AMF(接入与移动性管理功能)
udm - UDM/AUSF(统一数据管理/认证服务器功能)
smf - SMF/PGW-C(会话管理功能/PDN 连接网关控制面)
upf - UPF/PGW-U(用户面功能/PDN 连接网关用户面)
gnb - gNodeB(5G 基站)
ue - UE(用户终端)
pcf - PCF/PCRF(策略控制功能)
nrf - NRF(网络存储功能)
qos - QoS 模板
tc - Traffic Control 流量控制规则
smpolicy - Smpolicy(会话策略规则)
操作 (action):
add - 添加网元实例
edit - 编辑网元(单个或批量)
default-rule-add - 一键配置完整 PCF 默认规则链路(QoS → TC → PCC → sm_policy_default → PCF)
通用选项:
--url <地址> 5GC 仪表地址(默认 https://192.168.3.89)
--project <工程> 目标工程名称
--name <名称> 网元名称
--id <id> 网元 ID(edit 模式)
--headed 以有头模式运行(显示浏览器)
--help 显示本帮助
字段修改(edit 模式 --set-<field> <value>):
AMF: name|sbi_ip|sbi_port|amf_name|guami|mcc|mnc|sst|sd|ap1|ap2|ap3|ap4|ap5
UDM: name|auth_supi|auth_op_type|op_opc|aud_method|scheme|id|priority
SMF: name|pfcp_ip|n3_ip|n6_ip|dnn|snssai|sliceamba_type
UPF: name|n4_ip|n3_ip|n6_ip|dnn|snssai|count|static_arp|ue_ip_pool
GNB: name|ngap_ip|user_sip_ip_v4|mcc|mnc|stac|etac|node_id|cell_count|replay_ip|replay_port
UE: name|count|mcc|mnc|s_imsi|key|opc|imeisv|msisdn|user_sip_ip_v4|user_sip_ip_v6|replay_ip|replay_port
PCF: http2_sip|http2_port|mcc|mnc
PCF默认规则: --pcf-name <名称> --qos-id <ID> --tc-id <ID> --pcc-id <ID> --precedence <值>
添加示例:
node 5gc.js amf add --name AMF_TEST --project XW_S5GC_1 --sbi-ip 10.0.0.1 --mcc 460 --mnc 01
node 5gc.js gnb add --name GNB_TEST --project XW_S5GC_1 --count 1 --mcc 460 --mnc 01 --stac 1 --etac 100
node 5gc.js ue add --name UE_001 --imsi 460001234567890 --msisdn 8613888888888
node 5gc.js smf add --name SMF_TEST --project XW_S5GC_1 --pfcp-ip 10.0.0.2
node 5gc.js upf add --name UPF_TEST --project XW_S5GC_1 --n4-ip 10.0.0.3
node 5gc.js qos add --project XW_SUPF_5_1_2_4 --qos-id qos_new --5qi 8 --maxbr-ul 10000000 --maxbr-dl 20000000
node 5gc.js tc add --project XW_SUPF_5_1_2_4 --tc-id tc_new --flow-status ENABLED
node 5gc.js pcc add --project XW_SUPF_5_1_2_4 --pcc-id pcc_new --qos qos1 --tc tc1
node 5gc.js pcf default-rule-add --project XW_SUPF_5_1_2_4 --pcf-name pcc
node 5gc.js pcf default-rule-add --project XW_SUPF_5_1_2_4 --pcf-name pcc --qos-id qos1 --tc-id tc1 --pcc-id pcc_default --precedence 50
编辑示例:
node 5gc.js ue edit --project XW_S5GC_1 --set-msisdn 8613888888888
node 5gc.js ue edit --id 10337 --set-msisdn 8613888888888
node 5gc.js gnb edit --project XW_S5GC_1 --set-user_sip_ip_v4 200.200.200.200
node 5gc.js upf edit --project XW_S5GC_1 --set-n4_ip 10.0.0.5
`);
}
FILE:scripts/amf-add-skill.js
#!/usr/bin/env node
/**
* AMF 添加脚本 - 完整修复版
* 功能:登录状态缓存 + .projectSelect 选工程 + evaluate 填写表单 + 算法全勾选 + NSSAI
*/
const { chromium } = require('playwright');
let globalBaseUrl = 'https://192.168.3.89';
const fs = require('fs');
const path = require('path');
// 配置
const CONFIG = {
urls: {
login: '/login',
amfEdit: '/sim_5gc/amf/edit',
amfManagement: '/sim_5gc/amf/index'
},
credentials: {
email: '[email protected]',
password: 'dotouch'
},
sessionDir: path.join(__dirname, '.sessions'),
getSessionFile() {
const host = globalBaseUrl.replace(/https?:\/\//, '').replace(/\./g, '_');
return `5gc_session_host.json`;
}
};
// 会话管理
class SessionManager {
constructor() {
this.sessionPath = path.join(CONFIG.sessionDir, CONFIG.getSessionFile());
}
async saveSession(context) {
try {
const storageState = await context.storageState();
fs.writeFileSync(this.sessionPath, JSON.stringify({ storageState }, null, 2));
return true;
} catch {
return false;
}
}
async loadSession(browser) {
try {
if (!fs.existsSync(this.sessionPath)) return null;
const { storageState } = JSON.parse(fs.readFileSync(this.sessionPath, 'utf8'));
return await browser.newContext({ storageState, ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
} catch {
return null;
}
}
}
// 算法配置:直接点击 layui 复选框的可见元素
async function configureAlgorithmsSuccess(page) {
await page.waitForSelector('.layui-form-checkbox', { timeout: 5000 });
await page.waitForTimeout(300);
const checkboxCount = await page.locator('.layui-form-checkbox').count();
console.log(` 算法复选框数量: checkboxCount`);
for (let i = 0; i < Math.min(checkboxCount, 8); i++) {
await page.locator('.layui-form-checkbox').nth(i).click();
await page.waitForTimeout(80);
}
const priorities = [
'ea[NEA0]', 'ea[128-NEA1]', 'ea[128-NEA2]', 'ea[128-NEA3]',
'ia[NIA0]', 'ia[128-NIA1]', 'ia[128-NIA2]', 'ia[128-NIA3]'
];
const vals = ['1', '2', '3', '4', '1', '2', '3', '4'];
for (let i = 0; i < priorities.length; i++) {
const inp = page.locator(`input[name="priorities[i]"]`);
if (await inp.count() > 0) {
await inp.fill(vals[i]);
}
}
console.log(` ✅ 算法配置完成`);
}
// 工程选择(精确匹配,分页遍历)
async function selectProject(page, projectName, forceSwitch = true) {
if (!forceSwitch) {
console.log(` 🔧 保持当前工程(用户未指定工程)`);
return true;
}
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', timeout: 15000 });
await page.waitForSelector('.jsgrid-row, .jsgrid-alt-row', { timeout: 5000 }).catch(() => {});
await page.evaluate(() => {
const inputs = document.querySelectorAll('input[type="text"], input[name="name"]');
for (const inp of inputs) { inp.value = ''; }
});
await page.waitForTimeout(300);
for (let pageNum = 1; pageNum <= 200; pageNum++) {
const clicked = await page.evaluate((targetName) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === targetName) {
const icon = cells[1].querySelector('.iconfont');
if (icon) {
icon.click();
return true;
}
}
}
return false;
}, projectName);
if (clicked) {
await page.waitForTimeout(2000);
return true;
}
const nextBtn = page.locator('.jsgrid-pager a:has-text("Next")');
if (!(await nextBtn.count())) break;
try {
REPLACED
} catch (e) {
break;
}
}
console.log(` ❌ 未找到工程 "projectName"(精确匹配)`);
return false;
}
// 添加 AMF 主流程
async function addAmf(amfName, projectName, explicitProject = true, amfConfig = {}) {
const startTime = Date.now();
const sessionManager = new SessionManager();
const defaultConfig = {
mcc: '460', mnc: '01', region_id: '1', set_id: '1', pointer: '1',
ngap_sip: '200.20.20.1', ngap_port: '38412',
http2_sip: '200.20.20.5', http2_port: '8080',
stac: '101', etac: '102'
};
const cfg = { ...defaultConfig, ...amfConfig };
let browser = null;
try {
browser = await chromium.launch({ headless: true, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
let context = await sessionManager.loadSession(browser);
let needLogin = true;
if (context) {
const testPage = await context.newPage();
await testPage.goto(`globalBaseUrlCONFIG.urls.amfManagement`, { waitUntil: 'networkidle', timeout: 10000 }).catch(() => {});
if (!testPage.url().includes('/login')) {
needLogin = false;
}
await testPage.close();
}
if (needLogin) {
context = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
}
const page = await context.newPage();
if (needLogin) {
await page.goto(`globalBaseUrlCONFIG.urls.login`, { waitUntil: 'networkidle', timeout: 15000 });
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill(CONFIG.credentials.email);
await page.getByRole('textbox', { name: '密码' }).fill(CONFIG.credentials.password);
await page.getByRole('button', { name: '登录' }).click();
await page.waitForLoadState('networkidle', { timeout: 10000 });
await sessionManager.saveSession(context);
}
// 选择工程(仅当用户显式指定工程时才切换)
if (!(await selectProject(page, projectName, explicitProject))) {
throw new Error(`工程 "projectName" 不存在或无法选中`);
}
// 进入编辑页面
await page.goto(`globalBaseUrlCONFIG.urls.amfEdit`, { waitUntil: 'networkidle', timeout: 15000 });
if (!page.url().includes('/amf/edit')) {
await page.goto(`globalBaseUrl/sim_5gc/amf/edit`);
await page.waitForSelector('input[name="name"]', { timeout: 10000 });
}
// 通过 evaluate 直接填写表单
await page.evaluate(({ amfName, cfg }) => {
const set = (name, value) => {
const el = document.querySelector(`input[name="name"]`);
if (el) {
el.value = value;
el.dispatchEvent(new Event('input', { bubbles: true }));
}
};
set('name', amfName);
set('mcc', cfg.mcc);
set('mnc', cfg.mnc);
set('region_id', cfg.region_id);
set('set_id', cfg.set_id);
set('pointer', cfg.pointer);
set('ngap_sip', cfg.ngap_sip);
set('ngap_port', cfg.ngap_port);
set('http2_sip', cfg.http2_sip);
set('http2_port', cfg.http2_port);
set('stac', cfg.stac);
set('etac', cfg.etac);
}, { amfName, cfg });
// 类型选择:仿真设备
await page.locator('.layui-unselect').first().click();
await page.waitForTimeout(300);
await page.locator('dd').filter({ hasText: '仿真设备' }).click();
// 配置算法
await configureAlgorithmsSuccess(page);
// 配置 NSSAI
await page.getByRole('row', { name: /数量.*nssai/ }).getByRole('button').click();
await page.waitForTimeout(500);
await page.locator('input[name="config[count][]"]').fill('1');
await page.getByRole('row', { name: /nssai.*添加.*删除/ }).locator('span').click();
await page.waitForTimeout(800);
const iframeEl = page.locator('iframe[name="layui-layer-iframe2"]');
const iframe = await iframeEl.contentFrame({ timeout: 5000 });
await iframe.getByRole('row', { name: /\*.*SST.*SD/ }).getByRole('button').click();
await iframe.locator('input[name="nssai[snssai_sst][]"]').fill('1');
await iframe.locator('input[name="nssai[snssai_sd][]"]').fill('111111');
await iframe.getByRole('button', { name: '提交' }).click();
await page.waitForTimeout(800);
// 提交表单
await page.getByRole('button', { name: '提交' }).click();
// 等待页面跳转到 AMF 列表页面,若未跳转则强制跳转
try {
await page.waitForURL(`**/amf/index`, { timeout: 8000 });
} catch (e) {
await page.goto(`globalBaseUrlCONFIG.urls.amfManagement`, { waitUntil: 'networkidle', timeout: 15000 });
}
await page.waitForTimeout(2000);
// 验证结果:只要页面成功跳转到 AMF 列表页,即认为添加成功
let found = false;
const finalUrl = page.url();
if (finalUrl.includes('/amf/index')) {
console.log(` ✅ 页面已跳转至 AMF 列表: finalUrl`);
found = true;
}
await browser.close();
const totalTime = (Date.now() - startTime) / 1000;
if (found) {
return { success: true, amfName, totalTime };
} else {
return { success: false, amfName, totalTime };
}
} catch (err) {
if (browser) await browser.close();
throw err;
}
}
// 主函数
async function main() {
const args = process.argv.slice(2);
if (args.length === 0) {
console.log('用法: node amf-add-skill.js <AMF名称> [--project <工程名>] [--url <地址>] [--mcc 460] [...]');
process.exit(1);
}
let amfName = null;
let projectName = '5G_basic_process';
let amfConfig = {};
let explicitProject = false;
for (let i = 0; i < args.length; i++) {
if (!args[i].startsWith('-')) {
amfName = args[i];
} else if (args[i] === '--project' || args[i] === '-p') {
projectName = args[++i];
explicitProject = true;
} else if (args[i] === '--url') {
let u = args[++i];
if (u && !u.startsWith('http')) u = 'https://' + u;
globalBaseUrl = u;
} else if (args[i].startsWith('--')) {
amfConfig[args[i].substring(2)] = args[++i];
}
}
if (!amfName) {
console.error('错误: 请指定 AMF 名称');
process.exit(1);
}
console.log(`AMF: amfName | 工程: projectName | 地址: globalBaseUrl`);
try {
const result = await addAmf(amfName, projectName, explicitProject, amfConfig);
console.log(result.success
? `成功! AMF "result.amfName" 添加完成 (result.totalTime.toFixed(2)s)`
: `失败! 未找到 AMF "result.amfName"`);
process.exit(result.success ? 0 : 1);
} catch (err) {
console.error(`执行异常: err.message`);
process.exit(1);
}
}
main();
FILE:scripts/default-rule-add-skill.js
/**
* default-rule-add-skill.js - PCF 默认规则一键添加工具
*
* 完整链路(一次性完成):
* 1. 创建 QoS 模板(自动选5qi)
* 2. 创建 Traffic Control(ENABLED)
* 3. 创建 PCC 规则(绑定 qos + tc)
* 4. 创建/更新 sm_policy_default(绑定 pcc)
* 5. PCF default_smpolicy → sm_policy_default
*
* 用法:
* node default-rule-add-skill.js --project XW_SUPF_5_1_2_4 --headed
* node default-rule-add-skill.js --project XW_SUPF_5_1_2_4 --qos-id qos1 --tc-id tc1 --pcc-id pcc_default --headed
*
* 参数(均有默认值,可全部省略):
* --project 工程名(默认 XW_S5GC_1)
* --pcf-name PCF实例名称(必填,如 qqq)
* --qos-id QoS模板ID(默认自动生成 qos_default_{timestamp})
* --5qi 5QI值(不指定则自动选择未使用的值)
* --maxbr-ul 上行最大比特率(默认 10000000)
* --maxbr-dl 下行最大比特率(默认 20000000)
* --gbr-ul 上行保证比特率(默认 5000000)
* --gbr-dl 下行保证比特率(默认 5000000)
* --tc-id TC规则ID(默认自动生成 tc_default_{timestamp})
* --flow-status TC流状态(默认 ENABLED)
* --pcc-id PCC规则ID(默认 pcc_default)
* --precedence PCC优先级(默认 63)
* --headed 显示浏览器窗口
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
function parseArgs() {
const args = process.argv.slice(2);
const ts = Date.now();
const opts = {
project: 'XW_S5GC_1',
pcfName: null, // null = 使用 pccId 作为 PCF 名称(向后兼容)
// QoS 参数
qosId: null, // null = 自动生成
qi: null,
maxbrUl: '10000000',
maxbrDl: '20000000',
gbrUl: '5000000',
gbrDl: '5000000',
// TC 参数
tcId: null, // null = 自动生成
flowStatus: 'ENABLED',
// PCC 参数
pccId: null, // null = 自动生成
precedence: '63',
// PCF 参数(网元名称)
pcfName: null, // 若未提供则使用 pccId 作为默认名称
headed: false,
};
for (let i = 0; i < args.length; i++) {
if (args[i] === '--project' || args[i] === '-p') opts.project = args[++i];
else if (args[i] === '--qos-id') opts.qosId = args[++i];
else if (args[i] === '--5qi') opts.qi = args[++i];
else if (args[i] === '--maxbr-ul') opts.maxbrUl = args[++i];
else if (args[i] === '--maxbr-dl') opts.maxbrDl = args[++i];
else if (args[i] === '--gbr-ul') opts.gbrUl = args[++i];
else if (args[i] === '--gbr-dl') opts.gbrDl = args[++i];
else if (args[i] === '--tc-id') opts.tcId = args[++i];
else if (args[i] === '--flow-status') opts.flowStatus = args[++i];
else if (args[i] === '--pcc-id') opts.pccId = args[++i];
else if (args[i] === '--precedence') opts.precedence = args[++i];
else if (args[i] === '--pcf-name') opts.pcfName = args[++i];
else if (args[i] === '--headed') opts.headed = true;
}
// 自动生成ID(如果未指定)
if (!opts.qosId) opts.qosId = `qos_default_ts`;
if (!opts.tcId) opts.tcId = `tc_default_ts`;
if (!opts.pccId) opts.pccId = `pcc_default`;
// 如果未提供 PCF 名称,默认使用 PCC ID(与业务保持一致)
if (!opts.pcfName) opts.pcfName = opts.pccId;
return opts;
}
// ─── 通用工具 ────────────────────────────────────────────────────────────
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, name) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.locator('input[name="project_search_name"]').fill(name);
await page.keyboard.press('Enter');
await page.waitForTimeout(3000);
const found = await page.evaluate((n) => {
let clicked = false;
document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === n) {
cells[1].querySelector('.iconfont')?.click();
clicked = true;
}
});
return clicked;
}, name);
if (!found) { console.error(`❌ 未找到工程: name`); process.exit(1); }
await page.waitForTimeout(3000);
console.log(`✅ 工程 "name" 已选`);
}
async function goto(page, url) {
await page.goto(`globalBaseUrlurl`, { waitUntil: 'load', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
}
// ─── Step 1: 创建 QoS 模板 ──────────────────────────────────────────────
async function getUsedQis(page) {
await goto(page, '/sim_5gc/predfPolicy/qos/index');
return await page.evaluate(() => {
const qis = [];
document.querySelectorAll('.layui-table tbody tr').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 4 && cells[3].textContent.trim()) {
qis.push(parseInt(cells[3].textContent.trim()));
}
});
return qis;
});
}
async function addQos(page, opts) {
// 自动选 5qi(先获取已用列表)
if (!opts.qi) {
await goto(page, '/sim_5gc/predfPolicy/qos/index');
const used = await page.evaluate(() => {
const qis = [];
document.querySelectorAll('.layui-table tbody tr').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 4 && cells[3].textContent.trim()) {
qis.push(parseInt(cells[3].textContent.trim()));
}
});
return qis;
});
const candidates = [8, 9, 6, 5, 7, 4, 3, 2, 1];
for (const c of candidates) { if (!used.includes(c)) { opts.qi = String(c); break; } }
if (!opts.qi) opts.qi = String(used[0] + 1);
console.log(` i️ 已用5qi: used.join(','),自动选择 opts.qi`);
}
await goto(page, '/sim_5gc/predfPolicy/qos/index');
await page.locator('button:has-text("添加")').click();
await page.waitForTimeout(3000);
await page.waitForSelector('iframe[name="layui-layer-iframe2"]', { timeout: 10000 });
const frame = page.frame('layui-layer-iframe2');
await frame.waitForLoadState('domcontentloaded');
await page.waitForTimeout(500);
await frame.locator('input[name="qosId"]').fill(opts.qosId);
await frame.locator('input[name="5qi"]').fill(opts.qi);
await frame.locator('input[name="maxbrUl"]').fill(opts.maxbrUl);
await frame.locator('input[name="maxbrDl"]').fill(opts.maxbrDl);
await frame.locator('input[name="gbrUl"]').fill(opts.gbrUl);
await frame.locator('input[name="gbrDl"]').fill(opts.gbrDl);
await frame.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log(` ✅ QoS模板 opts.qosId 已创建 (5qi=opts.qi)`);
}
// ─── Step 2: 创建 TC ────────────────────────────────────────────────────
async function addTc(page, opts) {
await goto(page, '/sim_5gc/predfPolicy/trafficCtl/index');
await page.locator('button:has-text("添加")').click();
await page.waitForTimeout(3000);
// 等待 iframe 出现在 DOM 中
await page.waitForSelector('iframe[name="layui-layer-iframe2"]', { timeout: 10000 });
const frame = page.frame('layui-layer-iframe2');
await frame.waitForLoadState('domcontentloaded');
// 等待 tcId input 出现
await frame.waitForSelector('input[name="tcId"]', { timeout: 10000 });
await page.waitForTimeout(500);
await frame.locator('input[name="tcId"]').fill(opts.tcId);
// 等待 select[name="flowStatus"] 出现在 DOM 中
const sel = frame.locator('select[name="flowStatus"]');
try {
await sel.waitFor({ state: 'attached', timeout: 5000 });
await sel.selectOption(opts.flowStatus, { force: true });
console.log(` flowStatus = opts.flowStatus`);
} catch(e) {
// 如果 select 不存在(如没有 flowStatus 字段),跳过
console.log(` i️ flowStatus select 不存在,跳过`);
}
await frame.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log(` ✅ TC opts.tcId 已创建 (flowStatus=opts.flowStatus)`);
}
// ─── Step 3: 创建 PCC ────────────────────────────────────────────────────
async function addPcc(page, opts) {
await goto(page, '/sim_5gc/predfPolicy/pcc/index');
// 检查是否已存在同名 PCC,存在则先删除
const existingId = await page.evaluate((targetId) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 10 && cells[2].textContent.trim() === targetId) {
return cells[1].textContent.trim(); // 返回数字ID
}
}
return null;
}, opts.pccId);
if (existingId) {
// 删除旧记录
await page.evaluate((id) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 10 && cells[1].textContent.trim() === id) {
const links = cells[9].querySelectorAll('a');
for (const l of links) { if (l.textContent.trim() === '删除') { l.click(); return; } }
}
}
}, existingId);
await page.waitForTimeout(1500);
// 处理删除确认对话框
const confirmBtn = page.locator('.layui-layer-dialog .layui-layer-btn0, .layui-layer-btn a:first-child');
if (await confirmBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
await confirmBtn.click();
await page.waitForTimeout(2000);
}
// 确保遮罩关闭
await page.keyboard.press('Escape');
await page.waitForTimeout(1000);
console.log(` 🗑️ 已删除旧 PCC opts.pccId,准备重建`);
}
await page.locator('button:has-text("添加")').click();
await page.waitForTimeout(3000);
await page.waitForFunction(() => window.location.href.includes('/predfPolicy/pcc/edit'), { timeout: 10000 });
await page.waitForTimeout(3000);
await page.locator('input[name="pccRuleId"]').fill(opts.pccId);
await page.locator('input[name="precedence"]').fill(opts.precedence);
// xm-select[0] = refQosData
await page.evaluate(() => document.querySelectorAll('input.xm-select-default')[0].parentElement.click());
await page.waitForTimeout(1000);
const qosOpt = page.locator('.xm-option.show-icon', { hasText: opts.qosId });
if (await qosOpt.isVisible({ timeout: 3000 }).catch(() => false)) await qosOpt.click();
await page.waitForTimeout(500);
// xm-select[1] = refTcData
await page.evaluate(() => document.querySelectorAll('input.xm-select-default')[1].parentElement.click());
await page.waitForTimeout(1000);
const tcOpt = page.locator('.xm-option.show-icon', { hasText: opts.tcId });
if (await tcOpt.isVisible({ timeout: 3000 }).catch(() => false)) await tcOpt.click();
await page.waitForTimeout(500);
await page.keyboard.press('Escape');
await page.waitForTimeout(300);
await page.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log(` ✅ PCC规则 opts.pccId 已创建 (refQosData=opts.qosId, refTcData=opts.tcId)`);
}
// ─── Step 4: 创建/更新 sm_policy_default(使用正确的 form_data 格式)────────────
async function addOrUpdateSmpolicy(page, pccId) {
console.log(`\n=== Step 4: 创建/更新 sm_policy_default (pccRules=pccId) ===`);
// 1. 进入 PCF,选中 pccId 行,点击 smpolicy 按钮
await goto(page, '/sim_5gc/pcf/index');
await page.waitForTimeout(3000);
await page.evaluate((targetName) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === targetName) {
const cb = row.querySelector('input[type="checkbox"]');
if (cb) cb.click();
}
}
}, pccId);
await page.waitForTimeout(500);
await page.locator('button:has-text("smpolicy")').click({ force: true });
await page.waitForTimeout(3000);
console.log(' smpolicy 页面 URL:', page.url());
// 2. 获取 CSRF token(从页面)
const token = await page.evaluate(() => document.querySelector('input[name="_token"]')?.value || '');
if (!token) {
console.error(' ❌ 未找到 _token');
return false;
}
console.log(' _token: ...' + token.substring(0, 10) + '...');
// 3. 检查是否已有 sm_policy_default
const existing = await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === 'sm_policy_default') {
return { id: cells[1].textContent.trim(), pccRules: cells[4].textContent.trim() };
}
}
return null;
});
if (!existing) {
console.log(' ℹ️ sm_policy_default 不存在,正在创建...');
await page.locator('button:has-text("添加")').click({ force: true });
await page.waitForTimeout(3000);
await page.waitForSelector('iframe[name="layui-layer-iframe2"]', { timeout: 15000 });
const frm = page.frame('layui-layer-iframe2');
await frm.waitForLoadState('domcontentloaded');
await page.waitForTimeout(1000);
// 构造正确的 form_data JSON
const formDataJson = JSON.stringify({
name: 'sm_policy_default',
pccRules: [pccId],
reflectiveQoSTimer: 86400
});
const params = new URLSearchParams();
params.append('_token', token);
params.append('form_data', formDataJson);
console.log(' form_data:', formDataJson);
const resp = await frm.evaluate(async (args) => {
const { tok, bodyStr } = args;
try {
const r = await fetch('/sim_5gc/smpolicy/default/edit', {
method: 'POST',
body: bodyStr,
credentials: 'include',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'x-csrf-token': tok,
'x-requested-with': 'XMLHttpRequest'
}
});
const text = await r.text();
return { status: r.status, body: text };
} catch(e) {
return { error: e.message };
}
}, { tok: token, bodyStr: params.toString() });
console.log(' 创建响应:', resp.status);
if (resp.status >= 400) {
try { console.log(' 错误:', JSON.stringify(JSON.parse(resp.body))); }
catch { console.log(' 响应:', resp.body?.substring(0, 200)); }
return false;
} else {
console.log(' ✅ 创建成功!响应:', resp.body?.substring(0, 200));
return true;
}
} else {
console.log(' ✅ sm_policy_default 已存在 (id=' + existing.id + ', pccRules=' + existing.pccRules + ')');
if (existing.pccRules.includes(pccId)) {
console.log(' ✅ pccRules 已包含 ' + pccId);
return true;
} else {
console.log(' ℹ️ 更新 sm_policy_default,添加 pccRules=' + pccId + '...');
// 点击编辑
await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === 'sm_policy_default') {
const links = row.querySelectorAll('a');
for (const l of links) { if (l.textContent.trim() === '编辑') { l.click(); return; } }
}
}
});
await page.waitForTimeout(3000);
await page.waitForSelector('iframe[name="layui-layer-iframe2"]', { timeout: 15000 });
const frm = page.frame('layui-layer-iframe2');
await frm.waitForLoadState('domcontentloaded');
await page.waitForTimeout(1000);
// 获取当前 pccRules
const currentPcc = await frm.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
return inputs.length > 1 ? inputs[1].value : '';
});
const existingRules = currentPcc ? currentPcc.split(',').filter(Boolean) : [];
if (!existingRules.includes(pccId)) existingRules.push(pccId);
const recId = await frm.evaluate(() => {
const el = document.querySelector('input[name="id"]');
return el ? el.value : '';
});
// 更新用的 form_data
const formDataJson = JSON.stringify({
name: 'sm_policy_default',
pccRules: existingRules,
reflectiveQoSTimer: 86400,
id: recId
});
const params = new URLSearchParams();
params.append('_token', token);
params.append('form_data', formDataJson);
const resp = await frm.evaluate(async (args) => {
const { tok, bodyStr, recId } = args;
try {
const r = await fetch('/sim_5gc/smpolicy/default/edit/' + recId, {
method: 'POST',
body: bodyStr,
credentials: 'include',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'x-csrf-token': tok,
'x-requested-with': 'XMLHttpRequest'
}
});
const text = await r.text();
return { status: r.status, body: text };
} catch(e) {
return { error: e.message };
}
}, { tok: token, bodyStr: params.toString(), recId });
console.log(' 更新响应:', resp.status);
if (resp.status >= 400) {
try { console.log(' 错误:', JSON.stringify(JSON.parse(resp.body))); }
catch { console.log(' 响应:', resp.body?.substring(0, 200)); }
return false;
} else {
console.log(' ✅ 更新成功!响应:', resp.body?.substring(0, 200));
return true;
}
}
}
}// ─── Step 5: PCF default_smpolicy ────────────────────────────────────────
// 正确流程(根据 UI 调试结果):
// 1. 在 PCF 列表先选中 qqq 行(单击,不要点编辑)
// 2. 再点击工具栏 "smpolicy" 按钮 → 页面加载 sm_policy_default 表单(带 qqq 上下文)
// 3. 创建 sm_policy_default(此时 name 应为 qqq 关联的默认策略名)
// 4. 保存后返回 PCF 编辑弹窗 → default_smpolicy 下拉有数据 → 选择 → 提交
async function setPcfDefaultSmpolicy(page, pcfName) {
console.log(`\n=== Step 5: 配置 PCF "pcfName" default_smpolicy ===`);
// 1. 进入 PCF 列表,点击指定 PCF 的编辑按钮
await goto(page, '/sim_5gc/pcf/index');
await page.waitForTimeout(3000);
const clicked = await page.evaluate((targetName) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === targetName) {
const links = row.querySelectorAll('a');
for (const l of links) { if (l.textContent.trim() === '编辑') { l.click(); return true; } }
}
}
return false;
}, pcfName);
if (!clicked) {
console.error(` ❌ 未找到 PCF "pcfName" 的编辑按钮`);
return false;
}
await page.waitForTimeout(3000);
await page.waitForSelector('iframe[name="layui-layer-iframe2"]', { timeout: 15000 });
const frm = page.frame('layui-layer-iframe2');
await frm.waitForLoadState('domcontentloaded');
await page.waitForTimeout(1000);
// 2. 获取 token 和 PCF ID
const token = await frm.evaluate(() => {
const meta = document.querySelector('meta[name="csrf-token"]');
return meta ? meta.getAttribute('content') : '';
});
if (!token) {
console.error(' ❌ 未找到 CSRF token (meta[name="csrf-token"])');
return false;
}
console.log(` Token: ...token.substring(0, 10)... (from meta tag)`);
const pcfId = await frm.evaluate(() => document.querySelector('input[name="id"]')?.value || '');
if (!pcfId) {
console.error(' ❌ 未找到 PCF ID');
return false;
}
console.log(` PCF ID: pcfId`);
// 3. 获取当前表单数据
const formData = await frm.evaluate(() => {
const form = document.querySelector('form');
if (!form) return {};
const data = new FormData(form);
const entries = {};
data.forEach((v, k) => { entries[k] = v; });
return entries;
});
// 4. 获取 sm_policy_default 的 ID - 通过主页面,不关闭弹窗
console.log(' 获取 sm_policy_default 的 ID...');
// 在主页面(不是 iframe)中打开新标签页查看 smpolicy 列表
const smpId = await page.evaluate(async () => {
// 在新窗口中打开 smpolicy 页面
const newWindow = window.open('/sim_5gc/smpolicy/default/index', '_blank');
if (!newWindow) return '';
// 等待新窗口加载
await new Promise(resolve => setTimeout(resolve, 2000));
// 从新窗口获取数据
const rows = newWindow.document.querySelectorAll('.layui-table tbody tr');
let foundId = '';
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === 'sm_policy_default') {
foundId = cells[1].textContent.trim();
break;
}
}
// 关闭新窗口
newWindow.close();
return foundId;
});
if (!smpId) {
console.error(' ❌ 未找到 sm_policy_default 的 ID');
// 尝试使用已知的 ID(如果之前创建过)
console.log(' ℹ️ 尝试使用默认 ID 9771');
return '9771'; // 返回默认 ID,让调用者决定
}
console.log(` sm_policy_default ID: smpId`);
// 5. 构造更新数据 - 设置 default_smpolicy 为 sm_policy_default 的 ID
const updateData = {
...formData,
'assoc_smpolicy[default_smpolicy]': smpId, // 使用 ID 而不是名称
'_token': token
};
// 移除空值(除了 select 字段)
Object.keys(updateData).forEach(key => {
if (updateData[key] === '' && !key.includes('select') && key !== 'assoc_smpolicy[default_smpolicy]') {
delete updateData[key];
}
});
// 6. 发送 POST 请求
const params = new URLSearchParams();
Object.entries(updateData).forEach(([k, v]) => {
params.append(k, v);
});
console.log(` 提交数据: assoc_smpolicy[default_smpolicy]=updateData['assoc_smpolicy[default_smpolicy]']`);
const resp = await frm.evaluate(async (args) => {
const { pcfId, bodyStr, token } = args;
try {
const r = await fetch(`/sim_5gc/pcf/edit/pcfId`, {
method: 'POST',
body: bodyStr,
credentials: 'include',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'x-csrf-token': token,
'x-requested-with': 'XMLHttpRequest'
}
});
const text = await r.text();
return { status: r.status, body: text };
} catch(e) {
return { error: e.message };
}
}, { pcfId, bodyStr: params.toString(), token });
console.log(` 响应状态: resp.status`);
if (resp.status >= 400) {
try { console.log(` 错误: JSON.stringify(JSON.parse(resp.body))`); }
catch { console.log(` 响应: resp.body?.substring(0, 200)`); }
return false;
} else {
console.log(` ✅ PCF "pcfName" default_smpolicy 设置成功!响应: resp.body?.substring(0, 100)`);
return true;
}
}async function verify(page, opts) {
await goto(page, '/sim_5gc/predfPolicy/pcc/index');
const pcc = await page.evaluate((id) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 8 && cells[2].textContent.trim() === id) {
return { pccRuleId: cells[2].textContent.trim(), precedence: cells[4].textContent.trim(), refQosData: cells[5].textContent.trim(), refTcData: cells[6].textContent.trim() };
}
}
return null;
}, opts.pccId);
await goto(page, '/sim_5gc/smpolicy/default/index');
const smp = await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 6 && cells[2].textContent.trim() === 'sm_policy_default') return { pccRules: cells[4].textContent.trim() };
}
return null;
});
await goto(page, '/sim_5gc/pcf/index');
await page.waitForTimeout(3000);
// 点击指定的 PCF 编辑按钮
const pcfName = opts.pcfName || opts.pccId;
const clicked = await page.evaluate((targetName) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === targetName) {
const links = row.querySelectorAll('a');
for (const l of links) { if (l.textContent.trim() === '编辑') { l.click(); return true; } }
}
}
return false;
}, pcfName);
if (!clicked) {
console.log(' ⚠️ 未找到 PCF "' + pcfName + '" 的编辑按钮,使用第一个 PCF');
await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
if (rows.length > 0) rows[0].querySelector('a')?.click();
});
}
await page.waitForTimeout(3000); const frame = page.frame('layui-layer-iframe2');
const pcfSmp = frame ? await frame.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
return inputs[0]?.parentElement?.textContent?.match(/[\w_]+/g)?.[0] || '';
}) : '';
console.log('\n========================================');
console.log('验证结果');
console.log('========================================');
const tests = [
{ name: `PCC opts.pccId 存在`, pass: !!pcc },
{ name: `refQosData = opts.qosId`, pass: pcc?.refQosData === opts.qosId },
{ name: `refTcData = opts.tcId`, pass: pcc?.refTcData === opts.tcId },
{ name: `sm_policy_default 包含 opts.pccId`, pass: smp?.pccRules?.includes(opts.pccId) },
{ name: `PCF default_smpolicy = sm_policy_default`, pass: pcfSmp === 'sm_policy_default' },
];
for (const t of tests) console.log(` '❌' t.name`);
if (pcc) console.log(`\n PCC: pccRuleId=pcc.pccRuleId, precedence=pcc.precedence, refQosData=pcc.refQosData, refTcData=pcc.refTcData`);
if (smp) console.log(` smp: pccRules=[smp.pccRules]`);
console.log('========================================');
return tests.every(t => t.pass);
}
// ─── 主流程 ─────────────────────────────────────────────────────────────
async function main() {
const opts = parseArgs();
console.log('\n========================================');
console.log('PCF 默认规则一键配置');
console.log(`工程: opts.project`);
console.log(`QoS: opts.qosId (5qi=opts.qi || '自动')`);
console.log(`TC: opts.tcId (flowStatus=opts.flowStatus)`);
console.log(`PCF: opts.pcfName || opts.pccId`);
console.log(`PCC: opts.pccId (precedence=opts.precedence)`);
console.log('========================================\n');
const browser = await chromium.launch({ headless: !opts.headed, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, opts.project);
console.log('📦 Step 1: 创建 QoS 模板...');
await addQos(page, opts);
console.log('📦 Step 2: 创建 Traffic Control...');
await addTc(page, opts);
console.log('📦 Step 3: 创建 PCC 规则...');
await addPcc(page, opts);
console.log('📦 Step 4: 更新 sm_policy_default...');
await addOrUpdateSmpolicy(page, opts.pccId);
console.log('📦 Step 5: 配置 PCF default_smpolicy...');
const pcfName = opts.pcfName || opts.pccId; // 向后兼容:如果没有指定 pcf-name,使用 pcc-id
await setPcfDefaultSmpolicy(page, pcfName);
console.log('\n📦 验证...');
const ok = await verify(page, opts);
console.log(ok ? '\n🎉 全部完成!' : '\n⚠️ 部分步骤存在问题,请检查');
await browser.close();
process.exit(ok ? 0 : 1);
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/nrf-add-skill.js
/**
* NRF 添加脚本
* 完整流程:登录 → 选工程 → 进NRF列表 → 点添加(弹窗iframe) → 填表单 → 提交
* 用法: node nrf-add-skill.js <名称> [--project <工程>] [--url <地址>] [--headed] \
* [--http2_sip <IP>] [--http2_port <端口>] [--MCC <值>] [--MNC <值>]
* 示例: node nrf-add-skill.js NRF-TEST --project XW_S5GC_1
*/
const { chromium } = require('playwright');
const path = require('path');
const fs = require('fs');
const BASE_URL = 'https://192.168.3.89';
const SESSION_DIR = path.join(__dirname, '.sessions');
function getSessionFile(baseUrl) {
const host = baseUrl.replace(/https?:\/\//, '').replace(/\./g, '_');
return `5gc_session_host.json`;
}
async function login(page, baseUrl) {
const sessionPath = path.join(SESSION_DIR, getSessionFile(baseUrl));
if (fs.existsSync(sessionPath)) {
try {
const storageState = JSON.parse(fs.readFileSync(sessionPath, 'utf8'));
if (storageState.cookies) {
await page.context().addCookies(storageState.cookies);
await page.goto(baseUrl + '/sim_5gc/project/index', { waitUntil: 'networkidle', timeout: 8000 }).catch(() => {});
if (!page.url().includes('/login')) {
console.log(' ✅ 使用缓存会话');
return true;
}
}
} catch {}
}
await page.goto(baseUrl + '/login', { waitUntil: 'networkidle', timeout: 15000 });
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForLoadState('networkidle');
const ctx = page.context();
const storageState = await ctx.storageState();
fs.writeFileSync(sessionPath, JSON.stringify({ cookies: storageState.cookies }, null, 2));
console.log(' ✅ 登录成功');
return true;
}
async function selectProject(page, projectName) {
await page.goto(BASE_URL + '/sim_5gc/project/index', { waitUntil: 'networkidle', timeout: 15000 });
await page.waitForSelector('.jsgrid-row, .jsgrid-alt-row', { timeout: 5000 }).catch(() => {});
await page.waitForTimeout(300);
for (let pageNum = 1; pageNum <= 200; pageNum++) {
const clicked = await page.evaluate((targetName) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === targetName) {
const icon = cells[1].querySelector('.iconfont');
if (icon) { icon.click(); return true; }
}
}
return false;
}, projectName);
if (clicked) { await page.waitForTimeout(2000); return true; }
const nextBtn = page.locator('.jsgrid-pager a:has-text("Next")');
if (!(await nextBtn.count())) break;
try { await nextBtn.click(); } catch { break; }
await page.waitForTimeout(1500);
}
console.log(` ❌ 未找到工程 "projectName"`);
return false;
}
async function main() {
const args = process.argv.slice(2);
if (args.length < 1) {
console.log('用法: node nrf-add-skill.js <名称> [--project <工程>] [--url <地址>] [--headed]');
console.log(' [--http2_sip <IP>] [--http2_port <端口>] [--MCC <值>] [--MNC <值>]');
console.log('示例: node nrf-add-skill.js NRF-TEST --project XW_S5GC_1');
process.exit(1);
}
const name = args[0];
let headless = true;
let project = 'XW_S5GC_1';
let http2_sip = '192.168.20.100';
let http2_port = '80';
let mcc = '460';
let mnc = '01';
for (let i = 1; i < args.length; i++) {
if (args[i] === '--headed') headless = false;
else if (args[i] === '--project') project = args[++i];
else if (args[i] === '--url') BASE_URL = args[++i];
else if (args[i] === '--http2_sip') http2_sip = args[++i];
else if (args[i] === '--http2_port') http2_port = args[++i];
else if (args[i] === '--MCC') mcc = args[++i];
else if (args[i] === '--MNC') mnc = args[++i];
}
console.log(`▶ 添加 NRF: name`);
console.log(` http2_sip=http2_sip http2_port=http2_port MCC=mcc MNC=mnc`);
console.log(` 工程: project`);
const browser = await chromium.launch({ headless, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page, BASE_URL);
const ok = await selectProject(page, project);
if (!ok) throw new Error('工程选择失败');
console.log(' ✓ 工程已选');
// 进 NRF 列表(先点"核心网"菜单,再点"NRF")
await page.evaluate(() => {
const links = document.querySelectorAll('a[href*="/nrf/"]');
for (const l of links) {
if (l.textContent.trim().includes('NRF')) { l.click(); return; }
}
});
await page.waitForTimeout(3000);
console.log(' ✓ 进入NRF列表,URL:', page.url());
// 点添加按钮
await page.waitForSelector('button:has-text("添加")', { timeout: 10000 }).catch(() => {});
await page.locator('button:has-text("添加")').first().click();
await page.waitForTimeout(2000);
console.log(' ✓ 点添加(弹窗)');
// 切换到弹窗 iframe
await page.locator('iframe[name="layui-layer-iframe2"]').waitFor({ timeout: 5000 });
const frame = page.frame('layui-layer-iframe2');
if (!frame) throw new Error('未找到弹窗 iframe');
await frame.waitForLoadState('domcontentloaded');
console.log(' ✓ 切换到弹窗iframe');
// 名称
await frame.locator('input[name="name"]').fill(name);
console.log(` ✓ name = name`);
// 类型下拉
await frame.getByRole('textbox', { name: '请选择' }).first().click();
await frame.getByRole('definition').filter({ hasText: '仿真设备' }).click();
await page.waitForTimeout(500);
console.log(' ✓ 类型 = 仿真设备');
// MCC
await frame.getByRole('textbox', { name: '三位数字', exact: true }).fill(mcc);
console.log(` ✓ MCC = mcc`);
// MNC
await frame.getByRole('textbox', { name: '二位或三位数字' }).fill(mnc);
console.log(` ✓ MNC = mnc`);
// HTTP2 SIP
await frame.locator('input[name="http2_sip"]').fill(http2_sip);
console.log(` ✓ http2_sip = http2_sip`);
// HTTP2 PORT
await frame.locator('input[name="http2_port"]').fill(http2_port);
console.log(` ✓ http2_port = http2_port`);
// 提交
await frame.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log(' ✓ 已提交');
const url = page.url();
if (url.includes('/nrf/index')) {
console.log(` ✅ 添加成功,URL: url`);
} else {
console.log(` ⚠️ 可能未保存,URL: url`);
}
await browser.close();
}
main().catch(e => { console.error('❌', e.message); process.exit(1); });
FILE:scripts/pcc-add-skill.js
/**
* pcc-add-skill.js - PCC规则添加工具(修复版)
*
* 用法:
* node pcc-add-skill.js --project XW_SUPF_5_1_2_4 --pcc-id pcc_new --qos qos2 --tc tc1 [--precedence 63] [--headed]
*
* 参数:
* --project 工程名(默认 XW_S5GC_1)
* --pcc-id 新PCC规则ID(必填,字母/数字/下划线)
* --precedence 优先级(默认 63,用户指定时用指定值)
* --qos QoS模板名称(必填,如 qos1 / qos2)
* --tc 流量控制名称(必填,如 tc1)
* --flow-desc 流描述(可选)
* --headed 显示浏览器窗口
*
* 完整链路:
* 点击"添加" → 主框架跳转 /predfPolicy/pcc/edit
* → 填写 pccRuleId + precedence
* → xm-select 选 qos(第0个)+ tc(第1个)+ 可选chg(第2个)
* → 提交 → 返回列表页
*
* xm-select 交互(Playwright locator):
* 1. JS: inputs[idx].parentElement.click() 打开下拉
* 2. Playwright locator: page.locator('.xm-option.show-icon', {hasText}).click() 选择选项
* 3. page.keyboard.press('Escape') 关闭下拉
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
function parseArgs() {
const args = process.argv.slice(2);
const opts = {
project: 'XW_S5GC_1',
pccId: null,
precedence: null, // null = 使用默认值63
qos: null, // 必填
tc: null, // 必填
flowDesc: null,
headed: false,
};
for (let i = 0; i < args.length; i++) {
if (args[i] === '--project' || args[i] === '-p') opts.project = args[++i];
else if (args[i] === '--pcc-id') opts.pccId = args[++i];
else if (args[i] === '--precedence') opts.precedence = args[++i];
else if (args[i] === '--qos') opts.qos = args[++i];
else if (args[i] === '--tc') opts.tc = args[++i];
else if (args[i] === '--flow-desc') opts.flowDesc = args[++i];
else if (args[i] === '--headed') opts.headed = true;
}
if (!opts.pccId) {
console.error('❌ 缺少 --pcc-id 参数');
process.exit(1);
}
if (!opts.qos) {
console.error('❌ 缺少 --qos 参数(QoS模板名称)');
process.exit(1);
}
if (!opts.tc) {
console.error('❌ 缺少 --tc 参数(流量控制名称)');
process.exit(1);
}
return opts;
}
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, projectName) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
// 先尝试搜索工程名
await page.locator('input[name="project_search_name"]').fill(projectName);
await page.keyboard.press('Enter');
await page.waitForTimeout(3000);
const clicked = await page.evaluate((name) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === name) {
const icon = cells[1].querySelector('.iconfont');
if (icon) { icon.click(); return true; }
}
}
return false;
}, projectName);
if (!clicked) {
// 尝试逐页查找
for (let p = 1; p <= 100; p++) {
const found = await page.evaluate((name) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === name) {
cells[1].querySelector('.iconfont').click();
return true;
}
}
return false;
}, projectName);
if (found) break;
const hasNext = await page.evaluate(() => {
const links = document.querySelectorAll('.jsgrid-pager a');
for (const l of links) {
if (l.textContent.trim() === 'Next' && !l.classList.contains('jsgrid-pager-disabled')) return true;
}
return false;
});
if (!hasNext) break;
await page.evaluate(() => {
const links = document.querySelectorAll('.jsgrid-pager a');
for (const l of links) {
if (l.textContent.trim() === 'Next') { l.click(); return; }
}
});
await page.waitForTimeout(2000);
}
}
await page.waitForTimeout(3000);
console.log(`✅ 工程 "projectName" 已选`);
}
async function main() {
const opts = parseArgs();
const browser = await chromium.launch({ headless: !opts.headed, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, opts.project);
// 去 PCC 列表页
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/pcc/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
console.log(`✅ 到达PCC列表页`);
// 点击添加按钮
await page.locator('button:has-text("添加")').click();
await page.waitForTimeout(3000);
// 等待输入框出现(URL跳转完成)
await page.waitForFunction(() => window.location.href.includes('/predfPolicy/pcc/edit'), { timeout: 10000 });
await page.waitForTimeout(3000);
console.log(`✅ 到达添加页: page.url()`);
// 填写文本字段
const precedence = opts.precedence !== null ? String(opts.precedence) : '63';
await page.locator('input[name="pccRuleId"]').fill(opts.pccId);
await page.locator('input[name="precedence"]').fill(precedence);
console.log(` pccRuleId="opts.pccId", precedence="precedence"'(默认63)'`);
// ── xm-select[0] = refQosData ──────────────────────────────────
await page.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
if (inputs[0]) inputs[0].parentElement.click();
});
await page.waitForTimeout(1000);
const qosVisible = await page.locator('.xm-option.show-icon', { hasText: opts.qos }).isVisible({ timeout: 3000 }).catch(() => false);
if (qosVisible) {
await page.locator('.xm-option.show-icon', { hasText: opts.qos }).click();
console.log(` ✅ refQosData=opts.qos 已选`);
} else {
console.log(` ❌ refQosData=opts.qos 不可见`);
}
await page.waitForTimeout(500);
// ── xm-select[1] = refTcData ─────────────────────────────────
await page.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
if (inputs[1]) inputs[1].parentElement.click();
});
await page.waitForTimeout(1000);
const tcVisible = await page.locator('.xm-option.show-icon', { hasText: opts.tc }).isVisible({ timeout: 3000 }).catch(() => false);
if (tcVisible) {
await page.locator('.xm-option.show-icon', { hasText: opts.tc }).click();
console.log(` ✅ refTcData=opts.tc 已选`);
} else {
console.log(` ❌ refTcData=opts.tc 不可见`);
}
await page.waitForTimeout(500);
// ── xm-select[2] = refChgData(可选,如有则自动选第一个)────────
await page.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
if (inputs[2]) inputs[2].parentElement.click();
});
await page.waitForTimeout(1000);
const firstChg = page.locator('.xm-option.show-icon').first();
if (await firstChg.isVisible({ timeout: 2000 }).catch(() => false)) {
const txt = await firstChg.textContent();
await firstChg.click();
console.log(` ℹ️ refChgData=(txt.trim()) 已选`);
}
await page.waitForTimeout(500);
// 关闭 xm-select 下拉(按 Escape 避免遮罩拦截提交按钮)
await page.keyboard.press('Escape');
await page.waitForTimeout(300);
// 提交
await page.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log(`✅ PCC规则 opts.pccId 已提交`);
// 验证
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/pcc/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const pccData = await page.evaluate((targetId) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 8 && cells[2].textContent.trim() === targetId) {
return {
pccRuleId: cells[2].textContent.trim(),
precedence: cells[4].textContent.trim(),
refQosData: cells[5].textContent.trim(),
refTcData: cells[6].textContent.trim(),
};
}
}
return null;
}, opts.pccId);
if (pccData) {
console.log('\n📋 验证结果:');
console.log(` pccRuleId = pccData.pccRuleId`);
console.log(` precedence = pccData.precedence`);
console.log(` refQosData = pccData.refQosData '❌'`);
console.log(` refTcData = pccData.refTcData '❌'`);
}
console.log('\n✅ 完成');
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/pcf-add-skill.js
/**
* PCF/PCRF 添加脚本
* 完整流程:登录 → 选工程 → 进PCF列表 → 点添加(弹窗iframe) → 填表单 → 提交
* 用法: node pcf-add-skill.js <名称> [--project <工程>] [--url <地址>] [--headed] \
* [--http2_sip <IP>] [--http2_port <端口>] [--MCC <值>] [--MNC <值>]
* 示例: node pcf-add-skill.js PCF-TEST --project XW_S5GC_1
*/
const { chromium } = require('playwright');
const path = require('path');
const fs = require('fs');
const BASE_URL = 'https://192.168.3.89';
const SESSION_DIR = path.join(__dirname, '.sessions');
function getSessionFile(baseUrl) {
const host = baseUrl.replace(/https?:\/\//, '').replace(/\./g, '_');
return `5gc_session_host.json`;
}
async function login(page, baseUrl) {
const sessionPath = path.join(SESSION_DIR, getSessionFile(baseUrl));
if (fs.existsSync(sessionPath)) {
try {
const storageState = JSON.parse(fs.readFileSync(sessionPath, 'utf8'));
if (storageState.cookies) {
await page.context().addCookies(storageState.cookies);
await page.goto(baseUrl + '/sim_5gc/project/index', { waitUntil: 'networkidle', timeout: 8000 }).catch(() => {});
if (!page.url().includes('/login')) {
console.log(' ✅ 使用缓存会话');
return true;
}
}
} catch {}
}
await page.goto(baseUrl + '/login', { waitUntil: 'networkidle', timeout: 15000 });
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForLoadState('networkidle');
const ctx = page.context();
const storageState = await ctx.storageState();
fs.writeFileSync(sessionPath, JSON.stringify({ cookies: storageState.cookies }, null, 2));
console.log(' ✅ 登录成功');
return true;
}
async function selectProject(page, projectName) {
await page.goto(BASE_URL + '/sim_5gc/project/index', { waitUntil: 'networkidle', timeout: 15000 });
await page.waitForSelector('.jsgrid-row, .jsgrid-alt-row', { timeout: 5000 }).catch(() => {});
await page.waitForTimeout(300);
for (let pageNum = 1; pageNum <= 200; pageNum++) {
const clicked = await page.evaluate((targetName) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === targetName) {
const icon = cells[1].querySelector('.iconfont');
if (icon) { icon.click(); return true; }
}
}
return false;
}, projectName);
if (clicked) { await page.waitForTimeout(2000); return true; }
const nextBtn = page.locator('.jsgrid-pager a:has-text("Next")');
if (!(await nextBtn.count())) break;
try { await nextBtn.click(); } catch { break; }
await page.waitForTimeout(1500);
}
console.log(` ❌ 未找到工程 "projectName"(精确匹配)`);
return false;
}
async function main() {
const args = process.argv.slice(2);
if (args.length < 1) {
console.log('用法: node pcf-add-skill.js <名称> [--project <工程>] [--url <地址>] [--headed]');
console.log(' [--http2_sip <IP>] [--http2_port <端口>] [--MCC <值>] [--MNC <值>]');
console.log('示例: node pcf-add-skill.js PCF-TEST --project XW_S5GC_1');
process.exit(1);
}
const name = args[0];
let headless = true;
let project = 'XW_S5GC_1';
let http2_sip = '192.168.20.90';
let http2_port = '80';
let mcc = '460';
let mnc = '01';
for (let i = 1; i < args.length; i++) {
if (args[i] === '--headed') headless = false;
else if (args[i] === '--project') project = args[++i];
else if (args[i] === '--url') BASE_URL = args[++i];
else if (args[i] === '--http2_sip') http2_sip = args[++i];
else if (args[i] === '--http2_port') http2_port = args[++i];
else if (args[i] === '--MCC') mcc = args[++i];
else if (args[i] === '--MNC') mnc = args[++i];
}
console.log(`▶ 添加 PCF: name`);
console.log(` http2_sip=http2_sip http2_port=http2_port MCC=mcc MNC=mnc`);
console.log(` 工程: project`);
const browser = await chromium.launch({ headless, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page, BASE_URL);
const ok = await selectProject(page, project);
if (!ok) throw new Error('工程选择失败');
console.log(' ✓ 工程已选');
// 进 PCF/PCRF 列表(用 JS 点击 sidebar 链接,兼容折叠菜单)
await page.evaluate(() => {
const links = document.querySelectorAll('a[href*="/pcf/"]');
for (const l of links) {
if (l.textContent.trim().includes('PCF')) { l.click(); return; }
}
});
await page.waitForTimeout(3000);
console.log(' ✓ 进入PCF列表,URL:', page.url());
// 点添加按钮(弹窗)
await page.waitForSelector('button:has-text("添加")', { timeout: 10000 }).catch(() => {});
await page.locator('button:has-text("添加")').first().click();
await page.waitForTimeout(2000);
console.log(' ✓ 点添加(弹窗)');
// 切换到弹窗 iframe
await page.locator('iframe[name="layui-layer-iframe2"]').waitFor({ timeout: 5000 });
const frame = page.frame('layui-layer-iframe2');
if (!frame) throw new Error('未找到弹窗 iframe');
await frame.waitForLoadState('domcontentloaded');
console.log(' ✓ 切换到弹窗iframe');
// 填名称
await frame.locator('input[name="name"]').fill(name);
console.log(` ✓ name = name`);
// 类型下拉:点击"请选择"
await frame.getByRole('textbox', { name: '请选择' }).click();
await frame.getByRole('definition').filter({ hasText: '仿真设备' }).click();
await page.waitForTimeout(500);
console.log(' ✓ 类型 = 仿真设备');
// 数量
await frame.locator('input[name="count"]').fill('1');
console.log(' ✓ count = 1');
// HTTP2 SIP
await frame.locator('input[name="http2_sip"]').fill(http2_sip);
console.log(` ✓ http2_sip = http2_sip`);
// HTTP2 PORT
await frame.locator('input[name="http2_port"]').fill(http2_port);
console.log(` ✓ http2_port = http2_port`);
// MCC - label 为"三位数字"
await frame.getByRole('textbox', { name: '三位数字', exact: true }).fill(mcc);
console.log(` ✓ MCC = mcc`);
// MNC - label 为"二位或三位数字"
await frame.getByRole('textbox', { name: '二位或三位数字' }).fill(mnc);
console.log(` ✓ MNC = mnc`);
// 提交
await frame.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log(' ✓ 已提交');
const url = page.url();
if (url.includes('/pcf/index')) {
console.log(` ✅ 添加成功,URL: url`);
} else {
console.log(` ⚠️ 可能未保存,URL: url`);
}
await browser.close();
}
main().catch(e => { console.error('❌', e.message); process.exit(1); });
FILE:scripts/qos-add-skill.js
/**
* qos-add-skill.js - QoS模板添加工具
*
* 用法:
* node qos-add-skill.js --project XW_SUPF_5_1_2_4 --qos-id qos3 --maxbr-ul 10000000 --maxbr-dl 20000000 --gbr-ul 5000000 --gbr-dl 5000000 [--headed]
* node qos-add-skill.js --project XW_SUPF_5_1_2_4 --qos-id qos3 --5qi 8 --maxbr-ul 10000000 --maxbr-dl 20000000 --gbr-ul 5000000 --gbr-dl 5000000 [--headed]
*
* 参数:
* --project 工程名(默认 XW_S5GC_1)
* --qos-id QoS模板ID(必填)
* --5qi 5QI值(不指定则自动从已有5qi列表中选择一个不同的值)
* --maxbr-ul 上行最大比特率(不指定则用默认值)
* --maxbr-dl 下行最大比特率(不指定则用默认值)
* --gbr-ul 上行保证比特率(不指定则用默认值)
* --gbr-dl 下行保证比特率(不指定则用默认值)
* --priority 优先级(默认空)
* --headed 显示浏览器窗口
*
* 默认值(用户未指定时):
* maxbrUl=10000000, maxbrDl=20000000, gbrUl=5000000, gbrDl=5000000
* 5qi=自动选择(从已有5qi列表中挑一个不存在的值,优先8/9/6/5...)
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
function parseArgs() {
const args = process.argv.slice(2);
const opts = {
project: 'XW_S5GC_1',
qosId: null,
qi: null,
maxbrUl: null,
maxbrDl: null,
gbrUl: null,
gbrDl: null,
priority: '',
headed: false,
};
for (let i = 0; i < args.length; i++) {
if (args[i] === '--project' || args[i] === '-p') opts.project = args[++i];
else if (args[i] === '--qos-id') opts.qosId = args[++i];
else if (args[i] === '--5qi') opts.qi = args[++i];
else if (args[i] === '--maxbr-ul') opts.maxbrUl = args[++i];
else if (args[i] === '--maxbr-dl') opts.maxbrDl = args[++i];
else if (args[i] === '--gbr-ul') opts.gbrUl = args[++i];
else if (args[i] === '--gbr-dl') opts.gbrDl = args[++i];
else if (args[i] === '--priority') opts.priority = args[++i];
else if (args[i] === '--headed') opts.headed = true;
}
if (!opts.qosId) {
console.error('❌ 缺少必要参数: --qos-id');
console.error(' 示例: node qos-add-skill.js --project XW_SUPF_5_1_2_4 --qos-id qos3 --maxbr-ul 10000000 --maxbr-dl 20000000 --gbr-ul 5000000 --gbr-dl 5000000');
process.exit(1);
}
return opts;
}
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, projectName) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
const clicked = await page.evaluate((name) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === name) {
const icon = cells[1].querySelector('.iconfont');
if (icon) { icon.click(); return true; }
}
}
return false;
}, projectName);
if (!clicked) { console.log('❌ 未找到工程'); process.exit(1); }
await page.waitForTimeout(3000);
console.log(`✅ 工程 "projectName" 已选`);
}
async function getUsed5qis(page) {
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/qos/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const usedQis = await page.evaluate(() => {
const qis = new Set();
document.querySelectorAll('.layui-table tbody tr').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 4) {
const qi = parseInt(cells[3].textContent.trim());
if (!isNaN(qi)) qis.add(qi);
}
});
return [...qis];
});
return usedQis;
}
function autoSelect5qi(usedQis) {
const candidates = [8, 9, 6, 5, 4, 3, 2, 1];
for (const c of candidates) {
if (!usedQis.includes(c)) return c;
}
return 8;
}
const DEFAULT_BR = { maxbrUl: '10000000', maxbrDl: '20000000', gbrUl: '5000000', gbrDl: '5000000' };
async function main() {
const opts = parseArgs();
const browser = await chromium.launch({ headless: !opts.headed, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, opts.project);
// 自动确定5qi(用户未指定时)
if (opts.qi === null) {
console.log('\n📋 检测已有QoS模板的5qi...');
const usedQis = await getUsed5qis(page);
console.log(` 已使用5qi: usedQis.join(', ')`);
opts.qi = autoSelect5qi(usedQis);
console.log(` ✅ 自动选择 5qi = opts.qi(与已有不同)`);
} else {
console.log(`\n📋 用户指定 5qi = opts.qi`);
}
// 应用默认值
const params = {
qosId: opts.qosId,
qi: opts.qi,
maxbrUl: opts.maxbrUl || DEFAULT_BR.maxbrUl,
maxbrDl: opts.maxbrDl || DEFAULT_BR.maxbrDl,
gbrUl: opts.gbrUl || DEFAULT_BR.gbrUl,
gbrDl: opts.gbrDl || DEFAULT_BR.gbrDl,
};
console.log('\n📋 最终参数:');
console.log(` qosId = params.qosId`);
console.log(` 5qi = params.qi`);
console.log(` maxbrUl = params.maxbrUl`);
console.log(` maxbrDl = params.maxbrDl`);
console.log(` gbrUl = params.gbrUl`);
console.log(` gbrDl = params.gbrDl`);
// 去添加页
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/qos/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
await page.locator('button:has-text("添加")').click();
await page.waitForTimeout(3000);
const frame = page.frame('layui-layer-iframe2');
if (!frame) { console.error('❌ 未找到弹窗iframe'); process.exit(1); }
await page.waitForTimeout(1000);
// 填写字段(使用 first() 确保能获取到元素)
await frame.locator('input[name="qosId"]').first().fill(params.qosId);
await frame.locator('input[name="5qi"]').first().fill(params.qi);
await frame.locator('input[name="maxbrUl"]').first().fill(params.maxbrUl);
await frame.locator('input[name="maxbrDl"]').first().fill(params.maxbrDl);
await frame.locator('input[name="gbrUl"]').first().fill(params.gbrUl);
await frame.locator('input[name="gbrDl"]').first().fill(params.gbrDl);
// 提交
await frame.locator('button:has-text("提交")').first().click();
await page.waitForTimeout(3000);
// 验证
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/qos/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const qosData = await page.evaluate((targetId) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
for (let i = 0; i < cells.length; i++) {
if (cells[i].textContent.trim() === targetId) {
return {
id: cells[1].textContent.trim(),
qi: cells[3].textContent.trim(),
maxbrUl: cells[4].textContent.trim(),
maxbrDl: cells[5].textContent.trim(),
gbrUl: cells[6].textContent.trim(),
gbrDl: cells[7].textContent.trim(),
};
}
}
}
return null;
}, params.qosId);
if (qosData) {
console.log('\n📋 保存的QoS数据:');
console.log(` ID=qosData.id, 5qi=qosData.qi, maxbrUl=qosData.maxbrUl, maxbrDl=qosData.maxbrDl, gbrUl=qosData.gbrUl, gbrDl=qosData.gbrDl`);
const ok = qosData.qi === params.qi && qosData.maxbrUl === params.maxbrUl && qosData.maxbrDl === params.maxbrDl && qosData.gbrUl === params.gbrUl && qosData.gbrDl === params.gbrDl;
console.log(ok ? '\n✅ QoS模板创建成功!' : '\n⚠️ 部分数据可能未正确保存');
}
console.log('\n✅ 完成');
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/smpolicy-ue-add-skill.js
/**
* smpolicy-ue-add-skill.js - UE Smpolicy 添加工具
*
* 用法:
* node smpolicy-ue-add-skill.js --project XW_SUPF_5_1_2_4 --name ue_test --dnn internet
* node smpolicy-ue-add-skill.js --project XW_SUPF_5_1_2_4 --name ue_test --dnn internet --imsi 460001234567890
* node smpolicy-ue-add-skill.js --project XW_SUPF_5_1_2_4 --name ue_test --dnn internet --sst 1 --sd 111111 --pcc-rules pcc2
*
* 参数:
* --project 工程名(默认 XW_S5GC_1)
* --name UE策略名称(必填)
* --dnn DNN(必填)
* --imsi IMSI起始值(可选,不填则自动生成)
* --imsi-num IMSI数量(默认 1)
* --sst sNssai SST(默认 1)
* --sd sNssai SD(默认 111111)
* --sess-rules 会话规则名称(xm-select,多个逗号分隔)
* --pcc-rules PCC规则名称(xm-select,多个逗号分隔)
* --pra-rules PRA规则名称(xm-select,可选)
* --ref-qos-timer reflectiveQoSTimer 值(可选)
* --headed 显示浏览器窗口
*
* 添加页:/sim_5gc/smpolicy/ue/edit(layui-layer-iframe2)
* xm-select: sessRules=idx0, pccRules=idx1, praRules=idx2
*
* xm-select 交互:
* 1. frame.evaluate(() => inputs[idx].parentElement.click()) 打开下拉
* 2. frame.locator('.xm-option', {hasText}).click() 选择选项
* 3. page.keyboard.press('Escape') 关闭下拉
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
function parseArgs() {
const args = process.argv.slice(2);
const opts = {
project: 'XW_S5GC_1',
name: null,
dnn: null,
imsi: null,
imsiNum: '1',
sst: '1',
sd: '111111',
sessRules: null,
pccRules: null,
praRules: null,
refQosTimer: null,
headed: false,
};
for (let i = 0; i < args.length; i++) {
if (args[i] === '--project' || args[i] === '-p') opts.project = args[++i];
else if (args[i] === '--name') opts.name = args[++i];
else if (args[i] === '--dnn') opts.dnn = args[++i];
else if (args[i] === '--imsi') opts.imsi = args[++i];
else if (args[i] === '--imsi-num') opts.imsiNum = args[++i];
else if (args[i] === '--sst') opts.sst = args[++i];
else if (args[i] === '--sd') opts.sd = args[++i];
else if (args[i] === '--sess-rules') opts.sessRules = args[++i];
else if (args[i] === '--pcc-rules') opts.pccRules = args[++i];
else if (args[i] === '--pra-rules') opts.praRules = args[++i];
else if (args[i] === '--ref-qos-timer') opts.refQosTimer = args[++i];
else if (args[i] === '--headed') opts.headed = true;
}
if (!opts.name) { console.error('❌ 缺少 --name 参数'); process.exit(1); }
if (!opts.dnn) { console.error('❌ 缺少 --dnn 参数'); process.exit(1); }
return opts;
}
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, name) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.locator('input[name="project_search_name"]').fill(name);
await page.keyboard.press('Enter');
await page.waitForTimeout(3000);
const found = await page.evaluate((n) => {
let result = false;
document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === n) {
cells[1].querySelector('.iconfont')?.click();
result = true;
}
});
return result;
}, name);
if (!found) { console.error(`❌ 未找到工程: name`); process.exit(1); }
await page.waitForTimeout(3000);
console.log(`✅ 工程 "name" 已选`);
}
/**
* 选择 xm-select 中的一个选项(支持多选,同一选项点击可切换选中状态)
*/
async function xmSelectChooseOne(frame, page, index, value) {
if (!value) return;
// 打开下拉
await frame.evaluate((idx) => {
const inputs = document.querySelectorAll('input.xm-select-default');
if (inputs[idx]) inputs[idx].parentElement.click();
}, index);
await page.waitForTimeout(1000);
// 点击目标选项
const clicked = await frame.evaluate((text) => {
const opts = document.querySelectorAll('.xm-option');
for (const opt of opts) {
if (opt.textContent.trim() === text) {
opt.click();
return true;
}
}
return false;
}, value);
if (clicked) {
console.log(` ✅ xm-select[index] = value`);
} else {
console.log(` ⚠️ xm-select[index] 未找到选项: value`);
}
await page.waitForTimeout(500);
await page.keyboard.press('Escape');
await page.waitForTimeout(300);
}
/**
* 选择 xm-select 中的多个选项(逗号分隔)
*/
async function xmSelectChooseMultiple(frame, page, index, values) {
if (!values) return;
const items = values.split(',').map(s => s.trim()).filter(Boolean);
for (const item of items) {
await xmSelectChooseOne(frame, page, index, item);
}
}
async function main() {
const opts = parseArgs();
const browser = await chromium.launch({ headless: !opts.headed, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, opts.project);
// 导航到 UE smpolicy 列表页
await page.goto(`globalBaseUrl/sim_5gc/smpolicy/ue/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
console.log(`✅ 到达 UE Smpolicy 列表页`);
// 点击添加按钮
await page.locator('button:has-text("添加")').click();
await page.waitForTimeout(3000);
// 获取编辑帧
const frame = page.frame('layui-layer-iframe2');
if (!frame) { console.error('❌ 未找到弹窗iframe'); process.exit(1); }
await frame.waitForLoadState('domcontentloaded');
await page.waitForTimeout(2000);
console.log(`✅ 进入弹窗iframe: frame.url()`);
// ① 填写文本字段
// 自动生成 IMSI(如果未提供)
const autoImsi = opts.imsi || `4600Date.now().toString().slice(-10)`;
const textFields = [
{ name: 'name', value: opts.name },
{ name: 'dnn', value: opts.dnn },
{ name: 'imsi', value: autoImsi },
{ name: 'imsi_num', value: opts.imsiNum },
{ name: 'sNssai[sst]', value: opts.sst },
{ name: 'sNssai[sd]', value: opts.sd },
];
if (opts.refQosTimer) {
textFields.push({ name: 'smPolicyDecision[reflectiveQoSTimer]', value: opts.refQosTimer });
}
for (const f of textFields) {
const loc = frame.locator(`[name="f.name"]`).first();
if (await loc.count() > 0) {
await loc.fill(String(f.value));
console.log(` ✅ f.name = "f.value"`);
} else {
console.log(` ⚠️ 字段 f.name 不存在`);
}
}
// ② xm-select 选择(sessRules=idx0, pccRules=idx1, praRules=idx2)
// sessRules 通常无数据(暂无数据),有则选
const sessDisplay = await frame.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
return inputs[0]?.parentElement?.textContent || '';
});
if (!sessDisplay.includes('暂无数据')) {
await xmSelectChooseMultiple(frame, page, 0, opts.sessRules);
} else if (opts.sessRules) {
console.log(` ℹ️ sessRules 无可用数据,跳过`);
}
// pccRules
await xmSelectChooseMultiple(frame, page, 1, opts.pccRules);
// praRules 通常无数据
const praDisplay = await frame.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
return inputs[2]?.parentElement?.textContent || '';
});
if (!praDisplay.includes('暂无数据')) {
await xmSelectChooseMultiple(frame, page, 2, opts.praRules);
} else if (opts.praRules) {
console.log(` ℹ️ praRules 无可用数据,跳过`);
}
// ③ 提交
await frame.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log(`✅ 已提交`);
// ④ 验证:回到列表页检查
await page.goto(`globalBaseUrl/sim_5gc/smpolicy/ue/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const added = await page.evaluate((targetName) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 8 && cells[2].textContent.trim() === targetName) {
return {
name: cells[2].textContent.trim(),
dnn: cells[3].textContent.trim(),
sst: cells[4].textContent.trim(),
sd: cells[5].textContent.trim(),
sessRules: cells[6].textContent.trim(),
pccRules: cells[7].textContent.trim(),
};
}
}
return null;
}, opts.name);
if (added) {
console.log('\n📋 验证结果:');
console.log(` name = added.name '❌'`);
console.log(` dnn = added.dnn '❌'`);
console.log(` sst = added.sst`);
console.log(` sd = added.sd`);
console.log(` sessRules = added.sessRules`);
console.log(` pccRules = added.pccRules`);
if (opts.pccRules) {
const expectedPccs = opts.pccRules.split(',').map(s => s.trim());
const match = expectedPccs.every(p => added.pccRules.includes(p));
console.log(` pccRules 匹配: '⚠️'`);
}
} else {
console.log('\n❌ 未在列表中找到创建的 UE Smpolicy');
}
console.log('\n✅ 完成');
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/smpolicy_add_pcc.js
/**
* smpolicy_add_pcc.js - 将 PCC 规则添加到 sm_policy_default 的 pccRules
*
* 用法:
* node smpolicy_add_pcc.js --project XW_SUPF_5_1_2_4 --pcc-id pcc_new
*
* 参数:
* --project 工程名(默认 XW_SUPF_5_1_2_4)
* --pcc-id PCC规则ID(必填,需已存在)
* --headed 显示浏览器窗口
*
* 完整链路:
* smpolicy/default/index → 编辑 sm_policy_default 弹窗(iframe)
* → pccRules xm-select(第1个)中添加 --pcc-id
* → 提交
*
* xm-select 交互(Playwright locator):
* 1. JS: inputs[idx].parentElement.click() 打开下拉
* 2. frame.locator('.xm-option.show-icon', {hasText}).click() 选择选项
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
function parseArgs() {
const args = process.argv.slice(2);
const opts = {
project: 'XW_SUPF_5_1_2_4',
pccId: null,
headed: false,
};
for (let i = 0; i < args.length; i++) {
if (args[i] === '--project' || args[i] === '-p') opts.project = args[++i];
else if (args[i] === '--pcc-id') opts.pccId = args[++i];
else if (args[i] === '--headed') opts.headed = true;
}
if (!opts.pccId) {
console.error('❌ 缺少 --pcc-id 参数');
process.exit(1);
}
return opts;
}
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, projectName) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.locator('input[name="project_search_name"]').fill(projectName);
await page.keyboard.press('Enter');
await page.waitForTimeout(3000);
const clicked = await page.evaluate((name) => {
let result = false;
document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === name) {
cells[1].querySelector('.iconfont')?.click();
result = true;
}
});
return result;
}, projectName);
if (!clicked) { console.error(`❌ 未找到工程: projectName`); process.exit(1); }
await page.waitForTimeout(3000);
console.log(`✅ 工程 "projectName" 已选`);
}
async function main() {
const opts = parseArgs();
const browser = await chromium.launch({ headless: !opts.headed, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, opts.project);
// 导航到 smpolicy/default/index
await page.goto(`globalBaseUrl/sim_5gc/smpolicy/default/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
console.log('✅ 到达 smpolicy/default/index');
// 点击"编辑"按钮(第1行 sm_policy_default)
await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
if (rows.length > 0) {
const editBtn = rows[0].querySelector('a');
if (editBtn) editBtn.click();
}
});
await page.waitForTimeout(3000);
// 进入弹窗 iframe
const frame = page.frame('layui-layer-iframe2');
if (!frame) { console.error('❌ 未找到弹窗iframe'); process.exit(1); }
console.log('✅ 进入弹窗iframe');
// 检查当前 pccRules 的值(pccRules = xm-select[1])
const before = await frame.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
if (inputs.length >= 2) {
return {
pccRulesValue: inputs[1].value,
pccRulesDisplay: inputs[1].parentElement.textContent.substring(0, 80),
};
}
return null;
});
console.log('\n📋 编辑前状态:', JSON.stringify(before));
// 打开 pccRules 下拉(第1个xm-select)
console.log(`\n▶ 添加 opts.pccId 到 pccRules...`);
await frame.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
if (inputs[1]) inputs[1].parentElement.click();
});
await page.waitForTimeout(1000);
// 用 Playwright locator 点击选项
const optLocator = frame.locator('.xm-option.show-icon', { hasText: opts.pccId });
const visible = await optLocator.isVisible({ timeout: 3000 }).catch(() => false);
if (visible) {
await optLocator.click();
console.log(` ✅ 选择 opts.pccId`);
} else {
console.log(` ❌ 选项 opts.pccId 不可见`);
const availOpts = await frame.evaluate(() =>
Array.from(document.querySelectorAll('.xm-option.show-icon')).map(o => o.textContent.trim())
);
console.log(` 可用选项: availOpts.join(', ')`);
}
await page.waitForTimeout(500);
// 关闭 xm-select 下拉(按 Escape 避免遮罩层)
await page.keyboard.press('Escape');
await page.waitForTimeout(300);
// 提交
await frame.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log('✅ 已提交');
// 验证
await page.goto(`globalBaseUrl/sim_5gc/smpolicy/default/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const updated = await page.evaluate((targetPccId) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 6 && cells[2].textContent.trim() === 'sm_policy_default') {
return { pccRules: cells[4].textContent.trim() };
}
}
return null;
}, opts.pccId);
if (updated) {
console.log('\n📋 更新后 pccRules:', updated.pccRules);
console.log(updated.pccRules.includes(opts.pccId) ? `\n🎉 成功将 opts.pccId 添加到 pccRules!` : `\n⚠️ 未检测到 opts.pccId`);
}
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/tc-add-skill.js
/**
* tc-add-skill.js - Traffic Control 流量控制模板添加工具
*
* 用法:
* node tc-add-skill.js --project XW_SUPF_5_1_2_4 --tc-id tc_new --flow-status ENABLED-UPLINK [--headed]
*
* 参数:
* --project 工程名(默认 XW_S5GC_1)
* --tc-id TC模板ID(必填,字母/数字/下划线)
* --flow-status flowStatus(默认 ENABLED-UPLINK)
* 可选值:ENABLED-UPLINK, ENABLED-DOWNLINK, ENABLED, DISABLED, REMOVED
* --headed 显示浏览器窗口
*
* 完整链路:
* 点击"添加" → 弹窗 iframe(layui-layer-iframe2)→ 填写 tcId + flowStatus(SELECT)
* → 提交 → 返回列表页
*
* 注意事项:
* - flowStatus 是 SELECT 下拉框,用 JS 方式设置值(layui 隐藏原生select)
* - tcId 是必填字段
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
function parseArgs() {
const args = process.argv.slice(2);
const opts = {
project: 'XW_S5GC_1',
tcId: null,
flowStatus: 'ENABLED-UPLINK',
headed: false,
};
for (let i = 0; i < args.length; i++) {
if (args[i] === '--project' || args[i] === '-p') opts.project = args[++i];
else if (args[i] === '--tc-id') opts.tcId = args[++i];
else if (args[i] === '--flow-status') opts.flowStatus = args[++i];
else if (args[i] === '--headed') opts.headed = true;
}
if (!opts.tcId) {
console.error('❌ 缺少必要参数: --tc-id');
console.error(' 示例: node tc-add-skill.js --project XW_SUPF_5_1_2_4 --tc-id tc_new --flow-status ENABLED-UPLINK');
process.exit(1);
}
return opts;
}
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000, waitUntil: 'domcontentloaded' });
await page.waitForTimeout(2000);
try { await page.locator('input[name="email"]').first().waitFor({ state: 'visible', timeout: 5000 }); } catch(e) {}
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, projectName) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.locator('input[name="project_search_name"]').fill(projectName);
await page.keyboard.press('Enter');
await page.waitForTimeout(3000);
const clicked = await page.evaluate((name) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === name) {
cells[1].querySelector('.iconfont').click();
return true;
}
}
return false;
}, projectName);
if (!clicked) { console.error(`❌ 未找到工程: projectName`); process.exit(1); }
await page.waitForTimeout(3000);
console.log(`✅ 工程 "projectName" 已选`);
}
async function main() {
const opts = parseArgs();
const browser = await chromium.launch({ headless: !opts.headed, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, opts.project);
// 去 TC 列表页
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/trafficCtl/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
console.log(`✅ 到达TC列表页`);
// 点击添加按钮
await page.locator('button:has-text("添加")').click();
await page.waitForTimeout(5000);
// 获取弹窗 iframe(layui-layer-iframe2)
const frame = page.frame('layui-layer-iframe2');
if (!frame) { console.error('❌ 未找到弹窗iframe'); process.exit(1); }
console.log(`✅ 进入弹窗iframe`);
// 填写 tcId
await frame.locator('input[name="tcId"]').fill(opts.tcId);
console.log(` tcId="opts.tcId"`);
// 设置 flowStatus(用 JS 方式,因为 layui 隐藏了原生 select)
await frame.evaluate((status) => {
const sel = document.querySelector('select[name="flowStatus"]');
if (sel) { sel.value = status; sel.dispatchEvent(new Event('change', { bubbles: true })); }
}, opts.flowStatus);
console.log(` flowStatus="opts.flowStatus"`);
// 提交
await frame.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log(`✅ TC模板 opts.tcId 已提交`);
// 验证
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/trafficCtl/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const tcData = await page.evaluate((targetId) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 4 && cells[2].textContent.trim() === targetId) {
return { tcId: cells[2].textContent.trim(), flowStatus: cells[3].textContent.trim() };
}
}
return null;
}, opts.tcId);
if (tcData) {
console.log('\n📋 验证结果:');
console.log(` tcId = tcData.tcId`);
console.log(` flowStatus = tcData.flowStatus '❌'`);
} else {
console.log('\n❌ TC模板未找到');
}
console.log('\n✅ 完成');
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });5GC Web仪表自动化技能,支持AMF/UDM/AUSF/SMF/PGW-C/UPF/PGW-U/GNB/UE/PCF/NRF/QoS/TC/PCC/smpolicy的批量添加与编辑及PCF默认规则一键配置
---
name: 5gc-web-dotouch
version: 1.0.0
description: 5GC Web仪表自动化技能,支持AMF/UDM/AUSF/SMF/PGW-C/UPF/PGW-U/GNB/UE/PCF/NRF/QoS/TC/PCC/smpolicy的批量添加与编辑及PCF默认规则一键配置
author: liuwei120
tags: [5gc, automation, playwright, network]
---
# 5GC Web 仪表自动化技能
> 统一管理 AMF、UDM/AUSF、SMF/PGW-C、UPF/PGW-U、GNB、UE、PCF、NRF 八类网元的添加与编辑操作,以及 PCC 规则、QoS 模板、Traffic Control、SMPolicy 和 PCF 默认规则一键配置。
---
## 目录
- [快速开始](#快速开始)
- [统一 CLI 入口](#统一-cli-入口)
- [技能详细文档](#技能详细文档)
- [AMF](#amf)
- [UDM/AUSF](#udmausf)
- [SMF/PGW-C](#smfpgw-c)
- [UPF/PGW-U](#upfpgw-u)
- [GNB](#gnb)
- [UE](#ue)
- [PCF/PCRF](#pcfpcrf)
- [PCC 规则](#pcc-规则)
- [QoS 模板](#qos-模板)
- [Traffic Control](#traffic-control)
- [SMPolicy](#smpolicy)
- [UE Smpolicy](#smpolicy-ue-add-skilljs)
- [DNN Smpolicy](#smpolicy-dnn)
- [DNN Smpolicy](#smpolicy-dnn)
- [TAC Smpolicy](#smpolicy-tac)
- [Cell Smpolicy](#smpolicy-cell)
- [Cell Forbidden Smpolicy](#smpolicy-cell-forbidden)
- [NRF](#nrf)
- [全局参数参考](#全局参数参考)
- [字段参考](#字段参考)
---
## 快速开始
### 安装方法
技能目录位于 `skills/5gc/`,由统一入口 `5gc.js` 统一调度,无需额外安装:
```bash
# 克隆或复制到本机
git clone <repo> ~/.openclaw/workspace/skills/5gc
# 直接使用统一入口(推荐)
node skills/5gc/scripts/5gc.js <entity> <action> [options]
# 或直接调用各脚本
node skills/5gc/scripts/amf-add-skill.js <参数>
```
### 前置要求
- Node.js ≥ 14
- Playwright(`npm i playwright && npx playwright install chromium`)
- 5GC 仪表地址:`https://192.168.3.89`(默认)
- 登录凭证:`[email protected]` / `dotouch`
- 仪表上已创建对应工程(如 `XW_S5GC_1`)
### 会话缓存
所有脚本自动复用 Playwright 会话缓存(`.sessions/` 目录),首次登录后再次运行无需重复登录。
---
## 统一 CLI 入口
### 路径
```
node skills/5gc/scripts/5gc.js <entity> <action> [options]
```
### 支持的网元与操作
| entity | add | edit | 特殊操作 |
|--------|-----|------|---------|
| `amf` | ✅ | ✅ | |
| `udm` | ✅ | ✅ | |
| `smf` | ✅ | ✅ | |
| `upf` | ✅ | ✅ | |
| `gnb` | ✅ | ✅ | |
| `ue` | ✅ | ✅ | |
| `pcf` | ✅ | ✅ | `default-rule-add` |
| `pcc` | ✅ | ✅ | |
| `qos` | ✅ | | |
| `tc` | ✅ | | |
| `smpolicy` | | | `add-pcc`, `ue-add`, `ue-edit`, `dnn-add`, `dnn-edit` |
| `nrf` | ✅ | ✅ | |
### 全局选项
| 选项 | 说明 |
|------|------|
| `--url <地址>` | 5GC 仪表地址,默认 `https://192.168.3.89` |
| `--headed` | 打开可见浏览器窗口(调试用) |
### 三种使用模式
```bash
# 1. 添加网元
node 5gc.js amf add <名称> [参数...]
# 2. 批量编辑(当前工程下所有该类网元)
node 5gc.js amf edit --project <工程> --set-<字段> <值>
# 3. 单个编辑(按名称精确匹配)
node 5gc.js amf edit --name <名称> --project <工程> --set-<字段> <值>
```
---
## 技能详细文档
---
### AMF
#### amf-add-skill.js
**功能**:在指定工程下添加一个 AMF 实例。
**使用方式**:
```bash
node 5gc.js amf add <名称> [选项...]
# 或直接调用
node skills/5gc/scripts/amf-add-skill.js <名称> [选项...]
```
**参数**:
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `<名称>` | AMF 实例名称(位置参数) | **必填** |
| `--project <工程>` / `-p <工程>` | 目标工程名称 | `5G_basic_process` |
| `--url <地址>` | 5GC 仪表地址 | `https://192.168.3.89` |
| `--mcc <值>` | MCC(移动国家码) | `460` |
| `--mnc <值>` | MNC(移动网络码) | `01` |
| `--ngap_sip <IP>` | NGAP 信令面 IP | `200.20.20.1` |
| `--ngap_port <端口>` | NGAP 端口 | `38412` |
| `--http2_sip <IP>` | HTTP2 服务 IP | `200.20.20.5` |
| `--http2_port <端口>` | HTTP2 端口 | `8080` |
| `--stac <值>` | 起始 TAC | `101` |
| `--etac <值>` | 结束 TAC | `102` |
| `--region_id <值>` | 区域 ID | `1` |
| `--set_id <值>` | Set ID | `1` |
| `--pointer <值>` | 指针 | `1` |
| `--headed` | 打开可见浏览器 | false |
**示例**:
```bash
# 基本添加
node 5gc.js amf add AMF_TEST --project XW_S5GC_1
# 指定 NGAP IP 和端口
node 5gc.js amf add AMF_PROD --project XW_S5GC_1 --ngap_sip 10.200.1.50 --ngap_port 38412
# 使用不同 MCC/MNC
node 5gc.js amf add AMF_CMCC --project XW_S5GC_1 --mcc 460 --mnc 00
```
---
#### amf-edit-skill.js
**功能**:修改 AMF 配置参数。支持单个修改或批量修改工程下所有 AMF。
**使用方式**:
```bash
node 5gc.js amf edit [选项...]
```
**参数**:
| 参数 | 说明 |
|------|------|
| `--project <工程>` / `-p <工程>` | 目标工程,不带 `--name` 时批量修改该工程下所有 AMF |
| `--name <名称>` | 精确匹配要修改的 AMF 名称 |
| `--id <ID>` | 按 AMF ID 修改 |
| `--set-<字段> <值>` | 修改指定字段的值,支持多组 |
| `--url <地址>` | 5GC 仪表地址 |
| `--headed` | 打开可见浏览器 |
**可编辑字段**:`name`, `mcc`, `mnc`, `ngap_sip`, `ngap_port`, `http2_sip`, `http2_port`, `stac`, `etac`, `region_id`, `set_id`, `pointer`, `ea[NEA0]`, `ea[128-NEA1]`, `ea[128-NEA2]`, `ea[128-NEA3]`, `ia[NIA0]`, `ia[128-NIA1]`, `ia[128-NIA2]`, `ia[128-NIA3]`
> ⚠️ `ea[NEA0]` 等算法字段:实际向表单填入字段名 `ea[NEA0]`(input[name="ea[NEA0]"]),layui checkbox 点击基于索引而非字段名,详情见 SKILL.md 算法配置章节。
**示例**:
```bash
# 批量修改工程下所有 AMF 的 NGAP IP
node 5gc.js amf edit --project XW_S5GC_1 --set-ngap_sip 10.200.1.99
# 修改指定 AMF
node 5gc.js amf edit --name AMF_TEST --project XW_S5GC_1 --set-ngap_sip 10.200.1.50 --set-http2_sip 10.200.1.51
# 按 ID 修改
node 5gc.js amf edit --id 6633 --set-ngap_port 38413
```
---
### UDM/AUSF
#### ausf-udm-add-skill.js
**功能**:在指定工程下添加一个 UDM/AUSF 实例。
**使用方式**:
```bash
node 5gc.js udm add <名称> [选项...]
```
**参数**:
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `<名称>` | UDM 实例名称(位置参数) | **必填** |
| `--project <工程>` / `-p <工程>` | 目标工程名称 | `5G_basic_process` |
| `--url <地址>` | 5GC 仪表地址 | `https://192.168.3.89` |
| `--count <数量>` | 实例数量 | `1` |
| `--sip <IP>` | SIP 服务 IP | `192.168.20.30` |
| `--port <端口>` | SIP 端口 | `80` |
| `--auth_method <方法>` | 认证方法 | `5G_AKA` |
| `--scheme <协议>` | 协议类型 | `HTTP` |
| `--priority <优先级>` | 优先级 | `8` |
| `--headed` | 打开可见浏览器 | false |
**示例**:
```bash
# 基本添加
node 5gc.js udm add UDM_TEST --project XW_S5GC_1
# 指定 SIP IP 和端口
node 5gc.js udm add UDM_PROD --project XW_S5GC_1 --sip 10.0.0.100 --port 8080
# 批量添加 3 个实例
node 5gc.js udm add UDM_CLUSTER --project XW_S5GC_1 --count 3 --sip 10.0.0.50
```
---
#### ausf-udm-edit-skill.js
**功能**:修改 UDM/AUSF 配置参数。支持批量和单个修改。
**使用方式**:
```bash
node 5gc.js udm edit [选项...]
```
**参数**:
| 参数 | 说明 |
|------|------|
| `--project <工程>` | 目标工程,不带 `--name` 时批量修改 |
| `--name <名称>` | 精确匹配要修改的 UDM 名称 |
| `--set-sip <IP>` | 修改 SIP IP |
| `--set-port <端口>` | 修改端口 |
| `--set-auth_method <方法>` | 修改认证方法 |
| `--set-scheme <协议>` | 修改协议 |
| `--set-count <数量>` | 修改实例数量 |
| `--url <地址>` | 5GC 仪表地址 |
| `--headed` | 打开可见浏览器 |
**示例**:
```bash
# 批量修改工程下所有 UDM 的 SIP IP
node 5gc.js udm edit --project XW_S5GC_1 --set-sip 10.0.0.99
# 修改指定 UDM
node 5gc.js udm edit --name UDM_TEST --project XW_S5GC_1 --set-sip 10.0.0.88 --set-port 8080
```
---
### SMF/PGW-C
#### smf-pgwc-add-skill.js
**功能**:在指定工程下添加一个 SMF/PGW-C 实例。
**使用方式**:
```bash
node 5gc.js smf add --name <名称> [选项...]
```
> ⚠️ 通过 5gc.js 统一调度时必须使用 `--name <名称>` 形式(不是位置参数)。
**参数**:
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `--name <名称>` | SMF 实例名称 | **必填** |
| `--project <工程>` / `-p <工程>` | 目标工程名称 | `XW_S5GC_1` |
| `--url <地址>` | 5GC 仪表地址 | `https://192.168.3.89` |
| `--pfcp_sip <IP>` | PFCP 信令面 IP | `200.20.20.25` |
| `--http2_sip <IP>` | HTTP2 服务 IP | `200.20.20.25` |
| `--mcc <值>` | MCC | `460` |
| `--mnc <值>` | MNC | `01` |
| `--pdu_capacity <数量>` | PDU 会话容量 | `200000` |
| `--ue_min <IP>` | UE IP 池起始 | `30.30.30.20` |
| `--ue_max <IP>` | UE IP 池结束 | `30.31.30.20` |
| `--interest_tac <TAC列表>` | 关注 TAC 列表(逗号分隔) | `101,102` |
| `--headed` | 打开可见浏览器 | false |
> ✅ **NSSAI 自动配置**:脚本在 SMF 创建后会自动打开 NSSAI 配置弹窗,添加一条默认 NSSAI(SST=1, SD=000001, DNN Group=cscn2net)。如需自定义 NSSAI 参数,请直接修改脚本中的硬编码值。
>
> ⚠️ ue_sip6 / ue_eip6 为硬编码值,不支持 CLI 参数覆盖。
**示例**:
```bash
# 基本添加
node 5gc.js smf add --name SMF_TEST --project XW_S5GC_1
# 指定工程和 IP/MCC
node 5gc.js smf add --name SMF_PROD --project XW_S5GC_1 --pfcp_sip 10.10.10.50 --http2_sip 10.10.10.51 --mcc 460 --mnc 01
```
---
#### smf-pgwc-edit-skill.js
**功能**:修改 SMF/PGW-C 配置参数。支持批量和单个修改。
**使用方式**:
```bash
node 5gc.js smf edit [选项...]
```
**参数**:
| 参数 | 说明 |
|------|------|
| `--project <工程>` | 目标工程,不带 `--name` 时批量修改 |
| `--name <名称>` | 精确匹配要修改的 SMF 名称 |
| `--set-pfcp_sip <IP>` | 修改 PFCP 信令面 IP |
| `--set-http2_sip <IP>` | 修改 HTTP2 服务 IP |
| `--set-mcc <值>` | 修改 MCC |
| `--set-mnc <值>` | 修改 MNC |
| `--set-pdu_capacity <数量>` | 修改 PDU 会话容量 |
| `--set-ue_min <IP>` | 修改 UE IP 池起始 |
| `--set-ue_max <IP>` | 修改 UE IP 池结束 |
| `--set-interest_tac <TAC列表>` | 修改关注 TAC 列表(逗号分隔) |
> ⚠️ 以下字段不支持 `--set-` 修改:dnn、n3_ip、n6_ip、snssai_sst、snssai_sd。如需修改,请通过仪表 UI 手动完成。NSSAI 配置请在添加时自动完成(见上文)。
**示例**:
```bash
# 批量修改工程下所有 SMF 的 HTTP2 IP
node 5gc.js smf edit --project XW_S5GC_1 --set-http2_sip 10.10.10.99
# 修改指定 SMF 的 pfcp_sip 和 MCC/MNC
node 5gc.js smf edit --name SMF_TEST --project XW_S5GC_1 --set-pfcp_sip 10.10.10.88 --set-mcc 460 --set-mnc 01
```
---
### UPF/PGW-U
#### upf-add-skill.js
**功能**:在指定工程下添加一个 UPF/PGW-U 实例。
**使用方式**:
```bash
node 5gc.js upf add <名称> [选项...]
```
**参数**:
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `<名称>` | UPF 实例名称(位置参数) | **必填** |
| `--project <工程>` / `-p <工程>` | 目标工程名称 | `XW_S5GC_1` |
| `--url <地址>` | 5GC 仪表地址 | `https://192.168.3.89` |
| `--n4_ip <IP>` | N4 接口 IP | `192.168.20.30` |
| `--n3_ip <IP>` | N3 接口 IP | `192.168.20.30` |
| `--n6_ip <IP>` | N6 接口 IP | `192.168.20.31` |
| `--n4_port <端口>` | N4 端口 | `8805` |
| `--MCC <值>` | MCC(注意大写) | `460` |
| `--MNC <值>` | MNC(注意大写) | `01` |
| `--pdu_capacity <数量>` | PDU 会话容量 | `20000` |
| `--ue_min <IP>` | UE IP 池起始 | `20.20.20.20` |
| `--ue_max <IP>` | UE IP 池结束 | `20.20.60.20` |
| `--headed` | 打开可见浏览器 | false |
> ⚠️ DNN、TAC、NSSAI 在添加脚本中为硬编码默认值,不支持命令行覆盖。如需修改,请使用 `upf edit` 脚本。
**示例**:
```bash
# 基本添加
node 5gc.js upf add UPF_TEST --project XW_S5GC_1
# 指定 N4/N3/N6 IP 和 MCC/MNC
node 5gc.js upf add UPF_PROD --project XW_S5GC_1 --n4_ip 10.0.0.50 --n6_ip 10.0.0.51 --MCC 460 --MNC 01
```
---
#### upf-edit-skill.js
**功能**:修改 UPF/PGW-U 配置参数。支持批量和单个修改。
**使用方式**:
```bash
node 5gc.js upf edit [选项...]
```
**参数**:
| 参数 | 说明 |
|------|------|
| `--project <工程>` | 目标工程,不带 `--name` 时批量修改 |
| `--name <名称>` | 精确匹配要修改的 UPF 名称 |
| `--set-n3_ip <IP>` | 修改 N3 接口 IP |
| `--set-n4_ip <IP>` | 修改 N4 接口 IP |
| `--set-n4_port <端口>` | 修改 N4 端口 |
| `--set-n6_ip <IP>` | 修改 N6 接口 IP |
| `--set-MCC <值>` | 修改 MCC(注意大写) |
| `--set-MNC <值>` | 修改 MNC(注意大写) |
| `--set-pdu_capacity <数量>` | 修改 PDU 会话容量 |
| `--set-ue_min <IP>` | 修改 UE IP 池起始 |
| `--set-ue_max <IP>` | 修改 UE IP 池结束 |
| `--url <地址>` | 5GC 仪表地址 |
| `--headed` | 打开可见浏览器 |
> ⚠️ `dnn`(DNN)和 TAC/NSSAI 在 UPF 表单中存储在 jsgrid 配置行内,不支持简单的 `--set-` 修改。
**示例**:
```bash
# 批量修改工程下所有 UPF 的 N4 IP
node 5gc.js upf edit --project XW_S5GC_1 --set-n4_ip 99.99.99.99
# 修改指定 UPF 的 N4/N6 IP 和 MCC/MNC
node 5gc.js upf edit --name UPF_TEST --project XW_S5GC_1 --set-n4_ip 88.88.88.88 --set-n6_ip 88.88.88.89 --set-MCC 460 --set-MNC 01
```
---
### GNB
#### gnb-add-skill.js
**功能**:在指定工程下添加一个 GNB 实例。
**使用方式**:
```bash
node 5gc.js gnb add <名称> [选项...]
```
**参数**:
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `<名称>` | GNB 实例名称(位置参数) | **必填** |
| `--project <工程>` / `-p <工程>` | 目标工程名称 | `XW_S5GC_1` |
| `--url <地址>` | 5GC 仪表地址 | `https://192.168.3.89` |
| `--ngap_sip <IP>` | NGAP 信令面 IP | `200.20.20.50` |
| `--user_sip_ip_v4 <IP>` | 用户面 IPv4 | `2.2.2.2` |
| `--mcc <值>` | MCC | `460` |
| `--mnc <值>` | MNC | `60` |
| `--stac <值>` | 起始 TAC | `0` |
| `--etac <值>` | 结束 TAC | `0` |
| `--node_id <ID>` | 节点 ID | `70` |
| `--cell_count <数量>` | 小区数量 | `1` |
| `--headed` | 打开可见浏览器 | false |
> ⚠️ `stac`/`etac`/`node_id` 非默认值时可能触发表单验证失败,建议先使用默认值添加后再用 `gnb edit` 修改。
**示例**:
```bash
# 基本添加
node 5gc.js gnb add GNB_TEST --project XW_S5GC_1
# 指定 NGAP IP、用户面 IP 和 TAC
node 5gc.js gnb add GNB_PROD --project XW_S5GC_1 --ngap_sip 200.20.20.100 --user_sip_ip_v4 3.3.3.3 --mcc 460 --mnc 60 --stac 1 --etac 10
```
---
#### gnb-edit-skill.js
**功能**:修改 GNB 配置参数。支持批量和单个修改。
**使用方式**:
```bash
node 5gc.js gnb edit [选项...]
```
**参数**:
| 参数 | 说明 |
|------|------|
| `--project <工程>` | 目标工程,不带 `--name` 时批量修改 |
| `--name <名称>` | 精确匹配要修改的 GNB 名称 |
| `--set-ngap_sip <IP>` | 修改 NGAP 信令面 IP |
| `--set-user_sip_ip_v4 <IP>` | 修改用户面 IPv4 |
| `--set-user_sip_ip_v6 <IP>` | 修改用户面 IPv6 |
| `--set-mcc <值>` | 修改 MCC |
| `--set-mnc <值>` | 修改 MNC |
| `--set-stac <值>` | 修改起始 TAC |
| `--set-etac <值>` | 修改结束 TAC |
| `--set-node_id <ID>` | 修改节点 ID |
| `--set-cell_count <数量>` | 修改小区数量 |
| `--set-replay_ip <IP>` | 修改回放 IP |
| `--set-replay_port <端口>` | 修改回放端口 |
| `--url <地址>` | 5GC 仪表地址 |
| `--headed` | 打开可见浏览器 |
**示例**:
```bash
# 批量修改工程下所有 GNB 的用户面 IP
node 5gc.js gnb edit --project XW_S5GC_1 --set-user_sip_ip_v4 99.99.99.99
# 修改指定 GNB 的 NGAP IP 和 MCC/MNC
node 5gc.js gnb edit --name GNB_TEST --project XW_S5GC_1 --set-ngap_sip 200.20.20.88 --set-mcc 461 --set-mnc 22
```
---
### UE
#### ue-add-skill.js
**功能**:在指定工程下添加一个或多个 UE 实例。
**使用方式**:
```bash
node 5gc.js ue add --name <名称> [选项...]
```
**参数**:
| 参数 | 短名 | 说明 | 默认值 |
|------|------|------|--------|
| `--name <名称>` | `-n <名称>` | UE 名称(只支持字母/数字/下划线) | **必填** |
| `--project <工程>` | `-p <工程>` | 目标工程名称 | `XW_S5GC_1` |
| `--url <地址>` | `-u <地址>` | 5GC 仪表地址 | `https://192.168.3.89` |
| `--imsi <值>` | | 起始 IMSI(15位) | `460001234567890` |
| `--msisdn <值>` | | MSISDN(13-15位,以 86 开头) | `8611111111111` |
| `--mcc <值>` | | MCC | `460` |
| `--mnc <值>` | | MNC | `01` |
| `--key <值>` | | KI 密钥(32位 hex) | `1111...`(32个1) |
| `--opc <值>` | | OPc 密钥(32位 hex) | `1111...`(32个1) |
| `--imeisv <值>` | | IMEISV(偶数位) | `8611111111111111` |
| `--sst <值>` | | NSSAI SST | `1` |
| `--sd <值>` | | NSSAI SD | `111111` |
| `--count <数量>` | `-c <数量>` | 连续添加数量 | `1` |
| `--headed` | | 打开可见浏览器 | false |
> **命名约束**:UE 名称只能包含字母、数字、下划线(`_`),不能使用连字符(`-`)或其他特殊字符。
**示例**:
```bash
# 基本添加
node 5gc.js ue add --name UE_001 --project XW_S5GC_1
# 指定 IMSI 和 MSISDN
node 5gc.js ue add --name UE_TEST --imsi 460000000000001 --msisdn 8613888888888 --project XW_S5GC_1
# 批量添加 10 个连续 UE
node 5gc.js ue add --name UE_BATCH --count 10 --project XW_S5GC_1 --msisdn 8613900000000
# 指定认证密钥
node 5gc.js ue add --name UE_AUTH --project XW_S5GC_1 --key 00112233445566778899aabbccddeeff --opc 11223344556677889900aabbccddeeff
```
---
#### ue-edit-skill.js
**功能**:修改 UE 配置参数。支持批量和单个修改。
**使用方式**:
```bash
node 5gc.js ue edit [选项...]
```
**参数**:
| 参数 | 说明 |
|------|------|
| `--project <工程>` | 目标工程,不带 `--name` 时批量修改该工程下所有 UE |
| `--name <名称>` | 精确匹配要修改的 UE 名称(不支持批量时按名称过滤) |
| `--id <ID>` | 按 UE ID 修改 |
| `--set-msisdn <值>` | 修改 MSISDN |
| `--set-s_imsi <值>` | 修改 IMSI |
| `--set-mcc <值>` | 修改 MCC |
| `--set-mnc <值>` | 修改 MNC |
| `--set-key <值>` | 修改 KI 密钥 |
| `--set-opc <值>` | 修改 OPc 密钥 |
| `--set-imeisv <值>` | 修改 IMEISV |
| `--set-sst <值>` | 修改 NSSAI SST |
| `--set-sd <值>` | 修改 NSSAI SD |
| `--set-replay_ip <IP>` | 修改回放 IP |
| `--set-replay_port <端口>` | 修改回放端口 |
| `--set-count <数量>` | 修改数量 |
| `--url <地址>` | 5GC 仪表地址 |
| `--headed` | 打开可见浏览器 |
> ⚠️ `user_sip_ip_v4`、`user_sip_ip_v6` 在 UE 编辑表单中不存在此字段名,无需修改。
**示例**:
```bash
# 批量修改工程下所有 UE 的 MSISDN
node 5gc.js ue edit --project XW_S5GC_1 --set-msisdn 8613911111111
# 修改指定 UE
node 5gc.js ue edit --name UE_001 --project XW_S5GC_1 --set-msisdn 8613988888888 --set-sst 1 --set-sd 222222
# 按 ID 修改
node 5gc.js ue edit --id 10337 --set-opc aabbccddeeff00112233445566778899 --set-imeisv 8611111111111112
```
---
### PCF/PCRF
#### pcf-add-skill.js
**功能**:在指定工程下添加一个 PCF/PCRF 实例。
**使用方式**:
```bash
node 5gc.js pcf add <名称> [选项...]
node skills/5gc/scripts/pcf-add-skill.js <名称> [选项...]
```
**参数**:
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `<名称>` | PCF 实例名称(位置参数) | **必填** |
| `--project <工程>` / `-p <工程>` | 目标工程名称 | `XW_S5GC_1` |
| `--url <地址>` | 5GC 仪表地址 | `https://192.168.3.89` |
| `--http2_sip <IP>` | HTTP2 服务 IP | `192.168.20.90` |
| `--http2_port <端口>` | HTTP2 端口 | `80` |
| `--MCC <值>` | MCC(注意大写) | `460` |
| `--MNC <值>` | MNC(注意大写) | `01` |
| `--headed` | 打开可见浏览器 | false |
**示例**:
```bash
node 5gc.js pcf add PCF-TEST --project XW_S5GC_1
node 5gc.js pcf add PCF-PROD --project XW_S5GC_1 --http2_sip 10.0.0.50 --MCC 460 --MNC 01
```
#### pcf-edit-skill.js
**功能**:编辑指定工程下的 PCF/PCRF 实例(支持单条和批量)。
**使用方式**:
```bash
# 批量编辑:修改工程下所有 PCF 的字段
node 5gc.js pcf edit --project <工程> --set-<字段> <值>
# 单条编辑:修改指定名称的 PCF
node 5gc.js pcf edit --name <名称> --project <工程> --set-<字段> <值>
```
**可编辑字段**:
| 参数 | 说明 |
|------|------|
| `--set-http2_sip <IP>` | 修改 HTTP2 服务 IP |
| `--set-http2_port <端口>` | 修改 HTTP2 端口 |
| `--set-MCC <值>` | 修改 MCC(注意大写) |
| `--set-MNC <值>` | 修改 MNC(注意大写) |
**示例**:
```bash
# 批量修改工程下所有 PCF 的 HTTP2 IP
node 5gc.js pcf edit --project XW_S5GC_1 --set-http2_sip 10.10.10.99
# 修改指定 PCF 的 HTTP2 IP 和 MNC
node 5gc.js pcf edit --name pcc --project XW_S5GC_1 --set-http2_sip 10.10.10.88 --set-MNC 01
```
#### default-rule-add-skill.js(PCF 默认规则一键配置)
**功能**:为指定工程一键配置完整的 PCF 默认规则链路,包含 QoS 模板 → Traffic Control → PCC 规则 → sm_policy_default → PCF default_smpolicy 全五步。
**使用方式**:
```bash
node 5gc.js pcf default-rule-add --project <工程> [选项...]
node skills/5gc/scripts/default-rule-add-skill.js --project <工程> [选项...]
```
**参数**(全部可选,有默认值):
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `--project` | 工程名 | `XW_S5GC_1` |
| `--qos-id` | QoS 模板 ID | `qos_default_{时间戳}` |
| `--5qi` | 5QI 值(不指定则自动选择未使用的值) | 自动(优先 8/9/6/5...) |
| `--maxbr-ul` | 上行最大比特率 | `10000000` |
| `--maxbr-dl` | 下行最大比特率 | `20000000` |
| `--gbr-ul` | 上行保证比特率 | `5000000` |
| `--gbr-dl` | 下行保证比特率 | `5000000` |
| `--tc-id` | TC 规则 ID | `tc_default_{时间戳}` |
| `--flow-status` | TC 流状态 | `ENABLED` |
| `--pcc-id` | PCC 规则 ID | `pcc_default` |
| `--precedence` | PCC 优先级 | `63` |
| `--headed` | 显示浏览器窗口(调试用) | off |
**示例**:
```bash
# 最简用法(自动生成所有 ID)
node 5gc.js pcf default-rule-add --project XW_SUPF_5_1_2_4
# 指定 QoS 参数(高速率)
node 5gc.js pcf default-rule-add --project XW_SUPF_5_1_2_4 \
--qos-id qos_high_rate --5qi 8 \
--maxbr-ul 50000000 --maxbr-dl 100000000 \
--gbr-ul 20000000 --gbr-dl 40000000
# 指定 PCC 优先级
node 5gc.js pcf default-rule-add --project XW_SUPF_5_1_2_4 --pcc-id pcc_new --precedence 50
# 调试模式
node 5gc.js pcf default-rule-add --project XW_SUPF_5_1_2_4 --headed
```
> **注意**:同一工程多次运行会自动删除旧的同名资源并重建,不会污染配置。
### PCC 规则
#### pcc-add-skill.js
**功能**:在指定工程下添加一条 PCC 规则(PCC 规则用于绑定 QoS 模板和 Traffic Control)。
**使用方式**:
```bash
node 5gc.js pcc add --project <工程> --pcc-id <ID> --qos <QoS名称> --tc <TC名称> [选项...]
node skills/5gc/scripts/pcc-add-skill.js --project <工程> --pcc-id <ID> --qos <QoS名称> --tc <TC名称> [选项...]
```
**参数**:
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `--project` | 工程名 | `XW_S5GC_1` |
| `--pcc-id` | PCC 规则 ID(字母/数字/下划线) | **必填** |
| `--qos` | 引用的 QoS 模板名称 | **必填** |
| `--tc` | 引用的 Traffic Control 名称 | **必填** |
| `--precedence` | 优先级(0-255) | `63` |
| `--flow-desc` | 流描述(可选) | |
| `--headed` | 显示浏览器窗口 | off |
**示例**:
```bash
# 基本添加
node 5gc.js pcc add --project XW_SUPF_5_1_2_4 --pcc-id pcc_new --qos qos1 --tc tc1
# 指定优先级
node 5gc.js pcc add --project XW_SUPF_5_1_2_4 --pcc-id pcc_high --qos qos2 --tc tc1 --precedence 50
```
#### pcc-edit-skill.js
**功能**:编辑已有 PCC 规则的 QoS/TC 绑定(切换 PCC 引用的 QoS 模板或 Traffic Control)。
**使用方式**:
```bash
node 5gc.js pcc edit --project <工程> --name <PCC名称> --set-qos <新QoS> [--set-tc <新TC>]
```
**参数**:
| 参数 | 说明 |
|------|------|
| `--project` | 工程名 |
| `--name` | 要修改的 PCC 规则名称(精确匹配) |
| `--set-qos <名称>` | 切换到新的 QoS 模板 |
| `--set-tc <名称>` | 切换到新的 Traffic Control |
| `--headed` | 显示浏览器窗口 |
**示例**:
```bash
# 修改 PCC 引用的 QoS(用于修改上下行速率)
node 5gc.js pcc edit --project XW_SUPF_5_1_2_4 --name pcc_default --set-qos qos_high_rate
# 同时修改 QoS 和 TC
node 5gc.js pcc edit --project XW_SUPF_5_1_2_4 --name pcc_default --set-qos qos1 --set-tc tc2
```
> ⚠️ **重要**:PCC 的 `refQosData` 和 `refTcData` 存储在 xm-select 多选组件中。编辑时会自动切换选择,无需手动取消旧选项。
### NRF(网络存储功能)
#### nrf-add-skill.js
**功能**:在指定工程下添加一个 NRF 实例。
**使用方式**:
```bash
node 5gc.js nrf add <名称> [选项...]
node skills/5gc/scripts/nrf-add-skill.js <名称> [选项...]
```
**参数**:
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `<名称>` | NRF 实例名称(位置参数) | **必填** |
| `--project <工程>` / `-p <工程>` | 目标工程名称 | `XW_S5GC_1` |
| `--url <地址>` | 5GC 仪表地址 | `https://192.168.3.89` |
| `--http2_sip <IP>` | HTTP2 服务 IP | `192.168.20.100` |
| `--http2_port <端口>` | HTTP2 端口 | `80` |
| `--MCC <值>` | MCC(注意大写) | `460` |
| `--MNC <值>` | MNC(注意大写) | `01` |
| `--headed` | 打开可见浏览器 | false |
**示例**:
```bash
node 5gc.js nrf add NRF-TEST --project XW_S5GC_1
node 5gc.js nrf add NRF-PROD --project XW_S5GC_1 --http2_sip 10.0.0.50 --MCC 460 --MNC 01
```
#### nrf-edit-skill.js
**功能**:编辑指定工程下的 NRF 实例(支持单条和批量)。
**使用方式**:
```bash
# 批量编辑:修改工程下所有 NRF 的字段
node 5gc.js nrf edit --project <工程> --set-<字段> <值>
# 单条编辑:修改指定名称的 NRF
node 5gc.js nrf edit --name <名称> --project <工程> --set-<字段> <值>
```
**可编辑字段**:
| 参数 | 说明 |
|------|------|
| `--set-http2_sip <IP>` | 修改 HTTP2 服务 IP |
| `--set-http2_port <端口>` | 修改 HTTP2 端口 |
| `--set-MCC <值>` | 修改 MCC(注意大写) |
| `--set-MNC <值>` | 修改 MNC(注意大写) |
**示例**:
```bash
# 批量修改工程下所有 NRF 的 HTTP2 IP
node 5gc.js nrf edit --project XW_S5GC_1 --set-http2_sip 10.10.10.99
# 修改指定 NRF 的 HTTP2 IP 和 MNC
node 5gc.js nrf edit --name nrf1 --project XW_S5GC_1 --set-http2_sip 10.10.10.88 --set-MNC 01
```
### QoS 模板
#### qos-add-skill.js
**功能**:在指定工程下添加一个 QoS(服务质量)模板,定义 5QI、上下行最大比特率、保证比特率等参数。
**使用方式**:
```bash
node 5gc.js qos add --project <工程> --qos-id <ID> [选项...]
node skills/5gc/scripts/qos-add-skill.js --project <工程> --qos-id <ID> [选项...]
```
**参数**:
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `--project` | 工程名 | `XW_S5GC_1` |
| `--qos-id` | QoS 模板 ID(字母/数字/下划线) | **必填** |
| `--5qi` | 5QI 值(不指定则自动选择) | 自动选择未使用的值(优先 8/9/6/5...) |
| `--maxbr-ul` | 上行最大比特率(bps) | `10000000` |
| `--maxbr-dl` | 下行最大比特率(bps) | `20000000` |
| `--gbr-ul` | 上行保证比特率(bps) | `5000000` |
| `--gbr-dl` | 下行保证比特率(bps) | `5000000` |
| `--priority` | 优先级 | 空 |
| `--headed` | 显示浏览器窗口 | off |
**示例**:
```bash
# 基本添加
node 5gc.js qos add --project XW_SUPF_5_1_2_4 --qos-id qos1
# 高速率 QoS
node 5gc.js qos add --project XW_SUPF_5_1_2_4 --qos-id qos_high \
--5qi 8 --maxbr-ul 50000000 --maxbr-dl 100000000 \
--gbr-ul 20000000 --gbr-dl 40000000
# 批量创建不同 5qi 的 QoS 模板
node 5gc.js qos add --project XW_SUPF_5_1_2_4 --qos-id qos_6 --5qi 6
node 5gc.js qos add --project XW_SUPF_5_1_2_4 --qos-id qos_9 --5qi 9
```
---
### Traffic Control
#### tc-add-skill.js
**功能**:在指定工程下添加一条 Traffic Control 流量控制规则,控制 UE 流量的启用/禁用状态。
**使用方式**:
```bash
node 5gc.js tc add --project <工程> --tc-id <ID> [选项...]
node skills/5gc/scripts/tc-add-skill.js --project <工程> --tc-id <ID> [选项...]
```
**参数**:
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `--project` | 工程名 | `XW_S5GC_1` |
| `--tc-id` | TC 规则 ID(字母/数字/下划线) | **必填** |
| `--flow-status` | 流状态 | `ENABLED` |
| `--flow-desc` | 流描述(可选) | |
| `--headed` | 显示浏览器窗口 | off |
> **flow-status 选项**:`ENABLED`(启用)、`DISABLED`(禁用)、`ENABLED-UPLINK`(仅上行)等。
**示例**:
```bash
# 基本添加
node 5gc.js tc add --project XW_SUPF_5_1_2_4 --tc-id tc1
# 指定流状态
node 5gc.js tc add --project XW_SUPF_5_1_2_4 --tc-id tc_uplink --flow-status ENABLED-UPLINK
```
---
### SMPolicy
#### smpolicy_add_pcc.js {#smpolicy-default}
**功能**:将已有 PCC 规则添加到工程默认的 `sm_policy_default` 会话策略中(追加到 pccRules 列表)。
**使用方式**:
```bash
node 5gc.js smpolicy add-pcc --project <工程> --pcc-id <PCC名称>
node skills/5gc/scripts/smpolicy_add_pcc.js --project <工程> --pcc-id <PCC名称>
```
**参数**:
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `--project` | 工程名 | `XW_SUPF_5_1_2_4` |
| `--pcc-id` | 已存在的 PCC 规则 ID | **必填** |
| `--headed` | 显示浏览器窗口 | off |
> **链路**:`smpolicy/default/index` → 编辑 `sm_policy_default` 弹窗 → pccRules xm-select 中追加指定 PCC。
**示例**:
```bash
# 将 PCC 添加到 sm_policy_default
node 5gc.js smpolicy add-pcc --project XW_SUPF_5_1_2_4 --pcc-id pcc_high_rate
# 添加多个 PCC 规则
node 5gc.js smpolicy add-pcc --project XW_SUPF_5_1_2_4 --pcc-id pcc_default
node 5gc.js smpolicy add-pcc --project XW_SUPF_5_1_2_4 --pcc-id pcc_video
```
---
#### smpolicy-ue-add-skill.js {#smpolicy-ue-add-skilljs}
**功能**:在指定工程下添加一条 UE Smpolicy 规则,按 IMSI/DNN/sNssai 匹配 UE 并关联 PCC 规则。
**使用方式**:
```bash
node 5gc.js smpolicy ue-add --project <工程> --name <名称> --dnn <DNN> [选项...]
node skills/5gc/scripts/smpolicy-ue-add-skill.js --project <工程> --name <名称> --dnn <DNN> [选项...]
```
**参数**:
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `--project` | 工程名 | `XW_S5GC_1` |
| `--name` | UE策略名称(字母/数字/下划线) | **必填** |
| `--dnn` | DNN | **必填** |
| `--imsi` | IMSI 起始值(不填则自动生成) | 自动 |
| `--imsi-num` | IMSI 数量 | `1` |
| `--sst` | sNssai SST | `1` |
| `--sd` | sNssai SD | `111111` |
| `--sess-rules` | 会话规则(xm-select,多个逗号分隔) | |
| `--pcc-rules` | PCC规则(xm-select,多个逗号分隔) | |
| `--pra-rules` | PRA规则(xm-select,可选) | |
| `--ref-qos-timer` | reflectiveQoSTimer 值(秒) | |
| `--headed` | 显示浏览器窗口 | off |
**示例**:
```bash
# 基本添加
node 5gc.js smpolicy ue-add --project XW_SUPF_5_1_2_4 --name ue_policy1 --dnn internet
# 指定 IMSI 和 sNssai
node 5gc.js smpolicy ue-add --project XW_SUPF_5_1_2_4 --name ue_policy1 --dnn internet \
--imsi 460001234567890 --sst 1 --sd 111111
# 绑定 PCC 规则(多个逗号分隔)
node 5gc.js smpolicy ue-add --project XW_SUPF_5_1_2_4 --name ue_policy2 --dnn internet \
--pcc-rules "pcc2,pcc_default"
# 指定反射 QoS 定时器
node 5gc.js smpolicy ue-add --project XW_SUPF_5_1_2_4 --name ue_policy3 --dnn internet \
--pcc-rules pcc2 --ref-qos-timer 60
```
#### smpolicy-ue-edit-skill.js
**功能**:编辑已有 UE Smpolicy 规则的字段(DNN、sNssai、PCC 绑定等)。
**使用方式**:
```bash
node 5gc.js smpolicy ue-edit --project <工程> --name <名称> [--dnn <新DNN>] [--pcc-rules <规则>] [选项...]
```
**参数**:
| 参数 | 说明 |
|------|------|
| `--project` | 工程名 |
| `--name` | 要编辑的 UE 策略名称(精确匹配) |
| `--dnn` | 新 DNN |
| `--imsi` | 新 IMSI 起始值 |
| `--sst` | 新 sNssai SST |
| `--sd` | 新 sNssai SD |
| `--sess-rules` | 会话规则(xm-select,多个逗号分隔) |
| `--pcc-rules` | PCC规则(xm-select,多个逗号分隔) |
| `--pra-rules` | PRA规则(xm-select) |
| `--ref-qos-timer` | reflectiveQoSTimer |
| `--headed` | 显示浏览器窗口 |
> ⚠️ xm-select 为多选模式。指定 `--pcc-rules` 时会叠加选中已有规则;编辑时需注意 toggle 行为。
**示例**:
```bash
# 修改 DNN
node 5gc.js smpolicy ue-edit --project XW_SUPF_5_1_2_4 --name ue_policy1 --dnn internet_new
# 修改 PCC 绑定
node 5gc.js smpolicy ue-edit --project XW_SUPF_5_1_2_4 --name ue_policy1 --pcc-rules pcc2,pcc_reg_test
# 修改 sNssai
node 5gc.js smpolicy ue-edit --project XW_SUPF_5_1_2_4 --name ue_policy1 --sst 1 --sd 222222
```
#### smpolicy-dnn-add-skill.js {#smpolicy-dnn}
**功能**:在指定工程下添加一条 DNN Smpolicy 规则,按 DNN/sNssai 匹配会话并关联 PCC 规则。
**使用方式**:
```bash
node 5gc.js smpolicy dnn-add --project <工程> --name <名称> --dnn <DNN> [选项...]
```
**参数**:
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `--project` | 工程名 | `XW_S5GC_1` |
| `--name` | DNN策略名称(必填) | **必填** |
| `--dnn` | DNN值(必填) | **必填** |
| `--sst` | sNssai SST | `1` |
| `--sd` | sNssai SD | `111111` |
| `--sess-rules` | 会话规则(xm-select,多个逗号分隔) | |
| `--pcc-rules` | PCC规则(xm-select,多个逗号分隔) | |
| `--pra-rules` | PRA规则(xm-select,可选) | |
| `--ref-qos-timer` | reflectiveQoSTimer(秒) | |
| `--headed` | 显示浏览器窗口 | off |
**示例**:
```bash
# 基本添加
node 5gc.js smpolicy dnn-add --project XW_SUPF_5_1_2_4 --name dnn_policy1 --dnn internet
# 绑定 PCC 规则
node 5gc.js smpolicy dnn-add --project XW_SUPF_5_1_2_4 --name dnn_policy1 --dnn internet --pcc-rules pcc2
# 多个 PCC 规则
node 5gc.js smpolicy dnn-add --project XW_SUPF_5_1_2_4 --name dnn_policy2 --dnn internet --pcc-rules "pcc2,pcc_default"
```
#### smpolicy-dnn-edit-skill.js
**功能**:编辑已有 DNN Smpolicy 规则的字段(DNN、sNssai、PCC 绑定等)。
**使用方式**:
```bash
node 5gc.js smpolicy dnn-edit --project <工程> --name <名称> [--dnn <新DNN>] [--pcc-rules <规则>] [选项...]
```
**参数**:
| 参数 | 说明 |
|------|------|
| `--project` | 工程名 |
| `--name` | 要编辑的 DNN 策略名称(精确匹配) |
| `--dnn` | 新 DNN 值 |
| `--sst` | 新 sNssai SST |
| `--sd` | 新 sNssai SD |
| `--sess-rules` | 会话规则(xm-select,多个逗号分隔) |
| `--pcc-rules` | PCC规则(xm-select,多个逗号分隔) |
| `--pra-rules` | PRA规则(xm-select) |
| `--ref-qos-timer` | reflectiveQoSTimer |
| `--headed` | 显示浏览器窗口 |
> ⚠️ xm-select 为多选模式。指定 `--pcc-rules` 时会叠加选中已有规则;编辑时需注意 toggle 行为。
**示例**:
```bash
# 修改 DNN
node 5gc.js smpolicy dnn-edit --project XW_SUPF_5_1_2_4 --name dnn_policy1 --dnn internet_new
# 修改 PCC 绑定
node 5gc.js smpolicy dnn-edit --project XW_SUPF_5_1_2_4 --name dnn_policy1 --pcc-rules pcc2,pcc_default
```
---
## 全局参数参考
以下参数所有脚本均支持:
| 参数 | 说明 | 适用范围 |
|------|------|---------|
| `--url <地址>` | 5GC 仪表 URL | 所有脚本 |
| `--project <工程>` / `-p <工程>` | 目标工程名称 | 所有脚本 |
| `--headed` | 打开可见 Chromium 窗口(调试用) | 所有脚本 |
| `--set-<字段> <值>` | 修改指定字段值 | 所有 edit 脚本 |
| `--name <名称>` | 按名称精确匹配 | 所有 edit 脚本 |
| `--id <ID>` | 按 ID 直接定位 | 所有 edit 脚本 |
---
## 字段参考
### AMF 字段
| 字段名 | 说明 | 示例值 |
|--------|------|--------|
| `mcc` | 移动国家码 | `460` |
| `mnc` | 移动网络码 | `01` |
| `ngap_sip` | NGAP 信令面 IP | `10.200.1.50` |
| `ngap_port` | NGAP 端口 | `38412` |
| `http2_sip` | HTTP2 服务 IP | `10.200.1.51` |
| `http2_port` | HTTP2 端口 | `8080` |
| `stac` | 起始 TAC | `101` |
| `etac` | 结束 TAC | `102` |
| `region_id` | 区域 ID | `1` |
| `set_id` | Set ID | `1` |
| `pointer` | 指针 | `1` |
| `ea[NEA0]` ~ `ea[128-NEA3]` | 加密算法(默认全选) | `1` |
| `ia[NIA0]` ~ `ia[128-NIA3]` | 完整性保护算法(默认全选) | `1` |
### UDM/AUSF 字段
| 字段名 | 说明 | 示例值 |
|--------|------|--------|
| `count` | 实例数量 | `3` |
| `sip` | SIP 服务 IP | `10.0.0.100` |
| `port` | 端口 | `80` |
| `auth_method` | 认证方法 | `5G_AKA` |
| `scheme` | 协议类型 | `HTTP` |
| `priority` | 优先级 | `8` |
### SMF/PGW-C 字段
| 字段名 | 说明 | 示例值 |
|--------|------|--------|
| `pfcp_sip` | PFCP 信令面 IP | `10.10.10.50` |
| `n3_ip` | N3 接口 IP | `10.10.10.50` |
| `n6_ip` | N6 接口 IP | `10.10.10.51` |
| `http2_sip` | HTTP2 服务 IP | `10.10.10.50` |
| `dnn` | DNN(数据网络名) | `internet` |
| `snssai_sst` | NSSAI SST | `1` |
| `snssai_sd` | NSSAI SD | `ffffff` |
| `mcc` | MCC | `460` |
| `mnc` | MNC | `01` |
| `pdu_capacity` | PDU 会话容量 | `200000` |
### UPF/PGW-U 字段
| 字段名 | 说明 | 示例值 |
|--------|------|--------|
| `n3_ip` | N3 接口 IP | `192.168.20.30` |
| `n4_ip` | N4 接口 IP(PFCP) | `192.168.20.30` |
| `n6_ip` | N6 接口 IP | `192.168.20.31` |
| `n6_gw` | N6 网关 IP | `192.168.20.1` |
| `dnn` | DNN | `internet` |
| `static_arp` | 静态 ARP | `192.168.20.254` |
| `sst` | NSSAI SST | `1` |
| `sd` | NSSAI SD | `ffffff` |
| `stac` | 起始 TAC | `101` |
| `etac` | 结束 TAC | `102` |
### GNB 字段
| 字段名 | 说明 | 示例值 |
|--------|------|--------|
| `ngap_sip` | NGAP 信令面 IP | `200.20.20.50` |
| `user_sip_ip_v4` | 用户面 IPv4 | `2.2.2.2` |
| `user_sip_ip_v6` | 用户面 IPv6 | `::1` |
| `mcc` | MCC | `460` |
| `mnc` | MNC | `60` |
| `stac` | 起始 TAC | `0` |
| `etac` | 结束 TAC | `0` |
| `node_id` | 节点 ID | `70` |
| `cell_count` | 小区数量 | `1` |
| `replay_ip` | 回放 IP | `0.0.0.0` |
| `replay_port` | 回放端口 | `0` |
### UE 字段
| 字段名 | 说明 | 示例值 |
|--------|------|--------|
| `s_imsi` | 起始 IMSI(15位) | `460001234567890` |
| `msisdn` | MSISDN(13-15位,86开头) | `8613888888888` |
| `mcc` | MCC | `460` |
| `mnc` | MNC | `01` |
| `key` | KI 密钥(32位 hex) | `001122...` |
| `op_opc` | OPc 密钥(32位 hex) | `aabbcc...` |
| `imeisv` | IMEISV(15位,偶数) | `8611111111111111` |
| `nssai_sst` | NSSAI SST | `1` |
| `nssai_sd` | NSSAI SD | `111111` |
| `user_sip_ip_v4` | 用户面 IPv4 | `自动分配` |
| `user_sip_ip_v6` | 用户面 IPv6 | `自动分配` |
| `replay_ip` | 回放 IP | `0.0.0.0` |
| `replay_port` | 回放端口 | `0` |
#### default-rule-add-skill.js(PCF 默认规则一键配置)
**功能**:为指定工程一键配置完整的 PCF 默认规则链路,包含 QoS 模板 → Traffic Control → PCC 规则 → sm_policy_default → PCF default_smpolicy 全五步。
**使用方式**:
```bash
node 5gc.js pcf default-rule-add --project <工程> [选项...]
node skills/5gc/scripts/default-rule-add-skill.js --project <工程> [选项...]
```
**参数**(全部可选,有默认值):
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `--project` | 工程名 | `XW_S5GC_1` |
| `--pcf-name` | **PCF 实例名称**(必填,指定要为哪个 PCF 配置默认规则) | 无 |
| `--qos-id` | QoS 模板 ID | `qos_default_{时间戳}` |
| `--5qi` | 5QI 值(不指定则自动选择未使用的值) | 自动(优先 8/9/6/5...) |
| `--maxbr-ul` | 上行最大比特率 | `10000000` |
| `--maxbr-dl` | 下行最大比特率 | `20000000` |
| `--gbr-ul` | 上行保证比特率 | `5000000` |
| `--gbr-dl` | 下行保证比特率 | `5000000` |
| `--tc-id` | TC 规则 ID | `tc_default_{时间戳}` |
| `--flow-status` | TC 流状态 | `ENABLED` |
| `--pcc-id` | PCC 规则 ID | `pcc_default` |
| `--precedence` | PCC 优先级 | `63` |
| `--headed` | 显示浏览器窗口(调试用) | off |
**示例**:
```bash
# 最简用法(自动生成所有 ID)
node 5gc.js pcf default-rule-add --project XW_SUPF_5_1_2_4 --pcf-name pcc
# 指定 QoS 参数(高速率)
node 5gc.js pcf default-rule-add --project XW_SUPF_5_1_2_4 --pcf-name pcc \
--qos-id qos_high_rate --5qi 8 \
--maxbr-ul 50000000 --maxbr-dl 100000000 \
--gbr-ul 20000000 --gbr-dl 40000000
# 指定 PCC 优先级
node 5gc.js pcf default-rule-add --project XW_SUPF_5_1_2_4 --pcf-name pcc --pcc-id pcc_new --precedence 50
# 调试模式
node 5gc.js pcf default-rule-add --project XW_SUPF_5_1_2_4 --pcf-name pcc --headed
```
**完整链路**:
1. ✅ **QoS 模板创建**:自动选择未使用的 5QI,创建 QoS 模板
2. ✅ **Traffic Control 创建**:创建 ENABLED 状态的 TC 规则
3. ✅ **PCC 规则创建**:创建 PCC 规则,绑定 QoS 和 TC
4. ✅ **sm_policy_default 创建/更新**:创建或更新默认会话策略,绑定 PCC 规则
5. ✅ **PCF default_smpolicy 设置**:为指定 PCF 实例设置 default_smpolicy 为 sm_policy_default
**注意事项**:
- 同一工程多次运行会自动删除旧的同名资源并重建,不会污染配置
- 必须指定 `--pcf-name` 参数,明确要为哪个 PCF 实例配置默认规则
- 脚本会自动处理弹窗(iframe)和 CSRF token,无需手动操作
- 所有步骤都有验证检查,确保配置成功
**已测试工程**:
- ✅ XW_SUPF_5_1_11_2(PCF "qqq")
- ✅ XW_SUPF_5_1_8_1(PCF "pcc")
- ✅ XW_SUPF_5_1_4_1(PCF "pcc")
### PCF/PCRF 字段
| 字段名 | 说明 | 示例值 |
|--------|------|--------|
| `http2_sip` | HTTP2 服务 IP | `192.168.20.90` |
| `http2_port` | HTTP2 端口 | `80` |
| `MCC` | MCC(大写) | `460` |
| `MNC` | MNC(大写) | `01` |
| `count` | 实例数量 | `1` |
FILE:scripts/5gc.js
/**
* 5GC Web 仪表统一 CLI
*
* 用法: node 5gc.js <entity> <action> [options]
*
* entity (网元类型): amf | udm | smf | upf | gnb | ue | pcf | nrf | qos | tc | smpolicy
* action (操作类型): add | edit | default-rule-add | default-rule-edit
*
* 通用选项:
* --url <地址> 5GC 仪表地址(默认 https://192.168.3.89)
* --project <工程> 目标工程名称
* --name <名称> 网元名称(用于单条记录筛选)
* --id <id> 网元 ID(直接编辑指定 ID)
* --headed 以有头模式运行(显示浏览器窗口)
*
* 字段修改(edit 模式)--set-<field> <value>:
* AMF: name|sbi_ip|sbi_port|amf_name|guami|mcc|mnc|sst|sd|ap1|ap2|ap3|ap4|ap5
* UDM: name|auth_supi|auth_op_type|op_opc|aud_method|scheme|id|priority
* SMF: name|pfcp_ip|n3_ip|n6_ip|dnn|snssai|sliceamba_type
* UPF: name|n4_ip|n3_ip|n6_ip|dnn|snssai|count|static_arp|ue_ip_pool
* GNB: name|ngap_ip|user_sip_ip_v4|mcc|mnc|stac|etac|node_id|cell_count|replay_ip|replay_port
* UE: name|count|mcc|mnc|s_imsi|key|opc|imeisv|msisdn|user_sip_ip_v4|user_sip_ip_v6|replay_ip|replay_port
*
* 示例:
* node 5gc.js amf add --name AMF_TEST --project XW_S5GC_1 --sbi-ip 10.0.0.1
* node 5gc.js gnb add --name GNB_TEST --project XW_S5GC_1 --count 1 --mcc 460 --mnc 01 --stac 1 --etac 100
* node 5gc.js ue add --name UE_001 --imsi 460001234567890 --msisdn 8613888888888
* node 5gc.js ue edit --project XW_S5GC_1 --set-msisdn 8613888888888
* node 5gc.js ue edit --id 10337 --set-msisdn 8613888888888
* node 5gc.js gnb edit --project XW_S5GC_1 --set-user_sip_ip_v4 200.200.200.200
* node 5gc.js upf edit --project XW_S5GC_1 --set-n4_ip 10.0.0.5
* node 5gc.js pcf default-rule-add --project XW_SUPF_5_1_2_4 --pcf-name pcc
* node 5gc.js pcf default-rule-add --project XW_SUPF_5_1_2_4 --pcf-name pcc --qos-id qos1 --tc-id tc1 --pcc-id pcc_default --precedence 50
* node 5gc.js qos add --project XW_SUPF_5_1_2_4 --qos-id qos_new --5qi 8 --maxbr-ul 10000000 --maxbr-dl 20000000
* node 5gc.js tc add --project XW_SUPF_5_1_2_4 --tc-id tc_new --flow-status ENABLED
* node 5gc.js pcc add --project XW_SUPF_5_1_2_4 --pcc-id pcc_new --qos qos1 --tc tc1
* node 5gc.js smpolicy default-add-pcc --project XW_SUPF_5_1_2_4 --pcc-id pcc_new
*/
const { spawn } = require('child_process');
const path = require('path');
const fs = require('fs');
const SCRIPTS_DIR = __dirname;
const argv = process.argv.slice(2);
if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') {
printHelp();
process.exit(0);
}
const entity = argv[0].toLowerCase();
const action = (argv[1] || '').toLowerCase();
const VALID_ENTITIES = ['amf', 'udm', 'smf', 'upf', 'gnb', 'ue', 'pcf', 'pcc', 'nrf', 'qos', 'tc', 'smpolicy'];
const VALID_ACTIONS = ['add', 'edit', 'default-rule-add', 'add-pcc', 'ue-add', 'ue-edit', 'dnn-add', 'dnn-edit'];
if (!VALID_ENTITIES.includes(entity)) {
console.error(`\n❌ 未知网元类型: entity`);
console.error(' 可用: ' + VALID_ENTITIES.join(', '));
process.exit(1);
}
if (!action || !VALID_ACTIONS.includes(action)) {
console.error(`\n❌ 未知操作: action || '(空)'`);
console.error(' 用法: node 5gc.js <entity> <action> [options]');
console.error(' 示例: node 5gc.js amf add --help');
process.exit(1);
}
// 子脚本映射
// 所有 edit 均映射到 edit 脚本(单条 + 批量二合一)
const scriptMap = {
'amf:add': 'amf-add-skill.js',
'amf:edit': 'amf-edit-skill.js',
'udm:add': 'ausf-udm-add-skill.js',
'udm:edit': 'ausf-udm-edit-skill.js',
'smf:add': 'smf-pgwc-add-skill.js',
'smf:edit': 'smf-pgwc-edit-skill.js',
'upf:add': 'upf-add-skill.js',
'upf:edit': 'upf-edit-skill.js',
'gnb:add': 'gnb-add-skill.js',
'gnb:edit': 'gnb-edit-skill.js',
'ue:add': 'ue-add-skill.js',
'ue:edit': 'ue-edit-skill.js',
'pcf:add': 'pcf-add-skill.js',
'pcf:edit': 'pcf-edit-skill.js',
'pcf:default-rule-add': 'default-rule-add-skill.js',
'pcc:add': 'pcc-add-skill.js',
'pcc:edit': 'pcc-edit-skill.js',
'nrf:add': 'nrf-add-skill.js',
'nrf:edit': 'nrf-edit-skill.js',
'qos:add': 'qos-add-skill.js',
'tc:add': 'tc-add-skill.js',
'smpolicy:add-pcc': 'smpolicy_add_pcc.js',
'smpolicy:ue-add': 'smpolicy-ue-add-skill.js',
'smpolicy:ue-edit': 'smpolicy-ue-edit-skill.js',
'smpolicy:dnn-add': 'smpolicy-dnn-add-skill.js',
'smpolicy:dnn-edit': 'smpolicy-dnn-edit-skill.js',
};
const scriptFile = scriptMap[`entity:action`];
const scriptPath = path.join(SCRIPTS_DIR, scriptFile);
if (!fs.existsSync(scriptPath)) {
console.error(`\n❌ 脚本不存在: scriptPath`);
process.exit(1);
}
function normalizeChildArgs(entity, action, args) {
const out = [];
let positionalName = null;
for (let i = 0; i < args.length; i++) {
const arg = args[i];
const next = i + 1 < args.length ? args[i + 1] : undefined;
if (arg === '--name' && next !== undefined) {
if (entity === 'ue' && action === 'add') {
out.push('--name', next);
} else {
positionalName = next;
}
i++;
continue;
}
if ((entity === 'smf' || entity === 'upf' || entity === 'gnb') && arg === '--pfcp-ip' && next !== undefined) {
out.push('--pfcp_sip', next); i++; continue;
}
if (entity === 'smf' && arg === '--n3-ip' && next !== undefined) {
out.push('--http2_sip', next); i++; continue;
}
if (entity === 'upf' && arg === '--n4-ip' && next !== undefined) {
out.push('--n4_ip', next); i++; continue;
}
if (entity === 'upf' && arg === '--n3-ip' && next !== undefined) {
out.push('--n3_ip', next); i++; continue;
}
if (entity === 'upf' && arg === '--n6-ip' && next !== undefined) {
out.push('--n6_ip', next); i++; continue;
}
if (entity === 'gnb' && arg === '--ngap-ip' && next !== undefined) {
out.push('--ngap_sip', next); i++; continue;
}
if (entity === 'gnb' && arg === '--user-sip-ip-v4' && next !== undefined) {
out.push('--user_sip_ip_v4', next); i++; continue;
}
if (entity === 'gnb' && arg === '--node-id' && next !== undefined) {
out.push('--node_id', next); i++; continue;
}
if (entity === 'amf' && action === 'add') {
if (arg === '--sbi-ip' && next !== undefined) { out.push('--http2_sip', next); i++; continue; }
if (arg === '--sst' && next !== undefined) { i++; continue; }
if (arg === '--sd' && next !== undefined) { i++; continue; }
}
if (entity === 'udm' && action === 'add') {
if (arg === '--auth-supi' && next !== undefined) { i++; continue; }
if (arg === '--auth-op-type' && next !== undefined) { i++; continue; }
if (arg === '--opc' && next !== undefined) { out.push('--op_opc', next); i++; continue; }
}
out.push(arg);
}
if (positionalName) out.unshift(positionalName);
return out;
}
// 去掉 entity 和 action 后的参数传给子脚本
const childArgv = normalizeChildArgs(entity, action, argv.slice(2));
console.log(`\n▶ 5GC entity.toUpperCase() action`);
console.log(' → node ' + scriptFile + ' ' + childArgv.join(' ') + '\n');
// 用子进程调用,保持 CLI 参数隔离
const child = spawn('node', [scriptPath, ...childArgv], {
stdio: 'inherit',
shell: true,
cwd: SCRIPTS_DIR,
});
child.on('exit', (code) => process.exit(code || 0));
child.on('error', (err) => { console.error('启动失败:', err.message); process.exit(1); });
function printHelp() {
console.log(`
5GC Web 仪表自动化 - 统一 CLI
=============================
用法:
node 5gc.js <entity> <action> [options]
网元类型 (entity):
amf - AMF(接入与移动性管理功能)
udm - UDM/AUSF(统一数据管理/认证服务器功能)
smf - SMF/PGW-C(会话管理功能/PDN 连接网关控制面)
upf - UPF/PGW-U(用户面功能/PDN 连接网关用户面)
gnb - gNodeB(5G 基站)
ue - UE(用户终端)
pcf - PCF/PCRF(策略控制功能)
nrf - NRF(网络存储功能)
qos - QoS 模板
tc - Traffic Control 流量控制规则
smpolicy - Smpolicy(会话策略规则)
操作 (action):
add - 添加网元实例
edit - 编辑网元(单个或批量)
default-rule-add - 一键配置完整 PCF 默认规则链路(QoS → TC → PCC → sm_policy_default → PCF)
通用选项:
--url <地址> 5GC 仪表地址(默认 https://192.168.3.89)
--project <工程> 目标工程名称
--name <名称> 网元名称
--id <id> 网元 ID(edit 模式)
--headed 以有头模式运行(显示浏览器)
--help 显示本帮助
字段修改(edit 模式 --set-<field> <value>):
AMF: name|sbi_ip|sbi_port|amf_name|guami|mcc|mnc|sst|sd|ap1|ap2|ap3|ap4|ap5
UDM: name|auth_supi|auth_op_type|op_opc|aud_method|scheme|id|priority
SMF: name|pfcp_ip|n3_ip|n6_ip|dnn|snssai|sliceamba_type
UPF: name|n4_ip|n3_ip|n6_ip|dnn|snssai|count|static_arp|ue_ip_pool
GNB: name|ngap_ip|user_sip_ip_v4|mcc|mnc|stac|etac|node_id|cell_count|replay_ip|replay_port
UE: name|count|mcc|mnc|s_imsi|key|opc|imeisv|msisdn|user_sip_ip_v4|user_sip_ip_v6|replay_ip|replay_port
PCF: http2_sip|http2_port|mcc|mnc
PCF默认规则: --pcf-name <名称> --qos-id <ID> --tc-id <ID> --pcc-id <ID> --precedence <值>
添加示例:
node 5gc.js amf add --name AMF_TEST --project XW_S5GC_1 --sbi-ip 10.0.0.1 --mcc 460 --mnc 01
node 5gc.js gnb add --name GNB_TEST --project XW_S5GC_1 --count 1 --mcc 460 --mnc 01 --stac 1 --etac 100
node 5gc.js ue add --name UE_001 --imsi 460001234567890 --msisdn 8613888888888
node 5gc.js smf add --name SMF_TEST --project XW_S5GC_1 --pfcp-ip 10.0.0.2
node 5gc.js upf add --name UPF_TEST --project XW_S5GC_1 --n4-ip 10.0.0.3
node 5gc.js qos add --project XW_SUPF_5_1_2_4 --qos-id qos_new --5qi 8 --maxbr-ul 10000000 --maxbr-dl 20000000
node 5gc.js tc add --project XW_SUPF_5_1_2_4 --tc-id tc_new --flow-status ENABLED
node 5gc.js pcc add --project XW_SUPF_5_1_2_4 --pcc-id pcc_new --qos qos1 --tc tc1
node 5gc.js pcf default-rule-add --project XW_SUPF_5_1_2_4 --pcf-name pcc
node 5gc.js pcf default-rule-add --project XW_SUPF_5_1_2_4 --pcf-name pcc --qos-id qos1 --tc-id tc1 --pcc-id pcc_default --precedence 50
编辑示例:
node 5gc.js ue edit --project XW_S5GC_1 --set-msisdn 8613888888888
node 5gc.js ue edit --id 10337 --set-msisdn 8613888888888
node 5gc.js gnb edit --project XW_S5GC_1 --set-user_sip_ip_v4 200.200.200.200
node 5gc.js upf edit --project XW_S5GC_1 --set-n4_ip 10.0.0.5
`);
}
FILE:scripts/5gc_session_192_168_3_89.json
{
"storageState": {
"cookies": [
{
"name": "XSRF-TOKEN",
"value": "eyJpdiI6InY2dUdGdmJ0MjhHSlMyRFhiOUhWSkE9PSIsInZhbHVlIjoiR29BbWM4SzVrajdSTVUzc0JIV1Jnclo0MkhOcVQzMEZ0UUJqL01uektKTEUwa0dySXh5WkZoOEZ5T0Y2Q3A5ZExtc2tEMXRwMktLSXJhR2lLL01xaVk4WGJBQUd3YzkrT2NidDh3cmZ0cGdJWkx2dmRaUXl1R1lFZ3F4RVhXRXkiLCJtYWMiOiI4MGY5YTFmODNiY2VkZWY1OGI0OGVhY2Q2ZTQ4M2VkYTkyZGQ0MGY5NGU3ZTZiOTFiNmY3ZmRhMTJhNDI0N2NiIn0%3D",
"domain": "192.168.3.89",
"path": "/",
"expires": 1776486919.581771,
"httpOnly": false,
"secure": false,
"sameSite": "Lax"
},
{
"name": "laravel_5gc_session",
"value": "eyJpdiI6IjJOQWd3Vk02b0FObGtvSlM5dTJna3c9PSIsInZhbHVlIjoiNUx3TnVqc0xxUWYxTHNLbDNLY2RkTDhrcXZua2xEbVJRaDAzYjVybVE5TUxQaUNEaE5mM0FsemxJUnhkOWNQbVU3aGs1Q1FFYU1POTRSZnRpR2swSUhRTVdMWTQxZ2xBSWpjSUk4UXFXamJFQ2dYM0dGSGdRQjVMRmZKdVQrY04iLCJtYWMiOiIxZGY2Y2NjNGE1OTBhNTE3NWRlZGVlNTdkOTZlNTc4ZDM1MjVhZWExZDlmMTAzYTdkN2M3OGRhYWJjYWZlMjY3In0%3D",
"domain": "192.168.3.89",
"path": "/",
"expires": 1776486919.581886,
"httpOnly": true,
"secure": false,
"sameSite": "Lax"
},
{
"name": "kodUserLanguage",
"value": "zh_CN",
"domain": "192.168.3.89",
"path": "/",
"expires": -1,
"httpOnly": false,
"secure": false,
"sameSite": "Lax"
}
],
"origins": [
{
"origin": "https://192.168.3.89",
"localStorage": [
{
"name": "auth.token",
"value": "eyJpdiI6IkdvQTMyZVU0eUZEZ1oycjJlT1NnXC9BPT0iLCJ2YWx1ZSI6InZMS3J2V0VwaEJGNENpTjI3MmltOWp0dUxpWUhjOVNOWjFCSFwvaGVLY09vPSIsIm1hYyI6IjAyZGIzZTFlOGU2NzI5OTFhMTJjNWFlMTc2MWRhNmU0ZjQwNzBlYmFkMDg1NWM5ZDdkNTAxMDA2OGY4ZjAwNDIifQ=="
},
{
"name": "i18n.language_code",
"value": "zh_CN"
},
{
"name": "auth.email",
"value": "[email protected]"
}
]
}
]
}
}
FILE:scripts/5gc_test_all.js
/**
* 5GC 技能全面回归测试
*
* 测试策略:
* - add 脚本:添加测试实例(用唯一名称),验证提交后URL跳转
* - edit 脚本:编辑已有实例,验证字段修改
* - bulk-edit 脚本:批量修改目标工程下所有实例
*
* 运行方式:
* node 5gc_test_all.js # 全部测试
* node 5gc_test_all.js --amf # 只测 AMF
* node 5gc_test_all.js --headed # 有头模式(可见浏览器)
*/
const { chromium } = require('playwright');
const { spawn } = require('child_process');
const path = require('path');
const fs = require('fs');
const SCRIPTS_DIR = path.join(__dirname);
const BASE_URL = 'https://192.168.3.89';
const LOGIN_EMAIL = '[email protected]';
const LOGIN_PWD = 'dotouch';
const TEST_PROJECT = 'XW_S5GC_1'; // 已知存在的工程
const TS = Date.now();
// ─── helpers ────────────────────────────────────────────────
async function login(page) {
await page.goto(BASE_URL + '/login', { waitUntil: 'networkidle', timeout: 15000 });
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill(LOGIN_EMAIL);
await page.getByRole('textbox', { name: '密码' }).fill(LOGIN_PWD);
await page.getByRole('checkbox', { name: '记住我' }).check();
await page.getByRole('button', { name: '登录' }).click();
await page.waitForLoadState('networkidle');
}
async function selectProjectFast(page, projectName) {
await page.goto(BASE_URL + '/sim_5gc/project/index', { waitUntil: 'networkidle', timeout: 15000 });
await page.waitForTimeout(2000);
for (let i = 0; i < 20; i++) {
const found = await page.evaluate((target) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === target) {
if (row.classList.contains('jsgrid-selected-row')) return 'already';
const ic = row.querySelector('.layui-icon');
if (ic) { ic.click(); return 'clicked'; }
row.click(); return 'clicked';
}
}
return 'not-found';
}, projectName);
if (found === 'already' || found === 'clicked') return true;
// 翻页
const hasNext = await page.evaluate(() => {
return Array.from(document.querySelectorAll('.jsgrid-pager a'))
.some(a => a.innerText.trim() === 'Next');
});
if (!hasNext) break;
await page.evaluate(() => {
const links = document.querySelectorAll('.jsgrid-pager a');
for (const l of links) { if (l.innerText.trim() === 'Next') { l.click(); break; } }
});
await page.waitForTimeout(1500);
}
return false;
}
function resultPath(name) {
return path.join(SCRIPTS_DIR, '..', 'test_results', `test_TS_name.json`);
}
function saveResult(name, ok, detail) {
const dir = path.join(SCRIPTS_DIR, '..', 'test_results');
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(resultPath(name), JSON.stringify({ name, ok, detail, ts: new Date().toISOString() }, null, 2));
}
function runChild(scriptFile, args) {
return new Promise((resolve) => {
const child = spawn('node', [scriptFile, ...args], { stdio: 'pipe', shell: true });
let out = '', err = '';
child.stdout.on('data', d => out += d.toString());
child.stderr.on('data', d => err += d.toString());
child.on('close', code => resolve({ code, out, err }));
});
}
// ─── 测试用例定义 ───────────────────────────────────────────
const TEST_CASES = [
// ── AMF ──────────────────────────────────────────────
{
tag: 'AMF添加', entity: 'amf', action: 'add', headed: false,
args: ['--name', `AMF_TEST_TS`, '--project', TEST_PROJECT,
'--sbi-ip', '10.200.1.1', '--mcc', '460', '--mnc', '01',
'--sst', '1', '--sd', '000001']
},
{
tag: 'AMF编辑', entity: 'amf', action: 'edit', headed: false,
args: ['--name', `AMF_TEST_TS`, '--project', TEST_PROJECT,
'--set-sbi_ip', '10.200.1.100']
},
// ── AUSF/UDM ─────────────────────────────────────────
{
tag: 'UDM添加', entity: 'udm', action: 'add', headed: false,
args: ['--name', `UDM_TEST_TS`, '--project', TEST_PROJECT,
'--sip', '10.200.5.1', '--port', '80']
},
{
tag: 'UDM编辑', entity: 'udm', action: 'edit', headed: false,
args: ['--name', `UDM_TEST_TS`, '--project', TEST_PROJECT,
'--set-op_opc', 'aaaaaaaaaaaabbbbccccccccccccdddd']
},
// ── SMF ──────────────────────────────────────────────
{
tag: 'SMF添加', entity: 'smf', action: 'add', headed: false,
args: ['--name', `SMF_TEST_TS`, '--project', TEST_PROJECT,
'--pfcp-ip', '10.200.2.1', '--n3-ip', '10.200.2.2',
'--n6-ip', '10.200.2.3', '--dnn', 'internet']
},
{
tag: 'SMF编辑', entity: 'smf', action: 'edit', headed: false,
args: ['--name', `SMF_TEST_TS`, '--project', TEST_PROJECT,
'--set-dnn', 'internet_updated']
},
{
tag: 'SMF批量编辑', entity: 'smf', action: 'edit', headed: false,
args: ['--project', TEST_PROJECT, '--set-dnn', 'internet']
},
// ── UPF ──────────────────────────────────────────────
{
tag: 'UPF添加', entity: 'upf', action: 'add', headed: false,
args: ['--name', `UPF_TEST_TS`, '--project', TEST_PROJECT,
'--n4-ip', '10.200.3.1', '--n3-ip', '10.200.3.2',
'--n6-ip', '10.200.3.3', '--dnn', 'internet']
},
{
tag: 'UPF编辑', entity: 'upf', action: 'edit', headed: false,
args: ['--name', `UPF_TEST_TS`, '--project', TEST_PROJECT,
'--set-n4_ip', '10.200.3.100']
},
{
tag: 'UPF批量编辑', entity: 'upf', action: 'edit', headed: false,
args: ['--project', TEST_PROJECT, '--set-n4_ip', '10.200.3.1']
},
// ── GNB ──────────────────────────────────────────────
{
tag: 'GNB添加', entity: 'gnb', action: 'add', headed: false,
args: ['--name', `GNB_TEST_TS`, '--project', TEST_PROJECT,
'--count', '1', '--ngap-ip', '10.200.4.1',
'--user-sip-ip-v4', '10.200.4.2', '--mcc', '460', '--mnc', '01',
'--stac', '1', '--etac', '100', '--node-id', 'gni_000004']
},
{
tag: 'GNB编辑', entity: 'gnb', action: 'edit', headed: false,
args: ['--name', `GNB_TEST_TS`, '--project', TEST_PROJECT,
'--set-replay_ip', '10.200.4.200']
},
{
tag: 'GNB批量编辑', entity: 'gnb', action: 'edit', headed: false,
args: ['--project', TEST_PROJECT, '--set-replay_ip', '10.200.4.200']
},
// ── UE ───────────────────────────────────────────────
{
tag: 'UE添加', entity: 'ue', action: 'add', headed: false,
args: ['--name', `UE_TEST_TS`, '--project', TEST_PROJECT,
'--imsi', `46000TS`.slice(0, 15), '--msisdn', '8613888888888',
'--mcc', '460', '--mnc', '01']
},
{
tag: 'UE编辑', entity: 'ue', action: 'edit', headed: false,
args: ['--project', TEST_PROJECT, '--name', `UE_TEST_TS`,
'--set-msisdn', '8613888889999']
},
{
tag: 'UE批量编辑', entity: 'ue', action: 'edit', headed: false,
args: ['--project', TEST_PROJECT, '--set-msisdn', '8613888888888']
},
];
// ─── 5gc.js 统一入口测试 ───────────────────────────────────
async function testViaCli(testCase, headed) {
const cliPath = path.join(SCRIPTS_DIR, '5gc.js');
const args = [testCase.entity, testCase.action, ...testCase.args];
if (headed) args.push('--headed');
const child = spawn('node', [cliPath, ...args], { stdio: 'pipe', shell: true });
let out = '', err = '';
child.stdout.on('data', d => out += d.toString());
child.stderr.on('data', d => err += d.toString());
return new Promise((resolve) => {
child.on('close', code => resolve({ code, out: out + err }));
});
}
function classifyRunResult(output, code) {
const text = String(output || '');
const hardFailPatterns = [
'异常',
'失败',
'❌',
'未找到工程',
'工程下没有',
'字段 dnn 未找到或无法填写',
'未找到:',
'未找到 UE:',
'未找到弹窗iframe',
'Timeout',
'timeout',
'found 0 ',
'请指定 ',
];
if (code !== 0) return { ok: false, reason: `exit=code` };
const hit = hardFailPatterns.find(p => text.includes(p));
if (hit) return { ok: false, reason: `pattern:hit` };
return { ok: true, reason: 'clean' };
}
// ─── 主测试运行器 ──────────────────────────────────────────
async function main() {
const argv = process.argv.slice(2);
const headed = argv.includes('--headed');
const filter = argv.find(a => !a.startsWith('--'));
// 登录一次,获取 playwright context
const browser = await chromium.launch({
headless: !headed,
args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*']
});
const ctx = await browser.newContext({ ignoreHTTPSErrors: true });
const page = await ctx.newPage();
await login(page);
const ok = await selectProjectFast(page, TEST_PROJECT);
if (!ok) { console.log('工程选择失败'); await browser.close(); return; }
const toRun = TEST_CASES.filter(t => !filter || t.tag.toLowerCase().includes(filter.toLowerCase()));
console.log(`\n========================================`);
console.log(`5GC 全面回归测试 (toRun.length 个用例, headed=headed)`);
console.log(`========================================\n`);
const results = [];
for (const tc of toRun) {
process.stdout.write(`tc.tag.padEnd(20) ... `);
// 每次重走工程选择(部分脚本内部会切工程)
try {
const r = await testViaCli(tc, headed);
const judged = classifyRunResult(r.out, r.code);
const ok = judged.ok;
console.log(ok ? '✅ PASS' : '❌ FAIL');
if (!ok) console.log(' 输出:', r.out.substring(0, 200).replace(/\n/g, ' '));
results.push({ ...tc, pass: ok, detail: r.out.substring(0, 300), reason: judged.reason, exitCode: r.code });
} catch (e) {
console.log('❌ ERROR:', e.message);
results.push({ ...tc, pass: false, detail: e.message });
}
// 恢复工程上下文
await selectProjectFast(page, TEST_PROJECT).catch(() => {});
}
await browser.close();
// 汇总
const pas = results.filter(r => r.pass).length;
const fail = results.filter(r => !r.pass).length;
console.log(`\n========================================`);
console.log(`结果: pas 通过 / fail 失败`);
if (fail > 0) {
console.log('\n失败用例:');
results.filter(r => !r.pass).forEach(r => console.log(` ❌ r.tag: r.detail.substring(0, 100)`));
}
console.log('========================================\n');
// 保存详细结果
const reportDir = path.join(SCRIPTS_DIR, '..', 'test_results');
if (!fs.existsSync(reportDir)) fs.mkdirSync(reportDir, { recursive: true });
const reportFile = path.join(reportDir, `report_TS.json`);
fs.writeFileSync(reportFile, JSON.stringify({ ts: new Date().toISOString(), results }, null, 2));
console.log(`详细报告: reportFile`);
}
main().catch(e => { console.error('Fatal:', e.message); process.exit(1); });
FILE:scripts/amf-add-skill.js
#!/usr/bin/env node
/**
* AMF 添加脚本 - 完整修复版
* 功能:登录状态缓存 + .projectSelect 选工程 + evaluate 填写表单 + 算法全勾选 + NSSAI
*/
const { chromium } = require('playwright');
let globalBaseUrl = 'https://192.168.3.89';
const fs = require('fs');
const path = require('path');
// 配置
const CONFIG = {
urls: {
login: '/login',
amfEdit: '/sim_5gc/amf/edit',
amfManagement: '/sim_5gc/amf/index'
},
credentials: {
email: '[email protected]',
password: 'dotouch'
},
sessionDir: path.join(__dirname, '.sessions'),
getSessionFile() {
const host = globalBaseUrl.replace(/https?:\/\//, '').replace(/\./g, '_');
return `5gc_session_host.json`;
}
};
// 会话管理
class SessionManager {
constructor() {
this.sessionPath = path.join(CONFIG.sessionDir, CONFIG.getSessionFile());
}
async saveSession(context) {
try {
const storageState = await context.storageState();
fs.writeFileSync(this.sessionPath, JSON.stringify({ storageState }, null, 2));
return true;
} catch {
return false;
}
}
async loadSession(browser) {
try {
if (!fs.existsSync(this.sessionPath)) return null;
const { storageState } = JSON.parse(fs.readFileSync(this.sessionPath, 'utf8'));
return await browser.newContext({ storageState, ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
} catch {
return null;
}
}
}
// 算法配置:直接点击 layui 复选框的可见元素
async function configureAlgorithmsSuccess(page) {
await page.waitForSelector('.layui-form-checkbox', { timeout: 5000 });
await page.waitForTimeout(300);
const checkboxCount = await page.locator('.layui-form-checkbox').count();
console.log(` 算法复选框数量: checkboxCount`);
for (let i = 0; i < Math.min(checkboxCount, 8); i++) {
await page.locator('.layui-form-checkbox').nth(i).click();
await page.waitForTimeout(80);
}
const priorities = [
'ea[NEA0]', 'ea[128-NEA1]', 'ea[128-NEA2]', 'ea[128-NEA3]',
'ia[NIA0]', 'ia[128-NIA1]', 'ia[128-NIA2]', 'ia[128-NIA3]'
];
const vals = ['1', '2', '3', '4', '1', '2', '3', '4'];
for (let i = 0; i < priorities.length; i++) {
const inp = page.locator(`input[name="priorities[i]"]`);
if (await inp.count() > 0) {
await inp.fill(vals[i]);
}
}
console.log(` ✅ 算法配置完成`);
}
// 工程选择(精确匹配,分页遍历)
async function selectProject(page, projectName, forceSwitch = true) {
if (!forceSwitch) {
console.log(` 🔧 保持当前工程(用户未指定工程)`);
return true;
}
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', timeout: 15000 });
await page.waitForSelector('.jsgrid-row, .jsgrid-alt-row', { timeout: 5000 }).catch(() => {});
await page.evaluate(() => {
const inputs = document.querySelectorAll('input[type="text"], input[name="name"]');
for (const inp of inputs) { inp.value = ''; }
});
await page.waitForTimeout(300);
for (let pageNum = 1; pageNum <= 200; pageNum++) {
const clicked = await page.evaluate((targetName) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === targetName) {
const icon = cells[1].querySelector('.iconfont');
if (icon) {
icon.click();
return true;
}
}
}
return false;
}, projectName);
if (clicked) {
await page.waitForTimeout(2000);
return true;
}
const nextBtn = page.locator('.jsgrid-pager a:has-text("Next")');
if (!(await nextBtn.count())) break;
try {
REPLACED
} catch (e) {
break;
}
}
console.log(` ❌ 未找到工程 "projectName"(精确匹配)`);
return false;
}
// 添加 AMF 主流程
async function addAmf(amfName, projectName, explicitProject = true, amfConfig = {}) {
const startTime = Date.now();
const sessionManager = new SessionManager();
const defaultConfig = {
mcc: '460', mnc: '01', region_id: '1', set_id: '1', pointer: '1',
ngap_sip: '200.20.20.1', ngap_port: '38412',
http2_sip: '200.20.20.5', http2_port: '8080',
stac: '101', etac: '102'
};
const cfg = { ...defaultConfig, ...amfConfig };
let browser = null;
try {
browser = await chromium.launch({ headless: true, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
let context = await sessionManager.loadSession(browser);
let needLogin = true;
if (context) {
const testPage = await context.newPage();
await testPage.goto(`globalBaseUrlCONFIG.urls.amfManagement`, { waitUntil: 'networkidle', timeout: 10000 }).catch(() => {});
if (!testPage.url().includes('/login')) {
needLogin = false;
}
await testPage.close();
}
if (needLogin) {
context = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
}
const page = await context.newPage();
if (needLogin) {
await page.goto(`globalBaseUrlCONFIG.urls.login`, { waitUntil: 'networkidle', timeout: 15000 });
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill(CONFIG.credentials.email);
await page.getByRole('textbox', { name: '密码' }).fill(CONFIG.credentials.password);
await page.getByRole('button', { name: '登录' }).click();
await page.waitForLoadState('networkidle', { timeout: 10000 });
await sessionManager.saveSession(context);
}
// 选择工程(仅当用户显式指定工程时才切换)
if (!(await selectProject(page, projectName, explicitProject))) {
throw new Error(`工程 "projectName" 不存在或无法选中`);
}
// 进入编辑页面
await page.goto(`globalBaseUrlCONFIG.urls.amfEdit`, { waitUntil: 'networkidle', timeout: 15000 });
if (!page.url().includes('/amf/edit')) {
await page.goto(`globalBaseUrl/sim_5gc/amf/edit`);
await page.waitForSelector('input[name="name"]', { timeout: 10000 });
}
// 通过 evaluate 直接填写表单
await page.evaluate(({ amfName, cfg }) => {
const set = (name, value) => {
const el = document.querySelector(`input[name="name"]`);
if (el) {
el.value = value;
el.dispatchEvent(new Event('input', { bubbles: true }));
}
};
set('name', amfName);
set('mcc', cfg.mcc);
set('mnc', cfg.mnc);
set('region_id', cfg.region_id);
set('set_id', cfg.set_id);
set('pointer', cfg.pointer);
set('ngap_sip', cfg.ngap_sip);
set('ngap_port', cfg.ngap_port);
set('http2_sip', cfg.http2_sip);
set('http2_port', cfg.http2_port);
set('stac', cfg.stac);
set('etac', cfg.etac);
}, { amfName, cfg });
// 类型选择:仿真设备
await page.locator('.layui-unselect').first().click();
await page.waitForTimeout(300);
await page.locator('dd').filter({ hasText: '仿真设备' }).click();
// 配置算法
await configureAlgorithmsSuccess(page);
// 配置 NSSAI
await page.getByRole('row', { name: /数量.*nssai/ }).getByRole('button').click();
await page.waitForTimeout(500);
await page.locator('input[name="config[count][]"]').fill('1');
await page.getByRole('row', { name: /nssai.*添加.*删除/ }).locator('span').click();
await page.waitForTimeout(800);
const iframeEl = page.locator('iframe[name="layui-layer-iframe2"]');
const iframe = await iframeEl.contentFrame({ timeout: 5000 });
await iframe.getByRole('row', { name: /\*.*SST.*SD/ }).getByRole('button').click();
await iframe.locator('input[name="nssai[snssai_sst][]"]').fill('1');
await iframe.locator('input[name="nssai[snssai_sd][]"]').fill('111111');
await iframe.getByRole('button', { name: '提交' }).click();
await page.waitForTimeout(800);
// 提交表单
await page.getByRole('button', { name: '提交' }).click();
// 等待页面跳转到 AMF 列表页面,若未跳转则强制跳转
try {
await page.waitForURL(`**/amf/index`, { timeout: 8000 });
} catch (e) {
await page.goto(`globalBaseUrlCONFIG.urls.amfManagement`, { waitUntil: 'networkidle', timeout: 15000 });
}
await page.waitForTimeout(2000);
// 验证结果:只要页面成功跳转到 AMF 列表页,即认为添加成功
let found = false;
const finalUrl = page.url();
if (finalUrl.includes('/amf/index')) {
console.log(` ✅ 页面已跳转至 AMF 列表: finalUrl`);
found = true;
}
await browser.close();
const totalTime = (Date.now() - startTime) / 1000;
if (found) {
return { success: true, amfName, totalTime };
} else {
return { success: false, amfName, totalTime };
}
} catch (err) {
if (browser) await browser.close();
throw err;
}
}
// 主函数
async function main() {
const args = process.argv.slice(2);
if (args.length === 0) {
console.log('用法: node amf-add-skill.js <AMF名称> [--project <工程名>] [--url <地址>] [--mcc 460] [...]');
process.exit(1);
}
let amfName = null;
let projectName = '5G_basic_process';
let amfConfig = {};
let explicitProject = false;
for (let i = 0; i < args.length; i++) {
if (!args[i].startsWith('-')) {
amfName = args[i];
} else if (args[i] === '--project' || args[i] === '-p') {
projectName = args[++i];
explicitProject = true;
} else if (args[i] === '--url') {
let u = args[++i];
if (u && !u.startsWith('http')) u = 'https://' + u;
globalBaseUrl = u;
} else if (args[i].startsWith('--')) {
amfConfig[args[i].substring(2)] = args[++i];
}
}
if (!amfName) {
console.error('错误: 请指定 AMF 名称');
process.exit(1);
}
console.log(`AMF: amfName | 工程: projectName | 地址: globalBaseUrl`);
try {
const result = await addAmf(amfName, projectName, explicitProject, amfConfig);
console.log(result.success
? `成功! AMF "result.amfName" 添加完成 (result.totalTime.toFixed(2)s)`
: `失败! 未找到 AMF "result.amfName"`);
process.exit(result.success ? 0 : 1);
} catch (err) {
console.error(`执行异常: err.message`);
process.exit(1);
}
}
main();
FILE:scripts/amf-edit-skill.js
#!/usr/bin/env node
/**
* 🚀 AMF配置修改技能 - 智能版本
* 特性:
* 1. 智能AMF选择逻辑
* 2. 基于文字匹配,不使用固定ID
* 3. 支持部分字段修改
* 4. 继承登录优化和成功算法配置
*/
const { chromium } = require('playwright');
const fs = require('fs');
const path = require('path');
// 配置
let globalBaseUrl = 'https://192.168.3.89';
const CONFIG = {
baseUrl: globalBaseUrl,
urls: {
login: '/login',
amfManagement: '/sim_5gc/amf/index',
amfEdit: '/sim_5gc/amf/edit'
},
credentials: {
email: '[email protected]',
password: 'dotouch'
},
sessionDir: path.join(__dirname, '.sessions'),
// 动态session文件名:根据URL生成,避免跨仪表缓存冲突
getSessionFile: function() {
const host = this.baseUrl.replace(/https?:\/\//, '').replace(/\./g, '_');
return `5gc_session_host.json`;
}
};
// 会话管理器(复用登录优化)
class SessionManager {
constructor() {
this.sessionPath = path.join(CONFIG.sessionDir, CONFIG.getSessionFile());
}
async saveSession(context) {
try {
const storageState = await context.storageState();
fs.writeFileSync(this.sessionPath, JSON.stringify({ storageState }, null, 2));
return true;
} catch (error) {
return false;
}
}
async loadSession(browser) {
try {
if (!fs.existsSync(this.sessionPath)) return null;
const { storageState } = JSON.parse(fs.readFileSync(this.sessionPath, 'utf8'));
const context = await browser.newContext({
storageState,
ignoreHTTPSErrors: true,
viewport: { width: 1920, height: 1080 }
});
return context;
} catch (error) {
return null;
}
}
}
// 智能AMF选择器
class AMFSelector {
constructor(page) {
this.page = page;
}
// 获取AMF列表
async getAMFList() {
console.log('🔍 获取AMF列表...');
await this.page.goto(`CONFIG.baseUrlCONFIG.urls.amfManagement`, {
waitUntil: 'networkidle',
timeout: 15000
});
await this.page.waitForTimeout(1000);
// 查找AMF表格
const tableSelector = 'table.layui-table';
const tableExists = await this.page.locator(tableSelector).count() > 0;
if (!tableExists) {
console.log('⚠️ 未找到AMF表格,可能没有配置或页面结构不同');
// 调试:查看页面结构
const pageText = await this.page.textContent('body').catch(() => '');
console.log('📄 页面内容片段:', pageText.substring(0, 300));
// 查找其他可能的表格
const allTables = await this.page.locator('table').all();
console.log(`🔍 找到 allTables.length 个表格`);
for (let i = 0; i < allTables.length; i++) {
const table = allTables[i];
const tableHtml = await table.innerHTML().catch(() => '');
console.log(`表格 i + 1 片段:`, tableHtml.substring(0, 200));
}
return [];
}
// 调试:查看表格内容
const tableHtml = await this.page.locator(tableSelector).innerHTML().catch(() => '');
console.log('📊 表格HTML片段:', tableHtml.substring(0, 500));
// 提取AMF名称 - 使用 layui-table 单元格直接定位(稳定可靠)
// layui-table 列结构: 复选框(0) | ID(1) | 名称(2) | NGAP IP(3) | HTTP2 IP(4) | ...
const rowSelector = 'table.layui-table tbody tr, .layui-table-body tbody tr';
const amfRows = await this.page.locator(rowSelector).all();
const amfList = [];
console.log(`📊 找到 amfRows.length 行数据`);
for (let i = 0; i < amfRows.length; i++) {
const row = amfRows[i];
// layui-table 列: 复选框(0) | ID(1) | 名称(2) | NGAP IP(3) | HTTP2 IP(4) | 端口(5) | ...
const cells = await row.locator('td').all();
if (cells.length < 3) continue;
const idCell = await cells[1].textContent().catch(() => '');
const nameCell = await cells[2].textContent().catch(() => '');
const id = idCell.trim();
const name = nameCell.trim();
if (!id || !id.match(/^\d+$/) || !name || name === '名称' || name === '编辑') continue;
amfList.push({ id, name, row });
console.log(` ✅ 提取到: ID=id, 名称=name`);
}
// 去重
const uniqueAmfList = [];
const seenNames = new Set();
for (const amf of amfList) {
if (!seenNames.has(amf.name)) {
seenNames.add(amf.name);
uniqueAmfList.push(amf);
}
}
console.log(`📋 找到 uniqueAmfList.length 个AMF配置:`);
uniqueAmfList.forEach((amf, idx) => {
console.log(` idx + 1. [amf.id] amf.name`);
});
return uniqueAmfList;
}
// 智能选择AMF
async selectAMF(amfName = null) {
const amfList = await this.getAMFList();
if (amfList.length === 0) {
throw new Error('未找到任何AMF配置,请先添加AMF');
}
// 情况1: 用户指定了AMF名称
if (amfName) {
console.log(`🎯 用户指定AMF名称: "amfName"`);
// 精确匹配
const exactMatch = amfList.find(amf => amf.name === amfName);
if (exactMatch) {
console.log(`✅ 找到精确匹配: exactMatch.name`);
return exactMatch;
}
// 模糊匹配(包含)
const fuzzyMatches = amfList.filter(amf => amf.name.includes(amfName));
if (fuzzyMatches.length === 1) {
console.log(`✅ 找到模糊匹配: fuzzyMatches[0].name`);
return fuzzyMatches[0];
} else if (fuzzyMatches.length > 1) {
console.log(`⚠️ 找到多个模糊匹配:`);
fuzzyMatches.forEach((amf, idx) => {
console.log(` idx + 1. amf.name`);
});
throw new Error(`找到多个匹配的AMF,请指定更精确的名称`);
}
throw new Error(`未找到名称为"amfName"的AMF配置`);
}
// 情况2: 未指定名称,但只有一个AMF
if (amfList.length === 1) {
console.log(`✅ 只有一个AMF配置,自动选择: amfList[0].name`);
return amfList[0];
}
// 情况3: 多个AMF,未指定名称 → 返回全部(批量模式)
console.log(`⚠️ 未指定AMF名称,找到 amfList.length 个AMF → 进入批量修改模式`);
amfList.forEach((amf, idx) => {
console.log(` idx + 1. [amf.id] amf.name`);
});
return amfList; // 返回数组,editAMF 将检测到并进入批量
}
// 点击编辑按钮进入编辑页面 - 优先使用URL直接访问
async clickEditButton(selectedAMF) {
console.log(`🖱️ 进入编辑页面: selectedAMF.name`);
// 方法1: 直接用ID访问编辑页面(最稳定)
if (selectedAMF.id) {
console.log(` 直接访问: /sim_5gc/amf/edit/selectedAMF.id`);
await this.page.goto(`CONFIG.baseUrl/sim_5gc/amf/edit/selectedAMF.id`);
await this.page.waitForSelector('input[name="name"]', { timeout: 10000 });
await this.page.waitForTimeout(1000);
console.log('✅ 通过URL进入编辑页面');
return true;
}
// 方法2: 使用编辑按钮(备用)
if (selectedAMF.editButton) {
try {
await selectedAMF.editButton.click({ timeout: 5000 });
await this.page.waitForLoadState('networkidle', { timeout: 10000 });
await this.page.waitForTimeout(1000);
if (this.page.url().includes('/amf/edit')) {
console.log('✅ 通过编辑按钮进入编辑页面');
return true;
}
} catch (error) {
console.log(`⚠️ 编辑按钮点击失败: error.message`);
}
}
// 方法3: 查找所有编辑按钮,按索引点击
console.log('🔍 查找所有编辑按钮...');
const allEditButtons = await this.page.locator('a:has-text("编辑"), button:has-text("编辑")').all();
console.log(`找到 allEditButtons.length 个编辑按钮`);
if (allEditButtons.length > selectedAMF.index) {
try {
await allEditButtons[selectedAMF.index].click({ timeout: 5000 });
await this.page.waitForLoadState('networkidle', { timeout: 10000 });
await this.page.waitForTimeout(1000);
if (this.page.url().includes('/amf/edit')) {
console.log(`✅ 通过索引 selectedAMF.index 进入编辑页面`);
return true;
}
} catch (error) {
console.log(`⚠️ 索引点击失败: error.message`);
}
}
// 方法3: 直接构造编辑URL(RESTful风格)
console.log('🔗 尝试直接访问编辑URL...');
if (selectedAMF.id) {
// 正确的URL格式: /amf/edit/6084 (不是 /amf/edit?id=6084)
const editUrl = `CONFIG.baseUrl/sim_5gc/amf/edit/selectedAMF.id`;
console.log(`访问: editUrl`);
await this.page.goto(editUrl, {
waitUntil: 'networkidle',
timeout: 15000
});
await this.page.waitForTimeout(1000);
if (this.page.url().includes('/amf/edit')) {
console.log('✅ 通过直接URL进入编辑页面');
return true;
}
}
// 方法4: 使用标准编辑页面URL
console.log('🌐 尝试标准编辑页面...');
await this.page.goto(`CONFIG.baseUrlCONFIG.urls.amfEdit`, {
waitUntil: 'networkidle',
timeout: 15000
});
await this.page.waitForTimeout(1000);
if (this.page.url().includes('/amf/edit')) {
console.log('✅ 通过标准URL进入编辑页面');
return true;
}
throw new Error('无法进入编辑页面,请检查页面结构或权限');
}
}
// 配置修改器
class ConfigModifier {
constructor(page) {
this.page = page;
}
// 修改配置字段
async modifyConfig(fieldUpdates = {}) {
console.log('🔧 修改配置字段...');
if (Object.keys(fieldUpdates).length === 0) {
console.log('⚠️ 没有需要修改的字段,直接提交');
return;
}
// 等待页面加载完成
await this.page.waitForLoadState('networkidle', { timeout: 10000 });
// 修改字段
for (const [fieldName, newValue] of Object.entries(fieldUpdates)) {
await this.updateField(fieldName, newValue);
}
console.log('✅ 配置修改完成');
}
// 更新单个字段
async updateField(fieldName, newValue) {
console.log(` 📝 修改 fieldName: newValue`);
// 常见字段映射
const fieldSelectors = {
// 基础字段
name: 'input[name="name"]',
mcc: 'input[name="mcc"]',
mnc: 'input[name="mnc"]',
region_id: 'input[name="region_id"]',
set_id: 'input[name="set_id"]',
pointer: 'input[name="pointer"]',
ngap_sip: 'input[name="ngap_sip"]',
ngap_port: 'input[name="ngap_port"]',
http2_sip: 'input[name="http2_sip"]',
http2_port: 'input[name="http2_port"]',
stac: 'input[name="stac"]',
etac: 'input[name="etac"]',
// 算法字段
'ea[NEA0]': 'input[name="ea[NEA0]"]',
'ea[128-NEA1]': 'input[name="ea[128-NEA1]"]',
'ea[128-NEA2]': 'input[name="ea[128-NEA2]"]',
'ea[128-NEA3]': 'input[name="ea[128-NEA3]"]',
'ia[NIA0]': 'input[name="ia[NIA0]"]',
'ia[128-NIA1]': 'input[name="ia[128-NIA1]"]',
'ia[128-NIA2]': 'input[name="ia[128-NIA2]"]',
'ia[128-NIA3]': 'input[name="ia[128-NIA3]"]',
// 设备类型(特殊处理)
deviceType: 'input[placeholder*="选择"], input[name*="device"]'
};
// 设备类型特殊处理(支持仿真设备/被测设备)
if (fieldName === 'deviceType' || fieldName === 'type') {
try {
await this.page.locator('.layui-unselect').first().click();
await this.page.waitForTimeout(300);
const targetLabel = newValue === '被测设备' ? '被测设备' : '仿真设备';
await this.page.locator('dd').filter({ hasText: targetLabel }).click();
console.log(` ✅ deviceType 已设置为 targetLabel`);
} catch (error) {
console.log(` ❌ deviceType 设置失败: error.message`);
}
return;
}
// 算法字段特殊处理(点击 layui 可见复选框,与 amf-add 保持一致)
if (fieldName.startsWith('ea[') || fieldName.startsWith('ia[')) {
try {
const algoMap = {
'ea[NEA0]': 0, 'ea[128-NEA1]': 1, 'ea[128-NEA2]': 2, 'ea[128-NEA3]': 3,
'ia[NIA0]': 4, 'ia[128-NIA1]': 5, 'ia[128-NIA2]': 6, 'ia[128-NIA3]': 7
};
const idx = algoMap[fieldName];
if (idx !== undefined) {
await this.page.locator('.layui-form-checkbox').nth(idx).click();
await this.page.waitForTimeout(80);
const inp = this.page.locator(`input[name="fieldName"]`);
if (await inp.count() > 0) {
await inp.fill(newValue.toString());
}
console.log(` ✅ fieldName 已勾选并设置优先级为 newValue`);
}
} catch (error) {
console.log(` ❌ fieldName 设置失败: error.message`);
}
return;
}
let selector = fieldSelectors[fieldName];
if (!selector) {
// 尝试通用查找
selector = `input[name*="fieldName"], input[placeholder*="fieldName"]`;
}
try {
const field = this.page.locator(selector).first();
const count = await field.count();
if (count > 0) {
await field.click();
await field.fill('');
await field.fill(newValue.toString());
await this.page.waitForTimeout(100);
console.log(` ✅ fieldName 修改成功`);
} else {
console.log(` ⚠️ 未找到字段: fieldName`);
}
} catch (error) {
console.log(` ❌ 修改 fieldName 失败: error.message`);
}
}
// 提交修改
async submitChanges() {
console.log('📤 提交修改...');
const beforeUrl = this.page.url();
// 查找提交按钮
const submitButtons = await this.page.locator('button:has-text("提交"), input[type="submit"][value*="提交"]').all();
if (submitButtons.length === 0) {
throw new Error('未找到提交按钮');
}
await submitButtons[0].click();
await this.page.waitForTimeout(5000);
const afterUrl = this.page.url();
const urlChanged = afterUrl !== beforeUrl;
if (urlChanged) {
console.log('✅ 提交成功,URL已变化');
} else {
console.log('⚠️ URL未变化,但可能已提交成功');
}
return urlChanged;
}
}
/**
* 选择工程函数(支持翻页)
* @param {Page} page - Playwright页面对象
* @param {string} projectName - 工程名称
* @param {boolean} forceSwitch - 是否强制切换工程
*/
async function selectProject(page, projectName, forceSwitch = true) {
if (!forceSwitch) {
console.log(` 🔧 保持当前工程(用户未指定工程)`);
return true;
}
await page.goto(`CONFIG.baseUrl/sim_5gc/project/index`, {
waitUntil: 'networkidle',
timeout: 15000
});
await page.waitForSelector('.jsgrid-row, .jsgrid-alt-row', { timeout: 5000 }).catch(() => {});
// 清除搜索框,避免残留内容干扰
await page.evaluate(() => {
const inputs = document.querySelectorAll('input[type="text"], input[name="name"]');
for (const inp of inputs) { inp.value = ''; }
});
await page.waitForTimeout(300);
for (let pageNum = 1; pageNum <= 200; pageNum++) {
// 通过 evaluate 点击操作列 td:nth(1) 里的 ● 图标(iconfont layui-ext-xuanzhong1)
const clicked = await page.evaluate((targetName) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === targetName) {
// 操作列 td:nth(1) 里的 iconfont 元素是●选择图标
const icon = cells[1].querySelector('.iconfont');
if (icon) {
icon.click();
return true;
}
}
}
return false;
}, projectName);
if (clicked) {
await page.waitForTimeout(2000);
return true;
}
const nextBtn = page.locator('.jsgrid-pager a:has-text("Next")');
if (!(await nextBtn.count())) break;
try {
await page.evaluate(() => { var links = document.querySelectorAll('.jsgrid-pager a'); for (var i = 0; i < links.length; i++) { if (links[i].innerText.trim() === 'Next') { links[i].click(); break; } } });
await page.waitForTimeout(2000);
} catch { break; }
}
console.log(` ❌ 未找到工程 "projectName"(精确匹配)`);
return false;
}
// 主执行函数
async function editAMF(amfName = null, projectName = '5G_basic_process', fieldUpdates = {}) {
console.log('='.repeat(60));
console.log('🚀 AMF配置修改技能 - 智能版本');
console.log('='.repeat(60));
const startTime = Date.now();
const sessionManager = new SessionManager();
let browser = null;
let skippedLogin = false;
try {
// 1. 初始化浏览器
console.log('1. 初始化...');
browser = await chromium.launch({
headless: true,
args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*']
});
// 2. 尝试加载会话
let context = await sessionManager.loadSession(browser);
if (context) {
console.log('✅ 使用缓存会话,跳过登录');
skippedLogin = true;
} else {
console.log('📝 需要登录');
context = await browser.newContext({
ignoreHTTPSErrors: true,
viewport: { width: 1920, height: 1080 }
});
}
const page = await context.newPage();
// 3. 登录(如果需要)
if (!skippedLogin) {
console.log('2. 登录...');
await page.goto(`CONFIG.baseUrlCONFIG.urls.login`, {
waitUntil: 'networkidle',
timeout: 15000
});
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill(CONFIG.credentials.email);
await page.getByRole('textbox', { name: '密码' }).fill(CONFIG.credentials.password);
await page.getByRole('button', { name: '登录' }).click();
await page.waitForLoadState('networkidle', { timeout: 10000 });
await page.waitForTimeout(1000);
// 保存会话
await sessionManager.saveSession(context);
console.log('✅ 登录成功,会话已保存');
}
// 4. 选择工程
console.log('3. 选择工程...');
if (!(await selectProject(page, projectName, true))) {
throw new Error(`工程 "projectName" 不存在或无法选中`);
}
// 4. 智能选择AMF(支持单条和批量)
console.log('4. 智能选择AMF...');
const selector = new AMFSelector(page);
const selected = await selector.selectAMF(amfName);
// 5. 进入编辑页面(单条或批量)
console.log('5. 进入编辑页面...');
// 批量模式:selected 是 AMF 数组
if (Array.isArray(selected)) {
let batchSuccess = 0;
for (const amf of selected) {
process.stdout.write(` ▶ amf.name [amf.id] ... `);
try {
await selector.clickEditButton(amf);
const modifier = new ConfigModifier(page);
await modifier.modifyConfig(fieldUpdates);
const urlChanged = await modifier.submitChanges();
await page.waitForTimeout(2000);
const ok = page.url().includes('/amf/index');
console.log(ok ? '✅' : '❌');
if (ok) batchSuccess++;
// 返回列表页继续下一个
await page.goto(`CONFIG.baseUrlCONFIG.urls.amfManagement`, { waitUntil: 'networkidle', timeout: 10000 });
await page.waitForTimeout(1500);
} catch (e) {
console.log(`❌ e.message`);
}
await page.waitForTimeout(500);
}
await browser.close();
const totalTime = (Date.now() - startTime) / 1000;
console.log(`\n批量完成: batchSuccess/selected.length 成功 耗时: totalTime.toFixed(1)s`);
return { success: batchSuccess > 0, batchSummary: { total: selected.length, success: batchSuccess } };
}
// 单条模式
const selectedAMF = selected;
await selector.clickEditButton(selectedAMF);
// 验证是否在编辑页面
if (!page.url().includes('/amf/edit')) {
console.log('⚠️ 当前URL:', page.url());
console.log('尝试直接访问编辑页面...');
await page.goto(`CONFIG.baseUrlCONFIG.urls.amfEdit`, {
waitUntil: 'networkidle',
timeout: 10000
});
}
console.log('✅ 进入编辑页面');
// 7. 修改配置
console.log('6. 修改配置...');
const modifier = new ConfigModifier(page);
await modifier.modifyConfig(fieldUpdates);
// 8. 提交修改
console.log('7. 提交修改...');
const urlChanged = await modifier.submitChanges();
// 9. 验证结果
console.log('8. 验证结果...');
await page.goto(`CONFIG.baseUrlCONFIG.urls.amfManagement`, {
waitUntil: 'networkidle',
timeout: 10000
});
await page.waitForTimeout(2000);
const pageText = await page.textContent('body').catch(() => '');
const found = pageText.includes(selectedAMF.name);
// 9. 清理
await browser.close();
const totalTime = (Date.now() - startTime) / 1000;
console.log('\n' + '='.repeat(60));
console.log('📊 执行结果');
console.log('='.repeat(60));
if (found) {
console.log(`✅ 成功!AMF "selectedAMF.name" 修改完成`);
console.log(`⏱️ 总耗时: totalTime.toFixed(2)秒`);
console.log(`🔗 URL变化: '否'`);
console.log(`🔑 登录优化: '重新登录'`);
console.log(`📝 修改字段: Object.keys(fieldUpdates).length 个`);
if (Object.keys(fieldUpdates).length > 0) {
console.log('修改详情:');
Object.entries(fieldUpdates).forEach(([field, value]) => {
console.log(` - field: value`);
});
}
return {
success: true,
amfName: selectedAMF.name,
totalTime: totalTime,
urlChanged: urlChanged,
skippedLogin: skippedLogin,
modifiedFields: Object.keys(fieldUpdates)
};
} else {
console.log(`❌ 失败!未找到配置 "selectedAMF.name"`);
console.log(`⏱️ 总耗时: totalTime.toFixed(2)秒`);
return {
success: false,
amfName: selectedAMF.name,
totalTime: totalTime
};
}
} catch (error) {
console.error(`❌ 执行失败: error.message`);
if (browser) {
await browser.close();
}
return {
success: false,
amfName: amfName,
error: error.message,
totalTime: (Date.now() - startTime) / 1000
};
}
}
// 参数解析
function parseArgs() {
const args = process.argv.slice(2);
const result = {
amfName: null,
projectName: '5G_basic_process', // 默认工程
baseUrl: globalBaseUrl, // 默认5GC地址
fieldUpdates: {}
};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg.startsWith('--')) {
const key = arg.substring(2);
if (i + 1 < args.length && !args[i + 1].startsWith('--')) {
// 字段修改参数: --field value
if (key.startsWith('set-')) {
const fieldName = key.substring(4);
result.fieldUpdates[fieldName] = args[i + 1];
i++;
} else if (key === 'amf' || key === 'name') {
result.amfName = args[i + 1];
i++;
} else if (key === 'project' || key === 'p') {
result.projectName = args[i + 1];
i++;
} else if (key === 'url') {
result.baseUrl = args[i + 1];
if (!result.baseUrl.startsWith('http')) result.baseUrl = 'https://' + result.baseUrl;
globalBaseUrl = result.baseUrl;
CONFIG.baseUrl = result.baseUrl; // 同步更新CONFIG
i++;
}
} else if (key === 'project' || key === 'p') {
// --project 但没有指定值,使用默认值
console.log('⚠️ 使用了--project参数但未指定工程名称,将使用默认工程');
}
} else if (i === 0 && !arg.startsWith('--')) {
// 第一个非选项参数作为AMF名称
result.amfName = arg;
}
}
return result;
}
// 帮助信息
function showHelp() {
console.log('='.repeat(60));
console.log('🚀 AMF配置修改技能 - 使用说明');
console.log('='.repeat(60));
console.log('\n📋 功能特性:');
console.log(' 1. 智能工程选择(支持默认/指定工程)');
console.log(' 2. 智能AMF选择(按名称匹配)');
console.log(' 3. 支持部分字段修改');
console.log(' 4. 登录状态优化(会话缓存)');
console.log(' 5. 基于文字匹配,不使用固定ID');
console.log('\n🚀 使用方法:');
console.log(' node amf-edit-skill.js <AMF名称> [选项]');
console.log(' node amf-edit-skill.js --amf <名称> [选项]');
console.log('\n🎯 示例:');
console.log(' # 修改指定AMF的IP地址');
console.log(' node amf-edit-skill.js qqqwww --set-ngap_sip 192.168.99.99');
console.log(' # 修改多个字段');
console.log(' node amf-edit-skill.js test_001 --set-ngap_sip 10.0.0.1 --set-http2_sip 10.0.0.2');
console.log(' # 自动选择(只有一个AMF时)');
console.log(' node amf-edit-skill.js --set-stac 200');
console.log('\n🔧 可修改字段:');
console.log(' --set-name <新名称> 修改AMF名称');
console.log(' --set-mcc <值> 修改MCC');
console.log(' --set-mnc <值> 修改MNC');
console.log(' --set-region_id <值> 修改Region ID');
console.log(' --set-set_id <值> 修改Set ID');
console.log(' --set-pointer <值> 修改Pointer');
console.log(' --set-ngap_sip <IP> 修改NGAP SIP地址');
console.log(' --set-ngap_port <端口> 修改NGAP端口');
console.log(' --set-http2_sip <IP> 修改HTTP2 SIP地址');
console.log(' --set-http2_port <端口> 修改HTTP2端口');
console.log(' --set-stac <值> 修改起始TAC');
console.log(' --set-etac <值> 修改结束TAC');
console.log(' --set-ea[NEA0] <优先级> 修改加密算法NEA0');
console.log(' --set-ea[128-NEA1] <优先级> ...');
console.log(' --set-ia[NIA0] <优先级> 修改完保算法NIA0');
console.log(' --set-deviceType <类型> 修改设备类型(仿真设备 / 被测设备)');
console.log('\n🔧 工程选择:');
console.log(' --project, -p <工程名> 指定工程(默认: 5G_basic_process)');
console.log(' # 不指定工程时自动选择 "5G_basic_process"');
console.log(' # 指定工程示例: node amf-edit-skill.js qqqwww --project 5G_basic_process');
console.log('\n🎯 智能选择逻辑:');
console.log(' 工程选择:');
console.log(' 1. 用户指定工程 → 自动切换到指定工程');
console.log(' 2. 未指定工程 → 自动选择 "5G_basic_process"');
console.log(' AMF选择:');
console.log(' 1. 用户指定AMF名称 → 按名称查找');
console.log(' 2. 未指定名称 + 单个AMF → 自动选择');
console.log(' 3. 未指定名称 + 多个AMF → 反问用户');
console.log('\n💡 提示:');
console.log(' - 字段名称区分大小写');
console.log(' - 支持精确匹配和模糊匹配');
console.log(' - 登录状态缓存24小时有效');
console.log('\n' + '='.repeat(60));
}
// 主函数
async function main() {
const args = parseArgs();
// 显示帮助
if (args.amfName === '--help' || args.amfName === '-h' || process.argv.includes('--help')) {
showHelp();
process.exit(0);
}
// 显示版本
if (args.amfName === '--version' || args.amfName === '-v' || process.argv.includes('--version')) {
console.log('AMF编辑技能 v1.0 (智能版本)');
console.log('状态: 🚧 开发中');
console.log('更新: 2026-03-22');
process.exit(0);
}
console.log('🎯 任务配置:');
console.log(` AMF名称: args.amfName || '(自动选择)'`);
console.log(` 工程选择: args.projectName '(用户指定)'`);
console.log(` 修改字段: Object.keys(args.fieldUpdates).length 个`);
if (Object.keys(args.fieldUpdates).length > 0) {
console.log(' 修改详情:');
Object.entries(args.fieldUpdates).forEach(([field, value]) => {
console.log(` - field: value`);
});
}
// 执行编辑
const result = await editAMF(args.amfName, args.projectName, args.fieldUpdates);
console.log('\n' + '='.repeat(60));
console.log('完成时间:', new Date().toLocaleString());
console.log('='.repeat(60));
process.exit(result.success ? 0 : 1);
}
// 执行
main().catch(error => {
console.error('程序异常:', error);
process.exit(1);
});
FILE:scripts/ausf-udm-add-skill.js
#!/usr/bin/env node
/**
* AUSF/UDM 添加技能
*
* 用法: node ausf-udm-add-skill.js <名称> [--url IP] [--project 工程] [--count N] [--sip IP] [--port N]
*
* 示例:
* node ausf-udm-add-skill.js myudf --project basic_5g_onoff --url 192.168.3.89
* node ausf-udm-add-skill.js UDM_TEST --url 192.168.3.89 --project 5G_basic_process
*/
const { chromium } = require('playwright');
const fs = require('fs');
const path = require('path');
// ==================== 配置 ====================
let globalBaseUrl = 'https://192.168.3.89';
const CONFIG = {
credentials: {
email: '[email protected]',
password: 'dotouch'
},
urls: {
login: '/login',
ausfEdit: '/sim_5gc/ausf/edit',
ausfManagement: '/sim_5gc/ausf/index'
},
sessionDir: path.join(__dirname, '.sessions'),
getSessionFile() {
const host = globalBaseUrl.replace(/https?:\/\//, '').replace(/\./g, '_');
return `5gc_session_host.json`;
}
};
// 会话管理
class SessionManager {
constructor() {
this.sessionPath = CONFIG.getSessionFile();
}
async saveSession(context) {
try {
const storageState = await context.storageState();
fs.writeFileSync(this.sessionPath, JSON.stringify({ storageState }, null, 2));
return true;
} catch { return false; }
}
async loadSession(browser) {
try {
if (!fs.existsSync(this.sessionPath)) return null;
const { storageState } = JSON.parse(fs.readFileSync(this.sessionPath, 'utf8'));
return await browser.newContext({ storageState, ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
} catch { return null; }
}
}
// ==================== 参数解析 ====================
function parseArgs() {
const args = process.argv.slice(2);
let baseUrl = globalBaseUrl;
const result = {
name: null,
project: '5G_basic_process',
config: {
type: '1',
count: '1',
sip: '192.168.20.30',
port: '80'
}
};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (!arg.startsWith('-')) {
result.name = arg;
} else if (arg === '--url' || arg === '-u') {
let url = args[++i];
if (url && !url.startsWith('http')) url = 'https://' + url;
baseUrl = url;
globalBaseUrl = baseUrl;
} else if (arg === '--project' || arg === '-p') {
result.project = args[++i];
} else if (arg.startsWith('--')) {
result.config[arg.substring(2)] = args[++i];
}
}
if (!result.name) {
console.error('用法: node ausf-udm-add-skill.js <名称> [--project 工程] [--url 地址] [--sip IP] [--port N]');
process.exit(1);
}
return result;
}
// ==================== 工程选择(支持翻页,与 amf-add 逻辑一致)====================
async function selectProject(page, projectName, forceSwitch = true) {
if (!forceSwitch) {
console.log(' 🔧 保持当前工程');
return true;
}
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', timeout: 15000 });
await page.waitForSelector('.jsgrid-row, .jsgrid-alt-row', { timeout: 5000 }).catch(() => {});
await page.waitForTimeout(300);
for (let pageNum = 1; pageNum <= 200; pageNum++) {
const clicked = await page.evaluate((targetName) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === targetName) {
const icon = cells[1].querySelector('.iconfont');
if (icon) { icon.click(); return true; }
}
}
return false;
}, projectName);
if (clicked) {
await page.waitForTimeout(2000);
return true;
}
const nextBtn = page.locator('.jsgrid-pager a:has-text("Next")');
if (!(await nextBtn.count())) break;
try {
await page.evaluate(() => { var links = document.querySelectorAll('.jsgrid-pager a'); for (var i = 0; i < links.length; i++) { if (links[i].innerText.trim() === 'Next') { links[i].click(); break; } } });
await page.waitForTimeout(2000);
} catch { break; }
}
console.log(` ❌ 未找到工程 "projectName"`);
return false;
}
// ==================== 主流程 ====================
async function addAusf(ausfName, projectName, explicitProject, ausfConfig) {
const startTime = Date.now();
const sessionManager = new SessionManager();
const cfg = { ...{ type: '1', count: '1', sip: '192.168.20.30', port: '80' }, ...ausfConfig };
let browser = null;
try {
browser = await chromium.launch({ headless: true, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
let context = await sessionManager.loadSession(browser);
let needLogin = true;
if (context) {
const testPage = await context.newPage();
await testPage.goto(`globalBaseUrlCONFIG.urls.ausfManagement`, { waitUntil: 'networkidle', timeout: 10000 }).catch(() => {});
if (!testPage.url().includes('/login')) needLogin = false;
await testPage.close();
}
if (needLogin) {
context = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
}
const page = await context.newPage();
if (needLogin) {
await page.goto(`globalBaseUrlCONFIG.urls.login`, { waitUntil: 'networkidle', timeout: 15000 });
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill(CONFIG.credentials.email);
await page.getByRole('textbox', { name: '密码' }).fill(CONFIG.credentials.password);
await page.getByRole('button', { name: '登录' }).click();
await page.waitForLoadState('networkidle', { timeout: 10000 });
await sessionManager.saveSession(context);
}
// 选择工程
if (!(await selectProject(page, projectName, explicitProject))) {
throw new Error(`工程 "projectName" 不存在或无法选中`);
}
// 进入编辑页面
await page.goto(`globalBaseUrlCONFIG.urls.ausfEdit`, { waitUntil: 'networkidle', timeout: 15000 });
if (!page.url().includes('/ausf/edit')) {
await page.goto(`globalBaseUrl/sim_5gc/ausf/edit`);
await page.waitForSelector('input[name="name"]', { timeout: 10000 });
}
// 填写表单
await page.evaluate(({ ausfName, cfg }) => {
const set = (name, value) => {
const el = document.querySelector(`input[name="name"]`);
if (el) { el.value = value; el.dispatchEvent(new Event('input', { bubbles: true })); }
};
set('name', ausfName);
set('sip', cfg.sip);
set('port', cfg.port);
set('count', cfg.count);
}, { ausfName, cfg });
// 类型选择:仿真设备
await page.locator('.layui-unselect').first().click();
await page.waitForTimeout(300);
await page.locator('dd').filter({ hasText: '仿真设备' }).click();
// 提交表单
await page.getByRole('button', { name: '提交' }).click();
// 等待页面跳转
try {
await page.waitForURL(`**/ausf/index`, { timeout: 8000 });
} catch (e) {
await page.goto(`globalBaseUrlCONFIG.urls.ausfManagement`, { waitUntil: 'networkidle', timeout: 15000 });
}
await page.waitForTimeout(2000);
// 验证结果:只要页面跳转到列表页即认为成功
let found = false;
const finalUrl = page.url();
if (finalUrl.includes('/ausf/index')) {
console.log(` ✅ 页面已跳转至 AUSF/UDM 列表: finalUrl`);
found = true;
}
await browser.close();
const totalTime = (Date.now() - startTime) / 1000;
if (found) {
return { success: true, ausfName, totalTime };
} else {
return { success: false, ausfName, totalTime };
}
} catch (err) {
if (browser) await browser.close();
throw err;
}
}
// ==================== 启动 ====================
async function main() {
const args = parseArgs();
console.log(`AUSF/UDM: args.name | 工程: args.project | 地址: globalBaseUrl`);
try {
const result = await addAusf(args.name, args.project, true, args.config);
console.log(result.success
? `成功! AUSF/UDM "result.ausfName" 添加完成 (result.totalTime.toFixed(2)s)`
: `失败! 未找到 AUSF/UDM "result.ausfName"`);
process.exit(result.success ? 0 : 1);
} catch (err) {
console.error(`执行异常: err.message`);
process.exit(1);
}
}
main();
FILE:scripts/ausf-udm-edit-skill.js
#!/usr/bin/env node
/**
* AUSF/UDM 编辑技能(单条+批量二合一)
*
* 单个编辑: node ausf-udm-edit-skill.js <名称> --project <工程> --set-<字段> <值>
* node ausf-udm-edit-skill.js --name <名称> --project <工程> --set-<字段> <值>
* node ausf-udm-edit-skill.js --id <ID> --set-<字段> <值>
*
* 批量编辑: node ausf-udm-edit-skill.js --project <工程> --set-<字段> <值>
*
* 可编辑字段: sip, port, mcc, mnc, ngap_port, http2_port
*
* 示例:
* node ausf-udm-edit-skill.js --project XW_S5GC_1 --set-sip 10.0.0.99 (批量)
* node ausf-udm-edit-skill.js --name udm --project XW_S5GC_1 --set-sip 10.0.0.99
* node ausf-udm-edit-skill.js --id 4772 --set-sip 10.0.0.99
*/
const { chromium } = require('playwright');
const path = require('path');
const fs = require('fs');
// 全局配置
let globalBaseUrl = 'https://192.168.3.89';
const CONFIG = {
sessionDir: path.join(__dirname, '.sessions'),
getSessionFile() {
const host = globalBaseUrl.replace(/https?:\/\//, '').replace(/\./g, '_');
return `5gc_session_host.json`;
},
credentials: {
email: '[email protected]',
password: 'dotouch'
},
urls: {
login: '/login',
ausfManagement: '/sim_5gc/ausf/index',
}
};
// 会话管理
class SessionManager {
constructor() {
this.sessionPath = path.join(CONFIG.sessionDir, CONFIG.getSessionFile());
}
async saveSession(context) {
try {
const storageState = await context.storageState();
fs.writeFileSync(this.sessionPath, JSON.stringify({ storageState }, null, 2));
} catch {}
}
async loadSession(browser) {
try {
if (!fs.existsSync(this.sessionPath)) return null;
const { storageState } = JSON.parse(fs.readFileSync(this.sessionPath, 'utf8'));
return await browser.newContext({ storageState, ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
} catch { return null; }
}
}
// ─── 参数解析 ─────────────────────────────────────────────────
function parseArgs() {
const args = process.argv.slice(2);
const result = { name: null, project: 'XW_S5GC_1', targetId: null, fields: {} };
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === '--help' || arg === '-h') { printHelp(); process.exit(0); }
else if (!arg.startsWith('-')) { if (!result.name) result.name = arg; }
else if (arg === '--url') { globalBaseUrl = args[++i]; }
else if (arg === '--project' || arg === '-p') { result.project = args[++i]; }
else if (arg === '--name') { result.name = args[++i]; }
else if (arg === '--id') { result.targetId = args[++i]; }
else if (arg.startsWith('--set-')) {
const key = arg.substring(6).replace(/-/g, '_');
result.fields[key] = args[++i];
} else if (arg.startsWith('--')) {
const key = arg.substring(2).replace(/-/g, '_');
result.fields[key] = args[++i];
}
}
return result;
}
function printHelp() {
console.log(`
AUSF/UDM 编辑(单条+批量二合一)
===============================
用法:
node ausf-udm-edit-skill.js --project <工程> --set-<字段> <值> (批量)
node ausf-udm-edit-skill.js --name <名称> --project <工程> --set-<字段> <值>
node ausf-udm-edit-skill.js --id <ID> --set-<字段> <值>
可编辑字段: sip, port, mcc, mnc, ngap_port, http2_port
`);
}
// ─── 工程选择 ─────────────────────────────────────────────────
async function selectProject(page, projectName) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', timeout: 15000 });
await page.waitForTimeout(2000);
for (let pageNum = 1; pageNum <= 20; pageNum++) {
const clicked = await page.evaluate((name) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === name) {
if (row.classList.contains('jsgrid-selected-row')) return 'already';
const icon = cells[1].querySelector('.iconfont');
if (icon) { icon.click(); return 'clicked'; }
const td = row.querySelector('td.project_select');
if (td) { td.click(); return 'clicked'; }
row.click(); return 'clicked';
}
}
return 'not-found';
}, projectName);
if (clicked !== 'not-found') { await page.waitForTimeout(2000); return true; }
const hasNext = await page.evaluate(() =>
Array.from(document.querySelectorAll('.jsgrid-pager a')).some(a => a.innerText.trim() === 'Next'));
if (!hasNext) break;
await page.evaluate(() => {
const links = document.querySelectorAll('.jsgrid-pager a');
for (const l of links) { if (l.innerText.trim() === 'Next') { l.click(); break; } }
});
await page.waitForTimeout(1500);
}
return false;
}
// ─── 查找 AUSF 列表 ───────────────────────────────────────────
async function findAusfList(page) {
const sidebarUrl = await page.evaluate(() => {
const links = document.querySelectorAll('a[href*="/ausf/index"]');
return links[0]?.href || '';
});
if (!sidebarUrl) throw new Error('未找到 AUSF 侧边栏链接');
let ausfList = [];
await page.goto(sidebarUrl, { waitUntil: 'networkidle', timeout: 15000 });
await page.waitForTimeout(2000);
for (let pageNum = 1; pageNum <= 50; pageNum++) {
const rows = await page.locator('.layui-table tbody tr').all();
for (const row of rows) {
const cells = await row.locator('td').all();
if (cells.length >= 3) {
const id = (await cells[1].textContent()).trim();
const n = (await cells[2].textContent()).trim();
if (id && id.match(/^\d+$/) && n && n !== '名称' && n !== '编辑') {
if (!ausfList.some(u => u.id === id)) ausfList.push({ id, name: n });
}
}
}
const hasNext = await page.evaluate(() =>
Array.from(document.querySelectorAll('.layui-table .layui-laypage a'))
.some(a => a.innerText.trim() === 'Next'));
if (!hasNext) break;
await page.evaluate(() => {
const links = document.querySelectorAll('.layui-table .layui-laypage a');
for (const l of links) { if (l.innerText.trim() === 'Next') { l.click(); break; } }
});
await page.waitForTimeout(2000);
}
return ausfList;
}
// ─── 修改单个 AUSF ────────────────────────────────────────────
async function modifyAndSubmit(page, ausfId, fields) {
await page.goto(`globalBaseUrl/sim_5gc/ausf/edit/ausfId`, { waitUntil: 'networkidle', timeout: 15000 });
await page.waitForTimeout(3000);
if (fields.sip) { await page.locator('input[name="sip"]').fill(fields.sip); }
if (fields.port) { await page.locator('input[name="port"]').fill(fields.port); }
if (fields.mcc) { await page.locator('input[name="mcc"]').fill(fields.mcc); }
if (fields.mnc) { await page.locator('input[name="mnc"]').fill(fields.mnc); }
if (fields.ngap_port) { await page.locator('input[name="ngap_port"]').fill(fields.ngap_port); }
if (fields.http2_port) { await page.locator('input[name="http2_port"]').fill(fields.http2_port); }
await page.getByRole('button', { name: '提交' }).click();
await page.waitForTimeout(3000);
try { await page.waitForURL('**/ausf/index', { timeout: 5000 }); } catch {}
return page.url().includes('/ausf/index');
}
// ─── 主函数 ──────────────────────────────────────────────────
async function main() {
const args = parseArgs();
if (Object.keys(args.fields).length === 0) {
console.error('请至少指定一个 --set-<字段>'); printHelp(); process.exit(1);
}
console.log('▶ AUSF/UDM 编辑 工程:', args.project, ' 字段:', JSON.stringify(args.fields));
const browser = await chromium.launch({ headless: true, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const sessionManager = new SessionManager();
let context = await sessionManager.loadSession(browser);
let needLogin = true;
if (context) {
const testPage = await context.newPage();
await testPage.goto(`globalBaseUrlCONFIG.urls.ausfManagement`, { waitUntil: 'networkidle', timeout: 8000 }).catch(() => {});
if (!testPage.url().includes('/login')) needLogin = false;
await testPage.close();
}
if (needLogin) context = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await context.newPage();
if (needLogin) {
console.log(' 🔐 登录...');
await page.goto(`globalBaseUrlCONFIG.urls.login`, { waitUntil: 'networkidle', timeout: 15000 });
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill(CONFIG.credentials.email);
await page.getByRole('textbox', { name: '密码' }).fill(CONFIG.credentials.password);
await page.getByRole('button', { name: '登录' }).click();
await page.waitForLoadState('networkidle');
await sessionManager.saveSession(context);
console.log(' ✅ 登录成功');
} else {
console.log(' ✅ 使用缓存会话');
}
if (!await selectProject(page, args.project)) {
console.log(' ❌ 工程未找到:', args.project);
await browser.close(); process.exit(1);
}
console.log(' ✅ 工程已选');
// ── 批量模式:无 name 且无 targetId ──────────────────────
if (!args.name && !args.targetId) {
const ausfList = await findAusfList(page);
if (ausfList.length === 0) { console.log(' ⚠ 没有 AUSF/UDM'); await browser.close(); process.exit(0); }
console.log(' 找到', ausfList.length, '个 AUSF/UDM,进入批量模式');
let success = 0;
for (const { id, name } of ausfList) {
process.stdout.write(' ▶ [' + id + '] ' + name + ' ... ');
try {
const ok = await modifyAndSubmit(page, id, args.fields);
console.log(ok ? '✅' : '❌');
if (ok) success++;
} catch (e) { console.log('❌', e.message); }
await page.waitForTimeout(500);
}
await browser.close();
console.log('\n完成:', success + '/' + ausfList.length, '成功');
process.exit(success > 0 ? 0 : 1);
}
// ── 单个模式 ────────────────────────────────────────────
let resolvedId = args.targetId;
if (!resolvedId) {
const ausfList = await findAusfList(page);
const target = ausfList.find(u => u.name === args.name);
if (!target) { console.log(' ❌ 未找到:', args.name); await browser.close(); process.exit(1); }
resolvedId = target.id;
}
process.stdout.write(' → AUSF/UDM ' + resolvedId + ' ... ');
try {
const ok = await modifyAndSubmit(page, resolvedId, args.fields);
console.log(ok ? '✅' : '❌');
await browser.close();
process.exit(ok ? 0 : 1);
} catch (e) { console.log('❌', e.message); await browser.close(); process.exit(1); }
}
main().catch(e => { console.error('错误:', e.message); process.exit(1); });
FILE:scripts/batch1.sh
#!/bin/bash
cd /home/dotouch/.openclaw/workspace/skills/5gc/scripts
TS=$(date +%s)
echo "=== UPF add === $(date)"
node upf-add-skill.js UPF_REG_$TS --project XW_S5GC_1 --pfcp_ip 10.200.2.60 --s5s8_ip 10.200.2.61 --teid 20000 --mcc 460 --mnc 01 2>&1 | tail -5
echo "=== GNB add === $(date)"
node gnb-add-skill.js GNB_REG_$TS --project XW_S5GC_1 --gNB_ID 10000 --mcc 460 --mnc 01 --Tac 101 2>&1 | tail -5
echo "=== UE add === $(date)"
node ue-add-skill.js UE_REG_$TS --project XW_S5GC_1 --imsi 460010000000001 --key 0000111122223333 --opc 00000000000000000000000000000000 --sqn 000000000001 2>&1 | tail -5
echo "=== NRF add === $(date)"
node nrf-add-skill.js NRF_REG_$TS --project XW_S5GC_1 --sbi_ip 10.200.2.70 --http_port 8080 2>&1 | tail -5
echo "All batch1 done $(date)"
FILE:scripts/batch2.sh
#!/bin/bash
cd /home/dotouch/.openclaw/workspace/skills/5gc/scripts
TS=$(date +%s)
BASE="node 5gc.js"
echo "=== QoS add === $(date)"
$BASE qos add --project XW_SUPF_5_1_2_4 --qos-id qos_reg_$TS --5qi 8 --maxbr-ul 10000000 --maxbr-dl 20000000 2>&1 | tail -5
echo "=== TC add === $(date)"
$BASE tc add --project XW_SUPF_5_1_2_4 --tc-id tc_reg_$TS --flow-status ENABLED 2>&1 | tail -5
echo "=== PCC add === $(date)"
$BASE pcc add --project XW_SUPF_5_1_2_4 --pcc-id pcc_reg_$TS --qos qos_reg_$TS --tc tc_reg_$TS 2>&1 | tail -5
echo "All batch2 done $(date)"
FILE:scripts/check_pcc.js
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
async function main() {
const browser = await chromium.launch({ headless: true, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await page.goto(globalBaseUrl + '/login', { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
await page.goto(globalBaseUrl + '/sim_5gc/project/index', { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.evaluate(n => {
document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === n) cells[1].querySelector('.iconfont').click();
});
}, 'XW_SUPF_5_1_2_4');
await page.waitForTimeout(3000);
await page.goto(globalBaseUrl + '/sim_5gc/predfPolicy/pcc/index', { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const result = await page.evaluate(() => {
// 找包含 pcc_default_test 的行
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
// cells[2] 是 pccRuleId
if (cells.length >= 5 && cells[2].textContent.trim() === 'pcc_default_test') {
return {
id: cells[1].textContent.trim(),
pccRuleId: cells[2].textContent.trim(),
precedence: cells[4].textContent.trim(),
qosId: cells[5].textContent.trim(),
};
}
}
return null;
});
console.log('pcc_default_test:', JSON.stringify(result, null, 2));
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/check_pcc_detail.js
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
async function main() {
const browser = await chromium.launch({ headless: true, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await page.goto(globalBaseUrl + '/login', { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
await page.goto(globalBaseUrl + '/sim_5gc/project/index', { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.locator('input[name="project_search_name"]').fill('XW_SUPF_SN4_5_2_8');
await page.keyboard.press('Enter');
await page.waitForTimeout(3000);
await page.evaluate(() => {
document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === 'XW_SUPF_SN4_5_2_8') cells[1].querySelector('.iconfont').click();
});
});
await page.waitForTimeout(3000);
await page.goto(globalBaseUrl + '/sim_5gc/predfPolicy/pcc/index', { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const result = await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 9 && cells[2].textContent.trim() === 'pcc_default') {
const headers = Array.from(document.querySelectorAll('.layui-table thead th')).map((th, i) => i + ':' + th.textContent.trim());
return { headers, cells: Array.from(cells).map((c, i) => i + '=' + c.textContent.trim()) };
}
}
return null;
});
console.log('表头:', JSON.stringify(result ? result.headers : 'null'));
console.log('行数据:', JSON.stringify(result ? result.cells : 'null'));
// 也查看PCC编辑页的字段
await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 9 && cells[2].textContent.trim() === 'pcc_default') {
const links = cells[9].querySelectorAll('a');
for (const l of links) { if (l.textContent.trim() === '编辑') { l.click(); return; } }
}
}
});
await page.waitForTimeout(3000);
const frame = page.frame('layui-layer-iframe2');
if (frame) {
const fields = await frame.evaluate(() => {
return Array.from(document.querySelectorAll('.layui-form-item')).map(item => {
const label = item.querySelector('.layui-form-label')?.textContent?.trim() || '';
const input = item.querySelector('input[name]');
const select = item.querySelector('select');
return { label, name: input?.name || select?.name || '', value: input?.value || '' };
});
});
console.log('\nPCC编辑页字段:', JSON.stringify(fields, null, 2));
// xm-select 值
const xmValues = await frame.evaluate(() => {
return Array.from(document.querySelectorAll('input.xm-select-default')).map((inp, i) => ({ idx: i, value: inp.value }));
});
console.log('\nxm-select值:', JSON.stringify(xmValues));
}
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/cleanup.js
/**
* cleanup.js - 清理测试数据
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, name) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.evaluate((n) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === n) {
cells[1].querySelector('.iconfont').click();
return;
}
}
}, name);
await page.waitForTimeout(3000);
console.log(`✅ 工程 "name" 已选`);
}
async function deleteByName(page, url, targetId) {
await page.goto(url, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const found = await page.evaluate((id) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
for (const cell of cells) {
if (cell.textContent.trim() === id) {
const btn = row.querySelector('a');
if (btn && btn.textContent.trim() === '删除') {
btn.click();
return true;
}
}
}
}
return false;
}, targetId);
if (found) {
await page.waitForTimeout(1500);
try {
await page.locator('.layui-layer-btn0').click();
await page.waitForTimeout(2000);
console.log(` ✅ 已删除 targetId`);
} catch(e) {
console.log(` ⚠️ 确认框未出现 targetId`);
}
} else {
console.log(` ℹ️ targetId 不存在,跳过`);
}
}
async function main() {
const browser = await chromium.launch({ headless: false, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, 'XW_SUPF_5_1_2_4');
console.log('\n🗑️ 清理测试数据...');
await deleteByName(page, `globalBaseUrl/sim_5gc/predfPolicy/qos/index`, 'qos3');
await deleteByName(page, `globalBaseUrl/sim_5gc/predfPolicy/pcc/index`, 'pcc_default_test');
console.log('\n✅ 清理完成');
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/cleanup_pcc_default.js
/**
* cleanup_partial.js - 清理中间状态
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
}
async function selectProject(page, name) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.locator('input[name="project_search_name"]').fill(name);
await page.keyboard.press('Enter');
await page.waitForTimeout(3000);
await page.evaluate((n) => {
document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === n) cells[1].querySelector('.iconfont').click();
});
}, name);
await page.waitForTimeout(3000);
}
async function deleteByName(page, url, targetName) {
await page.goto(url, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const found = await page.evaluate((name) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
for (const cell of cells) {
if (cell.textContent.trim() === name) {
const links = row.querySelectorAll('a');
for (const l of links) {
if (l.textContent.trim() === '删除') { l.click(); return true; }
}
}
}
}
return false;
}, targetName);
if (found) {
await page.waitForTimeout(1500);
try { await page.locator('.layui-layer-btn0').click(); await page.waitForTimeout(2000); } catch(e) {}
console.log(` ✅ 已删除 targetName`);
} else {
console.log(` ℹ️ targetName 不存在`);
}
}
async function main() {
const browser = await chromium.launch({ headless: false, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, 'XW_SUPF_SN4_5_2_8');
console.log('🗑️ 清理...');
await deleteByName(page, `globalBaseUrl/sim_5gc/predfPolicy/pcc/index`, 'pcc_default');
await deleteByName(page, `globalBaseUrl/sim_5gc/smpolicy/default/index`, 'sm_policy_default');
// 也清理旧的
await deleteByName(page, `globalBaseUrl/sim_5gc/predfPolicy/pcc/index`, 'pcc_qos1');
await deleteByName(page, `globalBaseUrl/sim_5gc/predfPolicy/qos/index`, 'qos1');
await deleteByName(page, `globalBaseUrl/sim_5gc/predfPolicy/trafficCtl/index`, 'tc1');
console.log('\n✅ 完成');
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/cleanup_pcc_qos1.js
/**
* cleanup_wrong_pcc.js - 删除之前错误创建的 pcc_qos1
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
}
async function selectProject(page, name) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.locator('input[name="project_search_name"]').fill(name);
await page.keyboard.press('Enter');
await page.waitForTimeout(3000);
await page.evaluate((n) => {
document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === n) cells[1].querySelector('.iconfont').click();
});
}, name);
await page.waitForTimeout(3000);
}
async function main() {
const browser = await chromium.launch({ headless: false, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, 'XW_SUPF_SN4_5_2_8');
// 删除 pcc_qos1
console.log('🗑️ 删除 pcc_qos1...');
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/pcc/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === 'pcc_qos1') {
const links = row.querySelectorAll('a');
for (const l of links) {
if (l.textContent.trim() === '删除') { l.click(); return; }
}
}
}
});
await page.waitForTimeout(1500);
try { await page.locator('.layui-layer-btn0').click(); await page.waitForTimeout(2000); } catch(e) {}
console.log(' ✅ pcc_qos1 已删除');
// 删除 sm_policy_default
console.log('🗑️ 删除 sm_policy_default...');
await page.goto(`globalBaseUrl/sim_5gc/smpolicy/default/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === 'sm_policy_default') {
const links = row.querySelectorAll('a');
for (const l of links) {
if (l.textContent.trim() === '删除') { l.click(); return; }
}
}
}
});
await page.waitForTimeout(1500);
try { await page.locator('.layui-layer-btn0').click(); await page.waitForTimeout(2000); } catch(e) {}
console.log(' ✅ sm_policy_default 已删除');
console.log('\n✅ 清理完成');
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/debug_load.js
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: true, args: ['--no-sandbox','--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: {width:1920,height:1080} });
const page = await ctx.newPage();
await page.goto('https://192.168.3.89/login', { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', {name:'E-Mail地址'}).fill('[email protected]');
await page.getByRole('textbox', {name:'密码'}).fill('dotouch');
await page.getByRole('button', {name:'登录'}).click();
await page.waitForTimeout(2500);
await page.goto('https://192.168.3.89/sim_5gc/project/index', { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.evaluate((name) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === name) {
const icon = cells[1].querySelector('.iconfont');
if (icon) { icon.click(); return; }
}
}
}, 'XW_SUPF_5_1_2_4');
await page.waitForTimeout(3000);
await page.goto('https://192.168.3.89/sim_5gc/load/index', { ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
// Click dedicated edit
await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 5 && cells[2].textContent.trim() === 'dedicated') {
const links = Array.from(cells[5].querySelectorAll('a'));
const editLink = links.find(l => l.textContent.trim() === '编辑');
if (editLink) { editLink.click(); return; }
}
}
});
await page.waitForSelector('.layui-layer', { timeout: 5000 });
await page.waitForTimeout(1000);
for (let i = 0; i < 10; i++) {
const allFrames = await page.frames();
const frameList = allFrames.map(f => ({ name: f.name(), url: f.url() }));
const mainFrame = page.mainFrame();
const mainUrl = mainFrame.url();
console.log('[' + (i*500) + 'ms] mainFrame=' + mainUrl + ' frames=' + JSON.stringify(frameList));
if (mainUrl.includes('/load/edit/')) { console.log('FOUND in mainFrame!'); break; }
await new Promise(r => setTimeout(r, 500));
}
// Try getting inputs from main frame
const mainUrl = page.mainFrame().url();
console.log('Main frame URL:', mainUrl);
if (mainUrl.includes('/load/edit/')) {
const inputs = await page.evaluate(() => {
const allInputs = document.querySelectorAll('input, textarea');
return Array.from(allInputs).map(el => ({ tag: el.tagName, name: el.name, val: el.value ? el.value.substring(0, 40) : '[empty]' })).filter(el => el.name);
});
console.log('Inputs from main frame:', JSON.stringify(inputs.slice(0, 15), null, 2));
}
await browser.close();
})().catch(e => console.error(e.message));
FILE:scripts/debug_pcc_columns.js
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
async function main() {
const browser = await chromium.launch({ headless: true, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await page.goto(globalBaseUrl + '/login', { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
await page.goto(globalBaseUrl + '/sim_5gc/project/index', { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.locator('input[name="project_search_name"]').fill('XW_SUPF_SN4_5_2_8');
await page.keyboard.press('Enter');
await page.waitForTimeout(3000);
await page.evaluate(() => {
document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === 'XW_SUPF_SN4_5_2_8') cells[1].querySelector('.iconfont').click();
});
});
await page.waitForTimeout(3000);
await page.goto(globalBaseUrl + '/sim_5gc/predfPolicy/pcc/index', { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const headers = await page.evaluate(() => {
return Array.from(document.querySelectorAll('.layui-table thead th')).map((th, i) => i + ':' + th.textContent.trim());
});
console.log('表头:', JSON.stringify(headers));
const rows = await page.evaluate(() => {
return Array.from(document.querySelectorAll('.layui-table tbody tr')).map((row, ri) => {
return Array.from(row.querySelectorAll('td')).map((td, i) => i + '=' + td.textContent.trim());
});
});
console.log('行数:', rows.length);
if (rows.length > 0) {
console.log('第1行:', JSON.stringify(rows[0]));
console.log('列数:', rows[0].length);
}
// 查看 TCC列表
await page.goto(globalBaseUrl + '/sim_5gc/predfPolicy/trafficCtl/index', { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const tcHeaders = await page.evaluate(() => {
return Array.from(document.querySelectorAll('.layui-table thead th')).map((th, i) => i + ':' + th.textContent.trim());
});
const tcRows = await page.evaluate(() => {
return Array.from(document.querySelectorAll('.layui-table tbody tr')).map(row => {
return Array.from(row.querySelectorAll('td')).map((td, i) => i + '=' + td.textContent.trim());
});
});
console.log('\nTC表头:', JSON.stringify(tcHeaders));
console.log('TC行数:', tcRows.length);
if (tcRows.length > 0) console.log('TC第1行:', JSON.stringify(tcRows[0]));
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/debug_pcc_edit.js
/**
* debug_pcc_edit.js - 探索 PCC 添加页的完整字段结构
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, name) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.locator('input[name="project_search_name"]').fill(name);
await page.keyboard.press('Enter');
await page.waitForTimeout(3000);
await page.evaluate((n) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === n) {
cells[1].querySelector('.iconfont').click();
return;
}
}
}, name);
await page.waitForTimeout(3000);
console.log(`✅ 工程 "name" 已选`);
}
async function main() {
const browser = await chromium.launch({ headless: false, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, 'XW_SUPF_SN4_5_2_8');
// 去 PCC 列表
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/pcc/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
await page.locator('button:has-text("添加")').click();
await page.waitForTimeout(3000);
await page.waitForFunction(() => window.location.href.includes('/predfPolicy/pcc/edit'), { timeout: 10000 });
await page.waitForTimeout(3000);
// 打印页面内容
const pageText = await page.evaluate(() => document.body.innerText);
console.log('\n📄 PCC添加页内容:');
console.log(pageText.substring(0, 3000));
// 打印所有字段
const fields = await page.evaluate(() => {
const result = [];
document.querySelectorAll('.layui-form-item').forEach(item => {
const label = item.querySelector('.layui-form-label')?.textContent?.trim() || '';
const input = item.querySelector('input[name]');
const select = item.querySelector('select[name]');
if (input) {
result.push({ label, name: input.name, value: input.value, type: input.type });
} else if (select) {
result.push({ label, name: select.name });
}
});
return result;
});
console.log('\n📋 表单字段 (layui-form-item):');
fields.forEach(f => console.log(` "f.label" -> name="f.name" value="f.value || ''"`));
// xm-select 数量和显示文本
const xmSelects = await page.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
return Array.from(inputs).map((inp, idx) => {
const parent = inp.parentElement;
return { idx, display: parent.textContent.substring(0, 200) };
});
});
console.log('\n📋 xm-select 下拉列表:');
xmSelects.forEach(s => console.log(` [s.idx]: s.display`));
// 尝试点击每个 xm-select 看选项
for (let i = 0; i < xmSelects.length; i++) {
await page.evaluate((idx) => {
const inputs = document.querySelectorAll('input.xm-select-default');
if (inputs[idx]) inputs[idx].parentElement.click();
}, i);
await page.waitForTimeout(1000);
const options = await page.evaluate(() => {
return Array.from(document.querySelectorAll('.xm-option.show-icon')).map(o => o.textContent.trim());
});
console.log(`\n xm-select[i] 选项:`, JSON.stringify(options));
await page.keyboard.press('Escape');
await page.waitForTimeout(500);
}
await page.screenshot({ path: 'pcc_add_debug.png', fullPage: true });
console.log('\n📸 截图: pcc_add_debug.png');
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/debug_pcc_edit_verify.js
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
async function main() {
const browser = await chromium.launch({ headless: false, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await page.goto(globalBaseUrl + '/login', { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
await page.goto(globalBaseUrl + '/sim_5gc/project/index', { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.locator('input[name="project_search_name"]').fill('XW_SUPF_SN4_5_2_8');
await page.keyboard.press('Enter');
await page.waitForTimeout(3000);
await page.evaluate(() => {
document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === 'XW_SUPF_SN4_5_2_8') cells[1].querySelector('.iconfont').click();
});
});
await page.waitForTimeout(3000);
// 直接导航到 PCC 编辑页(ID=17484)
await page.goto(globalBaseUrl + '/sim_5gc/predfPolicy/pcc/edit/17484', { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
// 打印页面内容
const pageText = await page.evaluate(() => document.body.innerText);
console.log('PCC编辑页内容(前2000字):', pageText.substring(0, 2000));
// 检查 xm-select 的值
const xmValues = await page.evaluate(() => {
return Array.from(document.querySelectorAll('input.xm-select-default')).map((inp, i) => {
return { idx: i, value: inp.value, parentText: inp.parentElement.textContent.substring(0, 100) };
});
});
console.log('\nxm-select值:', JSON.stringify(xmValues, null, 2));
await page.screenshot({ path: 'pcc_edit_check.png', fullPage: true });
console.log('截图: pcc_edit_check.png');
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/debug_pcf_xmselect.js
/**
* debug_pcf_xmselect.js - 深入分析 PCF 弹窗中 default_smpolicy xm-select 的行为
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000, waitUntil: 'domcontentloaded' });
await page.waitForTimeout(2000);
try { await page.locator('input[name="email"]').first().waitFor({ state: 'visible', timeout: 5000 }); } catch(e) {}
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, name) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.locator('input[name="project_search_name"]').fill(name);
await page.keyboard.press('Enter');
await page.waitForTimeout(3000);
await page.evaluate((n) => {
document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === n) cells[1].querySelector('.iconfont').click();
});
}, name);
await page.waitForTimeout(3000);
console.log(`✅ 工程 "name" 已选`);
}
async function main() {
const browser = await chromium.launch({ headless: false, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, 'XW_SUPF_5_1_2_4');
await page.goto(`globalBaseUrl/sim_5gc/pcf/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
// 点击编辑按钮
await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
if (rows.length > 0) {
const links = rows[0].querySelectorAll('a');
for (const l of links) { if (l.textContent.trim() === '编辑') { l.click(); return; } }
}
});
await page.waitForTimeout(3000);
await page.waitForSelector('iframe[name="layui-layer-iframe2"]', { timeout: 5000 });
await page.waitForTimeout(2000);
const frame = page.frame('layui-layer-iframe2');
console.log('✅ 找到弹窗iframe:', frame.url());
// 初始状态
const initVal = await frame.evaluate(() => document.querySelectorAll('input.xm-select-default')[0]?.value);
console.log('初始 default_smpolicy value:', initVal);
// 打开下拉
await frame.evaluate(() => document.querySelectorAll('input.xm-select-default')[0]?.parentElement.click());
await page.waitForTimeout(1500);
// 查看所有可见选项
const options = await frame.evaluate(() => {
return Array.from(document.querySelectorAll('.xm-option.show-icon')).map(o => ({
text: o.textContent.trim(),
visible: o.offsetHeight > 0 && window.getComputedStyle(o).display !== 'none',
selected: o.classList.contains('selected')
}));
});
console.log('\n下拉选项:', JSON.stringify(options, null, 2));
// 直接用 JS 点击 sm_policy_default 选项(不通过 Playwright locator)
const clicked = await frame.evaluate(() => {
const opts = document.querySelectorAll('.xm-option.show-icon');
for (const opt of opts) {
if (opt.textContent.trim() === 'sm_policy_default') {
opt.click();
return true;
}
}
return false;
});
console.log('\nJS 点击 sm_policy_default:', clicked);
await page.waitForTimeout(1000);
// 检查点击后的值
const afterJsClick = await frame.evaluate(() => document.querySelectorAll('input.xm-select-default')[0]?.value);
console.log('JS点击后 value:', afterJsClick);
// 也测试 Playwright locator 点击
// 先重新打开下拉
await frame.evaluate(() => document.querySelectorAll('input.xm-select-default')[0]?.parentElement.click());
await page.waitForTimeout(1500);
const pwVisible = await frame.locator('.xm-option.show-icon', { hasText: 'sm_policy_default' }).isVisible({ timeout: 3000 }).catch(() => false);
console.log('\nPlaywright locator sm_policy_default 可见:', pwVisible);
if (pwVisible) {
await frame.locator('.xm-option.show-icon', { hasText: 'sm_policy_default' }).first().click();
console.log('Playwright locator 点击完成');
await page.waitForTimeout(500);
}
const afterPwClick = await frame.evaluate(() => document.querySelectorAll('input.xm-select-default')[0]?.value);
console.log('Playwright点击后 value:', afterPwClick);
await page.screenshot({ path: 'pcf_xmselect_debug.png', fullPage: true });
console.log('截图: pcf_xmselect_debug.png');
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/debug_popup.js
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: true, args: ['--no-sandbox','--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: {width:1920,height:1080} });
const page = await ctx.newPage();
await page.goto('https://192.168.3.89/login', { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', {name:'E-Mail地址'}).fill('[email protected]');
await page.getByRole('textbox', {name:'密码'}).fill('dotouch');
await page.getByRole('button', {name:'登录'}).click();
await page.waitForTimeout(2500);
await page.goto('https://192.168.3.89/sim_5gc/project/index', { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.evaluate((name) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === name) {
const icon = cells[1].querySelector('.iconfont');
if (icon) { icon.click(); return; }
}
}
}, 'XW_SUPF_5_1_2_4');
await page.waitForTimeout(3000);
await page.goto('https://192.168.3.89/sim_5gc/predfPolicy/pcc/index', { ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
// Click pcc2 edit link
await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
if (row.textContent.includes('pcc2') && row.textContent.includes('编辑')) {
const links = Array.from(row.querySelectorAll('a'));
const editLink = links.find(l => l.textContent.trim() === '编辑');
if (editLink) { editLink.click(); return; }
}
}
});
await page.waitForSelector('.layui-layer', { timeout: 5000 });
await page.waitForTimeout(500);
for (let i = 0; i < 15; i++) {
const frameInfo = await page.evaluate(() => {
const iframe = document.querySelector('.layui-layer iframe');
if (!iframe) return { src: 'no iframe' };
return { name: iframe.name, id: iframe.id, src: iframe.getAttribute('src'), laySrc: iframe.getAttribute('lay-src') };
});
const allFrames = await page.frames();
const frameList = allFrames.map(f => ({ name: f.name(), url: f.url() }));
console.log('[' + (i*500) + 'ms] iframe=' + JSON.stringify(frameInfo) + ' frames=' + JSON.stringify(frameList));
if (frameInfo.src && frameInfo.src.includes('/predfPolicy/pcc/edit/')) { console.log('FOUND!'); break; }
await new Promise(r => setTimeout(r, 500));
}
await browser.close();
})().catch(e => console.error(e.message));
FILE:scripts/debug_qos_add.js
/**
* debug_qos_add.js - 探索 QoS 添加页面结构
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000, waitUntil: 'domcontentloaded' });
await page.waitForTimeout(2000);
try { await page.locator('input[name="email"]').first().waitFor({ state: 'visible', timeout: 5000 }); } catch(e) {}
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, name) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.locator('input[name="project_search_name"]').fill(name);
await page.keyboard.press('Enter');
await page.waitForTimeout(3000);
await page.evaluate((n) => {
document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === n) cells[1].querySelector('.iconfont').click();
});
}, name);
await page.waitForTimeout(3000);
console.log(`✅ 工程 "name" 已选`);
}
async function main() {
const browser = await chromium.launch({ headless: false, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, 'XW_SUPF_5_1_2_4');
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/qos/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
console.log('QoS列表页 URL:', page.url());
await page.locator('button:has-text("添加")').click();
await page.waitForTimeout(5000);
console.log('点击后 URL:', page.url());
console.log('layui-layer弹窗数量:', await page.locator('.layui-layer').count());
// 获取所有frames
const allFrames = page.frames();
console.log('\n所有frames:');
for (const f of allFrames) {
console.log(` name="f.name()", url="f.url()"`);
}
// 找到layui-layer-iframe2(QoS编辑页)
const frame = page.frame('layui-layer-iframe2');
if (!frame) { console.log('❌ 未找到 layui-layer-iframe2'); await browser.close(); return; }
console.log('✅ 找到 layui-layer-iframe2:', frame.url());
await page.waitForTimeout(2000);
const inputs = await frame.evaluate(() => {
return Array.from(document.querySelectorAll('input[name], select[name]')).map(inp => ({
name: inp.name || '',
tag: inp.tagName,
type: inp.type || '',
placeholder: inp.placeholder || '',
value: inp.value ? inp.value.substring(0, 30) : ''
}));
});
console.log('\niframe内字段:', JSON.stringify(inputs, null, 2));
const btns = await frame.evaluate(() => {
return Array.from(document.querySelectorAll('button, .layui-btn')).map(b => b.textContent.trim()).filter(t => t);
});
console.log('iframe内按钮:', JSON.stringify(btns));
await page.screenshot({ path: 'qos_add_popup.png', fullPage: true });
console.log('截图: qos_add_popup.png');
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/debug_search.js
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
async function main() {
const browser = await chromium.launch({ headless: false, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await page.goto(globalBaseUrl + '/login', { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
await page.goto(globalBaseUrl + '/sim_5gc/project/index', { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
// 调试:找到所有input
const inputs = await page.evaluate(() => {
const result = [];
document.querySelectorAll('input').forEach(el => {
result.push({ type: el.type, name: el.name, id: el.id, placeholder: el.placeholder, class: el.className });
});
return result;
});
console.log('页面所有input:', JSON.stringify(inputs, null, 2));
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/debug_smpolicy.js
/**
* debug_smpolicy.js - 探索 sm_policy/default 页面结构
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000, waitUntil: 'domcontentloaded' });
await page.waitForTimeout(2000);
try { await page.locator('input[name="email"]').first().waitFor({ state: 'visible', timeout: 5000 }); } catch(e) {}
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, name) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.locator('input[name="project_search_name"]').fill(name);
await page.keyboard.press('Enter');
await page.waitForTimeout(3000);
await page.evaluate((n) => {
document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === n) cells[1].querySelector('.iconfont').click();
});
}, name);
await page.waitForTimeout(3000);
console.log(`✅ 工程 "name" 已选`);
}
async function main() {
const browser = await chromium.launch({ headless: false, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, 'XW_SUPF_5_1_2_4');
await page.goto(`globalBaseUrl/sim_5gc/smpolicy/default/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
// 检查表头
const headers = await page.evaluate(() => {
return Array.from(document.querySelectorAll('.layui-table thead th')).map((th, i) => i + ':' + th.textContent.trim());
});
console.log('表头:', JSON.stringify(headers));
// 检查所有行
const allRows = await page.evaluate(() => {
return Array.from(document.querySelectorAll('.layui-table tbody tr')).map(row => {
const cells = row.querySelectorAll('td');
return Array.from(cells).map((td, i) => i + '=' + td.textContent.trim().substring(0, 30));
});
});
console.log('行数:', allRows.length);
allRows.forEach((r, i) => console.log(` row[i]: r.join(' | ')`));
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/debug_tc.js
/**
* debug_tc.js - 探索 Traffic Control 添加页字段
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
}
async function selectProject(page, name) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.locator('input[name="project_search_name"]').fill(name);
await page.keyboard.press('Enter');
await page.waitForTimeout(3000);
await page.evaluate((n) => {
document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === n) cells[1].querySelector('.iconfont').click();
});
}, name);
await page.waitForTimeout(3000);
}
async function main() {
const browser = await chromium.launch({ headless: false, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, 'XW_SUPF_SN4_5_2_8');
// 查看 TC 列表
console.log('\n📋 TC列表:');
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/trafficCtl/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const tcList = await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
const list = [];
rows.forEach(row => {
const cells = row.querySelectorAll('td');
list.push(Array.from(cells).map(c => c.textContent.trim()));
});
return list;
});
console.log(JSON.stringify(tcList, null, 2));
// 查看 TC 添加页字段
await page.locator('button:has-text("添加")').click();
await page.waitForTimeout(3000);
const frame = page.frame('layui-layer-iframe2');
if (frame) {
const fields = await frame.evaluate(() => {
const result = [];
document.querySelectorAll('.layui-form-item').forEach(item => {
const label = item.querySelector('.layui-form-label')?.textContent?.trim() || '';
const input = item.querySelector('input[name]');
if (input) result.push({ label, name: input.name, value: input.value });
});
return result;
});
console.log('\n📋 TC添加页字段:');
fields.forEach(f => console.log(` "f.label" -> name="f.name"`));
}
// 查看 PCC 列表已有数据(如果有)
console.log('\n📋 PCC列表:');
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/pcc/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const pccList = await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
const list = [];
rows.forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 8) list.push(Array.from(cells).map(c => c.textContent.trim()));
});
return list;
});
console.log(JSON.stringify(pccList, null, 2));
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/debug_tc_add.js
/**
* debug_tc_add.js - 调试 TC 创建
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
}
async function selectProject(page, name) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.locator('input[name="project_search_name"]').fill(name);
await page.keyboard.press('Enter');
await page.waitForTimeout(3000);
await page.evaluate((n) => {
document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === n) cells[1].querySelector('.iconfont').click();
});
}, name);
await page.waitForTimeout(3000);
}
async function main() {
const browser = await chromium.launch({ headless: false, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, 'XW_SUPF_SN4_5_2_8');
// 去 TC 列表页
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/trafficCtl/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
console.log('TC列表页URL:', page.url());
console.log('TC列表页内容(前500字):', (await page.evaluate(() => document.body.innerText)).substring(0, 500));
// 点添加按钮
const addBtn = page.locator('button:has-text("添加")');
console.log('\n添加按钮数量:', await addBtn.count());
await addBtn.click();
await page.waitForTimeout(3000);
// 检查是否有iframe弹窗
const iframeCount = await page.locator('iframe[name="layui-layer-iframe2"]').count();
console.log('iframe数量:', iframeCount);
if (iframeCount > 0) {
const frame = page.frame('layui-layer-iframe2');
console.log('iframe URL:', frame.url());
// 等待 iframe 内容加载
await frame.waitForLoadState('domcontentloaded');
await page.waitForTimeout(2000);
// 打印 iframe 内容
const frameText = await frame.evaluate(() => document.body.innerText);
console.log('\niframe内容:', frameText.substring(0, 1000));
// 找所有input
const inputs = await frame.evaluate(() => {
const result = [];
document.querySelectorAll('input[name], select[name], textarea[name]').forEach(el => {
result.push({ tag: el.tagName, name: el.name, value: el.value, type: el.type });
});
return result;
});
console.log('\niframe输入字段:', JSON.stringify(inputs, null, 2));
// 尝试填写
const tcIdInput = frame.locator('input[name="tcId"]').first();
console.log('\ntcId input 数量:', await tcIdInput.count());
if (await tcIdInput.count() > 0) {
await tcIdInput.fill('tc1');
console.log('已填写 tcId=tc1');
// 点提交
const submitBtn = frame.locator('button:has-text("提交")').first();
console.log('提交按钮数量:', await submitBtn.count());
await submitBtn.click();
await page.waitForTimeout(3000);
console.log('\n提交后URL:', page.url());
// 检查TC列表
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/trafficCtl/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const tcRows = await page.evaluate(() => {
return Array.from(document.querySelectorAll('.layui-table tbody tr')).map(row => {
return Array.from(row.querySelectorAll('td')).map(td => td.textContent.trim());
});
});
console.log('\nTC列表行数:', tcRows.length);
console.log('TC列表:', JSON.stringify(tcRows));
} else {
// 没有tcId input,说明可能还在iframe里但DOM不同
console.log('未找到tcId input,检查是否直接嵌入页面...');
const directInputs = await page.evaluate(() => {
const result = [];
document.querySelectorAll('input[name]').forEach(el => {
result.push({ name: el.name, value: el.value });
});
return result;
});
console.log('页面直接input:', JSON.stringify(directInputs));
}
} else {
// 检查页面上的内容
const pageText = await page.evaluate(() => document.body.innerText);
console.log('\n无iframe,页面内容:', pageText.substring(0, 500));
}
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/debug_xmselect_state.js
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
async function main() {
const browser = await chromium.launch({ headless: true, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
// 选择工程
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.locator('input[name="project_search_name"]').fill('XW_SUPF_5_1_2_4');
await page.keyboard.press('Enter');
await page.waitForTimeout(3000);
await page.evaluate(() => {
document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === 'XW_SUPF_5_1_2_4') cells[1].querySelector('.iconfont').click();
});
});
await page.waitForTimeout(3000);
// 直接打开 PCC 编辑页 ID=17491
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/pcc/edit/17491`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const before = await page.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
return { value: inputs[0]?.value, display: inputs[0]?.parentElement?.textContent?.substring(0, 120) };
});
console.log('编辑前:', JSON.stringify(before));
// 打开 xm-select[0]
await page.evaluate(() => document.querySelectorAll('input.xm-select-default')[0].parentElement.click());
await page.waitForTimeout(1000);
// 检查所有 .xm-option 的状态
const allOptions = await page.evaluate(() => {
return Array.from(document.querySelectorAll('.xm-option')).map(opt => ({
text: opt.textContent.trim(),
classes: opt.className,
visible: opt.offsetWidth > 0 && opt.offsetHeight > 0,
}));
});
console.log('\n下拉打开后所有选项:');
allOptions.forEach(o => console.log(` ['隐藏'] "o.text" classes=o.classes`));
// 检查 .xm-option.show-icon
const showIconOpts = await page.evaluate(() =>
Array.from(document.querySelectorAll('.xm-option.show-icon')).map(o => o.textContent.trim())
);
console.log('\n.show-icon 选项:', showIconOpts);
// 检查 .xm-option.selected
const selectedOpts = await page.evaluate(() =>
Array.from(document.querySelectorAll('.xm-option.selected')).map(o => o.textContent.trim())
);
console.log('.selected 选项:', selectedOpts);
// 尝试点击 qos_high_rate(用 locator)
const qosLocator = page.locator('.xm-option', { hasText: 'qos_high_rate' });
console.log('\nqos_high_rate isVisible:', await qosLocator.isVisible().catch(() => false));
console.log('qos_high_rate count:', await qosLocator.count());
// 点击已选项
const selLocator = page.locator('.xm-option.selected');
if (await selLocator.count() > 0) {
console.log('\n点击已选项...');
await selLocator.first().click();
await page.waitForTimeout(1000);
console.log('点击后 value:', await page.evaluate(() => document.querySelectorAll('input.xm-select-default')[0].value));
}
// 重新打开
await page.evaluate(() => document.querySelectorAll('input.xm-select-default')[0].parentElement.click());
await page.waitForTimeout(1000);
const afterReopen = await page.evaluate(() => {
return Array.from(document.querySelectorAll('.xm-option')).map(opt => ({
text: opt.textContent.trim(),
visible: opt.offsetWidth > 0 && opt.offsetHeight > 0,
}));
});
console.log('\n重新打开后所有选项:');
afterReopen.forEach(o => console.log(` ['隐藏'] "o.text"`));
const showIconAfter = await page.evaluate(() =>
Array.from(document.querySelectorAll('.xm-option.show-icon')).map(o => o.textContent.trim())
);
console.log('重新打开后 .show-icon:', showIconAfter);
const selectedAfter = await page.evaluate(() =>
Array.from(document.querySelectorAll('.xm-option.selected')).map(o => o.textContent.trim())
);
console.log('重新打开后 .selected:', selectedAfter);
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/default-rule-add-SKILL.md
# default-rule-add Skill
## 功能
为指定工程一键配置完整的 **PCF 默认规则**链路,包括:
QoS模板 → Traffic Control → PCC规则 → sm_policy_default → PCF default_smpolicy
## 触发条件
用户说"添加默认规则"、"配置PCF默认规则"、"创建默认规则"、"添加PCF规则",
或任何包含"默认规则"+"工程"的请求。
## 脚本
`skills/5gc/scripts/default-rule-add-skill.js`
## 核心参数
所有参数均可省略,使用默认值。
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `--project` | 工程名 | `XW_S5GC_1` |
| `--qos-id` | QoS模板ID | `qos_default_{时间戳}` |
| `--5qi` | 5QI值 | 自动选择未使用的值(优先8/9/6/5...) |
| `--maxbr-ul` | 上行最大比特率 | `10000000` |
| `--maxbr-dl` | 下行最大比特率 | `20000000` |
| `--gbr-ul` | 上行保证比特率 | `5000000` |
| `--gbr-dl` | 下行保证比特率 | `5000000` |
| `--tc-id` | TC规则ID | `tc_default_{时间戳}` |
| `--flow-status` | TC流状态 | `ENABLED` |
| `--pcc-id` | PCC规则ID | `pcc_default` |
| `--precedence` | PCC优先级 | `63` |
| `--headed` | 显示浏览器窗口(调试用) | off |
## 使用示例
```bash
# 最简用法(自动生成所有ID)
node skills/5gc/scripts/default-rule-add-skill.js --project XW_SUPF_5_1_2_4
# 指定 QoS 和 TC
node skills/5gc/scripts/default-rule-add-skill.js --project XW_SUPF_5_1_2_4 --qos-id qos1 --tc-id tc1 --pcc-id pcc_default
# 完整参数
node skills/5gc/scripts/default-rule-add-skill.js --project XW_SUPF_5_1_2_4 --qos-id qos_new --5qi 8 --maxbr-ul 20000000 --maxbr-dl 50000000 --pcc-id pcc_new --precedence 50
```
## 自然语言调用示例
模型应将以下用户表述转换为对应的脚本调用:
| 用户表述 | 转换为 |
|----------|--------|
| "为XW_SUPF_5_1_2_4添加默认规则" | `--project XW_SUPF_5_1_2_4` |
| "用qos1和tc1创建默认规则" | `--qos-id qos1 --tc-id tc1` |
| "创建5qi=9的默认规则" | `--5qi 9` |
| "优先级设为50" | `--precedence 50` |
| "调测模式运行" | `--headed` |
## 完整链路
```
Step 1: 创建 QoS 模板
→ 5GC仪表: /sim_5gc/predfPolicy/qos/index
Step 2: 创建 Traffic Control
→ 5GC仪表: /sim_5gc/predfPolicy/trafficCtl/index
Step 3: 创建 PCC 规则(绑定 qos + tc)
→ 5GC仪表: /sim_5gc/predfPolicy/pcc/index
Step 4: 更新 sm_policy_default(pccRules 添加新PCC)
→ 5GC仪表: /sim_5gc/smpolicy/default/index
Step 5: PCF default_smpolicy → sm_policy_default
→ 5GC仪表: /sim_5gc/pcf/index
```
## 注意事项
- 同一工程可多次调用,每次创建不同的 PCC/QoS/TC
- sm_policy_default 如已存在会自动编辑追加,不会覆盖原有 pccRules
- 所有 xm-select 下拉使用 Playwright locator 点击 `.xm-option.show-icon`
- 提交前按 Escape 关闭下拉遮罩层
- 5qi 自动选择逻辑:优先从 [8,9,6,5,7,4,3,2,1] 中选未使用的值
FILE:scripts/default-rule-add-skill.js
/**
* default-rule-add-skill.js - PCF 默认规则一键添加工具
*
* 完整链路(一次性完成):
* 1. 创建 QoS 模板(自动选5qi)
* 2. 创建 Traffic Control(ENABLED)
* 3. 创建 PCC 规则(绑定 qos + tc)
* 4. 创建/更新 sm_policy_default(绑定 pcc)
* 5. PCF default_smpolicy → sm_policy_default
*
* 用法:
* node default-rule-add-skill.js --project XW_SUPF_5_1_2_4 --headed
* node default-rule-add-skill.js --project XW_SUPF_5_1_2_4 --qos-id qos1 --tc-id tc1 --pcc-id pcc_default --headed
*
* 参数(均有默认值,可全部省略):
* --project 工程名(默认 XW_S5GC_1)
* --pcf-name PCF实例名称(必填,如 qqq)
* --qos-id QoS模板ID(默认自动生成 qos_default_{timestamp})
* --5qi 5QI值(不指定则自动选择未使用的值)
* --maxbr-ul 上行最大比特率(默认 10000000)
* --maxbr-dl 下行最大比特率(默认 20000000)
* --gbr-ul 上行保证比特率(默认 5000000)
* --gbr-dl 下行保证比特率(默认 5000000)
* --tc-id TC规则ID(默认自动生成 tc_default_{timestamp})
* --flow-status TC流状态(默认 ENABLED)
* --pcc-id PCC规则ID(默认 pcc_default)
* --precedence PCC优先级(默认 63)
* --headed 显示浏览器窗口
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
function parseArgs() {
const args = process.argv.slice(2);
const ts = Date.now();
const opts = {
project: 'XW_S5GC_1',
pcfName: null, // null = 使用 pccId 作为 PCF 名称(向后兼容)
// QoS 参数
qosId: null, // null = 自动生成
qi: null,
maxbrUl: '10000000',
maxbrDl: '20000000',
gbrUl: '5000000',
gbrDl: '5000000',
// TC 参数
tcId: null, // null = 自动生成
flowStatus: 'ENABLED',
// PCC 参数
pccId: null, // null = 自动生成
precedence: '63',
// PCF 参数(网元名称)
pcfName: null, // 若未提供则使用 pccId 作为默认名称
headed: false,
};
for (let i = 0; i < args.length; i++) {
if (args[i] === '--project' || args[i] === '-p') opts.project = args[++i];
else if (args[i] === '--qos-id') opts.qosId = args[++i];
else if (args[i] === '--5qi') opts.qi = args[++i];
else if (args[i] === '--maxbr-ul') opts.maxbrUl = args[++i];
else if (args[i] === '--maxbr-dl') opts.maxbrDl = args[++i];
else if (args[i] === '--gbr-ul') opts.gbrUl = args[++i];
else if (args[i] === '--gbr-dl') opts.gbrDl = args[++i];
else if (args[i] === '--tc-id') opts.tcId = args[++i];
else if (args[i] === '--flow-status') opts.flowStatus = args[++i];
else if (args[i] === '--pcc-id') opts.pccId = args[++i];
else if (args[i] === '--precedence') opts.precedence = args[++i];
else if (args[i] === '--pcf-name') opts.pcfName = args[++i];
else if (args[i] === '--headed') opts.headed = true;
}
// 自动生成ID(如果未指定)
if (!opts.qosId) opts.qosId = `qos_default_ts`;
if (!opts.tcId) opts.tcId = `tc_default_ts`;
if (!opts.pccId) opts.pccId = `pcc_default`;
// 如果未提供 PCF 名称,默认使用 PCC ID(与业务保持一致)
if (!opts.pcfName) opts.pcfName = opts.pccId;
return opts;
}
// ─── 通用工具 ────────────────────────────────────────────────────────────
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, name) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.locator('input[name="project_search_name"]').fill(name);
await page.keyboard.press('Enter');
await page.waitForTimeout(3000);
const found = await page.evaluate((n) => {
let clicked = false;
document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === n) {
cells[1].querySelector('.iconfont')?.click();
clicked = true;
}
});
return clicked;
}, name);
if (!found) { console.error(`❌ 未找到工程: name`); process.exit(1); }
await page.waitForTimeout(3000);
console.log(`✅ 工程 "name" 已选`);
}
async function goto(page, url) {
await page.goto(`globalBaseUrlurl`, { waitUntil: 'load', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
}
// ─── Step 1: 创建 QoS 模板 ──────────────────────────────────────────────
async function getUsedQis(page) {
await goto(page, '/sim_5gc/predfPolicy/qos/index');
return await page.evaluate(() => {
const qis = [];
document.querySelectorAll('.layui-table tbody tr').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 4 && cells[3].textContent.trim()) {
qis.push(parseInt(cells[3].textContent.trim()));
}
});
return qis;
});
}
async function addQos(page, opts) {
// 自动选 5qi(先获取已用列表)
if (!opts.qi) {
await goto(page, '/sim_5gc/predfPolicy/qos/index');
const used = await page.evaluate(() => {
const qis = [];
document.querySelectorAll('.layui-table tbody tr').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 4 && cells[3].textContent.trim()) {
qis.push(parseInt(cells[3].textContent.trim()));
}
});
return qis;
});
const candidates = [8, 9, 6, 5, 7, 4, 3, 2, 1];
for (const c of candidates) { if (!used.includes(c)) { opts.qi = String(c); break; } }
if (!opts.qi) opts.qi = String(used[0] + 1);
console.log(` i️ 已用5qi: used.join(','),自动选择 opts.qi`);
}
await goto(page, '/sim_5gc/predfPolicy/qos/index');
await page.locator('button:has-text("添加")').click();
await page.waitForTimeout(3000);
await page.waitForSelector('iframe[name="layui-layer-iframe2"]', { timeout: 10000 });
const frame = page.frame('layui-layer-iframe2');
await frame.waitForLoadState('domcontentloaded');
await page.waitForTimeout(500);
await frame.locator('input[name="qosId"]').fill(opts.qosId);
await frame.locator('input[name="5qi"]').fill(opts.qi);
await frame.locator('input[name="maxbrUl"]').fill(opts.maxbrUl);
await frame.locator('input[name="maxbrDl"]').fill(opts.maxbrDl);
await frame.locator('input[name="gbrUl"]').fill(opts.gbrUl);
await frame.locator('input[name="gbrDl"]').fill(opts.gbrDl);
await frame.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log(` ✅ QoS模板 opts.qosId 已创建 (5qi=opts.qi)`);
}
// ─── Step 2: 创建 TC ────────────────────────────────────────────────────
async function addTc(page, opts) {
await goto(page, '/sim_5gc/predfPolicy/trafficCtl/index');
await page.locator('button:has-text("添加")').click();
await page.waitForTimeout(3000);
// 等待 iframe 出现在 DOM 中
await page.waitForSelector('iframe[name="layui-layer-iframe2"]', { timeout: 10000 });
const frame = page.frame('layui-layer-iframe2');
await frame.waitForLoadState('domcontentloaded');
// 等待 tcId input 出现
await frame.waitForSelector('input[name="tcId"]', { timeout: 10000 });
await page.waitForTimeout(500);
await frame.locator('input[name="tcId"]').fill(opts.tcId);
// 等待 select[name="flowStatus"] 出现在 DOM 中
const sel = frame.locator('select[name="flowStatus"]');
try {
await sel.waitFor({ state: 'attached', timeout: 5000 });
await sel.selectOption(opts.flowStatus, { force: true });
console.log(` flowStatus = opts.flowStatus`);
} catch(e) {
// 如果 select 不存在(如没有 flowStatus 字段),跳过
console.log(` i️ flowStatus select 不存在,跳过`);
}
await frame.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log(` ✅ TC opts.tcId 已创建 (flowStatus=opts.flowStatus)`);
}
// ─── Step 3: 创建 PCC ────────────────────────────────────────────────────
async function addPcc(page, opts) {
await goto(page, '/sim_5gc/predfPolicy/pcc/index');
// 检查是否已存在同名 PCC,存在则先删除
const existingId = await page.evaluate((targetId) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 10 && cells[2].textContent.trim() === targetId) {
return cells[1].textContent.trim(); // 返回数字ID
}
}
return null;
}, opts.pccId);
if (existingId) {
// 删除旧记录
await page.evaluate((id) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 10 && cells[1].textContent.trim() === id) {
const links = cells[9].querySelectorAll('a');
for (const l of links) { if (l.textContent.trim() === '删除') { l.click(); return; } }
}
}
}, existingId);
await page.waitForTimeout(1500);
// 处理删除确认对话框
const confirmBtn = page.locator('.layui-layer-dialog .layui-layer-btn0, .layui-layer-btn a:first-child');
if (await confirmBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
await confirmBtn.click();
await page.waitForTimeout(2000);
}
// 确保遮罩关闭
await page.keyboard.press('Escape');
await page.waitForTimeout(1000);
console.log(` 🗑️ 已删除旧 PCC opts.pccId,准备重建`);
}
await page.locator('button:has-text("添加")').click();
await page.waitForTimeout(3000);
await page.waitForFunction(() => window.location.href.includes('/predfPolicy/pcc/edit'), { timeout: 10000 });
await page.waitForTimeout(3000);
await page.locator('input[name="pccRuleId"]').fill(opts.pccId);
await page.locator('input[name="precedence"]').fill(opts.precedence);
// xm-select[0] = refQosData
await page.evaluate(() => document.querySelectorAll('input.xm-select-default')[0].parentElement.click());
await page.waitForTimeout(1000);
const qosOpt = page.locator('.xm-option.show-icon', { hasText: opts.qosId });
if (await qosOpt.isVisible({ timeout: 3000 }).catch(() => false)) await qosOpt.click();
await page.waitForTimeout(500);
// xm-select[1] = refTcData
await page.evaluate(() => document.querySelectorAll('input.xm-select-default')[1].parentElement.click());
await page.waitForTimeout(1000);
const tcOpt = page.locator('.xm-option.show-icon', { hasText: opts.tcId });
if (await tcOpt.isVisible({ timeout: 3000 }).catch(() => false)) await tcOpt.click();
await page.waitForTimeout(500);
await page.keyboard.press('Escape');
await page.waitForTimeout(300);
await page.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log(` ✅ PCC规则 opts.pccId 已创建 (refQosData=opts.qosId, refTcData=opts.tcId)`);
}
// ─── Step 4: 创建/更新 sm_policy_default(使用正确的 form_data 格式)────────────
async function addOrUpdateSmpolicy(page, pccId) {
console.log(`\n=== Step 4: 创建/更新 sm_policy_default (pccRules=pccId) ===`);
// 1. 进入 PCF,选中 pccId 行,点击 smpolicy 按钮
await goto(page, '/sim_5gc/pcf/index');
await page.waitForTimeout(3000);
await page.evaluate((targetName) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === targetName) {
const cb = row.querySelector('input[type="checkbox"]');
if (cb) cb.click();
}
}
}, pccId);
await page.waitForTimeout(500);
await page.locator('button:has-text("smpolicy")').click({ force: true });
await page.waitForTimeout(3000);
console.log(' smpolicy 页面 URL:', page.url());
// 2. 获取 CSRF token(从页面)
const token = await page.evaluate(() => document.querySelector('input[name="_token"]')?.value || '');
if (!token) {
console.error(' ❌ 未找到 _token');
return false;
}
console.log(' _token: ...' + token.substring(0, 10) + '...');
// 3. 检查是否已有 sm_policy_default
const existing = await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === 'sm_policy_default') {
return { id: cells[1].textContent.trim(), pccRules: cells[4].textContent.trim() };
}
}
return null;
});
if (!existing) {
console.log(' ℹ️ sm_policy_default 不存在,正在创建...');
await page.locator('button:has-text("添加")').click({ force: true });
await page.waitForTimeout(3000);
await page.waitForSelector('iframe[name="layui-layer-iframe2"]', { timeout: 15000 });
const frm = page.frame('layui-layer-iframe2');
await frm.waitForLoadState('domcontentloaded');
await page.waitForTimeout(1000);
// 构造正确的 form_data JSON
const formDataJson = JSON.stringify({
name: 'sm_policy_default',
pccRules: [pccId],
reflectiveQoSTimer: 86400
});
const params = new URLSearchParams();
params.append('_token', token);
params.append('form_data', formDataJson);
console.log(' form_data:', formDataJson);
const resp = await frm.evaluate(async (args) => {
const { tok, bodyStr } = args;
try {
const r = await fetch('/sim_5gc/smpolicy/default/edit', {
method: 'POST',
body: bodyStr,
credentials: 'include',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'x-csrf-token': tok,
'x-requested-with': 'XMLHttpRequest'
}
});
const text = await r.text();
return { status: r.status, body: text };
} catch(e) {
return { error: e.message };
}
}, { tok: token, bodyStr: params.toString() });
console.log(' 创建响应:', resp.status);
if (resp.status >= 400) {
try { console.log(' 错误:', JSON.stringify(JSON.parse(resp.body))); }
catch { console.log(' 响应:', resp.body?.substring(0, 200)); }
return false;
} else {
console.log(' ✅ 创建成功!响应:', resp.body?.substring(0, 200));
return true;
}
} else {
console.log(' ✅ sm_policy_default 已存在 (id=' + existing.id + ', pccRules=' + existing.pccRules + ')');
if (existing.pccRules.includes(pccId)) {
console.log(' ✅ pccRules 已包含 ' + pccId);
return true;
} else {
console.log(' ℹ️ 更新 sm_policy_default,添加 pccRules=' + pccId + '...');
// 点击编辑
await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === 'sm_policy_default') {
const links = row.querySelectorAll('a');
for (const l of links) { if (l.textContent.trim() === '编辑') { l.click(); return; } }
}
}
});
await page.waitForTimeout(3000);
await page.waitForSelector('iframe[name="layui-layer-iframe2"]', { timeout: 15000 });
const frm = page.frame('layui-layer-iframe2');
await frm.waitForLoadState('domcontentloaded');
await page.waitForTimeout(1000);
// 获取当前 pccRules
const currentPcc = await frm.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
return inputs.length > 1 ? inputs[1].value : '';
});
const existingRules = currentPcc ? currentPcc.split(',').filter(Boolean) : [];
if (!existingRules.includes(pccId)) existingRules.push(pccId);
const recId = await frm.evaluate(() => {
const el = document.querySelector('input[name="id"]');
return el ? el.value : '';
});
// 更新用的 form_data
const formDataJson = JSON.stringify({
name: 'sm_policy_default',
pccRules: existingRules,
reflectiveQoSTimer: 86400,
id: recId
});
const params = new URLSearchParams();
params.append('_token', token);
params.append('form_data', formDataJson);
const resp = await frm.evaluate(async (args) => {
const { tok, bodyStr, recId } = args;
try {
const r = await fetch('/sim_5gc/smpolicy/default/edit/' + recId, {
method: 'POST',
body: bodyStr,
credentials: 'include',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'x-csrf-token': tok,
'x-requested-with': 'XMLHttpRequest'
}
});
const text = await r.text();
return { status: r.status, body: text };
} catch(e) {
return { error: e.message };
}
}, { tok: token, bodyStr: params.toString(), recId });
console.log(' 更新响应:', resp.status);
if (resp.status >= 400) {
try { console.log(' 错误:', JSON.stringify(JSON.parse(resp.body))); }
catch { console.log(' 响应:', resp.body?.substring(0, 200)); }
return false;
} else {
console.log(' ✅ 更新成功!响应:', resp.body?.substring(0, 200));
return true;
}
}
}
}// ─── Step 5: PCF default_smpolicy ────────────────────────────────────────
// 正确流程(根据 UI 调试结果):
// 1. 在 PCF 列表先选中 qqq 行(单击,不要点编辑)
// 2. 再点击工具栏 "smpolicy" 按钮 → 页面加载 sm_policy_default 表单(带 qqq 上下文)
// 3. 创建 sm_policy_default(此时 name 应为 qqq 关联的默认策略名)
// 4. 保存后返回 PCF 编辑弹窗 → default_smpolicy 下拉有数据 → 选择 → 提交
async function setPcfDefaultSmpolicy(page, pcfName) {
console.log(`\n=== Step 5: 配置 PCF "pcfName" default_smpolicy ===`);
// 1. 进入 PCF 列表,点击指定 PCF 的编辑按钮
await goto(page, '/sim_5gc/pcf/index');
await page.waitForTimeout(3000);
const clicked = await page.evaluate((targetName) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === targetName) {
const links = row.querySelectorAll('a');
for (const l of links) { if (l.textContent.trim() === '编辑') { l.click(); return true; } }
}
}
return false;
}, pcfName);
if (!clicked) {
console.error(` ❌ 未找到 PCF "pcfName" 的编辑按钮`);
return false;
}
await page.waitForTimeout(3000);
await page.waitForSelector('iframe[name="layui-layer-iframe2"]', { timeout: 15000 });
const frm = page.frame('layui-layer-iframe2');
await frm.waitForLoadState('domcontentloaded');
await page.waitForTimeout(1000);
// 2. 获取 token 和 PCF ID
const token = await frm.evaluate(() => {
const meta = document.querySelector('meta[name="csrf-token"]');
return meta ? meta.getAttribute('content') : '';
});
if (!token) {
console.error(' ❌ 未找到 CSRF token (meta[name="csrf-token"])');
return false;
}
console.log(` Token: ...token.substring(0, 10)... (from meta tag)`);
const pcfId = await frm.evaluate(() => document.querySelector('input[name="id"]')?.value || '');
if (!pcfId) {
console.error(' ❌ 未找到 PCF ID');
return false;
}
console.log(` PCF ID: pcfId`);
// 3. 获取当前表单数据
const formData = await frm.evaluate(() => {
const form = document.querySelector('form');
if (!form) return {};
const data = new FormData(form);
const entries = {};
data.forEach((v, k) => { entries[k] = v; });
return entries;
});
// 4. 获取 sm_policy_default 的 ID - 通过主页面,不关闭弹窗
console.log(' 获取 sm_policy_default 的 ID...');
// 在主页面(不是 iframe)中打开新标签页查看 smpolicy 列表
const smpId = await page.evaluate(async () => {
// 在新窗口中打开 smpolicy 页面
const newWindow = window.open('/sim_5gc/smpolicy/default/index', '_blank');
if (!newWindow) return '';
// 等待新窗口加载
await new Promise(resolve => setTimeout(resolve, 2000));
// 从新窗口获取数据
const rows = newWindow.document.querySelectorAll('.layui-table tbody tr');
let foundId = '';
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === 'sm_policy_default') {
foundId = cells[1].textContent.trim();
break;
}
}
// 关闭新窗口
newWindow.close();
return foundId;
});
if (!smpId) {
console.error(' ❌ 未找到 sm_policy_default 的 ID');
// 尝试使用已知的 ID(如果之前创建过)
console.log(' ℹ️ 尝试使用默认 ID 9771');
return '9771'; // 返回默认 ID,让调用者决定
}
console.log(` sm_policy_default ID: smpId`);
// 5. 构造更新数据 - 设置 default_smpolicy 为 sm_policy_default 的 ID
const updateData = {
...formData,
'assoc_smpolicy[default_smpolicy]': smpId, // 使用 ID 而不是名称
'_token': token
};
// 移除空值(除了 select 字段)
Object.keys(updateData).forEach(key => {
if (updateData[key] === '' && !key.includes('select') && key !== 'assoc_smpolicy[default_smpolicy]') {
delete updateData[key];
}
});
// 6. 发送 POST 请求
const params = new URLSearchParams();
Object.entries(updateData).forEach(([k, v]) => {
params.append(k, v);
});
console.log(` 提交数据: assoc_smpolicy[default_smpolicy]=updateData['assoc_smpolicy[default_smpolicy]']`);
const resp = await frm.evaluate(async (args) => {
const { pcfId, bodyStr, token } = args;
try {
const r = await fetch(`/sim_5gc/pcf/edit/pcfId`, {
method: 'POST',
body: bodyStr,
credentials: 'include',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'x-csrf-token': token,
'x-requested-with': 'XMLHttpRequest'
}
});
const text = await r.text();
return { status: r.status, body: text };
} catch(e) {
return { error: e.message };
}
}, { pcfId, bodyStr: params.toString(), token });
console.log(` 响应状态: resp.status`);
if (resp.status >= 400) {
try { console.log(` 错误: JSON.stringify(JSON.parse(resp.body))`); }
catch { console.log(` 响应: resp.body?.substring(0, 200)`); }
return false;
} else {
console.log(` ✅ PCF "pcfName" default_smpolicy 设置成功!响应: resp.body?.substring(0, 100)`);
return true;
}
}async function verify(page, opts) {
await goto(page, '/sim_5gc/predfPolicy/pcc/index');
const pcc = await page.evaluate((id) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 8 && cells[2].textContent.trim() === id) {
return { pccRuleId: cells[2].textContent.trim(), precedence: cells[4].textContent.trim(), refQosData: cells[5].textContent.trim(), refTcData: cells[6].textContent.trim() };
}
}
return null;
}, opts.pccId);
await goto(page, '/sim_5gc/smpolicy/default/index');
const smp = await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 6 && cells[2].textContent.trim() === 'sm_policy_default') return { pccRules: cells[4].textContent.trim() };
}
return null;
});
await goto(page, '/sim_5gc/pcf/index');
await page.waitForTimeout(3000);
// 点击指定的 PCF 编辑按钮
const pcfName = opts.pcfName || opts.pccId;
const clicked = await page.evaluate((targetName) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === targetName) {
const links = row.querySelectorAll('a');
for (const l of links) { if (l.textContent.trim() === '编辑') { l.click(); return true; } }
}
}
return false;
}, pcfName);
if (!clicked) {
console.log(' ⚠️ 未找到 PCF "' + pcfName + '" 的编辑按钮,使用第一个 PCF');
await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
if (rows.length > 0) rows[0].querySelector('a')?.click();
});
}
await page.waitForTimeout(3000); const frame = page.frame('layui-layer-iframe2');
const pcfSmp = frame ? await frame.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
return inputs[0]?.parentElement?.textContent?.match(/[\w_]+/g)?.[0] || '';
}) : '';
console.log('\n========================================');
console.log('验证结果');
console.log('========================================');
const tests = [
{ name: `PCC opts.pccId 存在`, pass: !!pcc },
{ name: `refQosData = opts.qosId`, pass: pcc?.refQosData === opts.qosId },
{ name: `refTcData = opts.tcId`, pass: pcc?.refTcData === opts.tcId },
{ name: `sm_policy_default 包含 opts.pccId`, pass: smp?.pccRules?.includes(opts.pccId) },
{ name: `PCF default_smpolicy = sm_policy_default`, pass: pcfSmp === 'sm_policy_default' },
];
for (const t of tests) console.log(` '❌' t.name`);
if (pcc) console.log(`\n PCC: pccRuleId=pcc.pccRuleId, precedence=pcc.precedence, refQosData=pcc.refQosData, refTcData=pcc.refTcData`);
if (smp) console.log(` smp: pccRules=[smp.pccRules]`);
console.log('========================================');
return tests.every(t => t.pass);
}
// ─── 主流程 ─────────────────────────────────────────────────────────────
async function main() {
const opts = parseArgs();
console.log('\n========================================');
console.log('PCF 默认规则一键配置');
console.log(`工程: opts.project`);
console.log(`QoS: opts.qosId (5qi=opts.qi || '自动')`);
console.log(`TC: opts.tcId (flowStatus=opts.flowStatus)`);
console.log(`PCF: opts.pcfName || opts.pccId`);
console.log(`PCC: opts.pccId (precedence=opts.precedence)`);
console.log('========================================\n');
const browser = await chromium.launch({ headless: !opts.headed, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, opts.project);
console.log('📦 Step 1: 创建 QoS 模板...');
await addQos(page, opts);
console.log('📦 Step 2: 创建 Traffic Control...');
await addTc(page, opts);
console.log('📦 Step 3: 创建 PCC 规则...');
await addPcc(page, opts);
console.log('📦 Step 4: 更新 sm_policy_default...');
await addOrUpdateSmpolicy(page, opts.pccId);
console.log('📦 Step 5: 配置 PCF default_smpolicy...');
const pcfName = opts.pcfName || opts.pccId; // 向后兼容:如果没有指定 pcf-name,使用 pcc-id
await setPcfDefaultSmpolicy(page, pcfName);
console.log('\n📦 验证...');
const ok = await verify(page, opts);
console.log(ok ? '\n🎉 全部完成!' : '\n⚠️ 部分步骤存在问题,请检查');
await browser.close();
process.exit(ok ? 0 : 1);
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/default-smpolicy-edit-skill.js
/**
* default-smpolicy-edit-skill.js - Default Smpolicy 配置修改技能
*
* 完整链路(只读模式):
* 读取 PCC规则 pcc2 的 flowDescription(直接导航到 /predfPolicy/pcc/edit/{id})
* 读取信令负载 dedicated 的 pcc_rule(直接导航到 /load/edit/{id})
*
* 链路(修改模式):
* PCC规则 /predfPolicy/pcc/edit/{id} → flowInfos[flowDescription][] 修改
* 信令负载 /load/edit/{id} → process_conf[2][params][2][param_val] 修改
*
* 用法(读取):
* node default-smpolicy-edit-skill.js --project XW_SUPF_5_1_2_4
*
* 用法(修改):
* node default-smpolicy-edit-skill.js --project XW_SUPF_5_1_2_4
* --flow-desc "permit out ip from aaaa::203:9:121 to assigned"
* --load-ip "permit out ip from aaaa::203:9:121/24 to assigned"
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
function parseArgs() {
const args = process.argv.slice(2);
const opts = {
project: 'XW_S5GC_1',
flowDesc: null, // PCC规则 flowDescription 新值
loadIp: null, // 信令负载 pcc_rule 新值
loadName: 'dedicated', // 信令负载名称(默认 dedicated)
pccRuleId: 'pcc2', // PCC规则 ID
headed: false,
};
for (let i = 0; i < args.length; i++) {
if (args[i] === '--project' || args[i] === '-p') opts.project = args[++i];
else if (args[i] === '--flow-desc') opts.flowDesc = args[++i];
else if (args[i] === '--load-ip') opts.loadIp = args[++i];
else if (args[i] === '--load-name') opts.loadName = args[++i];
else if (args[i] === '--pcc-rule-id') opts.pccRuleId = args[++i];
else if (args[i] === '--headed') opts.headed = true;
}
return opts;
}
// ─── 登录 ───────────────────────────────────────────────────────────────
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
// ─── 选择工程 ───────────────────────────────────────────────────────────
async function selectProject(page, projectName) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
const clicked = await page.evaluate((name) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === name) {
const icon = cells[1].querySelector('.iconfont');
if (icon) { icon.click(); return true; }
}
}
return false;
}, projectName);
if (!clicked) {
for (let p = 2; p <= 10; p++) {
const nextBtn = document.querySelector('.jsgrid-pager a.jsgrid-pager-next');
if (!nextBtn) break;
nextBtn.click();
await new Promise(r => setTimeout(r, 1000));
const found = page.evaluate((n) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === n) {
const icon = cells[1].querySelector('.iconfont');
if (icon) { icon.click(); return true; }
}
}
return false;
}, projectName);
if (found) break;
}
}
await page.waitForTimeout(3000);
console.log(`✅ 工程 "projectName" 已选`);
}
// ─── 修改 PCC 规则 flowDescription ──────────────────────────────────────
// 直接导航到 /predfPolicy/pcc/edit/{id}(无需弹窗)
// input[name="flowInfos[flowDescription][]"]
async function editPccFlowDesc(page, pccRuleId, newFlowDesc) {
// ① 先到列表页获取 pccRuleId 对应的数字 ID
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/pcc/index`, { waitUntil: 'commit', ignoreHTTPSErrors: true });
// 等 pccRuleId 文本出现在页面中
try {
await page.waitForFunction(
(id) => document.body.textContent.includes(id),
pccRuleId,
{ timeout: 20000 }
);
} catch(e) {
console.log(' ❌ pcc2 未出现在页面文本中(20秒超时)');
return null;
}
// 等 DOM 彻底渲染
await page.waitForTimeout(3000);
const pccId = await page.evaluate((targetId) => {
const tables = document.querySelectorAll('.layui-table');
for (let ti = 0; ti < tables.length; ti++) {
const rows = tables[ti].querySelectorAll('tbody tr');
for (let ri = 0; ri < rows.length; ri++) {
const cells = rows[ri].querySelectorAll('td');
if (cells.length >= 9 && cells[2].textContent.trim() === targetId) {
return JSON.stringify({ found: true, id: cells[1].textContent.trim() });
}
}
}
return JSON.stringify({ found: false });
}, pccRuleId);
const parsed = JSON.parse(pccId);
console.log(' [debug] PCC lookup:', JSON.stringify(parsed));
if (!parsed.found) { console.log(`❌ 未找到PCC规则ID: pccRuleId`); return null; }
const numericId = parsed.id;
console.log(`✅ 找到 PCC规则 "pccRuleId",数字ID=numericId`);
// ② 直接导航到编辑页
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/pcc/edit/numericId`, { waitUntil: 'commit', ignoreHTTPSErrors: true });
// 等 flowDescription input 出现
await page.waitForFunction(
() => document.querySelector('input[name="flowInfos[flowDescription][]"]') !== null,
{ timeout: 15000 }
);
await page.waitForTimeout(1000);
// ③ 修改 flowDescription
const result = await page.evaluate((arg) => {
const inputs = document.querySelectorAll('input[name="flowInfos[flowDescription][]"]');
if (inputs.length > 0) {
const inp = inputs[0];
const oldVal = inp.value;
if (arg.newVal !== null) {
inp.value = arg.newVal;
inp.dispatchEvent(new Event('input', {bubbles: true}));
}
return { found: true, value: oldVal };
}
return { found: false };
}, { newVal: newFlowDesc });
if (result.found) {
if (newFlowDesc !== null) {
console.log(` flowDescription → "newFlowDesc"`);
// ④ 提交
await page.locator('button:has-text("提交")').click();
await page.waitForTimeout(2000);
} else {
console.log(` 当前 flowDescription: "result.value"`);
return result.value;
}
} else {
console.log('⚠️ 未找到 flowDescription input');
}
return true;
}
// ─── 修改信令负载 pcc_rule ───────────────────────────────────────────────
// 直接导航到 /load/edit/{id}
// textarea: process_conf[2][params][2][param_val] 实际是 input[type=text]
async function editLoadPccRule(page, loadName, newIp) {
// ① 先到列表页获取 dedicated 对应的数字 ID
await page.goto(`globalBaseUrl/sim_5gc/load/index`, { waitUntil: 'commit', ignoreHTTPSErrors: true });
// 等 dedicated 文本出现在页面中
try {
await page.waitForFunction(
(name) => document.body.textContent.includes(name),
loadName,
{ timeout: 20000 }
);
} catch(e) {
console.log(' ❌ dedicated 未出现在页面文本中(20秒超时)');
return null;
}
await page.waitForTimeout(3000);
const loadIdResult = await page.evaluate((targetName) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 5 && cells[2].textContent.trim() === targetName) {
// cells[0]=checkbox, cells[1]=ID, cells[2]=名称, ...
return JSON.stringify({ found: true, id: cells[1].textContent.trim() });
}
}
return JSON.stringify({ found: false });
}, loadName);
const parsedLoad = JSON.parse(loadIdResult);
console.log(' [debug] Load lookup:', JSON.stringify(parsedLoad));
if (!parsedLoad.found) { console.log(`❌ 未找到信令负载条目ID: loadName`); return null; }
const loadId = parsedLoad.id;
if (!loadId) { console.log(`❌ 未找到信令负载条目ID: loadName`); return null; }
console.log(`✅ 找到信令负载 "loadName",ID=loadId`);
// ② 直接导航到编辑页
await page.goto(`globalBaseUrl/sim_5gc/load/edit/loadId`, { waitUntil: 'commit', ignoreHTTPSErrors: true });
// 等 pcc_rule input 出现
await page.waitForFunction(
() => document.querySelector('input[name="process_conf[2][params][2][param_val]"]') !== null,
{ timeout: 15000 }
);
await page.waitForTimeout(1000);
// ③ 修改 pcc_rule
const result = await page.evaluate((arg) => {
// pcc_rule 在 process_conf[2][params][2][param_val]
const inputs = Array.from(document.querySelectorAll('input'));
const pccInput = inputs.find(i => i.name === 'process_conf[2][params][2][param_val]');
if (pccInput) {
const oldVal = pccInput.value;
if (arg.newVal !== null) {
pccInput.value = arg.newVal;
pccInput.dispatchEvent(new Event('input', {bubbles: true}));
}
return { found: true, value: oldVal };
}
return { found: false };
}, { newVal: newIp });
if (result.found) {
if (newIp !== null) {
console.log(` pcc_rule → "newIp"`);
// ④ 提交
await page.locator('button:has-text("提交")').click();
await page.waitForTimeout(2000);
} else {
console.log(` 当前 pcc_rule: "result.value"`);
return result.value;
}
} else {
console.log('⚠️ 未找到 pcc_rule input');
}
return true;
}
// ─── 主流程 ───────────────────────────────────────────────────────────
async function main() {
const opts = parseArgs();
const browser = await chromium.launch({
headless: !opts.headed,
args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*']
});
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, opts.project);
// ─── PCC规则 flowDescription ───────────────────────────────────────
if (opts.flowDesc !== null) {
console.log(`\n▶ 修改 PCC规则 "opts.pccRuleId" flowDescription`);
await editPccFlowDesc(page, opts.pccRuleId, opts.flowDesc);
} else {
console.log(`\n▶ 读取 PCC规则 "opts.pccRuleId" 当前 flowDescription`);
const val = await editPccFlowDesc(page, opts.pccRuleId, null);
if (val) console.log(` → "val"`);
}
// ─── 信令负载 pcc_rule ──────────────────────────────────────────────
if (opts.loadIp !== null) {
console.log(`\n▶ 修改信令负载 "opts.loadName" pcc_rule`);
await editLoadPccRule(page, opts.loadName, opts.loadIp);
} else {
console.log(`\n▶ 读取信令负载 "opts.loadName" 当前 pcc_rule`);
const val = await editLoadPccRule(page, opts.loadName, null);
if (val) console.log(` → "val"`);
}
console.log('\n✅ 完成');
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/explore_pcf_new.js
/**
* explore_pcf_new.js - 探索工程 XW_SUPF_SN4_5_2_8 的 PCF 配置情况
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, name) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
const clicked = await page.evaluate((n) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === n) {
cells[1].querySelector('.iconfont').click();
return true;
}
}
return false;
}, name);
if (!clicked) {
console.log('❌ 未找到工程:', name);
process.exit(1);
}
await page.waitForTimeout(3000);
console.log(`✅ 工程 "name" 已选`);
}
async function main() {
const browser = await chromium.launch({ headless: false, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, 'XW_SUPF_SN4_5_2_8');
// 1. 查看 PCF 列表
console.log('\n📋 PCF 列表:');
await page.goto(`globalBaseUrl/sim_5gc/pcf/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const pcfList = await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
const pcfs = [];
rows.forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 10) {
pcfs.push({ id: cells[1].textContent.trim(), name: cells[2].textContent.trim() });
}
});
return pcfs;
});
console.log(` 找到 pcfList.length 个 PCF:`, JSON.stringify(pcfList));
// 2. 点击第一个 PCF 的编辑,查看 sm_policy_default 配置
if (pcfList.length > 0) {
await page.goto(`globalBaseUrl/sim_5gc/pcf/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.evaluate((targetId) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 10 && cells[1].textContent.trim() === targetId) {
const links = cells[9].querySelectorAll('a');
for (const link of links) {
if (link.textContent.trim() === '编辑') { link.click(); return; }
}
}
}
}, pcfList[0].id);
await page.waitForTimeout(3000);
const frame = page.frame('layui-layer-iframe2');
if (frame) {
const iframeText = await frame.evaluate(() => document.body.innerText);
console.log('\n📄 PCF编辑弹窗内容:');
console.log(iframeText.substring(0, 2000));
}
}
// 3. 查看 smpolicy/default 列表
console.log('\n📋 smpolicy/default 列表:');
await page.goto(`globalBaseUrl/sim_5gc/smpolicy/default/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const smpolicyList = await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
const list = [];
rows.forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 6) {
list.push({
id: cells[1].textContent.trim(),
name: cells[2].textContent.trim(),
pccRules: cells[4].textContent.trim(),
});
}
});
return list;
});
console.log(` 找到 smpolicyList.length 个默认规则:`, JSON.stringify(smpolicyList));
// 4. 查看已有 QoS 模板
console.log('\n📋 QoS 模板列表:');
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/qos/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const qosList = await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
const list = [];
rows.forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 8) {
list.push({
qosId: cells[2].textContent.trim(),
qi: cells[3].textContent.trim(),
maxbrUl: cells[4].textContent.trim(),
maxbrDl: cells[5].textContent.trim(),
gbrUl: cells[6].textContent.trim(),
gbrDl: cells[7].textContent.trim(),
});
}
});
return list;
});
console.log(` 找到 qosList.length 个QoS模板:`, JSON.stringify(qosList));
// 5. 查看 PCC 规则
console.log('\n📋 PCC 规则列表:');
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/pcc/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const pccList = await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
const list = [];
rows.forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 6) {
list.push({
pccRuleId: cells[2].textContent.trim(),
precedence: cells[4].textContent.trim(),
qosId: cells[5].textContent.trim(),
});
}
});
return list;
});
console.log(` 找到 pccList.length 个PCC规则:`, JSON.stringify(pccList));
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/explore_smpolicy_dnn.js
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
async function main() {
const browser = await chromium.launch({ headless: false, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.locator('input[name="project_search_name"]').fill('XW_SUPF_5_1_2_4');
await page.keyboard.press('Enter');
await page.waitForTimeout(3000);
await page.evaluate(() => {
document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === 'XW_SUPF_5_1_2_4') cells[1].querySelector('.iconfont').click();
});
});
await page.waitForTimeout(3000);
// 探索 DNN 列表页
await page.goto(`globalBaseUrl/sim_5gc/smpolicy/dnn/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
console.log('DNN列表页:', page.url());
// 点添加
await page.locator('button:has-text("添加")').click();
await page.waitForTimeout(3000);
const frames = page.frames();
const editFrame = frames.find(f => f.url().includes('/edit'));
if (editFrame) {
await editFrame.waitForLoadState('domcontentloaded');
console.log('编辑帧:', editFrame.url());
const fields = await editFrame.evaluate(() => {
const result = [];
document.querySelectorAll('input[name], select[name]').forEach(el => result.push({ name: el.name, value: el.value }));
return result;
});
console.log('字段:', JSON.stringify(fields));
const xmState = await editFrame.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
return Array.from(inputs).map((inp, i) => ({ idx: i, display: inp.parentElement?.textContent?.substring(0, 80) }));
});
console.log('xm-select:', JSON.stringify(xmState));
const btns = await editFrame.evaluate(() => Array.from(document.querySelectorAll('button')).map(b => b.textContent.trim()));
console.log('按钮:', btns);
}
// 关闭弹窗,探索编辑页
await page.keyboard.press('Escape');
await page.waitForTimeout(1000);
// 找已有记录看能否编辑
await page.goto(`globalBaseUrl/sim_5gc/smpolicy/dnn/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const rows = await page.evaluate(() => {
const result = [];
document.querySelectorAll('.layui-table tbody tr').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 8) result.push({ id: cells[1].textContent.trim(), name: cells[2].textContent.trim(), dnn: cells[3].textContent.trim() });
});
return result;
});
console.log('\n现有DNN策略:', JSON.stringify(rows.slice(0, 3)));
if (rows.length > 0) {
// 直接访问编辑页
await page.goto(`globalBaseUrl/sim_5gc/smpolicy/dnn/edit/rows[0].id`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
console.log('\n编辑页URL:', page.url());
console.log('页面内容:', (await page.evaluate(() => document.body.innerText)).substring(0, 500));
const dnnFrames = page.frames();
console.log('iframe数量:', dnnFrames.length);
const dnnFields = await page.evaluate(() => {
const result = [];
document.querySelectorAll('input[name], select[name]').forEach(el => result.push({ name: el.name, value: el.value }));
return result;
});
console.log('主页面字段:', JSON.stringify(dnnFields));
const dnnBtns = await page.evaluate(() => Array.from(document.querySelectorAll('button')).map(b => b.textContent.trim()));
console.log('主页面按钮:', dnnBtns);
}
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/explore_smpolicy_ue.js
/**
* explore_smpolicy_ue.js - 探索 UE Smpolicy 添加页结构
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
async function main() {
const browser = await chromium.launch({ headless: false, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
// 选择工程
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.locator('input[name="project_search_name"]').fill('XW_SUPF_5_1_2_4');
await page.keyboard.press('Enter');
await page.waitForTimeout(3000);
await page.evaluate(() => {
document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === 'XW_SUPF_5_1_2_4') cells[1].querySelector('.iconfont').click();
});
});
await page.waitForTimeout(3000);
// 导航到 UE smpolicy 列表
await page.goto(`globalBaseUrl/sim_5gc/smpolicy/ue/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
console.log('UE smpolicy列表页:', page.url());
console.log('页面内容:', (await page.evaluate(() => document.body.innerText)).substring(0, 500));
// 点添加按钮
const addBtn = page.locator('button:has-text("添加")');
console.log('添加按钮:', await addBtn.count(), '个');
await addBtn.click();
await page.waitForTimeout(3000);
// 检查弹窗iframe
const frames = page.frames();
console.log('所有iframe:', frames.map(f => ({ url: f.url(), name: f.name() })));
const editFrame = frames.find(f => f.url().includes('/edit'));
if (editFrame) {
console.log('\n编辑帧 URL:', editFrame.url());
await editFrame.waitForLoadState('domcontentloaded');
await page.waitForTimeout(2000);
// 列出所有input/select
const fields = await editFrame.evaluate(() => {
const result = [];
document.querySelectorAll('input[name], select[name], textarea[name]').forEach(el => {
result.push({ tag: el.tagName, name: el.name, value: el.value, type: el.type || '' });
});
return result;
});
console.log('\n字段:', JSON.stringify(fields, null, 2));
// xm-select 状态
const xmState = await editFrame.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
return Array.from(inputs).map((inp, i) => ({
idx: i, name: inp.name, value: inp.value,
display: inp.parentElement?.textContent?.substring(0, 100)
}));
});
console.log('\nxm-select 状态:', JSON.stringify(xmState, null, 2));
// 列出所有可点击按钮
const btns = await editFrame.evaluate(() =>
Array.from(document.querySelectorAll('button')).map(b => ({ text: b.textContent.trim(), type: b.type }))
);
console.log('\n按钮:', JSON.stringify(btns));
// 截图
await page.screenshot({ path: 'smpolicy_ue_add.png', fullPage: true });
console.log('\n截图: smpolicy_ue_add.png');
} else {
console.log('未找到 /edit iframe');
const allIframes = page.frames().map(f => ({ name: f.name(), url: f.url() }));
console.log('所有frames:', JSON.stringify(allIframes));
await page.screenshot({ path: 'smpolicy_ue_no_iframe.png', fullPage: true });
}
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/explore_smpolicy_ue_edit.js
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
async function main() {
const browser = await chromium.launch({ headless: false, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.locator('input[name="project_search_name"]').fill('XW_SUPF_5_1_2_4');
await page.keyboard.press('Enter');
await page.waitForTimeout(3000);
await page.evaluate(() => {
document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === 'XW_SUPF_5_1_2_4') cells[1].querySelector('.iconfont').click();
});
});
await page.waitForTimeout(3000);
// 直接访问编辑页
await page.goto(`globalBaseUrl/sim_5gc/smpolicy/ue/edit/8756`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
console.log('编辑页URL:', page.url());
console.log('页面内容:', (await page.evaluate(() => document.body.innerText)).substring(0, 800));
const frames = page.frames();
console.log('\n所有iframe:', frames.map(f => ({ url: f.url(), name: f.name() })));
const fields = await page.evaluate(() => {
const result = [];
document.querySelectorAll('input[name], select[name], textarea[name]').forEach(el => {
result.push({ tag: el.tagName, name: el.name, value: el.value, type: el.type || '' });
});
return result;
});
console.log('\n主页面字段:', JSON.stringify(fields, null, 2));
const btns = await page.evaluate(() =>
Array.from(document.querySelectorAll('button')).map(b => b.textContent.trim())
);
console.log('\n主页面按钮:', btns);
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/explore_target.js
/**
* explore_target.js - 探索 XW_SUPF_SN4_5_2_8 的 PCF 配置
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, name) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
// 使用搜索
const searchInput = page.locator('input[type="search"], input[placeholder*="搜索"], input[name*="search"], .jsgrid-search input').first();
await searchInput.fill(name);
await page.keyboard.press('Enter');
await page.waitForTimeout(3000);
const clicked = await page.evaluate((n) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === n) {
cells[1].querySelector('.iconfont').click();
return true;
}
}
return false;
}, name);
if (!clicked) { console.log('❌ 选择工程失败'); process.exit(1); }
await page.waitForTimeout(3000);
console.log(`✅ 工程 "name" 已选`);
}
async function main() {
const browser = await chromium.launch({ headless: false, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, 'XW_SUPF_SN4_5_2_8');
// 1. 查看 PCF 列表
console.log('\n📋 PCF 列表:');
await page.goto(`globalBaseUrl/sim_5gc/pcf/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const pcfList = await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
const pcfs = [];
rows.forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 10) {
pcfs.push({ id: cells[1].textContent.trim(), name: cells[2].textContent.trim() });
}
});
return pcfs;
});
console.log(` 找到 pcfList.length 个 PCF:`, JSON.stringify(pcfList));
// 2. 查看 smpolicy/default
console.log('\n📋 smpolicy/default:');
await page.goto(`globalBaseUrl/sim_5gc/smpolicy/default/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const smpolicyList = await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
const list = [];
rows.forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 6) {
list.push({
id: cells[1].textContent.trim(),
name: cells[2].textContent.trim(),
pccRules: cells[4].textContent.trim(),
});
}
});
return list;
});
console.log(` 找到 smpolicyList.length 个默认规则:`, JSON.stringify(smpolicyList));
// 3. 查看 QoS 模板
console.log('\n📋 QoS 模板:');
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/qos/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const qosList = await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
const list = [];
rows.forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 8) {
list.push({
qosId: cells[2].textContent.trim(),
qi: cells[3].textContent.trim(),
maxbrUl: cells[4].textContent.trim(),
maxbrDl: cells[5].textContent.trim(),
});
}
});
return list;
});
console.log(` 找到 qosList.length 个QoS模板:`, JSON.stringify(qosList));
// 4. 查看 PCC 规则
console.log('\n📋 PCC 规则:');
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/pcc/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const pccList = await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
const list = [];
rows.forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 6) {
list.push({
pccRuleId: cells[2].textContent.trim(),
precedence: cells[4].textContent.trim(),
qosId: cells[5].textContent.trim(),
});
}
});
return list;
});
console.log(` 找到 pccList.length 个PCC规则:`, JSON.stringify(pccList));
// 5. 如果有PCF,查看其编辑弹窗的默认规则配置
if (pcfList.length > 0) {
console.log('\n📋 PCF编辑弹窗(查看 default_smpolicy 等):');
await page.goto(`globalBaseUrl/sim_5gc/pcf/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.evaluate((targetId) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 10 && cells[1].textContent.trim() === targetId) {
const links = cells[9].querySelectorAll('a');
for (const link of links) {
if (link.textContent.trim() === '编辑') { link.click(); return; }
}
}
}
}, pcfList[0].id);
await page.waitForTimeout(3000);
const frame = page.frame('layui-layer-iframe2');
if (frame) {
const text = await frame.evaluate(() => document.body.innerText);
console.log(text.substring(0, 1500));
}
}
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/find_project.js
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
async function main() {
const browser = await chromium.launch({ headless: true, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await page.goto(globalBaseUrl + '/login', { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
await page.goto(globalBaseUrl + '/sim_5gc/project/index', { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const allProjects = [];
let pageNum = 1;
let emptyCount = 0;
while (pageNum <= 200) {
const projects = await page.evaluate(() => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
const names = [];
rows.forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 3) names.push(cells[2].textContent.trim());
});
return names;
});
if (projects.length === 0) {
emptyCount++;
if (emptyCount >= 3) {
console.log('连续3页为空,停止');
break;
}
} else {
emptyCount = 0;
allProjects.push(...projects);
}
if (pageNum % 10 === 0) {
console.log(`Page pageNum: projects.length projects, total allProjects.length`);
}
// 检查Next按钮状态
const nextState = await page.evaluate(() => {
const links = document.querySelectorAll('.jsgrid-pager a');
for (const l of links) {
if (l.textContent.trim() === 'Next') {
return l.classList.contains('jsgrid-pager-disabled') || l.getAttribute('aria-disabled') === 'true';
}
}
return true;
});
if (nextState) break;
await page.evaluate(() => {
const links = document.querySelectorAll('.jsgrid-pager a');
for (const l of links) {
if (l.textContent.trim() === 'Next') { l.click(); break; }
}
});
await page.waitForTimeout(2000);
pageNum++;
}
console.log(`\n完成!总工程数: allProjects.length`);
// 搜索目标
const targets = allProjects.filter(n => n.includes('SN4') || n.includes('5_2_8') || n.includes('5_2'));
console.log('\n包含SN4/5_2_8/5_2的工程:', JSON.stringify(targets, null, 2));
// 打印所有SUPF工程
const supf = allProjects.filter(n => n.includes('SUPF'));
console.log('\n所有SUPF工程:', JSON.stringify(supf, null, 2));
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/find_target_project.js
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
async function main() {
const browser = await chromium.launch({ headless: true, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await page.goto(globalBaseUrl + '/login', { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
await page.goto(globalBaseUrl + '/sim_5gc/project/index', { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
// 尝试触发搜索
await page.locator('input[type="search"], input[name*="search"], input[placeholder*="搜索"]').first().fill('SUPF_SN4');
await page.keyboard.press('Enter');
await page.waitForTimeout(3000);
const searchResults = await page.evaluate(() => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
return Array.from(rows).map(row => {
const cells = row.querySelectorAll('td');
return cells.length >= 3 ? cells[2].textContent.trim() : null;
}).filter(Boolean);
});
console.log('搜索结果 SUPF_SN4:', JSON.stringify(searchResults));
// 清除搜索,看总共多少页
await page.locator('input[type="search"], input[name*="search"], input[placeholder*="搜索"]').first().fill('');
await page.keyboard.press('Enter');
await page.waitForTimeout(3000);
// 快速计算总页数
const pageInfo = await page.evaluate(() => {
const pager = document.querySelector('.jsgrid-pager');
if (!pager) return null;
const text = pager.textContent;
// 查找类似 "1 / 50" 的模式
const match = text.match(/(\d+)\s*\/\s*(\d+)/);
if (match) return { current: parseInt(match[1]), total: parseInt(match[2]) };
// 也可能分开显示
const spans = pager.querySelectorAll('span');
const nums = [];
spans.forEach(s => { if (!isNaN(parseInt(s.textContent))) nums.push(parseInt(s.textContent)); });
return nums.length >= 2 ? { current: nums[0], total: nums[1] } : null;
});
console.log('分页信息:', pageInfo);
// 直接跳到后面几页看看
if (pageInfo && pageInfo.total > 10) {
// 跳到第5页
await page.evaluate(() => {
const links = document.querySelectorAll('.jsgrid-pager a');
links.forEach(l => {
if (l.textContent.trim() === '5') l.click();
});
});
await page.waitForTimeout(2000);
const p5 = await page.evaluate(() => {
return Array.from(document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row')).map(row => {
const cells = row.querySelectorAll('td');
return cells.length >= 3 ? cells[2].textContent.trim() : null;
}).filter(Boolean);
});
console.log('第5页:', JSON.stringify(p5));
// 跳到第10页
await page.evaluate(() => {
const links = document.querySelectorAll('.jsgrid-pager a');
links.forEach(l => {
if (l.textContent.trim() === '10') l.click();
});
});
await page.waitForTimeout(2000);
const p10 = await page.evaluate(() => {
return Array.from(document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row')).map(row => {
const cells = row.querySelectorAll('td');
return cells.length >= 3 ? cells[2].textContent.trim() : null;
}).filter(Boolean);
});
console.log('第10页:', JSON.stringify(p10));
}
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/fix_pcc_qos.js
/**
* fix_pcc_qos.js - 在 PCC 编辑模式下修复 refQosData(qos1)
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
}
async function selectProject(page, name) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.locator('input[name="project_search_name"]').fill(name);
await page.keyboard.press('Enter');
await page.waitForTimeout(3000);
await page.evaluate((n) => {
document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === n) cells[1].querySelector('.iconfont').click();
});
}, name);
await page.waitForTimeout(3000);
}
async function main() {
const browser = await chromium.launch({ headless: false, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, 'XW_SUPF_SN4_5_2_8');
// 直接打开 PCC 编辑页 ID=17484
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/pcc/edit/17484`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
// 检查初始状态
const initial = await page.evaluate(() => {
return Array.from(document.querySelectorAll('input.xm-select-default')).map((inp, i) => ({ idx: i, value: inp.value }));
});
console.log('初始 xm-select:', JSON.stringify(initial));
// 方法1:用 Playwright locator 直接点击 xm-select 选项
// 先打开下拉
await page.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
if (inputs[0]) inputs[0].parentElement.click();
});
await page.waitForTimeout(1500);
// 用 Playwright locator 找到可见选项并点击
// 在页面上查找包含 "qos1" 的选项
const optionLocator = page.locator('.xm-option.show-icon', { hasText: 'qos1' }).first();
const optionVisible = await optionLocator.isVisible({ timeout: 3000 }).catch(() => false);
console.log('qos1 选项可见:', optionVisible);
if (optionVisible) {
await optionLocator.click();
console.log('用 locator 点击了 qos1');
await page.waitForTimeout(1000);
} else {
// 备选:用 JS 点击
const clicked = await page.evaluate(() => {
const opts = document.querySelectorAll('.xm-option.show-icon');
for (const opt of opts) {
if (opt.textContent.trim() === 'qos1') {
opt.click();
return true;
}
}
return false;
});
console.log('JS 点击 qos1:', clicked ? '成功' : '失败');
}
// 再次检查值
const afterSelect = await page.evaluate(() => {
return Array.from(document.querySelectorAll('input.xm-select-default')).map((inp, i) => ({ idx: i, value: inp.value }));
});
console.log('选择后 xm-select:', JSON.stringify(afterSelect));
// 提交
await page.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log('提交后URL:', page.url());
// 验证
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/pcc/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const result = await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 7 && cells[2].textContent.trim() === 'pcc_default') {
return { refQosData: cells[5].textContent.trim(), refTcData: cells[6].textContent.trim() };
}
}
return null;
});
console.log('\n📋 验证结果:');
console.log(' refQosData:', result ? result.refQosData : 'null');
console.log(' refTcData:', result ? result.refTcData : 'null');
if (result) {
console.log(result.refQosData === 'qos1' && result.refTcData === 'tc1' ? '\n🎉 全部正确!' : '\n❌ 仍有问题');
}
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/force_fix.js
/**
* force_fix.js - 强制删除并重建 qos3
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, name) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.evaluate((n) => {
document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === n) {
cells[1].querySelector('.iconfont').click();
}
});
}, name);
await page.waitForTimeout(3000);
console.log(`✅ 工程 "name" 已选`);
}
async function main() {
const browser = await chromium.launch({ headless: false, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, 'XW_SUPF_5_1_2_4');
// ── 1. 删除 qos3 ──────────────────────────────────────────
console.log('\n🗑️ Step 1: 删除旧 qos3...');
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/qos/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
// 强制显示所有行并删除
await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
for (const cell of cells) {
if (cell.textContent.trim() === 'qos3') {
const deleteBtn = Array.from(row.querySelectorAll('a')).find(a => a.textContent.trim() === '删除');
if (deleteBtn) {
deleteBtn.click();
return;
}
}
}
}
});
await page.waitForTimeout(1500);
// 确认删除
const confirmBtn = page.locator('.layui-layer-btn0');
if (await confirmBtn.count() > 0) {
await confirmBtn.click();
await page.waitForTimeout(2000);
console.log(' ✅ 已确认删除 qos3');
} else {
console.log(' ⚠️ 未找到确认按钮,qos3 可能不存在');
}
// ── 2. 重新创建 qos3 ─────────────────────────────────────
console.log('\n📦 Step 2: 创建新 qos3 (5qi=8)...');
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/qos/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
await page.locator('button:has-text("添加")').click();
await page.waitForTimeout(2000);
const frame = page.frame('layui-layer-iframe2');
await frame.waitForLoadState('domcontentloaded');
// 填写所有字段
await frame.locator('input[name="qosId"]').fill('qos3');
await frame.locator('input[name="5qi"]').fill('8');
await frame.locator('input[name="maxbrUl"]').fill('10000000');
await frame.locator('input[name="maxbrDl"]').fill('20000000');
await frame.locator('input[name="gbrUl"]').fill('5000000');
await frame.locator('input[name="gbrDl"]').fill('5000000');
console.log(' 已填写所有字段: qosId=qos3, 5qi=8, maxbrUl=10000000, maxbrDl=20000000, gbrUl=5000000, gbrDl=5000000');
await frame.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log(' ✅ 已提交');
// ── 3. 验证 qos3 的 5qi ───────────────────────────────────
console.log('\n🔍 Step 3: 验证 qos3 数据...');
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/qos/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const qos3Data = await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
for (let i = 0; i < cells.length; i++) {
if (cells[i].textContent.trim() === 'qos3') {
return Array.from(cells).map((c, idx) => idx + ':' + c.textContent.trim()).join(' | ');
}
}
}
return 'qos3 未找到';
});
console.log(' ' + qos3Data);
// 解析并验证
const qiMatch = qos3Data.match(/3:(\d+)/);
if (qiMatch && qiMatch[1] === '8') {
console.log('\n✅✅✅ qos3.5qi = 8(正确!)');
} else {
console.log(`\n❌ qos3.5qi = '未知'(期望 8)`);
}
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/full_flow.js
/**
* full_flow.js - 完整流程:创建QoS模板 → 创建PCC规则 → 添加到默认规则
*
* 逻辑:
* 1. 如果用户指定了5qi/maxbr-ul/maxbr-dl/gbr-ul/gbr-dl → 用用户指定的
* 2. 如果用户没指定 → 自动选择(5qi自动选一个不同的,参数用默认值)
* 3. 创建QoS模板 → 创建PCC规则 → 添加到sm_policy_default
*
* 用法:
* node full_flow.js --project XW_SUPF_5_1_2_4 --qos-id qos3 --maxbr-ul 10000000 --maxbr-dl 20000000 --gbr-ul 5000000 --gbr-dl 5000000 [--5qi 8] [--headed]
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
function parseArgs() {
const args = process.argv.slice(2);
const opts = {
project: 'XW_S5GC_1',
qosId: null,
// 用户指定的值(null表示未指定,使用默认值)
qi: null,
maxbrUl: null,
maxbrDl: null,
gbrUl: null,
gbrDl: null,
priority: '',
headed: false,
};
for (let i = 0; i < args.length; i++) {
if (args[i] === '--project' || args[i] === '-p') opts.project = args[++i];
else if (args[i] === '--qos-id') opts.qosId = args[++i];
else if (args[i] === '--5qi') opts.qi = args[++i];
else if (args[i] === '--maxbr-ul') opts.maxbrUl = args[++i];
else if (args[i] === '--maxbr-dl') opts.maxbrDl = args[++i];
else if (args[i] === '--gbr-ul') opts.gbrUl = args[++i];
else if (args[i] === '--gbr-dl') opts.gbrDl = args[++i];
else if (args[i] === '--priority') opts.priority = args[++i];
else if (args[i] === '--headed') opts.headed = true;
}
if (!opts.qosId) {
console.error('❌ 缺少必要参数: --qos-id');
console.error(' 示例: node full_flow.js --project XW_SUPF_5_1_2_4 --qos-id qos3 --maxbr-ul 10000000 --maxbr-dl 20000000 --gbr-ul 5000000 --gbr-dl 5000000');
process.exit(1);
}
// 如果用户指定了maxbr/gbr,必须全部指定
const hasBr = [opts.maxbrUl, opts.maxbrDl, opts.gbrUl, opts.gbrDl].some(v => v !== null);
const allBr = [opts.maxbrUl, opts.maxbrDl, opts.gbrUl, opts.gbrDl].every(v => v !== null);
if (hasBr && !allBr) {
console.error('❌ maxbr-ul, maxbr-dl, gbr-ul, gbr-dl 需要同时指定或都不指定');
process.exit(1);
}
return opts;
}
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, projectName) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
const clicked = await page.evaluate((name) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === name) {
const icon = cells[1].querySelector('.iconfont');
if (icon) { icon.click(); return true; }
}
}
return false;
}, projectName);
if (!clicked) { console.log('❌ 未找到工程'); process.exit(1); }
await page.waitForTimeout(3000);
console.log(`✅ 工程 "projectName" 已选`);
}
// 获取已使用的5qi列表
async function getUsed5qis(page) {
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/qos/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const usedQis = await page.evaluate(() => {
const qis = new Set();
document.querySelectorAll('.layui-table tbody tr').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 4) {
const qi = parseInt(cells[3].textContent.trim());
if (!isNaN(qi)) qis.add(qi);
}
});
return [...qis];
});
return usedQis;
}
function autoSelect5qi(usedQis) {
const candidates = [8, 9, 6, 5, 4, 3, 2, 1];
for (const c of candidates) {
if (!usedQis.includes(c)) return c;
}
return 8;
}
function autoFillParams() {
// 默认参数(如果用户没指定)
return {
maxbrUl: '10000000',
maxbrDl: '20000000',
gbrUl: '5000000',
gbrDl: '5000000',
};
}
// 删除已有的QoS模板(如果存在)
async function deleteQosIfExists(page, qosId) {
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/qos/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const exists = await page.evaluate((targetId) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
for (const cell of cells) {
if (cell.textContent.trim() === targetId) {
return true;
}
}
}
return false;
}, qosId);
if (!exists) {
console.log(` ℹ️ QoS模板 qosId 不存在,无需删除`);
return false;
}
// 点击删除
await page.evaluate((targetId) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
for (let i = 0; i < cells.length; i++) {
if (cells[i].textContent.trim() === targetId) {
const deleteBtn = row.querySelector('a:has-text("删除")');
if (deleteBtn) { deleteBtn.click(); return; }
}
}
}
}, qosId);
await page.waitForTimeout(2000);
// 确认删除(如果有确认框)
try {
const confirmBtn = page.locator('.layui-layer-btn0');
if (await confirmBtn.count() > 0) {
await confirmBtn.click();
await page.waitForTimeout(2000);
}
} catch(e) {}
console.log(` ✅ 已删除旧 QoS模板 qosId`);
return true;
}
// 删除已有的PCC规则(如果存在)
async function deletePccIfExists(page, pccId) {
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/pcc/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const exists = await page.evaluate((targetId) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === targetId) {
return true;
}
}
return false;
}, pccId);
if (!exists) {
console.log(` ℹ️ PCC规则 pccId 不存在,无需删除`);
return false;
}
await page.evaluate((targetId) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === targetId) {
const deleteBtn = row.querySelector('a:has-text("删除")');
if (deleteBtn) { deleteBtn.click(); return; }
}
}
}, pccId);
await page.waitForTimeout(2000);
try {
const confirmBtn = page.locator('.layui-layer-btn0');
if (await confirmBtn.count() > 0) {
await confirmBtn.click();
await page.waitForTimeout(2000);
}
} catch(e) {}
console.log(` ✅ 已删除旧 PCC规则 pccId`);
return true;
}
// 添加QoS模板
async function addQos(page, opts) {
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/qos/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
await page.locator('button:has-text("添加")').click();
await page.waitForTimeout(2000);
const iframe = page.locator('iframe[name="layui-layer-iframe2"]');
if (await iframe.count() > 0) {
const frame = page.frame('layui-layer-iframe2');
await fillQosForm(frame, opts);
} else {
await fillQosForm(page, opts);
}
}
async function fillQosForm(page, opts) {
await page.waitForTimeout(2000);
await page.locator('input[name="qosId"]').first().fill(opts.qosId);
console.log(` qosId = opts.qosId`);
await page.locator('input[name="5qi"]').first().fill(opts.qi);
console.log(` 5qi = opts.qi`);
await page.locator('input[name="maxbrUl"]').first().fill(opts.maxbrUl);
console.log(` maxbrUl = opts.maxbrUl`);
await page.locator('input[name="maxbrDl"]').first().fill(opts.maxbrDl);
console.log(` maxbrDl = opts.maxbrDl`);
await page.locator('input[name="gbrUl"]').first().fill(opts.gbrUl);
console.log(` gbrUl = opts.gbrUl`);
await page.locator('input[name="gbrDl"]').first().fill(opts.gbrDl);
console.log(` gbrDl = opts.gbrDl`);
await page.locator('button:has-text("提交")').first().click();
await page.waitForTimeout(3000);
}
// 添加PCC规则
async function xmSelectChoose(frame, index, optionText) {
await frame.evaluate((idx) => {
const inputs = document.querySelectorAll('input.xm-select-default');
if (inputs[idx]) inputs[idx].parentElement.click();
}, index);
await frame.waitForTimeout(1000);
await frame.evaluate((text) => {
const opts = document.querySelectorAll('.xm-option.show-icon');
for (const opt of opts) {
if (opt.textContent.trim() === text) {
opt.click();
return;
}
}
}, optionText);
await frame.waitForTimeout(500);
}
async function addPcc(page, opts) {
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/pcc/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
await page.locator('button:has-text("添加")').click();
await page.waitForTimeout(2000);
const frame = page.frameLocator('iframe[name="layui-layer-iframe2"]');
const f = frame.locator('iframe').contentFrame() ? frame.locator('iframe').contentFrame() : page.frame('layui-layer-iframe2');
// 直接用 evaluate 在 frame 里操作
const frameEl = await page.frame('layui-layer-iframe2');
await frameEl.waitForLoadState('domcontentloaded');
await frameEl.locator('input[name="pccRuleId"]').fill(opts.pccId);
await frameEl.locator('input[name="precedence"]').fill('10');
console.log(` pccRuleId = opts.pccId, precedence = 10`);
// 选择QoS(第0个xm-select)
await xmSelectChoose(frameEl, 0, opts.qosId);
// 按Escape关闭下拉
await page.keyboard.press('Escape');
await page.waitForTimeout(500);
await frameEl.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
}
// 添加PCC规则到 sm_policy_default
async function addPccToSmpolicy(page, pccId) {
await page.goto(`globalBaseUrl/sim_5gc/smpolicy/default/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
// 点击编辑
await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
if (rows.length > 0) {
rows[0].querySelector('a').click();
}
});
await page.waitForTimeout(3000);
const frameEl = await page.frame('layui-layer-iframe2');
await frameEl.waitForLoadState('domcontentloaded');
// 检查是否已经添加过
const alreadySelected = await frameEl.evaluate((targetId) => {
const inputs = document.querySelectorAll('input.xm-select-default');
if (inputs.length >= 2) {
const parent = inputs[1].parentElement;
return parent.textContent.includes(targetId);
}
return false;
}, pccId);
if (alreadySelected) {
console.log(` ℹ️ pccId 已在pccRules中,跳过`);
await page.keyboard.press('Escape');
return;
}
// 点击 pccRules 下拉(第1个xm-select)
await frameEl.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
if (inputs[1]) inputs[1].parentElement.click();
});
await frameEl.waitForTimeout(1000);
await frameEl.evaluate((text) => {
const opts = document.querySelectorAll('.xm-option.show-icon');
for (const opt of opts) {
if (opt.textContent.trim() === text) {
opt.click();
return;
}
}
}, pccId);
await page.keyboard.press('Escape');
await page.waitForTimeout(500);
await frameEl.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log(` ✅ 已添加 pccId 到 pccRules`);
}
async function main() {
const opts = parseArgs();
const pccId = `pcc_opts.qosId`;
// 参数处理
const defaults = autoFillParams();
const params = {
qosId: opts.qosId,
qi: opts.qi,
maxbrUl: opts.maxbrUl || defaults.maxbrUl,
maxbrDl: opts.maxbrDl || defaults.maxbrDl,
gbrUl: opts.gbrUl || defaults.gbrUl,
gbrDl: opts.gbrDl || defaults.gbrDl,
};
const browser = await chromium.launch({ headless: !opts.headed, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, opts.project);
// 自动确定5qi
if (params.qi === null) {
console.log('\n📋 检测已有QoS模板的5qi...');
const usedQis = await getUsed5qis(page);
console.log(` 已使用5qi: usedQis.join(', ')`);
params.qi = String(autoSelect5qi(usedQis));
console.log(` ✅ 自动选择 5qi = params.qi`);
} else {
console.log(`\n📋 用户指定 5qi = params.qi`);
}
// 显示最终参数
console.log('\n📋 最终参数:');
console.log(` qosId = params.qosId`);
console.log(` 5qi = params.qi`);
console.log(` maxbrUl = params.maxbrUl`);
console.log(` maxbrDl = params.maxbrDl`);
console.log(` gbrUl = params.gbrUl`);
console.log(` gbrDl = params.gbrDl`);
// 清理旧数据
console.log('\n🗑️ 清理旧数据...');
await deleteQosIfExists(page, opts.qosId);
await deletePccIfExists(page, pccId);
// 创建QoS
console.log('\n📦 创建QoS模板...');
await addQos(page, params);
// 创建PCC规则
console.log('\n📦 创建PCC规则...');
await addPcc(page, { ...params, pccId });
// 添加到 sm_policy_default
console.log('\n📦 添加到 sm_policy_default...');
await addPccToSmpolicy(page, pccId);
// 验证
console.log('\n📋 验证结果...');
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/qos/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const qosRow = await page.evaluate((targetId) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
for (let i = 0; i < cells.length; i++) {
if (cells[i].textContent.trim() === targetId) {
return Array.from(cells).map(c => c.textContent.trim());
}
}
}
return null;
}, opts.qosId);
if (qosRow) {
console.log(`\n✅ qos3 已创建: 5qi=qosRow[3], maxbrUl=qosRow[4], maxbrDl=qosRow[5], gbrUl=qosRow[6], gbrDl=qosRow[7]`);
}
await page.goto(`globalBaseUrl/sim_5gc/smpolicy/default/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const smpolicy = await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 6) {
return cells[4].textContent.trim();
}
}
return '';
});
console.log(`\n✅ sm_policy_default pccRules = smpolicy`);
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/full_regression_detached.sh
#!/usr/bin/env bash
set -euo pipefail
ROOT="/home/dotouch/.openclaw/workspace"
BASE="node $ROOT/skills/5gc/scripts/5gc.js"
OUTDIR="$ROOT/skills/5gc/test_results"
mkdir -p "$OUTDIR"
TS="$(date +%Y%m%d-%H%M%S)"
LOG="$OUTDIR/TS-full-regression.log"
run() {
echo "===== $1 =====" | tee -a "$LOG"
shift
"$@" 2>&1 | tee -a "$LOG"
echo | tee -a "$LOG"
}
# add / edit / bulk for main entities still in scope
run "AMF add" bash -lc "$BASE amf add --name AMF_REG_FULL_TS --project XW_S5GC_1"
run "AMF single edit" bash -lc "$BASE amf edit --name AMF_REG_FULL_TS --project XW_S5GC_1 --set-mcc 460"
run "AMF bulk edit" bash -lc "$BASE amf edit --project XW_S5GC_1 --set-mnc 01"
run "UDM add" bash -lc "$BASE udm add --name UDM_REG_FULL_TS --project XW_S5GC_1 --sip 10.200.5.9 --port 80"
run "UDM single edit" bash -lc "$BASE udm edit --name UDM_REG_FULL_TS --project XW_S5GC_1 --set-port 81"
run "UDM bulk edit" bash -lc "$BASE udm edit --project XW_S5GC_1 --set-port 82"
run "UPF add" bash -lc "$BASE upf add --name UPF_REG_FULL_TS --project XW_S5GC_1 --n4-ip 10.200.3.11 --n3-ip 10.200.3.12 --n6-ip 10.200.3.13"
run "UPF single edit" bash -lc "$BASE upf edit --name UPF_REG_FULL_TS --project XW_S5GC_1 --set-n4_ip 10.200.3.14"
run "UPF bulk edit" bash -lc "$BASE upf edit --project XW_S5GC_1 --set-n4_ip 10.200.3.15"
run "GNB add" bash -lc "$BASE gnb add --name GNB_REG_FULL_TS --project XW_S5GC_1 --count 100 --ngap-ip 10.200.4.11 --user-sip-ip-v4 10.200.4.12 --mcc 460 --mnc 01 --stac 1 --etac 100 --node-id 70"
run "GNB single edit" bash -lc "$BASE gnb edit --name GNB_REG_FULL_TS --project XW_S5GC_1 --set-replay_ip 10.200.4.13"
run "GNB bulk edit" bash -lc "$BASE gnb edit --project XW_S5GC_1 --set-replay_ip 10.200.4.14"
run "UE add" bash -lc "$BASE ue add --name UE_REG_FULL_TS --project XW_S5GC_1 --imsi 460001234567890 --msisdn 8613888888888 --mcc 460 --mnc 01"
run "UE single edit" bash -lc "$BASE ue edit --name UE_SMOKE_FINAL --project XW_S5GC_1 --set-msisdn 8613888886666"
run "UE bulk edit" bash -lc "$BASE ue edit --project XW_S5GC_1 --set-msisdn 8613888885555"
# additional entities/features
run "SMF add" bash -lc "$BASE smf add --name SMF_REG_FULL_TS --project XW_S5GC_1 --pfcp-ip 10.200.2.11 --n3-ip 10.200.2.12 --n6-ip 10.200.2.13"
run "SMF single edit" bash -lc "$BASE smf edit --name SMF_REG_FULL_TS --project XW_S5GC_1 --set-dnn internet_updated"
run "SMF bulk edit" bash -lc "$BASE smf edit --project XW_S5GC_1 --set-dnn internet"
run "PCF default-rule-add" bash -lc "$BASE pcf default-rule-add --project XW_SUPF_5_1_2_4 --pcf-name pcc"
run "NRF add" bash -lc "$BASE nrf add --name NRF_REG_FULL_TS --project XW_S5GC_1"
run "NRF single edit" bash -lc "$BASE nrf edit --name NRF_REG_FULL_TS --project XW_S5GC_1 --set-http2_port 81"
run "QOS add" bash -lc "$BASE qos add --project XW_SUPF_5_1_2_4 --qos-id qos_reg_TS --5qi 8 --maxbr-ul 10000000 --maxbr-dl 20000000"
run "TC add" bash -lc "$BASE tc add --project XW_SUPF_5_1_2_4 --tc-id tc_reg_TS --flow-status ENABLED"
run "PCC add" bash -lc "$BASE pcc add --project XW_SUPF_5_1_2_4 --pcc-id pcc_reg_TS --qos qos_reg_TS --tc tc_reg_TS"
echo "FULL REGRESSION DONE: $LOG" | tee -a "$LOG"
FILE:scripts/gnb-add-skill.js
#!/usr/bin/env node
/**
* GNB 添加脚本
* 用法: node gnb-add-skill.js <名称> [--headed] [--ngap_sip IP] [--project 工程]
* 示例: node gnb-add-skill.js GNB-TEST
* node gnb-add-skill.js GNB-TEST --ngap_sip 200.20.20.50 --headed
*/
const { chromium } = require('playwright');
const fs = require('fs');
const path = require('path');
let BASE_URL = 'https://192.168.3.89';
const CONFIG = {
urls: { login: '/login', gnbList: '/sim_5gc/gnb/index' },
credentials: { email: '[email protected]', password: 'dotouch' },
sessionDir: path.join(__dirname, '.sessions'),
getSessionFile() {
return `5gc_gnb_add_\/\//, '').replace(/\./g, '_').json`;
}
};
class SessionManager {
constructor() {
if (!fs.existsSync(CONFIG.sessionDir)) fs.mkdirSync(CONFIG.sessionDir, { recursive: true });
this.sp = path.join(CONFIG.sessionDir, CONFIG.getSessionFile());
}
async loadSession(browser) {
if (!fs.existsSync(this.sp)) return null;
try {
const { storageState } = JSON.parse(fs.readFileSync(this.sp, 'utf8'));
return await browser.newContext({ storageState, ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
} catch { return null; }
}
async saveSession(context) {
fs.writeFileSync(this.sp, JSON.stringify({ storageState: await context.storageState() }, null, 2));
}
}
// 选择工程
async function selectProject(page, projectName) {
await page.goto(`BASE_URL/sim_5gc/project/index`, { waitUntil: 'networkidle', timeout: 15000 });
try {
await page.waitForSelector('.jsgrid-row, .jsgrid-alt-row', { timeout: 10000 });
} catch {
console.log(' ⚠ 等待 jsgrid 行超时');
}
await page.waitForTimeout(2000);
for (let pageNum = 1; pageNum <= 20; pageNum++) {
// Try Playwright click on .project_select td first (most reliable)
const found = await page.evaluate((target) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === target) {
const td = row.querySelector('td.project_select');
if (td) {
td.click();
return true;
}
}
}
return false;
}, projectName);
if (found) {
await page.waitForTimeout(2000);
return true;
}
const nextBtn = page.locator('.jsgrid-pager a').filter({ hasText: 'Next' }).first();
if (!(await nextBtn.count())) { console.log(' ⚠ 已到最后一页'); break; }
try {
await page.evaluate(() => {
var links = document.querySelectorAll('.jsgrid-pager a');
for (var i = 0; i < links.length; i++) {
if (links[i].innerText.trim() === 'Next') { links[i].click(); break; }
}
});
await page.waitForTimeout(2000);
} catch { break; }
}
console.log(` ❌ 未找到工程 "projectName"`);
return false;
}
async function main() {
const args = process.argv.slice(2);
if (args.length < 1) {
console.log('用法: node gnb-add-skill.js <名称> [--project <工程>] [--ngap_sip <IP>] [--headed]');
console.log(' [--user_sip_ip_v4 <IP>] [--mcc <值>] [--mnc <值>]');
console.log(' [--stac <值>] [--etac <值>] [--node_id <ID>] [--cell_count <数量>]');
console.log('示例: node gnb-add-skill.js GNB-TEST --project XW_S5GC_1');
console.log(' node gnb-add-skill.js GNB-PROD --ngap_sip 200.20.20.100 --mcc 460 --mnc 60 --stac 1 --etac 10');
process.exit(1);
}
const gnbName = args[0];
let headless = true;
let project = 'XW_S5GC_1';
let count = '1';
let ngap_sip = '200.20.20.50';
let user_sip_ip_v4 = '2.2.2.2';
let mcc = '460';
let mnc = '60';
let stac = '0';
let etac = '0';
let node_id = '70';
let cell_count = '1';
for (let i = 1; i < args.length; i++) {
if (args[i] === '--headed') {
headless = false;
} else if ( args[i] === '--ngap_sip') {
ngap_sip = args[++i];
} else if (args[i] === '--count') {
count = args[++i];
} else if (args[i] === '--project') {
project = args[++i];
} else if (args[i] === '--user_sip_ip_v4') {
user_sip_ip_v4 = args[++i];
} else if (args[i] === '--mcc') {
mcc = args[++i];
} else if (args[i] === '--mnc') {
mnc = args[++i];
} else if (args[i] === '--stac') {
stac = args[++i];
} else if (args[i] === '--etac') {
etac = args[++i];
} else if (args[i] === '--node_id') {
node_id = args[++i];
} else if (args[i] === '--cell_count') {
cell_count = args[++i];
}
}
console.log(`▶ 添加 GNB: gnbName`);
console.log(` ngap_sip=ngap_sip user_sip_ip_v4=user_sip_ip_v4 mcc=mcc mnc=mnc`);
console.log(` TAC=stac~etac node_id=node_id count=count cell_count=cell_count`);
console.log(` 模式: '有头' 工程: project`);
const sessionMgr = new SessionManager();
const browser = await chromium.launch({ headless, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const context = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await context.newPage();
// 登录
await page.goto(`BASE_URLCONFIG.urls.login`, { waitUntil: 'networkidle' });
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill(CONFIG.credentials.email);
await page.getByRole('textbox', { name: '密码' }).fill(CONFIG.credentials.password);
await page.getByRole('checkbox', { name: '记住我' }).check();
await page.getByRole('button', { name: '登录' }).click();
await page.waitForLoadState('networkidle');
await sessionMgr.saveSession(context);
console.log(' ✓ 登录成功');
// 选工程
const ok = await selectProject(page, project);
if (!ok) throw new Error('工程选择失败');
console.log(' ✓ 工程已选');
// 进 GNB 列表,点添加
await page.goto(`BASE_URLCONFIG.urls.gnbList`, { waitUntil: 'networkidle' });
await page.waitForTimeout(2000);
await page.locator('button:has-text("添加"), a:has-text("添加")').first().click();
await page.waitForTimeout(2000);
console.log(' ✓ 进入添加页面');
// ===== 填写表单(用 page.fill 确保 layui 能收到事件) =====
// 1. 名称
await page.locator('input[name="name"]').fill(gnbName);
console.log(` ✓ name = gnbName`);
// 2. 类型下拉:点击打开,再用 Playwright 点击 dd 选项(触发真实事件)
await page.locator('.layui-form-select >> nth=0').click();
await page.waitForTimeout(500);
await page.locator('dd[lay-value="1"] >> nth=0').click();
await page.waitForTimeout(500);
console.log(' ✓ 类型 = 仿真设备');
// 3. 数量
await page.locator('input[name="count"]').fill(count);
console.log(` ✓ count = count`);
// 4. NGAP起始IP
await page.locator('input[name="ngap_sip"]').fill(ngap_sip);
console.log(` ✓ ngap_sip = ngap_sip`);
// 5. 用户面起始IP(SBI IP地址)
await page.locator('input[name="user_sip_ip_v4"]').fill(user_sip_ip_v4);
console.log(` ✓ user_sip_ip_v4 = user_sip_ip_v4`);
// 6. MCC/MNC
await page.locator('textarea[name="mcc"]').fill(mcc);
await page.locator('textarea[name="mnc"]').fill(mnc);
console.log(` ✓ mcc = mcc, mnc = mnc`);
// 7. TAC范围
await page.locator('input[name="stac"]').fill(stac);
await page.locator('input[name="etac"]').fill(etac);
console.log(` ✓ TAC = stac~etac`);
// 8. node_id
await page.locator('input[name="node_id"]').fill(node_id);
console.log(` ✓ node_id = node_id`);
console.log(' ✓ 表单填写完成');
// 拦截提交后的 response
let submitResponse = null;
page.on('response', res => {
if (res.url().includes('/sim_5gc/gnb/') && res.status() >= 200 && res.status() < 400) {
submitResponse = { status: res.status(), url: res.url() };
}
});
// 提交
await page.getByRole('button', { name: '确定' }).click();
await page.waitForTimeout(3000);
console.log(' 最终 URL:', page.url());
// 判断成功:重定向到 /gnb/index,或者收到 200/302 响应
const url = page.url();
let success = false;
if (url.includes('gnb/index')) {
// 进一步检查:看是否还在添加页(可能失败)
const onEditPage = await page.evaluate(() => document.querySelector('input[name="name"]') !== null);
success = !onEditPage;
}
if (success) {
console.log(' ✅ 添加成功');
} else {
console.log(' ⚠ 可能未保存,请检查页面');
// 检查错误信息
const detail = await page.evaluate(() => {
const err = document.querySelector('.layui-layer-msg, .layui-layer-content');
const text = err ? err.textContent.trim() : '';
const fields = {};
for (const sel of ['input[name="name"]','input[name="count"]','input[name="ngap_sip"]','input[name="user_sip_ip_v4"]','textarea[name="mcc"]','textarea[name="mnc"]','input[name="stac"]','input[name="etac"]','input[name="node_id"]']) {
const el = document.querySelector(sel);
if (el) fields[sel] = el.value;
}
return { text, fields, body: document.body.innerText.slice(0, 800) };
});
if (detail.text) console.log(' 错误信息:', detail.text);
console.log(' 字段快照:', JSON.stringify(detail.fields));
console.log(' 页面片段:', detail.body.replace(/\s+/g, ' ').slice(0, 300));
}
await browser.close();
}
main().catch(e => { console.error('异常:', e.message); process.exit(1); });
FILE:scripts/gnb-edit-skill.js
#!/usr/bin/env node
/**
* GNB 编辑脚本(单条 + 批量二合一)
* 用法: node gnb-edit-skill.js [--project 工程] [--headed] --set-<字段名> <值>
* 示例: node gnb-edit-skill.js --project XW_S5GC_basic --set-replay_ip 200.20.20.250
* node gnb-edit-skill.js --project XW_S5GC_basic --set-stac 0 --set-etac 0 --headed
*
* 支持的字段: name, count, ngap_sip, ngap_port, user_sip_ip_v4, user_sip_ip_v6,
* mcc, mnc, stac, etac, node_id, node_id_len, replay_ip, replay_port,
* cell_count, position_x, position_y, gateway_ind
*/
const { chromium } = require('playwright');
const fs = require('fs');
const path = require('path');
const BASE_URL = 'https://192.168.3.89';
const CONFIG = {
urls: { login: '/login', gnbList: '/sim_5gc/gnb/index' },
credentials: { email: '[email protected]', password: 'dotouch' },
sessionDir: path.join(__dirname, '.sessions'),
getSessionFile() {
return `5gc_gnb_bulk_\/\//, '').replace(/\./g, '_').json`;
}
};
class SessionManager {
constructor() {
if (!fs.existsSync(CONFIG.sessionDir)) fs.mkdirSync(CONFIG.sessionDir, { recursive: true });
this.sp = path.join(CONFIG.sessionDir, CONFIG.getSessionFile());
}
async loadSession(browser) {
if (!fs.existsSync(this.sp)) return null;
try {
const { storageState } = JSON.parse(fs.readFileSync(this.sp, 'utf8'));
return await browser.newContext({ storageState, ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
} catch { return null; }
}
async saveSession(context) {
fs.writeFileSync(this.sp, JSON.stringify({ storageState: await context.storageState() }, null, 2));
}
}
// 选择工程
async function selectProject(page, projectName) {
await page.goto(`BASE_URL/sim_5gc/project/index`, { waitUntil: 'networkidle', timeout: 15000 });
try {
await page.waitForSelector('.jsgrid-row, .jsgrid-alt-row', { timeout: 10000 });
} catch {
console.log(' ⚠ 等待 jsgrid 行超时');
}
await page.waitForTimeout(2000);
for (let pageNum = 1; pageNum <= 20; pageNum++) {
const result = await page.evaluate((target) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === target) {
if (row.classList.contains('jsgrid-selected-row')) return 'already-selected';
const controlCell = row.querySelector('.jsgrid-cell.jsgrid-control-field');
if (controlCell) {
const icon = controlCell.querySelector('.layui-icon');
if (icon) { icon.click(); return 'clicked'; }
}
row.click();
return 'clicked';
}
}
return 'not-found';
}, projectName);
if (result === 'already-selected') {
console.log(' ✓ 工程已在第一行(已选中状态),跳过');
await page.waitForTimeout(2000);
return true;
}
if (result === 'clicked') {
console.log(' ✓ 已点击工程行图标,等待切换...');
await page.waitForTimeout(3000);
return true;
}
const nextBtn = page.locator('.jsgrid-pager a').filter({ hasText: 'Next' }).first();
if (!(await nextBtn.count())) { console.log(' ⚠ 已到最后一页'); break; }
try {
await nextBtn.click({ force: true });
await page.waitForTimeout(2000);
} catch { break; }
}
console.log(` ❌ 未找到工程 "projectName"`);
return false;
}
// 获取 GNB 列表所有 ID(layui-table)
async function fetchAllGnbIds(page, nameFilter = null) {
// 等待行出现
await page.waitForTimeout(3000);
return await page.evaluate((filter) => {
// layui-table: tbody tr 中每行 td 顺序对应列
const tbody = document.querySelector('.layui-table-body');
if (!tbody) return [];
const rows = tbody.querySelectorAll('tr');
const ids = [];
const seen = new Set();
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3) {
// GNB 列表列顺序: ID(col1), 名称(col2), NGAP起始IP(col3), ...
const id = cells[1].textContent.trim(); // 第2列是 ID
const name = cells[2].textContent.trim(); // 第3列是名称
// filter 为空则全部通过,为空字符串则跳过表头
if (id && !seen.has(id) && (!filter || name === filter)) {
seen.add(id);
ids.push(id);
}
}
}
return ids;
}, nameFilter);
}
// 编辑单个 GNB
async function editSingleGnb(page, gnbId, edits) {
const editUrl = `BASE_URL/sim_5gc/gnb/edit/gnbId`;
await page.goto(editUrl, { waitUntil: 'networkidle' });
await page.waitForTimeout(2000);
for (const [field, val] of Object.entries(edits)) {
const inputSelector = `input[name="field"], textarea[name="field"]`;
try {
const exists = await page.locator(inputSelector).count();
if (exists === 0) {
console.log(` ⚠ 字段 field 不存在,跳过`);
continue;
}
await page.locator(inputSelector).fill(String(val));
console.log(` ✓ field → val`);
} catch (e) {
console.log(` ⚠ 字段 field 填写失败`);
}
}
// 提交
await page.getByRole('button', { name: '确定' }).click();
await page.waitForTimeout(3000);
const success = page.url().includes('gnb/index');
if (!success) console.log(' ⚠ 提交后未返回列表,可能失败');
return success;
}
async function main() {
const args = process.argv.slice(2);
if (args.length < 1) {
console.log('用法: node gnb-edit-skill.js [--project 工程] [--headed] --set-<字段名> <值>');
console.log('示例: node gnb-edit-skill.js --project XW_S5GC_basic --set-replay_ip 200.20.20.250');
console.log(' node gnb-edit-skill.js --project XW_S5GC_basic --set-stac 0 --set-etac 0 --headed');
console.log('');
console.log('支持的字段: name, count, ngap_sip, ngap_port, user_sip_ip_v4, user_sip_ip_v6,');
console.log(' mcc, mnc, stac, etac, node_id, node_id_len, replay_ip, replay_port,');
console.log(' cell_count, position_x, position_y, gateway_ind');
process.exit(1);
}
let projectName = 'XW_S5GC_1';
let headless = true;
let nameFilter = null; // --name 时只编辑该名称的 GNB
const edits = {};
for (let i = 0; i < args.length; i++) {
if (args[i] === '--project') {
projectName = args[++i];
} else if (args[i] === '--name') {
nameFilter = args[++i];
} else if (args[i] === '--headed') {
headless = false;
} else if (args[i].startsWith('--set-')) {
const field = args[i].slice(6);
edits[field] = args[++i];
} else if (!args[i].startsWith('-')) {
// 兼容不带 --name 的位置参数
if (!nameFilter) nameFilter = args[i];
}
}
if (Object.keys(edits).length === 0) {
console.log('请指定要修改的字段,例如: --set-replay_ip 200.20.20.250');
process.exit(1);
}
console.log(`▶ 批量编辑 GNB,工程: projectName' + nameFilter : ''`);
for (const [k, v] of Object.entries(edits)) console.log(` 修改: k = v`);
console.log(` 模式: '有头'`);
const sessionMgr = new SessionManager();
const browser = await chromium.launch({ headless, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const context = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await context.newPage();
// 登录
await page.goto(`BASE_URLCONFIG.urls.login`, { waitUntil: 'networkidle' });
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill(CONFIG.credentials.email);
await page.getByRole('textbox', { name: '密码' }).fill(CONFIG.credentials.password);
await page.getByRole('checkbox', { name: '记住我' }).check();
await page.getByRole('button', { name: '登录' }).click();
await page.waitForLoadState('networkidle');
await sessionMgr.saveSession(context);
console.log(' ✓ 登录成功');
// 选工程
const ok = await selectProject(page, projectName);
if (!ok) throw new Error('工程选择失败');
console.log(' ✓ 工程已选');
// 进 GNB 列表
await page.goto(`BASE_URLCONFIG.urls.gnbList`, { waitUntil: 'networkidle' });
await page.waitForTimeout(4000); // 等待 layui-table 渲染
// 获取所有 GNB ID
const gnbIds = await fetchAllGnbIds(page, nameFilter);
if (gnbIds.length === 0) {
console.log(' ⚠ 工程下没有 GNB');
await browser.close();
return;
}
console.log(` ✓ 找到 gnbIds.length 个 GNB`);
// 逐个编辑
let successCount = 0;
for (let i = 0; i < gnbIds.length; i++) {
const gnbId = gnbIds[i];
console.log(`\n[i + 1/gnbIds.length] 编辑 GNB ID: gnbId`);
const ok = await editSingleGnb(page, gnbId, edits);
if (ok) successCount++;
}
console.log(`\n=== 完成 ===`);
console.log(`成功编辑 successCount / gnbIds.length 个 GNB`);
await browser.close();
}
main().catch(e => { console.error('异常:', e.message); process.exit(1); });
FILE:scripts/nrf-add-skill.js
/**
* NRF 添加脚本
* 完整流程:登录 → 选工程 → 进NRF列表 → 点添加(弹窗iframe) → 填表单 → 提交
* 用法: node nrf-add-skill.js <名称> [--project <工程>] [--url <地址>] [--headed] \
* [--http2_sip <IP>] [--http2_port <端口>] [--MCC <值>] [--MNC <值>]
* 示例: node nrf-add-skill.js NRF-TEST --project XW_S5GC_1
*/
const { chromium } = require('playwright');
const path = require('path');
const fs = require('fs');
const BASE_URL = 'https://192.168.3.89';
const SESSION_DIR = path.join(__dirname, '.sessions');
function getSessionFile(baseUrl) {
const host = baseUrl.replace(/https?:\/\//, '').replace(/\./g, '_');
return `5gc_session_host.json`;
}
async function login(page, baseUrl) {
const sessionPath = path.join(SESSION_DIR, getSessionFile(baseUrl));
if (fs.existsSync(sessionPath)) {
try {
const storageState = JSON.parse(fs.readFileSync(sessionPath, 'utf8'));
if (storageState.cookies) {
await page.context().addCookies(storageState.cookies);
await page.goto(baseUrl + '/sim_5gc/project/index', { waitUntil: 'networkidle', timeout: 8000 }).catch(() => {});
if (!page.url().includes('/login')) {
console.log(' ✅ 使用缓存会话');
return true;
}
}
} catch {}
}
await page.goto(baseUrl + '/login', { waitUntil: 'networkidle', timeout: 15000 });
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForLoadState('networkidle');
const ctx = page.context();
const storageState = await ctx.storageState();
fs.writeFileSync(sessionPath, JSON.stringify({ cookies: storageState.cookies }, null, 2));
console.log(' ✅ 登录成功');
return true;
}
async function selectProject(page, projectName) {
await page.goto(BASE_URL + '/sim_5gc/project/index', { waitUntil: 'networkidle', timeout: 15000 });
await page.waitForSelector('.jsgrid-row, .jsgrid-alt-row', { timeout: 5000 }).catch(() => {});
await page.waitForTimeout(300);
for (let pageNum = 1; pageNum <= 200; pageNum++) {
const clicked = await page.evaluate((targetName) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === targetName) {
const icon = cells[1].querySelector('.iconfont');
if (icon) { icon.click(); return true; }
}
}
return false;
}, projectName);
if (clicked) { await page.waitForTimeout(2000); return true; }
const nextBtn = page.locator('.jsgrid-pager a:has-text("Next")');
if (!(await nextBtn.count())) break;
try { await nextBtn.click(); } catch { break; }
await page.waitForTimeout(1500);
}
console.log(` ❌ 未找到工程 "projectName"`);
return false;
}
async function main() {
const args = process.argv.slice(2);
if (args.length < 1) {
console.log('用法: node nrf-add-skill.js <名称> [--project <工程>] [--url <地址>] [--headed]');
console.log(' [--http2_sip <IP>] [--http2_port <端口>] [--MCC <值>] [--MNC <值>]');
console.log('示例: node nrf-add-skill.js NRF-TEST --project XW_S5GC_1');
process.exit(1);
}
const name = args[0];
let headless = true;
let project = 'XW_S5GC_1';
let http2_sip = '192.168.20.100';
let http2_port = '80';
let mcc = '460';
let mnc = '01';
for (let i = 1; i < args.length; i++) {
if (args[i] === '--headed') headless = false;
else if (args[i] === '--project') project = args[++i];
else if (args[i] === '--url') BASE_URL = args[++i];
else if (args[i] === '--http2_sip') http2_sip = args[++i];
else if (args[i] === '--http2_port') http2_port = args[++i];
else if (args[i] === '--MCC') mcc = args[++i];
else if (args[i] === '--MNC') mnc = args[++i];
}
console.log(`▶ 添加 NRF: name`);
console.log(` http2_sip=http2_sip http2_port=http2_port MCC=mcc MNC=mnc`);
console.log(` 工程: project`);
const browser = await chromium.launch({ headless, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page, BASE_URL);
const ok = await selectProject(page, project);
if (!ok) throw new Error('工程选择失败');
console.log(' ✓ 工程已选');
// 进 NRF 列表(先点"核心网"菜单,再点"NRF")
await page.evaluate(() => {
const links = document.querySelectorAll('a[href*="/nrf/"]');
for (const l of links) {
if (l.textContent.trim().includes('NRF')) { l.click(); return; }
}
});
await page.waitForTimeout(3000);
console.log(' ✓ 进入NRF列表,URL:', page.url());
// 点添加按钮
await page.waitForSelector('button:has-text("添加")', { timeout: 10000 }).catch(() => {});
await page.locator('button:has-text("添加")').first().click();
await page.waitForTimeout(2000);
console.log(' ✓ 点添加(弹窗)');
// 切换到弹窗 iframe
await page.locator('iframe[name="layui-layer-iframe2"]').waitFor({ timeout: 5000 });
const frame = page.frame('layui-layer-iframe2');
if (!frame) throw new Error('未找到弹窗 iframe');
await frame.waitForLoadState('domcontentloaded');
console.log(' ✓ 切换到弹窗iframe');
// 名称
await frame.locator('input[name="name"]').fill(name);
console.log(` ✓ name = name`);
// 类型下拉
await frame.getByRole('textbox', { name: '请选择' }).first().click();
await frame.getByRole('definition').filter({ hasText: '仿真设备' }).click();
await page.waitForTimeout(500);
console.log(' ✓ 类型 = 仿真设备');
// MCC
await frame.getByRole('textbox', { name: '三位数字', exact: true }).fill(mcc);
console.log(` ✓ MCC = mcc`);
// MNC
await frame.getByRole('textbox', { name: '二位或三位数字' }).fill(mnc);
console.log(` ✓ MNC = mnc`);
// HTTP2 SIP
await frame.locator('input[name="http2_sip"]').fill(http2_sip);
console.log(` ✓ http2_sip = http2_sip`);
// HTTP2 PORT
await frame.locator('input[name="http2_port"]').fill(http2_port);
console.log(` ✓ http2_port = http2_port`);
// 提交
await frame.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log(' ✓ 已提交');
const url = page.url();
if (url.includes('/nrf/index')) {
console.log(` ✅ 添加成功,URL: url`);
} else {
console.log(` ⚠️ 可能未保存,URL: url`);
}
await browser.close();
}
main().catch(e => { console.error('❌', e.message); process.exit(1); });
FILE:scripts/nrf-edit-skill.js
/**
* NRF 编辑脚本
* 完整流程:登录 → 选工程 → 进NRF列表 → 点编辑(弹窗iframe) → 填表单 → 提交 → 验证
* 用法:
* node nrf-edit-skill.js --project <工程> --set-<字段> <值> # 批量
* node nrf-edit-skill.js --name <名称> --project <工程> --set-<字段> <值> # 单条
* 示例:
* node nrf-edit-skill.js --project XW_S5GC_1 --set-http2_sip 10.0.0.1
* node nrf-edit-skill.js --name NRF-TEST --project XW_S5GC_1 --set-http2_sip 10.0.0.1
*/
const { chromium } = require('playwright');
const path = require('path');
const fs = require('fs');
const BASE_URL = 'https://192.168.3.89';
const SESSION_DIR = path.join(__dirname, '.sessions');
function getSessionFile() {
const host = BASE_URL.replace(/https?:\/\//, '').replace(/\./g, '_');
return `5gc_session_host.json`;
}
function parseArgs() {
const args = process.argv.slice(2);
const fields = {};
let name = null, project = 'XW_S5GC_1', headed = false;
for (let i = 0; i < args.length; i++) {
if (args[i] === '--name' && i + 1 < args.length) name = args[++i];
else if ((args[i] === '--project' || args[i] === '-p') && i + 1 < args.length) project = args[++i];
else if (args[i] === '--headed') headed = true;
else if (args[i].startsWith('--set-')) {
const k = args[i].slice(6).replace(/-/g, '_');
fields[k] = args[i + 1] != null && !args[i + 1].startsWith('--') ? args[++i] : 'true';
}
}
return { fields, name, project, headed };
}
async function login(page) {
const sessionPath = path.join(SESSION_DIR, getSessionFile());
if (fs.existsSync(sessionPath)) {
try {
const storageState = JSON.parse(fs.readFileSync(sessionPath, 'utf8'));
if (storageState.cookies) {
await page.context().addCookies(storageState.cookies);
await page.goto(BASE_URL + '/sim_5gc/project/index', { waitUntil: 'networkidle', timeout: 8000 }).catch(() => {});
if (!page.url().includes('/login')) {
console.log(' ✅ 使用缓存会话');
return true;
}
}
} catch {}
}
await page.goto(BASE_URL + '/login', { waitUntil: 'networkidle', timeout: 15000 });
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForLoadState('networkidle');
const ctx = page.context();
const storageState = await ctx.storageState();
fs.writeFileSync(sessionPath, JSON.stringify({ cookies: storageState.cookies }, null, 2));
console.log(' ✅ 登录成功');
return true;
}
async function selectProject(page, projectName) {
await page.goto(BASE_URL + '/sim_5gc/project/index', { waitUntil: 'networkidle', timeout: 15000 });
await page.waitForSelector('.jsgrid-row, .jsgrid-alt-row', { timeout: 5000 }).catch(() => {});
await page.waitForTimeout(300);
for (let pageNum = 1; pageNum <= 200; pageNum++) {
const clicked = await page.evaluate((targetName) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === targetName) {
const icon = cells[1].querySelector('.iconfont');
if (icon) { icon.click(); return true; }
}
}
return false;
}, projectName);
if (clicked) { await page.waitForTimeout(2000); return true; }
const nextBtn = page.locator('.jsgrid-pager a:has-text("Next")');
if (!(await nextBtn.count())) break;
try { await nextBtn.click(); } catch { break; }
await page.waitForTimeout(1500);
}
console.log(` ❌ 未找到工程 "projectName"`);
return false;
}
async function main() {
const { fields, name: targetName, project, headed } = parseArgs();
if (Object.keys(fields).length === 0) {
console.error('请至少指定一个 --set-<字段>'); process.exit(1);
}
console.log(`▶ NRF 编辑 工程: project 字段: JSON.stringify(fields)`);
const browser = await chromium.launch({ headless: !headed, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
const ok = await selectProject(page, project);
if (!ok) throw new Error('工程选择失败');
console.log(` ✅ 工程已选`);
// 进入 NRF 列表
await page.evaluate(() => {
const links = document.querySelectorAll('a[href*="/nrf/"]');
for (const l of links) {
if (l.textContent.trim().includes('NRF')) { l.click(); return; }
}
});
await page.waitForTimeout(2500);
console.log(' ✓ 进入NRF列表,URL:', page.url());
// 收集 NRF 行
const rows = await page.locator('.layui-table tbody tr').all();
const targets = [];
for (const row of rows) {
const cells = await row.locator('td').all();
if (cells.length >= 10) {
const id = parseInt((await cells[1].textContent()).trim(), 10);
const name = (await cells[2].textContent()).trim();
if (!isNaN(id) && name && (!targetName || name === targetName)) {
targets.push({ id, name });
}
}
}
if (targets.length === 0) { console.log(' 未找到匹配的 NRF'); await browser.close(); return; }
console.log(` 找到 targets.length 个 NRF`);
for (const nrf of targets) {
process.stdout.write(`▶ [nrf.id] nrf.name ... `);
try {
// 重新进入 NRF 列表
await page.evaluate(() => {
const links = document.querySelectorAll('a[href*="/nrf/"]');
for (const l of links) {
if (l.textContent.trim().includes('NRF')) { l.click(); return; }
}
});
await page.waitForTimeout(2000);
// 找到该行的编辑按钮
const allRows = await page.locator('.layui-table tbody tr').all();
let opened = false;
for (const row of allRows) {
const cells = await row.locator('td').all();
if (cells.length >= 10 && parseInt((await cells[1].textContent()).trim(), 10) === nrf.id) {
await cells[9].locator('a:has-text("编辑")').click();
opened = true;
break;
}
}
if (!opened) { console.log('❌ 打开编辑弹窗失败'); continue; }
// 等待弹窗
await page.waitForSelector('iframe[name="layui-layer-iframe2"]', { timeout: 5000 });
await page.waitForTimeout(2000);
const frame = page.frame('layui-layer-iframe2');
if (!frame) { console.log('❌ 未找到 iframe'); continue; }
await frame.waitForLoadState('domcontentloaded');
await page.waitForTimeout(500);
// 填写字段(使用 locator.fill)
if (fields.http2_sip) await frame.locator('input[name="http2_sip"]').fill(fields.http2_sip);
if (fields.http2_port) await frame.locator('input[name="http2_port"]').fill(fields.http2_port);
if (fields.MCC) await frame.getByRole('textbox', { name: '三位数字', exact: true }).fill(fields.MCC);
if (fields.MNC) await frame.getByRole('textbox', { name: '二位或三位数字' }).fill(fields.MNC);
// 点提交
await frame.locator('button:has-text("提交")').click();
await page.waitForTimeout(2000);
// 强制关闭 layer
await page.evaluate(() => {
const b = document.querySelector('.layui-layer-close');
if (b) b.click();
});
await page.waitForTimeout(1000);
// 重新进入列表,验证
await page.evaluate(() => {
const links = document.querySelectorAll('a[href*="/nrf/"]');
for (const l of links) {
if (l.textContent.trim().includes('NRF')) { l.click(); return; }
}
});
await page.waitForTimeout(2000);
const rows2 = await page.locator('.layui-table tbody tr').all();
let found = false;
for (const row of rows2) {
const cells = await row.locator('td').all();
if (cells.length >= 10 && parseInt((await cells[1].textContent()).trim(), 10) === nrf.id) {
await cells[9].locator('a:has-text("编辑")').click();
await page.waitForTimeout(2000);
await page.waitForSelector('iframe[name="layui-layer-iframe2"]', { timeout: 5000 });
await page.waitForTimeout(500);
const frame2 = page.frame('layui-layer-iframe2');
let verified = true;
let actualVal = 'N/A';
if (fields.http2_sip) {
actualVal = await frame2.locator('input[name="http2_sip"]').inputValue();
verified = verified && actualVal === fields.http2_sip;
}
if (fields.http2_port) verified = verified && (await frame2.locator('input[name="http2_port"]').inputValue()) === fields.http2_port;
if (fields.MCC) verified = verified && (await frame2.getByRole('textbox', { name: '三位数字', exact: true }).inputValue()) === fields.MCC;
if (fields.MNC) verified = verified && (await frame2.getByRole('textbox', { name: '二位或三位数字' }).inputValue()) === fields.MNC;
console.log(verified ? '✅' : '❌');
found = true;
await page.evaluate(() => { const b = document.querySelector('.layui-layer-close'); if (b) b.click(); });
break;
}
}
if (!found) console.log('❌');
} catch (e) {
console.log(`❌ e.message`);
}
}
await browser.close();
}
main().catch(e => { console.error('❌', e.message); process.exit(1); });
FILE:scripts/package-lock.json
{
"name": "scripts",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "scripts",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"playwright": "^1.59.1",
"yargs": "^18.0.0"
}
},
"node_modules/ansi-regex": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/ansi-styles": {
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/cliui": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz",
"integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==",
"license": "ISC",
"dependencies": {
"string-width": "^7.2.0",
"strip-ansi": "^7.1.0",
"wrap-ansi": "^9.0.0"
},
"engines": {
"node": ">=20"
}
},
"node_modules/emoji-regex": {
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
"license": "MIT"
},
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"license": "ISC",
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/get-east-asian-width": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz",
"integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/playwright": {
"version": "1.59.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz",
"integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==",
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.59.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.59.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz",
"integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==",
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/string-width": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^10.3.0",
"get-east-asian-width": "^1.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/strip-ansi": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
"integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.2.2"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
"integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.2.1",
"string-width": "^7.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"license": "ISC",
"engines": {
"node": ">=10"
}
},
"node_modules/yargs": {
"version": "18.0.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz",
"integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==",
"license": "MIT",
"dependencies": {
"cliui": "^9.0.1",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"string-width": "^7.2.0",
"y18n": "^5.0.5",
"yargs-parser": "^22.0.0"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=23"
}
},
"node_modules/yargs-parser": {
"version": "22.0.0",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz",
"integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==",
"license": "ISC",
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=23"
}
}
}
}
FILE:scripts/package.json
{
"name": "scripts",
"version": "1.0.0",
"description": "",
"main": "5gc.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"playwright": "^1.59.1",
"yargs": "^18.0.0"
}
}
FILE:scripts/parse_args.js
/**
* 统一 CLI 参数解析器
*
* 所有 5GC 脚本均使用此解析器,保证参数格式一致。
*
* 标准格式:
* ADD: node <script> --name <名称> --project <工程> [--field <值> ...]
* EDIT: node <script> --name <名称> --project <工程> --set-<field> <value>
* node <script> --id <ID> --set-<field> <value>
* node <script> --project <工程> --set-<field> <value> (批量)
*
* 用法: const args = parseArgs(); → { name, project, id, sets, headed, url }
*/
const URL_RE = /^--url$/i;
const PROJECT_RE = /^--project$|^--project$/i;
const NAME_RE = /^--name$/i;
const ID_RE = /^--id$/i;
const HEADED_RE = /^--headed$/i;
const SET_RE = /^--set-/;
function parseArgs(argv) {
const args = argv || process.argv.slice(2);
const result = {
name: null,
project: 'XW_S5GC_1', // 默认工程
id: null,
sets: {},
headed: false,
url: 'https://192.168.3.89',
_positional: [] // 保留位置参数兼容
};
for (let i = 0; i < args.length; i++) {
const a = args[i];
// 统一转小写匹配(处理 --Name, --NAME 等)
const al = a.toLowerCase();
if (URL_RE.test(a)) {
let u = args[++i];
if (u && !u.startsWith('http')) u = 'https://' + u;
result.url = u || result.url;
} else if (PROJECT_RE.test(a) || al === '-p') {
result.project = args[++i] || result.project;
} else if (NAME_RE.test(a) || al === '-n') {
result.name = args[++i] || result.name;
} else if (ID_RE.test(a)) {
result.id = args[++i] || result.id;
} else if (HEADED_RE.test(a) || al === '--head' || al === '-h' && args[i + 1] === undefined) {
result.headed = true;
} else if (SET_RE.test(a)) {
const key = a.substring(5).replace(/-/g, '_'); // --set-msisdn → msisdn
result.sets[key] = args[++i];
} else if (a.startsWith('--')) {
// 其他 --field value 形式(非 --set-)
const key = a.substring(2).replace(/-/g, '_');
result.sets[key] = args[++i];
} else if (!a.startsWith('-')) {
// 纯位置参数(非选项):视为 name
if (!result.name) result.name = a;
else result._positional.push(a);
}
// 忽略单独出现的 -h(它是 --headed 的缩写,但后面没值时才有意义)
}
return result;
}
/**
* 统一打印帮助信息
*/
function printHelp(additionalFields = []) {
console.log(`
用法: node <script> --name <名称> --project <工程> [--set-<字段> <值>] [--headed]
参数:
--name <名称> 网元名称(ADD/EDIT 时使用)
--project <工程> 目标工程(默认: XW_S5GC_1)
--id <ID> 直接指定网元 ID(EDIT 时使用)
--set-<字段> <值> 修改指定字段的值(EDIT 时使用)
--url <地址> 5GC 仪表地址(默认: https://192.168.3.89)
--headed 以有头模式运行(显示浏览器窗口)
示例:
# 添加
node <script> --name MyAMF --project XW_S5GC_1 --mcc 460 --mnc 01
node <script> --name MyGNB --project XW_S5GC_1 --count 1 --mcc 460 --mnc 01 --stac 1 --etac 100
# 编辑(单个)
node <script> --id 12345 --set-sbi_ip 10.0.0.1
node <script> --name MyAMF --project XW_S5GC_1 --set-sbi_ip 10.0.0.1
# 批量编辑
node <script> --project XW_S5GC_1 --set-replay_ip 10.0.0.1
可编辑字段:additionalFields.map(f => '\n ' + f).join('')
`);
}
// 暴露
module.exports = { parseArgs, printHelp };
FILE:scripts/pcc-add-skill.js
/**
* pcc-add-skill.js - PCC规则添加工具(修复版)
*
* 用法:
* node pcc-add-skill.js --project XW_SUPF_5_1_2_4 --pcc-id pcc_new --qos qos2 --tc tc1 [--precedence 63] [--headed]
*
* 参数:
* --project 工程名(默认 XW_S5GC_1)
* --pcc-id 新PCC规则ID(必填,字母/数字/下划线)
* --precedence 优先级(默认 63,用户指定时用指定值)
* --qos QoS模板名称(必填,如 qos1 / qos2)
* --tc 流量控制名称(必填,如 tc1)
* --flow-desc 流描述(可选)
* --headed 显示浏览器窗口
*
* 完整链路:
* 点击"添加" → 主框架跳转 /predfPolicy/pcc/edit
* → 填写 pccRuleId + precedence
* → xm-select 选 qos(第0个)+ tc(第1个)+ 可选chg(第2个)
* → 提交 → 返回列表页
*
* xm-select 交互(Playwright locator):
* 1. JS: inputs[idx].parentElement.click() 打开下拉
* 2. Playwright locator: page.locator('.xm-option.show-icon', {hasText}).click() 选择选项
* 3. page.keyboard.press('Escape') 关闭下拉
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
function parseArgs() {
const args = process.argv.slice(2);
const opts = {
project: 'XW_S5GC_1',
pccId: null,
precedence: null, // null = 使用默认值63
qos: null, // 必填
tc: null, // 必填
flowDesc: null,
headed: false,
};
for (let i = 0; i < args.length; i++) {
if (args[i] === '--project' || args[i] === '-p') opts.project = args[++i];
else if (args[i] === '--pcc-id') opts.pccId = args[++i];
else if (args[i] === '--precedence') opts.precedence = args[++i];
else if (args[i] === '--qos') opts.qos = args[++i];
else if (args[i] === '--tc') opts.tc = args[++i];
else if (args[i] === '--flow-desc') opts.flowDesc = args[++i];
else if (args[i] === '--headed') opts.headed = true;
}
if (!opts.pccId) {
console.error('❌ 缺少 --pcc-id 参数');
process.exit(1);
}
if (!opts.qos) {
console.error('❌ 缺少 --qos 参数(QoS模板名称)');
process.exit(1);
}
if (!opts.tc) {
console.error('❌ 缺少 --tc 参数(流量控制名称)');
process.exit(1);
}
return opts;
}
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, projectName) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
// 先尝试搜索工程名
await page.locator('input[name="project_search_name"]').fill(projectName);
await page.keyboard.press('Enter');
await page.waitForTimeout(3000);
const clicked = await page.evaluate((name) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === name) {
const icon = cells[1].querySelector('.iconfont');
if (icon) { icon.click(); return true; }
}
}
return false;
}, projectName);
if (!clicked) {
// 尝试逐页查找
for (let p = 1; p <= 100; p++) {
const found = await page.evaluate((name) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === name) {
cells[1].querySelector('.iconfont').click();
return true;
}
}
return false;
}, projectName);
if (found) break;
const hasNext = await page.evaluate(() => {
const links = document.querySelectorAll('.jsgrid-pager a');
for (const l of links) {
if (l.textContent.trim() === 'Next' && !l.classList.contains('jsgrid-pager-disabled')) return true;
}
return false;
});
if (!hasNext) break;
await page.evaluate(() => {
const links = document.querySelectorAll('.jsgrid-pager a');
for (const l of links) {
if (l.textContent.trim() === 'Next') { l.click(); return; }
}
});
await page.waitForTimeout(2000);
}
}
await page.waitForTimeout(3000);
console.log(`✅ 工程 "projectName" 已选`);
}
async function main() {
const opts = parseArgs();
const browser = await chromium.launch({ headless: !opts.headed, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, opts.project);
// 去 PCC 列表页
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/pcc/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
console.log(`✅ 到达PCC列表页`);
// 点击添加按钮
await page.locator('button:has-text("添加")').click();
await page.waitForTimeout(3000);
// 等待输入框出现(URL跳转完成)
await page.waitForFunction(() => window.location.href.includes('/predfPolicy/pcc/edit'), { timeout: 10000 });
await page.waitForTimeout(3000);
console.log(`✅ 到达添加页: page.url()`);
// 填写文本字段
const precedence = opts.precedence !== null ? String(opts.precedence) : '63';
await page.locator('input[name="pccRuleId"]').fill(opts.pccId);
await page.locator('input[name="precedence"]').fill(precedence);
console.log(` pccRuleId="opts.pccId", precedence="precedence"'(默认63)'`);
// ── xm-select[0] = refQosData ──────────────────────────────────
await page.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
if (inputs[0]) inputs[0].parentElement.click();
});
await page.waitForTimeout(1000);
const qosVisible = await page.locator('.xm-option.show-icon', { hasText: opts.qos }).isVisible({ timeout: 3000 }).catch(() => false);
if (qosVisible) {
await page.locator('.xm-option.show-icon', { hasText: opts.qos }).click();
console.log(` ✅ refQosData=opts.qos 已选`);
} else {
console.log(` ❌ refQosData=opts.qos 不可见`);
}
await page.waitForTimeout(500);
// ── xm-select[1] = refTcData ─────────────────────────────────
await page.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
if (inputs[1]) inputs[1].parentElement.click();
});
await page.waitForTimeout(1000);
const tcVisible = await page.locator('.xm-option.show-icon', { hasText: opts.tc }).isVisible({ timeout: 3000 }).catch(() => false);
if (tcVisible) {
await page.locator('.xm-option.show-icon', { hasText: opts.tc }).click();
console.log(` ✅ refTcData=opts.tc 已选`);
} else {
console.log(` ❌ refTcData=opts.tc 不可见`);
}
await page.waitForTimeout(500);
// ── xm-select[2] = refChgData(可选,如有则自动选第一个)────────
await page.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
if (inputs[2]) inputs[2].parentElement.click();
});
await page.waitForTimeout(1000);
const firstChg = page.locator('.xm-option.show-icon').first();
if (await firstChg.isVisible({ timeout: 2000 }).catch(() => false)) {
const txt = await firstChg.textContent();
await firstChg.click();
console.log(` ℹ️ refChgData=(txt.trim()) 已选`);
}
await page.waitForTimeout(500);
// 关闭 xm-select 下拉(按 Escape 避免遮罩拦截提交按钮)
await page.keyboard.press('Escape');
await page.waitForTimeout(300);
// 提交
await page.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log(`✅ PCC规则 opts.pccId 已提交`);
// 验证
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/pcc/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const pccData = await page.evaluate((targetId) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 8 && cells[2].textContent.trim() === targetId) {
return {
pccRuleId: cells[2].textContent.trim(),
precedence: cells[4].textContent.trim(),
refQosData: cells[5].textContent.trim(),
refTcData: cells[6].textContent.trim(),
};
}
}
return null;
}, opts.pccId);
if (pccData) {
console.log('\n📋 验证结果:');
console.log(` pccRuleId = pccData.pccRuleId`);
console.log(` precedence = pccData.precedence`);
console.log(` refQosData = pccData.refQosData '❌'`);
console.log(` refTcData = pccData.refTcData '❌'`);
}
console.log('\n✅ 完成');
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/pcc-edit-skill.js
/**
* pcc-edit-skill.js - PCC 规则编辑(修改 QoS/TC 绑定)
*
* 用法:
* node 5gc.js pcc edit --project XW_SUPF_5_1_2_4 --name pcc_default --set-qos qos_high_rate
* node 5gc.js pcc edit --project XW_SUPF_5_1_2_4 --name pcc_default --set-qos qos_new --set-tc tc_new
*
* 参数:
* --project 工程名(默认 XW_S5GC_1)
* --name PCC规则名称(精确匹配,要修改的 PCC)
* --set-qos 新的 QoS 模板名称
* --set-tc 新的 Traffic Control 名称
* --headed 显示浏览器窗口
*
* 链路:
* PCC列表页 → 找到 PCC 数字ID → 直接导航到 /predfPolicy/pcc/edit/{id}
* → 修改 xm-select → 提交
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
function parseArgs() {
const args = process.argv.slice(2);
const opts = {
project: 'XW_S5GC_1',
name: null,
newQos: null,
newTc: null,
headed: false,
};
for (let i = 0; i < args.length; i++) {
if (args[i] === '--project' || args[i] === '-p') opts.project = args[++i];
else if (args[i] === '--name') opts.name = args[++i];
else if (args[i] === '--set-qos') opts.newQos = args[++i];
else if (args[i] === '--set-tc') opts.newTc = args[++i];
else if (args[i] === '--headed') opts.headed = true;
}
if (!opts.name) {
console.error('❌ 缺少 --name 参数');
process.exit(1);
}
return opts;
}
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, name) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.locator('input[name="project_search_name"]').fill(name);
await page.keyboard.press('Enter');
await page.waitForTimeout(3000);
const found = await page.evaluate((n) => {
let result = false;
document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === n) {
cells[1].querySelector('.iconfont')?.click();
result = true;
}
});
return result;
}, name);
if (!found) { console.error(`❌ 未找到工程: name`); process.exit(1); }
await page.waitForTimeout(3000);
console.log(`✅ 工程 "name" 已选`);
}
async function main() {
const opts = parseArgs();
const browser = await chromium.launch({ headless: !opts.headed, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, opts.project);
// ① 去 PCC 列表页,找到目标 PCC 的数字 ID
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/pcc/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const pccInfo = await page.evaluate((targetName) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 10 && cells[2].textContent.trim() === targetName) {
return { id: cells[1].textContent.trim(), pccRuleId: cells[2].textContent.trim() };
}
}
return null;
}, opts.name);
if (!pccInfo) {
console.error(`❌ 未找到 PCC: opts.name`);
process.exit(1);
}
console.log(`✅ 找到 PCC "opts.name",数字ID=pccInfo.id`);
// ② 直接导航到编辑页
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/pcc/edit/pccInfo.id`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
console.log(` 编辑页URL: page.url()`);
// ③ 修改 refQosData(第0个xm-select)
if (opts.newQos) {
// 打开下拉
await page.evaluate(() => document.querySelectorAll('input.xm-select-default')[0].parentElement.click());
await page.waitForTimeout(800);
// 直接点击目标选项(xm-select 切换时自动取消旧的)
const clicked = await page.evaluate((targetText) => {
const allOpts = document.querySelectorAll('.xm-option');
for (const opt of allOpts) {
if (opt.textContent.trim() === targetText) {
opt.click();
return true;
}
}
return false;
}, opts.newQos);
if (clicked) {
console.log(` ✅ refQosData → opts.newQos`);
} else {
console.log(` ❌ 选项 opts.newQos 未找到`);
}
await page.waitForTimeout(500);
}
// ④ 修改 refTcData(第1个xm-select)
if (opts.newTc) {
await page.evaluate(() => document.querySelectorAll('input.xm-select-default')[1].parentElement.click());
await page.waitForTimeout(800);
const clicked = await page.evaluate((targetText) => {
const allOpts = document.querySelectorAll('.xm-option');
for (const opt of allOpts) {
if (opt.textContent.trim() === targetText) {
opt.click();
return true;
}
}
return false;
}, opts.newTc);
if (clicked) {
console.log(` ✅ refTcData → opts.newTc`);
} else {
console.log(` ❌ 选项 opts.newTc 未找到`);
}
await page.waitForTimeout(500);
}
// 关闭下拉
await page.keyboard.press('Escape');
await page.waitForTimeout(300);
// ⑤ 提交
await page.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log('✅ 已提交');
// ⑥ 验证
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/pcc/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const updated = await page.evaluate((targetName) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 8 && cells[2].textContent.trim() === targetName) {
return {
pccRuleId: cells[2].textContent.trim(),
precedence: cells[4].textContent.trim(),
refQosData: cells[5].textContent.trim(),
refTcData: cells[6].textContent.trim(),
};
}
}
return null;
}, opts.name);
if (updated) {
console.log('\n📋 验证结果:');
console.log(` pccRuleId = updated.pccRuleId`);
console.log(` precedence = updated.precedence`);
console.log(` refQosData = updated.refQosData '❌') : ''`);
console.log(` refTcData = updated.refTcData '❌') : ''`);
}
await browser.close();
console.log('\n✅ 完成');
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/pcc_qos_explore.js
/**
* pcc_qos_explore.js - 探索 PCC 规则的编辑页面,找到 QoS 字段
*
* 用法:node pcc_qos_explore.js --project XW_SUPF_5_1_2_4 --pcc-id pcc_test_maxbr001
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
function parseArgs() {
const args = process.argv.slice(2);
const opts = { project: 'XW_S5GC_1', pccId: null, headed: true };
for (let i = 0; i < args.length; i++) {
if (args[i] === '--project' || args[i] === '-p') opts.project = args[++i];
else if (args[i] === '--pcc-id') opts.pccId = args[++i];
else if (args[i] === '--headed') opts.headed = true;
}
if (!opts.pccId) { console.error('❌ 缺少 --pcc-id'); process.exit(1); }
return opts;
}
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, projectName) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
const clicked = await page.evaluate((name) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === name) {
const icon = cells[1].querySelector('.iconfont');
if (icon) { icon.click(); return true; }
}
}
return false;
}, projectName);
if (!clicked) {
console.log('❌ 未找到工程'); process.exit(1);
}
await page.waitForTimeout(3000);
console.log(`✅ 工程 "projectName" 已选`);
}
async function main() {
const opts = parseArgs();
const browser = await chromium.launch({ headless: !opts.headed, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, opts.project);
// 去PCC列表页
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/pcc/index`, { waitUntil: 'commit', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
// 找到目标PCC规则的数字ID
const pccIdResult = await page.evaluate((targetPccId) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
// cells[1] = ID, cells[2] = pccRuleId
if (cells.length >= 5 && cells[2].textContent.trim() === targetPccId) {
return cells[1].textContent.trim();
}
}
return null;
}, opts.pccId);
if (!pccIdResult) {
console.log(`❌ 未找到PCC规则: opts.pccId`);
await browser.close();
return;
}
console.log(`✅ 找到PCC规则 ID=pccIdResult`);
// 导航到编辑页
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/pcc/edit/pccIdResult`, { waitUntil: 'commit', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
// 打印所有输入字段
const inputs = await page.evaluate(() => {
const result = [];
document.querySelectorAll('input, select, textarea').forEach(el => {
if (el.name || el.id) {
result.push({
tag: el.tagName,
name: el.name,
id: el.id,
type: el.type,
value: el.value,
placeholder: el.placeholder
});
}
});
return result;
});
console.log('\n📋 页面所有输入字段:');
inputs.forEach(i => {
console.log(` i.tag [i.type] name="i.name" id="i.id" value="i.value" placeholder="i.placeholder"`);
});
// 查找与 maxbr/gbr 相关的字段
const qosFields = await page.evaluate(() => {
const allText = document.body.innerText.toLowerCase();
const keywords = ['maxbr', 'gbr', 'ul', 'dl', 'qos'];
const result = {};
document.querySelectorAll('input, select, textarea').forEach(el => {
const name = (el.name || '').toLowerCase();
const id = (el.id || '').toLowerCase();
for (const kw of keywords) {
if (name.includes(kw) || id.includes(kw)) {
result[name || id] = el.value || '(空)';
}
}
});
return result;
});
console.log('\n🔍 与QoS相关的字段:');
if (Object.keys(qosFields).length > 0) {
Object.entries(qosFields).forEach(([k, v]) => console.log(` k = v`));
} else {
console.log(' 未找到匹配的字段');
}
// 保存截图
await page.screenshot({ path: 'pcc_qos_explore.png', fullPage: true });
console.log('\n📸 截图已保存: pcc_qos_explore.png');
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/pcf-add-skill.js
/**
* PCF/PCRF 添加脚本
* 完整流程:登录 → 选工程 → 进PCF列表 → 点添加(弹窗iframe) → 填表单 → 提交
* 用法: node pcf-add-skill.js <名称> [--project <工程>] [--url <地址>] [--headed] \
* [--http2_sip <IP>] [--http2_port <端口>] [--MCC <值>] [--MNC <值>]
* 示例: node pcf-add-skill.js PCF-TEST --project XW_S5GC_1
*/
const { chromium } = require('playwright');
const path = require('path');
const fs = require('fs');
const BASE_URL = 'https://192.168.3.89';
const SESSION_DIR = path.join(__dirname, '.sessions');
function getSessionFile(baseUrl) {
const host = baseUrl.replace(/https?:\/\//, '').replace(/\./g, '_');
return `5gc_session_host.json`;
}
async function login(page, baseUrl) {
const sessionPath = path.join(SESSION_DIR, getSessionFile(baseUrl));
if (fs.existsSync(sessionPath)) {
try {
const storageState = JSON.parse(fs.readFileSync(sessionPath, 'utf8'));
if (storageState.cookies) {
await page.context().addCookies(storageState.cookies);
await page.goto(baseUrl + '/sim_5gc/project/index', { waitUntil: 'networkidle', timeout: 8000 }).catch(() => {});
if (!page.url().includes('/login')) {
console.log(' ✅ 使用缓存会话');
return true;
}
}
} catch {}
}
await page.goto(baseUrl + '/login', { waitUntil: 'networkidle', timeout: 15000 });
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForLoadState('networkidle');
const ctx = page.context();
const storageState = await ctx.storageState();
fs.writeFileSync(sessionPath, JSON.stringify({ cookies: storageState.cookies }, null, 2));
console.log(' ✅ 登录成功');
return true;
}
async function selectProject(page, projectName) {
await page.goto(BASE_URL + '/sim_5gc/project/index', { waitUntil: 'networkidle', timeout: 15000 });
await page.waitForSelector('.jsgrid-row, .jsgrid-alt-row', { timeout: 5000 }).catch(() => {});
await page.waitForTimeout(300);
for (let pageNum = 1; pageNum <= 200; pageNum++) {
const clicked = await page.evaluate((targetName) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === targetName) {
const icon = cells[1].querySelector('.iconfont');
if (icon) { icon.click(); return true; }
}
}
return false;
}, projectName);
if (clicked) { await page.waitForTimeout(2000); return true; }
const nextBtn = page.locator('.jsgrid-pager a:has-text("Next")');
if (!(await nextBtn.count())) break;
try { await nextBtn.click(); } catch { break; }
await page.waitForTimeout(1500);
}
console.log(` ❌ 未找到工程 "projectName"(精确匹配)`);
return false;
}
async function main() {
const args = process.argv.slice(2);
if (args.length < 1) {
console.log('用法: node pcf-add-skill.js <名称> [--project <工程>] [--url <地址>] [--headed]');
console.log(' [--http2_sip <IP>] [--http2_port <端口>] [--MCC <值>] [--MNC <值>]');
console.log('示例: node pcf-add-skill.js PCF-TEST --project XW_S5GC_1');
process.exit(1);
}
const name = args[0];
let headless = true;
let project = 'XW_S5GC_1';
let http2_sip = '192.168.20.90';
let http2_port = '80';
let mcc = '460';
let mnc = '01';
for (let i = 1; i < args.length; i++) {
if (args[i] === '--headed') headless = false;
else if (args[i] === '--project') project = args[++i];
else if (args[i] === '--url') BASE_URL = args[++i];
else if (args[i] === '--http2_sip') http2_sip = args[++i];
else if (args[i] === '--http2_port') http2_port = args[++i];
else if (args[i] === '--MCC') mcc = args[++i];
else if (args[i] === '--MNC') mnc = args[++i];
}
console.log(`▶ 添加 PCF: name`);
console.log(` http2_sip=http2_sip http2_port=http2_port MCC=mcc MNC=mnc`);
console.log(` 工程: project`);
const browser = await chromium.launch({ headless, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page, BASE_URL);
const ok = await selectProject(page, project);
if (!ok) throw new Error('工程选择失败');
console.log(' ✓ 工程已选');
// 进 PCF/PCRF 列表(用 JS 点击 sidebar 链接,兼容折叠菜单)
await page.evaluate(() => {
const links = document.querySelectorAll('a[href*="/pcf/"]');
for (const l of links) {
if (l.textContent.trim().includes('PCF')) { l.click(); return; }
}
});
await page.waitForTimeout(3000);
console.log(' ✓ 进入PCF列表,URL:', page.url());
// 点添加按钮(弹窗)
await page.waitForSelector('button:has-text("添加")', { timeout: 10000 }).catch(() => {});
await page.locator('button:has-text("添加")').first().click();
await page.waitForTimeout(2000);
console.log(' ✓ 点添加(弹窗)');
// 切换到弹窗 iframe
await page.locator('iframe[name="layui-layer-iframe2"]').waitFor({ timeout: 5000 });
const frame = page.frame('layui-layer-iframe2');
if (!frame) throw new Error('未找到弹窗 iframe');
await frame.waitForLoadState('domcontentloaded');
console.log(' ✓ 切换到弹窗iframe');
// 填名称
await frame.locator('input[name="name"]').fill(name);
console.log(` ✓ name = name`);
// 类型下拉:点击"请选择"
await frame.getByRole('textbox', { name: '请选择' }).click();
await frame.getByRole('definition').filter({ hasText: '仿真设备' }).click();
await page.waitForTimeout(500);
console.log(' ✓ 类型 = 仿真设备');
// 数量
await frame.locator('input[name="count"]').fill('1');
console.log(' ✓ count = 1');
// HTTP2 SIP
await frame.locator('input[name="http2_sip"]').fill(http2_sip);
console.log(` ✓ http2_sip = http2_sip`);
// HTTP2 PORT
await frame.locator('input[name="http2_port"]').fill(http2_port);
console.log(` ✓ http2_port = http2_port`);
// MCC - label 为"三位数字"
await frame.getByRole('textbox', { name: '三位数字', exact: true }).fill(mcc);
console.log(` ✓ MCC = mcc`);
// MNC - label 为"二位或三位数字"
await frame.getByRole('textbox', { name: '二位或三位数字' }).fill(mnc);
console.log(` ✓ MNC = mnc`);
// 提交
await frame.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log(' ✓ 已提交');
const url = page.url();
if (url.includes('/pcf/index')) {
console.log(` ✅ 添加成功,URL: url`);
} else {
console.log(` ⚠️ 可能未保存,URL: url`);
}
await browser.close();
}
main().catch(e => { console.error('❌', e.message); process.exit(1); });
FILE:scripts/pcf-edit-skill.js
/**
* pcf-edit-skill.js - PCF/PCRF 编辑
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
function parseArgs() {
const args = process.argv.slice(2);
const fields = {};
let name = null, project = 'XW_S5GC_1', headed = false;
for (let i = 0; i < args.length; i++) {
if (args[i] === '--name' && i + 1 < args.length) name = args[++i];
else if ((args[i] === '--project' || args[i] === '-p') && i + 1 < args.length) project = args[++i];
else if (args[i] === '--headed') headed = true;
else if (args[i].startsWith('--set-')) {
const k = args[i].slice(6).replace(/-/g, '_');
fields[k] = args[i + 1] != null && !args[i + 1].startsWith('--') ? args[++i] : 'true';
}
}
return { fields, name, project, headed };
}
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, projectName) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
let found = false;
for (const row of await page.locator('.jsgrid-row, .jsgrid-alt-row').all()) {
const cells = await row.locator('td').all();
if (cells.length >= 3) {
const text = (await cells[2].textContent()).trim();
if (text === projectName) {
await cells[1].locator('.iconfont').click();
found = true;
break;
}
}
}
if (!found) { console.error(`❌ 未找到工程: projectName`); process.exit(1); }
await page.waitForTimeout(3000);
console.log(`✅ 工程 "projectName" 已选`);
}
async function main() {
const { fields, name: targetName, project, headed } = parseArgs();
if (Object.keys(fields).length === 0) {
console.error('请至少指定一个 --set-<字段>'); process.exit(1);
}
const browser = await chromium.launch({ headless: !headed, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, project);
await page.goto(`globalBaseUrl/sim_5gc/pcf/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
const rows = await page.locator('.layui-table tbody tr').all();
const targets = [];
for (const row of rows) {
const cells = await row.locator('td').all();
if (cells.length >= 10) {
const id = parseInt((await cells[1].textContent()).trim(), 10);
const name = (await cells[2].textContent()).trim();
if (!isNaN(id) && name && (!targetName || name === targetName)) {
targets.push({ id, name });
}
}
}
if (targets.length === 0) { console.log('未找到匹配的 PCF'); await browser.close(); return; }
console.log(`找到 targets.length 个 PCF`);
for (const pcf of targets) {
process.stdout.write(`▶ [pcf.id] pcf.name ... `);
try {
await page.goto(`globalBaseUrl/sim_5gc/pcf/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
const allRows = await page.locator('.layui-table tbody tr').all();
let opened = false;
for (const row of allRows) {
const cells = await row.locator('td').all();
if (cells.length >= 10 && parseInt((await cells[1].textContent()).trim(), 10) === pcf.id) {
await cells[9].locator('a:has-text("编辑")').click();
opened = true;
break;
}
}
if (!opened) { console.log('❌ 打开弹窗失败'); continue; }
await page.waitForSelector('iframe[name="layui-layer-iframe2"]', { timeout: 5000 });
await page.waitForTimeout(2000);
const frame = page.locator('iframe[name="layui-layer-iframe2"]').contentFrame();
await page.waitForTimeout(500);
if (fields.http2_sip) await frame.locator('input[name="http2_sip"]').fill(fields.http2_sip);
if (fields.http2_port) await frame.locator('input[name="http2_port"]').fill(fields.http2_port);
if (fields.MCC) await frame.locator('input[name="MCC"]').fill(fields.MCC);
if (fields.MNC) await frame.locator('input[name="MNC"]').fill(fields.MNC);
if (fields.count) await frame.locator('input[name="count"]').fill(String(fields.count));
await frame.locator('button:has-text("提交")').click();
await page.waitForTimeout(2000);
await page.evaluate(() => {
const b = document.querySelector('.layui-layer-close');
if (b) b.click();
});
await page.waitForTimeout(1000);
await page.goto(`globalBaseUrl/sim_5gc/pcf/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
const rows2 = await page.locator('.layui-table tbody tr').all();
let found = false;
for (const row of rows2) {
const cells = await row.locator('td').all();
if (cells.length >= 10 && parseInt((await cells[1].textContent()).trim(), 10) === pcf.id) {
await cells[9].locator('a:has-text("编辑")').click();
await page.waitForTimeout(2000);
await page.waitForSelector('iframe[name="layui-layer-iframe2"]', { timeout: 5000 });
await page.waitForTimeout(500);
const frame2 = page.locator('iframe[name="layui-layer-iframe2"]').contentFrame();
let verified = true;
let actualVal = 'N/A';
if (fields.http2_sip) {
actualVal = await frame2.locator('input[name="http2_sip"]').inputValue();
verified = verified && actualVal === fields.http2_sip;
}
if (fields.http2_port) verified = verified && (await frame2.locator('input[name="http2_port"]').inputValue()) === fields.http2_port;
if (fields.MCC) verified = verified && (await frame2.locator('input[name="MCC"]').inputValue()) === fields.MCC;
if (fields.MNC) verified = verified && (await frame2.locator('input[name="MNC"]').inputValue()) === fields.MNC;
console.log(verified ? '✅' : '❌');
found = true;
await page.evaluate(() => { const b = document.querySelector('.layui-layer-close'); if (b) b.click(); });
break;
}
}
if (!found) console.log('❌');
} catch (e) {
console.log(`❌ e.message`);
}
}
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/pcf_smpolicy_explore.js
/**
* pcf_smpolicy_explore.js - 探索 PCF Smpolicy 页面
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, projectName) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
const clicked = await page.evaluate((name) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === name) {
const icon = cells[1].querySelector('.iconfont');
if (icon) { icon.click(); return true; }
}
}
return false;
}, projectName);
if (!clicked) { console.log('❌ 未找到工程'); process.exit(1); }
await page.waitForTimeout(3000);
console.log(`✅ 工程 "projectName" 已选`);
}
async function main() {
const browser = await chromium.launch({ headless: false, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, 'XW_SUPF_5_1_2_4');
// 探索 PCF 列表
await page.goto(`globalBaseUrl/sim_5gc/pcf/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const pcfList = await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
const pcfs = [];
rows.forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 10) {
pcfs.push({ id: cells[1].textContent.trim(), name: cells[2].textContent.trim() });
}
});
return pcfs;
});
console.log(`\n找到 pcfList.length 个 PCF:`, pcfList);
// 点击第一个 PCF 的编辑
await page.goto(`globalBaseUrl/sim_5gc/pcf/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.evaluate((targetId) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 10 && cells[1].textContent.trim() === targetId) {
const links = cells[9].querySelectorAll('a');
for (const link of links) {
if (link.textContent.trim() === '编辑') { link.click(); return; }
}
}
}
}, pcfList[0].id);
await page.waitForTimeout(3000);
console.log(`PCF编辑弹窗URL: page.url()`);
// 切换到iframe
const frame = page.frame('layui-layer-iframe2');
if (frame) {
console.log('✅ 进入弹窗iframe');
// 获取iframe内容
const iframeContent = await frame.evaluate(() => ({
url: window.location.href,
title: document.title,
text: document.body.innerText.substring(0, 3000)
}));
console.log('\n📄 iframe内容:');
console.log(iframeContent.text);
// 查找smpolicy相关的链接
const links = await frame.evaluate(() => {
const result = [];
document.querySelectorAll('a[href]').forEach(a => {
if (a.textContent.trim()) result.push({ text: a.textContent.trim(), href: a.href });
});
return result;
});
console.log('\n🔗 iframe内所有链接:');
links.forEach(l => console.log(` "l.text": l.href`));
// 查找包含smpolicy的链接
const smpolicyLinks = links.filter(l =>
l.href.includes('smpolicy') ||
l.text.includes('smpolicy') ||
l.text.includes('Smpolicy') ||
l.text.includes('默认')
);
if (smpolicyLinks.length > 0) {
console.log('\n🔍 smpolicy相关链接:');
smpolicyLinks.forEach(l => console.log(` "l.text": l.href`));
}
}
// 截图
await page.screenshot({ path: 'pcf_edit.png', fullPage: true });
console.log('\n📸 截图: pcf_edit.png');
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/qos-add-skill.js
/**
* qos-add-skill.js - QoS模板添加工具
*
* 用法:
* node qos-add-skill.js --project XW_SUPF_5_1_2_4 --qos-id qos3 --maxbr-ul 10000000 --maxbr-dl 20000000 --gbr-ul 5000000 --gbr-dl 5000000 [--headed]
* node qos-add-skill.js --project XW_SUPF_5_1_2_4 --qos-id qos3 --5qi 8 --maxbr-ul 10000000 --maxbr-dl 20000000 --gbr-ul 5000000 --gbr-dl 5000000 [--headed]
*
* 参数:
* --project 工程名(默认 XW_S5GC_1)
* --qos-id QoS模板ID(必填)
* --5qi 5QI值(不指定则自动从已有5qi列表中选择一个不同的值)
* --maxbr-ul 上行最大比特率(不指定则用默认值)
* --maxbr-dl 下行最大比特率(不指定则用默认值)
* --gbr-ul 上行保证比特率(不指定则用默认值)
* --gbr-dl 下行保证比特率(不指定则用默认值)
* --priority 优先级(默认空)
* --headed 显示浏览器窗口
*
* 默认值(用户未指定时):
* maxbrUl=10000000, maxbrDl=20000000, gbrUl=5000000, gbrDl=5000000
* 5qi=自动选择(从已有5qi列表中挑一个不存在的值,优先8/9/6/5...)
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
function parseArgs() {
const args = process.argv.slice(2);
const opts = {
project: 'XW_S5GC_1',
qosId: null,
qi: null,
maxbrUl: null,
maxbrDl: null,
gbrUl: null,
gbrDl: null,
priority: '',
headed: false,
};
for (let i = 0; i < args.length; i++) {
if (args[i] === '--project' || args[i] === '-p') opts.project = args[++i];
else if (args[i] === '--qos-id') opts.qosId = args[++i];
else if (args[i] === '--5qi') opts.qi = args[++i];
else if (args[i] === '--maxbr-ul') opts.maxbrUl = args[++i];
else if (args[i] === '--maxbr-dl') opts.maxbrDl = args[++i];
else if (args[i] === '--gbr-ul') opts.gbrUl = args[++i];
else if (args[i] === '--gbr-dl') opts.gbrDl = args[++i];
else if (args[i] === '--priority') opts.priority = args[++i];
else if (args[i] === '--headed') opts.headed = true;
}
if (!opts.qosId) {
console.error('❌ 缺少必要参数: --qos-id');
console.error(' 示例: node qos-add-skill.js --project XW_SUPF_5_1_2_4 --qos-id qos3 --maxbr-ul 10000000 --maxbr-dl 20000000 --gbr-ul 5000000 --gbr-dl 5000000');
process.exit(1);
}
return opts;
}
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, projectName) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
const clicked = await page.evaluate((name) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === name) {
const icon = cells[1].querySelector('.iconfont');
if (icon) { icon.click(); return true; }
}
}
return false;
}, projectName);
if (!clicked) { console.log('❌ 未找到工程'); process.exit(1); }
await page.waitForTimeout(3000);
console.log(`✅ 工程 "projectName" 已选`);
}
async function getUsed5qis(page) {
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/qos/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const usedQis = await page.evaluate(() => {
const qis = new Set();
document.querySelectorAll('.layui-table tbody tr').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 4) {
const qi = parseInt(cells[3].textContent.trim());
if (!isNaN(qi)) qis.add(qi);
}
});
return [...qis];
});
return usedQis;
}
function autoSelect5qi(usedQis) {
const candidates = [8, 9, 6, 5, 4, 3, 2, 1];
for (const c of candidates) {
if (!usedQis.includes(c)) return c;
}
return 8;
}
const DEFAULT_BR = { maxbrUl: '10000000', maxbrDl: '20000000', gbrUl: '5000000', gbrDl: '5000000' };
async function main() {
const opts = parseArgs();
const browser = await chromium.launch({ headless: !opts.headed, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, opts.project);
// 自动确定5qi(用户未指定时)
if (opts.qi === null) {
console.log('\n📋 检测已有QoS模板的5qi...');
const usedQis = await getUsed5qis(page);
console.log(` 已使用5qi: usedQis.join(', ')`);
opts.qi = autoSelect5qi(usedQis);
console.log(` ✅ 自动选择 5qi = opts.qi(与已有不同)`);
} else {
console.log(`\n📋 用户指定 5qi = opts.qi`);
}
// 应用默认值
const params = {
qosId: opts.qosId,
qi: opts.qi,
maxbrUl: opts.maxbrUl || DEFAULT_BR.maxbrUl,
maxbrDl: opts.maxbrDl || DEFAULT_BR.maxbrDl,
gbrUl: opts.gbrUl || DEFAULT_BR.gbrUl,
gbrDl: opts.gbrDl || DEFAULT_BR.gbrDl,
};
console.log('\n📋 最终参数:');
console.log(` qosId = params.qosId`);
console.log(` 5qi = params.qi`);
console.log(` maxbrUl = params.maxbrUl`);
console.log(` maxbrDl = params.maxbrDl`);
console.log(` gbrUl = params.gbrUl`);
console.log(` gbrDl = params.gbrDl`);
// 去添加页
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/qos/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
await page.locator('button:has-text("添加")').click();
await page.waitForTimeout(3000);
const frame = page.frame('layui-layer-iframe2');
if (!frame) { console.error('❌ 未找到弹窗iframe'); process.exit(1); }
await page.waitForTimeout(1000);
// 填写字段(使用 first() 确保能获取到元素)
await frame.locator('input[name="qosId"]').first().fill(params.qosId);
await frame.locator('input[name="5qi"]').first().fill(params.qi);
await frame.locator('input[name="maxbrUl"]').first().fill(params.maxbrUl);
await frame.locator('input[name="maxbrDl"]').first().fill(params.maxbrDl);
await frame.locator('input[name="gbrUl"]').first().fill(params.gbrUl);
await frame.locator('input[name="gbrDl"]').first().fill(params.gbrDl);
// 提交
await frame.locator('button:has-text("提交")').first().click();
await page.waitForTimeout(3000);
// 验证
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/qos/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const qosData = await page.evaluate((targetId) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
for (let i = 0; i < cells.length; i++) {
if (cells[i].textContent.trim() === targetId) {
return {
id: cells[1].textContent.trim(),
qi: cells[3].textContent.trim(),
maxbrUl: cells[4].textContent.trim(),
maxbrDl: cells[5].textContent.trim(),
gbrUl: cells[6].textContent.trim(),
gbrDl: cells[7].textContent.trim(),
};
}
}
}
return null;
}, params.qosId);
if (qosData) {
console.log('\n📋 保存的QoS数据:');
console.log(` ID=qosData.id, 5qi=qosData.qi, maxbrUl=qosData.maxbrUl, maxbrDl=qosData.maxbrDl, gbrUl=qosData.gbrUl, gbrDl=qosData.gbrDl`);
const ok = qosData.qi === params.qi && qosData.maxbrUl === params.maxbrUl && qosData.maxbrDl === params.maxbrDl && qosData.gbrUl === params.gbrUl && qosData.gbrDl === params.gbrDl;
console.log(ok ? '\n✅ QoS模板创建成功!' : '\n⚠️ 部分数据可能未正确保存');
}
console.log('\n✅ 完成');
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/qos_add_fields.js
/**
* qos_add_fields.js - 检查添加表单的实际字段名
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
}
async function selectProject(page, name) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.evaluate((n) => {
document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === n) {
cells[1].querySelector('.iconfont').click();
}
});
}, name);
await page.waitForTimeout(3000);
}
async function main() {
const browser = await chromium.launch({ headless: false, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, 'XW_SUPF_5_1_2_4');
// 去添加页
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/qos/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
await page.locator('button:has-text("添加")').click();
await page.waitForTimeout(2000);
const frame = page.frame('layui-layer-iframe2');
if (!frame) { console.log('❌ 未找到iframe'); await browser.close(); return; }
// 打印所有输入字段(带label/placeholder)
const fields = await frame.evaluate(() => {
const result = [];
// 找所有有name属性的input
document.querySelectorAll('input[name], select[name], textarea[name]').forEach(el => {
// 获取label
const id = el.id || el.name;
const label = el.closest('.layui-form-item')?.querySelector('.layui-form-label')?.textContent ||
el.closest('label')?.textContent ||
el.closest('.layui-select')?.previousElementSibling?.textContent || '';
result.push({
tag: el.tagName,
name: el.name,
id: el.id,
type: el.type,
value: el.value,
label: label.trim()
});
});
return result;
});
console.log('\n📋 表单字段:');
fields.forEach(f => console.log(` f.tag [f.type] name="f.name" label="f.label" value="f.value"`));
// 也打印表格形式的字段
const tableFields = await frame.evaluate(() => {
const result = [];
// 找所有 layui-form-item
document.querySelectorAll('.layui-form-item').forEach(item => {
const label = item.querySelector('.layui-form-label')?.textContent?.trim() || '';
const input = item.querySelector('input[name]');
if (input) {
result.push({ name: input.name, label, value: input.value, type: input.type });
}
});
return result;
});
console.log('\n📋 表格字段 (layui-form-item):');
tableFields.forEach(f => console.log(` name="f.name" label="f.label" value="f.value"`));
await page.screenshot({ path: 'qos_add_form.png' });
console.log('\n📸 截图: qos_add_form.png');
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/qos_detail_explore.js
/**
* qos_detail_explore.js - 探索 QoS 服务质量控制页面
* 找到 maxbrUl, maxbrDl, gbrUl, gbrDl 在哪里设置
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
function parseArgs() {
const args = process.argv.slice(2);
const opts = { project: 'XW_SUPF_5_1_2_4', headed: true };
for (let i = 0; i < args.length; i++) {
if (args[i] === '--project' || args[i] === '-p') opts.project = args[++i];
else if (args[i] === '--headed') opts.headed = true;
}
return opts;
}
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, projectName) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
const clicked = await page.evaluate((name) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === name) {
const icon = cells[1].querySelector('.iconfont');
if (icon) { icon.click(); return true; }
}
}
return false;
}, projectName);
if (!clicked) { console.log('❌ 未找到工程'); process.exit(1); }
await page.waitForTimeout(3000);
console.log(`✅ 工程 "projectName" 已选`);
}
async function main() {
const opts = parseArgs();
const browser = await chromium.launch({ headless: !opts.headed, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, opts.project);
// 导航到 QoS 服务质量控制页面
console.log('\n🌐 导航到 QoS 配置页面...');
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/qos/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
console.log(` URL: page.url()`);
// 打印页面所有文字
const pageText = await page.evaluate(() => document.body.innerText);
console.log('\n📄 页面内容(前3000字符):');
console.log(pageText.substring(0, 3000));
// 获取所有输入字段
const inputs = await page.evaluate(() => {
const result = [];
document.querySelectorAll('input, select, textarea').forEach(el => {
if (el.name || el.id) {
result.push({
tag: el.tagName,
name: el.name,
id: el.id,
type: el.type,
value: el.value,
placeholder: el.placeholder
});
}
});
return result;
});
console.log('\n📋 输入字段:');
inputs.forEach(i => console.log(` i.tag [i.type] name="i.name" id="i.id" value="i.value"`));
// 获取表格内容(如果有的话)
const tableData = await page.evaluate(() => {
const tables = document.querySelectorAll('.layui-table, table');
const result = [];
tables.forEach((tbl, idx) => {
const headers = [];
tbl.querySelectorAll('thead th').forEach(th => headers.push(th.textContent.trim()));
const rows = [];
tbl.querySelectorAll('tbody tr').forEach(tr => {
const cells = [];
tr.querySelectorAll('td').forEach(td => cells.push(td.textContent.trim()));
if (cells.length > 0) rows.push(cells);
});
if (headers.length > 0 || rows.length > 0) {
result.push({ idx, headers, rows });
}
});
return result;
});
console.log('\n📊 表格:');
tableData.forEach(t => {
console.log(` 表格t.idx: headers=JSON.stringify(t.headers)`);
t.rows.slice(0, 10).forEach(r => console.log(` row: JSON.stringify(r)`));
});
// 获取 layui-table 的确切内容(layui框架)
const layuiTable = await page.evaluate(() => {
const tables = document.querySelectorAll('.layui-table');
const result = [];
tables.forEach((tbl, ti) => {
const headers = [];
tbl.querySelectorAll('thead th').forEach(th => headers.push(th.textContent.trim()));
const rows = [];
tbl.querySelectorAll('tbody tr').forEach(tr => {
const cells = Array.from(tr.querySelectorAll('td')).map(td => td.textContent.trim());
rows.push(cells);
});
result.push({ ti, headers, rows });
});
return result;
});
if (layuiTable.length > 0) {
console.log('\n📊 Layui表格:');
layuiTable.forEach(t => {
console.log(` 表格t.ti: headers=JSON.stringify(t.headers)`);
t.rows.slice(0, 20).forEach(r => console.log(` JSON.stringify(r)`));
});
}
// 获取所有按钮
const buttons = await page.evaluate(() => {
const btns = [];
document.querySelectorAll('button, a.layui-btn, .layui-btn').forEach(b => {
if (b.textContent.trim()) {
btns.push({ text: b.textContent.trim(), href: b.href || '', onclick: b.getAttribute('onclick') || '' });
}
});
return btns;
});
console.log('\n🔘 按钮:');
buttons.forEach(b => console.log(` "b.text" href=b.href onclick=b.onclick`));
// 截图
await page.screenshot({ path: 'qos_detail.png', fullPage: true });
console.log('\n📸 截图已保存: qos_detail.png');
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/qos_raw_check.js
/**
* qos_raw_check.js - 直接查看 qos3 的原始数据
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
}
async function selectProject(page, name) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.evaluate((n) => {
document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === n) {
cells[1].querySelector('.iconfont').click();
}
});
}, name);
await page.waitForTimeout(3000);
}
async function main() {
const browser = await chromium.launch({ headless: true, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, 'XW_SUPF_5_1_2_4');
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/qos/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
// 打印所有表格
const tables = await page.evaluate(() => {
const result = [];
document.querySelectorAll('.layui-table').forEach((tbl, ti) => {
const headers = Array.from(tbl.querySelectorAll('thead th')).map((th, i) => i + ':' + th.textContent.trim());
const rows = Array.from(tbl.querySelectorAll('tbody tr')).map(tr =>
Array.from(tr.querySelectorAll('td')).map((td, i) => i + ':' + td.textContent.trim())
);
if (rows.length > 0) {
result.push({ ti, headers, rows });
}
});
return result;
});
tables.forEach(t => {
console.log(`\n📊 表格t.ti:`);
console.log(' 表头:', t.headers);
t.rows.forEach(r => {
const hasQos3 = r.some(c => c.includes('qos3'));
if (hasQos3) {
console.log(' ★ qos3行:', r.join(' | '));
}
});
});
// 直接查找 qos3 的数据
const qos3Data = await page.evaluate(() => {
// 方法:直接搜索包含 qos3 的行
const allRows = document.querySelectorAll('.layui-table tbody tr');
for (const row of allRows) {
const cells = row.querySelectorAll('td');
// 找 qosId 列
for (let i = 0; i < cells.length; i++) {
if (cells[i].textContent.trim() === 'qos3') {
return Array.from(cells).map((c, idx) => idx + '=' + c.textContent.trim());
}
}
}
return null;
});
console.log('\n🔍 qos3 直接搜索结果:', qos3Data);
// 也检查是否是编辑页
console.log('\n🌐 当前URL:', page.url());
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/qos_template_explore.js
/**
* qos_template_explore.js - 探索 QoS 模板配置页面
* 查找 maxbrUl, maxbrDl, gbrUl, gbrDl 在哪里设置
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
function parseArgs() {
const args = process.argv.slice(2);
const opts = { project: 'XW_SUPF_5_1_2_4', headed: true };
for (let i = 0; i < args.length; i++) {
if (args[i] === '--project' || args[i] === '-p') opts.project = args[++i];
else if (args[i] === '--headed') opts.headed = true;
}
return opts;
}
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, projectName) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
const clicked = await page.evaluate((name) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === name) {
const icon = cells[1].querySelector('.iconfont');
if (icon) { icon.click(); return true; }
}
}
return false;
}, projectName);
if (!clicked) { console.log('❌ 未找到工程'); process.exit(1); }
await page.waitForTimeout(3000);
console.log(`✅ 工程 "projectName" 已选`);
}
async function explorePage(page, url, description) {
console.log(`\n🌐 探索: description`);
console.log(` URL: url`);
await page.goto(url, { waitUntil: 'commit', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const url_ok = page.url();
console.log(` 当前URL: url_ok`);
// 获取所有侧边栏链接
const sidebarLinks = await page.evaluate(() => {
const links = [];
document.querySelectorAll('a[href]').forEach(a => {
if (a.textContent.trim()) {
links.push({ href: a.href, text: a.textContent.trim().substring(0, 50) });
}
});
return links;
});
console.log(` 侧边栏链接 (sidebarLinks.length个):`);
sidebarLinks.slice(0, 20).forEach(l => {
if (l.href.includes('/sim_5gc/') || l.href.includes('/predf')) {
console.log(` - l.text: l.href`);
}
});
// 查找包含 maxbr, gbr, qos 的链接
const qosLinks = sidebarLinks.filter(l =>
l.text.toLowerCase().includes('qos') ||
l.text.toLowerCase().includes('maxbr') ||
l.text.toLowerCase().includes('gbr') ||
l.href.toLowerCase().includes('qos') ||
l.href.toLowerCase().includes('maxbr') ||
l.href.toLowerCase().includes('template')
);
if (qosLinks.length > 0) {
console.log('\n 🔍 QoS相关链接:');
qosLinks.forEach(l => console.log(` - l.text: l.href`));
}
// 在页面文本中搜索关键词
const pageText = await page.evaluate(() => document.body.innerText);
const keywords = ['maxbrUl', 'maxbrDl', 'gbrUl', 'gbrDl', 'maxbr', 'gbr', 'qos', 'QoS'];
console.log('\n 📄 页面文本搜索:');
keywords.forEach(kw => {
const regex = new RegExp(kw, 'gi');
const matches = pageText.match(regex);
if (matches) {
console.log(` "kw" 出现 matches.length 次`);
}
});
}
async function main() {
const opts = parseArgs();
const browser = await chromium.launch({ headless: !opts.headed, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, opts.project);
// 探索多个可能包含 QoS 配置的页面
await explorePage(page, `globalBaseUrl/sim_5gc/predfPolicy/pcc/index`, 'PCC列表');
await explorePage(page, `globalBaseUrl/sim_5gc/predfPolicy/index`, 'Predefined Policy列表');
await explorePage(page, `globalBaseUrl/sim_5gc/qos/index`, 'QoS配置');
await explorePage(page, `globalBaseUrl/sim_5gc/index`, '首页');
// 尝试查找包含 qos 的所有链接
console.log('\n\n🔍 深度搜索整个 sidebar...');
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
const allLinks = await page.evaluate(() => {
const links = [];
document.querySelectorAll('a[href]').forEach(a => {
if (a.href && a.textContent.trim()) {
links.push({ href: a.href, text: a.textContent.trim() });
}
});
return links;
});
// 过滤可能相关的链接
const relevant = allLinks.filter(l =>
l.href.includes('qos') ||
l.href.includes('pcc') ||
l.href.includes('predf') ||
l.href.includes('template') ||
l.href.includes('config')
);
console.log('\n相关链接:');
relevant.forEach(l => console.log(` l.text: l.href`));
// 保存截图
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/pcc/index`, { waitUntil: 'commit', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.screenshot({ path: 'qos_explore.png', fullPage: true });
console.log('\n📸 截图已保存: qos_explore.png');
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/qos_verify.js
/**
* qos_verify.js - 验证 qos3 是否创建成功
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, projectName) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
const clicked = await page.evaluate((name) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === name) {
const icon = cells[1].querySelector('.iconfont');
if (icon) { icon.click(); return true; }
}
}
return false;
}, projectName);
if (!clicked) { console.log('❌ 未找到工程'); process.exit(1); }
await page.waitForTimeout(3000);
console.log(`✅ 工程 "projectName" 已选`);
}
async function main() {
const browser = await chromium.launch({ headless: false, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, 'XW_SUPF_5_1_2_4');
// 去QoS列表页
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/qos/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
// 搜索 qos3
const qos3Exists = await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
for (const cell of cells) {
if (cell.textContent.trim() === 'qos3') {
return true;
}
}
}
return false;
});
console.log(`\n'❌ qos3 不存在'`);
// 找到 qos3 的行
const qos3Row = await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
for (let i = 0; i < cells.length; i++) {
if (cells[i].textContent.trim() === 'qos3') {
// 返回这一行的所有单元格内容
return Array.from(cells).map(c => c.textContent.trim());
}
}
}
return null;
});
if (qos3Row) {
console.log('\n📋 qos3 行内容:');
console.log(JSON.stringify(qos3Row));
}
await page.screenshot({ path: 'qos_verify.png', fullPage: true });
console.log('\n📸 截图: qos_verify.png');
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/reg_smf.sh
#!/bin/bash
cd /home/dotouch/.openclaw/workspace/skills/5gc/scripts
node smf-pgwc-add-skill.js SMF_REG_$(date +%s) --project XW_S5GC_1 --pfcp_sip 10.200.2.50 --http2_sip 10.200.2.51 --ue_min 30.30.30.30 --ue_max 30.30.30.60 --mcc 460 --mnc 01 --pdu_capacity 200000 --interest_tac 101
FILE:scripts/regression.sh
#!/bin/bash
cd /home/dotouch/.openclaw/workspace/skills/5gc/scripts
TS=$(date +%s)
SMF_NAME="SMF_REG_TS"
SMF_RESULT=$(bash -c '
SMF_NAME="SMF_REG_'"TS"'"
node smf-pgwc-add-skill.js "$SMF_NAME" --project XW_S5GC_1 --pfcp_sip 10.200.2.50 --http2_sip 10.200.2.51 --ue_min 30.30.30.30 --ue_max 30.30.30.60 --mcc 460 --mnc 01 --pdu_capacity 200000 --interest_tac 101
' 2>&1)
echo "=== SMF ADD ==="
echo "$SMF_RESULT"
if echo "$SMF_RESULT" | grep -q "添加成功"; then
echo "✅ SMF ADD PASS"
else
echo "❌ SMF ADD FAIL"
fi
FILE:scripts/regression_check.js
/**
* regression_check.js - 全面检查工程 XW_SUPF_5_1_2_4 的当前配置状态
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
}
async function selectProject(page, name) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.locator('input[name="project_search_name"]').fill(name);
await page.keyboard.press('Enter');
await page.waitForTimeout(3000);
await page.evaluate((n) => {
document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === n) cells[1].querySelector('.iconfont').click();
});
}, name);
await page.waitForTimeout(3000);
}
async function getTableData(page, url) {
await page.goto(url, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
return await page.evaluate(() => {
const headers = Array.from(document.querySelectorAll('.layui-table thead th')).map((th, i) => i + ':' + th.textContent.trim());
const rows = Array.from(document.querySelectorAll('.layui-table tbody tr')).map(row =>
Array.from(row.querySelectorAll('td')).map(td => td.textContent.trim())
);
return { headers, rows };
});
}
async function main() {
const browser = await chromium.launch({ headless: true, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, 'XW_SUPF_5_1_2_4');
console.log('========================================');
console.log('工程 XW_SUPF_5_1_2_4 配置状态');
console.log('========================================\n');
// 1. PCF 列表
const pcf = await getTableData(page, `globalBaseUrl/sim_5gc/pcf/index`);
console.log('📋 PCF列表:');
if (pcf.rows.length > 0) {
console.log(` 表头: ')).join(', ')`);
pcf.rows.forEach(r => {
if (r.some(c => c.trim())) console.log(` r.filter(c => c.trim()).join(' | ')`);
});
} else { console.log(' 无数据'); }
// 2. PCF 编辑弹窗(查看 default_smpolicy 等)
console.log('\n📋 PCF编辑弹窗(default_smpolicy配置):');
await page.goto(`globalBaseUrl/sim_5gc/pcf/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
if (rows.length > 0) {
const links = rows[0].querySelectorAll('a');
for (const l of links) { if (l.textContent.trim() === '编辑') { l.click(); return; } }
}
});
await page.waitForTimeout(3000);
const frame = page.frame('layui-layer-iframe2');
if (frame) {
const pcfEditData = await frame.evaluate(() => {
const inputs = Array.from(document.querySelectorAll('input.xm-select-default')).map((inp, i) => ({
idx: i, value: inp.value, display: inp.parentElement.textContent.substring(0, 80)
}));
const selects = Array.from(document.querySelectorAll('select[name]')).map(s => s.name + '=' + s.value);
return { inputs, selects };
});
console.log(' xm-select:', JSON.stringify(pcfEditData.inputs));
}
// 3. smpolicy/default
const smp = await getTableData(page, `globalBaseUrl/sim_5gc/smpolicy/default/index`);
console.log('\n📋 smpolicy/default:');
if (smp.rows.length > 0) {
smp.rows.forEach(r => {
if (r.some(c => c.trim())) console.log(` ID=r[1] | name=r[2] | pccRules=r[4] | sessRules=r[3]`);
});
} else { console.log(' 无数据'); }
// 4. QoS 模板
const qos = await getTableData(page, `globalBaseUrl/sim_5gc/predfPolicy/qos/index`);
console.log('\n📋 QoS模板:');
if (qos.rows.length > 0) {
qos.rows.forEach(r => {
if (r[2]) console.log(` qosId=r[2] | 5qi=r[3] | maxbrUl=r[4] | maxbrDl=r[5] | gbrUl=r[6] | gbrDl=r[7]`);
});
} else { console.log(' 无数据'); }
// 5. TC 模板
const tc = await getTableData(page, `globalBaseUrl/sim_5gc/predfPolicy/trafficCtl/index`);
console.log('\n📋 Traffic Control模板:');
if (tc.rows.length > 0) {
tc.rows.forEach(r => {
if (r[2]) console.log(` tcId=r[2] | flowStatus=r[3]`);
});
} else { console.log(' 无数据'); }
// 6. PCC 规则
const pcc = await getTableData(page, `globalBaseUrl/sim_5gc/predfPolicy/pcc/index`);
console.log('\n📋 PCC规则:');
if (pcc.rows.length > 0) {
pcc.rows.forEach(r => {
if (r[2]) console.log(` pccRuleId=r[2] | precedence=r[4] | refQosData=r[5] | refTcData=r[6] | refChgData=r[7]`);
});
} else { console.log(' 无数据'); }
console.log('\n========================================');
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/regression_full.js
/**
* regression_full.js - 回归测试:清理 + 重建 + 验证
*
* 测试工程: XW_SUPF_5_1_2_4
*
* 步骤:
* 1. 清理无效 pcc_default(refQosData/refTcData 为空)
* 2. 创建新 PCC 规则 pcc_reg_test(绑定 qos1 + tc1)
* 3. 将 pcc_reg_test 添加到 sm_policy_default 的 pccRules
* 4. 验证所有绑定正确
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
const TEST_PROJECT = 'XW_SUPF_5_1_2_4';
const PCC_NEW = 'pcc_reg_test';
const QOS_ID = 'qos1';
const TC_ID = 'tc1';
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
}
async function selectProject(page, name) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.locator('input[name="project_search_name"]').fill(name);
await page.keyboard.press('Enter');
await page.waitForTimeout(3000);
await page.evaluate((n) => {
document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === n) cells[1].querySelector('.iconfont').click();
});
}, name);
await page.waitForTimeout(3000);
console.log(`✅ 工程 "name" 已选`);
}
async function goto(page, url) {
await page.goto(`globalBaseUrlurl`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
}
async function delPcc(page, pccRuleId) {
await goto(page, '/sim_5gc/predfPolicy/pcc/index');
const del = await page.evaluate((id) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 10 && cells[2].textContent.trim() === id) {
const links = cells[9].querySelectorAll('a');
for (const l of links) {
if (l.textContent.trim() === '删除') { l.click(); return true; }
}
}
}
return false;
}, pccRuleId);
if (del) {
await page.waitForTimeout(2000);
console.log(` 🗑️ 已删除 pcc_default`);
}
}
async function addPcc(page, pccId, qosId, tcId, precedence = '63') {
await goto(page, '/sim_5gc/predfPolicy/pcc/index');
await page.locator('button:has-text("添加")').click();
await page.waitForTimeout(3000);
await page.waitForFunction(() => window.location.href.includes('/predfPolicy/pcc/edit'), { timeout: 10000 });
await page.waitForTimeout(3000);
console.log(` 添加页URL: page.url()`);
// 填写字段
await page.locator('input[name="pccRuleId"]').fill(pccId);
await page.locator('input[name="precedence"]').fill(precedence);
// xm-select[0] = refQosData
await page.evaluate(() => document.querySelectorAll('input.xm-select-default')[0].parentElement.click());
await page.waitForTimeout(1000);
await page.locator('.xm-option.show-icon', { hasText: qosId }).click();
console.log(` ✅ refQosData=qosId`);
await page.waitForTimeout(500);
// xm-select[1] = refTcData
await page.evaluate(() => document.querySelectorAll('input.xm-select-default')[1].parentElement.click());
await page.waitForTimeout(1000);
await page.locator('.xm-option.show-icon', { hasText: tcId }).click();
console.log(` ✅ refTcData=tcId`);
await page.waitForTimeout(500);
// 提交
await page.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log(` ✅ PCC pccId 已提交`);
}
async function addPccToSmpolicy(page, pccId) {
await goto(page, '/sim_5gc/smpolicy/default/index');
await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
if (rows.length > 0) rows[0].querySelector('a')?.click();
});
await page.waitForTimeout(3000);
const frame = page.frame('layui-layer-iframe2');
if (!frame) { console.error('❌ iframe 未找到'); return; }
// pccRules = xm-select[1]
await frame.evaluate(() => document.querySelectorAll('input.xm-select-default')[1].parentElement.click());
await page.waitForTimeout(1000);
const opt = frame.locator('.xm-option.show-icon', { hasText: pccId });
if (await opt.isVisible({ timeout: 3000 }).catch(() => false)) {
await opt.click();
console.log(` ✅ pccRules+=pccId`);
} else {
const avail = await frame.evaluate(() =>
Array.from(document.querySelectorAll('.xm-option.show-icon')).map(o => o.textContent.trim())
);
console.log(` ❌ pccId 不可见,可用: avail.join(', ')`);
return;
}
await page.waitForTimeout(500);
// 关闭 xm-select 下拉(按 Escape 避免遮罩层)
await page.keyboard.press('Escape');
await page.waitForTimeout(300);
await frame.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log(` ✅ sm_policy_default 已提交`);
}
async function verify(page) {
await goto(page, '/sim_5gc/predfPolicy/pcc/index');
const pccData = await page.evaluate((targetId) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 8 && cells[2].textContent.trim() === targetId) {
return {
pccRuleId: cells[2].textContent.trim(),
precedence: cells[4].textContent.trim(),
refQosData: cells[5].textContent.trim(),
refTcData: cells[6].textContent.trim(),
};
}
}
return null;
}, PCC_NEW);
await goto(page, '/sim_5gc/smpolicy/default/index');
const smpData = await page.evaluate((targetId) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 6 && cells[2].textContent.trim() === targetId) {
return { pccRules: cells[4].textContent.trim() };
}
}
return null;
}, 'sm_policy_default');
console.log('\n========================================');
console.log('回归测试结果');
console.log('========================================');
const tests = [
{ name: `PCC PCC_NEW 创建`, pass: !!pccData },
{ name: `PCC refQosData=QOS_ID`, pass: pccData?.refQosData === QOS_ID },
{ name: `PCC refTcData=TC_ID`, pass: pccData?.refTcData === TC_ID },
{ name: `sm_policy_default 包含 PCC_NEW`, pass: smpData?.pccRules?.includes(PCC_NEW) },
];
let allPass = true;
for (const t of tests) {
console.log(` '❌' t.name`);
if (!t.pass) allPass = false;
}
console.log('========================================');
if (allPass) {
console.log('🎉 全部通过!');
} else {
console.log('⚠️ 部分失败');
if (pccData) console.log(' PCC数据:', JSON.stringify(pccData));
if (smpData) console.log(' smp数据:', JSON.stringify(smpData));
}
return allPass;
}
async function main() {
console.log('========================================');
console.log('5GC PCC 技能回归测试');
console.log(`工程: TEST_PROJECT | PCC: PCC_NEW`);
console.log('========================================\n');
const browser = await chromium.launch({ headless: false, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, TEST_PROJECT);
// Step 1: 删除无效 pcc_default
console.log('📦 Step 1: 清理无效 pcc_default...');
await delPcc(page, 'pcc_default');
// Step 2: 创建新 PCC 规则
console.log(`\n📦 Step 2: 创建 PCC PCC_NEW (qos=QOS_ID, tc=TC_ID)...`);
await addPcc(page, PCC_NEW, QOS_ID, TC_ID);
// Step 3: 添加到 sm_policy_default
console.log(`\n📦 Step 3: 添加到 sm_policy_default...`);
await addPccToSmpolicy(page, PCC_NEW);
// Step 4: 验证
console.log('\n📦 Step 4: 验证...');
const ok = await verify(page);
await browser.close();
process.exit(ok ? 0 : 1);
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/regression_test.js
/**
* regression_test.js - 回归测试:测试每个技能的正确性
*
* 测试工程:XW_SUPF_5_1_2_4(有完整数据)
* 测试项目:
* 1. qos-add-skill - QoS模板添加
* 2. pcc-add-skill - PCC规则添加(使用已有的qos1+tc1)
* 3. smpolicy_edit - sm_policy_default 编辑(绑定PCC规则)
* 4. pcf-edit-skill - PCF default_smpolicy 配置
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
const PROJECT = 'XW_SUPF_5_1_2_4';
const TARGET_PCF_ID = '12770';
// ── 工具函数 ──────────────────────────────────────────────────────────
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000, waitUntil: 'domcontentloaded' });
await page.waitForTimeout(2000);
// 如果页面已登录(URL变回login或自动跳转),等待login表单出现
const emailInput = page.locator('input[name="email"]').first();
try { await emailInput.waitFor({ state: 'visible', timeout: 5000 }); } catch(e) {}
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, name) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.locator('input[name="project_search_name"]').fill(name);
await page.keyboard.press('Enter');
await page.waitForTimeout(3000);
await page.evaluate((n) => {
document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === n) cells[1].querySelector('.iconfont').click();
});
}, name);
await page.waitForTimeout(3000);
console.log(`✅ 工程 "name" 已选`);
}
async function getTableRows(page, url) {
await page.goto(url, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
return await page.evaluate(() => {
const rows = Array.from(document.querySelectorAll('.layui-table tbody tr')).map(row =>
Array.from(row.querySelectorAll('td')).map(td => td.textContent.trim())
);
return rows;
});
}
async function waitAndGetFrame(page) {
await page.waitForTimeout(3000);
return page.frame('layui-layer-iframe2');
}
function assert(label, actual, expected) {
const pass = actual === expected || String(actual).trim() === String(expected).trim();
console.log(` '❌' label: `got "${actual", expected "expected"`}`);
return pass;
}
// ── 测试用例 ──────────────────────────────────────────────────────────
async function test_qos_add(browser, ctx) {
console.log('\n━━━ TEST 1: qos-add-skill ━━━');
const page = await ctx.newPage();
await login(page);
await selectProject(page, PROJECT);
// 生成测试用的唯一 qosId
const testQosId = 'qos_regtest_' + Date.now();
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/qos/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
// 获取当前已有的5qi列表
const existing5qi = await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
const qiSet = new Set();
rows.forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 4 && cells[3]) {
const qi = cells[3].textContent.trim();
if (qi) qiSet.add(qi);
}
});
return Array.from(qiSet);
});
// 选择一个不存在的5qi
const target5qi = ['8','9','6','5','7','10'].find(q => !existing5qi.includes(q)) || '8';
console.log(` 已有5qi: [existing5qi.join(',')], 将使用 5qi=target5qi`);
// 点击添加按钮
await page.locator('button:has-text("添加")').click();
await page.waitForTimeout(3000);
// 填写表单
await page.locator('input[name="qosId"]').fill(testQosId);
await page.locator('input[name="fiveQi"]').fill(target5qi);
await page.locator('input[name="maxbrUl"]').fill('10000000');
await page.locator('input[name="maxbrDl"]').fill('20000000');
await page.locator('input[name="gbrUl"]').fill('5000000');
await page.locator('input[name="gbrDl"]').fill('5000000');
await page.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
// 验证
const rows = await getTableRows(page, `globalBaseUrl/sim_5gc/predfPolicy/qos/index`);
const found = rows.find(r => r[2] === testQosId);
if (found) {
console.log(` ✅ QoS模板 testQosId 创建成功`);
console.log(` 5qi=found[3], maxbrUl=found[4], maxbrDl=found[5], gbrUl=found[6], gbrDl=found[7]`);
} else {
console.log(` ❌ QoS模板 testQosId 未找到`);
}
await page.close();
return testQosId;
}
async function test_pcc_add(browser, ctx, qosId, tcId) {
console.log('\n━━━ TEST 2: pcc-add-skill ━━━');
const page = await ctx.newPage();
await login(page);
await selectProject(page, PROJECT);
const testPccId = 'pcc_regtest_' + Date.now();
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/pcc/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
// 点击添加
await page.locator('button:has-text("添加")').click();
await page.waitForTimeout(3000);
await page.waitForFunction(() => window.location.href.includes('/predfPolicy/pcc/edit'), { timeout: 10000 });
await page.waitForTimeout(3000);
// 填写
await page.locator('input[name="pccRuleId"]').fill(testPccId);
await page.locator('input[name="precedence"]').fill('63');
// xm-select[0] = refQosData
await page.evaluate(() => document.querySelectorAll('input.xm-select-default')[0]?.parentElement.click());
await page.waitForTimeout(1000);
const qosVisible = await page.locator('.xm-option.show-icon', { hasText: qosId }).isVisible({ timeout: 3000 }).catch(() => false);
if (qosVisible) {
await page.locator('.xm-option.show-icon', { hasText: qosId }).click();
console.log(` ✅ refQosData=qosId`);
} else {
console.log(` ❌ refQosData=qosId 不可见`);
}
await page.waitForTimeout(500);
// xm-select[1] = refTcData
await page.evaluate(() => document.querySelectorAll('input.xm-select-default')[1]?.parentElement.click());
await page.waitForTimeout(1000);
const tcVisible = await page.locator('.xm-option.show-icon', { hasText: tcId }).isVisible({ timeout: 3000 }).catch(() => false);
if (tcVisible) {
await page.locator('.xm-option.show-icon', { hasText: tcId }).click();
console.log(` ✅ refTcData=tcId`);
} else {
console.log(` ❌ refTcData=tcId 不可见`);
}
await page.waitForTimeout(500);
await page.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
// 验证
const rows = await getTableRows(page, `globalBaseUrl/sim_5gc/predfPolicy/pcc/index`);
const found = rows.find(r => r[2] === testPccId);
if (found) {
console.log(` ✅ PCC规则 testPccId 创建成功`);
console.log(` precedence=found[4], refQosData=found[5], refTcData=found[6]`);
} else {
console.log(` ❌ PCC规则 testPccId 未找到`);
}
await page.close();
return testPccId;
}
async function test_smpolicy_edit(browser, ctx, pccId) {
console.log('\n━━━ TEST 3: smpolicy_edit (pccRules绑定) ━━━');
const page = await ctx.newPage();
await login(page);
await selectProject(page, PROJECT);
await page.goto(`globalBaseUrl/sim_5gc/smpolicy/default/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
// 找到 sm_policy_default 的行
const smpId = await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === 'sm_policy_default') {
const links = cells[9].querySelectorAll('a');
for (const l of links) { if (l.textContent.trim() === '编辑') return cells[1].textContent.trim(); }
}
}
return null;
});
if (!smpId) { console.log(' ❌ sm_policy_default 未找到'); await page.close(); return; }
console.log(` sm_policy_default ID=smpId`);
await page.goto(`globalBaseUrl/sim_5gc/smpolicy/default/edit/smpId`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const frame = page.frame('layui-layer-iframe2');
if (!frame) { console.log(' ❌ 无法获取iframe'); await page.close(); return; }
// 检查 pccRules xm-select 当前值
const beforePccRules = await frame.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
return inputs.length >= 2 ? inputs[1].value : '';
});
console.log(` 编辑前 pccRules value="beforePccRules"`);
// 点击 pccRules xm-select(第1个)
await frame.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
if (inputs[1]) inputs[1].parentElement.click();
});
await page.waitForTimeout(1000);
// 检查是否有选项包含目标pccId
const pccOptVisible = await frame.locator('.xm-option.show-icon', { hasText: pccId }).isVisible({ timeout: 3000 }).catch(() => false);
console.log(` 目标 PCC 选项 "pccId" 可见: pccOptVisible`);
if (pccOptVisible) {
await frame.locator('.xm-option.show-icon', { hasText: pccId }).click();
console.log(` ✅ 选择 pccId`);
}
await page.waitForTimeout(500);
// 获取更新后的 pccRules
const afterPccRules = await frame.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
return inputs.length >= 2 ? inputs[1].value : '';
});
console.log(` 编辑后 pccRules value="afterPccRules"`);
// 提交
await frame.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
// 验证
await page.goto(`globalBaseUrl/sim_5gc/smpolicy/default/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const finalPcc = await page.evaluate((id) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 6 && cells[1].textContent.trim() === id) {
return cells[4].textContent.trim(); // pccRules 列
}
}
return null;
}, smpId);
console.log(` 最终 pccRules="finalPcc"`);
if (finalPcc && finalPcc.includes(pccId)) {
console.log(` ✅ sm_policy_default pccRules 绑定成功`);
} else {
console.log(` ⚠️ pccRules="finalPcc"(可能已经包含或绑定方式不同)`);
}
await page.close();
}
async function test_pcf_edit(browser, ctx, smpolicyName) {
console.log('\n━━━ TEST 4: pcf-edit-skill (default_smpolicy) ━━━');
const page = await ctx.newPage();
await login(page);
await selectProject(page, PROJECT);
await page.goto(`globalBaseUrl/sim_5gc/pcf/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
// 点击 PCF 编辑按钮
await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
if (rows.length > 0) {
const links = rows[0].querySelectorAll('a');
for (const l of links) { if (l.textContent.trim() === '编辑') { l.click(); return; } }
}
});
await page.waitForTimeout(3000);
const frame = page.frame('layui-layer-iframe2');
if (!frame) { console.log(' ❌ 无法获取iframe'); await page.close(); return; }
// 检查 default_smpolicy xm-select 当前值
const beforeDefault = await frame.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
return inputs.length >= 1 ? inputs[0].value : '';
});
console.log(` 编辑前 default_smpolicy value="beforeDefault"`);
// 点击 default_smpolicy xm-select(第0个)
await frame.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
if (inputs[0]) inputs[0].parentElement.click();
});
await page.waitForTimeout(1000);
// 检查目标 smpolicy 是否可见
const smpOptVisible = await frame.locator('.xm-option.show-icon', { hasText: smpolicyName }).isVisible({ timeout: 3000 }).catch(() => false);
console.log(` 目标 smpolicy "smpolicyName" 可见: smpOptVisible`);
if (smpOptVisible) {
await frame.locator('.xm-option.show-icon', { hasText: smpolicyName }).click();
console.log(` ✅ default_smpolicy=smpolicyName`);
} else {
console.log(` ❌ default_smpolicy=smpolicyName 不可见`);
}
await page.waitForTimeout(500);
// 获取更新后的值
const afterDefault = await frame.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
return inputs.length >= 1 ? inputs[0].value : '';
});
console.log(` 编辑后 default_smpolicy value="afterDefault"`);
// 提交
await frame.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log(` ✅ PCF 提交完成`);
await page.close();
}
// ── 主流程 ─────────────────────────────────────────────────────────────
async function main() {
const browser = await chromium.launch({ headless: true, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
console.log('========================================');
console.log('5GC 技能回归测试');
console.log(`工程: PROJECT`);
console.log('========================================');
let qosId = null, pccId = null;
try {
// TEST 1: QoS模板添加
qosId = await test_qos_add(browser, ctx);
} catch(e) {
console.log(' ❌ TEST 1 异常:', e.message);
}
try {
// TEST 2: PCC规则添加(使用已有的qos1+tc1)
pccId = await test_pcc_add(browser, ctx, 'qos1', 'tc1');
} catch(e) {
console.log(' ❌ TEST 2 异常:', e.message);
}
try {
// TEST 3: sm_policy_default 编辑(绑定 PCC)
if (pccId) await test_smpolicy_edit(browser, ctx, pccId);
} catch(e) {
console.log(' ❌ TEST 3 异常:', e.message);
}
try {
// TEST 4: PCF default_smpolicy 配置
await test_pcf_edit(browser, ctx, 'sm_policy_default');
} catch(e) {
console.log(' ❌ TEST 4 异常:', e.message);
}
console.log('\n========================================');
console.log('回归测试完成');
console.log('========================================');
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/run-5gc-detached.sh
#!/usr/bin/env bash
set -euo pipefail
# 5GC detached runner
# 用法:
# ./run-5gc-detached.sh <label> -- <actual command...>
# 示例:
# ./run-5gc-detached.sh bulk-ue -- node skills/5gc/scripts/5gc.js ue edit --project XW_S5GC_1 --set-msisdn 8613888888888
if [ "$#" -lt 3 ]; then
echo "Usage: $0 <label> -- <command...>" >&2
exit 1
fi
LABEL="$1"
shift
if [ "$1" != "--" ]; then
echo "Usage: $0 <label> -- <command...>" >&2
exit 1
fi
shift
ROOT="/home/dotouch/.openclaw/workspace/skills/5gc/test_results"
mkdir -p "$ROOT"
TS="$(date +%Y%m%d-%H%M%S)"
LOG="$ROOT/TS-LABEL.log"
PIDFILE="$ROOT/TS-LABEL.pid"
CMD=("$@")
{
echo "[$(date '+%F %T')] label=$LABEL"
echo "[$(date '+%F %T')] cwd=$(pwd)"
printf '[%s] cmd=' "$(date '+%F %T')"
printf '%q ' "CMD[@]"
echo
} >> "$LOG"
# 尽量脱离当前聊天 exec 生命周期
nohup setsid "CMD[@]" >> "$LOG" 2>&1 < /dev/null &
PID=$!
echo "$PID" > "$PIDFILE"
echo "started"
echo "label=$LABEL"
echo "pid=$PID"
echo "log=$LOG"
echo "pidfile=$PIDFILE"
FILE:scripts/run_smf_add.sh
#!/bin/bash
cd /home/dotouch/.openclaw/workspace/skills/5gc/scripts
TS=$(date +%H%M%S)
node smf-pgwc-add-skill.js \
--name SMF_FULL_$TS \
--project XW_S5GC_1 \
--pfcp_sip 10.200.2.50 \
--http2_sip 10.200.2.51 \
--ue_min 30.30.30.30 \
--ue_max 30.30.30.60 \
--mcc 460 \
--mnc 01 \
--pdu_capacity 200000 \
--interest_tac "101"
FILE:scripts/setup_pcf_default.js
/**
* setup_pcf_default.js - 为任意工程配置PCF默认规则(完整版)
*
* 完整链路:
* 1. 创建 QoS 模板(自动选不同5qi)
* 2. 创建 Traffic Control 模板(tc1)
* 3. 创建 PCC 规则,填写所有必填 xm-select
* 4. 创建 sm_policy_default 并绑定 PCC
* 5. 配置 PCF 的 default_smpolicy 下拉
*
* 用法: node setup_pcf_default.js --project XW_SUPF_SN4_5_2_8 [--qos-id qos1] [--tc-id tc1] [--5qi 8] \
* [--maxbr-ul 10000000 --maxbr-dl 20000000 --gbr-ul 5000000 --gbr-dl 5000000] [--headed]
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
function parseArgs() {
const args = process.argv.slice(2);
const opts = {
project: null,
qosId: 'qos1',
tcId: 'tc1',
qi: null,
maxbrUl: null,
maxbrDl: null,
gbrUl: null,
gbrDl: null,
pccId: null,
precedence: null,
headed: false,
};
for (let i = 0; i < args.length; i++) {
if (args[i] === '--project' || args[i] === '-p') opts.project = args[++i];
else if (args[i] === '--qos-id') opts.qosId = args[++i];
else if (args[i] === '--tc-id') opts.tcId = args[++i];
else if (args[i] === '--5qi') opts.qi = args[++i];
else if (args[i] === '--maxbr-ul') opts.maxbrUl = args[++i];
else if (args[i] === '--maxbr-dl') opts.maxbrDl = args[++i];
else if (args[i] === '--gbr-ul') opts.gbrUl = args[++i];
else if (args[i] === '--gbr-dl') opts.gbrDl = args[++i];
else if (args[i] === '--pcc-id') opts.pccId = args[++i];
else if (args[i] === '--precedence') opts.precedence = args[++i];
else if (args[i] === '--headed') opts.headed = true;
}
if (!opts.project) {
console.error('❌ 缺少 --project 参数');
process.exit(1);
}
return opts;
}
const DEFAULT_BR = { maxbrUl: '10000000', maxbrDl: '20000000', gbrUl: '5000000', gbrDl: '5000000' };
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, name) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.locator('input[name="project_search_name"]').fill(name);
await page.keyboard.press('Enter');
await page.waitForTimeout(3000);
const clicked = await page.evaluate((n) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === n) {
cells[1].querySelector('.iconfont').click();
return true;
}
}
return false;
}, name);
if (!clicked) { console.log('❌ 选择工程失败'); process.exit(1); }
await page.waitForTimeout(3000);
console.log(`✅ 工程 "name" 已选`);
}
// 获取已使用的5qi(排除空值)
async function getUsed5qis(page) {
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/qos/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const qis = await page.evaluate(() => {
const set = new Set();
document.querySelectorAll('.layui-table tbody tr').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 4) {
const v = cells[3].textContent.trim();
if (v && !isNaN(parseInt(v))) set.add(parseInt(v));
}
});
return [...set];
});
return qis;
}
function autoSelect5qi(usedQis) {
const candidates = [8, 9, 6, 5, 4, 3, 2, 1];
for (const c of candidates) {
if (!usedQis.includes(c)) return c;
}
return 8;
}
// 通用xm-select选择函数
async function xmSelectChoose(page, index, optionText) {
await page.evaluate((idx) => {
const inputs = document.querySelectorAll('input.xm-select-default');
if (inputs[idx]) inputs[idx].parentElement.click();
}, index);
await page.waitForTimeout(1000);
const result = await page.evaluate((text) => {
const opts = document.querySelectorAll('.xm-option.show-icon');
for (const opt of opts) {
if (opt.textContent.trim() === text) {
opt.click();
return 'selected';
}
}
return 'not_found';
}, optionText);
if (result === 'selected') {
console.log(` ✅ 选择 optionText`);
} else {
console.log(` ⚠️ 未找到选项: optionText`);
}
await page.waitForTimeout(300);
}
// ─── 创建 QoS 模板 ────────────────────────────────────────────────────────
async function addQos(page, opts) {
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/qos/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
await page.locator('button:has-text("添加")').click();
await page.waitForTimeout(2000);
const iframe = page.locator('iframe[name="layui-layer-iframe2"]');
const frame = (await iframe.count() > 0) ? page.frame('layui-layer-iframe2') : page;
await frame.waitForTimeout(2000);
await frame.locator('input[name="qosId"]').first().fill(String(opts.qosId));
await frame.locator('input[name="5qi"]').first().fill(String(opts.qi));
await frame.locator('input[name="maxbrUl"]').first().fill(String(opts.maxbrUl));
await frame.locator('input[name="maxbrDl"]').first().fill(String(opts.maxbrDl));
await frame.locator('input[name="gbrUl"]').first().fill(String(opts.gbrUl));
await frame.locator('input[name="gbrDl"]').first().fill(String(opts.gbrDl));
await frame.locator('button:has-text("提交")').first().click();
await page.waitForTimeout(3000);
console.log(` ✅ QoS模板 opts.qosId 已创建 (5qi=opts.qi)`);
}
// ─── 创建 Traffic Control 模板 ───────────────────────────────────────────
async function addTc(page, opts) {
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/trafficCtl/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
// 检查是否已存在
const exists = await page.evaluate((id) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
for (const cell of cells) {
if (cell.textContent.trim() === id) return true;
}
}
return false;
}, opts.tcId);
if (exists) {
console.log(` ℹ️ TC模板 opts.tcId 已存在,跳过`);
return;
}
await page.locator('button:has-text("添加")').click();
await page.waitForTimeout(3000);
const iframe = page.locator('iframe[name="layui-layer-iframe2"]');
const frame = (await iframe.count() > 0) ? page.frame('layui-layer-iframe2') : page;
await frame.waitForTimeout(2000);
// 填写 tcId(必填)
await frame.locator('input[name="tcId"]').first().fill(String(opts.tcId));
console.log(` tcId = opts.tcId`);
await frame.locator('button:has-text("提交")').first().click();
await page.waitForTimeout(3000);
console.log(` ✅ TC模板 opts.tcId 已创建`);
}
// ─── 创建 PCC 规则(所有必填xm-select) ─────────────────────────────────
async function addPcc(page, opts) {
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/pcc/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
await page.locator('button:has-text("添加")').click();
await page.waitForTimeout(3000);
await page.waitForFunction(() => window.location.href.includes('/predfPolicy/pcc/edit'), { timeout: 10000 });
await page.waitForTimeout(3000);
// 填写文本字段
await page.locator('input[name="pccRuleId"]').fill(String(opts.pccId));
const precedence = opts.precedence !== null ? String(opts.precedence) : '63';
await page.locator('input[name="precedence"]').fill(precedence);
console.log(` pccRuleId=opts.pccId, precedence=precedence`);
// xm-select[0] = refQosData(QoS)
await xmSelectChoose(page, 0, opts.qosId);
await page.keyboard.press('Escape');
await page.waitForTimeout(500);
// xm-select[1] = refTcData(Traffic Control)
await xmSelectChoose(page, 1, opts.tcId);
await page.keyboard.press('Escape');
await page.waitForTimeout(500);
// xm-select[2] = refChgData(Charging)—— 如果有可选的
// 先检查是否有 Charging 选项
const hasCharging = await page.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
if (inputs[2]) {
inputs[2].parentElement.click();
}
return false;
});
await page.waitForTimeout(1000);
const chargingOpts = await page.evaluate(() => {
return Array.from(document.querySelectorAll('.xm-option.show-icon')).map(o => o.textContent.trim());
});
await page.keyboard.press('Escape');
await page.waitForTimeout(500);
if (chargingOpts.length > 0) {
console.log(` ℹ️ Charging 有选项: JSON.stringify(chargingOpts),尝试选择`);
await xmSelectChoose(page, 2, chargingOpts[0]);
await page.keyboard.press('Escape');
await page.waitForTimeout(500);
}
await page.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log(` ✅ PCC规则 opts.pccId 已创建`);
}
// ─── 创建 sm_policy_default ────────────────────────────────────────────────
async function addSmpolicyDefault(page, opts) {
await page.goto(`globalBaseUrl/sim_5gc/smpolicy/default/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
await page.locator('button:has-text("添加")').click();
await page.waitForTimeout(3000);
await page.locator('iframe[name="layui-layer-iframe2"]').waitFor({ timeout: 5000 });
const frame = page.frame('layui-layer-iframe2');
await frame.waitForLoadState('domcontentloaded');
await frame.waitForTimeout(1000);
// 填写名称
await frame.locator('input[name="name"]').fill(String(opts.name));
// smpolicy edit iframe 中 xm-select[1] = pccRules
await frame.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
if (inputs[1]) inputs[1].parentElement.click();
});
await frame.waitForTimeout(1000);
await frame.evaluate((text) => {
const opts2 = document.querySelectorAll('.xm-option.show-icon');
for (const opt of opts2) {
if (opt.textContent.trim() === text) { opt.click(); return; }
}
}, opts.pccId);
await page.keyboard.press('Escape');
await frame.waitForTimeout(500);
await frame.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log(` ✅ sm_policy_default 已创建并绑定 opts.pccId`);
}
// ─── 配置 PCF 的 default_smpolicy ────────────────────────────────────────
async function configurePcfDefaultSmpolicy(page, smpolicyName) {
await page.goto(`globalBaseUrl/sim_5gc/pcf/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
if (rows.length > 0) {
const links = rows[0].querySelectorAll('a');
for (const l of links) {
if (l.textContent.trim() === '编辑') { l.click(); return; }
}
}
});
await page.waitForTimeout(3000);
await page.locator('iframe[name="layui-layer-iframe2"]').waitFor({ timeout: 5000 });
const frame = page.frame('layui-layer-iframe2');
await frame.waitForLoadState('domcontentloaded');
await frame.waitForTimeout(1000);
// default_smpolicy 下拉 = xm-select[0]
await frame.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
if (inputs[0]) inputs[0].parentElement.click();
});
await frame.waitForTimeout(1000);
await frame.evaluate((text) => {
const opts2 = document.querySelectorAll('.xm-option.show-icon');
for (const opt of opts2) {
if (opt.textContent.trim() === text) { opt.click(); return; }
}
}, smpolicyName);
await page.keyboard.press('Escape');
await frame.waitForTimeout(500);
await frame.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log(` ✅ PCF default_smpolicy 已配置为 smpolicyName`);
}
// ─── 验证 PCC 规则的 refQosData 和 refTcData ─────────────────────────────
async function verifyPcc(page, pccId) {
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/pcc/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const pccData = await page.evaluate((targetId) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
// cells[2]=pccRuleId, cells[4]=precedence, cells[5]=refQosData, cells[6]=refTcData
if (cells.length >= 7 && cells[2].textContent.trim() === targetId) {
return {
id: cells[1].textContent.trim(),
pccRuleId: cells[2].textContent.trim(),
precedence: cells[4].textContent.trim(),
refQosData: cells[5].textContent.trim(),
refTcData: cells[6].textContent.trim(),
};
}
}
return null;
}, pccId);
return pccData;
}
async function main() {
const opts = parseArgs();
const browser = await chromium.launch({ headless: !opts.headed, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, opts.project);
// 自动确定 5qi
if (opts.qi === null) {
console.log('\n📋 检测已有 QoS 模板的 5qi...');
const usedQis = await getUsed5qis(page);
opts.qi = autoSelect5qi(usedQis);
console.log(` 已使用5qi: usedQis.join(', '),自动选择 5qi=opts.qi`);
} else {
console.log(`\n📋 用户指定 5qi = opts.qi`);
}
const params = {
qosId: opts.qosId,
tcId: opts.tcId,
qi: opts.qi,
maxbrUl: opts.maxbrUl || DEFAULT_BR.maxbrUl,
maxbrDl: opts.maxbrDl || DEFAULT_BR.maxbrDl,
gbrUl: opts.gbrUl || DEFAULT_BR.gbrUl,
gbrDl: opts.gbrDl || DEFAULT_BR.gbrDl,
pccId: opts.pccId || `pcc_opts.qosId`,
name: 'sm_policy_default',
precedence: opts.precedence,
};
console.log('\n📋 最终参数:');
console.log(` qosId = params.qosId (5qi=params.qi)`);
console.log(` tcId = params.tcId`);
console.log(` maxbrUl = params.maxbrUl`);
console.log(` maxbrDl = params.maxbrDl`);
console.log(` gbrUl = params.gbrUl`);
console.log(` gbrDl = params.gbrDl`);
console.log(` pccId = params.pccId`);
// Step 1: 创建 QoS
console.log('\n📦 Step 1: 创建 QoS 模板...');
await addQos(page, params);
// Step 2: 创建 TC
console.log('\n📦 Step 2: 创建 Traffic Control 模板...');
await addTc(page, params);
// Step 3: 创建 PCC
console.log('\n📦 Step 3: 创建 PCC 规则...');
await addPcc(page, params);
// Step 4: 创建 sm_policy_default
console.log('\n📦 Step 4: 创建 sm_policy_default...');
await addSmpolicyDefault(page, params);
// Step 5: 配置 PCF default_smpolicy
console.log('\n📦 Step 5: 配置 PCF default_smpolicy...');
await configurePcfDefaultSmpolicy(page, params.name);
// 验证
console.log('\n📋 验证 PCC 规则...');
const pccResult = await verifyPcc(page, params.pccId);
if (pccResult) {
console.log(` pccRuleId = pccResult.pccRuleId`);
console.log(` precedence = pccResult.precedence`);
console.log(` refQosData = pccResult.refQosData '❌'`);
console.log(` refTcData = pccResult.refTcData '❌'`);
}
await page.goto(`globalBaseUrl/sim_5gc/smpolicy/default/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const smp = await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 6 && cells[2].textContent.trim() === 'sm_policy_default') {
return cells[4].textContent.trim();
}
}
return null;
});
console.log(`\n sm_policy_default pccRules = smp`);
console.log('\n✅ 完成!');
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/setup_pcf_default_v2.js
/**
* setup_pcf_default_v2.js - 为任意工程配置PCF默认规则(修复版)
*
* 修复内容:
* 1. TC 创建:填写必填字段 flowStatus(SELECT)
* 2. PCC 创建:正确选择所有 xm-select(refQosData, refTcData)
*
* 用法: node setup_pcf_default_v2.js --project XW_SUPF_SN4_5_2_8 [--headed]
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
function parseArgs() {
const args = process.argv.slice(2);
const opts = { project: null, headed: false };
for (let i = 0; i < args.length; i++) {
if (args[i] === '--project' || args[i] === '-p') opts.project = args[++i];
else if (args[i] === '--headed') opts.headed = true;
}
if (!opts.project) { console.error('❌ 缺少 --project'); process.exit(1); }
return opts;
}
const DEFAULT_BR = { maxbrUl: '10000000', maxbrDl: '20000000', gbrUl: '5000000', gbrDl: '5000000' };
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, name) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.locator('input[name="project_search_name"]').fill(name);
await page.keyboard.press('Enter');
await page.waitForTimeout(3000);
const clicked = await page.evaluate((n) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === n) {
cells[1].querySelector('.iconfont').click();
return true;
}
}
return false;
}, name);
if (!clicked) { console.log('❌ 选择工程失败'); process.exit(1); }
await page.waitForTimeout(3000);
console.log(`✅ 工程 "name" 已选`);
}
async function getUsed5qis(page) {
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/qos/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const qis = await page.evaluate(() => {
const set = new Set();
document.querySelectorAll('.layui-table tbody tr').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 4) {
const v = parseInt(cells[3].textContent.trim());
if (!isNaN(v)) set.add(v);
}
});
return [...set];
});
return qis;
}
function autoSelect5qi(usedQis) {
const cands = [8, 9, 6, 5, 4, 3, 2, 1];
for (const c of cands) if (!usedQis.includes(c)) return c;
return 8;
}
// ─── 创建 QoS ──────────────────────────────────────────────────────────
async function addQos(page, opts) {
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/qos/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
await page.locator('button:has-text("添加")').click();
await page.waitForTimeout(2000);
const frame = page.frame('layui-layer-iframe2');
if (!frame) { console.log('❌ 未找到弹窗iframe'); return; }
await frame.waitForTimeout(2000);
await frame.locator('input[name="qosId"]').fill(String(opts.qosId));
await frame.locator('input[name="5qi"]').fill(String(opts.qi));
await frame.locator('input[name="maxbrUl"]').fill(String(opts.maxbrUl));
await frame.locator('input[name="maxbrDl"]').fill(String(opts.maxbrDl));
await frame.locator('input[name="gbrUl"]').fill(String(opts.gbrUl));
await frame.locator('input[name="gbrDl"]').fill(String(opts.gbrDl));
await frame.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log(` ✅ QoS opts.qosId (5qi=opts.qi)`);
}
// ─── 创建 TC ──────────────────────────────────────────────────────────
async function addTc(page, tcId) {
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/trafficCtl/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
// 检查是否已存在
const exists = await page.evaluate((id) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
for (const c of cells) { if (c.textContent.trim() === id) return true; }
}
return false;
}, tcId);
if (exists) { console.log(` ℹ️ TC tcId 已存在,跳过`); return; }
await page.locator('button:has-text("添加")').click();
await page.waitForTimeout(3000);
const frame = page.frame('layui-layer-iframe2');
if (!frame) { console.log('❌ TC弹窗iframe未找到'); return; }
await frame.waitForLoadState('domcontentloaded');
await frame.waitForTimeout(2000);
// 填写 tcId
await frame.locator('input[name="tcId"]').fill(tcId);
console.log(` tcId = tcId`);
// 通过 JS 直接设置 flowStatus 的值(避免 visibility 问题)
const flowStatusSet = await frame.evaluate(() => {
const sel = document.querySelector('select[name="flowStatus"]');
if (!sel) return false;
// 选择 "ENABLED" 或第一个非空选项
const opts = Array.from(sel.options).map(o => o.value).filter(v => v && v !== '');
if (opts.length > 0) {
sel.value = opts[0];
sel.dispatchEvent(new Event('change', { bubbles: true }));
return opts[0];
}
return false;
});
console.log(` flowStatus = flowStatusSet (via JS)`);
await frame.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
// 验证
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/trafficCtl/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const verify = await page.evaluate((id) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
for (const c of cells) { if (c.textContent.trim() === id) return true; }
}
return false;
}, tcId);
console.log(` '❌' TC tcId '创建失败(请手动检查)'`);
}
// ─── 创建 PCC ──────────────────────────────────────────────────────────
async function addPcc(page, opts) {
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/pcc/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
await page.locator('button:has-text("添加")').click();
await page.waitForTimeout(3000);
await page.waitForFunction(() => window.location.href.includes('/predfPolicy/pcc/edit'), { timeout: 10000 });
await page.waitForTimeout(3000);
// 填写文本字段
await page.locator('input[name="pccRuleId"]').fill(String(opts.pccId));
const precedence = opts.precedence !== null ? String(opts.precedence) : '63';
await page.locator('input[name="precedence"]').fill(precedence);
console.log(` pccRuleId=opts.pccId, precedence=precedence`);
// 逐个处理 xm-select(用 Playwright locator 直接点击可见选项)
// xm-select[0] = refQosData, xm-select[1] = refTcData, xm-select[2] = refChgData, xm-select[3] = refUmData
// refQosData = qosId
await page.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
if (inputs[0]) inputs[0].parentElement.click();
});
await page.waitForTimeout(1000);
const qos1Visible = await page.locator('.xm-option.show-icon', { hasText: opts.qosId }).isVisible({ timeout: 3000 }).catch(() => false);
if (qos1Visible) {
await page.locator('.xm-option.show-icon', { hasText: opts.qosId }).click();
console.log(` ✅ refQosData=opts.qosId 已选`);
} else {
console.log(` ⚠️ refQosData=opts.qosId 选项不可见`);
}
await page.waitForTimeout(500);
// refTcData = tcId
await page.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
if (inputs[1]) inputs[1].parentElement.click();
});
await page.waitForTimeout(1000);
const tcVisible = await page.locator('.xm-option.show-icon', { hasText: opts.tcId }).isVisible({ timeout: 3000 }).catch(() => false);
if (tcVisible) {
await page.locator('.xm-option.show-icon', { hasText: opts.tcId }).click();
console.log(` ✅ refTcData=opts.tcId 已选`);
} else {
console.log(` ⚠️ refTcData=opts.tcId 选项不可见`);
}
await page.waitForTimeout(500);
// refChgData — 如果有可选的
await page.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
if (inputs[2]) inputs[2].parentElement.click();
});
await page.waitForTimeout(1000);
const firstChgOpt = page.locator('.xm-option.show-icon').first();
if (await firstChgOpt.isVisible({ timeout: 2000 }).catch(() => false)) {
await firstChgOpt.click();
console.log(` ℹ️ refChgData 已选第一项`);
}
await page.waitForTimeout(500);
await page.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log(` ✅ PCC opts.pccId 已提交`);
}
// ─── 创建 sm_policy_default ────────────────────────────────────────────
async function addSmpolicyDefault(page, opts) {
await page.goto(`globalBaseUrl/sim_5gc/smpolicy/default/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
await page.locator('button:has-text("添加")').click();
await page.waitForTimeout(3000);
await page.locator('iframe[name="layui-layer-iframe2"]').waitFor({ timeout: 5000 });
const frame = page.frame('layui-layer-iframe2');
await frame.waitForLoadState('domcontentloaded');
await frame.waitForTimeout(1000);
await frame.locator('input[name="name"]').fill(opts.name);
// pccRules = xm-select[1] — 用 Playwright locator 点击
await frame.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
if (inputs[1]) inputs[1].parentElement.click();
});
await page.waitForTimeout(1000);
const pccOpt = frame.locator('.xm-option.show-icon', { hasText: opts.pccId });
if (await pccOpt.isVisible({ timeout: 3000 }).catch(() => false)) {
await pccOpt.click();
console.log(` ✅ pccRules=opts.pccId`);
} else {
console.log(` ⚠️ pccRules=opts.pccId 不可见`);
}
await page.waitForTimeout(500);
await page.keyboard.press('Escape');
await page.waitForTimeout(500);
await frame.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log(` ✅ sm_policy_default 已创建`);
}
// ─── 配置 PCF default_smpolicy ──────────────────────────────────────────
async function configurePcfDefaultSmpolicy(page, smpolicyName) {
await page.goto(`globalBaseUrl/sim_5gc/pcf/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
if (rows.length > 0) {
const links = rows[0].querySelectorAll('a');
for (const l of links) { if (l.textContent.trim() === '编辑') { l.click(); return; } }
}
});
await page.waitForTimeout(3000);
await page.locator('iframe[name="layui-layer-iframe2"]').waitFor({ timeout: 5000 });
const frame = page.frame('layui-layer-iframe2');
await frame.waitForLoadState('domcontentloaded');
await frame.waitForTimeout(1000);
// default_smpolicy = xm-select[0]
await frame.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
if (inputs[0]) inputs[0].parentElement.click();
});
await page.waitForTimeout(1000);
const smpOpt = frame.locator('.xm-option.show-icon', { hasText: smpolicyName });
if (await smpOpt.isVisible({ timeout: 3000 }).catch(() => false)) {
await smpOpt.click();
console.log(` ✅ default_smpolicy=smpolicyName`);
} else {
console.log(` ⚠️ default_smpolicy=smpolicyName 不可见`);
}
await page.waitForTimeout(500);
await frame.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log(` ✅ PCF default_smpolicy = smpolicyName`);
}
// ─── 验证 ──────────────────────────────────────────────────────────────
async function verify(page, pccId, tcId) {
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/pcc/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const pccData = await page.evaluate((targetId) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
// cells[5]=refQosData, cells[6]=refTcData
if (cells.length >= 7 && cells[2].textContent.trim() === targetId) {
return {
pccRuleId: cells[2].textContent.trim(),
precedence: cells[4].textContent.trim(),
refQosData: cells[5].textContent.trim(),
refTcData: cells[6].textContent.trim(),
};
}
}
return null;
}, pccId);
return pccData;
}
async function main() {
const opts = parseArgs();
const pccId = `pcc_default`;
const tcId = `tc1`;
const qosId = `qos1`;
// 自动选5qi
const browser = await chromium.launch({ headless: !opts.headed, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, opts.project);
console.log('\n📋 检测 5qi...');
const usedQis = await getUsed5qis(page);
const qi = autoSelect5qi(usedQis);
console.log(` 已使用5qi: usedQis.join(', '),将使用 5qi=qi`);
const params = {
qosId, tcId, qi,
maxbrUl: DEFAULT_BR.maxbrUl,
maxbrDl: DEFAULT_BR.maxbrDl,
gbrUl: DEFAULT_BR.gbrUl,
gbrDl: DEFAULT_BR.gbrDl,
pccId, name: 'sm_policy_default',
precedence: null, // 默认63
};
console.log('\n📋 参数:');
console.log(` qosId=qosId, tcId=tcId, 5qi=qi`);
console.log(` maxbrUl=params.maxbrUl, maxbrDl=params.maxbrDl`);
console.log(` gbrUl=params.gbrUl, gbrDl=params.gbrDl`);
console.log('\n📦 Step 1: QoS...');
await addQos(page, params);
console.log('\n📦 Step 2: TC...');
await addTc(page, tcId);
console.log('\n📦 Step 3: PCC...');
await addPcc(page, params);
console.log('\n📦 Step 4: sm_policy_default...');
await addSmpolicyDefault(page, params);
console.log('\n📦 Step 5: PCF default_smpolicy...');
await configurePcfDefaultSmpolicy(page, params.name);
console.log('\n📋 验证 PCC 规则:');
const result = await verify(page, pccId, tcId);
if (result) {
console.log(` pccRuleId = result.pccRuleId`);
console.log(` precedence = result.precedence`);
console.log(` refQosData = result.refQosData '❌'`);
console.log(` refTcData = result.refTcData '❌'`);
} else {
console.log(' ❌ 未找到 PCC 规则');
}
console.log('\n✅ 完成!');
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/smf-pgwc-add-skill.js
#!/usr/bin/env node
/**
* SMF/PGW-C 添加脚本
* 用法: node smf-pgwc-add-skill.js <名称> [--project <工程>] [--url <地址>] [--headed] \
* [--pfcp_sip <IP>] [--http2_sip <IP>] [--mcc <值>] [--mnc <值>] \
* [--pdu_capacity <数量>] [--ue_min <IP>] [--ue_max <IP>] \
* [--interest_tac <TAC列表>]
* 示例: node smf-pgwc-add-skill.js SMF-TEST --project XW_S5GC_1
* node smf-pgwc-add-skill.js SMF-PROD --pfcp_sip 10.10.10.50 --http2_sip 10.10.10.51 --mcc 460 --mnc 01
*/
const { chromium } = require('playwright');
const fs = require('fs');
const path = require('path');
let BASE_URL = 'https://192.168.3.89';
const CONFIG = {
urls: { login: '/login', smfList: '/sim_5gc/smf/index' },
credentials: { email: '[email protected]', password: 'dotouch' },
sessionDir: path.join(__dirname, '.sessions'),
getSessionFile() {
return `5gc_smf_add_\/\//, '').replace(/\./g, '_').json`;
}
};
class SessionManager {
constructor() {
if (!fs.existsSync(CONFIG.sessionDir)) fs.mkdirSync(CONFIG.sessionDir, { recursive: true });
this.sp = path.join(CONFIG.sessionDir, CONFIG.getSessionFile());
}
async loadSession(browser) {
if (!fs.existsSync(this.sp)) return null;
try {
const { storageState } = JSON.parse(fs.readFileSync(this.sp, 'utf8'));
return await browser.newContext({ storageState, ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
} catch { return null; }
}
async saveSession(context) {
fs.writeFileSync(this.sp, JSON.stringify({ storageState: await context.storageState() }, null, 2));
}
}
// 选择工程(点击目标工程行内的 .layui-icon 图标)
async function selectProject(page, projectName) {
await page.goto(`BASE_URL/sim_5gc/project/index`, { waitUntil: 'networkidle', timeout: 15000 });
try {
await page.waitForSelector('.jsgrid-row, .jsgrid-alt-row', { timeout: 10000 });
} catch {
console.log(' ⚠ 等待 jsgrid 行超时');
}
await page.waitForTimeout(2000);
for (let pageNum = 1; pageNum <= 20; pageNum++) {
const result = await page.evaluate((target) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === target) {
if (row.classList.contains('jsgrid-selected-row')) return 'already-selected';
const controlCell = row.querySelector('.jsgrid-cell.jsgrid-control-field');
if (controlCell) {
const icon = controlCell.querySelector('.layui-icon');
if (icon) { icon.click(); return 'clicked'; }
}
row.click();
return 'clicked';
}
}
return 'not-found';
}, projectName);
if (result === 'already-selected') {
console.log(' ✓ 工程已在第一行(已选中状态),跳过');
await page.waitForTimeout(2000);
return true;
}
if (result === 'clicked') {
console.log(' ✓ 已点击工程行图标,等待切换...');
await page.waitForTimeout(3000);
return true;
}
const nextBtn = page.locator('.jsgrid-pager a').filter({ hasText: 'Next' }).first();
if (!(await nextBtn.count())) { console.log(' ⚠ 已到最后一页'); break; }
try {
await page.evaluate(() => {
var links = document.querySelectorAll('.jsgrid-pager a');
for (var i = 0; i < links.length; i++) {
if (links[i].innerText.trim() === 'Next') { links[i].click(); break; }
}
});
await page.waitForTimeout(2000);
} catch { break; }
}
console.log(` ❌ 未找到工程 "projectName"`);
return false;
}
async function main() {
const args = process.argv.slice(2);
if (args.length < 1) {
console.log('用法: node smf-pgwc-add-skill.js <名称> [--project <工程>] [--url <地址>] [--headed]');
console.log(' [--pfcp_sip <IP>] [--http2_sip <IP>] [--mcc <值>] [--mnc <值>]');
console.log(' [--pdu_capacity <数量>] [--ue_min <IP>] [--ue_max <IP>] [--interest_tac <TAC列表>]');
console.log('示例: node smf-pgwc-add-skill.js SMF-TEST --project XW_S5GC_1');
console.log(' node smf-pgwc-add-skill.js SMF-PROD --pfcp_sip 10.10.10.50 --mcc 460 --mnc 01');
process.exit(1);
}
const smfName = args[0];
let headless = true;
let url = BASE_URL;
let project = 'XW_S5GC_1';
// 可选字段
let pfcp_sip = '200.20.20.25';
let http2_sip = '200.20.20.25';
let mcc = '460';
let mnc = '01';
let pdu_capacity = '200000';
let ue_min = '30.30.30.20';
let ue_max = '30.31.30.20';
let interest_tac = '101\n102';
for (let i = 1; i < args.length; i++) {
if (args[i] === '--headed') {
headless = false;
} else if (args[i] === '--url') {
url = args[++i].startsWith('http') ? args[i] : `https://args[i]`;
} else if (args[i] === '--project') {
project = args[++i];
} else if (args[i] === '--pfcp_sip') {
pfcp_sip = args[++i];
} else if (args[i] === '--http2_sip') {
http2_sip = args[++i];
} else if (args[i] === '--mcc') {
mcc = args[++i];
} else if (args[i] === '--mnc') {
mnc = args[++i];
} else if (args[i] === '--pdu_capacity') {
pdu_capacity = args[++i];
} else if (args[i] === '--ue_min') {
ue_min = args[++i];
} else if (args[i] === '--ue_max') {
ue_max = args[++i];
} else if (args[i] === '--interest_tac') {
interest_tac = args[++i].replace(/,/g, '\n');
}
}
BASE_URL = url;
console.log(`▶ 添加 SMF: smfName`);
console.log(` 模式: '有头'`);
console.log(` 工程: project`);
console.log(` pfcp_sip=pfcp_sip http2_sip=http2_sip mcc=mcc mnc=mnc pdu_capacity=pdu_capacity`);
const sessionMgr = new SessionManager();
const browser = await chromium.launch({ headless, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const context = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await context.newPage();
// 登录
await page.goto(`BASE_URLCONFIG.urls.login`, { waitUntil: 'networkidle' });
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill(CONFIG.credentials.email);
await page.getByRole('textbox', { name: '密码' }).fill(CONFIG.credentials.password);
await page.getByRole('checkbox', { name: '记住我' }).check();
await page.getByRole('button', { name: '登录' }).click();
await page.waitForLoadState('networkidle');
await sessionMgr.saveSession(context);
console.log(' ✓ 登录成功');
// 选工程
const ok = await selectProject(page, project);
if (!ok) throw new Error('工程选择失败');
console.log(' ✓ 工程已选');
// 进 SMF 列表
await page.goto(`BASE_URLCONFIG.urls.smfList`, { waitUntil: 'networkidle' });
await page.waitForTimeout(2000);
// 点添加按钮
await page.locator('button:has-text("添加"), a:has-text("添加")').first().click();
await page.waitForTimeout(2000);
console.log(' ✓ 进入添加页面');
// ===== 填写表单 =====
// 1. 名称
await page.locator('input[name="name"]').fill(smfName);
console.log(` ✓ name = smfName`);
// 2. N4/Sxb起始IP(必填)
await page.locator('input[name="pfcp_sip"]').fill(pfcp_sip);
console.log(` ✓ pfcp_sip = pfcp_sip`);
// 3. SBI起始地址
await page.locator('input[name="http2_sip"]').fill(http2_sip);
console.log(` ✓ http2_sip = http2_sip`);
// 4. 选择类型:仿真设备
await page.getByRole('textbox', { name: '请选择' }).first().click();
await page.waitForTimeout(300);
await page.getByRole('definition').filter({ hasText: '仿真设备' }).click();
await page.waitForTimeout(300);
console.log(' ✓ 类型 = 仿真设备');
// 5. 上行隧道信息分配:UPF
await page.getByRole('textbox', { name: '请选择' }).nth(1).click();
await page.waitForTimeout(300);
await page.getByRole('definition').filter({ hasText: 'UPF' }).click();
await page.waitForTimeout(300);
console.log(' ✓ 上行隧道 = UPF');
// 6. UE IP分配:SMF
await page.getByRole('textbox', { name: '请选择' }).nth(2).click();
await page.waitForTimeout(300);
await page.getByRole('definition').filter({ hasText: 'SMF' }).click();
await page.waitForTimeout(300);
console.log(' ✓ UE IP分配 = SMF');
// 7. UE IPv4池
await page.locator('input[name="ue_min"]').fill(ue_min);
await page.locator('input[name="ue_max"]').fill(ue_max);
console.log(` ✓ ue_min = ue_min, ue_max = ue_max`);
// 8. UE IPv6池
await page.locator('input[name="ue_sip6"]').fill('1:2:3:1::1');
await page.locator('input[name="ue_eip6"]').fill('1:2:3:ffff::1');
console.log(' ✓ ue_sip6 = 1:2:3:1::1, ue_eip6 = 1:2:3:ffff::1');
// 9. MCC/MNC
await page.getByRole('textbox', { name: '三位数字', exact: true }).fill(mcc);
await page.getByRole('textbox', { name: '二位或三位数字' }).fill(mnc);
console.log(` ✓ MCC = mcc, MNC = mnc`);
// 10. PDU容量
await page.locator('input[name="pdu_capacity"]').fill(pdu_capacity);
console.log(` ✓ pdu_capacity = pdu_capacity`);
// 11. TAC列表
await page.locator('textarea[name="interest_tac"]').fill(interest_tac);
console.log(` ✓ interest_tac = interest_tac.replace(/\n/g, ',')`);
console.log(' ✓ 表单填写完成(基础信息)');
// NSSAI 弹窗关闭后,删除遮罩层
await page.evaluate(() => {
document.querySelectorAll('.layui-layer-shade').forEach(el => el.remove());
});
await page.waitForTimeout(500);
// 提交 SMF 表单(先不加 NSSAI,等 SMF 创建后再加)
// 注意:这里直接用 layui 原生提交,因为手动 fetch 提交无法跟随重定向
console.log(' -> 第1步:先提交 SMF(不含 NSSAI)...');
await page.getByRole('button', { name: '提交' }).click();
await page.waitForTimeout(4000);
if (!page.url().includes('smf/index')) {
// 如果提交后没到列表页(可能是 422),报告错误
console.log(' ⚠ SMF 提交未跳转到列表,当前 URL:', page.url());
// 尝试从 URL 提取 SMF ID
const smfIdFromUrl = await page.evaluate(() => {
const m = window.location.href.match(/\/smf\/edit\/(\d+)/);
return m ? m[1] : null;
});
if (smfIdFromUrl) {
console.log(' SMF ID from URL:', smfIdFromUrl, '- 继续尝试添加 NSSAI...');
} else {
await browser.close();
console.log(' ❌ SMF 创建失败');
process.exit(1);
}
} else {
console.log(' ✅ SMF 基本信息已创建');
}
// 第2步:重新打开刚创建的 SMF 的编辑页,添加 NSSAI
// jsgrid 返回 JSON 格式数据(不是 HTML),通过 jQuery.post 获取
const newSmfId = await page.evaluate(async (createdName) => {
const csrf = jQuery('meta[name=csrf-token]').attr('content');
const json = await new Promise((resolve) => {
jQuery.post('/sim_5gc/smf/index', {
page: 1, pageSize: 100, model: 'SmfInfo', _token: csrf
}, resolve, 'json');
});
if (json && json.data) {
// 找刚创建的 SMF(名称匹配且 ID 最大)
const matches = json.data.filter(s => s.name === createdName);
if (matches.length > 0) return matches[0].id;
// 否则取最新 ID
const maxId = Math.max(...json.data.map(s => s.id));
return maxId;
}
return null;
}, smfName);
if (!newSmfId) {
console.log(' ⚠ 无法获取新 SMF ID,跳过 NSSAI 配置');
} else {
console.log(' -> 第2步:打开 SMF ID=' + newSmfId + ' 的编辑页添加 NSSAI...');
await page.goto(BASE_URL + '/sim_5gc/smf/edit/' + newSmfId, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(4000);
// 在编辑页添加 NSSAI
await page.locator('button.nssaiAdd').click();
await page.waitForTimeout(2000);
const nssaiCell2 = page.locator('table.layui-table tbody tr td:nth-child(2)');
if (await nssaiCell2.isVisible({ timeout: 3000 }).catch(() => false)) {
await nssaiCell2.click();
await page.waitForTimeout(2000);
const nssaiFL2 = page.frameLocator('#layui-layer-iframe2');
await page.waitForTimeout(2000);
await nssaiFL2.locator('button').first().click();
await page.waitForTimeout(1500);
await nssaiFL2.locator('input.sst').fill('1');
await nssaiFL2.locator('input.sd').fill('000001');
await page.waitForTimeout(500);
await nssaiFL2.locator('input.xm-select-default').first().locator('..').click();
await page.waitForTimeout(1000);
const dnnOpt2 = nssaiFL2.locator('.xm-option.show-icon').first();
if (await dnnOpt2.isVisible({ timeout: 3000 }).catch(() => false)) {
await dnnOpt2.click();
}
await page.waitForTimeout(500);
await page.keyboard.press('Escape');
await page.waitForTimeout(300);
await nssaiFL2.locator('button:has-text("提交")').click();
await page.waitForTimeout(2000);
console.log(' ✅ NSSAI 配置完成(SST=1, SD=000001)');
// 关闭弹窗遮罩
await page.evaluate(() => { document.querySelectorAll('.layui-layer-shade,.layui-layer').forEach(el => el.remove()); });
await page.waitForTimeout(500);
// 关键:nssaiAdd 创建的行包含 TAC inputs(config[count][]/stac[]/etac[])但为空,
// 需填入有效值否则服务器 422。从 interest_tac 解析 TAC 列表来填充。
const tacVals = await page.evaluate(() => {
const ta = document.querySelector('textarea[name="interest_tac"]');
if (!ta) return [];
return ta.value.split(/[\r\n]+/).map(t => t.trim()).filter(Boolean);
});
await page.evaluate((tacs) => {
const rows = document.querySelectorAll('table.layui-table tbody tr');
rows.forEach(row => {
const countEl = row.querySelector('input[name="config[count][]"]');
const stacEl = row.querySelector('input[name="config[stac][]"]');
const etacEl = row.querySelector('input[name="config[etac][]"]');
if (countEl && !countEl.value) {
countEl.value = tacs.length > 0 ? tacs.length : 1;
countEl.dispatchEvent(new Event('input', {bubbles: true}));
}
if (stacEl && !stacEl.value) {
stacEl.value = tacs[0] || '101';
stacEl.dispatchEvent(new Event('input', {bubbles: true}));
}
if (etacEl && !etacEl.value) {
etacEl.value = tacs.length > 1 ? tacs[tacs.length-1] : (tacs[0] || '101');
etacEl.dispatchEvent(new Event('input', {bubbles: true}));
}
});
}, tacVals);
await page.waitForTimeout(500);
// 保存编辑页
await page.getByRole('button', { name: '提交' }).first().click();
await page.waitForTimeout(4000);
} else {
console.log(' ⚠ NSSAI 配置行未找到,跳过');
}
}
console.log(' 最终 URL:', page.url());
const success = page.url().includes('smf/index');
console.log(success ? ' ✅ 添加成功' : ' ⚠ 可能未保存,请检查页面(可加 --headed 查看)');
await browser.close();
}
main().catch(e => { console.error('异常:', e.message); process.exit(1); });
FILE:scripts/smf-pgwc-edit-skill.js
#!/usr/bin/env node
/**
* SMF/PGW-C 编辑脚本(单条 + 批量二合一)
* 用法: node smf-pgwc-edit-skill.js [--project 工程] [--headed] --set-<字段名> <值>
* 示例: node smf-pgwc-edit-skill.js --project XW_S5GC_basic --set-http2_sip 200.20.20.99
* node smf-pgwc-edit-skill.js --project XW_S5GC_basic --set-http2_sip 200.20.20.99 --set-pfcp_sip 200.20.20.88 --headed
*/
const { chromium } = require('playwright');
const fs = require('fs');
const path = require('path');
let BASE_URL = 'https://192.168.3.89';
const CONFIG = {
urls: { login: '/login', smfList: '/sim_5gc/smf/index' },
credentials: { email: '[email protected]', password: 'dotouch' },
sessionDir: path.join(__dirname, '.sessions'),
getSessionFile() {
return `5gc_smf_bulk_\/\//, '').replace(/\./g, '_').json`;
}
};
class SessionManager {
constructor() {
if (!fs.existsSync(CONFIG.sessionDir)) fs.mkdirSync(CONFIG.sessionDir, { recursive: true });
this.sp = path.join(CONFIG.sessionDir, CONFIG.getSessionFile());
}
async loadSession(browser) {
if (!fs.existsSync(this.sp)) return null;
try {
const { storageState } = JSON.parse(fs.readFileSync(this.sp, 'utf8'));
return await browser.newContext({ storageState, ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
} catch { return null; }
}
async saveSession(context) {
fs.writeFileSync(this.sp, JSON.stringify({ storageState: await context.storageState() }, null, 2));
}
}
// 选择工程
async function selectProject(page, projectName) {
await page.goto(`BASE_URL/sim_5gc/project/index`, { waitUntil: 'networkidle', timeout: 15000 });
try {
await page.waitForSelector('.jsgrid-row, .jsgrid-alt-row', { timeout: 10000 });
} catch {
console.log(' ⚠ 等待 jsgrid 行超时');
}
await page.waitForTimeout(2000);
for (let pageNum = 1; pageNum <= 20; pageNum++) {
const result = await page.evaluate((target) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
// 遍历所有行的工程名,找到目标工程
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === target) {
// 只有同时具备 'jsgrid-selected-row' class 才认为已选中
if (row.classList.contains('jsgrid-selected-row')) {
return 'already-selected';
}
// 未选中,则尝试点击该行的控制按钮(图标)进行切换
const controlCell = row.querySelector('.jsgrid-cell.jsgrid-control-field');
if (controlCell) {
const icon = controlCell.querySelector('.layui-icon');
if (icon) { icon.click(); return 'clicked'; }
}
// 若没有控制按钮,直接点击行
row.click();
return 'clicked';
}
}
return 'not-found';
}, projectName);
if (result === 'already-selected') {
console.log(' ✓ 工程已在第一行(已选中状态),跳过');
await page.waitForTimeout(2000);
return true;
}
if (result === 'clicked') {
console.log(' ✓ 已点击工程行图标,等待切换...');
await page.waitForTimeout(3000);
return true;
}
const nextBtn = page.locator('.jsgrid-pager a').filter({ hasText: 'Next' }).first();
if (!(await nextBtn.count())) { console.log(' ⚠ 已到最后一页'); break; }
try {
await nextBtn.click({ force: true });
await page.waitForTimeout(2000);
} catch { break; }
}
console.log(` ❌ 未找到工程 "projectName"`);
return false;
}
// 获取 SMF 列表所有 ID(去重)
async function fetchAllSmfIds(page, nameFilter = null) {
return await page.evaluate((filter) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
const ids = [];
const seen = new Set();
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3) {
const id = cells[1].textContent.trim();
const name = cells[2].textContent.trim();
if (id && !seen.has(id) && (!filter || name === filter)) {
seen.add(id);
ids.push(id);
}
}
}
return ids;
}, nameFilter);
}
// 编辑单个 SMF
async function editSingleSmf(page, smfId, edits) {
const editUrl = `BASE_URL/sim_5gc/smf/edit/smfId`;
await page.goto(editUrl, { waitUntil: 'networkidle' });
await page.waitForTimeout(2000);
// 只修改用户指定的字段
for (const [field, val] of Object.entries(edits)) {
const selector = `input[name="field"]`;
try {
await page.waitForSelector(selector, { state: 'visible', timeout: 5000 });
await page.click(selector, { force: true });
await page.fill(selector, val);
console.log(` ✓ field → val`);
} catch {
console.log(` ⚠ 字段 field 未找到或无法填写`);
}
}
console.log(' ✓ 字段修改完成');
// 提交
await page.getByRole('button', { name: '提交' }).click();
await page.waitForTimeout(3000);
const success = page.url().includes('smf/index');
if (!success) console.log(' ⚠ 提交后未返回列表,可能失败');
return success;
}
async function main() {
const args = process.argv.slice(2);
if (args.length < 1) {
console.log('用法: node smf-pgwc-edit-skill.js [--project 工程] [--headed] --set-<字段名> <值>');
console.log('示例: node smf-pgwc-edit-skill.js --project XW_S5GC_basic --set-http2_sip 200.20.20.99');
console.log(' node smf-pgwc-edit-skill.js --project XW_S5GC_basic --set-http2_sip 200.20.20.99 --set-pfcp_sip 200.20.20.88 --headed');
console.log('');
console.log('支持的字段: http2_sip, http2_port, pfcp_sip, pfcp_port, ue_min, ue_max, pdu_capacity, mcc, mnc');
process.exit(1);
}
let projectName = 'XW_S5GC_1';
let nameFilter = null;
let headless = true;
const edits = {};
for (let i = 0; i < args.length; i++) {
if (args[i] === '--project') {
projectName = args[++i];
} else if (args[i] === '--name') {
nameFilter = args[++i];
} else if (args[i] === '--headed') {
headless = false;
} else if (args[i].startsWith('--set-')) {
const field = args[i].slice(6);
edits[field] = args[++i];
} else if (!args[i].startsWith('-')) {
if (!nameFilter) nameFilter = args[i];
}
}
if (Object.keys(edits).length === 0) {
console.log('用法: node smf-pgwc-edit-skill.js [--project 工程] [--name <名称>] [--headed] --set-<字段名> <值>');
console.log('示例: node smf-pgwc-edit-skill.js --project XW_S5GC_1 --name SMF001 --set-pfcp_ip 10.0.0.5');
process.exit(1);
}
console.log(`▶ 批量编辑 SMF,工程: projectName' + nameFilter : ''`);
console.log(` 修改字段:`, edits);
console.log(` 模式: '有头'`);
const sessionMgr = new SessionManager();
const browser = await chromium.launch({ headless, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const context = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await context.newPage();
// 登录
await page.goto(`BASE_URLCONFIG.urls.login`, { waitUntil: 'networkidle' });
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill(CONFIG.credentials.email);
await page.getByRole('textbox', { name: '密码' }).fill(CONFIG.credentials.password);
await page.getByRole('checkbox', { name: '记住我' }).check();
await page.getByRole('button', { name: '登录' }).click();
await page.waitForLoadState('networkidle');
await sessionMgr.saveSession(context);
console.log(' ✓ 登录成功');
// 选工程
const ok = await selectProject(page, projectName);
if (!ok) throw new Error('工程选择失败');
console.log(' ✓ 工程已选');
// 进 SMF 列表
await page.goto(`BASE_URLCONFIG.urls.smfList`, { waitUntil: 'networkidle' });
await page.waitForTimeout(2000);
// 获取所有 SMF ID
const smfIds = await fetchAllSmfIds(page, nameFilter);
if (smfIds.length === 0) {
console.log(' ⚠ 工程下没有 SMF');
await browser.close();
return;
}
console.log(` found smfIds.length SMF(s) in project "projectName"`);
// 逐个编辑
let successCount = 0;
for (let i = 0; i < smfIds.length; i++) {
const smfId = smfIds[i];
console.log(`\n[i + 1/smfIds.length] 编辑 SMF ID: smfId`);
const ok = await editSingleSmf(page, smfId, edits);
if (ok) successCount++;
}
console.log(`\n=== 完成 ===`);
console.log(`成功编辑 successCount / smfIds.length 个 SMF`);
await browser.close();
}
main().catch(e => { console.error('异常:', e.message); process.exit(1); });
FILE:scripts/smpolicy-cell-add-skill.js
/**
* smpolicy-cell-add-skill.js - Cell Smpolicy 添加技能
*
* 用法:
* node smpolicy-cell-add-skill.js --project XW_SUPF_5_1_2_4 --name cell_policy_test
* --node-id 0x00001 --node-id-len 28 --cell-id 0x00001 [--headed]
*
* 参数:
* --project 工程名(默认 XW_S5GC_1)
* --name 策略名称(必填)
* --node-id Node ID(必填)
* --node-id-len Node ID Len(默认 28)
* --cell-id Cell ID(必填)
* --headed 显示浏览器
*
* 添加页:/sim_5gc/smpolicy/cell/edit(iframe弹窗)
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
function parseArgs() {
const args = process.argv.slice(2);
const opts = { project: 'XW_S5GC_1', name: null, nodeId: null, nodeIdLen: '28', cellId: null, headed: false };
for (let i = 0; i < args.length; i++) {
if (args[i] === '--project' || args[i] === '-p') opts.project = args[++i];
else if (args[i] === '--name') opts.name = args[++i];
else if (args[i] === '--node-id') opts.nodeId = args[++i];
else if (args[i] === '--node-id-len') opts.nodeIdLen = args[++i];
else if (args[i] === '--cell-id') opts.cellId = args[++i];
else if (args[i] === '--headed') opts.headed = true;
}
if (!opts.name || !opts.nodeId || !opts.cellId) { console.error('❌ 缺少必要参数 (--name, --node-id, --cell-id)'); process.exit(1); }
return opts;
}
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, projectName) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
const clicked = await page.evaluate((name) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === name) {
const icon = cells[1].querySelector('.iconfont');
if (icon) { icon.click(); return true; }
}
}
return false;
}, projectName);
if (!clicked) {
for (let p = 2; p <= 10; p++) {
const nextBtn = document.querySelector('.jsgrid-pager a.jsgrid-pager-next');
if (!nextBtn) break;
nextBtn.click();
await new Promise(r => setTimeout(r, 1000));
const found = page.evaluate((n) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === n) {
const icon = cells[1].querySelector('.iconfont');
if (icon) { icon.click(); return true; }
}
}
return false;
}, projectName);
if (found) break;
}
}
await page.waitForTimeout(3000);
console.log(`✅ 工程 "projectName" 已选`);
}
async function openAddDialog(page, listUrl) {
await page.goto(listUrl, { waitUntil: 'commit', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.evaluate(() => {
const btns = Array.from(document.querySelectorAll('button'));
for (const b of btns) { if (b.textContent.trim().includes('添加')) { b.click(); return; } }
});
await page.waitForFunction(() => Array.from(document.querySelectorAll('iframe')).some(f => f.src && f.src.includes('/edit')), { timeout: 10000 });
await page.waitForTimeout(2000);
const iframeSrc = await page.evaluate(() => {
const iframes = Array.from(document.querySelectorAll('iframe'));
for (const f of iframes) { if (f.src && f.src.includes('/edit')) return f.src; }
return null;
});
console.log('✅ 弹窗已打开:', iframeSrc);
await page.waitForTimeout(2000);
const frame = page.frames().find(f => f.url().includes('/edit'));
if (!frame) throw new Error('无法获取iframe');
await new Promise(r => setTimeout(r, 2000));
return frame;
}
async function main() {
const opts = parseArgs();
const browser = await chromium.launch({ headless: !opts.headed, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, opts.project);
const frame = await openAddDialog(page, `globalBaseUrl/sim_5gc/smpolicy/cell/index`);
const fields = [
{ name: 'name', value: opts.name },
{ name: 'node_id', value: opts.nodeId },
{ name: 'node_id_len', value: opts.nodeIdLen },
{ name: 'cell_id', value: opts.cellId },
];
for (const f of fields) {
const loc = frame.locator(`[name="f.name"]`);
if (await loc.count() > 0) { await loc.fill(String(f.value)); console.log(` ✅ f.name = "f.value"`); }
}
const submitBtn = frame.locator('button:has-text("提交"), button:has-text("保存")');
await submitBtn.click();
console.log(' ✅ 已提交');
await new Promise(r => setTimeout(r, 3000));
console.log('\n✅ Cell Smpolicy 添加完成');
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/smpolicy-cell-forbidden-add-skill.js
/**
* smpolicy-cell-forbidden-add-skill.js - Cell Forbidden Smpolicy 添加技能
*
* 用法:
* node smpolicy-cell-forbidden-add-skill.js --project XW_SUPF_5_1_2_4 --name cell_forb_test
* --node-id 0x00001 --node-id-len 28 --cell-id 0x00002 [--headed]
*
* 参数:
* --project 工程名(默认 XW_S5GC_1)
* --name 策略名称(必填)
* --node-id Node ID(必填)
* --node-id-len Node ID Len(默认 28)
* --cell-id Cell ID(必填)
* --headed 显示浏览器
*
* 添加页:/sim_5gc/smpolicy/cell_forbidden/edit(iframe弹窗)
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
function parseArgs() {
const args = process.argv.slice(2);
const opts = { project: 'XW_S5GC_1', name: null, nodeId: null, nodeIdLen: '28', cellId: null, headed: false };
for (let i = 0; i < args.length; i++) {
if (args[i] === '--project' || args[i] === '-p') opts.project = args[++i];
else if (args[i] === '--name') opts.name = args[++i];
else if (args[i] === '--node-id') opts.nodeId = args[++i];
else if (args[i] === '--node-id-len') opts.nodeIdLen = args[++i];
else if (args[i] === '--cell-id') opts.cellId = args[++i];
else if (args[i] === '--headed') opts.headed = true;
}
if (!opts.name || !opts.nodeId || !opts.cellId) { console.error('❌ 缺少必要参数 (--name, --node-id, --cell-id)'); process.exit(1); }
return opts;
}
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, projectName) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
const clicked = await page.evaluate((name) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === name) {
const icon = cells[1].querySelector('.iconfont');
if (icon) { icon.click(); return true; }
}
}
return false;
}, projectName);
if (!clicked) {
for (let p = 2; p <= 10; p++) {
const nextBtn = document.querySelector('.jsgrid-pager a.jsgrid-pager-next');
if (!nextBtn) break;
nextBtn.click();
await new Promise(r => setTimeout(r, 1000));
const found = page.evaluate((n) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === n) {
const icon = cells[1].querySelector('.iconfont');
if (icon) { icon.click(); return true; }
}
}
return false;
}, projectName);
if (found) break;
}
}
await page.waitForTimeout(3000);
console.log(`✅ 工程 "projectName" 已选`);
}
async function openAddDialog(page, listUrl) {
await page.goto(listUrl, { waitUntil: 'commit', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.evaluate(() => {
const btns = Array.from(document.querySelectorAll('button'));
for (const b of btns) { if (b.textContent.trim().includes('添加')) { b.click(); return; } }
});
await page.waitForFunction(() => Array.from(document.querySelectorAll('iframe')).some(f => f.src && f.src.includes('/edit')), { timeout: 10000 });
await page.waitForTimeout(2000);
const iframeSrc = await page.evaluate(() => {
const iframes = Array.from(document.querySelectorAll('iframe'));
for (const f of iframes) { if (f.src && f.src.includes('/edit')) return f.src; }
return null;
});
console.log('✅ 弹窗已打开:', iframeSrc);
await page.waitForTimeout(2000);
const frame = page.frames().find(f => f.url().includes('/edit'));
if (!frame) throw new Error('无法获取iframe');
await new Promise(r => setTimeout(r, 2000));
return frame;
}
async function main() {
const opts = parseArgs();
const browser = await chromium.launch({ headless: !opts.headed, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, opts.project);
const frame = await openAddDialog(page, `globalBaseUrl/sim_5gc/smpolicy/cell_forbidden/index`);
const fields = [
{ name: 'name', value: opts.name },
{ name: 'node_id', value: opts.nodeId },
{ name: 'node_id_len', value: opts.nodeIdLen },
{ name: 'cell_id', value: opts.cellId },
];
for (const f of fields) {
const loc = frame.locator(`[name="f.name"]`);
if (await loc.count() > 0) { await loc.fill(String(f.value)); console.log(` ✅ f.name = "f.value"`); }
}
const submitBtn = frame.locator('button:has-text("提交"), button:has-text("保存")');
await submitBtn.click();
console.log(' ✅ 已提交');
await new Promise(r => setTimeout(r, 3000));
console.log('\n✅ Cell Forbidden Smpolicy 添加完成');
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/smpolicy-dnn-add-skill.js
/**
* smpolicy-dnn-add-skill.js - DNN Smpolicy 添加工具
*
* 用法:
* node smpolicy-dnn-add-skill.js --project XW_SUPF_5_1_2_4 --name dnn_policy1 --dnn internet
* node smpolicy-dnn-add-skill.js --project XW_SUPF_5_1_2_4 --name dnn_policy1 --dnn internet --pcc-rules pcc2
*
* 参数:
* --project 工程名(默认 XW_S5GC_1)
* --name DNN策略名称(必填)
* --dnn DNN值(必填)
* --sst sNssai SST(默认 1)
* --sd sNssai SD(默认 111111)
* --sess-rules 会话规则(xm-select,多个逗号分隔)
* --pcc-rules PCC规则(xm-select,多个逗号分隔)
* --pra-rules PRA规则(xm-select,可选)
* --ref-qos-timer reflectiveQoSTimer 值(秒)
* --headed 显示浏览器窗口
*
* 添加页:/sim_5gc/smpolicy/dnn/edit(layui-layer-iframe2)
* xm-select: sessRules=idx0, pccRules=idx1, praRules=idx2
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
function parseArgs() {
const args = process.argv.slice(2);
const opts = {
project: 'XW_S5GC_1',
name: null, dnn: null,
sst: '1', sd: '111111',
sessRules: null, pccRules: null, praRules: null,
refQosTimer: null,
headed: false,
};
for (let i = 0; i < args.length; i++) {
if (args[i] === '--project' || args[i] === '-p') opts.project = args[++i];
else if (args[i] === '--name') opts.name = args[++i];
else if (args[i] === '--dnn') opts.dnn = args[++i];
else if (args[i] === '--sst') opts.sst = args[++i];
else if (args[i] === '--sd') opts.sd = args[++i];
else if (args[i] === '--sess-rules') opts.sessRules = args[++i];
else if (args[i] === '--pcc-rules') opts.pccRules = args[++i];
else if (args[i] === '--pra-rules') opts.praRules = args[++i];
else if (args[i] === '--ref-qos-timer') opts.refQosTimer = args[++i];
else if (args[i] === '--headed') opts.headed = true;
}
if (!opts.name) { console.error('❌ 缺少 --name 参数'); process.exit(1); }
if (!opts.dnn) { console.error('❌ 缺少 --dnn 参数'); process.exit(1); }
return opts;
}
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, name) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.locator('input[name="project_search_name"]').fill(name);
await page.keyboard.press('Enter');
await page.waitForTimeout(3000);
const found = await page.evaluate((n) => {
let result = false;
document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === n) {
cells[1].querySelector('.iconfont')?.click();
result = true;
}
});
return result;
}, name);
if (!found) { console.error(`❌ 未找到工程: name`); process.exit(1); }
await page.waitForTimeout(3000);
console.log(`✅ 工程 "name" 已选`);
}
/**
* 在 xm-select 下拉中选择一个选项(点击切换)
*/
async function xmSelectChooseOne(target, page, index, value) {
if (!value) return;
await target.evaluate((idx) => {
const inputs = document.querySelectorAll('input.xm-select-default');
if (inputs[idx]) inputs[idx].parentElement.click();
}, index);
await page.waitForTimeout(1000);
const clicked = await target.evaluate((text) => {
const opts = document.querySelectorAll('.xm-option');
for (const opt of opts) {
if (opt.textContent.trim() === text) {
opt.click();
return true;
}
}
return false;
}, value);
if (clicked) {
console.log(` ✅ xm-select[index] = value`);
} else {
console.log(` ⚠️ xm-select[index] 未找到: value`);
}
await page.waitForTimeout(500);
await page.keyboard.press('Escape');
await page.waitForTimeout(300);
}
async function xmSelectChooseMultiple(target, page, index, values) {
if (!values) return;
for (const item of values.split(',').map(s => s.trim()).filter(Boolean)) {
await xmSelectChooseOne(target, page, index, item);
}
}
async function main() {
const opts = parseArgs();
const browser = await chromium.launch({ headless: !opts.headed, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, opts.project);
// 导航到 DNN smpolicy 列表
await page.goto(`globalBaseUrl/sim_5gc/smpolicy/dnn/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
console.log('✅ 到达 DNN Smpolicy 列表页');
// 添加弹窗
await page.locator('button:has-text("添加")').click();
await page.waitForTimeout(3000);
const frame = page.frame('layui-layer-iframe2');
if (!frame) { console.error('❌ 未找到弹窗iframe'); process.exit(1); }
await frame.waitForLoadState('domcontentloaded');
await page.waitForTimeout(2000);
console.log(`✅ 进入弹窗iframe: frame.url()`);
// ① 填写文本字段
const textFields = [
{ name: 'name', value: opts.name },
{ name: 'dnn', value: opts.dnn },
{ name: 'sNssai[sst]', value: opts.sst },
{ name: 'sNssai[sd]', value: opts.sd },
];
if (opts.refQosTimer) {
textFields.push({ name: 'smPolicyDecision[reflectiveQoSTimer]', value: opts.refQosTimer });
}
for (const f of textFields) {
const loc = frame.locator(`[name="f.name"]`).first();
if (await loc.count() > 0) {
await loc.fill(String(f.value));
console.log(` ✅ f.name = "f.value"`);
}
}
// ② xm-select(sessRules=idx0, pccRules=idx1, praRules=idx2)
const sessDisplay = await frame.evaluate(() => document.querySelectorAll('input.xm-select-default')[0]?.parentElement?.textContent || '');
if (!sessDisplay.includes('暂无数据')) {
await xmSelectChooseMultiple(frame, page, 0, opts.sessRules);
}
await xmSelectChooseMultiple(frame, page, 1, opts.pccRules);
const praDisplay = await frame.evaluate(() => document.querySelectorAll('input.xm-select-default')[2]?.parentElement?.textContent || '');
if (!praDisplay.includes('暂无数据')) {
await xmSelectChooseMultiple(frame, page, 2, opts.praRules);
}
// ③ 提交
await frame.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log('✅ 已提交');
// ④ 验证
await page.goto(`globalBaseUrl/sim_5gc/smpolicy/dnn/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const added = await page.evaluate((targetName) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 8 && cells[2].textContent.trim() === targetName) {
return {
name: cells[2].textContent.trim(),
dnn: cells[3].textContent.trim(),
sst: cells[4].textContent.trim(),
sd: cells[5].textContent.trim(),
pccRules: cells[7].textContent.trim(),
};
}
}
return null;
}, opts.name);
if (added) {
console.log('\n📋 验证结果:');
console.log(` name = added.name '❌'`);
console.log(` dnn = added.dnn '❌'`);
console.log(` sst/sd = added.sst/added.sd`);
console.log(` pccRules = added.pccRules`);
} else {
console.log('\n❌ 未在列表中找到创建的 DNN Smpolicy');
}
console.log('\n✅ 完成');
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/smpolicy-dnn-edit-skill.js
/**
* smpolicy-dnn-edit-skill.js - DNN Smpolicy 编辑工具
*
* 用法:
* node smpolicy-dnn-edit-skill.js --project XW_SUPF_5_1_2_4 --name dnn_policy1 --dnn internet_new
* node smpolicy-dnn-edit-skill.js --project XW_SUPF_5_1_2_4 --name dnn_policy1 --pcc-rules pcc2,pcc_default
*
* 参数:
* --project 工程名(默认 XW_S5GC_1)
* --name DNN策略名称(精确匹配,要编辑的策略)
* --dnn 新 DNN 值(可选)
* --sst 新 sNssai SST(可选)
* --sd 新 sNssai SD(可选)
* --sess-rules 会话规则(xm-select,多个逗号分隔)
* --pcc-rules PCC规则(xm-select,多个逗号分隔)
* --pra-rules PRA规则(xm-select,可选)
* --ref-qos-timer reflectiveQoSTimer(秒)
* --headed 显示浏览器窗口
*
* 编辑页:/sim_5gc/smpolicy/dnn/edit/{id}(直接渲染在主页面,非 iframe)
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
function parseArgs() {
const args = process.argv.slice(2);
const opts = {
project: 'XW_S5GC_1',
name: null,
dnn: null, sst: null, sd: null,
sessRules: null, pccRules: null, praRules: null,
refQosTimer: null,
headed: false,
};
for (let i = 0; i < args.length; i++) {
if (args[i] === '--project' || args[i] === '-p') opts.project = args[++i];
else if (args[i] === '--name') opts.name = args[++i];
else if (args[i] === '--dnn') opts.dnn = args[++i];
else if (args[i] === '--sst') opts.sst = args[++i];
else if (args[i] === '--sd') opts.sd = args[++i];
else if (args[i] === '--sess-rules') opts.sessRules = args[++i];
else if (args[i] === '--pcc-rules') opts.pccRules = args[++i];
else if (args[i] === '--pra-rules') opts.praRules = args[++i];
else if (args[i] === '--ref-qos-timer') opts.refQosTimer = args[++i];
else if (args[i] === '--headed') opts.headed = true;
}
if (!opts.name) { console.error('❌ 缺少 --name 参数'); process.exit(1); }
return opts;
}
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, name) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.locator('input[name="project_search_name"]').fill(name);
await page.keyboard.press('Enter');
await page.waitForTimeout(3000);
const found = await page.evaluate((n) => {
let result = false;
document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === n) {
cells[1].querySelector('.iconfont')?.click();
result = true;
}
});
return result;
}, name);
if (!found) { console.error(`❌ 未找到工程: name`); process.exit(1); }
await page.waitForTimeout(3000);
console.log(`✅ 工程 "name" 已选`);
}
async function xmSelectChooseOne(target, page, index, value) {
if (!value) return;
await target.evaluate((idx) => {
const inputs = document.querySelectorAll('input.xm-select-default');
if (inputs[idx]) inputs[idx].parentElement.click();
}, index);
await page.waitForTimeout(1000);
const clicked = await target.evaluate((text) => {
const opts = document.querySelectorAll('.xm-option');
for (const opt of opts) {
if (opt.textContent.trim() === text) {
opt.click();
return true;
}
}
return false;
}, value);
if (clicked) {
console.log(` ✅ xm-select[index] += value`);
} else {
console.log(` ⚠️ xm-select[index] 未找到: value`);
}
await page.waitForTimeout(500);
await page.keyboard.press('Escape');
await page.waitForTimeout(300);
}
async function main() {
const opts = parseArgs();
const browser = await chromium.launch({ headless: !opts.headed, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, opts.project);
// ① 去列表页找数字 ID
await page.goto(`globalBaseUrl/sim_5gc/smpolicy/dnn/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const dnnInfo = await page.evaluate((targetName) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 8 && cells[2].textContent.trim() === targetName) {
return { id: cells[1].textContent.trim(), name: cells[2].textContent.trim() };
}
}
return null;
}, opts.name);
if (!dnnInfo) {
console.error(`❌ 未找到 DNN Smpolicy: opts.name`);
process.exit(1);
}
console.log(`✅ 找到 DNN Smpolicy "opts.name",数字ID=dnnInfo.id`);
// ② 直接导航到编辑页(主页面渲染,无 iframe)
await page.goto(`globalBaseUrl/sim_5gc/smpolicy/dnn/edit/dnnInfo.id`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
console.log(`✅ 进入编辑页面`);
// ③ 修改文本字段(只处理提供的字段)
const textFields = [
{ name: 'name', value: opts.name },
{ name: 'dnn', value: opts.dnn },
{ name: 'sNssai[sst]', value: opts.sst },
{ name: 'sNssai[sd]', value: opts.sd },
{ name: 'smPolicyDecision[reflectiveQoSTimer]', value: opts.refQosTimer },
];
for (const f of textFields) {
if (f.value !== null) {
const loc = page.locator(`[name="f.name"]`).first();
if (await loc.count() > 0) {
await loc.fill(String(f.value));
console.log(` ✅ f.name = "f.value"`);
}
}
}
// ④ xm-select(sessRules=idx0, pccRules=idx1, praRules=idx2)
if (opts.sessRules) {
const sessDisplay = await page.evaluate(() => document.querySelectorAll('input.xm-select-default')[0]?.parentElement?.textContent || '');
if (!sessDisplay.includes('暂无数据')) {
for (const item of opts.sessRules.split(',').map(s => s.trim()).filter(Boolean)) {
await xmSelectChooseOne(page, page, 0, item);
}
}
}
if (opts.pccRules) {
for (const item of opts.pccRules.split(',').map(s => s.trim()).filter(Boolean)) {
await xmSelectChooseOne(page, page, 1, item);
}
}
if (opts.praRules) {
const praDisplay = await page.evaluate(() => document.querySelectorAll('input.xm-select-default')[2]?.parentElement?.textContent || '');
if (!praDisplay.includes('暂无数据')) {
for (const item of opts.praRules.split(',').map(s => s.trim()).filter(Boolean)) {
await xmSelectChooseOne(page, page, 2, item);
}
}
}
// ⑤ 提交
await page.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log('✅ 已提交');
// ⑥ 验证
await page.goto(`globalBaseUrl/sim_5gc/smpolicy/dnn/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const updated = await page.evaluate((targetName) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 8 && cells[2].textContent.trim() === targetName) {
return {
name: cells[2].textContent.trim(),
dnn: cells[3].textContent.trim(),
sst: cells[4].textContent.trim(),
sd: cells[5].textContent.trim(),
pccRules: cells[7].textContent.trim(),
};
}
}
return null;
}, opts.name);
if (updated) {
console.log('\n📋 验证结果:');
console.log(` name = updated.name`);
console.log(` dnn = updated.dnn '❌') : ''`);
console.log(` pccRules = updated.pccRules`);
}
console.log('\n✅ 完成');
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/smpolicy-tac-add-skill.js
/**
* smpolicy-tac-add-skill.js - TAC Smpolicy 添加技能
*
* 用法:
* node smpolicy-tac-add-skill.js --project XW_SUPF_5_1_2_4 --name tac_policy_test --tac 000100 [--headed]
*
* 参数:
* --project 工程名(默认 XW_S5GC_1)
* --name 策略名称(必填)
* --tac TAC值,只能填纯数字(必填)
* --headed 显示浏览器
*
* 添加页:/sim_5gc/smpolicy/tac/edit(iframe弹窗,仅name+tac两个字段)
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
function parseArgs() {
const args = process.argv.slice(2);
const opts = { project: 'XW_S5GC_1', name: null, tac: null, headed: false };
for (let i = 0; i < args.length; i++) {
if (args[i] === '--project' || args[i] === '-p') opts.project = args[++i];
else if (args[i] === '--name') opts.name = args[++i];
else if (args[i] === '--tac') opts.tac = args[++i];
else if (args[i] === '--headed') opts.headed = true;
}
if (!opts.name || !opts.tac) { console.error('❌ 缺少 --name 或 --tac'); process.exit(1); }
if (!/^\d+$/.test(opts.tac)) { console.error('❌ TAC必须全是数字'); process.exit(1); }
return opts;
}
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, projectName) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
const clicked = await page.evaluate((name) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === name) {
const icon = cells[1].querySelector('.iconfont');
if (icon) { icon.click(); return true; }
}
}
return false;
}, projectName);
if (!clicked) {
for (let p = 2; p <= 10; p++) {
const nextBtn = document.querySelector('.jsgrid-pager a.jsgrid-pager-next');
if (!nextBtn) break;
nextBtn.click();
await new Promise(r => setTimeout(r, 1000));
const found = page.evaluate((n) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === n) {
const icon = cells[1].querySelector('.iconfont');
if (icon) { icon.click(); return true; }
}
}
return false;
}, projectName);
if (found) break;
}
}
await page.waitForTimeout(3000);
console.log(`✅ 工程 "projectName" 已选`);
}
async function openAddDialog(page, listUrl) {
await page.goto(listUrl, { waitUntil: 'commit', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.evaluate(() => {
const btns = Array.from(document.querySelectorAll('button'));
for (const b of btns) { if (b.textContent.trim().includes('添加')) { b.click(); return; } }
});
await page.waitForFunction(() => Array.from(document.querySelectorAll('iframe')).some(f => f.src && f.src.includes('/edit')), { timeout: 10000 });
await page.waitForTimeout(2000);
const iframeSrc = await page.evaluate(() => {
const iframes = Array.from(document.querySelectorAll('iframe'));
for (const f of iframes) { if (f.src && f.src.includes('/edit')) return f.src; }
return null;
});
console.log('✅ 弹窗已打开:', iframeSrc);
await page.waitForTimeout(2000);
const frame = page.frames().find(f => f.url().includes('/edit'));
if (!frame) throw new Error('无法获取iframe');
await new Promise(r => setTimeout(r, 2000));
return frame;
}
async function main() {
const opts = parseArgs();
const browser = await chromium.launch({ headless: !opts.headed, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, opts.project);
const frame = await openAddDialog(page, `globalBaseUrl/sim_5gc/smpolicy/tac/index`);
const nameLoc = frame.locator('[name="name"]');
const tacLoc = frame.locator('[name="tac"]');
if (await nameLoc.count() > 0) { await nameLoc.fill(opts.name); console.log(` ✅ name = "opts.name"`); }
if (await tacLoc.count() > 0) { await tacLoc.fill(opts.tac); console.log(` ✅ tac = "opts.tac"`); }
const submitBtn = frame.locator('button:has-text("提交"), button:has-text("保存")');
await submitBtn.click();
console.log(' ✅ 已提交');
await new Promise(r => setTimeout(r, 3000));
console.log('\n✅ TAC Smpolicy 添加完成');
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/smpolicy-ue-add-skill.js
/**
* smpolicy-ue-add-skill.js - UE Smpolicy 添加工具
*
* 用法:
* node smpolicy-ue-add-skill.js --project XW_SUPF_5_1_2_4 --name ue_test --dnn internet
* node smpolicy-ue-add-skill.js --project XW_SUPF_5_1_2_4 --name ue_test --dnn internet --imsi 460001234567890
* node smpolicy-ue-add-skill.js --project XW_SUPF_5_1_2_4 --name ue_test --dnn internet --sst 1 --sd 111111 --pcc-rules pcc2
*
* 参数:
* --project 工程名(默认 XW_S5GC_1)
* --name UE策略名称(必填)
* --dnn DNN(必填)
* --imsi IMSI起始值(可选,不填则自动生成)
* --imsi-num IMSI数量(默认 1)
* --sst sNssai SST(默认 1)
* --sd sNssai SD(默认 111111)
* --sess-rules 会话规则名称(xm-select,多个逗号分隔)
* --pcc-rules PCC规则名称(xm-select,多个逗号分隔)
* --pra-rules PRA规则名称(xm-select,可选)
* --ref-qos-timer reflectiveQoSTimer 值(可选)
* --headed 显示浏览器窗口
*
* 添加页:/sim_5gc/smpolicy/ue/edit(layui-layer-iframe2)
* xm-select: sessRules=idx0, pccRules=idx1, praRules=idx2
*
* xm-select 交互:
* 1. frame.evaluate(() => inputs[idx].parentElement.click()) 打开下拉
* 2. frame.locator('.xm-option', {hasText}).click() 选择选项
* 3. page.keyboard.press('Escape') 关闭下拉
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
function parseArgs() {
const args = process.argv.slice(2);
const opts = {
project: 'XW_S5GC_1',
name: null,
dnn: null,
imsi: null,
imsiNum: '1',
sst: '1',
sd: '111111',
sessRules: null,
pccRules: null,
praRules: null,
refQosTimer: null,
headed: false,
};
for (let i = 0; i < args.length; i++) {
if (args[i] === '--project' || args[i] === '-p') opts.project = args[++i];
else if (args[i] === '--name') opts.name = args[++i];
else if (args[i] === '--dnn') opts.dnn = args[++i];
else if (args[i] === '--imsi') opts.imsi = args[++i];
else if (args[i] === '--imsi-num') opts.imsiNum = args[++i];
else if (args[i] === '--sst') opts.sst = args[++i];
else if (args[i] === '--sd') opts.sd = args[++i];
else if (args[i] === '--sess-rules') opts.sessRules = args[++i];
else if (args[i] === '--pcc-rules') opts.pccRules = args[++i];
else if (args[i] === '--pra-rules') opts.praRules = args[++i];
else if (args[i] === '--ref-qos-timer') opts.refQosTimer = args[++i];
else if (args[i] === '--headed') opts.headed = true;
}
if (!opts.name) { console.error('❌ 缺少 --name 参数'); process.exit(1); }
if (!opts.dnn) { console.error('❌ 缺少 --dnn 参数'); process.exit(1); }
return opts;
}
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, name) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.locator('input[name="project_search_name"]').fill(name);
await page.keyboard.press('Enter');
await page.waitForTimeout(3000);
const found = await page.evaluate((n) => {
let result = false;
document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === n) {
cells[1].querySelector('.iconfont')?.click();
result = true;
}
});
return result;
}, name);
if (!found) { console.error(`❌ 未找到工程: name`); process.exit(1); }
await page.waitForTimeout(3000);
console.log(`✅ 工程 "name" 已选`);
}
/**
* 选择 xm-select 中的一个选项(支持多选,同一选项点击可切换选中状态)
*/
async function xmSelectChooseOne(frame, page, index, value) {
if (!value) return;
// 打开下拉
await frame.evaluate((idx) => {
const inputs = document.querySelectorAll('input.xm-select-default');
if (inputs[idx]) inputs[idx].parentElement.click();
}, index);
await page.waitForTimeout(1000);
// 点击目标选项
const clicked = await frame.evaluate((text) => {
const opts = document.querySelectorAll('.xm-option');
for (const opt of opts) {
if (opt.textContent.trim() === text) {
opt.click();
return true;
}
}
return false;
}, value);
if (clicked) {
console.log(` ✅ xm-select[index] = value`);
} else {
console.log(` ⚠️ xm-select[index] 未找到选项: value`);
}
await page.waitForTimeout(500);
await page.keyboard.press('Escape');
await page.waitForTimeout(300);
}
/**
* 选择 xm-select 中的多个选项(逗号分隔)
*/
async function xmSelectChooseMultiple(frame, page, index, values) {
if (!values) return;
const items = values.split(',').map(s => s.trim()).filter(Boolean);
for (const item of items) {
await xmSelectChooseOne(frame, page, index, item);
}
}
async function main() {
const opts = parseArgs();
const browser = await chromium.launch({ headless: !opts.headed, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, opts.project);
// 导航到 UE smpolicy 列表页
await page.goto(`globalBaseUrl/sim_5gc/smpolicy/ue/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
console.log(`✅ 到达 UE Smpolicy 列表页`);
// 点击添加按钮
await page.locator('button:has-text("添加")').click();
await page.waitForTimeout(3000);
// 获取编辑帧
const frame = page.frame('layui-layer-iframe2');
if (!frame) { console.error('❌ 未找到弹窗iframe'); process.exit(1); }
await frame.waitForLoadState('domcontentloaded');
await page.waitForTimeout(2000);
console.log(`✅ 进入弹窗iframe: frame.url()`);
// ① 填写文本字段
// 自动生成 IMSI(如果未提供)
const autoImsi = opts.imsi || `4600Date.now().toString().slice(-10)`;
const textFields = [
{ name: 'name', value: opts.name },
{ name: 'dnn', value: opts.dnn },
{ name: 'imsi', value: autoImsi },
{ name: 'imsi_num', value: opts.imsiNum },
{ name: 'sNssai[sst]', value: opts.sst },
{ name: 'sNssai[sd]', value: opts.sd },
];
if (opts.refQosTimer) {
textFields.push({ name: 'smPolicyDecision[reflectiveQoSTimer]', value: opts.refQosTimer });
}
for (const f of textFields) {
const loc = frame.locator(`[name="f.name"]`).first();
if (await loc.count() > 0) {
await loc.fill(String(f.value));
console.log(` ✅ f.name = "f.value"`);
} else {
console.log(` ⚠️ 字段 f.name 不存在`);
}
}
// ② xm-select 选择(sessRules=idx0, pccRules=idx1, praRules=idx2)
// sessRules 通常无数据(暂无数据),有则选
const sessDisplay = await frame.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
return inputs[0]?.parentElement?.textContent || '';
});
if (!sessDisplay.includes('暂无数据')) {
await xmSelectChooseMultiple(frame, page, 0, opts.sessRules);
} else if (opts.sessRules) {
console.log(` ℹ️ sessRules 无可用数据,跳过`);
}
// pccRules
await xmSelectChooseMultiple(frame, page, 1, opts.pccRules);
// praRules 通常无数据
const praDisplay = await frame.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
return inputs[2]?.parentElement?.textContent || '';
});
if (!praDisplay.includes('暂无数据')) {
await xmSelectChooseMultiple(frame, page, 2, opts.praRules);
} else if (opts.praRules) {
console.log(` ℹ️ praRules 无可用数据,跳过`);
}
// ③ 提交
await frame.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log(`✅ 已提交`);
// ④ 验证:回到列表页检查
await page.goto(`globalBaseUrl/sim_5gc/smpolicy/ue/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const added = await page.evaluate((targetName) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 8 && cells[2].textContent.trim() === targetName) {
return {
name: cells[2].textContent.trim(),
dnn: cells[3].textContent.trim(),
sst: cells[4].textContent.trim(),
sd: cells[5].textContent.trim(),
sessRules: cells[6].textContent.trim(),
pccRules: cells[7].textContent.trim(),
};
}
}
return null;
}, opts.name);
if (added) {
console.log('\n📋 验证结果:');
console.log(` name = added.name '❌'`);
console.log(` dnn = added.dnn '❌'`);
console.log(` sst = added.sst`);
console.log(` sd = added.sd`);
console.log(` sessRules = added.sessRules`);
console.log(` pccRules = added.pccRules`);
if (opts.pccRules) {
const expectedPccs = opts.pccRules.split(',').map(s => s.trim());
const match = expectedPccs.every(p => added.pccRules.includes(p));
console.log(` pccRules 匹配: '⚠️'`);
}
} else {
console.log('\n❌ 未在列表中找到创建的 UE Smpolicy');
}
console.log('\n✅ 完成');
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/smpolicy-ue-edit-skill.js
/**
* smpolicy-ue-edit-skill.js - UE Smpolicy 编辑工具
*
* 用法:
* node smpolicy-ue-edit-skill.js --project XW_SUPF_5_1_2_4 --name ue_test --dnn internet_new
* node smpolicy-ue-edit-skill.js --project XW_SUPF_5_1_2_4 --name ue_test --pcc-rules pcc2,pcc_default
* node smpolicy-ue-edit-skill.js --project XW_SUPF_5_1_2_4 --name ue_test --sst 1 --sd 222222
*
* 参数:
* --project 工程名(默认 XW_S5GC_1)
* --name UE策略名称(精确匹配,要编辑的策略)
* --dnn 新 DNN(可选)
* --imsi 新 IMSI 起始值(可选)
* --sst 新 sNssai SST(可选)
* --sd 新 sNssai SD(可选)
* --sess-rules 会话规则(xm-select,可选,多个逗号分隔)
* --pcc-rules PCC规则(xm-select,可选,多个逗号分隔)
* --pra-rules PRA规则(xm-select,可选)
* --ref-qos-timer reflectiveQoSTimer(可选)
* --headed 显示浏览器窗口
*
* 编辑页:/sim_5gc/smpolicy/ue/edit/{id}(layui-layer-iframe2)
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
function parseArgs() {
const args = process.argv.slice(2);
const opts = {
project: 'XW_S5GC_1',
name: null,
dnn: null, imsi: null,
sst: null, sd: null,
sessRules: null, pccRules: null, praRules: null,
refQosTimer: null,
headed: false,
};
for (let i = 0; i < args.length; i++) {
if (args[i] === '--project' || args[i] === '-p') opts.project = args[++i];
else if (args[i] === '--name') opts.name = args[++i];
else if (args[i] === '--dnn') opts.dnn = args[++i];
else if (args[i] === '--imsi') opts.imsi = args[++i];
else if (args[i] === '--sst') opts.sst = args[++i];
else if (args[i] === '--sd') opts.sd = args[++i];
else if (args[i] === '--sess-rules') opts.sessRules = args[++i];
else if (args[i] === '--pcc-rules') opts.pccRules = args[++i];
else if (args[i] === '--pra-rules') opts.praRules = args[++i];
else if (args[i] === '--ref-qos-timer') opts.refQosTimer = args[++i];
else if (args[i] === '--headed') opts.headed = true;
}
if (!opts.name) { console.error('❌ 缺少 --name 参数'); process.exit(1); }
return opts;
}
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, name) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.locator('input[name="project_search_name"]').fill(name);
await page.keyboard.press('Enter');
await page.waitForTimeout(3000);
const found = await page.evaluate((n) => {
let result = false;
document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === n) {
cells[1].querySelector('.iconfont')?.click();
result = true;
}
});
return result;
}, name);
if (!found) { console.error(`❌ 未找到工程: name`); process.exit(1); }
await page.waitForTimeout(3000);
console.log(`✅ 工程 "name" 已选`);
}
/**
* 在 xm-select 下拉中选中指定文本(点击切换)
* @param {Page|Frame} target - 操作 DOM 的上下文(page 或 frame)
* @param {Page} page - 用于 timing 和 keyboard 的 page
*/
async function xmSelectChoose(target, page, index, value) {
if (!value) return;
// 打开下拉
await target.evaluate((idx) => {
const inputs = document.querySelectorAll('input.xm-select-default');
if (inputs[idx]) inputs[idx].parentElement.click();
}, index);
await page.waitForTimeout(1000);
const clicked = await target.evaluate((text) => {
const opts = document.querySelectorAll('.xm-option');
for (const opt of opts) {
if (opt.textContent.trim() === text) {
opt.click();
return true;
}
}
return false;
}, value);
if (clicked) {
console.log(` ✅ xm-select[index] += value`);
} else {
console.log(` ⚠️ xm-select[index] 未找到: value`);
}
await page.waitForTimeout(500);
await page.keyboard.press('Escape');
await page.waitForTimeout(300);
}
async function main() {
const opts = parseArgs();
const browser = await chromium.launch({ headless: !opts.headed, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, opts.project);
// ① 去列表页找到 UE Smpolicy 的数字 ID
await page.goto(`globalBaseUrl/sim_5gc/smpolicy/ue/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const ueInfo = await page.evaluate((targetName) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 8 && cells[2].textContent.trim() === targetName) {
return { id: cells[1].textContent.trim(), name: cells[2].textContent.trim() };
}
}
return null;
}, opts.name);
if (!ueInfo) {
console.error(`❌ 未找到 UE Smpolicy: opts.name`);
process.exit(1);
}
console.log(`✅ 找到 UE Smpolicy "opts.name",数字ID=ueInfo.id`);
// ② 直接导航到编辑页(UE smpolicy 编辑页直接渲染在主页面,非 iframe)
await page.goto(`globalBaseUrl/sim_5gc/smpolicy/ue/edit/ueInfo.id`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
// 判断是否有弹窗 iframe(添加模式有 iframe,编辑模式直接渲染)
const frame = page.frame('layui-layer-iframe2');
const activePage = frame || page;
await (frame ? frame.waitForLoadState('domcontentloaded') : page.waitForLoadState('domcontentloaded'));
await page.waitForTimeout(2000);
console.log(`✅ 进入'编辑页面'`);
// ③ 修改文本字段(只修改提供的字段)
const textFields = [
{ name: 'name', value: opts.name },
{ name: 'dnn', value: opts.dnn },
{ name: 'imsi', value: opts.imsi },
{ name: 'sNssai[sst]', value: opts.sst },
{ name: 'sNssai[sd]', value: opts.sd },
{ name: 'smPolicyDecision[reflectiveQoSTimer]', value: opts.refQosTimer },
];
for (const f of textFields) {
if (f.value !== null) {
const loc = activePage.locator(`[name="f.name"]`).first();
if (await loc.count() > 0) {
await loc.fill(String(f.value));
console.log(` ✅ f.name = "f.value"`);
}
}
}
// ④ xm-select 修改(sessRules=idx0, pccRules=idx1, praRules=idx2)
// 编辑模式下需要先读取当前值,切换时只处理新指定的
const getXmDisplay = (idx) => frame
? frame.evaluate((i) => document.querySelectorAll('input.xm-select-default')[i]?.parentElement?.textContent || '', idx)
: page.evaluate((i) => document.querySelectorAll('input.xm-select-default')[i]?.parentElement?.textContent || '', idx);
if (opts.sessRules) {
const sessDisplay = await getXmDisplay(0);
if (!sessDisplay.includes('暂无数据')) {
for (const item of opts.sessRules.split(',').map(s => s.trim()).filter(Boolean)) {
await xmSelectChoose(frame || page, page, 0, item);
}
}
}
if (opts.pccRules) {
for (const item of opts.pccRules.split(',').map(s => s.trim()).filter(Boolean)) {
await xmSelectChoose(frame || page, page, 1, item);
}
}
if (opts.praRules) {
const praDisplay = await getXmDisplay(2);
if (!praDisplay.includes('暂无数据')) {
for (const item of opts.praRules.split(',').map(s => s.trim()).filter(Boolean)) {
await xmSelectChoose(frame || page, page, 2, item);
}
}
}
// ⑤ 提交
activePage.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log('✅ 已提交');
// ⑥ 验证
await page.goto(`globalBaseUrl/sim_5gc/smpolicy/ue/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const updated = await page.evaluate((targetName) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 8 && cells[2].textContent.trim() === targetName) {
return {
name: cells[2].textContent.trim(),
dnn: cells[3].textContent.trim(),
sst: cells[4].textContent.trim(),
sd: cells[5].textContent.trim(),
sessRules: cells[6].textContent.trim(),
pccRules: cells[7].textContent.trim(),
};
}
}
return null;
}, opts.name);
if (updated) {
console.log('\n📋 验证结果:');
console.log(` name = updated.name`);
console.log(` dnn = updated.dnn '❌') : ''`);
console.log(` pccRules = updated.pccRules`);
console.log(` sst = updated.sst`);
console.log(` sd = updated.sd`);
}
console.log('\n✅ 完成');
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/smpolicy_add_pcc.js
/**
* smpolicy_add_pcc.js - 将 PCC 规则添加到 sm_policy_default 的 pccRules
*
* 用法:
* node smpolicy_add_pcc.js --project XW_SUPF_5_1_2_4 --pcc-id pcc_new
*
* 参数:
* --project 工程名(默认 XW_SUPF_5_1_2_4)
* --pcc-id PCC规则ID(必填,需已存在)
* --headed 显示浏览器窗口
*
* 完整链路:
* smpolicy/default/index → 编辑 sm_policy_default 弹窗(iframe)
* → pccRules xm-select(第1个)中添加 --pcc-id
* → 提交
*
* xm-select 交互(Playwright locator):
* 1. JS: inputs[idx].parentElement.click() 打开下拉
* 2. frame.locator('.xm-option.show-icon', {hasText}).click() 选择选项
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
function parseArgs() {
const args = process.argv.slice(2);
const opts = {
project: 'XW_SUPF_5_1_2_4',
pccId: null,
headed: false,
};
for (let i = 0; i < args.length; i++) {
if (args[i] === '--project' || args[i] === '-p') opts.project = args[++i];
else if (args[i] === '--pcc-id') opts.pccId = args[++i];
else if (args[i] === '--headed') opts.headed = true;
}
if (!opts.pccId) {
console.error('❌ 缺少 --pcc-id 参数');
process.exit(1);
}
return opts;
}
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, projectName) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.locator('input[name="project_search_name"]').fill(projectName);
await page.keyboard.press('Enter');
await page.waitForTimeout(3000);
const clicked = await page.evaluate((name) => {
let result = false;
document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === name) {
cells[1].querySelector('.iconfont')?.click();
result = true;
}
});
return result;
}, projectName);
if (!clicked) { console.error(`❌ 未找到工程: projectName`); process.exit(1); }
await page.waitForTimeout(3000);
console.log(`✅ 工程 "projectName" 已选`);
}
async function main() {
const opts = parseArgs();
const browser = await chromium.launch({ headless: !opts.headed, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, opts.project);
// 导航到 smpolicy/default/index
await page.goto(`globalBaseUrl/sim_5gc/smpolicy/default/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
console.log('✅ 到达 smpolicy/default/index');
// 点击"编辑"按钮(第1行 sm_policy_default)
await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
if (rows.length > 0) {
const editBtn = rows[0].querySelector('a');
if (editBtn) editBtn.click();
}
});
await page.waitForTimeout(3000);
// 进入弹窗 iframe
const frame = page.frame('layui-layer-iframe2');
if (!frame) { console.error('❌ 未找到弹窗iframe'); process.exit(1); }
console.log('✅ 进入弹窗iframe');
// 检查当前 pccRules 的值(pccRules = xm-select[1])
const before = await frame.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
if (inputs.length >= 2) {
return {
pccRulesValue: inputs[1].value,
pccRulesDisplay: inputs[1].parentElement.textContent.substring(0, 80),
};
}
return null;
});
console.log('\n📋 编辑前状态:', JSON.stringify(before));
// 打开 pccRules 下拉(第1个xm-select)
console.log(`\n▶ 添加 opts.pccId 到 pccRules...`);
await frame.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
if (inputs[1]) inputs[1].parentElement.click();
});
await page.waitForTimeout(1000);
// 用 Playwright locator 点击选项
const optLocator = frame.locator('.xm-option.show-icon', { hasText: opts.pccId });
const visible = await optLocator.isVisible({ timeout: 3000 }).catch(() => false);
if (visible) {
await optLocator.click();
console.log(` ✅ 选择 opts.pccId`);
} else {
console.log(` ❌ 选项 opts.pccId 不可见`);
const availOpts = await frame.evaluate(() =>
Array.from(document.querySelectorAll('.xm-option.show-icon')).map(o => o.textContent.trim())
);
console.log(` 可用选项: availOpts.join(', ')`);
}
await page.waitForTimeout(500);
// 关闭 xm-select 下拉(按 Escape 避免遮罩层)
await page.keyboard.press('Escape');
await page.waitForTimeout(300);
// 提交
await frame.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log('✅ 已提交');
// 验证
await page.goto(`globalBaseUrl/sim_5gc/smpolicy/default/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const updated = await page.evaluate((targetPccId) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 6 && cells[2].textContent.trim() === 'sm_policy_default') {
return { pccRules: cells[4].textContent.trim() };
}
}
return null;
}, opts.pccId);
if (updated) {
console.log('\n📋 更新后 pccRules:', updated.pccRules);
console.log(updated.pccRules.includes(opts.pccId) ? `\n🎉 成功将 opts.pccId 添加到 pccRules!` : `\n⚠️ 未检测到 opts.pccId`);
}
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/smpolicy_default_edit.js
/**
* smpolicy_default_edit.js - 编辑 sm_policy_default 的 pccRules
* 添加 pcc_default_test 到默认规则
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, projectName) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
const clicked = await page.evaluate((name) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === name) {
const icon = cells[1].querySelector('.iconfont');
if (icon) { icon.click(); return true; }
}
}
return false;
}, projectName);
if (!clicked) { console.log('❌ 未找到工程'); process.exit(1); }
await page.waitForTimeout(3000);
console.log(`✅ 工程 "projectName" 已选`);
}
async function main() {
const browser = await chromium.launch({ headless: false, args: ['--no-sandbox', '--ignore-certificate-issues'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, 'XW_SUPF_5_1_2_4');
// 导航到 smpolicy/default/index
await page.goto(`globalBaseUrl/sim_5gc/smpolicy/default/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
console.log('✅ 到达 smpolicy/default/index');
// 点击"编辑"按钮(第一个)
await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
if (rows.length > 0) {
const editBtn = rows[0].querySelector('a');
if (editBtn) editBtn.click();
}
});
await page.waitForTimeout(3000);
console.log('✅ 点击了编辑按钮');
console.log(` 当前URL: page.url()`);
// 检查是否有弹窗iframe
const hasIframe = await page.locator('iframe[name="layui-layer-iframe2"]').count();
console.log(` 弹窗iframe数量: hasIframe`);
if (hasIframe > 0) {
const frame = page.frame('layui-layer-iframe2');
console.log('✅ 进入弹窗iframe');
// 获取iframe内容
const iframeText = await frame.evaluate(() => document.body.innerText);
console.log('\n📄 iframe内容:');
console.log(iframeText.substring(0, 3000));
// 获取所有输入字段
const inputs = await frame.evaluate(() => {
const result = [];
document.querySelectorAll('input, select, textarea').forEach(el => {
if (el.name || el.id) {
result.push({ tag: el.tagName, name: el.name, type: el.type, value: el.value });
}
});
return result;
});
console.log('\n📋 iframe输入字段:');
inputs.forEach(i => console.log(` i.tag [i.type] name="i.name" value="i.value"`));
// 查找pccRules相关的字段
const pccInputs = inputs.filter(i => i.name && (i.name.includes('pcc') || i.name.includes('Pcc')));
console.log('\n🔍 pccRules相关字段:', pccInputs);
// 截图
await page.screenshot({ path: 'smpolicy_edit.png' });
console.log('\n📸 截图: smpolicy_edit.png');
} else {
// 没有弹窗,可能直接显示了编辑表单
console.log(' 未发现iframe,当前页面编辑');
const pageText = await page.evaluate(() => document.body.innerText);
console.log('\n📄 页面内容:');
console.log(pageText.substring(0, 3000));
await page.screenshot({ path: 'smpolicy_edit_no_iframe.png' });
}
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/smpolicy_default_explore.js
/**
* smpolicy_default_explore.js - 探索 smpolicy/default 页面
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, projectName) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
const clicked = await page.evaluate((name) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === name) {
const icon = cells[1].querySelector('.iconfont');
if (icon) { icon.click(); return true; }
}
}
return false;
}, projectName);
if (!clicked) { console.log('❌ 未找到工程'); process.exit(1); }
await page.waitForTimeout(3000);
console.log(`✅ 工程 "projectName" 已选`);
}
async function main() {
const browser = await chromium.launch({ headless: false, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, 'XW_SUPF_5_1_2_4');
// 导航到 smpolicy/default/index
console.log('\n🌐 导航到 smpolicy/default/index...');
await page.goto(`globalBaseUrl/sim_5gc/smpolicy/default/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
console.log(` URL: page.url()`);
// 获取页面内容
const pageText = await page.evaluate(() => document.body.innerText);
console.log('\n📄 页面内容(前4000字符):');
console.log(pageText.substring(0, 4000));
// 获取所有输入字段
const inputs = await page.evaluate(() => {
const result = [];
document.querySelectorAll('input, select, textarea').forEach(el => {
if (el.name || el.id) {
result.push({ tag: el.tagName, name: el.name, type: el.type, value: el.value });
}
});
return result;
});
console.log('\n📋 输入字段:');
inputs.forEach(i => console.log(` i.tag [i.type] name="i.name" value="i.value"`));
// 获取layui表格
const tables = await page.evaluate(() => {
const result = [];
document.querySelectorAll('.layui-table').forEach((tbl, idx) => {
const headers = Array.from(tbl.querySelectorAll('thead th')).map(th => th.textContent.trim());
const rows = Array.from(tbl.querySelectorAll('tbody tr')).map(tr =>
Array.from(tr.querySelectorAll('td')).map(td => td.textContent.trim())
);
result.push({ idx, headers, rows });
});
return result;
});
console.log('\n📊 表格:');
tables.forEach(t => {
console.log(` 表格t.idx: headers=JSON.stringify(t.headers)`);
t.rows.slice(0, 10).forEach(r => console.log(` JSON.stringify(r)`));
});
// 获取所有按钮
const buttons = await page.evaluate(() => {
const result = [];
document.querySelectorAll('button, .layui-btn').forEach(b => {
if (b.textContent.trim()) result.push(b.textContent.trim());
});
return [...new Set(result)];
});
console.log('\n🔘 按钮:', buttons);
// 截图
await page.screenshot({ path: 'smpolicy_default.png', fullPage: true });
console.log('\n📸 截图: smpolicy_default.png');
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/tc-add-skill.js
/**
* tc-add-skill.js - Traffic Control 流量控制模板添加工具
*
* 用法:
* node tc-add-skill.js --project XW_SUPF_5_1_2_4 --tc-id tc_new --flow-status ENABLED-UPLINK [--headed]
*
* 参数:
* --project 工程名(默认 XW_S5GC_1)
* --tc-id TC模板ID(必填,字母/数字/下划线)
* --flow-status flowStatus(默认 ENABLED-UPLINK)
* 可选值:ENABLED-UPLINK, ENABLED-DOWNLINK, ENABLED, DISABLED, REMOVED
* --headed 显示浏览器窗口
*
* 完整链路:
* 点击"添加" → 弹窗 iframe(layui-layer-iframe2)→ 填写 tcId + flowStatus(SELECT)
* → 提交 → 返回列表页
*
* 注意事项:
* - flowStatus 是 SELECT 下拉框,用 JS 方式设置值(layui 隐藏原生select)
* - tcId 是必填字段
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
function parseArgs() {
const args = process.argv.slice(2);
const opts = {
project: 'XW_S5GC_1',
tcId: null,
flowStatus: 'ENABLED-UPLINK',
headed: false,
};
for (let i = 0; i < args.length; i++) {
if (args[i] === '--project' || args[i] === '-p') opts.project = args[++i];
else if (args[i] === '--tc-id') opts.tcId = args[++i];
else if (args[i] === '--flow-status') opts.flowStatus = args[++i];
else if (args[i] === '--headed') opts.headed = true;
}
if (!opts.tcId) {
console.error('❌ 缺少必要参数: --tc-id');
console.error(' 示例: node tc-add-skill.js --project XW_SUPF_5_1_2_4 --tc-id tc_new --flow-status ENABLED-UPLINK');
process.exit(1);
}
return opts;
}
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000, waitUntil: 'domcontentloaded' });
await page.waitForTimeout(2000);
try { await page.locator('input[name="email"]').first().waitFor({ state: 'visible', timeout: 5000 }); } catch(e) {}
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, projectName) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.locator('input[name="project_search_name"]').fill(projectName);
await page.keyboard.press('Enter');
await page.waitForTimeout(3000);
const clicked = await page.evaluate((name) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === name) {
cells[1].querySelector('.iconfont').click();
return true;
}
}
return false;
}, projectName);
if (!clicked) { console.error(`❌ 未找到工程: projectName`); process.exit(1); }
await page.waitForTimeout(3000);
console.log(`✅ 工程 "projectName" 已选`);
}
async function main() {
const opts = parseArgs();
const browser = await chromium.launch({ headless: !opts.headed, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, opts.project);
// 去 TC 列表页
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/trafficCtl/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
console.log(`✅ 到达TC列表页`);
// 点击添加按钮
await page.locator('button:has-text("添加")').click();
await page.waitForTimeout(5000);
// 获取弹窗 iframe(layui-layer-iframe2)
const frame = page.frame('layui-layer-iframe2');
if (!frame) { console.error('❌ 未找到弹窗iframe'); process.exit(1); }
console.log(`✅ 进入弹窗iframe`);
// 填写 tcId
await frame.locator('input[name="tcId"]').fill(opts.tcId);
console.log(` tcId="opts.tcId"`);
// 设置 flowStatus(用 JS 方式,因为 layui 隐藏了原生 select)
await frame.evaluate((status) => {
const sel = document.querySelector('select[name="flowStatus"]');
if (sel) { sel.value = status; sel.dispatchEvent(new Event('change', { bubbles: true })); }
}, opts.flowStatus);
console.log(` flowStatus="opts.flowStatus"`);
// 提交
await frame.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log(`✅ TC模板 opts.tcId 已提交`);
// 验证
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/trafficCtl/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const tcData = await page.evaluate((targetId) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 4 && cells[2].textContent.trim() === targetId) {
return { tcId: cells[2].textContent.trim(), flowStatus: cells[3].textContent.trim() };
}
}
return null;
}, opts.tcId);
if (tcData) {
console.log('\n📋 验证结果:');
console.log(` tcId = tcData.tcId`);
console.log(` flowStatus = tcData.flowStatus '❌'`);
} else {
console.log('\n❌ TC模板未找到');
}
console.log('\n✅ 完成');
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/test_full_flow.sh
#!/bin/bash
cd /home/dotouch/.openclaw/workspace/skills/5gc/scripts
SMF_NAME="SMF_FULL_$(date +%H%M%S)"
node smf-pgwc-add-skill.js "$SMF_NAME" --project XW_S5GC_1 --pfcp_sip 10.200.2.50 --http2_sip 10.200.2.51 --ue_min 30.30.30.30 --ue_max 30.30.30.60 --mcc 460 --mnc 01 --pdu_capacity 200000 --interest_tac 101
FILE:scripts/test_lookup.js
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: true, args: ['--no-sandbox','--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: {width:1920,height:1080} });
const page = await ctx.newPage();
// Login
await page.goto('https://192.168.3.89/login', { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', {name:'E-Mail地址'}).fill('[email protected]');
await page.getByRole('textbox', {name:'密码'}).fill('dotouch');
await page.getByRole('button', {name:'登录'}).click();
await page.waitForTimeout(2500);
console.log('Logged in');
// Select project
await page.goto('https://192.168.3.89/sim_5gc/project/index', { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.evaluate(function(name) {
var rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (var i = 0; i < rows.length; i++) {
var cells = rows[i].querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === name) {
var icon = cells[1].querySelector('.iconfont');
if (icon) { icon.click(); return; }
}
}
}, 'XW_SUPF_5_1_2_4');
await page.waitForTimeout(3000);
console.log('Project selected, URL:', page.url());
// Navigate to PCC
await page.goto('https://192.168.3.89/sim_5gc/predfPolicy/pcc/index', { waitUntil: 'commit', ignoreHTTPSErrors: true });
// Wait for pcc2 text
try {
await page.waitForFunction(function() {
return document.body.textContent.includes('pcc2');
}, { timeout: 15000 });
console.log('pcc2 found in body!');
} catch(e) {
console.log('pcc2 NOT found in body within timeout');
await browser.close();
return;
}
// Now query DOM
await page.waitForTimeout(3000);
const result = await page.evaluate(function() {
var tables = document.querySelectorAll('.layui-table');
var info = [];
for (var ti = 0; ti < tables.length; ti++) {
var table = tables[ti];
var rows = table.querySelectorAll('tbody tr');
info.push('TABLE ' + ti + ': ' + rows.length + ' rows');
for (var ri = 0; ri < rows.length; ri++) {
var row = rows[ri];
var cells = row.querySelectorAll('td');
var txt = ' row' + ri + ' cells=' + cells.length + ': ';
for (var ci = 0; ci < cells.length; ci++) {
txt += '[' + ci + ']="' + cells[ci].textContent.trim().substring(0, 15) + '" ';
}
info.push(txt);
}
}
return info.join('\n');
});
console.log('DOM structure:\n' + result);
// Now try the exact query from the skill
const lookup = await page.evaluate(function(targetId) {
var tables = document.querySelectorAll('.layui-table');
for (var ti = 0; ti < tables.length; ti++) {
var table = tables[ti];
var rows = table.querySelectorAll('tbody tr');
for (var ri = 0; ri < rows.length; ri++) {
var row = rows[ri];
var cells = row.querySelectorAll('td');
if (cells.length < 9) continue;
var c0 = cells[0].textContent.trim();
var c1 = cells[1].textContent.trim();
if (c1 === targetId) return { found: true, id: c0 };
}
}
return { found: false };
}, 'pcc2');
console.log('Lookup result:', JSON.stringify(lookup));
await browser.close();
})().catch(function(e) { console.error(e.message); });
FILE:scripts/test_nav.js
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: true, args: ['--no-sandbox','--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: {width:1920,height:1080} });
const page = await ctx.newPage();
await page.goto('https://192.168.3.89/login', { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', {name:'E-Mail地址'}).fill('[email protected]');
await page.getByRole('textbox', {name:'密码'}).fill('dotouch');
await page.getByRole('button', {name:'登录'}).click();
await page.waitForTimeout(2500);
await page.goto('https://192.168.3.89/sim_5gc/project/index', { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.evaluate(function(name) {
var rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (var i = 0; i < rows.length; i++) {
var cells = rows[i].querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === name) {
var icon = cells[1].querySelector('.iconfont');
if (icon) { icon.click(); return; }
}
}
}, 'XW_SUPF_5_1_2_4');
await page.waitForTimeout(3000);
console.log('URL after select:', page.url());
// Try clicking PCC sidebar link
const pccLink = page.locator('a[href*="/predfPolicy/pcc"]').first();
const cnt = await pccLink.count();
console.log('PCC link count:', cnt);
if (cnt > 0) {
await pccLink.click();
await page.waitForTimeout(5000);
const info = await page.evaluate(function() {
var rows = document.querySelectorAll('.layui-table tbody tr');
return { url: window.location.href, rowCount: rows.length };
});
console.log('After click PCC:', JSON.stringify(info));
} else {
// try goto with both commit and extended wait
await page.goto('https://192.168.3.89/sim_5gc/predfPolicy/pcc/index', { waitUntil: 'commit', ignoreHTTPSErrors: true });
await page.waitForTimeout(8000);
const info2 = await page.evaluate(function() {
var rows = document.querySelectorAll('.layui-table tbody tr');
return { url: window.location.href, rowCount: rows.length };
});
console.log('After goto+wait PCC:', JSON.stringify(info2));
}
await browser.close();
})().catch(function(e) { console.error(e.message); });
FILE:scripts/test_nav2.js
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: true, args: ['--no-sandbox','--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: {width:1920,height:1080} });
const page = await ctx.newPage();
await page.goto('https://192.168.3.89/login', { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', {name:'E-Mail地址'}).fill('[email protected]');
await page.getByRole('textbox', {name:'密码'}).fill('dotouch');
await page.getByRole('button', {name:'登录'}).click();
await page.waitForTimeout(2500);
await page.goto('https://192.168.3.89/sim_5gc/project/index', { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.evaluate(function(name) {
var rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (var i = 0; i < rows.length; i++) {
var cells = rows[i].querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === name) {
var icon = cells[1].querySelector('.iconfont');
if (icon) { icon.click(); return; }
}
}
}, 'XW_SUPF_5_1_2_4');
await page.waitForTimeout(3000);
console.log('URL after select:', page.url());
// Navigate to PCC and wait for pcc2 specifically
await page.goto('https://192.168.3.89/sim_5gc/predfPolicy/pcc/index', { waitUntil: 'commit', ignoreHTTPSErrors: true });
try {
await page.waitForFunction(function() {
// Check if pcc2 text appears in any table
return document.body.textContent.includes('pcc2');
}, { timeout: 15000 });
console.log('pcc2 appeared!');
} catch(e) {
console.log('pcc2 did not appear within timeout');
}
await page.waitForTimeout(2000);
const info = await page.evaluate(function() {
var rows = document.querySelectorAll('.layui-table tbody tr');
var result = [];
for (var i = 0; i < rows.length; i++) {
var txt = rows[i].textContent.trim();
if (txt.length > 5) result.push(txt.substring(0, 60));
}
return { url: window.location.href, rows: result };
});
console.log(JSON.stringify(info, null, 2));
await browser.close();
})().catch(function(e) { console.error(e.message); });
FILE:scripts/test_pcc_single.js
/**
* test_pcc_single.js - 单测 pcc-add-skill 的添加流程
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000, waitUntil: 'domcontentloaded' });
await page.waitForTimeout(2000);
try { await page.locator('input[name="email"]').first().waitFor({ state: 'visible', timeout: 5000 }); } catch(e) {}
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, name) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.locator('input[name="project_search_name"]').fill(name);
await page.keyboard.press('Enter');
await page.waitForTimeout(3000);
await page.evaluate((n) => {
document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === n) cells[1].querySelector('.iconfont').click();
});
}, name);
await page.waitForTimeout(3000);
console.log(`✅ 工程 "name" 已选`);
}
async function main() {
const browser = await chromium.launch({ headless: false, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, 'XW_SUPF_5_1_2_4');
const testPccId = 'pcc_regtest_' + Date.now();
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/pcc/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
console.log('PCC列表页 URL:', page.url());
console.log('弹窗数量:', await page.locator('.layui-layer').count());
// 点击添加
await page.locator('button:has-text("添加")').click();
// 等待 URL 变化(主框架跳转到 /predfPolicy/pcc/edit)
await page.waitForFunction(() => window.location.href.includes('/predfPolicy/pcc/edit'), { timeout: 10000 });
await page.waitForTimeout(3000);
console.log('点击后 URL:', page.url());
console.log('弹窗数量:', await page.locator('.layui-layer').count());
// 检查页面上的iframe
const iframes = await page.locator('iframe').all();
for (const iframe of iframes) {
console.log(` iframe: name="await iframe.getAttribute('name')", src="await iframe.getAttribute('src')"`);
}
// PCC 编辑页是主框架直接跳转,表单直接在主页面上
// 但是 URL 显示的是 /predfPolicy/pcc/edit,这意味着表单可能在某个地方
// 尝试查找表单
const bodyText = await page.evaluate(() => document.body.innerText.substring(0, 500));
console.log('\n页面内容(前500字):', bodyText);
// 查找input
const inputs = await page.evaluate(() => {
return Array.from(document.querySelectorAll('input[name]')).map(inp => ({
name: inp.name || '',
type: inp.type || '',
value: inp.value ? inp.value.substring(0, 30) : ''
}));
});
console.log('\n页面input:', JSON.stringify(inputs));
await page.screenshot({ path: 'pcc_add_page.png', fullPage: true });
console.log('截图: pcc_add_page.png');
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/test_pcf_edit.js
/**
* test_pcf_edit.js - 单测 PCF 编辑弹窗的 default_smpolicy 下拉
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000, waitUntil: 'domcontentloaded' });
await page.waitForTimeout(2000);
try { await page.locator('input[name="email"]').first().waitFor({ state: 'visible', timeout: 5000 }); } catch(e) {}
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, name) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.locator('input[name="project_search_name"]').fill(name);
await page.keyboard.press('Enter');
await page.waitForTimeout(3000);
await page.evaluate((n) => {
document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === n) cells[1].querySelector('.iconfont').click();
});
}, name);
await page.waitForTimeout(3000);
console.log(`✅ 工程 "name" 已选`);
}
async function main() {
const browser = await chromium.launch({ headless: false, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, 'XW_SUPF_5_1_2_4');
await page.goto(`globalBaseUrl/sim_5gc/pcf/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
console.log('PCF列表 URL:', page.url());
// 点击编辑按钮
await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
if (rows.length > 0) {
const links = rows[0].querySelectorAll('a');
for (const l of links) { if (l.textContent.trim() === '编辑') { l.click(); return; } }
}
});
await page.waitForTimeout(3000);
// 等待弹窗
await page.waitForSelector('iframe[name="layui-layer-iframe2"]', { timeout: 5000 });
await page.waitForTimeout(2000);
const frame = page.frame('layui-layer-iframe2');
if (!frame) { console.log('❌ 未找到弹窗iframe'); await browser.close(); return; }
console.log('✅ 找到弹窗iframe:', frame.url());
// 检查 xm-select 状态
const xmData = await frame.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
return Array.from(inputs).map((inp, i) => ({
idx: i, value: inp.value, display: inp.parentElement.textContent.substring(0, 80)
}));
});
console.log('\n当前 xm-select 状态:', JSON.stringify(xmData, null, 2));
// 点击 default_smpolicy xm-select(第0个)
console.log('\n▶ 尝试选择 sm_policy_default ...');
await frame.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
if (inputs[0]) inputs[0].parentElement.click();
});
await page.waitForTimeout(1500);
// Playwright locator 在 frame 内查找
const smpOptVisible = await frame.locator('.xm-option.show-icon', { hasText: 'sm_policy_default' }).isVisible({ timeout: 3000 }).catch(() => false);
console.log('sm_policy_default 选项可见:', smpOptVisible);
if (smpOptVisible) {
await frame.locator('.xm-option.show-icon', { hasText: 'sm_policy_default' }).first().click();
console.log('✅ sm_policy_default 已点击');
await page.keyboard.press('Escape');
await page.waitForTimeout(500);
} else {
console.log('❌ sm_policy_default 选项不可见');
// JS fallback
const clicked = await frame.evaluate(() => {
const opts = document.querySelectorAll('.xm-option.show-icon');
for (const opt of opts) {
if (opt.textContent.trim().includes('sm_policy_default')) { opt.click(); return true; }
}
return false;
});
console.log('JS fallback:', clicked);
}
// 获取更新后的值
const afterValue = await frame.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
return inputs.length >= 1 ? inputs[0].value : '';
});
console.log('选择后 default_smpolicy value:', afterValue);
// 提交
await frame.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log('✅ 提交完成');
// 验证
await page.goto(`globalBaseUrl/sim_5gc/pcf/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
if (rows.length > 0) {
const links = rows[0].querySelectorAll('a');
for (const l of links) { if (l.textContent.trim() === '编辑') { l.click(); return; } }
}
});
await page.waitForTimeout(3000);
await page.waitForSelector('iframe[name="layui-layer-iframe2"]', { timeout: 5000 });
await page.waitForTimeout(2000);
const frame2 = page.frame('layui-layer-iframe2');
const finalVal = await frame2.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
return inputs.length >= 1 ? inputs[0].value : '';
});
console.log(`\n最终 default_smpolicy="finalVal"`);
if (finalVal && finalVal.includes('sm_policy_default')) {
console.log('🎉 PCF default_smpolicy 配置成功!');
}
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/test_qos_single.js
/**
* test_qos_single.js - 单测 qos-add-skill
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000, waitUntil: 'domcontentloaded' });
await page.waitForTimeout(2000);
try { await page.locator('input[name="email"]').first().waitFor({ state: 'visible', timeout: 5000 }); } catch(e) {}
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, name) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.locator('input[name="project_search_name"]').fill(name);
await page.keyboard.press('Enter');
await page.waitForTimeout(3000);
await page.evaluate((n) => {
document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === n) cells[1].querySelector('.iconfont').click();
});
}, name);
await page.waitForTimeout(3000);
console.log(`✅ 工程 "name" 已选`);
}
async function main() {
const browser = await chromium.launch({ headless: false, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, 'XW_SUPF_5_1_2_4');
const testQosId = 'qos_regtest_' + Date.now();
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/qos/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
console.log('到达QoS列表页');
// 点击添加
await page.locator('button:has-text("添加")').click();
await page.waitForTimeout(5000);
// 获取frame
const frame = page.frame('layui-layer-iframe2');
if (!frame) { console.log('❌ 未找到 layui-layer-iframe2'); await browser.close(); return; }
console.log('✅ 找到frame:', frame.url());
// 填写
await frame.locator('input[name="qosId"]').fill(testQosId);
await frame.locator('input[name="5qi"]').fill('9');
await frame.locator('input[name="maxbrUl"]').fill('10000000');
await frame.locator('input[name="maxbrDl"]').fill('20000000');
await frame.locator('input[name="gbrUl"]').fill('5000000');
await frame.locator('input[name="gbrDl"]').fill('5000000');
console.log('✅ 填写完成');
// 提交
await frame.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log('✅ 提交完成');
// 验证
const rows = await page.evaluate(() => {
return Array.from(document.querySelectorAll('.layui-table tbody tr')).map(row =>
Array.from(row.querySelectorAll('td')).map(td => td.textContent.trim())
);
});
const found = rows.find(r => r[2] === testQosId);
if (found) {
console.log(`\n🎉 QoS模板创建成功!`);
console.log(` qosId=found[2], 5qi=found[3], maxbrUl=found[4], maxbrDl=found[5], gbrUl=found[6], gbrDl=found[7]`);
} else {
console.log(`\n❌ QoS模板 testQosId 未找到`);
}
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/test_smf_basic.sh
#!/bin/bash
cd /home/dotouch/.openclaw/workspace/skills/5gc/scripts
TS=$(date +%H%M%S)
node smf-pgwc-add-skill.js \
--name SMF_BASIC_$TS \
--project XW_S5GC_1 \
--pfcp_sip 10.200.2.50 \
--http2_sip 10.200.2.51 \
--ue_min 30.30.30.30 \
--ue_max 30.30.30.60 \
--mcc 460 \
--mnc 01 \
--pdu_capacity 200000 \
--interest_tac "101"
FILE:scripts/test_smpolicy_edit.js
/**
* test_smpolicy_edit.js - 单测 sm_policy_default 的 pccRules 绑定
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000, waitUntil: 'domcontentloaded' });
await page.waitForTimeout(2000);
try { await page.locator('input[name="email"]').first().waitFor({ state: 'visible', timeout: 5000 }); } catch(e) {}
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, name) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.locator('input[name="project_search_name"]').fill(name);
await page.keyboard.press('Enter');
await page.waitForTimeout(3000);
await page.evaluate((n) => {
document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === n) cells[1].querySelector('.iconfont').click();
});
}, name);
await page.waitForTimeout(3000);
console.log(`✅ 工程 "name" 已选`);
}
async function main() {
const browser = await chromium.launch({ headless: false, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, 'XW_SUPF_5_1_2_4');
await page.goto(`globalBaseUrl/sim_5gc/smpolicy/default/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
// 找到 sm_policy_default 的数字 ID 和行
const rowData = await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
// 找到名称为 sm_policy_default 的行
for (let i = 0; i < cells.length; i++) {
if (cells[i].textContent.trim() === 'sm_policy_default') {
// 这一行的ID在第 i-1 个单元格(往前一格)
const id = i > 0 ? cells[i - 1].textContent.trim() : null;
return { id, rowHtml: row.innerHTML.substring(0, 300) };
}
}
}
return null;
});
console.log('sm_policy_default ID:', rowData?.id);
console.log('rowHtml:', rowData?.rowHtml);
if (!rowData?.id) { console.log('❌ sm_policy_default 未找到'); await browser.close(); return; }
// 点击编辑按钮
await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
if (row.innerHTML.includes('sm_policy_default')) {
const links = row.querySelectorAll('a');
for (const l of links) { if (l.textContent.trim() === '编辑') { l.click(); return; } }
}
}
});
await page.waitForTimeout(3000);
// 等待并获取弹窗iframe
await page.waitForSelector('iframe[name="layui-layer-iframe2"]', { timeout: 5000 });
await page.waitForTimeout(2000);
const frame = page.frame('layui-layer-iframe2');
if (!frame) { console.log('❌ 未找到弹窗iframe'); await browser.close(); return; }
console.log('✅ 找到弹窗iframe:', frame.url());
// 检查 xm-select
const xmData = await frame.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
return Array.from(inputs).map((inp, i) => ({
idx: i, value: inp.value, display: inp.parentElement.textContent.substring(0, 80)
}));
});
console.log('\n当前 xm-select 状态:', JSON.stringify(xmData, null, 2));
// 点击 pccRules xm-select(第1个xm-select,index=1)
console.log('\n▶ 尝试选择 pcc1 ...');
await frame.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
if (inputs[1]) inputs[1].parentElement.click();
});
await page.waitForTimeout(1500);
// Playwright locator 在 frame 内查找
const pccOptVisible = await frame.locator('.xm-option.show-icon', { hasText: 'pcc1' }).isVisible({ timeout: 3000 }).catch(() => false);
console.log('pcc1 选项可见:', pccOptVisible);
if (pccOptVisible) {
await frame.locator('.xm-option.show-icon', { hasText: 'pcc1' }).first().click();
console.log('✅ pcc1 已点击');
// 关闭 xm-select 下拉
await page.keyboard.press('Escape');
await page.waitForTimeout(500);
} else {
const clicked = await frame.evaluate(() => {
const opts = document.querySelectorAll('.xm-option.show-icon');
for (const opt of opts) {
if (opt.textContent.trim() === 'pcc1') { opt.click(); return true; }
}
return false;
});
console.log('JS fallback 点击 pcc1:', clicked);
}
await page.waitForTimeout(1000);
// 提交
await frame.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log('✅ 提交完成');
// 验证
await page.goto(`globalBaseUrl/sim_5gc/smpolicy/default/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const finalPcc = await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
if (row.innerHTML.includes('sm_policy_default')) {
const cells = row.querySelectorAll('td');
// pccRules 列
for (let i = 0; i < cells.length; i++) {
if (cells[i].textContent.trim() === 'sm_policy_default') {
return i + 2 < cells.length ? cells[i + 2].textContent.trim() : null;
}
}
}
}
return null;
});
console.log(`\n最终 pccRules="finalPcc"`);
if (finalPcc && finalPcc.includes('pcc1')) {
console.log('🎉 sm_policy_default pccRules 绑定 pcc1 成功!');
} else {
console.log('⚠️ pccRules 可能已包含 pcc1 或绑定方式不同');
}
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/test_tc_add.js
/**
* test_tc_add.js - 单测 TC (Traffic Control) 添加
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000, waitUntil: 'domcontentloaded' });
await page.waitForTimeout(2000);
try { await page.locator('input[name="email"]').first().waitFor({ state: 'visible', timeout: 5000 }); } catch(e) {}
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, name) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.locator('input[name="project_search_name"]').fill(name);
await page.keyboard.press('Enter');
await page.waitForTimeout(3000);
await page.evaluate((n) => {
document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === n) cells[1].querySelector('.iconfont').click();
});
}, name);
await page.waitForTimeout(3000);
console.log(`✅ 工程 "name" 已选`);
}
async function main() {
const browser = await chromium.launch({ headless: false, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, 'XW_SUPF_5_1_2_4');
const testTcId = 'tc_regtest_' + Date.now();
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/trafficCtl/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
await page.locator('button:has-text("添加")').click();
await page.waitForTimeout(5000);
const frame = page.frame('layui-layer-iframe2');
if (!frame) { console.log('❌ 未找到 layui-layer-iframe2'); await browser.close(); return; }
console.log('✅ 找到 layui-layer-iframe2:', frame.url());
await page.waitForTimeout(2000);
// 填写 tcId
await frame.locator('input[name="tcId"]').fill(testTcId);
console.log('✅ 填写 tcId');
// 使用 JS 方式设置 select(layui 的 select 可能被隐藏但JS可以设值)
const selectResult = await frame.evaluate(() => {
const sel = document.querySelector('select[name="flowStatus"]');
if (sel) {
sel.value = 'ENABLED-UPLINK';
sel.dispatchEvent(new Event('change', { bubbles: true }));
return { found: true, value: sel.value };
}
return { found: false };
});
console.log('flowStatus select 结果:', JSON.stringify(selectResult));
// 提交
await frame.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log('✅ 提交完成');
// 验证
const rows = await page.evaluate(() => {
return Array.from(document.querySelectorAll('.layui-table tbody tr')).map(row =>
Array.from(row.querySelectorAll('td')).map(td => td.textContent.trim())
);
});
const found = rows.find(r => r[2] === testTcId);
if (found) {
console.log(`\n🎉 TC创建成功! tcId=found[2], flowStatus=found[3]`);
} else {
console.log(`\n❌ TC testTcId 未找到`);
}
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/test_xhr.js
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: true, args: ['--no-sandbox','--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: {width:1920,height:1080} });
const page = await ctx.newPage();
await page.goto('https://192.168.3.89/login', { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', {name:'E-Mail地址'}).fill('[email protected]');
await page.getByRole('textbox', {name:'密码'}).fill('dotouch');
await page.getByRole('button', {name:'登录'}).click();
await page.waitForTimeout(2500);
await page.goto('https://192.168.3.89/sim_5gc/project/index', { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.evaluate(function(name) {
var rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (var i = 0; i < rows.length; i++) {
var cells = rows[i].querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === name) {
var icon = cells[1].querySelector('.iconfont');
if (icon) { icon.click(); return; }
}
}
}, 'XW_SUPF_5_1_2_4');
await page.waitForTimeout(3000);
console.log('URL after select:', page.url());
// Try fetching the PCC list API directly (no auth cookies needed in same context)
const apiData = await page.evaluate(async function() {
try {
// First get CSRF token from page
const token = document.querySelector('meta[name="csrf-token"]') ||
document.querySelector('input[name="_token"]');
const tokenVal = token ? token.content || token.value : '';
const resp = await fetch('/predfPolicy/pcc/index', {
headers: { 'X-Requested-With': 'XMLHttpRequest', 'X-CSRF-TOKEN': tokenVal }
});
return { status: resp.status, url: resp.url, contentType: resp.headers.get('content-type') };
} catch(e) { return { error: e.message }; }
});
console.log('API response:', JSON.stringify(apiData));
// Alternative: use XHR
const xhrData = await page.evaluate(function() {
return new Promise(function(resolve) {
var xhr = new XMLHttpRequest();
xhr.open('GET', '/predfPolicy/pcc/index', true);
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) resolve({ status: xhr.status, len: xhr.responseText.length, url: xhr.responseURL });
};
xhr.send();
setTimeout(function() { resolve({ timeout: true }); }, 5000);
});
});
console.log('XHR response:', JSON.stringify(xhrData));
await browser.close();
})().catch(function(e) { console.error(e.message); });
FILE:scripts/ubuntu_smoke_test.js
const { chromium } = require('playwright');
(async()=>{
const browser = await chromium.launch({headless:true,args:['--no-sandbox','--ignore-certificate-errors','--disable-dev-shm-usage','--no-proxy-server','--proxy-server=direct://','--proxy-bypass-list=*']});
const context = await browser.newContext({ignoreHTTPSErrors:true, viewport:{width:1440,height:900}});
const page = await context.newPage();
const base='https://192.168.3.89';
await page.goto(base+'/login',{waitUntil:'commit', timeout:20000});
await page.waitForTimeout(3000);
await page.getByRole('textbox',{name:'E-Mail地址'}).fill('[email protected]');
await page.getByRole('textbox',{name:'密码'}).fill('dotouch');
const remember=page.getByRole('checkbox',{name:'记住我'});
if (await remember.count()) await remember.check().catch(()=>{});
await page.getByRole('button',{name:'登录'}).click();
await page.waitForTimeout(5000);
await page.goto(base+'/sim_5gc/project/index',{waitUntil:'commit', timeout:20000});
await page.waitForTimeout(5000);
const result = await page.evaluate(()=>({
title: document.title,
rows: document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row').length,
hasSearch: !!document.querySelector('input[name="project_search_name"]'),
url: location.href
}));
console.log(JSON.stringify(result));
await browser.close();
})().catch(e=>{ console.error(e.stack||e.message); process.exit(1); });
FILE:scripts/ue-add-skill.js
/**
* UE 添加脚本 - ue-add-skill.js
* 用法: node ue-add-skill.js --name <名称> [--imsi <imsi>] [--msisdn <msisdn>] [--mcc <mcc>] [--mnc <mnc>] [--project <工程>] [--headed]
*
* 工程默认: XW_S5GC_1
* UE 表单: /sim_5gc/ue/edit(不是工单弹窗)
*/
const { chromium } = require('playwright');
const fs = require('fs');
const path = require('path');
const BASE_URL = 'https://192.168.3.89';
const LOGIN_EMAIL = '[email protected]';
const LOGIN_PWD = 'dotouch';
const SESSION_DIR = path.join(__dirname, '.sessions');
const SESSION_FILE = path.join(SESSION_DIR, `5gc_ue_add_\/\//, '').replace(/\./g, '_').json`);
const yargs = require('yargs');
const args = yargs(process.argv.slice(2))
.option('name', { alias: 'n', type: 'string', demandOption: true, describe: 'UE名称(只支持字母/数字/下划线)' })
.option('url', { alias: 'u', type: 'string', default: BASE_URL, describe: '5GC仪表地址' })
.option('imsi', { type: 'string', default: '460001234567890', describe: '起始IMSI(15位)' })
.option('msisdn', { type: 'string', default: '8611111111111', describe: 'MSISDN(13-15位)' })
.option('mcc', { type: 'string', default: '460', describe: 'MCC' })
.option('mnc', { type: 'string', default: '01', describe: 'MNC' })
.option('project', { alias: 'p', type: 'string', default: 'XW_S5GC_1', describe: '工程名称' })
.option('count', { alias: 'c', type: 'string', default: '1', describe: '数量' })
.option('key', { type: 'string', default: '11111111111111111111111111111111', describe: 'KI密钥(32位hex)' })
.option('opc', { type: 'string', default: '11111111111111111111111111111111', describe: 'OPc密钥(32位hex)' })
.option('imeisv', { type: 'string', default: '8611111111111111', describe: 'IMEISV' })
.option('sst', { type: 'string', default: '1', describe: 'NSSAI SST' })
.option('sd', { type: 'string', default: '111111', describe: 'NSSAI SD' })
.option('headed', { type: 'boolean', default: false, describe: '显示浏览器' })
.parse();
const HEADLESS = !args.headed;
const BASE = args.url;
function shouldBypassProxy(rawUrl) {
try {
const u = new URL(rawUrl);
const h = (u.hostname || '').toLowerCase();
if (h === 'localhost' || h === '127.0.0.1' || h === '::1') return true;
if (/^10\./.test(h)) return true;
if (/^192\.168\./.test(h)) return true;
const m = h.match(/^172\.(\d+)\./);
if (m) {
const n = Number(m[1]);
if (n >= 16 && n <= 31) return true;
}
return false;
} catch {
return false;
}
}
console.log('▶ 添加 UE: ' + args.name);
console.log(' URL: ' + BASE);
console.log(' 工程: ' + args.project);
async function loadSession(browser) {
try {
if (!fs.existsSync(SESSION_FILE)) return null;
const { storageState } = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf8'));
return await browser.newContext({ ignoreHTTPSErrors: true, storageState });
} catch {
return null;
}
}
async function saveSession(context) {
try {
if (!fs.existsSync(SESSION_DIR)) fs.mkdirSync(SESSION_DIR, { recursive: true });
fs.writeFileSync(SESSION_FILE, JSON.stringify({ storageState: await context.storageState() }, null, 2));
} catch {}
}
(async () => {
const launchArgs = ['--no-sandbox', '--disable-dev-shm-usage', '--ignore-certificate-errors'];
if (shouldBypassProxy(BASE)) {
launchArgs.push('--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*');
console.log(' → 检测到私网目标,浏览器将直连,不走系统代理');
}
const browser = await chromium.launch({
headless: HEADLESS,
args: launchArgs
});
let context = await loadSession(browser);
let needLogin = true;
if (context) {
const testPage = await context.newPage();
await testPage.goto(BASE + '/sim_5gc/ue/index', { waitUntil: 'domcontentloaded', timeout: 10000 }).catch(() => {});
if (!testPage.url().includes('/login')) needLogin = false;
await testPage.close();
}
if (!context) {
context = await browser.newContext({ ignoreHTTPSErrors: true });
}
const page = await context.newPage();
// 登录
if (needLogin) {
await page.goto(BASE + '/login', { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill(LOGIN_EMAIL);
await page.getByRole('textbox', { name: '密码' }).fill(LOGIN_PWD);
await page.getByRole('checkbox', { name: '记住我' }).check();
await page.getByRole('button', { name: '登录' }).click();
await page.waitForLoadState('networkidle');
await saveSession(context);
}
console.log(' ✓ 登录成功');
// 选择工程
const ok = await selectProject(page, args.project);
if (!ok) throw new Error('工程选择失败');
console.log(' ✓ 工程已选: ' + args.project);
// 直接打开 UE 添加页面(/sim_5gc/ue/edit)
await page.goto(BASE + '/sim_5gc/ue/edit', { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForTimeout(3000);
console.log(' ✓ UE 编辑页已打开');
// ===== 填写表单 =====
// 名称
await page.locator('input[name="name"]').fill(args.name);
await page.waitForTimeout(100);
// 数量(必填)
await page.locator('input[name="count"]').fill(args.count || '1');
// MCC
await page.locator('input[name="mcc"]').fill(args.mcc);
// MNC
await page.locator('input[name="mnc"]').fill(args.mnc);
// 勾选所有加密算法(NEA0, NEA1, NEA2, NEA3)和完整性算法(NIA0, NIA1, NIA2, NIA3)
// layui 隐藏 input → 用 page.evaluate() 直接设 checked + 触发 change 事件
await page.evaluate(() => {
const eas = document.querySelectorAll('input[name="ea[]"]');
for (const cb of eas) {
if (!cb.checked) {
cb.checked = true;
cb.dispatchEvent(new Event('change', { bubbles: true }));
}
}
const ias = document.querySelectorAll('input[name="ia[]"]');
for (const cb of ias) {
if (!cb.checked) {
cb.checked = true;
cb.dispatchEvent(new Event('change', { bubbles: true }));
}
}
});
await page.waitForTimeout(200);
console.log(' → 已勾选全部加密/完整性算法');
// 起始 IMSI(必填)
await page.locator('input[name="s_imsi"]').fill(args.imsi);
// KI(key,必填)
await page.locator('input[name="key"]').fill(args.key);
// OPc 类型选择(layui select: 点击 .layui-form-select,再选 dd)
// 找到 op_opc_tp 对应的 layui-select(通过兄弟元素或位置)
const opcSelectWrapper = page.locator('.layui-form-select').filter({ has: page.locator('select[name="op_opc_tp"]') });
if (await opcSelectWrapper.count() > 0) {
await opcSelectWrapper.click();
await page.waitForTimeout(500);
await page.locator('dd[lay-value="opc"]').first().click();
await page.waitForTimeout(200);
}
// OPc 值
await page.locator('input[name="op_opc"]').fill(args.opc);
// IMEISV(必填)
await page.locator('input[name="imeisv"]').fill(args.imeisv || '8611111111111111');
// MSISDN(必填)
await page.locator('input[name="msisdn"]').fill(args.msisdn);
// ===== NSSAI 配置:点击"添加NSSAI"按钮添加一行 =====
const nssaiAddBtn = page.locator('button.nssaiAdd');
if (await nssaiAddBtn.count() > 0) {
await nssaiAddBtn.click();
await page.waitForTimeout(500);
console.log(' → NSSAI 行已添加');
}
// 填写 NSSAI(SST 和 SD)
const nssaiSst = page.locator('input[name="nssai[snssai_sst][]"]');
if (await nssaiSst.count() > 0) {
await nssaiSst.first().fill(args.sst);
}
const nssaiSd = page.locator('input[name="nssai[snssai_sd][]"]');
if (await nssaiSd.count() > 0) {
await nssaiSd.first().fill(args.sd);
}
console.log(' → NSSAI: SST=' + args.sst + ', SD=' + args.sd);
await page.waitForTimeout(500);
// ===== 提交:layui form,用 JS 点击提交按钮 =====
console.log(' → 提交表单...');
await page.evaluate(() => {
const btn = document.querySelector('button[lay-filter="formDemo"]');
if (btn) btn.click();
});
await page.waitForTimeout(5000);
// 判断是否成功:URL 变成 /ue/index,或者停留在 /ue/edit 但有空值
const finalUrl = page.url();
console.log(' → 提交后 URL:', finalUrl);
// 判断是否成功:跳转到列表验证
await page.goto(BASE + '/sim_5gc/ue/index', { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForTimeout(2000);
const rows = await page.locator('.layui-table tbody tr').all();
let found = false;
for (const row of rows) {
const text = await row.innerText().catch(() => '');
if (text.includes(args.name)) {
found = true;
break;
}
}
if (found) {
console.log(' ✅ UE 添加成功: ' + args.name);
} else {
console.log(' ⚠ UE 添加后列表中未找到(可能在其他页)');
}
await browser.close();
console.log('完成');
})().catch(e => { console.error('异常:', e.message); process.exit(1); });
/**
* 选择工程(jsgrid 列表,点击控制列图标)
*/
async function selectProject(page, projectName) {
await page.goto(BASE_URL + '/sim_5gc/project/index', { waitUntil: 'networkidle', timeout: 15000 });
try {
await page.waitForSelector('.jsgrid-row, .jsgrid-alt-row', { timeout: 10000 });
} catch {
console.log(' ⚠ 等待 jsgrid 行超时');
}
await page.waitForTimeout(2000);
for (let pageNum = 1; pageNum <= 20; pageNum++) {
const result = await page.evaluate((target) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === target) {
if (row.classList.contains('jsgrid-selected-row')) return 'already-selected';
const controlCell = row.querySelector('.jsgrid-cell.jsgrid-control-field');
if (controlCell) {
const icon = controlCell.querySelector('.layui-icon');
if (icon) { icon.click(); return 'clicked'; }
}
row.click();
return 'clicked';
}
}
return 'not-found';
}, projectName);
if (result === 'already-selected') {
await page.waitForTimeout(2000);
return true;
}
if (result === 'clicked') {
await page.waitForTimeout(3000);
return true;
}
// 翻页
const nextBtn = page.locator('.jsgrid-pager a').filter({ hasText: 'Next' }).first();
if (!(await nextBtn.count())) break;
try {
await page.evaluate(() => {
var links = document.querySelectorAll('.jsgrid-pager a');
for (var i = 0; i < links.length; i++) {
if (links[i].innerText.trim() === 'Next') { links[i].click(); break; }
}
});
await page.waitForTimeout(2000);
} catch { break; }
}
return false;
}
FILE:scripts/ue-edit-skill.js
/**
* UE 编辑脚本 - ue-edit-skill.js
*
* 单个编辑: node ue-edit-skill.js --id <ue_id> [--field <字段名> --value <值>]
* node ue-edit-skill.js --name <ue_name> --set-<field> <value>
*
* 批量编辑: node ue-edit-skill.js --project <工程> --set-<field> <value>
*
* 可编辑字段: name, mcc, mnc, s_imsi, key, op_opc, imeisv, msisdn,
* user_sip_ip_v4, user_sip_ip_v6, replay_ip, replay_port,
* nssai_sst, nssai_sd 等
*
* 示例:
* node ue-edit-skill.js --id 10337 --set-msisdn 8611111111112
* node ue-edit-skill.js --name UE_TEST_001 --set-msisdn 8611111111112
* node ue-edit-skill.js --project XW_S5GC_1 --set-msisdn 8611111111112
*/
const { chromium } = require('playwright');
const BASE_URL = 'https://192.168.3.89';
const LOGIN_EMAIL = '[email protected]';
const LOGIN_PWD = 'dotouch';
// 解析 --set-<field> <value> 参数
function parseSetArgs(argv) {
const sets = {};
for (let i = 0; i < argv.length; i++) {
if (argv[i] === '--set-msisdn') sets.msisdn = argv[++i];
else if (argv[i] === '--set-imsi') sets.s_imsi = argv[++i];
else if (argv[i] === '--set-mcc') sets.mcc = argv[++i];
else if (argv[i] === '--set-mnc') sets.mnc = argv[++i];
else if (argv[i] === '--set-key') sets.key = argv[++i];
else if (argv[i] === '--set-opc') sets.op_opc = argv[++i];
else if (argv[i] === '--set-imeisv') sets.imeisv = argv[++i];
else if (argv[i] === '--set-name') sets.name = argv[++i];
else if (argv[i] === '--set-sst') sets.nssai_sst = argv[++i];
else if (argv[i] === '--set-sd') sets.nssai_sd = argv[++i];
else if (argv[i] === '--set-user_sip_ip_v4') sets.user_sip_ip_v4 = argv[++i];
else if (argv[i] === '--set-user_sip_ip_v6') sets.user_sip_ip_v6 = argv[++i];
else if (argv[i] === '--set-replay_ip') sets.replay_ip = argv[++i];
else if (argv[i] === '--set-replay_port') sets.replay_port = argv[++i];
else if (argv[i] === '--set-count') sets.count = argv[++i];
}
return sets;
}
const argv = process.argv.slice(2);
const args = {};
let projectExplicit = false; // 标记 project 是否显式传入
for (let i = 0; i < argv.length; i++) {
if (argv[i] === '--id') args.id = argv[++i];
else if (argv[i] === '--name') args.name = argv[++i];
else if (argv[i] === '--project') { args.project = argv[++i]; projectExplicit = true; }
else if (argv[i] === '--url') args.url = argv[++i];
else if (argv[i] === '--headed') args.headed = true;
}
args.sets = parseSetArgs(argv);
args.url = args.url || BASE_URL;
if (!args.id && !args.name && !args.project) {
console.log('用法:');
console.log(' 单个编辑: node ue-edit-skill.js --id <ue_id> --set-<field> <value>');
console.log(' 按名称: node ue-edit-skill.js --name <ue_name> --set-<field> <value>');
console.log(' 批量: node ue-edit-skill.js --project <工程> --set-<field> <value>');
console.log('\n可编辑字段: msisdn, imsi, mcc, mnc, key, opc, imeisv, name, sst, sd, user_sip_ip_v4, user_sip_ip_v6, replay_ip, replay_port, count');
process.exit(1);
}
const BASE = args.url;
const HEADLESS = !args.headed;
console.log('▶ UE 编辑');
console.log(' URL:', BASE);
if (args.id) console.log(' UE ID:', args.id);
if (args.name) console.log(' UE 名称:', args.name);
if (args.project) console.log(' 工程:', args.project);
console.log(' 修改字段:', args.sets);
(async () => {
const browser = await chromium.launch({ headless: HEADLESS, args: ['--no-sandbox','--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const context = await browser.newContext({ ignoreHTTPSErrors: true });
const page = await context.newPage();
// 登录
await page.goto(BASE + '/login', { waitUntil: 'networkidle', timeout: 15000 });
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill(LOGIN_EMAIL);
await page.getByRole('textbox', { name: '密码' }).fill(LOGIN_PWD);
await page.getByRole('checkbox', { name: '记住我' }).check();
await page.getByRole('button', { name: '登录' }).click();
await page.waitForLoadState('networkidle');
console.log(' ✓ 登录成功');
// 选择工程(单个编辑和批量编辑都需要)
if (args.project) {
const ok = await selectProject(page, args.project);
if (!ok) throw new Error('工程选择失败: ' + args.project);
console.log(' ✓ 工程已选:', args.project);
} else if (!args.id && !args.name) {
// 既没有 --id/--name 也没有 --project,报错
console.error('用法: node ue-edit-skill.js --id <ue_id> --set-<field> <value>');
console.error(' node ue-edit-skill.js --project <工程> --set-<field> <value>');
process.exit(1);
}
// ===== 单个编辑 =====
if (args.id || args.name) {
let ueId = args.id;
if (!ueId) {
ueId = await findUeIdByName(page, args.name);
if (!ueId) throw new Error('未找到 UE: ' + args.name);
console.log(' ✓ 找到 UE ID:', ueId);
}
await editUeById(page, ueId, args.sets);
} else if (projectExplicit) {
// ===== 批量编辑: 仅在显式传入 --project 时执行 =====
const count = await bulkEditUe(page, args.sets, args.name || null);
console.log(' ✓ 批量编辑完成,共', count, '个 UE');
}
await browser.close();
console.log('完成');
})().catch(e => { console.error('异常:', e.message); process.exit(1); });
/**
* 填写 UE 表单字段
*/
async function fillUeForm(page, sets) {
// 基本字段
if (sets.name) { await page.locator('input[name="name"]').fill(sets.name); await page.waitForTimeout(100); }
if (sets.count) { await page.locator('input[name="count"]').fill(sets.count); await page.waitForTimeout(100); }
if (sets.mcc) { await page.locator('input[name="mcc"]').fill(sets.mcc); await page.waitForTimeout(100); }
if (sets.mnc) { await page.locator('input[name="mnc"]').fill(sets.mnc); await page.waitForTimeout(100); }
if (sets.s_imsi) { await page.locator('input[name="s_imsi"]').fill(sets.s_imsi); await page.waitForTimeout(100); }
if (sets.key) { await page.locator('input[name="key"]').fill(sets.key); await page.waitForTimeout(100); }
if (sets.op_opc) { await page.locator('input[name="op_opc"]').fill(sets.op_opc); await page.waitForTimeout(100); }
if (sets.imeisv) { await page.locator('input[name="imeisv"]').fill(sets.imeisv); await page.waitForTimeout(100); }
if (sets.msisdn) { await page.locator('input[name="msisdn"]').fill(sets.msisdn); await page.waitForTimeout(100); }
if (sets.user_sip_ip_v4) { await page.locator('input[name="user_sip_ip_v4"]').fill(sets.user_sip_ip_v4); await page.waitForTimeout(100); }
if (sets.user_sip_ip_v6) { await page.locator('input[name="user_sip_ip_v6"]').fill(sets.user_sip_ip_v6); await page.waitForTimeout(100); }
if (sets.replay_ip) { await page.locator('input[name="replay_ip"]').fill(sets.replay_ip); await page.waitForTimeout(100); }
if (sets.replay_port) { await page.locator('input[name="replay_port"]').fill(sets.replay_port); await page.waitForTimeout(100); }
// NSSAI
if (sets.nssai_sst || sets.nssai_sd) {
const nssaiAddBtn = page.locator('button.nssaiAdd');
if (await nssaiAddBtn.count() > 0) {
await nssaiAddBtn.click();
await page.waitForTimeout(500);
}
if (sets.nssai_sst) {
const sst = page.locator('input[name="nssai[snssai_sst][]"]');
if (await sst.count() > 0) await sst.first().fill(sets.nssai_sst);
}
if (sets.nssai_sd) {
const sd = page.locator('input[name="nssai[snssai_sd][]"]');
if (await sd.count() > 0) await sd.first().fill(sets.nssai_sd);
}
}
}
/**
* 通过 ID 编辑单个 UE
*/
async function editUeById(page, ueId, sets) {
// 确保工程上下文:先进入 UE 列表
await page.goto(BASE_URL + '/sim_5gc/ue/index', { waitUntil: 'networkidle', timeout: 15000 });
await page.waitForTimeout(3000);
const url0 = page.url();
// 如果在工程选择页,说明需要先选工程
if (url0.includes('/project/index')) {
await selectProject(page, 'XW_S5GC_1');
console.log(' ✓ 工程已选: XW_S5GC_1');
await page.waitForTimeout(1000);
}
await page.goto(BASE_URL + '/sim_5gc/ue/edit/' + ueId, { waitUntil: 'networkidle', timeout: 15000 });
await page.waitForTimeout(3000);
const url = page.url();
if (!url.includes('/ue/edit/')) {
console.log(' ⚠ UE ID ' + ueId + ' 不存在(URL: ' + url + ')');
return;
}
await fillUeForm(page, sets);
await page.waitForTimeout(500);
// 提交
await page.evaluate(() => {
const btn = document.querySelector('button[lay-filter="formDemo"]');
if (btn) btn.click();
});
await page.waitForTimeout(4000);
const finalUrl = page.url();
if (finalUrl.includes('/ue/index')) {
console.log(' ✓ UE ID', ueId, '修改成功');
} else {
const err = await page.locator('.layui-layer-msg').innerText().catch(() => '');
console.log(' ⚠ UE ID', ueId, '修改后 URL:', finalUrl, err ? '错误: ' + err : '');
}
}
/**
* 在 UE 列表中根据名称找 ID
*/
async function findUeIdByName(page, name) {
await page.goto(BASE_URL + '/sim_5gc/ue/index', { waitUntil: 'networkidle', timeout: 15000 });
await page.waitForTimeout(3000);
const rows = await page.locator('.layui-table tbody tr').all();
for (const row of rows) {
const cells = await row.locator('td').all();
if (cells.length >= 3) {
const cell2 = await cells[2].innerText().catch(() => '');
if (cell2.trim() === name) {
// ID 在 cells[1]
const idCell = await cells[1].innerText().catch(() => '');
return idCell.trim();
}
}
}
return null;
}
/**
* 批量编辑: 对工程下所有 UE 进行字段修改
* @param {string|null} nameFilter - 可选,按名称过滤
*/
async function bulkEditUe(page, sets, nameFilter = null) {
await page.goto(BASE_URL + '/sim_5gc/ue/index', { waitUntil: 'networkidle', timeout: 15000 });
await page.waitForTimeout(3000);
const rows = await page.locator('.layui-table tbody tr').all();
let count = 0;
const processedIds = new Set();
for (const row of rows) {
const cells = await row.locator('td').all();
if (cells.length < 3) continue;
const idCell = await cells[1].innerText().catch(() => '');
const nameCell = await cells[2].innerText().catch(() => '');
const id = idCell.trim();
const name = nameCell.trim();
// 跳过表头、空行、按钮行、或已处理过的 ID
if (!id || id === 'ID' || !name || name === '编辑' || processedIds.has(id)) continue;
// 如果指定了 nameFilter,只处理该名称的 UE
if (nameFilter && name !== nameFilter) continue;
processedIds.add(id);
await editUeById(page, id, sets);
count++;
}
return count;
}
/**
* 选择工程
*/
async function selectProject(page, projectName) {
await page.goto(BASE_URL + '/sim_5gc/project/index', { waitUntil: 'networkidle', timeout: 15000 });
try { await page.waitForSelector('.jsgrid-row, .jsgrid-alt-row', { timeout: 10000 }); } catch {}
await page.waitForTimeout(2000);
for (let pageNum = 1; pageNum <= 20; pageNum++) {
const result = await page.evaluate((target) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === target) {
if (row.classList.contains('jsgrid-selected-row')) return 'already-selected';
const controlCell = row.querySelector('.jsgrid-cell.jsgrid-control-field');
if (controlCell) {
const icon = controlCell.querySelector('.layui-icon');
if (icon) { icon.click(); return 'clicked'; }
}
row.click();
return 'clicked';
}
}
return 'not-found';
}, projectName);
if (result === 'already-selected') { await page.waitForTimeout(2000); return true; }
if (result === 'clicked') { await page.waitForTimeout(3000); return true; }
const nextBtn = page.locator('.jsgrid-pager a').filter({ hasText: 'Next' }).first();
if (!(await nextBtn.count())) break;
try {
// Use JS click instead of Playwright click for jsgrid pagination reliability
await page.evaluate(() => {
const links = document.querySelectorAll('.jsgrid-pager a');
for (const link of links) {
if (link.innerText.trim() === 'Next') { link.click(); break; }
}
});
await page.waitForTimeout(2000);
} catch { break; }
}
return false;
}
FILE:scripts/ue_add.sh
#!/bin/bash
cd /home/dotouch/.openclaw/workspace/skills/5gc/scripts
node ue-add-skill.js --name UE_REG_TEST --project XW_S5GC_1 --imsi 460010000000001 --key 0000111122223333 --opc 00000000000000000000000000000000 --sqn 000000000001
FILE:scripts/upf-add-skill.js
#!/usr/bin/env node
/**
* UPF/PGW-U 添加脚本
* 完整流程:登录 → 选工程 → 填表单 → 点NSSAI行按钮添加一行 → 填NSSAI/TAC → 提交
* 用法: node upf-add-skill.js <名称> [--project <工程>] [--url <地址>] [--headed] \
* [--n4_ip <IP>] [--n3_ip <IP>] [--n6_ip <IP>] [--n4_port <端口>] \
* [--MCC <值>] [--MNC <值>] [--pdu_capacity <数量>] \
* [--ue_min <IP>] [--ue_max <IP>]
* 示例: node upf-add-skill.js UPF-TEST --project XW_S5GC_1
* node upf-add-skill.js UPF-PROD --n4_ip 10.0.0.50 --n6_ip 10.0.0.51 --MCC 460 --MNC 01
*/
const { chromium } = require('playwright');
const fs = require('fs');
const path = require('path');
const BASE_URL = 'https://192.168.3.89';
const CONFIG = {
urls: { login: '/login', upfEdit: '/sim_5gc/upf/edit', upfList: '/sim_5gc/upf/index' },
credentials: { email: '[email protected]', password: 'dotouch' },
sessionDir: path.join(__dirname, '.sessions'),
getSessionFile() {
return `5gc_session_\/\//, '').replace(/\./g, '_').json`;
}
};
class SessionManager {
constructor() {
if (!fs.existsSync(CONFIG.sessionDir)) fs.mkdirSync(CONFIG.sessionDir, { recursive: true });
this.sp = path.join(CONFIG.sessionDir, CONFIG.getSessionFile());
}
async loadSession(browser) {
if (!fs.existsSync(this.sp)) return null;
const { storageState } = JSON.parse(fs.readFileSync(this.sp, 'utf8'));
return await browser.newContext({ storageState, ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
}
async saveSession(context) {
fs.writeFileSync(this.sp, JSON.stringify({ storageState: await context.storageState() }, null, 2));
}
}
// 选择工程(jsgrid 分页遍历,精确匹配,点击 checkbox)
async function selectProject(page, projectName) {
await page.goto(`BASE_URL/sim_5gc/project/index`, { waitUntil: 'networkidle', timeout: 15000 });
await page.waitForSelector('.jsgrid-row, .jsgrid-alt-row', { timeout: 5000 }).catch(() => {});
for (let pageNum = 1; pageNum <= 20; pageNum++) {
const result = await page.evaluate((targetName) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === targetName) {
const checkbox = cells[0].querySelector('input[type="checkbox"]');
if (checkbox) { checkbox.click(); return 'clicked'; }
const icon = cells[1].querySelector('.iconfont');
if (icon) { icon.click(); return 'icon-clicked'; }
}
}
return 'not-found';
}, projectName);
if (result === 'clicked' || result === 'icon-clicked') {
await page.waitForTimeout(2000);
return true;
}
// 点下一页
const nextBtn = page.locator('.jsgrid-pager a').filter({ hasText: 'Next' }).first();
if (!(await nextBtn.count())) break;
try {
await page.evaluate(() => {
var links = document.querySelectorAll('.jsgrid-pager a');
for (var i = 0; i < links.length; i++) {
if (links[i].innerText.trim() === 'Next') { links[i].click(); break; }
}
});
await page.waitForTimeout(2000);
} catch { break; }
}
console.log(` ❌ 未找到工程 "projectName"`);
return false;
}
// 添加 UPF 主流程
async function addUpf(upfName, projectName, upfConfig = {}) {
const startTime = Date.now();
const sessionMgr = new SessionManager();
const browser = await chromium.launch({ headless: true, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const context = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await context.newPage();
// 登录
await page.goto(`BASE_URLCONFIG.urls.login`, { waitUntil: 'networkidle' });
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill(CONFIG.credentials.email);
await page.getByRole('textbox', { name: '密码' }).fill(CONFIG.credentials.password);
await page.getByRole('checkbox', { name: '记住我' }).check();
await page.getByRole('button', { name: '登录' }).click();
await page.waitForLoadState('networkidle');
await sessionMgr.saveSession(context);
console.log(' ✓ 登录成功');
// 选工程(通过 jsgrid 精确匹配 + 点击 iconfont)
const ok = await selectProject(page, projectName);
if (!ok) throw new Error(`工程 "projectName" 未找到`);
console.log(` ✓ 工程 "projectName" 已选`);
await page.waitForTimeout(1000);
// 直接导航到 UPF 列表页(工程已在 sidebar 激活)
await page.goto(`BASE_URL/sim_5gc/upf/index`, { waitUntil: 'networkidle' });
await page.waitForTimeout(2000);
console.log(' ✓ 进入 UPF/PGW-U');
// 点添加
await page.getByRole('button', { name: '添加' }).click();
await page.waitForTimeout(1500);
console.log(' ✓ 点添加');
// === 填基本字段 ===
await page.evaluate((name) => {
const setVal = (n, v) => {
const el = document.querySelector(`input[name="n"]`);
if (!el) return;
el.value = v;
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
};
setVal('name', name);
}, upfName);
// type: select → 1=仿真设备(layui隐藏了原select,用JS设值)
await page.evaluate(() => {
const sel = document.querySelector('select[name="type"]');
if (sel) {
sel.value = '1';
sel.dispatchEvent(new Event('change', { bubbles: true }));
}
});
console.log(' ✓ 类型已选');
// 基本网络字段
const cfg = {
n4_ip: upfConfig.n4_ip || '192.168.20.30',
n4_port: upfConfig.n4_port || '8805',
n3_ip: upfConfig.n3_ip || '192.168.20.30',
n6_ip: upfConfig.n6_ip || '192.168.20.31',
pdu_capacity: upfConfig.pdu_capacity || '20000',
ue_min: upfConfig.ue_min || '20.20.20.20',
ue_max: upfConfig.ue_max || '20.20.60.20',
};
await page.evaluate((c) => {
const setVal = (n, v) => {
const el = document.querySelector(`input[name="n"]`);
if (!el) return;
el.value = v;
el.dispatchEvent(new Event('input', { bubbles: true }));
};
setVal('n4_ip', c.n4_ip);
setVal('n4_port', c.n4_port);
setVal('n3_ip', c.n3_ip);
setVal('n6_ip', c.n6_ip);
setVal('pdu_capacity', c.pdu_capacity);
setVal('ue_min', c.ue_min);
setVal('ue_max', c.ue_max);
}, cfg);
console.log(' ✓ 基本字段已填');
// MCC/MNC(通过 page.evaluate 设置,因为 layui 需要事件触发)
if (upfConfig.MCC || upfConfig.MNC) {
await page.evaluate((c) => {
const setVal = (n, v) => {
const el = document.querySelector(`input[name="n"]`);
if (!el) return;
el.value = v;
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
};
setVal('MCC', c.MCC || '460');
setVal('MNC', c.MNC || '01');
}, { MCC: upfConfig.MCC, MNC: upfConfig.MNC });
console.log(` ✓ MCC=upfConfig.MCC || '460' MNC=upfConfig.MNC || '01'`);
}
// === NSSAI / TAC 配置 ===
// 点击"数量*nssai*类型*TAC起始*"行末尾的"刷新/添加"按钮(添加一行)
await page.getByRole('row', { name: /\* 数量.*nssai.*类型.*TAC起始/ }).locator('button').click();
await page.waitForTimeout(800);
console.log(' ✓ NSSAI 行已添加');
// 填写 config 字段
await page.evaluate(() => {
const setVal = (n, v) => {
const el = document.querySelector(`input[name="n"]`);
if (!el) return;
el.value = v;
el.dispatchEvent(new Event('input', { bubbles: true }));
};
setVal('config[count][]', '1');
setVal('config[nssai][]', '1');
setVal('config[stac][]', '101');
setVal('config[etac][]', '102');
});
console.log(' ✓ NSSAI/TAC 已填');
// === 提交 ===
await page.getByRole('button', { name: '提交' }).click();
console.log(' ✓ 已提交');
await page.waitForTimeout(3000);
const finalUrl = page.url();
console.log(' 最终 URL:', finalUrl);
let success = finalUrl.includes('upf/index');
if (success) {
console.log(' ✅ 跳转成功 - UPF 添加完成');
} else {
const txt = await page.evaluate(() => document.body.innerText.slice(0, 300));
console.log(' 页面文本:', txt.replace(/\s+/g, ' ').slice(0, 200));
await page.screenshot({ path: 'add-result.png' });
}
// 验证列表中是否存在
await page.reload({ waitUntil: 'networkidle' });
await page.waitForTimeout(3000);
// UPF 表:ID在第2列(cells[1]),名称在第3列(cells[2])
const debug = await page.evaluate((targetName) => {
const allRows = document.querySelectorAll('table tbody tr');
const entries = [];
for (const row of allRows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3) {
entries.push(cells[2].textContent.trim()); // 名称列
}
}
return { url: window.location.href, entries: entries.slice(0, 20), found: entries.includes(targetName) };
}, upfName);
console.log('列表调试 - URL:', debug.url);
console.log('UPF 列表:', debug.entries.join(', '));
console.log(debug.found ? `✅ UPF "upfName" 已出现在列表中!` : `⚠ UPF "upfName" 不在列表中`);
await browser.close();
const totalTime = (Date.now() - startTime) / 1000;
return { success, upfName, totalTime };
}
// 主函数
async function main() {
const args = process.argv.slice(2);
if (args.length === 0) {
console.log('用法: node upf-add-skill.js <UPF名称> [--project <工程名>] [--url <地址>] [--headed]');
console.log(' [--n4_ip <IP>] [--n3_ip <IP>] [--n6_ip <IP>]');
console.log(' [--MCC <值>] [--MNC <值>] [--pdu_capacity <数量>]');
console.log(' [--ue_min <IP>] [--ue_max <IP>]');
console.log('示例: node upf-add-skill.js UPF-TEST --project XW_S5GC_1');
console.log(' node upf-add-skill.js UPF-PROD --n4_ip 10.0.0.50 --n6_ip 10.0.0.51 --MCC 460 --MNC 01');
process.exit(1);
}
let upfName = null, projectName = 'XW_S5GC_1', explicitProject = false;
let urlOverride = null;
const upfConfig = {
n4_ip: '192.168.20.30',
n4_port: '8805',
n3_ip: '192.168.20.30',
n6_ip: '192.168.20.31',
MCC: '460',
MNC: '01',
pdu_capacity: '20000',
ue_min: '20.20.20.20',
ue_max: '20.20.60.20',
};
for (let i = 0; i < args.length; i++) {
if (!args[i].startsWith('-')) upfName = args[i];
else if (args[i] === '--project' || args[i] === '-p') { projectName = args[++i]; explicitProject = true; }
else if (args[i] === '--url') { urlOverride = args[++i]; }
else if (args[i] === '--n4_ip') { upfConfig.n4_ip = args[++i]; }
else if (args[i] === '--n4_port') { upfConfig.n4_port = args[++i]; }
else if (args[i] === '--n3_ip') { upfConfig.n3_ip = args[++i]; }
else if (args[i] === '--n6_ip') { upfConfig.n6_ip = args[++i]; }
else if (args[i] === '--MCC') { upfConfig.MCC = args[++i]; }
else if (args[i] === '--MNC') { upfConfig.MNC = args[++i]; }
else if (args[i] === '--pdu_capacity') { upfConfig.pdu_capacity = args[++i]; }
else if (args[i] === '--ue_min') { upfConfig.ue_min = args[++i]; }
else if (args[i] === '--ue_max') { upfConfig.ue_max = args[++i]; }
}
if (!upfName) { console.error('错误: 请指定 UPF 名称'); process.exit(1); }
if (urlOverride) {
if (!urlOverride.startsWith('http')) urlOverride = 'https://' + urlOverride;
}
const targetUrl = urlOverride || BASE_URL;
console.log(`▶ 添加 UPF: upfName | 工程: projectName | 地址: targetUrl`);
console.log(` n4_ip=upfConfig.n4_ip n3_ip=upfConfig.n3_ip n6_ip=upfConfig.n6_ip MCC=upfConfig.MCC MNC=upfConfig.MNC`);
try {
process.env.UPF_ADD_BASE_URL = targetUrl;
const result = await addUpf(upfName, projectName, upfConfig);
console.log(`\n总耗时: result.totalTime.toFixed(1)s`);
process.exit(result.success ? 0 : 1);
} catch (err) {
console.error('异常:', err.message);
process.exit(1);
}
}
main();
FILE:scripts/upf-edit-skill.js
#!/usr/bin/env node
/**
* UPF/PGW-U 编辑脚本(单条 + 批量二合一) - 生产版
* 用法: node upf-edit-skill.js [--project <工程名称>] --set-<字段名> <值>
* 示例: node upf-edit-skill.js --project XW_S5GC_basic \
* --set-n4_ip 9.9.9.9 --set-n3_ip 3.3.3.3
*
* 脚本会在指定工程下遍历所有 UPF,逐个打开编辑页面并写入提供的字段值。
*/
const { chromium } = require('playwright');
const fs = require('fs');
const path = require('path');
let BASE_URL = 'https://192.168.3.89';
const CONFIG = {
urls: { login: '/login', upfList: '/sim_5gc/upf/index' },
credentials: { email: '[email protected]', password: 'dotouch' },
sessionDir: path.join(__dirname, '.sessions'),
getSessionFile() {
return `5gc_upf_bulk_\/\//, '').replace(/\./g, '_').json`;
}
};
class SessionManager {
constructor() {
if (!fs.existsSync(CONFIG.sessionDir)) fs.mkdirSync(CONFIG.sessionDir, { recursive: true });
this.sp = path.join(CONFIG.sessionDir, CONFIG.getSessionFile());
}
async loadSession(browser) {
if (!fs.existsSync(this.sp)) return null;
const { storageState } = JSON.parse(fs.readFileSync(this.sp, 'utf8'));
return await browser.newContext({ storageState, ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
}
async saveSession(context) {
fs.writeFileSync(this.sp, JSON.stringify({ storageState: await context.storageState() }, null, 2));
}
}
// 选择工程(点击目标工程行内的 .layui-icon 图标)
async function selectProject(page, projectName) {
await page.goto(`BASE_URL/sim_5gc/project/index`, { waitUntil: 'networkidle', timeout: 15000 });
// 等待 jsgrid 表格渲染完成(最多 10 秒)
try {
await page.waitForSelector('.jsgrid-row, .jsgrid-alt-row', { timeout: 10000 });
} catch {
console.log(' ⚠ 等待 jsgrid 行超时');
}
await page.waitForTimeout(2000); // 额外等待确保页面完全就绪
for (let pageNum = 1; pageNum <= 20; pageNum++) {
// 在当前页查找目标工程行
const result = await page.evaluate((target) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === target) {
// 只有同时具备 'jsgrid-selected-row' class 才认为已选中
if (row.classList.contains('jsgrid-selected-row')) {
return 'already-selected';
}
// 未选中,点击该行控制列中的 .layui-icon 图标进行切换
const controlCell = row.querySelector('.jsgrid-cell.jsgrid-control-field');
if (controlCell) {
const icon = controlCell.querySelector('.layui-icon');
if (icon) {
icon.click();
return 'clicked';
}
}
// 备用:直接点击行
row.click();
return 'clicked';
}
}
return 'not-found';
}, projectName);
if (result === 'already-selected') {
console.log(' ✓ 工程已在第一行(已选中状态),跳过');
await page.waitForTimeout(2000);
return true;
}
if (result === 'clicked') {
console.log(' ✓ 已点击工程行图标,等待切换...');
await page.waitForTimeout(3000); // 等待工程切换(行会跑到第一行)
return true;
}
// 当前页未找到,尝试翻页
const nextBtn = page.locator('.jsgrid-pager a').filter({ hasText: 'Next' }).first();
if (!(await nextBtn.count())) {
console.log(' ⚠ 已到最后一页,仍未找到工程');
break;
}
try {
await nextBtn.click({ force: true });
await page.waitForTimeout(2000);
} catch {
break;
}
}
console.log(` ❌ 未找到工程 "projectName"`);
return false;
}
/**
* 获取当前列表页所有 UPF 的 ID
*/
async function fetchAllUpfIds(page, nameFilter = null) {
// 表格结构: 第2列为 ID,第3列为名称(去重)
return await page.evaluate((filter) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
const ids = [];
const seen = new Set();
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3) {
const id = cells[1].textContent.trim();
const name = cells[2].textContent.trim();
if (id && !seen.has(id) && (!filter || name === filter)) {
seen.add(id);
ids.push(id);
}
}
}
return ids;
}, nameFilter);
}
/**
* 对单个 UPF 执行编辑操作
*/
async function editSingleUpf(page, upfId, edits) {
// 只修改用户指定的字段,不触碰其他字段
const editUrl = `BASE_URL/sim_5gc/upf/edit/upfId`;
await page.goto(editUrl, { waitUntil: 'networkidle' });
await page.waitForTimeout(2000);
// 使用 page.fill 方式写入字段(点击 → 输入 → 触发完整事件链)
for (const [field, val] of Object.entries(edits)) {
const selector = `input[name="field"]`;
try {
await page.waitForSelector(selector, { state: 'visible', timeout: 5000 });
await page.click(selector, { force: true });
await page.fill(selector, val);
console.log(` ✓ field → val`);
} catch {
console.log(` ⚠ 字段 field 未找到或无法填写`);
}
}
console.log(' ✓ 字段修改完成');
// 提交
await page.getByRole('button', { name: '提交' }).click();
await page.waitForTimeout(3000);
// 通过是否回到列表页判断成功
const success = page.url().includes('upf/index');
if (!success) console.log(' ⚠ 提交后未返回列表,可能失败');
return success;
}
async function main() {
// 参数解析
const args = process.argv.slice(2);
let projectName = 'XW_S5GC_1';
let nameFilter = null;
const edits = {};
let headless = true; // 默认无头模式
for (let i = 0; i < args.length; i++) {
if (args[i] === '--project') {
projectName = args[++i];
} else if (args[i] === '--name') {
nameFilter = args[++i];
} else if (args[i] === '--headed') {
headless = false;
} else if (args[i].startsWith('--set-')) {
// '--set-' is 5 chars, the field name starts after that. Slice from index 6 to drop the extra '-'
const field = args[i].slice(6);
edits[field] = args[++i];
} else if (!args[i].startsWith('-')) {
if (!nameFilter) nameFilter = args[i];
}
}
if (Object.keys(edits).length === 0) {
console.log('用法: node upf-edit-skill.js [--project <工程>] [--headed] --set-<字段> <值>');
console.log('示例: node upf-edit-skill.js --project XW_S5GC_basic --set-n4_ip 9.9.9.9');
console.log(' --headed 打开浏览器窗口(调试用)');
process.exit(1);
}
console.log(`▶ 批量编辑 UPF,工程: projectName' + nameFilter : ''`);
console.log(` 修改字段:`, edits);
console.log(` 模式: '有头'`);
const sessionMgr = new SessionManager();
const browser = await chromium.launch({ headless, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const context = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await context.newPage();
// 登录
await page.goto(`BASE_URLCONFIG.urls.login`, { waitUntil: 'networkidle' });
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill(CONFIG.credentials.email);
await page.getByRole('textbox', { name: '密码' }).fill(CONFIG.credentials.password);
await page.getByRole('checkbox', { name: '记住我' }).check();
await page.getByRole('button', { name: '登录' }).click();
await page.waitForLoadState('networkidle');
await sessionMgr.saveSession(context);
console.log(' ✓ 登录成功');
// 选工程
const projOk = await selectProject(page, projectName);
if (!projOk) throw new Error('工程选择失败');
console.log(' ✓ 工程已选');
// 进入 UPF 列表
await page.goto(`BASE_URLCONFIG.urls.upfList`, { waitUntil: 'networkidle' });
await page.waitForTimeout(2000);
// 读取全部 UPF ID
const upfIds = await fetchAllUpfIds(page, nameFilter);
console.log(` found upfIds.length UPF(s) in project "projectName"`);
let successCount = 0;
for (let idx = 0; idx < upfIds.length; idx++) {
const id = upfIds[idx];
console.log(`\n[idx + 1/upfIds.length] 编辑 UPF ID: id`);
try {
const ok = await editSingleUpf(page, id, edits);
if (ok) {
console.log(' ✅ 编辑成功');
successCount++;
} else {
console.log(' ⚠ 编辑后未返回列表,可能失败');
}
} catch (e) {
console.log(' ❌ 异常:', e.message);
}
// 防止触发风控,稍作延迟
await page.waitForTimeout(1500);
}
console.log('\n=== 完成 ===');
console.log(`成功编辑 successCount / upfIds.length 个 UPF`);
await browser.close();
}
main().catch(e => { console.error('异常:', e.message); process.exit(1); });
FILE:scripts/verify_final.js
/**
* verify_final.js - 最终验证
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
}
async function selectProject(page, projectName) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.evaluate((name) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === name) {
const icon = cells[1].querySelector('.iconfont');
if (icon) { icon.click(); return true; }
}
}
return false;
}, projectName);
await page.waitForTimeout(3000);
}
async function main() {
const browser = await chromium.launch({ headless: true, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, 'XW_SUPF_5_1_2_4');
// 1. 验证 smpolicy default 的 pccRules
await page.goto(`globalBaseUrl/sim_5gc/smpolicy/default/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const smpolicy = await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 6) {
return {
id: cells[1].textContent.trim(),
name: cells[2].textContent.trim(),
pccRules: cells[4].textContent.trim(),
};
}
}
return null;
});
console.log('\n========== 验证结果 ==========\n');
console.log('📋 sm_policy_default:');
console.log(` ID: smpolicy.id`);
console.log(` 名称: smpolicy.name`);
console.log(` pccRules: smpolicy.pccRules`);
const hasPccDefaultTest = smpolicy.pccRules.includes('pcc_default_test');
console.log(` ✅ pcc_default_test 已添加: '否'`);
// 2. 验证 qos3
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/qos/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const qos = await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
// cells[2] = qosId, cells[4]=maxbrUl, cells[5]=maxbrDl, cells[6]=gbrUl, cells[7]=gbrDl
for (let i = 0; i < cells.length; i++) {
if (cells[i].textContent.trim() === 'qos3') {
return {
id: cells[1].textContent.trim(),
qosId: cells[2].textContent.trim(),
qi: cells[3].textContent.trim(),
maxbrUl: cells[4].textContent.trim(),
maxbrDl: cells[5].textContent.trim(),
gbrUl: cells[6].textContent.trim(),
gbrDl: cells[7].textContent.trim(),
};
}
}
}
return null;
});
console.log('\n📋 qos3 QoS模板:');
console.log(` ID: qos.id`);
console.log(` qosId: qos.qosId`);
console.log(` 5qi: qos.qi`);
console.log(` maxbrUl: qos.maxbrUl`);
console.log(` maxbrDl: qos.maxbrDl`);
console.log(` gbrUl: qos.gbrUl`);
console.log(` gbrDl: qos.gbrDl`);
const qosMatch =
qos.maxbrUl === '10000000' &&
qos.maxbrDl === '20000000' &&
qos.gbrUl === '5000000' &&
qos.gbrDl === '5000000';
console.log(` ✅ QoS参数匹配: '否'`);
// 3. 验证 pcc_default_test 使用的 QoS
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/pcc/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const pcc = await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 10 && cells[2].textContent.trim() === 'pcc_default_test') {
return {
id: cells[1].textContent.trim(),
qosId: cells[3].textContent.trim(),
precedence: cells[4].textContent.trim(),
};
}
}
return null;
});
console.log('\n📋 pcc_default_test PCC规则:');
console.log(` ID: pcc.id`);
console.log(` 使用的QoS模板: pcc.qosId || '(通过名称引用)'`);
console.log(` precedence: pcc.precedence`);
console.log('\n========== 总结 ==========\n');
if (hasPccDefaultTest && qosMatch) {
console.log('✅✅✅ 任务完成!');
console.log(' 1. qos3 QoS模板已创建,参数正确');
console.log(' 2. pcc_default_test PCC规则已创建,使用qos3');
console.log(' 3. pcc_default_test 已添加到 sm_policy_default 的 pccRules');
console.log('\n PCF默认规则现已包含 pcc_default_test,其QoS参数:');
console.log(' - maxbrUl = 10000000');
console.log(' - maxbrDl = 20000000');
console.log(' - gbrUl = 5000000');
console.log(' - gbrDl = 5000000');
} else {
console.log('❌ 部分验证未通过');
}
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/verify_pcc_qos.js
/**
* verify_pcc_qos.js - 验证 pcc_default_test 的 QoS 值
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, projectName) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
const clicked = await page.evaluate((name) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === name) {
const icon = cells[1].querySelector('.iconfont');
if (icon) { icon.click(); return true; }
}
}
return false;
}, projectName);
if (!clicked) { console.log('❌ 未找到工程'); process.exit(1); }
await page.waitForTimeout(3000);
console.log(`✅ 工程 "projectName" 已选`);
}
async function main() {
const browser = await chromium.launch({ headless: false, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, 'XW_SUPF_5_1_2_4');
// 导航到 PCC 列表
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/pcc/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
// 找到 pcc_default_test 的行
const pccRow = await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 10 && cells[2].textContent.trim() === 'pcc_default_test') {
return {
id: cells[1].textContent.trim(),
qosId: cells[3].textContent.trim(),
precedence: cells[4].textContent.trim(),
};
}
}
return null;
});
if (pccRow) {
console.log('\n📋 pcc_default_test 信息:');
console.log(` ID: pccRow.id`);
console.log(` qosId(使用的QoS模板): pccRow.qosId`);
console.log(` precedence: pccRow.precedence`);
}
// 导航到 QoS 列表查看 qos3 的详细信息
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/qos/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
// 找到 qos3
const qosRow = await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
for (const cell of cells) {
if (cell.textContent.trim() === 'qos3') {
// 返回整行
return Array.from(cells).map(c => c.textContent.trim());
}
}
}
return null;
});
if (qosRow) {
console.log('\n📋 qos3 QoS模板信息:');
console.log(` qosId: qosRow[1]`);
console.log(` 5qi: qosRow[2]`);
console.log(` maxbrUl: qosRow[3]`);
console.log(` maxbrDl: qosRow[4]`);
console.log(` gbrUl: qosRow[5]`);
console.log(` gbrDl: qosRow[6]`);
// 验证值
const expected = { maxbrUl: '10000000', maxbrDl: '20000000', gbrUl: '5000000', gbrDl: '5000000' };
const actual = { maxbrUl: qosRow[3], maxbrDl: qosRow[4], gbrUl: qosRow[5], gbrDl: qosRow[6] };
console.log('\n✅ 验证结果:');
let allMatch = true;
for (const [key, val] of Object.entries(expected)) {
const match = actual[key] === val;
console.log(` key: 期望=val, 实际=actual[key] '❌'`);
if (!match) allMatch = false;
}
if (allMatch) {
console.log('\n🎉 所有 QoS 参数配置正确!');
}
}
await page.screenshot({ path: 'verify_result.png', fullPage: true });
console.log('\n📸 截图: verify_result.png');
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:test_results/openclaw-exec-kill-repro-2026-04-16.md
# OpenClaw exec 长任务异常复现报告
日期:2026-04-16
环境:Ubuntu 24.04 / OpenClaw 2026.4.14 / control-ui(webchat)
历史对比:旧版本 3.23-2 未出现该问题
## 结论
当前版本通过 control-ui / webchat 发起的 `exec` 长任务存在异常终止问题,表现为任务在约 5-10 秒或稍后被 `SIGTERM` / `SIGKILL`,与文档中 `tools.exec.timeoutSec` 默认 1800 秒的预期不符。
该问题已通过最小复现证明与业务脚本无关:纯 shell / 纯 Python 长任务同样会被提前终止。
## 排查结论
已排除:
- OOM / 内存不足
- cgroup memory 限制
- systemd MemoryMax/CPUQuota 限制
- 5GC 业务脚本本身导致
- Playwright 特有问题
- 站点可达性问题
已确认:
- 当前 `exec` 任务作为 `openclaw-gateway` 子进程运行,位于同一 control-group
- 当前版本这条 control-ui/webchat → gateway → exec 的执行链路,对中长任务存在异常回收/终止行为
## 最小复现
### 复现 1:纯 shell 长任务
```bash
#!/usr/bin/env bash
set -euo pipefail
for i in $(seq 1 24); do
echo "tick:$i $(date '+%F %T')"
sleep 5
done
echo done
```
实际结果:
- 输出 `tick:1 ...`
- 随后被 `SIGTERM`
### 复现 2:纯 Python 长任务
```bash
python3 - <<'PY'
import time
for i in range(1,61):
print(f'pytick:{i}', flush=True)
time.sleep(1)
print('done', flush=True)
PY
```
实际结果:
- 输出到 `pytick:9/10`
- 随后被 `SIGKILL`
## 关联公开 issue
- openclaw/openclaw#66359
- 标题:`[Bug] exec commands receive SIGKILL frequently (every conversation turn)`
- 版本:2026.4.12
- 症状与本地现象高度相似
- openclaw/openclaw#66748 / #66749
- 与 subagent background exec / heartbeat wake 相关
- 说明 2026.4.x 期间 background exec 链路存在活跃改动
## 当前可行绕过方案
对于长任务(例如 Playwright 批量回归、批量编辑、长时间自动化):
- 不要继续通过当前聊天 exec 路径运行
- 改用 detached 执行方式(nohup / setsid / systemd-run / 独立 shell)
- 或等待上游修复后再回归 chat exec 路径
## 建议
1. 短期:用 detached runner 恢复长任务可用性
2. 中期:结合 #66359 向上游补充 Linux/control-ui 的最小复现
3. 如需稳定生产使用,可考虑临时回退到 3.23-2 或避开当前 exec 链路
FILE:test_results/report_1776309510682.json
{
"ts": "2026-04-16T03:25:53.204Z",
"results": [
{
"tag": "AMF添加",
"entity": "amf",
"action": "add",
"headed": false,
"args": [
"--name",
"AMF_TEST_1776309510682",
"--project",
"XW_S5GC_1",
"--sbi-ip",
"10.200.1.1",
"--mcc",
"460",
"--mnc",
"01",
"--sst",
"1",
"--sd",
"000001"
],
"pass": false,
"detail": "\n▶ 5GC AMF add\n → node amf-add-skill.js --name AMF_TEST_1776309510682 --project XW_S5GC_1 --sbi-ip 10.200.1.1 --mcc 460 --mnc 01 --sst 1 --sd 000001\n\n错误: 请指定 AMF 名称\n"
},
{
"tag": "AMF编辑",
"entity": "amf",
"action": "edit",
"headed": false,
"args": [
"--name",
"AMF_TEST_1776309510682",
"--project",
"XW_S5GC_1",
"--set-sbi_ip",
"10.200.1.100"
],
"pass": false,
"detail": "\n▶ 5GC AMF edit\n → node amf-edit-skill.js --name AMF_TEST_1776309510682 --project XW_S5GC_1 --set-sbi_ip 10.200.1.100\n\n🎯 任务配置:\n AMF名称: AMF_TEST_1776309510682\n 工程选择: XW_S5GC_1 (用户指定)\n 修改字段: 1 个\n 修改详情:\n - sbi_ip: 10.200.1.100\n============================================================\n🚀 AMF"
},
{
"tag": "UDM添加",
"entity": "udm",
"action": "add",
"headed": false,
"args": [
"--name",
"UDM_TEST_1776309510682",
"--project",
"XW_S5GC_1",
"--auth-supi",
"460001776309510682000000",
"--auth-op-type",
"opc",
"--opc",
"63b3c4d1d5c03f4e82c5f4a5b2e7d9c1"
],
"pass": false,
"detail": "\n▶ 5GC UDM add\n → node ausf-udm-add-skill.js --name UDM_TEST_1776309510682 --project XW_S5GC_1 --auth-supi 460001776309510682000000 --auth-op-type opc --opc 63b3c4d1d5c03f4e82c5f4a5b2e7d9c1\n\n用法: node ausf-udm-add-skill.js <名称> [--project 工程] [--url 地址] [--sip IP] [--port N]\n"
},
{
"tag": "UDM编辑",
"entity": "udm",
"action": "edit",
"headed": false,
"args": [
"--name",
"UDM_TEST_1776309510682",
"--project",
"XW_S5GC_1",
"--set-op_opc",
"aaaaaaaaaaaabbbbccccccccccccdddd"
],
"pass": false,
"detail": "\n▶ 5GC UDM edit\n → node ausf-udm-edit-skill.js --name UDM_TEST_1776309510682 --project XW_S5GC_1 --set-op_opc aaaaaaaaaaaabbbbccccccccccccdddd\n\n▶ AUSF/UDM 编辑 工程: XW_S5GC_1 字段: {\"op_opc\":\"aaaaaaaaaaaabbbbccccccccccccdddd\"}\n 🔐 登录...\n ✅ 登录成功\n ✅ 工程已选\n ❌ 未找到: UDM_TEST_1776309510682\n"
},
{
"tag": "SMF添加",
"entity": "smf",
"action": "add",
"headed": false,
"args": [
"--name",
"SMF_TEST_1776309510682",
"--project",
"XW_S5GC_1",
"--pfcp-ip",
"10.200.2.1",
"--n3-ip",
"10.200.2.2",
"--n6-ip",
"10.200.2.3",
"--dnn",
"internet"
],
"pass": true,
"detail": "\n▶ 5GC SMF add\n → node smf-pgwc-add-skill.js --name SMF_TEST_1776309510682 --project XW_S5GC_1 --pfcp-ip 10.200.2.1 --n3-ip 10.200.2.2 --n6-ip 10.200.2.3 --dnn internet\n\n▶ 添加 SMF: --name\n 模式: 无头\n 工程: XW_S5GC_1\n pfcp_sip=200.20.20.25 http2_sip=200.20.20.25 mcc=460 mnc=01 pdu_capacity=200000\n ✓ 登"
},
{
"tag": "SMF编辑",
"entity": "smf",
"action": "edit",
"headed": false,
"args": [
"--name",
"SMF_TEST_1776309510682",
"--project",
"XW_S5GC_1",
"--set-dnn",
"internet_updated"
],
"pass": true,
"detail": "\n▶ 5GC SMF edit\n → node smf-pgwc-edit-skill.js --name SMF_TEST_1776309510682 --project XW_S5GC_1 --set-dnn internet_updated\n\n▶ 批量编辑 SMF,工程: XW_S5GC_1,名称: SMF_TEST_1776309510682\n 修改字段: { dnn: 'internet_updated' }\n 模式: 无头\n ✓ 登录成功\n ✓ 已点击工程行图标,等待切换...\n ✓ 工程已选\n ⚠ 工程下没有 SMF\n"
},
{
"tag": "SMF批量编辑",
"entity": "smf",
"action": "edit",
"headed": false,
"args": [
"--project",
"XW_S5GC_1",
"--set-dnn",
"internet"
],
"pass": true,
"detail": "\n▶ 5GC SMF edit\n → node smf-pgwc-edit-skill.js --project XW_S5GC_1 --set-dnn internet\n\n▶ 批量编辑 SMF,工程: XW_S5GC_1\n 修改字段: { dnn: 'internet' }\n 模式: 无头\n ✓ 登录成功\n ✓ 已点击工程行图标,等待切换...\n ✓ 工程已选\n found 6 SMF(s) in project \"XW_S5GC_1\"\n\n[1/6] 编辑 SMF ID: 7384\n ⚠ 字段 dnn 未找到或无法填写\n ✓ 字段修改完成\n\n[2/6] 编辑 SMF ID:"
},
{
"tag": "UPF添加",
"entity": "upf",
"action": "add",
"headed": false,
"args": [
"--name",
"UPF_TEST_1776309510682",
"--project",
"XW_S5GC_1",
"--n4-ip",
"10.200.3.1",
"--n3-ip",
"10.200.3.2",
"--n6-ip",
"10.200.3.3",
"--dnn",
"internet"
],
"pass": false,
"detail": "\n▶ 5GC UPF add\n → node upf-add-skill.js --name UPF_TEST_1776309510682 --project XW_S5GC_1 --n4-ip 10.200.3.1 --n3-ip 10.200.3.2 --n6-ip 10.200.3.3 --dnn internet\n\n▶ 添加 UPF: internet | 工程: XW_S5GC_1 | 地址: https://192.168.3.89\n n4_ip=192.168.20.30 n3_ip=192.168.20.30 n6_ip=192.168.20.31 MCC=460 "
},
{
"tag": "UPF编辑",
"entity": "upf",
"action": "edit",
"headed": false,
"args": [
"--name",
"UPF_TEST_1776309510682",
"--project",
"XW_S5GC_1",
"--set-n4_ip",
"10.200.3.100"
],
"pass": true,
"detail": "\n▶ 5GC UPF edit\n → node upf-edit-skill.js --name UPF_TEST_1776309510682 --project XW_S5GC_1 --set-n4_ip 10.200.3.100\n\n▶ 批量编辑 UPF,工程: XW_S5GC_1,名称: UPF_TEST_1776309510682\n 修改字段: { n4_ip: '10.200.3.100' }\n 模式: 无头\n ✓ 登录成功\n ✓ 已点击工程行图标,等待切换...\n ✓ 工程已选\n found 0 UPF(s) in project \"XW_S5GC_1\"\n\n=== 完成"
},
{
"tag": "UPF批量编辑",
"entity": "upf",
"action": "edit",
"headed": false,
"args": [
"--project",
"XW_S5GC_1",
"--set-n4_ip",
"10.200.3.1"
],
"pass": true,
"detail": "\n▶ 5GC UPF edit\n → node upf-edit-skill.js --project XW_S5GC_1 --set-n4_ip 10.200.3.1\n\n▶ 批量编辑 UPF,工程: XW_S5GC_1\n 修改字段: { n4_ip: '10.200.3.1' }\n 模式: 无头\n ✓ 登录成功\n ✓ 已点击工程行图标,等待切换...\n ✓ 工程已选\n found 10 UPF(s) in project \"XW_S5GC_1\"\n\n[1/10] 编辑 UPF ID: 11159\n ✓ n4_ip → 10.200.3.1\n ✓ 字段修改完成\n ✅ 编辑成功"
},
{
"tag": "GNB添加",
"entity": "gnb",
"action": "add",
"headed": false,
"args": [
"--name",
"GNB_TEST_1776309510682",
"--project",
"XW_S5GC_1",
"--count",
"1",
"--ngap-ip",
"10.200.4.1",
"--user-sip-ip-v4",
"10.200.4.2",
"--mcc",
"460",
"--mnc",
"01",
"--stac",
"1",
"--etac",
"100",
"--node-id",
"gni_000004"
],
"pass": false,
"detail": "\n▶ 5GC GNB add\n → node gnb-add-skill.js --name GNB_TEST_1776309510682 --project XW_S5GC_1 --count 1 --ngap-ip 10.200.4.1 --user-sip-ip-v4 10.200.4.2 --mcc 460 --mnc 01 --stac 1 --etac 100 --node-id gni_000004\n\n▶ 添加 GNB: --name\n ngap_sip=200.20.20.50 user_sip_ip_v4=2.2.2.2 mcc=460 mnc=01\n TAC=1~10"
},
{
"tag": "GNB编辑",
"entity": "gnb",
"action": "edit",
"headed": false,
"args": [
"--name",
"GNB_TEST_1776309510682",
"--project",
"XW_S5GC_1",
"--set-replay_ip",
"10.200.4.200"
],
"pass": true,
"detail": "\n▶ 5GC GNB edit\n → node gnb-edit-skill.js --name GNB_TEST_1776309510682 --project XW_S5GC_1 --set-replay_ip 10.200.4.200\n\n▶ 批量编辑 GNB,工程: XW_S5GC_1,名称: GNB_TEST_1776309510682\n 修改: replay_ip = 10.200.4.200\n 模式: 无头\n ✓ 登录成功\n ✓ 已点击工程行图标,等待切换...\n ✓ 工程已选\n ⚠ 工程下没有 GNB\n"
},
{
"tag": "GNB批量编辑",
"entity": "gnb",
"action": "edit",
"headed": false,
"args": [
"--project",
"XW_S5GC_1",
"--set-replay_ip",
"10.200.4.200"
],
"pass": true,
"detail": "\n▶ 5GC GNB edit\n → node gnb-edit-skill.js --project XW_S5GC_1 --set-replay_ip 10.200.4.200\n\n▶ 批量编辑 GNB,工程: XW_S5GC_1\n 修改: replay_ip = 10.200.4.200\n 模式: 无头\n ✓ 登录成功\n ✓ 已点击工程行图标,等待切换...\n ✓ 工程已选\n ⚠ 工程下没有 GNB\n"
},
{
"tag": "UE添加",
"entity": "ue",
"action": "add",
"headed": false,
"args": [
"--name",
"UE_TEST_1776309510682",
"--imsi",
"460001776309510682",
"--msisdn",
"8613888888888",
"--mcc",
"460",
"--mnc",
"01"
],
"pass": false,
"detail": "\n▶ 5GC UE add\n → node ue-add-skill.js --name UE_TEST_1776309510682 --imsi 460001776309510682 --msisdn 8613888888888 --mcc 460 --mnc 01\n\n▶ 添加 UE: UE_TEST_1776309510682\n URL: https://192.168.3.89\n 工程: XW_S5GC_1\n异常: page.goto: Timeout 15000ms exceeded.\nCall log:\n - navigating to \"https://192.168.3."
},
{
"tag": "UE编辑",
"entity": "ue",
"action": "edit",
"headed": false,
"args": [
"--project",
"XW_S5GC_1",
"--name",
"UE_TEST_1776309510682",
"--set-msisdn",
"8613888889999"
],
"pass": false,
"detail": "\n▶ 5GC UE edit\n → node ue-edit-skill.js --project XW_S5GC_1 --name UE_TEST_1776309510682 --set-msisdn 8613888889999\n\n▶ UE 编辑\n URL: https://192.168.3.89\n UE 名称: UE_TEST_1776309510682\n 工程: XW_S5GC_1\n 修改字段: { msisdn: '8613888889999' }\n ✓ 登录成功\n ✓ 工程已选: XW_S5GC_1\n异常: 未找到 UE: UE_TEST_1776309510682\n"
},
{
"tag": "UE批量编辑",
"entity": "ue",
"action": "edit",
"headed": false,
"args": [
"--project",
"XW_S5GC_1",
"--set-msisdn",
"8613888888888"
],
"pass": true,
"detail": "\n▶ 5GC UE edit\n → node ue-edit-skill.js --project XW_S5GC_1 --set-msisdn 8613888888888\n\n▶ UE 编辑\n URL: https://192.168.3.89\n 工程: XW_S5GC_1\n 修改字段: { msisdn: '8613888888888' }\n ✓ 登录成功\n ✓ 工程已选: XW_S5GC_1\n ✓ 批量编辑完成,共 0 个 UE\n完成\n"
}
]
}5GC Web仪表自动化技能,支持AMF/UDM/AUSF/SMF/PGW-C/UPF/PGW-U/GNB/UE/PCF/NRF的批量添加与编辑
--- name: 5gc-web-dotouch version: 1.0.0 description: 5GC Web仪表自动化技能,支持AMF/UDM/AUSF/SMF/PGW-C/UPF/PGW-U/GNB/UE/PCF/NRF的批量添加与编辑 author: liuwei120 tags: [5gc, automation, playwright, network] --- # 5GC Web 仪表自动化技能 > 统一管理 AMF、UDM/AUSF、SMF/PGW-C、UPF/PGW-U、GNB、UE、PCF、NRF 八类网元的添加与编辑操作。 --- ## 目录 - [快速开始](#快速开始) - [统一 CLI 入口](#统一-cli-入口) - [技能详细文档](#技能详细文档) - [AMF](#amf) - [UDM/AUSF](#udmausf) - [SMF/PGW-C](#smfpgw-c) - [UPF/PGW-U](#upfpgw-u) - [GNB](#gnb) - [UE](#ue) - [全局参数参考](#全局参数参考) - [字段参考](#字段参考) --- ## 快速开始 ### 安装方法 技能目录位于 `skills/5gc/`,由统一入口 `5gc.js` 统一调度,无需额外安装: ```bash # 克隆或复制到本机 git clone <repo> ~/.openclaw/workspace/skills/5gc # 直接使用统一入口(推荐) node skills/5gc/scripts/5gc.js <entity> <action> [options] # 或直接调用各脚本 node skills/5gc/scripts/amf-add-skill.js <参数> ``` ### 前置要求 - Node.js ≥ 14 - Playwright(`npm i playwright && npx playwright install chromium`) - 5GC 仪表地址:`https://192.168.3.89`(默认) - 登录凭证:`[email protected]` / `dotouch` - 仪表上已创建对应工程(如 `XW_S5GC_1`) ### 会话缓存 所有脚本自动复用 Playwright 会话缓存(`.sessions/` 目录),首次登录后再次运行无需重复登录。 --- ## 统一 CLI 入口 ### 路径 ``` node skills/5gc/scripts/5gc.js <entity> <action> [options] ``` ### 支持的网元与操作 | entity | add | edit | |--------|-----|------| | `amf` | ✅ | ✅ | | `udm` | ✅ | ✅ | | `smf` | ✅ | ✅ | | `upf` | ✅ | ✅ | | `gnb` | ✅ | ✅ | | `ue` | ✅ | ✅ | | `pcf` | ✅ | ✅ | | `nrf` | ✅ | ✅ | ### 全局选项 | 选项 | 说明 | |------|------| | `--url <地址>` | 5GC 仪表地址,默认 `https://192.168.3.89` | | `--headed` | 打开可见浏览器窗口(调试用) | ### 三种使用模式 ```bash # 1. 添加网元 node 5gc.js amf add <名称> [参数...] # 2. 批量编辑(当前工程下所有该类网元) node 5gc.js amf edit --project <工程> --set-<字段> <值> # 3. 单个编辑(按名称精确匹配) node 5gc.js amf edit --name <名称> --project <工程> --set-<字段> <值> ``` --- ## 技能详细文档 --- ### AMF #### amf-add-skill.js **功能**:在指定工程下添加一个 AMF 实例。 **使用方式**: ```bash node 5gc.js amf add <名称> [选项...] # 或直接调用 node skills/5gc/scripts/amf-add-skill.js <名称> [选项...] ``` **参数**: | 参数 | 说明 | 默认值 | |------|------|--------| | `<名称>` | AMF 实例名称(位置参数) | **必填** | | `--project <工程>` / `-p <工程>` | 目标工程名称 | `5G_basic_process` | | `--url <地址>` | 5GC 仪表地址 | `https://192.168.3.89` | | `--mcc <值>` | MCC(移动国家码) | `460` | | `--mnc <值>` | MNC(移动网络码) | `01` | | `--ngap_sip <IP>` | NGAP 信令面 IP | `200.20.20.1` | | `--ngap_port <端口>` | NGAP 端口 | `38412` | | `--http2_sip <IP>` | HTTP2 服务 IP | `200.20.20.5` | | `--http2_port <端口>` | HTTP2 端口 | `8080` | | `--stac <值>` | 起始 TAC | `101` | | `--etac <值>` | 结束 TAC | `102` | | `--region_id <值>` | 区域 ID | `1` | | `--set_id <值>` | Set ID | `1` | | `--pointer <值>` | 指针 | `1` | | `--headed` | 打开可见浏览器 | false | **示例**: ```bash # 基本添加 node 5gc.js amf add AMF_TEST --project XW_S5GC_1 # 指定 NGAP IP 和端口 node 5gc.js amf add AMF_PROD --project XW_S5GC_1 --ngap_sip 10.200.1.50 --ngap_port 38412 # 使用不同 MCC/MNC node 5gc.js amf add AMF_CMCC --project XW_S5GC_1 --mcc 460 --mnc 00 ``` --- #### amf-edit-skill.js **功能**:修改 AMF 配置参数。支持单个修改或批量修改工程下所有 AMF。 **使用方式**: ```bash node 5gc.js amf edit [选项...] ``` **参数**: | 参数 | 说明 | |------|------| | `--project <工程>` / `-p <工程>` | 目标工程,不带 `--name` 时批量修改该工程下所有 AMF | | `--name <名称>` | 精确匹配要修改的 AMF 名称 | | `--id <ID>` | 按 AMF ID 修改 | | `--set-<字段> <值>` | 修改指定字段的值,支持多组 | | `--url <地址>` | 5GC 仪表地址 | | `--headed` | 打开可见浏览器 | **可编辑字段**:`name`, `mcc`, `mnc`, `ngap_sip`, `ngap_port`, `http2_sip`, `http2_port`, `stac`, `etac`, `region_id`, `set_id`, `pointer`, `ea[NEA0]`, `ea[128-NEA1]`, `ea[128-NEA2]`, `ea[128-NEA3]`, `ia[NIA0]`, `ia[128-NIA1]`, `ia[128-NIA2]`, `ia[128-NIA3]` > ⚠️ `ea[NEA0]` 等算法字段:实际向表单填入字段名 `ea[NEA0]`(input[name="ea[NEA0]"]),layui checkbox 点击基于索引而非字段名,详情见 SKILL.md 算法配置章节。 **示例**: ```bash # 批量修改工程下所有 AMF 的 NGAP IP node 5gc.js amf edit --project XW_S5GC_1 --set-ngap_sip 10.200.1.99 # 修改指定 AMF node 5gc.js amf edit --name AMF_TEST --project XW_S5GC_1 --set-ngap_sip 10.200.1.50 --set-http2_sip 10.200.1.51 # 按 ID 修改 node 5gc.js amf edit --id 6633 --set-ngap_port 38413 ``` --- ### UDM/AUSF #### ausf-udm-add-skill.js **功能**:在指定工程下添加一个 UDM/AUSF 实例。 **使用方式**: ```bash node 5gc.js udm add <名称> [选项...] ``` **参数**: | 参数 | 说明 | 默认值 | |------|------|--------| | `<名称>` | UDM 实例名称(位置参数) | **必填** | | `--project <工程>` / `-p <工程>` | 目标工程名称 | `5G_basic_process` | | `--url <地址>` | 5GC 仪表地址 | `https://192.168.3.89` | | `--count <数量>` | 实例数量 | `1` | | `--sip <IP>` | SIP 服务 IP | `192.168.20.30` | | `--port <端口>` | SIP 端口 | `80` | | `--auth_method <方法>` | 认证方法 | `5G_AKA` | | `--scheme <协议>` | 协议类型 | `HTTP` | | `--priority <优先级>` | 优先级 | `8` | | `--headed` | 打开可见浏览器 | false | **示例**: ```bash # 基本添加 node 5gc.js udm add UDM_TEST --project XW_S5GC_1 # 指定 SIP IP 和端口 node 5gc.js udm add UDM_PROD --project XW_S5GC_1 --sip 10.0.0.100 --port 8080 # 批量添加 3 个实例 node 5gc.js udm add UDM_CLUSTER --project XW_S5GC_1 --count 3 --sip 10.0.0.50 ``` --- #### ausf-udm-edit-skill.js **功能**:修改 UDM/AUSF 配置参数。支持批量和单个修改。 **使用方式**: ```bash node 5gc.js udm edit [选项...] ``` **参数**: | 参数 | 说明 | |------|------| | `--project <工程>` | 目标工程,不带 `--name` 时批量修改 | | `--name <名称>` | 精确匹配要修改的 UDM 名称 | | `--set-sip <IP>` | 修改 SIP IP | | `--set-port <端口>` | 修改端口 | | `--set-auth_method <方法>` | 修改认证方法 | | `--set-scheme <协议>` | 修改协议 | | `--set-count <数量>` | 修改实例数量 | | `--url <地址>` | 5GC 仪表地址 | | `--headed` | 打开可见浏览器 | **示例**: ```bash # 批量修改工程下所有 UDM 的 SIP IP node 5gc.js udm edit --project XW_S5GC_1 --set-sip 10.0.0.99 # 修改指定 UDM node 5gc.js udm edit --name UDM_TEST --project XW_S5GC_1 --set-sip 10.0.0.88 --set-port 8080 ``` --- ### SMF/PGW-C #### smf-pgwc-add-skill.js **功能**:在指定工程下添加一个 SMF/PGW-C 实例。 **使用方式**: ```bash node 5gc.js smf add <名称> [选项...] ``` **参数**: | 参数 | 说明 | 默认值 | |------|------|--------| | `<名称>` | SMF 实例名称(位置参数) | **必填** | | `--project <工程>` / `-p <工程>` | 目标工程名称 | `XW_S5GC_1` | | `--url <地址>` | 5GC 仪表地址 | `https://192.168.3.89` | | `--pfcp_sip <IP>` | PFCP 信令面 IP | `200.20.20.25` | | `--http2_sip <IP>` | HTTP2 服务 IP | `200.20.20.25` | | `--mcc <值>` | MCC | `460` | | `--mnc <值>` | MNC | `01` | | `--pdu_capacity <数量>` | PDU 会话容量 | `200000` | | `--ue_min <IP>` | UE IP 池起始 | `30.30.30.20` | | `--ue_max <IP>` | UE IP 池结束 | `30.31.30.20` | | `--interest_tac <TAC列表>` | 关注 TAC 列表(逗号分隔) | `101,102` | | `--headed` | 打开可见浏览器 | false | > ⚠️ dnn、snssai_sst、snssai_sd 在 SMF 表单中不存在或非 input 字段,无需配置。 **示例**: ```bash # 基本添加 node 5gc.js smf add SMF_TEST --project XW_S5GC_1 # 指定工程和 IP/MCC node 5gc.js smf add SMF_PROD --project XW_S5GC_1 --pfcp_sip 10.10.10.50 --http2_sip 10.10.10.51 --mcc 460 --mnc 01 ``` --- #### smf-pgwc-edit-skill.js **功能**:修改 SMF/PGW-C 配置参数。支持批量和单个修改。 **使用方式**: ```bash node 5gc.js smf edit [选项...] ``` **参数**: | 参数 | 说明 | |------|------| | `--project <工程>` | 目标工程,不带 `--name` 时批量修改 | | `--name <名称>` | 精确匹配要修改的 SMF 名称 | | `--set-pfcp_sip <IP>` | 修改 PFCP 信令面 IP | | `--set-http2_sip <IP>` | 修改 HTTP2 服务 IP | | `--set-mcc <值>` | 修改 MCC | | `--set-mnc <值>` | 修改 MNC | | `--set-pdu_capacity <数量>` | 修改 PDU 会话容量 | | `--set-ue_min <IP>` | 修改 UE IP 池起始 | | `--set-ue_max <IP>` | 修改 UE IP 池结束 | | `--set-interest_tac <TAC列表>` | 修改关注 TAC 列表(逗号分隔) | > ⚠️ 以下字段在 SMF 表单中不存在或不是标准 input:dnn、n3_ip、n6_ip、snssai_sst、snssai_sd。如需修改,请通过仪表 UI 手动完成或直接编辑数据库。 **示例**: ```bash # 批量修改工程下所有 SMF 的 HTTP2 IP node 5gc.js smf edit --project XW_S5GC_1 --set-http2_sip 10.10.10.99 # 修改指定 SMF 的 pfcp_sip 和 MCC/MNC node 5gc.js smf edit --name SMF_TEST --project XW_S5GC_1 --set-pfcp_sip 10.10.10.88 --set-mcc 460 --set-mnc 01 ``` --- ### UPF/PGW-U #### upf-add-skill.js **功能**:在指定工程下添加一个 UPF/PGW-U 实例。 **使用方式**: ```bash node 5gc.js upf add <名称> [选项...] ``` **参数**: | 参数 | 说明 | 默认值 | |------|------|--------| | `<名称>` | UPF 实例名称(位置参数) | **必填** | | `--project <工程>` / `-p <工程>` | 目标工程名称 | `XW_S5GC_1` | | `--url <地址>` | 5GC 仪表地址 | `https://192.168.3.89` | | `--n4_ip <IP>` | N4 接口 IP | `192.168.20.30` | | `--n3_ip <IP>` | N3 接口 IP | `192.168.20.30` | | `--n6_ip <IP>` | N6 接口 IP | `192.168.20.31` | | `--n4_port <端口>` | N4 端口 | `8805` | | `--MCC <值>` | MCC(注意大写) | `460` | | `--MNC <值>` | MNC(注意大写) | `01` | | `--pdu_capacity <数量>` | PDU 会话容量 | `20000` | | `--ue_min <IP>` | UE IP 池起始 | `20.20.20.20` | | `--ue_max <IP>` | UE IP 池结束 | `20.20.60.20` | | `--headed` | 打开可见浏览器 | false | > ⚠️ DNN、TAC、NSSAI 在添加脚本中为硬编码默认值,不支持命令行覆盖。如需修改,请使用 `upf edit` 脚本。 **示例**: ```bash # 基本添加 node 5gc.js upf add UPF_TEST --project XW_S5GC_1 # 指定 N4/N3/N6 IP 和 MCC/MNC node 5gc.js upf add UPF_PROD --project XW_S5GC_1 --n4_ip 10.0.0.50 --n6_ip 10.0.0.51 --MCC 460 --MNC 01 ``` --- #### upf-edit-skill.js **功能**:修改 UPF/PGW-U 配置参数。支持批量和单个修改。 **使用方式**: ```bash node 5gc.js upf edit [选项...] ``` **参数**: | 参数 | 说明 | |------|------| | `--project <工程>` | 目标工程,不带 `--name` 时批量修改 | | `--name <名称>` | 精确匹配要修改的 UPF 名称 | | `--set-n3_ip <IP>` | 修改 N3 接口 IP | | `--set-n4_ip <IP>` | 修改 N4 接口 IP | | `--set-n4_port <端口>` | 修改 N4 端口 | | `--set-n6_ip <IP>` | 修改 N6 接口 IP | | `--set-MCC <值>` | 修改 MCC(注意大写) | | `--set-MNC <值>` | 修改 MNC(注意大写) | | `--set-pdu_capacity <数量>` | 修改 PDU 会话容量 | | `--set-ue_min <IP>` | 修改 UE IP 池起始 | | `--set-ue_max <IP>` | 修改 UE IP 池结束 | | `--url <地址>` | 5GC 仪表地址 | | `--headed` | 打开可见浏览器 | > ⚠️ `dnn`(DNN)和 TAC/NSSAI 在 UPF 表单中存储在 jsgrid 配置行内,不支持简单的 `--set-` 修改。 **示例**: ```bash # 批量修改工程下所有 UPF 的 N4 IP node 5gc.js upf edit --project XW_S5GC_1 --set-n4_ip 99.99.99.99 # 修改指定 UPF 的 N4/N6 IP 和 MCC/MNC node 5gc.js upf edit --name UPF_TEST --project XW_S5GC_1 --set-n4_ip 88.88.88.88 --set-n6_ip 88.88.88.89 --set-MCC 460 --set-MNC 01 ``` --- ### GNB #### gnb-add-skill.js **功能**:在指定工程下添加一个 GNB 实例。 **使用方式**: ```bash node 5gc.js gnb add <名称> [选项...] ``` **参数**: | 参数 | 说明 | 默认值 | |------|------|--------| | `<名称>` | GNB 实例名称(位置参数) | **必填** | | `--project <工程>` / `-p <工程>` | 目标工程名称 | `XW_S5GC_1` | | `--url <地址>` | 5GC 仪表地址 | `https://192.168.3.89` | | `--ngap_sip <IP>` | NGAP 信令面 IP | `200.20.20.50` | | `--user_sip_ip_v4 <IP>` | 用户面 IPv4 | `2.2.2.2` | | `--mcc <值>` | MCC | `460` | | `--mnc <值>` | MNC | `60` | | `--stac <值>` | 起始 TAC | `0` | | `--etac <值>` | 结束 TAC | `0` | | `--node_id <ID>` | 节点 ID | `70` | | `--cell_count <数量>` | 小区数量 | `1` | | `--headed` | 打开可见浏览器 | false | > ⚠️ `stac`/`etac`/`node_id` 非默认值时可能触发表单验证失败,建议先使用默认值添加后再用 `gnb edit` 修改。 **示例**: ```bash # 基本添加 node 5gc.js gnb add GNB_TEST --project XW_S5GC_1 # 指定 NGAP IP、用户面 IP 和 TAC node 5gc.js gnb add GNB_PROD --project XW_S5GC_1 --ngap_sip 200.20.20.100 --user_sip_ip_v4 3.3.3.3 --mcc 460 --mnc 60 --stac 1 --etac 10 ``` --- #### gnb-edit-skill.js **功能**:修改 GNB 配置参数。支持批量和单个修改。 **使用方式**: ```bash node 5gc.js gnb edit [选项...] ``` **参数**: | 参数 | 说明 | |------|------| | `--project <工程>` | 目标工程,不带 `--name` 时批量修改 | | `--name <名称>` | 精确匹配要修改的 GNB 名称 | | `--set-ngap_sip <IP>` | 修改 NGAP 信令面 IP | | `--set-user_sip_ip_v4 <IP>` | 修改用户面 IPv4 | | `--set-user_sip_ip_v6 <IP>` | 修改用户面 IPv6 | | `--set-mcc <值>` | 修改 MCC | | `--set-mnc <值>` | 修改 MNC | | `--set-stac <值>` | 修改起始 TAC | | `--set-etac <值>` | 修改结束 TAC | | `--set-node_id <ID>` | 修改节点 ID | | `--set-cell_count <数量>` | 修改小区数量 | | `--set-replay_ip <IP>` | 修改回放 IP | | `--set-replay_port <端口>` | 修改回放端口 | | `--url <地址>` | 5GC 仪表地址 | | `--headed` | 打开可见浏览器 | **示例**: ```bash # 批量修改工程下所有 GNB 的用户面 IP node 5gc.js gnb edit --project XW_S5GC_1 --set-user_sip_ip_v4 99.99.99.99 # 修改指定 GNB 的 NGAP IP 和 MCC/MNC node 5gc.js gnb edit --name GNB_TEST --project XW_S5GC_1 --set-ngap_sip 200.20.20.88 --set-mcc 461 --set-mnc 22 ``` --- ### UE #### ue-add-skill.js **功能**:在指定工程下添加一个或多个 UE 实例。 **使用方式**: ```bash node 5gc.js ue add --name <名称> [选项...] ``` **参数**: | 参数 | 短名 | 说明 | 默认值 | |------|------|------|--------| | `--name <名称>` | `-n <名称>` | UE 名称(只支持字母/数字/下划线) | **必填** | | `--project <工程>` | `-p <工程>` | 目标工程名称 | `XW_S5GC_1` | | `--url <地址>` | `-u <地址>` | 5GC 仪表地址 | `https://192.168.3.89` | | `--imsi <值>` | | 起始 IMSI(15位) | `460001234567890` | | `--msisdn <值>` | | MSISDN(13-15位,以 86 开头) | `8611111111111` | | `--mcc <值>` | | MCC | `460` | | `--mnc <值>` | | MNC | `01` | | `--key <值>` | | KI 密钥(32位 hex) | `1111...`(32个1) | | `--opc <值>` | | OPc 密钥(32位 hex) | `1111...`(32个1) | | `--imeisv <值>` | | IMEISV(偶数位) | `8611111111111111` | | `--sst <值>` | | NSSAI SST | `1` | | `--sd <值>` | | NSSAI SD | `111111` | | `--count <数量>` | `-c <数量>` | 连续添加数量 | `1` | | `--headed` | | 打开可见浏览器 | false | > **命名约束**:UE 名称只能包含字母、数字、下划线(`_`),不能使用连字符(`-`)或其他特殊字符。 **示例**: ```bash # 基本添加 node 5gc.js ue add --name UE_001 --project XW_S5GC_1 # 指定 IMSI 和 MSISDN node 5gc.js ue add --name UE_TEST --imsi 460000000000001 --msisdn 8613888888888 --project XW_S5GC_1 # 批量添加 10 个连续 UE node 5gc.js ue add --name UE_BATCH --count 10 --project XW_S5GC_1 --msisdn 8613900000000 # 指定认证密钥 node 5gc.js ue add --name UE_AUTH --project XW_S5GC_1 --key 00112233445566778899aabbccddeeff --opc 11223344556677889900aabbccddeeff ``` --- #### ue-edit-skill.js **功能**:修改 UE 配置参数。支持批量和单个修改。 **使用方式**: ```bash node 5gc.js ue edit [选项...] ``` **参数**: | 参数 | 说明 | |------|------| | `--project <工程>` | 目标工程,不带 `--name` 时批量修改该工程下所有 UE | | `--name <名称>` | 精确匹配要修改的 UE 名称(不支持批量时按名称过滤) | | `--id <ID>` | 按 UE ID 修改 | | `--set-msisdn <值>` | 修改 MSISDN | | `--set-s_imsi <值>` | 修改 IMSI | | `--set-mcc <值>` | 修改 MCC | | `--set-mnc <值>` | 修改 MNC | | `--set-key <值>` | 修改 KI 密钥 | | `--set-opc <值>` | 修改 OPc 密钥 | | `--set-imeisv <值>` | 修改 IMEISV | | `--set-sst <值>` | 修改 NSSAI SST | | `--set-sd <值>` | 修改 NSSAI SD | | `--set-replay_ip <IP>` | 修改回放 IP | | `--set-replay_port <端口>` | 修改回放端口 | | `--set-count <数量>` | 修改数量 | | `--url <地址>` | 5GC 仪表地址 | | `--headed` | 打开可见浏览器 | > ⚠️ `user_sip_ip_v4`、`user_sip_ip_v6` 在 UE 编辑表单中不存在此字段名,无需修改。 **示例**: ```bash # 批量修改工程下所有 UE 的 MSISDN node 5gc.js ue edit --project XW_S5GC_1 --set-msisdn 8613911111111 # 修改指定 UE node 5gc.js ue edit --name UE_001 --project XW_S5GC_1 --set-msisdn 8613988888888 --set-sst 1 --set-sd 222222 # 按 ID 修改 node 5gc.js ue edit --id 10337 --set-opc aabbccddeeff00112233445566778899 --set-imeisv 8611111111111112 ``` --- ### PCF/PCRF #### pcf-add-skill.js **功能**:在指定工程下添加一个 PCF/PCRF 实例。 **使用方式**: ```bash node 5gc.js pcf add <名称> [选项...] node skills/5gc/scripts/pcf-add-skill.js <名称> [选项...] ``` **参数**: | 参数 | 说明 | 默认值 | |------|------|--------| | `<名称>` | PCF 实例名称(位置参数) | **必填** | | `--project <工程>` / `-p <工程>` | 目标工程名称 | `XW_S5GC_1` | | `--url <地址>` | 5GC 仪表地址 | `https://192.168.3.89` | | `--http2_sip <IP>` | HTTP2 服务 IP | `192.168.20.90` | | `--http2_port <端口>` | HTTP2 端口 | `80` | | `--MCC <值>` | MCC(注意大写) | `460` | | `--MNC <值>` | MNC(注意大写) | `01` | | `--headed` | 打开可见浏览器 | false | **示例**: ```bash node 5gc.js pcf add PCF-TEST --project XW_S5GC_1 node 5gc.js pcf add PCF-PROD --project XW_S5GC_1 --http2_sip 10.0.0.50 --MCC 460 --MNC 01 ``` #### pcf-edit-skill.js **功能**:编辑指定工程下的 PCF/PCRF 实例(支持单条和批量)。 **使用方式**: ```bash # 批量编辑:修改工程下所有 PCF 的字段 node 5gc.js pcf edit --project <工程> --set-<字段> <值> # 单条编辑:修改指定名称的 PCF node 5gc.js pcf edit --name <名称> --project <工程> --set-<字段> <值> ``` **可编辑字段**: | 参数 | 说明 | |------|------| | `--set-http2_sip <IP>` | 修改 HTTP2 服务 IP | | `--set-http2_port <端口>` | 修改 HTTP2 端口 | | `--set-MCC <值>` | 修改 MCC(注意大写) | | `--set-MNC <值>` | 修改 MNC(注意大写) | **示例**: ```bash # 批量修改工程下所有 PCF 的 HTTP2 IP node 5gc.js pcf edit --project XW_S5GC_1 --set-http2_sip 10.10.10.99 # 修改指定 PCF 的 HTTP2 IP 和 MNC node 5gc.js pcf edit --name pcc --project XW_S5GC_1 --set-http2_sip 10.10.10.88 --set-MNC 01 ``` ### NRF #### nrf-add-skill.js **功能**:在指定工程下添加一个 NRF 实例。 **使用方式**: ```bash node 5gc.js nrf add <名称> [选项...] node skills/5gc/scripts/nrf-add-skill.js <名称> [选项...] ``` **参数**: | 参数 | 说明 | 默认值 | |------|------|--------| | `<名称>` | NRF 实例名称(位置参数) | **必填** | | `--project <工程>` / `-p <工程>` | 目标工程名称 | `XW_S5GC_1` | | `--url <地址>` | 5GC 仪表地址 | `https://192.168.3.89` | | `--http2_sip <IP>` | HTTP2 服务 IP | `192.168.20.100` | | `--http2_port <端口>` | HTTP2 端口 | `80` | | `--MCC <值>` | MCC(注意大写) | `460` | | `--MNC <值>` | MNC(注意大写) | `01` | | `--headed` | 打开可见浏览器 | false | **示例**: ```bash node 5gc.js nrf add NRF-TEST --project XW_S5GC_1 node 5gc.js nrf add NRF-PROD --project XW_S5GC_1 --http2_sip 10.0.0.50 --MCC 460 --MNC 01 ``` #### nrf-edit-skill.js **功能**:编辑指定工程下的 NRF 实例(支持单条和批量)。 **使用方式**: ```bash # 批量编辑:修改工程下所有 NRF 的字段 node 5gc.js nrf edit --project <工程> --set-<字段> <值> # 单条编辑:修改指定名称的 NRF node 5gc.js nrf edit --name <名称> --project <工程> --set-<字段> <值> ``` **可编辑字段**: | 参数 | 说明 | |------|------| | `--set-http2_sip <IP>` | 修改 HTTP2 服务 IP | | `--set-http2_port <端口>` | 修改 HTTP2 端口 | | `--set-MCC <值>` | 修改 MCC(注意大写) | | `--set-MNC <值>` | 修改 MNC(注意大写) | **示例**: ```bash # 批量修改工程下所有 NRF 的 HTTP2 IP node 5gc.js nrf edit --project XW_S5GC_1 --set-http2_sip 10.10.10.99 # 修改指定 NRF 的 HTTP2 IP 和 MNC node 5gc.js nrf edit --name nrf1 --project XW_S5GC_1 --set-http2_sip 10.10.10.88 --set-MNC 01 ``` ### NRF(网络存储功能) #### nrf-add-skill.js **功能**:在指定工程下添加一个 NRF 实例。 **使用方式**: ```bash node 5gc.js nrf add <名称> [选项...] node skills/5gc/scripts/nrf-add-skill.js <名称> [选项...] ``` **参数**: | 参数 | 说明 | 默认值 | |------|------|--------| | `<名称>` | NRF 实例名称(位置参数) | **必填** | | `--project <工程>` / `-p <工程>` | 目标工程名称 | `XW_S5GC_1` | | `--url <地址>` | 5GC 仪表地址 | `https://192.168.3.89` | | `--http2_sip <IP>` | HTTP2 服务 IP | `192.168.20.100` | | `--http2_port <端口>` | HTTP2 端口 | `80` | | `--MCC <值>` | MCC(注意大写) | `460` | | `--MNC <值>` | MNC(注意大写) | `01` | | `--headed` | 打开可见浏览器 | false | **示例**: ```bash node 5gc.js nrf add NRF-TEST --project XW_S5GC_1 node 5gc.js nrf add NRF-PROD --project XW_S5GC_1 --http2_sip 10.0.0.50 --MCC 460 --MNC 01 ``` #### nrf-edit-skill.js **功能**:编辑指定工程下的 NRF 实例(支持单条和批量)。 **使用方式**: ```bash # 批量编辑:修改工程下所有 NRF 的字段 node 5gc.js nrf edit --project <工程> --set-<字段> <值> # 单条编辑:修改指定名称的 NRF node 5gc.js nrf edit --name <名称> --project <工程> --set-<字段> <值> ``` **可编辑字段**: | 参数 | 说明 | |------|------| | `--set-http2_sip <IP>` | 修改 HTTP2 服务 IP | | `--set-http2_port <端口>` | 修改 HTTP2 端口 | | `--set-MCC <值>` | 修改 MCC(注意大写) | | `--set-MNC <值>` | 修改 MNC(注意大写) | **示例**: ```bash # 批量修改工程下所有 NRF 的 HTTP2 IP node 5gc.js nrf edit --project XW_S5GC_1 --set-http2_sip 10.10.10.99 # 修改指定 NRF 的 HTTP2 IP 和 MNC node 5gc.js nrf edit --name nrf1 --project XW_S5GC_1 --set-http2_sip 10.10.10.88 --set-MNC 01 ``` ## 全局参数参考 以下参数所有脚本均支持: | 参数 | 说明 | 适用范围 | |------|------|---------| | `--url <地址>` | 5GC 仪表 URL | 所有脚本 | | `--project <工程>` / `-p <工程>` | 目标工程名称 | 所有脚本 | | `--headed` | 打开可见 Chromium 窗口(调试用) | 所有脚本 | | `--set-<字段> <值>` | 修改指定字段值 | 所有 edit 脚本 | | `--name <名称>` | 按名称精确匹配 | 所有 edit 脚本 | | `--id <ID>` | 按 ID 直接定位 | 所有 edit 脚本 | --- ## 字段参考 ### AMF 字段 | 字段名 | 说明 | 示例值 | |--------|------|--------| | `mcc` | 移动国家码 | `460` | | `mnc` | 移动网络码 | `01` | | `ngap_sip` | NGAP 信令面 IP | `10.200.1.50` | | `ngap_port` | NGAP 端口 | `38412` | | `http2_sip` | HTTP2 服务 IP | `10.200.1.51` | | `http2_port` | HTTP2 端口 | `8080` | | `stac` | 起始 TAC | `101` | | `etac` | 结束 TAC | `102` | | `region_id` | 区域 ID | `1` | | `set_id` | Set ID | `1` | | `pointer` | 指针 | `1` | | `ea[NEA0]` ~ `ea[128-NEA3]` | 加密算法(默认全选) | `1` | | `ia[NIA0]` ~ `ia[128-NIA3]` | 完整性保护算法(默认全选) | `1` | ### UDM/AUSF 字段 | 字段名 | 说明 | 示例值 | |--------|------|--------| | `count` | 实例数量 | `3` | | `sip` | SIP 服务 IP | `10.0.0.100` | | `port` | 端口 | `80` | | `auth_method` | 认证方法 | `5G_AKA` | | `scheme` | 协议类型 | `HTTP` | | `priority` | 优先级 | `8` | ### SMF/PGW-C 字段 | 字段名 | 说明 | 示例值 | |--------|------|--------| | `pfcp_sip` | PFCP 信令面 IP | `10.10.10.50` | | `n3_ip` | N3 接口 IP | `10.10.10.50` | | `n6_ip` | N6 接口 IP | `10.10.10.51` | | `http2_sip` | HTTP2 服务 IP | `10.10.10.50` | | `dnn` | DNN(数据网络名) | `internet` | | `snssai_sst` | NSSAI SST | `1` | | `snssai_sd` | NSSAI SD | `ffffff` | | `mcc` | MCC | `460` | | `mnc` | MNC | `01` | | `pdu_capacity` | PDU 会话容量 | `200000` | ### UPF/PGW-U 字段 | 字段名 | 说明 | 示例值 | |--------|------|--------| | `n3_ip` | N3 接口 IP | `192.168.20.30` | | `n4_ip` | N4 接口 IP(PFCP) | `192.168.20.30` | | `n6_ip` | N6 接口 IP | `192.168.20.31` | | `n6_gw` | N6 网关 IP | `192.168.20.1` | | `dnn` | DNN | `internet` | | `static_arp` | 静态 ARP | `192.168.20.254` | | `sst` | NSSAI SST | `1` | | `sd` | NSSAI SD | `ffffff` | | `stac` | 起始 TAC | `101` | | `etac` | 结束 TAC | `102` | ### GNB 字段 | 字段名 | 说明 | 示例值 | |--------|------|--------| | `ngap_sip` | NGAP 信令面 IP | `200.20.20.50` | | `user_sip_ip_v4` | 用户面 IPv4 | `2.2.2.2` | | `user_sip_ip_v6` | 用户面 IPv6 | `::1` | | `mcc` | MCC | `460` | | `mnc` | MNC | `60` | | `stac` | 起始 TAC | `0` | | `etac` | 结束 TAC | `0` | | `node_id` | 节点 ID | `70` | | `cell_count` | 小区数量 | `1` | | `replay_ip` | 回放 IP | `0.0.0.0` | | `replay_port` | 回放端口 | `0` | ### UE 字段 | 字段名 | 说明 | 示例值 | |--------|------|--------| | `s_imsi` | 起始 IMSI(15位) | `460001234567890` | | `msisdn` | MSISDN(13-15位,86开头) | `8613888888888` | | `mcc` | MCC | `460` | | `mnc` | MNC | `01` | | `key` | KI 密钥(32位 hex) | `001122...` | | `op_opc` | OPc 密钥(32位 hex) | `aabbcc...` | | `imeisv` | IMEISV(15位,偶数) | `8611111111111111` | | `nssai_sst` | NSSAI SST | `1` | | `nssai_sd` | NSSAI SD | `111111` | | `user_sip_ip_v4` | 用户面 IPv4 | `自动分配` | | `user_sip_ip_v6` | 用户面 IPv6 | `自动分配` | | `replay_ip` | 回放 IP | `0.0.0.0` | | `replay_port` | 回放端口 | `0` | ### PCF/PCRF 字段 | 字段名 | 说明 | 示例值 | |--------|------|--------| | `http2_sip` | HTTP2 服务 IP | `192.168.20.90` | | `http2_port` | HTTP2 端口 | `80` | | `MCC` | MCC(大写) | `460` | | `MNC` | MNC(大写) | `01` | | `count` | 实例数量 | `1` | FILE:scripts/5gc.js /** * 5GC Web 仪表统一 CLI * * 用法: node 5gc.js <entity> <action> [options] * * entity (网元类型): amf | udm | smf | upf | gnb | ue | pcf | nrf * action (操作类型): add | edit * * 通用选项: * --url <地址> 5GC 仪表地址(默认 https://192.168.3.89) * --project <工程> 目标工程名称 * --name <名称> 网元名称(用于单条记录筛选) * --id <id> 网元 ID(直接编辑指定 ID) * --headed 以有头模式运行(显示浏览器窗口) * * 字段修改(edit 模式)--set-<field> <value>: * AMF: name|sbi_ip|sbi_port|amf_name|guami|mcc|mnc|sst|sd|ap1|ap2|ap3|ap4|ap5 * UDM: name|auth_supi|auth_op_type|op_opc|aud_method|scheme|id|priority * SMF: name|pfcp_ip|n3_ip|n6_ip|dnn|snssai|sliceamba_type * UPF: name|n4_ip|n3_ip|n6_ip|dnn|snssai|count|static_arp|ue_ip_pool * GNB: name|ngap_ip|user_sip_ip_v4|mcc|mnc|stac|etac|node_id|cell_count|replay_ip|replay_port * UE: name|count|mcc|mnc|s_imsi|key|opc|imeisv|msisdn|user_sip_ip_v4|user_sip_ip_v6|replay_ip|replay_port * * 示例: * node 5gc.js amf add --name AMF_TEST --project XW_S5GC_1 --sbi-ip 10.0.0.1 * node 5gc.js gnb add --name GNB_TEST --project XW_S5GC_1 --count 1 --mcc 460 --mnc 01 --stac 1 --etac 100 * node 5gc.js ue add --name UE_001 --imsi 460001234567890 --msisdn 8613888888888 * node 5gc.js ue edit --project XW_S5GC_1 --set-msisdn 8613888888888 * node 5gc.js ue edit --id 10337 --set-msisdn 8613888888888 * node 5gc.js gnb edit --project XW_S5GC_1 --set-user_sip_ip_v4 200.200.200.200 * node 5gc.js upf edit --project XW_S5GC_1 --set-n4_ip 10.0.0.5 */ const { spawn } = require('child_process'); const path = require('path'); const fs = require('fs'); const SCRIPTS_DIR = __dirname; const argv = process.argv.slice(2); if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') { printHelp(); process.exit(0); } const entity = argv[0].toLowerCase(); const action = (argv[1] || '').toLowerCase(); const VALID_ENTITIES = ['amf', 'udm', 'smf', 'upf', 'gnb', 'ue', 'pcf', 'nrf']; const VALID_ACTIONS = ['add', 'edit']; if (!VALID_ENTITIES.includes(entity)) { console.error(`\n❌ 未知网元类型: entity`); console.error(' 可用: ' + VALID_ENTITIES.join(', ')); process.exit(1); } if (!action || !VALID_ACTIONS.includes(action)) { console.error(`\n❌ 未知操作: action || '(空)'`); console.error(' 用法: node 5gc.js <entity> <action> [options]'); console.error(' 示例: node 5gc.js amf add --help'); process.exit(1); } // 子脚本映射 // 所有 edit 均映射到 edit 脚本(单条 + 批量二合一) const scriptMap = { 'amf:add': 'amf-add-skill.js', 'amf:edit': 'amf-edit-skill.js', 'udm:add': 'ausf-udm-add-skill.js', 'udm:edit': 'ausf-udm-edit-skill.js', 'smf:add': 'smf-pgwc-add-skill.js', 'smf:edit': 'smf-pgwc-edit-skill.js', 'upf:add': 'upf-add-skill.js', 'upf:edit': 'upf-edit-skill.js', 'gnb:add': 'gnb-add-skill.js', 'gnb:edit': 'gnb-edit-skill.js', 'ue:add': 'ue-add-skill.js', 'ue:edit': 'ue-edit-skill.js', 'pcf:add': 'pcf-add-skill.js', 'pcf:edit': 'pcf-edit-skill.js', 'nrf:add': 'nrf-add-skill.js', 'nrf:edit': 'nrf-edit-skill.js', }; const scriptFile = scriptMap[`entity:action`]; const scriptPath = path.join(SCRIPTS_DIR, scriptFile); if (!fs.existsSync(scriptPath)) { console.error(`\n❌ 脚本不存在: scriptPath`); process.exit(1); } // 去掉 entity 和 action 后的参数传给子脚本 const childArgv = argv.slice(2); console.log(`\n▶ 5GC entity.toUpperCase() action`); console.log(' → node ' + scriptFile + ' ' + childArgv.join(' ') + '\n'); // 用子进程调用,保持 CLI 参数隔离 const child = spawn('node', [scriptPath, ...childArgv], { stdio: 'inherit', shell: true, cwd: SCRIPTS_DIR, }); child.on('exit', (code) => process.exit(code || 0)); child.on('error', (err) => { console.error('启动失败:', err.message); process.exit(1); }); function printHelp() { console.log(` 5GC Web 仪表自动化 - 统一 CLI ============================= 用法: node 5gc.js <entity> <action> [options] 网元类型 (entity): amf - AMF(接入与移动性管理功能) udm - UDM/AUSF(统一数据管理/认证服务器功能) smf - SMF/PGW-C(会话管理功能/PDN 连接网关控制面) upf - UPF/PGW-U(用户面功能/PDN 连接网关用户面) gnb - gNodeB(5G 基站) ue - UE(用户终端) pcf - PCF/PCRF(策略控制功能) nrf - NRF(网络存储功能) 操作 (action): add - 添加网元实例 edit - 编辑网元(单个或批量) 通用选项: --url <地址> 5GC 仪表地址(默认 https://192.168.3.89) --project <工程> 目标工程名称 --name <名称> 网元名称 --id <id> 网元 ID(edit 模式) --headed 以有头模式运行(显示浏览器) --help 显示本帮助 字段修改(edit 模式 --set-<field> <value>): AMF: name|sbi_ip|sbi_port|amf_name|guami|mcc|mnc|sst|sd|ap1|ap2|ap3|ap4|ap5 UDM: name|auth_supi|auth_op_type|op_opc|aud_method|scheme|id|priority SMF: name|pfcp_ip|n3_ip|n6_ip|dnn|snssai|sliceamba_type UPF: name|n4_ip|n3_ip|n6_ip|dnn|snssai|count|static_arp|ue_ip_pool GNB: name|ngap_ip|user_sip_ip_v4|mcc|mnc|stac|etac|node_id|cell_count|replay_ip|replay_port \n PCF: http2_sip|http2_port|mcc|mnc 添加示例: node 5gc.js amf add --name AMF_TEST --project XW_S5GC_1 --sbi-ip 10.0.0.1 --mcc 460 --mnc 01 node 5gc.js gnb add --name GNB_TEST --project XW_S5GC_1 --count 1 --mcc 460 --mnc 01 --stac 1 --etac 100 node 5gc.js ue add --name UE_001 --imsi 460001234567890 --msisdn 8613888888888 node 5gc.js smf add --name SMF_TEST --project XW_S5GC_1 --pfcp-ip 10.0.0.2 node 5gc.js upf add --name UPF_TEST --project XW_S5GC_1 --n4-ip 10.0.0.3 编辑示例: node 5gc.js ue edit --project XW_S5GC_1 --set-msisdn 8613888888888 node 5gc.js ue edit --id 10337 --set-msisdn 8613888888888 node 5gc.js gnb edit --project XW_S5GC_1 --set-user_sip_ip_v4 200.200.200.200 node 5gc.js upf edit --project XW_S5GC_1 --set-n4_ip 10.0.0.5 `); } FILE:scripts/5gc_session_192_168_3_89.json { "storageState": { "cookies": [ { "name": "XSRF-TOKEN", "value": "eyJpdiI6InIxMm43cDZvL2Q1cy9tTnppcDZBQUE9PSIsInZhbHVlIjoiVE9RTVNsZHZKdlFZcmM1TkIrbmRJZjNtTTA3K1RmdTZpcWMyVEV0eS9CS3FVb0JuVHQ4QUpQcy8zdWRGS0pvMmpWb243RnBMRjdMQ0NvOW0wZDkyZllzanBWQ2xiU3l0dTlxZmRERHRkOHZQd0UrN2tkVWJtdVRYcDhYUm92VnYiLCJtYWMiOiI0YjcwNjVjMDhlNjBkOGQyZTJkOGE0ZTNlNjA0ODQwZjYwYTE4ZGMyYjJlYzYzZjg2ODVhNDU2YzY0YTMxNzdhIn0%3D", "domain": "192.168.3.89", "path": "/", "expires": 1774724910.451521, "httpOnly": false, "secure": false, "sameSite": "Lax" }, { "name": "laravel_5gc_session", "value": "eyJpdiI6IlNyOW9JNEYzQzczdEhDNllPUytoR0E9PSIsInZhbHVlIjoiR3g1NVIxMVI1cW1nK1ZKeVRLZjF2VGVMSUpqeCt0d0VTZlNBVlhQRWZTTG9KbW5YUXVob2VXVlFmQ0kwYzFXS1lRMEd5cmJhM25vRUNzU2RaQ2FHMWhPb0xUa0gvM2dNSkg3bmw0bC9kbmpScWRZcXdRV1ZFbDZaVndleXlGZUwiLCJtYWMiOiIwNGZkZGQ4MzE4OTAxZTUwYzVhYTRlYTE5MDVlMDA4OGNkN2FkMTlhOTJjOTRhOWFhNzJmOWYwZDI4YjRhNjI0In0%3D", "domain": "192.168.3.89", "path": "/", "expires": 1774724910.451715, "httpOnly": true, "secure": false, "sameSite": "Lax" }, { "name": "kodUserLanguage", "value": "zh_CN", "domain": "192.168.3.89", "path": "/", "expires": -1, "httpOnly": false, "secure": false, "sameSite": "Lax" } ], "origins": [ { "origin": "https://192.168.3.89", "localStorage": [ { "name": "auth.token", "value": "eyJpdiI6IkdvQTMyZVU0eUZEZ1oycjJlT1NnXC9BPT0iLCJ2YWx1ZSI6InZMS3J2V0VwaEJGNENpTjI3MmltOWp0dUxpWUhjOVNOWjFCSFwvaGVLY09vPSIsIm1hYyI6IjAyZGIzZTFlOGU2NzI5OTFhMTJjNWFlMTc2MWRhNmU0ZjQwNzBlYmFkMDg1NWM5ZDdkNTAxMDA2OGY4ZjAwNDIifQ==" }, { "name": "i18n.language_code", "value": "zh_CN" }, { "name": "auth.email", "value": "[email protected]" } ] } ] } } FILE:scripts/5gc_test_all.js /** * 5GC 技能全面回归测试 * * 测试策略: * - add 脚本:添加测试实例(用唯一名称),验证提交后URL跳转 * - edit 脚本:编辑已有实例,验证字段修改 * - bulk-edit 脚本:批量修改目标工程下所有实例 * * 运行方式: * node 5gc_test_all.js # 全部测试 * node 5gc_test_all.js --amf # 只测 AMF * node 5gc_test_all.js --headed # 有头模式(可见浏览器) */ const { chromium } = require('playwright'); const { spawn } = require('child_process'); const path = require('path'); const fs = require('fs'); const SCRIPTS_DIR = path.join(__dirname); const BASE_URL = 'https://192.168.3.89'; const LOGIN_EMAIL = '[email protected]'; const LOGIN_PWD = 'dotouch'; const TEST_PROJECT = 'XW_S5GC_1'; // 已知存在的工程 const TS = Date.now(); // ─── helpers ──────────────────────────────────────────────── async function login(page) { await page.goto(BASE_URL + '/login', { waitUntil: 'networkidle', timeout: 15000 }); await page.getByRole('textbox', { name: 'E-Mail地址' }).fill(LOGIN_EMAIL); await page.getByRole('textbox', { name: '密码' }).fill(LOGIN_PWD); await page.getByRole('checkbox', { name: '记住我' }).check(); await page.getByRole('button', { name: '登录' }).click(); await page.waitForLoadState('networkidle'); } async function selectProjectFast(page, projectName) { await page.goto(BASE_URL + '/sim_5gc/project/index', { waitUntil: 'networkidle', timeout: 15000 }); await page.waitForTimeout(2000); for (let i = 0; i < 20; i++) { const found = await page.evaluate((target) => { const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row'); for (const row of rows) { const cells = row.querySelectorAll('td'); if (cells.length >= 3 && cells[2].textContent.trim() === target) { if (row.classList.contains('jsgrid-selected-row')) return 'already'; const ic = row.querySelector('.layui-icon'); if (ic) { ic.click(); return 'clicked'; } row.click(); return 'clicked'; } } return 'not-found'; }, projectName); if (found === 'already' || found === 'clicked') return true; // 翻页 const hasNext = await page.evaluate(() => { return Array.from(document.querySelectorAll('.jsgrid-pager a')) .some(a => a.innerText.trim() === 'Next'); }); if (!hasNext) break; await page.evaluate(() => { const links = document.querySelectorAll('.jsgrid-pager a'); for (const l of links) { if (l.innerText.trim() === 'Next') { l.click(); break; } } }); await page.waitForTimeout(1500); } return false; } function resultPath(name) { return path.join(SCRIPTS_DIR, '..', 'test_results', `test_TS_name.json`); } function saveResult(name, ok, detail) { const dir = path.join(SCRIPTS_DIR, '..', 'test_results'); if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(resultPath(name), JSON.stringify({ name, ok, detail, ts: new Date().toISOString() }, null, 2)); } function runChild(scriptFile, args) { return new Promise((resolve) => { const child = spawn('node', [scriptFile, ...args], { stdio: 'pipe', shell: true }); let out = '', err = ''; child.stdout.on('data', d => out += d.toString()); child.stderr.on('data', d => err += d.toString()); child.on('close', code => resolve({ code, out, err })); }); } // ─── 测试用例定义 ─────────────────────────────────────────── const TEST_CASES = [ // ── AMF ────────────────────────────────────────────── { tag: 'AMF添加', entity: 'amf', action: 'add', headed: false, args: ['--name', `AMF_TEST_TS`, '--project', TEST_PROJECT, '--sbi-ip', '10.200.1.1', '--mcc', '460', '--mnc', '01', '--sst', '1', '--sd', '000001'], verify: async (page) => { await page.waitForURL('**/sim_5gc/amf/index', { timeout: 15000 }); return page.url().includes('/amf/index'); } }, { tag: 'AMF编辑', entity: 'amf', action: 'edit', headed: false, args: ['--name', `AMF_TEST_TS`, '--project', TEST_PROJECT, '--set-sbi_ip', '10.200.1.100'], verify: async (page) => { await page.waitForURL('**/sim_5gc/amf/index', { timeout: 15000 }); return page.url().includes('/amf/index'); } }, // ── AUSF/UDM ───────────────────────────────────────── { tag: 'UDM添加', entity: 'udm', action: 'add', headed: false, args: ['--name', `UDM_TEST_TS`, '--project', TEST_PROJECT, '--auth-supi', `46000TS000000`, '--auth-op-type', 'opc', '--opc', '63b3c4d1d5c03f4e82c5f4a5b2e7d9c1'], verify: async (page) => { await page.waitForURL('**/sim_5gc/udm/index', { timeout: 15000 }); return page.url().includes('/udm/index'); } }, { tag: 'UDM编辑', entity: 'udm', action: 'edit', headed: false, args: ['--name', `UDM_TEST_TS`, '--project', TEST_PROJECT, '--set-op_opc', 'aaaaaaaaaaaabbbbccccccccccccdddd'], verify: async (page) => { await page.waitForURL(/sim_5gc\/udm\//, { timeout: 15000 }); return true; } }, // ── SMF ────────────────────────────────────────────── { tag: 'SMF添加', entity: 'smf', action: 'add', headed: false, args: ['--name', `SMF_TEST_TS`, '--project', TEST_PROJECT, '--pfcp-ip', '10.200.2.1', '--n3-ip', '10.200.2.2', '--n6-ip', '10.200.2.3', '--dnn', 'internet'], verify: async (page) => { await page.waitForURL('**/sim_5gc/smf/index', { timeout: 15000 }); return page.url().includes('/smf/index'); } }, { tag: 'SMF编辑', entity: 'smf', action: 'edit', headed: false, args: ['--name', `SMF_TEST_TS`, '--project', TEST_PROJECT, '--set-dnn', 'internet_updated'], verify: async (page) => { await page.waitForURL(/sim_5gc\/smf\//, { timeout: 15000 }); return true; } }, { tag: 'SMF批量编辑', entity: 'smf', action: 'edit', headed: false, args: ['--project', TEST_PROJECT, '--set-dnn', 'internet'], verify: async (page) => { // 批量编辑 SMF 只要不报错即算通过 await page.waitForTimeout(5000); const url = page.url(); return !url.includes('/project/index'); // 没卡在工程选择页即可 } }, // ── UPF ────────────────────────────────────────────── { tag: 'UPF添加', entity: 'upf', action: 'add', headed: false, args: ['--name', `UPF_TEST_TS`, '--project', TEST_PROJECT, '--n4-ip', '10.200.3.1', '--n3-ip', '10.200.3.2', '--n6-ip', '10.200.3.3', '--dnn', 'internet'], verify: async (page) => { await page.waitForURL('**/sim_5gc/upf/index', { timeout: 15000 }); return page.url().includes('/upf/index'); } }, { tag: 'UPF编辑', entity: 'upf', action: 'edit', headed: false, args: ['--name', `UPF_TEST_TS`, '--project', TEST_PROJECT, '--set-n4_ip', '10.200.3.100'], verify: async (page) => { await page.waitForURL(/sim_5gc\/upf\//, { timeout: 15000 }); return true; } }, { tag: 'UPF批量编辑', entity: 'upf', action: 'edit', headed: false, args: ['--project', TEST_PROJECT, '--set-n4_ip', '10.200.3.1'], verify: async (page) => { await page.waitForTimeout(5000); const url = page.url(); return !url.includes('/project/index'); } }, // ── GNB ────────────────────────────────────────────── { tag: 'GNB添加', entity: 'gnb', action: 'add', headed: false, args: ['--name', `GNB_TEST_TS`, '--project', TEST_PROJECT, '--count', '1', '--ngap-ip', '10.200.4.1', '--user-sip-ip-v4', '10.200.4.2', '--mcc', '460', '--mnc', '01', '--stac', '1', '--etac', '100', '--node-id', 'gni_000004'], verify: async (page) => { await page.waitForURL('**/sim_5gc/gnb/index', { timeout: 15000 }); return page.url().includes('/gnb/index'); } }, { tag: 'GNB编辑', entity: 'gnb', action: 'edit', headed: false, args: ['--name', `GNB_TEST_TS`, '--project', TEST_PROJECT, '--set-replay_ip', '10.200.4.200'], verify: async (page) => { await page.waitForURL(/sim_5gc\/gnb\//, { timeout: 15000 }); return true; } }, { tag: 'GNB批量编辑', entity: 'gnb', action: 'edit', headed: false, args: ['--project', TEST_PROJECT, '--set-replay_ip', '10.200.4.200'], verify: async (page) => { await page.waitForTimeout(5000); const url = page.url(); return !url.includes('/project/index'); } }, // ── UE ─────────────────────────────────────────────── { tag: 'UE添加', entity: 'ue', action: 'add', headed: false, args: ['--name', `UE_TEST_TS`, '--imsi', `46000TS`, '--msisdn', '8613888888888', '--mcc', '460', '--mnc', '01'], verify: async (page) => { await page.waitForURL('**/sim_5gc/ue/index', { timeout: 15000 }); return page.url().includes('/ue/index'); } }, { tag: 'UE编辑', entity: 'ue', action: 'edit', headed: false, args: ['--project', TEST_PROJECT, '--name', `UE_TEST_TS`, '--set-msisdn', '8613888889999'], verify: async (page) => { await page.waitForTimeout(3000); const url = page.url(); return url.includes('/ue/index') || url.includes('/ue/edit/'); } }, { tag: 'UE批量编辑', entity: 'ue', action: 'edit', headed: false, args: ['--project', TEST_PROJECT, '--set-msisdn', '8613888888888'], verify: async (page) => { await page.waitForTimeout(5000); const url = page.url(); return !url.includes('/project/index'); } }, ]; // ─── 5gc.js 统一入口测试 ─────────────────────────────────── async function testViaCli(testCase, headed) { const cliPath = path.join(SCRIPTS_DIR, '5gc.js'); const args = [testCase.entity, testCase.action, ...testCase.args]; if (headed) args.push('--headed'); const child = spawn('node', [cliPath, ...args], { stdio: 'pipe', shell: true }); let out = '', err = ''; child.stdout.on('data', d => out += d.toString()); child.stderr.on('data', d => err += d.toString()); return new Promise((resolve) => { child.on('close', code => resolve({ code, out: out + err })); }); } // ─── 主测试运行器 ────────────────────────────────────────── async function main() { const argv = process.argv.slice(2); const headed = argv.includes('--headed'); const filter = argv.find(a => !a.startsWith('--')); // 登录一次,获取 playwright context const browser = await chromium.launch({ headless: !headed, args: ['--no-sandbox', '--ignore-certificate-errors'] }); const ctx = await browser.newContext({ ignoreHTTPSErrors: true }); const page = await ctx.newPage(); await login(page); const ok = await selectProjectFast(page, TEST_PROJECT); if (!ok) { console.log('工程选择失败'); await browser.close(); return; } const toRun = TEST_CASES.filter(t => !filter || t.tag.toLowerCase().includes(filter.toLowerCase())); console.log(`\n========================================`); console.log(`5GC 全面回归测试 (toRun.length 个用例, headed=headed)`); console.log(`========================================\n`); const results = []; for (const tc of toRun) { process.stdout.write(`tc.tag.padEnd(20) ... `); // 每次重走工程选择(部分脚本内部会切工程) try { const r = await testViaCli(tc, headed); // 简单判断:进程退出码0 且输出无"异常" const ok = r.code === 0 && !r.out.includes('异常') && !r.out.includes('失败') && !r.out.includes('❌'); console.log(ok ? '✅ PASS' : '❌ FAIL'); if (!ok) console.log(' 输出:', r.out.substring(0, 200).replace(/\n/g, ' ')); results.push({ ...tc, pass: ok, detail: r.out.substring(0, 300) }); } catch (e) { console.log('❌ ERROR:', e.message); results.push({ ...tc, pass: false, detail: e.message }); } // 恢复工程上下文 await selectProjectFast(page, TEST_PROJECT).catch(() => {}); } await browser.close(); // 汇总 const pas = results.filter(r => r.pass).length; const fail = results.filter(r => !r.pass).length; console.log(`\n========================================`); console.log(`结果: pas 通过 / fail 失败`); if (fail > 0) { console.log('\n失败用例:'); results.filter(r => !r.pass).forEach(r => console.log(` ❌ r.tag: r.detail.substring(0, 100)`)); } console.log('========================================\n'); // 保存详细结果 const reportDir = path.join(SCRIPTS_DIR, '..', 'test_results'); if (!fs.existsSync(reportDir)) fs.mkdirSync(reportDir, { recursive: true }); const reportFile = path.join(reportDir, `report_TS.json`); fs.writeFileSync(reportFile, JSON.stringify({ ts: new Date().toISOString(), results }, null, 2)); console.log(`详细报告: reportFile`); } main().catch(e => { console.error('Fatal:', e.message); process.exit(1); }); FILE:scripts/amf-add-skill.js #!/usr/bin/env node /** * AMF 添加脚本 - 完整修复版 * 功能:登录状态缓存 + .projectSelect 选工程 + evaluate 填写表单 + 算法全勾选 + NSSAI */ const { chromium } = require('playwright'); let globalBaseUrl = 'https://192.168.3.89'; const fs = require('fs'); const path = require('path'); // 配置 const CONFIG = { urls: { login: '/login', amfEdit: '/sim_5gc/amf/edit', amfManagement: '/sim_5gc/amf/index' }, credentials: { email: '[email protected]', password: 'dotouch' }, sessionDir: path.join(__dirname, '.sessions'), getSessionFile() { const host = globalBaseUrl.replace(/https?:\/\//, '').replace(/\./g, '_'); return `5gc_session_host.json`; } }; // 会话管理 class SessionManager { constructor() { this.sessionPath = path.join(CONFIG.sessionDir, CONFIG.getSessionFile()); } async saveSession(context) { try { const storageState = await context.storageState(); fs.writeFileSync(this.sessionPath, JSON.stringify({ storageState }, null, 2)); return true; } catch { return false; } } async loadSession(browser) { try { if (!fs.existsSync(this.sessionPath)) return null; const { storageState } = JSON.parse(fs.readFileSync(this.sessionPath, 'utf8')); return await browser.newContext({ storageState, ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } }); } catch { return null; } } } // 算法配置:直接点击 layui 复选框的可见元素 async function configureAlgorithmsSuccess(page) { await page.waitForSelector('.layui-form-checkbox', { timeout: 5000 }); await page.waitForTimeout(300); const checkboxCount = await page.locator('.layui-form-checkbox').count(); console.log(` 算法复选框数量: checkboxCount`); for (let i = 0; i < Math.min(checkboxCount, 8); i++) { await page.locator('.layui-form-checkbox').nth(i).click(); await page.waitForTimeout(80); } const priorities = [ 'ea[NEA0]', 'ea[128-NEA1]', 'ea[128-NEA2]', 'ea[128-NEA3]', 'ia[NIA0]', 'ia[128-NIA1]', 'ia[128-NIA2]', 'ia[128-NIA3]' ]; const vals = ['1', '2', '3', '4', '1', '2', '3', '4']; for (let i = 0; i < priorities.length; i++) { const inp = page.locator(`input[name="priorities[i]"]`); if (await inp.count() > 0) { await inp.fill(vals[i]); } } console.log(` ✅ 算法配置完成`); } // 工程选择(精确匹配,分页遍历) async function selectProject(page, projectName, forceSwitch = true) { if (!forceSwitch) { console.log(` 🔧 保持当前工程(用户未指定工程)`); return true; } await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', timeout: 15000 }); await page.waitForSelector('.jsgrid-row, .jsgrid-alt-row', { timeout: 5000 }).catch(() => {}); await page.evaluate(() => { const inputs = document.querySelectorAll('input[type="text"], input[name="name"]'); for (const inp of inputs) { inp.value = ''; } }); await page.waitForTimeout(300); for (let pageNum = 1; pageNum <= 200; pageNum++) { const clicked = await page.evaluate((targetName) => { const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row'); for (const row of rows) { const cells = row.querySelectorAll('td'); if (cells.length >= 3 && cells[2].textContent.trim() === targetName) { const icon = cells[1].querySelector('.iconfont'); if (icon) { icon.click(); return true; } } } return false; }, projectName); if (clicked) { await page.waitForTimeout(2000); return true; } const nextBtn = page.locator('.jsgrid-pager a:has-text("Next")'); if (!(await nextBtn.count())) break; try { REPLACED } catch (e) { break; } } console.log(` ❌ 未找到工程 "projectName"(精确匹配)`); return false; } // 添加 AMF 主流程 async function addAmf(amfName, projectName, explicitProject = true, amfConfig = {}) { const startTime = Date.now(); const sessionManager = new SessionManager(); const defaultConfig = { mcc: '460', mnc: '01', region_id: '1', set_id: '1', pointer: '1', ngap_sip: '200.20.20.1', ngap_port: '38412', http2_sip: '200.20.20.5', http2_port: '8080', stac: '101', etac: '102' }; const cfg = { ...defaultConfig, ...amfConfig }; let browser = null; try { browser = await chromium.launch({ headless: true, args: ['--no-sandbox', '--ignore-certificate-errors'] }); let context = await sessionManager.loadSession(browser); let needLogin = true; if (context) { const testPage = await context.newPage(); await testPage.goto(`globalBaseUrlCONFIG.urls.amfManagement`, { waitUntil: 'networkidle', timeout: 10000 }).catch(() => {}); if (!testPage.url().includes('/login')) { needLogin = false; } await testPage.close(); } if (needLogin) { context = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } }); } const page = await context.newPage(); if (needLogin) { await page.goto(`globalBaseUrlCONFIG.urls.login`, { waitUntil: 'networkidle', timeout: 15000 }); await page.getByRole('textbox', { name: 'E-Mail地址' }).fill(CONFIG.credentials.email); await page.getByRole('textbox', { name: '密码' }).fill(CONFIG.credentials.password); await page.getByRole('button', { name: '登录' }).click(); await page.waitForLoadState('networkidle', { timeout: 10000 }); await sessionManager.saveSession(context); } // 选择工程(仅当用户显式指定工程时才切换) if (!(await selectProject(page, projectName, explicitProject))) { throw new Error(`工程 "projectName" 不存在或无法选中`); } // 进入编辑页面 await page.goto(`globalBaseUrlCONFIG.urls.amfEdit`, { waitUntil: 'networkidle', timeout: 15000 }); if (!page.url().includes('/amf/edit')) { await page.goto(`globalBaseUrl/sim_5gc/amf/edit`); await page.waitForSelector('input[name="name"]', { timeout: 10000 }); } // 通过 evaluate 直接填写表单 await page.evaluate(({ amfName, cfg }) => { const set = (name, value) => { const el = document.querySelector(`input[name="name"]`); if (el) { el.value = value; el.dispatchEvent(new Event('input', { bubbles: true })); } }; set('name', amfName); set('mcc', cfg.mcc); set('mnc', cfg.mnc); set('region_id', cfg.region_id); set('set_id', cfg.set_id); set('pointer', cfg.pointer); set('ngap_sip', cfg.ngap_sip); set('ngap_port', cfg.ngap_port); set('http2_sip', cfg.http2_sip); set('http2_port', cfg.http2_port); set('stac', cfg.stac); set('etac', cfg.etac); }, { amfName, cfg }); // 类型选择:仿真设备 await page.locator('.layui-unselect').first().click(); await page.waitForTimeout(300); await page.locator('dd').filter({ hasText: '仿真设备' }).click(); // 配置算法 await configureAlgorithmsSuccess(page); // 配置 NSSAI await page.getByRole('row', { name: /数量.*nssai/ }).getByRole('button').click(); await page.waitForTimeout(500); await page.locator('input[name="config[count][]"]').fill('1'); await page.getByRole('row', { name: /nssai.*添加.*删除/ }).locator('span').click(); await page.waitForTimeout(800); const iframeEl = page.locator('iframe[name="layui-layer-iframe2"]'); const iframe = await iframeEl.contentFrame({ timeout: 5000 }); await iframe.getByRole('row', { name: /\*.*SST.*SD/ }).getByRole('button').click(); await iframe.locator('input[name="nssai[snssai_sst][]"]').fill('1'); await iframe.locator('input[name="nssai[snssai_sd][]"]').fill('111111'); await iframe.getByRole('button', { name: '提交' }).click(); await page.waitForTimeout(800); // 提交表单 await page.getByRole('button', { name: '提交' }).click(); // 等待页面跳转到 AMF 列表页面,若未跳转则强制跳转 try { await page.waitForURL(`**/amf/index`, { timeout: 8000 }); } catch (e) { await page.goto(`globalBaseUrlCONFIG.urls.amfManagement`, { waitUntil: 'networkidle', timeout: 15000 }); } await page.waitForTimeout(2000); // 验证结果:只要页面成功跳转到 AMF 列表页,即认为添加成功 let found = false; const finalUrl = page.url(); if (finalUrl.includes('/amf/index')) { console.log(` ✅ 页面已跳转至 AMF 列表: finalUrl`); found = true; } await browser.close(); const totalTime = (Date.now() - startTime) / 1000; if (found) { return { success: true, amfName, totalTime }; } else { return { success: false, amfName, totalTime }; } } catch (err) { if (browser) await browser.close(); throw err; } } // 主函数 async function main() { const args = process.argv.slice(2); if (args.length === 0) { console.log('用法: node amf-add-skill.js <AMF名称> [--project <工程名>] [--url <地址>] [--mcc 460] [...]'); process.exit(1); } let amfName = null; let projectName = '5G_basic_process'; let amfConfig = {}; let explicitProject = false; for (let i = 0; i < args.length; i++) { if (!args[i].startsWith('-')) { amfName = args[i]; } else if (args[i] === '--project' || args[i] === '-p') { projectName = args[++i]; explicitProject = true; } else if (args[i] === '--url') { let u = args[++i]; if (u && !u.startsWith('http')) u = 'https://' + u; globalBaseUrl = u; } else if (args[i].startsWith('--')) { amfConfig[args[i].substring(2)] = args[++i]; } } if (!amfName) { console.error('错误: 请指定 AMF 名称'); process.exit(1); } console.log(`AMF: amfName | 工程: projectName | 地址: globalBaseUrl`); try { const result = await addAmf(amfName, projectName, explicitProject, amfConfig); console.log(result.success ? `成功! AMF "result.amfName" 添加完成 (result.totalTime.toFixed(2)s)` : `失败! 未找到 AMF "result.amfName"`); process.exit(result.success ? 0 : 1); } catch (err) { console.error(`执行异常: err.message`); process.exit(1); } } main(); FILE:scripts/amf-edit-skill.js #!/usr/bin/env node /** * 🚀 AMF配置修改技能 - 智能版本 * 特性: * 1. 智能AMF选择逻辑 * 2. 基于文字匹配,不使用固定ID * 3. 支持部分字段修改 * 4. 继承登录优化和成功算法配置 */ const { chromium } = require('playwright'); const fs = require('fs'); const path = require('path'); // 配置 let globalBaseUrl = 'https://192.168.3.89'; const CONFIG = { baseUrl: globalBaseUrl, urls: { login: '/login', amfManagement: '/sim_5gc/amf/index', amfEdit: '/sim_5gc/amf/edit' }, credentials: { email: '[email protected]', password: 'dotouch' }, sessionDir: path.join(__dirname, '.sessions'), // 动态session文件名:根据URL生成,避免跨仪表缓存冲突 getSessionFile: function() { const host = this.baseUrl.replace(/https?:\/\//, '').replace(/\./g, '_'); return `5gc_session_host.json`; } }; // 会话管理器(复用登录优化) class SessionManager { constructor() { this.sessionPath = path.join(CONFIG.sessionDir, CONFIG.getSessionFile()); } async saveSession(context) { try { const storageState = await context.storageState(); fs.writeFileSync(this.sessionPath, JSON.stringify({ storageState }, null, 2)); return true; } catch (error) { return false; } } async loadSession(browser) { try { if (!fs.existsSync(this.sessionPath)) return null; const { storageState } = JSON.parse(fs.readFileSync(this.sessionPath, 'utf8')); const context = await browser.newContext({ storageState, ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } }); return context; } catch (error) { return null; } } } // 智能AMF选择器 class AMFSelector { constructor(page) { this.page = page; } // 获取AMF列表 async getAMFList() { console.log('🔍 获取AMF列表...'); await this.page.goto(`CONFIG.baseUrlCONFIG.urls.amfManagement`, { waitUntil: 'networkidle', timeout: 15000 }); await this.page.waitForTimeout(1000); // 查找AMF表格 const tableSelector = 'table.layui-table'; const tableExists = await this.page.locator(tableSelector).count() > 0; if (!tableExists) { console.log('⚠️ 未找到AMF表格,可能没有配置或页面结构不同'); // 调试:查看页面结构 const pageText = await this.page.textContent('body').catch(() => ''); console.log('📄 页面内容片段:', pageText.substring(0, 300)); // 查找其他可能的表格 const allTables = await this.page.locator('table').all(); console.log(`🔍 找到 allTables.length 个表格`); for (let i = 0; i < allTables.length; i++) { const table = allTables[i]; const tableHtml = await table.innerHTML().catch(() => ''); console.log(`表格 i + 1 片段:`, tableHtml.substring(0, 200)); } return []; } // 调试:查看表格内容 const tableHtml = await this.page.locator(tableSelector).innerHTML().catch(() => ''); console.log('📊 表格HTML片段:', tableHtml.substring(0, 500)); // 提取AMF名称 - 使用 layui-table 单元格直接定位(稳定可靠) // layui-table 列结构: 复选框(0) | ID(1) | 名称(2) | NGAP IP(3) | HTTP2 IP(4) | ... const rowSelector = 'table.layui-table tbody tr, .layui-table-body tbody tr'; const amfRows = await this.page.locator(rowSelector).all(); const amfList = []; console.log(`📊 找到 amfRows.length 行数据`); for (let i = 0; i < amfRows.length; i++) { const row = amfRows[i]; // layui-table 列: 复选框(0) | ID(1) | 名称(2) | NGAP IP(3) | HTTP2 IP(4) | 端口(5) | ... const cells = await row.locator('td').all(); if (cells.length < 3) continue; const idCell = await cells[1].textContent().catch(() => ''); const nameCell = await cells[2].textContent().catch(() => ''); const id = idCell.trim(); const name = nameCell.trim(); if (!id || !id.match(/^\d+$/) || !name || name === '名称' || name === '编辑') continue; amfList.push({ id, name, row }); console.log(` ✅ 提取到: ID=id, 名称=name`); } // 去重 const uniqueAmfList = []; const seenNames = new Set(); for (const amf of amfList) { if (!seenNames.has(amf.name)) { seenNames.add(amf.name); uniqueAmfList.push(amf); } } console.log(`📋 找到 uniqueAmfList.length 个AMF配置:`); uniqueAmfList.forEach((amf, idx) => { console.log(` idx + 1. [amf.id] amf.name`); }); return uniqueAmfList; } // 智能选择AMF async selectAMF(amfName = null) { const amfList = await this.getAMFList(); if (amfList.length === 0) { throw new Error('未找到任何AMF配置,请先添加AMF'); } // 情况1: 用户指定了AMF名称 if (amfName) { console.log(`🎯 用户指定AMF名称: "amfName"`); // 精确匹配 const exactMatch = amfList.find(amf => amf.name === amfName); if (exactMatch) { console.log(`✅ 找到精确匹配: exactMatch.name`); return exactMatch; } // 模糊匹配(包含) const fuzzyMatches = amfList.filter(amf => amf.name.includes(amfName)); if (fuzzyMatches.length === 1) { console.log(`✅ 找到模糊匹配: fuzzyMatches[0].name`); return fuzzyMatches[0]; } else if (fuzzyMatches.length > 1) { console.log(`⚠️ 找到多个模糊匹配:`); fuzzyMatches.forEach((amf, idx) => { console.log(` idx + 1. amf.name`); }); throw new Error(`找到多个匹配的AMF,请指定更精确的名称`); } throw new Error(`未找到名称为"amfName"的AMF配置`); } // 情况2: 未指定名称,但只有一个AMF if (amfList.length === 1) { console.log(`✅ 只有一个AMF配置,自动选择: amfList[0].name`); return amfList[0]; } // 情况3: 多个AMF,未指定名称 → 返回全部(批量模式) console.log(`⚠️ 未指定AMF名称,找到 amfList.length 个AMF → 进入批量修改模式`); amfList.forEach((amf, idx) => { console.log(` idx + 1. [amf.id] amf.name`); }); return amfList; // 返回数组,editAMF 将检测到并进入批量 } // 点击编辑按钮进入编辑页面 - 优先使用URL直接访问 async clickEditButton(selectedAMF) { console.log(`🖱️ 进入编辑页面: selectedAMF.name`); // 方法1: 直接用ID访问编辑页面(最稳定) if (selectedAMF.id) { console.log(` 直接访问: /sim_5gc/amf/edit/selectedAMF.id`); await this.page.goto(`CONFIG.baseUrl/sim_5gc/amf/edit/selectedAMF.id`); await this.page.waitForSelector('input[name="name"]', { timeout: 10000 }); await this.page.waitForTimeout(1000); console.log('✅ 通过URL进入编辑页面'); return true; } // 方法2: 使用编辑按钮(备用) if (selectedAMF.editButton) { try { await selectedAMF.editButton.click({ timeout: 5000 }); await this.page.waitForLoadState('networkidle', { timeout: 10000 }); await this.page.waitForTimeout(1000); if (this.page.url().includes('/amf/edit')) { console.log('✅ 通过编辑按钮进入编辑页面'); return true; } } catch (error) { console.log(`⚠️ 编辑按钮点击失败: error.message`); } } // 方法3: 查找所有编辑按钮,按索引点击 console.log('🔍 查找所有编辑按钮...'); const allEditButtons = await this.page.locator('a:has-text("编辑"), button:has-text("编辑")').all(); console.log(`找到 allEditButtons.length 个编辑按钮`); if (allEditButtons.length > selectedAMF.index) { try { await allEditButtons[selectedAMF.index].click({ timeout: 5000 }); await this.page.waitForLoadState('networkidle', { timeout: 10000 }); await this.page.waitForTimeout(1000); if (this.page.url().includes('/amf/edit')) { console.log(`✅ 通过索引 selectedAMF.index 进入编辑页面`); return true; } } catch (error) { console.log(`⚠️ 索引点击失败: error.message`); } } // 方法3: 直接构造编辑URL(RESTful风格) console.log('🔗 尝试直接访问编辑URL...'); if (selectedAMF.id) { // 正确的URL格式: /amf/edit/6084 (不是 /amf/edit?id=6084) const editUrl = `CONFIG.baseUrl/sim_5gc/amf/edit/selectedAMF.id`; console.log(`访问: editUrl`); await this.page.goto(editUrl, { waitUntil: 'networkidle', timeout: 15000 }); await this.page.waitForTimeout(1000); if (this.page.url().includes('/amf/edit')) { console.log('✅ 通过直接URL进入编辑页面'); return true; } } // 方法4: 使用标准编辑页面URL console.log('🌐 尝试标准编辑页面...'); await this.page.goto(`CONFIG.baseUrlCONFIG.urls.amfEdit`, { waitUntil: 'networkidle', timeout: 15000 }); await this.page.waitForTimeout(1000); if (this.page.url().includes('/amf/edit')) { console.log('✅ 通过标准URL进入编辑页面'); return true; } throw new Error('无法进入编辑页面,请检查页面结构或权限'); } } // 配置修改器 class ConfigModifier { constructor(page) { this.page = page; } // 修改配置字段 async modifyConfig(fieldUpdates = {}) { console.log('🔧 修改配置字段...'); if (Object.keys(fieldUpdates).length === 0) { console.log('⚠️ 没有需要修改的字段,直接提交'); return; } // 等待页面加载完成 await this.page.waitForLoadState('networkidle', { timeout: 10000 }); // 修改字段 for (const [fieldName, newValue] of Object.entries(fieldUpdates)) { await this.updateField(fieldName, newValue); } console.log('✅ 配置修改完成'); } // 更新单个字段 async updateField(fieldName, newValue) { console.log(` 📝 修改 fieldName: newValue`); // 常见字段映射 const fieldSelectors = { // 基础字段 name: 'input[name="name"]', mcc: 'input[name="mcc"]', mnc: 'input[name="mnc"]', region_id: 'input[name="region_id"]', set_id: 'input[name="set_id"]', pointer: 'input[name="pointer"]', ngap_sip: 'input[name="ngap_sip"]', ngap_port: 'input[name="ngap_port"]', http2_sip: 'input[name="http2_sip"]', http2_port: 'input[name="http2_port"]', stac: 'input[name="stac"]', etac: 'input[name="etac"]', // 算法字段 'ea[NEA0]': 'input[name="ea[NEA0]"]', 'ea[128-NEA1]': 'input[name="ea[128-NEA1]"]', 'ea[128-NEA2]': 'input[name="ea[128-NEA2]"]', 'ea[128-NEA3]': 'input[name="ea[128-NEA3]"]', 'ia[NIA0]': 'input[name="ia[NIA0]"]', 'ia[128-NIA1]': 'input[name="ia[128-NIA1]"]', 'ia[128-NIA2]': 'input[name="ia[128-NIA2]"]', 'ia[128-NIA3]': 'input[name="ia[128-NIA3]"]', // 设备类型(特殊处理) deviceType: 'input[placeholder*="选择"], input[name*="device"]' }; // 设备类型特殊处理(支持仿真设备/被测设备) if (fieldName === 'deviceType' || fieldName === 'type') { try { await this.page.locator('.layui-unselect').first().click(); await this.page.waitForTimeout(300); const targetLabel = newValue === '被测设备' ? '被测设备' : '仿真设备'; await this.page.locator('dd').filter({ hasText: targetLabel }).click(); console.log(` ✅ deviceType 已设置为 targetLabel`); } catch (error) { console.log(` ❌ deviceType 设置失败: error.message`); } return; } // 算法字段特殊处理(点击 layui 可见复选框,与 amf-add 保持一致) if (fieldName.startsWith('ea[') || fieldName.startsWith('ia[')) { try { const algoMap = { 'ea[NEA0]': 0, 'ea[128-NEA1]': 1, 'ea[128-NEA2]': 2, 'ea[128-NEA3]': 3, 'ia[NIA0]': 4, 'ia[128-NIA1]': 5, 'ia[128-NIA2]': 6, 'ia[128-NIA3]': 7 }; const idx = algoMap[fieldName]; if (idx !== undefined) { await this.page.locator('.layui-form-checkbox').nth(idx).click(); await this.page.waitForTimeout(80); const inp = this.page.locator(`input[name="fieldName"]`); if (await inp.count() > 0) { await inp.fill(newValue.toString()); } console.log(` ✅ fieldName 已勾选并设置优先级为 newValue`); } } catch (error) { console.log(` ❌ fieldName 设置失败: error.message`); } return; } let selector = fieldSelectors[fieldName]; if (!selector) { // 尝试通用查找 selector = `input[name*="fieldName"], input[placeholder*="fieldName"]`; } try { const field = this.page.locator(selector).first(); const count = await field.count(); if (count > 0) { await field.click(); await field.fill(''); await field.fill(newValue.toString()); await this.page.waitForTimeout(100); console.log(` ✅ fieldName 修改成功`); } else { console.log(` ⚠️ 未找到字段: fieldName`); } } catch (error) { console.log(` ❌ 修改 fieldName 失败: error.message`); } } // 提交修改 async submitChanges() { console.log('📤 提交修改...'); const beforeUrl = this.page.url(); // 查找提交按钮 const submitButtons = await this.page.locator('button:has-text("提交"), input[type="submit"][value*="提交"]').all(); if (submitButtons.length === 0) { throw new Error('未找到提交按钮'); } await submitButtons[0].click(); await this.page.waitForTimeout(5000); const afterUrl = this.page.url(); const urlChanged = afterUrl !== beforeUrl; if (urlChanged) { console.log('✅ 提交成功,URL已变化'); } else { console.log('⚠️ URL未变化,但可能已提交成功'); } return urlChanged; } } /** * 选择工程函数(支持翻页) * @param {Page} page - Playwright页面对象 * @param {string} projectName - 工程名称 * @param {boolean} forceSwitch - 是否强制切换工程 */ async function selectProject(page, projectName, forceSwitch = true) { if (!forceSwitch) { console.log(` 🔧 保持当前工程(用户未指定工程)`); return true; } await page.goto(`CONFIG.baseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', timeout: 15000 }); await page.waitForSelector('.jsgrid-row, .jsgrid-alt-row', { timeout: 5000 }).catch(() => {}); // 清除搜索框,避免残留内容干扰 await page.evaluate(() => { const inputs = document.querySelectorAll('input[type="text"], input[name="name"]'); for (const inp of inputs) { inp.value = ''; } }); await page.waitForTimeout(300); for (let pageNum = 1; pageNum <= 200; pageNum++) { // 通过 evaluate 点击操作列 td:nth(1) 里的 ● 图标(iconfont layui-ext-xuanzhong1) const clicked = await page.evaluate((targetName) => { const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row'); for (const row of rows) { const cells = row.querySelectorAll('td'); if (cells.length >= 3 && cells[2].textContent.trim() === targetName) { // 操作列 td:nth(1) 里的 iconfont 元素是●选择图标 const icon = cells[1].querySelector('.iconfont'); if (icon) { icon.click(); return true; } } } return false; }, projectName); if (clicked) { await page.waitForTimeout(2000); return true; } const nextBtn = page.locator('.jsgrid-pager a:has-text("Next")'); if (!(await nextBtn.count())) break; try { await page.evaluate(() => { var links = document.querySelectorAll('.jsgrid-pager a'); for (var i = 0; i < links.length; i++) { if (links[i].innerText.trim() === 'Next') { links[i].click(); break; } } }); await page.waitForTimeout(2000); } catch { break; } } console.log(` ❌ 未找到工程 "projectName"(精确匹配)`); return false; } // 主执行函数 async function editAMF(amfName = null, projectName = '5G_basic_process', fieldUpdates = {}) { console.log('='.repeat(60)); console.log('🚀 AMF配置修改技能 - 智能版本'); console.log('='.repeat(60)); const startTime = Date.now(); const sessionManager = new SessionManager(); let browser = null; let skippedLogin = false; try { // 1. 初始化浏览器 console.log('1. 初始化...'); browser = await chromium.launch({ headless: true, args: ['--no-sandbox', '--ignore-certificate-errors'] }); // 2. 尝试加载会话 let context = await sessionManager.loadSession(browser); if (context) { console.log('✅ 使用缓存会话,跳过登录'); skippedLogin = true; } else { console.log('📝 需要登录'); context = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } }); } const page = await context.newPage(); // 3. 登录(如果需要) if (!skippedLogin) { console.log('2. 登录...'); await page.goto(`CONFIG.baseUrlCONFIG.urls.login`, { waitUntil: 'networkidle', timeout: 15000 }); await page.getByRole('textbox', { name: 'E-Mail地址' }).fill(CONFIG.credentials.email); await page.getByRole('textbox', { name: '密码' }).fill(CONFIG.credentials.password); await page.getByRole('button', { name: '登录' }).click(); await page.waitForLoadState('networkidle', { timeout: 10000 }); await page.waitForTimeout(1000); // 保存会话 await sessionManager.saveSession(context); console.log('✅ 登录成功,会话已保存'); } // 4. 选择工程 console.log('3. 选择工程...'); if (!(await selectProject(page, projectName, true))) { throw new Error(`工程 "projectName" 不存在或无法选中`); } // 4. 智能选择AMF(支持单条和批量) console.log('4. 智能选择AMF...'); const selector = new AMFSelector(page); const selected = await selector.selectAMF(amfName); // 5. 进入编辑页面(单条或批量) console.log('5. 进入编辑页面...'); // 批量模式:selected 是 AMF 数组 if (Array.isArray(selected)) { let batchSuccess = 0; for (const amf of selected) { process.stdout.write(` ▶ amf.name [amf.id] ... `); try { await selector.clickEditButton(amf); const modifier = new ConfigModifier(page); await modifier.modifyConfig(fieldUpdates); const urlChanged = await modifier.submitChanges(); await page.waitForTimeout(2000); const ok = page.url().includes('/amf/index'); console.log(ok ? '✅' : '❌'); if (ok) batchSuccess++; // 返回列表页继续下一个 await page.goto(`CONFIG.baseUrlCONFIG.urls.amfManagement`, { waitUntil: 'networkidle', timeout: 10000 }); await page.waitForTimeout(1500); } catch (e) { console.log(`❌ e.message`); } await page.waitForTimeout(500); } await browser.close(); const totalTime = (Date.now() - startTime) / 1000; console.log(`\n批量完成: batchSuccess/selected.length 成功 耗时: totalTime.toFixed(1)s`); return { success: batchSuccess > 0, batchSummary: { total: selected.length, success: batchSuccess } }; } // 单条模式 const selectedAMF = selected; await selector.clickEditButton(selectedAMF); // 验证是否在编辑页面 if (!page.url().includes('/amf/edit')) { console.log('⚠️ 当前URL:', page.url()); console.log('尝试直接访问编辑页面...'); await page.goto(`CONFIG.baseUrlCONFIG.urls.amfEdit`, { waitUntil: 'networkidle', timeout: 10000 }); } console.log('✅ 进入编辑页面'); // 7. 修改配置 console.log('6. 修改配置...'); const modifier = new ConfigModifier(page); await modifier.modifyConfig(fieldUpdates); // 8. 提交修改 console.log('7. 提交修改...'); const urlChanged = await modifier.submitChanges(); // 9. 验证结果 console.log('8. 验证结果...'); await page.goto(`CONFIG.baseUrlCONFIG.urls.amfManagement`, { waitUntil: 'networkidle', timeout: 10000 }); await page.waitForTimeout(2000); const pageText = await page.textContent('body').catch(() => ''); const found = pageText.includes(selectedAMF.name); // 9. 清理 await browser.close(); const totalTime = (Date.now() - startTime) / 1000; console.log('\n' + '='.repeat(60)); console.log('📊 执行结果'); console.log('='.repeat(60)); if (found) { console.log(`✅ 成功!AMF "selectedAMF.name" 修改完成`); console.log(`⏱️ 总耗时: totalTime.toFixed(2)秒`); console.log(`🔗 URL变化: '否'`); console.log(`🔑 登录优化: '重新登录'`); console.log(`📝 修改字段: Object.keys(fieldUpdates).length 个`); if (Object.keys(fieldUpdates).length > 0) { console.log('修改详情:'); Object.entries(fieldUpdates).forEach(([field, value]) => { console.log(` - field: value`); }); } return { success: true, amfName: selectedAMF.name, totalTime: totalTime, urlChanged: urlChanged, skippedLogin: skippedLogin, modifiedFields: Object.keys(fieldUpdates) }; } else { console.log(`❌ 失败!未找到配置 "selectedAMF.name"`); console.log(`⏱️ 总耗时: totalTime.toFixed(2)秒`); return { success: false, amfName: selectedAMF.name, totalTime: totalTime }; } } catch (error) { console.error(`❌ 执行失败: error.message`); if (browser) { await browser.close(); } return { success: false, amfName: amfName, error: error.message, totalTime: (Date.now() - startTime) / 1000 }; } } // 参数解析 function parseArgs() { const args = process.argv.slice(2); const result = { amfName: null, projectName: '5G_basic_process', // 默认工程 baseUrl: globalBaseUrl, // 默认5GC地址 fieldUpdates: {} }; for (let i = 0; i < args.length; i++) { const arg = args[i]; if (arg.startsWith('--')) { const key = arg.substring(2); if (i + 1 < args.length && !args[i + 1].startsWith('--')) { // 字段修改参数: --field value if (key.startsWith('set-')) { const fieldName = key.substring(4); result.fieldUpdates[fieldName] = args[i + 1]; i++; } else if (key === 'amf' || key === 'name') { result.amfName = args[i + 1]; i++; } else if (key === 'project' || key === 'p') { result.projectName = args[i + 1]; i++; } else if (key === 'url') { result.baseUrl = args[i + 1]; if (!result.baseUrl.startsWith('http')) result.baseUrl = 'https://' + result.baseUrl; globalBaseUrl = result.baseUrl; CONFIG.baseUrl = result.baseUrl; // 同步更新CONFIG i++; } } else if (key === 'project' || key === 'p') { // --project 但没有指定值,使用默认值 console.log('⚠️ 使用了--project参数但未指定工程名称,将使用默认工程'); } } else if (i === 0 && !arg.startsWith('--')) { // 第一个非选项参数作为AMF名称 result.amfName = arg; } } return result; } // 帮助信息 function showHelp() { console.log('='.repeat(60)); console.log('🚀 AMF配置修改技能 - 使用说明'); console.log('='.repeat(60)); console.log('\n📋 功能特性:'); console.log(' 1. 智能工程选择(支持默认/指定工程)'); console.log(' 2. 智能AMF选择(按名称匹配)'); console.log(' 3. 支持部分字段修改'); console.log(' 4. 登录状态优化(会话缓存)'); console.log(' 5. 基于文字匹配,不使用固定ID'); console.log('\n🚀 使用方法:'); console.log(' node amf-edit-skill.js <AMF名称> [选项]'); console.log(' node amf-edit-skill.js --amf <名称> [选项]'); console.log('\n🎯 示例:'); console.log(' # 修改指定AMF的IP地址'); console.log(' node amf-edit-skill.js qqqwww --set-ngap_sip 192.168.99.99'); console.log(' # 修改多个字段'); console.log(' node amf-edit-skill.js test_001 --set-ngap_sip 10.0.0.1 --set-http2_sip 10.0.0.2'); console.log(' # 自动选择(只有一个AMF时)'); console.log(' node amf-edit-skill.js --set-stac 200'); console.log('\n🔧 可修改字段:'); console.log(' --set-name <新名称> 修改AMF名称'); console.log(' --set-mcc <值> 修改MCC'); console.log(' --set-mnc <值> 修改MNC'); console.log(' --set-region_id <值> 修改Region ID'); console.log(' --set-set_id <值> 修改Set ID'); console.log(' --set-pointer <值> 修改Pointer'); console.log(' --set-ngap_sip <IP> 修改NGAP SIP地址'); console.log(' --set-ngap_port <端口> 修改NGAP端口'); console.log(' --set-http2_sip <IP> 修改HTTP2 SIP地址'); console.log(' --set-http2_port <端口> 修改HTTP2端口'); console.log(' --set-stac <值> 修改起始TAC'); console.log(' --set-etac <值> 修改结束TAC'); console.log(' --set-ea[NEA0] <优先级> 修改加密算法NEA0'); console.log(' --set-ea[128-NEA1] <优先级> ...'); console.log(' --set-ia[NIA0] <优先级> 修改完保算法NIA0'); console.log(' --set-deviceType <类型> 修改设备类型(仿真设备 / 被测设备)'); console.log('\n🔧 工程选择:'); console.log(' --project, -p <工程名> 指定工程(默认: 5G_basic_process)'); console.log(' # 不指定工程时自动选择 "5G_basic_process"'); console.log(' # 指定工程示例: node amf-edit-skill.js qqqwww --project 5G_basic_process'); console.log('\n🎯 智能选择逻辑:'); console.log(' 工程选择:'); console.log(' 1. 用户指定工程 → 自动切换到指定工程'); console.log(' 2. 未指定工程 → 自动选择 "5G_basic_process"'); console.log(' AMF选择:'); console.log(' 1. 用户指定AMF名称 → 按名称查找'); console.log(' 2. 未指定名称 + 单个AMF → 自动选择'); console.log(' 3. 未指定名称 + 多个AMF → 反问用户'); console.log('\n💡 提示:'); console.log(' - 字段名称区分大小写'); console.log(' - 支持精确匹配和模糊匹配'); console.log(' - 登录状态缓存24小时有效'); console.log('\n' + '='.repeat(60)); } // 主函数 async function main() { const args = parseArgs(); // 显示帮助 if (args.amfName === '--help' || args.amfName === '-h' || process.argv.includes('--help')) { showHelp(); process.exit(0); } // 显示版本 if (args.amfName === '--version' || args.amfName === '-v' || process.argv.includes('--version')) { console.log('AMF编辑技能 v1.0 (智能版本)'); console.log('状态: 🚧 开发中'); console.log('更新: 2026-03-22'); process.exit(0); } console.log('🎯 任务配置:'); console.log(` AMF名称: args.amfName || '(自动选择)'`); console.log(` 工程选择: args.projectName '(用户指定)'`); console.log(` 修改字段: Object.keys(args.fieldUpdates).length 个`); if (Object.keys(args.fieldUpdates).length > 0) { console.log(' 修改详情:'); Object.entries(args.fieldUpdates).forEach(([field, value]) => { console.log(` - field: value`); }); } // 执行编辑 const result = await editAMF(args.amfName, args.projectName, args.fieldUpdates); console.log('\n' + '='.repeat(60)); console.log('完成时间:', new Date().toLocaleString()); console.log('='.repeat(60)); process.exit(result.success ? 0 : 1); } // 执行 main().catch(error => { console.error('程序异常:', error); process.exit(1); }); FILE:scripts/ausf-udm-add-skill.js #!/usr/bin/env node /** * AUSF/UDM 添加技能 * * 用法: node ausf-udm-add-skill.js <名称> [--url IP] [--project 工程] [--count N] [--sip IP] [--port N] * * 示例: * node ausf-udm-add-skill.js myudf --project basic_5g_onoff --url 192.168.3.89 * node ausf-udm-add-skill.js UDM_TEST --url 192.168.3.89 --project 5G_basic_process */ const { chromium } = require('playwright'); const fs = require('fs'); const path = require('path'); // ==================== 配置 ==================== let globalBaseUrl = 'https://192.168.3.89'; const CONFIG = { credentials: { email: '[email protected]', password: 'dotouch' }, urls: { login: '/login', ausfEdit: '/sim_5gc/ausf/edit', ausfManagement: '/sim_5gc/ausf/index' }, sessionDir: path.join(__dirname, '.sessions'), getSessionFile() { const host = globalBaseUrl.replace(/https?:\/\//, '').replace(/\./g, '_'); return `5gc_session_host.json`; } }; // 会话管理 class SessionManager { constructor() { this.sessionPath = CONFIG.getSessionFile(); } async saveSession(context) { try { const storageState = await context.storageState(); fs.writeFileSync(this.sessionPath, JSON.stringify({ storageState }, null, 2)); return true; } catch { return false; } } async loadSession(browser) { try { if (!fs.existsSync(this.sessionPath)) return null; const { storageState } = JSON.parse(fs.readFileSync(this.sessionPath, 'utf8')); return await browser.newContext({ storageState, ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } }); } catch { return null; } } } // ==================== 参数解析 ==================== function parseArgs() { const args = process.argv.slice(2); let baseUrl = globalBaseUrl; const result = { name: null, project: '5G_basic_process', config: { type: '1', count: '1', sip: '192.168.20.30', port: '80' } }; for (let i = 0; i < args.length; i++) { const arg = args[i]; if (!arg.startsWith('-')) { result.name = arg; } else if (arg === '--url' || arg === '-u') { let url = args[++i]; if (url && !url.startsWith('http')) url = 'https://' + url; baseUrl = url; globalBaseUrl = baseUrl; } else if (arg === '--project' || arg === '-p') { result.project = args[++i]; } else if (arg.startsWith('--')) { result.config[arg.substring(2)] = args[++i]; } } if (!result.name) { console.error('用法: node ausf-udm-add-skill.js <名称> [--project 工程] [--url 地址] [--sip IP] [--port N]'); process.exit(1); } return result; } // ==================== 工程选择(支持翻页,与 amf-add 逻辑一致)==================== async function selectProject(page, projectName, forceSwitch = true) { if (!forceSwitch) { console.log(' 🔧 保持当前工程'); return true; } await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', timeout: 15000 }); await page.waitForSelector('.jsgrid-row, .jsgrid-alt-row', { timeout: 5000 }).catch(() => {}); await page.waitForTimeout(300); for (let pageNum = 1; pageNum <= 200; pageNum++) { const clicked = await page.evaluate((targetName) => { const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row'); for (const row of rows) { const cells = row.querySelectorAll('td'); if (cells.length >= 3 && cells[2].textContent.trim() === targetName) { const icon = cells[1].querySelector('.iconfont'); if (icon) { icon.click(); return true; } } } return false; }, projectName); if (clicked) { await page.waitForTimeout(2000); return true; } const nextBtn = page.locator('.jsgrid-pager a:has-text("Next")'); if (!(await nextBtn.count())) break; try { await page.evaluate(() => { var links = document.querySelectorAll('.jsgrid-pager a'); for (var i = 0; i < links.length; i++) { if (links[i].innerText.trim() === 'Next') { links[i].click(); break; } } }); await page.waitForTimeout(2000); } catch { break; } } console.log(` ❌ 未找到工程 "projectName"`); return false; } // ==================== 主流程 ==================== async function addAusf(ausfName, projectName, explicitProject, ausfConfig) { const startTime = Date.now(); const sessionManager = new SessionManager(); const cfg = { ...{ type: '1', count: '1', sip: '192.168.20.30', port: '80' }, ...ausfConfig }; let browser = null; try { browser = await chromium.launch({ headless: true, args: ['--no-sandbox', '--ignore-certificate-errors'] }); let context = await sessionManager.loadSession(browser); let needLogin = true; if (context) { const testPage = await context.newPage(); await testPage.goto(`globalBaseUrlCONFIG.urls.ausfManagement`, { waitUntil: 'networkidle', timeout: 10000 }).catch(() => {}); if (!testPage.url().includes('/login')) needLogin = false; await testPage.close(); } if (needLogin) { context = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } }); } const page = await context.newPage(); if (needLogin) { await page.goto(`globalBaseUrlCONFIG.urls.login`, { waitUntil: 'networkidle', timeout: 15000 }); await page.getByRole('textbox', { name: 'E-Mail地址' }).fill(CONFIG.credentials.email); await page.getByRole('textbox', { name: '密码' }).fill(CONFIG.credentials.password); await page.getByRole('button', { name: '登录' }).click(); await page.waitForLoadState('networkidle', { timeout: 10000 }); await sessionManager.saveSession(context); } // 选择工程 if (!(await selectProject(page, projectName, explicitProject))) { throw new Error(`工程 "projectName" 不存在或无法选中`); } // 进入编辑页面 await page.goto(`globalBaseUrlCONFIG.urls.ausfEdit`, { waitUntil: 'networkidle', timeout: 15000 }); if (!page.url().includes('/ausf/edit')) { await page.goto(`globalBaseUrl/sim_5gc/ausf/edit`); await page.waitForSelector('input[name="name"]', { timeout: 10000 }); } // 填写表单 await page.evaluate(({ ausfName, cfg }) => { const set = (name, value) => { const el = document.querySelector(`input[name="name"]`); if (el) { el.value = value; el.dispatchEvent(new Event('input', { bubbles: true })); } }; set('name', ausfName); set('sip', cfg.sip); set('port', cfg.port); set('count', cfg.count); }, { ausfName, cfg }); // 类型选择:仿真设备 await page.locator('.layui-unselect').first().click(); await page.waitForTimeout(300); await page.locator('dd').filter({ hasText: '仿真设备' }).click(); // 提交表单 await page.getByRole('button', { name: '提交' }).click(); // 等待页面跳转 try { await page.waitForURL(`**/ausf/index`, { timeout: 8000 }); } catch (e) { await page.goto(`globalBaseUrlCONFIG.urls.ausfManagement`, { waitUntil: 'networkidle', timeout: 15000 }); } await page.waitForTimeout(2000); // 验证结果:只要页面跳转到列表页即认为成功 let found = false; const finalUrl = page.url(); if (finalUrl.includes('/ausf/index')) { console.log(` ✅ 页面已跳转至 AUSF/UDM 列表: finalUrl`); found = true; } await browser.close(); const totalTime = (Date.now() - startTime) / 1000; if (found) { return { success: true, ausfName, totalTime }; } else { return { success: false, ausfName, totalTime }; } } catch (err) { if (browser) await browser.close(); throw err; } } // ==================== 启动 ==================== async function main() { const args = parseArgs(); console.log(`AUSF/UDM: args.name | 工程: args.project | 地址: globalBaseUrl`); try { const result = await addAusf(args.name, args.project, true, args.config); console.log(result.success ? `成功! AUSF/UDM "result.ausfName" 添加完成 (result.totalTime.toFixed(2)s)` : `失败! 未找到 AUSF/UDM "result.ausfName"`); process.exit(result.success ? 0 : 1); } catch (err) { console.error(`执行异常: err.message`); process.exit(1); } } main(); FILE:scripts/ausf-udm-edit-skill.js #!/usr/bin/env node /** * AUSF/UDM 编辑技能(单条+批量二合一) * * 单个编辑: node ausf-udm-edit-skill.js <名称> --project <工程> --set-<字段> <值> * node ausf-udm-edit-skill.js --name <名称> --project <工程> --set-<字段> <值> * node ausf-udm-edit-skill.js --id <ID> --set-<字段> <值> * * 批量编辑: node ausf-udm-edit-skill.js --project <工程> --set-<字段> <值> * * 可编辑字段: sip, port, mcc, mnc, ngap_port, http2_port * * 示例: * node ausf-udm-edit-skill.js --project XW_S5GC_1 --set-sip 10.0.0.99 (批量) * node ausf-udm-edit-skill.js --name udm --project XW_S5GC_1 --set-sip 10.0.0.99 * node ausf-udm-edit-skill.js --id 4772 --set-sip 10.0.0.99 */ const { chromium } = require('playwright'); const path = require('path'); const fs = require('fs'); // 全局配置 let globalBaseUrl = 'https://192.168.3.89'; const CONFIG = { sessionDir: path.join(__dirname, '.sessions'), getSessionFile() { const host = globalBaseUrl.replace(/https?:\/\//, '').replace(/\./g, '_'); return `5gc_session_host.json`; }, credentials: { email: '[email protected]', password: 'dotouch' }, urls: { login: '/login', ausfManagement: '/sim_5gc/ausf/index', } }; // 会话管理 class SessionManager { constructor() { this.sessionPath = path.join(CONFIG.sessionDir, CONFIG.getSessionFile()); } async saveSession(context) { try { const storageState = await context.storageState(); fs.writeFileSync(this.sessionPath, JSON.stringify({ storageState }, null, 2)); } catch {} } async loadSession(browser) { try { if (!fs.existsSync(this.sessionPath)) return null; const { storageState } = JSON.parse(fs.readFileSync(this.sessionPath, 'utf8')); return await browser.newContext({ storageState, ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } }); } catch { return null; } } } // ─── 参数解析 ───────────────────────────────────────────────── function parseArgs() { const args = process.argv.slice(2); const result = { name: null, project: 'XW_S5GC_1', targetId: null, fields: {} }; for (let i = 0; i < args.length; i++) { const arg = args[i]; if (arg === '--help' || arg === '-h') { printHelp(); process.exit(0); } else if (!arg.startsWith('-')) { if (!result.name) result.name = arg; } else if (arg === '--url') { globalBaseUrl = args[++i]; } else if (arg === '--project' || arg === '-p') { result.project = args[++i]; } else if (arg === '--name') { result.name = args[++i]; } else if (arg === '--id') { result.targetId = args[++i]; } else if (arg.startsWith('--set-')) { const key = arg.substring(6).replace(/-/g, '_'); result.fields[key] = args[++i]; } else if (arg.startsWith('--')) { const key = arg.substring(2).replace(/-/g, '_'); result.fields[key] = args[++i]; } } return result; } function printHelp() { console.log(` AUSF/UDM 编辑(单条+批量二合一) =============================== 用法: node ausf-udm-edit-skill.js --project <工程> --set-<字段> <值> (批量) node ausf-udm-edit-skill.js --name <名称> --project <工程> --set-<字段> <值> node ausf-udm-edit-skill.js --id <ID> --set-<字段> <值> 可编辑字段: sip, port, mcc, mnc, ngap_port, http2_port `); } // ─── 工程选择 ───────────────────────────────────────────────── async function selectProject(page, projectName) { await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', timeout: 15000 }); await page.waitForTimeout(2000); for (let pageNum = 1; pageNum <= 20; pageNum++) { const clicked = await page.evaluate((name) => { const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row'); for (const row of rows) { const cells = row.querySelectorAll('td'); if (cells.length >= 3 && cells[2].textContent.trim() === name) { if (row.classList.contains('jsgrid-selected-row')) return 'already'; const icon = cells[1].querySelector('.iconfont'); if (icon) { icon.click(); return 'clicked'; } const td = row.querySelector('td.project_select'); if (td) { td.click(); return 'clicked'; } row.click(); return 'clicked'; } } return 'not-found'; }, projectName); if (clicked !== 'not-found') { await page.waitForTimeout(2000); return true; } const hasNext = await page.evaluate(() => Array.from(document.querySelectorAll('.jsgrid-pager a')).some(a => a.innerText.trim() === 'Next')); if (!hasNext) break; await page.evaluate(() => { const links = document.querySelectorAll('.jsgrid-pager a'); for (const l of links) { if (l.innerText.trim() === 'Next') { l.click(); break; } } }); await page.waitForTimeout(1500); } return false; } // ─── 查找 AUSF 列表 ─────────────────────────────────────────── async function findAusfList(page) { const sidebarUrl = await page.evaluate(() => { const links = document.querySelectorAll('a[href*="/ausf/index"]'); return links[0]?.href || ''; }); if (!sidebarUrl) throw new Error('未找到 AUSF 侧边栏链接'); let ausfList = []; await page.goto(sidebarUrl, { waitUntil: 'networkidle', timeout: 15000 }); await page.waitForTimeout(2000); for (let pageNum = 1; pageNum <= 50; pageNum++) { const rows = await page.locator('.layui-table tbody tr').all(); for (const row of rows) { const cells = await row.locator('td').all(); if (cells.length >= 3) { const id = (await cells[1].textContent()).trim(); const n = (await cells[2].textContent()).trim(); if (id && id.match(/^\d+$/) && n && n !== '名称' && n !== '编辑') { if (!ausfList.some(u => u.id === id)) ausfList.push({ id, name: n }); } } } const hasNext = await page.evaluate(() => Array.from(document.querySelectorAll('.layui-table .layui-laypage a')) .some(a => a.innerText.trim() === 'Next')); if (!hasNext) break; await page.evaluate(() => { const links = document.querySelectorAll('.layui-table .layui-laypage a'); for (const l of links) { if (l.innerText.trim() === 'Next') { l.click(); break; } } }); await page.waitForTimeout(2000); } return ausfList; } // ─── 修改单个 AUSF ──────────────────────────────────────────── async function modifyAndSubmit(page, ausfId, fields) { await page.goto(`globalBaseUrl/sim_5gc/ausf/edit/ausfId`, { waitUntil: 'networkidle', timeout: 15000 }); await page.waitForTimeout(3000); if (fields.sip) { await page.locator('input[name="sip"]').fill(fields.sip); } if (fields.port) { await page.locator('input[name="port"]').fill(fields.port); } if (fields.mcc) { await page.locator('input[name="mcc"]').fill(fields.mcc); } if (fields.mnc) { await page.locator('input[name="mnc"]').fill(fields.mnc); } if (fields.ngap_port) { await page.locator('input[name="ngap_port"]').fill(fields.ngap_port); } if (fields.http2_port) { await page.locator('input[name="http2_port"]').fill(fields.http2_port); } await page.getByRole('button', { name: '提交' }).click(); await page.waitForTimeout(3000); try { await page.waitForURL('**/ausf/index', { timeout: 5000 }); } catch {} return page.url().includes('/ausf/index'); } // ─── 主函数 ────────────────────────────────────────────────── async function main() { const args = parseArgs(); if (Object.keys(args.fields).length === 0) { console.error('请至少指定一个 --set-<字段>'); printHelp(); process.exit(1); } console.log('▶ AUSF/UDM 编辑 工程:', args.project, ' 字段:', JSON.stringify(args.fields)); const browser = await chromium.launch({ headless: true, args: ['--no-sandbox', '--ignore-certificate-errors'] }); const sessionManager = new SessionManager(); let context = await sessionManager.loadSession(browser); let needLogin = true; if (context) { const testPage = await context.newPage(); await testPage.goto(`globalBaseUrlCONFIG.urls.ausfManagement`, { waitUntil: 'networkidle', timeout: 8000 }).catch(() => {}); if (!testPage.url().includes('/login')) needLogin = false; await testPage.close(); } if (needLogin) context = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } }); const page = await context.newPage(); if (needLogin) { console.log(' 🔐 登录...'); await page.goto(`globalBaseUrlCONFIG.urls.login`, { waitUntil: 'networkidle', timeout: 15000 }); await page.getByRole('textbox', { name: 'E-Mail地址' }).fill(CONFIG.credentials.email); await page.getByRole('textbox', { name: '密码' }).fill(CONFIG.credentials.password); await page.getByRole('button', { name: '登录' }).click(); await page.waitForLoadState('networkidle'); await sessionManager.saveSession(context); console.log(' ✅ 登录成功'); } else { console.log(' ✅ 使用缓存会话'); } if (!await selectProject(page, args.project)) { console.log(' ❌ 工程未找到:', args.project); await browser.close(); process.exit(1); } console.log(' ✅ 工程已选'); // ── 批量模式:无 name 且无 targetId ────────────────────── if (!args.name && !args.targetId) { const ausfList = await findAusfList(page); if (ausfList.length === 0) { console.log(' ⚠ 没有 AUSF/UDM'); await browser.close(); process.exit(0); } console.log(' 找到', ausfList.length, '个 AUSF/UDM,进入批量模式'); let success = 0; for (const { id, name } of ausfList) { process.stdout.write(' ▶ [' + id + '] ' + name + ' ... '); try { const ok = await modifyAndSubmit(page, id, args.fields); console.log(ok ? '✅' : '❌'); if (ok) success++; } catch (e) { console.log('❌', e.message); } await page.waitForTimeout(500); } await browser.close(); console.log('\n完成:', success + '/' + ausfList.length, '成功'); process.exit(success > 0 ? 0 : 1); } // ── 单个模式 ──────────────────────────────────────────── let resolvedId = args.targetId; if (!resolvedId) { const ausfList = await findAusfList(page); const target = ausfList.find(u => u.name === args.name); if (!target) { console.log(' ❌ 未找到:', args.name); await browser.close(); process.exit(1); } resolvedId = target.id; } process.stdout.write(' → AUSF/UDM ' + resolvedId + ' ... '); try { const ok = await modifyAndSubmit(page, resolvedId, args.fields); console.log(ok ? '✅' : '❌'); await browser.close(); process.exit(ok ? 0 : 1); } catch (e) { console.log('❌', e.message); await browser.close(); process.exit(1); } } main().catch(e => { console.error('错误:', e.message); process.exit(1); }); FILE:scripts/gnb-add-skill.js #!/usr/bin/env node /** * GNB 添加脚本 * 用法: node gnb-add-skill.js <名称> [--headed] [--ngap_sip IP] [--project 工程] * 示例: node gnb-add-skill.js GNB-TEST * node gnb-add-skill.js GNB-TEST --ngap_sip 200.20.20.50 --headed */ const { chromium } = require('playwright'); const fs = require('fs'); const path = require('path'); let BASE_URL = 'https://192.168.3.89'; const CONFIG = { urls: { login: '/login', gnbList: '/sim_5gc/gnb/index' }, credentials: { email: '[email protected]', password: 'dotouch' }, sessionDir: path.join(__dirname, '.sessions'), getSessionFile() { return `5gc_gnb_add_\/\//, '').replace(/\./g, '_').json`; } }; class SessionManager { constructor() { if (!fs.existsSync(CONFIG.sessionDir)) fs.mkdirSync(CONFIG.sessionDir, { recursive: true }); this.sp = path.join(CONFIG.sessionDir, CONFIG.getSessionFile()); } async loadSession(browser) { if (!fs.existsSync(this.sp)) return null; try { const { storageState } = JSON.parse(fs.readFileSync(this.sp, 'utf8')); return await browser.newContext({ storageState, ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } }); } catch { return null; } } async saveSession(context) { fs.writeFileSync(this.sp, JSON.stringify({ storageState: await context.storageState() }, null, 2)); } } // 选择工程 async function selectProject(page, projectName) { await page.goto(`BASE_URL/sim_5gc/project/index`, { waitUntil: 'networkidle', timeout: 15000 }); try { await page.waitForSelector('.jsgrid-row, .jsgrid-alt-row', { timeout: 10000 }); } catch { console.log(' ⚠ 等待 jsgrid 行超时'); } await page.waitForTimeout(2000); for (let pageNum = 1; pageNum <= 20; pageNum++) { // Try Playwright click on .project_select td first (most reliable) const found = await page.evaluate((target) => { const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row'); for (const row of rows) { const cells = row.querySelectorAll('td'); if (cells.length >= 3 && cells[2].textContent.trim() === target) { const td = row.querySelector('td.project_select'); if (td) { td.click(); return true; } } } return false; }, projectName); if (found) { await page.waitForTimeout(2000); return true; } const nextBtn = page.locator('.jsgrid-pager a').filter({ hasText: 'Next' }).first(); if (!(await nextBtn.count())) { console.log(' ⚠ 已到最后一页'); break; } try { await page.evaluate(() => { var links = document.querySelectorAll('.jsgrid-pager a'); for (var i = 0; i < links.length; i++) { if (links[i].innerText.trim() === 'Next') { links[i].click(); break; } } }); await page.waitForTimeout(2000); } catch { break; } } console.log(` ❌ 未找到工程 "projectName"`); return false; } async function main() { const args = process.argv.slice(2); if (args.length < 1) { console.log('用法: node gnb-add-skill.js <名称> [--project <工程>] [--ngap_sip <IP>] [--headed]'); console.log(' [--user_sip_ip_v4 <IP>] [--mcc <值>] [--mnc <值>]'); console.log(' [--stac <值>] [--etac <值>] [--node_id <ID>] [--cell_count <数量>]'); console.log('示例: node gnb-add-skill.js GNB-TEST --project XW_S5GC_1'); console.log(' node gnb-add-skill.js GNB-PROD --ngap_sip 200.20.20.100 --mcc 460 --mnc 60 --stac 1 --etac 10'); process.exit(1); } const gnbName = args[0]; let headless = true; let project = 'XW_S5GC_1'; let ngap_sip = '200.20.20.50'; let user_sip_ip_v4 = '2.2.2.2'; let mcc = '460'; let mnc = '60'; let stac = '0'; let etac = '0'; let node_id = '70'; let cell_count = '1'; for (let i = 1; i < args.length; i++) { if (args[i] === '--headed') { headless = false; } else if ( args[i] === '--ngap_sip') { ngap_sip = args[++i]; } else if (args[i] === '--project') { project = args[++i]; } else if (args[i] === '--user_sip_ip_v4') { user_sip_ip_v4 = args[++i]; } else if (args[i] === '--mcc') { mcc = args[++i]; } else if (args[i] === '--mnc') { mnc = args[++i]; } else if (args[i] === '--stac') { stac = args[++i]; } else if (args[i] === '--etac') { etac = args[++i]; } else if (args[i] === '--node_id') { node_id = args[++i]; } else if (args[i] === '--cell_count') { cell_count = args[++i]; } } console.log(`▶ 添加 GNB: gnbName`); console.log(` ngap_sip=ngap_sip user_sip_ip_v4=user_sip_ip_v4 mcc=mcc mnc=mnc`); console.log(` TAC=stac~etac node_id=node_id cell_count=cell_count`); console.log(` 模式: '有头' 工程: project`); const sessionMgr = new SessionManager(); const browser = await chromium.launch({ headless, args: ['--no-sandbox', '--ignore-certificate-errors'] }); const context = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } }); const page = await context.newPage(); // 登录 await page.goto(`BASE_URLCONFIG.urls.login`, { waitUntil: 'networkidle' }); await page.getByRole('textbox', { name: 'E-Mail地址' }).fill(CONFIG.credentials.email); await page.getByRole('textbox', { name: '密码' }).fill(CONFIG.credentials.password); await page.getByRole('checkbox', { name: '记住我' }).check(); await page.getByRole('button', { name: '登录' }).click(); await page.waitForLoadState('networkidle'); await sessionMgr.saveSession(context); console.log(' ✓ 登录成功'); // 选工程 const ok = await selectProject(page, project); if (!ok) throw new Error('工程选择失败'); console.log(' ✓ 工程已选'); // 进 GNB 列表,点添加 await page.goto(`BASE_URLCONFIG.urls.gnbList`, { waitUntil: 'networkidle' }); await page.waitForTimeout(2000); await page.locator('button:has-text("添加"), a:has-text("添加")').first().click(); await page.waitForTimeout(2000); console.log(' ✓ 进入添加页面'); // ===== 填写表单(用 page.fill 确保 layui 能收到事件) ===== // 1. 名称 await page.locator('input[name="name"]').fill(gnbName); console.log(` ✓ name = gnbName`); // 2. 类型下拉:点击打开,再用 Playwright 点击 dd 选项(触发真实事件) await page.locator('.layui-form-select >> nth=0').click(); await page.waitForTimeout(500); await page.locator('dd[lay-value="1"] >> nth=0').click(); await page.waitForTimeout(500); console.log(' ✓ 类型 = 仿真设备'); // 3. 数量 await page.locator('input[name="count"]').fill(cell_count); console.log(` ✓ count = cell_count`); // 4. NGAP起始IP await page.locator('input[name="ngap_sip"]').fill(ngap_sip); console.log(` ✓ ngap_sip = ngap_sip`); // 5. 用户面起始IP(SBI IP地址) await page.locator('input[name="user_sip_ip_v4"]').fill(user_sip_ip_v4); console.log(` ✓ user_sip_ip_v4 = user_sip_ip_v4`); // 6. MCC/MNC await page.locator('textarea[name="mcc"]').fill(mcc); await page.locator('textarea[name="mnc"]').fill(mnc); console.log(` ✓ mcc = mcc, mnc = mnc`); // 7. TAC范围 await page.locator('input[name="stac"]').fill(stac); await page.locator('input[name="etac"]').fill(etac); console.log(` ✓ TAC = stac~etac`); // 8. node_id await page.locator('input[name="node_id"]').fill(node_id); console.log(` ✓ node_id = node_id`); console.log(' ✓ 表单填写完成'); // 拦截提交后的 response let submitResponse = null; page.on('response', res => { if (res.url().includes('/sim_5gc/gnb/') && res.status() >= 200 && res.status() < 400) { submitResponse = { status: res.status(), url: res.url() }; } }); // 提交 await page.getByRole('button', { name: '确定' }).click(); await page.waitForTimeout(3000); console.log(' 最终 URL:', page.url()); // 判断成功:重定向到 /gnb/index,或者收到 200/302 响应 const url = page.url(); let success = false; if (url.includes('gnb/index')) { // 进一步检查:看是否还在添加页(可能失败) const onEditPage = await page.evaluate(() => document.querySelector('input[name="name"]') !== null); success = !onEditPage; } if (success) { console.log(' ✅ 添加成功'); } else { console.log(' ⚠ 可能未保存,请检查页面'); // 检查错误信息 const errorMsg = await page.evaluate(() => { const err = document.querySelector('.layui-layer-msg, .layui-layer-content'); return err ? err.textContent.trim() : ''; }); if (errorMsg) console.log(' 错误信息:', errorMsg); } await browser.close(); } main().catch(e => { console.error('异常:', e.message); process.exit(1); }); FILE:scripts/gnb-edit-skill.js #!/usr/bin/env node /** * GNB 编辑脚本(单条 + 批量二合一) * 用法: node gnb-edit-skill.js [--project 工程] [--headed] --set-<字段名> <值> * 示例: node gnb-edit-skill.js --project XW_S5GC_basic --set-replay_ip 200.20.20.250 * node gnb-edit-skill.js --project XW_S5GC_basic --set-stac 0 --set-etac 0 --headed * * 支持的字段: name, count, ngap_sip, ngap_port, user_sip_ip_v4, user_sip_ip_v6, * mcc, mnc, stac, etac, node_id, node_id_len, replay_ip, replay_port, * cell_count, position_x, position_y, gateway_ind */ const { chromium } = require('playwright'); const fs = require('fs'); const path = require('path'); const BASE_URL = 'https://192.168.3.89'; const CONFIG = { urls: { login: '/login', gnbList: '/sim_5gc/gnb/index' }, credentials: { email: '[email protected]', password: 'dotouch' }, sessionDir: path.join(__dirname, '.sessions'), getSessionFile() { return `5gc_gnb_bulk_\/\//, '').replace(/\./g, '_').json`; } }; class SessionManager { constructor() { if (!fs.existsSync(CONFIG.sessionDir)) fs.mkdirSync(CONFIG.sessionDir, { recursive: true }); this.sp = path.join(CONFIG.sessionDir, CONFIG.getSessionFile()); } async loadSession(browser) { if (!fs.existsSync(this.sp)) return null; try { const { storageState } = JSON.parse(fs.readFileSync(this.sp, 'utf8')); return await browser.newContext({ storageState, ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } }); } catch { return null; } } async saveSession(context) { fs.writeFileSync(this.sp, JSON.stringify({ storageState: await context.storageState() }, null, 2)); } } // 选择工程 async function selectProject(page, projectName) { await page.goto(`BASE_URL/sim_5gc/project/index`, { waitUntil: 'networkidle', timeout: 15000 }); try { await page.waitForSelector('.jsgrid-row, .jsgrid-alt-row', { timeout: 10000 }); } catch { console.log(' ⚠ 等待 jsgrid 行超时'); } await page.waitForTimeout(2000); for (let pageNum = 1; pageNum <= 20; pageNum++) { const result = await page.evaluate((target) => { const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row'); for (const row of rows) { const cells = row.querySelectorAll('td'); if (cells.length >= 3 && cells[2].textContent.trim() === target) { if (row.classList.contains('jsgrid-selected-row')) return 'already-selected'; const controlCell = row.querySelector('.jsgrid-cell.jsgrid-control-field'); if (controlCell) { const icon = controlCell.querySelector('.layui-icon'); if (icon) { icon.click(); return 'clicked'; } } row.click(); return 'clicked'; } } return 'not-found'; }, projectName); if (result === 'already-selected') { console.log(' ✓ 工程已在第一行(已选中状态),跳过'); await page.waitForTimeout(2000); return true; } if (result === 'clicked') { console.log(' ✓ 已点击工程行图标,等待切换...'); await page.waitForTimeout(3000); return true; } const nextBtn = page.locator('.jsgrid-pager a').filter({ hasText: 'Next' }).first(); if (!(await nextBtn.count())) { console.log(' ⚠ 已到最后一页'); break; } try { await nextBtn.click({ force: true }); await page.waitForTimeout(2000); } catch { break; } } console.log(` ❌ 未找到工程 "projectName"`); return false; } // 获取 GNB 列表所有 ID(layui-table) async function fetchAllGnbIds(page, nameFilter = null) { // 等待行出现 await page.waitForTimeout(3000); return await page.evaluate((filter) => { // layui-table: tbody tr 中每行 td 顺序对应列 const tbody = document.querySelector('.layui-table-body'); if (!tbody) return []; const rows = tbody.querySelectorAll('tr'); const ids = []; const seen = new Set(); for (const row of rows) { const cells = row.querySelectorAll('td'); if (cells.length >= 3) { // GNB 列表列顺序: ID(col1), 名称(col2), NGAP起始IP(col3), ... const id = cells[1].textContent.trim(); // 第2列是 ID const name = cells[2].textContent.trim(); // 第3列是名称 // filter 为空则全部通过,为空字符串则跳过表头 if (id && !seen.has(id) && (!filter || name === filter)) { seen.add(id); ids.push(id); } } } return ids; }, nameFilter); } // 编辑单个 GNB async function editSingleGnb(page, gnbId, edits) { const editUrl = `BASE_URL/sim_5gc/gnb/edit/gnbId`; await page.goto(editUrl, { waitUntil: 'networkidle' }); await page.waitForTimeout(2000); for (const [field, val] of Object.entries(edits)) { const inputSelector = `input[name="field"], textarea[name="field"]`; try { const exists = await page.locator(inputSelector).count(); if (exists === 0) { console.log(` ⚠ 字段 field 不存在,跳过`); continue; } await page.locator(inputSelector).fill(String(val)); console.log(` ✓ field → val`); } catch (e) { console.log(` ⚠ 字段 field 填写失败`); } } // 提交 await page.getByRole('button', { name: '确定' }).click(); await page.waitForTimeout(3000); const success = page.url().includes('gnb/index'); if (!success) console.log(' ⚠ 提交后未返回列表,可能失败'); return success; } async function main() { const args = process.argv.slice(2); if (args.length < 1) { console.log('用法: node gnb-edit-skill.js [--project 工程] [--headed] --set-<字段名> <值>'); console.log('示例: node gnb-edit-skill.js --project XW_S5GC_basic --set-replay_ip 200.20.20.250'); console.log(' node gnb-edit-skill.js --project XW_S5GC_basic --set-stac 0 --set-etac 0 --headed'); console.log(''); console.log('支持的字段: name, count, ngap_sip, ngap_port, user_sip_ip_v4, user_sip_ip_v6,'); console.log(' mcc, mnc, stac, etac, node_id, node_id_len, replay_ip, replay_port,'); console.log(' cell_count, position_x, position_y, gateway_ind'); process.exit(1); } let projectName = 'XW_S5GC_1'; let headless = true; let nameFilter = null; // --name 时只编辑该名称的 GNB const edits = {}; for (let i = 0; i < args.length; i++) { if (args[i] === '--project') { projectName = args[++i]; } else if (args[i] === '--name') { nameFilter = args[++i]; } else if (args[i] === '--headed') { headless = false; } else if (args[i].startsWith('--set-')) { const field = args[i].slice(6); edits[field] = args[++i]; } else if (!args[i].startsWith('-')) { // 兼容不带 --name 的位置参数 if (!nameFilter) nameFilter = args[i]; } } if (Object.keys(edits).length === 0) { console.log('请指定要修改的字段,例如: --set-replay_ip 200.20.20.250'); process.exit(1); } console.log(`▶ 批量编辑 GNB,工程: projectName' + nameFilter : ''`); for (const [k, v] of Object.entries(edits)) console.log(` 修改: k = v`); console.log(` 模式: '有头'`); const sessionMgr = new SessionManager(); const browser = await chromium.launch({ headless, args: ['--no-sandbox', '--ignore-certificate-errors'] }); const context = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } }); const page = await context.newPage(); // 登录 await page.goto(`BASE_URLCONFIG.urls.login`, { waitUntil: 'networkidle' }); await page.getByRole('textbox', { name: 'E-Mail地址' }).fill(CONFIG.credentials.email); await page.getByRole('textbox', { name: '密码' }).fill(CONFIG.credentials.password); await page.getByRole('checkbox', { name: '记住我' }).check(); await page.getByRole('button', { name: '登录' }).click(); await page.waitForLoadState('networkidle'); await sessionMgr.saveSession(context); console.log(' ✓ 登录成功'); // 选工程 const ok = await selectProject(page, projectName); if (!ok) throw new Error('工程选择失败'); console.log(' ✓ 工程已选'); // 进 GNB 列表 await page.goto(`BASE_URLCONFIG.urls.gnbList`, { waitUntil: 'networkidle' }); await page.waitForTimeout(4000); // 等待 layui-table 渲染 // 获取所有 GNB ID const gnbIds = await fetchAllGnbIds(page, nameFilter); if (gnbIds.length === 0) { console.log(' ⚠ 工程下没有 GNB'); await browser.close(); return; } console.log(` ✓ 找到 gnbIds.length 个 GNB`); // 逐个编辑 let successCount = 0; for (let i = 0; i < gnbIds.length; i++) { const gnbId = gnbIds[i]; console.log(`\n[i + 1/gnbIds.length] 编辑 GNB ID: gnbId`); const ok = await editSingleGnb(page, gnbId, edits); if (ok) successCount++; } console.log(`\n=== 完成 ===`); console.log(`成功编辑 successCount / gnbIds.length 个 GNB`); await browser.close(); } main().catch(e => { console.error('异常:', e.message); process.exit(1); }); FILE:scripts/nrf-add-skill.js /** * NRF 添加脚本 * 完整流程:登录 → 选工程 → 进NRF列表 → 点添加(弹窗iframe) → 填表单 → 提交 * 用法: node nrf-add-skill.js <名称> [--project <工程>] [--url <地址>] [--headed] \ * [--http2_sip <IP>] [--http2_port <端口>] [--MCC <值>] [--MNC <值>] * 示例: node nrf-add-skill.js NRF-TEST --project XW_S5GC_1 */ const { chromium } = require('playwright'); const path = require('path'); const fs = require('fs'); const BASE_URL = 'https://192.168.3.89'; const SESSION_DIR = path.join(__dirname, '.sessions'); function getSessionFile(baseUrl) { const host = baseUrl.replace(/https?:\/\//, '').replace(/\./g, '_'); return `5gc_session_host.json`; } async function login(page, baseUrl) { const sessionPath = path.join(SESSION_DIR, getSessionFile(baseUrl)); if (fs.existsSync(sessionPath)) { try { const storageState = JSON.parse(fs.readFileSync(sessionPath, 'utf8')); if (storageState.cookies) { await page.context().addCookies(storageState.cookies); await page.goto(baseUrl + '/sim_5gc/project/index', { waitUntil: 'networkidle', timeout: 8000 }).catch(() => {}); if (!page.url().includes('/login')) { console.log(' ✅ 使用缓存会话'); return true; } } } catch {} } await page.goto(baseUrl + '/login', { waitUntil: 'networkidle', timeout: 15000 }); await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]'); await page.getByRole('textbox', { name: '密码' }).fill('dotouch'); await page.getByRole('button', { name: '登录' }).click(); await page.waitForLoadState('networkidle'); const ctx = page.context(); const storageState = await ctx.storageState(); fs.writeFileSync(sessionPath, JSON.stringify({ cookies: storageState.cookies }, null, 2)); console.log(' ✅ 登录成功'); return true; } async function selectProject(page, projectName) { await page.goto(BASE_URL + '/sim_5gc/project/index', { waitUntil: 'networkidle', timeout: 15000 }); await page.waitForSelector('.jsgrid-row, .jsgrid-alt-row', { timeout: 5000 }).catch(() => {}); await page.waitForTimeout(300); for (let pageNum = 1; pageNum <= 200; pageNum++) { const clicked = await page.evaluate((targetName) => { const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row'); for (const row of rows) { const cells = row.querySelectorAll('td'); if (cells.length >= 3 && cells[2].textContent.trim() === targetName) { const icon = cells[1].querySelector('.iconfont'); if (icon) { icon.click(); return true; } } } return false; }, projectName); if (clicked) { await page.waitForTimeout(2000); return true; } const nextBtn = page.locator('.jsgrid-pager a:has-text("Next")'); if (!(await nextBtn.count())) break; try { await nextBtn.click(); } catch { break; } await page.waitForTimeout(1500); } console.log(` ❌ 未找到工程 "projectName"`); return false; } async function main() { const args = process.argv.slice(2); if (args.length < 1) { console.log('用法: node nrf-add-skill.js <名称> [--project <工程>] [--url <地址>] [--headed]'); console.log(' [--http2_sip <IP>] [--http2_port <端口>] [--MCC <值>] [--MNC <值>]'); console.log('示例: node nrf-add-skill.js NRF-TEST --project XW_S5GC_1'); process.exit(1); } const name = args[0]; let headless = true; let project = 'XW_S5GC_1'; let http2_sip = '192.168.20.100'; let http2_port = '80'; let mcc = '460'; let mnc = '01'; for (let i = 1; i < args.length; i++) { if (args[i] === '--headed') headless = false; else if (args[i] === '--project') project = args[++i]; else if (args[i] === '--url') BASE_URL = args[++i]; else if (args[i] === '--http2_sip') http2_sip = args[++i]; else if (args[i] === '--http2_port') http2_port = args[++i]; else if (args[i] === '--MCC') mcc = args[++i]; else if (args[i] === '--MNC') mnc = args[++i]; } console.log(`▶ 添加 NRF: name`); console.log(` http2_sip=http2_sip http2_port=http2_port MCC=mcc MNC=mnc`); console.log(` 工程: project`); const browser = await chromium.launch({ headless, args: ['--no-sandbox', '--ignore-certificate-errors'] }); const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } }); const page = await ctx.newPage(); await login(page, BASE_URL); const ok = await selectProject(page, project); if (!ok) throw new Error('工程选择失败'); console.log(' ✓ 工程已选'); // 进 NRF 列表(先点"核心网"菜单,再点"NRF") await page.evaluate(() => { const links = document.querySelectorAll('a[href*="/nrf/"]'); for (const l of links) { if (l.textContent.trim().includes('NRF')) { l.click(); return; } } }); await page.waitForTimeout(3000); console.log(' ✓ 进入NRF列表,URL:', page.url()); // 点添加按钮 await page.waitForSelector('button:has-text("添加")', { timeout: 10000 }).catch(() => {}); await page.locator('button:has-text("添加")').first().click(); await page.waitForTimeout(2000); console.log(' ✓ 点添加(弹窗)'); // 切换到弹窗 iframe await page.locator('iframe[name="layui-layer-iframe2"]').waitFor({ timeout: 5000 }); const frame = page.frame('layui-layer-iframe2'); if (!frame) throw new Error('未找到弹窗 iframe'); await frame.waitForLoadState('domcontentloaded'); console.log(' ✓ 切换到弹窗iframe'); // 名称 await frame.locator('input[name="name"]').fill(name); console.log(` ✓ name = name`); // 类型下拉 await frame.getByRole('textbox', { name: '请选择' }).first().click(); await frame.getByRole('definition').filter({ hasText: '仿真设备' }).click(); await page.waitForTimeout(500); console.log(' ✓ 类型 = 仿真设备'); // MCC await frame.getByRole('textbox', { name: '三位数字', exact: true }).fill(mcc); console.log(` ✓ MCC = mcc`); // MNC await frame.getByRole('textbox', { name: '二位或三位数字' }).fill(mnc); console.log(` ✓ MNC = mnc`); // HTTP2 SIP await frame.locator('input[name="http2_sip"]').fill(http2_sip); console.log(` ✓ http2_sip = http2_sip`); // HTTP2 PORT await frame.locator('input[name="http2_port"]').fill(http2_port); console.log(` ✓ http2_port = http2_port`); // 提交 await frame.locator('button:has-text("提交")').click(); await page.waitForTimeout(3000); console.log(' ✓ 已提交'); const url = page.url(); if (url.includes('/nrf/index')) { console.log(` ✅ 添加成功,URL: url`); } else { console.log(` ⚠️ 可能未保存,URL: url`); } await browser.close(); } main().catch(e => { console.error('❌', e.message); process.exit(1); }); FILE:scripts/nrf-edit-skill.js /** * NRF 编辑脚本 * 完整流程:登录 → 选工程 → 进NRF列表 → 点编辑(弹窗iframe) → 填表单 → 提交 → 验证 * 用法: * node nrf-edit-skill.js --project <工程> --set-<字段> <值> # 批量 * node nrf-edit-skill.js --name <名称> --project <工程> --set-<字段> <值> # 单条 * 示例: * node nrf-edit-skill.js --project XW_S5GC_1 --set-http2_sip 10.0.0.1 * node nrf-edit-skill.js --name NRF-TEST --project XW_S5GC_1 --set-http2_sip 10.0.0.1 */ const { chromium } = require('playwright'); const path = require('path'); const fs = require('fs'); const BASE_URL = 'https://192.168.3.89'; const SESSION_DIR = path.join(__dirname, '.sessions'); function getSessionFile() { const host = BASE_URL.replace(/https?:\/\//, '').replace(/\./g, '_'); return `5gc_session_host.json`; } function parseArgs() { const args = process.argv.slice(2); const fields = {}; let name = null, project = 'XW_S5GC_1', headed = false; for (let i = 0; i < args.length; i++) { if (args[i] === '--name' && i + 1 < args.length) name = args[++i]; else if ((args[i] === '--project' || args[i] === '-p') && i + 1 < args.length) project = args[++i]; else if (args[i] === '--headed') headed = true; else if (args[i].startsWith('--set-')) { const k = args[i].slice(6).replace(/-/g, '_'); fields[k] = args[i + 1] != null && !args[i + 1].startsWith('--') ? args[++i] : 'true'; } } return { fields, name, project, headed }; } async function login(page) { const sessionPath = path.join(SESSION_DIR, getSessionFile()); if (fs.existsSync(sessionPath)) { try { const storageState = JSON.parse(fs.readFileSync(sessionPath, 'utf8')); if (storageState.cookies) { await page.context().addCookies(storageState.cookies); await page.goto(BASE_URL + '/sim_5gc/project/index', { waitUntil: 'networkidle', timeout: 8000 }).catch(() => {}); if (!page.url().includes('/login')) { console.log(' ✅ 使用缓存会话'); return true; } } } catch {} } await page.goto(BASE_URL + '/login', { waitUntil: 'networkidle', timeout: 15000 }); await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]'); await page.getByRole('textbox', { name: '密码' }).fill('dotouch'); await page.getByRole('button', { name: '登录' }).click(); await page.waitForLoadState('networkidle'); const ctx = page.context(); const storageState = await ctx.storageState(); fs.writeFileSync(sessionPath, JSON.stringify({ cookies: storageState.cookies }, null, 2)); console.log(' ✅ 登录成功'); return true; } async function selectProject(page, projectName) { await page.goto(BASE_URL + '/sim_5gc/project/index', { waitUntil: 'networkidle', timeout: 15000 }); await page.waitForSelector('.jsgrid-row, .jsgrid-alt-row', { timeout: 5000 }).catch(() => {}); await page.waitForTimeout(300); for (let pageNum = 1; pageNum <= 200; pageNum++) { const clicked = await page.evaluate((targetName) => { const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row'); for (const row of rows) { const cells = row.querySelectorAll('td'); if (cells.length >= 3 && cells[2].textContent.trim() === targetName) { const icon = cells[1].querySelector('.iconfont'); if (icon) { icon.click(); return true; } } } return false; }, projectName); if (clicked) { await page.waitForTimeout(2000); return true; } const nextBtn = page.locator('.jsgrid-pager a:has-text("Next")'); if (!(await nextBtn.count())) break; try { await nextBtn.click(); } catch { break; } await page.waitForTimeout(1500); } console.log(` ❌ 未找到工程 "projectName"`); return false; } async function main() { const { fields, name: targetName, project, headed } = parseArgs(); if (Object.keys(fields).length === 0) { console.error('请至少指定一个 --set-<字段>'); process.exit(1); } console.log(`▶ NRF 编辑 工程: project 字段: JSON.stringify(fields)`); const browser = await chromium.launch({ headless: !headed, args: ['--no-sandbox', '--ignore-certificate-errors'] }); const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } }); const page = await ctx.newPage(); await login(page); const ok = await selectProject(page, project); if (!ok) throw new Error('工程选择失败'); console.log(` ✅ 工程已选`); // 进入 NRF 列表 await page.evaluate(() => { const links = document.querySelectorAll('a[href*="/nrf/"]'); for (const l of links) { if (l.textContent.trim().includes('NRF')) { l.click(); return; } } }); await page.waitForTimeout(2500); console.log(' ✓ 进入NRF列表,URL:', page.url()); // 收集 NRF 行 const rows = await page.locator('.layui-table tbody tr').all(); const targets = []; for (const row of rows) { const cells = await row.locator('td').all(); if (cells.length >= 10) { const id = parseInt((await cells[1].textContent()).trim(), 10); const name = (await cells[2].textContent()).trim(); if (!isNaN(id) && name && (!targetName || name === targetName)) { targets.push({ id, name }); } } } if (targets.length === 0) { console.log(' 未找到匹配的 NRF'); await browser.close(); return; } console.log(` 找到 targets.length 个 NRF`); for (const nrf of targets) { process.stdout.write(`▶ [nrf.id] nrf.name ... `); try { // 重新进入 NRF 列表 await page.evaluate(() => { const links = document.querySelectorAll('a[href*="/nrf/"]'); for (const l of links) { if (l.textContent.trim().includes('NRF')) { l.click(); return; } } }); await page.waitForTimeout(2000); // 找到该行的编辑按钮 const allRows = await page.locator('.layui-table tbody tr').all(); let opened = false; for (const row of allRows) { const cells = await row.locator('td').all(); if (cells.length >= 10 && parseInt((await cells[1].textContent()).trim(), 10) === nrf.id) { await cells[9].locator('a:has-text("编辑")').click(); opened = true; break; } } if (!opened) { console.log('❌ 打开编辑弹窗失败'); continue; } // 等待弹窗 await page.waitForSelector('iframe[name="layui-layer-iframe2"]', { timeout: 5000 }); await page.waitForTimeout(2000); const frame = page.frame('layui-layer-iframe2'); if (!frame) { console.log('❌ 未找到 iframe'); continue; } await frame.waitForLoadState('domcontentloaded'); await page.waitForTimeout(500); // 填写字段(使用 locator.fill) if (fields.http2_sip) await frame.locator('input[name="http2_sip"]').fill(fields.http2_sip); if (fields.http2_port) await frame.locator('input[name="http2_port"]').fill(fields.http2_port); if (fields.MCC) await frame.getByRole('textbox', { name: '三位数字', exact: true }).fill(fields.MCC); if (fields.MNC) await frame.getByRole('textbox', { name: '二位或三位数字' }).fill(fields.MNC); // 点提交 await frame.locator('button:has-text("提交")').click(); await page.waitForTimeout(2000); // 强制关闭 layer await page.evaluate(() => { const b = document.querySelector('.layui-layer-close'); if (b) b.click(); }); await page.waitForTimeout(1000); // 重新进入列表,验证 await page.evaluate(() => { const links = document.querySelectorAll('a[href*="/nrf/"]'); for (const l of links) { if (l.textContent.trim().includes('NRF')) { l.click(); return; } } }); await page.waitForTimeout(2000); const rows2 = await page.locator('.layui-table tbody tr').all(); let found = false; for (const row of rows2) { const cells = await row.locator('td').all(); if (cells.length >= 10 && parseInt((await cells[1].textContent()).trim(), 10) === nrf.id) { await cells[9].locator('a:has-text("编辑")').click(); await page.waitForTimeout(2000); await page.waitForSelector('iframe[name="layui-layer-iframe2"]', { timeout: 5000 }); await page.waitForTimeout(500); const frame2 = page.frame('layui-layer-iframe2'); let verified = true; let actualVal = 'N/A'; if (fields.http2_sip) { actualVal = await frame2.locator('input[name="http2_sip"]').inputValue(); verified = verified && actualVal === fields.http2_sip; } if (fields.http2_port) verified = verified && (await frame2.locator('input[name="http2_port"]').inputValue()) === fields.http2_port; if (fields.MCC) verified = verified && (await frame2.getByRole('textbox', { name: '三位数字', exact: true }).inputValue()) === fields.MCC; if (fields.MNC) verified = verified && (await frame2.getByRole('textbox', { name: '二位或三位数字' }).inputValue()) === fields.MNC; console.log(verified ? '✅' : '❌'); found = true; await page.evaluate(() => { const b = document.querySelector('.layui-layer-close'); if (b) b.click(); }); break; } } if (!found) console.log('❌'); } catch (e) { console.log(`❌ e.message`); } } await browser.close(); } main().catch(e => { console.error('❌', e.message); process.exit(1); }); FILE:scripts/parse_args.js /** * 统一 CLI 参数解析器 * * 所有 5GC 脚本均使用此解析器,保证参数格式一致。 * * 标准格式: * ADD: node <script> --name <名称> --project <工程> [--field <值> ...] * EDIT: node <script> --name <名称> --project <工程> --set-<field> <value> * node <script> --id <ID> --set-<field> <value> * node <script> --project <工程> --set-<field> <value> (批量) * * 用法: const args = parseArgs(); → { name, project, id, sets, headed, url } */ const URL_RE = /^--url$/i; const PROJECT_RE = /^--project$|^--project$/i; const NAME_RE = /^--name$/i; const ID_RE = /^--id$/i; const HEADED_RE = /^--headed$/i; const SET_RE = /^--set-/; function parseArgs(argv) { const args = argv || process.argv.slice(2); const result = { name: null, project: 'XW_S5GC_1', // 默认工程 id: null, sets: {}, headed: false, url: 'https://192.168.3.89', _positional: [] // 保留位置参数兼容 }; for (let i = 0; i < args.length; i++) { const a = args[i]; // 统一转小写匹配(处理 --Name, --NAME 等) const al = a.toLowerCase(); if (URL_RE.test(a)) { let u = args[++i]; if (u && !u.startsWith('http')) u = 'https://' + u; result.url = u || result.url; } else if (PROJECT_RE.test(a) || al === '-p') { result.project = args[++i] || result.project; } else if (NAME_RE.test(a) || al === '-n') { result.name = args[++i] || result.name; } else if (ID_RE.test(a)) { result.id = args[++i] || result.id; } else if (HEADED_RE.test(a) || al === '--head' || al === '-h' && args[i + 1] === undefined) { result.headed = true; } else if (SET_RE.test(a)) { const key = a.substring(5).replace(/-/g, '_'); // --set-msisdn → msisdn result.sets[key] = args[++i]; } else if (a.startsWith('--')) { // 其他 --field value 形式(非 --set-) const key = a.substring(2).replace(/-/g, '_'); result.sets[key] = args[++i]; } else if (!a.startsWith('-')) { // 纯位置参数(非选项):视为 name if (!result.name) result.name = a; else result._positional.push(a); } // 忽略单独出现的 -h(它是 --headed 的缩写,但后面没值时才有意义) } return result; } /** * 统一打印帮助信息 */ function printHelp(additionalFields = []) { console.log(` 用法: node <script> --name <名称> --project <工程> [--set-<字段> <值>] [--headed] 参数: --name <名称> 网元名称(ADD/EDIT 时使用) --project <工程> 目标工程(默认: XW_S5GC_1) --id <ID> 直接指定网元 ID(EDIT 时使用) --set-<字段> <值> 修改指定字段的值(EDIT 时使用) --url <地址> 5GC 仪表地址(默认: https://192.168.3.89) --headed 以有头模式运行(显示浏览器窗口) 示例: # 添加 node <script> --name MyAMF --project XW_S5GC_1 --mcc 460 --mnc 01 node <script> --name MyGNB --project XW_S5GC_1 --count 1 --mcc 460 --mnc 01 --stac 1 --etac 100 # 编辑(单个) node <script> --id 12345 --set-sbi_ip 10.0.0.1 node <script> --name MyAMF --project XW_S5GC_1 --set-sbi_ip 10.0.0.1 # 批量编辑 node <script> --project XW_S5GC_1 --set-replay_ip 10.0.0.1 可编辑字段:additionalFields.map(f => '\n ' + f).join('') `); } // 暴露 module.exports = { parseArgs, printHelp }; FILE:scripts/pcf-add-skill.js /** * PCF/PCRF 添加脚本 * 完整流程:登录 → 选工程 → 进PCF列表 → 点添加(弹窗iframe) → 填表单 → 提交 * 用法: node pcf-add-skill.js <名称> [--project <工程>] [--url <地址>] [--headed] \ * [--http2_sip <IP>] [--http2_port <端口>] [--MCC <值>] [--MNC <值>] * 示例: node pcf-add-skill.js PCF-TEST --project XW_S5GC_1 */ const { chromium } = require('playwright'); const path = require('path'); const fs = require('fs'); const BASE_URL = 'https://192.168.3.89'; const SESSION_DIR = path.join(__dirname, '.sessions'); function getSessionFile(baseUrl) { const host = baseUrl.replace(/https?:\/\//, '').replace(/\./g, '_'); return `5gc_session_host.json`; } async function login(page, baseUrl) { const sessionPath = path.join(SESSION_DIR, getSessionFile(baseUrl)); if (fs.existsSync(sessionPath)) { try { const storageState = JSON.parse(fs.readFileSync(sessionPath, 'utf8')); if (storageState.cookies) { await page.context().addCookies(storageState.cookies); await page.goto(baseUrl + '/sim_5gc/project/index', { waitUntil: 'networkidle', timeout: 8000 }).catch(() => {}); if (!page.url().includes('/login')) { console.log(' ✅ 使用缓存会话'); return true; } } } catch {} } await page.goto(baseUrl + '/login', { waitUntil: 'networkidle', timeout: 15000 }); await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]'); await page.getByRole('textbox', { name: '密码' }).fill('dotouch'); await page.getByRole('button', { name: '登录' }).click(); await page.waitForLoadState('networkidle'); const ctx = page.context(); const storageState = await ctx.storageState(); fs.writeFileSync(sessionPath, JSON.stringify({ cookies: storageState.cookies }, null, 2)); console.log(' ✅ 登录成功'); return true; } async function selectProject(page, projectName) { await page.goto(BASE_URL + '/sim_5gc/project/index', { waitUntil: 'networkidle', timeout: 15000 }); await page.waitForSelector('.jsgrid-row, .jsgrid-alt-row', { timeout: 5000 }).catch(() => {}); await page.waitForTimeout(300); for (let pageNum = 1; pageNum <= 200; pageNum++) { const clicked = await page.evaluate((targetName) => { const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row'); for (const row of rows) { const cells = row.querySelectorAll('td'); if (cells.length >= 3 && cells[2].textContent.trim() === targetName) { const icon = cells[1].querySelector('.iconfont'); if (icon) { icon.click(); return true; } } } return false; }, projectName); if (clicked) { await page.waitForTimeout(2000); return true; } const nextBtn = page.locator('.jsgrid-pager a:has-text("Next")'); if (!(await nextBtn.count())) break; try { await nextBtn.click(); } catch { break; } await page.waitForTimeout(1500); } console.log(` ❌ 未找到工程 "projectName"(精确匹配)`); return false; } async function main() { const args = process.argv.slice(2); if (args.length < 1) { console.log('用法: node pcf-add-skill.js <名称> [--project <工程>] [--url <地址>] [--headed]'); console.log(' [--http2_sip <IP>] [--http2_port <端口>] [--MCC <值>] [--MNC <值>]'); console.log('示例: node pcf-add-skill.js PCF-TEST --project XW_S5GC_1'); process.exit(1); } const name = args[0]; let headless = true; let project = 'XW_S5GC_1'; let http2_sip = '192.168.20.90'; let http2_port = '80'; let mcc = '460'; let mnc = '01'; for (let i = 1; i < args.length; i++) { if (args[i] === '--headed') headless = false; else if (args[i] === '--project') project = args[++i]; else if (args[i] === '--url') BASE_URL = args[++i]; else if (args[i] === '--http2_sip') http2_sip = args[++i]; else if (args[i] === '--http2_port') http2_port = args[++i]; else if (args[i] === '--MCC') mcc = args[++i]; else if (args[i] === '--MNC') mnc = args[++i]; } console.log(`▶ 添加 PCF: name`); console.log(` http2_sip=http2_sip http2_port=http2_port MCC=mcc MNC=mnc`); console.log(` 工程: project`); const browser = await chromium.launch({ headless, args: ['--no-sandbox', '--ignore-certificate-errors'] }); const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } }); const page = await ctx.newPage(); await login(page, BASE_URL); const ok = await selectProject(page, project); if (!ok) throw new Error('工程选择失败'); console.log(' ✓ 工程已选'); // 进 PCF/PCRF 列表(用 JS 点击 sidebar 链接,兼容折叠菜单) await page.evaluate(() => { const links = document.querySelectorAll('a[href*="/pcf/"]'); for (const l of links) { if (l.textContent.trim().includes('PCF')) { l.click(); return; } } }); await page.waitForTimeout(3000); console.log(' ✓ 进入PCF列表,URL:', page.url()); // 点添加按钮(弹窗) await page.waitForSelector('button:has-text("添加")', { timeout: 10000 }).catch(() => {}); await page.locator('button:has-text("添加")').first().click(); await page.waitForTimeout(2000); console.log(' ✓ 点添加(弹窗)'); // 切换到弹窗 iframe await page.locator('iframe[name="layui-layer-iframe2"]').waitFor({ timeout: 5000 }); const frame = page.frame('layui-layer-iframe2'); if (!frame) throw new Error('未找到弹窗 iframe'); await frame.waitForLoadState('domcontentloaded'); console.log(' ✓ 切换到弹窗iframe'); // 填名称 await frame.locator('input[name="name"]').fill(name); console.log(` ✓ name = name`); // 类型下拉:点击"请选择" await frame.getByRole('textbox', { name: '请选择' }).click(); await frame.getByRole('definition').filter({ hasText: '仿真设备' }).click(); await page.waitForTimeout(500); console.log(' ✓ 类型 = 仿真设备'); // 数量 await frame.locator('input[name="count"]').fill('1'); console.log(' ✓ count = 1'); // HTTP2 SIP await frame.locator('input[name="http2_sip"]').fill(http2_sip); console.log(` ✓ http2_sip = http2_sip`); // HTTP2 PORT await frame.locator('input[name="http2_port"]').fill(http2_port); console.log(` ✓ http2_port = http2_port`); // MCC - label 为"三位数字" await frame.getByRole('textbox', { name: '三位数字', exact: true }).fill(mcc); console.log(` ✓ MCC = mcc`); // MNC - label 为"二位或三位数字" await frame.getByRole('textbox', { name: '二位或三位数字' }).fill(mnc); console.log(` ✓ MNC = mnc`); // 提交 await frame.locator('button:has-text("提交")').click(); await page.waitForTimeout(3000); console.log(' ✓ 已提交'); const url = page.url(); if (url.includes('/pcf/index')) { console.log(` ✅ 添加成功,URL: url`); } else { console.log(` ⚠️ 可能未保存,URL: url`); } await browser.close(); } main().catch(e => { console.error('❌', e.message); process.exit(1); }); FILE:scripts/pcf-edit-skill.js /** * pcf-edit-skill.js - PCF/PCRF 编辑 */ const { chromium } = require('playwright'); const globalBaseUrl = 'https://192.168.3.89'; function parseArgs() { const args = process.argv.slice(2); const fields = {}; let name = null, project = 'XW_S5GC_1', headed = false; for (let i = 0; i < args.length; i++) { if (args[i] === '--name' && i + 1 < args.length) name = args[++i]; else if ((args[i] === '--project' || args[i] === '-p') && i + 1 < args.length) project = args[++i]; else if (args[i] === '--headed') headed = true; else if (args[i].startsWith('--set-')) { const k = args[i].slice(6).replace(/-/g, '_'); fields[k] = args[i + 1] != null && !args[i + 1].startsWith('--') ? args[++i] : 'true'; } } return { fields, name, project, headed }; } async function login(page) { await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 }); await page.waitForTimeout(1500); await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]'); await page.getByRole('textbox', { name: '密码' }).fill('dotouch'); await page.getByRole('button', { name: '登录' }).click(); await page.waitForTimeout(2500); console.log('✅ 登录成功'); } async function selectProject(page, projectName) { await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true }); await page.waitForTimeout(2000); let found = false; for (const row of await page.locator('.jsgrid-row, .jsgrid-alt-row').all()) { const cells = await row.locator('td').all(); if (cells.length >= 3) { const text = (await cells[2].textContent()).trim(); if (text === projectName) { await cells[1].locator('.iconfont').click(); found = true; break; } } } if (!found) { console.error(`❌ 未找到工程: projectName`); process.exit(1); } await page.waitForTimeout(3000); console.log(`✅ 工程 "projectName" 已选`); } async function main() { const { fields, name: targetName, project, headed } = parseArgs(); if (Object.keys(fields).length === 0) { console.error('请至少指定一个 --set-<字段>'); process.exit(1); } const browser = await chromium.launch({ headless: !headed, args: ['--no-sandbox', '--ignore-certificate-errors'] }); const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } }); const page = await ctx.newPage(); await login(page); await selectProject(page, project); await page.goto(`globalBaseUrl/sim_5gc/pcf/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true }); await page.waitForTimeout(2000); const rows = await page.locator('.layui-table tbody tr').all(); const targets = []; for (const row of rows) { const cells = await row.locator('td').all(); if (cells.length >= 10) { const id = parseInt((await cells[1].textContent()).trim(), 10); const name = (await cells[2].textContent()).trim(); if (!isNaN(id) && name && (!targetName || name === targetName)) { targets.push({ id, name }); } } } if (targets.length === 0) { console.log('未找到匹配的 PCF'); await browser.close(); return; } console.log(`找到 targets.length 个 PCF`); for (const pcf of targets) { process.stdout.write(`▶ [pcf.id] pcf.name ... `); try { await page.goto(`globalBaseUrl/sim_5gc/pcf/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true }); await page.waitForTimeout(2000); const allRows = await page.locator('.layui-table tbody tr').all(); let opened = false; for (const row of allRows) { const cells = await row.locator('td').all(); if (cells.length >= 10 && parseInt((await cells[1].textContent()).trim(), 10) === pcf.id) { await cells[9].locator('a:has-text("编辑")').click(); opened = true; break; } } if (!opened) { console.log('❌ 打开弹窗失败'); continue; } await page.waitForSelector('iframe[name="layui-layer-iframe2"]', { timeout: 5000 }); await page.waitForTimeout(2000); const frame = page.locator('iframe[name="layui-layer-iframe2"]').contentFrame(); await page.waitForTimeout(500); if (fields.http2_sip) await frame.locator('input[name="http2_sip"]').fill(fields.http2_sip); if (fields.http2_port) await frame.locator('input[name="http2_port"]').fill(fields.http2_port); if (fields.MCC) await frame.locator('input[name="MCC"]').fill(fields.MCC); if (fields.MNC) await frame.locator('input[name="MNC"]').fill(fields.MNC); if (fields.count) await frame.locator('input[name="count"]').fill(String(fields.count)); await frame.locator('button:has-text("提交")').click(); await page.waitForTimeout(2000); await page.evaluate(() => { const b = document.querySelector('.layui-layer-close'); if (b) b.click(); }); await page.waitForTimeout(1000); await page.goto(`globalBaseUrl/sim_5gc/pcf/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true }); await page.waitForTimeout(2000); const rows2 = await page.locator('.layui-table tbody tr').all(); let found = false; for (const row of rows2) { const cells = await row.locator('td').all(); if (cells.length >= 10 && parseInt((await cells[1].textContent()).trim(), 10) === pcf.id) { await cells[9].locator('a:has-text("编辑")').click(); await page.waitForTimeout(2000); await page.waitForSelector('iframe[name="layui-layer-iframe2"]', { timeout: 5000 }); await page.waitForTimeout(500); const frame2 = page.locator('iframe[name="layui-layer-iframe2"]').contentFrame(); let verified = true; let actualVal = 'N/A'; if (fields.http2_sip) { actualVal = await frame2.locator('input[name="http2_sip"]').inputValue(); verified = verified && actualVal === fields.http2_sip; } if (fields.http2_port) verified = verified && (await frame2.locator('input[name="http2_port"]').inputValue()) === fields.http2_port; if (fields.MCC) verified = verified && (await frame2.locator('input[name="MCC"]').inputValue()) === fields.MCC; if (fields.MNC) verified = verified && (await frame2.locator('input[name="MNC"]').inputValue()) === fields.MNC; console.log(verified ? '✅' : '❌'); found = true; await page.evaluate(() => { const b = document.querySelector('.layui-layer-close'); if (b) b.click(); }); break; } } if (!found) console.log('❌'); } catch (e) { console.log(`❌ e.message`); } } await browser.close(); } main().catch(e => { console.error(e); process.exit(1); }); FILE:scripts/smf-pgwc-add-skill.js #!/usr/bin/env node /** * SMF/PGW-C 添加脚本 * 用法: node smf-pgwc-add-skill.js <名称> [--project <工程>] [--url <地址>] [--headed] \ * [--pfcp_sip <IP>] [--http2_sip <IP>] [--mcc <值>] [--mnc <值>] \ * [--pdu_capacity <数量>] [--ue_min <IP>] [--ue_max <IP>] \ * [--interest_tac <TAC列表>] * 示例: node smf-pgwc-add-skill.js SMF-TEST --project XW_S5GC_1 * node smf-pgwc-add-skill.js SMF-PROD --pfcp_sip 10.10.10.50 --http2_sip 10.10.10.51 --mcc 460 --mnc 01 */ const { chromium } = require('playwright'); const fs = require('fs'); const path = require('path'); let BASE_URL = 'https://192.168.3.89'; const CONFIG = { urls: { login: '/login', smfList: '/sim_5gc/smf/index' }, credentials: { email: '[email protected]', password: 'dotouch' }, sessionDir: path.join(__dirname, '.sessions'), getSessionFile() { return `5gc_smf_add_\/\//, '').replace(/\./g, '_').json`; } }; class SessionManager { constructor() { if (!fs.existsSync(CONFIG.sessionDir)) fs.mkdirSync(CONFIG.sessionDir, { recursive: true }); this.sp = path.join(CONFIG.sessionDir, CONFIG.getSessionFile()); } async loadSession(browser) { if (!fs.existsSync(this.sp)) return null; try { const { storageState } = JSON.parse(fs.readFileSync(this.sp, 'utf8')); return await browser.newContext({ storageState, ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } }); } catch { return null; } } async saveSession(context) { fs.writeFileSync(this.sp, JSON.stringify({ storageState: await context.storageState() }, null, 2)); } } // 选择工程(点击目标工程行内的 .layui-icon 图标) async function selectProject(page, projectName) { await page.goto(`BASE_URL/sim_5gc/project/index`, { waitUntil: 'networkidle', timeout: 15000 }); try { await page.waitForSelector('.jsgrid-row, .jsgrid-alt-row', { timeout: 10000 }); } catch { console.log(' ⚠ 等待 jsgrid 行超时'); } await page.waitForTimeout(2000); for (let pageNum = 1; pageNum <= 20; pageNum++) { const result = await page.evaluate((target) => { const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row'); for (const row of rows) { const cells = row.querySelectorAll('td'); if (cells.length >= 3 && cells[2].textContent.trim() === target) { if (row.classList.contains('jsgrid-selected-row')) return 'already-selected'; const controlCell = row.querySelector('.jsgrid-cell.jsgrid-control-field'); if (controlCell) { const icon = controlCell.querySelector('.layui-icon'); if (icon) { icon.click(); return 'clicked'; } } row.click(); return 'clicked'; } } return 'not-found'; }, projectName); if (result === 'already-selected') { console.log(' ✓ 工程已在第一行(已选中状态),跳过'); await page.waitForTimeout(2000); return true; } if (result === 'clicked') { console.log(' ✓ 已点击工程行图标,等待切换...'); await page.waitForTimeout(3000); return true; } const nextBtn = page.locator('.jsgrid-pager a').filter({ hasText: 'Next' }).first(); if (!(await nextBtn.count())) { console.log(' ⚠ 已到最后一页'); break; } try { await page.evaluate(() => { var links = document.querySelectorAll('.jsgrid-pager a'); for (var i = 0; i < links.length; i++) { if (links[i].innerText.trim() === 'Next') { links[i].click(); break; } } }); await page.waitForTimeout(2000); } catch { break; } } console.log(` ❌ 未找到工程 "projectName"`); return false; } async function main() { const args = process.argv.slice(2); if (args.length < 1) { console.log('用法: node smf-pgwc-add-skill.js <名称> [--project <工程>] [--url <地址>] [--headed]'); console.log(' [--pfcp_sip <IP>] [--http2_sip <IP>] [--mcc <值>] [--mnc <值>]'); console.log(' [--pdu_capacity <数量>] [--ue_min <IP>] [--ue_max <IP>] [--interest_tac <TAC列表>]'); console.log('示例: node smf-pgwc-add-skill.js SMF-TEST --project XW_S5GC_1'); console.log(' node smf-pgwc-add-skill.js SMF-PROD --pfcp_sip 10.10.10.50 --mcc 460 --mnc 01'); process.exit(1); } const smfName = args[0]; let headless = true; let url = BASE_URL; let project = 'XW_S5GC_1'; // 可选字段 let pfcp_sip = '200.20.20.25'; let http2_sip = '200.20.20.25'; let mcc = '460'; let mnc = '01'; let pdu_capacity = '200000'; let ue_min = '30.30.30.20'; let ue_max = '30.31.30.20'; let interest_tac = '101\n102'; for (let i = 1; i < args.length; i++) { if (args[i] === '--headed') { headless = false; } else if (args[i] === '--url') { url = args[++i].startsWith('http') ? args[i] : `https://args[i]`; } else if (args[i] === '--project') { project = args[++i]; } else if (args[i] === '--pfcp_sip') { pfcp_sip = args[++i]; } else if (args[i] === '--http2_sip') { http2_sip = args[++i]; } else if (args[i] === '--mcc') { mcc = args[++i]; } else if (args[i] === '--mnc') { mnc = args[++i]; } else if (args[i] === '--pdu_capacity') { pdu_capacity = args[++i]; } else if (args[i] === '--ue_min') { ue_min = args[++i]; } else if (args[i] === '--ue_max') { ue_max = args[++i]; } else if (args[i] === '--interest_tac') { interest_tac = args[++i].replace(/,/g, '\n'); } } BASE_URL = url; console.log(`▶ 添加 SMF: smfName`); console.log(` 模式: '有头'`); console.log(` 工程: project`); console.log(` pfcp_sip=pfcp_sip http2_sip=http2_sip mcc=mcc mnc=mnc pdu_capacity=pdu_capacity`); const sessionMgr = new SessionManager(); const browser = await chromium.launch({ headless, args: ['--no-sandbox', '--ignore-certificate-errors'] }); const context = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } }); const page = await context.newPage(); // 登录 await page.goto(`BASE_URLCONFIG.urls.login`, { waitUntil: 'networkidle' }); await page.getByRole('textbox', { name: 'E-Mail地址' }).fill(CONFIG.credentials.email); await page.getByRole('textbox', { name: '密码' }).fill(CONFIG.credentials.password); await page.getByRole('checkbox', { name: '记住我' }).check(); await page.getByRole('button', { name: '登录' }).click(); await page.waitForLoadState('networkidle'); await sessionMgr.saveSession(context); console.log(' ✓ 登录成功'); // 选工程 const ok = await selectProject(page, project); if (!ok) throw new Error('工程选择失败'); console.log(' ✓ 工程已选'); // 进 SMF 列表 await page.goto(`BASE_URLCONFIG.urls.smfList`, { waitUntil: 'networkidle' }); await page.waitForTimeout(2000); // 点添加按钮 await page.locator('button:has-text("添加"), a:has-text("添加")').first().click(); await page.waitForTimeout(2000); console.log(' ✓ 进入添加页面'); // ===== 填写表单 ===== // 1. 名称 await page.locator('input[name="name"]').fill(smfName); console.log(` ✓ name = smfName`); // 2. N4/Sxb起始IP(必填) await page.locator('input[name="pfcp_sip"]').fill(pfcp_sip); console.log(` ✓ pfcp_sip = pfcp_sip`); // 3. SBI起始地址 await page.locator('input[name="http2_sip"]').fill(http2_sip); console.log(` ✓ http2_sip = http2_sip`); // 4. 选择类型:仿真设备 await page.getByRole('textbox', { name: '请选择' }).first().click(); await page.waitForTimeout(300); await page.getByRole('definition').filter({ hasText: '仿真设备' }).click(); await page.waitForTimeout(300); console.log(' ✓ 类型 = 仿真设备'); // 5. 上行隧道信息分配:UPF await page.getByRole('textbox', { name: '请选择' }).nth(1).click(); await page.waitForTimeout(300); await page.getByRole('definition').filter({ hasText: 'UPF' }).click(); await page.waitForTimeout(300); console.log(' ✓ 上行隧道 = UPF'); // 6. UE IP分配:SMF await page.getByRole('textbox', { name: '请选择' }).nth(2).click(); await page.waitForTimeout(300); await page.getByRole('definition').filter({ hasText: 'SMF' }).click(); await page.waitForTimeout(300); console.log(' ✓ UE IP分配 = SMF'); // 7. UE IPv4池 await page.locator('input[name="ue_min"]').fill(ue_min); await page.locator('input[name="ue_max"]').fill(ue_max); console.log(` ✓ ue_min = ue_min, ue_max = ue_max`); // 8. UE IPv6池 await page.locator('input[name="ue_sip6"]').fill('1:2:3:1::1'); await page.locator('input[name="ue_eip6"]').fill('1:2:3:ffff::1'); console.log(' ✓ ue_sip6 = 1:2:3:1::1, ue_eip6 = 1:2:3:ffff::1'); // 9. MCC/MNC await page.getByRole('textbox', { name: '三位数字', exact: true }).fill(mcc); await page.getByRole('textbox', { name: '二位或三位数字' }).fill(mnc); console.log(` ✓ MCC = mcc, MNC = mnc`); // 10. PDU容量 await page.locator('input[name="pdu_capacity"]').fill(pdu_capacity); console.log(` ✓ pdu_capacity = pdu_capacity`); // 11. TAC列表 await page.locator('textarea[name="interest_tac"]').fill(interest_tac); console.log(` ✓ interest_tac = interest_tac.replace(/\n/g, ',')`); // 12. TAC/NSSAI 配置区域(可选,暂跳过避免弹窗干扰提交) // 如需配置,请在添加成功后手动编辑 console.log(' ℹ TAC/NSSAI配置暂跳过(添加后可手动编辑)'); console.log(' ✓ 表单填写完成'); // 关闭遮罩层 await page.evaluate(() => { document.querySelectorAll('.layui-layer-shade').forEach(el => el.remove()); document.querySelectorAll('.layui-layer').forEach(el => el.style.display = 'none'); }); await page.waitForTimeout(300); // 提交 await page.getByRole('button', { name: '提交' }).click(); await page.waitForTimeout(3000); console.log(' 最终 URL:', page.url()); const success = page.url().includes('smf/index'); console.log(success ? ' ✅ 添加成功' : ' ⚠ 可能未保存,请检查页面(可加 --headed 查看)'); await browser.close(); } main().catch(e => { console.error('异常:', e.message); process.exit(1); }); FILE:scripts/smf-pgwc-edit-skill.js #!/usr/bin/env node /** * SMF/PGW-C 编辑脚本(单条 + 批量二合一) * 用法: node smf-pgwc-edit-skill.js [--project 工程] [--headed] --set-<字段名> <值> * 示例: node smf-pgwc-edit-skill.js --project XW_S5GC_basic --set-http2_sip 200.20.20.99 * node smf-pgwc-edit-skill.js --project XW_S5GC_basic --set-http2_sip 200.20.20.99 --set-pfcp_sip 200.20.20.88 --headed */ const { chromium } = require('playwright'); const fs = require('fs'); const path = require('path'); let BASE_URL = 'https://192.168.3.89'; const CONFIG = { urls: { login: '/login', smfList: '/sim_5gc/smf/index' }, credentials: { email: '[email protected]', password: 'dotouch' }, sessionDir: path.join(__dirname, '.sessions'), getSessionFile() { return `5gc_smf_bulk_\/\//, '').replace(/\./g, '_').json`; } }; class SessionManager { constructor() { if (!fs.existsSync(CONFIG.sessionDir)) fs.mkdirSync(CONFIG.sessionDir, { recursive: true }); this.sp = path.join(CONFIG.sessionDir, CONFIG.getSessionFile()); } async loadSession(browser) { if (!fs.existsSync(this.sp)) return null; try { const { storageState } = JSON.parse(fs.readFileSync(this.sp, 'utf8')); return await browser.newContext({ storageState, ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } }); } catch { return null; } } async saveSession(context) { fs.writeFileSync(this.sp, JSON.stringify({ storageState: await context.storageState() }, null, 2)); } } // 选择工程 async function selectProject(page, projectName) { await page.goto(`BASE_URL/sim_5gc/project/index`, { waitUntil: 'networkidle', timeout: 15000 }); try { await page.waitForSelector('.jsgrid-row, .jsgrid-alt-row', { timeout: 10000 }); } catch { console.log(' ⚠ 等待 jsgrid 行超时'); } await page.waitForTimeout(2000); for (let pageNum = 1; pageNum <= 20; pageNum++) { const result = await page.evaluate((target) => { const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row'); // 遍历所有行的工程名,找到目标工程 for (const row of rows) { const cells = row.querySelectorAll('td'); if (cells.length >= 3 && cells[2].textContent.trim() === target) { // 只有同时具备 'jsgrid-selected-row' class 才认为已选中 if (row.classList.contains('jsgrid-selected-row')) { return 'already-selected'; } // 未选中,则尝试点击该行的控制按钮(图标)进行切换 const controlCell = row.querySelector('.jsgrid-cell.jsgrid-control-field'); if (controlCell) { const icon = controlCell.querySelector('.layui-icon'); if (icon) { icon.click(); return 'clicked'; } } // 若没有控制按钮,直接点击行 row.click(); return 'clicked'; } } return 'not-found'; }, projectName); if (result === 'already-selected') { console.log(' ✓ 工程已在第一行(已选中状态),跳过'); await page.waitForTimeout(2000); return true; } if (result === 'clicked') { console.log(' ✓ 已点击工程行图标,等待切换...'); await page.waitForTimeout(3000); return true; } const nextBtn = page.locator('.jsgrid-pager a').filter({ hasText: 'Next' }).first(); if (!(await nextBtn.count())) { console.log(' ⚠ 已到最后一页'); break; } try { await nextBtn.click({ force: true }); await page.waitForTimeout(2000); } catch { break; } } console.log(` ❌ 未找到工程 "projectName"`); return false; } // 获取 SMF 列表所有 ID(去重) async function fetchAllSmfIds(page, nameFilter = null) { return await page.evaluate((filter) => { const rows = document.querySelectorAll('.layui-table tbody tr'); const ids = []; const seen = new Set(); for (const row of rows) { const cells = row.querySelectorAll('td'); if (cells.length >= 3) { const id = cells[1].textContent.trim(); const name = cells[2].textContent.trim(); if (id && !seen.has(id) && (!filter || name === filter)) { seen.add(id); ids.push(id); } } } return ids; }, nameFilter); } // 编辑单个 SMF async function editSingleSmf(page, smfId, edits) { const editUrl = `BASE_URL/sim_5gc/smf/edit/smfId`; await page.goto(editUrl, { waitUntil: 'networkidle' }); await page.waitForTimeout(2000); // 只修改用户指定的字段 for (const [field, val] of Object.entries(edits)) { const selector = `input[name="field"]`; try { await page.waitForSelector(selector, { state: 'visible', timeout: 5000 }); await page.click(selector, { force: true }); await page.fill(selector, val); console.log(` ✓ field → val`); } catch { console.log(` ⚠ 字段 field 未找到或无法填写`); } } console.log(' ✓ 字段修改完成'); // 提交 await page.getByRole('button', { name: '提交' }).click(); await page.waitForTimeout(3000); const success = page.url().includes('smf/index'); if (!success) console.log(' ⚠ 提交后未返回列表,可能失败'); return success; } async function main() { const args = process.argv.slice(2); if (args.length < 1) { console.log('用法: node smf-pgwc-edit-skill.js [--project 工程] [--headed] --set-<字段名> <值>'); console.log('示例: node smf-pgwc-edit-skill.js --project XW_S5GC_basic --set-http2_sip 200.20.20.99'); console.log(' node smf-pgwc-edit-skill.js --project XW_S5GC_basic --set-http2_sip 200.20.20.99 --set-pfcp_sip 200.20.20.88 --headed'); console.log(''); console.log('支持的字段: http2_sip, http2_port, pfcp_sip, pfcp_port, ue_min, ue_max, pdu_capacity, mcc, mnc'); process.exit(1); } let projectName = 'XW_S5GC_1'; let nameFilter = null; let headless = true; const edits = {}; for (let i = 0; i < args.length; i++) { if (args[i] === '--project') { projectName = args[++i]; } else if (args[i] === '--name') { nameFilter = args[++i]; } else if (args[i] === '--headed') { headless = false; } else if (args[i].startsWith('--set-')) { const field = args[i].slice(6); edits[field] = args[++i]; } else if (!args[i].startsWith('-')) { if (!nameFilter) nameFilter = args[i]; } } if (Object.keys(edits).length === 0) { console.log('用法: node smf-pgwc-edit-skill.js [--project 工程] [--name <名称>] [--headed] --set-<字段名> <值>'); console.log('示例: node smf-pgwc-edit-skill.js --project XW_S5GC_1 --name SMF001 --set-pfcp_ip 10.0.0.5'); process.exit(1); } console.log(`▶ 批量编辑 SMF,工程: projectName' + nameFilter : ''`); console.log(` 修改字段:`, edits); console.log(` 模式: '有头'`); const sessionMgr = new SessionManager(); const browser = await chromium.launch({ headless, args: ['--no-sandbox', '--ignore-certificate-errors'] }); const context = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } }); const page = await context.newPage(); // 登录 await page.goto(`BASE_URLCONFIG.urls.login`, { waitUntil: 'networkidle' }); await page.getByRole('textbox', { name: 'E-Mail地址' }).fill(CONFIG.credentials.email); await page.getByRole('textbox', { name: '密码' }).fill(CONFIG.credentials.password); await page.getByRole('checkbox', { name: '记住我' }).check(); await page.getByRole('button', { name: '登录' }).click(); await page.waitForLoadState('networkidle'); await sessionMgr.saveSession(context); console.log(' ✓ 登录成功'); // 选工程 const ok = await selectProject(page, projectName); if (!ok) throw new Error('工程选择失败'); console.log(' ✓ 工程已选'); // 进 SMF 列表 await page.goto(`BASE_URLCONFIG.urls.smfList`, { waitUntil: 'networkidle' }); await page.waitForTimeout(2000); // 获取所有 SMF ID const smfIds = await fetchAllSmfIds(page, nameFilter); if (smfIds.length === 0) { console.log(' ⚠ 工程下没有 SMF'); await browser.close(); return; } console.log(` found smfIds.length SMF(s) in project "projectName"`); // 逐个编辑 let successCount = 0; for (let i = 0; i < smfIds.length; i++) { const smfId = smfIds[i]; console.log(`\n[i + 1/smfIds.length] 编辑 SMF ID: smfId`); const ok = await editSingleSmf(page, smfId, edits); if (ok) successCount++; } console.log(`\n=== 完成 ===`); console.log(`成功编辑 successCount / smfIds.length 个 SMF`); await browser.close(); } main().catch(e => { console.error('异常:', e.message); process.exit(1); }); FILE:scripts/ue-add-skill.js /** * UE 添加脚本 - ue-add-skill.js * 用法: node ue-add-skill.js --name <名称> [--imsi <imsi>] [--msisdn <msisdn>] [--mcc <mcc>] [--mnc <mnc>] [--project <工程>] [--headed] * * 工程默认: XW_S5GC_1 * UE 表单: /sim_5gc/ue/edit(不是工单弹窗) */ const { chromium } = require('playwright'); const BASE_URL = 'https://192.168.3.89'; const LOGIN_EMAIL = '[email protected]'; const LOGIN_PWD = 'dotouch'; const yargs = require('yargs'); const args = yargs(process.argv.slice(2)) .option('name', { alias: 'n', type: 'string', demandOption: true, describe: 'UE名称(只支持字母/数字/下划线)' }) .option('url', { alias: 'u', type: 'string', default: BASE_URL, describe: '5GC仪表地址' }) .option('imsi', { type: 'string', default: '460001234567890', describe: '起始IMSI(15位)' }) .option('msisdn', { type: 'string', default: '8611111111111', describe: 'MSISDN(13-15位)' }) .option('mcc', { type: 'string', default: '460', describe: 'MCC' }) .option('mnc', { type: 'string', default: '01', describe: 'MNC' }) .option('project', { alias: 'p', type: 'string', default: 'XW_S5GC_1', describe: '工程名称' }) .option('count', { alias: 'c', type: 'string', default: '1', describe: '数量' }) .option('key', { type: 'string', default: '11111111111111111111111111111111', describe: 'KI密钥(32位hex)' }) .option('opc', { type: 'string', default: '11111111111111111111111111111111', describe: 'OPc密钥(32位hex)' }) .option('imeisv', { type: 'string', default: '8611111111111111', describe: 'IMEISV' }) .option('sst', { type: 'string', default: '1', describe: 'NSSAI SST' }) .option('sd', { type: 'string', default: '111111', describe: 'NSSAI SD' }) .option('headed', { type: 'boolean', default: false, describe: '显示浏览器' }) .parse(); const HEADLESS = !args.headed; const BASE = args.url; console.log('▶ 添加 UE: ' + args.name); console.log(' URL: ' + BASE); console.log(' 工程: ' + args.project); (async () => { const browser = await chromium.launch({ headless: HEADLESS, args: ['--no-sandbox', '--disable-dev-shm-usage', '--ignore-certificate-errors'] }); const context = await browser.newContext({ ignoreHTTPSErrors: true, }); const page = await context.newPage(); // 登录 await page.goto(BASE + '/login', { waitUntil: 'networkidle', timeout: 15000 }); await page.getByRole('textbox', { name: 'E-Mail地址' }).fill(LOGIN_EMAIL); await page.getByRole('textbox', { name: '密码' }).fill(LOGIN_PWD); await page.getByRole('checkbox', { name: '记住我' }).check(); await page.getByRole('button', { name: '登录' }).click(); await page.waitForLoadState('networkidle'); console.log(' ✓ 登录成功'); // 选择工程 const ok = await selectProject(page, args.project); if (!ok) throw new Error('工程选择失败'); console.log(' ✓ 工程已选: ' + args.project); // 直接打开 UE 添加页面(/sim_5gc/ue/edit) await page.goto(BASE + '/sim_5gc/ue/edit', { waitUntil: 'networkidle', timeout: 15000 }); await page.waitForTimeout(3000); console.log(' ✓ UE 编辑页已打开'); // ===== 填写表单 ===== // 名称 await page.locator('input[name="name"]').fill(args.name); await page.waitForTimeout(100); // 数量(必填) await page.locator('input[name="count"]').fill(args.count || '1'); // MCC await page.locator('input[name="mcc"]').fill(args.mcc); // MNC await page.locator('input[name="mnc"]').fill(args.mnc); // 勾选所有加密算法(NEA0, NEA1, NEA2, NEA3)和完整性算法(NIA0, NIA1, NIA2, NIA3) // layui 隐藏 input → 用 page.evaluate() 直接设 checked + 触发 change 事件 await page.evaluate(() => { const eas = document.querySelectorAll('input[name="ea[]"]'); for (const cb of eas) { if (!cb.checked) { cb.checked = true; cb.dispatchEvent(new Event('change', { bubbles: true })); } } const ias = document.querySelectorAll('input[name="ia[]"]'); for (const cb of ias) { if (!cb.checked) { cb.checked = true; cb.dispatchEvent(new Event('change', { bubbles: true })); } } }); await page.waitForTimeout(200); console.log(' → 已勾选全部加密/完整性算法'); // 起始 IMSI(必填) await page.locator('input[name="s_imsi"]').fill(args.imsi); // KI(key,必填) await page.locator('input[name="key"]').fill(args.key); // OPc 类型选择(layui select: 点击 .layui-form-select,再选 dd) // 找到 op_opc_tp 对应的 layui-select(通过兄弟元素或位置) const opcSelectWrapper = page.locator('.layui-form-select').filter({ has: page.locator('select[name="op_opc_tp"]') }); if (await opcSelectWrapper.count() > 0) { await opcSelectWrapper.click(); await page.waitForTimeout(500); await page.locator('dd[lay-value="opc"]').first().click(); await page.waitForTimeout(200); } // OPc 值 await page.locator('input[name="op_opc"]').fill(args.opc); // IMEISV(必填) await page.locator('input[name="imeisv"]').fill(args.imeisv || '8611111111111111'); // MSISDN(必填) await page.locator('input[name="msisdn"]').fill(args.msisdn); // ===== NSSAI 配置:点击"添加NSSAI"按钮添加一行 ===== const nssaiAddBtn = page.locator('button.nssaiAdd'); if (await nssaiAddBtn.count() > 0) { await nssaiAddBtn.click(); await page.waitForTimeout(500); console.log(' → NSSAI 行已添加'); } // 填写 NSSAI(SST 和 SD) const nssaiSst = page.locator('input[name="nssai[snssai_sst][]"]'); if (await nssaiSst.count() > 0) { await nssaiSst.first().fill(args.sst); } const nssaiSd = page.locator('input[name="nssai[snssai_sd][]"]'); if (await nssaiSd.count() > 0) { await nssaiSd.first().fill(args.sd); } console.log(' → NSSAI: SST=' + args.sst + ', SD=' + args.sd); await page.waitForTimeout(500); // ===== 提交:layui form,用 JS 点击提交按钮 ===== console.log(' → 提交表单...'); await page.evaluate(() => { const btn = document.querySelector('button[lay-filter="formDemo"]'); if (btn) btn.click(); }); await page.waitForTimeout(5000); // 判断是否成功:URL 变成 /ue/index,或者停留在 /ue/edit 但有空值 const finalUrl = page.url(); console.log(' → 提交后 URL:', finalUrl); // 判断是否成功:跳转到列表验证 await page.goto(BASE + '/sim_5gc/ue/index', { waitUntil: 'networkidle', timeout: 15000 }); await page.waitForTimeout(2000); const rows = await page.locator('.layui-table tbody tr').all(); let found = false; for (const row of rows) { const text = await row.innerText().catch(() => ''); if (text.includes(args.name)) { found = true; break; } } if (found) { console.log(' ✅ UE 添加成功: ' + args.name); } else { console.log(' ⚠ UE 添加后列表中未找到(可能在其他页)'); } await browser.close(); console.log('完成'); })().catch(e => { console.error('异常:', e.message); process.exit(1); }); /** * 选择工程(jsgrid 列表,点击控制列图标) */ async function selectProject(page, projectName) { await page.goto(BASE_URL + '/sim_5gc/project/index', { waitUntil: 'networkidle', timeout: 15000 }); try { await page.waitForSelector('.jsgrid-row, .jsgrid-alt-row', { timeout: 10000 }); } catch { console.log(' ⚠ 等待 jsgrid 行超时'); } await page.waitForTimeout(2000); for (let pageNum = 1; pageNum <= 20; pageNum++) { const result = await page.evaluate((target) => { const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row'); for (const row of rows) { const cells = row.querySelectorAll('td'); if (cells.length >= 3 && cells[2].textContent.trim() === target) { if (row.classList.contains('jsgrid-selected-row')) return 'already-selected'; const controlCell = row.querySelector('.jsgrid-cell.jsgrid-control-field'); if (controlCell) { const icon = controlCell.querySelector('.layui-icon'); if (icon) { icon.click(); return 'clicked'; } } row.click(); return 'clicked'; } } return 'not-found'; }, projectName); if (result === 'already-selected') { await page.waitForTimeout(2000); return true; } if (result === 'clicked') { await page.waitForTimeout(3000); return true; } // 翻页 const nextBtn = page.locator('.jsgrid-pager a').filter({ hasText: 'Next' }).first(); if (!(await nextBtn.count())) break; try { await page.evaluate(() => { var links = document.querySelectorAll('.jsgrid-pager a'); for (var i = 0; i < links.length; i++) { if (links[i].innerText.trim() === 'Next') { links[i].click(); break; } } }); await page.waitForTimeout(2000); } catch { break; } } return false; } FILE:scripts/ue-edit-skill.js /** * UE 编辑脚本 - ue-edit-skill.js * * 单个编辑: node ue-edit-skill.js --id <ue_id> [--field <字段名> --value <值>] * node ue-edit-skill.js --name <ue_name> --set-<field> <value> * * 批量编辑: node ue-edit-skill.js --project <工程> --set-<field> <value> * * 可编辑字段: name, mcc, mnc, s_imsi, key, op_opc, imeisv, msisdn, * user_sip_ip_v4, user_sip_ip_v6, replay_ip, replay_port, * nssai_sst, nssai_sd 等 * * 示例: * node ue-edit-skill.js --id 10337 --set-msisdn 8611111111112 * node ue-edit-skill.js --name UE_TEST_001 --set-msisdn 8611111111112 * node ue-edit-skill.js --project XW_S5GC_1 --set-msisdn 8611111111112 */ const { chromium } = require('playwright'); const BASE_URL = 'https://192.168.3.89'; const LOGIN_EMAIL = '[email protected]'; const LOGIN_PWD = 'dotouch'; // 解析 --set-<field> <value> 参数 function parseSetArgs(argv) { const sets = {}; for (let i = 0; i < argv.length; i++) { if (argv[i] === '--set-msisdn') sets.msisdn = argv[++i]; else if (argv[i] === '--set-imsi') sets.s_imsi = argv[++i]; else if (argv[i] === '--set-mcc') sets.mcc = argv[++i]; else if (argv[i] === '--set-mnc') sets.mnc = argv[++i]; else if (argv[i] === '--set-key') sets.key = argv[++i]; else if (argv[i] === '--set-opc') sets.op_opc = argv[++i]; else if (argv[i] === '--set-imeisv') sets.imeisv = argv[++i]; else if (argv[i] === '--set-name') sets.name = argv[++i]; else if (argv[i] === '--set-sst') sets.nssai_sst = argv[++i]; else if (argv[i] === '--set-sd') sets.nssai_sd = argv[++i]; else if (argv[i] === '--set-user_sip_ip_v4') sets.user_sip_ip_v4 = argv[++i]; else if (argv[i] === '--set-user_sip_ip_v6') sets.user_sip_ip_v6 = argv[++i]; else if (argv[i] === '--set-replay_ip') sets.replay_ip = argv[++i]; else if (argv[i] === '--set-replay_port') sets.replay_port = argv[++i]; else if (argv[i] === '--set-count') sets.count = argv[++i]; } return sets; } const argv = process.argv.slice(2); const args = {}; let projectExplicit = false; // 标记 project 是否显式传入 for (let i = 0; i < argv.length; i++) { if (argv[i] === '--id') args.id = argv[++i]; else if (argv[i] === '--name') args.name = argv[++i]; else if (argv[i] === '--project') { args.project = argv[++i]; projectExplicit = true; } else if (argv[i] === '--url') args.url = argv[++i]; else if (argv[i] === '--headed') args.headed = true; } args.sets = parseSetArgs(argv); args.url = args.url || BASE_URL; if (!args.id && !args.name && !args.project) { console.log('用法:'); console.log(' 单个编辑: node ue-edit-skill.js --id <ue_id> --set-<field> <value>'); console.log(' 按名称: node ue-edit-skill.js --name <ue_name> --set-<field> <value>'); console.log(' 批量: node ue-edit-skill.js --project <工程> --set-<field> <value>'); console.log('\n可编辑字段: msisdn, imsi, mcc, mnc, key, opc, imeisv, name, sst, sd, user_sip_ip_v4, user_sip_ip_v6, replay_ip, replay_port, count'); process.exit(1); } const BASE = args.url; const HEADLESS = !args.headed; console.log('▶ UE 编辑'); console.log(' URL:', BASE); if (args.id) console.log(' UE ID:', args.id); if (args.name) console.log(' UE 名称:', args.name); if (args.project) console.log(' 工程:', args.project); console.log(' 修改字段:', args.sets); (async () => { const browser = await chromium.launch({ headless: HEADLESS, args: ['--no-sandbox','--ignore-certificate-errors'] }); const context = await browser.newContext({ ignoreHTTPSErrors: true }); const page = await context.newPage(); // 登录 await page.goto(BASE + '/login', { waitUntil: 'networkidle', timeout: 15000 }); await page.getByRole('textbox', { name: 'E-Mail地址' }).fill(LOGIN_EMAIL); await page.getByRole('textbox', { name: '密码' }).fill(LOGIN_PWD); await page.getByRole('checkbox', { name: '记住我' }).check(); await page.getByRole('button', { name: '登录' }).click(); await page.waitForLoadState('networkidle'); console.log(' ✓ 登录成功'); // 选择工程(单个编辑和批量编辑都需要) if (args.project) { const ok = await selectProject(page, args.project); if (!ok) throw new Error('工程选择失败: ' + args.project); console.log(' ✓ 工程已选:', args.project); } else if (!args.id && !args.name) { // 既没有 --id/--name 也没有 --project,报错 console.error('用法: node ue-edit-skill.js --id <ue_id> --set-<field> <value>'); console.error(' node ue-edit-skill.js --project <工程> --set-<field> <value>'); process.exit(1); } // ===== 单个编辑 ===== if (args.id || args.name) { let ueId = args.id; if (!ueId) { ueId = await findUeIdByName(page, args.name); if (!ueId) throw new Error('未找到 UE: ' + args.name); console.log(' ✓ 找到 UE ID:', ueId); } await editUeById(page, ueId, args.sets); } else if (projectExplicit) { // ===== 批量编辑: 仅在显式传入 --project 时执行 ===== const count = await bulkEditUe(page, args.sets, args.name || null); console.log(' ✓ 批量编辑完成,共', count, '个 UE'); } await browser.close(); console.log('完成'); })().catch(e => { console.error('异常:', e.message); process.exit(1); }); /** * 填写 UE 表单字段 */ async function fillUeForm(page, sets) { // 基本字段 if (sets.name) { await page.locator('input[name="name"]').fill(sets.name); await page.waitForTimeout(100); } if (sets.count) { await page.locator('input[name="count"]').fill(sets.count); await page.waitForTimeout(100); } if (sets.mcc) { await page.locator('input[name="mcc"]').fill(sets.mcc); await page.waitForTimeout(100); } if (sets.mnc) { await page.locator('input[name="mnc"]').fill(sets.mnc); await page.waitForTimeout(100); } if (sets.s_imsi) { await page.locator('input[name="s_imsi"]').fill(sets.s_imsi); await page.waitForTimeout(100); } if (sets.key) { await page.locator('input[name="key"]').fill(sets.key); await page.waitForTimeout(100); } if (sets.op_opc) { await page.locator('input[name="op_opc"]').fill(sets.op_opc); await page.waitForTimeout(100); } if (sets.imeisv) { await page.locator('input[name="imeisv"]').fill(sets.imeisv); await page.waitForTimeout(100); } if (sets.msisdn) { await page.locator('input[name="msisdn"]').fill(sets.msisdn); await page.waitForTimeout(100); } if (sets.user_sip_ip_v4) { await page.locator('input[name="user_sip_ip_v4"]').fill(sets.user_sip_ip_v4); await page.waitForTimeout(100); } if (sets.user_sip_ip_v6) { await page.locator('input[name="user_sip_ip_v6"]').fill(sets.user_sip_ip_v6); await page.waitForTimeout(100); } if (sets.replay_ip) { await page.locator('input[name="replay_ip"]').fill(sets.replay_ip); await page.waitForTimeout(100); } if (sets.replay_port) { await page.locator('input[name="replay_port"]').fill(sets.replay_port); await page.waitForTimeout(100); } // NSSAI if (sets.nssai_sst || sets.nssai_sd) { const nssaiAddBtn = page.locator('button.nssaiAdd'); if (await nssaiAddBtn.count() > 0) { await nssaiAddBtn.click(); await page.waitForTimeout(500); } if (sets.nssai_sst) { const sst = page.locator('input[name="nssai[snssai_sst][]"]'); if (await sst.count() > 0) await sst.first().fill(sets.nssai_sst); } if (sets.nssai_sd) { const sd = page.locator('input[name="nssai[snssai_sd][]"]'); if (await sd.count() > 0) await sd.first().fill(sets.nssai_sd); } } } /** * 通过 ID 编辑单个 UE */ async function editUeById(page, ueId, sets) { // 确保工程上下文:先进入 UE 列表 await page.goto(BASE_URL + '/sim_5gc/ue/index', { waitUntil: 'networkidle', timeout: 15000 }); await page.waitForTimeout(3000); const url0 = page.url(); // 如果在工程选择页,说明需要先选工程 if (url0.includes('/project/index')) { await selectProject(page, 'XW_S5GC_1'); console.log(' ✓ 工程已选: XW_S5GC_1'); await page.waitForTimeout(1000); } await page.goto(BASE_URL + '/sim_5gc/ue/edit/' + ueId, { waitUntil: 'networkidle', timeout: 15000 }); await page.waitForTimeout(3000); const url = page.url(); if (!url.includes('/ue/edit/')) { console.log(' ⚠ UE ID ' + ueId + ' 不存在(URL: ' + url + ')'); return; } await fillUeForm(page, sets); await page.waitForTimeout(500); // 提交 await page.evaluate(() => { const btn = document.querySelector('button[lay-filter="formDemo"]'); if (btn) btn.click(); }); await page.waitForTimeout(4000); const finalUrl = page.url(); if (finalUrl.includes('/ue/index')) { console.log(' ✓ UE ID', ueId, '修改成功'); } else { const err = await page.locator('.layui-layer-msg').innerText().catch(() => ''); console.log(' ⚠ UE ID', ueId, '修改后 URL:', finalUrl, err ? '错误: ' + err : ''); } } /** * 在 UE 列表中根据名称找 ID */ async function findUeIdByName(page, name) { await page.goto(BASE_URL + '/sim_5gc/ue/index', { waitUntil: 'networkidle', timeout: 15000 }); await page.waitForTimeout(3000); const rows = await page.locator('.layui-table tbody tr').all(); for (const row of rows) { const cells = await row.locator('td').all(); if (cells.length >= 3) { const cell2 = await cells[2].innerText().catch(() => ''); if (cell2.trim() === name) { // ID 在 cells[1] const idCell = await cells[1].innerText().catch(() => ''); return idCell.trim(); } } } return null; } /** * 批量编辑: 对工程下所有 UE 进行字段修改 * @param {string|null} nameFilter - 可选,按名称过滤 */ async function bulkEditUe(page, sets, nameFilter = null) { await page.goto(BASE_URL + '/sim_5gc/ue/index', { waitUntil: 'networkidle', timeout: 15000 }); await page.waitForTimeout(3000); const rows = await page.locator('.layui-table tbody tr').all(); let count = 0; const processedIds = new Set(); for (const row of rows) { const cells = await row.locator('td').all(); if (cells.length < 3) continue; const idCell = await cells[1].innerText().catch(() => ''); const nameCell = await cells[2].innerText().catch(() => ''); const id = idCell.trim(); const name = nameCell.trim(); // 跳过表头、空行、按钮行、或已处理过的 ID if (!id || id === 'ID' || !name || name === '编辑' || processedIds.has(id)) continue; // 如果指定了 nameFilter,只处理该名称的 UE if (nameFilter && name !== nameFilter) continue; processedIds.add(id); await editUeById(page, id, sets); count++; } return count; } /** * 选择工程 */ async function selectProject(page, projectName) { await page.goto(BASE_URL + '/sim_5gc/project/index', { waitUntil: 'networkidle', timeout: 15000 }); try { await page.waitForSelector('.jsgrid-row, .jsgrid-alt-row', { timeout: 10000 }); } catch {} await page.waitForTimeout(2000); for (let pageNum = 1; pageNum <= 20; pageNum++) { const result = await page.evaluate((target) => { const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row'); for (const row of rows) { const cells = row.querySelectorAll('td'); if (cells.length >= 3 && cells[2].textContent.trim() === target) { if (row.classList.contains('jsgrid-selected-row')) return 'already-selected'; const controlCell = row.querySelector('.jsgrid-cell.jsgrid-control-field'); if (controlCell) { const icon = controlCell.querySelector('.layui-icon'); if (icon) { icon.click(); return 'clicked'; } } row.click(); return 'clicked'; } } return 'not-found'; }, projectName); if (result === 'already-selected') { await page.waitForTimeout(2000); return true; } if (result === 'clicked') { await page.waitForTimeout(3000); return true; } const nextBtn = page.locator('.jsgrid-pager a').filter({ hasText: 'Next' }).first(); if (!(await nextBtn.count())) break; try { // Use JS click instead of Playwright click for jsgrid pagination reliability await page.evaluate(() => { const links = document.querySelectorAll('.jsgrid-pager a'); for (const link of links) { if (link.innerText.trim() === 'Next') { link.click(); break; } } }); await page.waitForTimeout(2000); } catch { break; } } return false; } FILE:scripts/upf-add-skill.js #!/usr/bin/env node /** * UPF/PGW-U 添加脚本 * 完整流程:登录 → 选工程 → 填表单 → 点NSSAI行按钮添加一行 → 填NSSAI/TAC → 提交 * 用法: node upf-add-skill.js <名称> [--project <工程>] [--url <地址>] [--headed] \ * [--n4_ip <IP>] [--n3_ip <IP>] [--n6_ip <IP>] [--n4_port <端口>] \ * [--MCC <值>] [--MNC <值>] [--pdu_capacity <数量>] \ * [--ue_min <IP>] [--ue_max <IP>] * 示例: node upf-add-skill.js UPF-TEST --project XW_S5GC_1 * node upf-add-skill.js UPF-PROD --n4_ip 10.0.0.50 --n6_ip 10.0.0.51 --MCC 460 --MNC 01 */ const { chromium } = require('playwright'); const fs = require('fs'); const path = require('path'); const BASE_URL = 'https://192.168.3.89'; const CONFIG = { urls: { login: '/login', upfEdit: '/sim_5gc/upf/edit', upfList: '/sim_5gc/upf/index' }, credentials: { email: '[email protected]', password: 'dotouch' }, sessionDir: path.join(__dirname, '.sessions'), getSessionFile() { return `5gc_session_\/\//, '').replace(/\./g, '_').json`; } }; class SessionManager { constructor() { if (!fs.existsSync(CONFIG.sessionDir)) fs.mkdirSync(CONFIG.sessionDir, { recursive: true }); this.sp = path.join(CONFIG.sessionDir, CONFIG.getSessionFile()); } async loadSession(browser) { if (!fs.existsSync(this.sp)) return null; const { storageState } = JSON.parse(fs.readFileSync(this.sp, 'utf8')); return await browser.newContext({ storageState, ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } }); } async saveSession(context) { fs.writeFileSync(this.sp, JSON.stringify({ storageState: await context.storageState() }, null, 2)); } } // 选择工程(jsgrid 分页遍历,精确匹配,点击 checkbox) async function selectProject(page, projectName) { await page.goto(`BASE_URL/sim_5gc/project/index`, { waitUntil: 'networkidle', timeout: 15000 }); await page.waitForSelector('.jsgrid-row, .jsgrid-alt-row', { timeout: 5000 }).catch(() => {}); for (let pageNum = 1; pageNum <= 20; pageNum++) { const result = await page.evaluate((targetName) => { const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row'); for (const row of rows) { const cells = row.querySelectorAll('td'); if (cells.length >= 3 && cells[2].textContent.trim() === targetName) { const checkbox = cells[0].querySelector('input[type="checkbox"]'); if (checkbox) { checkbox.click(); return 'clicked'; } const icon = cells[1].querySelector('.iconfont'); if (icon) { icon.click(); return 'icon-clicked'; } } } return 'not-found'; }, projectName); if (result === 'clicked' || result === 'icon-clicked') { await page.waitForTimeout(2000); return true; } // 点下一页 const nextBtn = page.locator('.jsgrid-pager a').filter({ hasText: 'Next' }).first(); if (!(await nextBtn.count())) break; try { await page.evaluate(() => { var links = document.querySelectorAll('.jsgrid-pager a'); for (var i = 0; i < links.length; i++) { if (links[i].innerText.trim() === 'Next') { links[i].click(); break; } } }); await page.waitForTimeout(2000); } catch { break; } } console.log(` ❌ 未找到工程 "projectName"`); return false; } // 添加 UPF 主流程 async function addUpf(upfName, projectName, upfConfig = {}) { const startTime = Date.now(); const sessionMgr = new SessionManager(); const browser = await chromium.launch({ headless: true, args: ['--no-sandbox', '--ignore-certificate-errors'] }); const context = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } }); const page = await context.newPage(); // 登录 await page.goto(`BASE_URLCONFIG.urls.login`, { waitUntil: 'networkidle' }); await page.getByRole('textbox', { name: 'E-Mail地址' }).fill(CONFIG.credentials.email); await page.getByRole('textbox', { name: '密码' }).fill(CONFIG.credentials.password); await page.getByRole('checkbox', { name: '记住我' }).check(); await page.getByRole('button', { name: '登录' }).click(); await page.waitForLoadState('networkidle'); await sessionMgr.saveSession(context); console.log(' ✓ 登录成功'); // 选工程(通过 jsgrid 精确匹配 + 点击 iconfont) const ok = await selectProject(page, projectName); if (!ok) throw new Error(`工程 "projectName" 未找到`); console.log(` ✓ 工程 "projectName" 已选`); await page.waitForTimeout(1000); // 直接导航到 UPF 列表页(工程已在 sidebar 激活) await page.goto(`BASE_URL/sim_5gc/upf/index`, { waitUntil: 'networkidle' }); await page.waitForTimeout(2000); console.log(' ✓ 进入 UPF/PGW-U'); // 点添加 await page.getByRole('button', { name: '添加' }).click(); await page.waitForTimeout(1500); console.log(' ✓ 点添加'); // === 填基本字段 === await page.evaluate((name) => { const setVal = (n, v) => { const el = document.querySelector(`input[name="n"]`); if (!el) return; el.value = v; el.dispatchEvent(new Event('input', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true })); }; setVal('name', name); }, upfName); // type: select → 1=仿真设备(layui隐藏了原select,用JS设值) await page.evaluate(() => { const sel = document.querySelector('select[name="type"]'); if (sel) { sel.value = '1'; sel.dispatchEvent(new Event('change', { bubbles: true })); } }); console.log(' ✓ 类型已选'); // 基本网络字段 const cfg = { n4_ip: upfConfig.n4_ip || '192.168.20.30', n4_port: upfConfig.n4_port || '8805', n3_ip: upfConfig.n3_ip || '192.168.20.30', n6_ip: upfConfig.n6_ip || '192.168.20.31', pdu_capacity: upfConfig.pdu_capacity || '20000', ue_min: upfConfig.ue_min || '20.20.20.20', ue_max: upfConfig.ue_max || '20.20.60.20', }; await page.evaluate((c) => { const setVal = (n, v) => { const el = document.querySelector(`input[name="n"]`); if (!el) return; el.value = v; el.dispatchEvent(new Event('input', { bubbles: true })); }; setVal('n4_ip', c.n4_ip); setVal('n4_port', c.n4_port); setVal('n3_ip', c.n3_ip); setVal('n6_ip', c.n6_ip); setVal('pdu_capacity', c.pdu_capacity); setVal('ue_min', c.ue_min); setVal('ue_max', c.ue_max); }, cfg); console.log(' ✓ 基本字段已填'); // MCC/MNC(通过 page.evaluate 设置,因为 layui 需要事件触发) if (upfConfig.MCC || upfConfig.MNC) { await page.evaluate((c) => { const setVal = (n, v) => { const el = document.querySelector(`input[name="n"]`); if (!el) return; el.value = v; el.dispatchEvent(new Event('input', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true })); }; setVal('MCC', c.MCC || '460'); setVal('MNC', c.MNC || '01'); }, { MCC: upfConfig.MCC, MNC: upfConfig.MNC }); console.log(` ✓ MCC=upfConfig.MCC || '460' MNC=upfConfig.MNC || '01'`); } // === NSSAI / TAC 配置 === // 点击"数量*nssai*类型*TAC起始*"行末尾的"刷新/添加"按钮(添加一行) await page.getByRole('row', { name: /\* 数量.*nssai.*类型.*TAC起始/ }).locator('button').click(); await page.waitForTimeout(800); console.log(' ✓ NSSAI 行已添加'); // 填写 config 字段 await page.evaluate(() => { const setVal = (n, v) => { const el = document.querySelector(`input[name="n"]`); if (!el) return; el.value = v; el.dispatchEvent(new Event('input', { bubbles: true })); }; setVal('config[count][]', '1'); setVal('config[nssai][]', '1'); setVal('config[stac][]', '101'); setVal('config[etac][]', '102'); }); console.log(' ✓ NSSAI/TAC 已填'); // === 提交 === await page.getByRole('button', { name: '提交' }).click(); console.log(' ✓ 已提交'); await page.waitForTimeout(3000); const finalUrl = page.url(); console.log(' 最终 URL:', finalUrl); let success = finalUrl.includes('upf/index'); if (success) { console.log(' ✅ 跳转成功 - UPF 添加完成'); } else { const txt = await page.evaluate(() => document.body.innerText.slice(0, 300)); console.log(' 页面文本:', txt.replace(/\s+/g, ' ').slice(0, 200)); await page.screenshot({ path: 'add-result.png' }); } // 验证列表中是否存在 await page.reload({ waitUntil: 'networkidle' }); await page.waitForTimeout(3000); // UPF 表:ID在第2列(cells[1]),名称在第3列(cells[2]) const debug = await page.evaluate((targetName) => { const allRows = document.querySelectorAll('table tbody tr'); const entries = []; for (const row of allRows) { const cells = row.querySelectorAll('td'); if (cells.length >= 3) { entries.push(cells[2].textContent.trim()); // 名称列 } } return { url: window.location.href, entries: entries.slice(0, 20), found: entries.includes(targetName) }; }, upfName); console.log('列表调试 - URL:', debug.url); console.log('UPF 列表:', debug.entries.join(', ')); console.log(debug.found ? `✅ UPF "upfName" 已出现在列表中!` : `⚠ UPF "upfName" 不在列表中`); await browser.close(); const totalTime = (Date.now() - startTime) / 1000; return { success, upfName, totalTime }; } // 主函数 async function main() { const args = process.argv.slice(2); if (args.length === 0) { console.log('用法: node upf-add-skill.js <UPF名称> [--project <工程名>] [--url <地址>] [--headed]'); console.log(' [--n4_ip <IP>] [--n3_ip <IP>] [--n6_ip <IP>]'); console.log(' [--MCC <值>] [--MNC <值>] [--pdu_capacity <数量>]'); console.log(' [--ue_min <IP>] [--ue_max <IP>]'); console.log('示例: node upf-add-skill.js UPF-TEST --project XW_S5GC_1'); console.log(' node upf-add-skill.js UPF-PROD --n4_ip 10.0.0.50 --n6_ip 10.0.0.51 --MCC 460 --MNC 01'); process.exit(1); } let upfName = null, projectName = 'XW_S5GC_1', explicitProject = false; let urlOverride = null; const upfConfig = { n4_ip: '192.168.20.30', n4_port: '8805', n3_ip: '192.168.20.30', n6_ip: '192.168.20.31', MCC: '460', MNC: '01', pdu_capacity: '20000', ue_min: '20.20.20.20', ue_max: '20.20.60.20', }; for (let i = 0; i < args.length; i++) { if (!args[i].startsWith('-')) upfName = args[i]; else if (args[i] === '--project' || args[i] === '-p') { projectName = args[++i]; explicitProject = true; } else if (args[i] === '--url') { urlOverride = args[++i]; } else if (args[i] === '--n4_ip') { upfConfig.n4_ip = args[++i]; } else if (args[i] === '--n4_port') { upfConfig.n4_port = args[++i]; } else if (args[i] === '--n3_ip') { upfConfig.n3_ip = args[++i]; } else if (args[i] === '--n6_ip') { upfConfig.n6_ip = args[++i]; } else if (args[i] === '--MCC') { upfConfig.MCC = args[++i]; } else if (args[i] === '--MNC') { upfConfig.MNC = args[++i]; } else if (args[i] === '--pdu_capacity') { upfConfig.pdu_capacity = args[++i]; } else if (args[i] === '--ue_min') { upfConfig.ue_min = args[++i]; } else if (args[i] === '--ue_max') { upfConfig.ue_max = args[++i]; } } if (!upfName) { console.error('错误: 请指定 UPF 名称'); process.exit(1); } if (urlOverride) { if (!urlOverride.startsWith('http')) urlOverride = 'https://' + urlOverride; } const targetUrl = urlOverride || BASE_URL; console.log(`▶ 添加 UPF: upfName | 工程: projectName | 地址: targetUrl`); console.log(` n4_ip=upfConfig.n4_ip n3_ip=upfConfig.n3_ip n6_ip=upfConfig.n6_ip MCC=upfConfig.MCC MNC=upfConfig.MNC`); try { process.env.UPF_ADD_BASE_URL = targetUrl; const result = await addUpf(upfName, projectName, upfConfig); console.log(`\n总耗时: result.totalTime.toFixed(1)s`); process.exit(result.success ? 0 : 1); } catch (err) { console.error('异常:', err.message); process.exit(1); } } main(); FILE:scripts/upf-edit-skill.js #!/usr/bin/env node /** * UPF/PGW-U 编辑脚本(单条 + 批量二合一) - 生产版 * 用法: node upf-edit-skill.js [--project <工程名称>] --set-<字段名> <值> * 示例: node upf-edit-skill.js --project XW_S5GC_basic \ * --set-n4_ip 9.9.9.9 --set-n3_ip 3.3.3.3 * * 脚本会在指定工程下遍历所有 UPF,逐个打开编辑页面并写入提供的字段值。 */ const { chromium } = require('playwright'); const fs = require('fs'); const path = require('path'); let BASE_URL = 'https://192.168.3.89'; const CONFIG = { urls: { login: '/login', upfList: '/sim_5gc/upf/index' }, credentials: { email: '[email protected]', password: 'dotouch' }, sessionDir: path.join(__dirname, '.sessions'), getSessionFile() { return `5gc_upf_bulk_\/\//, '').replace(/\./g, '_').json`; } }; class SessionManager { constructor() { if (!fs.existsSync(CONFIG.sessionDir)) fs.mkdirSync(CONFIG.sessionDir, { recursive: true }); this.sp = path.join(CONFIG.sessionDir, CONFIG.getSessionFile()); } async loadSession(browser) { if (!fs.existsSync(this.sp)) return null; const { storageState } = JSON.parse(fs.readFileSync(this.sp, 'utf8')); return await browser.newContext({ storageState, ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } }); } async saveSession(context) { fs.writeFileSync(this.sp, JSON.stringify({ storageState: await context.storageState() }, null, 2)); } } // 选择工程(点击目标工程行内的 .layui-icon 图标) async function selectProject(page, projectName) { await page.goto(`BASE_URL/sim_5gc/project/index`, { waitUntil: 'networkidle', timeout: 15000 }); // 等待 jsgrid 表格渲染完成(最多 10 秒) try { await page.waitForSelector('.jsgrid-row, .jsgrid-alt-row', { timeout: 10000 }); } catch { console.log(' ⚠ 等待 jsgrid 行超时'); } await page.waitForTimeout(2000); // 额外等待确保页面完全就绪 for (let pageNum = 1; pageNum <= 20; pageNum++) { // 在当前页查找目标工程行 const result = await page.evaluate((target) => { const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row'); for (const row of rows) { const cells = row.querySelectorAll('td'); if (cells.length >= 3 && cells[2].textContent.trim() === target) { // 只有同时具备 'jsgrid-selected-row' class 才认为已选中 if (row.classList.contains('jsgrid-selected-row')) { return 'already-selected'; } // 未选中,点击该行控制列中的 .layui-icon 图标进行切换 const controlCell = row.querySelector('.jsgrid-cell.jsgrid-control-field'); if (controlCell) { const icon = controlCell.querySelector('.layui-icon'); if (icon) { icon.click(); return 'clicked'; } } // 备用:直接点击行 row.click(); return 'clicked'; } } return 'not-found'; }, projectName); if (result === 'already-selected') { console.log(' ✓ 工程已在第一行(已选中状态),跳过'); await page.waitForTimeout(2000); return true; } if (result === 'clicked') { console.log(' ✓ 已点击工程行图标,等待切换...'); await page.waitForTimeout(3000); // 等待工程切换(行会跑到第一行) return true; } // 当前页未找到,尝试翻页 const nextBtn = page.locator('.jsgrid-pager a').filter({ hasText: 'Next' }).first(); if (!(await nextBtn.count())) { console.log(' ⚠ 已到最后一页,仍未找到工程'); break; } try { await nextBtn.click({ force: true }); await page.waitForTimeout(2000); } catch { break; } } console.log(` ❌ 未找到工程 "projectName"`); return false; } /** * 获取当前列表页所有 UPF 的 ID */ async function fetchAllUpfIds(page, nameFilter = null) { // 表格结构: 第2列为 ID,第3列为名称(去重) return await page.evaluate((filter) => { const rows = document.querySelectorAll('.layui-table tbody tr'); const ids = []; const seen = new Set(); for (const row of rows) { const cells = row.querySelectorAll('td'); if (cells.length >= 3) { const id = cells[1].textContent.trim(); const name = cells[2].textContent.trim(); if (id && !seen.has(id) && (!filter || name === filter)) { seen.add(id); ids.push(id); } } } return ids; }, nameFilter); } /** * 对单个 UPF 执行编辑操作 */ async function editSingleUpf(page, upfId, edits) { // 只修改用户指定的字段,不触碰其他字段 const editUrl = `BASE_URL/sim_5gc/upf/edit/upfId`; await page.goto(editUrl, { waitUntil: 'networkidle' }); await page.waitForTimeout(2000); // 使用 page.fill 方式写入字段(点击 → 输入 → 触发完整事件链) for (const [field, val] of Object.entries(edits)) { const selector = `input[name="field"]`; try { await page.waitForSelector(selector, { state: 'visible', timeout: 5000 }); await page.click(selector, { force: true }); await page.fill(selector, val); console.log(` ✓ field → val`); } catch { console.log(` ⚠ 字段 field 未找到或无法填写`); } } console.log(' ✓ 字段修改完成'); // 提交 await page.getByRole('button', { name: '提交' }).click(); await page.waitForTimeout(3000); // 通过是否回到列表页判断成功 const success = page.url().includes('upf/index'); if (!success) console.log(' ⚠ 提交后未返回列表,可能失败'); return success; } async function main() { // 参数解析 const args = process.argv.slice(2); let projectName = 'XW_S5GC_1'; let nameFilter = null; const edits = {}; let headless = true; // 默认无头模式 for (let i = 0; i < args.length; i++) { if (args[i] === '--project') { projectName = args[++i]; } else if (args[i] === '--name') { nameFilter = args[++i]; } else if (args[i] === '--headed') { headless = false; } else if (args[i].startsWith('--set-')) { // '--set-' is 5 chars, the field name starts after that. Slice from index 6 to drop the extra '-' const field = args[i].slice(6); edits[field] = args[++i]; } else if (!args[i].startsWith('-')) { if (!nameFilter) nameFilter = args[i]; } } if (Object.keys(edits).length === 0) { console.log('用法: node upf-edit-skill.js [--project <工程>] [--headed] --set-<字段> <值>'); console.log('示例: node upf-edit-skill.js --project XW_S5GC_basic --set-n4_ip 9.9.9.9'); console.log(' --headed 打开浏览器窗口(调试用)'); process.exit(1); } console.log(`▶ 批量编辑 UPF,工程: projectName' + nameFilter : ''`); console.log(` 修改字段:`, edits); console.log(` 模式: '有头'`); const sessionMgr = new SessionManager(); const browser = await chromium.launch({ headless, args: ['--no-sandbox', '--ignore-certificate-errors'] }); const context = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } }); const page = await context.newPage(); // 登录 await page.goto(`BASE_URLCONFIG.urls.login`, { waitUntil: 'networkidle' }); await page.getByRole('textbox', { name: 'E-Mail地址' }).fill(CONFIG.credentials.email); await page.getByRole('textbox', { name: '密码' }).fill(CONFIG.credentials.password); await page.getByRole('checkbox', { name: '记住我' }).check(); await page.getByRole('button', { name: '登录' }).click(); await page.waitForLoadState('networkidle'); await sessionMgr.saveSession(context); console.log(' ✓ 登录成功'); // 选工程 const projOk = await selectProject(page, projectName); if (!projOk) throw new Error('工程选择失败'); console.log(' ✓ 工程已选'); // 进入 UPF 列表 await page.goto(`BASE_URLCONFIG.urls.upfList`, { waitUntil: 'networkidle' }); await page.waitForTimeout(2000); // 读取全部 UPF ID const upfIds = await fetchAllUpfIds(page, nameFilter); console.log(` found upfIds.length UPF(s) in project "projectName"`); let successCount = 0; for (let idx = 0; idx < upfIds.length; idx++) { const id = upfIds[idx]; console.log(`\n[idx + 1/upfIds.length] 编辑 UPF ID: id`); try { const ok = await editSingleUpf(page, id, edits); if (ok) { console.log(' ✅ 编辑成功'); successCount++; } else { console.log(' ⚠ 编辑后未返回列表,可能失败'); } } catch (e) { console.log(' ❌ 异常:', e.message); } // 防止触发风控,稍作延迟 await page.waitForTimeout(1500); } console.log('\n=== 完成 ==='); console.log(`成功编辑 successCount / upfIds.length 个 UPF`); await browser.close(); } main().catch(e => { console.error('异常:', e.message); process.exit(1); });