@yanhaidao/wecom 2.3.190 → 2.3.260
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/.github/workflows/release.yml +23 -4
- package/README.md +7 -1
- package/changelog/v2.3.26.md +21 -0
- package/package.json +2 -2
- package/src/agent/handler.ts +2 -0
- package/src/app/account-runtime.ts +5 -1
- package/src/app/index.ts +116 -0
- package/src/channel.ts +1 -1
- package/src/config/media.test.ts +1 -1
- package/src/config/media.ts +3 -5
- package/src/context-store.ts +264 -0
- package/src/onboarding.ts +1 -1
- package/src/outbound.test.ts +404 -2
- package/src/outbound.ts +81 -6
- package/src/runtime/dispatcher.ts +24 -5
- package/src/runtime/session-manager.test.ts +135 -0
- package/src/runtime/session-manager.ts +40 -8
- package/src/runtime/source-registry.ts +79 -0
- package/src/runtime.ts +3 -0
- package/src/target.ts +20 -8
- package/src/transport/bot-ws/media.test.ts +2 -2
- package/src/transport/bot-ws/media.ts +1 -1
- package/src/transport/bot-ws/reply.test.ts +1 -1
- package/src/transport/bot-ws/reply.ts +8 -3
- package/src/transport/http/registry.ts +1 -1
- package/src/types/runtime.ts +1 -0
- package/src/wecom_msg_adapter/markdown_adapter.ts +331 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { prepareInboundSession } from "./session-manager.js";
|
|
3
|
+
|
|
4
|
+
const {
|
|
5
|
+
resolveRuntimeRoute,
|
|
6
|
+
getPeerContextToken,
|
|
7
|
+
registerWecomSourceSnapshot,
|
|
8
|
+
} = vi.hoisted(() => ({
|
|
9
|
+
resolveRuntimeRoute: vi.fn(),
|
|
10
|
+
getPeerContextToken: vi.fn(),
|
|
11
|
+
registerWecomSourceSnapshot: vi.fn(),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
vi.mock("./routing-bridge.js", () => ({
|
|
15
|
+
resolveRuntimeRoute,
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
vi.mock("../context-store.js", () => ({
|
|
19
|
+
getPeerContextToken,
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
vi.mock("./source-registry.js", () => ({
|
|
23
|
+
registerWecomSourceSnapshot,
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
function createCore() {
|
|
27
|
+
const finalizeInboundContext = vi.fn((ctx) => ctx);
|
|
28
|
+
const recordInboundSession = vi.fn(async () => {});
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
core: {
|
|
32
|
+
channel: {
|
|
33
|
+
session: {
|
|
34
|
+
resolveStorePath: vi.fn(() => "/tmp/wecom-session-store"),
|
|
35
|
+
readSessionUpdatedAt: vi.fn(() => 1234567890),
|
|
36
|
+
recordInboundSession,
|
|
37
|
+
},
|
|
38
|
+
reply: {
|
|
39
|
+
resolveEnvelopeFormatOptions: vi.fn(() => ({ envelope: "default" })),
|
|
40
|
+
formatAgentEnvelope: vi.fn(() => "formatted-body"),
|
|
41
|
+
finalizeInboundContext,
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
} as any,
|
|
45
|
+
finalizeInboundContext,
|
|
46
|
+
recordInboundSession,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function createMediaService() {
|
|
51
|
+
return {
|
|
52
|
+
normalizeFirstAttachment: vi.fn(async () => undefined),
|
|
53
|
+
saveInboundAttachment: vi.fn(async () => undefined),
|
|
54
|
+
} as any;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
describe("prepareInboundSession", () => {
|
|
58
|
+
beforeEach(() => {
|
|
59
|
+
vi.clearAllMocks();
|
|
60
|
+
resolveRuntimeRoute.mockReturnValue({
|
|
61
|
+
sessionKey: "agent:ops_bot:wecom:direct:hidaomax",
|
|
62
|
+
agentId: "ops_bot",
|
|
63
|
+
accountId: "default",
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("marks bot-ws turns as the current wecom surface", async () => {
|
|
68
|
+
getPeerContextToken.mockReturnValue("ctx-bot");
|
|
69
|
+
const { core, finalizeInboundContext, recordInboundSession } = createCore();
|
|
70
|
+
|
|
71
|
+
const result = await prepareInboundSession({
|
|
72
|
+
core,
|
|
73
|
+
cfg: {} as any,
|
|
74
|
+
event: {
|
|
75
|
+
accountId: "default",
|
|
76
|
+
transport: "bot-ws",
|
|
77
|
+
messageId: "msg-bot-1",
|
|
78
|
+
conversation: {
|
|
79
|
+
peerKind: "direct",
|
|
80
|
+
peerId: "HiDaoMax",
|
|
81
|
+
senderId: "HiDaoMax",
|
|
82
|
+
},
|
|
83
|
+
senderName: "HiDaoMax",
|
|
84
|
+
text: "hello",
|
|
85
|
+
} as any,
|
|
86
|
+
mediaService: createMediaService(),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(finalizeInboundContext).toHaveBeenCalledWith(
|
|
90
|
+
expect.objectContaining({
|
|
91
|
+
Provider: "wecom",
|
|
92
|
+
Surface: "wecom",
|
|
93
|
+
OriginatingChannel: "wecom",
|
|
94
|
+
OriginatingTo: "wecom:context:ctx-bot",
|
|
95
|
+
To: "wecom:user:HiDaoMax",
|
|
96
|
+
}),
|
|
97
|
+
);
|
|
98
|
+
expect(recordInboundSession).toHaveBeenCalledTimes(1);
|
|
99
|
+
expect(result.ctx.Provider).toBe("wecom");
|
|
100
|
+
expect(result.ctx.Surface).toBe("wecom");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("keeps agent-callback turns on provider-only context", async () => {
|
|
104
|
+
getPeerContextToken.mockReturnValue(undefined);
|
|
105
|
+
const { core, finalizeInboundContext } = createCore();
|
|
106
|
+
|
|
107
|
+
const result = await prepareInboundSession({
|
|
108
|
+
core,
|
|
109
|
+
cfg: {} as any,
|
|
110
|
+
event: {
|
|
111
|
+
accountId: "default",
|
|
112
|
+
transport: "agent-callback",
|
|
113
|
+
messageId: "msg-agent-1",
|
|
114
|
+
conversation: {
|
|
115
|
+
peerKind: "direct",
|
|
116
|
+
peerId: "HiDaoMax",
|
|
117
|
+
senderId: "HiDaoMax",
|
|
118
|
+
},
|
|
119
|
+
senderName: "HiDaoMax",
|
|
120
|
+
text: "hello",
|
|
121
|
+
} as any,
|
|
122
|
+
mediaService: createMediaService(),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
expect(finalizeInboundContext).toHaveBeenCalledWith(
|
|
126
|
+
expect.objectContaining({
|
|
127
|
+
Provider: "wecom",
|
|
128
|
+
OriginatingChannel: "wecom",
|
|
129
|
+
OriginatingTo: "wecom:user:HiDaoMax",
|
|
130
|
+
}),
|
|
131
|
+
);
|
|
132
|
+
expect(result.ctx.Provider).toBe("wecom");
|
|
133
|
+
expect(result.ctx).not.toHaveProperty("Surface");
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
|
|
2
2
|
import type { WecomMediaService } from "../shared/media-service.js";
|
|
3
3
|
import type { UnifiedInboundEvent } from "../types/index.js";
|
|
4
|
+
import { getPeerContextToken } from "../context-store.js";
|
|
5
|
+
import { buildWecomContextTarget } from "../target.js";
|
|
4
6
|
import { resolveRuntimeRoute } from "./routing-bridge.js";
|
|
5
7
|
import { registerWecomSourceSnapshot } from "./source-registry.js";
|
|
6
8
|
|
|
@@ -18,12 +20,20 @@ export async function prepareInboundSession(params: {
|
|
|
18
20
|
}): Promise<PreparedSession> {
|
|
19
21
|
const { core, cfg, event, mediaService } = params;
|
|
20
22
|
const route = resolveRuntimeRoute({ core, cfg, event });
|
|
21
|
-
|
|
23
|
+
const source =
|
|
24
|
+
event.transport === "bot-ws"
|
|
25
|
+
? "bot-ws"
|
|
26
|
+
: event.transport === "agent-callback"
|
|
27
|
+
? "agent-callback"
|
|
28
|
+
: undefined;
|
|
29
|
+
if (source) {
|
|
22
30
|
registerWecomSourceSnapshot({
|
|
23
31
|
accountId: event.accountId,
|
|
24
|
-
source
|
|
32
|
+
source,
|
|
25
33
|
messageId: event.messageId,
|
|
26
34
|
sessionKey: route.sessionKey,
|
|
35
|
+
peerKind: event.conversation.peerKind,
|
|
36
|
+
peerId: event.conversation.peerId,
|
|
27
37
|
});
|
|
28
38
|
}
|
|
29
39
|
const storePath = core.channel.session.resolveStorePath(cfg.session?.store, {
|
|
@@ -46,6 +56,30 @@ export async function prepareInboundSession(params: {
|
|
|
46
56
|
const mediaPath = firstAttachment
|
|
47
57
|
? await mediaService.saveInboundAttachment(event, firstAttachment)
|
|
48
58
|
: undefined;
|
|
59
|
+
const defaultOriginatingTo =
|
|
60
|
+
event.conversation.peerKind === "group"
|
|
61
|
+
? `wecom:group:${event.conversation.peerId}`
|
|
62
|
+
: `wecom:user:${event.conversation.peerId}`;
|
|
63
|
+
const contextToken =
|
|
64
|
+
event.transport === "bot-ws"
|
|
65
|
+
? getPeerContextToken(event.accountId, event.conversation.peerId)
|
|
66
|
+
: undefined;
|
|
67
|
+
const originatingTo = contextToken
|
|
68
|
+
? buildWecomContextTarget(contextToken)
|
|
69
|
+
: defaultOriginatingTo;
|
|
70
|
+
const providerContext =
|
|
71
|
+
event.transport === "bot-ws"
|
|
72
|
+
? {
|
|
73
|
+
// Bot WS inbound turns already have a live reply handle bound to the
|
|
74
|
+
// current req_id. Mark the current surface as WeCom so core final text
|
|
75
|
+
// stays on that handle and replaces the placeholder instead of being
|
|
76
|
+
// re-routed as a second active-push message.
|
|
77
|
+
Provider: "wecom" as const,
|
|
78
|
+
Surface: "wecom" as const,
|
|
79
|
+
}
|
|
80
|
+
: {
|
|
81
|
+
Provider: "wecom" as const,
|
|
82
|
+
};
|
|
49
83
|
|
|
50
84
|
const ctx = core.channel.reply.finalizeInboundContext({
|
|
51
85
|
Body: body,
|
|
@@ -65,13 +99,11 @@ export async function prepareInboundSession(params: {
|
|
|
65
99
|
ConversationLabel: `${event.conversation.peerKind}:${event.conversation.peerId}`,
|
|
66
100
|
SenderName: event.senderName ?? event.conversation.senderId,
|
|
67
101
|
SenderId: event.conversation.senderId,
|
|
68
|
-
|
|
69
|
-
|
|
102
|
+
// Keep Originating* populated so explicit route-to-origin flows and message
|
|
103
|
+
// tools can still resolve the active peer context when needed.
|
|
104
|
+
...providerContext,
|
|
70
105
|
OriginatingChannel: "wecom",
|
|
71
|
-
OriginatingTo:
|
|
72
|
-
event.conversation.peerKind === "group"
|
|
73
|
-
? `wecom:group:${event.conversation.peerId}`
|
|
74
|
-
: `wecom:user:${event.conversation.peerId}`,
|
|
106
|
+
OriginatingTo: originatingTo,
|
|
75
107
|
MessageSid: event.messageId,
|
|
76
108
|
CommandAuthorized: true,
|
|
77
109
|
MediaPath: mediaPath,
|
|
@@ -7,14 +7,19 @@ export type WecomSourceSnapshot = {
|
|
|
7
7
|
messageId?: string;
|
|
8
8
|
sessionKey?: string;
|
|
9
9
|
sessionId?: string;
|
|
10
|
+
peerKind?: "direct" | "group";
|
|
11
|
+
peerId?: string;
|
|
10
12
|
};
|
|
11
13
|
|
|
12
14
|
const MAX_MESSAGE_FACTS = 2048;
|
|
13
15
|
const MAX_SESSION_SNAPSHOTS = 1024;
|
|
16
|
+
const MAX_CONVERSATION_SNAPSHOTS = 1024;
|
|
14
17
|
|
|
15
18
|
const messageFacts = new Map<string, WecomSourceSnapshot>();
|
|
16
19
|
const sessionSnapshotsByAccountKey = new Map<string, WecomSourceSnapshot>();
|
|
17
20
|
const sessionSnapshotsByLooseKey = new Map<string, WecomSourceSnapshot>();
|
|
21
|
+
const conversationSnapshotsByAccountKey = new Map<string, WecomSourceSnapshot>();
|
|
22
|
+
const conversationSnapshotsByLooseKey = new Map<string, WecomSourceSnapshot>();
|
|
18
23
|
|
|
19
24
|
function normalizeOptional(value: string | null | undefined): string | undefined {
|
|
20
25
|
const trimmed = String(value ?? "").trim();
|
|
@@ -33,6 +38,24 @@ function accountScopedSessionKey(
|
|
|
33
38
|
return `${accountId}::${kind}::${value}`;
|
|
34
39
|
}
|
|
35
40
|
|
|
41
|
+
function normalizePeerId(value: string | null | undefined): string | undefined {
|
|
42
|
+
const trimmed = String(value ?? "").trim();
|
|
43
|
+
return trimmed ? trimmed.toLowerCase() : undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function normalizePeerKind(value: string | null | undefined): "direct" | "group" | undefined {
|
|
47
|
+
const trimmed = String(value ?? "").trim().toLowerCase();
|
|
48
|
+
return trimmed === "direct" || trimmed === "group" ? trimmed : undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function accountScopedConversationKey(
|
|
52
|
+
accountId: string,
|
|
53
|
+
peerKind: "direct" | "group",
|
|
54
|
+
peerId: string,
|
|
55
|
+
): string {
|
|
56
|
+
return `${accountId}::peer::${peerKind}::${peerId}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
36
59
|
function pruneOldest<T>(map: Map<string, T>, maxSize: number): void {
|
|
37
60
|
while (map.size > maxSize) {
|
|
38
61
|
const oldestKey = map.keys().next().value;
|
|
@@ -62,12 +85,37 @@ function writeSessionSnapshot(snapshot: WecomSourceSnapshot): void {
|
|
|
62
85
|
pruneOldest(sessionSnapshotsByLooseKey, MAX_SESSION_SNAPSHOTS * 2);
|
|
63
86
|
}
|
|
64
87
|
|
|
88
|
+
function writeConversationSnapshot(snapshot: WecomSourceSnapshot): void {
|
|
89
|
+
const peerKind = normalizePeerKind(snapshot.peerKind);
|
|
90
|
+
const peerId = normalizePeerId(snapshot.peerId);
|
|
91
|
+
if (!peerKind || !peerId) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
conversationSnapshotsByAccountKey.set(
|
|
95
|
+
accountScopedConversationKey(snapshot.accountId, peerKind, peerId),
|
|
96
|
+
{
|
|
97
|
+
...snapshot,
|
|
98
|
+
peerKind,
|
|
99
|
+
peerId,
|
|
100
|
+
},
|
|
101
|
+
);
|
|
102
|
+
conversationSnapshotsByLooseKey.set(`peer::${peerKind}::${peerId}`, {
|
|
103
|
+
...snapshot,
|
|
104
|
+
peerKind,
|
|
105
|
+
peerId,
|
|
106
|
+
});
|
|
107
|
+
pruneOldest(conversationSnapshotsByAccountKey, MAX_CONVERSATION_SNAPSHOTS);
|
|
108
|
+
pruneOldest(conversationSnapshotsByLooseKey, MAX_CONVERSATION_SNAPSHOTS);
|
|
109
|
+
}
|
|
110
|
+
|
|
65
111
|
export function registerWecomSourceSnapshot(params: {
|
|
66
112
|
accountId: string;
|
|
67
113
|
source: WecomSourcePlane;
|
|
68
114
|
messageId?: string | null;
|
|
69
115
|
sessionKey?: string | null;
|
|
70
116
|
sessionId?: string | null;
|
|
117
|
+
peerKind?: "direct" | "group" | null;
|
|
118
|
+
peerId?: string | null;
|
|
71
119
|
}): void {
|
|
72
120
|
const accountId = normalizeOptional(params.accountId);
|
|
73
121
|
if (!accountId) return;
|
|
@@ -85,6 +133,8 @@ export function registerWecomSourceSnapshot(params: {
|
|
|
85
133
|
...(normalizeOptional(params.sessionId)
|
|
86
134
|
? { sessionId: normalizeOptional(params.sessionId) }
|
|
87
135
|
: {}),
|
|
136
|
+
...(normalizePeerKind(params.peerKind) ? { peerKind: normalizePeerKind(params.peerKind) } : {}),
|
|
137
|
+
...(normalizePeerId(params.peerId) ? { peerId: normalizePeerId(params.peerId) } : {}),
|
|
88
138
|
};
|
|
89
139
|
|
|
90
140
|
if (snapshot.messageId) {
|
|
@@ -93,16 +143,21 @@ export function registerWecomSourceSnapshot(params: {
|
|
|
93
143
|
}
|
|
94
144
|
|
|
95
145
|
writeSessionSnapshot(snapshot);
|
|
146
|
+
writeConversationSnapshot(snapshot);
|
|
96
147
|
}
|
|
97
148
|
|
|
98
149
|
export function resolveWecomSourceSnapshot(params: {
|
|
99
150
|
accountId?: string | null;
|
|
100
151
|
sessionKey?: string | null;
|
|
101
152
|
sessionId?: string | null;
|
|
153
|
+
peerKind?: "direct" | "group" | null;
|
|
154
|
+
peerId?: string | null;
|
|
102
155
|
}): WecomSourceSnapshot | undefined {
|
|
103
156
|
const accountId = normalizeOptional(params.accountId);
|
|
104
157
|
const sessionKey = normalizeOptional(params.sessionKey);
|
|
105
158
|
const sessionId = normalizeOptional(params.sessionId);
|
|
159
|
+
const peerKind = normalizePeerKind(params.peerKind);
|
|
160
|
+
const peerId = normalizePeerId(params.peerId);
|
|
106
161
|
|
|
107
162
|
if (accountId && sessionKey) {
|
|
108
163
|
const scoped = sessionSnapshotsByAccountKey.get(
|
|
@@ -124,6 +179,16 @@ export function resolveWecomSourceSnapshot(params: {
|
|
|
124
179
|
const loose = sessionSnapshotsByLooseKey.get(`sessionId::${sessionId}`);
|
|
125
180
|
if (loose) return loose;
|
|
126
181
|
}
|
|
182
|
+
if (accountId && peerKind && peerId) {
|
|
183
|
+
const scoped = conversationSnapshotsByAccountKey.get(
|
|
184
|
+
accountScopedConversationKey(accountId, peerKind, peerId),
|
|
185
|
+
);
|
|
186
|
+
if (scoped) return scoped;
|
|
187
|
+
}
|
|
188
|
+
if (peerKind && peerId) {
|
|
189
|
+
const loose = conversationSnapshotsByLooseKey.get(`peer::${peerKind}::${peerId}`);
|
|
190
|
+
if (loose) return loose;
|
|
191
|
+
}
|
|
127
192
|
return undefined;
|
|
128
193
|
}
|
|
129
194
|
|
|
@@ -146,12 +211,24 @@ export function clearWecomSourceAccount(accountId: string): void {
|
|
|
146
211
|
sessionSnapshotsByLooseKey.delete(key);
|
|
147
212
|
}
|
|
148
213
|
}
|
|
214
|
+
for (const [key, value] of conversationSnapshotsByAccountKey) {
|
|
215
|
+
if (value.accountId === normalized || key.startsWith(`${normalized}::`)) {
|
|
216
|
+
conversationSnapshotsByAccountKey.delete(key);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
for (const [key, value] of conversationSnapshotsByLooseKey) {
|
|
220
|
+
if (value.accountId === normalized) {
|
|
221
|
+
conversationSnapshotsByLooseKey.delete(key);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
149
224
|
}
|
|
150
225
|
|
|
151
226
|
export function isWecomBotWsSource(params: {
|
|
152
227
|
accountId?: string | null;
|
|
153
228
|
sessionKey?: string | null;
|
|
154
229
|
sessionId?: string | null;
|
|
230
|
+
peerKind?: "direct" | "group" | null;
|
|
231
|
+
peerId?: string | null;
|
|
155
232
|
}): boolean {
|
|
156
233
|
return resolveWecomSourceSnapshot(params)?.source === "bot-ws";
|
|
157
234
|
}
|
|
@@ -160,6 +237,8 @@ export function isWecomAgentSource(params: {
|
|
|
160
237
|
accountId?: string | null;
|
|
161
238
|
sessionKey?: string | null;
|
|
162
239
|
sessionId?: string | null;
|
|
240
|
+
peerKind?: "direct" | "group" | null;
|
|
241
|
+
peerId?: string | null;
|
|
163
242
|
}): boolean {
|
|
164
243
|
return resolveWecomSourceSnapshot(params)?.source === "agent-callback";
|
|
165
244
|
}
|
package/src/runtime.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
export {
|
|
2
|
+
getActiveBotWsReplyHandle,
|
|
2
3
|
getAccountRuntime,
|
|
3
4
|
getAccountRuntimeSnapshot,
|
|
4
5
|
getBotWsPushHandle,
|
|
5
6
|
getWecomRuntime,
|
|
7
|
+
registerActiveBotWsReplyHandle,
|
|
6
8
|
registerAccountRuntime,
|
|
7
9
|
registerBotWsPushHandle,
|
|
8
10
|
setWecomRuntime,
|
|
11
|
+
unregisterActiveBotWsReplyHandle,
|
|
9
12
|
unregisterBotWsPushHandle,
|
|
10
13
|
unregisterAccountRuntime,
|
|
11
14
|
} from "./app/index.js";
|
package/src/target.ts
CHANGED
|
@@ -26,6 +26,18 @@ export interface ScopedWecomTarget {
|
|
|
26
26
|
rawTarget: string;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
export function buildWecomContextTarget(contextToken: string): string {
|
|
30
|
+
return `wecom:context:${contextToken}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function resolveWecomContextTarget(raw: string | undefined): { contextToken: string } | undefined {
|
|
34
|
+
const trimmed = raw?.trim();
|
|
35
|
+
if (!trimmed) return undefined;
|
|
36
|
+
const match = trimmed.match(/^(?:wecom|wechatwork|wework|qywx):context:(.+)$/i);
|
|
37
|
+
const contextToken = match?.[1]?.trim();
|
|
38
|
+
return contextToken ? { contextToken } : undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
29
41
|
/**
|
|
30
42
|
* Parses a raw target string into a WeComTarget object.
|
|
31
43
|
* 解析原始目标字符串为 WeComTarget 对象。
|
|
@@ -86,16 +98,16 @@ export function resolveWecomTarget(raw: string | undefined, options?: { preferUs
|
|
|
86
98
|
return { chatid: clean };
|
|
87
99
|
}
|
|
88
100
|
|
|
89
|
-
// Pure digits: Default to
|
|
90
|
-
// 原因:1)
|
|
91
|
-
// 2)
|
|
92
|
-
// 3)
|
|
93
|
-
//
|
|
101
|
+
// Pure digits: Default to User (纯数字默认为用户)
|
|
102
|
+
// 原因:1) Bot WS 主动推送只接受 touser/chatid,不接受 toparty/totag
|
|
103
|
+
// 2) 用户 ID 在企业微信中常为纯数字
|
|
104
|
+
// 3) 部门推送应使用显式前缀 "party:xxx" 或通过 Agent 模式
|
|
105
|
+
// 如果确实需要发送到部门,请使用 party: 前缀或 Agent 路径
|
|
94
106
|
if (/^\d+$/.test(clean)) {
|
|
95
|
-
if (options?.preferUserForDigits) {
|
|
96
|
-
return {
|
|
107
|
+
if (options?.preferUserForDigits === false) {
|
|
108
|
+
return { toparty: clean };
|
|
97
109
|
}
|
|
98
|
-
return {
|
|
110
|
+
return { touser: clean };
|
|
99
111
|
}
|
|
100
112
|
|
|
101
113
|
// Default to User (默认为用户)
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { WSClient } from "@wecom/aibot-node-sdk";
|
|
2
2
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { loadOutboundMediaFromUrl } from "openclaw/plugin-sdk/media-runtime";
|
|
3
4
|
|
|
4
5
|
import { uploadAndSendBotWsMedia } from "./media.js";
|
|
5
|
-
import { loadOutboundMediaFromUrl } from "openclaw/plugin-sdk";
|
|
6
6
|
|
|
7
|
-
vi.mock("openclaw/plugin-sdk", () => ({
|
|
7
|
+
vi.mock("openclaw/plugin-sdk/media-runtime", () => ({
|
|
8
8
|
detectMime: vi.fn(),
|
|
9
9
|
loadOutboundMediaFromUrl: vi.fn(),
|
|
10
10
|
}));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { WeComMediaType, WsFrameHeaders, WSClient } from "@wecom/aibot-node-sdk";
|
|
2
|
-
import { detectMime, loadOutboundMediaFromUrl } from "openclaw/plugin-sdk";
|
|
2
|
+
import { detectMime, loadOutboundMediaFromUrl } from "openclaw/plugin-sdk/media-runtime";
|
|
3
3
|
|
|
4
4
|
const IMAGE_MAX_BYTES = 10 * 1024 * 1024;
|
|
5
5
|
const VIDEO_MAX_BYTES = 10 * 1024 * 1024;
|
|
@@ -2,7 +2,7 @@ import os from "node:os";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import type { WSClient } from "@wecom/aibot-node-sdk";
|
|
4
4
|
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
|
5
|
-
import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk";
|
|
5
|
+
import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/infra-runtime";
|
|
6
6
|
import { uploadAndSendBotWsMedia } from "./media.js";
|
|
7
7
|
import { createBotWsReplyHandle } from "./reply.js";
|
|
8
8
|
|
|
@@ -5,10 +5,11 @@ import {
|
|
|
5
5
|
type EventMessage,
|
|
6
6
|
type WSClient,
|
|
7
7
|
} from "@wecom/aibot-node-sdk";
|
|
8
|
-
import { formatErrorMessage } from "openclaw/plugin-sdk";
|
|
8
|
+
import { formatErrorMessage } from "openclaw/plugin-sdk/infra-runtime";
|
|
9
9
|
import { resolveWecomMediaMaxBytes, resolveWecomMergedMediaLocalRoots } from "../../config/index.js";
|
|
10
10
|
import { getWecomRuntime } from "../../runtime.js";
|
|
11
11
|
import type { ReplyHandle, ReplyPayload } from "../../types/index.js";
|
|
12
|
+
import { toWeComMarkdownV2 } from "../../wecom_msg_adapter/markdown_adapter.js";
|
|
12
13
|
import { uploadAndSendBotWsMedia } from "./media.js";
|
|
13
14
|
|
|
14
15
|
const PLACEHOLDER_KEEPALIVE_MS = 3000;
|
|
@@ -307,13 +308,13 @@ export function createBotWsReplyHandle(params: {
|
|
|
307
308
|
// Send push message for other events
|
|
308
309
|
await params.client.sendMessage(peerId, {
|
|
309
310
|
msgtype: "markdown",
|
|
310
|
-
markdown: { content: finalText },
|
|
311
|
+
markdown: { content: toWeComMarkdownV2(finalText) },
|
|
311
312
|
});
|
|
312
313
|
} else {
|
|
313
314
|
await params.client.replyStream(
|
|
314
315
|
params.frame,
|
|
315
316
|
resolveStreamId(),
|
|
316
|
-
finalText,
|
|
317
|
+
toWeComMarkdownV2(finalText),
|
|
317
318
|
info.kind === "final",
|
|
318
319
|
);
|
|
319
320
|
}
|
|
@@ -356,5 +357,9 @@ export function createBotWsReplyHandle(params: {
|
|
|
356
357
|
}
|
|
357
358
|
params.onFail?.(error);
|
|
358
359
|
},
|
|
360
|
+
markExternalActivity: () => {
|
|
361
|
+
notifyPeerActive();
|
|
362
|
+
stopPlaceholderKeepalive();
|
|
363
|
+
},
|
|
359
364
|
};
|
|
360
365
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
2
|
-
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk";
|
|
2
|
+
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/routing";
|
|
3
3
|
|
|
4
4
|
import type { ResolvedAgentAccount, TransportSessionPatch } from "../../types/index.js";
|
|
5
5
|
import { monitorState } from "../../monitor/state.js";
|
package/src/types/runtime.ts
CHANGED
|
@@ -95,6 +95,7 @@ export type ReplyHandle = {
|
|
|
95
95
|
context: ReplyContext;
|
|
96
96
|
deliver: (payload: ReplyPayload, info: ReplyDeliveryInfo) => Promise<void>;
|
|
97
97
|
fail?: (error: unknown) => Promise<void>;
|
|
98
|
+
markExternalActivity?: () => void;
|
|
98
99
|
};
|
|
99
100
|
|
|
100
101
|
export type TransportSessionSnapshot = {
|