# utils-ok

> 一站式 Node.js 工具库 —— 字符串处理、文件操作、异步工具、云存储、数据库、AI 对话、自动化测试，开箱即用。

## 特性

- **跨平台** —— macOS / Windows / Linux 全兼容
- **按需引用** —— 每个模块独立导出，tree-shaking 友好
- **内置日志** —— 自带 `wLogger`，零配置即可输出结构化日志
- **云原生** —— 集成阿里云 OSS / MinIO / MySQL / OpenAI / 钉钉机器人
- **工程化** —— 内置 ESLint / blacklint 检查、Git 工具、Hosts 管理等 CLI 辅助

## 安装

```bash
pnpm add utils-ok
# 或
npm install utils-ok
# 或
yarn add utils-ok
```

## 快速开始

```typescript
import { strUtil, fileUtil, asyncUtil, baseUtil } from 'utils-ok';

// 字符串处理
strUtil.md5('hello');           // MD5 哈希
strUtil.random(16);             // 随机字符串
strUtil.md2html('# 标题');       // Markdown → HTML

// 文件操作
fileUtil.delete('/tmp/a.txt');  // 删除文件
fileUtil.tmpFile('.json');      // 获取临时文件路径
fileUtil.getJSON('/path/to/config.json');

// 异步工具
await asyncUtil.sleep(1000);    // 延时 1 秒

// 加密 & 工具
baseUtil.encrypt('data', 'key'); // DES 加密
baseUtil.uuid();                 // UUID v7
baseUtil.retry(fn, 3, 1000);     // 重试 3 次，间隔 1 秒
```

## API 参考

### 📦 数据处理

#### strUtil — 字符串工具

| 方法 | 说明 |
|------|------|
| `parseDBUri(uri)` | 解析数据库 URI，提取 host/port/user/pass/db |
| `isJsonStr(str)` | 判断是否为合法 JSON 字符串 |
| `safeIntNumber(val)` | 安全转整数 |
| `safeBoolean(val)` | 安全转布尔值 |
| `md5(str)` | MD5 哈希 |
| `hasUnicode(str)` | 检测是否含 Unicode 字符 |
| `isSimilar(str1, str2, opts)` | 字符串相似度比较 |
| `isSimilarCn(str1, str2)` | 中文相似度比较 |
| `md2html(markdown)` | Markdown 转 HTML（带样式） |
| `sqlStr(str)` | 转义 SQL 字符串中的单引号 |
| `regExp(str)` | 字符串 → RegExp 对象 |
| `random(len, prefix)` | 生成随机字母数字字符串 |
| `randomCn(len)` | 生成中文数字字符串 |
| `uid(n)` | 生成唯一 ID |
| `cutStrByLength(str, len)` | 按长度切割字符串 |
| `cutStrTail(length, str)` | 截断尾部字符串 |
| `cleanColor(str)` | 清除 ANSI 颜色码 |

#### arrUtil — 数组工具

| 方法 | 说明 |
|------|------|
| `sum(arr)` | 数字数组求和 |
| `getMostSimilar(target, list)` | 从列表中找最相似项 |
| `getUniq(list)` | 字符串数组去重 |
| `getUniqByKey(list, key)` | 对象数组按 key 去重 |
| `getUniqBySimilar(list, rate)` | 按相似度去重 |

#### objUtil — 对象工具

| 方法 | 说明 |
|------|------|
| `search(obj, key)` | 递归搜索对象中的匹配项 |
| `isRegExp(str)` | 判断是否为合法正则 |
| `clean(obj, keepEmptyStr)` | 清除 null/undefined 键 |
| `cleanKey(obj, key)` | 递归删除指定键 |
| `isObj(val)` | 判断是否为普通对象 |
| `isAsyncFunc(fn)` | 判断是否为异步函数 |

#### numUtil — 数字工具

| 方法 | 说明 |
|------|------|
| `randomInt(len)` | 生成指定长度随机整数 |

#### dateUtil — 日期工具

| 方法 | 说明 |
|------|------|
| `mm` | moment 实例（中文 locale） |
| `getTime(hours, sec)` | 获取时间戳，可选偏移 |
| `humanizeDuration(begin, end)` | 人性化时长显示 |
| `isWorkDay(date)` | 是否工作日 |
| `isFriday(date)` | 是否周五 |
| `isMonthEnd(date)` | 是否月末 |

