@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
package/lib/protocol.js CHANGED
@@ -1,4 +1,3 @@
1
- import { pickCredential } from 'zhin.js/adapter';
2
1
  import { resolveIcqqMediaFile } from "./cq-message.js";
3
2
  export { resolveIcqqInboundMessageId } from "./icqq-inbound.js";
4
3
  /**
@@ -172,25 +171,24 @@ export const Actions = {
172
171
  SET_NOTIFY: "set_notify",
173
172
  GET_NOTIFY: "get_notify",
174
173
  };
175
- export function resolveIcqqConfig(config = {}) {
176
- const entry = config.endpoints?.find((item) => item.context === 'icqq');
177
- const id = pickCredential(config.id, entry?.id, process.env.ICQQ_ACCOUNT);
174
+ export function resolveIcqqConfig(config) {
175
+ const id = typeof config.id === 'string' ? config.id.trim() : '';
178
176
  if (!/^\d+$/.test(id)) {
179
- throw new TypeError('ICQQ adapter requires numeric id (QQ uin) via plugins.<key>.id or ICQQ_ACCOUNT');
177
+ throw new TypeError('ICQQ endpoint requires a numeric id (QQ uin)');
180
178
  }
181
- const autoReconnect = config.autoReconnect ?? entry?.autoReconnect ?? true;
182
- const outboundMedia = config.outboundMedia ?? entry?.outboundMedia;
183
- const password = config.password ?? entry?.password;
184
- const platform = config.platform ?? entry?.platform ?? 2;
185
- const ver = config.ver ?? entry?.ver ?? '9.1.70';
186
- const dataDir = config.dataDir ?? entry?.dataDir ?? `data/icqq/${id}`;
187
- const signApiAddr = config.signApiAddr ?? entry?.signApiAddr;
188
- const ignoreSelf = config.ignoreSelf ?? entry?.ignoreSelf ?? true;
189
- const resend = config.resend ?? entry?.resend;
190
- const cacheGroupMember = config.cacheGroupMember ?? entry?.cacheGroupMember ?? true;
191
- const autoServer = config.autoServer ?? entry?.autoServer ?? true;
192
- const qqnt = config.qqnt ?? entry?.qqnt;
193
- const ntLogin = config.ntLogin ?? entry?.ntLogin;
179
+ const autoReconnect = config.autoReconnect ?? true;
180
+ const outboundMedia = config.outboundMedia;
181
+ const password = config.password;
182
+ const platform = config.platform ?? 2;
183
+ const ver = config.ver ?? '9.1.70';
184
+ const dataDir = config.dataDir ?? `data/icqq/${id}`;
185
+ const signApiAddr = config.signApiAddr;
186
+ const ignoreSelf = config.ignoreSelf ?? true;
187
+ const resend = config.resend;
188
+ const cacheGroupMember = config.cacheGroupMember ?? true;
189
+ const autoServer = config.autoServer ?? true;
190
+ const qqnt = config.qqnt;
191
+ const ntLogin = config.ntLogin;
194
192
  return {
195
193
  context: 'icqq',
196
194
  id,
@@ -262,6 +260,7 @@ const ICQQ_STANDALONE_ELEMENTS = new Set([
262
260
  'video',
263
261
  'file',
264
262
  'json',
263
+ 'share',
265
264
  'long_msg',
266
265
  ]);
267
266
  /**
@@ -356,7 +355,7 @@ export function formatOutboundBody(payload) {
356
355
  return { type: 'long_msg', resid };
357
356
  }
358
357
  case 'share':
359
- return formatMusicShareJson(item.data);
358
+ return formatShare(item.data);
360
359
  default: {
361
360
  const text = item.data?.text;
362
361
  if (typeof text === 'string' && text)
@@ -382,44 +381,42 @@ function isWireSegment(value) {
382
381
  && value !== null
383
382
  && typeof value.type === 'string';
384
383
  }
385
- function formatMusicShareJson(data) {
384
+ function formatShare(data) {
386
385
  if (!data)
387
386
  throw new TypeError('ICQQ share segment requires data');
388
387
  const config = data.config;
389
- const title = String(data.title ?? '');
390
- const desc = String(data.artist ?? '');
391
- const jumpUrl = String(data.url ?? '');
392
- const musicUrl = String(data.audio ?? '');
393
- const preview = String(data.image ?? '');
394
- const tag = config?.appid === 100497308 ? 'QQ音乐'
395
- : config?.appid === 100495085 ? '网易云音乐'
396
- : config?.appid === 100243533 ? '酷我音乐'
397
- : config?.appid === 205141 ? '酷狗音乐'
398
- : '音乐';
399
- const json = {
400
- app: 'com.tencent.structmsg',
401
- view: 'music',
402
- ver: '0.0.0.1',
403
- desc: 'music',
404
- prompt: `[分享]${title}`,
405
- meta: {
406
- music: {
407
- action: '',
408
- android_pkg_name: config?.package ?? '',
409
- app_type: 1,
410
- appid: config?.appid ?? 0,
411
- ctime: 0,
412
- desc,
413
- jumpUrl,
414
- musicUrl,
415
- preview,
416
- sourceMsgId: '0',
417
- source_icon: config?.icon ?? '',
418
- source_url: '',
419
- tag,
420
- title,
421
- },
422
- },
388
+ const title = String(data.title ?? '').trim();
389
+ const url = String(data.url ?? '').trim();
390
+ if (!title)
391
+ throw new TypeError('ICQQ share segment requires title');
392
+ if (!url)
393
+ throw new TypeError('ICQQ share segment requires url');
394
+ const optionalString = (value) => {
395
+ const text = String(value ?? '').trim();
396
+ return text || undefined;
397
+ };
398
+ const appid = Number(config?.appid);
399
+ const appname = optionalString(config?.package);
400
+ const appsign = optionalString(config?.sign);
401
+ const shareConfig = Number.isSafeInteger(appid) && appid > 0
402
+ ? {
403
+ appid,
404
+ ...(appname ? { appname } : {}),
405
+ ...(appsign ? { appsign } : {}),
406
+ }
407
+ : undefined;
408
+ const summary = optionalString(data.artist);
409
+ const content = optionalString(data.content);
410
+ const image = optionalString(data.image);
411
+ const audio = optionalString(data.audio);
412
+ return {
413
+ type: 'share',
414
+ title,
415
+ url,
416
+ ...(summary ? { summary } : {}),
417
+ ...(content ? { content } : {}),
418
+ ...(image ? { image } : {}),
419
+ ...(audio ? { audio } : {}),
420
+ ...(shareConfig ? { config: shareConfig } : {}),
423
421
  };
424
- return { type: 'json', data: json };
425
422
  }
package/lib/types.d.ts CHANGED
@@ -9,12 +9,6 @@ export interface IcqqSenderInfo {
9
9
  card?: string;
10
10
  title?: string;
11
11
  }
12
- export interface IcqqEndpointConfig {
13
- context: "icqq";
14
- name: `${number}`;
15
- autoReconnect?: boolean;
16
- outboundMedia?: "file" | "base64";
17
- }
18
12
  export interface FriendInfo {
19
13
  user_id: number;
20
14
  nickname: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-icqq",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "Zhin.js ICQQ adapter — direct connection to @icqqjs/icqq",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -34,31 +34,29 @@
34
34
  "directory": "plugins/adapters/icqq"
35
35
  },
36
36
  "dependencies": {
37
- "@zhin.js/adapter": "1.1.12",
38
- "@zhin.js/core": "1.1.35",
39
- "@zhin.js/feature-kit": "1.1.0",
40
- "@zhin.js/im-contract": "1.1.0",
41
- "@zhin.js/logger": "1.1.0",
42
- "@zhin.js/permission": "1.1.0",
43
- "@zhin.js/tool": "1.1.0"
37
+ "@zhin.js/adapter": "1.1.14",
38
+ "@zhin.js/agent-feature": "1.1.1",
39
+ "@zhin.js/core": "1.1.37",
40
+ "@zhin.js/feature-kit": "1.1.1",
41
+ "@zhin.js/im-contract": "1.1.1",
42
+ "@zhin.js/logger": "1.1.1",
43
+ "@zhin.js/permission": "1.1.1",
44
+ "@zhin.js/skill": "1.1.1",
45
+ "@zhin.js/tool": "1.1.1"
44
46
  },
45
47
  "peerDependencies": {
46
48
  "@icqqjs/icqq": "^1.12.2",
47
49
  "zod": "^4.0.0",
48
- "@zhin.js/adapter": "^1.1.12",
49
- "@zhin.js/agent": "^1.1.23",
50
- "@zhin.js/command": "^1.1.0",
51
- "@zhin.js/core": "^1.1.35",
52
- "@zhin.js/prompt-section": "^1.1.0",
53
- "zhin.js": "^1.1.0"
50
+ "@zhin.js/adapter": "^1.1.14",
51
+ "@zhin.js/command": "^1.1.1",
52
+ "@zhin.js/core": "^1.1.37",
53
+ "@zhin.js/prompt-section": "^1.1.1",
54
+ "zhin.js": "^1.1.2"
54
55
  },
55
56
  "peerDependenciesMeta": {
56
57
  "@icqqjs/icqq": {
57
58
  "optional": true
58
59
  },
59
- "@zhin.js/agent": {
60
- "optional": true
61
- },
62
60
  "@zhin.js/command": {
63
61
  "optional": true
64
62
  },
@@ -78,9 +76,8 @@
78
76
  "typescript": "^6.0.3",
79
77
  "vitest": "^4.1.10",
80
78
  "zod": "^4.4.3",
81
- "@zhin.js/agent": "1.1.23",
82
- "@zhin.js/prompt-section": "1.1.0",
83
- "zhin.js": "1.1.0"
79
+ "@zhin.js/prompt-section": "1.1.1",
80
+ "zhin.js": "1.1.2"
84
81
  },
85
82
  "files": [
86
83
  "adapters",
@@ -89,10 +86,11 @@
89
86
  "schema.json",
90
87
  "src",
91
88
  "lib",
92
- "agent",
93
- "tools",
89
+ "agents",
94
90
  "README.md",
95
- "CHANGELOG.md"
91
+ "CHANGELOG.md",
92
+ "PERMITS.md",
93
+ "prompt-sections"
96
94
  ],
97
95
  "publishConfig": {
98
96
  "access": "public",
@@ -119,6 +117,14 @@
119
117
  {
120
118
  "package": "@zhin.js/tool",
121
119
  "api": "^1.0.0"
120
+ },
121
+ {
122
+ "package": "@zhin.js/skill",
123
+ "api": "^1.0.0"
124
+ },
125
+ {
126
+ "package": "@zhin.js/agent-feature",
127
+ "api": "^1.0.0"
122
128
  }
123
129
  ],
124
130
  "plugins": []
package/plugin.js CHANGED
@@ -9,7 +9,7 @@ export default definePlugin({
9
9
  displayName: 'ICQQ Adapter',
10
10
  },
11
11
  setup(context) {
12
- // 运行中 endpoint 注册表(icqq.endpoint list 的"运行中"数据源)
12
+ // 运行中 endpoint 注册表(icqq endpoint list 的"运行中"数据源)
13
13
  context.resources.provide(icqqRuntimeStateToken, createEndpointRuntimeState());
14
14
  const disposePlatform = context.resources.has(permissionHostToken)
15
15
  ? context.resources.use(permissionHostToken).registerPlatform('icqq', createSceneRolePlatformChecker())
@@ -0,0 +1,16 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { defineAgentPromptSection } from '@zhin.js/prompt-section';
3
+ export default defineAgentPromptSection({
4
+ title: 'ICQQ / QQ',
5
+ content: [
6
+ 'ICQQ capabilities are grouped into directory, interaction, group-admin, and group-state Skills.',
7
+ 'Use discover for the user intent, then load only the matching Skill before calling its private Tool.',
8
+ 'The current IM operation selects the bot Client automatically; never guess user_id or group_id.',
9
+ ].join('\n'),
10
+ layer: 'tools',
11
+ order: 70,
12
+ retention: 'preferred',
13
+ maxChars: 1200,
14
+ profiles: ['interactive'],
15
+ platforms: ['icqq'],
16
+ });
@@ -3,10 +3,9 @@ import { defineAgentPromptSection } from '@zhin.js/prompt-section';
3
3
  export default defineAgentPromptSection({
4
4
  title: 'ICQQ / QQ',
5
5
  content: [
6
- 'On icqq/QQ: if icqq__send_user_like is in your tool list, call it with { user_id, times }.',
7
- 'The current IM operation selects the bot Client automatically. user_id is the sender to like. times is 1-20.',
8
- 'Do not claim the tool is missing until calling it this turn or load_tool("icqq__send_user_like") fails.',
9
- 'Other social tools include icqq__poke and icqq__friend_list.',
6
+ 'ICQQ capabilities are grouped into directory, interaction, group-admin, and group-state Skills.',
7
+ 'Use discover for the user intent, then load only the matching Skill before calling its private Tool.',
8
+ 'The current IM operation selects the bot Client automatically; never guess user_id or group_id.',
10
9
  ].join('\n'),
11
10
  layer: 'tools',
12
11
  order: 70,
package/schema.json CHANGED
@@ -76,13 +76,15 @@
76
76
  },
77
77
  "endpoints": {
78
78
  "type": "array",
79
+ "minItems": 1,
79
80
  "description": "多账号:一个插件实例挂多个 endpoint。每项与顶层字段同构(id 必填,其余覆盖顶层)",
80
81
  "items": {
81
82
  "type": "object",
82
- "additionalProperties": true,
83
+ "additionalProperties": false,
83
84
  "properties": {
84
85
  "id": {
85
86
  "type": "string",
87
+ "pattern": "^[0-9]+$",
86
88
  "description": "QQ uin"
87
89
  },
88
90
  "master": {
@@ -147,6 +149,10 @@
147
149
  "ntLogin": {
148
150
  "type": "boolean",
149
151
  "description": "是否使用 NT 登录方式"
152
+ },
153
+ "commandPrefix": {
154
+ "type": "string",
155
+ "description": "本 endpoint 的命令前缀"
150
156
  }
151
157
  },
152
158
  "required": ["id"]
@@ -0,0 +1,131 @@
1
+ import type { Client } from '@icqqjs/icqq';
2
+ import type {
3
+ EndpointContentPort,
4
+ EndpointContentResolveContext,
5
+ } from 'zhin.js/adapter';
6
+ import type {
7
+ ConversationMessage,
8
+ ConversationReference,
9
+ ForwardEntry,
10
+ Segment,
11
+ } from '@zhin.js/im-contract';
12
+ import { normalizeForwardMsgResponse } from './forward-msg.js';
13
+
14
+ /** Owns ICQQ-observed message content and recursive merged-forward expansion. */
15
+ export class IcqqContentResolver {
16
+ readonly #client: Client;
17
+ readonly #messages = new Map<string, ConversationMessage>();
18
+
19
+ readonly port: EndpointContentPort = Object.freeze({
20
+ resolve: (
21
+ reference: ConversationReference,
22
+ context: EndpointContentResolveContext,
23
+ ) => this.#resolve(reference, context),
24
+ });
25
+
26
+ constructor(client: Client) {
27
+ this.#client = client;
28
+ }
29
+
30
+ remember(messageId: string, message: ConversationMessage): void {
31
+ this.#messages.set(messageId, message);
32
+ }
33
+
34
+ async #resolve(
35
+ reference: ConversationReference,
36
+ context: EndpointContentResolveContext,
37
+ ) {
38
+ context.signal.throwIfAborted();
39
+ if (reference.kind === 'message') {
40
+ const cached = this.#messages.get(reference.message.id);
41
+ return cached
42
+ ? Object.freeze({ status: 'resolved' as const, reference, value: cached })
43
+ : Object.freeze({ status: 'not_found' as const, code: 'icqq_message_not_observed' });
44
+ }
45
+ if (reference.kind === 'forward') {
46
+ try {
47
+ const entries = await this.#resolveForwardEntries(
48
+ reference.forwardId,
49
+ context,
50
+ { remainingEntries: context.maxEntries, path: new Set<string>() },
51
+ 0,
52
+ );
53
+ if (entries.length === 0) {
54
+ return Object.freeze({ status: 'not_found' as const, code: 'icqq_forward_not_found' });
55
+ }
56
+ return Object.freeze({
57
+ status: 'resolved' as const,
58
+ reference,
59
+ value: Object.freeze(entries),
60
+ });
61
+ } catch (error) {
62
+ if (context.signal.aborted) throw error;
63
+ return Object.freeze({
64
+ status: 'failed' as const,
65
+ code: 'icqq_forward_fetch_failed',
66
+ message: error instanceof Error ? error.message : String(error),
67
+ });
68
+ }
69
+ }
70
+ if (
71
+ reference.media.kind === 'url'
72
+ || reference.media.kind === 'base64'
73
+ || reference.media.kind === 'path'
74
+ ) {
75
+ return Object.freeze({ status: 'resolved' as const, reference, value: reference.media });
76
+ }
77
+ return Object.freeze({
78
+ status: 'unsupported' as const,
79
+ code: 'icqq_media_reference_unsupported',
80
+ });
81
+ }
82
+
83
+ async #resolveForwardEntries(
84
+ forwardId: string,
85
+ context: EndpointContentResolveContext,
86
+ state: { remainingEntries: number; readonly path: Set<string> },
87
+ depth: number,
88
+ ): Promise<readonly ForwardEntry[]> {
89
+ context.signal.throwIfAborted();
90
+ if (state.remainingEntries <= 0 || state.path.has(forwardId)) return Object.freeze([]);
91
+ state.path.add(forwardId);
92
+ try {
93
+ // ICQQ has no cancellable getForwardMsg API. Keep the generation lease
94
+ // until the native operation settles, then honor cancellation.
95
+ const raw = await this.#client.getForwardMsg(forwardId);
96
+ context.signal.throwIfAborted();
97
+ const entries = normalizeForwardMsgResponse(raw).slice(0, state.remainingEntries);
98
+ state.remainingEntries -= entries.length;
99
+ const expanded: ForwardEntry[] = [];
100
+ for (const entry of entries) {
101
+ context.signal.throwIfAborted();
102
+ const segments: Segment[] = [];
103
+ for (const segment of entry.segments) {
104
+ if (segment.type !== 'forward' || depth >= context.maxDepth) {
105
+ segments.push(segment);
106
+ continue;
107
+ }
108
+ const nestedId = String(
109
+ (segment.data as { forward_id?: unknown }).forward_id ?? '',
110
+ ).trim();
111
+ if (!nestedId || state.path.has(nestedId) || state.remainingEntries <= 0) {
112
+ segments.push(segment);
113
+ continue;
114
+ }
115
+ const nested = await this.#resolveForwardEntries(nestedId, context, state, depth + 1);
116
+ segments.push(Object.freeze({
117
+ ...segment,
118
+ data: Object.freeze({
119
+ ...segment.data,
120
+ ...(nested.length > 0 ? { entries: nested } : {}),
121
+ }),
122
+ }));
123
+ }
124
+ expanded.push(Object.freeze({ ...entry, segments: Object.freeze(segments) }));
125
+ }
126
+ return Object.freeze(expanded);
127
+ } finally {
128
+ state.path.delete(forwardId);
129
+ }
130
+ }
131
+ }
package/src/cq-message.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /** ICQQ 历史文本视图 → canonical MessageSegment。出站只使用原生 Sendable。 */
2
- import { isMediaRef, type MediaRef } from "@zhin.js/core";
2
+ import { isMediaRef, type MediaRef } from '@zhin.js/im-contract';
3
3
  import type { MessageSegment } from "zhin.js";
