- 修复运行时 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
4317 lines
168 KiB
Vue
4317 lines
168 KiB
Vue
<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 Body(Markdown)</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>
|