| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317 |
- // ==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 = `
- <style>
- #sp-helper-ui { position:fixed; bottom:20px; right:20px; z-index:2147483647; font-family:system-ui,sans-serif; font-size:13px; }
- #sp-helper-ui .sp-btn { padding:10px 16px; border-radius:8px; border:none; cursor:pointer; color:#fff; font-size:13px;
- box-shadow:0 2px 12px rgba(0,0,0,0.3); transition:all 0.2s; margin-left:8px; }
- #sp-helper-ui .sp-btn:hover { filter:brightness(1.1); }
- #sp-helper-ui .sp-btn.wait { background:#555; cursor:not-allowed; }
- #sp-helper-ui .sp-btn.ready { background:#0078d4; }
- #sp-helper-ui .sp-btn.ok { background:#107c10; }
- #sp-helper-ui .sp-btn.submit { background:#d83b01; }
- #sp-helper-panel { display:none; position:fixed; bottom:70px; right:20px; z-index:2147483647; background:#fff; border-radius:12px;
- box-shadow:0 4px 24px rgba(0,0,0,0.2); padding:20px; width:360px; font-family:system-ui,sans-serif; font-size:13px; }
- #sp-helper-panel h3 { margin:0 0 12px; font-size:15px; }
- #sp-helper-panel label { display:block; margin:8px 0 4px; font-weight:600; }
- #sp-helper-panel input, #sp-helper-panel textarea { width:100%; padding:6px 8px; border:1px solid #ccc; border-radius:4px;
- font-size:12px; box-sizing:border-box; font-family:Consolas,monospace; }
- #sp-helper-panel textarea { height:60px; resize:vertical; }
- #sp-helper-panel .sp-row { display:flex; gap:8px; margin-top:12px; }
- #sp-helper-panel .sp-row button { flex:1; padding:8px; border:none; border-radius:6px; cursor:pointer; font-size:13px; color:#fff; }
- </style>
- <div style="display:flex;align-items:center;">
- <button id="sp-btn-copy" class="sp-btn wait">⏳ 等待播放...</button>
- <button id="sp-btn-submit" class="sp-btn submit" style="display:none">🚀 提交转录</button>
- <button id="sp-btn-settings" class="sp-btn" style="background:#666;padding:10px 12px" title="设置">⚙️</button>
- </div>
- <div id="sp-helper-panel">
- <h3>⚙️ SP Stream Helper 设置</h3>
- <label>API Endpoint</label>
- <input id="sp-set-endpoint" placeholder="https://xxx.execute-api.ap-northeast-1.amazonaws.com">
- <label>API Key</label>
- <input id="sp-set-apikey" type="password" placeholder="your-api-key">
- <div class="sp-row">
- <button id="sp-set-save" style="background:#0078d4">💾 保存</button>
- <button id="sp-set-close" style="background:#888">关闭</button>
- </div>
- </div>
- <div id="sp-submit-panel" style="display:none;position:fixed;bottom:70px;right:20px;z-index:2147483647;background:#fff;border-radius:12px;box-shadow:0 4px 24px rgba(0,0,0,0.2);padding:20px;width:380px;font-family:system-ui,sans-serif;font-size:13px;">
- <h3 style="margin:0 0 12px;font-size:15px;">🚀 提交转录任务</h3>
- <label style="display:block;margin:8px 0 4px;font-weight:600;">Cookie (从 Cookie-Editor 导出)</label>
- <textarea id="sp-submit-cookie" style="width:100%;height:80px;padding:6px 8px;border:1px solid #ccc;border-radius:4px;font-size:11px;box-sizing:border-box;font-family:Consolas,monospace;resize:vertical;" placeholder="粘贴 Cookie-Editor 导出的 JSON"></textarea>
- <label style="display:block;margin:8px 0 4px;font-weight:600;">会议语言</label>
- <select id="sp-submit-lang" style="width:100%;padding:6px 8px;border:1px solid #ccc;border-radius:4px;font-size:13px;">
- <option value="auto">自动识别</option>
- <option value="zh-CN" selected>中文 (简体)</option>
- <option value="zh-TW">中文 (繁体)</option>
- <option value="zh-HK">中文 (粤语)</option>
- <option value="en-US">English (US)</option>
- </select>
- <div style="display:flex;gap:8px;margin-top:12px;">
- <button id="sp-submit-go" style="flex:1;padding:8px;border:none;border-radius:6px;cursor:pointer;font-size:13px;color:#fff;background:#d83b01;">🚀 提交</button>
- <button id="sp-submit-cancel" style="flex:1;padding:8px;border:none;border-radius:6px;cursor:pointer;font-size:13px;color:#fff;background:#888;">取消</button>
- </div>
- </div>`;
- 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 <span style="font-size:11px;opacity:0.6">${capturedManifest.time}</span>`;
- 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; }
- }
- })();
|