msic: init project

This commit is contained in:
WJG 2024-01-24 23:14:28 +08:00
commit 3d472763c1
No known key found for this signature in database
GPG Key ID: 258474EF8590014A
16 changed files with 1731 additions and 0 deletions

3
.env.example Normal file
View 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
View File

@ -0,0 +1,6 @@
node_modules
dist
.DS_Store
.yarn
.env
.mi.json

19
.vscode/launch.json vendored Normal file
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,4 @@
import { println } from "./utils/base";
// todo
println("hello world!");

143
src/utils/base.ts Normal file
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,9 @@
import dotenv from "dotenv";
dotenv.config();
async function main() {
console.log("hello world!");
}
main();

13
tsconfig.json Normal file
View 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
View 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
}));

1135
yarn.lock Normal file

File diff suppressed because it is too large Load Diff