@imweapp/openclaw-imwe 2026.4.12-alpha.2 → 2026.4.12-alpha.4
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/README.md +36 -20
- package/openclaw.plugin.json +1 -47
- package/package.json +3 -3
- package/proto/PbChatEmoteContent.proto +34 -0
- package/proto/PbChatLocationContent.proto +15 -0
- package/proto/PbChatMsg.proto +4 -2
- package/src/accounts.ts +28 -7
- package/src/api-client.ts +48 -0
- package/src/config-schema.ts +3 -2
- package/src/emote/api.ts +25 -0
- package/src/emote/handle.ts +36 -0
- package/src/emote/index.ts +3 -0
- package/src/monitor.ts +21 -0
- package/src/proto/inbound.codec.ts +116 -0
- package/src/proto/proto-types.ts +32 -0
- package/src/proto/registry.ts +8 -0
- package/src/secret-input.ts +6 -0
- package/src/types.ts +20 -4
package/README.md
CHANGED
|
@@ -7,17 +7,21 @@ imwe 即时通讯渠道插件,AppKey/AppSecret HMAC-SHA256 签名认证,支
|
|
|
7
7
|
|
|
8
8
|
### 单账号
|
|
9
9
|
|
|
10
|
+
凭证写在 `accounts.default` 内,共享配置(`dmPolicy`、`allowFrom`)放在顶层作为默认值。`apiBaseUrl` 可省略,默认使用 `https://im-pre.imweapi.com`。
|
|
11
|
+
|
|
10
12
|
```json
|
|
11
13
|
{
|
|
12
14
|
"channels": {
|
|
13
15
|
"imwe": {
|
|
14
|
-
"
|
|
15
|
-
"appKey": "your-app-key",
|
|
16
|
-
"appSecret": "your-app-secret",
|
|
16
|
+
"defaultAccount": "default",
|
|
17
17
|
"dmPolicy": "open",
|
|
18
|
-
"allowFrom": [
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
"allowFrom": ["*"],
|
|
19
|
+
"accounts": {
|
|
20
|
+
"default": {
|
|
21
|
+
"appKey": "your-app-key",
|
|
22
|
+
"appSecret": "your-app-secret"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
21
25
|
}
|
|
22
26
|
}
|
|
23
27
|
}
|
|
@@ -25,30 +29,42 @@ imwe 即时通讯渠道插件,AppKey/AppSecret HMAC-SHA256 签名认证,支
|
|
|
25
29
|
|
|
26
30
|
### 多账号
|
|
27
31
|
|
|
32
|
+
顶层写共享默认值,每个账号只写凭证。
|
|
33
|
+
|
|
28
34
|
```json
|
|
29
35
|
{
|
|
30
36
|
"channels": {
|
|
31
37
|
"imwe": {
|
|
32
|
-
"apiBaseUrl": "https://api.imwe.example.com",
|
|
33
38
|
"defaultAccount": "default",
|
|
39
|
+
"dmPolicy": "open",
|
|
40
|
+
"allowFrom": ["*"],
|
|
34
41
|
"accounts": {
|
|
35
42
|
"default": {
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"appSecret": "your-app-secret",
|
|
39
|
-
"dmPolicy": "open",
|
|
40
|
-
"allowFrom": [
|
|
41
|
-
"*"
|
|
42
|
-
],
|
|
43
|
+
"appKey": "app-key-1",
|
|
44
|
+
"appSecret": "app-secret-1"
|
|
43
45
|
},
|
|
44
46
|
"bot2": {
|
|
45
|
-
"
|
|
47
|
+
"appKey": "app-key-2",
|
|
48
|
+
"appSecret": "app-secret-2"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 使用 SecretRef 引用环境变量
|
|
57
|
+
|
|
58
|
+
凭证字段支持 SecretRef 对象,通过 `{ source: 'env', provider: 'default', id: '...' }` 引用环境变量:
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"channels": {
|
|
63
|
+
"imwe": {
|
|
64
|
+
"accounts": {
|
|
65
|
+
"default": {
|
|
46
66
|
"appKey": "your-app-key",
|
|
47
|
-
"appSecret": "
|
|
48
|
-
"dmPolicy": "open",
|
|
49
|
-
"allowFrom": [
|
|
50
|
-
"*"
|
|
51
|
-
],
|
|
67
|
+
"appSecret": { "source": "env", "provider": "default", "id": "IMWE_APP_SECRET" }
|
|
52
68
|
}
|
|
53
69
|
}
|
|
54
70
|
}
|
package/openclaw.plugin.json
CHANGED
|
@@ -7,52 +7,6 @@
|
|
|
7
7
|
"configSchema": {
|
|
8
8
|
"type": "object",
|
|
9
9
|
"additionalProperties": false,
|
|
10
|
-
"properties": {
|
|
11
|
-
"accounts": {
|
|
12
|
-
"type": "object",
|
|
13
|
-
"description": "多账号配置,key 为 accountId",
|
|
14
|
-
"additionalProperties": {
|
|
15
|
-
"type": "object",
|
|
16
|
-
"additionalProperties": false,
|
|
17
|
-
"properties": {
|
|
18
|
-
"name": { "type": "string" },
|
|
19
|
-
"enabled": { "type": "boolean" },
|
|
20
|
-
"apiBaseUrl": { "type": "string" },
|
|
21
|
-
"appKey": { "type": "string" },
|
|
22
|
-
"appSecret": { "type": "string" },
|
|
23
|
-
"dmPolicy": {
|
|
24
|
-
"type": "string",
|
|
25
|
-
"enum": ["pairing", "allowlist", "open", "disabled"]
|
|
26
|
-
},
|
|
27
|
-
"allowFrom": {
|
|
28
|
-
"type": "array",
|
|
29
|
-
"items": { "type": ["string", "number"] }
|
|
30
|
-
},
|
|
31
|
-
"pollIntervalMs": {
|
|
32
|
-
"type": "number",
|
|
33
|
-
"description": "短轮询间隔(毫秒),默认 3000"
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
},
|
|
38
|
-
"defaultAccount": { "type": "string" },
|
|
39
|
-
"name": { "type": "string" },
|
|
40
|
-
"enabled": { "type": "boolean" },
|
|
41
|
-
"apiBaseUrl": { "type": "string" },
|
|
42
|
-
"appKey": { "type": "string" },
|
|
43
|
-
"appSecret": { "type": "string" },
|
|
44
|
-
"dmPolicy": {
|
|
45
|
-
"type": "string",
|
|
46
|
-
"enum": ["pairing", "allowlist", "open", "disabled"]
|
|
47
|
-
},
|
|
48
|
-
"allowFrom": {
|
|
49
|
-
"type": "array",
|
|
50
|
-
"items": { "type": ["string", "number"] }
|
|
51
|
-
},
|
|
52
|
-
"pollIntervalMs": {
|
|
53
|
-
"type": "number",
|
|
54
|
-
"description": "短轮询间隔(毫秒),默认 3000"
|
|
55
|
-
}
|
|
56
|
-
}
|
|
10
|
+
"properties": {}
|
|
57
11
|
}
|
|
58
12
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@imweapp/openclaw-imwe",
|
|
3
|
-
"version": "2026.4.12-alpha.
|
|
3
|
+
"version": "2026.4.12-alpha.4",
|
|
4
4
|
"description": "OpenClaw imwe 渠道插件 —— AppKey/AppSecret 签名认证,支持多账号、私聊文字消息",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"quickstartAllowFrom": true
|
|
62
62
|
},
|
|
63
63
|
"install": {
|
|
64
|
-
"npmSpec": "@
|
|
64
|
+
"npmSpec": "@imweapp/opencalw-imwe",
|
|
65
65
|
"defaultChoice": "npm",
|
|
66
66
|
"minHostVersion": ">=2026.4.11"
|
|
67
67
|
},
|
|
@@ -69,5 +69,5 @@
|
|
|
69
69
|
"pluginApi": ">=2026.4.11"
|
|
70
70
|
}
|
|
71
71
|
},
|
|
72
|
-
"gitHead": "
|
|
72
|
+
"gitHead": "cde7947e1487818e4ae2262172741a4e7786bfd2"
|
|
73
73
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// PbChatEmoteContent.proto — 表情消息内容定义
|
|
2
|
+
//
|
|
3
|
+
// 来源:im_proto/MsgCore/PbChatEmoteContent.proto
|
|
4
|
+
// 当 emoteType=GIFS(1) 时,插件通过 emoteId 拼接 URL 下载 GIF。
|
|
5
|
+
|
|
6
|
+
syntax = "proto3";
|
|
7
|
+
|
|
8
|
+
package proto;
|
|
9
|
+
|
|
10
|
+
option java_package = "com.imwe.im.proto";
|
|
11
|
+
|
|
12
|
+
message PbChatEmoteContent {
|
|
13
|
+
|
|
14
|
+
enum EmoteType {
|
|
15
|
+
EMOTE_TYPE_UNKNOWN = 0;
|
|
16
|
+
EMOTE_TYPE_GIFS = 1;
|
|
17
|
+
EMOTE_TYPE_STICKER = 2;
|
|
18
|
+
EMOTE_TYPE_EMOJI = 3;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
enum EmoteFileType {
|
|
22
|
+
EMOTE_FILE_TYPE_UNKNOWN = 0;
|
|
23
|
+
EMOTE_FILE_TYPE_WEBP = 1;
|
|
24
|
+
EMOTE_FILE_TYPE_GIF = 2;
|
|
25
|
+
EMOTE_FILE_TYPE_PNG = 3;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
string emoteId = 1;
|
|
29
|
+
string blurHash = 2;
|
|
30
|
+
int32 width = 3;
|
|
31
|
+
int32 height = 4;
|
|
32
|
+
EmoteFileType type = 5;
|
|
33
|
+
EmoteType emoteType = 6;
|
|
34
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// PbChatLocationContent.proto — 位置消息内容
|
|
2
|
+
//
|
|
3
|
+
// 来源:im_proto/Core/PbChatLocationContent.proto(精简拷贝,仅保留插件所需字段)
|
|
4
|
+
// 插件仅提取核心位置信息(lat/lng/address/name),不处理截图和请求关联。
|
|
5
|
+
|
|
6
|
+
syntax = "proto3";
|
|
7
|
+
|
|
8
|
+
package proto;
|
|
9
|
+
|
|
10
|
+
message PbChatLocationContent {
|
|
11
|
+
double latitude = 1; // 纬度
|
|
12
|
+
double longitude = 2; // 经度
|
|
13
|
+
optional string address = 3; // 完整地址
|
|
14
|
+
optional string name = 4; // 地点名称
|
|
15
|
+
}
|
package/proto/PbChatMsg.proto
CHANGED
|
@@ -15,8 +15,10 @@ package proto;
|
|
|
15
15
|
import "PbChatTextContent.proto";
|
|
16
16
|
import "PbChatAudioContent.proto";
|
|
17
17
|
import "PbChatRichMediaContent.proto";
|
|
18
|
+
import "PbChatEmoteContent.proto";
|
|
18
19
|
import "PbMarkdownContent.proto";
|
|
19
20
|
import "PbMsgReadStampContent.proto";
|
|
21
|
+
import "PbChatLocationContent.proto";
|
|
20
22
|
|
|
21
23
|
option java_outer_classname = "ChatMessageProto";
|
|
22
24
|
option java_package = "com.imwe.im.proto";
|
|
@@ -35,10 +37,10 @@ message PbChatMsgEnvelope {
|
|
|
35
37
|
bytes callContent = 5; // 音视频呼叫(占位,不解析)
|
|
36
38
|
bytes businessCardContent = 6; // 个人名片(占位,不解析)
|
|
37
39
|
bytes forwardContent = 7; // 合并转发(占位,不解析)
|
|
38
|
-
|
|
40
|
+
PbChatEmoteContent emoteContent = 8; // 表情消息(插件处理)
|
|
39
41
|
PbChatRichMediaContent richMediaContent = 9; // 图片/视频(插件处理)
|
|
40
42
|
bytes locationRequestContent = 10; // 位置请求(占位,不解析)
|
|
41
|
-
|
|
43
|
+
PbChatLocationContent locationContent = 11; // 位置消息(插件处理)
|
|
42
44
|
bytes groupInviteContent = 12; // 群邀请(占位,不解析)
|
|
43
45
|
PbChatRichMediaContent fileContent = 13; // 文件消息(插件处理)
|
|
44
46
|
bytes aiNoteSummaryContent = 14; // AI笔记总结(占位,不解析)
|
package/src/accounts.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from 'openclaw/plugin-sdk/account-helpers';
|
|
15
15
|
import { normalizeAccountId } from 'openclaw/plugin-sdk/account-id';
|
|
16
16
|
import type { OpenClawConfig } from 'openclaw/plugin-sdk/config-runtime';
|
|
17
|
+
import { coerceSecretRef } from 'openclaw/plugin-sdk/provider-auth';
|
|
17
18
|
import type { ImweAccountConfig, ImweConfig, ResolvedImweAccount } from './types.js';
|
|
18
19
|
|
|
19
20
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -40,6 +41,28 @@ function mergeImweAccountConfig(cfg: OpenClawConfig, accountId: string): ImweAcc
|
|
|
40
41
|
});
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
45
|
+
// SecretRef 解析 helper
|
|
46
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
/** 将纯字符串或 SecretRef 对象解析为字符串值。env 类型 ref 从 process.env 读取。 */
|
|
49
|
+
function resolveSecretLike(value: unknown): string | undefined {
|
|
50
|
+
if (value != null && typeof value === 'string') {
|
|
51
|
+
const trimmed = value.trim();
|
|
52
|
+
if (trimmed) return trimmed;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const ref = coerceSecretRef(value);
|
|
56
|
+
if (!ref) return undefined;
|
|
57
|
+
|
|
58
|
+
// env 类型 SecretRef 从 process.env 读取
|
|
59
|
+
if (ref.source === 'env') {
|
|
60
|
+
return process.env[ref.id]?.trim() || undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
43
66
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
44
67
|
// 凭证解析:优先级 账号级 config > 顶层 config > 环境变量
|
|
45
68
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -48,13 +71,11 @@ function resolveImweCredentials(
|
|
|
48
71
|
merged: ImweAccountConfig,
|
|
49
72
|
accountId: string,
|
|
50
73
|
): { appKey: string; appSecret: string; source: ResolvedImweAccount['credentialSource'] } {
|
|
51
|
-
// 1. config 里的 appKey + appSecret
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
source: 'config',
|
|
57
|
-
};
|
|
74
|
+
// 1. config 里的 appKey + appSecret(支持 SecretRef)
|
|
75
|
+
const configKey = resolveSecretLike(merged.appKey);
|
|
76
|
+
const configSecret = resolveSecretLike(merged.appSecret);
|
|
77
|
+
if (configKey && configSecret) {
|
|
78
|
+
return { appKey: configKey, appSecret: configSecret, source: 'config' };
|
|
58
79
|
}
|
|
59
80
|
|
|
60
81
|
// 2. 环境变量(仅 default 账号,避免多账号混用同一组 env)
|
package/src/api-client.ts
CHANGED
|
@@ -216,6 +216,54 @@ export async function postJson<T>(
|
|
|
216
216
|
}
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
220
|
+
// getJson — 签名 GET 请求(JSON 响应)
|
|
221
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* 发送带签名的 GET 请求,返回 JSON 响应。
|
|
225
|
+
*
|
|
226
|
+
* @param apiBaseUrl API 基础地址
|
|
227
|
+
* @param path 请求路径
|
|
228
|
+
* @param auth 鉴权凭证
|
|
229
|
+
* @param opts.timeoutMs 请求超时(毫秒)
|
|
230
|
+
* @throws 非 2xx 响应时抛出包含状态码的 Error
|
|
231
|
+
*/
|
|
232
|
+
export async function getJson<T>(
|
|
233
|
+
apiBaseUrl: string,
|
|
234
|
+
path: string,
|
|
235
|
+
auth: { apiKey: string; apiSecret: string },
|
|
236
|
+
opts?: { timeoutMs?: number },
|
|
237
|
+
): Promise<T> {
|
|
238
|
+
const url = `${apiBaseUrl.replace(/\/$/, '')}${path}`;
|
|
239
|
+
const emptyBody = new Uint8Array(0);
|
|
240
|
+
|
|
241
|
+
const controller = new AbortController();
|
|
242
|
+
const timer = opts?.timeoutMs ? setTimeout(() => controller.abort(), opts.timeoutMs) : null;
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
const res = await fetch(url, {
|
|
246
|
+
method: 'GET',
|
|
247
|
+
headers: {
|
|
248
|
+
Accept: 'application/json',
|
|
249
|
+
...buildAuthHeaders(auth.apiKey, auth.apiSecret, 'GET', path, emptyBody),
|
|
250
|
+
},
|
|
251
|
+
signal: controller.signal,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
if (!res.ok) {
|
|
255
|
+
const errText = await res.text().catch(() => '');
|
|
256
|
+
throw new Error(`imwe API ${path} 返回 ${res.status}: ${errText}`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return res.json() as Promise<T>;
|
|
260
|
+
} finally {
|
|
261
|
+
if (timer) {
|
|
262
|
+
clearTimeout(timer);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
219
267
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
220
268
|
// getMe — 获取当前机器人信息(文档 §三)
|
|
221
269
|
// ─────────────────────────────────────────────────────────────────────────────
|
package/src/config-schema.ts
CHANGED
|
@@ -10,13 +10,14 @@ import {
|
|
|
10
10
|
DmPolicySchema,
|
|
11
11
|
} from 'openclaw/plugin-sdk/channel-config-schema';
|
|
12
12
|
import { z } from 'openclaw/plugin-sdk/zod';
|
|
13
|
+
import { buildSecretInputSchema } from './secret-input.js';
|
|
13
14
|
|
|
14
15
|
const imweAccountSchema = z.object({
|
|
15
16
|
name: z.string().optional(),
|
|
16
17
|
enabled: z.boolean().optional(),
|
|
17
18
|
apiBaseUrl: z.string().optional(),
|
|
18
|
-
appKey:
|
|
19
|
-
appSecret:
|
|
19
|
+
appKey: buildSecretInputSchema().optional(),
|
|
20
|
+
appSecret: buildSecretInputSchema().optional(),
|
|
20
21
|
dmPolicy: DmPolicySchema.optional(),
|
|
21
22
|
allowFrom: AllowFromListSchema,
|
|
22
23
|
/** 短轮询间隔(毫秒),默认 3000 */
|
package/src/emote/api.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { getJson } from '../api-client.js';
|
|
2
|
+
|
|
3
|
+
export type EmoteUrlSchema = {
|
|
4
|
+
original: string;
|
|
5
|
+
stickerOriginal: string;
|
|
6
|
+
preview: string;
|
|
7
|
+
compress: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export async function fetchEmoteUrlSchema(
|
|
11
|
+
apiBaseUrl: string,
|
|
12
|
+
auth: { apiKey: string; apiSecret: string },
|
|
13
|
+
): Promise<EmoteUrlSchema> {
|
|
14
|
+
return getJson<EmoteUrlSchema>(apiBaseUrl, '/api/im/emotes/urlSchema', auth);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function resolveEmoteGifUrl(
|
|
18
|
+
apiBaseUrl: string,
|
|
19
|
+
auth: { apiKey: string; apiSecret: string },
|
|
20
|
+
emoteId: string,
|
|
21
|
+
): Promise<string> {
|
|
22
|
+
const schema = await fetchEmoteUrlSchema(apiBaseUrl, auth);
|
|
23
|
+
const prefix = schema.original.endsWith('/') ? schema.original : `${schema.original}/`;
|
|
24
|
+
return `${prefix}${emoteId}`;
|
|
25
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { resolveEmoteGifUrl } from './api.js';
|
|
2
|
+
|
|
3
|
+
type EmoteData = {
|
|
4
|
+
emoteId: string;
|
|
5
|
+
emoteType: number;
|
|
6
|
+
fileType?: number;
|
|
7
|
+
blurHash?: string;
|
|
8
|
+
width?: number;
|
|
9
|
+
height?: number;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type EmoteContext = {
|
|
13
|
+
apiBaseUrl: string;
|
|
14
|
+
auth: { apiKey: string; apiSecret: string };
|
|
15
|
+
log?: { info?: (...args: unknown[]) => void; warn?: (...args: unknown[]) => void };
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export async function handleEmoteMessage(
|
|
19
|
+
emoteData: EmoteData,
|
|
20
|
+
ctx: EmoteContext,
|
|
21
|
+
): Promise<Record<string, unknown> | null> {
|
|
22
|
+
try {
|
|
23
|
+
const gifUrl = await resolveEmoteGifUrl(ctx.apiBaseUrl, ctx.auth, emoteData.emoteId);
|
|
24
|
+
ctx.log?.info?.(`[emote] GIF URL resolved: ${gifUrl}`);
|
|
25
|
+
|
|
26
|
+
const { saveMediaSource } = await import('openclaw/plugin-sdk/media-runtime');
|
|
27
|
+
const saved = await saveMediaSource(gifUrl);
|
|
28
|
+
ctx.log?.info?.(`[emote] GIF downloaded: path=${saved.path}, contentType=${saved.contentType}`);
|
|
29
|
+
|
|
30
|
+
const { buildMediaPayload } = await import('openclaw/plugin-sdk/reply-payload');
|
|
31
|
+
return buildMediaPayload([{ path: saved.path, contentType: saved.contentType }]);
|
|
32
|
+
} catch (err) {
|
|
33
|
+
ctx.log?.warn?.(`[emote] GIF 处理失败: emoteId=${emoteData.emoteId}, error=${String(err)}`);
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
package/src/monitor.ts
CHANGED
|
@@ -40,11 +40,13 @@ import type { ChannelGatewayContext } from 'openclaw/plugin-sdk/channel-contract
|
|
|
40
40
|
import { createTypingCallbacks } from 'openclaw/plugin-sdk/channel-reply-pipeline';
|
|
41
41
|
import type { OpenClawConfig } from 'openclaw/plugin-sdk/config-runtime';
|
|
42
42
|
import { resolveStateDir } from 'openclaw/plugin-sdk/state-paths';
|
|
43
|
+
import { toLocationContext } from 'openclaw/plugin-sdk/channel-inbound';
|
|
43
44
|
import { pullMessages } from './api-client.js';
|
|
44
45
|
import type { BotInfo, PullState } from './api-client.js';
|
|
45
46
|
import type { E2eeService } from './e2ee/index.js';
|
|
46
47
|
import { downloadAndDecryptFile } from './file-transfer/index.js';
|
|
47
48
|
import { isMarkdownContent, extractMarkdownDigest } from './markdown-detect.js';
|
|
49
|
+
import { handleEmoteMessage } from './emote/index.js';
|
|
48
50
|
import { uploadEncryptedFile } from './file-transfer/index.js';
|
|
49
51
|
import { inferMediaType, inferMediaTypeFromMime } from './media-utils.js';
|
|
50
52
|
import type { E2eeDecryptFn } from './proto/inbound.codec.js';
|
|
@@ -438,6 +440,24 @@ async function handleInboundMessage(
|
|
|
438
440
|
}
|
|
439
441
|
}
|
|
440
442
|
|
|
443
|
+
// ── 3.5 处理入站表情消息(GIF)──────────────────────────────────────────
|
|
444
|
+
if (msg.emoteData) {
|
|
445
|
+
if (msg.emoteData.emoteType === 1) {
|
|
446
|
+
const emoteMediaFields = await handleEmoteMessage(msg.emoteData, {
|
|
447
|
+
apiBaseUrl: outbound.apiBaseUrl,
|
|
448
|
+
auth: outbound.auth,
|
|
449
|
+
log,
|
|
450
|
+
});
|
|
451
|
+
if (emoteMediaFields) {
|
|
452
|
+
Object.assign(mediaFields, emoteMediaFields);
|
|
453
|
+
}
|
|
454
|
+
} else {
|
|
455
|
+
log?.info?.(
|
|
456
|
+
`[${account.accountId}] 忽略非 GIF 表情消息: emoteType=${msg.emoteData.emoteType}, emoteId=${msg.emoteData.emoteId}`,
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
441
461
|
// ── 4. 构造 PascalCase ctx 并通过 finalizeInboundContext 规范化 ─────────
|
|
442
462
|
const cacheScopeKey = botInfo.botMainAcctId;
|
|
443
463
|
const replyToId = msg.referenceClientMsgId ?? undefined;
|
|
@@ -483,6 +503,7 @@ async function handleInboundMessage(
|
|
|
483
503
|
ReplyToBody: quotedMessage?.body,
|
|
484
504
|
ReplyToSender: quotedMessage?.senderId,
|
|
485
505
|
ReplyToIsQuote: quotedMessage ? true : undefined,
|
|
506
|
+
...(msg.locationData ? toLocationContext(msg.locationData) : {}),
|
|
486
507
|
...mediaFields,
|
|
487
508
|
});
|
|
488
509
|
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* - richMediaContent(图片/视频消息,oneof 字段 9)
|
|
23
23
|
* - fileContent(文件消息,oneof 字段 13)
|
|
24
24
|
* - audioContent(音频消息,oneof 字段 4)
|
|
25
|
+
* - locationContent(位置消息,oneof 字段 11)
|
|
25
26
|
* 其他类型返回 skip,由 monitor.ts 跳过。
|
|
26
27
|
*/
|
|
27
28
|
|
|
@@ -34,8 +35,11 @@ import type {
|
|
|
34
35
|
DecodedRichMediaContent,
|
|
35
36
|
DecodedAudioContent,
|
|
36
37
|
DecodedFileMeta,
|
|
38
|
+
DecodedEmoteContent,
|
|
37
39
|
} from './proto-types.js';
|
|
38
40
|
import type { ImweInboundMessage } from '../types.js';
|
|
41
|
+
import type { NormalizedLocation } from 'openclaw/plugin-sdk/channel-inbound';
|
|
42
|
+
import { formatLocationText } from 'openclaw/plugin-sdk/channel-inbound';
|
|
39
43
|
|
|
40
44
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
41
45
|
// 常量
|
|
@@ -117,6 +121,8 @@ export type InboundTextMessage = ImweInboundMessage & {
|
|
|
117
121
|
convType: number;
|
|
118
122
|
/** 接收方 ID(单聊=账户ID,群聊=群组ID) */
|
|
119
123
|
toId: string;
|
|
124
|
+
/** 位置消息结构化数据(仅位置消息时存在) */
|
|
125
|
+
locationData?: NormalizedLocation;
|
|
120
126
|
};
|
|
121
127
|
|
|
122
128
|
/** 解包失败或不支持的消息类型,由调用方决定是否记录日志 */
|
|
@@ -377,6 +383,62 @@ export async function decodeInboundPacket(packetBytes: Uint8Array): Promise<Inbo
|
|
|
377
383
|
return { ok: true, message };
|
|
378
384
|
}
|
|
379
385
|
|
|
386
|
+
if (envelope.locationContent) {
|
|
387
|
+
// ── 位置消息(字段11)─────────────────────────────────────────────────
|
|
388
|
+
const loc = envelope.locationContent;
|
|
389
|
+
const locationData: NormalizedLocation = {
|
|
390
|
+
latitude: loc.latitude,
|
|
391
|
+
longitude: loc.longitude,
|
|
392
|
+
address: loc.address || undefined,
|
|
393
|
+
name: loc.name || undefined,
|
|
394
|
+
source: 'pin',
|
|
395
|
+
};
|
|
396
|
+
const message: InboundTextMessage = {
|
|
397
|
+
msgId: deliver.clientMsgId,
|
|
398
|
+
senderId: deliver.fromId,
|
|
399
|
+
content: formatLocationText(locationData),
|
|
400
|
+
timestampMs: Number(deliver.serverStamp),
|
|
401
|
+
serverMsgId: deliver.serverMsgId,
|
|
402
|
+
convType: deliver.convType,
|
|
403
|
+
toId: deliver.toId,
|
|
404
|
+
locationData,
|
|
405
|
+
};
|
|
406
|
+
return { ok: true, message };
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (envelope.emoteContent) {
|
|
410
|
+
// ── 表情消息(字段8)─────────────────────────────────────────────────
|
|
411
|
+
const emote = envelope.emoteContent as DecodedEmoteContent;
|
|
412
|
+
const EMOTE_TYPE_GIFS = 1;
|
|
413
|
+
if (emote.emoteType !== EMOTE_TYPE_GIFS) {
|
|
414
|
+
return {
|
|
415
|
+
ok: false,
|
|
416
|
+
skip: {
|
|
417
|
+
reason: 'unsupported-content-type',
|
|
418
|
+
detail: `emoteType=${emote.emoteType}, emoteId=${emote.emoteId}`,
|
|
419
|
+
},
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
const message: InboundTextMessage = {
|
|
423
|
+
msgId: deliver.clientMsgId,
|
|
424
|
+
senderId: deliver.fromId,
|
|
425
|
+
content: '',
|
|
426
|
+
timestampMs: Number(deliver.serverStamp),
|
|
427
|
+
serverMsgId: deliver.serverMsgId,
|
|
428
|
+
convType: deliver.convType,
|
|
429
|
+
toId: deliver.toId,
|
|
430
|
+
emoteData: {
|
|
431
|
+
emoteId: emote.emoteId,
|
|
432
|
+
blurHash: emote.blurHash || undefined,
|
|
433
|
+
width: emote.width || undefined,
|
|
434
|
+
height: emote.height || undefined,
|
|
435
|
+
fileType: emote.type ?? 0,
|
|
436
|
+
emoteType: emote.emoteType,
|
|
437
|
+
},
|
|
438
|
+
};
|
|
439
|
+
return { ok: true, message };
|
|
440
|
+
}
|
|
441
|
+
|
|
380
442
|
return {
|
|
381
443
|
ok: false,
|
|
382
444
|
skip: {
|
|
@@ -614,6 +676,60 @@ async function decodeInboundPacketPlaintextTail(
|
|
|
614
676
|
return { ok: true, message };
|
|
615
677
|
}
|
|
616
678
|
|
|
679
|
+
if (envelope.locationContent) {
|
|
680
|
+
const loc = envelope.locationContent;
|
|
681
|
+
const locationData: NormalizedLocation = {
|
|
682
|
+
latitude: loc.latitude,
|
|
683
|
+
longitude: loc.longitude,
|
|
684
|
+
address: loc.address || undefined,
|
|
685
|
+
name: loc.name || undefined,
|
|
686
|
+
source: 'pin',
|
|
687
|
+
};
|
|
688
|
+
const message: InboundTextMessage = {
|
|
689
|
+
msgId: deliver.clientMsgId,
|
|
690
|
+
senderId: deliver.fromId,
|
|
691
|
+
content: formatLocationText(locationData),
|
|
692
|
+
timestampMs: Number(deliver.serverStamp),
|
|
693
|
+
serverMsgId: deliver.serverMsgId,
|
|
694
|
+
convType: deliver.convType,
|
|
695
|
+
toId: deliver.toId,
|
|
696
|
+
locationData,
|
|
697
|
+
};
|
|
698
|
+
return { ok: true, message };
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (envelope.emoteContent) {
|
|
702
|
+
const emote = envelope.emoteContent as DecodedEmoteContent;
|
|
703
|
+
const EMOTE_TYPE_GIFS = 1;
|
|
704
|
+
if (emote.emoteType !== EMOTE_TYPE_GIFS) {
|
|
705
|
+
return {
|
|
706
|
+
ok: false,
|
|
707
|
+
skip: {
|
|
708
|
+
reason: 'unsupported-content-type',
|
|
709
|
+
detail: `emoteType=${emote.emoteType}, emoteId=${emote.emoteId}`,
|
|
710
|
+
},
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
const message: InboundTextMessage = {
|
|
714
|
+
msgId: deliver.clientMsgId,
|
|
715
|
+
senderId: deliver.fromId,
|
|
716
|
+
content: '',
|
|
717
|
+
timestampMs: Number(deliver.serverStamp),
|
|
718
|
+
serverMsgId: deliver.serverMsgId,
|
|
719
|
+
convType: deliver.convType,
|
|
720
|
+
toId: deliver.toId,
|
|
721
|
+
emoteData: {
|
|
722
|
+
emoteId: emote.emoteId,
|
|
723
|
+
blurHash: emote.blurHash || undefined,
|
|
724
|
+
width: emote.width || undefined,
|
|
725
|
+
height: emote.height || undefined,
|
|
726
|
+
fileType: emote.type ?? 0,
|
|
727
|
+
emoteType: emote.emoteType,
|
|
728
|
+
},
|
|
729
|
+
};
|
|
730
|
+
return { ok: true, message };
|
|
731
|
+
}
|
|
732
|
+
|
|
617
733
|
return {
|
|
618
734
|
ok: false,
|
|
619
735
|
skip: {
|
package/src/proto/proto-types.ts
CHANGED
|
@@ -146,6 +146,34 @@ export interface DecodedMarkdownContent {
|
|
|
146
146
|
markdown: string;
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
150
|
+
// 位置内容层(PbChatLocationContent.proto)
|
|
151
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
/** PbChatLocationContent 解码后的字段 */
|
|
154
|
+
export interface DecodedLocationContent {
|
|
155
|
+
latitude: number;
|
|
156
|
+
longitude: number;
|
|
157
|
+
address?: string;
|
|
158
|
+
name?: string;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
162
|
+
// 表情内容层(PbChatEmoteContent.proto)
|
|
163
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
/** PbChatEmoteContent 解码后的字段 */
|
|
166
|
+
export interface DecodedEmoteContent {
|
|
167
|
+
emoteId: string;
|
|
168
|
+
blurHash?: string;
|
|
169
|
+
width?: number;
|
|
170
|
+
height?: number;
|
|
171
|
+
/** EmoteFileType 枚举值:0=UNKNOWN, 1=WEBP, 2=GIF, 3=PNG */
|
|
172
|
+
type: number;
|
|
173
|
+
/** EmoteType 枚举值:0=UNKNOWN, 1=GIFS, 2=STICKER, 3=EMOJI */
|
|
174
|
+
emoteType: number;
|
|
175
|
+
}
|
|
176
|
+
|
|
149
177
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
150
178
|
// 信封层(PbChatMsg.proto)
|
|
151
179
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -167,6 +195,10 @@ export interface DecodedChatMsgEnvelope {
|
|
|
167
195
|
audioContent?: DecodedAudioContent;
|
|
168
196
|
/** Markdown 消息(oneof 字段 16) */
|
|
169
197
|
markdownContent?: DecodedMarkdownContent;
|
|
198
|
+
/** 位置消息(oneof 字段 11) */
|
|
199
|
+
locationContent?: DecodedLocationContent;
|
|
200
|
+
/** 表情消息(oneof 字段 8) */
|
|
201
|
+
emoteContent?: DecodedEmoteContent;
|
|
170
202
|
}
|
|
171
203
|
|
|
172
204
|
// ─────────────────────────────────────────────────────────────────────────────
|
package/src/proto/registry.ts
CHANGED
|
@@ -88,6 +88,10 @@ export type ProtoRegistry = {
|
|
|
88
88
|
PbChatFileMeta: Type;
|
|
89
89
|
/** 音频消息内容(PbChatMsgEnvelope 字段 4) */
|
|
90
90
|
PbChatAudioContent: Type;
|
|
91
|
+
/** 位置消息内容(PbChatMsgEnvelope 字段 11) */
|
|
92
|
+
PbChatLocationContent: Type;
|
|
93
|
+
/** 表情消息内容(PbChatMsgEnvelope 字段 8) */
|
|
94
|
+
PbChatEmoteContent: Type;
|
|
91
95
|
|
|
92
96
|
// ── 发送层(PbSingleChatMsg.proto) ──────────────────────────────────────
|
|
93
97
|
/** 单聊消息请求体,作为 PbRequest.body 的内容 */
|
|
@@ -153,6 +157,8 @@ function buildRegistry(root: Root): ProtoRegistry {
|
|
|
153
157
|
PbChatRichMediaContent: root.lookupType('proto.PbChatRichMediaContent'),
|
|
154
158
|
PbChatFileMeta: root.lookupType('proto.PbChatFileMeta'),
|
|
155
159
|
PbChatAudioContent: root.lookupType('proto.PbChatAudioContent'),
|
|
160
|
+
PbChatLocationContent: root.lookupType('proto.PbChatLocationContent'),
|
|
161
|
+
PbChatEmoteContent: root.lookupType('proto.PbChatEmoteContent'),
|
|
156
162
|
// 发送层
|
|
157
163
|
PbSingleChatMsgReqBody: root.lookupType('proto.PbSingleChatMsgReqBody'),
|
|
158
164
|
PbSingleChatDeviceMsg: root.lookupType('proto.PbSingleChatDeviceMsg'),
|
|
@@ -193,6 +199,8 @@ export async function getRegistry(): Promise<ProtoRegistry> {
|
|
|
193
199
|
resolve(PROTO_DIR, 'PbChatFileMeta.proto'), // 需在 PbChatRichMediaContent/PbChatAudioContent 之前加载(被 import)
|
|
194
200
|
resolve(PROTO_DIR, 'PbChatRichMediaContent.proto'), // 需在 PbChatMsg.proto 之前加载(被 import)
|
|
195
201
|
resolve(PROTO_DIR, 'PbChatAudioContent.proto'), // 需在 PbChatMsg.proto 之前加载(被 import)
|
|
202
|
+
resolve(PROTO_DIR, 'PbChatLocationContent.proto'), // 需在 PbChatMsg.proto 之前加载(被 import)
|
|
203
|
+
resolve(PROTO_DIR, 'PbChatEmoteContent.proto'), // 需在 PbChatMsg.proto 之前加载(被 import)
|
|
196
204
|
resolve(PROTO_DIR, 'PbMarkdownContent.proto'), // 需在 PbChatMsg.proto 之前加载(被 import)
|
|
197
205
|
resolve(PROTO_DIR, 'PbMsgReadStampContent.proto'), // 需在 PbChatMsg.proto 之前加载(被 import)
|
|
198
206
|
resolve(PROTO_DIR, 'PbChatMsg.proto'),
|
package/src/types.ts
CHANGED
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
* 无需 token、无需登录流程、无需持久化任何凭证。
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import type { NormalizedLocation } from 'openclaw/plugin-sdk/channel-inbound';
|
|
16
|
+
import type { SecretInput } from 'openclaw/plugin-sdk/secret-input';
|
|
17
|
+
|
|
15
18
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
16
19
|
// 第一层:原始配置(对应 openclaw.json 里用户写的内容)
|
|
17
20
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -25,14 +28,14 @@ export type ImweAccountConfig = {
|
|
|
25
28
|
apiBaseUrl?: string;
|
|
26
29
|
/**
|
|
27
30
|
* 应用 Key,由 imwe 开放平台颁发,用于标识调用方身份。
|
|
28
|
-
*
|
|
31
|
+
* 支持纯字符串或 SecretRef 对象(如 { source: 'env', provider: 'default', id: 'IMWE_APP_KEY' })。
|
|
29
32
|
*/
|
|
30
|
-
appKey?: string;
|
|
33
|
+
appKey?: string | SecretInput;
|
|
31
34
|
/**
|
|
32
35
|
* 应用 Secret,与 AppKey 配对,用于 HMAC-SHA256 签名。
|
|
33
|
-
*
|
|
36
|
+
* 支持纯字符串或 SecretRef 对象(如 { source: 'env', provider: 'default', id: 'IMWE_APP_SECRET' })。
|
|
34
37
|
*/
|
|
35
|
-
appSecret?: string;
|
|
38
|
+
appSecret?: string | SecretInput;
|
|
36
39
|
/** DM 安全策略:pairing(配对审批)| allowlist | open | disabled */
|
|
37
40
|
dmPolicy?: 'pairing' | 'allowlist' | 'open' | 'disabled';
|
|
38
41
|
/** DM 发送者白名单(imwe userId 列表) */
|
|
@@ -138,6 +141,19 @@ export type ImweInboundMessage = {
|
|
|
138
141
|
referenceClientMsgId?: string;
|
|
139
142
|
/** 多媒体附件列表(文本消息时为 undefined) */
|
|
140
143
|
attachments?: ImweAttachment[];
|
|
144
|
+
/** 位置消息结构化数据(仅位置消息时存在) */
|
|
145
|
+
locationData?: NormalizedLocation;
|
|
146
|
+
/** 表情消息结构化数据(仅表情消息时存在) */
|
|
147
|
+
emoteData?: {
|
|
148
|
+
emoteId: string;
|
|
149
|
+
blurHash?: string;
|
|
150
|
+
width?: number;
|
|
151
|
+
height?: number;
|
|
152
|
+
/** 表情文件格式:0=UNKNOWN, 1=WEBP, 2=GIF, 3=PNG */
|
|
153
|
+
fileType: number;
|
|
154
|
+
/** 表情类型:0=UNKNOWN, 1=GIFS, 2=STICKER, 3=EMOJI */
|
|
155
|
+
emoteType: number;
|
|
156
|
+
};
|
|
141
157
|
};
|
|
142
158
|
|
|
143
159
|
/**
|