feat: polish admin policy UI and tool selection
This commit is contained in:
parent
68b2ace43f
commit
99cbea30da
@ -9,6 +9,7 @@ LOGS_DIR = "./logs"
|
|||||||
USER_SPACE_DIR = "./users"
|
USER_SPACE_DIR = "./users"
|
||||||
USERS_DB_FILE = f"{DATA_DIR}/users.json"
|
USERS_DB_FILE = f"{DATA_DIR}/users.json"
|
||||||
INVITE_CODES_FILE = f"{DATA_DIR}/invite_codes.json"
|
INVITE_CODES_FILE = f"{DATA_DIR}/invite_codes.json"
|
||||||
|
ADMIN_POLICY_FILE = f"{DATA_DIR}/admin_policy.json"
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"DEFAULT_PROJECT_PATH",
|
"DEFAULT_PROJECT_PATH",
|
||||||
@ -18,4 +19,5 @@ __all__ = [
|
|||||||
"USER_SPACE_DIR",
|
"USER_SPACE_DIR",
|
||||||
"USERS_DB_FILE",
|
"USERS_DB_FILE",
|
||||||
"INVITE_CODES_FILE",
|
"INVITE_CODES_FILE",
|
||||||
|
"ADMIN_POLICY_FILE",
|
||||||
]
|
]
|
||||||
|
|||||||
@ -137,13 +137,20 @@ class MainTerminal:
|
|||||||
self.focused_files = {} # {path: content} 存储聚焦的文件内容
|
self.focused_files = {} # {path: content} 存储聚焦的文件内容
|
||||||
|
|
||||||
self.current_session_id = 0 # 用于标识不同的任务会话
|
self.current_session_id = 0 # 用于标识不同的任务会话
|
||||||
|
# 工具类别(可被管理员动态覆盖)
|
||||||
|
self.tool_categories_map = dict(TOOL_CATEGORIES)
|
||||||
|
self.admin_forced_category_states: Dict[str, Optional[bool]] = {}
|
||||||
|
self.admin_disabled_models: List[str] = []
|
||||||
|
self.admin_policy_ui_blocks: Dict[str, bool] = {}
|
||||||
|
self.admin_policy_version: Optional[str] = None
|
||||||
|
|
||||||
# 工具启用状态
|
# 工具启用状态
|
||||||
self.tool_category_states = {
|
self.tool_category_states: Dict[str, bool] = {
|
||||||
key: category.default_enabled
|
key: category.default_enabled
|
||||||
for key, category in TOOL_CATEGORIES.items()
|
for key, category in self.tool_categories_map.items()
|
||||||
}
|
}
|
||||||
self.disabled_tools = set()
|
self.disabled_tools: Set[str] = set()
|
||||||
self.disabled_notice_tools = set()
|
self.disabled_notice_tools: Set[str] = set()
|
||||||
self._refresh_disabled_tools()
|
self._refresh_disabled_tools()
|
||||||
self.thinking_fast_interval = THINKING_FAST_INTERVAL
|
self.thinking_fast_interval = THINKING_FAST_INTERVAL
|
||||||
self.default_disabled_tool_categories: List[str] = []
|
self.default_disabled_tool_categories: List[str] = []
|
||||||
@ -358,12 +365,12 @@ class MainTerminal:
|
|||||||
if isinstance(raw_disabled, list):
|
if isinstance(raw_disabled, list):
|
||||||
disabled_categories = [
|
disabled_categories = [
|
||||||
key for key in raw_disabled
|
key for key in raw_disabled
|
||||||
if isinstance(key, str) and key in TOOL_CATEGORIES
|
if isinstance(key, str) and key in self.tool_categories_map
|
||||||
]
|
]
|
||||||
self.default_disabled_tool_categories = disabled_categories
|
self.default_disabled_tool_categories = disabled_categories
|
||||||
|
|
||||||
# Reset category states to defaults before applying overrides
|
# Reset category states to defaults before applying overrides
|
||||||
for key, category in TOOL_CATEGORIES.items():
|
for key, category in self.tool_categories_map.items():
|
||||||
self.tool_category_states[key] = False if key in disabled_categories else category.default_enabled
|
self.tool_category_states[key] = False if key in disabled_categories else category.default_enabled
|
||||||
self._refresh_disabled_tools()
|
self._refresh_disabled_tools()
|
||||||
|
|
||||||
@ -539,30 +546,71 @@ class MainTerminal:
|
|||||||
|
|
||||||
def set_tool_category_enabled(self, category: str, enabled: bool) -> None:
|
def set_tool_category_enabled(self, category: str, enabled: bool) -> None:
|
||||||
"""设置工具类别的启用状态 / Toggle tool category enablement."""
|
"""设置工具类别的启用状态 / Toggle tool category enablement."""
|
||||||
if category not in TOOL_CATEGORIES:
|
categories = self.tool_categories_map
|
||||||
|
if category not in categories:
|
||||||
raise ValueError(f"未知的工具类别: {category}")
|
raise ValueError(f"未知的工具类别: {category}")
|
||||||
|
forced = self.admin_forced_category_states.get(category)
|
||||||
|
if isinstance(forced, bool) and forced != enabled:
|
||||||
|
raise ValueError("该类别被管理员强制为启用/禁用,无法修改")
|
||||||
self.tool_category_states[category] = bool(enabled)
|
self.tool_category_states[category] = bool(enabled)
|
||||||
self._refresh_disabled_tools()
|
self._refresh_disabled_tools()
|
||||||
|
|
||||||
|
def set_admin_policy(
|
||||||
|
self,
|
||||||
|
categories: Optional[Dict[str, "ToolCategory"]] = None,
|
||||||
|
forced_category_states: Optional[Dict[str, Optional[bool]]] = None,
|
||||||
|
disabled_models: Optional[List[str]] = None,
|
||||||
|
) -> None:
|
||||||
|
"""应用管理员策略(工具分类、强制开关、模型禁用)。"""
|
||||||
|
if categories:
|
||||||
|
self.tool_categories_map = dict(categories)
|
||||||
|
# 重新构建启用状态映射,保留已有值
|
||||||
|
new_states: Dict[str, bool] = {}
|
||||||
|
for key, cat in self.tool_categories_map.items():
|
||||||
|
if key in self.tool_category_states:
|
||||||
|
new_states[key] = self.tool_category_states[key]
|
||||||
|
else:
|
||||||
|
new_states[key] = cat.default_enabled
|
||||||
|
self.tool_category_states = new_states
|
||||||
|
# 清理已被移除的类别
|
||||||
|
for removed in list(self.tool_category_states.keys()):
|
||||||
|
if removed not in self.tool_categories_map:
|
||||||
|
self.tool_category_states.pop(removed, None)
|
||||||
|
|
||||||
|
self.admin_forced_category_states = forced_category_states or {}
|
||||||
|
self.admin_disabled_models = disabled_models or []
|
||||||
|
self._refresh_disabled_tools()
|
||||||
|
|
||||||
def get_tool_settings_snapshot(self) -> List[Dict[str, object]]:
|
def get_tool_settings_snapshot(self) -> List[Dict[str, object]]:
|
||||||
"""获取工具类别状态快照 / Return tool category states snapshot."""
|
"""获取工具类别状态快照 / Return tool category states snapshot."""
|
||||||
snapshot: List[Dict[str, object]] = []
|
snapshot: List[Dict[str, object]] = []
|
||||||
for key, category in TOOL_CATEGORIES.items():
|
categories = self.tool_categories_map
|
||||||
|
for key, category in categories.items():
|
||||||
|
forced = self.admin_forced_category_states.get(key)
|
||||||
|
enabled = self.tool_category_states.get(key, category.default_enabled)
|
||||||
|
if isinstance(forced, bool):
|
||||||
|
enabled = forced
|
||||||
snapshot.append({
|
snapshot.append({
|
||||||
"id": key,
|
"id": key,
|
||||||
"label": category.label,
|
"label": category.label,
|
||||||
"enabled": self.tool_category_states.get(key, category.default_enabled),
|
"enabled": enabled,
|
||||||
"tools": list(category.tools),
|
"tools": list(category.tools),
|
||||||
|
"locked": isinstance(forced, bool),
|
||||||
|
"locked_state": forced if isinstance(forced, bool) else None,
|
||||||
})
|
})
|
||||||
return snapshot
|
return snapshot
|
||||||
|
|
||||||
def _refresh_disabled_tools(self) -> None:
|
def _refresh_disabled_tools(self) -> None:
|
||||||
"""刷新禁用工具列表 / Refresh disabled tool set."""
|
"""刷新禁用工具列表 / Refresh disabled tool set."""
|
||||||
disabled = set()
|
disabled: Set[str] = set()
|
||||||
notice = set()
|
notice: Set[str] = set()
|
||||||
for key, enabled in self.tool_category_states.items():
|
categories = self.tool_categories_map
|
||||||
if not enabled:
|
for key, category in categories.items():
|
||||||
category = TOOL_CATEGORIES[key]
|
state = self.tool_category_states.get(key, category.default_enabled)
|
||||||
|
forced = self.admin_forced_category_states.get(key)
|
||||||
|
if isinstance(forced, bool):
|
||||||
|
state = forced
|
||||||
|
if not state:
|
||||||
disabled.update(category.tools)
|
disabled.update(category.tools)
|
||||||
if not getattr(category, "silent_when_disabled", False):
|
if not getattr(category, "silent_when_disabled", False):
|
||||||
notice.update(category.tools)
|
notice.update(category.tools)
|
||||||
|
|||||||
228
modules/admin_policy_manager.py
Normal file
228
modules/admin_policy_manager.py
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
"""管理员策略配置管理。
|
||||||
|
|
||||||
|
职责:
|
||||||
|
- 持久化管理员在工具分类、模型禁用、前端功能禁用等方面的策略。
|
||||||
|
- 支持作用域:global / role / user / invite_code,按优先级合并得到最终生效策略。
|
||||||
|
- 仅在此文件读写配置,避免侵入现有模块。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, Any, Tuple
|
||||||
|
|
||||||
|
from config.paths import ADMIN_POLICY_FILE
|
||||||
|
|
||||||
|
# 可用的模型 key(与前端、model_profiles 保持一致)
|
||||||
|
ALLOWED_MODELS = {"kimi", "deepseek", "qwen3-max", "qwen3-vl-plus"}
|
||||||
|
|
||||||
|
# UI 禁用项键名,前后端统一
|
||||||
|
UI_BLOCK_KEYS = [
|
||||||
|
"collapse_workspace",
|
||||||
|
"block_file_manager",
|
||||||
|
"block_personal_space",
|
||||||
|
"block_upload",
|
||||||
|
"block_conversation_review",
|
||||||
|
"block_tool_toggle",
|
||||||
|
"block_realtime_terminal",
|
||||||
|
"block_focus_panel",
|
||||||
|
"block_token_panel",
|
||||||
|
"block_compress_conversation",
|
||||||
|
"block_virtual_monitor",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_file() -> Path:
|
||||||
|
path = Path(ADMIN_POLICY_FILE).expanduser().resolve()
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
if not path.exists():
|
||||||
|
_save_policy(_default_policy(), path)
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def _read_policy(path: Path) -> Dict[str, Any]:
|
||||||
|
try:
|
||||||
|
return json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
except Exception:
|
||||||
|
return _default_policy()
|
||||||
|
|
||||||
|
|
||||||
|
def _save_policy(payload: Dict[str, Any], path: Path | None = None) -> None:
|
||||||
|
target = path or _ensure_file()
|
||||||
|
target.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def _default_policy() -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"updated_at": time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
"global": _blank_config(),
|
||||||
|
"roles": {},
|
||||||
|
"users": {},
|
||||||
|
"invites": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _blank_config() -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"category_overrides": {}, # id -> {label, tools, default_enabled?}
|
||||||
|
"remove_categories": [], # ids
|
||||||
|
"forced_category_states": {}, # id -> true/false
|
||||||
|
"disabled_models": [], # list of model keys
|
||||||
|
"ui_blocks": {}, # key -> bool
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _merge_config(base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""浅合并策略,数组采取并集/覆盖逻辑。"""
|
||||||
|
merged = _blank_config()
|
||||||
|
base = base or {}
|
||||||
|
override = override or {}
|
||||||
|
|
||||||
|
merged["category_overrides"] = {
|
||||||
|
**(base.get("category_overrides") or {}),
|
||||||
|
**(override.get("category_overrides") or {}),
|
||||||
|
}
|
||||||
|
merged["remove_categories"] = list({
|
||||||
|
*[c for c in base.get("remove_categories") or [] if isinstance(c, str)],
|
||||||
|
*[c for c in override.get("remove_categories") or [] if isinstance(c, str)],
|
||||||
|
})
|
||||||
|
merged["forced_category_states"] = {
|
||||||
|
**(base.get("forced_category_states") or {}),
|
||||||
|
**(override.get("forced_category_states") or {}),
|
||||||
|
}
|
||||||
|
merged["disabled_models"] = list({
|
||||||
|
*[m for m in base.get("disabled_models") or [] if m in ALLOWED_MODELS],
|
||||||
|
*[m for m in override.get("disabled_models") or [] if m in ALLOWED_MODELS],
|
||||||
|
})
|
||||||
|
ui_base = base.get("ui_blocks") or {}
|
||||||
|
ui_override = override.get("ui_blocks") or {}
|
||||||
|
merged["ui_blocks"] = {**ui_base, **ui_override}
|
||||||
|
return merged
|
||||||
|
|
||||||
|
|
||||||
|
def load_policy() -> Dict[str, Any]:
|
||||||
|
path = _ensure_file()
|
||||||
|
payload = _read_policy(path)
|
||||||
|
# 补全缺失字段
|
||||||
|
if not isinstance(payload, dict):
|
||||||
|
payload = _default_policy()
|
||||||
|
payload.setdefault("updated_at", time.strftime("%Y-%m-%d %H:%M:%S"))
|
||||||
|
payload.setdefault("global", _blank_config())
|
||||||
|
payload.setdefault("roles", {})
|
||||||
|
payload.setdefault("users", {})
|
||||||
|
payload.setdefault("invites", {})
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
def save_scope_policy(target_type: str, target_value: str, config: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""更新指定作用域的策略并保存,返回最新策略。"""
|
||||||
|
if target_type not in {"global", "role", "user", "invite"}:
|
||||||
|
raise ValueError("invalid target_type")
|
||||||
|
|
||||||
|
policy = load_policy()
|
||||||
|
normalized = _blank_config()
|
||||||
|
normalized = _merge_config(normalized, config or {})
|
||||||
|
|
||||||
|
if target_type == "global":
|
||||||
|
policy["global"] = normalized
|
||||||
|
elif target_type == "role":
|
||||||
|
policy.setdefault("roles", {})[target_value] = normalized
|
||||||
|
elif target_type == "user":
|
||||||
|
policy.setdefault("users", {})[target_value] = normalized
|
||||||
|
else:
|
||||||
|
policy.setdefault("invites", {})[target_value] = normalized
|
||||||
|
|
||||||
|
policy["updated_at"] = time.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
_save_policy(policy)
|
||||||
|
return policy
|
||||||
|
|
||||||
|
|
||||||
|
def _collect_categories_with_overrides(overrides: Dict[str, Any]) -> Dict[str, Dict[str, Any]]:
|
||||||
|
"""从 override 字典生成 {id: {label, tools, default_enabled}}"""
|
||||||
|
from core.tool_config import TOOL_CATEGORIES # 延迟导入避免循环
|
||||||
|
|
||||||
|
base: Dict[str, Dict[str, Any]] = {
|
||||||
|
key: {
|
||||||
|
"label": cat.label,
|
||||||
|
"tools": list(cat.tools),
|
||||||
|
"default_enabled": bool(cat.default_enabled),
|
||||||
|
"silent_when_disabled": getattr(cat, "silent_when_disabled", False),
|
||||||
|
}
|
||||||
|
for key, cat in TOOL_CATEGORIES.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_ids = set(overrides.get("remove_categories") or [])
|
||||||
|
for rid in remove_ids:
|
||||||
|
base.pop(rid, None)
|
||||||
|
|
||||||
|
for cid, payload in (overrides.get("category_overrides") or {}).items():
|
||||||
|
if not isinstance(cid, str):
|
||||||
|
continue
|
||||||
|
if not isinstance(payload, dict):
|
||||||
|
continue
|
||||||
|
label = payload.get("label") or cid
|
||||||
|
tools = payload.get("tools") or []
|
||||||
|
if not isinstance(tools, list):
|
||||||
|
continue
|
||||||
|
default_enabled = bool(payload.get("default_enabled", True))
|
||||||
|
base[cid] = {
|
||||||
|
"label": str(label),
|
||||||
|
"tools": [t for t in tools if isinstance(t, str)],
|
||||||
|
"default_enabled": default_enabled,
|
||||||
|
"silent_when_disabled": bool(payload.get("silent_when_disabled", False)),
|
||||||
|
}
|
||||||
|
return base
|
||||||
|
|
||||||
|
|
||||||
|
def get_effective_policy(username: str | None, role: str | None, invite_code: str | None) -> Dict[str, Any]:
|
||||||
|
"""按优先级合并策略,返回生效配置。"""
|
||||||
|
policy = load_policy()
|
||||||
|
scopes: Tuple[Tuple[str, Dict[str, Any]], ...] = (
|
||||||
|
("global", policy.get("global") or _blank_config()),
|
||||||
|
("role", (policy.get("roles") or {}).get(role or "", {})),
|
||||||
|
("invite", (policy.get("invites") or {}).get(invite_code or "", {})),
|
||||||
|
("user", (policy.get("users") or {}).get(username or "", {})),
|
||||||
|
)
|
||||||
|
|
||||||
|
merged = _blank_config()
|
||||||
|
for _, cfg in scopes:
|
||||||
|
merged = _merge_config(merged, cfg or {})
|
||||||
|
|
||||||
|
# 计算最终分类
|
||||||
|
categories = _collect_categories_with_overrides(merged)
|
||||||
|
forced_states = {
|
||||||
|
key: bool(val) if isinstance(val, bool) else None
|
||||||
|
for key, val in (merged.get("forced_category_states") or {}).items()
|
||||||
|
if key
|
||||||
|
}
|
||||||
|
disabled_models = [m for m in merged.get("disabled_models") or [] if m in ALLOWED_MODELS]
|
||||||
|
ui_blocks = {k: bool(v) for k, v in (merged.get("ui_blocks") or {}).items() if k in UI_BLOCK_KEYS}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"categories": categories,
|
||||||
|
"forced_category_states": forced_states,
|
||||||
|
"disabled_models": disabled_models,
|
||||||
|
"ui_blocks": ui_blocks,
|
||||||
|
"updated_at": policy.get("updated_at"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def describe_defaults() -> Dict[str, Any]:
|
||||||
|
"""返回默认(未覆盖)工具分类,用于前端渲染。"""
|
||||||
|
from core.tool_config import TOOL_CATEGORIES
|
||||||
|
|
||||||
|
return {
|
||||||
|
"categories": {
|
||||||
|
key: {
|
||||||
|
"label": cat.label,
|
||||||
|
"tools": list(cat.tools),
|
||||||
|
"default_enabled": bool(cat.default_enabled),
|
||||||
|
"silent_when_disabled": getattr(cat, "silent_when_disabled", False),
|
||||||
|
}
|
||||||
|
for key, cat in TOOL_CATEGORIES.items()
|
||||||
|
},
|
||||||
|
"models": sorted(list(ALLOWED_MODELS)),
|
||||||
|
"ui_block_keys": UI_BLOCK_KEYS,
|
||||||
|
}
|
||||||
14
static/admin_policy/index.html
Normal file
14
static/admin_policy/index.html
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>管理员策略配置</title>
|
||||||
|
<link rel="stylesheet" href="/static/dist/assets/adminPolicy.css" />
|
||||||
|
<script src="/static/security.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="admin-policy-app"></div>
|
||||||
|
<script type="module" src="/static/dist/assets/adminPolicy.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -70,6 +70,7 @@
|
|||||||
:is-connected="isConnected"
|
:is-connected="isConnected"
|
||||||
:panel-menu-open="panelMenuOpen"
|
:panel-menu-open="panelMenuOpen"
|
||||||
:panel-mode="panelMode"
|
:panel-mode="panelMode"
|
||||||
|
:file-manager-disabled="policyUiBlocks.block_file_manager"
|
||||||
@toggle-panel-menu="togglePanelMenu"
|
@toggle-panel-menu="togglePanelMenu"
|
||||||
@select-panel="selectPanelMode"
|
@select-panel="selectPanelMode"
|
||||||
@open-file-manager="openGuiFileManager"
|
@open-file-manager="openGuiFileManager"
|
||||||
@ -187,6 +188,13 @@
|
|||||||
:icon-style="iconStyle"
|
:icon-style="iconStyle"
|
||||||
:tool-category-icon="toolCategoryIcon"
|
:tool-category-icon="toolCategoryIcon"
|
||||||
:selected-images="selectedImages"
|
:selected-images="selectedImages"
|
||||||
|
:block-upload="policyUiBlocks.block_upload"
|
||||||
|
:block-tool-toggle="policyUiBlocks.block_tool_toggle"
|
||||||
|
:block-realtime-terminal="policyUiBlocks.block_realtime_terminal"
|
||||||
|
:block-focus-panel="policyUiBlocks.block_focus_panel"
|
||||||
|
:block-token-panel="policyUiBlocks.block_token_panel"
|
||||||
|
:block-compress-conversation="policyUiBlocks.block_compress_conversation"
|
||||||
|
:block-conversation-review="policyUiBlocks.block_conversation_review"
|
||||||
@update:input-message="inputSetMessage"
|
@update:input-message="inputSetMessage"
|
||||||
@input-change="handleInputChange"
|
@input-change="handleInputChange"
|
||||||
@input-focus="handleInputFocus"
|
@input-focus="handleInputFocus"
|
||||||
|
|||||||
@ -16,6 +16,7 @@
|
|||||||
<button type="button" :disabled="refreshing" @click="handleManualRefresh">
|
<button type="button" :disabled="refreshing" @click="handleManualRefresh">
|
||||||
{{ refreshing ? '刷新中...' : '立即刷新' }}
|
{{ refreshing ? '刷新中...' : '立即刷新' }}
|
||||||
</button>
|
</button>
|
||||||
|
<a class="link-btn" href="/admin/policy" target="_blank" rel="noopener">策略配置</a>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@ -557,6 +558,20 @@ const inviteStatus = (remaining: number | null | undefined) => {
|
|||||||
color: #5b4b35;
|
color: #5b4b35;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.link-btn {
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.12);
|
||||||
|
background: #fff;
|
||||||
|
color: #2a2013;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-btn:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
|
|||||||
1074
static/src/admin/PolicyApp.vue
Normal file
1074
static/src/admin/PolicyApp.vue
Normal file
File diff suppressed because it is too large
Load Diff
4
static/src/admin/policyMain.ts
Normal file
4
static/src/admin/policyMain.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { createApp } from 'vue';
|
||||||
|
import PolicyApp from './PolicyApp.vue';
|
||||||
|
|
||||||
|
createApp(PolicyApp).mount('#admin-policy-app');
|
||||||
@ -28,6 +28,7 @@ import { usePersonalizationStore } from './stores/personalization';
|
|||||||
import { useModelStore } from './stores/model';
|
import { useModelStore } from './stores/model';
|
||||||
import { useChatActionStore } from './stores/chatActions';
|
import { useChatActionStore } from './stores/chatActions';
|
||||||
import { useMonitorStore } from './stores/monitor';
|
import { useMonitorStore } from './stores/monitor';
|
||||||
|
import { usePolicyStore } from './stores/policy';
|
||||||
import { ICONS, TOOL_CATEGORY_ICON_MAP } from './utils/icons';
|
import { ICONS, TOOL_CATEGORY_ICON_MAP } from './utils/icons';
|
||||||
import { initializeLegacySocket } from './composables/useLegacySocket';
|
import { initializeLegacySocket } from './composables/useLegacySocket';
|
||||||
import { useConnectionStore } from './stores/connection';
|
import { useConnectionStore } from './stores/connection';
|
||||||
@ -435,8 +436,21 @@ const appOptions = {
|
|||||||
}
|
}
|
||||||
return this.thinkingMode ? 'thinking' : 'fast';
|
return this.thinkingMode ? 'thinking' : 'fast';
|
||||||
},
|
},
|
||||||
|
policyUiBlocks() {
|
||||||
|
const store = usePolicyStore();
|
||||||
|
return store.uiBlocks || {};
|
||||||
|
},
|
||||||
|
adminDisabledModels() {
|
||||||
|
const store = usePolicyStore();
|
||||||
|
return store.disabledModelSet;
|
||||||
|
},
|
||||||
modelOptions() {
|
modelOptions() {
|
||||||
return this.models || [];
|
const disabledSet = this.adminDisabledModels || new Set();
|
||||||
|
const options = this.models || [];
|
||||||
|
return options.map((opt) => ({
|
||||||
|
...opt,
|
||||||
|
disabled: disabledSet.has(opt.key)
|
||||||
|
})).filter((opt) => true);
|
||||||
},
|
},
|
||||||
titleRibbonVisible() {
|
titleRibbonVisible() {
|
||||||
return !this.isMobileViewport && this.chatDisplayMode === 'chat';
|
return !this.isMobileViewport && this.chatDisplayMode === 'chat';
|
||||||
@ -466,7 +480,7 @@ const appOptions = {
|
|||||||
})
|
})
|
||||||
,
|
,
|
||||||
displayModeSwitchDisabled() {
|
displayModeSwitchDisabled() {
|
||||||
return false;
|
return !!this.policyUiBlocks.block_virtual_monitor;
|
||||||
},
|
},
|
||||||
displayLockEngaged() {
|
displayLockEngaged() {
|
||||||
return false;
|
return false;
|
||||||
@ -652,6 +666,28 @@ const appOptions = {
|
|||||||
}
|
}
|
||||||
this.uiCloseMobileOverlay();
|
this.uiCloseMobileOverlay();
|
||||||
},
|
},
|
||||||
|
applyPolicyUiLocks() {
|
||||||
|
const policyStore = usePolicyStore();
|
||||||
|
const blocks = policyStore.uiBlocks;
|
||||||
|
if (blocks.collapse_workspace) {
|
||||||
|
this.uiSetWorkspaceCollapsed(true);
|
||||||
|
}
|
||||||
|
if (blocks.block_virtual_monitor && this.chatDisplayMode === 'monitor') {
|
||||||
|
this.uiSetChatDisplayMode('chat');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isPolicyBlocked(key: string, message?: string) {
|
||||||
|
const policyStore = usePolicyStore();
|
||||||
|
if (policyStore.uiBlocks[key]) {
|
||||||
|
this.uiPushToast({
|
||||||
|
title: '已被管理员禁用',
|
||||||
|
message: message || '被管理员强制禁用',
|
||||||
|
type: 'warning'
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
handleClickOutsideMobileMenu(event) {
|
handleClickOutsideMobileMenu(event) {
|
||||||
if (!this.isMobileViewport || !this.mobileOverlayMenuOpen) {
|
if (!this.isMobileViewport || !this.mobileOverlayMenuOpen) {
|
||||||
return;
|
return;
|
||||||
@ -666,6 +702,10 @@ const appOptions = {
|
|||||||
if (this.isMobileViewport) {
|
if (this.isMobileViewport) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (this.isPolicyBlocked('collapse_workspace', '工作区已被管理员强制折叠')) {
|
||||||
|
this.uiSetWorkspaceCollapsed(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const nextState = !this.workspaceCollapsed;
|
const nextState = !this.workspaceCollapsed;
|
||||||
this.uiSetWorkspaceCollapsed(nextState);
|
this.uiSetWorkspaceCollapsed(nextState);
|
||||||
if (nextState) {
|
if (nextState) {
|
||||||
@ -674,6 +714,11 @@ const appOptions = {
|
|||||||
},
|
},
|
||||||
handleDisplayModeToggle() {
|
handleDisplayModeToggle() {
|
||||||
if (this.displayModeSwitchDisabled) {
|
if (this.displayModeSwitchDisabled) {
|
||||||
|
// 显式提示管理员禁用
|
||||||
|
this.isPolicyBlocked('block_virtual_monitor', '虚拟显示器已被管理员禁用');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.chatDisplayMode === 'chat' && this.isPolicyBlocked('block_virtual_monitor', '虚拟显示器已被管理员禁用')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const next = this.chatDisplayMode === 'chat' ? 'monitor' : 'chat';
|
const next = this.chatDisplayMode === 'chat' ? 'monitor' : 'chat';
|
||||||
@ -912,6 +957,9 @@ const appOptions = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
openGuiFileManager() {
|
openGuiFileManager() {
|
||||||
|
if (this.isPolicyBlocked('block_file_manager', '文件管理器已被管理员禁用')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
window.open('/file-manager', '_blank');
|
window.open('/file-manager', '_blank');
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -1393,6 +1441,10 @@ const appOptions = {
|
|||||||
this.applyStatusSnapshot(statusData);
|
this.applyStatusSnapshot(statusData);
|
||||||
// 立即更新配额和运行模式,避免等待其他慢接口
|
// 立即更新配额和运行模式,避免等待其他慢接口
|
||||||
this.fetchUsageQuota();
|
this.fetchUsageQuota();
|
||||||
|
// 拉取管理员策略
|
||||||
|
const policyStore = usePolicyStore();
|
||||||
|
await policyStore.fetchPolicy();
|
||||||
|
this.applyPolicyUiLocks();
|
||||||
|
|
||||||
// 获取当前对话信息
|
// 获取当前对话信息
|
||||||
const statusConversationId = statusData.conversation && statusData.conversation.current_id;
|
const statusConversationId = statusData.conversation && statusData.conversation.current_id;
|
||||||
@ -2270,6 +2322,9 @@ const appOptions = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
openPersonalPage() {
|
openPersonalPage() {
|
||||||
|
if (this.isPolicyBlocked('block_personal_space', '个人空间已被管理员禁用')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.personalizationOpenDrawer();
|
this.personalizationOpenDrawer();
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -2303,6 +2358,15 @@ const appOptions = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
handleFileSelected(files) {
|
handleFileSelected(files) {
|
||||||
|
const policyStore = usePolicyStore();
|
||||||
|
if (policyStore.uiBlocks?.block_upload) {
|
||||||
|
this.uiPushToast({
|
||||||
|
title: '上传被禁用',
|
||||||
|
message: '已被管理员禁用上传功能',
|
||||||
|
type: 'warning'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.uploadHandleSelected(files);
|
this.uploadHandleSelected(files);
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -2528,6 +2592,9 @@ const appOptions = {
|
|||||||
if (!this.isConnected) {
|
if (!this.isConnected) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (this.isPolicyBlocked('block_tool_toggle', '工具启用/禁用已被管理员锁定')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.modeMenuOpen = false;
|
this.modeMenuOpen = false;
|
||||||
this.modelMenuOpen = false;
|
this.modelMenuOpen = false;
|
||||||
const nextState = this.inputToggleToolMenu();
|
const nextState = this.inputToggleToolMenu();
|
||||||
@ -2666,6 +2733,9 @@ const appOptions = {
|
|||||||
if (this.uploading || !this.isConnected) {
|
if (this.uploading || !this.isConnected) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (this.isPolicyBlocked('block_upload', '上传功能已被管理员禁用')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.triggerFileUpload();
|
this.triggerFileUpload();
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -2714,6 +2784,15 @@ const appOptions = {
|
|||||||
if (!this.isConnected || this.streamingMessage) {
|
if (!this.isConnected || this.streamingMessage) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const policyStore = usePolicyStore();
|
||||||
|
if (policyStore.isModelDisabled(key)) {
|
||||||
|
this.uiPushToast({
|
||||||
|
title: '模型被禁用',
|
||||||
|
message: '被管理员强制禁用',
|
||||||
|
type: 'warning'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (this.conversationHasImages && key !== 'qwen3-vl-plus') {
|
if (this.conversationHasImages && key !== 'qwen3-vl-plus') {
|
||||||
this.uiPushToast({
|
this.uiPushToast({
|
||||||
title: '切换失败',
|
title: '切换失败',
|
||||||
@ -2869,6 +2948,9 @@ const appOptions = {
|
|||||||
if (!this.isConnected) {
|
if (!this.isConnected) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (this.isPolicyBlocked('block_realtime_terminal', '实时终端已被管理员禁用')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.openRealtimeTerminal();
|
this.openRealtimeTerminal();
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -2876,6 +2958,9 @@ const appOptions = {
|
|||||||
if (!this.isConnected) {
|
if (!this.isConnected) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (this.isPolicyBlocked('block_focus_panel', '聚焦面板已被管理员禁用')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.toggleFocusPanel();
|
this.toggleFocusPanel();
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -2883,6 +2968,9 @@ const appOptions = {
|
|||||||
if (!this.currentConversationId) {
|
if (!this.currentConversationId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (this.isPolicyBlocked('block_token_panel', '用量统计已被管理员禁用')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.toggleTokenPanel();
|
this.toggleTokenPanel();
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -2890,10 +2978,16 @@ const appOptions = {
|
|||||||
if (this.compressing || this.streamingMessage || !this.isConnected) {
|
if (this.compressing || this.streamingMessage || !this.isConnected) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (this.isPolicyBlocked('block_compress_conversation', '压缩对话已被管理员禁用')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.compressConversation();
|
this.compressConversation();
|
||||||
},
|
},
|
||||||
|
|
||||||
openReviewDialog() {
|
openReviewDialog() {
|
||||||
|
if (this.isPolicyBlocked('block_conversation_review', '对话引用已被管理员禁用')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!this.isConnected) {
|
if (!this.isConnected) {
|
||||||
this.uiPushToast({
|
this.uiPushToast({
|
||||||
title: '无法使用',
|
title: '无法使用',
|
||||||
@ -3165,7 +3259,9 @@ const appOptions = {
|
|||||||
id: item.id,
|
id: item.id,
|
||||||
label: item.label || item.id,
|
label: item.label || item.id,
|
||||||
enabled: !!item.enabled,
|
enabled: !!item.enabled,
|
||||||
tools: Array.isArray(item.tools) ? item.tools : []
|
tools: Array.isArray(item.tools) ? item.tools : [],
|
||||||
|
locked: !!item.locked,
|
||||||
|
locked_state: typeof item.locked_state === 'boolean' ? item.locked_state : null
|
||||||
}));
|
}));
|
||||||
debugLog('[ToolSettings] Snapshot applied', {
|
debugLog('[ToolSettings] Snapshot applied', {
|
||||||
received: categories.length,
|
received: categories.length,
|
||||||
@ -3215,6 +3311,15 @@ const appOptions = {
|
|||||||
if (this.toolSettingsLoading) {
|
if (this.toolSettingsLoading) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const policyStore = usePolicyStore();
|
||||||
|
if (policyStore.isCategoryLocked(categoryId)) {
|
||||||
|
this.uiPushToast({
|
||||||
|
title: '无法修改',
|
||||||
|
message: '该工具类别被管理员强制设置',
|
||||||
|
type: 'warning'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
const previousSnapshot = this.toolSettings.map((item) => ({ ...item }));
|
const previousSnapshot = this.toolSettings.map((item) => ({ ...item }));
|
||||||
const updatedSettings = this.toolSettings.map((item) => {
|
const updatedSettings = this.toolSettings.map((item) => {
|
||||||
if (item.id === categoryId) {
|
if (item.id === categoryId) {
|
||||||
@ -3239,6 +3344,13 @@ const appOptions = {
|
|||||||
this.applyToolSettingsSnapshot(data.categories);
|
this.applyToolSettingsSnapshot(data.categories);
|
||||||
} else {
|
} else {
|
||||||
console.warn('更新工具设置失败:', data);
|
console.warn('更新工具设置失败:', data);
|
||||||
|
if (data && (data.message || data.error)) {
|
||||||
|
this.uiPushToast({
|
||||||
|
title: '无法切换工具',
|
||||||
|
message: data.message || data.error,
|
||||||
|
type: 'warning'
|
||||||
|
});
|
||||||
|
}
|
||||||
this.toolSetSettings(previousSnapshot);
|
this.toolSetSettings(previousSnapshot);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -74,6 +74,13 @@
|
|||||||
:current-conversation-id="currentConversationId"
|
:current-conversation-id="currentConversationId"
|
||||||
:icon-style="iconStyle"
|
:icon-style="iconStyle"
|
||||||
:tool-category-icon="toolCategoryIcon"
|
:tool-category-icon="toolCategoryIcon"
|
||||||
|
:block-upload="blockUpload"
|
||||||
|
:block-tool-toggle="blockToolToggle"
|
||||||
|
:block-realtime-terminal="blockRealtimeTerminal"
|
||||||
|
:block-focus-panel="blockFocusPanel"
|
||||||
|
:block-token-panel="blockTokenPanel"
|
||||||
|
:block-compress-conversation="blockCompressConversation"
|
||||||
|
:block-conversation-review="blockConversationReview"
|
||||||
@quick-upload="triggerQuickUpload"
|
@quick-upload="triggerQuickUpload"
|
||||||
@pick-images="$emit('pick-images')"
|
@pick-images="$emit('pick-images')"
|
||||||
@toggle-tool-menu="$emit('toggle-tool-menu')"
|
@toggle-tool-menu="$emit('toggle-tool-menu')"
|
||||||
@ -147,9 +154,16 @@ const props = defineProps<{
|
|||||||
currentConversationId: string | null;
|
currentConversationId: string | null;
|
||||||
iconStyle: (key: string) => Record<string, string>;
|
iconStyle: (key: string) => Record<string, string>;
|
||||||
toolCategoryIcon: (categoryId: string) => string;
|
toolCategoryIcon: (categoryId: string) => string;
|
||||||
modelOptions: Array<{ key: string; label: string; description: string }>;
|
modelOptions: Array<{ key: string; label: string; description: string; disabled?: boolean }>;
|
||||||
currentModelKey: string;
|
currentModelKey: string;
|
||||||
selectedImages?: string[];
|
selectedImages?: string[];
|
||||||
|
blockUpload?: boolean;
|
||||||
|
blockToolToggle?: boolean;
|
||||||
|
blockRealtimeTerminal?: boolean;
|
||||||
|
blockFocusPanel?: boolean;
|
||||||
|
blockTokenPanel?: boolean;
|
||||||
|
blockCompressConversation?: boolean;
|
||||||
|
blockConversationReview?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const inputStore = useInputStore();
|
const inputStore = useInputStore();
|
||||||
|
|||||||
@ -1,7 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<transition name="quick-menu">
|
<transition name="quick-menu">
|
||||||
<div v-if="open" class="quick-menu" @click.stop>
|
<div v-if="open" class="quick-menu" @click.stop>
|
||||||
<button type="button" class="menu-entry" @click="$emit('quick-upload')" :disabled="!isConnected || uploading">
|
<button
|
||||||
|
type="button"
|
||||||
|
class="menu-entry"
|
||||||
|
@click="$emit('quick-upload')"
|
||||||
|
:disabled="!isConnected || uploading"
|
||||||
|
>
|
||||||
{{ uploading ? '上传中...' : '上传文件' }}
|
{{ uploading ? '上传中...' : '上传文件' }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@ -108,7 +113,7 @@
|
|||||||
:key="category.id"
|
:key="category.id"
|
||||||
type="button"
|
type="button"
|
||||||
class="menu-entry submenu-entry"
|
class="menu-entry submenu-entry"
|
||||||
:class="{ disabled: !category.enabled }"
|
:class="{ disabled: !category.enabled || category.locked }"
|
||||||
@click.stop="$emit('update-tool-category', category.id, !category.enabled)"
|
@click.stop="$emit('update-tool-category', category.id, !category.enabled)"
|
||||||
:disabled="streamingMessage || !isConnected || toolSettingsLoading"
|
:disabled="streamingMessage || !isConnected || toolSettingsLoading"
|
||||||
>
|
>
|
||||||
@ -116,7 +121,14 @@
|
|||||||
<span class="icon icon-sm" :style="getIconStyle(toolCategoryIcon(category.id))" aria-hidden="true"></span>
|
<span class="icon icon-sm" :style="getIconStyle(toolCategoryIcon(category.id))" aria-hidden="true"></span>
|
||||||
<span>{{ category.label }}</span>
|
<span>{{ category.label }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="entry-arrow">{{ category.enabled ? '禁用' : '启用' }}</span>
|
<span class="entry-arrow">
|
||||||
|
<template v-if="category.locked">
|
||||||
|
被管理员锁定
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ category.enabled ? '禁用' : '启用' }}
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -186,8 +198,15 @@ const props = defineProps<{
|
|||||||
modeMenuOpen: boolean;
|
modeMenuOpen: boolean;
|
||||||
runMode?: 'fast' | 'thinking' | 'deep';
|
runMode?: 'fast' | 'thinking' | 'deep';
|
||||||
modelMenuOpen: boolean;
|
modelMenuOpen: boolean;
|
||||||
modelOptions: Array<{ key: string; label: string; description: string }>;
|
modelOptions: Array<{ key: string; label: string; description: string; disabled?: boolean }>;
|
||||||
currentModelKey: string;
|
currentModelKey: string;
|
||||||
|
blockUpload?: boolean;
|
||||||
|
blockToolToggle?: boolean;
|
||||||
|
blockRealtimeTerminal?: boolean;
|
||||||
|
blockFocusPanel?: boolean;
|
||||||
|
blockTokenPanel?: boolean;
|
||||||
|
blockCompressConversation?: boolean;
|
||||||
|
blockConversationReview?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
|
|||||||
@ -68,7 +68,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
<button class="sidebar-manage-btn" @click="$emit('open-file-manager')" title="打开桌面式文件管理器">管理</button>
|
<button
|
||||||
|
class="sidebar-manage-btn"
|
||||||
|
@click="$emit('open-file-manager')"
|
||||||
|
:title="fileManagerDisabled ? '已被管理员禁用' : '打开桌面式文件管理器'"
|
||||||
|
>
|
||||||
|
管理
|
||||||
|
</button>
|
||||||
<h3>
|
<h3>
|
||||||
<span v-if="panelMode === 'files'" class="icon-label">
|
<span v-if="panelMode === 'files'" class="icon-label">
|
||||||
<span class="icon icon-sm" :style="iconStyle('folder')" aria-hidden="true"></span>
|
<span class="icon icon-sm" :style="iconStyle('folder')" aria-hidden="true"></span>
|
||||||
@ -146,6 +152,7 @@ const props = defineProps<{
|
|||||||
panelMenuOpen: boolean;
|
panelMenuOpen: boolean;
|
||||||
panelMode: 'files' | 'todo' | 'subAgents';
|
panelMode: 'files' | 'todo' | 'subAgents';
|
||||||
runMode: 'fast' | 'thinking' | 'deep';
|
runMode: 'fast' | 'thinking' | 'deep';
|
||||||
|
fileManagerDisabled?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
|
|||||||
@ -191,17 +191,19 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="run-mode-options model-options">
|
<div class="run-mode-options model-options">
|
||||||
<button
|
<button
|
||||||
v-for="option in modelOptions"
|
v-for="option in filteredModelOptions"
|
||||||
:key="option.id"
|
:key="option.id"
|
||||||
type="button"
|
type="button"
|
||||||
class="run-mode-card"
|
class="run-mode-card"
|
||||||
:class="{ active: form.default_model === option.value }"
|
:class="[{ active: form.default_model === option.value }, { disabled: option.disabled }]"
|
||||||
:aria-pressed="form.default_model === option.value"
|
:aria-pressed="form.default_model === option.value"
|
||||||
@click.prevent="setDefaultModel(option.value)"
|
:disabled="option.disabled"
|
||||||
|
@click.prevent="!option.disabled && setDefaultModel(option.value)"
|
||||||
>
|
>
|
||||||
<div class="run-mode-card-header">
|
<div class="run-mode-card-header">
|
||||||
<span class="run-mode-title">{{ option.label }}</span>
|
<span class="run-mode-title">{{ option.label }}</span>
|
||||||
<span v-if="option.badge" class="run-mode-badge">{{ option.badge }}</span>
|
<span v-if="option.badge" class="run-mode-badge">{{ option.badge }}</span>
|
||||||
|
<span v-if="option.disabled" class="run-mode-badge danger">已禁用</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="run-mode-desc">{{ option.desc }}</p>
|
<p class="run-mode-desc">{{ option.desc }}</p>
|
||||||
</button>
|
</button>
|
||||||
@ -521,6 +523,7 @@ import { storeToRefs } from 'pinia';
|
|||||||
import { usePersonalizationStore } from '@/stores/personalization';
|
import { usePersonalizationStore } from '@/stores/personalization';
|
||||||
import { useResourceStore } from '@/stores/resource';
|
import { useResourceStore } from '@/stores/resource';
|
||||||
import { useUiStore } from '@/stores/ui';
|
import { useUiStore } from '@/stores/ui';
|
||||||
|
import { usePolicyStore } from '@/stores/policy';
|
||||||
import { useTheme } from '@/utils/theme';
|
import { useTheme } from '@/utils/theme';
|
||||||
import type { ThemeKey } from '@/utils/theme';
|
import type { ThemeKey } from '@/utils/theme';
|
||||||
|
|
||||||
@ -577,6 +580,8 @@ const runModeOptions: Array<{ id: string; label: string; desc: string; value: Ru
|
|||||||
{ id: 'deep', label: '深度思考', desc: '整轮对话都使用思考模型', value: 'deep' }
|
{ id: 'deep', label: '深度思考', desc: '整轮对话都使用思考模型', value: 'deep' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const policyStore = usePolicyStore();
|
||||||
|
|
||||||
const modelOptions = [
|
const modelOptions = [
|
||||||
{ id: 'deepseek', label: 'DeepSeek', desc: '通用 + 思考强化', value: 'deepseek' },
|
{ id: 'deepseek', label: 'DeepSeek', desc: '通用 + 思考强化', value: 'deepseek' },
|
||||||
{ id: 'kimi', label: 'Kimi', desc: '默认模型,兼顾通用对话', value: 'kimi' },
|
{ id: 'kimi', label: 'Kimi', desc: '默认模型,兼顾通用对话', value: 'kimi' },
|
||||||
@ -584,6 +589,13 @@ const modelOptions = [
|
|||||||
{ id: 'qwen3-vl-plus', label: 'Qwen-VL', desc: '图文多模态,思考/快速均可', value: 'qwen3-vl-plus', badge: '图文' }
|
{ id: 'qwen3-vl-plus', label: 'Qwen-VL', desc: '图文多模态,思考/快速均可', value: 'qwen3-vl-plus', badge: '图文' }
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
const filteredModelOptions = computed(() =>
|
||||||
|
modelOptions.map((opt) => ({
|
||||||
|
...opt,
|
||||||
|
disabled: policyStore.disabledModelSet.has(opt.value)
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
const thinkingPresets = [
|
const thinkingPresets = [
|
||||||
{ id: 'low', label: '低', value: 10 },
|
{ id: 'low', label: '低', value: 10 },
|
||||||
{ id: 'medium', label: '中', value: 5 },
|
{ id: 'medium', label: '中', value: 5 },
|
||||||
@ -645,6 +657,14 @@ const setDefaultRunMode = (value: RunModeValue) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const setDefaultModel = (value: string) => {
|
const setDefaultModel = (value: string) => {
|
||||||
|
if (policyStore.disabledModelSet.has(value)) {
|
||||||
|
uiStore.pushToast({
|
||||||
|
title: '模型被禁用',
|
||||||
|
message: '已被管理员禁用,无法选择',
|
||||||
|
type: 'warning'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (checkModeModelConflict(form.value.default_run_mode, value)) {
|
if (checkModeModelConflict(form.value.default_run_mode, value)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -67,9 +67,9 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="collapsed-control-btn monitor-mode-btn"
|
class="collapsed-control-btn monitor-mode-btn"
|
||||||
:class="{ active: displayMode === 'monitor' }"
|
:class="{ active: displayMode === 'monitor', blocked: displayModeDisabled }"
|
||||||
:disabled="displayModeDisabled"
|
|
||||||
@click="$emit('toggle-display-mode')"
|
@click="$emit('toggle-display-mode')"
|
||||||
|
:aria-disabled="displayModeDisabled"
|
||||||
:title="displayMode === 'monitor' ? '退出显示器' : '虚拟显示器'"
|
:title="displayMode === 'monitor' ? '退出显示器' : '虚拟显示器'"
|
||||||
>
|
>
|
||||||
<span class="sr-only">{{ displayMode === 'monitor' ? '退出显示器' : '虚拟显示器' }}</span>
|
<span class="sr-only">{{ displayMode === 'monitor' ? '退出显示器' : '虚拟显示器' }}</span>
|
||||||
|
|||||||
78
static/src/stores/policy.ts
Normal file
78
static/src/stores/policy.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { defineStore } from 'pinia';
|
||||||
|
|
||||||
|
export interface EffectivePolicy {
|
||||||
|
categories: Record<string, { label: string; tools: string[]; default_enabled?: boolean; locked?: boolean; locked_state?: boolean | null }>;
|
||||||
|
forced_category_states: Record<string, boolean | null>;
|
||||||
|
disabled_models: string[];
|
||||||
|
ui_blocks: Record<string, boolean>;
|
||||||
|
updated_at?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PolicyState {
|
||||||
|
loaded: boolean;
|
||||||
|
loading: boolean;
|
||||||
|
error: string;
|
||||||
|
policy: EffectivePolicy | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_POLICY: EffectivePolicy = {
|
||||||
|
categories: {},
|
||||||
|
forced_category_states: {},
|
||||||
|
disabled_models: [],
|
||||||
|
ui_blocks: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const usePolicyStore = defineStore('policy', {
|
||||||
|
state: (): PolicyState => ({
|
||||||
|
loaded: false,
|
||||||
|
loading: false,
|
||||||
|
error: '',
|
||||||
|
policy: null
|
||||||
|
}),
|
||||||
|
getters: {
|
||||||
|
disabledModelSet(state): Set<string> {
|
||||||
|
return new Set(state.policy?.disabled_models || []);
|
||||||
|
},
|
||||||
|
forcedCategoryStates(state): Record<string, boolean | null> {
|
||||||
|
return state.policy?.forced_category_states || {};
|
||||||
|
},
|
||||||
|
uiBlocks(state): Record<string, boolean> {
|
||||||
|
return state.policy?.ui_blocks || {};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
async fetchPolicy() {
|
||||||
|
if (this.loading) return;
|
||||||
|
this.loading = true;
|
||||||
|
this.error = '';
|
||||||
|
try {
|
||||||
|
const resp = await fetch('/api/effective-policy');
|
||||||
|
const result = await resp.json();
|
||||||
|
if (!resp.ok || !result.success) {
|
||||||
|
throw new Error(result.error || '加载策略失败');
|
||||||
|
}
|
||||||
|
this.policy = result.data || DEFAULT_POLICY;
|
||||||
|
this.loaded = true;
|
||||||
|
} catch (error: any) {
|
||||||
|
this.error = error?.message || '加载策略失败';
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isModelDisabled(key: string): boolean {
|
||||||
|
return this.disabledModelSet.has(key);
|
||||||
|
},
|
||||||
|
isCategoryLocked(id: string): boolean {
|
||||||
|
const states = this.forcedCategoryStates;
|
||||||
|
return typeof states[id] === 'boolean';
|
||||||
|
},
|
||||||
|
categoryLockedState(id: string): boolean | null {
|
||||||
|
const states = this.forcedCategoryStates;
|
||||||
|
if (typeof states[id] === 'boolean') return states[id] as boolean;
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
isBlocked(action: string): boolean {
|
||||||
|
return !!this.uiBlocks[action];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -4,6 +4,9 @@ export interface ToolCategory {
|
|||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
locked?: boolean;
|
||||||
|
locked_state?: boolean | null;
|
||||||
|
tools?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ToolState {
|
interface ToolState {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { useUiStore } from './ui';
|
import { useUiStore } from './ui';
|
||||||
import { useFileStore } from './file';
|
import { useFileStore } from './file';
|
||||||
|
import { usePolicyStore } from './policy';
|
||||||
|
|
||||||
interface UploadState {
|
interface UploadState {
|
||||||
uploading: boolean;
|
uploading: boolean;
|
||||||
@ -29,6 +30,16 @@ export const useUploadStore = defineStore('upload', {
|
|||||||
if (!file || this.uploading) {
|
if (!file || this.uploading) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const policyStore = usePolicyStore();
|
||||||
|
if (policyStore.uiBlocks?.block_upload) {
|
||||||
|
const uiStore = useUiStore();
|
||||||
|
uiStore.pushToast({
|
||||||
|
title: '上传被禁用',
|
||||||
|
message: '已被管理员禁用上传功能',
|
||||||
|
type: 'warning'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
const uiStore = useUiStore();
|
const uiStore = useUiStore();
|
||||||
const fileStore = useFileStore();
|
const fileStore = useFileStore();
|
||||||
this.setUploading(true);
|
this.setUploading(true);
|
||||||
|
|||||||
@ -155,6 +155,17 @@
|
|||||||
color: var(--claude-accent);
|
color: var(--claude-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.monitor-mode-btn.blocked {
|
||||||
|
color: var(--claude-text-tertiary);
|
||||||
|
opacity: 0.55;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-mode-btn.blocked:hover {
|
||||||
|
color: var(--claude-accent);
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
.conversation-menu-btn {
|
.conversation-menu-btn {
|
||||||
color: #31271d;
|
color: #31271d;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import vue from '@vitejs/plugin-vue';
|
|||||||
|
|
||||||
const entry = fileURLToPath(new URL('./static/src/main.ts', import.meta.url));
|
const entry = fileURLToPath(new URL('./static/src/main.ts', import.meta.url));
|
||||||
const adminEntry = fileURLToPath(new URL('./static/src/admin/main.ts', import.meta.url));
|
const adminEntry = fileURLToPath(new URL('./static/src/admin/main.ts', import.meta.url));
|
||||||
|
const adminPolicyEntry = fileURLToPath(new URL('./static/src/admin/policyMain.ts', import.meta.url));
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [vue()],
|
plugins: [vue()],
|
||||||
@ -13,7 +14,8 @@ export default defineConfig({
|
|||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
input: {
|
input: {
|
||||||
main: entry,
|
main: entry,
|
||||||
admin: adminEntry
|
admin: adminEntry,
|
||||||
|
adminPolicy: adminPolicyEntry
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
entryFileNames: 'assets/[name].js',
|
entryFileNames: 'assets/[name].js',
|
||||||
|
|||||||
178
web_server.py
178
web_server.py
@ -21,6 +21,7 @@ import time
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from collections import defaultdict, deque, Counter
|
from collections import defaultdict, deque, Counter
|
||||||
from config.model_profiles import get_model_profile
|
from config.model_profiles import get_model_profile
|
||||||
|
from modules import admin_policy_manager
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
from werkzeug.routing import BaseConverter
|
from werkzeug.routing import BaseConverter
|
||||||
import secrets
|
import secrets
|
||||||
@ -759,6 +760,19 @@ def is_admin_user(record=None) -> bool:
|
|||||||
role = get_current_user_role(record)
|
role = get_current_user_role(record)
|
||||||
return isinstance(role, str) and role.lower() == 'admin'
|
return isinstance(role, str) and role.lower() == 'admin'
|
||||||
|
|
||||||
|
def resolve_admin_policy(record=None) -> Dict[str, Any]:
|
||||||
|
"""获取当前用户生效的管理员策略。"""
|
||||||
|
if record is None:
|
||||||
|
record = get_current_user_record()
|
||||||
|
username = record.username if record else None
|
||||||
|
role = get_current_user_role(record)
|
||||||
|
invite_code = getattr(record, "invite_code", None)
|
||||||
|
try:
|
||||||
|
return admin_policy_manager.get_effective_policy(username, role, invite_code)
|
||||||
|
except Exception as exc:
|
||||||
|
debug_log(f"[admin_policy] 加载失败: {exc}")
|
||||||
|
return admin_policy_manager.get_effective_policy(username, role, invite_code)
|
||||||
|
|
||||||
|
|
||||||
def admin_required(view_func):
|
def admin_required(view_func):
|
||||||
@wraps(view_func)
|
@wraps(view_func)
|
||||||
@ -850,6 +864,38 @@ def get_user_resources(username: Optional[str] = None) -> Tuple[Optional[WebTerm
|
|||||||
attach_user_broadcast(terminal, username)
|
attach_user_broadcast(terminal, username)
|
||||||
terminal.username = username
|
terminal.username = username
|
||||||
terminal.quota_update_callback = lambda metric=None: emit_user_quota_update(username)
|
terminal.quota_update_callback = lambda metric=None: emit_user_quota_update(username)
|
||||||
|
|
||||||
|
# 应用管理员策略(工具分类、强制开关、模型禁用)
|
||||||
|
try:
|
||||||
|
from core.tool_config import ToolCategory
|
||||||
|
|
||||||
|
policy = resolve_admin_policy(user_manager.get_user(username))
|
||||||
|
categories_map = {
|
||||||
|
cid: ToolCategory(
|
||||||
|
label=cat.get("label") or cid,
|
||||||
|
tools=list(cat.get("tools") or []),
|
||||||
|
default_enabled=bool(cat.get("default_enabled", True)),
|
||||||
|
silent_when_disabled=bool(cat.get("silent_when_disabled", False)),
|
||||||
|
)
|
||||||
|
for cid, cat in policy.get("categories", {}).items()
|
||||||
|
}
|
||||||
|
forced_states = policy.get("forced_category_states") or {}
|
||||||
|
disabled_models = policy.get("disabled_models") or []
|
||||||
|
terminal.set_admin_policy(categories_map, forced_states, disabled_models)
|
||||||
|
terminal.admin_policy_ui_blocks = policy.get("ui_blocks") or {}
|
||||||
|
terminal.admin_policy_version = policy.get("updated_at")
|
||||||
|
# 若当前模型被禁用,则回退到第一个可用模型
|
||||||
|
if terminal.model_key in disabled_models:
|
||||||
|
for candidate in ["kimi", "deepseek", "qwen3-vl-plus", "qwen3-max"]:
|
||||||
|
if candidate not in disabled_models:
|
||||||
|
try:
|
||||||
|
terminal.set_model(candidate)
|
||||||
|
session["model_key"] = terminal.model_key
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
except Exception as exc:
|
||||||
|
debug_log(f"[admin_policy] 应用失败: {exc}")
|
||||||
return terminal, workspace
|
return terminal, workspace
|
||||||
|
|
||||||
|
|
||||||
@ -1365,6 +1411,9 @@ def conversation_page(conversation_id):
|
|||||||
@login_required
|
@login_required
|
||||||
def terminal_page():
|
def terminal_page():
|
||||||
"""终端监控页面"""
|
"""终端监控页面"""
|
||||||
|
policy = resolve_admin_policy(get_current_user_record())
|
||||||
|
if policy.get("ui_blocks", {}).get("block_realtime_terminal"):
|
||||||
|
return "实时终端已被管理员禁用", 403
|
||||||
return app.send_static_file('terminal.html')
|
return app.send_static_file('terminal.html')
|
||||||
|
|
||||||
|
|
||||||
@ -1372,6 +1421,9 @@ def terminal_page():
|
|||||||
@login_required
|
@login_required
|
||||||
def gui_file_manager_page():
|
def gui_file_manager_page():
|
||||||
"""桌面式文件管理器页面"""
|
"""桌面式文件管理器页面"""
|
||||||
|
policy = resolve_admin_policy(get_current_user_record())
|
||||||
|
if policy.get("ui_blocks", {}).get("block_file_manager"):
|
||||||
|
return "文件管理器已被管理员禁用", 403
|
||||||
return send_from_directory(Path(app.static_folder) / 'file_manager', 'index.html')
|
return send_from_directory(Path(app.static_folder) / 'file_manager', 'index.html')
|
||||||
|
|
||||||
|
|
||||||
@ -1407,6 +1459,13 @@ def admin_monitor_page():
|
|||||||
"""管理员监控页面入口"""
|
"""管理员监控页面入口"""
|
||||||
return send_from_directory(str(ADMIN_ASSET_DIR), 'index.html')
|
return send_from_directory(str(ADMIN_ASSET_DIR), 'index.html')
|
||||||
|
|
||||||
|
@app.route('/admin/policy')
|
||||||
|
@login_required
|
||||||
|
@admin_required
|
||||||
|
def admin_policy_page():
|
||||||
|
"""管理员策略配置页面"""
|
||||||
|
return send_from_directory(Path(app.static_folder) / 'admin_policy', 'index.html')
|
||||||
|
|
||||||
|
|
||||||
@app.route('/admin/assets/<path:filename>')
|
@app.route('/admin/assets/<path:filename>')
|
||||||
@login_required
|
@login_required
|
||||||
@ -1513,6 +1572,16 @@ def get_status(terminal: WebTerminal, workspace: UserWorkspace, username: str):
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
status['container'] = {"success": False, "error": str(exc)}
|
status['container'] = {"success": False, "error": str(exc)}
|
||||||
status['version'] = AGENT_VERSION
|
status['version'] = AGENT_VERSION
|
||||||
|
try:
|
||||||
|
policy = resolve_admin_policy(user_manager.get_user(username))
|
||||||
|
status['admin_policy'] = {
|
||||||
|
"ui_blocks": policy.get("ui_blocks") or {},
|
||||||
|
"disabled_models": policy.get("disabled_models") or [],
|
||||||
|
"forced_category_states": policy.get("forced_category_states") or {},
|
||||||
|
"version": policy.get("updated_at"),
|
||||||
|
}
|
||||||
|
except Exception as exc:
|
||||||
|
debug_log(f"[status] 附加管理员策略失败: {exc}")
|
||||||
return jsonify(status)
|
return jsonify(status)
|
||||||
|
|
||||||
@app.route('/api/container-status')
|
@app.route('/api/container-status')
|
||||||
@ -1568,6 +1637,37 @@ def admin_dashboard_snapshot_api():
|
|||||||
logging.exception("Failed to build admin dashboard")
|
logging.exception("Failed to build admin dashboard")
|
||||||
return jsonify({"success": False, "error": str(exc)}), 500
|
return jsonify({"success": False, "error": str(exc)}), 500
|
||||||
|
|
||||||
|
@app.route('/api/admin/policy', methods=['GET', 'POST'])
|
||||||
|
@api_login_required
|
||||||
|
@admin_api_required
|
||||||
|
def admin_policy_api():
|
||||||
|
if request.method == 'GET':
|
||||||
|
try:
|
||||||
|
data = admin_policy_manager.load_policy()
|
||||||
|
defaults = admin_policy_manager.describe_defaults()
|
||||||
|
return jsonify({"success": True, "data": data, "defaults": defaults})
|
||||||
|
except Exception as exc:
|
||||||
|
return jsonify({"success": False, "error": str(exc)}), 500
|
||||||
|
# POST 更新
|
||||||
|
payload = request.get_json() or {}
|
||||||
|
target_type = payload.get("target_type")
|
||||||
|
target_value = payload.get("target_value") or ""
|
||||||
|
config = payload.get("config") or {}
|
||||||
|
try:
|
||||||
|
saved = admin_policy_manager.save_scope_policy(target_type, target_value, config)
|
||||||
|
return jsonify({"success": True, "data": saved})
|
||||||
|
except ValueError as exc:
|
||||||
|
return jsonify({"success": False, "error": str(exc)}), 400
|
||||||
|
except Exception as exc:
|
||||||
|
return jsonify({"success": False, "error": str(exc)}), 500
|
||||||
|
|
||||||
|
@app.route('/api/effective-policy', methods=['GET'])
|
||||||
|
@api_login_required
|
||||||
|
def effective_policy_api():
|
||||||
|
record = get_current_user_record()
|
||||||
|
policy = resolve_admin_policy(record)
|
||||||
|
return jsonify({"success": True, "data": policy})
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/usage', methods=['GET'])
|
@app.route('/api/usage', methods=['GET'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
@ -1652,6 +1752,16 @@ def update_model(terminal: WebTerminal, workspace: UserWorkspace, username: str)
|
|||||||
if not model_key:
|
if not model_key:
|
||||||
return jsonify({"success": False, "error": "缺少 model_key"}), 400
|
return jsonify({"success": False, "error": "缺少 model_key"}), 400
|
||||||
|
|
||||||
|
# 管理员禁用模型校验
|
||||||
|
policy = resolve_admin_policy(get_current_user_record())
|
||||||
|
disabled_models = set(policy.get("disabled_models") or [])
|
||||||
|
if model_key in disabled_models:
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"error": "该模型已被管理员禁用",
|
||||||
|
"message": "被管理员强制禁用"
|
||||||
|
}), 403
|
||||||
|
|
||||||
terminal.set_model(model_key)
|
terminal.set_model(model_key)
|
||||||
# fast-only 时 run_mode 可能被强制为 fast
|
# fast-only 时 run_mode 可能被强制为 fast
|
||||||
session["model_key"] = terminal.model_key
|
session["model_key"] = terminal.model_key
|
||||||
@ -1698,6 +1808,9 @@ def update_model(terminal: WebTerminal, workspace: UserWorkspace, username: str)
|
|||||||
def get_personalization_settings(terminal: WebTerminal, workspace: UserWorkspace, username: str):
|
def get_personalization_settings(terminal: WebTerminal, workspace: UserWorkspace, username: str):
|
||||||
"""获取个性化配置"""
|
"""获取个性化配置"""
|
||||||
try:
|
try:
|
||||||
|
policy = resolve_admin_policy(get_current_user_record())
|
||||||
|
if policy.get("ui_blocks", {}).get("block_personal_space"):
|
||||||
|
return jsonify({"success": False, "error": "个人空间已被管理员禁用"}), 403
|
||||||
data = load_personalization_config(workspace.data_dir)
|
data = load_personalization_config(workspace.data_dir)
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"success": True,
|
"success": True,
|
||||||
@ -1721,6 +1834,9 @@ def update_personalization_settings(terminal: WebTerminal, workspace: UserWorksp
|
|||||||
"""更新个性化配置"""
|
"""更新个性化配置"""
|
||||||
payload = request.get_json() or {}
|
payload = request.get_json() or {}
|
||||||
try:
|
try:
|
||||||
|
policy = resolve_admin_policy(get_current_user_record())
|
||||||
|
if policy.get("ui_blocks", {}).get("block_personal_space"):
|
||||||
|
return jsonify({"success": False, "error": "个人空间已被管理员禁用"}), 403
|
||||||
config = save_personalization_config(workspace.data_dir, payload)
|
config = save_personalization_config(workspace.data_dir, payload)
|
||||||
try:
|
try:
|
||||||
terminal.apply_personalization_preferences(config)
|
terminal.apply_personalization_preferences(config)
|
||||||
@ -1766,6 +1882,9 @@ def update_personalization_settings(terminal: WebTerminal, workspace: UserWorksp
|
|||||||
@with_terminal
|
@with_terminal
|
||||||
def get_files(terminal: WebTerminal, workspace: UserWorkspace, username: str):
|
def get_files(terminal: WebTerminal, workspace: UserWorkspace, username: str):
|
||||||
"""获取文件树"""
|
"""获取文件树"""
|
||||||
|
policy = resolve_admin_policy(get_current_user_record())
|
||||||
|
if policy.get("ui_blocks", {}).get("collapse_workspace") or policy.get("ui_blocks", {}).get("block_file_manager"):
|
||||||
|
return jsonify({"success": False, "error": "文件浏览已被管理员禁用"}), 403
|
||||||
structure = terminal.context_manager.get_project_structure()
|
structure = terminal.context_manager.get_project_structure()
|
||||||
return jsonify(structure)
|
return jsonify(structure)
|
||||||
|
|
||||||
@ -1792,6 +1911,9 @@ def _format_entry(entry) -> Dict[str, Any]:
|
|||||||
@with_terminal
|
@with_terminal
|
||||||
def gui_list_entries(terminal: WebTerminal, workspace: UserWorkspace, username: str):
|
def gui_list_entries(terminal: WebTerminal, workspace: UserWorkspace, username: str):
|
||||||
"""列出指定目录内容"""
|
"""列出指定目录内容"""
|
||||||
|
policy = resolve_admin_policy(get_current_user_record())
|
||||||
|
if policy.get("ui_blocks", {}).get("block_file_manager"):
|
||||||
|
return jsonify({"success": False, "error": "文件管理已被管理员禁用"}), 403
|
||||||
relative_path = request.args.get('path') or ""
|
relative_path = request.args.get('path') or ""
|
||||||
manager = get_gui_manager(workspace)
|
manager = get_gui_manager(workspace)
|
||||||
try:
|
try:
|
||||||
@ -1821,6 +1943,9 @@ def gui_create_entry(terminal: WebTerminal, workspace: UserWorkspace, username:
|
|||||||
parent = payload.get('path') or ""
|
parent = payload.get('path') or ""
|
||||||
name = payload.get('name') or ""
|
name = payload.get('name') or ""
|
||||||
entry_type = payload.get('type') or "file"
|
entry_type = payload.get('type') or "file"
|
||||||
|
policy = resolve_admin_policy(get_current_user_record())
|
||||||
|
if policy.get("ui_blocks", {}).get("block_file_manager"):
|
||||||
|
return jsonify({"success": False, "error": "文件管理已被管理员禁用"}), 403
|
||||||
manager = get_gui_manager(workspace)
|
manager = get_gui_manager(workspace)
|
||||||
try:
|
try:
|
||||||
new_path = manager.create_entry(parent, name, entry_type)
|
new_path = manager.create_entry(parent, name, entry_type)
|
||||||
@ -1842,6 +1967,9 @@ def gui_create_entry(terminal: WebTerminal, workspace: UserWorkspace, username:
|
|||||||
def gui_delete_entries(terminal: WebTerminal, workspace: UserWorkspace, username: str):
|
def gui_delete_entries(terminal: WebTerminal, workspace: UserWorkspace, username: str):
|
||||||
payload = request.get_json() or {}
|
payload = request.get_json() or {}
|
||||||
paths = payload.get('paths') or []
|
paths = payload.get('paths') or []
|
||||||
|
policy = resolve_admin_policy(get_current_user_record())
|
||||||
|
if policy.get("ui_blocks", {}).get("block_file_manager"):
|
||||||
|
return jsonify({"success": False, "error": "文件管理已被管理员禁用"}), 403
|
||||||
manager = get_gui_manager(workspace)
|
manager = get_gui_manager(workspace)
|
||||||
try:
|
try:
|
||||||
result = manager.delete_entries(paths)
|
result = manager.delete_entries(paths)
|
||||||
@ -1866,6 +1994,9 @@ def gui_rename_entry(terminal: WebTerminal, workspace: UserWorkspace, username:
|
|||||||
new_name = payload.get('new_name')
|
new_name = payload.get('new_name')
|
||||||
if not path or not new_name:
|
if not path or not new_name:
|
||||||
return jsonify({"success": False, "error": "缺少 path 或 new_name"}), 400
|
return jsonify({"success": False, "error": "缺少 path 或 new_name"}), 400
|
||||||
|
policy = resolve_admin_policy(get_current_user_record())
|
||||||
|
if policy.get("ui_blocks", {}).get("block_file_manager"):
|
||||||
|
return jsonify({"success": False, "error": "文件管理已被管理员禁用"}), 403
|
||||||
manager = get_gui_manager(workspace)
|
manager = get_gui_manager(workspace)
|
||||||
try:
|
try:
|
||||||
new_path = manager.rename_entry(path, new_name)
|
new_path = manager.rename_entry(path, new_name)
|
||||||
@ -1888,6 +2019,9 @@ def gui_copy_entries(terminal: WebTerminal, workspace: UserWorkspace, username:
|
|||||||
payload = request.get_json() or {}
|
payload = request.get_json() or {}
|
||||||
paths = payload.get('paths') or []
|
paths = payload.get('paths') or []
|
||||||
target_dir = payload.get('target_dir') or ""
|
target_dir = payload.get('target_dir') or ""
|
||||||
|
policy = resolve_admin_policy(get_current_user_record())
|
||||||
|
if policy.get("ui_blocks", {}).get("block_file_manager"):
|
||||||
|
return jsonify({"success": False, "error": "文件管理已被管理员禁用"}), 403
|
||||||
manager = get_gui_manager(workspace)
|
manager = get_gui_manager(workspace)
|
||||||
try:
|
try:
|
||||||
result = manager.copy_entries(paths, target_dir)
|
result = manager.copy_entries(paths, target_dir)
|
||||||
@ -1929,6 +2063,9 @@ def gui_move_entries(terminal: WebTerminal, workspace: UserWorkspace, username:
|
|||||||
@with_terminal
|
@with_terminal
|
||||||
@rate_limited("gui_file_upload", 10, 300, scope="user")
|
@rate_limited("gui_file_upload", 10, 300, scope="user")
|
||||||
def gui_upload_entry(terminal: WebTerminal, workspace: UserWorkspace, username: str):
|
def gui_upload_entry(terminal: WebTerminal, workspace: UserWorkspace, username: str):
|
||||||
|
policy = resolve_admin_policy(get_current_user_record())
|
||||||
|
if policy.get("ui_blocks", {}).get("block_upload"):
|
||||||
|
return jsonify({"success": False, "error": "文件上传已被管理员禁用"}), 403
|
||||||
if 'file' not in request.files:
|
if 'file' not in request.files:
|
||||||
return jsonify({"success": False, "error": "未找到文件"}), 400
|
return jsonify({"success": False, "error": "未找到文件"}), 400
|
||||||
file_obj = request.files['file']
|
file_obj = request.files['file']
|
||||||
@ -2135,6 +2272,13 @@ def get_todo_list(terminal: WebTerminal, workspace: UserWorkspace, username: str
|
|||||||
@rate_limited("legacy_upload", 20, 300, scope="user")
|
@rate_limited("legacy_upload", 20, 300, scope="user")
|
||||||
def upload_file(terminal: WebTerminal, workspace: UserWorkspace, username: str):
|
def upload_file(terminal: WebTerminal, workspace: UserWorkspace, username: str):
|
||||||
"""处理前端文件上传请求"""
|
"""处理前端文件上传请求"""
|
||||||
|
policy = resolve_admin_policy(get_current_user_record())
|
||||||
|
if policy.get("ui_blocks", {}).get("block_upload"):
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"error": "文件上传已被管理员禁用",
|
||||||
|
"message": "被管理员禁用上传"
|
||||||
|
}), 403
|
||||||
if 'file' not in request.files:
|
if 'file' not in request.files:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"success": False,
|
"success": False,
|
||||||
@ -2351,7 +2495,21 @@ def tool_settings(terminal: WebTerminal, workspace: UserWorkspace, username: str
|
|||||||
}), 400
|
}), 400
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
policy = resolve_admin_policy(get_current_user_record())
|
||||||
|
if policy.get("ui_blocks", {}).get("block_tool_toggle"):
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"error": "工具开关已被管理员禁用",
|
||||||
|
"message": "被管理员强制禁用"
|
||||||
|
}), 403
|
||||||
enabled = bool(data['enabled'])
|
enabled = bool(data['enabled'])
|
||||||
|
forced = getattr(terminal, "admin_forced_category_states", {}) or {}
|
||||||
|
if isinstance(forced.get(category), bool) and forced[category] != enabled:
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"error": "该工具类别已被管理员强制为启用/禁用,无法修改",
|
||||||
|
"message": "被管理员强制启用/禁用"
|
||||||
|
}), 403
|
||||||
terminal.set_tool_category_enabled(category, enabled)
|
terminal.set_tool_category_enabled(category, enabled)
|
||||||
snapshot = terminal.get_tool_settings_snapshot()
|
snapshot = terminal.get_tool_settings_snapshot()
|
||||||
socketio.emit('tool_settings_updated', {
|
socketio.emit('tool_settings_updated', {
|
||||||
@ -2372,6 +2530,9 @@ def tool_settings(terminal: WebTerminal, workspace: UserWorkspace, username: str
|
|||||||
@with_terminal
|
@with_terminal
|
||||||
def get_terminals(terminal: WebTerminal, workspace: UserWorkspace, username: str):
|
def get_terminals(terminal: WebTerminal, workspace: UserWorkspace, username: str):
|
||||||
"""获取终端会话列表"""
|
"""获取终端会话列表"""
|
||||||
|
policy = resolve_admin_policy(get_current_user_record())
|
||||||
|
if policy.get("ui_blocks", {}).get("block_realtime_terminal"):
|
||||||
|
return jsonify({"success": False, "error": "实时终端已被管理员禁用"}), 403
|
||||||
if terminal.terminal_manager:
|
if terminal.terminal_manager:
|
||||||
result = terminal.terminal_manager.list_terminals()
|
result = terminal.terminal_manager.list_terminals()
|
||||||
return jsonify(result)
|
return jsonify(result)
|
||||||
@ -2510,6 +2671,10 @@ def handle_terminal_subscribe(data):
|
|||||||
if not username or not terminal or not terminal.terminal_manager:
|
if not username or not terminal or not terminal.terminal_manager:
|
||||||
emit('error', {'message': 'Terminal system not initialized'})
|
emit('error', {'message': 'Terminal system not initialized'})
|
||||||
return
|
return
|
||||||
|
policy = resolve_admin_policy(user_manager.get_user(username))
|
||||||
|
if policy.get("ui_blocks", {}).get("block_realtime_terminal"):
|
||||||
|
emit('error', {'message': '实时终端已被管理员禁用'})
|
||||||
|
return
|
||||||
|
|
||||||
if request.sid not in terminal_rooms:
|
if request.sid not in terminal_rooms:
|
||||||
terminal_rooms[request.sid] = set()
|
terminal_rooms[request.sid] = set()
|
||||||
@ -2564,6 +2729,10 @@ def handle_get_terminal_output(data):
|
|||||||
if not terminal or not terminal.terminal_manager:
|
if not terminal or not terminal.terminal_manager:
|
||||||
emit('error', {'message': 'Terminal system not initialized'})
|
emit('error', {'message': 'Terminal system not initialized'})
|
||||||
return
|
return
|
||||||
|
policy = resolve_admin_policy(user_manager.get_user(username))
|
||||||
|
if policy.get("ui_blocks", {}).get("block_realtime_terminal"):
|
||||||
|
emit('error', {'message': '实时终端已被管理员禁用'})
|
||||||
|
return
|
||||||
|
|
||||||
result = terminal.terminal_manager.get_terminal_output(session_name, lines)
|
result = terminal.terminal_manager.get_terminal_output(session_name, lines)
|
||||||
|
|
||||||
@ -2967,6 +3136,9 @@ def get_conversation_messages(conversation_id, terminal: WebTerminal, workspace:
|
|||||||
def compress_conversation(conversation_id, terminal: WebTerminal, workspace: UserWorkspace, username: str):
|
def compress_conversation(conversation_id, terminal: WebTerminal, workspace: UserWorkspace, username: str):
|
||||||
"""压缩指定对话的大体积消息,生成压缩版新对话"""
|
"""压缩指定对话的大体积消息,生成压缩版新对话"""
|
||||||
try:
|
try:
|
||||||
|
policy = resolve_admin_policy(get_current_user_record())
|
||||||
|
if policy.get("ui_blocks", {}).get("block_compress_conversation"):
|
||||||
|
return jsonify({"success": False, "error": "压缩对话已被管理员禁用"}), 403
|
||||||
normalized_id = conversation_id if conversation_id.startswith('conv_') else f"conv_{conversation_id}"
|
normalized_id = conversation_id if conversation_id.startswith('conv_') else f"conv_{conversation_id}"
|
||||||
result = terminal.context_manager.compress_conversation(normalized_id)
|
result = terminal.context_manager.compress_conversation(normalized_id)
|
||||||
|
|
||||||
@ -3081,6 +3253,9 @@ def duplicate_conversation(conversation_id, terminal: WebTerminal, workspace: Us
|
|||||||
@with_terminal
|
@with_terminal
|
||||||
def review_conversation_preview(conversation_id, terminal: WebTerminal, workspace: UserWorkspace, username: str):
|
def review_conversation_preview(conversation_id, terminal: WebTerminal, workspace: UserWorkspace, username: str):
|
||||||
"""生成对话回顾预览(不落盘,只返回前若干行文本)"""
|
"""生成对话回顾预览(不落盘,只返回前若干行文本)"""
|
||||||
|
policy = resolve_admin_policy(get_current_user_record())
|
||||||
|
if policy.get("ui_blocks", {}).get("block_conversation_review"):
|
||||||
|
return jsonify({"success": False, "error": "对话引用已被管理员禁用"}), 403
|
||||||
try:
|
try:
|
||||||
current_id = terminal.context_manager.current_conversation_id
|
current_id = terminal.context_manager.current_conversation_id
|
||||||
if conversation_id == current_id:
|
if conversation_id == current_id:
|
||||||
@ -3121,6 +3296,9 @@ def review_conversation_preview(conversation_id, terminal: WebTerminal, workspac
|
|||||||
@with_terminal
|
@with_terminal
|
||||||
def review_conversation(conversation_id, terminal: WebTerminal, workspace: UserWorkspace, username: str):
|
def review_conversation(conversation_id, terminal: WebTerminal, workspace: UserWorkspace, username: str):
|
||||||
"""生成完整对话回顾 Markdown 文件"""
|
"""生成完整对话回顾 Markdown 文件"""
|
||||||
|
policy = resolve_admin_policy(get_current_user_record())
|
||||||
|
if policy.get("ui_blocks", {}).get("block_conversation_review"):
|
||||||
|
return jsonify({"success": False, "error": "对话引用已被管理员禁用"}), 403
|
||||||
try:
|
try:
|
||||||
current_id = terminal.context_manager.current_conversation_id
|
current_id = terminal.context_manager.current_conversation_id
|
||||||
if conversation_id == current_id:
|
if conversation_id == current_id:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user