agent-Specialization/static/src/components/personalization/PersonalizationDrawer.vue
JOJO b4030c386e refactor(frontend): 颜色 token 系统两层重构 + 三主题填色 + 去半透明
将前端配色从散落的硬编码/双命名体系(--claude-* / --theme-*)重构为
两层 token 结构(原始层 + 中性语义层),并按"经典抄 Claude、浅色抄
ChatGPT、深色自研"的定位完成三主题填色,同时建立 stylelint 防回退栏杆。
代码大量由 AI 生成的历史遗留配色问题(同语义多值、跨主题塌缩、强调色
撞文字色、fallback 第二真相源等)在此一并收口。

== 一、Token 结构重构(_tokens.scss)==
- 重写为两层:原始层 + 语义层;组件只引用中性语义名
  (--surface-* / --text-* / --border-* / --accent* / --state-* 等)。
- --claude-* / --theme-* 降级为兼容别名(41 个转发),迁移完成后删除。
- 三主题(classic / light / dark)+ 首屏回退(:root:not([data-theme]),
  与经典同值)全部补齐,结构对齐、不再缺项。

== 二、三主题填色 ==
- 经典 = Claude 官网亮色盘:暖奶油表面阶梯
  canvas #faf9f5 < soft #f5f0e8 < card #efe9de < cream-strong #e8e0d2
  < 纯白浮起;文字 #141413;hairline #e6dfd8;暖橙 primary #cc785c。
- 浅色 = ChatGPT 亮色盘:冷白 #ffffff / 侧栏 #f9f9f9 / 嵌套 #f3f3f3,
  中性灰文字,近黑 primary #181818。
- 深色 = 自研中性灰阶,本轮完全不动(原样保留)。
- 修复塌缩 bug:强调色在 light/dark 原被误填成灰(CTA 失去品牌色),
  现统一回品牌 primary;light 表面原全塌成 #ffffff(过于平面),
  现按灰阶拉开层次。
- 强调色克制(CTA-only voltage):primary 仅用于发送按钮/logo 等极少
  数处;hover / 选中 / 运行态一律走中性灰(运行态指示器从 amber 改灰)。
- 对话区背景从纯白改为暖奶油 canvas;个人空间下拉菜单、git 状态条、
  二级菜单等收口到统一语义色。

== 三、全面去半透明 ==
- 所有实体面板/二级菜单背景改为不透明(surface token 由半透明白改实色,
  --mobile-menu 三亮色主题改实色)。
- 移除已不透明面板上失效的 backdrop-filter 磨砂(移动端菜单、右键菜单、
  模型模式下拉等)。
- 遮罩 scrim(--overlay-scrim)与刻意玻璃质感装饰保留半透明。

== 四、块展开底色统一 ==
- 思考/工具块展开区底色统一为对话背景色:.collapsible-block 及其 header
  背景由 --surface-soft 改 transparent,与堆叠块(stacked-block)一致,
  消除"单块奶油底 vs 多块透明"的不一致。

== 五、布局修复 ==
- 工作区隐藏后残留空白:.workspace-panel--collapsed 补 flex-basis:0 +
  width/min-width:0,修正 flex-basis:auto 被子元素内容宽度撑开导致内联
  width:0 失效、collapsed 后仍占 ~300px 的问题。

== 六、防回退栏杆(新增)==
- 新增 .stylelintrc.cjs:三条规则拦截裸 hex / rgb()|hsl() 字面色 /
  var(--x, fallback) 兜底 / prefers-color-scheme;存量未清理文件用
  BASELINE_EXEMPT 临时豁免(现仅剩 5 个 admin 页面,本轮按用户要求不处理),
  清理一个移除一个、不再回退。
- package.json:build 脚本接入 stylelint(tsc → stylelint → vite),
  新增 lint:css 脚本及 stylelint/postcss-scss/postcss-html 依赖。
- 修复 25 处 \!important 笔误(zsh 历史扩展污染导致的无效 CSS)。

== 七、辅助脚本(新增 scripts/)==
- lint_check.py:Node 环境不可用时的 stylelint 替身(复刻三规则 + 括号
  校验,支持多行声明检测)。
- color_map.py:硬编码色 → token 反查,判定可映射/野色。
- strip_fallback.py:安全剥离已定义 token 的冗余 fallback。

== 八、文档 ==
- CLAUDE.md / AGENTS.md 前端设计规范更新:颜色规则扩写为三主题定位 +
  两层 token + 表面层次 + 强调色克制 + color-mix 派生 + stylelint 栏杆;
  新增"实体面板禁止半透明"规则;"禁止边缘光晕"补充"仅在用户明确允许
  或要求时才可使用"。

注:前端构建验证(npm run build / lint:css)由用户本机完成;AI 工具
上下文 Node v24 段错误无法运行 node/npm/vite/stylelint。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 16:20:58 +08:00

