@zhin.js/adapter-icqq 1.1.3 → 1.1.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/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
1
1
  # @zhin.js/adapter-icqq
2
2
 
3
+ ## 1.1.5
4
+
5
+ ### Patch Changes
6
+
7
+ - b9b3953: Fix adapter connection races, transport health reporting, candidate rollback and delivery acknowledgement boundaries. Separate generation admission from locally observed connectivity, reject unconfirmed message receipts, and mark uncertain delivery results without recommending automatic replay.
8
+
9
+ Class and compact endpoints should expose a `transportState` getter to report physical connection or local listener health. Unobserved endpoints now report an unknown transport and remain offline in diagnostics. Platform stability tiers remain unchanged until real account acceptance evidence is available.
10
+
11
+ - Updated dependencies [b9b3953]
12
+ - Updated dependencies [b9b3953]
13
+ - Updated dependencies [b9b3953]
14
+ - Updated dependencies [b9b3953]
15
+ - Updated dependencies [b9b3953]
16
+ - Updated dependencies [b9b3953]
17
+ - Updated dependencies [b9b3953]
18
+ - @zhin.js/adapter@1.1.16
19
+ - @zhin.js/im-contract@1.1.3
20
+ - @zhin.js/core@1.1.40
21
+ - @zhin.js/tool@1.1.3
22
+ - @zhin.js/skill@1.1.3
23
+ - @zhin.js/agent-feature@1.1.3
24
+
25
+ ## 1.1.4
26
+
27
+ ### Patch Changes
28
+
29
+ - 43eda99: Unify Notice and Request payloads with their public types and runtime exports. Use camelCase fields, preserve native data under metadata, and bind canonical endpoint identity and request actions to the held generation. Remove legacy data aliases and from factories.
30
+
31
+ Keep login and lifecycle signals as independent SystemEvent payloads on system.receive, without chat conversation fields or user interaction. Update built-in adapter projections and authoring guidance.
32
+
33
+ - Updated dependencies [43eda99]
34
+ - Updated dependencies [0de4836]
35
+ - @zhin.js/core@1.1.39
36
+ - @zhin.js/permission@1.1.3
37
+ - @zhin.js/adapter@1.1.15
38
+
3
39
  ## 1.1.3
4
40
 
5
41
  ### Patch Changes
package/README.md CHANGED
@@ -175,3 +175,9 @@ IM turn 的场景,才使用 `icqqClient.get(context, endpointId)` 显式选择
175
175
  ## License
176
176
 
177
177
  MIT
178
+
179
+ ### 发送回执与未知结果
180
+
181
+ 发送成功回执只接受平台真实消息 ID。网络超时、断线、缺少消息 ID 的响应会报告 `deliveryUnknown`,框架不会自动重发;手动重发可能重复发送。
182
+
183
+ ICQQ 原生分享和文件 API 可能已经成功,但只返回空值或文件 ID;这类结果不能当作可引用、可撤回的消息 ID,因此报告投递结果未知。平台操作仍只执行一次。文件撤回应使用平台专用文件 API。
package/lib/endpoint.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Client } from '@icqqjs/icqq';
2
- import { Endpoint, type EndpointControl, type EndpointManagement, type EndpointSendRequest } from 'zhin.js/adapter';
2
+ import { Endpoint, type EndpointControl, type EndpointManagement, type EndpointSendRequest, type EndpointTransportState } from 'zhin.js/adapter';
3
3
  import type { EndpointContentPort } from '@zhin.js/adapter';
4
4
  import { type LoginAssist } from '@zhin.js/core';
5
5
  import type { CapabilityId } from 'zhin.js';
