Skip to content

Commit a8a0728

Browse files
Created Multiple New Plugins, including a webui-test
1 parent 6abe31a commit a8a0728

File tree

10 files changed

+1278
-112
lines changed

10 files changed

+1278
-112
lines changed

package-lock.json

Lines changed: 0 additions & 34 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

plugins/ai-chat-zuki.js

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
// AI Chat (OpenAI-compatible) plugin
2+
// Lets users ask questions with an in-game command and/or a Discord slash command
3+
// without touching any core files. Designed to work with any OpenAI-compatible API
4+
// (e.g., Zuki/Journey free endpoints) by configuring the base URL, model and key.
5+
6+
const Constants = require('../src/util/constants.js');
7+
8+
module.exports = {
9+
defaultEnabled: false,
10+
displayName: 'AI Chat (OpenAI-compatible)',
11+
description: 'Ask AI with !ai <question> or /ai, using a configurable OpenAI-compatible API (e.g., Zuki free model).',
12+
13+
// All fields render as a short text input in the plugin modal. Parse as needed.
14+
// Keep at <= 5 fields to satisfy Discord modal limits
15+
configSchema: {
16+
command: { type: 'text', label: 'In-game command (without prefix)', default: 'ai' },
17+
apiUrl: { type: 'text', label: 'API base (OpenAI-compatible)', default: 'https://api.zukijourney.com/v1' },
18+
apiKey: { type: 'text', label: 'API key (optional if not required)', default: '' },
19+
model: { type: 'text', label: 'Model name', default: 'gpt-3.5-turbo' },
20+
systemPrompt: { type: 'text', label: 'System prompt (optional)', default: 'You are a helpful assistant. If the user\'s request is ambiguous yet plausibly about the video game Rust, interpret and answer in the Rust (video game) context. If it\'s clearly not about Rust, answer normally.' }
21+
},
22+
23+
// Simple in-memory cooldown per guild
24+
_cooldowns: {},
25+
26+
// In-game command handler: !ai <question>
27+
onInGameCommand: async ({ rustplus, client, command }) => {
28+
const guildId = rustplus.guildId;
29+
const instance = client.getInstance(guildId);
30+
const settings = (instance.pluginSettings && instance.pluginSettings['ai-chat-zuki.js']) || {};
31+
32+
const prefix = rustplus.generalSettings.prefix || '!';
33+
const cmd = (settings.command || 'ai').trim().toLowerCase();
34+
const expectedStart = `${prefix}${cmd}`;
35+
if (!command.toLowerCase().startsWith(expectedStart)) return false;
36+
37+
const question = command.slice(expectedStart.length).trim();
38+
if (!question) {
39+
await rustplus.sendInGameMessage(`Usage: ${expectedStart} <question>`);
40+
return true;
41+
}
42+
43+
if (!module.exports._passCooldown(guildId, settings)) {
44+
await rustplus.sendInGameMessage('Please wait before sending another request.');
45+
return true;
46+
}
47+
48+
try {
49+
const resp = await module.exports._chatComplete(client, guildId, settings, question);
50+
const text = (resp || 'No response').replace(/\s+/g, ' ').trim();
51+
// In-game message length is limited; trim to be safe
52+
const max = Math.max(32, Constants.MAX_LENGTH_TEAM_MESSAGE || 128);
53+
await rustplus.sendInGameMessage(text.length > max ? `${text.slice(0, max - 3)}...` : text);
54+
} catch (e) {
55+
await rustplus.sendInGameMessage(`AI error: ${e?.message || e}`);
56+
}
57+
return true;
58+
},
59+
60+
// Provide a /ai question:<string> slash command
61+
slashCommands: [
62+
{
63+
name: 'ai',
64+
getData(client, guildId) {
65+
const Builder = require('@discordjs/builders');
66+
return new Builder.SlashCommandBuilder()
67+
.setName('ai')
68+
.setDescription('Ask the configured AI model a question')
69+
.addStringOption(option => option
70+
.setName('question')
71+
.setDescription('Your question')
72+
.setRequired(true));
73+
},
74+
async execute(client, interaction) {
75+
const verifyId = Math.floor(100000 + Math.random() * 900000);
76+
client.logInteraction(interaction, verifyId, 'slashCommand');
77+
if (!await client.validatePermissions(interaction)) return;
78+
await interaction.deferReply({ ephemeral: false });
79+
80+
const guildId = interaction.guildId;
81+
const instance = client.getInstance(guildId);
82+
const settings = (instance.pluginSettings && instance.pluginSettings['ai-chat-zuki.js']) || {};
83+
84+
const question = interaction.options.getString('question');
85+
if (!module.exports._passCooldown(guildId, settings)) {
86+
await client.interactionEditReply(interaction, { content: 'Please wait before sending another request.' });
87+
return;
88+
}
89+
try {
90+
const answer = await module.exports._chatComplete(client, guildId, settings, question);
91+
await client.interactionEditReply(interaction, { content: answer || 'No response' });
92+
} catch (e) {
93+
await client.interactionEditReply(interaction, { content: `AI error: ${e?.message || e}` });
94+
}
95+
}
96+
}
97+
],
98+
99+
// Helpers
100+
_passCooldown(guildId, settings) {
101+
const cd = Math.max(0, parseInt((settings.cooldownSeconds ?? '10'), 10) || 0);
102+
const now = Date.now();
103+
const last = module.exports._cooldowns[guildId] || 0;
104+
if (now - last < cd * 1000) return false;
105+
module.exports._cooldowns[guildId] = now;
106+
return true;
107+
},
108+
109+
async _chatComplete(client, guildId, settings, userContent) {
110+
const base = (settings.apiUrl || 'https://api.zukijourney.com/v1').trim().replace(/\/$/, '');
111+
const url = `${base}/chat/completions`;
112+
const apiKey = (settings.apiKey || '').trim();
113+
const model = (settings.model || 'gpt-3.5-turbo').trim();
114+
const system = (settings.systemPrompt || 'You are a helpful assistant. If the user\'s request is ambiguous yet plausibly about the video game Rust, interpret and answer in the Rust (video game) context. If it\'s clearly not about Rust, answer normally.').trim();
115+
const temperature = Math.max(0, Math.min(2, parseFloat(settings.temperature ?? '0.7') || 0.7));
116+
const maxTokens = Math.max(1, parseInt(settings.maxTokens ?? '512', 10) || 512);
117+
118+
const body = {
119+
model,
120+
messages: [
121+
{ role: 'system', content: system },
122+
{ role: 'user', content: userContent }
123+
],
124+
temperature,
125+
max_tokens: maxTokens
126+
};
127+
128+
const headers = { 'Content-Type': 'application/json' };
129+
if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`;
130+
131+
let json;
132+
try {
133+
const resp = await fetch(url, { method: 'POST', headers, body: JSON.stringify(body) });
134+
if (!resp.ok) {
135+
let txt = '';
136+
try { txt = await resp.text(); } catch (_) {}
137+
throw new Error(`${resp.status} ${txt}`);
138+
}
139+
json = await resp.json();
140+
} catch (e) {
141+
client.log(client.intlGet(null, 'errorCap'), `[ai-chat] request failed: ${e?.message || e}`, 'error');
142+
throw e;
143+
}
144+
145+
try {
146+
return (json.choices && json.choices[0] && json.choices[0].message && json.choices[0].message.content) || null;
147+
} catch (_) { return null; }
148+
}
149+
};

0 commit comments

Comments
 (0)