For migrating plugins from API v2.0 to v3.0, see the Migration Guide. For Reference documentation and type definitions, see the DTS file.
Welcome to the Risuai Plugin Development Guide! This guide will help you create powerful, secure plugins for Risuai using API v3.0
- Getting Started
- Plugin Structure
- API v3.0 Architecture
- Core Concepts
- Working with the DOM
- Plugin UI
- Data Storage
- Advanced Features
- Best Practices
- Examples
- Troubleshooting
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.
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.
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
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 define your plugin's identity and configuration: It must be placed at the very top of your plugin script.
-
//@name- Internal plugin name (must be unique)//@name my_pluginWe do not recommend changing this after publishing, as it may break existing installations.
-
//@api- API version (use3.0for new plugins)//@api 3.0
-
//@display-name- User-friendly display name//@display-name My Awesome PluginUnlike
//@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,intSyntax://@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.jsPut 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.0Required 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//@nameand//@apimetadata, so that the update checker can read it easily.
API v3.0 plugins run inside a sandboxed iframe for security. This architecture provides:
- Isolation: Each plugin runs in its own isolated context
- Security: Limited access to the main application prevents data leaks
- Custom UI: The iframe can display custom interfaces
- 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()
+=====================================+
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
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 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 safelyYour plugin has access to two separate DOM contexts:
-
Your iframe's DOM (standard
documentobject)- 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
-
Main application DOM (via
getRootDocument())- Restricted access through
SafeDocument/SafeElementwrappers - 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.
- Restricted access through
We recommend using your iframe's DOM for custom UI whenever possible, and only access the main document when absolutely necessary.
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);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);
}The SafeElement wrapper provides secure DOM manipulation with these methods:
// 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// 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');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();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');// 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');// 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');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 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.
await element.focus();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 '#'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
});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
Add buttons and menu items to Risuai's interface:
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'
);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)
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);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 itemsUse pluginStorage when:
- You want data to sync across devices
- Data is specific to a save file
- Storing user preferences or plugin state
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
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:
charactersmodulesenabledModulesmoduleIntergrationpluginV2personaspluginspluginCustomStorage
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(): UseRisuai.getCharacter()Risuai.setChar(): UseRisuai.setCharacter()
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)}`);Direct browser fetch (may have CORS issues):
const response = await Risuai.nativeFetch('https://api.example.com/data');
const data = await response.json();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
}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
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);// Read image assets
const imageData = await Risuai.readImage('asset-id');
// Save assets
await Risuai.saveAsset(assetData, 'my-asset');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!Always handle errors gracefully:
(async () => {
try {
const data = await Risuai.getDatabase();
// Process data
} catch (error) {
console.log(`Error: ${error.message}`);
}
})();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);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);
}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)Don't try to break out of the iframe or access restricted APIs. The sandbox is for user security.
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//@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}`);
}
})();//@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}`);
}
})();//@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}`);
}
})();//@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}`);
}
})();//@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}`);
}
})();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 objectProblem: 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>');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);Problem: Confusing pluginStorage with safeLocalStorage.
pluginStorage: Save-file specific, syncs across devicessafeLocalStorage: Device-specific, shared between plugins
// For user preferences (syncs)
Risuai.pluginStorage.setItem('preference', 'value');
// For device-specific data
Risuai.safeLocalStorage.setItem('device_id', 'uuid');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!');
});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');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)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
Risuaiobject instead of global functions - Use
getRootDocument()instead ofdocument - SafeElement methods instead of standard HTMLElement
- Event listeners return IDs instead of using function references
- Iframe-based isolation instead of shared document context
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.