Spaces:
Running
Running
Upload 103 files
Browse files- 前端页面/components/用户体验_国际化.js +0 -0
- 前端页面/core/全局配置.js +971 -959
- 前端页面/market/Skill安装流程.js +218 -214
- 前端页面/market/安装来源.js +18 -0
- 前端页面/market/资源安装引擎.js +0 -0
- 前端页面/styles/主题基础.css +0 -0
前端页面/components/用户体验_国际化.js
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
前端页面/core/全局配置.js
CHANGED
|
@@ -1,117 +1,117 @@
|
|
| 1 |
-
// 前端页面/core/全局配置.js
|
| 2 |
-
// ==========================================
|
| 3 |
-
// ⚙️ 全局配置中心
|
| 4 |
-
// ==========================================
|
| 5 |
-
// 作用:集中管理所有配置常量,便于维护和修改
|
| 6 |
-
// 关联文件:
|
| 7 |
-
// - 网络请求API.js (API基础地址)
|
| 8 |
-
// - 性能优化工具.js (缓存配置)
|
| 9 |
-
// - 侧边栏数据引擎.js (分页配置)
|
| 10 |
-
// - 所有业务组件
|
| 11 |
-
// ==========================================
|
| 12 |
-
// 🏗️ P2架构优化:配置集中化
|
| 13 |
-
// 🏗️ P2质量优化:JSDoc 类型注释
|
| 14 |
-
// ==========================================
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
// ==========================================
|
| 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, SSE_MAX_RESPONSE_BYTES: number, SSE_MAX_LINE_BYTES: number, SSE_MAX_LINES: number, SSE_YIELD_LINES: number, SSE_YIELD_BYTES: number, SSE_YIELD_CHUNKS: 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, PROFILE_MEDIA_URL_MAX_BYTES: number, 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, REQUEST_BODY_ERRORS: Record<string, { status: number, msgKey: 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 |
-
REQUEST_BODY_ERRORS: {
|
| 28 |
-
request_body_invalid: { status: 400, msgKey: 'feedback.invalid_request_data' },
|
| 29 |
-
request_body_disconnected: { status: 400, msgKey: 'feedback.network_unavailable' },
|
| 30 |
-
request_body_too_large: { status: 413, msgKey: 'feedback.request_too_large' },
|
| 31 |
-
request_body_timeout: { status: 408, msgKey: 'feedback.request_receive_timeout' },
|
| 32 |
-
request_body_busy: { status: 429, msgKey: 'feedback.request_busy' },
|
| 33 |
-
request_body_encoding: { status: 415, msgKey: 'feedback.request_encoding_unsupported' },
|
| 34 |
-
},
|
| 35 |
-
// 仅公开榜单使用由正文派生的条件验证器,私有响应仍禁止留存。
|
| 36 |
-
PUBLIC_CONDITIONAL_GET_PATHS: ['/api/items', '/api/creators', '/api/creators/search', '/api/posts', '/api/prompts', '/api/tasks'],
|
| 37 |
-
CONDITIONAL_ETAG_PATTERN: /^W\/"rk-[a-f0-9]{64}"$/,
|
| 38 |
-
|
| 39 |
-
// 云端 API 基础地址
|
| 40 |
-
BASE_URL: "https://zhiwei666-comfyui-ranking-api.hf.space",
|
| 41 |
-
|
| 42 |
-
// 请求超时时间(毫秒)
|
| 43 |
-
TIMEOUT: 30000,
|
| 44 |
-
VIDEO_UPLOAD_TIMEOUT: 300000,
|
| 45 |
-
VIDEO_UPLOAD_RESPONSE_MAX_CHARS: 65536,
|
| 46 |
-
LOCAL_CACHE_STATS_TIMEOUT: 10000,
|
| 47 |
-
LOCAL_CACHE_CLEAR_TIMEOUT: 60000,
|
| 48 |
-
LOCAL_CACHE_RESPONSE_MAX_BYTES: 8192,
|
| 49 |
-
// 工作流终态编码上限为512 MiB,另保留64 MiB进度数据;行预算包含data前缀。
|
| 50 |
-
SSE_MAX_RESPONSE_BYTES: 576 * 1024 * 1024,
|
| 51 |
-
SSE_MAX_LINE_BYTES: 512 * 1024 * 1024,
|
| 52 |
-
SSE_MAX_LINES: 262144,
|
| 53 |
-
SSE_YIELD_LINES: 256,
|
| 54 |
-
SSE_YIELD_BYTES: 1024 * 1024,
|
| 55 |
-
SSE_YIELD_CHUNKS: 256,
|
| 56 |
-
|
| 57 |
-
// GET 请求重试次数(实际尝试 MAX_RETRIES + 1 次)
|
| 58 |
-
// 🔧 修复:此前本值为 3 且全库无人读取,真正的重试次数硬编码在 网络请求_基础设施.js 中为 2,
|
| 59 |
-
// 现改为由基础设施读取本配置;取值 2 以保持既有的「共 3 次尝试」行为不变
|
| 60 |
-
MAX_RETRIES: 2,
|
| 61 |
-
|
| 62 |
-
// 重试间隔(毫秒,指数退避基数:1s、2s、4s…)
|
| 63 |
-
RETRY_DELAY: 1000,
|
| 64 |
-
|
| 65 |
-
// 🐢 弱网预算分级(仅作用于 GET,由 网络请求_基础设施.js 按 endpoint 路径精确匹配)
|
| 66 |
-
// 默认预算下弱网一次列表请求最长要烧 3×30s + 1s + 2s = 93s,期间用户只能盯骨架屏。
|
| 67 |
-
// 列表类:首屏已可由本地缓存先行上屏(见 侧边栏数据引擎.js),且列表往往是会话的第一个请求、
|
| 68 |
-
// 可能撞上 HF Space 冷启动(本项目无预热机制),故只砍掉一次冗余重试、保留 30s 单次超时,
|
| 69 |
-
// 93s → 61s,冷启动仍有机会在前两次尝试内完成
|
| 70 |
-
LIST_RETRIES: 1,
|
| 71 |
-
// 搜索类:必然发生在列表已加载之后(空间已被唤醒),可放心收紧,93s → 10s×2 + 1s = 21s
|
| 72 |
-
SEARCH_TIMEOUT: 10000,
|
| 73 |
-
SEARCH_RETRIES: 1,
|
| 74 |
-
PROFILE_RESOURCE_TIMEOUT: 5000,
|
| 75 |
-
CREATOR_CHART_SCRIPT_URL: "https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js",
|
| 76 |
-
|
| 77 |
-
// 🐢 后台补新预算(仅作用于 GET,由 网络请求_基础设施.js 按「本地是否已有可兜底数据」判定)
|
| 78 |
-
// 七大列表视图都是「本地数据先上屏 → 再发请求补新」,此时界面上已经有内容,
|
| 79 |
-
// 这次请求纯属后台行为;它每多挂 1 秒就多占 1 个并发额度(全局仅 6 个),
|
| 80 |
-
// 云端不通时会把前台请求(详情页、点赞、轮询)全部挤到排队,表现为「切哪个界面都要等」。
|
| 81 |
-
// 故一旦本地已有该请求的缓存(含过期),单次超时压到 10s 且不再重试,最坏耗时 10s
|
| 82 |
-
BACKGROUND_TIMEOUT: 10000,
|
| 83 |
-
BANNER_IMAGE_TIMEOUT: 75000,
|
| 84 |
-
WORKFLOW_DOWNLOAD_TIMEOUT: 120000, // 工作流流式下载预算(2 分钟)
|
| 85 |
-
BACKGROUND_RETRIES: 0,
|
| 86 |
-
|
| 87 |
-
// ☁️ 云端不可达冷却(消费方见 状态管理.js 的「云端可达性」段落)
|
| 88 |
-
// navigator.onLine 只反映本机网卡,云端单独不可达(HF Space 宕机/冷启动、DNS 污染、被墙)
|
| 89 |
-
// 时它仍为 true,于是每个界面、每次轮询都要从头烧一遍完整重试预算,且互不共享失败结论。
|
| 90 |
-
// 首次确证云端不可达后进入冷却:冷却期内所有 GET 一律不联网(有缓存直接返回、无缓存立即失败),
|
| 91 |
-
// 并发额度全部让给前台。被动探测——冷却一过,下一个真实请求充当探针;
|
| 92 |
-
// 期间任意一次成功响应(含 POST/PUT/DELETE)立即清零,不需等到冷却结束
|
| 93 |
-
CLOUD_DOWN_COOLDOWN: 60 * 1000,
|
| 94 |
-
|
| 95 |
-
// 🔍 创作者搜索请求的取消分组 ID
|
| 96 |
-
// 用户在搜索框继续输入时,侧边栏数据引擎.js 据此撤销上一次仍在途的搜索请求。
|
| 97 |
-
// 不撤销的话,作废请求会持续占用并发额度(最多 6 个)与超时预算,
|
| 98 |
-
// 把最新那次搜索挤到后面排队。仅搜索请求归入本分组,不影响其它任何请求
|
| 99 |
-
SEARCH_COMPONENT_ID: "sidebar-search",
|
| 100 |
-
// 上传相对路径的云端图片读取入口。
|
| 101 |
-
IMAGE_PROXY_PATH: "/api/image_proxy",
|
| 102 |
-
PROFILE_MEDIA_URL_MAX_BYTES: 16384,
|
| 103 |
-
// 本地广告预载要求真实图片,熔断占位图不能作为下载成功。
|
| 104 |
-
LOCAL_IMAGE_PROXY_PREFIX: "/community_hub/image?url=",
|
| 105 |
-
COMPLETE_IMAGE_QUERY_SUFFIX: "&require_complete=1",
|
| 106 |
-
// 身份相关或私有 GET 禁止通用响应缓存;公开列表仍使用原缓存策略。
|
| 107 |
-
PRIVATE_GET_PATTERNS: [
|
| 108 |
-
/^\/api\/(wallet|messages|chats|admin|disputes|users)(?:\/|$)/,
|
| 109 |
-
/^\/api\/my-(posts|prompts|tasks)(?:\/|$)/,
|
| 110 |
-
/^\/api\/(tasks|prompts)\/[^/]+$/
|
| 111 |
-
]
|
| 112 |
-
};
|
| 113 |
-
|
| 114 |
-
|
| 115 |
export const LOCAL_PERFORMANCE = Object.freeze({
|
| 116 |
MAX_ENDPOINT_CHARS: 4096,
|
| 117 |
MAX_COUNT: 1000000,
|
|
@@ -121,849 +121,861 @@ export const LOCAL_PERFORMANCE = Object.freeze({
|
|
| 121 |
LABELS: Object.freeze({ tools: 'nav.tools', skills: 'nav.skills', apps: 'nav.apps', recommends: 'nav.recommends', creators: 'nav.creators', tasks: 'nav.tasks', posts: 'nav.posts', prompts: 'nav.prompts' })
|
| 122 |
});
|
| 123 |
|
| 124 |
-
export const SKILL_INSTALL = Object.freeze({
|
| 125 |
-
PATH: '/community_hub/skill_install',
|
| 126 |
-
PREFLIGHT_TIMEOUT: 30000,
|
| 127 |
-
CONTROL_TIMEOUT: 45000,
|
| 128 |
-
RESULT_TIMEOUT: 5000,
|
| 129 |
-
STREAM_TIMEOUT: 1200000,
|
| 130 |
-
RESPONSE_MAX_BYTES: 1024 * 1024,
|
| 131 |
-
PATH_MAX_BYTES: 32768,
|
| 132 |
-
MAX_DIRECTORIES: 128,
|
| 133 |
-
MAX_ITEMS: 1,
|
| 134 |
-
MODAL_WIDTH: '640px'
|
| 135 |
-
});
|
| 136 |
-
|
| 137 |
-
export const CHART_LIFECYCLE = Object.freeze({
|
| 138 |
-
WIDTH_WAIT_TIMEOUT: 5000,
|
| 139 |
-
RESIZE_DELAY: 300
|
| 140 |
-
});
|
| 141 |
-
|
| 142 |
-
// 发布视频的本地读取预算,不改变云端上传额度。
|
| 143 |
-
export const LAZY_IMAGES = Object.freeze({
|
| 144 |
-
ROOT_MARGIN: "200px 0px",
|
| 145 |
-
THRESHOLD: 0.01,
|
| 146 |
-
MOUNT_TIMEOUT: 30000,
|
| 147 |
-
READ_TIMEOUT: 30000
|
| 148 |
-
});
|
| 149 |
-
|
| 150 |
-
export const PUBLISH_MEDIA_LIMITS = Object.freeze({
|
| 151 |
-
MAX_IMAGES: 9,
|
| 152 |
-
IMAGE_MAX_EDGE: 1920,
|
| 153 |
-
IMAGE_QUALITY: 0.85,
|
| 154 |
-
VIDEO_MAX_BYTES: 100 * 1024 * 1024,
|
| 155 |
-
VIDEO_MAX_SECONDS: 180,
|
| 156 |
-
VIDEO_METADATA_TIMEOUT: 15000
|
| 157 |
-
});
|
| 158 |
-
|
| 159 |
-
// 上传前处理预算;浏览器解码本身仍由宿主管理。
|
| 160 |
-
export const IMAGE_PROCESS = {
|
| 161 |
-
HEADER_BYTES: 256 * 1024,
|
| 162 |
-
MAX_INPUT_BYTES: 50 * 1024 * 1024,
|
| 163 |
-
MAX_PIXELS: 40 * 1024 * 1024,
|
| 164 |
-
MAX_SOURCE_EDGE: 16384,
|
| 165 |
-
MAX_OUTPUT_EDGE: 4096,
|
| 166 |
-
MAX_OUTPUT_BYTES: 5 * 1024 * 1024,
|
| 167 |
-
TIMEOUT: 15000,
|
| 168 |
-
MIN_QUALITY: 0.3,
|
| 169 |
-
QUALITY_STEP: 0.1,
|
| 170 |
-
AVATAR_MAX_BYTES: 2 * 1024 * 1024,
|
| 171 |
-
AVATAR_SIZE: 512
|
| 172 |
-
};
|
| 173 |
-
|
| 174 |
-
// ==========================================
|
| 175 |
-
// 🔐 管理员配置 (P0安全修复:管理员账号配置化)
|
| 176 |
-
// ==========================================
|
| 177 |
-
// 注意:前端仅用于UI显示控制,后端从环境变量 ADMIN_ACCOUNTS 读取
|
| 178 |
-
// 修改此处需同步修改云端环境变量
|
| 179 |
-
export const ADMIN_ACCOUNTS = new Set(["123456", "888888"]);
|
| 180 |
-
|
| 181 |
-
/**
|
| 182 |
-
* 检查账号是否为管理员
|
| 183 |
-
* @param {string} account - 用户账号
|
| 184 |
-
* @returns {boolean}
|
| 185 |
-
*/
|
| 186 |
-
export function isAdmin(account) {
|
| 187 |
-
return ADMIN_ACCOUNTS.has(account);
|
| 188 |
-
}
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
// ==========================================
|
| 192 |
-
// 💾 缓存配置
|
| 193 |
-
// ==========================================
|
| 194 |
-
const RESOURCE_LIST_PREFIXES = [
|
| 195 |
-
"ListCache_tools_", "ListCache_skills_", "ListCache_apps_", "ListCache_recommends_"
|
| 196 |
-
];
|
| 197 |
-
export const CACHE = {
|
| 198 |
-
// 缓存键前缀
|
| 199 |
-
PREFIX: "ComfyRanking_",
|
| 200 |
-
|
| 201 |
-
// 默认缓存时
|
| 202 |
-
DEFAULT_TTL: 2 * 60 * 60 * 1000, // 2小时
|
| 203 |
-
|
| 204 |
-
// 各类数据缓存时间
|
| 205 |
-
TTL: {
|
| 206 |
-
LIST_DATA: 5 * 60 * 1000, // 列表数据:5分钟
|
| 207 |
-
DETAIL_DATA: 3 * 60 * 1000, // 详情数据:3分钟
|
| 208 |
-
USER_PROFILE: 2 * 60 * 1000, // 用户资料:2分钟
|
| 209 |
-
WALLET: 30 * 1000, // 钱包数据:30秒
|
| 210 |
-
CREATORS: 5 * 60 * 1000, // 创作者列表:5分钟
|
| 211 |
-
CHAT_HISTORY: 60 * 1000 // 聊天记录:1分钟
|
| 212 |
-
},
|
| 213 |
-
|
| 214 |
-
// 内存缓存最大条目数
|
| 215 |
-
MAX_MEMORY_ITEMS: 100,
|
| 216 |
-
|
| 217 |
-
// 本插件 localStorage 持久缓存的估算容量上限(不影响用户/会话等其它键)
|
| 218 |
-
MAX_STORAGE_BYTES: 2 * 1024 * 1024,
|
| 219 |
-
|
| 220 |
-
// localStorage 键名(旧版兼容)
|
| 221 |
-
SESSION_REMEMBER_TTL: 30 * 24 * 60 * 60 * 1000,
|
| 222 |
-
INSTALLED_VERSION_UNCONFIRMED: "__installed_unconfirmed__", // 安装成功但没有实际版本证明
|
| 223 |
-
SESSION_TEMP_TTL: 24 * 60 * 60 * 1000,
|
| 224 |
-
LEGACY_KEYS: {
|
| 225 |
-
USER: "ComfyCommunity_User",
|
| 226 |
-
TOKEN: "ComfyCommunity_Token",
|
| 227 |
-
LIST_CACHE: "ComfyCommunity_ListCache",
|
| 228 |
-
PROFILE_CACHE: "ComfyCommunity_ProfileCache",
|
| 229 |
-
CHAT_HISTORY: "ComfyCommunity_ChatHistory"
|
| 230 |
-
},
|
| 231 |
-
|
| 232 |
-
// 本地背景存储键(不上传云端)
|
| 233 |
-
LOCAL_KEYS: {
|
| 234 |
-
BANNER_CONFIG: "ComfyRanking_BannerConfig", // 广告配置持久缓存
|
| 235 |
-
CREATORS_SESSION: "ComfyRanking_CreatorsCache", // 沿用创作者临时缓存键
|
| 236 |
-
SIDEBAR_BACKGROUND: "ComfyRanking_SidebarBackground", // 工具背景图 Base64
|
| 237 |
-
PROFILE_BANNER_CACHE: "ComfyRanking_ProfileBannerCache", // 个人资料背景图本地缓存
|
| 238 |
-
ACQUIRED_ITEMS: "ComfyRanking_AcquiredItems", // 已获取的资源记录
|
| 239 |
-
PROFILE_LIST_PREFIX: "ProfileList_", // 兼容旧个人列表业务键
|
| 240 |
-
INSTALLED_VERSION_PREFIX: "ComfyCommunity_LocalVer_", // 沿用既有安装版本键
|
| 241 |
-
ITEM_OWNERSHIP_PREFIX: "ItemOwnership_", // 缓存管理器业务键前缀
|
| 242 |
-
// 🚀 新增:私信和通知本地缓存
|
| 243 |
-
NOTIFICATIONS: "ComfyRanking_Notifications", // 通知消息缓存
|
| 244 |
-
CHAT_LIST: "ComfyRanking_ChatList", // 对话列表缓存
|
| 245 |
-
CHAT_HISTORY_PREFIX: "ComfyRanking_ChatHistory_", // 聊天记录前缀
|
| 246 |
-
NOTIF_CLEAR_TIME: "ComfyRanking_NotifClearTime", // 通知清空时间戳
|
| 247 |
-
CHAT_CLEAR_PREFIX: "ComfyRanking_ChatCleared_" // 聊天清空时间戳前缀
|
| 248 |
-
},
|
| 249 |
-
|
| 250 |
-
REFUND_CACHE_PREFIXES: [
|
| 251 |
-
"api_/api/items", "api_/api/creators", "api_/api/wallet",
|
| 252 |
-
...RESOURCE_LIST_PREFIXES
|
| 253 |
-
],
|
| 254 |
-
RESOURCE_LIST_PREFIXES,
|
| 255 |
-
|
| 256 |
-
// 🚀 新增:消息轮询配置
|
| 257 |
-
MESSAGE_POLL: {
|
| 258 |
-
INTERVAL: 30 * 1000, // 轮询间隔:30秒
|
| 259 |
-
ENABLED: true // 是否启用定时检查
|
| 260 |
-
}
|
| 261 |
-
};
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
// ==========================================
|
| 265 |
-
// 📜 分页配置
|
| 266 |
-
// ==========================================
|
| 267 |
-
export const PAGINATION = {
|
| 268 |
-
// 工具/应用每页数量
|
| 269 |
-
ITEMS_PAGE_SIZE: 20,
|
| 270 |
-
|
| 271 |
-
// 创作者每页数量
|
| 272 |
-
CREATORS_PAGE_SIZE: 12,
|
| 273 |
-
SIDEBAR_ITEM_FETCH_LIMIT: 200,
|
| 274 |
-
SIDEBAR_CREATOR_FETCH_LIMIT: 500,
|
| 275 |
-
SIDEBAR_CREATOR_SEARCH_LIMIT: 50,
|
| 276 |
-
PROFILE_USERS_PAGE_SIZE: 20,
|
| 277 |
-
PROFILE_RESOURCE_FETCH_LIMIT: 200,
|
| 278 |
-
|
| 279 |
-
// 评论每页数量
|
| 280 |
-
COMMENTS_PAGE_SIZE: 10,
|
| 281 |
-
|
| 282 |
-
// 消息每页数量
|
| 283 |
-
MESSAGES_PAGE_SIZE: 20,
|
| 284 |
-
CHAT_MESSAGES_PAGE_SIZE: 50,
|
| 285 |
-
|
| 286 |
-
// 滚动加载触发阈值(距底部像素)
|
| 287 |
-
SCROLL_THRESHOLD: 300,
|
| 288 |
-
|
| 289 |
-
// 防抖延迟(毫秒)
|
| 290 |
-
DEBOUNCE_DELAY: 100
|
| 291 |
-
};
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
// ==========================================
|
| 295 |
-
// 💰 打赏等级配置
|
| 296 |
-
// ==========================================
|
| 297 |
-
export const TIP_LEVELS = {
|
| 298 |
-
// 每颗星星需要的积分
|
| 299 |
-
POINTS_PER_STAR: 100,
|
| 300 |
-
|
| 301 |
-
// 每个月亮需要的星星数
|
| 302 |
-
STARS_PER_MOON: 5,
|
| 303 |
-
|
| 304 |
-
// 每个太阳需要的月亮数
|
| 305 |
-
MOONS_PER_SUN: 5,
|
| 306 |
-
|
| 307 |
-
// 最高等级太阳数
|
| 308 |
-
MAX_SUNS: 9,
|
| 309 |
-
|
| 310 |
-
// 最高积分(9太阳 = 22500积分)
|
| 311 |
-
MAX_POINTS: 22500,
|
| 312 |
-
|
| 313 |
-
// 等级图标
|
| 314 |
-
ICONS: {
|
| 315 |
-
STAR: "⭐",
|
| 316 |
-
MOON: "🌙",
|
| 317 |
-
SUN: "☀️",
|
| 318 |
-
CROWN: "👑"
|
| 319 |
-
},
|
| 320 |
-
|
| 321 |
-
// 等级称号
|
| 322 |
-
TITLES: {
|
| 323 |
-
MAX_LEVEL: "👑 至尊赞助者",
|
| 324 |
-
HIGH_SUN: "超级赞助者",
|
| 325 |
-
LOW_SUN: "黄金赞助者",
|
| 326 |
-
HIGH_MOON: "白银赞助者",
|
| 327 |
-
LOW_MOON: "青铜赞助者",
|
| 328 |
-
HIGH_STAR: "热心粉丝",
|
| 329 |
-
DEFAULT: "支持者"
|
| 330 |
-
}
|
| 331 |
-
};
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
// ==========================================
|
| 335 |
-
// 🎨 UI 配置
|
| 336 |
-
// ==========================================
|
| 337 |
-
export const UI = {
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
},
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
//
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
}
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
// ==========
|
| 405 |
-
//
|
| 406 |
-
// ===========
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
//
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
// 是否启用
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
// 是否启用
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
// 是否启用
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
//
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
//
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
//
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
//
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
//
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
//
|
| 514 |
-
//
|
| 515 |
-
|
| 516 |
-
/
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
/
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
}
|
| 553 |
-
|
| 554 |
-
/**
|
| 555 |
-
* 获取账号区分的
|
| 556 |
-
* @param {string} [account] - 账号,不传则自动获取当前登录账
|
| 557 |
-
* @returns {string}
|
| 558 |
-
*/
|
| 559 |
-
export function
|
| 560 |
-
const acc = account || getCurrentAccount();
|
| 561 |
-
return acc
|
| 562 |
-
? `${CACHE.LOCAL_KEYS.
|
| 563 |
-
: CACHE.LOCAL_KEYS.
|
| 564 |
-
}
|
| 565 |
-
|
| 566 |
-
/**
|
| 567 |
-
* 获取缓存键
|
| 568 |
-
* @param {string}
|
| 569 |
-
* @returns {string}
|
| 570 |
-
*/
|
| 571 |
-
export function
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
*
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
}
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
/
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
// ==========================================
|
| 608 |
-
//
|
| 609 |
-
//
|
| 610 |
-
//
|
| 611 |
-
//
|
| 612 |
-
//
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
/
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
|
| 650 |
-
|
| 651 |
-
if (
|
| 652 |
-
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
if (
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
if (host
|
| 675 |
-
|
| 676 |
-
const
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
if (
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
const
|
| 689 |
-
|
| 690 |
-
const [
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
}
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
|
| 751 |
-
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
|
| 755 |
-
}
|
| 756 |
-
}
|
| 757 |
-
|
| 758 |
-
/**
|
| 759 |
-
* 🚀
|
| 760 |
-
* @param {string} account - 用户账号
|
| 761 |
-
* @param {
|
| 762 |
-
*
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
|
| 766 |
-
|
| 767 |
-
|
| 768 |
-
|
| 769 |
-
|
| 770 |
-
|
| 771 |
-
|
| 772 |
-
|
| 773 |
-
|
| 774 |
-
|
| 775 |
-
|
| 776 |
-
|
| 777 |
-
|
| 778 |
-
|
| 779 |
-
|
| 780 |
-
|
| 781 |
-
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
|
| 785 |
-
|
| 786 |
-
|
| 787 |
-
|
| 788 |
-
|
| 789 |
-
|
| 790 |
-
|
| 791 |
-
|
| 792 |
-
|
| 793 |
-
|
| 794 |
-
|
| 795 |
-
|
| 796 |
-
|
| 797 |
-
|
| 798 |
-
|
| 799 |
-
|
| 800 |
-
|
| 801 |
-
|
| 802 |
-
|
| 803 |
-
|
| 804 |
-
|
| 805 |
-
|
| 806 |
-
|
| 807 |
-
|
| 808 |
-
|
| 809 |
-
|
| 810 |
-
|
| 811 |
-
|
| 812 |
-
|
| 813 |
-
|
| 814 |
-
|
| 815 |
-
|
| 816 |
-
|
| 817 |
-
|
| 818 |
-
|
| 819 |
-
|
| 820 |
-
|
| 821 |
-
|
| 822 |
-
|
| 823 |
-
|
| 824 |
-
|
| 825 |
-
|
| 826 |
-
|
| 827 |
-
|
| 828 |
-
|
| 829 |
-
|
| 830 |
-
|
| 831 |
-
|
| 832 |
-
|
| 833 |
-
|
| 834 |
-
|
| 835 |
-
|
| 836 |
-
|
| 837 |
-
|
| 838 |
-
|
| 839 |
-
|
| 840 |
-
|
| 841 |
-
|
| 842 |
-
|
| 843 |
-
|
| 844 |
-
|
| 845 |
-
|
| 846 |
-
|
| 847 |
-
|
| 848 |
-
|
| 849 |
-
|
| 850 |
-
|
| 851 |
-
|
| 852 |
-
|
| 853 |
-
|
| 854 |
-
|
| 855 |
-
|
| 856 |
-
|
| 857 |
-
|
| 858 |
-
|
| 859 |
-
|
| 860 |
-
|
| 861 |
-
|
| 862 |
-
|
| 863 |
-
|
| 864 |
-
|
| 865 |
-
}
|
| 866 |
-
|
| 867 |
-
|
| 868 |
-
//
|
| 869 |
-
//
|
| 870 |
-
//
|
| 871 |
-
|
| 872 |
-
|
| 873 |
-
|
| 874 |
-
|
| 875 |
-
|
| 876 |
-
|
| 877 |
-
|
| 878 |
-
|
| 879 |
-
|
| 880 |
-
|
| 881 |
-
|
| 882 |
-
|
| 883 |
-
|
| 884 |
-
|
| 885 |
-
|
| 886 |
-
|
| 887 |
-
|
| 888 |
-
|
| 889 |
-
|
| 890 |
-
|
| 891 |
-
|
| 892 |
-
|
| 893 |
-
|
| 894 |
-
|
| 895 |
-
|
| 896 |
-
|
| 897 |
-
|
| 898 |
-
|
| 899 |
-
|
| 900 |
-
|
| 901 |
-
|
| 902 |
-
|
| 903 |
-
|
| 904 |
-
|
| 905 |
-
|
| 906 |
-
|
| 907 |
-
|
| 908 |
-
|
| 909 |
-
|
| 910 |
-
|
| 911 |
-
|
| 912 |
-
|
| 913 |
-
|
| 914 |
-
|
| 915 |
-
|
| 916 |
-
|
| 917 |
-
recommendForm:
|
| 918 |
-
privateRepoWhenLink:
|
| 919 |
-
|
| 920 |
-
|
| 921 |
-
}
|
| 922 |
-
|
| 923 |
-
|
| 924 |
-
|
| 925 |
-
|
| 926 |
-
|
| 927 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 928 |
enableKeyboardShortcuts: false,
|
| 929 |
-
enableLocalPerformanceStats: false,
|
| 930 |
-
showCreatorBanner: true, // 创作者卡片显示背景图
|
| 931 |
-
enableAnimations: true, // ✨ 榜单切换动画
|
| 932 |
-
enableSoundEffects: true, // 🔊 动画音效
|
| 933 |
-
language: 'zh-CN', // 🌐 界面语言
|
| 934 |
-
cacheExpireSeconds: 7200, // ⏱️ 缓存刷新间隔(秒),默认2小时
|
| 935 |
-
// 🚀 后续可扩展更多设置项
|
| 936 |
-
// compactMode: false, // 紧凑模式
|
| 937 |
-
// darkMode: true, // 深色模式
|
| 938 |
-
};
|
| 939 |
-
|
| 940 |
-
/**
|
| 941 |
-
* 📦 获取当前设置
|
| 942 |
-
*/
|
| 943 |
-
export function getSettings(options = {}) {
|
| 944 |
-
try {
|
| 945 |
-
const saved = localStorage.getItem(SETTINGS_KEY);
|
| 946 |
-
if (saved) {
|
| 947 |
-
const value = JSON.parse(saved);
|
| 948 |
-
if (options.strict && (!value || typeof value !== 'object' || Array.isArray(value))) throw new Error('Invalid settings');
|
| 949 |
-
return { ...DEFAULT_SETTINGS, ...value };
|
| 950 |
-
}
|
| 951 |
-
} catch (e) { if (options.strict) throw e; }
|
| 952 |
-
return { ...DEFAULT_SETTINGS };
|
| 953 |
-
}
|
| 954 |
-
|
| 955 |
-
/**
|
| 956 |
-
* 💾 保存设置
|
| 957 |
-
*/
|
| 958 |
-
export function areKeyboardShortcutsEnabled() {
|
| 959 |
-
return getSettings().enableKeyboardShortcuts === true;
|
| 960 |
-
}
|
| 961 |
-
|
| 962 |
-
export function saveSettings(settings) {
|
| 963 |
-
try {
|
| 964 |
-
localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
|
| 965 |
-
// 🔔 广播设置变更事件,让其他组件响应
|
| 966 |
-
window.dispatchEvent(new CustomEvent("comfy-settings-changed", { detail: settings }));
|
| 967 |
-
return true;
|
| 968 |
-
} catch (e) { return false; }
|
| 969 |
-
}
|
|
|
|
| 1 |
+
// 前端页面/core/全局配置.js
|
| 2 |
+
// ==========================================
|
| 3 |
+
// ⚙️ 全局配置中心
|
| 4 |
+
// ==========================================
|
| 5 |
+
// 作用:集中管理所有配置常量,便于维护和修改
|
| 6 |
+
// 关联文件:
|
| 7 |
+
// - 网络请求API.js (API基础地址)
|
| 8 |
+
// - 性能优化工具.js (缓存配置)
|
| 9 |
+
// - 侧边栏数据引擎.js (分页配置)
|
| 10 |
+
// - 所有业务组件
|
| 11 |
+
// ==========================================
|
| 12 |
+
// 🏗️ P2架构优化:配置集中化
|
| 13 |
+
// 🏗️ P2质量优化:JSDoc 类型注释
|
| 14 |
+
// ==========================================
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
// ==========================================
|
| 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, SSE_MAX_RESPONSE_BYTES: number, SSE_MAX_LINE_BYTES: number, SSE_MAX_LINES: number, SSE_YIELD_LINES: number, SSE_YIELD_BYTES: number, SSE_YIELD_CHUNKS: 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, PROFILE_MEDIA_URL_MAX_BYTES: number, 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, REQUEST_BODY_ERRORS: Record<string, { status: number, msgKey: 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 |
+
REQUEST_BODY_ERRORS: {
|
| 28 |
+
request_body_invalid: { status: 400, msgKey: 'feedback.invalid_request_data' },
|
| 29 |
+
request_body_disconnected: { status: 400, msgKey: 'feedback.network_unavailable' },
|
| 30 |
+
request_body_too_large: { status: 413, msgKey: 'feedback.request_too_large' },
|
| 31 |
+
request_body_timeout: { status: 408, msgKey: 'feedback.request_receive_timeout' },
|
| 32 |
+
request_body_busy: { status: 429, msgKey: 'feedback.request_busy' },
|
| 33 |
+
request_body_encoding: { status: 415, msgKey: 'feedback.request_encoding_unsupported' },
|
| 34 |
+
},
|
| 35 |
+
// 仅公开榜单使用由正文派生的条件验证器,私有响应仍禁止留存。
|
| 36 |
+
PUBLIC_CONDITIONAL_GET_PATHS: ['/api/items', '/api/creators', '/api/creators/search', '/api/posts', '/api/prompts', '/api/tasks'],
|
| 37 |
+
CONDITIONAL_ETAG_PATTERN: /^W\/"rk-[a-f0-9]{64}"$/,
|
| 38 |
+
|
| 39 |
+
// 云端 API 基础地址
|
| 40 |
+
BASE_URL: "https://zhiwei666-comfyui-ranking-api.hf.space",
|
| 41 |
+
|
| 42 |
+
// 请求超时时间(毫秒)
|
| 43 |
+
TIMEOUT: 30000,
|
| 44 |
+
VIDEO_UPLOAD_TIMEOUT: 300000,
|
| 45 |
+
VIDEO_UPLOAD_RESPONSE_MAX_CHARS: 65536,
|
| 46 |
+
LOCAL_CACHE_STATS_TIMEOUT: 10000,
|
| 47 |
+
LOCAL_CACHE_CLEAR_TIMEOUT: 60000,
|
| 48 |
+
LOCAL_CACHE_RESPONSE_MAX_BYTES: 8192,
|
| 49 |
+
// 工作流终态编码上限为512 MiB,另保留64 MiB进度数据;行预算包含data前缀。
|
| 50 |
+
SSE_MAX_RESPONSE_BYTES: 576 * 1024 * 1024,
|
| 51 |
+
SSE_MAX_LINE_BYTES: 512 * 1024 * 1024,
|
| 52 |
+
SSE_MAX_LINES: 262144,
|
| 53 |
+
SSE_YIELD_LINES: 256,
|
| 54 |
+
SSE_YIELD_BYTES: 1024 * 1024,
|
| 55 |
+
SSE_YIELD_CHUNKS: 256,
|
| 56 |
+
|
| 57 |
+
// GET 请求重试次数(实际尝试 MAX_RETRIES + 1 次)
|
| 58 |
+
// 🔧 修复:此前本值为 3 且全库无人读取,真正的重试次数硬编码在 网络请求_基础设施.js 中为 2,
|
| 59 |
+
// 现改为由基础设施读取本配置;取值 2 以保持既有的「共 3 次尝试」行为不变
|
| 60 |
+
MAX_RETRIES: 2,
|
| 61 |
+
|
| 62 |
+
// 重试间隔(毫秒,指数退避基数:1s、2s、4s…)
|
| 63 |
+
RETRY_DELAY: 1000,
|
| 64 |
+
|
| 65 |
+
// 🐢 弱网预算分级(仅作用于 GET,由 网络请求_基础设施.js 按 endpoint 路径精确匹配)
|
| 66 |
+
// 默认预算下弱网一次列表请求最长要烧 3×30s + 1s + 2s = 93s,期间用户只能盯骨架屏。
|
| 67 |
+
// 列表类:首屏已可由本地缓存先行上屏(见 侧边栏数据引擎.js),且列表往往是会话的第一个请求、
|
| 68 |
+
// 可能撞上 HF Space 冷启动(本项目无预热机制),故只砍掉一次冗余重试、保留 30s 单次超时,
|
| 69 |
+
// 93s → 61s,冷启动仍有机会在前两次尝试内完成
|
| 70 |
+
LIST_RETRIES: 1,
|
| 71 |
+
// 搜索类:必然发生在列表已加载之后(空间已被唤醒),可放心收紧,93s → 10s×2 + 1s = 21s
|
| 72 |
+
SEARCH_TIMEOUT: 10000,
|
| 73 |
+
SEARCH_RETRIES: 1,
|
| 74 |
+
PROFILE_RESOURCE_TIMEOUT: 5000,
|
| 75 |
+
CREATOR_CHART_SCRIPT_URL: "https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js",
|
| 76 |
+
|
| 77 |
+
// 🐢 后台补新预算(仅作用于 GET,由 网络请求_基础设施.js 按「本地是否已有可兜底数据」判定)
|
| 78 |
+
// 七大列表视图都是「本地数据先上屏 → 再发请求补新」,此时界面上已经有内容,
|
| 79 |
+
// 这次请求纯属后台行为;它每多挂 1 秒就多占 1 个并发额度(全局仅 6 个),
|
| 80 |
+
// 云端不通时会把前台请求(详情页、点赞、轮询)全部挤到排队,表现为「切哪个界面都要等」。
|
| 81 |
+
// 故一旦本地已有该请求的缓存(含过期),单次超时压到 10s 且不再重试,最坏耗时 10s
|
| 82 |
+
BACKGROUND_TIMEOUT: 10000,
|
| 83 |
+
BANNER_IMAGE_TIMEOUT: 75000,
|
| 84 |
+
WORKFLOW_DOWNLOAD_TIMEOUT: 120000, // 工作流流式下载预算(2 分钟)
|
| 85 |
+
BACKGROUND_RETRIES: 0,
|
| 86 |
+
|
| 87 |
+
// ☁️ 云端不可达冷却(消费方见 状态管理.js 的「云端可达性」段落)
|
| 88 |
+
// navigator.onLine 只反映本机网卡,云端单独不可达(HF Space 宕机/冷启动、DNS 污染、被墙)
|
| 89 |
+
// 时它仍为 true,于是每个界面、每次轮询都要从头烧一遍完整重试预算,且互不共享失败结论。
|
| 90 |
+
// 首次确证云端不可达后进入冷却:冷却期内所有 GET 一律不联网(有缓存直接返回、无缓存立即失败),
|
| 91 |
+
// 并发额度全部让给前台。被动探测——冷却一过,下一个真实请求充当探针;
|
| 92 |
+
// 期间任意一次成功响应(含 POST/PUT/DELETE)立即清零,不需等到冷却结束
|
| 93 |
+
CLOUD_DOWN_COOLDOWN: 60 * 1000,
|
| 94 |
+
|
| 95 |
+
// 🔍 创作者搜索请求的取消分组 ID
|
| 96 |
+
// 用户在搜索框继续输入时,侧边栏数据引擎.js 据此撤销上一次仍在途的搜索请求。
|
| 97 |
+
// 不撤销的话,作废请求会持续占用并发额度(最多 6 个)与超时预算,
|
| 98 |
+
// 把最新那次搜索挤到后面排队。仅搜索请求归入本分组,不影响其它任何请求
|
| 99 |
+
SEARCH_COMPONENT_ID: "sidebar-search",
|
| 100 |
+
// 上传相对路径的云端图片读取入口。
|
| 101 |
+
IMAGE_PROXY_PATH: "/api/image_proxy",
|
| 102 |
+
PROFILE_MEDIA_URL_MAX_BYTES: 16384,
|
| 103 |
+
// 本地广告预载要求真实图片,熔断占位图不能作为下载成功。
|
| 104 |
+
LOCAL_IMAGE_PROXY_PREFIX: "/community_hub/image?url=",
|
| 105 |
+
COMPLETE_IMAGE_QUERY_SUFFIX: "&require_complete=1",
|
| 106 |
+
// 身份相关或私有 GET 禁止通用响应缓存;公开列表仍使用原缓存策略。
|
| 107 |
+
PRIVATE_GET_PATTERNS: [
|
| 108 |
+
/^\/api\/(wallet|messages|chats|admin|disputes|users)(?:\/|$)/,
|
| 109 |
+
/^\/api\/my-(posts|prompts|tasks)(?:\/|$)/,
|
| 110 |
+
/^\/api\/(tasks|prompts)\/[^/]+$/
|
| 111 |
+
]
|
| 112 |
+
};
|
| 113 |
+
|
| 114 |
+
|
| 115 |
export const LOCAL_PERFORMANCE = Object.freeze({
|
| 116 |
MAX_ENDPOINT_CHARS: 4096,
|
| 117 |
MAX_COUNT: 1000000,
|
|
|
|
| 121 |
LABELS: Object.freeze({ tools: 'nav.tools', skills: 'nav.skills', apps: 'nav.apps', recommends: 'nav.recommends', creators: 'nav.creators', tasks: 'nav.tasks', posts: 'nav.posts', prompts: 'nav.prompts' })
|
| 122 |
});
|
| 123 |
|
| 124 |
+
export const SKILL_INSTALL = Object.freeze({
|
| 125 |
+
PATH: '/community_hub/skill_install',
|
| 126 |
+
PREFLIGHT_TIMEOUT: 30000,
|
| 127 |
+
CONTROL_TIMEOUT: 45000,
|
| 128 |
+
RESULT_TIMEOUT: 5000,
|
| 129 |
+
STREAM_TIMEOUT: 1200000,
|
| 130 |
+
RESPONSE_MAX_BYTES: 1024 * 1024,
|
| 131 |
+
PATH_MAX_BYTES: 32768,
|
| 132 |
+
MAX_DIRECTORIES: 128,
|
| 133 |
+
MAX_ITEMS: 1,
|
| 134 |
+
MODAL_WIDTH: '640px'
|
| 135 |
+
});
|
| 136 |
+
|
| 137 |
+
export const CHART_LIFECYCLE = Object.freeze({
|
| 138 |
+
WIDTH_WAIT_TIMEOUT: 5000,
|
| 139 |
+
RESIZE_DELAY: 300
|
| 140 |
+
});
|
| 141 |
+
|
| 142 |
+
// 发布视频的本地读取预算,不改变云端上传额度。
|
| 143 |
+
export const LAZY_IMAGES = Object.freeze({
|
| 144 |
+
ROOT_MARGIN: "200px 0px",
|
| 145 |
+
THRESHOLD: 0.01,
|
| 146 |
+
MOUNT_TIMEOUT: 30000,
|
| 147 |
+
READ_TIMEOUT: 30000
|
| 148 |
+
});
|
| 149 |
+
|
| 150 |
+
export const PUBLISH_MEDIA_LIMITS = Object.freeze({
|
| 151 |
+
MAX_IMAGES: 9,
|
| 152 |
+
IMAGE_MAX_EDGE: 1920,
|
| 153 |
+
IMAGE_QUALITY: 0.85,
|
| 154 |
+
VIDEO_MAX_BYTES: 100 * 1024 * 1024,
|
| 155 |
+
VIDEO_MAX_SECONDS: 180,
|
| 156 |
+
VIDEO_METADATA_TIMEOUT: 15000
|
| 157 |
+
});
|
| 158 |
+
|
| 159 |
+
// 上传前处理预算;浏览器解码本身仍由宿主管理。
|
| 160 |
+
export const IMAGE_PROCESS = {
|
| 161 |
+
HEADER_BYTES: 256 * 1024,
|
| 162 |
+
MAX_INPUT_BYTES: 50 * 1024 * 1024,
|
| 163 |
+
MAX_PIXELS: 40 * 1024 * 1024,
|
| 164 |
+
MAX_SOURCE_EDGE: 16384,
|
| 165 |
+
MAX_OUTPUT_EDGE: 4096,
|
| 166 |
+
MAX_OUTPUT_BYTES: 5 * 1024 * 1024,
|
| 167 |
+
TIMEOUT: 15000,
|
| 168 |
+
MIN_QUALITY: 0.3,
|
| 169 |
+
QUALITY_STEP: 0.1,
|
| 170 |
+
AVATAR_MAX_BYTES: 2 * 1024 * 1024,
|
| 171 |
+
AVATAR_SIZE: 512
|
| 172 |
+
};
|
| 173 |
+
|
| 174 |
+
// ==========================================
|
| 175 |
+
// 🔐 管理员配置 (P0安全修复:管理员账号配置化)
|
| 176 |
+
// ==========================================
|
| 177 |
+
// 注意:前端仅用于UI显示控制,后端从环境变量 ADMIN_ACCOUNTS 读取
|
| 178 |
+
// 修改此处需同步修改云端环境变量
|
| 179 |
+
export const ADMIN_ACCOUNTS = new Set(["123456", "888888"]);
|
| 180 |
+
|
| 181 |
+
/**
|
| 182 |
+
* 检查账号是否为管理员
|
| 183 |
+
* @param {string} account - 用户账号
|
| 184 |
+
* @returns {boolean}
|
| 185 |
+
*/
|
| 186 |
+
export function isAdmin(account) {
|
| 187 |
+
return ADMIN_ACCOUNTS.has(account);
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
// ==========================================
|
| 192 |
+
// 💾 缓存配置
|
| 193 |
+
// ==========================================
|
| 194 |
+
const RESOURCE_LIST_PREFIXES = [
|
| 195 |
+
"ListCache_tools_", "ListCache_skills_", "ListCache_apps_", "ListCache_recommends_"
|
| 196 |
+
];
|
| 197 |
+
export const CACHE = {
|
| 198 |
+
// 缓存键前缀
|
| 199 |
+
PREFIX: "ComfyRanking_",
|
| 200 |
+
|
| 201 |
+
// 默认缓存时���(毫秒)
|
| 202 |
+
DEFAULT_TTL: 2 * 60 * 60 * 1000, // 2小时
|
| 203 |
+
|
| 204 |
+
// 各类数据缓存时间
|
| 205 |
+
TTL: {
|
| 206 |
+
LIST_DATA: 5 * 60 * 1000, // 列表数据:5分钟
|
| 207 |
+
DETAIL_DATA: 3 * 60 * 1000, // 详情数据:3分钟
|
| 208 |
+
USER_PROFILE: 2 * 60 * 1000, // 用户资料:2分钟
|
| 209 |
+
WALLET: 30 * 1000, // 钱包数据:30秒
|
| 210 |
+
CREATORS: 5 * 60 * 1000, // 创作者列表:5分钟
|
| 211 |
+
CHAT_HISTORY: 60 * 1000 // 聊天记录:1分钟
|
| 212 |
+
},
|
| 213 |
+
|
| 214 |
+
// 内存缓存最大条目数
|
| 215 |
+
MAX_MEMORY_ITEMS: 100,
|
| 216 |
+
|
| 217 |
+
// 本插件 localStorage 持久缓存的估算容量上限(不影响用户/会话等其它键)
|
| 218 |
+
MAX_STORAGE_BYTES: 2 * 1024 * 1024,
|
| 219 |
+
|
| 220 |
+
// localStorage 键名(旧版兼容)
|
| 221 |
+
SESSION_REMEMBER_TTL: 30 * 24 * 60 * 60 * 1000,
|
| 222 |
+
INSTALLED_VERSION_UNCONFIRMED: "__installed_unconfirmed__", // 安装成功但没有实际版本证明
|
| 223 |
+
SESSION_TEMP_TTL: 24 * 60 * 60 * 1000,
|
| 224 |
+
LEGACY_KEYS: {
|
| 225 |
+
USER: "ComfyCommunity_User",
|
| 226 |
+
TOKEN: "ComfyCommunity_Token",
|
| 227 |
+
LIST_CACHE: "ComfyCommunity_ListCache",
|
| 228 |
+
PROFILE_CACHE: "ComfyCommunity_ProfileCache",
|
| 229 |
+
CHAT_HISTORY: "ComfyCommunity_ChatHistory"
|
| 230 |
+
},
|
| 231 |
+
|
| 232 |
+
// 本地背景存储键(不上传云端)
|
| 233 |
+
LOCAL_KEYS: {
|
| 234 |
+
BANNER_CONFIG: "ComfyRanking_BannerConfig", // 广告配置持久缓存
|
| 235 |
+
CREATORS_SESSION: "ComfyRanking_CreatorsCache", // 沿用创作者临时缓存键
|
| 236 |
+
SIDEBAR_BACKGROUND: "ComfyRanking_SidebarBackground", // 工具背景图 Base64
|
| 237 |
+
PROFILE_BANNER_CACHE: "ComfyRanking_ProfileBannerCache", // 个人资料背景图本地缓存
|
| 238 |
+
ACQUIRED_ITEMS: "ComfyRanking_AcquiredItems", // 已获取的资源记录
|
| 239 |
+
PROFILE_LIST_PREFIX: "ProfileList_", // 兼容旧个人列表业务键
|
| 240 |
+
INSTALLED_VERSION_PREFIX: "ComfyCommunity_LocalVer_", // 沿用既有安装版本键
|
| 241 |
+
ITEM_OWNERSHIP_PREFIX: "ItemOwnership_", // 缓存管理器业务键前缀
|
| 242 |
+
// 🚀 新增:私信和通知本地缓存
|
| 243 |
+
NOTIFICATIONS: "ComfyRanking_Notifications", // 通知消息缓存
|
| 244 |
+
CHAT_LIST: "ComfyRanking_ChatList", // 对话列表缓存
|
| 245 |
+
CHAT_HISTORY_PREFIX: "ComfyRanking_ChatHistory_", // 聊天记录前缀
|
| 246 |
+
NOTIF_CLEAR_TIME: "ComfyRanking_NotifClearTime", // 通知清空时间戳
|
| 247 |
+
CHAT_CLEAR_PREFIX: "ComfyRanking_ChatCleared_" // 聊天清空时间戳前缀
|
| 248 |
+
},
|
| 249 |
+
|
| 250 |
+
REFUND_CACHE_PREFIXES: [
|
| 251 |
+
"api_/api/items", "api_/api/creators", "api_/api/wallet",
|
| 252 |
+
...RESOURCE_LIST_PREFIXES
|
| 253 |
+
],
|
| 254 |
+
RESOURCE_LIST_PREFIXES,
|
| 255 |
+
|
| 256 |
+
// 🚀 新增:消息轮询配置
|
| 257 |
+
MESSAGE_POLL: {
|
| 258 |
+
INTERVAL: 30 * 1000, // 轮询间隔:30秒
|
| 259 |
+
ENABLED: true // 是否启用定时检查
|
| 260 |
+
}
|
| 261 |
+
};
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
// ==========================================
|
| 265 |
+
// 📜 分页配置
|
| 266 |
+
// ==========================================
|
| 267 |
+
export const PAGINATION = {
|
| 268 |
+
// 工具/应用每页数量
|
| 269 |
+
ITEMS_PAGE_SIZE: 20,
|
| 270 |
+
|
| 271 |
+
// 创作者每页数量
|
| 272 |
+
CREATORS_PAGE_SIZE: 12,
|
| 273 |
+
SIDEBAR_ITEM_FETCH_LIMIT: 200,
|
| 274 |
+
SIDEBAR_CREATOR_FETCH_LIMIT: 500,
|
| 275 |
+
SIDEBAR_CREATOR_SEARCH_LIMIT: 50,
|
| 276 |
+
PROFILE_USERS_PAGE_SIZE: 20,
|
| 277 |
+
PROFILE_RESOURCE_FETCH_LIMIT: 200,
|
| 278 |
+
|
| 279 |
+
// 评论每页数量
|
| 280 |
+
COMMENTS_PAGE_SIZE: 10,
|
| 281 |
+
|
| 282 |
+
// 消息每页数量
|
| 283 |
+
MESSAGES_PAGE_SIZE: 20,
|
| 284 |
+
CHAT_MESSAGES_PAGE_SIZE: 50,
|
| 285 |
+
|
| 286 |
+
// 滚动加载触发阈值(距底部像素)
|
| 287 |
+
SCROLL_THRESHOLD: 300,
|
| 288 |
+
|
| 289 |
+
// 防抖延迟(毫秒)
|
| 290 |
+
DEBOUNCE_DELAY: 100
|
| 291 |
+
};
|
| 292 |
+
|
| 293 |
+
|
| 294 |
+
// ==========================================
|
| 295 |
+
// 💰 打赏等级配置
|
| 296 |
+
// ==========================================
|
| 297 |
+
export const TIP_LEVELS = {
|
| 298 |
+
// 每颗星星需要的积分
|
| 299 |
+
POINTS_PER_STAR: 100,
|
| 300 |
+
|
| 301 |
+
// 每个月亮需要的星星数
|
| 302 |
+
STARS_PER_MOON: 5,
|
| 303 |
+
|
| 304 |
+
// 每个太阳需要的月亮数
|
| 305 |
+
MOONS_PER_SUN: 5,
|
| 306 |
+
|
| 307 |
+
// 最高等级太阳数
|
| 308 |
+
MAX_SUNS: 9,
|
| 309 |
+
|
| 310 |
+
// 最高积分(9太阳 = 22500积分)
|
| 311 |
+
MAX_POINTS: 22500,
|
| 312 |
+
|
| 313 |
+
// 等级图标
|
| 314 |
+
ICONS: {
|
| 315 |
+
STAR: "⭐",
|
| 316 |
+
MOON: "🌙",
|
| 317 |
+
SUN: "☀️",
|
| 318 |
+
CROWN: "👑"
|
| 319 |
+
},
|
| 320 |
+
|
| 321 |
+
// 等级称号
|
| 322 |
+
TITLES: {
|
| 323 |
+
MAX_LEVEL: "👑 至尊赞助者",
|
| 324 |
+
HIGH_SUN: "超级赞助者",
|
| 325 |
+
LOW_SUN: "黄金赞助者",
|
| 326 |
+
HIGH_MOON: "白银赞助者",
|
| 327 |
+
LOW_MOON: "青铜赞助者",
|
| 328 |
+
HIGH_STAR: "热心粉丝",
|
| 329 |
+
DEFAULT: "支持者"
|
| 330 |
+
}
|
| 331 |
+
};
|
| 332 |
+
|
| 333 |
+
|
| 334 |
+
// ==========================================
|
| 335 |
+
// 🎨 UI 配置
|
| 336 |
+
// ==========================================
|
| 337 |
+
export const UI = {
|
| 338 |
+
INSTALL_SOURCE: Object.freeze({
|
| 339 |
+
MAX_URL_BYTES: 4096,
|
| 340 |
+
CASE_INSENSITIVE_HOSTS: Object.freeze(['github.com'])
|
| 341 |
+
}),
|
| 342 |
+
INSTALL_TRANSFER: Object.freeze({
|
| 343 |
+
MAX_BYTES: 2 * 1024 * 1024 * 1024,
|
| 344 |
+
MAX_RATE: Number.MAX_SAFE_INTEGER,
|
| 345 |
+
BASE: 1024,
|
| 346 |
+
STAGES: Object.freeze(['downloading', 'download_done']),
|
| 347 |
+
UNIT_KEYS: Object.freeze(['install.transfer_b', 'install.transfer_kib', 'install.transfer_mib', 'install.transfer_gib'])
|
| 348 |
+
}),
|
| 349 |
+
|
| 350 |
+
CHAT_MAX_MSG_LENGTH: 1000,
|
| 351 |
+
CHAT_TIME_GAP_SECONDS: 5 * 60,
|
| 352 |
+
MAX_DISPUTE_EVIDENCE: 6,
|
| 353 |
+
RECHARGE_POLL_INTERVAL: 3000,
|
| 354 |
+
RECHARGE_POLL_LIMIT: 100,
|
| 355 |
+
RECHARGE_QR_MAX_BYTES: 2048,
|
| 356 |
+
PROFILE_BANNER_CACHE_BYTES: 512 * 1024,
|
| 357 |
+
// 与云端公开版本接口的缺省值一致;用于网络不可用时的本地展示。
|
| 358 |
+
DEFAULT_PROJECT_VERSION: Object.freeze({ stage: "beta", major: 2, minor: 0, patch: 0 }),
|
| 359 |
+
ABOUT: {
|
| 360 |
+
AUTHOR_URL: "https://space.bilibili.com/2114638644",
|
| 361 |
+
AUTHOR_BADGE_URL: "https://img.shields.io/badge/bilibili-猪的飞行梦-00A1D6?logo=bilibili&logoColor=white"
|
| 362 |
+
},
|
| 363 |
+
// Toast 消息配置
|
| 364 |
+
TOAST: {
|
| 365 |
+
OFFSET: 50,
|
| 366 |
+
GAP: 10,
|
| 367 |
+
DURATION: 3000, // 显示时长(毫秒)
|
| 368 |
+
MAX_VISIBLE: 3, // 最大同时显示数量
|
| 369 |
+
POSITION: "top-center" // 位置
|
| 370 |
+
},
|
| 371 |
+
|
| 372 |
+
// 动画配置
|
| 373 |
+
ANIMATION: {
|
| 374 |
+
DURATION_FAST: 150,
|
| 375 |
+
DURATION_NORMAL: 300,
|
| 376 |
+
DURATION_SLOW: 500,
|
| 377 |
+
EASING: "cubic-bezier(0.25, 0.8, 0.25, 1)"
|
| 378 |
+
},
|
| 379 |
+
|
| 380 |
+
// 颜色主题
|
| 381 |
+
COLORS: {
|
| 382 |
+
PRIMARY: "#2196F3",
|
| 383 |
+
SUCCESS: "#4CAF50",
|
| 384 |
+
WARNING: "#FF9800",
|
| 385 |
+
ERROR: "#F44336",
|
| 386 |
+
INFO: "#2196F3",
|
| 387 |
+
TIP: "#E91E63",
|
| 388 |
+
|
| 389 |
+
// 深色主题背景
|
| 390 |
+
BG_DARK: "var(--comfy-menu-bg)",
|
| 391 |
+
BG_CARD: "var(--comfy-input-bg)",
|
| 392 |
+
BG_INPUT: "var(--comfy-input-bg)",
|
| 393 |
+
|
| 394 |
+
// 边框
|
| 395 |
+
BORDER: "#444",
|
| 396 |
+
BORDER_LIGHT: "#555"
|
| 397 |
+
},
|
| 398 |
+
|
| 399 |
+
// 图片占位图(🚀 关键修复:全部改为内联 SVG,避免外部网络请求)
|
| 400 |
+
PLACEHOLDERS: {
|
| 401 |
+
// 头像占位图(👤 图标)
|
| 402 |
+
AVATAR: `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='150' height='150'%3E%3Crect fill='%23333' width='150' height='150' rx='75'/%3E%3Ctext x='75' y='100' text-anchor='middle' fill='%23888' font-size='60'%3E👤%3C/text%3E%3C/svg%3E`,
|
| 403 |
+
// 封面占位图(🖼️ 图标)
|
| 404 |
+
COVER: `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='400' height='260'%3E%3Crect fill='%23222' width='400' height='260'/%3E%3Ctext x='200' y='140' text-anchor='middle' fill='%23666' font-size='48'%3E🖼️%3C/text%3E%3C/svg%3E`,
|
| 405 |
+
// 小头像占位图(用于列表)
|
| 406 |
+
AVATAR_SMALL: `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Crect fill='%23333' width='40' height='40' rx='20'/%3E%3Ctext x='20' y='26' text-anchor='middle' fill='%23888' font-size='16'%3E👤%3C/text%3E%3C/svg%3E`,
|
| 407 |
+
// 加载占位图
|
| 408 |
+
LOADING: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100'%3E%3Crect fill='%231a1a1a' width='100' height='100'/%3E%3C/svg%3E"
|
| 409 |
+
}
|
| 410 |
+
};
|
| 411 |
+
|
| 412 |
+
// 🚀 导出 PLACEHOLDERS 便于直接使用
|
| 413 |
+
export const PLACEHOLDERS = UI.PLACEHOLDERS;
|
| 414 |
+
|
| 415 |
+
|
| 416 |
+
// ==========================================
|
| 417 |
+
// 📝 表单验证配置
|
| 418 |
+
// ==========================================
|
| 419 |
+
export const VALIDATION = {
|
| 420 |
+
// 账号规则
|
| 421 |
+
ACCOUNT: {
|
| 422 |
+
MIN_LENGTH: 6,
|
| 423 |
+
MAX_LENGTH: 20,
|
| 424 |
+
PATTERN: /^[a-zA-Z0-9_]{6,20}$/,
|
| 425 |
+
MESSAGE: "账号仅支持6-20位字母、数字、下划线"
|
| 426 |
+
},
|
| 427 |
+
|
| 428 |
+
// 密码规则
|
| 429 |
+
PASSWORD: {
|
| 430 |
+
MIN_LENGTH: 6,
|
| 431 |
+
PATTERN: /^[a-zA-Z0-9!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]{6,}$/,
|
| 432 |
+
MESSAGE: "密码至少6位,支持字母、数字和常见特殊字符"
|
| 433 |
+
},
|
| 434 |
+
|
| 435 |
+
// 昵称规则
|
| 436 |
+
NAME: {
|
| 437 |
+
MIN_LENGTH: 1,
|
| 438 |
+
MAX_LENGTH: 20,
|
| 439 |
+
MESSAGE: "昵称长度1-20个字符"
|
| 440 |
+
},
|
| 441 |
+
|
| 442 |
+
// 个人介绍规则
|
| 443 |
+
INTRO: {
|
| 444 |
+
MAX_LENGTH: 100,
|
| 445 |
+
MESSAGE: "个人介绍不能超过100字"
|
| 446 |
+
},
|
| 447 |
+
|
| 448 |
+
// 验证码规则
|
| 449 |
+
VERIFY_CODE: {
|
| 450 |
+
LENGTH: 6,
|
| 451 |
+
PATTERN: /^\d{6}$/,
|
| 452 |
+
EXPIRE_SECONDS: 600,
|
| 453 |
+
MESSAGE: "请输入6位数字验证码"
|
| 454 |
+
}
|
| 455 |
+
};
|
| 456 |
+
|
| 457 |
+
|
| 458 |
+
// ==========================================
|
| 459 |
+
// 🔧 功能开关
|
| 460 |
+
// ==========================================
|
| 461 |
+
export const FEATURES = {
|
| 462 |
+
// 是否启用调试模式
|
| 463 |
+
DEBUG: false,
|
| 464 |
+
|
| 465 |
+
// 是否启用性能监控
|
| 466 |
+
PERFORMANCE_MONITOR: false,
|
| 467 |
+
|
| 468 |
+
// 是否启用离线模式提示
|
| 469 |
+
OFFLINE_DETECTION: true,
|
| 470 |
+
|
| 471 |
+
// 是否启用图片懒加载
|
| 472 |
+
LAZY_LOAD_IMAGES: true,
|
| 473 |
+
|
| 474 |
+
// 是否启用分页加载
|
| 475 |
+
PAGINATION_ENABLED: true,
|
| 476 |
+
|
| 477 |
+
// 是否启用请求缓存
|
| 478 |
+
REQUEST_CACHE: true,
|
| 479 |
+
|
| 480 |
+
// 是否启用骨架屏
|
| 481 |
+
SKELETON_LOADING: true
|
| 482 |
+
};
|
| 483 |
+
|
| 484 |
+
|
| 485 |
+
// ==========================================
|
| 486 |
+
// 🌐 运行环境开关
|
| 487 |
+
// ==========================================
|
| 488 |
+
// 移动端网页版运行时开关:Web 引导页(云端Space代码/移动端网页/index.html)
|
| 489 |
+
// 在模块求值前置位 window.__RANKING_WEB_MODE__ = true;ComfyUI 环境不置位,恒为 false,
|
| 490 |
+
// 本地侧边栏行为零变化。所有「仅本地可用」功能的门控均以此为准
|
| 491 |
+
export const IS_WEB_MODE = typeof window !== "undefined" && window.__RANKING_WEB_MODE__ === true;
|
| 492 |
+
|
| 493 |
+
|
| 494 |
+
// ==========================================
|
| 495 |
+
// 📊 统计埋点配置(预留)
|
| 496 |
+
// ==========================================
|
| 497 |
+
export const ANALYTICS = {
|
| 498 |
+
// 是否启用统计
|
| 499 |
+
ENABLED: false,
|
| 500 |
+
|
| 501 |
+
// 统计服务地址
|
| 502 |
+
ENDPOINT: "",
|
| 503 |
+
|
| 504 |
+
// 采样率(0-1)
|
| 505 |
+
SAMPLE_RATE: 1.0
|
| 506 |
+
};
|
| 507 |
+
|
| 508 |
+
|
| 509 |
+
// ==========================================
|
| 510 |
+
// 🔗 外部链接
|
| 511 |
+
// ==========================================
|
| 512 |
+
export const LINKS = {
|
| 513 |
+
// GitHub 仓库
|
| 514 |
+
GITHUB_REPO: "https://github.com/a63976659/ComfyUI-Ranking",
|
| 515 |
+
|
| 516 |
+
// 文档地址
|
| 517 |
+
DOCS: "",
|
| 518 |
+
|
| 519 |
+
// 反馈地址
|
| 520 |
+
FEEDBACK: ""
|
| 521 |
+
};
|
| 522 |
+
|
| 523 |
+
|
| 524 |
+
// ==========================================
|
| 525 |
+
// 🛠️ 辅助函数
|
| 526 |
+
// ==========================================
|
| 527 |
+
|
| 528 |
+
/**
|
| 529 |
+
* 获取完整的 API 地址
|
| 530 |
+
* @param {string} endpoint - API 端点
|
| 531 |
+
* @returns {string}
|
| 532 |
+
*/
|
| 533 |
+
export function getApiUrl(endpoint) {
|
| 534 |
+
return `${API.BASE_URL}${endpoint}`;
|
| 535 |
+
}
|
| 536 |
+
|
| 537 |
+
/**
|
| 538 |
+
* 获取当前登录用户账号
|
| 539 |
+
* @returns {string|null}
|
| 540 |
+
*/
|
| 541 |
+
export function getCurrentAccount() {
|
| 542 |
+
try {
|
| 543 |
+
const userStr = localStorage.getItem(CACHE.LEGACY_KEYS.USER) || sessionStorage.getItem(CACHE.LEGACY_KEYS.USER);
|
| 544 |
+
if (userStr) {
|
| 545 |
+
const userData = JSON.parse(userStr);
|
| 546 |
+
return userData?.user?.account || null;
|
| 547 |
+
}
|
| 548 |
+
} catch (e) {
|
| 549 |
+
console.warn("获取当前账号失败。");
|
| 550 |
+
}
|
| 551 |
+
return null;
|
| 552 |
+
}
|
| 553 |
+
|
| 554 |
+
/**
|
| 555 |
+
* 获取账号区分的界面背景图存储键
|
| 556 |
+
* @param {string} [account] - 账号,不传则自动获取当前登录账号
|
| 557 |
+
* @returns {string}
|
| 558 |
+
*/
|
| 559 |
+
export function getBackgroundKey(account) {
|
| 560 |
+
const acc = account || getCurrentAccount();
|
| 561 |
+
return acc
|
| 562 |
+
? `${CACHE.LOCAL_KEYS.SIDEBAR_BACKGROUND}_${acc}`
|
| 563 |
+
: CACHE.LOCAL_KEYS.SIDEBAR_BACKGROUND;
|
| 564 |
+
}
|
| 565 |
+
|
| 566 |
+
/**
|
| 567 |
+
* 获取账号区分的个人资料卡背景缓存键
|
| 568 |
+
* @param {string} [account] - 账号,不传则自动获取当前登录账号
|
| 569 |
+
* @returns {string}
|
| 570 |
+
*/
|
| 571 |
+
export function getBannerCacheKey(account) {
|
| 572 |
+
const acc = account || getCurrentAccount();
|
| 573 |
+
return acc
|
| 574 |
+
? `${CACHE.LOCAL_KEYS.PROFILE_BANNER_CACHE}_${acc}`
|
| 575 |
+
: CACHE.LOCAL_KEYS.PROFILE_BANNER_CACHE;
|
| 576 |
+
}
|
| 577 |
+
|
| 578 |
+
/**
|
| 579 |
+
* 获取缓存键(带前缀)
|
| 580 |
+
* @param {string} key - 缓存键
|
| 581 |
+
* @returns {string}
|
| 582 |
+
*/
|
| 583 |
+
export function getCacheKey(key) {
|
| 584 |
+
return `${CACHE.PREFIX}${key}`;
|
| 585 |
+
}
|
| 586 |
+
|
| 587 |
+
/**
|
| 588 |
+
* 检查功能是否启用
|
| 589 |
+
* @param {string} feature - 功能名
|
| 590 |
+
* @returns {boolean}
|
| 591 |
+
*/
|
| 592 |
+
export function isFeatureEnabled(feature) {
|
| 593 |
+
return FEATURES[feature] === true;
|
| 594 |
+
}
|
| 595 |
+
|
| 596 |
+
/**
|
| 597 |
+
* 获取打赏等级配置
|
| 598 |
+
* @returns {Object}
|
| 599 |
+
*/
|
| 600 |
+
export function getTipLevelConfig() {
|
| 601 |
+
return { ...TIP_LEVELS };
|
| 602 |
+
}
|
| 603 |
+
|
| 604 |
+
|
| 605 |
+
// ==========================================
|
| 606 |
+
// 🚀 SWR 头像缓存工具(跨组件共享)
|
| 607 |
+
// ==========================================
|
| 608 |
+
// 实现原理:
|
| 609 |
+
// 1. 从 localStorage 读取缓存,0延迟渲染
|
| 610 |
+
// 2. 后台静默校对云端数据
|
| 611 |
+
// 3. 有更新时自动刷新 DOM
|
| 612 |
+
// 缓存键:ComfyCommunity_ProfileCache_{account}
|
| 613 |
+
|
| 614 |
+
const PROFILE_CACHE_PREFIX = "ComfyCommunity_ProfileCache_";
|
| 615 |
+
const _pendingProfileRequests = new Map(); // 每账号共享请求与全部订阅者
|
| 616 |
+
const PENDING_TIMEOUT = 30000; // 挂起请求超时清理时间(毫秒)
|
| 617 |
+
let nextAvatarRenderId = 0;
|
| 618 |
+
|
| 619 |
+
// ==========================================
|
| 620 |
+
// 🔒 HTML 转义(底层公共工具)
|
| 621 |
+
// ==========================================
|
| 622 |
+
// 放在本文件(零依赖的底层模块)而非 components/互动工具函数.js,是因为
|
| 623 |
+
// UI交互提示组件.js 与 打赏等级工具.js 都需要它,而互动工具函数.js 反向依赖这两者
|
| 624 |
+
// (导入 showToast、re-export renderTipBoardHTML),从那里导入会形成循环依赖。
|
| 625 |
+
// 互动工具函数.js 仍以 re-export 方式对外暴露同名函数,既有调用方无需改动。
|
| 626 |
+
|
| 627 |
+
/**
|
| 628 |
+
* HTML转义(统一版:各组件局部副本已归一到此)
|
| 629 |
+
* @param {*} str - 原始字符串(非字符串会先强制转换,兼容数字等入参)
|
| 630 |
+
* @returns {string} 转义后的字符串
|
| 631 |
+
*/
|
| 632 |
+
export function escapeHtml(str) {
|
| 633 |
+
if (str === null || str === undefined || str === "") return "";
|
| 634 |
+
return String(str).replace(/&/g, "&")
|
| 635 |
+
.replace(/</g, "<")
|
| 636 |
+
.replace(/>/g, ">")
|
| 637 |
+
.replace(/"/g, """)
|
| 638 |
+
.replace(/'/g, "'");
|
| 639 |
+
}
|
| 640 |
+
|
| 641 |
+
/** 资料图片只接受明确的图片表示或网络地址,不相对宿主解析业务路径。 */
|
| 642 |
+
export function normalizeProfileImageUrl(value, unwrapProxy = false, serviceBaseUrl = API.BASE_URL) {
|
| 643 |
+
if (typeof value !== 'string' || !value || value.trim() !== value || /[\u0000-\u001f\u007f]/.test(value)) return '';
|
| 644 |
+
let source = value;
|
| 645 |
+
let layers = 0;
|
| 646 |
+
while (source.startsWith(API.LOCAL_IMAGE_PROXY_PREFIX)) {
|
| 647 |
+
if (++layers > 10) return '';
|
| 648 |
+
let encoded = source.slice(API.LOCAL_IMAGE_PROXY_PREFIX.length);
|
| 649 |
+
if (encoded.endsWith(API.COMPLETE_IMAGE_QUERY_SUFFIX)) encoded = encoded.slice(0, -API.COMPLETE_IMAGE_QUERY_SUFFIX.length);
|
| 650 |
+
try { source = decodeURIComponent(encoded); } catch (_) { return ''; }
|
| 651 |
+
if (!source || source.trim() !== source || /[\u0000-\u001f\u007f]/.test(source)) return '';
|
| 652 |
+
}
|
| 653 |
+
if (!layers && /^data:image\/[a-z0-9.+-]+(?:;[^,]*)?,.+$/i.test(source)) return value;
|
| 654 |
+
if (!layers && /^blob:https?:\/\//i.test(source)) {
|
| 655 |
+
try {
|
| 656 |
+
const blob = new URL(source);
|
| 657 |
+
if (typeof location !== 'undefined' && location.origin && blob.origin !== location.origin) return '';
|
| 658 |
+
return blob.origin === 'null' ? '' : value;
|
| 659 |
+
} catch (_) { return ''; }
|
| 660 |
+
}
|
| 661 |
+
if (!/^https?:\/\//i.test(source) || source.includes('\\')) return '';
|
| 662 |
+
try {
|
| 663 |
+
if (value.length > API.PROFILE_MEDIA_URL_MAX_BYTES
|
| 664 |
+
|| encodeURIComponent(value).replace(/%[0-9a-f]{2}/gi, 'x').length > API.PROFILE_MEDIA_URL_MAX_BYTES) return '';
|
| 665 |
+
const url = new URL(source);
|
| 666 |
+
const authority = source.match(/^https?:\/\/([^/?#]*)/i)?.[1];
|
| 667 |
+
if (!authority || authority.includes('@') || authority.includes('%') || authority.endsWith(':') || url.port === '0') return '';
|
| 668 |
+
if (url.username || url.password) return '';
|
| 669 |
+
const host = url.hostname.toLowerCase().replace(/\.$/, '');
|
| 670 |
+
if (/^\d+\.\d+\.\d+\.\d+$/.test(host)
|
| 671 |
+
&& authority.replace(/:[0-9]+$/, '').replace(/\.$/, '').toLowerCase() !== host) return '';
|
| 672 |
+
const base = new URL(serviceBaseUrl);
|
| 673 |
+
const baseHost = base.hostname.toLowerCase().replace(/\.$/, '');
|
| 674 |
+
if (host === baseHost) {
|
| 675 |
+
const expectedPath = base.pathname.replace(/\/$/, '') + API.IMAGE_PROXY_PATH;
|
| 676 |
+
const sourcePath = source.slice(source.indexOf('://') + 3 + authority.length).split(/[?#]/, 1)[0] || '/';
|
| 677 |
+
if (url.protocol !== base.protocol || url.port !== base.port || sourcePath !== expectedPath) return '';
|
| 678 |
+
return unwrapProxy ? source : value;
|
| 679 |
+
}
|
| 680 |
+
if (!host.startsWith('[') && !/^\d+\.\d+\.\d+\.\d+$/.test(host)) {
|
| 681 |
+
const labels = host.split('.');
|
| 682 |
+
if (host.length > 253 || labels.length < 2 || !labels.every(label => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label))
|
| 683 |
+
|| /^(?:0x[0-9a-f]+|[0-9]+)$/i.test(labels.at(-1))) return '';
|
| 684 |
+
}
|
| 685 |
+
if (!host || host === 'localhost' || host.endsWith('.localhost') || host.endsWith('.local')) return '';
|
| 686 |
+
if (host.startsWith('[')) {
|
| 687 |
+
if (!/^\[[23][0-9a-f]{0,3}:/i.test(host)) return '';
|
| 688 |
+
const pieces = host.slice(1, -1).split('::');
|
| 689 |
+
const left = pieces[0] ? pieces[0].split(':').map(part => parseInt(part, 16)) : [];
|
| 690 |
+
const right = pieces[1] ? pieces[1].split(':').map(part => parseInt(part, 16)) : [];
|
| 691 |
+
const groups = pieces.length === 1 ? left : [...left, ...Array(8 - left.length - right.length).fill(0), ...right];
|
| 692 |
+
const [first, second, third] = groups;
|
| 693 |
+
if (first === 0x2002 || (first === 0x3fff && second < 0x1000) || (first === 0x2001 && second === 0xdb8)) return '';
|
| 694 |
+
if (first === 0x2001 && second < 0x200) {
|
| 695 |
+
const exception = (second === 1 && groups.slice(2, 7).every(part => part === 0) && [1, 2].includes(groups[7]))
|
| 696 |
+
|| second === 3 || (second === 4 && third === 0x112) || (second >= 0x20 && second <= 0x3f);
|
| 697 |
+
if (!exception) return '';
|
| 698 |
+
}
|
| 699 |
+
} else if (/^\d+\.\d+\.\d+\.\d+$/.test(host)) {
|
| 700 |
+
const rawHost = authority.replace(/:[0-9]+$/, '').replace(/\.$/, '').toLowerCase();
|
| 701 |
+
if (rawHost !== host) return '';
|
| 702 |
+
const [a,b,c,last] = host.split('.').map(Number);
|
| 703 |
+
if (a === 0 || a === 10 || a === 127 || a >= 224 || (a === 100 && b >= 64 && b <= 127)
|
| 704 |
+
|| (a === 169 && b === 254) || (a === 172 && b >= 16 && b <= 31)
|
| 705 |
+
|| (a === 192 && b === 168) || (a === 198 && (b === 18 || b === 19))
|
| 706 |
+
|| (a === 192 && b === 0 && ((c === 0 && ![9, 10].includes(last)) || c === 2))
|
| 707 |
+
|| (a === 198 && b === 51 && c === 100) || (a === 203 && b === 0 && c === 113)) return '';
|
| 708 |
+
}
|
| 709 |
+
return unwrapProxy ? source : value;
|
| 710 |
+
} catch (_) { return ''; }
|
| 711 |
+
}
|
| 712 |
+
|
| 713 |
+
/** 统一字段映射:将后端 avatarDataUrl 映射为前端 avatar */
|
| 714 |
+
function _mapAvatarField(profile) {
|
| 715 |
+
if (profile && profile.avatarDataUrl && !profile.avatar) {
|
| 716 |
+
profile.avatar = profile.avatarDataUrl;
|
| 717 |
+
}
|
| 718 |
+
}
|
| 719 |
+
|
| 720 |
+
const PUBLIC_PROFILE_FIELDS = ["account", "name", "avatar", "avatarDataUrl", "intro", "gender", "age",
|
| 721 |
+
"country", "region", "created_at", "followers", "following", "privacy", "is_admin", "bannerUrl",
|
| 722 |
+
"receivedLikes", "receivedFavorites", "receivedUses"];
|
| 723 |
+
function _publicProfile(profile) {
|
| 724 |
+
return Object.fromEntries(PUBLIC_PROFILE_FIELDS.filter(key => Object.prototype.hasOwnProperty.call(profile, key))
|
| 725 |
+
.map(key => [key, ['avatar', 'avatarDataUrl', 'bannerUrl'].includes(key)
|
| 726 |
+
? normalizeProfileImageUrl(profile[key]) : profile[key]]));
|
| 727 |
+
}
|
| 728 |
+
|
| 729 |
+
function _validProfile(profile) {
|
| 730 |
+
return !!profile && typeof profile === 'object' && !Array.isArray(profile)
|
| 731 |
+
&& ['name', 'avatar', 'avatarDataUrl'].every(field => profile[field] == null || typeof profile[field] === 'string');
|
| 732 |
+
}
|
| 733 |
+
|
| 734 |
+
/**
|
| 735 |
+
* 🚀 获取缓存的用户资料(同步)
|
| 736 |
+
* @param {string} account - 用户账号
|
| 737 |
+
* @returns {Object|null} 缓存的用户资料
|
| 738 |
+
*/
|
| 739 |
+
export function getCachedProfile(account) {
|
| 740 |
+
if (!account) return null;
|
| 741 |
+
try {
|
| 742 |
+
const cached = localStorage.getItem(PROFILE_CACHE_PREFIX + account);
|
| 743 |
+
if (!cached) return null;
|
| 744 |
+
const profile = JSON.parse(cached);
|
| 745 |
+
if (!_validProfile(profile)) return null;
|
| 746 |
+
const publicProfile = _publicProfile(profile);
|
| 747 |
+
_mapAvatarField(publicProfile);
|
| 748 |
+
// 旧资料缓存可能含联系方式;读取返回过滤副本并尝试迁移,不因迁移失败隐藏公开内容。
|
| 749 |
+
if (JSON.stringify(publicProfile) !== cached) {
|
| 750 |
+
try { localStorage.setItem(PROFILE_CACHE_PREFIX + account, JSON.stringify(publicProfile)); } catch (_) {}
|
| 751 |
+
}
|
| 752 |
+
return publicProfile;
|
| 753 |
+
} catch {
|
| 754 |
+
return null;
|
| 755 |
+
}
|
| 756 |
+
}
|
| 757 |
+
|
| 758 |
+
/**
|
| 759 |
+
* 🚀 保存用户资料到缓存
|
| 760 |
+
* @param {string} account - 用户账号
|
| 761 |
+
* @param {Object} profile - 用户资料
|
| 762 |
+
*/
|
| 763 |
+
export function setCachedProfile(account, profile) {
|
| 764 |
+
if (!account || !_validProfile(profile)) return;
|
| 765 |
+
try {
|
| 766 |
+
localStorage.setItem(PROFILE_CACHE_PREFIX + account, JSON.stringify(_publicProfile(profile)));
|
| 767 |
+
} catch {}
|
| 768 |
+
}
|
| 769 |
+
|
| 770 |
+
/**
|
| 771 |
+
* 🚀 SWR 模式获取用户资料(先返回缓存,后台校对)
|
| 772 |
+
* @param {string} account - 用户账号
|
| 773 |
+
* @param {Function} apiFn - API 调用函数 (account) => Promise<{data: profile}>
|
| 774 |
+
* @param {Function} onUpdate - 数据更新时的回调 (profile) => void
|
| 775 |
+
* @returns {Object|null} 初始缓存数据
|
| 776 |
+
*/
|
| 777 |
+
export function getProfileWithSWR(account, apiFn, onUpdate) {
|
| 778 |
+
if (!account) return null;
|
| 779 |
+
|
| 780 |
+
// 第一轨:同步返回缓存
|
| 781 |
+
const cached = getCachedProfile(account);
|
| 782 |
+
|
| 783 |
+
// 第二轨:同账号只取一次,但每个视图都订阅结果。
|
| 784 |
+
let entry = _pendingProfileRequests.get(account);
|
| 785 |
+
if (!entry && typeof apiFn === 'function') {
|
| 786 |
+
entry = { listeners: new Set(), timer: null };
|
| 787 |
+
_pendingProfileRequests.set(account, entry);
|
| 788 |
+
const release = () => {
|
| 789 |
+
if (_pendingProfileRequests.get(account) === entry) _pendingProfileRequests.delete(account);
|
| 790 |
+
clearTimeout(entry.timer);
|
| 791 |
+
entry.listeners.clear();
|
| 792 |
+
};
|
| 793 |
+
entry.timer = setTimeout(release, PENDING_TIMEOUT);
|
| 794 |
+
Promise.resolve().then(() => apiFn(account)).then(res => {
|
| 795 |
+
// 超时后的旧结果不能覆盖新请求,也不能重新写入旧资料。
|
| 796 |
+
if (_pendingProfileRequests.get(account) !== entry) return;
|
| 797 |
+
if (res?.status && res.status !== 'success') return;
|
| 798 |
+
const data = res && Object.hasOwn(res, 'data') ? res.data : res;
|
| 799 |
+
if (!_validProfile(data)) return;
|
| 800 |
+
const profile = _publicProfile(data);
|
| 801 |
+
_mapAvatarField(profile);
|
| 802 |
+
setCachedProfile(account, profile);
|
| 803 |
+
for (const listener of Array.from(entry.listeners)) {
|
| 804 |
+
try { listener(profile); } catch { /* 单个视图异常不阻断其它订阅者 */ }
|
| 805 |
+
}
|
| 806 |
+
}).catch(() => {}).finally(release);
|
| 807 |
+
}
|
| 808 |
+
if (entry && typeof onUpdate === 'function') entry.listeners.add(onUpdate);
|
| 809 |
+
|
| 810 |
+
return cached;
|
| 811 |
+
}
|
| 812 |
+
|
| 813 |
+
/**
|
| 814 |
+
* 🚀 渲染带 SWR 缓存的头像+名称 HTML
|
| 815 |
+
* @param {Object} options
|
| 816 |
+
* @param {string} options.account - 用户账号
|
| 817 |
+
* @param {string} options.fallbackAvatar - 备用头像 URL(从原始数据中获取)
|
| 818 |
+
* @param {string} options.fallbackName - 备用名称
|
| 819 |
+
* @param {number} options.avatarSize - 头像尺寸 (px)
|
| 820 |
+
* @param {Function} options.apiFn - API 调用函数
|
| 821 |
+
* @param {string} options.containerId - 唯一容器 ID(用于 DOM 更新)
|
| 822 |
+
* @returns {string} HTML 字符串
|
| 823 |
+
*/
|
| 824 |
+
export function renderAvatarWithSWR(options) {
|
| 825 |
+
const { account, fallbackAvatar, fallbackName, avatarSize = 24, apiFn, containerId } = options;
|
| 826 |
+
const renderId = String(++nextAvatarRenderId);
|
| 827 |
+
const size = typeof avatarSize === 'number' || typeof avatarSize === 'string' ? Number(avatarSize) : NaN;
|
| 828 |
+
const safeSize = Number.isFinite(size) && size > 0 ? size : 24;
|
| 829 |
+
|
| 830 |
+
// 从缓存获取
|
| 831 |
+
const cached = getCachedProfile(account);
|
| 832 |
+
const avatar = cached?.avatar || cached?.avatarDataUrl || normalizeProfileImageUrl(fallbackAvatar);
|
| 833 |
+
const name = cached?.name || fallbackName || account || '';
|
| 834 |
+
|
| 835 |
+
// 后台静默校对并更新 DOM
|
| 836 |
+
if (apiFn && containerId) {
|
| 837 |
+
setTimeout(() => {
|
| 838 |
+
const container = document.getElementById(containerId);
|
| 839 |
+
if (!container || container.dataset.profileRender !== renderId) return;
|
| 840 |
+
getProfileWithSWR(account, apiFn, (profile) => {
|
| 841 |
+
if (!container.isConnected || document.getElementById(containerId) !== container
|
| 842 |
+
|| container.dataset.profileRender !== renderId) return;
|
| 843 |
+
|
| 844 |
+
const avatarImg = container.querySelector('.swr-avatar');
|
| 845 |
+
const nameSpan = container.querySelector('.swr-name');
|
| 846 |
+
|
| 847 |
+
if (avatarImg && profile.avatar) {
|
| 848 |
+
if (avatarImg.tagName === 'IMG') avatarImg.src = profile.avatar;
|
| 849 |
+
else {
|
| 850 |
+
const image = document.createElement('img');
|
| 851 |
+
image.className = 'swr-avatar';
|
| 852 |
+
image.alt = '';
|
| 853 |
+
image.style.cssText = avatarImg.style.cssText;
|
| 854 |
+
image.style.objectFit = 'cover';
|
| 855 |
+
image.src = profile.avatar;
|
| 856 |
+
avatarImg.replaceWith(image);
|
| 857 |
+
}
|
| 858 |
+
} else if (avatarImg && avatarImg.tagName !== 'IMG' && profile.name) {
|
| 859 |
+
avatarImg.textContent = Array.from(profile.name)[0]?.toUpperCase() || 'U';
|
| 860 |
+
}
|
| 861 |
+
if (nameSpan && profile.name) {
|
| 862 |
+
nameSpan.textContent = profile.name;
|
| 863 |
+
}
|
| 864 |
+
});
|
| 865 |
+
}, 0);
|
| 866 |
+
}
|
| 867 |
+
|
| 868 |
+
// 生成初始 HTML(无头像时显示首字母背景)
|
| 869 |
+
// 🔒 XSS防护:avatar/name/initial 均来自用户可修改的资料(昵称首字母可能是 < ),
|
| 870 |
+
// 写入 HTML 前必须转义;上方 SWR 刷新路径用的是 .src / .textContent,本身就是安全的。
|
| 871 |
+
const initial = Array.from(String(name || account || 'U'))[0].toUpperCase();
|
| 872 |
+
const avatarHtml = avatar
|
| 873 |
+
? `<img class="swr-avatar" alt="" src="${escapeHtml(avatar)}" style="width: ${safeSize}px; height: ${safeSize}px; border-radius: 50%; object-fit: cover; flex-shrink: 0; background: #333;">`
|
| 874 |
+
: `<div class="swr-avatar" aria-hidden="true" style="width: ${safeSize}px; height: ${safeSize}px; border-radius: 50%; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; align-items: center; justify-content: center; color: white; font-size: ${Math.floor(safeSize * 0.5)}px; font-weight: bold; flex-shrink: 0;">${escapeHtml(initial)}</div>`;
|
| 875 |
+
|
| 876 |
+
return `<span id="${escapeHtml(containerId)}" data-profile-render="${renderId}" style="display: inline-flex; align-items: center; gap: 6px;">${avatarHtml}<span class="swr-name">${escapeHtml(name)}</span></span>`;
|
| 877 |
+
}
|
| 878 |
+
|
| 879 |
+
|
| 880 |
+
// ==========================================
|
| 881 |
+
// 📦 导出默认配置对象
|
| 882 |
+
// ==========================================
|
| 883 |
+
export default {
|
| 884 |
+
IMAGE_PROCESS,
|
| 885 |
+
API,
|
| 886 |
+
CACHE,
|
| 887 |
+
PAGINATION,
|
| 888 |
+
TIP_LEVELS,
|
| 889 |
+
UI,
|
| 890 |
+
VALIDATION,
|
| 891 |
+
FEATURES,
|
| 892 |
+
IS_WEB_MODE,
|
| 893 |
+
ANALYTICS,
|
| 894 |
+
LINKS,
|
| 895 |
+
getApiUrl,
|
| 896 |
+
getCacheKey,
|
| 897 |
+
isFeatureEnabled,
|
| 898 |
+
getCurrentAccount,
|
| 899 |
+
getBackgroundKey,
|
| 900 |
+
getBannerCacheKey,
|
| 901 |
+
getCachedProfile,
|
| 902 |
+
setCachedProfile,
|
| 903 |
+
getProfileWithSWR,
|
| 904 |
+
renderAvatarWithSWR
|
| 905 |
+
};
|
| 906 |
+
|
| 907 |
+
|
| 908 |
+
// 提示词定价输入边界,与云端 PromptCreate/PromptUpdate 对应。
|
| 909 |
+
export const PROMPT_LIMITS = Object.freeze({ MAX_PRICE: 10000 });
|
| 910 |
+
|
| 911 |
+
// 任务定价边界,与云端 task_publication/task_terms 对应。
|
| 912 |
+
export const TASK_LIMITS = Object.freeze({ MAX_PRICE: 2147483647, MAX_REFERENCE_IMAGES: 6, DEPOSIT_RATIOS: Object.freeze([10, 20, 30, 50]), TIP_AMOUNTS: Object.freeze([10, 50, 100, 500]) });
|
| 913 |
+
|
| 914 |
+
|
| 915 |
+
export const TYPE_RULES = {
|
| 916 |
+
tool: {
|
| 917 |
+
recommendForm: false, resType: { selectable: ['link', 'netdisk'] },
|
| 918 |
+
privateRepoWhenLink: true, price: true, refund: true, originalRequired: true
|
| 919 |
+
},
|
| 920 |
+
skill: {
|
| 921 |
+
recommendForm: false, resType: { selectable: ['link', 'netdisk'] },
|
| 922 |
+
privateRepoWhenLink: true, price: true, refund: true, originalRequired: true
|
| 923 |
+
},
|
| 924 |
+
app: {
|
| 925 |
+
recommendForm: false, resType: { fixed: 'json' },
|
| 926 |
+
privateRepoWhenLink: true, price: true, refund: true, originalRequired: true
|
| 927 |
+
},
|
| 928 |
+
recommend: {
|
| 929 |
+
recommendForm: true, resType: { lockedByForm: true },
|
| 930 |
+
privateRepoWhenLink: false, price: false, refund: false, originalRequired: false,
|
| 931 |
+
formToResType: { recommend_tool: 'link', recommend_skill: 'link', recommend_app: 'json', recommend_link: 'netdisk' }
|
| 932 |
+
}
|
| 933 |
+
};
|
| 934 |
+
|
| 935 |
+
// 🔧 设置项 localStorage Key
|
| 936 |
+
const SETTINGS_KEY = "ComfyCommunity_Settings";
|
| 937 |
+
|
| 938 |
+
// 🔧 默认设置值
|
| 939 |
+
const DEFAULT_SETTINGS = {
|
| 940 |
enableKeyboardShortcuts: false,
|
| 941 |
+
enableLocalPerformanceStats: false,
|
| 942 |
+
showCreatorBanner: true, // 创作者卡片显示背景图
|
| 943 |
+
enableAnimations: true, // ✨ 榜单切换动画
|
| 944 |
+
enableSoundEffects: true, // 🔊 动画音效
|
| 945 |
+
language: 'zh-CN', // 🌐 界面语言
|
| 946 |
+
cacheExpireSeconds: 7200, // ⏱️ 缓存刷新间隔(秒),默认2小时
|
| 947 |
+
// 🚀 后续可扩展更多设置项
|
| 948 |
+
// compactMode: false, // 紧凑模式
|
| 949 |
+
// darkMode: true, // 深色模式
|
| 950 |
+
};
|
| 951 |
+
|
| 952 |
+
/**
|
| 953 |
+
* 📦 获取当前设置
|
| 954 |
+
*/
|
| 955 |
+
export function getSettings(options = {}) {
|
| 956 |
+
try {
|
| 957 |
+
const saved = localStorage.getItem(SETTINGS_KEY);
|
| 958 |
+
if (saved) {
|
| 959 |
+
const value = JSON.parse(saved);
|
| 960 |
+
if (options.strict && (!value || typeof value !== 'object' || Array.isArray(value))) throw new Error('Invalid settings');
|
| 961 |
+
return { ...DEFAULT_SETTINGS, ...value };
|
| 962 |
+
}
|
| 963 |
+
} catch (e) { if (options.strict) throw e; }
|
| 964 |
+
return { ...DEFAULT_SETTINGS };
|
| 965 |
+
}
|
| 966 |
+
|
| 967 |
+
/**
|
| 968 |
+
* 💾 保存设置
|
| 969 |
+
*/
|
| 970 |
+
export function areKeyboardShortcutsEnabled() {
|
| 971 |
+
return getSettings().enableKeyboardShortcuts === true;
|
| 972 |
+
}
|
| 973 |
+
|
| 974 |
+
export function saveSettings(settings) {
|
| 975 |
+
try {
|
| 976 |
+
localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
|
| 977 |
+
// 🔔 广播设置变更事件,让其他组件响应
|
| 978 |
+
window.dispatchEvent(new CustomEvent("comfy-settings-changed", { detail: settings }));
|
| 979 |
+
return true;
|
| 980 |
+
} catch (e) { return false; }
|
| 981 |
+
}
|
前端页面/market/Skill安装流程.js
CHANGED
|
@@ -1,214 +1,218 @@
|
|
| 1 |
-
import { SKILL_INSTALL, escapeHtml } from '../core/全局配置.js';
|
| 2 |
-
import {
|
| 3 |
-
import {
|
| 4 |
-
import {
|
| 5 |
-
import {
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
const
|
| 9 |
-
|
| 10 |
-
&&
|
| 11 |
-
|
| 12 |
-
const
|
| 13 |
-
const
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
error
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
const
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
||
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
}
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
&&
|
| 77 |
-
&& value.directories
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
const
|
| 85 |
-
const
|
| 86 |
-
const
|
| 87 |
-
const
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
const
|
| 91 |
-
|
| 92 |
-
node.
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
const
|
| 96 |
-
|
| 97 |
-
let
|
| 98 |
-
|
| 99 |
-
const
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
const
|
| 107 |
-
const
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
if (!
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
const
|
| 131 |
-
folder.
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
}
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
&& result.
|
| 153 |
-
&&
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
&&
|
| 160 |
-
&&
|
| 161 |
-
&&
|
| 162 |
-
&& result.preview.
|
| 163 |
-
&& result.preview.items
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
if (!
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
}
|
| 214 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { SKILL_INSTALL, escapeHtml } from '../core/全局配置.js';
|
| 2 |
+
import { describeInstallSource } from './安装来源.js';
|
| 3 |
+
import { readLocalJSON } from '../core/本机请求读取.js';
|
| 4 |
+
import { requestSSE } from '../core/网络请求_基础设施.js';
|
| 5 |
+
import { t } from '../components/用户体验_国际化.js';
|
| 6 |
+
import { globalModal } from '../components/全局弹窗管理器.js';
|
| 7 |
+
|
| 8 |
+
const object = value => value && typeof value === 'object' && !Array.isArray(value);
|
| 9 |
+
const pathText = value => typeof value === 'string' && value.length > 0
|
| 10 |
+
&& !/[\u0000-\u001f\u007f]/u.test(value)
|
| 11 |
+
&& new TextEncoder().encode(value).length <= SKILL_INSTALL.PATH_MAX_BYTES;
|
| 12 |
+
const planId = value => typeof value === 'string' && /^[a-f0-9]{32}$/.test(value);
|
| 13 |
+
const versionId = value => typeof value === 'string' && /^[a-f0-9]{40}$/.test(value);
|
| 14 |
+
const count = value => Number.isSafeInteger(value) && value >= 0;
|
| 15 |
+
|
| 16 |
+
function failure(code) {
|
| 17 |
+
const error = new Error('skill_operation_failed');
|
| 18 |
+
error.code = code;
|
| 19 |
+
return error;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
export function skillErrorMessage(error) {
|
| 23 |
+
const errorKeys = {
|
| 24 |
+
local_update_required: 'skill.local_update_required',
|
| 25 |
+
invalid_target: 'install.error_invalid_target',
|
| 26 |
+
invalid_repository: 'install.error_invalid_repository',
|
| 27 |
+
invalid_skill_package: 'skill.invalid_package',
|
| 28 |
+
invalid_plan: 'skill.plan_invalid',
|
| 29 |
+
plan_expired: 'skill.plan_invalid',
|
| 30 |
+
plan_owner_mismatch: 'skill.identity_changed',
|
| 31 |
+
auth_failed: 'skill.identity_changed',
|
| 32 |
+
source_changed: 'skill.source_changed',
|
| 33 |
+
target_changed: 'skill.target_changed',
|
| 34 |
+
target_conflict: 'skill.target_changed',
|
| 35 |
+
recovery_conflict: 'skill.target_changed',
|
| 36 |
+
source_unavailable: 'skill.source_unavailable',
|
| 37 |
+
invalid_metadata: 'skill.source_unavailable',
|
| 38 |
+
directory_too_large: 'skill.directory_too_large',
|
| 39 |
+
package_too_large: 'install.error_package_too_large',
|
| 40 |
+
insufficient_disk_space: 'install.error_disk_space',
|
| 41 |
+
permission_denied: 'install.error_permission_denied',
|
| 42 |
+
install_queue_full: 'install.error_queue_full',
|
| 43 |
+
install_queue_timeout: 'install.error_queue_timeout',
|
| 44 |
+
};
|
| 45 |
+
const errorKey = Object.hasOwn(errorKeys, error?.code) ? errorKeys[error.code] : 'install.error_generic';
|
| 46 |
+
return t(errorKey);
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
async function control(action, body, signal, timeout = SKILL_INSTALL.CONTROL_TIMEOUT) {
|
| 50 |
+
const controller = new AbortController();
|
| 51 |
+
const abort = () => controller.abort();
|
| 52 |
+
signal?.addEventListener('abort', abort, { once: true });
|
| 53 |
+
if (signal?.aborted) controller.abort();
|
| 54 |
+
const timer = setTimeout(abort, timeout);
|
| 55 |
+
try {
|
| 56 |
+
if (controller.signal.aborted) throw failure('operation_cancelled');
|
| 57 |
+
const response = await fetch(SKILL_INSTALL.PATH, {
|
| 58 |
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
| 59 |
+
body: JSON.stringify({ ...body, action }), cache: 'no-store', signal: controller.signal,
|
| 60 |
+
});
|
| 61 |
+
if (response.status === 404 || response.status === 405) throw failure('local_update_required');
|
| 62 |
+
const value = await readLocalJSON(response, controller, SKILL_INSTALL.RESPONSE_MAX_BYTES);
|
| 63 |
+
if (!response.ok || !object(value) || value.status !== 'success' || !(value.installed === false
|
| 64 |
+
|| action === 'status' && value.phase === 'complete' && value.installed === true)
|
| 65 |
+
|| Object.hasOwn(value, 'error') || Object.hasOwn(value, 'error_code')) {
|
| 66 |
+
throw failure(object(value) && typeof value.error_code === 'string' ? value.error_code : 'invalid_metadata');
|
| 67 |
+
}
|
| 68 |
+
return value;
|
| 69 |
+
} finally {
|
| 70 |
+
clearTimeout(timer); signal?.removeEventListener('abort', abort); controller.abort();
|
| 71 |
+
}
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
function browseResult(value) {
|
| 75 |
+
return value.phase === 'browse' && pathText(value.path) && pathText(value.parent)
|
| 76 |
+
&& typeof value.selectable === 'boolean' && typeof value.truncated === 'boolean'
|
| 77 |
+
&& Array.isArray(value.directories) && value.directories.length <= SKILL_INSTALL.MAX_DIRECTORIES
|
| 78 |
+
&& value.directories.every(row => object(row) && pathText(row.name) && pathText(row.path));
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
export function chooseSkillDirectory(scope) {
|
| 82 |
+
if (!scope.isCurrent()) return Promise.resolve(null);
|
| 83 |
+
return new Promise(resolve => {
|
| 84 |
+
const content = document.createElement('div'); content.className = 'skill-directory';
|
| 85 |
+
const hint = document.createElement('p'); hint.textContent = t('skill.directory_hint');
|
| 86 |
+
const label = document.createElement('label'); label.className = 'skill-directory__label';
|
| 87 |
+
const title = document.createElement('span'); title.textContent = t('skill.directory_path');
|
| 88 |
+
const input = document.createElement('input'); input.type = 'text'; input.autocomplete = 'off'; input.spellcheck = false;
|
| 89 |
+
label.append(title, input);
|
| 90 |
+
const actions = document.createElement('div'); actions.className = 'skill-directory__actions';
|
| 91 |
+
const button = (label, callback) => {
|
| 92 |
+
const node = document.createElement('button'); node.type = 'button'; node.textContent = label;
|
| 93 |
+
node.addEventListener('click', callback); return node;
|
| 94 |
+
};
|
| 95 |
+
const notice = document.createElement('p'); notice.setAttribute('role', 'status'); notice.setAttribute('aria-live', 'polite');
|
| 96 |
+
const list = document.createElement('ul'); list.className = 'skill-directory__list'; list.setAttribute('aria-label', t('skill.directory_folders'));
|
| 97 |
+
let closed = false, generation = 0, selected = null, controller, modal;
|
| 98 |
+
let releaseScope = () => {};
|
| 99 |
+
const current = () => !closed && scope.isCurrent();
|
| 100 |
+
const finish = value => {
|
| 101 |
+
if (closed) return;
|
| 102 |
+
closed = true; generation++; controller?.abort(); releaseScope();
|
| 103 |
+
resolve(value); if (modal) globalModal.closeModal(modal);
|
| 104 |
+
};
|
| 105 |
+
content._cleanup = () => finish(null);
|
| 106 |
+
const open = button(t('skill.directory_open'), () => browse(input.value));
|
| 107 |
+
const parent = button(t('skill.directory_parent'), () => selected && browse(selected.parent));
|
| 108 |
+
const select = button(t('skill.directory_select'), () => {
|
| 109 |
+
if (current() && selected?.selectable && input.value === selected.path) finish(selected.path);
|
| 110 |
+
});
|
| 111 |
+
select.disabled = parent.disabled = true;
|
| 112 |
+
const cancel = button(t('common.cancel'), () => finish(null));
|
| 113 |
+
actions.append(open, parent, select, cancel);
|
| 114 |
+
input.addEventListener('input', () => {
|
| 115 |
+
generation++; controller?.abort(); selected = null; select.disabled = parent.disabled = true;
|
| 116 |
+
content.removeAttribute('aria-busy'); list.replaceChildren(); notice.textContent = t('skill.directory_verify');
|
| 117 |
+
});
|
| 118 |
+
async function browse(path) {
|
| 119 |
+
if (!current()) return;
|
| 120 |
+
const own = ++generation; controller?.abort(); controller = new AbortController();
|
| 121 |
+
selected = null; select.disabled = parent.disabled = true;
|
| 122 |
+
content.setAttribute('aria-busy', 'true'); notice.textContent = t('common.loading'); list.replaceChildren();
|
| 123 |
+
try {
|
| 124 |
+
const value = await control('browse', path === undefined ? {} : { path }, controller.signal);
|
| 125 |
+
if (!current() || own !== generation) return;
|
| 126 |
+
if (!browseResult(value)) throw failure('invalid_metadata');
|
| 127 |
+
selected = value; input.value = value.path;
|
| 128 |
+
select.disabled = !value.selectable; parent.disabled = value.parent === value.path;
|
| 129 |
+
for (const row of value.directories) {
|
| 130 |
+
const item = document.createElement('li');
|
| 131 |
+
const folder = document.createElement('button'); folder.type = 'button'; folder.textContent = row.name;
|
| 132 |
+
folder.addEventListener('click', () => { if (current() && own === generation) browse(row.path); });
|
| 133 |
+
item.append(folder); list.append(item);
|
| 134 |
+
}
|
| 135 |
+
notice.textContent = t(value.truncated ? 'skill.directory_truncated' : 'skill.directory_ready');
|
| 136 |
+
} catch (error) {
|
| 137 |
+
if (current() && own === generation) notice.textContent = skillErrorMessage(error);
|
| 138 |
+
} finally {
|
| 139 |
+
if (current() && own === generation) content.removeAttribute('aria-busy');
|
| 140 |
+
}
|
| 141 |
+
}
|
| 142 |
+
content.append(hint, label, actions, notice, list);
|
| 143 |
+
modal = globalModal.openModal(t('skill.directory_title'), content, { width: SKILL_INSTALL.MODAL_WIDTH });
|
| 144 |
+
releaseScope = scope.addCleanup(() => finish(null));
|
| 145 |
+
if (!current()) { finish(null); return; }
|
| 146 |
+
input.focus(); browse();
|
| 147 |
+
});
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
function confirmed(result, version) {
|
| 151 |
+
return object(result) && result.status === 'success' && result.phase === 'complete'
|
| 152 |
+
&& result.installed === true && (!Object.hasOwn(result, 'outcomeUnknown') || result.outcomeUnknown === false)
|
| 153 |
+
&& result.installed_version === version
|
| 154 |
+
&& !Object.hasOwn(result, 'error') && !Object.hasOwn(result, 'error_code');
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
function validReady(result, plan, target) {
|
| 158 |
+
return object(result) && result.status === 'success' && result.phase === 'ready'
|
| 159 |
+
&& result.installed === false && (!Object.hasOwn(result, 'outcomeUnknown') || result.outcomeUnknown === false)
|
| 160 |
+
&& !Object.hasOwn(result, 'error') && !Object.hasOwn(result, 'error_code') && versionId(result.installed_version)
|
| 161 |
+
&& describeInstallSource(result.repository) !== null
|
| 162 |
+
&& object(result.preview) && result.preview.id === plan && pathText(result.preview.target) && result.preview.target === target
|
| 163 |
+
&& Array.isArray(result.preview.items) && result.preview.items.length > 0
|
| 164 |
+
&& result.preview.items.length <= SKILL_INSTALL.MAX_ITEMS
|
| 165 |
+
&& result.preview.items.every(row => object(row) && pathText(row.name)
|
| 166 |
+
&& !/[\\/]/u.test(row.name) && typeof row.skill === 'boolean' && typeof row.exists === 'boolean'
|
| 167 |
+
&& count(row.files) && count(row.bytes));
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
export async function runSkillInstall({ target, credentials, scope, confirm, onProgress, signal = scope.confirmationSignal }) {
|
| 171 |
+
let plan, committing = false;
|
| 172 |
+
try {
|
| 173 |
+
if (!scope.isCurrent() || signal.aborted) return { status: 'cancelled' };
|
| 174 |
+
const selected = await control('select', { ...credentials, target }, signal);
|
| 175 |
+
if (!planId(selected.plan_id) || selected.phase !== 'selected' || selected.target !== target
|
| 176 |
+
|| !count(selected.expires_at)) throw failure('invalid_metadata');
|
| 177 |
+
plan = selected.plan_id;
|
| 178 |
+
if (!scope.isCurrent() || signal.aborted) return { status: 'cancelled' };
|
| 179 |
+
const parameters = { ...credentials, plan_id: plan };
|
| 180 |
+
const stream = action => requestSSE(SKILL_INSTALL.PATH, { ...parameters, action },
|
| 181 |
+
event => { if (scope.isCurrent()) onProgress?.({ ...event, skillAction: action }); },
|
| 182 |
+
{ signal, timeout: SKILL_INSTALL.STREAM_TIMEOUT });
|
| 183 |
+
const ready = await stream('prepare');
|
| 184 |
+
if (!scope.isCurrent() || signal.aborted) return { status: 'cancelled' };
|
| 185 |
+
if (!validReady(ready, plan, target)) throw failure(ready?.error_code || 'invalid_metadata');
|
| 186 |
+
const source = describeInstallSource(ready.repository);
|
| 187 |
+
const html = `<div class="skill-install-preview"><p>${escapeHtml(t('install.source', { source: source.label }))}</p>`
|
| 188 |
+
+ `<p>${escapeHtml(t('skill.preview_target', { path: ready.preview.target }))}</p>`
|
| 189 |
+
+ `<p>${escapeHtml(t('skill.preview_hint'))}</p><ul>`
|
| 190 |
+
+ ready.preview.items.map(row => `<li>${escapeHtml(row.name)} — ${escapeHtml(t(row.exists ? 'skill.preview_replace' : 'skill.preview_new'))}`
|
| 191 |
+
+ ` · ${escapeHtml(t(row.skill ? 'skill.preview_skill' : 'skill.preview_shared'))}</li>`).join('') + '</ul></div>';
|
| 192 |
+
if (!await confirm(html, { html: true }) || !scope.isCurrent() || signal.aborted) return { status: 'cancelled' };
|
| 193 |
+
committing = true;
|
| 194 |
+
let result;
|
| 195 |
+
try { result = await stream('commit'); }
|
| 196 |
+
catch (error) {
|
| 197 |
+
if (error?.fallbackSafe) return { status: 'error', error_code: 'local_update_required', outcomeUnknown: false };
|
| 198 |
+
}
|
| 199 |
+
if (confirmed(result, ready.installed_version)) return result;
|
| 200 |
+
if (object(result) && result.status === 'error' && result.outcomeUnknown === false
|
| 201 |
+
&& typeof result.error_code === 'string') return result;
|
| 202 |
+
if (scope.isCurrent() && !signal.aborted) {
|
| 203 |
+
try {
|
| 204 |
+
onProgress?.({ stage: 'validate', progress: 99, skillAction: 'status' });
|
| 205 |
+
const receipt = await control('status', parameters, signal, SKILL_INSTALL.RESULT_TIMEOUT);
|
| 206 |
+
if (scope.isCurrent() && !signal.aborted && receipt.plan_id === plan && receipt.target === target
|
| 207 |
+
&& confirmed(receipt, ready.installed_version)) return receipt;
|
| 208 |
+
} catch {}
|
| 209 |
+
}
|
| 210 |
+
return { status: 'error', outcomeUnknown: true };
|
| 211 |
+
} catch (error) {
|
| 212 |
+
throw failure(error?.fallbackSafe ? 'local_update_required' : error?.code || 'install_failed');
|
| 213 |
+
} finally {
|
| 214 |
+
if (plan && !committing) {
|
| 215 |
+
try { await control('discard', { ...credentials, plan_id: plan }); } catch {}
|
| 216 |
+
}
|
| 217 |
+
}
|
| 218 |
+
}
|
前端页面/market/安装来源.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { UI } from '../core/全局配置.js';
|
| 2 |
+
|
| 3 |
+
export function describeInstallSource(value) {
|
| 4 |
+
const spec = UI.INSTALL_SOURCE;
|
| 5 |
+
if (typeof value !== 'string' || value.length > spec.MAX_URL_BYTES) return null;
|
| 6 |
+
const text = value.trim();
|
| 7 |
+
if (!text || /[\u0000-\u0020\u007f\\]/u.test(text)
|
| 8 |
+
|| new TextEncoder().encode(text).length > spec.MAX_URL_BYTES) return null;
|
| 9 |
+
try {
|
| 10 |
+
const url = new URL(text);
|
| 11 |
+
if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash) return null;
|
| 12 |
+
let path = url.pathname.replace(/\/+$/u, '');
|
| 13 |
+
if (path.endsWith('.git')) path = path.slice(0, -4);
|
| 14 |
+
if (!path || path === '/') return null;
|
| 15 |
+
const identityPath = spec.CASE_INSENSITIVE_HOSTS.includes(url.hostname) ? path.toLowerCase() : path;
|
| 16 |
+
return { identity: url.origin + identityPath, label: url.host + path };
|
| 17 |
+
} catch { return null; }
|
| 18 |
+
}
|
前端页面/market/资源安装引擎.js
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
前端页面/styles/主题基础.css
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|