// ==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 = `
🚀 提交转录任务
`;
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; }
}
})();