feat(personalization): 将回答时必须考虑的信息从条列式改为大段文字输入
This commit is contained in:
parent
b2cbe0ab5d
commit
19746e67d3
@ -252,7 +252,7 @@ class ToolsDefinitionContextToolsMixin:
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "manage_personalization",
|
||||
"description": "管理用户个性化设置。支持读取所有配置,或更新单个字段。可修改字段:self_identify(AI自称,最多20字)、user_name(AI如何称呼用户,最多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_identify(AI自称,最多20字)、user_name(AI如何称呼用户,最多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"]
|
||||
|
||||
@ -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":
|
||||
# 主题验证
|
||||
|
||||
@ -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,20 +759,25 @@ 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 []
|
||||
cleaned = []
|
||||
for item in value:
|
||||
if not isinstance(item, str):
|
||||
continue
|
||||
text = item.strip()
|
||||
if not text:
|
||||
continue
|
||||
cleaned.append(text[:MAX_CONSIDERATION_LENGTH])
|
||||
if len(cleaned) >= MAX_CONSIDERATION_ITEMS:
|
||||
break
|
||||
return cleaned
|
||||
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):
|
||||
continue
|
||||
text = item.strip()
|
||||
if not text:
|
||||
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]:
|
||||
|
||||
@ -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.slice(1)
|
||||
: rawBeforeLines;
|
||||
: 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.slice(0, -1)
|
||||
: rawAfterLines;
|
||||
: 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)
|
||||
? Math.max(1, contextOldEndLine - contextOldStartLine + 1)
|
||||
: Math.max(1, (oldText ? oldText.split('\n').length : 1));
|
||||
const oldLineCount =
|
||||
Number.isFinite(contextOldStartLine) && Number.isFinite(contextOldEndLine)
|
||||
? Math.max(1, contextOldEndLine - contextOldStartLine + 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,16 +1025,20 @@ 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) => {
|
||||
const separator = line.indexOf(':');
|
||||
if (separator > 0) {
|
||||
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>`;
|
||||
}
|
||||
});
|
||||
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);
|
||||
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>';
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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.slice(1)
|
||||
: rawBeforeLines;
|
||||
: 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.slice(0, -1)
|
||||
: rawAfterLines;
|
||||
: 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)
|
||||
? Math.max(1, contextOldEndLine - contextOldStartLine + 1)
|
||||
: Math.max(1, (oldText ? oldText.split('\n').length : 1));
|
||||
const oldLineCount =
|
||||
Number.isFinite(contextOldStartLine) && Number.isFinite(contextOldEndLine)
|
||||
? Math.max(1, contextOldEndLine - contextOldStartLine + 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,16 +1001,20 @@ 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) => {
|
||||
const separator = line.indexOf(':');
|
||||
if (separator > 0) {
|
||||
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>`;
|
||||
}
|
||||
});
|
||||
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);
|
||||
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>';
|
||||
}
|
||||
|
||||
@ -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>`;
|
||||
|
||||
@ -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)"
|
||||
@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>
|
||||
<span class="settings-row-desc">每次回答前都会参考这些背景信息</span>
|
||||
</div>
|
||||
<textarea
|
||||
:value="form.considerations"
|
||||
rows="6"
|
||||
maxlength="2000"
|
||||
placeholder="例如:用户家有一只泰迪犬,棕色,公狗,2014年开始养的..."
|
||||
@input="personalization.updateConsiderations($event.target.value)"
|
||||
@focus="personalization.clearFeedback()"
|
||||
></textarea>
|
||||
</div>
|
||||
<label
|
||||
v-if="currentBlockDisplayMode === 'stacked'"
|
||||
@ -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"
|
||||
>
|
||||
<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,仅在手机本地运行,无需网络。
|
||||
<section v-else-if="activeTab === 'voice'" key="voice" class="settings-page">
|
||||
<div class="settings-section" style="margin-bottom: 16px">
|
||||
<p
|
||||
class="settings-section-desc"
|
||||
style="
|
||||
margin: 0;
|
||||
color: var(--text-secondary);
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
"
|
||||
>
|
||||
端侧语音识别模型(SenseVoice int8),支持中英混说 + 自动标点。 模型约
|
||||
228MB,仅在手机本地运行,无需网络。
|
||||
</p>
|
||||
</div>
|
||||
<div class="settings-action-row">
|
||||
@ -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 {
|
||||
|
||||
@ -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,9 +417,10 @@ export const usePersonalizationStore = defineStore('personalization', {
|
||||
: null
|
||||
};
|
||||
// 如果theme发生变化,应用到界面
|
||||
const currentTheme = (typeof window !== 'undefined' && window.localStorage)
|
||||
? window.localStorage.getItem(THEME_STORAGE_KEY)
|
||||
: null;
|
||||
const currentTheme =
|
||||
typeof window !== 'undefined' && window.localStorage
|
||||
? window.localStorage.getItem(THEME_STORAGE_KEY)
|
||||
: null;
|
||||
if (this.form.theme !== currentTheme) {
|
||||
this.applyTheme(this.form.theme);
|
||||
}
|
||||
@ -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');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user