Files
FlowPilot/sidepanel/contribution-content-update-service.js
T
2026-05-19 13:29:18 +00:00

187 lines
6.0 KiB
JavaScript

(() => {
const PORTAL_BASE_URL = 'https://flowpilot.qlhazycoder.top';
const CONTENT_SUMMARY_API_URL = `${PORTAL_BASE_URL}/api/content-summary`;
const CACHE_KEY_PREFIX = 'multipage-contribution-content-summary-v2';
const FETCH_TIMEOUT_MS = 6000;
function normalizeScopeId(value = '', fallback = '') {
return String(value || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '-') || fallback;
}
function normalizeScope(options = {}) {
const flowId = normalizeScopeId(options.flowId || options.flow || options.activeFlowId, 'openai');
const targetFallback = flowId === 'kiro' ? 'kiro-rs' : 'cpa';
const targetId = normalizeScopeId(options.targetId || options.target || options.activeTargetId, targetFallback);
return { flowId, targetId };
}
function getCacheKey(scope = normalizeScope()) {
return `${CACHE_KEY_PREFIX}:${scope.flowId}:${scope.targetId}`;
}
function buildSummaryApiUrl(scope = normalizeScope()) {
const url = new URL(CONTENT_SUMMARY_API_URL);
url.searchParams.set('flow', scope.flowId);
url.searchParams.set('target', scope.targetId);
return url.toString();
}
function sanitizeItem(item = {}) {
return {
slug: String(item?.slug || '').trim(),
title: String(item?.title || '').trim(),
text: String(item?.text || '').trim(),
isEnabled: Boolean(item?.is_enabled ?? item?.isEnabled),
hasContent: Boolean(item?.has_content ?? item?.hasContent),
isVisible: Boolean(item?.is_visible ?? item?.isVisible),
updatedAt: String(item?.updated_at ?? item?.updatedAt ?? '').trim(),
updatedAtDisplay: String(item?.updated_at_display ?? item?.updatedAtDisplay ?? '').trim(),
flowId: normalizeScopeId(item?.flow_id ?? item?.flowId, ''),
targetId: normalizeScopeId(item?.target_id ?? item?.targetId, ''),
scope: String(item?.scope || '').trim(),
};
}
function buildSnapshot(payload = {}, scope = normalizeScope()) {
const items = Array.isArray(payload?.items)
? payload.items.map(sanitizeItem).filter((item) => item.slug)
: [];
const responseScope = normalizeScope({
flowId: payload?.flow_id || payload?.flowId || scope.flowId,
targetId: payload?.target_id || payload?.targetId || scope.targetId,
});
const promptVersion = String(payload?.prompt_version || '').trim();
const latestUpdatedAt = String(payload?.latest_updated_at || '').trim();
const latestUpdatedAtDisplay = String(payload?.latest_updated_at_display || '').trim();
const hasVisibleUpdates = Boolean(payload?.has_visible_updates) && Boolean(promptVersion);
return {
status: hasVisibleUpdates ? 'update-available' : 'idle',
promptVersion,
hasVisibleUpdates,
latestUpdatedAt,
latestUpdatedAtDisplay,
items,
flowId: responseScope.flowId,
targetId: responseScope.targetId,
portalUrl: PORTAL_BASE_URL,
apiUrl: buildSummaryApiUrl(responseScope),
checkedAt: Date.now(),
};
}
function readCache(scope = normalizeScope()) {
try {
const raw = localStorage.getItem(getCacheKey(scope));
if (!raw) {
return null;
}
const parsed = JSON.parse(raw);
if (!parsed || typeof parsed !== 'object') {
return null;
}
const snapshot = buildSnapshot({
items: parsed.items,
prompt_version: parsed.promptVersion,
has_visible_updates: parsed.hasVisibleUpdates,
latest_updated_at: parsed.latestUpdatedAt,
latest_updated_at_display: parsed.latestUpdatedAtDisplay,
flow_id: parsed.flowId,
target_id: parsed.targetId,
}, scope);
if (!Number.isFinite(parsed.checkedAt)) {
return snapshot;
}
snapshot.checkedAt = parsed.checkedAt;
return snapshot;
} catch (error) {
return null;
}
}
function writeCache(snapshot, scope = normalizeScope(snapshot)) {
try {
localStorage.setItem(getCacheKey(scope), JSON.stringify(snapshot));
} catch (error) {
// Ignore cache write failures.
}
}
async function fetchContentSummary(options = {}) {
const scope = normalizeScope(options);
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
try {
const response = await fetch(buildSummaryApiUrl(scope), {
method: 'GET',
headers: {
Accept: 'application/json',
},
cache: 'no-store',
signal: controller.signal,
});
if (!response.ok) {
throw new Error(`内容摘要请求失败:${response.status}`);
}
const payload = await response.json();
if (!payload || payload.ok !== true) {
throw new Error('内容摘要返回格式异常');
}
const snapshot = buildSnapshot(payload, scope);
writeCache(snapshot, scope);
return snapshot;
} catch (error) {
if (error?.name === 'AbortError') {
throw new Error('内容摘要请求超时');
}
throw error;
} finally {
clearTimeout(timeoutId);
}
}
async function getContentUpdateSnapshot(options = {}) {
const scope = normalizeScope(options);
try {
return await fetchContentSummary(scope);
} catch (error) {
const cached = readCache(scope);
if (cached) {
return {
...cached,
fromCache: true,
errorMessage: error?.message || '内容摘要获取失败',
};
}
return {
status: 'error',
promptVersion: '',
hasVisibleUpdates: false,
latestUpdatedAt: '',
latestUpdatedAtDisplay: '',
items: [],
flowId: scope.flowId,
targetId: scope.targetId,
portalUrl: PORTAL_BASE_URL,
apiUrl: buildSummaryApiUrl(scope),
checkedAt: Date.now(),
errorMessage: error?.message || '内容摘要获取失败',
};
}
}
window.SidepanelContributionContentService = {
buildSummaryApiUrl,
getContentUpdateSnapshot,
portalUrl: PORTAL_BASE_URL,
apiUrl: CONTENT_SUMMARY_API_URL,
};
})();