seo-mcp-server
Version:
SEO MCP Server (minimal) - ai_content_detect only
228 lines • 8.8 kB
JavaScript
// 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
;