@imweapp/openclaw-imwe 2026.4.12-alpha.1 → 2026.4.12-alpha.3
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 +40 -129
- package/openclaw.plugin.json +1 -47
- package/package.json +3 -3
- package/src/accounts.ts +28 -7
- package/src/config-schema.ts +3 -2
- package/src/secret-input.ts +6 -0
- package/src/send.ts +5 -4
- package/src/types.ts +6 -4
package/README.md
CHANGED
|
@@ -2,86 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
imwe 即时通讯渠道插件,AppKey/AppSecret HMAC-SHA256 签名认证,支持多账号、私聊文字消息。
|
|
4
4
|
|
|
5
|
-
## 目录结构
|
|
6
|
-
|
|
7
|
-
```
|
|
8
|
-
extensions/imwe/
|
|
9
|
-
├── proto/ # Protobuf 消息定义(语言无关的契约文件)
|
|
10
|
-
│ ├── PbPacket.proto # 传输层:PbPacket / PbRequest / PbResponse
|
|
11
|
-
│ ├── PbChatDeliverMsg.proto # 投递层:PbChatMsgDeliverBody
|
|
12
|
-
│ ├── PbChatMsg.proto # 信封层:PbChatMsgEnvelope / PbMsgRspBody
|
|
13
|
-
│ ├── PbChatTextContent.proto # 内容层:PbChatTextContent / BodyRange
|
|
14
|
-
│ ├── PbSingleChatMsg.proto # 发送层:PbSingleChatMsgReqBody
|
|
15
|
-
│ └── PbBoxPullProto.proto # 事件箱:PbBoxPullReq / PbBoxPullRsp
|
|
16
|
-
├── index.ts # 插件主入口(defineChannelPluginEntry)
|
|
17
|
-
├── setup-entry.ts # 轻量入口(setup-only 模式)
|
|
18
|
-
├── package.json # npm 包配置
|
|
19
|
-
└── src/
|
|
20
|
-
├── proto/ # Protobuf 编解码层(TypeScript)
|
|
21
|
-
│ ├── codec.ts # encode/decode 统一导出入口
|
|
22
|
-
│ ├── registry.ts # protobufjs 类型注册表(单例)
|
|
23
|
-
│ ├── proto-types.ts # decode 结果的 TypeScript 接口(仅用于类型断言)
|
|
24
|
-
│ ├── inbound.codec.ts # 入站消息四层解包
|
|
25
|
-
│ └── send.codec.ts # 出站消息编码 + 发送响应解码
|
|
26
|
-
├── types.ts # 类型定义(三层:Config / ImweConfig / ResolvedAccount)
|
|
27
|
-
├── accounts.ts # 账号解析(listAccountIds / resolveImweAccount)
|
|
28
|
-
├── config-schema.ts # Zod 配置 schema
|
|
29
|
-
├── api-client.ts # HTTP API 客户端(签名 + protobuf 传输)
|
|
30
|
-
├── monitor.ts # 事件箱短轮询消息监听器
|
|
31
|
-
├── send.ts # 发送文字消息(outbound 适配器入口)
|
|
32
|
-
├── setup-core.ts # setup 适配器(openclaw setup imwe)
|
|
33
|
-
├── channel.ts # ChannelPlugin 主对象(组装所有适配器)
|
|
34
|
-
└── channel.runtime.ts # 运行时模块统一导出(懒加载目标)
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
## 认证方式
|
|
38
|
-
|
|
39
|
-
每次 HTTP 请求携带以下签名头(对应文档 §1):
|
|
40
|
-
|
|
41
|
-
```
|
|
42
|
-
X-Api-Key: <appKey>
|
|
43
|
-
X-Timestamp: <毫秒时间戳>
|
|
44
|
-
X-Signature: Base64(HMAC-SHA256(appSecret, signString))
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
签名原文(signString):
|
|
48
|
-
|
|
49
|
-
```
|
|
50
|
-
{METHOD}&{PATH}&{TIMESTAMP}&{BODY_HASH}
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
- `METHOD`:HTTP 方法大写,固定为 `POST`
|
|
54
|
-
- `PATH`:请求路径,如 `/api/im/open/bot/sendMessage`
|
|
55
|
-
- `TIMESTAMP`:与 `X-Timestamp` 相同的毫秒时间戳字符串
|
|
56
|
-
- `BODY_HASH`:`hex(SHA-256(body))`,body 为空时对空字符串 `""` 哈希
|
|
57
|
-
|
|
58
|
-
无需登录流程,无需持久化任何凭证,AppKey/AppSecret 写入 config 即可。
|
|
59
|
-
|
|
60
|
-
## 数据流
|
|
61
|
-
|
|
62
|
-
```
|
|
63
|
-
用户消息
|
|
64
|
-
→ imwe 平台
|
|
65
|
-
→ monitor.ts (long polling POST /messages/poll)
|
|
66
|
-
→ channelRuntime.reply.dispatchReply(...)
|
|
67
|
-
→ OpenClaw AI 推理
|
|
68
|
-
→ outbound.sendText(...)
|
|
69
|
-
→ api-client.sendTextMessage (POST /messages/send + HMAC 签名)
|
|
70
|
-
→ imwe 平台 → 用户
|
|
71
|
-
```
|
|
72
5
|
|
|
73
6
|
## 配置示例
|
|
74
7
|
|
|
75
8
|
### 单账号
|
|
76
9
|
|
|
10
|
+
凭证写在 `accounts.default` 内,共享配置(`dmPolicy`、`allowFrom`)放在顶层作为默认值。`apiBaseUrl` 可省略,默认使用 `https://im-pre.imweapi.com`。
|
|
11
|
+
|
|
77
12
|
```json
|
|
78
13
|
{
|
|
79
14
|
"channels": {
|
|
80
15
|
"imwe": {
|
|
81
|
-
"
|
|
82
|
-
"
|
|
83
|
-
"
|
|
84
|
-
"
|
|
16
|
+
"defaultAccount": "default",
|
|
17
|
+
"dmPolicy": "open",
|
|
18
|
+
"allowFrom": ["*"],
|
|
19
|
+
"accounts": {
|
|
20
|
+
"default": {
|
|
21
|
+
"appKey": "your-app-key",
|
|
22
|
+
"appSecret": "your-app-secret"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
85
25
|
}
|
|
86
26
|
}
|
|
87
27
|
}
|
|
@@ -89,23 +29,23 @@ X-Signature: Base64(HMAC-SHA256(appSecret, signString))
|
|
|
89
29
|
|
|
90
30
|
### 多账号
|
|
91
31
|
|
|
32
|
+
顶层写共享默认值,每个账号只写凭证。
|
|
33
|
+
|
|
92
34
|
```json
|
|
93
35
|
{
|
|
94
36
|
"channels": {
|
|
95
37
|
"imwe": {
|
|
96
|
-
"
|
|
97
|
-
"
|
|
38
|
+
"defaultAccount": "default",
|
|
39
|
+
"dmPolicy": "open",
|
|
40
|
+
"allowFrom": ["*"],
|
|
98
41
|
"accounts": {
|
|
99
|
-
"
|
|
100
|
-
"appKey": "key-
|
|
101
|
-
"appSecret": "secret-
|
|
102
|
-
"dmPolicy": "pairing"
|
|
42
|
+
"default": {
|
|
43
|
+
"appKey": "app-key-1",
|
|
44
|
+
"appSecret": "app-secret-1"
|
|
103
45
|
},
|
|
104
46
|
"bot2": {
|
|
105
|
-
"appKey": "key-
|
|
106
|
-
"appSecret": "secret-
|
|
107
|
-
"dmPolicy": "allowlist",
|
|
108
|
-
"allowFrom": ["123456"]
|
|
47
|
+
"appKey": "app-key-2",
|
|
48
|
+
"appSecret": "app-secret-2"
|
|
109
49
|
}
|
|
110
50
|
}
|
|
111
51
|
}
|
|
@@ -113,60 +53,31 @@ X-Signature: Base64(HMAC-SHA256(appSecret, signString))
|
|
|
113
53
|
}
|
|
114
54
|
```
|
|
115
55
|
|
|
116
|
-
###
|
|
117
|
-
|
|
118
|
-
```bash
|
|
119
|
-
IMWE_API_BASE_URL=https://api.imwe.example.com
|
|
120
|
-
IMWE_APP_KEY=your-app-key
|
|
121
|
-
IMWE_APP_SECRET=your-app-secret
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
## E2EE 端到端加密
|
|
125
|
-
|
|
126
|
-
单聊消息默认启用 E2EE(端到端加密),与 imwe iOS 客户端完全互通。无需额外配置,插件启动时自动完成密钥协商。
|
|
127
|
-
|
|
128
|
-
### 安全边界
|
|
129
|
-
|
|
130
|
-
- **加密范围**:单聊文本 / Markdown / 媒体消息的 `PbChatMsgEnvelope`(含 URL、caption、blurHash 等元数据)均经 E2EE 加密
|
|
131
|
-
- **不在加密范围内**:媒体文件正文(CDN 资源本身以明文上传/下载,仅信令层走 E2EE);typing signal 保持明文
|
|
132
|
-
- 加解密失败时消息**不会降级为明文**发送或投递
|
|
56
|
+
### 使用 SecretRef 引用环境变量
|
|
133
57
|
|
|
134
|
-
|
|
58
|
+
凭证字段支持 SecretRef 对象,通过 `{ source: 'env', provider: 'default', id: '...' }` 引用环境变量:
|
|
135
59
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"channels": {
|
|
63
|
+
"imwe": {
|
|
64
|
+
"accounts": {
|
|
65
|
+
"default": {
|
|
66
|
+
"appKey": "your-app-key",
|
|
67
|
+
"appSecret": { "source": "env", "provider": "default", "id": "IMWE_APP_SECRET" }
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
143
73
|
```
|
|
144
74
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
### Mock Server(开发/测试)
|
|
148
|
-
|
|
149
|
-
项目提供了一个轻量 mock E2EE 服务端,用于本地开发与集成测试:
|
|
75
|
+
### 环境变量(单账号 default)
|
|
150
76
|
|
|
151
77
|
```bash
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
# 自定义端口
|
|
156
|
-
PORT=4000 node tests/mock-server/server.js
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
mock server 覆盖 `getMe` / `pullMessages` / `sendMessage` + 5 个 E2EE 端点(create / replenish / apply / queryRequireNum / query),状态机行为详见 `tests/mock-server/README.md`。
|
|
160
|
-
|
|
161
|
-
在集成测试中可 programmatic 引入:
|
|
162
|
-
|
|
163
|
-
```js
|
|
164
|
-
import { server, PORT, resetState } from './tests/mock-server/server.js';
|
|
78
|
+
IMWE_API_BASE_URL=https://api.imwe.example.com
|
|
79
|
+
IMWE_APP_KEY=your-app-key
|
|
80
|
+
IMWE_APP_SECRET=your-app-secret
|
|
165
81
|
```
|
|
166
82
|
|
|
167
83
|
## 安装与配置
|
|
168
|
-
|
|
169
|
-
```bash
|
|
170
|
-
openclaw plugins install @openclaw/imwe
|
|
171
|
-
openclaw setup imwe --http-url https://api.imwe.example.com --token <appKey> --private-key <appSecret>
|
|
172
|
-
```
|
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.3",
|
|
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": "3815a36581e83ca8f98a95d1229b069233c98897"
|
|
73
73
|
}
|
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/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/send.ts
CHANGED
|
@@ -211,14 +211,15 @@ async function encryptAndSendWithRetry(
|
|
|
211
211
|
return await encryptAndSend(outbound, to, envelopeBytes, clientMsgId);
|
|
212
212
|
} catch (err) {
|
|
213
213
|
if (err instanceof DeviceNotMatchError && outbound.e2eeService) {
|
|
214
|
-
// DEVICE_NOT_MATCH 重试:handleDeviceNotMatch →
|
|
214
|
+
// DEVICE_NOT_MATCH 重试:handleDeviceNotMatch → 生成新 clientMsgId 重发一次
|
|
215
|
+
const retryClientMsgId = genClientMsgId();
|
|
215
216
|
outbound.log?.info?.(
|
|
216
|
-
`${prefix}: DEVICE_NOT_MATCH,执行单次重试, to=${to},
|
|
217
|
+
`${prefix}: DEVICE_NOT_MATCH,执行单次重试, to=${to}, oldClientMsgId=${clientMsgId}, newClientMsgId=${retryClientMsgId}, errData=${JSON.stringify(err.data)}`,
|
|
217
218
|
);
|
|
218
219
|
await outbound.e2eeService.handleDeviceNotMatch(err.data);
|
|
219
|
-
const result = await encryptAndSend(outbound, to, envelopeBytes,
|
|
220
|
+
const result = await encryptAndSend(outbound, to, envelopeBytes, retryClientMsgId);
|
|
220
221
|
outbound.log?.info?.(
|
|
221
|
-
`${prefix}: DEVICE_NOT_MATCH 重试成功, to=${to}, clientMsgId=${
|
|
222
|
+
`${prefix}: DEVICE_NOT_MATCH 重试成功, to=${to}, clientMsgId=${retryClientMsgId}`,
|
|
222
223
|
);
|
|
223
224
|
return result;
|
|
224
225
|
}
|
package/src/types.ts
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* 无需 token、无需登录流程、无需持久化任何凭证。
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import type { SecretInput } from 'openclaw/plugin-sdk/secret-input';
|
|
16
|
+
|
|
15
17
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
16
18
|
// 第一层:原始配置(对应 openclaw.json 里用户写的内容)
|
|
17
19
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -25,14 +27,14 @@ export type ImweAccountConfig = {
|
|
|
25
27
|
apiBaseUrl?: string;
|
|
26
28
|
/**
|
|
27
29
|
* 应用 Key,由 imwe 开放平台颁发,用于标识调用方身份。
|
|
28
|
-
*
|
|
30
|
+
* 支持纯字符串或 SecretRef 对象(如 { source: 'env', provider: 'default', id: 'IMWE_APP_KEY' })。
|
|
29
31
|
*/
|
|
30
|
-
appKey?: string;
|
|
32
|
+
appKey?: string | SecretInput;
|
|
31
33
|
/**
|
|
32
34
|
* 应用 Secret,与 AppKey 配对,用于 HMAC-SHA256 签名。
|
|
33
|
-
*
|
|
35
|
+
* 支持纯字符串或 SecretRef 对象(如 { source: 'env', provider: 'default', id: 'IMWE_APP_SECRET' })。
|
|
34
36
|
*/
|
|
35
|
-
appSecret?: string;
|
|
37
|
+
appSecret?: string | SecretInput;
|
|
36
38
|
/** DM 安全策略:pairing(配对审批)| allowlist | open | disabled */
|
|
37
39
|
dmPolicy?: 'pairing' | 'allowlist' | 'open' | 'disabled';
|
|
38
40
|
/** DM 发送者白名单(imwe userId 列表) */
|