feat: 完成 issue #165 [A2] 菜单播种 v2——原生菜单(type=1/component/iconify)+PUT 幂等更新+admin 角色

This commit is contained in:
2026-08-06 12:53:15 +08:00
parent 91b16ec6c3
commit ddd59345ed
+54 -32
View File
@@ -31,32 +31,40 @@ import urllib.error
import urllib.parse
import urllib.request
# ---- iAOP 模块注册表(镜像 web/shared/session.js 的 MODULES,顺序即菜单顺序) ----
# ---- iAOP 模块注册表 v2(Epic #163 原生重构:type=1 原生菜单,component→views/iaop/*) ----
# path=/iaop/<key>,component=iaop/<key>/index(与 apps/web-antdv-next/src/views/iaop/* 对应);
# icon 为 iconify 名;users 不注册(FBA 系统管理承载)、mobile 暂不注册(见 B9)。
MODULES = [
{"key": "cockpit", "title": "配置化驾驶舱", "icon": "🚀", "path": "/cockpit/"},
{"key": "chat", "title": "对话助手", "icon": "💬", "path": "/chat/assistant.html"},
{"key": "studio", "title": "模板配置台", "icon": "🧩", "path": "/studio/", "children": [
{"key": "import", "title": "点位导入"},
{"key": "hyper", "title": "模型超参"},
{"key": "layout", "title": "驾驶舱编排"},
{"key": "version", "title": "版本发布"},
{"key": "cockpit", "title": "配置化驾驶舱", "icon": "mdi:view-dashboard",
"path": "/iaop/cockpit", "component": "iaop/cockpit/index"},
{"key": "chat", "title": "对话助手", "icon": "mdi:chat-processing-outline",
"path": "/iaop/chat", "component": "iaop/chat/index"},
{"key": "studio", "title": "模板配置台", "icon": "mdi:widgets-outline",
"path": "/iaop/studio", "component": "iaop/studio/index", "children": [
{"key": "import", "title": "点位导入", "component": "iaop/studio/import"},
{"key": "hyper", "title": "模型超参", "component": "iaop/studio/hyper"},
{"key": "layout", "title": "驾驶舱编排", "component": "iaop/studio/layout"},
{"key": "version", "title": "版本发布", "component": "iaop/studio/version"},
]},
{"key": "admin", "title": "管理控制台", "icon": "⚙️", "path": "/admin/", "children": [
{"key": "models", "title": "模型管理"},
{"key": "kb", "title": "知识库管理"},
{"key": "alerts", "title": "告警确认"},
{"key": "audit", "title": "审计查询"},
{"key": "admin", "title": "管理控制台", "icon": "mdi:cog-outline",
"path": "/iaop/admin", "component": "iaop/admin/index", "children": [
{"key": "models", "title": "模型管理", "component": "iaop/admin/models"},
{"key": "kb", "title": "知识库管理", "component": "iaop/admin/kb"},
{"key": "alerts", "title": "告警确认", "component": "iaop/admin/alerts"},
{"key": "audit", "title": "审计查询", "component": "iaop/admin/audit"},
]},
{"key": "users", "title": "用户与角色", "icon": "👤", "path": "/auth/users.html"},
{"key": "mobile", "title": "移动端驾驶舱", "icon": "📱", "path": "/mobile/"},
]
# 角色 → 可见模块(PRD 8.2;admin 通常为 is_superuser,无需分配,看到全部)
# 角色 → 可见 iAOP 模块(PRD 8.2;admin 角色为新增业务管理员,非超管)
ROLE_MENUS = {
"viewer": ["cockpit", "chat", "mobile"],
"engineer": ["cockpit", "chat", "mobile", "studio"],
"viewer": ["cockpit", "chat"],
"engineer": ["cockpit", "chat", "studio"],
"admin": ["cockpit", "chat", "studio", "admin"],
}
# admin 角色额外分配:FBA 原生管理菜单(存在即分配)
FBA_ADMIN_MENUS = ["system", "monitor", "log", "scheduler"]
class FbaClient:
def __init__(self, base: str, username: str, password: str):
@@ -133,21 +141,26 @@ def main():
# ---- 1. 播种菜单(幂等:按 name 查重) --------------------------------
existing = flatten_menu_names(cli.get("/sys/menus"))
created, skipped = 0, 0
created, updated = 0, 0
def ensure_menu(name, title, path, mtype, sort, parent_id=None):
nonlocal created, skipped
if name in existing:
skipped += 1
return existing[name]
def upsert_menu(name, title, path, mtype, sort, parent_id=None,
component=None, icon=None, perms=None):
nonlocal created, updated
body = {
"title": title, "name": name, "path": path,
"parent_id": parent_id, "sort": sort, "icon": None,
"parent_id": parent_id, "sort": sort, "icon": icon,
"type": mtype, # 0目录 1菜单
"component": None, "perms": f"iaop:{name}",
"component": component, "perms": perms or f"iaop:{name}",
"status": 1, "display": 1, "cache": 0,
"link": None, "remark": "iAOP 模块(seed_iaop_menus.py)",
"link": None, "remark": "iAOP 原生菜单(seed_iaop_menus.py v2)",
}
if name in existing:
# 幂等:已存在(含 #161 旧版 type=0/iframe 菜单)→ PUT 更新关键字段
mid = existing[name]
cli.put(f"/sys/menus/{mid}", body)
updated += 1
print(f"[upd] 更新菜单 {name}({title})")
return mid
cli.post("/sys/menus", body)
created += 1
# 创建后重新拉取以拿到 id(接口不回传 id)
@@ -159,13 +172,17 @@ def main():
for i, m in enumerate(MODULES):
children = m.get("children")
if children:
pid = ensure_menu(m["key"], m["title"], m["path"], 0, i)
pid = upsert_menu(m["key"], m["title"], m["path"], 0, i,
component=m.get("component"), icon=m.get("icon"))
for j, c in enumerate(children):
ensure_menu(c["key"], c["title"], f"{m['path']}#{c['key']}",
1, j, parent_id=pid)
upsert_menu(c["key"], c["title"],
f"{m['path']}/{c['key']}", 1, j,
parent_id=pid, component=c.get("component"),
icon=c.get("icon"))
else:
ensure_menu(m["key"], m["title"], m["path"], 1, i)
print(f"[ok] 菜单:新建 {created},已存在跳过 {skipped}")
upsert_menu(m["key"], m["title"], m["path"], 1, i,
component=m.get("component"), icon=m.get("icon"))
print(f"[ok] 菜单:新建 {created},更新 {updated}")
# ---- 2. 播种角色并分配菜单 -------------------------------------------
menu_ids = flatten_menu_names(cli.get("/sys/menus"))
@@ -195,6 +212,11 @@ def main():
ids = []
for k in mod_keys:
ids.extend(module_menu_ids(k))
# admin 角色额外分配 FBA 原生管理菜单(system/monitor/log/scheduler 等)
if role_name == "admin":
for fba_name in FBA_ADMIN_MENUS:
if fba_name in menu_ids:
ids.append(menu_ids[fba_name])
cli.put(f"/sys/roles/{rid}/menus", {"menus": ids})
print(f"[ok] 角色 {role_name} 菜单:{', '.join(mod_keys)}({len(ids)} 项)")