Skip to content

宏 ​

宏(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-_]*$——字母开头,后接字母、数字、底线或连字号。唯一的例外是注解宏 {{//}}。

参数 ​

参数之间用 :: 分隔:

text
{{setvar::hp::50}}
{{datetimeformat::YYYY-MM-DD HH:mm:ss}}
{{roll::3d6+4}}

单个 : 作为兜底也认({{roll: 1d20}}),但因为正文里经常出现单冒号,推荐用 ::。

逗号列表宏({{random}}、{{pick}})两种写法均可:

text
{{random::red::green::blue}}
{{random::red,green,blue}}

列表项里要写字面意义的逗号,转义为 \,。

嵌套宏 ​

宏可以嵌套。内层先解析,结果作为外层的参数:

text
{{setvar::greeting::Hello, {{user}}!}}
{{if {{getvar::showHeader}}}}# Header{{/if}}
{{each::{{getvar::roster}}}}- {{loop_value::name}}{{/each}}

嵌套深度没有硬上限,但嵌套太深通常是宏在做太多事——拆成中间变量。

范围宏 ​

部分宏({{if}}、{{each}}、{{trim}}、{{//}},以及大多数接受参数的自定义宏)支持「开 / 闭标签 + 中间内容」的写法。中间的内容会作为最后一个位置参数传给宏:

text
{{if .ready}}
角色已就绪。
{{/if}}

{{each::npcs}}
- {{loop_key}}: {{loop_value::hp}} HP
{{/each}}

预设情况下,范围宏的内容会被自动整理——首尾空白被剥掉,首个非空行的共同前导缩进会从所有行里减掉。要原样保留空白,在宏名前加 # 标志位:

text
{{#if .verbose}}
   这一段的缩进和首尾空白均会原样保留。
{{/if}}

转义 ​

要输出字面的双花括号,前面加反斜杠:

text
\{{not a macro}}

反斜杠在后处理阶段被去掉,原始的 {{...}} 字符串原样到达 AI。这是在 prompt 里教模型宏语法而不让示例真的被执行的官方写法。

老式标签 ​

一些非花括号的老式标签,会在宏解析前被自动重写:

老式现代等价
<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 局部变量,$ 永远指全局变量。两个作用域不共享命名。

变量表达式可以和控制流组合:

text
{{if .hp <= 0}}
你死了。
{{else}}
你还剩 {{.hp}} 点 HP。
{{/if}}

变量名允许底线和连字号,但最后一个字符必须是 word 字符:my-var 合法,my- 不合法(会和 -- 自减操作符冲突)。

默认值惰性求值

?? 和 || 只在变量缺失 / falsy 时才会求 fallback 表达式。这能让你跳过昂贵的默认值:{{.cachedSummary ?? {{summary}}}} 只在缓存未命中时才会调用 {{summary}}。

控制流 ​

条件 — {{if}} / {{else}} ​

text
{{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}} ​

text
{{each::collection}}
{{loop_key}}: {{loop_value}}
{{/each}}

collection 接受以下形式:

  1. 内嵌 JSON 字面量——{{each::["sword","shield"]}} 或 {{each::{"a":1,"b":2}}}。
  2. 变量名——{{each::npcs}} 读局部变量 npcs(找不到 fallback 到全局),并把字符串当 JSON 解析。
  3. 解析结果为集合的嵌套宏——{{each::{{getglobalvar::roster}}}}。

body 里:

宏含义
{{loop_key}}当前 key(数组时是字符串形式的索引)
{{loop_value}}当前值整体(对象会自动 JSON 字符串化)
{{loop_value::field}}用点号路径深入值,语义同 {{getvar}}

嵌套 {{each}} 会自然屏蔽外层的 loop_key / loop_value。对象的迭代顺序遵循 JavaScript 原生规则——字符串 key 走插入顺序,整数样 key 走升序。

空集合或不可迭代值渲染为空字符串。引擎没有对迭代次数加保护——body 里再次 {{each}} 自己负责防爆。

注解 — {{//}} ​

行内:

text
{{// 这一行被忽略}}

范围形式:

text
{{//}}
多行注解。
里面可以写 {{宏}} 和 \{转义\}——什么都不会运行。
{{///}}

注解解析为空字符串,内容原样吃掉。给 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)—删除
{{pushvar::name::value}}—把 value 推入 name 中存储的 JSON 数组;缺失时自动建为 []。支持点号路径(如 roster.alice.inv)。
{{popvar::name}}—从 name 的 JSON 数组弹出最后一个元素;空或缺失时为无操作。支持点号路径。

全局对应的有 setglobalvar / addglobalvar / incglobalvar / decglobalvar / deleteglobalvar。

addvar 是重载的:两侧均为数值字符串时做数值加;现有值是 JSON 数组时 push;否则按字符串拼接。

用 {{noop}} 锚住空白

拼接、范围宏 body、+= " text" 等场景下,前导/尾部空白经常被引擎的自动整理或参数 trim 吃掉。在要保留的空白前后插一个 {{noop}}(解析为空字符串)就能锚住它。例如 {{addvar::story::{{noop}} 这是新的一段。}}。

结构化值的点号路径 ​

变量可以存任何 JSON 可序列化的值。当变量存的是 JSON 字符串化的对象 / 数组时,点号路径直接生效:

text
{{setvar::npcs::{"alice":{"hp":40},"bob":{"hp":30}}}}
{{getvar::npcs.alice.hp}}    → 40
{{getvar::npcs.alice}}        → {"hp":40}
{{getvar::list.0}}            → list 的第一个元素

写入也支持点号路径。 {{setvar}}、{{deletevar}}、{{pushvar}}、{{popvar}} 均接受点号路径名,因此可以在多轮叙事中维护同一个结构化变量,不必重写整盘。

text
{{setvar::roster.alice.hp::50}}              <!-- 引入 Alice,初始 50 HP -->
{{setvar::roster.alice.mood::cautious}}      <!-- 设置她的情绪 -->
{{pushvar::roster.alice.inventory::dagger}}  <!-- 给她一把匕首 -->
{{pushvar::roster.alice.inventory::potion}}
{{incvar::roster.alice.hp}}                  <!-- 回 1 HP -->
{{popvar::roster.alice.inventory}}           <!-- 她使用了一件物品 -->
{{deletevar::roster.alice.mood}}             <!-- 情绪重置 -->

中间节点按需自动创建。纯数字路径段产生数组下标(例如 {{setvar::log.0::一位老人到来}} 写入 log 的第 0 个元素)。

对非 JSON 值用点号路径时,会 fallback 到字面键查找——名字真的就是 a.b 的变量也能读到。

这套和 {{each}} 配合很自然:一个 NPC 名册、一份背包字典、一本任务日志均可以塞进单一变量,prompt 组装时再渲染到 prompt 或世界书条目里:

text
{{each::npcs}}
- {{loop_key}}: {{loop_value::hp}} HP
{{/each}}

单键读写 —— {{getvarkey}} / {{setvarkey}} ​

实现来源说明

本节的按键寻址宏族来自 SillyTavern 原版;点号路径则是 Luker 的扩展,单个宏即可访问任意深度。为兼容面向原版 SillyTavern 编写的内容,两种写法均可用。

SillyTavern 的按键寻址宏族可以直接使用:

宏别名作用域
{{getvarkey::name::key}}getvarindex局部
{{setvarkey::name:🔑:value}}setvarindex局部
{{getglobalvarkey::name::key}}getglobalvarindex全局
{{setglobalvarkey::name:🔑:value}}setglobalvarindex全局

它们读写 JSON 字符串化变量里的单个键(或数组下标)。键看起来是数字时会建成数组,否则建成对象:

text
{{setvarkey::roster::alice::40}}[{{getvarkey::roster::alice}}]  → [40]
{{setvarkey::log::0::first}}{{setvarkey::log::1::second}}        → 键是数字,所以 `log` 是数组
[{{getvarkey::log::1}}]                                          → [second]

新写的内容建议优先用上面的点号路径:一个宏就能抵达任意深度,写入也用同一套语法。varkey 只能处理一层,要写到 roster.alice.hp 得自己拼「读—改—写」。

它有一件点号路径做不到的事:键会被原样取用,所以当键本身含点号时只能用它。

text
{{setvarkey::metrics::p95.latency::120}}   → 设置字面键 "p95.latency"
{{getvarkey::metrics::p95.latency}}        → 120
{{getvar::metrics.p95.latency}}            → 空:点号形式会在每个点处切分

逐楼层变量 ​

原生 SillyTavern 里,副作用宏 {{setvar::hp::50}} 只在 prompt 范本 里(预设、世界书、首楼)才会运行。AI 在回复里写同样的字面量什么都不会发生,还会原样显示出来污染叙事。

Luker 用逐楼层变量提取解决这个问题。一条消息(AI 回复、用户消息、swipe、续写)保存时,Luker 会:

  1. 扫描文本里的 {{setvar}}、{{addvar}}、{{incvar}}、{{decvar}}、{{deletevar}}。
  2. 把里面嵌套的展示宏({{user}}、{{getvar::other}}、{{time}}……)对当前状态求值。
  3. 把 op 立刻 apply 到 chat_metadata.variables。
  4. 在 message.extra.var_ops 上追加一条结构化记录。
  5. 从可见文本里把字面量删掉。

当你删消息、切 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保存的聊天摘要

第三方扩展可以注册更多宏,宏浏览器会标出每个宏的来源。

标志位 ​

标志位是放在开头 {{ 和宏名之间的单字符,用来改变宏的解析或求值行为。

/ — 闭合标记 ​

标记一个标签是范围宏的闭合半边。和同名的开标签配对,把两者之间的内容作为宏的最后一个位置参数传入:

text
{{if .ready}} ...body... {{/if}}
{{each::npcs}} ...body... {{/each}}
{{setvar::longText}} ...body... {{/setvar}}

闭合标签自身不接受参数。

# — 保留空白 ​

只对范围宏有效。 默认情况下,引擎会先对 body 做自动整理(剥掉首尾空白,再把第一个非空行的公共前导缩进从所有行里减掉)才交给 handler。加上 # 标志会跳过这一步,body 原样传入:

text
{{#if .verbose}}
    这 4 空格缩进
    以及首尾的换行均会原样保留。
{{/if}}

这个标志也兼容老的 Handlebars 风格写法 {{#if …}} —— 跟 {{if …}} + 不自动 trim 等价。

加在非范围宏前面时,# 被接受但没有任何效果。

! ? ~ > — 预留未实现 ​

标志位预期含义现状
!比同段文本里的其他宏先解析仅解析
?比其他宏后解析仅解析
~标记为可重新求值仅解析
>把 | 当输出过滤器的管道无效果(见下文 管道符)

这些 token 解析器能识别,但运行时没有任何 hook 消费它们。{{if !.dead}} 里的 ! 是另一回事——那是 {{if}} 内部的条件取反,不是这里的标志位。

标志位之间、标志位和宏名之间均允许空白,多个可以组合:{{ #each ::list}} … {{/each}}。

| — 管道符(普通字符) ​

与 SillyTavern 行为差异的由来

原版 SillyTavern 将管道符解析为参数终止符,并支持 {{macro|uppercase}} 一类输出修饰器。该实现在上游被暂时禁用(issue #5618),Luker 遵循上游的词法分析器,因此本节描述的行为与上游保持一致。

管道符在宏参数里没有特殊含义。lexer 不把 | 当参数终止符,所以它是普通内容,会原样传给宏:

text
{{setvar::menu::sword | shield | bow}}   → 存入 "sword | shield | bow"
{{getvar::name|filter}}                  → 读取名为 `name|filter` 的变量

管道符没有转义写法;反斜杠会被原样存入:

text
{{setvar::k::a \| b}}   → 存入 "a \| b"

输出修饰器不可用

{{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 哨兵字符会在后处理里被清掉。

自定义与插件宏 ​

扩展可以注册自己的宏:

js
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}},那个字段就不跑宏。

常见范式 ​

预设里的条件段 ​

text
{{if .difficulty == "hard"}}
世界冷酷无情,失败不可逆。
{{else}}
世界宽容,死亡只是挫折,不是终点。
{{/if}}

一个能扛住 swipe 和删除的计数器 ​

在角色卡的 first_mes 或 alt greeting 里初始化变量:

text
{{setvar::turn::0}}

在 AI 回复里(通过一条指示 AI 写这个的世界书条目),让 AI 自己 incr:

text
{{incvar::turn}}

逐楼层提取会把字面量删掉、把 op 记下来,用户看到的是干净叙事、变量正常递增。删那条消息时,重播会把 op 撤掉。

对当前 chat 稳定的随机选择 ​

text
{{pick::酒馆斗殴::一个安静的夜晚::不速之客}}

掷一次后,对该 chat 的该位置就锁住了——这种「今天的氛围」式的随机就该在重渲染里不抖。

渲染一个结构化集合 ​

text
# 进行中的任务
{{each::quests}}{{if {{loop_value::status}} == "active"}}
- {{loop_value::name}}
{{/if}}{{/each}}

AI 用 {{setvar::quests::…}} 在回复里维护 quests 结构;上面那段写在世界书里,prompt 组装时按当前状态铺开。

根据 flag 切换的作者笔记 ​

text
{{if $debug_verbose}}
[叙事声音:把推理过程讲得格外清楚]
{{/if}}

从 Quick Reply 或斜杠命令切的全局 flag,成为一个可选的命令,只在需要时随 prompt 一起发出去。

参考 ​

基于 SillyTavern 构建