修复 CI 部署:使用原生 ssh 替代 appleboy action

This commit is contained in:
2569718930@qq.com
2026-05-19 14:22:43 +08:00
parent cd9de0926b
commit 9db37c6d7b
2 changed files with 102 additions and 82 deletions
+6 -6
View File
@@ -69,14 +69,14 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Deploy to VPS - name: Deploy to VPS
uses: appleboy/ssh-action@v1 run: |
with: mkdir -p ~/.ssh
host: ${{ secrets.VPS_HOST }} echo "${{ secrets.VPS_SSH_KEY }}" > ~/.ssh/id_rsa
username: ${{ secrets.VPS_USER }} chmod 600 ~/.ssh/id_rsa
key: ${{ secrets.VPS_SSH_KEY }} ssh -o StrictHostKeyChecking=accept-new ${{ secrets.VPS_USER }}@${{ secrets.VPS_HOST }} "
script: |
cd /root/PolyWeather cd /root/PolyWeather
git pull origin main git pull origin main
docker compose up -d --build docker compose up -d --build
sleep 10 sleep 10
curl -s http://localhost:8000/healthz curl -s http://localhost:8000/healthz
"
@@ -3,13 +3,7 @@
import clsx from "clsx"; import clsx from "clsx";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { RefreshCw } from "lucide-react"; import { RefreshCw } from "lucide-react";
import { import { useCallback, useEffect, useMemo, useRef, useState } from "react";
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import styles from "./Dashboard.module.css"; import styles from "./Dashboard.module.css";
import { scanRootClass } from "./scan-root-styles"; import { scanRootClass } from "./scan-root-styles";
import { import {
@@ -18,9 +12,7 @@ import {
useProAccess, useProAccess,
} from "@/hooks/useDashboardStore"; } from "@/hooks/useDashboardStore";
import { I18nProvider, useI18n } from "@/hooks/useI18n"; import { I18nProvider, useI18n } from "@/hooks/useI18n";
import type { import type { ScanOpportunityRow } from "@/lib/dashboard-types";
ScanOpportunityRow,
} from "@/lib/dashboard-types";
import { AiPinnedForecastView } from "@/components/dashboard/scan-terminal/AiPinnedForecastView"; import { AiPinnedForecastView } from "@/components/dashboard/scan-terminal/AiPinnedForecastView";
import { MobileCityPicker } from "@/components/dashboard/scan-terminal/MobileCityPicker"; import { MobileCityPicker } from "@/components/dashboard/scan-terminal/MobileCityPicker";
import { WelcomeOverlay } from "@/components/dashboard/scan-terminal/WelcomeOverlay"; import { WelcomeOverlay } from "@/components/dashboard/scan-terminal/WelcomeOverlay";
@@ -78,18 +70,16 @@ function ScanTerminalScreen() {
const accountHref = proAccess.authenticated const accountHref = proAccess.authenticated
? "/account" ? "/account"
: "/auth/login?next=%2Faccount"; : "/auth/login?next=%2Faccount";
const { const { refreshScanTerminalManually, scanError, scanLoading, terminalData } =
refreshScanTerminalManually, useScanTerminalQuery({
scanError, isPro,
scanLoading, proAccessLoading: proAccess.loading,
terminalData, });
} = useScanTerminalQuery({
isPro,
proAccessLoading: proAccess.loading,
});
const [selectedRowId, setSelectedRowId] = useState<string | null>(null); const [selectedRowId, setSelectedRowId] = useState<string | null>(null);
const [activeView, setActiveView] = useState<ScanTerminalContentView>("map"); const [activeView, setActiveView] = useState<ScanTerminalContentView>("map");
const [mapSelectedCityName, setMapSelectedCityName] = useState<string | null>(null); const [mapSelectedCityName, setMapSelectedCityName] = useState<string | null>(
null,
);
const [showScanPaywall, setShowScanPaywall] = useState(false); const [showScanPaywall, setShowScanPaywall] = useState(false);
const [isMobileViewport, setIsMobileViewport] = useState(false); const [isMobileViewport, setIsMobileViewport] = useState(false);
@@ -185,12 +175,17 @@ function ScanTerminalScreen() {
}); });
const selectedRow = useMemo(() => { const selectedRow = useMemo(() => {
if (!timeSortedRows.length) return null; if (!timeSortedRows.length) return null;
return timeSortedRows.find((row) => row.id === selectedRowId) || timeSortedRows[0] || null; return (
timeSortedRows.find((row) => row.id === selectedRowId) ||
timeSortedRows[0] ||
null
);
}, [timeSortedRows, selectedRowId]); }, [timeSortedRows, selectedRowId]);
useEffect(() => { useEffect(() => {
if (!timeSortedRows.length) return; if (!timeSortedRows.length) return;
if (selectedRowId && timeSortedRows.some((row) => row.id === selectedRowId)) return; if (selectedRowId && timeSortedRows.some((row) => row.id === selectedRowId))
return;
setSelectedRowId(timeSortedRows[0].id); setSelectedRowId(timeSortedRows[0].id);
}, [selectedRowId, timeSortedRows]); }, [selectedRowId, timeSortedRows]);
@@ -205,7 +200,8 @@ function ScanTerminalScreen() {
const cityKey = normalizeCityKey(rawCityName); const cityKey = normalizeCityKey(rawCityName);
if (!cityKey || mapFocusedRow) return null; if (!cityKey || mapFocusedRow) return null;
const selectedDetail = const selectedDetail =
store.selectedDetail && normalizeCityKey(store.selectedDetail.name) === cityKey store.selectedDetail &&
normalizeCityKey(store.selectedDetail.name) === cityKey
? store.selectedDetail ? store.selectedDetail
: Object.values(store.cityDetailsByName).find( : Object.values(store.cityDetailsByName).find(
(detail) => normalizeCityKey(detail?.name) === cityKey, (detail) => normalizeCityKey(detail?.name) === cityKey,
@@ -246,7 +242,8 @@ function ScanTerminalScreen() {
display_name: displayName, display_name: displayName,
selected_date: selectedDetail?.local_date || null, selected_date: selectedDetail?.local_date || null,
local_date: selectedDetail?.local_date || null, local_date: selectedDetail?.local_date || null,
local_time: selectedDetail?.local_time || selectedSummary?.local_time || null, local_time:
selectedDetail?.local_time || selectedSummary?.local_time || null,
temp_symbol: tempSymbol, temp_symbol: tempSymbol,
current_temp: currentTemp, current_temp: currentTemp,
current_max_so_far: current_max_so_far:
@@ -309,28 +306,32 @@ function ScanTerminalScreen() {
"多模型高温预测", "多模型高温预测",
"实时观测偏差", "实时观测偏差",
"未来日期城市决策卡", "未来日期城市决策卡",
"Telegram 群内价 5U",
]; ];
useEffect(() => { useEffect(() => {
if (!activeDetailRow) return; if (!activeDetailRow) return;
if (!findDetailForCity(store.cityDetailsByName, activeDetailRow.city)) { if (!findDetailForCity(store.cityDetailsByName, activeDetailRow.city)) {
void store.ensureCityDetail(activeDetailRow.city, false, "panel").catch(() => {}); void store
.ensureCityDetail(activeDetailRow.city, false, "panel")
.catch(() => {});
} }
}, [activeDetailRow, store.cityDetailsByName, store.ensureCityDetail]); }, [activeDetailRow, store.cityDetailsByName, store.ensureCityDetail]);
const handleMapCitySelect = useCallback((cityName: string) => { const handleMapCitySelect = useCallback(
setMapSelectedCityName(cityName); (cityName: string) => {
lastMapSelectedCityRef.current = normalizeCityKey(cityName); setMapSelectedCityName(cityName);
const matchedRow = findRowForCity(timeSortedRows, cityName); lastMapSelectedCityRef.current = normalizeCityKey(cityName);
if (matchedRow) { const matchedRow = findRowForCity(timeSortedRows, cityName);
store.preloadCityFromRow(matchedRow); if (matchedRow) {
setSelectedRowId(matchedRow.id); store.preloadCityFromRow(matchedRow);
} else { setSelectedRowId(matchedRow.id);
void store.ensureCityDetail(cityName, false, "panel").catch(() => {}); } else {
setSelectedRowId(null); void store.ensureCityDetail(cityName, false, "panel").catch(() => {});
} setSelectedRowId(null);
}, [store, timeSortedRows]); }
},
[store, timeSortedRows],
);
useEffect(() => { useEffect(() => {
if (activeView !== "map") return; if (activeView !== "map") return;
@@ -344,36 +345,44 @@ function ScanTerminalScreen() {
addAiPinnedCity(selectedCity); addAiPinnedCity(selectedCity);
}, [activeView, addAiPinnedCity, store.selectedCity, timeSortedRows]); }, [activeView, addAiPinnedCity, store.selectedCity, timeSortedRows]);
const handleSelectRow = useCallback((row: ScanOpportunityRow) => { const handleSelectRow = useCallback(
const cityName = row.city || row.city_display_name || row.display_name || ""; (row: ScanOpportunityRow) => {
if (!cityName) return; const cityName =
setSelectedRowId(row.id); row.city || row.city_display_name || row.display_name || "";
store.preloadCityFromRow(row); if (!cityName) return;
const selectedCityKey = normalizeCityKey(store.selectedCity); setSelectedRowId(row.id);
const rowCityKey = normalizeCityKey(cityName); store.preloadCityFromRow(row);
const hasCachedDetail = const selectedCityKey = normalizeCityKey(store.selectedCity);
Boolean(findDetailForCity(store.cityDetailsByName, cityName)) || const rowCityKey = normalizeCityKey(cityName);
Object.values(store.cityDetailsByName).some((detail) => const hasCachedDetail =
rowMatchesCity(row, detail?.name || detail?.display_name || ""), Boolean(findDetailForCity(store.cityDetailsByName, cityName)) ||
); Object.values(store.cityDetailsByName).some((detail) =>
if (store.isPanelOpen && selectedCityKey === rowCityKey) { rowMatchesCity(row, detail?.name || detail?.display_name || ""),
if (!hasCachedDetail) { );
void store.ensureCityDetail(cityName, false, "panel").catch(() => {}); if (store.isPanelOpen && selectedCityKey === rowCityKey) {
if (!hasCachedDetail) {
void store.ensureCityDetail(cityName, false, "panel").catch(() => {});
}
return;
} }
return; void store.selectCity(cityName);
} },
void store.selectCity(cityName); [store],
}, [store]); );
const handleOpenDecisionRow = useCallback((row: ScanOpportunityRow) => { const handleOpenDecisionRow = useCallback(
const cityName = row.city || row.city_display_name || row.display_name || ""; (row: ScanOpportunityRow) => {
if (!cityName) return; const cityName =
setSelectedRowId(row.id); row.city || row.city_display_name || row.display_name || "";
store.preloadCityFromRow(row); if (!cityName) return;
addAiPinnedCity(cityName); setSelectedRowId(row.id);
setActiveView("analysis"); store.preloadCityFromRow(row);
void store.selectCity(cityName); addAiPinnedCity(cityName);
}, [addAiPinnedCity, store]); setActiveView("analysis");
void store.selectCity(cityName);
},
[addAiPinnedCity, store],
);
const openScanPaywall = useCallback(() => { const openScanPaywall = useCallback(() => {
setShowScanPaywall(true); setShowScanPaywall(true);
@@ -430,7 +439,6 @@ function ScanTerminalScreen() {
); );
} }
return ( return (
<div className={scanTerminalRootClassName}> <div className={scanTerminalRootClassName}>
<div <div
@@ -498,7 +506,11 @@ function ScanTerminalScreen() {
<section className="scan-list-section"> <section className="scan-list-section">
<div className="scan-list-header"> <div className="scan-list-header">
<div className="scan-list-tabs" role="tablist" aria-label={isEn ? "Content view" : "内容视图"}> <div
className="scan-list-tabs"
role="tablist"
aria-label={isEn ? "Content view" : "内容视图"}
>
{isMobileViewport ? ( {isMobileViewport ? (
<button <button
type="button" type="button"
@@ -518,7 +530,9 @@ function ScanTerminalScreen() {
aria-selected={resolvedView === "map"} aria-selected={resolvedView === "map"}
className={resolvedView === "map" ? "active" : ""} className={resolvedView === "map" ? "active" : ""}
onClick={() => { onClick={() => {
lastMapSelectedCityRef.current = normalizeCityKey(store.selectedCity); lastMapSelectedCityRef.current = normalizeCityKey(
store.selectedCity,
);
setActiveView("map"); setActiveView("map");
}} }}
> >
@@ -541,13 +555,19 @@ function ScanTerminalScreen() {
</div> </div>
<div className="scan-list-status"> <div className="scan-list-status">
{terminalData?.generated_at ? ( {terminalData?.generated_at ? (
<span className={clsx("scan-status-chip", terminalData?.stale ? "stale" : "live")}> <span
className={clsx(
"scan-status-chip",
terminalData?.stale ? "stale" : "live",
)}
>
{isEn ? "Updated" : "已更新"} {serverAgeText || ""} {isEn ? "Updated" : "已更新"} {serverAgeText || ""}
</span> </span>
) : null} ) : null}
{terminalData?.stale && localAgeText ? ( {terminalData?.stale && localAgeText ? (
<span className="scan-status-chip stale"> <span className="scan-status-chip stale">
{isEn ? "Local fetch " : "本地下发 "}{localAgeText} {isEn ? "Local fetch " : "本地下发 "}
{localAgeText}
</span> </span>
) : null} ) : null}
{isPro ? ( {isPro ? (
@@ -557,12 +577,13 @@ function ScanTerminalScreen() {
onClick={refreshScanTerminalManually} onClick={refreshScanTerminalManually}
disabled={scanLoading} disabled={scanLoading}
title={ title={
isEn isEn ? "Force refresh decision cards" : "强制刷新决策卡"
? "Force refresh decision cards"
: "强制刷新决策卡"
} }
> >
<RefreshCw size={14} className={scanLoading ? "spin" : undefined} /> <RefreshCw
size={14}
className={scanLoading ? "spin" : undefined}
/>
{isEn ? "Refresh" : "刷新"} {isEn ? "Refresh" : "刷新"}
</button> </button>
) : null} ) : null}
@@ -613,4 +634,3 @@ export function ScanTerminalDashboard() {
</I18nProvider> </I18nProvider>
); );
} }