diff --git a/.DS_Store b/.DS_Store
index 2948516..10b0dbf 100644
Binary files a/.DS_Store and b/.DS_Store differ
diff --git a/config/paths.py b/config/paths.py
index 1fd835b..c19de5b 100644
--- a/config/paths.py
+++ b/config/paths.py
@@ -9,6 +9,7 @@ LOGS_DIR = "./logs"
USER_SPACE_DIR = "./users"
USERS_DB_FILE = f"{DATA_DIR}/users.json"
INVITE_CODES_FILE = f"{DATA_DIR}/invite_codes.json"
+ADMIN_POLICY_FILE = f"{DATA_DIR}/admin_policy.json"
__all__ = [
"DEFAULT_PROJECT_PATH",
@@ -18,4 +19,5 @@ __all__ = [
"USER_SPACE_DIR",
"USERS_DB_FILE",
"INVITE_CODES_FILE",
+ "ADMIN_POLICY_FILE",
]
diff --git a/core/main_terminal.py b/core/main_terminal.py
index 7ca1990..be9c384 100644
--- a/core/main_terminal.py
+++ b/core/main_terminal.py
@@ -137,13 +137,20 @@ class MainTerminal:
self.focused_files = {} # {path: content} 存储聚焦的文件内容
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
- for key, category in TOOL_CATEGORIES.items()
+ for key, category in self.tool_categories_map.items()
}
- self.disabled_tools = set()
- self.disabled_notice_tools = set()
+ self.disabled_tools: Set[str] = set()
+ self.disabled_notice_tools: Set[str] = set()
self._refresh_disabled_tools()
self.thinking_fast_interval = THINKING_FAST_INTERVAL
self.default_disabled_tool_categories: List[str] = []
@@ -358,12 +365,12 @@ class MainTerminal:
if isinstance(raw_disabled, list):
disabled_categories = [
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
# 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._refresh_disabled_tools()
@@ -539,30 +546,71 @@ class MainTerminal:
def set_tool_category_enabled(self, category: str, enabled: bool) -> None:
"""设置工具类别的启用状态 / Toggle tool category enablement."""
- if category not in TOOL_CATEGORIES:
+ categories = self.tool_categories_map
+ if category not in categories:
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._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]]:
"""获取工具类别状态快照 / Return tool category states snapshot."""
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({
"id": key,
"label": category.label,
- "enabled": self.tool_category_states.get(key, category.default_enabled),
+ "enabled": enabled,
"tools": list(category.tools),
+ "locked": isinstance(forced, bool),
+ "locked_state": forced if isinstance(forced, bool) else None,
})
return snapshot
def _refresh_disabled_tools(self) -> None:
"""刷新禁用工具列表 / Refresh disabled tool set."""
- disabled = set()
- notice = set()
- for key, enabled in self.tool_category_states.items():
- if not enabled:
- category = TOOL_CATEGORIES[key]
+ disabled: Set[str] = set()
+ notice: Set[str] = set()
+ categories = self.tool_categories_map
+ for key, category in categories.items():
+ 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)
if not getattr(category, "silent_when_disabled", False):
notice.update(category.tools)
diff --git a/modules/admin_policy_manager.py b/modules/admin_policy_manager.py
new file mode 100644
index 0000000..ead2bcc
--- /dev/null
+++ b/modules/admin_policy_manager.py
@@ -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,
+ }
diff --git a/static/admin_policy/index.html b/static/admin_policy/index.html
new file mode 100644
index 0000000..7e049b6
--- /dev/null
+++ b/static/admin_policy/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+ 管理员策略配置
+
+
+
+
+
+
+
+
diff --git a/static/src/App.vue b/static/src/App.vue
index cfbca65..c2b34ec 100644
--- a/static/src/App.vue
+++ b/static/src/App.vue
@@ -70,6 +70,7 @@
:is-connected="isConnected"
:panel-menu-open="panelMenuOpen"
:panel-mode="panelMode"
+ :file-manager-disabled="policyUiBlocks.block_file_manager"
@toggle-panel-menu="togglePanelMenu"
@select-panel="selectPanelMode"
@open-file-manager="openGuiFileManager"
@@ -187,6 +188,13 @@
:icon-style="iconStyle"
:tool-category-icon="toolCategoryIcon"
: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"
@input-change="handleInputChange"
@input-focus="handleInputFocus"
diff --git a/static/src/admin/AdminDashboardApp.vue b/static/src/admin/AdminDashboardApp.vue
index 2700abb..3cab4c1 100644
--- a/static/src/admin/AdminDashboardApp.vue
+++ b/static/src/admin/AdminDashboardApp.vue
@@ -16,6 +16,7 @@
+ 策略配置
@@ -557,6 +558,20 @@ const inviteStatus = (remaining: number | null | undefined) => {
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 {
border: none;
border-radius: 999px;
diff --git a/static/src/admin/PolicyApp.vue b/static/src/admin/PolicyApp.vue
new file mode 100644
index 0000000..5c54049
--- /dev/null
+++ b/static/src/admin/PolicyApp.vue
@@ -0,0 +1,1074 @@
+
+
+
+
+
+
+ {{ banner.message }}
+
+
+
+
+
+
+
工具分类
+
+
+
+
+ 分类 ID
+ 名称
+ 工具列表
+ 默认启用
+ 强制状态
+ 操作
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
已删除分类
+
+
+ 无
+
+ {{ cid }}
+
+
+
+
+
+
+ 说明:
+
+ - 优先级:用户 > 邀请码 > 角色 > 全局。
+ - 分类“强制状态”会覆盖用户侧的工具开关,并在前端提示“被管理员强制”。
+ - UI 禁用项会在用户操作时弹出右上角提示。
+
+
+
+
+
+
+
+
diff --git a/static/src/admin/policyMain.ts b/static/src/admin/policyMain.ts
new file mode 100644
index 0000000..4f97ea3
--- /dev/null
+++ b/static/src/admin/policyMain.ts
@@ -0,0 +1,4 @@
+import { createApp } from 'vue';
+import PolicyApp from './PolicyApp.vue';
+
+createApp(PolicyApp).mount('#admin-policy-app');
diff --git a/static/src/app.ts b/static/src/app.ts
index d37652f..6ae6b37 100644
--- a/static/src/app.ts
+++ b/static/src/app.ts
@@ -28,6 +28,7 @@ import { usePersonalizationStore } from './stores/personalization';
import { useModelStore } from './stores/model';
import { useChatActionStore } from './stores/chatActions';
import { useMonitorStore } from './stores/monitor';
+import { usePolicyStore } from './stores/policy';
import { ICONS, TOOL_CATEGORY_ICON_MAP } from './utils/icons';
import { initializeLegacySocket } from './composables/useLegacySocket';
import { useConnectionStore } from './stores/connection';
@@ -435,8 +436,21 @@ const appOptions = {
}
return this.thinkingMode ? 'thinking' : 'fast';
},
+ policyUiBlocks() {
+ const store = usePolicyStore();
+ return store.uiBlocks || {};
+ },
+ adminDisabledModels() {
+ const store = usePolicyStore();
+ return store.disabledModelSet;
+ },
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() {
return !this.isMobileViewport && this.chatDisplayMode === 'chat';
@@ -466,7 +480,7 @@ const appOptions = {
})
,
displayModeSwitchDisabled() {
- return false;
+ return !!this.policyUiBlocks.block_virtual_monitor;
},
displayLockEngaged() {
return false;
@@ -652,6 +666,28 @@ const appOptions = {
}
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) {
if (!this.isMobileViewport || !this.mobileOverlayMenuOpen) {
return;
@@ -666,6 +702,10 @@ const appOptions = {
if (this.isMobileViewport) {
return;
}
+ if (this.isPolicyBlocked('collapse_workspace', '工作区已被管理员强制折叠')) {
+ this.uiSetWorkspaceCollapsed(true);
+ return;
+ }
const nextState = !this.workspaceCollapsed;
this.uiSetWorkspaceCollapsed(nextState);
if (nextState) {
@@ -674,6 +714,11 @@ const appOptions = {
},
handleDisplayModeToggle() {
if (this.displayModeSwitchDisabled) {
+ // 显式提示管理员禁用
+ this.isPolicyBlocked('block_virtual_monitor', '虚拟显示器已被管理员禁用');
+ return;
+ }
+ if (this.chatDisplayMode === 'chat' && this.isPolicyBlocked('block_virtual_monitor', '虚拟显示器已被管理员禁用')) {
return;
}
const next = this.chatDisplayMode === 'chat' ? 'monitor' : 'chat';
@@ -912,6 +957,9 @@ const appOptions = {
},
openGuiFileManager() {
+ if (this.isPolicyBlocked('block_file_manager', '文件管理器已被管理员禁用')) {
+ return;
+ }
window.open('/file-manager', '_blank');
},
@@ -1393,6 +1441,10 @@ const appOptions = {
this.applyStatusSnapshot(statusData);
// 立即更新配额和运行模式,避免等待其他慢接口
this.fetchUsageQuota();
+ // 拉取管理员策略
+ const policyStore = usePolicyStore();
+ await policyStore.fetchPolicy();
+ this.applyPolicyUiLocks();
// 获取当前对话信息
const statusConversationId = statusData.conversation && statusData.conversation.current_id;
@@ -2270,6 +2322,9 @@ const appOptions = {
},
openPersonalPage() {
+ if (this.isPolicyBlocked('block_personal_space', '个人空间已被管理员禁用')) {
+ return;
+ }
this.personalizationOpenDrawer();
},
@@ -2303,6 +2358,15 @@ const appOptions = {
},
handleFileSelected(files) {
+ const policyStore = usePolicyStore();
+ if (policyStore.uiBlocks?.block_upload) {
+ this.uiPushToast({
+ title: '上传被禁用',
+ message: '已被管理员禁用上传功能',
+ type: 'warning'
+ });
+ return;
+ }
this.uploadHandleSelected(files);
},
@@ -2528,6 +2592,9 @@ const appOptions = {
if (!this.isConnected) {
return;
}
+ if (this.isPolicyBlocked('block_tool_toggle', '工具启用/禁用已被管理员锁定')) {
+ return;
+ }
this.modeMenuOpen = false;
this.modelMenuOpen = false;
const nextState = this.inputToggleToolMenu();
@@ -2666,6 +2733,9 @@ const appOptions = {
if (this.uploading || !this.isConnected) {
return;
}
+ if (this.isPolicyBlocked('block_upload', '上传功能已被管理员禁用')) {
+ return;
+ }
this.triggerFileUpload();
},
@@ -2714,6 +2784,15 @@ const appOptions = {
if (!this.isConnected || this.streamingMessage) {
return;
}
+ const policyStore = usePolicyStore();
+ if (policyStore.isModelDisabled(key)) {
+ this.uiPushToast({
+ title: '模型被禁用',
+ message: '被管理员强制禁用',
+ type: 'warning'
+ });
+ return;
+ }
if (this.conversationHasImages && key !== 'qwen3-vl-plus') {
this.uiPushToast({
title: '切换失败',
@@ -2869,6 +2948,9 @@ const appOptions = {
if (!this.isConnected) {
return;
}
+ if (this.isPolicyBlocked('block_realtime_terminal', '实时终端已被管理员禁用')) {
+ return;
+ }
this.openRealtimeTerminal();
},
@@ -2876,6 +2958,9 @@ const appOptions = {
if (!this.isConnected) {
return;
}
+ if (this.isPolicyBlocked('block_focus_panel', '聚焦面板已被管理员禁用')) {
+ return;
+ }
this.toggleFocusPanel();
},
@@ -2883,6 +2968,9 @@ const appOptions = {
if (!this.currentConversationId) {
return;
}
+ if (this.isPolicyBlocked('block_token_panel', '用量统计已被管理员禁用')) {
+ return;
+ }
this.toggleTokenPanel();
},
@@ -2890,10 +2978,16 @@ const appOptions = {
if (this.compressing || this.streamingMessage || !this.isConnected) {
return;
}
+ if (this.isPolicyBlocked('block_compress_conversation', '压缩对话已被管理员禁用')) {
+ return;
+ }
this.compressConversation();
},
openReviewDialog() {
+ if (this.isPolicyBlocked('block_conversation_review', '对话引用已被管理员禁用')) {
+ return;
+ }
if (!this.isConnected) {
this.uiPushToast({
title: '无法使用',
@@ -3165,7 +3259,9 @@ const appOptions = {
id: item.id,
label: item.label || item.id,
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', {
received: categories.length,
@@ -3215,6 +3311,15 @@ const appOptions = {
if (this.toolSettingsLoading) {
return;
}
+ const policyStore = usePolicyStore();
+ if (policyStore.isCategoryLocked(categoryId)) {
+ this.uiPushToast({
+ title: '无法修改',
+ message: '该工具类别被管理员强制设置',
+ type: 'warning'
+ });
+ return;
+ }
const previousSnapshot = this.toolSettings.map((item) => ({ ...item }));
const updatedSettings = this.toolSettings.map((item) => {
if (item.id === categoryId) {
@@ -3239,6 +3344,13 @@ const appOptions = {
this.applyToolSettingsSnapshot(data.categories);
} else {
console.warn('更新工具设置失败:', data);
+ if (data && (data.message || data.error)) {
+ this.uiPushToast({
+ title: '无法切换工具',
+ message: data.message || data.error,
+ type: 'warning'
+ });
+ }
this.toolSetSettings(previousSnapshot);
}
} catch (error) {
diff --git a/static/src/components/input/InputComposer.vue b/static/src/components/input/InputComposer.vue
index a2c8ffc..abac9c8 100644
--- a/static/src/components/input/InputComposer.vue
+++ b/static/src/components/input/InputComposer.vue
@@ -74,6 +74,13 @@
:current-conversation-id="currentConversationId"
:icon-style="iconStyle"
: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"
@pick-images="$emit('pick-images')"
@toggle-tool-menu="$emit('toggle-tool-menu')"
@@ -147,9 +154,16 @@ const props = defineProps<{
currentConversationId: string | null;
iconStyle: (key: string) => Record;
toolCategoryIcon: (categoryId: string) => string;
- modelOptions: Array<{ key: string; label: string; description: string }>;
+ modelOptions: Array<{ key: string; label: string; description: string; disabled?: boolean }>;
currentModelKey: string;
selectedImages?: string[];
+ blockUpload?: boolean;
+ blockToolToggle?: boolean;
+ blockRealtimeTerminal?: boolean;
+ blockFocusPanel?: boolean;
+ blockTokenPanel?: boolean;
+ blockCompressConversation?: boolean;
+ blockConversationReview?: boolean;
}>();
const inputStore = useInputStore();
diff --git a/static/src/components/input/QuickMenu.vue b/static/src/components/input/QuickMenu.vue
index 979f97d..7f891e9 100644
--- a/static/src/components/input/QuickMenu.vue
+++ b/static/src/components/input/QuickMenu.vue
@@ -1,7 +1,12 @@
@@ -186,8 +198,15 @@ const props = defineProps<{
modeMenuOpen: boolean;
runMode?: 'fast' | 'thinking' | 'deep';
modelMenuOpen: boolean;
- modelOptions: Array<{ key: string; label: string; description: string }>;
+ modelOptions: Array<{ key: string; label: string; description: string; disabled?: boolean }>;
currentModelKey: string;
+ blockUpload?: boolean;
+ blockToolToggle?: boolean;
+ blockRealtimeTerminal?: boolean;
+ blockFocusPanel?: boolean;
+ blockTokenPanel?: boolean;
+ blockCompressConversation?: boolean;
+ blockConversationReview?: boolean;
}>();
defineEmits<{
diff --git a/static/src/components/panels/LeftPanel.vue b/static/src/components/panels/LeftPanel.vue
index 8692f26..b8f3c4b 100644
--- a/static/src/components/panels/LeftPanel.vue
+++ b/static/src/components/panels/LeftPanel.vue
@@ -68,7 +68,13 @@
-
+
@@ -146,6 +152,7 @@ const props = defineProps<{
panelMenuOpen: boolean;
panelMode: 'files' | 'todo' | 'subAgents';
runMode: 'fast' | 'thinking' | 'deep';
+ fileManagerDisabled?: boolean;
}>();
defineEmits<{
diff --git a/static/src/components/personalization/PersonalizationDrawer.vue b/static/src/components/personalization/PersonalizationDrawer.vue
index 139b38c..c0e11da 100644
--- a/static/src/components/personalization/PersonalizationDrawer.vue
+++ b/static/src/components/personalization/PersonalizationDrawer.vue
@@ -191,17 +191,19 @@
@@ -521,6 +523,7 @@ import { storeToRefs } from 'pinia';
import { usePersonalizationStore } from '@/stores/personalization';
import { useResourceStore } from '@/stores/resource';
import { useUiStore } from '@/stores/ui';
+import { usePolicyStore } from '@/stores/policy';
import { useTheme } 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' }
];
+const policyStore = usePolicyStore();
+
const modelOptions = [
{ id: 'deepseek', label: 'DeepSeek', desc: '通用 + 思考强化', value: 'deepseek' },
{ 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: '图文' }
] as const;
+const filteredModelOptions = computed(() =>
+ modelOptions.map((opt) => ({
+ ...opt,
+ disabled: policyStore.disabledModelSet.has(opt.value)
+ }))
+);
+
const thinkingPresets = [
{ id: 'low', label: '低', value: 10 },
{ id: 'medium', label: '中', value: 5 },
@@ -645,6 +657,14 @@ const setDefaultRunMode = (value: RunModeValue) => {
};
const setDefaultModel = (value: string) => {
+ if (policyStore.disabledModelSet.has(value)) {
+ uiStore.pushToast({
+ title: '模型被禁用',
+ message: '已被管理员禁用,无法选择',
+ type: 'warning'
+ });
+ return;
+ }
if (checkModeModelConflict(form.value.default_run_mode, value)) {
return;
}
diff --git a/static/src/components/sidebar/ConversationSidebar.vue b/static/src/components/sidebar/ConversationSidebar.vue
index 947c8e1..ffefadc 100644
--- a/static/src/components/sidebar/ConversationSidebar.vue
+++ b/static/src/components/sidebar/ConversationSidebar.vue
@@ -67,9 +67,9 @@