×
¥
查看详情
🔥 会员专享 文生文 代码生成

异步函数生成器

👁️ 78 次查看
📅 Dec 8, 2025
💡 核心价值: 本提示词专为JavaScript开发场景设计,能够根据具体异步任务需求生成符合最佳实践的async/await函数代码。通过结构化参数输入,自动构建包含错误处理、性能优化和技术说明的高质量异步解决方案,适用于数据获取、文件操作、API调用等多种异步场景,帮助开发者快速实现可靠且易于维护的异步编程逻辑。

🎯 可自定义参数(3个)

异步任务描述
需要实现的异步任务具体描述
函数用途
函数的主要用途和目标
复杂度级别
任务的复杂程度

🎨 效果示例

函数定义 async function fetchProducts(endpoint, { pageSize = 50, maxPages = 5, headers = {}, signal, onProgress } = {})

  • 参数说明
    • endpoint: 字符串,产品分页接口的起始地址(如 /products 或完整URL)
    • pageSize: 每页大小,默认 50。若起始URL未包含该参数则自动添加
    • maxPages: 最大抓取页数,默认 5
    • headers: 追加到每次请求的请求头对象
    • signal: 可选的 AbortSignal,用于整体任务取消
    • onProgress: 可选回调函数,每页完成时报告进度。形如 onProgress({ page, url, received, total, fromCache, retries, status })

代码实现

// 简单的全局(进程内)ETag缓存,按URL键控
const ETagCache = new Map(); // url -> { etag: string, body: any }

/**
 * 解析 Retry-After,返回等待毫秒数。如果不存在则按指数退避生成。
 * attempt 从 1 开始计数(第一次重试的序号)
 */
function computeRetryDelayMs(headers, attempt, baseMs = 400) {
  // 优先使用服务端提供的 Retry-After
  const retryAfter = headers?.get?.('Retry-After');
  if (retryAfter) {
    // 可能是秒,或HTTP日期
    const seconds = Number(retryAfter);
    if (Number.isFinite(seconds) && seconds >= 0) {
      return Math.max(0, seconds * 1000);
    }
    const dateMs = Date.parse(retryAfter);
    if (!Number.isNaN(dateMs)) {
      return Math.max(0, dateMs - Date.now());
    }
  }
  // 指数退避 + 抖动:base * 2^(attempt-1) + [0, 250)
  const exp = baseMs * Math.pow(2, attempt - 1);
  const jitter = Math.floor(Math.random() * 250);
  return exp + jitter;
}

/** 支持可取消的 sleep */
function sleep(ms, signal) {
  return new Promise((resolve, reject) => {
    if (signal?.aborted) return reject(signal.reason || new DOMException('Aborted', 'AbortError'));
    const t = setTimeout(() => {
      cleanup();
      resolve();
    }, ms);
    const onAbort = () => {
      cleanup();
      reject(signal.reason || new DOMException('Aborted', 'AbortError'));
    };
    const cleanup = () => {
      clearTimeout(t);
      if (signal) signal.removeEventListener('abort', onAbort);
    };
    if (signal) signal.addEventListener('abort', onAbort, { once: true });
  });
}

/** 为相对/绝对URL安全地添加/覆盖查询参数 */
function ensureQueryParam(url, key, value) {
  const [path, query = ''] = url.split('?');
  const sp = new URLSearchParams(query);
  if (!sp.has(key)) sp.set(key, String(value));
  return `${path}?${sp.toString()}`;
}

/** 从响应中解析 next 链接(优先 body.next,其次 Link: <...>; rel=next) */
function getNextUrl(body, headers) {
  if (body && typeof body.next === 'string' && body.next) return body.next;
  const link = headers?.get?.('Link');
  if (link) {
    const m = link.match(/<([^>]+)>\s*;\s*rel="?next"?/i);
    if (m) return m[1];
  }
  return null;
}

/** 将原始产品数据规范化为 {id, name, price, updatedAt} */
function normalizeItem(raw) {
  const id = raw?.id ?? raw?.productId ?? raw?._id ?? null;
  const name = raw?.name ?? raw?.title ?? '';
  const priceVal = typeof raw?.price === 'number' ? raw.price : parseFloat(raw?.price);
  const updatedRaw = raw?.updatedAt ?? raw?.updated_at ?? raw?.modifiedAt ?? raw?.updated ?? raw?.modified ?? null;

  let updatedISO;
  if (updatedRaw) {
    const d = new Date(updatedRaw);
    updatedISO = Number.isFinite(d.getTime()) ? d.toISOString() : new Date(0).toISOString();
  } else {
    updatedISO = new Date(0).toISOString(); // 缺失时使用极早时间,排序置后
  }

  return {
    id,
    name,
    price: Number.isFinite(priceVal) ? priceVal : null,
    updatedAt: updatedISO,
  };
}

/**
 * 单请求封装:ETag缓存、8s超时、最多3次重试(网络错误/429/5xx)、支持 Retry-After。
 * 返回 { body, status, headers, fromCache, retries }
 */
async function fetchWithRetry(url, { headers = {}, signal, timeoutMs = 8000, maxRetries = 3 }) {
  let attempt = 0; // 含初次请求
  let lastErr = null;

  while (attempt <= maxRetries) {
    const controller = new AbortController();
    const composedSignal = controller.signal;
    // 绑定外部 signal -> 内部
    const onAbort = () => controller.abort(signal.reason || new DOMException('Aborted', 'AbortError'));
    if (signal) {
      if (signal.aborted) {
        throw signal.reason || new DOMException('Aborted', 'AbortError');
      }
      signal.addEventListener('abort', onAbort, { once: true });
    }

    // 设置超时
    const timeoutId = setTimeout(() => {
      controller.abort(new DOMException('Timeout', 'TimeoutError'));
    }, timeoutMs);

    try {
      attempt++;
      const reqHeaders = new Headers(headers);
      const cached = ETagCache.get(url);
      if (cached?.etag) {
        reqHeaders.set('If-None-Match', cached.etag);
      }

      const res = await fetch(url, { method: 'GET', headers: reqHeaders, signal: composedSignal });
      const { status, headers: resHeaders } = res;

      // 处理 304:使用缓存体
      if (status === 304) {
        clearTimeout(timeoutId);
        if (signal) signal.removeEventListener('abort', onAbort);
        if (cached?.body != null) {
          return { body: cached.body, status, headers: resHeaders, fromCache: true, retries: attempt - 1 };
        }
        // 接收到304但无缓存体,视为异常(不重试 or 作为一次错误后按指数退避重试)
        lastErr = new Error('304 received but cache body missing');
        // 尝试下一次重试(极少见的状态不同步)
      } else if (status >= 200 && status < 300) {
        // 正常 2xx
        const body = await res.json().catch(() => {
          throw new Error('Failed to parse JSON');
        });
        const etag = resHeaders.get('ETag') || resHeaders.get('Etag') || resHeaders.get('etag');
        if (etag) {
          ETagCache.set(url, { etag, body });
        }
        clearTimeout(timeoutId);
        if (signal) signal.removeEventListener('abort', onAbort);
        return { body, status, headers: resHeaders, fromCache: false, retries: attempt - 1 };
      } else if (status === 429 || (status >= 500 && status < 600)) {
        // 需要重试的状态码
        const delay = computeRetryDelayMs(resHeaders, attempt);
        clearTimeout(timeoutId);
        if (signal) signal.removeEventListener('abort', onAbort);
        if (attempt > maxRetries) {
          const text = await res.text().catch(() => '');
          throw new Error(`Request failed after retries: ${status} ${text?.slice(0, 160)}`);
        }
        await sleep(delay, signal);
        continue; // 下一次尝试
      } else {
        // 其他 4xx:不重试
        const text = await res.text().catch(() => '');
        clearTimeout(timeoutId);
        if (signal) signal.removeEventListener('abort', onAbort);
        throw new Error(`Request failed: ${status} ${text?.slice(0, 160)}`);
      }
    } catch (err) {
      clearTimeout(timeoutId);
      if (signal) signal.removeEventListener('abort', onAbort);

      // 取消直接抛出
      if (err?.name === 'AbortError') throw err;

      lastErr = err;
      if (attempt > maxRetries) {
        throw lastErr;
      }
      const delay = computeRetryDelayMs(null, attempt);
      await sleep(delay, signal);
      continue;
    }
  }

  // 理论上不会到达
  throw lastErr || new Error('Unknown fetch error');
}

/**
 * 主函数:分页抓取产品,重试/超时/并发/ETag缓存/标准化/去重/排序。
 * 返回 { items, total, fromCache, durationMs, errors }
 */
