Files
ST-Amily2-Chat-Optimisation/SL/bus/GUIDE.md

9.7 KiB
Raw Blame History

Amily2Bus 开发者实战指南

本文档面向 Amily2 扩展的维护者与协作开发者,介绍如何在实际业务中使用总线系统。 API 参考请查阅同目录下的 README.md


一、总线是什么?为什么用它?

Amily2Bus 是一个 服务注册与发现 系统。它解决的核心问题:

  • 解耦循环依赖 — 模块之间不再需要互相 import只需通过总线 query() 按名字查找
  • 身份隔离 — 每个插件注册后拿到专属上下文Capability Token日志自动标注来源文件存储自动隔离
  • 可选依赖 — 查询不到服务不会崩溃,只返回 null,适合渐进式集成

一句话理解register() = 我是谁,expose() = 我能做什么,query() = 我要找谁帮忙。


二、注册一个新服务3 步)

Step 1注册身份

// 在你的模块顶层(文件加载时执行)
let _ctx = null;

if (window.Amily2Bus) {
    try {
        _ctx = window.Amily2Bus.register('MyService');
        _ctx.log('Init', 'info', 'MyService 已上线。');
    } catch (e) {
        console.warn('[MyService] Bus 注册失败(可能是热重载导致重复注册):', e);
    }
}

注意:每个名字只能注册一次(严格锁)。热重载时会抛异常,用 try-catch 包住即可,页面刷新后会重置。

Step 2暴露能力

// 把你希望其他模块能调用的函数暴露出去
_ctx.expose({
    doSomething,           // 暴露已有函数
    getStatus: () => 'ok', // 也可以内联
});

暴露后的对象会被 Object.freeze(),外部无法篡改。

Step 3完成

其他模块现在可以通过 window.Amily2Bus.query('MyService') 找到你暴露的方法了。


三、调用其他服务

const superMemory = window.Amily2Bus.query('SuperMemory');
if (superMemory) {
    await superMemory.awaitSync();
}

关键原则:总是做 null 检查。服务可能未加载、未注册、或被禁用。

项目中已注册的服务一览

服务名 用途 主要暴露方法
NccsApi NCCS 网络通道 call(messages, options), getSettings()
MessagePipeline 消息处理管线 execute(pipelineCtx)
SuperMemory 超级记忆系统 initialize(), forceSyncAll(), awaitSync(), pushUpdate(), purge()
TableSystem 表格系统 processMessageUpdate(), fillWithSecondaryApi(), generateTableContent(), renderTables()
TavernHelper ST 操作封装 25+ 方法(聊天、世界书、角色卡等)
LoreService 世界书读写锁 withLoreLock(), loadBook(), ensureBook(), saveBook()
Config 配置管理 get(), set(), getSettings(), migrate()
ApiProfiles API 配置文件管理 Profile CRUD + 密钥管理
ApiKeyStore API 密钥安全存储 getKey(), setKey()
PUBLIC 系统元信息 getAvailableModules(), getRegisteredPlugins(), ping()

使用 window.Amily2Bus.query('PUBLIC').getAvailableModules() 可在控制台实时查看所有已暴露服务。


四、使用上下文的三大能力

注册后拿到的 ctx 对象提供三种开箱即用的能力:

4.1 日志ctx.log

ctx.log('ModuleName', 'info', '这是一条日志');
// 输出: [14:32:01] [MyService::ModuleName] [INFO]: 这是一条日志

级别:debug / info / warn / error

调试时可在控制台动态开启某个服务的 debug 级别:

window.Amily2Bus.Logger.setLevel('MyService', 'all');

4.2 文件存储ctx.file

基于 IndexedDB 的虚拟文件系统,按服务名自动隔离。

await ctx.file.write('cache/data.json', { key: 'value' });
const data = await ctx.file.read('cache/data.json');
const files = await ctx.file.list();        // 列出本服务所有文件
await ctx.file.delete('cache/data.json');
await ctx.file.clearAll();                  // 清空本服务所有文件

路径禁止使用 ..,系统会做安全校验。

4.3 网络请求ctx.model

统一的 AI 模型调用接口,支持直连和 ST 预设两种模式。

const { Options } = ctx.model;

// 直连模式
const opt = Options.builder()
    .setMode('direct')
    .setApiUrl('https://api.example.com/v1')
    .setApiKey('sk-...')
    .setModel('claude-sonnet-4-20250514')
    .setMaxTokens(4096)
    .setTemperature(0.7)
    .setFakeStream(true)   // 防 CloudFlare 524 超时
    .build();

const reply = await ctx.model.call(messages, opt);

// ST 预设模式
const presetOpt = Options.builder()
    .setMode('preset')
    .setPresetName('MyProfile')
    .build();

const reply2 = await ctx.model.call(messages, presetOpt);

为什么用 ctx.model 而不是直接 fetch

  • 自动处理 FakeStream 防超时
  • 自动处理 ST 后端代理路由
  • 日志自动关联到你的服务名
  • 统一的错误处理与响应解析

