mirror of
https://github.com/Wx-2025/ST-Amily2-Chat-Optimisation.git
synced 2026-06-13 20:55:50 +00:00
release: v2.2.5 [2026-06-10 12:41:11]
### 修复 - **翰林院(RAG)API Key 污染**: - 修复 `saveSettingsFromUI` 无差别遍历翰林院面板内全部 `[data-setting-key]` 输入(包含被 `profile-sync` 接管隐藏的字段),导致掩码占位符 `••••••••` 被当作真值写回 `settings.rerank.apiKey` / `settings.retrieval.apiKey`,URL / model 也被 Profile 值覆盖到 legacy 字段。修复后会跳过祖先带 `data-profile-hidden` 的输入 - `getRerankSettings` / `getEmbedRetrievalSettings` 同时加入防御性还原:识别历史污染留下的 `••••••••` 时归为空字符串,避免取消 Profile 分配后实际请求带占位符 token 被 401 ---
This commit is contained in:
51
core/memory-blocks/builtin-blocks.js
Normal file
51
core/memory-blocks/builtin-blocks.js
Normal file
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* core/memory-blocks/builtin-blocks.js
|
||||
*
|
||||
* 内置块注册。当前只把剧情优化原硬编码的 sulv1-4 迁过来,作为新流水线的首批
|
||||
* 静态块——既验证 substitution 流程正常,又保留原行为字节级一致。
|
||||
*
|
||||
* 旧位置:core/summarizer.js 中 processPlotOptimization 的硬编码 replacements。
|
||||
*/
|
||||
|
||||
import { register } from './registry.js';
|
||||
|
||||
let initialized = false;
|
||||
|
||||
export function registerBuiltinBlocks() {
|
||||
if (initialized) return;
|
||||
initialized = true;
|
||||
|
||||
// 剧情优化(processPlotOptimization)的四个速率占位符
|
||||
register({
|
||||
id: 'plotOpt.sulv1',
|
||||
placeholder: 'sulv1',
|
||||
context: 'plotOptimization',
|
||||
generator: { type: 'static', valueKey: 'plotOpt_rateMain', defaultValue: 1.0 },
|
||||
name: '主线剧情速率',
|
||||
order: 1,
|
||||
});
|
||||
register({
|
||||
id: 'plotOpt.sulv2',
|
||||
placeholder: 'sulv2',
|
||||
context: 'plotOptimization',
|
||||
generator: { type: 'static', valueKey: 'plotOpt_ratePersonal', defaultValue: 1.0 },
|
||||
name: '个人线速率',
|
||||
order: 2,
|
||||
});
|
||||
register({
|
||||
id: 'plotOpt.sulv3',
|
||||
placeholder: 'sulv3',
|
||||
context: 'plotOptimization',
|
||||
generator: { type: 'static', valueKey: 'plotOpt_rateErotic', defaultValue: 1.0 },
|
||||
name: '速率3(留空)',
|
||||
order: 3,
|
||||
});
|
||||
register({
|
||||
id: 'plotOpt.sulv4',
|
||||
placeholder: 'sulv4',
|
||||
context: 'plotOptimization',
|
||||
generator: { type: 'static', valueKey: 'plotOpt_rateCuckold', defaultValue: 1.0 },
|
||||
name: '速率4(留空)',
|
||||
order: 4,
|
||||
});
|
||||
}
|
||||
98
core/memory-blocks/executor.js
Normal file
98
core/memory-blocks/executor.js
Normal file
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* core/memory-blocks/executor.js
|
||||
*
|
||||
* 工作流执行器:拉 context 下的全部块 → Promise.all 并发执行 generator
|
||||
* → 把每个块的结果按 placeholder 替换回模板。
|
||||
*
|
||||
* 核心 API:
|
||||
* applyToTemplate(template, opts) 单模板进,字符串出
|
||||
* applyToTemplates(templates, opts) 多模板进(数组或对象),结构同形出;
|
||||
* 块只执行一次,对每个模板复用结果
|
||||
* generateBlockMap(opts) 不替换,返回 { id → value } 给调用方自己玩
|
||||
*
|
||||
* 中断行为:opts.signal 由调用方控制,传给每个 handler;任一 handler 抛
|
||||
* AbortError 时,executor 也抛 AbortError 向上传递(与现有 callAI 体系一致)。
|
||||
*/
|
||||
|
||||
import { getHandler } from './generator-handlers.js';
|
||||
import { listByContext } from './registry.js';
|
||||
|
||||
function escapeForRegex(s) {
|
||||
return String(s).replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
|
||||
}
|
||||
|
||||
async function runBlock(block, ctx) {
|
||||
const handler = getHandler(block.generator?.type);
|
||||
if (!handler) {
|
||||
console.warn(`[MemoryBlocks] 未注册的 generator 类型 "${block.generator?.type}",块 ${block.id} 已跳过。`);
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const value = await handler(block, ctx);
|
||||
if (value === null || value === undefined) return null;
|
||||
return { block, value: String(value) };
|
||||
} catch (error) {
|
||||
if (error?.name === 'AbortError') throw error;
|
||||
console.error(`[MemoryBlocks] 块 ${block.id} 生成失败:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function substituteOne(template, results) {
|
||||
if (typeof template !== 'string' || !template) return template ?? '';
|
||||
let out = template;
|
||||
for (const r of results) {
|
||||
if (!r) continue;
|
||||
const re = new RegExp(escapeForRegex(r.block.placeholder), 'g');
|
||||
out = out.replace(re, r.value);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 context 下的所有块,返回 [ {block, value} | null, ... ]。
|
||||
* 内部使用,applyToTemplate(s) 复用。
|
||||
*/
|
||||
async function executeBlocks({ context, settings, signal, extras } = {}) {
|
||||
const blocks = listByContext(context);
|
||||
if (blocks.length === 0) return [];
|
||||
const ctx = { settings: settings ?? {}, signal, context, extras };
|
||||
return await Promise.all(blocks.map(b => runBlock(b, ctx)));
|
||||
}
|
||||
|
||||
export async function applyToTemplate(template, opts = {}) {
|
||||
if (typeof template !== 'string' || !template) return template ?? '';
|
||||
const results = await executeBlocks(opts);
|
||||
return substituteOne(template, results);
|
||||
}
|
||||
|
||||
/**
|
||||
* 多模板批处理。templates 可以是:
|
||||
* - 字符串数组 → 返回字符串数组
|
||||
* - 对象 { key: template } → 返回对象 { key: replaced }
|
||||
* - 字符串 → 退化为 applyToTemplate
|
||||
*/
|
||||
export async function applyToTemplates(templates, opts = {}) {
|
||||
const results = await executeBlocks(opts);
|
||||
|
||||
if (typeof templates === 'string') return substituteOne(templates, results);
|
||||
if (Array.isArray(templates)) return templates.map(t => substituteOne(t, results));
|
||||
if (templates && typeof templates === 'object') {
|
||||
const out = {};
|
||||
for (const [k, v] of Object.entries(templates)) out[k] = substituteOne(v, results);
|
||||
return out;
|
||||
}
|
||||
return templates;
|
||||
}
|
||||
|
||||
/**
|
||||
* 不替换,只把块结果汇成 Map<id, value>,调用方拿去自由组合。
|
||||
*/
|
||||
export async function generateBlockMap(opts = {}) {
|
||||
const results = await executeBlocks(opts);
|
||||
const map = new Map();
|
||||
for (const r of results) {
|
||||
if (r) map.set(r.block.id, r.value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
46
core/memory-blocks/generator-handlers.js
Normal file
46
core/memory-blocks/generator-handlers.js
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* core/memory-blocks/generator-handlers.js
|
||||
*
|
||||
* type → handler 函数 的注册表。BlockDefinition.generator.type 在这里查表后执行。
|
||||
*
|
||||
* Handler 签名:async (block, ctx) => string | null
|
||||
* - block: BlockDefinition
|
||||
* - ctx: ExecuteContext { settings, signal, context, extras }
|
||||
* - 返回 string:替换值;返回 null/undefined:视为"无内容,保留占位符"
|
||||
*
|
||||
* 当前内置 'static';'ai_call'/'plugin' 在后续 Phase 注册(保留接口)。
|
||||
*/
|
||||
|
||||
const handlers = new Map();
|
||||
|
||||
export function registerHandler(type, fn) {
|
||||
if (!type || typeof fn !== 'function') {
|
||||
throw new Error('[MemoryBlocks] registerHandler 需要 type 字符串 + 函数 fn。');
|
||||
}
|
||||
handlers.set(type, fn);
|
||||
}
|
||||
|
||||
export function unregisterHandler(type) {
|
||||
handlers.delete(type);
|
||||
}
|
||||
|
||||
export function getHandler(type) {
|
||||
return handlers.get(type) ?? null;
|
||||
}
|
||||
|
||||
export function listHandlerTypes() {
|
||||
return [...handlers.keys()];
|
||||
}
|
||||
|
||||
// ── 内置 handler:static ──────────────────────────────────────────────────────
|
||||
registerHandler('static', async (block, ctx) => {
|
||||
const gen = block.generator || {};
|
||||
// 优先级:硬编码 value > settings[valueKey] > defaultValue > ''
|
||||
if (gen.value !== undefined) return String(gen.value);
|
||||
if (gen.valueKey != null) {
|
||||
const v = ctx?.settings?.[gen.valueKey];
|
||||
if (v !== undefined && v !== null && v !== '') return String(v);
|
||||
}
|
||||
if (gen.defaultValue !== undefined) return String(gen.defaultValue);
|
||||
return '';
|
||||
});
|
||||
51
core/memory-blocks/index.js
Normal file
51
core/memory-blocks/index.js
Normal file
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* core/memory-blocks/index.js
|
||||
*
|
||||
* 记忆块工作流系统对外入口。导入此模块即触发:
|
||||
* 1. generator-handlers 加载 → 注册内置 'static' handler
|
||||
* 2. registerBuiltinBlocks() → 注册首批内置块(sulv1-4)
|
||||
*
|
||||
* 公开 API:
|
||||
* - register / unregister / getById / listByContext / listAll
|
||||
* - registerHandler / getHandler / listHandlerTypes
|
||||
* - applyToTemplate(template, opts)
|
||||
* - applyToTemplates(templates, opts) ← 多模板批处理首选
|
||||
* - generateBlockMap(opts)
|
||||
*
|
||||
* opts 字段:{ context, settings, signal?, extras? }
|
||||
*
|
||||
* 设计目标:
|
||||
* - BlockDefinition 纯数据,可 JSON 序列化(Phase 3 用户自定义导入导出)
|
||||
* - generator 通过 type 查表,handler 集中注册,便于扩展 ai_call / plugin
|
||||
* - 同一 context 下的块 Promise.all 并发;任一块抛 AbortError 整体中断
|
||||
*/
|
||||
|
||||
export {
|
||||
register,
|
||||
unregister,
|
||||
getById,
|
||||
listByContext,
|
||||
listAll,
|
||||
clear,
|
||||
replaceContextBlocks,
|
||||
} from './registry.js';
|
||||
|
||||
export {
|
||||
registerHandler,
|
||||
unregisterHandler,
|
||||
getHandler,
|
||||
listHandlerTypes,
|
||||
} from './generator-handlers.js';
|
||||
|
||||
export {
|
||||
applyToTemplate,
|
||||
applyToTemplates,
|
||||
generateBlockMap,
|
||||
} from './executor.js';
|
||||
|
||||
import { registerBuiltinBlocks } from './builtin-blocks.js';
|
||||
|
||||
// 导入此模块即完成内置块注册(幂等)
|
||||
registerBuiltinBlocks();
|
||||
|
||||
export { registerBuiltinBlocks };
|
||||
63
core/memory-blocks/registry.js
Normal file
63
core/memory-blocks/registry.js
Normal file
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* core/memory-blocks/registry.js
|
||||
*
|
||||
* BlockDefinition 的注册中心。所有块共享同一个全局 Map。
|
||||
*
|
||||
* 调用方:
|
||||
* - 内置块:builtin-blocks.js 在 bootstrap 时注册
|
||||
* - 用户块:未来 UI / JSON 导入注册
|
||||
* - 插件块:战斗系统等外部模块注册
|
||||
*
|
||||
* 字段校验只做最小必填检查,避免后续扩展时频繁报错。
|
||||
*/
|
||||
|
||||
const blocks = new Map();
|
||||
|
||||
function validate(def) {
|
||||
if (!def || typeof def !== 'object') throw new Error('[MemoryBlocks] BlockDefinition 必须是对象。');
|
||||
if (!def.id) throw new Error('[MemoryBlocks] BlockDefinition.id 必填。');
|
||||
if (!def.placeholder) throw new Error(`[MemoryBlocks] BlockDefinition[${def.id}].placeholder 必填。`);
|
||||
if (!def.context) throw new Error(`[MemoryBlocks] BlockDefinition[${def.id}].context 必填。`);
|
||||
if (!def.generator?.type) throw new Error(`[MemoryBlocks] BlockDefinition[${def.id}].generator.type 必填。`);
|
||||
}
|
||||
|
||||
export function register(def) {
|
||||
validate(def);
|
||||
blocks.set(def.id, { enabled: true, ...def });
|
||||
}
|
||||
|
||||
export function unregister(id) {
|
||||
return blocks.delete(id);
|
||||
}
|
||||
|
||||
export function getById(id) {
|
||||
return blocks.get(id) ?? null;
|
||||
}
|
||||
|
||||
export function listByContext(context) {
|
||||
const out = [];
|
||||
for (const b of blocks.values()) {
|
||||
if (b.context === context && b.enabled !== false) out.push(b);
|
||||
}
|
||||
out.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
||||
return out;
|
||||
}
|
||||
|
||||
export function listAll() {
|
||||
return [...blocks.values()];
|
||||
}
|
||||
|
||||
export function clear() {
|
||||
blocks.clear();
|
||||
}
|
||||
|
||||
/** 批量替换(用于 JSON 导入时整体覆盖某 context 下的块) */
|
||||
export function replaceContextBlocks(context, defs) {
|
||||
for (const [id, b] of blocks) {
|
||||
if (b.context === context) blocks.delete(id);
|
||||
}
|
||||
for (const d of defs) {
|
||||
if (d.context !== context) continue; // 防止越界注册
|
||||
register(d);
|
||||
}
|
||||
}
|
||||
56
core/memory-blocks/types.js
Normal file
56
core/memory-blocks/types.js
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* core/memory-blocks/types.js — 类型契约(JSDoc 文档,无运行时代码)
|
||||
*
|
||||
* BlockDefinition 是工作流的最小单位,描述"如何为某个占位符产出内容"。
|
||||
* 所有字段必须 JSON 可序列化,为后续支持 JSON 导入导出做准备。
|
||||
*
|
||||
* 生成器(generator)只承载"用哪个 handler、参数是什么"的元数据,
|
||||
* 真正的执行逻辑由 generator-handlers.js 按 type 查表的 handler 函数承担,
|
||||
* 因此 BlockDefinition 本身永远不持有函数引用、可直接 JSON.stringify。
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} StaticGenerator 直接读取 settings 或常量值
|
||||
* @property {'static'} type
|
||||
* @property {string} [valueKey] - 从 ctx.settings[valueKey] 读取
|
||||
* @property {*} [defaultValue]- valueKey 不存在/为空时的兜底
|
||||
* @property {*} [value] - 硬编码值,优先级高于 valueKey
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} AiCallGenerator (Phase 2 预留)
|
||||
* @property {'ai_call'} type
|
||||
* @property {string} apiSlot
|
||||
* @property {string} promptTemplate
|
||||
* @property {string} [extractTag]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} PluginGenerator (Phase 3 预留:战斗模块走这条)
|
||||
* @property {'plugin'} type
|
||||
* @property {string} handlerKey - 在 handler 注册表里查 handler 函数
|
||||
* @property {Object} [params]
|
||||
*/
|
||||
|
||||
/** @typedef {StaticGenerator | AiCallGenerator | PluginGenerator} GeneratorSpec */
|
||||
|
||||
/**
|
||||
* @typedef {Object} BlockDefinition
|
||||
* @property {string} id - 全局唯一
|
||||
* @property {string} placeholder - 在模板中要被替换的占位符(按字面量匹配,正则元字符自动转义)
|
||||
* @property {string} context - 所属流水线,如 'plotOptimization'
|
||||
* @property {GeneratorSpec} generator
|
||||
* @property {string} [name] - UI 显示名
|
||||
* @property {boolean} [enabled=true]
|
||||
* @property {number} [order] - 仅影响 listByContext 的返回顺序;执行并发,不阻塞
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} ExecuteContext
|
||||
* @property {Object} settings - extension_settings[extensionName]
|
||||
* @property {AbortSignal} [signal] - 来自调用方的中断信号
|
||||
* @property {string} context
|
||||
* @property {Object} [extras] - 额外上下文,供 handler 自取
|
||||
*/
|
||||
|
||||
export {};
|
||||
Reference in New Issue
Block a user