agent-Specialization/static/src/app/methods/resources.ts
JOJO 9618148ed3 refactor(execution): 移除执行环境 TTL 自动回退机制
- 后端:移除 host_execution_direct_until、_refresh_execution_mode_by_ttl
  及 HOST_EXECUTION_DIRECT_TTL_SECONDS 配置,切换执行环境后一直生效
- 前端:移除到期定时同步、回退检测、自动回退 toast 及权限自动降级逻辑
- 文档:AGENTS.md 与 host_sandbox_and_permission_model.md 同步更新
2026-07-21 17:12:57 +08:00

346 lines
10 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 {
formatTokenCount,
formatBytes,
formatPercentage,
formatRate,
formatResetTime,
formatQuotaValue,
quotaTypeLabel,
buildQuotaResetSummary,
isQuotaExceeded as isQuotaExceededUtil,
buildQuotaToastMessage
} from '../../utils/formatters';
export const resourceMethods = {
hasContainerStats() {
return !!(
this.containerStatus &&
this.containerStatus.mode === 'docker' &&
this.containerStatus.stats
);
},
containerStatusClass() {
if (!this.containerStatus) {
return 'status-pill--host';
}
if (this.containerStatus.mode !== 'docker') {
return 'status-pill--host';
}
const rawStatus =
(this.containerStatus.state &&
(this.containerStatus.state.status || this.containerStatus.state.Status)) ||
'';
const status = String(rawStatus).toLowerCase();
if (status.includes('running')) {
return 'status-pill--running';
}
if (status.includes('paused')) {
return 'status-pill--stopped';
}
if (status.includes('exited') || status.includes('dead')) {
return 'status-pill--stopped';
}
return 'status-pill--running';
},
containerStatusText() {
if (!this.containerStatus) {
return '未知';
}
if (this.containerStatus.mode !== 'docker') {
return '宿主机模式';
}
const rawStatus =
(this.containerStatus.state &&
(this.containerStatus.state.status || this.containerStatus.state.Status)) ||
'';
const status = String(rawStatus).toLowerCase();
if (status.includes('running')) {
return '运行中';
}
if (status.includes('paused')) {
return '已暂停';
}
if (status.includes('exited') || status.includes('dead')) {
return '已停止';
}
return rawStatus || '容器模式';
},
formatTime(value) {
if (!value) {
return '未知时间';
}
let date;
if (typeof value === 'number') {
date = new Date(value);
} else if (typeof value === 'string') {
const parsed = Date.parse(value);
if (!Number.isNaN(parsed)) {
date = new Date(parsed);
} else {
const numeric = Number(value);
if (!Number.isNaN(numeric)) {
date = new Date(numeric);
}
}
} else if (value instanceof Date) {
date = value;
}
if (!date || Number.isNaN(date.getTime())) {
return String(value);
}
const now = Date.now();
const diff = now - date.getTime();
if (diff < 60000) {
return '刚刚';
}
if (diff < 3600000) {
const mins = Math.floor(diff / 60000);
return `${mins} 分钟前`;
}
if (diff < 86400000) {
const hours = Math.floor(diff / 3600000);
return `${hours} 小时前`;
}
const formatter = new Intl.DateTimeFormat('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
return formatter.format(date);
},
async updateCurrentContextTokens() {
await this.resourceUpdateCurrentContextTokens(this.currentConversationId);
},
async fetchConversationTokenStatistics() {
await this.resourceFetchConversationTokenStatistics(this.currentConversationId);
},
toggleTokenPanel() {
this.resourceToggleTokenPanel();
},
applyStatusSnapshot(status) {
this.resourceApplyStatusSnapshot(status);
// 模型/思考模式/推理强度的权威来源规则:
// - 非空对话:以对话 meta 为准enterConversation bootstrap 恢复),
// status 快照不得覆盖terminal 状态可能滞后于对话加载)
// - 空对话/首页terminal 状态即权威,从 status 同步
const convId = this.currentConversationId;
const hasConversation =
typeof convId === 'string' && convId.length > 0 && !convId.startsWith('temp_');
if (!hasConversation) {
if (status && typeof status.thinking_mode !== 'undefined') {
this.thinkingMode = !!status.thinking_mode;
}
if (status && typeof status.run_mode === 'string') {
// 历史值 deep 映射为 thinking
this.runMode = status.run_mode === 'deep' ? 'thinking' : status.run_mode;
} else if (status && typeof status.thinking_mode !== 'undefined') {
this.runMode = status.thinking_mode ? 'thinking' : 'fast';
}
if (status && Object.prototype.hasOwnProperty.call(status, 'reasoning_effort')) {
this.reasoningEffort =
typeof status.reasoning_effort === 'string' ? status.reasoning_effort : null;
}
if (status && typeof status.model_key === 'string') {
this.modelSet(status.model_key);
}
}
if (status && typeof status.permission_mode === 'string') {
this.currentPermissionMode = status.permission_mode;
}
const pendingModes = status?.pending_runtime_modes || {};
if (typeof pendingModes.permission_mode === 'string') {
this.pendingPermissionMode = pendingModes.permission_mode;
} else {
this.pendingPermissionMode = '';
}
if (status?.execution_mode && typeof status.execution_mode === 'object') {
if (typeof status.execution_mode.mode === 'string') {
this.currentExecutionMode = status.execution_mode.mode;
}
this.executionModeEnabled =
typeof status.execution_mode_enabled === 'boolean' ? status.execution_mode_enabled : true;
}
if (typeof status.network_permission === 'string') {
this.currentNetworkPermission = status.network_permission;
}
this.networkPermissionEnabled =
typeof status.network_permission_enabled === 'boolean' ? status.network_permission_enabled : false;
if (typeof pendingModes.network_permission === 'string') {
this.pendingNetworkPermission = pendingModes.network_permission;
} else {
this.pendingNetworkPermission = '';
}
if (typeof pendingModes.execution_mode === 'string') {
this.pendingExecutionMode = pendingModes.execution_mode;
} else {
this.pendingExecutionMode = '';
}
if (status && typeof status.has_images !== 'undefined') {
this.conversationHasImages = !!status.has_images;
}
if (status && typeof status.has_videos !== 'undefined') {
this.conversationHasVideos = !!status.has_videos;
}
const compression = status?.conversation?.compression;
if (compression && typeof compression === 'object') {
this.compressionInProgress = !!compression.in_progress;
this.compressionMode = compression.mode || '';
this.compressionStage = compression.stage || '';
this.compressionError = compression.error || '';
} else {
this.compressionInProgress = false;
this.compressionMode = '';
this.compressionStage = '';
this.compressionError = '';
}
},
updateContainerStatus(status) {
this.resourceUpdateContainerStatus(status);
},
pollContainerStats() {
return this.resourcePollContainerStats();
},
startContainerStatsPolling() {
this.resourceStartContainerStatsPolling();
},
stopContainerStatsPolling() {
this.resourceStopContainerStatsPolling();
},
pollProjectStorage() {
return this.resourcePollProjectStorage();
},
startProjectStoragePolling() {
this.resourceStartProjectStoragePolling();
},
stopProjectStoragePolling() {
this.resourceStopProjectStoragePolling();
},
fetchUsageQuota() {
return this.resourceFetchUsageQuota();
},
startUsageQuotaPolling() {
this.resourceStartUsageQuotaPolling();
},
stopUsageQuotaPolling() {
this.resourceStopUsageQuotaPolling();
},
async downloadFile(path) {
if (!path) {
this.fileHideContextMenu();
return;
}
const url = `/api/download/file?path=${encodeURIComponent(path)}`;
const name = path.split('/').pop() || 'file';
await this.downloadResource(url, name);
},
async downloadFolder(path) {
if (!path) {
this.fileHideContextMenu();
return;
}
const url = `/api/download/folder?path=${encodeURIComponent(path)}`;
const segments = path.split('/').filter(Boolean);
const folderName = segments.length ? segments.pop() : 'folder';
await this.downloadResource(url, `${folderName}.zip`);
},
async downloadResource(url, filename) {
// Android App 内优先走原生下载桥接,解决 WebView 中 a.download / blob URL 无法触发系统下载的问题
const androidBridge = (window as any).AndroidDownloadBridge;
if (androidBridge && typeof androidBridge.downloadFile === 'function') {
try {
const absoluteUrl = new URL(url, window.location.href).href;
androidBridge.downloadFile(absoluteUrl, filename || 'download');
return;
} catch (error) {
console.warn('Android 桥接下载失败,回退:', error);
}
}
try {
const response = await fetch(url);
if (!response.ok) {
let message = response.statusText;
try {
const errorData = await response.json();
message = errorData.error || errorData.message || message;
} catch (err) {
message = await response.text();
}
this.uiPushToast({
title: '下载失败',
message: message || '无法完成下载',
type: 'error'
});
return;
}
const blob = await response.blob();
const downloadName = filename || 'download';
const link = document.createElement('a');
const href = URL.createObjectURL(blob);
link.href = href;
link.download = downloadName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(href);
} catch (error) {
console.error('下载失败:', error);
this.uiPushToast({
title: '下载失败',
message: error.message || String(error),
type: 'error'
});
} finally {
this.fileHideContextMenu();
}
},
formatTokenCount,
formatBytes,
formatPercentage,
formatRate,
formatResetTime,
formatQuotaValue,
quotaTypeLabel,
quotaResetSummary() {
return buildQuotaResetSummary(this.usageQuota);
},
isQuotaExceeded(type) {
return isQuotaExceededUtil(this.usageQuota, type);
},
showQuotaToast(payload) {
if (!payload) {
return;
}
const type = payload.type || 'fast';
const message = buildQuotaToastMessage(type, this.usageQuota, payload.reset_at);
this.uiShowQuotaToastMessage(message, type);
}
};