MVU Exec Engine
独立的后端 TypeScript 库,用于:
- 动态提示词编排(预设/preset、人物卡/persona、世界书/worldbook、历史消息),构建可执行的 LLM messages。
- 按 MVU 思路解析并应用 区块,程序化变量更新,输出 stat/display/delta 三视图并生成基础 Schema 进行约束。
- 兼容 SillyTavern(ST)素材:支持 ST preset/persona/regex 的适配与 prompt order;支持 ST 风格的正则脚本门控(placement/markdownOnly/promptOnly/depth 等)。
- 提供 Provider(OpenAI/Anthropic/Gemini/Echo)、持久化(示例内存)、事件回调等扩展点,易于集成到任意应用。
环境
- Node >= 22.18.0(ESM)
安装
- 在项目根目录:
npm i,然后npm run build - 作为库使用:发布到私有/公共仓库后,在业务项目中
import { Engine, ... } from 'mvu-exec-engine'
快速上手
import { Engine, InMemoryStore, EchoProvider } from 'mvu-exec-engine';
const engine = new Engine({ provider: new EchoProvider(), store: new InMemoryStore(), sessionId: 'demo' });
await engine.loadMaterials({
preset: { system: '你是严谨可靠的助手。所有变量更新必须在 <UpdateVariable> 中按格式输出。' },
persona: { name: '理', define: '圣女,温柔端庄。' },
worldbook: [],
initialVariables: {
日期: ["03月15日", "mm月dd日"],
理: { 好感度: [0, "[-30,100]之间"] },
$meta: { strictSet: true }
}
});
const { messages } = await engine.compose({ history: [{ role: 'user', content: '打个招呼。' }] });
const reply = await engine.generate(messages); // 内部会做 generate 阶段预处理
const rendered = await engine.postprocessReply(reply.content); // 渲染阶段后处理(模板/注入/正则)
const result = await engine.applyVariablesFromReply(rendered); // 解析并应用 <UpdateVariable>核心能力
- Prompt 组合:从 preset/persona/worldbook 生成会话 messages;支持 ST prompt order、按位/按目标/按正则的 @INJECT、按条目就近贴片
[GENERATE:{idx}:BEFORE|AFTER]。 - 模板运行时:使用
StaticTemplateRuntime(纯字符串模板,仅支持 mustache 风格占位符与简单宏,不执行任何脚本/EJS)。SafeFunctionTemplateRuntime/VMTemplateRuntime只是对StaticTemplateRuntime的兼容别名。模板环境可访问variables/display/delta/persona/preset/globals/getVar等。 - 正则规则:在 generate/render 两阶段对 messages 或最终 reply 进行按序替换;兼容 ST 脚本的门控字段(markdownOnly/promptOnly/stPlacement/minDepth/maxDepth/runOnEdit 等)。
- 变量引擎:解析并执行
_.set/add/assign/insert/remove|unset|delete,维护 stat/display/delta;自动从初始变量生成 Schema,支持$meta.extensible=false拒绝越权写入。 - Provider:内置 Echo/OpenAI/Anthropic/Gemini,支持代理(ALL_PROXY/HTTPS_PROXY/HTTP_PROXY/SOCKS 等)与简单用量估算。
示例与命令
- 运行前先
npm run build - 快速演示(EchoProvider):
npm run demo - 基于 ST 素材的演示:
- 预览+生成:
npm run demo:best - 交互式 CLI:
npm run demo:best:chat- 环境变量:
PROVIDER=gemini|openai|anthropic(也可通过是否设置GEMINI_API_KEY自动选择)USER_TITLE=豪哥(注入到 globals 中用于模板替换)- 代理:
ALL_PROXY/HTTPS_PROXY/HTTP_PROXY/SOCKS_PROXY或REQUEST_PROXY_URL+REQUEST_PROXY_ENABLED=1
- 运行后可选择 persona 的
first_mes/alternate_greetings,输入消息对话,/exit退出。
- 环境变量:
- Web Demo:
npm run demo:web然后访问http://localhost:3000 - v0.2 API 演示:
npm run demo:v02
- 预览+生成:
v0.2 新增/常用 API
- 流式辅助:
engine.generateStream(messages, { postprocess?, emitVariableState? }):真正流式(SSE/Chunk)。事件:{ type: 'content', delta, content? }文本增量;当postprocess: true时,content为累计渲染结果。{ type: 'usage', data }用量快照(inputTokens/outputTokens/model)。{ type: 'variable_state', data }可选,尾部根据最终文本解析变量并下发状态。{ type: 'final', text }末尾补齐全文本。
engine.postprocessStream(asyncIterable):对任意文本增量应用渲染规则,输出{ type:'content', delta, content }。
- Prompt 序列选择(ST 对齐):
engine.listPromptOrders()、engine.selectPromptOrder('last'|'global'|{ characterId })
- 全局与覆盖:
engine.setUserGlobals({ user, user_title })、engine.setUserOverrides({ ... })
- 调试与导出:
engine.getUsage()、engine.exportConversation({ format: 'json'|'object' })
- 正则规则:
engine.listRegexRules()、engine.validateRegex(rules)、engine.updateRegexRules(rules)
- 事件:
engine.on('compose:start'|'generate:end'|'postprocess:end'|'promptOrder:changed', handler)
SillyTavern 适配
adaptStPresetToMaterials(stPreset[, { preferCharacterId }]):从 ST preset 生成 preset/worldbook/modelParams/meta(含 prompt order)。adaptStPersonaToMaterials(stPersona):从 ST 人物卡生成 persona/worldbook/regex(含 ST world/entries)。adaptStRegexToRules(scripts):将 ST regex 脚本转为materials.regex规则数组(保留门控字段)。
Provider 与代理配置
- 选择 Provider:
PROVIDER=gemini|openai|anthropic|claude(best_chat_cli 中使用);若检测到GEMINI_API_KEY会优先选 Gemini。
- OpenAI:
OPENAI_API_KEY、OPENAI_BASE_URL(可选)、OPENAI_MODEL(默认gpt-4o-mini) - Anthropic:
ANTHROPIC_API_KEY、ANTHROPIC_BASE_URL(可选)、ANTHROPIC_MODEL - Gemini:
GEMINI_API_KEY、GEMINI_MODEL、GEMINI_RESPONSE_MIME(可选)、GEMINI_USE_HEADER=1(走 header 鉴权,否则 query key) - 代理:
- 通用:
ALL_PROXY/HTTPS_PROXY/HTTP_PROXY/SOCKS_PROXY(支持 socks5h/socks5),或设置REQUEST_PROXY_URL+REQUEST_PROXY_ENABLED=1 - 跳过域名:
NO_PROXY/no_proxy
- 通用:
- 调试:
DEBUG_PROXY=1打印代理相关信息
Cloudflare Worker 快速示例(流式)
// worker.ts
import { Engine, InMemoryStore, OpenAIProvider } from 'mvu-exec-engine';
export default {
async fetch(req: Request, env: any) {
const url = new URL(req.url);
if (url.pathname === '/chat') {
const provider = new OpenAIProvider({ apiKey: env.OPENAI_API_KEY, model: 'gpt-4o-mini' });
const engine = new Engine({ provider, store: new InMemoryStore() }); // 默认 StaticTemplateRuntime
await engine.loadMaterials({
preset: { system: '你是严谨的助手。' },
persona: { name: '理', define: '温柔端庄。' },
worldbook: [],
});
const { messages } = await engine.compose({ history: [{ role: 'user', content: '打个招呼' }] });
const { readable, writable } = new TransformStream();
const writer = writable.getWriter();
(async () => {
const enc = new TextEncoder();
for await (const ev of engine.generateStream(messages, { postprocess: true, emitVariableState: true })) {
if (ev.type === 'content') await writer.write(enc.encode(ev.delta));
if (ev.type === 'final') await writer.write(enc.encode('\n'));
}
await writer.close();
})();
return new Response(readable, { headers: { 'Content-Type': 'text/plain; charset=utf-8' } });
}
return new Response('ok');
}
}更多文档
- 使用说明(推荐阅读):
docs/USAGE_CN.md - 模板与实施规范:
docs/TEMPLATE_PROMPT_CONVERSION.md - UI 集成规范:
docs/UIDEV_SPEC.md - Cloudflare 部署指南:
docs/DEPLOY_CLOUDFLARE.md
注意
- 本库不再支持
<% %>EJS 脚本;如模板中出现将抛出E_TEMPLATE_SCRIPT_NOT_SUPPORTED。stripCode仍用于兼容 ST 模板中的残留标记,但不会执行脚本。 - Schema 来自初始变量的结构推断,
$meta.extensible=false会阻止对象/数组新增项;被拒绝的命令将写入VariableData.errors。 - engines 要求 Node >= 22.18.0。
切换模板运行时(可选)
- 默认:
StaticTemplateRuntime(纯字符串模板,不执行脚本) - 兼容别名:
SafeFunctionTemplateRuntime/VMTemplateRuntime实际都复用StaticTemplateRuntime,不再提供 node:vm/new Function 执行环境。import { Engine, StaticTemplateRuntime } from 'mvu-exec-engine'; const engine = new Engine({ provider, store, runtime: new StaticTemplateRuntime() });
流式接口说明(摘要)
- 事件模型:
{ type: 'content', delta, content? }文本增量;当postprocess: true时,content为累计渲染结果(适合直接显示)。{ type: 'usage', data }用量快照(inputTokens/outputTokens/model)。{ type: 'variable_state', data }可选;尾部将最终文本解析后下发变量更新结果与最新变量树。{ type: 'final', text }最终文本收束。
- 两种使用方式:
- 推荐:
engine.generateStream(messages, { postprocess: true, emitVariableState: true }),获取“已渲染”的增量,末尾携带变量状态。 - 自定义:若要对“原始增量”自行处理,可不传
postprocess,并用engine.postprocessStream(rawChunks)处理。
- 推荐:
- 终止:传入
{ signal: AbortController.signal }到 Provider 的sendStream(内置 Provider 已支持)。