async function fetchProducts(endpoint, { pageSize = 50, maxPages = 5, headers = {}, signal, onProgress } = {}) {
  const start = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();

  // 并发上限
  const MAX_CONCURRENCY = 3;

  // 初始URL:注入 pageSize(若已存在则不改变)
  let firstUrl = ensureQueryParam(endpoint, 'pageSize', pageSize);

  // 结果汇总
  const byId = new Map(); // id -> normalizedItem(保留最新updatedAt)
  let anyFromCache = false;
  let serverTotal = null;
  const errors = [];

  // 队列 + 并发 worker
  const queue = [];
  const visited = new Set(); // 防止意外循环
  let pagesFetched = 0;

  // 入队初始页
  queue.push(firstUrl);
  visited.add(firstUrl);

  async function processPage(url, pageIndex) {
    const resp = await fetchWithRetry(url, { headers, signal, timeoutMs: 8000, maxRetries: 3 });
    const { body, status, headers: resHeaders, fromCache, retries } = resp;
    anyFromCache = anyFromCache || !!fromCache;

    // 解析产品列表
    const list = body?.items ?? body?.products ?? body?.data ?? [];
    const total = (typeof body?.total === 'number' && body.total >= 0) ? body.total : null;
    if (serverTotal == null && total != null) serverTotal = total;

    // 规范化 + 去重(按最新updatedAt保留)
    for (const raw of list) {
      const item = normalizeItem(raw);
      if (item.id == null) continue; // 跳过无ID项
      const prev = byId.get(item.id);
      if (!prev) {
        byId.set(item.id, item);
      } else {
        const prevTs = new Date(prev.updatedAt).getTime();
        const currTs = new Date(item.updatedAt).getTime();
        if (currTs >= prevTs) {
          byId.set(item.id, item);
        }
      }
    }

    // 进度回调
    if (typeof onProgress === 'function') {
      try {
        onProgress({
          page: pageIndex,
          url,
          received: Array.isArray(list) ? list.length : 0,
          total,
          fromCache: !!fromCache,
          retries,
          status,
        });
      } catch {
        // 保持稳健,忽略回调内部错误
      }
    }

    // 解析下一页
    const nextUrl = getNextUrl(body, resHeaders);
    return { nextUrl };
  }

  // Worker:从队列取任务直到满足条件或被取消
  async function worker(id) {
    while (!signal?.aborted && queue.length > 0 && pagesFetched < maxPages) {
      const url = queue.shift();
      const pageIndex = pagesFetched + 1;
      try {
        const { nextUrl } = await processPage(url, pageIndex);
        pagesFetched++;
        if (nextUrl && !visited.has(nextUrl) && pagesFetched < maxPages) {
          visited.add(nextUrl);
          queue.push(nextUrl);
        }
      } catch (err) {
        // 记录错误并继续处理队列中其他任务(线性分页通常不会再有后续)
        errors.push({
          url,
          message: String(err?.message || err),
          name: err?.name || 'Error',
        });
        // 失败页不增加 pagesFetched(因为没有成功处理该页)
        // 不再入队下一页,因为无法获取 next 链接
      }
    }
  }

  // 启动并发 worker(在单链分页上并发自然退化为 1,但逻辑仍健壮)
  const workers = Array.from({ length: Math.min(MAX_CONCURRENCY, queue.length) }, (_, i) => worker(i));
  await Promise.all(workers);

  // 汇总与排序(按 updatedAt 倒序)
  const items = Array.from(byId.values()).sort(
    (a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
  );

  const end = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
  return {
    items,
    total: serverTotal != null ? serverTotal : items.length,
    fromCache: anyFromCache,
    durationMs: Math.round(end - start),
    errors,
  };
}

错误处理

  • 网络错误、5xx、429:
    • 最多 3 次重试(指数退避:约 400ms、800ms、1600ms,包含随机抖动)
    • 若响应包含 Retry-After,则优先使用该值(支持秒与HTTP日期)
  • 304 Not Modified:
    • 命中缓存时返回缓存体,并标记 fromCache=true
    • 若收到 304 时缓存缺失(极少见状态不同步),将按重试策略处理
  • 超时:
    • 每次请求 8s 超时(AbortController)
    • 若调用方传入 signal,取消会立即中止所有等待与请求
  • 其他 4xx:
    • 不重试,直接记录错误
  • onProgress 回调内抛错被安全忽略,不影响主流程

使用示例

// 取消控制(可选)
const controller = new AbortController();

// 进度回调(可选)
function onProgress(evt) {
  // evt: { page, url, received, total, fromCache, retries, status }
  console.log(`Page #${evt.page} status=${evt.status} items=${evt.received} total=${evt.total ?? 'n/a'} cache=${evt.fromCache} retries=${evt.retries}`);
}

(async () => {
  try {
    const result = await fetchProducts('/products', {
      pageSize: 100,
      maxPages: 5,
      headers: { Accept: 'application/json' },
      signal: controller.signal,
      onProgress,
    });

    console.log('Total:', result.total);
    console.log('Duration(ms):', result.durationMs);
    console.log('From cache:', result.fromCache);
    console.log('Errors:', result.errors);
    console.log('First item:', result.items[0]); // {id,name,price,updatedAt}
  } catch (e) {
    console.error('Fetch failed:', e);
  }
})();

技术要点

  • 异步与重试策略
    • 使用 async/await 保持可读性,单独封装 fetchWithRetry 提升复用性与可测试性
    • 对网络错误、429 与 5xx 进行指数退避重试,并尊重服务端 Retry-After
    • 重试等待通过 sleep 实现,并支持外部 AbortSignal 取消
  • 超时与取消
    • 每次请求创建独立 AbortController 并设置 8s 超时
    • 合并外部 signal:外部取消会中止所有请求与等待
  • ETag 缓存
    • 以 URL 为键缓存 {etag, body},命中时发送 If-None-Match
    • 收到 304 时使用缓存体,减少带宽与延迟
  • 并发分页
    • 任务队列 + worker 设计,最大并发 3
    • 单链式 next 会自然退化为并发 1,但在存在分支或预取场景也能稳定工作
  • 数据规范化与去重
    • 统一为 {id,name,price,updatedAt},缺失字段安全处理
    • 按 id 去重,并保留最新 updatedAt 的记录
    • 按 updatedAt 倒序排序,便于展示最新变更
  • 返回结构
    • {items, total, fromCache, durationMs, errors}
    • total 优先使用服务端 total,否则使用去重后的 items.length
  • 兼容性与最佳实践
    • 使用原生 fetch、Headers、AbortController 与 URLSearchParams
    • 不依赖第三方库,避免安全风险
    • 所有回调与错误都进行边界处理,保证稳健性与可维护性

函数定义

async function scrapeArticles(urls, { concurrency = 5, timeoutMs = 6000, cacheTTL = 600000, signal } = {})

  • 参数说明

    • urls: string[],待抓取的 URL 列表(仅支持 http/https)
    • concurrency: number,并发上限(默认 5)
    • timeoutMs: number,单请求超时毫秒数(默认 6000ms)
    • cacheTTL: number,内存缓存 TTL(默认 10 分钟 = 600000ms)
    • signal: AbortSignal,可选的外部取消信号,用于整体任务取消
  • 返回值

    • Promise<{ list: Array<{ url: string; title: string | null; publishedAt: string | null; length: number }>, failed: Array<{ url: string; reason: string }> }>
    • list:去重后的成功结果集合(依据 canonical 或规范化 URL)
    • failed:失败的 URL 与失败原因

注意:若传入的 AbortSignal 触发,将尽可能中止正在执行的请求;函数不会抛错而是把已完成和失败的请求通过 allSettled 汇总并返回(失败中会标记 Abort)。

代码实现

// 模块级内存缓存(基于 TTL)。键为规范化 URL 或 canonical 规范化 URL。
const __articleCache = new Map(); // key -> { value, expiresAt }

/**
 * 规范化 URL:
 * - 只接受 http/https
 * - 小写协议与主机名,移除 hash
 * - 清理默认端口(:80 / :443)
 * - 规范路径(去除多余斜杠与根路径尾部斜杠)
 * - 搜索参数排序且移除常见追踪参数
 */
function normalizeUrl(input) {
  let u;
  try {
    u = new URL(input);
  } catch {
    throw new Error(`Invalid URL: ${input}`);
  }
  if (!/^https?:$/.test(u.protocol)) {
    throw new Error(`Unsupported protocol (only http/https): ${input}`);
  }
  u.protocol = u.protocol.toLowerCase();
  u.hostname = u.hostname.toLowerCase();

  // remove default ports
  if ((u.protocol === 'http:' && u.port === '80') || (u.protocol === 'https:' && u.port === '443')) {
    u.port = '';
  }

  // remove hash
  u.hash = '';

  // normalize path: collapse multiple slashes
  u.pathname = u.pathname.replace(/\/{2,}/g, '/');
  // remove trailing slash except root
  if (u.pathname.length > 1 && u.pathname.endsWith('/')) {
    u.pathname = u.pathname.slice(0, -1);
  }

  // clean & sort search params
  const trackingParams = new Set([
    'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',
    'fbclid', 'gclid', 'yclid', 'mc_cid', 'mc_eid'
  ]);
  const params = Array.from(u.searchParams.entries())
    .filter(([k]) => !trackingParams.has(k.toLowerCase()))
    .sort(([a], [b]) => a.localeCompare(b));
  u.search = '';
  for (const [k, v] of params) u.searchParams.append(k, v);

  return u.toString();
}

/** 简易 HTML 实体解码(覆盖常见实体与数字实体) */
function decodeHtmlEntities(str) {
  if (!str) return str;
  const map = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", nbsp: ' ' };
  return str
    .replace(/&(#\d+|#x[0-9a-fA-F]+|[a-zA-Z]+);/g, (m, t) => {
      if (t[0] === '#') {
        const code = t[1].toLowerCase() === 'x' ? parseInt(t.slice(2), 16) : parseInt(t.slice(1), 10);
        if (!Number.isNaN(code)) return String.fromCodePoint(code);
        return m;
      }
      return map[t] ?? m;
    })
    .trim();
}

/** 从标签中提取纯文本(粗略移除内部标签) */
function stripTags(html) {
  return decodeHtmlEntities(
    html
      .replace(/<script[\s\S]*?<\/script>/gi, '')
      .replace(/<style[\s\S]*?<\/style>/gi, '')
      .replace(/<[^>]+>/g, ' ')
      .replace(/\s+/g, ' ')
      .trim()
  );
}

/** 获取首个匹配的开始/结束标签内容 */
function getFirstTagInnerHTML(html, tag) {
  const re = new RegExp(`<${tag}\\b[^>]*>([\\s\\S]*?)<\\/${tag}>`, 'i');
  const m = html.match(re);
  return m ? m[1] : null;
}

/** 解析 <title> 与 <h1> */
function extractTitle(html) {
  const titleRaw = getFirstTagInnerHTML(html, 'title');
  if (titleRaw) {
    const t = stripTags(titleRaw);
    if (t) return t;
  }
  const h1Raw = getFirstTagInnerHTML(html, 'h1');
  if (h1Raw) {
    const t = stripTags(h1Raw);
    if (t) return t;
  }
  return null;
}

/** 解析 <link rel="canonical" href="...">(返回相对或绝对 URL 字符串) */
function extractCanonical(html) {
  // 捕获所有 link 标签,解析 rel 和 href
  const links = html.match(/<link\b[^>]*>/gi) || [];
  for (const tag of links) {
    // 解析属性
    const attrs = {};
    tag.replace(/(\w[\w:-]*)\s*=\s*("([^"]*)"|'([^']*)'|([^\s"'>]+))/g, (_, name, _v, v1, v2, v3) => {
      attrs[name.toLowerCase()] = (v1 ?? v2 ?? v3 ?? '').trim();
      return _;
    });
    const rel = (attrs.rel || '').toLowerCase();
    if (rel.split(/\s+/).includes('canonical') && attrs.href) {
      return attrs.href.trim();
    }
  }
  return null;
}

/** 解析 meta[property="article:published_time"] 或 meta[name="article:published_time"] */
function extractPublishedTimeFromMeta(html) {
  const metas = html.match(/<meta\b[^>]*>/gi) || [];
  for (const tag of metas) {
    const attrs = {};
    tag.replace(/(\w[\w:-]*)\s*=\s*("([^"]*)"|'([^']*)'|([^\s"'>]+))/g, (_, name, _v, v1, v2, v3) => {
      attrs[name.toLowerCase()] = (v1 ?? v2 ?? v3 ?? '').trim();
      return _;
    });
    const prop = (attrs.property || attrs.name || '').toLowerCase();
    if (prop === 'article:published_time' && attrs.content) {
      return attrs.content.trim();
    }
  }
  return null;
}

/** 从首个 <p> 文本中尽力提取日期时间(支持 ISO 与常见中文格式) */
function extractPublishedTimeFromFirstParagraph(html) {
  const p = getFirstTagInnerHTML(html, 'p');
  if (!p) return null;
  const text = stripTags(p);

  // 1) 直接尝试 Date.parse 可识别格式(包含 ISO/RFC)
  const direct = Date.parse(text);
  if (!Number.isNaN(direct)) return new Date(direct).toISOString();

  // 2) 匹配 YYYY-MM-DD( HH:MM(:SS)?(Z|±HH:MM)?)
  const isoLike = text.match(/(\d{4})-(\d{1,2})-(\d{1,2})(?:[ T](\d{1,2}):(\d{2})(?::(\d{2}))?(?:Z|([+-]\d{2}:?\d{2}))?)?/);
  if (isoLike) {
    const [ , Y, M, D, h='0', m='0', s='0', tz] = isoLike;
    const base = new Date(Date.UTC(+Y, +M - 1, +D, +h, +m, +s));
    if (tz && tz !== 'Z') {
      // 处理时区偏移
      const m2 = tz.match(/([+-]\d{2}):?(\d{2})/);
      if (m2) {
        const offsetMin = (parseInt(m2[1], 10) * 60) + parseInt(m2[2], 10);
        // UTC = local - offset => 已按 UTC 构建了 base,无需额外处理
        // 但 isoLike 在无 tz 时按 UTC 处理;有 tz 时我们已用 UTC 构造,再按 offset 调整
        base.setUTCMinutes(base.getUTCMinutes() - offsetMin);
      }
    }
    return base.toISOString();
  }

  // 3) 中文日期:YYYY年MM月DD日 HH:MM(:SS)?
  const zh = text.match(/(\d{4})年(\d{1,2})月(\d{1,2})日(?:\s+(\d{1,2}):(\d{2})(?::(\d{2}))?)?/);
  if (zh) {
    const [ , Y, M, D, h='0', m='0', s='0'] = zh;
    const d = new Date(+Y, +M - 1, +D, +h, +m, +s);
    return new Date(d.getTime()).toISOString();
  }

  return null;
}

/** 提取 HTML 信息:title、canonical、publishedAt(ISO string 或 null) */
function parseHtml(html, baseUrl) {
  const title = extractTitle(html);

  let publishedAt = extractPublishedTimeFromMeta(html);
  if (publishedAt) {
    const ts = Date.parse(publishedAt);
    publishedAt = Number.isNaN(ts) ? null : new Date(ts).toISOString();
  } else {
    publishedAt = extractPublishedTimeFromFirstParagraph(html);
  }

  const canonicalHref = extractCanonical(html);
  let canonical = null;
  if (canonicalHref) {
    try {
      const abs = new URL(canonicalHref, baseUrl).toString();
      canonical = abs;
    } catch {
      // ignore bad canonical
    }
  }

  return { title, canonical, publishedAt };
}

/** TTL 缓存:读取 */
function cacheGet(key) {
  const hit = __articleCache.get(key);
  if (hit && hit.expiresAt > Date.now()) return hit.value;
  if (hit) __articleCache.delete(key);
  return null;
}

/** TTL 缓存:写入 */
function cacheSet(key, value, ttl) {
  __articleCache.set(key, { value, expiresAt: Date.now() + ttl });
}

/** 组合多个 AbortSignal(支持 AbortSignal.any 的环境;无则降级) */
function combineSignals(signals) {
  const valid = signals.filter(Boolean);
  if (valid.length === 0) return undefined;
  if (typeof AbortSignal !== 'undefined' && typeof AbortSignal.any === 'function') {
    return AbortSignal.any(valid);
  }
  const controller = new AbortController();
  const onAbort = (e) => {
    try { controller.abort(e?.target?.reason); } catch { controller.abort(); }
    cleanup();
  };
  const cleanup = () => valid.forEach(s => s.removeEventListener('abort', onAbort));
  let alreadyAborted = false;
  for (const s of valid) {
    if (s.aborted) {
      alreadyAborted = true;
      try { controller.abort(s.reason); } catch { controller.abort(); }
      break;
    }
  }
  if (!alreadyAborted) valid.forEach(s => s.addEventListener('abort', onAbort, { once: true }));
  return controller.signal;
}

/** 基于并发的 limiter */
function createLimiter(limit, outerSignal) {
  let active = 0;
  const queue = [];
  const runNext = () => {
    if (outerSignal?.aborted) return;
    if (active >= limit || queue.length === 0) return;
    const task = queue.shift();
    active++;
    task().finally(() => {
      active--;
      runNext();
    });
  };
  return (fn) => new Promise((resolve) => {
    const wrapped = () => fn().then(resolve, resolve); // 将错误留待 allSettled 处理
    queue.push(wrapped);
    runNext();
  });
}

