Skill: liuwei120/5gc-automation (version 1.0.0)

--- 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`（默认） - 登录凭证：`[REDACTED_SECRET]` / `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` |

path: scripts/5gc.js
content: /**
 * 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
`);
}


path: scripts/amf-add-skill.js
content: #!/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: '[REDACTED_SECRET]',
    [REDACTED_SECRET]'
  },
  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(`${globalBaseUrl}${CONFIG.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(`${globalBaseUrl}${CONFIG.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(`${globalBaseUrl}${CONFIG.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(`${globalBaseUrl}${CONFIG.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();


path: scripts/default-rule-add-skill.js
content: /**
 * 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('[REDACTED_SECRET]');
  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(`${globalBaseUrl}${url}`, { 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, [REDACTED_SECRET]);
  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(`   [REDACTED_SECRET], 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.pass ? '✅' : '❌'} ${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); });


path: scripts/nrf-add-skill.js
content: /**
 * 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('[REDACTED_SECRET]');
  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); });


path: scripts/pcc-add-skill.js
content: /**
 * 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('[REDACTED_SECRET]');
  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}"${opts.precedence !== null ? '（用户指定）' : '（默认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} ${pccData.refQosData === opts.qos ? '✅' : '❌'}`);
    console.log(`   refTcData  = ${pccData.refTcData} ${pccData.refTcData === opts.tc ? '✅' : '❌'}`);
  }

  console.log('\n✅ 完成');
  await browser.close();
}

main().catch(e => { console.error(e); process.exit(1); });


path: scripts/pcf-add-skill.js
content: /**
 * 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('[REDACTED_SECRET]');
  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); });


path: scripts/qos-add-skill.js
content: /**
 * 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('[REDACTED_SECRET]');
  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); });


path: scripts/smpolicy-ue-add-skill.js
content: /**
 * 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('[REDACTED_SECRET]');
  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 || `4600${Date.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} ${added.name === opts.name ? '✅' : '❌'}`);
    console.log(`   dnn       = ${added.dnn} ${added.dnn === opts.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 匹配: ${match ? '✅' : '⚠️'}`);
    }
  } else {
    console.log('\n❌ 未在列表中找到创建的 UE Smpolicy');
  }

  console.log('\n✅ 完成');
  await browser.close();
}

main().catch(e => { console.error(e); process.exit(1); });


path: scripts/smpolicy_add_pcc.js
content: /**
 * 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('[REDACTED_SECRET]');
  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); });

path: scripts/tc-add-skill.js
content: /**
 * 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('[REDACTED_SECRET]');
  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} ${tcData.flowStatus === opts.flowStatus ? '✅' : '❌'}`);
  } else {
    console.log('\n❌ TC模板未找到');
  }

  console.log('\n✅ 完成');
  await browser.close();
}

main().catch(e => { console.error(e); process.exit(1); });

Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)