3226 lines
122 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<transition name="personal-page-fade" appear>
<div
class="personal-page-overlay"
v-if="visible"
@click="closeDropdown"
@mousedown.self="personalization.handleOverlayPressStart($event)"
@mouseup.self="personalization.handleOverlayPressEnd"
@mouseleave.self="personalization.handleOverlayPressCancel"
@touchstart.self.prevent="personalization.handleOverlayPressStart($event)"
@touchend.self.prevent="personalization.handleOverlayPressEnd"
@touchcancel.self="personalization.handleOverlayPressCancel"
>
<div class="personal-page-card settings-redesign-card" data-tutorial="personal-card">
<button
type="button"
class="settings-close-button"
data-tutorial="personal-close"
aria-label="关闭个人空间"
@click="personalization.closeDrawer()"
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round">
<path d="M6 6l12 12M18 6 6 18" />
</svg>
</button>
<div class="personalization-body settings-redesign-body" v-if="!loading">
<form
class="personal-form settings-redesign-form"
@submit.prevent="personalization.save()"
>
<div class="settings-redesign-layout">
<nav class="settings-redesign-tabs" aria-label="个人空间分组切换">
<button
v-for="tab in personalTabs"
:key="tab.id"
type="button"
class="settings-redesign-tab"
:data-tutorial="`personal-tab-${tab.id}`"
:class="{ active: activeTab === tab.id }"
:aria-pressed="activeTab === tab.id"
@click.prevent="setActiveTab(tab.id)"
>
<span
v-if="tab.id === 'context'"
class="settings-tab-icon settings-tab-icon--chat"
aria-hidden="true"
>
<svg viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M5 6.5c0-1.38 1.12-2.5 2.5-2.5h13c1.38 0 2.5 1.12 2.5 2.5v8.5c0 1.38-1.12 2.5-2.5 2.5h-5.6l-3.4 3.2.6-3.2H7.5c-1.38 0-2.5-1.12-2.5-2.5V6.5z"
stroke="currentColor"
stroke-width="1.7"
stroke-linejoin="round"
/>
<path
d="M9 9.5h10"
stroke="currentColor"
stroke-width="1.7"
stroke-linecap="round"
/>
<path
d="M9 13h6"
stroke="currentColor"
stroke-width="1.7"
stroke-linecap="round"
/>
</svg>
</span>
<span
v-else
class="icon settings-tab-icon"
:style="settingsTabIconStyle(tab.icon)"
aria-hidden="true"
></span>
<span>{{ tab.label }}</span>
</button>
</nav>
<section class="settings-redesign-content" data-tutorial="personal-content-shell">
<header class="settings-redesign-content-header">
<h2>{{ activeTabLabel }}</h2>
</header>
<div class="settings-redesign-title-line" aria-hidden="true"></div>
<div class="settings-redesign-scroll">
<transition name="personal-page-vertical" mode="out-in">
<section v-if="activeTab === 'general'" key="general" class="settings-page">
<label class="settings-toggle-row">
<span class="settings-row-copy">
<span class="settings-row-title">自动生成对话标题</span>
<span class="settings-row-desc"
>默认开启;关闭后标题将沿用首条消息</span
>
</span>
<input
type="checkbox"
:checked="form.auto_generate_title"
@change="
personalization.updateField({
key: 'auto_generate_title',
value: $event.target.checked
})
"
/>
<span class="fancy-check" aria-hidden="true"
><svg viewBox="0 0 64 64">
<path
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
pathLength="575.0541381835938"
class="fancy-path"
></path></svg
></span>
</label>
<div class="settings-action-row">
<span class="settings-row-copy">
<span class="settings-row-title">新手教程</span>
<span class="settings-row-desc"
>通过高亮与说明窗了解主要功能入口和常用设置</span
>
</span>
<button
type="button"
class="settings-secondary-button"
@click="startTutorial"
>
开始教程
</button>
</div>
<div class="settings-action-row" v-if="isAppShell">
<span class="settings-row-copy">
<span class="settings-row-title">软件更新</span>
<span class="settings-row-desc"
>当前版本:{{ appCurrentVersionText }} ·
{{ appUpdateCheckedText }}</span
>
</span>
<div class="settings-inline-actions">
<span class="settings-mini-status" :class="{ warning: appHasUpdate }">{{
appUpdateStateText
}}</span>
<button
type="button"
class="settings-secondary-button"
:disabled="appUpdateChecking"
@click="checkAppUpdate"
>
{{ appUpdateChecking ? '检查中...' : '检查更新' }}
</button>
<button
v-if="appHasUpdate"
type="button"
class="settings-primary-button"
@click="downloadLatestApp"
>
下载
</button>
</div>
</div>
<div class="settings-action-row danger-zone">
<span class="settings-row-copy">
<span class="settings-row-title">退出登录</span>
<span class="settings-row-desc">结束当前账号会话</span>
</span>
<button
type="button"
class="settings-secondary-button danger"
@click="personalization.logout()"
>
退出登录
</button>
</div>
</section>
<section
v-else-if="activeTab === 'preferences'"
key="preferences"
class="settings-page"
data-tutorial="personal-page-preferences"
>
<label class="settings-toggle-row">
<span class="settings-row-copy">
<span class="settings-row-title">启用个性化提示</span>
<span class="settings-row-desc">开启后才会注入您的偏好</span>
</span>
<input
type="checkbox"
:checked="form.enabled"
:disabled="toggleUpdating"
@change="personalization.toggleEnabled()"
/>
<span class="fancy-check" aria-hidden="true"
><svg viewBox="0 0 64 64">
<path
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
pathLength="575.0541381835938"
class="fancy-path"
></path></svg
></span>
</label>
<label class="settings-input-row">
<span class="settings-row-title">AI 智能体自称</span>
<input
type="text"
:value="form.self_identify"
maxlength="20"
placeholder="如小秘、助理小A"
@input="
personalization.updateField({
key: 'self_identify',
value: $event.target.value
})
"
@focus="personalization.clearFeedback()"
/>
</label>
<label class="settings-input-row">
<span class="settings-row-title">用户称呼</span>
<input
type="text"
:value="form.user_name"
maxlength="20"
placeholder="如Jojo、老师"
@input="
personalization.updateField({
key: 'user_name',
value: $event.target.value
})
"
@focus="personalization.clearFeedback()"
/>
</label>
<label class="settings-input-row">
<span class="settings-row-title">职业</span>
<input
type="text"
:value="form.profession"
maxlength="20"
placeholder="如:产品经理、设计师"
@input="
personalization.updateField({
key: 'profession',
value: $event.target.value
})
"
@focus="personalization.clearFeedback()"
/>
</label>
<div class="settings-input-row stackable">
<span class="settings-row-title">交流语气</span>
<div class="settings-input-stack">
<input
type="text"
:value="form.tone"
maxlength="20"
placeholder="请选择或输入语气"
@input="
personalization.updateField({
key: 'tone',
value: $event.target.value
})
"
@focus="personalization.clearFeedback()"
/>
<div class="settings-chip-row">
<button
v-for="preset in tonePresets"
:key="preset"
type="button"
@click.prevent="personalization.applyTonePreset(preset)"
>
{{ preset }}
</button>
</div>
</div>
</div>
<div class="settings-list-row tall">
<span class="settings-row-copy">
<span class="settings-row-title">回答时必须考虑的信息</span>
<span class="settings-row-desc"
>最多 {{ maxConsiderations }} 条,可拖动排序</span
>
</span>
<div class="settings-input-stack wide">
<div class="settings-add-row">
<input
type="text"
:value="newConsideration"
maxlength="50"
placeholder="输入后点击 + 添加"
@input="personalization.updateNewConsideration($event.target.value)"
@focus="personalization.clearFeedback()"
/>
<button
type="button"
:disabled="
!newConsideration || form.considerations.length >= maxConsiderations
"
@click="personalization.addConsideration()"
>
+
</button>
</div>
<ul class="settings-consideration-list" v-if="form.considerations.length">
<li
v-for="(item, idx) in form.considerations"
:key="`consideration-${idx}`"
draggable="true"
@dragstart="personalization.considerationDragStart(idx, $event)"
@dragover.prevent="personalization.considerationDragOver(idx, $event)"
@drop.prevent="personalization.considerationDrop(idx, $event)"
@dragend="personalization.considerationDragEnd()"
>
<span class="drag-handle" aria-hidden="true">≡</span>
<span>{{ item }}</span>
<button
type="button"
@click="personalization.removeConsideration(idx)"
>
</button>
</li>
</ul>
</div>
</div>
<div class="settings-select-row">
<span class="settings-row-copy">
<span class="settings-row-title">智能体交流风格</span>
<span class="settings-row-desc">智能体和您交流时使用何种形式</span>
</span>
<div
class="settings-select-wrap"
:class="{ open: activeDropdown === 'communication' }"
@click.stop
>
<button
type="button"
class="settings-select-button"
@click="toggleDropdown('communication')"
>
{{ communicationStyleLabel }}
<span class="select-chevron" aria-hidden="true"></span>
</button>
<div
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
:style="activeDropdown ? floatingMenuStyle : undefined"
>
<button
type="button"
class="settings-menu-option"
:class="{ selected: form.communication_style === 'default' }"
@click="selectCommunicationStyle('default')"
>
<strong>默认</strong><span>标准 AI 风格</span
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
<button
type="button"
class="settings-menu-option"
:class="{ selected: form.communication_style === 'human_like' }"
@click="selectCommunicationStyle('human_like')"
>
<strong>拟人</strong><span>像聊天一样模仿人类语言风格</span
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
</div>
</div>
</div>
</section>
<section
v-else-if="activeTab === 'model'"
key="model"
class="settings-page"
data-tutorial="personal-page-model"
>
<div class="settings-select-row">
<span class="settings-row-copy"
><span class="settings-row-title">默认模型</span
><span class="settings-row-desc"
>为新对话/登录后首次请求选择首选模型</span
></span
>
<div
class="settings-select-wrap"
:class="{ open: activeDropdown === 'model' }"
@click.stop
>
<button
type="button"
class="settings-select-button"
@click="toggleDropdown('model')"
>
{{ defaultModelLabel }}
<span class="select-chevron" aria-hidden="true"></span>
</button>
<div
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
:style="activeDropdown ? floatingMenuStyle : undefined"
>
<button
v-for="option in filteredModelOptions"
:key="option.id"
type="button"
class="settings-menu-option"
:class="{
selected: form.default_model === option.value,
disabled: option.disabled
}"
:disabled="option.disabled"
@click="selectDefaultModel(option.value)"
>
<strong>{{ option.label }}</strong
><span>{{ option.disabled ? '已被管理员禁用' : option.desc }}</span
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
</div>
</div>
</div>
<div class="settings-select-row">
<span class="settings-row-copy"
><span class="settings-row-title">默认运行模式</span
><span class="settings-row-desc"
>登录或新建任务时的初始运行模式</span
></span
>
<div
class="settings-select-wrap"
:class="{ open: activeDropdown === 'run-mode' }"
@click.stop
>
<button
type="button"
class="settings-select-button"
@click="toggleDropdown('run-mode')"
>
{{ runModeLabel }}
<span class="select-chevron" aria-hidden="true"></span>
</button>
<div
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
:style="activeDropdown ? floatingMenuStyle : undefined"
>
<button
v-for="option in runModeOptions"
:key="option.id"
type="button"
class="settings-menu-option"
:class="{ selected: isRunModeActive(option.value) }"
@click="selectDefaultRunMode(option.value)"
>
<strong>{{ option.label }}</strong
><span>{{ option.desc }}</span
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
</div>
</div>
</div>
<div class="settings-list-row tall">
<span class="settings-row-copy"
><span class="settings-row-title">思考频率</span
><span class="settings-row-desc"
>连续快速模式运行多少轮后,系统强制切换到思考模型</span
></span
>
<div class="settings-input-stack wide">
<div class="settings-chip-row right">
<button
v-for="preset in thinkingPresets"
:key="preset.id"
type="button"
:class="{ active: isPresetActive(preset.value) }"
@click.prevent="applyThinkingPreset(preset.value)"
>
{{ preset.label }} · {{ preset.value }}轮
</button>
</div>
<div class="settings-number-row">
<input
type="number"
:min="thinkingIntervalRange.min"
:max="thinkingIntervalRange.max"
:placeholder="`默认 ${thinkingIntervalDefault} 轮`"
:value="form.thinking_interval ?? ''"
@input="handleThinkingInput"
@focus="personalization.clearFeedback()"
/>
<button type="button" @click.prevent="restoreThinkingInterval">
恢复默认
</button>
</div>
</div>
</div>
</section>
<section
v-else-if="activeTab === 'appearance'"
key="appearance"
class="settings-page"
>
<div class="settings-select-row">
<span class="settings-row-copy"
><span class="settings-row-title">主题切换</span
><span class="settings-row-desc">在经典、明亮和夜间之间切换</span></span
>
<div
class="settings-select-wrap"
:class="{ open: activeDropdown === 'theme' }"
@click.stop
>
<button
type="button"
class="settings-select-button"
@click="toggleDropdown('theme')"
>
{{ themeLabel }} <span class="select-chevron" aria-hidden="true"></span>
</button>
<div
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
:style="activeDropdown ? floatingMenuStyle : undefined"
>
<button
v-for="option in themeOptions"
:key="option.id"
type="button"
class="settings-menu-option"
:class="{ selected: activeTheme === option.id }"
@click="selectThemeOption(option.id)"
>
<strong>{{ option.label }}</strong
><span>{{ option.desc }}</span
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
</div>
</div>
</div>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">默认隐藏工作区</span
><span class="settings-row-desc"
>打开页面时默认收起左侧工作区,仍可随时手动展开</span
></span
><input
type="checkbox"
:checked="form.default_hide_workspace"
@change="applyDefaultHideWorkspaceOption($event.target.checked)"
/><span class="fancy-check" aria-hidden="true"
><svg viewBox="0 0 64 64">
<path
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
pathLength="575.0541381835938"
class="fancy-path"
></path></svg></span
></label>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">使用自定义称呼</span
><span class="settings-row-desc"
>对话区域使用个性化设置中的自称和称呼</span
></span
><input
type="checkbox"
:checked="form.use_custom_names"
@change="
personalization.updateField({
key: 'use_custom_names',
value: $event.target.checked
})
" /><span class="fancy-check" aria-hidden="true"
><svg viewBox="0 0 64 64">
<path
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
pathLength="575.0541381835938"
class="fancy-path"
></path></svg></span
></label>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">增强工具显示</span
><span class="settings-row-desc"
>工具块显示格式化内容,关闭则显示原始 JSON</span
></span
><input
type="checkbox"
:checked="form.enhanced_tool_display"
@change="
personalization.updateField({
key: 'enhanced_tool_display',
value: $event.target.checked
})
" /><span class="fancy-check" aria-hidden="true"
><svg viewBox="0 0 64 64">
<path
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
pathLength="575.0541381835938"
class="fancy-path"
></path></svg></span
></label>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">显示 Git 状态栏</span
><span class="settings-row-desc"
>工作区存在 Git 仓库时,在输入栏上方显示分支和变更统计</span
></span
><input
type="checkbox"
:checked="form.show_git_status_bar"
@change="
personalization.updateField({
key: 'show_git_status_bar',
value: $event.target.checked
})
" /><span class="fancy-check" aria-hidden="true"
><svg viewBox="0 0 64 64">
<path
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
pathLength="575.0541381835938"
class="fancy-path"
></path></svg></span
></label>
<div class="settings-select-row">
<span class="settings-row-copy"
><span class="settings-row-title">堆叠块显示模式</span
><span class="settings-row-desc">选择思考/工具块的显示方式</span></span
>
<div
class="settings-select-wrap"
:class="{ open: activeDropdown === 'block-display' }"
@click.stop
>
<button
type="button"
class="settings-select-button"
@click="toggleDropdown('block-display')"
>
{{ blockDisplayLabel }}
<span class="select-chevron" aria-hidden="true"></span>
</button>
<div
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
:style="activeDropdown ? floatingMenuStyle : undefined"
>
<button
v-for="option in blockDisplayOptions"
:key="option.id"
type="button"
class="settings-menu-option"
:class="{ selected: currentBlockDisplayMode === option.value }"
@click="selectBlockDisplayMode(option.value)"
>
<strong>{{ option.label }}</strong
><span>{{ option.desc }}</span
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
</div>
</div>
</div>
<div class="settings-select-row">
<span class="settings-row-copy"
><span class="settings-row-title">简略消息显示</span
><span class="settings-row-desc"
>审核、子智能体等紧凑系统消息的显示形式</span
></span
>
<div
class="settings-select-wrap"
:class="{ open: activeDropdown === 'compact-message' }"
@click.stop
>
<button
type="button"
class="settings-select-button"
@click="toggleDropdown('compact-message')"
>
{{ compactMessageDisplayLabel }}
<span class="select-chevron" aria-hidden="true"></span>
</button>
<div
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
:style="activeDropdown ? floatingMenuStyle : undefined"
>
<button
v-for="option in compactMessageDisplayOptions"
:key="option.id"
type="button"
class="settings-menu-option"
:class="{ selected: currentCompactMessageDisplay === option.value }"
@click="selectCompactMessageDisplay(option.value)"
>
<strong>{{ option.label }}</strong
><span>{{ option.desc }}</span
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
</div>
</div>
</div>
</section>
<section
v-else-if="activeTab === 'workspace'"
key="workspace"
class="settings-page"
data-tutorial="personal-page-workspace"
>
<div class="settings-select-row">
<span class="settings-row-copy"
><span class="settings-row-title">默认权限</span
><span class="settings-row-desc">新建对话时的默认权限模式</span></span
>
<div
class="settings-select-wrap"
:class="{ open: activeDropdown === 'permission' }"
@click.stop
>
<button
type="button"
class="settings-select-button"
@click="toggleDropdown('permission')"
>
{{ permissionModeLabel }}
<span class="select-chevron" aria-hidden="true"></span>
</button>
<div
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
:style="activeDropdown ? floatingMenuStyle : undefined"
>
<button
v-for="option in permissionModeOptions"
:key="option.id"
type="button"
class="settings-menu-option"
:class="{ selected: form.default_permission_mode === option.id }"
@click="selectDefaultPermissionMode(option.id)"
>
<strong>{{ option.label }}</strong
><span>{{ option.desc }}</span
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
</div>
</div>
</div>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">允许根目录创建文件</span
><span class="settings-row-desc"
>关闭则禁止在工作区根目录创建文件</span
></span
><input
type="checkbox"
:checked="form.allow_root_file_creation"
@change="
personalization.updateField({
key: 'allow_root_file_creation',
value: $event.target.checked
})
" /><span class="fancy-check" aria-hidden="true"
><svg viewBox="0 0 64 64">
<path
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
pathLength="575.0541381835938"
class="fancy-path"
></path></svg></span
></label>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">AGENTS.md 自动注入</span
><span class="settings-row-desc"
>工作区根目录存在 AGENTS.md 时自动注入系统提示词</span
></span
><input
type="checkbox"
:checked="form.agents_md_auto_inject"
@change="
personalization.updateField({
key: 'agents_md_auto_inject',
value: $event.target.checked
})
" /><span class="fancy-check" aria-hidden="true"
><svg viewBox="0 0 64 64">
<path
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
pathLength="575.0541381835938"
class="fancy-path"
></path></svg></span
></label>
<div class="settings-section-divider">
<span class="settings-section-divider__label">目标模式</span>
</div>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">审核智能体主动取证</span
><span class="settings-row-desc"
>开启后可运行只读命令核实目标,关闭则仅依据对话判断</span
></span
><input
type="checkbox"
:checked="form.goal_review_mode === 'active'"
@change="
personalization.updateField({
key: 'goal_review_mode',
value: $event.target.checked ? 'active' : 'readonly'
})
" /><span class="fancy-check" aria-hidden="true"
><svg viewBox="0 0 64 64">
<path
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
pathLength="575.0541381835938"
class="fancy-path"
></path></svg></span
></label>
<div class="settings-select-row">
<span class="settings-row-copy"
><span class="settings-row-title">最大自动续命轮数</span
><span class="settings-row-desc"
>主模型停下后最多自动继续 1-100 轮</span
></span
>
<input
type="number"
class="settings-number-input"
min="1"
max="100"
:value="form.goal_max_turns"
@change="
personalization.updateField({
key: 'goal_max_turns',
value: clampGoalMaxTurns($event.target.value)
})
"
/>
</div>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">启用累计 token 上限</span
><span class="settings-row-desc"
>累计输入输出超过上限时停止目标模式</span
></span
><input
type="checkbox"
:checked="goalTokenLimitEnabled"
@change="toggleGoalTokenLimit($event.target.checked)" /><span
class="fancy-check"
aria-hidden="true"
><svg viewBox="0 0 64 64">
<path
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
pathLength="575.0541381835938"
class="fancy-path"
></path></svg></span
></label>
<div v-if="goalTokenLimitEnabled" class="settings-select-row">
<span class="settings-row-copy"
><span class="settings-row-title">累计 token 上限</span
><span class="settings-row-desc">达到该累计消耗即停止目标模式</span></span
>
<input
type="number"
class="settings-number-input"
min="1000"
step="1000"
:value="form.goal_max_tokens || 100000"
@change="
personalization.updateField({
key: 'goal_max_tokens',
value: clampGoalMaxTokens($event.target.value)
})
"
/>
</div>
</section>
<section
v-else-if="activeTab === 'context'"
key="context"
class="settings-page"
>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">最近对话提示</span
><span class="settings-row-desc"
>把当前工作区最近若干个非空对话注入系统提示词</span
></span
><input
type="checkbox"
:checked="form.recent_conversations_prompt_enabled"
@change="
personalization.updateField({
key: 'recent_conversations_prompt_enabled',
value: $event.target.checked
})
" /><span class="fancy-check" aria-hidden="true"
><svg viewBox="0 0 64 64">
<path
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
pathLength="575.0541381835938"
class="fancy-path"
></path></svg></span
></label>
<div
class="settings-input-row"
v-if="form.recent_conversations_prompt_enabled"
>
<span class="settings-row-title">最近对话注入数量</span>
<div class="settings-number-row">
<input
type="number"
:min="recentConversationsPromptLimitRange.min"
:max="recentConversationsPromptLimitRange.max"
:value="form.recent_conversations_prompt_limit"
@input="handleRecentConversationsPromptLimitInput"
@blur="commitRecentConversationsPromptLimitInput"
/><button type="button" @click="restoreRecentConversationsPromptLimit">
恢复默认
</button>
</div>
</div>
<div class="settings-group-block">
<div class="settings-group-title">
<span class="settings-row-title">上下文压缩策略</span
><span class="settings-row-desc"
>可分别控制自动浅层压缩与自动深层压缩</span
>
</div>
<label class="settings-toggle-row inner"
><span class="settings-row-copy"
><span class="settings-row-title">自动浅层压缩</span></span
><input
type="checkbox"
:checked="form.auto_shallow_compress_enabled"
@change="
personalization.updateField({
key: 'auto_shallow_compress_enabled',
value: $event.target.checked
})
" /><span class="fancy-check" aria-hidden="true"
><svg viewBox="0 0 64 64">
<path
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
pathLength="575.0541381835938"
class="fancy-path"
></path></svg></span
></label>
<label class="settings-toggle-row inner"
><span class="settings-row-copy"
><span class="settings-row-title">自动深层压缩</span></span
><input
type="checkbox"
:checked="form.auto_deep_compress_enabled"
@change="
personalization.updateField({
key: 'auto_deep_compress_enabled',
value: $event.target.checked
})
" /><span class="fancy-check" aria-hidden="true"
><svg viewBox="0 0 64 64">
<path
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
pathLength="575.0541381835938"
class="fancy-path"
></path></svg></span
></label>
<div class="settings-compression-grid">
<label
><span>浅压缩触发上下文</span
><input
type="number"
:value="form.shallow_compress_trigger_tokens ?? ''"
@input="
handleCompressionNumberInput(
'shallow_compress_trigger_tokens',
$event
)
"
/></label>
<label
><span>浅压缩保留最近工具数</span
><input
type="number"
:value="form.shallow_compress_keep_recent_tools ?? ''"
@input="
handleCompressionNumberInput(
'shallow_compress_keep_recent_tools',
$event
)
"
/></label>
<label
><span>浅压缩保留当前轮工具数</span
><input
type="number"
:value="form.shallow_compress_keep_user_turn_tools ?? ''"
@input="
handleCompressionNumberInput(
'shallow_compress_keep_user_turn_tools',
$event
)
"
/></label>
<label
><span>每轮最大替换数量</span
><input
type="number"
:value="form.shallow_compress_max_replace_per_round ?? ''"
@input="
handleCompressionNumberInput(
'shallow_compress_max_replace_per_round',
$event
)
"
/></label>
<label
><span>工具调用间隔触发</span
><input
type="number"
:value="form.shallow_compress_trigger_tool_calls_interval ?? ''"
@input="
handleCompressionNumberInput(
'shallow_compress_trigger_tool_calls_interval',
$event
)
"
/></label>
<label
><span>深压缩触发上下文</span
><input
type="number"
:value="form.deep_compress_trigger_tokens ?? ''"
@input="
handleCompressionNumberInput('deep_compress_trigger_tokens', $event)
"
/></label>
</div>
<label class="settings-toggle-row inner"
><span class="settings-row-copy"
><span class="settings-row-title">深压缩直接注入</span
><span class="settings-row-desc"
>开启后直接注入历次压缩全文,关闭则只提示文件位置</span
></span
><input
type="checkbox"
:checked="form.deep_compress_form === 'inject'"
@change="
personalization.updateField({
key: 'deep_compress_form',
value: $event.target.checked ? 'inject' : 'file'
})
" /><span class="fancy-check" aria-hidden="true"
><svg viewBox="0 0 64 64">
<path
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
pathLength="575.0541381835938"
class="fancy-path"
></path></svg></span
></label>
<label class="settings-toggle-row inner"
><span class="settings-row-copy"
><span class="settings-row-title">手动压缩后等待用户</span
><span class="settings-row-desc"
>开启后手动压缩只插入引导语并等待输入,关闭则自动继续工作</span
></span
><input
type="checkbox"
:checked="form.deep_compress_behavior === 'wait'"
@change="
personalization.updateField({
key: 'deep_compress_behavior',
value: $event.target.checked ? 'wait' : 'continue'
})
" /><span class="fancy-check" aria-hidden="true"
><svg viewBox="0 0 64 64">
<path
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
pathLength="575.0541381835938"
class="fancy-path"
></path></svg></span
></label>
<div class="settings-inline-actions right">
<button
type="button"
class="settings-secondary-button"
@click="restoreCompressionDefaults"
>
恢复压缩默认值
</button>
</div>
</div>
</section>
<section v-else-if="activeTab === 'tools'" key="tools" class="settings-page">
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">静默禁用工具</span
><span class="settings-row-desc"
>禁用工具时不再注入提示消息,模型不会感知被禁用项</span
></span
><input
type="checkbox"
:checked="form.silent_tool_disable"
@change="
personalization.updateField({
key: 'silent_tool_disable',
value: $event.target.checked
})
" /><span class="fancy-check" aria-hidden="true"
><svg viewBox="0 0 64 64">
<path
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
pathLength="575.0541381835938"
class="fancy-path"
></path></svg></span
></label>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">工具意图提示</span
><span class="settings-row-desc"
>调用工具时先用简短文字告诉你要做什么</span
></span
><input
type="checkbox"
:checked="form.tool_intent_enabled"
@change="
personalization.updateField({
key: 'tool_intent_enabled',
value: $event.target.checked
})
" /><span class="fancy-check" aria-hidden="true"
><svg viewBox="0 0 64 64">
<path
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
pathLength="575.0541381835938"
class="fancy-path"
></path></svg></span
></label>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">Skill 提示系统</span
><span class="settings-row-desc"
>检测到特定关键词时自动提示模型阅读相关 skill</span
></span
><input
type="checkbox"
:checked="form.skill_hints_enabled"
@change="
personalization.updateField({
key: 'skill_hints_enabled',
value: $event.target.checked
})
" /><span class="fancy-check" aria-hidden="true"
><svg viewBox="0 0 64 64">
<path
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
pathLength="575.0541381835938"
class="fancy-path"
></path></svg></span
></label>
<div class="settings-group-block">
<div class="settings-group-title">
<span class="settings-row-title">强约束系统</span
><span class="settings-row-desc"
>仅对个人空间中已启用的 skill 生效</span
>
</div>
<label class="settings-toggle-row inner"
><span class="settings-row-title">Terminal 系列工具</span
><input
type="checkbox"
:checked="form.skill_strict_terminal_enabled"
@change="
personalization.updateField({
key: 'skill_strict_terminal_enabled',
value: $event.target.checked
})
" /><span class="fancy-check" aria-hidden="true"
><svg viewBox="0 0 64 64">
<path
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
pathLength="575.0541381835938"
class="fancy-path"
></path></svg></span
></label>
<label class="settings-toggle-row inner"
><span class="settings-row-title">子智能体系列工具</span
><input
type="checkbox"
:checked="form.skill_strict_sub_agent_enabled"
@change="
personalization.updateField({
key: 'skill_strict_sub_agent_enabled',
value: $event.target.checked
})
" /><span class="fancy-check" aria-hidden="true"
><svg viewBox="0 0 64 64">
<path
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
pathLength="575.0541381835938"
class="fancy-path"
></path></svg></span
></label>
<label class="settings-toggle-row inner"
><span class="settings-row-title">run_command 前台模式</span
><input
type="checkbox"
:checked="form.skill_strict_run_command_foreground_enabled"
@change="
personalization.updateField({
key: 'skill_strict_run_command_foreground_enabled',
value: $event.target.checked
})
" /><span class="fancy-check" aria-hidden="true"
><svg viewBox="0 0 64 64">
<path
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
pathLength="575.0541381835938"
class="fancy-path"
></path></svg></span
></label>
<label class="settings-toggle-row inner"
><span class="settings-row-title">run_command 后台模式</span
><input
type="checkbox"
:checked="form.skill_strict_run_command_background_enabled"
@change="
personalization.updateField({
key: 'skill_strict_run_command_background_enabled',
value: $event.target.checked
})
" /><span class="fancy-check" aria-hidden="true"
><svg viewBox="0 0 64 64">
<path
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
pathLength="575.0541381835938"
class="fancy-path"
></path></svg></span
></label>
</div>
<div class="settings-group-block" v-if="skillsCatalog.length">
<div class="settings-group-title">
<span class="settings-row-title">可用 Skills</span
><span class="settings-row-desc"
>勾选后会注入 system prompt并同步到工作区的 skills/ 目录</span
>
</div>
<div class="settings-check-grid">
<label
v-for="skill in skillsCatalog"
:key="skill.id"
class="settings-toggle-row inner"
><span class="settings-row-title">{{ skill.label }}</span
><input
type="checkbox"
:checked="form.enabled_skills.includes(skill.id)"
@change="personalization.toggleSkill(skill.id)" /><span
class="fancy-check"
aria-hidden="true"
><svg viewBox="0 0 64 64">
<path
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
pathLength="575.0541381835938"
class="fancy-path"
></path></svg></span
></label>
</div>
</div>
<div class="settings-group-block" v-if="toolCategories.length">
<div class="settings-group-title">
<span class="settings-row-title">默认禁用工具类别</span
><span class="settings-row-desc"
>选择后,这些类别在新任务中保持关闭</span
>
</div>
<div class="settings-check-grid">
<label
v-for="category in toolCategories"
:key="category.id"
class="settings-toggle-row inner"
><span class="settings-row-title">{{ category.label }}</span
><input
type="checkbox"
:checked="form.disabled_tool_categories.includes(category.id)"
@change="toggleCategory(category.id)" /><span
class="fancy-check"
aria-hidden="true"
><svg viewBox="0 0 64 64">
<path
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
pathLength="575.0541381835938"
class="fancy-path"
></path></svg></span
></label>
</div>
</div>
</section>
<section
v-else-if="activeTab === 'files'"
key="files"
class="settings-page"
data-tutorial="personal-page-image"
>
<div class="settings-select-row">
<span class="settings-row-copy"
><span class="settings-row-title">图片压缩</span
><span class="settings-row-desc"
>发送图片或调用 view_image 时自动等比缩放</span
></span
>
<div
class="settings-select-wrap"
:class="{ open: activeDropdown === 'image' }"
@click.stop
>
<button
type="button"
class="settings-select-button"
@click="toggleDropdown('image')"
>
{{ imageCompressionLabel }}
<span class="select-chevron" aria-hidden="true"></span>
</button>
<div
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
:style="activeDropdown ? floatingMenuStyle : undefined"
>
<button
v-for="option in imageCompressionOptions"
:key="option.id"
type="button"
class="settings-menu-option"
:class="{ selected: form.image_compression === option.id }"
@click="selectImageCompression(option.id)"
>
<strong>{{ option.label }}</strong
><span>{{ option.desc }}</span
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
</div>
</div>
</div>
</section>
<section
v-else-if="activeTab === 'data'"
key="data"
class="settings-page usage-summary-page"
data-tutorial="personal-page-usage"
>
<div class="usage-summary-card settings-stats-card">
<div class="usage-summary-header">
<div>
<p class="usage-summary-eyebrow">对话用量</p>
<h3>用量统计</h3>
<p class="usage-summary-desc">
累计统计所有对话的输入/输出 Token、对话数量、用户消息与工具调用
</p>
</div>
</div>
<div class="usage-summary-grid usage-summary-grid--tokens">
<div class="usage-summary-item">
<div class="label">累计输入</div>
<div class="value value--success">
{{ formatTokenCount(usageSummary.total_input_tokens) }}
</div>
</div>
<div class="usage-summary-item">
<div class="label">累计输出</div>
<div class="value value--warning">
{{ formatTokenCount(usageSummary.total_output_tokens) }}
</div>
</div>
</div>
<div class="usage-summary-grid usage-summary-grid--counts">
<div class="usage-summary-item">
<div class="label">总对话数</div>
<div class="value">
{{ formatTokenCount(usageSummary.total_conversations) }}
</div>
</div>
<div class="usage-summary-item">
<div class="label">用户消息数</div>
<div class="value">
{{ formatTokenCount(usageSummary.total_user_messages) }}
</div>
</div>
<div class="usage-summary-item">
<div class="label">工具调用次数</div>
<div class="value">
{{ formatTokenCount(usageSummary.total_tools) }}
</div>
</div>
</div>
<div class="usage-summary-meta">
<span v-if="usageError" class="usage-summary-error">{{ usageError }}</span
><span v-else-if="usageLoading">正在同步最新统计...</span
><span v-else>最近更新:{{ usageUpdatedText }}</span
><button
type="button"
class="usage-summary-refresh"
@click="fetchUsageSummary"
:disabled="usageLoading"
>
{{ usageLoading ? '刷新中...' : '刷新数据' }}
</button>
</div>
</div>
</section>
<section
v-else-if="activeTab === 'admin'"
key="admin"
class="settings-page admin-monitor-page"
>
<div class="settings-action-row" v-if="showMcpConfigEntry">
<span class="settings-row-copy"
><span class="settings-row-title">MCP 服务器管理</span
><span class="settings-row-desc"
>统一配置 MCP Server集中管理工具扩展能力</span
></span
><button
type="button"
class="settings-secondary-button"
@click="openMcpConfig"
>
打开
</button>
</div>
<div class="settings-action-row">
<span class="settings-row-copy"
><span class="settings-row-title">管理员监控入口</span
><span class="settings-row-desc"
>展示配额趋势、容器运行、项目存储与上传安全</span
></span
><button
type="button"
class="settings-secondary-button"
@click="openAdminPanel"
>
打开
</button>
</div>
<div class="settings-action-row">
<span class="settings-row-copy"
><span class="settings-row-title">API 管理</span
><span class="settings-row-desc">管理 API 调用与账户</span></span
><button
type="button"
class="settings-secondary-button"
@click="openApiAdmin"
>
打开
</button>
</div>
<div class="settings-action-row">
<span class="settings-row-copy"
><span class="settings-row-title">自定义工具管理</span
><span class="settings-row-desc"
>在线创建/编辑自定义工具文件</span
></span
><button
type="button"
class="settings-secondary-button"
@click="openCustomTools"
>
打开
</button>
</div>
</section>
</transition>
</div>
<div class="settings-save-bar">
<div class="personal-status-group">
<transition name="personal-status-fade"
><span class="status success" v-if="status">{{ status }}</span></transition
><transition name="personal-status-fade"
><span class="status error" v-if="error">{{ error }}</span></transition
>
</div>
<button type="submit" class="settings-primary-button" :disabled="saving">
{{ saving ? '保存中...' : '保存设置' }}
</button>
</div>
</section>
</div>
</form>
</div>
<div class="personalization-loading" v-else>正在加载个性化配置...</div>
</div>
</div>
</transition>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted, nextTick, onBeforeUnmount } from 'vue';
import { storeToRefs } from 'pinia';
import { usePersonalizationStore } from '@/stores/personalization';
import { useResourceStore } from '@/stores/resource';
import { useUiStore, persistDefaultHideWorkspace } from '@/stores/ui';
import { usePolicyStore } from '@/stores/policy';
import { useTutorialStore } from '@/stores/tutorial';
import { useModelStore } from '@/stores/model';
import { formatTokenCount } from '@/utils/formatters';
import { ICONS } from '@/utils/icons';
import { useTheme } from '@/utils/theme';
import type { ThemeKey } from '@/utils/theme';
defineOptions({ name: 'PersonalizationDrawer' });
const personalization = usePersonalizationStore();
const resourceStore = useResourceStore();
const uiStore = useUiStore();
const tutorialStore = useTutorialStore();
const {
visible,
loading,
form,
tonePresets,
newConsideration,
maxConsiderations,
status,
error,
saving,
toggleUpdating,
toolCategories,
skillsCatalog,
thinkingIntervalDefault,
thinkingIntervalRange,
recentConversationsPromptLimitRange,
experiments
} = storeToRefs(personalization);
// ---- 目标模式设置辅助 ----
const goalTokenLimitEnabled = computed(
() => typeof form.value.goal_max_tokens === 'number' && form.value.goal_max_tokens > 0
);
const clampGoalMaxTurns = (raw: any): number => {
const n = Math.round(Number(raw));
if (!Number.isFinite(n)) return 5;
return Math.min(100, Math.max(1, n));
};
const clampGoalMaxTokens = (raw: any): number => {
const n = Math.round(Number(raw));
if (!Number.isFinite(n)) return 100000;
return Math.min(100000000, Math.max(1000, n));
};
const toggleGoalTokenLimit = (enabled: boolean) => {
personalization.updateField({
key: 'goal_max_tokens',
value: enabled ? form.value.goal_max_tokens || 100000 : null
});
};
type IconKey = keyof typeof ICONS;
type PersonalTab =
| 'general'
| 'preferences'
| 'model'
| 'appearance'
| 'workspace'
| 'context'
| 'tools'
| 'files'
| 'data'
| 'admin';
const baseTabs = [
{ id: 'general', label: '常规', icon: 'settings' },
{ id: 'preferences', label: '个性化', icon: 'userPen' },
{ id: 'model', label: '模型与思考', icon: 'brainCog' },
{ id: 'appearance', label: '外观与显示', icon: 'monitor' },
{ id: 'workspace', label: '工作区与权限', icon: 'folder' },
{ id: 'context', label: '上下文', icon: 'chatBubble' },
{ id: 'tools', label: '工具与 Skills', icon: 'wrench' },
{ id: 'files', label: '文件与图片', icon: 'file' },
{ id: 'data', label: '数据管理', icon: 'layers' }
] as const satisfies ReadonlyArray<{ id: PersonalTab; label: string; icon: IconKey }>;
const sessionRole = ref('');
const sessionHostMode = ref(false);
const isAdmin = computed(() => {
const quotaRole = (resourceStore.usageQuota.role || '').toLowerCase();
const loginRole = (sessionRole.value || '').toLowerCase();
return quotaRole === 'admin' || loginRole === 'admin';
});
const isHostMode = computed(() => {
const mode = (resourceStore.containerStatus?.mode || '').toLowerCase();
if (mode) {
return mode === 'host';
}
return sessionHostMode.value;
});
const showMcpConfigEntry = computed(() => isAdmin.value && isHostMode.value);
const isAppShell = computed(() => {
if (typeof window === 'undefined') return false;
const params = new URLSearchParams(window.location.search);
if (params.has('app_shell')) return true;
return Boolean((window as any)?.AndroidThemeBridge);
});
const personalTabs = computed(() => {
const tabs: Array<{ id: PersonalTab; label: string; icon: IconKey }> = [...baseTabs];
if (isAdmin.value) {
tabs.push({ id: 'admin', label: '管理员', icon: 'wrench' });
}
return tabs;
});
const activeTab = ref<PersonalTab>('general');
const activeDropdown = ref<string | null>(null);
const floatingMenuStyle = ref<Record<string, string>>({});
const activeTabLabel = computed(() => {
return personalTabs.value.find((tab) => tab.id === activeTab.value)?.label || '常规';
});
const updateFloatingMenuPosition = async () => {
if (!activeDropdown.value || typeof window === 'undefined') {
floatingMenuStyle.value = {};
return;
}
await nextTick();
const button = document.querySelector<HTMLElement>(
'.settings-select-wrap.open .settings-select-button'
);
if (!button) {
return;
}
const rect = button.getBoundingClientRect();
const menuWidth = Math.min(300, Math.max(240, window.innerWidth - 32));
const left = Math.max(16, Math.min(rect.right - menuWidth, window.innerWidth - menuWidth - 16));
const top = Math.max(16, Math.min(rect.bottom + 10, window.innerHeight - 80));
floatingMenuStyle.value = {
position: 'fixed',
top: `${Math.round(top)}px`,
left: `${Math.round(left)}px`,
right: 'auto',
width: `${Math.round(menuWidth)}px`,
zIndex: '2147483647'
};
};
const toggleDropdown = async (key: string) => {
activeDropdown.value = activeDropdown.value === key ? null : key;
await updateFloatingMenuPosition();
};
const closeDropdown = () => {
activeDropdown.value = null;
floatingMenuStyle.value = {};
};
const settingsTabIconStyle = (icon: IconKey) => ({
'--icon-src': `url(${ICONS[icon]})`
});
onMounted(() => {
window.addEventListener('resize', updateFloatingMenuPosition);
window.addEventListener('scroll', updateFloatingMenuPosition, true);
});
onBeforeUnmount(() => {
window.removeEventListener('resize', updateFloatingMenuPosition);
window.removeEventListener('scroll', updateFloatingMenuPosition, true);
});
type RunModeValue = 'fast' | 'thinking' | 'deep' | null;
type PermissionModeValue = 'readonly' | 'approval' | 'auto_approval' | 'unrestricted';
type CompressionField =
| 'shallow_compress_trigger_tokens'
| 'shallow_compress_keep_recent_tools'
| 'shallow_compress_keep_user_turn_tools'
| 'shallow_compress_max_replace_per_round'
| 'shallow_compress_trigger_tool_calls_interval'
| 'deep_compress_trigger_tokens';
const runModeOptions: Array<{
id: string;
label: string;
desc: string;
value: RunModeValue;
badge?: string;
}> = [
{ id: 'fast', label: '快速模式', desc: '追求响应速度,跳过思考模型', value: 'fast' },
{ id: 'thinking', label: '思考模式', desc: '首轮回复会先输出思考过程', value: 'thinking' },
{ id: 'deep', label: '深度思考', desc: '整轮对话都使用思考模型', value: 'deep' }
];
const permissionModeOptions: Array<{
id: PermissionModeValue;
label: string;
desc: string;
}> = [
{ id: 'readonly', label: '只读', desc: '仅允许读取/检索类工具,修改操作将被拒绝' },
{ id: 'approval', label: '批准', desc: '对工作区文件进行修改的工具需人工批准后执行' },
{
id: 'auto_approval',
label: '自动审核',
desc: '工作区内写入直通;高风险操作由后台审核智能体自动审批'
},
{ id: 'unrestricted', label: '无限制', desc: '工具按常规流程直接执行' }
];
const policyStore = usePolicyStore();
const modelStore = useModelStore();
const filteredModelOptions = computed(() =>
(modelStore.models || []).map((opt: any) => {
const multimodal = String(opt.multimodal || 'none');
return {
id: opt.key,
value: opt.key,
label: opt.label,
desc: opt.description || '',
badge: multimodal === 'image,video' ? '图文' : opt.deepOnly ? '深度思考' : undefined,
deepOnly: !!opt.deepOnly,
fastOnly: !!opt.fastOnly,
disabled: policyStore.disabledModelSet.has(opt.key)
};
})
);
const thinkingPresets = [
{ id: 'low', label: '低', value: 10 },
{ id: 'medium', label: '中', value: 5 },
{ id: 'high', label: '高', value: 3 }
];
const imageCompressionOptions = [
{ id: 'original', label: '原图', desc: '不压缩' },
{ id: '1080p', label: '1080p', desc: '最长边不超过 1080p 等比缩放' },
{ id: '720p', label: '720p', desc: '最长边不超过 720p 等比缩放' },
{ id: '540p', label: '540p', desc: '最长边不超过 540p 等比缩放' }
] as const;
const defaultModelLabel = computed(() => {
return (
filteredModelOptions.value.find((option: any) => option.value === form.value.default_model)
?.label || '未设置'
);
});
const runModeLabel = computed(() => {
return runModeOptions.find((option) => isRunModeActive(option.value))?.label || '未设置';
});
const permissionModeLabel = computed(() => {
return (
permissionModeOptions.find((option) => option.id === form.value.default_permission_mode)
?.label || '未设置'
);
});
const imageCompressionLabel = computed(() => {
return (
imageCompressionOptions.find((option) => option.id === form.value.image_compression)?.label ||
'未设置'
);
});
const communicationStyleLabel = computed(() => {
return form.value.communication_style === 'human_like' ? '拟人' : '默认';
});
const currentBlockDisplayMode = computed(() => experiments.value.blockDisplayMode);
const blockDisplayLabel = computed(() => {
return (
blockDisplayOptions.find((option) => option.value === currentBlockDisplayMode.value)?.label ||
'堆叠动画'
);
});
const currentCompactMessageDisplay = computed(
() => form.value.compact_message_display || 'full'
);
const compactMessageDisplayLabel = computed(() => {
return (
compactMessageDisplayOptions.find(
(option) => option.value === currentCompactMessageDisplay.value
)?.label || '完整信息'
);
});
const usageSummary = ref({
total_input_tokens: 0,
total_output_tokens: 0,
total_conversations: 0,
total_user_messages: 0,
total_tools: 0
});
const usageLoading = ref(false);
const usageError = ref('');
const usageUpdatedAt = ref<string | null>(null);
const usageUpdatedText = computed(() => {
if (!usageUpdatedAt.value) {
return '尚未刷新';
}
const date = new Date(usageUpdatedAt.value);
if (Number.isNaN(date.getTime())) {
return '时间未知';
}
return date.toLocaleString('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
});
const appCurrentVersionCode = ref<number | null>(null);
const appCurrentVersionName = ref<string>('');
const appUpdateChecking = ref(false);
const appUpdateError = ref('');
const appUpdateCheckedAt = ref<string | null>(null);
const appUpdateInfo = ref<{
latestVersionCode: number;
latestVersionName: string;
apkUrl: string;
fileSizeBytes: number;
changelog?: string;
publishedAt?: string | null;
hasUpdate?: boolean | null;
} | null>(null);
if (typeof window !== 'undefined') {
const params = new URLSearchParams(window.location.search);
const vcRaw = params.get('app_vc') || '';
const vnRaw = params.get('app_vn') || '';
const bridge = (window as any)?.AndroidThemeBridge;
const bridgeVcRaw =
bridge && typeof bridge.getAppVersionCode === 'function'
? String(bridge.getAppVersionCode() || '')
: '';
const bridgeVnRaw =
bridge && typeof bridge.getAppVersionName === 'function'
? String(bridge.getAppVersionName() || '')
: '';
// App 端优先使用 Bridge 实时版本,避免 WebView 恢复旧 URL 参数导致显示过期版本号
const finalVcRaw = bridgeVcRaw || vcRaw;
const finalVnRaw = bridgeVnRaw || vnRaw;
appCurrentVersionCode.value = /^\d+$/.test(finalVcRaw) ? Number(finalVcRaw) : null;
appCurrentVersionName.value = finalVnRaw || '';
}
const hydrateAppVersionFromBridge = () => {
if (typeof window === 'undefined') return;
const bridge = (window as any)?.AndroidThemeBridge;
if (!bridge) return;
try {
const vcRaw =
typeof bridge.getAppVersionCode === 'function'
? String(bridge.getAppVersionCode() || '')
: '';
const vnRaw =
typeof bridge.getAppVersionName === 'function'
? String(bridge.getAppVersionName() || '')
: '';
if (vcRaw && /^\d+$/.test(vcRaw)) {
appCurrentVersionCode.value = Number(vcRaw);
}
if (vnRaw) {
appCurrentVersionName.value = vnRaw;
}
} catch {
// ignore bridge read failures
}
};
const appCurrentVersionText = computed(() => {
return appCurrentVersionName.value || '未知';
});
const appHasUpdate = computed(() => {
if (!appUpdateInfo.value) return false;
if (typeof appUpdateInfo.value.hasUpdate === 'boolean') return appUpdateInfo.value.hasUpdate;
if (appCurrentVersionCode.value == null) return false;
return Number(appUpdateInfo.value.latestVersionCode || 0) > appCurrentVersionCode.value;
});
const appDownloadUrl = computed(() => appUpdateInfo.value?.apkUrl || '');
const appUpdateStateText = computed(() => {
if (!appUpdateInfo.value) return '未检查';
return appHasUpdate.value ? '发现新版本' : '已是最新';
});
const appUpdateCheckedText = computed(() => {
if (!appUpdateCheckedAt.value) return '尚未检查更新';
const d = new Date(appUpdateCheckedAt.value);
if (Number.isNaN(d.getTime())) return '刚刚检查过';
return `最近检查:${d.toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })}`;
});
const fetchUsageSummary = async () => {
if (usageLoading.value) {
return;
}
usageLoading.value = true;
usageError.value = '';
try {
const response = await fetch('/api/conversations/statistics');
const payload = await response.json();
if (!response.ok || !payload.success) {
throw new Error(payload.error || payload.message || '获取用量统计失败');
}
const data = payload.data || {};
const tokenStats = data.token_statistics || {};
usageSummary.value = {
total_input_tokens: Number(tokenStats.total_input_tokens || 0),
total_output_tokens: Number(tokenStats.total_output_tokens || 0),
total_conversations: Number(data.total_conversations || 0),
total_user_messages: Number(data.total_user_messages || 0),
total_tools: Number(data.total_tools || 0)
};
usageUpdatedAt.value = new Date().toISOString();
} catch (error: any) {
usageError.value = error?.message || '获取用量统计失败';
} finally {
usageLoading.value = false;
}
};
const checkAppUpdate = async () => {
if (appUpdateChecking.value) return;
hydrateAppVersionFromBridge();
appUpdateChecking.value = true;
appUpdateError.value = '';
try {
const params = new URLSearchParams();
if (appCurrentVersionCode.value != null) {
params.set('currentVersionCode', String(appCurrentVersionCode.value));
}
const suffix = params.toString() ? `?${params.toString()}` : '';
const resp = await fetch(`/api/app/version${suffix}`);
const payload = await resp.json();
if (!resp.ok || !payload?.success) {
throw new Error(payload?.error || '检查更新失败');
}
const data = payload?.data || {};
appUpdateInfo.value = {
latestVersionCode: Number(data.latestVersionCode || 0),
latestVersionName: String(data.latestVersionName || ''),
apkUrl: String(data.apkUrl || ''),
fileSizeBytes: Number(data.fileSizeBytes || 0),
changelog: String(data.changelog || ''),
publishedAt: data.publishedAt || null,
hasUpdate: typeof data.hasUpdate === 'boolean' ? data.hasUpdate : null
};
appUpdateCheckedAt.value = new Date().toISOString();
} catch (error: any) {
appUpdateError.value = error?.message || '检查更新失败';
} finally {
appUpdateChecking.value = false;
}
};
const downloadLatestApp = () => {
const url = appDownloadUrl.value;
if (!url) {
appUpdateError.value = '未拿到下载地址';
return;
}
window.location.href = url;
};
onMounted(async () => {
hydrateAppVersionFromBridge();
try {
const resp = await fetch('/api/session-status', { credentials: 'same-origin' });
if (!resp.ok) return;
const payload = await resp.json();
const snapshot = payload?.session || {};
sessionRole.value = String(snapshot.role || '');
sessionHostMode.value = !!snapshot.host_mode;
} catch (_err) {
sessionRole.value = '';
}
});
watch(
() => [activeTab.value, visible.value],
([tab, isVisible]) => {
if (isVisible && tab === 'data') {
fetchUsageSummary();
}
if (
isVisible &&
tab === 'general' &&
isAppShell.value &&
!appUpdateInfo.value &&
!appUpdateChecking.value
) {
checkAppUpdate();
}
}
);
const setActiveTab = (tab: PersonalTab) => {
activeTab.value = tab;
closeDropdown();
};
const startTutorial = () => {
fetch('/api/tutorial-status', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify({ tutorial_completed: true })
}).catch(() => {});
tutorialStore.markCompleted();
personalization.closeDrawer();
window.setTimeout(() => {
tutorialStore.startTutorial();
}, 320);
};
const applyThinkingPreset = (value: number) => {
personalization.setThinkingInterval(value);
};
const isRunModeActive = (value: RunModeValue) => {
if (value === null) {
return !form.value.default_run_mode;
}
return form.value.default_run_mode === value;
};
const setDefaultRunMode = (value: RunModeValue) => {
if (checkModeModelConflict(value, form.value.default_model)) {
return;
}
personalization.setDefaultRunMode(value);
};
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;
}
personalization.setDefaultModel(value);
};
const setDefaultPermissionMode = (value: PermissionModeValue) => {
personalization.setDefaultPermissionMode(value);
};
const setCommunicationStyle = (value: 'default' | 'human_like') => {
personalization.setCommunicationStyle(value);
};
const selectDefaultModel = (value: string) => {
setDefaultModel(value);
closeDropdown();
};
const selectDefaultRunMode = (value: RunModeValue) => {
setDefaultRunMode(value);
closeDropdown();
};
const selectDefaultPermissionMode = (value: PermissionModeValue) => {
setDefaultPermissionMode(value);
closeDropdown();
};
const selectCommunicationStyle = (value: 'default' | 'human_like') => {
setCommunicationStyle(value);
closeDropdown();
};
const selectImageCompression = (value: (typeof imageCompressionOptions)[number]['id']) => {
personalization.setImageCompression(value);
closeDropdown();
};
const selectBlockDisplayMode = (mode: 'traditional' | 'stacked' | 'minimal') => {
handleBlockDisplayModeChange(mode);
closeDropdown();
};
const checkModeModelConflict = (mode: RunModeValue, model: string | null): boolean => {
const found = (filteredModelOptions.value || []).find((item: any) => item.value === model);
const warnings: string[] = [];
if (found?.deepOnly && mode && mode !== 'deep') {
warnings.push(`${found.label} 仅支持深度思考模式,已保持原设置。`);
}
if (found?.fastOnly && mode && mode !== 'fast') {
warnings.push(`${found.label} 仅支持快速模式,已保持原设置。`);
}
if (warnings.length) {
uiStore.pushToast({
title: '模型/思考模式不兼容',
message: warnings.join(' '),
type: 'warning',
duration: 6000
});
return true;
}
return false;
};
const handleThinkingInput = (event: Event) => {
const target = event.target as HTMLInputElement;
if (!target.value) {
personalization.setThinkingInterval(null);
return;
}
const parsed = Number(target.value);
personalization.setThinkingInterval(Number.isNaN(parsed) ? null : parsed);
};
const restoreThinkingInterval = () => {
personalization.setThinkingInterval(null);
};
const handleRecentConversationsPromptLimitInput = (event: Event) => {
const target = event.target as HTMLInputElement | null;
if (!target) {
return;
}
personalization.updateField({
key: 'recent_conversations_prompt_limit',
value: target.value
});
};
const commitRecentConversationsPromptLimitInput = (event: Event) => {
const target = event.target as HTMLInputElement | null;
if (!target || !target.value) {
personalization.setRecentConversationsPromptLimit(null);
return;
}
const parsed = Number(target.value);
personalization.setRecentConversationsPromptLimit(Number.isNaN(parsed) ? null : parsed);
};
const restoreRecentConversationsPromptLimit = () => {
personalization.setRecentConversationsPromptLimit(null);
};
const handleCompressionNumberInput = (key: CompressionField, event: Event) => {
const target = event.target as HTMLInputElement | null;
if (!target || !target.value) {
personalization.updateField({ key, value: null });
return;
}
const parsed = Number(target.value);
if (!Number.isFinite(parsed)) {
personalization.updateField({ key, value: null });
return;
}
personalization.updateField({ key, value: Math.round(parsed) });
};
const restoreCompressionDefaults = () => {
personalization.updateField({ key: 'shallow_compress_trigger_tokens', value: null });
personalization.updateField({ key: 'shallow_compress_keep_recent_tools', value: null });
personalization.updateField({ key: 'shallow_compress_max_replace_per_round', value: null });
personalization.updateField({ key: 'shallow_compress_trigger_tool_calls_interval', value: null });
personalization.updateField({ key: 'deep_compress_trigger_tokens', value: null });
};
const isPresetActive = (value: number) => {
if (
form.value.thinking_interval === null ||
typeof form.value.thinking_interval === 'undefined'
) {
return value === thinkingIntervalDefault.value;
}
return form.value.thinking_interval === value;
};
const toggleCategory = (categoryId: string) => {
personalization.toggleDefaultToolCategory(categoryId);
};
const blockDisplayOptions = [
{
id: 'traditional',
label: '传统列表',
desc: '按时间顺序垂直排列,经典样式',
value: 'traditional' as const
},
{
id: 'stacked',
label: '堆叠动画',
desc: '超过 6 条自动收纳为"更多",动态展示',
value: 'stacked' as const,
badge: '推荐'
},
{
id: 'minimal',
label: '极简模式',
desc: '摘要行 + 无缝展开,流式输出效果',
value: 'minimal' as const,
badge: '新'
}
];
const handleBlockDisplayModeChange = (mode: 'traditional' | 'stacked' | 'minimal') => {
personalization.setBlockDisplayMode(mode);
};
const compactMessageDisplayOptions = [
{
id: 'full',
label: '完整信息',
desc: '审核、子智能体等系统消息显示完整原始内容',
value: 'full' as const
},
{
id: 'brief',
label: '简略信息',
desc: '用一行横线概要替代系统消息',
value: 'brief' as const
}
];
const selectCompactMessageDisplay = (mode: 'full' | 'brief') => {
personalization.setCompactMessageDisplay(mode);
closeDropdown();
};
const openAdminPanel = () => {
window.open('/admin/monitor', '_blank', 'noopener');
personalization.closeDrawer();
};
const openCustomTools = () => {
window.open('/admin/custom-tools', '_blank', 'noopener');
personalization.closeDrawer();
};
const openApiAdmin = () => {
window.open('/admin/api', '_blank', 'noopener');
personalization.closeDrawer();
};
const openMcpConfig = () => {
window.open('/admin/policy', '_blank', 'noopener');
personalization.closeDrawer();
};
// ===== 主题切换 =====
const { setTheme, loadTheme } = useTheme();
const themeOptions: Array<{ id: ThemeKey; label: string; desc: string; swatches: string[] }> = [
{
id: 'classic',
label: '经典',
desc: '米色质感,柔和高对比',
swatches: ['#eeece2', '#f7f3ea', '#da7756']
},
{
id: 'light',
label: '明亮',
desc: '纯白底色 + 优雅灰,简洁清爽',
swatches: ['#ffffff', '#f7f7f8', '#6b7280']
},
{
id: 'dark',
label: '夜间',
desc: '深灰 + 黑,低亮度并保持彩色点缀',
swatches: ['#1a1a1a', '#2a2a2a', '#3a3a3a']
}
];
const activeTheme = ref<ThemeKey>(loadTheme());
const themeLabel = computed(() => {
return themeOptions.find((option) => option.id === activeTheme.value)?.label || '经典';
});
// 监听store中的theme变化同步到activeTheme用于从后端加载主题后更新UI
watch(
() => personalization.form.theme,
(newTheme) => {
if (newTheme && newTheme !== activeTheme.value) {
activeTheme.value = newTheme as ThemeKey;
setTheme(newTheme as ThemeKey);
}
},
{ immediate: true }
);
const selectThemeOption = (theme: ThemeKey) => {
applyThemeOption(theme);
closeDropdown();
};
const applyThemeOption = async (theme: ThemeKey) => {
activeTheme.value = theme;
setTheme(theme);
// 同步更新到store并保存到后端配置文件
personalization.updateField({ key: 'theme', value: theme });
try {
const resp = await fetch('/api/personalization', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ theme })
});
const result = await resp.json();
if (!resp.ok || !result.success) {
console.warn('保存主题到后端失败:', result.error);
}
} catch (error) {
console.warn('保存主题到后端失败:', error);
}
};
const applyDefaultHideWorkspaceOption = async (hidden: boolean) => {
const next = !!hidden;
persistDefaultHideWorkspace(next);
personalization.updateField({ key: 'default_hide_workspace', value: next });
if (next) {
uiStore.setWorkspaceCollapsed(true);
}
try {
const resp = await fetch('/api/personalization', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ default_hide_workspace: next })
});
const result = await resp.json();
if (!resp.ok || !result.success) {
console.warn('保存默认隐藏工作区失败:', result.error);
}
} catch (error) {
console.warn('保存默认隐藏工作区失败:', error);
}
};
</script>
<style scoped>
.settings-redesign-card {
--settings-floating-menu-bg: color-mix(in srgb, var(--surface-soft) 90%, var(--surface-raised));
--settings-floating-menu-hover: var(--hover-bg);
--settings-floating-menu-shadow: none;
--settings-tab-hover-bg: var(--theme-tab-active);
--settings-tab-active-bg: var(--theme-tab-active);
position: relative;
width: min(70vw, calc(100vw - 24px));
height: min(80vh, calc(100vh - 24px));
padding: 30px 42px 24px;
border-radius: 32px;
overflow: visible;
background: var(--theme-surface-soft);
border: 1px solid var(--theme-control-border);
box-shadow: none;
}
:global(html[data-theme='dark']) .settings-redesign-card,
:global(body[data-theme='dark']) .settings-redesign-card {
--settings-floating-menu-bg: var(--surface-panel);
--settings-floating-menu-hover: var(--hover-bg);
--settings-floating-menu-shadow: none;
--settings-tab-hover-bg: var(--hover-bg);
--settings-tab-active-bg: var(--hover-bg);
}
.settings-close-button {
position: absolute;
top: 27px;
left: 45px;
width: 42px;
height: 42px;
border: 0;
border-radius: 50%;
background: transparent;
color: var(--claude-text);
display: grid;
place-items: center;
cursor: pointer;
z-index: 3;
}
.settings-close-button:hover {
background: var(--hover-bg);
}
.settings-close-button svg {
width: 25px;
height: 25px;
stroke-width: 2.15;
}
.settings-redesign-body,
.settings-redesign-form {
height: 100%;
min-height: 0;
}
.settings-redesign-layout {
height: 100%;
display: grid;
grid-template-columns: minmax(190px, 2fr) minmax(0, 8fr);
column-gap: 34px;
min-height: 0;
}
.settings-redesign-tabs {
margin-top: 68px;
height: calc(100% - 68px);
padding-right: 28px;
border-right: 1px solid var(--theme-control-border);
display: flex;
flex-direction: column;
gap: 3px;
min-width: 0;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: none;
-ms-overflow-style: none;
}
.settings-redesign-tab {
width: 100%;
height: 47px;
flex: 0 0 auto;
border: 0;
border-radius: 12px;
background: transparent;
color: var(--claude-text);
padding: 0 13px;
display: flex;
align-items: center;
gap: 14px;
cursor: pointer;
text-align: left;
font-size: 16px;
letter-spacing: -0.01em;
}
.settings-redesign-tab:hover {
background: var(--settings-tab-hover-bg);
}
.settings-redesign-tab.active {
background: var(--settings-tab-active-bg);
font-weight: 640;
}
.settings-tab-icon {
--icon-size: 20px;
flex: 0 0 auto;
}
.settings-tab-icon--chat {
width: 30px;
height: 30px;
display: inline-flex;
align-items: center;
justify-content: center;
margin-left: -5px;
margin-right: -5px;
}
.settings-tab-icon--chat svg {
width: 30px;
height: 30px;
}
.settings-redesign-content {
min-width: 0;
min-height: 0;
height: 100%;
overflow: visible;
display: flex;
flex-direction: column;
position: relative;
}
.settings-redesign-content-header {
height: 46px;
flex: 0 0 46px;
display: flex;
align-items: center;
}
.settings-redesign-content-header h2 {
margin: 0;
color: var(--claude-text);
font-size: 28px;
line-height: 1;
letter-spacing: -0.045em;
font-weight: 730;
}
.settings-redesign-title-line {
height: 1px;
background: var(--theme-control-border);
margin: 22px 0 8px;
flex: 0 0 1px;
}
.settings-redesign-scroll {
min-height: 0;
flex: 1 1 auto;
overflow-y: auto;
overflow-x: visible;
padding-right: 18px;
padding-bottom: 86px;
scroll-padding-bottom: 86px;
scrollbar-width: none;
-ms-overflow-style: none;
}
.settings-redesign-tabs::-webkit-scrollbar,
.settings-redesign-scroll::-webkit-scrollbar,
.settings-floating-menu::-webkit-scrollbar,
.app-update-changelog-content::-webkit-scrollbar {
display: none;
}
.settings-floating-menu,
.app-update-changelog-content {
scrollbar-width: none;
-ms-overflow-style: none;
}
.settings-page {
max-width: 900px;
min-height: 0;
}
.settings-select-row,
.settings-input-row,
.settings-list-row,
.settings-action-row,
.settings-toggle-row {
min-height: 64px;
border-bottom: 1px solid var(--theme-control-border);
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 18px;
padding: 11px 0;
color: var(--claude-text);
}
.settings-list-row.tall,
.settings-input-row.stackable {
align-items: flex-start;
}
.settings-list-row.tall {
grid-template-columns: minmax(180px, 1fr) minmax(0, 360px);
}
.settings-row-copy {
min-width: 0;
display: block;
}
.settings-row-title {
display: block;
color: var(--claude-text);
font-size: 14px;
font-weight: 550;
letter-spacing: -0.01em;
line-height: 1.32;
}
.settings-row-desc {
display: block;
margin-top: 3px;
color: var(--claude-text-secondary);
font-size: 12.5px;
line-height: 1.38;
}
.settings-select-wrap {
position: relative;
display: inline-flex;
justify-self: end;
}
.settings-select-button {
width: max-content;
min-width: 0;
height: 36px;
border: 0;
border-radius: 12px;
background: transparent;
color: var(--claude-text);
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 0 8px 0 10px;
cursor: pointer;
font-size: 15px;
white-space: nowrap;
}
.settings-select-button:hover {
background: var(--hover-bg);
}
.settings-select-button .select-chevron {
width: 12px;
height: 12px;
display: inline-flex;
align-items: center;
justify-content: center;
transition: transform 0.15s ease;
}
.settings-select-button .select-chevron::before {
content: '';
width: 7px;
height: 7px;
border-right: 1.5px solid currentColor;
border-bottom: 1.5px solid currentColor;
transform: translateY(-1.5px) rotate(45deg);
}
.settings-select-wrap.open .settings-select-button .select-chevron {
transform: rotate(180deg);
}
.settings-floating-menu {
position: fixed;
right: auto;
top: auto;
width: 300px;
max-height: min(420px, 60vh);
overflow: auto;
padding: 6px;
border-radius: 20px;
background: var(--settings-floating-menu-bg);
border: 1px solid var(--theme-control-border);
box-shadow: var(--settings-floating-menu-shadow);
opacity: 1;
backdrop-filter: none;
z-index: 2147483647;
display: none;
}
.settings-select-wrap.open .settings-floating-menu {
display: block;
}
.settings-floating-menu.dark {
background: var(--surface-panel) !important;
box-shadow: none !important;
}
.settings-floating-menu.dark .settings-menu-option:hover {
background: var(--hover-bg) !important;
}
:global(html[data-theme='dark']) :global(.settings-floating-menu),
:global(body[data-theme='dark']) :global(.settings-floating-menu) {
background: var(--surface-panel) !important;
box-shadow: none !important;
}
:global(html[data-theme='dark']) :global(.settings-menu-option:hover),
:global(body[data-theme='dark']) :global(.settings-menu-option:hover) {
background: var(--hover-bg) !important;
}
.settings-menu-option {
width: 100%;
min-height: 38px;
border: 0;
border-radius: 12px;
background: transparent;
color: var(--claude-text);
display: grid;
grid-template-columns: minmax(0, 1fr) 22px;
row-gap: 0;
column-gap: 8px;
align-items: center;
padding: 7px 10px;
text-align: left;
cursor: pointer;
}
.settings-menu-option:hover {
background: var(--settings-floating-menu-hover);
}
.settings-menu-option:disabled {
opacity: 0.48;
cursor: not-allowed;
}
.settings-menu-option strong {
grid-column: 1;
grid-row: 1;
display: block;
color: var(--claude-text);
font-size: 15px;
font-weight: 550;
letter-spacing: -0.01em;
min-width: 0;
}
.settings-menu-option span {
grid-column: 1;
grid-row: 2;
display: block;
margin-top: 0;
color: var(--claude-text-secondary);
font-size: 11px;
line-height: 1.15;
min-width: 0;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.settings-menu-option svg {
grid-column: 2;
grid-row: 1 / span 2;
align-self: center;
width: 21px;
height: 21px;
fill: none;
stroke: var(--claude-text);
stroke-width: 2.25;
stroke-linecap: round;
stroke-linejoin: round;
opacity: 0;
}
.settings-menu-option.selected svg {
opacity: 1;
}
.settings-input-row input,
.settings-add-row input,
.settings-number-row input,
.settings-number-input,
.settings-compression-grid input {
height: 38px;
border: 1px solid var(--theme-control-border);
border-radius: 12px;
background: transparent;
color: var(--claude-text);
padding: 0 12px;
outline: none;
font-size: 13px;
}
.settings-number-input {
width: 120px;
text-align: right;
}
.settings-section-divider {
display: flex;
align-items: center;
margin: 18px 0 6px;
}
.settings-section-divider__label {
font-size: 12px;
font-weight: 600;
color: var(--claude-text-secondary);
letter-spacing: 0.04em;
}
.settings-section-divider::after {
content: '';
flex: 1;
height: 1px;
margin-left: 10px;
background: var(--theme-control-border);
}
.settings-input-row > input {
width: min(360px, 40vw);
text-align: right;
}
.settings-input-row input:focus,
.settings-add-row input:focus,
.settings-number-row input:focus,
.settings-number-input:focus,
.settings-compression-grid input:focus {
border-color: var(--claude-text-secondary);
box-shadow: none;
}
.settings-input-stack {
width: min(430px, 46vw);
display: grid;
gap: 8px;
justify-self: end;
}
.settings-input-stack.wide {
width: min(360px, 42vw);
}
.settings-input-stack input {
width: 100%;
}
.settings-chip-row {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.settings-chip-row.right {
justify-content: flex-end;
}
.settings-chip-row button,
.settings-number-row button,
.settings-add-row button,
.settings-secondary-button,
.settings-primary-button {
border: 1px solid var(--theme-control-border);
border-radius: 999px;
background: transparent;
color: var(--claude-text);
min-height: 32px;
padding: 0 12px;
font-size: 12px;
cursor: pointer;
}
.settings-chip-row button:hover,
.settings-number-row button:hover,
.settings-add-row button:hover,
.settings-secondary-button:hover {
background: var(--hover-bg);
}
.settings-chip-row button.active,
.settings-primary-button {
border-color: var(--claude-accent);
background: var(--claude-accent);
color: var(--on-accent);
}
.settings-secondary-button.danger {
color: var(--claude-warning);
}
.settings-inline-actions {
display: inline-flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
flex-wrap: wrap;
}
.settings-inline-actions.right {
margin-top: 10px;
}
.settings-mini-status {
color: var(--claude-text-secondary);
font-size: 12px;
}
.settings-mini-status.warning {
color: var(--claude-warning);
}
.settings-add-row,
.settings-number-row {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
}
.settings-add-row input,
.settings-number-row input {
min-width: 0;
flex: 1 1 auto;
}
.settings-consideration-list {
margin: 0;
padding: 0;
list-style: none;
display: grid;
gap: 6px;
}
.settings-consideration-list li {
min-height: 34px;
border: 1px solid var(--theme-control-border);
border-radius: 12px;
padding: 0 8px;
display: grid;
grid-template-columns: 18px minmax(0, 1fr) 28px;
gap: 8px;
align-items: center;
color: var(--claude-text);
font-size: 13px;
}
.settings-consideration-list button {
border: 0;
background: transparent;
color: var(--claude-text-secondary);
cursor: pointer;
}
.drag-handle {
color: var(--claude-text-secondary);
cursor: grab;
}
.settings-toggle-row {
cursor: pointer;
position: relative;
}
.settings-toggle-row input[type='checkbox'] {
position: absolute;
opacity: 0;
pointer-events: none;
}
.settings-toggle-row .fancy-check {
width: 28px;
height: 28px;
border-radius: 6px;
background: transparent;
display: inline-flex;
align-items: center;
justify-content: center;
box-shadow: none;
transition: opacity 0.2s ease;
}
.settings-toggle-row .fancy-check svg {
width: 22px;
height: 22px;
overflow: visible;
}
.fancy-path {
fill: none;
stroke: var(--claude-text-secondary);
stroke-width: 5;
stroke-linecap: round;
stroke-linejoin: round;
transition:
stroke-dasharray 0.5s ease,
stroke-dashoffset 0.5s ease,
stroke 0.2s ease;
stroke-dasharray: 241 9999999;
stroke-dashoffset: 0;
}
.settings-toggle-row input:checked + .fancy-check .fancy-path {
stroke: var(--claude-text-secondary);
stroke-dasharray: 70.5096664428711 9999999;
stroke-dashoffset: -262.2723388671875;
}
.settings-group-block {
border-bottom: 1px solid var(--theme-control-border);
padding: 14px 0 16px;
}
.settings-group-title {
margin-bottom: 6px;
}
.settings-toggle-row.inner {
min-height: 48px;
padding: 7px 0 7px 18px;
border-bottom: 0;
}
.settings-check-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0 26px;
}
.settings-compression-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px 14px;
margin-top: 10px;
}
.settings-compression-grid label {
display: grid;
gap: 5px;
color: var(--claude-text-secondary);
font-size: 12px;
}
.settings-save-bar {
position: absolute;
right: 18px;
bottom: 0;
left: 0;
min-height: 54px;
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
background: transparent;
pointer-events: none;
}
.settings-save-bar > * {
pointer-events: auto;
}
.personal-status-group {
min-height: 20px;
font-size: 12px;
}
.status.success {
color: var(--claude-success);
}
.status.error,
.usage-summary-error {
color: var(--claude-warning);
}
.usage-summary-page {
display: block;
padding: 0;
}
.settings-stats-card,
.usage-summary-card {
width: 100%;
max-width: 720px;
padding: 0;
min-height: 0;
display: flex;
flex-direction: column;
gap: 14px;
}
.usage-summary-header h3 {
margin: 4px 0 6px;
font-size: 20px;
}
.usage-summary-eyebrow {
margin: 0;
font-size: 11px;
color: var(--claude-accent-strong);
letter-spacing: 0.16em;
text-transform: uppercase;
}
.usage-summary-desc,
.usage-summary-note,
.usage-summary-meta {
margin: 0;
color: var(--claude-text-secondary);
font-size: 12px;
}
.usage-summary-grid {
display: grid;
gap: 10px;
}
.usage-summary-grid--tokens {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.usage-summary-grid--counts {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.usage-summary-item {
padding: 12px 14px;
border-radius: 14px;
border: 1px solid var(--theme-control-border);
background: transparent;
}
.usage-summary-item .label {
font-size: 12px;
color: var(--claude-text-secondary);
}
.usage-summary-item .value {
margin-top: 4px;
font-size: 20px;
font-weight: 600;
color: var(--claude-text);
}
.usage-summary-item .value--success {
color: var(--claude-success);
}
.usage-summary-item .value--warning {
color: var(--claude-warning);
}
.usage-summary-meta {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.usage-summary-refresh {
border: 1px solid var(--theme-control-border);
border-radius: 999px;
padding: 7px 14px;
background: transparent;
color: var(--claude-text);
font-size: 12px;
cursor: pointer;
}
.admin-monitor-page {
display: block;
padding: 0;
}
@media (max-width: 900px) {
.settings-redesign-card {
width: 100vw;
height: 100vh;
border-radius: 0;
padding: 30px 24px;
}
.settings-close-button {
left: 27px;
top: 20px;
}
.settings-redesign-layout {
grid-template-columns: 1fr;
gap: 0;
}
.settings-redesign-tabs {
margin-top: 68px;
height: auto;
padding-right: 0;
padding-bottom: 12px;
border-right: 0;
border-bottom: 1px solid var(--theme-control-border);
flex-direction: row;
overflow: auto;
scrollbar-width: none;
}
.settings-redesign-tabs::-webkit-scrollbar {
display: none;
}
.settings-redesign-tab {
width: auto;
white-space: nowrap;
}
.settings-redesign-content {
padding-top: 18px;
}
.settings-select-row,
.settings-input-row,
.settings-list-row,
.settings-action-row,
.settings-toggle-row {
grid-template-columns: minmax(0, 1fr);
gap: 8px;
}
.settings-select-wrap,
.settings-input-stack,
.settings-input-row > input {
justify-self: stretch;
width: 100%;
}
.settings-select-button {
margin-left: auto;
}
.settings-floating-menu {
width: min(340px, calc(100vw - 48px));
}
.settings-check-grid,
.settings-compression-grid,
.usage-summary-grid--tokens,
.usage-summary-grid--counts {
grid-template-columns: 1fr;
}
}
</style>