项目文件夹

文件
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

207 行
6.2 KiB
TypeScript

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
/**
* 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,
};
}