@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/mi/common.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { readJSON, writeJSON } from '../utils/io.js';
|
|
2
|
+
import { getAccount } from './account.js';
|
|
3
|
+
import { MiNA } from './mina.js';
|
|
4
|
+
import { MIoT } from './miot.js';
|
|
5
|
+
import { Debugger } from '../utils/debug.js';
|
|
6
|
+
import type { MiAccount, MiPass } from './typing.js';
|
|
7
|
+
|
|
8
|
+
interface Store {
|
|
9
|
+
miot?: MiAccount;
|
|
10
|
+
mina?: MiAccount;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const kConfigFile = '.mi.json';
|
|
14
|
+
|
|
15
|
+
export function updateMiAccount(account: MiAccount): (updated: MiAccount) => void {
|
|
16
|
+
return (updated: MiAccount) => {
|
|
17
|
+
if (updated.serviceToken) {
|
|
18
|
+
account.serviceToken = updated.serviceToken;
|
|
19
|
+
}
|
|
20
|
+
if (updated.deviceId) {
|
|
21
|
+
account.deviceId = updated.deviceId;
|
|
22
|
+
}
|
|
23
|
+
if (updated.pass?.ssecurity) {
|
|
24
|
+
if (!account.pass) account.pass = { code: 0 } as MiPass;
|
|
25
|
+
(account.pass as MiPass).ssecurity = updated.pass.ssecurity;
|
|
26
|
+
}
|
|
27
|
+
if (updated.pass?.nonce) {
|
|
28
|
+
if (!account.pass) account.pass = { code: 0 } as MiPass;
|
|
29
|
+
(account.pass as MiPass).nonce = updated.pass.nonce;
|
|
30
|
+
}
|
|
31
|
+
if (updated.pass?.passToken) {
|
|
32
|
+
if (!account.pass) account.pass = { code: 0 } as MiPass;
|
|
33
|
+
(account.pass as MiPass).passToken = updated.pass.passToken;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function getMiService(config: {
|
|
39
|
+
service: 'miot' | 'mina';
|
|
40
|
+
userId?: string;
|
|
41
|
+
password?: string;
|
|
42
|
+
passToken?: string;
|
|
43
|
+
did?: string;
|
|
44
|
+
relogin?: boolean;
|
|
45
|
+
}): Promise<MiNA | MIoT | undefined> {
|
|
46
|
+
const { service, relogin, ...rest } = config;
|
|
47
|
+
const overrides: any = relogin ? {} : rest;
|
|
48
|
+
|
|
49
|
+
// 如果有 passToken,同时设置 pass 对象
|
|
50
|
+
if (overrides.passToken) {
|
|
51
|
+
overrides.pass = {
|
|
52
|
+
...overrides.pass,
|
|
53
|
+
passToken: overrides.passToken,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const store: Store = (await readJSON(kConfigFile)) ?? {};
|
|
58
|
+
|
|
59
|
+
// 从缓存中获取 deviceId,如果没有则生成新的(16 位随机大写字母)
|
|
60
|
+
let deviceId = store[service]?.deviceId;
|
|
61
|
+
if (!deviceId) {
|
|
62
|
+
deviceId = Array(16).fill(0).map(() =>
|
|
63
|
+
'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'[Math.floor(Math.random() * 36)]
|
|
64
|
+
).join('');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 构建账户信息,优先使用传入的参数,其次使用缓存
|
|
68
|
+
let account = {
|
|
69
|
+
deviceId,
|
|
70
|
+
...store[service],
|
|
71
|
+
...overrides,
|
|
72
|
+
sid: service === 'miot' ? 'xiaomiio' : 'micoapi',
|
|
73
|
+
} as MiAccount;
|
|
74
|
+
|
|
75
|
+
// 从缓存中补充 pass 信息(如果有)
|
|
76
|
+
const cached = store[service];
|
|
77
|
+
if (cached?.pass) {
|
|
78
|
+
account.pass = {
|
|
79
|
+
...account.pass,
|
|
80
|
+
...cached.pass,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 检查是否有足够的凭证
|
|
85
|
+
// passToken 是可选的辅助凭证,password 是必需的登录凭证
|
|
86
|
+
const hasPassword = !!account.userId && !!account.password;
|
|
87
|
+
|
|
88
|
+
if (!hasPassword) {
|
|
89
|
+
console.error('❌ 缺少必需的登录凭证:需要 userId 和 password');
|
|
90
|
+
console.log('💡 passToken 是可选的辅助凭证,不能完全替代密码');
|
|
91
|
+
console.log('💡 配置示例:{ userId: "123", password: "xxx", passToken: "yyy" }');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const result = await getAccount(account);
|
|
96
|
+
if (Debugger.debug) {
|
|
97
|
+
console.log('💡 getAccount 结果:',{result:result});
|
|
98
|
+
}
|
|
99
|
+
if (!result?.serviceToken || !result.pass?.ssecurity) {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
store[service] = result;
|
|
103
|
+
await writeJSON(kConfigFile, store);
|
|
104
|
+
return service === 'miot' ? new MIoT(result as any) : new MiNA(result as any);
|
|
105
|
+
}
|
package/src/mi/index.ts
ADDED
package/src/mi/mina.ts
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { encodeQuery } from '../utils/codec.js';
|
|
2
|
+
import { Debugger } from '../utils/debug.js';
|
|
3
|
+
import { jsonEncode,jsonDecode } from '../utils/parse.js';
|
|
4
|
+
import { uuid } from '../utils/hash.js';
|
|
5
|
+
import { Http } from '../utils/http.js';
|
|
6
|
+
import { updateMiAccount } from './common.js';
|
|
7
|
+
import type { MiAccount, MiConversations, MiNADevice } from './typing.js';
|
|
8
|
+
|
|
9
|
+
type MiNAAccount = MiAccount & { device: MiNADevice };
|
|
10
|
+
|
|
11
|
+
export class MiNA {
|
|
12
|
+
account: MiNAAccount;
|
|
13
|
+
|
|
14
|
+
constructor(account: MiNAAccount) {
|
|
15
|
+
this.account = account as any;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
static async getDevice(account: MiNAAccount): Promise<MiNAAccount> {
|
|
19
|
+
if (account.sid !== 'micoapi') {
|
|
20
|
+
return account;
|
|
21
|
+
}
|
|
22
|
+
const devices = await MiNA.__callMiNA(account, 'GET', '/admin/v2/device_list');
|
|
23
|
+
if (Debugger.debug) {
|
|
24
|
+
console.log('🐛 MiNA 设备列表:', jsonEncode(devices, { prettier: true }));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
console.log('🔍 查找设备 account.did:', account.did);
|
|
28
|
+
|
|
29
|
+
const device = (devices ?? []).find((e: any) => {
|
|
30
|
+
const matches = [e.deviceID, e.miotDID, e.name, e.alias, e.mac].includes(account.did);
|
|
31
|
+
console.log(`🔍 检查设备 ${e.name} (miotDID: ${e.miotDID}):`, {
|
|
32
|
+
deviceID: e.deviceID,
|
|
33
|
+
miotDID: e.miotDID,
|
|
34
|
+
name: e.name,
|
|
35
|
+
alias: e.alias,
|
|
36
|
+
mac: e.mac,
|
|
37
|
+
matches,
|
|
38
|
+
});
|
|
39
|
+
return matches;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (device) {
|
|
43
|
+
account.device = { ...device, deviceId: device.deviceID };
|
|
44
|
+
console.log('✅ 找到设备:', device.name);
|
|
45
|
+
} else {
|
|
46
|
+
console.error('❌ 未找到匹配的设备,account.did:', account.did);
|
|
47
|
+
}
|
|
48
|
+
return account;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private static async __callMiNA(
|
|
52
|
+
account: MiNAAccount,
|
|
53
|
+
method: 'GET' | 'POST',
|
|
54
|
+
path: string,
|
|
55
|
+
_data?: any,
|
|
56
|
+
): Promise<any> {
|
|
57
|
+
const data = {
|
|
58
|
+
..._data,
|
|
59
|
+
requestId: uuid(),
|
|
60
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
61
|
+
};
|
|
62
|
+
const url = `https://api2.mina.mi.com${path}`;
|
|
63
|
+
const config = {
|
|
64
|
+
account,
|
|
65
|
+
setAccount: updateMiAccount(account),
|
|
66
|
+
headers: { 'User-Agent': 'MICO/AndroidApp/@SHIP.TO.2A2FE0D7@/2.4.40' },
|
|
67
|
+
cookies: {
|
|
68
|
+
userId: account.userId,
|
|
69
|
+
serviceToken: account.serviceToken,
|
|
70
|
+
sn: account.device?.serialNumber,
|
|
71
|
+
hardware: account.device?.hardware,
|
|
72
|
+
deviceId: account.device?.deviceId,
|
|
73
|
+
deviceSNProfile: account.device?.deviceSNProfile,
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
let res: any;
|
|
77
|
+
if (method === 'GET') {
|
|
78
|
+
res = await Http.get(url, data, config);
|
|
79
|
+
} else {
|
|
80
|
+
res = await Http.post(url, encodeQuery(data), config);
|
|
81
|
+
}
|
|
82
|
+
if (res.code !== 0) {
|
|
83
|
+
if (Debugger.debug) {
|
|
84
|
+
console.error('❌ _callMiNA failed', res);
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
return res.data;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private async _callMiNA(method: 'GET' | 'POST', path: string, data?: any): Promise<any> {
|
|
92
|
+
return MiNA.__callMiNA(this.account, method, path, data);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 调用小爱音箱上的 ubus 服务
|
|
97
|
+
*/
|
|
98
|
+
callUbus(scope: string, command: string, _message?: any) {
|
|
99
|
+
const message = jsonEncode(_message ?? {});
|
|
100
|
+
return this._callMiNA('POST', '/remote/ubus', {
|
|
101
|
+
deviceId: this.account.device?.deviceId,
|
|
102
|
+
path: scope,
|
|
103
|
+
method: command,
|
|
104
|
+
message,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 获取设备列表
|
|
110
|
+
*/
|
|
111
|
+
getDevices() {
|
|
112
|
+
return this._callMiNA('GET', '/admin/v2/device_list');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 获取设备播放状态
|
|
117
|
+
*/
|
|
118
|
+
async getStatus(): Promise<
|
|
119
|
+
| {
|
|
120
|
+
volume: number;
|
|
121
|
+
status: 'idle' | 'playing' | 'paused' | 'stopped' | 'unknown';
|
|
122
|
+
media_type?: number;
|
|
123
|
+
loop_type?: number;
|
|
124
|
+
}
|
|
125
|
+
| undefined
|
|
126
|
+
> {
|
|
127
|
+
const data = await this.callUbus('mediaplayer', 'player_get_play_status');
|
|
128
|
+
const res = jsonDecode(data?.info);
|
|
129
|
+
if (!data || data.code !== 0 || !res) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const map = { 0: 'idle', 1: 'playing', 2: 'paused', 3: 'stopped' } as any;
|
|
133
|
+
return {
|
|
134
|
+
...res,
|
|
135
|
+
status: map[res.status] ?? 'unknown',
|
|
136
|
+
volume: res.volume,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 获取音量
|
|
142
|
+
*/
|
|
143
|
+
async getVolume() {
|
|
144
|
+
const data = await this.getStatus();
|
|
145
|
+
return data?.volume;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* 设置音量
|
|
150
|
+
*/
|
|
151
|
+
async setVolume(_volume: number) {
|
|
152
|
+
const volume = Math.round(clamp(_volume, 6, 100));
|
|
153
|
+
const res = await this.callUbus('mediaplayer', 'player_set_volume', {
|
|
154
|
+
volume,
|
|
155
|
+
});
|
|
156
|
+
return res?.code === 0;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 播放
|
|
161
|
+
*/
|
|
162
|
+
async play({ text, url, save = 0 }: { text?: string; url?: string; save?: 0 | 1 } = {}) {
|
|
163
|
+
let res: any;
|
|
164
|
+
if (url) {
|
|
165
|
+
console.log(`🔊 MiNA.play URL: ${url}`);
|
|
166
|
+
res = await this.callUbus('mediaplayer', 'player_play_url', {
|
|
167
|
+
url,
|
|
168
|
+
type: 1,
|
|
169
|
+
});
|
|
170
|
+
console.log(`🔊 MiNA.play URL result:`, JSON.stringify(res));
|
|
171
|
+
} else if (text) {
|
|
172
|
+
console.log(`🔊 MiNA.play text: ${text.slice(0, 50)}...`);
|
|
173
|
+
res = await this.callUbus('mibrain', 'text_to_speech', {
|
|
174
|
+
text,
|
|
175
|
+
save,
|
|
176
|
+
});
|
|
177
|
+
console.log(`🔊 MiNA.play text result:`, JSON.stringify(res));
|
|
178
|
+
} else {
|
|
179
|
+
res = await this.callUbus('mediaplayer', 'player_play_operation', {
|
|
180
|
+
action: 'play',
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
return res?.code === 0;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* 暂停播放
|
|
188
|
+
*/
|
|
189
|
+
async pause() {
|
|
190
|
+
const res = await this.callUbus('mediaplayer', 'player_play_operation', {
|
|
191
|
+
action: 'pause',
|
|
192
|
+
});
|
|
193
|
+
return res?.code === 0;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* 播放或暂停
|
|
198
|
+
*/
|
|
199
|
+
async playOrPause() {
|
|
200
|
+
const res = await this.callUbus('mediaplayer', 'player_play_operation', {
|
|
201
|
+
action: 'toggle',
|
|
202
|
+
});
|
|
203
|
+
return res?.code === 0;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* 停止播放
|
|
208
|
+
*/
|
|
209
|
+
async stop() {
|
|
210
|
+
const res = await this.callUbus('mediaplayer', 'player_play_operation', {
|
|
211
|
+
action: 'stop',
|
|
212
|
+
});
|
|
213
|
+
return res?.code === 0;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* 获取对话消息列表
|
|
218
|
+
*/
|
|
219
|
+
async getConversations(options?: {
|
|
220
|
+
limit?: number;
|
|
221
|
+
timestamp?: number;
|
|
222
|
+
}): Promise<MiConversations | undefined> {
|
|
223
|
+
const { limit = 10, timestamp } = options ?? {};
|
|
224
|
+
const res = await Http.get(
|
|
225
|
+
'https://userprofile.mina.mi.com/device_profile/v2/conversation',
|
|
226
|
+
{
|
|
227
|
+
limit,
|
|
228
|
+
timestamp,
|
|
229
|
+
requestId: uuid(),
|
|
230
|
+
source: 'dialogu',
|
|
231
|
+
hardware: this.account.device?.hardware,
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
account: this.account,
|
|
235
|
+
setAccount: updateMiAccount(this.account),
|
|
236
|
+
headers: {
|
|
237
|
+
'User-Agent':
|
|
238
|
+
'Mozilla/5.0 (Linux; Android 10; 000; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/119.0.6045.193 Mobile Safari/537.36 /XiaoMi/HybridView/ micoSoundboxApp/i appVersion/A_2.4.40',
|
|
239
|
+
Referer: 'https://userprofile.mina.mi.com/dialogue-note/index.html',
|
|
240
|
+
},
|
|
241
|
+
cookies: {
|
|
242
|
+
userId: this.account.userId,
|
|
243
|
+
serviceToken: this.account.serviceToken,
|
|
244
|
+
deviceId: this.account.device?.deviceId,
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
);
|
|
248
|
+
if (res.code !== 0) {
|
|
249
|
+
if (Debugger.debug) {
|
|
250
|
+
console.error('❌ getConversations failed', res);
|
|
251
|
+
}
|
|
252
|
+
return undefined;
|
|
253
|
+
}
|
|
254
|
+
return jsonDecode(res.data);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function clamp(value: number, min: number, max: number): number {
|
|
259
|
+
return Math.min(Math.max(value, min), max);
|
|
260
|
+
}
|
|
261
|
+
|
package/src/mi/miot.ts
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { jsonEncode } from '../utils/parse.js';
|
|
2
|
+
import { decodeMIoT, encodeFormData, encodeMIoT } from '../utils/codec.js';
|
|
3
|
+
import { Http } from '../utils/http.js';
|
|
4
|
+
import { updateMiAccount } from './common.js';
|
|
5
|
+
import { Debugger } from '../utils/debug.js';
|
|
6
|
+
import type { MIoTDevice, MiAccount } from './typing.js';
|
|
7
|
+
|
|
8
|
+
type MIoTAccount = MiAccount & { device: MIoTDevice };
|
|
9
|
+
|
|
10
|
+
export class MIoT {
|
|
11
|
+
account: MIoTAccount;
|
|
12
|
+
|
|
13
|
+
constructor(account: MIoTAccount) {
|
|
14
|
+
this.account = account;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
static async getDevice(account: MIoTAccount): Promise<MIoTAccount> {
|
|
18
|
+
if (account.sid !== 'xiaomiio') {
|
|
19
|
+
return account;
|
|
20
|
+
}
|
|
21
|
+
const devices = await MIoT.__callMIoT(account, 'POST', '/home/device_list', {
|
|
22
|
+
getVirtualModel: false,
|
|
23
|
+
getHuamiDevices: 0,
|
|
24
|
+
});
|
|
25
|
+
if (Debugger.debug) {
|
|
26
|
+
console.log('🐛 MIoT 设备列表:', jsonEncode(devices, { prettier: true }));
|
|
27
|
+
}
|
|
28
|
+
const device = (devices?.list ?? []).find((e: any) =>
|
|
29
|
+
[e.did, e.name, e.mac].includes(account.did),
|
|
30
|
+
);
|
|
31
|
+
if (device) {
|
|
32
|
+
account.device = device;
|
|
33
|
+
}
|
|
34
|
+
return account;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private static async __callMIoT(
|
|
38
|
+
account: MIoTAccount,
|
|
39
|
+
method: 'GET' | 'POST',
|
|
40
|
+
path: string,
|
|
41
|
+
_data?: any,
|
|
42
|
+
) {
|
|
43
|
+
const url = `https://api.io.mi.com/app${path}`;
|
|
44
|
+
const config = {
|
|
45
|
+
account,
|
|
46
|
+
setAccount: updateMiAccount(account),
|
|
47
|
+
rawResponse: true,
|
|
48
|
+
validateStatus: () => true,
|
|
49
|
+
headers: {
|
|
50
|
+
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/604.1',
|
|
51
|
+
'x-xiaomi-protocal-flag-cli': 'PROTOCAL-HTTP2',
|
|
52
|
+
},
|
|
53
|
+
cookies: {
|
|
54
|
+
userId: account.userId,
|
|
55
|
+
serviceToken: account.serviceToken,
|
|
56
|
+
PassportDeviceId: account.deviceId,
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const data = encodeMIoT(path, _data, account.pass!.ssecurity!);
|
|
61
|
+
if (Debugger.debug) {
|
|
62
|
+
console.log('MIoT 请求:', {
|
|
63
|
+
url,
|
|
64
|
+
method,
|
|
65
|
+
cookies: config.cookies,
|
|
66
|
+
body: data,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let res: any;
|
|
71
|
+
if (method === 'GET') {
|
|
72
|
+
res = await Http.get(url, data, config);
|
|
73
|
+
} else {
|
|
74
|
+
// POST body 使用 application/x-www-form-urlencoded 格式
|
|
75
|
+
const formData = encodeFormData(data);
|
|
76
|
+
if (Debugger.debug) {
|
|
77
|
+
console.log('POST body:', formData);
|
|
78
|
+
}
|
|
79
|
+
res = await Http.post(url, formData, {
|
|
80
|
+
...config,
|
|
81
|
+
headers: {
|
|
82
|
+
...config.headers,
|
|
83
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 检查错误
|
|
89
|
+
if (res.status === 401) {
|
|
90
|
+
console.error('❌ 401 错误响应:', res.data);
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// MiService 返回的是明文 JSON,直接解析
|
|
95
|
+
if (res.data && typeof res.data === 'object' && res.data.code === 0) {
|
|
96
|
+
return res.data.result;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 如果是加密字符串,尝试解密
|
|
100
|
+
if (typeof res.data === 'string') {
|
|
101
|
+
try {
|
|
102
|
+
res = await decodeMIoT(
|
|
103
|
+
account.pass!.ssecurity!,
|
|
104
|
+
data._nonce,
|
|
105
|
+
res.data,
|
|
106
|
+
res.headers['miot-content-encoding'] === 'GZIP',
|
|
107
|
+
);
|
|
108
|
+
return res?.result;
|
|
109
|
+
} catch (e) {
|
|
110
|
+
console.error('❌ 解密失败:', e);
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
console.error('❌ 未知响应格式:', res);
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private async _callMIoT(method: 'GET' | 'POST', path: string, data?: any) {
|
|
120
|
+
return MIoT.__callMIoT(this.account, method, path, data);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 获取 MIoT 设备列表
|
|
125
|
+
*/
|
|
126
|
+
async getDevices(getVirtualModel = false, getHuamiDevices = 0) {
|
|
127
|
+
const res = await this._callMIoT('POST', '/home/device_list', {
|
|
128
|
+
getVirtualModel: getVirtualModel,
|
|
129
|
+
getHuamiDevices: getHuamiDevices,
|
|
130
|
+
});
|
|
131
|
+
return res?.list;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 获取 MIoT 设备属性值
|
|
136
|
+
*/
|
|
137
|
+
async getProperty(scope: number, property: number) {
|
|
138
|
+
const res = await this._callMIoTSpec('prop/get', [
|
|
139
|
+
{
|
|
140
|
+
did: this.account.device.did,
|
|
141
|
+
siid: scope,
|
|
142
|
+
piid: property,
|
|
143
|
+
},
|
|
144
|
+
]);
|
|
145
|
+
return (res ?? [])?.[0]?.value;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* 设置 MIoT 设备属性值
|
|
150
|
+
*/
|
|
151
|
+
async setProperty(scope: number, property: number, value: any) {
|
|
152
|
+
const res = await this._callMIoTSpec('prop/set', [
|
|
153
|
+
{
|
|
154
|
+
did: this.account.device.did,
|
|
155
|
+
siid: scope,
|
|
156
|
+
piid: property,
|
|
157
|
+
value: value,
|
|
158
|
+
},
|
|
159
|
+
]);
|
|
160
|
+
return (res ?? [])?.[0]?.code === 0;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* 调用 MIoT 设备能力指令
|
|
165
|
+
*/
|
|
166
|
+
async doAction(scope: number, action: number, args: any = []) {
|
|
167
|
+
const res = await this._callMIoTSpec('action', {
|
|
168
|
+
did: this.account.device.did,
|
|
169
|
+
siid: scope,
|
|
170
|
+
aiid: action,
|
|
171
|
+
in: Array.isArray(args) ? args : [args],
|
|
172
|
+
});
|
|
173
|
+
return res?.code === 0;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* 调用 MIoT 设备 RPC 指令
|
|
178
|
+
*/
|
|
179
|
+
rpc(method: string, params: any, id = 1) {
|
|
180
|
+
return this._callMIoT('POST', `/home/rpc/${this.account.device.did}`, {
|
|
181
|
+
id,
|
|
182
|
+
method,
|
|
183
|
+
params,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private _callMIoTSpec(command: string, params: any, datasource = 2) {
|
|
188
|
+
return this._callMIoT('POST', `/miotspec/${command}`, {
|
|
189
|
+
params,
|
|
190
|
+
datasource,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
package/src/mi/typing.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 小米账号类型定义
|
|
3
|
+
*/
|
|
4
|
+
export interface MiAccount {
|
|
5
|
+
/** 小米 ID(数字) */
|
|
6
|
+
userId?: string;
|
|
7
|
+
/** 密码 */
|
|
8
|
+
password?: string;
|
|
9
|
+
/** 登录凭证 */
|
|
10
|
+
passToken?: string;
|
|
11
|
+
/** 服务类型 */
|
|
12
|
+
sid: 'micoapi' | 'xiaomiio';
|
|
13
|
+
/** 设备 ID */
|
|
14
|
+
deviceId?: string;
|
|
15
|
+
/** 服务 Token */
|
|
16
|
+
serviceToken?: string;
|
|
17
|
+
/** 登录态 */
|
|
18
|
+
pass?: MiPass;
|
|
19
|
+
/** 设备标识(名称/ID/MAC) */
|
|
20
|
+
did?: string;
|
|
21
|
+
/** 设备信息 */
|
|
22
|
+
device?: MiNADevice | MIoTDevice;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 登录态
|
|
27
|
+
*/
|
|
28
|
+
export interface MiPass {
|
|
29
|
+
code: number;
|
|
30
|
+
qs?: string;
|
|
31
|
+
_sign?: string;
|
|
32
|
+
callback?: string;
|
|
33
|
+
location?: string;
|
|
34
|
+
nonce?: string;
|
|
35
|
+
ssecurity?: string;
|
|
36
|
+
passToken?: string;
|
|
37
|
+
notificationUrl?: string;
|
|
38
|
+
cUserId?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* MiNA 设备
|
|
43
|
+
*/
|
|
44
|
+
export interface MiNADevice {
|
|
45
|
+
deviceID: string;
|
|
46
|
+
deviceId?: string;
|
|
47
|
+
miotDID: string;
|
|
48
|
+
name: string;
|
|
49
|
+
alias: string;
|
|
50
|
+
mac: string;
|
|
51
|
+
hardware: string;
|
|
52
|
+
serialNumber: string;
|
|
53
|
+
rom: string;
|
|
54
|
+
deviceSNProfile?: string;
|
|
55
|
+
extra?: {
|
|
56
|
+
fw_version: string;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* MIoT 设备
|
|
62
|
+
*/
|
|
63
|
+
export interface MIoTDevice {
|
|
64
|
+
did: string;
|
|
65
|
+
name: string;
|
|
66
|
+
mac: string;
|
|
67
|
+
model: string;
|
|
68
|
+
extra?: {
|
|
69
|
+
fw_version: string;
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 对话消息
|
|
75
|
+
*/
|
|
76
|
+
export interface MiConversation {
|
|
77
|
+
query: string;
|
|
78
|
+
time: number;
|
|
79
|
+
answers: Array<{
|
|
80
|
+
type: string;
|
|
81
|
+
tts?: string;
|
|
82
|
+
url?: string;
|
|
83
|
+
}>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 对话列表
|
|
88
|
+
*/
|
|
89
|
+
export interface MiConversations {
|
|
90
|
+
records: MiConversation[];
|
|
91
|
+
hasMore: boolean;
|
|
92
|
+
cursor?: number;
|
|
93
|
+
}
|