agent-Specialization/static/src/app/methods/ui/dialog.ts
JOJO ec294e2545 feat(work-mode): 新增运行模式系统 plan/ask/execute 三档
- 输入栏新增运行模式切换器,仅对话空闲可切换,新对话继承当前选择
- plan 档锁定只读权限与沙箱执行环境(UI 禁用+后端强制),网络权限保持可调
- 计划写入 .astrion/plan/,submit_plan 提交后前端弹窗审批,批准自动切换 execute
- ask 档直接输出讨论并禁用 ask_user;execute 档自行补全细节直接开工
- 个性化新增默认运行模式(默认 plan),主提示词加运行模式让位注解
- 修复后台命令绕过只读权限的漏洞(宿主机按 sandbox_write_access 选只读沙箱计划)
- 计划审批与询问弹窗改用 100dvh 动态视口高度,适配移动端 Safari
2026-08-13 12:52:19 +08:00

318 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// @ts-nocheck
import { debugLog } from '../common';
import { usePolicyStore } from '../../../stores/policy';
import { useModelStore } from '../../../stores/model';
import { usePersonalizationStore } from '../../../stores/personalization';
import { useTutorialStore } from '../../../stores/tutorial';
import { renderMarkdown as renderMarkdownHelper } from '../../../composables/useMarkdownRenderer';
import { scrollToBottom as scrollToBottomHelper, conditionalScrollToBottom as conditionalScrollToBottomHelper, scrollThinkingToBottom as scrollThinkingToBottomHelper } from '../../../composables/useScrollControl';
import { startResize as startPanelResize, handleResize as handlePanelResize, stopResize as stopPanelResize } from '../../../composables/usePanelResize';
import {
SUB_AGENT_DONE_PREFIX_RE,
BG_RUN_COMMAND_DONE_PREFIX_RE,
userMDebug,
UI_BOUNCE_TRACE_MAX,
uiBounceTraceLastTsByKey,
isUiBounceTraceEnabled,
uiBounceTrace,
isConnectionDiagEnabled,
pushConnectionDiagRecord,
connectionDiag,
parseSubAgentDoneLabel,
parseBackgroundRunCommandDoneLabel,
parseSystemNoticeLabel,
} from './shared';
export const dialogMethods = {
openPersonalPage() {
if (this.isPolicyBlocked('block_personal_space', '个人空间已被管理员禁用')) {
return;
}
this.personalizationOpenDrawer();
},
minimizeUserQuestionDialog() {
if (!Array.isArray(this.pendingUserQuestions) || !this.pendingUserQuestions.length) {
return;
}
this.userQuestionDialogVisible = false;
this.userQuestionMinimized = true;
},
restoreUserQuestionDialog() {
if (!Array.isArray(this.pendingUserQuestions) || !this.pendingUserQuestions.length) {
return;
}
this.userQuestionDialogVisible = true;
this.userQuestionMinimized = false;
},
async fetchPendingUserQuestions() {
if (!this.currentConversationId) {
this.pendingUserQuestions = [];
return;
}
try {
const response = await fetch(
`/api/user-questions/pending?conversation_id=${encodeURIComponent(this.currentConversationId)}`
);
const payload = await response.json().catch(() => ({}));
if (!response.ok || !payload?.success) {
return;
}
const items = Array.isArray(payload.items) ? payload.items : [];
this.pendingUserQuestions = items;
if (items.length > 0) {
this.userQuestionActiveIndex = Math.min(
Math.max(0, Number(this.userQuestionActiveIndex || 0)),
Math.max(0, items.length - 1)
);
} else {
this.userQuestionDialogVisible = false;
this.userQuestionMinimized = false;
this.userQuestionActiveIndex = 0;
}
} catch (_error) {
// ignore
}
},
approveToolApproval(approvalId) {
return this.decideToolApproval(approvalId, 'approved');
},
async fetchPendingPlanApprovals() {
if (!this.currentConversationId) {
this.pendingPlanApprovals = [];
return;
}
try {
const response = await fetch(
`/api/plan-approvals/pending?conversation_id=${encodeURIComponent(this.currentConversationId)}`
);
const payload = await response.json().catch(() => ({}));
if (!response.ok || !payload?.success) {
return;
}
this.pendingPlanApprovals = Array.isArray(payload.items) ? payload.items : [];
} catch (_error) {
// ignore
}
},
async submitPlanApproval(payload) {
const approvalId = String(payload?.approval_id || '').trim();
if (!approvalId) {
return;
}
this.answeringPlanApprovalIds = [approvalId];
try {
const response = await fetch(
`/api/plan-approvals/${encodeURIComponent(approvalId)}/answer`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
approved: payload?.approved === true,
comment: String(payload?.comment || '').trim()
})
}
);
const result = await response.json().catch(() => ({}));
if (!response.ok || !result?.success) {
throw new Error(result?.message || result?.error || '提交计划决策失败');
}
this.pendingPlanApprovals = (this.pendingPlanApprovals || []).filter(
(item) => item && String(item.approval_id || '') !== approvalId
);
// 批准后后端已自动切换到 execute 并恢复权限与执行环境:立即刷新模式显示
if (payload?.approved === true) {
this.fetchWorkMode();
this.fetchPermissionMode();
this.fetchExecutionMode();
this.uiPushToast({
title: '计划已批准',
message: '已切换到执行模式,开始实施',
type: 'success',
duration: 2200
});
} else {
this.uiPushToast({
title: '已拒绝计划',
message: 'AI 将根据你的意见修订后重新提交',
type: 'info',
duration: 2200
});
}
} catch (error) {
const msg = error instanceof Error ? error.message : String(error || '提交计划决策失败');
this.uiPushToast({ title: '提交计划决策失败', message: msg, type: 'error' });
} finally {
this.answeringPlanApprovalIds = [];
}
},
rejectToolApproval(approvalId) {
return this.decideToolApproval(approvalId, 'rejected');
},
openReviewDialog() {
if (this.isPolicyBlocked('block_conversation_review', '对话引用已被管理员禁用')) {
return;
}
if (!this.isConnected) {
this.uiPushToast({
title: '无法使用',
message: '当前未连接,无法生成回顾文件',
type: 'warning'
});
return;
}
this.reviewDialogOpen = true;
this.reviewSelectedConversationId = null;
this.reviewPreviewLines = [];
this.reviewPreviewError = null;
this.reviewGeneratedPath = null;
this.closeQuickMenu();
// 弹窗使用独立列表(仅含有内容的对话),加载完成后自动选中首个可用项
this.loadReviewConversations();
},
autoSelectReviewConversation() {
if (this.reviewSelectedConversationId) return;
const fallback = this.reviewConversations.find((c) => c.id !== this.currentConversationId);
if (fallback && fallback.id) {
this.reviewSelectedConversationId = fallback.id;
this.loadReviewPreview(fallback.id);
}
},
closeHeaderMenu() {
this.headerMenuOpen = false;
},
handleReviewSelect(id) {
if (id === this.currentConversationId) {
this.uiPushToast({
title: '无法引用当前对话',
message: '请选择其他对话生成回顾',
type: 'warning'
});
return;
}
this.reviewSelectedConversationId = id;
this.loadReviewPreview(id);
},
confirmAction(options = {}) {
return this.uiRequestConfirm(options);
},
async submitUserQuestionAnswers(answers) {
const list = Array.isArray(answers) ? answers : [];
if (!list.length) {
return;
}
const ids = list.map((item) => String(item?.question_id || '')).filter(Boolean);
this.answeringUserQuestionIds = ids;
try {
for (const answer of list) {
const questionId = String(answer?.question_id || '').trim();
if (!questionId) {
continue;
}
const response = await fetch(`/api/user-questions/${encodeURIComponent(questionId)}/answer`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
selected_option_id: answer?.selected_option_id || undefined,
text: answer?.text || '',
dismissed: answer?.dismissed === true
})
});
const payload = await response.json().catch(() => ({}));
if (!response.ok || !payload?.success) {
throw new Error(payload?.message || payload?.error || '提交回答失败');
}
}
this.pendingUserQuestions = (this.pendingUserQuestions || []).filter(
(item) => item && !ids.includes(String(item.question_id || ''))
);
if (!this.pendingUserQuestions.length) {
this.userQuestionDialogVisible = false;
this.userQuestionMinimized = false;
this.userQuestionActiveIndex = 0;
if (typeof this.restoreUserQuestionTitle === 'function') {
this.restoreUserQuestionTitle();
}
} else {
this.userQuestionActiveIndex = Math.min(this.userQuestionActiveIndex, this.pendingUserQuestions.length - 1);
}
} catch (error) {
const msg = error instanceof Error ? error.message : String(error || '提交回答失败');
this.uiPushToast({ title: '提交回答失败', message: msg, type: 'error' });
} finally {
this.answeringUserQuestionIds = [];
}
},
// 用户点击「不回答」:只将当前查看的这个问题标记为 dismissed其余问题保持待回答。
// 模型侧会收到「用户没有回答或不想回答」的工具结果并改为在对话中直接提问。
async dismissUserQuestions(questionId) {
const id = String(questionId || '').trim();
if (!id) {
return;
}
await this.submitUserQuestionAnswers([{ question_id: id, dismissed: true }]);
},
async answerUserQuestionFromComposer(text) {
const clean = String(text || '').trim();
if (!clean || !Array.isArray(this.pendingUserQuestions) || !this.pendingUserQuestions.length) {
return false;
}
const index = Math.min(
Math.max(0, Number(this.userQuestionActiveIndex || 0)),
Math.max(0, this.pendingUserQuestions.length - 1)
);
const question = this.pendingUserQuestions[index] || this.pendingUserQuestions[0];
if (!question?.question_id) {
return false;
}
await this.submitUserQuestionAnswers([{ question_id: question.question_id, text: clean }]);
if (this.pendingUserQuestions.length > 0) {
this.userQuestionDialogVisible = true;
this.userQuestionMinimized = false;
}
return true;
},
async decideToolApproval(approvalId, decision) {
const id = String(approvalId || '').trim();
if (!id) {
return;
}
if (!Array.isArray(this.decidingApprovalIds)) {
this.decidingApprovalIds = [];
}
if (!this.decidingApprovalIds.includes(id)) {
this.decidingApprovalIds.push(id);
}
try {
const response = await fetch(`/api/tool-approvals/${encodeURIComponent(id)}/decision`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ decision })
});
const payload = await response.json().catch(() => ({}));
if (!response.ok || !payload?.success) {
throw new Error(payload?.message || payload?.error || '提交审批失败');
}
this.pendingToolApprovals = (this.pendingToolApprovals || []).filter(
(item) => item && item.approval_id !== id
);
if (!this.pendingToolApprovals.length) {
this.rightCollapsed = true;
if (this.isMobileViewport && this.activeMobileOverlay === 'approval') {
this.closeMobileOverlay();
}
}
} catch (error) {
const msg = error instanceof Error ? error.message : String(error || '审批失败');
this.uiPushToast({
title: '审批失败',
message: msg,
type: 'error'
});
} finally {
this.decidingApprovalIds = (this.decidingApprovalIds || []).filter((item) => item !== id);
}
}
};