等到秋叶终于金黄，等到华发悄然苍苍
在博客内放置一个基于Mirages主题的Steam游戏时长小组件
项目特性：做了对Mirages主题日间模式、日落模式及夜间模式的自适应，简洁美观（迫真）。 1 准备工作1.1 环境要求PHP 7.2 或更高版本；PHP 扩展含 curl、json；服务器可正常访问 api.steampowered.com和 store.steampowered.com。1.2 获取Steam ID64访问STEAMID I/O，在 input...处输入Steam个人主页直链，点击 lookup。 返回的绿框处的数字即为Steam ID64，将之记录下来。1.3 获取Steam API Key前往Steam Web API密钥，绑定域（即你的网站域名），申请即可。 返回的绿框处的密钥即为API Key，将之记录下来。 与公开的ID64不同，API Key是高风险密钥，与你的Steam账号高度关联，请务必妥善保管，不得以任何明文形式泄露。如有泄露风险，麻烦即刻在该页面注销密钥。 2 部署代码在网站根目录创建一个文件夹，命名为 steam-recent-games，后续文件均创建于此内。2.1 api创建文件夹 api，在其中放置三个文件： config.php <?php return [ // STEAM_API_KEY = 你的key // STEAM_ID64 = 你的steam_id64 'steam_api_key' => '', 'steam_id64' => '', // 返回游戏数量 'limit' => 5, // 服务端缓存 'cache_ttl_seconds' => 1800, ]; recent-games.php <?php declare(strict_types=1); const STEAM_RECENT_GAMES_VERSION = '1.0.0'; const STEAM_IMAGE_SCHEMA_VERSION = 'store_appdetails_v4'; header('Content-Type: application/json; charset=utf-8'); header('X-Content-Type-Options: nosniff'); header('Cache-Control: public, max-age=120'); if ($_SERVER['REQUEST_METHOD'] !== 'GET') { http_response_code(405); header('Allow: GET'); echo json_encode(['error' => 'Method Not Allowed'], JSON_UNESCAPED_UNICODE); exit; } $origin = $_SERVER['HTTP_ORIGIN'] ?? ''; if ($origin !== '') { $host = $_SERVER['HTTP_HOST'] ?? ''; $originHost = parse_url($origin, PHP_URL_HOST); if (is_string($originHost) && hash_equals($host, $originHost)) { header('Access-Control-Allow-Origin: ' . $origin); header('Vary: Origin'); } } try { $config = load_config(); $apiKey = get_config_value($config, 'steam_api_key', 'STEAM_API_KEY'); $steamId64 = get_config_value($config, 'steam_id64', 'STEAM_ID64'); $limit = max(1, min(20, (int)($config['limit'] ?? 5))); $cacheTtl = max(60, (int)($config['cache_ttl_seconds'] ?? 1800)); if ($apiKey === '' || $steamId64 === '') { throw new RuntimeException('Steam API is not configured.'); } $cacheFile = cache_file($steamId64); $cached = read_cache($cacheFile, $cacheTtl); $expiredCache = read_cache($cacheFile, 0, true); $imageCache = read_image_cache(); if (is_array($cached) && cache_is_current($cached)) { $cached = apply_image_cache_to_response($cached, $imageCache); echo json_encode(array_merge($cached, ['cached' => true]), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); exit; } if (is_array($cached) && cache_has_games($cached)) { $refreshed = refresh_cached_game_images($cached, $imageCache); write_cache($cacheFile, $refreshed); write_image_cache_from_games($refreshed['response']['games'] ?? [], $imageCache); echo json_encode(array_merge($refreshed, [ 'cached' => true, 'image_refreshed' => true, ]), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); exit; } $payload = fetch_recent_games($apiKey, $steamId64); $games = normalize_games($payload['response']['games'] ?? [], $limit, $expiredCache, $imageCache); $result = [ 'response' => [ 'total_count' => count($games), 'games' => $games, ], 'cached' => false, 'generated_at' => gmdate('c'), 'version' => STEAM_RECENT_GAMES_VERSION, 'image_schema' => STEAM_IMAGE_SCHEMA_VERSION, ]; write_cache($cacheFile, $result); write_image_cache_from_games($games, $imageCache); echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); } catch (Throwable $e) { $stale = read_stale_fallback(); if (is_array($stale)) { http_response_code(200); echo json_encode(array_merge($stale, [ 'cached' => true, 'stale' => true, 'warning' => 'Steam data is temporarily unavailable; showing cached data.', ]), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); exit; } http_response_code(500); echo json_encode(['error' => $e->getMessage()], JSON_UNESCAPED_UNICODE); } function read_stale_fallback(): ?array { try { $cache = read_cache(cache_file('fallback'), 86400 * 7, true); if (!is_array($cache)) { return null; } $imageCache = read_image_cache(); if (cache_is_current($cache)) { return apply_image_cache_to_response($cache, $imageCache); } return cache_has_games($cache) ? refresh_cached_game_images($cache, $imageCache) : null; } catch (Throwable $e) { return null; } } function cache_is_current(array $cache): bool { return (string)($cache['image_schema'] ?? '') === STEAM_IMAGE_SCHEMA_VERSION; } function cache_has_games(array $cache): bool { return isset($cache['response']['games']) && is_array($cache['response']['games']); } function refresh_cached_game_images(array $cache, array $imageCache = []): array { $games = $cache['response']['games']; $storeDetails = fetch_store_appdetails_for_games($games); $previousImages = merge_image_maps($imageCache, previous_image_map($cache)); $refreshedGames = []; foreach ($games as $game) { if (!is_array($game)) { continue; } $appid = (int)($game['appid'] ?? 0); $game['appid'] = $appid; $game['name'] = (string)($game['name'] ?? 'Unknown Game'); $game['playtime_2weeks'] = (int)($game['playtime_2weeks'] ?? 0); $game['playtime_forever'] = (int)($game['playtime_forever'] ?? 0); $game['image'] = resolve_game_image($appid, $storeDetails, $previousImages); $refreshedGames[] = $game; } $cache['response']['games'] = $refreshedGames; $cache['response']['total_count'] = count($refreshedGames); $cache['cached'] = false; $cache['generated_at'] = gmdate('c'); $cache['version'] = STEAM_RECENT_GAMES_VERSION; $cache['image_schema'] = STEAM_IMAGE_SCHEMA_VERSION; return $cache; } function load_config(): array { $configPath = __DIR__ . '/config.php'; if (is_file($configPath)) { $config = require $configPath; if (!is_array($config)) { throw new RuntimeException('Invalid config.php.'); } return $config; } return []; } function get_config_value(array $config, string $key, string $envKey): string { $env = getenv($envKey); if (is_string($env) && trim($env) !== '') { return trim($env); } return trim((string)($config[$key] ?? '')); } function cache_file(string $steamId64): string { $cacheDir = dirname(__DIR__) . '/cache'; if (!is_dir($cacheDir) && !mkdir($cacheDir, 0750, true) && !is_dir($cacheDir)) { throw new RuntimeException('Unable to create cache directory.'); } return $cacheDir . '/recent-games-' . sha1($steamId64) . '.json'; } function read_cache(string $path, int $ttl, bool $ignoreTtl = false): ?array { if (!is_file($path)) { return null; } if (!$ignoreTtl && (time() - filemtime($path)) > $ttl) { return null; } $handle = fopen($path, 'rb'); if ($handle === false) { return null; } $raw = ''; while (!feof($handle)) { $chunk = fread($handle, 8192); if ($chunk === false) { fclose($handle); return null; } $raw .= $chunk; } fclose($handle); $decoded = json_decode($raw, true); return is_array($decoded) ? $decoded : null; } function write_cache(string $path, array $data): void { write_json_file($path, $data); copy($path, cache_file('fallback')); } function write_json_file(string $path, array $data): void { $encoded = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); if ($encoded === false) { return; } file_put_contents($path, $encoded, LOCK_EX); } function fetch_recent_games(string $apiKey, string $steamId64): array { if (!function_exists('curl_init')) { throw new RuntimeException('PHP cURL extension is not enabled.'); } $query = http_build_query([ 'key' => $apiKey, 'steamid' => $steamId64, 'format' => 'json', ]); $url = 'https://api.steampowered.com/IPlayerService/GetRecentlyPlayedGames/v0001/?' . $query; $ch = curl_init($url); if ($ch === false) { throw new RuntimeException('Unable to initialize cURL.'); } curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 10, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_HTTPHEADER => ['Accept: application/json'], CURLOPT_USERAGENT => 'SteamRecentGames/' . STEAM_RECENT_GAMES_VERSION, ]); $body = curl_exec($ch); $status = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE); $error = curl_error($ch); curl_close($ch); if ($body === false || $status < 200 || $status >= 300) { throw new RuntimeException('Steam API request failed' . ($error !== '' ? ': ' . $error : '.')); } $decoded = json_decode($body, true); if (!is_array($decoded)) { throw new RuntimeException('Steam API returned invalid JSON.'); } return $decoded; } function normalize_games(array $games, int $limit, ?array $previousCache = null, array $imageCache = []): array { usort($games, static function (array $a, array $b): int { return (int)($b['playtime_2weeks'] ?? 0) <=> (int)($a['playtime_2weeks'] ?? 0); }); $games = array_slice($games, 0, $limit); $storeDetails = fetch_store_appdetails_for_games($games); $previousImages = is_array($previousCache) ? merge_image_maps($imageCache, previous_image_map($previousCache)) : $imageCache; return array_map(static function (array $game) use ($storeDetails, $previousImages): array { $appid = (int)($game['appid'] ?? 0); return [ 'appid' => $appid, 'name' => (string)($game['name'] ?? 'Unknown Game'), 'playtime_2weeks' => (int)($game['playtime_2weeks'] ?? 0), 'playtime_forever' => (int)($game['playtime_forever'] ?? 0), 'image' => resolve_game_image($appid, $storeDetails, $previousImages), ]; }, $games); } function apply_image_cache_to_response(array $cache, array $imageCache): array { if (!cache_has_games($cache) || !$imageCache) { return $cache; } foreach ($cache['response']['games'] as $index => $game) { if (!is_array($game)) { continue; } $appid = (int)($game['appid'] ?? 0); $image = trim((string)($game['image'] ?? '')); if ( $appid > 0 && isset($imageCache[$appid]) && is_valid_image_url($imageCache[$appid]) && ($image === '' || is_fallback_image_url($appid, $image)) ) { $cache['response']['games'][$index]['image'] = $imageCache[$appid]; } } return $cache; } function merge_image_maps(array $primary, array $secondary): array { foreach ($secondary as $appid => $url) { if (!isset($primary[$appid])) { $primary[$appid] = $url; } } return $primary; } function read_image_cache(): array { $cache = read_cache(image_cache_file(), 0, true); if (!is_array($cache) || (string)($cache['schema'] ?? '') !== STEAM_IMAGE_SCHEMA_VERSION) { return []; } $images = []; foreach (($cache['images'] ?? []) as $appid => $entry) { $appid = (int)$appid; $url = is_array($entry) ? trim((string)($entry['url'] ?? '')) : ''; if ($appid > 0 && $url !== '' && is_valid_image_url($url) && !is_fallback_image_url($appid, $url)) { $images[$appid] = $url; } } return $images; } function write_image_cache_from_games(array $games, array $existingImages = []): void { $images = $existingImages; foreach ($games as $game) { if (!is_array($game)) { continue; } $appid = (int)($game['appid'] ?? 0); $url = trim((string)($game['image'] ?? '')); if ($appid > 0 && $url !== '' && is_valid_image_url($url) && !is_fallback_image_url($appid, $url)) { $images[$appid] = $url; } } $payload = [ 'schema' => STEAM_IMAGE_SCHEMA_VERSION, 'generated_at' => gmdate('c'), 'images' => [], ]; foreach ($images as $appid => $url) { $payload['images'][(string)$appid] = [ 'url' => $url, 'updated_at' => gmdate('c'), ]; } write_json_file(image_cache_file(), $payload); } function previous_image_map(array $cache): array { if (!cache_has_games($cache)) { return []; } $images = []; foreach ($cache['response']['games'] as $game) { if (!is_array($game)) { continue; } $appid = (int)($game['appid'] ?? 0); $image = trim((string)($game['image'] ?? '')); if ($appid > 0 && $image !== '' && is_valid_image_url($image) && !is_fallback_image_url($appid, $image)) { $images[$appid] = $image; } } return $images; } function fetch_store_appdetails_for_games(array $games): array { $appids = []; foreach ($games as $game) { $appid = (int)($game['appid'] ?? 0); if ($appid > 0) { $appids[] = $appid; } } if (!$appids) { return []; } return fetch_store_appdetails(array_values(array_unique($appids))); } function resolve_game_image(int $appid, array $storeDetails, array $previousImages = []): string { $fallback = fallback_game_image($appid); if ($appid <= 0) { return $fallback; } $data = $storeDetails[(string)$appid]['data'] ?? null; if (is_array($data)) { foreach (['header_image', 'capsule_image'] as $field) { $image = trim((string)($data[$field] ?? '')); if ($image !== '' && is_valid_image_url($image)) { return $image; } } } if (isset($previousImages[$appid]) && is_valid_image_url($previousImages[$appid])) { return $previousImages[$appid]; } return $fallback; } function fetch_store_appdetails(array $appids): array { if (!function_exists('curl_init') || !function_exists('curl_multi_init')) { return []; } $multi = curl_multi_init(); if ($multi === false) { return []; } $handles = []; foreach ($appids as $appid) { $appid = (int)$appid; if ($appid <= 0) { continue; } $query = http_build_query([ 'appids' => $appid, 'filters' => 'basic', ]); $url = 'https://store.steampowered.com/api/appdetails?' . $query; $ch = curl_init($url); if ($ch === false) { continue; } curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 2, CURLOPT_TIMEOUT => 4, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_HTTPHEADER => ['Accept: application/json'], CURLOPT_USERAGENT => 'SteamRecentGames/' . STEAM_RECENT_GAMES_VERSION, ]); $key = (string)$appid; $handles[$key] = $ch; curl_multi_add_handle($multi, $ch); } if (!$handles) { curl_multi_close($multi); return []; } do { $status = curl_multi_exec($multi, $running); if ($running) { curl_multi_select($multi, 1.0); } } while ($running && $status === CURLM_OK); $details = []; foreach ($handles as $appid => $ch) { $body = curl_multi_getcontent($ch); $httpStatus = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE); curl_multi_remove_handle($multi, $ch); curl_close($ch); if ($body === false || $httpStatus < 200 || $httpStatus >= 300) { continue; } $decoded = json_decode($body, true); if (is_array($decoded) && isset($decoded[$appid]) && is_array($decoded[$appid])) { $details[$appid] = $decoded[$appid]; } } curl_multi_close($multi); return $details; } function fallback_game_image(int $appid): string { return 'https://cdn.cloudflare.steamstatic.com/steam/apps/' . $appid . '/header.jpg'; } function image_cache_file(): string { $cacheDir = dirname(__DIR__) . '/cache'; if (!is_dir($cacheDir) && !mkdir($cacheDir, 0750, true) && !is_dir($cacheDir)) { throw new RuntimeException('Unable to create cache directory.'); } return $cacheDir . '/image-url-cache.json'; } function is_fallback_image_url(int $appid, string $url): bool { return $url === fallback_game_image($appid); } function is_valid_image_url(string $url): bool { $parts = parse_url($url); if (!is_array($parts)) { return false; } $scheme = strtolower((string)($parts['scheme'] ?? '')); return $scheme === 'https' && !empty($parts['host']); } .htaccess <FilesMatch "^config\.php$"> Require all denied </FilesMatch> 2.2 assets创建文件夹 assets，在其中放置两个文件： steam-recent-games.css .steam-games { --steam-row-hover: rgba(255, 255, 255, 0.56); --steam-row-active: rgba(255, 255, 255, 0.68); --steam-border: rgba(28, 34, 42, 0.12); --steam-text: #24272d; --steam-muted: #697386; --steam-accent: #168f7d; --steam-accent-soft: rgba(22, 143, 125, 0.14); --steam-row-shadow: 0 10px 24px rgba(22, 28, 38, 0.09); --steam-cover-bg: rgba(28, 34, 42, 0.08); padding: 0 1.5rem; color: var(--steam-text); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; } .steam-games__title { margin: 0 0 1.2rem; padding-bottom: 0.8rem; color: var(--steam-text); font-size: 1.4em; font-weight: 700; } .steam-games__list { overflow: hidden; border-bottom: 1px solid var(--steam-border); background: transparent; } .steam-games__item { position: relative; display: flex; align-items: center; gap: 18px; min-height: 72px; padding: 12px 14px; border-bottom: 1px solid var(--steam-border); outline: none; transition: background-color 180ms ease, box-shadow 180ms ease, transform 180ms ease; } .steam-games__item:last-child { border-bottom: 0; } .steam-games__item::before { position: absolute; inset: 10px auto 10px 0; width: 3px; border-radius: 999px; background: var(--steam-accent); content: ""; opacity: 0; transform: scaleY(0.45); transition: opacity 180ms ease, transform 180ms ease; } .steam-games__item:hover, .steam-games__item:focus-visible { z-index: 1; background: var(--steam-row-hover); box-shadow: var(--steam-row-shadow); transform: translateX(4px); } .steam-games__item:hover::before, .steam-games__item:focus-visible::before { opacity: 1; transform: scaleY(1); } .steam-games__cover { width: 145px; aspect-ratio: 460 / 215; border-radius: 7px; overflow: hidden; flex: 0 0 auto; background: var(--steam-cover-bg); box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.16); } .steam-games__cover img { display: block; width: 100%; height: 100%; margin: 0 !important; object-fit: cover; transition: filter 180ms ease, transform 220ms ease; } .steam-games__item:hover .steam-games__cover img, .steam-games__item:focus-visible .steam-games__cover img { filter: saturate(1.08) contrast(1.04); transform: scale(1.035); } .steam-games__info { flex: 1 1 auto; min-width: 0; } .steam-games__name { margin: 0; overflow: hidden; color: var(--steam-text); font-size: 16px; font-weight: 650; line-height: 1.4; text-overflow: ellipsis; white-space: nowrap; } .steam-games__meta { margin-top: 3px; color: var(--steam-muted); font-size: 12px; line-height: 1.4; } .steam-games__time { min-width: 145px; flex: 0 0 auto; color: var(--steam-muted); font-size: 13px; line-height: 1.5; text-align: right; } .steam-games__time span { display: block; } .steam-games__recent { display: inline-block; color: var(--steam-accent); font-weight: 650; } .steam-games__recent::before { display: inline-block; width: 0.52em; height: 0.52em; margin-right: 0.45em; border-radius: 999px; background: var(--steam-accent); box-shadow: 0 0 0 5px var(--steam-accent-soft); content: ""; vertical-align: 0.04em; } .steam-games__state { padding: 2rem; color: var(--steam-muted); font-size: 14px; text-align: center; } .steam-games__source { margin-top: 1rem; color: var(--steam-muted); font-size: 0.85em; text-align: right; } .steam-games__source a { color: var(--steam-muted); text-decoration: none; transition: color 160ms ease; } .steam-games__source a:hover { color: var(--steam-accent); } body.theme-dark .steam-games, html.theme-dark .steam-games, .theme-dark .steam-games, [data-theme="dark"] .steam-games, [theme="dark"] .steam-games { --steam-row-hover: rgba(255, 255, 255, 0.075); --steam-row-active: rgba(255, 255, 255, 0.1); --steam-border: rgba(255, 255, 255, 0.09); --steam-text: #f2f4f7; --steam-muted: #b5bdc9; --steam-accent: #4fd1b8; --steam-accent-soft: rgba(79, 209, 184, 0.16); --steam-row-shadow: 0 12px 24px rgba(0, 0, 0, 0.22); --steam-cover-bg: rgba(255, 255, 255, 0.08); } body.theme-white .steam-games, html.theme-white .steam-games, .theme-white .steam-games, body.theme-light .steam-games, html.theme-light .steam-games, .theme-light .steam-games, body.theme-sunset .steam-games, html.theme-sunset .steam-games, .theme-sunset .steam-games, [data-theme="white"] .steam-games, [data-theme="light"] .steam-games, [data-theme="sunset"] .steam-games, [theme="white"] .steam-games, [theme="light"] .steam-games, [theme="sunset"] .steam-games { --steam-row-hover: rgba(255, 255, 255, 0.54); --steam-row-active: rgba(255, 255, 255, 0.66); --steam-border: rgba(32, 39, 50, 0.13); --steam-text: #20242a; --steam-muted: #5f6877; --steam-accent: #b65f2b; --steam-accent-soft: rgba(182, 95, 43, 0.14); --steam-row-shadow: 0 10px 24px rgba(46, 36, 28, 0.1); --steam-cover-bg: rgba(32, 39, 50, 0.08); } @media (max-width: 576px) { .steam-games { padding: 0 1rem; } .steam-games__item { gap: 12px; padding: 11px 12px; } .steam-games__cover { width: 126px; } .steam-games__info { display: none; } .steam-games__time { min-width: 118px; margin-left: auto; } } @media (prefers-reduced-motion: reduce) { .steam-games__item, .steam-games__item::before, .steam-games__cover img, .steam-games__source a { transition: none; } .steam-games__item:hover, .steam-games__item:focus-visible { transform: none; } } steam-recent-games.js (function () { 'use strict'; var DEFAULTS = { endpoint: '/steam-recent-games/api/recent-games.php', cacheKey: 'steamRecentGames:v2', cacheTtl: 30 * 60 * 1000, }; document.addEventListener('DOMContentLoaded', function () { var widgets = document.querySelectorAll('[data-steam-recent-games]'); Array.prototype.forEach.call(widgets, initSteamGames); }); function initSteamGames(root) { var config = { endpoint: root.getAttribute('data-endpoint') || DEFAULTS.endpoint, cacheKey: root.getAttribute('data-cache-key') || DEFAULTS.cacheKey, cacheTtl: Number(root.getAttribute('data-cache-ttl') || DEFAULTS.cacheTtl), }; var list = root.querySelector('[data-steam-games-list]'); if (!list) return; renderState(list, '绝赞加载中...'); var cached = readCache(config); if (cached) { renderGames(list, cached); } fetch(config.endpoint, { headers: { Accept: 'application/json' }, credentials: 'same-origin', }) .then(function (response) { if (!response.ok) { throw new Error('请求失败：HTTP ' + response.status); } return response.json(); }) .then(function (data) { if (data.error) { throw new Error(data.error); } var games = data && data.response && Array.isArray(data.response.games) ? data.response.games : []; if (!games.length) { throw new Error('最近没有在玩游戏捏'); } writeCache(config, games); renderGames(list, games); }) .catch(function (error) { if (!cached) { renderState(list, error.message || '数据加载失败。', true); } }); } function renderGames(list, games) { var fragment = document.createDocumentFragment(); games.forEach(function (game) { fragment.appendChild(createGameItem(game)); }); list.innerHTML = ''; list.appendChild(fragment); } function createGameItem(game) { var item = document.createElement('div'); item.className = 'steam-games__item'; item.tabIndex = 0; var cover = document.createElement('div'); cover.className = 'steam-games__cover'; var image = document.createElement('img'); image.loading = 'lazy'; image.alt = game.name || 'Steam game'; image.src = game.image || 'https://cdn.cloudflare.steamstatic.com/steam/apps/' + game.appid + '/header.jpg'; image.addEventListener('error', function () { image.style.opacity = '0.45'; }); cover.appendChild(image); var info = document.createElement('div'); info.className = 'steam-games__info'; var title = document.createElement('div'); title.className = 'steam-games__name'; title.textContent = game.name || 'Unknown Game'; info.appendChild(title); var meta = document.createElement('div'); meta.className = 'steam-games__meta'; meta.textContent = Number(game.playtime_2weeks) > 0 ? '近两周有在玩' : '近两周暂无游玩记录'; info.appendChild(meta); var playtime = document.createElement('div'); playtime.className = 'steam-games__time'; if (Number(game.playtime_2weeks) > 0) { var recent = document.createElement('span'); recent.className = 'steam-games__recent'; recent.textContent = '最近 ' + formatPlaytime(game.playtime_2weeks); playtime.appendChild(recent); } var total = document.createElement('span'); total.className = 'steam-games__total'; total.textContent = '总时长 ' + formatPlaytime(game.playtime_forever); playtime.appendChild(total); item.appendChild(cover); item.appendChild(info); item.appendChild(playtime); return item; } function renderState(list, message, isError) { var state = document.createElement('div'); state.className = 'steam-games__state'; if (isError) state.setAttribute('role', 'alert'); state.textContent = message; list.innerHTML = ''; list.appendChild(state); } function formatPlaytime(minutes) { var value = Number(minutes || 0); if (value <= 0) return '0 分钟'; var hours = Math.floor(value / 60); var mins = value % 60; if (hours <= 0) return mins + ' 分钟'; if (mins <= 0) return hours + ' 小时'; return hours + ' 小时 ' + mins + ' 分钟'; } function readCache(config) { try { var raw = localStorage.getItem(config.cacheKey); if (!raw) return null; var cache = JSON.parse(raw); if (!cache || Date.now() - cache.timestamp > config.cacheTtl) return null; return Array.isArray(cache.data) ? cache.data : null; } catch (_) { return null; } } function writeCache(config, data) { try { localStorage.setItem(config.cacheKey, JSON.stringify({ timestamp: Date.now(), data: data, })); } catch (_) { // 隐私模式访问的话本地存储估计会失效 } } }()); 2.3 cache创建文件夹 cache，在其中放置： .htaccess Require all denied 2.4 文件树综上，项目文件结构应当为：steam-recent-games/ api/ .htaccess config.php recent-games.php assets/ steam-recent-games.css steam-recent-games.js cache/ .htaccess3 配置与应用编辑 steam-recent-games/api/config.php，填入你的API Key和ID64：return [ 'steam_api_key' => '你的 Steam API Key', 'steam_id64' => '你的 Steam ID64', 'limit' => 5, 'cache_ttl_seconds' => 1800, ];出于安全考量，更推荐使用服务器环境变量配置 STEAM_API_KEY和 STEAM_ID64。接着，在 主题设置-主题自定义扩展-head标签结束前引入css和js： 最后，只需将以下代码嵌入到任何你想放置小组件的地方： 最近在玩ヾ(•ω•`) 绝赞加载中... 数据来自 Steam Web API 大功告成！::EOC:04::4 DEMO预览 最近在玩ヾ(•ω•`) 绝赞加载中... 数据来自 Steam Web API 5 如果是启用了PJAX的情况..？如果启用了主题的全站PJAX，则可能出现组件加载不出来的情况。这时 assets/steam-recent-games.js需要换成下面这个版本： steam-recent-games.js (function () { 'use strict'; var DEFAULTS = { endpoint: '/steam-recent-games/api/recent-games.php', cacheKey: 'steamRecentGames:v2', cacheTtl: 30 * 60 * 1000 }; window.initSteamGames = function initSteamGames(scope) { var root = scope && scope.nodeType === 1 ? scope : document; var widgets; if (root.matches && root.matches('[data-steam-recent-games]')) { widgets = [root]; } else { widgets = root.querySelectorAll('[data-steam-recent-games]'); } Array.prototype.forEach.call(widgets, initSteamGamesWidget); }; document.addEventListener('DOMContentLoaded', function () { window.initSteamGames(); }); function initSteamGamesWidget(root) { if (root.getAttribute('data-steam-games-ready') === '1') { return; } root.setAttribute('data-steam-games-ready', '1'); var config = { endpoint: root.getAttribute('data-endpoint') || DEFAULTS.endpoint, cacheKey: root.getAttribute('data-cache-key') || DEFAULTS.cacheKey, cacheTtl: Number(root.getAttribute('data-cache-ttl') || DEFAULTS.cacheTtl) }; var list = root.querySelector('[data-steam-games-list]'); if (!list) return; renderState(list, '绝赞加载中...'); var cached = readCache(config); if (cached) { renderGames(list, cached); } fetch(config.endpoint, { headers: { Accept: 'application/json' }, credentials: 'same-origin' }) .then(function (response) { if (!response.ok) { throw new Error('请求失败：HTTP ' + response.status); } return response.json(); }) .then(function (data) { if (data.error) { throw new Error(data.error); } var games = data && data.response && Array.isArray(data.response.games) ? data.response.games : []; if (!games.length) { throw new Error('最近没有在玩游戏捏'); } writeCache(config, games); renderGames(list, games); }) .catch(function (error) { root.removeAttribute('data-steam-games-ready'); if (!cached) { renderState(list, error.message || '数据加载失败', true); } }); } function renderGames(list, games) { var fragment = document.createDocumentFragment(); games.forEach(function (game) { fragment.appendChild(createGameItem(game)); }); list.innerHTML = ''; list.appendChild(fragment); } function createGameItem(game) { var item = document.createElement('div'); item.className = 'steam-games__item'; item.tabIndex = 0; var cover = document.createElement('div'); cover.className = 'steam-games__cover'; var image = document.createElement('img'); image.loading = 'lazy'; image.alt = game.name || 'Steam game'; image.src = game.image || 'https://cdn.cloudflare.steamstatic.com/steam/apps/' + game.appid + '/header.jpg'; image.addEventListener('error', function () { image.style.opacity = '0.45'; }); cover.appendChild(image); var info = document.createElement('div'); info.className = 'steam-games__info'; var title = document.createElement('div'); title.className = 'steam-games__name'; title.textContent = game.name || 'Unknown Game'; info.appendChild(title); var meta = document.createElement('div'); meta.className = 'steam-games__meta'; meta.textContent = Number(game.playtime_2weeks) > 0 ? '近两周有在玩' : '近两周暂无游玩记录'; info.appendChild(meta); var playtime = document.createElement('div'); playtime.className = 'steam-games__time'; if (Number(game.playtime_2weeks) > 0) { var recent = document.createElement('span'); recent.className = 'steam-games__recent'; recent.textContent = '最近 ' + formatPlaytime(game.playtime_2weeks); playtime.appendChild(recent); } var total = document.createElement('span'); total.className = 'steam-games__total'; total.textContent = '总时长 ' + formatPlaytime(game.playtime_forever); playtime.appendChild(total); item.appendChild(cover); item.appendChild(info); item.appendChild(playtime); return item; } function renderState(list, message, isError) { var state = document.createElement('div'); state.className = 'steam-games__state'; if (isError) { state.setAttribute('role', 'alert'); } state.textContent = message; list.innerHTML = ''; list.appendChild(state); } function formatPlaytime(minutes) { var value = Number(minutes || 0); if (value <= 0) return '0 分钟'; var hours = Math.floor(value / 60); var mins = value % 60; if (hours <= 0) return mins + ' 分钟'; if (mins <= 0) return hours + ' 小时'; return hours + ' 小时 ' + mins + ' 分钟'; } function readCache(config) { try { var raw = localStorage.getItem(config.cacheKey); if (!raw) return null; var cache = JSON.parse(raw); if (!cache || Date.now() - cache.timestamp > config.cacheTtl) return null; return Array.isArray(cache.data) ? cache.data : null; } catch (_) { return null; } } function writeCache(config, data) { try { localStorage.setItem(config.cacheKey, JSON.stringify({ timestamp: Date.now(), data: data })); } catch (_) { // 隐私模式访问的话本地存储估计会失效 } } }()); 然后在 主题设置的 PJAX RELOAD里加一行initSteamGames();即可。6 未来可能的改进方向尚未做主题字体的自适应，主要是觉得这个组件跟宋体不搭；引入游戏名称汉化的API；点击跳转对应的游戏商城页面；添加加载动画，比如懒加载；游戏封面源换成更稳定的SteamDB；待补充。7 已发现的问题游戏库设为“私密”的游戏也会被展示。Demo已于2026.09.01修复该问题，但本页示例的代码未更新，最新版择日再端上来。
ChatGPT外区账号数据导出被要求绑定手机号的解决方案
最近转战更经济的土区ChatGPT plus，于是打算对美区的旧账号数据进行本地备份；而如今通过聊天页的 用户数据→控制→ 导出数据去请求导出的话，可能会被要求绑定手机号。 因为众所周知的原因，+86的号码在此是不可用的。如果不想再花钱去接码平台找一个外区临时手机号，可以尝试访问OpenAI的隐私中心OpenAI Privacy Portal，然后 提交隐私请求→下载我的数据。这个地方同样是可以导出数据的，而且不需要乱七八糟的验证。 请求成功后就可以在本页面时刻查看导出进度了。 同时OpenAI会给账号邮箱发送一封邮件： 导出成功后它们会再次通过邮件形式将数据包发送到邮箱里，耗费时间依数据大小而定，听说最长的可能要等上七天。其实还有一种导出方案是使用浏览器插件，目前我用过的一款叫Chatgpt Degrade Checker，不过他是对聊天一页一页去爬的，每次抓取到一百多页就会开始报错，个人估计是请求太频繁导致的。（而且它的的抓取范围不包括“项目”）::EOC:07::这个插件对聊天数据少的朋友应该有些帮助。
无月，夜空清澈；云少，细碎分布仅记录了肉眼可见的大型星座。记录时间主要为晚上八点到十点，所述可视情况仅限于笔者校内。图片都是手机直拍，夜间会自动使用长曝光算法，出片星点会显著比肉眼可见要多。手持拍摄，容易手抖，模糊是正常的。::Cat_gif:Pat-pat::天顶夏季大三角 原图简单后期，附上标签： 大三角大概在晚八点升至中天，由织女一-河鼓二-天津四三颗超级亮星构成。具体有多亮呢？县城里路灯下抬头可见。织女一，即织女星，是大三角中醒目的一颗，位于天琴座，星座内其余星点较暗淡，肉眼难以辨认。河鼓二，即牛郎星，是第二亮星，位于天鹰座，星座的同样不好辨认。天津四排老三，位于天鹅座，星座大致呈十字形。天鹅座是三个星座中难得容易勾勒出来的，大十字的形状在天顶十分显眼： 大三角刚好分布在银河周围。若沿着天鹅座的天津四向辇道增五作一条直线，我们便可依此确定银河的大致走向。二者接近于平行关系。由于学校里的波特尔光害已高达5.9，图片不作足够久的长曝光的话是很难看见银河的。于是可以简单作示意图： 用Stellarium模拟的同区域的夜空： 河鼓二和织女一两颗夏夜的亮星被分隔开来，在长河两岸遥遥相望——这就是我国古代劳动人民通过夜观天象，创造出的牛郎织女的传说。有诗云：迢迢牵牛星，皎皎河汉女。纤纤擢素手，札札弄机杼。终日不成章，泣涕零如雨。河汉清且浅，相去复几许。盈盈一水间，脉脉不得语。北天 N小熊座仅勾陈一（即现北极星）可见。仙后座北偏东方向，十分显眼的M字形： 行政楼上空的仙后座。你能从图中辨认出来吗？ 仙王座正北方向隐约可见，呈盾牌形。仙王与仙后同框： 东北英仙座拍摄时英仙座尚未完全升起，因此只拍到了局部： 西方的先民不仅勾勒出了夜空中的各式星座，还为它们创造出形形色色的传奇故事，赋予了这些星座云谲波诡的神秘色彩。英仙座也就是古希腊英雄人物珀尔修斯。传说中，埃塞俄比亚国王刻甫斯（仙王座）与王后卡西奥佩娅（仙后座）育有一女安德洛墨达（仙女座）。王后三不五时炫耀女儿的美貌，此举引来了波塞冬妻子的怨恨。于是波塞冬派出海怪刻托（鲸鱼座）颠覆埃塞俄比亚王国。惊骇之下，国王与王后遵从神谕指示，献祭安德洛墨达。此时宙斯之子珀耳修斯（英仙座）碰巧路过，力战海怪刻托，救下了被锁在礁石上的安德洛墨达。于是两人相爱，结为夫妻，此事传为一段佳话。后来宙斯将他们提升为星座，于是我们的夜空就中有了这些璀璨的形象。东天 E仙女座东偏北方向，较为清晰。 飞马座沿着仙女座抬头往上看就是飞马座。飞马座有一个标志性的四边形，由室宿一、二和壁宿一、二四颗2等亮星组成。危宿三是“马头”的位置，另外两条线代表“马腿”。 东南南鱼座仅鱼眼位置的北落师门可见，作为一颗1.15星等的亮星十分耀眼。南天 S人马座即古希腊神话中的半人马喀戎，是阿喀琉斯、伊阿宋、赫拉克勒斯等一众英雄的老师。观察时起云了，未拍摄记录。西南天蝎座天蝎座就在人马座旁边，拍摄时也有一层薄云经过。 天蝎座是夏秋季的标志性星座，巨大、明亮，地位堪比冬春季的猎户座。这里有个地狱笑话 (典故) 。波塞冬之子奥利安（猎户座）是地球上最英俊强大的巨人猎手，曾声称自己天下无敌，能杀死世上任何野兽。此举触怒了众神。为惩罚奥利安，大地女神盖亚派出一只巨大毒蝎，偷袭了奥利安的脚后跟（阿喀琉斯：这我熟）。剧毒作用下，这位不可一世的巨人最终不治身亡。于是我们可以看到，天蝎座从东边升起时，猎户座永远在西沉，仿若一场永无止境的追逐。也就是说，你永远不会看到这两个星座出现在同一片夜空中。天蝎座真的很亮。这个季节的夜间，它会在南面地平附近流转，而学校南边是海，海民夜间打着大灯事渔作业： 在这种情况下这个星座用肉眼依然清晰可辨。西天 W牧夫座仅大角星可见，九点半后已降到地平以下，未拍摄记录。小结？末尾不写点什么感觉很空::EOC:13::考虑到已经大四，估计这是留在学校的最后一个夏末，也是夏末最后一次在学校天台散心了。所以想着，都记录下来吧，总会有些意义的。
省流：当需要在Word中自定义样式时，对需应用于多层级标题的，建议直接在自带的“标题1”、“标题2”……基础上改动使用，可免去许多不必要的麻烦。 1事出有因最近在参悟研究学校的毕业论文排版规范。在图注方面，规定要求的是章号+圆点+图序（如“图1.1”）。为了实现图注编号自动化，就需要用到word的层级功能，具体操作就是要在 插入题注里启用并设置章节号，如此题注就能自动判断出自身的所在章节位置。 但是你在 章节起始样式翻找就会发现，这里只有Word自带的牢九门样式“标题1”~“标题9”，无法应用自建样式。 如果你在各级标题应用的都是自己煞费苦心创建的样式，那么恭喜，在这里无论怎么选，都会弹窗报错，然后在正文中也无法正常显示，原章号处会显示为 错误!文档中没有指定样式的文字。字样： 非常幸运 (怨种) 的是，在此之前，我已经完成了各级样式的定义，且都一一作了绑定： 从上图可以看出，在定义多级列表的窗口里，是支持将各级别链接到自定义样式的。但如前文所述，在题注窗口里，你只能从Word自带的默认样式里进行选择。这就造成了一种事实上的矛盾。2寻求解法在网上一番搜索，经过不懈努力，终于得出结论……该问题无解::heybox:07::只能说微软就是这么设计的……我个人倾向于认为这是个bug，还是老早就存在且到目前为止也没修复的那种::Cat_gif:Big_hammer::2.1编辑域这里讲一个不治本但治标的方法。仅可用于应急。快捷键 Alt+F9，显示文档中的域代码，选中“章号”部分的域代码，右键点选 编辑域： 在弹出窗口的域属性中就能找到自创建的样式，选中并确认。 再次Alt+F9还原，现在图注就可以正常显示了。 只不过，这种改动是一次性的，依然不能改变下一次插入题注时因无法绑定到自创建样式导致图注照样报错的痼疾。其他报错的地方也只能像这样一个个地改。2.2启用默认样式正确的做法是，对于层级标题，直接在自带样式（即命名为“标题1，2，3……”的样式）的基础上定义格式。 当然别忘了多级列表里也要逐一做好绑定。 3结论层级标题的样式老老实实用自带的标题样式改，不要创建新样式。其他正文样式可随意。又是被微软摁着头吃史的一天
五一国际劳动节快乐！……月末发点存货::EOC:36::广工揭的冬季大六边形iPhone|摄于3月19日学校的夜空能见度一直不错，山卡拉有山卡拉的好处。 天狼星-参宿四-南河三，天狼星-参宿七-毕宿五-五车二-北河三-南河三。即使是早春的夜里，也能在中天被望见，东升西落，横亘半壁星野。我们不睡，它们不睡。 猎户巡夜iPhone|摄于3月19日陪伴了我一整个冬夜的大星座。道声早别。 北斗上中天iPhone|摄于3月19日北斗七星，要论亲眼所见，这次可能真的是第一次。这个星象占了天球相当辽阔的面积，在学校的夜里仰头就能望见，高悬近中天，美得不可方物。我很少用洋洋大观来形容一个东西，北斗星绝对实至名归。 马卡良星链 Markarian's ChainDwarf3|摄于3月23日就是传说中那个拍得好叫马卡良拍不好叫马卡劣的玩意积累曝光14880s。第一次处理星系，后期略显乏力。得益于相当可观的曝光时间，出片效果也还可以啦。 “星空项链？”马卡良星系链，是室女座星系团内的星系群，包含于其中的诸多星系共同构成了一条完美的弧线，因此得名星系链。除了图上标注的八个大星系外，星系链旁边还有很多小星系。这里的星系，每一个都像银河系一样包含亿数以万计的恒星；而它们的所在也不过是宇宙中一个微不足道的小角落--全宇宙还有数以万亿,计的星系，很难不让人觉得，宇宙很可能热闹得要命。 风车星系 M101Dwarf3|摄于3月24日 也是简单后期，而且按审美动了下配色...满打满算积累有八个小时，所以即使目标很小，细节也还不错::Cat_gif:Be_wiped:: 最后的碎碎念 所幸三月份能抽不少时间去记录上述一二，因为紧接而来的是大量比赛和工作的四月。这也是为什么4月能发得出手的全都是3月的东西然后上个月开始打的正大杯，今天也出成绩了。昨天群里才有同学说，省一其实就是国三。上了省一没上国三，是最倒霉的情况。然后……请看VCR： 孩子们这不好笑.jpg这个故事告诉我们千万不要乱奶，特别是对着自己人（误） 赛前预期，最不济也有个国三，果然希望越大失望越大哈哈。五月份对我好点吧。
一些粗浅的想法，权当博主看《家庭、私有制和国家的起源》的读后感。两性议题无疑是当今社会现象中冲突频发的一环。欲究根本，我们上溯其滥觞，回到人类社会家庭将将形成的时期，不难在母系社会与其孕育父权制的历史中找到答案。恩氏在《家庭、私有制和国家的起源》中指出，历史上的第一个阶级对立，恰恰是同男性对女性在一夫一妻制婚姻中的对立一起发展起来的；而第一个阶级压迫，正是由于女性被男性所奴役而产生的。父权制与私有制相伴而生。在原始公社制度下，母系社会曾主导社会组织，女性在氏族共同体中占据举足轻重的经济社会地位。随着生产力的提高和畜牧业、农业的发展，生产剩余开始出现，个体占有财富的可能性增强，家庭成为私有财产继承的单位，男性对财富的控制权节节攀升。恩格斯作出如此评价：“母权制的废除是女性历史上一次世界性的失败。”这一历史转捩点，意味着女性逐渐被排除在公共生产之外，成为家庭内部的附属品，婚姻制度也从母系社会的相对自由走向一夫一妻制的强制规范，而这一切，正是私有制发展的结果。可见，性别矛盾的根源，直指阶级矛盾。言及所谓父权制，up主懒澡的杰基总结，“通过对历史上父权制的考察，我们能发现它有一个鲜明的内涵，即所谓的父权制，就是指年长男性对女性和年轻男性的统治。”父权制在压迫女性的同时，也以不同方式剥削年轻男性，构建了一整套基于权力等级的社会结构。这一体系下，本应压迫而应团结在一起的底层男女的家庭，在男性主导生产劳动（养家糊口）与女性主导劳动再生产（料理家庭）的普遍构成中，常常表现为一个家庭内男性所受社会压迫对女性的转嫁。但随着工业化发展，生产率的提高，洗衣机、电冰箱等工业品走入家庭，解放了家庭妇女的双手，越来越多的女性走出家庭，投入社会化大生产中，女性群体的发声似乎开始有了一席之地。从变化中我们不难窥见，同经济基础已然不再相适应的父权制，开始松动了。在后资本主义时代，在社会中的个人日趋被原子化的今天，将一夫一妻的家庭锁定在最基础的生产单位的资本主义，在剥削的持续加压下，无疑会成为彻底挤碎传统家庭关系的倍增器。事物总是不可避免地走向反面，促成这一生产单位巩固起来的推力，也将不可避免地造成家庭的瓦解。只有当家庭启动消亡的进程，当女性逐渐摆脱“主内”的标签，挣脱作为男性客体化个体与纯粹的劳动力再生产工具 (生育工具) 的枷锁，在社会生产中重新成为经济独立与社会独立的双重主体，女性才能在事实意义上走向“解放”，使得占绝大部分人口的两性劳动者走向真正的和解，归于统一战线。在阶级社会，“谁是我们的敌人，谁是我们的朋友，”是性同伐异还是团结起来消灭压迫根源？教员这句话的含金量还在提升。私有制的消灭将孕育事实上的女性解放。从平权斗争到妇女解放，任重而道远。
“……读书人的事，能算偷么？”::Cat_gif:Spill_the_tea::众所周知B站上有很多很好看的装扮素材::Cat_gif:Big_hammer::但是买了只能在B站上用难免有些浪费（）如果能全部扒下来，应用到博客或者别的什么地方上...多是一件美事啊准备工具ProxyPin是一个全平台的开源抓包工具，UI美观，简单易用，重点是不要钱。对于移动端的ProxyPin，首次使用前须安装CA证书。进入软件，切换到 👤我的标签页，点击 HTTPS代理->安装根证书，根据提示进行操作即可。比利比利 (哔哩哔哩) ♂APP上有直接的装扮中心入口，因而本次主要在移动端上进行操作。以“点赞效果”为例首先打开APP，藉由 我的->推荐服务：个性装扮->右上角头像进入 我的装扮。 运行ProxyPin，点击主页右下方的 ▶图标，开始抓包。切换回b站app，进入点赞效果页面，确保目标点赞动画加载完成后，回到ProxyPin，点击右下角 ⏹停止抓包。以关键字“thumbup”进行过滤，找方法为GET、格式为json的包，在响应（Response）一栏下拉，很容易找到对应装扮名称的三个点赞动画的文件链接，从上到下分别指向完整动图、不带点赞大拇指的动图以及静态预览图。 点赞动画文件为svga格式。将两个bin文件后缀改为 .svga，上传到Svga Tools即可预览。 两份svga一份有大拇哥另一份没有其他素材，基本上都是 .webp或 .png格式，获取方法大同小异。特殊地，对于进度条动画，其基于Lottie Animation实现，需要通过Lottie的编辑工具进行预览。以同样的抓包方式找到后缀为 .json的文件（关键字“dash_icon”），将文件中所有代码粘贴到JSON Editor - Lottie Docs的编辑框里，就能播放啦。 克喵鲁.jpg嘛，偷出来的素材用来干什么都好（不可以拿来卖钱啦）比如，你可以像我一样，用来装点博客！ 如此一来，一篇拼好文就顺利水完了，甚是斯巴拉西。喜欢这个抓包工具别忘了去项目地址点个star哦。本文部分方法论指导来源
基于Siril等图像处理软件的玫瑰星云（NGC2237）后期处理
这两天前些天试着拍了校准场，再自己叠图，发现效果比直接拿机内自动叠的图去修要好得多得多得多得多得多……故留此实战笔记。一、预处理1.1 素材整理与筛选整理19日晚实战素材（目标为NGC2237玫瑰星云）：场张数快门增益滤镜亮场229120s60双窄带暗场120120s60双窄带偏置场2300.0001s60双窄带平场////由于后面相机断过电，遂未能补拍平场...::Cat_gif:Stealth_mode::（经评论区autobot同学提醒，下所用脚本并不包含对偏置场的处理，因此实际出片仅为亮暗场叠加的结果）1.2 素材叠加 使用叠图脚本前务必为项目准备一个较为宽裕的硬盘空间，100G以上为宜。 大量素材的叠加可以显著提高最终出图的信噪比。这里使用Siril官方的预处理（Pre-processing）脚本进行自动叠图：打开 Siril，点击左上角的蓝色小房子图标，设定工作目录。在工作目录内，分别创建Lights（亮场）、Darks（暗场）、Biases（偏置场）三个新文件夹，将素材文件分别置入对应的文件夹中。 选择 脚本->OSC_Preprocessing_WithoutFlat（彩色相机_预处理_无平场），然后软件就会自动跑起来。 在运行过程中，软件生成的所有处理文件会被放置在一个名为 process的文件夹内。如果中途出现报错，软件会自动停下来。检查报错信息，在排查并解决问题后，删除 process文件夹，再重新运行脚本。脚本顺利运行完毕，则如下图所示，1分钟不到就跑完了： 此时会在工作目录生成一个名为 result_（总积累时间）.fit的最终预处理文件。 随后把多余的 process文件夹（拢共吃了我80多个G）删除就好。二、后期处理2.1 拉伸与裁切用 Siril 打开叠好的图片（.fits 格式）。按上文处理好的.fit文件，在 线性模式下，一般只能看到些许星点： 在软件右下方找到显示方式，由 线性改为 自动拉伸： 此时就能较为清晰地看到图像了::Cat_gif:Pat-pat:: 注意到，在四周存在一些叠加不全因而割裂的边缘。软件内可单击框选图片，右键进行适当裁切即可。 2.2 背景提取点击 图像处理 -> 背景提取，在弹出对话框中选择生成。调整生成的样本（左键添加，右键删除），使得样本覆盖星云主体以外的所有部分。 确定后点击计算背景，确定没问题后点击应用。 可见处理后的图片亮度均匀了许多，星云部分也更加突出。2.3 色彩校准点击 图像处理 -> 色彩校准 -> 测光法色彩校准，搜索目标，如本笔记中为玫瑰星云NGC2237（或C50）。选中输出结果后点击 从图像中获取元数据，再点击 好，待软件处理完后点击关闭。 2.4 除去绿色噪声彩色相机出图易出现发绿的情况，此时需要除去绿色噪声。点击 图像处理 -> 除去绿色噪声 ，设置默认（如图）,点击 应用即可。 2.5 反双曲正弦变换点击 图像处理 -> 反双曲线变换。适当加大拉伸因子可使星云更明显。尽量不做太过暴力的因子拉伸。 此时，将显示方式重新改回 线性： 2.6 星点移除点击 图像处理 -> 星点加工 ->StarNet星点移除，在弹出对话框中勾选 预拉伸线性图像，应用后等待软件自动执行完毕即可。处理完毕后预览框中为星云主体图，并能在工作目录中找到新生成的两个已分离文件，分别为 去星图（starless） 与 星点图（前缀 starmask） ： 2.7 Starless图：直方图变换点击 图像处理 -> 直方图变换。在弹出对话框中点击齿轮图标（自动拉伸），应用即可。 找到右上角的下载按钮，转存图片为 Tiff 格式，采样格式选择 32 位浮点数。2.8 Starless图：AI降噪运行 opaz DeNoise AI，拖入方才转存的.tif 图片，选择一种最优的降噪算法。一般而言，低光（Low Light）和高噪点（Severe Noise）在这类场景中处理得最好。找到满意的算法后，点击 保存图像。 2.9 Starless图：调色使用 Photoshop 打开降噪完成的图片，选择 滤镜 ->Camera Raw滤镜。调色部分比较凭感觉，调色方法因需要达到的效果而异，没有放诸四海而皆准的方案。每个选项都试一试，凭自己的审美去调，达到突出主体、压暗背景的目的即可。 处理完成后还是按照.tif 格式保存图片。将调好色的图片拖入 Siril 中，转存为.fit 格式，采样格式选择 32 位浮点数。将星点图拖入 Siril 中，检查图片，如果对图片不满意可以重复上述处理去星图的步骤对图片进行处理，此处不再赘述。2.10 星图合成点击 图像处理 -> 星点加工 -> 星点重绘，在弹出的对话框中，左边选择去星图，右边选择星点图（均为.fit格式）。可以适当地使用拉伸因子提高星云与星点的能见度。对于星点，可拉低高光保护点，以在拉伸星点时一定程度上遏制星点肥大的问题。 二者分别 应用 后关闭对话框，保存图片为.tif 格式，采样格式选择 16 位无符号整数。至此，本次深空摄影的后期处理全部完成。如若还需进一步优化，在 Photoshop 中处理最后的.tif 图片文件即可，不赘述。三、最终成品 追加：SHO伪哈勃色调色方案其实还做了个调哈勃色的笔记（效果是下图酱紫），看情况再补充上来吧，累鼠了::Cat_gif:Out_like_a_light:: 这篇笔记上周就已经躺在草稿里了，结果摸到现在才动笔。本着不能留着过年的原则，终于是写完了。一开始的想法是，咱一次多写几篇东西，每隔段时间放一篇出来，才能实现可持续性摸鱼。结果几篇简单的提前摸完，这篇最早的现在才写好...好嘛，现在是真的快过年了。那就在这里提前祝路过的朋友新年快乐叭::EOC:20::
CSS+SVG实现一个不简洁但美观且带遮罩和红色心情❤️的网站运行时间样式
通过简单的CSS+SVG实现，效果请见本站页脚::Cat_gif:Be_wiped::实现代码把下面两个代码块塞到合适的地方即可。 适配移动端尺寸在CSS样式中补充： /* 移动端适配 */ @media (max-width: 768px) { .time-container { padding: 6px 15px; /* 更紧凑 */ gap: 6px; } .time-text { font-size: 0.8rem; /* 更小字号 */ } .heartbeat-icon { width: 18px; /* 更小心（） */ height: 18px; } .time-number { font-size: 0.85rem; /* 数字单独调整 */ } }适配Mirages主题的夜间模式如果你在使用同款主题，可以继续在CSS样式中补充： /* 夜间模式 */ body.theme-dark .time-container { background: rgba(255,255,255,0.15); box-shadow: 0 2px 12px rgba(0,0,0,0.3); } body.theme-dark .time-text { color: #f0f0f0; text-shadow: 0 1px 2px rgba(0,0,0,0.2); } body.theme-dark .time-number { color: #ff7777; text-shadow: 0 2px 4px rgba(255,120,120,0.3); } body.theme-dark .heartbeat-icon svg { fill: #ff5555; filter: drop-shadow(0 0 6px rgba(255,80,80,0.4)); } /* 添加过渡动画 */ .time-container, .time-text, .time-number, .heartbeat-icon svg { transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); }如果是其他博客主题，代码可能不完全适用，请具体情况具体分析。其他调整默认样式其实有点大不啦叽，适合我这种内容空空的页脚如果你喜欢小小的也很可爱，可以从以下CSS样式着手调整：1.调整遮罩尺寸 /* 修改前 */ .time-container { padding: 10px 25px; border-radius: 30px; margin: 15px 0; } /* 修改后 */ .time-container { padding: 8px 20px; /* 减少内边距 */ border-radius: 20px; /* 更圆润的弧度 */ margin: 10px 0; /* 减少外边距 */ }2.调整文字大小 /* 修改前 */ .time-text { font-size: 1.1rem; } /* 修改后 */ .time-text { font-size: 0.9rem; /* 缩小文字 */ }3.调整心跳icon尺寸 /* 修改前 */ .heartbeat-icon { width: 24px; height: 24px; } /* 修改后 */ .heartbeat-icon { width: 20px; /* 缩小图标 */ height: 20px; margin-left: 4px; /* 调整间距 */ }以上是几个栗子，建议依照自己的审美，动动小手自己试试哈。最后这里，可能还有聪明的吴彦祖要问：主包主包，你的炽热心跳❤️确实很好看，但是太单调了，有没有更加美观又实用的样式推荐一下嘛？有的，兄弟有的： /* 修改前 */ .heartbeat-icon svg { width: 100%; height: 100%; fill: #e74c3c; } /* 修改后 */ .heartbeat-icon svg { width: 100%; height: 100%; fill: #c00085; /* 改成紫色 */ }类似的，对于夜间模式适配部分： /* 修改前 */ body.theme-dark .heartbeat-icon svg { fill: #ff5555; filter: drop-shadow(0 0 6px rgba(255,80,80,0.4)); } /* 修改后 */ body.theme-dark .heartbeat-icon svg { fill: #d50096; /* 改成更紫的紫色 */ filter: drop-shadow(0 0 6px rgba(255,80,80,0.4)); }对他使用紫色心情🩷吧。
超可爱的猫老师微信（首套！）表情包 & Mirages主题表情包一键部署
非常好猫老师，使我的表情旋转::Cat_gif:Zoomies::近几天EverydayOneCat在微信平台上新了一组表情包::EOC:04::看到之后我啪的一下很快啊，就全部下载下来了！::Cat_gif:Shake-shake::已经等不及了，现在就端上来罢！::EOC:16::表情一览::Cat_gif:Let_me::::Cat_gif:Spill_the_tea::::Cat_gif:Be_wiped::::Cat_gif:Shake-shake::::Cat_gif:Zoomies::::Cat_gif:Oh::::Cat_gif:Hii::::Cat_gif:Big_hammer::::Cat_gif:Stealth_mode::::Cat_gif:Yoho::::Cat_gif:Hibye::::Cat_gif:Out_like_a_light::::Cat_gif:Paw-paw::::Cat_gif:Poke-poke::::Cat_gif:Pat-pat::::Cat_gif:Nom-nom::如上一共16个GIF动态表情，这个猫老师就是赞啦。获取表情::EOC:24::在这里领养 (xià zǎi) ：下崽地址key: catcat16个表情打包，解压即食。压缩包内的 package.json为Mirages主题表情配置文件，无需部署则可自行删除。如何在Mirages主题中部署新猫包如果你是7.10.3 及以上版本：将下载好的 Cat_gif.zip上传至 主题目录/usr/biaoqing中解压即可。我已经将 package.json的内容全部配置好了。对命名等不满意可以自己再改一下::Cat_gif:Let_me::如果你是更早期的版本：建议升级为最新版本...然后再执行上文步骤更多说明请参见主题官方帮助文档。效果测试：详见评论区 新猫包现已加入本博客表情豪华套餐！请在评论区中自由测试::Cat_gif:Yoho:: 支持表情包作者 你问我资瓷不资瓷，我当然是资瓷的啦B站关注猫老师不迷路：EverydayOneCat的个人主页同时，你也可以在微信平台搜索Everyday One Cat：关注猫老师的公众号，将微信表情收入囊中，并赞赏作者！ 赞赏我点开是黑屏，你们有什么头绪吗 更多猫老师表情包