release: v2.2.6 [2026-06-13 01:02:05]

### 新功能
- **翰林院向量化质量升级**:
  - **边界感知切块**:替换四个来源(聊天记录/小说/世界书/手动)的纯字符硬切——优先在段落边界断开,其次句末标点(含中文引号闭合),极端长串才硬切;句子/对话不再被拦腰截断,embedding 质量同步受益。仅影响新录入,已有向量无需重建
  - **注入时序重排**:检索结果注入提示词前按时序重排(聊天记录按楼层、小说按卷/章/节——中文数字章节号可解析),rerank 只决定"选哪些块",不再决定呈现顺序;修复"不打不相识的剧情之后紧跟关系亲密"这类因按相关度排序导致的认知时间错乱
  - **断层提示**:聊天记录相邻块楼层跳跃时自动插入"与上文相隔约 N 楼,并非连续发生"提示行,消除中间剧情缺失造成的割裂感
  - **时间标识**:新录入的聊天记录块在来源标识中带上消息发送时间(ST 向量存储不持久化元数据,时间必须写入块文本才能在检索后取回;旧格式块兼容解析)
- **记忆块工作流(memory-blocks)**:剧情优化新增"自定义记忆块"体系——占位符驱动的并发工作流框架
  - 在剧情优化面板「匹配替换 (sulv)」下方可增删自定义块:每个块定义一个占位符,执行剧情优化时主/拦截提示词中的占位符会被块的产出替换
  - **静态块**:直接输出固定内容;**AI 调用块**:用所选 API 功能槽独立请求一次,把回复(或其中指定 `<标签>` 的内容)作为替换值
  - 原有 sulv1-4 速率占位符迁入同一框架,行为与旧版逐字节一致
  - 块定义为纯 JSON、随设置持久化,为后续导入导出与战斗系统接入预留扩展点
  - 框架层新增**顺序拼接式 Chain**(`composeChain`):与占位符替换并列的第二种组合范式——同链的块并发执行后按 `order` 排序、以 `separator` 拼接并可选 `header/footer` 包裹,产出一个完整注入块;为记忆注入合成块与战斗系统"底部战报块"预留的承载结构,本版本暂无 UI 入口
- **API 连接配置**:
  - 角色世界书(cwb)与一键生卡(autoCharCard)纳入旧配置自动迁移:老用户首次加载会把旧 URL / Key / 模型自动迁移为连接配置并分配槽位(一键生卡仅在规划者与执行者配置一致或规划者为空时迁移,避免悄悄改变行为)
  - **profile 已分配时参数控件 informational 化**:主面板 / 并发剧情优化 / 角色世界书 / 术语表的温度、maxTokens 控件在槽位分配 profile 后自动禁用并显示"由连接配置控制"提示,消除"改了没效果"的用户陷阱
  - **profile 状态卡新增"本设备无 Key"警示**:API Key 仅保存在最初填写它的设备/浏览器上(安全设计,不随云端设置同步),换设备后状态卡会直接亮出警示徽标,不必等到调用报错才发现
### 修复
- **独立聊天记忆从摆设变真功能(原作遗留坑)**:此前向量数据"随卡不随聊天"——开启"独立聊天记忆"后录入仍存进角色库、查询却去查一个从未被写入过的聊天集合、计数恒为 0,整体静默失效。现已重构为聊天级分桶:
  - 独立模式下,聊天记录类向量按当前聊天隔离存储与检索,同一张卡开多个聊天(不同剧情线)的记忆互不污染
  - 小说 / 世界书 / 手动录入属于"知识",仍随角色卡跨聊天共享;全局库不受影响
  - 知识管理列表为聊天专属库显示"聊天级"徽标;聊天级库禁止移动到全局
  - 统一模式(默认关闭独立记忆)的存量数据与行为完全不变
  - 已知限制:聊天专属记忆跟随聊天文件,重命名聊天文件会使其失联(与 ST 官方向量扩展同等限制)