/** 单 URL 抓取与解析 */
async function fetchOne(originalUrl, { timeoutMs, cacheTTL, outerSignal }) {
  const normInput = normalizeUrl(originalUrl);

  // 命中缓存
  const cached = cacheGet(normInput);
  if (cached) return cached;

  // 每请求单独超时控制
  const timeoutCtrl = new AbortController();
  const tId = setTimeout(() => {
    try { timeoutCtrl.abort(new Error('Timeout')); } catch { timeoutCtrl.abort(); }
  }, timeoutMs);

  const combinedSignal = combineSignals([timeoutCtrl.signal, outerSignal]);

  try {
    const res = await fetch(originalUrl, {
      method: 'GET',
      headers: {
        'Accept': 'text/html,application/xhtml+xml;q=0.9,*/*;q=0.8',
        // 可选 UA,部分站点对默认 UA 严格
        'User-Agent': 'scrapeArticles/1.0 (+https://example.com)'
      },
      redirect: 'follow',
      // gzip/br/deflate 由 fetch 实现自动协商与解压
      signal: combinedSignal
    });

    if (!res.ok) {
      throw new Error(`HTTP ${res.status} ${res.statusText}`);
    }

    const finalUrl = res.url || originalUrl;
    const html = await res.text();

    const { title, canonical, publishedAt } = parseHtml(html, finalUrl);

    // 使用 canonical 或最终跳转后的 URL 进行规范化
    const finalNorm = normalizeUrl(canonical ? new URL(canonical, finalUrl).toString() : finalUrl);

    const result = {
      url: finalNorm,
      title: title || null,
      publishedAt: publishedAt || null,
      // 以字符长度衡量,如需字节可用 Buffer.byteLength(html, 'utf8')(Node 环境)
      length: html.length
    };

    // 写入缓存(输入规范化 URL 与 canonical 规范化 URL 都写入,提升命中率)
    cacheSet(normInput, result, cacheTTL);
    cacheSet(finalNorm, result, cacheTTL);

    return result;
  } finally {
    clearTimeout(tId);
  }
}

/**
 * 主函数:批量抓取文章标题与时间(限流、超时、缓存、去重、allSettled 汇总)
 */
async function scrapeArticles(urls, { concurrency = 5, timeoutMs = 6000, cacheTTL = 600000, signal } = {}) {
  if (!Array.isArray(urls)) throw new Error('urls must be an array of strings');
  if (concurrency <= 0) throw new Error('concurrency must be > 0');

  // 预过滤非法/重复输入(按规范化 URL 去重)
  const uniqueInputs = [];
  const seen = new Set();
  for (const u of urls) {
    if (typeof u !== 'string' || !u.trim()) continue;
    try {
      const n = normalizeUrl(u);
      if (!seen.has(n)) {
        seen.add(n);
        uniqueInputs.push({ original: u, normalized: n });
      }
    } catch {
      // 跳过非法 URL(最终会在 failed 中体现)
      uniqueInputs.push({ original: u, normalized: null });
    }
  }

  const limiter = createLimiter(concurrency, signal);
  const tasks = uniqueInputs.map(({ original, normalized }) =>
    limiter(async () => {
      if (!normalized) {
        // 预规范化失败
        return { status: 'rejected', reason: new Error('Invalid URL'), original };
      }
      try {
        const value = await fetchOne(original, { timeoutMs, cacheTTL, outerSignal: signal });
        return { status: 'fulfilled', value, original };
      } catch (err) {
        return { status: 'rejected', reason: err, original };
      }
    })
  );

  // 等待任务全部 settle(内部每项已包装为 fulfilled/rejected 结构)
  const settled = await Promise.all(tasks);

  // 汇总:去重(基于 canonical 或规范化 URL)
  const finalList = [];
  const finalFailed = [];

  const dedupe = new Set();
  for (const s of settled) {
    if (s.status === 'fulfilled') {
      const item = s.value;
      const key = normalizeUrl(item.url);
      if (!dedupe.has(key)) {
        dedupe.add(key);
        finalList.push(item);
      }
    } else {
      const reason = s.reason;
      const msg = (reason && (reason.message || String(reason))) || 'Unknown error';
      finalFailed.push({
        url: s.original,
        reason: msg
      });
    }
  }

  return { list: finalList, failed: finalFailed };
}

错误处理

  • 输入校验:urls 必须为字符串数组;concurrency > 0;不支持非 http/https 协议
  • 超时控制:每个请求通过 AbortController 实现 timeoutMs 超时,中止 fetch
  • 取消控制:可接收外部 AbortSignal;与超时信号组合,任一触发即中止请求
  • 网络/HTTP 错误:非 2xx/3xx 响应抛出 HTTP 错误并记入 failed
  • HTML 解析容错:采用健壮的正则提取,若 title 或时间缺失,返回 null 而非抛错
  • 缓存过期:TTL 到期自动清理;命中后直接返回缓存结果,减少重复请求

注意:函数最终使用 allSettled 风格汇总,无论个别请求失败或被中止,均返回 { list, failed },不会整体抛错(除非参数校验失败)。

使用示例

(async () => {
  const urls = [
    'https://example.com/article/123',
    'https://example.com/article/123?utm_source=twitter',
    'https://blog.example.org/post/abc',
    'https://news.example.com/path#section',
    'invalid-url'
  ];

  const controller = new AbortController();
  // 可选:在 10 秒后取消整个批量任务
  // setTimeout(() => controller.abort(new Error('User aborted')), 10000);

  const { list, failed } = await scrapeArticles(urls, {
    concurrency: 4,
    timeoutMs: 6000,
    cacheTTL: 10 * 60 * 1000,
    signal: controller.signal
  });

  console.log('Success:', list);
  console.log('Failed:', failed);
})();
  • 预期输出说明
    • list 中每项包含:
      • url:canonical 规范化 URL(若页面提供),否则为最终跳转后的规范化 URL
      • title:页面标题或首个 h1(若 title 缺失)
      • publishedAt:优先取 meta[property="article:published_time"],否则尝试从首个段落解析时间(ISO 字符串)
      • length:HTML 文本的字符长度
    • failed 包含无法请求/解析的 URL 与失败原因(如超时、HTTP 错误、无效 URL、被取消等)