### 📁 文件 & Shell

#### fileUtil — 文件操作

| 方法 | 说明 |
|------|------|
| `cp(from, to)` | 拷贝文件或文件夹 |
| `mockFile(target, sizeMb)` | 生成指定大小的文件 |
| `delete(file)` | 删除文件或文件夹 |
| `tmpFile(ext)` | 获取临时文件路径 |
| `tmpdir()` | 系统临时目录路径 |
| `getSizeKb(target)` | 文件/文件夹大小 (KB) |
| `checkPath(target)` | 路径合法性检查 |
| `clean(file)` | 清空文件内容 |
| `deleteOldFiles(dir, opts)` | 按时间删除旧文件 |
| `rename(old, new)` | 重命名文件 |
| `genEmptyFile(opts)` | 创建空白文件 |
| `cleanDir(dir)` | 清空文件夹 |
| `saveTempFile(urlOrStr)` | 下载 URL 或保存字符串为临时文件 |
| `saveTempFileSync(url)` | 同步下载文件 |
| `saveFileStr(str, dest)` | 保存字符串到文件（异步） |
| `saveFileStrSync(str, dest)` | 保存字符串到文件（同步） |
| `saveFileUrl(url, dest, name)` | 通过 URL 保存文件 |
| `getFileStr(target)` | 读取文件字符串 |
| `getFileRowCount(target)` | 获取文件行数 |
| `getMd5(file)` | 计算文件 MD5（异步） |
| `getMd5ByShell(file)` | 计算文件 MD5（同步） |
| `getFileBufferByUrl(url)` | 通过 HTTP 获取文件 Buffer |
| `getJsonByUrl(url)` | 通过 HTTP 获取 JSON 数据 |
| `getFirstLine(target)` | 读取文件第一行 |
| `getLines(target, limit)` | 读取文件行数组 |
| `getTailLinesStr(target, lines)` | 读取末尾几行 |
| `getHeadLinesStr(target, lines)` | 读取开头几行 |
| `createFile(target, data)` | 创建文件并写入初始数据 |
| `getJSON(target)` | 读取 JSON 文件 |
| `img2base64(target)` | 图片转 base64 |
| `dirWalker(dir)` | 递归遍历文件夹 |
| `lineOfCode(opts)` | 代码行数统计（依赖 cloc） |
| `savePressFileAndDeploy(url, dest)` | 下载压缩包并自动解压 |

#### shUtil — Shell 工具

| 方法 | 说明 |
|------|------|
| `processExist(name)` | 进程是否存在（跨平台） |
| `pidExist(pid)` | 指定 PID 是否存在 |
| `safeKill(pid)` | 安全终止进程树 |
| `cmdExist(cmd)` | 命令是否存在（跨平台） |
| `timeOut(cmd, sec)` | 命令超时执行 |
| `findFilesByText(opts)` | 按内容搜索文件（跨平台） |
| `getMacSysRunTimeInfo()` | macOS 系统运行信息¹ |
| `getMacAvailStorage()` | macOS 剩余存储空间¹ |
| `isHdpiDisplay()` | 检测 Retina / 4K 显示器¹ |
| `nohup(cmd, sec)` | 后台运行命令（仅 Unix） |
| `log(cmd, logname)` | 命令输出重定向到日志 |

> ¹ 仅 macOS 可用，Windows/Linux 调用时返回 `undefined` 并输出 warning。

#### hostUtil — Hosts 管理

| 方法 | 说明 |
|------|------|
| `getHosts(format)` | 获取 hosts 文件内容 |
| `bak(bakFile)` | 备份 hosts 文件 |
| `restoreBak(bakFile)` | 从备份恢复 |
| `check(ip, host)` | 检查 hosts 映射 |
| `upsert(ip, host)` | 插入或更新映射 |
| `remove(host, ip)` | 删除映射 |
| `setHosts(hosts)` | 批量设置映射 |
| `cleanHosts(hosts)` | 批量清理映射 |
| `removeHosts(hosts)` | 批量删除映射 |

### ⚡ 异步 & 基础工具

#### asyncUtil — 异步工具

| 方法 | 说明 |
|------|------|
| `sleep(ms, tick)` | 异步延时，可选每秒打印进度 |

#### baseUtil — 基础工具

