npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

parsecard

v2.0.3

Published

SillyTavern 角色卡、世界书、正则脚本的解析与序列化工具库(面向对象,TypeScript)

Readme

ParseCard v2.0

SillyTavern 角色卡、世界书、正则脚本、OpenAI 预设的解析与序列化工具库

TypeScript 实现,面向对象设计,零外部依赖,核心层同时支持 Node.js 和浏览器环境。

功能特性

  • 🎭 角色卡 — 面向对象的CharacterCard 类,支持 V2/V3 格式自动兼容,getter/setter 自动同步顶层与data冗余字段
  • 📖 世界书WorldBook + WorldBookEntry 类,独立/内嵌两种格式自动检测、统一解析、互转
  • 🔧 正则脚本RegexScript 类,支持独立文件和角色卡内嵌
  • 🧩 OpenAI 预设OpenAIPreset 类,支持 SillyTavern Chat Completion 预设的 prompts 增删改查、启用状态和排序管理
  • 🖼️ PNG 读写 — 从 PNG tEXt chunk 中读写角色卡数据,支持 ccv3 / chara 兼容和无底图生成
  • 📂 文件 I/O — 一行代码加载 PNG / JSON 文件,同步 + 异步双版本
  • 🔗 链式调用card.bindWorldBook(wb).addRegexScript(script)
  • 🛡️ 鲁棒性 — 字段缺失、类型错误、null 值全面容错,并尽量保留未知字段以支持 round-trip
  • 🌐 跨环境 — 核心层 parsecard 不依赖 Node.js API,浏览器可直接使用;文件 I/O 通过 parsecard/node 按需引入

安装

npm install parsecard

快速开始

角色卡 — 基本用法

import { CharacterCard } from 'parsecard';

// 从 JSON 对象解析(自动兼容 V2 / V3)
const card = CharacterCard.fromJSON(jsonData);
console.log(card.name);           // 角色名
console.log(card.description);// 角色描述
console.log(card.firstMes);       // 第一条消息

// 直接属性赋值(toJSON 时自动同步顶层冗余字段)
card.name = '新名字';
card.description = '新描述';
card.tags = ['标签1', '标签2'];

// 序列化(输出 SillyTavern 兼容格式)
const output = card.toJSON();

角色卡 — 从PNG 读写

import { CharacterCard } from 'parsecard';

// 从 PNG 二进制数据解析
const card = CharacterCard.fromPNG(pngBuffer);

// 写入 PNG(传入底图,或传null 自动生成 1×1 透明 PNG)
const pngData = card.toPNG(pngBuffer);
const pngFromScratch = card.toPNG(); // 无底图

角色卡 — Node.js 文件操作

import {
    loadCharacterCard,
    loadCharacterCardAsync,
    saveCharacterCard,
    saveCharacterCardAsync,
} from 'parsecard/node';

// 一行加载(自动识别 PNG / JSON 格式)
const card = loadCharacterCard('角色卡.png');

// 修改
card.name = '修改后的角色';
card.fav = true;

// 保存(根据扩展名自动选择格式)
saveCharacterCard(card, 'output.png');
saveCharacterCard(card, 'output.json');

// 异步版本
const card2 = await loadCharacterCardAsync('角色卡.json');
await saveCharacterCardAsync(card2, 'output.png');

世界书

import { WorldBook, WorldBookEntry, WorldBookEntryPosition, TriggerType } from 'parsecard';

// 自动检测格式解析(独立 / 内嵌)
const wb = WorldBook.fromJSON(jsonData);

// 也可明确指定格式
const wb1 = WorldBook.fromStandaloneJSON(rawStandalone);
const wb2 = WorldBook.fromEmbeddedJSON(rawEmbedded);

// 条目操作
const entry = wb.addEntry({ comment: '新条目', content: '内容', keys: ['关键词'] });
wb.removeEntry(entry.uid);

// 查询
const constants = wb.findConstant();     // 蓝灯条目
const depthEntries = wb.findDepth();                                     // 深度插入
const beforeChar = wb.findByPosition(WorldBookEntryPosition.BEFORE_CHAR); // 按位置
const custom = wb.findEntries(e => e.probability< 50);                  // 自定义条件

// 条目触发类型管理
entry.triggerType = TriggerType.CONSTANT;// 设为蓝灯
entry.disable();                           // 禁用
entry.enable();                            // 启用

// 合并两个世界书
const merged = wb.merge(otherBook, '合并后的名称');

// 格式互转
const standalone = wb.toStandaloneJSON();   // 独立格式
const embedded = wb.toEmbeddedJSON();       // 内嵌格式

// 人类可读描述
console.log(wb.describe());

正则脚本

import { RegexScript, RegexPlacement, SubstituteRegex } from 'parsecard';

// 解析
const script = RegexScript.fromJSON(jsonData);
console.log(script.scriptName);
console.log(script.findRegex);