技术要点

  • 并发控制:自实现轻量 limiter,避免额外依赖;保证同一时间最多 concurrency 个请求
  • 取消与超时:使用 AbortController;通过 combineSignals 合并外部 signal 与超时 signal,任一触发即中断 fetch
  • allSettled 汇总:每个任务内部已包裹为 fulfilled/rejected 结构,最终统一汇总为 { list, failed }
  • 去重策略:优先使用页面 canonical 链接规范化;无 canonical 时以最终跳转 URL 的规范化结果为去重键
  • 规范化 URL:去除 hash、处理默认端口、排序查询参数、清理常见追踪参数,尽量将等价 URL 归一
  • 内存缓存(TTL):输入规范化 URL 与 canonical 规范化 URL 同步写入缓存,提升命中率;减少重复抓取与压力
  • 解析策略:
    • 标题:先取 ,缺失时取首个 <h1></li> <li>时间:优先 meta[property="article:published_time"];缺失时尝试从首段文本解析常见时间格式(ISO/中文日期)</li> <li>HTML 解析采用正则提取常见标签,避免引入重型 DOM 解析库,满足中等复杂度抓取任务</li> </ul> </li> <li>兼容性: <ul> <li>需运行在支持 fetch 与 AbortController 的环境(Node.js 18+ 或现代浏览器)</li> <li>gzip/br 自动协商由 fetch 实现;无需显式设置 Accept-Encoding</li> </ul> </li> <li>安全与健壮性: <ul> <li>限制协议为 http/https,拒绝其他协议</li> <li>对错误进行分门别类的捕获与记录,避免影响整体任务</li> <li>避免使用已废弃 API 与不必要的第三方依赖</li> </ul> </li> </ul> <p>如果你需要扩展:支持重试策略、更多日期格式、内容字节长度统计、或将缓存替换为 LRU 等,我可以进一步完善。</p> </div> </div> </div> <div class="prompt-examples-v4-tab-content " id="example-2"> <div class="prompt-examples-v4-output"> <button class="prompt-examples-v4-popup-btn" onclick="showExamplePopup(2, 'CSV批量上报', this)" title="在弹窗中查看完整内容"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/> </svg> </button> <div class="html-content-display prompt-examples-v4-output-text"> <p>函数定义 async function processCsvAndUpload(csvPath, endpoint, { batchSize = 100, concurrency = 4, signal, onProgress } = {})</p> <ul> <li>参数说明 <ul> <li>csvPath: string。本地 UTF-8 CSV 文件路径,首行为表头。</li> <li>endpoint: string。批量上报的 HTTP(S) JSON 接口地址(POST)。</li> <li>options: <ul> <li>batchSize: number,分批大小,默认 100。</li> <li>concurrency: number,同时并发发送的批量请求数上限,默认 4。</li> <li>signal: AbortSignal,可选。用于全程取消操作。</li> <li>onProgress: function,可选。形如 onProgress({ linesRead, batchesDispatched, batchesCompleted, batchesSucceeded, batchesFailed, recordsProcessed })。</li> </ul> </li> </ul> </li> <li>返回值 <ul> <li>Promise<{ total, batchesSucceeded, batchesFailed, skipped, elapsedMs, files }></li> <li>total: 成功进入上报流程的记录数(不含表头,不含被跳过的空行/损坏行)。</li> <li>batchesSucceeded: 成功上报(HTTP 2xx)的批次数。</li> <li>batchesFailed: 达到重试上限仍失败的批次数。</li> <li>skipped: 本地解析阶段跳过的行数(空行/无效行/字段数不匹配)。</li> <li>elapsedMs: 总耗时(毫秒)。</li> <li>files: { success: string, errors: string },分别为 success.ndjson 与 errors.ndjson 的绝对路径。</li> </ul> </li> </ul> <p>代码实现 注意:该实现仅依赖 Node.js 内置模块(Node.js >= 18,内置 fetch),无第三方依赖。</p> <pre><code class="language-js">// Node.js >= 18 import fs from 'node:fs'; import path from 'node:path'; import { once } from 'node:events'; export async function processCsvAndUpload( csvPath, endpoint, { batchSize = 100, concurrency = 4, signal, onProgress } = {} ) { assertPositiveInteger(batchSize, 'batchSize'); assertPositiveInteger(concurrency, 'concurrency'); if (typeof endpoint !== 'string' || !/^https?:\/\//i.test(endpoint)) { throw new TypeError('endpoint must be a valid HTTP/HTTPS URL string'); } if (signal?.aborted) throw abortError(); const start = Date.now(); const dir = path.resolve(path.dirname(csvPath)); const successPath = path.join(dir, 'success.ndjson'); const errorsPath = path.join(dir, 'errors.ndjson'); // 输出文件写入流(NDJSON) const successStream = fs.createWriteStream(successPath, { flags: 'w', encoding: 'utf8', highWaterMark: 1 << 20 }); const errorsStream = fs.createWriteStream(errorsPath, { flags: 'w', encoding: 'utf8', highWaterMark: 1 << 20 }); // 捕获写入流错误 const streamErrorHandler = (err) => { // 将任何写入错误升级为操作失败 throw err; }; successStream.on('error', streamErrorHandler); errorsStream.on('error', streamErrorHandler); const stats = { linesRead: 0, // 读到的有效数据行(不含表头) recordsProcessed: 0, // 进入批处理的记录数 batchesDispatched: 0, batchesCompleted: 0, batchesSucceeded: 0, batchesFailed: 0, skipped: 0, }; const notify = () => { if (typeof onProgress === 'function') { try { onProgress({ linesRead: stats.linesRead, batchesDispatched: stats.batchesDispatched, batchesCompleted: stats.batchesCompleted, batchesSucceeded: stats.batchesSucceeded, batchesFailed: stats.batchesFailed, recordsProcessed: stats.recordsProcessed, }); } catch { // 忽略用户回调中的异常,避免影响主流程 } } }; // 简单的并发控制器(不创建队列:当活跃任务达到 concurrency 时,读入会等待最早完成的任务) const active = new Set(); async function runLimited(fn) { while (active.size >= concurrency) { if (signal?.aborted) throw abortError(); await Promise.race(active); } let p; p = (async () => { try { return await fn(); } finally { active.delete(p); } })(); active.add(p); return p; } // 写入 NDJSON 的工具函数(处理背压) async function writeNdjson(stream, obj) { const line = JSON.stringify(obj) + '\n'; if (!stream.write(line)) { await once(stream, 'drain'); } } // 解析 Retry-After 头(秒或日期) function parseRetryAfter(headerValue) { if (!headerValue) return null; const secs = Number(headerValue); if (Number.isFinite(secs)) return secs * 1000; const date = new Date(headerValue); const delta = date.getTime() - Date.now(); return Number.isFinite(delta) && delta > 0 ? delta : null; } // 带指数退避重试的 POST async function postWithRetry(url, jsonBody, { signal, maxRetries = 3, baseDelayMs = 500 }) { let attempt = 0; // 最多 attempt = 0..maxRetries,成功或遇到不可重试错误则提前返回 while (attempt <= maxRetries) { if (signal?.aborted) throw abortError(); try { const res = await fetch(url, { method: 'POST', headers: { 'content-type': 'application/json', 'accept': 'application/json, text/plain, */*', }, body: jsonBody, // 将外部 signal 直接传入:可在请求中途取消 signal, keepalive: true, }); const status = res.status; if (res.ok) { // 尝试读取少量响应信息(非强制) let responseSnippet = ''; try { responseSnippet = await res.text(); if (responseSnippet.length > 2048) responseSnippet = responseSnippet.slice(0, 2048); } catch { /* ignore body read errors */ } return { ok: true, status, responseSnippet }; } // 可重试的状态码:5xx/429 if ((status >= 500 && status <= 599) || status === 429) { if (attempt === maxRetries) { const bodyText = await safeReadText(res); return { ok: false, retryable: true, status, bodyText, }; } // 计算等待时间:Retry-After 优先,其次指数退避 + 抖动 const retryAfterMs = parseRetryAfter(res.headers.get('retry-after')); const backoff = retryAfterMs ?? Math.min(30000, baseDelayMs * 2 ** attempt) + Math.floor(Math.random() * 250); await sleep(backoff, signal); attempt += 1; continue; } // 不可重试的 4xx/其它错误 const bodyText = await safeReadText(res); return { ok: false, retryable: false, status, bodyText }; } catch (err) { // fetch 抛出(网络错误/超时/取消) if (isAbortError(err)) throw err; if (attempt === maxRetries) { return { ok: false, retryable: true, status: 0, bodyText: String(err?.message ?? err) }; } const backoff = Math.min(30000, baseDelayMs * 2 ** attempt) + Math.floor(Math.random() * 250); await sleep(backoff, signal); attempt += 1; } } // 正常不会走到此处 return { ok: false, retryable: true, status: 0, bodyText: 'unknown error' }; } function isAbortError(err) { return (err && (err.name === 'AbortError' || err.code === 'ABORT_ERR')); } function abortError() { // 在 Node18+ 中 DOMException 可能不可用,退回 Error try { // eslint-disable-next-line no-undef return new DOMException('The operation was aborted', 'AbortError'); } catch { const e = new Error('The operation was aborted'); e.name = 'AbortError'; return e; } } async function sleep(ms, signal) { if (ms <= 0) return; await new Promise((resolve, reject) => { const t = setTimeout(resolve, ms); const onAbort = () => { clearTimeout(t); reject(abortError()); }; if (signal) { if (signal.aborted) { onAbort(); } else { signal.addEventListener('abort', onAbort, { once: true }); } } }); } async function safeReadText(res) { try { const t = await res.text(); return t.length > 4096 ? t.slice(0, 4096) : t; } catch { return ''; } } function assertPositiveInteger(n, name) { if (!Number.isInteger(n) || n <= 0) { throw new TypeError(`${name} must be a positive integer`); } } // CSV 流式解析(RFC4180 近似实现) async function* readCsvRecords(filePath, { signal }) { const stream = fs.createReadStream(filePath, { encoding: 'utf8', highWaterMark: 1 << 20 }); if (signal) { const onAbort = () => stream.destroy(abortError()); if (signal.aborted) onAbort(); else signal.addEventListener('abort', onAbort, { once: true }); } let buf = ''; let i = 0; let inQuotes = false; let field = ''; let record = []; let sawAny = false; for await (const chunk of stream) { if (!sawAny && chunk.charCodeAt(0) === 0xFEFF) { // 去除 UTF-8 BOM buf += chunk.slice(1); } else { buf += chunk; } sawAny = true; while (i < buf.length) { const ch = buf[i]; if (inQuotes) { if (ch === '"') { const next = buf[i + 1]; if (next === '"') { field += '"'; // 转义双引号 i += 2; continue; } inQuotes = false; i += 1; continue; } field += ch; i += 1; continue; } else { if (ch === '"') { inQuotes = true; i += 1; continue; } if (ch === ',') { record.push(field); field = ''; i += 1; continue; } if (ch === '\n' || ch === '\r') { // 结束一条记录 record.push(field); field = ''; // 跳过 CRLF if (ch === '\r' && buf[i + 1] === '\n') i += 2; else i += 1; yield record; record = []; continue; } field += ch; i += 1; } } // 移除已消费段,保留尾部未完成字段/记录 buf = ''; i = 0; } // 文件结束,输出最后一条(若存在) if (inQuotes) { // 未闭合引号,作为损坏行交给上层处理(会被当作字段数不匹配) } if (field.length > 0 || record.length > 0) { record.push(field); yield record; } } // 表头唯一化(重复列名添加后缀 _2, _3,...) function uniquifyHeaders(headers) { const map = new Map(); return headers.map((h0) => { let h = String(h0 ?? '').trim(); if (h.startsWith('\uFEFF')) h = h.slice(1); // 再次保险去BOM const count = (map.get(h) ?? 0) + 1; map.set(h, count); return count === 1 ? h : `${h}_${count}`; }); } // 主流程:读取 -> 组批 -> 并发上报 const batch = []; let batchId = 0; let rowNumber = 0; // 数据行号(不含表头,首条数据行为 1) let headers = null; let headerLen = 0; const reader = readCsvRecords(csvPath, { signal }); try { // 读取表头 const first = await reader.next(); if (first.done) { await closeStreams(); return finalizeResult(); } headers = uniquifyHeaders(first.value); headerLen = headers.length; // 持续读取数据行 for await (const rec of reader) { if (signal?.aborted) throw abortError(); // 跳过完全空行 if (rec.length === 1 && rec[0].trim() === '') { stats.skipped += 1; await writeNdjson(errorsStream, { status: 'error', reason: 'empty_line', rowNumber: rowNumber + 1, // 预增前的下一行 }); continue; } rowNumber += 1; stats.linesRead += 1; // 字段对齐:严格要求字段数一致,不一致则跳过并记录 if (rec.length !== headerLen) { stats.skipped += 1; await writeNdjson(errorsStream, { status: 'error', reason: 'field_count_mismatch', rowNumber, expected: headerLen, actual: rec.length, raw: rec, }); // 每 1000 行或错误发生时可通知 if (rowNumber % 1000 === 0) notify(); continue; } const obj = {}; for (let i = 0; i < headerLen; i += 1) { obj[headers[i]] = rec[i]; } batch.push({ rowNumber, data: obj }); stats.recordsProcessed += 1; if (batch.length >= batchSize) { const toSend = batch.splice(0, batch.length); stats.batchesDispatched += 1; const myBatchId = ++batchId; // 并发受限执行 runLimited(() => handleBatch(myBatchId, toSend)).catch(async (err) => { // 将内部错误冒泡:取消时会是 AbortError,其他错误也终止流程 // 尽快关闭流并重抛 await closeStreams().catch(() => {}); throw err; }); // 每发送一个批次就通知一次 notify(); } if (rowNumber % 1000 === 0) { notify(); } } // 发送最后不足一批的数据 if (batch.length > 0) { const toSend = batch.splice(0, batch.length); stats.batchesDispatched += 1; const myBatchId = ++batchId; runLimited(() => handleBatch(myBatchId, toSend)).catch(async (err) => { await closeStreams().catch(() => {}); throw err; }); notify(); } // 等待所有进行中的任务完成 while (active.size > 0) { if (signal?.aborted) throw abortError(); await Promise.race(active); } await closeStreams(); return finalizeResult(); } catch (err) { // 主流程异常(包含取消) await closeStreams().catch(() => {}); throw err; } async function handleBatch(myBatchId, items) { if (signal?.aborted) throw abortError(); // 准备 JSON body(仅发送 data 对象数组) const payload = items.map((it) => it.data); const jsonBody = JSON.stringify(payload); const res = await postWithRetry(endpoint, jsonBody, { signal, maxRetries: 3, baseDelayMs: 500 }); stats.batchesCompleted += 1; if (res.ok) { stats.batchesSucceeded += 1; // 对成功的每条记录写入 success.ndjson(保留可观测性) // 如需降采样,可考虑仅写入批级记录 for (const it of items) { await writeNdjson(successStream, { status: 'success', batchId: myBatchId, rowNumber: it.rowNumber, endpoint, responseStatus: 200, }); } } else { stats.batchesFailed += 1; // 将整个批次写入死信文件(逐条记录,包含失败原因) const reason = res.retryable ? 'network_or_server' : 'client_error'; const message = res.bodyText ? truncate(res.bodyText, 2048) : undefined; for (const it of items) { await writeNdjson(errorsStream, { status: 'error', reason, batchId: myBatchId, rowNumber: it.rowNumber, endpoint, httpStatus: res.status, message, data: it.data, }); } } notify(); } function truncate(s, n) { return s && s.length > n ? s.slice(0, n) : s; } async function closeStreams() { await Promise.all([ new Promise((resolve) => successStream.end(resolve)), new Promise((resolve) => errorsStream.end(resolve)), ]); } function finalizeResult() { const elapsedMs = Date.now() - start; return { total: stats.recordsProcessed, batchesSucceeded: stats.batchesSucceeded, batchesFailed: stats.batchesFailed, skipped: stats.skipped, elapsedMs, files: { success: successPath, errors: errorsPath, }, }; } } </code></pre> <p>错误处理</p> <ul> <li>全链路取消支持:通过 options.signal 将 AbortSignal 传入 CSV 读取、重试等待与 fetch 请求中。一旦外部取消,立即抛出 AbortError 并中止流程。</li> <li>CSV 解析异常: <ul> <li>引号未闭合、字段数与表头不一致、空行等,均计入 skipped,并写入 errors.ndjson,便于后续排查。</li> </ul> </li> <li>网络/服务端错误: <ul> <li>对 5xx 与 429 进行最多 3 次指数退避重试,并支持 Retry-After 头;网络异常(如 ECONNRESET)同样重试。</li> <li>超过重试上限仍失败的批次,将整批写入 errors.ndjson(逐记录记录),并计入 batchesFailed。</li> </ul> </li> <li>客户端错误(如 4xx 非 429):视为不可重试错误,直接写入 errors.ndjson。</li> <li>写入流错误:监听 success.ndjson 与 errors.ndjson 的 error 事件,任何写入异常都会终止流程并抛出。</li> </ul> <p>使用示例</p> <pre><code class="language-js">import { processCsvAndUpload } from './processCsvAndUpload.js'; const ac = new AbortController(); (async () => { try { const result = await processCsvAndUpload( './data/input.csv', 'https://api.example.com/ingest/batch', { batchSize: 200, concurrency: 4, signal: ac.signal, onProgress: ({ linesRead, batchesDispatched, batchesCompleted, batchesSucceeded, batchesFailed, recordsProcessed }) => { console.log( `lines=${linesRead}, dispatched=${batchesDispatched}, completed=${batchesCompleted}, ok=${batchesSucceeded}, fail=${batchesFailed}, records=${recordsProcessed}` ); }, } ); console.log('Done:', result); // 预期输出: // { // total: 12345, // batchesSucceeded: 60, // batchesFailed: 2, // skipped: 10, // elapsedMs: 9876, // files: { success: '/abs/path/success.ndjson', errors: '/abs/path/errors.ndjson' } // } } catch (e) { if (e.name === 'AbortError') { console.error('Canceled by user'); } else { console.error('Failed:', e); } } })(); // 在需要时取消 // ac.abort(); </code></pre> <p>技术要点</p> <ul> <li>流式 CSV 解析: <ul> <li>自实现近 RFC4180 的解析器,逐字符处理引号、转义双引号、CRLF/LF 行尾,避免一次性读入整个文件。</li> <li>去除 UTF-8 BOM;对重复列名自动唯一化,保证对象键无冲突。</li> </ul> </li> <li>分批与并发: <ul> <li>使用轻量并发控制器(Set + Promise.race)限制并发数,实现读入与上传的流水线式处理,减少内存占用。</li> <li>读入端仅在达成批次时才等待并发空位,有效平衡 I/O 与网络的吞吐。</li> </ul> </li> <li>可靠性: <ul> <li>对 5xx/429/网络错误进行指数退避重试,并优先尊重 Retry-After;加入抖动防止同步重试。</li> <li>将不可重试错误与超过重试上限的请求统一写入 errors.ndjson(逐记录),提升可观测性和后续补偿操作的便利性。</li> <li>成功写入 success.ndjson(逐记录),若数据量极大可调整为批级记录以降低 I/O。</li> </ul> </li> <li>取消与背压: <ul> <li>将 AbortSignal 贯穿 CSV 读取、sleep、fetch;任何阶段取消都会快速失败。</li> <li>NDJSON 写入考虑背压,使用 drain 事件进行 await,避免内存暴涨。</li> </ul> </li> <li>统计与进度: <ul> <li>提供 linesRead、recordsProcessed、批次派发/完成/成功/失败等指标;默认在派发批次、完成批次与每 1000 行时进行回调。</li> </ul> </li> <li>兼容性与安全: <ul> <li>使用 Node.js 内置 fetch(Node >= 18),无第三方依赖。</li> <li>避免使用弃用 API;严格校验输入参数,防止错误使用。</li> <li>HTTP 请求限定为 POST JSON,设置合理的 Accept/Content-Type,避免注入风险。</li> </ul> </li> <li>可扩展建议: <ul> <li>如果服务端支持部分成功反馈,可在成功回包中解析并区分记录级成功/失败,细化 success/errors 记录。</li> <li>如需严格内存控制,可在并发达到上限时暂停文件读取(当前设计通过等待最早完成任务达到类似效果;必要时可接入 stream.pause/resume)。</li> </ul> </li> </ul> </div> </div> </div> </div> </div> </section> <div id="examplePopupModal" class="prompt-examples-v4-modal"> <div class="prompt-examples-v4-modal-content"> <div class="prompt-examples-v4-modal-header"> <h3 id="popupTitle">示例详情</h3> <button class="prompt-examples-v4-modal-close" onclick="closeExamplePopup()"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <line x1="18" y1="6" x2="6" y2="18"></line> <line x1="6" y1="6" x2="18" y2="18"></line> </svg> </button> </div> <div class="prompt-examples-v4-modal-body"> <div id="popupContent" class="prompt-examples-v4-popup-content"></div> </div> </div> </div> <script> // 弹窗功能JavaScript function showExamplePopup(exampleIndex, title, buttonElement) { const modal = document.getElementById('examplePopupModal'); const popupTitle = document.getElementById('popupTitle'); const popupContent = document.getElementById('popupContent'); // 设置弹窗标题 popupTitle.textContent = title; // 获取对应示例的内容 const tabContent = document.getElementById('example-' + exampleIndex); const outputText = tabContent.querySelector('.prompt-examples-v4-output-text'); if (outputText) { // 克隆内容到弹窗中 popupContent.innerHTML = outputText.innerHTML; } else { popupContent.innerHTML = '<p>暂无内容</p>'; } // 显示弹窗 modal.classList.add('show'); document.body.style.overflow = 'hidden'; // 防止背景滚动 } function closeExamplePopup() { const modal = document.getElementById('examplePopupModal'); modal.classList.remove('show'); document.body.style.overflow = ''; // 恢复背景滚动 } // 点击背景关闭弹窗 document.addEventListener('DOMContentLoaded', function() { const modal = document.getElementById('examplePopupModal'); if (modal) { modal.addEventListener('click', function(e) { if (e.target === modal) { closeExamplePopup(); } }); } // ESC键关闭弹窗 document.addEventListener('keydown', function(e) { if (e.key === 'Escape') { closeExamplePopup(); } }); }); </script> <script> document.addEventListener('DOMContentLoaded', function() { // Tab切换功能 const tabs = document.querySelectorAll('.prompt-examples-v4-tab'); const contents = document.querySelectorAll('.prompt-examples-v4-tab-content'); tabs.forEach(tab => { tab.addEventListener('click', function() { const targetId = this.getAttribute('data-tab'); // 移除所有active状态 tabs.forEach(t => t.classList.remove('active')); contents.forEach(c => c.classList.remove('active')); // 添加当前active状态 this.classList.add('active'); const targetContent = document.getElementById(targetId); if (targetContent) { targetContent.classList.add('active'); } }); }); }); </script> </div> <section class="usage-section"> <h2 class="section-title">📖 如何使用</h2> <div class="usage-tabs"> <button class="usage-tab active" onclick="switchUsageTab('chat')">Chat 用户</button> <button class="usage-tab" onclick="switchUsageTab('developer')">开发者</button> </div> <div class="usage-content active" id="chat-content"> <div class="usage-card"> <div class="usage-card-title"> <span>⚡</span> 30秒出活:复制 → 粘贴 → 搞定 </div> <div class="usage-card-desc"> 与其花几十分钟和AI聊天、试错,不如直接复制这些经过千人验证的模板,修改几个 {{变量}} 就能立刻获得专业级输出。省下来的时间,足够你轻松享受两杯咖啡! </div> <a href="javascript:void(0)" class="usage-card-action disabled" id="usageCopyBtn"> <span>加载中...</span> <span></span> </a> </div> <div class="usage-card"> <div class="usage-card-title"> <span>💬</span> 不会填参数?让 AI 反过来问你 </div> <div class="usage-card-desc"> 不确定变量该填什么?一键转为对话模式,AI 会像资深顾问一样逐步引导你,问几个问题就能自动生成完美匹配你需求的定制结果。零门槛,开口就行。 </div> <a href="https://tools.explinks.com/prompt-interactive?slug=js_async_function_generator" class="usage-card-action"> <span>转为对话模式</span> <span>→</span> </a> </div> <div class="usage-card"> <div class="usage-card-title"> <span>🚀</span> 告别复制粘贴,Chat 里直接调用 </div> <div class="usage-card-desc"> 无需切换,输入 / 唤醒 8000+ 专家级提示词。 插件将全站提示词库深度集成于 Chat 输入框。基于当前对话语境,系统智能推荐最契合的 Prompt 并自动完成参数化,让海量资源触手可及,从此彻底告别"手动搬运"。 </div> <span class="usage-card-action" style="background:linear-gradient(135deg,#94a3b8,#64748b);box-shadow:0 4px 12px rgba(100,116,139,0.3);cursor:default;pointer-events:none;"> <span>即将推出</span> </span> </div> </div> <div class="usage-content" id="developer-content"> <div class="usage-card"> <div class="usage-card-title"> <span>🔌</span> 接口一调,提示词自己会进化 </div> <div class="usage-card-desc"> 手动跑一次还行,跑一百次呢?通过 API 接口动态注入变量,接入批量评价引擎,让程序自动迭代出更高质量的提示词方案。Prompt 会自己进化,你只管收结果。 </div> <a href="https://tools.explinks.com/prompt-api?slug=js_async_function_generator" class="usage-card-action"> <span>发布 API</span> <span>→</span> </a> </div> <div class="usage-card"> <div class="usage-card-title"> <span>🤖</span> 一键变成你的专属 Agent 应用 </div> <div class="usage-card-desc"> 不想每次都配参数?把这条提示词直接发布成独立 Agent,内嵌图片生成、参数优化等工具,分享链接就能用。给团队或客户一个"开箱即用"的完整方案。 </div> <a href="javascript:void(0)" class="usage-card-action disabled"> <span>创建 Agent</span> <span>→</span> </a> </div> </div> </section> <script> // 切换使用指南标签页 function switchUsageTab(tab) { document.querySelectorAll('.usage-tab').forEach(t => t.classList.remove('active')); document.querySelectorAll('.usage-content').forEach(c => c.classList.remove('active')); if (event) { event.target.classList.add('active'); } else { // 提供一个备用方式找到对应的tab let tabs = document.querySelectorAll('.usage-tab'); for(let i = 0; i < tabs.length; i++) { if(tabs[i].getAttribute('onclick').includes(tab)) { tabs[i].classList.add('active'); break; } } } const contentArea = document.getElementById(tab + '-content'); if (contentArea) { contentArea.classList.add('active'); } } // 优雅的 Toast 提示组件 function customToast(message, isError = false) { // 如果页面已经有相同提示,先移除 const existingToast = document.getElementById('custom-toast-message'); if (existingToast) { existingToast.remove(); } const toast = document.createElement('div'); toast.id = 'custom-toast-message'; toast.textContent = message; // 极简拟物化样式 Object.assign(toast.style, { position: 'fixed', top: '40px', left: '50%', transform: 'translateX(-50%) translateY(-20px)', background: isError ? '#fee2e2' : '#ffffff', color: isError ? '#ef4444' : '#10b981', padding: '12px 24px', borderRadius: '12px', boxShadow: '0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1)', border: isError ? '1px solid #fecaca' : '1px solid #d1fae5', fontSize: '14px', fontWeight: '600', zIndex: '9999', opacity: '0', transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)', display: 'flex', alignItems: 'center', gap: '8px' }); // 成功状态添加一个小勾,失败添加一个叉 const icon = document.createElement('span'); icon.textContent = isError ? '❌' : '✓'; toast.prepend(icon); document.body.appendChild(toast); // 触发动画 requestAnimationFrame(() => { toast.style.opacity = '1'; toast.style.transform = 'translateX(-50%) translateY(0)'; }); // 3秒后自动消失然后解除 DOM setTimeout(() => { toast.style.opacity = '0'; toast.style.transform = 'translateX(-50%) translateY(-20px)'; setTimeout(() => toast.remove(), 300); }, 3000); } // 复制提示词功能(调用已经有的方法) function copyPromptContent() { const contentArea = document.getElementById('leftPromptContentArea'); if (contentArea && contentArea.textContent) { const textToCopy = contentArea.textContent; if (typeof copyTextToClipboard === 'function') { copyTextToClipboard(textToCopy, () => { if (typeof showToast === 'function') showToast('复制成功'); else customToast('提示词已复制到剪贴板'); }); } else if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(textToCopy).then(() => { customToast('提示词已复制到剪贴板'); }).catch(() => { fallbackCopyTextToClipboard(textToCopy); }); } else { fallbackCopyTextToClipboard(textToCopy); } } else { customToast('获取提示词内容失败,请先获取内容或联系管理员!', true); } } // 针对非安全环境(http)的旧版浏览器复制降级方案 function fallbackCopyTextToClipboard(text) { var textArea = document.createElement("textarea"); textArea.value = text; // 防止页面滚动和闪烁 textArea.style.top = "0"; textArea.style.left = "0"; textArea.style.position = "fixed"; textArea.style.opacity = "0"; document.body.appendChild(textArea); textArea.focus(); textArea.select(); try { var successful = document.execCommand('copy'); if (successful) { if (typeof showToast === 'function') showToast('复制成功'); else customToast('提示词已复制到剪贴板'); } else { customToast('复制失败,请手动选中并复制', true); } } catch (err) { customToast('复制失败,浏览器不支持', true); } document.body.removeChild(textArea); } // 动态更新"复制提示词"按钮状态 function updateUsageCopyBtnState() { const btn = document.getElementById('usageCopyBtn'); if (!btn) return; // 检查是否登录 var token = (function() { var m = document.cookie.match(/(?:^|; )Admin-Token=([^;]+)/); return m ? decodeURIComponent(m[1]) : null; })(); if (!token) { // 未登录 btn.classList.remove('disabled'); btn.innerHTML = '<span>会员登录后可复制</span><span>→</span>'; btn.onclick = function(e) { e.preventDefault(); if (typeof openLoginDialog === 'function') { openLoginDialog(); } else { window.location.href = '/login'; } }; return; } // 已登录,检查是否为会员 if (typeof window.fetchVipStatus === 'function') { window.fetchVipStatus().then(function(result) { btn.classList.remove('disabled'); if (result && result.isVip) { // VIP 会员 btn.innerHTML = '<span>复制提示词</span><span>→</span>'; btn.onclick = function(e) { e.preventDefault(); copyPromptContent(); }; } else { // 非终身会员 btn.innerHTML = '<span>终身会员免费复制</span><span>→</span>'; btn.onclick = function(e) { e.preventDefault(); window.open('/user/vip', '_blank'); }; } }).catch(function() { btn.innerHTML = '<span>状态异常,请刷新</span><span></span>'; }); } else { // 兜底:获取不到方法时,默认要求开通会员 btn.classList.remove('disabled'); btn.innerHTML = '<span>终身会员免费复制</span><span>→</span>'; btn.onclick = function(e) { e.preventDefault(); window.open('/user/vip', '_blank'); }; } } document.addEventListener('DOMContentLoaded', updateUsageCopyBtnState); // 监听登录成功消息重新渲染 window.addEventListener('message', function(event) { if (event.data === 'loginSuccess') { setTimeout(updateUsageCopyBtnState, 300); } }); </script> <div class="features-summary-section"> <h2 class="section-title">✅ 特性总结</h2> <div class="features-summary-list"> <div class="feature-summary-item"> <span class="feature-summary-bullet"></span> <span class="feature-summary-text">一键生成符合最佳实践的异步函数,内置注释与说明,直接拷贝可用。</span> </div> <div class="feature-summary-item"> <span class="feature-summary-bullet"></span> <span class="feature-summary-text">自动加上超时与重试策略,网络不稳也能稳妥返回,显著减少线上告警。</span> </div> <div class="feature-summary-item"> <span class="feature-summary-bullet"></span> <span class="feature-summary-text">内建错误分类与兜底提示,异常可追踪可定位,排查成本更可控更透明。</span> </div> <div class="feature-summary-item"> <span class="feature-summary-bullet"></span> <span class="feature-summary-text">支持并发与队列控制,批量请求不阻塞,稳定提升整体吞吐与响应速度。</span> </div> <div class="feature-summary-item"> <span class="feature-summary-bullet"></span> <span class="feature-summary-text">可按业务场景定制参数与模板,团队统一规范,跨项目快速复用落地。</span> </div> <div class="feature-summary-item"> <span class="feature-summary-bullet"></span> <span class="feature-summary-text">自动生成使用示例与调用说明,新人也能即刻上手,减少口头传授成本。</span> </div> <div class="feature-summary-item"> <span class="feature-summary-bullet"></span> <span class="feature-summary-text">针对数据获取、文件处理等常见任务,给出稳健方案,避免重复造轮子。</span> </div> <div class="feature-summary-item"> <span class="feature-summary-bullet"></span> <span class="feature-summary-text">结合边界条件检查与输入校验,预防隐形缺陷,降低不可预期风险。</span> </div> <div class="feature-summary-item"> <span class="feature-summary-bullet"></span> <span class="feature-summary-text">输出结构清晰、注释标准,代码易读易测,方便后续扩展与维护工作。</span> </div> </div> </div> <div class="problem-section"> <h2 class="section-title">🎯 解决的问题</h2> <div class="scenario-card"> <div class="scenario-text"> <div class="html-content-display solved-problems-text"> <p>为前端/全栈/Node 开发者与技术团队提供一键式的异步函数生成体验:基于简单的任务描述,自动产出符合最佳实践的 async/await 代码、完善的错误处理、清晰的使用示例与关键说明;显著缩短开发与评审时间、降低线上故障风险、统一团队编码风格;覆盖数据获取、文件读写与服务请求等高频场景,帮助个人快速交付、团队高质量复用,并推动从试用到持续付费的转化。</p> </div> </div> </div> </div> <style> .problem-section { margin-bottom: 32px; } .problem-section .scenario-card { background-color: #f8f9fa; border-radius: 12px; padding: 24px; } .problem-section .scenario-text { color: #333; line-height: 1.6; } .problem-section .scenario-text .solved-problems-text { background-color: transparent !important; border: none !important; padding: 0 !important; margin: 0 !important; font-size: 15px; } </style> <div class="version-history-section"> <h2 class="section-title">🕒 版本历史</h2> <div class="coming-soon-wrapper"> <div class="coming-soon-content"> <div class="version-timeline"> <div class="version-item current"> <div class="version-badge">当前版本</div> <div class="version-header"> <span class="version-number">v2.1</span> <span class="version-date">2024-01-15</span> </div> <div class="version-title">优化输出结构,增强情节连贯性</div> <ul class="version-changes"> <li>✨ 新增章节节奏控制参数</li> <li>🔧 优化人物关系描述逻辑</li> <li>📝 改进主题深化引导语</li> <li>🎯 增强情节转折点设计</li> </ul> </div> <div class="version-item "> <div class="version-header"> <span class="version-number">v2.0</span> <span class="version-date">2023-12-20</span> </div> <div class="version-title">重构提示词架构,提升生成质量</div> <ul class="version-changes"> <li>🚀 全新的提示词结构设计</li> <li>📊 增加输出格式化选项</li> <li>💡 优化角色塑造引导</li> </ul> </div> <div class="version-item "> <div class="version-header"> <span class="version-number">v1.5</span> <span class="version-date">2023-11-10</span> </div> <div class="version-title">修复已知问题,提升稳定性</div> <ul class="version-changes"> <li>🐛 修复长文本处理bug</li> <li>⚡ 提升响应速度</li> </ul> </div> <div class="version-item "> <div class="version-header"> <span class="version-number">v1.0</span> <span class="version-date">2023-10-01</span> </div> <div class="version-title">首次发布</div> <ul class="version-changes"> <li>🎉 初始版本上线</li> </ul> </div> </div> </div> <div class="coming-soon-overlay"> <div class="coming-soon-badge">COMING SOON</div> <div class="coming-soon-text">版本历史追踪,即将启航</div> <div class="coming-soon-subtext">记录每一次提示词的进化与升级,敬请期待。</div> </div> </div> </div> <div class="reviews-section"> <div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 24px;"> <h2 class="section-title" style="margin-bottom: 0;">💬 用户评价</h2> </div> <div class="coming-soon-wrapper"> <div class="coming-soon-content"> <div class="reviews-stats"> <div class="rating-summary"> <div class="rating-score">4.8</div> <div class="rating-stars">⭐⭐⭐⭐⭐</div> <div class="rating-count">基于 28 条评价</div> </div> <div class="rating-bars"> <div class="rating-bar-item"> <span class="rating-label">5星</span> <div class="rating-bar"> <div class="rating-bar-fill" style="width: 85%"></div> </div> <span class="rating-percent">85%</span> </div> <div class="rating-bar-item"> <span class="rating-label">4星</span> <div class="rating-bar"> <div class="rating-bar-fill" style="width: 12%"></div> </div> <span class="rating-percent">12%</span> </div> <div class="rating-bar-item"> <span class="rating-label">3星</span> <div class="rating-bar"> <div class="rating-bar-fill" style="width: 3%"></div> </div> <span class="rating-percent">3%</span> </div> </div> </div> <div class="reviews-list"> <div class="review-item"> <div class="review-header"> <div class="review-avatar">👤</div> <div class="review-info"> <div class="review-author">电商运营 - 张先生</div> <div class="review-meta"> <span class="review-stars">⭐⭐⭐⭐⭐</span> <span class="review-date">2025-01-15</span> </div> </div> </div> <div class="review-content"> 双十一用这个提示词生成了20多张海报,效果非常好!点击率提升了35%,节省了大量设计时间。参数调整很灵活,能快速适配不同节日。 </div> <div class="review-tags"> <span class="review-tag">效果好</span> <span class="review-tag">节省时间</span> </div> </div> <div class="review-item"> <div class="review-header"> <div class="review-avatar">👤</div> <div class="review-info"> <div class="review-author">品牌设计师 - 李女士</div> <div class="review-meta"> <span class="review-stars">⭐⭐⭐⭐⭐</span> <span class="review-date">2025-01-10</span> </div> </div> </div> <div class="review-content"> 作为设计师,这个提示词帮我快速生成创意方向,大大提升了工作效率。生成的海报氛围感很强,稍作调整就能直接使用。 </div> <div class="review-tags"> <span class="review-tag">创意好</span> <span class="review-tag">专业</span> </div> </div> </div> <div class="reviews-footer"> <button class="load-more-reviews" onclick="alert('加载更多评论功能开发中...')"> 查看更多评价 </button> </div> </div> <div class="coming-soon-overlay"> <div class="coming-soon-badge">COMING SOON</div> <div class="coming-soon-text">用户评价与反馈系统,即将上线</div> <div class="coming-soon-subtext">倾听真实反馈,在这里留下您的使用心得,敬请期待。</div> </div> </div> </div> <div class="feedback-modal" id="feedbackModal" onclick="closeFeedbackModal(event)"> <div class="feedback-modal-content" onclick="event.stopPropagation()"> <button class="feedback-close" onclick="closeFeedbackModal()">×</button> <div class="feedback-header"> <div class="feedback-icon">💬</div> <h2 class="feedback-title">提交反馈</h2> <p class="feedback-subtitle">您的反馈对我们非常重要</p> <div class="feedback-tip"> <span class="feedback-tip-icon">💡</span> <span class="feedback-tip-text">您的真实感受,是提示词通往完美的最后一块拼图。</span> </div> </div> <form class="feedback-form" onsubmit="handleFeedbackSubmit(event)"> <div class="feedback-rating-group"> <label class="feedback-label">整体评价 <span style="color: #ef4444;">*</span></label> <div class="feedback-rating-stars" id="ratingStars"> <span class="rating-star active" data-rating="1" onclick="setRating(1)">★</span> <span class="rating-star active" data-rating="2" onclick="setRating(2)">★</span> <span class="rating-star active" data-rating="3" onclick="setRating(3)">★</span> <span class="rating-star active" data-rating="4" onclick="setRating(4)">★</span> <span class="rating-star active" data-rating="5" onclick="setRating(5)">★</span> </div> <input type="hidden" name="rating" id="ratingValue" value="5" required> <div class="rating-text" id="ratingText">非常满意</div> </div> <div class="feedback-type-group"> <label class="feedback-label">反馈类型</label> <div class="feedback-type-options"> <label class="feedback-type-option"> <input type="radio" name="feedbackType" value="bug"> <span class="feedback-type-label">🪲 Bug反馈</span> </label> <label class="feedback-type-option"> <input type="radio" name="feedbackType" value="feature"> <span class="feedback-type-label">💡 功能建议</span> </label> <label class="feedback-type-option"> <input type="radio" name="feedbackType" value="content"> <span class="feedback-type-label">📝 内容问题</span> </label> <label class="feedback-type-option"> <input type="radio" name="feedbackType" value="review" checked> <span class="feedback-type-label">⭐ 使用评价</span> </label> </div> </div> <div class="feedback-input-group"> <label class="feedback-label">反馈内容 <span style="color: #ef4444;">*</span></label> <textarea class="feedback-textarea" id="feedbackContent" name="feedbackContent" placeholder="请详细描述您遇到的问题或建议..." maxlength="500" required oninput="updateFeedbackCharCount()" ></textarea> <div class="feedback-char-count"><span id="feedbackCharCount">0</span> / 500</div> </div> <button type="submit" class="feedback-submit-btn">提交反馈</button> </form> </div> </div> <script> // 打开反馈弹窗 function openFeedbackModal() { var modal = document.getElementById('feedbackModal'); if(modal) { modal.classList.add('show'); document.body.style.overflow = 'hidden'; } } // 设置星级评分 function setRating(rating) { var stars = document.querySelectorAll('.rating-star'); var ratingTexts = ['很不满意', '不满意', '一般', '满意', '非常满意']; // 更新星星显示 for(var i = 0; i < stars.length; i++) { if (i < rating) { stars[i].classList.add('active'); } else { stars[i].classList.remove('active'); } } // 更新隐藏字段和文字 var ratingValueInput = document.getElementById('ratingValue'); var ratingTextDiv = document.getElementById('ratingText'); if(ratingValueInput) ratingValueInput.value = rating; if(ratingTextDiv) ratingTextDiv.textContent = ratingTexts[rating - 1]; } // 关闭反馈弹窗 function closeFeedbackModal(event) { if (!event || event.target.id === 'feedbackModal' || event.target.classList.contains('feedback-close')) { var modal = document.getElementById('feedbackModal'); if(modal) { modal.classList.remove('show'); } document.body.style.overflow = ''; // 重置表单 var form = document.querySelector('.feedback-form'); if(form) form.reset(); var charCount = document.getElementById('feedbackCharCount'); if(charCount) charCount.textContent = '0'; // 重置星级为5星 setRating(5); } } // 更新字符计数 function updateFeedbackCharCount() { var contentElem = document.getElementById('feedbackContent'); var countElem = document.getElementById('feedbackCharCount'); if(contentElem && countElem) { countElem.textContent = contentElem.value.length; } } // 处理反馈提交 function handleFeedbackSubmit(event) { event.preventDefault(); var form = event.target; var formData = new FormData(form); var feedbackData = { type: formData.get('feedbackType'), rating: formData.get('rating'), content: formData.get('feedbackContent') }; console.log('Feedback submitted:', feedbackData); alert('感谢您的反馈!您的评价已经提交。'); closeFeedbackModal({target: {id: 'feedbackModal'}}); } </script> </div> <div class="prompt-detail-v5-right"> <div class="prompt-info-right right-panel"> <div id="action-panel"> <div class="action-card" style="text-align: center; color: #94a3b8; padding: 40px 20px;"> 加载中... </div> </div> </div> <script> document.addEventListener('DOMContentLoaded', function() { renderActionPanel(); }); // 监听登录或支付成功消息重新渲染(强制刷新 Promise 缓存) window.addEventListener('message', function(event) { if (event.data === 'loginSuccess' || event.data === 'vipSuccess') { setTimeout(function() { window._vipStatusPromise = null; // 清除旧的 Promise 缓存 renderActionPanel(); }, 300); } }); function renderActionPanel() { var panel = document.getElementById('action-panel'); // 检查是否登录 var token = (function() { var m = document.cookie.match(/(?:^|; )Admin-Token=([^;]+)/); return m ? decodeURIComponent(m[1]) : null; })(); if (!token) { // 未登录 renderNonVipPanel(); return; } // 已登录,复用全局 fetchVipStatus(与 Header 共享同一请求,不会重复调用接口) if (typeof window.fetchVipStatus === 'function') { window.fetchVipStatus().then(function(result) { if (result && result.isVip) { renderVipPanel(); } else { renderNonVipPanel(); } }); } else { // 兜底:如果 header 还没加载完 renderNonVipPanel(); } } // 提示词定制化工具数据 var toolsBaseUrl = "https://tools.explinks.com"; var productBaseUrl = "https://prompts.explinks.com"; var slug = "js_async_function_generator"; var panelTools = [ { id: 'formatter', icon: '📝', name: '参数填写器', desc: '可视化填写变量参数', color: 'purple', toolPath: '/prompt-formatter', productPath: '/product/formatter' }, { id: 'interactive', icon: '💬', name: 'Web Chat 适配器', desc: '转为交互式对话脚本', color: 'green', toolPath: '/prompt-interactive', productPath: '/product/interactive' }, { id: 'translate', icon: '🌐', name: '本地化翻译器', desc: '一键翻译为多语言版本', color: 'blue', toolPath: '/prompt-translate', productPath: '/product/translate' }, { id: 'optimizer', icon: '🎯', name: '个性化调校', desc: '根据场景微调提示词', color: 'orange', toolPath: '/prompt-optimizer', productPath: '/product/optimizer' }, { id: 'api', icon: '⚡', name: 'API 动态调校', desc: '接口调用 + 批量评价优化', color: 'cyan', toolPath: '/prompt-api', productPath: '/product/api' }, // { id: 'agent', icon: '🤖', name: '生成个人工作助手', desc: '发布为独立 Agent 应用', color: 'pink', badge: '即将推出' }, // { id: 'batch', icon: '📊', name: '批量数据生成器', desc: 'Form表单或Excel上传,批量生成结果', color: 'amber', badge: '即将推出' } ]; function renderToolsHtml(isLoggedIn) { return panelTools.map(function(tool) { var badgeHtml = tool.badge ? '<span class="panel-tool-badge">' + tool.badge + '</span>' : ''; var href = 'javascript:void(0)'; var targetHtml = ''; if (tool.badge) { href = 'javascript:window.showToast("功能开发中")'; } else { // 统一跳转到产品介绍页,不区分登录状态 href = productBaseUrl + tool.productPath; targetHtml = ' target="_blank"'; } return '<a class="panel-tool-item" href="' + href + '"' + targetHtml + '>' + '<div class="panel-tool-icon ' + tool.color + '">' + tool.icon + '</div>' + '<div class="panel-tool-info">' + '<div class="panel-tool-name">' + tool.name + '</div>' + '<div class="panel-tool-desc">' + tool.desc + '</div>' + '</div>' + '<span class="panel-tool-arrow">›</span>' + badgeHtml + '</a>'; }).join(''); } function renderVipPanel() { var panel = document.getElementById('action-panel'); panel.innerHTML = '' + '' + '<button class="copy-core-btn" id="rightCopyBtn" onclick="handleRightCopyClick()">' + '<span class="copy-icon">📋</span>' + '<span>复制提示词</span>' + '</button>' + '<div class="copy-sub-info">' + '<span class="free-badge">会员</span>' + '<span>无限次复制 · 已解锁全部工具</span>' + '</div>' + '' + '<div class="panel-member-active">' + '<span class="panel-member-active-icon">✨</span>' + '<div class="panel-member-active-info">' + '<div class="panel-member-active-title">终身会员</div>' + '<div class="panel-member-active-desc">已解锁全部功能</div>' + '</div>' + '</div>' + '' + '<div class="panel-tools-section">' + '<div class="panel-tools-title">🛠 提示词定制化工具</div>' + '<div class="panel-tools-subtitle">5 种 AI 工具,把同一条提示词变成你的专属版本</div>' + '<div class="panel-tools-grid">' + renderToolsHtml(true) + '</div>' + '</div>'; } function renderNonVipPanel() { var panel = document.getElementById('action-panel'); var currentPrice = 188; // 按钮文案根据登录状态差异化显示,但点击行为统一打开弹窗 var token = (function() { var m = document.cookie.match(/(?:^|; )Admin-Token=([^;]+)/); return m ? decodeURIComponent(m[1]) : null; })(); var isLoggedIn = !!token; var copyBtnText = isLoggedIn ? '终身会员免费复制' : '会员登录后可复制'; panel.innerHTML = '' + '' + '<button class="copy-core-btn" id="rightCopyBtn" onclick="handleRightCopyClick()">' + '<span>' + copyBtnText + '</span>' + '</button>' + '' + '<div class="panel-member-hint">' + '<p class="panel-member-hint-text">' + '开通会员解锁 <strong>无限复制</strong> + 全部工具<br>' + '<span style="font-size:12px; color:#64748b;">¥' + currentPrice + ' 终身 · 7天无理由退款</span>' + '</p>' + '<a href="/user/vip" target="_blank" class="panel-member-link">' + '了解会员权益 <span>→</span>' + '</a>' + '</div>' + '' + '<div class="panel-tools-section">' + '<div class="panel-tools-title">🛠 提示词定制化工具</div>' + '<div class="panel-tools-subtitle">5 种 AI 工具,把同一条提示词变成你的专属版本</div>' + '<div class="panel-tools-grid">' + renderToolsHtml(isLoggedIn) + '</div>' + '</div>'; } function handleRightCopyClick() { const contentArea = document.getElementById('leftPromptContentArea'); if (!contentArea || !contentArea.textContent.trim()) { if (typeof window.showToast === 'function') { window.showToast('提示词内容未加载完全,或无权限复制', true); } else { alert('提示词内容未加载完全,或无权限复制'); } return; } if (typeof openFillCopyModal === 'function') { openFillCopyModal({ promptName: window.__promptName || '', promptContent: contentArea.textContent, promptSlug: window.__promptSlug || '', apiBaseUrl: window.__promptApiBaseUrl || '', onCopySuccess: function() { var btn = document.getElementById('rightCopyBtn'); if (btn) { btn.classList.add('copied'); btn.innerHTML = '<span class="copy-icon">✅</span><span>已复制到剪贴板</span>'; setTimeout(function() { btn.classList.remove('copied'); btn.innerHTML = '<span class="copy-icon">📋</span><span>复制提示词</span>'; }, 2000); } } }); } } </script> </div> </div> </div> <script src="https://static.explinks.com/prompt/static/js/home/zone-section.js?v=1775816724940"></script> <div id="footer"> <div class="footer-container"> <div class="footer-content"> <div class="footer-section"> <h5 class="footer-section-title"> 热门提示词</h5> <ul class="footer-section-links"> <li> <a href="https://prompts.explinks.com/quick_keyword_generator" target="_blank">快速关键词生成助手</a> </li> <li> <a href="https://prompts.explinks.com/content_strategy_guide" target="_blank">内容营销策略制定</a> </li> <li> <a href="https://prompts.explinks.com/rapid_test_scenario" target="_blank">快速测试场景生成器</a> </li> <li> <a href="https://prompts.explinks.com/novel_writing_guide" target="_blank">小说创作策略指南</a> </li> <li> <a href="https://prompts.explinks.com/seo_keyword_generator" target="_blank">SEO优化关键词生成助手</a> </li> <li> <a href="https://prompts.explinks.com/article_title_generator" target="_blank">文章标题生成器</a> </li> </ul> </div> <div class="footer-section"> <h5 class="footer-section-title"> 热门角色</h5> <ul class="footer-section-links"> <li> <a href="https://prompts.explinks.com/category/content_creator" target="_blank">内容创作者</a> </li> <li> <a href="https://prompts.explinks.com/category/developer" target="_blank">开发者</a> </li> <li> <a href="https://prompts.explinks.com/category/product_manager" target="_blank">产品经理</a> </li> <li> <a href="https://prompts.explinks.com/category/business_consultant" target="_blank">商业顾问</a> </li> <li> <a href="https://prompts.explinks.com/category/marketing_personnel" target="_blank">市场营销</a> </li> <li> <a href="https://prompts.explinks.com/category/business_owner" target="_blank">企业管理者</a> </li> <li> <a href="https://prompts.explinks.com/category/seo_expert" target="_blank">SEO专家</a> </li> <li> <a href="https://prompts.explinks.com/category/data_analyst" target="_blank">数据分析师</a> </li> </ul> </div> <div class="footer-section"> <h5 class="footer-section-title"> 热门业务</h5> <ul class="footer-section-links"> <li> <a href="https://prompts.explinks.com/category/developer?biz_cat_slug=code" target="_blank">代码</a> </li> <li> <a href="https://prompts.explinks.com/category/content_creator?biz_cat_slug=content_creation" target="_blank">内容创作</a> </li> <li> <a href="https://prompts.explinks.com/category/human_resources_personnel?biz_cat_slug=human_resources" target="_blank">人力资源</a> </li> <li> <a href="https://prompts.explinks.com/category/data_analyst?biz_cat_slug=data_analysis" target="_blank">数据分析</a> </li> <li> <a href="https://prompts.explinks.com/category/writer?biz_cat_slug=creative_writing" target="_blank">创意写作</a> </li> <li> <a href="https://prompts.explinks.com/category/illustrator?biz_cat_slug=art" target="_blank">艺术插画</a> </li> </ul> </div> <div class="footer-section"> <h5 class="footer-section-title"> 大模型API</h5> <ul class="footer-section-links"> <li> <a href="https://www.explinks.com/api/ai_deepseek_brand" target="_blank">DeepSeek</a> </li> <li> <a href="https://www.explinks.com/api/ai_openai_brand" target="_blank">OpenAI</a> </li> <li> <a href="https://www.explinks.com/api/ai_anthropic_brand" target="_blank">Claude</a> </li> <li> <a href="https://www.explinks.com/api/ai_gemini_brand" target="_blank">Gemini</a> </li> <li> <a href="https://www.explinks.com/api/ai_grok_brand" target="_blank">Grok</a> </li> <li> <a href="https://www.explinks.com/api/ai_tongyi_brand" target="_blank">Qwen</a> </li> </ul> </div> <div class="footer-section"> <h5 class="footer-section-title"> 使用我们的提示词工具</h5> <ul class="footer-section-links"> <li> <a target="_blank">提示词API化工具(敬请期待)</a> </li> <li> <a href="https://tools.explinks.com/prompt-generator" target="_blank">提示词应用工具</a> </li> <li> <a href="https://console.explinks.com/myHome/prompts" target="_blank">我的提示词库</a> </li> <li> <a href="https://prompts.explinks.com/packs/partners" target="_blank">加入分销计划,零成本获得收益</a> </li> </ul> </div> </div> <div class="footer-bottom"> <div class="footer-brand"> <div class="footer-brand-logo"> <a href="https://www.explinks.com/" target="_blank"> <figure class="footer-logo-wrapper"> <img decoding="async" src="https://cdn.explinks.com/wp-content/uploads/2023/12/image-e1703756327221.png" alt="幂简集成ICON" class="footer-logo-img"/> </figure> </a> </div> </div> <div class="footer-bottom-left"> <div class="footer-copyright"> <p>Copyright © 2024 All Rights Reserved <a href="https://www.explinks.com/company/about" target="_blank">北京蜜堂有信科技有限公司</a></p> </div> <div class="footer-address"> <p>公司地址: 北京市朝阳区光华路和乔大厦C座1508</p> </div> </div> <div class="footer-bottom-right"> <div class="footer-license-info"> <div class="footer-license-item"> <p>增值电信业务经营许可证:京B2-20191889</p> </div> <div class="footer-license-icon"> <img decoding="async" src="https://cdn.explinks.com/wp-content/uploads/2023/12/police.png" alt="icon" class="footer-police-icon"/> </div> <div class="footer-license-item"> <p><a href="https://beian.miit.gov.cn/" target="_blank" rel="nofollow">京ICP备18034931号-7</a></p> </div> </div> <div class="footer-feedback"> <p>意见反馈:010-53324933,mtyy@miitang.com</p> </div> </div> </div> </div> </div> <div class="sidebar-components-container"> <div class="sidebar"> <button class="sidebar-button" title="反馈问题" id="feedbackButton"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <circle cx="12" cy="12" r="10"/> <path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/> <line x1="12" y1="17" x2="12" y2="17"/> </svg> </button> <button class="sidebar-button up-button" title="返回顶部"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <path d="M12 19V5"/> <path d="M5 12l7-7 7 7"/> </svg> </button> </div> <div class="toast" id="comingSoonToast"> <div class="toast-content"> <span>敬请期待...</span> <button class="toast-close">×</button> </div> </div> <div class="modal" id="feedbackModal"> <div class="modal-content"> <div class="modal-header"> <h3 class="modal-title">反馈问题</h3> <button class="modal-close">×</button> </div> <form id="feedbackForm"> <div class="form-group"> <label class="form-label" for="description">描述 <span class="required">*</span></label> <textarea class="form-control" id="description" maxlength="200" required placeholder="请在此描述您要反馈的问题(200字以内)"></textarea> </div> <div class="form-group"> <label class="form-label">截图</label> <div class="image-upload" id="imageUpload"> <input type="file" id="imageInput" multiple accept="image/*" style="display: none;"> <p>点击或拖拽图片到此处上传(最多5张)</p> </div> <div class="image-preview" id="imagePreview"></div> </div> <div class="form-group"> <label class="form-label" for="contact">联系方式</label> <input type="text" class="form-control" id="contact" placeholder="QQ/邮箱/任选其一"> </div> <div class="modal-footer"> <button type="button" class="btn btn-secondary" id="cancelButton">取消</button> <button type="submit" class="btn btn-primary">确定</button> </div> </form> </div> </div> <script> document.addEventListener('DOMContentLoaded', () => { const upButton = document.querySelector('.up-button'); const toast = document.getElementById('comingSoonToast'); const toastClose = document.querySelector('.toast-close'); const feedbackButton = document.getElementById('feedbackButton'); const feedbackModal = document.getElementById('feedbackModal'); const modalClose = feedbackModal.querySelector('.modal-close'); const cancelButton = document.getElementById('cancelButton'); const feedbackForm = document.getElementById('feedbackForm'); const imageUpload = document.getElementById('imageUpload'); const imageInput = document.getElementById('imageInput'); const imagePreview = document.getElementById('imagePreview'); let toastTimeout; // 返回顶部按钮逻辑 window.addEventListener('scroll', () => { if (window.scrollY > 300) { upButton.classList.add('show'); } else { upButton.classList.remove('show'); } }); upButton.addEventListener('click', () => { window.scrollTo({ top: 0, behavior: 'smooth' }); }); // 显示提示框函数 function showToast() { clearTimeout(toastTimeout); toast.classList.add('show'); toastTimeout = setTimeout(() => { toast.classList.remove('show'); }, 3000); } // 关闭提示框 toastClose.addEventListener('click', () => { toast.classList.remove('show'); clearTimeout(toastTimeout); }); // // 为三个按钮添加点击事件 // const comingSoonButtons = [ // document.querySelector('button[title="AI助理"]'), // document.querySelector('button[title="最近浏览"]'), // document.querySelector('button[title="对比列表"]') // ]; // // comingSoonButtons.forEach(button => { // button.addEventListener('click', showToast); // }); // 反馈模态框相关逻辑 function openModal() { feedbackModal.classList.add('show'); document.body.style.overflow = 'hidden'; } function closeModal() { feedbackModal.classList.remove('show'); document.body.style.overflow = ''; feedbackForm.reset(); imagePreview.innerHTML = ''; } feedbackButton.addEventListener('click', openModal); modalClose.addEventListener('click', closeModal); cancelButton.addEventListener('click', closeModal); // 点击模态框外部关闭 feedbackModal.addEventListener('click', (e) => { if (e.target === feedbackModal) { closeModal(); } }); // 图片上传相关逻辑 imageUpload.addEventListener('click', () => { imageInput.click(); }); imageInput.addEventListener('change', handleImageUpload); function handleImageUpload() { const files = Array.from(imageInput.files); const existingImages = imagePreview.querySelectorAll('.image-container'); const totalImages = existingImages.length + files.length; if (totalImages > 5) { alert('最多只能上传5张图片'); return; } files.forEach(file => { if (!file.type.startsWith('image/')) { return; } const reader = new FileReader(); reader.onload = (e) => { const container = document.createElement('div'); container.className = 'image-container'; const img = document.createElement('img'); img.src = e.target.result; container.appendChild(img); const deleteBtn = document.createElement('button'); deleteBtn.className = 'image-delete'; deleteBtn.innerHTML = '×'; deleteBtn.title = '删除图片'; deleteBtn.onclick = function () { if (confirm('确定要删除这张图片吗?')) { container.remove(); } }; container.appendChild(deleteBtn); imagePreview.appendChild(container); }; reader.readAsDataURL(file); }); } // 拖拽上传 imageUpload.addEventListener('dragover', (e) => { e.preventDefault(); imageUpload.style.borderColor = '#4a90e2'; }); imageUpload.addEventListener('dragleave', () => { imageUpload.style.borderColor = '#ddd'; }); imageUpload.addEventListener('drop', (e) => { e.preventDefault(); imageUpload.style.borderColor = '#ddd'; const files = Array.from(e.dataTransfer.files); if (files.length > 5) { alert('最多只能上传5张图片'); return; } imageInput.files = e.dataTransfer.files; handleImageUpload(); }); // 表单提交 feedbackForm.addEventListener('submit', async (e) => { e.preventDefault(); const description = document.getElementById('description').value.trim(); const contact = document.getElementById('contact').value.trim(); // if (!description || !contact) { if (!description) { alert('请填写反馈内容!'); return; } // 收集图片数据 const images = []; const imageElements = imagePreview.querySelectorAll('img'); imageElements.forEach(img => { images.push(img.src); }); // 准备要提交的数据 const formData = { description, contact, images }; try { // 从 cookie 中获取用户 Token const tokenMatch = document.cookie.match(/(?:^|; )Admin-Token=([^;]+)/); const token = tokenMatch ? decodeURIComponent(tokenMatch[1]) : ''; const headers = { 'Content-Type': 'application/json' }; if (token) { headers['Authorization'] = 'Bearer ' + token; } const response = await fetch('https://api.explinks.com/feedback/submit', { method: 'POST', headers: headers, body: JSON.stringify(formData) }); if (response.ok) { alert('反馈提交成功!'); closeModal(); } else { throw new Error('提交失败'); } } catch (error) { alert('提交失败,请稍后重试!'); console.error('提交表单时出错:', error); } }); }); </script> </div> <script src="https://static.explinks.com/prompt/static/js/statistics/index.js?v=1775816724940"></script> <script src="https://static.explinks.com/hub/static/script/baidu-analytics.js?v=1775816724940"></script> <script charset="UTF-8" id="LA_COLLECT" src="//sdk.51.la/js-sdk-pro.min.js"></script> <script>LA.init({id: "3NS00J5GfuD7Tvg6", ck: "3NS00J5GfuD7Tvg6"})</script> <script> (function(){ var h = window.location.hostname; var d = ''; try { var p = h.split('.'); if (p.length >= 2) { d = '.' + p.slice(-2).join('.'); } } catch (e) {} var isIp = /^\d{1,3}(?:\.\d{1,3}){3}$/.test(h); var domainAttr = (!isIp && h !== 'localhost' && d) ? ('; domain=' + d) : ''; function getCookie(name){ var cs = document.cookie ? document.cookie.split('; ') : []; for (var i=0;i<cs.length;i++){ var parts = cs[i].split('='); var key = parts.shift(); var value = parts.join('='); if (key === name){ return decodeURIComponent(value || ''); } } return null; } function delCookie(name){ document.cookie = name + '=; path=/; Max-Age=0; SameSite=Lax' + domainAttr; } window.clearPromotionCookie = function(){ delCookie('promotion-code'); delCookie('promotion-code-set'); }; var setTs = getCookie('promotion-code-set'); var setNum = Number(setTs); if (getCookie('promotion-code') && setTs) { var ttl = 7200; document.cookie = 'promotion-code-set=' + encodeURIComponent(setTs) + '; path=/; Max-Age=' + ttl + '; SameSite=Lax' + domainAttr; } if (setTs && String(setNum) === setTs) { fetch('https://prompts-api.explinks.com/api/user/should-clear-promotion-code', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ setTimeMillis: setTs, referer: document.referrer || '' }) }) .then(function(r){ return r ? r.json() : null; }) .then(function(res){ if (res && (res.success === true || res.code === 200) && (res.data === true || res.data === 'true')) { window.clearPromotionCookie(); } }) .catch(function(){}); } var u = new URL(window.location.href); var v = (u.searchParams.get('ref') || u.searchParams.get('REF') || '').split('?')[0].trim(); if (v) { (function(){ var api = 'https://prompts-api.explinks.com/api/user/validate-promotion-code'; var payload = { promotionCode: v, referer: document.referrer || '' }; fetch(api, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }) .then(function(r){ return r ? r.json() : null; }) .then(function(res){ if (res && (res.success === true || res.code === 200) && (res.data === true || res.data === 'true')) { var ttl = 7200; var setTime = Date.now(); document.cookie = 'promotion-code=' + encodeURIComponent(v) + '; path=/; Max-Age=' + ttl + '; SameSite=Lax' + domainAttr; document.cookie = 'promotion-code-set=' + encodeURIComponent(String(setTime)) + '; path=/; Max-Age=' + ttl + '; SameSite=Lax' + domainAttr; } }) .catch(function(){}); })(); } })(); </script> <script src="https://static.explinks.com/prompt/static/js/statistics/view-count.js?v=1775816724940"></script> <script> // 页面加载完成后自动记录浏览量 ViewCountStatistics.autoRecordViewCount('js_async_function_generator'); </script> <script src="https://static.explinks.com/prompt/static/js/prompt-detail-v5/countdown.js?v=1775816724940"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script> <script> // 初始化代码高亮 document.addEventListener('DOMContentLoaded', function () { hljs.highlightAll(); }); </script> <script> window.__promptName = "异步函数生成器"; window.__promptSlug = "js_async_function_generator"; window.__promptApiBaseUrl = "https://prompts-api.explinks.com"; </script> <link rel="stylesheet" href="https://static.explinks.com/prompt/static/css/common/fill-copy-modal/fill-copy-modal.css?v=1775816724940"> <div id="fcmOverlay" class="fcm-overlay"> <div class="fcm-modal"> <div class="fcm-header"> <div class="fcm-title-row"> <div class="fcm-title-left"> <span class="fcm-title-icon">📋</span> <div class="fcm-title-text">提示词复制</div> </div> <button class="fcm-close-btn" onclick="closeFillCopyModal()" type="button">×</button> </div> <div class="fcm-prompt-name" id="fcmPromptName"></div> <div class="fcm-tabs"> <button class="fcm-tab fcm-tab-active" id="fcmTabRaw" onclick="setFillCopyMode('raw')" type="button">复制原文</button> <button class="fcm-tab" id="fcmTabFilled" onclick="setFillCopyMode('filled')" type="button">填空后复制</button> </div> </div> <div class="fcm-body"> <div class="fcm-raw-preview" id="fcmRawPreview"></div> <div class="fcm-fill-section" id="fcmFillSection"> <div class="fcm-fill-hint">在当前页面填写参数后直接复制:</div> <div class="fcm-form-grid" id="fcmFormGrid"></div> <div class="fcm-validation-hint" id="fcmValidationHint" style="display: none;"></div> <div class="fcm-progress" id="fcmProgress" style="display: none;"> <div class="fcm-progress-track"> <div class="fcm-progress-bar" id="fcmProgressBar"></div> </div> <span class="fcm-progress-text" id="fcmProgressText">已填 0/0 个参数</span> </div> </div> </div> <div class="fcm-footer"> <button class="fcm-btn-clear" onclick="clearFillCopyForm()" type="button">清空</button> <button class="fcm-btn-copy" id="fcmCopyBtn" onclick="performFillCopy()" type="button">复制原文</button> </div> </div> </div> <script src="https://static.explinks.com/prompt/static/js/common/fill-copy-modal/fill-copy-modal.js?v=1775816724940"></script> <script src="https://static.explinks.com/prompt/static/js/common/qrcode.min.js"></script> <link rel="stylesheet" href="https://static.explinks.com/prompt/static/css/common/vip-purchase/vip-purchase.css?v=1775816724940"> <div id="vpPayModal" class="vp-pay-modal" role="dialog" aria-modal="true" aria-labelledby="vpPayTitle"> <div class="vp-pay-modal-content"> <span class="vp-close-modal" id="vpClosePayModal" aria-label="关闭">×</span> <div id="vpPayContent"> <h3 id="vpPayTitle" class="vp-pay-title"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="vertical-align: middle; margin-right: 6px;"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg> 开通终身会员 </h3> <a href="/user/vip" target="_blank" class="vp-privilege-guide"> 点击了解永久会员特权及简介 > </a> <div class="vp-price-panel" aria-label="价格"> <div class="vp-price-left"> <span class="vp-label">支付金额</span> <div class="vp-price-detail"> <span class="vp-price-value" id="vpModalPrice">¥0</span> <span class="vp-price-original" id="vpModalOriginalPrice">原价¥0</span> </div> </div> <div class="vp-price-badge" id="vpModalSaved">省0元</div> </div> <div class="vp-guarantee-box"> <div class="vp-guarantee-header"> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#16a34a" stroke-width="2.5"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><path d="M9 12l2 2 4-4"/></svg> <span class="vp-guarantee-title-text">7天无理由退款保障</span> </div> <div class="vp-guarantee-points"> <div class="vp-guarantee-point">✓ 不满意随时退款,无需任何理由</div> <div class="vp-guarantee-point">✓ 一键申请,24小时内原路退回</div> </div> </div> <div class="vp-qrcode-panel"> <div class="vp-qrcode-wrapper"> <div id="vpQrCode" class="vp-qrcode-img"></div> <div class="vp-qrcode-status"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#64748b" stroke-width="2"><path d="M12 2v20M2 12h20"/></svg> </div> </div> <p class="vp-qrcode-tip" id="vpQrTip">请使用支付宝/微信扫码支付</p> <p id="vpQrPayee" class="vp-payee-text">(收款方:幂简提示词宝典)</p> <div class="vp-qrcode-countdown"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#94a3b8" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg> <span>二维码有效期:<span id="vpQrCountdown">5:00</span></span> </div> </div> <div class="vp-security-tips"> <div class="vp-security-item"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#16a34a" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg> <span>SSL加密传输</span> </div> <div class="vp-security-item"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#16a34a" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg> <span>官方支付通道</span> </div> <div class="vp-security-item"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#16a34a" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg> <span>资金安全保障</span> </div> </div> <div class="vp-pay-footer-note"> 支付即表示同意<a href="#" class="vp-link-text">《用户协议》</a>和<a href="#" class="vp-link-text">《隐私政策》</a> </div> <button class="vp-action-btn vp-btn-outline" id="vpCancelPayBtn" style="display:none;">取消</button> <button class="vp-action-btn vp-btn-primary" id="vpConfirmPayBtn" style="display:none;">确认付款</button> </div> <div id="vpSuccessView" class="vp-success-view"> <div class="vp-success-icon">✓</div> <h3 class="vp-success-title">支付成功</h3> <p class="vp-success-desc">恭喜您,已成功升级为终身会员!全站提示词资源已为您解锁。</p> <button class="vp-btn-full vp-btn-primary" id="vpSuccessUseBtn">立即开始使用</button> </div> </div> </div> <div id="vpPayResultModal" class="vp-pay-result-modal" role="dialog" aria-modal="true"> <div class="vp-pay-result-content"> <div class="vp-pay-result-icon"> <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" stroke-width="2"> <circle cx="12" cy="12" r="10"/> <path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/> <line x1="12" y1="17" x2="12.01" y2="17"/> </svg> </div> <h3 class="vp-pay-result-title">请确认支付结果</h3> <p class="vp-pay-result-desc">请确认您是否已完成支付</p> <div class="vp-pay-result-buttons"> <div class="vp-pay-result-btn-wrapper"> <button class="vp-pay-result-btn vp-pay-result-success" id="vpPaySuccessBtn"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="20 6 9 17 4 12"></polyline></svg> <span>我已支付成功</span> </button> <p class="vp-pay-result-btn-note vp-pay-result-highlight">点击后会员资格秒到账</p> <p class="vp-pay-result-btn-note vp-pay-result-warning">未支付请勿点击</p> </div> <div class="vp-pay-result-btn-wrapper"> <button class="vp-pay-result-btn vp-pay-result-cancel" id="vpPayResultCancelBtn"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg> <span>未支付 / 遇到问题</span> </button> <p class="vp-pay-result-btn-note">如有疑问,请联系客服处理</p> </div> </div> </div> </div> <div id="vpRefundModal" class="vp-refund-overlay" aria-modal="true" role="dialog"> <div class="vp-refund-modal"> <div class="vp-modal-header"> <h2>申请退款</h2> </div> <div class="vp-order-line"> <div class="vp-order-row"> <span class="vp-product" id="vpRefundProductName">提示词宝典 · 终身会员</span> <span class="vp-price" id="vpRefundPrice">¥0</span> </div> <div class="vp-order-id" id="vpRefundOrderNo">订单号:-</div> <div class="vp-order-id" id="vpRefundPurchaseTime">购买时间:-</div> </div> <div class="vp-info-simple"> <div class="vp-info-line"> <span class="vp-info-icon">🛡️</span> <div class="vp-info-text"> <div class="vp-first-line"> <span class="vp-info-strong">7天无理由</span> <span>会员试用权益,不满意直接退</span> <span class="vp-tooltip-icon">? <span class="vp-tooltip-text">每位用户仅享一次试用权益</span> </span> </div> </div> </div> <div class="vp-info-line"> <span class="vp-info-icon">⏰</span> <span class="vp-info-text"> <span class="vp-info-strong">确认收货后不可退</span> <span id="vpRefundAutoConfirm">- 自动收货</span> <span class="vp-deadline-tag">之后关闭通道</span> </span> </div> </div> <div class="vp-amount-bar"> <span class="vp-amount-left">退款金额</span> <span class="vp-amount-right" id="vpRefundAmount">¥0.00</span> </div> <div class="vp-action-row"> <button class="vp-btn vp-btn-outline" id="vpRefundCancelBtn">再想想</button> <button class="vp-btn vp-btn-blue" id="vpRefundConfirmBtn">确定</button> </div> </div> </div> <script src="https://static.explinks.com/prompt/static/js/common/vip-purchase/vip-purchase.js?v=1775816724940"></script> </body> </html>