@nextclaw/channel-extension-weixin 0.1.6 → 0.1.7

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/index.d.ts CHANGED
@@ -2,6 +2,5 @@ import { WeixinAuthPollResult, WeixinAuthStartResult, WeixinLoginParams, WeixinL
2
2
  import { WeixinAuthCapability } from "./services/weixin-auth-capability.service.js";
3
3
  import { WeixinAccountConfig, WeixinChannelConfig, WeixinInboundMessage } from "./types/weixin-extension.types.js";
4
4
  import { WeixinChannelAdapter } from "./services/weixin-channel-adapter.service.js";
5
- import { WeixinExtensionRuntime } from "./services/weixin-extension-runtime.service.js";
6
5
  import { DEFAULT_WEIXIN_BASE_URL, WEIXIN_CHANNEL_CONFIG_SCHEMA, WEIXIN_CHANNEL_CONFIG_UI_HINTS, WEIXIN_CHANNEL_ID, WEIXIN_EXTENSION_ID } from "./utils/weixin-config.utils.js";
7
- export { DEFAULT_WEIXIN_BASE_URL, WEIXIN_CHANNEL_CONFIG_SCHEMA, WEIXIN_CHANNEL_CONFIG_UI_HINTS, WEIXIN_CHANNEL_ID, WEIXIN_EXTENSION_ID, type WeixinAccountConfig, WeixinAuthCapability, type WeixinAuthPollResult, type WeixinAuthStartResult, WeixinChannelAdapter, type WeixinChannelConfig, WeixinExtensionRuntime, type WeixinInboundMessage, type WeixinLoginParams, WeixinLoginService };
6
+ export { DEFAULT_WEIXIN_BASE_URL, WEIXIN_CHANNEL_CONFIG_SCHEMA, WEIXIN_CHANNEL_CONFIG_UI_HINTS, WEIXIN_CHANNEL_ID, WEIXIN_EXTENSION_ID, type WeixinAccountConfig, WeixinAuthCapability, type WeixinAuthPollResult, type WeixinAuthStartResult, WeixinChannelAdapter, type WeixinChannelConfig, type WeixinInboundMessage, type WeixinLoginParams, WeixinLoginService };
package/dist/index.js CHANGED
@@ -2,5 +2,4 @@ import { DEFAULT_WEIXIN_BASE_URL, WEIXIN_CHANNEL_CONFIG_SCHEMA, WEIXIN_CHANNEL_C
2
2
  import { WeixinLoginService } from "./services/weixin-login.service.js";
3
3
  import { WeixinAuthCapability } from "./services/weixin-auth-capability.service.js";
4
4
  import { WeixinChannelAdapter } from "./services/weixin-channel-adapter.service.js";
5
- import { WeixinExtensionRuntime } from "./services/weixin-extension-runtime.service.js";
6
- export { DEFAULT_WEIXIN_BASE_URL, WEIXIN_CHANNEL_CONFIG_SCHEMA, WEIXIN_CHANNEL_CONFIG_UI_HINTS, WEIXIN_CHANNEL_ID, WEIXIN_EXTENSION_ID, WeixinAuthCapability, WeixinChannelAdapter, WeixinExtensionRuntime, WeixinLoginService };
5
+ export { DEFAULT_WEIXIN_BASE_URL, WEIXIN_CHANNEL_CONFIG_SCHEMA, WEIXIN_CHANNEL_CONFIG_UI_HINTS, WEIXIN_CHANNEL_ID, WEIXIN_EXTENSION_ID, WeixinAuthCapability, WeixinChannelAdapter, WeixinLoginService };
package/dist/main.js CHANGED
@@ -1,24 +1,14 @@
1
1
  import { WeixinAuthCapability } from "./services/weixin-auth-capability.service.js";
2
2
  import { WeixinChannelAdapter } from "./services/weixin-channel-adapter.service.js";
3
- import { WeixinExtensionRuntime } from "./services/weixin-extension-runtime.service.js";
4
- import { NextClawExtension } from "@nextclaw/extension-sdk";
3
+ import { toWeixinSubmittedMessage } from "./utils/weixin-submitted-message.utils.js";
4
+ import { startChannelExtension, warnNcpEventError } from "@nextclaw/extension-sdk";
5
5
  //#region src/main.ts
6
- function readRequiredString(value, name) {
7
- if (typeof value !== "string" || !value.trim()) throw new Error(`${name} is required`);
8
- return value.trim();
9
- }
10
- function readOptionalString(value) {
11
- return typeof value === "string" && value.trim() ? value.trim() : void 0;
12
- }
13
- const extension = new NextClawExtension();
14
- const weixin = extension.channels.use("weixin");
15
- const runtime = new WeixinExtensionRuntime(weixin, new WeixinChannelAdapter());
16
- extension.capabilities.provide("channel.auth", new WeixinAuthCapability({ channel: weixin }));
17
- extension.capabilities.provideHandler("channel.outbound.sendText", async (payload) => await runtime.sendOutboundText({
18
- to: readRequiredString(payload.to, "to"),
19
- text: readRequiredString(payload.text, "text"),
20
- accountId: readOptionalString(payload.accountId)
21
- }));
22
- await runtime.start();
6
+ await startChannelExtension({
7
+ channelId: "weixin",
8
+ createAdapter: () => new WeixinChannelAdapter(),
9
+ mapInbound: toWeixinSubmittedMessage,
10
+ createAuthCapability: ({ channel }) => new WeixinAuthCapability({ channel }),
11
+ onNcpEventError: warnNcpEventError("weixin")
12
+ });
23
13
  //#endregion
24
14
  export {};
@@ -45,6 +45,7 @@ declare class WeixinChannelAdapter {
45
45
  }) => Promise<void>;
46
46
  private readonly resolveReplySession;
47
47
  private readonly listAvailableAccountIds;
48
+ private readonly listConfiguredAccountIds;
48
49
  private readonly resolveCanonicalRoute;
49
50
  private readonly resolveConfiguredCanonicalRoute;
50
51
  private readonly resolveRuntimeAccount;
@@ -4,6 +4,8 @@ import { NcpEventQueue, TERMINAL_NCP_EVENT_TYPES, WeixinReplyChat } from "./weix
4
4
  import { resolveWeixinInboundAttachments } from "../utils/weixin-inbound-media.utils.js";
5
5
  import { readWeixinEventSessionId, resolveWeixinSessionRoute } from "../utils/weixin-session-route.utils.js";
6
6
  import { WeixinTypingController } from "./weixin-typing-controller.service.js";
7
+ import { createHash } from "node:crypto";
8
+ import { appendFileSync } from "node:fs";
7
9
  import { NcpReplyConsumer } from "@nextclaw/ncp-toolkit";
8
10
  //#region src/services/weixin-channel-adapter.service.ts
9
11
  const DEFAULT_WEIXIN_BASE_URL = "https://ilinkai.weixin.qq.com";
@@ -12,6 +14,19 @@ const WEIXIN_MESSAGE_ITEM_TYPE_IMAGE = 2;
12
14
  const WEIXIN_MESSAGE_ITEM_TYPE_VOICE = 3;
13
15
  const WEIXIN_MESSAGE_ITEM_TYPE_FILE = 4;
14
16
  const WEIXIN_MESSAGE_ITEM_TYPE_VIDEO = 5;
17
+ function textHash(text) {
18
+ return createHash("sha256").update(text).digest("hex").slice(0, 12);
19
+ }
20
+ function writeWeixinDebugProbe(event, payload) {
21
+ const file = process.env.NEXTCLAW_WEIXIN_DEBUG_LOG?.trim();
22
+ if (!file) return;
23
+ appendFileSync(file, `${JSON.stringify({
24
+ at: (/* @__PURE__ */ new Date()).toISOString(),
25
+ event,
26
+ pid: process.pid,
27
+ ...payload
28
+ })}\n`);
29
+ }
15
30
  async function defaultSleep(ms, signal) {
16
31
  await new Promise((resolve) => {
17
32
  const timer = setTimeout(resolve, Math.max(0, ms));
@@ -140,6 +155,15 @@ var WeixinChannelAdapter = class {
140
155
  if (!route) return;
141
156
  const sessionId = readWeixinEventSessionId(event);
142
157
  if (!sessionId) return;
158
+ writeWeixinDebugProbe("ncp-event", {
159
+ sessionId,
160
+ type: event.type,
161
+ replySessionOpen: this.replySessions.has(sessionId),
162
+ routeConversationId: route.conversationId,
163
+ routeAccountId: route.accountId,
164
+ messageId: "payload" in event && event.payload && typeof event.payload === "object" && "messageId" in event.payload ? event.payload.messageId : void 0,
165
+ runId: "payload" in event && event.payload && typeof event.payload === "object" && "runId" in event.payload ? event.payload.runId : void 0
166
+ });
143
167
  const session = this.resolveReplySession(sessionId, this.resolveCanonicalRoute(route));
144
168
  session.queue.push(event);
145
169
  if (TERMINAL_NCP_EVENT_TYPES.has(event.type)) {
@@ -153,6 +177,14 @@ var WeixinChannelAdapter = class {
153
177
  };
154
178
  sendText = async (params) => {
155
179
  const { account, contextToken, conversationId, text } = params;
180
+ writeWeixinDebugProbe("send-text", {
181
+ accountId: account.accountId,
182
+ conversationId,
183
+ textHash: textHash(text),
184
+ textLength: text.length,
185
+ textPreview: text.slice(0, 80),
186
+ hasContextToken: Boolean(contextToken)
187
+ });
156
188
  await this.api.sendTextMessage({
157
189
  baseUrl: account.baseUrl,
158
190
  token: account.token,
@@ -193,11 +225,12 @@ var WeixinChannelAdapter = class {
193
225
  return session;
194
226
  };
195
227
  listAvailableAccountIds = () => {
196
- return Array.from(new Set([
197
- ...this.config.defaultAccountId ? [this.config.defaultAccountId] : [],
198
- ...Object.keys(this.config.accounts ?? {}),
199
- ...this.store.listAccountIds()
200
- ]));
228
+ const configuredAccountIds = this.listConfiguredAccountIds();
229
+ if (configuredAccountIds.length > 0) return configuredAccountIds;
230
+ return this.store.listAccountIds();
231
+ };
232
+ listConfiguredAccountIds = () => {
233
+ return Array.from(new Set([...this.config.defaultAccountId ? [this.config.defaultAccountId] : [], ...Object.keys(this.config.accounts ?? {})]));
201
234
  };
202
235
  resolveCanonicalRoute = (route) => {
203
236
  const remembered = this.canonicalRoutes.get(normalizeRouteKey(route.conversationId));
@@ -221,6 +254,8 @@ var WeixinChannelAdapter = class {
221
254
  return null;
222
255
  };
223
256
  resolveRuntimeAccount = (accountId) => {
257
+ const configuredAccountIds = this.listConfiguredAccountIds();
258
+ if (configuredAccountIds.length > 0 && !configuredAccountIds.includes(accountId)) return null;
224
259
  const stored = this.store.loadAccount(accountId);
225
260
  if (!stored?.token) return null;
226
261
  const accountConfig = this.config.accounts?.[accountId] ?? {};
@@ -289,6 +324,15 @@ var WeixinChannelAdapter = class {
289
324
  const extractedText = extractText(message);
290
325
  const text = attachments.length > 0 && isSyntheticAttachmentText(extractedText) ? "" : extractedText;
291
326
  if (!text && attachments.length === 0) return;
327
+ writeWeixinDebugProbe("inbound-message", {
328
+ accountId: account.accountId,
329
+ senderId,
330
+ messageId: message.message_id,
331
+ textHash: text ? textHash(text) : void 0,
332
+ textLength: text.length,
333
+ textPreview: text.slice(0, 80),
334
+ attachmentCount: attachments.length
335
+ });
292
336
  const contextToken = message.context_token?.trim();
293
337
  if (contextToken) this.contextTokens.set(`${account.accountId}:${senderId}`, contextToken);
294
338
  this.canonicalRoutes.set(normalizeRouteKey(senderId), {
@@ -2,9 +2,11 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync
2
2
  import { homedir } from "node:os";
3
3
  import { join, resolve } from "node:path";
4
4
  //#region src/stores/weixin-account.store.ts
5
+ const NEXTCLAW_HOME_ENV_KEY = "NEXTCLAW_HOME";
6
+ const NEXTCLAW_DEFAULT_HOME_DIR = ".nextclaw";
5
7
  function resolveNextclawHome() {
6
- const override = process.env.NEXTCLAW_HOME?.trim();
7
- return resolve(override || join(homedir(), ".nextclaw"));
8
+ const override = process.env[NEXTCLAW_HOME_ENV_KEY]?.trim();
9
+ return resolve(override || join(homedir(), NEXTCLAW_DEFAULT_HOME_DIR));
8
10
  }
9
11
  function resolveWeixinDataDir() {
10
12
  return join(resolveNextclawHome(), "channels", "weixin");
@@ -33,17 +33,5 @@ type WeixinRuntimeAccount = {
33
33
  pollTimeoutMs: number;
34
34
  allowFrom: string[];
35
35
  };
36
- type WeixinChannelAdapter = {
37
- configure: (config: WeixinChannelConfig) => Promise<void>;
38
- start: () => Promise<void>;
39
- stop: () => Promise<void>;
40
- onMessage: (handler: (message: WeixinInboundMessage) => void | Promise<void>) => () => void;
41
- sendNcpEvent: (event: NcpEndpointEvent) => Promise<void>;
42
- sendOutboundText: (params: {
43
- to: string;
44
- text: string;
45
- accountId?: string | null;
46
- }) => Promise<void>;
47
- };
48
36
  //#endregion
49
- export { WeixinAccountConfig, WeixinChannelAdapter, WeixinChannelConfig, WeixinInboundMessage, WeixinRuntimeAccount };
37
+ export { WeixinAccountConfig, WeixinChannelConfig, WeixinInboundMessage, WeixinRuntimeAccount };
@@ -0,0 +1,22 @@
1
+ //#region src/utils/weixin-submitted-message.utils.ts
2
+ function toWeixinSubmittedMessage(message) {
3
+ return {
4
+ conversationId: message.conversationId,
5
+ senderId: message.senderId,
6
+ content: {
7
+ type: "text",
8
+ text: message.text
9
+ },
10
+ ...message.attachments ? { attachments: message.attachments } : {},
11
+ metadata: {
12
+ ...message.accountId ? {
13
+ accountId: message.accountId,
14
+ account_id: message.accountId
15
+ } : {},
16
+ ...message.contextToken ? { context_token: message.contextToken } : {},
17
+ ...message.raw === void 0 ? {} : { raw: message.raw }
18
+ }
19
+ };
20
+ }
21
+ //#endregion
22
+ export { toWeixinSubmittedMessage };
@@ -13,9 +13,6 @@
13
13
  "id": "weixin",
14
14
  "name": "Weixin",
15
15
  "description": "Weixin QR login + getupdates long-poll channel",
16
- "outbound": {
17
- "text": true
18
- },
19
16
  "auth": {
20
17
  "type": "request-response"
21
18
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/channel-extension-weixin",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
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/extension-sdk": "0.1.6",
22
- "@nextclaw/ncp": "0.5.12",
23
- "@nextclaw/ncp-toolkit": "0.5.17"
21
+ "@nextclaw/extension-sdk": "0.1.7",
22
+ "@nextclaw/ncp": "0.5.13",
23
+ "@nextclaw/ncp-toolkit": "0.5.18"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@types/node": "^20.17.6",
@@ -1,26 +0,0 @@
1
- import { WeixinChannelAdapter } from "../types/weixin-extension.types.js";
2
- import { ExtensionChannel } from "@nextclaw/extension-sdk";
3
-
4
- //#region src/services/weixin-extension-runtime.service.d.ts
5
- declare class WeixinExtensionRuntime {
6
- private readonly channel;
7
- private readonly adapter;
8
- private readonly cleanups;
9
- private started;
10
- constructor(channel: ExtensionChannel, adapter: WeixinChannelAdapter);
11
- readonly start: () => Promise<void>;
12
- readonly stop: () => Promise<void>;
13
- readonly sendOutboundText: (params: {
14
- to: string;
15
- text: string;
16
- accountId?: string | null;
17
- }) => Promise<{
18
- accepted: true;
19
- }>;
20
- private readonly applyConfig;
21
- private readonly submitMessage;
22
- private readonly sendNcpEvent;
23
- private readonly drainCleanups;
24
- }
25
- //#endregion
26
- export { WeixinExtensionRuntime };
@@ -1,70 +0,0 @@
1
- //#region src/services/weixin-extension-runtime.service.ts
2
- var WeixinExtensionRuntime = class {
3
- cleanups = [];
4
- started = false;
5
- constructor(channel, adapter) {
6
- this.channel = channel;
7
- this.adapter = adapter;
8
- }
9
- start = async () => {
10
- if (this.started) return;
11
- this.started = true;
12
- this.cleanups.push(this.adapter.onMessage(this.submitMessage));
13
- this.cleanups.push(this.channel.onNcpEvent(this.sendNcpEvent));
14
- this.cleanups.push(this.channel.config.onChange(async () => {
15
- await this.applyConfig();
16
- }));
17
- await this.applyConfig();
18
- };
19
- stop = async () => {
20
- if (!this.started) return;
21
- this.started = false;
22
- this.drainCleanups();
23
- await this.adapter.stop();
24
- };
25
- sendOutboundText = async (params) => {
26
- await this.adapter.sendOutboundText(params);
27
- return { accepted: true };
28
- };
29
- applyConfig = async () => {
30
- const config = await this.channel.config.get();
31
- await this.adapter.configure(config);
32
- if (config.enabled === false) {
33
- await this.adapter.stop();
34
- return;
35
- }
36
- await this.adapter.start();
37
- };
38
- submitMessage = async (message) => {
39
- await this.channel.submitMessage({
40
- conversationId: message.conversationId,
41
- senderId: message.senderId,
42
- content: {
43
- type: "text",
44
- text: message.text
45
- },
46
- ...message.attachments ? { attachments: message.attachments } : {},
47
- metadata: {
48
- ...message.accountId ? {
49
- accountId: message.accountId,
50
- account_id: message.accountId
51
- } : {},
52
- ...message.contextToken ? { context_token: message.contextToken } : {},
53
- ...message.raw === void 0 ? {} : { raw: message.raw }
54
- }
55
- });
56
- };
57
- sendNcpEvent = async (event) => {
58
- try {
59
- await this.adapter.sendNcpEvent(event);
60
- } catch (error) {
61
- const message = error instanceof Error ? error.message : String(error);
62
- console.warn(`[weixin] failed to send NCP event: ${message}`);
63
- }
64
- };
65
- drainCleanups = () => {
66
- for (const cleanup of this.cleanups.splice(0).reverse()) cleanup();
67
- };
68
- };
69
- //#endregion
70
- export { WeixinExtensionRuntime };