@zhin.js/adapter-icqq 1.1.17 → 1.1.19

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.
Files changed (59) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/README.md +55 -10
  3. package/adapters/icqq/index.js +3 -19
  4. package/adapters/icqq/index.ts +2 -25
  5. package/commands/icqq/endpoint/definition.js +3 -25
  6. package/commands/icqq/endpoint/definition.ts +3 -24
  7. package/commands/icqq/login/code/index.js +28 -0
  8. package/commands/icqq/login/code/index.ts +30 -0
  9. package/commands/icqq/login/confirm/index.js +20 -0
  10. package/commands/icqq/login/confirm/index.ts +21 -0
  11. package/commands/icqq/login/definition.js +65 -0
  12. package/commands/icqq/login/definition.ts +72 -0
  13. package/commands/icqq/login/qr/index.js +53 -0
  14. package/commands/icqq/login/qr/index.ts +55 -0
  15. package/commands/icqq/login/retry/index.js +21 -0
  16. package/commands/icqq/login/retry/index.ts +23 -0
  17. package/commands/icqq/login/status/index.js +31 -0
  18. package/commands/icqq/login/status/index.ts +34 -0
  19. package/lib/adapter.d.ts +13 -0
  20. package/lib/adapter.js +27 -0
  21. package/lib/client.d.ts +6 -9
  22. package/lib/client.js +1 -3
  23. package/lib/console-spec.d.ts +17 -0
  24. package/lib/console-spec.js +21 -0
  25. package/lib/console.d.ts +2 -0
  26. package/lib/console.js +13 -0
  27. package/lib/endpoint.d.ts +12 -15
  28. package/lib/endpoint.js +373 -164
  29. package/lib/group-moderation.d.ts +6 -3
  30. package/lib/group-moderation.js +9 -8
  31. package/lib/group-scene.d.ts +3 -3
  32. package/lib/group-scene.js +8 -7
  33. package/lib/icqq-login-assist.d.ts +3 -1
  34. package/lib/icqq-login-assist.js +3 -1
  35. package/lib/icqq-login-control.d.ts +24 -0
  36. package/lib/icqq-login-control.js +25 -0
  37. package/lib/index.d.ts +3 -2
  38. package/lib/index.js +2 -2
  39. package/lib/protocol.d.ts +13 -0
  40. package/lib/protocol.js +24 -9
  41. package/package.json +25 -17
  42. package/pages/index/index.js +15 -0
  43. package/pages/index/index.ts +16 -0
  44. package/plugin.js +3 -5
  45. package/schema.json +174 -0
  46. package/src/adapter.ts +29 -0
  47. package/src/client.ts +6 -11
  48. package/src/console-spec.ts +26 -0
  49. package/src/console.ts +17 -0
  50. package/src/endpoint.ts +381 -178
  51. package/src/group-moderation.ts +10 -10
  52. package/src/group-scene.ts +8 -8
  53. package/src/icqq-login-assist.ts +6 -2
  54. package/src/icqq-login-control.ts +42 -0
  55. package/src/index.ts +4 -1
  56. package/src/protocol.ts +35 -8
  57. package/lib/icqq-runtime-state.d.ts +0 -1
  58. package/lib/icqq-runtime-state.js +0 -6
  59. package/src/icqq-runtime-state.ts +0 -7
@@ -1,17 +1,17 @@
1
1
  import type { Client } from '@icqqjs/icqq';
2
- import type { GroupModerationAction, GroupModerationMember } from 'zhin.js/adapter';
2
+ import { createEndpointOperation, type EndpointMemberInspectionRequest, type EndpointModerationRequest, type GroupModerationMember } from 'zhin.js/adapter';
3
3
 
4
4
  /** Native role/join facts stay in the adapter; the moderation plugin owns policy. */
