Skip to content

Latest commit

 

History

History
1302 lines (987 loc) · 34.8 KB

File metadata and controls

1302 lines (987 loc) · 34.8 KB

For migrating plugins from API v2.0 to v3.0, see the Migration Guide. For Reference documentation and type definitions, see the DTS file.

Risuai Plugin Development Guide

Welcome to the Risuai Plugin Development Guide! This guide will help you create powerful, secure plugins for Risuai using API v3.0

Table of Contents

  1. Getting Started
  2. Plugin Structure
  3. API v3.0 Architecture
  4. Core Concepts
  5. Working with the DOM
  6. Plugin UI
  7. Data Storage
  8. Advanced Features
  9. Best Practices
  10. Examples
  11. Troubleshooting

Getting Started

What are Risuai Plugins?

Risuai plugins are JavaScript extensions that can add new features, customize behavior, and integrate with external services. Plugins run in a secure, sandboxed environment to protect user data and privacy.

Plugin Structure

Template

We recommend starting with our Typescript plugin template for best practices and type safety.

You can download Typescript template from Risuai app -> Plugin Settings -> </> Menu -> Download plugin template.

If you are using IDE like Visual Studio Code, you can open the template folder directly and start coding with IntelliSense support.

Initial Setup

You can import your plugin script directly in Risuai app via Plugin Settings -> Import Plugin.

if your browser support local file access, we recommend using Hot Reload feature for faster development cycle. to use Hot Reload, import the plugin via Plugin Settings -> </> Menu -> Import plugin with hot reload

Basic Structure

Every plugin starts with metadata comments and a main script:

//@name My Awesome Plugin
//@display-name My Awesome Plugin
//@api 3.0
//@arg api_key string Your API key
//@arg max_retries int Maximum retry attempts
//@link https://github.com/yourname/plugin Documentation

// Your plugin code here
(async () => {
  try {
    console.log('Plugin initialized');

    // Your initialization code

  } catch (error) {
    console.log(`Error: ${error.message}`);
  }
})();

Metadata Comments

Metadata comments define your plugin's identity and configuration: It must be placed at the very top of your plugin script.

Required Metadata

  • //@name - Internal plugin name (must be unique)

    //@name my_plugin

    We do not recommend changing this after publishing, as it may break existing installations.

  • //@api - API version (use 3.0 for new plugins)

    //@api 3.0

Optional Metadata

  • //@display-name - User-friendly display name

    //@display-name My Awesome Plugin

    Unlike //@name, this can be changed freely without breaking installations.

  • //@arg - Define plugin arguments

    //@arg setting_name string Description of the setting
    //@arg max_items int Maximum number of items

    Supported types: string, int Syntax: //@arg <name> <type> <description and optional metadata>

  • //@link - Add custom links

    //@link https://example.com/docs Documentation
    //@link https://example.com/support Get Support

    The links will appear in the plugin settings UI.

  • //@update-url - URL to check for updates

    //@update-url https://raw.githubusercontent.com/username/repo/branch/plugin.js

    Put your plugin's latest raw js file URL here for automatic update checks. the server must support CORS and Range requests. We recommend hosting on GitHub repo and referencing the raw file URL. (e.g. https://raw.githubusercontent.com/username/repo/branch/plugin.js).

  • //@version - Version of your plugin

    //@version 1.0.0

    Required for update checks. Although other version formats are supported, we recommend using Semantic Versioning (e.g. 1.0.0, 2.1.3). This should be updated manually by you whenever you release a new version. unlike other metadata, this metadata requires to be high on the file, ideally right below the //@name and //@api metadata, so that the update checker can read it easily.

API v3.0 Architecture

The Iframe Sandbox Model

API v3.0 plugins run inside a sandboxed iframe for security. This architecture provides:

  1. Isolation: Each plugin runs in its own isolated context
  2. Security: Limited access to the main application prevents data leaks
  3. Custom UI: The iframe can display custom interfaces
  4. Structured Communication: Data is safely passed using structured cloning
+=====================================+
|   Main Risuai Application          |
|                                     |
|  +===============================+ |
|  |  Plugin Iframe (Hidden)       | |
|  |                               | |
|  |  - Your Plugin Code           | |
|  |  - Custom UI (optional)       | |
|  |  - Risuai API access          | |
|  +===============================+ |
|                                     |
|  Safe DOM Access via getRootDocument()
+=====================================+

Important: Everything is Async!

