@thclouds/openclaw-channel-taidesk 0.1.2 → 0.1.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 +32 -4
- package/package.json +3 -1
- package/src/api/api.ts +3 -3
- package/src/channel.ts +2 -2
- package/src/inbound-handler.ts +47 -15
- package/src/messaging/process-message.ts +6 -6
- package/src/monitor/monitor.ts +10 -4
- package/src/mqtt/taidesk-mqtt-client.ts +66 -6
- package/src/types.ts +5 -2
package/README.md
CHANGED
|
@@ -2,11 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
OpenClaw 渠道插件:**Taidesk MQTT 入站**(订阅 **`/open` + agentId**,中间无 `/`)+ **HTTP 出站**。
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## 技术说明
|
|
6
6
|
|
|
7
|
-
- [MQTT / HTTP
|
|
8
|
-
- [四渠道对比与选型](docs/CHANNEL-COMPARISON.zh-CN.md)
|
|
9
|
-
- [开发计划](docs/TAIDESK-PLUGIN-DEV-PLAN.zh-CN.md)
|
|
7
|
+
- [MQTT / HTTP 契约与字段说明](docs/TAIDESK-WS-CONTRACT.zh-CN.md)
|
|
10
8
|
|
|
11
9
|
## 依赖
|
|
12
10
|
|
|
@@ -35,13 +33,43 @@ OpenClaw 渠道插件:**Taidesk MQTT 入站**(订阅 **`/open` + agentId**
|
|
|
35
33
|
}
|
|
36
34
|
```
|
|
37
35
|
|
|
36
|
+
**MQTT 鉴权**:未配置 `mqttUsername` 且未显式配置 `mqttPassword` 时为**匿名连接**(`apiKey` 仅用于 HTTP 出站,不会当作 MQTT 密码)。若 Broker 要求用户名/密码,请配置 `mqttUsername` 或 `mqttPassword`。
|
|
37
|
+
|
|
38
38
|
订阅 topic:**`/open` + `agentId`**(例:`agentId` 为 `2014147198056767490` 时 topic 为 `/open2014147198056767490`)。入站 JSON 见契约文档中的 `agent_msg` 示例。
|
|
39
39
|
|
|
40
|
+
## 本地 MQTT 自测(连接 + 可选触发下行)
|
|
41
|
+
|
|
42
|
+
联调脚本位于 **`tests/live/taidesk-mqtt-smoke.ts`**(与 `tests/unit` 同属 `tests/`,目录说明见 [`tests/README.md`](tests/README.md))。脚本会:
|
|
43
|
+
|
|
44
|
+
1. 用与线上一致的参数连接 MQTT、订阅 `/open`+`agentId`;
|
|
45
|
+
2. 若设置 **`TAIDESK_TEST_BEARER`**,在连接成功后 **GET** `TAIDESK_TEST_GET_URL`(默认
|
|
46
|
+
`https://demo.devlake.thclouds.com:17443/api/app-console/ai/v1/agent/<agentId>/test`),由控制台经 MQTT 下发测试消息,脚本打印收到的 payload;
|
|
47
|
+
3. 默认运行 **120s**(`TAIDESK_SMOKE_MS` 可改)。
|
|
48
|
+
|
|
49
|
+
仅连 MQTT、不调用 HTTP(例如只等自然消息):
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npm run smoke:mqtt
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
连接 + 触发测试接口(Bearer **不要提交到仓库**,只在本地或 CI 密钥里配置):
|
|
56
|
+
|
|
57
|
+
```powershell
|
|
58
|
+
$env:TAIDESK_MQTT_TLS_INSECURE='1'
|
|
59
|
+
$env:TAIDESK_TEST_BEARER='你的BearerToken'
|
|
60
|
+
npm run smoke:mqtt
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
若 Node 报错 **`certificate has expired`**,说明 HTTPS/MQTT 的 TLS 证书已过期;`TAIDESK_MQTT_TLS_INSECURE=1` 会同时作用于 **MQTT 与上述测试 GET**(**仅开发**)。OpenClaw 网关跑插件时也可设该变量;**根治**请更换服务端证书。
|
|
64
|
+
|
|
65
|
+
可用 `TAIDESK_MQTT_URL`、`TAIDESK_AGENT_ID`、`TAIDESK_TEST_GET_URL` 等覆盖;`TAIDESK_SKIP_HTTP_TEST=1` 只测 MQTT、不发 GET。
|
|
66
|
+
|
|
40
67
|
## 脚本
|
|
41
68
|
|
|
42
69
|
| 命令 | 说明 |
|
|
43
70
|
|------|------|
|
|
44
71
|
| `npm run type-check` | TypeScript 检查 |
|
|
72
|
+
| `npm run smoke:mqtt` | 实连 MQTT,收消息冒烟 |
|
|
45
73
|
| `npm test` | Vitest 单元测试 |
|
|
46
74
|
|
|
47
75
|
## 许可
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thclouds/openclaw-channel-taidesk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "OpenClaw Taidesk channel plugin (MQTT inbound + HTTP outbound)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "thclouds",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"type-check": "tsc -p tsconfig.json --noEmit",
|
|
18
18
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
19
19
|
"test": "vitest run",
|
|
20
|
+
"smoke:mqtt": "tsx tests/live/taidesk-mqtt-smoke.ts",
|
|
20
21
|
"prepublishOnly": "npm run typecheck && npm test"
|
|
21
22
|
},
|
|
22
23
|
"publishConfig": {
|
|
@@ -36,6 +37,7 @@
|
|
|
36
37
|
"devDependencies": {
|
|
37
38
|
"@types/node": "^22.10.0",
|
|
38
39
|
"openclaw": "2026.3.23",
|
|
40
|
+
"tsx": "^4.19.0",
|
|
39
41
|
"typescript": "^5.8.0",
|
|
40
42
|
"vitest": "^3.1.0"
|
|
41
43
|
},
|
package/src/api/api.ts
CHANGED
|
@@ -10,12 +10,12 @@ function resolveTaideskWebhookUrl(config: TaideskAccountConfig): string {
|
|
|
10
10
|
export async function sendTaideskText(params: {
|
|
11
11
|
cfg: OpenClawConfig;
|
|
12
12
|
accountId: string;
|
|
13
|
-
|
|
13
|
+
sessionId: string;
|
|
14
14
|
text: string;
|
|
15
15
|
log?: (m: string) => void;
|
|
16
16
|
errLog?: (m: string) => void;
|
|
17
17
|
}): Promise<{ ok: boolean; status: number; body?: string }> {
|
|
18
|
-
const { cfg, accountId,
|
|
18
|
+
const { cfg, accountId, sessionId, text } = params;
|
|
19
19
|
const log = params.log ?? (() => {});
|
|
20
20
|
const errLog = params.errLog ?? log;
|
|
21
21
|
const { config } = resolveTaideskAccount(cfg, accountId);
|
|
@@ -28,7 +28,7 @@ export async function sendTaideskText(params: {
|
|
|
28
28
|
headers.Authorization = `Bearer ${bearer}`;
|
|
29
29
|
}
|
|
30
30
|
const body = JSON.stringify({
|
|
31
|
-
|
|
31
|
+
sessionId,
|
|
32
32
|
text,
|
|
33
33
|
});
|
|
34
34
|
log(`taidesk outbound POST ${url.toString()} len=${body.length}`);
|
package/src/channel.ts
CHANGED
|
@@ -117,7 +117,7 @@ export const taideskPlugin: ChannelPlugin<ResolvedTaideskAccount> = {
|
|
|
117
117
|
resolveTarget: ({ to }: { to?: string }) => {
|
|
118
118
|
const trimmed = to?.trim();
|
|
119
119
|
if (!trimmed) {
|
|
120
|
-
return { ok: false as const, error: new Error("taidesk: --to <
|
|
120
|
+
return { ok: false as const, error: new Error("taidesk: --to <sessionId> required") };
|
|
121
121
|
}
|
|
122
122
|
return { ok: true as const, to: trimmed };
|
|
123
123
|
},
|
|
@@ -127,7 +127,7 @@ export const taideskPlugin: ChannelPlugin<ResolvedTaideskAccount> = {
|
|
|
127
127
|
const result = await sendTaideskText({
|
|
128
128
|
cfg,
|
|
129
129
|
accountId: id,
|
|
130
|
-
|
|
130
|
+
sessionId: to,
|
|
131
131
|
text,
|
|
132
132
|
log: (m) => {
|
|
133
133
|
console.info(`[taidesk] ${m}`);
|
package/src/inbound-handler.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
1
3
|
import type { TaideskInboundEvent } from "./types.js";
|
|
2
4
|
|
|
5
|
+
function stableEventIdFromRawPayload(raw: string): string {
|
|
6
|
+
return createHash("sha256").update(raw, "utf8").digest("hex").slice(0, 32);
|
|
7
|
+
}
|
|
8
|
+
|
|
3
9
|
function parseTaideskCreateTimeMs(createTime: string): number {
|
|
4
10
|
if (!createTime.trim()) {
|
|
5
11
|
return Date.now();
|
|
@@ -11,9 +17,9 @@ function parseTaideskCreateTimeMs(createTime: string): number {
|
|
|
11
17
|
|
|
12
18
|
/**
|
|
13
19
|
* Taidesk MQTT 载荷:`event: agent_msg`,`data` 为业务体。
|
|
14
|
-
*
|
|
20
|
+
* 完整形态含 `data.id` / `sessionId` / `userId`;根级发送方回退为 **`agentId`**(兼容旧版根级 `userId`);精简形态仅根级 `agentId` 作会话路由,不作发送方。
|
|
15
21
|
*/
|
|
16
|
-
function parseAgentMsgEnvelope(v: Record<string, unknown
|
|
22
|
+
function parseAgentMsgEnvelope(v: Record<string, unknown>, raw: string): TaideskInboundEvent | null {
|
|
17
23
|
if (v.event !== "agent_msg") {
|
|
18
24
|
return null;
|
|
19
25
|
}
|
|
@@ -22,9 +28,32 @@ function parseAgentMsgEnvelope(v: Record<string, unknown>): TaideskInboundEvent
|
|
|
22
28
|
return null;
|
|
23
29
|
}
|
|
24
30
|
const d = data as Record<string, unknown>;
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
|
|
31
|
+
const idStr = d.id != null ? String(d.id).trim() : "";
|
|
32
|
+
const eventId = idStr !== "" ? idStr : stableEventIdFromRawPayload(raw);
|
|
33
|
+
|
|
34
|
+
const sessionStr = d.sessionId != null ? String(d.sessionId).trim() : "";
|
|
35
|
+
const rootAgent =
|
|
36
|
+
v.agentId != null && String(v.agentId).trim() !== "" ? String(v.agentId).trim() : "";
|
|
37
|
+
/** 会话维度:优先 `data.sessionId`,否则根级 `agentId`(控制台精简推送无 sessionId 时)。 */
|
|
38
|
+
const sessionId = sessionStr !== "" ? sessionStr : rootAgent;
|
|
39
|
+
if (!sessionId) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const fromData = d.userId != null ? String(d.userId).trim() : "";
|
|
44
|
+
/** 有 `data.sessionId` 时根级 `agentId` 表示发送方回退;无 `data.sessionId` 时根级 `agentId` 已用于会话路由,发送方仅认根级 `userId`(兼容)。 */
|
|
45
|
+
let fromRoot = "";
|
|
46
|
+
if (sessionStr !== "") {
|
|
47
|
+
const rootAgentId =
|
|
48
|
+
v.agentId != null && String(v.agentId).trim() !== "" ? String(v.agentId).trim() : "";
|
|
49
|
+
const rootUserId =
|
|
50
|
+
typeof v.userId === "string" && v.userId.trim() !== "" ? v.userId.trim() : "";
|
|
51
|
+
fromRoot = rootAgentId !== "" ? rootAgentId : rootUserId;
|
|
52
|
+
} else {
|
|
53
|
+
fromRoot = typeof v.userId === "string" && v.userId.trim() !== "" ? v.userId.trim() : "";
|
|
54
|
+
}
|
|
55
|
+
const userId = fromData !== "" ? fromData : fromRoot !== "" ? fromRoot : "0";
|
|
56
|
+
|
|
28
57
|
const content = d.content;
|
|
29
58
|
let text = "";
|
|
30
59
|
if (content && typeof content === "object") {
|
|
@@ -35,14 +64,11 @@ function parseAgentMsgEnvelope(v: Record<string, unknown>): TaideskInboundEvent
|
|
|
35
64
|
}
|
|
36
65
|
const tenantId = typeof d.tenantId === "string" ? d.tenantId : undefined;
|
|
37
66
|
const createTime = typeof d.createTime === "string" ? d.createTime : "";
|
|
38
|
-
const timestamp = parseTaideskCreateTimeMs(createTime);
|
|
39
|
-
if (!eventId || !sessionId || !userId) {
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
67
|
+
const timestamp = createTime ? parseTaideskCreateTimeMs(createTime) : Date.now();
|
|
42
68
|
return {
|
|
43
69
|
type: "agent_msg",
|
|
44
70
|
eventId,
|
|
45
|
-
|
|
71
|
+
sessionId,
|
|
46
72
|
tenantId,
|
|
47
73
|
userId,
|
|
48
74
|
text,
|
|
@@ -50,23 +76,29 @@ function parseAgentMsgEnvelope(v: Record<string, unknown>): TaideskInboundEvent
|
|
|
50
76
|
};
|
|
51
77
|
}
|
|
52
78
|
|
|
53
|
-
/** 兼容旧版扁平 JSON
|
|
79
|
+
/** 兼容旧版扁平 JSON(测试/迁移);会话字段优先 `sessionId`,否则 `conversationId`。 */
|
|
54
80
|
function parseLegacyFlat(v: Record<string, unknown>): TaideskInboundEvent | null {
|
|
55
81
|
if (v.type === "ping" || v.type === "pong") {
|
|
56
82
|
return null;
|
|
57
83
|
}
|
|
58
84
|
const eventId = typeof v.eventId === "string" ? v.eventId : "";
|
|
59
|
-
const
|
|
85
|
+
const sessionIdFromSession =
|
|
86
|
+
typeof v.sessionId === "string" && v.sessionId.trim() !== "" ? v.sessionId.trim() : "";
|
|
87
|
+
const sessionIdFromConv =
|
|
88
|
+
typeof v.conversationId === "string" && v.conversationId.trim() !== ""
|
|
89
|
+
? v.conversationId.trim()
|
|
90
|
+
: "";
|
|
91
|
+
const sessionId = sessionIdFromSession !== "" ? sessionIdFromSession : sessionIdFromConv;
|
|
60
92
|
const userId = typeof v.userId === "string" ? v.userId : "";
|
|
61
93
|
const text = typeof v.text === "string" ? v.text : "";
|
|
62
94
|
const timestamp = typeof v.timestamp === "number" ? v.timestamp : Date.now();
|
|
63
|
-
if (!eventId || !
|
|
95
|
+
if (!eventId || !sessionId || !userId) {
|
|
64
96
|
return null;
|
|
65
97
|
}
|
|
66
98
|
return {
|
|
67
99
|
type: typeof v.type === "string" ? v.type : "message",
|
|
68
100
|
eventId,
|
|
69
|
-
|
|
101
|
+
sessionId,
|
|
70
102
|
tenantId: typeof v.tenantId === "string" ? v.tenantId : undefined,
|
|
71
103
|
userId,
|
|
72
104
|
text,
|
|
@@ -77,7 +109,7 @@ function parseLegacyFlat(v: Record<string, unknown>): TaideskInboundEvent | null
|
|
|
77
109
|
export function parseTaideskInboundJson(raw: string): TaideskInboundEvent | null {
|
|
78
110
|
try {
|
|
79
111
|
const v = JSON.parse(raw) as Record<string, unknown>;
|
|
80
|
-
const agent = parseAgentMsgEnvelope(v);
|
|
112
|
+
const agent = parseAgentMsgEnvelope(v, raw);
|
|
81
113
|
if (agent) {
|
|
82
114
|
return agent;
|
|
83
115
|
}
|
|
@@ -21,14 +21,14 @@ export async function processTaideskInbound(params: {
|
|
|
21
21
|
const ctx: MsgContext = {
|
|
22
22
|
Body: event.text ?? "",
|
|
23
23
|
From: event.userId,
|
|
24
|
-
To: event.
|
|
24
|
+
To: event.sessionId,
|
|
25
25
|
MessageSid: event.eventId,
|
|
26
26
|
Timestamp: event.timestamp,
|
|
27
27
|
OriginatingChannel: "taidesk",
|
|
28
|
-
OriginatingTo: event.
|
|
28
|
+
OriginatingTo: event.sessionId,
|
|
29
29
|
AccountId: accountId,
|
|
30
30
|
ChatType: "direct",
|
|
31
|
-
NativeChannelId: event.
|
|
31
|
+
NativeChannelId: event.sessionId,
|
|
32
32
|
CommandAuthorized: true,
|
|
33
33
|
};
|
|
34
34
|
|
|
@@ -36,7 +36,7 @@ export async function processTaideskInbound(params: {
|
|
|
36
36
|
cfg,
|
|
37
37
|
channel: "taidesk",
|
|
38
38
|
accountId,
|
|
39
|
-
peer: { kind: "direct", id: event.
|
|
39
|
+
peer: { kind: "direct", id: event.sessionId },
|
|
40
40
|
});
|
|
41
41
|
|
|
42
42
|
ctx.SessionKey = route.sessionKey;
|
|
@@ -54,7 +54,7 @@ export async function processTaideskInbound(params: {
|
|
|
54
54
|
updateLastRoute: {
|
|
55
55
|
sessionKey: route.mainSessionKey,
|
|
56
56
|
channel: "taidesk",
|
|
57
|
-
to: event.
|
|
57
|
+
to: event.sessionId,
|
|
58
58
|
accountId,
|
|
59
59
|
},
|
|
60
60
|
onRecordError: (err) => errLog(`recordInboundSession: ${String(err)}`),
|
|
@@ -78,7 +78,7 @@ export async function processTaideskInbound(params: {
|
|
|
78
78
|
await sendTaideskText({
|
|
79
79
|
cfg,
|
|
80
80
|
accountId,
|
|
81
|
-
|
|
81
|
+
sessionId: event.sessionId,
|
|
82
82
|
text,
|
|
83
83
|
log,
|
|
84
84
|
errLog,
|
package/src/monitor/monitor.ts
CHANGED
|
@@ -3,7 +3,10 @@ import type { PluginRuntime } from "openclaw/plugin-sdk/core";
|
|
|
3
3
|
|
|
4
4
|
import { shouldSkipDedup } from "../dedup.js";
|
|
5
5
|
import { parseTaideskInboundJson } from "../inbound-handler.js";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
buildMqttConnectAuth,
|
|
8
|
+
createTaideskMqttConnection,
|
|
9
|
+
} from "../mqtt/taidesk-mqtt-client.js";
|
|
7
10
|
import { processTaideskInbound } from "../messaging/process-message.js";
|
|
8
11
|
import { resolveTaideskChannelRuntime } from "../runtime.js";
|
|
9
12
|
import type { TaideskAccountConfig } from "../types.js";
|
|
@@ -24,13 +27,16 @@ export type MonitorTaideskOpts = {
|
|
|
24
27
|
export function monitorTaideskProvider(opts: MonitorTaideskOpts): { close: () => void } {
|
|
25
28
|
const log = opts.log ?? (() => {});
|
|
26
29
|
const errLog = opts.errLog ?? log;
|
|
27
|
-
const
|
|
30
|
+
const mqttAuth = buildMqttConnectAuth(opts.accountConfig);
|
|
31
|
+
log(
|
|
32
|
+
`taidesk mqtt connecting account=${opts.accountId} topic=/open${opts.accountConfig.agentId} auth=${mqttAuth.mode}`,
|
|
33
|
+
);
|
|
28
34
|
|
|
29
35
|
const conn = createTaideskMqttConnection({
|
|
30
36
|
mqttUrl: opts.accountConfig.mqttUrl,
|
|
31
37
|
agentId: opts.accountConfig.agentId,
|
|
32
|
-
mqttUsername:
|
|
33
|
-
mqttPassword:
|
|
38
|
+
mqttUsername: mqttAuth.username,
|
|
39
|
+
mqttPassword: mqttAuth.password,
|
|
34
40
|
clientId: opts.accountConfig.mqttClientId,
|
|
35
41
|
abortSignal: opts.abortSignal,
|
|
36
42
|
onConnect: () => {
|
|
@@ -1,4 +1,46 @@
|
|
|
1
|
-
import mqtt from "mqtt";
|
|
1
|
+
import mqtt, { type IClientOptions } from "mqtt";
|
|
2
|
+
|
|
3
|
+
export type MqttConnectAuthMode = "none" | "password-only" | "user-pass";
|
|
4
|
+
|
|
5
|
+
export type MqttConnectAuth = {
|
|
6
|
+
mode: MqttConnectAuthMode;
|
|
7
|
+
username?: string;
|
|
8
|
+
password?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 从账号配置推导 MQTT CONNECT 鉴权。
|
|
13
|
+
* 多数 Taidesk/DevLake Broker 为**匿名**:不要把 HTTP 的 apiKey 默认当作 MQTT 密码,否则会被服务端断开。
|
|
14
|
+
* - 未配置 mqttUsername 且未显式配置 mqttPassword → 不传用户名/密码
|
|
15
|
+
* - 配置了 mqttUsername → 密码为 mqttPassword ?? apiKey ?? token
|
|
16
|
+
* - 仅显式配置 mqttPassword(非空)且无 username → 仅密码(password-only)
|
|
17
|
+
* - mqttPassword 为空字符串 → 不传密码(可与 mqttUsername 组合表示「只要用户名」)
|
|
18
|
+
*/
|
|
19
|
+
export function buildMqttConnectAuth(account: {
|
|
20
|
+
mqttUsername?: string;
|
|
21
|
+
mqttPassword?: string;
|
|
22
|
+
apiKey?: string;
|
|
23
|
+
token?: string;
|
|
24
|
+
}): MqttConnectAuth {
|
|
25
|
+
const user = account.mqttUsername?.trim();
|
|
26
|
+
const explicitPwd = account.mqttPassword;
|
|
27
|
+
|
|
28
|
+
if (explicitPwd !== undefined) {
|
|
29
|
+
if (explicitPwd === "") {
|
|
30
|
+
if (user) return { mode: "user-pass", username: user };
|
|
31
|
+
return { mode: "none" };
|
|
32
|
+
}
|
|
33
|
+
if (user) return { mode: "user-pass", username: user, password: explicitPwd };
|
|
34
|
+
return { mode: "password-only", password: explicitPwd };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (user) {
|
|
38
|
+
const password = account.apiKey ?? account.token ?? "";
|
|
39
|
+
return { mode: "user-pass", username: user, password };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { mode: "none" };
|
|
43
|
+
}
|
|
2
44
|
|
|
3
45
|
/** Taidesk 控制台常用 https://host/ws,mqtt.js WebSocket 需 wss:// */
|
|
4
46
|
export function normalizeMqttConnectUrl(mqttUrl: string): string {
|
|
@@ -16,6 +58,7 @@ export type TaideskMqttClientOptions = {
|
|
|
16
58
|
mqttUrl: string;
|
|
17
59
|
/** 订阅 topic:`/open` + agentId(与 Taidesk Broker 一致,中间无 `/`) */
|
|
18
60
|
agentId: string;
|
|
61
|
+
/** 已由 buildMqttConnectAuth 解析;未设置时不要传 apiKey 作为默认密码 */
|
|
19
62
|
mqttUsername?: string;
|
|
20
63
|
mqttPassword?: string;
|
|
21
64
|
clientId?: string;
|
|
@@ -24,6 +67,11 @@ export type TaideskMqttClientOptions = {
|
|
|
24
67
|
onClose?: () => void;
|
|
25
68
|
onError?: (err: Error) => void;
|
|
26
69
|
abortSignal?: AbortSignal;
|
|
70
|
+
/**
|
|
71
|
+
* 默认遵循 Node TLS 校验。设为 `false` 仅用于自签/过期证书的开发环境(勿用于生产)。
|
|
72
|
+
* 也可设置环境变量 `TAIDESK_MQTT_TLS_INSECURE=1`(在 options 未传时生效)。
|
|
73
|
+
*/
|
|
74
|
+
rejectUnauthorized?: boolean;
|
|
27
75
|
};
|
|
28
76
|
|
|
29
77
|
/**
|
|
@@ -33,13 +81,25 @@ export type TaideskMqttClientOptions = {
|
|
|
33
81
|
export function createTaideskMqttConnection(opts: TaideskMqttClientOptions): { close: () => void } {
|
|
34
82
|
const topic = `/open${opts.agentId.trim()}`;
|
|
35
83
|
const connectUrl = normalizeMqttConnectUrl(opts.mqttUrl);
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
84
|
+
const clientOpts: IClientOptions = {
|
|
85
|
+
protocolVersion: 4,
|
|
86
|
+
clean: true,
|
|
39
87
|
clientId: opts.clientId ?? `openclaw-taidesk-${Date.now()}`,
|
|
40
88
|
reconnectPeriod: 5_000,
|
|
41
89
|
connectTimeout: 30_000,
|
|
42
|
-
}
|
|
90
|
+
};
|
|
91
|
+
if (opts.mqttUsername !== undefined && opts.mqttUsername !== "") {
|
|
92
|
+
clientOpts.username = opts.mqttUsername;
|
|
93
|
+
}
|
|
94
|
+
if (opts.mqttPassword !== undefined && opts.mqttPassword !== "") {
|
|
95
|
+
clientOpts.password = opts.mqttPassword;
|
|
96
|
+
}
|
|
97
|
+
const tlsInsecure =
|
|
98
|
+
opts.rejectUnauthorized === false || process.env.TAIDESK_MQTT_TLS_INSECURE === "1";
|
|
99
|
+
if (tlsInsecure) {
|
|
100
|
+
clientOpts.rejectUnauthorized = false;
|
|
101
|
+
}
|
|
102
|
+
const client = mqtt.connect(connectUrl, clientOpts);
|
|
43
103
|
|
|
44
104
|
const onAbort = (): void => {
|
|
45
105
|
client.end(true);
|
|
@@ -49,7 +109,7 @@ export function createTaideskMqttConnection(opts: TaideskMqttClientOptions): { c
|
|
|
49
109
|
client.on("connect", () => {
|
|
50
110
|
client.subscribe(topic, { qos: 0 }, (err) => {
|
|
51
111
|
if (err) {
|
|
52
|
-
opts.onError?.(err);
|
|
112
|
+
opts.onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
53
113
|
return;
|
|
54
114
|
}
|
|
55
115
|
opts.onConnect?.();
|
package/src/types.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
export type TaideskInboundEvent = {
|
|
5
5
|
type: string;
|
|
6
6
|
eventId: string;
|
|
7
|
-
|
|
7
|
+
/** Taidesk 会话:`data.sessionId`;无则根级 `agentId`(精简推送)。用于 peer 路由与出站 HTTP。 */
|
|
8
|
+
sessionId: string;
|
|
8
9
|
tenantId?: string;
|
|
9
10
|
userId: string;
|
|
10
11
|
text?: string;
|
|
@@ -21,9 +22,11 @@ export type TaideskAccountConfig = {
|
|
|
21
22
|
/** 出站 Webhook 完整 URL;可用 `{id}` 占位,发送前替换为 agentId */
|
|
22
23
|
webhook: string;
|
|
23
24
|
apiKey: string;
|
|
25
|
+
/** 若设置:MQTT 密码为 mqttPassword ?? apiKey ?? token */
|
|
24
26
|
mqttUsername?: string;
|
|
27
|
+
/** 显式 MQTT 密码;与 mqttUsername 均未配置时**不会**把 apiKey 当作 MQTT 密码(匿名连接) */
|
|
25
28
|
mqttPassword?: string;
|
|
26
29
|
mqttClientId?: string;
|
|
27
|
-
/** HTTP 出站 Bearer
|
|
30
|
+
/** HTTP 出站 Bearer;在已配置 mqttUsername 时可作为 MQTT 密码后备 */
|
|
28
31
|
token?: string;
|
|
29
32
|
};
|