跳到主要内容

去除ai味

收藏1

仅限QQagen用,其它使用情况不知道,qqa内可自行填加屏蔽的关键词

让 AI 安装

面包最新版本 0.1.0更新于 2026-11-01下载 0 次

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
15let api = null;
16
17export function setup(a) {
18 api = a;
19}
20
21/** 内置 AI 腔(都是整词删除;感觉太激进就关 builtinAIisms 用自己的)。 */
22const 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 */
50export 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/** 解析禁说词(逗号、顿号、换行都认)。 */
81export function parseWords(raw) {
82 return String(raw ?? '')
83 .split(/[,,、\s]+/)
84 .map((w) => w.trim())
85 .filter(Boolean);
86}
87
88/** 对一段文本做清洗:替换规则(先长后短)→ 禁说词删除 → 内置AI腔删除 → 收拾空白。 */
89export 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/** 拼一段「别用这些词」的提示词尾巴。 */
124export 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 惯例)。 */
140function 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
152export 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};

文件内容是平台检查时读出的文本,只供查看。平台不会运行包里的任何脚本或程序。