fix(security): retire default-credential surface (yanxue-20260914-cma-defaultcred-code-fix)

- Login.vue: no prefilled default creds; no anonymous entity enumeration on mount

- deploy.sh: readiness polling (0.5s/30s cap) replaces fixed sleep 3; login smoke reads same-source 600 secret file, missing file = explicit abort

- 19 verify/check/gen_report scripts: shared _admin_creds() loader (CMA_ADMIN_ENV -> ops .secrets -> profile copy); no hardcoded creds

- tests: single TEST_USER_PASSWORD fixture constant; docs: pointer to 600 secret file; removed 4 legacy .bak + pycache residue
This commit is contained in:
Hermes CI Fix
2026-09-14 17:39:53 +08:00
parent 8adc783501
commit 40d522cd34
39 changed files with 602 additions and 71 deletions
@@ -26,6 +26,32 @@ import urllib.request
import pymysql
# 2026-09-14 凭据面收口(cma-defaultcred-code-fix):原快照里的硬编码默认账号/口令改为同源加载,
# 快照的期/阈值断言一字未动(历史原文仍在 git 历史里)。
# ---- 管理员凭据同源加载(2026-09-14 cma-defaultcred-code-fix)----
# 生产凭据只存 600 密钥文件;禁止在脚本/文档/日志里硬编码口令。
# 顺序:$CMA_ADMIN_ENV → wecom-ops 主副本(ops 轮换落点,权威) → 本 profile 副本。
def _admin_creds():
"""返回 (user, password, 来源文件);未找到 → 显式报错退出(不静默退回默认口令)"""
for p in (os.environ.get("CMA_ADMIN_ENV"),
"/root/.hermes/profiles/wecom-ops/.secrets/cma-admin.env",
"/root/.hermes/profiles/wecom-fullstack/.secrets/cma-admin.env"):
if p and os.path.isfile(p):
kv = {}
for line in open(p, encoding="utf-8"):
line = line.strip()
if "=" in line and not line.startswith("#"):
k, v = line.split("=", 1)
kv[k.strip()] = v.strip().strip('"').strip("'")
if kv.get("CMA_ADMIN_USER") and kv.get("CMA_ADMIN_PASS"):
return kv["CMA_ADMIN_USER"], kv["CMA_ADMIN_PASS"], p
raise SystemExit("[错误] 未找到 CMA 管理员凭据:请设 $CMA_ADMIN_ENV,或准备 600 权限密钥文件 "
".secrets/cma-admin.env(键名 CMA_ADMIN_USER / CMA_ADMIN_PASS)")
ADMIN_USER, ADMIN_PASS, CRED_SRC = _admin_creds()
print(f"[creds] 来源={CRED_SRC} 账号={ADMIN_USER}(口令不回显)")
if os.environ.get("FORCE_SNAPSHOT") != "1":
print("⛔ 这是 20260908 的历史验收快照,不是活检查;复跑结果不代表当前回归。")
print(" 现行活检查:python3 backend/scripts/verify_bep_threshold_fix.py")
@@ -120,7 +146,7 @@ check("576 GAP red pending", alerts.get(576) == ("red", "pending"), str(alerts.g
check("577 COVER_CASH yellow pending", alerts.get(577) == ("yellow", "pending"), str(alerts.get(577)))
print("== 5. bep-card 端点 ==")
login = http_json("/api/cma/auth/login", "POST", {"username": "admin", "password": "admin123", "entity_id": 1})
login = http_json("/api/cma/auth/login", "POST", {"username": ADMIN_USER, "password": ADMIN_PASS, "entity_id": 1})
card = http_json("/api/cma/dashboard/bep-card", token=login["token"])
check("available", card.get("available") is True)
check("book red", card["book"]["cover_level"] == "red", str(card["book"]))
@@ -18,6 +18,30 @@ import sys
import urllib.request
import urllib.error
# ---- 管理员凭据同源加载(2026-09-14 cma-defaultcred-code-fix)----
# 生产凭据只存 600 密钥文件;禁止在脚本/文档/日志里硬编码口令。
# 顺序:$CMA_ADMIN_ENV → wecom-ops 主副本(ops 轮换落点,权威) → 本 profile 副本。
def _admin_creds():
"""返回 (user, password, 来源文件);未找到 → 显式报错退出(不静默退回默认口令)"""
for p in (os.environ.get("CMA_ADMIN_ENV"),
"/root/.hermes/profiles/wecom-ops/.secrets/cma-admin.env",
"/root/.hermes/profiles/wecom-fullstack/.secrets/cma-admin.env"):
if p and os.path.isfile(p):
kv = {}
for line in open(p, encoding="utf-8"):
line = line.strip()
if "=" in line and not line.startswith("#"):
k, v = line.split("=", 1)
kv[k.strip()] = v.strip().strip('"').strip("'")
if kv.get("CMA_ADMIN_USER") and kv.get("CMA_ADMIN_PASS"):
return kv["CMA_ADMIN_USER"], kv["CMA_ADMIN_PASS"], p
raise SystemExit("[错误] 未找到 CMA 管理员凭据:请设 $CMA_ADMIN_ENV,或准备 600 权限密钥文件 "
".secrets/cma-admin.env(键名 CMA_ADMIN_USER / CMA_ADMIN_PASS)")
ADMIN_USER, ADMIN_PASS, CRED_SRC = _admin_creds()
print(f"[creds] 来源={CRED_SRC} 账号={ADMIN_USER}(口令不回显)")
BASE = "http://127.0.0.1:8010"
MARK = "【系统验证临时数据-验证后删除】"
KPI_CODE = "E2E_WT_TIER"
@@ -75,7 +99,7 @@ def main():
# ── 1. 登录(人token, entity=1) ──
st, r = http("POST", "/api/cma/auth/login",
{"username": "admin", "password": "admin123", "entity_id": 1})
{"username": ADMIN_USER, "password": ADMIN_PASS, "entity_id": 1})
check("登录 admin entity=1", st == 200, f"http={st}")
token = r.get("token") if st == 200 else ""
auth = {"Authorization": f"Bearer {token}"}
@@ -11,6 +11,31 @@ import time
import urllib.request
import websocket
import os # noqa: E402 (凭据加载器需要)
# ---- 管理员凭据同源加载(2026-09-14 cma-defaultcred-code-fix)----
# 生产凭据只存 600 密钥文件;禁止在脚本/文档/日志里硬编码口令。
# 顺序:$CMA_ADMIN_ENV → wecom-ops 主副本(ops 轮换落点,权威) → 本 profile 副本。
def _admin_creds():
"""返回 (user, password, 来源文件);未找到 → 显式报错退出(不静默退回默认口令)"""
for p in (os.environ.get("CMA_ADMIN_ENV"),
"/root/.hermes/profiles/wecom-ops/.secrets/cma-admin.env",
"/root/.hermes/profiles/wecom-fullstack/.secrets/cma-admin.env"):
if p and os.path.isfile(p):
kv = {}
for line in open(p, encoding="utf-8"):
line = line.strip()
if "=" in line and not line.startswith("#"):
k, v = line.split("=", 1)
kv[k.strip()] = v.strip().strip('"').strip("'")
if kv.get("CMA_ADMIN_USER") and kv.get("CMA_ADMIN_PASS"):
return kv["CMA_ADMIN_USER"], kv["CMA_ADMIN_PASS"], p
raise SystemExit("[错误] 未找到 CMA 管理员凭据:请设 $CMA_ADMIN_ENV,或准备 600 权限密钥文件 "
".secrets/cma-admin.env(键名 CMA_ADMIN_USER / CMA_ADMIN_PASS)")
ADMIN_USER, ADMIN_PASS, CRED_SRC = _admin_creds()
print(f"[creds] 来源={CRED_SRC} 账号={ADMIN_USER}(口令不回显)")
CDP_HTTP = "http://127.0.0.1:9224"
BASE = "https://cma.sxbh.ltd/api/cma"
@@ -78,7 +103,7 @@ def login_via_fetch(c, entity_id):
(async () => {{
const r = await fetch('{BASE}/auth/login', {{
method: 'POST', headers: {{'Content-Type': 'application/json'}},
body: JSON.stringify({{username:'admin', password:'admin123', entity_id:{entity_id}}})
body: JSON.stringify({{username:'{ADMIN_USER}', password:'{ADMIN_PASS}', entity_id:{entity_id}}})
}});
const j = await r.json();
if (!r.ok) return 'LOGIN_FAIL:' + JSON.stringify(j);
@@ -3,6 +3,31 @@
"""2026-09-07 预算版本管理UI修复 - 生产API验证"""
import json, urllib.request, ssl, sys
import os # noqa: E402 (凭据加载器需要)
# ---- 管理员凭据同源加载(2026-09-14 cma-defaultcred-code-fix)----
# 生产凭据只存 600 密钥文件;禁止在脚本/文档/日志里硬编码口令。
# 顺序:$CMA_ADMIN_ENV → wecom-ops 主副本(ops 轮换落点,权威) → 本 profile 副本。
def _admin_creds():
"""返回 (user, password, 来源文件);未找到 → 显式报错退出(不静默退回默认口令)"""
for p in (os.environ.get("CMA_ADMIN_ENV"),
"/root/.hermes/profiles/wecom-ops/.secrets/cma-admin.env",
"/root/.hermes/profiles/wecom-fullstack/.secrets/cma-admin.env"):
if p and os.path.isfile(p):
kv = {}
for line in open(p, encoding="utf-8"):
line = line.strip()
if "=" in line and not line.startswith("#"):
k, v = line.split("=", 1)
kv[k.strip()] = v.strip().strip('"').strip("'")
if kv.get("CMA_ADMIN_USER") and kv.get("CMA_ADMIN_PASS"):
return kv["CMA_ADMIN_USER"], kv["CMA_ADMIN_PASS"], p
raise SystemExit("[错误] 未找到 CMA 管理员凭据:请设 $CMA_ADMIN_ENV,或准备 600 权限密钥文件 "
".secrets/cma-admin.env(键名 CMA_ADMIN_USER / CMA_ADMIN_PASS)")
ADMIN_USER, ADMIN_PASS, CRED_SRC = _admin_creds()
print(f"[creds] 来源={CRED_SRC} 账号={ADMIN_USER}(口令不回显)")
BASE = "https://cma.sxbh.ltd/api/cma"
def http(url, data=None, headers=None, method=None):
@@ -14,7 +39,7 @@ def http(url, data=None, headers=None, method=None):
return json.loads(r.read().decode())
def login(entity_id):
body = json.dumps({"username": "admin", "password": "admin123", "entity_id": entity_id}).encode()
body = json.dumps({"username": ADMIN_USER, "password": ADMIN_PASS, "entity_id": entity_id}).encode()
r = http(BASE + "/auth/login", data=body, headers={"Content-Type": "application/json"}, method="POST")
return r["token"]
+26 -1
View File
@@ -18,6 +18,31 @@ import sys
import urllib.error
import urllib.request
import os # noqa: E402 (凭据加载器需要)
# ---- 管理员凭据同源加载(2026-09-14 cma-defaultcred-code-fix)----
# 生产凭据只存 600 密钥文件;禁止在脚本/文档/日志里硬编码口令。
# 顺序:$CMA_ADMIN_ENV → wecom-ops 主副本(ops 轮换落点,权威) → 本 profile 副本。
def _admin_creds():
"""返回 (user, password, 来源文件);未找到 → 显式报错退出(不静默退回默认口令)"""
for p in (os.environ.get("CMA_ADMIN_ENV"),
"/root/.hermes/profiles/wecom-ops/.secrets/cma-admin.env",
"/root/.hermes/profiles/wecom-fullstack/.secrets/cma-admin.env"):
if p and os.path.isfile(p):
kv = {}
for line in open(p, encoding="utf-8"):
line = line.strip()
if "=" in line and not line.startswith("#"):
k, v = line.split("=", 1)
kv[k.strip()] = v.strip().strip('"').strip("'")
if kv.get("CMA_ADMIN_USER") and kv.get("CMA_ADMIN_PASS"):
return kv["CMA_ADMIN_USER"], kv["CMA_ADMIN_PASS"], p
raise SystemExit("[错误] 未找到 CMA 管理员凭据:请设 $CMA_ADMIN_ENV,或准备 600 权限密钥文件 "
".secrets/cma-admin.env(键名 CMA_ADMIN_USER / CMA_ADMIN_PASS)")
ADMIN_USER, ADMIN_PASS, CRED_SRC = _admin_creds()
print(f"[creds] 来源={CRED_SRC} 账号={ADMIN_USER}(口令不回显)")
BASE = "http://127.0.0.1:8010"
TMP_MARK = "【系统验证临时数据-验证后删除】"
@@ -52,7 +77,7 @@ def main():
# 1. 登录博海账套 (admin, entity_id=2)
st, r = http("POST", "/api/cma/auth/login",
{"username": "admin", "password": "admin123", "entity_id": 2})
{"username": ADMIN_USER, "password": ADMIN_PASS, "entity_id": 2})
check("登录博海(entity_id=2)", st == 200 and "token" in (r or {}),
f"st={st} keys={sorted((r or {}).keys())}")
token = (r or {}).get("token", "")
@@ -23,6 +23,30 @@ import urllib.request
import urllib.error
from datetime import datetime
# ---- 管理员凭据同源加载(2026-09-14 cma-defaultcred-code-fix)----
# 生产凭据只存 600 密钥文件;禁止在脚本/文档/日志里硬编码口令。
# 顺序:$CMA_ADMIN_ENV → wecom-ops 主副本(ops 轮换落点,权威) → 本 profile 副本。
def _admin_creds():
"""返回 (user, password, 来源文件);未找到 → 显式报错退出(不静默退回默认口令)"""
for p in (os.environ.get("CMA_ADMIN_ENV"),
"/root/.hermes/profiles/wecom-ops/.secrets/cma-admin.env",
"/root/.hermes/profiles/wecom-fullstack/.secrets/cma-admin.env"):
if p and os.path.isfile(p):
kv = {}
for line in open(p, encoding="utf-8"):
line = line.strip()
if "=" in line and not line.startswith("#"):
k, v = line.split("=", 1)
kv[k.strip()] = v.strip().strip('"').strip("'")
if kv.get("CMA_ADMIN_USER") and kv.get("CMA_ADMIN_PASS"):
return kv["CMA_ADMIN_USER"], kv["CMA_ADMIN_PASS"], p
raise SystemExit("[错误] 未找到 CMA 管理员凭据:请设 $CMA_ADMIN_ENV,或准备 600 权限密钥文件 "
".secrets/cma-admin.env(键名 CMA_ADMIN_USER / CMA_ADMIN_PASS)")
ADMIN_USER, ADMIN_PASS, CRED_SRC = _admin_creds()
print(f"[creds] 来源={CRED_SRC} 账号={ADMIN_USER}(口令不回显)")
BASE = "http://127.0.0.1:8010"
MARK = "【系统验证临时数据-验证后删除】"
PREFIX = "E2E-RD-"
@@ -63,7 +87,7 @@ def main():
# ── 0. 登录(admin entity=1) ──
st, r = http("POST", "/api/cma/auth/login",
{"username": "admin", "password": "admin123", "entity_id": 1})
{"username": ADMIN_USER, "password": ADMIN_PASS, "entity_id": 1})
check("登录 admin entity=1", st == 200, f"http={st}")
token = r.get("token") if st == 200 else ""
auth = {"Authorization": f"Bearer {token}"}
@@ -15,8 +15,33 @@ import sys
import urllib.request
import urllib.error
import os # noqa: E402 (凭据加载器需要)
# ---- 管理员凭据同源加载(2026-09-14 cma-defaultcred-code-fix)----
# 生产凭据只存 600 密钥文件;禁止在脚本/文档/日志里硬编码口令。
# 顺序:$CMA_ADMIN_ENV → wecom-ops 主副本(ops 轮换落点,权威) → 本 profile 副本。
def _admin_creds():
"""返回 (user, password, 来源文件);未找到 → 显式报错退出(不静默退回默认口令)"""
for p in (os.environ.get("CMA_ADMIN_ENV"),
"/root/.hermes/profiles/wecom-ops/.secrets/cma-admin.env",
"/root/.hermes/profiles/wecom-fullstack/.secrets/cma-admin.env"):
if p and os.path.isfile(p):
kv = {}
for line in open(p, encoding="utf-8"):
line = line.strip()
if "=" in line and not line.startswith("#"):
k, v = line.split("=", 1)
kv[k.strip()] = v.strip().strip('"').strip("'")
if kv.get("CMA_ADMIN_USER") and kv.get("CMA_ADMIN_PASS"):
return kv["CMA_ADMIN_USER"], kv["CMA_ADMIN_PASS"], p
raise SystemExit("[错误] 未找到 CMA 管理员凭据:请设 $CMA_ADMIN_ENV,或准备 600 权限密钥文件 "
".secrets/cma-admin.env(键名 CMA_ADMIN_USER / CMA_ADMIN_PASS)")
ADMIN_USER, ADMIN_PASS, CRED_SRC = _admin_creds()
print(f"[creds] 来源={CRED_SRC} 账号={ADMIN_USER}(口令不回显)")
BASE = "http://127.0.0.1:8010"
ADMIN = {"username": "admin", "password": "admin123"}
ADMIN = {"username": ADMIN_USER, "password": ADMIN_PASS}
MARK = "【系统验证临时数据-验证后删除】"
TEMP_TAG = "e2e-verify-temp"
RULE_TYPE = "customer_concentration"
+25 -1
View File
@@ -7,6 +7,30 @@ import pymysql
# 2026-09-12 库访问治理(ops):凭据从 backend/.env 读取,无明文兜底
import os
# ---- 管理员凭据同源加载(2026-09-14 cma-defaultcred-code-fix)----
# 生产凭据只存 600 密钥文件;禁止在脚本/文档/日志里硬编码口令。
# 顺序:$CMA_ADMIN_ENV → wecom-ops 主副本(ops 轮换落点,权威) → 本 profile 副本。
def _admin_creds():
"""返回 (user, password, 来源文件);未找到 → 显式报错退出(不静默退回默认口令)"""
for p in (os.environ.get("CMA_ADMIN_ENV"),
"/root/.hermes/profiles/wecom-ops/.secrets/cma-admin.env",
"/root/.hermes/profiles/wecom-fullstack/.secrets/cma-admin.env"):
if p and os.path.isfile(p):
kv = {}
for line in open(p, encoding="utf-8"):
line = line.strip()
if "=" in line and not line.startswith("#"):
k, v = line.split("=", 1)
kv[k.strip()] = v.strip().strip('"').strip("'")
if kv.get("CMA_ADMIN_USER") and kv.get("CMA_ADMIN_PASS"):
return kv["CMA_ADMIN_USER"], kv["CMA_ADMIN_PASS"], p
raise SystemExit("[错误] 未找到 CMA 管理员凭据:请设 $CMA_ADMIN_ENV,或准备 600 权限密钥文件 "
".secrets/cma-admin.env(键名 CMA_ADMIN_USER / CMA_ADMIN_PASS)")
ADMIN_USER, ADMIN_PASS, CRED_SRC = _admin_creds()
print(f"[creds] 来源={CRED_SRC} 账号={ADMIN_USER}(口令不回显)")
try:
from dotenv import load_dotenv as _ld_gov
_ld_gov('/root/cma-management/backend/.env')
@@ -38,7 +62,7 @@ def get(path, token):
def main():
# 1. 登录 entity1
r = post("/api/cma/auth/login", {"username": "admin", "password": "admin123", "entity_id": 1})
r = post("/api/cma/auth/login", {"username": ADMIN_USER, "password": ADMIN_PASS, "entity_id": 1})
token = r.get("access_token") or r.get("token")
assert token, f"登录失败: {r}"
print("1. 登录 entity1 OK")
@@ -3,6 +3,31 @@ import json
import sys
import urllib.request
import os # noqa: E402 (凭据加载器需要)
# ---- 管理员凭据同源加载(2026-09-14 cma-defaultcred-code-fix)----
# 生产凭据只存 600 密钥文件;禁止在脚本/文档/日志里硬编码口令。
# 顺序:$CMA_ADMIN_ENV → wecom-ops 主副本(ops 轮换落点,权威) → 本 profile 副本。
def _admin_creds():
"""返回 (user, password, 来源文件);未找到 → 显式报错退出(不静默退回默认口令)"""
for p in (os.environ.get("CMA_ADMIN_ENV"),
"/root/.hermes/profiles/wecom-ops/.secrets/cma-admin.env",
"/root/.hermes/profiles/wecom-fullstack/.secrets/cma-admin.env"):
if p and os.path.isfile(p):
kv = {}
for line in open(p, encoding="utf-8"):
line = line.strip()
if "=" in line and not line.startswith("#"):
k, v = line.split("=", 1)
kv[k.strip()] = v.strip().strip('"').strip("'")
if kv.get("CMA_ADMIN_USER") and kv.get("CMA_ADMIN_PASS"):
return kv["CMA_ADMIN_USER"], kv["CMA_ADMIN_PASS"], p
raise SystemExit("[错误] 未找到 CMA 管理员凭据:请设 $CMA_ADMIN_ENV,或准备 600 权限密钥文件 "
".secrets/cma-admin.env(键名 CMA_ADMIN_USER / CMA_ADMIN_PASS)")
ADMIN_USER, ADMIN_PASS, CRED_SRC = _admin_creds()
print(f"[creds] 来源={CRED_SRC} 账号={ADMIN_USER}(口令不回显)")
BASE = "http://127.0.0.1:8010"
@@ -28,7 +53,7 @@ def post(path, body, token=None, method="POST"):
def main():
# 1. 登录(账套模式 entity_id=1)
status, login = post("/api/cma/auth/login", {"username": "admin", "password": "admin123", "entity_id": 1})
status, login = post("/api/cma/auth/login", {"username": ADMIN_USER, "password": ADMIN_PASS, "entity_id": 1})
token = login.get("token") or login.get("access_token")
if status != 200 or not token:
print("FAIL login:", status, login)
+25 -1
View File
@@ -8,6 +8,30 @@ import os
import json
import urllib.request
# ---- 管理员凭据同源加载(2026-09-14 cma-defaultcred-code-fix)----
# 生产凭据只存 600 密钥文件;禁止在脚本/文档/日志里硬编码口令。
# 顺序:$CMA_ADMIN_ENV → wecom-ops 主副本(ops 轮换落点,权威) → 本 profile 副本。
def _admin_creds():
"""返回 (user, password, 来源文件);未找到 → 显式报错退出(不静默退回默认口令)"""
for p in (os.environ.get("CMA_ADMIN_ENV"),
"/root/.hermes/profiles/wecom-ops/.secrets/cma-admin.env",
"/root/.hermes/profiles/wecom-fullstack/.secrets/cma-admin.env"):
if p and os.path.isfile(p):
kv = {}
for line in open(p, encoding="utf-8"):
line = line.strip()
if "=" in line and not line.startswith("#"):
k, v = line.split("=", 1)
kv[k.strip()] = v.strip().strip('"').strip("'")
if kv.get("CMA_ADMIN_USER") and kv.get("CMA_ADMIN_PASS"):
return kv["CMA_ADMIN_USER"], kv["CMA_ADMIN_PASS"], p
raise SystemExit("[错误] 未找到 CMA 管理员凭据:请设 $CMA_ADMIN_ENV,或准备 600 权限密钥文件 "
".secrets/cma-admin.env(键名 CMA_ADMIN_USER / CMA_ADMIN_PASS)")
ADMIN_USER, ADMIN_PASS, CRED_SRC = _admin_creds()
print(f"[creds] 来源={CRED_SRC} 账号={ADMIN_USER}(口令不回显)")
BASE = os.getenv("CMA_BASE", "http://127.0.0.1:8010")
@@ -24,7 +48,7 @@ def post(path, body, token=None):
def main():
# 登录(账套模式必须 entity_id)
login = post("/api/cma/auth/login", {"username": "admin", "password": "admin123", "entity_id": 1})
login = post("/api/cma/auth/login", {"username": ADMIN_USER, "password": ADMIN_PASS, "entity_id": 1})
token = login.get("token") or login.get("access_token")
if not token:
print("❌ 登录失败:", login)
@@ -15,8 +15,33 @@ import sys
import urllib.request
import urllib.error
import os # noqa: E402 (凭据加载器需要)
# ---- 管理员凭据同源加载(2026-09-14 cma-defaultcred-code-fix)----
# 生产凭据只存 600 密钥文件;禁止在脚本/文档/日志里硬编码口令。
# 顺序:$CMA_ADMIN_ENV → wecom-ops 主副本(ops 轮换落点,权威) → 本 profile 副本。
def _admin_creds():
"""返回 (user, password, 来源文件);未找到 → 显式报错退出(不静默退回默认口令)"""
for p in (os.environ.get("CMA_ADMIN_ENV"),
"/root/.hermes/profiles/wecom-ops/.secrets/cma-admin.env",
"/root/.hermes/profiles/wecom-fullstack/.secrets/cma-admin.env"):
if p and os.path.isfile(p):
kv = {}
for line in open(p, encoding="utf-8"):
line = line.strip()
if "=" in line and not line.startswith("#"):
k, v = line.split("=", 1)
kv[k.strip()] = v.strip().strip('"').strip("'")
if kv.get("CMA_ADMIN_USER") and kv.get("CMA_ADMIN_PASS"):
return kv["CMA_ADMIN_USER"], kv["CMA_ADMIN_PASS"], p
raise SystemExit("[错误] 未找到 CMA 管理员凭据:请设 $CMA_ADMIN_ENV,或准备 600 权限密钥文件 "
".secrets/cma-admin.env(键名 CMA_ADMIN_USER / CMA_ADMIN_PASS)")
ADMIN_USER, ADMIN_PASS, CRED_SRC = _admin_creds()
print(f"[creds] 来源={CRED_SRC} 账号={ADMIN_USER}(口令不回显)")
BASE = "http://127.0.0.1:8010"
ADMIN = {"username": "admin", "password": "admin123"}
ADMIN = {"username": ADMIN_USER, "password": ADMIN_PASS}
MARK = "【系统验证临时数据-验证后删除】"
# 规则目标:酣客 → 应收账款周转天数(F_AR_DAYS, id=6,有 2026-09 预测日志)
@@ -5,6 +5,31 @@
"""
import sys
import os # noqa: E402 (凭据加载器需要)
# ---- 管理员凭据同源加载(2026-09-14 cma-defaultcred-code-fix)----
# 生产凭据只存 600 密钥文件;禁止在脚本/文档/日志里硬编码口令。
# 顺序:$CMA_ADMIN_ENV → wecom-ops 主副本(ops 轮换落点,权威) → 本 profile 副本。
def _admin_creds():
"""返回 (user, password, 来源文件);未找到 → 显式报错退出(不静默退回默认口令)"""
for p in (os.environ.get("CMA_ADMIN_ENV"),
"/root/.hermes/profiles/wecom-ops/.secrets/cma-admin.env",
"/root/.hermes/profiles/wecom-fullstack/.secrets/cma-admin.env"):
if p and os.path.isfile(p):
kv = {}
for line in open(p, encoding="utf-8"):
line = line.strip()
if "=" in line and not line.startswith("#"):
k, v = line.split("=", 1)
kv[k.strip()] = v.strip().strip('"').strip("'")
if kv.get("CMA_ADMIN_USER") and kv.get("CMA_ADMIN_PASS"):
return kv["CMA_ADMIN_USER"], kv["CMA_ADMIN_PASS"], p
raise SystemExit("[错误] 未找到 CMA 管理员凭据:请设 $CMA_ADMIN_ENV,或准备 600 权限密钥文件 "
".secrets/cma-admin.env(键名 CMA_ADMIN_USER / CMA_ADMIN_PASS)")
ADMIN_USER, ADMIN_PASS, CRED_SRC = _admin_creds()
print(f"[creds] 来源={CRED_SRC} 账号={ADMIN_USER}(口令不回显)")
env = {}
with open('/root/cma-management/backend/.env') as f:
for line in f:
@@ -85,7 +110,7 @@ def api(method, path, token=None, body=None):
return e.code, e.read().decode()
def login(eid):
st, body = api("POST", "/api/cma/auth/login", body={"username": "admin", "password": "admin123", "entity_id": eid})
st, body = api("POST", "/api/cma/auth/login", body={"username": ADMIN_USER, "password": ADMIN_PASS, "entity_id": eid})
if st != 200:
print(f" ⚠️ 登录 entity{eid} 失败 {st}: {body[:200]}")
return None
+26 -1
View File
@@ -10,6 +10,31 @@ import sys
import time
import urllib.request
import os # noqa: E402 (凭据加载器需要)
# ---- 管理员凭据同源加载(2026-09-14 cma-defaultcred-code-fix)----
# 生产凭据只存 600 密钥文件;禁止在脚本/文档/日志里硬编码口令。
# 顺序:$CMA_ADMIN_ENV → wecom-ops 主副本(ops 轮换落点,权威) → 本 profile 副本。
def _admin_creds():
"""返回 (user, password, 来源文件);未找到 → 显式报错退出(不静默退回默认口令)"""
for p in (os.environ.get("CMA_ADMIN_ENV"),
"/root/.hermes/profiles/wecom-ops/.secrets/cma-admin.env",
"/root/.hermes/profiles/wecom-fullstack/.secrets/cma-admin.env"):
if p and os.path.isfile(p):
kv = {}
for line in open(p, encoding="utf-8"):
line = line.strip()
if "=" in line and not line.startswith("#"):
k, v = line.split("=", 1)
kv[k.strip()] = v.strip().strip('"').strip("'")
if kv.get("CMA_ADMIN_USER") and kv.get("CMA_ADMIN_PASS"):
return kv["CMA_ADMIN_USER"], kv["CMA_ADMIN_PASS"], p
raise SystemExit("[错误] 未找到 CMA 管理员凭据:请设 $CMA_ADMIN_ENV,或准备 600 权限密钥文件 "
".secrets/cma-admin.env(键名 CMA_ADMIN_USER / CMA_ADMIN_PASS)")
ADMIN_USER, ADMIN_PASS, CRED_SRC = _admin_creds()
print(f"[creds] 来源={CRED_SRC} 账号={ADMIN_USER}(口令不回显)")
BASE = "http://127.0.0.1:8010/api/cma"
MARK = "【系统验证临时数据-验证后删除】"
PASSED = []
@@ -36,7 +61,7 @@ def api(method, path, token=None, body=None):
return e.code, {"detail": raw[:300]}
def login(entity_id):
st, d = api("POST", "/auth/login", body={"username": "admin", "password": "admin123", "entity_id": entity_id})
st, d = api("POST", "/auth/login", body={"username": ADMIN_USER, "password": ADMIN_PASS, "entity_id": entity_id})
assert st == 200, f"login entity{entity_id} fail: {d}"
return d["token"]
@@ -16,10 +16,35 @@ import time
import urllib.request
import websocket
import os # noqa: E402 (凭据加载器需要)
# ---- 管理员凭据同源加载(2026-09-14 cma-defaultcred-code-fix)----
# 生产凭据只存 600 密钥文件;禁止在脚本/文档/日志里硬编码口令。
# 顺序:$CMA_ADMIN_ENV → wecom-ops 主副本(ops 轮换落点,权威) → 本 profile 副本。
def _admin_creds():
"""返回 (user, password, 来源文件);未找到 → 显式报错退出(不静默退回默认口令)"""
for p in (os.environ.get("CMA_ADMIN_ENV"),
"/root/.hermes/profiles/wecom-ops/.secrets/cma-admin.env",
"/root/.hermes/profiles/wecom-fullstack/.secrets/cma-admin.env"):
if p and os.path.isfile(p):
kv = {}
for line in open(p, encoding="utf-8"):
line = line.strip()
if "=" in line and not line.startswith("#"):
k, v = line.split("=", 1)
kv[k.strip()] = v.strip().strip('"').strip("'")
if kv.get("CMA_ADMIN_USER") and kv.get("CMA_ADMIN_PASS"):
return kv["CMA_ADMIN_USER"], kv["CMA_ADMIN_PASS"], p
raise SystemExit("[错误] 未找到 CMA 管理员凭据:请设 $CMA_ADMIN_ENV,或准备 600 权限密钥文件 "
".secrets/cma-admin.env(键名 CMA_ADMIN_USER / CMA_ADMIN_PASS)")
ADMIN_USER, ADMIN_PASS, CRED_SRC = _admin_creds()
print(f"[creds] 来源={CRED_SRC} 账号={ADMIN_USER}(口令不回显)")
CDP_HTTP = "http://127.0.0.1:9224"
FIXED_URL = "http://127.0.0.1:5199"
MULTI_URL = "http://127.0.0.1:5200"
USER, PWD = "admin", "admin123"
USER, PWD = ADMIN_USER, ADMIN_PASS
FAILS = []
PASSES = []
@@ -12,6 +12,31 @@ import time
import urllib.request
import websocket
import os # noqa: E402 (凭据加载器需要)
# ---- 管理员凭据同源加载(2026-09-14 cma-defaultcred-code-fix)----
# 生产凭据只存 600 密钥文件;禁止在脚本/文档/日志里硬编码口令。
# 顺序:$CMA_ADMIN_ENV → wecom-ops 主副本(ops 轮换落点,权威) → 本 profile 副本。
def _admin_creds():
"""返回 (user, password, 来源文件);未找到 → 显式报错退出(不静默退回默认口令)"""
for p in (os.environ.get("CMA_ADMIN_ENV"),
"/root/.hermes/profiles/wecom-ops/.secrets/cma-admin.env",
"/root/.hermes/profiles/wecom-fullstack/.secrets/cma-admin.env"):
if p and os.path.isfile(p):
kv = {}
for line in open(p, encoding="utf-8"):
line = line.strip()
if "=" in line and not line.startswith("#"):
k, v = line.split("=", 1)
kv[k.strip()] = v.strip().strip('"').strip("'")
if kv.get("CMA_ADMIN_USER") and kv.get("CMA_ADMIN_PASS"):
return kv["CMA_ADMIN_USER"], kv["CMA_ADMIN_PASS"], p
raise SystemExit("[错误] 未找到 CMA 管理员凭据:请设 $CMA_ADMIN_ENV,或准备 600 权限密钥文件 "
".secrets/cma-admin.env(键名 CMA_ADMIN_USER / CMA_ADMIN_PASS)")
ADMIN_USER, ADMIN_PASS, CRED_SRC = _admin_creds()
print(f"[creds] 来源={CRED_SRC} 账号={ADMIN_USER}(口令不回显)")
CDP_HTTP = "http://127.0.0.1:9225"
TITLE_NEEDLE = "E2E-验证 建议中心UI实证"
@@ -80,7 +105,7 @@ def main():
(async () => {
const d = await (await fetch('/api/cma/auth/login', {method:'POST',
headers:{'Content-Type':'application/json'},
body: JSON.stringify({username:'admin', password:'admin123', entity_id:1})
body: JSON.stringify({username:'__CMA_ADMIN_USER__', password:'__CMA_ADMIN_PASS__', entity_id:1})
})).json();
if (!d.token) return 'LOGIN_FAIL: ' + JSON.stringify(d).slice(0,200);
localStorage.setItem('cma_token', d.token);
@@ -90,7 +115,7 @@ def main():
location.href = '/ai-suggestions';
return 'LOGIN_OK entity=' + d.entity_id;
})()
"""
""".replace("__CMA_ADMIN_USER__", ADMIN_USER).replace("__CMA_ADMIN_PASS__", ADMIN_PASS)
print("LOGIN:", c.eval(js_login))
time.sleep(6)
print("URL:", c.eval("location.href"))
+6 -2
View File
@@ -134,11 +134,15 @@ def client(db) -> Generator[TestClient, None, None]:
# ── 测试数据工厂 ──
# 测试夹具口令(仅内存 SQLite 测试库使用;不是生产默认口令——生产凭据只存 600 密钥文件
# .secrets/cma-admin.env)。可用 $CMA_TEST_USER_PASSWORD 覆盖;用例一律从这里导入,禁止写字面量。
TEST_USER_PASSWORD = os.environ.get("CMA_TEST_USER_PASSWORD", "test-fixture-pass")
def create_test_user(db: Session, **kwargs) -> User:
"""创建测试用户"""
defaults = {
"username": "testadmin",
"password_hash": hashlib.sha256("admin123".encode()).hexdigest(),
"password_hash": hashlib.sha256(TEST_USER_PASSWORD.encode()).hexdigest(),
"name": "测试管理员",
"role": "ceo",
}
@@ -150,7 +154,7 @@ def create_test_user(db: Session, **kwargs) -> User:
return user
def get_token_for_user(client: TestClient, username: str = "testadmin", password: str = "admin123") -> str:
def get_token_for_user(client: TestClient, username: str = "testadmin", password: str = TEST_USER_PASSWORD) -> str:
"""获取测试用户的token(账套模式:需entity_id)"""
resp = client.post("/api/cma/auth/login", json={
"username": username,
@@ -17,7 +17,7 @@ from fastapi.testclient import TestClient
from sqlalchemy.orm import Session
from app.models import Entity, UserEntity
from tests.conftest import create_test_user, auth_header
from tests.conftest import create_test_user, auth_header, TEST_USER_PASSWORD
BOT_KEY = {"X-BOT-KEY": "cma-bot-finance-2026"}
@@ -93,7 +93,7 @@ def _ensure_entity(db: Session, eid: int) -> Entity:
def _login(client: TestClient, entity_id: int = 1) -> str:
resp = client.post("/api/cma/auth/login", json={
"username": "testadmin", "password": "admin123", "entity_id": entity_id,
"username": "testadmin", "password": TEST_USER_PASSWORD, "entity_id": entity_id,
})
assert resp.status_code == 200, f"登录 entity{entity_id} 失败: {resp.text[:300]}"
return resp.json().get("token")
+4 -4
View File
@@ -4,7 +4,7 @@
import pytest
from fastapi.testclient import TestClient
from sqlalchemy.orm import Session
from tests.conftest import create_test_user, get_token_for_user, auth_header
from tests.conftest import create_test_user, get_token_for_user, auth_header, TEST_USER_PASSWORD
class TestAuth:
@@ -16,7 +16,7 @@ class TestAuth:
resp = client.post("/api/cma/auth/login", json={
"username": "testadmin",
"password": "admin123",
"password": TEST_USER_PASSWORD,
"entity_id": 1,
})
@@ -41,7 +41,7 @@ class TestAuth:
"""用户不存在"""
resp = client.post("/api/cma/auth/login", json={
"username": "nobody",
"password": "admin123",
"password": TEST_USER_PASSWORD,
})
assert resp.status_code == 401
@@ -89,7 +89,7 @@ class TestAuth:
resp = client.post("/api/cma/auth/register", json={
"username": "testadmin",
"password": "admin123",
"password": TEST_USER_PASSWORD,
"name": "重复用户",
})
+4 -4
View File
@@ -5,7 +5,7 @@ import pytest
from fastapi.testclient import TestClient
from sqlalchemy.orm import Session
from datetime import datetime
from tests.conftest import create_test_user, get_token_for_user, auth_header, create_test_kpi
from tests.conftest import create_test_user, get_token_for_user, auth_header, create_test_kpi, TEST_USER_PASSWORD
from app.models import KPIAlert, KPIValue
@@ -74,7 +74,7 @@ class TestDashboard:
def test_my_kpis(self, client: TestClient, db: Session):
user = create_test_user(db, username="biz_user", name="业务经理", role="business")
token = get_token_for_user(client, username="biz_user", password="admin123")
token = get_token_for_user(client, username="biz_user", password=TEST_USER_PASSWORD)
kpi = create_test_kpi(db, responsible_user="biz_user")
kpi2 = create_test_kpi(db, kpi_code="F_OTHER", kpi_name="无关KPI", responsible_user="其他人")
resp = client.get("/api/cma/dashboard/my-kpis", headers=auth_header(token))
@@ -82,7 +82,7 @@ class TestDashboard:
def test_my_kpis_ceo_sees_all(self, client: TestClient, db: Session):
user = create_test_user(db, username="ceo_user", name="CEO", role="ceo")
token = get_token_for_user(client, username="ceo_user", password="admin123")
token = get_token_for_user(client, username="ceo_user", password=TEST_USER_PASSWORD)
kpi = create_test_kpi(db, kpi_code="F_KPI_A", responsible_user="张三")
resp = client.get("/api/cma/dashboard/my-kpis", headers=auth_header(token))
assert resp.status_code == 200
@@ -103,7 +103,7 @@ class TestDashboard:
def test_my_dashboard(self, client: TestClient, db: Session):
user = create_test_user(db, username="ceo_user", name="CEO", role="ceo")
token = get_token_for_user(client, username="ceo_user", password="admin123")
token = get_token_for_user(client, username="ceo_user", password=TEST_USER_PASSWORD)
kpi = create_test_kpi(db)
resp = client.get("/api/cma/dashboard/my-dashboard", headers=auth_header(token))
assert resp.status_code == 200
@@ -13,7 +13,7 @@ from fastapi.testclient import TestClient
from sqlalchemy.orm import Session
from app.models import KPIDefinition, KPIValue, DataSourceConfig, Entity
from tests.conftest import create_test_user, get_token_for_user, auth_header
from tests.conftest import create_test_user, get_token_for_user, auth_header, TEST_USER_PASSWORD
DATA = "/api/cma/data"
@@ -65,7 +65,7 @@ class TestExcelImportEntityIsolation:
k1 = self._mk_kpi(db, "F_REVENUE", 1)
_ensure_entity(db, 2)
create_test_user(db)
token2 = _login(client, "testadmin", "admin123", entity_id=2)
token2 = _login(client, "testadmin", TEST_USER_PASSWORD, entity_id=2)
before = db.query(KPIValue).filter(KPIValue.kpi_id == k1.id).count()
df = pd.DataFrame({
@@ -91,7 +91,7 @@ class TestExcelImportEntityIsolation:
_ensure_entity(db, 2)
k1 = self._mk_kpi(db, "F_REVENUE", 1)
create_test_user(db)
token2 = _login(client, "testadmin", "admin123", entity_id=2)
token2 = _login(client, "testadmin", TEST_USER_PASSWORD, entity_id=2)
df = pd.DataFrame({
"kpi_code": ["F_REVENUE"],
@@ -156,7 +156,7 @@ class TestSourceCRUDEntityIsolation:
_ensure_entity(db, 2)
create_test_user(db)
token1 = get_token_for_user(client)
token2 = _login(client, "testadmin", "admin123", entity_id=2)
token2 = _login(client, "testadmin", TEST_USER_PASSWORD, entity_id=2)
r1 = client.post(f"{DATA}/sources", headers=auth_header(token1),
json={"name": "entity1数据源", "source_type": "manual"})
@@ -178,7 +178,7 @@ class TestSourceCRUDEntityIsolation:
_ensure_entity(db, 2)
create_test_user(db)
token1 = get_token_for_user(client)
token2 = _login(client, "testadmin", "admin123", entity_id=2)
token2 = _login(client, "testadmin", TEST_USER_PASSWORD, entity_id=2)
r1 = client.post(f"{DATA}/sources", headers=auth_header(token1),
json={"name": "entity1的源", "source_type": "manual"})
+2 -2
View File
@@ -13,7 +13,7 @@ from sqlalchemy.orm import Session
from app.models import Entity, KPIValue, User, UserEntity
from app.utils import kpi_forecast_engine as engine
from tests.conftest import create_test_kpi, create_test_user, get_token_for_user, auth_header
from tests.conftest import create_test_kpi, create_test_user, get_token_for_user, auth_header, TEST_USER_PASSWORD
BASE = "/api/cma/predict"
@@ -34,7 +34,7 @@ def _token_for_entity(client: TestClient, db: Session, eid: int) -> str:
db.add(UserEntity(user_id=user.id, entity_id=eid))
db.commit()
resp = client.post("/api/cma/auth/login", json={
"username": "testadmin", "password": "admin123", "entity_id": eid})
"username": "testadmin", "password": TEST_USER_PASSWORD, "entity_id": eid})
assert resp.status_code == 200, f"登录 entity{eid} 失败: {resp.text[:200]}"
return resp.json()["token"]
+2 -1
View File
@@ -9,6 +9,7 @@ from datetime import date
from tests.conftest import (
create_test_user, get_token_for_user, auth_header, create_test_kpi,
TEST_USER_PASSWORD,
)
from app.models import Objective, KR, KPIDefinition, Entity
@@ -253,7 +254,7 @@ class TestMultiTenant:
# 用 entity 2 登录
ent2_user = create_test_user(db, username="user2", role="ceo")
resp2 = client.post("/api/cma/auth/login", json={
"username": "user2", "password": "admin123", "entity_id": 2})
"username": "user2", "password": TEST_USER_PASSWORD, "entity_id": 2})
token2 = resp2.json().get("token")
assert token2
+2 -2
View File
@@ -6,7 +6,7 @@ import pytest
from fastapi.testclient import TestClient
from sqlalchemy.orm import Session
from app.models import Entity, User, UserEntity, StrategicMap, StrategicMapVersion
from tests.conftest import create_test_user, get_token_for_user, auth_header
from tests.conftest import create_test_user, get_token_for_user, auth_header, TEST_USER_PASSWORD
def _create_second_entity(db: Session, user: User) -> int:
@@ -18,7 +18,7 @@ def _create_second_entity(db: Session, user: User) -> int:
def _token_for_entity(client: TestClient, entity_id: int) -> str:
resp = client.post("/api/cma/auth/login", json={
"username": "testadmin", "password": "admin123", "entity_id": entity_id,
"username": "testadmin", "password": TEST_USER_PASSWORD, "entity_id": entity_id,
})
assert resp.status_code == 200
data = resp.json()
+2 -2
View File
@@ -5,7 +5,7 @@ import pytest
from fastapi.testclient import TestClient
from sqlalchemy.orm import Session
from app.models import Entity, User, UserEntity
from tests.conftest import create_test_user, get_token_for_user, auth_header
from tests.conftest import create_test_user, get_token_for_user, auth_header, TEST_USER_PASSWORD
def _create_second_entity(db: Session, user: User) -> int:
@@ -20,7 +20,7 @@ def _token_for_entity(client: TestClient, entity_id: int) -> str:
"""以指定账套登录获取token"""
resp = client.post("/api/cma/auth/login", json={
"username": "testadmin",
"password": "admin123",
"password": TEST_USER_PASSWORD,
"entity_id": entity_id,
})
assert resp.status_code == 200
@@ -15,7 +15,7 @@ from sqlalchemy.orm import Session
from app.models import KPIDefinition, KPIValue, KPIAlert, ActionPlan, AISuggestion, Entity, UserEntity, ActualCost
from app.api import ai_analysis
from tests.conftest import create_test_kpi, create_test_user, get_token_for_user, auth_header
from tests.conftest import create_test_kpi, create_test_user, get_token_for_user, auth_header, TEST_USER_PASSWORD
def _ensure_entity(db: Session, eid: int) -> Entity:
@@ -44,7 +44,7 @@ def _seed_user(db: Session, entity_id: int):
def _token(client: TestClient, entity_id: int) -> str:
"""登录并返回指定账套的 token(get_token_for_user 固定 entity1,这里手动登录)"""
resp = client.post("/api/cma/auth/login", json={
"username": "testadmin", "password": "admin123", "entity_id": entity_id,
"username": "testadmin", "password": TEST_USER_PASSWORD, "entity_id": entity_id,
})
assert resp.status_code == 200, f"登录 entity{entity_id} 失败: {resp.text[:300]}"
return resp.json().get("token")
+2 -2
View File
@@ -14,7 +14,7 @@ from fastapi.testclient import TestClient
from sqlalchemy.orm import Session
from app.models import CashForecast, ForecastAccuracy, Entity, User, UserEntity
from tests.conftest import create_test_user, get_token_for_user, auth_header
from tests.conftest import create_test_user, get_token_for_user, auth_header, TEST_USER_PASSWORD
BASE = "/api/cma/predict"
@@ -35,7 +35,7 @@ def _token_for_entity(client: TestClient, db: Session, eid: int) -> str:
db.add(UserEntity(user_id=user.id, entity_id=eid))
db.commit()
resp = client.post("/api/cma/auth/login", json={
"username": "testadmin", "password": "admin123", "entity_id": eid})
"username": "testadmin", "password": TEST_USER_PASSWORD, "entity_id": eid})
assert resp.status_code == 200, f"登录 entity{eid} 失败: {resp.text[:200]}"
return resp.json()["token"]
+2 -2
View File
@@ -9,7 +9,7 @@ generate(weekly/monthly/special) / history / history/{id}
import pytest
from fastapi.testclient import TestClient
from sqlalchemy.orm import Session
from tests.conftest import create_test_user, get_token_for_user, auth_header
from tests.conftest import create_test_user, get_token_for_user, auth_header, TEST_USER_PASSWORD
from app.models import (
KPIDefinition, KPIValue, BudgetPlan, StrategicMap, KPIAlert, ActionPlan,
Subject, ReportHistory,
@@ -668,7 +668,7 @@ class TestDupont:
db.add(UserEntity(user_id=user.id, entity_id=entity_id))
db.commit()
resp = client.post("/api/cma/auth/login", json={
"username": "testadmin", "password": "admin123", "entity_id": entity_id,
"username": "testadmin", "password": TEST_USER_PASSWORD, "entity_id": entity_id,
})
assert resp.status_code == 200, f"账套登录失败({entity_id}): {resp.status_code} {resp.text[:200]}"
return resp.json()["token"]
@@ -16,7 +16,7 @@ from app.models import KPIDefinition, KPIAlert, Entity
from app.models.knowledge import KnowledgeSummary, KnowledgeEvent
from app.models.knowledge_article import KnowledgeArticle
from app.services import knowledge_service
from tests.conftest import create_test_kpi, create_test_user, get_token_for_user, auth_header
from tests.conftest import create_test_kpi, create_test_user, get_token_for_user, auth_header, TEST_USER_PASSWORD
SUMMARY_FIELDS = dict(
level="daily", period_key="2026-09-01", content="测试摘要内容",
@@ -35,7 +35,7 @@ def _ensure_entity(db: Session, eid: int, name: str = None) -> Entity:
def _login_token(client: TestClient, username: str = "testadmin",
password: str = "admin123", entity_id: int = 1) -> str:
password: str = TEST_USER_PASSWORD, entity_id: int = 1) -> str:
"""登录指定账套返回 token(conftest.get_token_for_user 固定 entity1,这里参数化)"""
resp = client.post("/api/cma/auth/login", json={
"username": username, "password": password, "entity_id": entity_id,
+9 -9
View File
@@ -13,7 +13,7 @@ from sqlalchemy.orm import Session
from app.models import KPIDefinition, KPIAlert, Entity, UserEntity
from app.api.alert_rules import AlertRule, _eval_threshold
from tests.conftest import create_test_kpi, create_test_user, get_token_for_user, auth_header
from tests.conftest import create_test_kpi, create_test_user, get_token_for_user, auth_header, TEST_USER_PASSWORD
BOT_KEY = {"X-BOT-KEY": "cma-bot-finance-2026"}
@@ -103,7 +103,7 @@ class TestAlertRulesIsolation:
k1 = _seed_entity_kpi(db, entity_id=1, code="AR_E1")
db.commit()
create_test_user(db)
token = get_token_for_user(client, "testadmin", "admin123")
token = get_token_for_user(client, "testadmin", TEST_USER_PASSWORD)
headers = auth_header(token)
resp = client.post("/api/cma/alert-rules", headers=headers, json={
@@ -128,7 +128,7 @@ class TestAlertRulesIsolation:
entity_id=1))
db.commit()
create_test_user(db)
token = get_token_for_user(client, "testadmin", "admin123")
token = get_token_for_user(client, "testadmin", TEST_USER_PASSWORD)
r1 = client.get(f"/api/cma/alert-rules/kpi/{k1.id}", headers=auth_header(token))
assert r1.status_code == 200
@@ -149,7 +149,7 @@ class TestReportsIsolation:
data_status="verified", entity_id=1))
db.commit()
create_test_user(db)
token = get_token_for_user(client, "testadmin", "admin123")
token = get_token_for_user(client, "testadmin", TEST_USER_PASSWORD)
r1 = client.get("/api/cma/reports/profit-summary?period=2026-06", headers=auth_header(token))
assert r1.status_code == 200
@@ -164,7 +164,7 @@ class TestReportsIsolation:
_seed_entity_kpi(db, entity_id=2, code="TR_E2")
db.commit()
create_test_user(db)
token = get_token_for_user(client, "testadmin", "admin123")
token = get_token_for_user(client, "testadmin", TEST_USER_PASSWORD)
r1 = client.get("/api/cma/reports/kpi-trends", headers=auth_header(token))
assert r1.status_code == 200
@@ -212,7 +212,7 @@ class TestCheckGovernanceRetired:
_seed_entity_kpi(db, entity_id=1, code="GOV_KPI")
db.commit()
create_test_user(db)
token = get_token_for_user(client, "testadmin", "admin123")
token = get_token_for_user(client, "testadmin", TEST_USER_PASSWORD)
resp = client.get("/api/cma/data-quality/check-governance", headers=auth_header(token))
assert resp.status_code == 404, f"应 404,实际 {resp.status_code}: {resp.text[:200]}"
@@ -225,7 +225,7 @@ class TestCheckGovernanceRetired:
_seed_entity_kpi(db, entity_id=1, code="GOV2_KPI")
db.commit()
create_test_user(db)
token = get_token_for_user(client, "testadmin", "admin123")
token = get_token_for_user(client, "testadmin", TEST_USER_PASSWORD)
resp = client.get("/api/cma/data-quality/governance-check", headers=auth_header(token))
assert resp.status_code == 200, resp.text[:300]
@@ -242,7 +242,7 @@ class TestCrossEntityEndpointAuth:
def _login(self, client, db):
_ensure_entity(db, 2)
create_test_user(db)
return get_token_for_user(client, "testadmin", "admin123") # 登录账套 entity_id=1
return get_token_for_user(client, "testadmin", TEST_USER_PASSWORD) # 登录账套 entity_id=1
# ── 1. data-quality/governance-check ──────────────────
def test_governance_check_cross_entity_403(self, client, db):
@@ -352,7 +352,7 @@ class TestDupontSessionScope:
def _login(self, client, db):
_ensure_entity(db, 2)
create_test_user(db)
return get_token_for_user(client, "testadmin", "admin123") # 登录账套 entity_id=1
return get_token_for_user(client, "testadmin", TEST_USER_PASSWORD) # 登录账套 entity_id=1
def test_dupont_cross_entity_bohai_403(self, client, db):
"""酣客会话显式 entity=bohai(博海真实KPI)→ 403(旧豁免已撤销)"""
+26 -1
View File
@@ -3,8 +3,33 @@
import subprocess, json, sys
# 登录获取token
import os # noqa: E402 (凭据加载器需要)
# ---- 管理员凭据同源加载(2026-09-14 cma-defaultcred-code-fix)----
# 生产凭据只存 600 密钥文件;禁止在脚本/文档/日志里硬编码口令。
# 顺序:$CMA_ADMIN_ENV → wecom-ops 主副本(ops 轮换落点,权威) → 本 profile 副本。
def _admin_creds():
"""返回 (user, password, 来源文件);未找到 → 显式报错退出(不静默退回默认口令)"""
for p in (os.environ.get("CMA_ADMIN_ENV"),
"/root/.hermes/profiles/wecom-ops/.secrets/cma-admin.env",
"/root/.hermes/profiles/wecom-fullstack/.secrets/cma-admin.env"):
if p and os.path.isfile(p):
kv = {}
for line in open(p, encoding="utf-8"):
line = line.strip()
if "=" in line and not line.startswith("#"):
k, v = line.split("=", 1)
kv[k.strip()] = v.strip().strip('"').strip("'")
if kv.get("CMA_ADMIN_USER") and kv.get("CMA_ADMIN_PASS"):
return kv["CMA_ADMIN_USER"], kv["CMA_ADMIN_PASS"], p
raise SystemExit("[错误] 未找到 CMA 管理员凭据:请设 $CMA_ADMIN_ENV,或准备 600 权限密钥文件 "
".secrets/cma-admin.env(键名 CMA_ADMIN_USER / CMA_ADMIN_PASS)")
ADMIN_USER, ADMIN_PASS, CRED_SRC = _admin_creds()
print(f"[creds] 来源={CRED_SRC} 账号={ADMIN_USER}(口令不回显)")
r = subprocess.run(
"curl -s -X POST http://127.0.0.1:8010/api/cma/auth/login -H 'Content-Type: application/json' -d '{\"username\":\"admin\",\"password\":\"admin123\"}'",
f"curl -s -X POST http://127.0.0.1:8010/api/cma/auth/login -H 'Content-Type: application/json' -d '{{\"username\":\"{ADMIN_USER}\",\"password\":\"{ADMIN_PASS}\"}}'",
shell=True, capture_output=True, text=True, timeout=10
)
login = json.loads(r.stdout)
+26 -1
View File
@@ -2,10 +2,35 @@
"""管理会计OS数据详情检查"""
import json, urllib.request
import os # noqa: E402 (凭据加载器需要)
# ---- 管理员凭据同源加载(2026-09-14 cma-defaultcred-code-fix)----
# 生产凭据只存 600 密钥文件;禁止在脚本/文档/日志里硬编码口令。
# 顺序:$CMA_ADMIN_ENV → wecom-ops 主副本(ops 轮换落点,权威) → 本 profile 副本。
def _admin_creds():
"""返回 (user, password, 来源文件);未找到 → 显式报错退出(不静默退回默认口令)"""
for p in (os.environ.get("CMA_ADMIN_ENV"),
"/root/.hermes/profiles/wecom-ops/.secrets/cma-admin.env",
"/root/.hermes/profiles/wecom-fullstack/.secrets/cma-admin.env"):
if p and os.path.isfile(p):
kv = {}
for line in open(p, encoding="utf-8"):
line = line.strip()
if "=" in line and not line.startswith("#"):
k, v = line.split("=", 1)
kv[k.strip()] = v.strip().strip('"').strip("'")
if kv.get("CMA_ADMIN_USER") and kv.get("CMA_ADMIN_PASS"):
return kv["CMA_ADMIN_USER"], kv["CMA_ADMIN_PASS"], p
raise SystemExit("[错误] 未找到 CMA 管理员凭据:请设 $CMA_ADMIN_ENV,或准备 600 权限密钥文件 "
".secrets/cma-admin.env(键名 CMA_ADMIN_USER / CMA_ADMIN_PASS)")
ADMIN_USER, ADMIN_PASS, CRED_SRC = _admin_creds()
print(f"[creds] 来源={CRED_SRC} 账号={ADMIN_USER}(口令不回显)")
BASE = "http://127.0.0.1:8010"
def login():
data = json.dumps({"username":"admin","password":"admin123"}).encode()
data = json.dumps({"username":ADMIN_USER,"password":ADMIN_PASS}).encode()
req = urllib.request.Request(BASE + "/api/cma/auth/login", data=data,
headers={"Content-Type":"application/json"}, method="POST")
resp = json.loads(urllib.request.urlopen(req).read())
+26 -1
View File
@@ -2,10 +2,35 @@
"""查看原始API返回的key和样本数据"""
import json, urllib.request
import os # noqa: E402 (凭据加载器需要)
# ---- 管理员凭据同源加载(2026-09-14 cma-defaultcred-code-fix)----
# 生产凭据只存 600 密钥文件;禁止在脚本/文档/日志里硬编码口令。
# 顺序:$CMA_ADMIN_ENV → wecom-ops 主副本(ops 轮换落点,权威) → 本 profile 副本。
def _admin_creds():
"""返回 (user, password, 来源文件);未找到 → 显式报错退出(不静默退回默认口令)"""
for p in (os.environ.get("CMA_ADMIN_ENV"),
"/root/.hermes/profiles/wecom-ops/.secrets/cma-admin.env",
"/root/.hermes/profiles/wecom-fullstack/.secrets/cma-admin.env"):
if p and os.path.isfile(p):
kv = {}
for line in open(p, encoding="utf-8"):
line = line.strip()
if "=" in line and not line.startswith("#"):
k, v = line.split("=", 1)
kv[k.strip()] = v.strip().strip('"').strip("'")
if kv.get("CMA_ADMIN_USER") and kv.get("CMA_ADMIN_PASS"):
return kv["CMA_ADMIN_USER"], kv["CMA_ADMIN_PASS"], p
raise SystemExit("[错误] 未找到 CMA 管理员凭据:请设 $CMA_ADMIN_ENV,或准备 600 权限密钥文件 "
".secrets/cma-admin.env(键名 CMA_ADMIN_USER / CMA_ADMIN_PASS)")
ADMIN_USER, ADMIN_PASS, CRED_SRC = _admin_creds()
print(f"[creds] 来源={CRED_SRC} 账号={ADMIN_USER}(口令不回显)")
BASE = "http://127.0.0.1:8010"
def login():
data = json.dumps({"username":"admin","password":"admin123"}).encode()
data = json.dumps({"username":ADMIN_USER,"password":ADMIN_PASS}).encode()
req = urllib.request.Request(BASE + "/api/cma/auth/login", data=data,
headers={"Content-Type":"application/json"}, method="POST")
resp = json.loads(urllib.request.urlopen(req).read())
+34 -2
View File
@@ -59,7 +59,22 @@ echo " ✓ 服务重启完成"
# ── 部署后冒烟测试 ──
echo ""
echo "===== 冒烟测试 ====="
sleep 3
# 就绪轮询(2026-09-14 cma-defaultcred-code-fix):journalctl 实测冷启动就绪间隔 2/2/3/3/5s,
# 固定 sleep 3 会让贴边/超时的冷启动误判为冒烟红;改为每 0.5s 探一次 /health,命中即进入判据。
# 轮询用尽仍不健康 → 下面的健康判据照常判 ❌(不静默放行)。
# 探测上限可由 SMOKE_READY_MAX 覆盖(离线演练用短上限,生产不设即 30s)。
SMOKE_READY_MAX="${SMOKE_READY_MAX:-30}"
HEALTH_URL="http://127.0.0.1:8010/health"
READY_TRIES=$(( SMOKE_READY_MAX * 2 ))
READY_TRY=0
READY_T0=$(date +%s)
while [ "$READY_TRY" -lt "$READY_TRIES" ]; do
if curl -sf "$HEALTH_URL" > /dev/null 2>&1; then break; fi
sleep 0.5
READY_TRY=$((READY_TRY + 1))
done
READY_ELAPSED=$(( $(date +%s) - READY_T0 ))
echo " ⏱ 就绪探测: 耗时 ${READY_ELAPSED}s / 轮询 ${READY_TRY} 次(上限 ${SMOKE_READY_MAX}s,命中健康即停)"
SMOKE_FAIL=0
if curl -sf http://127.0.0.1:8010/health > /dev/null 2>&1; then
echo " ✅ 后端健康检查通过"
@@ -78,9 +93,26 @@ else
echo " ❌ Schema不一致,请检查: cd /root/cma-management/backend && venv/bin/python3 scripts/schema_check.py"
SMOKE_FAIL=1
fi
# 凭据同源(2026-09-14 cma-defaultcred-code-fix):登录冒烟不再硬编码默认账号/口令 —— 轮换后必须自动跟随。
# 落点:$CMA_ADMIN_ENV 或 600 密钥文件(默认 wecom-ops 主副本);缺失/缺键 → 显式报错并中止,禁止静默退回默认口令。
CMA_ADMIN_ENV_FILE="${CMA_ADMIN_ENV:-/root/.hermes/profiles/wecom-ops/.secrets/cma-admin.env}"
if [ ! -f "$CMA_ADMIN_ENV_FILE" ]; then
echo " ❌ 管理员凭据文件缺失: $CMA_ADMIN_ENV_FILE"
echo " 请准备 600 权限密钥文件(键名 CMA_ADMIN_USER / CMA_ADMIN_PASS),或用 CMA_ADMIN_ENV 指定路径"
exit 1
fi
set -a
# shellcheck disable=SC1090
. "$CMA_ADMIN_ENV_FILE"
set +a
if [ -z "${CMA_ADMIN_USER:-}" ] || [ -z "${CMA_ADMIN_PASS:-}" ]; then
echo " ❌ 凭据文件缺少 CMA_ADMIN_USER / CMA_ADMIN_PASS: $CMA_ADMIN_ENV_FILE"
exit 1
fi
echo " ℹ️ 登录冒烟凭据来源: $CMA_ADMIN_ENV_FILE(账号 ${CMA_ADMIN_USER},口令不回显)"
TOKEN=$(curl -s -X POST http://127.0.0.1:8010/api/cma/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin123","entity_id":1}' | python3 -c "import sys,json; print(json.load(sys.stdin).get('token',''))" 2>/dev/null)
-d "{\"username\":\"${CMA_ADMIN_USER}\",\"password\":\"${CMA_ADMIN_PASS}\",\"entity_id\":1}" | python3 -c "import sys,json; print(json.load(sys.stdin).get('token',''))" 2>/dev/null)
if [ -n "$TOKEN" ]; then
echo " ✅ 登录API通过"
if curl -sf -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8010/api/cma/kpis > /dev/null 2>&1; then
+4 -1
View File
@@ -80,9 +80,12 @@ get_entity_id 解析链(backend/app/deps.py):
```bash
# 1. 隔离验证:登录账套A → 全系统只显示A的数据
# 凭据见 600 密钥文件(CMA_ADMIN_USER / CMA_ADMIN_PASS),默认落点
# /root/.hermes/profiles/wecom-ops/.secrets/cma-admin.env
set -a; . "${CMA_ADMIN_ENV:-/root/.hermes/profiles/wecom-ops/.secrets/cma-admin.env}"; set +a
curl -s -X POST http://127.0.0.1:8010/api/cma/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin123","entity_id":2}'
-d "{\"username\":\"$CMA_ADMIN_USER\",\"password\":\"$CMA_ADMIN_PASS\",\"entity_id\":2}"
# → token;用该token访问 /api/cma/kpis 只返回博海数据
# 2. 越权验证:未授权entity请求返回403
+1 -1
View File
@@ -28,7 +28,7 @@
## ③ 验证路径
- **自动脚本**:
1. 生产 API 双账套断言:`/root/cma-management/backend/scripts/verify_budget_versions_ui_fix.py`(`cd backend && venv/bin/python3 scripts/verify_budget_versions_ui_fix.py`;用 admin/admin123 分别登录 entity1/entity2,断言新字段存在 + active→draft 映射 + plan_count==kpi_count + last_updated==updated_at)
1. 生产 API 双账套断言:`/root/cma-management/backend/scripts/verify_budget_versions_ui_fix.py`(`cd backend && venv/bin/python3 scripts/verify_budget_versions_ui_fix.py`;凭据见 600 密钥文件 `CMA_ADMIN_USER` / `CMA_ADMIN_PASS`(脚本同源读取:`$CMA_ADMIN_ENV` 或 `.secrets/cma-admin.env`)分别登录 entity1/entity2,断言新字段存在 + active→draft 映射 + plan_count==kpi_count + last_updated==updated_at)
2. 浏览器页面 DOM 实测:`/root/cma-management/backend/scripts/verify_budget_versions_ui_browser.py`(需先起 Chrome CDP 9224,命令见 cma-fullstack-ops 技能 references/budget-version-ui-field-state-fix-20260907.md 的 Chrome 起法;登录博海 entity2 断言 v2026H2 行=草稿/25条/462/最后更新非空/「提交审批」按钮,切酣客 entity1 断言 5 个 active 行有提交按钮 + 2026H2 approved「已批准」无按钮)
- **期望输出**:两个脚本末尾均输出 `ALL_PASS` 且 exit 0
- **已测基线**(最近一次验证日期+结果):2026-09-07 复跑双脚本 ALL_PASS。实测数据:博海 entity2 `v2026H2 | status=active | approval_status=draft | plan_count=25 | total=462.0 | last_updated='2026-08-30 11:24'`;酣客 entity1 共 6 版本,5 个 active(v1.0/v2.0/incremental/flexible/zero_based-20260826)→ 草稿+「提交审批」按钮,2026H2 approved → 「已批准」无按钮
@@ -53,12 +53,13 @@
## 三、curl 命令示例
### 1. 登录拿 token(admin/admin123,账套 entity_id=1 酣客)
### 1. 登录拿 token(凭据见 600 密钥文件 CMA_ADMIN_USER / CMA_ADMIN_PASS,账套 entity_id=1 酣客)
```bash
set -a; . "${CMA_ADMIN_ENV:-/root/.hermes/profiles/wecom-ops/.secrets/cma-admin.env}"; set +a
TOKEN=$(curl -s -X POST http://127.0.0.1:8010/api/cma/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin123","entity_id":1}' \
-d "{\"username\":\"$CMA_ADMIN_USER\",\"password\":\"$CMA_ADMIN_PASS\",\"entity_id\":1}" \
| python3 -c "import sys,json;print(json.load(sys.stdin)['token'])")
echo "$TOKEN" # 响应字段含 token/entity_id/entity_name/user
+7 -3
View File
@@ -30,7 +30,7 @@
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { ref, reactive, computed } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { authApi } from '../api/index'
@@ -39,7 +39,9 @@ import { isFixedEntity, getFixedEntityId } from '../utils/entity'
const router = useRouter()
const loading = ref(false)
const entities = ref<any[]>([])
const form = reactive({ username: 'admin', password: 'admin123', entity_id: undefined as number | undefined })
// 凭据面收口(2026-09-14 cma-defaultcred-code-fix):不再预填默认口令。
// 演示包若确需"一键登录"便利,只能以构建期开关(如 VITE_DEMO_PREFILL,默认关闭)恢复,不在源码里留默认口令。
const form = reactive({ username: '', password: '', entity_id: undefined as number | undefined })
// ── 固定企业模式(Phase C):构建期锁定单一企业,隐藏下拉、登录直接带固定 entity_id ──
const fixedMode = isFixedEntity()
@@ -89,7 +91,9 @@ async function handleLogin() {
loading.value = false
}
onMounted(() => loadEntities(form.username))
// 凭据面收口:不在挂载时用硬编码用户名(原 'admin')触发企业枚举
// (login-entities 是公开端点,以固定用户名预取=给匿名访客做用户名存在性探测)。
// 企业列表仅在用户输入用户名时由 onUsernameChange 加载;固定企业版展示走 fixedEntityLabel 的"企业 #id"兜底。
</script>
<style scoped>
+26 -1
View File
@@ -2,10 +2,35 @@
"""汇总管理会计OS管理能力报告"""
import json, urllib.request
import os # noqa: E402 (凭据加载器需要)
# ---- 管理员凭据同源加载(2026-09-14 cma-defaultcred-code-fix)----
# 生产凭据只存 600 密钥文件;禁止在脚本/文档/日志里硬编码口令。
# 顺序:$CMA_ADMIN_ENV → wecom-ops 主副本(ops 轮换落点,权威) → 本 profile 副本。
def _admin_creds():
"""返回 (user, password, 来源文件);未找到 → 显式报错退出(不静默退回默认口令)"""
for p in (os.environ.get("CMA_ADMIN_ENV"),
"/root/.hermes/profiles/wecom-ops/.secrets/cma-admin.env",
"/root/.hermes/profiles/wecom-fullstack/.secrets/cma-admin.env"):
if p and os.path.isfile(p):
kv = {}
for line in open(p, encoding="utf-8"):
line = line.strip()
if "=" in line and not line.startswith("#"):
k, v = line.split("=", 1)
kv[k.strip()] = v.strip().strip('"').strip("'")
if kv.get("CMA_ADMIN_USER") and kv.get("CMA_ADMIN_PASS"):
return kv["CMA_ADMIN_USER"], kv["CMA_ADMIN_PASS"], p
raise SystemExit("[错误] 未找到 CMA 管理员凭据:请设 $CMA_ADMIN_ENV,或准备 600 权限密钥文件 "
".secrets/cma-admin.env(键名 CMA_ADMIN_USER / CMA_ADMIN_PASS)")
ADMIN_USER, ADMIN_PASS, CRED_SRC = _admin_creds()
print(f"[creds] 来源={CRED_SRC} 账号={ADMIN_USER}(口令不回显)")
BASE = "http://127.0.0.1:8010"
def login():
data = json.dumps({"username":"admin","password":"admin123"}).encode()
data = json.dumps({"username":ADMIN_USER,"password":ADMIN_PASS}).encode()
req = urllib.request.Request(BASE + "/api/cma/auth/login", data=data,
headers={"Content-Type":"application/json"}, method="POST")
resp = json.loads(urllib.request.urlopen(req).read())
+15 -1
View File
@@ -30,7 +30,21 @@ DB_PORT = 3306
DB_USER = _CMA_DB_USER
DB_PASS = _CMA_DB_PASS
DB_NAME = "cma"
DEFAULT_PASSWORD = "admin123"
# 新建用户的初始口令不再硬编码(2026-09-14 cma-defaultcred-code-fix):
# 从 $CMA_DEFAULT_USER_PASSWORD 或 600 密钥文件同源读取;缺失 → 显式报错退出(禁止静默用默认口令)。
DEFAULT_PASSWORD = _os_gov.getenv('CMA_DEFAULT_USER_PASSWORD', '')
if not DEFAULT_PASSWORD:
for _p in (_os_gov.getenv('CMA_ADMIN_ENV'),
'/root/.hermes/profiles/wecom-ops/.secrets/cma-admin.env',
'/root/.hermes/profiles/wecom-fullstack/.secrets/cma-admin.env'):
if _p and _os_gov.path.isfile(_p):
for _line in open(_p, encoding='utf-8'):
_line = _line.strip()
if _line.startswith('CMA_DEFAULT_USER_PASSWORD='):
DEFAULT_PASSWORD = _line.split('=', 1)[1].strip().strip('"').strip("'")
if not DEFAULT_PASSWORD:
raise SystemExit("[错误] 未找到新建用户初始口令:请设 $CMA_DEFAULT_USER_PASSWORD,"
"或在 600 密钥文件($CMA_ADMIN_ENV / .secrets/cma-admin.env)里定义 CMA_DEFAULT_USER_PASSWORD")
# ==== 工具函数 ====
def mcp_call(name, arguments={}):