// ==UserScript== // @name SharePoint Stream Helper // @namespace https://github.com/sp-stream-helper // @version 2.0 // @description 一键提交 SharePoint Stream 视频到转录+总结服务 // @match https://*.sharepoint.com/personal/*/_layouts/*/stream.aspx* // @match https://*.sharepoint.com/sites/*/_layouts/*/stream.aspx* // @grant none // @run-at document-start // ==/UserScript== (function () { 'use strict'; let capturedManifest = null; let btnReady = false; // ======================================== // Settings (stored via GM_setValue) // ======================================== const DEFAULTS = { apiEndpoint: '', apiKey: '', }; function getSetting(key) { try { return localStorage.getItem('sp_helper_' + key) || DEFAULTS[key]; } catch { return DEFAULTS[key]; } } function setSetting(key, val) { try { localStorage.setItem('sp_helper_' + key, val); } catch {} } // ======================================== // Intercept fetch/XHR at document-start // ======================================== const origFetch = window.fetch; window.fetch = function (input, init) { try { const url = typeof input === 'string' ? input : (input?.url || ''); const method = (init?.method || input?.method || 'GET').toUpperCase(); const headers = extractHeaders(init?.headers || input?.headers); maybeCapture(method, url, headers); } catch (e) {} return origFetch.apply(this, arguments); }; const origOpen = XMLHttpRequest.prototype.open; const origSetHeader = XMLHttpRequest.prototype.setRequestHeader; const origSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function (method, url) { this._spH = { method: (method || 'GET').toUpperCase(), url: url || '', headers: {} }; return origOpen.apply(this, arguments); }; XMLHttpRequest.prototype.setRequestHeader = function (name, value) { if (this._spH) this._spH.headers[name] = value; return origSetHeader.apply(this, arguments); }; XMLHttpRequest.prototype.send = function () { if (this._spH) maybeCapture(this._spH.method, this._spH.url, this._spH.headers); return origSend.apply(this, arguments); }; function extractHeaders(raw) { const h = {}; if (!raw) return h; if (raw instanceof Headers) raw.forEach((v, k) => { h[k] = v; }); else if (Array.isArray(raw)) raw.forEach(([k, v]) => { h[k] = v; }); else Object.keys(raw).forEach(k => { h[k] = raw[k]; }); return h; } function maybeCapture(method, url, headers) { if (!url.includes('videomanifest') || method === 'OPTIONS') return; if (!Object.keys(headers).some(k => k.toLowerCase() === 'x-spopactoken')) return; capturedManifest = { method, url, headers: { ...headers }, time: new Date().toLocaleTimeString() }; console.log('[SP Helper] ✅ Captured videomanifest'); if (btnReady) updateUI(); } // ======================================== // Generate cURL // ======================================== function toCurl(req) { let pacToken = '', origin = '', referer = ''; for (const [k, v] of Object.entries(req.headers)) { const l = k.toLowerCase(); if (l === 'x-spopactoken') pacToken = v; if (l === 'origin') origin = v; if (l === 'referer') referer = v; } const parts = [`curl '${req.url}'`]; parts.push(` -H 'accept: */*'`); parts.push(` -H 'accept-language: zh-CN,zh;q=0.9'`); if (origin) parts.push(` -H 'origin: ${origin}'`); parts.push(` -H 'priority: u=1, i'`); if (referer) parts.push(` -H 'referer: ${referer}'`); parts.push(` -H 'sec-ch-ua: "Chromium";v="146", "Not-A.Brand";v="24", "Google Chrome";v="146"'`); parts.push(` -H 'sec-ch-ua-mobile: ?0'`); parts.push(` -H 'sec-ch-ua-platform: "Windows"'`); parts.push(` -H 'sec-fetch-dest: empty'`); parts.push(` -H 'sec-fetch-mode: cors'`); parts.push(` -H 'sec-fetch-site: cross-site'`); parts.push(` -H 'user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36'`); if (pacToken) parts.push(` -H 'x-spopactoken: ${pacToken.replace(/'/g, "'\\''")}'`); return parts.join(' \\\n'); } // ======================================== // UI // ======================================== function createUI() { const container = document.createElement('div'); container.id = 'sp-helper-ui'; container.innerHTML = `

⚙️ SP Stream Helper 设置

