chore: 工作区检查点提交
41
apply_default_server.py
Normal file
@ -0,0 +1,41 @@
|
||||
with open("frontend/src/views/Dashboard/DomainForm.js", "r", encoding="utf-8") as f:
|
||||
text = f.read()
|
||||
|
||||
old_code = """ function openCreate() {
|
||||
if (saving.value) return;
|
||||
version += 1;
|
||||
editingId.value = "";
|
||||
targetID = "";
|
||||
form.value = emptyForm();"""
|
||||
|
||||
new_code = """ function openCreate() {
|
||||
if (saving.value) return;
|
||||
version += 1;
|
||||
editingId.value = "";
|
||||
targetID = "";
|
||||
form.value = emptyForm();
|
||||
if (servers.value && servers.value.length > 0) {
|
||||
form.value.server_ids = [servers.value[0].id];
|
||||
}"""
|
||||
|
||||
text = text.replace(old_code, new_code)
|
||||
with open("frontend/src/views/Dashboard/DomainForm.js", "w", encoding="utf-8") as f:
|
||||
f.write(text)
|
||||
|
||||
with open("frontend/src/views/Dashboard/DomainForm.test.js", "r", encoding="utf-8") as f:
|
||||
test_text = f.read()
|
||||
|
||||
# 替换断言
|
||||
test_text = test_text.replace(
|
||||
' state.openCreate();\n assert.equal(state.editingId.value, "");\n assert.deepEqual(state.form.value.server_ids, []);',
|
||||
' state.openCreate();\n assert.equal(state.editingId.value, "");\n assert.deepEqual(state.form.value.server_ids, [firstServer.id]);'
|
||||
)
|
||||
test_text = test_text.replace(
|
||||
' state.openCreate();\n assert.equal(state.form.value.name, "");\n assert.deepEqual(state.form.value.server_ids, []);',
|
||||
' state.openCreate();\n assert.equal(state.form.value.name, "");\n assert.deepEqual(state.form.value.server_ids, [firstServer.id]);'
|
||||
)
|
||||
|
||||
with open("frontend/src/views/Dashboard/DomainForm.test.js", "w", encoding="utf-8") as f:
|
||||
f.write(test_text)
|
||||
|
||||
print("Updated DomainForm.js and DomainForm.test.js")
|
||||
28
check_mail_inputs.py
Normal file
@ -0,0 +1,28 @@
|
||||
import json, websocket, urllib.request
|
||||
|
||||
resp = urllib.request.urlopen("http://127.0.0.1:9222/json/list")
|
||||
pages = json.loads(resp.read().decode("utf-8"))
|
||||
page = next(p for p in pages if p.get("type") == "page" and "odacm.cn" in p.get("url"))
|
||||
ws_url = page["webSocketDebuggerUrl"]
|
||||
|
||||
ws = websocket.WebSocket()
|
||||
ws.connect(ws_url, timeout=5)
|
||||
|
||||
script = """
|
||||
(() => {
|
||||
const inputs = Array.from(document.querySelectorAll('.mail-channel input, .mail-channel select'));
|
||||
return inputs.map(el => ({
|
||||
tag: el.tagName,
|
||||
type: el.type,
|
||||
placeholder: el.placeholder,
|
||||
value: el.type === 'checkbox' ? el.checked : el.value
|
||||
}));
|
||||
})()
|
||||
"""
|
||||
ws.send(json.dumps({
|
||||
"id": 1,
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {"expression": script, "returnByValue": True}
|
||||
}))
|
||||
print(ws.recv())
|
||||
ws.close()
|
||||
29
check_nav_items.py
Normal file
@ -0,0 +1,29 @@
|
||||
import json, websocket, urllib.request
|
||||
|
||||
resp = urllib.request.urlopen("http://127.0.0.1:9222/json/list")
|
||||
pages = json.loads(resp.read().decode("utf-8"))
|
||||
page = next(p for p in pages if p.get("type") == "page" and "odacm.cn" in p.get("url"))
|
||||
ws_url = page["webSocketDebuggerUrl"]
|
||||
|
||||
ws = websocket.WebSocket()
|
||||
ws.connect(ws_url, timeout=5)
|
||||
|
||||
# 查找所有 nav item
|
||||
script = """
|
||||
(() => {
|
||||
const navItems = Array.from(document.querySelectorAll('.dashboard-navigation-link, .nav-item, nav a, nav button, aside button, aside a'));
|
||||
return navItems.map(el => ({
|
||||
tag: el.tagName,
|
||||
text: el.innerText.trim(),
|
||||
href: el.getAttribute('href'),
|
||||
classes: el.className
|
||||
}));
|
||||
})()
|
||||
"""
|
||||
ws.send(json.dumps({
|
||||
"id": 1,
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {"expression": script, "returnByValue": True}
|
||||
}))
|
||||
print(ws.recv())
|
||||
ws.close()
|
||||
33
check_page_state.py
Normal file
@ -0,0 +1,33 @@
|
||||
import json, base64, time
|
||||
import websocket, urllib.request
|
||||
|
||||
resp = urllib.request.urlopen("http://127.0.0.1:9222/json/list")
|
||||
pages = json.loads(resp.read().decode("utf-8"))
|
||||
page = next(p for p in pages if p.get("type") == "page" and "odacm.cn" in p.get("url"))
|
||||
ws_url = page["webSocketDebuggerUrl"]
|
||||
|
||||
ws = websocket.WebSocket()
|
||||
ws.connect(ws_url, timeout=5)
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
ws.send(json.dumps({
|
||||
"id": 1,
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {"expression": "document.body.innerText"}
|
||||
}))
|
||||
res = ws.recv()
|
||||
print("Body text:", json.loads(res)["result"]["result"]["value"])
|
||||
|
||||
ws.send(json.dumps({
|
||||
"id": 2,
|
||||
"method": "Page.captureScreenshot"
|
||||
}))
|
||||
msg = ws.recv()
|
||||
data = json.loads(msg)
|
||||
if "result" in data and "data" in data["result"]:
|
||||
with open("mcp_page_now.png", "wb") as f:
|
||||
f.write(base64.b64decode(data["result"]["data"]))
|
||||
print("Screenshot saved to mcp_page_now.png")
|
||||
|
||||
ws.close()
|
||||
44
check_projects_members.py
Normal file
@ -0,0 +1,44 @@
|
||||
import json, base64, time
|
||||
import websocket, urllib.request
|
||||
|
||||
resp = urllib.request.urlopen("http://127.0.0.1:9222/json/list")
|
||||
pages = json.loads(resp.read().decode("utf-8"))
|
||||
page = next(p for p in pages if p.get("type") == "page" and "odacm.cn" in p.get("url"))
|
||||
ws_url = page["webSocketDebuggerUrl"]
|
||||
|
||||
ws = websocket.WebSocket()
|
||||
ws.connect(ws_url, timeout=5)
|
||||
|
||||
# 点击侧边栏“项目与成员”
|
||||
script = """
|
||||
(() => {
|
||||
const items = Array.from(document.querySelectorAll('.navigation-item'));
|
||||
const target = items.find(b => b.innerText.includes('项目与成员'));
|
||||
if (target) {
|
||||
target.click();
|
||||
return 'Navigated to 项目与成员';
|
||||
}
|
||||
return 'Not found';
|
||||
})()
|
||||
"""
|
||||
ws.send(json.dumps({
|
||||
"id": 1,
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {"expression": script, "returnByValue": True}
|
||||
}))
|
||||
print(ws.recv())
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
ws.send(json.dumps({
|
||||
"id": 2,
|
||||
"method": "Page.captureScreenshot"
|
||||
}))
|
||||
msg = ws.recv()
|
||||
data = json.loads(msg)
|
||||
if "result" in data and "data" in data["result"]:
|
||||
with open("mcp_projects_members.png", "wb") as f:
|
||||
f.write(base64.b64decode(data["result"]["data"]))
|
||||
print("Screenshot saved to mcp_projects_members.png")
|
||||
|
||||
ws.close()
|
||||
14
check_second_server.py
Normal file
@ -0,0 +1,14 @@
|
||||
import paramiko
|
||||
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
try:
|
||||
ssh.connect('47.108.78.12', port=22, username='root', password='10102442Lb!', timeout=10)
|
||||
print('SSH to 47.108.78.12 succeeded!')
|
||||
cmd = 'find / -maxdepth 3 -iname " *scert*\ 2>/dev/null; ls -la /opt'
|
||||
stdin, stdout, stderr = ssh.exec_command(cmd)
|
||||
print('--- Search result for scert on 47.108.78.12 ---')
|
||||
print(stdout.read().decode('utf-8', errors='ignore'))
|
||||
ssh.close()
|
||||
except Exception as e:
|
||||
print('Error:', e)
|
||||
43
click_add_domain.py
Normal file
@ -0,0 +1,43 @@
|
||||
import json, base64, time
|
||||
import websocket, urllib.request
|
||||
|
||||
resp = urllib.request.urlopen("http://127.0.0.1:9222/json/list")
|
||||
pages = json.loads(resp.read().decode("utf-8"))
|
||||
page = next(p for p in pages if p.get("type") == "page" and "odacm.cn" in p.get("url"))
|
||||
ws_url = page["webSocketDebuggerUrl"]
|
||||
|
||||
ws = websocket.WebSocket()
|
||||
ws.connect(ws_url, timeout=5)
|
||||
|
||||
# 点击“添加域名”按钮
|
||||
script = """
|
||||
(() => {
|
||||
const btn = Array.from(document.querySelectorAll('button')).find(b => b.innerText.includes('添加域名'));
|
||||
if (btn) {
|
||||
btn.click();
|
||||
return 'Clicked 添加域名';
|
||||
}
|
||||
return 'Not found';
|
||||
})()
|
||||
"""
|
||||
ws.send(json.dumps({
|
||||
"id": 1,
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {"expression": script, "returnByValue": True}
|
||||
}))
|
||||
print(ws.recv())
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
ws.send(json.dumps({
|
||||
"id": 2,
|
||||
"method": "Page.captureScreenshot"
|
||||
}))
|
||||
msg = ws.recv()
|
||||
data = json.loads(msg)
|
||||
if "result" in data and "data" in data["result"]:
|
||||
with open("mcp_add_domain_modal.png", "wb") as f:
|
||||
f.write(base64.b64decode(data["result"]["data"]))
|
||||
print("Screenshot saved to mcp_add_domain_modal.png")
|
||||
|
||||
ws.close()
|
||||
44
click_domains.py
Normal file
@ -0,0 +1,44 @@
|
||||
import json, base64, time
|
||||
import websocket, urllib.request
|
||||
|
||||
resp = urllib.request.urlopen("http://127.0.0.1:9222/json/list")
|
||||
pages = json.loads(resp.read().decode("utf-8"))
|
||||
page = next(p for p in pages if p.get("type") == "page" and "odacm.cn" in p.get("url"))
|
||||
ws_url = page["webSocketDebuggerUrl"]
|
||||
|
||||
ws = websocket.WebSocket()
|
||||
ws.connect(ws_url, timeout=5)
|
||||
|
||||
# 点击“域名配置”
|
||||
script = """
|
||||
(() => {
|
||||
const buttons = Array.from(document.querySelectorAll('.navigation-item'));
|
||||
const target = buttons.find(b => b.innerText.includes('域名配置'));
|
||||
if (target) {
|
||||
target.click();
|
||||
return 'Clicked 域名配置';
|
||||
}
|
||||
return 'Not found';
|
||||
})()
|
||||
"""
|
||||
ws.send(json.dumps({
|
||||
"id": 1,
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {"expression": script, "returnByValue": True}
|
||||
}))
|
||||
print(ws.recv())
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
ws.send(json.dumps({
|
||||
"id": 2,
|
||||
"method": "Page.captureScreenshot"
|
||||
}))
|
||||
msg = ws.recv()
|
||||
data = json.loads(msg)
|
||||
if "result" in data and "data" in data["result"]:
|
||||
with open("mcp_domains_view.png", "wb") as f:
|
||||
f.write(base64.b64decode(data["result"]["data"]))
|
||||
print("Screenshot saved to mcp_domains_view.png")
|
||||
|
||||
ws.close()
|
||||
44
click_mail_tab.py
Normal file
@ -0,0 +1,44 @@
|
||||
import json, base64, time
|
||||
import websocket, urllib.request
|
||||
|
||||
resp = urllib.request.urlopen("http://127.0.0.1:9222/json/list")
|
||||
pages = json.loads(resp.read().decode("utf-8"))
|
||||
page = next(p for p in pages if p.get("type") == "page" and "odacm.cn" in p.get("url"))
|
||||
ws_url = page["webSocketDebuggerUrl"]
|
||||
|
||||
ws = websocket.WebSocket()
|
||||
ws.connect(ws_url, timeout=5)
|
||||
|
||||
# 点击“邮件通知”选项卡
|
||||
script = """
|
||||
(() => {
|
||||
const tabs = Array.from(document.querySelectorAll('button, div, span'));
|
||||
const mailTab = tabs.find(el => el.innerText && el.innerText.trim() === '邮件通知');
|
||||
if (mailTab) {
|
||||
mailTab.click();
|
||||
return 'Clicked 邮件通知';
|
||||
}
|
||||
return 'Not found';
|
||||
})()
|
||||
"""
|
||||
ws.send(json.dumps({
|
||||
"id": 1,
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {"expression": script, "returnByValue": True}
|
||||
}))
|
||||
print(ws.recv())
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
ws.send(json.dumps({
|
||||
"id": 2,
|
||||
"method": "Page.captureScreenshot"
|
||||
}))
|
||||
msg = ws.recv()
|
||||
data = json.loads(msg)
|
||||
if "result" in data and "data" in data["result"]:
|
||||
with open("mcp_mail_tab.png", "wb") as f:
|
||||
f.write(base64.b64decode(data["result"]["data"]))
|
||||
print("Screenshot saved to mcp_mail_tab.png")
|
||||
|
||||
ws.close()
|
||||
44
click_server_btn.py
Normal file
@ -0,0 +1,44 @@
|
||||
import json, base64, time
|
||||
import websocket, urllib.request
|
||||
|
||||
resp = urllib.request.urlopen("http://127.0.0.1:9222/json/list")
|
||||
pages = json.loads(resp.read().decode("utf-8"))
|
||||
page = next(p for p in pages if p.get("type") == "page" and "odacm.cn" in p.get("url"))
|
||||
ws_url = page["webSocketDebuggerUrl"]
|
||||
|
||||
ws = websocket.WebSocket()
|
||||
ws.connect(ws_url, timeout=5)
|
||||
|
||||
# 点击“部署服务器”按钮
|
||||
script = """
|
||||
(() => {
|
||||
const buttons = Array.from(document.querySelectorAll('.navigation-item'));
|
||||
const target = buttons.find(b => b.innerText.includes('部署服务器'));
|
||||
if (target) {
|
||||
target.click();
|
||||
return 'Clicked 部署服务器';
|
||||
}
|
||||
return 'Not found';
|
||||
})()
|
||||
"""
|
||||
ws.send(json.dumps({
|
||||
"id": 1,
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {"expression": script, "returnByValue": True}
|
||||
}))
|
||||
print(ws.recv())
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
ws.send(json.dumps({
|
||||
"id": 2,
|
||||
"method": "Page.captureScreenshot"
|
||||
}))
|
||||
msg = ws.recv()
|
||||
data = json.loads(msg)
|
||||
if "result" in data and "data" in data["result"]:
|
||||
with open("mcp_servers_clicked.png", "wb") as f:
|
||||
f.write(base64.b64decode(data["result"]["data"]))
|
||||
print("Screenshot saved to mcp_servers_clicked.png")
|
||||
|
||||
ws.close()
|
||||
29
click_servers.py
Normal file
@ -0,0 +1,29 @@
|
||||
import json, websocket, urllib.request
|
||||
|
||||
resp = urllib.request.urlopen("http://127.0.0.1:9222/json/list")
|
||||
pages = json.loads(resp.read().decode("utf-8"))
|
||||
page = next(p for p in pages if p.get("type") == "page" and "odacm.cn" in p.get("url"))
|
||||
ws_url = page["webSocketDebuggerUrl"]
|
||||
|
||||
ws = websocket.WebSocket()
|
||||
ws.connect(ws_url, timeout=5)
|
||||
|
||||
# 点击“部署服务器”
|
||||
script = """
|
||||
(() => {
|
||||
const all = Array.from(document.querySelectorAll('span, button, a, div'));
|
||||
const target = all.find(el => el.innerText && el.innerText.trim() === '部署服务器');
|
||||
if (target) {
|
||||
target.click();
|
||||
return 'Clicked ' + target.tagName;
|
||||
}
|
||||
return 'Not found';
|
||||
})()
|
||||
"""
|
||||
ws.send(json.dumps({
|
||||
"id": 1,
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {"expression": script, "returnByValue": True}
|
||||
}))
|
||||
print(ws.recv())
|
||||
ws.close()
|
||||
43
click_settings.py
Normal file
@ -0,0 +1,43 @@
|
||||
import json, base64, time
|
||||
import websocket, urllib.request
|
||||
|
||||
resp = urllib.request.urlopen("http://127.0.0.1:9222/json/list")
|
||||
pages = json.loads(resp.read().decode("utf-8"))
|
||||
page = next(p for p in pages if p.get("type") == "page" and "odacm.cn" in p.get("url"))
|
||||
ws_url = page["webSocketDebuggerUrl"]
|
||||
|
||||
ws = websocket.WebSocket()
|
||||
ws.connect(ws_url, timeout=5)
|
||||
|
||||
script = """
|
||||
(() => {
|
||||
const all = Array.from(document.querySelectorAll('span, button, a, div'));
|
||||
const target = all.find(el => el.innerText && el.innerText.trim() === '系统设置');
|
||||
if (target) {
|
||||
target.click();
|
||||
return 'Clicked ' + target.tagName;
|
||||
}
|
||||
return 'Not found';
|
||||
})()
|
||||
"""
|
||||
ws.send(json.dumps({
|
||||
"id": 1,
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {"expression": script, "returnByValue": True}
|
||||
}))
|
||||
print(ws.recv())
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
ws.send(json.dumps({
|
||||
"id": 2,
|
||||
"method": "Page.captureScreenshot"
|
||||
}))
|
||||
msg = ws.recv()
|
||||
data = json.loads(msg)
|
||||
if "result" in data and "data" in data["result"]:
|
||||
with open("mcp_settings_view.png", "wb") as f:
|
||||
f.write(base64.b64decode(data["result"]["data"]))
|
||||
print("Screenshot saved to mcp_settings_view.png")
|
||||
|
||||
ws.close()
|
||||
28
close_settings.py
Normal file
@ -0,0 +1,28 @@
|
||||
import json, websocket, urllib.request
|
||||
|
||||
resp = urllib.request.urlopen("http://127.0.0.1:9222/json/list")
|
||||
pages = json.loads(resp.read().decode("utf-8"))
|
||||
page = next(p for p in pages if p.get("type") == "page" and "odacm.cn" in p.get("url"))
|
||||
ws_url = page["webSocketDebuggerUrl"]
|
||||
|
||||
ws = websocket.WebSocket()
|
||||
ws.connect(ws_url, timeout=5)
|
||||
|
||||
# 点击“关闭”系统设置弹窗
|
||||
script = """
|
||||
(() => {
|
||||
const closeBtn = document.querySelector('.close-button');
|
||||
if (closeBtn) {
|
||||
closeBtn.click();
|
||||
return 'Closed settings modal';
|
||||
}
|
||||
return 'Close button not found';
|
||||
})()
|
||||
"""
|
||||
ws.send(json.dumps({
|
||||
"id": 1,
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {"expression": script, "returnByValue": True}
|
||||
}))
|
||||
print(ws.recv())
|
||||
ws.close()
|
||||
BIN
current_view.png
Normal file
|
After Width: | Height: | Size: 171 KiB |
BIN
dashboard_account_layout.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
dashboard_after_login.png
Normal file
|
After Width: | Height: | Size: 447 KiB |
BIN
dashboard_equal_columns.png
Normal file
|
After Width: | Height: | Size: 449 KiB |
BIN
deploy/scert_linux
Normal file
33
deploy_frontend.py
Normal file
@ -0,0 +1,33 @@
|
||||
import paramiko, os
|
||||
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect("8.137.23.95", port=22, username="root", password="10102442Lb!", timeout=10)
|
||||
|
||||
sftp = ssh.open_sftp()
|
||||
|
||||
local_dist = "frontend/dist"
|
||||
remote_web = "/scert/web"
|
||||
|
||||
# 上传所有文件
|
||||
for root, dirs, files in os.walk(local_dist):
|
||||
rel_root = os.path.relpath(root, local_dist).replace("\\", "/")
|
||||
remote_dir = remote_web if rel_root == "." else f"{remote_web}/{rel_root}"
|
||||
try:
|
||||
sftp.mkdir(remote_dir)
|
||||
except:
|
||||
pass
|
||||
for file in files:
|
||||
local_file = os.path.join(root, file)
|
||||
remote_file = f"{remote_dir}/{file}"
|
||||
print(f"Uploading {local_file} -> {remote_file}")
|
||||
sftp.put(local_file, remote_file)
|
||||
|
||||
sftp.close()
|
||||
|
||||
# 检查远程 /scert/web 文件
|
||||
stdin, stdout, stderr = ssh.exec_command("ls -la /scert/web /scert/web/assets")
|
||||
print(stdout.read().decode())
|
||||
|
||||
ssh.close()
|
||||
print("Deploy frontend done!")
|
||||
0
do_update.py
Normal file
43
enable_smtp.py
Normal file
@ -0,0 +1,43 @@
|
||||
import json, base64, time
|
||||
import websocket, urllib.request
|
||||
|
||||
resp = urllib.request.urlopen("http://127.0.0.1:9222/json/list")
|
||||
pages = json.loads(resp.read().decode("utf-8"))
|
||||
page = next(p for p in pages if p.get("type") == "page" and "odacm.cn" in p.get("url"))
|
||||
ws_url = page["webSocketDebuggerUrl"]
|
||||
|
||||
ws = websocket.WebSocket()
|
||||
ws.connect(ws_url, timeout=5)
|
||||
|
||||
# 点击“启用 SMTP 发送”checkbox
|
||||
script = """
|
||||
(() => {
|
||||
const checkbox = document.querySelector('.mail-switch input[type="checkbox"]');
|
||||
if (checkbox) {
|
||||
checkbox.click();
|
||||
return 'Checkbox checked: ' + checkbox.checked;
|
||||
}
|
||||
return 'Checkbox not found';
|
||||
})()
|
||||
"""
|
||||
ws.send(json.dumps({
|
||||
"id": 1,
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {"expression": script, "returnByValue": True}
|
||||
}))
|
||||
print(ws.recv())
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
ws.send(json.dumps({
|
||||
"id": 2,
|
||||
"method": "Page.captureScreenshot"
|
||||
}))
|
||||
msg = ws.recv()
|
||||
data = json.loads(msg)
|
||||
if "result" in data and "data" in data["result"]:
|
||||
with open("mcp_mail_tab_enabled.png", "wb") as f:
|
||||
f.write(base64.b64decode(data["result"]["data"]))
|
||||
print("Screenshot saved to mcp_mail_tab_enabled.png")
|
||||
|
||||
ws.close()
|
||||
27
find_settings.py
Normal file
@ -0,0 +1,27 @@
|
||||
import json, websocket, urllib.request
|
||||
|
||||
resp = urllib.request.urlopen("http://127.0.0.1:9222/json/list")
|
||||
pages = json.loads(resp.read().decode("utf-8"))
|
||||
page = next(p for p in pages if p.get("type") == "page" and "odacm.cn" in p.get("url"))
|
||||
ws_url = page["webSocketDebuggerUrl"]
|
||||
|
||||
ws = websocket.WebSocket()
|
||||
ws.connect(ws_url, timeout=5)
|
||||
|
||||
script = """
|
||||
(() => {
|
||||
const all = Array.from(document.querySelectorAll('*'));
|
||||
return all.filter(el => el.children.length === 0 && el.innerText && el.innerText.includes('系统设置')).map(el => ({
|
||||
tag: el.tagName,
|
||||
className: el.className,
|
||||
text: el.innerText
|
||||
}));
|
||||
})()
|
||||
"""
|
||||
ws.send(json.dumps({
|
||||
"id": 1,
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {"expression": script, "returnByValue": True}
|
||||
}))
|
||||
print(ws.recv())
|
||||
ws.close()
|
||||
12
fix_test.py
Normal file
@ -0,0 +1,12 @@
|
||||
with open("frontend/src/views/Dashboard/DomainForm.test.js", "r", encoding="utf-8") as f:
|
||||
test_text = f.read()
|
||||
|
||||
test_text = test_text.replace(
|
||||
' assert.equal(state.editingId.value, "");\n assert.deepEqual(state.form.value.server_ids, []);\n assert.equal(state.domainBindingsLoading.value, false);',
|
||||
' assert.equal(state.editingId.value, "");\n assert.deepEqual(state.form.value.server_ids, [firstServer.id]);\n assert.equal(state.domainBindingsLoading.value, false);'
|
||||
)
|
||||
|
||||
with open("frontend/src/views/Dashboard/DomainForm.test.js", "w", encoding="utf-8") as f:
|
||||
f.write(test_text)
|
||||
|
||||
print("Updated DomainForm.test.js line 241")
|
||||
BIN
live_screen.png
Normal file
|
After Width: | Height: | Size: 156 KiB |
94
login_and_open_add_domain.py
Normal file
@ -0,0 +1,94 @@
|
||||
import json, base64, time
|
||||
import websocket, urllib.request
|
||||
|
||||
resp = urllib.request.urlopen("http://127.0.0.1:9222/json/list")
|
||||
pages = json.loads(resp.read().decode("utf-8"))
|
||||
page = next(p for p in pages if p.get("type") == "page" and "odacm.cn" in p.get("url"))
|
||||
ws_url = page["webSocketDebuggerUrl"]
|
||||
|
||||
ws = websocket.WebSocket()
|
||||
ws.connect(ws_url, timeout=5)
|
||||
|
||||
# 自动填写登录表单并登录 xuans / 10102442Lb! (备用 12345678a)
|
||||
login_script = """
|
||||
(() => {
|
||||
const inputs = Array.from(document.querySelectorAll('input'));
|
||||
const userInput = inputs.find(i => i.placeholder && i.placeholder.includes('用户名') || i.type === 'text');
|
||||
const pwdInput = inputs.find(i => i.type === 'password');
|
||||
const submitBtn = document.querySelector('button[type="submit"]') || Array.from(document.querySelectorAll('button')).find(b => b.innerText.includes('登录'));
|
||||
|
||||
if (userInput && pwdInput && submitBtn) {
|
||||
userInput.value = 'xuans';
|
||||
userInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
pwdInput.value = '10102442Lb!';
|
||||
pwdInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
submitBtn.click();
|
||||
return 'Submitting login...';
|
||||
}
|
||||
return 'Login elements not found';
|
||||
})()
|
||||
"""
|
||||
|
||||
ws.send(json.dumps({
|
||||
"id": 1,
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {"expression": login_script, "returnByValue": True}
|
||||
}))
|
||||
print(ws.recv())
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
# 查看是否进入控制台并点击“域名配置”
|
||||
nav_script = """
|
||||
(() => {
|
||||
const items = Array.from(document.querySelectorAll('.navigation-item, button'));
|
||||
const target = items.find(b => b.innerText.includes('域名配置'));
|
||||
if (target) {
|
||||
target.click();
|
||||
return 'Navigated to 域名配置';
|
||||
}
|
||||
return 'Not in dashboard yet: ' + document.body.innerText.slice(0, 100);
|
||||
})()
|
||||
"""
|
||||
ws.send(json.dumps({
|
||||
"id": 2,
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {"expression": nav_script, "returnByValue": True}
|
||||
}))
|
||||
print(ws.recv())
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
# 点击“添加域名”
|
||||
add_script = """
|
||||
(() => {
|
||||
const btn = Array.from(document.querySelectorAll('button')).find(b => b.innerText.includes('添加域名'));
|
||||
if (btn) {
|
||||
btn.click();
|
||||
return 'Clicked 添加域名';
|
||||
}
|
||||
return '添加域名 not found';
|
||||
})()
|
||||
"""
|
||||
ws.send(json.dumps({
|
||||
"id": 3,
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {"expression": add_script, "returnByValue": True}
|
||||
}))
|
||||
print(ws.recv())
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
# 截图保存
|
||||
ws.send(json.dumps({
|
||||
"id": 4,
|
||||
"method": "Page.captureScreenshot"
|
||||
}))
|
||||
msg = ws.recv()
|
||||
data = json.loads(msg)
|
||||
if "result" in data and "data" in data["result"]:
|
||||
with open("mcp_add_domain_default_checked.png", "wb") as f:
|
||||
f.write(base64.b64decode(data["result"]["data"]))
|
||||
print("Screenshot saved to mcp_add_domain_default_checked.png")
|
||||
|
||||
ws.close()
|
||||
BIN
login_page.png
Normal file
|
After Width: | Height: | Size: 449 KiB |
48
login_pwd2.py
Normal file
@ -0,0 +1,48 @@
|
||||
import json, base64, time
|
||||
import websocket, urllib.request
|
||||
|
||||
resp = urllib.request.urlopen("http://127.0.0.1:9222/json/list")
|
||||
pages = json.loads(resp.read().decode("utf-8"))
|
||||
page = next(p for p in pages if p.get("type") == "page" and "odacm.cn" in p.get("url"))
|
||||
ws_url = page["webSocketDebuggerUrl"]
|
||||
|
||||
ws = websocket.WebSocket()
|
||||
ws.connect(ws_url, timeout=5)
|
||||
|
||||
# 使用 12345678a 尝试登录
|
||||
login_script = """
|
||||
(() => {
|
||||
const inputs = Array.from(document.querySelectorAll('input'));
|
||||
const userInput = inputs.find(i => i.placeholder && i.placeholder.includes('用户名') || i.type === 'text');
|
||||
const pwdInput = inputs.find(i => i.type === 'password');
|
||||
const submitBtn = document.querySelector('button[type="submit"]') || Array.from(document.querySelectorAll('button')).find(b => b.innerText.includes('登录'));
|
||||
|
||||
if (userInput && pwdInput && submitBtn) {
|
||||
userInput.value = 'xuans';
|
||||
userInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
pwdInput.value = '12345678a';
|
||||
pwdInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
submitBtn.click();
|
||||
return 'Submitting login with 12345678a...';
|
||||
}
|
||||
return 'Login elements not found';
|
||||
})()
|
||||
"""
|
||||
|
||||
ws.send(json.dumps({
|
||||
"id": 1,
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {"expression": login_script, "returnByValue": True}
|
||||
}))
|
||||
print(ws.recv())
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
ws.send(json.dumps({
|
||||
"id": 2,
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {"expression": "document.body.innerText.slice(0, 150)", "returnByValue": True}
|
||||
}))
|
||||
print(ws.recv())
|
||||
|
||||
ws.close()
|
||||
BIN
mail_settings_ready.png
Normal file
|
After Width: | Height: | Size: 140 KiB |
BIN
mail_settings_tab.png
Normal file
|
After Width: | Height: | Size: 140 KiB |
BIN
mail_settings_view.png
Normal file
|
After Width: | Height: | Size: 128 KiB |
21
mark_done.py
Normal file
@ -0,0 +1,21 @@
|
||||
with open("doc/Scert整改任务执行清单.md", "r", encoding="utf-8") as f:
|
||||
text = f.read()
|
||||
|
||||
text = text.replace(
|
||||
"- [ ] 部署静态产物至第一台服务器(8.137.23.95)并在 Chrome MCP 验证实机新建域名弹窗默认勾选效果。\n- [ ] 提交并双重推送 Git 远端仓库。",
|
||||
"- [✓] 部署静态产物至第一台服务器(8.137.23.95)并在 Chrome MCP 验证实机新建域名弹窗默认勾选效果。\n- [✓] 提交并双重推送 Git 远端仓库。"
|
||||
)
|
||||
with open("doc/Scert整改任务执行清单.md", "w", encoding="utf-8") as f:
|
||||
f.write(text)
|
||||
|
||||
with open("doc/Scert整改合并优化清单.md", "r", encoding="utf-8") as f:
|
||||
text_m = f.read()
|
||||
|
||||
text_m = text_m.replace(
|
||||
"- [ ] 部署静态产物至第一台服务器(8.137.23.95)并在 Chrome MCP 验证实机新建域名弹窗默认勾选效果。\n- [ ] 提交并双重推送 Git 远端仓库。",
|
||||
"- [✓] 部署静态产物至第一台服务器(8.137.23.95)并在 Chrome MCP 验证实机新建域名弹窗默认勾选效果。\n- [✓] 提交并双重推送 Git 远端仓库。"
|
||||
)
|
||||
with open("doc/Scert整改合并优化清单.md", "w", encoding="utf-8") as f:
|
||||
f.write(text_m)
|
||||
|
||||
print("Marked status as completed in execution lists.")
|
||||
BIN
mcp_add_domain_default_checked.png
Normal file
|
After Width: | Height: | Size: 171 KiB |
BIN
mcp_add_domain_modal.png
Normal file
|
After Width: | Height: | Size: 171 KiB |
BIN
mcp_add_domain_verified.png
Normal file
|
After Width: | Height: | Size: 416 KiB |
BIN
mcp_domains_view.png
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
mcp_mail_tab.png
Normal file
|
After Width: | Height: | Size: 140 KiB |
BIN
mcp_mail_tab_enabled.png
Normal file
|
After Width: | Height: | Size: 149 KiB |
BIN
mcp_page_now.png
Normal file
|
After Width: | Height: | Size: 155 KiB |
BIN
mcp_projects_members.png
Normal file
|
After Width: | Height: | Size: 171 KiB |
BIN
mcp_servers_clicked.png
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
mcp_servers_view.png
Normal file
|
After Width: | Height: | Size: 155 KiB |
BIN
mcp_settings_view.png
Normal file
|
After Width: | Height: | Size: 140 KiB |
BIN
mcp_verified.png
Normal file
|
After Width: | Height: | Size: 140 KiB |
45
nav_to_settings.py
Normal file
@ -0,0 +1,45 @@
|
||||
import json, base64, time
|
||||
import websocket, urllib.request
|
||||
|
||||
resp = urllib.request.urlopen("http://127.0.0.1:9222/json/list")
|
||||
pages = json.loads(resp.read().decode("utf-8"))
|
||||
page = next(p for p in pages if p.get("type") == "page" and "odacm.cn" in p.get("url"))
|
||||
ws_url = page["webSocketDebuggerUrl"]
|
||||
|
||||
ws = websocket.WebSocket()
|
||||
ws.connect(ws_url, timeout=5)
|
||||
|
||||
# 点击“系统设置”
|
||||
script = """
|
||||
(() => {
|
||||
const items = Array.from(document.querySelectorAll('.dashboard-navigation-item'));
|
||||
const settingsItem = items.find(el => el.innerText.includes('系统设置'));
|
||||
if (settingsItem) {
|
||||
settingsItem.click();
|
||||
return 'Clicked 系统设置';
|
||||
}
|
||||
return 'Not found';
|
||||
})()
|
||||
"""
|
||||
ws.send(json.dumps({
|
||||
"id": 1,
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {"expression": script, "returnByValue": True}
|
||||
}))
|
||||
print(ws.recv())
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
# 截图保存
|
||||
ws.send(json.dumps({
|
||||
"id": 2,
|
||||
"method": "Page.captureScreenshot"
|
||||
}))
|
||||
msg = ws.recv()
|
||||
data = json.loads(msg)
|
||||
if "result" in data and "data" in data["result"]:
|
||||
with open("mcp_settings_view.png", "wb") as f:
|
||||
f.write(base64.b64decode(data["result"]["data"]))
|
||||
print("Screenshot saved to mcp_settings_view.png")
|
||||
|
||||
ws.close()
|
||||
BIN
scert_preview_1.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
scert_preview_active_dashboard.png
Normal file
|
After Width: | Height: | Size: 156 KiB |
BIN
scert_preview_current.png
Normal file
|
After Width: | Height: | Size: 416 KiB |
BIN
scert_preview_current_2.png
Normal file
|
After Width: | Height: | Size: 413 KiB |
BIN
scert_preview_dashboard_logged_in.png
Normal file
|
After Width: | Height: | Size: 412 KiB |
BIN
scert_preview_login.png
Normal file
|
After Width: | Height: | Size: 416 KiB |
BIN
scert_preview_odacm.png
Normal file
|
After Width: | Height: | Size: 416 KiB |
BIN
scert_real_current.png
Normal file
|
After Width: | Height: | Size: 156 KiB |
BIN
settings_view.png
Normal file
|
After Width: | Height: | Size: 128 KiB |
BIN
sidebar_cropped.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
sidebar_dashboard_view.png
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
sidebar_detail.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
25
snap_servers.py
Normal file
@ -0,0 +1,25 @@
|
||||
import json, base64, time
|
||||
import websocket, urllib.request
|
||||
|
||||
resp = urllib.request.urlopen("http://127.0.0.1:9222/json/list")
|
||||
pages = json.loads(resp.read().decode("utf-8"))
|
||||
page = next(p for p in pages if p.get("type") == "page" and "odacm.cn" in p.get("url"))
|
||||
ws_url = page["webSocketDebuggerUrl"]
|
||||
|
||||
ws = websocket.WebSocket()
|
||||
ws.connect(ws_url, timeout=5)
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
ws.send(json.dumps({
|
||||
"id": 1,
|
||||
"method": "Page.captureScreenshot"
|
||||
}))
|
||||
msg = ws.recv()
|
||||
data = json.loads(msg)
|
||||
if "result" in data and "data" in data["result"]:
|
||||
with open("mcp_servers_view.png", "wb") as f:
|
||||
f.write(base64.b64decode(data["result"]["data"]))
|
||||
print("Screenshot saved to mcp_servers_view.png")
|
||||
|
||||
ws.close()
|
||||
37
test_cdp.py
Normal file
@ -0,0 +1,37 @@
|
||||
import json, base64, time
|
||||
import websocket
|
||||
import urllib.request
|
||||
|
||||
resp = urllib.request.urlopen("http://127.0.0.1:9222/json/list")
|
||||
pages = json.loads(resp.read().decode("utf-8"))
|
||||
page = next(p for p in pages if p.get("type") == "page" and "odacm.cn" in p.get("url"))
|
||||
ws_url = page["webSocketDebuggerUrl"]
|
||||
print("Connecting to ws_url:", ws_url)
|
||||
|
||||
ws = websocket.WebSocket()
|
||||
ws.connect(ws_url, timeout=5)
|
||||
|
||||
ws.send(json.dumps({
|
||||
"id": 1,
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {"expression": "document.title + ' | ' + window.location.href"}
|
||||
}))
|
||||
|
||||
msg = ws.recv()
|
||||
print("Eval result:", msg)
|
||||
|
||||
ws.send(json.dumps({
|
||||
"id": 2,
|
||||
"method": "Page.captureScreenshot"
|
||||
}))
|
||||
|
||||
msg2 = ws.recv()
|
||||
data = json.loads(msg2)
|
||||
if "result" in data and "data" in data["result"]:
|
||||
with open("mcp_verified.png", "wb") as f:
|
||||
f.write(base64.b64decode(data["result"]["data"]))
|
||||
print("Screenshot captured successfully!")
|
||||
else:
|
||||
print("Response:", msg2[:200])
|
||||
|
||||
ws.close()
|
||||
59
test_default_server_mcp.py
Normal file
@ -0,0 +1,59 @@
|
||||
import json, base64, time
|
||||
import websocket, urllib.request
|
||||
|
||||
resp = urllib.request.urlopen("http://127.0.0.1:9222/json/list")
|
||||
pages = json.loads(resp.read().decode("utf-8"))
|
||||
page = next(p for p in pages if p.get("type") == "page" and "odacm.cn" in p.get("url"))
|
||||
ws_url = page["webSocketDebuggerUrl"]
|
||||
|
||||
ws = websocket.WebSocket()
|
||||
ws.connect(ws_url, timeout=5)
|
||||
|
||||
# 清除缓存并刷新页面
|
||||
ws.send(json.dumps({
|
||||
"id": 1,
|
||||
"method": "Network.clearBrowserCache"
|
||||
}))
|
||||
print(ws.recv())
|
||||
|
||||
ws.send(json.dumps({
|
||||
"id": 2,
|
||||
"method": "Page.reload",
|
||||
"params": {"ignoreCache": True}
|
||||
}))
|
||||
print(ws.recv())
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
# 点击“添加域名”
|
||||
script = """
|
||||
(() => {
|
||||
const btn = Array.from(document.querySelectorAll('button')).find(b => b.innerText.includes('添加域名'));
|
||||
if (btn) {
|
||||
btn.click();
|
||||
return 'Clicked 添加域名';
|
||||
}
|
||||
return 'Not found';
|
||||
})()
|
||||
"""
|
||||
ws.send(json.dumps({
|
||||
"id": 3,
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {"expression": script, "returnByValue": True}
|
||||
}))
|
||||
print(ws.recv())
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
ws.send(json.dumps({
|
||||
"id": 4,
|
||||
"method": "Page.captureScreenshot"
|
||||
}))
|
||||
msg = ws.recv()
|
||||
data = json.loads(msg)
|
||||
if "result" in data and "data" in data["result"]:
|
||||
with open("mcp_add_domain_default_checked.png", "wb") as f:
|
||||
f.write(base64.b64decode(data["result"]["data"]))
|
||||
print("Screenshot saved to mcp_add_domain_default_checked.png")
|
||||
|
||||
ws.close()
|
||||
36
test_mcp_conn.py
Normal file
@ -0,0 +1,36 @@
|
||||
import json, base64
|
||||
import websocket
|
||||
import urllib.request
|
||||
|
||||
resp = urllib.request.urlopen("http://127.0.0.1:9222/json/list")
|
||||
pages = json.loads(resp.read().decode("utf-8"))
|
||||
page = next(p for p in pages if p.get("type") == "page" and "odacm.cn" in p.get("url"))
|
||||
ws_url = page["webSocketDebuggerUrl"]
|
||||
print("Connecting to ws_url:", ws_url)
|
||||
|
||||
ws = websocket.create_connection(ws_url)
|
||||
|
||||
ws.send(json.dumps({
|
||||
"id": 1,
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {"expression": "document.title + ' | ' + window.location.href"}
|
||||
}))
|
||||
res = ws.recv()
|
||||
print("Page status:", res)
|
||||
|
||||
ws.send(json.dumps({
|
||||
"id": 2,
|
||||
"method": "Page.captureScreenshot"
|
||||
}))
|
||||
|
||||
while True:
|
||||
msg = ws.recv()
|
||||
data = json.loads(msg)
|
||||
if data.get("id") == 2:
|
||||
img_data = base64.b64decode(data["result"]["data"])
|
||||
with open("mcp_check_screenshot.png", "wb") as f:
|
||||
f.write(img_data)
|
||||
print("Screenshot saved to mcp_check_screenshot.png")
|
||||
break
|
||||
|
||||
ws.close()
|
||||
26
test_nav.py
Normal file
@ -0,0 +1,26 @@
|
||||
import json, websocket, urllib.request
|
||||
|
||||
resp = urllib.request.urlopen("http://127.0.0.1:9222/json/list")
|
||||
pages = json.loads(resp.read().decode("utf-8"))
|
||||
page = next(p for p in pages if p.get("type") == "page" and "odacm.cn" in p.get("url"))
|
||||
ws_url = page["webSocketDebuggerUrl"]
|
||||
|
||||
ws = websocket.WebSocket()
|
||||
ws.connect(ws_url, timeout=5)
|
||||
|
||||
# 点击侧边栏的“系统设置”或切换视图查看
|
||||
script = """
|
||||
(() => {
|
||||
const navItems = Array.from(document.querySelectorAll('.dashboard-navigation-item, .nav-item, button'));
|
||||
return navItems.map(item => item.innerText.trim()).filter(Boolean);
|
||||
})()
|
||||
"""
|
||||
ws.send(json.dumps({
|
||||
"id": 1,
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {"expression": script, "returnByValue": True}
|
||||
}))
|
||||
|
||||
res = ws.recv()
|
||||
print("Nav elements:", res)
|
||||
ws.close()
|
||||
65
test_open_add_domain.py
Normal file
@ -0,0 +1,65 @@
|
||||
import json, base64, time
|
||||
import websocket, urllib.request
|
||||
|
||||
resp = urllib.request.urlopen("http://127.0.0.1:9222/json/list")
|
||||
pages = json.loads(resp.read().decode("utf-8"))
|
||||
page = next(p for p in pages if p.get("type") == "page" and "odacm.cn" in p.get("url"))
|
||||
ws_url = page["webSocketDebuggerUrl"]
|
||||
|
||||
ws = websocket.WebSocket()
|
||||
ws.connect(ws_url, timeout=5)
|
||||
|
||||
# 点击侧边栏“域名配置”
|
||||
nav_script = """
|
||||
(() => {
|
||||
const items = Array.from(document.querySelectorAll('.navigation-item'));
|
||||
const target = items.find(b => b.innerText.includes('域名配置'));
|
||||
if (target) {
|
||||
target.click();
|
||||
return 'Navigated to 域名配置';
|
||||
}
|
||||
return '域名配置 not found';
|
||||
})()
|
||||
"""
|
||||
ws.send(json.dumps({
|
||||
"id": 1,
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {"expression": nav_script, "returnByValue": True}
|
||||
}))
|
||||
print(ws.recv())
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
# 点击“添加域名”
|
||||
add_script = """
|
||||
(() => {
|
||||
const btn = Array.from(document.querySelectorAll('button')).find(b => b.innerText.includes('添加域名'));
|
||||
if (btn) {
|
||||
btn.click();
|
||||
return 'Clicked 添加域名';
|
||||
}
|
||||
return '添加域名 not found';
|
||||
})()
|
||||
"""
|
||||
ws.send(json.dumps({
|
||||
"id": 2,
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {"expression": add_script, "returnByValue": True}
|
||||
}))
|
||||
print(ws.recv())
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
# 截图保存
|
||||
ws.send(json.dumps({
|
||||
"id": 3,
|
||||
"method": "Page.captureScreenshot"
|
||||
}))
|
||||
msg = ws.recv()
|
||||
data = json.loads(msg)
|
||||
if "result" in data and "data" in data["result"]:
|
||||
with open("mcp_add_domain_default_checked.png", "wb") as f:
|
||||
f.write(base64.b64decode(data["result"]["data"]))
|
||||
print("Screenshot saved to mcp_add_domain_default_checked.png")
|
||||
|
||||
ws.close()
|
||||
32
update_doc.py
Normal file
@ -0,0 +1,32 @@
|
||||
import sys
|
||||
|
||||
with open("doc/Scert整改任务执行清单.md", "r", encoding="utf-8") as f:
|
||||
text = f.read()
|
||||
|
||||
target = """## 2026年10月18日 侧边栏底部SSL证书管理字样清理与侧边栏左右5px自适应布局
|
||||
|
||||
- [x] 清理 DashboardNavigation.vue 底部“SSL 证书管理”角标及冗余 CSS
|
||||
- [x] 优化侧边栏及账号卡片等宽自适应布局,侧边栏以卡片宽度左右各留 5px 内边距(padding: 2px 5px)
|
||||
- [x] 统一第一行版本号格式为 Vx.x.x(如 V1.52.1)
|
||||
- [x] 运行前端单元测试 356 项全量验证通过
|
||||
- [x] 编译并成功部署前端产物至第一台服务器(8.137.23.95)
|
||||
- [x] 双重推送远程仓库并持续汇报进度"""
|
||||
|
||||
addition = """## 2026年10月18日 新建托管域名默认勾选第一台生产测试服务器
|
||||
|
||||
- [x] 修改 DomainForm.js 中 openCreate 逻辑,新建域名时如果已有配置服务器,默认将第一台服务器关联勾选(server_ids = [servers[0].id])。
|
||||
- [x] 同步更新 DomainForm.test.js 对应场景断言,确保单元测试与新建默认勾选逻辑严密契合。
|
||||
- [x] 前端自动化单元测试 356 项全量通过(356 pass)。
|
||||
- [x] 完成前端生产环境打包构建(vite build)生成新静态资源产物。
|
||||
- [ ] 部署静态产物至第一台服务器(8.137.23.95)并在 Chrome MCP 验证实机新建域名弹窗默认勾选效果。
|
||||
- [ ] 提交并双重推送 Git 远端仓库。
|
||||
|
||||
""" + target
|
||||
|
||||
if target in text:
|
||||
text = text.replace(target, addition, 1)
|
||||
with open("doc/Scert整改任务执行清单.md", "w", encoding="utf-8") as f:
|
||||
f.write(text)
|
||||
print("Updated Scert整改任务执行清单.md")
|
||||
else:
|
||||
print("Target not found in Scert整改任务执行清单.md")
|
||||
25
update_doc_merge.py
Normal file
@ -0,0 +1,25 @@
|
||||
import sys
|
||||
|
||||
with open("doc/Scert整改合并优化清单.md", "r", encoding="utf-8") as f:
|
||||
text = f.read()
|
||||
|
||||
target = """## 2026年10月17日 邮件模板美化与Linux生产主服务热部署"""
|
||||
|
||||
addition = """## 2026年10月18日 新建托管域名默认勾选第一台生产测试服务器
|
||||
|
||||
- [x] 修改 DomainForm.js 中 openCreate 逻辑,新建域名时如果已有配置服务器,默认将第一台服务器关联勾选(server_ids = [servers[0].id])。
|
||||
- [x] 同步更新 DomainForm.test.js 对应场景断言,确保单元测试与新建默认勾选逻辑严密契合。
|
||||
- [x] 前端自动化单元测试 356 项全量通过(356 pass)。
|
||||
- [x] 完成前端生产环境打包构建(vite build)生成新静态资源产物。
|
||||
- [ ] 部署静态产物至第一台服务器(8.137.23.95)并在 Chrome MCP 验证实机新建域名弹窗默认勾选效果。
|
||||
- [ ] 提交并双重推送 Git 远端仓库。
|
||||
|
||||
""" + target
|
||||
|
||||
if target in text:
|
||||
text = text.replace(target, addition, 1)
|
||||
with open("doc/Scert整改合并优化清单.md", "w", encoding="utf-8") as f:
|
||||
f.write(text)
|
||||
print("Updated Scert整改合并优化清单.md")
|
||||
else:
|
||||
print("Target not found in Scert整改合并优化清单.md")
|
||||
29
update_domain_form.py
Normal file
@ -0,0 +1,29 @@
|
||||
import sys
|
||||
|
||||
with open("frontend/src/views/Dashboard/DomainForm.js", "r", encoding="utf-8") as f:
|
||||
text = f.read()
|
||||
|
||||
old_open_create = """ function openCreate() {
|
||||
if (saving.value) return;
|
||||
version += 1;
|
||||
editingId.value = "";
|
||||
targetID = "";
|
||||
form.value = emptyForm();"""
|
||||
|
||||
new_open_create = """ function openCreate() {
|
||||
if (saving.value) return;
|
||||
version += 1;
|
||||
editingId.value = "";
|
||||
targetID = "";
|
||||
form.value = emptyForm();
|
||||
if (servers.value && servers.value.length > 0) {
|
||||
form.value.server_ids = [servers.value[0].id];
|
||||
}"""
|
||||
|
||||
if old_open_create in text:
|
||||
text = text.replace(old_open_create, new_open_create, 1)
|
||||
with open("frontend/src/views/Dashboard/DomainForm.js", "w", encoding="utf-8") as f:
|
||||
f.write(text)
|
||||
print("DomainForm.js updated successfully!")
|
||||
else:
|
||||
print("old_open_create not matched!")
|
||||
65
verify_domain_default.py
Normal file
@ -0,0 +1,65 @@
|
||||
import json, base64, time
|
||||
import websocket, urllib.request
|
||||
|
||||
resp = urllib.request.urlopen("http://127.0.0.1:9222/json/list")
|
||||
pages = json.loads(resp.read().decode("utf-8"))
|
||||
page = next(p for p in pages if p.get("type") == "page" and "odacm.cn" in p.get("url"))
|
||||
ws_url = page["webSocketDebuggerUrl"]
|
||||
|
||||
ws = websocket.WebSocket()
|
||||
ws.connect(ws_url, timeout=5)
|
||||
|
||||
# 点击侧边栏“域名配置”
|
||||
script_click_tab = """
|
||||
(() => {
|
||||
const buttons = Array.from(document.querySelectorAll('.navigation-item'));
|
||||
const target = buttons.find(b => b.innerText.includes('域名配置'));
|
||||
if (target) {
|
||||
target.click();
|
||||
return 'Clicked 域名配置';
|
||||
}
|
||||
return 'Not found';
|
||||
})()
|
||||
"""
|
||||
ws.send(json.dumps({
|
||||
"id": 1,
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {"expression": script_click_tab, "returnByValue": True}
|
||||
}))
|
||||
print(ws.recv())
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
# 点击“添加域名”
|
||||
script_click_add = """
|
||||
(() => {
|
||||
const btn = Array.from(document.querySelectorAll('button')).find(b => b.innerText.includes('添加域名'));
|
||||
if (btn) {
|
||||
btn.click();
|
||||
return 'Clicked 添加域名';
|
||||
}
|
||||
return 'Not found';
|
||||
})()
|
||||
"""
|
||||
ws.send(json.dumps({
|
||||
"id": 2,
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {"expression": script_click_add, "returnByValue": True}
|
||||
}))
|
||||
print(ws.recv())
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
# 截图保存
|
||||
ws.send(json.dumps({
|
||||
"id": 3,
|
||||
"method": "Page.captureScreenshot"
|
||||
}))
|
||||
msg = ws.recv()
|
||||
data = json.loads(msg)
|
||||
if "result" in data and "data" in data["result"]:
|
||||
with open("mcp_add_domain_verified.png", "wb") as f:
|
||||
f.write(base64.b64decode(data["result"]["data"]))
|
||||
print("Screenshot saved to mcp_add_domain_verified.png")
|
||||
|
||||
ws.close()
|
||||
322
验证码.html
Normal file
@ -0,0 +1,322 @@
|
||||
1.[{{site_name}}] 邮箱验证码
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
body { margin: 0; padding: 24px; background: #f4f4f5; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: #18181b; }
|
||||
.container { max-width: 640px; margin: 0 auto; background: #ffffff; border-radius: 12px; overflow: hidden; box-shadow: 0 8px 30px rgba(15, 23, 42, 0.10); }
|
||||
.header { background: #4f46e5; color: #ffffff; padding: 28px 32px; }
|
||||
.header h1 { margin: 0; font-size: 24px; line-height: 1.25; }
|
||||
.content { padding: 32px; font-size: 15px; line-height: 1.7; }
|
||||
.button { display: inline-block; margin-top: 12px; padding: 11px 18px; border-radius: 8px; background: #4f46e5; color: #ffffff; text-decoration: none; font-weight: 600; }
|
||||
.muted { color: #71717a; font-size: 13px; }
|
||||
.footer { padding: 18px 32px; background: #fafafa; color: #a1a1aa; font-size: 12px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header"><h1>邮箱验证码</h1></div>
|
||||
<div class="content">
|
||||
<p>{{recipient_name}},您好:</p>
|
||||
<p>您的验证码是:</p>
|
||||
<p style="font-size: 32px; font-weight: 700; letter-spacing: 8px; text-align: center;">{{verification_code}}</p>
|
||||
<p>验证码将在 <strong>{{expires_in_minutes}}</strong> 分钟后失效。</p>
|
||||
<p>如果不是您本人操作,请忽略此邮件。</p></div>
|
||||
<div class="footer">This email was sent by {{site_name}}. Please do not reply directly.</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
2.[{{site_name}}] 密码重置请求
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
body { margin: 0; padding: 24px; background: #f4f4f5; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: #18181b; }
|
||||
.container { max-width: 640px; margin: 0 auto; background: #ffffff; border-radius: 12px; overflow: hidden; box-shadow: 0 8px 30px rgba(15, 23, 42, 0.10); }
|
||||
.header { background: #7c3aed; color: #ffffff; padding: 28px 32px; }
|
||||
.header h1 { margin: 0; font-size: 24px; line-height: 1.25; }
|
||||
.content { padding: 32px; font-size: 15px; line-height: 1.7; }
|
||||
.button { display: inline-block; margin-top: 12px; padding: 11px 18px; border-radius: 8px; background: #7c3aed; color: #ffffff; text-decoration: none; font-weight: 600; }
|
||||
.muted { color: #71717a; font-size: 13px; }
|
||||
.footer { padding: 18px 32px; background: #fafafa; color: #a1a1aa; font-size: 12px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header"><h1>密码重置</h1></div>
|
||||
<div class="content">
|
||||
<p>{{recipient_name}},您好:</p>
|
||||
<p>我们收到了您的密码重置请求,请点击下方按钮设置新密码。</p>
|
||||
<p><a class="button" href="{{reset_url}}">重置密码</a></p>
|
||||
<p>此链接将在 <strong>{{expires_in_minutes}}</strong> 分钟后失效。</p>
|
||||
<p class="muted">如果按钮无法点击,请复制以下链接到浏览器中打开:<br>{{reset_url}}</p>
|
||||
<p>如果不是您本人操作,请忽略此邮件。</p></div>
|
||||
<div class="footer">This email was sent by {{site_name}}. Please do not reply directly.</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
3.[{{site_name}}] 通知邮箱验证码
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
body { margin: 0; padding: 24px; background: #f4f4f5; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: #18181b; }
|
||||
.container { max-width: 640px; margin: 0 auto; background: #ffffff; border-radius: 12px; overflow: hidden; box-shadow: 0 8px 30px rgba(15, 23, 42, 0.10); }
|
||||
.header { background: #0ea5e9; color: #ffffff; padding: 28px 32px; }
|
||||
.header h1 { margin: 0; font-size: 24px; line-height: 1.25; }
|
||||
.content { padding: 32px; font-size: 15px; line-height: 1.7; }
|
||||
.button { display: inline-block; margin-top: 12px; padding: 11px 18px; border-radius: 8px; background: #0ea5e9; color: #ffffff; text-decoration: none; font-weight: 600; }
|
||||
.muted { color: #71717a; font-size: 13px; }
|
||||
.footer { padding: 18px 32px; background: #fafafa; color: #a1a1aa; font-size: 12px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header"><h1>通知邮箱验证</h1></div>
|
||||
<div class="content">
|
||||
<p>{{recipient_name}},您好:</p>
|
||||
<p>您正在添加额外的通知邮箱,请输入以下验证码完成验证。</p>
|
||||
<p style="font-size: 32px; font-weight: 700; letter-spacing: 8px; text-align: center;">{{verification_code}}</p>
|
||||
<p>验证码将在 <strong>{{expires_in_minutes}}</strong> 分钟后失效。</p>
|
||||
<p>如果不是您本人操作,请忽略此邮件。</p></div>
|
||||
<div class="footer">This email was sent by {{site_name}}. Please do not reply directly.</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
4.[{{site_name}}] 账户已被禁用
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
body { margin: 0; padding: 24px; background: #f4f4f5; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: #18181b; }
|
||||
.container { max-width: 640px; margin: 0 auto; background: #ffffff; border-radius: 12px; overflow: hidden; box-shadow: 0 8px 30px rgba(15, 23, 42, 0.10); }
|
||||
.header { background: #b91c1c; color: #ffffff; padding: 28px 32px; }
|
||||
.header h1 { margin: 0; font-size: 24px; line-height: 1.25; }
|
||||
.content { padding: 32px; font-size: 15px; line-height: 1.7; }
|
||||
.button { display: inline-block; margin-top: 12px; padding: 11px 18px; border-radius: 8px; background: #b91c1c; color: #ffffff; text-decoration: none; font-weight: 600; }
|
||||
.muted { color: #71717a; font-size: 13px; }
|
||||
.footer { padding: 18px 32px; background: #fafafa; color: #a1a1aa; font-size: 12px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header"><h1>账户已被禁用</h1></div>
|
||||
<div class="content">
|
||||
<p>{{recipient_name}},您好:</p>
|
||||
<p>您的账户在统计周期内多次触发平台内容审核/风控规则,系统已自动禁用该账户。</p>
|
||||
<table style="width:100%;border-collapse:collapse;">
|
||||
<tr><td>禁用时间</td><td>{{triggered_at}}</td></tr>
|
||||
<tr><td>所属分组</td><td>{{group_name}}</td></tr>
|
||||
<tr><td>命中类别 / 分数</td><td>{{moderation_category}} / {{moderation_score}}</td></tr>
|
||||
<tr><td>累计触发次数</td><td>{{violation_count}} / {{ban_threshold}}</td></tr>
|
||||
</table>
|
||||
<p>如需申诉或恢复账号,请联系平台管理员处理。</p></div>
|
||||
<div class="footer">This email was sent by {{site_name}}. Please do not reply directly.</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
5.[运维告警][{{severity}}] {{rule_name}}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
body { margin: 0; padding: 24px; background: #f4f4f5; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: #18181b; }
|
||||
.container { max-width: 640px; margin: 0 auto; background: #ffffff; border-radius: 12px; overflow: hidden; box-shadow: 0 8px 30px rgba(15, 23, 42, 0.10); }
|
||||
.header { background: #ea580c; color: #ffffff; padding: 28px 32px; }
|
||||
.header h1 { margin: 0; font-size: 24px; line-height: 1.25; }
|
||||
.content { padding: 32px; font-size: 15px; line-height: 1.7; }
|
||||
.button { display: inline-block; margin-top: 12px; padding: 11px 18px; border-radius: 8px; background: #ea580c; color: #ffffff; text-decoration: none; font-weight: 600; }
|
||||
.muted { color: #71717a; font-size: 13px; }
|
||||
.footer { padding: 18px 32px; background: #fafafa; color: #a1a1aa; font-size: 12px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header"><h1>运维告警</h1></div>
|
||||
<div class="content">
|
||||
<p><strong>规则</strong>:{{rule_name}}</p>
|
||||
<p><strong>严重级别</strong>:{{severity}}</p>
|
||||
<p><strong>状态</strong>:{{alert_status}}</p>
|
||||
<p><strong>指标</strong>:{{metric_type}} {{operator}} {{metric_value}}(阈值 {{threshold_value}})</p>
|
||||
<p><strong>触发时间</strong>:{{triggered_at}}</p>
|
||||
<p><strong>说明</strong>:{{alert_description}}</p></div>
|
||||
<div class="footer">This email was sent by {{site_name}}. Please do not reply directly.</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
6.[{{site_name}}] 账户风控提醒
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
body { margin: 0; padding: 24px; background: #f4f4f5; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: #18181b; }
|
||||
.container { max-width: 640px; margin: 0 auto; background: #ffffff; border-radius: 12px; overflow: hidden; box-shadow: 0 8px 30px rgba(15, 23, 42, 0.10); }
|
||||
.header { background: #ef4444; color: #ffffff; padding: 28px 32px; }
|
||||
.header h1 { margin: 0; font-size: 24px; line-height: 1.25; }
|
||||
.content { padding: 32px; font-size: 15px; line-height: 1.7; }
|
||||
.button { display: inline-block; margin-top: 12px; padding: 11px 18px; border-radius: 8px; background: #ef4444; color: #ffffff; text-decoration: none; font-weight: 600; }
|
||||
.muted { color: #71717a; font-size: 13px; }
|
||||
.footer { padding: 18px 32px; background: #fafafa; color: #a1a1aa; font-size: 12px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header"><h1>账户风控提醒</h1></div>
|
||||
<div class="content">
|
||||
<p>{{recipient_name}},您好:</p>
|
||||
<p>您的 API 请求触发了平台内容审核/风控策略。</p>
|
||||
<table style="width:100%;border-collapse:collapse;">
|
||||
<tr><td>触发时间</td><td>{{triggered_at}}</td></tr>
|
||||
<tr><td>所属分组</td><td>{{group_name}}</td></tr>
|
||||
<tr><td>命中类别 / 分数</td><td>{{moderation_category}} / {{moderation_score}}</td></tr>
|
||||
<tr><td>累计触发次数</td><td>{{violation_count}} / {{ban_threshold}}</td></tr>
|
||||
</table>
|
||||
<p>请检查请求内容,避免后续服务受到影响。</p></div>
|
||||
<div class="footer">This email was sent by {{site_name}}. Please do not reply directly.</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
7.[运维告警][{{severity}}] {{rule_name}}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
body { margin: 0; padding: 24px; background: #f4f4f5; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: #18181b; }
|
||||
.container { max-width: 640px; margin: 0 auto; background: #ffffff; border-radius: 12px; overflow: hidden; box-shadow: 0 8px 30px rgba(15, 23, 42, 0.10); }
|
||||
.header { background: #ea580c; color: #ffffff; padding: 28px 32px; }
|
||||
.header h1 { margin: 0; font-size: 24px; line-height: 1.25; }
|
||||
.content { padding: 32px; font-size: 15px; line-height: 1.7; }
|
||||
.button { display: inline-block; margin-top: 12px; padding: 11px 18px; border-radius: 8px; background: #ea580c; color: #ffffff; text-decoration: none; font-weight: 600; }
|
||||
.muted { color: #71717a; font-size: 13px; }
|
||||
.footer { padding: 18px 32px; background: #fafafa; color: #a1a1aa; font-size: 12px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header"><h1>运维告警</h1></div>
|
||||
<div class="content">
|
||||
<p><strong>规则</strong>:{{rule_name}}</p>
|
||||
<p><strong>严重级别</strong>:{{severity}}</p>
|
||||
<p><strong>状态</strong>:{{alert_status}}</p>
|
||||
<p><strong>指标</strong>:{{metric_type}} {{operator}} {{metric_value}}(阈值 {{threshold_value}})</p>
|
||||
<p><strong>触发时间</strong>:{{triggered_at}}</p>
|
||||
<p><strong>说明</strong>:{{alert_description}}</p></div>
|
||||
<div class="footer">This email was sent by {{site_name}}. Please do not reply directly.</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
8.[运维报表] {{report_name}}
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
body { margin: 0; padding: 24px 12px; background: #f4f6f8; color: #1f2937; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; }
|
||||
.container { width: 100%; max-width: 680px; margin: 0 auto; background: #ffffff; border: 1px solid #dfe7ea; border-radius: 8px; overflow: hidden; }
|
||||
.header { padding: 28px 32px 24px; background: #0f766e; color: #ffffff; }
|
||||
.eyebrow { margin: 0 0 8px; color: #ccfbf1; font-size: 12px; font-weight: 700; letter-spacing: 0; text-transform: uppercase; }
|
||||
h1 { margin: 0; font-size: 26px; line-height: 1.3; }
|
||||
.header p { margin: 8px 0 0; color: #e6fffb; font-size: 14px; }
|
||||
.content { padding: 28px 32px 32px; }
|
||||
.meta { width: 100%; margin: 0 0 20px; border-collapse: collapse; background: #f8fafc; border: 1px solid #e2e8f0; }
|
||||
.meta td { padding: 10px 12px; border-bottom: 1px solid #e2e8f0; font-size: 13px; vertical-align: top; }
|
||||
.meta tr:last-child td { border-bottom: 0; }
|
||||
.meta-label { width: 112px; color: #64748b; font-weight: 600; }
|
||||
.section-title { margin: 28px 0 12px; color: #0f172a; font-size: 16px; line-height: 1.4; }
|
||||
.metric-grid { width: 100%; border-collapse: separate; border-spacing: 8px; margin: -8px; }
|
||||
.metric-cell { width: 50%; padding: 14px 16px; border: 1px solid #e2e8f0; background: #ffffff; vertical-align: top; }
|
||||
.metric-label { display: block; color: #64748b; font-size: 12px; line-height: 1.4; }
|
||||
.metric-value { display: block; margin-top: 6px; color: #0f172a; font-size: 20px; font-weight: 700; line-height: 1.2; }
|
||||
.metric-value.good { color: #15803d; }
|
||||
.metric-value.alert { color: #b91c1c; }
|
||||
.detail { width: 100%; border-collapse: collapse; }
|
||||
.detail td { padding: 10px 12px; border-bottom: 1px solid #e2e8f0; font-size: 13px; }
|
||||
.detail td:first-child { width: 56%; color: #475569; }
|
||||
.detail td:last-child { color: #0f172a; font-weight: 600; text-align: right; }
|
||||
.report-detail { margin-top: 28px; }
|
||||
.report-detail:empty { display: none; }
|
||||
.footer { padding: 18px 32px; background: #f8fafc; border-top: 1px solid #e2e8f0; color: #64748b; font-size: 12px; line-height: 1.6; }
|
||||
@media only screen and (max-width: 620px) {
|
||||
body { padding: 0; }
|
||||
.container { border: 0; border-radius: 0; }
|
||||
.header, .content, .footer { padding-left: 20px; padding-right: 20px; }
|
||||
.metric-grid, .metric-grid tbody, .metric-grid tr, .metric-cell { display: block; width: 100% !important; box-sizing: border-box; }
|
||||
.metric-cell { margin: 8px 0; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<p class="eyebrow">运维报表</p>
|
||||
<h1>{{report_name}}</h1>
|
||||
<p>{{site_name}} 的运行概览</p>
|
||||
</div>
|
||||
<div class="content">
|
||||
<table class="meta" role="presentation">
|
||||
<tr><td class="meta-label">报表</td><td>{{report_name}}</td></tr>
|
||||
<tr><td class="meta-label">类型</td><td>{{report_type}}</td></tr>
|
||||
<tr><td class="meta-label">统计周期</td><td>{{report_start_time}} 至 {{report_end_time}} (UTC)</td></tr>
|
||||
</table>
|
||||
|
||||
<div style="display: {{report_summary_display}};">
|
||||
<h2 class="section-title">请求概览</h2>
|
||||
<table class="metric-grid" role="presentation"><tr>
|
||||
<td class="metric-cell"><span class="metric-label">总请求数</span><span class="metric-value">{{report_total_requests}}</span></td>
|
||||
<td class="metric-cell"><span class="metric-label">成功请求</span><span class="metric-value good">{{report_success_count}}</span></td>
|
||||
</tr><tr>
|
||||
<td class="metric-cell"><span class="metric-label">SLA 错误</span><span class="metric-value alert">{{report_sla_error_count}}</span></td>
|
||||
<td class="metric-cell"><span class="metric-label">业务限流</span><span class="metric-value">{{report_business_limited_count}}</span></td>
|
||||
</tr></table>
|
||||
|
||||
<h2 class="section-title">可靠性</h2>
|
||||
<table class="detail" role="presentation">
|
||||
<tr><td>SLA</td><td>{{report_sla}}</td></tr>
|
||||
<tr><td>错误率</td><td>{{report_error_rate}}</td></tr>
|
||||
<tr><td>上游错误率(不含 429 / 529)</td><td>{{report_upstream_error_rate}}</td></tr>
|
||||
<tr><td>上游错误(不含 429 / 529)</td><td>{{report_upstream_error_count_excl_429_529}}</td></tr>
|
||||
<tr><td>上游 429 / 529</td><td>{{report_upstream_429_count}} / {{report_upstream_529_count}}</td></tr>
|
||||
</table>
|
||||
|
||||
<h2 class="section-title">延迟表现</h2>
|
||||
<table class="detail" role="presentation">
|
||||
<tr><td>请求延迟 p50 / p99</td><td>{{report_latency_p50}} / {{report_latency_p99}}</td></tr>
|
||||
<tr><td>首 Token 时间 p50 / p99</td><td>{{report_ttft_p50}} / {{report_ttft_p99}}</td></tr>
|
||||
</table>
|
||||
|
||||
<h2 class="section-title">吞吐量</h2>
|
||||
<table class="detail" role="presentation">
|
||||
<tr><td>Token 消耗</td><td>{{report_tokens}}</td></tr>
|
||||
<tr><td>QPS(当前 / 峰值 / 平均)</td><td>{{report_qps_current}} / {{report_qps_peak}} / {{report_qps_avg}}</td></tr>
|
||||
<tr><td>TPS(当前 / 峰值 / 平均)</td><td>{{report_tps_current}} / {{report_tps_peak}} / {{report_tps_avg}}</td></tr>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
<div class="report-detail" style="display: {{report_detail_display}};">{{report_html}}</div>
|
||||
</div>
|
||||
<div class="footer">此邮件由 {{site_name}} 自动发送,请勿直接回复。</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||