@shenhh/popo-native 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 +53 -0
- package/package.json +65 -0
- package/src/accounts.ts +52 -0
- package/src/auth.ts +86 -0
- package/src/bot.ts +363 -0
- package/src/cards.ts +217 -0
- package/src/channel.ts +709 -0
- package/src/client.ts +118 -0
- package/src/config-schema.ts +89 -0
- package/src/crypto.ts +66 -0
- package/src/media.ts +603 -0
- package/src/monitor.ts +261 -0
- package/src/outbound.ts +138 -0
- package/src/policy.ts +93 -0
- package/src/probe.ts +29 -0
- package/src/reply-dispatcher.ts +126 -0
- package/src/runtime.ts +14 -0
- package/src/send.ts +424 -0
- package/src/subscription.ts +171 -0
- package/src/targets.ts +68 -0
- package/src/team.ts +460 -0
- package/src/types.ts +104 -0
package/index.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
+
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
|
|
3
|
+
import { popoNativePlugin } from "./src/channel.js";
|
|
4
|
+
import { setPopoNativeRuntime } from "./src/runtime.js";
|
|
5
|
+
|
|
6
|
+
export { monitorPopoNativeProvider } from "./src/monitor.js";
|
|
7
|
+
export {
|
|
8
|
+
sendMessagePopoNative,
|
|
9
|
+
sendCardPopoNative,
|
|
10
|
+
createStreamCardPopoNative,
|
|
11
|
+
updateStreamCardPopoNative,
|
|
12
|
+
updateInstructionVariableOptions,
|
|
13
|
+
} from "./src/send.js";
|
|
14
|
+
export {
|
|
15
|
+
uploadImagePopoNative,
|
|
16
|
+
sendImagePopoNative,
|
|
17
|
+
sendFilePopoNative,
|
|
18
|
+
sendMediaPopoNative,
|
|
19
|
+
recallMessagePopoNative,
|
|
20
|
+
getMessageReadAckPopoNative,
|
|
21
|
+
configureCardCallbackPopoNative,
|
|
22
|
+
downloadMessageFilePopoNative,
|
|
23
|
+
registerFileUploadPopoNative,
|
|
24
|
+
} from "./src/media.js";
|
|
25
|
+
export { probePopoNative } from "./src/probe.js";
|
|
26
|
+
export { popoNativePlugin } from "./src/channel.js";
|
|
27
|
+
export {
|
|
28
|
+
createTeam,
|
|
29
|
+
inviteToTeam,
|
|
30
|
+
dropTeam,
|
|
31
|
+
getTeamMembers,
|
|
32
|
+
getTeamInfo,
|
|
33
|
+
updateTeamInfo,
|
|
34
|
+
updateTeamManagement,
|
|
35
|
+
} from "./src/team.js";
|
|
36
|
+
export {
|
|
37
|
+
configureSubscription,
|
|
38
|
+
removeSubscription,
|
|
39
|
+
listSubscriptions,
|
|
40
|
+
} from "./src/subscription.js";
|
|
41
|
+
|
|
42
|
+
const plugin = {
|
|
43
|
+
id: "popo-native",
|
|
44
|
+
name: "POPO Native",
|
|
45
|
+
description: "POPO Native API channel plugin",
|
|
46
|
+
configSchema: emptyPluginConfigSchema(),
|
|
47
|
+
register(api: OpenClawPluginApi) {
|
|
48
|
+
setPopoNativeRuntime(api.runtime);
|
|
49
|
+
api.registerChannel({ plugin: popoNativePlugin });
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export default plugin;
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@shenhh/popo-native",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "OpenClaw POPO Native API 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-native.git"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"openclaw",
|
|
26
|
+
"popo",
|
|
27
|
+
"netease",
|
|
28
|
+
"chatbot",
|
|
29
|
+
"ai",
|
|
30
|
+
"claude",
|
|
31
|
+
"native-api"
|
|
32
|
+
],
|
|
33
|
+
"openclaw": {
|
|
34
|
+
"extensions": [
|
|
35
|
+
"./index.ts"
|
|
36
|
+
],
|
|
37
|
+
"channel": {
|
|
38
|
+
"id": "popo-native",
|
|
39
|
+
"label": "POPO Native",
|
|
40
|
+
"selectionLabel": "POPO Native (网易)",
|
|
41
|
+
"docsPath": "/channels/popo-native",
|
|
42
|
+
"docsLabel": "popo-native",
|
|
43
|
+
"blurb": "POPO enterprise messaging via Native API.",
|
|
44
|
+
"aliases": [],
|
|
45
|
+
"order": 81
|
|
46
|
+
},
|
|
47
|
+
"install": {
|
|
48
|
+
"npmSpec": "@shenhh/clawdbot-popo-native",
|
|
49
|
+
"localPath": ".",
|
|
50
|
+
"defaultChoice": "npm"
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"zod": "^4.3.6"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@types/node": "^25.0.10",
|
|
58
|
+
"openclaw": "2026.1.29",
|
|
59
|
+
"tsx": "^4.21.0",
|
|
60
|
+
"typescript": "^5.7.0"
|
|
61
|
+
},
|
|
62
|
+
"peerDependencies": {
|
|
63
|
+
"openclaw": ">=2026.1.29"
|
|
64
|
+
}
|
|
65
|
+
}
|
package/src/accounts.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ClawdbotConfig } from "openclaw/plugin-sdk";
|
|
2
|
+
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk";
|
|
3
|
+
import type { PopoNativeConfig, ResolvedPopoNativeAccount } from "./types.js";
|
|
4
|
+
|
|
5
|
+
export function resolvePopoNativeCredentials(cfg?: PopoNativeConfig): {
|
|
6
|
+
appId: string;
|
|
7
|
+
appSecret: string;
|
|
8
|
+
token?: string;
|
|
9
|
+
aesKey?: string;
|
|
10
|
+
server: string;
|
|
11
|
+
} | null {
|
|
12
|
+
const appId = cfg?.appId?.trim();
|
|
13
|
+
const appSecret = cfg?.appSecret?.trim();
|
|
14
|
+
if (!appId || !appSecret) return null;
|
|
15
|
+
return {
|
|
16
|
+
appId,
|
|
17
|
+
appSecret,
|
|
18
|
+
token: cfg?.token?.trim() || undefined,
|
|
19
|
+
aesKey: cfg?.aesKey?.trim() || undefined,
|
|
20
|
+
server: cfg?.server ?? "https://open.popo.netease.com",
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function resolvePopoNativeAccount(params: {
|
|
25
|
+
cfg: ClawdbotConfig;
|
|
26
|
+
accountId?: string | null;
|
|
27
|
+
}): ResolvedPopoNativeAccount {
|
|
28
|
+
const popoCfg = params.cfg.channels?.["popo-native"] as PopoNativeConfig | undefined;
|
|
29
|
+
const enabled = popoCfg?.enabled !== false;
|
|
30
|
+
const creds = resolvePopoNativeCredentials(popoCfg);
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
accountId: params.accountId?.trim() || DEFAULT_ACCOUNT_ID,
|
|
34
|
+
enabled,
|
|
35
|
+
configured: Boolean(creds),
|
|
36
|
+
appId: creds?.appId,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function listPopoNativeAccountIds(_cfg: ClawdbotConfig): string[] {
|
|
41
|
+
return [DEFAULT_ACCOUNT_ID];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function resolveDefaultPopoNativeAccountId(_cfg: ClawdbotConfig): string {
|
|
45
|
+
return DEFAULT_ACCOUNT_ID;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function listEnabledPopoNativeAccounts(cfg: ClawdbotConfig): ResolvedPopoNativeAccount[] {
|
|
49
|
+
return listPopoNativeAccountIds(cfg)
|
|
50
|
+
.map((accountId) => resolvePopoNativeAccount({ cfg, accountId }))
|
|
51
|
+
.filter((account) => account.enabled && account.configured);
|
|
52
|
+
}
|
package/src/auth.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { PopoNativeConfig, PopoNativeToken } from "./types.js";
|
|
2
|
+
import { resolvePopoNativeCredentials } from "./accounts.js";
|
|
3
|
+
|
|
4
|
+
// Token cache
|
|
5
|
+
let cachedToken: PopoNativeToken | null = null;
|
|
6
|
+
let cachedAppId: string | null = null;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Get a valid access token, fetching a new one if necessary.
|
|
10
|
+
* Native API has 24h expiry and no refresh token.
|
|
11
|
+
*/
|
|
12
|
+
export async function getAccessToken(cfg: PopoNativeConfig): Promise<string> {
|
|
13
|
+
const creds = resolvePopoNativeCredentials(cfg);
|
|
14
|
+
if (!creds) {
|
|
15
|
+
throw new Error("POPO Native credentials not configured (appId, appSecret required)");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const now = Date.now();
|
|
19
|
+
|
|
20
|
+
// Check if we have a valid cached token (with 1 minute buffer)
|
|
21
|
+
if (
|
|
22
|
+
cachedToken &&
|
|
23
|
+
cachedAppId === creds.appId &&
|
|
24
|
+
cachedToken.accessExpiredAt > now + 60000
|
|
25
|
+
) {
|
|
26
|
+
return cachedToken.openAccessToken;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Get a new token
|
|
30
|
+
cachedToken = await fetchNewToken(cfg);
|
|
31
|
+
cachedAppId = creds.appId;
|
|
32
|
+
return cachedToken.openAccessToken;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Fetch a new token using appId and appSecret.
|
|
37
|
+
* Native API: POST /open-apis/token
|
|
38
|
+
* Response: { openAccessToken, accessExpiredAt }
|
|
39
|
+
*/
|
|
40
|
+
async function fetchNewToken(cfg: PopoNativeConfig): Promise<PopoNativeToken> {
|
|
41
|
+
const creds = resolvePopoNativeCredentials(cfg);
|
|
42
|
+
if (!creds) {
|
|
43
|
+
throw new Error("POPO Native credentials not configured");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const response = await fetch(`${creds.server}/open-apis/token`, {
|
|
47
|
+
method: "POST",
|
|
48
|
+
headers: {
|
|
49
|
+
"Content-Type": "application/json",
|
|
50
|
+
},
|
|
51
|
+
body: JSON.stringify({
|
|
52
|
+
appId: creds.appId,
|
|
53
|
+
appSecret: creds.appSecret,
|
|
54
|
+
}),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (!response.ok) {
|
|
58
|
+
throw new Error(`POPO Native token request failed: ${response.status} ${response.statusText}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const data = (await response.json()) as {
|
|
62
|
+
errcode?: number;
|
|
63
|
+
errmsg?: string;
|
|
64
|
+
data?: {
|
|
65
|
+
openAccessToken: string;
|
|
66
|
+
accessExpiredAt: number;
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
if (data.errcode !== 0 || !data.data) {
|
|
71
|
+
throw new Error(`POPO Native token request failed: ${data.errmsg || "unknown error"}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
openAccessToken: data.data.openAccessToken,
|
|
76
|
+
accessExpiredAt: data.data.accessExpiredAt,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Clear the token cache.
|
|
82
|
+
*/
|
|
83
|
+
export function clearTokenCache() {
|
|
84
|
+
cachedToken = null;
|
|
85
|
+
cachedAppId = null;
|
|
86
|
+
}
|
package/src/bot.ts
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk";
|
|
2
|
+
import {
|
|
3
|
+
buildPendingHistoryContextFromMap,
|
|
4
|
+
recordPendingHistoryEntryIfEnabled,
|
|
5
|
+
clearHistoryEntriesIfEnabled,
|
|
6
|
+
DEFAULT_GROUP_HISTORY_LIMIT,
|
|
7
|
+
type HistoryEntry,
|
|
8
|
+
} from "openclaw/plugin-sdk";
|
|
9
|
+
import type { PopoNativeConfig, PopoNativeMediaInfo } from "./types.js";
|
|
10
|
+
import { getPopoNativeRuntime } from "./runtime.js";
|
|
11
|
+
import {
|
|
12
|
+
resolvePopoNativeGroupConfig,
|
|
13
|
+
resolvePopoNativeReplyPolicy,
|
|
14
|
+
resolvePopoNativeAllowlistMatch,
|
|
15
|
+
isPopoNativeGroupAllowed,
|
|
16
|
+
} from "./policy.js";
|
|
17
|
+
import { createPopoNativeReplyDispatcher } from "./reply-dispatcher.js";
|
|
18
|
+
import { downloadFilePopoNative } from "./media.js";
|
|
19
|
+
import type { PopoNativeMsgSendEvent } from "./types.js";
|
|
20
|
+
|
|
21
|
+
export type PopoNativeMessageEvent = PopoNativeMsgSendEvent;
|
|
22
|
+
|
|
23
|
+
function parseMessageContent(notify: string, msgType?: number): string {
|
|
24
|
+
// msgType: 1=text, 142=video, 171=file, 161=merge, 211=quote
|
|
25
|
+
if (msgType === 1 || !msgType) {
|
|
26
|
+
return notify;
|
|
27
|
+
}
|
|
28
|
+
return notify;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Infer placeholder text based on message type.
|
|
33
|
+
*/
|
|
34
|
+
function inferPlaceholder(msgType?: number): string {
|
|
35
|
+
switch (msgType) {
|
|
36
|
+
case 2: // image
|
|
37
|
+
return "<media:image>";
|
|
38
|
+
case 171: // file
|
|
39
|
+
return "<media:document>";
|
|
40
|
+
case 142: // video
|
|
41
|
+
return "<media:video>";
|
|
42
|
+
case 3: // audio
|
|
43
|
+
return "<media:audio>";
|
|
44
|
+
default:
|
|
45
|
+
return "";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Resolve media from a POPO message, downloading and saving to disk.
|
|
51
|
+
*/
|
|
52
|
+
async function resolvePopoNativeMediaList(params: {
|
|
53
|
+
cfg: ClawdbotConfig;
|
|
54
|
+
event: PopoNativeMessageEvent;
|
|
55
|
+
maxBytes: number;
|
|
56
|
+
log?: (msg: string) => void;
|
|
57
|
+
}): Promise<PopoNativeMediaInfo[]> {
|
|
58
|
+
const { cfg, event, maxBytes, log } = params;
|
|
59
|
+
const { msgType, fileInfo, videoInfo } = event.eventData;
|
|
60
|
+
|
|
61
|
+
// Only process media message types
|
|
62
|
+
// 2=image, 142=video, 171=file
|
|
63
|
+
const fileId = fileInfo?.fileId || videoInfo?.videoId;
|
|
64
|
+
if (!fileId || (msgType !== 2 && msgType !== 142 && msgType !== 171)) {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const out: PopoNativeMediaInfo[] = [];
|
|
69
|
+
const core = getPopoNativeRuntime();
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const result = await downloadFilePopoNative({
|
|
73
|
+
cfg,
|
|
74
|
+
fileId,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
let contentType = result.contentType;
|
|
78
|
+
if (!contentType) {
|
|
79
|
+
contentType = await core.media.detectMime({ buffer: result.buffer });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const saved = await core.channel.media.saveMediaBuffer(
|
|
83
|
+
result.buffer,
|
|
84
|
+
contentType,
|
|
85
|
+
"inbound",
|
|
86
|
+
maxBytes
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
out.push({
|
|
90
|
+
path: saved.path,
|
|
91
|
+
contentType: saved.contentType,
|
|
92
|
+
placeholder: inferPlaceholder(msgType),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
log?.(`popo-native: downloaded media (type=${msgType}), saved to ${saved.path}`);
|
|
96
|
+
} catch (err) {
|
|
97
|
+
log?.(`popo-native: failed to download media: ${String(err)}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Build media payload for inbound context.
|
|
105
|
+
*/
|
|
106
|
+
function buildPopoNativeMediaPayload(
|
|
107
|
+
mediaList: PopoNativeMediaInfo[]
|
|
108
|
+
): {
|
|
109
|
+
MediaPath?: string;
|
|
110
|
+
MediaType?: string;
|
|
111
|
+
MediaUrl?: string;
|
|
112
|
+
MediaPaths?: string[];
|
|
113
|
+
MediaUrls?: string[];
|
|
114
|
+
MediaTypes?: string[];
|
|
115
|
+
} {
|
|
116
|
+
const first = mediaList[0];
|
|
117
|
+
const mediaPaths = mediaList.map((media) => media.path);
|
|
118
|
+
const mediaTypes = mediaList.map((media) => media.contentType).filter(Boolean) as string[];
|
|
119
|
+
return {
|
|
120
|
+
MediaPath: first?.path,
|
|
121
|
+
MediaType: first?.contentType,
|
|
122
|
+
MediaUrl: first?.path,
|
|
123
|
+
MediaPaths: mediaPaths.length > 0 ? mediaPaths : undefined,
|
|
124
|
+
MediaUrls: mediaPaths.length > 0 ? mediaPaths : undefined,
|
|
125
|
+
MediaTypes: mediaTypes.length > 0 ? mediaTypes : undefined,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function parsePopoNativeMessageEvent(event: PopoNativeMessageEvent): {
|
|
130
|
+
sessionId: string;
|
|
131
|
+
messageId: string;
|
|
132
|
+
senderId: string;
|
|
133
|
+
senderEmail: string;
|
|
134
|
+
senderName?: string;
|
|
135
|
+
chatType: "p2p" | "group";
|
|
136
|
+
content: string;
|
|
137
|
+
contentType: string;
|
|
138
|
+
fileId?: string;
|
|
139
|
+
} {
|
|
140
|
+
const { eventData } = event;
|
|
141
|
+
const isGroup = eventData.sessionType === 3;
|
|
142
|
+
const content = parseMessageContent(eventData.notify, eventData.msgType);
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
sessionId: eventData.sessionId,
|
|
146
|
+
messageId: eventData.uuid,
|
|
147
|
+
senderId: eventData.from,
|
|
148
|
+
senderEmail: eventData.from,
|
|
149
|
+
chatType: isGroup ? "group" : "p2p",
|
|
150
|
+
content,
|
|
151
|
+
contentType: String(eventData.msgType ?? "1"),
|
|
152
|
+
fileId: eventData.fileInfo?.fileId || eventData.videoInfo?.videoId,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export async function handlePopoNativeMessage(params: {
|
|
157
|
+
cfg: ClawdbotConfig;
|
|
158
|
+
event: PopoNativeMessageEvent;
|
|
159
|
+
runtime?: RuntimeEnv;
|
|
160
|
+
chatHistories?: Map<string, HistoryEntry[]>;
|
|
161
|
+
}): Promise<void> {
|
|
162
|
+
const { cfg, event, runtime, chatHistories } = params;
|
|
163
|
+
const popoCfg = cfg.channels?.["popo-native"] as PopoNativeConfig | undefined;
|
|
164
|
+
const log = runtime?.log ?? console.log;
|
|
165
|
+
const error = runtime?.error ?? console.error;
|
|
166
|
+
|
|
167
|
+
const ctx = parsePopoNativeMessageEvent(event);
|
|
168
|
+
const isGroup = ctx.chatType === "group";
|
|
169
|
+
|
|
170
|
+
log(`popo-native: received message from ${ctx.senderEmail} in ${ctx.sessionId} (${ctx.chatType})`);
|
|
171
|
+
|
|
172
|
+
const historyLimit = Math.max(
|
|
173
|
+
0,
|
|
174
|
+
popoCfg?.historyLimit ?? cfg.messages?.groupChat?.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
if (isGroup) {
|
|
178
|
+
const groupPolicy = popoCfg?.groupPolicy ?? "open";
|
|
179
|
+
const groupAllowFrom = popoCfg?.groupAllowFrom ?? [];
|
|
180
|
+
const groupConfig = resolvePopoNativeGroupConfig({ cfg: popoCfg, groupId: ctx.sessionId });
|
|
181
|
+
|
|
182
|
+
// Check if this GROUP is allowed
|
|
183
|
+
const groupAllowed = isPopoNativeGroupAllowed({
|
|
184
|
+
groupPolicy,
|
|
185
|
+
allowFrom: groupAllowFrom,
|
|
186
|
+
senderId: ctx.sessionId,
|
|
187
|
+
senderName: undefined,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
if (!groupAllowed) {
|
|
191
|
+
log(`popo-native: group ${ctx.sessionId} not in allowlist`);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Additional sender-level allowlist check if group has specific allowFrom config
|
|
196
|
+
const senderAllowFrom = groupConfig?.allowFrom ?? [];
|
|
197
|
+
if (senderAllowFrom.length > 0) {
|
|
198
|
+
const senderAllowed = isPopoNativeGroupAllowed({
|
|
199
|
+
groupPolicy: "allowlist",
|
|
200
|
+
allowFrom: senderAllowFrom,
|
|
201
|
+
senderId: ctx.senderEmail,
|
|
202
|
+
senderName: ctx.senderName,
|
|
203
|
+
});
|
|
204
|
+
if (!senderAllowed) {
|
|
205
|
+
log(`popo-native: sender ${ctx.senderEmail} not in group ${ctx.sessionId} sender allowlist`);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Check @ mention requirement
|
|
211
|
+
const requireMention = groupConfig?.requireMention ?? popoCfg?.requireMention ?? true;
|
|
212
|
+
if (requireMention && event.eventData.atType !== 1 && event.eventData.atType !== 2) {
|
|
213
|
+
log(`popo-native: message not @ mentioning bot, skipping`);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
} else {
|
|
217
|
+
const dmPolicy = popoCfg?.dmPolicy ?? "pairing";
|
|
218
|
+
const allowFrom = popoCfg?.allowFrom ?? [];
|
|
219
|
+
|
|
220
|
+
if (dmPolicy === "allowlist") {
|
|
221
|
+
const match = resolvePopoNativeAllowlistMatch({
|
|
222
|
+
allowFrom,
|
|
223
|
+
senderId: ctx.senderEmail,
|
|
224
|
+
});
|
|
225
|
+
if (!match.allowed) {
|
|
226
|
+
log(`popo-native: sender ${ctx.senderEmail} not in DM allowlist`);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
const core = getPopoNativeRuntime();
|
|
234
|
+
|
|
235
|
+
const popoFrom = `popo-native:${ctx.senderEmail}`;
|
|
236
|
+
const popoTo = isGroup ? `group:${ctx.sessionId}` : `user:${ctx.senderEmail}`;
|
|
237
|
+
|
|
238
|
+
const route = core.channel.routing.resolveAgentRoute({
|
|
239
|
+
cfg,
|
|
240
|
+
channel: "popo-native",
|
|
241
|
+
peer: {
|
|
242
|
+
kind: isGroup ? "group" : "dm",
|
|
243
|
+
id: ctx.sessionId,
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const preview = ctx.content.replace(/\s+/g, " ").slice(0, 160);
|
|
248
|
+
const inboundLabel = isGroup
|
|
249
|
+
? `POPO Native message in group ${ctx.sessionId}`
|
|
250
|
+
: `POPO Native DM from ${ctx.senderEmail}`;
|
|
251
|
+
|
|
252
|
+
core.system.enqueueSystemEvent(`${inboundLabel}: ${preview}`, {
|
|
253
|
+
sessionKey: route.sessionKey,
|
|
254
|
+
contextKey: `popo-native:message:${ctx.sessionId}:${ctx.messageId}`,
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// Resolve media from message
|
|
258
|
+
const mediaMaxBytes = (popoCfg?.mediaMaxMb ?? 20) * 1024 * 1024;
|
|
259
|
+
const mediaList = await resolvePopoNativeMediaList({
|
|
260
|
+
cfg,
|
|
261
|
+
event,
|
|
262
|
+
maxBytes: mediaMaxBytes,
|
|
263
|
+
log,
|
|
264
|
+
});
|
|
265
|
+
const mediaPayload = buildPopoNativeMediaPayload(mediaList);
|
|
266
|
+
|
|
267
|
+
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
|
|
268
|
+
|
|
269
|
+
// Build message body
|
|
270
|
+
let messageBody = ctx.content;
|
|
271
|
+
const speaker = ctx.senderName ?? ctx.senderEmail;
|
|
272
|
+
messageBody = `${speaker}: ${messageBody}`;
|
|
273
|
+
|
|
274
|
+
// Apply global system prompt if configured
|
|
275
|
+
const systemPrompt = popoCfg?.systemPrompt?.trim();
|
|
276
|
+
if (systemPrompt) {
|
|
277
|
+
messageBody = `${systemPrompt}\n\n---\n\n${messageBody}`;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const envelopeFrom = isGroup ? `${ctx.sessionId}:${ctx.senderEmail}` : ctx.senderEmail;
|
|
281
|
+
|
|
282
|
+
const body = core.channel.reply.formatAgentEnvelope({
|
|
283
|
+
channel: "POPO Native",
|
|
284
|
+
from: envelopeFrom,
|
|
285
|
+
timestamp: new Date(),
|
|
286
|
+
envelope: envelopeOptions,
|
|
287
|
+
body: messageBody,
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
let combinedBody = body;
|
|
291
|
+
const historyKey = isGroup ? ctx.sessionId : undefined;
|
|
292
|
+
|
|
293
|
+
if (isGroup && historyKey && chatHistories) {
|
|
294
|
+
combinedBody = buildPendingHistoryContextFromMap({
|
|
295
|
+
historyMap: chatHistories,
|
|
296
|
+
historyKey,
|
|
297
|
+
limit: historyLimit,
|
|
298
|
+
currentMessage: combinedBody,
|
|
299
|
+
formatEntry: (entry) =>
|
|
300
|
+
core.channel.reply.formatAgentEnvelope({
|
|
301
|
+
channel: "POPO Native",
|
|
302
|
+
from: `${ctx.sessionId}:${entry.sender}`,
|
|
303
|
+
timestamp: entry.timestamp,
|
|
304
|
+
body: entry.body,
|
|
305
|
+
envelope: envelopeOptions,
|
|
306
|
+
}),
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
311
|
+
Body: combinedBody,
|
|
312
|
+
RawBody: ctx.content,
|
|
313
|
+
CommandBody: ctx.content,
|
|
314
|
+
From: popoFrom,
|
|
315
|
+
To: popoTo,
|
|
316
|
+
SessionKey: route.sessionKey,
|
|
317
|
+
AccountId: route.accountId,
|
|
318
|
+
ChatType: isGroup ? "group" : "direct",
|
|
319
|
+
GroupSubject: isGroup ? ctx.sessionId : undefined,
|
|
320
|
+
SenderName: ctx.senderName ?? ctx.senderEmail,
|
|
321
|
+
SenderId: ctx.senderEmail,
|
|
322
|
+
Provider: "popo-native" as const,
|
|
323
|
+
Surface: "popo-native" as const,
|
|
324
|
+
MessageSid: ctx.messageId,
|
|
325
|
+
Timestamp: Date.now(),
|
|
326
|
+
WasMentioned: isGroup && (event.eventData.atType === 1 || event.eventData.atType === 2),
|
|
327
|
+
CommandAuthorized: true,
|
|
328
|
+
OriginatingChannel: "popo-native" as const,
|
|
329
|
+
OriginatingTo: popoTo,
|
|
330
|
+
...mediaPayload,
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
const { dispatcher, replyOptions, markDispatchIdle } = createPopoNativeReplyDispatcher({
|
|
334
|
+
cfg,
|
|
335
|
+
agentId: route.agentId,
|
|
336
|
+
runtime: runtime as RuntimeEnv,
|
|
337
|
+
sessionId: ctx.sessionId,
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
log(`popo-native: dispatching to agent (session=${route.sessionKey})`);
|
|
341
|
+
|
|
342
|
+
const { queuedFinal, counts } = await core.channel.reply.dispatchReplyFromConfig({
|
|
343
|
+
ctx: ctxPayload,
|
|
344
|
+
cfg,
|
|
345
|
+
dispatcher,
|
|
346
|
+
replyOptions,
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
markDispatchIdle();
|
|
350
|
+
|
|
351
|
+
if (isGroup && historyKey && chatHistories) {
|
|
352
|
+
clearHistoryEntriesIfEnabled({
|
|
353
|
+
historyMap: chatHistories,
|
|
354
|
+
historyKey,
|
|
355
|
+
limit: historyLimit,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
log(`popo-native: dispatch complete (queuedFinal=${queuedFinal}, replies=${counts.final})`);
|
|
360
|
+
} catch (err) {
|
|
361
|
+
error(`popo-native: failed to dispatch message: ${String(err)}`);
|
|
362
|
+
}
|
|
363
|
+
}
|