feat(personalization): 将回答时必须考虑的信息从条列式改为大段文字输入

This commit is contained in:
JOJO 2026-07-10 15:40:18 +08:00
parent b2cbe0ab5d
commit 19746e67d3
7 changed files with 361 additions and 342 deletions

View File

@ -252,7 +252,7 @@ class ToolsDefinitionContextToolsMixin:
"type": "function",
"function": {
"name": "manage_personalization",
"description": "管理用户个性化设置。支持读取所有配置或更新单个字段。可修改字段self_identifyAI自称最多20字、user_nameAI如何称呼用户最多20字、profession用户职业最多20字、tone交流语气最多20字、considerations注意事项列表字符串数组最多10项每项最多50字、theme主题配色classic-经典/light-明亮/dark-暗黑、communication_style交流风格default-标准AI风格/human_like-拟人聊天风格/auto-自动选择交流风格、conversation_continuity对话连续性high-高/medium-中/low-低)。更新时会自动验证格式,验证通过后立即保存并生效,新主题会立即应用到界面。",
"description": "管理用户个性化设置。支持读取所有配置或更新单个字段。可修改字段self_identifyAI自称最多20字、user_nameAI如何称呼用户最多20字、profession用户职业最多20字、tone交流语气最多20字、considerations回答时必须考虑的信息字符串最多2000字、theme主题配色classic-经典/light-明亮/dark-暗黑、communication_style交流风格default-标准AI风格/human_like-拟人聊天风格/auto-自动选择交流风格、conversation_continuity对话连续性high-高/medium-中/low-低)。更新时会自动验证格式,验证通过后立即保存并生效,新主题会立即应用到界面。",
"parameters": {
"type": "object",
"properties": self._inject_intent({
@ -267,7 +267,7 @@ class ToolsDefinitionContextToolsMixin:
"description": "要更新的字段名仅action=update时需要"
},
"value": {
"description": "新值仅action=update时需要。注意事项提供字符串数组,其他字段提供字符串"
"description": "新值仅action=update时需要。注意事项提供字符串,其他字段提供字符串"
}
}),
"required": ["action"]

View File

@ -72,8 +72,7 @@ from modules.personalization_manager import (
save_personalization_config,
build_personalization_prompt,
MAX_SHORT_FIELD_LENGTH,
MAX_CONSIDERATION_LENGTH,
MAX_CONSIDERATION_ITEMS,
MAX_CONSIDERATION_TEXT_LENGTH,
ALLOWED_THEMES,
ALLOWED_COMMUNICATION_STYLES,
ALLOWED_CONVERSATION_CONTINUITY,
@ -1830,7 +1829,7 @@ class MainTerminalToolsExecutionMixin:
"user_name": f"AI如何称呼用户: {result.get('user_name') or '(未设置)'}",
"profession": f"用户职业: {result.get('profession') or '(未设置)'}",
"tone": f"交流语气: {result.get('tone') or '(未设置)'}",
"considerations": f"注意事项: {len(result.get('considerations') or [])}",
"considerations": f"注意事项: {'已设置' if (result.get('considerations') or '').strip() else '未设置'}",
"theme": f"主题配色: {result.get('theme', 'classic')}",
"communication_style": f"交流风格: {result.get('communication_style', 'default')}",
"conversation_continuity": f"对话连续性: {result.get('conversation_continuity', 'medium')}"
@ -1874,17 +1873,11 @@ class MainTerminalToolsExecutionMixin:
validation_errors.append(f"{field} 不能超过 {MAX_SHORT_FIELD_LENGTH} 个字符")
elif field == "considerations":
# 注意事项数组验证
if not isinstance(value, list):
validation_errors.append("considerations 必须是字符串数组")
else:
if len(value) > MAX_CONSIDERATION_ITEMS:
validation_errors.append(f"considerations 不能超过 {MAX_CONSIDERATION_ITEMS}")
for i, item in enumerate(value):
if not isinstance(item, str):
validation_errors.append(f"considerations[{i}] 必须是字符串")
elif len(item) > MAX_CONSIDERATION_LENGTH:
validation_errors.append(f"considerations[{i}] 不能超过 {MAX_CONSIDERATION_LENGTH} 个字符")
# 注意事项文本验证
if not isinstance(value, str):
validation_errors.append("considerations 必须是字符串")
elif len(value) > MAX_CONSIDERATION_TEXT_LENGTH:
validation_errors.append(f"considerations 不能超过 {MAX_CONSIDERATION_TEXT_LENGTH} 个字符")
elif field == "theme":
# 主题验证

View File

@ -5,7 +5,7 @@ from __future__ import annotations
import json
from copy import deepcopy
from pathlib import Path
from typing import Any, Dict, Iterable, Optional, Union
from typing import Any, Dict, Optional, Union
try:
from config.limits import THINKING_FAST_INTERVAL
@ -30,7 +30,7 @@ GOAL_MAX_TOKENS_MAX = 100_000_000
PERSONALIZATION_FILENAME = "personalization.json"
MAX_SHORT_FIELD_LENGTH = 20
MAX_CONSIDERATION_LENGTH = 50
MAX_CONSIDERATION_TEXT_LENGTH = 2000
MAX_CONSIDERATION_ITEMS = 10
TONE_PRESETS = ["健谈", "幽默", "直言不讳", "鼓励性", "诗意", "企业商务", "打破常规", "同理心"]
THINKING_INTERVAL_MIN = 1
@ -705,12 +705,10 @@ def build_personalization_prompt(
if config.get("tone"):
lines.append(f"用户希望你使用 {config['tone']} 的语气与TA交流")
considerations: Iterable[str] = config.get("considerations") or []
considerations = [item for item in considerations if item]
considerations: str = config.get("considerations") or ""
if considerations:
lines.append("用户希望你在回答问题时必须考虑的信息是:")
for idx, item in enumerate(considerations, 1):
lines.append(f"{idx}. {item}")
lines.append(considerations)
conversation_continuity = str(config.get("conversation_continuity") or "medium").strip().lower()
if conversation_continuity == "high":
@ -761,9 +759,13 @@ def _sanitize_short_field(value: Optional[str]) -> str:
return text[:MAX_SHORT_FIELD_LENGTH]
def _sanitize_considerations(value: Any) -> list:
if not isinstance(value, list):
return []
def _sanitize_considerations(value: Any) -> str:
"""Sanitize considerations text. Legacy array data is joined with newlines."""
if value is None:
return ""
if isinstance(value, str):
return value[:MAX_CONSIDERATION_TEXT_LENGTH]
if isinstance(value, list):
cleaned = []
for item in value:
if not isinstance(item, str):
@ -771,10 +773,11 @@ def _sanitize_considerations(value: Any) -> list:
text = item.strip()
if not text:
continue
cleaned.append(text[:MAX_CONSIDERATION_LENGTH])
cleaned.append(text)
if len(cleaned) >= MAX_CONSIDERATION_ITEMS:
break
return cleaned
return "\n".join(cleaned)[:MAX_CONSIDERATION_TEXT_LENGTH]
return ""
def _sanitize_thinking_interval(value: Any) -> Optional[int]:

View File

@ -375,8 +375,14 @@ function renderEditFile(result: any, args: any): string {
if (details.length > 0) {
html += '<div class="tool-result-diff scrollable">';
const renderDiffLine = (lineNo: number | null, marker: string, text: string, className = '') => {
const lineNoText = typeof lineNo === 'number' && Number.isFinite(lineNo) ? String(lineNo) : '';
const renderDiffLine = (
lineNo: number | null,
marker: string,
text: string,
className = ''
) => {
const lineNoText =
typeof lineNo === 'number' && Number.isFinite(lineNo) ? String(lineNo) : '';
html += `<div class="diff-line${className ? ` ${className}` : ''}"><span class="diff-line-number">${escapeHtml(lineNoText)}</span><span class="diff-marker">${escapeHtml(marker)}</span><span class="diff-content">${escapeHtml(text)}</span></div>`;
};
const hunks: any[] = [];
@ -391,14 +397,24 @@ function renderEditFile(result: any, args: any): string {
if (oldText) {
const oldLines = oldText.split('\n');
oldLines.forEach((line: string, lineIdx: number) => {
rows.push({ lineNo: startLine === null ? null : startLine + lineIdx, marker: '-', text: line, className: 'diff-remove' });
rows.push({
lineNo: startLine === null ? null : startLine + lineIdx,
marker: '-',
text: line,
className: 'diff-remove'
});
});
}
if (newText) {
const newLines = newText.split('\n');
newLines.forEach((line: string, lineIdx: number) => {
rows.push({ lineNo: startLine === null ? null : startLine + lineIdx, marker: '+', text: line, className: 'diff-add' });
rows.push({
lineNo: startLine === null ? null : startLine + lineIdx,
marker: '+',
text: line,
className: 'diff-add'
});
});
}
return rows;
@ -410,30 +426,37 @@ function renderEditFile(result: any, args: any): string {
const rawBeforeLines = Array.isArray(context.before_lines) ? context.before_lines : [];
const rawAfterLines = Array.isArray(context.after_lines) ? context.after_lines : [];
const beforeLines =
rawBeforeLines.length >= 2 && rawBeforeLines.every((item: any) => String(item?.text ?? '') === '')
rawBeforeLines.length >= 2 &&
rawBeforeLines.every((item: any) => String(item?.text ?? '') === '')
? []
:
rawBeforeLines.length >= 2 && String(rawBeforeLines[0]?.text ?? '') === ''
: rawBeforeLines.length >= 2 && String(rawBeforeLines[0]?.text ?? '') === ''
? rawBeforeLines.slice(1)
: rawBeforeLines;
const afterLines =
rawAfterLines.length >= 2 && rawAfterLines.every((item: any) => String(item?.text ?? '') === '')
rawAfterLines.length >= 2 &&
rawAfterLines.every((item: any) => String(item?.text ?? '') === '')
? []
:
rawAfterLines.length >= 2 && String(rawAfterLines[rawAfterLines.length - 1]?.text ?? '') === ''
: rawAfterLines.length >= 2 &&
String(rawAfterLines[rawAfterLines.length - 1]?.text ?? '') === ''
? rawAfterLines.slice(0, -1)
: rawAfterLines;
const contextOldStartLine = Number(context?.old_start_line ?? detail.matched_lines?.[0]);
const contextOldEndLine = Number(context?.old_end_line ?? contextOldStartLine);
const oldLineCount = Number.isFinite(contextOldStartLine) && Number.isFinite(contextOldEndLine)
const oldLineCount =
Number.isFinite(contextOldStartLine) && Number.isFinite(contextOldEndLine)
? Math.max(1, contextOldEndLine - contextOldStartLine + 1)
: Math.max(1, (oldText ? oldText.split('\n').length : 1));
: Math.max(1, oldText ? oldText.split('\n').length : 1);
const newLineCount = newText === '' ? 0 : newText.split('\n').length;
const afterContextOffset = newLineCount - oldLineCount;
beforeLines.forEach((item: any) => {
rows.push({ lineNo: Number(item.line), marker: ' ', text: String(item.text ?? ''), className: '' });
rows.push({
lineNo: Number(item.line),
marker: ' ',
text: String(item.text ?? ''),
className: ''
});
});
rows.push(...buildPairRows(context));
afterLines.forEach((item: any) => {
@ -441,28 +464,41 @@ function renderEditFile(result: any, args: any): string {
const lineNo = Number.isFinite(rawLineNo) ? rawLineNo + afterContextOffset : rawLineNo;
rows.push({ lineNo, marker: ' ', text: String(item.text ?? ''), className: '' });
});
const numberedRows = rows.filter((row) => typeof row.lineNo === 'number' && Number.isFinite(row.lineNo));
const numberedRows = rows.filter(
(row) => typeof row.lineNo === 'number' && Number.isFinite(row.lineNo)
);
hunks.push({
rows,
minLine: numberedRows.length ? Math.min(...numberedRows.map((row) => row.lineNo)) : null,
maxLine: numberedRows.length ? Math.max(...numberedRows.map((row) => row.lineNo)) : null,
minLine: numberedRows.length
? Math.min(...numberedRows.map((row) => row.lineNo))
: null,
maxLine: numberedRows.length ? Math.max(...numberedRows.map((row) => row.lineNo)) : null
});
});
} else {
const rows = buildPairRows();
const numberedRows = rows.filter((row) => typeof row.lineNo === 'number' && Number.isFinite(row.lineNo));
const numberedRows = rows.filter(
(row) => typeof row.lineNo === 'number' && Number.isFinite(row.lineNo)
);
hunks.push({
rows,
minLine: numberedRows.length ? Math.min(...numberedRows.map((row) => row.lineNo)) : null,
maxLine: numberedRows.length ? Math.max(...numberedRows.map((row) => row.lineNo)) : null,
maxLine: numberedRows.length ? Math.max(...numberedRows.map((row) => row.lineNo)) : null
});
}
});
const sortedHunks = hunks.sort((a, b) => (a.minLine ?? Number.MAX_SAFE_INTEGER) - (b.minLine ?? Number.MAX_SAFE_INTEGER));
const sortedHunks = hunks.sort(
(a, b) => (a.minLine ?? Number.MAX_SAFE_INTEGER) - (b.minLine ?? Number.MAX_SAFE_INTEGER)
);
const mergedHunks: any[] = [];
sortedHunks.forEach((hunk) => {
const last = mergedHunks[mergedHunks.length - 1];
if (last && hunk.minLine !== null && last.maxLine !== null && hunk.minLine <= last.maxLine + 1) {
if (
last &&
hunk.minLine !== null &&
last.maxLine !== null &&
hunk.minLine <= last.maxLine + 1
) {
last.rows.push(...hunk.rows);
last.maxLine = Math.max(last.maxLine, hunk.maxLine ?? last.maxLine);
} else {
@ -481,7 +517,8 @@ function renderEditFile(result: any, args: any): string {
return true;
})
.sort((a: any, b: any) => {
const lineDelta = (a.lineNo ?? Number.MAX_SAFE_INTEGER) - (b.lineNo ?? Number.MAX_SAFE_INTEGER);
const lineDelta =
(a.lineNo ?? Number.MAX_SAFE_INTEGER) - (b.lineNo ?? Number.MAX_SAFE_INTEGER);
return lineDelta || (markerOrder[a.marker] ?? 9) - (markerOrder[b.marker] ?? 9);
})
.forEach((row: any) => renderDiffLine(row.lineNo, row.marker, row.text, row.className));
@ -562,7 +599,10 @@ function renderReadFile(result: any, args: any): string {
matches.forEach((match: any, idx: number) => {
if (idx > 0) html += '<div class="search-match-separator">⋯</div>';
html += '<div class="search-match-item">';
const lineLabel = match.line_start === match.line_end ? `${match.line_start}` : `${match.line_start}-${match.line_end}`;
const lineLabel =
match.line_start === match.line_end
? `${match.line_start}`
: `${match.line_start}-${match.line_end}`;
html += `<div class="search-match-line">行 ${lineLabel}</div>`;
html += `<pre>${escapeHtml(match.snippet || match.content || '')}</pre>`;
html += '</div>';
@ -813,10 +853,12 @@ function renderConversationSearch(result: any, args: any): string {
const results = Array.isArray(result?.results) ? result.results : [];
const keywords = Array.isArray(result?.keywords)
? result.keywords
: (Array.isArray(args?.keywords) ? args.keywords : []);
: Array.isArray(args?.keywords)
? args.keywords
: [];
const keywordText = keywords.length
? keywords.join(' / ')
: (result?.query || args?.query || '(未指定)');
: result?.query || args?.query || '(未指定)';
let html = '<div class="tool-result-meta">';
html += `<div><strong>关键词:</strong>${escapeHtml(keywordText)}</div>`;
html += `<div><strong>日期范围:</strong>${escapeHtml(result?.start_date || args?.start_date || '不限')} ~ ${escapeHtml(result?.end_date || args?.end_date || '不限')}</div>`;
@ -941,21 +983,21 @@ function formatPersonalizationFieldValue(field: string, value: any): string {
return independenceMap[String(value)] || String(value);
}
if (field === 'considerations') {
if (!Array.isArray(value) || value.length === 0) {
if (typeof value !== 'string' || value.trim() === '') {
return '未设置';
}
return value.map((item) => String(item)).join('、');
return value.trim();
}
return String(value);
}
function renderAskUser(result: any, args: any): string {
const question = args.question || result.question || '';
const context = args.context || result.context || '';
const status = formatToolStatusLabel(result, '✓ 已回答');
const answerText = result.status === 'answered' ? String(result.answer_text || result.message || '').trim() : '';
const answerText =
result.status === 'answered' ? String(result.answer_text || result.message || '').trim() : '';
let html = '<div class="tool-result-meta">';
html += `<div><strong>状态:</strong>${escapeHtml(status)}</div>`;
@ -983,7 +1025,11 @@ function renderAskUser(result: any, args: any): string {
if (answerText) {
html += '<div class="tool-result-content">';
html += '<div class="tool-result-meta">';
answerText.split('\n').map((line) => line.trim()).filter(Boolean).forEach((line) => {
answerText
.split('\n')
.map((line) => line.trim())
.filter(Boolean)
.forEach((line) => {
const separator = line.indexOf('');
if (separator > 0) {
const label = line.slice(0, separator + 1);
@ -1001,10 +1047,7 @@ function renderAskUser(result: any, args: any): string {
function renderManagePersonalization(result: any, args: any): string {
const action = String(args.action || result.action || 'read');
const status = formatToolStatusLabel(
result,
action === 'update' ? '✓ 已更新' : '✓ 已读取'
);
const status = formatToolStatusLabel(result, action === 'update' ? '✓ 已更新' : '✓ 已读取');
let html = '<div class="tool-result-meta">';
html += `<div><strong>操作:</strong>${escapeHtml(action === 'update' ? '更新配置' : '读取配置')}</div>`;
@ -1118,7 +1161,8 @@ function renderCreateSubAgent(result: any, args: any): string {
const message = result.message ?? result.summary ?? '';
const stats = result.stats || {};
const runtimeSeconds = result.runtime_seconds ?? result.elapsed_seconds ?? stats.runtime_seconds ?? 0;
const runtimeSeconds =
result.runtime_seconds ?? result.elapsed_seconds ?? stats.runtime_seconds ?? 0;
const apiCalls = stats.api_calls ?? stats.turn_count ?? 0;
const toolCount =
(stats.files_read || 0) +
@ -1215,7 +1259,7 @@ function renderGetSubAgentStatus(result: any, args: any): string {
const found = item.found !== false;
const status = String(item.status || '');
const taskId = item.task_id ?? '';
const summary = item.summary ?? (item.final_result?.message) ?? (item.final_result?.summary) ?? '';
const summary = item.summary ?? item.final_result?.message ?? item.final_result?.summary ?? '';
html += '<div class="sub-agent-status-item">';
html += `<div class="sub-agent-status-header">子智能体 #${escapeHtml(String(agentId))}</div>`;
@ -1291,8 +1335,7 @@ function renderGetSubAgentStatus(result: any, args: any): string {
.code-block pre,
.output-block pre {
scrollbar-width: thin; /* Firefox */
scrollbar-color: color-mix(in srgb, var(--claude-text-secondary) 55%, transparent)
transparent;
scrollbar-color: color-mix(in srgb, var(--claude-text-secondary) 55%, transparent) transparent;
}
.tool-result-content.scrollable::-webkit-scrollbar,
@ -1571,7 +1614,11 @@ function renderGetSubAgentStatus(result: any, args: any): string {
/* 彩蛋 */
.easter-egg-content {
padding: 12px;
background: linear-gradient(135deg, color-mix(in srgb, var(--decorative-gold) 10%, transparent), color-mix(in srgb, var(--decorative-pink) 10%, transparent));
background: linear-gradient(
135deg,
color-mix(in srgb, var(--decorative-gold) 10%, transparent),
color-mix(in srgb, var(--decorative-pink) 10%, transparent)
);
border: 1px solid color-mix(in srgb, var(--decorative-gold) 30%, transparent);
border-radius: 8px;
font-size: 14px;

View File

@ -317,8 +317,14 @@ function renderEditFile(result: any, args: any): string {
if (details.length > 0) {
html += '<div class="tool-result-diff scrollable">';
const renderDiffLine = (lineNo: number | null, marker: string, text: string, className = '') => {
const lineNoText = typeof lineNo === 'number' && Number.isFinite(lineNo) ? String(lineNo) : '';
const renderDiffLine = (
lineNo: number | null,
marker: string,
text: string,
className = ''
) => {
const lineNoText =
typeof lineNo === 'number' && Number.isFinite(lineNo) ? String(lineNo) : '';
html += `<div class="diff-line${className ? ` ${className}` : ''}"><span class="diff-line-number">${escapeHtml(lineNoText)}</span><span class="diff-marker">${escapeHtml(marker)}</span><span class="diff-content">${escapeHtml(text)}</span></div>`;
};
const hunks: any[] = [];
@ -334,13 +340,23 @@ function renderEditFile(result: any, args: any): string {
const startLine = Number.isFinite(startLineCandidate) ? startLineCandidate : null;
if (oldText) {
oldLines.forEach((line: string, lineIdx: number) => {
rows.push({ lineNo: startLine === null ? null : startLine + lineIdx, marker: '-', text: line, className: 'diff-remove' });
rows.push({
lineNo: startLine === null ? null : startLine + lineIdx,
marker: '-',
text: line,
className: 'diff-remove'
});
});
}
if (newText) {
newLines.forEach((line: string, lineIdx: number) => {
rows.push({ lineNo: startLine === null ? null : startLine + lineIdx, marker: '+', text: line, className: 'diff-add' });
rows.push({
lineNo: startLine === null ? null : startLine + lineIdx,
marker: '+',
text: line,
className: 'diff-add'
});
});
}
return rows;
@ -352,30 +368,37 @@ function renderEditFile(result: any, args: any): string {
const rawBeforeLines = Array.isArray(context.before_lines) ? context.before_lines : [];
const rawAfterLines = Array.isArray(context.after_lines) ? context.after_lines : [];
const beforeLines =
rawBeforeLines.length >= 2 && rawBeforeLines.every((item: any) => String(item?.text ?? '') === '')
rawBeforeLines.length >= 2 &&
rawBeforeLines.every((item: any) => String(item?.text ?? '') === '')
? []
:
rawBeforeLines.length >= 2 && String(rawBeforeLines[0]?.text ?? '') === ''
: rawBeforeLines.length >= 2 && String(rawBeforeLines[0]?.text ?? '') === ''
? rawBeforeLines.slice(1)
: rawBeforeLines;
const afterLines =
rawAfterLines.length >= 2 && rawAfterLines.every((item: any) => String(item?.text ?? '') === '')
rawAfterLines.length >= 2 &&
rawAfterLines.every((item: any) => String(item?.text ?? '') === '')
? []
:
rawAfterLines.length >= 2 && String(rawAfterLines[rawAfterLines.length - 1]?.text ?? '') === ''
: rawAfterLines.length >= 2 &&
String(rawAfterLines[rawAfterLines.length - 1]?.text ?? '') === ''
? rawAfterLines.slice(0, -1)
: rawAfterLines;
const contextOldStartLine = Number(context?.old_start_line ?? detail.matched_lines?.[0]);
const contextOldEndLine = Number(context?.old_end_line ?? contextOldStartLine);
const oldLineCount = Number.isFinite(contextOldStartLine) && Number.isFinite(contextOldEndLine)
const oldLineCount =
Number.isFinite(contextOldStartLine) && Number.isFinite(contextOldEndLine)
? Math.max(1, contextOldEndLine - contextOldStartLine + 1)
: Math.max(1, (oldText ? oldText.split('\n').length : 1));
: Math.max(1, oldText ? oldText.split('\n').length : 1);
const newLineCount = newText === '' ? 0 : newText.split('\n').length;
const afterContextOffset = newLineCount - oldLineCount;
beforeLines.forEach((item: any) => {
rows.push({ lineNo: Number(item.line), marker: ' ', text: String(item.text ?? ''), className: '' });
rows.push({
lineNo: Number(item.line),
marker: ' ',
text: String(item.text ?? ''),
className: ''
});
});
rows.push(...buildPairRows(context));
afterLines.forEach((item: any) => {
@ -383,28 +406,41 @@ function renderEditFile(result: any, args: any): string {
const lineNo = Number.isFinite(rawLineNo) ? rawLineNo + afterContextOffset : rawLineNo;
rows.push({ lineNo, marker: ' ', text: String(item.text ?? ''), className: '' });
});
const numberedRows = rows.filter((row) => typeof row.lineNo === 'number' && Number.isFinite(row.lineNo));
const numberedRows = rows.filter(
(row) => typeof row.lineNo === 'number' && Number.isFinite(row.lineNo)
);
hunks.push({
rows,
minLine: numberedRows.length ? Math.min(...numberedRows.map((row) => row.lineNo)) : null,
maxLine: numberedRows.length ? Math.max(...numberedRows.map((row) => row.lineNo)) : null,
minLine: numberedRows.length
? Math.min(...numberedRows.map((row) => row.lineNo))
: null,
maxLine: numberedRows.length ? Math.max(...numberedRows.map((row) => row.lineNo)) : null
});
});
} else {
const rows = buildPairRows();
const numberedRows = rows.filter((row) => typeof row.lineNo === 'number' && Number.isFinite(row.lineNo));
const numberedRows = rows.filter(
(row) => typeof row.lineNo === 'number' && Number.isFinite(row.lineNo)
);
hunks.push({
rows,
minLine: numberedRows.length ? Math.min(...numberedRows.map((row) => row.lineNo)) : null,
maxLine: numberedRows.length ? Math.max(...numberedRows.map((row) => row.lineNo)) : null,
maxLine: numberedRows.length ? Math.max(...numberedRows.map((row) => row.lineNo)) : null
});
}
});
const sortedHunks = hunks.sort((a, b) => (a.minLine ?? Number.MAX_SAFE_INTEGER) - (b.minLine ?? Number.MAX_SAFE_INTEGER));
const sortedHunks = hunks.sort(
(a, b) => (a.minLine ?? Number.MAX_SAFE_INTEGER) - (b.minLine ?? Number.MAX_SAFE_INTEGER)
);
const mergedHunks: any[] = [];
sortedHunks.forEach((hunk) => {
const last = mergedHunks[mergedHunks.length - 1];
if (last && hunk.minLine !== null && last.maxLine !== null && hunk.minLine <= last.maxLine + 1) {
if (
last &&
hunk.minLine !== null &&
last.maxLine !== null &&
hunk.minLine <= last.maxLine + 1
) {
last.rows.push(...hunk.rows);
last.maxLine = Math.max(last.maxLine, hunk.maxLine ?? last.maxLine);
} else {
@ -428,13 +464,19 @@ function renderEditFile(result: any, args: any): string {
// 仅变更行之间优先按 marker 分组(- 在前 + 在后),再按行号
if (aIsChange && bIsChange) {
const markerDelta = (markerOrder[a.marker] ?? 9) - (markerOrder[b.marker] ?? 9);
return markerDelta || (a.lineNo ?? Number.MAX_SAFE_INTEGER) - (b.lineNo ?? Number.MAX_SAFE_INTEGER);
return (
markerDelta ||
(a.lineNo ?? Number.MAX_SAFE_INTEGER) - (b.lineNo ?? Number.MAX_SAFE_INTEGER)
);
}
// 上下文行或跨类型:按行号优先
const lineDelta = (a.lineNo ?? Number.MAX_SAFE_INTEGER) - (b.lineNo ?? Number.MAX_SAFE_INTEGER);
const lineDelta =
(a.lineNo ?? Number.MAX_SAFE_INTEGER) - (b.lineNo ?? Number.MAX_SAFE_INTEGER);
return lineDelta || (markerOrder[a.marker] ?? 9) - (markerOrder[b.marker] ?? 9);
});
filteredRows.forEach((row: any) => renderDiffLine(row.lineNo, row.marker, row.text, row.className));
filteredRows.forEach((row: any) =>
renderDiffLine(row.lineNo, row.marker, row.text, row.className)
);
});
html += '</div>';
}
@ -512,7 +554,10 @@ function renderReadFile(result: any, args: any): string {
matches.forEach((match: any, idx: number) => {
if (idx > 0) html += '<div class="search-match-separator">⋯</div>';
html += '<div class="search-match-item">';
const lineLabel = match.line_start === match.line_end ? `${match.line_start}` : `${match.line_start}-${match.line_end}`;
const lineLabel =
match.line_start === match.line_end
? `${match.line_start}`
: `${match.line_start}-${match.line_end}`;
html += `<div class="search-match-line">行 ${lineLabel}</div>`;
html += `<pre>${escapeHtml(match.snippet || match.content || '')}</pre>`;
html += '</div>';
@ -794,10 +839,12 @@ function renderConversationSearch(result: any, args: any): string {
const results = Array.isArray(result?.results) ? result.results : [];
const keywords = Array.isArray(result?.keywords)
? result.keywords
: (Array.isArray(args?.keywords) ? args.keywords : []);
: Array.isArray(args?.keywords)
? args.keywords
: [];
const keywordText = keywords.length
? keywords.join(' / ')
: (result?.query || args?.query || '(未指定)');
: result?.query || args?.query || '(未指定)';
let html = '<div class="tool-result-meta">';
html += `<div><strong>关键词:</strong>${escapeHtml(keywordText)}</div>`;
html += `<div><strong>日期范围:</strong>${escapeHtml(result?.start_date || args?.start_date || '不限')} ~ ${escapeHtml(result?.end_date || args?.end_date || '不限')}</div>`;
@ -897,10 +944,10 @@ function formatPersonalizationFieldValue(field: string, value: any): string {
return value ? '开启' : '关闭';
}
if (field === 'considerations') {
if (!Array.isArray(value) || value.length === 0) {
if (typeof value !== 'string' || value.trim() === '') {
return '未设置';
}
return value.map((item) => String(item)).join('、');
return value.trim();
}
if (field === 'communication_style') {
const styleMap: Record<string, string> = {
@ -921,12 +968,12 @@ function formatPersonalizationFieldValue(field: string, value: any): string {
return String(value);
}
function renderAskUser(result: any, args: any): string {
const question = args.question || result.question || '';
const context = args.context || result.context || '';
const status = formatToolStatusLabel(result, '✓ 已回答');
const answerText = result.status === 'answered' ? String(result.answer_text || result.message || '').trim() : '';
const answerText =
result.status === 'answered' ? String(result.answer_text || result.message || '').trim() : '';
let html = '<div class="tool-result-meta">';
html += `<div><strong>状态:</strong>${escapeHtml(status)}</div>`;
@ -954,7 +1001,11 @@ function renderAskUser(result: any, args: any): string {
if (answerText) {
html += '<div class="tool-result-content">';
html += '<div class="tool-result-meta">';
answerText.split('\n').map((line) => line.trim()).filter(Boolean).forEach((line) => {
answerText
.split('\n')
.map((line) => line.trim())
.filter(Boolean)
.forEach((line) => {
const separator = line.indexOf('');
if (separator > 0) {
const label = line.slice(0, separator + 1);
@ -1086,7 +1137,8 @@ function renderCreateSubAgent(result: any, args: any): string {
const message = result.message ?? result.summary ?? '';
const stats = result.stats || {};
const runtimeSeconds = result.runtime_seconds ?? result.elapsed_seconds ?? stats.runtime_seconds ?? 0;
const runtimeSeconds =
result.runtime_seconds ?? result.elapsed_seconds ?? stats.runtime_seconds ?? 0;
const apiCalls = stats.api_calls ?? stats.turn_count ?? 0;
const toolCount =
(stats.files_read || 0) +
@ -1183,7 +1235,7 @@ function renderGetSubAgentStatus(result: any, args: any): string {
const found = item.found !== false;
const status = String(item.status || '');
const taskId = item.task_id ?? '';
const summary = item.summary ?? (item.final_result?.message) ?? (item.final_result?.summary) ?? '';
const summary = item.summary ?? item.final_result?.message ?? item.final_result?.summary ?? '';
html += '<div class="sub-agent-status-item">';
html += `<div class="sub-agent-status-header">子智能体 #${escapeHtml(String(agentId))}</div>`;

View File

@ -25,9 +25,7 @@
</button>
<div class="personalization-body settings-redesign-body" v-if="!loading">
<form
class="personal-form settings-redesign-form"
>
<form class="personal-form settings-redesign-form">
<div class="settings-redesign-layout">
<nav class="settings-redesign-tabs" aria-label="个人空间分组切换">
<button
@ -88,9 +86,7 @@
<label class="settings-toggle-row">
<span class="settings-row-copy">
<span class="settings-row-title">自动生成对话标题</span>
<span class="settings-row-desc"
>默认开启关闭后标题将沿用首条消息</span
>
<span class="settings-row-desc">默认开启关闭后标题将沿用首条消息</span>
</span>
<input
type="checkbox"
@ -273,54 +269,19 @@
</div>
</div>
</div>
<div class="settings-list-row tall">
<span class="settings-row-copy">
<div class="settings-textarea-row">
<div class="settings-row-copy">
<span class="settings-row-title">回答时必须考虑的信息</span>
<span class="settings-row-desc"
>最多 {{ maxConsiderations }} 可拖动排序</span
>
</span>
<div class="settings-input-stack wide">
<div class="settings-add-row">
<input
type="text"
:value="newConsideration"
maxlength="50"
placeholder="输入后点击 + 添加"
@input="personalization.updateNewConsideration($event.target.value)"
<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()"
/>
<button
type="button"
:disabled="
!newConsideration || form.considerations.length >= maxConsiderations
"
@click="personalization.addConsideration()"
>
+
</button>
</div>
<ul class="settings-consideration-list" v-if="form.considerations.length">
<li
v-for="(item, idx) in form.considerations"
:key="`consideration-${idx}`"
draggable="true"
@dragstart="personalization.considerationDragStart(idx, $event)"
@dragover.prevent="personalization.considerationDragOver(idx, $event)"
@drop.prevent="personalization.considerationDrop(idx, $event)"
@dragend="personalization.considerationDragEnd()"
>
<span class="drag-handle" aria-hidden="true"></span>
<span>{{ item }}</span>
<button
type="button"
@click="personalization.removeConsideration(idx)"
>
</button>
</li>
</ul>
</div>
></textarea>
</div>
<label
v-if="currentBlockDisplayMode === 'stacked'"
@ -328,9 +289,7 @@
>
<span class="settings-row-copy">
<span class="settings-row-title">隐藏块间边线</span>
<span class="settings-row-desc"
>去掉堆叠块之间的分割线更简洁</span
>
<span class="settings-row-desc">去掉堆叠块之间的分割线更简洁</span>
</span>
<input
type="checkbox"
@ -453,7 +412,9 @@
<div class="settings-action-row">
<span class="settings-row-copy">
<span class="settings-row-title">上传诊断日志</span>
<span class="settings-row-desc">复现问题后点击把当前环境信息上传给开发者排查</span>
<span class="settings-row-desc"
>复现问题后点击把当前环境信息上传给开发者排查</span
>
</span>
<button
type="button"
@ -640,8 +601,9 @@
><input
type="checkbox"
:checked="form.default_hide_workspace"
@change="applyDefaultHideWorkspaceOption($event.target.checked)"
/><span class="fancy-check" aria-hidden="true"
@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"
@ -802,9 +764,7 @@
>
<span class="settings-row-copy">
<span class="settings-row-title">隐藏块间边线</span>
<span class="settings-row-desc"
>去掉堆叠块之间的分割线更简洁</span
>
<span class="settings-row-desc">去掉堆叠块之间的分割线更简洁</span>
</span>
<input
type="checkbox"
@ -1006,8 +966,7 @@
:class="{ selected: form.versioning_backup_mode === 'shallow' }"
@click="selectVersioningBackupMode('shallow')"
>
<strong>浅备份</strong
><span>速度快只回溯被编辑过的文件</span
<strong>浅备份</strong><span>速度快只回溯被编辑过的文件</span
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
<button
@ -1016,8 +975,7 @@
:class="{ selected: form.versioning_backup_mode === 'full' }"
@click="selectVersioningBackupMode('full')"
>
<strong>完全备份</strong
><span>完整工作区快照首次创建可能较慢</span
<strong>完全备份</strong><span>完整工作区快照首次创建可能较慢</span
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
</div>
@ -1379,9 +1337,7 @@
<div class="settings-group-block">
<div class="settings-group-title">
<span class="settings-row-title">强约束系统</span
><span class="settings-row-desc"
>仅对个人空间中已启用的 skill 生效</span
>
><span class="settings-row-desc">仅对个人空间中已启用的 skill 生效</span>
</div>
<label class="settings-toggle-row inner"
><span class="settings-row-title">Terminal 系列工具</span
@ -1460,7 +1416,8 @@
<div class="settings-group-title">
<span class="settings-row-title">可用 Skills</span
><span class="settings-row-desc"
>勾选后会注入 system prompt并同步到工作区的 .astrion/skills/ 目录</span
>勾选后会注入 system prompt并同步到工作区的 .astrion/skills/
目录</span
>
</div>
<div class="settings-check-grid">
@ -1487,9 +1444,7 @@
<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
>
><span class="settings-row-desc">选择后这些类别在新任务中保持关闭</span>
</div>
<div class="settings-check-grid">
<label
@ -1627,15 +1582,19 @@
</div>
</section>
<section
v-else-if="activeTab === 'voice'"
key="voice"
class="settings-page"
<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;
"
>
<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仅在手机本地运行无需网络
端侧语音识别模型SenseVoice int8支持中英混说 + 自动标点 模型约
228MB仅在手机本地运行无需网络
</p>
</div>
<div class="settings-action-row">
@ -1643,25 +1602,35 @@
<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-if="voiceDownloading"
>下载中 {{ voiceDownloadPercent }}% {{ voiceDownloadMsg }}</template
>
<template v-else-if="voiceModelPartial"
>下载不完整请重新下载</template
>
<template v-else>未下载 点击下载</template>
</span>
</span>
<div style="display: flex; gap: 6px;">
<div style="display: flex; gap: 6px">
<button
type="button"
class="settings-secondary-button"
:disabled="voiceDownloading"
@click="downloadVoiceModel"
>
{{ voiceDownloading ? '下载中...' : voiceModelReady ? '重新下载' : '下载模型' }}
{{
voiceDownloading
? '下载中...'
: voiceModelReady
? '重新下载'
: '下载模型'
}}
</button>
<button
v-if="voiceModelReady || voiceModelPartial"
type="button"
class="settings-secondary-button"
style="color: var(--state-error);"
style="color: var(--state-error)"
:disabled="voiceDownloading"
@click="deleteVoiceModel"
>
@ -1669,9 +1638,11 @@
</button>
</div>
</div>
<div class="settings-action-row" style="margin-top: 8px;">
<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 class="settings-row-desc" style="font-size: 12px"
>点击麦克风闪退先复现一次再点上传日志</span
>
</span>
<button
type="button"
@ -1681,11 +1652,20 @@
上传日志
</button>
</div>
<div v-if="voiceDownloading" class="voice-download-bar" style="margin: 8px 0 0;">
<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
class="voice-download-fill"
:style="{ width: voiceDownloadPercent + '%' }"
></div>
</div>
<span style="font-size: 11px; color: var(--text-secondary); margin-top: 4px;">{{ voiceDownloadPercent }}%</span>
<span style="font-size: 11px; color: var(--text-secondary); margin-top: 4px"
>{{ voiceDownloadPercent }}%</span
>
</div>
</section>
@ -1737,9 +1717,7 @@
<div class="settings-action-row">
<span class="settings-row-copy"
><span class="settings-row-title">自定义工具管理</span
><span class="settings-row-desc"
>在线创建/编辑自定义工具文件</span
></span
><span class="settings-row-desc">在线创建/编辑自定义工具文件</span></span
><button
type="button"
class="settings-secondary-button"
@ -1796,8 +1774,6 @@ const {
loading,
form,
tonePresets,
newConsideration,
maxConsiderations,
status,
error,
saving,
@ -1858,7 +1834,7 @@ const baseTabs = [
{ id: 'tools', label: '工具与 Skills', icon: 'wrench' },
{ id: 'files', label: '文件与图片', icon: 'file' },
{ id: 'data', label: '数据管理', icon: 'layers' },
{ id: 'voice', label: '语音模型', icon: 'mic' },
{ id: 'voice', label: '语音模型', icon: 'mic' }
] as const satisfies ReadonlyArray<{ id: PersonalTab; label: string; icon: IconKey }>;
const sessionRole = ref('');
@ -1917,7 +1893,9 @@ const checkVoiceModel = () => {
if (!voiceModelReady.value && typeof bridge.isModelPartial === 'function') {
voiceModelPartial.value = bridge.isModelPartial();
}
} catch (_) { /* ignore */ }
} catch (_) {
/* ignore */
}
}
};
@ -1969,7 +1947,10 @@ const saveDebugLog = async () => {
if (typeof bridge.collectDebugLog === 'function') {
log = bridge.collectDebugLog();
}
if (!log) { alert('无法收集日志'); return; }
if (!log) {
alert('无法收集日志');
return;
}
try {
const res = await fetch('/api/voice_debug', { method: 'POST', body: log });
if (res.ok) {
@ -2157,9 +2138,7 @@ const blockDisplayLabel = computed(() => {
);
});
const currentCompactMessageDisplay = computed(
() => form.value.compact_message_display || 'full'
);
const currentCompactMessageDisplay = computed(() => form.value.compact_message_display || 'full');
const compactMessageDisplayLabel = computed(() => {
return (
@ -3006,7 +2985,6 @@ const applyDefaultHideWorkspaceOption = async (hidden: boolean) => {
-ms-overflow-style: none;
}
.settings-redesign-tabs::-webkit-scrollbar,
.settings-redesign-scroll::-webkit-scrollbar,
.settings-floating-menu::-webkit-scrollbar,
@ -3379,37 +3357,41 @@ const applyDefaultHideWorkspaceOption = async (hidden: boolean) => {
flex: 1 1 auto;
}
.settings-consideration-list {
margin: 0;
padding: 0;
list-style: none;
display: grid;
gap: 6px;
.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-consideration-list li {
min-height: 34px;
.settings-textarea-row textarea {
width: 100%;
min-height: 120px;
max-height: 320px;
border: 1px solid var(--theme-control-border);
border-radius: 12px;
padding: 0 8px;
display: grid;
grid-template-columns: 18px minmax(0, 1fr) 28px;
gap: 8px;
align-items: center;
color: var(--claude-text);
font-size: 13px;
}
.settings-consideration-list button {
border: 0;
background: transparent;
color: var(--claude-text-secondary);
cursor: pointer;
color: var(--claude-text);
padding: 12px;
outline: none;
font-size: 13px;
line-height: 1.55;
resize: vertical;
overflow-y: auto;
font-family: inherit;
}
.drag-handle {
.settings-textarea-row textarea:focus {
border-color: var(--claude-text-secondary);
box-shadow: none;
}
.settings-textarea-row textarea::placeholder {
color: var(--claude-text-secondary);
cursor: grab;
opacity: 0.6;
}
.settings-toggle-row {

View File

@ -37,7 +37,7 @@ interface PersonalForm {
use_custom_names: boolean;
profession: string;
tone: string;
considerations: string[];
considerations: string;
thinking_interval: number | null;
disabled_tool_categories: string[];
default_run_mode: RunMode | null;
@ -78,12 +78,9 @@ interface PersonalizationState {
loaded: boolean;
status: string;
error: string;
maxConsiderations: number;
toggleUpdating: boolean;
overlayPressActive: boolean;
newConsideration: string;
tonePresets: string[];
draggedConsiderationIndex: number | null;
form: PersonalForm;
toolCategories: Array<{ id: string; label: string }>;
skillsCatalog: Array<{ id: string; label: string; description?: string }>;
@ -157,7 +154,7 @@ const defaultForm = (): PersonalForm => ({
use_custom_names: false,
profession: '',
tone: '',
considerations: [],
considerations: '',
thinking_interval: null,
disabled_tool_categories: [],
default_run_mode: null,
@ -244,12 +241,9 @@ export const usePersonalizationStore = defineStore('personalization', {
loaded: false,
status: '',
error: '',
maxConsiderations: 10,
toggleUpdating: false,
overlayPressActive: false,
newConsideration: '',
tonePresets: ['健谈', '幽默', '直言不讳', '鼓励性', '诗意', '企业商务', '打破常规', '同理心'],
draggedConsiderationIndex: null,
form: defaultForm(),
toolCategories: [],
skillsCatalog: [],
@ -268,7 +262,6 @@ export const usePersonalizationStore = defineStore('personalization', {
},
closeDrawer() {
this.visible = false;
this.draggedConsiderationIndex = null;
this.overlayPressActive = false;
},
handleOverlayPressStart(event: Event) {
@ -311,9 +304,7 @@ export const usePersonalizationStore = defineStore('personalization', {
applyPersonalizationData(data: any) {
// 若后端未返回默认模型(旧版本接口),保持当前已选模型而不是回退到内置模型
const fallbackModel =
(this.form && typeof this.form.default_model === 'string'
? this.form.default_model
: null);
this.form && typeof this.form.default_model === 'string' ? this.form.default_model : null;
const fallbackTheme = this.form?.theme || loadCachedTheme();
this.form = {
enabled: !!data.enabled,
@ -327,8 +318,9 @@ export const usePersonalizationStore = defineStore('personalization', {
: 'medium',
auto_generate_title: data.auto_generate_title !== false,
recent_conversations_prompt_enabled: !!data.recent_conversations_prompt_enabled,
recent_conversations_prompt_limit:
this.normalizeRecentConversationsPromptLimit(data.recent_conversations_prompt_limit),
recent_conversations_prompt_limit: this.normalizeRecentConversationsPromptLimit(
data.recent_conversations_prompt_limit
),
tool_intent_enabled: !!data.tool_intent_enabled,
skill_hints_enabled: !!data.skill_hints_enabled,
skill_strict_terminal_enabled: !!data.skill_strict_terminal_enabled,
@ -345,7 +337,9 @@ export const usePersonalizationStore = defineStore('personalization', {
auto_open_terminal_panel: data.auto_open_terminal_panel !== false,
stacked_hide_borders: !!data.stacked_hide_borders,
enhanced_tool_display_categories: Array.isArray(data.enhanced_tool_display_categories)
? data.enhanced_tool_display_categories.filter((item: unknown) => typeof item === 'string')
? data.enhanced_tool_display_categories.filter(
(item: unknown) => typeof item === 'string'
)
: [],
enabled_skills: Array.isArray(data.enabled_skills)
? data.enabled_skills.filter((item: unknown) => typeof item === 'string')
@ -355,7 +349,12 @@ export const usePersonalizationStore = defineStore('personalization', {
use_custom_names: !!data.use_custom_names,
profession: data.profession || '',
tone: data.tone || '',
considerations: Array.isArray(data.considerations) ? [...data.considerations] : [],
considerations:
typeof data.considerations === 'string'
? data.considerations
: Array.isArray(data.considerations)
? data.considerations.filter((item: unknown) => typeof item === 'string').join('\n')
: '',
thinking_interval:
typeof data.thinking_interval === 'number' ? data.thinking_interval : null,
disabled_tool_categories: Array.isArray(data.disabled_tool_categories)
@ -374,8 +373,7 @@ export const usePersonalizationStore = defineStore('personalization', {
? data.default_permission_mode
: 'unrestricted',
versioning_enabled_by_default: data.versioning_enabled_by_default !== false,
versioning_backup_mode:
data.versioning_backup_mode === 'full' ? 'full' : 'shallow',
versioning_backup_mode: data.versioning_backup_mode === 'full' ? 'full' : 'shallow',
versioning_restore_mode: 'overwrite',
default_model: typeof data.default_model === 'string' ? data.default_model : fallbackModel,
image_compression:
@ -407,9 +405,7 @@ export const usePersonalizationStore = defineStore('personalization', {
theme: ['classic', 'light', 'dark'].includes(data.theme) ? data.theme : fallbackTheme,
goal_review_mode: data.goal_review_mode === 'active' ? 'active' : 'readonly',
goal_end_conditions: Array.isArray(data.goal_end_conditions)
? data.goal_end_conditions.filter(
(x: any) => x === 'max_turns' || x === 'max_tokens'
)
? data.goal_end_conditions.filter((x: any) => x === 'max_turns' || x === 'max_tokens')
: ['max_turns'],
goal_max_turns:
typeof data.goal_max_turns === 'number' && data.goal_max_turns > 0
@ -421,7 +417,8 @@ export const usePersonalizationStore = defineStore('personalization', {
: null
};
// 如果theme发生变化应用到界面
const currentTheme = (typeof window !== 'undefined' && window.localStorage)
const currentTheme =
typeof window !== 'undefined' && window.localStorage
? window.localStorage.getItem(THEME_STORAGE_KEY)
: null;
if (this.form.theme !== currentTheme) {
@ -803,69 +800,14 @@ export const usePersonalizationStore = defineStore('personalization', {
this.clearFeedback();
this.scheduleAutoSave();
},
updateNewConsideration(value: string) {
this.newConsideration = value;
this.clearFeedback();
},
addConsideration() {
if (!this.newConsideration) {
return;
}
if (this.form.considerations.length >= this.maxConsiderations) {
return;
}
updateConsiderations(value: string) {
this.form = {
...this.form,
considerations: [...this.form.considerations, this.newConsideration]
};
this.newConsideration = '';
this.clearFeedback();
this.scheduleAutoSave();
},
removeConsideration(index: number) {
const items = [...this.form.considerations];
items.splice(index, 1);
this.form = {
...this.form,
considerations: items
considerations: value
};
this.clearFeedback();
this.scheduleAutoSave();
},
considerationDragStart(index: number, event: DragEvent) {
this.draggedConsiderationIndex = index;
if (event && event.dataTransfer) {
event.dataTransfer.effectAllowed = 'move';
}
},
considerationDragOver(index: number, event: DragEvent) {
if (event) {
event.preventDefault();
}
if (this.draggedConsiderationIndex === null || this.draggedConsiderationIndex === index) {
return;
}
const items = [...this.form.considerations];
const [moved] = items.splice(this.draggedConsiderationIndex, 1);
items.splice(index, 0, moved);
this.form = {
...this.form,
considerations: items
};
this.draggedConsiderationIndex = index;
this.clearFeedback();
},
considerationDrop(index: number, event: DragEvent) {
if (event) {
event.preventDefault();
}
this.considerationDragEnd();
this.considerationDragOver(index, event);
this.scheduleAutoSave();
},
considerationDragEnd() {
this.draggedConsiderationIndex = null;
},
async logout() {
try {
console.info('[auth-debug] logout clicked, sending POST /logout');