@shenhh/popo-oa 0.1.0
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/index.ts +36 -0
- package/openclaw.plugin.json +10 -0
- package/package.json +66 -0
- package/src/accounts.ts +53 -0
- package/src/auth.ts +104 -0
- package/src/bot.ts +285 -0
- package/src/channel.ts +209 -0
- package/src/client.ts +167 -0
- package/src/config-schema.ts +50 -0
- package/src/crypto.ts +44 -0
- package/src/monitor.ts +215 -0
- package/src/outbound.ts +72 -0
- package/src/policy.ts +60 -0
- package/src/probe.ts +44 -0
- package/src/reply-dispatcher.ts +130 -0
- package/src/runtime.ts +27 -0
- package/src/send.ts +129 -0
- package/src/targets.ts +25 -0
- package/src/types.ts +158 -0
package/index.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
+
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
|
|
3
|
+
import { popoOaPlugin } from "./src/channel.js";
|
|
4
|
+
import { setPopoOaRuntime } from "./src/runtime.js";
|
|
5
|
+
|
|
6
|
+
export { monitorPopoOaProvider } from "./src/monitor.js";
|
|
7
|
+
export {
|
|
8
|
+
sendMessagePopoOa,
|
|
9
|
+
sendTextPopoOa,
|
|
10
|
+
sendImagePopoOa,
|
|
11
|
+
sendNewsPopoOa,
|
|
12
|
+
sendListPopoOa,
|
|
13
|
+
sendButtonPopoOa,
|
|
14
|
+
} from "./src/send.js";
|
|
15
|
+
export { probePopoOa } from "./src/probe.js";
|
|
16
|
+
export { popoOaPlugin } from "./src/channel.js";
|
|
17
|
+
export { verifySignature, generateSignature } from "./src/crypto.js";
|
|
18
|
+
export type {
|
|
19
|
+
PopoOaConfig,
|
|
20
|
+
PopoOaXmlMessage,
|
|
21
|
+
PopoOaSendMessage,
|
|
22
|
+
ResolvedPopoOaAccount,
|
|
23
|
+
} from "./src/types.js";
|
|
24
|
+
|
|
25
|
+
const plugin = {
|
|
26
|
+
id: "popo-oa",
|
|
27
|
+
name: "POPO 服务号",
|
|
28
|
+
description: "POPO Official Account (服务号) channel plugin",
|
|
29
|
+
configSchema: emptyPluginConfigSchema(),
|
|
30
|
+
register(api: OpenClawPluginApi) {
|
|
31
|
+
setPopoOaRuntime(api.runtime);
|
|
32
|
+
api.registerChannel({ plugin: popoOaPlugin });
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export default plugin;
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@shenhh/popo-oa",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "OpenClaw POPO Official Account (服务号) channel plugin",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"files": [
|
|
8
|
+
"index.ts",
|
|
9
|
+
"src",
|
|
10
|
+
"openclaw.plugin.json"
|
|
11
|
+
],
|
|
12
|
+
"author": {
|
|
13
|
+
"name": "Hengheng Shen",
|
|
14
|
+
"email": "1048157315@qq.com"
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"cache": "~/.npm",
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/m1heng/clawdbot-popo-oa.git"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"openclaw",
|
|
26
|
+
"popo",
|
|
27
|
+
"netease",
|
|
28
|
+
"chatbot",
|
|
29
|
+
"ai",
|
|
30
|
+
"claude",
|
|
31
|
+
"official-account",
|
|
32
|
+
"服务号"
|
|
33
|
+
],
|
|
34
|
+
"openclaw": {
|
|
35
|
+
"extensions": [
|
|
36
|
+
"./index.ts"
|
|
37
|
+
],
|
|
38
|
+
"channel": {
|
|
39
|
+
"id": "popo-oa",
|
|
40
|
+
"label": "POPO 服务号",
|
|
41
|
+
"selectionLabel": "POPO 服务号 (网易)",
|
|
42
|
+
"docsPath": "/channels/popo-oa",
|
|
43
|
+
"docsLabel": "popo-oa",
|
|
44
|
+
"blurb": "POPO Official Account messaging via Open API.",
|
|
45
|
+
"aliases": [],
|
|
46
|
+
"order": 82
|
|
47
|
+
},
|
|
48
|
+
"install": {
|
|
49
|
+
"npmSpec": "@shenhh/clawdbot-popo-oa",
|
|
50
|
+
"localPath": ".",
|
|
51
|
+
"defaultChoice": "npm"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"zod": "^4.3.6"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@types/node": "^25.0.10",
|
|
59
|
+
"openclaw": "2026.1.29",
|
|
60
|
+
"tsx": "^4.21.0",
|
|
61
|
+
"typescript": "^5.7.0"
|
|
62
|
+
},
|
|
63
|
+
"peerDependencies": {
|
|
64
|
+
"openclaw": ">=2026.1.29"
|
|
65
|
+
}
|
|
66
|
+
}
|
package/src/accounts.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { ClawdbotConfig } from "openclaw/plugin-sdk";
|
|
2
|
+
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk";
|
|
3
|
+
import type { PopoOaConfig, ResolvedPopoOaAccount } from "./types.js";
|
|
4
|
+
|
|
5
|
+
export function resolvePopoOaCredentials(cfg?: PopoOaConfig): {
|
|
6
|
+
appId: string;
|
|
7
|
+
appSecret: string;
|
|
8
|
+
token: string;
|
|
9
|
+
server: string;
|
|
10
|
+
webhookPath: string;
|
|
11
|
+
} | null {
|
|
12
|
+
const appId = cfg?.appId?.trim();
|
|
13
|
+
const appSecret = cfg?.appSecret?.trim();
|
|
14
|
+
const token = cfg?.token?.trim();
|
|
15
|
+
if (!appId || !appSecret || !token) return null;
|
|
16
|
+
return {
|
|
17
|
+
appId,
|
|
18
|
+
appSecret,
|
|
19
|
+
token,
|
|
20
|
+
server: cfg?.server ?? "https://open.popo.netease.com",
|
|
21
|
+
webhookPath: cfg?.webhookPath ?? "/popo-oa/events",
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function resolvePopoOaAccount(params: {
|
|
26
|
+
cfg: ClawdbotConfig;
|
|
27
|
+
accountId?: string | null;
|
|
28
|
+
}): ResolvedPopoOaAccount {
|
|
29
|
+
const popoCfg = params.cfg.channels?.["popo-oa"] as PopoOaConfig | undefined;
|
|
30
|
+
const creds = resolvePopoOaCredentials(popoCfg);
|
|
31
|
+
|
|
32
|
+
if (!creds || !popoCfg) {
|
|
33
|
+
throw new Error("POPO OA not configured");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
id: creds.appId,
|
|
38
|
+
appId: creds.appId,
|
|
39
|
+
appSecret: creds.appSecret,
|
|
40
|
+
token: creds.token,
|
|
41
|
+
server: creds.server,
|
|
42
|
+
webhookPath: creds.webhookPath,
|
|
43
|
+
cfg: popoCfg,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function listPopoOaAccountIds(_cfg: ClawdbotConfig): string[] {
|
|
48
|
+
return [DEFAULT_ACCOUNT_ID];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function resolveDefaultPopoOaAccountId(_cfg: ClawdbotConfig): string {
|
|
52
|
+
return DEFAULT_ACCOUNT_ID;
|
|
53
|
+
}
|
package/src/auth.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { CachedAccessToken, AccessTokenResponse } from "./types.js";
|
|
2
|
+
|
|
3
|
+
// In-memory token cache: appId -> cached token
|
|
4
|
+
const tokenCache = new Map<string, CachedAccessToken>();
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Get access token for POPO OA API.
|
|
8
|
+
* Uses cached token if available and not expired.
|
|
9
|
+
*/
|
|
10
|
+
export async function getAccessToken({
|
|
11
|
+
appId,
|
|
12
|
+
appSecret,
|
|
13
|
+
server,
|
|
14
|
+
}: {
|
|
15
|
+
appId: string;
|
|
16
|
+
appSecret: string;
|
|
17
|
+
server: string;
|
|
18
|
+
}): Promise<string> {
|
|
19
|
+
const cached = tokenCache.get(appId);
|
|
20
|
+
|
|
21
|
+
// Return cached token if still valid (with 5 min buffer)
|
|
22
|
+
if (cached && cached.expiresAt > Date.now() + 5 * 60 * 1000) {
|
|
23
|
+
return cached.token;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Fetch new token
|
|
27
|
+
const token = await fetchAccessToken({ appId, appSecret, server });
|
|
28
|
+
|
|
29
|
+
// Cache the token
|
|
30
|
+
tokenCache.set(appId, {
|
|
31
|
+
token: token.accessToken,
|
|
32
|
+
expiresAt: Date.now() + token.expiresIn * 1000,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return token.accessToken;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Fetch new access token from POPO OA API.
|
|
40
|
+
*/
|
|
41
|
+
async function fetchAccessToken({
|
|
42
|
+
appId,
|
|
43
|
+
appSecret,
|
|
44
|
+
server,
|
|
45
|
+
}: {
|
|
46
|
+
appId: string;
|
|
47
|
+
appSecret: string;
|
|
48
|
+
server: string;
|
|
49
|
+
}): Promise<AccessTokenResponse> {
|
|
50
|
+
const url = new URL("/open/api/v1/token", server);
|
|
51
|
+
|
|
52
|
+
const response = await fetch(url.toString(), {
|
|
53
|
+
method: "POST",
|
|
54
|
+
headers: {
|
|
55
|
+
"Content-Type": "application/json",
|
|
56
|
+
},
|
|
57
|
+
body: JSON.stringify({
|
|
58
|
+
appId,
|
|
59
|
+
appSecret,
|
|
60
|
+
}),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (!response.ok) {
|
|
64
|
+
const text = await response.text();
|
|
65
|
+
throw new Error(
|
|
66
|
+
`Failed to get access token: ${response.status} ${response.statusText} - ${text}`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const data = (await response.json()) as {
|
|
71
|
+
code: number;
|
|
72
|
+
message?: string;
|
|
73
|
+
data?: {
|
|
74
|
+
accessToken: string;
|
|
75
|
+
expiresIn: number;
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
if (data.code !== 0) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`POPO OA API error: ${data.code} - ${data.message || "Unknown error"}`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
accessToken: data.data?.accessToken ?? "",
|
|
87
|
+
expiresIn: data.data?.expiresIn ?? 7200,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Clear cached token for an appId.
|
|
93
|
+
* Useful when token is known to be invalid.
|
|
94
|
+
*/
|
|
95
|
+
export function clearAccessToken(appId: string): void {
|
|
96
|
+
tokenCache.delete(appId);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Clear all cached tokens.
|
|
101
|
+
*/
|
|
102
|
+
export function clearAllAccessTokens(): void {
|
|
103
|
+
tokenCache.clear();
|
|
104
|
+
}
|
package/src/bot.ts
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk";
|
|
2
|
+
import {
|
|
3
|
+
recordPendingHistoryEntryIfEnabled,
|
|
4
|
+
clearHistoryEntriesIfEnabled,
|
|
5
|
+
DEFAULT_DM_HISTORY_LIMIT,
|
|
6
|
+
type HistoryEntry,
|
|
7
|
+
} from "openclaw/plugin-sdk";
|
|
8
|
+
import type { PopoOaConfig, PopoOaXmlMessage } from "./types.js";
|
|
9
|
+
import { getPopoOaRuntime } from "./runtime.js";
|
|
10
|
+
import { isUserAllowed, getDmSystemPrompt, isDmEnabled } from "./policy.js";
|
|
11
|
+
import { createPopoOaReplyDispatcher } from "./reply-dispatcher.js";
|
|
12
|
+
|
|
13
|
+
export interface PopoOaMessageEvent {
|
|
14
|
+
xml: PopoOaXmlMessage;
|
|
15
|
+
rawBody: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Parse XML message from POPO OA webhook.
|
|
20
|
+
*/
|
|
21
|
+
export function parsePopoOaXmlMessage(xml: string): PopoOaXmlMessage {
|
|
22
|
+
// Simple XML parsing using regex (for basic structure)
|
|
23
|
+
// In production, consider using a proper XML parser like fast-xml-parser
|
|
24
|
+
const extract = (tag: string): string | undefined => {
|
|
25
|
+
const match = xml.match(new RegExp(`<${tag}><!\\[CDATA\\[(.*?)\\]\\]></${tag}>`));
|
|
26
|
+
return match?.[1];
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const extractRaw = (tag: string): string | undefined => {
|
|
30
|
+
const match = xml.match(new RegExp(`<${tag}>(.*?)</${tag}>`));
|
|
31
|
+
return match?.[1];
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const msgType = (extract("MsgType") || "text") as PopoOaXmlMessage["MsgType"];
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
ToUserName: extract("ToUserName") || "",
|
|
38
|
+
FromUserName: extract("FromUserName") || "",
|
|
39
|
+
CreateTime: extractRaw("CreateTime") || String(Date.now()),
|
|
40
|
+
MsgType: msgType,
|
|
41
|
+
MsgId: extractRaw("MsgId"),
|
|
42
|
+
Content: extract("Content"),
|
|
43
|
+
PicUrl: extract("PicUrl"),
|
|
44
|
+
MediaId: extract("MediaId"),
|
|
45
|
+
Event: extract("Event") as PopoOaXmlMessage["Event"],
|
|
46
|
+
EventKey: extract("EventKey"),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Build passive reply XML for webhook response.
|
|
52
|
+
* This allows immediate reply without making a separate API call.
|
|
53
|
+
*/
|
|
54
|
+
export function buildPassiveReply(params: {
|
|
55
|
+
toUser: string;
|
|
56
|
+
fromUser: string;
|
|
57
|
+
content: string;
|
|
58
|
+
}): string {
|
|
59
|
+
const { toUser, fromUser, content } = params;
|
|
60
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
61
|
+
|
|
62
|
+
return `<xml>
|
|
63
|
+
<ToUserName><![CDATA[${toUser}]]></ToUserName>
|
|
64
|
+
<FromUserName><![CDATA[${fromUser}]]></FromUserName>
|
|
65
|
+
<CreateTime>${timestamp}</CreateTime>
|
|
66
|
+
<MsgType><![CDATA[text]]></MsgType>
|
|
67
|
+
<Content><![CDATA[${content}]]></Content>
|
|
68
|
+
</xml>`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Parse POPO OA message event into normalized format.
|
|
73
|
+
*/
|
|
74
|
+
export function parsePopoOaMessageEvent(event: PopoOaMessageEvent): {
|
|
75
|
+
openid: string;
|
|
76
|
+
serviceAccountId: string;
|
|
77
|
+
messageId?: string;
|
|
78
|
+
content: string;
|
|
79
|
+
msgType: string;
|
|
80
|
+
isEvent: boolean;
|
|
81
|
+
eventType?: string;
|
|
82
|
+
eventKey?: string;
|
|
83
|
+
timestamp: number;
|
|
84
|
+
} {
|
|
85
|
+
const { xml } = event;
|
|
86
|
+
const isEvent = xml.MsgType === "event";
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
openid: xml.FromUserName,
|
|
90
|
+
serviceAccountId: xml.ToUserName,
|
|
91
|
+
messageId: xml.MsgId,
|
|
92
|
+
content: xml.Content || "",
|
|
93
|
+
msgType: xml.MsgType,
|
|
94
|
+
isEvent,
|
|
95
|
+
eventType: xml.Event,
|
|
96
|
+
eventKey: xml.EventKey,
|
|
97
|
+
timestamp: parseInt(xml.CreateTime, 10) || Date.now(),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Handle incoming POPO OA message.
|
|
103
|
+
*/
|
|
104
|
+
export async function handlePopoOaMessage(params: {
|
|
105
|
+
cfg: ClawdbotConfig;
|
|
106
|
+
event: PopoOaMessageEvent;
|
|
107
|
+
runtime?: RuntimeEnv;
|
|
108
|
+
approvedUsers: Set<string>;
|
|
109
|
+
chatHistories?: Map<string, HistoryEntry[]>;
|
|
110
|
+
}): Promise<string | null> {
|
|
111
|
+
const { cfg, event, runtime, approvedUsers, chatHistories } = params;
|
|
112
|
+
const popoCfg = cfg.channels?.["popo-oa"] as PopoOaConfig | undefined;
|
|
113
|
+
const log = runtime?.log ?? console.log;
|
|
114
|
+
const error = runtime?.error ?? console.error;
|
|
115
|
+
|
|
116
|
+
if (!popoCfg) {
|
|
117
|
+
error("popo-oa: configuration not found");
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const ctx = parsePopoOaMessageEvent(event);
|
|
122
|
+
const openid = ctx.openid;
|
|
123
|
+
|
|
124
|
+
// Skip event messages (subscribe/unsubscribe etc)
|
|
125
|
+
if (ctx.isEvent) {
|
|
126
|
+
log(`popo-oa: received event ${ctx.eventType} from ${openid}`);
|
|
127
|
+
// Handle subscribe event - auto-approve user
|
|
128
|
+
if (ctx.eventType === "subscribe") {
|
|
129
|
+
approvedUsers.add(openid);
|
|
130
|
+
log(`popo-oa: auto-approved user ${openid} on subscribe`);
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Skip non-text messages for now
|
|
136
|
+
if (ctx.msgType !== "text") {
|
|
137
|
+
log(`popo-oa: skipping non-text message type ${ctx.msgType}`);
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
log(`popo-oa: received message from ${openid}: ${ctx.content.slice(0, 50)}`);
|
|
142
|
+
|
|
143
|
+
// Check if user is allowed
|
|
144
|
+
if (!isUserAllowed({ cfg: popoCfg, openid, approvedUsers })) {
|
|
145
|
+
log(`popo-oa: user ${openid} not allowed (policy=${popoCfg.dmPolicy})`);
|
|
146
|
+
return "您还没有被授权使用此服务号。请联系管理员添加您的账号。";
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Check if DM is enabled for this user
|
|
150
|
+
if (!isDmEnabled({ cfg: popoCfg, openid })) {
|
|
151
|
+
log(`popo-oa: DM disabled for ${openid}`);
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const core = getPopoOaRuntime();
|
|
157
|
+
if (!core) {
|
|
158
|
+
throw new Error("POPO OA runtime not initialized");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const historyLimit = Math.max(
|
|
162
|
+
0,
|
|
163
|
+
popoCfg.dmHistoryLimit ?? popoCfg.historyLimit ?? DEFAULT_DM_HISTORY_LIMIT
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
const route = core.channel.routing.resolveAgentRoute({
|
|
167
|
+
cfg,
|
|
168
|
+
channel: "popo-oa",
|
|
169
|
+
peer: {
|
|
170
|
+
kind: "dm",
|
|
171
|
+
id: openid,
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const preview = ctx.content.replace(/\s+/g, " ").slice(0, 160);
|
|
176
|
+
core.system.enqueueSystemEvent(`POPO 服务号消息 from ${openid}: ${preview}`, {
|
|
177
|
+
sessionKey: route.sessionKey,
|
|
178
|
+
contextKey: `popo-oa:message:${openid}:${ctx.messageId || Date.now()}`,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
|
|
182
|
+
|
|
183
|
+
// Build message body with system prompt if configured
|
|
184
|
+
let messageBody = ctx.content;
|
|
185
|
+
const systemPrompt = getDmSystemPrompt({ cfg: popoCfg, openid });
|
|
186
|
+
if (systemPrompt) {
|
|
187
|
+
messageBody = `${systemPrompt}\n\n---\n\n${messageBody}`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const body = core.channel.reply.formatAgentEnvelope({
|
|
191
|
+
channel: "POPO 服务号",
|
|
192
|
+
from: openid,
|
|
193
|
+
timestamp: new Date(ctx.timestamp),
|
|
194
|
+
envelope: envelopeOptions,
|
|
195
|
+
body: messageBody,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
let combinedBody = body;
|
|
199
|
+
const historyKey = openid;
|
|
200
|
+
|
|
201
|
+
if (historyKey && chatHistories) {
|
|
202
|
+
combinedBody = core.channel.reply.buildPendingHistoryContextFromMap({
|
|
203
|
+
historyMap: chatHistories,
|
|
204
|
+
historyKey,
|
|
205
|
+
limit: historyLimit,
|
|
206
|
+
currentMessage: combinedBody,
|
|
207
|
+
formatEntry: (entry) =>
|
|
208
|
+
core.channel.reply.formatAgentEnvelope({
|
|
209
|
+
channel: "POPO 服务号",
|
|
210
|
+
from: entry.sender,
|
|
211
|
+
timestamp: entry.timestamp,
|
|
212
|
+
body: entry.body,
|
|
213
|
+
envelope: envelopeOptions,
|
|
214
|
+
}),
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
219
|
+
Body: combinedBody,
|
|
220
|
+
RawBody: ctx.content,
|
|
221
|
+
CommandBody: ctx.content,
|
|
222
|
+
From: `popo-oa:${openid}`,
|
|
223
|
+
To: `user:${openid}`,
|
|
224
|
+
SessionKey: route.sessionKey,
|
|
225
|
+
AccountId: route.accountId,
|
|
226
|
+
ChatType: "direct",
|
|
227
|
+
SenderName: openid,
|
|
228
|
+
SenderId: openid,
|
|
229
|
+
Provider: "popo-oa" as const,
|
|
230
|
+
Surface: "popo-oa" as const,
|
|
231
|
+
MessageSid: ctx.messageId || String(Date.now()),
|
|
232
|
+
Timestamp: ctx.timestamp,
|
|
233
|
+
WasMentioned: false,
|
|
234
|
+
CommandAuthorized: true,
|
|
235
|
+
OriginatingChannel: "popo-oa" as const,
|
|
236
|
+
OriginatingTo: `user:${openid}`,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const { dispatcher, replyOptions, markDispatchIdle } = createPopoOaReplyDispatcher({
|
|
240
|
+
cfg,
|
|
241
|
+
agentId: route.agentId,
|
|
242
|
+
runtime: runtime as RuntimeEnv,
|
|
243
|
+
openid,
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
log(`popo-oa: dispatching to agent (session=${route.sessionKey})`);
|
|
247
|
+
|
|
248
|
+
const { queuedFinal, counts } = await core.channel.reply.dispatchReplyFromConfig({
|
|
249
|
+
ctx: ctxPayload,
|
|
250
|
+
cfg,
|
|
251
|
+
dispatcher,
|
|
252
|
+
replyOptions,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
markDispatchIdle();
|
|
256
|
+
|
|
257
|
+
// Record history entry
|
|
258
|
+
if (chatHistories && historyKey) {
|
|
259
|
+
recordPendingHistoryEntryIfEnabled({
|
|
260
|
+
historyMap: chatHistories,
|
|
261
|
+
historyKey,
|
|
262
|
+
entry: {
|
|
263
|
+
sender: openid,
|
|
264
|
+
body: ctx.content,
|
|
265
|
+
timestamp: new Date(ctx.timestamp),
|
|
266
|
+
},
|
|
267
|
+
limit: historyLimit,
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
clearHistoryEntriesIfEnabled({
|
|
271
|
+
historyMap: chatHistories,
|
|
272
|
+
historyKey,
|
|
273
|
+
limit: historyLimit,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
log(`popo-oa: dispatch complete (queuedFinal=${queuedFinal}, replies=${counts.final})`);
|
|
278
|
+
|
|
279
|
+
// Return null - replies will be sent via outbound adapter
|
|
280
|
+
return null;
|
|
281
|
+
} catch (err) {
|
|
282
|
+
error(`popo-oa: failed to dispatch message: ${String(err)}`);
|
|
283
|
+
return "抱歉,处理消息时出现了错误。请稍后重试。";
|
|
284
|
+
}
|
|
285
|
+
}
|