@zhin.js/adapter-icqq 8.0.2 → 8.0.5

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/src/protocol.ts CHANGED
@@ -13,6 +13,7 @@
13
13
  */
14
14
 
15
15
  import { pickCredential } from '@zhin.js/adapter';
16
+ import type { ConversationRef } from '@zhin.js/im-contract';
16
17
  import type { MessageSegment } from "zhin.js";
17
18
  import { resolveCqMediaArg } from "./cq-message.js";
18
19
 
@@ -272,7 +273,7 @@ export const Actions = {
272
273
 
273
274
  /** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
274
275
  export interface IcqqAdapterConfig {
275
- readonly name?: string;
276
+ readonly id?: string;
276
277
  readonly autoReconnect?: boolean;
277
278
  readonly outboundMedia?: 'file' | 'base64';
278
279
  readonly rpc?: {
@@ -283,7 +284,7 @@ export interface IcqqAdapterConfig {
283
284
  /** Transitional: legacy root `endpoints[]` with `context: icqq`. */
284
285
  readonly endpoints?: ReadonlyArray<{
285
286
  readonly context?: string;
286
- readonly name?: string;
287
+ readonly id?: string;
287
288
  readonly autoReconnect?: boolean;
288
289
  readonly outboundMedia?: 'file' | 'base64';
289
290
  readonly rpc?: {
@@ -296,7 +297,7 @@ export interface IcqqAdapterConfig {
296
297
 
297
298
  export interface ResolvedIcqqConfig {
298
299
  readonly context: 'icqq';
299
- readonly name: string;
300
+ readonly id: string;
300
301
  readonly autoReconnect: boolean;
301
302
  readonly outboundMedia?: 'file' | 'base64';
302
303
  readonly rpc?: {
@@ -313,14 +314,14 @@ export interface IcqqWireSegment {
313
314
 
314
315
  export interface IcqqInboundMessage {
315
316
  readonly id: string;
316
- readonly target: string;
317
+ readonly conversation: ConversationRef;
317
318
  readonly content: string;
318
319
  /**
319
320
  * 结构化段视图(canonical Segment,经 toCanonicalSegments 归一)。
320
321
  * 与 content(CQ 原文纯文本视图)同源,随 gateway.receive 上送。
321
322
  */
322
323
  readonly segments?: readonly MessageSegment[];
323
- readonly sender: string;
324
+ readonly sender: { readonly id: string; readonly name?: string };
324
325
  readonly channelType: 'private' | 'group' | 'channel';
325
326
  readonly metadata?: Record<string, unknown>;
326
327
  }
@@ -333,10 +334,10 @@ export type ParsedIcqqSendTarget =
333
334
 
334
335
  export function resolveIcqqConfig(config: IcqqAdapterConfig = {}): ResolvedIcqqConfig {
335
336
  const entry = config.endpoints?.find((item) => item.context === 'icqq');
336
- const name = pickCredential(config.name, entry?.name, process.env.ICQQ_ACCOUNT);
337
- if (!/^\d+$/.test(name)) {
337
+ const id = pickCredential(config.id, entry?.id, process.env.ICQQ_ACCOUNT);
338
+ if (!/^\d+$/.test(id)) {
338
339
  throw new TypeError(
339
- 'ICQQ adapter requires numeric name (QQ uin) via plugins.<key>.name or ICQQ_ACCOUNT',
340
+ 'ICQQ adapter requires numeric id (QQ uin) via plugins.<key>.id or ICQQ_ACCOUNT',
340
341
  );
341
342
  }
342
343
  const autoReconnect = config.autoReconnect ?? entry?.autoReconnect ?? true;
@@ -347,58 +348,66 @@ export function resolveIcqqConfig(config: IcqqAdapterConfig = {}): ResolvedIcqqC
347
348
  : undefined;
348
349
  return {
349
350
  context: 'icqq',
350
- name,
351
+ id,
351
352
  autoReconnect,
352
353
  ...(outboundMedia ? { outboundMedia } : {}),
353
354
  ...(rpc ? { rpc } : {}),
354
355
  };
355
356
  }
356
357
 
357
- /** Gateway reply target: `private:uid` / `group:gid` / `temp:gid:uid` / `channel:guild:channel`. */
358
- export function formatInboundTarget(input: {
358
+ /**
359
+ * 入站归一化 → ConversationRef:群临时会话(temp)映射为群容器内的 private
360
+ * 会话(parent kind 'group');频道消息的所属 guild 进 `parent`(kind 'channel')。
361
+ */
362
+ export function icqqInboundConversation(endpointKey: string, input: {
359
363
  readonly channelType: 'private' | 'group' | 'channel';
360
364
  readonly channelId: string;
361
365
  readonly channelParentGroupId?: string;
362
366
  readonly guildId?: string;
363
- }): string {
367
+ }): ConversationRef {
368
+ const endpoint = { id: endpointKey, adapter: endpointKey.split('\0')[0] ?? endpointKey };
364
369
  if (input.channelType === 'private' && input.channelParentGroupId) {
365
- return `temp:${input.channelParentGroupId}:${input.channelId}`;
370
+ return {
371
+ endpoint,
372
+ kind: 'private',
373
+ id: input.channelId,
374
+ parent: { kind: 'group', id: input.channelParentGroupId },
375
+ };
366
376
  }
367
377
  if (input.channelType === 'channel' && input.guildId) {
368
- return `channel:${input.guildId}:${input.channelId}`;
378
+ return {
379
+ endpoint,
380
+ kind: 'channel',
381
+ id: input.channelId,
382
+ parent: { kind: 'channel', id: input.guildId },
383
+ };
369
384
  }
370
- return `${input.channelType}:${input.channelId}`;
385
+ return { endpoint, kind: input.channelType, id: input.channelId };
371
386
  }
372
387
 
373
- export function parseSendTarget(target: string): ParsedIcqqSendTarget {
374
- const trimmed = target.trim();
375
- if (trimmed.startsWith('temp:')) {
376
- const rest = trimmed.slice(5);
377
- const [groupId, userId] = rest.split(':');
378
- if (!groupId || !userId) throw new TypeError(`Invalid icqq temp target: ${target}`);
379
- return { kind: 'temp', groupId: Number(groupId), userId: Number(userId) };
380
- }
381
- if (trimmed.startsWith('channel:')) {
382
- const rest = trimmed.slice(8);
383
- const idx = rest.indexOf(':');
384
- if (idx <= 0) throw new TypeError(`Invalid icqq channel target: ${target}`);
388
+ /**
389
+ * 出站:从 ConversationRef 派生 IPC action 路由。
390
+ * 群容器(parent kind 'group')内的 private 会话即 ICQQ 群临时会话(temp)。
391
+ */
392
+ export function icqqOutboundTarget(conversation: ConversationRef): ParsedIcqqSendTarget {
393
+ if (conversation.kind === 'private' && conversation.parent?.kind === 'group') {
385
394
  return {
386
- kind: 'channel',
387
- guildId: rest.slice(0, idx),
388
- channelId: rest.slice(idx + 1),
395
+ kind: 'temp',
396
+ groupId: Number(conversation.parent.id),
397
+ userId: Number(conversation.id),
389
398
  };
390
399
  }
391
- if (trimmed.startsWith('private:')) {
392
- return { kind: 'private', userId: Number(trimmed.slice(8)) };
400
+ if (conversation.kind === 'private') {
401
+ return { kind: 'private', userId: Number(conversation.id) };
393
402
  }
394
- if (trimmed.startsWith('group:')) {
395
- return { kind: 'group', groupId: Number(trimmed.slice(6)) };
403
+ if (conversation.kind === 'group') {
404
+ return { kind: 'group', groupId: Number(conversation.id) };
396
405
  }
397
- // Bare numeric id → private (common reply path)
398
- if (/^\d+$/.test(trimmed)) {
399
- return { kind: 'private', userId: Number(trimmed) };
406
+ const guildId = conversation.parent?.kind === 'channel' ? conversation.parent.id : undefined;
407
+ if (!guildId) {
408
+ throw new TypeError(`Invalid icqq channel conversation (missing guild parent): ${conversation.id}`);
400
409
  }
401
- throw new TypeError(`Invalid icqq send target: ${target}`);
410
+ return { kind: 'channel', guildId, channelId: conversation.id };
402
411
  }
403
412
 
404
413
  export function formatOutboundBody(payload: unknown): string {