@@ -20,6 +20,7 @@ export declare class IcqqEndpoint extends Endpoint<Client> {
20
20
  readonly management: EndpointManagement;
21
21
  readonly control: EndpointControl;
22
22
  constructor(options: IcqqEndpointOptions);
23
+ get transportState(): EndpointTransportState;
23
24
  start(signal: AbortSignal): Promise<void>;
24
25
  open(): void;
25
26
  close(): Promise<void>;
package/lib/endpoint.js CHANGED
@@ -2,7 +2,8 @@ import { Client, parseGroupMessageId, } from '@icqqjs/icqq';
2
2
  import { inspect } from 'node:util';
3
3
  import { AsyncLocalStorage } from 'node:async_hooks';
4
4
  import { Endpoint, } from 'zhin.js/adapter';
5
- import { receiveOneBotLikeSideEvent, SystemEvent, toCanonicalSegments } from '@zhin.js/core';
5
+ import { receiveOneBotLikeSideEvent, buildSystem, toCanonicalSegments } from '@zhin.js/core';
6
+ import { EndpointDeliveryError } from '@zhin.js/im-contract';
6
7
  import { formatCompact, getAdapterLogger, truncatePreview } from '@zhin.js/logger';
7
8
  import { runIcqqLoginAssistStep } from './icqq-login-assist.js';
8
9
  import { IcqqContentResolver } from './content-resolver.js';
@@ -49,6 +50,8 @@ export class IcqqEndpoint extends Endpoint {
49
50
  #open = false;
50
51
  #retiring = false;
51
52
  #started = false;
53
+ #stopped = false;
54
+ #everOnline = false;
52
55
  #retirementEpoch = 0;
53
56
  #retirementArmed = false;
54
57
  #disposeNativeEvents;
@@ -144,10 +147,22 @@ export class IcqqEndpoint extends Endpoint {
144
147
  }));
145
148
  });
146
149
  }
