@z-qinghui/migpt-claw 1.0.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/LICENSE +21 -0
- package/README.md +690 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/setup-entry.d.ts +3 -0
- package/dist/setup-entry.js +7 -0
- package/dist/setup-entry.js.map +1 -0
- package/dist/src/channel.d.ts +10 -0
- package/dist/src/channel.js +444 -0
- package/dist/src/channel.js.map +1 -0
- package/dist/src/config.d.ts +125 -0
- package/dist/src/config.js +146 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/message.d.ts +51 -0
- package/dist/src/message.js +145 -0
- package/dist/src/message.js.map +1 -0
- package/dist/src/mi/account.d.ts +5 -0
- package/dist/src/mi/account.js +162 -0
- package/dist/src/mi/account.js.map +1 -0
- package/dist/src/mi/common.d.ts +15 -0
- package/dist/src/mi/common.js +80 -0
- package/dist/src/mi/common.js.map +1 -0
- package/dist/src/mi/index.d.ts +4 -0
- package/dist/src/mi/index.js +10 -0
- package/dist/src/mi/index.js.map +1 -0
- package/dist/src/mi/mina.d.ts +66 -0
- package/dist/src/mi/mina.js +225 -0
- package/dist/src/mi/mina.js.map +1 -0
- package/dist/src/mi/miot.d.ts +35 -0
- package/dist/src/mi/miot.js +168 -0
- package/dist/src/mi/miot.js.map +1 -0
- package/dist/src/mi/typing.d.ts +90 -0
- package/dist/src/mi/typing.js +1 -0
- package/dist/src/mi/typing.js.map +1 -0
- package/dist/src/onboarding.d.ts +5 -0
- package/dist/src/onboarding.js +118 -0
- package/dist/src/onboarding.js.map +1 -0
- package/dist/src/openclaw-plugin-sdk.d.d.ts +185 -0
- package/dist/src/openclaw-plugin-sdk.d.js +1 -0
- package/dist/src/openclaw-plugin-sdk.d.js.map +1 -0
- package/dist/src/outbound.d.ts +5 -0
- package/dist/src/outbound.js +108 -0
- package/dist/src/outbound.js.map +1 -0
- package/dist/src/runtime.d.ts +6 -0
- package/dist/src/runtime.js +15 -0
- package/dist/src/runtime.js.map +1 -0
- package/dist/src/service.d.ts +70 -0
- package/dist/src/service.js +200 -0
- package/dist/src/service.js.map +1 -0
- package/dist/src/speaker.d.ts +62 -0
- package/dist/src/speaker.js +211 -0
- package/dist/src/speaker.js.map +1 -0
- package/dist/src/tts/mimo.d.ts +50 -0
- package/dist/src/tts/mimo.js +214 -0
- package/dist/src/tts/mimo.js.map +1 -0
- package/dist/src/types.d.ts +30 -0
- package/dist/src/types.js +1 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/utils/codec.d.ts +31 -0
- package/dist/src/utils/codec.js +144 -0
- package/dist/src/utils/codec.js.map +1 -0
- package/dist/src/utils/debug.d.ts +10 -0
- package/dist/src/utils/debug.js +15 -0
- package/dist/src/utils/debug.js.map +1 -0
- package/dist/src/utils/hash.d.ts +40 -0
- package/dist/src/utils/hash.js +75 -0
- package/dist/src/utils/hash.js.map +1 -0
- package/dist/src/utils/http.d.ts +24 -0
- package/dist/src/utils/http.js +151 -0
- package/dist/src/utils/http.js.map +1 -0
- package/dist/src/utils/index.d.ts +6 -0
- package/dist/src/utils/index.js +10 -0
- package/dist/src/utils/index.js.map +1 -0
- package/dist/src/utils/io.d.ts +26 -0
- package/dist/src/utils/io.js +53 -0
- package/dist/src/utils/io.js.map +1 -0
- package/dist/src/utils/parse.d.ts +26 -0
- package/dist/src/utils/parse.js +51 -0
- package/dist/src/utils/parse.js.map +1 -0
- package/index.ts +26 -0
- package/openclaw.plugin.json +344 -0
- package/package.json +106 -0
- package/setup-entry.ts +12 -0
- package/skills/migpt-volume/SKILL.md +182 -0
- package/skills/migpt-volume/index.ts +50 -0
- package/src/channel.ts +519 -0
- package/src/config.ts +299 -0
- package/src/message.ts +186 -0
- package/src/mi/account.ts +184 -0
- package/src/mi/common.ts +105 -0
- package/src/mi/index.ts +4 -0
- package/src/mi/mina.ts +261 -0
- package/src/mi/miot.ts +193 -0
- package/src/mi/typing.ts +93 -0
- package/src/onboarding.ts +136 -0
- package/src/openclaw-plugin-sdk.d.ts +185 -0
- package/src/outbound.ts +137 -0
- package/src/runtime.ts +14 -0
- package/src/service.ts +246 -0
- package/src/speaker.ts +264 -0
- package/src/tts/mimo.ts +300 -0
- package/src/types.ts +34 -0
- package/src/utils/codec.ts +206 -0
- package/src/utils/debug.ts +16 -0
- package/src/utils/hash.ts +104 -0
- package/src/utils/http.ts +193 -0
- package/src/utils/index.ts +5 -0
- package/src/utils/io.ts +68 -0
- package/src/utils/parse.ts +64 -0
- package/tsconfig.json +25 -0
package/src/config.ts
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import type { OpenClawConfig } from 'openclaw/plugin-sdk';
|
|
2
|
+
import type { MiServiceConfig } from './service.js';
|
|
3
|
+
import { DEFAULT_ACCOUNT_ID } from 'openclaw/plugin-sdk';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 小米音箱 Channel 配置
|
|
7
|
+
*/
|
|
8
|
+
export interface MiGPTConfig extends MiServiceConfig {
|
|
9
|
+
/** 启用频道 */
|
|
10
|
+
enabled?: boolean;
|
|
11
|
+
/** 默认账户 ID */
|
|
12
|
+
defaultAccount?: string;
|
|
13
|
+
/** 设备访问策略:pairing/allowlist/open */
|
|
14
|
+
devicePolicy?: 'pairing' | 'allowlist' | 'open';
|
|
15
|
+
/** 允许的设备名称白名单 */
|
|
16
|
+
allowFrom?: Array<string | number>;
|
|
17
|
+
/** 启用流式 TTS */
|
|
18
|
+
streaming?: boolean;
|
|
19
|
+
/** TTS 文本分块长度(汉字) */
|
|
20
|
+
textChunkLimit?: number;
|
|
21
|
+
/** TTS 语速(0.5-2.0) */
|
|
22
|
+
ttsSpeed?: number;
|
|
23
|
+
/** 默认音量(6-100) */
|
|
24
|
+
volume?: number;
|
|
25
|
+
/** 消息轮询间隔(毫秒) */
|
|
26
|
+
heartbeat?: number;
|
|
27
|
+
/** 设备名称列表 */
|
|
28
|
+
devices?: string[];
|
|
29
|
+
/** 账户配置 */
|
|
30
|
+
accounts?: Record<string, MiGPTAccountConfig>;
|
|
31
|
+
/** 音箱控制方式:mina/miot */
|
|
32
|
+
speakerControl?: 'mina' | 'miot';
|
|
33
|
+
/** 系统提示词:用于定制 AI 在音箱场景下的行为规范 */
|
|
34
|
+
systemPrompt?: string;
|
|
35
|
+
/** 启动时是否播报上线文案 */
|
|
36
|
+
announceOnStart?: boolean;
|
|
37
|
+
/** 上线播报文案 */
|
|
38
|
+
startupMessage?: string;
|
|
39
|
+
/** 收到消息时是否回复收到 */
|
|
40
|
+
acknowledgeOnReceive?: boolean;
|
|
41
|
+
/** 收到消息回复文案 */
|
|
42
|
+
receiveMessage?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 账户配置
|
|
47
|
+
*/
|
|
48
|
+
export interface MiGPTAccountConfig extends MiServiceConfig {
|
|
49
|
+
/** 启用账户 */
|
|
50
|
+
enabled?: boolean;
|
|
51
|
+
/** 账户名称 */
|
|
52
|
+
name?: string;
|
|
53
|
+
/** 设备名称列表 */
|
|
54
|
+
devices?: string[];
|
|
55
|
+
/** 系统提示词:用于定制 AI 在音箱场景下的行为规范 */
|
|
56
|
+
systemPrompt?: string;
|
|
57
|
+
/** 启动时是否播报上线文案 */
|
|
58
|
+
announceOnStart?: boolean;
|
|
59
|
+
/** 上线播报文案 */
|
|
60
|
+
startupMessage?: string;
|
|
61
|
+
/** 收到消息时是否回复收到 */
|
|
62
|
+
acknowledgeOnReceive?: boolean;
|
|
63
|
+
/** 收到消息回复文案 */
|
|
64
|
+
receiveMessage?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 解析后的账户信息
|
|
69
|
+
*/
|
|
70
|
+
export interface ResolvedMiAccount {
|
|
71
|
+
/** 账户 ID */
|
|
72
|
+
accountId: string;
|
|
73
|
+
/** 启用状态 */
|
|
74
|
+
enabled: boolean;
|
|
75
|
+
/** 是否已配置 */
|
|
76
|
+
configured: boolean;
|
|
77
|
+
/** 账户名称 */
|
|
78
|
+
name?: string;
|
|
79
|
+
/** 设备列表 */
|
|
80
|
+
devices: string[];
|
|
81
|
+
/** 配置详情 */
|
|
82
|
+
config: MiGPTAccountConfig;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* OpenClaw 配置类型扩展
|
|
87
|
+
*/
|
|
88
|
+
export interface ExtendedOpenClawConfig extends OpenClawConfig {
|
|
89
|
+
channels?: {
|
|
90
|
+
migpt?: MiGPTConfig;
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 列出所有账户 ID
|
|
96
|
+
*/
|
|
97
|
+
export function listMiAccountIds(cfg: ExtendedOpenClawConfig): string[] {
|
|
98
|
+
const migptCfg = cfg.channels?.migpt;
|
|
99
|
+
if (!migptCfg?.accounts) {
|
|
100
|
+
return [DEFAULT_ACCOUNT_ID];
|
|
101
|
+
}
|
|
102
|
+
return [DEFAULT_ACCOUNT_ID, ...Object.keys(migptCfg.accounts)];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 解析账户配置
|
|
107
|
+
*/
|
|
108
|
+
export function resolveMiAccount(
|
|
109
|
+
cfg: ExtendedOpenClawConfig,
|
|
110
|
+
accountId?: string,
|
|
111
|
+
): ResolvedMiAccount {
|
|
112
|
+
const id = accountId ?? DEFAULT_ACCOUNT_ID;
|
|
113
|
+
const migptCfg = cfg.channels?.migpt;
|
|
114
|
+
const isDefault = id === DEFAULT_ACCOUNT_ID;
|
|
115
|
+
|
|
116
|
+
// 获取账户特定配置
|
|
117
|
+
const accountConfig = isDefault
|
|
118
|
+
? { ...migptCfg }
|
|
119
|
+
: migptCfg?.accounts?.[id] ?? {};
|
|
120
|
+
|
|
121
|
+
// 合并全局和账户特定配置
|
|
122
|
+
const mergedConfig: MiGPTAccountConfig = {
|
|
123
|
+
enabled: isDefault ? migptCfg?.enabled : accountConfig.enabled,
|
|
124
|
+
userId: accountConfig.userId ?? migptCfg?.userId,
|
|
125
|
+
password: accountConfig.password ?? migptCfg?.password,
|
|
126
|
+
passToken: accountConfig.passToken ?? migptCfg?.passToken,
|
|
127
|
+
debug: accountConfig.debug ?? migptCfg?.debug,
|
|
128
|
+
timeout: accountConfig.timeout ?? migptCfg?.timeout,
|
|
129
|
+
devices: accountConfig.devices ?? migptCfg?.devices ?? [],
|
|
130
|
+
speakerControl: accountConfig.speakerControl ?? migptCfg?.speakerControl,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// 检查是否已配置
|
|
134
|
+
const configured = !!(
|
|
135
|
+
mergedConfig.userId &&
|
|
136
|
+
(mergedConfig.passToken || mergedConfig.password)
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
accountId: id,
|
|
141
|
+
enabled: mergedConfig.enabled ?? false,
|
|
142
|
+
configured,
|
|
143
|
+
name: accountConfig.name ?? (isDefault ? 'Default' : id),
|
|
144
|
+
devices: mergedConfig.devices ?? [],
|
|
145
|
+
config: mergedConfig,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* 获取默认账户 ID
|
|
151
|
+
*/
|
|
152
|
+
export function resolveDefaultMiAccountId(cfg: ExtendedOpenClawConfig): string {
|
|
153
|
+
return cfg.channels?.migpt?.defaultAccount ?? DEFAULT_ACCOUNT_ID;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* 应用账户配置
|
|
158
|
+
*/
|
|
159
|
+
export function applyMiAccountConfig(
|
|
160
|
+
cfg: ExtendedOpenClawConfig,
|
|
161
|
+
accountId: string,
|
|
162
|
+
updates: Partial<MiGPTAccountConfig>,
|
|
163
|
+
): ExtendedOpenClawConfig {
|
|
164
|
+
const isDefault = accountId === DEFAULT_ACCOUNT_ID;
|
|
165
|
+
const migptCfg = cfg.channels?.migpt ?? {};
|
|
166
|
+
|
|
167
|
+
if (isDefault) {
|
|
168
|
+
return {
|
|
169
|
+
...cfg,
|
|
170
|
+
channels: {
|
|
171
|
+
...cfg.channels,
|
|
172
|
+
migpt: {
|
|
173
|
+
...migptCfg,
|
|
174
|
+
...updates,
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
...cfg,
|
|
182
|
+
channels: {
|
|
183
|
+
...cfg.channels,
|
|
184
|
+
migpt: {
|
|
185
|
+
...migptCfg,
|
|
186
|
+
accounts: {
|
|
187
|
+
...migptCfg.accounts,
|
|
188
|
+
[accountId]: {
|
|
189
|
+
...migptCfg.accounts?.[accountId],
|
|
190
|
+
...updates,
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* 设置账户启用状态
|
|
200
|
+
*/
|
|
201
|
+
export function setMiAccountEnabled(
|
|
202
|
+
cfg: ExtendedOpenClawConfig,
|
|
203
|
+
accountId: string,
|
|
204
|
+
enabled: boolean,
|
|
205
|
+
): ExtendedOpenClawConfig {
|
|
206
|
+
const isDefault = accountId === DEFAULT_ACCOUNT_ID;
|
|
207
|
+
const migptCfg = cfg.channels?.migpt ?? {};
|
|
208
|
+
|
|
209
|
+
if (isDefault) {
|
|
210
|
+
return {
|
|
211
|
+
...cfg,
|
|
212
|
+
channels: {
|
|
213
|
+
...cfg.channels,
|
|
214
|
+
migpt: {
|
|
215
|
+
...migptCfg,
|
|
216
|
+
enabled,
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
...cfg,
|
|
224
|
+
channels: {
|
|
225
|
+
...cfg.channels,
|
|
226
|
+
migpt: {
|
|
227
|
+
...migptCfg,
|
|
228
|
+
accounts: {
|
|
229
|
+
...migptCfg.accounts,
|
|
230
|
+
[accountId]: {
|
|
231
|
+
...migptCfg.accounts?.[accountId],
|
|
232
|
+
enabled,
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* 删除账户
|
|
242
|
+
*/
|
|
243
|
+
export function deleteMiAccount(
|
|
244
|
+
cfg: ExtendedOpenClawConfig,
|
|
245
|
+
accountId: string,
|
|
246
|
+
): ExtendedOpenClawConfig {
|
|
247
|
+
const isDefault = accountId === DEFAULT_ACCOUNT_ID;
|
|
248
|
+
const migptCfg = cfg.channels?.migpt;
|
|
249
|
+
|
|
250
|
+
if (isDefault) {
|
|
251
|
+
// 删除整个 migpt 配置
|
|
252
|
+
const next = { ...cfg } as ExtendedOpenClawConfig;
|
|
253
|
+
const nextChannels = { ...cfg.channels };
|
|
254
|
+
delete (nextChannels as Record<string, unknown>).migpt;
|
|
255
|
+
if (Object.keys(nextChannels).length > 0) {
|
|
256
|
+
next.channels = nextChannels;
|
|
257
|
+
} else {
|
|
258
|
+
delete next.channels;
|
|
259
|
+
}
|
|
260
|
+
return next;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// 删除特定账户
|
|
264
|
+
const accounts = { ...migptCfg?.accounts };
|
|
265
|
+
delete accounts[accountId];
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
...cfg,
|
|
269
|
+
channels: {
|
|
270
|
+
...cfg.channels,
|
|
271
|
+
migpt: {
|
|
272
|
+
...migptCfg,
|
|
273
|
+
accounts: Object.keys(accounts).length > 0 ? accounts : undefined,
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* 解析允许的设备列表
|
|
281
|
+
*/
|
|
282
|
+
export function resolveMiAllowFrom(
|
|
283
|
+
cfg: ExtendedOpenClawConfig,
|
|
284
|
+
_accountId?: string,
|
|
285
|
+
): string[] {
|
|
286
|
+
const migptCfg = cfg.channels?.migpt;
|
|
287
|
+
const allowFrom = migptCfg?.allowFrom ?? [];
|
|
288
|
+
return allowFrom.map((entry) => String(entry).trim()).filter(Boolean);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* 格式化允许的设备列表
|
|
293
|
+
*/
|
|
294
|
+
export function formatMiAllowFrom(allowFrom: Array<string | number>): string[] {
|
|
295
|
+
return allowFrom
|
|
296
|
+
.map((entry) => String(entry).trim())
|
|
297
|
+
.filter(Boolean)
|
|
298
|
+
.map((entry) => entry.toLowerCase());
|
|
299
|
+
}
|
package/src/message.ts
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { MiService } from './service.js';
|
|
3
|
+
import { firstOf, lastOf } from './utils/parse.js';
|
|
4
|
+
|
|
5
|
+
export interface IMessage {
|
|
6
|
+
id: string;
|
|
7
|
+
sender: 'user';
|
|
8
|
+
text: string;
|
|
9
|
+
timestamp: number;
|
|
10
|
+
deviceId: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
class _MiMessage {
|
|
14
|
+
private _lastQueryMsg: Record<string, IMessage | undefined> = {};
|
|
15
|
+
private _tempQueryMsgs: Record<string, IMessage[]> = {};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 获取下一条消息
|
|
19
|
+
* @param deviceId 设备 ID
|
|
20
|
+
*/
|
|
21
|
+
async fetchNextMessage(deviceId: string): Promise<IMessage | undefined> {
|
|
22
|
+
if (!this._lastQueryMsg[deviceId]) {
|
|
23
|
+
return this._fetchFirstMessage(deviceId);
|
|
24
|
+
}
|
|
25
|
+
return this._fetchNextMessage(deviceId);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 拉取第一条消息(初始化)
|
|
30
|
+
*/
|
|
31
|
+
private async _fetchFirstMessage(deviceId: string) {
|
|
32
|
+
const msgs = await this._fetchHistoryMsgs(deviceId, {
|
|
33
|
+
limit: 1,
|
|
34
|
+
filterAnswer: false,
|
|
35
|
+
});
|
|
36
|
+
this._lastQueryMsg[deviceId] = msgs[0];
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 拉取下一条消息
|
|
42
|
+
*/
|
|
43
|
+
private async _fetchNextMessage(deviceId: string): Promise<IMessage | undefined> {
|
|
44
|
+
if (this._tempQueryMsgs[deviceId] && this._tempQueryMsgs[deviceId].length > 0) {
|
|
45
|
+
// 当前有暂存的新消息(从新到旧),依次处理之
|
|
46
|
+
return this._fetchNextTempMessage(deviceId);
|
|
47
|
+
}
|
|
48
|
+
// 拉取最新的 2 条 msg(用于和上一条消息比对是否连续)
|
|
49
|
+
const nextMsg = await this._fetchNext2Messages(deviceId);
|
|
50
|
+
if (nextMsg !== 'continue') {
|
|
51
|
+
return nextMsg;
|
|
52
|
+
}
|
|
53
|
+
// 继续向上拉取其他新消息
|
|
54
|
+
return this._fetchNextRemainingMessages(deviceId);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 拉取最新的 2 条消息,用于和上一条消息比对是否连续
|
|
59
|
+
*/
|
|
60
|
+
private async _fetchNext2Messages(deviceId: string): Promise<IMessage | 'continue' | undefined> {
|
|
61
|
+
const msgs = await this._fetchHistoryMsgs(deviceId, { limit: 2 });
|
|
62
|
+
if (msgs.length < 1 || firstOf(msgs)!.timestamp <= this._lastQueryMsg[deviceId]!.timestamp) {
|
|
63
|
+
// 没有拉到新消息
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (
|
|
67
|
+
firstOf(msgs)!.timestamp > this._lastQueryMsg[deviceId]!.timestamp &&
|
|
68
|
+
(msgs.length === 1 || lastOf(msgs)!.timestamp <= this._lastQueryMsg[deviceId]!.timestamp)
|
|
69
|
+
) {
|
|
70
|
+
// 刚好收到一条新消息
|
|
71
|
+
this._lastQueryMsg[deviceId] = firstOf(msgs);
|
|
72
|
+
return this._lastQueryMsg[deviceId];
|
|
73
|
+
}
|
|
74
|
+
// 还有其他新消息,暂存当前的新消息
|
|
75
|
+
for (const msg of msgs) {
|
|
76
|
+
if (msg.timestamp > this._lastQueryMsg[deviceId]!.timestamp) {
|
|
77
|
+
if (!this._tempQueryMsgs[deviceId]) {
|
|
78
|
+
this._tempQueryMsgs[deviceId] = [];
|
|
79
|
+
}
|
|
80
|
+
this._tempQueryMsgs[deviceId].push(msg);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return 'continue';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 继续向上拉取其他新消息
|
|
88
|
+
*/
|
|
89
|
+
private async _fetchNextRemainingMessages(deviceId: string, options?: {
|
|
90
|
+
maxPage?: number;
|
|
91
|
+
pageSize?: number;
|
|
92
|
+
}) {
|
|
93
|
+
let currentPage = 0;
|
|
94
|
+
const { maxPage = 3, pageSize = 10 } = options ?? {};
|
|
95
|
+
while (true) {
|
|
96
|
+
currentPage++;
|
|
97
|
+
if (currentPage > maxPage) {
|
|
98
|
+
// 拉取新消息超长,取消拉取
|
|
99
|
+
return this._fetchNextTempMessage(deviceId);
|
|
100
|
+
}
|
|
101
|
+
const nextTimestamp = lastOf(this._tempQueryMsgs[deviceId]!)!.timestamp;
|
|
102
|
+
const msgs = await this._fetchHistoryMsgs(deviceId, {
|
|
103
|
+
limit: pageSize,
|
|
104
|
+
timestamp: nextTimestamp,
|
|
105
|
+
});
|
|
106
|
+
for (const msg of msgs) {
|
|
107
|
+
if (msg.timestamp >= nextTimestamp) {
|
|
108
|
+
// 忽略上一页的消息
|
|
109
|
+
} else if (msg.timestamp > this._lastQueryMsg[deviceId]!.timestamp) {
|
|
110
|
+
// 继续添加新消息
|
|
111
|
+
this._tempQueryMsgs[deviceId].push(msg);
|
|
112
|
+
} else {
|
|
113
|
+
// 拉取到历史消息处
|
|
114
|
+
return this._fetchNextTempMessage(deviceId);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 读取暂存的消息
|
|
122
|
+
*/
|
|
123
|
+
private _fetchNextTempMessage(deviceId: string): IMessage | undefined {
|
|
124
|
+
const nextMsg = this._tempQueryMsgs[deviceId].pop();
|
|
125
|
+
if (nextMsg) {
|
|
126
|
+
this._lastQueryMsg[deviceId] = nextMsg;
|
|
127
|
+
}
|
|
128
|
+
return nextMsg;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 拉取历史消息
|
|
133
|
+
*/
|
|
134
|
+
private async _fetchHistoryMsgs(
|
|
135
|
+
deviceId: string,
|
|
136
|
+
options?: {
|
|
137
|
+
limit?: number;
|
|
138
|
+
timestamp?: number;
|
|
139
|
+
filterAnswer?: boolean;
|
|
140
|
+
},
|
|
141
|
+
): Promise<IMessage[]> {
|
|
142
|
+
const filterAnswer = options?.filterAnswer ?? true;
|
|
143
|
+
const conversation = await MiService.MiNA?.getConversations({
|
|
144
|
+
limit: options?.limit,
|
|
145
|
+
timestamp: options?.timestamp,
|
|
146
|
+
});
|
|
147
|
+
let records = conversation?.records ?? [];
|
|
148
|
+
|
|
149
|
+
if (filterAnswer) {
|
|
150
|
+
// 过滤有小爱回答的消息
|
|
151
|
+
records = records.filter(
|
|
152
|
+
(e) =>
|
|
153
|
+
['TTS', 'LLM'].includes(e.answers[0]?.type ?? '') && // 过滤 TTS 和 LLM 消息
|
|
154
|
+
e.answers.length === 1, // 播放音乐时会有 TTS、Audio 两个 Answer
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return records.map((e) => {
|
|
159
|
+
return {
|
|
160
|
+
id: randomUUID(),
|
|
161
|
+
sender: 'user',
|
|
162
|
+
text: e.query,
|
|
163
|
+
timestamp: e.time,
|
|
164
|
+
deviceId,
|
|
165
|
+
};
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* 清除设备消息缓存
|
|
171
|
+
*/
|
|
172
|
+
clear(deviceId: string) {
|
|
173
|
+
delete this._lastQueryMsg[deviceId];
|
|
174
|
+
delete this._tempQueryMsgs[deviceId];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* 清除所有缓存
|
|
179
|
+
*/
|
|
180
|
+
clearAll() {
|
|
181
|
+
this._lastQueryMsg = {};
|
|
182
|
+
this._tempQueryMsgs = {};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export const MiMessage = new _MiMessage();
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { encodeQuery, parseAuthPass } from '../utils/codec.js';
|
|
2
|
+
import { md5, sha1 } from '../utils/hash.js';
|
|
3
|
+
import { Http } from '../utils/http.js';
|
|
4
|
+
import { MiNA } from './mina.js';
|
|
5
|
+
import { MIoT } from './miot.js';
|
|
6
|
+
import type { MiAccount } from './typing.js';
|
|
7
|
+
|
|
8
|
+
const kLoginAPI = 'https://account.xiaomi.com/pass';
|
|
9
|
+
|
|
10
|
+
export async function getAccount(_account: MiAccount): Promise<MiAccount | undefined> {
|
|
11
|
+
let account = _account;
|
|
12
|
+
|
|
13
|
+
// 打印使用的认证方式
|
|
14
|
+
console.log('🔐 认证信息:', {
|
|
15
|
+
userId: account.userId,
|
|
16
|
+
hasPassToken: !!account.passToken,
|
|
17
|
+
hasPassword: !!account.password,
|
|
18
|
+
hasServiceToken: !!account.serviceToken,
|
|
19
|
+
authMode: account.password ? 'password' : (account.passToken ? 'passToken' : 'unknown'),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// 如果已经提供了 passToken 和 serviceToken,尝试直接使用缓存的登录态
|
|
23
|
+
if (account.passToken && account.serviceToken && account.pass?.ssecurity) {
|
|
24
|
+
console.log('🔄 尝试使用缓存的登录态 (passToken + serviceToken + ssecurity)...');
|
|
25
|
+
account.pass = {
|
|
26
|
+
code: 0,
|
|
27
|
+
passToken: account.passToken,
|
|
28
|
+
ssecurity: account.pass.ssecurity,
|
|
29
|
+
nonce: account.pass.nonce || '',
|
|
30
|
+
};
|
|
31
|
+
// 尝试直接获取设备列表,如果失败再重新登录
|
|
32
|
+
// 根据 sid 选择调用对应的服务
|
|
33
|
+
let devices: any;
|
|
34
|
+
if (account.sid === 'micoapi') {
|
|
35
|
+
devices = await MiNA.getDevice(account as any);
|
|
36
|
+
} else if (account.sid === 'xiaomiio') {
|
|
37
|
+
devices = await MIoT.getDevice(account as any);
|
|
38
|
+
} else {
|
|
39
|
+
devices = account;
|
|
40
|
+
}
|
|
41
|
+
if (devices.device) {
|
|
42
|
+
console.log('✅ 使用缓存的登录态成功');
|
|
43
|
+
return devices;
|
|
44
|
+
}
|
|
45
|
+
// 缓存失效,继续登录流程
|
|
46
|
+
console.log('⚠️ 缓存的登录态已失效,使用密码重新登录...');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 优先使用密码登录(如果有密码)
|
|
50
|
+
if (account.password) {
|
|
51
|
+
console.log('🔑 使用密码登录(passToken 作为 Cookie 辅助)...');
|
|
52
|
+
} else if (account.passToken) {
|
|
53
|
+
console.log('⚠️ 仅有 passToken 无法直接登录,passToken 需要配合密码使用');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let res = await Http.get(
|
|
57
|
+
`${kLoginAPI}/serviceLogin`,
|
|
58
|
+
{ sid: account.sid, _json: true, _locale: 'zh_CN' },
|
|
59
|
+
{ cookies: _getLoginCookies(account) },
|
|
60
|
+
);
|
|
61
|
+
if (res.isError) {
|
|
62
|
+
console.error('❌ 登录失败', res);
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
let pass = parseAuthPass(res);
|
|
66
|
+
console.log('📝 serviceLogin 响应:', { code: pass.code, description: pass.description,res: res });
|
|
67
|
+
|
|
68
|
+
if (pass.code !== 0) {
|
|
69
|
+
// 登录态失效,重新登录
|
|
70
|
+
console.log('📝 登录态失效,尝试重新认证...');
|
|
71
|
+
|
|
72
|
+
if (!account.password) {
|
|
73
|
+
console.error('❌ 缺少密码,无法重新登录。请配置 password 字段。');
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const data = {
|
|
78
|
+
_json: 'true',
|
|
79
|
+
qs: pass.qs,
|
|
80
|
+
sid: account.sid,
|
|
81
|
+
_sign: pass._sign,
|
|
82
|
+
callback: pass.callback,
|
|
83
|
+
user: account.userId,
|
|
84
|
+
hash: md5(account.password).toUpperCase(),
|
|
85
|
+
};
|
|
86
|
+
res = await Http.post(`${kLoginAPI}/serviceLoginAuth2`, encodeQuery(data), {
|
|
87
|
+
cookies: _getLoginCookies(account),
|
|
88
|
+
});
|
|
89
|
+
if (res.isError) {
|
|
90
|
+
console.error('❌ OAuth2 登录失败', res);
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
console.log('返回结果:', res.data);
|
|
94
|
+
pass = parseAuthPass(res);
|
|
95
|
+
console.log('📝 serviceLoginAuth2 响应:', {
|
|
96
|
+
code: pass.code,
|
|
97
|
+
hasPassToken: !!pass.passToken,
|
|
98
|
+
hasSsecurity: !!pass.ssecurity,
|
|
99
|
+
description: pass.description,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
if (pass.location?.includes('identity/authStart')) {
|
|
103
|
+
console.error('❌ 本次登录需要验证码,请检查 passToken 是否正确');
|
|
104
|
+
console.log('💡 当前使用的 passToken:', account.passToken?.slice(0, 20) + '...');
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
if (!pass.location || !pass.nonce || !pass.ssecurity) {
|
|
108
|
+
console.error('❌ 登录失败,请检查你的账号密码是否正确');
|
|
109
|
+
console.log('📋 返回数据:', {
|
|
110
|
+
hasLocation: !!pass.location,
|
|
111
|
+
hasNonce: !!pass.nonce,
|
|
112
|
+
hasSsecurity: !!pass.ssecurity,
|
|
113
|
+
hasPassToken: !!pass.passToken,
|
|
114
|
+
code: pass.code,
|
|
115
|
+
description: pass.description,
|
|
116
|
+
});
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
console.log('✅ 登录成功,获取 serviceToken...');
|
|
120
|
+
// 刷新登录态
|
|
121
|
+
const serviceToken = await _getServiceToken(pass);
|
|
122
|
+
if (!serviceToken) {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
console.log('✅ 获取 serviceToken 成功',{account:account,serviceToken:serviceToken});
|
|
126
|
+
account = { ...account, pass: pass as any, serviceToken };
|
|
127
|
+
console.log('📱 正在获取设备信息... account.did =', account.did);
|
|
128
|
+
console.log('📱 使用的 deviceId:', account.deviceId);
|
|
129
|
+
|
|
130
|
+
// 根据 sid 选择调用对应的服务
|
|
131
|
+
if (account.sid === 'micoapi') {
|
|
132
|
+
account = await MiNA.getDevice(account as any);
|
|
133
|
+
console.log('📱 MiNA.getDevice 结果:account.device =', account?.device);
|
|
134
|
+
} else if (account.sid === 'xiaomiio') {
|
|
135
|
+
account = await MIoT.getDevice(account as any);
|
|
136
|
+
console.log('📱 MIoT.getDevice 结果:account.device =', account?.device);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (account.did && !account.device) {
|
|
140
|
+
console.error(`❌ 找不到设备:${account.did}`);
|
|
141
|
+
console.log(
|
|
142
|
+
'🐛 请检查你的 did 与米家中的设备名称是否一致。注意错别字、空格和大小写,比如:音响 👉 音箱',
|
|
143
|
+
);
|
|
144
|
+
console.log(
|
|
145
|
+
'💡 建议打开 debug 选项,查看目标设备的真实 name、miotDID 或 mac 地址,更新 did 参数',
|
|
146
|
+
);
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
console.log('✅ 设备信息获取成功:', (account.device as any)?.name || (account.device as any)?.alias);
|
|
150
|
+
return account;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function _getLoginCookies(account: MiAccount): Record<string, string | undefined> {
|
|
154
|
+
return {
|
|
155
|
+
userId: account.userId,
|
|
156
|
+
deviceId: account.deviceId,
|
|
157
|
+
passToken: account.pass?.passToken,
|
|
158
|
+
sdkVersion: '3.9',
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function _getServiceToken(pass: any): Promise<string | undefined> {
|
|
163
|
+
const { location, nonce, ssecurity } = pass ?? {};
|
|
164
|
+
if (!location || !nonce || !ssecurity) {
|
|
165
|
+
console.error('❌ 无法获取 serviceToken,缺少必要参数');
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
const nsec = `nonce=${nonce}&${ssecurity}`;
|
|
169
|
+
const clientSign = sha1(nsec);
|
|
170
|
+
const url = location + '&clientSign=' + encodeURIComponent(clientSign);
|
|
171
|
+
|
|
172
|
+
const res = await Http.get(url, {}, { rawResponse: true });
|
|
173
|
+
|
|
174
|
+
// 从 set-cookie 中提取 serviceToken
|
|
175
|
+
const cookies = res.headers?.['set-cookie'] ?? [];
|
|
176
|
+
for (const cookie of cookies) {
|
|
177
|
+
const match = cookie.match(/serviceToken=([^;]+)/);
|
|
178
|
+
if (match) {
|
|
179
|
+
return match[1];
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
console.error('❌ 获取 Mi Service Token 失败');
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|