- **超级排序截断顺序修正**:开启"超级排序"时,时序重排发生在 top_n 截断之前,导致保留的是"时序最早"而非"最相关"的块,检索结果长期偏向最旧的聊天记录。现改为先按相关度截取 top_n、再做时序排序
- **翰林院向量化失败("向量化块数量不识别"反馈)**:
  - 一次性清洗 profile-sync 历史污染:`retrieval/rerank.apiKey` 中的掩码占位符在持久层根治(此前仅读取侧防御);`apiEndpoint` / `rerank.apiMode` 的非法值(如被旧版写入的空字符串)归一化为 `custom`
  - 修复 `apiEndpoint` 为空/非法时请求被硬定向到 `api.openai.com`、无视用户自定义 URL 的问题(CSP 拦截 / 401 的元凶)
  - 修复**本地代理(LM Studio/Ollama)模式**自始就缺少 URL 分支、同样被错误定向到 openai.com 的问题
  - API 模式下拉补全 `OpenAI 官方` / `Azure` 选项;默认 API 模式改为 `custom`(与默认 URL 配套),新用户不再因选项缺失导致首次保存写入空值
  - profile-sync 给下拉框赋不存在选项值的污染源头修复(影响所有模块面板,不止翰林院)
- **Rerank "API Key 未提供"报错升级**:当原因是"连接配置在本设备没有可用 Key"时,报错会直接说明 Key 的设备本地性并指引到 API 连接配置重新填写(向量化 Google 直连、获取模型列表同步处理)
- **旧配置迁移**:一键生卡迁移时排除掩码占位符,避免把历史污染的假 Key 迁入新连接配置
- **超级记忆稳定性专项**(针对"工作不大稳定"反馈,4 处根因一次修复):
  - **切聊天竞态污染**:CHAT_CHANGED 时超级记忆立即全量同步,而表格系统延迟 100ms 才加载新聊天的表格,导致【旧聊天】的表格内容被写进【新角色】的记忆世界书;两边表名不同时旧表条目无 GC 兜底会**永久残留**("记忆串台"元凶)。现 CHAT_CHANGED 只确保世界书存在,新状态同步交由 `loadTables()` 完成后的自动推送,单次且时序正确
  - **死代码双轨存储拆除**:`saveStateToMetadata` / `tryRestoreStateFromMetadata` 把表格状态写到 `msg.metadata`——该字段非 ST 持久化位(同 v2.2.5 二次填表修过的坑),写入即蒸发、恢复永远为空,且每次同步还白调一次 `saveChat()`。整条链路删除,表格状态唯一信源为表格系统的 `msg.extra.amily2_tables_data`
  - **`awaitSync()` 穿透**:同步队列正忙时 `pushUpdate` 会用一个立即 resolve 的空 Promise 覆盖 `_syncPromise`,Pipeline Stage 4 等待形同虚设、后续阶段在同步未完成时被放行。现忙时不覆盖,正在运行的 drain 循环自然吃掉新入队项
  - **开关打开不生效**:启动时若总开关为关,初始化早退且不注册监听器;此后在 UI 勾选开关只写设置,超级记忆直到刷新页面前都是死的。现勾选即触发初始化(幂等)
  - 附带:`forceSyncAll` 的表格角色推断改为复用 `events-schema.inferTableRole`,消除两处重复逻辑漂移风险;每次切聊天的双倍全量同步(restore 路径一次 + 显式一次)随死代码移除归一
### 重构
- 表格核心 `manager.js` 瘦身(约 1050 → 600 行):19 个 UI 突变操作拆分至 `actions/ui-mutations.js`,SuperMemory 事件分发拆分至 `events-dispatch.js`;全部经 re-export 保持兼容,外部调用路径零改动
- 角色世界书最后 2 处散乱的厂商 URL 判断迁移至 `detectVendor` 统一入口,业务路径上不再有硬编码的 URL substring 判断
This commit is contained in:
Jenkins CI
2026-06-13 01:02:05 +08:00
parent 1a4a10d42d
commit 0d7e3b799e
35 changed files with 1981 additions and 1034 deletions

View File

