@nextclaw/channel-extension-weixin 0.1.8 → 0.1.10
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/dist/main.js +2 -0
- package/dist/main.js.map +1 -0
- package/dist/services/weixin-api.service.d.ts +2 -1
- package/dist/services/weixin-api.service.d.ts.map +1 -0
- package/dist/services/weixin-api.service.js +2 -0
- package/dist/services/weixin-api.service.js.map +1 -0
- package/dist/services/weixin-auth-capability.service.d.ts +2 -1
- package/dist/services/weixin-auth-capability.service.d.ts.map +1 -0
- package/dist/services/weixin-auth-capability.service.js +2 -0
- package/dist/services/weixin-auth-capability.service.js.map +1 -0
- package/dist/services/weixin-channel-adapter.service.d.ts +2 -1
- package/dist/services/weixin-channel-adapter.service.d.ts.map +1 -0
- package/dist/services/weixin-channel-adapter.service.js +2 -0
- package/dist/services/weixin-channel-adapter.service.js.map +1 -0
- package/dist/services/weixin-login.service.d.ts +2 -1
- package/dist/services/weixin-login.service.d.ts.map +1 -0
- package/dist/services/weixin-login.service.js +2 -0
- package/dist/services/weixin-login.service.js.map +1 -0
- package/dist/services/weixin-media-part-reader.service.js +2 -0
- package/dist/services/weixin-media-part-reader.service.js.map +1 -0
- package/dist/services/weixin-reply-chat.service.js +2 -0
- package/dist/services/weixin-reply-chat.service.js.map +1 -0
- package/dist/services/weixin-typing-controller.service.js +2 -0
- package/dist/services/weixin-typing-controller.service.js.map +1 -0
- package/dist/stores/weixin-account.store.d.ts +2 -1
- package/dist/stores/weixin-account.store.d.ts.map +1 -0
- package/dist/stores/weixin-account.store.js +2 -0
- package/dist/stores/weixin-account.store.js.map +1 -0
- package/dist/types/weixin-extension.types.d.ts +2 -1
- package/dist/types/weixin-extension.types.d.ts.map +1 -0
- package/dist/utils/weixin-config.utils.d.ts +2 -1
- package/dist/utils/weixin-config.utils.d.ts.map +1 -0
- package/dist/utils/weixin-config.utils.js +2 -0
- package/dist/utils/weixin-config.utils.js.map +1 -0
- package/dist/utils/weixin-inbound-media.utils.js +2 -0
- package/dist/utils/weixin-inbound-media.utils.js.map +1 -0
- package/dist/utils/weixin-media.utils.js +2 -0
- package/dist/utils/weixin-media.utils.js.map +1 -0
- package/dist/utils/weixin-session-route.utils.js +2 -0
- package/dist/utils/weixin-session-route.utils.js.map +1 -0
- package/dist/utils/weixin-submitted-message.utils.js +2 -0
- package/dist/utils/weixin-submitted-message.utils.js.map +1 -0
- package/package.json +5 -5
package/dist/main.js
CHANGED
package/dist/main.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"main.js","names":[],"sources":["../src/main.ts"],"sourcesContent":["import { startChannelExtension, warnNcpEventError } from \"@nextclaw/extension-sdk\";\nimport { WeixinAuthCapability } from \"./services/weixin-auth-capability.service.js\";\nimport { WeixinChannelAdapter } from \"./services/weixin-channel-adapter.service.js\";\nimport { toWeixinSubmittedMessage } from \"./utils/weixin-submitted-message.utils.js\";\n\nawait startChannelExtension({\n channelId: \"weixin\",\n createAdapter: () => new WeixinChannelAdapter(),\n mapInbound: toWeixinSubmittedMessage,\n createAuthCapability: ({ channel }) => new WeixinAuthCapability({ channel }),\n onNcpEventError: warnNcpEventError(\"weixin\"),\n});\n"],"mappings":";;;;;AAKA,MAAM,sBAAsB;CAC1B,WAAW;CACX,qBAAqB,IAAI,sBAAsB;CAC/C,YAAY;CACZ,uBAAuB,EAAE,cAAc,IAAI,qBAAqB,EAAE,SAAS,CAAC;CAC5E,iBAAiB,kBAAkB,SAAS;CAC7C,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"weixin-api.service.d.ts","names":[],"sources":["../../src/services/weixin-api.service.ts"],"mappings":";KAEY,iBAAA;EACV,IAAA;EACA,SAAA;IAAc,IAAA;EAAA;EACd,UAAA;IACE,KAAA,GAAQ,cAAA;IACR,WAAA,GAAc,cAAA;IACd,MAAA;IACA,QAAA;IACA,UAAA;IACA,YAAA;IACA,WAAA;IACA,OAAA;EAAA;EAEF,UAAA;IAAe,IAAA;EAAA;EACf,SAAA;IACE,SAAA;IACA,KAAA,GAAQ,cAAA;IACR,GAAA;EAAA;AAAA;AAAA,KAIQ,cAAA;EACV,mBAAA;EACA,OAAA;EACA,YAAA;EACA,QAAA;AAAA;AAAA,KAGU,aAAA;EACV,UAAA;EACA,YAAA;EACA,UAAA;EACA,YAAA;EACA,SAAA,GAAY,iBAAA;EACZ,aAAA;AAAA;AAAA,KAGU,wBAAA;EACV,GAAA;EACA,OAAA;EACA,MAAA;EACA,IAAA,GAAO,aAAA;EACP,eAAA;AAAA;AAAA,KAGU,oBAAA;EACV,GAAA;EACA,OAAA;EACA,MAAA;EACA,aAAA;AAAA;AAAA,KAGU,oBAAA;EACV,MAAA;EACA,kBAAA;AAAA;AAAA,KAGU,sBAAA;EACV,MAAA;EACA,SAAA;EACA,YAAA;EACA,OAAA;EACA,aAAA;AAAA;AAAA,KAGU,wBAAA;EACV,GAAA;EACA,OAAA;EACA,MAAA;AAAA;AAAA,KAUU,eAAA;EACV,YAAA,GAAe,MAAA;IACb,OAAA;IACA,KAAA;IACA,MAAA;IACA,SAAA;IACA,MAAA,GAAS,WAAA;EAAA,MACL,OAAA,CAAQ,wBAAA;EACd,WAAA,GAAc,MAAA;IACZ,OAAA;IACA,KAAA;IACA,WAAA;IACA,YAAA;IACA,MAAA,GAAS,WAAA;EAAA,MACL,OAAA,CAAQ,oBAAA;EACd,WAAA,GAAc,MAAA;IACZ,OAAA;IACA,OAAA;IACA,MAAA,GAAS,WAAA;EAAA,MACL,OAAA,CAAQ,oBAAA;EACd,aAAA,GAAgB,MAAA;IACd,OAAA;IACA,MAAA;IACA,SAAA;IACA,MAAA,GAAS,WAAA;EAAA,MACL,OAAA,CAAQ,sBAAA;EACd,UAAA,GAAa,MAAA;IACX,OAAA;IACA,KAAA;IACA,QAAA;IACA,YAAA;IACA,MAAA;IACA,MAAA,GAAS,WAAA;EAAA,MACL,OAAA,CAAQ,wBAAA;EACd,eAAA,GAAkB,MAAA;IAChB,OAAA;IACA,KAAA;IACA,QAAA;IACA,IAAA;IACA,YAAA;IACA,MAAA,GAAS,WAAA;EAAA,MACL,OAAA;IAAU,SAAA;EAAA;EAChB,eAAA,GAAkB,MAAA;IAChB,OAAA;IACA,KAAA;IACA,QAAA;IACA,IAAA,EAAM,iBAAA;IACN,YAAA;IACA,MAAA,GAAS,WAAA;EAAA,MACL,OAAA;IAAU,SAAA;EAAA;AAAA"}
|
|
@@ -180,3 +180,5 @@ async function sendWeixinMessageItem(params) {
|
|
|
180
180
|
}
|
|
181
181
|
//#endregion
|
|
182
182
|
export { HttpWeixinApiClient, WEIXIN_API_TIMEOUT_MS, buildWeixinBaseInfo, fetchWeixinJson, normalizeWeixinBaseUrl, sendWeixinMessageItem };
|
|
183
|
+
|
|
184
|
+
//# sourceMappingURL=weixin-api.service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"weixin-api.service.js","names":[],"sources":["../../src/services/weixin-api.service.ts"],"sourcesContent":["import { randomInt, randomUUID } from \"node:crypto\";\n\nexport type WeixinMessageItem = {\n type?: number;\n text_item?: { text?: string };\n image_item?: {\n media?: WeixinCdnMedia;\n thumb_media?: WeixinCdnMedia;\n aeskey?: string;\n mid_size?: number;\n thumb_size?: number;\n thumb_height?: number;\n thumb_width?: number;\n hd_size?: number;\n };\n voice_item?: { text?: string };\n file_item?: {\n file_name?: string;\n media?: WeixinCdnMedia;\n len?: string;\n };\n};\n\nexport type WeixinCdnMedia = {\n encrypt_query_param?: string;\n aes_key?: string;\n encrypt_type?: number;\n full_url?: string;\n};\n\nexport type WeixinMessage = {\n message_id?: number;\n from_user_id?: string;\n to_user_id?: string;\n message_type?: number;\n item_list?: WeixinMessageItem[];\n context_token?: string;\n};\n\nexport type WeixinGetUpdatesResponse = {\n ret?: number;\n errcode?: number;\n errmsg?: string;\n msgs?: WeixinMessage[];\n get_updates_buf?: string;\n};\n\nexport type WeixinConfigResponse = {\n ret?: number;\n errcode?: number;\n errmsg?: string;\n typing_ticket?: string;\n};\n\nexport type WeixinQrCodeResponse = {\n qrcode?: string;\n qrcode_img_content?: string;\n};\n\nexport type WeixinQrStatusResponse = {\n status?: string;\n bot_token?: string;\n ilink_bot_id?: string;\n baseurl?: string;\n ilink_user_id?: string;\n};\n\nexport type WeixinSendTypingResponse = {\n ret?: number;\n errcode?: number;\n errmsg?: string;\n};\n\ntype WeixinBizResponse = {\n ret?: number;\n errcode?: number;\n errmsg?: string;\n message_id?: string | number;\n};\n\nexport type WeixinApiClient = {\n fetchUpdates: (params: {\n baseUrl: string;\n token: string;\n cursor?: string;\n timeoutMs: number;\n signal?: AbortSignal;\n }) => Promise<WeixinGetUpdatesResponse>;\n fetchConfig: (params: {\n baseUrl: string;\n token: string;\n ilinkUserId: string;\n contextToken: string;\n signal?: AbortSignal;\n }) => Promise<WeixinConfigResponse>;\n fetchQrCode: (params: {\n baseUrl: string;\n botType?: string;\n signal?: AbortSignal;\n }) => Promise<WeixinQrCodeResponse>;\n fetchQrStatus: (params: {\n baseUrl: string;\n qrcode: string;\n timeoutMs?: number;\n signal?: AbortSignal;\n }) => Promise<WeixinQrStatusResponse>;\n sendTyping: (params: {\n baseUrl: string;\n token: string;\n toUserId: string;\n typingTicket: string;\n status: 1 | 2;\n signal?: AbortSignal;\n }) => Promise<WeixinSendTypingResponse>;\n sendTextMessage: (params: {\n baseUrl: string;\n token: string;\n toUserId: string;\n text: string;\n contextToken?: string;\n signal?: AbortSignal;\n }) => Promise<{ messageId: string }>;\n sendMessageItem: (params: {\n baseUrl: string;\n token: string;\n toUserId: string;\n item: WeixinMessageItem;\n contextToken?: string;\n signal?: AbortSignal;\n }) => Promise<{ messageId: string }>;\n};\n\nexport const WEIXIN_API_TIMEOUT_MS = 15_000;\nexport const WEIXIN_MESSAGE_TYPE_BOT = 2;\nexport const WEIXIN_MESSAGE_ITEM_TYPE_TEXT = 1;\nexport const WEIXIN_MESSAGE_ITEM_TYPE_IMAGE = 2;\nexport const WEIXIN_MESSAGE_ITEM_TYPE_FILE = 4;\nexport const WEIXIN_UPLOAD_MEDIA_TYPE_IMAGE = 1;\nexport const WEIXIN_UPLOAD_MEDIA_TYPE_FILE = 3;\nexport const WEIXIN_MEDIA_ENCRYPT_TYPE_PACKED = 1;\nexport const DEFAULT_WEIXIN_BOT_TYPE = \"3\";\nconst WEIXIN_MESSAGE_STATE_FINISH = 2;\nconst WEIXIN_CHANNEL_VERSION = \"nextclaw-weixin/0.1.0\";\n\nexport function normalizeWeixinBaseUrl(baseUrl: string): string {\n return baseUrl.endsWith(\"/\") ? baseUrl : `${baseUrl}/`;\n}\n\nfunction buildUinHeader(): string {\n return Buffer.from(String(randomInt(0xffffffff)), \"utf8\").toString(\"base64\");\n}\n\nexport function buildWeixinBaseInfo(): { channel_version: string } {\n return {\n channel_version: WEIXIN_CHANNEL_VERSION,\n };\n}\n\nfunction stripSimpleMarkdown(text: string): string {\n return text\n .replace(/```[^\\n]*\\n?([\\s\\S]*?)```/g, \"$1\")\n .replace(/!\\[[^\\]]*]\\([^)]*\\)/g, \"\")\n .replace(/\\[([^\\]]+)]\\([^)]*\\)/g, \"$1\")\n .replace(/[*_~`>#-]+/g, \"\")\n .trim();\n}\n\nfunction assertSuccess(response: WeixinBizResponse, action: string): void {\n const failedRet = typeof response.ret === \"number\" && response.ret !== 0;\n const failedErrcode = typeof response.errcode === \"number\" && response.errcode !== 0;\n if (!failedRet && !failedErrcode) {\n return;\n }\n const detail = [\n response.ret === undefined ? null : `ret=${response.ret}`,\n response.errcode === undefined ? null : `errcode=${response.errcode}`,\n response.errmsg?.trim() ? `errmsg=${response.errmsg.trim()}` : null,\n ].filter(Boolean).join(\", \");\n throw new Error(detail ? `weixin ${action} failed: ${detail}` : `weixin ${action} failed`);\n}\n\nexport async function fetchWeixinJson<T>(params: {\n url: string;\n method?: \"GET\" | \"POST\";\n token?: string;\n body?: unknown;\n timeoutMs: number;\n headers?: Record<string, string>;\n signal?: AbortSignal;\n}): Promise<T> {\n const { body, method, signal, timeoutMs, token, url } = params;\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), Math.max(1_000, timeoutMs));\n const abort = () => controller.abort();\n signal?.addEventListener(\"abort\", abort, { once: true });\n try {\n const response = await fetch(url, {\n method: method ?? \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n AuthorizationType: \"ilink_bot_token\",\n \"X-WECHAT-UIN\": buildUinHeader(),\n ...(token ? { Authorization: `Bearer ${token}` } : {}),\n ...(params.headers ?? {}),\n },\n body: body === undefined ? undefined : JSON.stringify(body),\n signal: controller.signal,\n });\n const text = await response.text();\n if (!response.ok) {\n throw new Error(`weixin api ${response.status}: ${text}`);\n }\n return JSON.parse(text) as T;\n } finally {\n clearTimeout(timeout);\n signal?.removeEventListener(\"abort\", abort);\n }\n}\n\nexport function assertWeixinSuccess(response: WeixinBizResponse, action: string): void {\n assertSuccess(response, action);\n}\n\nexport class HttpWeixinApiClient implements WeixinApiClient {\n readonly fetchUpdates = async (params: {\n baseUrl: string;\n token: string;\n cursor?: string;\n timeoutMs: number;\n signal?: AbortSignal;\n }): Promise<WeixinGetUpdatesResponse> => {\n const { baseUrl, cursor, signal, timeoutMs, token } = params;\n try {\n const response = await fetchWeixinJson<WeixinGetUpdatesResponse>({\n url: new URL(\"ilink/bot/getupdates\", normalizeWeixinBaseUrl(baseUrl)).toString(),\n token,\n timeoutMs,\n signal,\n body: {\n get_updates_buf: cursor ?? \"\",\n base_info: buildWeixinBaseInfo(),\n },\n });\n assertSuccess(response, \"getupdates\");\n return response;\n } catch (error) {\n if (error instanceof Error && error.name === \"AbortError\") {\n return { ret: 0, msgs: [], get_updates_buf: cursor };\n }\n throw error;\n }\n };\n\n readonly fetchConfig = async (params: {\n baseUrl: string;\n token: string;\n ilinkUserId: string;\n contextToken: string;\n signal?: AbortSignal;\n }): Promise<WeixinConfigResponse> => {\n const { baseUrl, contextToken, ilinkUserId, signal, token } = params;\n const response = await fetchWeixinJson<WeixinConfigResponse>({\n url: new URL(\"ilink/bot/getconfig\", normalizeWeixinBaseUrl(baseUrl)).toString(),\n token,\n timeoutMs: WEIXIN_API_TIMEOUT_MS,\n signal,\n body: {\n ilink_user_id: ilinkUserId,\n context_token: contextToken,\n base_info: buildWeixinBaseInfo(),\n },\n });\n assertSuccess(response, \"getconfig\");\n return response;\n };\n\n readonly sendTyping = async (params: {\n baseUrl: string;\n token: string;\n toUserId: string;\n typingTicket: string;\n status: 1 | 2;\n signal?: AbortSignal;\n }): Promise<WeixinSendTypingResponse> => {\n const { baseUrl, signal, status, toUserId, token, typingTicket } = params;\n const response = await fetchWeixinJson<WeixinSendTypingResponse>({\n url: new URL(\"ilink/bot/sendtyping\", normalizeWeixinBaseUrl(baseUrl)).toString(),\n token,\n timeoutMs: WEIXIN_API_TIMEOUT_MS,\n signal,\n body: {\n ilink_user_id: toUserId,\n typing_ticket: typingTicket,\n status,\n base_info: buildWeixinBaseInfo(),\n },\n });\n assertSuccess(response, \"sendtyping\");\n return response;\n };\n\n readonly fetchQrCode = async (params: {\n baseUrl: string;\n botType?: string;\n signal?: AbortSignal;\n }): Promise<WeixinQrCodeResponse> => {\n const { baseUrl, botType, signal } = params;\n const url = new URL(\n `ilink/bot/get_bot_qrcode?bot_type=${encodeURIComponent(botType ?? DEFAULT_WEIXIN_BOT_TYPE)}`,\n normalizeWeixinBaseUrl(baseUrl),\n );\n return await fetchWeixinJson<WeixinQrCodeResponse>({\n url: url.toString(),\n method: \"GET\",\n timeoutMs: WEIXIN_API_TIMEOUT_MS,\n signal,\n });\n };\n\n readonly fetchQrStatus = async (params: {\n baseUrl: string;\n qrcode: string;\n timeoutMs?: number;\n signal?: AbortSignal;\n }): Promise<WeixinQrStatusResponse> => {\n const { baseUrl, qrcode, signal, timeoutMs } = params;\n const url = new URL(\n `ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(qrcode)}`,\n normalizeWeixinBaseUrl(baseUrl),\n );\n try {\n return await fetchWeixinJson<WeixinQrStatusResponse>({\n url: url.toString(),\n method: \"GET\",\n timeoutMs: timeoutMs ?? WEIXIN_API_TIMEOUT_MS,\n headers: {\n \"iLink-App-ClientVersion\": \"1\",\n },\n signal,\n });\n } catch (error) {\n if (error instanceof Error && error.name === \"AbortError\") {\n return { status: \"wait\" };\n }\n throw error;\n }\n };\n\n readonly sendMessageItem = async (params: {\n baseUrl: string;\n token: string;\n toUserId: string;\n item: WeixinMessageItem;\n contextToken?: string;\n signal?: AbortSignal;\n }): Promise<{ messageId: string }> => {\n const { baseUrl, contextToken, item, signal, token, toUserId } = params;\n const clientMessageId = randomUUID();\n const response = await fetchWeixinJson<WeixinBizResponse>({\n url: new URL(\"ilink/bot/sendmessage\", normalizeWeixinBaseUrl(baseUrl)).toString(),\n token,\n timeoutMs: WEIXIN_API_TIMEOUT_MS,\n signal,\n body: {\n msg: {\n from_user_id: \"\",\n to_user_id: toUserId,\n client_id: clientMessageId,\n message_type: WEIXIN_MESSAGE_TYPE_BOT,\n message_state: WEIXIN_MESSAGE_STATE_FINISH,\n item_list: [item],\n context_token: contextToken ?? \"\",\n },\n base_info: buildWeixinBaseInfo(),\n },\n });\n assertSuccess(response, \"sendmessage\");\n return {\n messageId:\n typeof response.message_id === \"string\"\n ? response.message_id\n : typeof response.message_id === \"number\"\n ? String(response.message_id)\n : clientMessageId,\n };\n };\n\n readonly sendTextMessage = async (params: {\n baseUrl: string;\n token: string;\n toUserId: string;\n text: string;\n contextToken?: string;\n signal?: AbortSignal;\n }): Promise<{ messageId: string }> => {\n const { baseUrl, contextToken, signal, text, token, toUserId } = params;\n return await this.sendMessageItem({\n baseUrl,\n token,\n toUserId,\n contextToken,\n signal,\n item: {\n type: WEIXIN_MESSAGE_ITEM_TYPE_TEXT,\n text_item: {\n text: stripSimpleMarkdown(text),\n },\n },\n });\n };\n}\n\nexport async function sendWeixinMessageItem(params: {\n baseUrl: string;\n token: string;\n toUserId: string;\n item: WeixinMessageItem;\n contextToken?: string;\n signal?: AbortSignal;\n}): Promise<{ messageId: string }> {\n return await new HttpWeixinApiClient().sendMessageItem(params);\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"weixin-auth-capability.service.d.ts","names":[],"sources":["../../src/services/weixin-auth-capability.service.ts"],"mappings":";;;;KAMK,wBAAA;EACH,OAAA,EAAS,gBAAA;EACT,YAAA,GAAe,IAAA,CAAK,kBAAA;AAAA;AAAA,cAGT,oBAAA;EAAA,iBACM,OAAA;EAAA,iBACA,YAAA;cAEL,IAAA,EAAM,wBAAA;EAAA,SAKT,KAAA,GACP,OAAA,EAAS,0BAAA,KACR,OAAA;EAAA,SAOM,IAAA,GACP,OAAA,EAAS,0BAAA,KACR,OAAA;EAAA,SAQM,KAAA,GACP,OAAA,EAAS,0BAAA,KACR,OAAA;EAAA,iBAQc,iBAAA;EAAA,iBAOA,UAAA;AAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"weixin-auth-capability.service.js","names":[],"sources":["../../src/services/weixin-auth-capability.service.ts"],"sourcesContent":["import type {\n ExtensionChannel,\n ExtensionCapabilityPayload,\n} from \"@nextclaw/extension-sdk\";\nimport { WeixinLoginService } from \"./weixin-login.service.js\";\n\ntype WeixinAuthCapabilityDeps = {\n channel: ExtensionChannel;\n loginService?: Pick<WeixinLoginService, \"start\" | \"poll\" | \"login\">;\n};\n\nexport class WeixinAuthCapability {\n private readonly channel: ExtensionChannel;\n private readonly loginService: Pick<WeixinLoginService, \"start\" | \"poll\" | \"login\">;\n\n constructor(deps: WeixinAuthCapabilityDeps) {\n this.channel = deps.channel;\n this.loginService = deps.loginService ?? new WeixinLoginService();\n }\n\n readonly start = async (\n request: ExtensionCapabilityPayload,\n ): Promise<unknown> =>\n await this.loginService.start({\n pluginConfig: await this.readCurrentConfig(),\n requestedAccountId: this.readString(request.accountId),\n baseUrl: this.readString(request.baseUrl),\n });\n\n readonly poll = async (\n request: ExtensionCapabilityPayload,\n ): Promise<unknown> => {\n const sessionId = this.readString(request.sessionId);\n if (!sessionId) {\n throw new Error(\"sessionId is required\");\n }\n return await this.loginService.poll({ sessionId });\n };\n\n readonly login = async (\n request: ExtensionCapabilityPayload,\n ): Promise<unknown> =>\n await this.loginService.login({\n pluginConfig: await this.readCurrentConfig(),\n requestedAccountId: this.readString(request.accountId),\n baseUrl: this.readString(request.baseUrl),\n verbose: request.verbose === true,\n });\n\n private readonly readCurrentConfig = async (): Promise<Record<string, unknown>> => {\n const config = await this.channel.config.get();\n return config && typeof config === \"object\" && !Array.isArray(config)\n ? config as Record<string, unknown>\n : {};\n };\n\n private readonly readString = (value: unknown): string | null =>\n typeof value === \"string\" && value.trim() ? value.trim() : null;\n}\n"],"mappings":";;AAWA,IAAa,uBAAb,MAAkC;CAChC;CACA;CAEA,YAAY,MAAgC;AAC1C,OAAK,UAAU,KAAK;AACpB,OAAK,eAAe,KAAK,gBAAgB,IAAI,oBAAoB;;CAGnE,QAAiB,OACf,YAEA,MAAM,KAAK,aAAa,MAAM;EAC5B,cAAc,MAAM,KAAK,mBAAmB;EAC5C,oBAAoB,KAAK,WAAW,QAAQ,UAAU;EACtD,SAAS,KAAK,WAAW,QAAQ,QAAQ;EAC1C,CAAC;CAEJ,OAAgB,OACd,YACqB;EACrB,MAAM,YAAY,KAAK,WAAW,QAAQ,UAAU;AACpD,MAAI,CAAC,UACH,OAAM,IAAI,MAAM,wBAAwB;AAE1C,SAAO,MAAM,KAAK,aAAa,KAAK,EAAE,WAAW,CAAC;;CAGpD,QAAiB,OACf,YAEA,MAAM,KAAK,aAAa,MAAM;EAC5B,cAAc,MAAM,KAAK,mBAAmB;EAC5C,oBAAoB,KAAK,WAAW,QAAQ,UAAU;EACtD,SAAS,KAAK,WAAW,QAAQ,QAAQ;EACzC,SAAS,QAAQ,YAAY;EAC9B,CAAC;CAEJ,oBAAqC,YAA8C;EACjF,MAAM,SAAS,MAAM,KAAK,QAAQ,OAAO,KAAK;AAC9C,SAAO,UAAU,OAAO,WAAW,YAAY,CAAC,MAAM,QAAQ,OAAO,GACjE,SACA,EAAE;;CAGR,cAA+B,UAC7B,OAAO,UAAU,YAAY,MAAM,MAAM,GAAG,MAAM,MAAM,GAAG"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"weixin-channel-adapter.service.d.ts","names":[],"sources":["../../src/services/weixin-channel-adapter.service.ts"],"mappings":";;;;;;KA0BK,iBAAA;EACH,GAAA,GAAM,eAAA;EACN,KAAA,GAAQ,kBAAA;EACR,KAAA,IAAS,EAAA,UAAY,MAAA,GAAS,WAAA,KAAgB,OAAA;EAC9C,MAAA,GAAS,IAAA,QAAY,OAAA;AAAA;AAAA,cAoGV,oBAAA;EAAA,QACH,cAAA;EAAA,iBACS,GAAA;EAAA,iBACA,KAAA;EAAA,iBACA,KAAA;EAAA,iBACA,MAAA;EAAA,iBACA,aAAA;EAAA,iBACA,gBAAA;EAAA,iBACA,aAAA;EAAA,iBACA,kBAAA;EAAA,iBACA,SAAA;EAAA,iBACA,aAAA;EAAA,iBACA,eAAA;EAAA,QACT,OAAA;EAAA,QACA,MAAA;cAEI,IAAA,GAAM,iBAAA;EAAA,SAoCT,SAAA,GAAmB,MAAA,EAAQ,mBAAA,KAAsB,OAAA;EAAA,SASjD,KAAA,QAAkB,OAAA;EAAA,SAYlB,IAAA,QAAiB,OAAA;EAAA,SAiBjB,SAAA,GACP,OAAA,GAAU,OAAA,EAAS,oBAAA,YAAgC,OAAA;EAAA,SAU5C,YAAA,GAAsB,KAAA,EAAO,gBAAA,KAAmB,OAAA;EAAA,SAgChD,kBAAA,GAA4B,OAAA,EAAS,oBAAA,KAAuB,OAAA;EAAA,SAI5D,QAAA,GAAkB,MAAA;IACzB,OAAA,EAAS,oBAAA;IACT,cAAA;IACA,IAAA;IACA,YAAA;EAAA,MACE,OAAA;EAAA,SAmBK,gBAAA,GAA0B,MAAA;IACjC,EAAA;IACA,IAAA;IACA,SAAA;EAAA,MACE,OAAA;EAAA,iBAea,mBAAA;EAAA,iBAqBA,uBAAA;EAAA,iBAQA,wBAAA;EAAA,iBAOA,qBAAA;EAAA,iBAaA,+BAAA;EAAA,iBAmBA,qBAAA;EAAA,iBAuBA,wBAAA;EAAA,iBAcA,kBAAA;EAAA,iBAQA,mBAAA;EAAA,iBAQA,cAAA;EAAA,iBAoCA,oBAAA;AAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"weixin-channel-adapter.service.js","names":[],"sources":["../../src/services/weixin-channel-adapter.service.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { appendFileSync } from \"node:fs\";\nimport type { NcpEndpointEvent } from \"@nextclaw/ncp\";\nimport { NcpReplyConsumer, type ChatTarget } from \"@nextclaw/ncp-toolkit\";\nimport { FileWeixinAccountStore, type WeixinAccountStore } from \"../stores/weixin-account.store.js\";\nimport { HttpWeixinApiClient, type WeixinApiClient, type WeixinMessage } from \"./weixin-api.service.js\";\nimport {\n NcpEventQueue,\n TERMINAL_NCP_EVENT_TYPES,\n WeixinReplyChat,\n type WeixinReplySession,\n} from \"./weixin-reply-chat.service.js\";\nimport { resolveWeixinInboundAttachments } from \"../utils/weixin-inbound-media.utils.js\";\nimport { readWeixinEventSessionId, resolveWeixinSessionRoute } from \"../utils/weixin-session-route.utils.js\";\nimport { WeixinTypingController } from \"./weixin-typing-controller.service.js\";\nimport type {\n WeixinChannelConfig,\n WeixinInboundMessage,\n WeixinRuntimeAccount,\n} from \"../types/weixin-extension.types.js\";\n\ntype WeixinCanonicalRoute = {\n conversationId: string;\n accountId?: string;\n};\n\ntype WeixinAdapterDeps = {\n api?: WeixinApiClient;\n store?: WeixinAccountStore;\n sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;\n logger?: Pick<typeof console, \"warn\">;\n};\n\nconst DEFAULT_WEIXIN_BASE_URL = \"https://ilinkai.weixin.qq.com\";\nconst DEFAULT_WEIXIN_POLL_TIMEOUT_MS = 35_000;\nconst WEIXIN_MESSAGE_ITEM_TYPE_IMAGE = 2;\nconst WEIXIN_MESSAGE_ITEM_TYPE_VOICE = 3;\nconst WEIXIN_MESSAGE_ITEM_TYPE_FILE = 4;\nconst WEIXIN_MESSAGE_ITEM_TYPE_VIDEO = 5;\n\nfunction textHash(text: string): string {\n return createHash(\"sha256\").update(text).digest(\"hex\").slice(0, 12);\n}\n\nfunction writeWeixinDebugProbe(event: string, payload: Record<string, unknown>): void {\n const file = process.env.NEXTCLAW_WEIXIN_DEBUG_LOG?.trim();\n if (!file) {\n return;\n }\n appendFileSync(file, `${JSON.stringify({\n at: new Date().toISOString(),\n event,\n pid: process.pid,\n ...payload,\n })}\\n`);\n}\n\nasync function defaultSleep(ms: number, signal?: AbortSignal): Promise<void> {\n await new Promise<void>((resolve) => {\n const timer = setTimeout(resolve, Math.max(0, ms));\n const onAbort = () => {\n clearTimeout(timer);\n resolve();\n };\n signal?.addEventListener(\"abort\", onAbort, { once: true });\n });\n}\n\nfunction readStringArray(value: unknown): string[] {\n if (!Array.isArray(value)) {\n return [];\n }\n return value\n .map((entry) => (typeof entry === \"string\" ? entry.trim() : \"\"))\n .filter(Boolean);\n}\n\nfunction isAllowedSender(allowFrom: string[], senderId: string): boolean {\n if (allowFrom.length === 0) {\n return true;\n }\n if (allowFrom.includes(senderId)) {\n return true;\n }\n return senderId.includes(\"|\") && senderId.split(\"|\").some((part) => allowFrom.includes(part));\n}\n\nfunction normalizeRouteKey(conversationId: string): string {\n return conversationId.toLowerCase();\n}\n\nfunction isSyntheticAttachmentText(text: string): boolean {\n return (\n text === \"[收到图片]\" ||\n text === \"[收到视频]\" ||\n text === \"[收到语音]\" ||\n /^\\[收到文件(?:: .+)?]$/.test(text)\n );\n}\n\nfunction extractText(message: WeixinMessage): string {\n const items = Array.isArray(message.item_list) ? message.item_list : [];\n for (const item of items) {\n const text = item.text_item?.text?.trim();\n if (text) {\n return text;\n }\n const voiceText = item.voice_item?.text?.trim();\n if (voiceText) {\n return voiceText;\n }\n }\n for (const item of items) {\n if (item.type === WEIXIN_MESSAGE_ITEM_TYPE_IMAGE) {\n return \"[收到图片]\";\n }\n if (item.type === WEIXIN_MESSAGE_ITEM_TYPE_VIDEO) {\n return \"[收到视频]\";\n }\n if (item.type === WEIXIN_MESSAGE_ITEM_TYPE_VOICE) {\n return \"[收到语音]\";\n }\n if (item.type === WEIXIN_MESSAGE_ITEM_TYPE_FILE) {\n const fileName = item.file_item?.file_name?.trim();\n return fileName ? `[收到文件: ${fileName}]` : \"[收到文件]\";\n }\n }\n return \"\";\n}\n\nexport class WeixinChannelAdapter {\n private messageHandler: ((message: WeixinInboundMessage) => void | Promise<void>) | null = null;\n private readonly api: WeixinApiClient;\n private readonly store: WeixinAccountStore;\n private readonly sleep: (ms: number, signal?: AbortSignal) => Promise<void>;\n private readonly logger: Pick<typeof console, \"warn\">;\n private readonly replyConsumer: NcpReplyConsumer;\n private readonly typingController: WeixinTypingController;\n private readonly contextTokens = new Map<string, string>();\n private readonly accountControllers = new Map<string, AbortController>();\n private readonly pollTasks: Promise<void>[] = [];\n private readonly replySessions = new Map<string, WeixinReplySession>();\n private readonly canonicalRoutes = new Map<string, WeixinCanonicalRoute>();\n private running = false;\n private config: WeixinChannelConfig = {};\n\n constructor(deps: WeixinAdapterDeps = {}) {\n this.api = deps.api ?? new HttpWeixinApiClient();\n this.store = deps.store ?? new FileWeixinAccountStore();\n this.sleep = deps.sleep ?? defaultSleep;\n this.logger = deps.logger ?? console;\n this.typingController = new WeixinTypingController({\n fetchTicket: async (runtime) => {\n const response = await this.api.fetchConfig({\n baseUrl: runtime.baseUrl,\n token: runtime.token,\n ilinkUserId: runtime.userId,\n contextToken: runtime.contextToken,\n });\n return response.typing_ticket?.trim();\n },\n sendTyping: async (params) => {\n const { baseUrl, status, ticket, token, userId } = params;\n await this.api.sendTyping({\n baseUrl,\n token,\n toUserId: userId,\n typingTicket: ticket,\n status,\n });\n },\n });\n this.replyConsumer = new NcpReplyConsumer(\n new WeixinReplyChat({\n resolveAccount: this.resolveSendAccount,\n resolveContextToken: this.resolveContextToken,\n sendText: this.sendText,\n typingController: this.typingController,\n }),\n );\n }\n\n readonly configure = async (config: WeixinChannelConfig): Promise<void> => {\n this.config = config;\n if (!this.running) {\n return;\n }\n await this.stop();\n await this.start();\n };\n\n readonly start = async (): Promise<void> => {\n if (this.running) {\n return;\n }\n this.running = true;\n for (const accountId of this.listAvailableAccountIds()) {\n const controller = new AbortController();\n this.accountControllers.set(accountId, controller);\n this.pollTasks.push(this.runPollingLoop(accountId, controller.signal));\n }\n };\n\n readonly stop = async (): Promise<void> => {\n if (!this.running) {\n return;\n }\n this.running = false;\n for (const controller of this.accountControllers.values()) {\n controller.abort();\n }\n this.accountControllers.clear();\n for (const session of this.replySessions.values()) {\n session.queue.close();\n }\n this.replySessions.clear();\n await Promise.allSettled(this.pollTasks.splice(0, this.pollTasks.length));\n await this.typingController.stopAll();\n };\n\n readonly onMessage = (\n handler: (message: WeixinInboundMessage) => void | Promise<void>,\n ): (() => void) => {\n this.messageHandler = handler;\n return () => {\n if (this.messageHandler === handler) {\n this.messageHandler = null;\n }\n };\n };\n\n readonly sendNcpEvent = async (event: NcpEndpointEvent): Promise<void> => {\n if (!this.running) {\n return;\n }\n const route = resolveWeixinSessionRoute(event);\n if (!route) {\n return;\n }\n const sessionId = readWeixinEventSessionId(event);\n if (!sessionId) {\n return;\n }\n writeWeixinDebugProbe(\"ncp-event\", {\n sessionId,\n type: event.type,\n replySessionOpen: this.replySessions.has(sessionId),\n routeConversationId: route.conversationId,\n routeAccountId: route.accountId,\n messageId: \"payload\" in event && event.payload && typeof event.payload === \"object\" &&\n \"messageId\" in event.payload ? event.payload.messageId : undefined,\n runId: \"payload\" in event && event.payload && typeof event.payload === \"object\" &&\n \"runId\" in event.payload ? event.payload.runId : undefined,\n });\n const session = this.resolveReplySession(sessionId, this.resolveCanonicalRoute(route));\n session.queue.push(event);\n if (TERMINAL_NCP_EVENT_TYPES.has(event.type)) {\n session.queue.close();\n this.replySessions.delete(sessionId);\n await session.consuming;\n }\n };\n\n readonly emitMessageForTest = async (message: WeixinInboundMessage): Promise<void> => {\n await this.messageHandler?.(message);\n };\n\n readonly sendText = async (params: {\n account: WeixinRuntimeAccount;\n conversationId: string;\n text: string;\n contextToken?: string;\n }): Promise<void> => {\n const { account, contextToken, conversationId, text } = params;\n writeWeixinDebugProbe(\"send-text\", {\n accountId: account.accountId,\n conversationId,\n textHash: textHash(text),\n textLength: text.length,\n textPreview: text.slice(0, 80),\n hasContextToken: Boolean(contextToken),\n });\n await this.api.sendTextMessage({\n baseUrl: account.baseUrl,\n token: account.token,\n toUserId: conversationId,\n text,\n contextToken,\n });\n };\n\n readonly sendOutboundText = async (params: {\n to: string;\n text: string;\n accountId?: string | null;\n }): Promise<void> => {\n const { accountId, text, to } = params;\n const target: ChatTarget = {\n conversationId: to,\n ...(accountId ? { accountId } : {}),\n };\n const account = this.resolveSendAccount(target);\n await this.sendText({\n account,\n conversationId: target.conversationId,\n text,\n contextToken: this.resolveContextToken(target, account.accountId),\n });\n };\n\n private readonly resolveReplySession = (\n sessionId: string,\n route: WeixinCanonicalRoute,\n ): WeixinReplySession => {\n const existing = this.replySessions.get(sessionId);\n if (existing) {\n return existing;\n }\n const queue = new NcpEventQueue();\n const consuming = this.replyConsumer.consume({\n target: {\n conversationId: route.conversationId,\n ...(route.accountId ? { accountId: route.accountId } : {}),\n },\n eventStream: queue,\n });\n const session = { queue, consuming };\n this.replySessions.set(sessionId, session);\n return session;\n };\n\n private readonly listAvailableAccountIds = (): string[] => {\n const configuredAccountIds = this.listConfiguredAccountIds();\n if (configuredAccountIds.length > 0) {\n return configuredAccountIds;\n }\n return this.store.listAccountIds();\n };\n\n private readonly listConfiguredAccountIds = (): string[] => {\n return Array.from(new Set([\n ...(this.config.defaultAccountId ? [this.config.defaultAccountId] : []),\n ...Object.keys(this.config.accounts ?? {}),\n ]));\n };\n\n private readonly resolveCanonicalRoute = (\n route: WeixinCanonicalRoute,\n ): WeixinCanonicalRoute => {\n const remembered = this.canonicalRoutes.get(normalizeRouteKey(route.conversationId));\n if (remembered) {\n return {\n ...remembered,\n ...(route.accountId ? { accountId: route.accountId } : {}),\n };\n }\n return this.resolveConfiguredCanonicalRoute(route.conversationId) ?? route;\n };\n\n private readonly resolveConfiguredCanonicalRoute = (\n conversationId: string,\n ): WeixinCanonicalRoute | null => {\n const routeKey = normalizeRouteKey(conversationId);\n const globalMatch = readStringArray(this.config.allowFrom)\n .find((candidate) => normalizeRouteKey(candidate) === routeKey);\n if (globalMatch) {\n return { conversationId: globalMatch };\n }\n for (const [accountId, accountConfig] of Object.entries(this.config.accounts ?? {})) {\n const accountMatch = readStringArray(accountConfig.allowFrom)\n .find((candidate) => normalizeRouteKey(candidate) === routeKey);\n if (accountMatch) {\n return { accountId, conversationId: accountMatch };\n }\n }\n return null;\n };\n\n private readonly resolveRuntimeAccount = (accountId: string): WeixinRuntimeAccount | null => {\n const configuredAccountIds = this.listConfiguredAccountIds();\n if (configuredAccountIds.length > 0 && !configuredAccountIds.includes(accountId)) {\n return null;\n }\n const stored = this.store.loadAccount(accountId);\n if (!stored?.token) {\n return null;\n }\n const accountConfig = this.config.accounts?.[accountId] ?? {};\n return {\n accountId,\n token: stored.token,\n enabled: accountConfig.enabled !== false && this.config.enabled !== false,\n baseUrl: accountConfig.baseUrl || stored.baseUrl || this.config.baseUrl || DEFAULT_WEIXIN_BASE_URL,\n pollTimeoutMs: this.config.pollTimeoutMs ?? DEFAULT_WEIXIN_POLL_TIMEOUT_MS,\n allowFrom: Array.from(new Set([\n ...readStringArray(this.config.allowFrom),\n ...readStringArray(accountConfig.allowFrom),\n ])),\n };\n };\n\n private readonly resolveSelectedAccountId = (requestedAccountId?: string): string => {\n if (requestedAccountId) {\n return requestedAccountId;\n }\n if (this.config.defaultAccountId) {\n return this.config.defaultAccountId;\n }\n const accountIds = this.listAvailableAccountIds();\n if (accountIds.length === 1 && accountIds[0]) {\n return accountIds[0];\n }\n throw new Error(\"weixin send failed: accountId is required when multiple accounts are configured\");\n };\n\n private readonly resolveSendAccount = (target: ChatTarget): WeixinRuntimeAccount => {\n const account = this.resolveRuntimeAccount(this.resolveSelectedAccountId(target.accountId));\n if (!account?.enabled || !account.token) {\n throw new Error(`weixin send failed: account \"${target.accountId ?? this.config.defaultAccountId ?? \"\"}\" is not logged in`);\n }\n return account;\n };\n\n private readonly resolveContextToken = (target: ChatTarget, accountId: string): string | undefined => {\n const metadataToken = target.metadata?.context_token;\n if (typeof metadataToken === \"string\" && metadataToken.trim()) {\n return metadataToken.trim();\n }\n return this.contextTokens.get(`${accountId}:${target.conversationId}`);\n };\n\n private readonly runPollingLoop = async (accountId: string, signal: AbortSignal): Promise<void> => {\n while (this.running && !signal.aborted) {\n try {\n const account = this.resolveRuntimeAccount(accountId);\n if (!account?.enabled) {\n await this.sleep(3_000, signal);\n continue;\n }\n const response = await this.api.fetchUpdates({\n baseUrl: account.baseUrl,\n token: account.token,\n cursor: this.store.loadCursor(accountId),\n timeoutMs: account.pollTimeoutMs,\n signal,\n });\n if (response.get_updates_buf !== undefined) {\n this.store.saveCursor(accountId, response.get_updates_buf);\n }\n for (const message of response.msgs ?? []) {\n await this.handleInboundMessage(account, message);\n }\n } catch (error) {\n if (!signal.aborted) {\n const message = error instanceof Error ? error.message : String(error);\n if (message.includes(\"errcode=-14\") || message.includes(\"session timeout\")) {\n this.store.deleteCursor(accountId);\n await this.sleep(1_000, signal);\n continue;\n }\n this.logger.warn(`[weixin] polling failed for ${accountId}: ${message}`);\n await this.sleep(3_000, signal);\n }\n }\n }\n };\n\n private readonly handleInboundMessage = async (\n account: WeixinRuntimeAccount,\n message: WeixinMessage,\n ): Promise<void> => {\n const senderId = message.from_user_id?.trim();\n if (!senderId || senderId === account.accountId || !isAllowedSender(account.allowFrom, senderId)) {\n return;\n }\n const attachments = await resolveWeixinInboundAttachments({\n message,\n baseUrl: account.baseUrl,\n });\n const extractedText = extractText(message);\n const text =\n attachments.length > 0 && isSyntheticAttachmentText(extractedText)\n ? \"\"\n : extractedText;\n if (!text && attachments.length === 0) {\n return;\n }\n writeWeixinDebugProbe(\"inbound-message\", {\n accountId: account.accountId,\n senderId,\n messageId: message.message_id,\n textHash: text ? textHash(text) : undefined,\n textLength: text.length,\n textPreview: text.slice(0, 80),\n attachmentCount: attachments.length,\n });\n const contextToken = message.context_token?.trim();\n if (contextToken) {\n this.contextTokens.set(`${account.accountId}:${senderId}`, contextToken);\n }\n this.canonicalRoutes.set(normalizeRouteKey(senderId), {\n accountId: account.accountId,\n conversationId: senderId,\n });\n if (contextToken) {\n void this.typingController.start({\n accountId: account.accountId,\n userId: senderId,\n contextToken,\n baseUrl: account.baseUrl,\n token: account.token,\n });\n }\n await this.messageHandler?.({\n conversationId: senderId,\n senderId,\n text,\n attachments,\n accountId: account.accountId,\n ...(contextToken ? { contextToken } : {}),\n raw: message,\n });\n };\n}\n"],"mappings":"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"}
|
|
@@ -52,4 +52,5 @@ declare class WeixinLoginService {
|
|
|
52
52
|
private readonly cleanupExpiredSessions;
|
|
53
53
|
}
|
|
54
54
|
//#endregion
|
|
55
|
-
export { WeixinAuthPollResult, WeixinAuthStartResult, WeixinLoginParams, WeixinLoginService };
|
|
55
|
+
export { WeixinAuthPollResult, WeixinAuthStartResult, WeixinLoginParams, WeixinLoginService };
|
|
56
|
+
//# sourceMappingURL=weixin-login.service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"weixin-login.service.d.ts","names":[],"sources":["../../src/services/weixin-login.service.ts"],"mappings":";;;;KAWY,iBAAA;EACV,YAAA,GAAe,MAAA;EACf,kBAAA;EACA,OAAA;EACA,OAAA;AAAA;AAAA,KAGU,qBAAA;EACV,OAAA;EACA,IAAA;EACA,SAAA;EACA,MAAA;EACA,SAAA;EACA,SAAA;EACA,UAAA;EACA,IAAA;AAAA;AAAA,KAGU,oBAAA;EACV,OAAA;EACA,MAAA;EACA,OAAA;EACA,UAAA;EACA,SAAA;EACA,KAAA;EACA,YAAA,GAAe,MAAA;AAAA;AAAA,KAWZ,sBAAA;EACH,GAAA,GAAM,eAAA;EACN,KAAA,GAAQ,kBAAA;AAAA;AAAA,cAiCG,kBAAA;EAAA,iBACM,GAAA;EAAA,iBACA,KAAA;EAAA,iBACA,QAAA;cAEL,IAAA,GAAM,sBAAA;EAAA,SAKT,KAAA,GAAe,MAAA,EAAQ,iBAAA,KAAoB,OAAA,CAAQ,qBAAA;EAAA,SAkCnD,IAAA;IAAc;EAAA;IAAiB,SAAA;EAAA,MAAsB,OAAA,CAAQ,oBAAA;EAAA,SAuE7D,KAAA,GACP,MAAA,EAAQ,iBAAA,KACP,OAAA;IAAU,YAAA,EAAc,MAAA;IAAyB,SAAA;IAA2B,KAAA;EAAA;EAAA,iBAuC9D,YAAA;EAAA,iBAqDA,4BAAA;EAAA,iBAiCA,sBAAA;AAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"weixin-login.service.js","names":[],"sources":["../../src/services/weixin-login.service.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport { FileWeixinAccountStore, type WeixinAccountStore } from \"../stores/weixin-account.store.js\";\nimport { HttpWeixinApiClient, type WeixinApiClient, type WeixinQrStatusResponse } from \"./weixin-api.service.js\";\nimport {\n buildLoggedInWeixinChannelConfig,\n DEFAULT_WEIXIN_BASE_URL,\n normalizeWeixinChannelConfig,\n WEIXIN_CHANNEL_ID,\n} from \"../utils/weixin-config.utils.js\";\nimport type { WeixinChannelConfig } from \"../types/weixin-extension.types.js\";\n\nexport type WeixinLoginParams = {\n pluginConfig?: Record<string, unknown>;\n requestedAccountId?: string | null;\n baseUrl?: string | null;\n verbose?: boolean;\n};\n\nexport type WeixinAuthStartResult = {\n channel: string;\n kind: \"qr_code\";\n sessionId: string;\n qrCode: string;\n qrCodeUrl: string;\n expiresAt: string;\n intervalMs: number;\n note?: string;\n};\n\nexport type WeixinAuthPollResult = {\n channel: string;\n status: \"pending\" | \"scanned\" | \"authorized\" | \"expired\" | \"error\";\n message?: string;\n nextPollMs?: number;\n accountId?: string | null;\n notes?: string[];\n pluginConfig?: Record<string, unknown>;\n};\n\ntype WeixinLoginSession = {\n currentConfig: WeixinChannelConfig;\n requestedAccountId?: string | null;\n baseUrl: string;\n qrCode: string;\n expiresAtMs: number;\n};\n\ntype WeixinLoginServiceDeps = {\n api?: WeixinApiClient;\n store?: WeixinAccountStore;\n};\n\nconst WEIXIN_LOGIN_TIMEOUT_MS = 8 * 60_000;\nconst WEIXIN_AUTH_POLL_INTERVAL_MS = 2_000;\nconst WEIXIN_AUTH_STATUS_TIMEOUT_MS = 5_000;\n\nasync function sleep(ms: number): Promise<void> {\n await new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));\n}\n\nfunction resolveLoginBaseUrl(params: WeixinLoginParams, currentConfig: WeixinChannelConfig): string {\n return params.baseUrl?.trim() || currentConfig.baseUrl || DEFAULT_WEIXIN_BASE_URL;\n}\n\nfunction normalizeQrStatus(status: string | undefined): \"pending\" | \"scanned\" | \"authorized\" | \"expired\" {\n const normalized = status?.trim().toLowerCase() ?? \"\";\n if (normalized === \"scaned\" || normalized === \"scanned\") {\n return \"scanned\";\n }\n if (normalized === \"confirmed\" || normalized === \"authorized\" || normalized === \"success\") {\n return \"authorized\";\n }\n if (normalized === \"expired\" || normalized === \"timeout\") {\n return \"expired\";\n }\n return \"pending\";\n}\n\nfunction hasAuthorizedCredentials(status: WeixinQrStatusResponse): boolean {\n return Boolean(status.bot_token?.trim() && status.ilink_bot_id?.trim());\n}\n\nexport class WeixinLoginService {\n private readonly api: WeixinApiClient;\n private readonly store: WeixinAccountStore;\n private readonly sessions = new Map<string, WeixinLoginSession>();\n\n constructor(deps: WeixinLoginServiceDeps = {}) {\n this.api = deps.api ?? new HttpWeixinApiClient();\n this.store = deps.store ?? new FileWeixinAccountStore();\n }\n\n readonly start = async (params: WeixinLoginParams): Promise<WeixinAuthStartResult> => {\n this.cleanupExpiredSessions();\n const currentConfig = normalizeWeixinChannelConfig(params.pluginConfig);\n const baseUrl = resolveLoginBaseUrl(params, currentConfig);\n const qrCode = await this.api.fetchQrCode({ baseUrl });\n const qrCodeUrl = qrCode.qrcode_img_content?.trim();\n const qrCodeValue = qrCode.qrcode?.trim();\n\n if (!qrCodeUrl || !qrCodeValue) {\n throw new Error(\"weixin login failed: QR code is unavailable\");\n }\n\n const sessionId = randomUUID();\n const expiresAtMs = Date.now() + WEIXIN_LOGIN_TIMEOUT_MS;\n this.sessions.set(sessionId, {\n currentConfig,\n requestedAccountId: params.requestedAccountId,\n baseUrl,\n qrCode: qrCodeValue,\n expiresAtMs,\n });\n\n return {\n channel: WEIXIN_CHANNEL_ID,\n kind: \"qr_code\",\n sessionId,\n qrCode: qrCodeValue,\n qrCodeUrl,\n expiresAt: new Date(expiresAtMs).toISOString(),\n intervalMs: WEIXIN_AUTH_POLL_INTERVAL_MS,\n note: \"请使用微信扫码,并在手机上确认登录。\",\n };\n };\n\n readonly poll = async ({ sessionId }: { sessionId: string }): Promise<WeixinAuthPollResult | null> => {\n this.cleanupExpiredSessions();\n const session = this.sessions.get(sessionId);\n if (!session) {\n return null;\n }\n if (session.expiresAtMs <= Date.now()) {\n this.sessions.delete(sessionId);\n return {\n channel: WEIXIN_CHANNEL_ID,\n status: \"expired\",\n message: \"二维码已过期,请重新扫码。\",\n };\n }\n\n try {\n const status = await this.api.fetchQrStatus({\n baseUrl: session.baseUrl,\n qrcode: session.qrCode,\n timeoutMs: WEIXIN_AUTH_STATUS_TIMEOUT_MS,\n });\n const normalizedStatus = hasAuthorizedCredentials(status)\n ? \"authorized\"\n : normalizeQrStatus(status.status);\n\n if (normalizedStatus === \"scanned\") {\n return {\n channel: WEIXIN_CHANNEL_ID,\n status: \"scanned\",\n message: \"二维码已扫码,请在微信中确认登录。\",\n nextPollMs: WEIXIN_AUTH_POLL_INTERVAL_MS,\n };\n }\n\n if (normalizedStatus === \"authorized\") {\n const result = this.confirmLogin(session, status);\n this.sessions.delete(sessionId);\n return {\n channel: WEIXIN_CHANNEL_ID,\n status: \"authorized\",\n message: \"微信已连接。\",\n nextPollMs: 0,\n accountId: result.accountId,\n notes: result.notes,\n pluginConfig: result.pluginConfig,\n };\n }\n\n if (normalizedStatus === \"expired\") {\n this.sessions.delete(sessionId);\n return {\n channel: WEIXIN_CHANNEL_ID,\n status: \"expired\",\n message: \"二维码已过期,请重新扫码。\",\n };\n }\n\n return {\n channel: WEIXIN_CHANNEL_ID,\n status: \"pending\",\n nextPollMs: WEIXIN_AUTH_POLL_INTERVAL_MS,\n };\n } catch (error) {\n return {\n channel: WEIXIN_CHANNEL_ID,\n status: \"error\",\n message: error instanceof Error ? error.message : String(error),\n };\n }\n };\n\n readonly login = async (\n params: WeixinLoginParams,\n ): Promise<{ pluginConfig: Record<string, unknown>; accountId?: string | null; notes?: string[] }> => {\n const started = await this.start(params);\n console.log(\"使用微信扫描以下二维码链接完成连接:\");\n console.log(started.qrCodeUrl);\n console.log(\"\");\n console.log(\"等待扫码确认...\");\n\n let seenScanned = false;\n while (Date.now() < new Date(started.expiresAt).getTime()) {\n const status = await this.poll({ sessionId: started.sessionId });\n if (!status) {\n throw new Error(\"weixin login failed: auth session not found\");\n }\n if (status.status === \"scanned\" && !seenScanned) {\n console.log(status.message ?? \"二维码已扫码,请在微信中确认登录。\");\n seenScanned = true;\n }\n if (status.status === \"authorized\") {\n return {\n pluginConfig: status.pluginConfig ?? {},\n accountId: status.accountId,\n notes: status.notes,\n };\n }\n if (status.status === \"expired\") {\n throw new Error(status.message ?? \"weixin login failed: QR code expired, please retry\");\n }\n if (status.status === \"error\") {\n throw new Error(status.message ?? \"weixin login failed\");\n }\n if (params.verbose) {\n process.stdout.write(\".\");\n }\n await sleep(status.nextPollMs ?? WEIXIN_AUTH_POLL_INTERVAL_MS);\n }\n\n throw new Error(\"weixin login timed out\");\n };\n\n private readonly confirmLogin = (\n session: WeixinLoginSession,\n status: WeixinQrStatusResponse,\n ): { pluginConfig: Record<string, unknown>; accountId: string; notes: string[] } => {\n const token = status.bot_token?.trim();\n const accountId = status.ilink_bot_id?.trim() || session.requestedAccountId?.trim();\n const baseUrl = status.baseurl?.trim() || session.baseUrl;\n const userId = status.ilink_user_id?.trim() || undefined;\n\n if (!token || !accountId) {\n throw new Error(\"weixin login failed: missing bot token or account id\");\n }\n\n const replacementAccountIds = this.resolveReplacementAccountIds({\n currentConfig: session.currentConfig,\n requestedAccountId: session.requestedAccountId,\n accountId,\n userId,\n });\n for (const replacementAccountId of replacementAccountIds) {\n this.store.deleteAccount(replacementAccountId);\n this.store.deleteCursor(replacementAccountId);\n }\n\n this.store.saveAccount({\n accountId,\n token,\n baseUrl,\n userId,\n savedAt: new Date().toISOString(),\n });\n\n const notes = [\n ...(session.requestedAccountId?.trim() && session.requestedAccountId.trim() !== accountId\n ? [`Weixin account resolved to ${accountId}.`]\n : []),\n ...replacementAccountIds.map((replacementAccountId) => `Replaced previous Weixin account: ${replacementAccountId}`),\n ...(userId ? [`Authorized initial user: ${userId}`] : []),\n ];\n\n return {\n accountId,\n notes,\n pluginConfig: buildLoggedInWeixinChannelConfig({\n config: session.currentConfig,\n accountId,\n baseUrl,\n allowUserId: userId,\n replaceAccountIds: replacementAccountIds,\n }) as Record<string, unknown>,\n };\n };\n\n private readonly resolveReplacementAccountIds = (params: {\n currentConfig: WeixinChannelConfig;\n requestedAccountId?: string | null;\n accountId: string;\n userId?: string;\n }): string[] => {\n const { accountId, currentConfig, requestedAccountId: requestedAccountIdRaw, userId } = params;\n const replacementIds = new Set<string>();\n const requestedAccountId = requestedAccountIdRaw?.trim();\n const defaultAccountId = currentConfig.defaultAccountId?.trim();\n\n if (requestedAccountId && requestedAccountId !== accountId) {\n replacementIds.add(requestedAccountId);\n } else if (!requestedAccountId && defaultAccountId && defaultAccountId !== accountId) {\n replacementIds.add(defaultAccountId);\n }\n\n if (userId) {\n for (const [candidateAccountId, accountConfig] of Object.entries(currentConfig.accounts ?? {})) {\n if (candidateAccountId !== accountId && accountConfig.allowFrom?.includes(userId)) {\n replacementIds.add(candidateAccountId);\n }\n }\n for (const candidateAccountId of this.store.listAccountIds()) {\n if (candidateAccountId !== accountId && this.store.loadAccount(candidateAccountId)?.userId === userId) {\n replacementIds.add(candidateAccountId);\n }\n }\n }\n\n return [...replacementIds];\n };\n\n private readonly cleanupExpiredSessions = (now = Date.now()): void => {\n for (const [sessionId, session] of this.sessions.entries()) {\n if (session.expiresAtMs <= now) {\n this.sessions.delete(sessionId);\n }\n }\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"weixin-media-part-reader.service.js","names":[],"sources":["../../src/services/weixin-media-part-reader.service.ts"],"sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport { basename, extname } from \"node:path\";\nimport type { ChatTarget } from \"@nextclaw/ncp-toolkit\";\nimport type { NcpMessagePart } from \"@nextclaw/ncp\";\n\nexport type ResolvedWeixinMediaPart = {\n bytes: Uint8Array;\n fileName: string;\n mimeType?: string;\n isImage: boolean;\n imageWidth?: number;\n imageHeight?: number;\n};\n\nconst IMAGE_FILE_EXTENSIONS = new Set([\n \".avif\",\n \".bmp\",\n \".gif\",\n \".heic\",\n \".jpeg\",\n \".jpg\",\n \".png\",\n \".svg\",\n \".webp\",\n]);\n\nfunction readString(value: unknown): string | undefined {\n if (typeof value !== \"string\") {\n return undefined;\n }\n const trimmed = value.trim();\n return trimmed || undefined;\n}\n\nfunction normalizeMimeType(value: string | undefined): string | undefined {\n if (!value) {\n return undefined;\n }\n const [mimeType] = value.split(\";\", 1);\n const normalized = mimeType?.trim().toLowerCase();\n return normalized || undefined;\n}\n\nfunction mimeTypeToExtension(mimeType: string | undefined): string {\n switch (mimeType) {\n case \"image/png\":\n return \".png\";\n case \"image/jpeg\":\n return \".jpg\";\n case \"image/gif\":\n return \".gif\";\n case \"image/webp\":\n return \".webp\";\n case \"image/svg+xml\":\n return \".svg\";\n case \"application/pdf\":\n return \".pdf\";\n default:\n return \".bin\";\n }\n}\n\nfunction isImageMimeType(mimeType: string | undefined): boolean {\n return Boolean(mimeType?.startsWith(\"image/\"));\n}\n\nfunction readPngDimensions(bytes: Uint8Array): { width: number; height: number } | null {\n if (bytes.byteLength < 24) {\n return null;\n }\n const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n const signature = view.getUint32(0);\n const ihdr = view.getUint32(12);\n if (signature !== 0x89504e47 || ihdr !== 0x49484452) {\n return null;\n }\n return {\n width: view.getUint32(16),\n height: view.getUint32(20),\n };\n}\n\nfunction readGifDimensions(bytes: Uint8Array): { width: number; height: number } | null {\n if (bytes.byteLength < 10) {\n return null;\n }\n const header = Buffer.from(bytes.subarray(0, 6)).toString(\"ascii\");\n if (header !== \"GIF87a\" && header !== \"GIF89a\") {\n return null;\n }\n const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n return {\n width: view.getUint16(6, true),\n height: view.getUint16(8, true),\n };\n}\n\nfunction readJpegDimensions(bytes: Uint8Array): { width: number; height: number } | null {\n if (bytes.byteLength < 4 || bytes[0] !== 0xff || bytes[1] !== 0xd8) {\n return null;\n }\n let offset = 2;\n while (offset + 9 < bytes.byteLength) {\n if (bytes[offset] !== 0xff) {\n offset += 1;\n continue;\n }\n const marker = bytes[offset + 1];\n offset += 2;\n if (marker === 0xd8 || marker === 0xd9) {\n continue;\n }\n if (offset + 2 > bytes.byteLength) {\n return null;\n }\n const length = (bytes[offset] << 8) | bytes[offset + 1];\n if (length < 2 || offset + length > bytes.byteLength) {\n return null;\n }\n const isSofMarker =\n (marker >= 0xc0 && marker <= 0xc3) ||\n (marker >= 0xc5 && marker <= 0xc7) ||\n (marker >= 0xc9 && marker <= 0xcb) ||\n (marker >= 0xcd && marker <= 0xcf);\n if (isSofMarker) {\n return {\n height: (bytes[offset + 3] << 8) | bytes[offset + 4],\n width: (bytes[offset + 5] << 8) | bytes[offset + 6],\n };\n }\n offset += length;\n }\n return null;\n}\n\nfunction readWebpDimensions(bytes: Uint8Array): { width: number; height: number } | null {\n if (bytes.byteLength < 30) {\n return null;\n }\n const header = Buffer.from(bytes.subarray(0, 4)).toString(\"ascii\");\n const webp = Buffer.from(bytes.subarray(8, 12)).toString(\"ascii\");\n if (header !== \"RIFF\" || webp !== \"WEBP\") {\n return null;\n }\n const chunk = Buffer.from(bytes.subarray(12, 16)).toString(\"ascii\");\n const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n if (chunk === \"VP8X\" && bytes.byteLength >= 30) {\n const width = 1 + (bytes[24] | (bytes[25] << 8) | (bytes[26] << 16));\n const height = 1 + (bytes[27] | (bytes[28] << 8) | (bytes[29] << 16));\n return { width, height };\n }\n if (chunk === \"VP8 \" && bytes.byteLength >= 30) {\n return {\n width: view.getUint16(26, true) & 0x3fff,\n height: view.getUint16(28, true) & 0x3fff,\n };\n }\n if (chunk === \"VP8L\" && bytes.byteLength >= 25) {\n const bits =\n bytes[21] |\n (bytes[22] << 8) |\n (bytes[23] << 16) |\n (bytes[24] << 24);\n return {\n width: (bits & 0x3fff) + 1,\n height: ((bits >> 14) & 0x3fff) + 1,\n };\n }\n return null;\n}\n\nfunction readImageDimensions(bytes: Uint8Array): { width: number; height: number } | null {\n return (\n readPngDimensions(bytes) ??\n readGifDimensions(bytes) ??\n readJpegDimensions(bytes) ??\n readWebpDimensions(bytes)\n );\n}\n\nfunction readFileNameFromUrl(url: string): string | undefined {\n try {\n const parsed = new URL(url);\n const name = basename(parsed.pathname).trim();\n return name || undefined;\n } catch {\n const name = basename(url).trim();\n return name || undefined;\n }\n}\n\nfunction resolveFetchUrl(url: string): string {\n try {\n return new URL(url).toString();\n } catch {\n const endpoint = process.env.NEXTCLAW_EXTENSION_ENDPOINT?.trim();\n if (!endpoint) {\n return url;\n }\n return new URL(url, endpoint.endsWith(\"/\") ? endpoint : `${endpoint}/`).toString();\n }\n}\n\nexport class WeixinMediaPartReader {\n read = async (\n target: ChatTarget,\n part: Extract<NcpMessagePart, { type: \"file\" }>,\n ): Promise<ResolvedWeixinMediaPart> => {\n if (part.contentBase64) {\n const bytes = Buffer.from(part.contentBase64, \"base64\");\n const mimeType = normalizeMimeType(part.mimeType);\n const fileName = this.resolveFileName(part, mimeType);\n const imageDimensions = this.resolveImageDimensions(bytes, fileName, mimeType);\n return {\n bytes,\n fileName,\n mimeType,\n isImage: this.isImageFile(fileName, mimeType),\n imageWidth: imageDimensions?.width,\n imageHeight: imageDimensions?.height,\n };\n }\n\n if (part.assetUri) {\n const contentPath = target.resolveAssetContentPath?.(part.assetUri)?.trim();\n if (contentPath) {\n const bytes = await readFile(contentPath);\n const mimeType = normalizeMimeType(part.mimeType);\n const fileName = this.resolveFileName(\n part,\n mimeType,\n basename(contentPath),\n );\n const imageDimensions = this.resolveImageDimensions(bytes, fileName, mimeType);\n return {\n bytes,\n fileName,\n mimeType,\n isImage: this.isImageFile(fileName, mimeType),\n imageWidth: imageDimensions?.width,\n imageHeight: imageDimensions?.height,\n };\n }\n if (!part.url) {\n throw new Error(\n `weixin send failed: asset \"${part.assetUri}\" is not readable`,\n );\n }\n }\n\n if (part.url) {\n const response = await fetch(resolveFetchUrl(part.url));\n if (!response.ok) {\n throw new Error(\n `weixin send failed: unable to download ${part.url} (${response.status})`,\n );\n }\n const bytes = new Uint8Array(await response.arrayBuffer());\n const mimeType =\n normalizeMimeType(part.mimeType) ??\n normalizeMimeType(response.headers.get(\"content-type\") ?? undefined);\n const fileName = this.resolveFileName(\n part,\n mimeType,\n readFileNameFromUrl(part.url),\n );\n const imageDimensions = this.resolveImageDimensions(bytes, fileName, mimeType);\n return {\n bytes,\n fileName,\n mimeType,\n isImage: this.isImageFile(fileName, mimeType),\n imageWidth: imageDimensions?.width,\n imageHeight: imageDimensions?.height,\n };\n }\n\n throw new Error(\"weixin send failed: file part is missing content\");\n };\n\n private resolveFileName = (\n part: Extract<NcpMessagePart, { type: \"file\" }>,\n mimeType?: string,\n fallbackName?: string,\n ): string => {\n const explicitName = readString(part.name);\n if (explicitName) {\n return explicitName;\n }\n if (fallbackName) {\n return fallbackName;\n }\n return `attachment${mimeTypeToExtension(mimeType)}`;\n };\n\n private isImageFile = (fileName: string, mimeType?: string): boolean => {\n if (isImageMimeType(mimeType)) {\n return true;\n }\n return IMAGE_FILE_EXTENSIONS.has(extname(fileName).toLowerCase());\n };\n\n private resolveImageDimensions = (\n bytes: Uint8Array,\n fileName: string,\n mimeType?: string,\n ): { width: number; height: number } | null => {\n if (!this.isImageFile(fileName, mimeType)) {\n return null;\n }\n return readImageDimensions(bytes);\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"weixin-reply-chat.service.js","names":[],"sources":["../../src/services/weixin-reply-chat.service.ts"],"sourcesContent":["import { NcpEventType, type NcpEndpointEvent, type NcpMessagePart } from \"@nextclaw/ncp\";\nimport { type Chat, type ChatTarget } from \"@nextclaw/ncp-toolkit\";\nimport { sendWeixinFileMessage, sendWeixinImageMessage } from \"../utils/weixin-media.utils.js\";\nimport { WeixinMediaPartReader } from \"./weixin-media-part-reader.service.js\";\nimport type { WeixinTypingController } from \"./weixin-typing-controller.service.js\";\nimport type { WeixinRuntimeAccount } from \"../types/weixin-extension.types.js\";\n\nexport const TERMINAL_NCP_EVENT_TYPES = new Set<NcpEndpointEvent[\"type\"]>([\n NcpEventType.MessageCompleted,\n NcpEventType.MessageFailed,\n NcpEventType.RunFinished,\n NcpEventType.RunError,\n]);\n\nexport type WeixinReplySession = {\n queue: NcpEventQueue;\n consuming: Promise<void>;\n};\n\nexport class NcpEventQueue implements AsyncIterable<NcpEndpointEvent> {\n private readonly events: NcpEndpointEvent[] = [];\n private waiting: ((result: IteratorResult<NcpEndpointEvent>) => void) | null = null;\n private closed = false;\n\n readonly push = (event: NcpEndpointEvent): void => {\n if (this.closed) {\n return;\n }\n const waiting = this.waiting;\n if (waiting) {\n this.waiting = null;\n waiting({ value: event, done: false });\n return;\n }\n this.events.push(event);\n };\n\n readonly close = (): void => {\n this.closed = true;\n const waiting = this.waiting;\n if (waiting) {\n this.waiting = null;\n waiting({ value: undefined, done: true });\n }\n };\n\n [Symbol.asyncIterator] = (): AsyncIterator<NcpEndpointEvent> => ({\n next: async () => {\n const event = this.events.shift();\n if (event) {\n return { value: event, done: false };\n }\n if (this.closed) {\n return { value: undefined, done: true };\n }\n return await new Promise<IteratorResult<NcpEndpointEvent>>((resolve) => {\n this.waiting = resolve;\n });\n },\n });\n}\n\nfunction renderPartText(part: NcpMessagePart): string {\n switch (part.type) {\n case \"text\":\n case \"rich-text\":\n return part.text;\n case \"source\":\n return [part.title ?? \"\", part.url ?? \"\", part.snippet ?? \"\"].filter(Boolean).join(\"\\n\");\n case \"card\": {\n const title = typeof part.payload.title === \"string\" ? part.payload.title.trim() : \"\";\n return title || JSON.stringify(part.payload);\n }\n case \"action\":\n return part.label.trim();\n case \"step-start\":\n return part.title?.trim() ?? \"\";\n default:\n return \"\";\n }\n}\n\nexport class WeixinReplyChat implements Chat {\n private readonly mediaPartReader = new WeixinMediaPartReader();\n\n constructor(\n private readonly deps: {\n resolveAccount: (target: ChatTarget) => WeixinRuntimeAccount;\n resolveContextToken: (target: ChatTarget, accountId: string) => string | undefined;\n sendText: (params: {\n account: WeixinRuntimeAccount;\n conversationId: string;\n text: string;\n contextToken?: string;\n }) => Promise<void>;\n typingController: WeixinTypingController;\n },\n ) {}\n\n readonly startTyping = async (target: ChatTarget): Promise<void> => {\n const account = this.deps.resolveAccount(target);\n const contextToken = this.deps.resolveContextToken(target, account.accountId);\n if (!contextToken) {\n return;\n }\n await this.deps.typingController.start({\n accountId: account.accountId,\n userId: target.conversationId,\n contextToken,\n baseUrl: account.baseUrl,\n token: account.token,\n });\n };\n\n readonly stopTyping = async (target: ChatTarget): Promise<void> => {\n const account = this.deps.resolveAccount(target);\n await this.deps.typingController.stop({\n accountId: account.accountId,\n userId: target.conversationId,\n });\n };\n\n readonly sendError = async (target: ChatTarget, message: string): Promise<void> => {\n if (message.trim()) {\n await this.sendText(target, message);\n }\n };\n\n readonly sendPart = async (target: ChatTarget, part: NcpMessagePart): Promise<void> => {\n if (part.type === \"file\") {\n await this.sendFilePart(target, part);\n return;\n }\n const text = renderPartText(part);\n if (text.trim()) {\n await this.sendText(target, text);\n }\n };\n\n private readonly sendText = async (target: ChatTarget, text: string): Promise<void> => {\n const account = this.deps.resolveAccount(target);\n await this.deps.sendText({\n account,\n conversationId: target.conversationId,\n text,\n contextToken: this.deps.resolveContextToken(target, account.accountId),\n });\n };\n\n private readonly sendFilePart = async (\n target: ChatTarget,\n part: Extract<NcpMessagePart, { type: \"file\" }>,\n ): Promise<void> => {\n const media = await this.mediaPartReader.read(target, part);\n const account = this.deps.resolveAccount(target);\n const contextToken = this.deps.resolveContextToken(target, account.accountId);\n if (media.isImage) {\n await sendWeixinImageMessage({\n baseUrl: account.baseUrl,\n token: account.token,\n toUserId: target.conversationId,\n bytes: media.bytes,\n width: media.imageWidth,\n height: media.imageHeight,\n contextToken,\n });\n return;\n }\n await sendWeixinFileMessage({\n baseUrl: account.baseUrl,\n token: account.token,\n toUserId: target.conversationId,\n fileName: media.fileName,\n bytes: media.bytes,\n contextToken,\n });\n };\n}\n"],"mappings":";;;;;AAOA,MAAa,2BAA2B,IAAI,IAA8B;CACxE,aAAa;CACb,aAAa;CACb,aAAa;CACb,aAAa;CACd,CAAC;AAOF,IAAa,gBAAb,MAAsE;CACpE,SAA8C,EAAE;CAChD,UAA+E;CAC/E,SAAiB;CAEjB,QAAiB,UAAkC;AACjD,MAAI,KAAK,OACP;EAEF,MAAM,UAAU,KAAK;AACrB,MAAI,SAAS;AACX,QAAK,UAAU;AACf,WAAQ;IAAE,OAAO;IAAO,MAAM;IAAO,CAAC;AACtC;;AAEF,OAAK,OAAO,KAAK,MAAM;;CAGzB,cAA6B;AAC3B,OAAK,SAAS;EACd,MAAM,UAAU,KAAK;AACrB,MAAI,SAAS;AACX,QAAK,UAAU;AACf,WAAQ;IAAE,OAAO,KAAA;IAAW,MAAM;IAAM,CAAC;;;CAI7C,CAAC,OAAO,wBAAyD,EAC/D,MAAM,YAAY;EAChB,MAAM,QAAQ,KAAK,OAAO,OAAO;AACjC,MAAI,MACF,QAAO;GAAE,OAAO;GAAO,MAAM;GAAO;AAEtC,MAAI,KAAK,OACP,QAAO;GAAE,OAAO,KAAA;GAAW,MAAM;GAAM;AAEzC,SAAO,MAAM,IAAI,SAA2C,YAAY;AACtE,QAAK,UAAU;IACf;IAEL;;AAGH,SAAS,eAAe,MAA8B;AACpD,SAAQ,KAAK,MAAb;EACE,KAAK;EACL,KAAK,YACH,QAAO,KAAK;EACd,KAAK,SACH,QAAO;GAAC,KAAK,SAAS;GAAI,KAAK,OAAO;GAAI,KAAK,WAAW;GAAG,CAAC,OAAO,QAAQ,CAAC,KAAK,KAAK;EAC1F,KAAK,OAEH,SADc,OAAO,KAAK,QAAQ,UAAU,WAAW,KAAK,QAAQ,MAAM,MAAM,GAAG,OACnE,KAAK,UAAU,KAAK,QAAQ;EAE9C,KAAK,SACH,QAAO,KAAK,MAAM,MAAM;EAC1B,KAAK,aACH,QAAO,KAAK,OAAO,MAAM,IAAI;EAC/B,QACE,QAAO;;;AAIb,IAAa,kBAAb,MAA6C;CAC3C,kBAAmC,IAAI,uBAAuB;CAE9D,YACE,MAWA;AAXiB,OAAA,OAAA;;CAanB,cAAuB,OAAO,WAAsC;EAClE,MAAM,UAAU,KAAK,KAAK,eAAe,OAAO;EAChD,MAAM,eAAe,KAAK,KAAK,oBAAoB,QAAQ,QAAQ,UAAU;AAC7E,MAAI,CAAC,aACH;AAEF,QAAM,KAAK,KAAK,iBAAiB,MAAM;GACrC,WAAW,QAAQ;GACnB,QAAQ,OAAO;GACf;GACA,SAAS,QAAQ;GACjB,OAAO,QAAQ;GAChB,CAAC;;CAGJ,aAAsB,OAAO,WAAsC;EACjE,MAAM,UAAU,KAAK,KAAK,eAAe,OAAO;AAChD,QAAM,KAAK,KAAK,iBAAiB,KAAK;GACpC,WAAW,QAAQ;GACnB,QAAQ,OAAO;GAChB,CAAC;;CAGJ,YAAqB,OAAO,QAAoB,YAAmC;AACjF,MAAI,QAAQ,MAAM,CAChB,OAAM,KAAK,SAAS,QAAQ,QAAQ;;CAIxC,WAAoB,OAAO,QAAoB,SAAwC;AACrF,MAAI,KAAK,SAAS,QAAQ;AACxB,SAAM,KAAK,aAAa,QAAQ,KAAK;AACrC;;EAEF,MAAM,OAAO,eAAe,KAAK;AACjC,MAAI,KAAK,MAAM,CACb,OAAM,KAAK,SAAS,QAAQ,KAAK;;CAIrC,WAA4B,OAAO,QAAoB,SAAgC;EACrF,MAAM,UAAU,KAAK,KAAK,eAAe,OAAO;AAChD,QAAM,KAAK,KAAK,SAAS;GACvB;GACA,gBAAgB,OAAO;GACvB;GACA,cAAc,KAAK,KAAK,oBAAoB,QAAQ,QAAQ,UAAU;GACvE,CAAC;;CAGJ,eAAgC,OAC9B,QACA,SACkB;EAClB,MAAM,QAAQ,MAAM,KAAK,gBAAgB,KAAK,QAAQ,KAAK;EAC3D,MAAM,UAAU,KAAK,KAAK,eAAe,OAAO;EAChD,MAAM,eAAe,KAAK,KAAK,oBAAoB,QAAQ,QAAQ,UAAU;AAC7E,MAAI,MAAM,SAAS;AACjB,SAAM,uBAAuB;IAC3B,SAAS,QAAQ;IACjB,OAAO,QAAQ;IACf,UAAU,OAAO;IACjB,OAAO,MAAM;IACb,OAAO,MAAM;IACb,QAAQ,MAAM;IACd;IACD,CAAC;AACF;;AAEF,QAAM,sBAAsB;GAC1B,SAAS,QAAQ;GACjB,OAAO,QAAQ;GACf,UAAU,OAAO;GACjB,UAAU,MAAM;GAChB,OAAO,MAAM;GACb;GACD,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"weixin-typing-controller.service.js","names":[],"sources":["../../src/services/weixin-typing-controller.service.ts"],"sourcesContent":["type WeixinTypingRuntime = {\n accountId: string;\n userId: string;\n contextToken: string;\n baseUrl: string;\n token: string;\n};\n\ntype WeixinTypingControllerOptions = {\n heartbeatMs?: number;\n ticketTtlMs?: number;\n fetchTicket: (runtime: WeixinTypingRuntime) => Promise<string | undefined>;\n sendTyping: (params: WeixinTypingRuntime & { ticket: string; status: 1 | 2 }) => Promise<void>;\n};\n\ntype TicketCacheEntry = {\n ticket: string;\n expiresAtMs: number;\n};\n\ntype ActiveSession = {\n heartbeat: NodeJS.Timeout;\n ticket: string;\n runtime: WeixinTypingRuntime;\n sequence: number;\n};\n\nfunction buildTypingKey(accountId: string, userId: string): string {\n return `${accountId}:${userId}`;\n}\n\nexport class WeixinTypingController {\n private readonly heartbeatMs: number;\n private readonly ticketTtlMs: number;\n private readonly ticketCache = new Map<string, TicketCacheEntry>();\n private readonly activeSessions = new Map<string, ActiveSession>();\n private readonly sessionSequences = new Map<string, number>();\n private readonly fetchTicket: WeixinTypingControllerOptions[\"fetchTicket\"];\n private readonly sendTyping: WeixinTypingControllerOptions[\"sendTyping\"];\n\n constructor(options: WeixinTypingControllerOptions) {\n const { fetchTicket, heartbeatMs, sendTyping, ticketTtlMs } = options;\n this.heartbeatMs = Math.max(1_000, Math.trunc(heartbeatMs ?? 5_000));\n this.ticketTtlMs = Math.max(this.heartbeatMs, Math.trunc(ticketTtlMs ?? 24 * 60 * 60 * 1_000));\n this.fetchTicket = fetchTicket;\n this.sendTyping = sendTyping;\n }\n\n start = async (runtime: WeixinTypingRuntime): Promise<void> => {\n const key = buildTypingKey(runtime.accountId, runtime.userId);\n const sequence = this.bumpSequence(key);\n await this.clearActiveSession({\n accountId: runtime.accountId,\n userId: runtime.userId,\n sendCancel: false,\n });\n const ticket = await this.getTicket(runtime);\n if (!ticket || this.sessionSequences.get(key) !== sequence) {\n return;\n }\n\n await this.sendTypingSafe({\n ...runtime,\n ticket,\n status: 1,\n });\n\n if (this.sessionSequences.get(key) !== sequence) {\n return;\n }\n\n const heartbeat = setInterval(() => {\n const active = this.activeSessions.get(key);\n if (!active || active.sequence !== sequence) {\n return;\n }\n void this.sendTypingSafe({\n ...active.runtime,\n ticket: active.ticket,\n status: 1,\n });\n }, this.heartbeatMs);\n\n this.activeSessions.set(key, {\n heartbeat,\n ticket,\n runtime,\n sequence,\n });\n };\n\n stop = async (params: {\n accountId: string;\n userId: string;\n sendCancel?: boolean;\n }): Promise<void> => {\n const key = buildTypingKey(params.accountId, params.userId);\n this.bumpSequence(key);\n await this.clearActiveSession(params);\n };\n\n stopAll = async (): Promise<void> => {\n const sessions = Array.from(this.activeSessions.values());\n this.activeSessions.clear();\n for (const session of sessions) {\n this.bumpSequence(buildTypingKey(session.runtime.accountId, session.runtime.userId));\n clearInterval(session.heartbeat);\n }\n await Promise.allSettled(\n sessions.map(async (session) => {\n await this.sendTypingSafe({\n ...session.runtime,\n ticket: session.ticket,\n status: 2,\n });\n }),\n );\n };\n\n private bumpSequence = (key: string): number => {\n const next = (this.sessionSequences.get(key) ?? 0) + 1;\n this.sessionSequences.set(key, next);\n return next;\n };\n\n private clearActiveSession = async (params: {\n accountId: string;\n userId: string;\n sendCancel?: boolean;\n }): Promise<void> => {\n const key = buildTypingKey(params.accountId, params.userId);\n const active = this.activeSessions.get(key);\n if (!active) {\n return;\n }\n clearInterval(active.heartbeat);\n this.activeSessions.delete(key);\n if (params.sendCancel === false) {\n return;\n }\n await this.sendTypingSafe({\n ...active.runtime,\n ticket: active.ticket,\n status: 2,\n });\n };\n\n private getTicket = async (runtime: WeixinTypingRuntime): Promise<string | undefined> => {\n const key = buildTypingKey(runtime.accountId, runtime.userId);\n const cached = this.ticketCache.get(key);\n if (cached && cached.expiresAtMs > Date.now()) {\n return cached.ticket;\n }\n\n const ticket = (await this.fetchTicket(runtime))?.trim();\n if (!ticket) {\n return undefined;\n }\n\n this.ticketCache.set(key, {\n ticket,\n expiresAtMs: Date.now() + this.ticketTtlMs,\n });\n return ticket;\n };\n\n private sendTypingSafe = async (params: WeixinTypingRuntime & { ticket: string; status: 1 | 2 }): Promise<void> => {\n try {\n await this.sendTyping(params);\n } catch {\n // Typing is best-effort and must never block the main reply path.\n }\n };\n}\n\nexport type { WeixinTypingRuntime };\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"weixin-account.store.d.ts","names":[],"sources":["../../src/stores/weixin-account.store.ts"],"mappings":";KAIY,mBAAA;EACV,SAAA;EACA,KAAA;EACA,OAAA;EACA,MAAA;EACA,OAAA;AAAA;AAAA,KAGU,kBAAA;EACV,cAAA;EACA,WAAA,GAAc,SAAA,aAAsB,mBAAA;EACpC,WAAA,GAAc,OAAA,EAAS,mBAAA;EACvB,aAAA,GAAgB,SAAA;EAChB,UAAA,GAAa,SAAA;EACb,UAAA,GAAa,SAAA,UAAmB,MAAA;EAChC,YAAA,GAAe,SAAA;AAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"weixin-account.store.js","names":[],"sources":["../../src/stores/weixin-account.store.ts"],"sourcesContent":["import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join, resolve } from \"node:path\";\n\nexport type StoredWeixinAccount = {\n accountId: string;\n token: string;\n baseUrl?: string;\n userId?: string;\n savedAt?: string;\n};\n\nexport type WeixinAccountStore = {\n listAccountIds: () => string[];\n loadAccount: (accountId: string) => StoredWeixinAccount | null;\n saveAccount: (account: StoredWeixinAccount) => void;\n deleteAccount: (accountId: string) => void;\n loadCursor: (accountId: string) => string | undefined;\n saveCursor: (accountId: string, cursor: string | undefined) => void;\n deleteCursor: (accountId: string) => void;\n};\n\nconst NEXTCLAW_HOME_ENV_KEY = \"NEXTCLAW_HOME\";\nconst NEXTCLAW_DEFAULT_HOME_DIR = \".nextclaw\";\n\nfunction resolveNextclawHome(): string {\n const override = process.env[NEXTCLAW_HOME_ENV_KEY]?.trim();\n return resolve(override || join(homedir(), NEXTCLAW_DEFAULT_HOME_DIR));\n}\n\nfunction resolveWeixinDataDir(): string {\n return join(resolveNextclawHome(), \"channels\", \"weixin\");\n}\n\nfunction resolveAccountsDir(): string {\n return join(resolveWeixinDataDir(), \"accounts\");\n}\n\nfunction resolveCursorsDir(): string {\n return join(resolveWeixinDataDir(), \"cursors\");\n}\n\nfunction toFileName(accountId: string): string {\n return `${encodeURIComponent(accountId)}.json`;\n}\n\nfunction readJsonFile<T>(filePath: string): T | null {\n if (!existsSync(filePath)) {\n return null;\n }\n try {\n return JSON.parse(readFileSync(filePath, \"utf-8\")) as T;\n } catch {\n return null;\n }\n}\n\nexport class FileWeixinAccountStore implements WeixinAccountStore {\n readonly listAccountIds = (): string[] => {\n if (!existsSync(resolveAccountsDir())) {\n return [];\n }\n return readdirSync(resolveAccountsDir())\n .filter((entry) => entry.endsWith(\".json\"))\n .map((entry) => decodeURIComponent(entry.slice(0, -5)))\n .filter(Boolean);\n };\n\n readonly loadAccount = (accountId: string): StoredWeixinAccount | null => {\n return readJsonFile<StoredWeixinAccount>(join(resolveAccountsDir(), toFileName(accountId)));\n };\n\n readonly saveAccount = (account: StoredWeixinAccount): void => {\n mkdirSync(resolveAccountsDir(), { recursive: true });\n writeFileSync(\n join(resolveAccountsDir(), toFileName(account.accountId)),\n JSON.stringify(account, null, 2),\n );\n };\n\n readonly deleteAccount = (accountId: string): void => {\n rmSync(join(resolveAccountsDir(), toFileName(accountId)), { force: true });\n };\n\n readonly loadCursor = (accountId: string): string | undefined => {\n const payload = readJsonFile<{ cursor?: string }>(join(resolveCursorsDir(), toFileName(accountId)));\n return payload?.cursor?.trim() || undefined;\n };\n\n readonly saveCursor = (accountId: string, cursor: string | undefined): void => {\n mkdirSync(resolveCursorsDir(), { recursive: true });\n writeFileSync(\n join(resolveCursorsDir(), toFileName(accountId)),\n JSON.stringify({ cursor: cursor ?? \"\" }, null, 2),\n );\n };\n\n readonly deleteCursor = (accountId: string): void => {\n rmSync(join(resolveCursorsDir(), toFileName(accountId)), { force: true });\n };\n}\n"],"mappings":";;;;AAsBA,MAAM,wBAAwB;AAC9B,MAAM,4BAA4B;AAElC,SAAS,sBAA8B;CACrC,MAAM,WAAW,QAAQ,IAAI,wBAAwB,MAAM;AAC3D,QAAO,QAAQ,YAAY,KAAK,SAAS,EAAE,0BAA0B,CAAC;;AAGxE,SAAS,uBAA+B;AACtC,QAAO,KAAK,qBAAqB,EAAE,YAAY,SAAS;;AAG1D,SAAS,qBAA6B;AACpC,QAAO,KAAK,sBAAsB,EAAE,WAAW;;AAGjD,SAAS,oBAA4B;AACnC,QAAO,KAAK,sBAAsB,EAAE,UAAU;;AAGhD,SAAS,WAAW,WAA2B;AAC7C,QAAO,GAAG,mBAAmB,UAAU,CAAC;;AAG1C,SAAS,aAAgB,UAA4B;AACnD,KAAI,CAAC,WAAW,SAAS,CACvB,QAAO;AAET,KAAI;AACF,SAAO,KAAK,MAAM,aAAa,UAAU,QAAQ,CAAC;SAC5C;AACN,SAAO;;;AAIX,IAAa,yBAAb,MAAkE;CAChE,uBAA0C;AACxC,MAAI,CAAC,WAAW,oBAAoB,CAAC,CACnC,QAAO,EAAE;AAEX,SAAO,YAAY,oBAAoB,CAAC,CACrC,QAAQ,UAAU,MAAM,SAAS,QAAQ,CAAC,CAC1C,KAAK,UAAU,mBAAmB,MAAM,MAAM,GAAG,GAAG,CAAC,CAAC,CACtD,OAAO,QAAQ;;CAGpB,eAAwB,cAAkD;AACxE,SAAO,aAAkC,KAAK,oBAAoB,EAAE,WAAW,UAAU,CAAC,CAAC;;CAG7F,eAAwB,YAAuC;AAC7D,YAAU,oBAAoB,EAAE,EAAE,WAAW,MAAM,CAAC;AACpD,gBACE,KAAK,oBAAoB,EAAE,WAAW,QAAQ,UAAU,CAAC,EACzD,KAAK,UAAU,SAAS,MAAM,EAAE,CACjC;;CAGH,iBAA0B,cAA4B;AACpD,SAAO,KAAK,oBAAoB,EAAE,WAAW,UAAU,CAAC,EAAE,EAAE,OAAO,MAAM,CAAC;;CAG5E,cAAuB,cAA0C;AAE/D,SADgB,aAAkC,KAAK,mBAAmB,EAAE,WAAW,UAAU,CAAC,CAAC,EACnF,QAAQ,MAAM,IAAI,KAAA;;CAGpC,cAAuB,WAAmB,WAAqC;AAC7E,YAAU,mBAAmB,EAAE,EAAE,WAAW,MAAM,CAAC;AACnD,gBACE,KAAK,mBAAmB,EAAE,WAAW,UAAU,CAAC,EAChD,KAAK,UAAU,EAAE,QAAQ,UAAU,IAAI,EAAE,MAAM,EAAE,CAClD;;CAGH,gBAAyB,cAA4B;AACnD,SAAO,KAAK,mBAAmB,EAAE,WAAW,UAAU,CAAC,EAAE,EAAE,OAAO,MAAM,CAAC"}
|
|
@@ -34,4 +34,5 @@ type WeixinRuntimeAccount = {
|
|
|
34
34
|
allowFrom: string[];
|
|
35
35
|
};
|
|
36
36
|
//#endregion
|
|
37
|
-
export { WeixinAccountConfig, WeixinChannelConfig, WeixinInboundMessage, WeixinRuntimeAccount };
|
|
37
|
+
export { WeixinAccountConfig, WeixinChannelConfig, WeixinInboundMessage, WeixinRuntimeAccount };
|
|
38
|
+
//# sourceMappingURL=weixin-extension.types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"weixin-extension.types.d.ts","names":[],"sources":["../../src/types/weixin-extension.types.ts"],"mappings":";;;;KAGY,mBAAA;EACV,OAAA;EACA,OAAA;EACA,MAAA;EACA,SAAA;AAAA;AAAA,KAGU,mBAAA;EACV,OAAA;EACA,gBAAA;EACA,OAAA;EACA,aAAA;EACA,SAAA;EACA,QAAA,GAAW,MAAA,SAAe,mBAAA;AAAA;AAAA,KAGhB,oBAAA;EACV,cAAA;EACA,QAAA;EACA,IAAA;EACA,WAAA,GAAc,0BAAA;EACd,SAAA;EACA,YAAA;EACA,GAAA;AAAA;AAAA,KAGU,oBAAA;EACV,SAAA;EACA,KAAA;EACA,OAAA;EACA,OAAA;EACA,aAAA;EACA,SAAA;AAAA"}
|
|
@@ -69,4 +69,5 @@ declare const WEIXIN_CHANNEL_CONFIG_UI_HINTS: {
|
|
|
69
69
|
};
|
|
70
70
|
};
|
|
71
71
|
//#endregion
|
|
72
|
-
export { DEFAULT_WEIXIN_BASE_URL, WEIXIN_CHANNEL_CONFIG_SCHEMA, WEIXIN_CHANNEL_CONFIG_UI_HINTS, WEIXIN_CHANNEL_ID, WEIXIN_EXTENSION_ID };
|
|
72
|
+
export { DEFAULT_WEIXIN_BASE_URL, WEIXIN_CHANNEL_CONFIG_SCHEMA, WEIXIN_CHANNEL_CONFIG_UI_HINTS, WEIXIN_CHANNEL_ID, WEIXIN_EXTENSION_ID };
|
|
73
|
+
//# sourceMappingURL=weixin-config.utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"weixin-config.utils.d.ts","names":[],"sources":["../../src/utils/weixin-config.utils.ts"],"mappings":";cAEa,mBAAA;AAAA,cACA,iBAAA;AAAA,cACA,uBAAA;AAAA,cA8GA,4BAAA;EAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cA+BA,8BAAA;EAAA"}
|
|
@@ -108,3 +108,5 @@ const WEIXIN_CHANNEL_CONFIG_UI_HINTS = {
|
|
|
108
108
|
};
|
|
109
109
|
//#endregion
|
|
110
110
|
export { DEFAULT_WEIXIN_BASE_URL, WEIXIN_CHANNEL_CONFIG_SCHEMA, WEIXIN_CHANNEL_CONFIG_UI_HINTS, WEIXIN_CHANNEL_ID, WEIXIN_EXTENSION_ID, buildLoggedInWeixinChannelConfig, normalizeWeixinChannelConfig };
|
|
111
|
+
|
|
112
|
+
//# sourceMappingURL=weixin-config.utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"weixin-config.utils.js","names":[],"sources":["../../src/utils/weixin-config.utils.ts"],"sourcesContent":["import type { WeixinAccountConfig, WeixinChannelConfig } from \"../types/weixin-extension.types.js\";\n\nexport const WEIXIN_EXTENSION_ID = \"nextclaw-channel-extension-weixin\";\nexport const WEIXIN_CHANNEL_ID = \"weixin\";\nexport const DEFAULT_WEIXIN_BASE_URL = \"https://ilinkai.weixin.qq.com\";\n\nfunction toRecord(value: unknown): Record<string, unknown> | undefined {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n return undefined;\n }\n return value as Record<string, unknown>;\n}\n\nfunction readString(value: unknown): string | undefined {\n if (typeof value !== \"string\") {\n return undefined;\n }\n const trimmed = value.trim();\n return trimmed || undefined;\n}\n\nfunction readStringArray(value: unknown): string[] | undefined {\n if (!Array.isArray(value)) {\n return undefined;\n }\n const values = value\n .map((entry) => readString(entry))\n .filter((entry): entry is string => Boolean(entry));\n return values.length > 0 ? values : undefined;\n}\n\nfunction normalizeAccountConfig(value: unknown): WeixinAccountConfig | undefined {\n const record = toRecord(value);\n if (!record) {\n return undefined;\n }\n return {\n enabled: typeof record.enabled === \"boolean\" ? record.enabled : undefined,\n baseUrl: readString(record.baseUrl),\n userId: readString(record.userId),\n allowFrom: readStringArray(record.allowFrom),\n };\n}\n\nexport function normalizeWeixinChannelConfig(value: unknown): WeixinChannelConfig {\n const record = toRecord(value);\n if (!record) {\n return {};\n }\n\n const accounts: Record<string, WeixinAccountConfig> = {};\n for (const [accountId, rawAccountConfig] of Object.entries(toRecord(record.accounts) ?? {})) {\n const normalized = normalizeAccountConfig(rawAccountConfig);\n if (normalized) {\n accounts[accountId] = normalized;\n }\n }\n\n return {\n enabled: typeof record.enabled === \"boolean\" ? record.enabled : undefined,\n defaultAccountId: readString(record.defaultAccountId),\n baseUrl: readString(record.baseUrl),\n pollTimeoutMs:\n typeof record.pollTimeoutMs === \"number\" && Number.isFinite(record.pollTimeoutMs)\n ? Math.max(1_000, Math.trunc(record.pollTimeoutMs))\n : undefined,\n allowFrom: readStringArray(record.allowFrom),\n accounts: Object.keys(accounts).length > 0 ? accounts : undefined,\n };\n}\n\nexport function buildLoggedInWeixinChannelConfig(params: {\n config: WeixinChannelConfig;\n accountId: string;\n baseUrl: string;\n allowUserId?: string;\n replaceAccountIds?: string[];\n}): WeixinChannelConfig {\n const { accountId, allowUserId, baseUrl, config, replaceAccountIds } = params;\n const current = normalizeWeixinChannelConfig(config);\n const replacementIds = new Set(\n (replaceAccountIds ?? [])\n .map((accountId) => readString(accountId))\n .filter((replacementAccountId): replacementAccountId is string =>\n Boolean(replacementAccountId) && replacementAccountId !== accountId\n ),\n );\n const accounts = Object.fromEntries(\n Object.entries(current.accounts ?? {}).filter(([accountId]) => !replacementIds.has(accountId)),\n );\n const currentAccount = current.accounts?.[accountId] ?? {};\n const allowFrom = new Set([\n ...(currentAccount.allowFrom ?? []),\n ...(allowUserId ? [allowUserId] : []),\n ]);\n\n return {\n ...current,\n enabled: true,\n defaultAccountId: accountId,\n baseUrl: current.baseUrl ?? baseUrl,\n accounts: {\n ...accounts,\n [accountId]: {\n ...currentAccount,\n enabled: true,\n baseUrl,\n ...(allowUserId ? { userId: allowUserId } : {}),\n allowFrom: allowFrom.size > 0 ? [...allowFrom] : undefined,\n },\n },\n };\n}\n\nexport const WEIXIN_CHANNEL_CONFIG_SCHEMA = {\n type: \"object\",\n additionalProperties: false,\n properties: {\n enabled: { type: \"boolean\" },\n defaultAccountId: { type: \"string\" },\n baseUrl: { type: \"string\" },\n pollTimeoutMs: { type: \"number\" },\n allowFrom: {\n type: \"array\",\n items: { type: \"string\" },\n },\n accounts: {\n type: \"object\",\n additionalProperties: {\n type: \"object\",\n additionalProperties: false,\n properties: {\n enabled: { type: \"boolean\" },\n baseUrl: { type: \"string\" },\n userId: { type: \"string\" },\n allowFrom: {\n type: \"array\",\n items: { type: \"string\" },\n },\n },\n },\n },\n },\n} as const;\n\nexport const WEIXIN_CHANNEL_CONFIG_UI_HINTS = {\n enabled: { label: \"Enabled\" },\n defaultAccountId: { label: \"Default Account ID\" },\n baseUrl: { label: \"API Base URL\" },\n pollTimeoutMs: { label: \"Long Poll Timeout (ms)\", advanced: true },\n allowFrom: { label: \"Allow From\" },\n} as const;\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"weixin-inbound-media.utils.js","names":[],"sources":["../../src/utils/weixin-inbound-media.utils.ts"],"sourcesContent":["import { createDecipheriv, randomUUID } from \"node:crypto\";\nimport { mkdir, writeFile } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { extname, resolve } from \"node:path\";\nimport type { ChannelSubmittedAttachment } from \"@nextclaw/extension-sdk\";\nimport type {\n WeixinCdnMedia,\n WeixinMessage,\n WeixinMessageItem,\n} from \"../services/weixin-api.service.js\";\n\ntype InboundAttachment = ChannelSubmittedAttachment;\ntype WeixinImageItem = NonNullable<WeixinMessageItem[\"image_item\"]>;\ntype WeixinFileItem = NonNullable<WeixinMessageItem[\"file_item\"]>;\n\nconst WEIXIN_INBOUND_MEDIA_MAX_BYTES = 100 * 1024 * 1024;\nconst FILE_EXTENSION_MIME_MAP: Record<string, string> = {\n \".bmp\": \"image/bmp\",\n \".cjs\": \"text/javascript\",\n \".css\": \"text/css\",\n \".csv\": \"text/csv\",\n \".gif\": \"image/gif\",\n \".go\": \"text/plain\",\n \".html\": \"text/html\",\n \".java\": \"text/plain\",\n \".js\": \"text/javascript\",\n \".json\": \"application/json\",\n \".jsx\": \"text/plain\",\n \".md\": \"text/markdown\",\n \".markdown\": \"text/markdown\",\n \".mjs\": \"text/javascript\",\n \".pdf\": \"application/pdf\",\n \".php\": \"text/plain\",\n \".png\": \"image/png\",\n \".py\": \"text/plain\",\n \".rb\": \"text/plain\",\n \".rs\": \"text/plain\",\n \".scss\": \"text/x-scss\",\n \".sh\": \"text/x-shellscript\",\n \".sql\": \"application/sql\",\n \".svg\": \"image/svg+xml\",\n \".swift\": \"text/plain\",\n \".ts\": \"text/plain\",\n \".tsx\": \"text/plain\",\n \".txt\": \"text/plain\",\n \".webp\": \"image/webp\",\n \".xml\": \"application/xml\",\n \".yaml\": \"application/yaml\",\n \".yml\": \"application/yaml\",\n};\n\nfunction detectMimeFromBuffer(buffer: Buffer): string | undefined {\n if (\n buffer.length >= 8 &&\n buffer\n .subarray(0, 8)\n .equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))\n ) {\n return \"image/png\";\n }\n if (buffer.length >= 3 && buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) {\n return \"image/jpeg\";\n }\n if (buffer.length >= 6 && [\"GIF87a\", \"GIF89a\"].includes(buffer.subarray(0, 6).toString(\"ascii\"))) {\n return \"image/gif\";\n }\n if (\n buffer.length >= 12 &&\n buffer.subarray(0, 4).toString(\"ascii\") === \"RIFF\" &&\n buffer.subarray(8, 12).toString(\"ascii\") === \"WEBP\"\n ) {\n return \"image/webp\";\n }\n if (buffer.length >= 4 && buffer.subarray(0, 4).toString(\"ascii\") === \"%PDF\") {\n return \"application/pdf\";\n }\n return undefined;\n}\n\nfunction detectMimeFromFileName(fileName?: string): string | undefined {\n const extension = extname(fileName ?? \"\").toLowerCase();\n if (extension === \".jpg\" || extension === \".jpeg\") {\n return \"image/jpeg\";\n }\n return FILE_EXTENSION_MIME_MAP[extension];\n}\n\nfunction mimeToExtension(contentType?: string, fileName?: string): string {\n const fileExtension = extname(fileName ?? \"\").trim();\n if (fileExtension) {\n return fileExtension;\n }\n switch (contentType) {\n case \"image/png\":\n return \".png\";\n case \"image/jpeg\":\n return \".jpg\";\n case \"image/gif\":\n return \".gif\";\n case \"image/webp\":\n return \".webp\";\n case \"application/pdf\":\n return \".pdf\";\n default:\n return \".bin\";\n }\n}\n\nfunction preferSpecificContentType(params: {\n reportedContentType?: string;\n detectedContentType?: string;\n fileName?: string;\n}): string | undefined {\n const { detectedContentType, fileName, reportedContentType } = params;\n const normalizedReported = reportedContentType?.trim().toLowerCase();\n if (normalizedReported && normalizedReported !== \"application/octet-stream\") {\n return normalizedReported;\n }\n return detectedContentType ?? detectMimeFromFileName(fileName) ?? reportedContentType;\n}\n\nasync function saveMediaBuffer(\n buffer: Buffer,\n contentType?: string,\n fileName?: string,\n): Promise<{ path: string; contentType?: string }> {\n if (buffer.length > WEIXIN_INBOUND_MEDIA_MAX_BYTES) {\n throw new Error(`media exceeds maxBytes (${buffer.length} > ${WEIXIN_INBOUND_MEDIA_MAX_BYTES})`);\n }\n const resolvedContentType = preferSpecificContentType({\n reportedContentType: contentType,\n detectedContentType: detectMimeFromBuffer(buffer),\n fileName,\n });\n const targetDir = resolve(tmpdir(), \"nextclaw-media\", \"inbound\");\n await mkdir(targetDir, { recursive: true });\n const targetPath = resolve(targetDir, `${randomUUID()}${mimeToExtension(resolvedContentType, fileName)}`);\n await writeFile(targetPath, buffer);\n return {\n path: targetPath,\n contentType: resolvedContentType,\n };\n}\n\nfunction parseEncodedAesKey(aesKeyBase64: string): Buffer {\n const decoded = Buffer.from(aesKeyBase64, \"base64\");\n if (decoded.length === 16) {\n return decoded;\n }\n if (decoded.length === 32 && /^[0-9a-fA-F]{32}$/.test(decoded.toString(\"ascii\"))) {\n return Buffer.from(decoded.toString(\"ascii\"), \"hex\");\n }\n throw new Error(`unsupported aes_key payload (${decoded.length} bytes after base64 decode)`);\n}\n\nfunction parseAesKey(media?: WeixinCdnMedia, imageItem?: WeixinImageItem): Buffer | undefined {\n const imageHexKey = imageItem?.aeskey?.trim();\n if (imageHexKey) {\n return Buffer.from(imageHexKey, \"hex\");\n }\n const encoded = media?.aes_key?.trim();\n if (!encoded) {\n return undefined;\n }\n return parseEncodedAesKey(encoded);\n}\n\nfunction decryptAesEcb(ciphertext: Buffer, key: Buffer): Buffer {\n const decipher = createDecipheriv(\"aes-128-ecb\", key, null);\n return Buffer.concat([decipher.update(ciphertext), decipher.final()]);\n}\n\nfunction buildFallbackDownloadUrl(baseUrl: string, encryptedQueryParam: string): string {\n const origin = new URL(baseUrl).origin;\n return `${origin}/download?encrypted_query_param=${encodeURIComponent(encryptedQueryParam)}`;\n}\n\nfunction resolveMediaUrl(media: WeixinCdnMedia | undefined, baseUrl: string): string | undefined {\n const fullUrl = media?.full_url?.trim();\n if (fullUrl) {\n return fullUrl;\n }\n const encryptedQueryParam = media?.encrypt_query_param?.trim();\n if (!encryptedQueryParam) {\n return undefined;\n }\n return buildFallbackDownloadUrl(baseUrl, encryptedQueryParam);\n}\n\nasync function fetchMediaBuffer(url: string): Promise<{ buffer: Buffer; contentType?: string }> {\n const response = await fetch(url);\n if (!response.ok) {\n throw new Error(`download failed: ${response.status} ${response.statusText}`);\n }\n return {\n buffer: Buffer.from(await response.arrayBuffer()),\n contentType: response.headers.get(\"content-type\") ?? undefined,\n };\n}\n\nasync function resolveImageAttachment(params: {\n item: WeixinMessageItem;\n baseUrl: string;\n}): Promise<InboundAttachment | null> {\n const imageItem = params.item.image_item;\n const media = imageItem?.media;\n const url = resolveMediaUrl(media, params.baseUrl);\n if (!url) {\n return null;\n }\n\n try {\n const { buffer, contentType } = await fetchMediaBuffer(url);\n const key = parseAesKey(media, imageItem);\n const plaintext = key ? decryptAesEcb(buffer, key) : buffer;\n const saved = await saveMediaBuffer(plaintext, contentType);\n return {\n path: saved.path,\n url,\n mimeType: saved.contentType ?? detectMimeFromBuffer(plaintext) ?? \"image/*\",\n size: plaintext.length,\n source: \"weixin\",\n status: \"ready\",\n };\n } catch {\n return {\n url,\n mimeType: \"image/*\",\n source: \"weixin\",\n status: \"remote-only\",\n errorCode: \"download_failed\",\n };\n }\n}\n\nasync function resolveFileAttachment(params: {\n item: WeixinMessageItem;\n baseUrl: string;\n}): Promise<InboundAttachment | null> {\n const fileItem: WeixinFileItem | undefined = params.item.file_item;\n const media = fileItem?.media;\n const fileName = fileItem?.file_name?.trim();\n const url = resolveMediaUrl(media, params.baseUrl);\n const hintedMimeType = detectMimeFromFileName(fileName);\n if (!url) {\n return fileName\n ? {\n name: fileName,\n mimeType: hintedMimeType,\n source: \"weixin\",\n status: \"remote-only\",\n errorCode: \"invalid_payload\",\n }\n : null;\n }\n\n try {\n const { buffer, contentType } = await fetchMediaBuffer(url);\n const key = parseAesKey(media);\n const plaintext = key ? decryptAesEcb(buffer, key) : buffer;\n const saved = await saveMediaBuffer(plaintext, contentType ?? hintedMimeType, fileName);\n return {\n name: fileName,\n path: saved.path,\n url,\n mimeType: saved.contentType ?? hintedMimeType ?? \"application/octet-stream\",\n size: plaintext.length,\n source: \"weixin\",\n status: \"ready\",\n };\n } catch {\n return {\n name: fileName,\n url,\n mimeType: hintedMimeType,\n source: \"weixin\",\n status: \"remote-only\",\n errorCode: \"download_failed\",\n };\n }\n}\n\nasync function resolveAttachmentFromItem(params: {\n item: WeixinMessageItem;\n baseUrl: string;\n}): Promise<InboundAttachment | null> {\n if (params.item.type === 2) {\n return resolveImageAttachment(params);\n }\n if (params.item.type === 4) {\n return resolveFileAttachment(params);\n }\n return null;\n}\n\nexport async function resolveWeixinInboundAttachments(params: {\n message: WeixinMessage;\n baseUrl: string;\n}): Promise<InboundAttachment[]> {\n const items = Array.isArray(params.message.item_list) ? params.message.item_list : [];\n const attachments: InboundAttachment[] = [];\n for (const item of items) {\n const attachment = await resolveAttachmentFromItem({\n item,\n baseUrl: params.baseUrl,\n });\n if (attachment) {\n attachments.push(attachment);\n }\n }\n return attachments;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"weixin-media.utils.js","names":[],"sources":["../../src/utils/weixin-media.utils.ts"],"sourcesContent":["import { createCipheriv, createHash, randomBytes } from \"node:crypto\";\nimport {\n WEIXIN_API_TIMEOUT_MS,\n WEIXIN_MEDIA_ENCRYPT_TYPE_PACKED,\n WEIXIN_MESSAGE_ITEM_TYPE_FILE,\n WEIXIN_MESSAGE_ITEM_TYPE_IMAGE,\n WEIXIN_UPLOAD_MEDIA_TYPE_FILE,\n WEIXIN_UPLOAD_MEDIA_TYPE_IMAGE,\n buildWeixinBaseInfo,\n fetchWeixinJson,\n normalizeWeixinBaseUrl,\n sendWeixinMessageItem,\n} from \"../services/weixin-api.service.js\";\nimport type { WeixinMessageItem } from \"../services/weixin-api.service.js\";\n\ntype WeixinUploadUrlResponse = {\n upload_param?: string;\n upload_full_url?: string;\n};\n\ntype UploadedWeixinMedia = {\n downloadEncryptedQueryParam: string;\n aesKeyHex: string;\n fileSize: number;\n fileSizeCiphertext: number;\n};\n\nconst WEIXIN_UPLOAD_TIMEOUT_MS = 60_000;\n\nfunction buildWeixinUploadUrl(params: {\n baseUrl: string;\n uploadParam: string;\n fileKey: string;\n}): string {\n const origin = new URL(params.baseUrl).origin;\n const query = new URLSearchParams({\n encrypted_query_param: params.uploadParam,\n filekey: params.fileKey,\n });\n return `${origin}/upload?${query.toString()}`;\n}\n\nfunction computeEncryptedSize(size: number): number {\n const blockSize = 16;\n const remainder = size % blockSize;\n return size + (remainder === 0 ? blockSize : blockSize - remainder);\n}\n\nfunction encryptWeixinBytes(bytes: Uint8Array, aesKey: Buffer): Buffer {\n const cipher = createCipheriv(\"aes-128-ecb\", aesKey, null);\n return Buffer.concat([cipher.update(Buffer.from(bytes)), cipher.final()]);\n}\n\nfunction encodeWeixinMediaAesKey(aesKeyHex: string): string {\n return Buffer.from(aesKeyHex, \"utf8\").toString(\"base64\");\n}\n\nasync function withUploadTimeout<T>(params: {\n timeoutMs: number;\n signal?: AbortSignal;\n handler: (signal: AbortSignal) => Promise<T>;\n}): Promise<T> {\n const { handler, signal: parentSignal, timeoutMs } = params;\n const controller = new AbortController();\n const timeout = setTimeout(\n () => controller.abort(),\n Math.max(1_000, timeoutMs),\n );\n const abort = () => controller.abort();\n parentSignal?.addEventListener(\"abort\", abort, { once: true });\n try {\n return await handler(controller.signal);\n } finally {\n clearTimeout(timeout);\n parentSignal?.removeEventListener(\"abort\", abort);\n }\n}\n\nasync function fetchWeixinUploadUrl(params: {\n baseUrl: string;\n token: string;\n toUserId: string;\n fileKey: string;\n mediaType: number;\n bytes: Uint8Array;\n aesKeyHex: string;\n signal?: AbortSignal;\n}): Promise<WeixinUploadUrlResponse> {\n const { aesKeyHex, baseUrl, bytes, fileKey, mediaType, signal, toUserId, token } = params;\n const rawFileMd5 = createHash(\"md5\")\n .update(Buffer.from(bytes))\n .digest(\"hex\");\n return await fetchWeixinJson<WeixinUploadUrlResponse>({\n url: new URL(\n \"ilink/bot/getuploadurl\",\n normalizeWeixinBaseUrl(baseUrl),\n ).toString(),\n token,\n timeoutMs: WEIXIN_API_TIMEOUT_MS,\n signal,\n body: {\n filekey: fileKey,\n media_type: mediaType,\n to_user_id: toUserId,\n rawsize: bytes.byteLength,\n rawfilemd5: rawFileMd5,\n filesize: computeEncryptedSize(bytes.byteLength),\n no_need_thumb: true,\n aeskey: aesKeyHex,\n base_info: buildWeixinBaseInfo(),\n },\n });\n}\n\nasync function uploadWeixinMedia(params: {\n baseUrl: string;\n uploadParam?: string;\n uploadFullUrl?: string;\n fileKey: string;\n bytes: Uint8Array;\n aesKey: Buffer;\n signal?: AbortSignal;\n}): Promise<UploadedWeixinMedia> {\n const { aesKey, baseUrl, bytes, fileKey, signal, uploadFullUrl, uploadParam } = params;\n const uploadUrl = uploadFullUrl?.trim()\n ? uploadFullUrl.trim()\n : uploadParam\n ? buildWeixinUploadUrl({\n baseUrl,\n uploadParam,\n fileKey,\n })\n : null;\n if (!uploadUrl) {\n throw new Error(\"weixin upload failed: upload url is missing\");\n }\n\n const ciphertext = encryptWeixinBytes(bytes, aesKey);\n const response = await withUploadTimeout({\n timeoutMs: WEIXIN_UPLOAD_TIMEOUT_MS,\n signal,\n handler: async (signal) =>\n fetch(uploadUrl, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/octet-stream\",\n },\n body: new Uint8Array(ciphertext),\n signal,\n }),\n });\n if (!response.ok) {\n const message = await response.text();\n throw new Error(\n `weixin upload failed: ${response.status} ${message || response.statusText}`,\n );\n }\n\n const downloadEncryptedQueryParam =\n response.headers.get(\"x-encrypted-param\")?.trim();\n if (!downloadEncryptedQueryParam) {\n throw new Error(\"weixin upload failed: x-encrypted-param is missing\");\n }\n\n return {\n downloadEncryptedQueryParam,\n aesKeyHex: aesKey.toString(\"hex\"),\n fileSize: bytes.byteLength,\n fileSizeCiphertext: ciphertext.byteLength,\n };\n}\n\nasync function sendUploadedWeixinMediaItem(params: {\n baseUrl: string;\n token: string;\n toUserId: string;\n contextToken?: string;\n bytes: Uint8Array;\n mediaType: number;\n item: (uploaded: UploadedWeixinMedia) => WeixinMessageItem;\n signal?: AbortSignal;\n}): Promise<{ messageId: string }> {\n const { baseUrl, bytes, contextToken, item, mediaType, signal, toUserId, token } = params;\n const fileKey = randomBytes(16).toString(\"hex\");\n const aesKey = randomBytes(16);\n const uploadUrl = await fetchWeixinUploadUrl({\n baseUrl,\n token,\n toUserId,\n fileKey,\n mediaType,\n bytes,\n aesKeyHex: aesKey.toString(\"hex\"),\n signal,\n });\n const uploaded = await uploadWeixinMedia({\n baseUrl,\n uploadParam: uploadUrl.upload_param,\n uploadFullUrl: uploadUrl.upload_full_url,\n fileKey,\n bytes,\n aesKey,\n signal,\n });\n return await sendWeixinMessageItem({\n baseUrl,\n token,\n toUserId,\n contextToken,\n signal,\n item: item(uploaded),\n });\n}\n\nexport async function sendWeixinFileMessage(params: {\n baseUrl: string;\n token: string;\n toUserId: string;\n fileName: string;\n bytes: Uint8Array;\n contextToken?: string;\n signal?: AbortSignal;\n}): Promise<{ messageId: string }> {\n const { baseUrl, bytes, contextToken, fileName, signal, toUserId, token } = params;\n return await sendUploadedWeixinMediaItem({\n baseUrl,\n token,\n toUserId,\n bytes,\n contextToken,\n mediaType: WEIXIN_UPLOAD_MEDIA_TYPE_FILE,\n signal,\n item: (uploaded) => ({\n type: WEIXIN_MESSAGE_ITEM_TYPE_FILE,\n file_item: {\n file_name: fileName,\n len: String(uploaded.fileSize),\n media: {\n encrypt_query_param: uploaded.downloadEncryptedQueryParam,\n aes_key: encodeWeixinMediaAesKey(uploaded.aesKeyHex),\n encrypt_type: WEIXIN_MEDIA_ENCRYPT_TYPE_PACKED,\n },\n },\n }),\n });\n}\n\nexport async function sendWeixinImageMessage(params: {\n baseUrl: string;\n token: string;\n toUserId: string;\n bytes: Uint8Array;\n width?: number;\n height?: number;\n contextToken?: string;\n signal?: AbortSignal;\n}): Promise<{ messageId: string }> {\n const { baseUrl, bytes, contextToken, signal, toUserId, token } = params;\n return await sendUploadedWeixinMediaItem({\n baseUrl,\n token,\n toUserId,\n bytes,\n contextToken,\n mediaType: WEIXIN_UPLOAD_MEDIA_TYPE_IMAGE,\n signal,\n item: (uploaded) => ({\n type: WEIXIN_MESSAGE_ITEM_TYPE_IMAGE,\n image_item: {\n media: {\n encrypt_query_param: uploaded.downloadEncryptedQueryParam,\n aes_key: encodeWeixinMediaAesKey(uploaded.aesKeyHex),\n encrypt_type: WEIXIN_MEDIA_ENCRYPT_TYPE_PACKED,\n },\n mid_size: uploaded.fileSizeCiphertext,\n },\n }),\n });\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"weixin-session-route.utils.js","names":[],"sources":["../../src/utils/weixin-session-route.utils.ts"],"sourcesContent":["export type WeixinSessionRoute = {\n conversationId: string;\n accountId?: string;\n};\n\nconst PEER_KINDS = new Set([\"direct\", \"group\", \"channel\"]);\n\nfunction readPayload(value: unknown): unknown {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n return undefined;\n }\n return (value as { payload?: unknown }).payload;\n}\n\nfunction readNestedMessageSessionId(payload: Record<string, unknown>): string | undefined {\n const message = payload.message;\n if (!message || typeof message !== \"object\" || Array.isArray(message)) {\n return undefined;\n }\n const sessionId = (message as { sessionId?: unknown }).sessionId;\n return typeof sessionId === \"string\" && sessionId.trim() ? sessionId.trim() : undefined;\n}\n\nexport function readWeixinEventSessionId(event: unknown): string | undefined {\n const payload = readPayload(event);\n if (!payload || typeof payload !== \"object\" || Array.isArray(payload)) {\n return undefined;\n }\n const sessionId = (payload as { sessionId?: unknown }).sessionId;\n if (typeof sessionId === \"string\" && sessionId.trim()) {\n return sessionId.trim();\n }\n return readNestedMessageSessionId(payload as Record<string, unknown>);\n}\n\nexport function resolveWeixinSessionRoute(event: unknown): WeixinSessionRoute | null {\n const sessionId = readWeixinEventSessionId(event);\n if (!sessionId) {\n return null;\n }\n const parts = sessionId.split(\":\");\n const normalizedParts = parts.map((part) => part.toLowerCase());\n if (normalizedParts[0] !== \"agent\" || parts.length < 5) {\n return null;\n }\n\n if (normalizedParts[2] === \"weixin\" && PEER_KINDS.has(normalizedParts[3] ?? \"\") && parts.length >= 5) {\n return {\n conversationId: parts.slice(4).join(\":\"),\n };\n }\n\n if (normalizedParts[2] === \"weixin\" && PEER_KINDS.has(normalizedParts[4] ?? \"\") && parts.length >= 6) {\n return {\n accountId: parts[3],\n conversationId: parts.slice(5).join(\":\"),\n };\n }\n\n return null;\n}\n"],"mappings":";AAKA,MAAM,aAAa,IAAI,IAAI;CAAC;CAAU;CAAS;CAAU,CAAC;AAE1D,SAAS,YAAY,OAAyB;AAC5C,KAAI,CAAC,SAAS,OAAO,UAAU,YAAY,MAAM,QAAQ,MAAM,CAC7D;AAEF,QAAQ,MAAgC;;AAG1C,SAAS,2BAA2B,SAAsD;CACxF,MAAM,UAAU,QAAQ;AACxB,KAAI,CAAC,WAAW,OAAO,YAAY,YAAY,MAAM,QAAQ,QAAQ,CACnE;CAEF,MAAM,YAAa,QAAoC;AACvD,QAAO,OAAO,cAAc,YAAY,UAAU,MAAM,GAAG,UAAU,MAAM,GAAG,KAAA;;AAGhF,SAAgB,yBAAyB,OAAoC;CAC3E,MAAM,UAAU,YAAY,MAAM;AAClC,KAAI,CAAC,WAAW,OAAO,YAAY,YAAY,MAAM,QAAQ,QAAQ,CACnE;CAEF,MAAM,YAAa,QAAoC;AACvD,KAAI,OAAO,cAAc,YAAY,UAAU,MAAM,CACnD,QAAO,UAAU,MAAM;AAEzB,QAAO,2BAA2B,QAAmC;;AAGvE,SAAgB,0BAA0B,OAA2C;CACnF,MAAM,YAAY,yBAAyB,MAAM;AACjD,KAAI,CAAC,UACH,QAAO;CAET,MAAM,QAAQ,UAAU,MAAM,IAAI;CAClC,MAAM,kBAAkB,MAAM,KAAK,SAAS,KAAK,aAAa,CAAC;AAC/D,KAAI,gBAAgB,OAAO,WAAW,MAAM,SAAS,EACnD,QAAO;AAGT,KAAI,gBAAgB,OAAO,YAAY,WAAW,IAAI,gBAAgB,MAAM,GAAG,IAAI,MAAM,UAAU,EACjG,QAAO,EACL,gBAAgB,MAAM,MAAM,EAAE,CAAC,KAAK,IAAI,EACzC;AAGH,KAAI,gBAAgB,OAAO,YAAY,WAAW,IAAI,gBAAgB,MAAM,GAAG,IAAI,MAAM,UAAU,EACjG,QAAO;EACL,WAAW,MAAM;EACjB,gBAAgB,MAAM,MAAM,EAAE,CAAC,KAAK,IAAI;EACzC;AAGH,QAAO"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"weixin-submitted-message.utils.js","names":[],"sources":["../../src/utils/weixin-submitted-message.utils.ts"],"sourcesContent":["import type { ChannelSubmittedMessageInput } from \"@nextclaw/extension-sdk\";\nimport type { WeixinInboundMessage } from \"../types/weixin-extension.types.js\";\n\nexport function toWeixinSubmittedMessage(\n message: WeixinInboundMessage,\n): ChannelSubmittedMessageInput {\n return {\n conversationId: message.conversationId,\n senderId: message.senderId,\n content: {\n type: \"text\",\n text: message.text,\n },\n ...(message.attachments ? { attachments: message.attachments } : {}),\n metadata: {\n ...(message.accountId ? { accountId: message.accountId, account_id: message.accountId } : {}),\n ...(message.contextToken ? { context_token: message.contextToken } : {}),\n ...(message.raw === undefined ? {} : { raw: message.raw }),\n },\n };\n}\n"],"mappings":";AAGA,SAAgB,yBACd,SAC8B;AAC9B,QAAO;EACL,gBAAgB,QAAQ;EACxB,UAAU,QAAQ;EAClB,SAAS;GACP,MAAM;GACN,MAAM,QAAQ;GACf;EACD,GAAI,QAAQ,cAAc,EAAE,aAAa,QAAQ,aAAa,GAAG,EAAE;EACnE,UAAU;GACR,GAAI,QAAQ,YAAY;IAAE,WAAW,QAAQ;IAAW,YAAY,QAAQ;IAAW,GAAG,EAAE;GAC5F,GAAI,QAAQ,eAAe,EAAE,eAAe,QAAQ,cAAc,GAAG,EAAE;GACvE,GAAI,QAAQ,QAAQ,KAAA,IAAY,EAAE,GAAG,EAAE,KAAK,QAAQ,KAAK;GAC1D;EACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nextclaw/channel-extension-weixin",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "NextClaw Weixin channel extension process.",
|
|
6
6
|
"type": "module",
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
"README.md"
|
|
19
19
|
],
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@nextclaw/
|
|
22
|
-
"@nextclaw/
|
|
23
|
-
"@nextclaw/ncp": "0.5.
|
|
21
|
+
"@nextclaw/extension-sdk": "0.1.10",
|
|
22
|
+
"@nextclaw/ncp": "0.5.16",
|
|
23
|
+
"@nextclaw/ncp-toolkit": "0.5.21"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@types/node": "^20.17.6",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"vitest": "^4.1.2"
|
|
29
29
|
},
|
|
30
30
|
"scripts": {
|
|
31
|
-
"build": "tsdown src/index.ts src/main.ts --dts --clean --target es2022 --no-fixedExtension --unbundle",
|
|
31
|
+
"build": "tsdown src/index.ts src/main.ts --dts.sourcemap --clean --target es2022 --no-fixedExtension --unbundle",
|
|
32
32
|
"lint": "eslint src --max-warnings=0",
|
|
33
33
|
"test": "vitest run",
|
|
34
34
|
"tsc": "tsc -p tsconfig.json"
|