mirror of
https://github.com/idootop/mi-gpt.git
synced 2025-04-07 10:11:31 +00:00
msic: init project
This commit is contained in:
commit
3d472763c1
3
.env.example
Normal file
3
.env.example
Normal file
|
@ -0,0 +1,3 @@
|
|||
MI_USER="Xiaomi Account"
|
||||
MI_PASS="Account Password"
|
||||
MI_DID="Device ID or Name (optional - fill in after retrieving from the device list)"
|
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
node_modules
|
||||
dist
|
||||
.DS_Store
|
||||
.yarn
|
||||
.env
|
||||
.mi.json
|
19
.vscode/launch.json
vendored
Normal file
19
.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Test.ts",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"args": ["${workspaceFolder}/tests/index.ts"],
|
||||
"runtimeArgs": [
|
||||
"--no-warnings",
|
||||
"--experimental-specifier-resolution=node",
|
||||
"--loader",
|
||||
"./tests/esm-loader.js"
|
||||
],
|
||||
"cwd": "${workspaceRoot}",
|
||||
"internalConsoleOptions": "openOnSessionStart"
|
||||
}
|
||||
]
|
||||
}
|
24
.vscode/settings.json
vendored
Normal file
24
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"files.eol": "\n",
|
||||
"editor.tabSize": 2,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"eslint.validate": [
|
||||
"javascript",
|
||||
"javascriptreact",
|
||||
"typescript",
|
||||
"typescriptreact"
|
||||
],
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
"search.exclude": {
|
||||
"**/.git": true,
|
||||
"**/node_modules": true,
|
||||
"*.lock": true
|
||||
},
|
||||
"files.exclude": {
|
||||
"**/.git": true,
|
||||
"**/node_modules": true
|
||||
}
|
||||
}
|
51
README.md
Normal file
51
README.md
Normal file
|
@ -0,0 +1,51 @@
|
|||
# MiGPT
|
||||
|
||||
> 🏠 Speak to Your Home – MiGPT Makes it Possible.
|
||||
|
||||
In a world where home is not just a place but an extension of our digital lives, MiGPT stands as a pioneering force, redefining the essence of smart living. It's not just about automation; it's about creating a home that understands you, responds to you, and evolves with you. With MiGPT, we've crafted an experience that transcends conventional smart home concepts, offering a seamless fusion of the XiaoAI speaker and Mi Home devices with the cutting-edge capabilities of ChatGPT.
|
||||
|
||||
## ✨ Highlights
|
||||
|
||||
- **Voice-Enabled Omnipresence**: With MiGPT, your voice becomes the universal remote to your living space. Command your environment with the ease of a spoken word, and watch as your home reacts with precision and grace.
|
||||
- **Intelligent Interactions**: MiGPT doesn't just listen; it understands context, learns preferences, and anticipates needs, turning mundane interactions into meaningful conversations with your home.
|
||||
- **AI and IoT Symbiosis**: At the core of MiGPT lies the perfect harmony between AI and IoT, creating a bridge between your digital commands and physical devices, ensuring that every element of your home is interconnected and intelligent.
|
||||
- **Futuristic Home Automation**: Step into the future where MiGPT leads the charge in home automation. It's not just about controlling devices; it's about a home that adapts to your lifestyle, mood, and - Voice-Powered Mastery: Unleash the full potential of your smart home with the power of your voice. MiGPT elevates voice control to new heights, offering unparalleled control over your home's ecosystem.
|
||||
- **Unprecedented Home Intelligence**: With MiGPT, experience a level of home intelligence that was once the realm of science fiction. Your home doesn't just perform tasks; it thinks, learns, and becomes an integral part of your life.
|
||||
|
||||
## ⚡️ Installation
|
||||
|
||||
```shell
|
||||
npm install mi-gpt # coming soon
|
||||
|
||||
# or
|
||||
yarn add mi-gpt
|
||||
|
||||
# or
|
||||
pnpm install mi-gpt
|
||||
```
|
||||
|
||||
## 🔥 Usage
|
||||
|
||||
```typescript
|
||||
import { MiGPT } from "mi-gpt";
|
||||
|
||||
async function main() {
|
||||
// coming soon
|
||||
}
|
||||
|
||||
main();
|
||||
```
|
||||
|
||||
## 🌈 Embrace the future
|
||||
|
||||
Welcome to the era of intuitive living with MiGPT, where every command is a conversation, and every interaction is an opportunity for your home to become more in sync with you. Imagine a space that not only listens but also comprehends and evolves—a living space that's as dynamic and intelligent as the world around you. This is not just smart home technology; this is MiGPT, the heartbeat of your AI-driven home, where the future of home automation isn't just arriving, it's already here, ready to transform your daily living into an experience of effortless intelligence.
|
||||
|
||||
Embrace the revolution. Embrace MiGPT.
|
||||
|
||||
## ❤️ Acknowledgement
|
||||
|
||||
- https://www.mi.com/
|
||||
- https://openai.com/
|
||||
- https://github.com/yihong0618/xiaogpt
|
||||
- https://github.com/inu1255/mi-service
|
||||
- https://github.com/Yonsm/MiService
|
58
package.json
Normal file
58
package.json
Normal file
|
@ -0,0 +1,58 @@
|
|||
{
|
||||
"name": "mi-gpt",
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"description": "Seamlessly integrate your XiaoAI speaker and Mi Home devices with ChatGPT for an enhanced smart home experience.",
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
"name": "Del Wang",
|
||||
"email": "hello@xbox.work",
|
||||
"url": "https://github.com/idootop"
|
||||
},
|
||||
"keywords": [
|
||||
"GPT",
|
||||
"ChatGPT",
|
||||
"mi",
|
||||
"xiaomi",
|
||||
"xiaoai",
|
||||
"mi-home",
|
||||
"home-assistant"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"prepublish": "npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.6.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.4.9",
|
||||
"dotenv": "^16.3.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsup": "^8.0.1",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"main": "dist/index.cjs",
|
||||
"module": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.cjs",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"homepage": "https://github.com/idootop/mi-gpt",
|
||||
"bugs": {
|
||||
"url": "https://github.com/idootop/mi-gpt/issues"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/idootop/mi-gpt.git"
|
||||
}
|
||||
}
|
4
src/index.ts
Normal file
4
src/index.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
import { println } from "./utils/base";
|
||||
|
||||
// todo
|
||||
println("hello world!");
|
143
src/utils/base.ts
Normal file
143
src/utils/base.ts
Normal file
|
@ -0,0 +1,143 @@
|
|||
import { isEmpty } from "./is";
|
||||
|
||||
export function timestamp() {
|
||||
return new Date().getTime();
|
||||
}
|
||||
|
||||
export async function delay(time: number) {
|
||||
return new Promise<void>((resolve) => setTimeout(resolve, time));
|
||||
}
|
||||
|
||||
export function println(...v: any[]) {
|
||||
console.log(...v);
|
||||
}
|
||||
|
||||
export function printJson(obj: any) {
|
||||
console.log(JSON.stringify(obj, undefined, 4));
|
||||
}
|
||||
|
||||
export function firstOf<T = any>(datas?: T[]) {
|
||||
return datas ? (datas.length < 1 ? undefined : datas[0]) : undefined;
|
||||
}
|
||||
|
||||
export function lastOf<T = any>(datas?: T[]) {
|
||||
return datas
|
||||
? datas.length < 1
|
||||
? undefined
|
||||
: datas[datas.length - 1]
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export function randomInt(min: number, max?: number) {
|
||||
if (!max) {
|
||||
max = min;
|
||||
min = 0;
|
||||
}
|
||||
return Math.floor(Math.random() * (max - min + 1) + min);
|
||||
}
|
||||
|
||||
export function pickOne<T = any>(datas: T[]) {
|
||||
return datas.length < 1 ? undefined : datas[randomInt(datas.length - 1)];
|
||||
}
|
||||
|
||||
export function range(start: number, end?: number) {
|
||||
if (!end) {
|
||||
end = start;
|
||||
start = 0;
|
||||
}
|
||||
return Array.from({ length: end - start }, (_, index) => start + index);
|
||||
}
|
||||
|
||||
export function clamp(num: number, min: number, max: number): number {
|
||||
return num < max ? (num > min ? num : min) : max;
|
||||
}
|
||||
|
||||
export function toInt(str: string) {
|
||||
return parseInt(str, 10);
|
||||
}
|
||||
|
||||
export function toDouble(str: string) {
|
||||
return parseFloat(str);
|
||||
}
|
||||
|
||||
export function toFixed(n: number, fractionDigits = 2) {
|
||||
let s = n.toFixed(fractionDigits);
|
||||
while (s[s.length - 1] === "0") {
|
||||
s = s.substring(0, s.length - 1);
|
||||
}
|
||||
if (s[s.length - 1] === ".") {
|
||||
s = s.substring(0, s.length - 1);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
export function toSet<T = any>(datas: T[], byKey?: (e: T) => any) {
|
||||
if (byKey) {
|
||||
const keys: any = {};
|
||||
const newDatas: T[] = [];
|
||||
datas.forEach((e) => {
|
||||
const key = jsonEncode({ key: byKey(e) }) as any;
|
||||
if (!keys[key]) {
|
||||
newDatas.push(e);
|
||||
keys[key] = true;
|
||||
}
|
||||
});
|
||||
return newDatas;
|
||||
}
|
||||
return Array.from(new Set(datas));
|
||||
}
|
||||
|
||||
export function jsonEncode(obj: any, options?: { prettier?: boolean }) {
|
||||
const { prettier } = options ?? {};
|
||||
try {
|
||||
return prettier ? JSON.stringify(obj, undefined, 4) : JSON.stringify(obj);
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function jsonDecode(json: string | undefined) {
|
||||
if (json == undefined) return undefined;
|
||||
try {
|
||||
return JSON.parse(json!);
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function withDefault<T = any>(e: any, defaultValue: T): T {
|
||||
return isEmpty(e) ? defaultValue : e;
|
||||
}
|
||||
|
||||
export function removeEmpty<T = any>(data: T): T {
|
||||
if (Array.isArray(data)) {
|
||||
return data.filter((e) => e != undefined) as any;
|
||||
}
|
||||
const res = {} as any;
|
||||
for (const key in data) {
|
||||
if (data[key] != undefined) {
|
||||
res[key] = data[key];
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
export const deepClone = <T>(obj: T): T => {
|
||||
if (obj === null || typeof obj !== "object") {
|
||||
return obj;
|
||||
}
|
||||
if (Array.isArray(obj)) {
|
||||
const copy: any[] = [];
|
||||
obj.forEach((item, index) => {
|
||||
copy[index] = deepClone(item);
|
||||
});
|
||||
return copy as unknown as T;
|
||||
}
|
||||
const copy = {} as T;
|
||||
for (const key in obj) {
|
||||
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||
(copy as any)[key] = deepClone((obj as any)[key]);
|
||||
}
|
||||
}
|
||||
return copy;
|
||||
};
|
104
src/utils/http.ts
Normal file
104
src/utils/http.ts
Normal file
|
@ -0,0 +1,104 @@
|
|||
import axios, { AxiosRequestConfig, CreateAxiosDefaults } from "axios";
|
||||
import { isNotEmpty } from "./is";
|
||||
|
||||
const _baseConfig: CreateAxiosDefaults = {
|
||||
timeout: 10 * 1000,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:107.0) Gecko/20100101 Firefox/107.0",
|
||||
},
|
||||
};
|
||||
|
||||
const _http = axios.create(_baseConfig);
|
||||
|
||||
interface HttpError {
|
||||
isError: true;
|
||||
error: any;
|
||||
code: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
type RequestConfig = AxiosRequestConfig<any> & {
|
||||
rawResponse?: boolean;
|
||||
cookies?: Record<string, string | number | boolean | undefined>;
|
||||
};
|
||||
|
||||
_http.interceptors.response.use(
|
||||
(res) => {
|
||||
const config: any = res.config;
|
||||
if (config.rawResponse) {
|
||||
return res;
|
||||
}
|
||||
return res.data;
|
||||
},
|
||||
(err) => {
|
||||
const error = err.response?.data?.error ?? err.response?.data ?? err;
|
||||
const apiError: HttpError = {
|
||||
error: err,
|
||||
isError: true,
|
||||
code: error.code ?? "UNKNOWN CODE",
|
||||
message: error.message ?? "UNKNOWN ERROR",
|
||||
};
|
||||
console.error(
|
||||
"❌ Network request failed:",
|
||||
apiError.code,
|
||||
apiError.message,
|
||||
error
|
||||
);
|
||||
return apiError;
|
||||
}
|
||||
);
|
||||
|
||||
class HTTPClient {
|
||||
async get<T = any>(
|
||||
url: string,
|
||||
query?:
|
||||
| Record<string, string | number | boolean | undefined>
|
||||
| RequestConfig,
|
||||
config?: RequestConfig
|
||||
): Promise<T | HttpError> {
|
||||
if (config === undefined) {
|
||||
config = query;
|
||||
query = undefined;
|
||||
}
|
||||
return _http.get<T>(
|
||||
HTTPClient._buildURL(url, query),
|
||||
HTTPClient._buildConfig(config)
|
||||
) as any;
|
||||
}
|
||||
|
||||
async post<T = any>(
|
||||
url: string,
|
||||
data?: any,
|
||||
config?: RequestConfig
|
||||
): Promise<T | HttpError> {
|
||||
return _http.post<T>(url, data, HTTPClient._buildConfig(config)) as any;
|
||||
}
|
||||
|
||||
private static _buildURL = (url: string, query?: Record<string, any>) => {
|
||||
const _url = new URL(url);
|
||||
for (const [key, value] of Object.entries(query ?? {})) {
|
||||
if (isNotEmpty(value)) {
|
||||
_url.searchParams.append(key, value.toString());
|
||||
}
|
||||
}
|
||||
return _url.href;
|
||||
};
|
||||
|
||||
private static _buildConfig = (config?: RequestConfig) => {
|
||||
if (config?.cookies) {
|
||||
config.headers = {
|
||||
...config.headers,
|
||||
Cookie: Object.entries(config.cookies)
|
||||
.map(
|
||||
([key, value]) => `${key}=${value == null ? "" : value.toString()};`
|
||||
)
|
||||
.join(" "),
|
||||
};
|
||||
}
|
||||
return config;
|
||||
};
|
||||
}
|
||||
|
||||
export const Http = new HTTPClient();
|
76
src/utils/io.ts
Normal file
76
src/utils/io.ts
Normal file
|
@ -0,0 +1,76 @@
|
|||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
|
||||
import { jsonDecode, jsonEncode } from "./json";
|
||||
|
||||
export const kRoot = process.cwd();
|
||||
|
||||
export const kEnvs = process.env as any;
|
||||
|
||||
export const exists = (filePath: string) => fs.existsSync(filePath);
|
||||
|
||||
export const deleteFile = (filePath: string) => {
|
||||
try {
|
||||
fs.rmSync(filePath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const readFile = <T = any>(
|
||||
filePath: string,
|
||||
options?: fs.WriteFileOptions
|
||||
) => {
|
||||
const dirname = path.dirname(filePath);
|
||||
if (!fs.existsSync(dirname)) {
|
||||
return undefined;
|
||||
}
|
||||
return new Promise<T | undefined>((resolve) => {
|
||||
fs.readFile(filePath, options, (err, data) => {
|
||||
resolve(err ? undefined : (data as any));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const writeFile = (
|
||||
filePath: string,
|
||||
data: string | NodeJS.ArrayBufferView,
|
||||
options?: fs.WriteFileOptions
|
||||
) => {
|
||||
const dirname = path.dirname(filePath);
|
||||
if (!fs.existsSync(dirname)) {
|
||||
fs.mkdirSync(dirname, { recursive: true });
|
||||
}
|
||||
return new Promise<boolean>((resolve) => {
|
||||
if (options) {
|
||||
fs.writeFile(filePath, data, options, (err) => {
|
||||
resolve(err ? false : true);
|
||||
});
|
||||
} else {
|
||||
fs.writeFile(filePath, data, (err) => {
|
||||
resolve(err ? false : true);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const readString = (filePath: string) =>
|
||||
readFile<string>(filePath, "utf8");
|
||||
|
||||
export const writeString = (filePath: string, content: string) =>
|
||||
writeFile(filePath, content, "utf8");
|
||||
|
||||
export const readJSON = async (filePath: string) =>
|
||||
jsonDecode(await readFile<string>(filePath, "utf8"));
|
||||
|
||||
export const writeJSON = (filePath: string, content: any) =>
|
||||
writeFile(filePath, jsonEncode(content) ?? "", "utf8");
|
||||
|
||||
export const getFiles = (dir: string) => {
|
||||
return new Promise<string[]>((resolve) => {
|
||||
fs.readdir(dir, (err, files) => {
|
||||
resolve(err ? [] : files);
|
||||
});
|
||||
});
|
||||
};
|
62
src/utils/is.ts
Normal file
62
src/utils/is.ts
Normal file
|
@ -0,0 +1,62 @@
|
|||
export function isNaN(e: unknown): boolean {
|
||||
return Number.isNaN(e);
|
||||
}
|
||||
|
||||
export function isNull(e: unknown): boolean {
|
||||
return e === null;
|
||||
}
|
||||
|
||||
export function isUndefined(e: unknown): boolean {
|
||||
return e === undefined;
|
||||
}
|
||||
|
||||
export function isNullish(e: unknown): boolean {
|
||||
return e === null || e === undefined;
|
||||
}
|
||||
|
||||
export function isNotNullish(e: unknown): boolean {
|
||||
return !isNullish(e);
|
||||
}
|
||||
|
||||
export function isNumber(e: unknown): boolean {
|
||||
return typeof e === 'number' && !isNaN(e);
|
||||
}
|
||||
|
||||
export function isString(e: unknown): boolean {
|
||||
return typeof e === 'string';
|
||||
}
|
||||
|
||||
export function isArray(e: unknown): boolean {
|
||||
return Array.isArray(e);
|
||||
}
|
||||
|
||||
export function isObject(e: unknown): boolean {
|
||||
return typeof e === 'object' && isNotNullish(e);
|
||||
}
|
||||
|
||||
export function isEmpty(e: any): boolean {
|
||||
if (e?.size ?? 0 > 0) return false;
|
||||
return (
|
||||
isNaN(e) ||
|
||||
isNullish(e) ||
|
||||
(isString(e) && (e.length < 1 || !/\S/.test(e))) ||
|
||||
(isArray(e) && e.length < 1) ||
|
||||
(isObject(e) && Object.keys(e).length < 1)
|
||||
);
|
||||
}
|
||||
|
||||
export function isNotEmpty(e: unknown): boolean {
|
||||
return !isEmpty(e);
|
||||
}
|
||||
|
||||
export function isStringNumber(e: any): boolean {
|
||||
return isString(e) && isNotEmpty(e) && !isNaN(Number(e));
|
||||
}
|
||||
|
||||
export function isFunction(e: unknown): boolean {
|
||||
return typeof e === 'function';
|
||||
}
|
||||
|
||||
export function isClass(e: any): boolean {
|
||||
return isFunction(e) && e.toString().startsWith('class ');
|
||||
}
|
8
tests/esm-loader.js
Normal file
8
tests/esm-loader.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
// https://github.com/TypeStrong/ts-node/discussions/1450#discussioncomment-1806115
|
||||
import { resolve as resolveTs } from 'ts-node/esm';
|
||||
|
||||
export function resolve(specifier, ctx, defaultResolve) {
|
||||
return resolveTs(specifier, ctx, defaultResolve);
|
||||
}
|
||||
|
||||
export { load, transformSource } from 'ts-node/esm';
|
9
tests/index.ts
Normal file
9
tests/index.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import dotenv from "dotenv";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
async function main() {
|
||||
console.log("hello world!");
|
||||
}
|
||||
|
||||
main();
|
13
tsconfig.json
Normal file
13
tsconfig.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"strict": true,
|
||||
"declaration": true,
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"esModuleInterop": true,
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
16
tsup.config.ts
Normal file
16
tsup.config.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { defineConfig } from "tsup";
|
||||
|
||||
export default defineConfig(() => ({
|
||||
entry: ["src/index.ts"],
|
||||
outDir: "dist",
|
||||
target: "node16",
|
||||
platform: "node",
|
||||
format: ["esm", "cjs"],
|
||||
splitting: false,
|
||||
sourcemap: false,
|
||||
treeshake: true,
|
||||
minify: true,
|
||||
clean: true,
|
||||
shims: true,
|
||||
dts: true, // Generate declaration file
|
||||
}));
|
Loading…
Reference in New Issue
Block a user