等待输入。
格式:sub2api
账号:0
跳过:0
暂无输出。
`;
document.documentElement.append(host);
elements = {
fab: shadow.querySelector(".fab"),
panel: shadow.querySelector(".panel"),
input: shadow.querySelector('[data-role="input"]'),
output: shadow.querySelector('[data-role="output"]'),
inputStatus: shadow.querySelector('[data-role="input-status"]'),
outputStatus: shadow.querySelector('[data-role="output-status"]'),
format: shadow.querySelector('[data-role="format"]'),
count: shadow.querySelector('[data-role="count"]'),
errors: shadow.querySelector('[data-role="errors"]'),
accounts: shadow.querySelector('[data-role="accounts"]'),
issues: shadow.querySelector('[data-role="issues"]'),
copy: shadow.querySelector('[data-action="copy"]'),
download: shadow.querySelector('[data-action="download"]'),
fileInput: shadow.querySelector('input[type="file"]'),
formatButtons: Array.from(shadow.querySelectorAll("[data-format]")),
};
bindUiEvents();
updateOutput();
}
function bindUiEvents() {
shadow.addEventListener("click", (event) => {
const button = event.target.closest("button");
if (!button) {
return;
}
const action = button.dataset.action;
if (action === "toggle") {
togglePanel();
} else if (action === "close") {
closePanel();
} else if (action === "fetch") {
fetchCurrentSession();
} else if (action === "read-page") {
readJsonFromCurrentPage();
} else if (action === "pick-files") {
elements.fileInput.click();
} else if (action === "clear") {
clearInput();
} else if (action === "copy") {
copyOutput();
} else if (action === "download") {
downloadOutput();
}
});
elements.input.addEventListener("input", scheduleConvert);
elements.fileInput.addEventListener("change", (event) => {
readFiles(event.target.files);
event.target.value = "";
});
elements.formatButtons.forEach((button) => {
button.addEventListener("click", () => {
state.format = button.dataset.format;
elements.formatButtons.forEach((item) => {
item.setAttribute("aria-pressed", String(item === button));
});
updateOutput();
});
});
}
function openPanel() {
ensureUi();
elements.panel.classList.add("is-open");
elements.fab.style.display = "none";
}
function closePanel() {
ensureUi();
elements.panel.classList.remove("is-open");
elements.fab.style.display = "";
}
function togglePanel() {
ensureUi();
if (elements.panel.classList.contains("is-open")) {
closePanel();
} else {
openPanel();
}
}
function setStatus(element, text, tone = "") {
element.textContent = text;
element.classList.toggle("ok", tone === "ok");
element.classList.toggle("error", tone === "error");
}
function clearInput() {
elements.input.value = "";
state.converted = [];
state.skipped = [];
state.sessions = [];
updateOutput();
setStatus(elements.inputStatus, "等待输入。");
}
function scheduleConvert() {
const text = elements.input.value;
if (!text.trim()) {
clearInput();
return;
}
try {
convertFromText(text);
if (state.converted.length) {
setStatus(elements.inputStatus, `解析完成:${state.converted.length} 个账号,跳过 ${state.skipped.length} 项。`, "ok");
} else {
setStatus(elements.inputStatus, "没有可转换账号。", "error");
}
} catch (error) {
state.converted = [];
state.skipped = [{
sourceName: "pasted-json",
path: "$",
reason: error instanceof Error ? error.message : "JSON 解析失败",
}];
state.outputText = "";
updateOutput();
setStatus(elements.inputStatus, error instanceof Error ? error.message : "JSON 解析失败", "error");
}
}
function updateOutput() {
const hasConverted = state.converted.length > 0;
const outputText = hasConverted ? JSON.stringify(buildOutputDocument(), null, 2) : "";
state.outputText = outputText;
elements.output.value = outputText;
elements.copy.disabled = !outputText;
elements.download.disabled = !outputText;
elements.format.textContent = OUTPUT_LABELS[state.format];
elements.count.textContent = String(state.converted.length);
elements.errors.textContent = String(state.skipped.length);
renderAccounts();
renderIssues();
if (outputText) {
setStatus(elements.outputStatus, `已生成 ${state.converted.length} 个账号的 ${OUTPUT_LABELS[state.format]} JSON。`, "ok");
} else {
setStatus(elements.outputStatus, "暂无输出。", state.skipped.length ? "error" : "");
}
}
function renderAccounts() {
elements.accounts.textContent = "";
if (!state.converted.length) {
const row = document.createElement("tr");
const cell = document.createElement("td");
cell.colSpan = 3;
cell.textContent = "暂无可转换账号。";
row.append(cell);
elements.accounts.append(row);
return;
}
state.converted.forEach((item) => {
const row = document.createElement("tr");
[item.name || "-", item.email || "-", formatDisplayDate(item.expiresAt) || "-"].forEach((text) => {
const cell = document.createElement("td");
cell.textContent = text;
cell.title = text;
row.append(cell);
});
elements.accounts.append(row);
});
}
function renderIssues() {
elements.issues.textContent = "";
elements.issues.classList.toggle("is-visible", state.skipped.length > 0);
state.skipped.forEach((item) => {
const line = document.createElement("div");
line.textContent = `${item.sourceName || "input"} ${item.path || ""}: ${item.reason}`;
elements.issues.append(line);
});
}
async function fetchCurrentSession() {
openPanel();
setStatus(elements.inputStatus, "正在读取当前 ChatGPT 登录 session...");
try {
const response = await fetch(SESSION_ENDPOINT, {
credentials: "include",
cache: "no-store",
headers: { Accept: "application/json" },
});
const text = await response.text();
if (!response.ok) {
throw new Error(`读取失败:HTTP ${response.status}`);
}
loadText(text, "chatgpt-session");
} catch (error) {
setStatus(elements.inputStatus, error instanceof Error ? error.message : "读取 session 失败", "error");
}
}
function readJsonFromCurrentPage() {
openPanel();
try {
const pre = document.querySelector("pre");
const text = (pre || document.body)?.textContent || "";
if (!text.trim()) {
throw new Error("当前页面没有可读取的 JSON 文本");
}
loadText(text, location.pathname.includes("/api/auth/session") ? "chatgpt-session" : "current-page");
} catch (error) {
setStatus(elements.inputStatus, error instanceof Error ? error.message : "读取本页 JSON 失败", "error");
}
}
function loadText(text, sourceName) {
elements.input.value = text.trim();
try {
convertFromText(elements.input.value, sourceName);
if (state.converted.length) {
setStatus(elements.inputStatus, `解析完成:${state.converted.length} 个账号,跳过 ${state.skipped.length} 项。`, "ok");
} else {
setStatus(elements.inputStatus, "没有可转换账号。", "error");
}
} catch (error) {
state.converted = [];
state.skipped = [{
sourceName,
path: "$",
reason: error instanceof Error ? error.message : "JSON 解析失败",
}];
updateOutput();
setStatus(elements.inputStatus, error instanceof Error ? error.message : "JSON 解析失败", "error");
}
}
async function readFiles(files) {
const jsonFiles = Array.from(files || []).filter((file) => file.name.toLowerCase().endsWith(".json"));
if (!jsonFiles.length) {
setStatus(elements.inputStatus, "没有选择 JSON 文件。", "error");
return;
}
const documents = [];
const skipped = [];
for (const file of jsonFiles) {
try {
const text = await file.text();
const parsed = JSON.parse(text);
const found = collectSessionLikeObjects(parsed, file.webkitRelativePath || file.name);
if (!found.length) {
skipped.push({
sourceName: file.webkitRelativePath || file.name,
path: "$",
reason: "未找到包含 accessToken 和 user/email 的 session 对象",
});
}
documents.push(...found);
} catch (error) {
skipped.push({
sourceName: file.webkitRelativePath || file.name,
path: "$",
reason: error instanceof Error ? error.message : "无法读取文件",
});
}
}
const now = new Date();
const converted = [];
const convertSkipped = [...skipped];
documents.forEach((item) => {
try {
converted.push(convertSession(item.value, {
now,
sourceName: item.sourceName,
sourcePath: item.path,
}));
} catch (error) {
convertSkipped.push({
sourceName: item.sourceName,
path: item.path,
reason: error instanceof Error ? error.message : "无法转换",
});
}
});
state.sessions = documents;
state.converted = converted;
state.skipped = convertSkipped;
elements.input.value = documents.length === 1
? JSON.stringify(documents[0].value, null, 2)
: JSON.stringify(documents.map((item) => item.value), null, 2);
updateOutput();
setStatus(elements.inputStatus, `读取 ${jsonFiles.length} 个文件,生成 ${converted.length} 个账号,跳过 ${convertSkipped.length} 项。`, converted.length ? "ok" : "error");
}
async function copyOutput() {
if (!state.outputText) {
return;
}
try {
if (typeof GM_setClipboard === "function") {
GM_setClipboard(state.outputText, "text");
} else {
await navigator.clipboard.writeText(state.outputText);
}
setStatus(elements.outputStatus, "已复制到剪贴板。", "ok");
} catch {
elements.output.select();
document.execCommand("copy");
setStatus(elements.outputStatus, "已复制到剪贴板。", "ok");
}
}
function downloadOutput() {
if (!state.outputText) {
return;
}
const first = state.converted[0];
const base = sanitizeFileToken(first?.email || first?.name || state.format);
const fileName = `${base}.${state.format}.${getTimestampToken()}.json`;
const blob = new Blob([state.outputText], { type: "application/json;charset=utf-8" });
const url = URL.createObjectURL(blob);
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = fileName;
document.body.append(anchor);
anchor.click();
anchor.remove();
setTimeout(() => URL.revokeObjectURL(url), 1000);
}
function init() {
ensureUi();
if (typeof GM_registerMenuCommand === "function") {
GM_registerMenuCommand("打开 GPT Session 转换器", openPanel);
GM_registerMenuCommand("读取当前 ChatGPT session", fetchCurrentSession);
}
if (location.pathname.includes("/api/auth/session")) {
openPanel();
readJsonFromCurrentPage();
}
}
init();
})();