5
5
  export async function inspectGroupMember(
6
- client: Client, groupId: string, userId: string, signal: AbortSignal,
6
+ client: Client, { groupId, userId, signal }: EndpointMemberInspectionRequest,
7
7
  ): Promise<GroupModerationMember> {
8
- signal.throwIfAborted();
8
+ const operation = createEndpointOperation({ signal });
9
9
  const group = numericId(groupId);
10
10
  const user = numericId(userId);
11
- const selfLookup = client.getGroupMemberInfo(group, client.uin, true);
11
+ const selfLookup = operation.run(() => client.getGroupMemberInfo(group, client.uin, true));
12
12
  const [self, member] = await Promise.all([
13
13
  selfLookup,
14
- user === client.uin ? selfLookup : client.getGroupMemberInfo(group, user, true),
14
+ user === client.uin ? selfLookup : operation.run(() => client.getGroupMemberInfo(group, user, true)),
15
15
  ]);
16
16
  signal.throwIfAborted();
17
17
  const joinedAt = member.join_time * 1000;
@@ -29,10 +29,10 @@ export async function inspectGroupMember(
29
29
  }
30
30
 
31
31
  export async function moderateGroupMember(
32
- client: Client, groupId: string, userId: string, action: GroupModerationAction,
33
- signal: AbortSignal, isActive: () => boolean,
32
+ { client, isActive }: { readonly client: Client; readonly isActive: () => boolean },
33
+ { groupId, userId, action, signal }: EndpointModerationRequest,
34
34
  ): Promise<void> {
35
- const member = await inspectGroupMember(client, groupId, userId, signal);
35
+ const member = await inspectGroupMember(client, { groupId: groupId, userId: userId, signal: signal });
36
36
  signal.throwIfAborted();
37
37
  if (!isActive() || !member.canModerate) throw new Error('Group moderation permission is unavailable');
38
38
  if (action.type === 'kick') {
@@ -45,8 +45,8 @@ export async function moderateGroupMember(
45
45
  throw new Error('Invalid group mute duration');
46
46
  }
47
47
  const ok = action.type === 'kick'
48
- ? await client.setGroupKick(numericId(groupId), numericId(userId))
49
- : await client.setGroupBan(numericId(groupId), numericId(userId), action.durationSeconds);
48
+ ? await createEndpointOperation({ signal }).run(() => client.setGroupKick(numericId(groupId), numericId(userId)))
49
+ : await createEndpointOperation({ signal }).run(() => client.setGroupBan(numericId(groupId), numericId(userId), action.durationSeconds));
50
50
  if (!ok) throw new Error('Platform did not confirm the moderation action');
51
51
  }
52
52
 
@@ -1,5 +1,5 @@
1
1
  import type { Client } from '@icqqjs/icqq';
2
- import type { GroupMemberProfile, GroupSceneFacts } from 'zhin.js/adapter';
2
+ import { createEndpointOperation, type EndpointGroupInspectionRequest, type EndpointMemberInspectionRequest, type GroupMemberProfile, type GroupSceneFacts } from 'zhin.js/adapter';
3
3
 
4
4
  /** Listing a very large roster only to find admins is not worth the packet; adminIds stay unknown. */
5
5
  export const ADMIN_SCAN_MAX_MEMBERS = 1000;
@@ -19,10 +19,10 @@ function numericId(value: string): number {
19
19
  * ADR 0041 scene facts: one uncached group-info read, plus (small groups only) one roster read for
20
20
  * admins. A roster failure leaves adminIds unknown instead of failing the other facts.
21
21
  */
22
- export async function inspectGroupScene(client: Client, groupId: string, signal: AbortSignal): Promise<GroupSceneFacts> {
23
- signal.throwIfAborted();
22
+ export async function inspectGroupScene(client: Client, { groupId, signal }: EndpointGroupInspectionRequest): Promise<GroupSceneFacts> {
23
+ const operation = createEndpointOperation({ signal });
24
24
  const group = numericId(groupId);
25
- const info = await client.getGroupInfo(group, true);
25
+ const info = await operation.run(() => client.getGroupInfo(group, true));
26
26
  signal.throwIfAborted();
27
27
  if (info.group_id !== group) return {};
28
28
  const memberCount = Number.isSafeInteger(info.member_count) && info.member_count > 0 ? info.member_count : undefined;
@@ -30,7 +30,7 @@ export async function inspectGroupScene(client: Client, groupId: string, signal:
30
30
  let adminIds: string[] | undefined;
31
31
  if (memberCount === undefined || memberCount <= ADMIN_SCAN_MAX_MEMBERS) {
32
32
  try {
33
- const roster = await client.getGroupMemberList(group);
33
+ const roster = await operation.run(() => client.getGroupMemberList(group));
34
34
  signal.throwIfAborted();
35
35
  adminIds = [...roster.values()]
36
36
  .filter(member => member.role === 'admin' && member.group_id === group)
@@ -55,12 +55,12 @@ export async function inspectGroupScene(client: Client, groupId: string, signal:
55
55
 
56
56
  /** ADR 0041 member facts from one uncached lookup (same rationale as the agent `freshMember` helper). */
57
57
  export async function inspectGroupMemberProfile(
58
- client: Client, groupId: string, userId: string, signal: AbortSignal,
58
+ client: Client, { groupId, userId, signal }: EndpointMemberInspectionRequest,
59
59
  ): Promise<GroupMemberProfile> {
60
- signal.throwIfAborted();
60
+ const operation = createEndpointOperation({ signal });
61
61
  const group = numericId(groupId);
62
62
  const user = numericId(userId);
63
- const member = await client.getGroupMemberInfo(group, user, true);
63
+ const member = await operation.run(() => client.getGroupMemberInfo(group, user, true));
64
64
  signal.throwIfAborted();
65
65
  if (member.user_id !== user || member.group_id !== group) return {};
66
66
  const card = typeof member.card === 'string' && member.card.trim() ? member.card.trim() : undefined;
@@ -1,6 +1,6 @@
1
1
  import type { Locale } from 'zhin.js/i18n';
2
2
  import { messages as t } from './i18n.js';
3
- import type { LoginAssist, LoginAssistType, PendingLoginTaskPayload } from '@zhin.js/core';
3
+ import type { LoginAssist, LoginAssistEndpointIdentity, LoginAssistType, PendingLoginTaskPayload } from '@zhin.js/core';
4
4
  import { formatCompact } from '@zhin.js/logger';
5
5
 
6
6
  type IcqqLoginClient = {
@@ -26,6 +26,8 @@ export async function runIcqqLoginAssistStep(input: {
26
26
  adapter: string;
27
27
  endpointKey: string;
28
28
  owner: object;
29
+ signal?: AbortSignal;
30
+ endpointIdentity?: LoginAssistEndpointIdentity;
29
31
  logger: Logger;
30
32
  step: LoginAssistType;
31
33
  event: unknown;
@@ -44,6 +46,7 @@ export async function runIcqqLoginAssistStep(input: {
44
46
  }));
45
47
 
46
48
  try {
49
+ input.signal?.throwIfAborted();
47
50
  if (step === 'device') {
48
51
  try {
49
52
  await client.sendSmsCode();
@@ -56,7 +59,8 @@ export async function runIcqqLoginAssistStep(input: {
56
59
  }
57
60
  }
58
61
 
59
- const value = await assist.waitForInput(adapter, endpointKey, step, payload, { owner });
62
+ const value = await assist.waitForInput(adapter, endpointKey, step, payload, { owner, signal: input.signal, endpointIdentity: input.endpointIdentity });
63
+ input.signal?.throwIfAborted();
60
64
  const text = normalizeSubmitValue(value);
61
65
 
62
66
  switch (step) {
@@ -0,0 +1,42 @@
1
+ /**
2
+ * ICQQ login control surface exposed to the Plugin Runtime, Console and the
3
+ * recovery command. Controls are projected from actual current incarnations in
4
+ * the owning IcqqAdapter; no plugin resource keeps a second Endpoint directory.
5
+ */
6
+ import { adapterFeatureId, isAdapterIndex } from 'zhin.js/adapter';
7
+ import type { FeatureId, PluginId } from 'zhin.js';
8
+ import { IcqqAdapter } from './adapter.js';
9
+ import type { PendingLoginTask } from '@zhin.js/core';
10
+
11
+ export interface IcqqLoginControlState {
12
+ readonly endpointId: string;
13
+ readonly transportState: string;
14
+ readonly online: boolean;
15
+ readonly pending: readonly PendingLoginTask[];
16
+ }
17
+
18
+ export interface IcqqLoginControl {
19
+ readonly endpointId: string;
20
+ state(): IcqqLoginControlState;
21
+ pending(): readonly PendingLoginTask[];
22
+ /** Force a login attempt; rejects when the endpoint is stopped or the SDK login fails. */
23
+ relogin(): Promise<void>;
24
+ submit(taskId: string, value: string | Record<string, unknown>): boolean;
25
+ cancel(taskId: string, reason?: string): boolean;
26
+ }
27
+
28
+ /** Read the owning Adapter's actual incarnations; this projection retains no independent directory. */
29
+ export function icqqLoginControls(context: Readonly<{ owner: PluginId | Readonly<{ id: PluginId }>; project<T>(id: FeatureId): T }>): ReadonlyMap<string, IcqqLoginControl> {
30
+ const index = context.project<unknown>(adapterFeatureId);
31
+ if (!isAdapterIndex(index)) throw new Error('ICQQ Adapter projection is unavailable');
32
+ const owner = typeof context.owner === 'string' ? context.owner : context.owner.id;
33
+ const controls = new Map<string, IcqqLoginControl>();
34
+ for (const adapter of index.forOwner(owner).adapters()) {
35
+ if (!(adapter instanceof IcqqAdapter)) continue;
36
+ for (const row of adapter.list()) {
37
+ const endpoint = adapter.getEndpoint(row.instanceId);
38
+ if (endpoint) controls.set(row.endpointId, endpoint.loginControl);
39
+ }
40
+ }
41
+ return controls;
42
+ }
package/src/index.ts CHANGED
@@ -3,12 +3,14 @@ export {
3
3
  formatInboundContent,
4
4
  formatOutboundBody,
5
5
  formatOutboundMessages,
6
+ formatOutboundMessageParts,
6
7
  icqqInboundConversation,
7
8
  icqqOutboundTarget,
8
9
  type ActionResult,
9
10
  type IcqqEndpointConfig,
10
11
  type IcqqInboundMessage,
11
12
  type IcqqWireSegment,
13
+ type IcqqOutboundMessagePart,
12
14
  type ParsedIcqqSendTarget,
13
15
  type ResolvedIcqqConfig,
14
16
  } from './protocol.js';
@@ -21,7 +23,6 @@ export {
21
23
  export * from './types.js';
22
24
 
23
25
  export {
24
- icqqClient,
25
26
  type IcqqClient,
26
27
  type IcqqClientEventMap,
27
28
  } from './client.js';
@@ -29,3 +30,5 @@ export {
29
30
  export {
30
31
  resolveIcqqInboundMessageId,
31
32
  } from './icqq-inbound.js';
33
+
34
+ export { IcqqAdapter } from './adapter.js';
package/src/protocol.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { MessageElem, Sendable, ShareElem } from '@icqqjs/icqq';
2
2
  import { isMediaRef, type ConversationRef } from '@zhin.js/im-contract';
3
+ import { resolveAdapterNotifyTarget, type AdapterNotifyTarget, type ResolvedAdapterNotifyTarget } from 'zhin.js/adapter';
3
4
  import type { MessageSegment, SendContent } from "zhin.js";
4
5
  import { resolveIcqqMediaFile } from "./cq-message.js";
5
6
 
@@ -246,6 +247,8 @@ export interface IcqqEndpointConfig {
246
247
  readonly autoServer?: boolean;
247
248
  readonly qqnt?: boolean;
248
249
  readonly ntLogin?: boolean;
250
+ /** Optional out-of-band outage notification target (usually a QQ adapter instance). */
251
+ readonly notify?: AdapterNotifyTarget;
249
252
  }
250
253
 
251
254
  export interface ResolvedIcqqConfig {
@@ -264,6 +267,7 @@ export interface ResolvedIcqqConfig {
264
267
  readonly autoServer?: boolean;
265
268
  readonly qqnt?: boolean;
266
269
  readonly ntLogin?: boolean;
270
+ readonly notify?: ResolvedAdapterNotifyTarget;
267
271
  }
268
272
 
269
273
  export interface IcqqWireSegment {
@@ -273,6 +277,9 @@ export interface IcqqWireSegment {
273
277
  }
274
278
 
275
279
  export interface IcqqInboundMessage {
280
+ /** Exact SDK callback object, operation-local and never a persisted projection. */
281
+ readonly event?: unknown;
282
+ readonly platformSentAt?: number;
276
283
  readonly id: string;
277
284
  readonly conversation: ConversationRef;
278
285
  readonly content: string;
@@ -311,6 +318,7 @@ export function resolveIcqqConfig(config: IcqqEndpointConfig): ResolvedIcqqConfi
311
318
  const autoServer = config.autoServer ?? true;
312
319
  const qqnt = config.qqnt;
313
320
  const ntLogin = config.ntLogin;
321
+ const notify = resolveAdapterNotifyTarget(config.notify);
314
322
  return {
315
323
  context: 'icqq',
316
324
  id,
@@ -327,6 +335,7 @@ export function resolveIcqqConfig(config: IcqqEndpointConfig): ResolvedIcqqConfi
327
335
  ...(autoServer != null ? { autoServer } : {}),
328
336
  ...(qqnt != null ? { qqnt } : {}),
329
337
  ...(ntLogin != null ? { ntLogin } : {}),
338
+ ...(notify ? { notify } : {}),
330
339
  };
331
340
  }
332
341
 
@@ -456,19 +465,37 @@ export function formatOutboundBody(payload: unknown): Sendable {
456
465
  * quote or whitespace are dropped: a standalone element cannot carry the quote.
457
466
  */
458
467
  export function formatOutboundMessages(payload: unknown): Sendable[] {
468
+ return formatOutboundMessageParts(payload).map(part => part.message);
469
+ }
470
+
471
+ /** Native bubble and the exact source segment positions which produced it. */
472
+ export interface IcqqOutboundMessagePart {
473
+ readonly message: Sendable;
474
+ readonly sourceIndices?: readonly number[];
475
+ }
476
+
477
+ export function formatOutboundMessageParts(payload: unknown): IcqqOutboundMessagePart[] {
459
478
  if (!Array.isArray(payload)) {
460
- return isWireSegment(payload) ? formatOutboundMessages([payload]) : [formatOutboundBody(payload)];
479
+ if (isWireSegment(payload)) return formatOutboundMessageParts([payload]);
480
+ const message = formatOutboundBody(payload);
481
+ return [{ message, ...(typeof payload === 'string' && message !== payload ? {} : { sourceIndices: [0] }) }];
461
482
  }
462
- const groups: (string | MessageElem)[][] = [[]];
463
- for (const element of formatOutboundElements(payload)) {
464
- if (isStandaloneElement(element)) groups.push([element], []);
465
- else groups.at(-1)!.push(element);
483
+ const groups: { element: string | MessageElem; sourceIndex: number }[][] = [[]];
484
+ for (const [sourceIndex, source] of payload.entries()) {
485
+ for (const element of formatOutboundElements([source])) {
486
+ const item = { element, sourceIndex };
487
+ if (isStandaloneElement(element)) groups.push([item], []);
488
+ else groups.at(-1)!.push(item);
489
+ }
466
490
  }
467
- const messages = groups.filter(group => group.some(element => typeof element === 'string'
491
+ const messages = groups.filter(group => group.some(({ element }) => typeof element === 'string'
468
492
  ? element.trim() !== ''
469
493
  : element.type !== 'reply' && element.type !== 'quote'));
470
- if (messages.length === 0) return ['\u200b'];
471
- return messages.map(group => group.length === 1 ? group[0]! : group);
494
+ if (messages.length === 0) return [{ message: '\u200b', sourceIndices: [] }];
495
+ return messages.map(group => ({
496
+ message: group.length === 1 ? group[0]!.element : group.map(item => item.element),
497
+ sourceIndices: [...new Set(group.map(item => item.sourceIndex))],
498
+ }));
472
499
  }
473
500
 
474
501
  function isStandaloneElement(element: string | MessageElem): element is MessageElem {
@@ -1 +0,0 @@
1
- export declare const icqqRuntimeStateToken: import("zhin.js").Token<import("@zhin.js/adapter").EndpointRuntimeState>;
@@ -1,6 +0,0 @@
1
- /**
2
- * ICQQ 插件实例的运行时状态:adapter create() 注册的 endpoint 列表。
3
- * 由 plugin.ts setup() provide,adapter create 与 `icqq endpoint` 命令共享(同一 owner generation)。
4
- */
5
- import { defineEndpointRuntimeStateToken } from 'zhin.js/adapter';
6
- export const icqqRuntimeStateToken = defineEndpointRuntimeStateToken('icqq');
@@ -1,7 +0,0 @@
1
- /**
2
- * ICQQ 插件实例的运行时状态:adapter create() 注册的 endpoint 列表。
3
- * 由 plugin.ts setup() provide,adapter create 与 `icqq endpoint` 命令共享(同一 owner generation)。
4
- */
5
- import { defineEndpointRuntimeStateToken } from 'zhin.js/adapter';
6
-
7
- export const icqqRuntimeStateToken = defineEndpointRuntimeStateToken('icqq');