@zhin.js/adapter-icqq 8.0.1 → 8.0.3

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 CHANGED
@@ -1,5 +1,28 @@
1
1
  # @zhin.js/adapter-icqq
2
2
 
3
+ ## 8.0.3
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [afc0e66]
8
+ - Updated dependencies [2e41ad5]
9
+ - Updated dependencies [9f57124]
10
+ - @zhin.js/core@1.5.2
11
+ - @zhin.js/adapter@1.1.5
12
+ - @zhin.js/im-contract@1.0.1
13
+ - @zhin.js/plugin-runtime@1.1.3
14
+ - @zhin.js/command@1.0.7
15
+ - @zhin.js/agent@1.1.3
16
+ - zhin.js@6.0.2
17
+
18
+ ## 8.0.2
19
+
20
+ ### Patch Changes
21
+
22
+ - Updated dependencies [696ab1b]
23
+ - @zhin.js/agent@1.1.2
24
+ - zhin.js@6.0.1
25
+
3
26
  ## 8.0.1
4
27
 
5
28
  ### Patch Changes
package/README.md CHANGED
@@ -5,8 +5,8 @@ ICQQ Plugin Runtime 适配器 — 通过 [@icqqjs/cli](https://github.com/icqqjs
5
5
  ## 功能特性
6
6
 
7
7
  - 群聊 / 私聊 / 群临时会话 / QQ 频道消息
8
- - 入站:`messageGatewayToken`(IPC 事件订阅)
9
- - 出站:`send({ target, payload })` → `send_group_msg` / `send_private_msg` / …
8
+ - 入站:`messageGatewayToken`(IPC 事件订阅),归一为 `gateway.receive({ conversation, message, content, ... })`
9
+ - 出站:`send({ conversation, payload })` → `send_group_msg` / `send_private_msg` / …(conversation kind/id/parent 路由)
10
10
  - Agent 工具:`agent/tools/`(戳一戳、群管、好友列表等)保留
11
11
  - Console Endpoint 管理:`src/endpoint.ts` 显式实现 `EndpointManagement`(好友/群/群成员列表、请求审批、删好友、踢人、禁言、设管理),Console 使用 `endpoint.friends` / `endpoint.groups` / `endpoint.group_members` 等规范 RPC
12
12
 
@@ -48,14 +48,14 @@ plugins:
48
48
 
49
49
  先执行 `icqq login`,再启动 Zhin。
50
50
 
51
- ## Send target
51
+ ## Send conversation
52
52
 
53
- | 类型 | target |
54
- |------|--------|
55
- | 私聊 | `private:uin` |
56
- | 群聊 | `group:gid` |
57
- | 群临时会话 | `temp:gid:uin` |
58
- | 频道 | `channel:guildId:channelId` |
53
+ | 类型 | conversation |
54
+ |------|--------------|
55
+ | 私聊 | `{ kind: 'private', id: uin }` |
56
+ | 群聊 | `{ kind: 'group', id: gid }` |
57
+ | 群临时会话 | `{ kind: 'private', id: uin, parent: { kind: 'group', id: gid } }` |
58
+ | 频道 | `{ kind: 'channel', id: channelId, parent: { kind: 'channel', id: guildId } }` |
59
59
 
60
60
  ## 架构
61
61
 
package/lib/endpoint.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * IcqqIpcEndpoint — lifecycle, outbound, IPC subscribe/admit for ICQQ daemon.
3
3
  */
4
- import type { EndpointControl, EndpointInstance, EndpointManagement } from '@zhin.js/adapter';
4
+ import type { EndpointControl, EndpointInstance, EndpointManagement, EndpointSendRequest } from '@zhin.js/adapter';
5
5
  import type { MessageGateway } from '@zhin.js/core/runtime';
6
6
  import type { CapabilityId } from '@zhin.js/plugin-runtime';
7
7
  import { type IcqqInboundMessage, type IpcEvent, type IpcResponse, type ResolvedIcqqConfig } from './protocol.js';
@@ -48,10 +48,7 @@ export declare class IcqqIpcEndpoint implements EndpointInstance {
48
48
  open(): void;
49
49
  close(): void;
50
50
  stop(): Promise<void>;
51
- send({ target, payload }: {
52
- readonly target: string;
53
- readonly payload: unknown;
54
- }): Promise<string>;
51
+ send({ conversation, payload }: EndpointSendRequest): Promise<string>;
55
52
  private static readonly EMOJI_MAP;
56
53
  private getEmojiId;
57
54
  /** Activity-feedback / OutboundHost reaction surface. */
package/lib/endpoint.js CHANGED
@@ -5,7 +5,7 @@ import { buildIcqqInboxNoticeRow, buildIcqqInboxRequestRow, buildIcqqSystemReque
5
5
  import { IcqqGuildCatalog, isIcqqGuildIpcEvent, normalizeIcqqGuildInboundMessage, } from './icqq-guild.js';
6
6
  import { IpcClient } from './ipc-client.js';
7
7
  import { materializeOutboundBase64, resolveIcqqOutboundMediaMode, } from './outbound-media.js';
8
- import { Actions, formatInboundContent, formatInboundTarget, formatOutboundBody, parseSendTarget, } from './protocol.js';
8
+ import { Actions, formatInboundContent, formatOutboundBody, icqqInboundConversation, icqqOutboundTarget, } from './protocol.js';
9
9
  const logger = getLogger('icqq');
10
10
  export class IcqqIpcEndpoint {
11
11
  #options;
@@ -187,7 +187,7 @@ export class IcqqIpcEndpoint {
187
187
  this.#started = false;
188
188
  logger.debug(formatCompact({ op: 'disconnect', endpoint: this.name }));
189
189
  }
190
- async send({ target, payload }) {
190
+ async send({ conversation, payload }) {
191
191
  if (!this.ipc) {
192
192
  throw new Error(`icqq endpoint ${this.name} 未连接(IPC 未初始化或已停止),无法发送消息`);
193
193
  }
@@ -196,7 +196,7 @@ export class IcqqIpcEndpoint {
196
196
  ? materializeOutboundBase64(payload, mediaMode)
197
197
  : payload;
198
198
  const message = formatOutboundBody(content);
199
- const parsed = parseSendTarget(target);
199
+ const parsed = icqqOutboundTarget(conversation);
200
200
  let action;
201
201
  let params;
202
202
  switch (parsed.kind) {
@@ -233,7 +233,7 @@ export class IcqqIpcEndpoint {
233
233
  logger.debug(formatCompact({
234
234
  op: 'icqq_send',
235
235
  endpoint: this.name,
236
- target,
236
+ target: `${conversation.kind}:${conversation.id}`,
237
237
  messageId,
238
238
  }));
239
239
  return messageId;
@@ -499,13 +499,14 @@ export class IcqqIpcEndpoint {
499
499
  }
500
500
  /** Test / internal: admit when open. */
501
501
  admit(msg) {
502
+ const conversation = msg.conversation;
502
503
  if (!this.#open) {
503
504
  logger.debug(formatCompact({
504
505
  op: 'icqq_inbound_skip',
505
506
  reason: 'endpoint_closed',
506
507
  endpoint: this.name,
507
508
  id: msg.id,
508
- target: msg.target,
509
+ target: `${conversation.kind}:${conversation.id}`,
509
510
  }));
510
511
  return;
511
512
  }
@@ -519,7 +520,7 @@ export class IcqqIpcEndpoint {
519
520
  op: 'icqq_inbound_admit',
520
521
  endpoint: this.name,
521
522
  id: msg.id,
522
- target: msg.target,
523
+ target: `${conversation.kind}:${conversation.id}`,
523
524
  sender: msg.sender,
524
525
  channelType: msg.channelType,
525
526
  mentioned: mentioned || undefined,
@@ -527,12 +528,11 @@ export class IcqqIpcEndpoint {
527
528
  segments: summarizeSegmentTypes(msg.segments),
528
529
  }));
529
530
  void this.#options.gateway.receive({
530
- adapter: this.#options.id,
531
- target: msg.target,
531
+ conversation,
532
+ message: { conversation, id: msg.id },
532
533
  content: msg.content,
533
534
  ...(msg.segments ? { segments: msg.segments } : {}),
534
535
  sender: msg.sender,
535
- id: msg.id,
536
536
  metadata: Object.freeze({
537
537
  endpoint: this.name,
538
538
  channelType: msg.channelType,
@@ -542,7 +542,7 @@ export class IcqqIpcEndpoint {
542
542
  }).catch((err) => {
543
543
  logger.warn(formatCompact({
544
544
  op: 'icqq_gateway_receive_failed',
545
- target: msg.target,
545
+ target: `${conversation.kind}:${conversation.id}`,
546
546
  error: err instanceof Error ? err.message : String(err),
547
547
  }));
548
548
  });
@@ -616,7 +616,7 @@ export class IcqqIpcEndpoint {
616
616
  }
617
617
  this.admit({
618
618
  id: normalized.messageId,
619
- target: formatInboundTarget({
619
+ conversation: icqqInboundConversation(String(this.#options.id), {
620
620
  channelType: normalized.channelType,
621
621
  channelId: normalized.channelId,
622
622
  channelParentGroupId: normalized.channelParentGroupId,
@@ -656,7 +656,7 @@ export class IcqqIpcEndpoint {
656
656
  }
657
657
  this.admit({
658
658
  id: normalized.messageId,
659
- target: formatInboundTarget({
659
+ conversation: icqqInboundConversation(String(this.#options.id), {
660
660
  channelType: 'channel',
661
661
  channelId: normalized.channelId,
662
662
  guildId: normalized.guildId,
package/lib/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { Actions, formatInboundContent, formatInboundTarget, formatOutboundBody, parseSendTarget, resolveIcqqConfig, type IcqqAdapterConfig, type IcqqInboundMessage, type IcqqWireSegment, type IpcEvent, type IpcMessage, type IpcRequest, type IpcResponse, type ParsedIcqqSendTarget, type ResolvedIcqqConfig, } from './protocol.js';
1
+ export { Actions, formatInboundContent, formatOutboundBody, icqqInboundConversation, icqqOutboundTarget, resolveIcqqConfig, type IcqqAdapterConfig, type IcqqInboundMessage, type IcqqWireSegment, type IpcEvent, type IpcMessage, type IpcRequest, type IpcResponse, type ParsedIcqqSendTarget, type ResolvedIcqqConfig, } from './protocol.js';
2
2
  export { IcqqIpcEndpoint, type CreateIcqqIpc, type IcqqEndpointOptions, type IcqqInboxHooks, type IcqqIpcTransport, } from './endpoint.js';
3
3
  export * from './types.js';
4
4
  export { getIcqqAgentDeps, registerIcqqAgentEndpoint, setIcqqAgentDeps, type IcqqAgentDeps, type IcqqAgentEndpoint, } from './icqq-agent-deps.js';
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- export { Actions, formatInboundContent, formatInboundTarget, formatOutboundBody, parseSendTarget, resolveIcqqConfig, } from './protocol.js';
1
+ export { Actions, formatInboundContent, formatOutboundBody, icqqInboundConversation, icqqOutboundTarget, resolveIcqqConfig, } from './protocol.js';
2
2
  export { IcqqIpcEndpoint, } from './endpoint.js';
3
3
  export * from './types.js';
4
4
  export { getIcqqAgentDeps, registerIcqqAgentEndpoint, setIcqqAgentDeps, } from './icqq-agent-deps.js';
@@ -61,7 +61,7 @@ export declare class IpcClient {
61
61
  */
62
62
  request(action: string, params?: Record<string, unknown>, timeoutMs?: number): Promise<IpcResponse>;
63
63
  /**
64
- * @deprecated 兼容旧调用方式:服务端已自动推送事件,type/id 过滤在客户端完成。
64
+ * 订阅 daemon 推送事件:服务端自动推送全部事件,type/id 过滤在客户端完成。
65
65
  * @returns 订阅句柄,包含 unsubscribe() 方法
66
66
  */
67
67
  subscribe(_action: string, params: Record<string, unknown>, onEvent: (event: IpcEvent) => void): {
package/lib/ipc-client.js CHANGED
@@ -310,7 +310,7 @@ export class IpcClient {
310
310
  });
311
311
  }
312
312
  /**
313
- * @deprecated 兼容旧调用方式:服务端已自动推送事件,type/id 过滤在客户端完成。
313
+ * 订阅 daemon 推送事件:服务端自动推送全部事件,type/id 过滤在客户端完成。
314
314
  * @returns 订阅句柄,包含 unsubscribe() 方法
315
315
  */
316
316
  subscribe(_action, params, onEvent) {
package/lib/protocol.d.ts CHANGED
@@ -11,6 +11,7 @@
11
11
  *
12
12
  * 登录相关:**推送**走 IpcEvent(`system.login.*`)。
13
13
  */
14
+ import type { ConversationRef } from '@zhin.js/im-contract';
14
15
  import type { MessageSegment } from "zhin.js";
15
16
  /** CLI → Daemon 请求 */
16
17
  export type IpcRequest = {
@@ -236,7 +237,7 @@ export interface IcqqWireSegment {
236
237
  }
237
238
  export interface IcqqInboundMessage {
238
239
  readonly id: string;
239
- readonly target: string;
240
+ readonly conversation: ConversationRef;
240
241
  readonly content: string;
241
242
  /**
242
243
  * 结构化段视图(canonical Segment,经 toCanonicalSegments 归一)。
@@ -263,13 +264,20 @@ export type ParsedIcqqSendTarget = {
263
264
  readonly channelId: string;
264
265
  };
265
266
  export declare function resolveIcqqConfig(config?: IcqqAdapterConfig): ResolvedIcqqConfig;
266
- /** Gateway reply target: `private:uid` / `group:gid` / `temp:gid:uid` / `channel:guild:channel`. */
267
- export declare function formatInboundTarget(input: {
267
+ /**
268
+ * 入站归一化 → ConversationRef:群临时会话(temp)映射为群容器内的 private
269
+ * 会话(parent kind 'group');频道消息的所属 guild 进 `parent`(kind 'channel')。
270
+ */
271
+ export declare function icqqInboundConversation(endpointId: string, input: {
268
272
  readonly channelType: 'private' | 'group' | 'channel';
269
273
  readonly channelId: string;
270
274
  readonly channelParentGroupId?: string;
271
275
  readonly guildId?: string;
272
- }): string;
273
- export declare function parseSendTarget(target: string): ParsedIcqqSendTarget;
276
+ }): ConversationRef;
277
+ /**
278
+ * 出站:从 ConversationRef 派生 IPC action 路由。
279
+ * 群容器(parent kind 'group')内的 private 会话即 ICQQ 群临时会话(temp)。
280
+ */
281
+ export declare function icqqOutboundTarget(conversation: ConversationRef): ParsedIcqqSendTarget;
274
282
  export declare function formatOutboundBody(payload: unknown): string;
275
283
  export declare function formatInboundContent(rawMessage: string): string;
package/lib/protocol.js CHANGED
@@ -206,47 +206,53 @@ export function resolveIcqqConfig(config = {}) {
206
206
  ...(rpc ? { rpc } : {}),
207
207
  };
208
208
  }
209
- /** Gateway reply target: `private:uid` / `group:gid` / `temp:gid:uid` / `channel:guild:channel`. */
210
- export function formatInboundTarget(input) {
209
+ /**
210
+ * 入站归一化 → ConversationRef:群临时会话(temp)映射为群容器内的 private
211
+ * 会话(parent kind 'group');频道消息的所属 guild 进 `parent`(kind 'channel')。
212
+ */
213
+ export function icqqInboundConversation(endpointId, input) {
214
+ const endpoint = { id: endpointId, adapter: endpointId.split('\0')[0] ?? endpointId };
211
215
  if (input.channelType === 'private' && input.channelParentGroupId) {
212
- return `temp:${input.channelParentGroupId}:${input.channelId}`;
216
+ return {
217
+ endpoint,
218
+ kind: 'private',
219
+ id: input.channelId,
220
+ parent: { kind: 'group', id: input.channelParentGroupId },
221
+ };
213
222
  }
214
223
  if (input.channelType === 'channel' && input.guildId) {
215
- return `channel:${input.guildId}:${input.channelId}`;
224
+ return {
225
+ endpoint,
226
+ kind: 'channel',
227
+ id: input.channelId,
228
+ parent: { kind: 'channel', id: input.guildId },
229
+ };
216
230
  }
217
- return `${input.channelType}:${input.channelId}`;
231
+ return { endpoint, kind: input.channelType, id: input.channelId };
218
232
  }
219
- export function parseSendTarget(target) {
220
- const trimmed = target.trim();
221
- if (trimmed.startsWith('temp:')) {
222
- const rest = trimmed.slice(5);
223
- const [groupId, userId] = rest.split(':');
224
- if (!groupId || !userId)
225
- throw new TypeError(`Invalid icqq temp target: ${target}`);
226
- return { kind: 'temp', groupId: Number(groupId), userId: Number(userId) };
227
- }
228
- if (trimmed.startsWith('channel:')) {
229
- const rest = trimmed.slice(8);
230
- const idx = rest.indexOf(':');
231
- if (idx <= 0)
232
- throw new TypeError(`Invalid icqq channel target: ${target}`);
233
+ /**
234
+ * 出站:从 ConversationRef 派生 IPC action 路由。
235
+ * 群容器(parent kind 'group')内的 private 会话即 ICQQ 群临时会话(temp)。
236
+ */
237
+ export function icqqOutboundTarget(conversation) {
238
+ if (conversation.kind === 'private' && conversation.parent?.kind === 'group') {
233
239
  return {
234
- kind: 'channel',
235
- guildId: rest.slice(0, idx),
236
- channelId: rest.slice(idx + 1),
240
+ kind: 'temp',
241
+ groupId: Number(conversation.parent.id),
242
+ userId: Number(conversation.id),
237
243
  };
238
244
  }
239
- if (trimmed.startsWith('private:')) {
240
- return { kind: 'private', userId: Number(trimmed.slice(8)) };
245
+ if (conversation.kind === 'private') {
246
+ return { kind: 'private', userId: Number(conversation.id) };
241
247
  }
242
- if (trimmed.startsWith('group:')) {
243
- return { kind: 'group', groupId: Number(trimmed.slice(6)) };
248
+ if (conversation.kind === 'group') {
249
+ return { kind: 'group', groupId: Number(conversation.id) };
244
250
  }
245
- // Bare numeric id → private (common reply path)
246
- if (/^\d+$/.test(trimmed)) {
247
- return { kind: 'private', userId: Number(trimmed) };
251
+ const guildId = conversation.parent?.kind === 'channel' ? conversation.parent.id : undefined;
252
+ if (!guildId) {
253
+ throw new TypeError(`Invalid icqq channel conversation (missing guild parent): ${conversation.id}`);
248
254
  }
249
- throw new TypeError(`Invalid icqq send target: ${target}`);
255
+ return { kind: 'channel', guildId, channelId: conversation.id };
250
256
  }
251
257
  export function formatOutboundBody(payload) {
252
258
  if (typeof payload === 'string') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-icqq",
3
- "version": "8.0.1",
3
+ "version": "8.0.3",
4
4
  "description": "Zhin.js ICQQ adapter for Plugin Runtime (IPC daemon client)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -34,19 +34,20 @@
34
34
  "directory": "plugins/adapters/icqq"
35
35
  },
36
36
  "dependencies": {
37
- "@zhin.js/adapter": "1.1.4",
38
- "@zhin.js/command": "1.0.6",
39
- "@zhin.js/core": "1.5.1",
37
+ "@zhin.js/adapter": "1.1.5",
38
+ "@zhin.js/command": "1.0.7",
39
+ "@zhin.js/core": "1.5.2",
40
+ "@zhin.js/im-contract": "1.0.1",
40
41
  "@zhin.js/logger": "1.0.75",
41
- "@zhin.js/plugin-runtime": "1.1.2"
42
+ "@zhin.js/plugin-runtime": "1.1.3"
42
43
  },
43
44
  "peerDependencies": {
44
45
  "zod": "^4.0.0",
45
- "@zhin.js/adapter": "1.1.4",
46
- "@zhin.js/agent": "1.1.1",
47
- "@zhin.js/core": "1.5.1",
48
- "@zhin.js/plugin-runtime": "1.1.2",
49
- "zhin.js": "6.0.1"
46
+ "@zhin.js/adapter": "1.1.5",
47
+ "@zhin.js/agent": "1.1.3",
48
+ "@zhin.js/core": "1.5.2",
49
+ "@zhin.js/plugin-runtime": "1.1.3",
50
+ "zhin.js": "6.0.2"
50
51
  },
51
52
  "peerDependenciesMeta": {
52
53
  "zhin.js": {
@@ -64,8 +65,8 @@
64
65
  "typescript": "^6.0.3",
65
66
  "vitest": "^4.1.10",
66
67
  "zod": "^4.4.3",
67
- "@zhin.js/agent": "1.1.1",
68
- "zhin.js": "6.0.1"
68
+ "@zhin.js/agent": "1.1.3",
69
+ "zhin.js": "6.0.2"
69
70
  },
70
71
  "files": [
71
72
  "adapters",
package/src/endpoint.ts CHANGED
@@ -1,7 +1,12 @@
1
1
  /**
2
2
  * IcqqIpcEndpoint — lifecycle, outbound, IPC subscribe/admit for ICQQ daemon.
3
3
  */
4
- import type { EndpointControl, EndpointInstance, EndpointManagement } from '@zhin.js/adapter';
4
+ import type {
5
+ EndpointControl,
6
+ EndpointInstance,
7
+ EndpointManagement,
8
+ EndpointSendRequest,
9
+ } from '@zhin.js/adapter';
5
10
  import type { MessageGateway } from '@zhin.js/core/runtime';
6
11
  import { formatCompact, getLogger } from '@zhin.js/logger';
7
12
  import type { CapabilityId } from '@zhin.js/plugin-runtime';
@@ -38,9 +43,9 @@ import {
38
43
  import {
39
44
  Actions,
40
45
  formatInboundContent,
41
- formatInboundTarget,
42
46
  formatOutboundBody,
43
- parseSendTarget,
47
+ icqqInboundConversation,
48
+ icqqOutboundTarget,
44
49
  type IcqqInboundMessage,
45
50
  type IpcEvent,
46
51
  type IpcResponse,
@@ -265,7 +270,7 @@ export class IcqqIpcEndpoint implements EndpointInstance {
265
270
  logger.debug(formatCompact({ op: 'disconnect', endpoint: this.name }));
266
271
  }
267
272
 
268
- async send({ target, payload }: { readonly target: string; readonly payload: unknown }): Promise<string> {
273
+ async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
269
274
  if (!this.ipc) {
270
275
  throw new Error(`icqq endpoint ${this.name} 未连接(IPC 未初始化或已停止),无法发送消息`);
271
276
  }
@@ -274,7 +279,7 @@ export class IcqqIpcEndpoint implements EndpointInstance {
274
279
  ? materializeOutboundBase64(payload, mediaMode)
275
280
  : payload;
276
281
  const message = formatOutboundBody(content);
277
- const parsed = parseSendTarget(target);
282
+ const parsed = icqqOutboundTarget(conversation);
278
283
  let action: string;
279
284
  let params: Record<string, unknown>;
280
285
  switch (parsed.kind) {
@@ -313,7 +318,7 @@ export class IcqqIpcEndpoint implements EndpointInstance {
313
318
  logger.debug(formatCompact({
314
319
  op: 'icqq_send',
315
320
  endpoint: this.name,
316
- target,
321
+ target: `${conversation.kind}:${conversation.id}`,
317
322
  messageId,
318
323
  }));
319
324
  return messageId;
@@ -630,13 +635,14 @@ export class IcqqIpcEndpoint implements EndpointInstance {
630
635
 
631
636
  /** Test / internal: admit when open. */
632
637
  admit(msg: IcqqInboundMessage): void {
638
+ const conversation = msg.conversation;
633
639
  if (!this.#open) {
634
640
  logger.debug(formatCompact({
635
641
  op: 'icqq_inbound_skip',
636
642
  reason: 'endpoint_closed',
637
643
  endpoint: this.name,
638
644
  id: msg.id,
639
- target: msg.target,
645
+ target: `${conversation.kind}:${conversation.id}`,
640
646
  }));
641
647
  return;
642
648
  }
@@ -650,7 +656,7 @@ export class IcqqIpcEndpoint implements EndpointInstance {
650
656
  op: 'icqq_inbound_admit',
651
657
  endpoint: this.name,
652
658
  id: msg.id,
653
- target: msg.target,
659
+ target: `${conversation.kind}:${conversation.id}`,
654
660
  sender: msg.sender,
655
661
  channelType: msg.channelType,
656
662
  mentioned: mentioned || undefined,
@@ -658,12 +664,11 @@ export class IcqqIpcEndpoint implements EndpointInstance {
658
664
  segments: summarizeSegmentTypes(msg.segments),
659
665
  }));
660
666
  void this.#options.gateway.receive({
661
- adapter: this.#options.id,
662
- target: msg.target,
667
+ conversation,
668
+ message: { conversation, id: msg.id },
663
669
  content: msg.content,
664
670
  ...(msg.segments ? { segments: msg.segments } : {}),
665
671
  sender: msg.sender,
666
- id: msg.id,
667
672
  metadata: Object.freeze({
668
673
  endpoint: this.name,
669
674
  channelType: msg.channelType,
@@ -673,7 +678,7 @@ export class IcqqIpcEndpoint implements EndpointInstance {
673
678
  }).catch((err) => {
674
679
  logger.warn(formatCompact({
675
680
  op: 'icqq_gateway_receive_failed',
676
- target: msg.target,
681
+ target: `${conversation.kind}:${conversation.id}`,
677
682
  error: err instanceof Error ? err.message : String(err),
678
683
  }));
679
684
  });
@@ -747,7 +752,7 @@ export class IcqqIpcEndpoint implements EndpointInstance {
747
752
  }
748
753
  this.admit({
749
754
  id: normalized.messageId,
750
- target: formatInboundTarget({
755
+ conversation: icqqInboundConversation(String(this.#options.id), {
751
756
  channelType: normalized.channelType,
752
757
  channelId: normalized.channelId,
753
758
  channelParentGroupId: normalized.channelParentGroupId,
@@ -789,7 +794,7 @@ export class IcqqIpcEndpoint implements EndpointInstance {
789
794
  }
790
795
  this.admit({
791
796
  id: normalized.messageId,
792
- target: formatInboundTarget({
797
+ conversation: icqqInboundConversation(String(this.#options.id), {
793
798
  channelType: 'channel',
794
799
  channelId: normalized.channelId,
795
800
  guildId: normalized.guildId,
package/src/index.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  export {
2
2
  Actions,
3
3
  formatInboundContent,
4
- formatInboundTarget,
5
4
  formatOutboundBody,
6
- parseSendTarget,
5
+ icqqInboundConversation,
6
+ icqqOutboundTarget,
7
7
  resolveIcqqConfig,
8
8
  type IcqqAdapterConfig,
9
9
  type IcqqInboundMessage,
package/src/ipc-client.ts CHANGED
@@ -366,7 +366,7 @@ export class IpcClient {
366
366
  }
367
367
 
368
368
  /**
369
- * @deprecated 兼容旧调用方式:服务端已自动推送事件,type/id 过滤在客户端完成。
369
+ * 订阅 daemon 推送事件:服务端自动推送全部事件,type/id 过滤在客户端完成。
370
370
  * @returns 订阅句柄,包含 unsubscribe() 方法
371
371
  */
372
372
  subscribe(
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
 
@@ -313,7 +314,7 @@ 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 归一)。
@@ -354,51 +355,59 @@ export function resolveIcqqConfig(config: IcqqAdapterConfig = {}): ResolvedIcqqC
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(endpointId: 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: endpointId, adapter: endpointId.split('\0')[0] ?? endpointId };
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 {