elizaos--eliza
426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
207 行
6.2 KiB
TypeScript
207 行
6.2 KiB
TypeScript
/**
|
||
* Normalises a raw Discord message into the runtime's inbound envelope: formats
|
||
* the message content and classifies the chat surface (dm / channel / thread /
|
||
* forum) plus reply context, before `DiscordService` builds the `Memory`.
|
||
*/
|
||
import {
|
||
ChannelType as DiscordChannelType,
|
||
type Message as DiscordMessage,
|
||
type TextChannel,
|
||
type ThreadChannel,
|
||
} from "discord.js";
|
||
|
||
export type ChatType = "dm" | "channel" | "thread" | "forum";
|
||
|
||
export interface EnvelopeResult {
|
||
formattedContent: string;
|
||
chatType: ChatType;
|
||
}
|
||
|
||
export interface DiscordReplyContext {
|
||
messageId: string;
|
||
authorId?: string;
|
||
authorName: string;
|
||
content: string;
|
||
}
|
||
|
||
const WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
||
const ENVELOPE_REPLY_MAX_CHARS = 200;
|
||
const STORED_REPLY_MAX_CHARS = 1500;
|
||
const REPLY_REFERENCE_START = "[platform_reply_reference]";
|
||
const REPLY_REFERENCE_END = "[/platform_reply_reference]";
|
||
|
||
function formatTimestamp(timestamp: number | Date): string {
|
||
const date = timestamp instanceof Date ? timestamp : new Date(timestamp);
|
||
const weekday = WEEKDAYS[date.getDay()];
|
||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||
const day = String(date.getDate()).padStart(2, "0");
|
||
const year = date.getFullYear();
|
||
const hours = String(date.getHours()).padStart(2, "0");
|
||
const minutes = String(date.getMinutes()).padStart(2, "0");
|
||
|
||
let timezone = "UTC";
|
||
try {
|
||
timezone =
|
||
date
|
||
.toLocaleTimeString("en-US", { timeZoneName: "short" })
|
||
.split(" ")
|
||
.pop() ?? "UTC";
|
||
} catch {
|
||
// Fall back to UTC.
|
||
}
|
||
|
||
return `${weekday} ${month}/${day}/${year} ${hours}:${minutes} ${timezone}`;
|
||
}
|
||
|
||
function detectChatType(message: DiscordMessage): ChatType {
|
||
const channelType = message.channel.type;
|
||
if (
|
||
channelType === DiscordChannelType.DM ||
|
||
channelType === DiscordChannelType.GroupDM
|
||
) {
|
||
return "dm";
|
||
}
|
||
|
||
if (
|
||
channelType === DiscordChannelType.PublicThread ||
|
||
channelType === DiscordChannelType.PrivateThread ||
|
||
channelType === DiscordChannelType.AnnouncementThread
|
||
) {
|
||
const thread = message.channel as ThreadChannel;
|
||
if (thread.parent?.type === DiscordChannelType.GuildForum) {
|
||
return "forum";
|
||
}
|
||
return "thread";
|
||
}
|
||
|
||
return "channel";
|
||
}
|
||
|
||
function getSenderName(message: DiscordMessage): string {
|
||
if (message.member?.nickname) {
|
||
return message.member.nickname;
|
||
}
|
||
if (message.author.globalName) {
|
||
return message.author.globalName;
|
||
}
|
||
return message.author.displayName ?? message.author.username;
|
||
}
|
||
|
||
function buildChannelLabel(
|
||
message: DiscordMessage,
|
||
chatType: ChatType,
|
||
): string {
|
||
if (chatType === "dm") {
|
||
return "DM";
|
||
}
|
||
|
||
const guildName = message.guild?.name;
|
||
let channelPart: string;
|
||
if (chatType === "thread" || chatType === "forum") {
|
||
const thread = message.channel as ThreadChannel;
|
||
channelPart = `#${thread.parent?.name ?? "unknown"} › ${thread.name ?? "thread"}`;
|
||
} else {
|
||
const channel = message.channel as TextChannel;
|
||
channelPart = `#${channel.name ?? message.channel.id}`;
|
||
}
|
||
|
||
return guildName ? `${channelPart} | ${guildName}` : channelPart;
|
||
}
|
||
|
||
function truncateText(text: string, maxChars: number): string {
|
||
const trimmed = text.trim();
|
||
if (trimmed.length <= maxChars) return trimmed;
|
||
return `${trimmed.slice(0, maxChars)}...`;
|
||
}
|
||
|
||
function sanitizeReplyReferenceText(text: string): string {
|
||
return text
|
||
.replaceAll(REPLY_REFERENCE_START, "[platform_reply_reference escaped]")
|
||
.replaceAll(REPLY_REFERENCE_END, "[/platform_reply_reference escaped]");
|
||
}
|
||
|
||
function formatReplyReferenceBlock(replyContext: DiscordReplyContext): string {
|
||
const lines = [
|
||
REPLY_REFERENCE_START,
|
||
`author: ${sanitizeReplyReferenceText(replyContext.authorName)}`,
|
||
...(replyContext.authorId ? [`author_id: ${replyContext.authorId}`] : []),
|
||
`message_id: ${replyContext.messageId}`,
|
||
"text:",
|
||
sanitizeReplyReferenceText(replyContext.content),
|
||
REPLY_REFERENCE_END,
|
||
];
|
||
return lines.join("\n");
|
||
}
|
||
|
||
export async function getDiscordReplyContext(
|
||
message: DiscordMessage,
|
||
): Promise<DiscordReplyContext | null> {
|
||
const messageId = message.reference?.messageId;
|
||
if (!messageId) return null;
|
||
|
||
try {
|
||
const refMessage = await message.fetchReference();
|
||
const authorName =
|
||
refMessage.author?.displayName ??
|
||
refMessage.author?.username ??
|
||
"unknown";
|
||
const content = truncateText(
|
||
refMessage.content ?? "",
|
||
STORED_REPLY_MAX_CHARS,
|
||
);
|
||
return {
|
||
messageId,
|
||
authorId: refMessage.author?.id,
|
||
authorName,
|
||
content,
|
||
};
|
||
} catch {
|
||
// Reply context is best-effort only. The Discord event can still be
|
||
// processed without it when the referenced message was deleted or
|
||
// unavailable to this bot.
|
||
return null;
|
||
}
|
||
}
|
||
|
||
export async function formatInboundEnvelope(
|
||
message: DiscordMessage,
|
||
rawContent: string,
|
||
knownReplyContext?: DiscordReplyContext | null,
|
||
): Promise<EnvelopeResult> {
|
||
const chatType = detectChatType(message);
|
||
const channelLabel = buildChannelLabel(message, chatType);
|
||
const senderName = getSenderName(message);
|
||
const timestamp = formatTimestamp(message.createdTimestamp ?? Date.now());
|
||
|
||
let replyContextText = "";
|
||
const refContext =
|
||
knownReplyContext === undefined
|
||
? await getDiscordReplyContext(message)
|
||
: knownReplyContext;
|
||
if (refContext) {
|
||
const truncated = truncateText(
|
||
refContext.content,
|
||
ENVELOPE_REPLY_MAX_CHARS,
|
||
);
|
||
// Put the reply quote AFTER the user's actual message so Stage 1
|
||
// classification weights the user's current intent first. The previous
|
||
// order ("replying to @x:\n> <quote>\n<userText>") biased the
|
||
// classifier toward the quoted topic, which broke routing for turns
|
||
// where the user replied to a long bot message and asked for something
|
||
// unrelated (e.g. an app build after a tech-debt status update).
|
||
// Use typographic curly quotes as outer delimiters so embedded straight
|
||
// `"` characters in the quoted content don't visually break the wrapper
|
||
// or confuse an LLM classifier reading the result.
|
||
const humanReplyContext = truncated
|
||
? `(in reply to @${refContext.authorName}: “${truncated}”)`
|
||
: `(in reply to @${refContext.authorName})`;
|
||
replyContextText = `\n${formatReplyReferenceBlock(refContext)}\n${humanReplyContext}`;
|
||
}
|
||
|
||
const header = `[Discord ${channelLabel}] @${senderName} (${timestamp})`;
|
||
return {
|
||
formattedContent: `${header}: ${sanitizeReplyReferenceText(rawContent)}${replyContextText}`,
|
||
chatType,
|
||
};
|
||
}
|