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

207 lines
6.8 KiB
TypeScript

// @ts-nocheck
import { debugLog } from '../common';
import { useTaskStore } from '../../../stores/task';
import {
extractSkillRefsFromMessage,
SKILL_MARKDOWN_LINK_RE,
} from './shared';
export const chatMethods = {
async clearChat() {
const confirmed = await this.confirmAction({
title: '清除对话',
message: '确定要清除所有对话记录吗?该操作不可撤销。',
confirmText: '清除',
cancelText: '取消'
});
if (confirmed) {
await this.executeSystemCommand('/clear', { showToast: false });
}
},
async compressConversation() {
if (!this.currentConversationId) {
this.uiPushToast({
title: '无法压缩',
message: '当前没有可压缩的对话。',
type: 'info'
});
return;
}
if (this.compressing) {
return;
}
const confirmed = await this.confirmAction({
title: '压缩对话',
message: '确定要压缩当前对话记录吗?较早的消息会被折叠并生成压缩摘要,当前对话 ID 保持不变。',
confirmText: '压缩',
cancelText: '取消'
});
if (!confirmed) {
return;
}
this.compressing = true;
this.compressionInProgress = true;
this.compressionMode = 'manual';
this.compressionStage = 'requesting';
this.compressionError = '';
if (this.compressionToastId) {
this.uiDismissToast(this.compressionToastId);
this.compressionToastId = null;
}
this.compressionToastId = this.uiPushToast({
title: '压缩中',
message: '对话正在压缩,请稍候…',
type: 'info',
duration: null,
closable: false
});
try {
const response = await fetch(`/api/conversations/${this.currentConversationId}/compress`, {
method: 'POST'
});
const result = await response.json();
if (response.ok && result.success) {
this.compressionStage = 'switching';
const newId = result.compressed_conversation_id;
const isInPlace = newId && newId === this.currentConversationId;
// in-place 压缩:对话 id 不变,不重新加载(避免 resetAllStates
// 重置滚动状态 + fetchAndDisplayHistory 清空重渲染导致闪烁和锁死)。
if (newId && !isInPlace) {
await this.loadConversation(newId, { force: true });
}
const guideMessage = (result.guide_message || '').trim();
// 手动压缩只有一种行为:后端已把引导语作为 user 消息追加进历史,
// 这里刷新展示即可,不触发新一轮请求(等待用户继续发送消息才工作)。
if (newId && !isInPlace) {
await this.loadConversation(newId, { force: true });
} else if (isInPlace) {
await this.loadConversation(this.currentConversationId, { force: true });
}
void guideMessage;
if (!isInPlace) {
await this.loadConversationsList();
}
debugLog('对话压缩完成:', result);
this.uiPushToast({
title: '压缩完成',
message: '已压缩较早的对话内容',
type: 'success',
duration: 2200
});
} else {
const message = result.message || result.error || '压缩失败';
this.compressionError = message;
this.uiPushToast({
title: '压缩失败',
message,
type: 'error'
});
}
} catch (error) {
console.error('压缩对话异常:', error);
this.compressionError = error.message || '请稍后重试';
this.uiPushToast({
title: '压缩对话异常',
message: error.message || '请稍后重试',
type: 'error'
});
} finally {
if (this.compressionToastId) {
this.uiDismissToast(this.compressionToastId);
this.compressionToastId = null;
}
this.compressing = false;
this.compressionInProgress = false;
this.compressionMode = '';
this.compressionStage = '';
}
},
async sendAutoUserMessage(text) {
const message = (text || '').trim();
if (!message || !this.isConnected) {
return false;
}
const quotaType = this.thinkingMode ? 'thinking' : 'fast';
if (this.isQuotaExceeded(quotaType)) {
this.showQuotaToast({ type: quotaType });
return false;
}
this.taskInProgress = true;
this.chatAddUserMessage(message, [], [], [], 'user');
this.chatStartAssistantMessage();
this.stopRequested = false;
if (typeof this.monitorShowPendingReply === 'function') {
this.monitorShowPendingReply();
}
try {
const { useTaskStore } = await import('../../../stores/task');
const taskStore = useTaskStore();
if (typeof this.clearProcessedEvents === 'function') {
this.clearProcessedEvents();
}
await taskStore.createTask(message, [], [], this.currentConversationId, {
eventHandler: (event: any) => this.handleTaskEvent(event)
});
} catch (error) {
console.error('[Message] 自动消息创建任务失败:', error);
this.uiPushToast({
title: '发送失败',
message: error?.message || '创建任务失败,请重试',
type: 'error'
});
this.streamingMessage = false;
this.taskInProgress = false;
if (typeof this.cleanupTrailingEmptyAssistantPlaceholder === 'function') {
this.cleanupTrailingEmptyAssistantPlaceholder('auto_create_task_failed');
}
if (typeof this.forceUnlockMonitor === 'function') {
this.forceUnlockMonitor('auto_create_task_failed');
}
return false;
}
if (this.autoScrollEnabled) {
this.scrollToBottom();
}
this.autoResizeInput();
setTimeout(() => {
if (this.currentConversationId) {
this.updateCurrentContextTokens();
}
}, 1000);
return true;
},
autoResizeInput() {
this.$nextTick(() => {
const textarea = this.getComposerElement('stadiumInput');
if (!textarea || !(textarea instanceof HTMLTextAreaElement)) {
return;
}
const previousHeight = textarea.offsetHeight;
textarea.style.height = 'auto';
const computedStyle = window.getComputedStyle(textarea);
const lineHeight = parseFloat(computedStyle.lineHeight || '20') || 20;
const maxHeight = lineHeight * 6;
const targetHeight = Math.min(textarea.scrollHeight, maxHeight);
this.inputSetLineCount(Math.max(1, Math.round(targetHeight / lineHeight)));
this.inputSetMultiline(targetHeight > lineHeight * 1.4);
if (Math.abs(targetHeight - previousHeight) <= 0.5) {
textarea.style.height = `${targetHeight}px`;
return;
}
textarea.style.height = `${previousHeight}px`;
void textarea.offsetHeight;
requestAnimationFrame(() => {
textarea.style.height = `${targetHeight}px`;
});
});
}
};