五、常见模式与最佳实践

模式 1可选依赖推荐

// 好 — 查不到就跳过,不会崩溃
const memory = window.Amily2Bus.query('SuperMemory');
if (memory) {
    await memory.pushUpdate(charId, data);
}

// 坏 — 如果 SuperMemory 没注册就直接报错
const memory = window.Amily2Bus.query('SuperMemory');
await memory.pushUpdate(charId, data); // TypeError: Cannot read property 'pushUpdate' of null

模式 2在 expose 中只暴露纯函数

// 好 — 暴露的是明确的功能入口
ctx.expose({
    processMessageUpdate,
    fillWithSecondaryApi,
});

// 坏 — 不要暴露整个类实例或内部状态
ctx.expose({
    instance: this,          // 泄露内部状态
    _privateHelper: helper,  // 私有方法不该暴露
});

模式 3热重载安全

开发中 SillyTavern 扩展可能被热重载,导致同名重复注册。始终用 try-catch

let _ctx = null;
if (window.Amily2Bus) {
    try {
        _ctx = window.Amily2Bus.register('MyService');
        _ctx.expose({ ... });
    } catch (e) {
        // 热重载时会走到这里,不影响功能
        console.warn('[MyService] 重复注册,跳过:', e.message);
    }
}

模式 4跨服务协作实际例子

消息管线中,super-memory-sync 阶段需要等待 SuperMemory 同步完成:

// core/pipeline/stages/super-memory-sync.js
async function execute(pipelineCtx) {
    const sm = window.Amily2Bus.query('SuperMemory');
    if (!sm) return; // SuperMemory 未加载,跳过此阶段

    await sm.awaitSync();
    // 继续管线后续逻辑...
}

表格系统更新后,通知 SuperMemory 同步变更:

// core/table-system/manager.js
const sm = window.Amily2Bus.query('SuperMemory');
if (sm?.pushUpdate) {
    await sm.pushUpdate(characterId, updatedData);
}

六、调试技巧

控制台快速检查

// 查看所有已注册的服务
window.Amily2Bus.query('PUBLIC').getRegisteredPlugins()

// 查看所有暴露了公共接口的服务
window.Amily2Bus.query('PUBLIC').getAvailableModules()

// 测试某个服务是否在线
window.Amily2Bus.query('NccsApi')  // 返回对象则在线null 则未注册

// 开启某服务的全部日志
window.Amily2Bus.Logger.setLevel('TableSystem', 'all')

// 系统心跳
window.Amily2Bus.query('PUBLIC').ping()  // => 'pong'

日志级别控制

日志使用位掩码,可按需组合:

级别 说明
debug 0x1 调试信息(生产环境默认关闭)
info 0x2 一般信息
warn 0x4 警告
error 0x8 错误
all 0xF 全部开启
// 只看 warn + error
window.Amily2Bus.Logger.setLevel('MyService', 0x4 | 0x8);
// 或用字符串
window.Amily2Bus.Logger.setLevel('MyService', 'warn');

七、添加新功能模块的完整流程

假设你要新增一个「自动摘要」功能模块:

1. 创建文件 core/auto-summary/AutoSummaryService.js
2. 在文件中注册总线身份
3. 实现核心逻辑
4. 暴露需要被其他模块调用的方法
5. 在 index.js 中 import 该文件(确保它被加载)
// core/auto-summary/AutoSummaryService.js
import { callNccsAI } from '../api/NccsApi.js';

let _ctx = null;

export async function summarize(text, maxLength = 200) {
    const messages = [
        { role: 'system', content: `请将以下内容压缩到${maxLength}字以内。` },
        { role: 'user', content: text }
    ];
    return await callNccsAI(messages);
}

// --- 总线注册 ---
if (window.Amily2Bus) {
    try {
        _ctx = window.Amily2Bus.register('AutoSummary');
        _ctx.expose({ summarize });
        _ctx.log('Init', 'info', 'AutoSummary 服务已就绪。');
    } catch (e) {
        console.warn('[AutoSummary] Bus 注册警告:', e);
    }
}

其他模块现在可以这样调用:

const summary = window.Amily2Bus.query('AutoSummary');
if (summary) {
    const result = await summary.summarize(longText);
}

八、注意事项

  1. 名字唯一register() 的名字是全局唯一的,确认不与已有服务冲突(参考上面的服务一览表)
  2. 不要存引用expose() 的对象会被冻结,暴露的应该是函数而非可变状态
  3. 加载顺序 — 总线在 index.jsinitializeAmilyBus() 中初始化,所有服务通过 import 自动注册。如果你的模块依赖其他服务,在运行时 query() 即可,不需要控制 import 顺序
  4. PUBLICAmily2 是保留名 — 不要尝试注册这两个名字
  5. 生产与开发 — 页面刷新会重置整个总线,不需要手动清理。热重载时的重复注册异常是预期行为,不影响功能