@zhin.js/adapter-icqq 1.1.0 → 1.1.2

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 (83) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/README.md +15 -6
  3. package/adapters/{icqq.js → icqq/index.js} +6 -6
  4. package/adapters/{icqq.ts → icqq/index.ts} +10 -10
  5. package/agents/icqq/agent.json +20 -0
  6. package/agents/icqq/boundaries.md +3 -0
  7. package/agents/icqq/conventions.md +3 -0
  8. package/agents/icqq/skills/icqq-directory/SKILL.md +20 -0
  9. package/{tools/friend_list.js → agents/icqq/skills/icqq-directory/tools/friend_list/index.js} +2 -1
  10. package/{tools/friend_list.ts → agents/icqq/skills/icqq-directory/tools/friend_list/index.ts} +2 -1
  11. package/{tools/group_list.js → agents/icqq/skills/icqq-directory/tools/group_list/index.js} +2 -1
  12. package/{tools/group_list.ts → agents/icqq/skills/icqq-directory/tools/group_list/index.ts} +2 -1
  13. package/{tools/group_member_info.js → agents/icqq/skills/icqq-directory/tools/group_member_info/index.js} +3 -1
  14. package/{tools/group_member_info.ts → agents/icqq/skills/icqq-directory/tools/group_member_info/index.ts} +3 -1
  15. package/{tools/user_info.js → agents/icqq/skills/icqq-directory/tools/user_info/index.js} +1 -0
  16. package/{tools/user_info.ts → agents/icqq/skills/icqq-directory/tools/user_info/index.ts} +1 -0
  17. package/agents/icqq/skills/icqq-group-admin/SKILL.md +25 -0
  18. package/{tools/announce.js → agents/icqq/skills/icqq-group-admin/tools/announce/index.js} +2 -0
  19. package/{tools/announce.ts → agents/icqq/skills/icqq-group-admin/tools/announce/index.ts} +2 -0
  20. package/{tools/essence.js → agents/icqq/skills/icqq-group-admin/tools/essence/index.js} +3 -1
  21. package/{tools/essence.ts → agents/icqq/skills/icqq-group-admin/tools/essence/index.ts} +3 -1
  22. package/{tools/group_invite.js → agents/icqq/skills/icqq-group-admin/tools/group_invite/index.js} +1 -1
  23. package/{tools/group_invite.ts → agents/icqq/skills/icqq-group-admin/tools/group_invite/index.ts} +1 -1
  24. package/{tools/group_kick.js → agents/icqq/skills/icqq-group-admin/tools/group_kick/index.js} +4 -3
  25. package/{tools/group_kick.ts → agents/icqq/skills/icqq-group-admin/tools/group_kick/index.ts} +4 -3
  26. package/{tools/set_anonymous.js → agents/icqq/skills/icqq-group-admin/tools/set_anonymous/index.js} +3 -1
  27. package/{tools/set_anonymous.ts → agents/icqq/skills/icqq-group-admin/tools/set_anonymous/index.ts} +3 -1
  28. package/{tools/set_mute.js → agents/icqq/skills/icqq-group-admin/tools/set_mute/index.js} +3 -2
  29. package/{tools/set_mute.ts → agents/icqq/skills/icqq-group-admin/tools/set_mute/index.ts} +3 -2
  30. package/{tools/set_title.js → agents/icqq/skills/icqq-group-admin/tools/set_title/index.js} +3 -2
  31. package/{tools/set_title.ts → agents/icqq/skills/icqq-group-admin/tools/set_title/index.ts} +3 -2
  32. package/agents/icqq/skills/icqq-group-state/SKILL.md +17 -0
  33. package/{tools/group_files.js → agents/icqq/skills/icqq-group-state/tools/group_files/index.js} +3 -1
  34. package/{tools/group_files.ts → agents/icqq/skills/icqq-group-state/tools/group_files/index.ts} +3 -1
  35. package/{tools/list_muted.js → agents/icqq/skills/icqq-group-state/tools/list_muted/index.js} +2 -1
  36. package/{tools/list_muted.ts → agents/icqq/skills/icqq-group-state/tools/list_muted/index.ts} +2 -1
  37. package/agents/icqq/skills/icqq-interaction/SKILL.md +19 -0
  38. package/{tools/group_sign.js → agents/icqq/skills/icqq-interaction/tools/group_sign/index.js} +2 -1
  39. package/{tools/group_sign.ts → agents/icqq/skills/icqq-interaction/tools/group_sign/index.ts} +2 -1
  40. package/{tools/poke.js → agents/icqq/skills/icqq-interaction/tools/poke/index.js} +2 -1
  41. package/{tools/poke.ts → agents/icqq/skills/icqq-interaction/tools/poke/index.ts} +2 -1
  42. package/{tools/send_user_like.js → agents/icqq/skills/icqq-interaction/tools/send_user_like/index.js} +4 -3
  43. package/{tools/send_user_like.ts → agents/icqq/skills/icqq-interaction/tools/send_user_like/index.ts} +3 -3
  44. package/agents/icqq/system.md +3 -0
  45. package/commands/{endpoint/add/[[id]].js → icqq/endpoint/add/[[id]]/index.js} +1 -1
  46. package/commands/icqq/endpoint/add/[[id]]/index.ts +3 -0
  47. package/{lib/icqq-endpoint-commands.js → commands/icqq/endpoint/definition.js} +10 -5
  48. package/{src/icqq-endpoint-commands.ts → commands/icqq/endpoint/definition.ts} +9 -5
  49. package/commands/{endpoint/list.js → icqq/endpoint/list/index.js} +1 -1
  50. package/commands/icqq/endpoint/list/index.ts +3 -0
  51. package/commands/{endpoint/remove/[id].js → icqq/endpoint/remove/[id]/index.js} +1 -1
  52. package/commands/icqq/endpoint/remove/[id]/index.ts +3 -0
  53. package/lib/content-resolver.d.ts +10 -0
  54. package/lib/content-resolver.js +98 -0
  55. package/lib/cq-message.d.ts +1 -1
  56. package/lib/cq-message.js +1 -1
  57. package/lib/endpoint.d.ts +1 -2
  58. package/lib/endpoint.js +34 -77
  59. package/lib/icqq-runtime-state.js +1 -1
  60. package/lib/index.d.ts +1 -1
  61. package/lib/outbound-media.d.ts +1 -1
  62. package/lib/outbound-media.js +1 -1
  63. package/lib/protocol.d.ts +4 -22
  64. package/lib/protocol.js +52 -55
  65. package/lib/types.d.ts +0 -6
  66. package/package.json +29 -23
  67. package/plugin.js +1 -1
  68. package/prompt-sections/platform/index.js +16 -0
  69. package/{agent/prompt-sections/platform.ts → prompt-sections/platform/index.ts} +3 -4
  70. package/schema.json +7 -1
  71. package/src/content-resolver.ts +131 -0
  72. package/src/cq-message.ts +1 -1
  73. package/src/endpoint.ts +43 -97
  74. package/src/icqq-runtime-state.ts +1 -1
  75. package/src/index.ts +1 -1
  76. package/src/outbound-media.ts +2 -2
  77. package/src/protocol.ts +56 -80
  78. package/src/types.ts +0 -7
  79. package/commands/endpoint/add/[[id]].ts +0 -3
  80. package/commands/endpoint/list.ts +0 -3
  81. package/commands/endpoint/remove/[id].ts +0 -3
  82. package/lib/icqq-endpoint-commands.d.ts +0 -1
  83. /package/{agent/PERMITS.md → PERMITS.md} +0 -0
