agent-Specialization/static/src/components/personalization/PersonalizationDrawer.vue
JOJO 4e1d08f106 fix(frontend): 修复多个前端交互问题
- 修复运行时 intent Unicode 转义显示异常(后端 extract_intent_from_partial 解码)
- 修复 ask_user 弹窗问题/说明不换行
- 修复刷新页面后等待回答提示不恢复(添加 fetchPendingUserQuestions 并在初始化路径调用)
- 修复回答问题弹窗长问题撑满窗口(header/context 限高滚动)
- 修复角色编辑器模型选择菜单定位与滚动
- 在 AGENTS.md 中再次强调前端调试日志必须统一筛选词

测试:/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/bin/python3.9 -m unittest test.test_server_refactor_smoke; npm run build
2026-07-15 00:31:13 +08:00

4317 lines
168 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">
<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-textarea-row">
<div class="settings-row-copy">
<span class="settings-row-title">回答时必须考虑的信息</span>
<span class="settings-row-desc">每次回答前都会参考这些背景信息</span>
</div>
<textarea
:value="form.considerations"
rows="6"
maxlength="2000"
placeholder="例如用户家有一只泰迪犬棕色公狗2014年开始养的..."
@input="personalization.updateConsiderations($event.target.value)"
@focus="personalization.clearFeedback()"
></textarea>
</div>
<label
v-if="currentBlockDisplayMode === 'stacked'"
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="stackedHideBorders"
@change="handleStackedHideBordersChange($event)"
/>
<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
v-if="currentBlockDisplayMode === 'minimal'"
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="minimalExpandHeightLimited"
@change="handleMinimalExpandHeightLimitedChange($event)"
/>
<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 === '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>
<button
type="button"
class="settings-menu-option"
:class="{ selected: form.communication_style === 'auto' }"
@click="selectCommunicationStyle('auto')"
>
<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>
<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 === 'conversation-continuity' }"
@click.stop
>
<button
type="button"
class="settings-select-button"
@click="toggleDropdown('conversation-continuity')"
>
{{ conversationContinuityLabel }}
<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.conversation_continuity === 'high' }"
@click="selectConversationContinuity('high')"
>
<strong>高</strong><span>更积极延续历史、记忆与项目背景</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.conversation_continuity === 'medium' }"
@click="selectConversationContinuity('medium')"
>
<strong>中</strong><span>当前对话优先,必要时参考历史</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.conversation_continuity === 'low' }"
@click="selectConversationContinuity('low')"
>
<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>
<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="uploadDiagnosticLog"
>
上传日志
</button>
</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.group_sidebar_by_workspace"
@change="
personalization.updateField({
key: 'group_sidebar_by_workspace',
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.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">显示助手状态形象</span
><span class="settings-row-desc"
>在欢迎页和对话末尾显示空闲、思考、工具调用等状态动画</span
></span
><input
type="checkbox"
:checked="form.show_status_avatar"
@change="
personalization.updateField({
key: 'show_status_avatar',
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>
<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_open_terminal_panel"
@change="
personalization.updateField({
key: 'auto_open_terminal_panel',
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>
<label
v-if="currentBlockDisplayMode === 'stacked'"
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="stackedHideBorders"
@change="handleStackedHideBordersChange($event)"
/>
<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
v-if="currentBlockDisplayMode === 'minimal'"
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="minimalExpandHeightLimited"
@change="handleMinimalExpandHeightLimitedChange($event)"
/>
<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 === '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.versioning_enabled_by_default"
@change="
personalization.updateField({
key: 'versioning_enabled_by_default',
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"
>浅备份只记录 AI 编辑的文件,完全备份会保存整个工作区快照</span
></span
>
<div
class="settings-select-wrap"
:class="{ open: activeDropdown === 'versioning-backup-mode' }"
@click.stop
>
<button
type="button"
class="settings-select-button"
@click="toggleDropdown('versioning-backup-mode')"
>
{{ versioningBackupModeLabel }}
<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.versioning_backup_mode === 'shallow' }"
@click="selectVersioningBackupMode('shallow')"
>
<strong>浅备份</strong><span>速度快,只回溯被编辑过的文件</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.versioning_backup_mode === 'full' }"
@click="selectVersioningBackupMode('full')"
>
<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>
<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>
<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并同步到工作区的 .astrion/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 === 'voice'" key="voice" class="settings-page">
<div class="settings-section" style="margin-bottom: 16px">
<p
class="settings-section-desc"
style="
margin: 0;
color: var(--text-secondary);
font-size: 13px;
line-height: 1.6;
"
>
端侧语音识别模型SenseVoice int8支持中英混说 + 自动标点。 模型约
228MB仅在手机本地运行无需网络。
</p>
</div>
<div class="settings-action-row">
<span class="settings-row-copy">
<span class="settings-row-title">语音识别模型</span>
<span class="settings-row-desc">
<template v-if="voiceModelReady">已下载 (228MB)</template>
<template v-else-if="voiceDownloading"
>下载中 {{ voiceDownloadPercent }}% — {{ voiceDownloadMsg }}</template
>
<template v-else-if="voiceModelPartial"
>下载不完整,请重新下载</template
>
<template v-else>未下载 — 点击下载</template>
</span>
</span>
<div style="display: flex; gap: 6px">
<button
type="button"
class="settings-secondary-button"
:disabled="voiceDownloading"
@click="downloadVoiceModel"
>
{{
voiceDownloading
? '下载中...'
: voiceModelReady
? '重新下载'
: '下载模型'
}}
</button>
<button
v-if="voiceModelReady || voiceModelPartial"
type="button"
class="settings-secondary-button"
style="color: var(--state-error)"
:disabled="voiceDownloading"
@click="deleteVoiceModel"
>
删除
</button>
</div>
</div>
<div class="settings-action-row" style="margin-top: 8px">
<span class="settings-row-copy">
<span class="settings-row-desc" style="font-size: 12px"
>点击麦克风闪退?先复现一次,再点「上传日志」</span
>
</span>
<button
type="button"
class="settings-secondary-button"
@click="saveDebugLog"
>
上传日志
</button>
</div>
<div
v-if="voiceDownloading"
class="voice-download-bar"
style="margin: 8px 0 0"
>
<div class="voice-download-track">
<div
class="voice-download-fill"
:style="{ width: voiceDownloadPercent + '%' }"
></div>
</div>
<span style="font-size: 11px; color: var(--text-secondary); margin-top: 4px"
>{{ voiceDownloadPercent }}%</span
>
</div>
</section>
<section
v-else-if="activeTab === 'sub-agents'"
key="sub-agents"
class="settings-page"
>
<div class="settings-section-desc" style="margin: 0 0 16px; color: var(--claude-text-secondary); font-size: 13px; line-height: 1.6">
管理多智能体模式下的子智能体角色。预设角色可编辑(创建自定义覆盖),自定义角色可创建/编辑/删除。
</div>
<!-- 压缩阈值配置 -->
<div class="settings-action-row" style="margin-bottom: 16px">
<span class="settings-row-copy">
<span class="settings-row-title">上下文压缩阈值</span>
<span class="settings-row-desc">
子智能体上下文 tokens 超过此值时触发深度压缩(默认 150000
</span>
</span>
<div style="display: flex; gap: 6px; align-items: center">
<input
type="number"
class="settings-number-input"
v-model.number="subAgentCompressThreshold"
:min="10000"
:step="10000"
style="width: 120px"
/>
<button
type="button"
class="settings-secondary-button"
:disabled="subAgentSettingsSaving"
@click="saveSubAgentSettings"
>
{{ subAgentSettingsSaving ? '保存中...' : '保存' }}
</button>
</div>
</div>
<!-- 角色列表 -->
<div class="settings-section-header" style="margin-bottom: 8px">
<span class="settings-section-title">角色列表</span>
<button
type="button"
class="settings-secondary-button"
@click="openRoleEditor()"
>
+ 新建角色
</button>
</div>
<div v-if="subAgentRolesLoading" class="settings-empty-hint">加载中...</div>
<div v-else-if="subAgentRoles.length === 0" class="settings-empty-hint">
暂无角色
</div>
<div v-else class="sub-agent-role-list">
<div
v-for="role in subAgentRoles"
:key="role.role_id"
class="sub-agent-role-card"
>
<div class="sub-agent-role-info">
<div class="sub-agent-role-name">
{{ role.name }}
<span
class="sub-agent-role-tag"
:class="role.is_custom ? 'tag-custom' : 'tag-preset'"
>
{{ role.is_custom ? '自定义' : '预设' }}
</span>
</div>
<div class="sub-agent-role-desc">{{ role.description || '无描述' }}</div>
<div class="sub-agent-role-meta">
ID: {{ role.role_id }} · 思考模式: {{ role.thinking_mode }}<template v-if="role.model_key"> · 模型: {{ role.model_key }}</template>
</div>
</div>
<div class="sub-agent-role-actions">
<button
type="button"
class="settings-secondary-button"
@click="openRoleEditor(role)"
>编辑</button>
<button
v-if="role.is_custom"
type="button"
class="settings-secondary-button danger"
@click="deleteRole(role)"
>删除</button>
</div>
</div>
</div>
<!-- 角色编辑弹窗(与个人空间同尺寸) -->
<transition name="personal-page-fade" appear>
<div
v-if="roleEditorOpen"
class="role-editor-drawer-overlay"
@click="closeDropdown"
@click.self="closeRoleEditor"
>
<div class="role-editor-drawer-card settings-redesign-card" style="padding: 30px 42px 24px; border-radius: 32px;" @click.stop>
<div class="role-editor-header">
<h2>{{ editingRole ? '编辑角色' : '新建角色' }}</h2>
<button type="button" class="settings-secondary-button" @click="closeRoleEditor">关闭</button>
</div>
<div class="role-editor-body" @click="closeDropdown">
<label class="settings-input-row">
<span class="settings-row-title">角色 ID</span>
<input
v-if="!editingRole"
type="text"
:value="roleForm.role_id"
maxlength="40"
placeholder="英文小写下划线,如 api-designer"
@input="roleForm.role_id = ($event.target as HTMLInputElement).value"
/>
<span v-else class="settings-row-desc" style="text-align: right; font-size: 14px;">{{ roleForm.role_id }}</span>
</label>
<label class="settings-input-row">
<span class="settings-row-title">显示名</span>
<input
type="text"
:value="roleForm.name"
maxlength="40"
placeholder="如 API Designer"
@input="roleForm.name = ($event.target as HTMLInputElement).value"
/>
</label>
<label class="settings-input-row">
<span class="settings-row-title">描述</span>
<input
type="text"
:value="roleForm.description"
maxlength="100"
placeholder="一句话描述职责"
@input="roleForm.description = ($event.target as HTMLInputElement).value"
/>
</label>
<div class="settings-select-row">
<span class="settings-row-copy">
<span class="settings-row-title">思考模式</span>
<span class="settings-row-desc">fast = 快速响应thinking = 深度推理</span>
</span>
<div
class="settings-select-wrap"
:class="{ open: activeDropdown === 'role-thinking' }"
@click.stop
>
<button
type="button"
class="settings-select-button"
@click="toggleDropdown('role-thinking')"
>
{{ roleForm.thinking_mode === 'thinking' ? 'thinking' : 'fast' }}
<span class="select-chevron" aria-hidden="true"></span>
</button>
<div
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
:style="activeDropdown === 'role-thinking' ? floatingMenuStyle : undefined"
>
<button
type="button"
class="settings-menu-option"
:class="{ selected: roleForm.thinking_mode === 'fast' }"
@click="roleForm.thinking_mode = 'fast'; closeDropdown()"
>
<strong>fast</strong><span>快速响应模式</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: roleForm.thinking_mode === 'thinking' }"
@click="roleForm.thinking_mode = 'thinking'; closeDropdown()"
>
<strong>thinking</strong><span>深度推理模式</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 === 'role-model' }"
@click.stop
>
<button
type="button"
class="settings-select-button"
@click="toggleDropdown('role-model')"
>
{{ roleForm.model_key || '默认模型' }}
<span class="select-chevron" aria-hidden="true"></span>
</button>
<div
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
:style="activeDropdown === 'role-model' ? floatingMenuStyle : undefined"
>
<button
type="button"
class="settings-menu-option"
:class="{ selected: !roleForm.model_key }"
@click="roleForm.model_key = ''; closeDropdown()"
>
<strong>默认模型</strong><span>使用配置文件中的 default_model</span><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
<button
v-for="m in subAgentModels"
:key="m.name"
type="button"
class="settings-menu-option"
:class="{ selected: roleForm.model_key === m.name }"
@click="roleForm.model_key = m.name; closeDropdown()"
>
<strong>{{ m.name }}</strong><span>{{ m.modes }} · {{ m.multimodal || '纯文本' }}</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-textarea-row">
<span class="settings-row-title">Prompt BodyMarkdown</span>
<textarea
:value="roleForm.body_prompt"
placeholder="角色的自定义 prompt body..."
@input="roleForm.body_prompt = ($event.target as HTMLTextAreaElement).value"
></textarea>
</div>
</div>
<div class="role-editor-footer">
<button type="button" class="settings-secondary-button" @click="closeRoleEditor">取消</button>
<button
type="button"
class="settings-primary-button"
:disabled="roleSaving || !roleForm.role_id || !roleForm.name || !roleForm.body_prompt"
@click="saveRole"
>
{{ roleSaving ? '保存中...' : '保存' }}
</button>
</div>
</div>
</div>
</transition>
</section>
<section
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>
</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,
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'
| 'voice'
| 'sub-agents'
| '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' },
{ id: 'voice', label: '语音模型', icon: 'mic' },
{ id: 'sub-agents', label: '子智能体', icon: 'bot' }
] 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 voiceModelReady = ref(false);
const voiceModelPartial = ref(false); // 文件存在但不完整
const voiceDownloading = ref(false);
const voiceDownloadPercent = ref(0);
const voiceDownloadMsg = ref('');
const checkVoiceModel = () => {
const bridge = (window as any)?.AndroidVoiceBridge;
if (bridge) {
try {
if (typeof bridge.isModelReady === 'function') {
voiceModelReady.value = bridge.isModelReady();
}
if (!voiceModelReady.value && typeof bridge.isModelPartial === 'function') {
voiceModelPartial.value = bridge.isModelPartial();
}
} catch (_) {
/* ignore */
}
}
};
const downloadVoiceModel = () => {
const bridge = (window as any)?.AndroidVoiceBridge;
if (!bridge) {
alert('仅在 App 内支持下载语音模型');
return;
}
voiceDownloading.value = true;
voiceDownloadPercent.value = 0;
voiceDownloadMsg.value = '准备下载...';
voiceModelPartial.value = false;
(window as any).__onVoiceDownloadProgress = (pct: number, msg: string) => {
voiceDownloadPercent.value = pct;
voiceDownloadMsg.value = msg;
if (pct >= 100) {
voiceDownloading.value = false;
voiceModelReady.value = true;
voiceModelPartial.value = false;
}
};
(window as any).__onVoiceModelReady = () => {
voiceModelReady.value = true;
voiceDownloading.value = false;
voiceModelPartial.value = false;
};
bridge.downloadModel();
};
const deleteVoiceModel = () => {
const bridge = (window as any)?.AndroidVoiceBridge;
if (!bridge) return;
if (typeof bridge.deleteModel === 'function') {
bridge.deleteModel();
}
voiceModelReady.value = false;
voiceModelPartial.value = false;
voiceDownloadPercent.value = 0;
voiceDownloadMsg.value = '';
};
const saveDebugLog = async () => {
const bridge = (window as any)?.AndroidVoiceBridge;
if (!bridge) return;
let log = '';
if (typeof bridge.collectDebugLog === 'function') {
log = bridge.collectDebugLog();
}
if (!log) {
alert('无法收集日志');
return;
}
try {
const res = await fetch('/api/voice_debug', { method: 'POST', body: log });
if (res.ok) {
alert('日志已上传到服务器');
} else {
alert('上传失败: ' + res.status);
}
} catch (e: any) {
alert('上传失败: ' + (e.message || e));
}
};
// 打开个人空间时检查模型状态
checkVoiceModel();
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'
);
const menu = document.querySelector<HTMLElement>(
'.settings-select-wrap.open .settings-floating-menu'
);
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 padding = 16;
const gap = 10;
const menuHeight = menu?.getBoundingClientRect().height || 0;
let top: number;
if (menuHeight > 0) {
const spaceBelow = window.innerHeight - rect.bottom - padding;
const spaceAbove = rect.top - padding;
if (spaceBelow >= menuHeight) {
top = rect.bottom + gap;
} else if (spaceAbove >= menuHeight) {
top = rect.top - menuHeight - gap;
} else if (spaceBelow >= spaceAbove) {
// 下方空间相对更大:菜单贴底部,内容通过 max-height 滚动
top = Math.max(rect.bottom + gap, window.innerHeight - menuHeight - padding);
} else {
// 上方空间相对更大:菜单贴顶部
top = padding;
}
} else {
top = Math.max(padding, Math.min(rect.bottom + gap, 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 versioningBackupModeLabel = computed(() => {
if (form.value.versioning_backup_mode === 'full') return '完全备份';
return '浅备份';
});
const imageCompressionLabel = computed(() => {
return (
imageCompressionOptions.find((option) => option.id === form.value.image_compression)?.label ||
'未设置'
);
});
const communicationStyleLabel = computed(() => {
if (form.value.communication_style === 'human_like') return '拟人';
if (form.value.communication_style === 'auto') return '自动';
return '默认';
});
const conversationContinuityLabel = computed(() => {
if (form.value.conversation_continuity === 'high') return '高';
if (form.value.conversation_continuity === 'low') return '低';
return '中';
});
const currentBlockDisplayMode = computed(() => experiments.value.blockDisplayMode);
const stackedHideBorders = computed(() => form.value.stacked_hide_borders);
const minimalExpandHeightLimited = computed(() => form.value.minimal_expand_height_limited);
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;
};
const uploadDiagnosticLog = async () => {
try {
const w = window as any;
const params = new URLSearchParams(window.location.search);
const screenInfo =
typeof window !== 'undefined'
? {
width: window.innerWidth,
height: window.innerHeight,
dpr: window.devicePixelRatio
}
: null;
const bridgeProbe = (name: string) => {
const b = w?.[name];
if (!b) return { exists: false };
const methods: Record<string, any> = {};
[
'getAppVersionCode',
'getAppVersionName',
'onThemeChanged',
'previewPdf',
'isPdfPreviewSupported',
'downloadFile',
'isSupported',
'isModelReady',
'isModelPartial',
'startRecording',
'stopRecording'
].forEach((m) => {
try {
methods[m] = typeof b[m] === 'function';
} catch {
methods[m] = 'error';
}
});
let versionCode = '';
let versionName = '';
try {
versionCode =
typeof b.getAppVersionCode === 'function' ? String(b.getAppVersionCode() || '') : '';
versionName =
typeof b.getAppVersionName === 'function' ? String(b.getAppVersionName() || '') : '';
} catch {}
return { exists: true, methods, versionCode, versionName };
};
const payload = {
source: 'diagnostic_log',
url: window.location.href,
userAgent: navigator.userAgent,
screen: screenInfo,
urlParams: {
app_vc: params.get('app_vc') || '',
app_vn: params.get('app_vn') || '',
app_shell: params.get('app_shell') || ''
},
bridges: {
AndroidThemeBridge: bridgeProbe('AndroidThemeBridge'),
AndroidPdfBridge: bridgeProbe('AndroidPdfBridge'),
AndroidDownloadBridge: bridgeProbe('AndroidDownloadBridge'),
AndroidVoiceBridge: bridgeProbe('AndroidVoiceBridge')
},
appCurrentVersionCode: appCurrentVersionCode.value,
appCurrentVersionName: appCurrentVersionName.value,
appUpdateInfo: appUpdateInfo.value,
appUpdateError: appUpdateError.value,
appUpdateCheckedAt: appUpdateCheckedAt.value,
timestamp: new Date().toISOString()
};
const resp = await fetch('/api/client_debug_log', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!resp.ok) throw new Error(`status ${resp.status}`);
alert('诊断日志已上传');
} catch (e: any) {
alert('上传诊断日志失败:' + (e?.message || String(e)));
}
};
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 === 'sub-agents') {
loadSubAgentRoles();
loadSubAgentSettings();
loadSubAgentModels();
}
if (
isVisible &&
tab === 'general' &&
isAppShell.value &&
!appUpdateInfo.value &&
!appUpdateChecking.value
) {
checkAppUpdate();
}
}
);
// ----- 子智能体管理 -----
const subAgentRoles = ref<any[]>([]);
const subAgentRolesLoading = ref(false);
const subAgentCompressThreshold = ref(150000);
const subAgentSettingsSaving = ref(false);
const subAgentModels = ref<any[]>([]);
const roleEditorOpen = ref(false);
const editingRole = ref<any>(null);
const roleSaving = ref(false);
const roleForm = ref({
role_id: '',
name: '',
description: '',
body_prompt: '',
thinking_mode: 'fast',
model_key: ''
});
const loadSubAgentRoles = async () => {
subAgentRolesLoading.value = true;
try {
const resp = await fetch('/api/multiagent/roles', { credentials: 'same-origin' });
const data = await resp.json();
if (data.success) {
subAgentRoles.value = data.roles || [];
}
} catch (e) {
// 静默处理
} finally {
subAgentRolesLoading.value = false;
}
};
const loadSubAgentModels = async () => {
try {
const resp = await fetch('/api/multiagent/models', { credentials: 'same-origin' });
const data = await resp.json();
if (data.success) {
subAgentModels.value = data.models || [];
}
} catch (e) {
// 静默处理
}
};
const loadSubAgentSettings = async () => {
try {
const resp = await fetch('/api/multiagent/settings', { credentials: 'same-origin' });
const data = await resp.json();
if (data.success && data.settings) {
subAgentCompressThreshold.value = data.settings.sub_agent_compress_threshold_tokens || 150000;
}
} catch (e) {
// 静默处理
}
};
const saveSubAgentSettings = async () => {
subAgentSettingsSaving.value = true;
try {
await fetch('/api/multiagent/settings', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify({ settings: { sub_agent_compress_threshold_tokens: subAgentCompressThreshold.value } })
});
} finally {
subAgentSettingsSaving.value = false;
}
};
const openRoleEditor = (role?: any) => {
if (role) {
editingRole.value = role;
roleForm.value = {
role_id: role.role_id || '',
name: role.name || '',
description: role.description || '',
body_prompt: role.body_prompt || '',
thinking_mode: role.thinking_mode || 'fast',
model_key: role.model_key || ''
};
} else {
editingRole.value = null;
roleForm.value = { role_id: '', name: '', description: '', body_prompt: '', thinking_mode: 'fast', model_key: '' };
}
closeDropdown();
roleEditorOpen.value = true;
};
const closeRoleEditor = () => {
roleEditorOpen.value = false;
editingRole.value = null;
closeDropdown();
};
const saveRole = async () => {
roleSaving.value = true;
try {
const isEdit = !!editingRole.value;
const url = isEdit
? `/api/multiagent/roles/${roleForm.value.role_id}`
: '/api/multiagent/roles';
const method = isEdit ? 'PUT' : 'POST';
const resp = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify({
...roleForm.value,
model_key: roleForm.value.model_key || null
})
});
const data = await resp.json();
if (data.success) {
closeRoleEditor();
await loadSubAgentRoles();
}
} finally {
roleSaving.value = false;
}
};
const deleteRole = async (role: any) => {
if (!confirm(`确认删除角色「${role.name}」?`)) return;
try {
const resp = await fetch(`/api/multiagent/roles/${role.role_id}`, {
method: 'DELETE',
credentials: 'same-origin'
});
const data = await resp.json();
if (data.success) {
await loadSubAgentRoles();
}
} catch (e) {
// 静默处理
}
};
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' | 'auto') => {
personalization.setCommunicationStyle(value);
};
const setConversationContinuity = (value: 'low' | 'medium' | 'high') => {
personalization.setConversationContinuity(value);
};
const selectDefaultModel = (value: string) => {
setDefaultModel(value);
closeDropdown();
};
const selectDefaultRunMode = (value: RunModeValue) => {
setDefaultRunMode(value);
closeDropdown();
};
const selectDefaultPermissionMode = (value: PermissionModeValue) => {
setDefaultPermissionMode(value);
closeDropdown();
};
const selectVersioningBackupMode = (value: 'shallow' | 'full') => {
personalization.setVersioningBackupMode(value);
closeDropdown();
};
const selectCommunicationStyle = (value: 'default' | 'human_like' | 'auto') => {
setCommunicationStyle(value);
closeDropdown();
};
const selectConversationContinuity = (value: 'low' | 'medium' | 'high') => {
setConversationContinuity(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 handleStackedHideBordersChange = (event: Event) => {
const target = event.target as HTMLInputElement;
personalization.updateField({
key: 'stacked_hide_borders',
value: target.checked
});
};
const handleMinimalExpandHeightLimitedChange = (event: Event) => {
const target = event.target as HTMLInputElement;
personalization.updateField({
key: 'minimal_expand_height_limited',
value: target.checked
});
};
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>
.sub-agent-role-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.sub-agent-role-card {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 11px 0;
border-bottom: 1px solid var(--theme-control-border);
}
.sub-agent-role-info {
flex: 1;
min-width: 0;
}
.sub-agent-role-name {
font-size: 14px;
font-weight: 550;
color: var(--claude-text);
display: flex;
align-items: center;
gap: 6px;
}
.sub-agent-role-tag {
font-size: 10px;
padding: 1px 8px;
border-radius: 999px;
font-weight: 400;
}
.sub-agent-role-tag.tag-preset {
background: color-mix(in srgb, var(--claude-text-secondary) 15%, transparent);
color: var(--claude-text-secondary);
}
.sub-agent-role-tag.tag-custom {
background: color-mix(in srgb, var(--claude-accent) 15%, transparent);
color: var(--claude-accent);
}
.sub-agent-role-desc {
font-size: 12.5px;
color: var(--claude-text-secondary);
margin-top: 3px;
line-height: 1.38;
}
.sub-agent-role-meta {
font-size: 11px;
color: var(--claude-text-secondary);
opacity: 0.7;
margin-top: 2px;
}
.sub-agent-role-actions {
display: flex;
gap: 6px;
flex-shrink: 0;
}
/* 角色编辑弹窗 — 复用 settings-redesign-card */
.role-editor-drawer-overlay {
position: fixed;
inset: 0;
background: var(--overlay-scrim);
display: flex;
align-items: center;
justify-content: center;
z-index: 2147483647;
}
.role-editor-drawer-card {
display: flex;
flex-direction: column;
overflow: visible;
}
.role-editor-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
flex-shrink: 0;
}
.role-editor-header h2 {
font-size: 20px;
font-weight: 600;
color: var(--claude-text);
margin: 0;
}
.role-editor-body {
flex: 1;
overflow-y: auto;
overflow-x: visible;
min-height: 0;
scrollbar-width: none;
-ms-overflow-style: none;
}
.role-editor-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
padding-top: 12px;
flex-shrink: 0;
}
.settings-section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.settings-section-title {
font-size: 14px;
font-weight: 550;
color: var(--claude-text);
}
.settings-empty-hint {
color: var(--claude-text-secondary);
font-size: 13px;
padding: 24px 0;
text-align: center;
}
.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,
.role-editor-body::-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-textarea-row {
min-height: 0;
border-bottom: 1px solid var(--theme-control-border);
display: flex;
flex-direction: column;
gap: 10px;
padding: 14px 0;
color: var(--claude-text);
}
.settings-textarea-row textarea {
width: 100%;
min-height: 120px;
max-height: 320px;
border: 1px solid var(--theme-control-border);
border-radius: 12px;
background: transparent;
color: var(--claude-text);
padding: 12px;
outline: none;
font-size: 13px;
line-height: 1.55;
resize: vertical;
overflow-y: auto;
font-family: inherit;
}
.settings-textarea-row textarea:focus {
border-color: var(--claude-text-secondary);
box-shadow: none;
}
.settings-textarea-row textarea::placeholder {
color: var(--claude-text-secondary);
opacity: 0.6;
}
.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;
}
}
/* ── 语音模型下载进度条 ── */
.voice-download-bar {
display: flex;
flex-direction: column;
gap: 4px;
}
.voice-download-track {
width: 100%;
height: 6px;
background: var(--surface-muted);
border-radius: 3px;
overflow: hidden;
}
.voice-download-fill {
height: 100%;
background: var(--accent-primary);
border-radius: 3px;
transition: width 0.3s ease;
}
</style>