CRITICAL: All API methods in v3.0 return Promises, even if they appear synchronous in the code. Always use await or .then() when calling API methods.

// L WRONG - Will not work as expected
const character = Risuai.getCharacter();

//  CORRECT - Always use await
const character = await Risuai.getCharacter();

//  ALSO CORRECT - Using .then()
Risuai.getCharacter().then(character => {
  // Work with character
});

This applies to ALL Risuai API methods, including:

  • Data access (getCharacter, getDatabase, etc.)
  • DOM operations via getRootDocument()
  • Storage operations
  • All other API methods

Core Concepts

Accessing the API

All API v3.0 functionality is available through the global Risuai object:

// Get character data
const character = await Risuai.getCharacter();

// Access the main document
const rootDoc = await Risuai.getRootDocument();

The Risuai Object

The Risuai global object is your gateway to all plugin functionality:

// Version information
console.log(Risuai.apiVersion); // "3.0"
console.log(Risuai.apiVersionCompatibleWith); // ["3.0"]

// Logging
console.log('This appears as: [Risuai Plugin: PluginName] This...');

// Container management
await Risuai.showContainer('fullscreen'); // Show your iframe UI
await Risuai.hideContainer(); // Hide your iframe UI

// DOM access
const doc = await Risuai.getRootDocument(); // Access main document safely

Working with the DOM

Two DOM Contexts

Your plugin has access to two separate DOM contexts:

  1. Your iframe's DOM (standard document object)

    • Full access using standard DOM APIs
    • Hidden by default
    • Use for your plugin's custom UI
    • Sandboxed from the main application, no additional security restrictions and breaking changes expected
  2. Main application DOM (via getRootDocument())

    • Restricted access through SafeDocument/SafeElement wrappers
    • Use to interact with Risuai's interface
    • Security restrictions prevent malicious behavior
    • Additional restrictions might be added in the future for user safety, including breaking changes.

We recommend using your iframe's DOM for custom UI whenever possible, and only access the main document when absolutely necessary.

Accessing your Iframe's DOM

Your plugin's iframe has full access to the standard DOM API:

// Create elements
const container = document.createElement('div');
const button = document.createElement('button');

// Set content
button.textContent = 'Click Me!';
container.appendChild(button);

// Add to iframe body
document.body.appendChild(container);

Accessing the Main Document

Remember: Use await because all API methods are async!

// Get the root document
const rootDoc = await Risuai.getRootDocument();

// Create elements
const container = await rootDoc.createElement('div');
const button = await rootDoc.createElement('button');

// Set content
await button.setTextContent('Click Me!');
await container.appendChild(button);

// Query existing elements
const chatArea = await rootDoc.querySelector('.chat-container');
if (chatArea) {
  await chatArea.appendChild(container);
}

SafeElement API

The SafeElement wrapper provides secure DOM manipulation with these methods:

Element Manipulation

// Adding and removing children
await element.appendChild(childElement);
await element.removeChild(childElement);
await element.prepend(childElement);
await element.remove();

// Replacing elements
await element.replaceChild(newChild, oldChild);
await element.replaceWith(newElement);

// Cloning
const copy = await element.cloneNode(true); // deep clone

Text Content

// Getting text (remember: async!)
const text = await element.innerText();
const content = await element.textContent();

// Setting text
await element.setTextContent('Hello World');
await element.setInnerText('Hello World');

HTML Content (Auto-Sanitized)

All HTML is automatically sanitized with DOMPurify to prevent XSS attacks:

// Set HTML (safe - scripts are removed)
await element.setInnerHTML('<div class="message">Hello!</div>');

// This will be sanitized - script tag removed
await element.setInnerHTML('<script>alert("XSS")</script>');

// Get HTML
const html = await element.getInnerHTML();
const outerHtml = await element.getOuterHTML();

Attributes (Security Restricted)

For security reasons, only x- prefixed custom attributes can be directly accessed:

//  Allowed - custom attributes
await element.setAttribute('x-plugin-id', 'my-id');
const id = await element.getAttribute('x-plugin-id');

// L Not allowed - will throw error
await element.setAttribute('onclick', 'alert()'); // Error!
await element.setAttribute('href', 'javascript:...'); // Error!

Use dedicated methods for standard attributes:

// For links, use createAnchorElement
const link = rootDoc.createAnchorElement('https://example.com');

// For styles, use style methods
await element.setStyle('color', 'red');

Styling