@@ -7,7 +7,8 @@ export default defineAgentTool({
7
7
  group_id: z.number().describe('目标群号'),
8
8
  }),
9
9
  adapter: 'icqq',
10
- permissions: ['role(master,admin)'],
10
+ scopes: ['group'],
11
+ requiresApproval: 'never',
11
12
  async execute({ group_id }, context) {
12
13
  const members = await context.$client.getGroupMemberList(group_id);
13
14
  const now = Math.floor(Date.now() / 1000);
@@ -6,7 +6,8 @@ export default defineAgentTool<{ group_id: number }>({
6
6
  group_id: z.number().describe('目标群号'),
7
7
  }),
8
8
  adapter: 'icqq',
9
- permissions: ['role(master,admin)'],
9
+ scopes:['group'],
10
+ requiresApproval: 'never',
10
11
  async execute({ group_id }, context) {
11
12
  const members = await context.$client.getGroupMemberList(group_id);
12
13
  const now = Math.floor(Date.now() / 1000);
@@ -0,0 +1,19 @@
1
+ ---
2
+ name: icqq-interaction
3
+ platforms:
4
+ - icqq
5
+ description: ICQQ 点赞、戳一戳和群签到互动能力。
6
+ keywords:
7
+ - "icqq"
8
+ - "点赞"
9
+ - "戳一戳"
10
+ - "签到"
11
+ tools:
12
+ - "poke"
13
+ - "send_user_like"
14
+ - "group_sign"
15
+ ---
16
+
17
+ # icqq-interaction
18
+
19
+ 用于低风险社交互动。确认目标用户或群后再调用,并遵守各 Tool 的审批策略。
@@ -7,7 +7,8 @@ export default defineAgentTool({
7
7
  group_id: z.number().describe('目标群号'),
8
8
  }),
9
9
  adapter: 'icqq',
10
- approval: 'never',
10
+ scopes: ['group'],
11
+ requiresApproval: 'never',
11
12
  async execute({ group_id }, context) {
12
13
  const client = context.$client;
13
14
  const groupInfo = client.pickGroup(group_id);
@@ -6,7 +6,8 @@ export default defineAgentTool<{ group_id: number }>({
6
6
  group_id: z.number().describe('目标群号'),
7
7
  }),
8
8
  adapter: 'icqq',
9
- approval: 'never',
9
+ scopes:['group'],
10
+ requiresApproval: 'never',
10
11
  async execute({ group_id }, context) {
11
12
  const client = context.$client;
12
13
  const groupInfo= client.pickGroup(group_id);
@@ -8,7 +8,8 @@ export default defineAgentTool({
8
8
  user_id: z.number().describe('要戳的目标成员 QQ号'),
9
9
  }),
10
10
  adapter: 'icqq',
11
- approval: 'never',
11
+ scopes: ['group'],
12
+ requiresApproval: 'never',
12
13
  async execute({ group_id, user_id }, context) {
13
14
  await context.$client.sendGroupPoke(group_id, user_id);
14
15
  return { success: true, message: `已戳了戳 ${user_id}` };
@@ -7,7 +7,8 @@ export default defineAgentTool<{ group_id: number; user_id: number }>({
7
7
  user_id: z.number().describe('要戳的目标成员 QQ号'),
8
8
  }),
9
9
  adapter: 'icqq',
10
- approval: 'never',
10
+ scopes:['group'],
11
+ requiresApproval: 'never',
11
12
  async execute({ group_id, user_id }, context) {
12
13
  await context.$client.sendGroupPoke(group_id, user_id);
13
14
  return { success: true, message: `已戳了戳 ${user_id}` };
@@ -7,13 +7,14 @@ export default defineAgentTool({
7
7
  user_id: z.number().describe('要点赞的目标用户 QQ号'),
8
8
  }),
9
9
  adapter: 'icqq',
10
- approval: 'never',
10
+ requiresApproval: 'never',
11
11
  async execute({ user_id }, context) {
12
12
  const successArray = [];
13
13
  await Promise.all([20, 20, 10].map(async (times) => {
14
14
  try {
15
- await context.$client.sendLike(user_id, times);
16
- successArray.push(times);
15
+ const isSuccess = await context.$client.sendLike(user_id, times);
16
+ if (isSuccess)
17
+ successArray.push(times);
17
18
  }
18
19
  catch { /* ignore */ }
19
20
  }));
@@ -6,13 +6,13 @@ export default defineAgentTool<{ user_id: number }>({
6
6
  user_id: z.number().describe('要点赞的目标用户 QQ号'),
7
7
  }),
8
8
  adapter: 'icqq',
9
- approval: 'never',
9
+ requiresApproval: 'never',
10
10
  async execute({ user_id }, context) {
11
11
  const successArray: number[] = [];
12
12
  await Promise.all([20, 20, 10].map(async (times) => {
13
13
  try {
14
- await context.$client.sendLike(user_id, times);
15
- successArray.push(times);
14
+ const isSuccess= await context.$client.sendLike(user_id, times);
15
+ if (isSuccess) successArray.push(times);
16
16
  } catch { /* ignore */ }
17
17
  }));
18
18
  const successTimes = successArray.reduce((a, b) => a + b, 0);
@@ -0,0 +1,3 @@
1
+ # ICQQ Platform Agent
2
+
3
+ You are the icqq platform specialist. Interpret requests in the current icqq conversation and load only the narrowest private Skill needed for the task.
@@ -1,3 +1,3 @@
1
1
  // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
- import { icqqEndpointCommands } from "../../../lib/icqq-endpoint-commands.js";
2
+ import { icqqEndpointCommands } from '../../definition.js';
3
3
  export default icqqEndpointCommands.add;
@@ -0,0 +1,3 @@
1
+ import { icqqEndpointCommands } from '../../definition.js';
2
+
3
+ export default icqqEndpointCommands.add;
@@ -1,21 +1,26 @@
1
- import { addEndpointToConfig, createEndpointCommands } from 'zhin.js/adapter';
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { createEndpointCommands, endpointConfigurationStoreToken } from 'zhin.js/adapter';
2
3
  import { defineCommand } from 'zhin.js/command';
3
- import { icqqRuntimeStateToken } from './icqq-runtime-state.js';
4
+ import { icqqRuntimeStateToken } from "../../../lib/icqq-runtime-state.js";
4
5
  export const icqqEndpointCommands = createEndpointCommands({
5
6
  adapterKey: 'icqq',
6
7
  adapterDisplayName: 'ICQQ',
7
8
  running: (use) => use(icqqRuntimeStateToken).endpoints.values(),
8
9
  describeEntry: () => 'direct(直连 @icqqjs/icqq)',
9
10
  addDescription: '登记 ICQQ endpoint(重启 zhin 生效)',
10
- bindFlow: ({ id }) => {
11
+ bindFlow: async ({ id, use }) => {
11
12
  if (!id) {
12
- return '用法:icqq.endpoint add <uin>(uin 为纯数字 QQ 号)';
13
+ return '用法:icqq endpoint add <uin>(uin 为纯数字 QQ 号)';
13
14
  }
14
15
  if (!/^\d+$/.test(id)) {
15
16
  return 'icqq endpoint 名必须是纯数字 QQ 号(uin)';
16
17
  }
17
18
  try {
18
- const filePath = addEndpointToConfig('icqq', { id });
19
+ const { filePath } = await use(endpointConfigurationStoreToken).add({
20
+ adapterKey: 'icqq',
21
+ entry: { id },
22
+ environment: {},
23
+ });
19
24
  return (`✅ endpoint「${id}」已追加到 ${filePath} 的 plugins.icqq.endpoints。\n` +
20
25
  `重启 zhin 后生效。`);
21
26
  }
@@ -1,6 +1,6 @@
1
- import { addEndpointToConfig, createEndpointCommands } from 'zhin.js/adapter';
1
+ import { createEndpointCommands, endpointConfigurationStoreToken } from 'zhin.js/adapter';
2
2
  import { defineCommand } from 'zhin.js/command';
3
- import { icqqRuntimeStateToken } from './icqq-runtime-state.js';
3
+ import { icqqRuntimeStateToken } from '../../../src/icqq-runtime-state.js';
4
4
 
5
5
  export const icqqEndpointCommands = createEndpointCommands({
6
6
  adapterKey: 'icqq',
@@ -8,15 +8,19 @@ export const icqqEndpointCommands = createEndpointCommands({
8
8
  running: (use) => use(icqqRuntimeStateToken).endpoints.values(),
9
9
  describeEntry: () => 'direct(直连 @icqqjs/icqq)',
10
10
  addDescription: '登记 ICQQ endpoint(重启 zhin 生效)',
11
- bindFlow: ({ id }) => {
11
+ bindFlow: async ({ id, use }) => {
12
12
  if (!id) {
13
- return '用法:icqq.endpoint add <uin>(uin 为纯数字 QQ 号)';
13
+ return '用法:icqq endpoint add <uin>(uin 为纯数字 QQ 号)';
14
14
  }
15
15
  if (!/^\d+$/.test(id)) {
16
16
  return 'icqq endpoint 名必须是纯数字 QQ 号(uin)';
17
17
  }
18
18
  try {
19
- const filePath = addEndpointToConfig('icqq', { id });
19
+ const { filePath } = await use(endpointConfigurationStoreToken).add({
20
+ adapterKey: 'icqq',
21
+ entry: { id },
22
+ environment: {},
23
+ });
20
24
  return (
21
25
  `✅ endpoint「${id}」已追加到 ${filePath} 的 plugins.icqq.endpoints。\n` +
22
26
  `重启 zhin 后生效。`
@@ -1,3 +1,3 @@
1
1
  // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
- import { icqqEndpointCommands } from "../../lib/icqq-endpoint-commands.js";
2
+ import { icqqEndpointCommands } from '../definition.js';
3
3
  export default icqqEndpointCommands.list;
@@ -0,0 +1,3 @@
1
+ import { icqqEndpointCommands } from '../definition.js';
2
+
3
+ export default icqqEndpointCommands.list;
@@ -1,3 +1,3 @@
1
1
  // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
- import { icqqEndpointCommands } from "../../../lib/icqq-endpoint-commands.js";
2
+ import { icqqEndpointCommands } from '../../definition.js';
3
3
  export default icqqEndpointCommands.remove;
@@ -0,0 +1,3 @@
1
+ import { icqqEndpointCommands } from '../../definition.js';
2
+
3
+ export default icqqEndpointCommands.remove;
@@ -0,0 +1,10 @@
1
+ import type { Client } from '@icqqjs/icqq';
2
+ import type { EndpointContentPort } from 'zhin.js/adapter';
3
+ import type { ConversationMessage } from '@zhin.js/im-contract';
4
+ /** Owns ICQQ-observed message content and recursive merged-forward expansion. */
5
+ export declare class IcqqContentResolver {
6
+ #private;
7
+ readonly port: EndpointContentPort;
8
+ constructor(client: Client);
9
+ remember(messageId: string, message: ConversationMessage): void;
10
+ }
@@ -0,0 +1,98 @@
1
+ import { normalizeForwardMsgResponse } from './forward-msg.js';
2
+ /** Owns ICQQ-observed message content and recursive merged-forward expansion. */
3
+ export class IcqqContentResolver {
4
+ #client;
5
+ #messages = new Map();
6
+ port = Object.freeze({
7
+ resolve: (reference, context) => this.#resolve(reference, context),
8
+ });
9
+ constructor(client) {
10
+ this.#client = client;
11
+ }
12
+ remember(messageId, message) {
13
+ this.#messages.set(messageId, message);
14
+ }
15
+ async #resolve(reference, context) {
16
+ context.signal.throwIfAborted();
17
+ if (reference.kind === 'message') {
18
+ const cached = this.#messages.get(reference.message.id);
19
+ return cached
20
+ ? Object.freeze({ status: 'resolved', reference, value: cached })
21
+ : Object.freeze({ status: 'not_found', code: 'icqq_message_not_observed' });
22
+ }
23
+ if (reference.kind === 'forward') {
24
+ try {
25
+ const entries = await this.#resolveForwardEntries(reference.forwardId, context, { remainingEntries: context.maxEntries, path: new Set() }, 0);
26
+ if (entries.length === 0) {
27
+ return Object.freeze({ status: 'not_found', code: 'icqq_forward_not_found' });
28
+ }
29
+ return Object.freeze({
30
+ status: 'resolved',
31
+ reference,
32
+ value: Object.freeze(entries),
33
+ });
34
+ }
35
+ catch (error) {
36
+ if (context.signal.aborted)
37
+ throw error;
38
+ return Object.freeze({
39
+ status: 'failed',
40
+ code: 'icqq_forward_fetch_failed',
41
+ message: error instanceof Error ? error.message : String(error),
42
+ });
43
+ }
44
+ }
45
+ if (reference.media.kind === 'url'
46
+ || reference.media.kind === 'base64'
47
+ || reference.media.kind === 'path') {
48
+ return Object.freeze({ status: 'resolved', reference, value: reference.media });
49
+ }
50
+ return Object.freeze({
51
+ status: 'unsupported',
52
+ code: 'icqq_media_reference_unsupported',
53
+ });
54
+ }
55
+ async #resolveForwardEntries(forwardId, context, state, depth) {
56
+ context.signal.throwIfAborted();
57
+ if (state.remainingEntries <= 0 || state.path.has(forwardId))
58
+ return Object.freeze([]);
59
+ state.path.add(forwardId);
60
+ try {
61
+ // ICQQ has no cancellable getForwardMsg API. Keep the generation lease
62
+ // until the native operation settles, then honor cancellation.
63
+ const raw = await this.#client.getForwardMsg(forwardId);
64
+ context.signal.throwIfAborted();
65
+ const entries = normalizeForwardMsgResponse(raw).slice(0, state.remainingEntries);
66
+ state.remainingEntries -= entries.length;
67
+ const expanded = [];
68
+ for (const entry of entries) {
69
+ context.signal.throwIfAborted();
70
+ const segments = [];
71
+ for (const segment of entry.segments) {
72
+ if (segment.type !== 'forward' || depth >= context.maxDepth) {
73
+ segments.push(segment);
74
+ continue;
75
+ }
76
+ const nestedId = String(segment.data.forward_id ?? '').trim();
77
+ if (!nestedId || state.path.has(nestedId) || state.remainingEntries <= 0) {
78
+ segments.push(segment);
79
+ continue;
80
+ }
81
+ const nested = await this.#resolveForwardEntries(nestedId, context, state, depth + 1);
82
+ segments.push(Object.freeze({
83
+ ...segment,
84
+ data: Object.freeze({
85
+ ...segment.data,
86
+ ...(nested.length > 0 ? { entries: nested } : {}),
87
+ }),
88
+ }));
89
+ }
90
+ expanded.push(Object.freeze({ ...entry, segments: Object.freeze(segments) }));
91
+ }
92
+ return Object.freeze(expanded);
93
+ }
94
+ finally {
95
+ state.path.delete(forwardId);
96
+ }
97
+ }
98
+ }
@@ -1,5 +1,5 @@
1
1
  /** ICQQ 历史文本视图 → canonical MessageSegment。出站只使用原生 Sendable。 */
2
- import { type MediaRef } from "@zhin.js/core";
2
+ import { type MediaRef } from '@zhin.js/im-contract';
3
3
  import type { MessageSegment } from "zhin.js";
4
4
  /**
5
5
  * CQ 媒体参数 / 入站元素媒体值 → canonical MediaRef:
package/lib/cq-message.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /** ICQQ 历史文本视图 → canonical MessageSegment。出站只使用原生 Sendable。 */
2
- import { isMediaRef } from "@zhin.js/core";
2
+ import { isMediaRef } from '@zhin.js/im-contract';
3
3
  const MAX_CQ_PARSE_LEN = 256_000;
4
4
  /**
5
5
  * CQ 媒体参数 / 入站元素媒体值 → canonical MediaRef:
package/lib/endpoint.d.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import { Client } from '@icqqjs/icqq';
2
- import { Endpoint } from 'zhin.js/adapter';
3
- import type { EndpointControl, EndpointManagement, EndpointSendRequest } from 'zhin.js/adapter';
2
+ import { Endpoint, type EndpointControl, type EndpointManagement, type EndpointSendRequest } from 'zhin.js/adapter';
4
3
  import type { EndpointContentPort } from '@zhin.js/adapter';
5
4
  import { type LoginAssist } from '@zhin.js/core';
6
5
  import type { CapabilityId } from 'zhin.js';
package/lib/endpoint.js CHANGED
@@ -1,16 +1,16 @@
1
1
  import { Client, parseGroupMessageId, } from '@icqqjs/icqq';
2
2
  import { inspect } from 'node:util';
3
3
  import { AsyncLocalStorage } from 'node:async_hooks';
4
- import { Endpoint } from 'zhin.js/adapter';
4
+ import { Endpoint, } from 'zhin.js/adapter';
5
5
  import { receiveOneBotLikeSideEvent, SystemEvent, toCanonicalSegments } from '@zhin.js/core';
6
6
  import { formatCompact, getAdapterLogger, truncatePreview } from '@zhin.js/logger';
7
7
  import { runIcqqLoginAssistStep } from './icqq-login-assist.js';
8
+ import { IcqqContentResolver } from './content-resolver.js';
8
9
  import { InboundMessageDeduper, findIcqqNestedMessageSource, isIcqqBotMentioned, isIcqqMessagePostType, normalizeIcqqInboundMessage, messageLookupFromIcqqSource, resolveIcqqQuoteIdFromEvent, resolveIcqqInboundMedia, shouldSkipSelfInboundMessage, } from './icqq-inbound.js';
9
10
  import { isIcqqNoticePayload, isIcqqRequestPayload, } from './icqq-inbox.js';
10
11
  import { IcqqGuildCatalog, normalizeIcqqGuildInboundMessage, } from './icqq-guild.js';
11
12
  import { prepareIcqqOutboundMedia, resolveIcqqOutboundMediaMode, } from './outbound-media.js';
12
13
  import { formatInboundContent, formatOutboundBody, icqqInboundConversation, icqqOutboundTarget, } from './protocol.js';
13
- import { normalizeForwardMsgResponse } from './forward-msg.js';
14
14
  const BOUND_EVENTS = [
15
15
  'message.private',
16
16
  'message.group',
@@ -56,80 +56,8 @@ export class IcqqEndpoint extends Endpoint {
56
56
  #heldInbound = [];
57
57
  #inflightInbound = new Set();
58
58
  #inboundOwner = new AsyncLocalStorage();
59
- #messageContent = new Map();
60
- content = Object.freeze({
61
- resolve: async (reference, context) => {
62
- context.signal.throwIfAborted();
63
- if (reference.kind === 'message') {
64
- const cached = this.#messageContent.get(reference.message.id);
65
- return cached
66
- ? Object.freeze({ status: 'resolved', reference, value: cached })
67
- : Object.freeze({ status: 'not_found', code: 'icqq_message_not_observed' });
68
- }
69
- if (reference.kind === 'forward') {
70
- try {
71
- const entries = await this.#resolveForwardEntries(reference.forwardId, context, { remainingEntries: context.maxEntries, path: new Set() }, 0);
72
- if (entries.length === 0)
73
- return Object.freeze({ status: 'not_found', code: 'icqq_forward_not_found' });
74
- return Object.freeze({ status: 'resolved', reference, value: Object.freeze(entries) });
75
- }
76
- catch (error) {
77
- if (context.signal.aborted)
78
- throw error;
79
- return Object.freeze({
80
- status: 'failed',
81
- code: 'icqq_forward_fetch_failed',
82
- message: error instanceof Error ? error.message : String(error),
83
- });
84
- }
85
- }
86
- if (reference.media.kind === 'url' || reference.media.kind === 'base64' || reference.media.kind === 'path') {
87
- return Object.freeze({ status: 'resolved', reference, value: reference.media });
88
- }
89
- return Object.freeze({ status: 'unsupported', code: 'icqq_media_reference_unsupported' });
90
- },
91
- });
92
- async #resolveForwardEntries(forwardId, context, state, depth) {
93
- context.signal.throwIfAborted();
94
- if (state.remainingEntries <= 0 || state.path.has(forwardId))
95
- return Object.freeze([]);
96
- state.path.add(forwardId);
97
- try {
98
- // ICQQ does not expose a cancellable getForwardMsg API. Keep the caller's
99
- // generation lease until the native operation actually settles, then honor
100
- // cancellation before publishing any resolved content.
101
- const raw = await this.client.getForwardMsg(forwardId);
102
- context.signal.throwIfAborted();
103
- const entries = normalizeForwardMsgResponse(raw).slice(0, state.remainingEntries);
104
- state.remainingEntries -= entries.length;
105
- const expanded = [];
106
- for (const entry of entries) {
107
- context.signal.throwIfAborted();
108
- const segments = [];
109
- for (const segment of entry.segments) {
110
- if (segment.type !== 'forward' || depth >= context.maxDepth) {
111
- segments.push(segment);
112
- continue;
113
- }
114
- const nestedId = String(segment.data.forward_id ?? '').trim();
115
- if (!nestedId || state.path.has(nestedId) || state.remainingEntries <= 0) {
116
- segments.push(segment);
117
- continue;
118
- }
119
- const nested = await this.#resolveForwardEntries(nestedId, context, state, depth + 1);
120
- segments.push(Object.freeze({
121
- ...segment,
122
- data: Object.freeze({ ...segment.data, ...(nested.length > 0 ? { entries: nested } : {}) }),
123
- }));
124
- }
125
- expanded.push(Object.freeze({ ...entry, segments: Object.freeze(segments) }));
126
- }
127
- return Object.freeze(expanded);
128
- }
129
- finally {
130
- state.path.delete(forwardId);
131
- }
132
- }
59
+ #contentResolver;
60
+ content;
133
61
  management = Object.freeze({
134
62
  listFriends: async () => Array.from(this.client.fl.values()).map((f) => ({
135
63
  user_id: f.user_id,
@@ -202,6 +130,8 @@ export class IcqqEndpoint extends Endpoint {
202
130
  super();
203
131
  const nativeConfig = resolveNativeClientConfig(options.config);
204
132
  this.client = new ManagedIcqqClient(Number(options.config.id), nativeConfig);
133
+ this.#contentResolver = new IcqqContentResolver(this.client);
134
+ this.content = this.#contentResolver.port;
205
135
  this.#logger = getAdapterLogger('icqq', options.config.id);
206
136
  this.#options = options;
207
137
  this.endpointName = options.config.id;
@@ -360,6 +290,27 @@ export class IcqqEndpoint extends Endpoint {
360
290
  }
361
291
  }
362
292
  async #sendNative(target, message) {
293
+ if (isIcqqShareElement(message)) {
294
+ switch (target.kind) {
295
+ case 'private':
296
+ await this.client.pickFriend(target.userId).share(message);
297
+ return {};
298
+ case 'group':
299
+ await this.client.pickGroup(target.groupId).share(message);
300
+ return {};
301
+ case 'temp':
302
+ await this.client.pickMember(target.groupId, target.userId).share(message);
303
+ return {};
304
+ case 'channel': {
305
+ const channel = this.client.pickGuild(target.guildId).channels.get(target.channelId);
306
+ if (!channel) {
307
+ throw new TypeError(`ICQQ guild channel is unavailable: ${target.guildId}/${target.channelId}`);
308
+ }
309
+ await channel.share(message);
310
+ return {};
311
+ }
312
+ }
313
+ }
363
314
  if (isIcqqFileElement(message)) {
364
315
  switch (target.kind) {
365
316
  case 'private': {
@@ -708,7 +659,7 @@ export class IcqqEndpoint extends Endpoint {
708
659
  const quoteId = resolveIcqqQuoteIdFromEvent(data);
709
660
  const quoted = messageLookupFromIcqqSource(findIcqqNestedMessageSource(data));
710
661
  if (quoteId && quoted) {
711
- this.#messageContent.set(quoteId, Object.freeze({
662
+ this.#contentResolver.remember(quoteId, Object.freeze({
712
663
  ref: Object.freeze({ conversation, id: quoteId }),
713
664
  ...(quoted.sender?.id ? { actor: Object.freeze({
714
665
  id: quoted.sender.id,
@@ -919,6 +870,12 @@ export class IcqqEndpoint extends Endpoint {
919
870
  }
920
871
  }
921
872
  }
873
+ function isIcqqShareElement(message) {
874
+ return !Array.isArray(message)
875
+ && typeof message === 'object'
876
+ && message !== null
877
+ && message.type === 'share';
878
+ }
922
879
  function formatIcqqSendablePreview(message) {
923
880
  if (typeof message === 'string')
924
881
  return message;
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * ICQQ 插件实例的运行时状态:adapter create() 注册的 endpoint 列表。
3
- * 由 plugin.ts setup() provide,adapter create 与 `icqq.endpoint` 命令共享(同一 owner generation)。
3
+ * 由 plugin.ts setup() provide,adapter create 与 `icqq endpoint` 命令共享(同一 owner generation)。
4
4
  */
5
5
  import { defineEndpointRuntimeStateToken } from 'zhin.js/adapter';
6
6
  export const icqqRuntimeStateToken = defineEndpointRuntimeStateToken('icqq');
package/lib/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { Actions, formatInboundContent, formatOutboundBody, icqqInboundConversation, icqqOutboundTarget, type ActionResult, type IcqqAdapterConfig, type IcqqInboundMessage, type IcqqWireSegment, type ParsedIcqqSendTarget, type ResolvedIcqqConfig, } from './protocol.js';
1
+ export { Actions, formatInboundContent, formatOutboundBody, icqqInboundConversation, icqqOutboundTarget, type ActionResult, type IcqqEndpointConfig, type IcqqInboundMessage, type IcqqWireSegment, type ParsedIcqqSendTarget, type ResolvedIcqqConfig, } from './protocol.js';
2
2
  export { IcqqEndpoint, type IcqqEndpointOptions, } from './endpoint.js';
3
3
  export * from './types.js';
4
4
  export { icqqClient, type IcqqClient, type IcqqClientEventMap, } from './client.js';
@@ -1,5 +1,5 @@
1
1
  import type { SendContent } from "zhin.js";
2
- import type { IcqqEndpointConfig } from "./types.js";
2
+ import type { IcqqEndpointConfig } from "./protocol.js";
3
3
  export type IcqqOutboundMediaMode = "file" | "base64";
4
4
  export declare function resolveIcqqOutboundMediaMode(config: Pick<IcqqEndpointConfig, "outboundMedia">): IcqqOutboundMediaMode;
5
5
  /**
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
4
  import * as crypto from "node:crypto";
5
- import { isMediaRef } from "@zhin.js/core";
5
+ import { isMediaRef } from '@zhin.js/im-contract';
6
6
  function extForMime(mime, fallback) {
7
7
  const map = {
8
8
  "image/jpeg": ".jpg",
package/lib/protocol.d.ts CHANGED
@@ -161,9 +161,9 @@ export declare const Actions: {
161
161
  readonly SET_NOTIFY: "set_notify";
162
162
  readonly GET_NOTIFY: "get_notify";
163
163
  };
164
- /** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
165
- export interface IcqqAdapterConfig {
166
- readonly id?: string;
164
+ /** One expanded ICQQ endpoint configuration produced by AdapterIndex. */
165
+ export interface IcqqEndpointConfig {
166
+ readonly id: string;
167
167
  readonly autoReconnect?: boolean;
168
168
  readonly outboundMedia?: 'file' | 'base64';
169
169
  readonly password?: string;
@@ -177,24 +177,6 @@ export interface IcqqAdapterConfig {
177
177
  readonly autoServer?: boolean;
178
178
  readonly qqnt?: boolean;
179
179
  readonly ntLogin?: boolean;
180
- /** Transitional: legacy root `endpoints[]` with `context: icqq`. */
181
- readonly endpoints?: ReadonlyArray<{
182
- readonly context?: string;
183
- readonly id?: string;
184
- readonly autoReconnect?: boolean;
185
- readonly outboundMedia?: 'file' | 'base64';
186
- readonly password?: string;
187
- readonly platform?: number;
188
- readonly ver?: string;
189
- readonly dataDir?: string;
190
- readonly signApiAddr?: string;
191
- readonly ignoreSelf?: boolean;
192
- readonly resend?: boolean;
193
- readonly cacheGroupMember?: boolean;
194
- readonly autoServer?: boolean;
195
- readonly qqnt?: boolean;
196
- readonly ntLogin?: boolean;
197
- }>;
198
180
  }
199
181
  export interface ResolvedIcqqConfig {
200
182
  readonly context: 'icqq';
@@ -251,7 +233,7 @@ export type ParsedIcqqSendTarget = {
251
233
  readonly guildId: string;
252
234
  readonly channelId: string;
253
235
  };
254
- export declare function resolveIcqqConfig(config?: IcqqAdapterConfig): ResolvedIcqqConfig;
236
+ export declare function resolveIcqqConfig(config: IcqqEndpointConfig): ResolvedIcqqConfig;
255
237
  /**
256
238
  * 入站归一化 → ConversationRef:群临时会话(temp)映射为群容器内的 private
257
239
  * 会话(parent kind 'group');频道消息的所属 guild 进 `parent`(kind 'channel')。