@@ -0,0 +1,54 @@
/**
* core/memory-blocks/ai-call-handler.js — 'ai_call' generator handlerPhase 2
*
* 执行一次独立 AI 调用,把回复(或其中指定标签的内容)作为块的替换值。
*
* 与 generator-handlers.js 分离的原因:本 handler 依赖 core/api.js牵涉
* DOM / ST 运行时),注册表本身保持零依赖,便于单测与 JSON 工具复用。
*
* generator 字段AiCallGenerator契约见 types.js
* apiSlot - callAI 的功能槽('main' / 'plotOpt' / 'nccs' ...),缺省 'main'
* promptTemplate - 作为 user 消息发送的提示词(必填,空则块跳过)
* systemPrompt - 可选,附加在前面的 system 消息
* extractTag - 可选,只取回复中最后一个 <tag>...</tag> 的内容;
* 标签缺失时回退为完整回复(宽容处理,模型偶发不包
* 标签时块仍有产出,而不是静默保留占位符)
*
* 失败语义(与 executor 约定一致):
* - callAI 内部捕获的 API 错误返回 null → 块产出 null → 占位符保留
* - AbortError 由 callAI 原样上抛 → executor 整体中断signal 贯穿 fetch
*/
import { callAI } from '../api.js';
import { extractContentByTag } from '../../utils/tagProcessor.js';
import { registerHandler } from './generator-handlers.js';
registerHandler('ai_call', async (block, ctx) => {
const gen = block.generator || {};
const prompt = typeof gen.promptTemplate === 'string' ? gen.promptTemplate.trim() : '';
if (!prompt) {
console.warn(`[MemoryBlocks] ai_call 块 ${block.id} 缺少 promptTemplate已跳过。`);
return null;
}
const messages = [];
if (typeof gen.systemPrompt === 'string' && gen.systemPrompt.trim()) {
messages.push({ role: 'system', content: gen.systemPrompt });
}
messages.push({ role: 'user', content: prompt });
const response = await callAI(messages, {
slot: gen.apiSlot || 'main',
signal: ctx?.signal,
});
if (!response || !response.trim()) return null;
if (gen.extractTag) {
const extracted = extractContentByTag(response, gen.extractTag);
if (extracted !== null && extracted.trim()) return extracted.trim();
console.warn(`[MemoryBlocks] ai_call 块 ${block.id} 回复中未找到 <${gen.extractTag}> 标签,回退为完整回复。`);
}
return response.trim();
});
export {};

View File

@@ -0,0 +1,97 @@
/**
* core/memory-blocks/chain.js
*
* 顺序拼接式工作流:把 context 下所有启用块的结果,按 block.order 排序后用 separator
* 拼接,并可选 header/footer 包裹,最终输出一个"完整的注入块"字符串。
*
* 与 executor.js模板替换式并列两种组合范式
* - executor: 模板里挖空 placeholder块负责填料 → 替换式
* - chain: 无模板,块各自产出一段文本 → 顺序拼成一整段
*
* 战斗系统设计稿§3.2)里的"战报作底部独立注入块"、未来的"记忆注入合成块"
* 都是 chain 模式的天然用例:战斗模块只需声明一个 BlockDefinitionorder 取大
* 就自动落在拼接末尾。
*
* ── Chain 定义纯数据、JSON 可序列化)─────────────────────────────────────
* {
* id: string // 与 BlockDefinition.context 对齐,块通过 context 隐式归属
* name?: string // UI 显示名
* separator?: string // 块间分隔符,默认 '\n\n'
* header?: string // 整段前缀,可选
* footer?: string // 整段后缀,可选
* }
*
* Chain 无须显式注册也能 compose——未注册时使用默认值方便临时拼接。
*/
import { executeContext } from './runner.js';
const chains = new Map();
const DEFAULT_SEPARATOR = '\n\n';
function validateChain(def) {
if (!def || typeof def !== 'object') throw new Error('[MemoryBlocks/Chain] 定义必须是对象。');
if (!def.id) throw new Error('[MemoryBlocks/Chain] Chain.id 必填。');
}
export function registerChain(def) {
validateChain(def);
chains.set(def.id, {
separator: DEFAULT_SEPARATOR,
header: '',
footer: '',
...def,
});
}
export function unregisterChain(id) {
return chains.delete(id);
}
export function getChain(id) {
return chains.get(id) ?? null;
}
export function listChains() {
return [...chains.values()];
}
/**
* 执行 Chain按 order 排序后拼接成最终字符串。
*
* @param {string} chainId
* @param {{ settings?, signal?, extras? }} [opts]
* @returns {Promise<string>}
*/
export async function composeChain(chainId, opts = {}) {
if (!chainId) return '';
const chain = getChain(chainId);
const results = await executeContext({ context: chainId, ...opts });
const sorted = results
.filter(r => r !== null)
.sort((a, b) => (a.block.order ?? 0) - (b.block.order ?? 0));
const separator = chain?.separator ?? DEFAULT_SEPARATOR;
const body = sorted.map(r => r.value).join(separator);
const parts = [chain?.header, body, chain?.footer]
.map(p => (typeof p === 'string' ? p : ''))
.filter(p => p.length > 0);
return parts.join(separator);
}
/**
* 取 Chain 的执行结果明细(含每块原值),用于调试或调用方自定义后处理。
*
* @returns {Promise<Array<{ block, value }>>}
*/
export async function inspectChain(chainId, opts = {}) {
if (!chainId) return [];
const results = await executeContext({ context: chainId, ...opts });
return results
.filter(r => r !== null)
.sort((a, b) => (a.block.order ?? 0) - (b.block.order ?? 0));
}

