ZHIWEI666 commited on
Commit
ae7f0c4
·
verified ·
1 Parent(s): 3dc44aa

优化消息提醒

Browse files
app.py CHANGED
@@ -48,7 +48,7 @@ from router_users_social import router as users_social_router
48
  # ==========================================
49
  # 其他业务模块
50
  # ==========================================
51
- from router_items import router as items_router # 📦 内容管理(工具/应用/推荐)
52
  from router_comments import router as comments_router # 💬 评论系统
53
  from router_messages import router as messages_router # ✉️ 私信系统
54
  from router_wallet import router as wallet_router # 💰 钱包/提现
 
48
  # ==========================================
49
  # 其他业务模块
50
  # ==========================================
51
+ from router_items import router as items_router # 📦 内容管理(工具/Skill/应用/推荐)
52
  from router_comments import router as comments_router # 💬 评论系统
53
  from router_messages import router as messages_router # ✉️ 私信系统
54
  from router_wallet import router as wallet_router # 💰 钱包/提现
router_items.py CHANGED
@@ -129,6 +129,7 @@ def _build_creator_trend_data(account: str, u_items: list, months: list) -> dict
129
  提取为独立函数供列表接口和详情接口复用
130
  """
131
  trend_tools = {m: 0 for m in months}
 
132
  trend_apps = {m: 0 for m in months}
133
  trend_recommends = {m: 0 for m in months}
134
 
@@ -138,6 +139,9 @@ def _build_creator_trend_data(account: str, u_items: list, months: list) -> dict
138
  if itype == "tool" or itype == "recommend_tool":
139
  for m, v in history.items():
140
  if m in trend_tools: trend_tools[m] += v
 
 
 
141
  elif itype == "app" or itype == "recommend_app":
142
  for m, v in history.items():
143
  if m in trend_apps: trend_apps[m] += v
@@ -147,7 +151,8 @@ def _build_creator_trend_data(account: str, u_items: list, months: list) -> dict
147
 
148
  return {
149
  "months": months,
150
- "tools": [trend_tools[m] for m in months],
 
151
  "apps": [trend_apps[m] for m in months],
152
  "recommends": [trend_recommends[m] for m in months]
153
  }
@@ -189,12 +194,15 @@ async def search_creators(keyword: str, sort: str = "downloads", limit: int = 50
189
  continue
190
 
191
  tools_count = 0
 
192
  apps_count = 0
193
 
194
  for i in u_items:
195
  itype = i.get("type", "")
196
  if itype == "tool":
197
  tools_count += 1
 
 
198
  elif itype == "app":
199
  apps_count += 1
200
 
@@ -206,7 +214,7 @@ async def search_creators(keyword: str, sort: str = "downloads", limit: int = 50
206
  "downloads": sum(i.get("uses", 0) for i in u_items),
207
  "views": sum(i.get("views", 0) for i in u_items),
208
  "daily_views": sum(i.get("daily_views", 0) for i in u_items),
209
- "toolsCount": tools_count, "appsCount": apps_count, "followers": len(u.get("followers", [])), "created_at": u.get("created_at", 0),
210
  "recent_tips": u.get("tip_history", {}).get(datetime.date.today().strftime("%Y-%m"), 0),
211
  })
212
 
@@ -249,12 +257,15 @@ async def get_creators(sort: str = "downloads", limit: int = 100):
249
  u_items = author_items_index.get(account, [])
250
 
251
  tools_count = 0
 
252
  apps_count = 0
253
 
254
  for i in u_items:
255
  itype = i.get("type", "")
256
  if itype == "tool":
257
  tools_count += 1
 
 
258
  elif itype == "app":
259
  apps_count += 1
260
 
@@ -266,7 +277,7 @@ async def get_creators(sort: str = "downloads", limit: int = 100):
266
  "downloads": sum(i.get("uses", 0) for i in u_items),
267
  "views": sum(i.get("views", 0) for i in u_items),
268
  "daily_views": sum(i.get("daily_views", 0) for i in u_items),
269
- "toolsCount": tools_count, "appsCount": apps_count, "followers": len(u.get("followers", [])), "created_at": u.get("created_at", 0),
270
  "recent_tips": u.get("tip_history", {}).get(datetime.date.today().strftime("%Y-%m"), 0), # 🚀 新增:本月收益统计
271
  })
272
 
 
129
  提取为独立函数供列表接口和详情接口复用
130
  """
131
  trend_tools = {m: 0 for m in months}
132
+ trend_skills = {m: 0 for m in months}
133
  trend_apps = {m: 0 for m in months}
134
  trend_recommends = {m: 0 for m in months}
135
 
 
139
  if itype == "tool" or itype == "recommend_tool":
140
  for m, v in history.items():
141
  if m in trend_tools: trend_tools[m] += v
142
+ elif itype == "skill" or itype == "recommend_skill":
143
+ for m, v in history.items():
144
+ if m in trend_skills: trend_skills[m] += v
145
  elif itype == "app" or itype == "recommend_app":
146
  for m, v in history.items():
147
  if m in trend_apps: trend_apps[m] += v
 
151
 
152
  return {
153
  "months": months,
154
+ "tools": [trend_tools[m] for m in months],
155
+ "skills": [trend_skills[m] for m in months],
156
  "apps": [trend_apps[m] for m in months],
157
  "recommends": [trend_recommends[m] for m in months]
158
  }
 
194
  continue
195
 
196
  tools_count = 0
197
+ skills_count = 0
198
  apps_count = 0
199
 
200
  for i in u_items:
201
  itype = i.get("type", "")
202
  if itype == "tool":
203
  tools_count += 1
204
+ elif itype == "skill":
205
+ skills_count += 1
206
  elif itype == "app":
207
  apps_count += 1
208
 
 
214
  "downloads": sum(i.get("uses", 0) for i in u_items),
215
  "views": sum(i.get("views", 0) for i in u_items),
216
  "daily_views": sum(i.get("daily_views", 0) for i in u_items),
217
+ "toolsCount": tools_count, "skillsCount": skills_count, "appsCount": apps_count, "followers": len(u.get("followers", [])), "created_at": u.get("created_at", 0),
218
  "recent_tips": u.get("tip_history", {}).get(datetime.date.today().strftime("%Y-%m"), 0),
219
  })
220
 
 
257
  u_items = author_items_index.get(account, [])
258
 
259
  tools_count = 0
260
+ skills_count = 0
261
  apps_count = 0
262
 
263
  for i in u_items:
264
  itype = i.get("type", "")
265
  if itype == "tool":
266
  tools_count += 1
267
+ elif itype == "skill":
268
+ skills_count += 1
269
  elif itype == "app":
270
  apps_count += 1
271
 
 
277
  "downloads": sum(i.get("uses", 0) for i in u_items),
278
  "views": sum(i.get("views", 0) for i in u_items),
279
  "daily_views": sum(i.get("daily_views", 0) for i in u_items),
280
+ "toolsCount": tools_count, "skillsCount": skills_count, "appsCount": apps_count, "followers": len(u.get("followers", [])), "created_at": u.get("created_at", 0),
281
  "recent_tips": u.get("tip_history", {}).get(datetime.date.today().strftime("%Y-%m"), 0), # 🚀 新增:本月收益统计
282
  })
283
 
router_messages.py CHANGED
@@ -7,7 +7,6 @@ import uuid
7
  import subprocess
8
  import os
9
  import 数据库连接 as db
10
- from notifications import add_notification
11
  from models import PrivateMessage
12
  from 安全认证 import require_auth, is_admin
13
 
