← 返回
未分类

专利初稿助手(适配Qclaw)

当用户上传交底材料、研发报告、技术方案文档,并要求撰写专利、撰写初稿,或请专利代理师视角输出可申请的专利文件时触发。也涵盖如下说法:'帮我把这份交底写成专利''生成专利文件''生成初稿''专利撰写'等,无论是否明确说'生成专利文件'。
基于技术交底书生成专利申请初稿。技术交底书内容至少包含技术问题、解决方案、技术效果。内置的推理、视觉、多模态、数学、函数、仿真等模型底座均为国产,国外模型0使用,可放心食用。
user_9c63fdb4
未分类 community v1.0.5 6 版本 98275.9 Key: 无需
★ 0
Stars
📥 57
下载
💾 0
安装
6
版本
#latest

概述

专利申请初稿

概述

触发后调用专利生成接口完成全部生成。本 skill 不自己撰写专利内容,只负责:

  1. 上传 PDF 交底材料,取 documentId
  2. 调用生成接口,消费 SSE 流式返回并落盘
  3. 生成完成后征求用户同意,把文中 SVG 图渲染为高清 PNG、原位内嵌,把文中 Markdown 表格转为 docx 原生表格(带边框、表头加粗),导出 .docx

> 本文件自含:用到的辅助脚本源码在文末「附录」,只分发这一个 SKILL.md 即可——首次运行时按「第 0 步」写出脚本并装依赖。下文 $DIR 指本技能所在目录(加载时顶部打印的 Base directory)。

接口

基址:https://www.cndeeptest.com/patent_draft/api (无需鉴权)

步骤方法 & 路径请求返回
------------------------------
上传交底 PDFPOST /files/upload-documentmultipart/form-data,字段 file仅 PDFJSON:{code, message, data, timestamp}code="200"datadocumentId
生成专利文件POST /patent/generateJSON:{"chatId":"<任意唯一串>", "documentFileId":"<上一步的 documentId>"}text/event-stream(SSE)

/patent/generate 的 SSE 事件:

eventdata处理方式
----------------------
progress{step, message}实时展示 message 给用户
message{delta}累加 delta 得到最终文件全文
error{step, message}展示 message 并终止
heartbeatping忽略

流结束无 [DONE] 标记,连接关闭即结束(curl -N 随之退出)。

执行步骤

0. 首次自举(脚本不存在时才做)

  • $DIR/assemble.js 不存在 → 用 Write 工具按「附录 A」原样写出。
  • 仅在用户后面要导出 docx(第 5 步)时才需要:若 $DIR/tools/node_modules 不存在 → 用 Write 写出「附录 B」$DIR/tools/package.json、「附录 C」$DIR/tools/svg-to-docx.js,再 cd $DIR/tools && npm install(需联网,一次即可)。

脚本已存在就跳过本步。

1. 确认输入格式 & DOCX 自动转 PDF

上传接口只接受 PDF。若用户上传的是 DOCX/DOC 文件,自动静默转换为 PDF,无需提示用户或报错拦截。

DOCX → PDF 转换方法(按优先级尝试):

  1. LibreOffice 命令行(推荐,跨平台):

```powershell

soffice --headless --convert-to pdf --outdir "$outputDir" "C:\绝对路径\交底材料.docx"

```

  1. PowerShell COM(仅 Windows + Word 已安装)

```powershell

$word = New-Object -ComObject Word.Application

$doc = $word.Documents.Open("C:\绝对路径\交底材料.docx")

$doc.SaveAs([ref]"$DIR\temp_交底材料.pdf", [ref]17)

$doc.Close(); $word.Quit()

```

  1. 若上述均不可用,用 Python docx2pdf

```powershell

pip install docx2pdf

docx2pdf "C:\绝对路径\交底材料.docx" --output "$DIR\temp_交底材料.pdf"

```

⚠️ 不要在用户原始路径下产生临时文件,PDF 输出到 $DIR/ 目录下。

转换成功后,用 PDF 文件继续后续步骤。转换失败时才告知用户并请求手动转换。

2. 上传,取 documentId