View File

@@ -0,0 +1,102 @@
/**
* core/memory-blocks/custom-blocks.js — 用户自定义块的持久化Phase 2
*
* 自定义块以纯 JSONBlockDefinition 数组)存于
* extension_settings[extensionName].memoryBlocks_customBlocks
* 与运行时注册中心registry.js双向同步
* - bootstrap / UI 初始化时 syncCustomBlocksFromSettings() 全量重放
* - 增删改 CRUD 同时更新 settings 与 registry并 saveSettingsDebounced
*
* 自定义块 id 一律以 'custom.' 为前缀,与内置块('plotOpt.sulv1' 等)天然
* 隔离CRUD 仅对该前缀生效,内置块不可经此修改或删除。
*/
import { extension_settings } from '/scripts/extensions.js';
import { saveSettingsDebounced } from '/script.js';
import { extensionName } from '../../utils/settings.js';
import { register, unregister, listAll } from './registry.js';
const STORAGE_KEY = 'memoryBlocks_customBlocks';
export const CUSTOM_ID_PREFIX = 'custom.';
export function isCustomBlockId(id) {
return typeof id === 'string' && id.startsWith(CUSTOM_ID_PREFIX);
}
function getStore() {
if (!extension_settings[extensionName]) extension_settings[extensionName] = {};
const s = extension_settings[extensionName];
if (!Array.isArray(s[STORAGE_KEY])) s[STORAGE_KEY] = [];
return s[STORAGE_KEY];
}
function persist() {
saveSettingsDebounced();
}
function newCustomId() {
return `${CUSTOM_ID_PREFIX}${Date.now().toString(36)}${Math.random().toString(36).slice(2, 7)}`;
}
/**
* 把 settings 中的自定义块全量重放进 registry幂等可重复调用
* 单个块定义损坏时跳过并告警,不影响其余块。
*/
export function syncCustomBlocksFromSettings() {
for (const b of listAll()) {
if (isCustomBlockId(b.id)) unregister(b.id);
}
for (const def of getStore()) {
try {
register(def);
} catch (error) {
console.warn(`[MemoryBlocks] 自定义块定义损坏,已跳过:`, def, error);
}
}
}
/** 列出某 context 下的自定义块settings 为权威源;不过滤 enabled。 */
export function listCustomBlocks(context) {
const store = getStore();
return context ? store.filter(b => b.context === context) : [...store];
}
export function getCustomBlock(id) {
return getStore().find(b => b.id === id) ?? null;
}
/**
* 新增自定义块。def 不含 id自动生成校验失败时抛错、不落库。
* @returns {Object} 落库后的完整定义
*/
export function addCustomBlock(def) {
const full = { enabled: true, ...def, id: newCustomId() };
register(full); // 先过 registry 校验,抛错则不落库
getStore().push(full);
persist();
return full;
}
/** 修改自定义块(浅合并 patchid/非 custom 块不可改)。 */
export function updateCustomBlock(id, patch) {
if (!isCustomBlockId(id)) throw new Error(`[MemoryBlocks] 仅自定义块可修改: ${id}`);
const store = getStore();
const idx = store.findIndex(b => b.id === id);
if (idx === -1) throw new Error(`[MemoryBlocks] 自定义块不存在: ${id}`);
const merged = { ...store[idx], ...patch, id };
register(merged); // 校验 + 覆盖注册
store[idx] = merged;
persist();
return merged;
}
export function deleteCustomBlock(id) {
if (!isCustomBlockId(id)) return false;
const store = getStore();
const idx = store.findIndex(b => b.id === id);
if (idx === -1) return false;
store.splice(idx, 1);
unregister(id);
persist();
return true;
}

