提交 277b350a authored 作者: 陈泽健's avatar 陈泽健

feat(test): 添加运维设置功能验证脚本和会议创建流程

- 新增 ops_query_person.py 脚本,实现运维设置→人员设置查询功能
- 新增 ops_add_typetag.py 脚本,实现类型标签新增功能验证
- 新增 open_admin_backend.py 脚本,提供后台登录保持浏览器开启功能
- 新增 phase14_create_meeting.py 脚本,实现前台新建会议流程
- 更新 SKILL.md 文档,添加运维人员核实和类型标签新增步骤说明
- 补充运维设置导航、人员查询、类型标签新增等实战教训和关键经验
上级 46442e56
......@@ -62,6 +62,8 @@ ARM架构 Ubuntu/openEuler 服务器远程自动化部署,严格按需求文
| 12 会议授权码 | 会议授权→全选→批量启用 | `phase10_13_robust.py` |
| 13 添加会议室 | 区域列表→添加"测试会议室"+第一个授权码 | `phase10_13_robust.py` |
| 14 新建会议 | 前台登录→新建会议→勾选"测试会议室"行→确定创建 | `web_operations_phase8_14.py --phase=14` |
| 15 运维人员核实 | admin后台→展开运维设置→人员设置→输入框查询admin@test是否存在(补充验证) | `ops_query_person.py` |
| 16 类型标签新增 | 运维设置→类型标签→右上角新增→名称"测试类型新增"→确定→核实列表(补充验证) | `ops_add_typetag.py` |
## ⚠️ 严格执行规则
......@@ -92,6 +94,8 @@ ARM架构 Ubuntu/openEuler 服务器远程自动化部署,严格按需求文
- `phase8_changepwd.py` 前台首登admin→改密 Admin@2024ub
- `verify_room_authcode.py` 核实会议室/授权码(状态"已激活")
- `verify_meeting_front.py` 稳健前台登录(协议处理)+核实会议创建
- `ops_query_person.py` 阶段15:admin后台 运维设置→人员设置→输入框查询用户(placeholder"请输入名称"),后台运行keep-alive保持浏览器
- `ops_add_typetag.py` 阶段16:运维设置→类型标签→右上角新增→名称"测试类型新增"→确定→核实;自动跳过readonly下拉、send_keys填名称
- `ssh_helper.py`/`deploy_config.json`/`plink.exe`/`pscp.exe` SSH工具与凭据
## 🔧 实战教训(根因+对策,下次不再踩)
......@@ -132,6 +136,18 @@ license.zip内`MEETFile.uas`虽被正确分发为meeting2.0的`license.txt`(md5
**L. 执行前先probe真实状态**
docker ps返回exit 127可能是`bash -c`引号问题而非未装docker;先`probe_state.py`看df/ls/docker/服务真实情况,判断全新vs已部署,避免误把已部署系统重跑覆盖。
**M. 运维设置→人员设置 导航:菜单项重名 + 展开时序**
后台左侧有**多个"人员设置"菜单**(组织管理/运维设置/等下各有一个),不能简单点第一个。对策:先定位文本含"运维设置"的`.el-submenu`,展开后**等约2秒**让子菜单动画完成,再在其下找可见的`.el-menu-item`"人员设置"点击;**展开与点击必须分两步+间隔**,否则子项未渲染会报"运维设置下无人员设置"。脚本`ops_query_person.py`已实现(展开→sleep→带重试点子项)。
**N. 人员设置查询框 placeholder 是"请输入名称"**
不是"关键字/搜索/账号/手机"。对策:匹配placeholder含**"名称"**的input;不确定时先`dump所有可见input的placeholder`再选。填入用户名回车后列表会**从2条筛选为1条**(精确命中),据此判断用户是否存在;若返回"暂无数据"则不存在。
**O. 类型标签新增弹窗:readonly"请选择"下拉会拦截名称框点击**
类型标签"新增"弹窗内可见输入框为:`checkbox / 请选择(readonly下拉) / number / 请输入类型标签名称(名称框) / 排序(number)`。名称框前有个只读"请选择"下拉,若用宽泛兜底匹配(如"任意text输入框")会先命中它,点击时报`element click intercepted: ...is not clickable ... el-dialog__wrapper would receive the click`。对策:填名称时**跳过 `readonly` 与 `type=number/checkbox/radio` 的input**,按 placeholder 含**"类型/标签/名称"**精确定位 → 名称框是**"请输入类型标签名称"**;用send_keys填,点确定后核实对话框已关闭+列表出现新行。
**P. 类型标签模块导航同"运维设置→二级菜单"**
类型标签在**运维设置**子菜单下(非组织管理)。进入方式同教训M:展开运维设置→等2s→点其下可见的"类型标签"。模块表头为"名称/排序",新增记录后排序默认0。
## 🎯 关键经验总结(实操要点,逐条遵守)
1. **解压必须用阻塞式执行**,nohup后台方式在sudo环境下不可靠(见教训I)
......@@ -184,3 +200,8 @@ docker ps返回exit 127可能是`bash -c`引号问题而非未装docker;先`pr
45. **会议授权码启用后状态文案是"已激活"**(不是"已启用"),操作按钮变"停用";判断启用看"已激活"或"停用"按钮(见教训G)。部门管理"添加"是图标按钮(无文字),部门编辑非必需可跳过(见教训H)。
46. **部署脚本用 setsid+nohup 后台启动 + 轮询日志**`/data/deploy_output.log``DEPLOY_WRAPPER_FINISHED`标记),脱离SSH会话避免10分钟工具超时杀掉部署;解压用前台阻塞式`tar -zxf`(见教训I)。
47. **执行前先probe真实服务器状态**(docker ps的exit 127可能是bash -c引号问题而非未装docker;df/ls看实际),确认是全新/已部署,再决定是否全量重跑——避免误判把已部署系统重跑覆盖(见教训L)。
48. **运维设置→人员设置核实用户**:先展开"运维设置"子菜单(定位文本含"运维设置"的`.el-submenu`,点其`.el-submenu__title`),**等2秒**让子项动画完成,再点其下可见的"人员设置"`.el-menu-item`**菜单项重名,勿点第一个**——组织管理/运维设置下都有"人员设置");脚本`ops_query_person.py`(见教训M)。
49. **人员设置查询框 placeholder = "请输入名称"**(非关键字/搜索/账号/手机);填入用户名回车后列表**精确筛选(2→1条)**即证存在,返回"暂无数据"即不存在;placeholder不确定时先`dump所有可见input的placeholder`排查(见教训N)。
50. **Element UI 子菜单展开有时序**:点`.el-submenu__title`展开后,子`.el-menu-item`需等动画(约2s)才可见;**展开+点击务必分两步带间隔**,合并在一次JS里立即查子项会报"未找到子项"。同理适用于所有需要先展开再进二级菜单的操作。
51. **类型标签新增**:运维设置→类型标签(同教训M展开法)→右上角"新增"→名称框填"测试类型新增"→确定→核实列表;新增弹窗内名称框 placeholder = **"请输入类型标签名称"**,模块表头"名称/排序",新增记录排序默认0;脚本`ops_add_typetag.py`(见教训P)。
52. **弹窗填值先排除 readonly/number/checkbox 输入框**:Element UI 弹窗常含只读下拉(placeholder"请选择", readonly)与数字框,宽泛兜底匹配会先命中只读框致`element click intercepted`。对策:填名称类字段时**跳过 `readonly` 与 `type=number/checkbox/radio`**,按 placeholder 含"名称/类型/标签"精确选;不确定先 dump 弹窗内 input 列表(见教训O)。
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""用 admin 登录后台 LoginAdmin(Admin@2024ub),登录成功后保持浏览器开启(不quit)。
供人工查看/操作后台。后台运行,浏览器窗口持续保留。"""
import sys, time, os
from datetime import datetime
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
if sys.platform=='win32':
try: sys.stdout.reconfigure(encoding='utf-8', errors='replace')
except: pass
BASE='https://192.168.9.76'
def log(m,l="INFO"): print(f"[{datetime.now().strftime('%H:%M:%S')}] [{l}] {m}",flush=True)
def login_admin(d, max_attempts=5):
"""后台LoginAdmin登录,带重试与验证码刷新。"""
for attempt in range(max_attempts):
log(f"登录尝试 #{attempt+1} (admin)")
d.get(f'{BASE}/#/LoginAdmin'); time.sleep(8)
# 已登录检测
body=d.execute_script('return document.body.innerText.substring(0,300);') or ''
if '系统管理' in body or '组织管理' in body or '用户管理' in body:
log("已处于登录态"); return True
# 填表
for inp in d.find_elements(By.TAG_NAME,'input'):
if not inp.is_displayed(): continue
p=inp.get_attribute('placeholder') or ''; t=inp.get_attribute('type') or ''
if '账号' in p or '手机' in p or '用户名' in p: inp.clear(); inp.send_keys('admin')
elif t=='password': inp.clear(); inp.send_keys('Admin@2024ub')
elif '验证码' in p or '图形' in p: inp.clear(); inp.send_keys('csba')
time.sleep(2)
d.execute_script("var b=document.querySelector('input[type=submit]');if(b)b.click();")
# 等待前端跳转(约11秒,等20秒保险)
for i in range(20):
time.sleep(1)
if 'LoginAdmin' not in d.current_url:
time.sleep(3); log(f"✅ 登录成功,URL: {d.current_url}"); return True
b=d.execute_script('return document.body.innerText.substring(0,300);') or ''
if '验证码错误' in b:
log("验证码错误,刷新重试"); d.refresh(); time.sleep(6); break
if '密码错误' in b or '账号不存在' in b:
log(f"登录报错: {[k for k in ['密码错误','账号不存在'] if k in b]}","ERROR"); return False
log("跳转未完成,重试...")
return False
def main():
opts=Options()
opts.add_argument('--ignore-certificate-errors')
opts.add_argument('--ignore-ssl-errors=yes')
opts.add_argument('--window-size=1920,1080')
opts.add_experimental_option('detach', True) # 脚本退出后浏览器仍保持开启
d=webdriver.Chrome(options=opts); d.implicitly_wait(8)
try:
if not login_admin(d):
log("❌ admin 后台登录失败,但浏览器保持开启供人工排查","ERROR")
else:
# 登录成功后停在后台首页
try:
d.execute_script("""var m=document.querySelectorAll('.el-menu-item');var hit=false;
for(var i=0;i<m.length;i++){if((m[i].textContent||'').indexOf('用户列表')>=0&&m[i].offsetParent!==null){m[i].click();hit=true;break;}}
if(!hit){for(var i=0;i<m.length;i++){if(m[i].offsetParent!==null){m[i].click();break;}}}""")
time.sleep(3)
except Exception: pass
log("✅ admin 已登录后台,浏览器保持开启。可手动操作。")
log("浏览器将一直保持开启(如需关闭,请停止此后台任务)...")
# 保持进程存活,浏览器不关闭
while True:
time.sleep(60)
try:
_=d.current_url # 探活
except Exception as e:
log(f"浏览器可能已被手动关闭: {e}","WARN"); break
except KeyboardInterrupt:
log("收到中断,退出(浏览器将关闭)")
finally:
# 注意:按要求不主动quit。仅在进程被杀时浏览器随之关闭。
try: d.quit()
except Exception: pass
if __name__=='__main__':
main()
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""admin后台: 运维设置→类型标签→新增"测试类型新增"→确定→核实。
严格按用户指定时序: 新增后等1s、输入后等1s。send_keys输入, 操作后验证。后台keep-alive保持浏览器。"""
import sys, time, os
from datetime import datetime
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
if sys.platform=='win32':
try: sys.stdout.reconfigure(encoding='utf-8', errors='replace')
except: pass
BASE='https://192.168.9.76'; REPORTS=r"E:\GithubData\ubains-module-test\AuxiliaryTool\ScriptTool\RemoteDeploy\reports"
os.makedirs(REPORTS,exist_ok=True)
TAG_NAME='测试类型新增'
def log(m,l="INFO"): print(f"[{datetime.now().strftime('%H:%M:%S')}] [{l}] {m}",flush=True)
def shot(d,n): d.save_screenshot(os.path.join(REPORTS,f'tag_{n}.png'))
def login_admin(d, max_attempts=5):
for attempt in range(max_attempts):
log(f"登录尝试 #{attempt+1} (admin)")
d.get(f'{BASE}/#/LoginAdmin'); time.sleep(8)
body=d.execute_script('return document.body.innerText.substring(0,300);') or ''
if '运维设置' in body or '组织管理' in body:
log("已处于登录态"); return True
for inp in d.find_elements(By.TAG_NAME,'input'):
if not inp.is_displayed(): continue
p=inp.get_attribute('placeholder') or ''; t=inp.get_attribute('type') or ''
if '账号' in p or '手机' in p or '用户名' in p: inp.clear(); inp.send_keys('admin')
elif t=='password': inp.clear(); inp.send_keys('Admin@2024ub')
elif '验证码' in p or '图形' in p: inp.clear(); inp.send_keys('csba')
time.sleep(2)
d.execute_script("var b=document.querySelector('input[type=submit]');if(b)b.click();")
for i in range(22):
time.sleep(1)
if 'LoginAdmin' not in d.current_url:
time.sleep(3); log(f"✅ 登录成功 {d.current_url}"); return True
b=d.execute_script('return document.body.innerText.substring(0,300);') or ''
if '验证码错误' in b: log("验证码错误,刷新重试"); d.refresh(); time.sleep(6); break
if '密码错误' in b or '账号不存在' in b: log("登录报错","ERROR"); return False
return False
def click_submenu_item(d, submenu_text, item_text):
"""展开指定子菜单(文本匹配)→等2s→点击其下可见item。带重试。"""
for attempt in range(4):
d.execute_script(f"""
var submenus=document.querySelectorAll('.el-submenu');
for(var i=0;i<submenus.length;i++){{
var title=submenus[i].querySelector('.el-submenu__title');
if(title&&(title.textContent||'').indexOf('{submenu_text}')>=0){{
if(!submenus[i].classList.contains('is-opened')){{var t=submenus[i].querySelector('.el-submenu__title');if(t&&t.offsetParent!==null)t.click();}}
break;
}}
}}
""")
time.sleep(2.0)
r=d.execute_script(f"""
var submenus=document.querySelectorAll('.el-submenu');
for(var i=0;i<submenus.length;i++){{
var title=submenus[i].querySelector('.el-submenu__title');
if(title&&(title.textContent||'').indexOf('{submenu_text}')>=0){{
var items=submenus[i].querySelectorAll('.el-menu-item');
for(var j=0;j<items.length;j++){{if(items[j].offsetParent===null)continue;
if((items[j].textContent||'').trim().indexOf('{item_text}')>=0){{items[j].click();return 'clicked';}}}}
return 'no-item-visible';
}}
}}
return 'no-submenu';
""")
log(f" 尝试#{attempt+1} [{submenu_text}→{item_text}]: {r}")
if r=='clicked':
time.sleep(5); return True
time.sleep(1.5)
return False
def main():
opts=Options()
opts.add_argument('--ignore-certificate-errors')
opts.add_argument('--ignore-ssl-errors=yes')
opts.add_argument('--window-size=1920,1080')
d=webdriver.Chrome(options=opts); d.implicitly_wait(8)
try:
if not login_admin(d):
log("❌ 登录失败","ERROR"); return 1
shot(d,'01_logged_in')
# 展开 运维设置 → 类型标签
log("展开[运维设置]→点击[类型标签]...")
if not click_submenu_item(d, '运维设置', '类型标签'):
log("❌ 未能进入类型标签","ERROR"); shot(d,'02_nav_fail')
log("=== 操作中断, 浏览器保持开启 ===")
_keepalive(d); return 2
shot(d,'02_typetag_page')
frag=d.execute_script('return document.body.innerText.substring(0,200);')
log(f"页面片段: {frag[:160]}")
# 点击右上角【新增】
log("点击右上角[新增]...")
r=d.execute_script("""var bs=document.querySelectorAll('button,span,a');
for(var i=0;i<bs.length;i++){var t=(bs[i].textContent||'').trim();
if(t==='新增'&&bs[i].offsetParent!==null){bs[i].click();return 'clicked';}}
return 'notfound';""")
log(f"新增: {r}")
time.sleep(1) # 用户指定: 等1秒
shot(d,'03_add_dialog')
# 类型名称输入"测试类型新增"
log(f"输入类型名称 [{TAG_NAME}]...")
# dump弹窗内input诊断
diag=d.execute_script("""
var ins=document.querySelectorAll('.el-dialog input:not([type=hidden]),.el-drawer input:not([type=hidden]),input');
var r=[];for(var i=0;i<ins.length;i++){if(ins[i].offsetParent===null)continue;
r.push((ins[i].placeholder||'(空)')+'|'+(ins[i].type||''));}
return JSON.stringify(r.slice(0,8));
""")
log(f"新增弹窗内可见输入框: {diag}")
filled=False
for el in d.find_elements(By.TAG_NAME,'input'):
if not el.is_displayed(): continue
t=el.get_attribute('type') or ''
if t in ('hidden','checkbox','radio'): continue
# 跳过只读下拉(如"请选择")和数字/排序框
if el.get_attribute('readonly'): continue
if t=='number': continue
p=el.get_attribute('placeholder') or ''
if any(k in p for k in ['类型','标签','名称']):
el.click(); time.sleep(0.3); el.clear(); el.send_keys(TAG_NAME); filled=True
log(f" 已填入(placeholder={p})"); break
if not filled:
log("未定位到类型名称输入框","WARN")
time.sleep(1) # 用户指定: 等1秒
shot(d,'04_name_filled')
# 点击【确定】
log("点击[确定]...")
r=d.execute_script("""var bs=document.querySelectorAll('.el-dialog__footer button,.dialog-footer button,button');
for(var i=0;i<bs.length;i++){var t=(bs[i].textContent||'').trim().replace(/\\s/g,'');
if((t==='确定')&&bs[i].offsetParent!==null){bs[i].click();return 'clicked';}}
return 'notfound';""")
log(f"确定: {r}")
time.sleep(5)
shot(d,'05_after_confirm')
# 核实: 对话框是否关闭 + 列表是否出现"测试类型新增"
dlg_open=d.execute_script("""var dl=document.querySelectorAll('.el-dialog,.el-drawer');
for(var i=0;i<dl.length;i++){if(dl[i].offsetParent!==null&&(dl[i].textContent||'').indexOf('类型')>=0)return true;}return false;""")
rows=d.execute_script("""
function flat(t){return (t||'').split(String.fromCharCode(10)).join('|');}
var trs=document.querySelectorAll('.el-table__body-wrapper tbody tr,.el-table__row');
var r=[];for(var i=0;i<trs.length;i++){if(trs[i].offsetParent===null)continue;r.push(flat(trs[i].innerText).slice(0,100));}
return JSON.stringify(r.slice(0,10));
""")
log(f"确定后表格行: {rows}")
has=d.execute_script('return document.body.innerText.indexOf(arguments[0])>=0;', TAG_NAME)
log(f"对话框是否仍打开(类型相关): {dlg_open} | 列表含[{TAG_NAME}]: {has}")
if has and not dlg_open:
log(f"✅ 核实通过: 类型标签 [{TAG_NAME}] 添加成功","OK")
res=0
elif has:
log(f"⚠️ 列表含[{TAG_NAME}]但对话框未关闭,需人工确认","WARN"); res=0
else:
log(f"❌ 未在列表中发现[{TAG_NAME}], 添加可能失败","ERROR"); res=2
# 诊断: 红字校验/错误提示
err=d.execute_script('var e=document.querySelectorAll(".el-form-item__error,.el-message__content,.el-messagebox__message");var r=[];for(var i=0;i<e.length;i++){if(e[i].offsetParent!==null)r.push(e[i].textContent.trim());}return JSON.stringify(r);')
if err!='[]': log(f"校验/提示信息: {err}","WARN")
log("=== 操作完成, 浏览器保持开启 ===")
_keepalive(d)
return res
except Exception as e:
log(f"异常: {e}","ERROR"); import traceback; traceback.print_exc(); shot(d,'error')
_keepalive(d); return 1
def _keepalive(d):
log("浏览器keep-alive (停止此后台任务可关闭)...")
try:
while True:
time.sleep(60)
try: _=d.current_url
except Exception as e:
log(f"浏览器可能被手动关闭: {e}","WARN"); break
except KeyboardInterrupt:
pass
finally:
try: d.quit()
except Exception: pass
if __name__=='__main__':
main()
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""admin后台: 展开[运维设置]→进入[人员设置]→输入框查询admin@test是否存在。
后台运行: 完成操作并打印结果后进入keep-alive, 浏览器保持开启。"""
import sys, time, os
from datetime import datetime
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
if sys.platform=='win32':
try: sys.stdout.reconfigure(encoding='utf-8', errors='replace')
except: pass
BASE='https://192.168.9.76'; REPORTS=r"E:\GithubData\ubains-module-test\AuxiliaryTool\ScriptTool\RemoteDeploy\reports"
os.makedirs(REPORTS,exist_ok=True)
TARGET='admin@test'
def log(m,l="INFO"): print(f"[{datetime.now().strftime('%H:%M:%S')}] [{l}] {m}",flush=True)
def shot(d,n): d.save_screenshot(os.path.join(REPORTS,f'ops_{n}.png'))
def login_admin(d, max_attempts=5):
for attempt in range(max_attempts):
log(f"登录尝试 #{attempt+1} (admin)")
d.get(f'{BASE}/#/LoginAdmin'); time.sleep(8)
body=d.execute_script('return document.body.innerText.substring(0,300);') or ''
if '运维设置' in body or '组织管理' in body:
log("已处于登录态"); return True
for inp in d.find_elements(By.TAG_NAME,'input'):
if not inp.is_displayed(): continue
p=inp.get_attribute('placeholder') or ''; t=inp.get_attribute('type') or ''
if '账号' in p or '手机' in p or '用户名' in p: inp.clear(); inp.send_keys('admin')
elif t=='password': inp.clear(); inp.send_keys('Admin@2024ub')
elif '验证码' in p or '图形' in p: inp.clear(); inp.send_keys('csba')
time.sleep(2)
d.execute_script("var b=document.querySelector('input[type=submit]');if(b)b.click();")
for i in range(22):
time.sleep(1)
if 'LoginAdmin' not in d.current_url:
time.sleep(3); log(f"✅ 登录成功 {d.current_url}"); return True
b=d.execute_script('return document.body.innerText.substring(0,300);') or ''
if '验证码错误' in b: log("验证码错误,刷新重试"); d.refresh(); time.sleep(6); break
if '密码错误' in b or '账号不存在' in b: log("登录报错","ERROR"); return False
return False
def click_person(d):
"""展开运维设置 → 等待 → 点击其下可见的'人员设置'。带重试。"""
for attempt in range(4):
# 1) 展开运维设置(若未展开)
d.execute_script("""
var submenus=document.querySelectorAll('.el-submenu');
for(var i=0;i<submenus.length;i++){
var title=submenus[i].querySelector('.el-submenu__title');
if(title&&(title.textContent||'').indexOf('运维设置')>=0){
if(!submenus[i].classList.contains('is-opened')){var t=submenus[i].querySelector('.el-submenu__title');if(t&&t.offsetParent!==null)t.click();}
break;
}
}
""")
time.sleep(2.0) # 等子菜单展开动画
# 2) 点击运维设置下可见的"人员设置"
r=d.execute_script("""
var submenus=document.querySelectorAll('.el-submenu');
for(var i=0;i<submenus.length;i++){
var title=submenus[i].querySelector('.el-submenu__title');
if(title&&(title.textContent||'').indexOf('运维设置')>=0){
var items=submenus[i].querySelectorAll('.el-menu-item');
for(var j=0;j<items.length;j++){if(items[j].offsetParent===null)continue;
if((items[j].textContent||'').trim().indexOf('人员设置')>=0){items[j].click();return 'clicked';}}
return 'no-item-visible';
}
}
return 'no-yunwei';
""")
log(f" 尝试#{attempt+1} 点击人员设置: {r}")
if r=='clicked':
time.sleep(5); return True
time.sleep(1.5)
return False
def main():
opts=Options()
opts.add_argument('--ignore-certificate-errors')
opts.add_argument('--ignore-ssl-errors=yes')
opts.add_argument('--window-size=1920,1080')
d=webdriver.Chrome(options=opts); d.implicitly_wait(8)
try:
if not login_admin(d):
log("❌ 登录失败","ERROR"); return 1
shot(d,'01_logged_in')
log("展开[运维设置]→进入[人员设置]...")
if not click_person(d):
log("❌ 未能进入人员设置","ERROR"); shot(d,'02_nav_fail')
else:
shot(d,'02_person_page')
frag=d.execute_script('return document.body.innerText.substring(0,200);')
log(f"页面片段: {frag[:180]}")
# 输入框查询 admin@test
log(f"在查询框输入 [{TARGET}] 并查询...")
# 先dump所有可见输入框placeholder,定位搜索框
diag=d.execute_script("""
var ins=document.querySelectorAll('input');var r=[];
for(var i=0;i<ins.length;i++){if(ins[i].offsetParent===null)continue;
var t=ins[i].type||'';if(t==='hidden'||t==='checkbox'||t==='radio')continue;
r.push(ins[i].placeholder||'(空)');}
return JSON.stringify(r);
""")
log(f"人员设置页可见输入框placeholder: {diag}")
filled_ph=None
chosen=None
for el in d.find_elements(By.TAG_NAME,'input'):
if not el.is_displayed(): continue
t=el.get_attribute('type') or ''
if t in ('hidden','checkbox','radio'): continue
p=el.get_attribute('placeholder') or ''
if any(k in p for k in ['关键字','搜索','姓名','账号','用户','手机','工号','登录名','名称']):
chosen=el; filled_ph=p; break
# 兜底: 第一个可见text输入框(排除分页/时间类)
if chosen is None:
for el in d.find_elements(By.TAG_NAME,'input'):
if not el.is_displayed(): continue
t=el.get_attribute('type') or ''; p=el.get_attribute('placeholder') or ''
if t=='text' and not any(k in p for k in ['条/页','小时','分钟','时间','区域','部门']):
chosen=el; filled_ph=p; break
if chosen is not None:
chosen.click(); time.sleep(0.3); chosen.clear(); chosen.send_keys(TARGET); time.sleep(0.6)
chosen.send_keys(Keys.ENTER)
log(f"已填入查询框(placeholder={filled_ph})并回车")
d.execute_script("""var bs=document.querySelectorAll('button,span,a');
for(var i=0;i<bs.length;i++){var t=(bs[i].textContent||'').trim();
if((t==='查询'||t==='搜索')&&bs[i].offsetParent!==null){bs[i].click();return;}}""")
time.sleep(4)
shot(d,'03_after_query')
rows=d.execute_script("""
function flat(t){return (t||'').split(String.fromCharCode(10)).join('|');}
var trs=document.querySelectorAll('.el-table__body-wrapper tbody tr,.el-table__row');
var r=[];for(var i=0;i<trs.length;i++){if(trs[i].offsetParent===null)continue;r.push(flat(trs[i].innerText).slice(0,120));}
return JSON.stringify(r.slice(0,10));
""")
log(f"查询后表格行: {rows}")
has=d.execute_script('return document.body.innerText.indexOf(arguments[0])>=0;', TARGET)
nodata=d.execute_script('var b=document.body.innerText;return b.indexOf("暂无数据")>=0;')
log(f"页面含[{TARGET}]: {has} | 显示'暂无数据': {nodata}")
if has:
log(f"✅ 核实通过: 运维设置-人员设置 查询到 [{TARGET}] 用户存在","OK")
else:
log(f"⚠️ 未查询到 [{TARGET}]","WARN")
log("=== 操作完成, 浏览器保持开启 (停止此后台任务可关闭) ===")
# keep-alive: 保持浏览器开启
while True:
time.sleep(60)
try: _=d.current_url
except Exception as e:
log(f"浏览器可能被手动关闭: {e}","WARN"); break
except KeyboardInterrupt:
log("收到中断,退出")
finally:
try: d.quit()
except Exception: pass
if __name__=='__main__':
main()
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""阶段14(稳健版): 前台登录(协议.el-checkbox处理) → 新建会议 → 勾选"测试会议室"行 → 确定创建 → 核实。
登录沿用 verify_meeting_front.py 已验证可用的稳健登录(check_agreement点.el-checkbox)。
全程Selenium页面UI操作, 不碰数据库/接口/配置。"""
import sys, time, os
from datetime import datetime
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
if sys.platform=='win32':
try: sys.stdout.reconfigure(encoding='utf-8', errors='replace')
except: pass
BASE='https://192.168.9.76'; REPORTS=r"E:\GithubData\ubains-module-test\AuxiliaryTool\ScriptTool\RemoteDeploy\reports"
ROOM='测试会议室'
def log(m,l="INFO"): print(f"[{datetime.now().strftime('%H:%M:%S')}] [{l}] {m}",flush=True)
def shot(d,n): d.save_screenshot(os.path.join(REPORTS,f'p14rb_{n}.png'))
def dismiss_dialogs(d):
d.execute_script("""
function clickBtn(kw){var c=document.querySelectorAll('.el-dialog__footer button,.el-message-box button,.el-dialog button,button');
for(var i=0;i<c.length;i++){if(c[i].offsetParent===null)continue;var t=(c[i].textContent||'').trim();
for(var k=0;k<kw.length;k++){if(t.indexOf(kw[k])>=0){c[i].click();return t;}}}return null;}
clickBtn(['同意','我已阅读','我知道了','确定','确认']);
document.querySelectorAll('.el-dialog__headerbtn,.el-message-box__headerbtn').forEach(function(c){if(c.offsetParent!==null)c.click();});
"""); time.sleep(1.5)
def check_agreement(d):
d.execute_script("""
var cbs=document.querySelectorAll('.el-checkbox');var done=false;
cbs.forEach(function(cb){if(cb.offsetParent===null)return;
var ctx=(cb.parentElement.textContent||'')+(cb.textContent||'');
if(ctx.indexOf('协议')>=0||ctx.indexOf('同意')>=0||ctx.indexOf('阅读')>=0){if(!cb.classList.contains('is-checked'))cb.click();done=true;}});
if(!done){var ins=document.querySelectorAll('input[type=checkbox]');
for(var i=0;i<ins.length;i++){if(ins[i].offsetParent!==null&&!ins[i].checked){ins[i].click();break;}}}
"""); time.sleep(1)
def front_login(d):
for attempt in range(6):
d.get(f'{BASE}/'); time.sleep(9); dismiss_dialogs(d)
for inp in d.find_elements(By.TAG_NAME,'input'):
if not inp.is_displayed(): continue
p=inp.get_attribute('placeholder') or ''; t=inp.get_attribute('type') or ''
if '手机' in p or '用户名' in p or '邮箱' in p or '账号' in p: inp.click();time.sleep(0.2);inp.clear();inp.send_keys('admin')
elif t=='password': inp.click();time.sleep(0.2);inp.clear();inp.send_keys('Admin@2024ub')
elif '图形' in p or '验证码' in p: inp.click();time.sleep(0.2);inp.clear();inp.send_keys('csba')
check_agreement(d); time.sleep(1.5)
d.execute_script("""var bs=document.querySelectorAll('button,input[type=submit]');
for(var i=0;i<bs.length;i++){var t=(bs[i].textContent||bs[i].value||'').trim();
if(t.indexOf('登录')>=0&&bs[i].offsetParent!==null){bs[i].click();return;}}""")
time.sleep(2); dismiss_dialogs(d)
for _ in range(20):
time.sleep(1); u=d.current_url
if '%2FHome' in u or ('platform=' in u and 'login' not in u.split('platform=')[1]):
time.sleep(4); return True
dismiss_dialogs(d)
log(f" 前台登录#{attempt+1}未跳转 url={d.current_url}")
d.delete_all_cookies(); time.sleep(2)
return False
def main():
opts=Options(); opts.add_argument('--ignore-certificate-errors'); opts.add_argument('--window-size=1920,1080')
d=webdriver.Chrome(options=opts); d.implicitly_wait(8)
try:
log("="*60); log("阶段14(稳健): 前台新建会议"); log("="*60)
if not front_login(d): log("前台登录失败","ERROR"); shot(d,'login_fail'); return 1
log("✅ 前台登录成功"); shot(d,'01_home')
# 点击新建会议
log("点击[新建会议]...")
r=d.execute_script("""var ns=document.querySelectorAll('span,a,div,button');
for(var i=0;i<ns.length;i++){var t=(ns[i].textContent||'').trim();
if(t==='新建会议'&&ns[i].offsetParent!==null){ns[i].click();return 'clicked';}}return 'notfound';""")
log(f"新建会议: {r}"); time.sleep(6); shot(d,'02_new_meeting')
body2=d.execute_script('return document.body.innerText;') or ''
log(f"新建会议页含[{ROOM}]: {ROOM in body2}")
# 勾选会议室行(用closest找包含"测试会议室"的tr)
log("勾选会议室行...")
sel=d.execute_script(f"""
var cbs=document.querySelectorAll('input[type=checkbox]');
for(var i=0;i<cbs.length;i++){{if(cbs[i].offsetParent===null)continue;
var row=cbs[i].closest('tr')||cbs[i].closest('.el-table__row');
if(row&&(row.textContent||'').indexOf('{ROOM}')>=0&&!cbs[i].checked){{cbs[i].click();return 'checked';}}}}
return 'notfound';
""")
log(f"勾选结果: {sel}"); time.sleep(2); shot(d,'03_room_selected')
# 点确定创建(可能是"确定创建"/"确定")
rc=d.execute_script("""var bs=document.querySelectorAll('button');
for(var i=0;i<bs.length;i++){var t=(bs[i].textContent||'').trim();
if((t.indexOf('确定创建')>=0||t==='确定')&&bs[i].offsetParent!==null){bs[i].click();return 'clicked:'+t;}}
return 'notfound';""")
log(f"确定创建: {rc}"); time.sleep(6); dismiss_dialogs(d); shot(d,'04_after_create')
# 核实会议是否创建(查看详情 / 会议列表含测试会议室)
rv=d.execute_script("""var bs=document.querySelectorAll('button,a,span');
for(var i=0;i<bs.length;i++){var t=(bs[i].textContent||'').trim();
if(t.indexOf('查看详情')>=0&&bs[i].offsetParent!==null){bs[i].click();return 'clicked';}}return 'notfound';""")
log(f"查看详情: {rv}"); time.sleep(5); shot(d,'05_detail')
body=d.execute_script('return document.body.innerText;') or ''
has_meeting = ROOM in body
log(f"详情页含[{ROOM}]: {has_meeting}")
log(f"页面片段: {body[:400]}")
# 兜底:进我的会议/今日会议查
if not has_meeting:
for kw in ['我的会议','今日会议','会议列表','我的预定']:
d.execute_script(f"""var ns=document.querySelectorAll('span,a,div,button');
for(var i=0;i<ns.length;i++){{var t=(ns[i].textContent||'').trim();
if(t==='{kw}'&&ns[i].offsetParent!==null){{ns[i].click();return;}}}}""")
time.sleep(4)
b=d.execute_script('return document.body.innerText;') or ''
if ROOM in b:
has_meeting=True; log(f"在[{kw}]页找到[{ROOM}]会议记录 —— 会议创建成功"); shot(d,'06_meeting_found'); break
log("✅ 阶段14完成(会议已创建)" if has_meeting else "⚠️ 阶段14: 会议创建待确认","OK" if has_meeting else "WARN")
return 0 if has_meeting else 2
except Exception as e:
log(f"异常: {e}","ERROR"); import traceback; traceback.print_exc(); shot(d,'error'); return 1
finally:
d.quit()
if __name__=='__main__':
sys.exit(main())
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""核实会议是否创建: 前台登录 → 首页"会议列表"模块 → dump会议记录。
只读核实, 不修改。用于阶段14结果判定。"""
import sys, time, os
from datetime import datetime
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
if sys.platform=='win32':
try: sys.stdout.reconfigure(encoding='utf-8', errors='replace')
except: pass
BASE='https://192.168.9.76'; REPORTS=r"E:\GithubData\ubains-module-test\AuxiliaryTool\ScriptTool\RemoteDeploy\reports"
ROOM='测试会议室'
def log(m,l="INFO"): print(f"[{datetime.now().strftime('%H:%M:%S')}] [{l}] {m}",flush=True)
def shot(d,n): d.save_screenshot(os.path.join(REPORTS,f'vlist_{n}.png'))
def dismiss(d):
d.execute_script("""function cb(kw){var c=document.querySelectorAll('button');for(var i=0;i<c.length;i++){if(c.offsetParent===null&&false)continue;if(c[i].offsetParent===null)continue;var t=(c[i].textContent||'').trim();for(var k=0;k<kw.length;k++){if(t.indexOf(kw[k])>=0){c[i].click();return;}}}return null;}cb(['同意','我知道了','确定','确认']);"""); time.sleep(1.2)
def check_agr(d):
d.execute_script("""var cbs=document.querySelectorAll('.el-checkbox');cbs.forEach(function(cb){if(cb.offsetParent===null)return;var ctx=(cb.parentElement.textContent||'')+(cb.textContent||'');if(ctx.indexOf('协议')>=0||ctx.indexOf('同意')>=0){if(!cb.classList.contains('is-checked'))cb.click();}});"""); time.sleep(1)
def flogin(d):
for a in range(6):
d.get(f'{BASE}/'); time.sleep(9); dismiss(d)
for inp in d.find_elements(By.TAG_NAME,'input'):
if not inp.is_displayed(): continue
p=inp.get_attribute('placeholder') or ''; t=inp.get_attribute('type') or ''
if '手机' in p or '用户名' in p or '账号' in p: inp.clear(); inp.send_keys('admin')
elif t=='password': inp.clear(); inp.send_keys('Admin@2024ub')
elif '图形' in p or '验证码' in p: inp.clear(); inp.send_keys('csba')
check_agr(d); time.sleep(1.5)
d.execute_script("var bs=document.querySelectorAll('button');for(var i=0;i<bs.length;i++){if((bs[i].textContent||'').indexOf('登录')>=0&&bs[i].offsetParent!==null){bs[i].click();return;}}")
time.sleep(2); dismiss(d)
for _ in range(20):
time.sleep(1); u=d.current_url
if '%2FHome' in u or ('platform=' in u and 'login' not in u.split('platform=')[1]): time.sleep(4); return True
dismiss(d)
d.delete_all_cookies(); time.sleep(2)
return False
def dump_meetings(d):
return d.execute_script("""
function flat(t){return (t||'').split(String.fromCharCode(10)).join('|');}
// 找所有会议表格行
var trs=document.querySelectorAll('.el-table__body-wrapper tbody tr,.el-table__row');
var r=[];
for(var i=0;i<trs.length;i++){if(trs[i].offsetParent===null)continue;
r.push(flat(trs[i].innerText).slice(0,120));}
return JSON.stringify(r.slice(0,20));
""")
def main():
opts=Options(); opts.add_argument('--ignore-certificate-errors'); opts.add_argument('--window-size=1920,1080')
d=webdriver.Chrome(options=opts); d.implicitly_wait(8)
try:
if not flogin(d): log("登录失败","ERROR"); return 1
log("✅ 前台登录成功"); time.sleep(5); shot(d,'01_home')
# 首页正文
body=d.execute_script('return document.body.innerText;') or ''
log(f"首页含[{ROOM}]: {ROOM in body}")
# 点"会议列表"(首页模块/菜单)
for kw in ['会议列表','我的会议','今日会议','会议日程']:
r=d.execute_script(f"""var ns=document.querySelectorAll('span,a,div,button,.el-menu-item');
for(var i=0;i<ns.length;i++){{var t=(ns[i].textContent||'').trim();
if(t==='{kw}'&&ns[i].offsetParent!==null){{ns[i].click();return 'clicked';}}}}return 'notfound';""")
log(f"点击[{kw}]: {r}"); time.sleep(4)
b=d.execute_script('return document.body.innerText;') or ''
if ROOM in b:
log(f"✅ [{kw}]页含[{ROOM}] —— 会议已创建"); shot(d,f'02_found_{kw}'); break
shot(d,'03_list_page')
log("=== 当前页会议表格行 ===")
log(dump_meetings(d))
# 最终判定
final_body=d.execute_script('return document.body.innerText;') or ''
has = ROOM in final_body
log("✅ 核实通过: 会议列表含测试会议室" if has else "⚠️ 会议列表未见测试会议室(会议可能未创建成功)","OK" if has else "WARN")
return 0 if has else 2
except Exception as e:
log(f"异常: {e}","ERROR"); import traceback; traceback.print_exc(); return 1
finally:
d.quit()
if __name__=='__main__':
sys.exit(main())
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论