@zhin.js/adapter-discord 7.0.13 → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +55 -0
- package/README.md +1 -1
- package/adapters/discord.js +8 -2
- package/adapters/discord.ts +8 -2
- package/lib/endpoint.d.ts +7 -2
- package/lib/endpoint.js +64 -0
- package/lib/gateway.d.ts +10 -0
- package/lib/gateway.js +24 -0
- package/lib/protocol.d.ts +1 -1
- package/lib/protocol.js +5 -15
- package/lib/side-event-dispatch.d.ts +8 -0
- package/lib/side-event-dispatch.js +24 -0
- package/package.json +14 -14
- package/src/endpoint.ts +88 -1
- package/src/gateway.ts +32 -0
- package/src/protocol.ts +5 -14
- package/src/side-event-dispatch.ts +37 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,60 @@
|
|
|
1
1
|
# @zhin.js/adapter-discord
|
|
2
2
|
|
|
3
|
+
## 8.0.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- f2c532f: Expose exact per-Endpoint message operations through one validated Adapter capability model, route Core control calls through declared active capabilities, and connect existing recall, edit, reaction, and typing implementations across platform adapters.
|
|
8
|
+
- Updated dependencies [b10d058]
|
|
9
|
+
- Updated dependencies [f2c532f]
|
|
10
|
+
- Updated dependencies [3dbf990]
|
|
11
|
+
- @zhin.js/host-http@1.0.12
|
|
12
|
+
- @zhin.js/adapter@1.2.0
|
|
13
|
+
- @zhin.js/core@1.5.13
|
|
14
|
+
- @zhin.js/agent@1.1.15
|
|
15
|
+
- zhin.js@6.0.13
|
|
16
|
+
|
|
17
|
+
## 7.0.14
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- 5969c5b: Remove legacy compound-string message targets and Endpoint control probing. Endpoint control and outbound Host operations now carry structured `MessageRef` identities. Endpoint send has one exact result contract (a platform message id), which IM Runtime projects into `DeliveryReceipt.message`; arbitrary result guessing is removed.
|
|
22
|
+
- 9a64283: Add canonical scoped Discord message resolution with structured attachment media references.
|
|
23
|
+
- 974772e: Replace the user-facing `Prompt` vocabulary with the `UserInteraction` authoring surface for input, confirmation, and selection. Commands and handlers now expose `interaction`; IM Runtime exposes `createInteraction`; schema-driven endpoint collection is named `SchemaInteraction`. The old prompt-named interaction types and properties are removed rather than aliased. User interactions render through one canonical Markdown and keyboard/list presentation module shared by commands and Agent `ask_user` turns.
|
|
24
|
+
|
|
25
|
+
Extract the transport-neutral interaction contract into `@zhin.js/interaction`. A discriminated `ask()` API supports text, number, confirmation, single-select, multi-select, and typed lists with structured `title`, `description`, and `tip` content. Typed `sequence()` interactions return one result object keyed by step id, render progress, and retry invalid replies without leaking invalid values to callers.
|
|
26
|
+
|
|
27
|
+
Preserve AI Markdown and card command actions through outbound publishing. QQ delivers Markdown with native command buttons; KOOK, Discord, Telegram, DingTalk, and Lark/Feishu now declare and encode their native Markdown dialects while retaining each adapter's interaction policy. Correct QQ callback button action encoding and button style mapping.
|
|
28
|
+
|
|
29
|
+
- 5969c5b: Add SideEventGateway so adapters forward notice/request/system into HandlerIndex. HandlerContext now exposes only generation-safe capabilities and prompt ports; live Endpoint escape hatches are removed.
|
|
30
|
+
- Updated dependencies [5969c5b]
|
|
31
|
+
- Updated dependencies [d336a3f]
|
|
32
|
+
- Updated dependencies [0c82a7e]
|
|
33
|
+
- Updated dependencies [b9217e4]
|
|
34
|
+
- Updated dependencies [5969c5b]
|
|
35
|
+
- Updated dependencies [5969c5b]
|
|
36
|
+
- Updated dependencies [974772e]
|
|
37
|
+
- Updated dependencies [5969c5b]
|
|
38
|
+
- Updated dependencies [5969c5b]
|
|
39
|
+
- Updated dependencies [2f786bd]
|
|
40
|
+
- Updated dependencies [63d89f9]
|
|
41
|
+
- Updated dependencies [71c7cdd]
|
|
42
|
+
- Updated dependencies [3cca0ea]
|
|
43
|
+
- Updated dependencies [1312ca0]
|
|
44
|
+
- Updated dependencies [985fa22]
|
|
45
|
+
- Updated dependencies [04b861d]
|
|
46
|
+
- Updated dependencies [a23d544]
|
|
47
|
+
- Updated dependencies [8cddabf]
|
|
48
|
+
- Updated dependencies [dbe5081]
|
|
49
|
+
- @zhin.js/im-contract@1.0.4
|
|
50
|
+
- @zhin.js/core@1.5.12
|
|
51
|
+
- @zhin.js/adapter@1.1.11
|
|
52
|
+
- @zhin.js/agent@1.1.14
|
|
53
|
+
- @zhin.js/host-http@1.0.11
|
|
54
|
+
- @zhin.js/command@1.0.15
|
|
55
|
+
- zhin.js@6.0.12
|
|
56
|
+
- @zhin.js/permission@1.0.3
|
|
57
|
+
|
|
3
58
|
## 7.0.13
|
|
4
59
|
|
|
5
60
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ Zhin.js Discord 适配器(Plugin Runtime),默认通过 **Gateway WebSocket
|
|
|
7
7
|
- Gateway WebSocket 入站(默认;无需公网 HTTPS / host)
|
|
8
8
|
- 解析 text / mention / attachment / embed / sticker / button
|
|
9
9
|
- 支持私聊、群组与服务器频道
|
|
10
|
-
- 出站 `send({ conversation, payload })` → Discord channel message(
|
|
10
|
+
- 出站 `send({ conversation, payload })` → Discord channel message(Markdown content / media / embed / keyboard)
|
|
11
11
|
- 约定式 `defineAdapter` / `definePlugin`(无需 `usePlugin`)
|
|
12
12
|
- Interactions HTTP webhook 延期(需 `httpHostToken`);配置 `connection: interactions` 会明确报错
|
|
13
13
|
|
package/adapters/discord.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Convention entry: discover `adapters/discord.ts` → defineAdapter.
|
|
4
4
|
*/
|
|
5
5
|
import { defineAdapter } from 'zhin.js/adapter';
|
|
6
|
-
import { messageGatewayToken } from '@zhin.js/core/runtime';
|
|
6
|
+
import { messageGatewayToken, sideEventGatewayToken } from '@zhin.js/core/runtime';
|
|
7
7
|
import { httpHostToken } from '@zhin.js/host-http';
|
|
8
8
|
import { DiscordGatewayEndpoint, DiscordInteractionsEndpoint, } from "../lib/endpoint.js";
|
|
9
9
|
import { resolveDiscordConfig, } from "../lib/protocol.js";
|
|
@@ -11,15 +11,19 @@ import { discordRuntimeStateToken } from "../lib/discord-runtime-state.js";
|
|
|
11
11
|
export { DiscordGatewayEndpoint, DiscordInteractionsEndpoint, } from "../lib/endpoint.js";
|
|
12
12
|
export default defineAdapter({
|
|
13
13
|
capabilities: ['inbound', 'outbound'],
|
|
14
|
-
operations:
|
|
14
|
+
operations: (context) => context.config.connection === 'interactions'
|
|
15
|
+
? ['recall']
|
|
16
|
+
: ['recall', 'reaction'],
|
|
15
17
|
// 媒体 url 直发或由 AttachmentBuilder 物化本地文件上传;message components 原生按钮承载交互段。
|
|
16
18
|
segments: {
|
|
17
19
|
outboundMedia: ['url', 'upload'],
|
|
18
20
|
interactive: 'native',
|
|
21
|
+
markdown: 'native',
|
|
19
22
|
},
|
|
20
23
|
create(context) {
|
|
21
24
|
const config = resolveDiscordConfig(context.config);
|
|
22
25
|
const gateway = context.use(messageGatewayToken);
|
|
26
|
+
const sideEvents = context.use(sideEventGatewayToken);
|
|
23
27
|
// 注册到插件运行时状态(discord.endpoint list 的"运行中"数据源)
|
|
24
28
|
context.use(discordRuntimeStateToken).endpoints.set(config.id, {
|
|
25
29
|
id: config.id,
|
|
@@ -29,6 +33,7 @@ export default defineAdapter({
|
|
|
29
33
|
return new DiscordInteractionsEndpoint({
|
|
30
34
|
id: context.id,
|
|
31
35
|
gateway,
|
|
36
|
+
sideEvents,
|
|
32
37
|
http: context.use(httpHostToken),
|
|
33
38
|
config,
|
|
34
39
|
});
|
|
@@ -36,6 +41,7 @@ export default defineAdapter({
|
|
|
36
41
|
return new DiscordGatewayEndpoint({
|
|
37
42
|
id: context.id,
|
|
38
43
|
gateway,
|
|
44
|
+
sideEvents,
|
|
39
45
|
config,
|
|
40
46
|
});
|
|
41
47
|
},
|
package/adapters/discord.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Convention entry: discover `adapters/discord.ts` → defineAdapter.
|
|
3
3
|
*/
|
|
4
4
|
import { defineAdapter } from 'zhin.js/adapter';
|
|
5
|
-
import { messageGatewayToken } from '@zhin.js/core/runtime';
|
|
5
|
+
import { messageGatewayToken, sideEventGatewayToken } from '@zhin.js/core/runtime';
|
|
6
6
|
import { httpHostToken } from '@zhin.js/host-http';
|
|
7
7
|
import {
|
|
8
8
|
DiscordGatewayEndpoint,
|
|
@@ -27,15 +27,19 @@ export type {
|
|
|
27
27
|
|
|
28
28
|
export default defineAdapter<DiscordAdapterConfig>({
|
|
29
29
|
capabilities: ['inbound', 'outbound'],
|
|
30
|
-
operations:
|
|
30
|
+
operations: (context) => context.config.connection === 'interactions'
|
|
31
|
+
? ['recall']
|
|
32
|
+
: ['recall', 'reaction'],
|
|
31
33
|
// 媒体 url 直发或由 AttachmentBuilder 物化本地文件上传;message components 原生按钮承载交互段。
|
|
32
34
|
segments: {
|
|
33
35
|
outboundMedia: ['url', 'upload'],
|
|
34
36
|
interactive: 'native',
|
|
37
|
+
markdown: 'native',
|
|
35
38
|
},
|
|
36
39
|
create(context) {
|
|
37
40
|
const config = resolveDiscordConfig(context.config);
|
|
38
41
|
const gateway = context.use(messageGatewayToken);
|
|
42
|
+
const sideEvents = context.use(sideEventGatewayToken);
|
|
39
43
|
// 注册到插件运行时状态(discord.endpoint list 的"运行中"数据源)
|
|
40
44
|
context.use(discordRuntimeStateToken).endpoints.set(config.id, {
|
|
41
45
|
id: config.id,
|
|
@@ -45,6 +49,7 @@ export default defineAdapter<DiscordAdapterConfig>({
|
|
|
45
49
|
return new DiscordInteractionsEndpoint({
|
|
46
50
|
id: context.id,
|
|
47
51
|
gateway,
|
|
52
|
+
sideEvents,
|
|
48
53
|
http: context.use(httpHostToken),
|
|
49
54
|
config,
|
|
50
55
|
});
|
|
@@ -52,6 +57,7 @@ export default defineAdapter<DiscordAdapterConfig>({
|
|
|
52
57
|
return new DiscordGatewayEndpoint({
|
|
53
58
|
id: context.id,
|
|
54
59
|
gateway,
|
|
60
|
+
sideEvents,
|
|
55
61
|
config,
|
|
56
62
|
});
|
|
57
63
|
},
|
package/lib/endpoint.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { EndpointControl, EndpointInstance, EndpointManagement, EndpointSendRequest } from 'zhin.js/adapter';
|
|
2
|
-
import type { MessageGateway } from '@zhin.js/core/runtime';
|
|
1
|
+
import type { EndpointContentPort, EndpointControl, EndpointInstance, EndpointManagement, EndpointSendRequest } from 'zhin.js/adapter';
|
|
2
|
+
import type { MessageGateway, SideEventGateway } from '@zhin.js/core/runtime';
|
|
3
3
|
import type { HttpHost } from '@zhin.js/host-http';
|
|
4
4
|
import { type MessageRef } from '@zhin.js/im-contract';
|
|
5
5
|
import type { CapabilityId } from 'zhin.js';
|
|
@@ -9,13 +9,16 @@ export type { CreateDiscordClient, DiscordClientTransport, } from './gateway.js'
|
|
|
9
9
|
export interface DiscordEndpointOptions {
|
|
10
10
|
readonly id: CapabilityId;
|
|
11
11
|
readonly gateway: MessageGateway;
|
|
12
|
+
readonly sideEvents?: SideEventGateway;
|
|
12
13
|
readonly config: ResolvedDiscordGatewayConfig;
|
|
13
14
|
readonly createClient?: CreateDiscordClient;
|
|
15
|
+
readonly fetch?: typeof globalThis.fetch;
|
|
14
16
|
}
|
|
15
17
|
export declare class DiscordGatewayEndpoint implements EndpointInstance {
|
|
16
18
|
#private;
|
|
17
19
|
readonly management: EndpointManagement;
|
|
18
20
|
readonly control: EndpointControl;
|
|
21
|
+
readonly content: EndpointContentPort;
|
|
19
22
|
constructor(options: DiscordEndpointOptions);
|
|
20
23
|
start(): Promise<void>;
|
|
21
24
|
open(): void;
|
|
@@ -51,6 +54,7 @@ export declare class DiscordGatewayEndpoint implements EndpointInstance {
|
|
|
51
54
|
export interface DiscordInteractionsEndpointOptions {
|
|
52
55
|
readonly id: CapabilityId;
|
|
53
56
|
readonly gateway: MessageGateway;
|
|
57
|
+
readonly sideEvents?: SideEventGateway;
|
|
54
58
|
readonly http: HttpHost;
|
|
55
59
|
readonly config: ResolvedDiscordInteractionsConfig;
|
|
56
60
|
readonly fetch?: typeof globalThis.fetch;
|
|
@@ -58,6 +62,7 @@ export interface DiscordInteractionsEndpointOptions {
|
|
|
58
62
|
export declare class DiscordInteractionsEndpoint implements EndpointInstance {
|
|
59
63
|
#private;
|
|
60
64
|
readonly control: EndpointControl;
|
|
65
|
+
readonly content: EndpointContentPort;
|
|
61
66
|
constructor(options: DiscordInteractionsEndpointOptions);
|
|
62
67
|
get isOpen(): boolean;
|
|
63
68
|
get config(): ResolvedDiscordInteractionsConfig;
|
package/lib/endpoint.js
CHANGED
|
@@ -7,6 +7,7 @@ import { registerDiscordAgentEndpoint } from './discord-agent-deps.js';
|
|
|
7
7
|
import { connectDiscordGatewayClient, defaultCreateClient, DEFAULT_INTENTS, resolveSenderRole, toMessageCreateOptions, } from './gateway.js';
|
|
8
8
|
import { discordInboundConversation, formatButtonContent, formatButtonSegments, formatInboundContent, formatInboundSegments, formatOutboundBody, senderDisplayName, } from './protocol.js';
|
|
9
9
|
import { registerDiscordInteractionRoutes } from './webhook.js';
|
|
10
|
+
import { receiveDiscordGuildMemberSideEvent } from './side-event-dispatch.js';
|
|
10
11
|
const DISCORD_API = 'https://discord.com/api/v10';
|
|
11
12
|
/** 出站 HTTP 调用统一 30s 超时。 */
|
|
12
13
|
const OUTBOUND_TIMEOUT_MS = 30_000;
|
|
@@ -14,6 +15,7 @@ export class DiscordGatewayEndpoint {
|
|
|
14
15
|
#logger;
|
|
15
16
|
#options;
|
|
16
17
|
#createClient;
|
|
18
|
+
#fetch;
|
|
17
19
|
#client = null;
|
|
18
20
|
#open = false;
|
|
19
21
|
#started = false;
|
|
@@ -32,10 +34,14 @@ export class DiscordGatewayEndpoint {
|
|
|
32
34
|
return emoji;
|
|
33
35
|
},
|
|
34
36
|
});
|
|
37
|
+
content = Object.freeze({
|
|
38
|
+
resolve: (reference, context) => resolveDiscordContent(this.#fetch, this.#options.config.token, reference, context),
|
|
39
|
+
});
|
|
35
40
|
constructor(options) {
|
|
36
41
|
this.#logger = getAdapterLogger('discord', options.config.id);
|
|
37
42
|
this.#options = options;
|
|
38
43
|
this.#createClient = options.createClient ?? defaultCreateClient;
|
|
44
|
+
this.#fetch = options.fetch ?? globalThis.fetch;
|
|
39
45
|
}
|
|
40
46
|
async start() {
|
|
41
47
|
if (this.#started)
|
|
@@ -50,6 +56,12 @@ export class DiscordGatewayEndpoint {
|
|
|
50
56
|
await connectDiscordGatewayClient(this.#client, this.#options.config, {
|
|
51
57
|
onMessage: (msg) => this.admit(msg),
|
|
52
58
|
onButton: (interaction) => this.admitButton(interaction),
|
|
59
|
+
onGuildMemberAdd: (member) => {
|
|
60
|
+
receiveDiscordGuildMemberSideEvent(this.#options.sideEvents, this.#options.config.id, 'member_increase', member, this.#logger);
|
|
61
|
+
},
|
|
62
|
+
onGuildMemberRemove: (member) => {
|
|
63
|
+
receiveDiscordGuildMemberSideEvent(this.#options.sideEvents, this.#options.config.id, 'member_decrease', member, this.#logger);
|
|
64
|
+
},
|
|
53
65
|
});
|
|
54
66
|
this.#logger.info(formatCompact({
|
|
55
67
|
op: 'connect',
|
|
@@ -309,6 +321,9 @@ export class DiscordInteractionsEndpoint {
|
|
|
309
321
|
control = Object.freeze({
|
|
310
322
|
recall: (message) => this.recallMessage(message),
|
|
311
323
|
});
|
|
324
|
+
content = Object.freeze({
|
|
325
|
+
resolve: (reference, context) => resolveDiscordContent(this.#fetch, this.#options.config.token, reference, context),
|
|
326
|
+
});
|
|
312
327
|
constructor(options) {
|
|
313
328
|
this.#logger = getAdapterLogger('discord', options.config.id);
|
|
314
329
|
this.#options = options;
|
|
@@ -408,6 +423,55 @@ export class DiscordInteractionsEndpoint {
|
|
|
408
423
|
});
|
|
409
424
|
}
|
|
410
425
|
}
|
|
426
|
+
async function resolveDiscordContent(fetch, token, reference, context) {
|
|
427
|
+
if (reference.kind === 'forward') {
|
|
428
|
+
return Object.freeze({ status: 'unsupported', code: 'discord_merged_forward_unavailable' });
|
|
429
|
+
}
|
|
430
|
+
if (reference.kind === 'media') {
|
|
431
|
+
return reference.media.kind === 'file'
|
|
432
|
+
? Object.freeze({ status: 'unsupported', code: 'discord_opaque_media_unavailable' })
|
|
433
|
+
: Object.freeze({ status: 'resolved', reference, value: reference.media });
|
|
434
|
+
}
|
|
435
|
+
try {
|
|
436
|
+
context.signal.throwIfAborted();
|
|
437
|
+
const response = await fetch(`${DISCORD_API}/channels/${reference.message.conversation.id}/messages/${reference.message.id}`, { headers: { Authorization: `Bot ${token}` }, signal: context.signal });
|
|
438
|
+
if (response.status === 404)
|
|
439
|
+
return Object.freeze({ status: 'not_found', code: 'discord_message_not_found' });
|
|
440
|
+
if (response.status === 403)
|
|
441
|
+
return Object.freeze({ status: 'forbidden', code: 'discord_message_forbidden' });
|
|
442
|
+
if (!response.ok)
|
|
443
|
+
return Object.freeze({ status: 'failed', code: 'discord_message_fetch_failed' });
|
|
444
|
+
const row = await response.json();
|
|
445
|
+
const author = row.author;
|
|
446
|
+
const attachments = Array.isArray(row.attachments) ? row.attachments : [];
|
|
447
|
+
const segments = [];
|
|
448
|
+
if (typeof row.content === 'string' && row.content.trim())
|
|
449
|
+
segments.push({ type: 'text', data: { text: row.content } });
|
|
450
|
+
for (const item of attachments.slice(0, context.maxEntries)) {
|
|
451
|
+
const attachment = item;
|
|
452
|
+
if (typeof attachment.url !== 'string')
|
|
453
|
+
continue;
|
|
454
|
+
const mime = typeof attachment.content_type === 'string' ? attachment.content_type : undefined;
|
|
455
|
+
const type = mime?.startsWith('image/') ? 'image' : mime?.startsWith('audio/') ? 'audio' : mime?.startsWith('video/') ? 'video' : 'file';
|
|
456
|
+
segments.push({ type, data: { media: { kind: 'url', value: attachment.url, ...(mime ? { mime_type: mime } : {}), ...(attachment.filename ? { file_name: String(attachment.filename) } : {}), ...(typeof attachment.size === 'number' ? { size: attachment.size } : {}) } } });
|
|
457
|
+
}
|
|
458
|
+
return Object.freeze({
|
|
459
|
+
status: 'resolved',
|
|
460
|
+
reference,
|
|
461
|
+
value: Object.freeze({
|
|
462
|
+
ref: reference.message,
|
|
463
|
+
...(author?.id ? { actor: Object.freeze({ id: String(author.id), ...(author.global_name || author.username ? { displayName: String(author.global_name ?? author.username) } : {}) }) } : {}),
|
|
464
|
+
segments: Object.freeze(segments),
|
|
465
|
+
timestamp: typeof row.timestamp === 'string' ? Date.parse(row.timestamp) : Date.now(),
|
|
466
|
+
}),
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
catch (error) {
|
|
470
|
+
if (context.signal.aborted)
|
|
471
|
+
return Object.freeze({ status: 'expired', code: 'turn_aborted' });
|
|
472
|
+
return Object.freeze({ status: 'failed', code: 'discord_content_resolution_failed', message: error instanceof Error ? error.message : String(error) });
|
|
473
|
+
}
|
|
474
|
+
}
|
|
411
475
|
/**
|
|
412
476
|
* Discord snowflake 是 64 位整数的字符串形式,超出 Number.MAX_SAFE_INTEGER,
|
|
413
477
|
* Number() 转换会丢精度。Console 社交面只把 group_id 当 JSON 值透传、
|
package/lib/gateway.d.ts
CHANGED
|
@@ -129,5 +129,15 @@ export declare function toMessageCreateOptions(body: DiscordOutboundBody): Promi
|
|
|
129
129
|
export interface DiscordGatewayConnectHandlers {
|
|
130
130
|
onMessage(msg: DiscordInboundMessage): void;
|
|
131
131
|
onButton(interaction: DiscordButtonInbound): void;
|
|
132
|
+
onGuildMemberAdd?(member: {
|
|
133
|
+
guildId: string;
|
|
134
|
+
userId: string;
|
|
135
|
+
userName?: string;
|
|
136
|
+
}): void;
|
|
137
|
+
onGuildMemberRemove?(member: {
|
|
138
|
+
guildId: string;
|
|
139
|
+
userId: string;
|
|
140
|
+
userName?: string;
|
|
141
|
+
}): void;
|
|
132
142
|
}
|
|
133
143
|
export declare function connectDiscordGatewayClient(client: DiscordClientTransport, config: ResolvedDiscordGatewayConfig, handlers: DiscordGatewayConnectHandlers): Promise<void>;
|
package/lib/gateway.js
CHANGED
|
@@ -224,6 +224,30 @@ export async function connectDiscordGatewayClient(client, config, handlers) {
|
|
|
224
224
|
sourceMessageId: interaction.message?.id,
|
|
225
225
|
});
|
|
226
226
|
});
|
|
227
|
+
client.on('guildMemberAdd', (raw) => {
|
|
228
|
+
const member = raw;
|
|
229
|
+
const guildId = member.guild?.id;
|
|
230
|
+
const userId = member.user?.id;
|
|
231
|
+
if (!guildId || !userId)
|
|
232
|
+
return;
|
|
233
|
+
handlers.onGuildMemberAdd?.({
|
|
234
|
+
guildId,
|
|
235
|
+
userId,
|
|
236
|
+
userName: member.user?.username || member.user?.displayName,
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
client.on('guildMemberRemove', (raw) => {
|
|
240
|
+
const member = raw;
|
|
241
|
+
const guildId = member.guild?.id;
|
|
242
|
+
const userId = member.user?.id;
|
|
243
|
+
if (!guildId || !userId)
|
|
244
|
+
return;
|
|
245
|
+
handlers.onGuildMemberRemove?.({
|
|
246
|
+
guildId,
|
|
247
|
+
userId,
|
|
248
|
+
userName: member.user?.username || member.user?.displayName,
|
|
249
|
+
});
|
|
250
|
+
});
|
|
227
251
|
client.once('clientReady', () => {
|
|
228
252
|
void (async () => {
|
|
229
253
|
try {
|
package/lib/protocol.d.ts
CHANGED
|
@@ -138,7 +138,7 @@ export declare function senderDisplayName(msg: DiscordInboundMessage): string;
|
|
|
138
138
|
export declare function formatInboundContent(msg: DiscordInboundMessage): string;
|
|
139
139
|
export declare function formatButtonContent(interaction: DiscordButtonInbound): string;
|
|
140
140
|
/**
|
|
141
|
-
* 入站消息 → canonical Segment[]
|
|
141
|
+
* 入站消息 → canonical Segment[];引用与附件不重复编码进文本视图。
|
|
142
142
|
* 附件 url 是 Discord CDN 真实 http(s) 地址,MediaRef kind=url;
|
|
143
143
|
* embeds / stickers 仅留在纯文本视图,不进段(最小侵入)。
|
|
144
144
|
*/
|
package/lib/protocol.js
CHANGED
|
@@ -80,21 +80,8 @@ export function senderDisplayName(msg) {
|
|
|
80
80
|
/** Build inbound text for MessageGateway.receive (gateway owns reply routing). */
|
|
81
81
|
export function formatInboundContent(msg) {
|
|
82
82
|
const parts = [];
|
|
83
|
-
if (msg.replyToId)
|
|
84
|
-
parts.push(`[reply:${msg.replyToId}]`);
|
|
85
83
|
if (msg.content?.trim())
|
|
86
84
|
parts.push(msg.content.trim());
|
|
87
|
-
for (const attachment of msg.attachments ?? []) {
|
|
88
|
-
const kind = attachment.contentType?.startsWith('image/')
|
|
89
|
-
? 'image'
|
|
90
|
-
: attachment.contentType?.startsWith('audio/')
|
|
91
|
-
? 'audio'
|
|
92
|
-
: attachment.contentType?.startsWith('video/')
|
|
93
|
-
? 'video'
|
|
94
|
-
: 'file';
|
|
95
|
-
const name = attachment.name || attachment.url || 'attachment';
|
|
96
|
-
parts.push(`[${kind}: ${name}]`);
|
|
97
|
-
}
|
|
98
85
|
for (const title of msg.embedTitles ?? []) {
|
|
99
86
|
parts.push(`[embed: ${title}]`);
|
|
100
87
|
}
|
|
@@ -102,7 +89,7 @@ export function formatInboundContent(msg) {
|
|
|
102
89
|
parts.push(`[sticker: ${name}]`);
|
|
103
90
|
}
|
|
104
91
|
const text = parts.join('\n').trim();
|
|
105
|
-
return text
|
|
92
|
+
return text;
|
|
106
93
|
}
|
|
107
94
|
export function formatButtonContent(interaction) {
|
|
108
95
|
return `[action: ${interaction.customId}]`;
|
|
@@ -117,7 +104,7 @@ function attachmentMediaKind(contentType) {
|
|
|
117
104
|
return 'file';
|
|
118
105
|
}
|
|
119
106
|
/**
|
|
120
|
-
* 入站消息 → canonical Segment[]
|
|
107
|
+
* 入站消息 → canonical Segment[];引用与附件不重复编码进文本视图。
|
|
121
108
|
* 附件 url 是 Discord CDN 真实 http(s) 地址,MediaRef kind=url;
|
|
122
109
|
* embeds / stickers 仅留在纯文本视图,不进段(最小侵入)。
|
|
123
110
|
*/
|
|
@@ -204,6 +191,9 @@ export function formatOutboundBody(payload) {
|
|
|
204
191
|
case 'text':
|
|
205
192
|
content += String(data.text ?? data.content ?? '');
|
|
206
193
|
break;
|
|
194
|
+
case 'markdown':
|
|
195
|
+
content += String(data.content ?? data.text ?? '');
|
|
196
|
+
break;
|
|
207
197
|
case 'at':
|
|
208
198
|
content += `<@${String(data.id ?? '')}>`;
|
|
209
199
|
break;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SideEventGateway } from '@zhin.js/core/runtime';
|
|
2
|
+
import { type getAdapterLogger } from '@zhin.js/logger';
|
|
3
|
+
export interface DiscordGuildMemberSideEvent {
|
|
4
|
+
readonly guildId: string;
|
|
5
|
+
readonly userId: string;
|
|
6
|
+
readonly userName?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function receiveDiscordGuildMemberSideEvent(sideEvents: SideEventGateway | undefined, configId: string, kind: 'member_increase' | 'member_decrease', event: DiscordGuildMemberSideEvent, logger: ReturnType<typeof getAdapterLogger>): void;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { buildNotice, senderFromId } from '@zhin.js/core';
|
|
2
|
+
import { formatCompact } from '@zhin.js/logger';
|
|
3
|
+
export function receiveDiscordGuildMemberSideEvent(sideEvents, configId, kind, event, logger) {
|
|
4
|
+
if (!sideEvents)
|
|
5
|
+
return;
|
|
6
|
+
void sideEvents.receiveNotice(buildNotice(event, {
|
|
7
|
+
$id: `discord:guild_member:${kind}:${event.guildId}:${event.userId}:${Date.now()}`,
|
|
8
|
+
$adapter: 'discord',
|
|
9
|
+
$endpoint: configId,
|
|
10
|
+
$type: 'notice',
|
|
11
|
+
$scene_id: event.guildId,
|
|
12
|
+
$scene_type: 'group',
|
|
13
|
+
$sub_type: kind,
|
|
14
|
+
$actor: senderFromId(event.userId, event.userName),
|
|
15
|
+
$timestamp: Date.now(),
|
|
16
|
+
})).catch((err) => {
|
|
17
|
+
logger.warn(formatCompact({
|
|
18
|
+
op: 'discord_side_event_failed',
|
|
19
|
+
endpoint: configId,
|
|
20
|
+
event: kind,
|
|
21
|
+
error: err instanceof Error ? err.message : String(err),
|
|
22
|
+
}));
|
|
23
|
+
});
|
|
24
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/adapter-discord",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "8.0.0",
|
|
4
4
|
"description": "Zhin.js Discord adapter for Plugin Runtime (Gateway WebSocket)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -33,20 +33,20 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"discord.js": "^14.27.0",
|
|
36
|
-
"@zhin.js/adapter": "1.
|
|
37
|
-
"@zhin.js/core": "1.5.
|
|
38
|
-
"@zhin.js/host-http": "1.0.
|
|
39
|
-
"@zhin.js/im-contract": "1.0.
|
|
36
|
+
"@zhin.js/adapter": "1.2.0",
|
|
37
|
+
"@zhin.js/core": "1.5.13",
|
|
38
|
+
"@zhin.js/host-http": "1.0.12",
|
|
39
|
+
"@zhin.js/im-contract": "1.0.4",
|
|
40
40
|
"@zhin.js/logger": "1.0.76",
|
|
41
|
-
"@zhin.js/permission": "1.0.
|
|
41
|
+
"@zhin.js/permission": "1.0.3"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
44
|
"zod": "^4.0.0",
|
|
45
|
-
"@zhin.js/adapter": "1.
|
|
46
|
-
"@zhin.js/agent": "1.1.
|
|
47
|
-
"@zhin.js/command": "1.0.
|
|
48
|
-
"@zhin.js/core": "1.5.
|
|
49
|
-
"zhin.js": "6.0.
|
|
45
|
+
"@zhin.js/adapter": "1.2.0",
|
|
46
|
+
"@zhin.js/agent": "1.1.15",
|
|
47
|
+
"@zhin.js/command": "1.0.15",
|
|
48
|
+
"@zhin.js/core": "1.5.13",
|
|
49
|
+
"zhin.js": "6.0.13"
|
|
50
50
|
},
|
|
51
51
|
"peerDependenciesMeta": {
|
|
52
52
|
"@zhin.js/agent": {
|
|
@@ -67,9 +67,9 @@
|
|
|
67
67
|
"typescript": "^6.0.3",
|
|
68
68
|
"vitest": "^4.1.10",
|
|
69
69
|
"zod": "^4.4.3",
|
|
70
|
-
"@zhin.js/agent": "1.1.
|
|
71
|
-
"@zhin.js/host-http": "1.0.
|
|
72
|
-
"zhin.js": "6.0.
|
|
70
|
+
"@zhin.js/agent": "1.1.15",
|
|
71
|
+
"@zhin.js/host-http": "1.0.12",
|
|
72
|
+
"zhin.js": "6.0.13"
|
|
73
73
|
},
|
|
74
74
|
"files": [
|
|
75
75
|
"adapters",
|
package/src/endpoint.ts
CHANGED
|
@@ -4,15 +4,19 @@
|
|
|
4
4
|
import { ChannelType } from 'discord.js';
|
|
5
5
|
import type {
|
|
6
6
|
EndpointChannel,
|
|
7
|
+
EndpointContentPort,
|
|
8
|
+
EndpointContentResolveContext,
|
|
7
9
|
EndpointControl,
|
|
8
10
|
EndpointGroup,
|
|
9
11
|
EndpointInstance,
|
|
10
12
|
EndpointManagement,
|
|
11
13
|
EndpointSendRequest,
|
|
12
14
|
} from 'zhin.js/adapter';
|
|
13
|
-
import type { MessageGateway } from '@zhin.js/core/runtime';
|
|
15
|
+
import type { MessageGateway, SideEventGateway } from '@zhin.js/core/runtime';
|
|
14
16
|
import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
|
|
15
17
|
import {
|
|
18
|
+
type ConversationReference,
|
|
19
|
+
type ConversationResolution,
|
|
16
20
|
type MessageRef,
|
|
17
21
|
} from '@zhin.js/im-contract';
|
|
18
22
|
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
@@ -42,6 +46,7 @@ import {
|
|
|
42
46
|
type ResolvedDiscordInteractionsConfig,
|
|
43
47
|
} from './protocol.js';
|
|
44
48
|
import { registerDiscordInteractionRoutes } from './webhook.js';
|
|
49
|
+
import { receiveDiscordGuildMemberSideEvent } from './side-event-dispatch.js';
|
|
45
50
|
|
|
46
51
|
const DISCORD_API = 'https://discord.com/api/v10';
|
|
47
52
|
/** 出站 HTTP 调用统一 30s 超时。 */
|
|
@@ -54,8 +59,10 @@ export type {
|
|
|
54
59
|
export interface DiscordEndpointOptions {
|
|
55
60
|
readonly id: CapabilityId;
|
|
56
61
|
readonly gateway: MessageGateway;
|
|
62
|
+
readonly sideEvents?: SideEventGateway;
|
|
57
63
|
readonly config: ResolvedDiscordGatewayConfig;
|
|
58
64
|
readonly createClient?: CreateDiscordClient;
|
|
65
|
+
readonly fetch?: typeof globalThis.fetch;
|
|
59
66
|
}
|
|
60
67
|
|
|
61
68
|
export class DiscordGatewayEndpoint implements EndpointInstance {
|
|
@@ -63,6 +70,7 @@ export class DiscordGatewayEndpoint implements EndpointInstance {
|
|
|
63
70
|
|
|
64
71
|
readonly #options: DiscordEndpointOptions;
|
|
65
72
|
readonly #createClient: CreateDiscordClient;
|
|
73
|
+
readonly #fetch: typeof globalThis.fetch;
|
|
66
74
|
#client: DiscordClientTransport | null = null;
|
|
67
75
|
#open = false;
|
|
68
76
|
#started = false;
|
|
@@ -84,11 +92,16 @@ export class DiscordGatewayEndpoint implements EndpointInstance {
|
|
|
84
92
|
return emoji;
|
|
85
93
|
},
|
|
86
94
|
});
|
|
95
|
+
readonly content: EndpointContentPort = Object.freeze({
|
|
96
|
+
resolve: (reference: ConversationReference, context: EndpointContentResolveContext) =>
|
|
97
|
+
resolveDiscordContent(this.#fetch, this.#options.config.token, reference, context),
|
|
98
|
+
});
|
|
87
99
|
|
|
88
100
|
constructor(options: DiscordEndpointOptions) {
|
|
89
101
|
this.#logger = getAdapterLogger('discord', options.config.id);
|
|
90
102
|
this.#options = options;
|
|
91
103
|
this.#createClient = options.createClient ?? defaultCreateClient;
|
|
104
|
+
this.#fetch = options.fetch ?? globalThis.fetch;
|
|
92
105
|
}
|
|
93
106
|
|
|
94
107
|
async start(): Promise<void> {
|
|
@@ -103,6 +116,24 @@ export class DiscordGatewayEndpoint implements EndpointInstance {
|
|
|
103
116
|
await connectDiscordGatewayClient(this.#client, this.#options.config, {
|
|
104
117
|
onMessage: (msg) => this.admit(msg),
|
|
105
118
|
onButton: (interaction) => this.admitButton(interaction),
|
|
119
|
+
onGuildMemberAdd: (member) => {
|
|
120
|
+
receiveDiscordGuildMemberSideEvent(
|
|
121
|
+
this.#options.sideEvents,
|
|
122
|
+
this.#options.config.id,
|
|
123
|
+
'member_increase',
|
|
124
|
+
member,
|
|
125
|
+
this.#logger,
|
|
126
|
+
);
|
|
127
|
+
},
|
|
128
|
+
onGuildMemberRemove: (member) => {
|
|
129
|
+
receiveDiscordGuildMemberSideEvent(
|
|
130
|
+
this.#options.sideEvents,
|
|
131
|
+
this.#options.config.id,
|
|
132
|
+
'member_decrease',
|
|
133
|
+
member,
|
|
134
|
+
this.#logger,
|
|
135
|
+
);
|
|
136
|
+
},
|
|
106
137
|
});
|
|
107
138
|
this.#logger.info(formatCompact({
|
|
108
139
|
op: 'connect',
|
|
@@ -405,6 +436,7 @@ export class DiscordGatewayEndpoint implements EndpointInstance {
|
|
|
405
436
|
export interface DiscordInteractionsEndpointOptions {
|
|
406
437
|
readonly id: CapabilityId;
|
|
407
438
|
readonly gateway: MessageGateway;
|
|
439
|
+
readonly sideEvents?: SideEventGateway;
|
|
408
440
|
readonly http: HttpHost;
|
|
409
441
|
readonly config: ResolvedDiscordInteractionsConfig;
|
|
410
442
|
readonly fetch?: typeof globalThis.fetch;
|
|
@@ -421,6 +453,10 @@ export class DiscordInteractionsEndpoint implements EndpointInstance {
|
|
|
421
453
|
readonly control: EndpointControl = Object.freeze({
|
|
422
454
|
recall: (message: MessageRef) => this.recallMessage(message),
|
|
423
455
|
});
|
|
456
|
+
readonly content: EndpointContentPort = Object.freeze({
|
|
457
|
+
resolve: (reference: ConversationReference, context: EndpointContentResolveContext) =>
|
|
458
|
+
resolveDiscordContent(this.#fetch, this.#options.config.token, reference, context),
|
|
459
|
+
});
|
|
424
460
|
|
|
425
461
|
constructor(options: DiscordInteractionsEndpointOptions) {
|
|
426
462
|
this.#logger = getAdapterLogger('discord', options.config.id);
|
|
@@ -527,6 +563,57 @@ export class DiscordInteractionsEndpoint implements EndpointInstance {
|
|
|
527
563
|
}
|
|
528
564
|
}
|
|
529
565
|
|
|
566
|
+
async function resolveDiscordContent(
|
|
567
|
+
fetch: typeof globalThis.fetch,
|
|
568
|
+
token: string,
|
|
569
|
+
reference: ConversationReference,
|
|
570
|
+
context: EndpointContentResolveContext,
|
|
571
|
+
): Promise<ConversationResolution> {
|
|
572
|
+
if (reference.kind === 'forward') {
|
|
573
|
+
return Object.freeze({ status: 'unsupported', code: 'discord_merged_forward_unavailable' });
|
|
574
|
+
}
|
|
575
|
+
if (reference.kind === 'media') {
|
|
576
|
+
return reference.media.kind === 'file'
|
|
577
|
+
? Object.freeze({ status: 'unsupported', code: 'discord_opaque_media_unavailable' })
|
|
578
|
+
: Object.freeze({ status: 'resolved', reference, value: reference.media });
|
|
579
|
+
}
|
|
580
|
+
try {
|
|
581
|
+
context.signal.throwIfAborted();
|
|
582
|
+
const response = await fetch(
|
|
583
|
+
`${DISCORD_API}/channels/${reference.message.conversation.id}/messages/${reference.message.id}`,
|
|
584
|
+
{ headers: { Authorization: `Bot ${token}` }, signal: context.signal },
|
|
585
|
+
);
|
|
586
|
+
if (response.status === 404) return Object.freeze({ status: 'not_found', code: 'discord_message_not_found' });
|
|
587
|
+
if (response.status === 403) return Object.freeze({ status: 'forbidden', code: 'discord_message_forbidden' });
|
|
588
|
+
if (!response.ok) return Object.freeze({ status: 'failed', code: 'discord_message_fetch_failed' });
|
|
589
|
+
const row = await response.json() as Record<string, unknown>;
|
|
590
|
+
const author = row.author as Record<string, unknown> | undefined;
|
|
591
|
+
const attachments = Array.isArray(row.attachments) ? row.attachments : [];
|
|
592
|
+
const segments: import('@zhin.js/im-contract').Segment[] = [];
|
|
593
|
+
if (typeof row.content === 'string' && row.content.trim()) segments.push({ type: 'text', data: { text: row.content } });
|
|
594
|
+
for (const item of attachments.slice(0, context.maxEntries)) {
|
|
595
|
+
const attachment = item as Record<string, unknown>;
|
|
596
|
+
if (typeof attachment.url !== 'string') continue;
|
|
597
|
+
const mime = typeof attachment.content_type === 'string' ? attachment.content_type : undefined;
|
|
598
|
+
const type = mime?.startsWith('image/') ? 'image' : mime?.startsWith('audio/') ? 'audio' : mime?.startsWith('video/') ? 'video' : 'file';
|
|
599
|
+
segments.push({ type, data: { media: { kind: 'url', value: attachment.url, ...(mime ? { mime_type: mime } : {}), ...(attachment.filename ? { file_name: String(attachment.filename) } : {}), ...(typeof attachment.size === 'number' ? { size: attachment.size } : {}) } } });
|
|
600
|
+
}
|
|
601
|
+
return Object.freeze({
|
|
602
|
+
status: 'resolved',
|
|
603
|
+
reference,
|
|
604
|
+
value: Object.freeze({
|
|
605
|
+
ref: reference.message,
|
|
606
|
+
...(author?.id ? { actor: Object.freeze({ id: String(author.id), ...(author.global_name || author.username ? { displayName: String(author.global_name ?? author.username) } : {}) }) } : {}),
|
|
607
|
+
segments: Object.freeze(segments),
|
|
608
|
+
timestamp: typeof row.timestamp === 'string' ? Date.parse(row.timestamp) : Date.now(),
|
|
609
|
+
}),
|
|
610
|
+
});
|
|
611
|
+
} catch (error) {
|
|
612
|
+
if (context.signal.aborted) return Object.freeze({ status: 'expired', code: 'turn_aborted' });
|
|
613
|
+
return Object.freeze({ status: 'failed', code: 'discord_content_resolution_failed', message: error instanceof Error ? error.message : String(error) });
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
530
617
|
/**
|
|
531
618
|
* Discord snowflake 是 64 位整数的字符串形式,超出 Number.MAX_SAFE_INTEGER,
|
|
532
619
|
* Number() 转换会丢精度。Console 社交面只把 group_id 当 JSON 值透传、
|
package/src/gateway.ts
CHANGED
|
@@ -287,6 +287,8 @@ function decodeBase64(value: string): Buffer {
|
|
|
287
287
|
export interface DiscordGatewayConnectHandlers {
|
|
288
288
|
onMessage(msg: DiscordInboundMessage): void;
|
|
289
289
|
onButton(interaction: DiscordButtonInbound): void;
|
|
290
|
+
onGuildMemberAdd?(member: { guildId: string; userId: string; userName?: string }): void;
|
|
291
|
+
onGuildMemberRemove?(member: { guildId: string; userId: string; userName?: string }): void;
|
|
290
292
|
}
|
|
291
293
|
|
|
292
294
|
export async function connectDiscordGatewayClient(
|
|
@@ -331,6 +333,36 @@ export async function connectDiscordGatewayClient(
|
|
|
331
333
|
});
|
|
332
334
|
});
|
|
333
335
|
|
|
336
|
+
client.on('guildMemberAdd', (raw) => {
|
|
337
|
+
const member = raw as {
|
|
338
|
+
guild?: { id?: string };
|
|
339
|
+
user?: { id?: string; username?: string; displayName?: string };
|
|
340
|
+
};
|
|
341
|
+
const guildId = member.guild?.id;
|
|
342
|
+
const userId = member.user?.id;
|
|
343
|
+
if (!guildId || !userId) return;
|
|
344
|
+
handlers.onGuildMemberAdd?.({
|
|
345
|
+
guildId,
|
|
346
|
+
userId,
|
|
347
|
+
userName: member.user?.username || member.user?.displayName,
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
client.on('guildMemberRemove', (raw) => {
|
|
352
|
+
const member = raw as {
|
|
353
|
+
guild?: { id?: string };
|
|
354
|
+
user?: { id?: string; username?: string; displayName?: string };
|
|
355
|
+
};
|
|
356
|
+
const guildId = member.guild?.id;
|
|
357
|
+
const userId = member.user?.id;
|
|
358
|
+
if (!guildId || !userId) return;
|
|
359
|
+
handlers.onGuildMemberRemove?.({
|
|
360
|
+
guildId,
|
|
361
|
+
userId,
|
|
362
|
+
userName: member.user?.username || member.user?.displayName,
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
|
|
334
366
|
client.once('clientReady', () => {
|
|
335
367
|
void (async () => {
|
|
336
368
|
try {
|
package/src/protocol.ts
CHANGED
|
@@ -230,19 +230,7 @@ export function senderDisplayName(msg: DiscordInboundMessage): string {
|
|
|
230
230
|
/** Build inbound text for MessageGateway.receive (gateway owns reply routing). */
|
|
231
231
|
export function formatInboundContent(msg: DiscordInboundMessage): string {
|
|
232
232
|
const parts: string[] = [];
|
|
233
|
-
if (msg.replyToId) parts.push(`[reply:${msg.replyToId}]`);
|
|
234
233
|
if (msg.content?.trim()) parts.push(msg.content.trim());
|
|
235
|
-
for (const attachment of msg.attachments ?? []) {
|
|
236
|
-
const kind = attachment.contentType?.startsWith('image/')
|
|
237
|
-
? 'image'
|
|
238
|
-
: attachment.contentType?.startsWith('audio/')
|
|
239
|
-
? 'audio'
|
|
240
|
-
: attachment.contentType?.startsWith('video/')
|
|
241
|
-
? 'video'
|
|
242
|
-
: 'file';
|
|
243
|
-
const name = attachment.name || attachment.url || 'attachment';
|
|
244
|
-
parts.push(`[${kind}: ${name}]`);
|
|
245
|
-
}
|
|
246
234
|
for (const title of msg.embedTitles ?? []) {
|
|
247
235
|
parts.push(`[embed: ${title}]`);
|
|
248
236
|
}
|
|
@@ -250,7 +238,7 @@ export function formatInboundContent(msg: DiscordInboundMessage): string {
|
|
|
250
238
|
parts.push(`[sticker: ${name}]`);
|
|
251
239
|
}
|
|
252
240
|
const text = parts.join('\n').trim();
|
|
253
|
-
return text
|
|
241
|
+
return text;
|
|
254
242
|
}
|
|
255
243
|
|
|
256
244
|
export function formatButtonContent(interaction: DiscordButtonInbound): string {
|
|
@@ -265,7 +253,7 @@ function attachmentMediaKind(contentType: string | undefined): 'image' | 'audio'
|
|
|
265
253
|
}
|
|
266
254
|
|
|
267
255
|
/**
|
|
268
|
-
* 入站消息 → canonical Segment[]
|
|
256
|
+
* 入站消息 → canonical Segment[];引用与附件不重复编码进文本视图。
|
|
269
257
|
* 附件 url 是 Discord CDN 真实 http(s) 地址,MediaRef kind=url;
|
|
270
258
|
* embeds / stickers 仅留在纯文本视图,不进段(最小侵入)。
|
|
271
259
|
*/
|
|
@@ -356,6 +344,9 @@ export function formatOutboundBody(payload: unknown): DiscordOutboundBody {
|
|
|
356
344
|
case 'text':
|
|
357
345
|
content += String(data.text ?? data.content ?? '');
|
|
358
346
|
break;
|
|
347
|
+
case 'markdown':
|
|
348
|
+
content += String(data.content ?? data.text ?? '');
|
|
349
|
+
break;
|
|
359
350
|
case 'at':
|
|
360
351
|
content += `<@${String(data.id ?? '')}>`;
|
|
361
352
|
break;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { buildNotice, senderFromId } from '@zhin.js/core';
|
|
2
|
+
import type { SideEventGateway } from '@zhin.js/core/runtime';
|
|
3
|
+
import { formatCompact, type getAdapterLogger } from '@zhin.js/logger';
|
|
4
|
+
|
|
5
|
+
export interface DiscordGuildMemberSideEvent {
|
|
6
|
+
readonly guildId: string;
|
|
7
|
+
readonly userId: string;
|
|
8
|
+
readonly userName?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function receiveDiscordGuildMemberSideEvent(
|
|
12
|
+
sideEvents: SideEventGateway | undefined,
|
|
13
|
+
configId: string,
|
|
14
|
+
kind: 'member_increase' | 'member_decrease',
|
|
15
|
+
event: DiscordGuildMemberSideEvent,
|
|
16
|
+
logger: ReturnType<typeof getAdapterLogger>,
|
|
17
|
+
): void {
|
|
18
|
+
if (!sideEvents) return;
|
|
19
|
+
void sideEvents.receiveNotice(buildNotice(event, {
|
|
20
|
+
$id: `discord:guild_member:${kind}:${event.guildId}:${event.userId}:${Date.now()}`,
|
|
21
|
+
$adapter: 'discord' as never,
|
|
22
|
+
$endpoint: configId,
|
|
23
|
+
$type: 'notice',
|
|
24
|
+
$scene_id: event.guildId,
|
|
25
|
+
$scene_type: 'group',
|
|
26
|
+
$sub_type: kind,
|
|
27
|
+
$actor: senderFromId(event.userId, event.userName),
|
|
28
|
+
$timestamp: Date.now(),
|
|
29
|
+
})).catch((err) => {
|
|
30
|
+
logger.warn(formatCompact({
|
|
31
|
+
op: 'discord_side_event_failed',
|
|
32
|
+
endpoint: configId,
|
|
33
|
+
event: kind,
|
|
34
|
+
error: err instanceof Error ? err.message : String(err),
|
|
35
|
+
}));
|
|
36
|
+
});
|
|
37
|
+
}
|