index.js
7.6 KB| 1 | // 去AI味(de-ai v1.0.0) |
| 2 | // |
| 3 | // 干什么:把 bot 回复里的“AI 腔”洗掉。两道工序,都可独立开关: |
| 4 | // ① before-llm-messages —— 在提示词末尾贴一行「别用这些词/腔调」,让模型源头避免; |
| 5 | // ② after-response —— 生成后直接把内置AI腔 + 自定义禁说词整词删除, |
| 6 | // 把「原词=替换词」的规则逐条替换(改的是最终要发出去的 |
| 7 | // response.message.content,orchestrator 在钩子之后才读它)。 |
| 8 | // |
| 9 | // 设计约束: |
| 10 | // · 纯 JS、零 import(不碰 fs/网络/环境变量),准入闸自然放行、不需要信任记录; |
| 11 | // · 只改最终回复文本,聊天历史、成本统计、记忆注入一概不碰; |
| 12 | // · 清洗后若整条被删空,退回原文,绝不发空消息。 |
| 13 | // · 内置词全部“整词删除”(不替换),且可一键关闭,只用你自己的规则。 |
| 14 | |
| 15 | let api = null; |
| 16 | |
| 17 | export function setup(a) { |
| 18 | api = a; |
| 19 | } |
| 20 | |
| 21 | /** 内置 AI 腔(都是整词删除;感觉太激进就关 builtinAIisms 用自己的)。 */ |
| 22 | const BUILTIN_AIISMS = [ |
| 23 | // 身份自报 |
| 24 | '作为一名AI,', '作为一名AI', '作为一个AI,', '作为一个AI', '作为一个AI语言模型,', |
| 25 | '作为AI助手', '作为AI,', '作为AI', '我是一个AI', '我是AI', |
| 26 | // 客服腔 |
| 27 | '很高兴为您服务,', '很高兴为您服务', '很高兴能帮到你', '请问有什么可以帮您?', |
| 28 | '请问有什么可以帮您', '有什么可以帮您的吗', '有什么需要帮助的吗', '有什么能帮你的吗', |
| 29 | // 公文/总结腔 |
| 30 | '综上所述,', '综上所述', '总而言之,', '总而言之', '总的来说,', '总的来说', |
| 31 | '需要注意的是,', '需要注意的是', '值得注意的是,', '值得注意的是', |
| 32 | // 机器人式的万能回应 |
| 33 | '当然可以!', '当然可以', '没问题,我可以', '我理解你的感受', '让我们一起', '让我们来', |
| 34 | '好的,我这就', '好的,马上', |
| 35 | // ── 用户定制的 AI 短句/口头禅(2026-10-31 加入,整词删除)── |
| 36 | '这条我认', '这流程我熟', '说到这', '你发现没', '说来有趣', |
| 37 | '我理解你的痛苦', '我理解你的焦虑', '你不是一个人', |
| 38 | '按照这个方案', '马上去行动', |
| 39 | '总而言之,', '总而言之', '首先,', '首先', '其次,', '其次', '最后,', '最后', '此外,', '此外', |
| 40 | '值得注意的是,', '值得注意的是', '表面看是', '归根结底', '往深了说', |
| 41 | '行这话我收下了', '这话我收下了', '我收下了', '我认了' |
| 42 | ]; |
| 43 | |
| 44 | /** 解析替换规则。 |
| 45 | * 支持两种写法(混用也行): |
| 46 | * · 每行一组:原词=替换词 |
| 47 | * · 同一行逗号隔开多条:原词=替换词,原词2=替换词2 |
| 48 | * `#` 开头是注释。替换值里带逗号也可以(只有当「逗号后面又跟了 原词= 」才视为换下一条)。 |
| 49 | */ |
| 50 | export function parseReplaceRules(raw) { |
| 51 | const out = []; |
| 52 | const push = (from, to) => { |
| 53 | const f = String(from ?? '').trim(); |
| 54 | const t = String(to ?? '').trim(); |
| 55 | if (f) out.push({ from: f, to: t }); |
| 56 | }; |
| 57 | for (const line0 of String(raw ?? '').split(/\r?\n/)) { |
| 58 | let rest = line0.replace(/#.*$/, '').trim(); |
| 59 | if (!rest) continue; |
| 60 | while (rest) { |
| 61 | const eq = rest.indexOf('='); |
| 62 | if (eq <= 0) break; // 没有 = 或 from 为空,剩下的当垃圾 |
| 63 | push(rest.slice(0, eq), ''); |
| 64 | rest = rest.slice(eq + 1); |
| 65 | // to 读到「后面还跟着 原词= 」的那个逗号为止(同一行多条规则用逗号隔开) |
| 66 | let cut = -1; |
| 67 | const m = /[,,]\s*(?=[^,,=]*=)/.exec(rest); |
| 68 | if (m) cut = m.index; |
| 69 | let to = cut >= 0 ? rest.slice(0, cut).replace(/[,,\s]+$/, '') : rest; |
| 70 | to = to.trim(); |
| 71 | // 值只剩分隔符/空白(如「码住=,」)=删掉这个词 |
| 72 | if (/^[,,\s=]+$/.test(to)) to = ''; |
| 73 | out[out.length - 1].to = to; |
| 74 | rest = cut >= 0 ? rest.slice(cut + m[0].length) : ''; |
| 75 | } |
| 76 | } |
| 77 | return out; |
| 78 | } |
| 79 | |
| 80 | /** 解析禁说词(逗号、顿号、换行都认)。 */ |
| 81 | export function parseWords(raw) { |
| 82 | return String(raw ?? '') |
| 83 | .split(/[,,、\s]+/) |
| 84 | .map((w) => w.trim()) |
| 85 | .filter(Boolean); |
| 86 | } |
| 87 | |
| 88 | /** 对一段文本做清洗:替换规则(先长后短)→ 禁说词删除 → 内置AI腔删除 → 收拾空白。 */ |
| 89 | export function rewriteReply(text, s = {}) { |
| 90 | let t = String(text ?? ''); |
| 91 | if (!t.trim()) return t; |
| 92 | |
| 93 | // ① 替换规则:先长后短,避免「A=B」「AB=C」互相踩 |
| 94 | const rules = parseReplaceRules(s.replaceRules).sort((a, b) => b.from.length - a.from.length); |
| 95 | for (const r of rules) { |
| 96 | t = t.split(r.from).join(r.to); |
| 97 | } |
| 98 | |
| 99 | // ② 禁说词整词删除 |
| 100 | for (const w of parseWords(s.noSayWords)) t = t.split(w).join(''); |
| 101 | |
| 102 | // ③ 内置 AI 腔整词删除 |
| 103 | if (s.builtinAIisms !== false) { |
| 104 | for (const w of BUILTIN_AIISMS) t = t.split(w).join(''); |
| 105 | } |
| 106 | |
| 107 | // ④ 收拾空白与残留标点:删词常留下「。哇!,这件事」这类残标点 |
| 108 | t = t.replace(/[ \t]{2,}/g, ' ') |
| 109 | .replace(/\n{3,}/g, '\n\n') |
| 110 | // 句末标点前的逗号/顿号去掉(「!,」→「!」) |
| 111 | .replace(/[,、]+([。!?;])/g, '$1') |
| 112 | // 相邻标点坍缩(「。。」「!!」「。,」之类) |
| 113 | .replace(/([。!?;:,、,.])[。!?;:,、,.]+/g, '$1') |
| 114 | // 开头残留的逗号/句号/空白去掉(「!」开头是合法的,不动) |
| 115 | .replace(/^[。,、:;,.\s]+/, '') |
| 116 | .replace(/[,、]+$/, '') |
| 117 | .trim(); |
| 118 | |
| 119 | // 清洗到空 → 退回原文,不发空消息 |
| 120 | return t || String(text ?? '').trim(); |
| 121 | } |
| 122 | |
| 123 | /** 拼一段「别用这些词」的提示词尾巴。 */ |
| 124 | export function buildDirective(s = {}) { |
| 125 | const parts = []; |
| 126 | if (s.builtinAIisms !== false) { |
| 127 | parts.push('别用这些AI腔/客服腔/公文腔(整句删掉再说):作为一名AI、很高兴为您服务、综上所述、有什么可以帮您的吗、当然可以!这类。'); |
| 128 | } |
| 129 | const banned = parseWords(s.noSayWords); |
| 130 | if (banned.length) parts.push(`这些词你绝对不能说:${banned.join('、')}。`); |
| 131 | const rules = parseReplaceRules(s.replaceRules); |
| 132 | if (rules.length) parts.push(`想用这些词时换成括号里的:${rules.map((r) => `${r.from}→${r.to || '删掉'}`).join('、')}。`); |
| 133 | const style = String(s.styleRules || '').trim(); |
| 134 | if (style) parts.push(style + ' '); |
| 135 | if (!parts.length) return ''; |
| 136 | return '【说话别带AI味】' + parts.join('') + '说人话,自然一点,像平时聊天。'; |
| 137 | } |
| 138 | |
| 139 | /** 把指令贴到提示词最后一条 user 消息尾巴(沿用记忆插件的 appendToLastUser 惯例)。 */ |
| 140 | function appendToLastUser(messages, text) { |
| 141 | if (!Array.isArray(messages) || !text) return false; |
| 142 | for (let i = messages.length - 1; i >= 0; i--) { |
| 143 | const m = messages[i]; |
| 144 | if (m && m.role === 'user' && typeof m.content === 'string') { |
| 145 | m.content = m.content.replace(/\s+$/, '') + '\n\n' + text; |
| 146 | return true; |
| 147 | } |
| 148 | } |
| 149 | return false; |
| 150 | } |
| 151 | |
| 152 | export const hooks = { |
| 153 | async 'before-llm-messages'(ctx = {}) { |
| 154 | const s = (api && typeof api.config === 'function' ? api.config() : null) || {}; |
| 155 | if (s.enabled === false || s.injectPrompt === false) return; |
| 156 | const text = buildDirective(s); |
| 157 | if (!text) return; |
| 158 | appendToLastUser(ctx.messages, text); |
| 159 | }, |
| 160 | |
| 161 | async 'after-response'(ctx = {}) { |
| 162 | const s = (api && typeof api.config === 'function' ? api.config() : null) || {}; |
| 163 | if (s.enabled === false || s.rewriteReply === false) return; |
| 164 | const msg = ctx?.response?.message; |
| 165 | if (!msg || typeof msg.content !== 'string' || !msg.content.trim()) return; |
| 166 | const next = rewriteReply(msg.content, s); |
| 167 | if (next !== msg.content) msg.content = next; |
| 168 | } |
| 169 | }; |
文件内容是平台检查时读出的文本,只供查看。平台不会运行包里的任何脚本或程序。

