ZHIWEI666 commited on
Commit
297651f
·
verified ·
1 Parent(s): 404f10e

Upload 103 files

Browse files
前端页面/components/互动工具函数.js CHANGED
@@ -43,20 +43,20 @@ export async function recordView(apiMethod, contentId, cachePrefix, onSuccess =
43
  return null;
44
  }
45
 
46
- DEBUG && console.warn(`[浏览量调试] recordView 被调用 - prefix: ${cachePrefix}, id: ${contentId}`);
47
  const now = Date.now();
48
  const cacheKey = `${cachePrefix}_${contentId}`;
49
  const cached = _viewRecordCache[cacheKey];
50
 
51
  // 检查是否在防抖/冷却时间内(成功5分钟,失败30秒)
52
  if (cached && now - cached.time < (cached.success ? VIEW_DEBOUNCE_MS : VIEW_ERROR_COOLDOWN_MS)) {
53
- DEBUG && console.warn(`[浏览量调试] 防抖拦截 - ${cacheKey}, 距上次: ${now - cached.time}ms, 上次状态: ${cached.success ? 'success' : 'error'}`);
54
  return null; // 防抖中,跳过
55
  }
56
 
57
  try {
58
  const res = await apiMethod(contentId);
59
- DEBUG && console.warn(`[浏览量调试] API响应:`, res?.status, res);
60
  if (res.status === "success") {
61
  // 成功后记录时间戳和成功状态
62
  _viewRecordCache[cacheKey] = { time: now, success: true };
@@ -66,10 +66,7 @@ export async function recordView(apiMethod, contentId, cachePrefix, onSuccess =
66
  } catch (err) {
67
  // 失败时记录较短冷却时间,网络恢复后可快速重试
68
  _viewRecordCache[cacheKey] = { time: now, success: false };
69
- // 浏览记录失败不影响页面显示,但输出详细错误便于调试
70
- DEBUG && console.warn(`[浏览量调试] 记录${cachePrefix}浏览量失败:`, err.message || err);
71
- DEBUG && console.warn(`[浏览量调试] 请求参数 - contentId: ${contentId}, cachePrefix: ${cachePrefix}`);
72
- DEBUG && console.warn(`[浏览量调试] 错误详情:`, err);
73
  }
74
  return null;
75
  }
 
43
  return null;
44
  }
45
 
46
+ DEBUG && console.warn('浏览量记录开始');
47
  const now = Date.now();
48
  const cacheKey = `${cachePrefix}_${contentId}`;
49
  const cached = _viewRecordCache[cacheKey];
50
 
51
  // 检查是否在防抖/冷却时间内(成功5分钟,失败30秒)
52
  if (cached && now - cached.time < (cached.success ? VIEW_DEBOUNCE_MS : VIEW_ERROR_COOLDOWN_MS)) {
53
+ DEBUG && console.warn('浏览量记录处于冷却期');
54
  return null; // 防抖中,跳过
55
  }
56
 
57
  try {
58
  const res = await apiMethod(contentId);
59
+ DEBUG && console.warn('浏览量记录收到响应');
60
  if (res.status === "success") {
61
  // 成功后记录时间戳和成功状态
62
  _viewRecordCache[cacheKey] = { time: now, success: true };
 
66
  } catch (err) {
67
  // 失败时记录较短冷却时间,网络恢复后可快速重试
68
  _viewRecordCache[cacheKey] = { time: now, success: false };
69
+ DEBUG && console.warn('浏览量记录失败');
 
 
 
70
  }
71
  return null;
72
  }
前端页面/components/动画音效引擎.js CHANGED
@@ -41,22 +41,47 @@ const SOUND_CONFIGS = {
41
 
42
  // 通用音效合成播放函数
43
  function _playSynthSound(ctx, config) {
44
- const oscillator = ctx.createOscillator();
45
- const gainNode = ctx.createGain();
46
- oscillator.connect(gainNode);
47
- gainNode.connect(ctx.destination);
48
- const now = ctx.currentTime;
49
- const dur = config.stopTime || config.duration;
50
- oscillator.type = config.type;
51
- oscillator.frequency.setValueAtTime(config.freqStart, now);
52
- oscillator.frequency.exponentialRampToValueAtTime(config.freqEnd, now + config.duration);
53
- gainNode.gain.setValueAtTime(config.gain, now);
54
- if (config.gainMid !== undefined) {
55
- gainNode.gain.linearRampToValueAtTime(config.gainMid, now + config.gainMidTime);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  }
57
- gainNode.gain.exponentialRampToValueAtTime(0.001, now + dur);
58
- oscillator.start(now);
59
- oscillator.stop(now + dur);
60
  }
61
 
62
  // 🔧 懒加载 AudioContext(确保在用户交互后创建)
@@ -65,7 +90,7 @@ function getAudioContext() {
65
  try {
66
  audioContext = new AudioContext();
67
  } catch (e) {
68
- console.warn('AudioContext 创建失败:', e);
69
  return null;
70
  }
71
  }
@@ -79,7 +104,8 @@ async function resumeAudioContext() {
79
  try {
80
  await ctx.resume();
81
  } catch (e) {
82
- console.warn('AudioContext resume 失败:', e);
 
83
  }
84
  }
85
  return ctx;
@@ -107,7 +133,7 @@ export async function playSound(type = 'whoosh') {
107
  break;
108
  }
109
  } catch (e) {
110
- console.warn('音效播放失败:', e);
111
  }
112
  }
113
 
@@ -529,7 +555,7 @@ export function initAnimationEngine() {
529
 
530
  // 🔒 在用户首次交互时预热 AudioContext
531
  const warmupAudio = () => {
532
- resumeAudioContext();
533
  document.removeEventListener('click', warmupAudio);
534
  document.removeEventListener('keydown', warmupAudio);
535
  };
 
41
 
42
  // 通用音效合成播放函数
43
  function _playSynthSound(ctx, config) {
44
+ let oscillator = null;
45
+ let gainNode = null;
46
+ let started = false;
47
+ let released = false;
48
+ const release = () => {
49
+ if (released) return;
50
+ released = true;
51
+ if (oscillator) {
52
+ oscillator.onended = null;
53
+ try { oscillator.disconnect(); } catch {}
54
+ }
55
+ if (gainNode) {
56
+ try { gainNode.disconnect(); } catch {}
57
+ }
58
+ };
59
+ try {
60
+ oscillator = ctx.createOscillator();
61
+ gainNode = ctx.createGain();
62
+ oscillator.onended = release;
63
+ oscillator.connect(gainNode);
64
+ gainNode.connect(ctx.destination);
65
+ const now = ctx.currentTime;
66
+ const dur = config.stopTime || config.duration;
67
+ oscillator.type = config.type;
68
+ oscillator.frequency.setValueAtTime(config.freqStart, now);
69
+ oscillator.frequency.exponentialRampToValueAtTime(config.freqEnd, now + config.duration);
70
+ gainNode.gain.setValueAtTime(config.gain, now);
71
+ if (config.gainMid !== undefined) {
72
+ gainNode.gain.linearRampToValueAtTime(config.gainMid, now + config.gainMidTime);
73
+ }
74
+ gainNode.gain.exponentialRampToValueAtTime(0.001, now + dur);
75
+ oscillator.start(now);
76
+ started = true;
77
+ oscillator.stop(now + dur);
78
+ } catch (error) {
79
+ if (started) {
80
+ try { oscillator.stop(); } catch {}
81
+ }
82
+ release();
83
+ throw error;
84
  }
 
 
 
85
  }
86
 
87
  // 🔧 懒加载 AudioContext(确保在用户交互后创建)
 
90
  try {
91
  audioContext = new AudioContext();
92
  } catch (e) {
93
+ console.warn('AudioContext 创建失败');
94
  return null;
95
  }
96
  }
 
