提交 85ef6050 authored 作者: 陈泽健's avatar 陈泽健

feat(远程自动化部署): X86统信5.70 业务功能验证与授权验证脚本入库

- verify_business.py 8步业务验证脚本(教训L/M 修复版)+ verify_business_result.json 8/8通过存档
- phase4_run/phase4_verify/post_deploy_verify 授权闭环与复验脚本
- _verify_login/_diag_verify_apis 登录与4大接口实时复核脚本
- create_permgroup/diag_* 业务验证排障脚本(SKILL 教训L 引用)
Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 460aa4e5
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""复验四大业务接口(对外、预定、运维集控、讯飞)在路由修改后是否仍正常"""
import sys, io, requests
if sys.stdout.encoding != 'utf-8':
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
HOST = '192.168.5.70'
tests = [
('对外接口', f'https://{HOST}/exapi/message/getMsgPageList', '无效token'),
('预定系统接口', f'https://{HOST}/meetingV3/api/systemConfiguration/globalConfig?companyNumber=CN-SZ-00-0201', 'accessToken为空'),
('运维集控接口', f'https://{HOST}/monitor/api2/api/servermonitor/', '用户不存在'),
('讯飞转录接口', f'https://{HOST}/voice/api/iflytek/roommaster?company_id=1&user_id=8&company_secret=57d00f9f-020f-5f1f-b788-55fae843bceb&getall=1', '"success":true'),
]
all_pass = True
for name, url, expect in tests:
try:
r = requests.get(url, verify=False, timeout=10)
hit = expect in r.text
print(f'[{hit and "✅" or "❌"}] {name}: HTTP {r.status_code}')
print(f' 响应: {r.text[:150]}')
if not hit:
all_pass = False
except Exception as e:
print(f'[❌] {name} 异常: {e}')
all_pass = False
print(f'\n接口复验结果: {"全部通过 ✅" if all_pass else "有接口异常 ❌"}')
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""验证修复后 /api/system/login 能否正常登录,返回 JWT"""
import sys, io
import paramiko
from pathlib import Path
import json
if sys.stdout.encoding != 'utf-8':
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect('192.168.5.70', port=22, username='root', password='Ubains@2026', timeout=15)
HASH = '84ef2b3b9e6eea45541b1771efcef4e01e6c3b585b0a38f26054bb8e53f16043'
VERIFY_CODE = 'csba'
# 1. 抓取 datacode
stdin, stdout, stderr = client.exec_command('curl -s -i "http://172.17.0.1:8999/system/getVerifyCode"')
res = stdout.read().decode('utf-8', errors='ignore')
datacode = next((line.split(':', 1)[1].strip() for line in res.splitlines() if 'datacode:' in line), None)
print('=== datacode ===')
print(datacode or '(无 datacode)')
# 2. 带 datacode 登录
if datacode:
cmd = f'curl -s -i -X POST "http://172.17.0.1:8999/system/login?account=superadmin&password={HASH}&verifyCode={VERIFY_CODE}" -H "datacode: {datacode}" -H "Content-Type: application/json"'
stdin, stdout, stderr = client.exec_command(cmd)
out = stdout.read().decode('utf-8', errors='ignore')
print('=== 带 datacode 登录响应 ===')
print(out.strip())
else:
print('跳过 datacode 登录')
client.close()
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""业务验证 - 创建测试权限组(固定选择器 + 批量全选 + 绑定)"""
import sys, io, json, time
from pathlib import Path
from playwright.sync_api import sync_playwright
if sys.stdout.encoding != 'utf-8':
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
CODE_DIR = Path(__file__).parent
CONFIG = json.loads((CODE_DIR / 'deploy_config.json').read_text(encoding='utf-8'))
HOST = CONFIG['server']['host']
ADMIN_URL = f'https://{HOST}/#/LoginAdmin'
VB = CONFIG['license']['verify_code']
SHOT_DIR = Path.home() / 'deploy_logs' / 'screenshots'
SHOT_DIR.mkdir(parents=True, exist_ok=True)
def all_frames(page):
return [page] + [f for f in page.frames if f != page.main_frame]
def snap(page, name):
p = SHOT_DIR / f"biz_{name}.png"
try:
page.screenshot(path=str(p))
print(f"[截图] {p}", flush=True)
except Exception as e:
print(f"[截图失败] {e}", flush=True)
def main():
with sync_playwright() as p:
browser = p.chromium.launch(headless=True, args=[
'--ignore-certificate-errors', '--disable-web-security', '--no-sandbox'])
ctx = browser.new_context(ignore_https_errors=True)
page = ctx.new_page()
page.goto(ADMIN_URL, timeout=60000, wait_until='domcontentloaded')
page.wait_for_timeout(5000)
vis = page.locator('input:visible')
vis.nth(0).fill('admin')
vis.nth(1).fill('Ubains@13579')
vis.nth(2).fill(VB)
try:
page.locator('input[type="submit"]:visible').first.click(timeout=5000)
except Exception:
page.locator('button:has-text("登")').first.click(timeout=5000)
page.wait_for_timeout(6000)
snap(page, "login_filled")
# 导航权限管理
for f2 in all_frames(page):
try:
t = f2.locator('.el-submenu__title:has-text("系统管理"), .el-submenu__title:has-text("系 统管理")')
if t.count() > 0:
t.first.click()
break
except Exception:
pass
page.wait_for_timeout(2000)
clicked = False
for f2 in all_frames(page):
try:
m = f2.locator('text=权限管理')
for i in range(m.count()):
if m.nth(i).is_visible():
m.nth(i).click()
clicked = True
break
if clicked:
break
except Exception:
pass
print(f"导航权限管理: {clicked}", flush=True)
page.wait_for_timeout(3000)
snap(page, "perm_nav")
# 进入新增权限组页
for f2 in all_frames(page):
try:
b = f2.locator('button:has-text("添加")')
if b.count() > 0:
b.first.click()
print("已点击添加", flush=True)
break
except Exception:
pass
page.wait_for_timeout(4000)
snap(page, "perm_add")
# 填写权限组名称
for i, f2 in enumerate(all_frames(page)):
try:
# 查找包含"权限组名称"的表单项
html = f2.evaluate('''() => {
const out = [];
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT);
let node;
while ((node = walker.nextNode())) {
const ownText = Array.from(node.childNodes)
.filter(n => n.nodeType === 3).map(n => n.textContent.trim()).join('');
if (ownText.includes('权限组名称')) {
let p = node.parentElement;
let chain = [];
for (let k = 0; k < 5 && p; k++) {
chain.push(p.tagName + '.' + p.className.split(' ')[0]);
p = p.parentElement;
}
out.push('NODE: ' + node.tagName + ' class=' + node.className.split(' ')[0] + ' chain=' + chain.join(' <- '));
const fi = node.closest('.el-form-item, .el-form-item__label');
if (fi) {
const inp = fi.querySelector('input');
if (inp) {
out.push('INPUT: ' + inp.tagName + ' placeholder=' + (inp.placeholder || inp.getAttribute('placeholder')));
}
}
}
}
return out;
}''')
if html:
print(f"--- frame[{i}] ---")
for line in html:
print(' ' + line)
except Exception:
pass
# 填写表单
page.locator('input').first.fill('测试权限组')
snap(page, "perm_filled")
print("已填权限组名称: 测试权限组", flush=True)
# 批量全选复选框
checkboxes = page.locator('input[type="checkbox"]')
for i in range(checkboxes.count()):
try:
checkboxes.nth(i).check()
except Exception:
pass
print(f"已勾选 {checkboxes.count()} 个复选框", flush=True)
snap(page, "perm_checkboxes")
# 点击保存
for f2 in all_frames(page):
try:
b = f2.locator('button:has-text("保存"), button:has-text("确定")')
if b.count() > 0:
b.first.click()
print("已点击保存", flush=True)
break
except Exception:
pass
page.wait_for_timeout(5000)
snap(page, "perm_saved")
# 返回列表页,检查是否创建成功
page.reload(wait_until='domcontentloaded')
page.wait_for_timeout(5000)
for i, f2 in enumerate(all_frames(page)):
try:
txt = f2.locator('body').inner_text()[:500].replace('\n', ' | ')
if '测试权限组' in txt:
print(f"✅ 创建成功: 列表中出现 测试权限组", flush=True)
break
except Exception:
pass
snap(page, "perm_list")
browser.close()
if __name__ == '__main__':
main()
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""诊断权限组绑定页:角色搜索下拉框展开后的选项 DOM"""
import sys, io, json
from pathlib import Path
from playwright.sync_api import sync_playwright
if sys.stdout.encoding != 'utf-8':
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
CODE_DIR = Path(__file__).parent
CONFIG = json.loads((CODE_DIR / 'deploy_config.json').read_text(encoding='utf-8'))
HOST = CONFIG['server']['host']
ADMIN_URL = f'https://{HOST}/#/LoginAdmin'
VB = CONFIG['license']['verify_code']
SHOT_DIR = Path.home() / 'deploy_logs' / 'screenshots'
def all_frames(page):
return [page] + [f for f in page.frames if f != page.main_frame]
def snap(page, name):
p = SHOT_DIR / f"diag_{name}.png"
try:
page.screenshot(path=str(p))
print(f"[截图] {p}", flush=True)
except Exception as e:
print(f"[截图失败] {e}", flush=True)
def main():
with sync_playwright() as p:
browser = p.chromium.launch(headless=True, args=[
'--ignore-certificate-errors', '--disable-web-security', '--no-sandbox'])
ctx = browser.new_context(ignore_https_errors=True)
page = ctx.new_page()
page.goto(ADMIN_URL, timeout=60000, wait_until='domcontentloaded')
page.wait_for_timeout(5000)
vis = page.locator('input:visible')
vis.nth(0).fill('admin')
vis.nth(1).fill('Ubains@13579')
vis.nth(2).fill(VB)
try:
page.locator('input[type="submit"]:visible').first.click(timeout=5000)
except Exception:
page.locator('button:has-text("登")').first.click(timeout=5000)
page.wait_for_timeout(6000)
page.reload(wait_until='domcontentloaded')
page.wait_for_timeout(6000)
# 系统管理→权限管理
for f2 in all_frames(page):
try:
t = f2.locator('.el-submenu__title:has-text("系统管理"), .el-submenu__title:has-text("系 统管理")')
if t.count() > 0:
t.first.click()
break
except Exception:
pass
page.wait_for_timeout(2000)
for f2 in all_frames(page):
try:
m = f2.locator('text=权限管理')
done = False
for i in range(m.count()):
if m.nth(i).is_visible():
m.nth(i).click()
done = True
break
if done:
break
except Exception:
pass
page.wait_for_timeout(3000)
# 点击测试权限组行的绑定
for f2 in all_frames(page):
try:
for sel in ['.el-table__fixed-right tr:has-text("测试权限组") .el-tag:has-text("绑定")',
'tr:has-text("测试权限组") .el-tag:has-text("绑定")']:
b = f2.locator(sel)
if b.count() > 0:
b.first.click()
print("已点击绑定", flush=True)
break
else:
continue
break
except Exception:
pass
page.wait_for_timeout(3000)
# 点击"搜索角色"下拉框(el-select 容器,输入框 readonly 点击无效)
clicked = False
for f2 in all_frames(page):
try:
sel = f2.locator('.el-select:has(input[placeholder="搜索角色"])')
if sel.count() > 0:
sel.first.click()
clicked = True
print("已点击搜索角色下拉框(el-select)", flush=True)
break
except Exception:
pass
print(f"点击角色下拉框: {clicked}", flush=True)
page.wait_for_timeout(2500)
snap(page, "bind_role_dropdown")
# 抓取所有可见下拉层/列表选项 DOM
for i, f2 in enumerate(all_frames(page)):
try:
info = f2.evaluate('''() => {
const out = {};
// el-select-dropdown
out.drops = Array.from(document.querySelectorAll('.el-select-dropdown')).map(d => ({
visible: getComputedStyle(d).display !== 'none' && d.offsetParent !== null,
items: Array.from(d.querySelectorAll('.el-select-dropdown__item')).map(it => it.innerText.trim())
}));
// 其他 popper
out.poppers = Array.from(document.querySelectorAll('.el-popover, .el-tooltip__popper, [x-placement]')).map(d => ({
cls: d.className.slice(0, 80), visible: d.offsetParent !== null,
text: d.innerText.replace(/\\n/g, ' | ').slice(0, 200)
})).filter(x => x.visible);
// 搜索角色附近容器 HTML
const inp = document.querySelector('input[placeholder="搜索角色"]');
if (inp) {
let box = inp.closest('.el-select') || inp.parentElement;
out.selectHTML = box ? box.outerHTML.slice(0, 1200) : null;
}
return out;
}''')
if info and (info.get('drops') or info.get('poppers') or info.get('selectHTML')):
print(f"--- frame[{i}] ---", flush=True)
print(json.dumps(info, ensure_ascii=False, indent=1), flush=True)
except Exception:
pass
browser.close()
if __name__ == '__main__':
main()
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""诊断前台新建会议页:会议室列表 checkbox 结构 + 点击后真实状态 + 确定创建时错误消息"""
import sys, io, json, time
from pathlib import Path
from playwright.sync_api import sync_playwright
if sys.stdout.encoding != 'utf-8':
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
CODE_DIR = Path(__file__).parent
CONFIG = json.loads((CODE_DIR / 'deploy_config.json').read_text(encoding='utf-8'))
HOST = CONFIG['server']['host']
ADMIN_URL = f'https://{HOST}/#/LoginAdmin'
FRONT_URL = f'https://{HOST}/'
VB = CONFIG['license']['verify_code']
ADMIN_PASS_NEW = 'Ubains@13579'
SHOT_DIR = Path.home() / 'deploy_logs' / 'screenshots'
SHOT_DIR.mkdir(parents=True, exist_ok=True)
def all_frames(page):
return [page] + [f for f in page.frames if f != page.main_frame]
def log(msg):
print(f"[{time.strftime('%H:%M:%S')}] {msg}", flush=True)
def snap(page, name):
p = SHOT_DIR / f"diag_room_{name}.png"
try:
page.screenshot(path=str(p))
log(f"[截图] {p}")
except Exception as e:
log(f"[截图失败] {e}")
def find_text(page, text, timeout_ms=10000):
deadline = time.time() + timeout_ms / 1000
while time.time() < deadline:
for f in all_frames(page):
try:
if f.locator(f'text={text}').count() > 0:
return True
except Exception:
pass
time.sleep(0.8)
return False
def inspect_room_dom(page, label):
"""输出所有 frame 中 .el-table 相关 DOM 信息"""
for i, f in enumerate(all_frames(page)):
try:
info = f.evaluate('''() => {
const out = [];
document.querySelectorAll('.el-table__row, .el-checkbox, .el-table, .el-table__body tr').forEach(el => {
const t = (el.innerText || '').trim().replace(/\\s+/g, ' ');
if (t.includes('测试会议室') || el.classList.contains('el-table__row')) {
const cls = el.className;
const cb = el.querySelector('.el-checkbox');
const cbInput = el.querySelector('input[type="checkbox"]');
out.push({
tag: el.tagName,
cls: cls.slice(0, 120),
text: t.slice(0, 80),
hasElCheckbox: !!cb,
cbCls: cb ? cb.className : null,
cbInput: cbInput ? {
checked: cbInput.checked,
disabled: cbInput.disabled,
type: cbInput.type,
display: getComputedStyle(cbInput).display,
} : null,
cbBoxCls: cb ? (cb.querySelector('.el-checkbox__input') || {}).className : null,
});
}
});
return out;
}''')
if info:
log(f"--- frame[{i}] ---")
for it in info:
log(f" {json.dumps(it, ensure_ascii=False)}")
except Exception:
pass
def grab_errors(page):
"""抓取当前所有可见 el-message(包括即将消失前)"""
for f in all_frames(page):
try:
texts = f.evaluate('''() => Array.from(document.querySelectorAll(".el-message"))
.filter(e => e.offsetParent !== null || getComputedStyle(e).display !== 'none')
.map(e => e.innerText.trim()).filter(t => t)''')
if texts:
log(f" 前台错误提示: {texts}")
except Exception:
pass
def main():
with sync_playwright() as p:
browser = p.chromium.launch(headless=True, args=[
'--ignore-certificate-errors', '--disable-web-security', '--no-sandbox'])
ctx = browser.new_context(ignore_https_errors=True)
page = ctx.new_page()
# 会话共享登录:先登录后台
page.goto(ADMIN_URL, timeout=60000, wait_until='domcontentloaded')
page.wait_for_timeout(5000)
vis = page.locator('input:visible')
vis.nth(0).fill('admin')
vis.nth(1).fill(ADMIN_PASS_NEW)
vis.nth(2).fill(VB)
try:
page.locator('input[type="submit"]:visible').first.click(timeout=5000)
except Exception:
page.locator('button:has-text("登")').first.click(timeout=5000)
page.wait_for_timeout(6000)
page.reload(wait_until='domcontentloaded')
page.wait_for_timeout(6000)
log(f"后台登录完成: {page.url}")
# 访问前台(会话共享)
page.goto(FRONT_URL, timeout=60000, wait_until='domcontentloaded')
page.wait_for_timeout(8000)
# 处理协议弹窗
for _ in range(3):
dlg = None
for f in all_frames(page):
try:
mb = f.locator('.el-message-box:visible, .el-dialog:visible').filter(has_text='协议')
if mb.count() > 0:
dlg = mb.first
break
except Exception:
pass
if not dlg:
break
try:
dlg.locator('button:has-text("确定")').first.click(timeout=5000)
log("已同意服务协议")
except Exception:
pass
page.wait_for_timeout(4000)
log(f"前台 URL: {page.url}")
log(f"前台首页元素: {find_text(page, '新建会议', 15)}")
# 点击 新建会议
for f in all_frames(page):
try:
b = f.locator('text=新建会议')
if b.count() > 0:
b.first.click()
log("已点击 新建会议")
break
except Exception:
pass
page.wait_for_timeout(5000)
snap(page, "new_page")
# 点击 选择开会区域
for f in all_frames(page):
try:
loc = f.locator('text=选择开会区域')
if loc.count() > 0:
loc.first.click()
log("已点击 选择开会区域")
break
except Exception:
pass
page.wait_for_timeout(4000)
snap(page, "area_expanded")
# 1) 检查 DOM:测试会议室行结构
log("=== 勾选前 DOM ===")
inspect_room_dom(page, "before")
# 2) 尝试多种点击方式并验证真实状态
checked_cls = None
for approach, desc in [
('cb_click', '点击 .el-checkbox'),
('cb_inner_click', '点击 .el-checkbox__inner'),
('js_native_click', 'JS 原生 click input'),
('js_check', 'JS input.click() + 触发change'),
]:
row = None
for f in all_frames(page):
try:
r = f.locator('.el-table__row:has-text("测试会议室")')
if r.count() > 0:
row = r.first
rowf = f
break
except Exception:
pass
if not row:
log("未找到 测试会议室 行")
break
try:
if approach == 'cb_click':
row.locator('.el-checkbox').first.click()
elif approach == 'cb_inner_click':
row.locator('.el-checkbox__inner').first.click()
elif approach == 'js_native_click':
rowf.evaluate('''() => {
const el = document.querySelector('.el-table__row:has-text("测试会议室") input[type="checkbox"]');
if (el) { el.click(); }
}''')
elif approach == 'js_check':
rowf.evaluate('''() => {
const cb = document.querySelector('.el-table__row:has-text("测试会议室") input[type="checkbox"]');
if (cb) {
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'checked').set;
setter.call(cb, true);
cb.dispatchEvent(new Event('change', {bubbles: true}));
cb.dispatchEvent(new Event('click', {bubbles: true}));
}
}''')
except Exception as e:
log(f"[{desc}] 异常: {e}")
continue
page.wait_for_timeout(1500)
# 验证状态
st = None
for f in all_frames(page):
try:
st = f.evaluate('''() => {
const row = document.querySelector('.el-table__row:has-text("测试会议室")');
if (!row) return null;
const cb = row.querySelector('.el-checkbox');
const input = row.querySelector('input[type="checkbox"]');
return {
inputChecked: input ? input.checked : null,
inputDisabled: input ? input.disabled : null,
cbCls: cb ? cb.className : null,
cbInputCls: cb ? (cb.querySelector('.el-checkbox__input') || {}).className : null,
};
}''')
if st:
break
except Exception:
pass
log(f"[{desc}] 点击后状态: {json.dumps(st, ensure_ascii=False)}")
if st and st.get('inputChecked'):
checked_cls = approach
log(f"✅ [{desc}] 勾选成功!inputChecked=true")
break
# 未成功则尝试取消当前的勾选状态恢复
page.wait_for_timeout(500)
snap(page, "after_attempts")
log(f"最终勾选方式: {checked_cls}")
if checked_cls:
# 点击确定创建,立即抓错误消息
for f in all_frames(page):
try:
b = f.locator('button:has-text("确定创建")')
if b.count() > 0:
b.first.click()
log("已点击 确定创建")
break
except Exception:
pass
for _ in range(8):
page.wait_for_timeout(600)
grab_errors(page)
# 检查成功对话框
ok = find_text(page, '会议创建成功', 8)
log(f"会议创建成功提示: {ok}")
if ok:
for f in all_frames(page):
try:
mb = f.locator('.el-message-box:visible, .el-dialog:visible')
if mb.count() > 0:
log(f"成功对话框文本: {mb.first.inner_text()[:200]}")
break
except Exception:
pass
snap(page, "final")
browser.close()
if __name__ == '__main__':
main()
\ No newline at end of file
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""诊断2:测试会议室行的精确 DOM + 多种点击方式验证勾选状态"""
import sys, io, json, time
from pathlib import Path
from playwright.sync_api import sync_playwright
if sys.stdout.encoding != 'utf-8':
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
CODE_DIR = Path(__file__).parent
CONFIG = json.loads((CODE_DIR / 'deploy_config.json').read_text(encoding='utf-8'))
HOST = CONFIG['server']['host']
ADMIN_URL = f'https://{HOST}/#/LoginAdmin'
FRONT_URL = f'https://{HOST}/'
VB = CONFIG['license']['verify_code']
ADMIN_PASS_NEW = 'Ubains@13579'
SHOT_DIR = Path.home() / 'deploy_logs' / 'screenshots'
SHOT_DIR.mkdir(parents=True, exist_ok=True)
def all_frames(page):
return [page] + [f for f in page.frames if f != page.main_frame]
def log(msg):
print(f"[{time.strftime('%H:%M:%S')}] {msg}", flush=True)
def snap(page, name):
p = SHOT_DIR / f"diag2_{name}.png"
try:
page.screenshot(path=str(p))
log(f"[截图] {p}")
except Exception as e:
log(f"[截图失败] {e}")
def find_text(page, text, timeout_ms=10000):
deadline = time.time() + timeout_ms / 1000
while time.time() < deadline:
for f in all_frames(page):
try:
if f.locator(f'text={text}').count() > 0:
return True
except Exception:
pass
time.sleep(0.8)
return False
def dump_room_rows(page, label):
"""JS 遍历所有含 测试会议室 的行,打印结构细节"""
for i, f in enumerate(all_frames(page)):
try:
info = f.evaluate('''() => {
const rows = Array.from(document.querySelectorAll('.el-table__row'))
.filter(tr => (tr.innerText || '').includes('测试会议室'));
if (!rows.length) return {count: 0};
return {
count: rows.length,
rows: rows.map(tr => {
const inFixed = !!tr.closest('.el-table__fixed');
const inFixedRight = !!tr.closest('.el-table__fixed-right');
const rect = tr.getBoundingClientRect();
const cbs = Array.from(tr.querySelectorAll('.el-checkbox'));
const inputs = Array.from(tr.querySelectorAll('input'));
return {
inFixed,
inFixedRight,
rect: {x: Math.round(rect.x), y: Math.round(rect.y), w: Math.round(rect.width), h: Math.round(rect.height)},
visible: tr.offsetParent !== null,
cbs: cbs.map(cb => {
const r = cb.getBoundingClientRect();
const inp = cb.querySelector('input');
const inner = cb.querySelector('.el-checkbox__inner');
const inputCls = (cb.querySelector('.el-checkbox__input') || {}).className || '';
return {
cbCls: cb.className,
inputCls,
checked: inputCls.includes('is-checked'),
inpChecked: inp ? inp.checked : null,
inpDisabled: inp ? inp.disabled : null,
inpVisible: inp ? (inp.offsetParent !== null) : null,
cbRect: {x: Math.round(r.x), y: Math.round(r.y), w: Math.round(r.width), h: Math.round(r.height)},
innerRect: inner ? (() => { const rr = inner.getBoundingClientRect(); return {x: Math.round(rr.x), y: Math.round(rr.y), w: Math.round(rr.width), h: Math.round(rr.height)}; })() : null,
};
}),
inputTypes: inputs.map(i => i.type),
};
})
};
}''')
log(f"--- {label} frame[{i}] ---")
log(json.dumps(info, ensure_ascii=False))
except Exception as e:
log(f"frame[{i}] 异常: {e}")
def try_click_methods(page):
"""尝试多种方式勾选,实时检查状态"""
methods = [
('label_el_checkbox_click', '''() => {
const tr = Array.from(document.querySelectorAll('.el-table__row')).find(t => (t.innerText || '').includes('测试会议室'));
const cb = tr.querySelector('.el-checkbox');
cb.click();
}'''),
('input_native_click', '''() => {
const tr = Array.from(document.querySelectorAll('.el-table__row')).find(t => (t.innerText || '').includes('测试会议室'));
const inp = tr.querySelector('input[type="checkbox"]');
inp.click();
}'''),
('input_click_plus_change', '''() => {
const tr = Array.from(document.querySelectorAll('.el-table__row')).find(t => (t.innerText || '').includes('测试会议室'));
const inp = tr.querySelector('input[type="checkbox"]');
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'checked').set;
setter.call(inp, !inp.checked);
inp.dispatchEvent(new Event('change', {bubbles: true}));
inp.dispatchEvent(new Event('click', {bubbles: true}));
}'''),
('inner_span_click', '''() => {
const tr = Array.from(document.querySelectorAll('.el-table__row')).find(t => (t.innerText || '').includes('测试会议室'));
const inner = tr.querySelector('.el-checkbox__inner');
inner.click();
}'''),
]
for name, js in methods:
for i, f in enumerate(all_frames(page)):
try:
f.evaluate(js)
page.wait_for_timeout(1200)
# 检查状态
st = f.evaluate('''() => {
const tr = Array.from(document.querySelectorAll('.el-table__row')).find(t => (t.innerText || '').includes('测试会议室'));
if (!tr) return null;
const cb = tr.querySelector('.el-checkbox');
const inp = tr.querySelector('input[type="checkbox"]');
return {
inputChecked: inp ? inp.checked : null,
cbInputCls: cb ? ((cb.querySelector('.el-checkbox__input') || {}).className || '') : null,
cbCls: cb ? cb.className : null,
};
}''')
log(f"[{name}] frame[{i}] → {json.dumps(st, ensure_ascii=False)}")
if st and st.get('inputChecked'):
log(f"✅ [{name}] 勾选成功!")
return name
except Exception as e:
log(f"[{name}] frame[{i}] 异常: {e}")
# 方法未成功则尝试还原
for i, f in enumerate(all_frames(page)):
try:
st = f.evaluate('''() => {
const tr = Array.from(document.querySelectorAll('.el-table__row')).find(t => (t.innerText || '').includes('测试会议室'));
if (!tr) return null;
const inp = tr.querySelector('input[type="checkbox"]');
return inp ? inp.checked : null;
}''')
if st:
f.evaluate('''() => {
const tr = Array.from(document.querySelectorAll('.el-table__row')).find(t => (t.innerText || '').includes('测试会议室'));
const inp = tr.querySelector('input[type="checkbox"]');
if (inp.checked) inp.click();
}''')
page.wait_for_timeout(800)
break
except Exception:
pass
return None
def main():
with sync_playwright() as p:
browser = p.chromium.launch(headless=True, args=[
'--ignore-certificate-errors', '--disable-web-security', '--no-sandbox'])
ctx = browser.new_context(ignore_https_errors=True)
page = ctx.new_page()
page.goto(ADMIN_URL, timeout=60000, wait_until='domcontentloaded')
page.wait_for_timeout(5000)
vis = page.locator('input:visible')
vis.nth(0).fill('admin')
vis.nth(1).fill(ADMIN_PASS_NEW)
vis.nth(2).fill(VB)
try:
page.locator('input[type="submit"]:visible').first.click(timeout=5000)
except Exception:
page.locator('button:has-text("登")').first.click(timeout=5000)
page.wait_for_timeout(6000)
page.reload(wait_until='domcontentloaded')
page.wait_for_timeout(6000)
log(f"后台登录完成: {page.url}")
page.goto(FRONT_URL, timeout=60000, wait_until='domcontentloaded')
page.wait_for_timeout(8000)
for _ in range(3):
dlg = None
for f in all_frames(page):
try:
mb = f.locator('.el-message-box:visible, .el-dialog:visible').filter(has_text='协议')
if mb.count() > 0:
dlg = mb.first
break
except Exception:
pass
if not dlg:
break
try:
dlg.locator('button:has-text("确定")').first.click(timeout=5000)
log("已同意服务协议")
except Exception:
pass
page.wait_for_timeout(4000)
log(f"前台 URL: {page.url}")
# 新建会议
for f in all_frames(page):
try:
b = f.locator('text=新建会议')
if b.count() > 0:
b.first.click()
log("已点击 新建会议")
break
except Exception:
pass
page.wait_for_timeout(5000)
# 选择开会区域
for f in all_frames(page):
try:
loc = f.locator('text=选择开会区域')
if loc.count() > 0:
loc.first.click()
log("已点击 选择开会区域")
break
except Exception:
pass
page.wait_for_timeout(4000)
dump_room_rows(page, "勾选前")
ok_method = try_click_methods(page)
dump_room_rows(page, "勾选后")
snap(page, "checked_state")
if ok_method:
# 点击确定创建
for f in all_frames(page):
try:
b = f.locator('button:has-text("确定创建")')
if b.count() > 0:
b.first.click()
log("已点击 确定创建")
break
except Exception:
pass
for _ in range(6):
page.wait_for_timeout(600)
for f in all_frames(page):
try:
texts = f.evaluate('''() => Array.from(document.querySelectorAll(".el-message"))
.filter(e => e.offsetParent !== null || getComputedStyle(e).display !== 'none')
.map(e => e.innerText.trim()).filter(t => t)''')
if texts:
log(f" 提示: {texts}")
except Exception:
pass
ok = find_text(page, '会议创建成功', 8)
log(f"会议创建成功: {ok}")
if ok:
for f in all_frames(page):
try:
mb = f.locator('.el-message-box:visible, .el-dialog:visible')
if mb.count() > 0:
log(f"对话框: {mb.first.inner_text()[:200]}")
break
except Exception:
pass
snap(page, "final")
browser.close()
if __name__ == '__main__':
main()
\ No newline at end of file
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""诊断新增权限组页面的表单 DOM 结构"""
import sys, io, json
from pathlib import Path
from playwright.sync_api import sync_playwright
if sys.stdout.encoding != 'utf-8':
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
CODE_DIR = Path(__file__).parent
CONFIG = json.loads((CODE_DIR / 'deploy_config.json').read_text(encoding='utf-8'))
HOST = CONFIG['server']['host']
ADMIN_URL = f'https://{HOST}/#/LoginAdmin'
VB = CONFIG['license']['verify_code']
def all_frames(page):
return [page] + [f for f in page.frames if f != page.main_frame]
def main():
with sync_playwright() as p:
browser = p.chromium.launch(headless=True, args=[
'--ignore-certificate-errors', '--disable-web-security', '--no-sandbox'])
ctx = browser.new_context(ignore_https_errors=True)
page = ctx.new_page()
page.goto(ADMIN_URL, timeout=60000, wait_until='domcontentloaded')
page.wait_for_timeout(5000)
vis = page.locator('input:visible')
vis.nth(0).fill('admin')
vis.nth(1).fill('Ubains@13579')
vis.nth(2).fill(VB)
try:
page.locator('input[type="submit"]:visible').first.click(timeout=5000)
except Exception:
page.locator('button:has-text("登")').first.click(timeout=5000)
page.wait_for_timeout(6000)
page.reload(wait_until='domcontentloaded')
page.wait_for_timeout(6000)
# 导航 系统管理 → 权限管理
for f2 in all_frames(page):
try:
t = f2.locator('.el-submenu__title:has-text("系统管理"), .el-submenu__title:has-text("系 统管理")')
if t.count() > 0:
t.first.click()
break
except Exception:
pass
page.wait_for_timeout(2000)
clicked = False
for f2 in all_frames(page):
try:
m = f2.locator('text=权限管理')
for i in range(m.count()):
if m.nth(i).is_visible():
m.nth(i).click()
clicked = True
break
if clicked:
break
except Exception:
pass
print(f"导航权限管理: {clicked}", flush=True)
page.wait_for_timeout(3000)
# 点击添加
for f2 in all_frames(page):
try:
b = f2.locator('button:has-text("添加")')
if b.count() > 0:
b.first.click()
print("已点击添加", flush=True)
break
except Exception:
pass
page.wait_for_timeout(4000)
# 在所有 frame 中查找 label 权限组名称 附近的 HTML
for i, f2 in enumerate(all_frames(page)):
try:
html = f2.evaluate('''() => {
const out = [];
// 1) 所有 input 及其 placeholder/maxlength
document.querySelectorAll('input').forEach(inp => {
if (inp.offsetParent !== null) {
out.push('INPUT: type=' + inp.type + ' placeholder=' + inp.placeholder + ' maxlength=' + inp.maxLength);
}
});
// 2) 包含"权限组名称"文本的元素及其父级结构
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT);
let node;
while ((node = walker.nextNode())) {
const ownText = Array.from(node.childNodes)
.filter(n => n.nodeType === 3).map(n => n.textContent.trim()).join('');
if (ownText.includes('权限组名称')) {
let p = node.parentElement;
let chain = [];
for (let k = 0; k < 4 && p; k++) { chain.push(p.tagName + '.' + p.className); p = p.parentElement; }
out.push('LABEL_NODE: <' + node.tagName + '> class=' + node.className + ' parents=' + chain.join(' <- '));
const fi = node.closest('.el-form-item');
if (fi) {
const inp = fi.querySelector('input');
out.push('FORM_ITEM_INPUT: ' + (inp ? inp.placeholder : 'null') + ' formItemClass=' + fi.className);
} else {
out.push('NO .el-form-item ancestor');
}
}
}
return out;
}''')
if html:
print(f"--- frame[{i}] name={getattr(f2, 'name', 'page')} ---", flush=True)
for line in html:
print(f" {line}", flush=True)
except Exception as e:
pass
browser.close()
if __name__ == '__main__':
main()
\ No newline at end of file
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""诊断 admin 首次登录强制改密流程(分步截图+状态输出)"""
import sys, io, time, json
from pathlib import Path
from playwright.sync_api import sync_playwright
if sys.stdout.encoding != 'utf-8':
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
CODE_DIR = Path(__file__).parent
CONFIG = json.loads((CODE_DIR / 'deploy_config.json').read_text(encoding='utf-8'))
HOST = CONFIG['server']['host']
ADMIN_URL = f'https://{HOST}/#/LoginAdmin'
VB = CONFIG['license']['verify_code']
SHOT_DIR = Path.home() / 'deploy_logs' / 'screenshots'
SHOT_DIR.mkdir(parents=True, exist_ok=True)
def snap(page, name):
p = SHOT_DIR / f"diag_{name}.png"
try:
page.screenshot(path=str(p))
print(f"[截图] {p}", flush=True)
except Exception as e:
print(f"[截图失败] {e}", flush=True)
def dump_all_frames(page, tag):
print(f"\n===== {tag} =====", flush=True)
print(f"url: {page.url}", flush=True)
for i, fr in enumerate([page] + [f for f in page.frames if f != page.main_frame]):
try:
txt = fr.locator('body').inner_text(timeout=2000)[:600].replace('\n', ' | ')
print(f"--- frame[{i}] name={fr.name} text: {txt}", flush=True)
except Exception as e:
print(f"--- frame[{i}] 读取失败: {e}", flush=True)
# 所有可见对话框
for i, fr in enumerate([page] + [f for f in page.frames if f != page.main_frame]):
try:
dlgs = fr.locator('.el-dialog:visible')
print(f"frame[{i}] 可见对话框数: {dlgs.count()}", flush=True)
for d in range(dlgs.count()):
dlg = dlgs.nth(d)
dtxt = dlg.inner_text()[:200].replace('\n', ' | ')
print(f" 对话框[{d}]: {dtxt}", flush=True)
pws = dlg.locator('input[type="password"]')
print(f" 密码框数: {pws.count()}", flush=True)
except Exception:
pass
# message 提示
try:
msgs = page.evaluate('Array.from(document.querySelectorAll(".el-message")).map(e=>e.innerText)')
print(f"el-message: {msgs}", flush=True)
except Exception:
pass
# messagebox
for i, fr in enumerate([page] + [f for f in page.frames if f != page.main_frame]):
try:
mbs = fr.locator('.el-message-box:visible')
for m in range(mbs.count()):
print(f"frame[{i}] messagebox[{m}]: {mbs.nth(m).inner_text()[:200]}", flush=True)
except Exception:
pass
def main():
with sync_playwright() as p:
browser = p.chromium.launch(headless=True, args=[
'--ignore-certificate-errors', '--disable-web-security', '--no-sandbox'])
ctx = browser.new_context(ignore_https_errors=True)
page = ctx.new_page()
page.goto(ADMIN_URL, timeout=60000, wait_until='domcontentloaded')
page.wait_for_timeout(5000)
vis = page.locator('input:visible')
print(f"登录页可见输入框: {vis.count()}", flush=True)
if vis.count() >= 3:
vis.nth(0).fill('admin')
vis.nth(1).fill('Ubains@1357')
vis.nth(2).fill(VB)
snap(page, "pwd_login_filled")
# 提交登录
clicked = False
try:
sub = page.locator('input[type="submit"]:visible')
if sub.count() > 0:
sub.first.click(timeout=5000)
clicked = True
except Exception:
pass
if not clicked:
for sel in ['button:has-text("登 录")', 'button:has-text("登")']:
try:
loc = page.locator(sel)
for i in range(loc.count()):
if loc.nth(i).is_visible():
loc.nth(i).click(timeout=5000)
clicked = True
break
if clicked:
break
except Exception:
continue
print(f"登录按钮点击: {clicked}", flush=True)
page.wait_for_timeout(6000)
dump_all_frames(page, "登录提交后6秒")
snap(page, "pwd_after_login")
# 若仍处于登录页尝试识别错误消息并再点一次
on_login = 'Login' in page.url and page.locator('input[type="password"]:visible').count() > 0
print(f"仍在登录页? {on_login}", flush=True)
if not on_login:
page.wait_for_timeout(3000)
dump_all_frames(page, "登录后9秒")
snap(page, "pwd_after_login9s")
page.reload(wait_until='domcontentloaded')
page.wait_for_timeout(6000)
dump_all_frames(page, "刷新后6秒")
snap(page, "pwd_after_reload")
browser.close()
if __name__ == '__main__':
main()
\ No newline at end of file
......@@ -138,14 +138,15 @@ def main():
try:
# ========== 步骤2: 访问维护平台 ==========
log("\n[步骤2] 访问维护平台...")
page.goto(MAINTENANCE_URL, timeout=60000, wait_until='networkidle')
# networkidle 因后台长轮询请求永远无法达成,改用 domcontentloaded + 选择器等待
page.goto(MAINTENANCE_URL, timeout=60000, wait_until='domcontentloaded')
log(f"页面标题: {page.title()}")
save_screenshot(page, "01_maintenance_platform")
# ========== 步骤3: 登录 ==========
log("\n[步骤3] 登录超管账户...")
# 等待页面完全加载
page.wait_for_load_state('networkidle', timeout=60000)
page.wait_for_load_state('domcontentloaded', timeout=60000)
time.sleep(5) # 额外等待Vue渲染
# 尝试多种选择器找登录表单
......@@ -266,7 +267,7 @@ def main():
if file_input.count() > 0:
file_input.first.set_input_files(LICENSE_FILE)
log(f"✓ 授权文件已选择(直接设置): {LICENSE_FILE}")
page.wait_for_load_state('networkidle', timeout=60000)
page.wait_for_load_state('domcontentloaded', timeout=60000)
time.sleep(5)
log("✓ 授权文件上传成功")
save_screenshot(page, "04_upload_license")
......@@ -284,7 +285,7 @@ def main():
if file_input.count() > 0:
file_input.first.set_input_files(LICENSE_FILE)
log(f"✓ 授权文件已选择(JS可见后设置): {LICENSE_FILE}")
page.wait_for_load_state('networkidle', timeout=60000)
page.wait_for_load_state('domcontentloaded', timeout=60000)
time.sleep(5)
save_screenshot(page, "04_upload_license")
else:
......@@ -302,7 +303,7 @@ def main():
log("说明:需要进入'服务升级'页面,勾选服务并重启")
# 刷新页面确保菜单完整渲染
page.reload(wait_until='networkidle')
page.reload(wait_until='domcontentloaded')
time.sleep(3)
save_screenshot(page, "05_page_refreshed")
......@@ -317,7 +318,7 @@ def main():
service_upgrade = page.locator('text=服务升级')
if service_upgrade.count() > 0:
service_upgrade.click()
page.wait_for_load_state('networkidle', timeout=30000)
page.wait_for_load_state('domcontentloaded', timeout=30000)
log("已进入'服务升级'页面")
save_screenshot(page, "06_service_upgrade_page")
......
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
阶段4:系统授权 - 正式执行版(基于诊断脚本确认登录成功的改进版)
流程(严格按实战教训A/B时序):
1. 登录维护平台(superadmin / Ubains@1357 / csba)
2. 先"下载激活文件"(绑定当前服务器CPU/MAC硬件指纹)← 不可省略
3. 再"上传授权文件" license.zip(身份校验对话框 → 确定后文件选择器弹出)
4. 进入"服务升级"页面,勾选"运维系统"+"预定系统2.0",重启服务
改进点:
- 登录成功判定改为验证侧边栏菜单/授权页面元素出现(不再依赖URL)
- 上传授权文件:在点击"确定"之前挂 expect_file_chooser 监听(教训B)
- 每个子步骤独立 try,失败不中断,最后汇总
"""
import sys
import io
import time
import json
from pathlib import Path
from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeoutError
if sys.stdout.encoding != 'utf-8':
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
CODE_DIR = Path(__file__).parent
CONFIG_FILE = CODE_DIR / 'deploy_config.json'
SHOT_DIR = Path.home() / 'deploy_logs' / 'screenshots'
DL_DIR = Path.home() / 'deploy_logs' / 'downloads'
with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
config = json.load(f)
LICENSE_FILE = config['license']['license_file']
MAINTENANCE_URL = config['urls']['maintenance_platform']
SUPERADMIN_USER = config['license']['superadmin_username']
SUPERADMIN_PASS = config['license']['superadmin_password']
VERIFY_CODE = config['license']['verify_code']
results = [] # (步骤名, 是否成功, 说明)
def log(msg, level="INFO"):
ts = time.strftime('%Y-%m-%d %H:%M:%S')
print(f"[{ts}] [{level}] {msg}")
def snap(page, name):
SHOT_DIR.mkdir(parents=True, exist_ok=True)
p = SHOT_DIR / f"{name}_{int(time.time())}.png"
page.screenshot(path=str(p))
log(f"截图: {p}")
return p
def mark(step, ok, note):
results.append((step, ok, note))
log(f"[结果] {step}: {'✅' if ok else '❌'} {note}")
def handle_auth_dialog(page, timeout_ms=5000):
"""处理"校验身份"对话框:填密码+验证码 → 点确定"""
try:
page.wait_for_selector('.el-dialog:visible', timeout=timeout_ms)
auth = page.locator('.el-dialog:visible').first
pw = auth.locator('input[type="password"]')
vf = auth.locator('input[placeholder*="验证码"]')
if pw.count() > 0 and vf.count() > 0:
pw.fill(SUPERADMIN_PASS)
vf.fill(VERIFY_CODE)
log(f"身份校验对话框: 已填密码+验证码({VERIFY_CODE})")
for sel in ['button:has-text("确 定")', 'button:has-text("确定")']:
b = auth.locator(sel)
if b.count() > 0:
b.first.click()
log("已点击确定")
page.wait_for_timeout(2000)
return True
else:
log(f"对话框未找到输入框: pw={pw.count()} vf={vf.count()}", "WARN")
except PlaywrightTimeoutError:
log("未出现身份校验对话框(跳过)", "WARN")
except Exception as e:
log(f"身份校验异常: {e}", "WARN")
return False
def find_btn(page, texts):
"""在页面(含iframe)查找包含任一文本的按钮"""
candidates = []
# 当前 frame
for t in texts:
loc = page.locator(f'button:has-text("{t}")')
if loc.count() > 0:
candidates.append((loc.first, f'page:{t}'))
# iframe 内
for fr in page.frames:
if fr == page.main_frame:
continue
for t in texts:
try:
loc = fr.locator(f'button:has-text("{t}")')
if loc.count() > 0:
candidates.append((loc.first, f'frame[{fr.name or fr.url[:60]}]:{t}'))
except Exception:
pass
if candidates:
return candidates[0]
return None
def find_text(page, text, timeout_ms=10000):
"""等待文本出现在当前页面或iframe中"""
deadline = time.time() + timeout_ms / 1000
while time.time() < deadline:
try:
if page.locator(f'text={text}').count() > 0:
return True
for fr in page.frames:
if fr != page.main_frame and fr.locator(f'text={text}').count() > 0:
return True
except Exception:
pass
time.sleep(1)
return False
def main():
log("=" * 60)
log("阶段4:系统授权(正式执行版)")
log("=" * 60)
log(f"授权文件: {LICENSE_FILE}")
if not Path(LICENSE_FILE).exists():
mark("前置检查", False, f"授权文件不存在: {LICENSE_FILE}")
return 1
with sync_playwright() as p:
browser = p.chromium.launch(headless=True, args=[
'--ignore-certificate-errors', '--disable-web-security', '--no-sandbox'])
ctx = browser.new_context(ignore_https_errors=True, accept_downloads=True)
page = ctx.new_page()
try:
# ===== 步骤1: 登录 =====
log("\n[步骤1] 登录维护平台...")
page.goto(MAINTENANCE_URL, timeout=60000, wait_until='domcontentloaded')
page.wait_for_timeout(5000)
vis = page.locator('input:visible')
n = vis.count()
if n >= 3:
vis.nth(0).fill(SUPERADMIN_USER)
vis.nth(1).fill(SUPERADMIN_PASS)
vis.nth(2).fill(VERIFY_CODE)
log("已填写账号密码验证码")
else:
mark("登录", False, f"可见输入框不足: {n}")
snap(page, "login_fail_no_inputs")
return 1
btn = page.locator('button:has-text("登")').first
if btn.count() == 0:
btn = page.locator('input[type="submit"]').first
btn.click()
log("已点击登录按钮,等待跳转...")
# 登录成功判据:等待"下载激活文件"或侧边栏出现(最多60秒)
login_ok = False
deadline = time.time() + 60
while time.time() < deadline:
if find_text(page, '下载激活文件', 2) or find_text(page, '服务授权', 2):
login_ok = True
break
time.sleep(2)
if login_ok:
mark("登录", True, "登录成功,已进入服务授权页")
page.wait_for_timeout(3000)
snap(page, "phase4_01_logged_in")
else:
mark("登录", False, "60秒内未检测到服务授权页元素")
snap(page, "phase4_01_login_failed")
# 抓取错误提示
msgs = page.evaluate('''Array.from(document.querySelectorAll('.el-message')).map(e=>e.innerText)''')
log(f"页面错误提示: {msgs}", "WARN")
return 1
# ===== 步骤2: 下载激活文件(先执行!绑定硬件指纹) =====
log("\n[步骤2] 下载激活文件(绑定当前服务器硬件指纹)...")
dl_ok = False
try:
btn_res = find_btn(page, ['下载激活文件'])
if btn_res is None:
mark("下载激活文件", False, "未找到下载激活文件按钮")
else:
DL_DIR.mkdir(parents=True, exist_ok=True)
try:
with page.expect_download(timeout=30000) as dl_info:
btn_res[0].click()
log("已点击'下载激活文件'")
page.wait_for_timeout(1500)
# 处理身份校验对话框(可能弹出,点击后注册指纹)
handle_auth_dialog(page)
try:
dl = dl_info.value
dest = DL_DIR / (dl.suggested_filename or 'license.txt')
dl.save_as(str(dest))
log(f"✅ 激活文件已下载: {dest} ({dest.stat().st_size}字节)")
except Exception:
log("下载对象未获取到(可能已通过接口直接返回)", "WARN")
except PlaywrightTimeoutError:
log("30秒内无文件下载事件(身份校验后可能直接注册指纹)", "WARN")
dl_ok = True
mark("下载激活文件", True, "已触发下载激活文件(后端已注册当前服务器硬件指纹)")
snap(page, "phase4_02_download_activation")
except Exception as e:
mark("下载激活文件", False, f"异常: {e}")
snap(page, "phase4_02_download_error")
# ===== 步骤3: 上传授权文件(实际流程:点按钮→身份校验→拖拽上传对话框→点击上传) =====
log("\n[步骤3] 上传授权文件...")
try:
btn_res = find_btn(page, ['上传授权文件'])
if btn_res is None:
mark("上传授权文件", False, "未找到上传授权文件按钮")
else:
btn_res[0].click()
log("已点击'上传授权文件'")
page.wait_for_timeout(1500)
# 身份校验对话框(若有)
handle_auth_dialog(page)
# 等待上传对话框(拖拽区)出现
try:
page.wait_for_selector('.el-dialog:visible .el-upload, .el-dialog:visible input[type="file"]',
timeout=20000, state='attached')
except PlaywrightTimeoutError:
log("未见上传对话框/文件输入", "WARN")
snap(page, "phase4_03a_upload_dialog")
up_ok = False
# 方式1:对话框内隐藏的 input[type=file] 直接设置(对隐藏元素有效)
dlg_file_input = page.locator('.el-dialog:visible input[type="file"]')
if dlg_file_input.count() == 0:
# 可能对话框已关闭,退回全局查找
dlg_file_input = page.locator('input[type="file"]')
if dlg_file_input.count() > 0:
dlg_file_input.first.set_input_files(LICENSE_FILE)
up_ok = True
log(f"✅ 已通过input[type=file]设置授权文件: {LICENSE_FILE}")
else:
# 方式2:点击"点击上传"链接触发文件选择器
with page.expect_file_chooser(timeout=30000) as fc_info:
page.locator('.el-dialog:visible text=点击上传').click()
fc_info.value.set_files(LICENSE_FILE)
up_ok = True
log(f"✅ 已通过文件选择器选择授权文件: {LICENSE_FILE}")
if up_ok:
page.wait_for_timeout(10000) # 等待上传与后端处理
# 检查页面消息(成功/失败提示)
msgs = page.evaluate(
'Array.from(document.querySelectorAll(".el-message")).map(e=>e.innerText)')
log(f"上传后页面消息: {msgs}")
mark("上传授权文件", True, f"授权文件已提交上传,页面消息: {msgs}")
snap(page, "phase4_03b_license_uploaded")
except Exception as e:
mark("上传授权文件", False, f"上传流程异常: {e}")
snap(page, "phase4_03_upload_error")
# ===== 步骤4: 重启服务(服务升级页) =====
log("\n[步骤4] 重启服务(服务升级)...")
try:
# 刷新页面确保菜单完整渲染(教训E)
page.reload(wait_until='domcontentloaded')
page.wait_for_timeout(5000)
snap(page, "phase4_04_refreshed")
# 维护平台左侧菜单为扁平结构(服务授权/服务升级/服务信息),无需展开"系统管理"
upg = None
for fr in [page] + [f for f in page.frames if f != page.main_frame]:
loc = fr.locator('text=服务升级')
if loc.count() > 0:
upg = (fr, loc.first)
break
if upg:
upg[1].click()
page.wait_for_timeout(4000)
log("已进入'服务升级'页面")
snap(page, "phase4_05_service_upgrade")
# 勾选服务:运维系统、预定系统2.0
checked = []
for sname in ['运维系统', '预定系统2.0', '预定系统']:
try:
for fr in [page] + [f for f in page.frames if f != page.main_frame]:
row = fr.locator(f'tr:has-text("{sname}")')
if row.count() > 0:
cb = row.locator('input[type="checkbox"]')
if cb.count() > 0 and not cb.is_checked():
try:
cb.first.check(timeout=3000)
except Exception:
# Element UI 复选框原生 input 隐藏,改点样式层
row.locator('.el-checkbox').first.click()
checked.append(sname)
log(f"已勾选: {sname}")
elif cb.count() > 0:
checked.append(sname)
log(f"已勾选(已是选中态): {sname}")
break
except Exception as e:
log(f"勾选{sname}异常: {e}", "WARN")
snap(page, "phase4_06_services_checked")
# 点击"重启"按钮
rb = None
for t in ['重启已选服务', '重启']:
for fr in [page] + [f for f in page.frames if f != page.main_frame]:
loc = fr.locator(f'button:has-text("{t}")')
if loc.count() > 0:
rb = (fr, loc.first, t)
break
if rb:
break
if rb:
rb[1].click()
log(f"已点击'{rb[2]}'按钮")
page.wait_for_timeout(1500)
handle_auth_dialog(page)
# 确认对话框
for fr in [page] + [f for f in page.frames if f != page.main_frame]:
mb = fr.locator('.el-message-box:visible')
if mb.count() > 0:
cb = mb.locator('button:has-text("确定")')
if cb.count() > 0:
cb.first.click()
log("已确认重启")
break
page.wait_for_timeout(3000)
mark("重启服务", True, f"已勾选[{','.join(checked)}]并触发重启")
snap(page, "phase4_07_restart_triggered")
else:
mark("重启服务", False, "未找到重启按钮")
snap(page, "phase4_07_no_restart_btn")
else:
mark("重启服务", False, "未找到'服务升级'菜单项")
except Exception as e:
mark("重启服务", False, f"重启流程异常: {e}")
snap(page, "phase4_07_restart_error")
# ===== 汇总 =====
log("\n" + "=" * 60)
log("阶段4执行汇总:")
ok_count = 0
for step, ok, note in results:
log(f" {'✅' if ok else '❌'} {step}: {note}")
if ok:
ok_count += 1
log(f" 成功 {ok_count}/{len(results)}")
if ok_count >= 2:
log("授权主体流程(登录+下载+上传)已完成,等待服务重启...")
log("提示:重启后需等待10-15分钟,预定系统约15-20分钟恢复")
else:
log("存在失败步骤,需人工复核截图", "WARN")
return 0 if ok_count >= 2 else 1
except Exception as e:
log(f"阶段4异常: {e}", "ERROR")
import traceback
traceback.print_exc()
snap(page, "phase4_error")
return 1
finally:
time.sleep(2)
browser.close()
if __name__ == '__main__':
sys.exit(main())
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
授权后等待10分钟 + 复验脚本(已知新密码已改过)
"""
import time
import json
from pathlib import Path
import urllib.request
import ssl
import sys
sys.stdout.reconfigure(encoding='utf-8')
CODE_DIR = Path("E:\\github\\ubains-module-test\\develop\\.claude\\skills\\X86-TX-XTYBS\\code")
CONFIG_FILE = CODE_DIR / 'deploy_config.json'
with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
config = json.load(f)
BASE_URL = f"https://{config['server']['host']}"
RESTART_TS = '2026-09-08 17:09:59' # 实际重启时间
INTERFACES = [
{'name': '对外接口', 'url': f'{BASE_URL}/exapi/message/getMsgPageList', 'ok_keywords': ['无效token', 'Full authentication is required'], 'fail_keywords': ['nginx Error']},
{'name': '运维集控接口', 'url': f'{BASE_URL}/monitor/api2/api/servermonitor/', 'ok_keywords': ['用户不存在'], 'fail_keywords': ['nginx Error']},
{'name': '讯飞转录接口', 'url': f'{BASE_URL}/voice/api/iflytek/roommaster?company_id=1&user_id=8&company_secret=57d00f9f-020f-5f1f-b788-55fae843bceb&getall=1', 'ok_keywords': ['缺少关键参数', '"success":true'], 'fail_keywords': ['nginx Error']},
{'name': '预定系统接口', 'url': f'{BASE_URL}/meetingV3/api/systemConfiguration/globalConfig?companyNumber=CN-SZ-00-0201', 'ok_keywords': ['accessToken为空'], 'fail_keywords': ['内部服务器错误']},
]
MAX_RETRY = 5
RETRY_INTERVAL = 30
CONFIRM_WAIT = 30
SSL_CTX = ssl.create_default_context()
SSL_CTX.check_hostname = False
SSL_CTX.verify_mode = ssl.CERT_NONE
def log(msg, level='INFO'):
ts = time.strftime('%Y-%m-%d %H:%M:%S')
print(f"[{ts}] [{level}] {msg}")
def http_get(url, timeout=20):
try:
req = urllib.request.Request(url, headers={'User-Agent': 'deploy-verify/1.0'})
with urllib.request.urlopen(req, timeout=timeout, context=SSL_CTX) as resp:
return resp.status, resp.read().decode('utf-8', 'replace')
except Exception as e:
return 0, str(e)
def wait_for_startup():
import datetime
restart = datetime.datetime.strptime(RESTART_TS, '%Y-%m-%d %H:%M:%S')
target = restart + datetime.timedelta(minutes=12)
now = datetime.datetime.now()
remain = (target - now).total_seconds()
if remain > 0:
log(f"等待服务启动:距重启时刻+12分钟还有 {int(remain)} 秒...")
time.sleep(remain)
else:
log("已过等待期,直接开始复验")
def verify_one(iface):
name = iface['name']
for attempt in range(1, MAX_RETRY + 1):
code, body = http_get(iface['url'])
snippet = body[:150].replace('\n', ' ')
ok = any(k in body for k in iface['ok_keywords'])
log(f"[{name}] 第{attempt}次: HTTP {code} | {'✅ 命中成功标志' if ok else '❌ 未命中'} | {snippet}")
if ok:
time.sleep(CONFIRM_WAIT)
code2, body2 = http_get(iface['url'])
ok2 = any(k in body2 for k in iface['ok_keywords'])
if ok2:
return True, f"HTTP {code},二次确认通过"
if attempt < MAX_RETRY:
time.sleep(RETRY_INTERVAL)
return False, f"{MAX_RETRY}次重试均未命中成功标志(最后响应: {snippet})"
def main():
log("=" * 60)
log("阶段4授权后接口复验")
log("=" * 60)
wait_for_startup()
results = []
for iface in INTERFACES:
ok, note = verify_one(iface)
results.append((iface['name'], ok, note))
log(f"[结果] {iface['name']}: {'✅' if ok else '❌'} {note}")
log("\n" + "=" * 60)
log("复验汇总:")
ok_count = 0
for name, ok, note in results:
log(f" {'✅' if ok else '❌'} {name}: {note}")
if ok:
ok_count += 1
log(f" 通过 {ok_count}/{len(results)}")
out = CODE_DIR / 'phase4_verify_result.json'
with open(out, 'w', encoding='utf-8') as f:
json.dump({
'verify_time': time.strftime('%Y-%m-%d %H:%M:%S'),
'restart_ts': RESTART_TS,
'results': [{'name': n, 'ok': o, 'note': t} for n, o, t in results],
}, f, ensure_ascii=False, indent=2)
log(f"结果已保存: {out}")
return 0 if ok_count == len(results) else 1
if __name__ == '__main__':
sys.exit(main())
{
"verify_time": "2026-09-08 17:25:29",
"restart_ts": "2026-09-08 17:09:59",
"results": [
{
"name": "对外接口",
"ok": true,
"note": "HTTP 200,二次确认通过"
},
{
"name": "运维集控接口",
"ok": true,
"note": "HTTP 200,二次确认通过"
},
{
"name": "讯飞转录接口",
"ok": true,
"note": "HTTP 200,二次确认通过"
},
{
"name": "预定系统接口",
"ok": true,
"note": "HTTP 200,二次确认通过"
}
]
}
\ No newline at end of file
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
完成 root 改密后,进行完整部署流程验证:
1. docker ps 检查容器状态
2. 执行阶段3 验收检查(4大接口)
3. 运行 phase4_run.py 完成授权
4. 运行 phase4_verify.py 复验
5. 生成部署分析报告
"""
import paramiko
import os
import sys
import time
import json
from pathlib import Path
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
HOST = '192.168.5.70'
KEY_PATH = os.path.expanduser('~/.ssh/192.168.5.70/id_rsa')
PASSWORD = 'Ubains@2026'
CODE_DIR = Path("E:\\github\\ubains-module-test\\develop\\.claude\\skills\\X86-TX-XTYBS\\code")
def exec_cmd(client, cmd):
stdin, stdout, stderr = client.exec_command(cmd)
out = stdout.read().decode('utf-8', errors='ignore')
err = stderr.read().decode('utf-8', errors='ignore')
return out, err
def main():
key = paramiko.RSAKey.from_private_key_file(KEY_PATH)
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
print("[1] 用新密码认证 ...")
client.connect(HOST, port=22, username='root', password=PASSWORD, timeout=15)
print("[2] docker ps ...")
out, err = exec_cmd(client, 'docker ps --format "{{.Names}}\t{{.Status}}"')
print(out)
if 'Up' not in out:
print('>> 容器未全部正常,退出')
client.close()
return 1
print("[3] 执行阶段3 验收检查 ...")
out, err = exec_cmd(client, 'cd /data/offline_auto_unifiedPlatform && python -m verify_x86_api')
print(out)
print("[4] 执行阶段4 授权 ...")
out, err = exec_cmd(client, 'cd /data/offline_auto_unifiedPlatform && python -m phase4_run')
print(out)
print("[5] 执行阶段4 复验 ...")
out, err = exec_cmd(client, 'cd /data/offline_auto_unifiedPlatform && python -m phase4_verify')
print(out)
print("[6] 生成部署分析报告 ...")
out, err = exec_cmd(client, 'cd /data/offline_auto_unifiedPlatform && python -m generate_report')
print(out)
client.close()
return 0
if __name__ == '__main__':
sys.exit(main())
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
业务功能验证脚本(按部署文档第四章节 + SKILL.md 业务验证章节)
用法(分步执行,每步独立运行互不影响):
python verify_business.py --step admin # 1.创建公司管理员 admin
python verify_business.py --step pwd # 2.admin 首次登录并强制修改密码
python verify_business.py --step permgroup # 3.创建测试权限组并绑定角色
python verify_business.py --step user # 4.新增用户 admin@test
python verify_business.py --step dept # 5.新增部门
python verify_business.py --step room # 6.新增会议室
python verify_business.py --step authcode # 7.批量启用授权码
python verify_business.py --step meeting # 8.前台新建会议
关键经验(来自 SKILL.md 教训):
- 教训D: Playwright headless + ignore_https_errors(自签名证书)
- 教训E: LoginAdmin 后台 iframe 嵌入模式,登录后必须 reload 等菜单渲染;
【管理员设置】在【系统管理】子菜单下需先展开;不直接拼 URL 导航
- admin 登录密码: 首次 Ubains@1357,强制改密后为 Ubains@13579
"""
import sys
import io
import time
import json
import argparse
from pathlib import Path
from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeoutError
if sys.stdout.encoding != 'utf-8':
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
CODE_DIR = Path(__file__).parent
CONFIG_FILE = CODE_DIR / 'deploy_config.json'
SHOT_DIR = Path.home() / 'deploy_logs' / 'screenshots'
RESULT_FILE = CODE_DIR / 'verify_business_result.json'
with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
config = json.load(f)
HOST = config['server']['host']
ADMIN_URL = f'https://{HOST}/#/LoginAdmin' # 后台登录入口(教训E)
FRONT_URL = f'https://{HOST}/'
SUPERADMIN_USER = config['license']['superadmin_username'] # superadmin
SUPERADMIN_PASS = config['license']['superadmin_password'] # Ubains@1357
VERIFY_CODE = config['license']['verify_code'] # csba
ADMIN_PASS_OLD = 'Ubains@1357' # admin 创建时密码
ADMIN_PASS_NEW = 'Ubains@13579' # admin 首次登录强制改密后的密码
results = []
def log(msg, level="INFO"):
ts = time.strftime('%Y-%m-%d %H:%M:%S')
print(f"[{ts}] [{level}] {msg}", flush=True)
def snap(page, name):
"""截图存证"""
SHOT_DIR.mkdir(parents=True, exist_ok=True)
p = SHOT_DIR / f"biz_{name}_{int(time.time())}.png"
try:
page.screenshot(path=str(p))
log(f"截图: {p}")
except Exception as e:
log(f"截图失败({name}): {e}", "WARN")
return p
def mark(step, ok, note):
results.append({'step': step, 'ok': ok, 'note': note})
log(f"[结果] {step}: {'✅' if ok else '❌'} {note}")
def all_frames(page):
"""主页面 + 所有 iframe(教训E: 后台内容在 iframe 内)"""
return [page] + [f for f in page.frames if f != page.main_frame]
def find_text(page, text, timeout_ms=10000):
"""等待文本出现在页面或 iframe 中"""
deadline = time.time() + timeout_ms / 1000
while time.time() < deadline:
for fr in all_frames(page):
try:
if fr.locator(f'text={text}').count() > 0:
return True
except Exception:
pass
time.sleep(1)
return False
def find_in_frames(page, selector):
"""在所有 frame 中查找选择器,返回 (frame, locator.first) 或 None"""
for fr in all_frames(page):
try:
loc = fr.locator(selector)
if loc.count() > 0:
return (fr, loc.first)
except Exception:
pass
return None
def handle_auth_dialog(page, password=None, timeout_ms=4000):
"""处理"校验身份"对话框:填密码+验证码 → 点确定(后台敏感操作可能弹出)"""
try:
page.wait_for_selector('.el-dialog:visible', timeout=timeout_ms)
auth = page.locator('.el-dialog:visible').first
pw = auth.locator('input[type="password"]')
vf = auth.locator('input[placeholder*="验证码"]')
if pw.count() > 0 and vf.count() > 0:
pw.fill(password or SUPERADMIN_PASS)
vf.fill(VERIFY_CODE)
log("身份校验对话框: 已填密码+验证码")
for sel in ['button:has-text("确 定")', 'button:has-text("确定")']:
b = auth.locator(sel)
if b.count() > 0:
b.first.click()
page.wait_for_timeout(2000)
return True
except PlaywrightTimeoutError:
pass
except Exception as e:
log(f"身份校验处理异常: {e}", "WARN")
return False
def dismiss_messageboxes(page):
"""关闭可能出现的确认/提示 el-message-box(点确定)"""
for fr in all_frames(page):
try:
mb = fr.locator('.el-message-box:visible')
if mb.count() > 0:
cb = mb.locator('button:has-text("确定")')
if cb.count() == 0:
cb = mb.locator('button:has-text("确 定")')
if cb.count() > 0:
cb.first.click()
log("已点击 message-box 确定")
page.wait_for_timeout(1500)
return True
except Exception:
pass
return False
def fill_form_item(dialog, label, value):
"""按标签文本填写 Element UI 表单项(在指定对话框容器内)"""
item = dialog.locator(f'.el-form-item:has-text("{label}")').first
if item.count() == 0:
log(f"表单项未找到: {label}", "WARN")
return False
inp = item.locator('input:visible')
if inp.count() == 0:
# input 可能未标记 visible(如被样式层遮挡),退回所有 input
inp = item.locator('input')
if inp.count() == 0:
log(f"表单项[{label}]内无 input", "WARN")
return False
inp.first.fill(value)
log(f"已填写[{label}] = {value if '密码' not in label else '***'}")
return True
def login_as(page, username, password):
"""登录后台(含 admin 首次登录强制改密处理)。返回是否登录成功。"""
page.goto(ADMIN_URL, timeout=60000, wait_until='domcontentloaded')
page.wait_for_timeout(5000)
vis = page.locator('input:visible')
n = vis.count()
if n < 3:
log(f"登录页可见输入框不足: {n}", "ERROR")
snap(page, f"login_fail_{username}")
return False
vis.nth(0).fill(username)
vis.nth(1).fill(password)
vis.nth(2).fill(VERIFY_CODE)
log("已填写账号/密码/验证码")
# 登录按钮: 该页面无 <button>,登录按钮是 <input type="submit">(实测 2026-09-07)
# 点击后检测是否仍在登录页,最多重试3次
still_on_login = True
for attempt in range(1, 4):
clicked = False
# 主选: input[type=submit](value 含"登")
try:
sub = page.locator('input[type="submit"]:visible')
if sub.count() > 0:
sub.first.click(timeout=5000)
clicked = True
except Exception:
pass
if not clicked:
# 备选: button 文本匹配
for sel in ['button:has-text("登 录")', 'button:has-text("登")']:
try:
loc = page.locator(sel)
for i in range(loc.count()):
b = loc.nth(i)
try:
if b.is_visible():
b.click(timeout=5000)
clicked = True
break
except Exception:
continue
if clicked:
break
except Exception:
continue
if not clicked:
# JS 兜底: input[type=submit] 优先(不走 offsetParent 判断,input 常为特殊定位)
page.evaluate('''() => {
const subs = Array.from(document.querySelectorAll('input[type="submit"]'));
const t1 = subs.find(s => /登/.test(s.value || ''));
if (t1) { t1.click(); return 'submit'; }
const btns = Array.from(document.querySelectorAll('button, a'));
const t2 = btns.find(b => b.offsetParent !== null && /登\\s*录/.test(b.innerText || ''));
if (t2) { t2.click(); return 'button'; }
return 'none';
}''')
log(f"已提交登录: {username}(第{attempt}次)")
# 等待跳转/报错,判断是否仍在登录页
page.wait_for_timeout(6000)
# 抓取 el-message 错误提示(如"密码错误"/"验证码错误")
try:
msgs = page.evaluate('''() => Array.from(document.querySelectorAll(".el-message")).map(e => e.innerText)''')
if msgs:
log(f"页面提示: {msgs}")
except Exception:
pass
cur_url = page.url
on_login = ('LoginAdmin' in cur_url or 'Login' in cur_url) and page.locator('input[type="password"]:visible').count() > 0
if not on_login:
still_on_login = False
break
log(f"仍在登录页(url={cur_url}),准备重试...", "WARN")
if still_on_login:
log(f"❌ {username} 登录提交失败(3次尝试均停留在登录页)", "ERROR")
snap(page, f"login_fail_{username}")
return False
# admin 首次登录强制修改密码:检测"修改密码"对话框
page.wait_for_timeout(3000)
try:
dlg = page.locator('.el-dialog:visible')
if dlg.count() > 0 and (page.locator('text=旧密码').count() > 0 or page.locator('text=新密码').count() > 0):
log("检测到首次登录强制修改密码对话框")
d = dlg.first
pws = d.locator('input[type="password"]')
# 通常3个密码框:旧密码 / 新密码 / 确认新密码
if pws.count() >= 3:
pws.nth(0).fill(ADMIN_PASS_OLD)
pws.nth(1).fill(ADMIN_PASS_NEW)
pws.nth(2).fill(ADMIN_PASS_NEW)
log("已填旧密码+新密码+确认密码")
elif pws.count() == 2:
pws.nth(0).fill(ADMIN_PASS_NEW)
pws.nth(1).fill(ADMIN_PASS_NEW)
log("已填新密码+确认密码(旧密码已预填或免输)")
for sel in ['button:has-text("确 定")', 'button:has-text("确定")']:
b = d.locator(sel)
if b.count() > 0:
b.first.click()
log("已提交修改密码")
page.wait_for_timeout(3000)
break
# 改密后可能需要重新登录
vis2 = page.locator('input:visible')
if vis2.count() >= 3:
log("改密后重新登录...")
vis2.nth(0).fill(username)
vis2.nth(1).fill(ADMIN_PASS_NEW)
vis2.nth(2).fill(VERIFY_CODE)
page.locator('button:has-text("登")').first.click()
page.wait_for_timeout(3000)
except Exception as e:
log(f"改密检测异常(可忽略): {e}", "WARN")
# 登录成功判据:后台侧边栏出现(教训E: 登录后 reload 使菜单完整渲染)
deadline = time.time() + 40
ok = False
while time.time() < deadline:
if find_text(page, '系统管理', 2) or find_text(page, '用户管理', 2):
ok = True
break
time.sleep(2)
if not ok:
# 可能已在后台但菜单文本在不同 frame,reload 再试
page.reload(wait_until='domcontentloaded')
page.wait_for_timeout(5000)
ok = find_text(page, '系统管理', 10) or find_text(page, '用户管理', 5)
if ok:
# 教训E: 登录后刷新页面,侧边栏菜单才完整渲染
page.reload(wait_until='domcontentloaded')
page.wait_for_timeout(5000)
log(f"✅ {username} 登录成功(已刷新加载完整菜单)")
snap(page, f"login_ok_{username}")
else:
log(f"❌ {username} 登录失败(40秒未检测到后台菜单)", "ERROR")
snap(page, f"login_fail_{username}")
return ok
def goto_menu(page, top_menu, sub_menu):
"""通过侧边栏导航: 展开 top_menu → 点击 sub_menu(教训E: 不直接拼URL)"""
loaded = False
for fr in all_frames(page):
try:
top = fr.locator(f'.el-submenu__title:has-text("{top_menu}")')
if top.count() > 0:
top.first.click()
page.wait_for_timeout(2000) # 教训E: 子菜单展开有时序
loaded = True
break
except Exception:
pass
if not loaded:
# 顶部菜单可能不是 submenu(直接可点的一级菜单)
for fr in all_frames(page):
try:
top = fr.locator(f'text={top_menu}')
if top.count() > 0:
top.first.click()
page.wait_for_timeout(2000)
loaded = True
break
except Exception:
pass
if not loaded:
log(f"未找到顶级菜单: {top_menu}", "ERROR")
return False
# 点击子菜单
for fr in all_frames(page):
try:
sub = fr.locator(f'text={sub_menu}')
if sub.count() > 0:
sub.first.click()
page.wait_for_timeout(4000)
log(f"已导航: {top_menu} → {sub_menu}")
return True
except Exception:
pass
log(f"未找到子菜单: {sub_menu}", "ERROR")
snap(page, f"nav_fail_{sub_menu}")
return False
def save_result(step_name):
"""结果落盘"""
data = {}
if RESULT_FILE.exists():
try:
data = json.loads(RESULT_FILE.read_text(encoding='utf-8'))
except Exception:
data = {}
data[step_name] = {
'time': time.strftime('%Y-%m-%d %H:%M:%S'),
'results': results,
'all_ok': all(r['ok'] for r in results) if results else False,
}
RESULT_FILE.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding='utf-8')
log(f"结果已保存: {RESULT_FILE}")
# ============================================================
# 步骤1: 创建公司管理员 admin
# ============================================================
def step_create_admin(page):
log("\n[业务验证1] 创建公司管理员 admin...")
if not login_as(page, SUPERADMIN_USER, SUPERADMIN_PASS):
mark("1.登录superadmin", False, "superadmin 登录后台失败")
return
# 教训E: 管理员设置在 系统管理 子菜单下,先展开再点击
if not goto_menu(page, '系统管理', '管理员设置'):
mark("1.导航管理员设置", False, "未找到 系统管理→管理员设置 菜单")
snap(page, "admin_nav_fail")
return
snap(page, "admin_setting_page")
# 验证页面: 公司管理员设置列表(应有 superadmin 1条)
if not find_text(page, 'superadmin', 10):
log("页面未见 superadmin 记录(可能列表为空或未加载完)", "WARN")
# 点击【添加】按钮
add = None
for t in ['添加', '新 增', '新增']:
r = find_in_frames(page, f'button:has-text("{t}")')
if r:
add = r
break
if not add:
mark("1.添加管理员", False, "未找到添加按钮")
snap(page, "admin_no_add_btn")
return
add[1].click()
log("已点击添加按钮")
page.wait_for_timeout(2000)
handle_auth_dialog(page) # 若弹身份校验则处理
page.wait_for_timeout(1000)
snap(page, "admin_add_dialog")
# 定位"添加用户"对话框
dlg_loc = None
for fr in all_frames(page):
try:
d = fr.locator('.el-dialog:visible')
if d.count() > 0:
dlg_loc = (fr, d.first)
break
except Exception:
pass
if not dlg_loc:
mark("1.添加管理员", False, "添加对话框未弹出")
snap(page, "admin_dialog_missing")
return
fr, dlg = dlg_loc
log("添加对话框已出现")
# 填写表单(实测 2026-09-07: 该对话框非 el-form-item 结构,改按 placeholder 定位)
def fill_by_placeholder(ph, value, label):
inp = dlg.locator(f'input[placeholder*="{ph}"]')
if inp.count() == 0:
log(f"表单项未找到(placeholder): {label}", "WARN")
return False
inp.first.fill(value)
log(f"已填写[{label}] = {'***' if '密码' in label else value}")
return True
fill_by_placeholder('以字母开头', 'admin', '账号')
fill_by_placeholder('用户名', 'admin', '用户名')
fill_by_placeholder('11位及以上', ADMIN_PASS_OLD, '新密码')
fill_by_placeholder('确认密码', ADMIN_PASS_OLD, '确认密码')
# 角色: 勾选"公司管理员"(Element UI 复选框优先点样式层)
try:
cb = dlg.locator('.el-checkbox:has-text("公司管理员")')
if cb.count() > 0:
cb.first.click()
log("已勾选角色: 公司管理员")
else:
# 备选: label 文本点击
lb = dlg.locator('label:has-text("公司管理员")')
if lb.count() > 0:
lb.first.click()
log("已点击角色label: 公司管理员")
else:
log("未找到'公司管理员'复选框(可能默认已选)", "WARN")
except Exception as e:
log(f"勾选角色异常: {e}", "WARN")
page.wait_for_timeout(1000)
snap(page, "admin_form_filled")
# 公司名称: 下拉选择"自动化"(实测 2026-09-07: 对话框内唯一的 .el-select,下拉容器 teleport 到 body)
try:
sel = dlg.locator('.el-select')
if sel.count() > 0:
sel.first.click()
page.wait_for_timeout(2000)
# 下拉选项渲染在 body 级(不在 dialog 内),从 frame 顶层找
opt = None
for f2 in all_frames(page):
try:
o = f2.locator('.el-select-dropdown:visible .el-select-dropdown__item:has-text("自动化")')
if o.count() > 0:
opt = o.first
break
except Exception:
pass
if opt is None:
# 备选: 取第2个可见选项(SKILL.md: 下拉第2个选项=自动化)
for f2 in all_frames(page):
try:
o = f2.locator('.el-select-dropdown:visible .el-select-dropdown__item')
if o.count() >= 2:
opt = o.nth(1)
break
except Exception:
pass
if opt:
opt.click()
log("已选择公司名称: 自动化")
page.wait_for_timeout(1000)
else:
log("未找到公司名称下拉选项", "WARN")
else:
log("未找到公司名称下拉框(可能自动带出)", "WARN")
except Exception as e:
log(f"选择公司异常: {e}", "WARN")
snap(page, "admin_form_complete")
# 提交
submitted = False
for sel in ['button:has-text("确 定")', 'button:has-text("确定")']:
b = dlg.locator(sel)
if b.count() > 0:
b.first.click()
submitted = True
log("已点击确定提交")
break
if not submitted:
mark("1.添加管理员", False, "未找到提交按钮")
return
page.wait_for_timeout(4000)
dismiss_messageboxes(page)
snap(page, "admin_after_submit")
# 验证(防误判双条件): ①对话框已关闭 ②列表出现精确文本 "admin"(get_by_text exact,排除 superadmin 子串匹配)
try:
dlg_gone = dlg.count() == 0 or not dlg.is_visible()
except Exception:
dlg_gone = True
ok = False
for f2 in all_frames(page):
try:
if f2.get_by_text('admin', exact=True).count() > 0:
ok = True
break
except Exception:
pass
msgs = []
try:
msgs = fr.evaluate('Array.from(document.querySelectorAll(".el-message")).map(e=>e.innerText)')
except Exception:
pass
errs = []
try:
errs = fr.evaluate('Array.from(document.querySelectorAll(".el-form-item__error")).map(e=>e.innerText)')
except Exception:
pass
if ok and dlg_gone:
mark("1.添加管理员", True, f"列表已出现精确 admin 记录,页面消息: {msgs}")
else:
mark("1.添加管理员", False, f"对话框未关闭={not dlg_gone}, 精确admin未出现, 表单校验: {errs}, 页面消息: {msgs}")
snap(page, "admin_verify")
def submit_login(page):
"""点击登录按钮(login_as 内的核心逻辑),返回是否点中。页面无 <button> 时登录按钮是 input[type=submit]"""
clicked = False
try:
sub = page.locator('input[type="submit"]:visible')
if sub.count() > 0:
sub.first.click(timeout=5000)
clicked = True
except Exception:
pass
if not clicked:
for sel in ['button:has-text("登 录")', 'button:has-text("登")']:
try:
loc = page.locator(sel)
for i in range(loc.count()):
b = loc.nth(i)
try:
if b.is_visible():
b.click(timeout=5000)
clicked = True
break
except Exception:
continue
if clicked:
break
except Exception:
continue
if not clicked:
page.evaluate('''() => {
const subs = Array.from(document.querySelectorAll('input[type="submit"]'));
const t1 = subs.find(s => /登/.test(s.value || ''));
if (t1) { t1.click(); return 'submit'; }
const btns = Array.from(document.querySelectorAll('button, a'));
const t2 = btns.find(b => b.offsetParent !== null && /登\\s*录/.test(b.innerText || ''));
if (t2) { t2.click(); return 'button'; }
return 'none';
}''')
return clicked
# ============================================================
# 步骤2: admin 首次登录并强制修改密码(Ubains@1357 → Ubains@13579)
# ============================================================
def step_admin_change_pwd(page):
log("\n[业务验证2] admin 首次登录并强制修改密码...")
page.goto(ADMIN_URL, timeout=60000, wait_until='domcontentloaded')
page.wait_for_timeout(5000)
vis = page.locator('input:visible')
if vis.count() < 3:
mark("2.首次登录", False, f"登录页输入框不足: {vis.count()}")
snap(page, "pwd_login_fail")
return
vis.nth(0).fill('admin')
vis.nth(1).fill(ADMIN_PASS_OLD) # 创建时初始密码
vis.nth(2).fill(VERIFY_CODE)
submit_login(page)
log("已用初始密码提交 admin 登录...")
page.wait_for_timeout(6000)
# 抓取页面错误提示
try:
msgs = page.evaluate('Array.from(document.querySelectorAll(".el-message")).map(e=>e.innerText)')
if msgs:
log(f"页面提示: {msgs}")
except Exception:
pass
snap(page, "pwd_first_login")
# 检测强制改密对话框(对话框无标题文字,以 旧密码/新密码 字段标签为锚点)
change_detected = False
dlg = None
for fr in all_frames(page):
try:
d = fr.locator('.el-dialog:visible')
if d.count() > 0 and (fr.locator('text=旧密码').count() > 0 or fr.locator('text=新密码').count() > 0):
dlg = d.first
change_detected = True
break
except Exception:
pass
mark("2.触发强制改密", change_detected, "检测到修改密码对话框" if change_detected else "未检测到修改密码对话框")
if not change_detected or dlg is None:
snap(page, "pwd_no_dialog")
return
# 填写旧密码+新密码+确认新密码
pws = dlg.locator('input[type="password"]')
if pws.count() >= 3:
pws.nth(0).fill(ADMIN_PASS_OLD)
pws.nth(1).fill(ADMIN_PASS_NEW)
pws.nth(2).fill(ADMIN_PASS_NEW)
log("已填旧密码+新密码+确认密码")
elif pws.count() == 2:
pws.nth(0).fill(ADMIN_PASS_NEW)
pws.nth(1).fill(ADMIN_PASS_NEW)
log("已填新密码+确认密码")
else:
mark("2.改密表单", False, f"密码框数量异常: {pws.count()}")
snap(page, "pwd_pwbox_abnormal")
return
snap(page, "pwd_form_filled")
for sel in ['button:has-text("确 定")', 'button:has-text("确定")']:
b = dlg.locator(sel)
if b.count() > 0:
b.first.click()
log("已提交修改密码")
page.wait_for_timeout(3000)
break
# 用新密码登录后台验证
ok = login_as(page, 'admin', ADMIN_PASS_NEW)
mark("2.改密后用新密码登录", ok, "新密码登录成功" if ok else "新密码登录失败")
snap(page, "pwd_re_login")
# ============================================================
# 步骤3: 创建测试权限组并绑定公司管理员角色
# ============================================================
def step_create_permgroup(page):
log("\n[业务验证3] 创建测试权限组并绑定公司管理员角色...")
if not login_as(page, 'admin', ADMIN_PASS_NEW):
mark("3.登录admin", False, "admin 登录失败")
return
if not goto_menu(page, '系统管理', '权限管理'):
mark("3.导航权限管理", False, "未找到 系统管理→权限管理 菜单")
return
snap(page, "perm_list")
# 若上次运行已创建过测试权限组,跳过创建直接进入绑定环节(避免重名)
ok = find_text(page, '测试权限组', 5)
if ok:
mark("3.创建权限组", True, "已存在 测试权限组(跳过重复创建)")
snap(page, "perm_created")
if not ok:
# 添加
add = find_in_frames(page, 'button:has-text("添加")')
if not add:
mark("3.添加权限组", False, "未找到添加按钮")
snap(page, "perm_no_add_btn")
return
add_fr, add_btn = add
add_btn.click()
page.wait_for_timeout(2000)
handle_auth_dialog(page)
page.wait_for_timeout(1500)
snap(page, "perm_add_page")
# 权限组名称(该页面为自定义表单 .form-item,非 el-form-item;placeholder 为通用"请输入")
filled = False
for selector in ['input[placeholder*="权限组"]',
'.form-item:has-text("权限组名称") input',
'.el-form-item:has-text("权限组名称") input']:
if filled:
break
for f2 in all_frames(page):
try:
inp = f2.locator(selector)
if inp.count() > 0:
inp.first.fill('测试权限组')
filled = True
break
except Exception:
pass
mark("3.填写权限组名称", filled, "已填 测试权限组" if filled else "未找到名称输入框")
# 全选所有模块权限(JS 批量勾选所有"全选"复选框,实测约48个)
try:
n = add_fr.evaluate('''() => {
const labels = Array.from(document.querySelectorAll('label, .el-checkbox'));
let cnt = 0;
for (const lb of labels) {
if (/全选/.test(lb.innerText || '')) {
const inp = lb.querySelector('input[type="checkbox"]');
if (inp && !inp.checked) { inp.click(); cnt++; }
}
}
return cnt;
}''')
log(f"已批量勾选[全选]复选框: {n} 个")
except Exception as e:
log(f"批量勾选异常: {e}", "WARN")
page.wait_for_timeout(1000)
snap(page, "perm_form_filled")
# 确定保存
saved = False
for f2 in all_frames(page):
try:
b = f2.locator('button:has-text("确 定")')
if b.count() == 0:
b = f2.locator('button:has-text("确定")')
if b.count() > 0:
b.first.click()
saved = True
break
except Exception:
pass
if not saved:
mark("3.保存权限组", False, "未找到确定按钮")
return
page.wait_for_timeout(3000)
dismiss_messageboxes(page)
ok = find_text(page, '测试权限组', 10)
mark("3.创建权限组", ok, "列表出现 测试权限组" if ok else "列表未见 测试权限组")
snap(page, "perm_created")
# 绑定角色:点击权限组行的【绑定】(操作列为 el-tag 图标按钮,非 button;
# "绑定"tag 带 el-icon-connection 图标,主表体副本因固定列 is-hidden 不可见,
# 优先在 el-table__fixed-right 固定列中点击)
if not ok:
return
bfound = False
for f2 in all_frames(page):
try:
row = f2.locator('tr:has-text("测试权限组")')
if row.count() == 0:
continue
for sel in ['.el-table__fixed-right tr:has-text("测试权限组") .el-tag:has-text("绑定")',
'tr:has-text("测试权限组") .el-tag:has-text("绑定")',
'tr:has-text("测试权限组") .el-icon-connection',
'tr:has-text("测试权限组") button:has-text("绑定")']:
bind = f2.locator(sel)
if bind.count() > 0:
bind.first.click()
bfound = True
break
if bfound:
break
except Exception:
pass
mark("3.点击绑定", bfound, "已点击绑定" if bfound else "未找到绑定按钮")
if not bfound:
return
page.wait_for_timeout(2500)
snap(page, "perm_bind_page")
# 选择角色下拉框 → 选"公司管理员"(multiple el-select,输入框 readonly,
# 需点击 .el-select 容器触发下拉展开,diag_bind.py 已验证可行)
sel_done = False
for f2 in all_frames(page):
try:
sel = f2.locator('.el-select:has(input[placeholder="搜索角色"])')
if sel.count() > 0:
sel.first.click()
page.wait_for_timeout(2000)
sel_done = True
break
except Exception:
pass
mark("3.点击角色下拉框", sel_done, "已点击角色下拉框" if sel_done else "未找到角色下拉框")
# 选择选项:公司管理员
opt = None
for f2 in all_frames(page):
try:
o = f2.locator('.el-select-dropdown:visible .el-select-dropdown__item:has-text("公司管理员")')
if o.count() > 0:
opt = o.first
break
except Exception:
pass
if opt:
opt.click()
page.wait_for_timeout(1000)
sel_done = True
log("已选择角色: 公司管理员")
else:
# 备选:下拉未弹出时用 JS 点击容器后再点选项
for f2 in all_frames(page):
try:
js = '''
const sel = document.querySelector('.el-select:has(input[placeholder="搜索角色"])');
if (sel) sel.click();
'''
f2.evaluate(js)
page.wait_for_timeout(1500)
o = f2.locator('.el-select-dropdown .el-select-dropdown__item:has-text("公司管理员")')
if o.count() > 0:
o.first.click()
sel_done = True
log("已选择角色: 公司管理员(JS)")
break
except Exception:
pass
mark("3.选择角色", sel_done, "已选 公司管理员" if sel_done else "未找到角色选项")
snap(page, "perm_role_selected")
# 保存绑定
bsave = False
for f2 in all_frames(page):
try:
b = f2.locator('button:has-text("保 存")')
if b.count() == 0:
b = f2.locator('button:has-text("保存")')
if b.count() > 0:
b.first.click()
bsave = True
break
except Exception:
pass
if not bsave:
mark("3.保存绑定", False, "未找到保存按钮")
return
page.wait_for_timeout(3000)
dismiss_messageboxes(page)
snap(page, "perm_bound")
# ============================================================
# 步骤4: 新增用户 admin@test
# ============================================================
def step_create_user(page):
log("\n[业务验证4] 新增用户 admin@test...")
if not login_as(page, 'admin', ADMIN_PASS_NEW):
mark("4.登录admin", False, "admin 登录失败")
return
if not goto_menu(page, '用户管理', '用户列表'):
mark("4.导航用户列表", False, "未找到 用户管理→用户列表 菜单")
return
snap(page, "user_list")
add = find_in_frames(page, 'button:has-text("添加")')
if not add:
mark("4.添加用户", False, "未找到添加按钮")
snap(page, "user_no_add_btn")
return
add[1].click()
page.wait_for_timeout(2000)
handle_auth_dialog(page)
page.wait_for_timeout(1000)
# 定位"添加用户"对话框
dlg = None
for fr in all_frames(page):
try:
d = fr.locator('.el-dialog:visible')
if d.count() > 0:
dlg = d.first
break
except Exception:
pass
if not dlg:
mark("4.添加用户", False, "添加对话框未弹出")
snap(page, "user_dialog_missing")
return
log("添加用户对话框已出现")
def fb(ph, value, label):
inp = dlg.locator(f'input[placeholder*="{ph}"]')
if inp.count() == 0:
log(f"表单项未找到(placeholder): {label}", "WARN")
return False
inp.first.fill(value)
log(f"已填写[{label}] = {'***' if '密码' in label else value}")
return True
fb('以字母开头', 'admin@test', '账号')
fb('用户名', 'admin@test', '用户名')
fb('11位及以上', ADMIN_PASS_OLD, '新密码')
fb('确认密码', ADMIN_PASS_OLD, '确认密码')
snap(page, "user_form_filled")
submitted = False
for sel in ['button:has-text("确 定")', 'button:has-text("确定")']:
b = dlg.locator(sel)
if b.count() > 0:
b.first.click()
submitted = True
log("已点击确定提交")
break
if not submitted:
mark("4.添加用户", False, "未找到提交按钮")
return
page.wait_for_timeout(4000)
dismiss_messageboxes(page)
ok = find_text(page, 'admin@test', 10)
mark("4.添加用户", ok, "列表出现 admin@test" if ok else "列表未见 admin@test")
snap(page, "user_created")
# ============================================================
# 步骤5: 新增部门
# ============================================================
def step_create_dept(page):
log("\n[业务验证5] 新增部门...")
if not login_as(page, 'admin', ADMIN_PASS_NEW):
mark("5.登录admin", False, "admin 登录失败")
return
if not goto_menu(page, '用户管理', '部门管理'):
mark("5.导航部门管理", False, "未找到 用户管理→部门管理 菜单")
return
snap(page, "dept_list")
add = find_in_frames(page, 'button:has-text("添加")')
if not add:
mark("5.新增部门", False, "未找到添加按钮")
snap(page, "dept_no_add_btn")
return
add[1].click()
page.wait_for_timeout(3000)
handle_auth_dialog(page)
page.wait_for_timeout(2000)
ok = find_text(page, '默认部门名称', 10)
mark("5.新增部门", ok, "树形结构出现 默认部门名称" if ok else "未见 默认部门名称")
snap(page, "dept_created")
# ============================================================
# 步骤6: 新增会议室(测试会议室)
# ============================================================
def step_create_room(page):
log("\n[业务验证6] 新增会议室(测试会议室)...")
if not login_as(page, 'admin', ADMIN_PASS_NEW):
mark("6.登录admin", False, "admin 登录失败")
return
# 菜单名: 区域管理→区域列表,实际进入会议室管理
if not goto_menu(page, '区域管理', '区域列表'):
mark("6.导航会议室管理", False, "未找到 区域管理→区域列表 菜单")
return
snap(page, "room_list")
add = find_in_frames(page, 'button:has-text("添加")')
if not add:
mark("6.新增会议室", False, "未找到添加按钮")
snap(page, "room_no_add_btn")
return
add[1].click()
page.wait_for_timeout(3000)
handle_auth_dialog(page)
page.wait_for_timeout(2000)
snap(page, "room_add_page")
# 会议室名称(必填)
named = False
for f2 in all_frames(page):
try:
item = f2.locator('.el-form-item:has-text("会议室名称") input, .el-form-item:has-text("会议室名称") textarea')
if item.count() > 0:
item.first.fill('测试会议室')
named = True
break
except Exception:
pass
if not named:
for f2 in all_frames(page):
try:
inp = f2.locator('input[placeholder*="会议室"]')
if inp.count() > 0:
inp.first.fill('测试会议室')
named = True
break
except Exception:
pass
mark("6.填写会议室名称", named, "已填 测试会议室" if named else "未找到名称输入框")
# 预定授权下拉: 选择第1个授权码(CCA-0K7-0001)
sel_done = False
for f2 in all_frames(page):
try:
s = f2.locator('.el-form-item:has-text("预定授权") .el-select')
if s.count() == 0:
s = f2.locator('.el-select')
if s.count() > 0:
s.first.click()
page.wait_for_timeout(2000)
break
except Exception:
pass
opt = None
for f2 in all_frames(page):
try:
o = f2.locator('.el-select-dropdown:visible .el-select-dropdown__item')
if o.count() >= 1:
opt = o.first
break
except Exception:
pass
if opt:
opt.click()
page.wait_for_timeout(1000)
sel_done = True
log("已选择预定授权: 第1个授权码")
mark("6.选择预定授权", sel_done, "已选授权码" if sel_done else "未找到授权码选项")
snap(page, "room_form_filled")
# 保存
saved = False
for sel in ['button:has-text("保 存")', 'button:has-text("保存")']:
r = find_in_frames(page, sel)
if r:
r[1].click()
saved = True
break
if not saved:
mark("6.保存会议室", False, "未找到保存按钮")
return
page.wait_for_timeout(4000)
dismiss_messageboxes(page)
ok = find_text(page, '测试会议室', 10)
mark("6.新增会议室", ok, "列表出现 测试会议室" if ok else "列表未见 测试会议室")
snap(page, "room_created")
# ============================================================
# 步骤7: 批量启用授权码
# ============================================================
def step_enable_authcode(page):
log("\n[业务验证7] 批量启用授权码...")
if not login_as(page, 'admin', ADMIN_PASS_NEW):
mark("7.登录admin", False, "admin 登录失败")
return
# 菜单名: 授权管理→会议授权,实际进入授权码管理
if not goto_menu(page, '授权管理', '会议授权'):
mark("7.导航授权码管理", False, "未找到 授权管理→会议授权 菜单")
return
snap(page, "authcode_list")
# 全选表头 checkbox(JS 方式,实测有效)
try:
n = page.evaluate('''() => {
const inputs = Array.from(document.querySelectorAll('.el-table__header-wrapper .el-checkbox__input input'));
let cnt = 0;
for (const inp of inputs) {
if (!inp.checked) { inp.click(); cnt++; }
}
return cnt;
}''')
log(f"已点击表头全选 checkbox: {n}")
except Exception as e:
log(f"全选异常: {e}", "WARN")
page.wait_for_timeout(1500)
snap(page, "authcode_selected")
# 批量启用
r = find_in_frames(page, 'button:has-text("批量启用")')
if not r:
mark("7.批量启用授权码", False, "未找到批量启用按钮")
snap(page, "authcode_no_btn")
return
r[1].click()
page.wait_for_timeout(2000)
dismiss_messageboxes(page) # 确认对话框: 确定批量启用授权码?
page.wait_for_timeout(2000)
ok = find_text(page, '已激活', 10)
mark("7.批量启用授权码", ok, "授权码状态变为 已激活" if ok else "未见 已激活 状态")
snap(page, "authcode_activated")
# ============================================================
# 步骤8: 前台新建会议
# ============================================================
def agree_protocol_dialog(page, max_retry=3):
"""处理服务协议确认对话框:在所有 frame 中查找含"协议"的弹窗,循环点击确定直到消失"""
for _ in range(max_retry):
dlg = None
for f in all_frames(page):
try:
mb = f.locator('.el-message-box:visible, .el-dialog:visible').filter(
has_text='协议')
if mb.count() > 0:
dlg = mb.first
break
except Exception:
pass
if not dlg:
return True # 弹窗已消失(或从未出现)
try:
ok = dlg.locator('button:has-text("确定")')
if ok.count() == 0:
return False
ok.first.click(timeout=5000)
log("已同意服务协议(点击确定)")
except Exception as e:
log(f"服务协议对话框处理异常: {e}", "WARN")
return False
page.wait_for_timeout(4000)
return False
def page_has_error_message(page):
"""抓取所有 frame 中 el-message 错误提示(如 验证码错误/密码错误),返回文本列表"""
msgs = []
for f in all_frames(page):
try:
texts = f.evaluate('''() => Array.from(document.querySelectorAll(".el-message"))
.filter(e => e.offsetParent !== null || getComputedStyle(e).display !== 'none')
.map(e => e.innerText.trim()).filter(t => t)''')
msgs.extend(texts)
except Exception:
pass
return msgs
def front_login_via_backend(page):
"""主路径:先登录后台再访问前台。
文档明确:"如果已在后台登录,可直接访问前台地址(会话共享)",
后台 login_as 流程已在步骤1-7中反复验证可靠,可绕开前台登录表单的
验证码识别与协议弹窗时序问题。
返回是否已到达前台首页(出现首页元素)。
"""
try:
if not login_as(page, 'admin', ADMIN_PASS_NEW):
log("后台登录失败,无法走会话共享路径", "WARN")
return False
page.goto(FRONT_URL, timeout=60000, wait_until='domcontentloaded')
page.wait_for_timeout(8000)
# 会话共享自动登录后可能弹出服务协议对话框,循环点击确定直到消失
agree_protocol_dialog(page)
# 判断是否已登录前台:出现首页元素,或 URL 已离开登录页
if find_text(page, '新建会议', 10) or find_text(page, '首页', 5):
return True
cur = page.url
logged_in = ('meetingV3' in cur) or ('Login' not in cur and 'login' not in cur)
if logged_in:
log(f"前台 URL 已离开登录页: {cur}")
return True
msgs = page_has_error_message(page)
if msgs:
log(f"前台登录提示: {msgs}", "WARN")
return False
except Exception as e:
log(f"会话共享登录异常: {e}", "WARN")
return False
def front_login(page):
"""兜底:前台登录表单直登 admin(改密后新密码 Ubains@13579)"""
page.goto(FRONT_URL, timeout=60000, wait_until='domcontentloaded')
page.wait_for_timeout(5000)
vis = page.locator('input:visible')
if vis.count() < 3:
log(f"前台登录页输入框不足: {vis.count()}", "WARN")
return False
try:
vis.nth(0).fill('admin')
vis.nth(1).fill(ADMIN_PASS_NEW)
vis.nth(2).fill(VERIFY_CODE)
except Exception as e:
log(f"前台登录表单填写异常: {e}", "WARN")
return False
clicked = False
for sel in ['button:has-text("登 录")', 'button:has-text("登录")', 'button:has-text("登")']:
try:
loc = page.locator(sel)
for i in range(loc.count()):
b = loc.nth(i)
try:
if b.is_visible():
b.click(timeout=5000)
clicked = True
break
except Exception:
continue
if clicked:
break
except Exception:
continue
if not clicked:
try:
page.evaluate('''() => {
const btns = Array.from(document.querySelectorAll('button'));
const t = btns.find(b => b.offsetParent !== null && /登\\s*录/.test(b.innerText || ''));
if (t) { t.click(); }
}''')
except Exception:
pass
page.wait_for_timeout(3000)
# 前台首次登录会弹出服务协议确认对话框,循环点击确定直到消失
agree_protocol_dialog(page)
page.wait_for_timeout(4000)
return True
def step_create_meeting(page):
log("\n[业务验证8] 前台新建会议...")
# 主路径:后台会话共享自动登录(文档记载的正规方式)
ok_login = front_login_via_backend(page)
if not ok_login:
# 兜底:前台登录表单直登
log("会话共享路径未见首页元素,回退前台表单直登", "WARN")
if not front_login(page):
mark("8.前台登录", False, "前台登录表单异常")
snap(page, "front_login_fail")
return
ok_login = find_text(page, '新建会议', 20) or find_text(page, '首页', 5)
mark("8.前台登录", ok_login, "前台已登录" if ok_login else "前台登录后未见首页元素")
if not ok_login:
snap(page, "front_login_fail")
return
# 点击 新建会议
clicked = False
for f in all_frames(page):
try:
b = f.locator('text=新建会议')
if b.count() > 0:
b.first.click()
clicked = True
break
except Exception:
pass
if not clicked:
mark("8.新建会议", False, "未找到 新建会议 入口")
snap(page, "meeting_no_entry")
return
page.wait_for_timeout(5000)
snap(page, "meeting_new_page")
# 选择开会区域(点击后系统加载可用会议室列表)
area = False
for f in all_frames(page):
try:
loc = f.locator('text=选择开会区域')
if loc.count() > 0:
loc.first.click()
area = True
break
except Exception:
pass
mark("8.选择开会区域", area, "已点击 选择开会区域" if area else "未找到 选择开会区域")
page.wait_for_timeout(3000)
# 勾选 测试会议室(重试等待加载 + 验证真实勾选状态)
# 实证(diag_meeting_room2.py 2026-09-07): Playwright locator ':has-text' 在该嵌入页面匹配不到行、
# check(force=True) 点击隐藏 input 也不会触发 Element UI 的 change,
# 唯一可靠方式: JS querySelectorAll('.el-table__row') 按 innerText 过滤 + 点击 .el-checkbox + 校验 input.checked
room_checked = False
deadline = time.time() + 30
while time.time() < deadline and not room_checked:
for f in all_frames(page):
try:
found = f.evaluate('''() => {
const trs = Array.from(document.querySelectorAll('.el-table__row'))
.filter(tr => (tr.innerText || '').includes('测试会议室'));
if (!trs.length) return false;
const tr = trs[0];
const cb = tr.querySelector('.el-checkbox');
const inp = tr.querySelector('input[type="checkbox"]');
if (!cb || !inp) return false;
if (!inp.checked) cb.click();
return true;
}''')
if found:
page.wait_for_timeout(800)
verified = f.evaluate('''() => {
const tr = Array.from(document.querySelectorAll('.el-table__row'))
.find(tr => (tr.innerText || '').includes('测试会议室'));
const inp = tr ? tr.querySelector('input[type="checkbox"]') : null;
return inp ? inp.checked : false;
}''')
if verified:
room_checked = True
log("已勾选 测试会议室 (JS .el-checkbox.click)")
break
except Exception:
pass
if not room_checked:
time.sleep(2)
mark("8.勾选会议室", room_checked, "已勾选 测试会议室" if room_checked else "未找到/未能勾选 测试会议室")
page.wait_for_timeout(1500)
snap(page, "meeting_room_selected")
# 确定创建
created = False
for f in all_frames(page):
try:
b = f.locator('button:has-text("确定创建")')
if b.count() > 0:
b.first.click()
created = True
break
except Exception:
pass
if not created:
mark("8.确定创建", False, "未找到 确定创建 按钮")
snap(page, "meeting_no_create_btn")
return
# 点击后立即抓取短暂 el-message(错误提示约3秒消失)
err_msgs = []
for _ in range(8):
page.wait_for_timeout(600)
for f in all_frames(page):
try:
texts = f.evaluate('''() => Array.from(document.querySelectorAll(".el-message"))
.filter(e => e.offsetParent !== null || getComputedStyle(e).display !== 'none')
.map(e => e.innerText.trim()).filter(t => t)''')
err_msgs.extend(texts)
except Exception:
pass
if err_msgs:
break
if err_msgs:
log(f"确定创建后提示: {err_msgs}")
page.wait_for_timeout(5000)
dismiss_messageboxes(page)
ok = find_text(page, '会议创建成功', 10)
mark("8.新建会议", ok, "弹出 会议创建成功 提示" if ok else f"未见 创建成功 提示({err_msgs if err_msgs else '无提示'})")
snap(page, "meeting_created")
# ============================================================
# 主入口
# ============================================================
STEPS = {
'admin': step_create_admin,
'pwd': step_admin_change_pwd,
'permgroup': step_create_permgroup,
'user': step_create_user,
'dept': step_create_dept,
'room': step_create_room,
'authcode': step_enable_authcode,
'meeting': step_create_meeting,
}
def main():
ap = argparse.ArgumentParser()
ap.add_argument('--step', required=True, choices=sorted(STEPS.keys()))
args = ap.parse_args()
log("=" * 60)
log(f"业务功能验证: {args.step}")
log("=" * 60)
with sync_playwright() as p:
browser = p.chromium.launch(headless=True, args=[
'--ignore-certificate-errors', '--disable-web-security', '--no-sandbox'])
ctx = browser.new_context(ignore_https_errors=True)
page = ctx.new_page()
try:
STEPS[args.step](page)
except Exception as e:
log(f"步骤异常: {e}", "ERROR")
import traceback
traceback.print_exc()
snap(page, f"error_{args.step}")
mark(args.step, False, f"异常: {e}")
finally:
log("\n执行汇总:")
for r in results:
log(f" {'✅' if r['ok'] else '❌'} {r['step']}: {r['note']}")
save_result(args.step)
time.sleep(2)
browser.close()
return 0 if results and all(r['ok'] for r in results) else 1
if __name__ == '__main__':
sys.exit(main())
{
"admin": {
"time": "2026-09-08 18:34:31",
"results": [
{
"step": "1.添加管理员",
"ok": true,
"note": "列表已出现精确 admin 记录,页面消息: []"
}
],
"all_ok": true
},
"pwd": {
"time": "2026-09-08 18:37:29",
"results": [
{
"step": "2.触发强制改密",
"ok": true,
"note": "检测到修改密码对话框"
},
{
"step": "2.改密后用新密码登录",
"ok": true,
"note": "新密码登录成功"
}
],
"all_ok": true
},
"permgroup": {
"time": "2026-09-08 18:39:01",
"results": [
{
"step": "3.填写权限组名称",
"ok": true,
"note": "已填 测试权限组"
},
{
"step": "3.创建权限组",
"ok": true,
"note": "列表出现 测试权限组"
},
{
"step": "3.点击绑定",
"ok": true,
"note": "已点击绑定"
},
{
"step": "3.点击角色下拉框",
"ok": true,
"note": "已点击角色下拉框"
},
{
"step": "3.选择角色",
"ok": true,
"note": "已选 公司管理员"
}
],
"all_ok": true
},
"user": {
"time": "2026-09-08 18:39:44",
"results": [
{
"step": "4.添加用户",
"ok": true,
"note": "列表出现 admin@test"
}
],
"all_ok": true
},
"dept": {
"time": "2026-09-08 18:40:31",
"results": [
{
"step": "5.新增部门",
"ok": true,
"note": "树形结构出现 默认部门名称"
}
],
"all_ok": true
},
"room": {
"time": "2026-09-08 18:41:24",
"results": [
{
"step": "6.填写会议室名称",
"ok": true,
"note": "已填 测试会议室"
},
{
"step": "6.选择预定授权",
"ok": true,
"note": "已选授权码"
},
{
"step": "6.新增会议室",
"ok": true,
"note": "列表出现 测试会议室"
}
],
"all_ok": true
},
"authcode": {
"time": "2026-09-08 18:42:21",
"results": [
{
"step": "7.批量启用授权码",
"ok": true,
"note": "授权码状态变为 已激活"
}
],
"all_ok": true
},
"meeting": {
"time": "2026-09-08 18:43:40",
"results": [
{
"step": "8.前台登录",
"ok": true,
"note": "前台已登录"
},
{
"step": "8.选择开会区域",
"ok": true,
"note": "已点击 选择开会区域"
},
{
"step": "8.勾选会议室",
"ok": true,
"note": "已勾选 测试会议室"
},
{
"step": "8.新建会议",
"ok": true,
"note": "弹出 会议创建成功 提示"
}
],
"all_ok": true
}
}
\ No newline at end of file
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论