实现输入框指令解析、高亮与补全。

让玩家与人设等占位在发送前可预览并正确展开。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-14 01:14:20 +08:00
co-authored by Cursor
parent 073d56cd85
commit 8a22cdc37c
6 changed files with 799 additions and 0 deletions
+98
View File
@@ -0,0 +1,98 @@
import { describe, expect, it, vi } from "vitest";
import {
expandAtDirectives,
expandIdentityDirectives,
listDirectiveCatalog,
} from "../src/directives/expand.js";
describe("expandAtDirectives", () => {
it("replaces @玩家 and @user with persona name", () => {
const r = expandAtDirectives("你好,@玩家;再见 @user。", {
persona: { name: "林晚" },
});
expect(r.text).toBe("你好,林晚;再见 林晚。");
expect(r.hits.map((h) => h.kind)).toEqual([
"persona_name",
"persona_name",
]);
});
it("falls back to 玩家 when no persona", () => {
const r = expandAtDirectives("我是@玩家", {});
expect(r.text).toBe("我是玩家");
});
it("replaces @人设 with description", () => {
const r = expandAtDirectives("人设:@人设", {
persona: { name: "林晚", description: "冷静的调查员" },
});
expect(r.text).toBe("人设:冷静的调查员");
});
it("compat {{user}}", () => {
const r = expandIdentityDirectives("对{{user}}说", {
persona: { name: "阿泽" },
});
expect(r.text).toBe("对阿泽说");
});
it("leaves unknown @token alone", () => {
const r = expandAtDirectives("写给@某人 的信", {
persona: { name: "林晚" },
});
expect(r.text).toBe("写给@某人 的信");
expect(r.changed).toBe(false);
});
it("does not treat @玩家X as @玩家", () => {
const r = expandAtDirectives("@玩家X来了", {
persona: { name: "林晚" },
});
expect(r.text).toBe("@玩家X来了");
});
it("expands @玩家 when followed by Chinese", () => {
const r = expandAtDirectives("且对@玩家没有因为", {
persona: { name: "林晚" },
});
expect(r.text).toBe("且对林晚没有因为");
});
it("rolls @rd100 with target", () => {
vi.spyOn(Math, "random").mockReturnValue(0.18); // → 19 on 1..100
const r = expandAtDirectives("检定 @rd100 30", {
persona: { name: "林晚" },
});
expect(r.text).toBe("检定 投掷结果为 19,目标 30(成功)");
vi.restoreAllMocks();
});
it("rolls @r3d10 without target", () => {
const seq = [0.3, 0.6, 0.1];
let i = 0;
vi.spyOn(Math, "random").mockImplementation(() => seq[i++] ?? 0);
const r = expandAtDirectives("伤害 @r3d10", {});
expect(r.text).toMatch(/^伤害 投掷结果为 \d\+\d\+\d = \d+(3d10)$/);
vi.restoreAllMocks();
});
it("identity expand skips dice", () => {
const r = expandIdentityDirectives("先 @rd100 50 再说", {
persona: { name: "林晚" },
});
expect(r.text).toBe("先 @rd100 50 再说");
});
it("lists catalog", () => {
expect(listDirectiveCatalog().some((x) => x.insert === "@玩家")).toBe(
true,
);
});
it("keeps @玩家 when the persona name is @玩家", () => {
const r = expandIdentityDirectives("清晨,@玩家推开门。对{{user}}点头。", {
persona: { name: "@玩家", description: "占位" },
});
expect(r.text).toBe("清晨,@玩家推开门。对@玩家点头。");
});
});