// ==UserScript==
// @name Docker镜像离线下载器
// @namespace http://tampermonkey.net/
// @version 1.0.2
// @description 在Docker Hub页面添加下载按钮,实现离线镜像下载功能
// @author lfree
// @match https://hub.docker.com/*
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// @grant GM_download
// @connect registry.lfree.org
// @connect registry-1.docker.io
// @connect auth.docker.io
// @connect production.cloudflare.docker.com
// @connect cloudflare.docker.com
// @connect cdn.docker.com
// @connect docker.com
// @connect docker.io
// @connect ghcr.io
// @connect gcr.io
// @connect quay.io
// @connect registry.k8s.io
// @connect *.docker.com
// @connect *.docker.io
// @connect *.cloudflare.com
// @connect *
// @require https://update.greasyfork.org/scripts/539732/1609156/tarballjs.js
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// ==================== 全局变量和配置 ====================
let downloadInProgress = false; // 下载进程状态标志
let manifestData = null; // 镜像清单数据存储
let downloadedLayers = new Map(); // 已下载镜像层的映射表
let tempLayerCache = new Map(); // 临时层数据缓存
let db = null; // IndexedDB数据库实例
let cachedArchitectures = null; // 缓存的架构信息,包含架构和对应的SHA256
let useTemporaryCache = false; // 是否使用临时缓存(minimal模式)
let selectedMemoryMode = 'minimal'; // 内存模式选择,直接使用最小内存模式
let downloadProgressMap = new Map(); // 实时下载进度映射
let progressUpdateInterval = null; // 进度更新定时器
let userManuallySelectedArch = false; // 标记用户是否手动选择过架构
// API和Registry配置
const API_BASE_URL = 'https://registry.lfree.org/api';
let hub_host = 'registry-1.docker.io';
const auth_url = 'https://auth.docker.io';
// 下载模式配置
let downloadMode = 'remote'; // 'remote' 或 'direct'
const DOWNLOAD_MODES = {
remote: '远程API',
direct: '直接访问'
};
// ==================== GM_xmlhttpRequest包装函数 ====================
/**
* GM_xmlhttpRequest的Promise包装函数,用于替代fetch以避免CORS问题
* 功能:将GM_xmlhttpRequest包装为Promise格式,提供与fetch相似的API
* @param {string} url - 请求URL
* @param {Object} options - 请求选项
* @returns {Promise<Response>} 包装后的响应对象
*/
function gmFetch(url, options = {}) {
return new Promise((resolve, reject) => {
const requestOptions = {
method: options.method || 'GET',
url: url,
headers: options.headers || {},
responseType: options.responseType || (options.stream ? 'arraybuffer' : 'json'),
timeout: options.timeout || 30000, // 30秒超时
onload: function(response) {
// 解析响应头为Headers对象格式
const headers = new Map();
if (response.responseHeaders) {
// GM_xmlhttpRequest返回的responseHeaders是字符串格式
const headersStr = response.responseHeaders;
const headerLines = headersStr.split('\r\n');
for (const line of headerLines) {
const colonIndex = line.indexOf(':');
if (colonIndex > 0) {
const key = line.substring(0, colonIndex).trim().toLowerCase();
const value = line.substring(colonIndex + 1).trim();
headers.set(key, value);
}
}
}
// 创建类似fetch Response的对象
const responseObj = {
ok: response.status >= 200 && response.status < 300,
status: response.status,
statusText: response.statusText,
headers: {
get: function(name) {
return headers.get(name.toLowerCase());
},
has: function(name) {
return headers.has(name.toLowerCase());
},
entries: function() {
return headers.entries();
},
keys: function() {
return headers.keys();
},
values: function() {
return headers.values();
},
forEach: function(callback) {
headers.forEach(callback);
}
},
url: response.finalUrl || url,
json: async function() {
try {
if (typeof response.response === 'string') {
return JSON.parse(response.response);
} else if (response.response instanceof ArrayBuffer) {
const text = new TextDecoder().decode(response.response);
return JSON.parse(text);
} else {
return response.response;
}
} catch (e) {
throw new Error('Invalid JSON response: ' + e.message);
}
},
text: async function() {
if (typeof response.response === 'string') {
return response.response;
} else if (response.response instanceof ArrayBuffer) {
return new TextDecoder().decode(response.response);
} else {
return response.responseText || String(response.response);
}
},
arrayBuffer: async function() {
if (response.response instanceof ArrayBuffer) {
return response.response;
} else if (typeof response.response === 'string') {
return new TextEncoder().encode(response.response).buffer;
} else {
throw new Error('Cannot convert response to ArrayBuffer');
}
},
body: {
getReader: function() {
// 简化的流读取器实现,适用于GM_xmlhttpRequest
let consumed = false;
return {
read: async function() {
if (consumed) {
return { done: true, value: undefined };
}
consumed = true;
let data;
if (response.response instanceof ArrayBuffer) {
data = new Uint8Array(response.response);
} else if (typeof response.response === 'string') {
data = new TextEncoder().encode(response.response);
} else {
data = new Uint8Array(0);
}
return { done: false, value: data };
}
};
}
}
};
resolve(responseObj);
},
onerror: function(response) {
const errorMsg = response.statusText || response.error || 'Network request failed';
reject(new Error(`GM_xmlhttpRequest error: ${errorMsg}`));
},
ontimeout: function() {
reject(new Error('Request timeout'));
},
onabort: function() {
reject(new Error('Request aborted'));
}
};
// 如果有请求体,添加到选项中
if (options.body) {
requestOptions.data = options.body;
}
// 设置Content-Type(如果需要)
if (options.body && !requestOptions.headers['Content-Type']) {
requestOptions.headers['Content-Type'] = 'application/json';
}
try {
GM_xmlhttpRequest(requestOptions);
} catch (error) {
reject(new Error('Failed to create GM_xmlhttpRequest: ' + error.message));
}
});
}
// ==================== 样式定义 ====================
/**
* 添加自定义CSS样式到页面
* 功能:定义下载器界面的所有视觉样式
*/
function addCustomStyles() {
GM_addStyle(`
/* 主要下载按钮样式 - 内联版本 */
.docker-download-btn {
background: linear-gradient(145deg, #28a745, #218838);
color: white;
border: 2px solid #28a745;
padding: 8px 16px;
border-radius: 6px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
display: inline-flex;
align-items: center;
gap: 6px;
margin: 0 8px 0 0;
box-shadow: 0 2px 8px rgba(40, 167, 69, 0.2);
text-decoration: none;
position: relative;
overflow: hidden;
vertical-align: middle;
}
/* 按钮悬停效果 */
.docker-download-btn:hover {
background: linear-gradient(145deg, #218838, #1e7e34);
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(40, 167, 69, 0.4);
color: white;
text-decoration: none;
}
/* 按钮禁用状态 */
.docker-download-btn:disabled {
background: linear-gradient(145deg, #6c757d, #5a6268);
cursor: not-allowed;
transform: none;
animation: downloadProgress 2s infinite linear;
}
/* 架构选择下拉框样式 - 与按钮统一 */
.arch-selector {
margin: 0;
padding: 8px 16px;
border: 2px solid #28a745;
border-radius: 6px;
background: white;
font-size: 13px;
font-weight: 600;
min-width: 120px;
height: 34px;
vertical-align: middle;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-sizing: border-box;
}
.arch-selector:hover {
border-color: #218838;
box-shadow: 0 2px 8px rgba(40, 167, 69, 0.25);
transform: translateY(-1px);
}
.arch-selector:focus {
outline: none;
border-color: #218838;
box-shadow: 0 0 0 3px rgba(40, 167, 69, 0.25);
}
/* 移除进度显示区域样式,改为在按钮上显示 */
/* 下载进度动画 */
@keyframes downloadProgress {
0% { background-position: -100% 0; }
100% { background-position: 100% 0; }
}
/* 内联按钮容器样式 */
.docker-download-container {
display: inline-flex !important;
align-items: center !important;
gap: 8px !important;
margin: 0 10px !important;
vertical-align: middle !important;
background: none !important;
border: none !important;
padding: 0 !important;
box-shadow: none !important;
font-family: inherit !important;
}
/* 检测架构按钮特殊样式 */
.detect-arch-btn {
background: linear-gradient(145deg, #6f42c1, #5a32a3);
border-color: #6f42c1;
}
.detect-arch-btn:hover {
background: linear-gradient(145deg, #5a32a3, #4e2a87);
border-color: #5a32a3;
}
/* 模式切换按钮样式 */
.mode-toggle-btn {
transition: all 0.2s ease;
position: relative;
overflow: hidden;
}
.mode-toggle-btn::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
transform: translate(-50%, -50%);
transition: width 0.6s, height 0.6s;
}
.mode-toggle-btn:active::before {
width: 300px;
height: 300px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.docker-download-container {
flex-direction: column !important;
align-items: stretch !important;
}
.docker-download-btn,
.arch-selector {
width: 100% !important;
margin: 5px 0 !important;
min-width: auto !important;
}
}
`);
}
// ==================== 数据存储管理 ====================
/**
* 初始化IndexedDB数据库
* 功能:创建和配置用于存储镜像层数据的本地数据库
* @returns {Promise} 数据库初始化完成的Promise
*/
function initIndexedDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('DockerImageStore', 1);
// 数据库连接失败处理
request.onerror = () => reject(request.error);
// 数据库连接成功处理
request.onsuccess = () => {
db = request.result;
resolve();
};
// 数据库结构升级处理
request.onupgradeneeded = (event) => {
const database = event.target.result;
// 创建镜像层存储表
if (!database.objectStoreNames.contains('layers')) {
const layerStore = database.createObjectStore('layers', { keyPath: 'digest' });
layerStore.createIndex('imageName', 'imageName', { unique: false });
}
// 创建镜像清单存储表
if (!database.objectStoreNames.contains('manifests')) {
const manifestStore = database.createObjectStore('manifests', { keyPath: 'key' });
}
};
});
}
// ==================== 页面信息提取 ====================
/**
* 从当前Docker Hub页面提取镜像信息
* 功能:自动识别并提取镜像名称和标签信息
* @returns {Object} 包含imageName和imageTag的对象
*/
function extractImageInfo() {
const url = window.location.pathname;
const pathParts = url.split('/').filter(part => part);
let imageName = '';
let imageTag = 'latest';
// Docker Hub URL格式分析和处理
// 支持的格式:
// - 官方镜像: /r/nginx, /_/nginx
// - 用户镜像: /r/username/imagename
// - 组织镜像: /r/organization/imagename
// - 镜像层页面: /layers/username/imagename/tag/images/sha256-...
// 解析URL路径段
if (pathParts.length >= 4 && pathParts[0] === 'layers') {
// 处理 layers 页面格式: /layers/username/imagename/tag/...
const namespace = pathParts[1];
const repoName = pathParts[2];
const tag = pathParts[3];
if (namespace === '_') {
// 官方镜像: /layers/_/nginx/latest/...
imageName = repoName;
} else {
// 用户/组织镜像: /layers/username/imagename/tag/...
imageName = namespace + '/' + repoName;
}
imageTag = tag;
// 从layers URL提取镜像信息
} else if (pathParts.length >= 2 && pathParts[0] === 'r') {
// 处理 /r/ 路径格式
if (pathParts.length === 2) {
// 官方镜像: /r/nginx
imageName = pathParts[1];
} else if (pathParts.length >= 3) {
// 用户/组织镜像: /r/username/imagename
imageName = pathParts[1] + '/' + pathParts[2];
}
// 检查是否有tags路径来提取特定标签
if (pathParts.includes('tags') && pathParts.length > pathParts.indexOf('tags') + 1) {
const tagIndex = pathParts.indexOf('tags') + 1;
imageTag = pathParts[tagIndex];
}
// 从/r/ URL提取镜像信息
} else if (pathParts.length >= 2 && pathParts[0] === '_') {
// 官方镜像的另一种格式: /_/nginx
imageName = pathParts[1];
// 从/_/ URL提取镜像信息
} else if (pathParts.length >= 2) {
// 其他格式的通用处理(保留原有逻辑作为备用)
imageName = pathParts[0] + '/' + pathParts[1];
// 通用格式提取镜像信息
}
// 从页面DOM元素提取信息作为备用方案
if (!imageName) {
// 尝试从页面标题获取镜像名称
const titleSelectors = [
'h1[data-testid="repository-title"]',
'.RepositoryNameHeader__repositoryName',
'h1.repository-title',
'.repository-name'
];
for (const selector of titleSelectors) {
const titleElement = document.querySelector(selector);
if (titleElement) {
imageName = titleElement.textContent.trim();
break;
}
}
// 尝试从面包屑导航获取
if (!imageName) {
const breadcrumbLinks = document.querySelectorAll('[data-testid="breadcrumb"] a, .breadcrumb a');
if (breadcrumbLinks.length >= 2) {
imageName = breadcrumbLinks[breadcrumbLinks.length - 1].textContent.trim();
}
}
}
// 提取标签信息
const tagSelectors = [
'[data-testid="tag-name"]',
'.tag-name',
'.current-tag',
'.active-tag'
];
for (const selector of tagSelectors) {
const tagElement = document.querySelector(selector);
if (tagElement) {
const tagText = tagElement.textContent.trim();
imageTag = tagText.replace(':', '').replace('Tag: ', '');
break;
}
}
// 从URL查询参数获取标签信息
const urlParams = new URLSearchParams(window.location.search);
const tagFromUrl = urlParams.get('tag');
if (tagFromUrl) {
imageTag = tagFromUrl;
}
// 最终结果处理和修正
if (imageName) {
// 对于官方镜像(不包含斜杠的镜像名),添加library前缀
if (!imageName.includes('/') && imageName !== '') {
imageName = 'library/' + imageName;
// 添加library前缀
}
}
console.log('最终提取的镜像信息:', { imageName, imageTag, url });
return { imageName, imageTag };
}
// ==================== UI界面创建 ====================
/**
* 创建内联下载按钮
* 功能:生成直接集成到Docker Hub界面中的下载按钮
* @returns {HTMLElement} 下载按钮DOM元素
*/
function createInlineDownloadButton() {
const button = document.createElement('button');
button.className = 'docker-download-btn';
button.id = 'downloadBtn';
button.innerHTML = '🚀 下载镜像';
button.title = '点击下载Docker镜像';
return button;
}
/**
* 创建架构选择下拉框
* 功能:生成紧凑的架构选择器,支持自动架构检测
* @returns {HTMLElement} 架构选择器DOM元素
*/
function createArchSelector() {
const select = document.createElement('select');
select.className = 'arch-selector';
select.id = 'archSelector';
select.title = '选择目标架构(将自动检测页面架构)';
select.innerHTML = `
<option value="">自动检测</option>
<option value="linux/amd64">linux/amd64</option>
<option value="linux/arm64">linux/arm64</option>
<option value="linux/arm/v7">linux/arm/v7</option>
<option value="linux/arm/v6">linux/arm/v6</option>
<option value="linux/386">linux/386</option>
<option value="windows/amd64">windows/amd64</option>
`;
// 添加用户手动选择监听器
select.addEventListener('change', (e) => {
if (e.target.value !== '') {
userManuallySelectedArch = true;
addLog(`用户手动选择架构: ${e.target.value}`);
// 更新标题显示用户选择的架构
e.target.title = `当前架构: ${e.target.value} (手动选择)`;
} else {
userManuallySelectedArch = false;
addLog('用户选择自动检测架构');
e.target.title = '选择目标架构(将自动检测页面架构)';
}
});
// 异步设置自动检测的架构和更新可用架构列表
setTimeout(async () => {
const indicator = document.getElementById('archIndicator');
try {
// 显示检测状态
if (indicator) {
indicator.style.display = 'inline';
indicator.textContent = '🔍 获取架构列表...';
}
// 首先获取镜像的可用架构列表
const availableArchs = await getAvailableArchitectures();
if (availableArchs && availableArchs.length > 0) {
// 更新架构选择器选项
updateArchSelectorOptions(select, availableArchs);
addLog(`已更新架构选择器,包含 ${availableArchs.length} 个可用架构`);
// 架构信息已更新到选择器中
if (indicator) {
indicator.textContent = '🔍 检测当前架构...';
}
}
// 只有在用户没有手动选择架构时才进行自动检测
if (!userManuallySelectedArch) {
const detectedArch = await autoDetectArchitecture();
if (detectedArch) {
// 检查选项中是否存在检测到的架构
const existingOption = select.querySelector(`option[value="${detectedArch}"]`);
if (existingOption) {
select.value = detectedArch;
addLog(`架构选择器已设置为检测到的架构: ${detectedArch}`);
} else {
// 如果选项中不存在,添加新选项
const newOption = document.createElement('option');
newOption.value = detectedArch;
newOption.textContent = detectedArch;
newOption.selected = true;
select.appendChild(newOption);
addLog(`已添加并选择检测到的架构: ${detectedArch}`);
}
// 更新选择器标题显示当前检测到的架构
select.title = `当前架构: ${detectedArch} (自动检测)`;
// 更新状态指示器
if (indicator) {
indicator.textContent = `✅ ${detectedArch}`;
indicator.style.color = '#28a745';
setTimeout(() => {
indicator.style.display = 'none';
}, 3000);
}
} else {
// 检测失败时的处理
if (indicator) {
indicator.textContent = '❌ 检测失败';
indicator.style.color = '#dc3545';
setTimeout(() => {
indicator.style.display = 'none';
}, 3000);
}
}
} else {
// 用户已手动选择,跳过自动检测
addLog('跳过自动架构检测(用户已手动选择)');
if (indicator) {
indicator.textContent = '✋ 已手动选择';
indicator.style.color = '#fd7e14';
setTimeout(() => {
indicator.style.display = 'none';
}, 2000);
}
}
} catch (error) {
addLog(`自动架构检测失败: ${error.message}`, 'error');
if (indicator) {
indicator.textContent = '❌ 检测失败';
indicator.style.color = '#dc3545';
setTimeout(() => {
indicator.style.display = 'none';
}, 3000);
}
}
}, 1000); // 延迟1秒等待页面完全加载
return select;
}
/**
* 创建下载模式切换按钮
* 功能:生成下载模式切换按钮,点击即可在远程API和直接访问之间切换
* @returns {HTMLElement} 模式切换按钮DOM元素
*/
function createModeToggleButton() {
const button = document.createElement('button');
button.className = 'docker-download-btn mode-toggle-btn';
button.id = 'modeToggleBtn';
// 基础样式(大小在外部统一设置)
button.style.cssText = `
background: linear-gradient(145deg, #6f42c1, #5a32a3);
border-color: #6f42c1;
`;
// 根据当前模式设置按钮文本和样式
function updateButtonText() {
if (downloadMode === 'remote') {
button.innerHTML = '🌐 远程API';
button.title = '当前:远程API模式,点击切换到直接访问模式';
button.style.background = 'linear-gradient(145deg, #007bff, #0056b3)';
button.style.borderColor = '#007bff';
} else {
button.innerHTML = '🔗 直接访问';
button.title = '当前:直接访问模式,点击切换到远程API模式';
button.style.background = 'linear-gradient(145deg, #6f42c1, #5a32a3)';
button.style.borderColor = '#6f42c1';
}
}
updateButtonText();
// 添加点击监听器
button.addEventListener('click', async (e) => {
e.preventDefault();
// 切换模式
downloadMode = downloadMode === 'remote' ? 'direct' : 'remote';
addLog(`下载模式已切换为: ${DOWNLOAD_MODES[downloadMode]}`);
// 更新按钮显示
updateButtonText();
// 显示切换动画
button.style.transform = 'scale(0.95)';
setTimeout(() => {
button.style.transform = 'scale(1)';
}, 150);
// 清理缓存的架构信息,因为不同模式可能有不同的结果
cachedArchitectures = null;
// 触发重新检测架构(保持用户的手动选择)
try {
const availableArchs = await getAvailableArchitectures();
const archSelector = document.getElementById('archSelector');
if (availableArchs && availableArchs.length > 0 && archSelector) {
const currentUserSelection = archSelector.value; // 保存用户当前选择
updateArchSelectorOptions(archSelector, availableArchs);
// 如果用户之前有手动选择,尝试恢复选择
if (userManuallySelectedArch && currentUserSelection) {
const optionExists = archSelector.querySelector(`option[value="${currentUserSelection}"]`);
if (optionExists) {
archSelector.value = currentUserSelection;
addLog(`已恢复用户手动选择的架构: ${currentUserSelection}`);
} else {
// 如果选项不存在,添加一个
const newOption = document.createElement('option');
newOption.value = currentUserSelection;
newOption.textContent = currentUserSelection;
newOption.selected = true;
archSelector.appendChild(newOption);
addLog(`已添加并恢复用户选择的架构: ${currentUserSelection}`);
}
}
addLog(`已更新架构选择器(${DOWNLOAD_MODES[downloadMode]}模式)`);
}
} catch (error) {
addLog(`切换模式后重新获取架构失败: ${error.message}`, 'error');
}
});
// 添加悬停效果
button.addEventListener('mouseenter', () => {
button.style.transform = 'translateY(-1px)';
button.style.boxShadow = '0 4px 12px rgba(111, 66, 193, 0.3)';
});
button.addEventListener('mouseleave', () => {
button.style.transform = 'translateY(0)';
button.style.boxShadow = '0 2px 8px rgba(111, 66, 193, 0.2)';
});
return button;
}
/**
* 创建检测架构按钮
* 功能:生成架构检测按钮
* @returns {HTMLElement} 检测按钮DOM元素
*/
function createDetectArchButton() {
const button = document.createElement('button');
button.className = 'docker-download-btn detect-arch-btn';
button.id = 'detectArchBtn';
button.innerHTML = '🔍 检测架构';
button.title = '检测镜像支持的架构';
return button;
}
// 移除进度显示区域创建函数,改为在按钮上显示进度
// ==================== 日志和工具函数 ====================
/**
* 添加日志信息(仅在控制台显示)
* 功能:在控制台记录下载进度和状态信息
* @param {string} message - 要显示的日志消息
* @param {string} type - 日志类型(info, error, success)
*/
function addLog(message, type = 'info') {
console.log('Docker Downloader:', message);
}
/**
* 更新下载按钮上的进度显示(增强版)
* 功能:在下载按钮上显示详细的下载信息和进度
* @param {string} text - 要显示的文本
* @param {string} status - 状态类型(downloading, complete, error)
* @param {Object} details - 详细信息对象
*/
function updateButtonProgress(text, status = 'downloading', details = {}) {
const downloadBtn = document.getElementById('downloadBtn');
if (!downloadBtn) return;
// 构建详细的按钮文本
let buttonText = text;
// 如果有详细信息,添加到按钮文本中
if (details.progress !== undefined) {
buttonText += ` ${details.progress}%`;
}
if (details.speed && details.speed > 0) {
buttonText += ` (${formatSpeed(details.speed)})`;
}
if (details.current && details.total) {
buttonText += ` [${details.current}/${details.total}]`;
}
if (details.size && status !== 'downloading') {
buttonText += ` ${formatSize(details.size)}`;
}
downloadBtn.textContent = buttonText;
// 根据状态设置按钮样式
switch (status) {
case 'downloading':
downloadBtn.style.background = 'linear-gradient(145deg, #007bff, #0056b3)';
downloadBtn.style.color = 'white';
downloadBtn.disabled = true;
break;
case 'complete':
downloadBtn.style.background = 'linear-gradient(145deg, #28a745, #218838)';
downloadBtn.style.color = 'white';
downloadBtn.disabled = false;
setTimeout(() => {
downloadBtn.textContent = '🚀 下载镜像';
downloadBtn.style.background = 'linear-gradient(145deg, #28a745, #218838)';
}, 3000);
break;
case 'error':
downloadBtn.style.background = 'linear-gradient(145deg, #dc3545, #c82333)';
downloadBtn.style.color = 'white';
downloadBtn.disabled = false;
setTimeout(() => {
downloadBtn.textContent = '🚀 下载镜像';
downloadBtn.style.background = 'linear-gradient(145deg, #28a745, #218838)';
}, 3000);
break;
case 'analyzing':
downloadBtn.style.background = 'linear-gradient(145deg, #6f42c1, #5a32a3)';
downloadBtn.style.color = 'white';
downloadBtn.disabled = true;
break;
case 'assembling':
downloadBtn.style.background = 'linear-gradient(145deg, #fd7e14, #e8690b)';
downloadBtn.style.color = 'white';
downloadBtn.disabled = true;
break;
default:
downloadBtn.style.background = 'linear-gradient(145deg, #28a745, #218838)';
downloadBtn.style.color = 'white';
downloadBtn.disabled = false;
}
}
/**
* 智能选择内存策略
* 功能:根据镜像大小和用户设置选择最适合的内存模式
*/
function chooseMemoryStrategy() {
if (!manifestData) return;
const totalSize = manifestData.totalSize;
addLog(`📊 镜像总大小: ${formatSize(totalSize)}`);
// 根据用户选择的模式和镜像大小确定存储策略
if (selectedMemoryMode === 'minimal') {
useTemporaryCache = true;
addLog(`💾 最小内存模式:所有层数据使用临时缓存`);
} else if (selectedMemoryMode === 'normal') {
useTemporaryCache = false;
addLog(`💾 标准模式:所有层数据使用IndexedDB存储`);
} else if (selectedMemoryMode === 'stream') {
useTemporaryCache = totalSize > 200 * 1024 * 1024; // 200MB阈值
if (useTemporaryCache) {
addLog(`🌊 流式模式:镜像较大 (${formatSize(totalSize)}),使用临时缓存避免IndexedDB限制`);
} else {
addLog(`🌊 流式模式:镜像较小 (${formatSize(totalSize)}),使用IndexedDB存储`);
}
} else if (selectedMemoryMode === 'auto') {
// 自动模式根据镜像总大小智能选择
if (totalSize > 1024 * 1024 * 1024) { // 1GB+
useTemporaryCache = true;
addLog(`🤖 自动模式:检测到超大镜像 (${formatSize(totalSize)}),自动选择临时缓存模式避免OOM`);
} else if (totalSize > 500 * 1024 * 1024) { // 500MB+
useTemporaryCache = true;
addLog(`🤖 自动模式:检测到大镜像 (${formatSize(totalSize)}),自动选择临时缓存模式`);
} else {
useTemporaryCache = false;
addLog(`🤖 自动模式:检测到中小镜像 (${formatSize(totalSize)}),自动选择标准存储模式`);
}
}
// 额外的智能提示
if (totalSize > 2 * 1024 * 1024 * 1024) { // 2GB+
addLog(`⚠️ 超大镜像警告: ${formatSize(totalSize)} - 建议使用最小内存模式,确保设备有足够内存`);
} else if (totalSize > 1024 * 1024 * 1024) { // 1GB+
addLog(`⚠️ 大镜像提示: ${formatSize(totalSize)} - 下载可能耗时较长,请保持网络连接稳定`);
}
}
/**
* 启动实时进度更新
* 功能:定期更新下载进度显示,提供流畅的用户体验
*/
function startRealTimeProgressUpdate() {
if (progressUpdateInterval) {
clearInterval(progressUpdateInterval);
}
progressUpdateInterval = setInterval(() => {
updateRealTimeProgress();
}, 500); // 每500ms更新一次进度
}
/**
* 停止实时进度更新
* 功能:清理进度更新定时器
*/
function stopRealTimeProgressUpdate() {
if (progressUpdateInterval) {
clearInterval(progressUpdateInterval);
progressUpdateInterval = null;
}
}
/**
* 更新实时进度显示
* 功能:计算并显示当前的总体下载进度
*/
function updateRealTimeProgress() {
if (!manifestData || downloadProgressMap.size === 0) return;
let totalDownloaded = 0;
let totalSpeed = 0;
let completedLayers = 0;
// 统计所有层的下载进度
for (const [digest, progressInfo] of downloadProgressMap.entries()) {
totalDownloaded += progressInfo.downloaded;
totalSpeed += progressInfo.speed;
if (progressInfo.completed) {
completedLayers++;
}
}
const totalSize = manifestData.totalSize;
const progress = totalSize > 0 ? Math.min(Math.round((totalDownloaded / totalSize) * 100), 100) : 0;
// 更新按钮显示
updateButtonProgress('⬇️ 下载镜像层', 'downloading', {
progress: progress,
current: completedLayers,
total: manifestData.layers.length,
speed: totalSpeed
});
}
/**
* 格式化文件大小显示
* 功能:将字节数转换为人类可读的格式(B, KB, MB, GB)
* @param {number} bytes - 要格式化的字节数
* @returns {string} 格式化后的大小字符串
*/
function formatSize(bytes) {
const sizes = ['B', 'KB', 'MB', 'GB'];
if (bytes === 0) return '0 B';
const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
}
/**
* 格式化下载速度
* 功能:将字节/秒转换为可读的速度单位
* @param {number} bytesPerSecond - 字节/秒
* @returns {string} 格式化的速度字符串
*/
function formatSpeed(bytesPerSecond) {
if (bytesPerSecond === 0) return '0 B/s';
const sizes = ['B/s', 'KB/s', 'MB/s', 'GB/s'];
const i = parseInt(Math.floor(Math.log(bytesPerSecond) / Math.log(1024)));
return Math.round(bytesPerSecond / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
}
/**
* 创建镜像信息展示区域
* 功能:创建类似download.html的镜像信息展示卡片
* @returns {HTMLElement} 镜像信息展示DOM元素
*/
function createImageInfoDisplay() {
const infoContainer = document.createElement('div');
infoContainer.id = 'dockerImageInfo';
infoContainer.className = 'docker-image-info';
infoContainer.style.cssText = `
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 12px;
margin-top: 8px;
font-size: 12px;
display: none;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
max-width: 500px;
`;
infoContainer.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<h4 style="margin: 0; color: #495057; font-size: 14px;">📋 镜像信息</h4>
<button id="toggleInfoBtn" style="background: none; border: none; cursor: pointer; color: #6c757d; font-size: 12px;">▼</button>
</div>
<div id="infoContent" style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px;">
<div class="info-item">
<div class="info-label" style="color: #6c757d; font-weight: 600; margin-bottom: 2px;">镜像名称</div>
<div class="info-value" id="displayImageName" style="color: #495057; font-family: monospace; font-size: 11px;">-</div>
</div>
<div class="info-item">
<div class="info-label" style="color: #6c757d; font-weight: 600; margin-bottom: 2px;">当前架构</div>
<div class="info-value" id="displayArchitecture" style="color: #495057; font-family: monospace;">-</div>
</div>
<div class="info-item">
<div class="info-label" style="color: #6c757d; font-weight: 600; margin-bottom: 2px;">总大小</div>
<div class="info-value" id="displayTotalSize" style="color: #495057;">-</div>
</div>
<div class="info-item">
<div class="info-label" style="color: #6c757d; font-weight: 600; margin-bottom: 2px;">层数量</div>
<div class="info-value" id="displayLayerCount" style="color: #495057;">-</div>
</div>
<div class="info-item" style="grid-column: span 2;">
<div class="info-label" style="color: #6c757d; font-weight: 600; margin-bottom: 2px;">下载进度</div>
<div id="displayProgress" style="color: #495057;">
<div style="background: #e9ecef; border-radius: 10px; height: 6px; margin: 4px 0;">
<div id="progressBar" style="background: #007bff; height: 100%; border-radius: 10px; width: 0%; transition: width 0.3s ease;"></div>
</div>
<div id="progressText" style="font-size: 11px; color: #6c757d;">准备中...</div>
</div>
</div>
<div class="info-item" style="grid-column: span 2;">
<div class="info-label" style="color: #6c757d; font-weight: 600; margin-bottom: 2px;">可用架构</div>
<div id="displayAvailableArchs" style="color: #495057; font-size: 11px;">检测中...</div>
</div>
</div>
`;
// 添加折叠/展开功能
const toggleBtn = infoContainer.querySelector('#toggleInfoBtn');
const content = infoContainer.querySelector('#infoContent');
toggleBtn.addEventListener('click', () => {
if (content.style.display === 'none') {
content.style.display = 'grid';
toggleBtn.textContent = '▼';
} else {
content.style.display = 'none';
toggleBtn.textContent = '▶';
}
});
return infoContainer;
}
/**
* 更新镜像信息展示
* 功能:更新信息展示区域的各项数据
* @param {Object} info - 镜像信息对象
*/
function updateImageInfoDisplay(info) {
const infoContainer = document.getElementById('dockerImageInfo');
if (!infoContainer) return;
// 显示信息容器
infoContainer.style.display = 'block';
// 更新各项信息
if (info.imageName) {
document.getElementById('displayImageName').textContent = info.imageName;
}
if (info.architecture) {
document.getElementById('displayArchitecture').textContent = info.architecture;
}
if (info.totalSize) {
document.getElementById('displayTotalSize').textContent = formatSize(info.totalSize);
}
if (info.layerCount !== undefined) {
document.getElementById('displayLayerCount').textContent = info.layerCount;
}
if (info.availableArchs) {
const archsElement = document.getElementById('displayAvailableArchs');
if (info.availableArchs.length > 0) {
archsElement.innerHTML = info.availableArchs.map(arch =>
`<span style="background: #e3f2fd; padding: 2px 6px; border-radius: 3px; margin: 1px; display: inline-block; font-family: monospace;">${arch}</span>`
).join('');
} else {
archsElement.textContent = '获取中...';
}
}
}
/**
* 更新下载进度展示
* 功能:更新进度条和进度文本
* @param {number} percent - 进度百分比
* @param {number} speed - 下载速度(字节/秒)
*/
function updateProgressDisplay(percent, speed = 0) {
const progressBar = document.getElementById('progressBar');
const progressText = document.getElementById('progressText');
if (progressBar && progressText) {
progressBar.style.width = `${percent}%`;
let statusText = '';
if (percent === 0) {
statusText = '准备中...';
progressBar.style.background = '#6c757d';
} else if (percent < 100) {
statusText = `下载中 ${percent}%`;
if (speed > 0) {
statusText += ` (${formatSpeed(speed)})`;
}
progressBar.style.background = '#007bff';
} else {
statusText = '下载完成';
progressBar.style.background = '#28a745';
}
progressText.textContent = statusText;
}
}
// ==================== 远程API访问功能 ====================
/**
* 通过远程API分析镜像信息
* 功能:调用远程API获取镜像的层级结构和元数据
* @param {string} imageName - 镜像名称
* @param {string} imageTag - 镜像标签
* @param {string} architecture - 目标架构(可选)
* @returns {Promise<Object>} 镜像清单数据的Promise
*/
async function analyzeImageRemote(imageName, imageTag, architecture = '') {
addLog(`[远程API] 开始分析镜像: ${imageName}:${imageTag}`);
// 构建API请求URL
let apiUrl = `${API_BASE_URL}/manifest?image=${encodeURIComponent(imageName)}&tag=${encodeURIComponent(imageTag)}`;
if (architecture) {
apiUrl += `&architecture=${encodeURIComponent(architecture)}`;
addLog(`[远程API] 指定架构: ${architecture}`);
}
try {
const response = await gmFetch(apiUrl);
if (!response.ok) {
throw new Error(`获取镜像信息失败: ${response.status}`);
}
const data = await response.json();
addLog(`[远程API] 镜像分析完成,共 ${data.layerCount} 层,总大小 ${formatSize(data.totalSize)}`);
return data;
} catch (error) {
addLog(`[远程API] 分析失败: ${error.message}`, 'error');
throw error;
}
}
/**
* 通过远程API检测镜像支持的架构
* 功能:获取镜像的所有可用架构平台信息
* @param {string} imageName - 镜像名称
* @param {string} imageTag - 镜像标签
* @returns {Promise<Array>} 可用架构列表的Promise
*/
async function detectArchitecturesRemote(imageName, imageTag) {
addLog(`[远程API] 开始检测镜像架构: ${imageName}:${imageTag}`);
try {
const response = await gmFetch(`${API_BASE_URL}/manifest?image=${encodeURIComponent(imageName)}&tag=${encodeURIComponent(imageTag)}`);
if (!response.ok) {
throw new Error(`获取镜像信息失败: ${response.status}`);
}
const data = await response.json();
if (data.multiArch && data.availablePlatforms) {
addLog(`[远程API] 检测完成,发现 ${data.availablePlatforms.length} 种架构`);
return data.availablePlatforms;
} else {
addLog('[远程API] 当前镜像仅支持单一架构');
return [];
}
} catch (error) {
addLog(`[远程API] 架构检测失败: ${error.message}`, 'error');
throw error;
}
}
/**
* 通过远程API下载单个镜像层
* 功能:从远程API下载指定的镜像层数据并存储到本地缓存
* @param {Object} layer - 层信息对象,包含digest、type、size等
* @param {string} fullImageName - 完整的镜像名称
* @returns {Promise} 下载完成的Promise
*/
async function downloadLayerRemote(layer, fullImageName) {
const layerIndex = manifestData.layers.indexOf(layer);
try {
addLog(`[远程API] 开始下载层: ${layer.digest.substring(0, 12)}... (类型: ${layer.type}, 大小: ${formatSize(layer.size || 0)}, 索引: ${layerIndex})`);
// 初始化进度跟踪
downloadProgressMap.set(layer.digest, {
downloaded: 0,
total: layer.size || 0,
speed: 0,
completed: false,
startTime: Date.now()
});
// 根据层类型构建不同的API端点URL
let apiEndpoint;
if (layer.type === 'config') {
// 配置层的下载端点
apiEndpoint = `${API_BASE_URL}/config?image=${encodeURIComponent(fullImageName)}&digest=${encodeURIComponent(layer.digest)}`;
} else {
// 普通镜像层的下载端点
apiEndpoint = `${API_BASE_URL}/layer?image=${encodeURIComponent(fullImageName)}&digest=${encodeURIComponent(layer.digest)}`;
}
// 发起HTTP下载请求
const response = await gmFetch(apiEndpoint, { stream: true, responseType: 'arraybuffer' });
if (!response.ok) {
throw new Error(`下载层失败: ${response.status}`);
}
// 使用流式读取来支持实时进度更新
const reader = response.body.getReader();
const chunks = [];
let receivedLength = 0;
const progressInfo = downloadProgressMap.get(layer.digest);
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
receivedLength += value.length;
// 更新进度信息
const now = Date.now();
const elapsed = (now - progressInfo.startTime) / 1000; // 秒
progressInfo.downloaded = receivedLength;
progressInfo.speed = elapsed > 0 ? receivedLength / elapsed : 0;
downloadProgressMap.set(layer.digest, progressInfo);
}
// 组装完整的数据
const arrayBuffer = new Uint8Array(receivedLength);
let position = 0;
for (const chunk of chunks) {
arrayBuffer.set(chunk, position);
position += chunk.length;
}
// 存储层数据
if (useTemporaryCache) {
// 使用临时缓存(minimal模式)
tempLayerCache.set(layer.digest, arrayBuffer.buffer);
addLog(`[远程API] 层数据存储到临时缓存: ${layer.digest.substring(0, 12)}... (${formatSize(arrayBuffer.byteLength)})`);
} else {
// 使用IndexedDB存储
await storeLayerData(layer.digest, arrayBuffer.buffer);
addLog(`[远程API] 层数据存储到IndexedDB: ${layer.digest.substring(0, 12)}... (${formatSize(arrayBuffer.byteLength)})`);
}
downloadedLayers.set(layer.digest, true);
// 标记为完成
progressInfo.completed = true;
downloadProgressMap.set(layer.digest, progressInfo);
addLog(`[远程API] 层下载完成: ${layer.digest.substring(0, 12)}... (${formatSize(arrayBuffer.byteLength)})`);
} catch (error) {
addLog(`[远程API] 层下载失败: ${layer.digest.substring(0, 12)}... - ${error.message}`, 'error');
// 清理进度信息
downloadProgressMap.delete(layer.digest);
throw error;
}
}
// ==================== Docker Registry 直接访问功能 ====================
/**
* 获取 Docker Token 用于认证
* 功能:从 Docker Hub 认证服务获取访问 token
* @param {string} imageName - 镜像名称
* @param {string} registryHost - 注册表主机地址
* @returns {Promise<string|null>} 认证 token
*/
async function getDockerToken(imageName, registryHost = null) {
try {
const targetRegistry = registryHost || hub_host;
if (targetRegistry === 'ghcr.io') {
return "QQ==";
} else if (targetRegistry === 'gcr.io') {
return null;
} else if (targetRegistry === 'quay.io') {
return null;
} else if (targetRegistry === 'registry.k8s.io') {
return null;
} else if (targetRegistry === 'registry-1.docker.io' || targetRegistry.includes('docker.io')) {
const tokenUrl = `${auth_url}/token?service=registry.docker.io&scope=repository:${imageName}:pull`;
const response = await gmFetch(tokenUrl, {
headers: {
'User-Agent': 'Docker-Client/19.03.12'
}
});
if (!response.ok) {
return null;
}
const data = await response.json();
return data.token || null;
} else {
return null;
}
} catch (error) {
addLog(`获取 Docker Token 失败: ${error.message}`, 'error');
return null;
}
}
/**
* 解析镜像清单层信息(Schema Version 2)
* 功能:解析标准的 Docker 镜像清单,提取层信息
* @param {Object} manifest - 镜像清单对象
* @param {string} imageName - 镜像名称
* @returns {Object} 解析后的层信息
*/
function parseManifestLayers(manifest, imageName) {
const layers = [];
let totalSize = 0;
// 添加配置层
if (manifest.config) {
const configSize = manifest.config.size || 0;
layers.push({
type: 'config',
digest: manifest.config.digest,
size: configSize,
mediaType: manifest.config.mediaType
});
totalSize += configSize;
}
// 添加镜像层
if (manifest.layers && Array.isArray(manifest.layers) && manifest.layers.length > 0) {
manifest.layers.forEach((layer, index) => {
if (!layer.digest) {
return;
}
const layerSize = layer.size || 0;
layers.push({
type: 'layer',
digest: layer.digest,
size: layerSize,
mediaType: layer.mediaType || 'application/vnd.docker.image.rootfs.diff.tar.gzip',
index: index
});
totalSize += layerSize;
});
}
return {
imageName,
manifest,
layers,
totalSize,
layerCount: layers.length
};
}
/**
* 解析 Schema Version 1 镜像清单
* 功能:处理旧版本的 Docker 镜像清单格式
* @param {Object} manifest - 镜像清单对象
* @param {string} imageName - 镜像名称
* @returns {Object} 解析后的层信息
*/
function parseManifestV1Layers(manifest, imageName) {
const layers = [];
let totalSize = 0;
if (manifest.fsLayers && Array.isArray(manifest.fsLayers)) {
manifest.fsLayers.forEach((layer, index) => {
layers.push({
type: 'layer',
digest: layer.blobSum,
size: 0, // V1 格式通常不包含大小信息
mediaType: 'application/vnd.docker.image.rootfs.diff.tar.gzip',
index: index
});
});
}
return {
imageName,
manifest,
layers,
totalSize,
layerCount: layers.length
};
}
/**
* 解析 Docker 镜像清单
* 功能:直接从 Docker Registry 获取并解析镜像清单
* @param {string} imageName - 镜像名称
* @param {string} tag - 镜像标签
* @param {string} token - 认证 token
* @param {string} registryHost - 注册表主机
* @param {string} architecture - 目标架构
* @returns {Promise<Object>} 解析后的镜像清单数据
*/
async function parseDockerManifest(imageName, tag, token, registryHost = null, architecture = null) {
try {
const targetRegistry = registryHost || hub_host;
const manifestUrl = `https://${targetRegistry}/v2/${imageName}/manifests/${tag}`;
const headers = {
'User-Agent': 'Docker-Client/19.03.12'
};
// 根据不同的注册表设置合适的 Accept 头
if (targetRegistry === 'ghcr.io') {
headers['Accept'] = 'application/vnd.oci.image.index.v1+json, application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json, application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.docker.distribution.manifest.v1+prettyjws';
} else if (targetRegistry === 'gcr.io') {
headers['Accept'] = 'application/vnd.docker.distribution.manifest.v2+json, application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.oci.image.manifest.v1+json, application/vnd.oci.image.index.v1+json';
} else if (targetRegistry === 'quay.io') {
headers['Accept'] = 'application/vnd.docker.distribution.manifest.v2+json, application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.oci.image.manifest.v1+json, application/vnd.oci.image.index.v1+json';
} else if (targetRegistry === 'registry.k8s.io') {
headers['Accept'] = 'application/vnd.docker.distribution.manifest.v2+json, application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.oci.image.manifest.v1+json, application/vnd.oci.image.index.v1+json';
} else {
headers['Accept'] = 'application/vnd.docker.distribution.manifest.v2+json, application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.oci.image.manifest.v1+json, application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.v1+prettyjws';
}
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const response = await gmFetch(manifestUrl, { headers });
if (!response.ok) {
const errorText = await response.text();
// 如果是401错误且没有token,尝试获取token
if (response.status === 401 && !token) {
const newToken = await getDockerToken(imageName, targetRegistry);
if (newToken) {
return await parseDockerManifest(imageName, tag, newToken, targetRegistry, architecture);
}
}
throw new Error(`获取manifest失败: ${response.status} - ${errorText}`);
}
const contentType = response.headers.get('Content-Type');
const manifest = await response.json();
const mediaType = manifest.mediaType || contentType;
// 处理多架构镜像
if (mediaType === 'application/vnd.docker.distribution.manifest.list.v2+json' ||
mediaType === 'application/vnd.oci.image.index.v1+json') {
if (!manifest.manifests || !Array.isArray(manifest.manifests)) {
throw new Error('多架构镜像格式错误:缺少manifests数组');
}
addLog(`发现多架构镜像,包含 ${manifest.manifests.length} 个架构`);
// 提取可用平台信息
const availablePlatforms = manifest.manifests
.filter(m => m.platform && m.platform.architecture && m.platform.os)
.map(m => ({
architecture: m.platform.architecture,
os: m.platform.os,
variant: m.platform.variant || null,
digest: m.digest,
mediaType: m.mediaType,
platform: `${m.platform.os}/${m.platform.architecture}${m.platform.variant ? '/' + m.platform.variant : ''}`
}))
.filter(p => p.os !== 'unknown' && p.architecture !== 'unknown');
addLog(`找到 ${availablePlatforms.length} 个有效架构`);
// 选择合适的架构
let selectedManifest = null;
if (architecture) {
const [targetOS, targetArch, targetVariant] = architecture.split('/');
selectedManifest = manifest.manifests.find(m => {
if (!m.platform || !m.platform.architecture || !m.platform.os) return false;
if (m.platform.os === 'unknown' || m.platform.architecture === 'unknown') return false;
return m.platform.os === (targetOS || 'linux') &&
m.platform.architecture === targetArch &&
(m.platform.variant || '') === (targetVariant || '');
});
}
// 如果没有找到指定架构,尝试默认架构
if (!selectedManifest) {
selectedManifest = manifest.manifests.find(m =>
m.platform && m.platform.architecture === 'amd64' && m.platform.os === 'linux'
);
if (!selectedManifest) {
selectedManifest = manifest.manifests.find(m =>
m.platform && m.platform.architecture && m.platform.os &&
m.platform.os !== 'unknown' && m.platform.architecture !== 'unknown'
);
}
if (!selectedManifest && manifest.manifests.length > 0) {
selectedManifest = manifest.manifests[0];
}
}
if (!selectedManifest) {
throw new Error('未找到合适的平台镜像');
}
// 获取特定架构的清单
const specificManifestUrl = `https://${targetRegistry}/v2/${imageName}/manifests/${selectedManifest.digest}`;
const specificResponse = await gmFetch(specificManifestUrl, { headers });
if (!specificResponse.ok) {
throw new Error(`获取特定架构manifest失败: ${specificResponse.status}`);
}
const specificManifest = await specificResponse.json();
const result = parseManifestLayers(specificManifest, imageName);
result.multiArch = true;
result.availablePlatforms = availablePlatforms;
result.selectedPlatform = selectedManifest.platform;
return result;
}
// 处理 Schema Version 1
if (manifest.schemaVersion === 1 ||
mediaType === 'application/vnd.docker.distribution.manifest.v1+prettyjws') {
const result = parseManifestV1Layers(manifest, imageName);
result.multiArch = false;
result.availablePlatforms = [];
result.selectedPlatform = { architecture: 'amd64', os: 'linux' };
return result;
}
// 处理 Schema Version 2 和 OCI 格式
if (manifest.schemaVersion === 2 ||
mediaType === 'application/vnd.docker.distribution.manifest.v2+json' ||
mediaType === 'application/vnd.oci.image.manifest.v1+json') {
const result = parseManifestLayers(manifest, imageName);
result.multiArch = false;
result.availablePlatforms = [];
result.selectedPlatform = { architecture: 'amd64', os: 'linux' };
return result;
}
// 默认处理
const result = parseManifestLayers(manifest, imageName);
result.multiArch = false;
result.availablePlatforms = [];
result.selectedPlatform = { architecture: 'amd64', os: 'linux' };
return result;
} catch (error) {
addLog(`解析 Docker 清单失败: ${error.message}`, 'error');
throw error;
}
}
/**
* 分析镜像信息,获取清单数据(统一接口)
* 功能:根据配置的模式选择使用远程API或直接访问Registry
* @param {string} imageName - 镜像名称
* @param {string} imageTag - 镜像标签
* @param {string} architecture - 目标架构(可选)
* @returns {Promise<Object>} 镜像清单数据的Promise
*/
async function analyzeImage(imageName, imageTag, architecture = '') {
addLog(`[${DOWNLOAD_MODES[downloadMode]}] 开始分析镜像: ${imageName}:${imageTag}`);
if (downloadMode === 'remote') {
return await analyzeImageRemote(imageName, imageTag, architecture);
} else {
return await analyzeImageDirect(imageName, imageTag, architecture);
}
}
/**
* 直接从 Docker Registry 分析镜像信息
* 功能:直接从 Docker Registry 获取镜像的层级结构和元数据
* @param {string} imageName - 镜像名称
* @param {string} imageTag - 镜像标签
* @param {string} architecture - 目标架构(可选)
* @returns {Promise<Object>} 镜像清单数据的Promise
*/
async function analyzeImageDirect(imageName, imageTag, architecture = '') {
addLog(`[直接访问] 开始分析镜像: ${imageName}:${imageTag}`);
try {
// 处理镜像名称和目标注册表
let processedImageName = imageName;
let targetRegistry = hub_host;
// 检测是否为第三方注册表
if (imageName.includes('.')) {
const parts = imageName.split('/');
const firstPart = parts[0];
const registryMap = {
'ghcr.io': 'ghcr.io',
'gcr.io': 'gcr.io',
'quay.io': 'quay.io',
'registry.k8s.io': 'registry.k8s.io',
'k8s.gcr.io': 'k8s.gcr.io'
};
if (registryMap[firstPart]) {
targetRegistry = registryMap[firstPart];
processedImageName = parts.slice(1).join('/');
}
}
// 对于 Docker Hub,为官方镜像添加 library 前缀
if (targetRegistry === hub_host) {
if (!processedImageName.includes('/') && !processedImageName.includes('@') && !processedImageName.includes(':')) {
processedImageName = 'library/' + processedImageName;
}
}
addLog(`[直接访问] 目标注册表: ${targetRegistry}, 处理后镜像名: ${processedImageName}`);
if (architecture) {
addLog(`[直接访问] 指定架构: ${architecture}`);
}
// 获取认证token(如果需要)
let token = null;
try {
const manifestData = await parseDockerManifest(processedImageName, imageTag, null, targetRegistry, architecture);
addLog(`[直接访问] 镜像分析完成,共 ${manifestData.layerCount} 层,总大小 ${formatSize(manifestData.totalSize)}`);
return manifestData;
} catch (anonymousError) {
// 如果匿名访问失败,尝试获取 token
addLog('[直接访问] 匿名访问失败,尝试获取认证token...');
token = await getDockerToken(processedImageName, targetRegistry);
if (token) {
addLog('[直接访问] 已获取认证token,重新尝试...');
const manifestData = await parseDockerManifest(processedImageName, imageTag, token, targetRegistry, architecture);
addLog(`[直接访问] 镜像分析完成,共 ${manifestData.layerCount} 层,总大小 ${formatSize(manifestData.totalSize)}`);
return manifestData;
} else {
throw anonymousError;
}
}
} catch (error) {
addLog(`[直接访问] 分析失败: ${error.message}`, 'error');
throw error;
}
}
/**
* 检测镜像支持的架构(统一接口)
* 功能:根据配置的模式选择使用远程API或直接访问Registry
* @param {string} imageName - 镜像名称
* @param {string} imageTag - 镜像标签
* @returns {Promise<Array>} 可用架构列表的Promise
*/
async function detectArchitectures(imageName, imageTag) {
addLog(`[${DOWNLOAD_MODES[downloadMode]}] 开始检测镜像架构: ${imageName}:${imageTag}`);
if (downloadMode === 'remote') {
return await detectArchitecturesRemote(imageName, imageTag);
} else {
const manifestData = await analyzeImageDirect(imageName, imageTag, '');
if (manifestData.multiArch && manifestData.availablePlatforms) {
addLog(`[直接访问] 检测完成,发现 ${manifestData.availablePlatforms.length} 种架构`);
return manifestData.availablePlatforms;
} else {
addLog('[直接访问] 当前镜像仅支持单一架构');
return [];
}
}
}
// ==================== 下载功能 ====================
/**
* 下载单个镜像层(统一接口,支持实时进度更新)
* 功能:根据配置的模式选择使用远程API或直接从Registry下载
* @param {Object} layer - 层信息对象,包含digest、type、size等
* @param {string} fullImageName - 完整的镜像名称
* @param {string} targetRegistry - 目标注册表地址(仅直接模式使用)
* @param {string} token - 认证token(仅直接模式使用)
* @returns {Promise} 下载完成的Promise
*/
async function downloadLayer(layer, fullImageName, targetRegistry, token) {
const layerIndex = manifestData.layers.indexOf(layer);
if (downloadMode === 'remote') {
return await downloadLayerRemote(layer, fullImageName);
} else {
return await downloadLayerDirect(layer, fullImageName, targetRegistry, token);
}
}
/**
* 直接从 Docker Registry 下载单个镜像层
* 功能:直接从 Docker Registry 下载指定的镜像层数据并存储到本地缓存,支持实时进度反馈
* @param {Object} layer - 层信息对象,包含digest、type、size等
* @param {string} fullImageName - 完整的镜像名称
* @param {string} targetRegistry - 目标注册表地址
* @param {string} token - 认证token
* @returns {Promise} 下载完成的Promise
*/
async function downloadLayerDirect(layer, fullImageName, targetRegistry, token) {
const layerIndex = manifestData.layers.indexOf(layer);
try {
addLog(`[直接访问] 开始下载层: ${layer.digest.substring(0, 12)}... (类型: ${layer.type}, 大小: ${formatSize(layer.size || 0)}, 索引: ${layerIndex})`);
// 初始化进度跟踪
downloadProgressMap.set(layer.digest, {
downloaded: 0,
total: layer.size || 0,
speed: 0,
completed: false,
startTime: Date.now()
});
// 构建 Docker Registry blob 下载 URL
const blobUrl = `https://${targetRegistry}/v2/${fullImageName}/blobs/${layer.digest}`;
// 设置请求头
const headers = {
'Accept': 'application/vnd.docker.image.rootfs.diff.tar.gzip, application/vnd.docker.container.image.v1+json, */*',
'User-Agent': 'Docker-Client/19.03.12'
};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
// 发起HTTP下载请求
const response = await gmFetch(blobUrl, { headers, stream: true, responseType: 'arraybuffer' });
if (!response.ok) {
const errorText = await response.text();
throw new Error(`下载层失败: ${response.status} - ${errorText}`);
}
// 使用流式读取来支持实时进度更新
const reader = response.body.getReader();
const chunks = [];
let receivedLength = 0;
const progressInfo = downloadProgressMap.get(layer.digest);
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
receivedLength += value.length;
// 更新进度信息
const now = Date.now();
const elapsed = (now - progressInfo.startTime) / 1000; // 秒
progressInfo.downloaded = receivedLength;
progressInfo.speed = elapsed > 0 ? receivedLength / elapsed : 0;
downloadProgressMap.set(layer.digest, progressInfo);
}
// 组装完整的数据
const arrayBuffer = new Uint8Array(receivedLength);
let position = 0;
for (const chunk of chunks) {
arrayBuffer.set(chunk, position);
position += chunk.length;
}
// 存储层数据
if (useTemporaryCache) {
// 使用临时缓存(minimal模式)
tempLayerCache.set(layer.digest, arrayBuffer.buffer);
addLog(`[直接访问] 层数据存储到临时缓存: ${layer.digest.substring(0, 12)}... (${formatSize(arrayBuffer.byteLength)})`);
} else {
// 使用IndexedDB存储
await storeLayerData(layer.digest, arrayBuffer.buffer);
addLog(`[直接访问] 层数据存储到IndexedDB: ${layer.digest.substring(0, 12)}... (${formatSize(arrayBuffer.byteLength)})`);
}
downloadedLayers.set(layer.digest, true);
// 标记为完成
progressInfo.completed = true;
downloadProgressMap.set(layer.digest, progressInfo);
addLog(`[直接访问] 层下载完成: ${layer.digest.substring(0, 12)}... (${formatSize(arrayBuffer.byteLength)})`);
} catch (error) {
addLog(`[直接访问] 层下载失败: ${layer.digest.substring(0, 12)}... - ${error.message}`, 'error');
// 清理进度信息
downloadProgressMap.delete(layer.digest);
throw error;
}
}
/**
* 存储层数据到IndexedDB
* 功能:将下载的层数据存储到IndexedDB数据库
* @param {string} digest - 层摘要
* @param {ArrayBuffer} data - 层数据
* @returns {Promise} 存储完成的Promise
*/
async function storeLayerData(digest, data) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['layers'], 'readwrite');
const store = transaction.objectStore('layers');
const layerRecord = {
digest: digest,
data: data,
timestamp: Date.now()
};
const request = store.put(layerRecord);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
/**
* 从存储中获取层数据
* 功能:优先从临时缓存获取,然后从IndexedDB获取
* @param {string} digest - 层摘要
* @returns {Promise<ArrayBuffer>} 层数据
*/
async function getLayerData(digest) {
// 首先检查临时缓存
if (tempLayerCache.has(digest)) {
return tempLayerCache.get(digest);
}
// 然后检查IndexedDB
return new Promise((resolve, reject) => {
const transaction = db.transaction(['layers'], 'readonly');
const store = transaction.objectStore('layers');
const request = store.get(digest);
request.onsuccess = () => {
if (request.result) {
resolve(request.result.data);
} else {
reject(new Error('未找到层数据: ' + digest));
}
};
request.onerror = () => reject(request.error);
});
}
// ==================== 文件生成功能 ====================
/**
* 生成下载文件名
* 功能:根据镜像名称、标签和架构生成规范的文件名
* @param {string} imageName - 镜像名称
* @param {string} imageTag - 镜像标签
* @param {string} architecture - 架构信息
* @returns {string} 生成的TAR文件名
*/
function generateFilename(imageName, imageTag, architecture) {
// 处理镜像名称,移除Docker Hub的library前缀
let cleanImageName = imageName;
if (cleanImageName.startsWith('library/')) {
cleanImageName = cleanImageName.substring(8);
}
// 替换文件名中的特殊字符为安全字符
cleanImageName = cleanImageName.replace(/[\/\\:]/g, '_').replace(/[<>:"|?*]/g, '-');
const cleanTag = imageTag.replace(/[\/\\:]/g, '_').replace(/[<>:"|?*]/g, '-');
const cleanArch = architecture.replace(/[\/\\:]/g, '_').replace(/[<>:"|?*]/g, '-') || 'amd64';
// 返回格式:imagename_tag_architecture.tar
return `${cleanImageName}_${cleanTag}_${cleanArch}.tar`;
}
/**
* 生成随机摘要值
* 功能:当镜像配置不可用时生成伪随机SHA256摘要
* @returns {string} 64位十六进制摘要字符串
*/
function generateFakeDigest() {
const chars = '0123456789abcdef';
let result = '';
for (let i = 0; i < 64; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
/**
* 使用tarballjs创建Docker TAR文件
* 功能:将所有下载的层组装成符合Docker标准的TAR格式文件
* @param {Map} layerDataMap - 层数据映射表
* @param {string} filename - 输出文件名
* @returns {Promise} 创建完成的Promise
*/
async function createDockerTar(layerDataMap, filename) {
addLog('开始创建Docker TAR文件...');
// 检查tarballjs库是否可用
if (!window.tarball || !window.tarball.TarWriter) {
throw new Error('tarballjs库未加载,无法创建TAR文件');
}
try {
const tar = new tarball.TarWriter();
// 第一步:处理镜像配置文件
const manifest = manifestData.manifest;
let configDigest = null;
let configData = null;
// 尝试从manifest中获取配置摘要
if (manifest.config && manifest.config.digest) {
configDigest = manifest.config.digest;
const rawConfigData = layerDataMap.get(configDigest);
if (rawConfigData) {
configData = new Uint8Array(rawConfigData);
addLog(`配置数据准备完成,大小: ${configData.length} 字节`);
}
}
// 如果没有配置数据,创建默认配置
if (!configData) {
configDigest = 'sha256:' + generateFakeDigest();
const configObj = {
architecture: "amd64",
os: "linux",
config: {},
rootfs: {
type: "layers",
diff_ids: manifestData.layers
.filter(l => l.type === 'layer')
.map(l => l.digest)
}
};
configData = new TextEncoder().encode(JSON.stringify(configObj));
addLog(`生成默认配置,大小: ${configData.length} 字节`);
}
// 第二步:添加配置文件到TAR
const configFileName = configDigest + '.json';
const configBlob = new Blob([configData], { type: 'application/json' });
const configFile = new File([configBlob], configFileName);
tar.addFile(configFileName, configFile);
addLog(`添加配置文件: ${configFileName}`);
// 第三步:添加所有镜像层到TAR
const layerDigests = [];
let layerIndex = 0;
for (const layer of manifestData.layers) {
if (layer.type === 'layer' && layer.digest) {
const layerDigest = layer.digest;
layerDigests.push(layerDigest);
const layerData = layerDataMap.has(layerDigest) ? layerDataMap.get(layerDigest) : await getLayerData(layerDigest);
if (layerData) {
// 每个层创建独立目录结构: digest/layer.tar
const layerFileName = layerDigest + '/layer.tar';
const layerUint8Array = new Uint8Array(layerData);
const layerBlob = new Blob([layerUint8Array], { type: 'application/octet-stream' });
const layerFile = new File([layerBlob], 'layer.tar');
tar.addFile(layerFileName, layerFile);
addLog(`添加层文件 ${layerIndex + 1}/${manifestData.layers.filter(l => l.type === 'layer').length}: ${layerFileName}`);
layerIndex++;
}
}
}
// 第四步:创建Docker manifest.json文件
let repoTag = manifestData.imageName;
if (repoTag.startsWith('library/')) {
repoTag = repoTag.substring(8);
}
if (!repoTag.includes(':')) {
repoTag += ':latest';
}
const dockerManifest = [{
Config: configFileName,
RepoTags: [repoTag],
Layers: layerDigests.map(digest => digest + '/layer.tar')
}];
const manifestBlob = new Blob([JSON.stringify(dockerManifest)], { type: 'application/json' });
const manifestFile = new File([manifestBlob], 'manifest.json');
tar.addFile('manifest.json', manifestFile);
addLog('添加manifest.json文件');
// 第五步:创建repositories文件
const repositories = {};
let repoName, tag;
if (manifestData.imageName.includes(':')) {
const parts = manifestData.imageName.split(':');
repoName = parts[0];
tag = parts[1];
} else {
repoName = manifestData.imageName;
tag = 'latest';
}
if (repoName.startsWith('library/')) {
repoName = repoName.substring(8);
}
repositories[repoName] = {};
repositories[repoName][tag] = configDigest.replace('sha256:', '');
const repositoriesBlob = new Blob([JSON.stringify(repositories)], { type: 'application/json' });
const repositoriesFile = new File([repositoriesBlob], 'repositories');
tar.addFile('repositories', repositoriesFile);
addLog('添加repositories文件');
// 第六步:下载生成的TAR文件
addLog('开始生成并下载TAR文件...');
tar.download(filename);
addLog(`TAR文件下载已触发: ${filename}`);
} catch (error) {
addLog(`创建TAR文件失败: ${error.message}`, 'error');
throw error;
}
}
// ==================== 完整下载流程 ====================
/**
* 执行完整的镜像下载流程
* 功能:包括分析、下载、组装的完整自动化流程
* @param {string} imageName - 镜像名称
* @param {string} imageTag - 镜像标签
* @param {string} architecture - 目标架构
*/
async function performDownload(imageName, imageTag, architecture) {
// 防止重复下载
if (downloadInProgress) {
addLog('下载正在进行中,请等待当前下载完成', 'error');
return;
}
const downloadBtn = document.getElementById('downloadBtn');
const originalText = downloadBtn.textContent;
try {
// 设置下载状态
downloadInProgress = true;
// 清理之前的下载数据
tempLayerCache.clear();
downloadedLayers.clear();
downloadProgressMap.clear();
// 第一步:分析镜像
addLog('=== 开始镜像下载流程 ===');
addLog(`镜像: ${imageName}:${imageTag}`);
if (architecture) {
addLog(`架构: ${architecture}`);
} else {
addLog('架构: 自动检测');
}
updateButtonProgress('🔍 分析镜像', 'analyzing', {
size: 0
});
manifestData = await analyzeImage(imageName, imageTag, architecture);
// 第二步:选择内存策略
chooseMemoryStrategy();
// 第三步:启动实时进度更新并开始下载
updateButtonProgress('⬇️ 下载镜像层', 'downloading', {
progress: 0,
current: 0,
total: manifestData.layers.length,
size: manifestData.totalSize
});
addLog(`开始下载 ${manifestData.layers.length} 个镜像层`);
// 启动实时进度更新
startRealTimeProgressUpdate();
// 准备下载参数(根据模式不同处理)
if (downloadMode === 'remote') {
// 远程API模式:直接使用原始镜像名
const downloadPromises = manifestData.layers.map(async (layer) => {
await downloadLayer(layer, imageName);
});
await Promise.all(downloadPromises);
} else {
// 直接访问模式:处理镜像名和注册表
let processedImageName = imageName;
let targetRegistry = hub_host;
// 检测是否为第三方注册表
if (imageName.includes('.')) {
const parts = imageName.split('/');
const firstPart = parts[0];
const registryMap = {
'ghcr.io': 'ghcr.io',
'gcr.io': 'gcr.io',
'quay.io': 'quay.io',
'registry.k8s.io': 'registry.k8s.io',
'k8s.gcr.io': 'k8s.gcr.io'
};
if (registryMap[firstPart]) {
targetRegistry = registryMap[firstPart];
processedImageName = parts.slice(1).join('/');
}
}
// 对于 Docker Hub,为官方镜像添加 library 前缀
if (targetRegistry === hub_host) {
if (!processedImageName.includes('/') && !processedImageName.includes('@') && !processedImageName.includes(':')) {
processedImageName = 'library/' + processedImageName;
}
}
// 获取认证token
const downloadToken = await getDockerToken(processedImageName, targetRegistry);
if (downloadToken) {
addLog(`[直接访问] 已获取下载认证token`);
}
// 并行下载所有层
const downloadPromises = manifestData.layers.map(async (layer) => {
await downloadLayer(layer, processedImageName, targetRegistry, downloadToken);
});
await Promise.all(downloadPromises);
}
// 停止实时进度更新
stopRealTimeProgressUpdate();
addLog('所有镜像层下载完成');
// 第四步:组装Docker TAR文件
updateButtonProgress('🔧 组装镜像', 'assembling', {
progress: 100,
current: manifestData.layers.length,
total: manifestData.layers.length,
size: manifestData.totalSize
});
addLog('开始组装Docker TAR文件');
const filename = generateFilename(imageName, imageTag, architecture || 'amd64');
// 根据存储模式传递不同的数据源
if (useTemporaryCache) {
await createDockerTar(tempLayerCache, filename);
} else {
// 对于IndexedDB模式,传递空Map,让createDockerTar使用getLayerData获取数据
await createDockerTar(new Map(), filename);
}
addLog('=== 镜像下载流程完成 ===');
updateButtonProgress('✅ 下载完成', 'complete', {
size: manifestData.totalSize
});
} catch (error) {
addLog(`下载失败: ${error.message}`, 'error');
updateButtonProgress('❌ 下载失败', 'error');
// 确保停止实时进度更新
stopRealTimeProgressUpdate();
} finally {
downloadInProgress = false;
// 清理进度数据
downloadProgressMap.clear();
}
}
// ==================== UI交互功能 ====================
/**
* 更新架构选择器选项
* 功能:根据检测到的可用架构更新下拉选择器
* @param {Array} platforms - 可用平台列表
*/
function updateArchSelector(platforms) {
const archSelector = document.getElementById('archSelector');
if (!archSelector || !platforms || platforms.length === 0) return;
// 清空现有选项
archSelector.innerHTML = '';
// 添加检测到的架构选项
platforms.forEach(platform => {
if (platform && platform.platform) {
const option = document.createElement('option');
option.value = platform.platform;
option.textContent = platform.platform;
archSelector.appendChild(option);
}
});
// 智能选择首选架构
// 优先级:linux/amd64 > linux/arm64 > 其他linux架构 > 第一个可用架构
let selectedPlatform = platforms.find(p => p.platform === 'linux/amd64') ||
platforms.find(p => p.platform === 'linux/arm64') ||
platforms.find(p => p.os === 'linux') ||
platforms[0];
if (selectedPlatform) {
archSelector.value = selectedPlatform.platform;
addLog(`自动选择架构: ${selectedPlatform.platform}`);
}
}
/**
* 绑定UI事件处理器
* 功能:为下载按钮和架构检测按钮绑定点击事件
*/
function bindEventHandlers() {
// 主下载按钮点击事件
const downloadBtn = document.getElementById('downloadBtn');
if (downloadBtn) {
downloadBtn.addEventListener('click', async () => {
const { imageName, imageTag } = extractImageInfo();
const archSelector = document.getElementById('archSelector');
let architecture = archSelector ? archSelector.value : '';
// 验证镜像信息是否提取成功
if (!imageName) {
addLog('无法获取镜像名称,请确保在正确的Docker Hub页面', 'error');
alert('无法获取镜像名称!\n\n请确保您在正确的Docker Hub镜像页面:\n- 官方镜像:hub.docker.com/r/nginx\n- 用户镜像:hub.docker.com/r/username/imagename');
return;
}
// 如果没有手动选择架构或选择了"自动检测",则使用自动检测功能
if (!architecture || architecture === '') {
addLog('未手动选择架构,启用自动检测...');
architecture = await autoDetectArchitecture();
// 更新架构选择器显示检测结果
if (archSelector && architecture) {
const existingOption = archSelector.querySelector(`option[value="${architecture}"]`);
if (existingOption) {
archSelector.value = architecture;
} else {
// 添加检测到的架构选项
const newOption = document.createElement('option');
newOption.value = architecture;
newOption.textContent = architecture;
newOption.selected = true;
archSelector.appendChild(newOption);
}
addLog(`自动检测并设置架构: ${architecture}`);
}
}
// 开始下载流程
await performDownload(imageName, imageTag, architecture);
});
} else {
console.log('下载按钮未找到,稍后重试绑定事件');
}
}
// ==================== 页面集成功能 ====================
/**
* 查找合适的位置插入下载按钮
* 功能:简化版本,直接查找页面标题
* @returns {Array} 包含插入点信息的数组
*/
function findInsertionPoints() {
const insertionPoints = [];
// 如果已经存在下载器,不重复添加
if (document.querySelector('[data-docker-downloader]')) {
console.log('页面已存在下载器,跳过插入点查找');
return insertionPoints;
}
// 方法1: 查找h1标题
const title = document.querySelector('h1');
if (title) {
console.log('找到h1标题:', title.textContent.substring(0, 50));
insertionPoints.push({
type: 'title',
element: title.parentElement || title,
position: 'inside',
description: 'H1标题区域'
});
}
// 方法2: 查找其他可能的标题元素
const altTitles = document.querySelectorAll('h2, h3, [data-testid*="title"], .repository-title, .repo-title');
altTitles.forEach((altTitle, index) => {
if (altTitle.textContent.trim()) {
console.log(`找到备用标题${index + 1}:`, altTitle.textContent.substring(0, 50));
insertionPoints.push({
type: 'alt-title',
element: altTitle.parentElement || altTitle,
position: 'inside',
description: `备用标题区域${index + 1}`
});
}
});
// 方法3: 查找页面主要内容区域
const mainContent = document.querySelector('main, .main-content, .content, #content');
if (mainContent && insertionPoints.length === 0) {
console.log('找到主要内容区域');
insertionPoints.push({
type: 'main',
element: mainContent,
position: 'prepend',
description: '主要内容区域顶部'
});
}
// 方法4: 最后的备用方案 - body元素
if (insertionPoints.length === 0) {
console.log('使用body作为最后的插入点');
insertionPoints.push({
type: 'body',
element: document.body,
position: 'prepend',
description: '页面顶部'
});
}
console.log(`找到 ${insertionPoints.length} 个可能的插入点`);
return insertionPoints;
}
/**
* 在指定位置插入下载按钮
* 功能:根据插入点类型和位置插入相应的按钮
* @param {Object} insertionPoint - 插入点信息对象
*/
function insertDownloadButtons(insertionPoint) {
const { element, position, type } = insertionPoint;
if (!element) {
console.error('插入点元素不存在');
return;
}
// 创建按钮容器
const buttonContainer = document.createElement('span');
buttonContainer.className = 'docker-download-container';
buttonContainer.setAttribute('data-docker-downloader', 'true');
buttonContainer.style.cssText = `
display: inline-flex;
align-items: center;
gap: 8px;
margin: 0 10px;
vertical-align: middle;
`;
// 创建下载按钮
const downloadBtn = createInlineDownloadButton();
// 创建模式切换按钮
const modeToggleBtn = createModeToggleButton();
// 创建架构选择器
const archSelector = createArchSelector();
// 统一所有元素的大小(以下载按钮为标准)
const standardStyle = {
fontSize: '13px',
padding: '8px 16px',
height: '34px', // 与下载按钮保持一致的高度
marginLeft: '8px'
};
// 应用统一样式到模式切换按钮
Object.assign(modeToggleBtn.style, {
fontSize: standardStyle.fontSize,
padding: standardStyle.padding,
height: standardStyle.height,
minWidth: '110px',
marginRight: '0',
marginLeft: '0'
});
// 应用统一样式到架构选择器
Object.assign(archSelector.style, {
fontSize: standardStyle.fontSize,
padding: standardStyle.padding,
height: standardStyle.height,
minWidth: '120px',
marginLeft: standardStyle.marginLeft,
marginRight: '0'
});
// 创建架构检测状态指示器
const archIndicator = document.createElement('span');
archIndicator.id = 'archIndicator';
archIndicator.style.cssText = `
font-size: 11px;
color: #666;
margin-left: 5px;
display: none;
`;
archIndicator.textContent = '🔍 检测中...';
// 将元素添加到容器(重新排列顺序:模式切换按钮 -> 下载按钮 -> 架构选择器 -> 指示器)
buttonContainer.appendChild(modeToggleBtn);
buttonContainer.appendChild(downloadBtn);
buttonContainer.appendChild(archSelector);
buttonContainer.appendChild(archIndicator);
// 根据位置类型插入按钮
switch (position) {
case 'after':
// 在元素后面插入
if (element.nextSibling) {
element.parentNode.insertBefore(buttonContainer, element.nextSibling);
} else {
element.parentNode.appendChild(buttonContainer);
}
break;
case 'before':
// 在元素前面插入
element.parentNode.insertBefore(buttonContainer, element);
break;
case 'inside':
// 在元素内部插入
element.appendChild(buttonContainer);
break;
case 'prepend':
// 在元素内部最前面插入
element.insertBefore(buttonContainer, element.firstChild);
break;
default:
// 默认在元素后面插入
element.parentNode.appendChild(buttonContainer);
}
// 移除复杂的悬停逻辑,保持架构选择器始终可见
// 进度信息直接显示在按钮上,不需要额外的进度区域
console.log(`下载按钮已插入到: ${insertionPoint.description}`);
}
/**
* 检查是否在Docker Hub镜像页面
* 功能:验证当前页面是否适合显示下载器
* @returns {boolean} 是否在合适的镜像页面
*/
function isDockerHubImagePage() {
const url = window.location.href;
const pathname = window.location.pathname;
// 首先检查是否在Docker Hub域名
if (!url.includes('hub.docker.com')) {
console.log('不在Docker Hub域名');
return false;
}
// 排除首页和其他非镜像页面
const excludePatterns = [
/^\/$/, // 首页
/^\/search/, // 搜索页面
/^\/explore/, // 探索页面
/^\/extensions/, // 扩展页面
/^\/pricing/, // 价格页面
/^\/signup/, // 注册页面
/^\/login/, // 登录页面
/^\/u\//, // 用户页面
/^\/orgs\//, // 组织页面
/^\/repositories/, // 仓库列表页面
/^\/settings/, // 设置页面
/^\/billing/, // 账单页面
/^\/support/ // 支持页面
];
// 如果匹配排除模式,直接返回false
const isExcludedPage = excludePatterns.some(pattern => pattern.test(pathname));
if (isExcludedPage) {
console.log('页面被排除:', pathname);
return false;
}
// 检查是否在镜像相关页面(严格检测)
const imagePagePatterns = [
/^\/r\/[^\/]+$/, // /r/nginx (官方镜像)
/^\/r\/[^\/]+\/[^\/]+$/, // /r/username/imagename (用户镜像)
/^\/_\/[^\/]+$/, // /_/nginx (官方镜像另一格式)
/^\/layers\/[^\/]+\/[^\/]+\/[^\/]+/, // /layers/username/imagename/tag/... (镜像层详情页面)
/^\/r\/[^\/]+\/tags/, // 官方镜像标签页面
/^\/r\/[^\/]+\/[^\/]+\/tags/ // 用户镜像标签页面
];
const isImagePage = imagePagePatterns.some(pattern => pattern.test(pathname));
console.log('页面路径检测:', pathname, '是镜像页面:', isImagePage);
return isImagePage;
}
// ==================== 初始化和启动 ====================
/**
* 清理旧的下载器界面
* 功能:删除页面上任何已存在的下载器界面
*/
function cleanupOldDownloaders() {
// 删除旧的大型下载器界面
const oldDownloaders = document.querySelectorAll('.docker-downloader-container');
oldDownloaders.forEach(element => {
if (element.querySelector('.docker-downloader-title')) {
element.remove();
console.log('已删除旧的下载器界面');
}
});
// 删除重复的按钮容器
const containers = document.querySelectorAll('.docker-download-container');
if (containers.length > 1) {
// 保留第一个,删除其余的
for (let i = 1; i < containers.length; i++) {
containers[i].remove();
console.log('已删除重复的下载按钮');
}
}
}
/**
* 初始化下载器主函数
* 功能:执行所有必要的初始化步骤并启动下载器
*/
async function initDownloader() {
try {
console.log('开始初始化Docker下载器...');
console.log('当前URL:', window.location.href);
console.log('当前路径:', window.location.pathname);
// 等待页面DOM加载完成
if (document.readyState === 'loading') {
console.log('等待DOM加载完成...');
await new Promise(resolve => {
document.addEventListener('DOMContentLoaded', resolve);
});
}
// 检查是否在合适的Docker Hub页面
const isImagePage = isDockerHubImagePage();
console.log('是否为镜像页面:', isImagePage);
if (!isImagePage) {
console.log('不在Docker Hub镜像页面,跳过下载器初始化');
return;
}
// 清理旧的下载器界面
console.log('清理旧的下载器界面...');
cleanupOldDownloaders();
// 避免重复初始化 - 检查是否还有按钮存在
const existingBtn = document.querySelector('.docker-download-btn');
if (existingBtn) {
console.log('下载按钮仍然存在,跳过初始化');
return;
}
// 添加CSS样式
addCustomStyles();
// 初始化本地数据库
await initIndexedDB();
// 等待页面元素完全加载
await new Promise(resolve => setTimeout(resolve, 1500));
// 查找插入点并插入下载按钮
console.log('开始查找插入点...');
const insertionPoints = findInsertionPoints();
console.log('找到插入点数量:', insertionPoints.length);
if (insertionPoints.length > 0) {
// 选择最佳插入点
let bestPoint = insertionPoints[0]; // 使用找到的第一个有效点
console.log('选择插入点:', bestPoint.description, bestPoint);
insertDownloadButtons(bestPoint);
// 在按钮插入后绑定事件处理器
setTimeout(() => {
bindEventHandlers();
// 设置架构变化监听器
setupArchChangeListener();
}, 100);
} else {
console.log('未找到插入点,这不应该发生,因为findInsertionPoints已经有备用方案');
// 强制备用方案:直接在body中插入
const forcePoint = {
element: document.body,
position: 'prepend',
type: 'force',
description: '强制插入到页面顶部'
};
console.log('使用强制插入点:', forcePoint.description);
insertDownloadButtons(forcePoint);
// 在按钮插入后绑定事件处理器
setTimeout(() => {
bindEventHandlers();
// 设置架构变化监听器
setupArchChangeListener();
}, 100);
}
// 记录初始化完成
addLog('Docker Hub 下载按钮已准备就绪');
// 显示当前检测到的镜像信息
const { imageName, imageTag } = extractImageInfo();
if (imageName) {
addLog(`检测到镜像: ${imageName}:${imageTag}`);
} else {
addLog('等待镜像信息加载...');
}
} catch (error) {
console.error('初始化下载器失败:', error);
// 即使初始化失败也不要影响页面正常使用
}
}
/**
* 设置页面变化监听器
* 功能:监听单页应用的路由变化并重新初始化
*/
function setupPageChangeListener() {
let currentUrl = window.location.href;
let initTimeout = null;
// 防抖函数,避免频繁初始化
function debouncedInit() {
if (initTimeout) {
clearTimeout(initTimeout);
}
initTimeout = setTimeout(() => {
console.log('页面变化检测到,重新初始化下载器');
initDownloader();
}, 1000); // 减少延迟时间
}
// 更敏感的URL变化检测
function checkUrlChange() {
if (window.location.href !== currentUrl) {
console.log('URL变化检测:', currentUrl, '->', window.location.href);
currentUrl = window.location.href;
debouncedInit();
}
}
// 使用MutationObserver监听DOM变化
let lastCheck = 0;
const observer = new MutationObserver((mutations) => {
const now = Date.now();
if (now - lastCheck > 1000) { // 减少检查间隔
lastCheck = now;
checkUrlChange();
// 检查是否有新的页面内容加载
for (const mutation of mutations) {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
for (const node of mutation.addedNodes) {
if (node.nodeType === Node.ELEMENT_NODE) {
// 检查是否包含镜像相关内容
const text = node.textContent || '';
if (text.includes('MANIFEST DIGEST') ||
text.includes('OS/ARCH') ||
text.includes('Image Layers') ||
node.querySelector && node.querySelector('[data-testid]')) {
console.log('检测到镜像页面内容加载');
debouncedInit();
break;
}
}
}
}
}
}
});
// 开始观察页面内容变化
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: false,
characterData: false
});
// 监听浏览器前进后退按钮
window.addEventListener('popstate', () => {
console.log('浏览器导航事件');
debouncedInit();
});
// 监听pushState和replaceState(SPA路由变化)
const originalPushState = history.pushState;
const originalReplaceState = history.replaceState;
history.pushState = function() {
originalPushState.apply(history, arguments);
console.log('pushState事件');
setTimeout(checkUrlChange, 100);
};
history.replaceState = function() {
originalReplaceState.apply(history, arguments);
console.log('replaceState事件');
setTimeout(checkUrlChange, 100);
};
// 定期检查URL变化(备用方案)
setInterval(checkUrlChange, 2000);
console.log('页面变化监听器已设置');
}
// ==================== 架构自动检测功能 ====================
/**
* 从页面DOM自动检测当前选中的架构
* 功能:从Docker Hub页面的架构选择器中提取当前选中的架构信息
* @returns {string} 检测到的架构字符串,如 'linux/arm64'
*/
function detectArchFromPageDOM() {
try {
// 方法1: 优先检测OS/ARCH部分的架构选择器(基于您的截图)
// 查找包含"OS/ARCH"文本的区域附近的选择器
const osArchHeaders = document.querySelectorAll('*');
for (const header of osArchHeaders) {
const headerText = header.textContent.trim();
if (headerText === 'OS/ARCH' || headerText.includes('OS/ARCH')) {
// 在OS/ARCH标题附近查找选择器
const parent = header.parentElement;
if (parent) {
// 查找父元素及其兄弟元素中的选择器
const nearbySelectors = parent.querySelectorAll('select, [role="combobox"], .MuiSelect-select');
for (const selector of nearbySelectors) {
let archText = '';
if (selector.tagName === 'SELECT') {
const selectedOption = selector.options[selector.selectedIndex];
archText = selectedOption ? selectedOption.textContent.trim() : selector.value;
} else {
archText = selector.textContent.trim();
}
if (archText.match(/^(linux|windows|darwin)\/.+/i)) {
addLog(`从OS/ARCH区域检测到架构: ${archText}`);
return archText.toLowerCase();
}
}
// 也检查父元素的下一个兄弟元素
let nextElement = parent.nextElementSibling;
while (nextElement) {
const selectors = nextElement.querySelectorAll('select, [role="combobox"], .MuiSelect-select');
for (const selector of selectors) {
let archText = '';
if (selector.tagName === 'SELECT') {
const selectedOption = selector.options[selector.selectedIndex];
archText = selectedOption ? selectedOption.textContent.trim() : selector.value;
} else {
archText = selector.textContent.trim();
}
if (archText.match(/^(linux|windows|darwin)\/.+/i)) {
addLog(`从OS/ARCH区域下方检测到架构: ${archText}`);
return archText.toLowerCase();
}
}
nextElement = nextElement.nextElementSibling;
// 只检查接下来的几个兄弟元素,避免检查过远
if (nextElement && nextElement.getBoundingClientRect().top - parent.getBoundingClientRect().top > 200) {
break;
}
}
}
}
}
// 方法2: 检测标准的架构下拉选择器
const osArchSelectors = [
'select', // 标准select元素
'select[aria-label*="arch"]', // 带有arch标签的select
'select[aria-label*="OS"]', // 带有OS标签的select
'.MuiSelect-select', // MUI选择器
'[role="combobox"]' // 下拉框角色
];
// 收集所有可能的架构选择器及其文本
const archCandidates = [];
for (const selector of osArchSelectors) {
const elements = document.querySelectorAll(selector);
for (const element of elements) {
let archText = '';
let elementInfo = '';
// 检查select元素的选中值
if (element.tagName === 'SELECT') {
const selectedValue = element.value;
const selectedOption = element.options[element.selectedIndex];
archText = selectedOption ? selectedOption.textContent.trim() : selectedValue;
elementInfo = `SELECT(${element.className})`;
} else {
// 检查其他元素的文本内容
archText = element.textContent.trim();
elementInfo = `${element.tagName}(${element.className})`;
}
if (archText.match(/^(linux|windows|darwin)\/.+/i)) {
archCandidates.push({
text: archText.toLowerCase(),
element: element,
info: elementInfo,
position: element.getBoundingClientRect()
});
}
}
}
// 如果找到多个候选者,选择最合适的一个
if (archCandidates.length > 0) {
addLog(`找到 ${archCandidates.length} 个架构候选:`);
archCandidates.forEach((candidate, index) => {
addLog(` 候选${index + 1}: ${candidate.text} (${candidate.info}) 位置: ${Math.round(candidate.position.top)}`);
});
// 优先选择位置较低的(通常OS/ARCH部分在页面下方)
archCandidates.sort((a, b) => b.position.top - a.position.top);
const selected = archCandidates[0];
addLog(`选择架构: ${selected.text} (${selected.info}) - 位置最低`);
return selected.text;
}
// 方法2: 检测MUI架构选择器(基于您提供的DOM结构)
const muiSelectors = [
'.MuiSelect-select.MuiSelect-outlined.MuiInputBase-input.MuiOutlinedInput-input.MuiInputBase-inputSizeSmall', // 完整MUI选择器
'.MuiSelect-select.MuiSelect-outlined', // MUI下拉选择器
'.MuiInputBase-input.MuiOutlinedInput-input', // MUI输入框
'div[role="combobox"]' // div形式的下拉框
];
for (const selector of muiSelectors) {
const elements = document.querySelectorAll(selector);
for (const element of elements) {
const text = element.textContent.trim();
// 检查是否包含架构格式 (os/arch)
if (text.match(/^(linux|windows|darwin)\/.+/i)) {
addLog(`从MUI选择器检测到架构: ${text}`);
return text.toLowerCase();
}
}
}
// 方法3: 专门检测您提供的MUI容器结构
const muiContainers = document.querySelectorAll('.MuiInputBase-root.MuiOutlinedInput-root');
for (const container of muiContainers) {
const selectDiv = container.querySelector('div[role="combobox"]');
if (selectDiv) {
const text = selectDiv.textContent.trim();
if (text.match(/^(linux|windows|darwin)\/.+/i)) {
addLog(`从MUI容器检测到架构: ${text}`);
return text.toLowerCase();
}
}
}
// 方法2: 查找包含架构信息的其他元素
const archPatterns = [
/linux\/amd64/i,
/linux\/arm64/i,
/linux\/arm\/v7/i,
/linux\/arm\/v6/i,
/linux\/386/i,
/windows\/amd64/i,
/darwin\/amd64/i,
/darwin\/arm64/i
];
// 搜索页面中所有可能包含架构信息的元素
const allElements = document.querySelectorAll('*');
for (const element of allElements) {
const text = element.textContent.trim();
for (const pattern of archPatterns) {
if (pattern.test(text) && text.length < 50) { // 避免匹配过长的文本
const match = text.match(pattern);
if (match) {
addLog(`从页面元素检测到架构: ${match[0]}`);
return match[0].toLowerCase();
}
}
}
}
addLog('未能从页面DOM检测到架构信息');
return null;
} catch (error) {
addLog(`DOM架构检测失败: ${error.message}`, 'error');
return null;
}
}
/**
* 从URL中的SHA256值检测架构(使用缓存信息)
* 功能:使用缓存的架构-SHA256映射快速检测架构
* @returns {Promise<string|null>} 检测到的架构字符串
*/
async function detectArchFromSHA256() {
try {
const url = window.location.href;
const sha256Match = url.match(/sha256-([a-f0-9]{64})/i);
if (!sha256Match) {
addLog('URL中未找到SHA256值');
return null;
}
const sha256 = sha256Match[1];
addLog(`从URL提取SHA256: ${sha256.substring(0, 12)}...`);
// 确保有缓存的架构信息
if (!cachedArchitectures) {
addLog('没有缓存的架构信息,先获取架构列表');
await getAvailableArchitectures();
}
if (!cachedArchitectures || !cachedArchitectures.platformMap) {
addLog('无法获取架构信息进行SHA256检测');
return null;
}
// 首先检查平台映射中是否有直接匹配的SHA256
for (const [architecture, platformInfo] of cachedArchitectures.platformMap) {
if (platformInfo.sha256 && platformInfo.sha256 === sha256) {
addLog(`✅ 通过缓存的平台信息匹配到架构: ${architecture}`);
return architecture;
}
if (platformInfo.digest && platformInfo.digest.includes(sha256)) {
addLog(`✅ 通过缓存的摘要信息匹配到架构: ${architecture}`);
return architecture;
}
}
// 如果缓存中没有找到,需要深度检查每个架构的层信息
addLog('缓存中未找到匹配,深度检查各架构的层信息...');
const { imageName, imageTag } = extractImageInfo();
if (!imageName) {
addLog('无法提取镜像信息进行深度SHA256检测');
return null;
}
for (const architecture of cachedArchitectures.architectures) {
addLog(` 深度检查架构: ${architecture}`);
try {
// 直接调用analyzeImage函数获取特定架构的清单
const archData = await analyzeImage(imageName, imageTag, architecture);
if (archData) {
// 检查这个架构的所有层是否包含我们的SHA256
if (archData.layers) {
for (const layer of archData.layers) {
if (layer.digest && layer.digest.includes(sha256)) {
addLog(`✅ SHA256匹配镜像层! 架构: ${architecture}`);
// 更新缓存信息
const platformInfo = cachedArchitectures.platformMap.get(architecture);
if (platformInfo) {
platformInfo.layerSha256 = sha256;
}
return architecture;
}
}
}
// 也检查配置层
if (archData.manifest && archData.manifest.config &&
archData.manifest.config.digest && archData.manifest.config.digest.includes(sha256)) {
addLog(`✅ SHA256匹配配置层! 架构: ${architecture}`);
// 更新缓存信息
const platformInfo = cachedArchitectures.platformMap.get(architecture);
if (platformInfo) {
platformInfo.configSha256 = sha256;
}
return architecture;
}
}
} catch (archError) {
addLog(` 检查架构 ${architecture} 时出错: ${archError.message}`);
}
}
addLog('SHA256架构检测未找到匹配结果');
return null;
} catch (error) {
addLog(`SHA256架构检测失败: ${error.message}`, 'error');
return null;
}
}
/**
* 综合架构自动检测
* 功能:结合多种方法自动检测当前页面的架构信息
* @returns {Promise<string|null>} 检测到的架构字符串
*/
async function autoDetectArchitecture() {
addLog('开始自动架构检测...');
// 优先级1: 从URL的SHA256检测(最准确)
const sha256Arch = await detectArchFromSHA256();
if (sha256Arch) {
return sha256Arch;
}
// 优先级2: 从页面DOM检测
const domArch = detectArchFromPageDOM();
if (domArch) {
return domArch;
}
// 优先级3: 使用默认架构
const defaultArch = 'linux/amd64';
addLog(`使用默认架构: ${defaultArch}`);
return defaultArch;
}
/**
* 获取镜像的可用架构列表并缓存架构-SHA256映射
* 功能:直接从 Docker Registry 获取镜像支持的所有架构及其对应的SHA256
* @returns {Promise<Array>} 可用架构列表
*/
async function getAvailableArchitectures() {
try {
// 提取镜像信息
const { imageName, imageTag } = extractImageInfo();
if (!imageName) {
addLog('无法提取镜像信息,跳过架构列表获取');
return [];
}
// 检查是否已有缓存
const cacheKey = `${imageName}:${imageTag}`;
if (cachedArchitectures && cachedArchitectures.cacheKey === cacheKey) {
addLog('使用缓存的架构信息');
return cachedArchitectures.architectures;
}
addLog(`获取镜像架构列表: ${imageName}:${imageTag}`);
// 直接调用分析镜像函数获取架构信息
const manifestData = await analyzeImage(imageName, imageTag, '');
if (manifestData.multiArch && manifestData.availablePlatforms) {
addLog(`发现多架构镜像,包含 ${manifestData.availablePlatforms.length} 个架构`);
// 缓存架构信息,包含架构和对应的SHA256/digest
cachedArchitectures = {
cacheKey: cacheKey,
architectures: manifestData.availablePlatforms.map(platform => platform.platform),
platformMap: new Map(manifestData.availablePlatforms.map(platform => [
platform.platform,
{
digest: platform.digest,
sha256: platform.digest ? platform.digest.replace('sha256:', '') : null
}
]))
};
addLog(`已缓存 ${manifestData.availablePlatforms.length} 个架构的SHA256映射`);
return cachedArchitectures.architectures;
} else {
addLog('发现单架构镜像,使用默认架构列表');
// 为单架构镜像创建缓存
cachedArchitectures = {
cacheKey: cacheKey,
architectures: ['linux/amd64'],
platformMap: new Map([['linux/amd64', { digest: null, sha256: null }]])
};
return cachedArchitectures.architectures;
}
} catch (error) {
addLog(`获取架构列表失败: ${error.message}`, 'error');
return [];
}
}
/**
* 更新架构选择器的选项
* 功能:根据获取到的可用架构更新下拉选择器选项
* @param {HTMLElement} selector - 架构选择器元素
* @param {Array} architectures - 可用架构列表
*/
function updateArchSelectorOptions(selector, architectures) {
if (!selector || !architectures || architectures.length === 0) {
return;
}
// 保存当前选中的值
const currentValue = selector.value;
// 清空现有选项,但保留"自动检测"选项
selector.innerHTML = '<option value="">自动检测</option>';
// 添加获取到的架构选项
architectures.forEach(arch => {
const option = document.createElement('option');
option.value = arch;
option.textContent = arch;
selector.appendChild(option);
});
// 如果之前有选中的值且仍然存在,恢复选中状态
if (currentValue && architectures.includes(currentValue)) {
selector.value = currentValue;
}
addLog(`架构选择器已更新: ${architectures.join(', ')}`);
}
/**
* 设置页面架构选择器变化监听
* 功能:监听Docker Hub页面上的OS/ARCH选择器变化,自动更新我们的架构选择器
*/
function setupArchChangeListener() {
// 监听所有可能的架构选择器变化
const archSelectors = [
'select', // 标准select元素
'.MuiSelect-select', // MUI选择器
'[role="combobox"]' // 下拉框角色
];
archSelectors.forEach(selector => {
const elements = document.querySelectorAll(selector);
elements.forEach(element => {
// 为每个可能的架构选择器添加变化监听
if (element.tagName === 'SELECT') {
element.addEventListener('change', handleArchChange);
addLog(`为SELECT元素添加了变化监听: ${element.className}`);
} else {
// 对于非select元素,使用MutationObserver监听内容变化
const observer = new MutationObserver(handleArchChange);
observer.observe(element, {
childList: true,
subtree: true,
characterData: true
});
addLog(`为元素添加了MutationObserver: ${element.className}`);
}
});
});
// 使用全局MutationObserver监听页面架构相关变化
const globalObserver = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
// 检查变化的节点是否包含架构信息
if (mutation.type === 'childList' || mutation.type === 'characterData') {
const target = mutation.target;
if (target.textContent && target.textContent.match(/^(linux|windows|darwin)\/.+/i)) {
handleArchChange();
}
}
});
});
// 监听页面主要内容区域的变化
const mainContent = document.querySelector('main, body');
if (mainContent) {
globalObserver.observe(mainContent, {
childList: true,
subtree: true,
characterData: true
});
}
addLog('架构变化监听器已设置');
}
/**
* 处理页面架构选择器变化
* 功能:当页面架构选择器发生变化时,自动更新我们的架构选择器
*/
async function handleArchChange() {
try {
// 防止频繁触发
if (handleArchChange.timeout) {
clearTimeout(handleArchChange.timeout);
}
handleArchChange.timeout = setTimeout(async () => {
addLog('检测到页面架构变化,正在更新...');
const ourArchSelector = document.getElementById('archSelector');
const archIndicator = document.getElementById('archIndicator');
// 显示更新状态
if (archIndicator) {
archIndicator.style.display = 'inline';
archIndicator.textContent = '🔄 更新中...';
archIndicator.style.color = '#007bff';
}
// 重新获取可用架构列表
const availableArchs = await getAvailableArchitectures();
if (availableArchs && availableArchs.length > 0 && ourArchSelector) {
updateArchSelectorOptions(ourArchSelector, availableArchs);
}
// 只有在用户没有手动选择时才重新检测架构
if (!userManuallySelectedArch) {
const detectedArch = await autoDetectArchitecture();
if (detectedArch && ourArchSelector) {
// 检查选项中是否存在检测到的架构
const existingOption = ourArchSelector.querySelector(`option[value="${detectedArch}"]`);
if (existingOption) {
ourArchSelector.value = detectedArch;
} else {
// 如果选项中不存在,添加新选项
const newOption = document.createElement('option');
newOption.value = detectedArch;
newOption.textContent = detectedArch;
newOption.selected = true;
ourArchSelector.appendChild(newOption);
}
ourArchSelector.title = `当前架构: ${detectedArch} (页面同步)`;
addLog(`架构选择器已同步更新为: ${detectedArch}`);
// 更新指示器
if (archIndicator) {
archIndicator.textContent = `✅ ${detectedArch}`;
archIndicator.style.color = '#28a745';
setTimeout(() => {
archIndicator.style.display = 'none';
}, 2000);
}
} else if (archIndicator) {
archIndicator.textContent = '❌ 更新失败';
archIndicator.style.color = '#dc3545';
setTimeout(() => {
archIndicator.style.display = 'none';
}, 2000);
}
} else {
// 用户已手动选择,不进行自动更新
addLog('跳过页面架构同步(用户已手动选择)');
if (archIndicator) {
archIndicator.textContent = '✋ 保持手动选择';
archIndicator.style.color = '#fd7e14';
setTimeout(() => {
archIndicator.style.display = 'none';
}, 2000);
}
}
}, 500); // 500ms防抖
} catch (error) {
addLog(`架构变化处理失败: ${error.message}`, 'error');
}
}
// ==================== 脚本入口点 ====================
// 脚本启动日志
console.log('Docker Hub 镜像下载器脚本已加载');
// 启动初始化流程
initDownloader();
// 设置页面变化监听
setupPageChangeListener();
})();