hikvision-cli 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/dist/commands/binding.d.ts +6 -0
- package/dist/commands/binding.d.ts.map +1 -0
- package/dist/commands/binding.js +274 -0
- package/dist/commands/binding.js.map +1 -0
- package/dist/commands/card.d.ts +6 -0
- package/dist/commands/card.d.ts.map +1 -0
- package/dist/commands/card.js +259 -0
- package/dist/commands/card.js.map +1 -0
- package/dist/commands/group.d.ts +6 -0
- package/dist/commands/group.d.ts.map +1 -0
- package/dist/commands/group.js +87 -0
- package/dist/commands/group.js.map +1 -0
- package/dist/commands/org.d.ts +6 -0
- package/dist/commands/org.d.ts.map +1 -0
- package/dist/commands/org.js +248 -0
- package/dist/commands/org.js.map +1 -0
- package/dist/commands/person.d.ts +6 -0
- package/dist/commands/person.d.ts.map +1 -0
- package/dist/commands/person.js +563 -0
- package/dist/commands/person.js.map +1 -0
- package/dist/commands/service.d.ts +6 -0
- package/dist/commands/service.d.ts.map +1 -0
- package/dist/commands/service.js +153 -0
- package/dist/commands/service.js.map +1 -0
- package/dist/commands/sync.d.ts +6 -0
- package/dist/commands/sync.d.ts.map +1 -0
- package/dist/commands/sync.js +138 -0
- package/dist/commands/sync.js.map +1 -0
- package/dist/commands/system.d.ts +6 -0
- package/dist/commands/system.d.ts.map +1 -0
- package/dist/commands/system.js +167 -0
- package/dist/commands/system.js.map +1 -0
- package/dist/commands/vehicle.d.ts +12 -0
- package/dist/commands/vehicle.d.ts.map +1 -0
- package/dist/commands/vehicle.js +550 -0
- package/dist/commands/vehicle.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +94 -0
- package/dist/index.js.map +1 -0
- package/dist/services/hikvisionClient.d.ts +25 -0
- package/dist/services/hikvisionClient.d.ts.map +1 -0
- package/dist/services/hikvisionClient.js +59 -0
- package/dist/services/hikvisionClient.js.map +1 -0
- package/dist/utils/config.d.ts +66 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +213 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/output.d.ts +42 -0
- package/dist/utils/output.d.ts.map +1 -0
- package/dist/utils/output.js +157 -0
- package/dist/utils/output.js.map +1 -0
- package/dist/utils/validation.d.ts +52 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +171 -0
- package/dist/utils/validation.js.map +1 -0
- package/package.json +29 -0
- package/src/commands/binding.ts +305 -0
- package/src/commands/card.ts +238 -0
- package/src/commands/group.ts +91 -0
- package/src/commands/org.ts +228 -0
- package/src/commands/person.ts +596 -0
- package/src/commands/service.ts +174 -0
- package/src/commands/sync.ts +156 -0
- package/src/commands/system.ts +137 -0
- package/src/commands/vehicle.ts +572 -0
- package/src/index.ts +111 -0
- package/src/services/hikvisionClient.ts +74 -0
- package/src/types/cli-table3.d.ts +20 -0
- package/src/utils/config.ts +199 -0
- package/src/utils/output.ts +181 -0
- package/src/utils/validation.ts +160 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hikvision 客户端工厂
|
|
3
|
+
*
|
|
4
|
+
* 组合 NodeHttpClient 和 API 创建完整客户端
|
|
5
|
+
* 兼容 CLI 和 Server 使用
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { NodeHttpClient } from 'hikvision-core-node';
|
|
9
|
+
import { PersonAPI } from 'hikvision-core-node';
|
|
10
|
+
import { CardAPI } from 'hikvision-core-node';
|
|
11
|
+
import { VehicleAPI } from 'hikvision-core-node';
|
|
12
|
+
import { GroupAPI } from 'hikvision-core-node';
|
|
13
|
+
import { PersonService } from 'hikvision-services';
|
|
14
|
+
import { CardService } from 'hikvision-services';
|
|
15
|
+
import { VehicleService } from 'hikvision-services';
|
|
16
|
+
import { HikvisionConfig } from 'hikvision-api-lib';
|
|
17
|
+
import type { CLIConfig } from '../utils/config';
|
|
18
|
+
|
|
19
|
+
export interface HikvisionClientWrapper {
|
|
20
|
+
client: NodeHttpClient;
|
|
21
|
+
personService: PersonService;
|
|
22
|
+
cardService: CardService;
|
|
23
|
+
vehicleService: VehicleService;
|
|
24
|
+
groupService: GroupAPI;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 将 CLIConfig 转换为 HikvisionConfig
|
|
29
|
+
*/
|
|
30
|
+
function toHikvisionConfig(cfg: CLIConfig | null): HikvisionConfig {
|
|
31
|
+
if (!cfg) {
|
|
32
|
+
throw new Error('配置无效,请先运行 hikvision-cli config set 进行配置');
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
host: cfg.host,
|
|
36
|
+
appKey: cfg.appKey,
|
|
37
|
+
appSecret: cfg.appSecret,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 创建 Hikvision 客户端实例(Node.js 版)
|
|
43
|
+
*/
|
|
44
|
+
export function createHikvisionClient(config: CLIConfig | null): HikvisionClientWrapper {
|
|
45
|
+
const hikConfig = toHikvisionConfig(config);
|
|
46
|
+
|
|
47
|
+
// 创建 NodeHttpClient
|
|
48
|
+
const httpClient = new NodeHttpClient({
|
|
49
|
+
host: hikConfig.host,
|
|
50
|
+
appKey: hikConfig.appKey,
|
|
51
|
+
appSecret: hikConfig.appSecret,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// 创建 API 实例(注入 IHttpClient)
|
|
55
|
+
const personAPI = new PersonAPI(httpClient);
|
|
56
|
+
const cardAPI = new CardAPI(httpClient);
|
|
57
|
+
const vehicleAPI = new VehicleAPI(httpClient);
|
|
58
|
+
const groupAPI = new GroupAPI(httpClient);
|
|
59
|
+
|
|
60
|
+
// 创建 Service 实例(注入 API)
|
|
61
|
+
const personService = new PersonService(personAPI);
|
|
62
|
+
const cardService = new CardService(cardAPI);
|
|
63
|
+
const vehicleService = new VehicleService(vehicleAPI);
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
client: httpClient,
|
|
67
|
+
personService,
|
|
68
|
+
cardService,
|
|
69
|
+
vehicleService,
|
|
70
|
+
groupService: groupAPI,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type { HikvisionConfig } from 'hikvision-api-lib';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
declare module 'cli-table3' {
|
|
2
|
+
interface TableOptions {
|
|
3
|
+
head?: string[];
|
|
4
|
+
colWidths?: number[];
|
|
5
|
+
style?: {
|
|
6
|
+
head?: string[];
|
|
7
|
+
border?: string[];
|
|
8
|
+
'padding-left'?: number[];
|
|
9
|
+
'padding-right'?: number[];
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
class Table {
|
|
14
|
+
constructor(options?: TableOptions);
|
|
15
|
+
push(row: any[]): void;
|
|
16
|
+
toString(): string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export = Table;
|
|
20
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI 配置管理
|
|
3
|
+
*
|
|
4
|
+
* 配置优先级(从高到低):
|
|
5
|
+
* 1. 环境变量(HIK_HOST, HIK_APP_KEY, HIK_APP_SECRET)
|
|
6
|
+
* 2. 项目配置文件(packages/server/config.json)
|
|
7
|
+
* 3. 用户配置文件(~/.hikvision-cli/config.json)
|
|
8
|
+
* 4. 默认值
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import * as os from 'os';
|
|
14
|
+
|
|
15
|
+
export interface CLIConfig {
|
|
16
|
+
host: string;
|
|
17
|
+
appKey: string;
|
|
18
|
+
appSecret: string;
|
|
19
|
+
outputFormat: 'table' | 'json' | 'yaml';
|
|
20
|
+
pageSize: number;
|
|
21
|
+
logLevel: 'debug' | 'info' | 'warn' | 'error';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 非敏感默认值
|
|
25
|
+
const DEFAULT_CONFIG: Omit<CLIConfig, 'appKey' | 'appSecret'> = {
|
|
26
|
+
host: '127.0.0.1:18443',
|
|
27
|
+
outputFormat: 'table',
|
|
28
|
+
pageSize: 20,
|
|
29
|
+
logLevel: 'info',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const CONFIG_DIR = path.join(os.homedir(), '.hikvision-cli');
|
|
33
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
34
|
+
|
|
35
|
+
export class ConfigManager {
|
|
36
|
+
private config: CLIConfig | null = null;
|
|
37
|
+
|
|
38
|
+
constructor() {
|
|
39
|
+
this.config = this.loadConfig();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 加载配置:优先用户配置,其次环境变量
|
|
44
|
+
*/
|
|
45
|
+
private loadConfig(): CLIConfig | null {
|
|
46
|
+
// 1. 尝试加载用户配置文件
|
|
47
|
+
try {
|
|
48
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
49
|
+
const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
|
|
50
|
+
const userConfig = JSON.parse(content);
|
|
51
|
+
// 如果用户配置中包含敏感信息,直接使用
|
|
52
|
+
if (userConfig.appKey && userConfig.appSecret) {
|
|
53
|
+
return { ...DEFAULT_CONFIG, ...userConfig } as CLIConfig;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.warn('⚠️ 配置文件加载失败');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 2. 尝试从环境变量读取
|
|
61
|
+
const host = process.env.HIK_HOST;
|
|
62
|
+
const appKey = process.env.HIK_APP_KEY;
|
|
63
|
+
const appSecret = process.env.HIK_APP_SECRET;
|
|
64
|
+
|
|
65
|
+
if (host && appKey && appSecret) {
|
|
66
|
+
return {
|
|
67
|
+
...DEFAULT_CONFIG,
|
|
68
|
+
host,
|
|
69
|
+
appKey,
|
|
70
|
+
appSecret,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 3. 尝试从项目配置文件读取
|
|
75
|
+
const projectConfigPath = path.resolve(__dirname, '../../../../server/config.json');
|
|
76
|
+
if (fs.existsSync(projectConfigPath)) {
|
|
77
|
+
try {
|
|
78
|
+
const projectConfig = JSON.parse(fs.readFileSync(projectConfigPath, 'utf-8'));
|
|
79
|
+
if (projectConfig.hikvision?.host && projectConfig.hikvision?.appKey && projectConfig.hikvision?.appSecret) {
|
|
80
|
+
return {
|
|
81
|
+
...DEFAULT_CONFIG,
|
|
82
|
+
host: projectConfig.hikvision.host,
|
|
83
|
+
appKey: projectConfig.hikvision.appKey,
|
|
84
|
+
appSecret: projectConfig.hikvision.appSecret,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
} catch (e) {
|
|
88
|
+
console.warn('⚠️ 项目配置文件加载失败:', e);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 3. 配置缺失,返回 null 表示需要用户配置
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 获取配置
|
|
98
|
+
* @returns 配置对象,如果未配置则返回 null
|
|
99
|
+
*/
|
|
100
|
+
get(): CLIConfig | null {
|
|
101
|
+
return this.config ? { ...this.config } : null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 检查是否已配置
|
|
106
|
+
*/
|
|
107
|
+
isConfigured(): boolean {
|
|
108
|
+
return this.config !== null && !!this.config.appKey && !!this.config.appSecret;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 更新配置
|
|
113
|
+
*/
|
|
114
|
+
update(updates: Partial<CLIConfig>): void {
|
|
115
|
+
if (!this.config) {
|
|
116
|
+
this.config = { ...DEFAULT_CONFIG } as CLIConfig;
|
|
117
|
+
}
|
|
118
|
+
this.config = { ...this.config, ...updates };
|
|
119
|
+
this.saveConfig();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 保存配置
|
|
124
|
+
*/
|
|
125
|
+
private saveConfig(): void {
|
|
126
|
+
try {
|
|
127
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
128
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
129
|
+
}
|
|
130
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(this.config, null, 2));
|
|
131
|
+
} catch (error) {
|
|
132
|
+
console.error('❌ 保存配置失败:', error);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 重置配置(删除用户配置文件)
|
|
138
|
+
*/
|
|
139
|
+
reset(): void {
|
|
140
|
+
try {
|
|
141
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
142
|
+
fs.unlinkSync(CONFIG_FILE);
|
|
143
|
+
}
|
|
144
|
+
this.config = null;
|
|
145
|
+
} catch (error) {
|
|
146
|
+
console.error('❌ 重置配置失败:', error);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 获取配置路径
|
|
152
|
+
*/
|
|
153
|
+
static getConfigPath(): string {
|
|
154
|
+
return CONFIG_FILE;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* 检查配置文件是否存在
|
|
159
|
+
*/
|
|
160
|
+
static hasConfigFile(): boolean {
|
|
161
|
+
return fs.existsSync(CONFIG_FILE);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* 对配置中的敏感信息进行脱敏
|
|
166
|
+
* @param cfg 配置对象
|
|
167
|
+
* @returns 脱敏后的配置(不包含原始敏感数据)
|
|
168
|
+
*/
|
|
169
|
+
maskSensitive(cfg: CLIConfig): CLIConfig {
|
|
170
|
+
if (!cfg) return cfg;
|
|
171
|
+
return {
|
|
172
|
+
...cfg,
|
|
173
|
+
appKey: cfg.appKey ? `***${cfg.appKey.slice(-4)}` : '',
|
|
174
|
+
appSecret: cfg.appSecret ? `***${cfg.appSecret.slice(-4)}` : '',
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* 提示用户进行配置
|
|
180
|
+
*/
|
|
181
|
+
static promptConfig(): void {
|
|
182
|
+
console.log('\n🔧 海康平台配置未设置,请进行配置:\n');
|
|
183
|
+
console.log('方法一:设置环境变量');
|
|
184
|
+
console.log(' export HIK_HOST=127.0.0.1:18443');
|
|
185
|
+
console.log(' export HIK_APP_KEY=你的AppKey');
|
|
186
|
+
console.log(' export HIK_APP_SECRET=你的AppSecret\n');
|
|
187
|
+
console.log('方法二:修改配置文件');
|
|
188
|
+
console.log(' 编辑 packages/server/config.json,添加 hikvision 配置:');
|
|
189
|
+
console.log(' { "hikvision": { "host": "127.0.0.1:18443", "appKey": "***", "appSecret": "***" } }\n');
|
|
190
|
+
console.log('方法三:使用 CLI 命令配置');
|
|
191
|
+
console.log(' hikvision-cli config set host 127.0.0.1:18443');
|
|
192
|
+
console.log(' hikvision-cli config set key 你的AppKey');
|
|
193
|
+
console.log(' hikvision-cli config set secret 你的AppSecret\n');
|
|
194
|
+
console.log('配置完成后重新运行命令。\n');
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// 导出单例
|
|
199
|
+
export const config = new ConfigManager();
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI 输出格式化
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import Table from 'cli-table3';
|
|
6
|
+
import { config } from './config';
|
|
7
|
+
|
|
8
|
+
export type OutputFormat = 'table' | 'json' | 'yaml';
|
|
9
|
+
|
|
10
|
+
export interface TableOptions {
|
|
11
|
+
title?: string;
|
|
12
|
+
columns?: string[];
|
|
13
|
+
rows?: any[][];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 格式化输出
|
|
18
|
+
*/
|
|
19
|
+
export function formatOutput(data: any, format?: OutputFormat): string {
|
|
20
|
+
const cfg = config.get();
|
|
21
|
+
const outputFormat = format || (cfg?.outputFormat || 'table');
|
|
22
|
+
|
|
23
|
+
switch (outputFormat) {
|
|
24
|
+
case 'json':
|
|
25
|
+
return JSON.stringify(data, null, 2);
|
|
26
|
+
case 'yaml':
|
|
27
|
+
return formatYaml(data);
|
|
28
|
+
case 'table':
|
|
29
|
+
default:
|
|
30
|
+
return formatTable(data);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 表格输出
|
|
36
|
+
*/
|
|
37
|
+
export function formatTable(data: any): string {
|
|
38
|
+
if (!data || typeof data !== 'object') {
|
|
39
|
+
return String(data);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 处理数组数据(直接返回数组)
|
|
43
|
+
if (Array.isArray(data)) {
|
|
44
|
+
if (data.length === 0) {
|
|
45
|
+
return '📭 暂无数据';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const columns = Object.keys(data[0]);
|
|
49
|
+
const table = new Table({
|
|
50
|
+
head: columns,
|
|
51
|
+
style: { head: ['cyan'], border: ['gray'] },
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
data.forEach((item: any) => {
|
|
55
|
+
const row = columns.map((col) => item[col] ?? '');
|
|
56
|
+
table.push(row);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return table.toString();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 处理分页数据
|
|
63
|
+
if (data.list && Array.isArray(data.list)) {
|
|
64
|
+
const title = data.total
|
|
65
|
+
? `📊 共 ${data.total} 条记录 (当前 ${data.list.length} 条)`
|
|
66
|
+
: '';
|
|
67
|
+
|
|
68
|
+
if (data.list.length === 0) {
|
|
69
|
+
return '📭 暂无数据';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const columns = Object.keys(data.list[0]);
|
|
73
|
+
const table = new Table({
|
|
74
|
+
head: columns,
|
|
75
|
+
style: { head: ['cyan'], border: ['gray'] },
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
data.list.forEach((item: any) => {
|
|
79
|
+
const row = columns.map((col) => item[col] ?? '');
|
|
80
|
+
table.push(row);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const output = table.toString();
|
|
84
|
+
return title ? `${title}\n\n${output}` : output;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 处理包含 data 数组的对象(如 { data: [...] })
|
|
88
|
+
if (data.data && Array.isArray(data.data)) {
|
|
89
|
+
if (data.data.length === 0) {
|
|
90
|
+
return '📭 暂无数据';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const columns = Object.keys(data.data[0]);
|
|
94
|
+
const table = new Table({
|
|
95
|
+
head: columns,
|
|
96
|
+
style: { head: ['cyan'], border: ['gray'] },
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
data.data.forEach((item: any) => {
|
|
100
|
+
const row = columns.map((col) => item[col] ?? '');
|
|
101
|
+
table.push(row);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
return table.toString();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 处理对象数据
|
|
108
|
+
return JSON.stringify(data, null, 2);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* YAML 输出(简化版)
|
|
113
|
+
*/
|
|
114
|
+
function formatYaml(data: any): string {
|
|
115
|
+
if (data === null || data === undefined) {
|
|
116
|
+
return 'null';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (typeof data !== 'object') {
|
|
120
|
+
return String(data);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (Array.isArray(data)) {
|
|
124
|
+
return data.map((item: any, i: number) => `- ${formatYaml(item)}`).join('\n');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return Object.entries(data)
|
|
128
|
+
.map(([key, value]: [string, any]) => `${key}: ${formatYaml(value)}`)
|
|
129
|
+
.join('\n');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 成功消息
|
|
134
|
+
*/
|
|
135
|
+
export function success(message: string): void {
|
|
136
|
+
console.log(`✅ ${message}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 错误消息
|
|
141
|
+
*/
|
|
142
|
+
export function error(message: string): void {
|
|
143
|
+
console.error(`❌ ${message}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* 警告消息
|
|
148
|
+
*/
|
|
149
|
+
export function warn(message: string): void {
|
|
150
|
+
console.warn(`⚠️ ${message}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* 信息消息
|
|
155
|
+
*/
|
|
156
|
+
export function info(message: string): void {
|
|
157
|
+
console.log(`ℹ️ ${message}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* 批量操作结果输出
|
|
162
|
+
*/
|
|
163
|
+
export function formatBatchResult(result: { success: number; failed: number; details?: any[] }): string {
|
|
164
|
+
const lines = [
|
|
165
|
+
`📊 批量操作完成`,
|
|
166
|
+
` ✅ 成功: ${result.success}`,
|
|
167
|
+
` ❌ 失败: ${result.failed}`,
|
|
168
|
+
];
|
|
169
|
+
|
|
170
|
+
if (result.failed > 0 && result.details) {
|
|
171
|
+
lines.push('');
|
|
172
|
+
lines.push('失败详情:');
|
|
173
|
+
result.details.forEach((detail: any, i: number) => {
|
|
174
|
+
if (!detail.success) {
|
|
175
|
+
lines.push(` ${i + 1}. ${detail.message || '未知错误'}`);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return lines.join('\n');
|
|
181
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI 输入验证工具
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
|
|
8
|
+
export interface ValidationResult {
|
|
9
|
+
valid: boolean;
|
|
10
|
+
content: string;
|
|
11
|
+
error?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface JsonValidationOptions {
|
|
15
|
+
requiredFields?: string[];
|
|
16
|
+
maxSize?: number;
|
|
17
|
+
maxItems?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 验证 JSON 文件内容
|
|
22
|
+
* @param content 文件内容字符串
|
|
23
|
+
* @param options 验证选项
|
|
24
|
+
* @returns 验证结果
|
|
25
|
+
*/
|
|
26
|
+
export function validateJsonFile(content: string, options: JsonValidationOptions = {}): ValidationResult {
|
|
27
|
+
const { requiredFields = [], maxSize = 10 * 1024 * 1024, maxItems } = options;
|
|
28
|
+
|
|
29
|
+
// 1. 大小验证
|
|
30
|
+
if (content.length > maxSize) {
|
|
31
|
+
return {
|
|
32
|
+
valid: false,
|
|
33
|
+
content,
|
|
34
|
+
error: `文件内容过大:${(content.length / 1024).toFixed(2)}KB,最大支持 ${(maxSize / 1024).toFixed(2)}KB`,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 2. JSON 格式验证
|
|
39
|
+
let parsed: any;
|
|
40
|
+
try {
|
|
41
|
+
parsed = JSON.parse(content);
|
|
42
|
+
} catch (e: any) {
|
|
43
|
+
return {
|
|
44
|
+
valid: false,
|
|
45
|
+
content,
|
|
46
|
+
error: `无效的 JSON 格式:${e.message}`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 3. 必需字段验证
|
|
51
|
+
for (const field of requiredFields) {
|
|
52
|
+
if (!parsed.hasOwnProperty(field)) {
|
|
53
|
+
return {
|
|
54
|
+
valid: false,
|
|
55
|
+
content,
|
|
56
|
+
error: `缺少必需字段:${field}`,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 4. 数组大小验证
|
|
62
|
+
if (maxItems) {
|
|
63
|
+
for (const field of requiredFields) {
|
|
64
|
+
if (Array.isArray(parsed[field]) && parsed[field].length > maxItems) {
|
|
65
|
+
return {
|
|
66
|
+
valid: false,
|
|
67
|
+
content,
|
|
68
|
+
error: `${field} 数组项数量超出限制:${parsed[field].length} > ${maxItems}`,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
valid: true,
|
|
76
|
+
content,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 验证文件路径是否安全(防止路径遍历)
|
|
82
|
+
* @param filePath 文件路径
|
|
83
|
+
* @param allowedDir 允许的基础目录
|
|
84
|
+
* @returns 是否安全
|
|
85
|
+
*/
|
|
86
|
+
export function isPathSafe(filePath: string, allowedDir?: string): boolean {
|
|
87
|
+
const resolved = path.resolve(filePath);
|
|
88
|
+
|
|
89
|
+
// 如果提供了允许目录,检查路径是否在允许范围内
|
|
90
|
+
if (allowedDir) {
|
|
91
|
+
const allowedResolved = path.resolve(allowedDir);
|
|
92
|
+
return resolved.startsWith(allowedResolved);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 基本检查:路径不能包含 .. (上级目录)
|
|
96
|
+
return !filePath.includes('..');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 验证输入字符串是否包含危险字符
|
|
101
|
+
* @param input 输入字符串
|
|
102
|
+
* @returns 是否安全
|
|
103
|
+
*/
|
|
104
|
+
export function isInputSafe(input: string): boolean {
|
|
105
|
+
// 检查是否有潜在的命令注入
|
|
106
|
+
const dangerousPatterns = [
|
|
107
|
+
/[;&|`$]/, // 命令分隔符和管道
|
|
108
|
+
/\${.*}/, // 命令替换
|
|
109
|
+
/\$\(.*\)/, // 命令替换
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
return !dangerousPatterns.some(pattern => pattern.test(input));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 验证手机号格式(中国大陆)
|
|
117
|
+
* @param phone 手机号
|
|
118
|
+
* @returns 是否有效
|
|
119
|
+
*/
|
|
120
|
+
export function isValidPhone(phone: string): boolean {
|
|
121
|
+
return /^1[3-9]\d{9}$/.test(phone);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 验证身份证号格式(中国大陆)
|
|
126
|
+
* @param idCard 身份证号
|
|
127
|
+
* @returns 是否有效
|
|
128
|
+
*/
|
|
129
|
+
export function isValidIdCard(idCard: string): boolean {
|
|
130
|
+
// 18位身份证号
|
|
131
|
+
if (!/^\d{17}[\dXx]$/.test(idCard)) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 校验位验证
|
|
136
|
+
const weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
|
|
137
|
+
const checkCodes = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'];
|
|
138
|
+
|
|
139
|
+
let sum = 0;
|
|
140
|
+
for (let i = 0; i < 17; i++) {
|
|
141
|
+
sum += parseInt(idCard[i]) * weights[i];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const checkCode = checkCodes[sum % 11];
|
|
145
|
+
return idCard[17].toUpperCase() === checkCode;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* 验证车牌号格式(中国大陆)
|
|
150
|
+
* @param plateNo 车牌号
|
|
151
|
+
* @returns 是否有效
|
|
152
|
+
*/
|
|
153
|
+
export function isValidPlateNo(plateNo: string): boolean {
|
|
154
|
+
// 普通车牌:省+城市字母+5位数字/字母
|
|
155
|
+
// 新能源车牌:6位
|
|
156
|
+
const pattern = /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-Z][A-Z0-9]{4,5}[A-Z0-9挂学警港澳]$/;
|
|
157
|
+
const newEnergyPattern = /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][DF]$[A-Z0-9]{5}$/;
|
|
158
|
+
|
|
159
|
+
return pattern.test(plateNo) || newEnergyPattern.test(plateNo);
|
|
160
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"declarationMap": true,
|
|
14
|
+
"sourceMap": true,
|
|
15
|
+
"resolveJsonModule": true
|
|
16
|
+
},
|
|
17
|
+
"include": ["src/**/*"],
|
|
18
|
+
"exclude": ["node_modules", "dist"]
|
|
19
|
+
}
|