agent-Specialization/static/src/components/personalization/PersonalizationDrawer.vue
JOJO 72a49a7c8d fix: 修复对话计时器持久化、子智能体工具美化与状态查询,统一后台通知池
- 新增 server/work_timer.py,在对话真正空闲(无前台任务、无后台子智能体/后台命令/压缩)时持久化 work_timer,并同步内存副本,解决刷新后计时器回退问题。
- server/chat_flow_task_main.py / chat_flow.py / tasks/models.py 在任务结束/取消时按空闲判定决定是否持久化。
- 前端 lifecycle.ts 在仍有后台任务时不提前停止计时器。
- 子智能体工具(create/terminate/get_status)渲染美化,task 参数放在顶部元信息区。
- 子智能体状态查询支持返回「已完成」「已终止」「不存在」;修复 wait_for_completion 在 final_result 就绪前返回导致的「ID 被占用」误报。
- 统一后台完成通知池 poll_completion_notifications,合并子智能体与后台 run_command 两路轮询,避免逐条触发工作循环与单工作区互斥冲突。
- 删除本次新增的各类 debug_log / notify_pool_log 调用及辅助脚本。
2026-06-25 04:16:44 +08:00

3619 lines
138 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-list-row tall">
<span class="settings-row-copy">
<span class="settings-row-title">回答时必须考虑的信息</span>
<span class="settings-row-desc"
>最多 {{ maxConsiderations }} 条,可拖动排序</span
>
</span>
<div class="settings-input-stack wide">
<div class="settings-add-row">
<input
type="text"
:value="newConsideration"
maxlength="50"
placeholder="输入后点击 + 添加"
@input="personalization.updateNewConsideration($event.target.value)"
@focus="personalization.clearFeedback()"
/>
<button
type="button"
:disabled="
!newConsideration || form.considerations.length >= maxConsiderations
"
@click="personalization.addConsideration()"
>
+
</button>
</div>
<ul class="settings-consideration-list" v-if="form.considerations.length">
<li
v-for="(item, idx) in form.considerations"
:key="`consideration-${idx}`"
draggable="true"
@dragstart="personalization.considerationDragStart(idx, $event)"
@dragover.prevent="personalization.considerationDragOver(idx, $event)"
@drop.prevent="personalization.considerationDrop(idx, $event)"
@dragend="personalization.considerationDragEnd()"
>
<span class="drag-handle" aria-hidden="true">≡</span>
<span>{{ item }}</span>
<button
type="button"
@click="personalization.removeConsideration(idx)"
>
</button>
</li>
</ul>
</div>
</div>
<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>
<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.use_custom_names"
@change="
personalization.updateField({
key: 'use_custom_names',
value: $event.target.checked
})
" /><span class="fancy-check" aria-hidden="true"
><svg viewBox="0 0 64 64">
<path
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
pathLength="575.0541381835938"
class="fancy-path"
></path></svg></span
></label>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">增强工具显示</span
><span class="settings-row-desc"
>工具块显示格式化内容,关闭则显示原始 JSON</span
></span
><input
type="checkbox"
:checked="form.enhanced_tool_display"
@change="
personalization.updateField({
key: 'enhanced_tool_display',
value: $event.target.checked
})
" /><span class="fancy-check" aria-hidden="true"
><svg viewBox="0 0 64 64">
<path
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
pathLength="575.0541381835938"
class="fancy-path"
></path></svg></span
></label>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">显示 Git 状态栏</span
><span class="settings-row-desc"
>工作区存在 Git 仓库时,在输入栏上方显示分支和变更统计</span
></span
><input
type="checkbox"
:checked="form.show_git_status_bar"
@change="
personalization.updateField({
key: 'show_git_status_bar',
value: $event.target.checked
})
" /><span class="fancy-check" aria-hidden="true"
><svg viewBox="0 0 64 64">
<path
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
pathLength="575.0541381835938"
class="fancy-path"
></path></svg></span
></label>
<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>
<div class="settings-select-row">
<span class="settings-row-copy"
><span class="settings-row-title">简略消息显示</span
><span class="settings-row-desc"
>审核、子智能体等紧凑系统消息的显示形式</span
></span
>
<div
class="settings-select-wrap"
:class="{ open: activeDropdown === 'compact-message' }"
@click.stop
>
<button
type="button"
class="settings-select-button"
@click="toggleDropdown('compact-message')"
>
{{ compactMessageDisplayLabel }}
<span class="select-chevron" aria-hidden="true"></span>
</button>
<div
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
:style="activeDropdown ? floatingMenuStyle : undefined"
>
<button
v-for="option in compactMessageDisplayOptions"
:key="option.id"
type="button"
class="settings-menu-option"
:class="{ selected: currentCompactMessageDisplay === option.value }"
@click="selectCompactMessageDisplay(option.value)"
>
<strong>{{ option.label }}</strong
><span>{{ option.desc }}</span
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
</div>
</div>
</div>
</section>
<section
v-else-if="activeTab === 'workspace'"
key="workspace"
class="settings-page"
data-tutorial="personal-page-workspace"
>
<div class="settings-select-row">
<span class="settings-row-copy"
><span class="settings-row-title">默认权限</span
><span class="settings-row-desc">新建对话时的默认权限模式</span></span
>
<div
class="settings-select-wrap"
:class="{ open: activeDropdown === 'permission' }"
@click.stop
>
<button
type="button"
class="settings-select-button"
@click="toggleDropdown('permission')"
>
{{ permissionModeLabel }}
<span class="select-chevron" aria-hidden="true"></span>
</button>
<div
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
:style="activeDropdown ? floatingMenuStyle : undefined"
>
<button
v-for="option in permissionModeOptions"
:key="option.id"
type="button"
class="settings-menu-option"
:class="{ selected: form.default_permission_mode === option.id }"
@click="selectDefaultPermissionMode(option.id)"
>
<strong>{{ option.label }}</strong
><span>{{ option.desc }}</span
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
</div>
</div>
</div>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">允许根目录创建文件</span
><span class="settings-row-desc"
>关闭则禁止在工作区根目录创建文件</span
></span
><input
type="checkbox"
:checked="form.allow_root_file_creation"
@change="
personalization.updateField({
key: 'allow_root_file_creation',
value: $event.target.checked
})
" /><span class="fancy-check" aria-hidden="true"
><svg viewBox="0 0 64 64">
<path
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
pathLength="575.0541381835938"
class="fancy-path"
></path></svg></span
></label>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">AGENTS.md 自动注入</span
><span class="settings-row-desc"
>工作区根目录存在 AGENTS.md 时自动注入系统提示词</span
></span
><input
type="checkbox"
:checked="form.agents_md_auto_inject"
@change="
personalization.updateField({
key: 'agents_md_auto_inject',
value: $event.target.checked
})
" /><span class="fancy-check" aria-hidden="true"
><svg viewBox="0 0 64 64">
<path
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
pathLength="575.0541381835938"
class="fancy-path"
></path></svg></span
></label>
<div class="settings-section-divider">
<span class="settings-section-divider__label">目标模式</span>
</div>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">审核智能体主动取证</span
><span class="settings-row-desc"
>开启后可运行只读命令核实目标,关闭则仅依据对话判断</span
></span
><input
type="checkbox"
:checked="form.goal_review_mode === 'active'"
@change="
personalization.updateField({
key: 'goal_review_mode',
value: $event.target.checked ? 'active' : 'readonly'
})
" /><span class="fancy-check" aria-hidden="true"
><svg viewBox="0 0 64 64">
<path
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
pathLength="575.0541381835938"
class="fancy-path"
></path></svg></span
></label>
<div class="settings-select-row">
<span class="settings-row-copy"
><span class="settings-row-title">最大自动续命轮数</span
><span class="settings-row-desc"
>主模型停下后最多自动继续 1-100 轮</span
></span
>
<input
type="number"
class="settings-number-input"
min="1"
max="100"
:value="form.goal_max_turns"
@change="
personalization.updateField({
key: 'goal_max_turns',
value: clampGoalMaxTurns($event.target.value)
})
"
/>
</div>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">启用累计 token 上限</span
><span class="settings-row-desc"
>累计输入输出超过上限时停止目标模式</span
></span
><input
type="checkbox"
:checked="goalTokenLimitEnabled"
@change="toggleGoalTokenLimit($event.target.checked)" /><span
class="fancy-check"
aria-hidden="true"
><svg viewBox="0 0 64 64">
<path
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
pathLength="575.0541381835938"
class="fancy-path"
></path></svg></span
></label>
<div v-if="goalTokenLimitEnabled" class="settings-select-row">
<span class="settings-row-copy"
><span class="settings-row-title">累计 token 上限</span
><span class="settings-row-desc">达到该累计消耗即停止目标模式</span></span
>
<input
type="number"
class="settings-number-input"
min="1000"
step="1000"
:value="form.goal_max_tokens || 100000"
@change="
personalization.updateField({
key: 'goal_max_tokens',
value: clampGoalMaxTokens($event.target.value)
})
"
/>
</div>
</section>
<section
v-else-if="activeTab === 'context'"
key="context"
class="settings-page"
>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">最近对话提示</span
><span class="settings-row-desc"
>把当前工作区最近若干个非空对话注入系统提示词</span
></span
><input
type="checkbox"
:checked="form.recent_conversations_prompt_enabled"
@change="
personalization.updateField({
key: 'recent_conversations_prompt_enabled',
value: $event.target.checked
})
" /><span class="fancy-check" aria-hidden="true"
><svg viewBox="0 0 64 64">
<path
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
pathLength="575.0541381835938"
class="fancy-path"
></path></svg></span
></label>
<div
class="settings-input-row"
v-if="form.recent_conversations_prompt_enabled"
>
<span class="settings-row-title">最近对话注入数量</span>
<div class="settings-number-row">
<input
type="number"
:min="recentConversationsPromptLimitRange.min"
:max="recentConversationsPromptLimitRange.max"
:value="form.recent_conversations_prompt_limit"
@input="handleRecentConversationsPromptLimitInput"
@blur="commitRecentConversationsPromptLimitInput"
/><button type="button" @click="restoreRecentConversationsPromptLimit">
恢复默认
</button>
</div>
</div>
<div class="settings-group-block">
<div class="settings-group-title">
<span class="settings-row-title">上下文压缩策略</span
><span class="settings-row-desc"
>可分别控制自动浅层压缩与自动深层压缩</span
>
</div>
<label class="settings-toggle-row inner"
><span class="settings-row-copy"
><span class="settings-row-title">自动浅层压缩</span></span
><input
type="checkbox"
:checked="form.auto_shallow_compress_enabled"
@change="
personalization.updateField({
key: 'auto_shallow_compress_enabled',
value: $event.target.checked
})
" /><span class="fancy-check" aria-hidden="true"
><svg viewBox="0 0 64 64">
<path
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
pathLength="575.0541381835938"
class="fancy-path"
></path></svg></span
></label>
<label class="settings-toggle-row inner"
><span class="settings-row-copy"
><span class="settings-row-title">自动深层压缩</span></span
><input
type="checkbox"
:checked="form.auto_deep_compress_enabled"
@change="
personalization.updateField({
key: 'auto_deep_compress_enabled',
value: $event.target.checked
})
" /><span class="fancy-check" aria-hidden="true"
><svg viewBox="0 0 64 64">
<path
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
pathLength="575.0541381835938"
class="fancy-path"
></path></svg></span
></label>
<div class="settings-compression-grid">
<label
><span>浅压缩触发上下文</span
><input
type="number"
:value="form.shallow_compress_trigger_tokens ?? ''"
@input="
handleCompressionNumberInput(
'shallow_compress_trigger_tokens',
$event
)
"
/></label>
<label
><span>浅压缩保留最近工具数</span
><input
type="number"
:value="form.shallow_compress_keep_recent_tools ?? ''"
@input="
handleCompressionNumberInput(
'shallow_compress_keep_recent_tools',
$event
)
"
/></label>
<label
><span>浅压缩保留当前轮工具数</span
><input
type="number"
:value="form.shallow_compress_keep_user_turn_tools ?? ''"
@input="
handleCompressionNumberInput(
'shallow_compress_keep_user_turn_tools',
$event
)
"
/></label>
<label
><span>每轮最大替换数量</span
><input
type="number"
:value="form.shallow_compress_max_replace_per_round ?? ''"
@input="
handleCompressionNumberInput(
'shallow_compress_max_replace_per_round',
$event
)
"
/></label>
<label
><span>工具调用间隔触发</span
><input
type="number"
:value="form.shallow_compress_trigger_tool_calls_interval ?? ''"
@input="
handleCompressionNumberInput(
'shallow_compress_trigger_tool_calls_interval',
$event
)
"
/></label>
<label
><span>深压缩触发上下文</span
><input
type="number"
:value="form.deep_compress_trigger_tokens ?? ''"
@input="
handleCompressionNumberInput('deep_compress_trigger_tokens', $event)
"
/></label>
</div>
<label class="settings-toggle-row inner"
><span class="settings-row-copy"
><span class="settings-row-title">深压缩直接注入</span
><span class="settings-row-desc"
>开启后直接注入历次压缩全文,关闭则只提示文件位置</span
></span
><input
type="checkbox"
:checked="form.deep_compress_form === 'inject'"
@change="
personalization.updateField({
key: 'deep_compress_form',
value: $event.target.checked ? 'inject' : 'file'
})
" /><span class="fancy-check" aria-hidden="true"
><svg viewBox="0 0 64 64">
<path
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
pathLength="575.0541381835938"
class="fancy-path"
></path></svg></span
></label>
<div class="settings-inline-actions right">
<button
type="button"
class="settings-secondary-button"
@click="restoreCompressionDefaults"
>
恢复压缩默认值
</button>
</div>
</div>
</section>
<section v-else-if="activeTab === 'tools'" key="tools" class="settings-page">
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">静默禁用工具</span
><span class="settings-row-desc"
>禁用工具时不再注入提示消息,模型不会感知被禁用项</span
></span
><input
type="checkbox"
:checked="form.silent_tool_disable"
@change="
personalization.updateField({
key: 'silent_tool_disable',
value: $event.target.checked
})
" /><span class="fancy-check" aria-hidden="true"
><svg viewBox="0 0 64 64">
<path
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
pathLength="575.0541381835938"
class="fancy-path"
></path></svg></span
></label>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">工具意图提示</span
><span class="settings-row-desc"
>调用工具时先用简短文字告诉你要做什么</span
></span
><input
type="checkbox"
:checked="form.tool_intent_enabled"
@change="
personalization.updateField({
key: 'tool_intent_enabled',
value: $event.target.checked
})
" /><span class="fancy-check" aria-hidden="true"
><svg viewBox="0 0 64 64">
<path
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
pathLength="575.0541381835938"
class="fancy-path"
></path></svg></span
></label>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">Skill 提示系统</span
><span class="settings-row-desc"
>检测到特定关键词时自动提示模型阅读相关 skill</span
></span
><input
type="checkbox"
:checked="form.skill_hints_enabled"
@change="
personalization.updateField({
key: 'skill_hints_enabled',
value: $event.target.checked
})
" /><span class="fancy-check" aria-hidden="true"
><svg viewBox="0 0 64 64">
<path
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
pathLength="575.0541381835938"
class="fancy-path"
></path></svg></span
></label>
<div class="settings-group-block">
<div class="settings-group-title">
<span class="settings-row-title">强约束系统</span
><span class="settings-row-desc"
>仅对个人空间中已启用的 skill 生效</span
>
</div>
<label class="settings-toggle-row inner"
><span class="settings-row-title">Terminal 系列工具</span
><input
type="checkbox"
:checked="form.skill_strict_terminal_enabled"
@change="
personalization.updateField({
key: 'skill_strict_terminal_enabled',
value: $event.target.checked
})
" /><span class="fancy-check" aria-hidden="true"
><svg viewBox="0 0 64 64">
<path
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
pathLength="575.0541381835938"
class="fancy-path"
></path></svg></span
></label>
<label class="settings-toggle-row inner"
><span class="settings-row-title">子智能体系列工具</span
><input
type="checkbox"
:checked="form.skill_strict_sub_agent_enabled"
@change="
personalization.updateField({
key: 'skill_strict_sub_agent_enabled',
value: $event.target.checked
})
" /><span class="fancy-check" aria-hidden="true"
><svg viewBox="0 0 64 64">
<path
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
pathLength="575.0541381835938"
class="fancy-path"
></path></svg></span
></label>
<label class="settings-toggle-row inner"
><span class="settings-row-title">run_command 前台模式</span
><input
type="checkbox"
:checked="form.skill_strict_run_command_foreground_enabled"
@change="
personalization.updateField({
key: 'skill_strict_run_command_foreground_enabled',
value: $event.target.checked
})
" /><span class="fancy-check" aria-hidden="true"
><svg viewBox="0 0 64 64">
<path
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
pathLength="575.0541381835938"
class="fancy-path"
></path></svg></span
></label>
<label class="settings-toggle-row inner"
><span class="settings-row-title">run_command 后台模式</span
><input
type="checkbox"
:checked="form.skill_strict_run_command_background_enabled"
@change="
personalization.updateField({
key: 'skill_strict_run_command_background_enabled',
value: $event.target.checked
})
" /><span class="fancy-check" aria-hidden="true"
><svg viewBox="0 0 64 64">
<path
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
pathLength="575.0541381835938"
class="fancy-path"
></path></svg></span
></label>
</div>
<div class="settings-group-block" v-if="skillsCatalog.length">
<div class="settings-group-title">
<span class="settings-row-title">可用 Skills</span
><span class="settings-row-desc"
>勾选后会注入 system prompt并同步到工作区的 skills/ 目录</span
>
</div>
<div class="settings-check-grid">
<label
v-for="skill in skillsCatalog"
:key="skill.id"
class="settings-toggle-row inner"
><span class="settings-row-title">{{ skill.label }}</span
><input
type="checkbox"
:checked="form.enabled_skills.includes(skill.id)"
@change="personalization.toggleSkill(skill.id)" /><span
class="fancy-check"
aria-hidden="true"
><svg viewBox="0 0 64 64">
<path
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
pathLength="575.0541381835938"
class="fancy-path"
></path></svg></span
></label>
</div>
</div>
<div class="settings-group-block" v-if="toolCategories.length">
<div class="settings-group-title">
<span class="settings-row-title">默认禁用工具类别</span
><span class="settings-row-desc"
>选择后,这些类别在新任务中保持关闭</span
>
</div>
<div class="settings-check-grid">
<label
v-for="category in toolCategories"
:key="category.id"
class="settings-toggle-row inner"
><span class="settings-row-title">{{ category.label }}</span
><input
type="checkbox"
:checked="form.disabled_tool_categories.includes(category.id)"
@change="toggleCategory(category.id)" /><span
class="fancy-check"
aria-hidden="true"
><svg viewBox="0 0 64 64">
<path
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
pathLength="575.0541381835938"
class="fancy-path"
></path></svg></span
></label>
</div>
</div>
</section>
<section
v-else-if="activeTab === 'files'"
key="files"
class="settings-page"
data-tutorial="personal-page-image"
>
<div class="settings-select-row">
<span class="settings-row-copy"
><span class="settings-row-title">图片压缩</span
><span class="settings-row-desc"
>发送图片或调用 view_image 时自动等比缩放</span
></span
>
<div
class="settings-select-wrap"
:class="{ open: activeDropdown === 'image' }"
@click.stop
>
<button
type="button"
class="settings-select-button"
@click="toggleDropdown('image')"
>
{{ imageCompressionLabel }}
<span class="select-chevron" aria-hidden="true"></span>
</button>
<div
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
:style="activeDropdown ? floatingMenuStyle : undefined"
>
<button
v-for="option in imageCompressionOptions"
:key="option.id"
type="button"
class="settings-menu-option"
:class="{ selected: form.image_compression === option.id }"
@click="selectImageCompression(option.id)"
>
<strong>{{ option.label }}</strong
><span>{{ option.desc }}</span
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
</div>
</div>
</div>
</section>
<section
v-else-if="activeTab === 'data'"
key="data"
class="settings-page usage-summary-page"
data-tutorial="personal-page-usage"
>
<div class="usage-summary-card settings-stats-card">
<div class="usage-summary-header">
<div>
<p class="usage-summary-eyebrow">对话用量</p>
<h3>用量统计</h3>
<p class="usage-summary-desc">
累计统计所有对话的输入/输出 Token、对话数量、用户消息与工具调用
</p>
</div>
</div>
<div class="usage-summary-grid usage-summary-grid--tokens">
<div class="usage-summary-item">
<div class="label">累计输入</div>
<div class="value value--success">
{{ formatTokenCount(usageSummary.total_input_tokens) }}
</div>
</div>
<div class="usage-summary-item">
<div class="label">累计输出</div>
<div class="value value--warning">
{{ formatTokenCount(usageSummary.total_output_tokens) }}
</div>
</div>
</div>
<div class="usage-summary-grid usage-summary-grid--counts">
<div class="usage-summary-item">
<div class="label">总对话数</div>
<div class="value">
{{ formatTokenCount(usageSummary.total_conversations) }}
</div>
</div>
<div class="usage-summary-item">
<div class="label">用户消息数</div>
<div class="value">
{{ formatTokenCount(usageSummary.total_user_messages) }}
</div>
</div>
<div class="usage-summary-item">
<div class="label">工具调用次数</div>
<div class="value">
{{ formatTokenCount(usageSummary.total_tools) }}
</div>
</div>
</div>
<div class="usage-summary-meta">
<span v-if="usageError" class="usage-summary-error">{{ usageError }}</span
><span v-else-if="usageLoading">正在同步最新统计...</span
><span v-else>最近更新:{{ usageUpdatedText }}</span
><button
type="button"
class="usage-summary-refresh"
@click="fetchUsageSummary"
:disabled="usageLoading"
>
{{ usageLoading ? '刷新中...' : '刷新数据' }}
</button>
</div>
</div>
</section>
<section
v-else-if="activeTab === '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 === 'admin'"
key="admin"
class="settings-page admin-monitor-page"
>
<div class="settings-action-row" v-if="showMcpConfigEntry">
<span class="settings-row-copy"
><span class="settings-row-title">MCP 服务器管理</span
><span class="settings-row-desc"
>统一配置 MCP Server集中管理工具扩展能力</span
></span
><button
type="button"
class="settings-secondary-button"
@click="openMcpConfig"
>
打开
</button>
</div>
<div class="settings-action-row">
<span class="settings-row-copy"
><span class="settings-row-title">管理员监控入口</span
><span class="settings-row-desc"
>展示配额趋势、容器运行、项目存储与上传安全</span
></span
><button
type="button"
class="settings-secondary-button"
@click="openAdminPanel"
>
打开
</button>
</div>
<div class="settings-action-row">
<span class="settings-row-copy"
><span class="settings-row-title">API 管理</span
><span class="settings-row-desc">管理 API 调用与账户</span></span
><button
type="button"
class="settings-secondary-button"
@click="openApiAdmin"
>
打开
</button>
</div>
<div class="settings-action-row">
<span class="settings-row-copy"
><span class="settings-row-title">自定义工具管理</span
><span class="settings-row-desc"
>在线创建/编辑自定义工具文件</span
></span
><button
type="button"
class="settings-secondary-button"
@click="openCustomTools"
>
打开
</button>
</div>
</section>
</transition>
</div>
<div class="settings-save-bar">
<div class="personal-status-group">
<transition name="personal-status-fade"
><span class="status success" v-if="status">{{ status }}</span></transition
><transition name="personal-status-fade"
><span class="status error" v-if="error">{{ error }}</span></transition
>
</div>
</div>
</section>
</div>
</form>
</div>
<div class="personalization-loading" v-else>正在加载个性化配置...</div>
</div>
</div>
</transition>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted, nextTick, onBeforeUnmount } from 'vue';
import { storeToRefs } from 'pinia';
import { usePersonalizationStore } from '@/stores/personalization';
import { useResourceStore } from '@/stores/resource';
import { useUiStore, persistDefaultHideWorkspace } from '@/stores/ui';
import { usePolicyStore } from '@/stores/policy';
import { useTutorialStore } from '@/stores/tutorial';
import { useModelStore } from '@/stores/model';
import { formatTokenCount } from '@/utils/formatters';
import { ICONS } from '@/utils/icons';
import { useTheme } from '@/utils/theme';
import type { ThemeKey } from '@/utils/theme';
defineOptions({ name: 'PersonalizationDrawer' });
const personalization = usePersonalizationStore();
const resourceStore = useResourceStore();
const uiStore = useUiStore();
const tutorialStore = useTutorialStore();
const {
visible,
loading,
form,
tonePresets,
newConsideration,
maxConsiderations,
status,
error,
saving,
toggleUpdating,
toolCategories,
skillsCatalog,
thinkingIntervalDefault,
thinkingIntervalRange,
recentConversationsPromptLimitRange,
experiments
} = storeToRefs(personalization);
// ---- 目标模式设置辅助 ----
const goalTokenLimitEnabled = computed(
() => typeof form.value.goal_max_tokens === 'number' && form.value.goal_max_tokens > 0
);
const clampGoalMaxTurns = (raw: any): number => {
const n = Math.round(Number(raw));
if (!Number.isFinite(n)) return 5;
return Math.min(100, Math.max(1, n));
};
const clampGoalMaxTokens = (raw: any): number => {
const n = Math.round(Number(raw));
if (!Number.isFinite(n)) return 100000;
return Math.min(100000000, Math.max(1000, n));
};
const toggleGoalTokenLimit = (enabled: boolean) => {
personalization.updateField({
key: 'goal_max_tokens',
value: enabled ? form.value.goal_max_tokens || 100000 : null
});
};
type IconKey = keyof typeof ICONS;
type PersonalTab =
| 'general'
| 'preferences'
| 'model'
| 'appearance'
| 'workspace'
| 'context'
| 'tools'
| 'files'
| 'data'
| 'admin';
const baseTabs = [
{ id: 'general', label: '常规', icon: 'settings' },
{ id: 'preferences', label: '个性化', icon: 'userPen' },
{ id: 'model', label: '模型与思考', icon: 'brainCog' },
{ id: 'appearance', label: '外观与显示', icon: 'monitor' },
{ id: 'workspace', label: '工作区与权限', icon: 'folder' },
{ id: 'context', label: '上下文', icon: 'chatBubble' },
{ id: 'tools', label: '工具与 Skills', icon: 'wrench' },
{ id: 'files', label: '文件与图片', icon: 'file' },
{ id: 'data', label: '数据管理', icon: 'layers' },
{ id: 'voice', label: '语音模型', icon: 'mic' },
] 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'
);
if (!button) {
return;
}
const rect = button.getBoundingClientRect();
const menuWidth = Math.min(300, Math.max(240, window.innerWidth - 32));
const left = Math.max(16, Math.min(rect.right - menuWidth, window.innerWidth - menuWidth - 16));
const top = Math.max(16, Math.min(rect.bottom + 10, window.innerHeight - 80));
floatingMenuStyle.value = {
position: 'fixed',
top: `${Math.round(top)}px`,
left: `${Math.round(left)}px`,
right: 'auto',
width: `${Math.round(menuWidth)}px`,
zIndex: '2147483647'
};
};
const toggleDropdown = async (key: string) => {
activeDropdown.value = activeDropdown.value === key ? null : key;
await updateFloatingMenuPosition();
};
const closeDropdown = () => {
activeDropdown.value = null;
floatingMenuStyle.value = {};
};
const settingsTabIconStyle = (icon: IconKey) => ({
'--icon-src': `url(${ICONS[icon]})`
});
onMounted(() => {
window.addEventListener('resize', updateFloatingMenuPosition);
window.addEventListener('scroll', updateFloatingMenuPosition, true);
});
onBeforeUnmount(() => {
window.removeEventListener('resize', updateFloatingMenuPosition);
window.removeEventListener('scroll', updateFloatingMenuPosition, true);
});
type RunModeValue = 'fast' | 'thinking' | 'deep' | null;
type PermissionModeValue = 'readonly' | 'approval' | 'auto_approval' | 'unrestricted';
type CompressionField =
| 'shallow_compress_trigger_tokens'
| 'shallow_compress_keep_recent_tools'
| 'shallow_compress_keep_user_turn_tools'
| 'shallow_compress_max_replace_per_round'
| 'shallow_compress_trigger_tool_calls_interval'
| 'deep_compress_trigger_tokens';
const runModeOptions: Array<{
id: string;
label: string;
desc: string;
value: RunModeValue;
badge?: string;
}> = [
{ id: 'fast', label: '快速模式', desc: '追求响应速度,跳过思考模型', value: 'fast' },
{ id: 'thinking', label: '思考模式', desc: '首轮回复会先输出思考过程', value: 'thinking' },
{ id: 'deep', label: '深度思考', desc: '整轮对话都使用思考模型', value: 'deep' }
];
const permissionModeOptions: Array<{
id: PermissionModeValue;
label: string;
desc: string;
}> = [
{ id: 'readonly', label: '只读', desc: '仅允许读取/检索类工具,修改操作将被拒绝' },
{ id: 'approval', label: '批准', desc: '对工作区文件进行修改的工具需人工批准后执行' },
{
id: 'auto_approval',
label: '自动审核',
desc: '工作区内写入直通;高风险操作由后台审核智能体自动审批'
},
{ id: 'unrestricted', label: '无限制', desc: '工具按常规流程直接执行' }
];
const policyStore = usePolicyStore();
const modelStore = useModelStore();
const filteredModelOptions = computed(() =>
(modelStore.models || []).map((opt: any) => {
const multimodal = String(opt.multimodal || 'none');
return {
id: opt.key,
value: opt.key,
label: opt.label,
desc: opt.description || '',
badge: multimodal === 'image,video' ? '图文' : opt.deepOnly ? '深度思考' : undefined,
deepOnly: !!opt.deepOnly,
fastOnly: !!opt.fastOnly,
disabled: policyStore.disabledModelSet.has(opt.key)
};
})
);
const thinkingPresets = [
{ id: 'low', label: '低', value: 10 },
{ id: 'medium', label: '中', value: 5 },
{ id: 'high', label: '高', value: 3 }
];
const imageCompressionOptions = [
{ id: 'original', label: '原图', desc: '不压缩' },
{ id: '1080p', label: '1080p', desc: '最长边不超过 1080p 等比缩放' },
{ id: '720p', label: '720p', desc: '最长边不超过 720p 等比缩放' },
{ id: '540p', label: '540p', desc: '最长边不超过 540p 等比缩放' }
] as const;
const defaultModelLabel = computed(() => {
return (
filteredModelOptions.value.find((option: any) => option.value === form.value.default_model)
?.label || '未设置'
);
});
const runModeLabel = computed(() => {
return runModeOptions.find((option) => isRunModeActive(option.value))?.label || '未设置';
});
const permissionModeLabel = computed(() => {
return (
permissionModeOptions.find((option) => option.id === form.value.default_permission_mode)
?.label || '未设置'
);
});
const imageCompressionLabel = computed(() => {
return (
imageCompressionOptions.find((option) => option.id === form.value.image_compression)?.label ||
'未设置'
);
});
const communicationStyleLabel = computed(() => {
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 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 === 'general' &&
isAppShell.value &&
!appUpdateInfo.value &&
!appUpdateChecking.value
) {
checkAppUpdate();
}
}
);
const setActiveTab = (tab: PersonalTab) => {
activeTab.value = tab;
closeDropdown();
};
const startTutorial = () => {
fetch('/api/tutorial-status', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify({ tutorial_completed: true })
}).catch(() => {});
tutorialStore.markCompleted();
personalization.closeDrawer();
window.setTimeout(() => {
tutorialStore.startTutorial();
}, 320);
};
const applyThinkingPreset = (value: number) => {
personalization.setThinkingInterval(value);
};
const isRunModeActive = (value: RunModeValue) => {
if (value === null) {
return !form.value.default_run_mode;
}
return form.value.default_run_mode === value;
};
const setDefaultRunMode = (value: RunModeValue) => {
if (checkModeModelConflict(value, form.value.default_model)) {
return;
}
personalization.setDefaultRunMode(value);
};
const setDefaultModel = (value: string) => {
if (policyStore.disabledModelSet.has(value)) {
uiStore.pushToast({
title: '模型被禁用',
message: '已被管理员禁用,无法选择',
type: 'warning'
});
return;
}
if (checkModeModelConflict(form.value.default_run_mode, value)) {
return;
}
personalization.setDefaultModel(value);
};
const setDefaultPermissionMode = (value: PermissionModeValue) => {
personalization.setDefaultPermissionMode(value);
};
const setCommunicationStyle = (value: 'default' | 'human_like' | '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 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 compactMessageDisplayOptions = [
{
id: 'full',
label: '完整信息',
desc: '审核、子智能体等系统消息显示完整原始内容',
value: 'full' as const
},
{
id: 'brief',
label: '简略信息',
desc: '用一行横线概要替代系统消息',
value: 'brief' as const
}
];
const selectCompactMessageDisplay = (mode: 'full' | 'brief') => {
personalization.setCompactMessageDisplay(mode);
closeDropdown();
};
const openAdminPanel = () => {
window.open('/admin/monitor', '_blank', 'noopener');
personalization.closeDrawer();
};
const openCustomTools = () => {
window.open('/admin/custom-tools', '_blank', 'noopener');
personalization.closeDrawer();
};
const openApiAdmin = () => {
window.open('/admin/api', '_blank', 'noopener');
personalization.closeDrawer();
};
const openMcpConfig = () => {
window.open('/admin/policy', '_blank', 'noopener');
personalization.closeDrawer();
};
// ===== 主题切换 =====
const { setTheme, loadTheme } = useTheme();
const themeOptions: Array<{ id: ThemeKey; label: string; desc: string; swatches: string[] }> = [
{
id: 'classic',
label: '经典',
desc: '米色质感,柔和高对比',
swatches: ['#eeece2', '#f7f3ea', '#da7756']
},
{
id: 'light',
label: '明亮',
desc: '纯白底色 + 优雅灰,简洁清爽',
swatches: ['#ffffff', '#f7f7f8', '#6b7280']
},
{
id: 'dark',
label: '夜间',
desc: '深灰 + 黑,低亮度并保持彩色点缀',
swatches: ['#1a1a1a', '#2a2a2a', '#3a3a3a']
}
];
const activeTheme = ref<ThemeKey>(loadTheme());
const themeLabel = computed(() => {
return themeOptions.find((option) => option.id === activeTheme.value)?.label || '经典';
});
// 监听store中的theme变化同步到activeTheme用于从后端加载主题后更新UI
watch(
() => personalization.form.theme,
(newTheme) => {
if (newTheme && newTheme !== activeTheme.value) {
activeTheme.value = newTheme as ThemeKey;
setTheme(newTheme as ThemeKey);
}
},
{ immediate: true }
);
const selectThemeOption = (theme: ThemeKey) => {
applyThemeOption(theme);
closeDropdown();
};
const applyThemeOption = async (theme: ThemeKey) => {
activeTheme.value = theme;
setTheme(theme);
// 同步更新到store并保存到后端配置文件
personalization.updateField({ key: 'theme', value: theme });
try {
const resp = await fetch('/api/personalization', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ theme })
});
const result = await resp.json();
if (!resp.ok || !result.success) {
console.warn('保存主题到后端失败:', result.error);
}
} catch (error) {
console.warn('保存主题到后端失败:', error);
}
};
const applyDefaultHideWorkspaceOption = async (hidden: boolean) => {
const next = !!hidden;
persistDefaultHideWorkspace(next);
personalization.updateField({ key: 'default_hide_workspace', value: next });
if (next) {
uiStore.setWorkspaceCollapsed(true);
}
try {
const resp = await fetch('/api/personalization', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ default_hide_workspace: next })
});
const result = await resp.json();
if (!resp.ok || !result.success) {
console.warn('保存默认隐藏工作区失败:', result.error);
}
} catch (error) {
console.warn('保存默认隐藏工作区失败:', error);
}
};
</script>
<style scoped>
.settings-redesign-card {
--settings-floating-menu-bg: color-mix(in srgb, var(--surface-soft) 90%, var(--surface-raised));
--settings-floating-menu-hover: var(--hover-bg);
--settings-floating-menu-shadow: none;
--settings-tab-hover-bg: var(--theme-tab-active);
--settings-tab-active-bg: var(--theme-tab-active);
position: relative;
width: min(70vw, calc(100vw - 24px));
height: min(80vh, calc(100vh - 24px));
padding: 30px 42px 24px;
border-radius: 32px;
overflow: visible;
background: var(--theme-surface-soft);
border: 1px solid var(--theme-control-border);
box-shadow: none;
}
:global(html[data-theme='dark']) .settings-redesign-card,
:global(body[data-theme='dark']) .settings-redesign-card {
--settings-floating-menu-bg: var(--surface-panel);
--settings-floating-menu-hover: var(--hover-bg);
--settings-floating-menu-shadow: none;
--settings-tab-hover-bg: var(--hover-bg);
--settings-tab-active-bg: var(--hover-bg);
}
.settings-close-button {
position: absolute;
top: 27px;
left: 45px;
width: 42px;
height: 42px;
border: 0;
border-radius: 50%;
background: transparent;
color: var(--claude-text);
display: grid;
place-items: center;
cursor: pointer;
z-index: 3;
}
.settings-close-button:hover {
background: var(--hover-bg);
}
.settings-close-button svg {
width: 25px;
height: 25px;
stroke-width: 2.15;
}
.settings-redesign-body,
.settings-redesign-form {
height: 100%;
min-height: 0;
}
.settings-redesign-layout {
height: 100%;
display: grid;
grid-template-columns: minmax(190px, 2fr) minmax(0, 8fr);
column-gap: 34px;
min-height: 0;
}
.settings-redesign-tabs {
margin-top: 68px;
height: calc(100% - 68px);
padding-right: 28px;
border-right: 1px solid var(--theme-control-border);
display: flex;
flex-direction: column;
gap: 3px;
min-width: 0;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: none;
-ms-overflow-style: none;
}
.settings-redesign-tab {
width: 100%;
height: 47px;
flex: 0 0 auto;
border: 0;
border-radius: 12px;
background: transparent;
color: var(--claude-text);
padding: 0 13px;
display: flex;
align-items: center;
gap: 14px;
cursor: pointer;
text-align: left;
font-size: 16px;
letter-spacing: -0.01em;
}
.settings-redesign-tab:hover {
background: var(--settings-tab-hover-bg);
}
.settings-redesign-tab.active {
background: var(--settings-tab-active-bg);
font-weight: 640;
}
.settings-tab-icon {
--icon-size: 20px;
flex: 0 0 auto;
}
.settings-tab-icon--chat {
width: 30px;
height: 30px;
display: inline-flex;
align-items: center;
justify-content: center;
margin-left: -5px;
margin-right: -5px;
}
.settings-tab-icon--chat svg {
width: 30px;
height: 30px;
}
.settings-redesign-content {
min-width: 0;
min-height: 0;
height: 100%;
overflow: visible;
display: flex;
flex-direction: column;
position: relative;
}
.settings-redesign-content-header {
height: 46px;
flex: 0 0 46px;
display: flex;
align-items: center;
}
.settings-redesign-content-header h2 {
margin: 0;
color: var(--claude-text);
font-size: 28px;
line-height: 1;
letter-spacing: -0.045em;
font-weight: 730;
}
.settings-redesign-title-line {
height: 1px;
background: var(--theme-control-border);
margin: 22px 0 8px;
flex: 0 0 1px;
}
.settings-redesign-scroll {
min-height: 0;
flex: 1 1 auto;
overflow-y: auto;
overflow-x: visible;
padding-right: 18px;
padding-bottom: 86px;
scroll-padding-bottom: 86px;
scrollbar-width: none;
-ms-overflow-style: none;
}
.settings-redesign-tabs::-webkit-scrollbar,
.settings-redesign-scroll::-webkit-scrollbar,
.settings-floating-menu::-webkit-scrollbar,
.app-update-changelog-content::-webkit-scrollbar {
display: none;
}
.settings-floating-menu,
.app-update-changelog-content {
scrollbar-width: none;
-ms-overflow-style: none;
}
.settings-page {
max-width: 900px;
min-height: 0;
}
.settings-select-row,
.settings-input-row,
.settings-list-row,
.settings-action-row,
.settings-toggle-row {
min-height: 64px;
border-bottom: 1px solid var(--theme-control-border);
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 18px;
padding: 11px 0;
color: var(--claude-text);
}
.settings-list-row.tall,
.settings-input-row.stackable {
align-items: flex-start;
}
.settings-list-row.tall {
grid-template-columns: minmax(180px, 1fr) minmax(0, 360px);
}
.settings-row-copy {
min-width: 0;
display: block;
}
.settings-row-title {
display: block;
color: var(--claude-text);
font-size: 14px;
font-weight: 550;
letter-spacing: -0.01em;
line-height: 1.32;
}
.settings-row-desc {
display: block;
margin-top: 3px;
color: var(--claude-text-secondary);
font-size: 12.5px;
line-height: 1.38;
}
.settings-select-wrap {
position: relative;
display: inline-flex;
justify-self: end;
}
.settings-select-button {
width: max-content;
min-width: 0;
height: 36px;
border: 0;
border-radius: 12px;
background: transparent;
color: var(--claude-text);
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 0 8px 0 10px;
cursor: pointer;
font-size: 15px;
white-space: nowrap;
}
.settings-select-button:hover {
background: var(--hover-bg);
}
.settings-select-button .select-chevron {
width: 12px;
height: 12px;
display: inline-flex;
align-items: center;
justify-content: center;
transition: transform 0.15s ease;
}
.settings-select-button .select-chevron::before {
content: '';
width: 7px;
height: 7px;
border-right: 1.5px solid currentColor;
border-bottom: 1.5px solid currentColor;
transform: translateY(-1.5px) rotate(45deg);
}
.settings-select-wrap.open .settings-select-button .select-chevron {
transform: rotate(180deg);
}
.settings-floating-menu {
position: fixed;
right: auto;
top: auto;
width: 300px;
max-height: min(420px, 60vh);
overflow: auto;
padding: 6px;
border-radius: 20px;
background: var(--settings-floating-menu-bg);
border: 1px solid var(--theme-control-border);
box-shadow: var(--settings-floating-menu-shadow);
opacity: 1;
backdrop-filter: none;
z-index: 2147483647;
display: none;
}
.settings-select-wrap.open .settings-floating-menu {
display: block;
}
.settings-floating-menu.dark {
background: var(--surface-panel) !important;
box-shadow: none !important;
}
.settings-floating-menu.dark .settings-menu-option:hover {
background: var(--hover-bg) !important;
}
:global(html[data-theme='dark']) :global(.settings-floating-menu),
:global(body[data-theme='dark']) :global(.settings-floating-menu) {
background: var(--surface-panel) !important;
box-shadow: none !important;
}
:global(html[data-theme='dark']) :global(.settings-menu-option:hover),
:global(body[data-theme='dark']) :global(.settings-menu-option:hover) {
background: var(--hover-bg) !important;
}
.settings-menu-option {
width: 100%;
min-height: 38px;
border: 0;
border-radius: 12px;
background: transparent;
color: var(--claude-text);
display: grid;
grid-template-columns: minmax(0, 1fr) 22px;
row-gap: 0;
column-gap: 8px;
align-items: center;
padding: 7px 10px;
text-align: left;
cursor: pointer;
}
.settings-menu-option:hover {
background: var(--settings-floating-menu-hover);
}
.settings-menu-option:disabled {
opacity: 0.48;
cursor: not-allowed;
}
.settings-menu-option strong {
grid-column: 1;
grid-row: 1;
display: block;
color: var(--claude-text);
font-size: 15px;
font-weight: 550;
letter-spacing: -0.01em;
min-width: 0;
}
.settings-menu-option span {
grid-column: 1;
grid-row: 2;
display: block;
margin-top: 0;
color: var(--claude-text-secondary);
font-size: 11px;
line-height: 1.15;
min-width: 0;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.settings-menu-option svg {
grid-column: 2;
grid-row: 1 / span 2;
align-self: center;
width: 21px;
height: 21px;
fill: none;
stroke: var(--claude-text);
stroke-width: 2.25;
stroke-linecap: round;
stroke-linejoin: round;
opacity: 0;
}
.settings-menu-option.selected svg {
opacity: 1;
}
.settings-input-row input,
.settings-add-row input,
.settings-number-row input,
.settings-number-input,
.settings-compression-grid input {
height: 38px;
border: 1px solid var(--theme-control-border);
border-radius: 12px;
background: transparent;
color: var(--claude-text);
padding: 0 12px;
outline: none;
font-size: 13px;
}
.settings-number-input {
width: 120px;
text-align: right;
}
.settings-section-divider {
display: flex;
align-items: center;
margin: 18px 0 6px;
}
.settings-section-divider__label {
font-size: 12px;
font-weight: 600;
color: var(--claude-text-secondary);
letter-spacing: 0.04em;
}
.settings-section-divider::after {
content: '';
flex: 1;
height: 1px;
margin-left: 10px;
background: var(--theme-control-border);
}
.settings-input-row > input {
width: min(360px, 40vw);
text-align: right;
}
.settings-input-row input:focus,
.settings-add-row input:focus,
.settings-number-row input:focus,
.settings-number-input:focus,
.settings-compression-grid input:focus {
border-color: var(--claude-text-secondary);
box-shadow: none;
}
.settings-input-stack {
width: min(430px, 46vw);
display: grid;
gap: 8px;
justify-self: end;
}
.settings-input-stack.wide {
width: min(360px, 42vw);
}
.settings-input-stack input {
width: 100%;
}
.settings-chip-row {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.settings-chip-row.right {
justify-content: flex-end;
}
.settings-chip-row button,
.settings-number-row button,
.settings-add-row button,
.settings-secondary-button,
.settings-primary-button {
border: 1px solid var(--theme-control-border);
border-radius: 999px;
background: transparent;
color: var(--claude-text);
min-height: 32px;
padding: 0 12px;
font-size: 12px;
cursor: pointer;
}
.settings-chip-row button:hover,
.settings-number-row button:hover,
.settings-add-row button:hover,
.settings-secondary-button:hover {
background: var(--hover-bg);
}
.settings-chip-row button.active,
.settings-primary-button {
border-color: var(--claude-accent);
background: var(--claude-accent);
color: var(--on-accent);
}
.settings-secondary-button.danger {
color: var(--claude-warning);
}
.settings-inline-actions {
display: inline-flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
flex-wrap: wrap;
}
.settings-inline-actions.right {
margin-top: 10px;
}
.settings-mini-status {
color: var(--claude-text-secondary);
font-size: 12px;
}
.settings-mini-status.warning {
color: var(--claude-warning);
}
.settings-add-row,
.settings-number-row {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
}
.settings-add-row input,
.settings-number-row input {
min-width: 0;
flex: 1 1 auto;
}
.settings-consideration-list {
margin: 0;
padding: 0;
list-style: none;
display: grid;
gap: 6px;
}
.settings-consideration-list li {
min-height: 34px;
border: 1px solid var(--theme-control-border);
border-radius: 12px;
padding: 0 8px;
display: grid;
grid-template-columns: 18px minmax(0, 1fr) 28px;
gap: 8px;
align-items: center;
color: var(--claude-text);
font-size: 13px;
}
.settings-consideration-list button {
border: 0;
background: transparent;
color: var(--claude-text-secondary);
cursor: pointer;
}
.drag-handle {
color: var(--claude-text-secondary);
cursor: grab;
}
.settings-toggle-row {
cursor: pointer;
position: relative;
}
.settings-toggle-row input[type='checkbox'] {
position: absolute;
opacity: 0;
pointer-events: none;
}
.settings-toggle-row .fancy-check {
width: 28px;
height: 28px;
border-radius: 6px;
background: transparent;
display: inline-flex;
align-items: center;
justify-content: center;
box-shadow: none;
transition: opacity 0.2s ease;
}
.settings-toggle-row .fancy-check svg {
width: 22px;
height: 22px;
overflow: visible;
}
.fancy-path {
fill: none;
stroke: var(--claude-text-secondary);
stroke-width: 5;
stroke-linecap: round;
stroke-linejoin: round;
transition:
stroke-dasharray 0.5s ease,
stroke-dashoffset 0.5s ease,
stroke 0.2s ease;
stroke-dasharray: 241 9999999;
stroke-dashoffset: 0;
}
.settings-toggle-row input:checked + .fancy-check .fancy-path {
stroke: var(--claude-text-secondary);
stroke-dasharray: 70.5096664428711 9999999;
stroke-dashoffset: -262.2723388671875;
}
.settings-group-block {
border-bottom: 1px solid var(--theme-control-border);
padding: 14px 0 16px;
}
.settings-group-title {
margin-bottom: 6px;
}
.settings-toggle-row.inner {
min-height: 48px;
padding: 7px 0 7px 18px;
border-bottom: 0;
}
.settings-check-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0 26px;
}
.settings-compression-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px 14px;
margin-top: 10px;
}
.settings-compression-grid label {
display: grid;
gap: 5px;
color: var(--claude-text-secondary);
font-size: 12px;
}
.settings-save-bar {
position: absolute;
right: 18px;
bottom: 0;
left: 0;
min-height: 54px;
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
background: transparent;
pointer-events: none;
}
.settings-save-bar > * {
pointer-events: auto;
}
.personal-status-group {
min-height: 20px;
font-size: 12px;
}
.status.success {
color: var(--claude-success);
}
.status.error,
.usage-summary-error {
color: var(--claude-warning);
}
.usage-summary-page {
display: block;
padding: 0;
}
.settings-stats-card,
.usage-summary-card {
width: 100%;
max-width: 720px;
padding: 0;
min-height: 0;
display: flex;
flex-direction: column;
gap: 14px;
}
.usage-summary-header h3 {
margin: 4px 0 6px;
font-size: 20px;
}
.usage-summary-eyebrow {
margin: 0;
font-size: 11px;
color: var(--claude-accent-strong);
letter-spacing: 0.16em;
text-transform: uppercase;
}
.usage-summary-desc,
.usage-summary-note,
.usage-summary-meta {
margin: 0;
color: var(--claude-text-secondary);
font-size: 12px;
}
.usage-summary-grid {
display: grid;
gap: 10px;
}
.usage-summary-grid--tokens {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.usage-summary-grid--counts {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.usage-summary-item {
padding: 12px 14px;
border-radius: 14px;
border: 1px solid var(--theme-control-border);
background: transparent;
}
.usage-summary-item .label {
font-size: 12px;
color: var(--claude-text-secondary);
}
.usage-summary-item .value {
margin-top: 4px;
font-size: 20px;
font-weight: 600;
color: var(--claude-text);
}
.usage-summary-item .value--success {
color: var(--claude-success);
}
.usage-summary-item .value--warning {
color: var(--claude-warning);
}
.usage-summary-meta {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.usage-summary-refresh {
border: 1px solid var(--theme-control-border);
border-radius: 999px;
padding: 7px 14px;
background: transparent;
color: var(--claude-text);
font-size: 12px;
cursor: pointer;
}
.admin-monitor-page {
display: block;
padding: 0;
}
@media (max-width: 900px) {
.settings-redesign-card {
width: 100vw;
height: 100vh;
border-radius: 0;
padding: 30px 24px;
}
.settings-close-button {
left: 27px;
top: 20px;
}
.settings-redesign-layout {
grid-template-columns: 1fr;
gap: 0;
}
.settings-redesign-tabs {
margin-top: 68px;
height: auto;
padding-right: 0;
padding-bottom: 12px;
border-right: 0;
border-bottom: 1px solid var(--theme-control-border);
flex-direction: row;
overflow: auto;
scrollbar-width: none;
}
.settings-redesign-tabs::-webkit-scrollbar {
display: none;
}
.settings-redesign-tab {
width: auto;
white-space: nowrap;
}
.settings-redesign-content {
padding-top: 18px;
}
.settings-select-row,
.settings-input-row,
.settings-list-row,
.settings-action-row,
.settings-toggle-row {
grid-template-columns: minmax(0, 1fr);
gap: 8px;
}
.settings-select-wrap,
.settings-input-stack,
.settings-input-row > input {
justify-self: stretch;
width: 100%;
}
.settings-select-button {
margin-left: auto;
}
.settings-floating-menu {
width: min(340px, calc(100vw - 48px));
}
.settings-check-grid,
.settings-compression-grid,
.usage-summary-grid--tokens,
.usage-summary-grid--counts {
grid-template-columns: 1fr;
}
}
/* ── 语音模型下载进度条 ── */
.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>