104
  try {
105
  await ctx.resume();
106
  } catch (e) {
107
+ console.warn('AudioContext resume 失败');
108
+ return null;
109
  }
110
  }
111
  return ctx;
 
133
  break;
134
  }
135
  } catch (e) {
136
+ console.warn('音效播放失败');
137
  }
138
  }
139
 
 
555
 
556
  // 🔒 在用户首次交互时预热 AudioContext
557
  const warmupAudio = () => {
558
+ if (getSettings().enableSoundEffects) resumeAudioContext();
559
  document.removeEventListener('click', warmupAudio);
560
  document.removeEventListener('keydown', warmupAudio);
561
  };
前端页面/components/图片展示测试.js CHANGED
@@ -879,7 +879,7 @@ class CyberImageViewer {
879
  img.src = url;
880
  this._onImageLoaded();
881
  } catch (err) {
882
- console.error('图片加载失败:', err);
883
  this._onImageError();
884
  }
885
  }
@@ -905,7 +905,7 @@ class CyberImageViewer {
905
  resolve(img);
906
  };
907
  img.onerror = (err) => {
908
- console.error('❌ 图片加载失败:', err?.name || 'Error');
909
  reject(new Error('图片加载失败'));
910
  };
911
  img.src = url;
 
879
  img.src = url;
880
  this._onImageLoaded();
881
  } catch (err) {
882
+ console.error('图片加载失败');
883
  this._onImageError();
884
  }
885
  }
 
905
  resolve(img);
906
  };
907
  img.onerror = (err) => {
908
+ console.error('❌ 图片加载失败');
909
  reject(new Error('图片加载失败'));
910
  };
911
  img.src = url;
