mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
feat: add smart extraction trigger mvp
This commit is contained in:
@@ -45,7 +45,6 @@ ST-BME(ST-Bionic-Memory-Ecology)是一个运行在 SillyTavern 第三方扩
|
|||||||
|
|
||||||
以下方向在现有代码中仅有预留开关、设计痕迹或路线图描述,尚不应视为完整能力:
|
以下方向在现有代码中仅有预留开关、设计痕迹或路线图描述,尚不应视为完整能力:
|
||||||
|
|
||||||
- **惊奇度分割 / 智能触发提取**
|
|
||||||
- **完整端到端测试与稳定性验证**
|
- **完整端到端测试与稳定性验证**
|
||||||
- **图谱可视化面板**
|
- **图谱可视化面板**
|
||||||
- **自定义 Schema 编辑体验增强**
|
- **自定义 Schema 编辑体验增强**
|
||||||
@@ -293,7 +292,7 @@ SillyTavern/public/scripts/extensions/third-party/ST-BME/
|
|||||||
## 路线图
|
## 路线图
|
||||||
|
|
||||||
- [ ] 补充端到端测试与典型场景回归样例
|
- [ ] 补充端到端测试与典型场景回归样例
|
||||||
- [ ] 完善惊奇度分割与智能触发提取
|
- [x] 完善惊奇度分割与智能触发提取(轻量 MVP)
|
||||||
- [ ] 完成反思条目生成与注入策略
|
- [ ] 完成反思条目生成与注入策略
|
||||||
- [ ] 提供图谱可视化界面
|
- [ ] 提供图谱可视化界面
|
||||||
- [ ] 增强 Schema 配置与编辑体验
|
- [ ] 增强 Schema 配置与编辑体验
|
||||||
|
|||||||
124
index.js
124
index.js
@@ -94,6 +94,7 @@ const defaultSettings = {
|
|||||||
// ① 惊奇度分割(P2)
|
// ① 惊奇度分割(P2)
|
||||||
enableSmartTrigger: false, // 启用惊奇度分割
|
enableSmartTrigger: false, // 启用惊奇度分割
|
||||||
triggerPatterns: "", // 自定义触发正则
|
triggerPatterns: "", // 自定义触发正则
|
||||||
|
smartTriggerThreshold: 2, // 轻量触发阈值
|
||||||
|
|
||||||
// ⑤ 主动遗忘(P2)
|
// ⑤ 主动遗忘(P2)
|
||||||
enableSleepCycle: false, // 启用主动遗忘
|
enableSleepCycle: false, // 启用主动遗忘
|
||||||
@@ -168,6 +169,103 @@ function saveGraphToChat() {
|
|||||||
|
|
||||||
// ==================== 核心流程 ====================
|
// ==================== 核心流程 ====================
|
||||||
|
|
||||||
|
const DEFAULT_TRIGGER_KEYWORDS = [
|
||||||
|
"突然",
|
||||||
|
"没想到",
|
||||||
|
"原来",
|
||||||
|
"其实",
|
||||||
|
"发现",
|
||||||
|
"背叛",
|
||||||
|
"死亡",
|
||||||
|
"复活",
|
||||||
|
"恢复记忆",
|
||||||
|
"失忆",
|
||||||
|
"告白",
|
||||||
|
"暴露",
|
||||||
|
"秘密",
|
||||||
|
"计划",
|
||||||
|
"规则",
|
||||||
|
"契约",
|
||||||
|
"位置",
|
||||||
|
"地点",
|
||||||
|
"离开",
|
||||||
|
"来到",
|
||||||
|
];
|
||||||
|
|
||||||
|
function getSmartTriggerDecision(chat, lastProcessed, settings) {
|
||||||
|
const pendingMessages = chat
|
||||||
|
.slice(lastProcessed + 1)
|
||||||
|
.filter((msg) => !msg.is_system)
|
||||||
|
.map((msg) => ({
|
||||||
|
role: msg.is_user ? "user" : "assistant",
|
||||||
|
content: msg.mes || "",
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (pendingMessages.length === 0) {
|
||||||
|
return { triggered: false, score: 0, reasons: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const reasons = [];
|
||||||
|
let score = 0;
|
||||||
|
const combinedText = pendingMessages.map((m) => m.content).join("\n");
|
||||||
|
|
||||||
|
const keywordHits = DEFAULT_TRIGGER_KEYWORDS.filter((keyword) =>
|
||||||
|
combinedText.includes(keyword),
|
||||||
|
);
|
||||||
|
if (keywordHits.length > 0) {
|
||||||
|
score += Math.min(2, keywordHits.length);
|
||||||
|
reasons.push(`关键词: ${keywordHits.slice(0, 3).join(", ")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const customPatterns = String(settings.triggerPatterns || "")
|
||||||
|
.split(/\r?\n|,/)
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
for (const pattern of customPatterns) {
|
||||||
|
try {
|
||||||
|
const regex = new RegExp(pattern, "i");
|
||||||
|
if (regex.test(combinedText)) {
|
||||||
|
score += 2;
|
||||||
|
reasons.push(`自定义触发: ${pattern}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 忽略无效正则,避免影响主流程
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const roleSwitchCount = pendingMessages.reduce((count, message, index) => {
|
||||||
|
if (index === 0) return count;
|
||||||
|
return count + (message.role !== pendingMessages[index - 1].role ? 1 : 0);
|
||||||
|
}, 0);
|
||||||
|
if (roleSwitchCount >= 2) {
|
||||||
|
score += 1;
|
||||||
|
reasons.push("多轮往返互动");
|
||||||
|
}
|
||||||
|
|
||||||
|
const punctuationHits = (combinedText.match(/[!?!?]/g) || []).length;
|
||||||
|
if (punctuationHits >= 2) {
|
||||||
|
score += 1;
|
||||||
|
reasons.push("情绪/冲突波动");
|
||||||
|
}
|
||||||
|
|
||||||
|
const entityLikeHits =
|
||||||
|
combinedText.match(
|
||||||
|
/[A-Z][a-z]{2,}|[\u4e00-\u9fff]{2,6}(先生|小姐|王国|城|镇|村|学院|组织|公司|小队|军团)/g,
|
||||||
|
) || [];
|
||||||
|
if (entityLikeHits.length > 0) {
|
||||||
|
score += 1;
|
||||||
|
reasons.push("疑似新实体/新地点");
|
||||||
|
}
|
||||||
|
|
||||||
|
const threshold = Math.max(1, settings.smartTriggerThreshold || 2);
|
||||||
|
return {
|
||||||
|
triggered: score >= threshold,
|
||||||
|
score,
|
||||||
|
reasons,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 提取管线:处理未提取的对话楼层
|
* 提取管线:处理未提取的对话楼层
|
||||||
*/
|
*/
|
||||||
@@ -194,8 +292,17 @@ async function runExtraction() {
|
|||||||
|
|
||||||
if (unprocessedStarts.length === 0) return;
|
if (unprocessedStarts.length === 0) return;
|
||||||
|
|
||||||
// 按 extractEvery 批次处理
|
const smartTriggerDecision = settings.enableSmartTrigger
|
||||||
if (unprocessedStarts.length < settings.extractEvery) return;
|
? getSmartTriggerDecision(chat, lastProcessed, settings)
|
||||||
|
: { triggered: false, score: 0, reasons: [] };
|
||||||
|
|
||||||
|
// 按 extractEvery 批次处理;若启用智能触发,则允许提前提取
|
||||||
|
if (
|
||||||
|
unprocessedStarts.length < settings.extractEvery &&
|
||||||
|
!smartTriggerDecision.triggered
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
isExtracting = true;
|
isExtracting = true;
|
||||||
|
|
||||||
@@ -219,7 +326,12 @@ async function runExtraction() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[ST-BME] 开始提取: 楼层 ${startIdx}-${endIdx}`);
|
console.log(
|
||||||
|
`[ST-BME] 开始提取: 楼层 ${startIdx}-${endIdx}` +
|
||||||
|
(smartTriggerDecision.triggered
|
||||||
|
? ` [智能触发 score=${smartTriggerDecision.score}; ${smartTriggerDecision.reasons.join(" / ")}]`
|
||||||
|
: ""),
|
||||||
|
);
|
||||||
|
|
||||||
const result = await extractMemories({
|
const result = await extractMemories({
|
||||||
graph: currentGraph,
|
graph: currentGraph,
|
||||||
@@ -697,6 +809,12 @@ function bindSettingsUI() {
|
|||||||
settings.enableSmartTrigger = $(this).prop("checked");
|
settings.enableSmartTrigger = $(this).prop("checked");
|
||||||
saveSettingsDebounced();
|
saveSettingsDebounced();
|
||||||
});
|
});
|
||||||
|
$("#st_bme_trigger_patterns")
|
||||||
|
.val(settings.triggerPatterns || "")
|
||||||
|
.on("input", function () {
|
||||||
|
settings.triggerPatterns = $(this).val();
|
||||||
|
saveSettingsDebounced();
|
||||||
|
});
|
||||||
|
|
||||||
// P2: 主动遗忘
|
// P2: 主动遗忘
|
||||||
$("#st_bme_sleep_cycle")
|
$("#st_bme_sleep_cycle")
|
||||||
|
|||||||
141
settings.html
141
settings.html
@@ -2,10 +2,11 @@
|
|||||||
<div class="inline-drawer">
|
<div class="inline-drawer">
|
||||||
<div class="inline-drawer-toggle inline-drawer-header">
|
<div class="inline-drawer-toggle inline-drawer-header">
|
||||||
<b>ST-BME 图谱记忆</b>
|
<b>ST-BME 图谱记忆</b>
|
||||||
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
|
<div
|
||||||
|
class="inline-drawer-icon fa-solid fa-circle-chevron-down down"
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="inline-drawer-content">
|
<div class="inline-drawer-content">
|
||||||
|
|
||||||
<!-- 基础设置 -->
|
<!-- 基础设置 -->
|
||||||
<div class="st-bme-section">
|
<div class="st-bme-section">
|
||||||
<div class="st-bme-row">
|
<div class="st-bme-row">
|
||||||
@@ -17,7 +18,14 @@
|
|||||||
|
|
||||||
<div class="st-bme-row">
|
<div class="st-bme-row">
|
||||||
<label for="st_bme_extract_every">每 N 条回复提取</label>
|
<label for="st_bme_extract_every">每 N 条回复提取</label>
|
||||||
<input type="number" id="st_bme_extract_every" class="text_pole" min="1" max="10" value="1" />
|
<input
|
||||||
|
type="number"
|
||||||
|
id="st_bme_extract_every"
|
||||||
|
class="text_pole"
|
||||||
|
min="1"
|
||||||
|
max="10"
|
||||||
|
value="1"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -43,7 +51,14 @@
|
|||||||
|
|
||||||
<div class="st-bme-row">
|
<div class="st-bme-row">
|
||||||
<label for="st_bme_inject_depth">注入深度</label>
|
<label for="st_bme_inject_depth">注入深度</label>
|
||||||
<input type="number" id="st_bme_inject_depth" class="text_pole" min="0" max="9999" value="4" />
|
<input
|
||||||
|
type="number"
|
||||||
|
id="st_bme_inject_depth"
|
||||||
|
class="text_pole"
|
||||||
|
min="0"
|
||||||
|
max="9999"
|
||||||
|
value="4"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -55,17 +70,41 @@
|
|||||||
|
|
||||||
<div class="st-bme-row">
|
<div class="st-bme-row">
|
||||||
<label for="st_bme_graph_weight">图扩散权重</label>
|
<label for="st_bme_graph_weight">图扩散权重</label>
|
||||||
<input type="number" id="st_bme_graph_weight" class="text_pole" min="0" max="1" step="0.1" value="0.6" />
|
<input
|
||||||
|
type="number"
|
||||||
|
id="st_bme_graph_weight"
|
||||||
|
class="text_pole"
|
||||||
|
min="0"
|
||||||
|
max="1"
|
||||||
|
step="0.1"
|
||||||
|
value="0.6"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="st-bme-row">
|
<div class="st-bme-row">
|
||||||
<label for="st_bme_vector_weight">向量权重</label>
|
<label for="st_bme_vector_weight">向量权重</label>
|
||||||
<input type="number" id="st_bme_vector_weight" class="text_pole" min="0" max="1" step="0.1" value="0.3" />
|
<input
|
||||||
|
type="number"
|
||||||
|
id="st_bme_vector_weight"
|
||||||
|
class="text_pole"
|
||||||
|
min="0"
|
||||||
|
max="1"
|
||||||
|
step="0.1"
|
||||||
|
value="0.3"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="st-bme-row">
|
<div class="st-bme-row">
|
||||||
<label for="st_bme_importance_weight">重要性权重</label>
|
<label for="st_bme_importance_weight">重要性权重</label>
|
||||||
<input type="number" id="st_bme_importance_weight" class="text_pole" min="0" max="1" step="0.1" value="0.1" />
|
<input
|
||||||
|
type="number"
|
||||||
|
id="st_bme_importance_weight"
|
||||||
|
class="text_pole"
|
||||||
|
min="0"
|
||||||
|
max="1"
|
||||||
|
step="0.1"
|
||||||
|
value="0.1"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -89,7 +128,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="st-bme-row st-bme-indent">
|
<div class="st-bme-row st-bme-indent">
|
||||||
<label for="st_bme_evo_neighbors">近邻数量</label>
|
<label for="st_bme_evo_neighbors">近邻数量</label>
|
||||||
<input type="number" id="st_bme_evo_neighbors" class="text_pole" min="1" max="10" value="5" />
|
<input
|
||||||
|
type="number"
|
||||||
|
id="st_bme_evo_neighbors"
|
||||||
|
class="text_pole"
|
||||||
|
min="1"
|
||||||
|
max="10"
|
||||||
|
value="5"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="st-bme-row">
|
<div class="st-bme-row">
|
||||||
@@ -100,7 +146,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="st-bme-row st-bme-indent">
|
<div class="st-bme-row st-bme-indent">
|
||||||
<label for="st_bme_conflict_threshold">对照阈值</label>
|
<label for="st_bme_conflict_threshold">对照阈值</label>
|
||||||
<input type="number" id="st_bme_conflict_threshold" class="text_pole" min="0.5" max="0.99" step="0.05" value="0.85" />
|
<input
|
||||||
|
type="number"
|
||||||
|
id="st_bme_conflict_threshold"
|
||||||
|
class="text_pole"
|
||||||
|
min="0.5"
|
||||||
|
max="0.99"
|
||||||
|
step="0.05"
|
||||||
|
value="0.85"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="st-bme-row">
|
<div class="st-bme-row">
|
||||||
@@ -111,7 +165,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="st-bme-row st-bme-indent">
|
<div class="st-bme-row st-bme-indent">
|
||||||
<label for="st_bme_synopsis_every">每 N 次提取更新</label>
|
<label for="st_bme_synopsis_every">每 N 次提取更新</label>
|
||||||
<input type="number" id="st_bme_synopsis_every" class="text_pole" min="1" max="20" value="5" />
|
<input
|
||||||
|
type="number"
|
||||||
|
id="st_bme_synopsis_every"
|
||||||
|
class="text_pole"
|
||||||
|
min="1"
|
||||||
|
max="20"
|
||||||
|
value="5"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -144,6 +205,15 @@
|
|||||||
<span>惊奇度分割 <small class="st-bme-hint">EM-LLM</small></span>
|
<span>惊奇度分割 <small class="st-bme-hint">EM-LLM</small></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="st-bme-row st-bme-indent">
|
||||||
|
<label for="st_bme_trigger_patterns">自定义触发模式</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="st_bme_trigger_patterns"
|
||||||
|
class="text_pole"
|
||||||
|
placeholder="突然,真相,秘密,背叛 或 正则"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="st-bme-row">
|
<div class="st-bme-row">
|
||||||
<label class="checkbox_label" for="st_bme_sleep_cycle">
|
<label class="checkbox_label" for="st_bme_sleep_cycle">
|
||||||
@@ -153,7 +223,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="st-bme-row st-bme-indent">
|
<div class="st-bme-row st-bme-indent">
|
||||||
<label for="st_bme_forget_threshold">遗忘阈值</label>
|
<label for="st_bme_forget_threshold">遗忘阈值</label>
|
||||||
<input type="number" id="st_bme_forget_threshold" class="text_pole" min="0.1" max="1.0" step="0.1" value="0.5" />
|
<input
|
||||||
|
type="number"
|
||||||
|
id="st_bme_forget_threshold"
|
||||||
|
class="text_pole"
|
||||||
|
min="0.1"
|
||||||
|
max="1.0"
|
||||||
|
step="0.1"
|
||||||
|
value="0.5"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="st-bme-row">
|
<div class="st-bme-row">
|
||||||
@@ -164,7 +242,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="st-bme-row st-bme-indent">
|
<div class="st-bme-row st-bme-indent">
|
||||||
<label for="st_bme_prob_chance">触发概率</label>
|
<label for="st_bme_prob_chance">触发概率</label>
|
||||||
<input type="number" id="st_bme_prob_chance" class="text_pole" min="0.01" max="0.5" step="0.05" value="0.15" />
|
<input
|
||||||
|
type="number"
|
||||||
|
id="st_bme_prob_chance"
|
||||||
|
class="text_pole"
|
||||||
|
min="0.01"
|
||||||
|
max="0.5"
|
||||||
|
step="0.05"
|
||||||
|
value="0.15"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="st-bme-row">
|
<div class="st-bme-row">
|
||||||
@@ -175,7 +261,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="st-bme-row st-bme-indent">
|
<div class="st-bme-row st-bme-indent">
|
||||||
<label for="st_bme_reflect_every">每 N 次提取反思</label>
|
<label for="st_bme_reflect_every">每 N 次提取反思</label>
|
||||||
<input type="number" id="st_bme_reflect_every" class="text_pole" min="3" max="30" value="10" />
|
<input
|
||||||
|
type="number"
|
||||||
|
id="st_bme_reflect_every"
|
||||||
|
class="text_pole"
|
||||||
|
min="3"
|
||||||
|
max="30"
|
||||||
|
value="10"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -188,17 +281,32 @@
|
|||||||
|
|
||||||
<div class="st-bme-row">
|
<div class="st-bme-row">
|
||||||
<label for="st_bme_embed_url">API 地址</label>
|
<label for="st_bme_embed_url">API 地址</label>
|
||||||
<input type="text" id="st_bme_embed_url" class="text_pole" placeholder="https://api.openai.com/v1" />
|
<input
|
||||||
|
type="text"
|
||||||
|
id="st_bme_embed_url"
|
||||||
|
class="text_pole"
|
||||||
|
placeholder="https://api.openai.com/v1"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="st-bme-row">
|
<div class="st-bme-row">
|
||||||
<label for="st_bme_embed_key">API Key</label>
|
<label for="st_bme_embed_key">API Key</label>
|
||||||
<input type="password" id="st_bme_embed_key" class="text_pole" placeholder="sk-..." />
|
<input
|
||||||
|
type="password"
|
||||||
|
id="st_bme_embed_key"
|
||||||
|
class="text_pole"
|
||||||
|
placeholder="sk-..."
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="st-bme-row">
|
<div class="st-bme-row">
|
||||||
<label for="st_bme_embed_model">模型</label>
|
<label for="st_bme_embed_model">模型</label>
|
||||||
<input type="text" id="st_bme_embed_model" class="text_pole" placeholder="text-embedding-3-small" />
|
<input
|
||||||
|
type="text"
|
||||||
|
id="st_bme_embed_model"
|
||||||
|
class="text_pole"
|
||||||
|
placeholder="text-embedding-3-small"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="st-bme-row">
|
<div class="st-bme-row">
|
||||||
@@ -241,7 +349,6 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
132
tests/smart-trigger.mjs
Normal file
132
tests/smart-trigger.mjs
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
|
||||||
|
function getSmartTriggerDecision(chat, lastProcessed, settings) {
|
||||||
|
const DEFAULT_TRIGGER_KEYWORDS = [
|
||||||
|
"突然",
|
||||||
|
"没想到",
|
||||||
|
"原来",
|
||||||
|
"其实",
|
||||||
|
"发现",
|
||||||
|
"背叛",
|
||||||
|
"死亡",
|
||||||
|
"复活",
|
||||||
|
"恢复记忆",
|
||||||
|
"失忆",
|
||||||
|
"告白",
|
||||||
|
"暴露",
|
||||||
|
"秘密",
|
||||||
|
"计划",
|
||||||
|
"规则",
|
||||||
|
"契约",
|
||||||
|
"位置",
|
||||||
|
"地点",
|
||||||
|
"离开",
|
||||||
|
"来到",
|
||||||
|
];
|
||||||
|
|
||||||
|
const pendingMessages = chat
|
||||||
|
.slice(lastProcessed + 1)
|
||||||
|
.filter((msg) => !msg.is_system)
|
||||||
|
.map((msg) => ({
|
||||||
|
role: msg.is_user ? "user" : "assistant",
|
||||||
|
content: msg.mes || "",
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (pendingMessages.length === 0) {
|
||||||
|
return { triggered: false, score: 0, reasons: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const reasons = [];
|
||||||
|
let score = 0;
|
||||||
|
const combinedText = pendingMessages.map((m) => m.content).join("\n");
|
||||||
|
|
||||||
|
const keywordHits = DEFAULT_TRIGGER_KEYWORDS.filter((keyword) =>
|
||||||
|
combinedText.includes(keyword),
|
||||||
|
);
|
||||||
|
if (keywordHits.length > 0) {
|
||||||
|
score += Math.min(2, keywordHits.length);
|
||||||
|
reasons.push(`关键词: ${keywordHits.slice(0, 3).join(", ")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const customPatterns = String(settings.triggerPatterns || "")
|
||||||
|
.split(/\r?\n|,/)
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
for (const pattern of customPatterns) {
|
||||||
|
try {
|
||||||
|
const regex = new RegExp(pattern, "i");
|
||||||
|
if (regex.test(combinedText)) {
|
||||||
|
score += 2;
|
||||||
|
reasons.push(`自定义触发: ${pattern}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore invalid regex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const roleSwitchCount = pendingMessages.reduce((count, message, index) => {
|
||||||
|
if (index === 0) return count;
|
||||||
|
return count + (message.role !== pendingMessages[index - 1].role ? 1 : 0);
|
||||||
|
}, 0);
|
||||||
|
if (roleSwitchCount >= 2) {
|
||||||
|
score += 1;
|
||||||
|
reasons.push("多轮往返互动");
|
||||||
|
}
|
||||||
|
|
||||||
|
const punctuationHits = (combinedText.match(/[!?!?]/g) || []).length;
|
||||||
|
if (punctuationHits >= 2) {
|
||||||
|
score += 1;
|
||||||
|
reasons.push("情绪/冲突波动");
|
||||||
|
}
|
||||||
|
|
||||||
|
const entityLikeHits =
|
||||||
|
combinedText.match(
|
||||||
|
/[A-Z][a-z]{2,}|[\u4e00-\u9fff]{2,6}(先生|小姐|王国|城|镇|村|学院|组织|公司|小队|军团)/g,
|
||||||
|
) || [];
|
||||||
|
if (entityLikeHits.length > 0) {
|
||||||
|
score += 1;
|
||||||
|
reasons.push("疑似新实体/新地点");
|
||||||
|
}
|
||||||
|
|
||||||
|
const threshold = Math.max(1, settings.smartTriggerThreshold || 2);
|
||||||
|
return {
|
||||||
|
triggered: score >= threshold,
|
||||||
|
score,
|
||||||
|
reasons,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const noTrigger = getSmartTriggerDecision(
|
||||||
|
[
|
||||||
|
{ is_user: true, mes: "今天天气不错。" },
|
||||||
|
{ is_user: false, mes: "是的,我们继续赶路。" },
|
||||||
|
],
|
||||||
|
-1,
|
||||||
|
{ triggerPatterns: "", smartTriggerThreshold: 3 },
|
||||||
|
);
|
||||||
|
assert.equal(noTrigger.triggered, false);
|
||||||
|
|
||||||
|
const keywordTrigger = getSmartTriggerDecision(
|
||||||
|
[
|
||||||
|
{ is_user: true, mes: "我们突然发现城堡地下有秘密。" },
|
||||||
|
{ is_user: false, mes: "原来失踪的人都被关在这里!" },
|
||||||
|
],
|
||||||
|
-1,
|
||||||
|
{ triggerPatterns: "", smartTriggerThreshold: 2 },
|
||||||
|
);
|
||||||
|
assert.equal(keywordTrigger.triggered, true);
|
||||||
|
assert.ok(keywordTrigger.score >= 2);
|
||||||
|
|
||||||
|
const customTrigger = getSmartTriggerDecision(
|
||||||
|
[
|
||||||
|
{ is_user: true, mes: "她轻声说出真相。" },
|
||||||
|
{ is_user: false, mes: "所有人都沉默了。" },
|
||||||
|
],
|
||||||
|
-1,
|
||||||
|
{ triggerPatterns: "真相|背叛", smartTriggerThreshold: 2 },
|
||||||
|
);
|
||||||
|
assert.equal(customTrigger.triggered, true);
|
||||||
|
assert.ok(customTrigger.reasons.some((r) => r.includes("自定义触发")));
|
||||||
|
|
||||||
|
console.log("smart-trigger tests passed");
|
||||||
Reference in New Issue
Block a user