View File

@@ -1,43 +1,27 @@
/**
* core/memory-blocks/executor.js
*
* 工作流执行器:拉 context 下的全部块 → Promise.all 并发执行 generator
* → 把每个块的结果按 placeholder 替换回模板
* 模板替换式工作流:用块结果 substitute 到模板的 placeholder 处。
* 与 chain.js顺序拼接式并列两种组合方式共用 runner.js 的底层执行原语
*
* 适用场景sulv1-4 这种"prompt 里已挖好占位符,块负责填料"。
*
* 核心 API
* applyToTemplate(template, opts) 单模板进,字符串出
* applyToTemplates(templates, opts) 多模板进(数组或对象),结构同形出;
* 块只执行一次,对每个模板复用结果
* generateBlockMap(opts) 不替换,返回 { id → value } 给调用方自己玩
* generateBlockMap(opts) 不替换,返回 { id → value } 给调用方自由组合
*
* 中断行为opts.signal 由调用方控制,传给每个 handler任一 handler 抛
* AbortError 时executor 也抛 AbortError 向上传递(与现有 callAI 体系一致)。
* AbortError 时整体抛出向上传递(与现有 callAI 体系一致)。
*/
import { getHandler } from './generator-handlers.js';
import { listByContext } from './registry.js';
import { executeContext } from './runner.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;
@@ -49,20 +33,9 @@ function substituteOne(template, results) {
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);
const results = await executeContext(opts);
return substituteOne(template, results);
}
@@ -73,7 +46,7 @@ export async function applyToTemplate(template, opts = {}) {
* - 字符串 → 退化为 applyToTemplate
*/
export async function applyToTemplates(templates, opts = {}) {
const results = await executeBlocks(opts);
const results = await executeContext(opts);
if (typeof templates === 'string') return substituteOne(templates, results);
if (Array.isArray(templates)) return templates.map(t => substituteOne(t, results));
@@ -89,7 +62,7 @@ export async function applyToTemplates(templates, opts = {}) {
* 不替换,只把块结果汇成 Map<id, value>,调用方拿去自由组合。
*/
export async function generateBlockMap(opts = {}) {
const results = await executeBlocks(opts);
const results = await executeContext(opts);
const map = new Map();
for (const r of results) {
if (r) map.set(r.block.id, r.value);

View File

@@ -3,19 +3,39 @@
*
* 记忆块工作流系统对外入口。导入此模块即触发:
* 1. generator-handlers 加载 → 注册内置 'static' handler
* 2. registerBuiltinBlocks() → 注册首批内置块sulv1-4
* 2. ai-call-handler 加载 → 注册 'ai_call' handlerPhase 2
* 3. registerBuiltinBlocks() → 注册首批内置块sulv1-4
* 4. syncCustomBlocksFromSettings() → 重放用户自定义块Phase 2
*
* 两种组合范式:
* - 模板替换式executor.jsprompt 里挖空 placeholder块填料 → 适合 sulv1-4
* - 顺序拼接式chain.js :块各自产出一段,按 order 拼接成完整注入块 →
* 适合记忆注入、战报底部块
*
* 公开 API
* - register / unregister / getById / listByContext / listAll
* - registerHandler / getHandler / listHandlerTypes
* - applyToTemplate(template, opts)
* - applyToTemplates(templates, opts) ← 多模板批处理首选
* - generateBlockMap(opts)
* Block
* register / unregister / getById / listByContext / listAll
* replaceContextBlocks (批量替换某 context 下全部块JSON 导入用)
* Handler
* registerHandler / unregisterHandler / getHandler / listHandlerTypes
* 模板替换式:
* applyToTemplate(template, opts)
* applyToTemplates(templates, opts) ← 多模板批处理首选
* generateBlockMap(opts)
* 顺序拼接式:
* registerChain(def) / unregisterChain / getChain / listChains
* composeChain(chainId, opts) → string
* inspectChain(chainId, opts) → Array<{block, value}>(调试/自定义后处理)
* 自定义块 CRUDPhase 2用户在 UI 增删改):
* listCustomBlocks / getCustomBlock / addCustomBlock /
* updateCustomBlock / deleteCustomBlock / syncCustomBlocksFromSettings
* CUSTOM_ID_PREFIX / isCustomBlockId
*
* opts 字段:{ context, settings, signal?, extras? }
* opts 字段:{ settings, signal?, extras? }
* context 对应 chainId / Block.context由各 API 自行传或从 chainId 推导)
*
* 设计目标:
* - BlockDefinition 纯数据,JSON 序列化Phase 3 用户自定义导入导出)
* - BlockDefinition / ChainDefinition 都是纯数据JSON 序列化Phase 3 用户自定义导入导出)
* - generator 通过 type 查表handler 集中注册,便于扩展 ai_call / plugin
* - 同一 context 下的块 Promise.all 并发;任一块抛 AbortError 整体中断
*/
@@ -43,9 +63,33 @@ export {
generateBlockMap,
} from './executor.js';
import { registerBuiltinBlocks } from './builtin-blocks.js';
export {
registerChain,
unregisterChain,
getChain,
listChains,
composeChain,
inspectChain,
} from './chain.js';
// 导入此模块即完成内置块注册(幂等)
export {
CUSTOM_ID_PREFIX,
isCustomBlockId,
listCustomBlocks,
getCustomBlock,
addCustomBlock,
updateCustomBlock,
deleteCustomBlock,
syncCustomBlocksFromSettings,
} from './custom-blocks.js';
import './ai-call-handler.js'; // 副作用:注册 'ai_call' handler
import { registerBuiltinBlocks } from './builtin-blocks.js';
import { syncCustomBlocksFromSettings } from './custom-blocks.js';
// 导入此模块即完成内置块注册与自定义块重放(均幂等)。
// ST 在 import 扩展脚本前已加载完 extension_settings此时读取是安全的。
registerBuiltinBlocks();
syncCustomBlocksFromSettings();
export { registerBuiltinBlocks };

View File

@@ -0,0 +1,40 @@
/**
* core/memory-blocks/runner.js
*
* 块执行的底层原语,被 executor.js模板替换和 chain.js顺序拼接共用。
*
* runBlock(block, ctx) → { block, value } | null
* 单块执行handler 抛 AbortError 时向上传递,其余异常吞掉并返回 null
* handler 返回 null/undefined 时同样返回 null视为"无内容"
*
* executeContext({ context, settings, signal, extras }) → Array<{block,value}|null>
* 按 context 拉块 → Promise.all 并发执行 → 返回结果数组(保留 null 占位以便上层
* 按 order 排序时不丢失映射关系,调用方过滤 null 即可)
*/
import { getHandler } from './generator-handlers.js';
import { listByContext } from './registry.js';
export 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;
}
}
export async function executeContext({ 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)));
}

View File

@@ -18,11 +18,12 @@
*/
/**
* @typedef {Object} AiCallGenerator (Phase 2 预留)
* @typedef {Object} AiCallGenerator 独立 AI 调用handler 见 ai-call-handler.js
* @property {'ai_call'} type
* @property {string} apiSlot
* @property {string} promptTemplate
* @property {string} [extractTag]
* @property {string} [apiSlot='main'] - callAI 功能槽ApiProfileManager.SLOTS 中的 chat 槽)
* @property {string} promptTemplate - 作为 user 消息发送;空则块跳过
* @property {string} [systemPrompt] - 可选的 system 消息
* @property {string} [extractTag] - 只取回复中 <tag>...</tag> 的内容;缺失时回退完整回复
*/
/**
@@ -36,7 +37,7 @@
/**
* @typedef {Object} BlockDefinition
* @property {string} id - 全局唯一
* @property {string} id - 全局唯一;用户自定义块以 'custom.' 为前缀(见 custom-blocks.js
* @property {string} placeholder - 在模板中要被替换的占位符(按字面量匹配,正则元字符自动转义)
* @property {string} context - 所属流水线,如 'plotOptimization'
* @property {GeneratorSpec} generator