| 方法 | 说明 |
|------|------|
| `sleep(ms)` | 异步延时（re-export） |
| `deeplog(obj, depth)` | 深度打印对象内容 |
| `getEnv(key)` | 获取环境变量 |
| `getProcessBar(total)` | 创建命令行进度条 |
| `store(key, value)` | 本地 KV 文件缓存读写 |
| `encrypt(str, key, iv)` | DES 加密 |
| `decrypt(encrypted, key, iv)` | DES 解密 |
| `genRsaKeyPairByNodeRsa()` | 生成 RSA 密钥对 |
| `rsaEncrypt(pubKey, raw)` | RSA 公钥加密 |
| `rsaDecrypt(priKey, data)` | RSA 私钥解密 |
| `uuid()` | 生成 UUID v7 |
| `getLocalIP()` | 获取本机局域网 IP |
| `waitUntil(fn, waitMs, intervalMs)` | 等待直到条件满足 |
| `warpPromise(promise, timeout)` | Promise 超时 + 中断 |
| `retry(fn, count, interval)` | 异步重试 |
| `safeRequire(file)` | 清除缓存后加载模块 |
| `getCallerData()` | 获取调用堆栈信息 |
| `getDepsPkgVersion(path, name)` | 获取依赖包版本号 |
| `logRes(resp)` | 格式化打印响应数据 |

#### resUtil — 响应构建

| 方法 | 说明 |
|------|------|
| `success(data)` | 成功响应 `{ code: 200 }` |
| `error(message)` | 服务器错误 `{ code: 500 }` |
| `authError(message)` | 认证错误 `{ code: 401 }` |
| `paramError(message)` | 参数错误 `{ code: 400 }` |
| `formatRes(data, code, msg)` | 自定义响应构建 |

### 🌐 网络 & 客户端

#### OssClient — 阿里云 OSS

```typescript
import { OssClient } from 'utils-ok';

const oss = new OssClient({
  region: 'oss-cn-hangzhou',
  bucket: 'my-bucket',
  accessKeyId: 'AK',
  accessKeySecret: 'SK',
});

await oss.put('./local.txt', 'remote.txt');     // 上传
await oss.get('remote.txt', './download.txt');   // 下载
await oss.putTxt('hello', 'hello.txt');          // 上传字符串
await oss.putJSON({ key: 'value' }, 'data.json');// 上传 JSON
const content = await oss.getFileStr('file.txt');// 读取内容
await oss.postDir('./dist', 'oss-dist/');        // 上传整个目录
```

| 方法 | 说明 |
|------|------|
| `put(localFile, ossFile)` | 上传本地文件 |
| `putUrl(url, ossFile)` | URL 转存 |
| `putTxt(str, ossFile)` | 上传字符串 |
| `putJSON(obj, ossFile)` | 上传 JSON 对象 |
| `store(key, obj, merge)` | 存储/更新 JSON 对象（支持合并） |
| `get(ossFile, localFile)` | 下载文件 |
| `getFilesByPath(ossDir, destDir)` | 下载文件夹 |
| `getFileStr(ossFile)` | 读取为字符串 |
| `getJSON(ossFile)` | 读取并解析 JSON |
| `deleteFile(ossFile)` | 删除文件 |
| `deleteFolder(ossPath)` | 递归清空文件夹 |
| `exist(ossFile)` | 检查是否存在 |
| `getFiles(ossPath)` | 获取文件列表 |
| `getFolders(ossPath)` | 获取子文件夹列表 |
| `postDir(localPath, ossFile)` | 递归上传目录 |
| `postDirZip(localPath, ossFile)` | 压缩为 ZIP 上传 |
| `postDirTarGz(localPath, ossFile)` | 压缩为 tar.gz 上传 |
| `setLifeCycle(days)` | 设置生命周期规则 |
| `getLifeCycle()` | 查询生命周期规则 |
| `delLifeCycle()` | 删除生命周期规则 |

#### MinioClient — MinIO 对象存储

API 与 OssClient 类似，额外支持：

| 方法 | 说明 |
|------|------|
| `getTmpUrl(ossFile, expires)` | 获取临时下载链接（默认 1h） |
| `uploadFile(localFile, ossFile)` | 上传本地文件 |
| `uploadDir(localPath, ossDir)` | 递归上传目录 |
| `downloadDir(ossDir, destDir)` | 下载目录 |
| `downloadFile(ossFile, localFile)` | 下载文件 |
| `getStr(ossFile)` | 读取为字符串 |
| `uploadDirZip(localPath, ossFile)` | 压缩为 ZIP 上传 |
| `uploadDirTarGz(localPath, ossFile)` | 压缩为 tar.gz 上传 |
| `setBucketPolicyGetFree(bktName)` | 设置 bucket 为公开读取 |
| `ossExist(ossFile)` | 检查文件或目录是否存在 |

