@zhin.js/adapter-icqq 1.1.4 → 1.1.6

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,48 @@
1
1
  # @zhin.js/adapter-icqq
2
2
 
3
+ ## 1.1.6
4
+
5
+ ### Patch Changes
6
+
7
+ - badcc1f: Unify lazy JSX and HTML delivery across commands, inbound middleware and registered components. Remove the former JSX/template component runtimes, respect declared adapter HTML policies, and preserve author/generation ownership for automatic and explicit replies.
8
+
9
+ Add the optional @zhin.js/components library with composable JSXRenderable display props, shared themes, symmetric spacing and local style/text overrides. Include tables, lists, Markdown with highlighted code blocks, and display-only checkbox, radio, switch and button states; provide a reusable preview gallery. Update scaffolds, examples and public authoring documentation together.
10
+
11
+ Add optional official Tailwind static-utility compilation to inline styles. Consolidate raster rendering on @pixel.js/shotium, remove the Satori image toolkit, and align raster format contracts with actual PNG/JPEG/WebP results.
12
+
13
+ Reject cascade-dependent Tailwind values including unset, track the installed Tailwind version in the bundled theme provenance, and preserve both repository and upstream MIT attribution. Keep optional gallery dependency failures visible and document asynchronous Tailwind initialization.
14
+
15
+ - Updated dependencies [badcc1f]
16
+ - @zhin.js/core@1.1.41
17
+ - @zhin.js/feature-kit@1.1.3
18
+ - @zhin.js/adapter@1.1.17
19
+ - @zhin.js/agent-feature@1.1.4
20
+ - @zhin.js/skill@1.1.4
21
+ - @zhin.js/tool@1.1.4
22
+ - @zhin.js/permission@1.1.4
23
+
24
+ ## 1.1.5
25
+
26
+ ### Patch Changes
27
+
28
+ - 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.
29
+
30
+ 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.
31
+
32
+ - Updated dependencies [b9b3953]
33
+ - Updated dependencies [b9b3953]
34
+ - Updated dependencies [b9b3953]
35
+ - Updated dependencies [b9b3953]
36
+ - Updated dependencies [b9b3953]
37
+ - Updated dependencies [b9b3953]
38
+ - Updated dependencies [b9b3953]
39
+ - @zhin.js/adapter@1.1.16
40
+ - @zhin.js/im-contract@1.1.3
41
+ - @zhin.js/core@1.1.40
42
+ - @zhin.js/tool@1.1.3
43
+ - @zhin.js/skill@1.1.3
44
+ - @zhin.js/agent-feature@1.1.3
45
+
3
46
  ## 1.1.4
4
47
 
5
48
  ### 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
@@ -3,6 +3,7 @@ import { inspect } from 'node:util';
3
3
  import { AsyncLocalStorage } from 'node:async_hooks';
4
4
  import { Endpoint, } from 'zhin.js/adapter';
5
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');
@@ -624,7 +624,8 @@ function mergeReplyFromSource(content, data) {
624
624
  }
625
625
  function quoteIdFromSegments(content) {
626
626
  const reply = content.find((item) => item.type === 'reply');
627
- const value = reply?.data?.message_id ?? reply?.data?.id;
627
+ const data = reply?.data;
628
+ const value = data?.message_id ?? data?.id;
628
629
  return value == null || !String(value).trim() ? undefined : String(value).trim();
629
630
  }
630
631
  export function resolveInboundContent(data) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-icqq",
3
- "version": "1.1.4",
3
+ "version": "1.1.6",
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.39",
40
- "@zhin.js/feature-kit": "1.1.2",
41
- "@zhin.js/im-contract": "1.1.2",
37
+ "@zhin.js/adapter": "1.1.17",
38
+ "@zhin.js/agent-feature": "1.1.4",
39
+ "@zhin.js/core": "1.1.41",
40
+ "@zhin.js/feature-kit": "1.1.3",
41
+ "@zhin.js/im-contract": "1.1.3",
42
42
  "@zhin.js/logger": "1.1.1",
43
- "@zhin.js/permission": "1.1.3",
44
- "@zhin.js/skill": "1.1.2",
45
- "@zhin.js/tool": "1.1.2"
43
+ "@zhin.js/permission": "1.1.4",
44
+ "@zhin.js/skill": "1.1.4",
45
+ "@zhin.js/tool": "1.1.4"
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.3",
52
- "@zhin.js/core": "^1.1.39",
53
- "@zhin.js/prompt-section": "^1.1.2",
54
- "zhin.js": "^1.1.4"
50
+ "@zhin.js/adapter": "^1.1.17",
51
+ "@zhin.js/command": "^1.1.5",
52
+ "@zhin.js/core": "^1.1.41",
53
+ "@zhin.js/prompt-section": "^1.1.3",
54
+ "zhin.js": "^1.1.6"
55
55
  },
56
56
  "peerDependenciesMeta": {
57
57
  "@icqqjs/icqq": {
@@ -76,8 +76,8 @@
76
76
  "typescript": "^6.0.3",
77
77
  "vitest": "^4.1.10",
78
78
  "zod": "^4.4.3",
79
- "@zhin.js/prompt-section": "1.1.2",
80
- "zhin.js": "1.1.4"
79
+ "@zhin.js/prompt-section": "1.1.3",
80
+ "zhin.js": "1.1.6"
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
22
  import { receiveOneBotLikeSideEvent, buildSystem, toCanonicalSegments, type LoginAssist } from '@zhin.js/core';
22
- import type { MessageRef } from '@zhin.js/im-contract';
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');
@@ -781,7 +781,8 @@ function mergeReplyFromSource(
781
781
 
782
782
  function quoteIdFromSegments(content: readonly MessageSegment[]): string | undefined {
783
783
  const reply = content.find((item) => item.type === 'reply');
784
- const value = reply?.data?.message_id ?? reply?.data?.id;
784
+ const data = reply?.data as Readonly<Record<string, unknown>> | undefined;
785
+ const value = data?.message_id ?? data?.id;
785
786
  return value == null || !String(value).trim() ? undefined : String(value).trim();
786
787
  }
787
788