System Login —— 获取平台 JSESSIONID
通过 chrome-devtools MCP(mcp__chrome-devtools__* 系列工具)登录 PowerData 平台,获取 JSESSIONID 会话凭证。
> 本 skill 是唯一固定前提:当前 agent 必须挂载了 chrome-devtools MCP。其余能力(视觉识别)按运行时环境分层降级,不硬编码。
适用场景
- 动态表单(pacp-build-form)需要会话凭证
- 动态列表(pacp-build-list)需要会话凭证
- 其他需要调用平台 API 但没有有效 session 的场景
- 会话中途过期(401),需要重新获取凭证
固定前提与能力分层
| 能力位 | 要求 | 说明 |
|---|---|---|
| chrome-devtools MCP | ✅ 必须 | 唯一固定依赖。导航、探测、填写、取值全靠它 |
| 视觉识别能力 | 分层降级 | 用于验证码识别、选择器视觉兜底。按下方策略处理,不硬编码 |
整体流程:三条路径自动选择
flowchart TD
A[触发登录] --> B{查本地缓存 session.json}
B -->|有缓存| C[校验有效性<br/>优先 CLI check_session.js<br/>兜底 浏览器 fetch getcurrentuser]
C -->|status=000 有效| D[✅ 直接复用 JSESSIONID]
C -->|401/失效| E[丢弃缓存]
B -->|无缓存| E
E --> F{用户提供了什么?}
F -->|地址+账号+密码| G[自动登录模式]
F -->|仅地址或无凭证| H[用户自登模式]
G --> G1[navigate 打开登录页]
G1 --> G2[探测表单元素]
G2 --> G3[填写账号密码]
G3 --> G4{有验证码?}
G4 -->|图片码| G5[按视觉能力策略处理]
G5 -->|识别成功| G6[填写验证码]
G5 -->|识别失败/无能力| G7[提示用户在浏览器手动填]
G4 -->|滑块码| G7
G4 -->|无验证码| G8
G6 --> G8[点击登录]
G7 --> G8
H --> H1[navigate 打开登录页]
H1 --> H2[提示用户在浏览器中登录]
H2 --> H3[等待用户主动告知登录完成]
G8 --> I[验证登录成功]
H3 --> I
I --> J[取 JSESSIONID]
J --> K[写入缓存 session.json]
K --> D
核心设计思想:用户自登模式是兜底王牌。因为浏览器对用户可见可操作,任何自动路径卡住时都降级到「你自己登,我等你登完取凭证」——理论上不可能完全卡死。
操作步骤
步骤 0:准备工作目录
session 缓存与其他 pacp- skill 共享,写入 cwd 下统一工作根目录 .pacp/*;截图、验证码等一次性临时文件仍用系统临时目录:
{cwd}/.pacp/
└── session.json # 会话缓存(与 build-form/build-list 共享,按系统地址维度)
{os.tmpdir()}/bwai-generate/pacp-system-login/
└── *.png # 截图、验证码临时文件(一次性)
> [!important] session.json 路径对齐(与 build-form/build-list 共享)
> session.json 统一放 cwd/.pacp/session.json,与 pacp-build-form / pacp-build-list 共享——它们的 BwaiClient.loadSession(baseUrl) 直接读此处的 jsessionid。格式按系统地址(baseUrl)为 key、字段 jsessionid,三方一致。
创建目录:
# session.json 共享目录(cwd 下 .pacp)
mkdir -p "$(pwd)/.pacp"
# 截图/验证码一次性临时目录(系统临时目录)
TMPDIR="$(node -e "console.log(require('os').tmpdir())")/bwai-generate/pacp-system-login"
mkdir -p "$TMPDIR"
步骤 1:查缓存,校验有效性
1a. 读取缓存文件 cwd/.pacp/session.json(若存在)。结构如下,按系统地址({scheme}://{host}:{port}{contextPath})为 key:
{
"http://localhost:9080/xzcf": {
"jsessionid": "B229FE6A...",
"contextPath": "/xzcf",
"userId": "SYSTEM",
"userName": "苏祥灿",
"cachedAt": "2026-07-09T12:30:00+08:00"
}
}
> contextPath 推断:从 URL 路径提取。如 http://localhost:9080/xzcf/app/index.html → contextPath 为 /xzcf(去掉 /app/* 前端路由部分,取 API 前缀)。
1b. 若有缓存,校验有效性。
> [!important] 优先用 CLI 校验,浏览器 evaluate_script 是兜底(2026-08-07 优化)
> 校验一个 JSESSIONID 是否过期,不需要启动浏览器。优先用本 skill 自带的 CLI:
>
> bash
> node scripts/check_session.js --base-url <baseUrl>
> # 未传 --jsessionid 时自动从 cwd/.pacp/session.json 按 baseUrl 读缓存
> # 返回 {success:true, valid:true, userId:"SYSTEM"} 或 {success:true, valid:false, reason:"session_invalid"}
>
>
> CLI 用 Node HTTP 直接调 getcurrentuser,3 秒内完成,不开浏览器、不占 chrome MCP。
> 只有以下情况才退回浏览器 evaluate_script 方式:
> - CLI 因网络/防火墙原因失败,且怀疑是假阴性
> - 当前已经在浏览器里操作(如刚做完自动登录步骤 3),顺手用浏览器校验更快
方式一(优先):CLI 校验(免浏览器)
node scripts/check_session.js --base-url ${baseUrl}
# 或显式传 jsessionid:node scripts/check_session.js --base-url ${baseUrl} --jsessionid ${值}
返回 {success:true, valid:true, userId, userName} → 缓存有效;{success:true, valid:false, reason} → 缓存失效。
方式二(兜底):浏览器 evaluate_script
只有 CLI 不可用时才用。必须先导航到目标系统页面(让 cookie 域名匹配),再执行:
mcp__chrome-devtools__navigate_page → url: ${baseUrl}
mcp__chrome-devtools__evaluate_script
async () => {
try {
const r = await fetch('${contextPath}/api/v2/platform/rms/user/getcurrentuser',
{ method: 'GET', credentials: 'include' });
const d = await r.json();
return d.status === '000' ? { valid: true, userId: d.data.YHID } : { valid: false };
} catch { return { valid: false }; }
}
valid: true→ 缓存有效,直接复用 JSESSIONID,流程结束。valid: false→ 丢弃该条缓存,继续步骤 2。
步骤 2:判断走哪条登录路径
根据用户提供的信息:
| 用户提供 | 走哪条路径 |
|---|---|
| 地址 + 账号 + 密码 | 自动登录模式(步骤 3) |
| 仅地址,或没有凭证 | 用户自登模式(步骤 4) |
> 如果用户什么都没提供,问一句:「需要登录哪个系统?给我地址就行,如果有账号密码我可以自动填,没有的话你自己在浏览器里登」。
步骤 3:自动登录模式
3a. 打开登录页
> [!important] 直接访问项目根网址,不要访问前端页面(2026-07-14 优化)
> 导航到根网址 = {scheme}://{host}:{port}{contextPath}(如 http://localhost:9001/xzcf),即去掉 /app/index.html / /#/... 等前端路由部分。根网址会直接进入登录页,避免先加载前端 SPA 再跳转一轮(省一次跳转 + 省 SPA 加载时间)。
>
> - 用户给的是前端页面 URL(如 http://host/xzcf/app/index.html#/list-render/xxx)→ 截取到 contextPath(http://host/xzcf)再导航
> - 用户给的是根网址 → 直接用
mcp__chrome-devtools__navigate_page → url: {项目根网址 scheme://host:port/contextPath}
3b. 探测登录表单元素
第一层:结构化探测(chrome-devtools 固定能力)
mcp__chrome-devtools__evaluate_script
() => {
const inputs = Array.from(document.querySelectorAll('input')).map(e => ({
type: e.type, name: e.name, id: e.id, placeholder: e.placeholder,
ariaLabel: e.getAttribute('aria-label'),
visible: e.offsetParent !== null
}));
const buttons = Array.from(document.querySelectorAll('button, input[type="submit"]')).map(e => ({
tag: e.tagName.toLowerCase(), type: e.type,
text: e.textContent?.trim().substring(0, 30),
id: e.id, visible: e.offsetParent !== null
}));
const imgs = Array.from(document.querySelectorAll('img')).map(e => ({
src: e.src?.substring(0, 80), id: e.id, alt: e.alt,
visible: e.offsetParent !== null
})).filter(i => /captcha|verify|code|验证/i.test(i.src + i.id + i.alt));
return { inputs, buttons, captchaImgs: imgs };
}
第二层:启发式匹配
按优先级匹配各元素(命中即停):
| 目标 | 匹配规则(按优先级) |
|---|---|
| 账号框 | ① id 为 #XTZH(实测:账号输入框,type=text,placeholder="请输入账号")—— 平台已知选择器 ② type=text + id/name/placeholder 含 user\|account\|username\|系统\|账号\|用户 ③ 第一个可见 type=text 输入框 |
| 密码框 | ① id 为 #YHPD(实测:密码输入框,type=password,placeholder="请输入密码")—— 平台已知选择器 ② type=password ③ placeholder 含 password\|密码 |
| 登录按钮 | ① id 为 #login —— 平台已知选择器 ② button 文本含 登录\|Login\|提交\|Submit ③ input[type=submit] |
| 验证码输入框 | ① id 为 #validateCode —— 平台已知选择器 ② placeholder/name 含 验证码\|captcha\|verify |
| 验证码图片 | ① id 为 #valicodeImg —— 平台已知选择器 ② 探测返回的 captchaImgs 中第一个 |
> 已知选择器优先:平台历史选择器(#XTZH/#YHPD/#login/#validateCode/#valicodeImg)作为第一优先级,命中则直接用,稳定性最高。
>
> ⚠️ 选择器语义说明(2026-07-09 实测确认):#XTZH=账号框(type=text),#YHPD=密码框(type=password)。早期文档曾把两者标注颠倒,以 type 字段和 placeholder 为准。
第三层:视觉兜底(按「视觉能力策略」处理)
如果结构化探测+启发式匹配无法定位必需元素(账号框、密码框、登录按钮),走下文「视觉能力策略」截图分析。
3c. 填写账号密码
> [!warning] 必须用 fill 模拟键盘输入,禁止 evaluate_script 设 value(2026-07-13 实测)
> 平台前端对密码有加密逻辑。用 evaluate_script 设 input.value 会绕过前端事件处理,触发加密异常导致登录失败(401 用户名/密码错误)。
> 正确做法:用 fill 工具模拟真实键盘输入,前端自然处理加密;密码填明文即可,AI 不要处理加密逻辑。
>
> ⚠️ 全角/半角(高频坑):密码里的标点(@ ! 等)按用户给的原样填,fill 不做转换。登录返回「用户名或密码错误」时优先排查这个(详见 FAQ)。
先 take_snapshot 取得账号框/密码框的 uid,再用 fill 填写:
mcp__chrome-devtools__take_snapshot → 取得 #XTZH、#YHPD 的 uid
mcp__chrome-devtools__fill → uid: {账号框uid}, value: {账号} # 明文
mcp__chrome-devtools__fill → uid: {密码框uid}, value: {密码} # 明文,前端自动加密
> 若平台有两个文本框(系统+账号),都填。fill 报错找不到元素时,重新 take_snapshot 获取最新 uid。
3d. 验证码处理(如有)
检测到验证码元素时,按验证码类型分流:
| 验证码类型 | 处理 |
|---|---|
| 图片验证码 | 截图后走「视觉能力策略」识别,识别成功则填写 #validateCode;识别失败或无视觉能力,提示用户在浏览器手动填写 |
| 滑块验证码 | 直接降级人工——提示用户在可见浏览器手动完成滑块(AI 无可靠处理手段,不浪费尝试) |
| 短信验证码 | 提示用户输入收到的验证码值,skill 填写 |
| 无验证码 | 跳过 |
3e. 点击登录
mcp__chrome-devtools__click → uid: {登录按钮uid}
3f. 等待登录完成
mcp__chrome-devtools__wait_for → text: ["首页", "工作台", "欢迎"] # 登录成功的标志文本
或等待 2-3 秒后用 evaluate_script 调 getcurrentuser 确认登录态。
> 登录后可能有弹窗(如「用户未设置所属部门」),用 take_snapshot 检测,有则点击「确定」。
步骤 4:用户自登模式(兜底王牌)
4a. 打开登录页
> 同步骤 3a:导航到项目根网址(scheme://host:port/contextPath,去掉前端路由),直接进登录页。
mcp__chrome-devtools__navigate_page → url: {项目根网址}
4b. 提示用户,等待主动告知
> 关键设计:不轮询,等用户主动告知。
提示用户:「我已在浏览器中打开登录页,请在浏览器里完成登录(填账号密码、过验证码)。登好后告诉我一声,我来取会话凭证。」
然后停止操作,等待用户回复(如「登好了」「取 session」「继续」)。收到确认后进入步骤 5。
> 为什么不轮询:chrome-devtools 的 evaluate_script 是同步 MCP 往返,轮询会产生大量工具调用,开销大且啰嗦。用户在自己的浏览器里操作,最清楚何时登完。
步骤 5:取 JSESSIONID(核心技术步骤)
> ⚠️ JSESSIONID 是 HttpOnly,document.cookie 和 cookieStore 都读不到。 唯一可靠路径:主动发请求让浏览器带上 cookie,再从网络请求头读取。
5a. 主动发请求,触发浏览器携带 cookie:
mcp__chrome-devtools__evaluate_script
async () => {
try {
const r = await fetch('${contextPath}/api/v2/platform/rms/user/getcurrentuser',
{ method: 'GET', credentials: 'include' });
const d = await r.json();
return { triggered: true, status: d.status, userId: d.data?.YHID };
} catch (e) { return { triggered: false, error: e.message }; }
}
> credentials: 'include' 让浏览器自动把 HttpOnly 的 JSESSIONID 附带到请求 cookie 头。
5b. 从网络请求列表找到该请求的 reqid:
mcp__chrome-devtools__list_network_requests → resourceTypes: ["xhr", "fetch"]
在返回列表中找 URL 包含 getcurrentuser 的请求,取其 reqid(通常取最后一个匹配的)。
> ⚠️ 导航清记录风险:list_network_requests 返回的是「自上次导航以来的请求」。如果在 5a 之后发生了页面导航,请求记录可能被清除。处理:确保 5a 之后不触发导航;若记录已清,重新执行 5a 再立即 5b。
>
> 💡 实测补充(2026-07-09):前端 hash 路由变化(URL 仅 # 后部分改变,如 #/admin → #/penalty/case/todo)不是整页导航,不会清除网络请求记录。只有整页跳转(window.location 变化)才会清记录。
5c. 读请求头,提取 JSESSIONID:
mcp__chrome-devtools__get_network_request → reqid: {上一步找到的reqid}
在返回的 Request Headers 中找 cookie: 行,提取 JSESSIONID=xxxx 的值。
> 示例:- cookie:JSESSIONID=B229FE6AAEB7949C1F02255E135313EC → JSESSIONID 为 B229FE6AAEB7949C1F02255E135313EC。
步骤 6:写入缓存
将 JSESSIONID 写入 session.json,按系统地址为 key:
# 读取现有缓存 → 追加/更新该系统条目 → 写回
node -e "
const fs = require('fs');
const path = require('path');
const f = path.join(process.cwd(), '.pacp', 'session.json');
fs.mkdirSync(path.dirname(f), { recursive: true });
let cache = {};
try { cache = JSON.parse(fs.readFileSync(f, 'utf8')); } catch {}
cache['${系统地址key}'] = {
jsessionid: '${JSESSIONID值}',
contextPath: '${contextPath}',
userId: '${userId}',
userName: '${userName}',
cachedAt: new Date().toISOString()
};
fs.writeFileSync(f, JSON.stringify(cache, null, 2));
console.log('缓存已写入:', f);
"
步骤 7:输出结果
向调用方(pacp-build-form / pacp-build-list / 用户)返回:
✅ 登录成功
系统:{系统地址}
用户:{userId} / {userName}
JSESSIONID:{值}
缓存:已写入,下次可直接复用
视觉能力策略(验证码识别 / 选择器视觉兜底)
> ⚠️ 本 skill 的使用者模型和 agent 不确定。视觉识别能力必须分层降级,不硬编码具体 MCP。
当需要视觉识别时(图片验证码、选择器视觉兜底),按以下顺序探测当前环境能力:
flowchart TD
A[需要视觉识别] --> B{你当前模型/agent<br/>自身有多模态视觉能力?}
B -->|是| C[截图后直接交给模型识别]
B -->|否| D{环境挂载了视觉 MCP 工具?}
D -->|是| E[调用视觉 MCP 工具识别<br/>如 zai-mcp-server 的<br/>extract_text_from_screenshot]
D -->|否| F[降级: 让用户在可见浏览器手动操作]
C --> G{识别成功?}
E --> G
G -->|是| H[✅ 自动填写]
G -->|否/超时| F
F --> I[✅ 用户完成后继续]
设计原则:
- 不硬编码具体 MCP 名称 —— 文本里写「视觉 MCP 工具」,举例时用「如 zai-mcp-server 的 extract_text_from_screenshot」,但作为示例而非硬性要求。
- 能力探测靠 agent 自我判断 —— 你(agent)对自身能力有认知:知道自己是多模态模型,或知道自己挂载了哪些 MCP 工具。按认知选择路径。
- 降级到人工不是失败 —— 用户自登模式本就是设计内的兜底王牌,视觉能力缺失自然走这条路径。
- 永远能拿到结果 —— 最底层是「用户在可见浏览器手动操作」,无论环境多受限都不会卡死。
截图方法(chrome-devtools 固定能力):
# 整页截图
mcp__chrome-devtools__take_screenshot → filePath: {临时目录}/captcha.png
# 元素截图(验证码图片)
mcp__chrome-devtools__take_screenshot → uid: {验证码图片uid}, filePath: {临时目录}/captcha.png
会话保活(配置生成过程中 401 处理)
pacp-build-form / pacp-build-list 在调用平台 API 过程中可能遇到 401(会话过期)。处理策略:
- 不直接报错,静默触发重新获取 JSESSIONID
- 若浏览器还开着且会话还在(只是配置 API 的 session 过期),直接重走「步骤 5」取新 JSESSIONID
- 若会话已完全过期,走完整登录流程(步骤 1-6)
- 取到新 JSESSIONID 后,重试原失败的请求
- 整个过程对调用方透明
常见问题
Q: navigate 后页面没跳到登录页?
平台可能已登录(会话有效),直接走步骤 1 校验缓存即可。或者系统地址需要调整(有些系统登录页在特定路径)。
Q: fill 报错找不到元素?
用 take_snapshot 重新获取页面最新的元素 uid(uid 可能因页面渲染变化)。snapshot 返回的 a11y 树含 uid,比硬编码选择器可靠。
Q: get_network_request 读不到 cookie 头?
检查是否发生了导航清除了请求记录。重新执行 5a(主动 fetch)后立即执行 5b/5c,中间不要触发导航。
Q: session.json 缓存校验总失败?
JSESSIONID 是会话级 cookie,浏览器关闭或服务重启后失效属正常。校验失败就走登录流程,登录后重新缓存。
Q: 登录返回「用户名或者密码错误」但密码看着对?
首先排查全角/半角(2026-07-14 实测,高频坑):密码里的标点(@ ! # 等)可能被填成全角(@ ! #),尤其用户从聊天工具复制密码时。PowerData 平台实测密码 SDxzcf@2026! 结尾是全角 !,用半角 ! 就登录失败。
排查:用 evaluate 查密码框 value 的 charCode——半角 !=33、@=64;全角 !=65281、@=65312。命中 65281/65312 即全角。
处理:向用户确认密码里标点的全/半角,按用户给的原样填(fill 传入什么填什么,不做转换)。登录失败时优先怀疑这个。
注意事项
- JSESSIONID 是会话级 cookie(
expires: -1),浏览器关闭或服务重启后失效,属正常现象。 - 同一账号重复登录可能导致旧 session 失效(互踢)。缓存优先复用已天然缓解此问题——能复用就不重登。
document.cookie/cookieStore读不到 HttpOnly 的 JSESSIONID,必须通过「主动 fetch + 网络请求头」路径获取。getcurrentuser接口在所有 PowerData 系统均可用,返回status:000即代表会话有效,是校验登录态的标准接口。校验优先用 CLIscripts/check_session.js(免浏览器),浏览器 evaluate_script 是兜底。- 缓存按系统地址维度存储,支持同时操作多个环境(不同 host:port/contextPath)。
- 跨平台兼容:临时目录用
os.tmpdir()自适应 Windows/Mac/Linux。