关于 s2-netbox-mcp
s2-netbox-mcp 是连接 LenelS2 S2 NetBox 门禁控制设备的本地 MCP 服务,面向需要通过 NBAPI 查询门禁数据的运维人员。它默认只读,写入和破坏性操作需分别显式启用。
主要功能
- 01查询人员与凭证
- 02查看门禁级别和读卡器
- 03检索事件与活动记录
- 04读取分区及自定义字段列表
- 05写入和破坏性操作需单独启用
适用场景
- 01查询人员的门禁权限
- 02调查门禁卡拒绝事件
- 03查看门禁设备与活动记录
MCP 相关
通过 MCP 工具向兼容客户端开放 LenelS2 S2 NetBox NBAPI 操作;默认只读,写入及破坏性操作由独立环境变量控制。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
npx -y s2-netbox-mcpNETBOX_BASE_URLBase URL of the S2 NetBox controller's web interface, e.g. https://netbox.example.internal (no trailing slash or path)NETBOX_USERNAMENBAPI session-login usernameNETBOX_PASSWORD密钥NBAPI session-login passwordNETBOX_ENABLE_WRITESSet to true/1/yes to register the write tools (lock/unlock, add/modify/delete). Unset keeps the server strictly read-only.NETBOX_ENABLE_DESTRUCTIVESet to true/1/yes together with NETBOX_ENABLE_WRITES to additionally register the 11 destructive (delete/remove) tools本地服务需要在你的电脑上安装运行环境(如 Node.js 18+)。各客户端的配置文件位置不同,请参考客户端文档。
工具
37收录的工具列表 · 更新于 2 周前
此工具无需参数。
详细介绍
s2-netbox-mcp
一个本地 MCP 服务,可将 LenelS2 S2 NetBox NBAPI 操作—— 人员/凭据、访问级别、门禁通道/读卡器/输出、时间规范、 节假日、门禁通道/读卡器组、威胁级别、事件/活动,以及 分区/UDF 列表——作为 MCP 工具提供,供任何兼容 MCP 的客户端 (Claude、Gemini/Antigravity 等)使用。
请勿将其与开源的 netboxlabs.com「NetBox」DCIM/IPAM 工具混淆。此项目针对的是 LenelS2 的 S2 NetBox 实体门禁控制设备及其 NBAPI (
Web-Based API for S2 NetBox and S2 Global,LenelS2 文档编号 #API-UG-14)。 另有较新的 NBAPI v1(文档编号 #API-UG-22,2024 年 4 月)和 v2(文档编号 #API2-UG-8, 2025 年 4 月)指南,参见docs/reference/中的参考副本。这两份指南中的命令和参数现已逐一与此服务的工具 功能进行差异比对:docs/reference/nbapi-command-diff.md记录了比对结果,包括有意未实现的文档命令及其原因。
警告
此服务会连接到真实的实体安全系统。 配置不当时,使用此服务的 AI Agent 可能会解锁门,或修改正在运行的建筑中的门禁数据。默认情况下,此服务为只读—— 写入和破坏性操作(锁定/解锁、添加/修改/删除)均需各自明确选择启用对应的环境变量(参见下文的写入权限 部分)——但你有责任决定启用哪些功能,以及使用哪个 MCP 客户端/模型。对生产控制器进行任何超出只读范围的部署前,请参阅
SECURITY.md。
安装
分三步,另有可选的第四步:检查要求、安装服务、将其注册到 MCP 客户端,并可选择添加 工作流技能。首次实际调用前,还请检查 控制器前置要求。
要求
-
Node.js >= 18.17(已在 Node 24 上测试),包含 npm。如果尚未安装 Node.js,在 Windows 上可使用 winget 安装,然后打开新的终端,以便将
node/npm加入PATH:winget install OpenJS.NodeJS.LTS -
可从此服务运行位置访问的 S2 NetBox 控制器,且已启用 NBAPI 并配置为使用会话登录身份验证(而非 MAC 身份验证——请参阅规范,了解为何 v1 不支持此方式)
-
具有 API 访问权限,并对你要查询的资源具有读取权限的 NetBox 操作员账户
安装 MCP 服务
有两种方式获取此服务:
选项 A — npm(无需克隆):
npm install -g s2-netbox-mcp
这会安装 s2-netbox-mcp 二进制文件;将 MCP 客户端的 command 指向
s2-netbox-mcp 即可(无需使用 node dist/index.js)。
选项 B — 克隆并构建:
npm install
npm run build
无论采用哪种方式,都请将 .env.example 复制为 .env 并填写真实值(或直接在 shell/Claude Code MCP 服务配置的
env 块中提供相同的变量——见下文)。切勿提交 .env——它已列入 gitignore。
cp .env.example .env
# edit .env
直接启动服务,确认它能正常运行(它会通过 stdio 等待 MCP 客户端——按 Ctrl+C 停止;若已打开会话,也会发送
Logout):
npm start
在 MCP 客户端中注册
此服务适用于任何支持标准 mcpServers
stdio 配置格式的 MCP 客户端——Claude Code、Antigravity 和 Gemini CLI 均已通过直接验证。具体使用哪段 JSON,取决于你在上文选择的安装方式:
如果通过 npm 安装(上文选项 A):
{
"mcpServers": {
"s2-netbox-mcp": {
"command": "s2-netbox-mcp",
"env": {
"NETBOX_BASE_URL": "https://netbox.example.internal",
"NETBOX_USERNAME": "svc-account",
"NETBOX_PASSWORD": "REPLACE_ME",
"NETBOX_ALLOW_INSECURE_TLS": "false"
}
}
}
}
如果克隆并在本地构建(上文选项 B):
{
"mcpServers": {
"s2-netbox-mcp": {
"command": "node",
"args": ["/absolute/path/to/s2-netbox-mcp/dist/index.js"],
"env": {
"NETBOX_BASE_URL": "https://netbox.example.internal",
"NETBOX_USERNAME": "svc-account",
"NETBOX_PASSWORD": "REPLACE_ME",
"NETBOX_ALLOW_INSECURE_TLS": "false"
}
}
}
}
将 args 路径替换为你机器上 dist/index.js 的实际绝对路径,并将 env 值替换为你实际的控制器信息(也可以完全省略 env,改为依赖项目目录中的 .env 文件——两种方式都可以,因为 src/index.ts 会通过 dotenv 加载 .env,然后再读取 process.env)。先运行 npm run build,确保 dist/index.js 已生成。
JSON 的放置位置取决于客户端:
| 客户端 | 配置文件 | 备注 |
|---|---|---|
| Claude Code | 项目中的 .mcp.json,或 claude_desktop_config.json | 也可以通过 claude mcp add-json s2-netbox-mcp '<json>' 以非交互方式添加 |
| Antigravity | ~/.gemini/config/mcp_config.json(Windows:%USERPROFILE%\.gemini\config\mcp_config.json) | 全局配置——适用于每个 Antigravity 会话 |
| Gemini CLI | ~/.gemini/settings.json,位于其自身的 mcpServers 键下 | 也可以通过 gemini mcp add s2-netbox-mcp <command> [args] -e KEY=value 以非交互方式添加 |
三者使用的 mcpServers 对象格式都与上文所示完全相同——只有外围文件和存放位置不同。
安装工作流技能(可选)
s2-netbox-workflows Agent Skill 在此服务的工具基础上,提供逐步引导的入职、离职、事件解锁和门禁卡拒绝工作流。需要先完成上文的服务注册。
在 Claude Code 中安装(插件):
/plugin marketplace add J-MaFf/s2-netbox-mcp
/plugin install s2-netbox-workflows@s2-netbox-mcp
plugins/s2-netbox-workflows/ 中的插件仅包含技能。它不会打包或启动 MCP 服务,因此不会在已注册的服务旁边再启动一个服务。要获取较新版本,请运行 claude plugin marketplace update s2-netbox-mcp,然后运行 claude plugin update s2-netbox-workflows@s2-netbox-mcp,最后重启 Claude Code。
手动安装(Claude Code 或任何读取 Agent Skills 的 Agent): 将 plugins/s2-netbox-workflows/skills/s2-netbox-workflows/ 复制或符号链接到 Agent 的技能目录中。对于 Claude Code,如果要让所有项目都能使用,请放在 ~/.claude/skills/;如果只用于单个项目,请放在项目中的 .claude/skills/。当请求与该技能匹配时,技能会自动加载,例如“为 Jane Doe 办理入职,门禁卡号为 12345”“周五 8 点到 5 点解锁大堂”,或“为什么门禁卡 4471 在码头门被拒绝?”
控制器前提条件
在此服务能够与控制器通信之前,请在 NetBox Web UI 中前往 Configuration → Site Settings → Network Controller → Data Integration,并确认以下三项均已勾选:
- 启用 V2
- 使用身份验证
- 使用登录用户名/密码进行身份验证(需要 setup 权限)
NBAPI 用户帐户还需要具有 NBAPI 读取权限 的角色(参见 NBAPI 文档中的 “Setting Up User Roles for the API” 一节)——如果登录成功,但无法读取此服务查询的资源,工具调用会返回 FAIL 或 APIERROR。如果设置了 NETBOX_ENABLE_WRITES,该角色需要具备 Read-Write API 权限(Configuration → Site Settings → User Roles → API Privilege)——只有读取工具才可以使用 Read-Only 权限。
故障排除
- “登录成功,但其他所有命令都返回
APIERROR 5。” 这是实时观察到的症状:未勾选 Use login username/password for authentication (requires setup privilege) 复选框,导致控制器进入 MAC 身份验证模式,而不是会话登录模式(本服务不支持 MAC 身份验证,详见规范)。Login仍会返回SUCCESS和会话 ID,但之后的所有命令(包括Logout)都会失败,并返回APIERROR 5。解决方法:在 Data Integration 选项卡中勾选该复选框。此服务的客户端会检测这一特定模式(重新登录成功后再次出现APIERROR 5),并直接指出该复选框,提示工具错误。 - “HTTP 410 Gone。” 配置的
NETBOX_API_PATH并非此控制器提供的路径。NetBox 6.x 在/nbws/goforms/nbapi提供 NBAPI(此服务使用的默认路径);2020 年文档中的/goforms/nbapi路径已在 6.x 中注销,每个请求都会返回 410。如果使用的是 6.x 之前的控制器,请显式设置NETBOX_API_PATH=/goforms/nbapi;如果使用的是 6.x,但仍遇到此问题,请再次确认NETBOX_API_PATH没有被误设为其他值。
环境变量
| 变量 | 必填 | 默认值 | 说明 |
|---|---|---|---|
NETBOX_BASE_URL | 是 | — | NetBox 控制器 Web 界面的基础 URL,例如 https://netbox.example.internal。末尾不要添加斜杠或路径 — 客户端会自行附加 NETBOX_API_PATH。 |
NETBOX_USERNAME | 是 | — | NBAPI 会话登录用户名。 |
NETBOX_PASSWORD | 是 | — | NBAPI 会话登录密码。绝不记录,也绝不写入任何受版本控制的文件。 |
NETBOX_ALLOW_INSECURE_TLS | 否 | false | 设置为 true/1/yes 以接受自签名/本地部署的 TLS 证书。仅在明确选择启用时生效 — 其他任何值(包括未设置)都会保留正常的证书验证。 |
NETBOX_API_PATH | 否 | /nbws/goforms/nbapi | 附加到 NETBOX_BASE_URL 的 NBAPI 路径。此默认值是在 NetBox 6.x 控制器上验证过的路径。仅在需要覆盖默认值时设置 — 例如设置为旧版、6.x 之前的路径 /goforms/nbapi;此路径在 6.x 控制器上会返回 HTTP 410 Gone(请参阅下文的控制器先决条件)。如果值没有以 / 开头,系统会自动添加。 |
NETBOX_ENABLE_WRITES | 否 | false | 设置为 true/1/yes 以注册写入工具(请参阅下文的写入权限)。未设置(或设置为其他任何值)时,服务将严格保持只读。 |
NETBOX_ENABLE_DESTRUCTIVE | 否 | false | 设置为 true/1/yes,并同时启用 NETBOX_ENABLE_WRITES,以额外注册 11 个破坏性工具(请参阅下文的写入权限)。 |
NETBOX_EVENT_API_PATH | 否 | 跟随 NETBOX_API_PATH | 仅供 trigger_event 使用的请求路径。未设置/为空时,会跟随 NETBOX_API_PATH 解析出的路径;设置为非空值时则原样使用(缺少开头的 / 时会自动添加)— 例如文档中 6.x 之前的 Event API 路径 /appd/nbapi,适用于控制器单独提供该路径的情况。 |
NETBOX_UNLOCK_HOLIDAY_GROUPS | 否 | 8,7,6 | 为受管理的解锁时段预留的假日组,按 first,middle,last 时段顺序排列 — 请参阅下文的计划解锁时段。必须是以逗号分隔的 1 至 3 个不同整数,且范围为 1..8;请预留控制器上没有其他用途的组。 |
NETBOX_UNLOCK_NAME_PREFIX | 否 | MCP Unlock Window | 受管理的解锁时段创建的每个对象的名称前缀:门户组(<prefix>)、时间规范组(<prefix> time specs)以及每个时段的假日/时间规范(<prefix> first/middle/last)。长度为 1-40 个字符,以确保最长名称(<prefix> time specs)符合 64 个字符的 NAME 限制。 |
NETBOX_DAILY_UNLOCK_HOLIDAY_GROUP | 否 | 5 | 为受管理的每日循环解锁时段预留的单个假日组 — 请参阅下文的计划每日解锁时段。必须是 1..8 范围内的单个整数,且不能属于 NETBOX_UNLOCK_HOLIDAY_GROUPS(这两个功能预留的组绝不会冲突)。 |
NETBOX_DAILY_UNLOCK_NAME_PREFIX | 否 | MCP Daily Unlock Window | 受管理的每日解锁时段创建的每个对象的名称前缀:门户组(<prefix>)、时间规范组(<prefix> time specs)以及单个假日/时间规范(<prefix> schedule)。长度为 1-40 个字符,以确保最长名称符合 64 个字符的 NAME 限制。 |
NETBOX_LIVE_TEST_PORTALKEY | 否 | — | 在 npm run test:live:write/npm run test:live:write:daily 期间,指定一扇可以安全进行物理解锁的门所对应的 PORTALKEY。只有这些脚本会读取此值,服务本身不会读取。 |
| 如果缺少三个必需变量中的任何一个,服务会向 stderr 输出一行可执行的提示,然后以非零状态退出——启动配置错误时绝不会输出堆栈跟踪。 |
默认只读
未设置任何与写入相关的环境变量时,此服务只注册 NBAPI 查询/读取命令:
LoginLogoutGetAPIVersionGetPersonSearchPersonDataGetCardAccessDetailsGetCardFormatsGetAccessLevel(s)GetAccessLevelGroup(s)GetPortalsGetReader(s)GetOutputsGetTimeSpec(s)GetTimeSpecGroup(s)GetHoliday(s)GetPortalGroup(s)GetReaderGroup(s)GetAccessLevelNamesGetPartitionsGetUDFListsGetUDFListItemsGetElevatorsGetFloorsPingAppGetEventHistoryListEventsGetAccessHistory
默认情况下,此服务只注册上面列出的查询/读取命令。在你通过下方写入权限中介绍的环境变量明确选择启用之前,它不会注册任何针对控制器的写入、删除或控制操作。在只读工具中,有四个复合工具:find_portals、get_unlock_window、get_daily_unlock_window 和 get_reader_access_history,它们只会发出读取命令。请注意,没有 GetPortal(单数)命令;真实的 NBAPI 只有 GetPortals(复数、分页、不支持筛选单个门户)。
写入权限
此服务包含写入/控制工具,但除非你明确选择启用,否则不会注册这些工具:
NETBOX_ENABLE_WRITES=true会注册下方“写入工具”表中列出的写入工具——45 个直通工具(用于创建、修改、锁定/解锁、激活和触发),以及三个复合写入工具set_portals_state、schedule_unlock_window和cancel_unlock_window。不设置此变量(默认情况)时,服务提供的工具范围与下方的只读工具完全一致——与该变量出现之前的只读状态逐字节相同。- 两个解锁窗口复合工具只会删除它们自己创建的节假日和时间规格(名称为
<prefix> first|middle|last——参见计划解锁窗口),且由于这些对象归服务所有,因此无需设置NETBOX_ENABLE_DESTRUCTIVE;它们绝不会删除其他任何内容。 NETBOX_ENABLE_DESTRUCTIVE=true,在设置NETBOX_ENABLE_WRITES的基础上再设置此变量,会注册 11 个破坏性工具(每个工具的描述都以DESTRUCTIVE:开头):delete_access_level、delete_access_level_group、delete_holiday、delete_portal_group、delete_reader_group、delete_time_spec、delete_time_spec_group、remove_credential、remove_person、remove_threat_level、remove_threat_level_group。无论工具本身是否已注册,另外还有两个通常不具破坏性的写入工具,在此标志关闭时也会分别拒绝一种特定的破坏性调用:modify_person会拒绝DELETED="TRUE"或PERSONPURGE="TRUE"的调用;modify_udf_list_items会拒绝任何列表项包含DELETE="1"的调用——两者都会在错误信息中提及NETBOX_ENABLE_DESTRUCTIVE,且不会向控制器发送任何内容。- 每个写入工具的描述都以
WRITE:开头(上述 11 个工具除外,它们以DESTRUCTIVE:开头);每次成功写入的结果文本都包含字面字符串SUCCESS,后接控制器响应数据的美化 JSON(命令无数据返回时可能为{}),因此你始终可以确认写入操作确实已执行。 - 客户端侧保护措施(例如“提供
READERKEY或READERGROUPKEY之一,但不能同时提供两者”)会在发出任何 NBAPI 命令之前,通过工具错误拒绝格式错误的调用——绝不会将不完整或推测得出的请求发送到控制器。
按与其他变量相同的方式设置这些变量——在 .env 中(参见 .env.example),或在 MCP 服务配置的 env 块中。
Agent 指引
此服务设置了 MCP instructions 字段,并始终注册 get_guide
工具,因此,任何连接到它的 Agent——无论是通过 npm 安装还是本地克隆,在 Claude Code、Antigravity、Gemini CLI 或任何其他 MCP 客户端中——都能立即获取此服务自身的 S2 NetBox 操作知识,无需单独安装技能,也无需额外步骤。
instructions 会说明访问模型(人员 → 凭据 → 访问级别 → 访问级别组决定人员可以访问什么;门户组 / 时间规范组决定何处以及何时可以访问),指出大多数参数都是数字型 KEY 字段而非名称(先使用匹配的 get_*/find_* 工具,将名称解析为其 KEY),并说明从控制器返回的文本是数据,不是需要遵循的指令。设置 NETBOX_ENABLE_WRITES 后,它还会明确规定,在执行锁定/解锁、门户状态、解锁时段和破坏性调用前必须先确认(下文的 write-safety 主题会详细说明);如果同时设置了 NETBOX_ENABLE_DESTRUCTIVE,还会增加一句话,列出带有 DESTRUCTIVE: 前缀的工具及其额外确认要求。
get_guide(始终注册;不会调用控制器)会返回涵盖六个主题的深入参考资料。不带参数调用时,它会列出全部六个主题,并为每个主题提供一行摘要;也可以将 topic 设为下列某个键,以获取该主题的完整内容:
| 主题键 | 涵盖内容 |
|---|---|
access-model | 人员 → 凭据 → 访问级别 → 访问级别组这一链路,以及门户组 / 时间规范组的名称表冲突。 |
unlock-windows | 节假日 + 时间规范 + 门户组 UNLOCKTIMESPECGROUPKEY 配置方法、日期/时间的包含规则,以及应优先使用组合工具的情况。 |
group-and-name-gotchas | modify_portal_group/modify_reader_group 会替换组的全部成员;modify_access_level 始终需要 TIMESPECGROUPKEY。 |
credentials-and-card-formats | 如何诊断 BIT MISMATCH 访问拒绝事件,以及 remove_person 的软删除行为。 |
api-quirks | STARTFROMKEY/NEXTKEY 分页、缺少单数形式的 get_portal,以及检查写入结果中是否包含字面值 SUCCESS。 |
write-safety | 说明目标和效果、等待明确确认、优先使用计划工具,以及撤销每一项写入。 |
get_guide 计入下文始终注册的只读工具:当两个开关都关闭时,工具面为 51 个工具;设置 NETBOX_ENABLE_WRITES 后为 98 个;再设置 NETBOX_ENABLE_DESTRUCTIVE 后为 113 个。
工作流技能
instructions 和 get_guide 会说明各个工具的行为。可选的 s2-netbox-workflows Agent 技能位于
plugins/s2-netbox-workflows/skills/s2-netbox-workflows/,提供了处理需要多次调用工具的任务的分步操作指南:
| 工作流 | 功能 | 会写入数据? |
|---|---|---|
| 入职 | 检查是否存在重复人员,创建人员并设置访问级别(块语法),发放卡片,然后进行验证。 | 是 |
| 离职 | 禁用所有卡片并移除访问级别,以便撤销操作。随后检查访问历史记录。只有在收到明确请求时才删除卡片或人员。 | 是 |
| 活动解锁 | 使用 find_portals 查找门,检查是否已有受管理的解锁时段,执行试运行,然后调用 schedule_unlock_window / schedule_daily_unlock_window。使用相应的取消工具撤销操作。 | 是 |
| 门禁卡被拒 | 查找拒绝记录,然后根据拒绝原因检查卡片格式、卡片状态、访问级别、读卡器组、时间表或门状态。 | 否 |
该技能会链接到 get_guide 主题,而不是重复其内容,并遵循相同的写入安全规则:说明目标和影响,等待明确同意,检查是否返回 SUCCESS,并说明如何撤销操作。它不包含任何特定站点的门禁代码或密钥。必须单独注册此 MCP 服务(参见在 MCP 客户端中注册),其写入步骤还需要启用写入权限中的写入关卡。
如需安装,请参阅安装工作流技能。
暴露的工具
读取工具(始终注册)
| 工具 | 封装的 NBAPI 命令 | 必需参数 |
|---|---|---|
check_connection | GetAPIVersion | — |
get_guide | —(纯进程内查询;不调用控制器) | —(可选 topic) |
get_person | GetPerson | PERSONID |
search_person_data | SearchPersonData | —(所有筛选条件均可选) |
get_card_access_details | GetCardAccessDetails | ENCODEDNUM、CARDFORMAT(可选 MAXRECORDS/OLDESTDTTM/RESOLVENAMES/RESOLVEDESCRIPTIONS) |
get_card_formats | GetCardFormats | — |
get_access_level | GetAccessLevel | ACCESSLEVELKEY(可选 RESOLVEGROUPNAMES) |
get_access_levels | GetAccessLevels | —(可选 PARTITIONKEY/STARTFROMKEY/STARTFROMNAME/WANTKEY) |
get_access_level_group | GetAccessLevelGroup | ACCESSLEVELGROUPKEY |
get_access_level_groups | GetAccessLevelGroups | —(可选 STARTFROMKEY/PARTITIONKEY) |
get_access_level_names | GetAccessLevelNames | —(可选 PARTITIONKEY/STARTFROMNAME) |
get_portals | GetPortals | —(可选 STARTFROMKEY/RESOLVEDESCRIPTIONS;不支持筛选单个门,仅返回每个门及其嵌套读卡器) |
get_reader | GetReader | READERKEY |
get_readers | GetReaders | —(可选 STARTFROMKEY;不支持按门 ID 筛选) |
get_outputs | GetOutputs | —(可选 STARTFROMKEY) |
find_portals | GetPortals + GetReaders(组合调用) | query(搜索词) |
get_event_history | GetEventHistory | —(可选 EVENTNAME/STARTDTTM/ENDDTTM/NEXTKEY) |
list_events | ListEvents | —(可选 RESOLVEPARTITIONNAMES,默认值为 true) |
get_access_history | GetAccessHistory | —(可选 STARTLOGID/AFTERLOGID/ORDER/MAXRECORDS/ENCODEDNUM/HOTSTAMP/CARDFORMAT/RESOLVENAMES/RESOLVEDESCRIPTIONS) |
get_reader_access_history | GetAccessHistory + GetPerson + GetReaders(组合调用) | READERKEY(可选 SCANWINDOW/MAXMATCHES/RESOLVEDESCRIPTIONS) |
get_time_spec | GetTimeSpec | TIMESPECKEY |
get_time_specs | GetTimeSpecs | —(可选 STARTFROMKEY) |
get_time_spec_group | GetTimeSpecGroup | TIMESPECGROUPKEY |
get_time_spec_groups | GetTimeSpecGroups | —(可选 STARTFROMKEY/RESOLVEMEMBERNAMES) |
get_holiday | GetHoliday | HOLIDAYKEY |
get_holidays | GetHolidays | —(无调用参数) |
get_portal_group | GetPortalGroup | PORTALGROUPKEY(可选 RESOLVEGROUPNAMES) |
get_portal_groups | GetPortalGroups | —(可选 STARTFROMKEY/RESOLVEGROUPNAMES) |
get_reader_group | GetReaderGroup | READERGROUPKEY |
get_reader_groups | GetReaderGroups | —(可选 STARTFROMKEY) |
get_partitions | GetPartitions | — |
get_udf_lists | GetUDFLists | — |
get_udf_list_items | GetUDFListItems | UDFLISTKEY |
get_elevators | GetElevators | —(可选 STARTFROMKEY) |
get_floors | GetFloors | —(可选 STARTFROMKEY) |
ping_app | PingApp | — |
get_threat_levels | GetThreatLevels | —(可选 ALLPARTITIONS) |
get_unlock_window | GetPortalGroups + GetPortalGroup + GetTimeSpecGroups + GetTimeSpecs + GetHolidays + GetHoliday(组合调用) | — |
get_daily_unlock_window | GetPortalGroups + GetPortalGroup + GetTimeSpecGroups + GetTimeSpecs + GetHolidays(组合调用) | — |
get_portal_states | GetPortalStates | —(可选 PORTALSTATES) |
get_portal_statuses | GetPortalStatuses | —(可选 ALLPARTITIONS/PORTALKEY/STATEKEY/PARTITIONKEY/LOCATIONKEY;表示每扇门的实时状态,而非其配置) |
get_locations | GetLocations | —(可选 ALLPARTITIONS/STARTFROMKEY) |
get_alarms | GetAlarms | —(可选 ALLPARTITIONS/PARTITIONKEY/ID/EVENTID/ACTIVITYID/OWNERID) |
get_picture | GetPicture | PERSONID(在 PICTURE 中返回 Base64 JPEG;可能较大) |
get_virtual_credential_request | GetVirtualCredentialRequest | PERSONID、CARDFORMAT |
get_mercury_panels | GetMercuryPanels | —(可选 ALLPARTITIONS/PARTITIONKEY/MERCURYKEY/NAME) |
get_mercury_panel | GetMercuryPanel | MERCURYKEY |
get_network_nodes | GetNetworkNodes | —(可选 ALLPARTITIONS/PARTITIONKEY/NODEKEY/UNIQUEIDENTIFIER/NAME) |
get_network_node | GetNetworkNode | NODEKEY(可选 PARTITIONKEY) |
get_sios | GetSios | MERCURYKEY(不存在不带筛选条件的 SIO 列表) |
get_sio | GetSio | SIOKEY |
有意不提供 get_portal(单数)工具——NBAPI 中不存在这样的命令;只有 GetPortals(复数)。get_card_access_details 和 get_access_history 通过 ENCODEDNUM/CARDFORMAT 标识卡片(get_access_history 还可选用 HOTSTAMP),而不是通过 PERSONID——这两个命令都没有 PERSONID 参数。 |
除 find_portals 外,每个读取工具都会原样透传对应 NBAPI 命令响应字段,返回精简的 JSON——不做重新整理。每个工具的输入架构仅声明其命令文档中列出的 PARAMS 字段——不包含虚构、重命名或透传的字段。上文所有 NBAPI 参数名称均直接照录自 NBAPI Command Reference(参见 specs/archive/s2-netbox-mcp-write.md 和归档的 specs/archive/s2-netbox-mcp.md)——没有任何虚构或猜测的名称。
其中 9 个工具是组合工具——它们会组合多个 NBAPI 命令并重新整理结果,而非透传单个命令:find_portals、get_unlock_window、get_daily_unlock_window 和 get_reader_access_history(只读,始终注册),以及 set_portals_state、schedule_unlock_window、cancel_unlock_window、schedule_daily_unlock_window 和 cancel_daily_unlock_window(写入操作,仅在启用 NETBOX_ENABLE_WRITES 时注册)。每个组合工具都会完整分页读取列表命令(持续跟进 NEXTKEY,直到值为 -1;对于 get_reader_access_history,则在有界的 SCANWINDOW 内使用 AFTERLOGID/NEXTLOGID),且只调用封闭许可列表中的命令。set_portals_state 可在一次调用中锁定、解锁(Extended Unlock 状态会持续到再次锁定)或短暂解锁多个门户——指定的 portalKeys 或所有门户——按顺序为每个门户调用一条命令,单次失败不会导致操作停止;结果会将门户分别列入 succeeded、alreadyInState(控制器返回“Portal state not changed”)和 failed,仅当 failed 不为空时才返回错误。5 个解锁时间窗工具的说明见下文的 计划解锁时间窗 和 计划每日解锁时间窗。
find_portals 用于在只知道门的位置时查找门。门户名称是站点代码(如 01OF05A),控制器上唯一可读的人类可读位置文本是各读卡器的 DESCRIPTION。GetPortals 不会返回该字段,而且这两个命令都不接受筛选条件。因此,find_portals 会读取 GetPortals 和 GetReaders 的所有页面,按 READERKEY 将它们关联起来,并返回 query 中每个词都出现在门户名称、读卡器名称或读卡器描述中的门户(不区分大小写)。例如,"maintenance office" 会匹配描述为 BREAKROOM TO MAINTENANCE OFFICE 的读卡器。每个匹配结果都包含其读卡器的名称和描述。结果还会列出 portalsWithoutDescriptions:其读卡器均没有描述的门户,这类门户只能通过名称查找。该工具不会调用这两个命令以外的命令。
get_portals 本身也接受 RESOLVEDESCRIPTIONS(默认值为 true——默认启用,与此代码库中其他所有 RESOLVEDESCRIPTIONS 标志的默认选择退出行为相同):除非显式设为 false,否则它会填充每个嵌套读卡器自身的 DESCRIPTION 字段——GetPortals 永远不会填充该字段,只会返回 READERKEY/NAME/PORTALORDER——每次调用都会通过一次 GetReaders 全表读取来完成(而非每个门户或读卡器各调用一次),并使用与其他 RESOLVEDESCRIPTIONS 工具相同的 src/readerDescriptions.ts helper。与那些会为扁平记录添加新的同级字段(READERDESCRIPTION)的工具不同,此处会直接填充每个嵌套读卡器对象的 DESCRIPTION,因为这是该读卡器在 GetReaders 中原生使用的字段名。将 RESOLVEDESCRIPTIONS: false 设为 false,即可按 GetPortals 返回的原样获取读卡器数据,不调用 GetReaders。这样,普通的 get_portals 列表就包含了自描述信息;但它不能替代 find_portals,后者仍用于按名称或描述进行搜索,而不只是列出门户。
get_reader_access_history 用于查明哪些人实际刷卡经过指定读卡器。例如,当读卡器没有 DESCRIPTION,且 find_portals 无法按名称找到它时,这项工具就很有用。GetAccessHistory 没有 READERKEY/PORTALKEY 筛选条件,因此这项工具会在客户端读取并筛选记录。它不使用日期范围(实际测试证明日期范围行不通——参见 specs/archive/get-reader-access-history.md 的 Goal 部分),而是扫描系统范围内最近的固定数量 SCANWINDOW 条记录(默认为 2000 条):先通过一次开销很低的 MAXRECORDS: '1' 调用获取当前最大的 LOGID,然后这项工具从 maxLogid - SCANWINDOW 开始,通过自己的 AFTERLOGID/NEXTLOGID 分页循环(与 NEXTKEY 的分页方式不同)向前遍历,只保留 READERKEY 匹配的记录。对于每位不同人员,工具会通过一次 GetPerson 调用为匹配记录中的 PERSONID 补充 FIRSTNAME/LASTNAME(例如,当 PERSONID 属于操作员类型而导致查询失败时,不会导致整个调用失败,而是将这两个字段留空)。结果数量上限为 MAXMATCHES(默认为 100 条,最早的匹配记录优先),并带有 truncated 标志。get_reader_access_history 还接受 RESOLVEDESCRIPTIONS(默认为 true,即默认开启;它是此代码库中唯一一个默认开启的可选布尔标志,其他所有可选布尔标志均默认关闭)。除非明确设为 false,否则它会通过一次 GetReaders 全表查询,在结果顶层附加单个 READERDESCRIPTION 字段,显示当前调用所用 READERKEY 对应的易读描述。由于每条 matches 记录按设计都具有相同的 READERKEY,因此该字段不会重复附加到每条记录上。设置 RESOLVEDESCRIPTIONS: false 可完全省略该字段(字段不会出现,这一点可与未知读卡器的 '' 区分开来),并跳过 GetReaders 调用。
get_access_history 可通过 RESOLVENAMES: true(默认为 false)为每条返回记录补充刷卡人员的姓名:启用后,它会针对结果中每个不同的 PERSONID 调用一次 GetPerson(与 get_reader_access_history 一样,使用共享的 src/personEnrichment.ts 辅助工具进行单次请求内的记忆化,不使用跨请求缓存),并为每条记录添加 FIRSTNAME/LASTNAME/FULLNAME/NOTES,同时保留所有原始字段。此操作会针对结果中每位不同人员额外调用一次 GetPerson,因此它采用可选启用,而非默认开启。
get_access_history 不支持日期范围筛选:此前的日期范围参数已全部移除,从而解决了 #47——这些参数与 GetAccessHistory 实际使用的 NBAPI 字段名称不匹配,而且本次会话中的实时受控 A/B 测试发现,即使字段名称正确,也无法生效:控制器会静默忽略这些字段,无论请求的范围如何,返回的记录都相同;没有报错,只是没有任何效果。如果只改名,只会把明显报错变成静默返回错误结果,因此我们选择弃用日期范围筛选,而不是修复它——理由与上文 get_reader_access_history 的说明相同。
get_access_history 和 get_card_access_details 也都接受
RESOLVEDESCRIPTIONS(默认值为 true——默认启用;与上述
get_reader_access_history 自身的 RESOLVEDESCRIPTIONS 一样,默认可选
关闭,与默认关闭的 RESOLVENAMES 不同):除非显式设为 false,否则
每条返回记录都会在现有的 READER(或 get_card_access_details 的
PORTALNAME)代码旁,附加读卡器的易读 READERDESCRIPTION,并保留
其他所有字段。两个工具都使用 get_reader_access_history 所用的同一个
src/readerDescriptions.ts 辅助模块。该选项默认启用而非关闭,是因为
与人员姓名补充不同,底层的 GetReaders 获取操作成本固定——无论返回
多少条结果记录,此控制器的完整读卡器表(68 个读卡器)都只需恰好 2 次
分页调用,因此无需让调用方选择启用,也不会产生随规模增长的成本。设置
RESOLVEDESCRIPTIONS: false 可跳过 GetReaders 调用,并获取普通的
(未补充信息的)响应。在 get_access_history 和
get_card_access_details 中,RESOLVENAMES 和 RESOLVEDESCRIPTIONS 是
相互独立的标志——同一次调用中可以请求其中任意一个、两者都请求,或都不请求。
get_card_access_details 也接受自身的 RESOLVENAMES: true(默认值为
false),通过 get_access_history 使用的同一个共享
src/personEnrichment.ts 辅助模块,为响应补充持卡人的
FIRSTNAME/LASTNAME/FULLNAME/NOTES。与响应中可能包含多个不同
PERSONID(每条记录一个)的 get_access_history 不同,
GetCardAccessDetails 的响应在顶层恰好包含一个 PERSONID——一张卡只
属于一个人——因此每次工具调用只需调用一次 GetPerson,而非为每个不同
的人分别调用一次。这四个补充字段位于响应的顶层,与
PERSONID/DISABLED/EXPDATE 并列,而不是重复添加到每条 ACCESS
记录中。
get_access_level 接受 RESOLVEGROUPNAMES(默认值为 true——默认
启用,与上述其他 RESOLVE* 标志一样,默认可选关闭;因为每次调用的
GetAccessLevel 恰好包含一个 TIMESPECGROUPKEY 和一个
READERGROUPKEY,所以解析两者始终只需固定大小的
GetTimeSpecGroups 和 GetReaderGroups 获取操作各一次,成本不会随任何
因素变化):除非显式设为 false,否则它会将响应中单独存在的
TIMESPECGROUPKEY/READERGROUPKEY 外键解析为新的同级字段
TIMESPECGROUPNAME/READERGROUPNAME,并使用新的
src/timeSpecGroupNames.ts/src/readerGroupNames.ts 辅助模块。
TIMESPECGROUPKEY 通过完整的分页 GetTimeSpecGroups 列表解析,并在
客户端筛选匹配的键——绝不使用单独的 GetTimeSpecGroup 命令;在此
控制器上已验证该命令存在故障:即使组确实存在,它也会返回
CODE=FAIL/ERRMSG="NOT FOUND"(src/unlockWindow/managed.ts 中已记录
相同的发现)。为保持一致,READERGROUPKEY 也通过完整的分页
GetReaderGroups 列表以相同方式解析。任一轴上的键为空或不存在时,
都会独立跳过该轴的获取操作,并仅为该轴的名称返回 '',不影响另一轴。
THREATLEVELGROUPKEY 绝不解析,保持原样——此服务的命令范围内根本
不存在用于威胁等级组的 NBAPI 读取命令。如果底层
GetTimeSpecGroups/GetReaderGroups 获取操作本身失败,该轴的名称会解析
为 '',但调用仍会成功,并保留完整的主要 GetAccessLevel 数据——补充
信息失败绝不会导致主要数据丢失。设置 RESOLVEGROUPNAMES: false 可跳过
这两项获取操作,并原样返回 GetAccessLevel 提供的响应。
get_portal_group 接受相同的 RESOLVEGROUPNAMES 标志(默认值为
true,与 get_access_level 上述 RESOLVEGROUPNAMES 的默认选择退出
方式和查询方式相同):除非显式设为 false,否则会将响应中原始的
UNLOCKTIMESPECGROUPKEY 外键解析为新的同级字段
UNLOCKTIMESPECGROUPNAME,并复用相同的 src/timeSpecGroupNames.ts
辅助程序(因此会使用相同的全分页列表解析方式,绝不会调用有缺陷的单数
GetTimeSpecGroup 命令)。单个 GetPortalGroup 响应恰好包含一个
UNLOCKTIMESPECGROUPKEY,因此始终只需进行一次固定开销的
GetTimeSpecGroups 获取,不会随任何因素增加。已经是人类可读形式的
PORTALS 子列表(每个 portal 包含 {PORTALKEY, NAME})完全保持不变。
绝不会解析 THREATLEVELGROUPKEY——此服务的命令接口中不存在用于读取
威胁级别组的 NBAPI 命令。若 UNLOCKTIMESPECGROUPKEY 为空或不存在,则
完全跳过获取,并将名称设为 '';如果底层的 GetTimeSpecGroups 获取本身
失败,则 UNLOCKTIMESPECGROUPNAME 会解析为 '',调用仍会成功,且主要的
GetPortalGroup 数据(包括 PORTALS)保持完整。设置
RESOLVEGROUPNAMES: false 可跳过获取,并按 GetPortalGroup 返回的原样
获取响应。
get_portal_groups 接受相同的 RESOLVEGROUPNAMES 标志(默认值为
true,与上述单数 get_portal_group 的默认选择退出方式和字段名相同;
这是按计划后续添加的功能):除非显式设为 false,否则会将每个返回组的原始
UNLOCKTIMESPECGROUPKEY 外键解析为新的同级字段
UNLOCKTIMESPECGROUPNAME,并复用相同的 src/timeSpecGroupNames.ts
辅助程序。与单数工具(其响应恰好包含一个 UNLOCKTIMESPECGROUPKEY,因此
最多进行一次有条件的获取)不同,这个复数工具会在每次调用中只构建一次
fetchTimeSpecGroupNames 映射——仅当页面上至少有一个组包含非空的
UNLOCKTIMESPECGROUPKEY 时才会构建(如果页面上所有组的键都为空,则不会调用
GetTimeSpecGroups)——然后使用同一个共享映射查找每个组,开销与上述
get_time_spec_groups 自身的 RESOLVEMEMBERNAMES 相同:每页最多获取一次,
绝不会为每个组分别获取。与单数的 GetPortalGroup 不同,
GetPortalGroups 的响应中每个条目本身就是扁平结构:
DETAILS.PORTALGROUPS.PORTALGROUP[],没有逐项的 PORTALGROUP 包装层,
因此不会对每个条目进行解包;这种包装层差异仅存在于单数命令自身的响应封装中。
每个组中已经是人类可读形式的 PORTALS 子列表(每个 portal 包含
{PORTALKEY, NAME})都完全保持不变。
绝不会解析 THREATLEVELGROUPKEY——此服务的命令接口中不存在用于读取
威胁级别组的 NBAPI 命令。若某个组的 UNLOCKTIMESPECGROUPKEY 为空或不存在,
则无需匹配即可将该组的 UNLOCKTIMESPECGROUPNAME 设为 '';若某个组的键在
获取的映射中没有匹配项,也会设为 ''。如果底层的 GetTimeSpecGroups 获取
本身失败,则每个组的 UNLOCKTIMESPECGROUPNAME 都会解析为 '',调用仍会
成功,且每个组的其他字段(包括 PORTALS)保持完整——扩充数据失败绝不会
导致主要数据丢失。设置 RESOLVEGROUPNAMES: false 可跳过获取,并按
GetPortalGroups 返回的原样获取各组。
list_events 接受 RESOLVEPARTITIONNAMES(默认值为 true——默认启用,与上文其他 RESOLVE* 标志采用相同的选择退出默认设置):除非显式设为 false,否则每个返回事件中原始的 PARTITIONID 都会通过每次调用一次 GetPartitions 获取结果,解析为新增的同级字段 PARTITIONNAME——而非每个事件调用一次,因为 GetPartitions 完全不接受 STARTFROMKEY,并且总是在单个响应中返回所有分区,因此开销不会随着返回的事件数量增加。此功能由新增的 src/partitionNames.ts 辅助模块实现,其结构与 src/readerDescriptions.ts 完全一致(返回 Map 的获取函数,且绝不抛出异常)。如果某个事件的 PARTITIONID 在获取的映射中没有匹配项,其 PARTITIONNAME 会解析为 '';如果底层 GetPartitions 获取本身失败,则所有事件的 PARTITIONNAME 都会解析为 '',且调用仍会成功,其他所有字段(包括 ACTIONS)均保持完整——增强数据失败不会导致主要数据丢失。设置 RESOLVEPARTITIONNAMES: false 可跳过 GetPartitions 调用,并使响应保持 ListEvents 提供的原样。
get_time_spec_groups 接受 RESOLVEMEMBERNAMES(默认值为 true——默认启用,与上文其他 RESOLVE* 标志采用相同的选择退出默认设置;因为解析一页中所有组成员的成本固定为一次 GetTimeSpecs 获取,不会随页面中的组或成员数量增加):除非显式设为 false,否则每个组的 TIMESPECKEYS.TIMESPECKEY 字段——GetTimeSpecGroups 返回该字段时是原始的 TIMESPECKEY 字符串——会替换为 {TIMESPECKEY, NAME} 对象列表,符合此代码库对其他组成员子列表的惯例;NBAPI 原生返回的这类子列表本身就是对象(如 get_access_level_group 的 ACCESSLEVELS、get_reader_group 的 READERS)。如果某个成员键在获取的 GetTimeSpecs 表中没有匹配项(对应未知或已删除的时间规格),其 NAME 会解析为 '',而不会被省略。其他所有字段(TIMESPECGROUPKEY、组自身的 NAME、DESCRIPTION)均保持不变。名称查询使用新增的 src/timeSpecNames.ts 辅助模块——每次调用都会完整分页获取一次 GetTimeSpecs,无论页面中有多少组或成员——并复用 keyList(src/paging.ts,从 src/unlockWindow/managed.ts 移出),以规范化原始键集合。如果底层 GetTimeSpecs 获取本身失败,则所有成员的 NAME 都会解析为 '',且调用仍会成功,所有组自身的字段均保持完整——增强数据失败不会导致主要数据丢失。RESOLVEMEMBERNAMES 仅适用于这个复数工具,不适用于单数工具 get_time_spec_group;在此控制器上,该单数工具已验证为不可用(即使组确实存在,也会返回 CODE=FAIL/ERRMSG="NOT FOUND"),且与此更改无关。设置 RESOLVEMEMBERNAMES: false 可跳过获取,并使 TIMESPECKEYS 保持 GetTimeSpecGroups 提供的原样(单个字符串或字符串数组)。
写入工具和破坏性工具
write(只需要 NETBOX_ENABLE_WRITES)和 destructive(需要 NETBOX_ENABLE_WRITES 和 NETBOX_ENABLE_DESTRUCTIVE)。每个写入工具的输入架构都为其命令声明了文档中规定的全部 PARAMS 字段,并与文档中标注的必填项和可选项一致——有关门控规则以及通用的 SUCCESS/WRITE:/DESTRUCTIVE: 约定,请参阅上文的“写入权限”部分。
| 工具 | 封装的 NBAPI 命令 | 必需参数 | 层级 |
|---|---|---|---|
lock_portal | LockPortal | PORTALKEY | 写入 |
unlock_portal | UnlockPortal | PORTALKEY | 写入 |
momentary_unlock_portal | MomentaryUnlockPortal | PORTALKEY | 写入 |
dog_on_next_exit_portal | DogOnNextExitPortal | PORTALKEY | 写入 |
activate_output | ActivateOutput | OUTPUTKEY | 写入 |
deactivate_output | DeactivateOutput | OUTPUTKEY | 写入 |
add_time_spec | AddTimeSpec | NAME | 写入 |
modify_time_spec | ModifyTimeSpec | TIMESPECKEY | 写入 |
add_time_spec_group | AddTimeSpecGroup | NAME | 写入 |
modify_time_spec_group | ModifyTimeSpecGroup | TIMESPECGROUPKEY | 写入 |
delete_time_spec | DeleteTimeSpec | TIMESPECKEY | 破坏性 |
delete_time_spec_group | DeleteTimeSpecGroup | TIMESPECGROUPKEY | 破坏性 |
add_holiday | AddHoliday | HOLIDAYNAME、STARTDATE、ENDDATE、HOLIDAYGROUPS | 写入 |
modify_holiday | ModifyHoliday | HOLIDAYKEY | 写入 |
delete_holiday | DeleteHoliday | HOLIDAYKEY | 破坏性 |
add_portal_group | AddPortalGroup | NAME、PORTALKEYS、UNLOCKTIMESPECGROUPKEY | 写入 |
modify_portal_group | ModifyPortalGroup | PORTALGROUPKEY、PORTALKEYS | 写入 |
delete_portal_group | DeletePortalGroup | PORTALGROUPKEY | 破坏性 |
add_reader_group | AddReaderGroup | NAME、READERKEYS | 写入 |
modify_reader_group | ModifyReaderGroup | READERGROUPKEY、READERKEYS | 写入 |
delete_reader_group | DeleteReaderGroup | READERGROUPKEY | 破坏性 |
add_access_level | AddAccessLevel | ACCESSLEVELNAME、TIMESPECGROUPKEY | 写入 |
modify_access_level | ModifyAccessLevel | ACCESSLEVELKEY、TIMESPECGROUPKEY | 写入 |
delete_access_level | DeleteAccessLevel | ACCESSLEVELKEY | 破坏性 |
add_access_level_group | AddAccessLevelGroup | NAME | 写入 |
modify_access_level_group | ModifyAccessLevelGroup | ACCESSLEVELGROUPKEY | 写入 |
delete_access_level_group | DeleteAccessLevelGroup | ACCESSLEVELGROUPKEY | 破坏性 |
add_person | AddPerson | LASTNAME | 写入 |
modify_person | ModifyPerson | PERSONID | 写入 |
remove_person | RemovePerson | PERSONID | 破坏性 |
add_credential | AddCredential | PERSONID、CARDFORMAT(加 ENCODEDNUM 或 HOTSTAMP) | 写入 |
modify_credential | ModifyCredential | PERSONID | 写入 |
remove_credential | RemoveCredential | PERSONID(加 CREDENTIALID 或 ENCODEDNUM/HOTSTAMP) | 破坏性 |
set_threat_level | SetThreatLevel | LEVELNAME(可选 LOCATIONKEYS) | 写入 |
add_threat_level | AddThreatLevel | LEVELNAME | 写入 |
modify_threat_level | ModifyThreatLevel | LEVELNAME、SEQNUM、COLOR | 写入 |
remove_threat_level | RemoveThreatLevel | LEVELNAME | 破坏性 |
add_threat_level_group | AddThreatLevelGroup | LEVELGROUPNAME | 写入 |
modify_threat_level_group | ModifyThreatLevelGroup | LEVELGROUPNAME、LEVELNAMES | 写入 |
remove_threat_level_group | RemoveThreatLevelGroup | LEVELGROUPNAME | 破坏性 |
trigger_event | TriggerEvent | EVENTNAME、EVENTACTION | 写入 |
insert_activity | InsertActivity | ACTIVITYTYPE | 写入 |
add_partition | AddPartition | NAME、TIMEZONE | 写入 |
switch_partition | SwitchPartition | PARTITIONKEY | 写入 |
modify_udf_list_items | ModifyUDFListItems | UDFLISTKEY、LISTITEMS | 写入 |
set_portals_state | 每个门户调用 LockPortal / UnlockPortal / MomentaryUnlockPortal,在 GetPortals 之后(组合工具) | action(portalKeys 可选;省略时 = 所有门户) | 写入 |
schedule_unlock_window | AddHoliday/ModifyHoliday、AddTimeSpec/ModifyTimeSpec、AddTimeSpecGroup/ModifyTimeSpecGroup、AddPortalGroup/ModifyPortalGroup,以及对剩余托管片段调用 DeleteTimeSpec/DeleteHoliday,并执行读取操作(组合工具) | start、end(portalKeys、acknowledgeSideEffects、dryRun 可选) | 写入 |
cancel_unlock_window | ModifyPortalGroup、DeleteHoliday、ModifyTimeSpecGroup、DeleteTimeSpec —— 仅限托管对象 —— 并执行读取操作(组合工具) | — | 写入 |
schedule_daily_unlock_window | AddHoliday/ModifyHoliday、AddTimeSpec/ModifyTimeSpec、AddTimeSpecGroup/ModifyTimeSpecGroup、AddPortalGroup/ModifyPortalGroup,并执行读取操作(组合工具) | startDate、endDate、dailyStartTime、dailyEndTime(portalKeys、acknowledgeSideEffects、dryRun 可选) | 写入 |
cancel_daily_unlock_window | ModifyPortalGroup、DeleteHoliday、ModifyTimeSpecGroup、DeleteTimeSpec —— 仅限托管对象 —— 并执行读取操作(组合工具) | — | 写入 |
add_duty_log | AddDutyLog | PERSONID、LOGTEXT(可选 ACTIVITYID/PARTITIONKEY) | 写入 |
add_virtual_credential_request | AddVirtualCredentialRequest | PERSONID、CARDFORMAT | 写入 |
remove_virtual_credential_request | RemoveVirtualCredentialRequest | PERSONID、CARDFORMAT | 破坏性 |
add_mercury_panel | AddMercuryPanel | NAME、TYPE、ENABLED、PARTITIONKEY、NETWORK(嵌套:IPADDRESS、TLSSECURE) | 写入 |
modify_mercury_panel | ModifyMercuryPanel | MERCURYKEY、NAME、ENABLED、NETWORK(嵌套:IPADDRESS、TLSSECURE) | 写入 |
delete_mercury_panel | DeleteMercuryPanel | MERCURYKEY | 破坏性 |
add_network_node | AddNetworkNode | NAME、TYPE、ENABLED、PARTITIONKEY、UNIQUEIDENTIFIER、DHCPENABLED | 写入 |
modify_network_node | ModifyNetworkNode | NODEKEY | 写入 |
delete_network_node | DeleteNetworkNode | NODEKEY | 破坏性 |
add_sio | AddSio | MERCURYKEY、NAME、MODEL、CHANNEL、ADDRESS、REVINPUT | 写入 |
modify_sio | ModifySio | SIOKEY、NAME、REVINPUT | 写入 |
delete_sio | DeleteSio | SIOKEY | 破坏性 |
十二个硬件工具(*_mercury_panel、*_network_node、*_sio—— | |||
六个写入工具和三个破坏性删除工具,以及上表中的六个读取工具)未经实时验证:本项目开发所依据的参考控制器没有 Mercury 面板和 SIO,因此这些工具的读取操作会在 npm run test:live 中跳过并通过,而写入操作从未在真实硬件上执行过。它们仅根据 2025 年 4 月版 NBAPI v2 指南构建——请参阅 src/tools/hardware.ts 中的头部注释,以及 | |||
docs/reference/nbapi-command-diff.md。 |
modify_portal_group 和 modify_reader_group 始终会用你发送的 PORTALKEYS/READERKEYS 替换组成员列表——在此控制器(6.2.0,已实时验证)上,如果省略列表或无法解析列表,组成员会被清空,而不是保持不变,因此这两个工具都要求提供完整的成员列表。
trigger_event 在 6.x 上未经实时验证;如果你的控制器将 Event API 与主 NBAPI 路径分开提供,可使用 NETBOX_EVENT_API_PATH 覆盖请求路径(请参阅上方的环境变量表)。
switch_partition 会更改此服务进程之后发出的每次调用所使用的分区,而不仅仅是调用方自己的下一次请求——NBAPI 会话会被缓存并重复使用,而 SwitchPartition 没有单次调用的作用范围。
人员/凭证工具与 Active Directory
如果此 NetBox 实例会从 Active
Directory 同步人员/访问级别数据,那么此服务对已同步字段所做的任何写入都会在下一次 AD 同步时被静默覆盖——add_person 和 modify_person 的工具描述中都包含此注意事项。另外,modify_person 的 ACCESSLEVELS 有两种语法:普通的访问级别名称字符串列表会替换该人员的整个访问级别集合,而 { ACCESSLEVELNAME, DELETE?, ACTDATE?, EXPDATE?, AUTOREMOVE? } 块的列表则是增量式的(添加/删除单个级别,不影响其余级别)。在同一次调用中混用这两种语法,会在客户端侧被拒绝,不会发送任何命令。
定时解锁时段
“在开始时间到结束时间之间解锁这些门”只需调用一次
schedule_unlock_window——控制器本身会执行此计划:无需任何进程持续运行来重新锁门,因此 MCP 主机可在调用返回后立即关闭。
工作原理(与操作员手动创建的对象相同)。 该时段由一个覆盖相应日期的 Holiday、一个不勾选任何星期几且只勾选一个 holiday group 的 Time Spec,以及一个将该 time spec 的 group 设为 Unlock Time Spec 的 Portal Group 实现。未勾选任何星期几但勾选了 holiday group G 的 time spec,只会在 group G 中某个 holiday 覆盖的日期生效,因此门户恰好会在该时段对应的日期和时间范围内解锁。跨午夜的时段最多会拆分为三个区段——first(开始时间 → 开始日期的 23:59)、middle(严格位于开始日期和结束日期之间的每一天,00:00 → 23:59;如有)以及 last(结束日期的 00:00 → 结束时间)——每个区段都配有各自的 holiday + time spec 对。
托管对象与单窗口模型。 该工具创建的所有对象都使用 NETBOX_UNLOCK_NAME_PREFIX 命名(默认值为 MCP Unlock Window):门户组的名称恰好是 <prefix>,时间规格组的名称为 <prefix> time specs(绝不能是 <prefix>——在此控制器上,组名在不同组类型之间全局唯一,因此门户组和时间规格组不能同名),每个时段对应的假日和时间规格分别命名为 <prefix> first、<prefix> middle、<prefix> last。名称就是身份标识。同一时间只能有一个托管窗口:安排新窗口时会重写这些相同的对象(修改已有对象、添加缺失对象、删除上一个窗口遗留的时段),使用相同参数调用两次是幂等的(只执行 Modify/Get 命令,键相同)。组合工具绝不会修改或删除名称不完全匹配上述名称的对象;如果用户创建的对象恰好使用了其中一个名称,就会被视为托管对象。应用顺序固定——解析门户、托管时间规格组、每个时段的假日 + 时间规格、组成员关系、删除遗留对象、托管门户组——并且每一步都会回读结果并与计划对比,之后工具才会报告 verified: true;任何不匹配都会导致工具报错并指出具体字段。如果任何应用步骤失败,工具会在返回错误前删除迄今写入的所有托管假日和时间规格以进行回滚(与 cancel_unlock_window 的清理逻辑一致),因此不会留下部分生效的窗口;错误文本会列出失败步骤、控制器的消息以及回滚删除的内容。
预留假日组。 NetBox 恰好有八个假日组(1–8),由控制器上的所有时间规格共享。NETBOX_UNLOCK_HOLIDAY_GROUPS(默认值为 8,7,6)会为每种时段类型(依次为 first、middle、last)预留一个组。预留的组不能用于控制器上的其他用途。如果配置的组少于三个,则只能安排所需时段数不超过组数的窗口(一个组 = 仅限同日窗口);工具绝不会让多个时段共用一个组,因为这样一来,每个时段都会在另一个时段的日期解锁。
副作用检查与 acknowledgeSideEffects。 组 G 中的假日会在对应日期抑制所有未勾选 G 的时间规格——例如,仅勾选组 1–3 的访问级别,在使用组 8 的窗口期间就会失去访问权限。写入任何内容之前,schedule_unlock_window 会读取所有时间规格和假日,并报告 suppressedTimeSpecs(除 Never 和自身之外,缺少计划所用组的时间规格)以及 overlappingHolidays(日期与窗口重叠的非托管假日——只报告,绝不修改)。如果任何时间规格会受到抑制,除非设置 acknowledgeSideEffects=true,否则调用会被拒绝且不会写入任何内容。无论是否已确认,dryRun=true 都会返回计划和报告,但不会写入任何内容。内置的 Always 时间规格会勾选全部八个组,因此不会受到影响。
取消。 cancel_unlock_window 会先在托管的门户组存在时,将其指向内置的 Never 时间规范组(并重新发送该组当前的门户);然后,无论该门户组是否存在,都会删除托管的节假日、清空托管的时间规范组并删除托管的时间规范。最后两项为尽力而为:如果控制器拒绝执行,工具仍会成功,并通过 leftBehind 列出遗留内容,因为只要门户组(若存在)已指向 Never,且不存在托管节假日,就无法解锁。托管的门户组和时间规范组会保留(分别指向 Never / 保持为空),供下一个时间窗口重复使用。只有在任何类型的托管对象——门户组、时间规范组、节假日或时间规范——都不存在时,工具才会报告无内容可取消。
get_unlock_window(始终注册,只读)会显示当前托管状态——门户组及其是否指向托管的时间规范组、该组的成员(通过 GetTimeSpecGroups 读取,因为在经过验证的 6.2.0 控制器上,GetTimeSpecGroup 会返回 FAIL/NOT FOUND)、托管的时间规范和节假日——以及由这些内容推导出的时间窗口和基于主机时钟的 activeNow。
限制和注意事项。
- 时间窗口必须在未来结束,且最长为 31 天。每个分区最多可有 30 个节假日,因此,如果时间窗口的分段会导致超过此上限,操作将被拒绝。
portalKeys只能填写键(使用get_portals或find_portals查询名称);未知键会在写入任何内容前被拒绝。 - NBAPI 中一天的结束时间为
23:59(内置的Always使用此时间),且ENDTIME包含该分钟的最后时刻,因此多日时间窗口的各分段之间不会出现午夜空档。时间窗口的门最多会在指定end分钟后 59 秒重新上锁(实时观察结果:结束时间为08:27的时间窗口在控制器时间08:27:59时重新上锁)。end为00:00表示“截至前一天的23:59”。 - 时间采用控制器的本地时间。默认 MCP 主机与控制器位于同一时区;主机时钟仅用于拒绝已过期的时间窗口以及计算
activeNow。 - 通过 NBAPI 无法观测到实际解锁状态:没有读取命令能够获取门户状态,
GetEventHistory也不包含 Unlock/Relock 活动。工具会通过读回配置对象并与计划进行比较来验证操作;请在 Monitor → Portal Status 中或亲自确认门本身的状态。 set_portals_state是即时操作的替代方案:其UNLOCK是一种延长解锁,会持续到执行LOCK,且不会安排自动重新上锁。
会话处理、会话过期后的重试以及错误映射均为自动处理,并与 NBAPI 文档一致:
- 首次调用工具时会触发
Login;会话 ID 会被缓存,并在同一次服务运行期间的后续每次调用中重复使用。 - 如果调用因
APIERROR 5(身份验证失败 / 会话过期)而失败,客户端会透明地重新登录一次,并重试原始命令。 <APIERROR>响应会显示为工具错误,例如"5: There was an authentication failure."<CODE>FAIL</CODE>响应会显示为工具错误,其中包含控制器的ERRMSG原文。<CODE>NOT FOUND</CODE>响应(例如未知的PERSONID)会作为正常的非错误结果返回,并说明“未找到”——不会作为异常抛出。- 进程退出前,
SIGINT/SIGTERM会触发对所有活动会话执行Logout。
每日定时解锁时间窗口
上文 计划解锁时段 的补充功能:“每天从 dailyStartTime 到 dailyEndTime 解锁这些门,日期范围从 startDate 到 endDate”,即一个跨日期范围 每日重复 的部分日期时段,这是 schedule_unlock_window 无法表达的(它表示一个连续时段,因此多日请求会让门在首日和末日之间的日期整夜保持解锁)。schedule_daily_unlock_window 会在每日时段之外的夜间重新锁门。
工作原理(复用同一机制)。 此功能复用上文所述的精确机制:Holiday + Time Spec + Portal Group:一个覆盖整个日期范围的 Holiday,一个不设置星期几且只勾选一个专用每日节假日组的 Time Spec,以及一个 Unlock Time Spec 为该时间规格所属组的 Portal Group。由于不设置星期几、且勾选了节假日组 G 的时间规格,会在组 G 中某个节假日涵盖的每个日期的 STARTTIME-ENDTIME 时段生效,因此一个节假日(涵盖范围内的每个日期)搭配一个部分日期时间规格,就足以表达“范围内每天的同一时段”——完全不需要拆分首日/中间日/末日时段。而连续时段功能的规划器必须将多日时段拆分为最多三个时段,正是因为中间日需要完整的 00:00-23:59 授权。此功能的计划始终只有一个时段。
环境变量与冲突安全性。 NETBOX_DAILY_UNLOCK_HOLIDAY_GROUP(默认值为 5)是此功能专用的唯一节假日组,NETBOX_DAILY_UNLOCK_NAME_PREFIX(默认值为 MCP Daily Unlock Window)用于命名其托管对象——这两个变量均见上方的环境变量表。NETBOX_DAILY_UNLOCK_HOLIDAY_GROUP 会在启动时经过验证,确保它绝不会属于 NETBOX_UNLOCK_HOLIDAY_GROUPS,因此此功能与连续时段功能始终使用互不重叠的节假日组。此外,由于每个功能的托管 Portal Group 和 Time Spec 组都以各自的前缀命名,托管对象名称也互不重叠。因此,这两个功能可以同时启用:每日时段和连续时段可以同时排程并同时解锁门,彼此之间没有共用的 NBAPI 对象。这也意味着,schedule_daily_unlock_window 的副作用检查不会针对当前生效的连续时段做特殊处理——如果两者的日期恰好重叠,它会像处理其他外部时间规格/节假日一样报告;这是正确且通用的行为,而非特殊情况。
托管对象与单个每日时间窗口模型。 此工具创建的所有对象都使用 NETBOX_DAILY_UNLOCK_NAME_PREFIX 命名:门户组的名称恰好为 <prefix>,时间规格组的名称为 <prefix> time specs(绝不会是 <prefix>——在此控制器上,组名称在不同组类型之间也必须唯一,与连续功能相同),唯一的假日和唯一的时间规格的名称为 <prefix> schedule(名称相同,但属于不同的对象表,因此不会冲突)。名称就是身份标识。同一时间只管理一个每日时间窗口:安排新的窗口时会重写这些相同的对象(修改现有对象,添加缺少的对象);使用相同参数调用两次是幂等的(只执行 Modify/Get 命令,键相同)。工具绝不会修改或删除名称不完全匹配上述名称之一的对象;如果用户创建的对象恰好使用其中一个名称,就会被视为托管对象。应用顺序固定——解析门户、托管时间规格组、托管假日、托管时间规格、组成员关系、托管门户组——每一步都会回读并与计划进行比较,然后工具才会报告 verified: true;任何不匹配都会产生描述相应字段的工具错误。如果任何应用步骤失败,工具会在返回错误前回滚,删除此前已写入的托管假日和时间规格(与 cancel_daily_unlock_window 的清理方式一致),因此不会留下部分生效的窗口;错误文本会指出失败步骤、控制器返回的消息以及回滚删除的内容。
副作用检查与 acknowledgeSideEffects。 与连续功能完全相同:保留的每日组中的假日会在其日期内抑制所有未关联该组的时间规格。写入任何内容之前,schedule_daily_unlock_window 会读取每个时间规格和假日,并报告 suppressedTimeSpecs(不含 Never 和自身、且未关联保留的每日组的时间规格)以及 overlappingHolidays(日期与该窗口重叠的非托管假日——仅报告,绝不触碰)。如果任何时间规格会受到抑制,除非设置 acknowledgeSideEffects=true,否则调用会被拒绝且不会写入任何内容。无论是否确认,dryRun=true 都会返回计划和报告,但不会写入任何内容。
取消。 cancel_daily_unlock_window 会先在托管门户组存在的情况下,将其指向内置的 Never 时间规格组(重新发送其当前门户);然后,无论该门户组是否存在,只要托管假日存在就将其删除;如果托管时间规格组或时间规格任一存在,则清空托管时间规格组并删除托管时间规格。最后两项操作会尽力执行:如果控制器拒绝,工具仍会成功,并在 leftBehind 下列出遗留对象,因为只要门户组(如果存在)已指向 Never,且不存在托管假日,就不会发生解锁。托管门户组和时间规格组会保留(分别指向 Never 和保持为空),供下一个每日时间窗口复用。只有在不存在任何类型的托管对象——门户组、时间规格组、假日或时间规格——时,工具才会报告没有内容可取消。
get_daily_unlock_window(始终注册,只读)会显示当前托管状态——门户组及其是否指向托管时间规格组、该组的成员、唯一的托管时间规格和假日(均为单个对象而非数组——此功能的每种对象都不会超过一个)——以及据此推导出的时间窗口和基于主机时钟的 activeNow。
限制与注意事项。
- 时间窗口的结束时间必须在未来,且跨度最多为 31 天
(
endDate - startDate)。每个分区最多允许 30 个节假日,因此,如果某个节假日会导致窗口超出此上限,则请求会被拒绝。portalKeys只能填写键(使用get_portals/find_portals将名称映射为键);未知键会在写入任何内容之前被拒绝。 - 不支持跨夜的每日时间窗口:
dailyEndTime必须严格晚于dailyStartTime(仅限同一天内的时刻)。类似“每晚从晚上 10 点到凌晨 5 点”的请求会被拒绝——未来可以扩展为两个时间段来实现,但不在当前范围内。 - 不支持按星期几筛选:整个
[startDate, endDate]日期范围内的每一天,都会在指定的每日时刻解锁(不提供“仅限工作日”筛选)。 - 日期格式为
YYYY-MM-DD,时间格式为HH:MM,二者均使用控制器本地时间;此处同样适用上述连续功能中的午夜重新上锁行为,以及主机与控制器时钟假设相关的注意事项。 - 通过 NBAPI 无法观测物理解锁状态——请像上文一样,在 Monitor → Portal Status 中查看门的状态,或亲自确认。
测试
npm test
使用手写的内存 HTTP 存根运行完整单元测试套件——无需网络连接,也不会联系在线控制器。
在线冒烟测试(可选)
npm run test:live
此测试会针对一个已配置的真实控制器运行所有读取工具,但不包括 get_unlock_window/get_daily_unlock_window(37 个工具中的 35 个——见下文的已公开工具),并为每个工具打印一行 PASS/FAIL,然后打印汇总结果;若有任何失败,则以非零状态码退出。只有在 NETBOX_BASE_URL、NETBOX_USERNAME 和 NETBOX_PASSWORD 均已设置时才会运行(如果存在 .env,则会从中加载);否则会打印一行说明跳过在线测试,并以 0 状态码退出。在任何情况下都不会打印 NETBOX_PASSWORD 的值,也绝不会发出写入/控制命令,无论 NETBOX_ENABLE_WRITES 的值如何。npm test 不会运行此脚本,也不要求 .env 文件存在。
在线写入冒烟测试(可选,需两次显式启用)
npm run test:live:write # CRUD round-trips only
npm run test:live:write -- --go # ... plus the real 2-minute unlock window
npm run test:live:write -- --go --start 14:30 # pin the unlock time (1-60 min ahead)
**PowerShell:**至少有一种 PowerShell/npm 组合会静默丢弃传递给
--后的标志(npm 会打印npm warn Unknown cli config "--go",且该标志不会传到脚本——于 2026-09-15 在线观察到)。如果--go未触发阶段 (c),请直接调用脚本:npx tsx scripts/live-check-write.ts --go。
除非同时设置了 NETBOX_BASE_URL、NETBOX_USERNAME、NETBOX_PASSWORD、NETBOX_ENABLE_WRITES=true 以及 NETBOX_LIVE_TEST_PORTALKEY,否则此测试会打印一行说明并以 0 状态码退出,不会发起任何网络请求。以下大多数往返操作都会直接针对控制器执行删除/移除操作(不受 MCP 服务自身 NETBOX_ENABLE_DESTRUCTIVE 限制的影响,因为此脚本会绕过该限制,直接调用 NBAPI 客户端),因此运行前请设置 NETBOX_ENABLE_DESTRUCTIVE=true。
否则,它会针对时间规格、时间规格组、假日、读卡器组和门户组,使用不同的前缀 MCP livecheck 依次执行添加 → 获取 → 修改 → 获取 → 删除的往返测试,并断言每次读回结果(门户组的解锁时间规格组为 Never,假日设在 2099 年,因此不会发生任何解锁)。然后,它会对人员执行往返测试(AddPerson → GetPerson → ModifyPerson → GetPerson),并对该人员的凭证执行往返测试(AddCredential → 使用 WANTCREDENTIALID 调用 GetPerson → 使用 DISABLED=1 调用 ModifyCredential → 读回 → RemoveCredential → 读回)→ RemovePerson;最后一次 GetPerson 返回 NOT FOUND 或 DELETED=TRUE 均可接受(绝不会发送 PERSONPURGE)。它还会测试访问级别(使用 TIMESPECGROUPKEY Never 调用 AddAccessLevel → GetAccessLevel → ModifyAccessLevel → 读回 → DeleteAccessLevel → 读回确认已删除),以及由第二个临时访问级别构建的访问级别组(AddAccessLevelGroup → GetAccessLevelGroup → ModifyAccessLevelGroup → 读回 → DeleteAccessLevelGroup;对于空集合,允许出现 GetTimeSpecGroup 文档中说明的相同 FAIL/ERRMSG="NOT FOUND" 异常)。它会测试威胁级别及威胁级别组(AddThreatLevel → AddThreatLevelGroup → ModifyThreatLevel → ModifyThreatLevelGroup → RemoveThreatLevelGroup → RemoveThreatLevel,通过再次调用 RemoveThreatLevel 失败来证明其已删除——此处不会使用 GetThreatLevels 进行往返验证,也绝不会调用 SetThreatLevel);插入带时间戳的 USERACTIVITY 记录(InsertActivity);通过 ModifyUDFListItems 对 UDF 列表项进行往返测试(如果未配置 UDF 列表,则记录一次 SKIPPED);以及执行 GetPartitions → SwitchPartition,切回会话自身所在的分区(绝不会调用 AddPartition)。往返测试前后,它都会清理中止运行留下的所有 MCP livecheck 项,包括人员、访问级别/组以及威胁级别/组。
然后,它会根据最新的 GetAccessHistory 记录估算控制器的时钟;如果估算时间与主机时钟相差超过 2 分钟,则拒绝运行门禁阶段,无论是否传入 --go。传给 schedule_unlock_window 的窗口时间始终采用控制器本地时间,而非主机本地时间。
传入 --go 时——仅在通知用户(发送推送通知,并通过聊天消息告知确切的解锁和重新上锁时间)并获得同意后才传入,因为用户需要观察门禁——它会打印一行 HEADS-UP,通过真实的 schedule_unlock_window 执行器安排指定门户实际解锁 2 分钟(2 分钟后解锁,4 分钟后重新上锁;或者使用 --start HH:MM),打印 OBSERVE: portal ... should unlock at HH:MM and relock at HH:MM — confirm on Monitor → Portal Status,每隔 30 秒轮询一次 get_unlock_window,直到重新上锁时间过后 1 分钟,然后调用 cancel_unlock_window,并断言受管理的门户组设为 Never,且不再包含任何受管理的假日、时间规格或时间规格组成员(允许存在 leftBehind,但会报告)。如果已存在受管理的窗口,它会拒绝执行该阶段,因此绝不会替换真实窗口。除下方受监督的单项操作外,它绝不会操作输出、调用 TriggerEvent,也不会执行门户锁定/解锁操作;绝不会打印密码;任何断言失败时都会以非零状态退出(但会先取消窗口);npm test 绝不会运行此测试。
受监督的单项操作
npm run test:live:write -- --action unlock_portal
npm run test:live:write -- --action set_threat_level --value High
--action <name> [--value <v>] 会对指定的门户(或其锁舌输出)执行一次写入操作,而不是运行上文的完整流程——完全跳过阶段 (b)、(b2) 和 (c)。仍需要设置 NETBOX_ENABLE_WRITES=true 和凭据变量(与上文相同的跳过方式),但不需要 NETBOX_ENABLE_DESTRUCTIVE,因为不会执行删除操作。如果 --action 与 --go 同时使用、操作名称未知,或 set_threat_level 缺少必需的 --value,则会拒绝运行——退出码为 2,且不会进行网络调用。它会打印发送的确切命令和参数(绝不包含凭据)、控制器返回的 CODE/DETAILS 或 ERRMSG,以及一行 OBSERVE: ...,说明应在门口或 Monitor 上检查什么;如果返回 FAIL 且 ERRMSG 为 "Portal state not changed",则会报告为带备注的 PASS,而非失败。成功或状态已经符合要求时退出码为 0,否则为 1;未知或无效参数的退出码为 2。
每项操作都可以撤销:
| 操作 | 效果 | 撤销方式 |
|---|---|---|
unlock_portal | UnlockPortal(扩展解锁) | lock_portal |
lock_portal | LockPortal | — |
momentary_unlock_portal | MomentaryUnlockPortal(会自动重新上锁) | — |
dog_on_next_exit_portal | DogOnNextExitPortal | lock_portal |
activate_output | 对门户的锁舌输出执行 ActivateOutput | deactivate_output |
deactivate_output | 对门户的锁舌输出执行 DeactivateOutput | — |
set_portals_state_unlock | 真实的 set_portals_state(setPortalsState)工具,操作为 UNLOCK | set_portals_state_lock |
set_portals_state_lock | set_portals_state,操作为 LOCK | — |
set_portals_state_momentary | set_portals_state,操作为 MOMENTARY_UNLOCK(会自动重新上锁) | — |
set_threat_level | SetThreatLevel LEVELNAME=<--value> | set_threat_level --value Default |
trigger_event_activate | TriggerEvent EVENTNAME=<--value> EVENTACTION=ACTIVATE PARTITIONID=1 | trigger_event_deactivate |
trigger_event_deactivate | TriggerEvent EVENTNAME=<--value> EVENTACTION=DEACTIVATE PARTITIONID=1 | — |
activate_output/deactivate_output 会通过查找 GetOutputs 中 NAME 以指定门户的 NAME 开头的条目来解析锁舌输出(例如,门户 "02OF01A" → 输出 "02OF01A EL");如果未找到,则会清楚地报告失败。与此脚本的其他部分一样,无法通过 --action 调用 AddPartition。
trigger_event_activate/trigger_event_deactivate 需要 --value <EVENTNAME>——该名称必须对应一个已存在的 NetBox 事件(无法通过 NBAPI 创建事件;请先在 NetBox UI 中创建)。这是 trigger_event 唯一的实时验证路径——上文的完整 CRUD 流程不会调用 TriggerEvent。这两项操作与其他所有命令一样,都会通过同一个 NetboxClient.call 执行,因此仍适用 NETBOX_EVENT_API_PATH 路由;脚本会打印所使用的 URL 路径。
实时写入冒烟测试——每日解锁窗口(可选,需两次选择加入)
npm run test:live:write:daily # CRUD round-trips only
npm run test:live:write:daily -- --go # ... plus the real 2-minute daily unlock window
**PowerShell:**参见上文“实时写入冒烟测试”下的同名注意事项——如果
--go被静默丢弃,请改用npx tsx scripts/live-check-write-daily.ts --go(已于 2026-09-15 在门户02OF01A上完成实时验证:现场确认解锁/重新上锁,16/16 个步骤均通过)。
这是上文 npm run test:live:write 的配套脚本,涵盖
schedule_daily_unlock_window/cancel_daily_unlock_window/
get_daily_unlock_window(它保留为单独的 npm 脚本,而不串接到
test:live:write,这样 -- --go 才会继续传递给目标脚本)。在与
npm run test:live:write 相同的门控条件下,它会输出一行并以退出码 0 跳过,不进行网络调用。否则,它会针对一个时间规格、一个时间规格组、一个节假日和一个门户组,按添加 → 获取 → 修改 → 获取 → 删除的顺序执行往返操作,使用独立前缀 MCP livecheck daily,并断言每次读回的结果。其中,门户组的解锁时间规格组为 Never,节假日设在 2099 年,因此不会发生解锁。随后,它会执行与 test:live:write 相同的控制器时钟偏差保护(无论是否指定 --go,只要时钟偏差超过 2 分钟,就会拒绝执行上述门禁阶段)。
指定 --go 时——与上文相同,仅在通知用户并获得许可后才可传入——脚本会输出一行 HEADS-UP,通过真实的 schedule_daily_unlock_window 执行器,在指定门户上安排一个实际的 2 分钟每日解锁窗口,仅覆盖今天的日期(dailyStartTime = 当前时间 + 2 分钟,dailyEndTime = 当前时间 + 4 分钟);随后输出一行 OBSERVE: ...,每隔 30 秒轮询一次 get_daily_unlock_window,直到重新上锁后一分钟,再调用 cancel_daily_unlock_window,并断言受管理的门户组设为 Never,且没有留下受管理的节假日或时间规格(允许但会报告 leftBehind)。如果已存在受管理的每日窗口,脚本会拒绝执行此阶段;绝不会触碰人员、凭据、访问级别、威胁级别、输出、事件、分区或 UDF 列表;绝不会打印密码;任何断言失败都会以非零状态退出(但会先取消窗口);npm test 绝不会运行此脚本。
不在范围内
- 照片上传——向
/nbws/goforms/upload发送的 multipart POST 请求,它根本不是 NBAPI XML 命令。读取人员照片在支持范围内:get_picture封装了GetPicture,并原样返回 Base64 JPEG。 - 电梯和楼层的写入操作——这并非取舍:v1 和 v2 指南都没有记录针对电梯或楼层的任何
Add/Modify/Delete命令,因此get_elevators/get_floors是只读的,因为 API 本身就是只读的。请参阅差异报告。 - Data Operations(批量导入/导出人员)——LenelS2 Data Operations 指南描述的是一项通过 Web UI 和 NAS 轮询实现的功能,没有自己的 API,因此无内容可封装。计划中不包括导入文件生成器、导出解析器或 NAS 自动化。
- 警报队列工作流命令(
AckAlarm、AckEvent、AlarmClearActions、AlarmSetOwner、EventClearActions)——get_alarms会读取警报,但此服务不会驱动操作员的警报队列。 StreamEvents/ 持久化的/appdevent/nbapi/event推送信息流- 基于 MAC 的身份验证(仅会话登录)
- S2 Global API 变体
- 已弃用的 NBAPI 命令(
EditPerson、EditThreatLevel、EditThreatLevelGroup、GetAccessDataLog、GetAccessCardDetails、LoginUserName、LoginUserPassword) - 多个并发的受管解锁时段、按时段命名,或在 MCP 主机上进行任何持久化(只有一个受管时段;名称即其标识)
- 在组合工具中按名称解析入口(仅支持键值——
get_portals/find_portals会映射名称) - 取消时自动删除受管入口组或时间规格组(它们会保留,指向
Never/ 空值,并供后续复用) - 主机上的任何调度器(Task Scheduler、进程内计时器)——控制器是唯一的调度器
- 服务内的确认提示——MCP 主机的权限模型和环境门控负责控制
- MCP 工具界面之外的任何 GUI/仪表板
请参阅 specs/archive/s2-netbox-mcp-write.md,了解构建写入工具界面所依据的完整需求;另请参阅 specs/archive/s2-netbox-mcp.md,了解原始的只读 v1 规格(已归档——其所有验收标准均已通过,包括实时验证)。
参与贡献
欢迎提交错误报告、功能请求和 PR——请参阅 CONTRIBUTING.md,了解工作流(先提 issue、分支命名和 PR 规范),以及适用于任何涉及写入/破坏性工具的更改的物理安全说明。
安全
发现漏洞?请参阅 SECURITY.md,了解如何私下报告,以及不同配置级别下可能造成的物理安全影响范围。
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。