// 创建
const newScript = RegexScript.create({
    scriptName: '我的正则',
    findRegex: '\\*\\*(.*?)\\*\\*',
    replaceString: '<b>$1</b>',
    placement: [RegexPlacement.AI_OUTPUT],
    markdownOnly: true,
});

// 启用/禁用
newScript.disable();
newScript.enable();

// 克隆(生成新ID)
const cloned = newScript.clone();

// 序列化
const output = newScript.toJSON();

OpenAI / Chat Completion 预设

import { OpenAIPreset } from 'parsecard';
import { loadOpenAIPreset, saveOpenAIPreset } from 'parsecard/node';

// 创建一个 SillyTavern 可直接使用的默认预设
const preset = OpenAIPreset.create({ name: '我的预设' });

// 内置条目包括 Main Prompt、World Info (before)、Char Description 等
preset.updatePrompt('main', {
    content: 'Write {{char}}\'s next reply in character.',
});

// 添加、移动、禁用自定义提示词
preset.addPrompt(
    { identifier: 'style', name: 'Style Guide', role: 'system', content: 'Keep replies concise.' },
    { after: 'main', enabled: true },
);
preset.movePrompt('style', 1);
preset.setPromptEnabled('enhanceDefinitions', false);

// 常用外层参数可以直接读写;其它参数用通用 getter/setter 保留
preset.temperature = 0.8;
preset.maxTokens = 600;
preset.setSetting('custom_provider_field', { keep: true });

saveOpenAIPreset(preset, '我的预设.json');

// 读取已有导出的 SillyTavern 预设,不会自动改写原结构
const imported = loadOpenAIPreset('导出的预设.json');
imported.ensureDefaultPrompts();      // 需要时补齐 ST 默认内置条目
imported.resetDefaultPromptOrder();   // 需要时恢复内置条目的默认顺序

绑定操作(链式调用)

import { CharacterCard, WorldBook, RegexScript } from 'parsecard';

const card = CharacterCard.create({ name: '新角色' });
const wb = WorldBook.fromJSON(rawWorldBook);
const script = RegexScript.fromJSON(rawScript);

// 链式绑定
card
    .bindWorldBook(wb)
    .addRegexScript(script);

// 访问
console.log(card.characterBook?.name);  // 世界书名称
console.log(card.regexScripts.length);  // 正则脚本数量

// 解绑
const detachedBook = card.unbindWorldBook();
const detachedScripts = card.unbindRegexScripts();

Node.js 文件操作汇总

import {
    // 角色卡
    loadCharacterCard, loadCharacterCardAsync,
    saveCharacterCard, saveCharacterCardAsync,
    // 世界书
    loadWorldBook, loadWorldBookAsync,
    saveWorldBook, saveWorldBookAsync,
    // 正则脚本
    loadRegexScript, loadRegexScriptAsync,
    saveRegexScript, saveRegexScriptAsync,
    // OpenAI / Chat Completion 预设
    loadOpenAIPreset, loadOpenAIPresetAsync,
    saveOpenAIPreset, saveOpenAIPresetAsync,
} from 'parsecard/node';

导入方式

// 核心功能(浏览器 + Node.js 通用)
import { CharacterCard, WorldBook, RegexScript, OpenAIPreset } from 'parsecard';

// 文件 I/O(仅 Node.js)
import { loadCharacterCard, saveCharacterCard, loadOpenAIPreset, saveOpenAIPreset } from 'parsecard/node';

时间戳兼容

CharacterCard.fromJSON()normalizeTimestamp() 会将以下 create_date 输入规范化为 ISO 8601 UTC:

  • ISO 8601 和其他 JavaScript 可解析的日期字符串。
  • 10 位 Unix 秒数字或数字字符串。
  • 13 位 Unix 毫秒数字或数字字符串。
  • 旧版使用的 "{10 位 Unix 秒}Z" 格式。

无法解析的输入保持向后兼容,会回退为当前时间。CharacterCard.toJSON({ create_date }) 也接受字符串、Unix 秒或 Unix 毫秒。

API 概览

CharacterCard

| 方法 / 属性 | 说明 | |---|---| | CharacterCard.fromJSON(raw) | 从 JSON 对象解析(V2/V3 自动兼容) | | CharacterCard.fromPNG(buffer) | 从 PNG 二进制数据解析 | | CharacterCard.create(overrides?) | 创建空白角色卡 | | .name, .description, .personality, .scenario | 基本属性(getter/setter) | | .firstMes, .mesExample, .creatorNotes | 消息与注释 | | .systemPrompt, .postHistoryInstructions | 提示词 | | .tags, .creator, .characterVersion | 元数据 | | .alternateGreetings | 替代问候语 | | .talkativeness, .fav, .world, .depthPrompt | 扩展属性 | | .characterBook | 获取内嵌世界书(WorldBook \| null) | | .bindWorldBook(wb) | 绑定世界书 | | .unbindWorldBook() | 解绑世界书 | | .regexScripts | 获取正则脚本列表 | | .addRegexScript(script) | 添加正则脚本 | | .removeRegexScript(id) | 移除正则脚本 | | .bindRegexScripts(scripts) | 批量绑定正则脚本 | | .unbindRegexScripts() | 解绑所有正则脚本 | | .toJSON(options?) | 序列化为 SillyTavern 兼容 JSON| | .toPNG(sourceImage?, options?) | 写入 PNG | | .clone() | 深拷贝 |

