@zhin.js/adapter-weixin-ilink 5.0.11 → 5.0.13

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,31 @@
1
1
  # @zhin.js/adapter-weixin-ilink
2
2
 
3
+ ## 5.0.13
4
+
5
+ ### Patch Changes
6
+
7
+ - 5969c5b: Add SideEventGateway so adapters forward notice/request/system into HandlerIndex. HandlerContext now exposes only generation-safe capabilities and prompt ports; live Endpoint escape hatches are removed.
8
+ - 3b3ec03: Emit canonical reply and media segments instead of eager quote text and media placeholders.
9
+ - Updated dependencies [5969c5b]
10
+ - Updated dependencies [5969c5b]
11
+ - Updated dependencies [5969c5b]
12
+ - Updated dependencies [974772e]
13
+ - Updated dependencies [5969c5b]
14
+ - Updated dependencies [2f786bd]
15
+ - Updated dependencies [1312ca0]
16
+ - @zhin.js/im-contract@1.0.4
17
+ - @zhin.js/core@1.5.12
18
+ - @zhin.js/adapter@1.1.11
19
+ - @zhin.js/command@1.0.15
20
+ - zhin.js@6.0.12
21
+
22
+ ## 5.0.12
23
+
24
+ ### Patch Changes
25
+
26
+ - @zhin.js/core@1.5.11
27
+ - zhin.js@6.0.11
28
+
3
29
  ## 5.0.11
4
30
 
5
31
  ### Patch Changes
@@ -3,7 +3,7 @@
3
3
  * Convention entry: discover `adapters/weixin-ilink.ts` → defineAdapter.
4
4
  */
5
5
  import { defineAdapter } from 'zhin.js/adapter';
6
- import { messageGatewayToken } from '@zhin.js/core/runtime';
6
+ import { messageGatewayToken, sideEventGatewayToken } from '@zhin.js/core/runtime';
7
7
  import { WeixinIlinkEndpoint } from "../lib/endpoint.js";
8
8
  import { resolveWeixinIlinkConfig, } from "../lib/protocol.js";
9
9
  import { weixinIlinkRuntimeStateToken } from "../lib/weixin-ilink-runtime-state.js";
