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:
Jenkins CI
2026-06-10 12:41:11 +08:00
parent 347016d5ac
commit 1a4a10d42d
15 changed files with 412 additions and 25 deletions

View 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,
});
}

View 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;
}

View 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()];
}
// ── 内置 handlerstatic ──────────────────────────────────────────────────────
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 '';
});

View 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 };

View 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);
}
}

View 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 {};