#### MysqlClient / DBClient — MySQL 数据库

```typescript
import { DBClient } from 'utils-ok';

const db = new DBClient({ uri: 'mysql://user:pass@host:3306/dbname' });
// 内部基于 Sequelize，支持 Model 定义
await db.close();
```

#### DingClient — 钉钉机器人

```typescript
import { DingClient } from 'utils-ok';

const ding = new DingClient('your-access-token', 'secret-key');
await ding.send('Hello, 钉钉!');
await ding.sendMd('通知标题', '## 正文内容');
```

| 方法 | 说明 |
|------|------|
| `send(msg, atMobiles, atUserIds)` | 发送文本消息，支持 @ 用户 |
| `sendMd(title, msg, atMobiles, atUserIds)` | 发送 Markdown 消息 |

#### OpenAiClient — OpenAI 对话

```typescript
import { OpenAiClient } from 'utils-ok';

const ai = new OpenAiClient({ apiKey: 'sk-xxx' });
const reply = await ai.chatCn('你好');
```

| 方法 | 说明 |
|------|------|
| `chat(prompt)` | 简单对话 |
| `chatCn(prompt)` | 中文系统提示对话 |
| `chatEn(prompt)` | 英文系统提示对话 |
| `createChatCompletion(messages, opts)` | 创建聊天补全 |
| `createCompletion(prompt, opts)` | 创建补全请求 |

#### PopClient — 阿里云 POP SDK

封装阿里云 POP 通用调用客户端。

#### SlsClient — 阿里云 SLS 日志服务

封装阿里云 SLS 日志服务客户端。

#### YuqueClient — 语雀 API

封装语雀知识库 API 客户端。

### 🛠️ 工程辅助

#### gitUtil — Git 工具

| 方法 | 说明 |
|------|------|
| `safeCheckout(opts)` | 安全检出分支/commit（带超时重试） |
| `getGitDir()` | 获取 Git 仓库根目录 |
| `getBranchName()` | 获取当前分支名 |
| `getBranchDiffFiles(opts)` | 获取工作分支 diff 文件列表 |
| `getBranchSourceCommit()` | 获取当前分支起点 commit |
| `getLastCommit(opts)` | 获取最后一次 commit 详情 |
| `formatGitHttpUrl(remote)` | Git SSH URL → HTTP URL 转换 |
| `getPRList(branch)` | 查询分支上的 PR 列表 |

#### lintUtil — Lint 工具

| 方法 | 说明 |
|------|------|
| `lintProj(opts)` | 全量 lint（eslint + blacklint + tsc + 用例规范） |
| `lintFixProj()` | 自动修复 lint 问题 + 清除 `.only()` 标记 |
| `eslint()` | 单独执行 eslint 检查 |
| `tscLint()` | 单独执行 tsc 类型检查 |
| `blackLint(dirs)` | 单独执行 blacklint 检查 |
| `testDirLint(testDir)` | 用例命名规范检查 |

#### ProjUtil — 项目路径工具

```typescript
import { ProjUtil } from 'utils-ok';

const proj = new ProjUtil(__dirname);
proj.getPkgVersion();     // 获取版本号
proj.getResourcePath();   // 获取 resource 目录
proj.bin('eslint', true); // 执行 node_modules/.bin 下的命令
```

| 方法 | 说明 |
|------|------|
| `getPkgVersion()` | 获取 package.json 版本号 |
| `getPkg()` | 读取 package.json |
| `getProjPath()` | 获取项目路径 |
| `getRootProjPath()` | 获取项目根路径 |
| `getResourcePath()` | 获取 resource 目录 |
| `getModulesPath()` | 获取 node_modules 路径 |
| `bin(cmd, async, silent)` | 执行 .bin 下的命令 |

#### ffmpegUtil — 视频处理

