UNPKG

seo-mcp-server

Version:

SEO MCP Server (minimal) - ai_content_detect only

228 lines 8.8 kB
"use strict"; // aiContentDetect.ts // 工具名称: ai_content_detect // 位置: 内容生成之后、SEO质检(validate_seo_content)之前的硬性前置检查 // 作用: 调用 5118 AI内容检测API,对内容进行AI痕迹评估,输出 percent、逐段 linesscore、mintxt 等; // 以阈值规则生成 pass/review/fail 决策、下一步动作(next_actions)与人类可读建议(suggestions)。 // API文档要点: // - 请求: POST http://apis.5118.com/aidetect // - Header: Authorization: <API_KEY> // Content-Type: application/x-www-form-urlencoded; charset=UTF-8 // - Body: content=<string> (100~6000字) // - 返回(官方示例): // { // "errcode": "0", // "errmsg": "", // "data": { // "linesscore": [ { "score": 2, "txt": "..." }, { "score": 3, "txt": "..." } ... ], // "percent": 43, // "mintxt": 0 // } // } // 字段含义与解读: // - errcode: "0" 表示成功;非"0"为错误 // - errmsg: 错误说明 // - data.mintxt: 0=长度合规(>=100字),1=内容太短(硬性不通过) // - data.percent: AI占比(值越高AI痕迹越重) // - data.linesscore[].score: 段落分值; <3 为疑似AI段落 // 判定阈值(可配置): // - maxPercent(默认35): percent<=35 通过 // - reviewPercent(默认55): 35<percent<=55 复核 // - minLineScore(默认3): 任一段<3 触发复写建议(通常进入review) // 决策输出: // - pass: boolean // - decision: "pass" | "review" | "fail" // * pass: mintxt==0 且 percent<=maxPercent 且 所有段score>=minLineScore // * review: (maxPercent<percent<=reviewPercent) 或 存在少量低分段 // * fail: percent>reviewPercent 或 mintxt!=0 或 errcode!=0 // 建议输出(next_actions): // - "revise_intro_reduce_cliches": 引言去套话 // - "add_verifiable_details": 补充可核验事实(时间/地点/数据来源) // - "split_long_sentences": 拆分长句 // - "remove_template_phrases": 去模板化表达 // - "reduce_keyword_bolding": 减少关键词加粗导致统计异常 // // 注意: 接口存在“非严格JSON”返回的情况。本工具内置“宽松解析”: 先尝试JSON.parse;失败则进行预清洗与正则回退提取。 // 诊断输出(diagnostics): // - raw: 原始响应文本 // - parser: "strict" | "relaxed" // - notes: 解析备注 Object.defineProperty(exports, "__esModule", { value: true }); exports.aiContentDetect = aiContentDetect; // 预清洗: 针对缺逗号/引号粘连的简单修复(尽力而为,失败则回退正则提取) function tryRelaxedParse(raw, notes) { try { return JSON.parse(raw); } catch { // 尝试移除前缀/后缀的非JSON字符 let t = raw.trim(); // 去掉可能出现的前缀 "Output:"、"> y" 等 t = t.replace(/^[^{\[]+/, ""); // 简单修正常见缺逗号情形: }{ -> },{ "0""errmsg" -> "0","errmsg" t = t.replace(/}\s*{/g, "},{"); t = t.replace(/"0""/g, '"0","'); t = t.replace(/]\s*"(percent|mintxt)"/g, '],"$1"'); // 再尝试解析 try { const obj = JSON.parse(t); notes.push("relaxed parse applied"); return obj; } catch { notes.push("relaxed parse failed"); return null; } } } // 正则回退提取: 仅抽取核心字段 function fallbackExtract(raw, notes) { const errcode = (raw.match(/"errcode"\s*:\s*"?(?<v>\d+)"?/)?.groups?.v) ?? ""; const errmsg = (raw.match(/"errmsg"\s*:\s*"(?<v>[^"]*)"/)?.groups?.v) ?? ""; const percentStr = (raw.match(/"percent"\s*:\s*(?<v>\d+)/)?.groups?.v) ?? "0"; const mintxtStr = (raw.match(/"mintxt"\s*:\s*(?<v>\d+)/)?.groups?.v) ?? "1"; // 行分数粗提取(注意该接口会把每段当作对象输出) const lineMatches = [...raw.matchAll(/{"score"\s*:\s*(\d+)\s*"txt"\s*:\s*"([^"]*)"}|{"score":\s*(\d+),"txt":"([^"]*)"}/g)]; const linesscore = []; let idx = 1; for (const m of lineMatches) { const s = Number(m[1] || m[3] || 0); const txt = (m[2] || m[4] || "").trim(); if (txt) linesscore.push({ idx: idx++, score: s, txt }); } notes.push("fallback extractor used"); return { errcode: errcode || "0", errmsg, percent: Number(percentStr), mintxt: Number(mintxtStr), linesscore, }; } async function aiContentDetect(input) { const { content, thresholds } = input; const apiKey = globalThis.__SEO_MCP_AI_DETECT_API_KEY__; if (!apiKey) { throw new Error('AI-Detect apiKey is not configured. Please set env SEO_MCP_AI_DETECT_API_KEY and restart seo-mcp.'); } const maxPercent = thresholds?.maxPercent ?? 35; const reviewPercent = thresholds?.reviewPercent ?? 55; const minLineScore = thresholds?.minLineScore ?? 3; // 基本校验 const len = (content || "").length; if (len < 100 || len > 6000) { return { errcode: "length_error", errmsg: `content length ${len} not in [100,6000]`, mintxt: len < 100 ? 1 : 0, percent: 0, linesscore: [], pass: false, decision: "fail", next_actions: ["increase_content_length_min_100"], suggestions: ["补充细节与事实,确保正文长度≥100字"], diagnostics: { parser: "strict", raw: "", notes: [] }, }; } // 发起请求 const qs = new URLSearchParams(); qs.set("content", content); const resp = await fetch("http://apis.5118.com/aidetect", { method: "POST", headers: { Authorization: apiKey, "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", Accept: "application/json", }, body: qs.toString(), }).catch((e) => ({ ok: false, status: 0, text: async () => String(e), })); const raw = await resp.text?.() ?? ""; const notes = []; let parser = "strict"; // 解析响应 let data = null; try { data = JSON.parse(raw); } catch { const obj = tryRelaxedParse(raw, notes); if (obj) { data = obj; parser = "relaxed"; } } let errcode = "0"; let errmsg = ""; let percent = 0; let mintxt = 1; let linesscore = []; if (data && typeof data === "object") { // 兼容 data 包裹 const container = data.data ? data : { data }; errcode = String(data.errcode ?? "0"); errmsg = String(data.errmsg ?? ""); const d = container.data || {}; percent = Number(d.percent ?? 0); mintxt = Number(d.mintxt ?? 1); if (Array.isArray(d.linesscore)) { let idx = 1; linesscore = d.linesscore.map((it) => ({ idx: idx++, score: Number(it?.score ?? 0), txt: String(it?.txt ?? ""), })); } } else { // 回退提取 const fb = fallbackExtract(raw, notes); errcode = fb.errcode; errmsg = fb.errmsg; percent = fb.percent; mintxt = fb.mintxt; linesscore = fb.linesscore; parser = "relaxed"; } // 判定逻辑 let decision = "pass"; const lowScoreSegments = linesscore.filter(x => x.score < minLineScore); const next_actions = []; const suggestions = []; if (mintxt !== 0 || errcode !== "0") { decision = "fail"; next_actions.push(mintxt !== 0 ? "increase_content_length_min_100" : "retry_request_later"); suggestions.push(mintxt !== 0 ? "正文不足100字,请补充事实与细节后再检测" : "接口繁忙或权限受限,请稍后重试"); } else if (percent > reviewPercent) { decision = "fail"; next_actions.push("remove_template_phrases", "add_verifiable_details", "split_long_sentences"); suggestions.push("AI占比较高,请去模板化并加入可核验事实,再次检测"); } else if (percent > maxPercent || lowScoreSegments.length > 0) { decision = "review"; if (percent > maxPercent) next_actions.push("remove_template_phrases", "add_verifiable_details"); if (lowScoreSegments.length > 0) next_actions.push("revise_intro_reduce_cliches", "split_long_sentences"); suggestions.push("存在边界风险段落,请按建议最小改写后复验"); } const pass = decision === "pass"; return { errcode, errmsg, mintxt, percent, linesscore, pass, decision, next_actions, suggestions, diagnostics: { parser, raw, notes }, }; } //# sourceMappingURL=aiContentDetect.js.map