SessionToJson

Read ChatGPT session JSON and convert it to CPA/Codex auth JSON.

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         SessionToJson
// @namespace    local.session-to-json
// @version      1.0.0
// @license      MIT
// @description  Read ChatGPT session JSON and convert it to CPA/Codex auth JSON.
// @match        *://*/*
// @match        file:///*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_setClipboard
// @grant        GM_xmlhttpRequest
// @connect      chatgpt.com
// @connect      localhost
// @connect      127.0.0.1
// @run-at       document-idle
// ==/UserScript==

(function () {
  'use strict';

  if (window.top !== window.self) {
    return;
  }

  const DEFAULT_SERVER_BASE_URL = 'http://localhost:8317';
  const DEFAULT_CPA_PASSWORD = 'admin123';
  const SERVER_BASE_URL_STORAGE_KEY = 'serverBaseUrl';
  const CPA_PASSWORD_STORAGE_KEY = 'cpaPassword';
  const UPLOAD_PATH = '/v0/management/auth-files';
  const CHATGPT_SESSION_URL = 'https://chatgpt.com/api/auth/session';
  const CHATGPT_PLUS_ELIGIBILITY_URL = 'https://chatgpt.com/backend-api/accounts/check/v4-2023-04-27?timezone_offset_min=-480';
  const CHATGPT_PLUS_PLAN_ID = 'chatgptplusplan';
  const CONVERSION_RULES = [
    { from: 'accessToken', to: 'access_token' },
    { from: 'account.id', to: 'account_id' },
    { from: 'user.email', to: 'email' },
    { from: 'expires', to: 'expired' },
    { from: 'account.id', to: 'chatgpt_account_id' },
    { from: 'account.planType', to: 'plan_type' },
    { from: 'account.planType', to: 'chatgpt_plan_type' },
    { from: 'sessionToken', to: 'session_token' },
    { to: 'last_refresh', value: '' },
    { to: 'refresh_token', value: '' },
    { to: 'type', value: 'codex' },
    { to: 'disabled', value: false },
    { to: 'id_token_synthetic', value: true }
  ];

  const elements = createApp();

  elements.toggleButton.addEventListener('click', openApp);
  elements.backdrop.addEventListener('click', closeApp);
  elements.closeButton.addEventListener('click', closeApp);
  document.addEventListener('keydown', closeAppOnEscape);
  elements.fetchChatGptSessionButton.addEventListener('click', fetchChatGptSessionJson);
  elements.readPageButton.addEventListener('click', readPageJson);
  elements.convertButton.addEventListener('click', convertJson);
  elements.copyButton.addEventListener('click', copyResult);
  elements.downloadButton.addEventListener('click', downloadResult);
  elements.uploadButton.addEventListener('click', uploadResult);
  elements.toggleCpaPasswordButton.addEventListener('click', toggleCpaPasswordVisibility);
  elements.serverUrlInput.addEventListener('change', saveServerBaseUrl);
  elements.serverUrlInput.addEventListener('blur', saveServerBaseUrl);
  elements.cpaPasswordInput.addEventListener('change', saveCpaPassword);
  elements.cpaPasswordInput.addEventListener('blur', saveCpaPassword);
  initializeStoredValues();

  function createApp() {
    const host = document.createElement('div');
    const shadow = host.attachShadow({ mode: 'closed' });

    host.id = 'session-to-json-userscript-root';
    shadow.innerHTML = `
      <style>
        :host {
          all: initial;
          color-scheme: dark;
          font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
        }

        * {
          box-sizing: border-box;
        }

        button,
        input,
        textarea {
          font: inherit;
        }

        .launcher {
          position: fixed;
          right: 18px;
          bottom: 18px;
          z-index: 2147483647;
          min-height: 38px;
          border: 1px solid rgba(75, 85, 99, 0.72);
          border-radius: 999px;
          padding: 0 16px;
          background: #111827;
          color: #f9fafb;
          cursor: pointer;
          box-shadow: 0 12px 32px rgba(0, 0, 0, 0.35);
          font-size: 13px;
          font-weight: 700;
        }

        .backdrop {
          position: fixed;
          inset: 0;
          z-index: 2147483646;
          background: rgba(3, 7, 18, 0.62);
          backdrop-filter: blur(3px);
        }

        .backdrop[hidden] {
          display: none;
        }

        .app {
          position: fixed;
          top: 50%;
          left: 50%;
          z-index: 2147483647;
          display: grid;
          gap: 12px;
          width: min(640px, calc(100vw - 36px));
          max-height: calc(100vh - 36px);
          overflow: auto;
          padding: 14px;
          border: 1px solid rgba(75, 85, 99, 0.72);
          border-radius: 16px;
          background: linear-gradient(145deg, #050816 0%, #0b1020 52%, #111827 100%);
          color: #e5e7eb;
          box-shadow: 0 24px 60px rgba(0, 0, 0, 0.45);
          transform: translate(-50%, -50%);
        }

        .app[hidden] {
          display: none;
        }

        .header {
          display: grid;
          gap: 10px;
          padding: 12px;
          border: 1px solid rgba(75, 85, 99, 0.6);
          border-radius: 14px;
          background: rgba(17, 24, 39, 0.88);
        }

        .header-top {
          display: flex;
          align-items: center;
          justify-content: space-between;
          gap: 10px;
        }

        h1 {
          margin: 0;
          color: #f9fafb;
          font-size: 20px;
          line-height: 1.1;
          letter-spacing: 0.02em;
        }

        .header-actions,
        .actions {
          display: flex;
          flex-wrap: wrap;
          justify-content: center;
          gap: 8px;
        }

        .panel {
          display: grid;
          gap: 7px;
          padding: 12px;
          border: 1px solid rgba(75, 85, 99, 0.58);
          border-radius: 14px;
          background: rgba(17, 24, 39, 0.78);
          box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03);
        }

        label {
          color: #d1d5db;
          font-size: 12px;
          font-weight: 700;
          letter-spacing: 0.04em;
        }

        input,
        textarea {
          width: 100%;
          border: 1px solid rgba(75, 85, 99, 0.72);
          border-radius: 10px;
          padding: 10px;
          background: rgba(3, 7, 18, 0.7);
          color: #f3f4f6;
          font-family: "Cascadia Code", Consolas, monospace;
          font-size: 12px;
          line-height: 1.5;
          outline: none;
          caret-color: #e5e7eb;
        }

        textarea {
          min-height: 150px;
          resize: vertical;
        }

        input {
          min-height: 38px;
          font-family: inherit;
        }

        input::placeholder,
        textarea::placeholder {
          color: #6b7280;
        }

        input:focus,
        textarea:focus {
          border-color: #9ca3af;
          box-shadow: 0 0 0 3px rgba(156, 163, 175, 0.16);
        }

        .password-field {
          display: grid;
          grid-template-columns: 1fr auto;
          gap: 8px;
        }

        button {
          min-height: 36px;
          border: 1px solid rgba(75, 85, 99, 0.72);
          border-radius: 10px;
          padding: 0 12px;
          background: rgba(31, 41, 55, 0.88);
          color: #e5e7eb;
          cursor: pointer;
          font-size: 12px;
          box-shadow: 0 8px 18px rgba(0, 0, 0, 0.2);
        }

        button:hover {
          border-color: #9ca3af;
          background: rgba(55, 65, 81, 0.92);
          color: #f9fafb;
        }

        button.primary {
          border-color: #4b5563;
          background: #374151;
          color: #ffffff;
          font-weight: 700;
        }

        .status {
          min-height: 20px;
          margin: 0;
          padding: 8px 10px;
          border: 1px solid rgba(75, 85, 99, 0.52);
          border-radius: 10px;
          background: rgba(17, 24, 39, 0.72);
          color: #d1d5db;
          font-size: 12px;
          line-height: 1.45;
        }

        .status.error {
          border-color: rgba(248, 113, 113, 0.5);
          color: #fca5a5;
        }
      </style>
      <button id="toggleButton" class="launcher" type="button">SessionToJson</button>
      <div id="backdrop" class="backdrop" hidden></div>
      <main id="app" class="app" aria-label="SessionToJson" role="dialog" aria-modal="true" hidden>
        <header class="header">
          <div class="header-top">
            <h1>SessionToJson</h1>
            <button id="closeButton" type="button">收起</button>
          </div>
          <div class="header-actions">
            <button id="fetchChatGptSessionButton" type="button">一键获取 Session</button>
            <button id="readPageButton" type="button">读取页面 JSON</button>
          </div>
        </header>

        <section class="panel">
          <label for="pageJson">页面 JSON</label>
          <textarea id="pageJson" spellcheck="false" placeholder="打开正文为 JSON 的页面,然后点击读取页面 JSON。"></textarea>
        </section>

        <div class="actions">
          <button id="convertButton" type="button" class="primary">转换</button>
        </div>

        <section class="panel">
          <label for="outputJson">输出 JSON</label>
          <textarea id="outputJson" spellcheck="false" readonly placeholder="转换后的 JSON 会显示在这里。"></textarea>
        </section>

        <section class="panel">
          <label for="serverUrl">服务器地址</label>
          <input id="serverUrl" type="url" spellcheck="false" placeholder="http://localhost:8317">
        </section>

        <section class="panel">
          <label for="cpaPassword">CPA 密码</label>
          <div class="password-field">
            <input id="cpaPassword" type="password" spellcheck="false" placeholder="请输入 CPA 密码">
            <button id="toggleCpaPasswordButton" type="button" aria-label="显示 CPA 密码">显示</button>
          </div>
        </section>

        <p id="status" class="status" role="status" aria-live="polite"></p>

        <div class="actions">
          <button id="copyButton" type="button">复制结果</button>
          <button id="downloadButton" type="button">下载 JSON</button>
          <button id="uploadButton" type="button">上传到 CPA</button>
        </div>
      </main>
    `;

    document.documentElement.appendChild(host);

    return {
      app: shadow.getElementById('app'),
      backdrop: shadow.getElementById('backdrop'),
      toggleButton: shadow.getElementById('toggleButton'),
      closeButton: shadow.getElementById('closeButton'),
      pageJsonInput: shadow.getElementById('pageJson'),
      outputJsonInput: shadow.getElementById('outputJson'),
      serverUrlInput: shadow.getElementById('serverUrl'),
      cpaPasswordInput: shadow.getElementById('cpaPassword'),
      toggleCpaPasswordButton: shadow.getElementById('toggleCpaPasswordButton'),
      statusElement: shadow.getElementById('status'),
      fetchChatGptSessionButton: shadow.getElementById('fetchChatGptSessionButton'),
      readPageButton: shadow.getElementById('readPageButton'),
      convertButton: shadow.getElementById('convertButton'),
      copyButton: shadow.getElementById('copyButton'),
      downloadButton: shadow.getElementById('downloadButton'),
      uploadButton: shadow.getElementById('uploadButton')
    };
  }

  function openApp() {
    elements.backdrop.hidden = false;
    elements.app.hidden = false;
  }

  function closeApp() {
    elements.app.hidden = true;
    elements.backdrop.hidden = true;
  }

  function closeAppOnEscape(event) {
    if (event.key === 'Escape' && !elements.app.hidden) {
      closeApp();
    }
  }

  async function fetchChatGptSessionJson() {
    setStatus('正在获取 ChatGPT Session...');

    try {
      const session = await requestJson(CHATGPT_SESSION_URL, {}, '获取 ChatGPT Session 失败');

      elements.pageJsonInput.value = JSON.stringify(session, null, 2);
      convertJson();
      await checkChatGptPlusEligibilityForFreePlan(session);
    } catch (error) {
      setError(`${getErrorMessage(error)}请确认已登录 ChatGPT。`);
    }
  }

  async function checkChatGptPlusEligibilityForFreePlan(source) {
    if (readPathOrDefault(source, 'account.planType', '') !== 'free') {
      return;
    }

    await checkChatGptPlusEligibility(source.accessToken);
  }

  async function checkChatGptPlusEligibility(accessToken) {
    if (!accessToken) {
      throw new Error('获取 ChatGPT Session 失败:缺少 accessToken');
    }

    const result = await requestJson(
      CHATGPT_PLUS_ELIGIBILITY_URL,
      { headers: { Authorization: `Bearer ${accessToken}` } },
      '检查 ChatGPT Plus 资格失败'
    );

    setChatGptPlusEligibilityStatus(hasChatGptPlusEligibility(result));
  }

  function hasChatGptPlusEligibility(result) {
    const offers = result && result.accounts && result.accounts.default && result.accounts.default.eligible_offers && Array.isArray(result.accounts.default.eligible_offers.offers)
      ? result.accounts.default.eligible_offers.offers
      : [];
    const plusOffer = offers.find((offer) => offer && offer.id === CHATGPT_PLUS_PLAN_ID);

    return Boolean(plusOffer && plusOffer.epl_enabled);
  }

  function setChatGptPlusEligibilityStatus(hasEligibility) {
    const message = hasEligibility ? '此账户有 ChatGPT Plus 订阅资格。' : '此账户暂无 ChatGPT Plus 订阅资格。';

    setStatus(`${elements.statusElement.textContent}${message}`);
  }

  async function readPageJson() {
    setStatus('正在读取当前页面 JSON...');

    try {
      const text = document.body ? document.body.innerText.trim() : '';
      const source = JSON.parse(text);

      elements.pageJsonInput.value = JSON.stringify(source, null, 2);
      convertJson();
      await checkChatGptPlusEligibilityForFreePlan(source);
    } catch (error) {
      setError(`无法从此页面读取有效 JSON。${getErrorMessage(error)}`);
    }
  }

  function convertJson() {
    try {
      const source = parseJson(elements.pageJsonInput.value);
      const result = convertSessionJson(source);

      elements.outputJsonInput.value = JSON.stringify(result.output, null, 2);
      setConversionStatus(result.warnings);
    } catch (error) {
      setError(getErrorMessage(error));
    }
  }

  async function copyResult() {
    try {
      requireOutput();

      if (typeof GM_setClipboard === 'function') {
        GM_setClipboard(elements.outputJsonInput.value, 'text');
      } else {
        await navigator.clipboard.writeText(elements.outputJsonInput.value);
      }

      setStatus('复制成功。');
    } catch (error) {
      setError(getErrorMessage(error));
    }
  }

  function downloadResult() {
    try {
      requireOutput();

      const source = parseJson(elements.pageJsonInput.value);
      const blob = new Blob([elements.outputJsonInput.value], { type: 'application/json' });
      const url = URL.createObjectURL(blob);
      const link = document.createElement('a');

      link.href = url;
      link.download = buildDownloadFilename(source);
      link.click();
      URL.revokeObjectURL(url);
      setStatus('开始下载。');
    } catch (error) {
      setError(getErrorMessage(error));
    }
  }

  async function uploadResult() {
    try {
      requireOutput();

      const source = parseJson(elements.pageJsonInput.value);
      const password = getCpaPassword(elements.cpaPasswordInput.value);
      const file = new File([elements.outputJsonInput.value], buildDownloadFilename(source), { type: 'application/json' });
      const formData = new FormData();

      formData.append('file', file);

      const response = await request(buildUploadUrl(elements.serverUrlInput.value), {
        method: 'POST',
        headers: { Authorization: `Bearer ${password}` },
        data: formData
      });

      if (!isSuccessStatus(response.status)) {
        throw new Error(`上传失败:HTTP ${response.status}`);
      }

      setStatus('上传成功。');
    } catch (error) {
      setError(getErrorMessage(error));
    }
  }

  function convertSessionJson(source) {
    const output = {};
    const warnings = [];

    for (const rule of CONVERSION_RULES) {
      if (Object.prototype.hasOwnProperty.call(rule, 'value')) {
        writePath(output, rule.to, rule.value);
        continue;
      }

      const resolved = readPath(source, rule.from);
      if (!resolved.found) {
        warnings.push(`Missing value for path "${rule.from}"`);
        continue;
      }

      writePath(output, rule.to, resolved.value);
    }

    writePath(
      output,
      'id_token',
      buildIdToken(
        readPath(source, 'user.email').value,
        readPath(source, 'account.id').value,
        readPath(source, 'account.planType').value,
        readPath(source, 'user.id').value,
        readPath(source, 'expires').value
      )
    );

    return { output, warnings };
  }

  function buildIdToken(email, accountId, planType, userId, expires) {
    if (!accountId) {
      return '';
    }

    const now = Math.trunc(Date.now() / 1000);
    const exp = epochFromValue(expires) || now + 90 * 24 * 60 * 60;
    const authInfo = { chatgpt_account_id: accountId };

    if (planType) {
      authInfo.chatgpt_plan_type = planType;
    }

    if (userId) {
      authInfo.chatgpt_user_id = userId;
      authInfo.user_id = userId;
    }

    const payload = {
      iat: now,
      exp,
      'https://api.openai.com/auth': authInfo
    };

    if (email) {
      payload.email = email;
    }

    return `${genBase64({ alg: 'none', typ: 'JWT', cpa_synthetic: true })}.${genBase64(payload)}.`;
  }

  function epochFromValue(value) {
    if (typeof value === 'number' && Number.isFinite(value)) {
      return Math.trunc(value);
    }

    if (typeof value === 'string' && value.trim()) {
      const numeric = Number(value);
      if (Number.isFinite(numeric)) {
        return Math.trunc(numeric);
      }

      const parsed = Date.parse(value);
      if (Number.isFinite(parsed)) {
        return Math.trunc(parsed / 1000);
      }
    }

    return 0;
  }

  function genBase64(value) {
    const json = JSON.stringify(value);

    return btoa(unescape(encodeURIComponent(json))).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
  }

  function parseJson(text) {
    try {
      return JSON.parse(text);
    } catch (error) {
      throw new Error('页面 JSON 不是有效的 JSON。');
    }
  }

  function buildDownloadFilename(source) {
    const email = sanitizeFilenamePart(readPathOrDefault(source, 'user.email', 'unknown-email'));
    const plan = sanitizeFilenamePart(readPathOrDefault(source, 'account.planType', 'unknown-plan'));

    return `codex-${email}-${plan}.json`;
  }

  function getServerBaseUrl(value) {
    const trimmed = String(value || '').trim();

    return trimmed || DEFAULT_SERVER_BASE_URL;
  }

  function getCpaPassword(value) {
    return String(value || DEFAULT_CPA_PASSWORD).trim();
  }

  function buildUploadUrl(value) {
    const baseUrl = getServerBaseUrl(value);
    let parsed;

    try {
      parsed = new URL(baseUrl);
    } catch (error) {
      throw new Error('服务器地址不是有效的 URL。');
    }

    if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
      throw new Error('服务器地址不是有效的 URL。');
    }

    return `${baseUrl.replace(/\/+$/, '')}${UPLOAD_PATH}`;
  }

  async function initializeStoredValues() {
    elements.serverUrlInput.value = getServerBaseUrl(await getStoredValue(SERVER_BASE_URL_STORAGE_KEY, DEFAULT_SERVER_BASE_URL));
    elements.cpaPasswordInput.value = getCpaPassword(await getStoredValue(CPA_PASSWORD_STORAGE_KEY, DEFAULT_CPA_PASSWORD));
  }

  function saveServerBaseUrl() {
    const value = getServerBaseUrl(elements.serverUrlInput.value);

    elements.serverUrlInput.value = value;
    setStoredValue(SERVER_BASE_URL_STORAGE_KEY, value);
  }

  function saveCpaPassword() {
    const value = String(elements.cpaPasswordInput.value || '').trim();

    elements.cpaPasswordInput.value = value;
    setStoredValue(CPA_PASSWORD_STORAGE_KEY, value);
  }

  function toggleCpaPasswordVisibility() {
    const shouldShow = elements.cpaPasswordInput.type === 'password';

    elements.cpaPasswordInput.type = shouldShow ? 'text' : 'password';
    elements.toggleCpaPasswordButton.textContent = shouldShow ? '隐藏' : '显示';
    elements.toggleCpaPasswordButton.setAttribute('aria-label', shouldShow ? '隐藏 CPA 密码' : '显示 CPA 密码');
  }

  function getStoredValue(key, fallback) {
    if (typeof GM_getValue === 'function') {
      return Promise.resolve(GM_getValue(key, fallback));
    }

    const value = localStorage.getItem(`sessionToJson:${key}`);

    return Promise.resolve(value === null ? fallback : value);
  }

  function setStoredValue(key, value) {
    if (typeof GM_setValue === 'function') {
      GM_setValue(key, value);
      return;
    }

    localStorage.setItem(`sessionToJson:${key}`, value);
  }

  async function requestJson(url, options, errorPrefix) {
    const response = await request(url, { ...options, responseType: 'json' });

    if (!isSuccessStatus(response.status)) {
      throw new Error(`${errorPrefix}:HTTP ${response.status}`);
    }

    if (response.body !== undefined && response.body !== null && response.body !== '') {
      return response.body;
    }

    try {
      return JSON.parse(response.text);
    } catch (error) {
      throw new Error(`${errorPrefix}:响应不是有效 JSON`);
    }
  }

  function request(url, options = {}) {
    const target = new URL(url, location.href);
    const method = options.method || 'GET';
    const headers = options.headers || {};

    if (target.origin === location.origin || typeof GM_xmlhttpRequest !== 'function') {
      return fetch(target.href, {
        method,
        headers,
        body: options.data,
        credentials: 'include'
      }).then(async (response) => {
        const text = await response.text();

        return {
          status: response.status,
          text,
          body: parseResponseBody(text, options.responseType)
        };
      });
    }

    return new Promise((resolve, reject) => {
      GM_xmlhttpRequest({
        method,
        url: target.href,
        headers,
        data: options.data,
        responseType: options.responseType || '',
        anonymous: false,
        onload: (response) => {
          resolve({
            status: response.status,
            text: response.responseText || '',
            body: response.response
          });
        },
        onerror: () => reject(new Error('网络请求失败。')),
        ontimeout: () => reject(new Error('网络请求超时。'))
      });
    });
  }

  function parseResponseBody(text, responseType) {
    if (responseType !== 'json' || !text) {
      return text;
    }

    try {
      return JSON.parse(text);
    } catch (error) {
      return null;
    }
  }

  function isSuccessStatus(status) {
    return status >= 200 && status < 300;
  }

  function readPath(source, path) {
    const parts = path.split('.').filter(Boolean);
    let current = source;

    for (const part of parts) {
      if (!hasOwn(current, part)) {
        return { found: false };
      }

      current = current[part];
    }

    return { found: true, value: current };
  }

  function readPathOrDefault(source, path, fallback) {
    const resolved = readPath(source, path);

    return resolved.found ? String(resolved.value) : fallback;
  }

  function writePath(target, path, value) {
    const parts = path.split('.').filter(Boolean);
    let current = target;

    for (let index = 0; index < parts.length - 1; index += 1) {
      const part = parts[index];

      if (!hasOwn(current, part) || current[part] === null || typeof current[part] !== 'object' || Array.isArray(current[part])) {
        current[part] = {};
      }

      current = current[part];
    }

    current[parts[parts.length - 1]] = value;
  }

  function sanitizeFilenamePart(value) {
    return String(value).replace(/[\\/:*?"<>|]/g, '-');
  }

  function hasOwn(value, key) {
    return value !== null && (typeof value === 'object' || typeof value === 'function') && Object.prototype.hasOwnProperty.call(value, key);
  }

  function requireOutput() {
    if (!elements.outputJsonInput.value.trim()) {
      throw new Error('请先转换 JSON 再执行此操作。');
    }
  }

  function setConversionStatus(warnings) {
    if (!warnings.length) {
      setStatus('转换完成。');
      return;
    }

    setStatus(`转换完成,但有 ${warnings.length} 条警告:${warnings.join(', ')}`);
  }

  function setStatus(message) {
    elements.statusElement.textContent = message;
    elements.statusElement.classList.remove('error');
  }

  function setError(message) {
    elements.statusElement.textContent = message;
    elements.statusElement.classList.add('error');
  }

  function getErrorMessage(error) {
    return error && error.message ? error.message : String(error);
  }
})();