4
4
 
5
5
  const MAX_CQ_PARSE_LEN = 256_000;
package/src/endpoint.ts CHANGED
@@ -5,29 +5,25 @@ import {
5
5
  type FileElem,
6
6
  type PttElem,
7
7
  type Sendable,
8
+ type ShareElem,
8
9
  type VideoElem,
9
10
  } from '@icqqjs/icqq';
10
11
  import { inspect } from 'node:util';
11
12
  import { AsyncLocalStorage } from 'node:async_hooks';
12
- import { Endpoint } from 'zhin.js/adapter';
13
- import type {
14
- EndpointControl,
15
- EndpointManagement,
16
- EndpointPendingRequest,
17
- EndpointSendRequest,
13
+ import {
14
+ Endpoint,
15
+ type EndpointControl,
16
+ type EndpointManagement,
17
+ type EndpointPendingRequest,
18
+ type EndpointSendRequest,
18
19
  } from 'zhin.js/adapter';
19
- import type { EndpointContentPort, EndpointContentResolveContext } from '@zhin.js/adapter';
20
+ import type { EndpointContentPort } from '@zhin.js/adapter';
20
21
  import { receiveOneBotLikeSideEvent, SystemEvent, toCanonicalSegments, type LoginAssist } from '@zhin.js/core';
21
- import type {
22
- ConversationMessage,
23
- ConversationReference,
24
- ForwardEntry,
25
- MessageRef,
26
- Segment,
27
- } from '@zhin.js/im-contract';
22
+ import type { MessageRef } from '@zhin.js/im-contract';
28
23
  import { formatCompact, getAdapterLogger, truncatePreview } from '@zhin.js/logger';
29
24
  import type { CapabilityId } from 'zhin.js';
30
25
  import { runIcqqLoginAssistStep } from './icqq-login-assist.js';
26
+ import { IcqqContentResolver } from './content-resolver.js';
31
27
  import {
32
28
  InboundMessageDeduper,
33
29
  findIcqqNestedMessageSource,
@@ -61,7 +57,6 @@ import {
61
57
  type ResolvedIcqqConfig,
62
58
  } from './protocol.js';
63
59
  import type { SystemMessage as IcqqSystemMessage } from './types.js';
64
- import { normalizeForwardMsgResponse } from './forward-msg.js';
65
60
 
66
61
  export interface IcqqEndpointOptions {
67
62
  readonly id: CapabilityId;
@@ -115,87 +110,8 @@ export class IcqqEndpoint extends Endpoint<Client> {
115
110
  #heldInbound: IcqqInboundMessage[] = [];
116
111
  readonly #inflightInbound = new Set<Promise<void>>();
117
112
  readonly #inboundOwner = new AsyncLocalStorage<symbol>();
118
- readonly #messageContent = new Map<string, ConversationMessage>();
119
-
120
- readonly content: EndpointContentPort = Object.freeze({
121
- resolve: async (reference: ConversationReference, context: EndpointContentResolveContext) => {
122
- context.signal.throwIfAborted();
123
- if (reference.kind === 'message') {
124
- const cached = this.#messageContent.get(reference.message.id);
125
- return cached
126
- ? Object.freeze({ status: 'resolved' as const, reference, value: cached })
127
- : Object.freeze({ status: 'not_found' as const, code: 'icqq_message_not_observed' });
128
- }
129
- if (reference.kind === 'forward') {
130
- try {
131
- const entries = await this.#resolveForwardEntries(
132
- reference.forwardId,
133
- context,
134
- { remainingEntries: context.maxEntries, path: new Set<string>() },
135
- 0,
136
- );
137
- if (entries.length === 0) return Object.freeze({ status: 'not_found' as const, code: 'icqq_forward_not_found' });
138
- return Object.freeze({ status: 'resolved' as const, reference, value: Object.freeze(entries) });
139
- } catch (error) {
140
- if (context.signal.aborted) throw error;
141
- return Object.freeze({
142
- status: 'failed' as const,
143
- code: 'icqq_forward_fetch_failed',
144
- message: error instanceof Error ? error.message : String(error),
145
- });
146
- }
147
- }
148
- if (reference.media.kind === 'url' || reference.media.kind === 'base64' || reference.media.kind === 'path') {
149
- return Object.freeze({ status: 'resolved' as const, reference, value: reference.media });
150
- }
151
- return Object.freeze({ status: 'unsupported' as const, code: 'icqq_media_reference_unsupported' });
152
- },
153
- });
154
-
155
- async #resolveForwardEntries(
156
- forwardId: string,
157
- context: EndpointContentResolveContext,
158
- state: { remainingEntries: number; readonly path: Set<string> },
159
- depth: number,
160
- ): Promise<readonly ForwardEntry[]> {
161
- context.signal.throwIfAborted();
162
- if (state.remainingEntries <= 0 || state.path.has(forwardId)) return Object.freeze([]);
163
- state.path.add(forwardId);
164
- try {
165
- // ICQQ does not expose a cancellable getForwardMsg API. Keep the caller's
166
- // generation lease until the native operation actually settles, then honor
167
- // cancellation before publishing any resolved content.
168
- const raw = await this.client.getForwardMsg(forwardId);
169
- context.signal.throwIfAborted();
170
- const entries = normalizeForwardMsgResponse(raw).slice(0, state.remainingEntries);
171
- state.remainingEntries -= entries.length;
172
- const expanded: ForwardEntry[] = [];
173
- for (const entry of entries) {
174
- context.signal.throwIfAborted();
175
- const segments: Segment[] = [];
176
- for (const segment of entry.segments) {
177
- if (segment.type !== 'forward' || depth >= context.maxDepth) {
178
- segments.push(segment);
179
- continue;
180
- }
181
- const nestedId = String((segment.data as { forward_id?: unknown }).forward_id ?? '').trim();
182
- if (!nestedId || state.path.has(nestedId) || state.remainingEntries <= 0) {
183
- segments.push(segment);
184
- continue;
185
- }
186
- const nested = await this.#resolveForwardEntries(nestedId, context, state, depth + 1);
187
- segments.push(Object.freeze({
188
- ...segment,
189
- data: Object.freeze({ ...segment.data, ...(nested.length > 0 ? { entries: nested } : {}) }),
190
- }));
191
- }
192
- expanded.push(Object.freeze({ ...entry, segments: Object.freeze(segments) }));
193
- }
194
- return Object.freeze(expanded);
195
- } finally {
196
- state.path.delete(forwardId);
197
- }
198
- }
113
+ readonly #contentResolver: IcqqContentResolver;
114
+ readonly content: EndpointContentPort;
199
115
 
200
116
  readonly management: EndpointManagement = Object.freeze<EndpointManagement>({
201
117
  listFriends: async () =>
@@ -275,6 +191,8 @@ export class IcqqEndpoint extends Endpoint<Client> {
275
191
  super();
276
192
  const nativeConfig = resolveNativeClientConfig(options.config);
277
193
  this.client = new ManagedIcqqClient(Number(options.config.id), nativeConfig);
194
+ this.#contentResolver = new IcqqContentResolver(this.client);
195
+ this.content = this.#contentResolver.port;
278
196
  this.#logger = getAdapterLogger('icqq', options.config.id);
279
197
  this.#options = options;
280
198
  this.endpointName = options.config.id;
@@ -436,6 +354,27 @@ export class IcqqEndpoint extends Endpoint<Client> {
436
354
  target: ReturnType<typeof icqqOutboundTarget>,
437
355
  message: Sendable,
438
356
  ): Promise<{ message_id?: unknown }> {
357
+ if (isIcqqShareElement(message)) {
358
+ switch (target.kind) {
359
+ case 'private':
360
+ await this.client.pickFriend(target.userId).share(message);
361
+ return {};
362
+ case 'group':
363
+ await this.client.pickGroup(target.groupId).share(message);
364
+ return {};
365
+ case 'temp':
366
+ await this.client.pickMember(target.groupId, target.userId).share(message);
367
+ return {};
368
+ case 'channel': {
369
+ const channel = this.client.pickGuild(target.guildId).channels.get(target.channelId);
370
+ if (!channel) {
371
+ throw new TypeError(`ICQQ guild channel is unavailable: ${target.guildId}/${target.channelId}`);
372
+ }
373
+ await channel.share(message);
374
+ return {};
375
+ }
376
+ }
377
+ }
439
378
  if (isIcqqFileElement(message)) {
440
379
  switch (target.kind) {
441
380
  case 'private': {
@@ -800,7 +739,7 @@ export class IcqqEndpoint extends Endpoint<Client> {
800
739
  const quoteId = resolveIcqqQuoteIdFromEvent(data);
801
740
  const quoted = messageLookupFromIcqqSource(findIcqqNestedMessageSource(data));
802
741
  if (quoteId && quoted) {
803
- this.#messageContent.set(quoteId, Object.freeze({
742
+ this.#contentResolver.remember(quoteId, Object.freeze({
804
743
  ref: Object.freeze({ conversation, id: quoteId }),
805
744
  ...(quoted.sender?.id ? { actor: Object.freeze({
806
745
  id: quoted.sender.id,
@@ -1014,6 +953,13 @@ export class IcqqEndpoint extends Endpoint<Client> {
1014
953
  }
1015
954
  }
1016
955
 
956
+ function isIcqqShareElement(message: Sendable): message is ShareElem {
957
+ return !Array.isArray(message)
958
+ && typeof message === 'object'
959
+ && message !== null
960
+ && message.type === 'share';
961
+ }
962
+
1017
963
  function formatIcqqSendablePreview(message: Sendable): string {
1018
964
  if (typeof message === 'string') return message;
1019
965
  try {
@@ -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