@nextclaw/channel-extension-weixin 0.1.6 → 0.1.8
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 +1 -2
- package/dist/index.js +1 -2
- package/dist/main.js +9 -19
- package/dist/services/weixin-channel-adapter.service.d.ts +1 -0
- package/dist/services/weixin-channel-adapter.service.js +49 -5
- package/dist/stores/weixin-account.store.js +4 -2
- package/dist/types/weixin-extension.types.d.ts +1 -13
- package/dist/utils/weixin-submitted-message.utils.js +22 -0
- package/nextclaw.extension.json +0 -3
- package/package.json +4 -4
- package/dist/services/weixin-extension-runtime.service.d.ts +0 -26
- package/dist/services/weixin-extension-runtime.service.js +0 -70
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,
|
|
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
|
-
|
|
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 {
|
|
4
|
-
import {
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
|
7
|
-
return resolve(override || join(homedir(),
|
|
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,
|
|
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 };
|
package/nextclaw.extension.json
CHANGED
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.8",
|
|
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
|
|
21
|
+
"@nextclaw/ncp-toolkit": "0.5.19",
|
|
22
|
+
"@nextclaw/extension-sdk": "0.1.8",
|
|
23
|
+
"@nextclaw/ncp": "0.5.14"
|
|
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 };
|