@@ -27,6 +27,7 @@ export default defineAdapter({
27
27
  return new WeixinIlinkEndpoint({
28
28
  id: context.id,
29
29
  gateway: context.use(messageGatewayToken),
30
+ sideEvents: context.use(sideEventGatewayToken),
30
31
  config,
31
32
  });
32
33
  },
@@ -2,7 +2,7 @@
2
2
  * Convention entry: discover `adapters/weixin-ilink.ts` → defineAdapter.
3
3
  */
4
4
  import { defineAdapter } from 'zhin.js/adapter';
5
- import { messageGatewayToken } from '@zhin.js/core/runtime';
5
+ import { messageGatewayToken, sideEventGatewayToken } from '@zhin.js/core/runtime';
6
6
  import { WeixinIlinkEndpoint } from '../src/endpoint.js';
7
7
  import {
8
8
  resolveWeixinIlinkConfig,
@@ -38,6 +38,7 @@ export default defineAdapter<WeixinIlinkAdapterConfig>({
38
38
  return new WeixinIlinkEndpoint({
39
39
  id: context.id,
40
40
  gateway: context.use(messageGatewayToken),
41
+ sideEvents: context.use(sideEventGatewayToken),
41
42
  config,
42
43
  });
43
44
  },
package/lib/endpoint.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { EndpointInstance, EndpointManagement, EndpointSendRequest } from 'zhin.js/adapter';
2
- import type { MessageGateway } from '@zhin.js/core/runtime';
2
+ import type { MessageGateway, SideEventGateway } from '@zhin.js/core/runtime';
3
3
  import type { CapabilityId } from 'zhin.js';
4
4
  import { getUpdates, notifyStart, notifyStop } from './ilink-api.js';
5
5
  import { type WeixinIlinkCredentials } from './credentials.js';
@@ -12,6 +12,7 @@ export type WeixinIlinkGetUpdates = typeof getUpdates;
12
12
  export interface WeixinIlinkEndpointOptions {
13
13
  readonly id: CapabilityId;
14
14
  readonly gateway: MessageGateway;
15
+ readonly sideEvents?: SideEventGateway;
15
16
  readonly config: ResolvedWeixinIlinkConfig;
16
17
  readonly resolveCredentials?: (config: ResolvedWeixinIlinkConfig, signal?: AbortSignal) => Promise<WeixinIlinkCredentials>;
17
18
  /** Test / internal: override network side effects. */
package/lib/endpoint.js CHANGED
@@ -18,7 +18,7 @@ import { sendWeixinMediaFile } from './weixin-send-media.js';
18
18
  import { materializeOutboundMedia } from './outbound-media.js';
19
19
  import { MessageItemType } from './ilink-types.js';
20
20
  import { getExtensionFromMime, sniffMimeFromBuffer } from './mime.js';
21
- import { formatInboundContent, formatOutboundSegments, inboundMessageId, segmentLocalPath, sleep, weixinIlinkInboundConversation, } from './protocol.js';
21
+ import { formatInboundContent, weixinInboundSegments, weixinReplyTo, formatOutboundSegments, inboundMessageId, segmentLocalPath, sleep, weixinIlinkInboundConversation, } from './protocol.js';
22
22
  const MAX_CONSECUTIVE_FAILURES = 3;
23
23
  const BACKOFF_DELAY_MS = 30_000;
24
24
  const RETRY_DELAY_MS = 2_000;
@@ -218,10 +218,13 @@ export class WeixinIlinkEndpoint {
218
218
  setContextToken(this.#options.config.id, userId, msg.context_token);
219
219
  }
220
220
  const conversation = weixinIlinkInboundConversation(String(this.#options.id), userId);
221
+ const replyTo = weixinReplyTo(msg);
221
222
  void this.#options.gateway.receive({
222
223
  conversation,
223
224
  message: { conversation, id: inboundMessageId(msg) },
224
225
  content: formatInboundContent(msg),
226
+ segments: weixinInboundSegments(msg),
227
+ ...(replyTo ? { replyTo } : {}),
225
228
  sender: { id: userId },
226
229
  endpointId: this.#options.config.id,
227
230
  metadata: Object.freeze({
package/lib/protocol.d.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * Weixin iLink protocol helpers (no legacy Adapter/Endpoint / segment-mapper).
3
3
  * Canonicalization is owned by gateway/core before endpoint.send.
4
4
  */
5
- import type { ConversationRef } from '@zhin.js/im-contract';
5
+ import type { ConversationRef, Segment } from '@zhin.js/im-contract';
6
6
  import type { WeixinMessage } from './ilink-types.js';
7
7
  /** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
8
8
  export interface WeixinIlinkAdapterConfig {
@@ -43,6 +43,10 @@ export type WeixinMessageWithMedia = WeixinMessage & {
43
43
  export declare function resolveWeixinIlinkConfig(config?: WeixinIlinkAdapterConfig): ResolvedWeixinIlinkConfig;
44
44
  /** Build inbound text for MessageGateway.receive (gateway owns reply routing). */
45
45
  export declare function formatInboundContent(msg: WeixinMessageWithMedia): string;
46
+ export declare function weixinInboundSegments(msg: WeixinMessageWithMedia): readonly Segment[];
47
+ export declare function weixinReplyTo(msg: WeixinMessageWithMedia): Readonly<{
48
+ id: string;
49
+ }> | undefined;
46
50
  export declare function inboundMessageId(msg: WeixinMessage): string;
47
51
  /**
48
52
  * 入站归一化 → ConversationRef:个人微信无群/频道概念,全部会话都是
package/lib/protocol.js CHANGED
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { pickCredential } from 'zhin.js/adapter';
6
6
  import { isMediaRef } from '@zhin.js/core';
7
- import { bodyFromItemList, isMediaItem } from './weixin-inbound.js';
7
+ import { bodyFromItemList } from './weixin-inbound.js';
8
8
  import { DEFAULT_API_BASE_URL, DEFAULT_CDN_BASE_URL } from './ilink-meta.js';
9
9
  const DEFAULT_LONG_POLL_TIMEOUT_MS = 35_000;
10
10
  export function resolveWeixinIlinkConfig(config = {}) {
@@ -30,27 +30,35 @@ export function resolveWeixinIlinkConfig(config = {}) {
30
30
  }
31
31
  /** Build inbound text for MessageGateway.receive (gateway owns reply routing). */
32
32
  export function formatInboundContent(msg) {
33
- const parts = [];
34
- const text = bodyFromItemList(msg.item_list);
33
+ return bodyFromItemList(msg.item_list).trim();
34
+ }
35
+ export function weixinInboundSegments(msg) {
36
+ const segments = [];
37
+ const text = formatInboundContent(msg);
35
38
  if (text)
36
- parts.push(text);
39
+ segments.push({ type: 'text', data: { text } });
37
40
  const media = msg._media;
38
41
  if (media?.decryptedPicPath) {
39
- parts.push(`[image: ${media.decryptedPicPath}]`);
42
+ segments.push({ type: 'image', data: { media: { kind: 'path', value: media.decryptedPicPath } } });
40
43
  }
41
44
  else if (media?.decryptedVideoPath) {
42
- parts.push(`[video: ${media.decryptedVideoPath}]`);
45
+ segments.push({ type: 'video', data: { media: { kind: 'path', value: media.decryptedVideoPath } } });
43
46
  }
44
47
  else if (media?.decryptedFilePath) {
45
- parts.push(`[file: ${media.decryptedFilePath}]`);
48
+ segments.push({ type: 'file', data: { media: { kind: 'path', value: media.decryptedFilePath } } });
46
49
  }
47
50
  else if (media?.decryptedVoicePath) {
48
- parts.push(`[record: ${media.decryptedVoicePath}]`);
51
+ segments.push({ type: 'audio', data: { media: { kind: 'path', value: media.decryptedVoicePath } } });
49
52
  }
50
- else if (!text && msg.item_list?.some((item) => isMediaItem(item))) {
51
- parts.push('[媒体消息]');
53
+ return Object.freeze(segments);
54
+ }
55
+ export function weixinReplyTo(msg) {
56
+ for (const item of msg.item_list ?? []) {
57
+ const id = item.ref_msg?.message_item?.msg_id;
58
+ if (id && String(id).trim())
59
+ return Object.freeze({ id: String(id).trim() });
52
60
  }
53
- return parts.join('\n').trim() || '';
61
+ return undefined;
54
62
  }
55
63
  export function inboundMessageId(msg) {
56
64
  return String(msg.message_id ?? msg.client_id ?? msg.seq ?? Date.now());
@@ -168,25 +168,7 @@ export function bodyFromItemList(itemList) {
168
168
  return "";
169
169
  for (const item of itemList) {
170
170
  if (item.type === MessageItemType.TEXT && item.text_item?.text != null) {
171
- const text = String(item.text_item.text);
172
- const ref = item.ref_msg;
173
- if (!ref)
174
- return text;
175
- // Quoted media is passed as MediaPath; only include the current text as body.
176
- if (ref.message_item && isMediaItem(ref.message_item))
177
- return text;
178
- // Build quoted context from both title and message_item content.
179
- const parts = [];
180
- if (ref.title)
181
- parts.push(ref.title);
182
- if (ref.message_item) {
183
- const refBody = bodyFromItemList([ref.message_item]);
184
- if (refBody)
185
- parts.push(refBody);
186
- }
187
- if (!parts.length)
188
- return text;
189
- return `[引用: ${parts.join(" | ")}]\n${text}`;
171
+ return String(item.text_item.text);
190
172
  }
191
173
  // 语音转文字:如果语音消息有 text 字段,直接使用文字内容
192
174
  if (item.type === MessageItemType.VOICE && item.voice_item?.text) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-weixin-ilink",
3
- "version": "5.0.11",
3
+ "version": "5.0.13",
4
4
  "type": "module",
5
5
  "description": "Zhin.js Weixin iLink (ClawBot) adapter for Plugin Runtime (long-poll)",
6
6
  "main": "./lib/index.js",
@@ -48,16 +48,16 @@
48
48
  }
49
49
  },
50
50
  "dependencies": {
51
- "@zhin.js/adapter": "1.1.10",
52
- "@zhin.js/core": "1.5.10",
53
- "@zhin.js/im-contract": "1.0.3",
51
+ "@zhin.js/adapter": "1.1.11",
52
+ "@zhin.js/core": "1.5.12",
53
+ "@zhin.js/im-contract": "1.0.4",
54
54
  "@zhin.js/logger": "1.0.76"
55
55
  },
56
56
  "peerDependencies": {
57
- "@zhin.js/adapter": "1.1.10",
58
- "@zhin.js/command": "1.0.14",
59
- "@zhin.js/core": "1.5.10",
60
- "zhin.js": "6.0.10"
57
+ "@zhin.js/adapter": "1.1.11",
58
+ "@zhin.js/command": "1.0.15",
59
+ "@zhin.js/core": "1.5.12",
60
+ "zhin.js": "6.0.12"
61
61
  },
62
62
  "peerDependenciesMeta": {
63
63
  "@zhin.js/command": {
@@ -71,7 +71,7 @@
71
71
  "@types/node": "^26.1.2",
72
72
  "typescript": "^6.0.3",
73
73
  "vitest": "^4.1.10",
74
- "zhin.js": "6.0.10"
74
+ "zhin.js": "6.0.12"
75
75
  },
76
76
  "ilink_appid": "bot",
77
77
  "engines": {
package/src/endpoint.ts CHANGED
@@ -10,7 +10,7 @@ import type {
10
10
  EndpointManagement,
11
11
  EndpointSendRequest,
12
12
  } from 'zhin.js/adapter';
13
- import type { MessageGateway } from '@zhin.js/core/runtime';
13
+ import type { MessageGateway, SideEventGateway } from '@zhin.js/core/runtime';
14
14
  import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
15
15
  import type { CapabilityId } from 'zhin.js';
16
16
  import { getUpdates, notifyStart, notifyStop, sendTyping } from './ilink-api.js';
@@ -45,6 +45,8 @@ import { getExtensionFromMime, sniffMimeFromBuffer } from './mime.js';
45
45
  import type { WeixinInboundMediaOpts } from './weixin-inbound.js';
46
46
  import {
47
47
  formatInboundContent,
48
+ weixinInboundSegments,
49
+ weixinReplyTo,
48
50
  formatOutboundSegments,
49
51
  inboundMessageId,
50
52
  segmentLocalPath,
@@ -71,6 +73,7 @@ export type WeixinIlinkGetUpdates = typeof getUpdates;
71
73
  export interface WeixinIlinkEndpointOptions {
72
74
  readonly id: CapabilityId;
73
75
  readonly gateway: MessageGateway;
76
+ readonly sideEvents?: SideEventGateway;
74
77
  readonly config: ResolvedWeixinIlinkConfig;
75
78
  readonly resolveCredentials?: (
76
79
  config: ResolvedWeixinIlinkConfig,
@@ -297,10 +300,13 @@ export class WeixinIlinkEndpoint implements EndpointInstance {
297
300
  setContextToken(this.#options.config.id, userId, msg.context_token);
298
301
  }
299
302
  const conversation = weixinIlinkInboundConversation(String(this.#options.id), userId);
303
+ const replyTo = weixinReplyTo(msg);
300
304
  void this.#options.gateway.receive({
301
305
  conversation,
302
306
  message: { conversation, id: inboundMessageId(msg) },
303
307
  content: formatInboundContent(msg),
308
+ segments: weixinInboundSegments(msg),
309
+ ...(replyTo ? { replyTo } : {}),
304
310
  sender: { id: userId },
305
311
  endpointId: this.#options.config.id,
306
312
  metadata: Object.freeze({
package/src/protocol.ts CHANGED
@@ -5,8 +5,8 @@
5
5
 
6
6
  import { pickCredential } from 'zhin.js/adapter';
7
7
  import { isMediaRef } from '@zhin.js/core';
8
- import type { ConversationRef } from '@zhin.js/im-contract';
9
- import { bodyFromItemList, isMediaItem } from './weixin-inbound.js';
8
+ import type { ConversationRef, Segment } from '@zhin.js/im-contract';
9
+ import { bodyFromItemList } from './weixin-inbound.js';
10
10
  import { DEFAULT_API_BASE_URL, DEFAULT_CDN_BASE_URL } from './ilink-meta.js';
11
11
  import type { WeixinMessage } from './ilink-types.js';
12
12
 
@@ -84,24 +84,32 @@ export function resolveWeixinIlinkConfig(
84
84
 
85
85
  /** Build inbound text for MessageGateway.receive (gateway owns reply routing). */
86
86
  export function formatInboundContent(msg: WeixinMessageWithMedia): string {
87
- const parts: string[] = [];
88
- const text = bodyFromItemList(msg.item_list);
89
- if (text) parts.push(text);
87
+ return bodyFromItemList(msg.item_list).trim();
88
+ }
90
89
 
90
+ export function weixinInboundSegments(msg: WeixinMessageWithMedia): readonly Segment[] {
91
+ const segments: Segment[] = [];
92
+ const text = formatInboundContent(msg);
93
+ if (text) segments.push({ type: 'text', data: { text } });
91
94
  const media = msg._media;
92
95
  if (media?.decryptedPicPath) {
93
- parts.push(`[image: ${media.decryptedPicPath}]`);
96
+ segments.push({ type: 'image', data: { media: { kind: 'path', value: media.decryptedPicPath } } });
94
97
  } else if (media?.decryptedVideoPath) {
95
- parts.push(`[video: ${media.decryptedVideoPath}]`);
98
+ segments.push({ type: 'video', data: { media: { kind: 'path', value: media.decryptedVideoPath } } });
96
99
  } else if (media?.decryptedFilePath) {
97
- parts.push(`[file: ${media.decryptedFilePath}]`);
100
+ segments.push({ type: 'file', data: { media: { kind: 'path', value: media.decryptedFilePath } } });
98
101
  } else if (media?.decryptedVoicePath) {
99
- parts.push(`[record: ${media.decryptedVoicePath}]`);
100
- } else if (!text && msg.item_list?.some((item) => isMediaItem(item))) {
101
- parts.push('[媒体消息]');
102
+ segments.push({ type: 'audio', data: { media: { kind: 'path', value: media.decryptedVoicePath } } });
102
103
  }
104
+ return Object.freeze(segments);
105
+ }
103
106
 
104
- return parts.join('\n').trim() || '';
107
+ export function weixinReplyTo(msg: WeixinMessageWithMedia): Readonly<{ id: string }> | undefined {
108
+ for (const item of msg.item_list ?? []) {
109
+ const id = item.ref_msg?.message_item?.msg_id;
110
+ if (id && String(id).trim()) return Object.freeze({ id: String(id).trim() });
111
+ }
112
+ return undefined;
105
113
  }
106
114
 
107
115
  export function inboundMessageId(msg: WeixinMessage): string {
@@ -211,20 +211,7 @@ export function bodyFromItemList(itemList?: MessageItem[]): string {
211
211
  if (!itemList?.length) return "";
212
212
  for (const item of itemList) {
213
213
  if (item.type === MessageItemType.TEXT && item.text_item?.text != null) {
214
- const text = String(item.text_item.text);
215
- const ref = item.ref_msg;
216
- if (!ref) return text;
217
- // Quoted media is passed as MediaPath; only include the current text as body.
218
- if (ref.message_item && isMediaItem(ref.message_item)) return text;
219
- // Build quoted context from both title and message_item content.
220
- const parts: string[] = [];
221
- if (ref.title) parts.push(ref.title);
222
- if (ref.message_item) {
223
- const refBody = bodyFromItemList([ref.message_item]);
224
- if (refBody) parts.push(refBody);
225
- }
226
- if (!parts.length) return text;
227
- return `[引用: ${parts.join(" | ")}]\n${text}`;
214
+ return String(item.text_item.text);
228
215
  }
229
216
  // 语音转文字:如果语音消息有 text 字段,直接使用文字内容
230
217
  if (item.type === MessageItemType.VOICE && item.voice_item?.text) {