Node.js 接入降重降AI接口

Node.js 调用降重降AI API 完整示例:原生 fetch 零依赖基础调用、逐段落异步处理、jszip 处理 OOXML 脚注占位符、p-limit 控制并发。

Node.js 接入降重降AI接口

Node.js 18+ 内置全局 fetch,调用降重降AI接口零依赖。接口一次改写同时完成降重和降AI,单次请求处理一个正文自然段(不超过1000字),详细参数说明见接口文档

基础调用

以下代码包含超时控制与错误重试,可直接复制运行(环境变量 LLMAPI_KEY):

const API_URL = 'https://api.llmapi.fit/completion/v2/reduce';
const API_KEY = process.env.LLMAPI_KEY || 'YOUR_API_KEY';

async function reduceParagraph(text, maxRetries = 3) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    const resp = await fetch(API_URL, {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${API_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ text }),
      signal: AbortSignal.timeout(180_000), // 高峰期智能排队最长约2分钟
    });

    if (resp.status === 200) {
      const data = await resp.json();
      if (data.code === 'success') return data.output_text;
      throw new Error(`业务错误: ${data.code} ${data.message}`);
    }

    if (resp.status === 429 || resp.status === 503) {
      // 429 请求过于频繁 / 503 GPU服务繁忙:等待后重试
      await new Promise(r => setTimeout(r, 4000));
      continue;
    }

    // 400 参数或余额问题、401 密钥问题:不可重试
    throw new Error(`HTTP ${resp.status}: ${await resp.text()}`);
  }
  throw new Error('重试次数已用尽,请稍后再试');
}

// 用法
const rewritten = await reduceParagraph('待改写的正文自然段……');

逐段落处理全文

接口按段落级别改写,用 for...of 顺序处理论文的正文自然段(并发方案见下文):

const paragraphs = [
  '第一个正文自然段……',
  '第二个正文自然段……',
];

const rewritten = [];
for (const p of paragraphs) {
  rewritten.push(await reduceParagraph(p));
  await new Promise(r => setTimeout(r, 500)); // 速率控制在 10 次/秒以内
}

并发控制(p-limit)

批量任务想并发时,用 p-limit 限制同时进行的请求数,配合间隔控制总量不超过 10 次/秒:

npm install p-limit
import pLimit from 'p-limit';

const limit = pLimit(5); // 最多 5 个并发
const rewritten = await Promise.all(
  paragraphs.map(p => limit(() => reduceParagraph(p)))
);

处理 Word 文档脚注(jszip)

论文段落常带脚注引用,直接发送会丢失。正确做法是把脚注引用替换为 [[FNn]] 占位符,改写后原样保留,写回时恢复(完整规范见接口文档第8节)。

Node 生态多数 docx 编辑库对脚注支持有限,直接用 jszip 操作 OOXML 包最可靠:

npm install jszip
import JSZip from 'jszip';

async function extractParagraphsWithPlaceholders(buffer) {
  const zip = await JSZip.loadAsync(buffer);

  // word/document.xml:正文的脚注引用替换为占位符
  const docXml = await zip.file('word/document.xml').async('string');
  let fnIndex = 0;
  const processed = docXml.replace(
    /<w:footnoteReference[^>]*\/>/g,
    () => `[[FN${fnIndex++}]]`
  );

  // 注意:此正则示意单段处理;实际按 <w:p> 切分后,
  // 每段重置 fnIndex 从 [[FN0]] 开始编号

  // word/footnotes.xml:条目存储文件,全程不做任何修改
  return processed;
}

<w:p> 切分段落、每段独立编号(从 [[FN0]] 开始),把含占位符的段落文本发给接口;返回结果按占位符切分写回,[[FNn]] 处插入原 <w:footnoteReference w:id="原id"/>,保存时 Word 按引用出现顺序自动重排编号。

常见问题

现象原因处理
401 unauthorizedAPI Key 错误检查请求头 Authorization: Bearer YOUR_API_KEY
400 textTooLong单段超过1000字按语义拆分成多个自然段分别调用
TimeoutError高峰期排队AbortSignal.timeout 保持 180 秒,重试 2-3 次
Node 16 及以下无 fetch版本过旧升级 Node 18+,或用 node-fetch + abort-controller

下一步