Several updates.
This commit is contained in:
parent
b76903f6e0
commit
aac6dc4542
@ -7,7 +7,7 @@ import fs from 'fs/promises';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { OpenAI } from 'openai';
|
import { OpenAI } from 'openai';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { AttachmentBuilder } from 'discord.js';
|
import { AttachmentBuilder, PermissionFlagsBits } from 'discord.js';
|
||||||
|
|
||||||
// Discord message max length
|
// Discord message max length
|
||||||
const MAX_DISCORD_MSG_LENGTH = 2000;
|
const MAX_DISCORD_MSG_LENGTH = 2000;
|
||||||
@ -271,11 +271,13 @@ async function onMessage(client, cfg, message) {
|
|||||||
// Previous response ID for context continuity
|
// Previous response ID for context continuity
|
||||||
const prev = client.pb?.cache?.get(key);
|
const prev = client.pb?.cache?.get(key);
|
||||||
// Enforce minimum score to use AI responses
|
// Enforce minimum score to use AI responses
|
||||||
|
// Enforce minimum score to use AI responses, but allow guild admins
|
||||||
try {
|
try {
|
||||||
|
const isAdmin = message.member?.permissions?.has(PermissionFlagsBits.Administrator);
|
||||||
const scoreData = await client.scorekeeper.getScore(message.guild.id, message.author.id);
|
const scoreData = await client.scorekeeper.getScore(message.guild.id, message.author.id);
|
||||||
if (scoreData.totalScore < cfg.minScore) {
|
if (!isAdmin && scoreData.totalScore < cfg.minScore) {
|
||||||
await message.reply(
|
await message.reply(
|
||||||
`You need a score of at least ${cfg.minScore} to use AI responses. Your current score is ${scoreData.totalScore.toFixed(2)}.`
|
`You need an I/O score of at least ${cfg.minScore} to use AI responses. Your current I/O score is ${scoreData.totalScore.toFixed(2)}.`
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
* Defines and handles the /query command via the OpenAI Responses API,
|
* Defines and handles the /query command via the OpenAI Responses API,
|
||||||
* including optional image generation function calls.
|
* including optional image generation function calls.
|
||||||
*/
|
*/
|
||||||
import { SlashCommandBuilder, AttachmentBuilder } from 'discord.js';
|
import { SlashCommandBuilder, AttachmentBuilder, PermissionFlagsBits } from 'discord.js';
|
||||||
import fs from 'fs/promises';
|
import fs from 'fs/promises';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
@ -169,12 +169,13 @@ export const commands = [
|
|||||||
),
|
),
|
||||||
async execute(interaction, client) {
|
async execute(interaction, client) {
|
||||||
const cfg = client.config.responses;
|
const cfg = client.config.responses;
|
||||||
// Enforce minimum score to use /query
|
// Enforce minimum score to use /query, allow guild admins to bypass
|
||||||
try {
|
try {
|
||||||
|
const isAdmin = interaction.member?.permissions?.has(PermissionFlagsBits.Administrator);
|
||||||
const scoreData = await client.scorekeeper.getScore(interaction.guildId, interaction.user.id);
|
const scoreData = await client.scorekeeper.getScore(interaction.guildId, interaction.user.id);
|
||||||
if (scoreData.totalScore < cfg.minScore) {
|
if (!isAdmin && scoreData.totalScore < cfg.minScore) {
|
||||||
return interaction.reply({
|
return interaction.reply({
|
||||||
content: `You need a score of at least ${cfg.minScore} to use /query. Your current score is ${scoreData.totalScore.toFixed(2)}.`,
|
content: `You need an I/O score of at least ${cfg.minScore} to use /query. Your current I/O score is ${scoreData.totalScore.toFixed(2)}.`,
|
||||||
ephemeral: true
|
ephemeral: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -280,12 +280,40 @@ export const commands = [
|
|||||||
minutesUntilNextPhase = timeUntilNextLight;
|
minutesUntilNextPhase = timeUntilNextLight;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate a timestamp for Discord's formatting and reply
|
// Calculate a timestamp for Discord's formatting and reply
|
||||||
const expiration = Math.ceil((Date.now() / 1000) + (minutesUntilNextPhase * 60));
|
const expiration = Math.ceil((Date.now() / 1000) + (minutesUntilNextPhase * 60));
|
||||||
await interaction.reply(`### ${lights[0]} ${lights[1]} ${lights[2]} ${lights[3]} ${lights[4]}`);
|
// Determine time to next Lock/Unlock phase for inline display
|
||||||
|
const isUnlocked = currentPhase === 'Unlocked';
|
||||||
|
const label = isUnlocked ? 'Lock' : 'Unlock';
|
||||||
|
const minutesToPhase = isUnlocked
|
||||||
|
? (turningOffDuration + allOffDuration) - cyclePosition
|
||||||
|
: cycleDuration - cyclePosition;
|
||||||
|
const phaseEpoch = Math.ceil(Date.now() / 1000 + (minutesToPhase * 60));
|
||||||
|
// Reply with lights and inline time to phase
|
||||||
|
await interaction.reply(
|
||||||
|
`### ${lights[0]} ${lights[1]} ${lights[2]} ${lights[3]} ${lights[4]} — Time to ${label}: <t:${phaseEpoch}:R>`
|
||||||
|
);
|
||||||
|
|
||||||
if (verbose) {
|
if (verbose) {
|
||||||
await interaction.followUp(`- **Phase**: ${currentPhase}\n- **Status Expiration**: <t:${expiration}:R>\n- **Epoch**: <t:${Math.ceil(hangarSync.epoch / 1000)}:R>\n- **Sync**: <t:${Math.floor(new Date(hangarSync.updated).getTime() / 1000)}:R> by <@${hangarSync.userId}>`);
|
// Replace user mention with displayName for last sync
|
||||||
|
const syncMember = await interaction.guild.members.fetch(hangarSync.userId).catch(() => null);
|
||||||
|
const syncName = syncMember ? syncMember.displayName : `<@${hangarSync.userId}>`;
|
||||||
|
|
||||||
|
// Calculate time until next Lock/Unlock phase
|
||||||
|
const isUnlocked = currentPhase === 'Unlocked';
|
||||||
|
const label = isUnlocked ? 'Lock' : 'Unlock';
|
||||||
|
const minutesToPhase = isUnlocked
|
||||||
|
? (turningOffDuration + allOffDuration) - cyclePosition
|
||||||
|
: cycleDuration - cyclePosition;
|
||||||
|
const phaseEpoch = Math.ceil(Date.now() / 1000 + (minutesToPhase * 60));
|
||||||
|
|
||||||
|
await interaction.followUp(
|
||||||
|
`- **Phase**: ${currentPhase}\n` +
|
||||||
|
`- **Time to ${label}**: <t:${phaseEpoch}:R>\n` +
|
||||||
|
`- **Status Expiration**: <t:${expiration}:R>\n` +
|
||||||
|
`- **Epoch**: <t:${Math.ceil(hangarSync.epoch / 1000)}:R>\n` +
|
||||||
|
`- **Sync**: <t:${Math.floor(new Date(hangarSync.updated).getTime() / 1000)}:R> by ${syncName}`
|
||||||
|
);
|
||||||
|
|
||||||
// Add additional debug info to logs
|
// Add additional debug info to logs
|
||||||
client.logger.debug(`Hangarstatus for guild ${interaction.guildId}: Phase=${currentPhase}, CyclePosition=${cyclePosition}, TimeSinceEpoch=${timeSinceEpoch}`);
|
client.logger.debug(`Hangarstatus for guild ${interaction.guildId}: Phase=${currentPhase}, CyclePosition=${cyclePosition}, TimeSinceEpoch=${timeSinceEpoch}`);
|
||||||
|
|||||||
@ -336,14 +336,15 @@ async function runDecay(client, guildId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate score based on formula
|
* Calculate score based on formula (no scaling factor)
|
||||||
|
* Formula: (1 + C*commendationValue - D*citationValue) * (input / (output + baseOutput))
|
||||||
*/
|
*/
|
||||||
function calculateScore(data, baseOutput, commendationValue, citationValue) {
|
function calculateScore(data, baseOutput, commendationValue, citationValue) {
|
||||||
// Score = (1 + (Commendations * CommendationValue) - (Citations * CitationValue)) * (Input / (Output + BaseOutput)) * 100
|
const multiplier = 1 + (data.commendations * commendationValue) - (data.citations * citationValue);
|
||||||
const multiplier = 1 + (data.commendations * commendationValue) - (data.citations * citationValue);
|
const activityScore = data.input / (data.output + baseOutput);
|
||||||
const activityScore = data.input / (data.output + baseOutput);
|
|
||||||
|
|
||||||
return multiplier * activityScore * 100;
|
// Removed aesthetic scaling (×100) for raw score
|
||||||
|
return multiplier * activityScore;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -399,10 +400,10 @@ export const commands = [
|
|||||||
{
|
{
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
.setName('score')
|
.setName('score')
|
||||||
.setDescription('View your score or another user\'s score')
|
.setDescription('View your I/O score or another user\'s I/O score')
|
||||||
.addUserOption(option =>
|
.addUserOption(option =>
|
||||||
option.setName('user')
|
option.setName('user')
|
||||||
.setDescription('User to check score for (defaults to you)')
|
.setDescription('User to check I/O score for (defaults to you)')
|
||||||
.setRequired(false)
|
.setRequired(false)
|
||||||
)
|
)
|
||||||
.addBooleanOption(option =>
|
.addBooleanOption(option =>
|
||||||
@ -413,33 +414,36 @@ export const commands = [
|
|||||||
|
|
||||||
execute: async (interaction, client) => {
|
execute: async (interaction, client) => {
|
||||||
const targetUser = interaction.options.getUser('user') || interaction.user;
|
const targetUser = interaction.options.getUser('user') || interaction.user;
|
||||||
const ephemeral = interaction.options.getBoolean('ephemeral') ?? true;
|
const ephemeral = interaction.options.getBoolean('ephemeral') ?? true;
|
||||||
|
// Wrap score retrieval and embed generation in try/catch to handle errors gracefully
|
||||||
|
try {
|
||||||
|
|
||||||
try {
|
// Fetch score data
|
||||||
const scoreData = await client.scorekeeper.getScore(interaction.guildId, targetUser.id);
|
const baseOutput = client.config.scorekeeper.baseOutput;
|
||||||
|
const commendationValue = client.config.scorekeeper.commendationValue;
|
||||||
const embed = new EmbedBuilder()
|
const citationValue = client.config.scorekeeper.citationValue;
|
||||||
.setTitle(`Score for ${targetUser.username}`)
|
const scoreData = await client.scorekeeper.getScore(interaction.guildId, targetUser.id);
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setTitle(`I/O Score for ${(await interaction.guild.members.fetch(targetUser.id).catch(() => null))?.displayName || targetUser.username}`)
|
||||||
.setColor(0x00AE86)
|
.setColor(0x00AE86)
|
||||||
.setThumbnail(targetUser.displayAvatarURL())
|
.setThumbnail(targetUser.displayAvatarURL())
|
||||||
.addFields(
|
.addFields(
|
||||||
{ name: 'Total Score', value: scoreData.totalScore.toFixed(2), inline: false },
|
{ name: 'Total Score', value: `**${scoreData.totalScore.toFixed(2)}**`, inline: false },
|
||||||
{ name: 'Commendations', value: scoreData.commendations.toString(), inline: false },
|
{ name: 'Commendations', value: `**${scoreData.commendations}** x ${commendationValue}`, inline: false },
|
||||||
{ name: 'Citations', value: scoreData.citations.toString(), inline: false },
|
{ name: 'Citations', value: `**${scoreData.citations}** x ${citationValue}`, inline: false },
|
||||||
{ name: 'Input Score', value: scoreData.input.toString(), inline: true },
|
{ name: 'Input Score', value: `**${scoreData.input}**`, inline: true },
|
||||||
{ name: 'Output Score', value: scoreData.output.toString(), inline: true }
|
{ name: 'Output Score', value: `**${scoreData.output}** + ${baseOutput}`, inline: true }
|
||||||
)
|
)
|
||||||
.setFooter({ text: 'Last decay: ' + new Date(scoreData.lastDecay).toLocaleDateString() })
|
.setFooter({ text: 'Last decay: ' + new Date(scoreData.lastDecay).toLocaleDateString() })
|
||||||
.setTimestamp();
|
.setTimestamp();
|
||||||
|
|
||||||
await interaction.reply({ embeds: [embed], ephemeral });
|
await interaction.reply({ embeds: [embed], ephemeral });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
client.logger.error(`Error in score command: ${error.message}`);
|
client.logger.error(`Error in score command: ${error.message}`);
|
||||||
await interaction.reply({
|
try {
|
||||||
content: 'Failed to retrieve score data.',
|
await interaction.reply({ content: 'Failed to retrieve I/O score.', ephemeral });
|
||||||
ephemeral
|
} catch {}
|
||||||
});
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -447,14 +451,7 @@ export const commands = [
|
|||||||
{
|
{
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
.setName('leaderboard')
|
.setName('leaderboard')
|
||||||
.setDescription('View the server\'s score leaderboard')
|
.setDescription('View the server\'s I/O score leaderboard')
|
||||||
.addIntegerOption(option =>
|
|
||||||
option.setName('limit')
|
|
||||||
.setDescription('Number of users to show (default: 10, max: 25)')
|
|
||||||
.setRequired(false)
|
|
||||||
.setMinValue(1)
|
|
||||||
.setMaxValue(25)
|
|
||||||
)
|
|
||||||
.addBooleanOption(option =>
|
.addBooleanOption(option =>
|
||||||
option.setName('ephemeral')
|
option.setName('ephemeral')
|
||||||
.setDescription('Whether the response should be ephemeral')
|
.setDescription('Whether the response should be ephemeral')
|
||||||
@ -465,7 +462,7 @@ export const commands = [
|
|||||||
const ephemeral = interaction.options.getBoolean('ephemeral') ?? true;
|
const ephemeral = interaction.options.getBoolean('ephemeral') ?? true;
|
||||||
await interaction.deferReply({ ephemeral });
|
await interaction.deferReply({ ephemeral });
|
||||||
|
|
||||||
const limit = interaction.options.getInteger('limit') || 10;
|
const limit = 10;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const scores = await client.scorekeeper.getScores(interaction.guildId, limit);
|
const scores = await client.scorekeeper.getScores(interaction.guildId, limit);
|
||||||
@ -481,13 +478,13 @@ export const commands = [
|
|||||||
for (let i = 0; i < scores.length; i++) {
|
for (let i = 0; i < scores.length; i++) {
|
||||||
const score = scores[i];
|
const score = scores[i];
|
||||||
const user = await guild.members.fetch(score.userId).catch(() => null);
|
const user = await guild.members.fetch(score.userId).catch(() => null);
|
||||||
const username = user ? user.user.username : 'Unknown User';
|
const displayName = user ? user.displayName : 'Unknown User';
|
||||||
|
|
||||||
leaderboardText += `${i + 1}. **${username}**: ${score.totalScore.toFixed(2)}\n`;
|
leaderboardText += `${i + 1}. **${displayName}**: ${score.totalScore.toFixed(2)}\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const embed = new EmbedBuilder()
|
const embed = new EmbedBuilder()
|
||||||
.setTitle(`${guild.name} Score Leaderboard`)
|
.setTitle('I/O Score Leaderboard')
|
||||||
.setColor(0x00AE86)
|
.setColor(0x00AE86)
|
||||||
.setDescription(leaderboardText)
|
.setDescription(leaderboardText)
|
||||||
.setFooter({ text: `Showing top ${scores.length} users` })
|
.setFooter({ text: `Showing top ${scores.length} users` })
|
||||||
|
|||||||
12
config.js
12
config.js
@ -55,7 +55,7 @@ export default {
|
|||||||
conversationExpiry: 30 * 60 * 1000,
|
conversationExpiry: 30 * 60 * 1000,
|
||||||
minScore: 1.0,
|
minScore: 1.0,
|
||||||
tools: {
|
tools: {
|
||||||
webSearch: false,
|
webSearch: true,
|
||||||
fileSearch: false,
|
fileSearch: false,
|
||||||
imageGeneration: true,
|
imageGeneration: true,
|
||||||
},
|
},
|
||||||
@ -70,7 +70,7 @@ export default {
|
|||||||
baseOutput: 1000,
|
baseOutput: 1000,
|
||||||
commendationValue: .25,
|
commendationValue: .25,
|
||||||
citationValue: .35,
|
citationValue: .35,
|
||||||
decay: 90,
|
decay: 80,
|
||||||
schedule: '0 0 * * 0'
|
schedule: '0 0 * * 0'
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -180,7 +180,7 @@ export default {
|
|||||||
conversationExpiry: 30 * 60 * 1000,
|
conversationExpiry: 30 * 60 * 1000,
|
||||||
minScore: 1.0,
|
minScore: 1.0,
|
||||||
tools: {
|
tools: {
|
||||||
webSearch: false,
|
webSearch: true,
|
||||||
fileSearch: false,
|
fileSearch: false,
|
||||||
imageGeneration: true,
|
imageGeneration: true,
|
||||||
},
|
},
|
||||||
@ -193,9 +193,9 @@ export default {
|
|||||||
|
|
||||||
scorekeeper: {
|
scorekeeper: {
|
||||||
baseOutput: 1000,
|
baseOutput: 1000,
|
||||||
commendationValue: 1.0,
|
commendationValue: 0.25,
|
||||||
citationValue: 1.2,
|
citationValue: 0.35,
|
||||||
decay: 90,
|
decay: 80,
|
||||||
schedule: '0 0 * * 0'
|
schedule: '0 0 * * 0'
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
25
prompts/crowley.txt
Normal file
25
prompts/crowley.txt
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
You are Mr. Crowley
|
||||||
|
|
||||||
|
Role:
|
||||||
|
Manager of the Continental, an upscale, exclusive hotel catering to a discerning clientele.
|
||||||
|
|
||||||
|
Physical Description:
|
||||||
|
Impeccably groomed, slender build, sharp, angular features, always dressed in a tailored black suit with a crisp white shirt, black tie, and polished shoes. Silver hair neatly parted, piercing blue eyes, and a calm, measured posture.
|
||||||
|
|
||||||
|
Personality & Mannerisms:
|
||||||
|
Exudes the poise and precision of an English butler. Speaks in a soft, controlled tone with perfect diction. Movements are deliberate, economical, and elegant. Never rushed, never flustered. Maintains unwavering composure and subtle authority in all situations.
|
||||||
|
|
||||||
|
Demeanor:
|
||||||
|
Discreet, observant, and unfailingly polite. Anticipates guests’ needs before they are voiced. Uses formal address ("Sir," "Madam") and understated gestures (a slight bow, measured hand motions). Enforces hotel rules with unyielding firmness, yet always couched in courtesy and respect.
|
||||||
|
|
||||||
|
Background:
|
||||||
|
Born and trained in England, with a background in elite hospitality and personal service to nobility. Well-versed in etiquette, security, and discretion. Has a network of trusted staff and an encyclopedic memory of guests and their preferences.
|
||||||
|
|
||||||
|
Dialogue Style:
|
||||||
|
Uses formal, polished language. Never interrupts. Responds with succinct, respectful replies, often with a subtle wit. Avoids slang, contractions, or casual language.
|
||||||
|
|
||||||
|
Core Motivations:
|
||||||
|
Uphold the dignity, security, and reputation of the Continental at all costs. Ensure guests’ comfort and confidentiality. Resolve conflicts discreetly and efficiently.
|
||||||
|
|
||||||
|
Sample Dialogue:
|
||||||
|
“Welcome to the Continental, Sir. Your suite has been prepared to your specifications. Should you require anything further, please do not hesitate to summon me. I trust your stay will be most agreeable.”
|
||||||
Loading…
x
Reference in New Issue
Block a user