WorldBook

| 方法 / 属性 | 说明 | |---|---| | WorldBook.fromJSON(raw) | 自动检测格式解析 | | WorldBook.fromStandaloneJSON(raw) | 解析独立格式 | | WorldBook.fromEmbeddedJSON(raw) | 解析内嵌格式 | | WorldBook.create(name?) | 创建空白世界书 | | .name, .entries, .length | 基本属性 | | .addEntry(overrides?) | 添加条目 | | .removeEntry(uid) | 移除条目 | | .getEntry(uid) | 按uid 获取 | | .findEntries(predicate) | 自定义查询 | | .findConstant() | 蓝灯条目 | | .findVectorized() | 向量化条目 | | .findDepth() | 深度插入条目 | | .findByPosition(pos) | 按位置查询 | | .findByTriggerType(type) | 按触发类型 | | .reindex() | 重编uid/displayIndex | | .merge(other, name?) | 合并世界书 | | .toStandaloneJSON() | 导出独立格式 | | .toEmbeddedJSON() | 导出内嵌格式 | | .describe() | 人类可读描述 | | .clone() | 深拷贝 |

WorldBookEntry

| 方法 / 属性 | 说明 | |---|---| | .uid, .keys, .secondaryKeys, .comment, .content | 基本属性 | | .constant, .selective, .enabled, .position, .depth, .role | 行为属性 | | .triggerType | 触发类型(getter/setter,自动管理 constant/vectorized) | | .isEnabled | 是否已启用 | | .enable() / .disable() | 启用/禁用 | | .describe() | 人类可读描述 | | .clone() | 深拷贝 |

RegexScript

| 方法 / 属性 | 说明 | |---|---| | RegexScript.fromJSON(raw) | 从 JSON 解析 | | RegexScript.create(overrides?) | 创建新脚本 | | .id, .scriptName, .findRegex, .replaceString | 基本属性 | | .placement, .disabled, .markdownOnly, .promptOnly | 行为属性 | | .isEnabled | 是否已启用 | | .enable() / .disable() | 启用/禁用 | | .toJSON() | 序列化| | .clone() | 深拷贝(生成新 ID) |

OpenAIPreset

| 方法 / 属性 | 说明 | |---|---| | OpenAIPreset.fromJSON(raw) | 从 SillyTavern OpenAI / Chat Completion 预设 JSON 解析 | | OpenAIPreset.create(overrides?) | 创建带默认内置 prompts 和默认 prompt_order 的可用预设 | | .name | 预设名称(部分 SillyTavern 文件可能没有该字段) | | .prompts, .promptOrder | prompts 定义列表和 prompt_order 分组 | | .defaultCharacterId | 默认操作的 prompt_order 分组,优先使用 100001 | | .getPrompt(identifier) | 获取 prompt | | .hasPrompt(identifier) | 判断 prompt 是否存在 | | .searchPrompts(query) | 按 identifier、名称或内容搜索 | | .listPromptEntries(options?) | 按 prompt_order 列出条目,可包含未排序 prompts | | .addPrompt(prompt, options?) | 新增 prompt,并可插入到指定位置 | | .updatePrompt(identifier, updates) | 修改 prompt;修改 identifier 时会同步 order 引用 | | .removePrompt(identifier, removeFromOrder?) | 删除 prompt,并默认移除 order 引用 | | .clonePrompt(identifier, overrides?, options?) | 复制 prompt 并生成新 identifier | | .isPromptEnabled(identifier, characterId?) | 查询启用状态 | | .setPromptEnabled(identifier, enabled, characterId?) | 设置启用状态 | | .movePrompt(identifier, index, characterId?) | 调整排序 | | .removePromptFromOrder(identifier, characterId?) | 仅移除排序引用,不删除 prompt 本体 | | .ensureDefaultPrompts(characterId?) | 补齐 SillyTavern 默认内置 prompts,不覆盖已有内容 | | .resetDefaultPromptOrder(characterId?) | 恢复内置 prompts 默认顺序,自定义条目追加在后 | | .temperature, .topP, .maxContext, .maxTokens | 常用外层参数 getter/setter | | .reasoningEffort, .showThoughts | 推理相关外层参数 getter/setter | | .getSetting(key) / .setSetting(key, value) | 通用外层参数读写;传 undefined 会删除字段 | | .toJSON() | 导出 SillyTavern 兼容 JSON | | .clone() | 深拷贝 |

