Ctrl K
ComunidadGuías…

src/dashboard/import-parser.js

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { unwrapPastedSecret } from './account-text-parser.js';

const EMAIL_RE = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/;
// A whole credential can be one opaque string: a show-auth-token /
// OneTimeAuthToken value is 47 chars of [A-Za-z0-9_-] (measured live), and the
// desktop callback is a `windsurf://…?access_token=…` deep link. Without this
// a token-only line was ERR_FORMAT_INVALID, so a pasted token could not be
// batch-imported at all (F3/F9).
const BARE_TOKEN_RE = /^[A-Za-z0-9_\-\.\$\/+=]{40,}$/;
const ABSOLUTE_URL_RE = /^[a-z][a-z0-9+.\-]*:\/\//i;

function tokenFromLine(line) {
  if (BARE_TOKEN_RE.test(line)) return line;
  if (!ABSOLUTE_URL_RE.test(line)) return '';
  try {
    return unwrapPastedSecret(line);
  } catch {
    // A URL with no token (or a proxy URL) is not a credential; the caller
    // reports the line as ERR_FORMAT_INVALID exactly as before.
    return '';
  }
}

export class BatchImportParseError extends Error {
  constructor(code, message = code) {
    super(message);
    this.code = code;
  }
}

function toTrimmedString(value) {
  if (value == null) return '';
  if (typeof value === 'string') return value.trim();
  if (typeof value === 'number' || typeof value === 'boolean') return String(value).trim();
  return '';
}

function readCaseInsensitive(obj, key) {
  if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return undefined;
  const match = Object.keys(obj).find(k => k.toLowerCase() === key.toLowerCase());
  return match ? obj[match] : undefined;
}

function getFirstString(obj, keys) {
  for (const key of keys) {
    const direct = toTrimmedString(readCaseInsensitive(obj, key));
    if (direct) return direct;
  }
  return '';
}

function normalizeProxy(rawProxy, parseProxyUrl) {
  if (!rawProxy) return { proxyRaw: null, proxy: null };
  if (typeof rawProxy === 'object' && !Array.isArray(rawProxy)) {
    const host = toTrimmedString(rawProxy.host);
    const port = Number.parseInt(rawProxy.port, 10);
    if (!host || !Number.isInteger(port) || port <= 0) {
      return { error: 'ERR_PROXY_FORMAT_INVALID' };
    }
    return {
      proxyRaw: `${toTrimmedString(rawProxy.type || 'http')}://${host}:${port}`,
      proxy: {
        type: toTrimmedString(rawProxy.type || 'http'),
        host,
        port,
        username: toTrimmedString(rawProxy.username),
        password: toTrimmedString(rawProxy.password),
      },
    };
  }
  const proxyRaw = toTrimmedString(rawProxy);
  if (!proxyRaw) return { proxyRaw: null, proxy: null };
  const proxy = parseProxyUrl(proxyRaw);
  if (!proxy) return { error: 'ERR_PROXY_FORMAT_INVALID' };
  return { proxyRaw, proxy };
}

function buildIssue({ lineNumber, raw, code, label }) {
  return {
    kind: 'issue',
    success: false,
    skipped: true,
    lineNumber,
    raw,
    label: label || (lineNumber ? `Line ${lineNumber}` : 'Skipped'),
    error: code,
  };
}

function buildEntry(base) {
  return {
    success: false,
    skipped: false,
    lineNumber: base.lineNumber || 0,
    raw: base.raw || '',
    proxyRaw: base.proxyRaw || null,
    proxy: base.proxy || null,
    label: base.label || '',
    ...base,
  };
}

function parsePasswordRemainder(remainder) {
  if (!remainder) return '';
  const trimmed = remainder.trimStart();
  const label = trimmed.match(/^(?:(?:密码|密碼|口令)\s*[::=]?\s*|(?:password|pass|pwd)\b\s*(?:[::=]\s*|\s+))/i);
  if (label) return trimmed.slice(label[0].length).trim();
  if (/^\s+/.test(remainder)) return remainder.trim();
  for (const separator of ['----', '|', ',', ':', ':', '=']) {
    if (trimmed.startsWith(separator)) return trimmed.slice(separator.length).trim();
  }
  return '';
}

function normalizeEmailPrefix(prefix) {
  const raw = String(prefix || '').trim();
  if (!raw) return '';
  const withoutLabel = raw
    .replace(/(?:\s*(?:邮箱|账号|账户|邮件|email|e-mail|mail|username|user)\s*[::=]?)+\s*$/i, '')
    .trim();
  return withoutLabel || '';
}

export function parseBatchImportTextLine(rawLine, lineNumber, parseProxyUrl) {
  const line = String(rawLine || '').trim();
  if (!line) return null;
  const emailMatch = line.match(EMAIL_RE);
  if (!emailMatch) {
    const token = tokenFromLine(line);
    if (token) {
      return buildEntry({ kind: 'token', lineNumber, raw: rawLine, token, label: '' });
    }
    return buildIssue({ lineNumber, raw: rawLine, code: 'ERR_FORMAT_INVALID' });
  }
  const email = emailMatch[0];
  const start = emailMatch.index || 0;
  const end = start + email.length;
  const prefix = normalizeEmailPrefix(line.slice(0, start));
  const remainder = line.slice(end);
  let proxyRaw = null;
  let proxy = null;
  if (prefix) {
    const normalizedProxy = normalizeProxy(prefix, parseProxyUrl);
    if (normalizedProxy.error) {
      return buildIssue({ lineNumber, raw: rawLine, code: normalizedProxy.error });
    }
    proxyRaw = normalizedProxy.proxyRaw;
    proxy = normalizedProxy.proxy;
  }
  const password = parsePasswordRemainder(remainder);
  if (!password) {
    return buildIssue({ lineNumber, raw: rawLine, code: 'ERR_FORMAT_INVALID' });
  }
  return buildEntry({
    kind: 'email_password',
    lineNumber,
    raw: rawLine,
    email,
    password,
    proxyRaw,
    proxy,
    label: email,
  });
}