150
+ get transportState() {
151
+ if (this.#stopped || this.#retiring)
152
+ return 'stopped';
153
+ if (!this.#started)
154
+ return 'idle';
155
+ if (this.client.isOnline())
156
+ return 'open';
157
+ if (!this.#everOnline)
158
+ return 'connecting';
159
+ return this.#options.config.autoReconnect ? 'reconnecting' : 'closed';
160
+ }
147
161
  async start(signal) {
148
162
  if (this.#started)
149
163
  return;
150
164
  signal.throwIfAborted();
165
+ this.#stopped = false;
151
166
  // Invalidate a guard left by a prior lifecycle epoch before admitting a
152
167
  // new login attempt on this endpoint instance.
153
168
  this.#retirementEpoch += 1;
@@ -204,6 +219,7 @@ export class IcqqEndpoint extends Endpoint {
204
219
  await Promise.allSettled([...this.#inflightInbound]);
205
220
  }
206
221
  async stop() {
222
+ this.#stopped = true;
207
223
  this.#assertExternalLifecycle('stop');
208
224
  this.#armRetirementFence();
209
225
  await Promise.allSettled([...this.#inflightInbound]);
@@ -259,7 +275,7 @@ export class IcqqEndpoint extends Endpoint {
259
275
  }
260
276
  async send({ conversation, payload }) {
261
277
  if (!this.#started)
262
- throw new Error('icqq endpoint 未连接');
278
+ throw new EndpointDeliveryError('endpoint_disconnected', 'icqq endpoint 未连接', 'not_sent');
263
279
  const mediaMode = resolveIcqqOutboundMediaMode(this.#options.config);
264
280
  const prepared = isIcqqSegmentPayload(payload)
265
281
  ? prepareIcqqOutboundMedia(payload, mediaMode)
@@ -281,7 +297,11 @@ export class IcqqEndpoint extends Endpoint {
281
297
  }));
282
298
  throw error;
283
299
  }
284
- const messageId = String(result?.message_id ?? `sent_${Date.now()}`);
300
+ const rawId = result?.message_id;
301
+ if ((typeof rawId !== 'string' && typeof rawId !== 'number') || String(rawId).trim() === '' || (typeof rawId === 'number' && !Number.isFinite(rawId))) {
302
+ throw new EndpointDeliveryError('delivery_unconfirmed', 'Message delivery is unconfirmed: ICQQ returned no message_id', 'unknown');
303
+ }
304
+ const messageId = String(rawId);
285
305
  this.#logger.info(`send ${conversation.kind}:${conversation.id} | id: ${messageId} | ${truncatePreview(preview, 80)}`);
286
306
  return messageId;
287
307
  }
@@ -314,12 +334,12 @@ export class IcqqEndpoint extends Endpoint {
314
334
  if (isIcqqFileElement(message)) {
315
335
  switch (target.kind) {
316
336
  case 'private': {
317
- const id = await this.client.pickFriend(target.userId).sendFile(message.file, message.name);
318
- return { message_id: id };
337
+ await this.client.pickFriend(target.userId).sendFile(message.file, message.name);
338
+ return {};
319
339
  }
320
340
  case 'group': {
321
- const stat = await this.client.pickGroup(target.groupId).sendFile(message.file, '/', message.name);
322
- return { message_id: stat.fid };
341
+ await this.client.pickGroup(target.groupId).sendFile(message.file, '/', message.name);
342
+ return {};
323
343
  }
324
344
  case 'temp':
325
345
  case 'channel':
@@ -489,6 +509,7 @@ export class IcqqEndpoint extends Endpoint {
489
509
  this.client.on('system.online', (event) => {
490
510
  if (this.#retiring)
491
511
  return;
512
+ this.#everOnline = true;
492
513
  try {
493
514
  this.#resolveStartOnline?.();
494
515
  this.#options.loginAssist.cancelOwned(this.#loginAssistOwner, 'online');
@@ -578,16 +599,13 @@ export class IcqqEndpoint extends Endpoint {
578
599
  });
579
600
  }
580
601
  #dispatchIcqqSystemSideEvent(type, payload) {
581
- const { scene_type, sub_type } = parseIcqqSystemScene(type);
582
- void this.emit('system.receive', SystemEvent.from(payload ?? {}, {
583
- $id: `system:${this.endpointName}:${type}:${Date.now()}`,
584
- $adapter: 'icqq',
585
- $endpoint: this.endpointName,
586
- $type: 'system',
587
- $scene_id: this.endpointName,
588
- $scene_type: scene_type,
589
- $sub_type: sub_type,
590
- $timestamp: Date.now(),
602
+ void this.emit('system.receive', buildSystem(payload ?? {}, {
603
+ id: `system:${this.endpointName}:${type}:${Date.now()}`,
604
+ clientAdapter: 'icqq',
605
+ endpointId: this.endpointName,
606
+ type: 'system',
607
+ name: `system.${type}`,
608
+ timestamp: Date.now(),
591
609
  })).catch((err) => {
592
610
  this.#logger.warn(formatCompact({
593
611
  op: 'icqq_system_side_event_failed',
@@ -984,20 +1002,5 @@ function serializeIcqqEvent(event) {
984
1002
  }
985
1003
  return Object.keys(out).length > 0 ? out : null;
986
1004
  }
987
- function parseIcqqSystemScene(type) {
988
- if (type.startsWith('login.')) {
989
- return { scene_type: 'login', sub_type: type.slice('login.'.length) || 'unknown' };
990
- }
991
- if (type.startsWith('offline.')) {
992
- return { scene_type: 'offline', sub_type: type.slice('offline.'.length) || 'unknown' };
993
- }
994
- if (type === 'online') {
995
- return { scene_type: 'online', sub_type: 'online' };
996
- }
997
- if (type === 'offline') {
998
- return { scene_type: 'offline', sub_type: 'offline' };
999
- }
1000
- return { scene_type: 'system', sub_type: type };
1001
- }
1002
1005
  const INBOUND_HOLD_LIMIT = 32;
1003
1006
  const ICQQ_SERIALIZE_SKIP = ['group', 'member', 'friend', 'discuss', 'client'];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-icqq",
3
- "version": "1.1.3",
3
+ "version": "1.1.5",
4
4
  "description": "Zhin.js ICQQ adapter — direct connection to @icqqjs/icqq",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -34,24 +34,24 @@
34
34
  "directory": "plugins/adapters/icqq"
35
35
  },
36
36
  "dependencies": {
37
- "@zhin.js/adapter": "1.1.15",
38
- "@zhin.js/agent-feature": "1.1.2",
39
- "@zhin.js/core": "1.1.38",
37
+ "@zhin.js/adapter": "1.1.16",
38
+ "@zhin.js/agent-feature": "1.1.3",
39
+ "@zhin.js/core": "1.1.40",
40
40
  "@zhin.js/feature-kit": "1.1.2",
41
- "@zhin.js/im-contract": "1.1.2",
41
+ "@zhin.js/im-contract": "1.1.3",
42
42
  "@zhin.js/logger": "1.1.1",
43
- "@zhin.js/permission": "1.1.2",
44
- "@zhin.js/skill": "1.1.2",
45
- "@zhin.js/tool": "1.1.2"
43
+ "@zhin.js/permission": "1.1.3",
44
+ "@zhin.js/skill": "1.1.3",
45
+ "@zhin.js/tool": "1.1.3"
46
46
  },
47
47
  "peerDependencies": {
48
48
  "@icqqjs/icqq": "^1.12.2",
49
49
  "zod": "^4.0.0",
50
- "@zhin.js/adapter": "^1.1.15",
51
- "@zhin.js/command": "^1.1.2",
52
- "@zhin.js/core": "^1.1.38",
50
+ "@zhin.js/adapter": "^1.1.16",
51
+ "@zhin.js/command": "^1.1.4",
52
+ "@zhin.js/core": "^1.1.40",
53
53
  "@zhin.js/prompt-section": "^1.1.2",
54
- "zhin.js": "^1.1.3"
54
+ "zhin.js": "^1.1.5"
55
55
  },
56
56
  "peerDependenciesMeta": {
57
57
  "@icqqjs/icqq": {
@@ -77,7 +77,7 @@
77
77
  "vitest": "^4.1.10",
78
78
  "zod": "^4.4.3",
79
79
  "@zhin.js/prompt-section": "1.1.2",
80
- "zhin.js": "1.1.3"
80
+ "zhin.js": "1.1.5"
81
81
  },
82
82
  "files": [
83
83
  "adapters",
package/src/endpoint.ts CHANGED
@@ -16,10 +16,11 @@ import {
16
16
  type EndpointManagement,
17
17
  type EndpointPendingRequest,
18
18
  type EndpointSendRequest,
19
+ type EndpointTransportState,
19
20
  } from 'zhin.js/adapter';
20
21
  import type { EndpointContentPort } from '@zhin.js/adapter';
21
- import { receiveOneBotLikeSideEvent, SystemEvent, toCanonicalSegments, type LoginAssist } from '@zhin.js/core';
22
- import type { MessageRef } from '@zhin.js/im-contract';
22
+ import { receiveOneBotLikeSideEvent, buildSystem, toCanonicalSegments, type LoginAssist } from '@zhin.js/core';
23
+ import { EndpointDeliveryError, type MessageRef } from '@zhin.js/im-contract';
23
24
  import { formatCompact, getAdapterLogger, truncatePreview } from '@zhin.js/logger';
24
25
  import type { CapabilityId } from 'zhin.js';
25
26
  import { runIcqqLoginAssistStep } from './icqq-login-assist.js';
@@ -103,6 +104,8 @@ export class IcqqEndpoint extends Endpoint<Client> {
103
104
  #open = false;
104
105
  #retiring = false;
105
106
  #started = false;
107
+ #stopped = false;
108
+ #everOnline = false;
106
109
  #retirementEpoch = 0;
107
110
  #retirementArmed = false;
108
111
  #disposeNativeEvents?: () => void;
@@ -207,9 +210,18 @@ export class IcqqEndpoint extends Endpoint<Client> {
207
210
  });
208
211
  }
209
212
 
213
+ get transportState(): EndpointTransportState {
214
+ if (this.#stopped || this.#retiring) return 'stopped';
215
+ if (!this.#started) return 'idle';
216
+ if (this.client.isOnline()) return 'open';
217
+ if (!this.#everOnline) return 'connecting';
218
+ return this.#options.config.autoReconnect ? 'reconnecting' : 'closed';
219
+ }
220
+
210
221
  async start(signal: AbortSignal): Promise<void> {
211
222
  if (this.#started) return;
212
223
  signal.throwIfAborted();
224
+ this.#stopped = false;
213
225
  // Invalidate a guard left by a prior lifecycle epoch before admitting a
214
226
  // new login attempt on this endpoint instance.
215
227
  this.#retirementEpoch += 1;
@@ -268,6 +280,7 @@ export class IcqqEndpoint extends Endpoint<Client> {
268
280
  }
269
281
 
270
282
  async stop(): Promise<void> {
283
+ this.#stopped = true;
271
284
  this.#assertExternalLifecycle('stop');
272
285
  this.#armRetirementFence();
273
286
  await Promise.allSettled([...this.#inflightInbound]);
@@ -319,7 +332,7 @@ export class IcqqEndpoint extends Endpoint<Client> {
319
332
  }
320
333
 
321
334
  async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
322
- if (!this.#started) throw new Error('icqq endpoint 未连接');
335
+ if (!this.#started) throw new EndpointDeliveryError('endpoint_disconnected', 'icqq endpoint 未连接', 'not_sent');
323
336
  const mediaMode = resolveIcqqOutboundMediaMode(this.#options.config);
324
337
  const prepared = isIcqqSegmentPayload(payload)
325
338
  ? prepareIcqqOutboundMedia(payload, mediaMode)
@@ -340,7 +353,11 @@ export class IcqqEndpoint extends Endpoint<Client> {
340
353
  }));
341
354
  throw error;
342
355
  }
343
- const messageId = String(result?.message_id ?? `sent_${Date.now()}`);
356
+ const rawId = result?.message_id;
357
+ if ((typeof rawId !== 'string' && typeof rawId !== 'number') || String(rawId).trim() === '' || (typeof rawId === 'number' && !Number.isFinite(rawId))) {
358
+ throw new EndpointDeliveryError('delivery_unconfirmed', 'Message delivery is unconfirmed: ICQQ returned no message_id', 'unknown');
359
+ }
360
+ const messageId = String(rawId);
344
361
  this.#logger.info(
345
362
  `send ${conversation.kind}:${conversation.id} | id: ${messageId} | ${truncatePreview(preview, 80)}`,
346
363
  );
@@ -378,12 +395,12 @@ export class IcqqEndpoint extends Endpoint<Client> {
378
395
  if (isIcqqFileElement(message)) {
379
396
  switch (target.kind) {
380
397
  case 'private': {
381
- const id = await this.client.pickFriend(target.userId).sendFile(message.file, message.name);
382
- return { message_id: id };
398
+ await this.client.pickFriend(target.userId).sendFile(message.file, message.name);
399
+ return {};
383
400
  }
384
401
  case 'group': {
385
- const stat = await this.client.pickGroup(target.groupId).sendFile(message.file, '/', message.name);
386
- return { message_id: stat.fid };
402
+ await this.client.pickGroup(target.groupId).sendFile(message.file, '/', message.name);
403
+ return {};
387
404
  }
388
405
  case 'temp':
389
406
  case 'channel':
@@ -560,6 +577,7 @@ export class IcqqEndpoint extends Endpoint<Client> {
560
577
  #bindSystemOnlineEvent(): void {
561
578
  this.client.on('system.online', (event) => {
562
579
  if (this.#retiring) return;
580
+ this.#everOnline = true;
563
581
  try {
564
582
  this.#resolveStartOnline?.();
565
583
  this.#options.loginAssist.cancelOwned(this.#loginAssistOwner, 'online');
@@ -659,16 +677,13 @@ export class IcqqEndpoint extends Endpoint<Client> {
659
677
  }
660
678
 
661
679
  #dispatchIcqqSystemSideEvent(type: string, payload: Record<string, unknown> | null): void {
662
- const { scene_type, sub_type } = parseIcqqSystemScene(type);
663
- void this.emit('system.receive', SystemEvent.from(payload ?? {}, {
664
- $id: `system:${this.endpointName}:${type}:${Date.now()}`,
665
- $adapter: 'icqq' as never,
666
- $endpoint: this.endpointName,
667
- $type: 'system',
668
- $scene_id: this.endpointName,
669
- $scene_type: scene_type,
670
- $sub_type: sub_type,
671
- $timestamp: Date.now(),
680
+ void this.emit('system.receive', buildSystem(payload ?? {}, {
681
+ id: `system:${this.endpointName}:${type}:${Date.now()}`,
682
+ clientAdapter: 'icqq',
683
+ endpointId: this.endpointName,
684
+ type: 'system',
685
+ name: `system.${type}`,
686
+ timestamp: Date.now(),
672
687
  })).catch((err) => {
673
688
  this.#logger.warn(formatCompact({
674
689
  op: 'icqq_system_side_event_failed',
@@ -1063,22 +1078,6 @@ function serializeIcqqEvent(event: unknown): Record<string, unknown> | null {
1063
1078
  return Object.keys(out).length > 0 ? out : null;
1064
1079
  }
1065
1080
 
1066
- function parseIcqqSystemScene(type: string): { scene_type: string; sub_type: string } {
1067
- if (type.startsWith('login.')) {
1068
- return { scene_type: 'login', sub_type: type.slice('login.'.length) || 'unknown' };
1069
- }
1070
- if (type.startsWith('offline.')) {
1071
- return { scene_type: 'offline', sub_type: type.slice('offline.'.length) || 'unknown' };
1072
- }
1073
- if (type === 'online') {
1074
- return { scene_type: 'online', sub_type: 'online' };
1075
- }
1076
- if (type === 'offline') {
1077
- return { scene_type: 'offline', sub_type: 'offline' };
1078
- }
1079
- return { scene_type: 'system', sub_type: type };
1080
- }
1081
-
1082
1081
  const INBOUND_HOLD_LIMIT = 32;
1083
1082
 
1084
1083
  const ICQQ_SERIALIZE_SKIP = ['group', 'member', 'friend', 'discuss', 'client'] as const;