// Individual style properties
await element.setStyle('color', 'blue');
await element.setStyle('fontSize', '16px');
const color = await element.getStyle('color');

// Style attribute as string
await element.setStyleAttribute('color: red; font-size: 14px;');
const styleStr = await element.getStyleAttribute();

// CSS classes
await element.addClass('active');
await element.removeClass('inactive');
await element.setClassName('container active');
const className = await element.getClassName();
const isActive = await element.hasClass('active');

Querying and Traversal

// Query descendants
const buttons = await element.querySelectorAll('.button');
const firstButton = await element.querySelector('.button');
const byId = await element.getElementById('submit-btn');
const byClass = await element.getElementsByClassName('message');

// Navigation
const children = await element.getChildren();
const parent = await element.getParent();

// Matching
const matches = await element.matches('.selected');

Dimensions and Position

const height = await element.clientHeight();
const width = await element.clientWidth();
const top = await element.clientTop();
const left = await element.clientLeft();

const rect = await element.getBoundingClientRect();
const rects = await element.getClientRects();

Event Listeners

Event listeners have security restrictions and return unique IDs:

// Add event listener - returns ID for later removal
const listenerId = await element.addEventListener('click', async (event) => {
  console.log('Element clicked!');

  // Do something with the event
  const target = event.target;
}, { capture: false });

// Remove event listener using the ID
await element.removeEventListener('click', listenerId);

Allowed Events (Unlimited):

  • Mouse: click, dblclick, contextmenu, mousedown, mouseup, mousemove, mouseover, mouseleave
  • Pointer: pointercancel, pointerdown, pointerenter, pointerleave, pointermove, pointerout, pointerover, pointerup
  • Scroll: scroll, scrollend

Allowed Events (Random Delay for Anti-Fingerprinting):

  • Keyboard: keydown, keyup, keypress (delayed randomly to prevent timing attacks)

Blocked Events: All other event types are blocked for security reasons.

Focus

await element.focus();

Element Creation

const doc = await Risuai.getRootDocument();

// Create regular elements (limited to whitelist)
const div = await doc.createElement('div');
const span = await doc.createElement('span');
const button = await doc.createElement('button');

// Non-whitelisted tags become <div>
const unknown = await doc.createElement('custom-element'); // Creates <div>

// Create validated anchor links
const link = await doc.createAnchorElement('https://example.com');

// Only http/https allowed
const badLink = await doc.createAnchorElement('javascript:alert()'); // href becomes '#'

Monitoring DOM Changes

Use SafeMutationObserver to watch for changes:

// Create observer
const observer = await Risuai.createMutationObserver(async (mutations) => {
  for (const mutation of mutations) {
    console.log(`Type: ${mutation.type}`);

    // mutation.target is a SafeElement
    const target = mutation.target;

    // mutation.addedNodes is SafeElement[]
    for (const node of mutation.addedNodes) {
      console.log(`Node added: ${await node.nodeName()}`);
    }
  }
});

// Start observing
const rootDoc = await Risuai.getRootDocument();
const body = await rootDoc.querySelector('body');
await observer.observe(body, {
  childList: true,
  subtree: true,
  attributes: true
});

Plugin UI

Using Your Iframe for Custom UI

Your plugin's iframe is hidden by default. You can show it to display custom interfaces: Unlike getRootDocument(), your iframe's document is the standard DOM API without restrictions.

// Build your UI in the iframe's document
async function showPluginUI() {
  // Access your iframe's document (standard DOM API)
  const myDoc = document;

  myDoc.body.innerHTML = `
    <div style="padding: 20px; background: #1e1e1e; color: white;">
      <h1>My Plugin Settings</h1>
      <button id="save-btn">Save</button>
      <button id="close-btn">Close</button>
    </div>
  `;

  // Add event listeners (standard DOM)
  myDoc.getElementById('save-btn').addEventListener('click', async () => {
    await saveSettings();
  });

  myDoc.getElementById('close-btn').addEventListener('click', async () => {
    await Risuai.hideContainer();
  });

  // Show the iframe in fullscreen
  await Risuai.showContainer('fullscreen');
}

When shown in fullscreen mode, your iframe:

  • Is moved to document.body
  • Positioned fixed at (0, 0)
  • Sized to 100% width and height
  • Has z-index of 1000
  • Border removed

Registering UI Elements in Risuai

Add buttons and menu items to Risuai's interface:

Settings Menu Item