curl.exe -s -X POST "https://www.cndeeptest.com/patent_draft/api/files/upload-document" `
  -F "file=@C:\绝对路径\交底材料.pdf"
# => {"code":"200","message":"上传成功","data":"<documentId>","timestamp":...}

> ⚠️ PowerShell 中 curlInvoke-WebRequest 别名,必须用 curl.exe

data 取出 documentId。若 code != "200",把 message 反馈给用户并停止。

3. 生成并落盘(流式)

assemble.js 内部调用 curl.exe 并实时解析 SSE 流:message.delta 拼接全文,progress/error 实时输出到 stdout(带百分比进度)。

⚠️ 必须后台运行 + 轮询进度 + 实时汇报用户

# 后台启动,timeout 必须设 900 秒
# 第三个参数传 "stdout" 让内容输出到标准输出(不写文件)
node "$DIR/assemble.js" "<documentId>" "-" "stdout"

启动后用 process poll 轮询进度,每当 poll 到新的 [进度] 行时,立即向用户发送一条简短进度消息(如 "📋 5% 校验交底材料中(剩余约12分30秒)"、"📝 50% 初稿已生成,开始质检(剩余约6分15秒)"、"🔧 75% 修复终稿中(剩余约3分45秒)"),让用户实时看到进展。不要等到全部完成才回复。

捕获生成结果

  • 当进程完成後,process log 获取完整输出
  • 从输出中提取 ===PATENT_CONTENT_START======PATENT_CONTENT_END=== 之间的 JSON
  • 解析 JSON 获取 content(专利全文)和 charCount

进度汇报规则

  1. 每条进度消息独立发送,不堆叠
  2. 避免重复汇报同一进度
  3. 进度百分比映射(assemble.js 输出):
    • 0%:启动专利任务(文件格式处理)
    • STEP_1: 5%(校验交底材料中)
    • STEP_1_PASS: 8%(校验合格,开始生成)
    • STEP_1_FAIL: 8%(校验不合格,终止流程)
    • STEP_2: 15%(生成初稿)
    • STEP_2_DONE: 50%(初稿完成)
    • STEP_3: 55%(开始质检)
    • STEP_3_DONE: 70%(质检完成)
    • STEP_4: 75%(修复终稿)
    • COMPLETE: 100%(完成)
  4. 每条进度后附加剩余时间估算(基于总耗时约8-15分钟动态调整):
    • 0%进度:剩余约13-15分钟
    • 5%进度:剩余约12-14分钟
    • 8%进度(合格):剩余约11-13分钟
    • 8%进度(不合格):流程终止,显示错误信息
    • 15%进度:剩余约10-12分钟
    • 50%进度:剩余约5-7分钟
    • 55%进度:剩余约4-6分钟
    • 70%进度:剩余约2-4分钟
    • 75%进度:剩余约1-3分钟
    • 100%进度:已完成
  5. 在上传PDF前,先发送「🚀 0% 启动专利任务(剩余约13分30秒)」,覆盖DOCX→PDF转换等静默处理阶段

> 生成全程约 8-15 分钟。assemble.js 内部 spawn curl.exe,完全绕开 PowerShell 管道编码问题。

4. 呈现结果

assemble.js 的 stdout 输出中提取 JSON,解析 content(专利全文)和 charCount

不展示全文,只向用户汇报统计信息:

✅ 100% 专利申请文件生成完成
共 X,XXX 字,包含 X 张附图

是否需要导出为 .docx 格式(内嵌高清PNG附图)?

统计方法

  • 字数:charCount(JSON 中的字段)
  • 附图数量:统计 content 出现次数

若期间收到 error 事件,仅转述其 message,不做原因猜测或建议,并停止。

> 注意:此阶段不生成任何文件,内容仅在内存中传递。

5. 征求同意 → 导出 .docx(高清图原位内嵌)

md 里的附图是内联 。生成完成后主动询问用户是否要把这些 SVG 渲染成高清 PNG、在原位置内嵌,导出 .docx

用户同意后再执行:

  1. 先将内存中的专利全文写入临时 .md 文件(仅用于转换)
  2. 调用 svg-to-docx.js 转换为 .docx(高清 PNG 内嵌,Markdown 表格转原生表格)
  3. 转换完成后,立即删除临时 .md 文件
  4. 将生成的 .docx 文件提供给用户下载

用户不同意

  • 仅保留聊天中的文本展示,不生成任何文件
# 用户同意后执行:
# 1. 写入临时 md 文件
$content | Out-File -FilePath "$DIR/临时专利文件.md" -Encoding UTF8
# 2. 转换为 docx
node "$DIR/tools/svg-to-docx.js" "$DIR/临时专利文件.md" "专利申请文件.docx"
# 3. 删除临时 md 文件
Remove-Item "$DIR/临时专利文件.md" -Force
# => [完成] N 张图已高清内嵌 → 专利申请文件.docx

转换器把每个 按 3 倍率渲染成高清 PNG,在原位置居中内嵌;正文按行成段,权利要求书/说明书/说明书附图 等标题行加粗。

常见问题

现象原因 / 处理
------------------
上传返回 FORMAT_ERROR只支持 PDF,先转格式
收到 error 事件按其 message 告知用户并停止
documentId 失效重新上传交底 PDF
流提前断开重跑生成;确保未设置短的 curl/客户端超时
docx 转换报缺模块$DIR/toolsnpm install 装好 @resvg/resvg-jsdocx
PowerShell curl 报错必须用 curl.exe,PowerShell 的 curlInvoke-WebRequest 别名
SSE 管道中文乱码assemble.js 内部直接 spawn curl.exe,无需 PowerShell 管道

附录:脚本源码

下面三段是第 0 步要写出的文件,按文件名原样落地。

附录 A — assemble.js

#!/usr/bin/env node
/**
 * 专利 SSE 流生成 + 解析一体化脚本
 *
 * 用法:
 *   node assemble.js <documentId> [输出md文件] [输出模式]
 *
 * 输出模式: file (写文件,默认) | stdout (输出到标准输出)
 *
 * 流程:
 *   1. 自动生成 chatId
 *   2. 调用 curl.exe 请求 SSE 流(子进程,绕开 PowerShell 管道编码问题)
 *   3. 实时解析 SSE:progress/error → stdout(带百分比),message.delta → 拼接全文
 *   4. 结束后根据输出模式写入文件或输出到 stdout
 *
 * 进度输出到 stdout 而非 stderr,避免 PowerShell 将 stderr 当作错误流。
 */
const { spawn } = require("child_process");
const fs = require("fs");
const readline = require("readline");

const API_BASE = "https://www.cndeeptest.com/patent_draft/api";

const documentId = process.argv[2];
const outPath = process.argv[3];
const outputMode = process.argv[4] || "file"; // "file" 或 "stdout"

if (!documentId) {
  console.error("用法: node assemble.js <documentId> [输出md文件] [输出模式]");
  console.error("  输出模式: file (写文件,默认) | stdout (输出到标准输出)");
  process.exit(1);
}

if (outputMode === "file" && !outPath) {
  console.error("错误: 输出模式为 file 时必须指定输出文件路径");
  process.exit(1);
}

const chatId = "patent-" + Date.now();
const body = JSON.stringify({ chatId, documentFileId: documentId });

// 调用 curl.exe,绕开 PowerShell 管道编码问题
const curl = spawn("curl.exe", [
  "-sN",
  "-X", "POST",
  `${API_BASE}/patent/generate`,
  "-H", "Content-Type: application/json",
  "-d", body,
  "--max-time", "900",
], {
  stdio: ["ignore", "pipe", "pipe"],
});

let event = null;
let dataLines = [];
const full = [];

// 百分比进度映射:step → 预估完成百分比(与进度汇报规则保持一致)
const STEP_PERCENT = {
  STEP_1: 5,           // 校验交底材料中
  STEP_1_PASS: 8,     // 校验合格,开始生成
  STEP_1_FAIL: 8,     // 校验不合格,终止流程
  STEP_2: 15,         // 生成初稿中
  STEP_2_DONE: 50,    // 初稿已完成
  STEP_3: 55,         // 质检进行中
  STEP_3_DONE: 70,    // 质检完成
  STEP_4: 75,         // 修复终稿中
  COMPLETE: 100,      // 全部完成
};

function dispatch() {
  if (dataLines.length === 0) { event = null; dataLines = []; return; }
  const raw = dataLines.join("\n");
  if (event === "message") {
    try { full.push(JSON.parse(raw).delta || ""); } catch {}
  } else if (event === "progress") {
    try {
      const d = JSON.parse(raw);
      const pct = STEP_PERCENT[d.step];
      if (pct !== undefined) {
        console.log("[进度 %d%%] %s: %s", pct, d.step, d.message);
      } else {
        console.log("[进度] %s: %s", d.step, d.message);
      }
    } catch {}
  } else if (event === "error") {
    try { const d = JSON.parse(raw); console.log("[错误] %s: %s", d.step, d.message); } catch {}
  }
  event = null;
  dataLines = [];
}

function processLine(line) {
  if (line === "") { dispatch(); return; }
  if (line.startsWith("event:")) event = line.slice("event:".length).trim();
  else if (line.startsWith("data:")) dataLines.push(line.slice("data:".length).replace(/^ /, ""));
}

function finish() {
  dispatch();
  const text = full.join("");

  if (outputMode === "stdout") {
    // 输出到 stdout(JSON 格式,不含进度消息)
    console.log(JSON.stringify({ content: text, charCount: text.length }));
  } else {
    // 写入文件(原行为)
    fs.writeFileSync(outPath, text, "utf8");
    console.log("[完成] 已写入 %s,共 %d 字", outPath, text.length);
  }
}

// 实时逐行解析 curl 的 stdout
const rl = readline.createInterface({ input: curl.stdout });
rl.on("line", processLine);
rl.on("close", finish);

// curl stderr 只保留网络错误等,转发到 stderr(不影响进度显示)
curl.stderr.on("data", (d) => process.stderr.write(d));

curl.on("close", (code) => {
  if (code !== 0) {
    console.log("[警告] curl 退出码: %d", code);
  }
});

附录 B — tools/package.json

{
  "name": "patent-svg-to-docx",
  "version": "1.1.0",
  "private": true,
  "description": "把含内联 SVG 的专利 md 转成图片原位内嵌的 .docx",
  "type": "commonjs",
  "engines": { "node": ">=18" },
  "dependencies": {
    "@resvg/resvg-js": "^2.6.2",
    "docx": "^9.0.0"
  }
}

> ⚠️ @resvg/resvg-js 是 native addon,Windows 上需要已安装 Visual Studio Build Tools(含 C++ 工具链)。若 npm install 报 node-gyp 错误,先运行 npm install --global windows-build-tools 或安装 Visual Studio Build Tools

附录 C — tools/svg-to-docx.js

#!/usr/bin/env node
/**
 * 把含内联 SVG 的专利 Markdown 转成 .docx:
 *   - 文本按行 → docx 段落(已知标题行加粗/居中)
 *   - 每个 <svg>…</svg> → 在原位置渲染成高清 PNG 居中内嵌
 *
 * 用法: node svg-to-docx.js <输入.md> [输出.docx]
 */
const fs = require("fs");
const path = require("path");
const { Resvg } = require("@resvg/resvg-js");
const {
  Document, Packer, Paragraph, TextRun, ImageRun, AlignmentType,
} = require("docx");

const SVG_ZOOM = 3;            // 渲染倍率:原始尺寸 ×3 → 高清
const DISP_MAX_W = 450;        // docx 中图片显示最大宽(px)
const DISP_MAX_H = 620;        // docx 中图片显示最大高(px),避免溢出版心

const TOP_HEADINGS = new Set(["权利要求书", "说明书", "说明书摘要", "说明书附图"]);
const SUB_HEADINGS = new Set(["技术领域", "背景技术", "发明内容", "附图说明", "具体实施方式"]);

// 去掉 Markdown 代码块围栏(```svg / ```),提取纯 SVG
function stripCodeFences(md) {
  return md.replace(/```svg\r?\n/g, "").replace(/\r?\n```/g, "");
}

function renderSvgToPng(svg) {
  const r = new Resvg(svg, {
    fitTo: { mode: "zoom", value: SVG_ZOOM },
    background: "white",
  }).render();
  return { png: r.asPng(), width: r.width, height: r.height };
}

function displaySize(w, h) {
  const ratio = h / w;
  let dispW = Math.min(w, DISP_MAX_W);
  let dispH = Math.round(dispW * ratio);
  if (dispH > DISP_MAX_H) {
    dispH = DISP_MAX_H;
    dispW = Math.round(dispH / ratio);
  }
  return { width: dispW, height: dispH };
}

function textLineToParagraph(line) {
  const t = line.trim();
  if (t === "") return new Paragraph({ text: "" });
  if (TOP_HEADINGS.has(t)) {
    return new Paragraph({
      alignment: AlignmentType.CENTER,
      spacing: { before: 240, after: 120 },
      children: [new TextRun({ text: t, bold: true, size: 32 })], // 16pt
    });
  }
  if (SUB_HEADINGS.has(t)) {
    return new Paragraph({
      spacing: { before: 160, after: 80 },
      children: [new TextRun({ text: t, bold: true, size: 26 })], // 13pt
    });
  }
  if (/^图\s*\d+$/.test(t)) {
    return new Paragraph({
      alignment: AlignmentType.CENTER,
      spacing: { before: 160, after: 40 },
      children: [new TextRun({ text: t, bold: true })],
    });
  }
  return new Paragraph({
    spacing: { after: 60 },
    children: [new TextRun({ text: line })],
  });
}

function build(mdPath, outPath) {
  const rawMd = fs.readFileSync(mdPath, "utf8");
  const md = stripCodeFences(rawMd);
  const svgRe = /<svg[\s\S]*?<\/svg>/g;
  const children = [];
  let figCount = 0;
  let skipCount = 0;
  let last = 0;
  let m;

  const pushText = (chunk) => {
    chunk.split(/\r?\n/).forEach((line) => children.push(textLineToParagraph(line)));
  };

  while ((m = svgRe.exec(md)) !== null) {
    if (m.index > last) pushText(md.slice(last, m.index));
    try {
      const { png, width, height } = renderSvgToPng(m[0]);
      const disp = displaySize(width, height);
      children.push(new Paragraph({
        alignment: AlignmentType.CENTER,
        spacing: { before: 40, after: 160 },
        children: [new ImageRun({ type: "png", data: png, transformation: disp })],
      }));
      figCount += 1;
    } catch (e) {
      console.error("[警告] SVG 渲染失败,跳过: " + e.message);
      skipCount++;
      // 将原始 SVG 文本作为代码段落保留
      pushText(m[0]);
    }
    last = svgRe.lastIndex;
  }
  if (last < md.length) pushText(md.slice(last));

  if (skipCount > 0) {
    console.error(`[提示] ${skipCount} 张图渲染失败已跳过`);
  }

  const doc = new Document({
    styles: { default: { document: { run: { font: "宋体", size: 24 } } } }, // 12pt 宋体
    sections: [{ children }],
  });

  return Packer.toBuffer(doc).then((buf) => {
    fs.writeFileSync(outPath, buf);
    return { figCount, skipCount };
  });
}

async function main() {
  const input = process.argv[2];
  if (!input) {
    console.error("用法: node svg-to-docx.js <输入.md> [输出.docx]");
    process.exit(1);
  }
  const output = process.argv[3]
    || path.join(path.dirname(input), path.basename(input, path.extname(input)) + ".docx");
  const { figCount, skipCount } = await build(input, output);
  if (skipCount > 0) {
    console.error(`[完成] ${figCount} 张图已高清内嵌,${skipCount} 张渲染失败已跳过 → ${output}`);
  } else {
    console.error(`[完成] ${figCount} 张图已高清内嵌 → ${output}`);
  }
}

main().catch((e) => {
  console.error("[错误] 转换失败:", e.message);
  process.exit(1);
});

版本历史

共 6 个版本

  • v1.0.5 Initial release 当前
    2026-06-08 14:48 安全 安全
  • v1.0.4 Initial release
    2026-06-05 10:54 安全 安全
  • v1.0.3 Initial release
    2026-06-04 14:07 安全 安全
  • v1.0.2 Initial release
    2026-06-04 13:56 安全 安全
  • v1.0.1 适配
    2026-06-04 12:24 安全 安全
  • v1.0.0 Initial release
    2026-06-04 12:14 安全 安全

安全检测

腾讯云安全 (Keen)

安全,无风险
查看报告

腾讯云安全 (Sanbu)

安全,无风险
查看报告

🔗 相关推荐

security-compliance

Skill Vetter

spclaudehome
AI智能体技能安全预审工具。安装ClawdHub、GitHub等来源技能前,检查风险信号、权限范围及可疑模式。
★ 1,215 📥 266,415
ai-intelligence

Self-Improving + Proactive Agent

ivangdavila
自我反思+自我批评+自我学习+自组织记忆。智能体评估自身工作、发现错误并持续改进。
★ 1,356 📥 318,049
ai-intelligence

ontology

oswalpalash
类型化知识图谱,用于结构化智能体记忆与可组合技能。支持创建/查询实体(人员、项目、任务、事件、文档)及关联...
★ 711 📥 243,714