Merge branch 'main' of https://github.com/moltbot/moltbot
This commit is contained in:
commit
e4875d5044
@ -80,6 +80,8 @@ Status: beta.
|
|||||||
- Agents: guard channel tool listActions to avoid plugin crashes. (#2859) Thanks @mbelinky.
|
- Agents: guard channel tool listActions to avoid plugin crashes. (#2859) Thanks @mbelinky.
|
||||||
- Providers: update MiniMax API endpoint and compatibility mode. (#3064) Thanks @hlbbbbbbb.
|
- Providers: update MiniMax API endpoint and compatibility mode. (#3064) Thanks @hlbbbbbbb.
|
||||||
- Telegram: treat more network errors as recoverable in polling. (#3013) Thanks @ryancontent.
|
- Telegram: treat more network errors as recoverable in polling. (#3013) Thanks @ryancontent.
|
||||||
|
- Discord: resolve usernames to user IDs for outbound messages. (#2649) Thanks @nonggialiang.
|
||||||
|
- Providers: update Moonshot Kimi model references to kimi-k2.5. (#2762) Thanks @MarvinCui.
|
||||||
- Gateway: suppress AbortError and transient network errors in unhandled rejections. (#2451) Thanks @Glucksberg.
|
- Gateway: suppress AbortError and transient network errors in unhandled rejections. (#2451) Thanks @Glucksberg.
|
||||||
- TTS: keep /tts status replies on text-only commands and avoid duplicate block-stream audio. (#2451) Thanks @Glucksberg.
|
- TTS: keep /tts status replies on text-only commands and avoid duplicate block-stream audio. (#2451) Thanks @Glucksberg.
|
||||||
- Security: pin npm overrides to keep tar@7.5.4 for install toolchains.
|
- Security: pin npm overrides to keep tar@7.5.4 for install toolchains.
|
||||||
|
|||||||
@ -130,9 +130,10 @@ Moonshot uses OpenAI-compatible endpoints, so configure it as a custom provider:
|
|||||||
|
|
||||||
- Provider: `moonshot`
|
- Provider: `moonshot`
|
||||||
- Auth: `MOONSHOT_API_KEY`
|
- Auth: `MOONSHOT_API_KEY`
|
||||||
- Example model: `moonshot/kimi-k2-0905-preview`
|
- Example model: `moonshot/kimi-k2.5`
|
||||||
- Kimi K2 model IDs:
|
- Kimi K2 model IDs:
|
||||||
{/* moonshot-kimi-k2-model-refs:start */}
|
{/* moonshot-kimi-k2-model-refs:start */}
|
||||||
|
- `moonshot/kimi-k2.5`
|
||||||
- `moonshot/kimi-k2-0905-preview`
|
- `moonshot/kimi-k2-0905-preview`
|
||||||
- `moonshot/kimi-k2-turbo-preview`
|
- `moonshot/kimi-k2-turbo-preview`
|
||||||
- `moonshot/kimi-k2-thinking`
|
- `moonshot/kimi-k2-thinking`
|
||||||
@ -141,7 +142,7 @@ Moonshot uses OpenAI-compatible endpoints, so configure it as a custom provider:
|
|||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
agents: {
|
agents: {
|
||||||
defaults: { model: { primary: "moonshot/kimi-k2-0905-preview" } }
|
defaults: { model: { primary: "moonshot/kimi-k2.5" } }
|
||||||
},
|
},
|
||||||
models: {
|
models: {
|
||||||
mode: "merge",
|
mode: "merge",
|
||||||
@ -150,7 +151,7 @@ Moonshot uses OpenAI-compatible endpoints, so configure it as a custom provider:
|
|||||||
baseUrl: "https://api.moonshot.ai/v1",
|
baseUrl: "https://api.moonshot.ai/v1",
|
||||||
apiKey: "${MOONSHOT_API_KEY}",
|
apiKey: "${MOONSHOT_API_KEY}",
|
||||||
api: "openai-completions",
|
api: "openai-completions",
|
||||||
models: [{ id: "kimi-k2-0905-preview", name: "Kimi K2 0905 Preview" }]
|
models: [{ id: "kimi-k2.5", name: "Kimi K2.5" }]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2396,8 +2396,8 @@ Use Moonshot's OpenAI-compatible endpoint:
|
|||||||
env: { MOONSHOT_API_KEY: "sk-..." },
|
env: { MOONSHOT_API_KEY: "sk-..." },
|
||||||
agents: {
|
agents: {
|
||||||
defaults: {
|
defaults: {
|
||||||
model: { primary: "moonshot/kimi-k2-0905-preview" },
|
model: { primary: "moonshot/kimi-k2.5" },
|
||||||
models: { "moonshot/kimi-k2-0905-preview": { alias: "Kimi K2" } }
|
models: { "moonshot/kimi-k2.5": { alias: "Kimi K2.5" } }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
models: {
|
models: {
|
||||||
@ -2409,8 +2409,8 @@ Use Moonshot's OpenAI-compatible endpoint:
|
|||||||
api: "openai-completions",
|
api: "openai-completions",
|
||||||
models: [
|
models: [
|
||||||
{
|
{
|
||||||
id: "kimi-k2-0905-preview",
|
id: "kimi-k2.5",
|
||||||
name: "Kimi K2 0905 Preview",
|
name: "Kimi K2.5",
|
||||||
reasoning: false,
|
reasoning: false,
|
||||||
input: ["text"],
|
input: ["text"],
|
||||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||||
@ -2426,7 +2426,7 @@ Use Moonshot's OpenAI-compatible endpoint:
|
|||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- Set `MOONSHOT_API_KEY` in the environment or use `moltbot onboard --auth-choice moonshot-api-key`.
|
- Set `MOONSHOT_API_KEY` in the environment or use `moltbot onboard --auth-choice moonshot-api-key`.
|
||||||
- Model ref: `moonshot/kimi-k2-0905-preview`.
|
- Model ref: `moonshot/kimi-k2.5`.
|
||||||
- Use `https://api.moonshot.cn/v1` if you need the China endpoint.
|
- Use `https://api.moonshot.cn/v1` if you need the China endpoint.
|
||||||
|
|
||||||
### Kimi Code
|
### Kimi Code
|
||||||
|
|||||||
@ -9,11 +9,12 @@ read_when:
|
|||||||
# Moonshot AI (Kimi)
|
# Moonshot AI (Kimi)
|
||||||
|
|
||||||
Moonshot provides the Kimi API with OpenAI-compatible endpoints. Configure the
|
Moonshot provides the Kimi API with OpenAI-compatible endpoints. Configure the
|
||||||
provider and set the default model to `moonshot/kimi-k2-0905-preview`, or use
|
provider and set the default model to `moonshot/kimi-k2.5`, or use
|
||||||
Kimi Code with `kimi-code/kimi-for-coding`.
|
Kimi Code with `kimi-code/kimi-for-coding`.
|
||||||
|
|
||||||
Current Kimi K2 model IDs:
|
Current Kimi K2 model IDs:
|
||||||
{/* moonshot-kimi-k2-ids:start */}
|
{/* moonshot-kimi-k2-ids:start */}
|
||||||
|
- `kimi-k2.5`
|
||||||
- `kimi-k2-0905-preview`
|
- `kimi-k2-0905-preview`
|
||||||
- `kimi-k2-turbo-preview`
|
- `kimi-k2-turbo-preview`
|
||||||
- `kimi-k2-thinking`
|
- `kimi-k2-thinking`
|
||||||
@ -39,9 +40,10 @@ Note: Moonshot and Kimi Code are separate providers. Keys are not interchangeabl
|
|||||||
env: { MOONSHOT_API_KEY: "sk-..." },
|
env: { MOONSHOT_API_KEY: "sk-..." },
|
||||||
agents: {
|
agents: {
|
||||||
defaults: {
|
defaults: {
|
||||||
model: { primary: "moonshot/kimi-k2-0905-preview" },
|
model: { primary: "moonshot/kimi-k2.5" },
|
||||||
models: {
|
models: {
|
||||||
// moonshot-kimi-k2-aliases:start
|
// moonshot-kimi-k2-aliases:start
|
||||||
|
"moonshot/kimi-k2.5": { alias: "Kimi K2.5" },
|
||||||
"moonshot/kimi-k2-0905-preview": { alias: "Kimi K2" },
|
"moonshot/kimi-k2-0905-preview": { alias: "Kimi K2" },
|
||||||
"moonshot/kimi-k2-turbo-preview": { alias: "Kimi K2 Turbo" },
|
"moonshot/kimi-k2-turbo-preview": { alias: "Kimi K2 Turbo" },
|
||||||
"moonshot/kimi-k2-thinking": { alias: "Kimi K2 Thinking" },
|
"moonshot/kimi-k2-thinking": { alias: "Kimi K2 Thinking" },
|
||||||
@ -59,6 +61,15 @@ Note: Moonshot and Kimi Code are separate providers. Keys are not interchangeabl
|
|||||||
api: "openai-completions",
|
api: "openai-completions",
|
||||||
models: [
|
models: [
|
||||||
// moonshot-kimi-k2-models:start
|
// moonshot-kimi-k2-models:start
|
||||||
|
{
|
||||||
|
id: "kimi-k2.5",
|
||||||
|
name: "Kimi K2.5",
|
||||||
|
reasoning: false,
|
||||||
|
input: ["text"],
|
||||||
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||||
|
contextWindow: 256000,
|
||||||
|
maxTokens: 8192
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "kimi-k2-0905-preview",
|
id: "kimi-k2-0905-preview",
|
||||||
name: "Kimi K2 0905 Preview",
|
name: "Kimi K2 0905 Preview",
|
||||||
|
|||||||
@ -31,7 +31,7 @@ const MINIMAX_API_COST = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const MOONSHOT_BASE_URL = "https://api.moonshot.ai/v1";
|
const MOONSHOT_BASE_URL = "https://api.moonshot.ai/v1";
|
||||||
const MOONSHOT_DEFAULT_MODEL_ID = "kimi-k2-0905-preview";
|
const MOONSHOT_DEFAULT_MODEL_ID = "kimi-k2.5";
|
||||||
const MOONSHOT_DEFAULT_CONTEXT_WINDOW = 256000;
|
const MOONSHOT_DEFAULT_CONTEXT_WINDOW = 256000;
|
||||||
const MOONSHOT_DEFAULT_MAX_TOKENS = 8192;
|
const MOONSHOT_DEFAULT_MAX_TOKENS = 8192;
|
||||||
const MOONSHOT_DEFAULT_COST = {
|
const MOONSHOT_DEFAULT_COST = {
|
||||||
@ -275,7 +275,7 @@ function buildMoonshotProvider(): ProviderConfig {
|
|||||||
models: [
|
models: [
|
||||||
{
|
{
|
||||||
id: MOONSHOT_DEFAULT_MODEL_ID,
|
id: MOONSHOT_DEFAULT_MODEL_ID,
|
||||||
name: "Kimi K2 0905 Preview",
|
name: "Kimi K2.5",
|
||||||
reasoning: false,
|
reasoning: false,
|
||||||
input: ["text"],
|
input: ["text"],
|
||||||
cost: MOONSHOT_DEFAULT_COST,
|
cost: MOONSHOT_DEFAULT_COST,
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import {
|
|||||||
createDiscordClient,
|
createDiscordClient,
|
||||||
normalizeDiscordPollInput,
|
normalizeDiscordPollInput,
|
||||||
normalizeStickerIds,
|
normalizeStickerIds,
|
||||||
parseRecipient,
|
parseAndResolveRecipient,
|
||||||
resolveChannelId,
|
resolveChannelId,
|
||||||
sendDiscordMedia,
|
sendDiscordMedia,
|
||||||
sendDiscordText,
|
sendDiscordText,
|
||||||
@ -49,7 +49,7 @@ export async function sendMessageDiscord(
|
|||||||
const chunkMode = resolveChunkMode(cfg, "discord", accountInfo.accountId);
|
const chunkMode = resolveChunkMode(cfg, "discord", accountInfo.accountId);
|
||||||
const textWithTables = convertMarkdownTables(text ?? "", tableMode);
|
const textWithTables = convertMarkdownTables(text ?? "", tableMode);
|
||||||
const { token, rest, request } = createDiscordClient(opts, cfg);
|
const { token, rest, request } = createDiscordClient(opts, cfg);
|
||||||
const recipient = parseRecipient(to);
|
const recipient = await parseAndResolveRecipient(to, opts.accountId);
|
||||||
const { channelId } = await resolveChannelId(rest, recipient, request);
|
const { channelId } = await resolveChannelId(rest, recipient, request);
|
||||||
let result: { id: string; channel_id: string } | { id: string | null; channel_id: string };
|
let result: { id: string; channel_id: string } | { id: string | null; channel_id: string };
|
||||||
try {
|
try {
|
||||||
@ -104,7 +104,7 @@ export async function sendStickerDiscord(
|
|||||||
): Promise<DiscordSendResult> {
|
): Promise<DiscordSendResult> {
|
||||||
const cfg = loadConfig();
|
const cfg = loadConfig();
|
||||||
const { rest, request } = createDiscordClient(opts, cfg);
|
const { rest, request } = createDiscordClient(opts, cfg);
|
||||||
const recipient = parseRecipient(to);
|
const recipient = await parseAndResolveRecipient(to, opts.accountId);
|
||||||
const { channelId } = await resolveChannelId(rest, recipient, request);
|
const { channelId } = await resolveChannelId(rest, recipient, request);
|
||||||
const content = opts.content?.trim();
|
const content = opts.content?.trim();
|
||||||
const stickers = normalizeStickerIds(stickerIds);
|
const stickers = normalizeStickerIds(stickerIds);
|
||||||
@ -131,7 +131,7 @@ export async function sendPollDiscord(
|
|||||||
): Promise<DiscordSendResult> {
|
): Promise<DiscordSendResult> {
|
||||||
const cfg = loadConfig();
|
const cfg = loadConfig();
|
||||||
const { rest, request } = createDiscordClient(opts, cfg);
|
const { rest, request } = createDiscordClient(opts, cfg);
|
||||||
const recipient = parseRecipient(to);
|
const recipient = await parseAndResolveRecipient(to, opts.accountId);
|
||||||
const { channelId } = await resolveChannelId(rest, recipient, request);
|
const { channelId } = await resolveChannelId(rest, recipient, request);
|
||||||
const content = opts.content?.trim();
|
const content = opts.content?.trim();
|
||||||
const payload = normalizeDiscordPollInput(poll);
|
const payload = normalizeDiscordPollInput(poll);
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import type { ChunkMode } from "../auto-reply/chunk.js";
|
|||||||
import { chunkDiscordTextWithMode } from "./chunk.js";
|
import { chunkDiscordTextWithMode } from "./chunk.js";
|
||||||
import { fetchChannelPermissionsDiscord, isThreadChannelType } from "./send.permissions.js";
|
import { fetchChannelPermissionsDiscord, isThreadChannelType } from "./send.permissions.js";
|
||||||
import { DiscordSendError } from "./send.types.js";
|
import { DiscordSendError } from "./send.types.js";
|
||||||
import { parseDiscordTarget } from "./targets.js";
|
import { parseDiscordTarget, resolveDiscordTarget } from "./targets.js";
|
||||||
import { normalizeDiscordToken } from "./token.js";
|
import { normalizeDiscordToken } from "./token.js";
|
||||||
|
|
||||||
const DISCORD_TEXT_LIMIT = 2000;
|
const DISCORD_TEXT_LIMIT = 2000;
|
||||||
@ -101,6 +101,44 @@ function parseRecipient(raw: string): DiscordRecipient {
|
|||||||
return { kind: target.kind, id: target.id };
|
return { kind: target.kind, id: target.id };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse and resolve Discord recipient, including username lookup.
|
||||||
|
* This enables sending DMs by username (e.g., "john.doe") by querying
|
||||||
|
* the Discord directory to resolve usernames to user IDs.
|
||||||
|
*
|
||||||
|
* @param raw - The recipient string (username, ID, or known format)
|
||||||
|
* @param accountId - Discord account ID to use for directory lookup
|
||||||
|
* @returns Parsed DiscordRecipient with resolved user ID if applicable
|
||||||
|
*/
|
||||||
|
export async function parseAndResolveRecipient(
|
||||||
|
raw: string,
|
||||||
|
accountId?: string,
|
||||||
|
): Promise<DiscordRecipient> {
|
||||||
|
const cfg = loadConfig();
|
||||||
|
const accountInfo = resolveDiscordAccount({ cfg, accountId });
|
||||||
|
|
||||||
|
// First try to resolve using directory lookup (handles usernames)
|
||||||
|
const resolved = await resolveDiscordTarget(raw, {
|
||||||
|
cfg,
|
||||||
|
accountId: accountInfo.accountId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (resolved) {
|
||||||
|
return { kind: resolved.kind, id: resolved.id };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to standard parsing (for channels, etc.)
|
||||||
|
const parsed = parseDiscordTarget(raw, {
|
||||||
|
ambiguousMessage: `Ambiguous Discord recipient "${raw.trim()}". Use "user:${raw.trim()}" for DMs or "channel:${raw.trim()}" for channel messages.`,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!parsed) {
|
||||||
|
throw new Error("Recipient is required for Discord sends");
|
||||||
|
}
|
||||||
|
|
||||||
|
return { kind: parsed.kind, id: parsed.id };
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeStickerIds(raw: string[]) {
|
function normalizeStickerIds(raw: string[]) {
|
||||||
const ids = raw.map((entry) => entry.trim()).filter(Boolean);
|
const ids = raw.map((entry) => entry.trim()).filter(Boolean);
|
||||||
if (ids.length === 0) {
|
if (ids.length === 0) {
|
||||||
|
|||||||
@ -1,7 +1,13 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import type { ClawdbotConfig } from "../config/config.js";
|
||||||
import { normalizeDiscordMessagingTarget } from "../channels/plugins/normalize/discord.js";
|
import { normalizeDiscordMessagingTarget } from "../channels/plugins/normalize/discord.js";
|
||||||
import { parseDiscordTarget, resolveDiscordChannelId } from "./targets.js";
|
import { listDiscordDirectoryPeersLive } from "./directory-live.js";
|
||||||
|
import { parseDiscordTarget, resolveDiscordChannelId, resolveDiscordTarget } from "./targets.js";
|
||||||
|
|
||||||
|
vi.mock("./directory-live.js", () => ({
|
||||||
|
listDiscordDirectoryPeersLive: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
describe("parseDiscordTarget", () => {
|
describe("parseDiscordTarget", () => {
|
||||||
it("parses user mention and prefixes", () => {
|
it("parses user mention and prefixes", () => {
|
||||||
@ -68,6 +74,38 @@ describe("resolveDiscordChannelId", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("resolveDiscordTarget", () => {
|
||||||
|
const cfg = { channels: { discord: {} } } as ClawdbotConfig;
|
||||||
|
const listPeers = vi.mocked(listDiscordDirectoryPeersLive);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
listPeers.mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns a resolved user for usernames", async () => {
|
||||||
|
listPeers.mockResolvedValueOnce([{ kind: "user", id: "user:999", name: "Jane" } as const]);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
resolveDiscordTarget("jane", { cfg, accountId: "default" }),
|
||||||
|
).resolves.toMatchObject({ kind: "user", id: "999", normalized: "user:999" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to parsing when lookup misses", async () => {
|
||||||
|
listPeers.mockResolvedValueOnce([]);
|
||||||
|
await expect(
|
||||||
|
resolveDiscordTarget("general", { cfg, accountId: "default" }),
|
||||||
|
).resolves.toMatchObject({ kind: "channel", id: "general" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not call directory lookup for explicit user ids", async () => {
|
||||||
|
listPeers.mockResolvedValueOnce([]);
|
||||||
|
await expect(
|
||||||
|
resolveDiscordTarget("user:123", { cfg, accountId: "default" }),
|
||||||
|
).resolves.toMatchObject({ kind: "user", id: "123" });
|
||||||
|
expect(listPeers).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("normalizeDiscordMessagingTarget", () => {
|
describe("normalizeDiscordMessagingTarget", () => {
|
||||||
it("defaults raw numeric ids to channels", () => {
|
it("defaults raw numeric ids to channels", () => {
|
||||||
expect(normalizeDiscordMessagingTarget("123")).toBe("channel:123");
|
expect(normalizeDiscordMessagingTarget("123")).toBe("channel:123");
|
||||||
|
|||||||
@ -5,8 +5,13 @@ import {
|
|||||||
type MessagingTarget,
|
type MessagingTarget,
|
||||||
type MessagingTargetKind,
|
type MessagingTargetKind,
|
||||||
type MessagingTargetParseOptions,
|
type MessagingTargetParseOptions,
|
||||||
|
type DirectoryConfigParams,
|
||||||
|
type ChannelDirectoryEntry,
|
||||||
} from "../channels/targets.js";
|
} from "../channels/targets.js";
|
||||||
|
|
||||||
|
import { listDiscordDirectoryPeersLive } from "./directory-live.js";
|
||||||
|
import { resolveDiscordAccount } from "./accounts.js";
|
||||||
|
|
||||||
export type DiscordTargetKind = MessagingTargetKind;
|
export type DiscordTargetKind = MessagingTargetKind;
|
||||||
|
|
||||||
export type DiscordTarget = MessagingTarget;
|
export type DiscordTarget = MessagingTarget;
|
||||||
@ -60,3 +65,60 @@ export function resolveDiscordChannelId(raw: string): string {
|
|||||||
const target = parseDiscordTarget(raw, { defaultKind: "channel" });
|
const target = parseDiscordTarget(raw, { defaultKind: "channel" });
|
||||||
return requireTargetKind({ platform: "Discord", target, kind: "channel" });
|
return requireTargetKind({ platform: "Discord", target, kind: "channel" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a Discord username to user ID using the directory lookup.
|
||||||
|
* This enables sending DMs by username instead of requiring explicit user IDs.
|
||||||
|
*
|
||||||
|
* @param raw - The username or raw target string (e.g., "john.doe")
|
||||||
|
* @param options - Directory configuration params (cfg, accountId, limit)
|
||||||
|
* @returns Parsed MessagingTarget with user ID, or undefined if not found
|
||||||
|
*/
|
||||||
|
export async function resolveDiscordTarget(
|
||||||
|
raw: string,
|
||||||
|
options: DirectoryConfigParams,
|
||||||
|
): Promise<MessagingTarget | undefined> {
|
||||||
|
const trimmed = raw.trim();
|
||||||
|
if (!trimmed) return undefined;
|
||||||
|
|
||||||
|
// If already a known format, parse directly
|
||||||
|
const directParse = parseDiscordTarget(trimmed, options);
|
||||||
|
if (directParse && directParse.kind !== "channel" && !isLikelyUsername(trimmed)) {
|
||||||
|
return directParse;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to resolve as a username via directory lookup
|
||||||
|
try {
|
||||||
|
const directoryEntries = await listDiscordDirectoryPeersLive({
|
||||||
|
...options,
|
||||||
|
query: trimmed,
|
||||||
|
limit: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const match = directoryEntries[0];
|
||||||
|
if (match && match.kind === "user") {
|
||||||
|
// Extract user ID from the directory entry (format: "user:<id>")
|
||||||
|
const userId = match.id.replace(/^user:/, "");
|
||||||
|
return buildMessagingTarget("user", userId, trimmed);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Directory lookup failed - fall through to parse as-is
|
||||||
|
// This preserves existing behavior for channel names
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to original parsing (for channels, etc.)
|
||||||
|
return parseDiscordTarget(trimmed, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a string looks like a Discord username (not a mention, prefix, or ID).
|
||||||
|
* Usernames typically don't start with special characters except underscore.
|
||||||
|
*/
|
||||||
|
function isLikelyUsername(input: string): boolean {
|
||||||
|
// Skip if it's already a known format
|
||||||
|
if (/^(user:|channel:|discord:|@|<@!?)|[\d]+$/.test(input)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Likely a username if it doesn't match known patterns
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user