Risuai.registerSetting(
  'My Plugin Settings',
  async () => {
    // Called when user clicks the menu item
    await Risuai.showContainer('fullscreen');
  },
  '<svg width="24" height="24">...</svg>', // Optional icon
  'html' // Icon type: 'html', 'img', or 'none'
);

Floating Action Button

Risuai.registerButton({
    name: 'Quick Action',
    icon: 'https://example.com/icon.png', // Optional icon URL
    iconType: 'img', // Icon type: 'html', 'img', or 'none'
    location: 'action' //you can also use 'chat' or 'hamburger' for chat or hamburger menu
}, async () => {
  // Called when user clicks the button
  const char = await Risuai.getCharacter();
  await console.log(`Current character: ${char.name}`);
});

Icon Types:

  • 'html' - Raw HTML (SVG, emoji, etc.)
  • 'img' - Image URL
  • 'none' - No icon (text only)

Data Storage

Plugin Arguments

Use arguments for user-configurable settings:

// Define in metadata
//@arg api_key string Your API key
//@arg max_retries int Maximum retry attempts

// Access in code (remember: async!)
const apiKey = await Risuai.getArgument('api_key');
const maxRetries = await Risuai.getArgument('max_retries');

// Update values
await Risuai.setArgument('max_retries', 5);

Plugin Storage (Recommended)

pluginStorage is save-file specific and syncs between devices:

// All operations are synchronous (wrapper around sync storage)
await Risuai.pluginStorage.setItem('user_preference', 'dark_mode');
await Risuai.pluginStorage.setItem('last_sync', Date.now().toString());

const preference = await  Risuai.pluginStorage.getItem('user_preference');
const allKeys = await Risuai.pluginStorage.keys();
const count = await Risuai.pluginStorage.length();

await Risuai.pluginStorage.removeItem('last_sync');
await Risuai.pluginStorage.clear(); // Remove all items

Use pluginStorage when:

  • You want data to sync across devices
  • Data is specific to a save file
  • Storing user preferences or plugin state

Safe Local Storage

safeLocalStorage is device-specific and shared between plugins:

// Same API as pluginStorage
await Risuai.safeLocalStorage.setItem('device_id', 'unique-id');
const deviceId = await Risuai.safeLocalStorage.getItem('device_id');

Use safeLocalStorage when:

  • Data should stay on one device
  • Storing device-specific settings
  • Sharing data between plugins

Database Access

Access Risuai's database for characters, personas, and more:

// Get database (remember: async!)
const db = await Risuai.getDatabase();

// Access allowed properties
console.log(db.characters);
console.log(db.personas);
console.log(db.modules);

// Update database
db.characters.push(newCharacter);
await Risuai.setDatabase(db); // Full save

// Or use lite version (faster)
await Risuai.setDatabaseLite(db);

Allowed database keys:

  • characters
  • modules
  • enabledModules
  • moduleIntergration
  • pluginV2
  • personas
  • plugins
  • pluginCustomStorage

Character Operations

Convenient methods for working with the current character:

// Get current character (async!)
const character = await Risuai.getCharacter();

console.log(character.name);
console.log(character.description);

// Modify character
character.customField = 'new value';

// Save changes
await Risuai.setCharacter(character);

Legacy names (still work, but prefer new names):

  • Risuai.getChar() : Use Risuai.getCharacter()
  • Risuai.setChar() : Use Risuai.setCharacter()

Advanced Features

Network Requests

Risuai Fetch (Recommended)

Uses Risuai's fetch with CORS handling and proxy support:

