chore: 工作区检查点提交

This commit is contained in:
xuans 2026-10-18 10:42:10 +08:00
parent 43e14ce775
commit a024e9b79d
67 changed files with 1508 additions and 0 deletions

41
apply_default_server.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
dashboard_after_login.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 447 KiB

BIN
dashboard_equal_columns.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 KiB

BIN
deploy/scert_linux Normal file

Binary file not shown.

33
deploy_frontend.py Normal file
View 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
View File

43
enable_smtp.py Normal file
View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 KiB

48
login_pwd2.py Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

BIN
mail_settings_tab.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

BIN
mail_settings_view.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

21
mark_done.py Normal file
View 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.")

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

BIN
mcp_add_domain_modal.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

BIN
mcp_add_domain_verified.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 416 KiB

BIN
mcp_domains_view.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

BIN
mcp_mail_tab.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

BIN
mcp_mail_tab_enabled.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

BIN
mcp_page_now.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

BIN
mcp_projects_members.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

BIN
mcp_servers_clicked.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

BIN
mcp_servers_view.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

BIN
mcp_settings_view.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

BIN
mcp_verified.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

45
nav_to_settings.py Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

BIN
scert_preview_current.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 416 KiB

BIN
scert_preview_current_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 413 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 412 KiB

BIN
scert_preview_login.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 416 KiB

BIN
scert_preview_odacm.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 416 KiB

BIN
scert_real_current.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

BIN
settings_view.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

BIN
sidebar_cropped.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

BIN
sidebar_dashboard_view.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

BIN
sidebar_detail.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

25
snap_servers.py Normal file
View 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
View 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()

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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>