宏
宏(Macro)是写成 {{name}} 形式的占位符,在 prompt 组装时被替换为动态内容。它们出现在预设、世界书、角色卡、聊天消息、斜杠命令、正则替换里——任何会被拼装进 prompt 的文本字段都能用宏。请求到达 AI 时,所有的宏都已经被替换成最终字符串。
Macros 2.0 / 实验性宏引擎
本页的特性跑在 chevrotain 实现的新宏引擎上——也就是 SillyTavern 引入的 Macros 2.0。Luker 预设启用它,可以在「用户设置 → 聊天/消息处理 → 实验性宏引擎」切换。
实验性引擎关掉时,宏仍然能用,但下面这些特性会退化到老的正则管线、不再可用:
| 特性 | 需要实验性引擎 |
|---|---|
{{if}} / {{else}} / {{each}} 控制流 | ✓ |
范围宏,如 {{setvar::k}}body{{/setvar}} | ✓ |
变量简写({{.var}}、{{$var}}、表达式) | ✓ |
标志位(#、/、……) | ✓ |
| 宏参数内嵌套宏 | ✓ |
| 范围宏内容保留前导空白 | ✓ |
| 多遍展开时的稳定替换顺序(从左到右、内层先于外层) | ✓ |
一般 {{user}}、{{setvar::name::value}}、{{time}}…… | 两种都能用 |
本页里某个特性看起来不生效,先检查这个开关。
语法
宏的形态是 {{name}}、{{name::arg}},或 {{name::arg1::arg2}}。宏名大小写不敏感——{{user}}、{{User}}、{{USER}} 解析到同一个宏。花括号和宏名之间允许空白:{{ user }} 等价于 {{user}}。
宏识别子必须符合 ^[a-zA-Z][\w-_]*$——字母开头,后接字母、数字、底线或连字号。唯一的例外是注解宏 {{//}}。
参数
参数之间用 :: 分隔:
{{setvar::hp::50}}
{{datetimeformat::YYYY-MM-DD HH:mm:ss}}
{{roll::3d6+4}}单个 : 作为兜底也认({{roll: 1d20}}),但因为正文里经常出现单冒号,推荐用 ::。
逗号列表宏({{random}}、{{pick}})两种写法都行:
{{random::red::green::blue}}
{{random::red,green,blue}}列表项里要写字面意义的逗号,转义为 \,。
嵌套宏
宏可以嵌套。内层先解析,结果作为外层的参数:
{{setvar::greeting::Hello, {{user}}!}}
{{if {{getvar::showHeader}}}}# Header{{/if}}
{{each::{{getvar::roster}}}}- {{loop_value::name}}{{/each}}嵌套深度没有硬上限,但嵌套太深通常是宏在做太多事——拆成中间变量。
范围宏
部分宏({{if}}、{{each}}、{{trim}}、{{//}},以及大多数接受参数的自定义宏)支持「开 / 闭标签 + 中间内容」的写法。中间的内容会作为最后一个位置参数传给宏:
{{if .ready}}
角色已就绪。
{{/if}}
{{each::npcs}}
- {{loop_key}}: {{loop_value::hp}} HP
{{/each}}预设情况下,范围宏的内容会被自动整理——首尾空白被剥掉,首个非空行的共同前导缩进会从所有行里减掉。要原样保留空白,在宏名前加 # 标志位:
{{#if .verbose}}
这一段的缩进和首尾空白都会原样保留。
{{/if}}转义
要输出字面的双花括号,前面加反斜杠:
\{{not a macro}}反斜杠在后处理阶段被去掉,原始的 {{...}} 字符串原样到达 AI。这是在 prompt 里教模型宏语法而不让示例真的被执行的官方写法。
老式标签
5 个非花括号的老式标签,会在宏解析前被自动重写:
| 老式 | 现代等价 |
|---|---|
<USER> | {{user}} |
<BOT> / <CHAR> | {{char}} |
<GROUP> / <CHARIFNOTGROUP> | {{group}} / {{charIfNotGroup}} |
新内容用花括号写法。老式标签为向后兼容保留。
{{time_UTC+N}} 同样会被重写为 {{time::UTC+N}}。
变量简写
除了完整的 {{getvar::name}} / {{setvar::name::value}},Luker 还提供一套变量表达式简写,读起来跟赋值语句一样:
| 写法 | 含义 | 返回 |
|---|---|---|
{{.name}} | 读局部变量 | 当前值 |
{{$name}} | 读全局变量 | 当前值 |
{{.name = 5}} | 赋值 | ''(空字符串) |
{{.name += 1}} | 加(数值加 / 字符串拼接) | '' |
{{.name -= 1}} | 减(仅数值;非数值会告警) | '' |
{{.name++}} | 自增 | '' |
{{.name--}} | 自减 | '' |
{{.name ?? "default"}} | 取值,未定义时取预设 | 值或预设 |
{{.name || "default"}} | 取值,falsy 时取预设 | 值或预设 |
{{.name ??= "x"}} | 未定义时才赋值 | 新值 |
{{.name ||= "x"}} | falsy 时才赋值 | 新值 |
{{.name == 5}} | 字符串相等 | "true" / "false" |
{{.name != 5}} | 字符串不等 | "true" / "false" |
{{.name > 5}} | 数值 > | "true" / "false" |
{{.name >= 5}} | 数值 >= | "true" / "false" |
{{.name < 5}} | 数值 < | "true" / "false" |
{{.name <= 5}} | 数值 <= | "true" / "false" |
. 永远指 chat 局部变量,$ 永远指全局变量。两个作用域不共享命名。
变量表达式可以和控制流组合:
{{if .hp <= 0}}
你死了。
{{else}}
你还剩 {{.hp}} 点 HP。
{{/if}}变量名允许底线和连字号,但最后一个字符必须是 word 字符:my-var 合法,my- 不合法(会和 -- 自减操作符冲突)。
默认值惰性求值
?? 和 || 只在变量缺失 / falsy 时才会求 fallback 表达式。这能让你跳过昂贵的默认值:{{.cachedSummary ?? {{summary}}}} 只在缓存未命中时才会调用 {{summary}}。
控制流
条件 — {{if}} / {{else}}
{{if condition}}then-content{{/if}}
{{if condition}}then{{else}}otherwise{{/if}}
{{if !condition}}negated{{/if}}condition 可以是:
- 字面值——空字符串、
false、off、0视为 falsy;其他都 truthy。 - 已注册的宏名(不带花括号)——
{{if description}}# Description{{/if}}会先把description解析出来。 - 嵌套宏——
{{if {{getvar::showHeader}}}}...{{/if}}。 - 变量简写——
{{if .ready}}、{{if $debugFlag}}。 - 变量表达式——
{{if .hp > 0}}。 - 以上任何一种前面加
!取反——{{if !.dead}}。
分支惰性求值
只有命中的那个分支里的嵌套宏才会被解析。{{if .casting}}{{.mana -= 10}}{{/if}} 在 .casting 为 false 时不会扣魔。把 {{if}} 套在昂贵或有副作用的宏外面是安全的。
迭代 — {{each}}
{{each::collection}}
{{loop_key}}: {{loop_value}}
{{/each}}collection 接受三种形式:
- 内嵌 JSON 字面量——
{{each::["sword","shield"]}}或{{each::{"a":1,"b":2}}}。 - 变量名——
{{each::npcs}}读局部变量npcs(找不到 fallback 到全局),并把字符串当 JSON 解析。 - 解析结果为集合的嵌套宏——
{{each::{{getglobalvar::roster}}}}。
body 里:
| 宏 | 含义 |
|---|---|
{{loop_key}} | 当前 key(数组时是字符串形式的索引) |
{{loop_value}} | 当前值整体(对象会自动 JSON 字符串化) |
{{loop_value::field}} | 用点号路径深入值,语义同 {{getvar}} |
嵌套 {{each}} 会自然屏蔽外层的 loop_key / loop_value。对象的迭代顺序遵循 JavaScript 原生规则——字符串 key 走插入顺序,整数样 key 走升序。
空集合或不可迭代值渲染为空字符串。引擎没有对迭代次数加保护——body 里再次 {{each}} 自己负责防爆。
注解 — {{//}}
行内:
{{// 这一行被忽略}}范围形式:
{{//}}
多行注解。
里面可以写 {{宏}} 和 \{转义\}——什么都不会运行。
{{///}}注解解析为空字符串,内容原样吃掉。给 prompt 里要标注但不想让 AI 看见的部分用。
变量
变量分两种作用域:
- 局部——按聊天独立保存,持久化在
chat_metadata.variables里。用来存聊天专属状态(HP、当前任务、回合数)。 - 全局——跨所有聊天共享,存在扩展设置里。用来存跨 chat 计数器、插件设置。
读
| 宏 | 变量简写 | 用途 |
|---|---|---|
{{getvar::name}} | {{.name}} | 读局部 |
{{getglobalvar::name}} | {{$name}} | 读全局 |
{{hasvar::name}}(别名 varexists) | — | "true" / "false" |
{{hasglobalvar::name}}(别名 globalvarexists) | — | "true" / "false" |
不存在的变量返回空字符串。
写
| 宏 | 变量简写 | 用途 |
|---|---|---|
{{setvar::name::value}} | {{.name = value}} | 设置局部 |
{{addvar::name::value}} | {{.name += value}} | 加(数值)/ 拼接(字符串)/ push(JSON 数组) |
{{incvar::name}} | {{.name++}} | +1 |
{{decvar::name}} | {{.name--}} | -1 |
{{deletevar::name}}(别名 flushvar) | — | 删除 |
全局对应的有 setglobalvar / addglobalvar / incglobalvar / decglobalvar / deleteglobalvar。
addvar 是重载的:两侧都是数值字符串时做数值加;现有值是 JSON 数组时 push;否则按字符串拼接。
用 {{noop}} 锚住空白
拼接、范围宏 body、+= " text" 等场景下,前导/尾部空白经常被引擎的自动整理或参数 trim 吃掉。在要保留的空白前后插一个 {{noop}}(解析为空字符串)就能锚住它。例如 {{addvar::story::{{noop}} 这是新的一段。}}。
结构化值的点号路径
变量可以存任何 JSON 可序列化的值。当变量存的是 JSON 字符串化的对象 / 数组时,点号路径直接生效:
{{setvar::npcs::{"alice":{"hp":40},"bob":{"hp":30}}}}
{{getvar::npcs.alice.hp}} → 40
{{getvar::npcs.alice}} → {"hp":40}
{{getvar::list.0}} → list 的第一个元素对非 JSON 值用点号路径时,会 fallback 到字面键查找——名字真的就是 a.b 的变量也能读到。
这套和 {{each}} 配合很自然:一个 NPC 名册、一份背包字典、一本任务日志都可以塞进单一变量,每轮 prompt 组装时再渲染到 prompt 或世界书条目里:
{{each::npcs}}
- {{loop_key}}: {{loop_value::hp}} HP
{{/each}}逐楼层变量
原生 SillyTavern 里,副作用宏 {{setvar::hp::50}} 只在 prompt 范本 里(预设、世界书、首楼)才会运行。AI 在回复里写同样的字面量什么都不会发生,还会原样显示出来污染叙事。
Luker 用逐楼层变量提取解决这个问题。一条消息(AI 回复、用户消息、swipe、续写)保存时,Luker 会:
- 扫描文本里的
{{setvar}}、{{addvar}}、{{incvar}}、{{decvar}}、{{deletevar}}。 - 把里面嵌套的展示宏(
{{user}}、{{getvar::other}}、{{time}}……)对当前状态求值。 - 把 op 立刻 apply 到
chat_metadata.variables。 - 在
message.extra.var_ops上追加一条结构化记录。 - 从可见文本里把字面量删掉。
当你删消息、切 swipe、重新生成、编辑时,Luker 会重播剩余的 op log,让变量状态跟可见的时间线保持一致。
这就是「逐楼层变量」面板背后的机制——每条带 op 的消息按钮栏会出现一个烧瓶图标,点开可以查看 / 编辑 / 删除 / 添加 op。结果就是 AI 能直接在自己回复里拥有和修改状态,而这个状态能扛住用户惯常的所有结构性操作。
完整的特性页面(重播语义、swipe 生命周期、op 编辑器、推荐的创作范式)见 逐楼层变量。
全局变量不会被提取
{{setglobalvar}} 这一家不在逐楼层提取的范围内——全局状态是跨 chat 的,跟某条消息绑不到一起。它们保留原生 SillyTavern 的语义。
内置宏目录
/? macros 会打开应用内浏览器,里面是同一份描述、能搜索、能看即时签名。
名称与参与方
| 宏 | 返回 |
|---|---|
{{user}} | 当前 persona 名 |
{{char}} | 当前角色名 |
{{group}}(别名 charIfNotGroup) | 群聊成员名字(逗号分隔),单聊时是角色名 |
{{groupNotMuted}} | 同 group 但排除被静音的成员 |
{{notChar}} | 除当前发言者之外的所有参与方 |
角色卡字段
| 宏 | 返回 |
|---|---|
{{charDescription}}(别名 description) | 描述字段 |
{{charPersonality}}(别名 personality) | 人格字段 |
{{charScenario}}(别名 scenario) | 场景字段 |
{{persona}} | 当前 persona 描述 |
{{charPrompt}} | 主提示词 override |
{{charInstruction}} | Post-History Instructions override |
{{charDepthPrompt}} | @ Depth Note |
{{charCreatorNotes}}(别名 creatorNotes) | 作者笔记 |
{{charVersion}} | 卡片版本号 |
{{charFirstMessage}}(别名 greeting) | 主问候语(索引 0) |
{{greeting::N}} | 第 N 条问候语——0 是主,1+ 是 alt greetings |
{{mesExamples}} | 对话示例,启用 instruct 时自动按 instruct 格式化 |
{{mesExamplesRaw}} | 对话示例原文 |
{{original}} | 角色级 override 里的原始 prompt 内容。单次有效——同一次替换里第二次调用返回 ""。只在 charPrompt / charInstruction 内有意义。 |
聊天状态
| 宏 | 返回 |
|---|---|
{{lastMessage}} | 最后一条消息的文本 |
{{lastMessageId}} | 最后一条消息的索引 |
{{lastUserMessage}} | 最后一条用户消息文本 |
{{lastCharMessage}} | 最后一条角色消息文本 |
{{firstIncludedMessageId}} | 当前 context 窗口里第一条消息的索引 |
{{firstDisplayedMessageId}} | 聊天卷动区里第一条可见消息的索引 |
{{lastSwipeId}} | 最后一条消息的 swipe 数(1-based) |
{{currentSwipeId}} | 当前活跃 swipe 的索引(1-based) |
{{allChatRange}} | 形如 0-12 的全消息范围字符串,空 chat 时是空字符串 |
{{idleDuration}}(别名 idle_duration) | 距上一次用户消息的可读时长 |
{{lastGenerationType}} | normal / impersonate / regenerate / quiet / swipe / continue |
时间与日期
| 宏 | 返回 |
|---|---|
{{time}} | 本地时间(locale 短格式,如 3:45 PM) |
{{time::UTC+2}} | 指定 UTC 偏移的本地时间 |
{{date}} | 本地日期(locale 长格式) |
{{weekday}} | 星期名(Monday、……) |
{{isotime}} | HH:mm |
{{isodate}} | YYYY-MM-DD |
{{datetimeformat::FORMAT}} | 用 moment.js 格式字符串渲染当前时间 |
{{timeDiff::A::B}} | 两个时间之间可读的绝对差 |
变量
完整列表见上面的 变量 一节。
控制流与工具
| 宏 | 返回 |
|---|---|
{{if cond}}…{{/if}} | 条件内容 |
{{else}} | 在 {{if}} 里标记 else 分支 |
{{each::col}}…{{/each}} | 迭代 |
{{loop_key}} / {{loop_value}} / {{loop_value::path}} | 在 {{each}} body 里 |
{{trim}} | 行内:后处理时把宏周围的换行删掉。范围形式:返回剥掉首尾空白后的内容 |
{{newline}} / {{newline::N}} | 1 个或 N 个换行 |
{{space}} / {{space::N}} | 1 个或 N 个空格 |
{{noop}} | 空字符串。在拼接 / 范围宏里作为空白锚点很好用 |
{{reverse::text}} | 反转字符串 |
{{//comment}}(别名 comment) | 注解(输出为空) |
随机
| 宏 | 返回 |
|---|---|
{{roll::1d20}} | 用 droll 语法掷骰(1d6、3d6+4……)。只有数字 N 时等价 1dN。每次渲染都重掷。 |
{{random::red::green::blue}} | 随机一项。每次渲染都重掷。 |
{{pick::red::green::blue}} | 随机一项,但对同一 chat 同一位置稳定。Seed = chat hash + content hash + 位置 + reroll seed。用 /reroll-pick 重置。 |
环境与 API
| 宏 | 返回 |
|---|---|
{{model}} | 当前 model 识别字 |
{{maxPrompt}}(别名 maxPromptTokens) | prompt 上下文 token 上限 |
{{maxContext}}(别名 maxContextTokens) | 上下文 token 上限 |
{{maxResponse}}(别名 maxResponseTokens) | 响应 token 上限 |
{{isMobile}} | 行动设备时 "true" |
{{hasExtension::name}} | 指定扩展已加载且启用时 "true" |
{{input}} | 当前输入框内容 |
{{outlet::key}} | 指定 key 的世界书 outlet 内容 |
{{banned::word}} | 给 Text Completion 后端注册一个 banned word;返回空 |
作者笔记与摘要
| 宏 | 返回 |
|---|---|
{{authorsNote}} | 当前 chat 生效的作者笔记 |
{{charAuthorsNote}} | 角色级作者笔记 |
{{defaultAuthorsNote}} | 预设作者笔记 |
{{summary}} | 聊天摘要——只在 Summarize 扩展加载后才注册 |
推理范本
| 宏 | 返回 |
|---|---|
{{reasoningPrefix}} | 推理段前缀 |
{{reasoningSuffix}} | 推理段后缀 |
{{reasoningSeparator}} | 推理与答案之间的分隔 |
Instruct 与系统提示词
下面这些只在对应的 instruct / sysprompt 开关打开时返回内容。
| 宏 | 返回 |
|---|---|
{{systemPrompt}} | 当前生效的系统提示词。启用 Prefer Character Prompt 时会切到 {{charPrompt}}。 |
{{defaultSystemPrompt}}(别名 instructSystem、instructSystemPrompt) | 设置的预设系统提示词 |
{{instructStoryStringPrefix}} / {{instructStoryStringSuffix}} | story string 包裹 |
{{instructUserPrefix}}(别名 instructInput) / {{instructUserSuffix}} | 用户回合 sequence |
{{instructAssistantPrefix}}(别名 instructOutput) / {{instructAssistantSuffix}}(别名 instructSeparator) | 助理回合 sequence |
{{instructFirstAssistantPrefix}}(别名 instructFirstOutputPrefix) / {{instructLastAssistantPrefix}}(别名 instructLastOutputPrefix) | 首 / 末助理变体 |
{{instructFirstUserPrefix}}(别名 instructFirstInput) / {{instructLastUserPrefix}}(别名 instructLastInput) | 首 / 末用户变体 |
{{instructSystemPrefix}} / {{instructSystemSuffix}} | 系统回合 sequence |
{{instructSystemInstructionPrefix}} | last-system sequence |
{{instructStop}} | stop sequence |
{{instructUserFiller}} | 对齐填充 |
{{exampleSeparator}}(别名 chatSeparator) / {{chatStart}} | context 范本标记 |
扩展注册
只在对应扩展安装且启用时才存在:
| 宏 | 来源 | 返回 |
|---|---|---|
{{charPrefix}} | Stable Diffusion | 正面图像生成 prompt 前缀 |
{{charNegativePrefix}} | Stable Diffusion | 负面图像生成 prompt 前缀 |
{{summary}} | Summarize | 保存的聊天摘要 |
第三方扩展可以注册更多宏,宏浏览器会标出每个宏的来源。
标志位
标志位是放在开头 {{ 和宏名之间的单字符,用来改变宏的解析或求值行为。
/ — 闭合标记
标记一个标签是范围宏的闭合半边。和同名的开标签配对,把两者之间的内容作为宏的最后一个位置参数传入:
{{if .ready}} ...body... {{/if}}
{{each::npcs}} ...body... {{/each}}
{{setvar::longText}} ...body... {{/setvar}}闭合标签自身不接受参数。
# — 保留空白
只对范围宏有效。 默认情况下,引擎会先对 body 做自动整理(剥掉首尾空白,再把第一个非空行的公共前导缩进从所有行里减掉)才交给 handler。加上 # 标志会跳过这一步,body 原样传入:
{{#if .verbose}}
这 4 空格缩进
以及首尾的换行都会原样保留。
{{/if}}这个标志也兼容老的 Handlebars 风格写法 {{#if …}} —— 跟 {{if …}} + 不自动 trim 等价。
加在非范围宏前面时,# 被接受但没有任何效果。
! ? ~ > — 预留未实现
| 标志位 | 预期含义 | 现状 |
|---|---|---|
! | 比同段文本里的其他宏先解析 | 仅解析 |
? | 比其他宏后解析 | 仅解析 |
~ | 标记为可重新求值 | 仅解析 |
> | 把 | 当输出过滤器的管道 | 仅解析(见下文 管道符) |
这些 token 解析器能识别,但运行时没有任何 hook 消费它们。{{if !.dead}} 里的 ! 是另一回事——那是 {{if}} 内部的条件取反,不是这里的标志位。
标志位之间、标志位和宏名之间都允许空白,多个可以组合:{{ #each ::list}} … {{/each}}。
| — 管道符(参数终止符)
管道符 \| 在宏参数里有特殊语义,即使没加 > 标志也一样。lexer 见到 \| 就会从「参数」模式切走,所以:
{{getvar::name|filter}}…会被解析为 getvar 宏 + 单个参数 name + 一个名为 filter 的「过滤器」标识符。过滤器执行链没接上,所以 filter 这个名字会被丢掉,整个宏的行为等价于 {{getvar::name}}。直接后果是:参数里出现字面 \| 会把那个参数截断——想要在参数里保留字面管道符,转义成 \|:
{{setvar::menu::sword \| shield \| bow}}管道符是预留语法
今天写 {{macro|uppercase}} 并不会把内容转大写——只是不报错地解析掉、丢掉过滤器名、把管道符前面的参数交给宏。要做字符串变换,写一个注册宏,或者用 regex 扩展。完整的管道过滤链留给将来的引擎版本。
斜杠命令里的管道宏 —— {{pipe}}、{{var::name}}
在斜杠命令闭包里(STscript —— /command1 | /command2 | … 链式调用、Quick Reply 等),斜杠命令作用域会再绑两个宏:
| 宏 | 作用域 | 含义 |
|---|---|---|
{{pipe}} | 斜杠命令闭包 | 上一条命令的输出 |
{{var::name}} | 斜杠命令闭包 | 用 /let / /var 创建的闭包内变量。和聊天变量(用 {{getvar::name}} 访问)不是一回事 |
这两个只在斜杠命令解析器里存在。不在 STscript 上下文里出现时,它们会原样输出。
STscript 里的 \| 是命令管道符,那是命令解析器的特性,不是宏引擎的。在单个宏的参数内部,\| 按上面管道符那一节的规则处理。
解析语义
引擎一次性扫每段文本,从左到右展开宏。
- 嵌套宏先解析,外层拿到的是已经求值完的字符串参数。除非该宏定义打开
delayArgResolution(当前只有{{if}}和{{each}})。 - 副作用宏(
{{setvar}}、{{addvar}}、{{incvar}}、{{decvar}}、{{deletevar}})立即生效,同一 pass 里后面的宏能读到新值。 - 逐楼层提取(见 逐楼层变量)发生在消息保存时,不是 prompt 组装时。
- 未知宏保留原始
{{...}}(嵌套参数仍然会展开)。不抛错也不告警。 - 参数数量 / 类型不符:预设
strictArgs: true时记一条 runtime warning 并保留原始宏文本;strictArgs: false时记 warning 但 handler 仍然跑。 - 结果规范化——每个 handler 的返回都会被规范化:
null/undefined→'',Date→ ISO 字符串,数组 / 对象 →JSON.stringify(...),其他 →String(...)。这就是{{loop_value}}对一个对象会输出 JSON 的原因。 - 后处理清扫——残留的
{{trim}}标记和零散的else哨兵字符会在后处理里被清掉。
自定义与插件宏
扩展可以注册自己的宏:
const ctx = Luker.getContext();
ctx.macros.register('myStatus', {
description: '返回插件状态字符串。',
category: 'utility',
handler: () => 'My plugin is active.',
});
ctx.macros.register('greet', {
description: '问候。',
unnamedArgs: [
{ name: 'name', type: 'string', description: '问候对象' },
],
handler: (mctx) => `Hello, ${mctx.unnamedArgs[0]}!`,
});注册之后 {{myStatus}} 和 {{greet::Bob}} 就能在所有支持宏的位置使用,包括世界书、预设、聊天消息。
完整的注册 API——参数类型、别名、范围宏、惰性解析、dynamicMacros 单次注入——在 Extension API · 宏与变量。
探索与调试
- 在任何解析宏的字段里输入
{{会触发自动补全。任何位置按 Ctrl+Space 也能召出来。 - 在 设置 → 自动补全设置 → 在所有宏字段中显示 里把全局补全打开。
- 运行
/? macros(或/? macro//? 4)打开 宏浏览器——一个可搜索的弹窗,列出所有注册的宏、分类、别名、参数、返回类型、示例、来源(核心 / 扩展 / 第三方)。 - 运行
/reroll-pick重置当前 chat 里所有{{pick}}的结果。可传一个种子(/reroll-pick mySeed)以重现。
宏在哪些字段里生效
凡是会被 prompt 管线吃进去的文本字段:
- 角色卡字段(description、personality、scenario、first message、alt greetings、对话示例、@ Depth Note、character prompts)
- 世界书条目正文、key、header
- 预设里的提示词条目
- 作者笔记
- 聊天消息(用户和 AI),带逐楼层变量提取
- 斜杠命令参数、Quick Reply 脚本
- 正则扩展的替换内容
- 宏自己的参数(嵌套)
那些只存字面文本、不会到 model 的字段——比如连接设置里的 API URL——不会解析宏。拿不准时用 {{date}} 试一下:如果显示成字面的 {{date}},那个字段就不跑宏。
常见范式
预设里的条件段
{{if .difficulty == "hard"}}
世界冷酷无情,失败不可逆。
{{else}}
世界宽容,死亡只是挫折,不是终点。
{{/if}}一个能扛住 swipe 和删除的计数器
在角色卡的 first_mes 或 alt greeting 里初始化变量:
{{setvar::turn::0}}在 AI 回复里(通过一条指示 AI 写这个的世界书条目),让 AI 自己 incr:
{{incvar::turn}}逐楼层提取会把字面量删掉、把 op 记下来,用户看到的是干净叙事、变量正常递增。删那条消息时,重播会把 op 撤掉。
对当前 chat 稳定的随机选择
{{pick::酒馆斗殴::一个安静的夜晚::不速之客}}掷一次后,对该 chat 的该位置就锁住了——这种「今天的氛围」式的随机就该在重渲染里不抖。
渲染一个结构化集合
# 进行中的任务
{{each::quests}}{{if {{loop_value::status}} == "active"}}
- {{loop_value::name}}
{{/if}}{{/each}}AI 用 {{setvar::quests::…}} 在回复里维护 quests 结构;上面那段写在世界书里,每轮 prompt 时按当前状态铺开。
根据 flag 切换的作者笔记
{{if $debug_verbose}}
[叙事声音:把推理过程讲得格外清楚]
{{/if}}从 Quick Reply 或斜杠命令切的全局 flag,成为一个可选的命令,只在需要时随 prompt 一起发出去。
参考
- 逐楼层变量 —— 楼层变量特性的完整说明。
- Extension API · 宏与变量 —— 在插件里注册自定义宏。
- 世界书基础 —— 大部分 prompt 侧宏的归宿。
- 预设系统 —— prompt 侧宏的另一处归宿。