您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
可以一键执行一系列操作。
// ==UserScript== // @name 【哔哩哔哩】一些任务 // @namespace https://github.com/AkagiYui/UserScript // @version 0.0.7 // @author AkagiYui // @description 可以一键执行一系列操作。 // @license MIT // @icon https://static.hdslb.com/images/favicon.ico // @homepage https://github.com/AkagiYui // @supportURL https://github.com/AkagiYui/UserScript/issues // @match https://space.bilibili.com/*/favlist* // @match https://www.bilibili.com/list/ml* // @match https://www.bilibili.com/list/watchlater* // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/preact.min.js // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_xmlhttpRequest // ==/UserScript== (e=>{if(typeof GM_addStyle=="function"){GM_addStyle(e);return}const t=document.createElement("style");t.textContent=e,document.head.append(t)})(' .floating-button{position:fixed;bottom:24px;right:24px;width:56px;height:56px;border-radius:50%;border:none;background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:0 4px 12px #00000026,0 2px 6px #0000001a;transition:all .3s cubic-bezier(.4,0,.2,1);z-index:9999;font-family:inherit;outline:none}.floating-button:hover{transform:translateY(-2px);box-shadow:0 6px 16px #0003,0 3px 8px #00000026;background:linear-gradient(135deg,#5a6fd8,#6a4190)}.floating-button:active{transform:translateY(0);box-shadow:0 2px 8px #0003,0 1px 4px #0000001a}.floating-button:focus-visible{outline:2px solid #667eea;outline-offset:2px}.floating-button svg{width:24px;height:24px;transition:transform .2s ease}.floating-button:hover svg{transform:scale(1.1)}.floating-button *{box-sizing:border-box}@media (max-width: 768px){.floating-button{bottom:16px;right:16px;width:48px;height:48px}.floating-button svg{width:20px;height:20px}}.modal-backdrop{position:fixed;top:0;left:0;right:0;bottom:0;background-color:#0009;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);z-index:10000;display:flex;align-items:center;justify-content:center;animation:modal-fade-in .3s cubic-bezier(.4,0,.2,1)}.modal-backdrop--closing{animation:modal-fade-out .3s cubic-bezier(.4,0,.2,1)}.modal-content{position:relative;width:100%;height:100%;background:linear-gradient(135deg,#667eea,#764ba2);border-radius:0;box-shadow:0 20px 40px #0000004d;display:flex;flex-direction:column;animation:modal-slide-up .3s cubic-bezier(.4,0,.2,1);overflow:hidden}.modal-content--closing{animation:modal-slide-down .3s cubic-bezier(.4,0,.2,1)}.modal-header{position:absolute;top:20px;left:80px;right:20px;z-index:10001;display:flex;justify-content:space-between;align-items:center}.modal-header-info{display:flex;align-items:center;gap:12px;flex-shrink:1;min-width:0}.modal-title{font-size:1.4rem;font-weight:600;color:#fff;text-shadow:0 1px 2px rgba(0,0,0,.5);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.modal-repo-link{font-size:.9rem;color:#87ceeb;text-decoration:none;transition:all .2s ease;text-shadow:0 1px 2px rgba(0,0,0,.5);white-space:nowrap;flex-shrink:0}.modal-repo-link:hover{color:#fff;text-shadow:0 1px 2px rgba(0,0,0,.5),0 0 8px rgba(135,206,235,.6);text-decoration:underline}.modal-close-button{width:48px;height:48px;border-radius:50%;border:none;background:#fff3;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s ease;outline:none;padding:0;box-sizing:border-box}.modal-close-button:hover{background:#ffffff4d;transform:translateY(2px)}.modal-close-button:active{transform:translateY(0);background:#fff6}.modal-close-button:focus-visible{outline:2px solid rgba(255,255,255,.8);outline-offset:2px}.modal-close-button svg{width:24px;height:24px;transition:transform .2s ease;display:block;flex-shrink:0}.modal-body{flex:1;padding:80px 40px 40px;overflow-y:auto;color:#fff}@keyframes modal-fade-in{0%{opacity:0}to{opacity:1}}@keyframes modal-fade-out{0%{opacity:1}to{opacity:0}}@keyframes modal-slide-up{0%{transform:translateY(100%)}to{transform:translateY(0)}}@keyframes modal-slide-down{0%{transform:translateY(0)}to{transform:translateY(100%)}}.modal-backdrop *,.modal-content *,.modal-header *,.modal-body *{box-sizing:border-box;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif}@media (max-width: 768px){.modal-header{top:16px;left:20px;right:16px}.modal-header-info{gap:8px}.modal-title{font-size:1.2rem}.modal-repo-link{font-size:.85rem}.modal-close-button{width:40px;height:40px}.modal-close-button svg{width:20px;height:20px}.modal-body{padding:60px 20px 20px}}@media (max-width: 480px){.modal-header-info{gap:6px}.modal-title{font-size:.9rem}.modal-repo-link{font-size:.8rem}}.script-card{background:#0006;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);border-radius:12px;border:1px solid rgba(255,255,255,.3);margin-bottom:16px;transition:all .3s ease;overflow:hidden}.script-card:hover{background:#00000080;border-color:#fff6;transform:translateY(-2px)}.script-card.running{border-color:#4caf50;box-shadow:0 0 20px #4caf504d}.script-header{padding:20px;cursor:pointer;display:flex;justify-content:space-between;align-items:center;-webkit-user-select:none;user-select:none}.script-info{flex:1;min-width:0}.script-name{font-size:1.2rem;font-weight:600;margin:0 0 8px;color:#fff;text-shadow:0 1px 2px rgba(0,0,0,.5)}.script-description{font-size:.9rem;margin:0 0 12px;color:#fffffff2;line-height:1.4;text-shadow:0 1px 2px rgba(0,0,0,.3)}.script-category{display:inline-block;padding:4px 12px;border-radius:16px;font-size:.8rem;font-weight:500;text-transform:uppercase;letter-spacing:.5px}.script-category.tool{background:#2196f333;color:#2196f3;border:1px solid rgba(33,150,243,.3)}.script-category.operation{background:#ff980033;color:#ff9800;border:1px solid rgba(255,152,0,.3)}.script-controls{display:flex;align-items:center;gap:12px;margin-left:20px}.progress-container{display:flex;align-items:center;gap:8px;min-width:120px}.progress-bar{width:80px;height:6px;background:#fff3;border-radius:3px;overflow:hidden}.progress-fill{height:100%;background:linear-gradient(90deg,#4caf50,#8bc34a);border-radius:3px;transition:width .3s ease}.progress-text{font-size:.8rem;color:#fffc;font-weight:500;min-width:32px}.expand-button{width:32px;height:32px;border:none;background:#ffffff1a;border-radius:50%;color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s ease}.expand-button:hover{background:#fff3}.expand-button.expanded{transform:rotate(180deg)}.script-body{padding:0 20px 20px;border-top:1px solid rgba(255,255,255,.2);animation:slideDown .3s ease;background:#0003}@keyframes slideDown{0%{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:translateY(0)}}.script-parameters{margin-top:10px;padding-top:16px}.parameter-group{margin-bottom:20px}.parameter-label{display:block;font-size:.9rem;font-weight:500;margin-bottom:8px;color:#fffffff2;text-shadow:0 1px 2px rgba(0,0,0,.3)}.parameter-label .required{color:#f44336;margin-left:4px}.script-input,.script-select,.script-textarea{width:100%;padding:12px;border:1px solid rgba(255,255,255,.3);border-radius:8px;background:#0000004d;color:#fff;font-size:.9rem;transition:all .2s ease;text-shadow:0 1px 2px rgba(0,0,0,.3)}.script-select{background:#00000080;color:#fff;border:1px solid rgba(255,255,255,.4)}.script-select option{background:#2a2a2a;color:#fff;padding:8px 12px;border:none;font-size:.9rem}.script-select option:hover,.script-select option:focus{background:#3a3a3a;color:#fff}.script-select option:checked,.script-select option:selected{background:#4a4a4a;color:#fff;font-weight:500}.script-input:focus,.script-select:focus,.script-textarea:focus{outline:none;border-color:#2196f3;background:#ffffff26}.script-input::placeholder,.script-textarea::placeholder{color:#ffffff80}.script-input:disabled,.script-select:disabled,.script-textarea:disabled{opacity:.6;cursor:not-allowed}.script-checkbox{display:flex;align-items:center;cursor:pointer;position:relative}.script-checkbox input[type=checkbox]{opacity:0;position:absolute;width:0;height:0}.checkmark{width:20px;height:20px;border:2px solid rgba(255,255,255,.3);border-radius:4px;background:#ffffff1a;position:relative;transition:all .2s ease}.script-checkbox input[type=checkbox]:checked+.checkmark{background:#2196f3;border-color:#2196f3}.script-checkbox input[type=checkbox]:checked+.checkmark:after{content:"";position:absolute;left:6px;top:2px;width:6px;height:10px;border:solid white;border-width:0 2px 2px 0;transform:rotate(45deg)}.parameter-description{font-size:.8rem;color:#fff9;margin:8px 0 0;line-height:1.4}.favorite-selector{position:relative}.favorite-input-wrapper{position:relative;display:flex;align-items:center}.favorite-input{width:100%;padding-right:36px}.favorite-clear-button{position:absolute;right:8px;top:50%;transform:translateY(-50%);width:24px;height:24px;border:none;background:#ffffff1a;color:#fff9;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s ease;z-index:1}.favorite-clear-button:hover{background:#fff3;color:#ffffffe6;transform:translateY(-50%) scale(1.1)}.favorite-clear-button:active{transform:translateY(-50%) scale(.95);background:#ffffff4d}.favorite-clear-button svg{width:14px;height:14px;display:block}.input-wrapper{position:relative;display:flex;align-items:center}.input-wrapper .script-input{width:100%;padding-right:36px}.textarea-wrapper{position:relative}.textarea-wrapper .script-textarea{width:100%;padding-right:36px}.script-card .clear-button{position:absolute;right:8px;top:50%;transform:translateY(-50%);width:24px;height:24px;border:none;background:#ffffff26;color:#fffc;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s ease;z-index:2;box-sizing:border-box}.script-card .clear-button:hover{background:#ffffff40;color:#fff;transform:translateY(-50%) scale(1.1)}.script-card .clear-button:active{transform:translateY(-50%) scale(.95);background:#ffffff59}.script-card .clear-button svg{width:16px;height:16px;display:block;flex-shrink:0;pointer-events:none}.script-card .clear-button svg path{stroke:currentColor}.script-card .clear-button:before{content:"\xD7";font-size:16px;font-weight:700;line-height:1;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);display:none}.script-card .clear-button:not(:has(svg)){font-size:16px;font-weight:700;line-height:1}.script-card .clear-button:not(:has(svg)):before{display:block}.script-card .clear-button-textarea{top:12px;transform:none}.script-card .clear-button-textarea:hover{transform:scale(1.1)}.script-card .clear-button-textarea:active{transform:scale(.95)}.script-card .clear-button-textarea svg{width:16px;height:16px;display:block;flex-shrink:0;pointer-events:none}.favorite-loading,.favorite-error{margin-top:8px;padding:8px 12px;border-radius:6px;font-size:.8rem;line-height:1.4}.favorite-loading{background:#2196f333;color:#2196f3;border:1px solid rgba(33,150,243,.3)}.favorite-error{background:#f4433633;color:#f44336;border:1px solid rgba(244,67,54,.3);display:flex;align-items:center;justify-content:space-between;gap:8px}.retry-button{padding:4px 8px;border:none;border-radius:4px;background:#f443364d;color:#f44336;font-size:.7rem;cursor:pointer;transition:all .2s ease;flex-shrink:0}.retry-button:hover{background:#f4433666;transform:translateY(-1px)}.retry-button:disabled{opacity:.6;cursor:not-allowed;transform:none}.script-actions{margin-top:24px;display:flex;justify-content:flex-end}.execute-button,.stop-button{padding:12px 24px;border:none;border-radius:8px;font-size:.9rem;font-weight:600;cursor:pointer;display:flex;align-items:center;gap:8px;transition:all .2s ease}.execute-button{background:linear-gradient(135deg,#4caf50,#45a049);color:#fff}.execute-button:hover{background:linear-gradient(135deg,#45a049,#3d8b40);transform:translateY(-1px)}.stop-button{background:linear-gradient(135deg,#f44336,#d32f2f);color:#fff}.stop-button:hover{background:linear-gradient(135deg,#d32f2f,#b71c1c);transform:translateY(-1px)}@media (max-width: 768px){.script-header{padding:16px;flex-direction:column;align-items:flex-start;gap:12px}.script-controls{margin-left:0;width:100%;justify-content:space-between}.script-body{padding:0 16px 16px}.progress-container{min-width:auto;flex:1}.progress-bar{flex:1;min-width:60px}}.log-panel{height:100%;width:100%;display:flex;flex-direction:column;background:#00000080;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);border-radius:12px;border:1px solid rgba(255,255,255,.3);overflow:hidden;box-sizing:border-box}.log-header{padding:20px;border-bottom:1px solid rgba(255,255,255,.2);display:flex;justify-content:space-between;align-items:center;background:#0000004d}.log-header h3{font-size:1.2rem;font-weight:600;margin:0;color:#fff;text-shadow:0 1px 2px rgba(0,0,0,.5)}.log-controls{display:flex;align-items:center;gap:12px}.log-count{font-size:.9rem;color:#ffffffb3;padding:4px 8px;background:#ffffff1a;border-radius:12px}.log-clear-button{padding:8px 12px;border:none;border-radius:6px;background:#f4433633;color:#f44336;font-size:.8rem;cursor:pointer;display:flex;align-items:center;gap:6px;transition:all .2s ease;border:1px solid rgba(244,67,54,.3)}.log-clear-button:hover{background:#f443364d;transform:translateY(-1px)}.log-container{flex:1;overflow-y:auto;padding:0}.log-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:#fff9;text-align:center;padding:40px 20px}.empty-icon{font-size:3rem;margin-bottom:16px;opacity:.7}.log-empty p{font-size:1.1rem;font-weight:500;margin:0 0 8px;color:#fffc}.log-empty span{font-size:.9rem;color:#ffffff80}.log-list{padding:12px 0}.log-entry{padding:12px 20px;border-left:3px solid transparent;transition:all .2s ease;animation:slideIn .3s ease}@keyframes slideIn{0%{opacity:0;transform:translate(-10px)}to{opacity:1;transform:translate(0)}}.log-entry:hover{background:#ffffff0d}.log-entry.info{border-left-color:#2196f3}.log-entry.warn{border-left-color:#ff9800}.log-entry.error{border-left-color:#f44336}.log-entry.success{border-left-color:#4caf50}.log-entry.debug{border-left-color:#9c27b0}.log-meta{display:flex;align-items:center;gap:8px;margin-bottom:4px;font-size:.8rem}.log-icon{font-size:1rem;line-height:1}.log-time{color:#fff9;font-family:Courier New,monospace;font-weight:500}.log-script{color:#ffffff80;background:#ffffff1a;padding:2px 6px;border-radius:4px;font-size:.7rem;font-weight:500}.log-message{color:#fffffff2;font-size:.9rem;line-height:1.4;word-break:break-word;margin-left:24px;text-shadow:0 1px 2px rgba(0,0,0,.3);white-space:pre-line}.log-entry.error .log-message{color:#ffcdd2}.log-entry.warn .log-message{color:#ffe0b2}.log-entry.success .log-message{color:#c8e6c9}.log-entry.debug .log-message{color:#e1bee7}.log-footer{padding:16px 20px;border-top:1px solid rgba(255,255,255,.1);background:#ffffff0d}.log-filters{display:flex;justify-content:center;flex-wrap:wrap}.filter-buttons{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.filter-button{display:flex;align-items:center;gap:4px;padding:6px 10px;border:2px solid transparent;border-radius:4px;background:#ffffff1a;color:#fff9;font-size:.8rem;cursor:pointer;transition:all .2s ease;-webkit-user-select:none;user-select:none}.filter-button:hover{background:#ffffff26;color:#fffc}.filter-button.active{color:#fff;background:#ffffff1a}.filter-button.info.active{border-color:#2196f3;background:#2196f333}.filter-button.success.active{border-color:#4caf50;background:#4caf5033}.filter-button.warn.active{border-color:#ff9800;background:#ff980033}.filter-button.error.active{border-color:#f44336;background:#f4433633}.filter-button.debug.active{border-color:#9c27b0;background:#9c27b033}.filter-icon{font-size:.9rem}.log-container::-webkit-scrollbar{width:6px}.log-container::-webkit-scrollbar-track{background:#ffffff1a}.log-container::-webkit-scrollbar-thumb{background:#ffffff4d;border-radius:3px}.log-container::-webkit-scrollbar-thumb:hover{background:#ffffff80}@media (max-width: 768px){.log-header{padding:16px;flex-direction:column;align-items:flex-start;gap:12px}.log-controls{width:100%;justify-content:space-between}.log-entry{padding:10px 16px}.log-message{margin-left:20px;font-size:.85rem}.log-filters{flex-direction:column;align-items:flex-start;gap:8px}.filter-buttons{gap:6px}.filter-button{font-size:.75rem;padding:4px 8px}.log-footer{padding:12px 16px}}.resize-handle{width:8px;height:100%;cursor:col-resize;position:relative;display:flex;align-items:center;justify-content:center;background:transparent;transition:all .2s ease;-webkit-user-select:none;user-select:none;flex-shrink:0}.resize-handle:hover{background:#ffffff1a}.resize-handle--dragging{background:#fff3}.resize-handle-line{position:absolute;left:50%;top:0;bottom:0;width:1px;background:#fff3;transform:translate(-50%);transition:all .2s ease}.resize-handle:hover .resize-handle-line{background:#fff6;width:2px}.resize-handle--dragging .resize-handle-line{background:#fff9;width:2px}.resize-handle-grip{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:4px;height:24px;display:flex;flex-direction:column;justify-content:space-between;align-items:center;opacity:0;transition:opacity .2s ease}.resize-handle:hover .resize-handle-grip,.resize-handle--dragging .resize-handle-grip{opacity:1}.resize-handle-dot{width:2px;height:2px;background:#fff9;border-radius:50%;flex-shrink:0}.resize-handle:hover .resize-handle-dot{background:#fffc}.resize-handle--dragging .resize-handle-dot{background:#fff}body.resizing,body.resizing *{cursor:col-resize!important;-webkit-user-select:none!important;user-select:none!important}@media (max-width: 1024px){.resize-handle{display:none}}@media (prefers-contrast: high){.resize-handle-line{background:#fffc}.resize-handle:hover .resize-handle-line{background:#fff}.resize-handle-dot{background:#ffffffe6}}@media (prefers-reduced-motion: reduce){.resize-handle,.resize-handle-line,.resize-handle-grip{transition:none}}.script-manager{width:100%;height:100%;display:flex;flex-direction:column;color:#fff;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif}.script-manager-header{padding:20px 24px 24px;text-align:center;background:#ffffff0d;border-bottom:1px solid rgba(255,255,255,.1);margin-top:0}.script-manager-header h1{font-size:1.8rem;font-weight:700;margin:0 0 8px;color:#fff;text-shadow:0 2px 4px rgba(0,0,0,.8),0 1px 2px rgba(0,0,0,.6),0 0 8px rgba(102,126,234,.3);line-height:1.2}.script-manager-header p{font-size:1rem;margin:0 0 20px;color:#ffffffe6;text-shadow:0 1px 2px rgba(0,0,0,.5)}.script-manager-header p a{color:#87ceeb;text-decoration:none;transition:all .2s ease;text-shadow:0 1px 2px rgba(0,0,0,.5)}.script-manager-header p a:hover{color:#fff;text-shadow:0 1px 2px rgba(0,0,0,.5),0 0 8px rgba(135,206,235,.6);text-decoration:underline}.status-bar{display:flex;justify-content:center;gap:24px;flex-wrap:wrap}.status-item{display:flex;align-items:center;gap:8px;padding:8px 16px;background:#ffffff1a;border-radius:20px;border:1px solid rgba(255,255,255,.2)}.status-label{font-size:.9rem;color:#ffffffb3}.status-value{font-size:.9rem;font-weight:600;color:#fff;background:#fff3;padding:2px 8px;border-radius:12px;min-width:24px;text-align:center}.status-value.running{background:linear-gradient(135deg,#4caf50,#45a049);color:#fff;animation:pulse 2s infinite}@keyframes pulse{0%,to{opacity:1}50%{opacity:.7}}.script-manager-content{flex:1;display:flex;gap:24px;padding:24px;min-height:0}.scripts-panel{flex-shrink:0;overflow-y:auto;padding-right:12px;box-sizing:border-box}.scripts-section{margin-bottom:32px}.scripts-section:last-child{margin-bottom:0}.scripts-section h2{font-size:1.3rem;font-weight:600;margin:0 0 20px;color:#fff;display:flex;align-items:center;gap:8px;padding-bottom:12px;border-bottom:2px solid rgba(255,255,255,.1)}.scripts-list{display:flex;flex-direction:column;gap:16px}.logs-panel{flex-shrink:0;display:flex;flex-direction:column;box-sizing:border-box}.scripts-panel::-webkit-scrollbar{width:8px}.scripts-panel::-webkit-scrollbar-track{background:#ffffff1a;border-radius:4px}.scripts-panel::-webkit-scrollbar-thumb{background:#ffffff4d;border-radius:4px}.scripts-panel::-webkit-scrollbar-thumb:hover{background:#ffffff80}@media (max-width: 1200px){.scripts-panel{width:400px!important;min-width:350px}.logs-panel{width:auto!important;flex:1;min-width:450px}}@media (max-width: 1024px){.script-manager-content{flex-direction:column;gap:20px}.scripts-panel{width:100%!important;min-width:auto;max-width:none;padding-right:0}.logs-panel{width:100%!important;min-width:auto;height:400px;min-height:300px}}@media (max-width: 768px){.script-manager-content{padding:16px;gap:16px}.script-manager-header{padding:20px 16px}.script-manager-header h1{font-size:1.6rem}.status-bar{gap:16px}.status-item{padding:6px 12px}.scripts-section h2{font-size:1.2rem}.logs-panel{height:350px;min-height:250px}}@media (max-width: 480px){.script-manager-header h1{font-size:1.4rem}.script-manager-header p{font-size:.9rem}.status-bar{flex-direction:column;align-items:center;gap:12px}.status-item{width:100%;max-width:200px;justify-content:space-between}.logs-panel{height:300px;min-height:200px}}.script-manager.loading{opacity:.7;pointer-events:none}.script-manager.loading:after{content:"";position:absolute;top:50%;left:50%;width:40px;height:40px;margin:-20px 0 0 -20px;border:3px solid rgba(255,255,255,.3);border-top:3px solid white;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.scripts-list:empty:after{content:"\u6682\u65E0\u811A\u672C";display:block;text-align:center;color:#ffffff80;font-style:italic;padding:40px 20px;background:#ffffff0d;border-radius:8px;border:1px dashed rgba(255,255,255,.2)}.task-panel{width:100%;height:100%;display:flex;flex-direction:column;overflow:hidden}.task-panel-header{text-align:center;margin-bottom:40px}.task-panel-header h1{font-size:2.5rem;font-weight:700;margin:0 0 16px;color:#fff;text-shadow:0 2px 4px rgba(0,0,0,.3)}.task-panel-header p{font-size:1.1rem;margin:0;color:#fffc;font-weight:400}.task-panel-content{flex:1;display:flex;flex-direction:column;gap:40px}.counter-section{background:#ffffff1a;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);border-radius:16px;padding:32px;text-align:center;border:1px solid rgba(255,255,255,.2)}.counter-section h2{font-size:1.5rem;font-weight:600;margin:0 0 24px;color:#fff}.counter-display{margin-bottom:24px}.counter-value{font-size:4rem;font-weight:700;color:#fff;text-shadow:0 2px 8px rgba(0,0,0,.3);display:inline-block;min-width:120px}.counter-controls{display:flex;gap:16px;justify-content:center;align-items:center}.counter-button{width:48px;height:48px;border-radius:50%;border:none;font-size:1.2rem;font-weight:600;cursor:pointer;transition:all .2s ease;display:flex;align-items:center;justify-content:center;outline:none}.counter-button--decrease{background:#ff6b6be6;color:#fff}.counter-button--decrease:hover{background:#ff6b6b;transform:scale(1.05)}.counter-button--reset{background:#fff3;color:#fff;width:auto;padding:0 20px;border-radius:24px;font-size:.9rem}.counter-button--reset:hover{background:#ffffff4d;transform:scale(1.05)}.counter-button--increase{background:#6bff6be6;color:#fff}.counter-button--increase:hover{background:#6bff6b;transform:scale(1.05)}.counter-button:active{transform:scale(.95)}.counter-button:focus-visible{outline:2px solid rgba(255,255,255,.8);outline-offset:2px}.placeholder-section{background:#ffffff1a;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);border-radius:16px;padding:32px;border:1px solid rgba(255,255,255,.2)}.placeholder-section h2{font-size:1.5rem;font-weight:600;margin:0 0 24px;color:#fff;text-align:center}.feature-list{display:flex;flex-direction:column;gap:20px}.feature-item{display:flex;align-items:center;gap:16px;padding:16px;background:#ffffff0d;border-radius:12px;border:1px solid rgba(255,255,255,.1);transition:all .2s ease}.feature-item:hover{background:#ffffff1a;transform:translateY(-2px)}.feature-icon{font-size:2rem;width:48px;height:48px;display:flex;align-items:center;justify-content:center;background:#ffffff1a;border-radius:12px;flex-shrink:0}.feature-text h3{font-size:1.1rem;font-weight:600;margin:0 0 4px;color:#fff}.feature-text p{font-size:.9rem;margin:0;color:#ffffffb3}@media (max-width: 768px){.task-panel-header h1{font-size:2rem}.task-panel-content{gap:24px}.counter-section,.placeholder-section{padding:24px}.counter-value{font-size:3rem}.counter-controls{gap:12px}.counter-button{width:40px;height:40px;font-size:1rem}.feature-list{gap:16px}.feature-item{padding:12px}.feature-icon{font-size:1.5rem;width:40px;height:40px}} '); (function (preact) { 'use strict'; var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); var f$1 = 0; function u$1(e2, t2, n, o2, i2, u2) { t2 || (t2 = {}); var a2, c2, p2 = t2; if ("ref" in p2) for (c2 in p2 = {}, t2) "ref" == c2 ? a2 = t2[c2] : p2[c2] = t2[c2]; var l2 = { type: e2, props: p2, key: n, ref: a2, __k: null, __: null, __b: 0, __e: null, __c: null, constructor: void 0, __v: --f$1, __i: -1, __u: 0, __source: i2, __self: u2 }; if ("function" == typeof e2 && (a2 = e2.defaultProps)) for (c2 in a2) void 0 === p2[c2] && (p2[c2] = a2[c2]); return preact.options.vnode && preact.options.vnode(l2), l2; } var t, r, u, i, o = 0, f = [], c = preact.options, e = c.__b, a = c.__r, v = c.diffed, l = c.__c, m = c.unmount, s = c.__; function p(n, t2) { c.__h && c.__h(r, n, o || t2), o = 0; var u2 = r.__H || (r.__H = { __: [], __h: [] }); return n >= u2.__.length && u2.__.push({}), u2.__[n]; } function d(n) { return o = 1, h(D, n); } function h(n, u2, i2) { var o2 = p(t++, 2); if (o2.t = n, !o2.__c && (o2.__ = [D(void 0, u2), function(n2) { var t2 = o2.__N ? o2.__N[0] : o2.__[0], r2 = o2.t(t2, n2); t2 !== r2 && (o2.__N = [r2, o2.__[1]], o2.__c.setState({})); }], o2.__c = r, !r.__f)) { var f2 = function(n2, t2, r2) { if (!o2.__c.__H) return true; var u3 = o2.__c.__H.__.filter(function(n3) { return !!n3.__c; }); if (u3.every(function(n3) { return !n3.__N; })) return !c2 || c2.call(this, n2, t2, r2); var i3 = o2.__c.props !== n2; return u3.forEach(function(n3) { if (n3.__N) { var t3 = n3.__[0]; n3.__ = n3.__N, n3.__N = void 0, t3 !== n3.__[0] && (i3 = true); } }), c2 && c2.call(this, n2, t2, r2) || i3; }; r.__f = true; var c2 = r.shouldComponentUpdate, e2 = r.componentWillUpdate; r.componentWillUpdate = function(n2, t2, r2) { if (this.__e) { var u3 = c2; c2 = void 0, f2(n2, t2, r2), c2 = u3; } e2 && e2.call(this, n2, t2, r2); }, r.shouldComponentUpdate = f2; } return o2.__N || o2.__; } function y(n, u2) { var i2 = p(t++, 3); !c.__s && C(i2.__H, u2) && (i2.__ = n, i2.u = u2, r.__H.__h.push(i2)); } function A(n) { return o = 5, T(function() { return { current: n }; }, []); } function T(n, r2) { var u2 = p(t++, 7); return C(u2.__H, r2) && (u2.__ = n(), u2.__H = r2, u2.__h = n), u2.__; } function j() { for (var n; n = f.shift(); ) if (n.__P && n.__H) try { n.__H.__h.forEach(z), n.__H.__h.forEach(B), n.__H.__h = []; } catch (t2) { n.__H.__h = [], c.__e(t2, n.__v); } } c.__b = function(n) { r = null, e && e(n); }, c.__ = function(n, t2) { n && t2.__k && t2.__k.__m && (n.__m = t2.__k.__m), s && s(n, t2); }, c.__r = function(n) { a && a(n), t = 0; var i2 = (r = n.__c).__H; i2 && (u === r ? (i2.__h = [], r.__h = [], i2.__.forEach(function(n2) { n2.__N && (n2.__ = n2.__N), n2.u = n2.__N = void 0; })) : (i2.__h.forEach(z), i2.__h.forEach(B), i2.__h = [], t = 0)), u = r; }, c.diffed = function(n) { v && v(n); var t2 = n.__c; t2 && t2.__H && (t2.__H.__h.length && (1 !== f.push(t2) && i === c.requestAnimationFrame || ((i = c.requestAnimationFrame) || w)(j)), t2.__H.__.forEach(function(n2) { n2.u && (n2.__H = n2.u), n2.u = void 0; })), u = r = null; }, c.__c = function(n, t2) { t2.some(function(n2) { try { n2.__h.forEach(z), n2.__h = n2.__h.filter(function(n3) { return !n3.__ || B(n3); }); } catch (r2) { t2.some(function(n3) { n3.__h && (n3.__h = []); }), t2 = [], c.__e(r2, n2.__v); } }), l && l(n, t2); }, c.unmount = function(n) { m && m(n); var t2, r2 = n.__c; r2 && r2.__H && (r2.__H.__.forEach(function(n2) { try { z(n2); } catch (n3) { t2 = n3; } }), r2.__H = void 0, t2 && c.__e(t2, r2.__v)); }; var k = "function" == typeof requestAnimationFrame; function w(n) { var t2, r2 = function() { clearTimeout(u2), k && cancelAnimationFrame(t2), setTimeout(n); }, u2 = setTimeout(r2, 35); k && (t2 = requestAnimationFrame(r2)); } function z(n) { var t2 = r, u2 = n.__c; "function" == typeof u2 && (n.__c = void 0, u2()), r = t2; } function B(n) { var t2 = r; n.__c = n.__(), r = t2; } function C(n, t2) { return !n || n.length !== t2.length || t2.some(function(t3, r2) { return t3 !== n[r2]; }); } function D(n, t2) { return "function" == typeof t2 ? t2(n) : t2; } function FloatingButton({ onClick }) { return /* @__PURE__ */ u$1( "button", { class: "floating-button", onClick, title: "打开任务面板", "aria-label": "打开任务面板", children: /* @__PURE__ */ u$1( "svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [ /* @__PURE__ */ u$1( "path", { d: "M12 2L2 7L12 12L22 7L12 2Z", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" } ), /* @__PURE__ */ u$1( "path", { d: "M2 17L12 22L22 17", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" } ), /* @__PURE__ */ u$1( "path", { d: "M2 12L12 17L22 12", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" } ) ] } ) } ); } function Modal({ isOpen, onClose, children }) { const [isClosing, setIsClosing] = d(false); y(() => { if (isOpen) { const originalOverflow = document.body.style.overflow; const originalPaddingRight = document.body.style.paddingRight; const scrollBarWidth = window.innerWidth - document.documentElement.clientWidth; document.body.style.overflow = "hidden"; if (scrollBarWidth > 0) { document.body.style.paddingRight = `${scrollBarWidth}px`; } return () => { document.body.style.overflow = originalOverflow; document.body.style.paddingRight = originalPaddingRight; }; } }, [isOpen]); const handleClose = () => { setIsClosing(true); setTimeout(() => { setIsClosing(false); onClose(); }, 300); }; const handleBackdropClick = (e2) => { if (e2.target === e2.currentTarget) { handleClose(); } }; if (!isOpen && !isClosing) { return null; } return /* @__PURE__ */ u$1( "div", { class: `modal-backdrop ${isClosing ? "modal-backdrop--closing" : ""}`, onClick: handleBackdropClick, children: /* @__PURE__ */ u$1("div", { class: `modal-content ${isClosing ? "modal-content--closing" : ""}`, children: [ /* @__PURE__ */ u$1("div", { class: "modal-header", children: [ /* @__PURE__ */ u$1("div", { class: "modal-header-info", children: [ /* @__PURE__ */ u$1("span", { class: "modal-title", children: "【哔哩哔哩】一些任务" }), /* @__PURE__ */ u$1( "a", { href: "https://github.com/AkagiYui", target: "_blank", rel: "noopener noreferrer", class: "modal-repo-link", title: "访问作者仓库", children: "AkagiYui" } ) ] }), /* @__PURE__ */ u$1( "button", { class: "modal-close-button", onClick: handleClose, title: "关闭面板", "aria-label": "关闭面板", children: /* @__PURE__ */ u$1( "svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: /* @__PURE__ */ u$1( "path", { d: "M19 9L12 16L5 9", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" } ) } ) } ) ] }), /* @__PURE__ */ u$1("div", { class: "modal-body", children }) ] }) } ); } var _GM_getValue = /* @__PURE__ */ (() => typeof GM_getValue != "undefined" ? GM_getValue : void 0)(); var _GM_setValue = /* @__PURE__ */ (() => typeof GM_setValue != "undefined" ? GM_setValue : void 0)(); var _GM_xmlhttpRequest = /* @__PURE__ */ (() => typeof GM_xmlhttpRequest != "undefined" ? GM_xmlhttpRequest : void 0)(); function ScriptCard({ script, onExecute, onStop, isRunning, progress = 0, favoriteList, favoriteListLoading = false, favoriteListError, onRetryFavoriteList }) { const [parameters, setParameters] = d({}); const [isExpanded, setIsExpanded] = d(false); const [focusedFavoriteInputs, setFocusedFavoriteInputs] = d(/* @__PURE__ */ new Set()); y(() => { const storageKey = `bili_tasks_params_${script.id}`; const savedParams = _GM_getValue(storageKey, "{}"); try { const parsedParams = JSON.parse(savedParams); const initialParams = {}; script.parameters.forEach((param) => { initialParams[param.key] = parsedParams[param.key] !== void 0 ? parsedParams[param.key] : param.defaultValue; }); setParameters(initialParams); } catch (error) { console.warn("Failed to load saved parameters:", error); const initialParams = {}; script.parameters.forEach((param) => { initialParams[param.key] = param.defaultValue; }); setParameters(initialParams); } }, [script.id, script.parameters]); y(() => { if (Object.keys(parameters).length > 0) { const storageKey = `bili_tasks_params_${script.id}`; _GM_setValue(storageKey, JSON.stringify(parameters)); } }, [parameters, script.id]); const handleParameterChange = (key, value) => { setParameters((prev) => ({ ...prev, [key]: value })); }; const handleExecute = () => { const missingParams = script.parameters.filter((param) => param.required && !parameters[param.key]).map((param) => param.label); if (missingParams.length > 0) { alert(`请填写必填参数: ${missingParams.join(", ")}`); return; } onExecute(script.id, parameters); }; const handleStop = () => { onStop(script.id); }; const isFavoriteIdParameter = (param) => { return param.type === "number" && (param.label.includes("收藏夹ID") || param.key.toLowerCase().includes("favorite")); }; const getFavoriteOptions = () => { if (!(favoriteList == null ? void 0 : favoriteList.list)) return []; return favoriteList.list.map((fav) => ({ value: `${fav.title}(${fav.id})`, // value设置为显示格式 id: fav.id, // 保留原始ID用于提取 title: fav.title })).sort((a2, b) => a2.value.localeCompare(b.value)); }; const getFavoriteTitleById = (id) => { if (!(favoriteList == null ? void 0 : favoriteList.list)) return null; const favorite = favoriteList.list.find((fav) => fav.id === id); return favorite ? favorite.title : null; }; const formatDisplayValue = (value) => { if (!value) return ""; const title = getFavoriteTitleById(value); return title ? `${title}(${value})` : value.toString(); }; const extractIdFromFormattedValue = (formattedValue) => { const directNumber = parseInt(formattedValue); if (!isNaN(directNumber) && directNumber.toString() === formattedValue) { return directNumber; } const match = formattedValue.match(/\((\d+)\)$/); if (match) { return parseInt(match[1]); } return null; }; const handleFavoriteInputFocus = (paramKey) => { setFocusedFavoriteInputs((prev) => new Set(prev).add(paramKey)); }; const handleFavoriteInputBlur = (paramKey) => { setFocusedFavoriteInputs((prev) => { const newSet = new Set(prev); newSet.delete(paramKey); return newSet; }); }; const getFavoriteInputDisplayValue = (paramKey, value) => { const isFocused = focusedFavoriteInputs.has(paramKey); if (isFocused || value === void 0 || value === null) { return value !== void 0 && value !== null ? value.toString() : ""; } return formatDisplayValue(value); }; const renderClearButton = (paramKey, hasValue, className = "") => { if (!hasValue || isRunning) return null; return /* @__PURE__ */ u$1( "button", { type: "button", class: `clear-button ${className}`, onClick: () => handleParameterChange(paramKey, ""), title: "清空内容", "aria-label": "清空内容", children: /* @__PURE__ */ u$1("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: /* @__PURE__ */ u$1( "path", { d: "M12 4L4 12M4 4L12 12", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round", "stroke-linejoin": "round" } ) }) } ); }; const renderParameterInput = (param) => { var _a; const value = parameters[param.key]; switch (param.type) { case "text": return /* @__PURE__ */ u$1("div", { class: "input-wrapper", children: [ /* @__PURE__ */ u$1( "input", { type: "text", value: value !== void 0 && value !== null ? value.toString() : "", onChange: (e2) => handleParameterChange(param.key, e2.target.value), placeholder: param.placeholder, disabled: isRunning, class: "script-input" } ), renderClearButton(param.key, value !== void 0 && value !== null && value !== "" && value.toString().trim() !== "") ] }); case "number": if (isFavoriteIdParameter(param)) { const favoriteOptions = getFavoriteOptions(); const displayValue = getFavoriteInputDisplayValue(param.key, value); return /* @__PURE__ */ u$1("div", { class: "favorite-selector", children: [ /* @__PURE__ */ u$1("div", { class: "favorite-input-wrapper", children: [ /* @__PURE__ */ u$1( "input", { type: "text", value: displayValue, onChange: (e2) => { const inputValue = e2.target.value; const extractedId = extractIdFromFormattedValue(inputValue); if (extractedId !== null) { handleParameterChange(param.key, extractedId); } }, onFocus: () => handleFavoriteInputFocus(param.key), onBlur: () => handleFavoriteInputBlur(param.key), placeholder: param.placeholder || "输入收藏夹ID或从下拉列表选择", disabled: isRunning, class: "script-input favorite-input", list: `favorites-${script.id}-${param.key}` } ), renderClearButton(param.key, value !== void 0 && value !== null && value !== "", "clear-button-favorite") ] }), /* @__PURE__ */ u$1("datalist", { id: `favorites-${script.id}-${param.key}`, children: favoriteOptions.map((option) => /* @__PURE__ */ u$1("option", { value: option.value }, option.id)) }), favoriteListLoading && /* @__PURE__ */ u$1("div", { class: "favorite-loading", children: /* @__PURE__ */ u$1("span", { children: "正在加载收藏夹列表..." }) }), favoriteListError && /* @__PURE__ */ u$1("div", { class: "favorite-error", children: [ /* @__PURE__ */ u$1("span", { children: [ "加载失败: ", favoriteListError ] }), onRetryFavoriteList && /* @__PURE__ */ u$1( "button", { type: "button", class: "retry-button", onClick: onRetryFavoriteList, disabled: isRunning, children: "重试" } ) ] }) ] }); } return /* @__PURE__ */ u$1("div", { class: "input-wrapper", children: [ /* @__PURE__ */ u$1( "input", { type: "number", value: value !== void 0 && value !== null ? value.toString() : "", onChange: (e2) => handleParameterChange(param.key, Number(e2.target.value)), placeholder: param.placeholder, disabled: isRunning, class: "script-input" } ), renderClearButton(param.key, value !== void 0 && value !== null && value !== "") ] }); case "boolean": return /* @__PURE__ */ u$1("label", { class: "script-checkbox", children: [ /* @__PURE__ */ u$1( "input", { type: "checkbox", checked: value === true, onChange: (e2) => handleParameterChange(param.key, e2.target.checked), disabled: isRunning } ), /* @__PURE__ */ u$1("span", { class: "checkmark" }) ] }); case "select": return /* @__PURE__ */ u$1( "select", { value: value || "", onChange: (e2) => handleParameterChange(param.key, e2.target.value), disabled: isRunning, class: "script-select", children: [ /* @__PURE__ */ u$1("option", { value: "", children: "请选择..." }), (_a = param.options) == null ? void 0 : _a.map((option) => /* @__PURE__ */ u$1("option", { value: option.value, children: option.label }, option.value)) ] } ); case "textarea": return /* @__PURE__ */ u$1("div", { class: "textarea-wrapper", children: [ /* @__PURE__ */ u$1( "textarea", { value: value !== void 0 && value !== null ? value.toString() : "", onChange: (e2) => handleParameterChange(param.key, e2.target.value), placeholder: param.placeholder, disabled: isRunning, class: "script-textarea", rows: 4 } ), renderClearButton(param.key, value !== void 0 && value !== null && value !== "" && value.toString().trim() !== "", "clear-button-textarea") ] }); default: return null; } }; return /* @__PURE__ */ u$1("div", { class: `script-card ${script.category} ${isRunning ? "running" : ""}`, children: [ /* @__PURE__ */ u$1("div", { class: "script-header", onClick: () => setIsExpanded(!isExpanded), children: [ /* @__PURE__ */ u$1("div", { class: "script-info", children: [ /* @__PURE__ */ u$1("h3", { class: "script-name", children: script.name }), /* @__PURE__ */ u$1("p", { class: "script-description", children: script.description }), /* @__PURE__ */ u$1("span", { class: `script-category ${script.category}`, children: script.category === "tool" ? "工具" : "操作" }) ] }), /* @__PURE__ */ u$1("div", { class: "script-controls", children: [ isRunning && /* @__PURE__ */ u$1("div", { class: "progress-container", children: [ /* @__PURE__ */ u$1("div", { class: "progress-bar", children: /* @__PURE__ */ u$1( "div", { class: "progress-fill", style: { width: `${progress}%` } } ) }), /* @__PURE__ */ u$1("span", { class: "progress-text", children: [ Math.round(progress), "%" ] }) ] }), /* @__PURE__ */ u$1( "button", { class: `expand-button ${isExpanded ? "expanded" : ""}`, type: "button", children: /* @__PURE__ */ u$1("svg", { width: "16", height: "16", viewBox: "0 0 16 16", children: /* @__PURE__ */ u$1("path", { d: "M4 6l4 4 4-4", stroke: "currentColor", "stroke-width": "2", fill: "none" }) }) } ) ] }) ] }), isExpanded && /* @__PURE__ */ u$1("div", { class: "script-body", children: [ script.parameters.length > 0 && /* @__PURE__ */ u$1("div", { class: "script-parameters", children: script.parameters.map((param) => /* @__PURE__ */ u$1("div", { class: "parameter-group", children: [ /* @__PURE__ */ u$1("label", { class: "parameter-label", children: [ param.label, param.required && /* @__PURE__ */ u$1("span", { class: "required", children: "*" }) ] }), renderParameterInput(param), param.description && /* @__PURE__ */ u$1("p", { class: "parameter-description", children: param.description }) ] }, param.key)) }), /* @__PURE__ */ u$1("div", { class: "script-actions", children: isRunning ? /* @__PURE__ */ u$1( "button", { class: "stop-button", onClick: handleStop, type: "button", children: [ /* @__PURE__ */ u$1("svg", { width: "16", height: "16", viewBox: "0 0 16 16", children: /* @__PURE__ */ u$1("rect", { x: "4", y: "4", width: "8", height: "8", fill: "currentColor" }) }), "停止执行" ] } ) : /* @__PURE__ */ u$1( "button", { class: "execute-button", onClick: handleExecute, type: "button", children: [ /* @__PURE__ */ u$1("svg", { width: "16", height: "16", viewBox: "0 0 16 16", children: /* @__PURE__ */ u$1("path", { d: "M5 3l8 5-8 5V3z", fill: "currentColor" }) }), "开始执行" ] } ) }) ] }) ] }); } function LogPanel({ logs, onClear }) { const logContainerRef = A(null); const [logLevelFilters, setLogLevelFilters] = d(() => { const saved = _GM_getValue("logLevelFilters", null); if (saved) { try { return JSON.parse(saved); } catch { } } return { info: true, success: true, warn: true, error: true, debug: false }; }); const filteredLogs = logs.filter((log) => logLevelFilters[log.level]); const toggleLogLevel = (level) => { const newFilters = { ...logLevelFilters, [level]: !logLevelFilters[level] }; setLogLevelFilters(newFilters); _GM_setValue("logLevelFilters", JSON.stringify(newFilters)); }; y(() => { if (logContainerRef.current) { logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight; } }, [filteredLogs]); const formatTime = (date) => { return date.toLocaleTimeString("zh-CN", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" }); }; const getLogIcon = (level) => { switch (level) { case "info": return "ℹ️"; case "warn": return "⚠️"; case "error": return "❌"; case "success": return "✅"; case "debug": return "🔍"; default: return "ℹ️"; } }; return /* @__PURE__ */ u$1("div", { class: "log-panel", children: [ /* @__PURE__ */ u$1("div", { class: "log-header", children: [ /* @__PURE__ */ u$1("h3", { children: "执行日志" }), /* @__PURE__ */ u$1("div", { class: "log-controls", children: [ /* @__PURE__ */ u$1("span", { class: "log-count", children: [ filteredLogs.length, "/", logs.length, " 条日志" ] }), /* @__PURE__ */ u$1( "button", { class: "log-clear-button", onClick: onClear, title: "清空日志", children: [ /* @__PURE__ */ u$1("svg", { width: "16", height: "16", viewBox: "0 0 16 16", children: /* @__PURE__ */ u$1( "path", { d: "M2 3h12M5.5 3V2a1 1 0 011-1h3a1 1 0 011 1v1M7 7v6M9 7v6M4 3v10a1 1 0 001 1h6a1 1 0 001-1V3", stroke: "currentColor", "stroke-width": "1.5", fill: "none" } ) }), "清空" ] } ) ] }) ] }), /* @__PURE__ */ u$1("div", { class: "log-container", ref: logContainerRef, children: filteredLogs.length === 0 ? /* @__PURE__ */ u$1("div", { class: "log-empty", children: [ /* @__PURE__ */ u$1("div", { class: "empty-icon", children: "📝" }), /* @__PURE__ */ u$1("p", { children: logs.length === 0 ? "暂无日志" : "无匹配的日志" }), /* @__PURE__ */ u$1("span", { children: logs.length === 0 ? "执行脚本后,日志将在这里显示" : "调整过滤条件以显示更多日志" }) ] }) : /* @__PURE__ */ u$1("div", { class: "log-list", children: filteredLogs.map((log) => /* @__PURE__ */ u$1("div", { class: `log-entry ${log.level}`, children: [ /* @__PURE__ */ u$1("div", { class: "log-meta", children: [ /* @__PURE__ */ u$1("span", { class: "log-icon", children: getLogIcon(log.level) }), /* @__PURE__ */ u$1("span", { class: "log-time", children: formatTime(log.timestamp) }), log.scriptId && /* @__PURE__ */ u$1("span", { class: "log-script", children: [ "[", log.scriptId, "]" ] }) ] }), /* @__PURE__ */ u$1("div", { class: "log-message", children: log.message }) ] }, log.id)) }) }), /* @__PURE__ */ u$1("div", { class: "log-footer", children: /* @__PURE__ */ u$1("div", { class: "log-filters", children: /* @__PURE__ */ u$1("div", { class: "filter-buttons", children: [ /* @__PURE__ */ u$1( "button", { class: `filter-button debug ${logLevelFilters.debug ? "active" : ""}`, onClick: () => toggleLogLevel("debug"), title: "切换调试日志显示", children: [ /* @__PURE__ */ u$1("span", { class: "filter-icon", children: "🔍" }), /* @__PURE__ */ u$1("span", { children: "调试" }) ] } ), /* @__PURE__ */ u$1( "button", { class: `filter-button info ${logLevelFilters.info ? "active" : ""}`, onClick: () => toggleLogLevel("info"), title: "切换信息日志显示", children: [ /* @__PURE__ */ u$1("span", { class: "filter-icon", children: "ℹ️" }), /* @__PURE__ */ u$1("span", { children: "信息" }) ] } ), /* @__PURE__ */ u$1( "button", { class: `filter-button success ${logLevelFilters.success ? "active" : ""}`, onClick: () => toggleLogLevel("success"), title: "切换成功日志显示", children: [ /* @__PURE__ */ u$1("span", { class: "filter-icon", children: "✅" }), /* @__PURE__ */ u$1("span", { children: "成功" }) ] } ), /* @__PURE__ */ u$1( "button", { class: `filter-button warn ${logLevelFilters.warn ? "active" : ""}`, onClick: () => toggleLogLevel("warn"), title: "切换警告日志显示", children: [ /* @__PURE__ */ u$1("span", { class: "filter-icon", children: "⚠️" }), /* @__PURE__ */ u$1("span", { children: "警告" }) ] } ), /* @__PURE__ */ u$1( "button", { class: `filter-button error ${logLevelFilters.error ? "active" : ""}`, onClick: () => toggleLogLevel("error"), title: "切换错误日志显示", children: [ /* @__PURE__ */ u$1("span", { class: "filter-icon", children: "❌" }), /* @__PURE__ */ u$1("span", { children: "错误" }) ] } ) ] }) }) }) ] }); } function ResizeHandle({ onMouseDown, isDragging }) { const handleRef = A(null); y(() => { const handleElement = handleRef.current; if (!handleElement) return; const handleMouseDown = (e2) => { e2.preventDefault(); onMouseDown(e2); }; handleElement.addEventListener("mousedown", handleMouseDown); return () => { handleElement.removeEventListener("mousedown", handleMouseDown); }; }, [onMouseDown]); return /* @__PURE__ */ u$1( "div", { ref: handleRef, class: `resize-handle ${isDragging ? "resize-handle--dragging" : ""}`, title: "拖拽调整面板宽度", children: [ /* @__PURE__ */ u$1("div", { class: "resize-handle-line" }), /* @__PURE__ */ u$1("div", { class: "resize-handle-grip", children: [ /* @__PURE__ */ u$1("div", { class: "resize-handle-dot" }), /* @__PURE__ */ u$1("div", { class: "resize-handle-dot" }), /* @__PURE__ */ u$1("div", { class: "resize-handle-dot" }), /* @__PURE__ */ u$1("div", { class: "resize-handle-dot" }), /* @__PURE__ */ u$1("div", { class: "resize-handle-dot" }), /* @__PURE__ */ u$1("div", { class: "resize-handle-dot" }) ] }) ] } ); } const ALL_SCRIPT_CONFIGS = [ // 工具脚本 { id: "bv2av", name: "BV/AV号转换", description: "将B站的BV号转换为AV号,或反之", category: "tool", isRunning: false, parameters: [ { key: "videoId", label: "视频ID", type: "text", defaultValue: "", required: true, placeholder: "输入BV号或AV号,如:BV1L9Uoa9EUx 或 av111298867365120", description: "支持BV号和AV号格式" } ] }, { id: "show_resource_info", name: "获取视频信息", description: "获取B站视频的详细信息", category: "tool", isRunning: false, parameters: [ { key: "videoIds", label: "视频ID列表", type: "textarea", defaultValue: "", required: true, placeholder: "每行一个视频ID,支持BV号或AV号", description: "批量获取多个视频的信息" } ] }, // 操作脚本 { id: "move_favorite_to_toview", name: "移动收藏夹视频到稍后再看", description: "从收藏夹中按指定规则选择视频添加到稍后再看", category: "operation", isRunning: false, parameters: [ { key: "favoriteId", label: "收藏夹ID", type: "number", defaultValue: "", required: true, description: "要操作的收藏夹ID" }, { key: "sortOrder", label: "排序规则", type: "select", defaultValue: "original", required: false, description: "选择视频的排序方式,影响添加到稍后再看的顺序,不影响视频在收藏夹中的顺序", options: [ { value: "original", label: "收藏夹原始顺序" }, { value: "shortest", label: "按时长从短到长排序" }, { value: "longest", label: "按时长从长到短排序" }, { value: "play_asc", label: "按播放数从少到多排序" }, { value: "play_desc", label: "按播放数从多到少排序" } ] }, { key: "shuffleVideos", label: "启用有偏向随机选择", type: "boolean", defaultValue: false, required: false, description: "在排序基础上增加随机性,排序靠前的视频有更高被选中概率,兼顾排序偏好和观看多样性" }, { key: "upTo", label: "稍后再看目标数量", type: "number", defaultValue: 100, required: true, description: "稍后再看补全到多少个资源(上限1000)" }, { key: "durationThreshold", label: "时长阈值(秒)", type: "number", defaultValue: 1800, required: false, description: "超过这个时长的视频不添加,0表示不限制" }, { key: "ignoreFrontPage", label: "忽略前几页", type: "number", defaultValue: 6, required: false, description: "忽略收藏夹前几页的内容" }, { key: "ignoreTitleKeywords", label: "忽略标题关键词", type: "text", defaultValue: "asmr,助眠,音声,触发音", required: false, description: "忽略标题中包含这些关键词的视频,用逗号分隔" } ] }, { id: "add_toview_to_favorite", name: "稍后再看添加到收藏夹", description: "把稍后再看的视频添加到指定收藏夹", category: "operation", isRunning: false, parameters: [ { key: "favoriteId", label: "目标收藏夹ID", type: "number", defaultValue: "", required: true, description: "要添加到的收藏夹ID" }, { key: "maxCount", label: "最大添加数量", type: "number", defaultValue: 0, required: false, description: "最多添加多少个视频,0表示全部添加" }, { key: "disableSpaceCheck", label: "关闭收藏夹剩余空间检查", type: "boolean", defaultValue: false, required: false, description: "启用后将跳过收藏夹容量检查,适用于添加重复视频的场景(重复视频不占用额外空间)" } ] }, { id: "move_favorite_to_another", name: "移动收藏夹视频", description: "将一个收藏夹的视频移动到另一个收藏夹", category: "operation", isRunning: false, parameters: [ { key: "fromFavorite", label: "源收藏夹ID", type: "number", defaultValue: "", required: true, description: "被移动的收藏夹ID" }, { key: "toFavorite", label: "目标收藏夹ID", type: "number", defaultValue: "", required: true, description: "移动到的收藏夹ID" }, { key: "upTo", label: "目标收藏夹上限", type: "number", defaultValue: 1e3, required: true, description: "目标收藏夹的视频数上限" }, { key: "onlyWithKeywords", label: "仅移动包含关键词的视频", type: "text", defaultValue: "", required: false, placeholder: "用逗号分隔多个关键词", description: "只移动标题中包含这些关键词的视频,空表示不过滤" } ] }, { id: "delete_timeout_lottery", name: "删除过期抽奖动态", description: "删除已过期的抽奖动态(仅限官方抽奖工具)", category: "operation", isRunning: false, parameters: [ { key: "detectOnly", label: "仅检测不删除", type: "boolean", defaultValue: false, required: false, description: "开启后只检测过期动态,不执行删除操作" }, { key: "notDeleteWinning", label: "不删除中奖动态", type: "boolean", defaultValue: true, required: false, description: "不删除已中奖的抽奖动态" }, { key: "userId", label: "用户ID", type: "number", defaultValue: "", required: false, description: "指定用户ID,不填则为当前登录用户。 填其他人可以扫描其他人的动态,但无法删除。" } ] }, { id: "clear_toview", name: "清空稍后再看", description: "由于新版「稍后再看」移除了清空按钮,特出该脚本以删除所有「稍后再看」的视频。", category: "operation", isRunning: false, parameters: [ { key: "confirm", label: "确认清空", type: "boolean", defaultValue: false, required: true, description: "确认要清空稍后再看列表(此操作不可恢复)" } ] } ]; const SCRIPT_CONFIGS = ALL_SCRIPT_CONFIGS.filter((script) => !script.disabled); function containsAnyKeyword(text, keywords, caseSensitive = false) { if (keywords.length === 0) return false; const searchText = caseSensitive ? text : text.toLowerCase(); return keywords.some((keyword) => { const searchKeyword = caseSensitive ? keyword : keyword.toLowerCase(); return searchText.includes(searchKeyword); }); } function generateId() { return Date.now().toString(36) + Math.random().toString(36).substring(2); } class ScriptExecutor { constructor(scriptId, onLog, onProgress) { __publicField(this, "execution"); __publicField(this, "onLog"); __publicField(this, "onProgress"); __publicField(this, "shouldStop", false); // 进度管理相关属性 __publicField(this, "currentStep", 0); __publicField(this, "totalSteps", 0); __publicField(this, "autoProgressEnabled", false); this.execution = { id: generateId(), scriptId, status: "running", startTime: /* @__PURE__ */ new Date(), progress: 0, logs: [] }; this.onLog = onLog; this.onProgress = onProgress; } /** * 记录日志 */ log(level, message) { const logEntry = { id: generateId(), timestamp: /* @__PURE__ */ new Date(), level, message, scriptId: this.execution.scriptId }; this.execution.logs.push(logEntry); this.onLog(logEntry); } /** * 设置总步骤数,启用基于步骤的进度管理 */ setTotalSteps(total) { this.totalSteps = total; this.currentStep = 0; this.autoProgressEnabled = true; } /** * 更新进度 * @param progressOrCurrentStep 进度百分比(0-100) 或当前步骤数 * @param totalSteps 总步骤数(可选,如果提供则使用基于步骤的进度计算) */ updateProgress(progressOrCurrentStep, totalSteps) { let progress; if (progressOrCurrentStep === void 0) { if (this.autoProgressEnabled && this.totalSteps > 0) { this.currentStep = Math.min(this.currentStep + 1, this.totalSteps); progress = Math.floor(this.currentStep / this.totalSteps * 100); } else { progress = Math.min(this.execution.progress + 5, 95); } } else if (totalSteps !== void 0) { this.currentStep = progressOrCurrentStep; this.totalSteps = totalSteps; this.autoProgressEnabled = true; progress = Math.floor(this.currentStep / this.totalSteps * 100); } else { progress = progressOrCurrentStep; if (progress >= 0 && progress <= 100) { this.autoProgressEnabled = false; } } this.execution.progress = Math.max(0, Math.min(100, progress)); this.onProgress(this.execution.progress); } /** * 检查是否应该停止执行 */ checkShouldStop() { if (this.shouldStop) { throw new Error("Script execution was stopped by user"); } } /** * 停止脚本执行 */ stop() { this.shouldStop = true; this.log("warn", "用户请求停止脚本执行"); } /** * 运行脚本的完整流程 */ async run(parameters) { try { this.log("debug", "开始执行脚本"); this.updateProgress(0); const result = await this.execute(parameters); this.execution.status = "completed"; this.execution.endTime = /* @__PURE__ */ new Date(); this.execution.result = result; if (this.execution.progress < 100) { this.updateProgress(100); } this.log("debug", "脚本执行完成"); } catch (error) { this.execution.status = this.shouldStop ? "stopped" : "failed"; this.execution.endTime = /* @__PURE__ */ new Date(); this.execution.error = error instanceof Error ? error.message : String(error); if (this.shouldStop) { this.log("warn", "脚本执行已停止"); } else { this.log("error", `脚本执行失败: ${this.execution.error}`); } } return this.execution; } /** * 获取执行状态 */ getExecution() { return { ...this.execution }; } } class TokenBucket { constructor(capacity, rate) { __publicField(this, "capacity"); __publicField(this, "tokens"); __publicField(this, "rate"); __publicField(this, "lastRefillTime"); this.capacity = capacity; this.tokens = capacity; this.rate = rate; this.lastRefillTime = Date.now(); } /** * 填充令牌 */ refill() { const now = Date.now(); const timePassed = (now - this.lastRefillTime) / 1e3; const newTokens = timePassed * this.rate; this.tokens = Math.min(this.capacity, this.tokens + newTokens); this.lastRefillTime = now; } /** * 消费令牌 * @param tokens 需要消费的令牌数量 * @returns 是否成功消费 */ consume(tokens = 1) { this.refill(); if (this.tokens >= tokens) { this.tokens -= tokens; return true; } return false; } /** * 等待直到可以消费指定数量的令牌 * @param tokens 需要消费的令牌数量 * @returns Promise,在可以消费时resolve */ async waitForTokens(tokens = 1) { return new Promise((resolve) => { const checkTokens = () => { if (this.consume(tokens)) { resolve(); } else { setTimeout(checkTokens, 100); } }; checkTokens(); }); } /** * 获取当前令牌数量 */ getTokens() { this.refill(); return this.tokens; } } class BiliApiClient { constructor(capacity = 10, rate = 0.7) { __publicField(this, "tokenBucket"); __publicField(this, "baseHeaders"); this.tokenBucket = new TokenBucket(capacity, rate); this.baseHeaders = { "Referer": "https://www.bilibili.com", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0", "Accept": "application/json, text/plain, */*", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8" }; } /** * 发送GET请求 */ async get(url, params) { await this.tokenBucket.waitForTokens(); const searchParams = new URLSearchParams(); if (params) { Object.entries(params).forEach(([key, value]) => { if (value !== void 0 && value !== null) { searchParams.append(key, String(value)); } }); } const fullUrl = searchParams.toString() ? `${url}?${searchParams}` : url; return new Promise((resolve, reject) => { _GM_xmlhttpRequest({ method: "GET", url: fullUrl, headers: this.baseHeaders, onload: (response) => { try { const data2 = JSON.parse(response.responseText); resolve(data2); } catch (error) { reject(new Error(`Failed to parse response: ${error}`)); } }, onerror: (error) => { reject(new Error(`Request failed: ${error}`)); }, ontimeout: () => { reject(new Error("Request timeout")); }, timeout: 1e4 }); }); } /** * 发送POST请求 */ async post(url, data2, params) { await this.tokenBucket.waitForTokens(); const searchParams = new URLSearchParams(); if (params) { Object.entries(params).forEach(([key, value]) => { if (value !== void 0 && value !== null) { searchParams.append(key, String(value)); } }); } const fullUrl = searchParams.toString() ? `${url}?${searchParams}` : url; const formData = new URLSearchParams(); if (data2) { Object.entries(data2).forEach(([key, value]) => { if (value !== void 0 && value !== null) { formData.append(key, String(value)); } }); } return new Promise((resolve, reject) => { _GM_xmlhttpRequest({ method: "POST", url: fullUrl, headers: { ...this.baseHeaders, "Content-Type": "application/x-www-form-urlencoded" }, data: formData.toString(), onload: (response) => { try { const responseData = JSON.parse(response.responseText); resolve(responseData); } catch (error) { reject(new Error(`Failed to parse response: ${error}`)); } }, onerror: (error) => { reject(new Error(`Request failed: ${error}`)); }, ontimeout: () => { reject(new Error("Request timeout")); }, timeout: 1e4 }); }); } /** * 获取当前令牌数量(用于调试) */ getTokenCount() { return this.tokenBucket.getTokens(); } } const biliApiClient = new BiliApiClient(); function getCsrfToken() { const cookies = document.cookie.split(";"); for (const cookie of cookies) { const [name, value] = cookie.trim().split("="); if (name === "bili_jct") { return value; } } throw new Error("CSRF token not found. Please make sure you are logged in."); } function getUserId() { const cookies = document.cookie.split(";"); for (const cookie of cookies) { const [name, value] = cookie.trim().split("="); if (name === "DedeUserID") { return value; } } throw new Error("User ID not found. Please make sure you are logged in."); } async function getToViewList() { const response = await biliApiClient.get("https://api.bilibili.com/x/v2/history/toview"); if (response.code !== 0) { throw new Error(`Failed to get toview list: ${response.message}`); } return response.data; } async function getFavoriteInfo(favoriteId) { const response = await biliApiClient.get( "https://api.bilibili.com/x/v3/fav/folder/info", { media_id: favoriteId } ); if (response.code !== 0) { throw new Error(`Failed to get favorite info: ${response.message}`); } return response.data; } async function getFavoriteResourceList(favoriteId, pageIndex = 1, pageSize = 20) { const response = await biliApiClient.get( "https://api.bilibili.com/x/v3/fav/resource/list", { media_id: favoriteId, pn: pageIndex, ps: pageSize, keyword: "", order: "mtime", type: 0, tid: 0, platform: "web" } ); if (response.code !== 0) { throw new Error(`Failed to get favorite resource list: ${response.message}`); } return response.data; } async function addToToView(videoId) { const response = await biliApiClient.post( "https://api.bilibili.com/x/v2/history/toview/add", { aid: videoId, csrf: getCsrfToken() } ); if (response.code !== 0) { throw new Error(`Failed to add to toview: ${response.message}`); } } async function clearToViewList() { const response = await biliApiClient.post( "https://api.bilibili.com/x/v2/history/toview/clear", { csrf: getCsrfToken() } ); if (response.code !== 0) { throw new Error(`Failed to clear toview list: ${response.message}`); } } async function deleteFromFavorite(favoriteId, resourceIds) { const resources = resourceIds.map((r2) => `${r2.id}:${r2.type}`).join(","); const response = await biliApiClient.post( "https://api.bilibili.com/x/v3/fav/resource/batch-del", { media_id: favoriteId, resources, csrf: getCsrfToken() } ); if (response.code !== 0) { throw new Error(`Failed to delete from favorite: ${response.message}`); } } async function addOrDeleteToFavorite(resourceId, resourceType, addFavoriteIds = [], delFavoriteIds = []) { const response = await biliApiClient.post( "https://api.bilibili.com/x/v3/fav/resource/deal", { rid: resourceId, type: resourceType, add_media_ids: addFavoriteIds.join(","), del_media_ids: delFavoriteIds.join(","), csrf: getCsrfToken() } ); if (response.code !== 0) { throw new Error(`Failed to add or delete to favorite: ${response.message}`); } } async function moveToFavorite(fromFavoriteId, toFavoriteId, resourceIds) { const resources = resourceIds.map((r2) => `${r2.id}:${r2.type}`).join(","); const response = await biliApiClient.post( "https://api.bilibili.com/x/v3/fav/resource/move", { src_media_id: fromFavoriteId.toString(), tar_media_id: toFavoriteId.toString(), resources, csrf: getCsrfToken() } ); if (response.code !== 0) { throw new Error(`Failed to move to favorite: ${response.message}`); } } async function getDynamicList(uid, offset = "") { const response = await biliApiClient.get( "https://api.bilibili.com/x/polymer/web-dynamic/v1/feed/space", { offset, host_mid: uid || getUserId(), timezone_offset: -480 } ); if (response.code !== 0) { throw new Error(`Failed to get dynamic list: ${response.message}`); } return response.data; } async function deleteDynamic(dynamicId) { const response = await biliApiClient.post( "https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/rm_dynamic", { dynamic_id: dynamicId }, void 0 ); if (response.code !== 0) { throw new Error(`Failed to delete dynamic: ${response.message}`); } } async function getLotteryInfo(dynamicId) { const response = await biliApiClient.get( "https://api.vc.bilibili.com/lottery_svr/v1/lottery_svr/lottery_notice", { business_id: dynamicId, business_type: "1", csrf: getCsrfToken(), web_location: "333.1330" } ); if (response.code !== 0) { throw new Error(`Failed to get lottery info: ${response.message}`); } return response.data; } async function getVideoInfo(videoId) { videoId = videoId.trim(); const url = `https://api.bilibili.com/x/web-interface/view`; const response = await biliApiClient.get(url, { bvid: videoId.toLocaleUpperCase().startsWith("BV") ? videoId : void 0, aid: videoId.toLocaleUpperCase().startsWith("AV") ? parseInt(videoId.slice(2)) : void 0 }); if (response.code !== 0) { throw new Error(`Failed to get video info: ${response.message}`); } return response.data; } async function getFavoriteList(uid) { const response = await biliApiClient.get( "https://api.bilibili.com/x/v3/fav/folder/created/list-all", { up_mid: getUserId() } ); if (response.code !== 0) { throw new Error(`Failed to get favorite list: ${response.message}`); } return response.data; } const XOR_CODE = 23442827791579n; const MASK_CODE = 2251799813685247n; const MAX_AID = 1n << 51n; const BASE = 58n; const data = "FcwAPNKTMug3GV5Lj7EJnHpWsx4tb8haYeviqBz6rkCy12mUSDQX9RdoZf"; function av2bv(aid) { const bytes = ["B", "V", "1", "0", "0", "0", "0", "0", "0", "0", "0", "0"]; let bvIndex = bytes.length - 1; let tmp = (MAX_AID | BigInt(aid)) ^ XOR_CODE; while (tmp > 0) { bytes[bvIndex] = data[Number(tmp % BigInt(BASE))]; tmp = tmp / BASE; bvIndex -= 1; } [bytes[3], bytes[9]] = [bytes[9], bytes[3]]; [bytes[4], bytes[7]] = [bytes[7], bytes[4]]; return bytes.join(""); } function bv2av(bvid) { const bvidArr = Array.from(bvid); [bvidArr[3], bvidArr[9]] = [bvidArr[9], bvidArr[3]]; [bvidArr[4], bvidArr[7]] = [bvidArr[7], bvidArr[4]]; bvidArr.splice(0, 3); const tmp = bvidArr.reduce((pre, bvidChar) => pre * BASE + BigInt(data.indexOf(bvidChar)), 0n); return Number(tmp & MASK_CODE ^ XOR_CODE); } function isValidAid(aid) { return typeof aid === "number" && Number.isInteger(aid) && aid > 0 && BigInt(aid) < MAX_AID; } class BvAvConverterExecutor extends ScriptExecutor { async execute(parameters) { const { videoId } = parameters; if (!videoId) { throw new Error("请输入视频ID"); } this.log("info", `开始转换视频ID: ${videoId}`); this.log("debug", `输入参数: ${JSON.stringify(parameters)}`); this.updateProgress(10); try { let result; if (videoId.startsWith("BV")) { this.log("debug", `检测到BV号格式,准备转换为AV号`); const aid = bv2av(videoId); this.log("debug", `转换结果: BV号 ${videoId} → AV号 ${aid}`); this.updateProgress(50); result = { input: videoId, output: `av${aid}`, type: "BV → AV" }; this.log("success", `转换成功: ${videoId} → av${aid}`); } else if (videoId.startsWith("av")) { const aid = parseInt(videoId.slice(2)); if (!isValidAid(aid)) { throw new Error("无效的AV号格式"); } const bvid = av2bv(aid); result = { input: videoId, output: bvid, type: "AV → BV" }; this.log("success", `转换成功: ${videoId} → ${bvid}`); } else { const aid = parseInt(videoId); if (isValidAid(aid)) { const bvid = av2bv(aid); result = { input: `av${aid}`, output: bvid, type: "AV → BV" }; this.log("success", `转换成功: av${aid} → ${bvid}`); } else { throw new Error("无法识别的视频ID格式,请输入BV号或AV号"); } } this.updateProgress(100); return result; } catch (error) { this.log("error", `转换失败: ${error instanceof Error ? error.message : String(error)}`); throw error; } } } class VideoInfoExecutor extends ScriptExecutor { async execute(parameters) { const { videoIds } = parameters; if (!videoIds) { throw new Error("请输入视频ID列表"); } const idList = videoIds.split("\n").filter((id) => id.trim()).map((id) => id.trim()); if (idList.length === 0) { throw new Error("请输入至少一个视频ID"); } this.log("info", `开始获取 ${idList.length} 个视频的信息`); this.setTotalSteps(idList.length); const results = []; const total = idList.length; for (let i2 = 0; i2 < total; i2++) { this.checkShouldStop(); const videoId = idList[i2]; this.log("info", `正在处理: ${videoId} (${i2 + 1}/${total})`); try { const info = await getVideoInfo(videoId); results.push({ id: videoId, title: info.title, duration: info.duration || 0, bvid: info.bvid, aid: info.aid, type: info.type }); this.log("success", `获取成功: ${videoId} - ${info.title}`); } catch (error) { this.log("error", `获取失败: ${videoId} - ${error instanceof Error ? error.message : String(error)}`); results.push({ id: videoId, error: error instanceof Error ? error.message : String(error) }); } this.updateProgress(i2 + 1, total); } const successCount = results.filter((r2) => !r2.error).length; const failCount = results.filter((r2) => r2.error).length; this.log("success", `视频信息获取任务完成!成功获取 ${successCount} 个视频信息,失败 ${failCount} 个`); if (successCount > 0) { this.log("info", `成功获取的视频:${results.filter((r2) => !r2.error).map((r2) => r2.title).join(", ")}`); } return { results, total: results.length, successCount, failCount }; } } class MoveFavoriteToToviewExecutor extends ScriptExecutor { /** * 计算基于位置的线性递减权重 * @param totalCount 视频总数 * @param position 视频在排序后列表中的位置(从0开始) * @returns 权重值 */ calculatePositionWeight(totalCount, position) { return Math.max(1, totalCount - position); } /** * 使用轮盘赌算法进行加权随机选择 * @param videos 已排序的视频列表 * @param targetCount 目标选择数量 * @returns 加权随机选择后的视频列表 */ performWeightedRandomSelection(videos, targetCount) { if (videos.length === 0 || targetCount <= 0) { return []; } const totalCount = videos.length; const actualTargetCount = Math.min(targetCount, totalCount); const selected = []; const availableVideos = [...videos]; this.log("debug", `开始有偏向随机选择,目标数量: ${actualTargetCount}`); const weights = availableVideos.map((_, index) => this.calculatePositionWeight(totalCount, index)); const maxWeight = Math.max(...weights); const minWeight = Math.min(...weights); const avgWeight = weights.reduce((sum, w2) => sum + w2, 0) / weights.length; this.log("debug", `权重分布: 最高${maxWeight}, 最低${minWeight}, 平均${avgWeight.toFixed(1)}`); for (let i2 = 0; i2 < actualTargetCount && availableVideos.length > 0; i2++) { const currentWeights = availableVideos.map( (_, index) => this.calculatePositionWeight(availableVideos.length, index) ); const selectedIndex = this.rouletteWheelSelection(currentWeights); selected.push(availableVideos[selectedIndex]); availableVideos.splice(selectedIndex, 1); } this.logSelectionDistribution(videos, selected, totalCount); return selected; } /** * 轮盘赌选择算法 * @param weights 权重数组 * @returns 选中的索引 */ rouletteWheelSelection(weights) { const totalWeight = weights.reduce((sum, weight) => sum + weight, 0); const randomValue = Math.random() * totalWeight; let cumulativeWeight = 0; for (let i2 = 0; i2 < weights.length; i2++) { cumulativeWeight += weights[i2]; if (randomValue <= cumulativeWeight) { return i2; } } return weights.length - 1; } /** * 记录选择结果的分布统计 * @param originalVideos 原始排序后的视频列表 * @param selectedVideos 选择后的视频列表 * @param totalCount 原始视频总数 */ logSelectionDistribution(originalVideos, selectedVideos, totalCount) { const positions = selectedVideos.map( (selected) => originalVideos.findIndex((original) => original.id === selected.id) ); const topTier = Math.ceil(totalCount * 0.2); const middleTier = Math.ceil(totalCount * 0.8); const topTierCount = positions.filter((pos) => pos < topTier).length; const middleTierCount = positions.filter((pos) => pos >= topTier && pos < middleTier).length; const bottomTierCount = positions.filter((pos) => pos >= middleTier).length; this.log("debug", `有偏向随机选择完成,实际选择分布:`); this.log("debug", `前20%区间选中: ${topTierCount}个 (${(topTierCount / selectedVideos.length * 100).toFixed(1)}%)`); this.log("debug", `中间60%区间选中: ${middleTierCount}个 (${(middleTierCount / selectedVideos.length * 100).toFixed(1)}%)`); this.log("debug", `后20%区间选中: ${bottomTierCount}个 (${(bottomTierCount / selectedVideos.length * 100).toFixed(1)}%)`); const topSelectedPositions = positions.slice(0, Math.min(5, positions.length)).sort((a2, b) => a2 - b); this.log("debug", `前5个选中视频的原始排序位置: [${topSelectedPositions.map((p2) => p2 + 1).join(", ")}]`); } async execute(parameters) { var _a, _b, _c, _d; const { favoriteId, sortOrder, shuffleVideos, upTo, durationThreshold, ignoreFrontPage, ignoreTitleKeywords } = parameters; if (!favoriteId) { throw new Error("请输入收藏夹ID"); } this.log("info", `开始从收藏夹 ${favoriteId} 移动视频到稍后再看`); this.updateProgress(10); const ignoreTitleKeywordList = ignoreTitleKeywords ? ignoreTitleKeywords.split(",").map((k2) => k2.trim()) : []; const maxCount = upTo || 1e3; let needCount = maxCount; const maxDuration = durationThreshold || 0; const ignorePageCount = ignoreFrontPage || 6; let originVideoInfos = []; let willMoveVideoInfos = []; try { this.log("info", `正在获取稍后再看当前数量...`); this.checkShouldStop(); const toviewList = await getToViewList(); const currentCount = toviewList.count; needCount = maxCount - currentCount; let log2 = `稍后再看当前视频数量: ${currentCount}/1000`; log2 += ` 还需要添加: ${needCount}`; if (needCount <= 0) { log2 += ` 稍后再看已达到目标数量,无需添加更多视频`; this.log("info", log2); return { added: 0, currentCount, needCount }; } this.log("info", log2); this.updateProgress(40); } catch (error) { this.log("error", `获取稍后再看数量失败: ${error instanceof Error ? error.message : String(error)}`); throw error; } try { this.log("info", `正在获取源收藏夹所有视频...`); let pageIndex = 1; const pageSize = 20; while (true) { this.checkShouldStop(); if (pageIndex <= ignorePageCount) { this.log("debug", `忽略第 ${pageIndex} 页视频...`); pageIndex++; continue; } const pageInfo = await getFavoriteResourceList(favoriteId, pageIndex, pageSize); originVideoInfos.push(...pageInfo.medias); const pageProgress = Math.min(70, (pageIndex - 1) * 5); this.updateProgress(20 + pageProgress); this.log("debug", `已获取 ${originVideoInfos.length} 个视频`); if (!pageInfo.has_more) break; pageIndex++; } } catch (error) { this.log("error", `获取源收藏夹视频失败: ${error instanceof Error ? error.message : String(error)}`); throw error; } const sortOrderValue = sortOrder || "original"; const shuffleEnabled = shuffleVideos === true; this.log("debug", `正在按 ${sortOrderValue} 规则排序视频...`); this.log("debug", `排序前视频数量: ${originVideoInfos.length}`); if (shuffleEnabled) { this.log("debug", "注意:已启用随机打乱,排序完成后将被随机打乱覆盖"); } switch (sortOrderValue) { case "shortest": originVideoInfos = originVideoInfos.sort((a2, b) => a2.duration - b.duration); this.log("debug", "已按时长从短到长排序"); if (originVideoInfos.length > 0) { this.log("debug", `最短视频: ${originVideoInfos[0].title} (${originVideoInfos[0].duration}秒)`); this.log("debug", `最长视频: ${originVideoInfos[originVideoInfos.length - 1].title} (${originVideoInfos[originVideoInfos.length - 1].duration}秒)`); } break; case "longest": originVideoInfos = originVideoInfos.sort((a2, b) => b.duration - a2.duration); this.log("debug", "已按时长从长到短排序"); if (originVideoInfos.length > 0) { this.log("debug", `最长视频: ${originVideoInfos[0].title} (${originVideoInfos[0].duration}秒)`); this.log("debug", `最短视频: ${originVideoInfos[originVideoInfos.length - 1].title} (${originVideoInfos[originVideoInfos.length - 1].duration}秒)`); } break; case "play_asc": originVideoInfos = originVideoInfos.sort((a2, b) => { var _a2, _b2; return (((_a2 = a2.cnt_info) == null ? void 0 : _a2.play) || 0) - (((_b2 = b.cnt_info) == null ? void 0 : _b2.play) || 0); }); this.log("debug", "已按播放数从少到多排序"); if (originVideoInfos.length > 0) { this.log("debug", `播放数最少视频: ${originVideoInfos[0].title} (${((_a = originVideoInfos[0].cnt_info) == null ? void 0 : _a.play) || 0}次播放)`); this.log("debug", `播放数最多视频: ${originVideoInfos[originVideoInfos.length - 1].title} (${((_b = originVideoInfos[originVideoInfos.length - 1].cnt_info) == null ? void 0 : _b.play) || 0}次播放)`); } break; case "play_desc": originVideoInfos = originVideoInfos.sort((a2, b) => { var _a2, _b2; return (((_a2 = b.cnt_info) == null ? void 0 : _a2.play) || 0) - (((_b2 = a2.cnt_info) == null ? void 0 : _b2.play) || 0); }); this.log("debug", "已按播放数从多到少排序"); if (originVideoInfos.length > 0) { this.log("debug", `播放数最多视频: ${originVideoInfos[0].title} (${((_c = originVideoInfos[0].cnt_info) == null ? void 0 : _c.play) || 0}次播放)`); this.log("debug", `播放数最少视频: ${originVideoInfos[originVideoInfos.length - 1].title} (${((_d = originVideoInfos[originVideoInfos.length - 1].cnt_info) == null ? void 0 : _d.play) || 0}次播放)`); } break; case "original": default: this.log("debug", "保持收藏夹原始顺序"); if (originVideoInfos.length > 0) { this.log("debug", `第一个视频: ${originVideoInfos[0].title}`); this.log("debug", `最后一个视频: ${originVideoInfos[originVideoInfos.length - 1].title}`); } break; } this.log("debug", `正在过滤视频...`); const filteredVideoInfos = []; for (const video of originVideoInfos) { if (ignoreTitleKeywordList.length > 0 && containsAnyKeyword(video.title, ignoreTitleKeywordList)) { continue; } if (maxDuration > 0 && video.duration > maxDuration) { continue; } filteredVideoInfos.push(video); } this.log("debug", `过滤完成,共 ${filteredVideoInfos.length} 个视频符合条件`); if (shuffleEnabled && filteredVideoInfos.length > 0) { this.log("info", "已启用有偏向随机选择,将基于排序结果进行加权随机选择"); willMoveVideoInfos = this.performWeightedRandomSelection(filteredVideoInfos, needCount); } else { willMoveVideoInfos = filteredVideoInfos.slice(0, needCount); } let log = `排序、过滤和选择完成,共 ${willMoveVideoInfos.length} 个视频将被添加`; log += ` 排序规则: ${sortOrderValue}`; log += ` 有偏向随机选择: ${shuffleEnabled ? "已启用" : "未启用"}`; if (shuffleEnabled) { log += ` 选择方式: 基于排序位置的加权随机选择`; } else { log += ` 选择方式: 按排序顺序依次选择`; } log += ` 将要移动的视频列表: ${willMoveVideoInfos.map((v2) => v2.title).join(", ")}`; this.log("debug", log); this.updateProgress(30); this.log("info", `正在添加视频到稍后再看...`); for (let i2 = 0; i2 < willMoveVideoInfos.length; i2++) { this.checkShouldStop(); const video = willMoveVideoInfos[i2]; this.log("info", `正在添加: ${video.title} (${i2 + 1}/${willMoveVideoInfos.length})`); try { await addToToView(video.id); this.log("success", `添加成功: ${video.title}`); } catch (error) { this.log("error", `添加失败: ${video.title} - ${error instanceof Error ? error.message : String(error)}`); } this.updateProgress(30 + Math.floor((i2 + 1) / willMoveVideoInfos.length * 60)); } this.log("success", `共添加 ${willMoveVideoInfos.length} 个视频到稍后再看`); this.log("info", `正在从收藏夹中删除已移动的视频...`); try { await deleteFromFavorite( favoriteId, willMoveVideoInfos.map((v2) => ({ id: v2.id, type: v2.type })) ); this.log("success", `删除成功`); this.updateProgress(100); } catch (error) { this.log("error", `删除失败: ${error instanceof Error ? error.message : String(error)}`); throw error; } this.log("success", `共删除 ${willMoveVideoInfos.length} 个视频从收藏夹`); } } class AddToviewToFavoriteExecutor extends ScriptExecutor { async execute(parameters) { const { favoriteId, maxCount, disableSpaceCheck } = parameters; if (!favoriteId) { throw new Error("请输入收藏夹ID"); } this.log("info", `开始将稍后再看的视频添加到收藏夹 ${favoriteId}`); this.updateProgress(5); try { const toviewList = await getToViewList(); if (!toviewList.list || toviewList.list.length === 0) { this.log("info", "稍后再看列表为空"); return { added: 0, total: 0 }; } const maxAdd = maxCount || toviewList.list.length; const videosToAdd = toviewList.list.slice(0, maxAdd).reverse(); this.log("info", `准备添加 ${videosToAdd.length} 个视频到收藏夹`); this.updateProgress(10); if (disableSpaceCheck) { this.log("info", "已关闭空间检查,将跳过容量验证"); } else { this.log("debug", "正在检查收藏夹容量..."); const favoriteInfo = await getFavoriteInfo(favoriteId); const currentCount = favoriteInfo.media_count; const toAddCount = videosToAdd.length; const remainingSpace = 1e3 - currentCount; let log = `收藏夹当前视频数量: ${currentCount}/1000`; log += ` 待添加视频数量: ${toAddCount}`; log += ` 剩余空间: ${remainingSpace}`; this.log("debug", log); if (currentCount + toAddCount > 1e3) { this.log("error", `收藏夹空间不足,无法添加所有视频。当前: ${currentCount},待添加: ${toAddCount},剩余空间: ${remainingSpace}`); throw new Error("收藏夹空间不足,无法添加所有视频"); } } this.updateProgress(20); let addedCount = 0; const total = videosToAdd.length; this.log("debug", `开始逐个添加 ${total} 个视频到收藏夹`); this.setTotalSteps(total); for (let i2 = 0; i2 < total; i2++) { this.checkShouldStop(); const video = videosToAdd[i2]; let log = `正在添加: ${video.title} (${i2 + 1}/${total})`; this.log("debug", log); try { await addOrDeleteToFavorite(video.aid, 2, [favoriteId], []); addedCount++; log += ` 添加成功: ${video.title}`; this.log("success", log); } catch (error) { log += ` 添加失败: ${error instanceof Error ? error.message : String(error)}`; this.log("error", log); throw error; } this.updateProgress(20 + Math.floor((i2 + 1) / total * 70)); } this.log("success", `操作完成,成功添加 ${addedCount}/${total} 个视频到收藏夹`); if (addedCount > 0) { this.log("info", `成功添加的视频:${videosToAdd.slice(0, addedCount).map((v2) => v2.title).join(", ")}`); } return { added: addedCount, total, videos: videosToAdd.slice(0, addedCount).map((v2) => ({ id: v2.id, title: v2.title })) }; } catch (error) { this.log("error", `操作失败: ${error instanceof Error ? error.message : String(error)}`); throw error; } } } class MoveFavoriteExecutor extends ScriptExecutor { async execute(parameters) { const { fromFavorite, toFavorite, upTo, onlyWithKeywords } = parameters; if (!fromFavorite || !toFavorite) { throw new Error("请输入源收藏夹ID和目标收藏夹ID"); } this.log("info", `开始从收藏夹 ${fromFavorite} 移动视频到收藏夹 ${toFavorite}`); this.updateProgress(10); const keywords = onlyWithKeywords ? onlyWithKeywords.split(",").map((k2) => k2.trim()) : []; const maxCount = upTo || 1e3; const originVideoInfos = []; const willMoveVideoInfos = []; try { this.log("info", `正在获取源收藏夹所有视频...`); let pageIndex = 1; const pageSize = 20; while (true) { this.checkShouldStop(); const pageInfo = await getFavoriteResourceList(fromFavorite, pageIndex, pageSize); originVideoInfos.push(...pageInfo.medias); const pageProgress = Math.min(30, (pageIndex - 1) * 3); this.updateProgress(20 + pageProgress); this.log("debug", `已获取 ${originVideoInfos.length} 个视频`); if (!pageInfo.has_more) break; pageIndex++; } } catch (error) { this.log("error", `获取源收藏夹视频失败: ${error instanceof Error ? error.message : String(error)}`); throw error; } this.log("debug", `正在过滤视频...`); for (const video of originVideoInfos) { if (keywords.length > 0 && !containsAnyKeyword(video.title, keywords)) { continue; } willMoveVideoInfos.push(video); if (willMoveVideoInfos.length >= maxCount) break; } let log = `过滤完成,共 ${willMoveVideoInfos.length} 个视频符合条件`; log += ` 将要移动的视频列表: ${willMoveVideoInfos.map((v2) => v2.title).join(", ")}`; this.log("debug", log); this.updateProgress(30); try { this.log("info", `正在获取目标收藏夹信息...`); this.checkShouldStop(); const targetFavoriteInfo = await getFavoriteInfo(toFavorite); const currentCount = targetFavoriteInfo.media_count; const remainingSpace = 1e3 - currentCount; log = `目标收藏夹当前视频数量: ${currentCount}/1000`; log += ` 剩余空间: ${remainingSpace}`; this.log("info", log); if (willMoveVideoInfos.length > remainingSpace) { this.log("error", `目标收藏夹空间不足,无法移动所有视频。当前: ${currentCount},待添加: ${willMoveVideoInfos.length},剩余空间: ${remainingSpace}`); throw new Error("目标收藏夹空间不足,无法移动所有视频"); } } catch (error) { this.log("error", `获取目标收藏夹信息失败: ${error instanceof Error ? error.message : String(error)}`); throw error; } this.updateProgress(40); this.log("info", `正在移动视频...`); try { this.checkShouldStop(); await moveToFavorite( fromFavorite, toFavorite, willMoveVideoInfos.map((v2) => ({ id: v2.id, type: v2.type })) ); } catch (error) { this.log("error", `视频移动失败: ${error instanceof Error ? error.message : String(error)}`); throw error; } this.log("success", `操作完成,共移动 ${willMoveVideoInfos.length} 个视频`); return { moved: willMoveVideoInfos.length, maxCount }; } } class DeleteTimeoutLotteryExecutor extends ScriptExecutor { async execute(parameters) { var _a, _b; const { detectOnly, notDeleteWinning, userId } = parameters; this.log("info", `开始${detectOnly ? "检测" : "删除"}过期抽奖动态`); let log = `执行参数: ${JSON.stringify(parameters)}。`; log += ` 仅检测模式: ${detectOnly ? "是" : "否"}。`; log += ` 保护中奖动态: ${notDeleteWinning ? "是" : "否"}。`; log += ` 目标用户: ${userId || "当前登录用户"}。`; log += "\n========== 开始执行过期抽奖动态处理 =========="; this.log("debug", log); this.updateProgress(10); try { let deletedCount = 0; let detectedCount = 0; let listOffset = ""; while (true) { this.checkShouldStop(); this.log("info", "正在获取动态列表..."); this.log("debug", `请求参数: userId=${userId || "当前用户"}, offset=${listOffset || "初始页面"}`); const dynamicData = await getDynamicList(userId, listOffset); const hasMore = dynamicData.has_more; listOffset = dynamicData.offset; for (let i2 = 0; i2 < dynamicData.items.length; i2++) { const item = dynamicData.items[i2]; this.checkShouldStop(); this.log("debug", `---------- 处理动态 ${i2 + 1}/${dynamicData.items.length} ----------`); const pageProgress = Math.floor(i2 / dynamicData.items.length * 15); this.updateProgress(10 + pageProgress); const dynamicId = item.id_str; const publisherInfo = item.modules.module_author; const publisherId = publisherInfo.mid; const publishTimestamp = publisherInfo.pub_ts; const dynamicText = (_a = item.modules.module_dynamic.desc) == null ? void 0 : _a.text.replace("\n", ""); const dynamicType = item.type; log = `动态ID: ${item.id_str}`; log += ` 发布时间:${new Date(publishTimestamp * 1e3).toLocaleString("zh-CN")}`; log += ` 动态类型: ${dynamicType}`; log += ` 动态内容: ${dynamicText}`; if (dynamicType !== "DYNAMIC_TYPE_FORWARD") { log += ` 非转发动态,跳过`; this.log("info", log); continue; } const originDynamic = item.orig; const originDynamicId = originDynamic.id_str; const originDynamicPublisherInfo = originDynamic.modules.module_author; const originDynamicPublisherId = originDynamicPublisherInfo.mid; const originDynamicPublisherName = originDynamicPublisherInfo.name; const originDynamicPublishTimestamp = originDynamicPublisherInfo.pub_ts; const originDynamicType = originDynamic.type; log += ` 被转发动态ID: ${originDynamicId}`; log += ` 被转发动态发布者: ${originDynamicPublisherName}(${originDynamicPublisherId})`; log += ` 被转发动态发布时间: ${new Date(originDynamicPublishTimestamp * 1e3).toLocaleString("zh-CN")}`; log += ` 被转发动态类型: ${originDynamicType}`; if (["DYNAMIC_TYPE_DRAW", "DYNAMIC_TYPE_WORD"].indexOf(originDynamicType) === -1) { log += ` 被转发动态非文本动态类型,跳过`; this.log("info", log); continue; } const hasLotteryNode = originDynamic.modules.module_dynamic.desc.rich_text_nodes.some( (node) => node.type === "RICH_TEXT_NODE_TYPE_LOTTERY" ); if (!hasLotteryNode) { log += ` 被转发动态非抽奖动态,跳过`; this.log("info", log); continue; } const originDynamicText = (_b = originDynamic.modules.module_dynamic.desc) == null ? void 0 : _b.text.replace("\n", ""); log += ` 被转发动态内容: ${originDynamicText}`; this.log("info", log); this.log("debug", "正在获取抽奖信息..."); const lotteryInfo = await getLotteryInfo(originDynamicId); this.log("debug", `抽奖信息: ${JSON.stringify(lotteryInfo)}`); const lotteryStatus = lotteryInfo.status; const lotteryTimestamp = lotteryInfo.lottery_time; const lotteryParticipated = lotteryInfo.participated; const lotteryFollowed = lotteryInfo.followed; const lotteryReposted = lotteryInfo.reposted; log = `抽奖状态: ${lotteryStatus}`; log += ` 开奖时间: ${new Date(lotteryTimestamp * 1e3).toLocaleString("zh-CN")}`; log += ` 是否参与: ${lotteryParticipated}`; log += ` 是否关注了发布者: ${lotteryFollowed}`; log += ` 是否转发了本动态: ${lotteryReposted}`; const lotteryResult = lotteryInfo.lottery_result; if (!lotteryResult) { log += ` 未找到开奖结果,跳过`; this.log("info", log); continue; } const win = Object.values(lotteryResult).some((userList) => { if (!Array.isArray(userList)) return false; return userList.some((user) => user.uid === publisherId); }); log += ` 是否中奖: ${win}`; detectedCount++; if (detectOnly) { log += ` 仅检测模式,跳过删除`; this.log("info", log); continue; } if (win && notDeleteWinning) { log += ` 不删除中奖动态,跳过`; this.log("info", log); continue; } this.log("debug", "正在删除动态..."); await deleteDynamic(dynamicId); deletedCount++; this.log("debug", "动态删除成功"); } this.log("debug", "========== 当前页面处理完成 =========="); if (!hasMore) { this.log("debug", "已到达最后一页,结束处理"); break; } const detectionProgress = Math.min(75, detectedCount * 3); this.updateProgress(10 + detectionProgress); this.log("debug", `当前进度: 已检测到 ${detectedCount} 个过期抽奖动态,已删除 ${deletedCount} 个`); } const message = detectOnly ? `检测完成,发现 ${detectedCount} 个过期抽奖动态` : `删除完成,共删除 ${deletedCount}/${detectedCount} 个过期抽奖动态`; this.log("success", message); return { detected: detectedCount, deleted: deletedCount, detectOnly }; } catch (error) { this.log("error", `操作失败: ${error instanceof Error ? error.message : String(error)}`); throw error; } } } class ClearToviewExecutor extends ScriptExecutor { async execute(parameters) { const { confirm } = parameters; if (!confirm) { throw new Error("请确认要清空稍后再看列表"); } this.log("info", "开始清空稍后再看列表"); this.updateProgress(20); try { const toviewList = await getToViewList(); const totalCount = toviewList.count; if (totalCount === 0) { this.log("info", "稍后再看列表已为空"); return { cleared: 0, total: 0 }; } this.log("info", `准备清空 ${totalCount} 个视频`); this.updateProgress(50); await clearToViewList(); this.updateProgress(100); this.log("success", `成功清空稍后再看列表,共清除 ${totalCount} 个视频`); return { cleared: totalCount, total: totalCount }; } catch (error) { this.log("error", `清空失败: ${error instanceof Error ? error.message : String(error)}`); throw error; } } } function createScriptExecutor(scriptId, onLog, onProgress) { switch (scriptId) { case "bv2av": return new BvAvConverterExecutor(scriptId, onLog, onProgress); case "show_resource_info": return new VideoInfoExecutor(scriptId, onLog, onProgress); case "move_favorite_to_toview": return new MoveFavoriteToToviewExecutor(scriptId, onLog, onProgress); case "add_toview_to_favorite": return new AddToviewToFavoriteExecutor(scriptId, onLog, onProgress); case "move_favorite_to_another": return new MoveFavoriteExecutor(scriptId, onLog, onProgress); case "delete_timeout_lottery": return new DeleteTimeoutLotteryExecutor(scriptId, onLog, onProgress); case "clear_toview": return new ClearToviewExecutor(scriptId, onLog, onProgress); default: throw new Error(`Unknown script type: ${scriptId}`); } } class ScriptExecutionManager { constructor(onLog, onProgress) { __publicField(this, "executors", /* @__PURE__ */ new Map()); __publicField(this, "onLog"); __publicField(this, "onProgress"); this.onLog = onLog; this.onProgress = onProgress; } /** * 执行脚本 */ async executeScript(scriptId, parameters) { const existingExecutor = Array.from(this.executors.values()).find((executor2) => executor2.getExecution().scriptId === scriptId && executor2.getExecution().status === "running"); if (existingExecutor) { throw new Error("该脚本已在运行中,请等待完成或停止后再试"); } const executor = createScriptExecutor( scriptId, this.onLog, (progress) => this.onProgress(scriptId, progress) ); const executionId = executor.getExecution().id; this.executors.set(executionId, executor); try { const result = await executor.run(parameters); return result; } finally { setTimeout(() => { this.executors.delete(executionId); }, 5e3); } } /** * 停止脚本执行 */ stopScript(scriptId) { const executor = Array.from(this.executors.values()).find((executor2) => executor2.getExecution().scriptId === scriptId && executor2.getExecution().status === "running"); if (executor) { executor.stop(); return true; } return false; } /** * 获取正在运行的脚本列表 */ getRunningScripts() { return Array.from(this.executors.values()).filter((executor) => executor.getExecution().status === "running").map((executor) => executor.getExecution().scriptId); } /** * 清理所有执行器 */ cleanup() { this.executors.clear(); } } function calculateNewWidths(currentX, state, config) { const deltaX = currentX - state.startX; let newLeftWidth = state.startLeftWidth + deltaX; let newRightWidth = state.startRightWidth - deltaX; if (newLeftWidth < config.minLeftWidth) { newLeftWidth = config.minLeftWidth; newRightWidth = config.containerWidth - newLeftWidth; } if (newRightWidth < config.minRightWidth) { newRightWidth = config.minRightWidth; newLeftWidth = config.containerWidth - newRightWidth; } const totalWidth = newLeftWidth + newRightWidth; if (totalWidth > config.containerWidth) { const ratio = config.containerWidth / totalWidth; newLeftWidth *= ratio; newRightWidth *= ratio; } return { leftWidth: newLeftWidth, rightWidth: newRightWidth }; } function calculateRatio(leftWidth, rightWidth) { const totalWidth = leftWidth + rightWidth; return totalWidth > 0 ? leftWidth / totalWidth : 0.4; } function calculateWidthsFromRatio(ratio, containerWidth, config) { let leftWidth = containerWidth * ratio; let rightWidth = containerWidth * (1 - ratio); if (leftWidth < config.minLeftWidth) { leftWidth = config.minLeftWidth; rightWidth = containerWidth - leftWidth; } if (rightWidth < config.minRightWidth) { rightWidth = config.minRightWidth; leftWidth = containerWidth - rightWidth; } return { leftWidth, rightWidth }; } function isMobileOrSmallScreen() { return window.innerWidth <= 1024; } function getContainerWidth(containerElement) { if (!containerElement) return 1200; return containerElement.clientWidth; } function debounce(func, delay) { let timeoutId; return (...args) => { clearTimeout(timeoutId); timeoutId = window.setTimeout(() => func(...args), delay); }; } function ScriptManager() { const containerRef = A(null); const [appState, setAppState] = d(() => ({ scripts: SCRIPT_CONFIGS.map((config) => ({ ...config })), executions: [], logs: [], selectedScript: null, isModalOpen: true, favoriteList: null, favoriteListLoading: false, favoriteListError: null })); const [panelRatio, setPanelRatio] = d(0.6); const [resizeState, setResizeState] = d({ isDragging: false, startX: 0, startLeftWidth: 0, startRightWidth: 0 }); const [panelWidths, setPanelWidths] = d({ leftWidth: 450, rightWidth: 600 }); const [executionManager] = d(() => new ScriptExecutionManager( (log) => { setAppState((prev) => ({ ...prev, logs: [...prev.logs, log] })); }, (scriptId, progress) => { setAppState((prev) => ({ ...prev, scripts: prev.scripts.map( (script) => script.id === scriptId ? { ...script, progress } : script ) })); } )); y(() => { const savedLogs = _GM_getValue("bili_tasks_logs", "[]"); try { const logs = JSON.parse(savedLogs).map((log) => ({ ...log, timestamp: new Date(log.timestamp) })); setAppState((prev) => ({ ...prev, logs })); } catch (error) { console.warn("Failed to load saved logs:", error); } const savedRatio = _GM_getValue("bili_tasks_panel_ratio", "0.6"); try { const ratio = parseFloat(savedRatio); if (ratio >= 0.2 && ratio <= 0.8) { setPanelRatio(ratio); } } catch (error) { console.warn("Failed to load saved panel ratio:", error); } loadFavoriteList(); }, []); const loadFavoriteList = async () => { setAppState((prev) => ({ ...prev, favoriteListLoading: true, favoriteListError: null })); try { const favoriteList = await getFavoriteList(); setAppState((prev) => ({ ...prev, favoriteList, favoriteListLoading: false })); } catch (error) { const errorMessage = error instanceof Error ? error.message : "获取收藏夹列表失败"; setAppState((prev) => ({ ...prev, favoriteListLoading: false, favoriteListError: errorMessage })); console.error("Failed to load favorite list:", error); } }; y(() => { const logsToSave = appState.logs.slice(-100); _GM_setValue("bili_tasks_logs", JSON.stringify(logsToSave)); }, [appState.logs]); y(() => { _GM_setValue("bili_tasks_panel_ratio", panelRatio.toString()); }, [panelRatio]); y(() => { const updatePanelWidths = () => { if (isMobileOrSmallScreen()) { setPanelWidths({ leftWidth: 450, rightWidth: 600 }); return; } const containerWidth = getContainerWidth(containerRef.current); const config = { minLeftWidth: 300, minRightWidth: 400, containerWidth: containerWidth - 48 - 24 - 8 // 减去左右padding(48px)、gap(24px)和分隔条宽度(8px) }; const { leftWidth, rightWidth } = calculateWidthsFromRatio(panelRatio, config.containerWidth, config); setPanelWidths({ leftWidth, rightWidth }); }; updatePanelWidths(); const debouncedResize = debounce(updatePanelWidths, 100); window.addEventListener("resize", debouncedResize); return () => { window.removeEventListener("resize", debouncedResize); }; }, [panelRatio]); const handleExecuteScript = async (scriptId, parameters) => { setAppState((prev) => ({ ...prev, scripts: prev.scripts.map( (script) => script.id === scriptId ? { ...script, isRunning: true, lastRun: /* @__PURE__ */ new Date() } : script ) })); try { await executionManager.executeScript(scriptId, parameters); } catch (error) { const errorLog = { id: generateId(), timestamp: /* @__PURE__ */ new Date(), level: "error", message: `脚本执行失败: ${error instanceof Error ? error.message : String(error)}`, scriptId }; setAppState((prev) => ({ ...prev, logs: [...prev.logs, errorLog] })); } finally { setAppState((prev) => ({ ...prev, scripts: prev.scripts.map( (script) => script.id === scriptId ? { ...script, isRunning: false } : script ) })); } }; const handleStopScript = (scriptId) => { const success = executionManager.stopScript(scriptId); if (success) { const stopLog = { id: generateId(), timestamp: /* @__PURE__ */ new Date(), level: "warn", message: "用户请求停止脚本执行", scriptId }; setAppState((prev) => ({ ...prev, logs: [...prev.logs, stopLog], scripts: prev.scripts.map( (script) => script.id === scriptId ? { ...script, isRunning: false } : script ) })); } }; const handleClearLogs = () => { setAppState((prev) => ({ ...prev, logs: [] })); _GM_setValue("bili_tasks_logs", "[]"); }; const handleResizeStart = (e2) => { if (isMobileOrSmallScreen()) return; const containerWidth = getContainerWidth(containerRef.current); const config = { minLeftWidth: 300, minRightWidth: 400, containerWidth: containerWidth - 48 - 24 - 8 // 减去左右padding(48px)、gap(24px)和分隔条宽度(8px) }; const { leftWidth, rightWidth } = calculateWidthsFromRatio(panelRatio, config.containerWidth, config); setResizeState({ isDragging: true, startX: e2.clientX, startLeftWidth: leftWidth, startRightWidth: rightWidth }); document.body.classList.add("resizing"); }; const handleResizeMove = (e2) => { if (!resizeState.isDragging || isMobileOrSmallScreen()) return; const containerWidth = getContainerWidth(containerRef.current); const config = { minLeftWidth: 300, minRightWidth: 400, containerWidth: containerWidth - 48 - 24 - 8 // 减去左右padding(48px)、gap(24px)和分隔条宽度(8px) }; const { leftWidth, rightWidth } = calculateNewWidths(e2.clientX, resizeState, config); const newRatio = calculateRatio(leftWidth, rightWidth); setPanelRatio(newRatio); setPanelWidths({ leftWidth, rightWidth }); }; const handleResizeEnd = () => { if (!resizeState.isDragging) return; setResizeState((prev) => ({ ...prev, isDragging: false })); document.body.classList.remove("resizing"); }; y(() => { if (resizeState.isDragging) { document.addEventListener("mousemove", handleResizeMove); document.addEventListener("mouseup", handleResizeEnd); return () => { document.removeEventListener("mousemove", handleResizeMove); document.removeEventListener("mouseup", handleResizeEnd); }; } }, [resizeState.isDragging, resizeState]); const getScriptProgress = (scriptId) => { const script = appState.scripts.find((s2) => s2.id === scriptId); return (script == null ? void 0 : script.progress) || 0; }; const toolScripts = appState.scripts.filter((script) => script.category === "tool"); const operationScripts = appState.scripts.filter((script) => script.category === "operation"); return /* @__PURE__ */ u$1("div", { class: "script-manager", ref: containerRef, children: /* @__PURE__ */ u$1("div", { class: "script-manager-content", children: [ /* @__PURE__ */ u$1( "div", { class: "scripts-panel", style: { width: isMobileOrSmallScreen() ? "auto" : `${panelWidths.leftWidth}px` }, children: [ /* @__PURE__ */ u$1("div", { class: "scripts-section", children: [ /* @__PURE__ */ u$1("h2", { children: "🔧 工具脚本 (日志输出目标信息)" }), /* @__PURE__ */ u$1("div", { class: "scripts-list", children: toolScripts.map((script) => /* @__PURE__ */ u$1( ScriptCard, { script, onExecute: handleExecuteScript, onStop: handleStopScript, isRunning: script.isRunning, progress: getScriptProgress(script.id), favoriteList: appState.favoriteList, favoriteListLoading: appState.favoriteListLoading, favoriteListError: appState.favoriteListError, onRetryFavoriteList: loadFavoriteList }, script.id )) }) ] }), /* @__PURE__ */ u$1("div", { class: "scripts-section", children: [ /* @__PURE__ */ u$1("h2", { children: "⚙️ 操作脚本 (不需要输出信息,正确执行完毕后即达成目标)" }), /* @__PURE__ */ u$1("div", { class: "scripts-list", children: operationScripts.map((script) => /* @__PURE__ */ u$1( ScriptCard, { script, onExecute: handleExecuteScript, onStop: handleStopScript, isRunning: script.isRunning, progress: getScriptProgress(script.id), favoriteList: appState.favoriteList, favoriteListLoading: appState.favoriteListLoading, favoriteListError: appState.favoriteListError, onRetryFavoriteList: loadFavoriteList }, script.id )) }) ] }) ] } ), !isMobileOrSmallScreen() && /* @__PURE__ */ u$1( ResizeHandle, { onMouseDown: handleResizeStart, isDragging: resizeState.isDragging } ), /* @__PURE__ */ u$1( "div", { class: "logs-panel", style: { width: isMobileOrSmallScreen() ? "auto" : `${panelWidths.rightWidth}px` }, children: /* @__PURE__ */ u$1( LogPanel, { logs: appState.logs, onClear: handleClearLogs } ) } ) ] }) }); } function TaskPanel(_props = {}) { return /* @__PURE__ */ u$1("div", { class: "task-panel", children: /* @__PURE__ */ u$1(ScriptManager, {}) }); } function App() { const [isModalOpen, setIsModalOpen] = d(false); const handleOpenModal = () => { setIsModalOpen(true); }; const handleCloseModal = () => { setIsModalOpen(false); }; return /* @__PURE__ */ u$1(preact.Fragment, { children: [ !isModalOpen && /* @__PURE__ */ u$1(FloatingButton, { onClick: handleOpenModal }), /* @__PURE__ */ u$1(Modal, { isOpen: isModalOpen, onClose: handleCloseModal, children: /* @__PURE__ */ u$1(TaskPanel, {}) }) ] }); } preact.render( /* @__PURE__ */ u$1(App, {}), (() => { const app = document.createElement("div"); app.id = "bili-tasks-app"; document.body.append(app); return app; })() ); })(preact);