const response = await Risuai.nativeFetch('https://api.example.com/data', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${await Risuai.getArgument('api_key')}`
  },
  body: JSON.stringify({ query: 'hello' })
});

const data = await response.json();
console.log(`Received: ${JSON.stringify(data)}`);

Native Fetch

Direct browser fetch (may have CORS issues):

const response = await Risuai.nativeFetch('https://api.example.com/data');
const data = await response.json();

Custom AI Providers

Add custom AI backend providers:

Risuai.addProvider(
  'MyCustomProvider',
  async (args, abortSignal) => {
    try {
      const response = await Risuai.nativeFetch(
        'https://api.my-llm.com/generate',
        {
          method: 'POST',
          headers: { 'Authorization': `Bearer ${await Risuai.getArgument('api_key')}` },
          body: JSON.stringify({
            messages: args.prompt_chat,
            temperature: args.temperature,
            max_tokens: args.max_tokens
          }),
          signal: abortSignal
        }
      );

      const data = await response.json();

      return {
        success: true,
        content: data.response // String or ReadableStream<string>
      };
    } catch (error) {
      console.log(`Provider error: ${error.message}`);
      return {
        success: false,
        content: `Error: ${error.message}`
      };
    }
  },
  {
    // Optional: custom tokenizer
    tokenizerFunc: async (content) => {
      // Return token IDs as number[]
      return [/* token ids */];
    }
  }
);

Provider Arguments:

{
  prompt_chat: OpenAIChat[], // Message history
  temperature: number,
  max_tokens: number,
  frequency_penalty: number,
  min_p: number,
  presence_penalty: number,
  repetition_penalty: number,
  top_k: number,
  top_p: number,
  mode: string
}

Script Handlers

Modify content at different processing stages:

// Add handler for display output
Risuai.addRisuScriptHandler('display', async (content) => {
  // Modify content before display
  return content.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
});

// Add handler for user input
Risuai.addRisuScriptHandler('input', async (content) => {
  // Process user input before sending
  return content.trim();
});

// Remove handler
Risuai.removeRisuScriptHandler('display', handlerFunction);

Available modes:

  • 'display' - Modify content before showing to user
  • 'output' - Modify AI output
  • 'process' - Modify content during processing
  • 'input' - Modify user input

Text Replacers

Replace or modify message arrays:

// Add replacer before sending to AI
Risuai.addRisuReplacer('beforeRequest', async (messages, type) => {
  // Add system message
  return [
    { role: 'system', content: 'You are a helpful assistant.' },
    ...messages
  ];
});

// Add replacer after receiving from AI
Risuai.addRisuReplacer('afterRequest', async (content, type) => {
  // Modify response text
  return content.toUpperCase();
});

// Remove replacer
Risuai.removeRisuReplacer('beforeRequest', replacerFunction);

Asset Management

// Read image assets
const imageData = await Risuai.readImage('asset-id');

// Save assets
await Risuai.saveAsset(assetData, 'my-asset');

Best Practices

1. Always Use Async/Await

All Risuai API methods are async - never forget await:

// L WRONG
const char = Risuai.getCharacter();
console.log(char.name); // undefined or Promise

//  CORRECT
const char = await Risuai.getCharacter();
console.log(char.name); // Works!

2. Wrap in Try-Catch

Always handle errors gracefully:

(async () => {
  try {
    const data = await Risuai.getDatabase();
    // Process data
  } catch (error) {
    console.log(`Error: ${error.message}`);
  }
})();

3. Use Plugin Storage for Persistence

Prefer pluginStorage over safeLocalStorage for syncable data:

//  Good - syncs across devices
Risuai.pluginStorage.setItem('settings', JSON.stringify(settings));

// Device-specific only
Risuai.safeLocalStorage.setItem('device_id', id);

4. Clean Up Resources

Remove event listeners when done:

const listeners = [];

// Store listener IDs
listeners.push(await element.addEventListener('click', handler));

// Clean up
for (const id of listeners) {
  await element.removeEventListener('click', id);
}

5. Use Modern Naming Conventions

Prefer new API names over deprecated ones:

//  Modern
await Risuai.getCharacter()
await Risuai.setCharacter(char)
await Risuai.getArgument(key)

// L Deprecated (still work but avoid)
await Risuai.getChar()
await Risuai.setChar(char)
await Risuai.getArg(key)

6. Respect the Sandbox

Don't try to break out of the iframe or access restricted APIs. The sandbox is for user security.

7. Document Your Plugin

Add clear comments and metadata:

//@name my_plugin
//@display-name My Awesome Plugin
//@api 3.0
//@arg api_key string Get your key at https://example.com
//@link https://github.com/user/plugin Documentation
//@link https://github.com/user/plugin/issues Report Issues

Examples

Example 1: Simple Settings Panel

//@name settings_example
//@display-name Settings Example
//@api 3.0
//@arg theme string Color theme (light/dark)

(async () => {
  try {
    // Register settings button
    Risuai.registerSetting(
      'Theme Settings',
      async () => {
        const theme = await Risuai.getArgument('theme');

        document.body.innerHTML = `
          <div style="padding: 20px; background: #2d2d2d; color: white; font-family: sans-serif;">
            <h1>Theme Settings</h1>
            <p>Current theme: <strong>${theme}</strong></p>
            <button id="light-btn">Light Theme</button>
            <button id="dark-btn">Dark Theme</button>
            <button id="close-btn">Close</button>
          </div>
        `;

        document.getElementById('light-btn').addEventListener('click', async () => {
          await Risuai.setArgument('theme', 'light');
          console.log('Theme set to light');
        });

        document.getElementById('dark-btn').addEventListener('click', async () => {
          await Risuai.setArgument('theme', 'dark');
          console.log('Theme set to dark');
        });

        document.getElementById('close-btn').addEventListener('click', () => {
          Risuai.hideContainer();
        });

        Risuai.showContainer('fullscreen');
      },
      'https://example.com/icon_src_here.png',
      'img'
    );

    console.log('Settings panel registered');
  } catch (error) {
    console.log(`Error: ${error.message}`);
  }
})();

Example 2: Character Info Display

//@name char_info
//@display-name Character Info Display
//@api 3.0

(async () => {
  try {
    Risuai.registerButton({
      name: 'Show Character Info',
      icon: '🛈',
      iconType: 'html',
      location: 'action',
    }, async () => {
        const char = await Risuai.getCharacter();

        const rootDoc = Risuai.getRootDocument();
        const body = rootDoc.querySelector('body');

        const infoBox = rootDoc.createElement('div');
        await infoBox.setStyle('position', 'fixed');
        await infoBox.setStyle('top', '50%');
        await infoBox.setStyle('left', '50%');
        await infoBox.setStyle('transform', 'translate(-50%, -50%)');
        await infoBox.setStyle('background', 'white');
        await infoBox.setStyle('padding', '20px');
        await infoBox.setStyle('border', '2px solid black');
        await infoBox.setStyle('zIndex', '9999');

        await infoBox.setInnerHTML(`
          <h2>${char.name}</h2>
          <p><strong>Description:</strong> ${char.description || 'No description'}</p>
          <button id="close-info">Close</button>
        `);

        await body.appendChild(infoBox);

        const closeBtn = await infoBox.querySelector('#close-info');
        if (closeBtn) {
          await closeBtn.addEventListener('click', async () => {
            await infoBox.remove();
          });
        }
      }
    );
    console.log('Character info button registered');
  } catch (error) {
    console.log(`Error: ${error.message}`);
  }
})();

Example 3: Custom AI Provider

//@name custom_llm
//@display-name Custom LLM Provider
//@api 3.0
//@arg endpoint string API endpoint URL
//@arg api_key string Your API key

(async () => {
  try {
    Risuai.addProvider(
      'CustomLLM',
      async (args, abortSignal) => {
        const endpoint = await Risuai.getArgument('endpoint');
        const apiKey = await Risuai.getArgument('api_key');

        try {
          const response = await Risuai.nativeFetch(endpoint, {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json',
              'Authorization': `Bearer ${apiKey}`
            },
            body: JSON.stringify({
              messages: args.prompt_chat,
              temperature: args.temperature,
              max_tokens: args.max_tokens,
              top_p: args.top_p
            }),
            signal: abortSignal
          });

          if (!response.ok) {
            throw new Error(`HTTP ${response.status}`);
          }

          const data = await response.json();

          return {
            success: true,
            content: data.choices[0].message.content
          };
        } catch (error) {
          console.log(`Provider error: ${error.message}`);
          return {
            success: false,
            content: `Error: ${error.message}`
          };
        }
      }
    );

    console.log('CustomLLM provider registered');
  } catch (error) {
    console.log(`Error: ${error.message}`);
  }
})();

Example 4: DOM Manipulation & Mutation Observer

//@name dom_monitor
//@display-name DOM Monitor
//@api 3.0

(async () => {
  try {
    const rootDoc = Risuai.getRootDocument();

    // Add a status indicator
    const indicator = rootDoc.createElement('div');
    await indicator.setStyle('position', 'fixed');
    await indicator.setStyle('bottom', '10px');
    await indicator.setStyle('right', '10px');
    await indicator.setStyle('padding', '10px');
    await indicator.setStyle('background', '#4CAF50');
    await indicator.setStyle('color', 'white');
    await indicator.setStyle('borderRadius', '5px');
    await indicator.setTextContent('Plugin Active');

    const body = rootDoc.querySelector('body');
    if (body) {
      await body.appendChild(indicator);
    }

    // Monitor DOM changes
    let changeCount = 0;
    const observer = Risuai.createMutationObserver(async (mutations) => {
      changeCount += mutations.length;
      await indicator.setTextContent(`Changes: ${changeCount}`);
    });

    if (body) {
      observer.observe(body, {
        childList: true,
        subtree: true,
        attributes: false
      });
    }

    console.log('DOM monitoring started');
  } catch (error) {
    console.log(`Error: ${error.message}`);
  }
})();

Example 5: Text Processing

//@name markdown_processor
//@display-name Markdown Processor
//@api 3.0

(async () => {
  try {
    // Process AI output to convert markdown-style bold
    Risuai.addRisuScriptHandler('output', async (content) => {
      // **bold** <strong>bold</strong>
      content = content.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');

      // *italic* <em>italic</em>
      content = content.replace(/\*(.+?)\*/g, '<em>$1</em>');

      return content;
    });

    // Process user input to add timestamps
    Risuai.addRisuScriptHandler('input', async (content) => {
      const timestamp = new Date().toLocaleTimeString();
      Risuai.pluginStorage.setItem('last_input_time', timestamp);

      return content;
    });

    console.log('Markdown processor registered');
  } catch (error) {
    console.log(`Error: ${error.message}`);
  }
})();

Troubleshooting

My API calls return undefined

Problem: Not using await on async methods.

// L Wrong
const char = Risuai.getCharacter();
console.log(char); // Promise or undefined

//  Correct
const char = await Risuai.getCharacter();
console.log(char); // Actual character object

Can't set element attributes

Problem: Trying to set non-x- prefixed attributes.

// L Wrong - throws error
await element.setAttribute('onclick', 'alert()');

//  Correct - use x- prefix for custom attributes
await element.setAttribute('x-custom-id', 'my-id');

//  Or use dedicated methods
await element.setStyle('color', 'red');
await element.setInnerHTML('<div>Safe content</div>');

Event listeners not working

Problem: Using standard addEventListener syntax or not storing the ID.

// L Wrong - need await and ID storage
element.addEventListener('click', handler);

//  Correct
const listenerId = await element.addEventListener('click', async (e) => {
  // Handle event
});

// Later, remove with ID
await element.removeEventListener('click', listenerId);

Plugin storage not persisting

Problem: Confusing pluginStorage with safeLocalStorage.

  • pluginStorage: Save-file specific, syncs across devices
  • safeLocalStorage: Device-specific, shared between plugins
// For user preferences (syncs)
Risuai.pluginStorage.setItem('preference', 'value');

// For device-specific data
Risuai.safeLocalStorage.setItem('device_id', 'uuid');

Script tags being removed from HTML

Problem: This is intentional! All HTML is sanitized with DOMPurify.

// Scripts are removed for security
await element.setInnerHTML('<script>alert("XSS")</script>');
// Result: empty element (script removed)

// Use event listeners instead
const button = rootDoc.createElement('button');
await button.setTextContent('Click Me');
await button.addEventListener('click', async () => {
  console.log('Button clicked!');
});

Can't access iframe DOM from root

Problem: Trying to access your iframe's document from root or vice versa.

// L Wrong - these are separate contexts
const rootDoc = Risuai.getRootDocument();
rootDoc.querySelector('#my-iframe-element'); // Won't find it

//  Correct - access each separately
// Your iframe's DOM:
document.getElementById('my-iframe-element');

// Risuai's main DOM:
const rootDoc = Risuai.getRootDocument();
await rootDoc.querySelector('.Risuai-element');

Changes not saving to database

Problem: Forgetting to call setDatabase() or setDatabaseLite().

// L Wrong - changes not saved
const db = await Risuai.getDatabase();
db.characters.push(newChar);

//  Correct - save changes
const db = await Risuai.getDatabase();
db.characters.push(newChar);
await Risuai.setDatabase(db); // Or setDatabaseLite(db)

Migration from API v2.1

If you're updating an older plugin, see the Migration Guide for detailed migration instructions from API v2.1 to v3.0.

Key differences:

  • All APIs are now async (use await)
  • Access through Risuai object instead of global functions
  • Use getRootDocument() instead of document
  • SafeElement methods instead of standard HTMLElement
  • Event listeners return IDs instead of using function references
  • Iframe-based isolation instead of shared document context

Generating Plugin via LLM

If you want to generate a plugin using an LLM like we recommend including only DTS file instead of including full documentation in the prompt, unless you are migrating from v2.0 to v3.0 in which case you should include the Migration Guide in the prompt too.