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", "type": "function",
"function": { "function": {
"name": "manage_personalization", "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": { "parameters": {
"type": "object", "type": "object",
"properties": self._inject_intent({ "properties": self._inject_intent({
@ -267,7 +267,7 @@ class ToolsDefinitionContextToolsMixin:
"description": "要更新的字段名仅action=update时需要" "description": "要更新的字段名仅action=update时需要"
}, },
"value": { "value": {
"description": "新值仅action=update时需要。注意事项提供字符串数组,其他字段提供字符串" "description": "新值仅action=update时需要。注意事项提供字符串,其他字段提供字符串"
} }
}), }),
"required": ["action"] "required": ["action"]

View File

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

View File

@ -5,7 +5,7 @@ from __future__ import annotations
import json import json
from copy import deepcopy from copy import deepcopy
from pathlib import Path from pathlib import Path
from typing import Any, Dict, Iterable, Optional, Union from typing import Any, Dict, Optional, Union
try: try:
from config.limits import THINKING_FAST_INTERVAL from config.limits import THINKING_FAST_INTERVAL
@ -30,7 +30,7 @@ GOAL_MAX_TOKENS_MAX = 100_000_000
PERSONALIZATION_FILENAME = "personalization.json" PERSONALIZATION_FILENAME = "personalization.json"
MAX_SHORT_FIELD_LENGTH = 20 MAX_SHORT_FIELD_LENGTH = 20
MAX_CONSIDERATION_LENGTH = 50 MAX_CONSIDERATION_TEXT_LENGTH = 2000
MAX_CONSIDERATION_ITEMS = 10 MAX_CONSIDERATION_ITEMS = 10
TONE_PRESETS = ["健谈", "幽默", "直言不讳", "鼓励性", "诗意", "企业商务", "打破常规", "同理心"] TONE_PRESETS = ["健谈", "幽默", "直言不讳", "鼓励性", "诗意", "企业商务", "打破常规", "同理心"]
THINKING_INTERVAL_MIN = 1 THINKING_INTERVAL_MIN = 1
@ -705,12 +705,10 @@ def build_personalization_prompt(
if config.get("tone"): if config.get("tone"):
lines.append(f"用户希望你使用 {config['tone']} 的语气与TA交流") lines.append(f"用户希望你使用 {config['tone']} 的语气与TA交流")
considerations: Iterable[str] = config.get("considerations") or [] considerations: str = config.get("considerations") or ""
considerations = [item for item in considerations if item]
if considerations: if considerations:
lines.append("用户希望你在回答问题时必须考虑的信息是:") lines.append("用户希望你在回答问题时必须考虑的信息是:")
for idx, item in enumerate(considerations, 1): lines.append(considerations)
lines.append(f"{idx}. {item}")
conversation_continuity = str(config.get("conversation_continuity") or "medium").strip().lower() conversation_continuity = str(config.get("conversation_continuity") or "medium").strip().lower()
if conversation_continuity == "high": if conversation_continuity == "high":
@ -761,20 +759,25 @@ def _sanitize_short_field(value: Optional[str]) -> str:
return text[:MAX_SHORT_FIELD_LENGTH] return text[:MAX_SHORT_FIELD_LENGTH]
def _sanitize_considerations(value: Any) -> list: def _sanitize_considerations(value: Any) -> str:
if not isinstance(value, list): """Sanitize considerations text. Legacy array data is joined with newlines."""
return [] if value is None:
cleaned = [] return ""
for item in value: if isinstance(value, str):
if not isinstance(item, str): return value[:MAX_CONSIDERATION_TEXT_LENGTH]
continue if isinstance(value, list):
text = item.strip() cleaned = []
if not text: for item in value:
continue if not isinstance(item, str):
cleaned.append(text[:MAX_CONSIDERATION_LENGTH]) continue
if len(cleaned) >= MAX_CONSIDERATION_ITEMS: text = item.strip()
break if not text:
return cleaned continue
cleaned.append(text)
if len(cleaned) >= MAX_CONSIDERATION_ITEMS:
break
return "\n".join(cleaned)[:MAX_CONSIDERATION_TEXT_LENGTH]
return ""
def _sanitize_thinking_interval(value: Any) -> Optional[int]: 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) { if (details.length > 0) {
html += '<div class="tool-result-diff scrollable">'; html += '<div class="tool-result-diff scrollable">';
const renderDiffLine = (lineNo: number | null, marker: string, text: string, className = '') => { const renderDiffLine = (
const lineNoText = typeof lineNo === 'number' && Number.isFinite(lineNo) ? String(lineNo) : ''; 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>`; 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[] = []; const hunks: any[] = [];
@ -391,14 +397,24 @@ function renderEditFile(result: any, args: any): string {
if (oldText) { if (oldText) {
const oldLines = oldText.split('\n'); const oldLines = oldText.split('\n');
oldLines.forEach((line: string, lineIdx: number) => { 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) { if (newText) {
const newLines = newText.split('\n'); const newLines = newText.split('\n');
newLines.forEach((line: string, lineIdx: number) => { 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; return rows;
@ -410,30 +426,37 @@ function renderEditFile(result: any, args: any): string {
const rawBeforeLines = Array.isArray(context.before_lines) ? context.before_lines : []; const rawBeforeLines = Array.isArray(context.before_lines) ? context.before_lines : [];
const rawAfterLines = Array.isArray(context.after_lines) ? context.after_lines : []; const rawAfterLines = Array.isArray(context.after_lines) ? context.after_lines : [];
const beforeLines = 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.slice(1) : rawBeforeLines;
: rawBeforeLines;
const afterLines = 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 &&
rawAfterLines.length >= 2 && String(rawAfterLines[rawAfterLines.length - 1]?.text ?? '') === '' String(rawAfterLines[rawAfterLines.length - 1]?.text ?? '') === ''
? rawAfterLines.slice(0, -1) ? rawAfterLines.slice(0, -1)
: rawAfterLines; : rawAfterLines;
const contextOldStartLine = Number(context?.old_start_line ?? detail.matched_lines?.[0]); const contextOldStartLine = Number(context?.old_start_line ?? detail.matched_lines?.[0]);
const contextOldEndLine = Number(context?.old_end_line ?? contextOldStartLine); const contextOldEndLine = Number(context?.old_end_line ?? contextOldStartLine);
const oldLineCount = Number.isFinite(contextOldStartLine) && Number.isFinite(contextOldEndLine) const oldLineCount =
? Math.max(1, contextOldEndLine - contextOldStartLine + 1) Number.isFinite(contextOldStartLine) && Number.isFinite(contextOldEndLine)
: Math.max(1, (oldText ? oldText.split('\n').length : 1)); ? Math.max(1, contextOldEndLine - contextOldStartLine + 1)
: Math.max(1, oldText ? oldText.split('\n').length : 1);
const newLineCount = newText === '' ? 0 : newText.split('\n').length; const newLineCount = newText === '' ? 0 : newText.split('\n').length;
const afterContextOffset = newLineCount - oldLineCount; const afterContextOffset = newLineCount - oldLineCount;
beforeLines.forEach((item: any) => { 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)); rows.push(...buildPairRows(context));
afterLines.forEach((item: any) => { afterLines.forEach((item: any) => {
@ -441,28 +464,41 @@ function renderEditFile(result: any, args: any): string {
const lineNo = Number.isFinite(rawLineNo) ? rawLineNo + afterContextOffset : rawLineNo; const lineNo = Number.isFinite(rawLineNo) ? rawLineNo + afterContextOffset : rawLineNo;
rows.push({ lineNo, marker: ' ', text: String(item.text ?? ''), className: '' }); 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({ hunks.push({
rows, rows,
minLine: numberedRows.length ? Math.min(...numberedRows.map((row) => row.lineNo)) : null, minLine: numberedRows.length
maxLine: numberedRows.length ? Math.max(...numberedRows.map((row) => row.lineNo)) : null, ? Math.min(...numberedRows.map((row) => row.lineNo))
: null,
maxLine: numberedRows.length ? Math.max(...numberedRows.map((row) => row.lineNo)) : null
}); });
}); });
} else { } else {
const rows = buildPairRows(); 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({ hunks.push({
rows, rows,
minLine: numberedRows.length ? Math.min(...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, 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[] = []; const mergedHunks: any[] = [];
sortedHunks.forEach((hunk) => { sortedHunks.forEach((hunk) => {
const last = mergedHunks[mergedHunks.length - 1]; 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.rows.push(...hunk.rows);
last.maxLine = Math.max(last.maxLine, hunk.maxLine ?? last.maxLine); last.maxLine = Math.max(last.maxLine, hunk.maxLine ?? last.maxLine);
} else { } else {
@ -481,7 +517,8 @@ function renderEditFile(result: any, args: any): string {
return true; return true;
}) })
.sort((a: any, b: any) => { .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); return lineDelta || (markerOrder[a.marker] ?? 9) - (markerOrder[b.marker] ?? 9);
}) })
.forEach((row: any) => renderDiffLine(row.lineNo, row.marker, row.text, row.className)); .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) => { matches.forEach((match: any, idx: number) => {
if (idx > 0) html += '<div class="search-match-separator">⋯</div>'; if (idx > 0) html += '<div class="search-match-separator">⋯</div>';
html += '<div class="search-match-item">'; 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 += `<div class="search-match-line">行 ${lineLabel}</div>`;
html += `<pre>${escapeHtml(match.snippet || match.content || '')}</pre>`; html += `<pre>${escapeHtml(match.snippet || match.content || '')}</pre>`;
html += '</div>'; html += '</div>';
@ -813,10 +853,12 @@ function renderConversationSearch(result: any, args: any): string {
const results = Array.isArray(result?.results) ? result.results : []; const results = Array.isArray(result?.results) ? result.results : [];
const keywords = Array.isArray(result?.keywords) const keywords = Array.isArray(result?.keywords)
? result.keywords ? result.keywords
: (Array.isArray(args?.keywords) ? args.keywords : []); : Array.isArray(args?.keywords)
? args.keywords
: [];
const keywordText = keywords.length const keywordText = keywords.length
? keywords.join(' / ') ? keywords.join(' / ')
: (result?.query || args?.query || '(未指定)'); : result?.query || args?.query || '(未指定)';
let html = '<div class="tool-result-meta">'; let html = '<div class="tool-result-meta">';
html += `<div><strong>关键词:</strong>${escapeHtml(keywordText)}</div>`; 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>`; 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); return independenceMap[String(value)] || String(value);
} }
if (field === 'considerations') { if (field === 'considerations') {
if (!Array.isArray(value) || value.length === 0) { if (typeof value !== 'string' || value.trim() === '') {
return '未设置'; return '未设置';
} }
return value.map((item) => String(item)).join('、'); return value.trim();
} }
return String(value); return String(value);
} }
function renderAskUser(result: any, args: any): string { function renderAskUser(result: any, args: any): string {
const question = args.question || result.question || ''; const question = args.question || result.question || '';
const context = args.context || result.context || ''; const context = args.context || result.context || '';
const status = formatToolStatusLabel(result, '✓ 已回答'); 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">'; let html = '<div class="tool-result-meta">';
html += `<div><strong>状态:</strong>${escapeHtml(status)}</div>`; html += `<div><strong>状态:</strong>${escapeHtml(status)}</div>`;
@ -983,16 +1025,20 @@ function renderAskUser(result: any, args: any): string {
if (answerText) { if (answerText) {
html += '<div class="tool-result-content">'; html += '<div class="tool-result-content">';
html += '<div class="tool-result-meta">'; html += '<div class="tool-result-meta">';
answerText.split('\n').map((line) => line.trim()).filter(Boolean).forEach((line) => { answerText
const separator = line.indexOf(''); .split('\n')
if (separator > 0) { .map((line) => line.trim())
const label = line.slice(0, separator + 1); .filter(Boolean)
const value = line.slice(separator + 1); .forEach((line) => {
html += `<div><strong>${escapeHtml(label)}</strong>${escapeHtml(value)}</div>`; const separator = line.indexOf('');
} else { if (separator > 0) {
html += `<div><strong>用户回答:</strong>${escapeHtml(line)}</div>`; const label = line.slice(0, separator + 1);
} const value = line.slice(separator + 1);
}); html += `<div><strong>${escapeHtml(label)}</strong>${escapeHtml(value)}</div>`;
} else {
html += `<div><strong>用户回答:</strong>${escapeHtml(line)}</div>`;
}
});
html += '</div></div>'; html += '</div></div>';
} }
@ -1001,10 +1047,7 @@ function renderAskUser(result: any, args: any): string {
function renderManagePersonalization(result: any, args: any): string { function renderManagePersonalization(result: any, args: any): string {
const action = String(args.action || result.action || 'read'); const action = String(args.action || result.action || 'read');
const status = formatToolStatusLabel( const status = formatToolStatusLabel(result, action === 'update' ? '✓ 已更新' : '✓ 已读取');
result,
action === 'update' ? '✓ 已更新' : '✓ 已读取'
);
let html = '<div class="tool-result-meta">'; let html = '<div class="tool-result-meta">';
html += `<div><strong>操作:</strong>${escapeHtml(action === 'update' ? '更新配置' : '读取配置')}</div>`; 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 message = result.message ?? result.summary ?? '';
const stats = result.stats || {}; 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 apiCalls = stats.api_calls ?? stats.turn_count ?? 0;
const toolCount = const toolCount =
(stats.files_read || 0) + (stats.files_read || 0) +
@ -1215,7 +1259,7 @@ function renderGetSubAgentStatus(result: any, args: any): string {
const found = item.found !== false; const found = item.found !== false;
const status = String(item.status || ''); const status = String(item.status || '');
const taskId = item.task_id ?? ''; 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-item">';
html += `<div class="sub-agent-status-header">子智能体 #${escapeHtml(String(agentId))}</div>`; 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, .code-block pre,
.output-block pre { .output-block pre {
scrollbar-width: thin; /* Firefox */ scrollbar-width: thin; /* Firefox */
scrollbar-color: color-mix(in srgb, var(--claude-text-secondary) 55%, transparent) scrollbar-color: color-mix(in srgb, var(--claude-text-secondary) 55%, transparent) transparent;
transparent;
} }
.tool-result-content.scrollable::-webkit-scrollbar, .tool-result-content.scrollable::-webkit-scrollbar,
@ -1571,7 +1614,11 @@ function renderGetSubAgentStatus(result: any, args: any): string {
/* 彩蛋 */ /* 彩蛋 */
.easter-egg-content { .easter-egg-content {
padding: 12px; 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: 1px solid color-mix(in srgb, var(--decorative-gold) 30%, transparent);
border-radius: 8px; border-radius: 8px;
font-size: 14px; font-size: 14px;

View File

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

View File

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

View File

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