export function parseBatchImportJsonItem(item, lineNumber, parseProxyUrl) {
  if (typeof item === 'string') return parseBatchImportTextLine(item, lineNumber, parseProxyUrl);
  if (!item || typeof item !== 'object' || Array.isArray(item)) {
    return buildIssue({ lineNumber, raw: JSON.stringify(item), code: 'ERR_UNSUPPORTED_IMPORT_ITEM' });
  }

  const nestedAccount = readCaseInsensitive(item, 'account');
  const nestedAccountValue = (
    nestedAccount && typeof nestedAccount === 'object' && !Array.isArray(nestedAccount)
  ) ? nestedAccount : null;

  const email = getFirstString(item, ['email', 'username', 'account'])
    || getFirstString(nestedAccountValue, ['email', 'username', 'account']);
  const password = getFirstString(item, ['password', 'pass']);
  const token = getFirstString(item, ['token', 'access_token', 'accessToken']);
  const apiKey = getFirstString(item, ['api_key', 'apiKey', 'key']);
  const windsurfApiKey = getFirstString(item, ['windsurf_api_key'])
    || getFirstString(readCaseInsensitive(item, 'windsurf_auth_status_raw'), ['apiKey']);
  const windsurfAuthToken = getFirstString(item, ['windsurf_auth_token'])
    || getFirstString(readCaseInsensitive(item, 'windsurf_auth_status_raw'), ['authToken']);
  const windsurfApiServerUrl = getFirstString(item, ['windsurf_api_server_url'])
    || getFirstString(readCaseInsensitive(item, 'windsurf_auth_status_raw'), ['apiServerUrl', 'api_server_url']);
  const exportEmail = getFirstString(item, ['github_email'])
    || getFirstString(readCaseInsensitive(item, 'windsurf_user_status'), ['email'])
    || getFirstString(readCaseInsensitive(item, 'windsurf_auth_status_raw'), ['email']);
  const label = getFirstString(item, ['label', 'name', 'note']) || email;
  const normalizedProxy = normalizeProxy(readCaseInsensitive(item, 'proxy'), parseProxyUrl);
  if (normalizedProxy.error) {
    return buildIssue({ lineNumber, raw: JSON.stringify(item), code: normalizedProxy.error, label });
  }

  if (token) {
    return buildEntry({
      kind: 'token',
      lineNumber,
      raw: JSON.stringify(item),
      token,
      label,
      email: email || label || '',
      proxyRaw: normalizedProxy.proxyRaw,
      proxy: normalizedProxy.proxy,
    });
  }

  if (apiKey) {
    return buildEntry({
      kind: 'api_key',
      lineNumber,
      raw: JSON.stringify(item),
      apiKey,
      label,
      email: email || label || '',
      proxyRaw: normalizedProxy.proxyRaw,
      proxy: normalizedProxy.proxy,
    });
  }

  if (windsurfApiKey) {
    return buildEntry({
      kind: 'api_key',
      lineNumber,
      raw: JSON.stringify(item),
      apiKey: windsurfApiKey,
      apiServerUrl: windsurfApiServerUrl || '',
      label: label || exportEmail,
      email: exportEmail || label || '',
      proxyRaw: normalizedProxy.proxyRaw,
      proxy: normalizedProxy.proxy,
    });
  }

  if (windsurfAuthToken) {
    return buildEntry({
      kind: 'token',
      lineNumber,
      raw: JSON.stringify(item),
      token: windsurfAuthToken,
      apiServerUrl: windsurfApiServerUrl || '',
      label: label || exportEmail,
      email: exportEmail || label || '',
      proxyRaw: normalizedProxy.proxyRaw,
      proxy: normalizedProxy.proxy,
    });
  }

  if (email && password) {
    return buildEntry({
      kind: 'email_password',
      lineNumber,
      raw: JSON.stringify(item),
      email,
      password,
      label,
      proxyRaw: normalizedProxy.proxyRaw,
      proxy: normalizedProxy.proxy,
    });
  }

  return buildIssue({
    lineNumber,
    raw: JSON.stringify(item),
    code: 'ERR_UNSUPPORTED_IMPORT_ITEM',
    label,
  });
}

function normalizeJsonContainer(parsed) {
  if (Array.isArray(parsed)) return parsed;
  if (!parsed || typeof parsed !== 'object') {
    return [parsed];
  }
  const accounts = readCaseInsensitive(parsed, 'accounts');
  if (Array.isArray(accounts)) return accounts;
  const items = readCaseInsensitive(parsed, 'items');
  if (Array.isArray(items)) return items;
  return [parsed];
}

export function parseBatchImportInput(text, parseProxyUrl) {
  const source = String(text || '');
  if (!source.trim()) {
    return { mode: 'empty', items: [] };
  }
  const trimmed = source.trim();
  if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
    let parsed;
    try {
      parsed = JSON.parse(trimmed);
    } catch {
      throw new BatchImportParseError('ERR_JSON_INVALID');
    }
    const items = normalizeJsonContainer(parsed)
      .map((item, index) => parseBatchImportJsonItem(item, index + 1, parseProxyUrl))
      .filter(Boolean);
    return { mode: 'json', items };
  }

  const items = source
    .replace(/\r\n?/g, '\n')
    .split('\n')
    .map((line, index) => parseBatchImportTextLine(line, index + 1, parseProxyUrl))
    .filter(Boolean);

  return { mode: 'text', items };
}