枚举常量

世界书插入位置 WorldBookEntryPosition

| 值 | 常量 | 说明 | |----|------|------| | 0 | BEFORE_CHAR | 角色定义之前 | | 1 | AFTER_CHAR | 角色定义之后 | | 2 | BEFORE_AUTHOR_NOTE | 作者注释之前 | | 3 | AFTER_AUTHOR_NOTE | 作者注释之后 | | 4 | AT_DEPTH | 在深度插入 | | 5 | BEFORE_EXAMPLE_MESSAGES | 示例消息前 | | 6 | AFTER_EXAMPLE_MESSAGES | 示例消息后 | | 7 | OUTLET |锚点 |

选择逻辑 SelectiveLogic

| 值 | 常量 | 说明 | |----|------|------| | 0 | AND_ANY | 与任意 | | 1 | NOT_ALL | 非所有 | | 2 | NOT_ANY | 非任何 | | 3 | AND_ALL | 与所有 |

角色身份 EntryRole

| 值 | 常量 | 说明 | |----|------|------| | 0 | SYSTEM | 系统 | | 1 | USER | 用户 | | 2 | ASSISTANT | AI|

正则作用范围 RegexPlacement

| 值 | 常量 | 说明 | |----|------|------| | 1 | USER_INPUT | 用户输入 | | 2 | AI_OUTPUT | AI 输出 | | 3 | SLASH_COMMAND | 快捷命令 | | 5 | WORLD_INFO | 世界信息 | | 6 | REASONING | 推理 |

正则宏替换 SubstituteRegex

| 值 | 常量 | 说明 | |----|------|------| | 0 | NONE | 不替换 | | 1 | RAW | 替换(原始) | | 2 | ESCAPED | 替换(转义) |

触发器EntryTrigger

| 值 | 说明 | |----|------| | normal | 正常生成 | | continue | 继续生成 | | impersonate | AI 帮答| | swipe | 滑动 | | regenerate | 重新生成 | | quiet | 静默 |

触发类型 TriggerType

| 值 | 说明 | |----|------| | keyword | 关键词触发(绿灯) | | constant | 常驻触发(蓝灯) | | vectorized | 向量化触发 |

错误处理

import { ParseCardError, InvalidFormatError, PNGError, FileIOError } from 'parsecard';

try {
    const card = CharacterCard.fromPNG(brokenBuffer);
} catch (e) {
    if (e instanceof PNGError) {
        // PNG 格式问题
    } else if (e instanceof InvalidFormatError) {
        // JSON 结构问题
    }
}

| 错误类 | code | 说明 | |--------|------|------| | ParseCardError | — | 基础错误类 | | InvalidFormatError | INVALID_FORMAT | JSON 格式/结构无效 | | PNGError | PNG_ERROR | PNG 文件格式错误 | | FileIOError | FILE_IO_ERROR | 文件读写失败(仅 parsecard/node) |

数据模型

世界书两种格式差异

本库内部使用统一格式,解析时自动处理两种 SillyTavern 格式的差异:

| 差异项 | 独立世界书 | 内嵌 character_book | |--------|-----------|-------------------| | entries类型 | Object {"0":{}} | Array [{}] | | ID 字段 | uid | id | | 主关键词 | key | keys | | 次关键词 | keysecondary | secondary_keys | | 插入顺序 | order | insertion_order | | 启用/禁用 | disable(反向) | enabled(正向) | | 扩展字段 | 平铺在条目上 | 包裹在 extensions 中 | | 命名风格 | camelCase | snake_case |

v1.0 → v2.0 迁移

v2.0 是破坏性重构,主要变化:

| v1.0 | v2.0 | |------|------| | parseCharacterCard(raw) | CharacterCard.fromJSON(raw) | | serializeCharacterCard(card) | card.toJSON() | | readCharacterCardFromPNG(buf) | CharacterCard.fromPNG(buf) | | writeCharacterCardToPNG(buf, card) | card.toPNG(buf) | | card.data.name | card.name | | card.data.first_mes | card.firstMes | | card.data.extensions.fav | card.fav | | setName(card, '名字') | card.name = '名字' | | bindWorldBook(card, wb) | card.bindWorldBook(wb) | | parseWorldBook(raw) | WorldBook.fromJSON(raw) | | parseRegexScript(raw) | RegexScript.fromJSON(raw) | | addEntry(wb, overrides) | wb.addEntry(overrides) | | findConstantEntries(wb) | wb.findConstant() | | describeWorldBook(wb) | wb.describe() | | JavaScript + JSDoc | TypeScript (strict) |

License

Apache License 2.0