前端页面/components/图片沙盒组件.js CHANGED
@@ -269,7 +269,7 @@ export function setupImageSandboxEvents(detailView) {
269
  try {
270
  openImageViewer(imageUrls, currentIndex, { opaque: true });
271
  } catch (err) {
272
- console.error('无法打开图像播放器:', err);
273
  // 显示用户可见的错误提示
274
  import('./UI交互提示组件.js').then(module => {
275
  module.showToast(t('image.viewer.load_failed'), 'error');
 
269
  try {
270
  openImageViewer(imageUrls, currentIndex, { opaque: true });
271
  } catch (err) {
272
+ console.error('无法打开图像播放器');
273
  // 显示用户可见的错误提示
274
  import('./UI交互提示组件.js').then(module => {
275
  module.showToast(t('image.viewer.load_failed'), 'error');
前端页面/components/性能优化工具.js CHANGED
@@ -15,7 +15,7 @@
15
  // 🔒 P0安全优化:敏感数据脱敏
16
  // ==========================================
17
 
18
- import { PAGINATION } from "../core/全局配置.js";
19
  // 📦 列表缓存工具需要本地可用的缓存实现;下方「向后兼容重导出」那行是纯转发、
20
  // 不会生成本地绑定,故此处必须单独 import(勿误删,否则 readListCache 运行时报错)
21
  import { setCache, getCacheWithMeta, removeCache, getCacheKeysByPrefix } from "../core/网络请求_缓存管理.js";
@@ -1392,7 +1392,7 @@ export function isEmptyListRefreshWithExistingData(nextItems, currentItems) {
1392
  * 渲染可访问的列表首屏失败状态,并提供显式重试入口。
1393
  * 使用 DOM/textContent,避免错误文案进入 HTML sink。
1394
  */
1395
- export function renderListLoadError(container, message, onRetry, t) {
1396
  if (!container) return;
1397
  const status = document.createElement("div");
1398
  status.className = "comfy-ranking-list-error";
@@ -1411,6 +1411,46 @@ export function renderListLoadError(container, message, onRetry, t) {
1411
 
1412
  status.append(text, retry);
1413
  container.replaceChildren(status);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1414
  }
1415
 
1416
  /**
 
15
  // 🔒 P0安全优化:敏感数据脱敏
16
  // ==========================================
17
 
18
+ import { API, PAGINATION } from "../core/全局配置.js";
19
  // 📦 列表缓存工具需要本地可用的缓存实现;下方「向后兼容重导出」那行是纯转发、
20
  // 不会生成本地绑定,故此处必须单独 import(勿误删,否则 readListCache 运行时报错)
21
  import { setCache, getCacheWithMeta, removeCache, getCacheKeysByPrefix } from "../core/网络请求_缓存管理.js";
 
1392
  * 渲染可访问的列表首屏失败状态,并提供显式重试入口。
1393
  * 使用 DOM/textContent,避免错误文案进入 HTML sink。
1394
  */
1395
+ export function renderListLoadError(container, message, onRetry, t, error = null) {
1396
  if (!container) return;
1397
  const status = document.createElement("div");
1398
  status.className = "comfy-ranking-list-error";
 
1411
 
1412
  status.append(text, retry);
1413
  container.replaceChildren(status);
1414
+ const requestId = error?.requestId;
1415
+ if (typeof requestId !== 'string' || !API.REQUEST_ID_PATTERN.test(requestId)) return;
1416
+
1417
+ const details = document.createElement('details');
1418
+ details.className = 'comfy-ranking-request-diagnostic';
1419
+ const summary = document.createElement('summary');
1420
+ summary.textContent = t('feedback.diagnostic_details');
1421
+ const label = document.createElement('label');
1422
+ label.className = 'comfy-ranking-request-diagnostic__label';
1423
+ label.textContent = t('feedback.request_id');
1424
+ const input = document.createElement('input');
1425
+ input.type = 'text'; input.readOnly = true; input.spellcheck = false;
1426
+ input.value = requestId;
1427
+ label.append(input);
1428
+ const copy = document.createElement('button');
1429
+ copy.type = 'button'; copy.textContent = t('common.copy');
1430
+ const feedback = document.createElement('p');
1431
+ feedback.className = 'comfy-ranking-request-diagnostic__feedback';
1432
+ feedback.setAttribute('role', 'status');
1433
+ feedback.setAttribute('aria-live', 'polite');
1434
+ let copying = false;
1435
+ copy.addEventListener('click', async () => {
1436
+ if (copying || !details.isConnected) return;
1437
+ copying = true; copy.setAttribute('aria-busy', 'true');
1438
+ try {
1439
+ if (typeof navigator.clipboard?.writeText !== 'function') throw new Error();
1440
+ await navigator.clipboard.writeText(requestId);
1441
+ if (details.isConnected) feedback.textContent = t('feedback.copied');
1442
+ } catch {
1443
+ if (details.isConnected) {
1444
+ feedback.textContent = t('feedback.copy_id_manually');
1445
+ if (document.activeElement === copy) { input.focus(); input.select(); }
1446
+ }
1447
+ } finally {
1448
+ copying = false;
1449
+ if (details.isConnected) copy.setAttribute('aria-busy', 'false');
1450
+ }
1451
+ });
1452
+ details.append(summary, label, copy, feedback);
1453
+ container.append(details);
1454
  }
1455
 
1456
  /**
前端页面/components/用户体验_国际化.js CHANGED
@@ -536,6 +536,10 @@ const translations = {
536
  'feedback.success': '操作成功',
537
  'feedback.error': '操作失败,请重试',
538
  'feedback.timeout': '请求超时',
 
 
 
 
539
  'feedback.response_format_error': '服务器响应格式错误',
540
  'feedback.saved': '已保存',
541
  'feedback.logged_out': '已退出登录',
@@ -757,6 +761,9 @@ const translations = {
757
  'crop.processing': '处理中...',
758
  'crop.process_failed': '处理失败,重试',
759
  'feedback.processing': '处理中...',
 
 
 
760
  'feedback.copied': '已复制到剪贴板',
761
  'feedback.copy_failed': '复制失败',
762
  'feedback.operation_success': '操作成功',
@@ -2254,6 +2261,10 @@ const translations = {
2254
  'feedback.success': 'Success',
2255
  'feedback.error': 'Failed, please retry',
2256
  'feedback.timeout': 'Request timeout',
 
 
 
 
2257
  'feedback.response_format_error': 'The server response format is invalid',
2258
  'feedback.saved': 'Saved',
2259
  'feedback.logged_out': 'Logged out',
@@ -2470,6 +2481,9 @@ const translations = {
2470
  'crop.processing': 'Processing...',
2471
  'crop.process_failed': 'Processing failed. Try again.',
2472
  'feedback.processing': 'Processing...',
 
 
 
2473
  'feedback.copied': 'Copied to clipboard',
2474
  'feedback.copy_failed': 'Could not copy',
2475
  'feedback.operation_success': 'Operation completed',
 
536
  'feedback.success': '操作成功',
537
  'feedback.error': '操作失败,请重试',
538
  'feedback.timeout': '请求超时',
539
+ 'feedback.network_timeout': '网络请求超时,请检查网络连接',
540
+ 'feedback.network_unavailable': '网络连接失败,请检查网络',
541
+ 'feedback.http_request_failed': '请求失败(HTTP {status})',
542
+ 'feedback.invalid_request_data': '数据格式不正确,请检查填写内容。',
543
  'feedback.response_format_error': '服务器响应格式错误',
544
  'feedback.saved': '已保存',
545
  'feedback.logged_out': '已退出登录',
 
761
  'crop.processing': '处理中...',
762
  'crop.process_failed': '处理失败,重试',
763
  'feedback.processing': '处理中...',
764
+ 'feedback.diagnostic_details': '诊断信息',
765
+ 'feedback.request_id': '请求编号',
766
+ 'feedback.copy_id_manually': '无法自动复制,请选择上方编号后手动复制。',
767
  'feedback.copied': '已复制到剪贴板',
768
  'feedback.copy_failed': '复制失败',
769
  'feedback.operation_success': '操作成功',
 
2261
  'feedback.success': 'Success',
2262
  'feedback.error': 'Failed, please retry',
2263
  'feedback.timeout': 'Request timeout',
2264
+ 'feedback.network_timeout': 'The network request timed out. Check your connection.',
2265
+ 'feedback.network_unavailable': 'Network connection failed. Check your connection.',
2266
+ 'feedback.http_request_failed': 'Request failed (HTTP {status})',
2267
+ 'feedback.invalid_request_data': 'The submitted data has an invalid format. Check the form.',
2268
  'feedback.response_format_error': 'The server response format is invalid',
2269
  'feedback.saved': 'Saved',
2270
  'feedback.logged_out': 'Logged out',
 
2481
  'crop.processing': 'Processing...',
2482
  'crop.process_failed': 'Processing failed. Try again.',
2483
  'feedback.processing': 'Processing...',
2484
+ 'feedback.diagnostic_details': 'Diagnostic details',
2485
+ 'feedback.request_id': 'Request ID',
2486
+ 'feedback.copy_id_manually': 'Automatic copying is unavailable. Select the ID above and copy it manually.',
2487
  'feedback.copied': 'Copied to clipboard',
2488
  'feedback.copy_failed': 'Could not copy',
2489
  'feedback.operation_success': 'Operation completed',
前端页面/core/侧边栏主程序.js CHANGED
@@ -27,7 +27,7 @@ import { initUXEnhancements, t, getLanguage } from "../components/用户体验
27
  try {
28
  initUXEnhancements();
29
  } catch (e) {
30
- console.warn('🎯 UX 增强初始化失败:', e);
31
  }
32
 
33
  // 统一设计 tokens 与键盘焦点样式;样式表只挂载一次,保持 ComfyUI 主题变量适配。
@@ -79,7 +79,7 @@ const BackgroundStore = {
79
  try {
80
  localStorage.setItem(getBackgroundKey(), base64);
81
  } catch (e) {
82
- console.warn("⚠️ 界面背景保存失败(存储空间不足或不可用):", e.message);
83
  }
84
  },
85
  load() {
@@ -409,7 +409,7 @@ export function buildSidebarDOM() {
409
  const view = createPublishTaskView(currentUser, taskData);
410
  showInlineView(view);
411
  } catch (err) {
412
- console.error('创建编辑任务视图失败:', err);
413
  }
414
  });
415
 
 
27
  try {
28
  initUXEnhancements();
29
  } catch (e) {
30
+ console.warn('🎯 UX 增强初始化失败');
31
  }
32
 
33
  // 统一设计 tokens 与键盘焦点样式;样式表只挂载一次,保持 ComfyUI 主题变量适配。
 
79
  try {
80
  localStorage.setItem(getBackgroundKey(), base64);
81
  } catch (e) {
82
+ console.warn("⚠️ 界面背景保存失败(存储空间不足或不可用)");
83
  }
84
  },
85
  load() {
 
409
  const view = createPublishTaskView(currentUser, taskData);
410
  showInlineView(view);
411
  } catch (err) {
412
+ console.error('创建编辑任务视图失败');
413
  }
414
  });
415
 
前端页面/core/侧边栏数据引擎.js CHANGED
@@ -650,7 +650,7 @@ export async function loadSidebarContent({
650
  renderListLoadError(contentArea, t('common.network_error_retry'), () => loadSidebarContent({
651
  tab, sort, keyword, contentArea, currentUser, renderToken, getRenderToken,
652
  force: true, expandItemId, isNavigationCurrent
653
- }), t);
654
  return;
655
  }
656
  }
@@ -659,7 +659,7 @@ export async function loadSidebarContent({
659
  renderListLoadError(contentArea, t('common.network_error_retry'), () => loadSidebarContent({
660
  tab, sort, keyword, contentArea, currentUser, renderToken, getRenderToken,
661
  force: true, expandItemId, isNavigationCurrent
662
- }), t);
663
  }
664
  }
665
 
 
650
  renderListLoadError(contentArea, t('common.network_error_retry'), () => loadSidebarContent({
651
  tab, sort, keyword, contentArea, currentUser, renderToken, getRenderToken,
652
  force: true, expandItemId, isNavigationCurrent
653
+ }), t, error);
654
  return;
655
  }
656
  }
 
659
  renderListLoadError(contentArea, t('common.network_error_retry'), () => loadSidebarContent({
660
  tab, sort, keyword, contentArea, currentUser, renderToken, getRenderToken,
661
  force: true, expandItemId, isNavigationCurrent
662
+ }), t, error);
663
  }
664
  }
665
 
前端页面/core/全局配置.js CHANGED
@@ -18,8 +18,10 @@
18
  // 🌐 API 配置
19
  // ==========================================
20
 
21
- /** @type {{ BASE_URL: string, TIMEOUT: number, VIDEO_UPLOAD_TIMEOUT: number, VIDEO_UPLOAD_RESPONSE_MAX_CHARS: number, LOCAL_CACHE_STATS_TIMEOUT: number, LOCAL_CACHE_CLEAR_TIMEOUT: number, LOCAL_CACHE_RESPONSE_MAX_BYTES: number, MAX_RETRIES: number, RETRY_DELAY: number, LIST_RETRIES: number, SEARCH_TIMEOUT: number, SEARCH_RETRIES: number, PROFILE_RESOURCE_TIMEOUT: number, CREATOR_CHART_SCRIPT_URL: string, BACKGROUND_TIMEOUT: number, BANNER_IMAGE_TIMEOUT: number, BACKGROUND_RETRIES: number, WORKFLOW_DOWNLOAD_TIMEOUT: number, CLOUD_DOWN_COOLDOWN: number, SEARCH_COMPONENT_ID: string, IMAGE_PROXY_PATH: string, LOCAL_IMAGE_PROXY_PREFIX: string, COMPLETE_IMAGE_QUERY_SUFFIX: string, PRIVATE_GET_PATTERNS: RegExp[], PUBLIC_CONDITIONAL_GET_PATHS: string[], CONDITIONAL_ETAG_PATTERN: RegExp, ENDPOINT_ERROR_SCOPE_HEADER: string, ENDPOINT_ERROR_SCOPE_VALUE: string }} */
22
  export const API = {
 
 
23
  ENDPOINT_ERROR_SCOPE_HEADER: "X-Ranking-Error-Scope",
24
  ENDPOINT_ERROR_SCOPE_VALUE: "endpoint",
25
  // 仅公开榜单使用由正文派生的条件验证器,私有响应仍禁止留存。
 
18
  // 🌐 API 配置
19
  // ==========================================
20
 
21
+ /** @type {{ BASE_URL: string, TIMEOUT: number, VIDEO_UPLOAD_TIMEOUT: number, VIDEO_UPLOAD_RESPONSE_MAX_CHARS: number, LOCAL_CACHE_STATS_TIMEOUT: number, LOCAL_CACHE_CLEAR_TIMEOUT: number, LOCAL_CACHE_RESPONSE_MAX_BYTES: number, MAX_RETRIES: number, RETRY_DELAY: number, LIST_RETRIES: number, SEARCH_TIMEOUT: number, SEARCH_RETRIES: number, PROFILE_RESOURCE_TIMEOUT: number, CREATOR_CHART_SCRIPT_URL: string, BACKGROUND_TIMEOUT: number, BANNER_IMAGE_TIMEOUT: number, BACKGROUND_RETRIES: number, WORKFLOW_DOWNLOAD_TIMEOUT: number, CLOUD_DOWN_COOLDOWN: number, SEARCH_COMPONENT_ID: string, IMAGE_PROXY_PATH: string, LOCAL_IMAGE_PROXY_PREFIX: string, COMPLETE_IMAGE_QUERY_SUFFIX: string, PRIVATE_GET_PATTERNS: RegExp[], PUBLIC_CONDITIONAL_GET_PATHS: string[], CONDITIONAL_ETAG_PATTERN: RegExp, REQUEST_ID_HEADER: string, REQUEST_ID_PATTERN: RegExp, ENDPOINT_ERROR_SCOPE_HEADER: string, ENDPOINT_ERROR_SCOPE_VALUE: string }} */
22
  export const API = {
23
+ REQUEST_ID_HEADER: "X-Ranking-Request-Id",
24
+ REQUEST_ID_PATTERN: /^(?:[a-f0-9]{32}|ERR_[0-9]{10,13}_[a-f0-9]{1,8})$/,
25
  ENDPOINT_ERROR_SCOPE_HEADER: "X-Ranking-Error-Scope",
26
  ENDPOINT_ERROR_SCOPE_VALUE: "endpoint",
27
  // 仅公开榜单使用由正文派生的条件验证器,私有响应仍禁止留存。
前端页面/core/网络请求_基础设施.js CHANGED
@@ -34,6 +34,26 @@ const BASE_URL = API.BASE_URL;
34
  // ⚡ P1性能优化:请求去重(相同GET请求只发一次)
35
  const pendingRequests = new Map();
36
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  // 🚀 P3优化:网络状态由 状态管理.js 的 isOnline() 统一提供(见下方离线模式支持),
38
  // 本模块不再自行注册 online/offline 监听。
39
 
@@ -486,18 +506,7 @@ async function request(endpoint, options = {}) {
486
 
487
  if (!response.ok && !result.validated) {
488
  if (!responseData || typeof responseData !== "object" || Array.isArray(responseData)) responseData = {};
489
- let errorMsg = `请求失败 (${response.status})`;
490
-
491
- // 🚀 核心修改:增加对 FastAPI 422 报错数组的解析
492
- if (Array.isArray(responseData.detail)) {
493
- errorMsg = "数据格式错误: " + responseData.detail.map(e => `${e.loc[e.loc.length-1]} (${e.type})`).join(", ");
494
- } else if (typeof responseData.detail === "string") {
495
- errorMsg = responseData.detail;
496
- } else if (responseData.message) {
497
- errorMsg = responseData.message;
498
- } else if (responseData.error) {
499
- errorMsg = responseData.error;
500
- }
501
 
502
  // 🚀 P1优化:4xx 错误不重试,5xx 错误可重试
503
  if (response.status >= 400 && response.status < 500) {
@@ -512,7 +521,7 @@ async function request(endpoint, options = {}) {
512
  window.dispatchEvent(new CustomEvent('comfy-ranking-auth-expired'));
513
  }
514
  }
515
- const error = new Error(errorMsg);
516
  error.status = response.status;
517
  if (typeof responseData.detail === "string" && /^[a-z][a-z0-9_]{0,79}$/.test(responseData.detail)) error.code = responseData.detail;
518
  throw error;
@@ -521,7 +530,7 @@ async function request(endpoint, options = {}) {
521
  // 应用明确标记且正文可读的503只影响当前接口,仍允许其它榜单联网。
522
  if (response.status === 503 && result.bodyConfirmed &&
523
  response.headers?.get?.(API.ENDPOINT_ERROR_SCOPE_HEADER) === API.ENDPOINT_ERROR_SCOPE_VALUE) {
524
- lastError = new Error(errorMsg);
525
  lastError.status = response.status;
526
  networkLevelFailure = false;
527
  break;
@@ -532,14 +541,14 @@ async function request(endpoint, options = {}) {
532
  // 与超时/连接失败一样共享冷却,避免切换榜单时每个 endpoint
533
  // 都重复访问一次;普通 500 仍只视为单接口服务端错误。
534
  if ([502, 503, 504].includes(response.status)) {
535
- lastError = new Error(errorMsg);
536
  lastError.status = response.status;
537
  networkLevelFailure = true;
538
  markCloudDown(endpoint);
539
  break;
540
  }
541
 
542
- lastError = new Error(errorMsg);
543
  lastError.status = response.status;
544
  continue; // 5xx 错误重试
545
  }
@@ -606,8 +615,8 @@ async function request(endpoint, options = {}) {
606
  // 让 GET 请求走过期缓存回退;无缓存时仍以该友好错误抛出。
607
  const isTimeout = error.name === 'AbortError';
608
  lastError = isTimeout
609
- ? new Error('网络请求超时,请检查网络连接')
610
- : new Error('网络连接失败,请检查网络');
611
  lastError.code = isTimeout ? 'network_timeout' : 'network_unavailable';
612
  if (fetchStarted) networkLevelFailure = true;
613
  // ☁️ 连接层失败(被墙 / DNS 污染 / 拒绝连接 / 证书错误)几乎不会是偶发抖动,
@@ -643,7 +652,7 @@ async function request(endpoint, options = {}) {
643
  }
644
  }
645
  // 仍无缓存,才抛异常
646
- throw lastError || new Error('请求失败');
647
  })();
648
 
649
  if (privateEntry) requestPromise.finally(() => activePrivateReads.delete(privateEntry)).catch(() => {});
@@ -710,7 +719,7 @@ export async function requestSSE(endpoint, body, onProgress, options = {}) {
710
  });
711
 
712
  if (!response.ok) {
713
- const error = new Error(`HTTP ${response.status}: ${response.statusText}`);
714
  error.status = response.status;
715
  // 仅当流式路由明确不存在/不接受该方法时,调用方可安全回退到旧端点。
716
  // 网络错误、超时和服务端错误都可能发生在操作已开始后,不可自动重发。
 
34
  // ⚡ P1性能优化:请求去重(相同GET请求只发一次)
35
  const pendingRequests = new Map();
36
 
37
+ function withRequestDiagnostic(error, response, data = null) {
38
+ let headerId;
39
+ try { headerId = response.headers?.get?.(API.REQUEST_ID_HEADER); } catch {}
40
+ for (const value of [headerId, data?.error_id]) {
41
+ if (typeof value === 'string' && API.REQUEST_ID_PATTERN.test(value)) {
42
+ error.requestId = value;
43
+ break;
44
+ }
45
+ }
46
+ return error;
47
+ }
48
+
49
+ function httpErrorMessage(data, status) {
50
+ if (Array.isArray(data.detail)) return t('feedback.invalid_request_data');
51
+ for (const value of [data.detail, data.message, data.error]) {
52
+ if (typeof value === 'string' && value.trim()) return value;
53
+ }
54
+ return t('feedback.http_request_failed', { status });
55
+ }
56
+
57
  // 🚀 P3优化:网络状态由 状态管理.js 的 isOnline() 统一提供(见下方离线模式支持),
58
  // 本模块不再自行注册 online/offline 监听。
59
 
 
506
 
507
  if (!response.ok && !result.validated) {
508
  if (!responseData || typeof responseData !== "object" || Array.isArray(responseData)) responseData = {};
509
+ const errorMsg = httpErrorMessage(responseData, response.status);
 
 
 
 
 
 
 
 
 
 
 
510
 
511
  // 🚀 P1优化:4xx 错误不重试,5xx 错误可重试
512
  if (response.status >= 400 && response.status < 500) {
 
521
  window.dispatchEvent(new CustomEvent('comfy-ranking-auth-expired'));
522
  }
523
  }
524
+ const error = withRequestDiagnostic(new Error(errorMsg), response, responseData);
525
  error.status = response.status;
526
  if (typeof responseData.detail === "string" && /^[a-z][a-z0-9_]{0,79}$/.test(responseData.detail)) error.code = responseData.detail;
527
  throw error;
 
530
  // 应用明确标记且正文可读的503只影响当前接口,仍允许其它榜单联网。
531
  if (response.status === 503 && result.bodyConfirmed &&
532
  response.headers?.get?.(API.ENDPOINT_ERROR_SCOPE_HEADER) === API.ENDPOINT_ERROR_SCOPE_VALUE) {
533
+ lastError = withRequestDiagnostic(new Error(errorMsg), response, responseData);
534
  lastError.status = response.status;
535
  networkLevelFailure = false;
536
  break;
 
541
  // 与超时/连接失败一样共享冷却,避免切换榜单时每个 endpoint
542
  // 都重复访问一次;普通 500 仍只视为单接口服务端错误。
543
  if ([502, 503, 504].includes(response.status)) {
544
+ lastError = withRequestDiagnostic(new Error(errorMsg), response, responseData);
545
  lastError.status = response.status;
546
  networkLevelFailure = true;
547
  markCloudDown(endpoint);
548
  break;
549
  }
550
 
551
+ lastError = withRequestDiagnostic(new Error(errorMsg), response, responseData);
552
  lastError.status = response.status;
553
  continue; // 5xx 错误重试
554
  }
 
615
  // 让 GET 请求走过期缓存回退;无缓存时仍以该友好错误抛出。
616
  const isTimeout = error.name === 'AbortError';
617
  lastError = isTimeout
618
+ ? new Error(t('feedback.network_timeout'))
619
+ : new Error(t('feedback.network_unavailable'));
620
  lastError.code = isTimeout ? 'network_timeout' : 'network_unavailable';
621
  if (fetchStarted) networkLevelFailure = true;
622
  // ☁️ 连接层失败(被墙 / DNS 污染 / 拒绝连接 / 证书错误)几乎不会是偶发抖动,
 
652
  }
653
  }
654
  // 仍无缓存,才抛异常
655
+ throw lastError || new Error(t('feedback.error'));
656
  })();
657
 
658
  if (privateEntry) requestPromise.finally(() => activePrivateReads.delete(privateEntry)).catch(() => {});
 
719
  });
720
 
721
  if (!response.ok) {
722
+ const error = withRequestDiagnostic(new Error(t('feedback.http_request_failed', { status: response.status })), response);
723
  error.status = response.status;
724
  // 仅当流式路由明确不存在/不接受该方法时,调用方可安全回退到旧端点。
725
  // 网络错误、超时和服务端错误都可能发生在操作已开始后,不可自动重发。
前端页面/core/网络请求_缓存管理.js CHANGED
@@ -31,6 +31,7 @@ let lastStorageBudgetSweepAt = 0;
31
  // 内存缓存(最快)
32
  const memoryCache = new Map();
33
  const memoryCacheOrder = []; // LRU 顺序追踪
 
34
 
35
  /**
36
  * 生成完整缓存键(带前缀)
@@ -118,6 +119,7 @@ export function setCache(key, value, ttl = DEFAULT_TTL, persist = true, metadata
118
  const fk = _fullKey(key);
119
  const expireAt = Date.now() + ttl;
120
  const cacheData = { schemaVersion: CACHE_SCHEMA_VERSION, value, expireAt, lastAccessAt: Date.now() };
 
121
  if (typeof metadata?.etag === "string" && API.CONDITIONAL_ETAG_PATTERN.test(metadata.etag)) cacheData.etag = metadata.etag;
122
 
123
  // 内存缓存
@@ -127,7 +129,17 @@ export function setCache(key, value, ttl = DEFAULT_TTL, persist = true, metadata
127
 
128
  // 持久化缓存
129
  if (persist) {
130
- const jsonStr = JSON.stringify(cacheData);
 
 
 
 
 
 
 
 
 
 
131
  let previousBytes = 0;
132
  let previousLengthKnown = false;
133
  try {
@@ -136,6 +148,7 @@ export function setCache(key, value, ttl = DEFAULT_TTL, persist = true, metadata
136
  previousLengthKnown = true;
137
  } catch {}
138
  if (_safeStorageWrite(fk, jsonStr)) {
 
139
  if (storageUsageBytes !== null && previousLengthKnown) {
140
  storageUsageBytes += (fk.length + jsonStr.length) * 2 - previousBytes;
141
  } else {
@@ -181,6 +194,7 @@ export function getCache(key) {
181
  // 回填内存缓存
182
  memoryCache.set(fk, cached);
183
  _updateLRU(fk);
 
184
  _touchStorageEntry(fk, cached);
185
  return cached.value;
186
  }
@@ -235,6 +249,7 @@ export function getCacheWithMeta(key, ignoreExpiry = false) {
235
  // 回填内存缓存
236
  memoryCache.set(fk, cached);
237
  _updateLRU(fk);
 
238
  _touchStorageEntry(fk, cached);
239
  return { value: cached.value, expired, found: true, ...(typeof cached.etag === "string" && API.CONDITIONAL_ETAG_PATTERN.test(cached.etag) ? { etag: cached.etag } : {}) };
240
  }
@@ -283,6 +298,7 @@ export function getCacheKeysByPrefix(prefix) {
283
  }
284
 
285
  function _touchStorageEntry(key, cached) {
 
286
  try {
287
  if (localStorage.getItem(key) === null) return;
288
  const now = Date.now();
 
31
  // 内存缓存(最快)
32
  const memoryCache = new Map();
33
  const memoryCacheOrder = []; // LRU 顺序追踪
34
+ const memoryOnlyEntries = new WeakSet();
35
 
36
  /**
37
  * 生成完整缓存键(带前缀)
 
119
  const fk = _fullKey(key);
120
  const expireAt = Date.now() + ttl;
121
  const cacheData = { schemaVersion: CACHE_SCHEMA_VERSION, value, expireAt, lastAccessAt: Date.now() };
122
+ memoryOnlyEntries.add(cacheData);
123
  if (typeof metadata?.etag === "string" && API.CONDITIONAL_ETAG_PATTERN.test(metadata.etag)) cacheData.etag = metadata.etag;
124
 
125
  // 内存缓存
 
129
 
130
  // 持久化缓存
131
  if (persist) {
132
+ let jsonStr;
133
+ try {
134
+ jsonStr = JSON.stringify(cacheData);
135
+ } catch {
136
+ console.warn('缓存无法序列化,保留内存缓存');
137
+ return;
138
+ }
139
+ if ((fk.length + jsonStr.length) * 2 > MAX_STORAGE_BYTES) {
140
+ console.warn('缓存条目超过持久化预算,保留内存缓存');
141
+ return;
142
+ }
143
  let previousBytes = 0;
144
  let previousLengthKnown = false;
145
  try {
 
148
  previousLengthKnown = true;
149
  } catch {}
150
  if (_safeStorageWrite(fk, jsonStr)) {
151
+ memoryOnlyEntries.delete(cacheData);
152
  if (storageUsageBytes !== null && previousLengthKnown) {
153
  storageUsageBytes += (fk.length + jsonStr.length) * 2 - previousBytes;
154
  } else {
 
194
  // 回填内存缓存
195
  memoryCache.set(fk, cached);
196
  _updateLRU(fk);
197
+ _enforceCacheLimit();
198
  _touchStorageEntry(fk, cached);
199
  return cached.value;
200
  }
 
249
  // 回填内存缓存
250
  memoryCache.set(fk, cached);
251
  _updateLRU(fk);
252
+ _enforceCacheLimit();
253
  _touchStorageEntry(fk, cached);
254
  return { value: cached.value, expired, found: true, ...(typeof cached.etag === "string" && API.CONDITIONAL_ETAG_PATTERN.test(cached.etag) ? { etag: cached.etag } : {}) };
255
  }
 
298
  }
299
 
300
  function _touchStorageEntry(key, cached) {
301
+ if (memoryOnlyEntries.has(cached)) return;
302
  try {
303
  if (localStorage.getItem(key) === null) return;
304
  const now = Date.now();
前端页面/post/讨论区组件.js CHANGED
@@ -436,7 +436,7 @@ export function createPostsView(currentUser, keyword = "") {
436
  renderPostsFromCache(allPostsData);
437
  showToast(t('post.network_cache'), "warning");
438
  } else {
439
- renderListLoadError(postsGrid, t('post.load_failed'), () => loadPosts(1, false), t);
440
  }
441
  }
442
  }
 
436
  renderPostsFromCache(allPostsData);
437
  showToast(t('post.network_cache'), "warning");
438
  } else {
439
+ renderListLoadError(postsGrid, t('post.load_failed'), () => loadPosts(1, false), t, err);
440
  }
441
  }
442
  }
前端页面/profile/个人中心视图.js CHANGED
@@ -97,6 +97,7 @@ export function showUserProfile(initialUserData, currentUser = null, isMe = true
97
  let userData = normalizeProfileData(initialUserData, initialUserData.account, isMe);
98
  let constructing = true;
99
  let renderGeneration = 0;
 
100
  const componentId = `profile-detail-${++profileSequence}`;
101
  const releaseChild = () => {
102
  const list = container.querySelector('#profile-list-container');
@@ -215,19 +216,18 @@ export function showUserProfile(initialUserData, currentUser = null, isMe = true
215
 
216
  container.querySelector("#btn-open-settings").onclick = () => { isSettingsView = true; render(); };
217
  container.querySelector("#btn-logout").onclick = async () => {
218
- if (!current()) return;
219
- if (await showConfirm(t('confirm.logout')) && current()) {
220
- // 获取当前账号(清除前获取)
221
- const currentAccount = getCurrentAccount();
222
-
223
- // 1. 完整清理:账号级 Profile 缓存、私有数据缓存、全量数据缓存、
224
- // 内存敏感数据与 Token/User/版本戳(与 401 强制登出共用同一函数)
225
  logoutAndClearUserData(currentAccount);
226
-
227
- // 2. 触发事件
228
  window.dispatchEvent(new CustomEvent("comfy-route-back"));
229
  window.dispatchEvent(new CustomEvent("comfy-user-logout"));
230
  showToast(t('feedback.logged_out'), "info");
 
 
231
  }
232
  };
233
  } else {
 
97
  let userData = normalizeProfileData(initialUserData, initialUserData.account, isMe);
98
  let constructing = true;
99
  let renderGeneration = 0;
100
+ let logoutPending = false;
101
  const componentId = `profile-detail-${++profileSequence}`;
102
  const releaseChild = () => {
103
  const list = container.querySelector('#profile-list-container');
 
216
 
217
  container.querySelector("#btn-open-settings").onclick = () => { isSettingsView = true; render(); };
218
  container.querySelector("#btn-logout").onclick = async () => {
219
+ if (!current() || logoutPending) return;
220
+ const currentAccount = getCurrentAccount();
221
+ logoutPending = true;
222
+ try {
223
+ if (!await showConfirm(t('confirm.logout')) || !scope.isCurrent()
224
+ || getCurrentAccount() !== currentAccount) return;
 
225
  logoutAndClearUserData(currentAccount);
 
 
226
  window.dispatchEvent(new CustomEvent("comfy-route-back"));
227
  window.dispatchEvent(new CustomEvent("comfy-user-logout"));
228
  showToast(t('feedback.logged_out'), "info");
229
+ } finally {
230
+ logoutPending = false;
231
  }
232
  };
233
  } else {
前端页面/profile/个人列表组件.js CHANGED
@@ -352,7 +352,7 @@ export async function renderProfileListContent(tabId, domElement, userData, curr
352
  const txCard = createTransactionCard(tx);
353
  txListDiv.appendChild(txCard);
354
  } catch (e) {
355
- console.error("交易卡片渲染失败:", tx.tx_id, e);
356
  }
357
  });
358
  }
@@ -727,7 +727,7 @@ function createPostItem(post, currentUser) {
727
  const view = module.createPostDetailView(post.id, currentUser);
728
  window.dispatchEvent(new CustomEvent("comfy-route-view", { detail: { view } }));
729
  }).catch(err => {
730
- console.error("加载帖子详情组件失败:", err);
731
  showToast(t('profile.post_detail_load_failed'), "error");
732
  });
733
  };
@@ -803,7 +803,7 @@ function createPromptItem(prompt, currentUser) {
803
  const view = module.createPromptDetailView(prompt.id, currentUser);
804
  window.dispatchEvent(new CustomEvent("comfy-route-view", { detail: { view } }));
805
  }).catch(err => {
806
- console.error("加载提示词详情组件失败:", err);
807
  showToast(t('profile.prompt_detail_load_failed'), "error");
808
  });
809
  };
 
352
  const txCard = createTransactionCard(tx);
353
  txListDiv.appendChild(txCard);
354
  } catch (e) {
355
+ console.error("交易卡片渲染失败");
356
  }
357
  });
358
  }
 
727
  const view = module.createPostDetailView(post.id, currentUser);
728
  window.dispatchEvent(new CustomEvent("comfy-route-view", { detail: { view } }));
729
  }).catch(err => {
730
+ console.error("加载帖子详情组件失败");
731
  showToast(t('profile.post_detail_load_failed'), "error");
732
  });
733
  };
 
803
  const view = module.createPromptDetailView(prompt.id, currentUser);
804
  window.dispatchEvent(new CustomEvent("comfy-route-view", { detail: { view } }));
805
  }).catch(err => {
806
+ console.error("加载提示词详情组件失败");
807
  showToast(t('profile.prompt_detail_load_failed'), "error");
808
  });
809
  };
前端页面/prompt/提示词组件.js CHANGED
@@ -564,7 +564,7 @@ export function createPromptsView(currentUser, keyword = "") {
564
  renderPromptsFromCache(allPromptsData);
565
  showToast(t('prompt.network_cache'), "warning");
566
  } else {
567
- renderListLoadError(promptsGrid, t('prompt.load_failed'), () => loadPrompts(1, false), t);
568
  }
569
  }
570
  }
 
564
  renderPromptsFromCache(allPromptsData);
565
  showToast(t('prompt.network_cache'), "warning");
566
  } else {
567
+ renderListLoadError(promptsGrid, t('prompt.load_failed'), () => loadPrompts(1, false), t, err);
568
  }
569
  }
570
  }
前端页面/social/私信聊天组件.js CHANGED
@@ -139,7 +139,8 @@ export function openChatModal(currentUser, targetAccount = null) {
139
  let pollTimer = null; // 🚀 P0-1:消息轮询定时器
140
 
141
  let constructing = true, observer, listGeneration = 0, historyGeneration = 0, conversationGeneration = 0;
142
- let sending = false;
 
143
  const drafts = new Map(), clearTimes = new Map(), pendingMessages = new Map();
144
  const componentId = `chat-${++chatSequence}`;
145
  const sessionMatches = captureRequestSession();
@@ -153,7 +154,7 @@ export function openChatModal(currentUser, targetAccount = null) {
153
  });
154
  scope.addCleanup(() => {
155
  clearTimeout(pollTimer); pollTimer = null; observer?.disconnect();
156
- drafts.clear(); clearTimes.clear(); pendingMessages.clear(); allMessages = []; currentTargetInfo = null; currentChatTarget = null;
157
  inputField.value = ''; messagesArea.replaceChildren(); chatListContainer.replaceChildren(); chatHeader.replaceChildren();
158
  window.dispatchEvent(new CustomEvent('comfy-chat-unread-changed'));
159
  });
@@ -223,12 +224,15 @@ export function openChatModal(currentUser, targetAccount = null) {
223
  const loadChatList = async () => {
224
  if (!scope.isCurrent()) return [];
225
  const generation = ++listGeneration;
 
226
  try {
227
  const res = await api.getChatList(currentUser.account, options);
228
  if (!scope.isCurrent() || generation !== listGeneration) return [];
229
  if (res?.status !== 'success' || !Array.isArray(res.data)) throw new Error('Invalid chat list');
230
  const list = res.data.filter(chat => chat && typeof chat === 'object' && !Array.isArray(chat)
231
- && typeof (chat.target_account || chat.account || chat.receiver || chat.sender) === 'string');
 
 
232
  renderChatList(list); return list;
233
  } catch (_) {
234
  if (scope.isCurrent() && generation === listGeneration) showToast(t('chat.load_list_failed'), 'error');
@@ -395,6 +399,11 @@ export function openChatModal(currentUser, targetAccount = null) {
395
  || (msg.sender === targetAccount && msg.receiver === currentUser.account)))
396
  .map(msg => ({ ...msg })).sort((a, b) => _normalizeTs(a.created_at) - _normalizeTs(b.created_at));
397
  renderMsgs(messages, avatar);
 
 
 
 
 
398
  } catch (_) {
399
  if (!current()) return;
400
  showToast(t('chat.load_history_failed'), 'error');
@@ -456,7 +465,8 @@ export function openChatModal(currentUser, targetAccount = null) {
456
  const poll = async () => {
457
  if (!scope.isCurrent()) return;
458
  const list = await loadChatList();
459
- if (scope.isCurrent() && currentChatTarget && list.some(chat => validUnreadCount(chat.unread_count) > 0)) {
 
460
  await loadChatHistory(currentChatTarget, currentTargetInfo?.name, currentTargetInfo?.avatar);
461
  }
462
  if (scope.isCurrent()) pollTimer = setTimeout(poll, CACHE.MESSAGE_POLL.INTERVAL);
 
139
  let pollTimer = null; // 🚀 P0-1:消息轮询定时器
140
 
141
  let constructing = true, observer, listGeneration = 0, historyGeneration = 0, conversationGeneration = 0;
142
+ let sending = false, readRevision = 0;
143
+ const acknowledgedReads = new Map();
144
  const drafts = new Map(), clearTimes = new Map(), pendingMessages = new Map();
145
  const componentId = `chat-${++chatSequence}`;
146
  const sessionMatches = captureRequestSession();
 
154
  });
155
  scope.addCleanup(() => {
156
  clearTimeout(pollTimer); pollTimer = null; observer?.disconnect();
157
+ drafts.clear(); clearTimes.clear(); pendingMessages.clear(); acknowledgedReads.clear(); allMessages = []; currentTargetInfo = null; currentChatTarget = null;
158
  inputField.value = ''; messagesArea.replaceChildren(); chatListContainer.replaceChildren(); chatHeader.replaceChildren();
159
  window.dispatchEvent(new CustomEvent('comfy-chat-unread-changed'));
160
  });
 
224
  const loadChatList = async () => {
225
  if (!scope.isCurrent()) return [];
226
  const generation = ++listGeneration;
227
+ const requestedReadRevision = readRevision;
228
  try {
229
  const res = await api.getChatList(currentUser.account, options);
230
  if (!scope.isCurrent() || generation !== listGeneration) return [];
231
  if (res?.status !== 'success' || !Array.isArray(res.data)) throw new Error('Invalid chat list');
232
  const list = res.data.filter(chat => chat && typeof chat === 'object' && !Array.isArray(chat)
233
+ && typeof (chat.target_account || chat.account || chat.receiver || chat.sender) === 'string')
234
+ .map(chat => (acknowledgedReads.get(chat.target_account || chat.account || chat.receiver || chat.sender) || 0) > requestedReadRevision
235
+ ? { ...chat, unread_count: 0 } : chat);
236
  renderChatList(list); return list;
237
  } catch (_) {
238
  if (scope.isCurrent() && generation === listGeneration) showToast(t('chat.load_list_failed'), 'error');
 
399
  || (msg.sender === targetAccount && msg.receiver === currentUser.account)))
400
  .map(msg => ({ ...msg })).sort((a, b) => _normalizeTs(a.created_at) - _normalizeTs(b.created_at));
401
  renderMsgs(messages, avatar);
402
+ acknowledgedReads.set(targetAccount, ++readRevision);
403
+ for (const button of chatListContainer.querySelectorAll('.chat-list-item')) {
404
+ if (button.dataset.account === targetAccount) button.querySelector('.chat-unread')?.remove();
405
+ }
406
+ window.dispatchEvent(new CustomEvent('comfy-chat-unread-changed'));
407
  } catch (_) {
408
  if (!current()) return;
409
  showToast(t('chat.load_history_failed'), 'error');
 
465
  const poll = async () => {
466
  if (!scope.isCurrent()) return;
467
  const list = await loadChatList();
468
+ if (scope.isCurrent() && currentChatTarget && list.some(chat => (chat.target_account || chat.account || chat.receiver || chat.sender) === currentChatTarget
469
+ && validUnreadCount(chat.unread_count) > 0)) {
470
  await loadChatHistory(currentChatTarget, currentTargetInfo?.name, currentTargetInfo?.avatar);
471
  }
472
  if (scope.isCurrent()) pollTimer = setTimeout(poll, CACHE.MESSAGE_POLL.INTERVAL);
前端页面/styles/主题基础.css CHANGED
@@ -1706,3 +1706,28 @@ button.ranking-rating__star { cursor: pointer; min-width: 28px; min-height: 28px
1706
  @media (forced-colors: active) { .posts-card__play { background: Canvas; border: 1px solid CanvasText; } .posts-card__play-icon { border-left-color: CanvasText; } }
1707
 
1708
  .creator-card__summary:focus-visible { outline:2px solid var(--rk-focus,#8ab4ff); outline-offset:-3px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1706
  @media (forced-colors: active) { .posts-card__play { background: Canvas; border: 1px solid CanvasText; } .posts-card__play-icon { border-left-color: CanvasText; } }
1707
 
1708
  .creator-card__summary:focus-visible { outline:2px solid var(--rk-focus,#8ab4ff); outline-offset:-3px; }
1709
+
1710
+ .comfy-ranking-root .comfy-ranking-request-diagnostic {
1711
+ box-sizing: border-box;
1712
+ grid-column: 1 / -1;
1713
+ width: 100%; max-width: 100%; min-width: 0;
1714
+ padding: 12px;
1715
+ color: var(--rk-muted);
1716
+ text-align: start;
1717
+ }
1718
+ .comfy-ranking-request-diagnostic summary { cursor: pointer; min-height: 24px; }
1719
+ .comfy-ranking-request-diagnostic__label { display: grid; gap: 6px; margin: 10px 0; }
1720
+ .comfy-ranking-request-diagnostic input {
1721
+ box-sizing: border-box; width: 100%; min-width: 0; min-height: 28px;
1722
+ color: var(--rk-text); background: var(--rk-panel);
1723
+ border: 1px solid var(--rk-border); border-radius: var(--rk-radius-sm);
1724
+ padding: 6px; font-family: monospace;
1725
+ }
1726
+ .comfy-ranking-request-diagnostic button {
1727
+ min-width: 48px; min-height: 28px; padding: 4px 10px;
1728
+ color: var(--rk-text); background: var(--rk-panel);
1729
+ border: 1px solid var(--rk-border); border-radius: var(--rk-radius-sm); cursor: pointer;
1730
+ }
1731
+ .comfy-ranking-request-diagnostic button[aria-busy="true"] { opacity: .65; }
1732
+ .comfy-ranking-request-diagnostic__feedback { margin: 6px 0 0; overflow-wrap: anywhere; }
1733
+ .comfy-ranking-request-diagnostic summary:focus-visible { outline: 2px solid var(--rk-focus); outline-offset: 3px; }
前端页面/task/任务榜组件.js CHANGED
@@ -344,7 +344,7 @@ export function createTasksView(currentUser, keyword = "") {
344
  showToast(t('task.network_cache'), "warning");
345
  continueSearchPagination();
346
  } else {
347
- renderListLoadError(tasksList, t('task.load_failed'), () => loadTasks(1, false), t);
348
  }
349
  }
350
  }
 
344
  showToast(t('task.network_cache'), "warning");
345
  continueSearchPagination();
346
  } else {
347
+ renderListLoadError(tasksList, t('task.load_failed'), () => loadTasks(1, false), t, err);
348
  }
349
  }
350
  }