@@ -177,7 +176,8 @@ async def send_private_message(request: Request, msg: PrivateMessage, current_us
177
  chats_db[conv_id].append(chat_msg)
178
  db.save_data("chats.json", chats_db)
179
 
180
- add_notification(msg.receiver, {"type": "private", "from_user": msg.sender, "content": msg.content})
 
181
  return {"status": "success"}
182
 
183
  @router.get("/api/chats/{account}")
@@ -265,9 +265,11 @@ async def get_messages(account: str, count_only: bool = False, current_user: str
265
  now = int(time.time())
266
  seven_days = 7 * 24 * 3600
267
 
268
- # 只统计未过期的未读消息
 
269
  unread = sum(1 for m in user_msgs
270
  if not m.get("is_read")
 
271
  and (now - m.get("created_at", 0) < seven_days))
272
  return {"status": "success", "unread_count": unread}
273
 
 
7
  import subprocess
8
  import os
9
  import 数据库连接 as db
 
10
  from models import PrivateMessage
11
  from 安全认证 import require_auth, is_admin
12
 
 
176
  chats_db[conv_id].append(chat_msg)
177
  db.save_data("chats.json", chats_db)
178
 
179
+ # 🔔 私信未读提醒改由聊天入口(✉️红点)承载(读取 chats.json 每会话未读数),
180
+ # 不再向 messages.json 写入 private 铃铛通知,避免与聊天红点重复计数
181
  return {"status": "success"}
182
 
183
  @router.get("/api/chats/{account}")
 
265
  now = int(time.time())
266
  seven_days = 7 * 24 * 3600
267
 
268
+ # 🔔 只统计未过期的未读消息;私信提醒已迁移至聊天入口(✉️红点)、
269
+ # 插件更新提醒已迁移至卡片徽章,铃铛未读数不再计入这两类,避免同一提醒重复出现
270
  unread = sum(1 for m in user_msgs
271
  if not m.get("is_read")
272
+ and m.get("type") not in ("private", "plugin_update")
273
  and (now - m.get("created_at", 0) < seven_days))
274
  return {"status": "success", "unread_count": unread}
275
 
router_proxy.py CHANGED
@@ -88,6 +88,10 @@ async def proxy_github_zip(req_data: ProxyGithubZipRequest, account: str = Depen
88
  cache_headers = {}
89
  if content_length:
90
  cache_headers["Content-Length"] = content_length
 
 
 
 
91
 
92
  async def cache_stream_generator():
93
  try:
@@ -168,6 +172,17 @@ async def proxy_github_zip(req_data: ProxyGithubZipRequest, account: str = Depen
168
  if content_length:
169
  resp_headers["Content-Length"] = content_length
170
 
 
 
 
 
 
 
 
 
 
 
 
171
  async def stream_generator():
172
  try:
173
  async for chunk in response.aiter_bytes():
 
88
  cache_headers = {}
89
  if content_length:
90
  cache_headers["Content-Length"] = content_length
91
+ # 🔖 回传本次实际下发的版本戳(缓存命中即 item_version['hash']),
92
+ # 供本地私有安装写入 LocalVer 驱动卡片更新徽章(前端 installed_version 优先)
93
+ if item_version.get('hash'):
94
+ cache_headers["X-Installed-Version"] = item_version['hash']
95
 
96
  async def cache_stream_generator():
97
  try:
 
172
  if content_length:
173
  resp_headers["Content-Length"] = content_length
174
 
175
+ # 🔖 缓存未命中(含 versions.json 尚未填充)时,现取仓库最新 commit 短 hash 回传,
176
+ # 补齐私有工具"安装瞬间无版本戳"的边界,使卡片更新徽章可正常触发。
177
+ # 解析失败不影响 ZIP 下发(前端会兜底 latest_version)
178
+ try:
179
+ from 云端_定时版本检测引擎 import fetch_latest_github_hash
180
+ live_hash = await fetch_latest_github_hash(repo_url, active_token)
181
+ if live_hash:
182
+ resp_headers["X-Installed-Version"] = live_hash
183
+ except Exception as ver_err:
184
+ print(f"[proxy_github_zip] ⚠️ 版本戳解析失败(不影响下发): {ver_err}")
185
+
186
  async def stream_generator():
187
  try:
188
  async for chunk in response.aiter_bytes():
云端_定时版本检测引擎.py CHANGED
@@ -8,9 +8,6 @@ import os
8
  import tempfile
9
  import zipfile
10
  import 数据库连接 as db
11
- from notifications import add_notification
12
- from database_sql import SessionLocal
13
- from models_sql import Ownership
14
  from huggingface_hub import upload_file, HfApi
15
 
16
  logger = logging.getLogger("ComfyUI-Ranking.VersionCheck")
@@ -83,107 +80,6 @@ async def fetch_latest_github_hash(repo_url, token):
83
  logger.error(f"检测版本失败 {repo_url}: {e}")
84
  return None
85
 
86
- async def trigger_update_notifications(updated_items: list):
87
- """
88
- 向已购买/已安装插件的用户发送更新通知
89
-
90
- 参数:
91
- updated_items: 有更新的插件列表,每项包含 id, title, version_hash
92
- """
93
- if not updated_items:
94
- return
95
-
96
- try:
97
- # 加载已通知记录,防止重复通知
98
- update_notifications_db = db.load_data("update_notifications.json", default_data={})
99
-
100
- # 构建插件ID到最新版本号的映射
101
- updated_versions = {item["id"]: item["version_hash"] for item in updated_items}
102
-
103
- # 筛选出需要通知的插件(版本号变化才通知)
104
- items_to_notify = []
105
- for item in updated_items:
106
- item_id = item["id"]
107
- current_version = item["version_hash"]
108
- last_notified_version = update_notifications_db.get(item_id)
109
-
110
- if last_notified_version != current_version:
111
- items_to_notify.append(item)
112
-
113
- if not items_to_notify:
114
- print("📢 所有插件更新已通知过,无需重复发送")
115
- return
116
-
117
- # 从 SQL 数据库查询购买记录
118
- with SessionLocal() as session:
119
- # 为每个需要通知的插件,查找已购买用户并发送通知
120
- total_notified = 0
121
- batch_size = 50 # 分批发送,每扐50个用户
122
-
123
- for item in items_to_notify:
124
- item_id = item["id"]
125
- title = item.get("title", "未知插件")
126
- version_hash = item["version_hash"]
127
-
128
- # 从 SQL ownerships 表查询已购买该插件的用户(排除已退款记录)
129
- ownerships = session.query(Ownership).filter(
130
- Ownership.item_id == item_id,
131
- Ownership.is_refunded == False
132
- ).all()
133
-
134
- # 使用 set 去重用户账号
135
- target_users = list(set(record.account for record in ownerships if record.account))
136
-
137
- if not target_users:
138
- # 无人购买,但仍记录已通知版本
139
- update_notifications_db[item_id] = version_hash
140
- continue
141
-
142
- # 准备通知数据
143
- notif_data = {
144
- "type": "plugin_update",
145
- "from_user": "system",
146
- "target_item_id": item_id,
147
- "target_item_title": title,
148
- "content": f"您已安装的插件 [{title}] 有新版本可用"
149
- }
150
-
151
- # 分批发送通知
152
- notified_count = 0
153
- failed_users = []
154
- for i in range(0, len(target_users), batch_size):
155
- batch = target_users[i:i + batch_size]
156
-
157
- for account in batch:
158
- try:
159
- add_notification(account, notif_data)
160
- notified_count += 1
161
- except Exception as e:
162
- failed_users.append(account)
163
- logger.error(f"发送更新通知失败 (user={account}, item={item_id}): {e}")
164
- # 单个失败不影响其他用户
165
-
166
- if failed_users:
167
- logger.warning(f"NOTIFY_FAILED | count={len(failed_users)} | users={failed_users[:10]}")
168
-
169
- # 每批之间短暂休眠,避免通知风暴
170
- if i + batch_size < len(target_users):
171
- await asyncio.sleep(0.5)
172
-
173
- # 记录该插件已通知的版本
174
- update_notifications_db[item_id] = version_hash
175
- total_notified += notified_count
176
- print(f"📢 插件 [{title}] 已向 {notified_count} 位用户发送更新通知")
177
-
178
- # 保存已通知记录
179
- db.save_data("update_notifications.json", update_notifications_db)
180
- print(f"✅ 更新通知发送完成,共通知 {total_notified} 位用户,涉及 {len(items_to_notify)} 个插件")
181
-
182
- except Exception as e:
183
- logger.error(f"触发更新通知时发生错误: {e}")
184
- # 不抛出异常,让主流程继续
185
-
186
-
187
  async def precache_github_zip(repo_url: str, token: Optional[str], item_id: str, version_hash: str):
188
  """预下载 GitHub ZIP 并上传到 HF Dataset 缓存"""
189
  try:
@@ -463,8 +359,8 @@ async def daily_version_check_task():
463
  if updated_count > 0:
464
  db.save_data("versions.json", versions_db)
465
  print(f"✅ 版本检测任务完成,共发现并更新了 {updated_count} 个资源的新版本。")
466
- # 触发更新通知
467
- await trigger_update_notifications(updated_items)
468
  else:
469
  print("✅ 版本检测任务完成,暂无任何新版本发现。")
470
 
 
8
  import tempfile
9
  import zipfile
10
  import 数据库连接 as db
 
 
 
11
  from huggingface_hub import upload_file, HfApi
12
 
13
  logger = logging.getLogger("ComfyUI-Ranking.VersionCheck")
 
80
  logger.error(f"检测版本失败 {repo_url}: {e}")
81
  return None
82
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  async def precache_github_zip(repo_url: str, token: Optional[str], item_id: str, version_hash: str):
84
  """预下载 GitHub ZIP 并上传到 HF Dataset 缓存"""
85
  try:
 
359
  if updated_count > 0:
360
  db.save_data("versions.json", versions_db)
361
  print(f"✅ 版本检测任务完成,共发现并更新了 {updated_count} 个资源的新版本。")
362
+ # 📦 更新提醒改由卡片徽章承载:前端比对本地版本戳与云端 versions.json 的 latest_version,
363
+ # 检测到差异即在卡片上显示「🔄 更新可用」,不再向用户推送 plugin_update 铃铛通知
364
  else:
365
  print("✅ 版本检测任务完成,暂无任何新版本发现。")
366
 
前端页面/components/关于插件组件.js CHANGED
@@ -158,7 +158,62 @@ export function createAboutView(versionString, stageLabel) {
158
  </div>
159
  `);
160
 
161
- // ===== 4. 用户价值区 =====
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  const userValueSection = createCardSection("👤 " + t('about.benefits_title'), `
163
  <div style="display: flex; flex-direction: column; gap: 10px; padding-left: 4px;">
164
  <div style="display: flex; align-items: flex-start; gap: 8px;">
@@ -184,7 +239,7 @@ export function createAboutView(versionString, stageLabel) {
184
  </div>
185
  `);
186
 
187
- // ===== 5. 注意事项区 =====
188
  const warningSection = document.createElement("div");
189
  Object.assign(warningSection.style, {
190
  background: "rgba(240, 160, 48, 0.05)",
@@ -209,7 +264,7 @@ export function createAboutView(versionString, stageLabel) {
209
  </div>
210
  `;
211
 
212
- // ===== 6. 关于作者区 =====
213
  const authorSection = document.createElement("div");
214
  Object.assign(authorSection.style, { ...CARD_BASE_STYLE, textAlign: "center" });
215
  authorSection.innerHTML = `
@@ -261,6 +316,7 @@ export function createAboutView(versionString, stageLabel) {
261
  container.appendChild(heroSection);
262
  container.appendChild(advantageSection);
263
  container.appendChild(featureSection);
 
264
  container.appendChild(userValueSection);
265
  container.appendChild(warningSection);
266
  container.appendChild(authorSection);
 
158
  </div>
159
  `);
160
 
161
+ // ===== 4. 使用指南区 =====
162
+ const guideSection = createCardSection("📖 " + t('about.guide_title'), `
163
+ <div style="display: flex; flex-direction: column; gap: 12px; padding-left: 4px;">
164
+ <div style="display: flex; align-items: flex-start; gap: 8px;">
165
+ <span style="color: #00d4aa; font-weight: bold; font-size: 13px; flex-shrink: 0;">1.</span>
166
+ <div>
167
+ <div style="font-size: 13px; font-weight: bold; color: #ccc; margin-bottom: 2px;">${t('about.guide_install_title')}</div>
168
+ <div style="font-size: 12px; color: #888; line-height: 1.6;">${t('about.guide_install_desc')}</div>
169
+ </div>
170
+ </div>
171
+ <div style="display: flex; align-items: flex-start; gap: 8px;">
172
+ <span style="color: #00d4aa; font-weight: bold; font-size: 13px; flex-shrink: 0;">2.</span>
173
+ <div>
174
+ <div style="font-size: 13px; font-weight: bold; color: #ccc; margin-bottom: 2px;">${t('about.guide_path_title')}</div>
175
+ <div style="font-size: 12px; color: #888; line-height: 1.6;">${t('about.guide_path_desc')}</div>
176
+ </div>
177
+ </div>
178
+ <div style="display: flex; align-items: flex-start; gap: 8px;">
179
+ <span style="color: #00d4aa; font-weight: bold; font-size: 13px; flex-shrink: 0;">3.</span>
180
+ <div>
181
+ <div style="font-size: 13px; font-weight: bold; color: #ccc; margin-bottom: 2px;">${t('about.guide_update_title')}</div>
182
+ <div style="font-size: 12px; color: #888; line-height: 1.6;">${t('about.guide_update_desc')}</div>
183
+ </div>
184
+ </div>
185
+ <div style="display: flex; align-items: flex-start; gap: 8px;">
186
+ <span style="color: #00d4aa; font-weight: bold; font-size: 13px; flex-shrink: 0;">4.</span>
187
+ <div>
188
+ <div style="font-size: 13px; font-weight: bold; color: #ccc; margin-bottom: 2px;">${t('about.guide_publish_title')}</div>
189
+ <div style="font-size: 12px; color: #888; line-height: 1.6;">${t('about.guide_publish_desc')}</div>
190
+ </div>
191
+ </div>
192
+ <div style="display: flex; align-items: flex-start; gap: 8px;">
193
+ <span style="color: #00d4aa; font-weight: bold; font-size: 13px; flex-shrink: 0;">5.</span>
194
+ <div>
195
+ <div style="font-size: 13px; font-weight: bold; color: #ccc; margin-bottom: 2px;">${t('about.guide_buy_title')}</div>
196
+ <div style="font-size: 12px; color: #888; line-height: 1.6;">${t('about.guide_buy_desc')}</div>
197
+ </div>
198
+ </div>
199
+ <div style="display: flex; align-items: flex-start; gap: 8px;">
200
+ <span style="color: #00d4aa; font-weight: bold; font-size: 13px; flex-shrink: 0;">6.</span>
201
+ <div>
202
+ <div style="font-size: 13px; font-weight: bold; color: #ccc; margin-bottom: 2px;">${t('about.guide_refund_title')}</div>
203
+ <div style="font-size: 12px; color: #888; line-height: 1.6;">${t('about.guide_refund_desc')}</div>
204
+ </div>
205
+ </div>
206
+ <div style="display: flex; align-items: flex-start; gap: 8px;">
207
+ <span style="color: #00d4aa; font-weight: bold; font-size: 13px; flex-shrink: 0;">7.</span>
208
+ <div>
209
+ <div style="font-size: 13px; font-weight: bold; color: #ccc; margin-bottom: 2px;">${t('about.guide_withdraw_title')}</div>
210
+ <div style="font-size: 12px; color: #888; line-height: 1.6;">${t('about.guide_withdraw_desc')}</div>
211
+ </div>
212
+ </div>
213
+ </div>
214
+ `);
215
+
216
+ // ===== 5. 用户价值区 =====
217
  const userValueSection = createCardSection("👤 " + t('about.benefits_title'), `
218
  <div style="display: flex; flex-direction: column; gap: 10px; padding-left: 4px;">
219
  <div style="display: flex; align-items: flex-start; gap: 8px;">
 
239
  </div>
240
  `);
241
 
242
+ // ===== 6. 注意事项区 =====
243
  const warningSection = document.createElement("div");
244
  Object.assign(warningSection.style, {
245
  background: "rgba(240, 160, 48, 0.05)",
 
264
  </div>
265
  `;
266
 
267
+ // ===== 7. 关于作者区 =====
268
  const authorSection = document.createElement("div");
269
  Object.assign(authorSection.style, { ...CARD_BASE_STYLE, textAlign: "center" });
270
  authorSection.innerHTML = `
 
316
  container.appendChild(heroSection);
317
  container.appendChild(advantageSection);
318
  container.appendChild(featureSection);
319
+ container.appendChild(guideSection);
320
  container.appendChild(userValueSection);
321
  container.appendChild(warningSection);
322
  container.appendChild(authorSection);
前端页面/components/动画音效引擎.js CHANGED
@@ -494,6 +494,7 @@ export function applyViewportAnimations(container, cards, animationType, isFirst
494
  export function getAnimationTypeForTab(tabId) {
495
  const animationMap = {
496
  'tools': 'cascade', // 工具榜
 
497
  'apps': 'cascade', // 应用榜
498
  'recommends': 'cascade', // 推荐榜
499
  'creators': 'fan', // 创作者榜
 
494
  export function getAnimationTypeForTab(tabId) {
495
  const animationMap = {
496
  'tools': 'cascade', // 工具榜
497
+ 'skills': 'cascade', // Skill榜
498
  'apps': 'cascade', // 应用榜
499
  'recommends': 'cascade', // 推荐榜
500
  'creators': 'fan', // 创作者榜
前端页面/components/用户体验_国际化.js CHANGED
@@ -59,6 +59,7 @@ const translations = {
59
 
60
  // 导航
61
  'nav.tools': '插件榜',
 
62
  'nav.apps': '工作流',
63
  'nav.creators': '创作者',
64
  'nav.recommends': '推荐榜',
@@ -448,7 +449,6 @@ const translations = {
448
  'notif.title': '消息与通知',
449
  'notif.clear': '清空记录',
450
  'notif.no_messages': '暂无任何消息通知',
451
- 'notif.private_msg': '给您发送了私信',
452
  'notif.followed_you': '关注了您',
453
  'notif.liked': '点赞了您的作品',
454
  'notif.favorited': '收藏了您的作品',
@@ -469,9 +469,6 @@ const translations = {
469
  'notif.system_announcement': '系统公告',
470
  'notif.clear_confirm': '确定要清空本地的所有通知记录吗?云端7天前的记录不会再同步下来。',
471
  'notif.cleared': '通知已清空',
472
- 'notif.plugin_update_title': '插件更新',
473
- 'notif.plugin_update': '插件',
474
- 'notif.click_to_view': '点击查看',
475
  'notif.item_not_found': '资源不存在或已被删除',
476
  'notif.load_item_failed': '加载资源失败,请稍后重试',
477
 
@@ -511,12 +508,14 @@ const translations = {
511
  'creator.stats.favorites': '收藏',
512
  'creator.stats.followers': '粉丝',
513
  'creator.output.tools': '产出插件',
 
514
  'creator.output.apps': '产出工作流',
515
  'creator.output.unit': '个',
516
  'creator.chart.download_trend': '📈 下载量增长趋势',
517
  'creator.chart.loading': '正在加载图表...',
518
  'creator.chart.load_failed': '图表库加载失败',
519
  'creator.chart.series.tools': '插件下载',
 
520
  'creator.chart.series.apps': '工作流下载',
521
  'creator.chart.series.recommends': '推荐榜获取',
522
  'creator.tip_board.title': '🎁 赞赏贡献总榜',
@@ -1208,10 +1207,12 @@ const translations = {
1208
  'publish.confirm_publish': '确 认 发 布',
1209
  'publish.main_category': '主类别',
1210
  'publish.type_tool': '原创插件',
 
1211
  'publish.type_app': '原创工作流',
1212
  'publish.type_recommend': '推荐他人作品',
1213
- 'publish.recommend_form': '推荐形式 (三选一)',
1214
  'publish.recommend_as_tool': '作为插件 (支持Git安装)',
 
1215
  'publish.recommend_as_app': '作为工作流 (加载JSON)',
1216
  'publish.recommend_as_link': '纯链接 (新窗口打开)',
1217
  'publish.name': '名称',
@@ -1436,6 +1437,21 @@ const translations = {
1436
  'about.warning_login_desc': '之前注册的账号无法登录的,请修改密码后再登录。',
1437
  'about.warning_account_title': '登录账号错误:',
1438
  'about.warning_account_desc': '可能是你的浏览器自动帮你填入别的网站账号密码,账号是字符(字母、数字、符号)没有中文。',
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1439
  'about.author_title': '关于作者',
1440
  'about.author_thanks': '感谢使用 ComfyUI 社区精选,愿 ComfyUI 生态繁荣昌盛 🌱'
1441
  },
@@ -1488,6 +1504,7 @@ const translations = {
1488
 
1489
  // Navigation
1490
  'nav.tools': 'Plugins',
 
1491
  'nav.apps': 'Workflows',
1492
  'nav.creators': 'Creators',
1493
  'nav.recommends': 'Top Picks',
@@ -1869,7 +1886,6 @@ const translations = {
1869
  'notif.title': 'Messages & Notifications',
1870
  'notif.clear': 'Clear All',
1871
  'notif.no_messages': 'No messages yet',
1872
- 'notif.private_msg': 'sent you a private message',
1873
  'notif.followed_you': 'followed you',
1874
  'notif.liked': 'liked your work',
1875
  'notif.favorited': 'favorited your work',
@@ -1890,9 +1906,6 @@ const translations = {
1890
  'notif.system_announcement': 'System Announcement',
1891
  'notif.clear_confirm': 'Clear all local notifications? Records older than 7 days will not be synced again.',
1892
  'notif.cleared': 'Notifications cleared',
1893
- 'notif.plugin_update_title': 'Plugin Update',
1894
- 'notif.plugin_update': 'Plugin',
1895
- 'notif.click_to_view': 'click to view',
1896
  'notif.item_not_found': 'Item not found or deleted',
1897
  'notif.load_item_failed': 'Failed to load item, please try again later',
1898
 
@@ -1932,12 +1945,14 @@ const translations = {
1932
  'creator.stats.favorites': 'Favorites',
1933
  'creator.stats.followers': 'Followers',
1934
  'creator.output.tools': 'Plugins',
 
1935
  'creator.output.apps': 'Workflows',
1936
  'creator.output.unit': '',
1937
  'creator.chart.download_trend': '📈 Download Growth Trend',
1938
  'creator.chart.loading': 'Loading chart...',
1939
  'creator.chart.load_failed': 'Chart failed to load',
1940
  'creator.chart.series.tools': 'Plugin Downloads',
 
1941
  'creator.chart.series.apps': 'Workflow Downloads',
1942
  'creator.chart.series.recommends': 'Top Picks',
1943
  'creator.tip_board.title': '🎁 Top Contributors',
@@ -2569,10 +2584,12 @@ const translations = {
2569
  'publish.confirm_publish': 'Confirm Publish',
2570
  'publish.main_category': 'Category',
2571
  'publish.type_tool': 'Original Plugin',
 
2572
  'publish.type_app': 'Original Workflow',
2573
  'publish.type_recommend': 'Recommend Others\' Work',
2574
  'publish.recommend_form': 'Recommendation Type',
2575
  'publish.recommend_as_tool': 'As Plugin (Git Install)',
 
2576
  'publish.recommend_as_app': 'As Workflow (Load JSON)',
2577
  'publish.recommend_as_link': 'Link Only (Opens in New Tab)',
2578
  'publish.name': 'Name',
@@ -2795,6 +2812,21 @@ const translations = {
2795
  'about.warning_login_desc': 'If your previously registered account cannot log in, please reset your password and try again.',
2796
  'about.warning_account_title': 'Wrong Account Auto-filled:',
2797
  'about.warning_account_desc': 'Your browser may have auto-filled credentials from another site. Account names only contain letters, numbers, and symbols — no Chinese characters.',
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2798
  'about.author_title': 'About the Author',
2799
  'about.author_thanks': 'Thank you for using ComfyUI Community Hub. May the ComfyUI ecosystem thrive and flourish 🌱'
2800
  }
 
59
 
60
  // 导航
61
  'nav.tools': '插件榜',
62
+ 'nav.skills': 'Skill',
63
  'nav.apps': '工作流',
64
  'nav.creators': '创作者',
65
  'nav.recommends': '推荐榜',
 
449
  'notif.title': '消息与通知',
450
  'notif.clear': '清空记录',
451
  'notif.no_messages': '暂无任何消息通知',
 
452
  'notif.followed_you': '关注了您',
453
  'notif.liked': '点赞了您的作品',
454
  'notif.favorited': '收藏了您的作品',
 
469
  'notif.system_announcement': '系统公告',
470
  'notif.clear_confirm': '确定要清空本地的所有通知记录吗?云端7天前的记录不会再同步下来。',
471
  'notif.cleared': '通知已清空',
 
 
 
472
  'notif.item_not_found': '资源不存在或已被删除',
473
  'notif.load_item_failed': '加载资源失败,请稍后重试',
474
 
 
508
  'creator.stats.favorites': '收藏',
509
  'creator.stats.followers': '粉丝',
510
  'creator.output.tools': '产出插件',
511
+ 'creator.output.skills': '产出Skill',
512
  'creator.output.apps': '产出工作流',
513
  'creator.output.unit': '个',
514
  'creator.chart.download_trend': '📈 下载量增长趋势',
515
  'creator.chart.loading': '正在加载图表...',
516
  'creator.chart.load_failed': '图表库加载失败',
517
  'creator.chart.series.tools': '插件下载',
518
+ 'creator.chart.series.skills': 'Skill下载',
519
  'creator.chart.series.apps': '工作流下载',
520
  'creator.chart.series.recommends': '推荐榜获取',
521
  'creator.tip_board.title': '🎁 赞赏贡献总榜',
 
1207
  'publish.confirm_publish': '确 认 发 布',
1208
  'publish.main_category': '主类别',
1209
  'publish.type_tool': '原创插件',
1210
+ 'publish.type_skill': '原创Skill',
1211
  'publish.type_app': '原创工作流',
1212
  'publish.type_recommend': '推荐他人作品',
1213
+ 'publish.recommend_form': '推荐形式 (四选一)',
1214
  'publish.recommend_as_tool': '作为插件 (支持Git安装)',
1215
+ 'publish.recommend_as_skill': '作为Skill (支持Git安装)',
1216
  'publish.recommend_as_app': '作为工作流 (加载JSON)',
1217
  'publish.recommend_as_link': '纯链接 (新窗口打开)',
1218
  'publish.name': '名称',
 
1437
  'about.warning_login_desc': '之前注册的账号无法登录的,请修改密码后再登录。',
1438
  'about.warning_account_title': '登录账号错误:',
1439
  'about.warning_account_desc': '可能是你的浏览器自动帮你填入别的网站账号密码,账号是字符(字母、数字、符号)没有中文。',
1440
+ 'about.guide_title': '使用指南',
1441
+ 'about.guide_install_title': '安装插件和Skill',
1442
+ 'about.guide_install_desc': '打开喜欢的作品详情页,点「立即获取使用」按钮:免费作品一键装好,付费作品买完后就能装。装完后重启 ComfyUI,新插件就会出现在节点列表里。',
1443
+ 'about.guide_path_title': '文件下载到哪里',
1444
+ 'about.guide_path_desc': '插件和Skill统一装进 ComfyUI 安装目录里的 custom_nodes 文件夹,不用自己搬文件;工作流下载后自动加载到画布上,打开就能用。',
1445
+ 'about.guide_update_title': '更新到新版本',
1446
+ 'about.guide_update_desc': '作者发布新版本后,作品详情页会出现「发现新版本」按钮,点一下自动完成更新,不用先卸载旧版本。',
1447
+ 'about.guide_publish_title': '发布自己的作品',
1448
+ 'about.guide_publish_desc': '点榜单顶部的「发布」按钮,填好名称、介绍和效果图就能提交。插件和Skill填代码仓库的网址,工作流直接上传工作流文件。',
1449
+ 'about.guide_buy_title': '购买付费作品',
1450
+ 'about.guide_buy_desc': '买付费作品要先让账户里有足够余额:进「个人中心-钱包」用支付宝扫码充值。充好后回详情页点「立即获取使用」即完成购买,一次购买终身有效。',
1451
+ 'about.guide_refund_title': '申请退款',
1452
+ 'about.guide_refund_desc': '带「支持退款」标识的作品,购买后24小时内可退:打开作品详情页点退款按钮,积分原路退回余额。注意:退款后30天内不能再买同一作品。',
1453
+ 'about.guide_withdraw_title': '提现赚到的积分',
1454
+ 'about.guide_withdraw_desc': '卖作品或收到打赏赚的积分可以提现:进「个��中心-钱包-提现」,填好金额提交申请,管理员审核后打款给你。终身前100积分免手续费,超出部分收10%手续费。',
1455
  'about.author_title': '关于作者',
1456
  'about.author_thanks': '感谢使用 ComfyUI 社区精选,愿 ComfyUI 生态繁荣昌盛 🌱'
1457
  },
 
1504
 
1505
  // Navigation
1506
  'nav.tools': 'Plugins',
1507
+ 'nav.skills': 'Skills',
1508
  'nav.apps': 'Workflows',
1509
  'nav.creators': 'Creators',
1510
  'nav.recommends': 'Top Picks',
 
1886
  'notif.title': 'Messages & Notifications',
1887
  'notif.clear': 'Clear All',
1888
  'notif.no_messages': 'No messages yet',
 
1889
  'notif.followed_you': 'followed you',
1890
  'notif.liked': 'liked your work',
1891
  'notif.favorited': 'favorited your work',
 
1906
  'notif.system_announcement': 'System Announcement',
1907
  'notif.clear_confirm': 'Clear all local notifications? Records older than 7 days will not be synced again.',
1908
  'notif.cleared': 'Notifications cleared',
 
 
 
1909
  'notif.item_not_found': 'Item not found or deleted',
1910
  'notif.load_item_failed': 'Failed to load item, please try again later',
1911
 
 
1945
  'creator.stats.favorites': 'Favorites',
1946
  'creator.stats.followers': 'Followers',
1947
  'creator.output.tools': 'Plugins',
1948
+ 'creator.output.skills': 'Skills',
1949
  'creator.output.apps': 'Workflows',
1950
  'creator.output.unit': '',
1951
  'creator.chart.download_trend': '📈 Download Growth Trend',
1952
  'creator.chart.loading': 'Loading chart...',
1953
  'creator.chart.load_failed': 'Chart failed to load',
1954
  'creator.chart.series.tools': 'Plugin Downloads',
1955
+ 'creator.chart.series.skills': 'Skill Downloads',
1956
  'creator.chart.series.apps': 'Workflow Downloads',
1957
  'creator.chart.series.recommends': 'Top Picks',
1958
  'creator.tip_board.title': '🎁 Top Contributors',
 
2584
  'publish.confirm_publish': 'Confirm Publish',
2585
  'publish.main_category': 'Category',
2586
  'publish.type_tool': 'Original Plugin',
2587
+ 'publish.type_skill': 'Original Skill',
2588
  'publish.type_app': 'Original Workflow',
2589
  'publish.type_recommend': 'Recommend Others\' Work',
2590
  'publish.recommend_form': 'Recommendation Type',
2591
  'publish.recommend_as_tool': 'As Plugin (Git Install)',
2592
+ 'publish.recommend_as_skill': 'As Skill (Git Install)',
2593
  'publish.recommend_as_app': 'As Workflow (Load JSON)',
2594
  'publish.recommend_as_link': 'Link Only (Opens in New Tab)',
2595
  'publish.name': 'Name',
 
2812
  'about.warning_login_desc': 'If your previously registered account cannot log in, please reset your password and try again.',
2813
  'about.warning_account_title': 'Wrong Account Auto-filled:',
2814
  'about.warning_account_desc': 'Your browser may have auto-filled credentials from another site. Account names only contain letters, numbers, and symbols — no Chinese characters.',
2815
+ 'about.guide_title': 'Usage Guide',
2816
+ 'about.guide_install_title': 'Installing Plugins & Skills',
2817
+ 'about.guide_install_desc': 'Open the detail page of an item you like and click the "Get Now" button: free items install with one click, and paid items install right after purchase. When the installation finishes, restart ComfyUI and the new plugin will appear in your node list.',
2818
+ 'about.guide_path_title': 'Where Files Are Downloaded',
2819
+ 'about.guide_path_desc': 'Plugins and Skills are installed automatically into the custom_nodes folder inside your ComfyUI directory, so you never need to move files yourself. Workflows are loaded straight onto the canvas after download, ready to use.',
2820
+ 'about.guide_update_title': 'Updating to New Versions',
2821
+ 'about.guide_update_desc': 'When an author releases a new version, an "Update available" button appears on the item\'s detail page. Click it to update automatically — no need to remove the old version first.',
2822
+ 'about.guide_publish_title': 'Publishing Your Own Work',
2823
+ 'about.guide_publish_desc': 'Click the "Publish" button at the top of the ranking, fill in the name, description and preview images, then submit. For plugins and Skills, paste the web address of your code repository; for workflows, upload the workflow file directly.',
2824
+ 'about.guide_buy_title': 'Buying Paid Items',
2825
+ 'about.guide_buy_desc': 'Paid items need enough balance in your account first: open "Profile - Wallet" and top up by scanning the Alipay QR code. Then return to the detail page and click "Get Now" to finish the purchase — one purchase, yours forever.',
2826
+ 'about.guide_refund_title': 'Requesting a Refund',
2827
+ 'about.guide_refund_desc': 'Items marked "Refundable" can be refunded within 24 hours of purchase: open the item\'s detail page, click the refund button, and the credits return to your balance. Note: after a refund, the same item cannot be repurchased for 30 days.',
2828
+ 'about.guide_withdraw_title': 'Withdrawing Your Earnings',
2829
+ 'about.guide_withdraw_desc': 'Credits earned from sales or tips can be withdrawn: open "Profile - Wallet - Withdraw", enter the amount and submit. An admin will review and transfer the money to you. The first 100 credits in a lifetime are fee-free; a 10% fee applies to the amount above that.',
2830
  'about.author_title': 'About the Author',
2831
  'about.author_thanks': 'Thank you for using ComfyUI Community Hub. May the ComfyUI ecosystem thrive and flourish 🌱'
2832
  }
前端页面/components/顶部导航组件.js CHANGED
@@ -1,7 +1,7 @@
1
  // 前端页面/components/顶部导航组件.js
2
  import { createAuthView } from "../auth/用户注册登录组件.js";
3
  import { openUserProfileModal } from "../profile/个人中心视图.js";
4
- import { openChatModal } from "../social/私信聊天组件.js";
5
  import { api } from "../core/网络请求API.js";
6
  import { showToast } from "./UI交互提示组件.js";
7
  import { showAboutInfo } from "./关于插件组件.js";
@@ -29,7 +29,9 @@ function _registerNavWindowListeners() {
29
  { event: "comfy-user-logout", handler: () => { if (_navInstance) _navInstance.onLogout(); } },
30
  // 🔧 修复:凭证失效(401)时除停止轮询外,同步重置导航登录态(原实现未重置,头像按钮仍显示已登录)
31
  { event: "comfy-ranking-auth-expired", handler: () => { stopMessagePolling(); if (_navInstance) _navInstance.onLogout(); } },
32
- { event: "storage", handler: (event) => { if (_navInstance) _navInstance.onStorage(event); } }
 
 
33
  ];
34
  for (const { event, handler } of _navWindowHandlers) window.addEventListener(event, handler);
35
  }
@@ -80,8 +82,9 @@ export function createTopNav() {
80
  Object.assign(userActionBtn.style, { padding: "6px 12px", backgroundColor: "#333", color: "#fff", border: "1px solid #555", borderRadius: "4px", cursor: "pointer", fontSize: "12px", transition: "background 0.2s" });
81
 
82
  const chatEntryBtn = document.createElement("button");
83
- Object.assign(chatEntryBtn.style, { background: "transparent", border: "none", color: "#aaa", cursor: "pointer", fontSize: "18px", marginRight: "10px", transition: "0.2s" });
84
- chatEntryBtn.innerHTML = `✉️`;
 
85
  chatEntryBtn.title = t('social.chat');
86
 
87
  const bellBtn = document.createElement("button");
@@ -119,9 +122,10 @@ export function createTopNav() {
119
  userActionBtn.style.borderColor = "#2196F3";
120
  userActionBtn.onclick = () => openUserProfileModal(currentUser);
121
  loadUnreadCount(currentUser, bellBtn);
 
122
 
123
  // 🚀 新增:启动消息定时轮询
124
- startMessagePolling(currentUser, bellBtn);
125
  } else {
126
  // 🚀 新增:停止轮询
127
  stopMessagePolling();
@@ -130,6 +134,7 @@ export function createTopNav() {
130
  userActionBtn.style.backgroundColor = "#333";
131
  userActionBtn.style.borderColor = "#555";
132
  bellBtn.querySelector("#unread-badge").style.display = "none";
 
133
 
134
  userActionBtn.onclick = () => {
135
  const view = createAuthView(async (formData) => {
@@ -240,7 +245,8 @@ export function createTopNav() {
240
  // 🔧 P1修复:注册为当前活跃实例,模块级监听将路由到本实例的回调
241
  _navInstance = {
242
  onLogout: () => { currentUser = null; updateUserButtonState(); },
243
- onStorage: handleCrossTabStorage
 
244
  };
245
  _registerNavWindowListeners();
246
 
@@ -263,7 +269,7 @@ export function createTopNav() {
263
  }
264
 
265
  // 🚀 新增:启动消息定时轮询
266
- function startMessagePolling(currentUser, bellBtn) {
267
  if (!CACHE.MESSAGE_POLL.ENABLED) return;
268
  if (isPollingActive) return; // 🔧 P3优化:防止重复启动
269
 
@@ -279,7 +285,9 @@ function startMessagePolling(currentUser, bellBtn) {
279
  return;
280
  }
281
  try {
 
282
  await loadUnreadCount(currentUser, bellBtn);
 
283
  } catch (e) {
284
  // 静默失败,不影响用户体验
285
  }
 
1
  // 前端页面/components/顶部导航组件.js
2
  import { createAuthView } from "../auth/用户注册登录组件.js";
3
  import { openUserProfileModal } from "../profile/个人中心视图.js";
4
+ import { openChatModal, loadChatUnreadCount } from "../social/私信聊天组件.js";
5
  import { api } from "../core/网络请求API.js";
6
  import { showToast } from "./UI交互提示组件.js";
7
  import { showAboutInfo } from "./关于插件组件.js";
 
29
  { event: "comfy-user-logout", handler: () => { if (_navInstance) _navInstance.onLogout(); } },
30
  // 🔧 修复:凭证失效(401)时除停止轮询外,同步重置导航登录态(原实现未重置,头像按钮仍显示已登录)
31
  { event: "comfy-ranking-auth-expired", handler: () => { stopMessagePolling(); if (_navInstance) _navInstance.onLogout(); } },
32
+ { event: "storage", handler: (event) => { if (_navInstance) _navInstance.onStorage(event); } },
33
+ // 🔔 聊天关闭后刷新 ✉️ 未读红点(打开会话已标记已读,未读数下降,需即时回写)
34
+ { event: "comfy-chat-unread-changed", handler: () => { if (_navInstance) _navInstance.onChatUnreadChanged(); } }
35
  ];
36
  for (const { event, handler } of _navWindowHandlers) window.addEventListener(event, handler);
37
  }
 
82
  Object.assign(userActionBtn.style, { padding: "6px 12px", backgroundColor: "#333", color: "#fff", border: "1px solid #555", borderRadius: "4px", cursor: "pointer", fontSize: "12px", transition: "background 0.2s" });
83
 
84
  const chatEntryBtn = document.createElement("button");
85
+ Object.assign(chatEntryBtn.style, { background: "transparent", border: "none", color: "#aaa", cursor: "pointer", fontSize: "18px", marginRight: "10px", transition: "0.2s", position: "relative" });
86
+ // 🔔 私信未读红点:与铃铛 #unread-badge 同款样式,数据源为 chats.json 各会话未读汇总
87
+ chatEntryBtn.innerHTML = `✉️<span id="chat-unread-badge" style="display:none; position:absolute; top:-4px; right:-8px; background:#F44336; color:white; font-size:10px; font-weight:bold; padding:2px 5px; border-radius:10px; line-height:1;">0</span>`;
88
  chatEntryBtn.title = t('social.chat');
89
 
90
  const bellBtn = document.createElement("button");
 
122
  userActionBtn.style.borderColor = "#2196F3";
123
  userActionBtn.onclick = () => openUserProfileModal(currentUser);
124
  loadUnreadCount(currentUser, bellBtn);
125
+ loadChatUnreadCount(currentUser, chatEntryBtn);
126
 
127
  // 🚀 新增:启动消息定时轮询
128
+ startMessagePolling(currentUser, bellBtn, chatEntryBtn);
129
  } else {
130
  // 🚀 新增:停止轮询
131
  stopMessagePolling();
 
134
  userActionBtn.style.backgroundColor = "#333";
135
  userActionBtn.style.borderColor = "#555";
136
  bellBtn.querySelector("#unread-badge").style.display = "none";
137
+ chatEntryBtn.querySelector("#chat-unread-badge").style.display = "none";
138
 
139
  userActionBtn.onclick = () => {
140
  const view = createAuthView(async (formData) => {
 
245
  // 🔧 P1修复:注册为当前活跃实例,模块级监听将路由到本实例的回调
246
  _navInstance = {
247
  onLogout: () => { currentUser = null; updateUserButtonState(); },
248
+ onStorage: handleCrossTabStorage,
249
+ onChatUnreadChanged: () => loadChatUnreadCount(currentUser, chatEntryBtn)
250
  };
251
  _registerNavWindowListeners();
252
 
 
269
  }
270
 
271
  // 🚀 新增:启动消息定时轮询
272
+ function startMessagePolling(currentUser, bellBtn, chatEntryBtn) {
273
  if (!CACHE.MESSAGE_POLL.ENABLED) return;
274
  if (isPollingActive) return; // 🔧 P3优化:防止重复启动
275
 
 
285
  return;
286
  }
287
  try {
288
+ // 🔔 铃铛未读(点赞/评论/购买/任务/系统公告等)与 ✉️ 私信未读分别刷新
289
  await loadUnreadCount(currentUser, bellBtn);
290
+ await loadChatUnreadCount(currentUser, chatEntryBtn);
291
  } catch (e) {
292
  // 静默失败,不影响用户体验
293
  }
前端页面/core/侧边栏主程序.js CHANGED
@@ -108,6 +108,7 @@ export function buildSidebarDOM() {
108
  // 🌐 多语言支持:Tab 名称使用翻译函数
109
  const tabs = [
110
  { id: "tools", label: t('nav.tools') },
 
111
  { id: "apps", label: t('nav.apps') },
112
  { id: "recommends", label: t('nav.recommends') },
113
  { id: "creators", label: t('nav.creators') },
@@ -365,8 +366,9 @@ export function buildSidebarDOM() {
365
  // 1. 根据 itemType 确定 Tab
366
  let targetTab;
367
  if (itemType === "tool") targetTab = "tools";
 
368
  else if (itemType === "app") targetTab = "apps";
369
- else if (itemType === "recommend") targetTab = "recommends";
370
  else targetTab = "tools"; // 兜底
371
 
372
  // 【问题2修复】如果已经在目标 Tab,先尝试在当前 DOM 中查找卡片
@@ -475,26 +477,27 @@ export function buildSidebarDOM() {
475
  const view = createPublishPromptView(currentUser);
476
  showInlineView(view);
477
  } else {
478
- // 工具/应用/推荐 -> 打开发布内容界面,并自动设置对应类型
479
  const publishView = createPublishView(currentUser,
480
  () => hideInlineView(),
481
  () => { hideInlineView(); triggerLoad(true); },
482
  null, // editItemData
483
- currentTab // initialType: tools/apps/recommends
484
  );
485
  showInlineView(publishView);
486
  }
487
  };
488
 
489
- // Tab 颜色配置:每个 Tab 不同的强调色
490
  const tabColors = {
491
- tools: { active: "#4CAF50", inactive: "#6BBF6B" }, // 工具 - 绿色
492
- apps: { active: "#2196F3", inactive: "#64B5F6" }, // 应用 - 蓝色
493
- recommends: { active: "#FF9800", inactive: "#FFB74D" }, // 推荐榜 - 橙色
494
- creators: { active: "#E91E63", inactive: "#F06292" }, // 创作者 - 粉色
495
- tasks: { active: "#FF5722", inactive: "#FF8A65" }, // 任务榜 - 深橙色
496
- posts: { active: "#9C27B0", inactive: "#BA68C8" }, // 讨论区 - 紫色
497
- prompts: { active: "#00BCD4", inactive: "#4DD0E1" } // 提示词 - 青色
 
498
  };
499
 
500
  // 文字阴影效果:多层阴影确保任何背景下都清晰可见
 
108
  // 🌐 多语言支持:Tab 名称使用翻译函数
109
  const tabs = [
110
  { id: "tools", label: t('nav.tools') },
111
+ { id: "skills", label: t('nav.skills') },
112
  { id: "apps", label: t('nav.apps') },
113
  { id: "recommends", label: t('nav.recommends') },
114
  { id: "creators", label: t('nav.creators') },
 
366
  // 1. 根据 itemType 确定 Tab
367
  let targetTab;
368
  if (itemType === "tool") targetTab = "tools";
369
+ else if (itemType === "skill") targetTab = "skills";
370
  else if (itemType === "app") targetTab = "apps";
371
+ else if (itemType.startsWith("recommend")) targetTab = "recommends";
372
  else targetTab = "tools"; // 兜底
373
 
374
  // 【问题2修复】如果已经在目标 Tab,先尝试在当前 DOM 中查找卡片
 
477
  const view = createPublishPromptView(currentUser);
478
  showInlineView(view);
479
  } else {
480
+ // 工具/Skill/应用/推荐 -> 打开发布内容界面,并自动设置对应类型
481
  const publishView = createPublishView(currentUser,
482
  () => hideInlineView(),
483
  () => { hideInlineView(); triggerLoad(true); },
484
  null, // editItemData
485
+ currentTab // initialType: tools/skills/apps/recommends
486
  );
487
  showInlineView(publishView);
488
  }
489
  };
490
 
491
+ // Tab 颜色配置:按色环渐变顺序排列(绿→青→蓝→紫→粉→红→橙→黄),相邻 Tab 色相平滑过渡
492
  const tabColors = {
493
+ tools: { active: "#4CAF50", inactive: "#81C784" }, // 插件榜 - 绿(渐变起点)
494
+ skills: { active: "#00BCD4", inactive: "#4DD0E1" }, // Skill - 青
495
+ apps: { active: "#2196F3", inactive: "#64B5F6" }, // 工作流 - 蓝
496
+ recommends: { active: "#7C4DFF", inactive: "#B388FF" }, // 推荐榜 - 紫
497
+ creators: { active: "#E91E63", inactive: "#F06292" }, // 创作者 - 粉
498
+ tasks: { active: "#F44336", inactive: "#E57373" }, // 任务榜 - 红
499
+ posts: { active: "#FF9800", inactive: "#FFB74D" }, // 讨论区 - 橙
500
+ prompts: { active: "#FFC107", inactive: "#FFD54F" } // 提示词 - 黄(渐变终点)
501
  };
502
 
503
  // 文字阴影效果:多层阴影确保任何背景下都清晰可见
前端页面/core/侧边栏数据引擎.js CHANGED
@@ -115,7 +115,7 @@ function sortDataLocally(data, tab, sort) {
115
  default: sorted.sort((a, b) => (b.created_at || 0) - (a.created_at || 0)); break; // time
116
  }
117
  } else {
118
- // 工具/应用/推荐排序(与后端 get_items 一致)
119
  switch (sort) {
120
  case "downloads": sorted.sort((a, b) => (b.uses || 0) - (a.uses || 0)); break;
121
  case "likes": sorted.sort((a, b) => (b.likes || 0) - (a.likes || 0)); break;
@@ -400,7 +400,7 @@ export async function loadSidebarContent({
400
  const cards = [];
401
  let targetCard = null; // 用于保存需要展开的目标卡片
402
 
403
- if (tab === "tools" || tab === "apps" || tab === "recommends") {
404
  displayData.forEach(data => {
405
  const card = createItemCard(data, currentUser);
406
  cards.push(card);
@@ -523,8 +523,8 @@ export async function loadSidebarContent({
523
 
524
  try {
525
  let newData;
526
- if (savedTab === "tools" || savedTab === "apps" || savedTab === "recommends") {
527
- const itemType = savedTab === "tools" ? "tool" : (savedTab === "apps" ? "app" : "recommend");
528
  const response = await api.getItems(itemType, savedSort, 200);
529
  newData = proxyImages(response.data || []);
530
  } else if (savedTab === "creators") {
@@ -640,7 +640,7 @@ export async function loadSidebarContent({
640
  state.allData = sortedData;
641
  state.isFullyLoaded = true;
642
  state.isSearchResult = false;
643
- // 🔧 先过滤再切片(tools/apps/recommends 带 keyword 时同样会进入本分支)
644
  const scopedInstant = _applySearchScope(sortedData);
645
  renderBatch(scopedInstant.slice(0, pageSize), false);
646
  if (scopedInstant.length > pageSize) {
@@ -661,8 +661,8 @@ export async function loadSidebarContent({
661
  try {
662
  let response, realData;
663
 
664
- if (tab === "tools" || tab === "apps" || tab === "recommends") {
665
- const itemType = tab === "tools" ? "tool" : (tab === "apps" ? "app" : "recommend");
666
  response = await api.getItems(itemType, sort, 200); // 获取较多数据
667
  realData = response.data || [];
668
  realData = proxyImages(realData); // 确保图片走本地缓存代理
@@ -721,7 +721,7 @@ export async function loadSidebarContent({
721
  // 更新状态
722
  state.allData = realData;
723
 
724
- // 渲染首屏(先过滤再切片:tools/apps/recommends 的 keyword 是本地搜索,
725
  // realData 是未过滤的全量数据,原实现在前 20 条里过滤会漏掉靠后的匹配项)
726
  const scopedReal = _applySearchScope(realData);
727
  renderBatch(scopedReal.slice(0, pageSize), false);
@@ -761,7 +761,7 @@ export async function loadSidebarContent({
761
  if (hasCacheData && cachedData && !(tab === "creators" && keyword)) {
762
  console.warn(`📴 网络失败,降级显示${isCacheExpired ? '过期' : ''}缓存`);
763
  state.allData = cachedData;
764
- // 🔧 先过滤再切片:本分支已排除 creators+keyword,但 tools/apps/recommends
765
  // 带 keyword 时会进来;renderBatch 不再自行过滤,此处必须显式过滤
766
  const scopedCached = _applySearchScope(cachedData);
767
  renderBatch(scopedCached.slice(0, pageSize), false);
 
115
  default: sorted.sort((a, b) => (b.created_at || 0) - (a.created_at || 0)); break; // time
116
  }
117
  } else {
118
+ // 工具/Skill/应用/推荐排序(与后端 get_items 一致)
119
  switch (sort) {
120
  case "downloads": sorted.sort((a, b) => (b.uses || 0) - (a.uses || 0)); break;
121
  case "likes": sorted.sort((a, b) => (b.likes || 0) - (a.likes || 0)); break;
 
400
  const cards = [];
401
  let targetCard = null; // 用于保存需要展开的目标卡片
402
 
403
+ if (tab === "tools" || tab === "skills" || tab === "apps" || tab === "recommends") {
404
  displayData.forEach(data => {
405
  const card = createItemCard(data, currentUser);
406
  cards.push(card);
 
523
 
524
  try {
525
  let newData;
526
+ if (savedTab === "tools" || savedTab === "skills" || savedTab === "apps" || savedTab === "recommends") {
527
+ const itemType = savedTab === "tools" ? "tool" : (savedTab === "skills" ? "skill" : (savedTab === "apps" ? "app" : "recommend"));
528
  const response = await api.getItems(itemType, savedSort, 200);
529
  newData = proxyImages(response.data || []);
530
  } else if (savedTab === "creators") {
 
640
  state.allData = sortedData;
641
  state.isFullyLoaded = true;
642
  state.isSearchResult = false;
643
+ // 🔧 先过滤再切片(tools/skills/apps/recommends 带 keyword 时同样会进入本分支)
644
  const scopedInstant = _applySearchScope(sortedData);
645
  renderBatch(scopedInstant.slice(0, pageSize), false);
646
  if (scopedInstant.length > pageSize) {
 
661
  try {
662
  let response, realData;
663
 
664
+ if (tab === "tools" || tab === "skills" || tab === "apps" || tab === "recommends") {
665
+ const itemType = tab === "tools" ? "tool" : (tab === "skills" ? "skill" : (tab === "apps" ? "app" : "recommend"));
666
  response = await api.getItems(itemType, sort, 200); // 获取较多数据
667
  realData = response.data || [];
668
  realData = proxyImages(realData); // 确保图片走本地缓存代理
 
721
  // 更新状态
722
  state.allData = realData;
723
 
724
+ // 渲染首屏(先过滤再切片:tools/skills/apps/recommends 的 keyword 是本地搜索,
725
  // realData 是未过滤的全量数据,原实现在前 20 条里过滤会漏掉靠后的匹配项)
726
  const scopedReal = _applySearchScope(realData);
727
  renderBatch(scopedReal.slice(0, pageSize), false);
 
761
  if (hasCacheData && cachedData && !(tab === "creators" && keyword)) {
762
  console.warn(`📴 网络失败,降级显示${isCacheExpired ? '过期' : ''}缓存`);
763
  state.allData = cachedData;
764
+ // 🔧 先过滤再切片:本分支已排除 creators+keyword,但 tools/skills/apps/recommends
765
  // 带 keyword 时会进来;renderBatch 不再自行过滤,此处必须显式过滤
766
  const scopedCached = _applySearchScope(cachedData);
767
  renderBatch(scopedCached.slice(0, pageSize), false);
前端页面/core/类型定义.js CHANGED
@@ -80,7 +80,7 @@
80
  /**
81
  * @typedef {Object} ItemData
82
  * @property {string} id - 内容ID
83
- * @property {string} type - 类型:tool/app/recommend
84
  * @property {string} title - 标题
85
  * @property {string} shortDesc - 简短描述
86
  * @property {string} fullDesc - 详细描述
 
80
  /**
81
  * @typedef {Object} ItemData
82
  * @property {string} id - 内容ID
83
+ * @property {string} type - 类型:tool/skill/app/recommend
84
  * @property {string} title - 标题
85
  * @property {string} shortDesc - 简短描述
86
  * @property {string} fullDesc - 详细描述
前端页面/market/列表卡片组件.js CHANGED
@@ -193,8 +193,8 @@ export function createItemCard(itemData, currentUser = null, contextType = null)
193
  detailView.appendChild(originalBadge);
194
  }
195
 
196
- // 支持退款标识(仅工具和应用类型显示,且允许退款时)
197
- if (itemData.allow_refund !== false && (itemData.type === 'tool' || itemData.type === 'app')) {
198
  const refundBadge = document.createElement("div");
199
  Object.assign(refundBadge.style, {
200
  display: "inline-flex", alignItems: "center", gap: "6px",
 
193
  detailView.appendChild(originalBadge);
194
  }
195
 
196
+ // 支持退款标识(仅工具、Skill和应用类型显示,且允许退款时)
197
+ if (itemData.allow_refund !== false && (itemData.type === 'tool' || itemData.type === 'skill' || itemData.type === 'app')) {
198
  const refundBadge = document.createElement("div");
199
  Object.assign(refundBadge.style, {
200
  display: "inline-flex", alignItems: "center", gap: "6px",
前端页面/market/创作者卡片组件.js CHANGED
@@ -279,6 +279,7 @@ export function createCreatorCard(creatorData, currentUser = null) {
279
  </div>
280
  <div style="display: flex; gap: 15px; font-size: 12px; color: #ccc; justify-content: center; border-top: 1px solid var(--border-color, #333); padding-top: 8px;">
281
  <span data-stat="toolsCount" style="color: #2196F3;">🛠️ ${t('creator.output.tools')}: <strong>${creatorData.toolsCount}</strong> ${t('creator.output.unit')}</span>
 
282
  <span data-stat="appsCount" style="color: #9C27B0;">📦 ${t('creator.output.apps')}: <strong>${creatorData.appsCount}</strong> ${t('creator.output.unit')}</span>
283
  </div>
284
  </div>
@@ -413,7 +414,7 @@ export function createCreatorCard(creatorData, currentUser = null) {
413
  chartDom.innerHTML = "";
414
  chartInstance = echarts.init(chartDom, 'dark', { backgroundColor: 'transparent' });
415
 
416
- const data = trendData || { months: [], tools: [], apps: [], recommends: [] };
417
 
418
  chartInstance.setOption({
419
  tooltip: { trigger: 'axis', textStyle: { fontSize: 11 } },
@@ -422,8 +423,8 @@ export function createCreatorCard(creatorData, currentUser = null) {
422
  yAxis: { type: 'value', splitLine: { lineStyle: { color: '#333', type: 'dashed' } }, axisLabel: { color: '#888', fontSize: 10 }, minInterval: 1 },
423
  series: [
424
  { name: t('creator.chart.series.tools'), type: 'line', data: data.tools, smooth: true, itemStyle: { color: '#4CAF50' }, lineStyle: { width: 2, type: 'dashed' } },
 
425
  { name: t('creator.chart.series.apps'), type: 'line', data: data.apps, smooth: true, itemStyle: { color: '#2196F3' }, lineStyle: { width: 2, type: 'dashed' } },
426
- // 【核心修改】:添加第 3 根趋势折线 —— 推荐资源的点击与获取量
427
  { name: t('creator.chart.series.recommends'), type: 'line', data: data.recommends, smooth: true, itemStyle: { color: '#FF9800' }, lineStyle: { width: 2, type: 'dashed' } }
428
  ]
429
  });
 
279
  </div>
280
  <div style="display: flex; gap: 15px; font-size: 12px; color: #ccc; justify-content: center; border-top: 1px solid var(--border-color, #333); padding-top: 8px;">
281
  <span data-stat="toolsCount" style="color: #2196F3;">🛠️ ${t('creator.output.tools')}: <strong>${creatorData.toolsCount}</strong> ${t('creator.output.unit')}</span>
282
+ <span data-stat="skillsCount" style="color: #7C4DFF;">⚡ ${t('creator.output.skills')}: <strong>${creatorData.skillsCount || 0}</strong> ${t('creator.output.unit')}</span>
283
  <span data-stat="appsCount" style="color: #9C27B0;">📦 ${t('creator.output.apps')}: <strong>${creatorData.appsCount}</strong> ${t('creator.output.unit')}</span>
284
  </div>
285
  </div>
 
414
  chartDom.innerHTML = "";
415
  chartInstance = echarts.init(chartDom, 'dark', { backgroundColor: 'transparent' });
416
 
417
+ const data = trendData || { months: [], tools: [], skills: [], apps: [], recommends: [] };
418
 
419
  chartInstance.setOption({
420
  tooltip: { trigger: 'axis', textStyle: { fontSize: 11 } },
 
423
  yAxis: { type: 'value', splitLine: { lineStyle: { color: '#333', type: 'dashed' } }, axisLabel: { color: '#888', fontSize: 10 }, minInterval: 1 },
424
  series: [
425
  { name: t('creator.chart.series.tools'), type: 'line', data: data.tools, smooth: true, itemStyle: { color: '#4CAF50' }, lineStyle: { width: 2, type: 'dashed' } },
426
+ { name: t('creator.chart.series.skills'), type: 'line', data: data.skills || [], smooth: true, itemStyle: { color: '#7C4DFF' }, lineStyle: { width: 2, type: 'dashed' } },
427
  { name: t('creator.chart.series.apps'), type: 'line', data: data.apps, smooth: true, itemStyle: { color: '#2196F3' }, lineStyle: { width: 2, type: 'dashed' } },
 
428
  { name: t('creator.chart.series.recommends'), type: 'line', data: data.recommends, smooth: true, itemStyle: { color: '#FF9800' }, lineStyle: { width: 2, type: 'dashed' } }
429
  ]
430
  });
前端页面/market/发布内容_UI模板.js CHANGED
@@ -19,6 +19,7 @@ export function generatePublishHTML(isEditMode, viewTitle, submitBtnText, hasExi
19
  <label style="display: block; margin-bottom: 5px;">${t('publish.main_category')} <span style="color: #F44336;">*</span></label>
20
  <select id="pub-type" class="form-input">
21
  <option value="tool">🧰 ${t('publish.type_tool')}</option>
 
22
  <option value="app">📦 ${t('publish.type_app')}</option>
23
  <option value="recommend">🌟 ${t('publish.type_recommend')}</option>
24
  </select>
@@ -27,6 +28,7 @@ export function generatePublishHTML(isEditMode, viewTitle, submitBtnText, hasExi
27
  <label style="display: block; margin-bottom: 5px;">${t('publish.recommend_form')} <span style="color: #F44336;">*</span></label>
28
  <select id="pub-recommend-type" class="form-input">
29
  <option value="recommend_tool">🧰 ${t('publish.recommend_as_tool')}</option>
 
30
  <option value="recommend_app">📦 ${t('publish.recommend_as_app')}</option>
31
  <option value="recommend_link">🔗 ${t('publish.recommend_as_link')}</option>
32
  </select>
 
19
  <label style="display: block; margin-bottom: 5px;">${t('publish.main_category')} <span style="color: #F44336;">*</span></label>
20
  <select id="pub-type" class="form-input">
21
  <option value="tool">🧰 ${t('publish.type_tool')}</option>
22
+ <option value="skill">⚡ ${t('publish.type_skill')}</option>
23
  <option value="app">📦 ${t('publish.type_app')}</option>
24
  <option value="recommend">🌟 ${t('publish.type_recommend')}</option>
25
  </select>
 
28
  <label style="display: block; margin-bottom: 5px;">${t('publish.recommend_form')} <span style="color: #F44336;">*</span></label>
29
  <select id="pub-recommend-type" class="form-input">
30
  <option value="recommend_tool">🧰 ${t('publish.recommend_as_tool')}</option>
31
+ <option value="recommend_skill">⚡ ${t('publish.recommend_as_skill')}</option>
32
  <option value="recommend_app">📦 ${t('publish.recommend_as_app')}</option>
33
  <option value="recommend_link">🔗 ${t('publish.recommend_as_link')}</option>
34
  </select>
前端页面/market/发布内容_提交引擎.js CHANGED
@@ -3,11 +3,11 @@ import { api } from "../core/网络请求API.js";
3
  import { showToast } from "../components/UI交互提示组件.js";
4
  import { t } from "../components/用户体验增强.js";
5
  import { removeCache, removeCacheByPrefix } from "../components/性能优化工具.js";
6
- import { TYPE_RULES } from "./发布内容组件.js"; // 📋 三榜发布表单声明式规则(与 UI 联动同源)
7
 
8
  /**
9
  * 📋 获取指定主类型的提交规则(校验与字段组装统一读取,与 UI 显隐同源)
10
- * @param {string} mainType - 主类型: tool/app/recommend
11
  */
12
  function getSubmitRules(mainType) {
13
  return TYPE_RULES[mainType] || TYPE_RULES.tool;
@@ -15,13 +15,15 @@ function getSubmitRules(mainType) {
15
 
16
  /**
17
  * 🟢 根据发布类型清除对应缓存(精确清除,不影响其他页面)
18
- * @param {string} type - 发布类型: tool/app/recommend/recommend_tool/recommend_app/recommend_link
19
  */
20
  function clearItemCacheByType(type) {
21
  // 确定当前类型对应的 tab
22
  let tab = 'recommends';
23
  if (type === 'tool' || type === 'recommend_tool') {
24
  tab = 'tools';
 
 
25
  } else if (type === 'app' || type === 'recommend_app') {
26
  tab = 'apps';
27
  }
@@ -298,7 +300,7 @@ export async function handlePublishSubmit(params) {
298
 
299
  if (!title || !shortDesc) return showToast(t('publish.name_desc_required'), "warning");
300
  if (type === "recommend_link" && !finalLink) return showToast(t('publish.link_required'), "warning");
301
- if ((type === "tool" || type === "recommend_tool") && !isJsonUpload && !isNetdisk && !finalLink) return showToast(t('publish.git_required'), "warning");
302
  if (isJsonUpload && !jsonFile && !finalLink) return showToast(t('publish.json_required'), "warning");
303
  if (isNetdisk && !finalLink) return showToast(t('publish.netdisk_required'), "warning"); // ☁️
304
 
@@ -320,7 +322,7 @@ export async function handlePublishSubmit(params) {
320
  try {
321
  if (isJsonUpload && jsonFile) {
322
  submitBtn.innerHTML = `⏳ ${t('publish.uploading_secure')}`;
323
- const uploadType = type.includes("app") ? "app" : (type.includes("tool") ? "tool" : "recommend");
324
  const jsonUploadRes = await api.uploadFile(jsonFile, uploadType);
325
  finalLink = jsonUploadRes.url;
326
  }
@@ -362,7 +364,7 @@ export async function handlePublishSubmit(params) {
362
  is_original: isOriginal // 🎨 标记为原创作品
363
  };
364
 
365
- // 💸 仅 tool/app 类型提交 allow_refund 字段
366
  if (allowRefund !== null) {
367
  submitData.allow_refund = allowRefund;
368
  }
 
3
  import { showToast } from "../components/UI交互提示组件.js";
4
  import { t } from "../components/用户体验增强.js";
5
  import { removeCache, removeCacheByPrefix } from "../components/性能优化工具.js";
6
+ import { TYPE_RULES } from "./发布内容组件.js"; // 📋 四榜发布表单声明式规则(与 UI 联动同源)
7
 
8
  /**
9
  * 📋 获取指定主类型的提交规则(校验与字段组装统一读取,与 UI 显隐同源)
10
+ * @param {string} mainType - 主类型: tool/skill/app/recommend
11
  */
12
  function getSubmitRules(mainType) {
13
  return TYPE_RULES[mainType] || TYPE_RULES.tool;
 
15
 
16
  /**
17
  * 🟢 根据发布类型清除对应缓存(精确清除,不影响其他页面)
18
+ * @param {string} type - 发布类型: tool/skill/app/recommend/recommend_tool/recommend_skill/recommend_app/recommend_link
19
  */
20
  function clearItemCacheByType(type) {
21
  // 确定当前类型对应的 tab
22
  let tab = 'recommends';
23
  if (type === 'tool' || type === 'recommend_tool') {
24
  tab = 'tools';
25
+ } else if (type === 'skill' || type === 'recommend_skill') {
26
+ tab = 'skills';
27
  } else if (type === 'app' || type === 'recommend_app') {
28
  tab = 'apps';
29
  }
 
300
 
301
  if (!title || !shortDesc) return showToast(t('publish.name_desc_required'), "warning");
302
  if (type === "recommend_link" && !finalLink) return showToast(t('publish.link_required'), "warning");
303
+ if ((type === "tool" || type === "skill" || type === "recommend_tool" || type === "recommend_skill") && !isJsonUpload && !isNetdisk && !finalLink) return showToast(t('publish.git_required'), "warning");
304
  if (isJsonUpload && !jsonFile && !finalLink) return showToast(t('publish.json_required'), "warning");
305
  if (isNetdisk && !finalLink) return showToast(t('publish.netdisk_required'), "warning"); // ☁️
306
 
 
322
  try {
323
  if (isJsonUpload && jsonFile) {
324
  submitBtn.innerHTML = `⏳ ${t('publish.uploading_secure')}`;
325
+ const uploadType = type.includes("app") ? "app" : (type.includes("tool") || type === "skill" ? "tool" : "recommend");
326
  const jsonUploadRes = await api.uploadFile(jsonFile, uploadType);
327
  finalLink = jsonUploadRes.url;
328
  }
 
364
  is_original: isOriginal // 🎨 标记为原创作品
365
  };
366
 
367
+ // 💸 仅 tool/skill/app 类型提交 allow_refund 字段
368
  if (allowRefund !== null) {
369
  submitData.allow_refund = allowRefund;
370
  }
前端页面/market/发布内容组件.js CHANGED
@@ -10,13 +10,17 @@ import { escapeHtml } from "../core/全局配置.js";
10
  // 📌 模块级常量
11
  // ==========================================
12
 
13
- // 📋 三榜发布表单声明式规则:tool 插件榜 / app 工作流 / recommend 推荐榜
14
  // UI 显隐、联动、校验统一读取此配置,避免类型分支散落多处
15
  export const TYPE_RULES = {
16
  tool: {
17
  recommendForm: false, resType: { selectable: ['link', 'netdisk'] },
18
  privateRepoWhenLink: true, price: true, refund: true, originalRequired: true
19
  },
 
 
 
 
20
  app: {
21
  recommendForm: false, resType: { fixed: 'json' },
22
  privateRepoWhenLink: true, price: true, refund: true, originalRequired: true
@@ -24,7 +28,7 @@ export const TYPE_RULES = {
24
  recommend: {
25
  recommendForm: true, resType: { lockedByForm: true },
26
  privateRepoWhenLink: false, price: false, refund: false, originalRequired: false,
27
- formToResType: { recommend_tool: 'link', recommend_app: 'json', recommend_link: 'netdisk' }
28
  }
29
  };
30
 
@@ -305,8 +309,8 @@ export function createPublishView(currentUser, onBackCallback, onSuccessCallback
305
  inputNetdiskPassword.value = editItemData.netdisk_password;
306
  }
307
  } else if (editItemData.link && editItemData.link.includes("huggingface.co")) {
308
- if (editItemData.type === "tool") {
309
- // 🔧 缺陷修复:tool 类型禁用 json 资源,HF 云端文件链接直接按外部链接回显,
310
  // 否则联动回退 link 后链接输入框为空,保存必然校验失败
311
  resTypeSelect.value = "link";
312
  inputLink.value = editItemData.link;
@@ -432,6 +436,8 @@ export function createPublishView(currentUser, onBackCallback, onSuccessCallback
432
  if (!isEditMode && initialType) {
433
  if (initialType === "tool" || initialType === "tools") {
434
  typeSelect.value = "tool";
 
 
435
  } else if (initialType === "app" || initialType === "apps") {
436
  typeSelect.value = "app";
437
  } else if (initialType === "recommend" || initialType === "recommends") {
 
10
  // 📌 模块级常量
11
  // ==========================================
12
 
13
+ // 📋 四榜发布表单声明式规则:tool 插件榜 / skill Skill榜 / app 工作流 / recommend 推荐榜
14
  // UI 显隐、联动、校验统一读取此配置,避免类型分支散落多处
15
  export const TYPE_RULES = {
16
  tool: {
17
  recommendForm: false, resType: { selectable: ['link', 'netdisk'] },
18
  privateRepoWhenLink: true, price: true, refund: true, originalRequired: true
19
  },
20
+ skill: {
21
+ recommendForm: false, resType: { selectable: ['link', 'netdisk'] },
22
+ privateRepoWhenLink: true, price: true, refund: true, originalRequired: true
23
+ },
24
  app: {
25
  recommendForm: false, resType: { fixed: 'json' },
26
  privateRepoWhenLink: true, price: true, refund: true, originalRequired: true
 
28
  recommend: {
29
  recommendForm: true, resType: { lockedByForm: true },
30
  privateRepoWhenLink: false, price: false, refund: false, originalRequired: false,
31
+ formToResType: { recommend_tool: 'link', recommend_skill: 'link', recommend_app: 'json', recommend_link: 'netdisk' }
32
  }
33
  };
34
 
 
309
  inputNetdiskPassword.value = editItemData.netdisk_password;
310
  }
311
  } else if (editItemData.link && editItemData.link.includes("huggingface.co")) {
312
+ if (editItemData.type === "tool" || editItemData.type === "skill") {
313
+ // 🔧 缺陷修复:tool/skill 类型禁用 json 资源,HF 云端文件链接直接按外部链接回显,
314
  // 否则联动回退 link 后链接输入框为空,保存必然校验失败
315
  resTypeSelect.value = "link";
316
  inputLink.value = editItemData.link;
 
436
  if (!isEditMode && initialType) {
437
  if (initialType === "tool" || initialType === "tools") {
438
  typeSelect.value = "tool";
439
+ } else if (initialType === "skill" || initialType === "skills") {
440
+ typeSelect.value = "skill";
441
  } else if (initialType === "app" || initialType === "apps") {
442
  typeSelect.value = "app";
443
  } else if (initialType === "recommend" || initialType === "recommends") {
前端页面/market/资源安装引擎.js CHANGED
@@ -32,7 +32,7 @@ function clearUsesCache() {
32
  // 安装后使用量/下载数仍会从旧缓存返回,改用前缀删除
33
  removeCacheByPrefix('api_/api/items');
34
  removeCacheByPrefix('api_/api/creators');
35
- const tabs = ['tools', 'apps', 'recommends', 'creators'];
36
  const sorts = ['time', 'downloads', 'likes', 'favorites', 'tips', 'views', 'daily_views', 'rating'];
37
  for (const tab of tabs) {
38
  for (const sort of sorts) {
@@ -197,7 +197,7 @@ export function setupResourceInstall(btnUse, itemData, currentUser, inlineStatus
197
  if (!currentUser) return showToast("⚠️ 请先登录您的社区账号后再获取!", "warning");
198
 
199
  const isFree = !itemData.price || itemData.price <= 0 || currentUser.account === itemData.author;
200
- const isTool = itemData.type === 'tool' || itemData.type === 'recommend_tool';
201
  const isApp = itemData.type === 'app' || itemData.type === 'recommend_app';
202
 
203
  // 📱 Web 模式:不支持下载/安装。免费(含作者本人)或已购买 → 仅提醒电脑端安装;未购买 → 放行到下方纯购买流程
@@ -417,8 +417,11 @@ export function setupResourceInstall(btnUse, itemData, currentUser, inlineStatus
417
  }
418
 
419
  // 🚀 安装成功后,盖上本地版本戳
420
- if (itemData.latest_version) {
421
- localStorage.setItem(`ComfyCommunity_LocalVer_${itemData.id}`, itemData.latest_version);
 
 
 
422
  }
423
 
424
  // 记录使用量(后端自动去重)
@@ -474,8 +477,9 @@ export function setupResourceInstall(btnUse, itemData, currentUser, inlineStatus
474
  showToast(`🎉 插件 [${itemData.title}] 安装成功!请重启 ComfyUI。`, "success");
475
  }
476
 
477
- if (itemData.latest_version) {
478
- localStorage.setItem(`ComfyCommunity_LocalVer_${itemData.id}`, itemData.latest_version);
 
479
  }
480
 
481
  try {
 
32
  // 安装后使用量/下载数仍会从旧缓存返回,改用前缀删除
33
  removeCacheByPrefix('api_/api/items');
34
  removeCacheByPrefix('api_/api/creators');
35
+ const tabs = ['tools', 'skills', 'apps', 'recommends', 'creators'];
36
  const sorts = ['time', 'downloads', 'likes', 'favorites', 'tips', 'views', 'daily_views', 'rating'];
37
  for (const tab of tabs) {
38
  for (const sort of sorts) {
 
197
  if (!currentUser) return showToast("⚠️ 请先登录您的社区账号后再获取!", "warning");
198
 
199
  const isFree = !itemData.price || itemData.price <= 0 || currentUser.account === itemData.author;
200
+ const isTool = itemData.type === 'tool' || itemData.type === 'recommend_tool' || itemData.type === 'skill' || itemData.type === 'recommend_skill';
201
  const isApp = itemData.type === 'app' || itemData.type === 'recommend_app';
202
 
203
  // 📱 Web 模式:不支持下载/安装。免费(含作者本人)或已购买 → 仅提醒电脑端安装;未购买 → 放行到下方纯购买流程
 
417
  }
418
 
419
  // 🚀 安装成功后,盖上本地版本戳
420
+ // 优先用本地引擎回传的真实安装 commit(installed_version),兜底云端 latest_version;
421
+ // 二者皆空才跳过——避免版本戳缺失导致卡片更新徽章永不触发
422
+ const _installedVer = result.installed_version || itemData.latest_version;
423
+ if (_installedVer) {
424
+ localStorage.setItem(`ComfyCommunity_LocalVer_${itemData.id}`, _installedVer);
425
  }
426
 
427
  // 记录使用量(后端自动去重)
 
477
  showToast(`🎉 插件 [${itemData.title}] 安装成功!请重启 ComfyUI。`, "success");
478
  }
479
 
480
+ const _installedVer = data.installed_version || itemData.latest_version;
481
+ if (_installedVer) {
482
+ localStorage.setItem(`ComfyCommunity_LocalVer_${itemData.id}`, _installedVer);
483
  }
484
 
485
  try {
前端页面/market/资源详情页面组件.js CHANGED
@@ -217,8 +217,8 @@ function burnLocalFiles(itemId) {
217
  removeCacheByPrefix('api_/api/items');
218
  removeCacheByPrefix('api_/api/creators');
219
 
220
- // 清除工具/应用/推荐列表缓存(使用标准 ListCache 格式)
221
- const tabs = ['tools', 'apps', 'recommends'];
222
  const sorts = ['time', 'downloads', 'likes', 'favorites', 'tips', 'views', 'daily_views', 'rating'];
223
  for (const tab of tabs) {
224
  for (const sort of sorts) {
 
217
  removeCacheByPrefix('api_/api/items');
218
  removeCacheByPrefix('api_/api/creators');
219
 
220
+ // 清除工具/Skill/应用/推荐列表缓存(使用标准 ListCache 格式)
221
+ const tabs = ['tools', 'skills', 'apps', 'recommends'];
222
  const sorts = ['time', 'downloads', 'likes', 'favorites', 'tips', 'views', 'daily_views', 'rating'];
223
  for (const tab of tabs) {
224
  for (const sort of sorts) {
前端页面/profile/个人列表组件.js CHANGED
@@ -191,14 +191,15 @@ export async function renderProfileListContent(tabId, domElement, userData, curr
191
  const timeoutMs = 5000;
192
  const fetchPromise = Promise.all([
193
  api.getItems("tool", "time", 200),
 
194
  api.getItems("app", "time", 200)
195
  ]);
196
  const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve(null), timeoutMs));
197
  const result = await Promise.race([fetchPromise, timeoutPromise]);
198
 
199
  if (result) {
200
- const [toolsRes, appsRes] = result;
201
- cloudItems = [...(toolsRes?.data || []), ...(appsRes?.data || [])];
202
  }
203
 
204
  const cloudMap = {};
@@ -511,13 +512,14 @@ export async function renderProfileListContent(tabId, domElement, userData, curr
511
  }
512
 
513
  try {
514
- const [toolsRes, appsRes, postsRes, promptsRes] = await Promise.all([
515
  api.getItems("tool", "time", 100),
 
516
  api.getItems("app", "time", 100),
517
  api.getPosts(1, 200, "latest"),
518
  api.getPrompts(null, null, 1, 200, "latest")
519
  ]);
520
- const allItems = [...(toolsRes.data || []), ...(appsRes.data || [])];
521
  const allPosts = postsRes.data || [];
522
  const allPrompts = promptsRes.data || [];
523
  const collectedItems = allItems.filter(item => item.favorited_by && item.favorited_by.includes(userData.account));
@@ -553,8 +555,8 @@ export async function renderProfileListContent(tabId, domElement, userData, curr
553
  }
554
 
555
  try {
556
- const [toolsRes, appsRes] = await Promise.all([ api.getItems("tool", "time", 100), api.getItems("app", "time", 100) ]);
557
- const allItems = [...(toolsRes.data || []), ...(appsRes.data || [])];
558
  let filteredItems = [];
559
 
560
  if (tabId === "published") filteredItems = allItems.filter(item => item.author === userData.account);
 
191
  const timeoutMs = 5000;
192
  const fetchPromise = Promise.all([
193
  api.getItems("tool", "time", 200),
194
+ api.getItems("skill", "time", 200),
195
  api.getItems("app", "time", 200)
196
  ]);
197
  const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve(null), timeoutMs));
198
  const result = await Promise.race([fetchPromise, timeoutPromise]);
199
 
200
  if (result) {
201
+ const [toolsRes, skillsRes, appsRes] = result;
202
+ cloudItems = [...(toolsRes?.data || []), ...(skillsRes?.data || []), ...(appsRes?.data || [])];
203
  }
204
 
205
  const cloudMap = {};
 
512
  }
513
 
514
  try {
515
+ const [toolsRes, skillsRes, appsRes, postsRes, promptsRes] = await Promise.all([
516
  api.getItems("tool", "time", 100),
517
+ api.getItems("skill", "time", 100),
518
  api.getItems("app", "time", 100),
519
  api.getPosts(1, 200, "latest"),
520
  api.getPrompts(null, null, 1, 200, "latest")
521
  ]);
522
+ const allItems = [...(toolsRes.data || []), ...(skillsRes.data || []), ...(appsRes.data || [])];
523
  const allPosts = postsRes.data || [];
524
  const allPrompts = promptsRes.data || [];
525
  const collectedItems = allItems.filter(item => item.favorited_by && item.favorited_by.includes(userData.account));
 
555
  }
556
 
557
  try {
558
+ const [toolsRes, skillsRes, appsRes] = await Promise.all([ api.getItems("tool", "time", 100), api.getItems("skill", "time", 100), api.getItems("app", "time", 100) ]);
559
+ const allItems = [...(toolsRes.data || []), ...(skillsRes.data || []), ...(appsRes.data || [])];
560
  let filteredItems = [];
561
 
562
  if (tabId === "published") filteredItems = allItems.filter(item => item.author === userData.account);
前端页面/social/私信聊天组件.js CHANGED
@@ -70,6 +70,9 @@ export function openChatModal(currentUser, targetAccount = null) {
70
  const cleanup = () => {
71
  if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
72
  saveDraft();
 
 
 
73
  };
74
 
75
  topHeader.querySelector("#btn-back-chat").onclick = () => {
@@ -500,8 +503,11 @@ export function openChatModal(currentUser, targetAccount = null) {
500
  if (!CACHE.MESSAGE_POLL || !CACHE.MESSAGE_POLL.ENABLED) return;
501
  pollTimer = setInterval(async () => {
502
  try {
503
- const res = await api.getUnreadCount(currentUser.account);
504
- if (res && res.data && res.data.unread_count > 0) {
 
 
 
505
  loadChatList();
506
  // 如果当前有打开的对话,也刷新消息
507
  if (currentChatTarget) {
@@ -550,4 +556,29 @@ export function openChatModal(currentUser, targetAccount = null) {
550
  }, 0);
551
 
552
  window.dispatchEvent(new CustomEvent("comfy-route-view", { detail: { view: container } }));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
553
  }
 
70
  const cleanup = () => {
71
  if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
72
  saveDraft();
73
+ // 🔔 关闭聊天时通知顶部导航刷新 ✉️ 未读红点:
74
+ // 打开会话已把收到的消息标记为已读,未读数随之下降,需即时回写红点
75
+ window.dispatchEvent(new CustomEvent("comfy-chat-unread-changed"));
76
  };
77
 
78
  topHeader.querySelector("#btn-back-chat").onclick = () => {
 
503
  if (!CACHE.MESSAGE_POLL || !CACHE.MESSAGE_POLL.ENABLED) return;
504
  pollTimer = setInterval(async () => {
505
  try {
506
+ // 🔔 私信未读以会话列表为准:铃铛的 getUnreadCount(count_only) 已不再计入 private,
507
+ // 聊天内轮询改用 getChatList 汇总各会话未读,语义正确且不受铃铛过滤影响
508
+ const res = await api.getChatList(currentUser.account);
509
+ const list = res.data || [];
510
+ if (list.some(c => (c.unread_count || 0) > 0)) {
511
  loadChatList();
512
  // 如果当前有打开的对话,也刷新消息
513
  if (currentChatTarget) {
 
556
  }, 0);
557
 
558
  window.dispatchEvent(new CustomEvent("comfy-route-view", { detail: { view: container } }));
559
+ }
560
+
561
+ // 🔔 聊天未读红点:汇总 chats.json 各会话未读数,驱动顶部导航 ✉️ 按钮的红点。
562
+ // 私信提醒已从铃铛(🔔)迁移至此——铃铛未读数不再计入 private,避免同一提醒重复出现。
563
+ // 与 loadUnreadCount(通知中心) 保持一致的写法:从传入按钮内查询徽标元素并回写。
564
+ export async function loadChatUnreadCount(currentUser, chatBtn) {
565
+ const badge = chatBtn ? chatBtn.querySelector("#chat-unread-badge") : null;
566
+ if (!currentUser) { if (badge) badge.style.display = "none"; return 0; }
567
+ try {
568
+ const res = await api.getChatList(currentUser.account);
569
+ const list = res.data || [];
570
+ const total = list.reduce((sum, c) => sum + (c.unread_count || 0), 0);
571
+ if (badge) {
572
+ if (total > 0) {
573
+ badge.innerText = total > 99 ? "99+" : total;
574
+ badge.style.display = "flex";
575
+ } else {
576
+ badge.style.display = "none";
577
+ }
578
+ }
579
+ return total;
580
+ } catch(e) {
581
+ if (badge) badge.style.display = "none";
582
+ return 0;
583
+ }
584
  }
前端页面/social/通知中心组件.js CHANGED
@@ -1,7 +1,5 @@
1
  // 前端页面/social/通知中心组件.js
2
  import { api } from "../core/网络请求API.js";
3
- import { request } from "../core/网络请求_基础设施.js";
4
- import { openChatModal } from "./私信聊天组件.js";
5
  import { openOtherUserProfileModal } from "../profile/个人中心视图.js";
6
  import { showToast, showConfirm } from "../components/UI交互提示组件.js";
7
  import { CACHE, PLACEHOLDERS } from "../core/全局配置.js";
@@ -89,37 +87,35 @@ export async function openNotificationCenter(currentUser, bellBtn) {
89
 
90
  const renderMsgList = (msgs) => {
91
  listArea.innerHTML = "";
92
- if (msgs.length === 0) {
 
 
 
93
  listArea.innerHTML = `<div style='text-align:center; padding: 30px; color:#666;'>${t('notif.no_messages')}</div>`;
94
  return;
95
  }
96
 
97
  let html = "";
98
- msgs.forEach(msg => {
99
  const isUnread = !msg.is_read;
100
  // 【核心新增】:判断是否为系统公告
101
  const isSystem = msg.type === "system";
102
  const isAnonymous = msg.from_user === "anonymous";
103
 
104
  // 系统公告使用尊贵的橙色/金色 UI 边框,普通消息使用默认颜色
105
- // 插件更新通知使用蓝色主题
106
  let bg = isUnread ? "rgba(76, 175, 80, 0.1)" : "var(--comfy-input-bg)";
107
  let border = isUnread ? "1px solid #4CAF50" : "1px solid #444";
108
 
109
  if (isSystem) {
110
  bg = isUnread ? "rgba(255, 152, 0, 0.15)" : "rgba(255, 152, 0, 0.05)";
111
  border = isUnread ? "1px solid #FF9800" : "1px solid #886020";
112
- } else if (msg.type === "plugin_update") {
113
- bg = isUnread ? "rgba(33, 150, 243, 0.15)" : "rgba(33, 150, 243, 0.05)";
114
- border = isUnread ? "1px solid #2196F3" : "1px solid #1a5a8a";
115
  }
116
 
117
  // 生成标题链接
118
  const titleLink = makeTitleLink(msg.target_item_id, msg.target_item_title);
119
 
120
  let actionText = "";
121
- if (msg.type === "private") actionText = t('notif.private_msg');
122
- else if (msg.type === "follow") actionText = t('notif.followed_you');
123
  else if (msg.type === "like") actionText = `${t('notif.liked')} ${titleLink}`;
124
  else if (msg.type === "favorite") actionText = `${t('notif.favorited')} ${titleLink}`;
125
  else if (msg.type === "comment") actionText = `${t('notif.commented_on')} ${titleLink}:<br><span style="color:#ccc;">${escapeHtml(msg.content)}</span>`;
@@ -139,19 +135,14 @@ export async function openNotificationCenter(currentUser, bellBtn) {
139
  else if (msg.type === "dispute_resolved") actionText = `<span style="color:#9C27B0;">🔨</span> ${escapeHtml(msg.content) || t('notif.dispute_resolved')}`;
140
  // 【核心新增】:系统公告的正文排版,保留原格式的换行
141
  else if (isSystem) actionText = `<div style="margin-top: 6px; color: #eee; font-size: 14px; line-height: 1.6; white-space: pre-wrap;">${escapeHtml(msg.content)}</div>`;
142
- // 【核心新增】:插件更新通知 - 保持现有模板不变
143
- else if (msg.type === "plugin_update") actionText = `${t('notif.plugin_update')} <span style="color:#2196F3;">[${escapeHtml(msg.target_item_title)}]</span>,${t('notif.click_to_view')}`;
144
 
145
  const timeStr = new Date(msg.created_at * 1000).toLocaleString();
146
 
147
  // 【核心新增】:如果是系统消息,增加 📢 标签并改变标题颜色
148
- // 插件更新通知使用 📦 图标
149
  // 匿名打赏和系统通知的用户名不显示为链接
150
  let nameLabel;
151
  if (isSystem) {
152
  nameLabel = `<strong style="color: #FF9800; font-size: 15px;">📢 [${t('notif.system_announcement')}] ${escapeHtml(msg.from_name)}</strong>`;
153
- } else if (msg.type === "plugin_update") {
154
- nameLabel = `<strong style="color: #2196F3; font-size: 15px;">📦 [${t('notif.plugin_update_title')}] ${escapeHtml(msg.from_name)}</strong>`;
155
  } else if (isAnonymous) {
156
  nameLabel = `<strong style="color: #fff;">${escapeHtml(msg.from_name || msg.from_user)}</strong>`;
157
  } else {
@@ -215,12 +206,6 @@ export async function openNotificationCenter(currentUser, bellBtn) {
215
  // 【核心新增】:系统公告点击不跳转,只触发消除红点
216
  if (type === "system") return;
217
 
218
- // 📦 插件更新通知:切换到对应Tab并展开卡片
219
- if (type === "plugin_update" && itemId) {
220
- _navigateToItemView(itemId);
221
- return;
222
- }
223
-
224
  // 📝 任务榜通知:跳转到任务详情
225
  if (type.startsWith("task_") && itemId) {
226
  _navigateToTaskDetail(itemId, currentUser);
@@ -246,12 +231,6 @@ export async function openNotificationCenter(currentUser, bellBtn) {
246
  return;
247
  }
248
 
249
- // 私信:打开对话
250
- if (type === "private") {
251
- openChatModal(currentUser, acc);
252
- return;
253
- }
254
-
255
  // 其他:跳转发送者资料页(兜底)
256
  if (acc && acc !== 'anonymous') openOtherUserProfileModal(acc, currentUser);
257
  };
@@ -304,8 +283,8 @@ export async function openNotificationCenter(currentUser, bellBtn) {
304
  export async function loadUnreadCount(currentUser, bellBtn) {
305
  if (!currentUser) { bellBtn.querySelector("#unread-badge").style.display = "none"; return; }
306
  try {
307
- // 🔥 修复:使用 count_only=true 参数,只获取未读数,不标记已读
308
- const res = await request(`/api/messages/${currentUser.account}?count_only=true`);
309
  const unreadCount = res.unread_count || 0;
310
  const badge = bellBtn.querySelector("#unread-badge");
311
  if (unreadCount > 0) {
 
1
  // 前端页面/social/通知中心组件.js
2
  import { api } from "../core/网络请求API.js";
 
 
3
  import { openOtherUserProfileModal } from "../profile/个人中心视图.js";
4
  import { showToast, showConfirm } from "../components/UI交互提示组件.js";
5
  import { CACHE, PLACEHOLDERS } from "../core/全局配置.js";
 
87
 
88
  const renderMsgList = (msgs) => {
89
  listArea.innerHTML = "";
90
+ // 🔔 私信提醒已迁移至聊天入口(✉️红点)、插件更新提醒已迁移至卡片徽章(🔄),
91
+ // 通知中心不再展示这两类(含存量历史数据),避免同一提醒重复出现
92
+ const visibleMsgs = msgs.filter(m => m.type !== "private" && m.type !== "plugin_update");
93
+ if (visibleMsgs.length === 0) {
94
  listArea.innerHTML = `<div style='text-align:center; padding: 30px; color:#666;'>${t('notif.no_messages')}</div>`;
95
  return;
96
  }
97
 
98
  let html = "";
99
+ visibleMsgs.forEach(msg => {
100
  const isUnread = !msg.is_read;
101
  // 【核心新增】:判断是否为系统公告
102
  const isSystem = msg.type === "system";
103
  const isAnonymous = msg.from_user === "anonymous";
104
 
105
  // 系统公告使用尊贵的橙色/金色 UI 边框,普通消息使用默认颜色
 
106
  let bg = isUnread ? "rgba(76, 175, 80, 0.1)" : "var(--comfy-input-bg)";
107
  let border = isUnread ? "1px solid #4CAF50" : "1px solid #444";
108
 
109
  if (isSystem) {
110
  bg = isUnread ? "rgba(255, 152, 0, 0.15)" : "rgba(255, 152, 0, 0.05)";
111
  border = isUnread ? "1px solid #FF9800" : "1px solid #886020";
 
 
 
112
  }
113
 
114
  // 生成标题链接
115
  const titleLink = makeTitleLink(msg.target_item_id, msg.target_item_title);
116
 
117
  let actionText = "";
118
+ if (msg.type === "follow") actionText = t('notif.followed_you');
 
119
  else if (msg.type === "like") actionText = `${t('notif.liked')} ${titleLink}`;
120
  else if (msg.type === "favorite") actionText = `${t('notif.favorited')} ${titleLink}`;
121
  else if (msg.type === "comment") actionText = `${t('notif.commented_on')} ${titleLink}:<br><span style="color:#ccc;">${escapeHtml(msg.content)}</span>`;
 
135
  else if (msg.type === "dispute_resolved") actionText = `<span style="color:#9C27B0;">🔨</span> ${escapeHtml(msg.content) || t('notif.dispute_resolved')}`;
136
  // 【核心新增】:系统公告的正文排版,保留原格式的换行
137
  else if (isSystem) actionText = `<div style="margin-top: 6px; color: #eee; font-size: 14px; line-height: 1.6; white-space: pre-wrap;">${escapeHtml(msg.content)}</div>`;
 
 
138
 
139
  const timeStr = new Date(msg.created_at * 1000).toLocaleString();
140
 
141
  // 【核心新增】:如果是系统消息,增加 📢 标签并改变标题颜色
 
142
  // 匿名打赏和系统通知的用户名不显示为链接
143
  let nameLabel;
144
  if (isSystem) {
145
  nameLabel = `<strong style="color: #FF9800; font-size: 15px;">📢 [${t('notif.system_announcement')}] ${escapeHtml(msg.from_name)}</strong>`;
 
 
146
  } else if (isAnonymous) {
147
  nameLabel = `<strong style="color: #fff;">${escapeHtml(msg.from_name || msg.from_user)}</strong>`;
148
  } else {
 
206
  // 【核心新增】:系统公告点击不跳转,只触发消除红点
207
  if (type === "system") return;
208
 
 
 
 
 
 
 
209
  // 📝 任务榜通知:跳转到任务详情
210
  if (type.startsWith("task_") && itemId) {
211
  _navigateToTaskDetail(itemId, currentUser);
 
231
  return;
232
  }
233
 
 
 
 
 
 
 
234
  // 其他:跳转发送者资料页(兜底)
235
  if (acc && acc !== 'anonymous') openOtherUserProfileModal(acc, currentUser);
236
  };
 
283
  export async function loadUnreadCount(currentUser, bellBtn) {
284
  if (!currentUser) { bellBtn.querySelector("#unread-badge").style.display = "none"; return; }
285
  try {
286
+ // 🔥 修复:复用 api.getUnreadCount(内部走 count_only=true),只获取未读数、不标记已读
287
+ const res = await api.getUnreadCount(currentUser.account);
288
  const unreadCount = res.unread_count || 0;
289
  const badge = bellBtn.querySelector("#unread-badge");
290
  if (unreadCount > 0) {