- 后端:移除 host_execution_direct_until、_refresh_execution_mode_by_ttl 及 HOST_EXECUTION_DIRECT_TTL_SECONDS 配置,切换执行环境后一直生效 - 前端:移除到期定时同步、回退检测、自动回退 toast 及权限自动降级逻辑 - 文档:AGENTS.md 与 host_sandbox_and_permission_model.md 同步更新
346 lines
10 KiB
TypeScript
346 lines
10 KiB
TypeScript
// @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);
|
||
}
|
||
};
|