Stabilize the decision workspace data boundaries

The scan terminal had grown into overlapping CSS, request-state, AI-provider, and city-card data responsibilities. This refactor separates those boundaries without changing product behavior: CSS modules are split by surface, city AI prompt/provider/fallback logic is isolated, and scan terminal request state now has reusable RemoteData adapters plus business-state tests.

Constraint: Preserve existing global scan-terminal class names and API responses during the refactor

Constraint: No new dependencies; keep this as a file-boundary cleanup

Rejected: Introduce React Query now | higher migration risk than the requested lightweight query-client path

Rejected: Rewrite AI stream behavior | progressive/fallback states are product-sensitive and were only adapter-split

Confidence: high

Scope-risk: moderate

Reversibility: clean

Directive: Keep AI stream state changes covered by business snapshots before changing fallback/cache wording

Tested: npm run test:business; npx tsc --noEmit; npm run build; python pytest -q; ruff check; py_compile targeted city AI modules

Not-tested: Live DeepSeek provider network replay and browser visual QA
This commit is contained in:
2569718930@qq.com
2026-04-28 14:45:34 +08:00
parent 12d90c5051
commit b122e7cbae
23 changed files with 5699 additions and 4940 deletions
@@ -29,6 +29,11 @@ export type AiCityStreamProgress = {
raw_length?: number | null;
};
export const scanTerminalQueryPolicy = {
autoRefreshMs: 10 * 60_000,
manualForceRefreshCooldownMs: 2 * 60_000,
} as const;
type AiCityStreamEvent = {
data: Record<string, unknown>;
event: string;
@@ -100,6 +105,32 @@ export function toRemoteError<T>(
};
}
export function shouldSkipManualTerminalRefresh({
hasCurrentData,
lastForcedRefreshAt,
now = Date.now(),
}: {
hasCurrentData: boolean;
lastForcedRefreshAt: number;
now?: number;
}) {
return (
hasCurrentData &&
lastForcedRefreshAt > 0 &&
now - lastForcedRefreshAt < scanTerminalQueryPolicy.manualForceRefreshCooldownMs
);
}
export function shouldRunAutoTerminalRefresh({
documentHidden,
isLoading,
}: {
documentHidden: boolean;
isLoading: boolean;
}) {
return !documentHidden && !isLoading;
}
async function readJsonOrThrow<T>(path: string, init?: RequestInit): Promise<T> {
const response = await fetchBackendApi(path, init);
if (response.ok) return response.json() as Promise<T>;