`; document.body.appendChild(container); // Load settings document.getElementById('sp-set-endpoint').value = getSetting('apiEndpoint'); document.getElementById('sp-set-apikey').value = getSetting('apiKey'); // Events document.getElementById('sp-btn-copy').addEventListener('click', onCopy); document.getElementById('sp-btn-submit').addEventListener('click', () => { document.getElementById('sp-submit-panel').style.display = 'block'; }); document.getElementById('sp-submit-go').addEventListener('click', onSubmit); document.getElementById('sp-submit-cancel').addEventListener('click', () => { document.getElementById('sp-submit-panel').style.display = 'none'; }); document.getElementById('sp-btn-settings').addEventListener('click', () => { const p = document.getElementById('sp-helper-panel'); p.style.display = p.style.display === 'none' ? 'block' : 'none'; }); document.getElementById('sp-set-save').addEventListener('click', () => { setSetting('apiEndpoint', document.getElementById('sp-set-endpoint').value.trim()); setSetting('apiKey', document.getElementById('sp-set-apikey').value.trim()); document.getElementById('sp-helper-panel').style.display = 'none'; updateUI(); }); document.getElementById('sp-set-close').addEventListener('click', () => { document.getElementById('sp-helper-panel').style.display = 'none'; }); btnReady = true; } function updateUI() { const btnCopy = document.getElementById('sp-btn-copy'); const btnSubmit = document.getElementById('sp-btn-submit'); if (!btnCopy) return; if (capturedManifest) { btnCopy.className = 'sp-btn ready'; btnCopy.innerHTML = `📋 复制 cURL ${capturedManifest.time}`; if (getSetting('apiEndpoint') && getSetting('apiKey')) { btnSubmit.style.display = 'inline-block'; } } } function onCopy() { if (!capturedManifest) return; const curl = toCurl(capturedManifest); navigator.clipboard.writeText(curl).catch(() => { const ta = document.createElement('textarea'); ta.value = curl; ta.style.cssText = 'position:fixed;left:-9999px'; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); ta.remove(); }); flashBtn('sp-btn-copy', '✅ 已复制!'); } async function onSubmit() { if (!capturedManifest) return; const endpoint = getSetting('apiEndpoint'); const apiKey = getSetting('apiKey'); const cookie = document.getElementById('sp-submit-cookie').value.trim(); const lang = document.getElementById('sp-submit-lang').value; if (!endpoint || !apiKey) { alert('请先在设置中填写 API Endpoint 和 API Key'); return; } if (!cookie) { alert('请粘贴 Cookie-Editor 导出的 Cookie JSON'); return; } const btn = document.getElementById('sp-submit-go'); btn.textContent = '⏳ 提交中...'; btn.disabled = true; try { const curl = toCurl(capturedManifest); const resp = await fetch(endpoint.replace(/\/$/, '') + '/submit', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ api_key: apiKey, curl, cookies: cookie, language: lang }), }); const data = await resp.json(); if (resp.ok) { document.getElementById('sp-submit-panel').style.display = 'none'; alert(`任务已提交!\nJob ID: ${data.job_id}\n完成后会发送邮件通知。`); } else { alert(`提交失败: ${data.error || resp.statusText}`); } } catch (e) { alert(`提交失败: ${e.message}`); } finally { btn.textContent = '🚀 提交'; btn.disabled = false; } } function flashBtn(id, text) { const btn = document.getElementById(id); if (!btn) return; const prev = btn.innerHTML; const prevBg = btn.style.background; btn.innerHTML = text; btn.style.background = '#107c10'; setTimeout(() => { btn.innerHTML = prev; btn.style.background = prevBg; }, 2000); } // ======================================== // Init // ======================================== if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } function init() { createUI(); if (capturedManifest) updateUI(); // PerformanceObserver fallback try { new PerformanceObserver((list) => { for (const e of list.getEntries()) { if (e.name.includes('videomanifest') && !capturedManifest) { const pac = getPacToken(); if (pac) { capturedManifest = { method: 'GET', url: e.name, headers: { 'x-spopactoken': pac }, time: new Date().toLocaleTimeString() }; updateUI(); } } } }).observe({ type: 'resource', buffered: true }); } catch (e) {} console.log('[SP Helper] v2.0 loaded'); } function getPacToken() { try { return window.g_fileInfo?.['.driveAccessCode'] || window.g_fileInfo?.['.driveAccessCodeV21']; } catch { return null; } } })();