| 方法 | 说明 |
|------|------|
| `mov2mp4(fileMov, fileMp4)` | MOV → MP4 转码 |
| `addPoster(srcMp4, pngFile)` | 插入封面首帧 |
| `getVideoLastImg(srcMp4, destPng)` | 提取视频最后一帧 |
| `setVideoLastImgAsPoster(srcMp4)` | 设置视频最后一帧为封面 |

### 🤖 浏览器自动化

#### WebDriver — Playwright 浏览器驱动

```typescript
import { WebDriver } from 'utils-ok';

const driver = new WebDriver({ headless: true });
await driver.init();
await driver.goto('https://example.com');
await driver.clickOn('#submit');
await driver.checkText('提交成功');
await driver.stop();
```

| 方法 | 说明 |
|------|------|
| `init()` | 初始化浏览器 |
| `stop()` | 关闭浏览器 |
| `goto(url)` | 导航到 URL |
| `clickOn(selector)` | 点击元素 |
| `input(text, selector)` | 输入文本 |
| `check(selector)` | 检查元素存在 |
| `checkText(text)` | 检查页面包含文本 |
| `checkMulti(selectors)` | 并行检查多个选择器 |
| `checkNotExists(selector)` | 检查元素不存在 |
| `getText(selector)` | 获取元素文本 |
| `waitForElement(selector, timeout)` | 等待元素出现 |
| `waitForAny(selectors, opts)` | 等待任意选择器出现 |
| `waitForFocusByCSS(css)` | 等待元素获得焦点 |
| `getElement(selector, opts)` | 查找单个元素 |
| `getElementsCount(selector)` | 获取匹配元素数量 |
| `hover(selector)` | 鼠标悬停 |
| `pressKeys(key, ...more)` | 键盘按键 |
| `mouse(action, ...args)` | 鼠标操作 |
| `keyboard(action, ...args)` | 键盘操作 |
| `dragAndDrop(source, target, ...)` | 拖拽元素 |
| `elementScreenShot(opts)` | 元素截图 |
| `redirect(url)` | JS 重定向 |
| `curl(clean)` | 获取当前 URL |
| `execute(script, args)` | 执行页面 JS |
| `coverage(opts)` | 代码覆盖率收集 |
| `webFetch(opts)` | 前端注入执行 fetch |
| `mixin(api)` | 混入自定义方法 |

#### RemoteLoginClient — 远程登录

```typescript
import { RemoteLoginClient } from 'utils-ok';

const client = new RemoteLoginClient({
  ip: '192.168.1.100',
  username: 'root',
  password: 'pass',
});
await client.ssh('ls -la');  // SSH 远程执行命令
await client.vnc();          // macOS 打开 VNC 连接
```

| 方法 | 说明 |
|------|------|
| `ssh(cmd)` | SSH 远程执行命令 |
| `vnc()` | 打开 VNC 连接（macOS） |

### 📋 日志

项目内置 `wLogger`（基于 winston），支持多级别日志输出：

```typescript
import { wLogger } from 'utils-ok';

const logger = wLogger();
logger.info('应用启动');
logger.debug('调试信息', { data: [1, 2, 3] });
logger.error('出错了', error);
```

## 跨平台支持

| 模块 | macOS | Windows | Linux |
|------|:-----:|:-------:|:-----:|
| 数据处理 (strUtil/arrUtil/...) | ✅ | ✅ | ✅ |
| 文件操作 (fileUtil) | ✅ | ✅ | ✅ |
| Shell 工具 (shUtil) | ✅ | ✅¹ | ✅ |
| Hosts 管理 (hostUtil) | ✅ | ✅ | ✅ |
| Git 工具 (gitUtil) | ✅ | ✅² | ✅ |
| 云存储 (OssClient/MinioClient) | ✅ | ✅ | ✅ |
| 数据库 (DBClient) | ✅ | ✅ | ✅ |
| 浏览器自动化 (WebDriver) | ✅ | ✅ | ✅ |
| AI 对话 (OpenAiClient) | ✅ | ✅ | ✅ |
| 钉钉机器人 (DingClient) | ✅ | ✅ | ✅ |

> ¹ `getMacSysRunTimeInfo`、`isHdpiDisplay`、`nohup` 为 Unix/macOS 专属功能。<br/>
> ² Windows 需要安装 Git for Windows。

## 版本历史

| 版本 | 说明 |
|------|------|
| 2.0.0 | 全面跨平台重构，支持 Windows/macOS/Linux |
| 1.x | 初始版本，仅 macOS/Linux |

## License

MIT
