hifun-tools 1.2.11 → 1.3.1
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/init/ende.d.ts +14 -0
- package/dist/init/ende.js +38 -0
- package/dist/init/index.d.ts +3 -4
- package/dist/init/index.js +54 -52
- package/dist/request/index.d.ts +174 -2
- package/dist/request/index.js +423 -48
- package/package.json +4 -1
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AES加密
|
|
3
|
+
* @param {string} text - 要加密的原文
|
|
4
|
+
* @param {string} [keyStr] - 密钥(长度16/24/32字节)
|
|
5
|
+
* @returns {string} 加密后的Base64字符串
|
|
6
|
+
*/
|
|
7
|
+
export declare function AesEncrypt(text: string, keyStr?: string): any;
|
|
8
|
+
/**
|
|
9
|
+
* AES解密
|
|
10
|
+
* @param {string} cipherText - 要解密的Base64密文
|
|
11
|
+
* @param {string} [keyStr] - 密钥(必须与加密时一致)
|
|
12
|
+
* @returns {string} 解密后的明文
|
|
13
|
+
*/
|
|
14
|
+
export declare function AesDecrypt(cipherText: any, keyStr?: string): any;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// 通用 AES 加解密工具
|
|
2
|
+
// 支持 TS / ESM / CJS / 浏览器
|
|
3
|
+
import CryptoJS from "crypto-js";
|
|
4
|
+
/**
|
|
5
|
+
* AES加密
|
|
6
|
+
* @param {string} text - 要加密的原文
|
|
7
|
+
* @param {string} [keyStr] - 密钥(长度16/24/32字节)
|
|
8
|
+
* @returns {string} 加密后的Base64字符串
|
|
9
|
+
*/
|
|
10
|
+
export function AesEncrypt(text, keyStr = "1234567890abcdef") {
|
|
11
|
+
const key = CryptoJS.enc.Utf8.parse(keyStr);
|
|
12
|
+
const encrypted = CryptoJS.AES.encrypt(text, key, {
|
|
13
|
+
mode: CryptoJS.mode.ECB,
|
|
14
|
+
padding: CryptoJS.pad.Pkcs7,
|
|
15
|
+
});
|
|
16
|
+
return encrypted.toString();
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* AES解密
|
|
20
|
+
* @param {string} cipherText - 要解密的Base64密文
|
|
21
|
+
* @param {string} [keyStr] - 密钥(必须与加密时一致)
|
|
22
|
+
* @returns {string} 解密后的明文
|
|
23
|
+
*/
|
|
24
|
+
export function AesDecrypt(cipherText, keyStr = "1234567890abcdef") {
|
|
25
|
+
const key = CryptoJS.enc.Utf8.parse(keyStr);
|
|
26
|
+
const decrypted = CryptoJS.AES.decrypt(cipherText, key, {
|
|
27
|
+
mode: CryptoJS.mode.ECB,
|
|
28
|
+
padding: CryptoJS.pad.Pkcs7,
|
|
29
|
+
});
|
|
30
|
+
return CryptoJS.enc.Utf8.stringify(decrypted);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 全局挂载(浏览器端)
|
|
34
|
+
*/
|
|
35
|
+
if (typeof window !== "undefined") {
|
|
36
|
+
window.Aesen = AesEncrypt;
|
|
37
|
+
window.Aesde = AesDecrypt;
|
|
38
|
+
}
|
package/dist/init/index.d.ts
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
interface InitConfig {
|
|
2
|
-
fileType: ("lineAddress" | "lineTenants")[];
|
|
3
|
-
}
|
|
4
1
|
type TenantConfig = {
|
|
5
2
|
PUBLIC_TITLE: string;
|
|
6
3
|
SITE_TITLE: string;
|
|
@@ -22,7 +19,9 @@ declare class InitCls {
|
|
|
22
19
|
private getTenantInfoStrictSync;
|
|
23
20
|
/** 严格初始化(必须在应用启动时调用) */
|
|
24
21
|
private initialize;
|
|
25
|
-
InitConfig({ fileType }:
|
|
22
|
+
InitConfig({ fileType }: {
|
|
23
|
+
fileType: string[];
|
|
24
|
+
}): Promise<any[]>;
|
|
26
25
|
private LoadGatewayConfig;
|
|
27
26
|
}
|
|
28
27
|
declare const HF: InitCls;
|
package/dist/init/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getResource } from "./getResource";
|
|
2
2
|
import { getOptimalDecodedString } from "./utils";
|
|
3
|
+
import { AesDecrypt } from "./ende";
|
|
3
4
|
class InitCls {
|
|
4
5
|
domainBaseUrl = "";
|
|
5
6
|
tenantConfig = null;
|
|
@@ -14,97 +15,99 @@ class InitCls {
|
|
|
14
15
|
/** 获取配置 */
|
|
15
16
|
getTenantConfig() {
|
|
16
17
|
if (!this.initialized || !this.tenant) {
|
|
17
|
-
throw new Error("租户尚未初始化或初始化失败");
|
|
18
|
+
throw new Error("租户尚未初始化或初始化失败getTenantConfig");
|
|
18
19
|
}
|
|
19
20
|
return this.tenantConfig;
|
|
20
21
|
}
|
|
21
22
|
/** 获取租户名 */
|
|
22
23
|
getTenant() {
|
|
23
24
|
if (!this.initialized || !this.tenant) {
|
|
24
|
-
throw new Error("租户尚未初始化或初始化失败");
|
|
25
|
+
throw new Error("租户尚未初始化或初始化失败getTenant");
|
|
25
26
|
}
|
|
26
27
|
return this.tenant;
|
|
27
28
|
}
|
|
28
29
|
getImgPath(imgName) {
|
|
29
30
|
if (!this.initialized || !this.tenant) {
|
|
30
|
-
throw new Error("租户尚未初始化或初始化失败");
|
|
31
|
+
throw new Error("租户尚未初始化或初始化失败getImgPath");
|
|
31
32
|
}
|
|
32
33
|
return getResource(`${this.tenant}/image/${imgName}`);
|
|
33
34
|
}
|
|
34
35
|
/** 严格同步获取租户信息 */
|
|
35
|
-
getTenantInfoStrictSync() {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const matchedEntry = Object.entries(data).find(([key]) => {
|
|
45
|
-
// key 是明文主机地址,直接比较即可
|
|
46
|
-
// 兼容 www 前缀的情况
|
|
47
|
-
// 如果当前 host 有 www 前缀,则也匹配无 www 前缀的配置
|
|
48
|
-
// 如果当前 host 无 www 前缀,则也匹配有 www 前缀的配置
|
|
49
|
-
return (key === host ||
|
|
50
|
-
(host.startsWith("www.") && key === host.substring(4)) ||
|
|
51
|
-
(!host.startsWith("www.") && key === "www." + host));
|
|
52
|
-
});
|
|
53
|
-
if (matchedEntry) {
|
|
54
|
-
const matched = matchedEntry[1];
|
|
55
|
-
if (data && matched) {
|
|
56
|
-
console.info(`🏠 匹配租户: ${matched}`);
|
|
57
|
-
return { tenant: matched };
|
|
58
|
-
}
|
|
59
|
-
}
|
|
36
|
+
async getTenantInfoStrictSync() {
|
|
37
|
+
try {
|
|
38
|
+
const response = await fetch(`/lineTenants.txt?t=${Date.now()}`, {
|
|
39
|
+
method: "GET",
|
|
40
|
+
cache: "no-store",
|
|
41
|
+
});
|
|
42
|
+
console.log(response);
|
|
43
|
+
if (!response.ok) {
|
|
44
|
+
throw new Error(`HTTP 状态码错误: ${response.status}`);
|
|
60
45
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
46
|
+
const rawText = await response.text();
|
|
47
|
+
const rawData = AesDecrypt(rawText);
|
|
48
|
+
const data = JSON.parse(rawData);
|
|
49
|
+
const host = location.host;
|
|
50
|
+
const matchedEntry = Object.entries(data).find(([key]) => {
|
|
51
|
+
return (key === host ||
|
|
52
|
+
(host.startsWith("www.") && key === host.substring(4)) ||
|
|
53
|
+
(!host.startsWith("www.") && key === "www." + host));
|
|
54
|
+
});
|
|
55
|
+
if (matchedEntry) {
|
|
56
|
+
const matched = matchedEntry[1];
|
|
57
|
+
if (data && matched) {
|
|
58
|
+
console.info(`🏠 匹配租户: ${matched}`);
|
|
59
|
+
return { tenant: matched };
|
|
69
60
|
}
|
|
70
|
-
throw new Error("lineTenants.txt 格式错误");
|
|
71
61
|
}
|
|
62
|
+
// 默认租户匹配
|
|
63
|
+
if (host.includes("iggame") || host.includes("localhost")) {
|
|
64
|
+
console.info(`🏠 iggame匹配到默认租户`);
|
|
65
|
+
return { tenant: "iggame" };
|
|
66
|
+
}
|
|
67
|
+
throw new Error("未找到匹配租户");
|
|
72
68
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
69
|
+
catch (error) {
|
|
70
|
+
console.error("解析 lineTenants.txt 失败:", error);
|
|
71
|
+
const host = location.host;
|
|
72
|
+
if (host.includes("iggame") || host.includes("localhost")) {
|
|
73
|
+
console.info(`🏠 iggame匹配到默认租户`);
|
|
74
|
+
return { tenant: "iggame" };
|
|
75
|
+
}
|
|
76
|
+
throw new Error("无法获取有效的租户信息");
|
|
79
77
|
}
|
|
80
|
-
throw new Error("无法获取有效的租户信息");
|
|
81
78
|
}
|
|
82
79
|
/** 严格初始化(必须在应用启动时调用) */
|
|
83
|
-
initialize() {
|
|
80
|
+
async initialize() {
|
|
84
81
|
if (this.initialized)
|
|
85
|
-
return;
|
|
82
|
+
return this.tenant;
|
|
86
83
|
try {
|
|
87
|
-
const tenantInfo = this.getTenantInfoStrictSync();
|
|
84
|
+
const tenantInfo = await this.getTenantInfoStrictSync();
|
|
88
85
|
this.tenant = tenantInfo.tenant;
|
|
89
86
|
this.tenantConfig = getResource(`${tenantInfo.tenant}/config.json`);
|
|
90
87
|
this.initialized = true;
|
|
91
88
|
console.info(`🏢 严格加载租户成功: ${this.tenant}`, this.getImgPath("logo-default.png"));
|
|
89
|
+
return this.tenant;
|
|
92
90
|
}
|
|
93
91
|
catch (error) {
|
|
94
92
|
console.error("严格租户加载失败:", error);
|
|
95
93
|
throw error;
|
|
96
94
|
}
|
|
97
95
|
}
|
|
98
|
-
InitConfig({ fileType }) {
|
|
96
|
+
async InitConfig({ fileType }) {
|
|
99
97
|
console.log("加载类型:", fileType);
|
|
98
|
+
const tasks = [];
|
|
100
99
|
if (fileType.includes("lineAddress")) {
|
|
101
100
|
console.log("初始化 lineAddress...");
|
|
102
|
-
this.LoadGatewayConfig();
|
|
101
|
+
tasks.push(this.LoadGatewayConfig());
|
|
103
102
|
}
|
|
104
103
|
if (fileType.includes("lineTenants")) {
|
|
105
104
|
console.log("初始化 lineTenants...");
|
|
106
|
-
this.initialize();
|
|
105
|
+
tasks.push(this.initialize());
|
|
107
106
|
}
|
|
107
|
+
// 等待所有异步任务完成
|
|
108
|
+
const results = await Promise.all(tasks);
|
|
109
|
+
console.log("所有初始化完成:", results);
|
|
110
|
+
return results;
|
|
108
111
|
}
|
|
109
112
|
async LoadGatewayConfig() {
|
|
110
113
|
if (this.domainBaseUrl)
|
|
@@ -112,8 +115,7 @@ class InitCls {
|
|
|
112
115
|
try {
|
|
113
116
|
const response = await fetch(`/lineAddress.txt?t=${Date.now()}`);
|
|
114
117
|
const configText = await response.text();
|
|
115
|
-
const baseUrl = JSON.parse(
|
|
116
|
-
console.log("baseUrlbaseUrl", baseUrl);
|
|
118
|
+
const baseUrl = JSON.parse(AesDecrypt(configText));
|
|
117
119
|
if (Array.isArray(baseUrl)) {
|
|
118
120
|
try {
|
|
119
121
|
this.domainBaseUrl = await getOptimalDecodedString(baseUrl);
|
package/dist/request/index.d.ts
CHANGED
|
@@ -1,2 +1,174 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* 错误类型区分
|
|
3
|
+
*/
|
|
4
|
+
export declare class HttpNetworkError extends Error {
|
|
5
|
+
code: number;
|
|
6
|
+
detail?: any;
|
|
7
|
+
constructor(message?: string, detail?: any);
|
|
8
|
+
}
|
|
9
|
+
export declare class HttpTimeoutError extends Error {
|
|
10
|
+
code: number;
|
|
11
|
+
detail?: any;
|
|
12
|
+
constructor(message?: string, detail?: any);
|
|
13
|
+
}
|
|
14
|
+
export declare class HttpBusinessError extends Error {
|
|
15
|
+
code: number;
|
|
16
|
+
data?: any;
|
|
17
|
+
constructor(code: number, message?: string, data?: any);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* 通用响应结构(按后端常见 { code, msg, data })
|
|
21
|
+
*/
|
|
22
|
+
export interface HttpResponseWrapper<T = any> {
|
|
23
|
+
code: number;
|
|
24
|
+
msg: string;
|
|
25
|
+
data: T;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 最终返回结构(包装)
|
|
29
|
+
*/
|
|
30
|
+
export interface HttpResult<T = any> {
|
|
31
|
+
code: number;
|
|
32
|
+
msg: string;
|
|
33
|
+
data: T | null;
|
|
34
|
+
raw?: any;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 请求可选参数(中文注释会显示在编辑器提示中)
|
|
38
|
+
*/
|
|
39
|
+
export interface HttpOptions {
|
|
40
|
+
/**
|
|
41
|
+
* 重试次数(网络错误 / 超时 时会触发重试)
|
|
42
|
+
* @default 3
|
|
43
|
+
*/
|
|
44
|
+
retry?: number;
|
|
45
|
+
/**
|
|
46
|
+
* 重试间隔(毫秒)
|
|
47
|
+
* @default 1000
|
|
48
|
+
*/
|
|
49
|
+
retryDelay?: number;
|
|
50
|
+
/**
|
|
51
|
+
* 单次请求超时时间(毫秒),超时会触发 AbortController
|
|
52
|
+
* @default 8000
|
|
53
|
+
*/
|
|
54
|
+
timeout?: number;
|
|
55
|
+
/**
|
|
56
|
+
* 是否以 FormData 方式发送(用于文件上传)
|
|
57
|
+
* @default false
|
|
58
|
+
*/
|
|
59
|
+
isFormData?: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* 是否返回完整 wrapper(包含 code/msg/data),true 返回 wrapper;false 默认直接返回 data(如果后端约定 data 包裹)
|
|
62
|
+
* @default false
|
|
63
|
+
*/
|
|
64
|
+
unwrap?: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* 额外请求头(会和默认 headers 合并)
|
|
67
|
+
*/
|
|
68
|
+
headers?: Record<string, string>;
|
|
69
|
+
/**
|
|
70
|
+
* 是否开启调试日志(会打印请求/响应信息)
|
|
71
|
+
* @default false
|
|
72
|
+
*/
|
|
73
|
+
debug?: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* GET 缓存(仅对 GET 生效)
|
|
76
|
+
* - 若为 true:使用默认 TTL
|
|
77
|
+
* - 若为数字:表示 TTL 毫秒
|
|
78
|
+
* - 默认不缓存
|
|
79
|
+
*/
|
|
80
|
+
cache?: boolean | number;
|
|
81
|
+
/**
|
|
82
|
+
* 期望的响应类型(json/text/blob)
|
|
83
|
+
* @default "json"
|
|
84
|
+
*/
|
|
85
|
+
responseType?: "json" | "text" | "blob";
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* 请求拦截器签名:可以同步或异步修改请求参数/headers
|
|
89
|
+
*/
|
|
90
|
+
export type RequestInterceptor = (ctx: {
|
|
91
|
+
url: string;
|
|
92
|
+
method: string;
|
|
93
|
+
params: Record<string, any>;
|
|
94
|
+
headers: Record<string, string>;
|
|
95
|
+
options: HttpOptions;
|
|
96
|
+
}) => Promise<void> | void;
|
|
97
|
+
/**
|
|
98
|
+
* 响应拦截器签名:可以处理 response wrapper 或者抛错
|
|
99
|
+
*/
|
|
100
|
+
export type ResponseInterceptor = <T = any>(ctx: {
|
|
101
|
+
url: string;
|
|
102
|
+
method: string;
|
|
103
|
+
params: Record<string, any>;
|
|
104
|
+
headers: Record<string, string>;
|
|
105
|
+
options: HttpOptions;
|
|
106
|
+
response: HttpResponseWrapper<T>;
|
|
107
|
+
}) => Promise<void> | void;
|
|
108
|
+
/**
|
|
109
|
+
* 错误拦截器签名
|
|
110
|
+
*/
|
|
111
|
+
export type ErrorInterceptor = (err: any) => Promise<void> | void;
|
|
112
|
+
/**
|
|
113
|
+
* 全局配置
|
|
114
|
+
*/
|
|
115
|
+
export interface HttpClientConfig {
|
|
116
|
+
/**
|
|
117
|
+
* 默认 retry 次数
|
|
118
|
+
*/
|
|
119
|
+
defaultRetry?: number;
|
|
120
|
+
/**
|
|
121
|
+
* 默认 retry delay(ms)
|
|
122
|
+
*/
|
|
123
|
+
defaultRetryDelay?: number;
|
|
124
|
+
/**
|
|
125
|
+
* 默认 timeout(ms)
|
|
126
|
+
*/
|
|
127
|
+
defaultTimeout?: number;
|
|
128
|
+
/**
|
|
129
|
+
* 默认缓存 TTL(ms)
|
|
130
|
+
*/
|
|
131
|
+
defaultCacheTTL?: number;
|
|
132
|
+
/**
|
|
133
|
+
* 签名函数(可替换为 HMAC/RSA)
|
|
134
|
+
* - 接收 (udid, timestamp) 返回 sign string
|
|
135
|
+
*/
|
|
136
|
+
signFn?: (udid: string, timestamp: number) => string;
|
|
137
|
+
/**
|
|
138
|
+
* paySign 函数(可选)
|
|
139
|
+
*/
|
|
140
|
+
paySignFn?: (udid: string, timestamp: number) => string;
|
|
141
|
+
/**
|
|
142
|
+
* 全局请求拦截器数组(按顺序执行)
|
|
143
|
+
*/
|
|
144
|
+
requestInterceptors?: RequestInterceptor[];
|
|
145
|
+
/**
|
|
146
|
+
* 全局响应拦截器数组(按顺序执行)
|
|
147
|
+
*/
|
|
148
|
+
responseInterceptors?: ResponseInterceptor[];
|
|
149
|
+
/**
|
|
150
|
+
* 全局错误拦截器数组(按顺序执行)
|
|
151
|
+
*/
|
|
152
|
+
errorInterceptors?: ErrorInterceptor[];
|
|
153
|
+
/**
|
|
154
|
+
* 是否默认打印请求日志
|
|
155
|
+
*/
|
|
156
|
+
debug?: boolean;
|
|
157
|
+
}
|
|
158
|
+
/** 导出用于设置全局配置的 API(在 app 启动处调用) */
|
|
159
|
+
export declare function setHttpClientConfig(cfg: Partial<HttpClientConfig>): void;
|
|
160
|
+
/** 导出注册拦截器方便使用 */
|
|
161
|
+
export declare function useRequestInterceptor(fn: RequestInterceptor): void;
|
|
162
|
+
export declare function useResponseInterceptor(fn: ResponseInterceptor): void;
|
|
163
|
+
export declare function useErrorInterceptor(fn: ErrorInterceptor): void;
|
|
164
|
+
/** 生成或获取 UDID */
|
|
165
|
+
export declare function generateUdid(): Promise<string>;
|
|
166
|
+
/**
|
|
167
|
+
* 核心请求函数
|
|
168
|
+
* @param endpoint 接口路径(相对或绝对)
|
|
169
|
+
* @param params GET 的 query 或 POST 的 body(如果需要同时指定 query/body,请将参数组织为 { query: {...}, body: {...} })
|
|
170
|
+
* @param method HTTP 方法
|
|
171
|
+
* @param options 可选项(超时、重试、缓存等)
|
|
172
|
+
* @returns Promise<HttpResult<T>> (默认 unwrap=false 会返回完整 wrapper。unwrap=true 则返回 data 内容)
|
|
173
|
+
*/
|
|
174
|
+
export declare function HttpServer<T = any>(endpoint: string, params?: Record<string, any>, method?: "GET" | "POST" | "PUT" | "DELETE", options?: HttpOptions): Promise<HttpResult<T> | T>;
|
package/dist/request/index.js
CHANGED
|
@@ -1,17 +1,115 @@
|
|
|
1
|
+
// httpClient.ts
|
|
1
2
|
import md5 from "md5";
|
|
2
3
|
import { Local, getParams, isMobile } from "../utils/index";
|
|
3
4
|
import { HF } from "../init";
|
|
4
|
-
/**
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
/**
|
|
6
|
+
* 错误类型区分
|
|
7
|
+
*/
|
|
8
|
+
export class HttpNetworkError extends Error {
|
|
9
|
+
code = -1;
|
|
10
|
+
detail;
|
|
11
|
+
constructor(message = "Network error", detail) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = "HttpNetworkError";
|
|
14
|
+
this.detail = detail;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export class HttpTimeoutError extends Error {
|
|
18
|
+
code = -2;
|
|
19
|
+
detail;
|
|
20
|
+
constructor(message = "Request timeout", detail) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = "HttpTimeoutError";
|
|
23
|
+
this.detail = detail;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export class HttpBusinessError extends Error {
|
|
27
|
+
code;
|
|
28
|
+
data;
|
|
29
|
+
constructor(code, message = "Business error", data) {
|
|
30
|
+
super(message);
|
|
31
|
+
this.name = "HttpBusinessError";
|
|
32
|
+
this.code = code;
|
|
33
|
+
this.data = data;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** 默认配置值 */
|
|
37
|
+
const DEFAULTS = {
|
|
38
|
+
defaultRetry: 3,
|
|
39
|
+
defaultRetryDelay: 1000,
|
|
40
|
+
defaultTimeout: 8000,
|
|
41
|
+
defaultCacheTTL: 60 * 1000, // 1 minute
|
|
42
|
+
};
|
|
43
|
+
/** 可变全局配置实例 */
|
|
44
|
+
const globalConfig = {
|
|
45
|
+
defaultRetry: DEFAULTS.defaultRetry,
|
|
46
|
+
defaultRetryDelay: DEFAULTS.defaultRetryDelay,
|
|
47
|
+
defaultTimeout: DEFAULTS.defaultTimeout,
|
|
48
|
+
defaultCacheTTL: DEFAULTS.defaultCacheTTL,
|
|
49
|
+
signFn: (udid, ts) => md5(udid + "jgyh,kasd" + ts),
|
|
50
|
+
paySignFn: (udid, ts) => md5(udid.substring(0, 6) + "8qiezi" + ts),
|
|
51
|
+
requestInterceptors: [],
|
|
52
|
+
responseInterceptors: [],
|
|
53
|
+
errorInterceptors: [],
|
|
54
|
+
debug: false,
|
|
55
|
+
};
|
|
56
|
+
/** 导出用于设置全局配置的 API(在 app 启动处调用) */
|
|
57
|
+
export function setHttpClientConfig(cfg) {
|
|
58
|
+
Object.assign(globalConfig, cfg);
|
|
59
|
+
}
|
|
60
|
+
/** 导出注册拦截器方便使用 */
|
|
61
|
+
export function useRequestInterceptor(fn) {
|
|
62
|
+
globalConfig.requestInterceptors.push(fn);
|
|
63
|
+
}
|
|
64
|
+
export function useResponseInterceptor(fn) {
|
|
65
|
+
globalConfig.responseInterceptors.push(fn);
|
|
66
|
+
}
|
|
67
|
+
export function useErrorInterceptor(fn) {
|
|
68
|
+
globalConfig.errorInterceptors.push(fn);
|
|
8
69
|
}
|
|
9
|
-
/**
|
|
10
|
-
|
|
70
|
+
/** 缓存(简单实现:sessionStorage,key = url+params md5;value 存储 { ts, ttl, data }) */
|
|
71
|
+
function cacheKeyFor(url, params) {
|
|
72
|
+
try {
|
|
73
|
+
return md5(url + JSON.stringify(params || {}));
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return md5(url + String(params));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function setCache(key, payload, ttl) {
|
|
80
|
+
const item = {
|
|
81
|
+
ts: Date.now(),
|
|
82
|
+
ttl,
|
|
83
|
+
payload,
|
|
84
|
+
};
|
|
85
|
+
try {
|
|
86
|
+
sessionStorage.setItem(`_http_cache_${key}`, JSON.stringify(item));
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// ignore
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function getCache(key) {
|
|
93
|
+
try {
|
|
94
|
+
const raw = sessionStorage.getItem(`_http_cache_${key}`);
|
|
95
|
+
if (!raw)
|
|
96
|
+
return null;
|
|
97
|
+
const parsed = JSON.parse(raw);
|
|
98
|
+
if (Date.now() - parsed.ts > parsed.ttl) {
|
|
99
|
+
sessionStorage.removeItem(`_http_cache_${key}`);
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
return parsed.payload;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/** 生成或获取 UDID */
|
|
109
|
+
export async function generateUdid() {
|
|
11
110
|
const udidFromUrl = getParams("udid");
|
|
12
111
|
const localFinger = Local("finger");
|
|
13
112
|
if (udidFromUrl) {
|
|
14
|
-
// 若 URL 参数 udid 与本地不一致,清除旧的密钥缓存
|
|
15
113
|
if (localFinger && localFinger !== udidFromUrl) {
|
|
16
114
|
localStorage.removeItem("SkeyData");
|
|
17
115
|
}
|
|
@@ -20,25 +118,54 @@ async function getFinger() {
|
|
|
20
118
|
return udidFromUrl;
|
|
21
119
|
}
|
|
22
120
|
if (localFinger)
|
|
23
|
-
return localFinger;
|
|
24
|
-
const
|
|
121
|
+
return Promise.resolve(localFinger);
|
|
122
|
+
const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
123
|
+
const newId = Array.from({ length: 32 }, () => chars.charAt(Math.floor(Math.random() * chars.length))).join("");
|
|
25
124
|
Local("finger", newId);
|
|
26
125
|
return newId;
|
|
27
126
|
}
|
|
28
|
-
/**
|
|
29
|
-
|
|
30
|
-
|
|
127
|
+
/**
|
|
128
|
+
* 核心请求函数
|
|
129
|
+
* @param endpoint 接口路径(相对或绝对)
|
|
130
|
+
* @param params GET 的 query 或 POST 的 body(如果需要同时指定 query/body,请将参数组织为 { query: {...}, body: {...} })
|
|
131
|
+
* @param method HTTP 方法
|
|
132
|
+
* @param options 可选项(超时、重试、缓存等)
|
|
133
|
+
* @returns Promise<HttpResult<T>> (默认 unwrap=false 会返回完整 wrapper。unwrap=true 则返回 data 内容)
|
|
134
|
+
*/
|
|
135
|
+
export async function HttpServer(endpoint, params = {}, method = "GET", options = {}) {
|
|
136
|
+
// 合并默认 options
|
|
137
|
+
const retry = options.retry ?? globalConfig.defaultRetry;
|
|
138
|
+
const retryDelay = options.retryDelay ?? globalConfig.defaultRetryDelay;
|
|
139
|
+
const timeout = options.timeout ?? globalConfig.defaultTimeout;
|
|
140
|
+
const debug = options.debug ?? globalConfig.debug ?? false;
|
|
141
|
+
const unwrap = options.unwrap ?? false;
|
|
142
|
+
const responseType = options.responseType ?? "json";
|
|
143
|
+
const isFormData = options.isFormData ?? false;
|
|
144
|
+
// 支持 params 拆分 query / body 的场景
|
|
145
|
+
let queryParams = {};
|
|
146
|
+
let bodyParams = {};
|
|
147
|
+
if (params &&
|
|
148
|
+
typeof params === "object" &&
|
|
149
|
+
("query" in params || "body" in params)) {
|
|
150
|
+
queryParams = params.query || {};
|
|
151
|
+
bodyParams = params.body || {};
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
if (method === "GET")
|
|
155
|
+
queryParams = params || {};
|
|
156
|
+
else
|
|
157
|
+
bodyParams = params || {};
|
|
158
|
+
}
|
|
159
|
+
// 生成 UDID / 签名 / baseUrl
|
|
160
|
+
const udid = await generateUdid();
|
|
31
161
|
const baseUrl = HF.getBaseUrl();
|
|
32
162
|
const fullUrl = new URL(endpoint, baseUrl).toString();
|
|
33
163
|
const timestamp = Date.now();
|
|
34
164
|
const lang = getParams("lang") || "YN";
|
|
35
|
-
//
|
|
36
|
-
const
|
|
37
|
-
...params,
|
|
165
|
+
// 组装公共字段(注意:不强制覆盖用户 bodyParams)
|
|
166
|
+
const baseFields = {
|
|
38
167
|
language: lang,
|
|
39
168
|
timestamp,
|
|
40
|
-
sign: md5(udid + "jgyh,kasd" + timestamp),
|
|
41
|
-
paySign: md5(udid.substring(0, 6) + "8qiezi" + timestamp),
|
|
42
169
|
currentUserAppVersion: "2.0.5",
|
|
43
170
|
channel: "",
|
|
44
171
|
version: "1.0.0",
|
|
@@ -47,18 +174,38 @@ export async function HttpServer(endpoint, params = {}, method = "GET") {
|
|
|
47
174
|
deviceType: "1",
|
|
48
175
|
udid,
|
|
49
176
|
};
|
|
50
|
-
|
|
177
|
+
const sign = globalConfig.signFn
|
|
178
|
+
? globalConfig.signFn(udid, timestamp)
|
|
179
|
+
: md5(udid + "jgyh,kasd" + timestamp);
|
|
180
|
+
const paySign = globalConfig.paySignFn
|
|
181
|
+
? globalConfig.paySignFn(udid, timestamp)
|
|
182
|
+
: md5(udid.substring(0, 6) + "8qiezi" + timestamp);
|
|
183
|
+
// 合并 body(不覆盖传入的 bodyParams)
|
|
184
|
+
const finalBody = {
|
|
185
|
+
...baseFields,
|
|
186
|
+
...bodyParams,
|
|
187
|
+
sign,
|
|
188
|
+
paySign,
|
|
189
|
+
};
|
|
190
|
+
// 构建 headers
|
|
51
191
|
const headers = {
|
|
52
192
|
"X-UDID": udid,
|
|
53
193
|
"X-Timestamp": String(timestamp),
|
|
54
194
|
"X-Language": lang,
|
|
55
|
-
"X-Sign":
|
|
195
|
+
"X-Sign": sign,
|
|
56
196
|
"Accept-Language": lang,
|
|
57
197
|
tenantSys: HF.getTenant(),
|
|
58
|
-
|
|
59
|
-
|
|
198
|
+
os: finalBody.os,
|
|
199
|
+
...options.headers,
|
|
60
200
|
};
|
|
61
|
-
//
|
|
201
|
+
// token
|
|
202
|
+
const token = Local("token") || getParams("token");
|
|
203
|
+
if (token) {
|
|
204
|
+
headers.Authorization = `HSBox ${token}`;
|
|
205
|
+
// 也放入 finalBody 保持兼容
|
|
206
|
+
finalBody.token = token;
|
|
207
|
+
}
|
|
208
|
+
// 额外 URL 参数(将附加到 headers 以保持与原逻辑一致)
|
|
62
209
|
const allParams = getParams();
|
|
63
210
|
Object.entries(allParams).forEach(([k, v]) => {
|
|
64
211
|
if (v)
|
|
@@ -67,35 +214,263 @@ export async function HttpServer(endpoint, params = {}, method = "GET") {
|
|
|
67
214
|
const puid = getParams("puid") || getParams("id");
|
|
68
215
|
if (puid)
|
|
69
216
|
headers.puid = puid;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
217
|
+
// 处理 URL(GET 时把 queryParams 拼接)
|
|
218
|
+
const url = method === "GET"
|
|
219
|
+
? `${fullUrl}${Object.keys(queryParams || {}).length
|
|
220
|
+
? "?" +
|
|
221
|
+
new URLSearchParams(Object.entries(queryParams).map(([k, v]) => [k, String(v)])).toString()
|
|
222
|
+
: ""}`
|
|
223
|
+
: fullUrl;
|
|
224
|
+
// 缓存 key(仅 GET)
|
|
225
|
+
const cacheOption = options.cache;
|
|
226
|
+
const cacheTTL = typeof cacheOption === "number"
|
|
227
|
+
? cacheOption
|
|
228
|
+
: cacheOption
|
|
229
|
+
? globalConfig.defaultCacheTTL
|
|
230
|
+
: 0;
|
|
231
|
+
const cacheKey = cacheTTL ? cacheKeyFor(url, queryParams) : "";
|
|
232
|
+
// 命中缓存直接返回(按 unwrap 处理)
|
|
233
|
+
if (cacheTTL) {
|
|
234
|
+
const cached = getCache(cacheKey);
|
|
235
|
+
if (cached !== null) {
|
|
236
|
+
if (debug)
|
|
237
|
+
console.info("[http][cache-hit]", url);
|
|
238
|
+
if (unwrap)
|
|
239
|
+
return cached;
|
|
240
|
+
return {
|
|
241
|
+
code: 200,
|
|
242
|
+
msg: "ok (cache)",
|
|
243
|
+
data: cached,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
74
246
|
}
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
247
|
+
// 如果不是 FormData,Content-Type: application/json
|
|
248
|
+
if (!isFormData)
|
|
249
|
+
headers["Content-Type"] = "application/json";
|
|
250
|
+
// body 构建
|
|
251
|
+
const body = method !== "GET"
|
|
252
|
+
? isFormData
|
|
253
|
+
? (() => {
|
|
254
|
+
const fm = new FormData();
|
|
255
|
+
Object.entries(finalBody).forEach(([k, v]) => {
|
|
256
|
+
if (v === undefined || v === null)
|
|
257
|
+
return;
|
|
258
|
+
fm.append(k, v);
|
|
259
|
+
});
|
|
260
|
+
return fm;
|
|
261
|
+
})()
|
|
262
|
+
: JSON.stringify(finalBody)
|
|
263
|
+
: undefined;
|
|
264
|
+
// 请求拦截器执行
|
|
265
|
+
for (const interceptor of globalConfig.requestInterceptors || []) {
|
|
266
|
+
try {
|
|
267
|
+
await interceptor({ url, method, params: finalBody, headers, options });
|
|
92
268
|
}
|
|
93
|
-
|
|
94
|
-
|
|
269
|
+
catch (e) {
|
|
270
|
+
// 拦截器抛错则中断请求
|
|
271
|
+
throw e;
|
|
95
272
|
}
|
|
96
273
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
274
|
+
// 真实 fetch 执行封装(含 retry + timeout)
|
|
275
|
+
let attempt = 0;
|
|
276
|
+
let lastErr = null;
|
|
277
|
+
while (attempt <= retry) {
|
|
278
|
+
const controller = new AbortController();
|
|
279
|
+
const signal = controller.signal;
|
|
280
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
281
|
+
try {
|
|
282
|
+
if (debug) {
|
|
283
|
+
console.info(`[http][request] ${method} ${url}`, {
|
|
284
|
+
headers,
|
|
285
|
+
bodyPreview: isFormData
|
|
286
|
+
? "FormData"
|
|
287
|
+
: body
|
|
288
|
+
? String(body).slice(0, 200)
|
|
289
|
+
: undefined,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
const res = await fetch(url, { method, headers, body, signal });
|
|
293
|
+
clearTimeout(timeoutId);
|
|
294
|
+
// 根据 responseType 解析
|
|
295
|
+
let parsed;
|
|
296
|
+
if (responseType === "blob") {
|
|
297
|
+
parsed = await res.blob();
|
|
298
|
+
// 对 blob 无法做 json wrapper 检查,直接返回
|
|
299
|
+
if (debug)
|
|
300
|
+
console.info(`[http][response][blob] ${url}`);
|
|
301
|
+
const result = {
|
|
302
|
+
code: res.status,
|
|
303
|
+
msg: res.statusText,
|
|
304
|
+
data: parsed,
|
|
305
|
+
raw: parsed,
|
|
306
|
+
};
|
|
307
|
+
// 缓存(不缓存 blob)
|
|
308
|
+
// 执行响应拦截器
|
|
309
|
+
for (const ri of globalConfig.responseInterceptors || []) {
|
|
310
|
+
try {
|
|
311
|
+
await ri({
|
|
312
|
+
url,
|
|
313
|
+
method,
|
|
314
|
+
params: finalBody,
|
|
315
|
+
headers,
|
|
316
|
+
options,
|
|
317
|
+
response: { code: res.status, msg: res.statusText, data: parsed },
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
catch (e) {
|
|
321
|
+
/* ignore interceptor error */
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (unwrap)
|
|
325
|
+
return parsed;
|
|
326
|
+
return result;
|
|
327
|
+
}
|
|
328
|
+
else if (responseType === "text") {
|
|
329
|
+
const text = await res.text();
|
|
330
|
+
parsed = text;
|
|
331
|
+
// 尝试将 text 转为 json wrapper(如果是 JSON 字符串)
|
|
332
|
+
let wrapper = null;
|
|
333
|
+
try {
|
|
334
|
+
wrapper = JSON.parse(text);
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
wrapper = null;
|
|
338
|
+
}
|
|
339
|
+
if (wrapper && wrapper.code !== undefined) {
|
|
340
|
+
// fallthrough to wrapper handling below
|
|
341
|
+
const w = wrapper;
|
|
342
|
+
// 响应拦截
|
|
343
|
+
for (const ri of globalConfig.responseInterceptors || []) {
|
|
344
|
+
try {
|
|
345
|
+
await ri({
|
|
346
|
+
url,
|
|
347
|
+
method,
|
|
348
|
+
params: finalBody,
|
|
349
|
+
headers,
|
|
350
|
+
options,
|
|
351
|
+
response: w,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
catch (e) { }
|
|
355
|
+
}
|
|
356
|
+
if (w.code === 0 || w.code === 200) {
|
|
357
|
+
// 缓存处理
|
|
358
|
+
if (cacheTTL)
|
|
359
|
+
setCache(cacheKey, w.data, cacheTTL);
|
|
360
|
+
if (unwrap)
|
|
361
|
+
return w.data;
|
|
362
|
+
return {
|
|
363
|
+
code: w.code,
|
|
364
|
+
msg: w.msg,
|
|
365
|
+
data: w.data,
|
|
366
|
+
raw: w,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
throw new HttpBusinessError(w.code, w.msg || "Business error", w.data);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
// text but not wrapper
|
|
375
|
+
if (unwrap)
|
|
376
|
+
return parsed;
|
|
377
|
+
return {
|
|
378
|
+
code: res.status,
|
|
379
|
+
msg: res.statusText,
|
|
380
|
+
data: parsed,
|
|
381
|
+
raw: parsed,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
// 默认 json
|
|
387
|
+
const text = await res.text();
|
|
388
|
+
let jsonWrapper;
|
|
389
|
+
try {
|
|
390
|
+
jsonWrapper = JSON.parse(text);
|
|
391
|
+
}
|
|
392
|
+
catch (e) {
|
|
393
|
+
throw new HttpNetworkError("Invalid JSON response", text);
|
|
394
|
+
}
|
|
395
|
+
// 执行响应拦截器(可抛错)
|
|
396
|
+
for (const ri of globalConfig.responseInterceptors || []) {
|
|
397
|
+
try {
|
|
398
|
+
await ri({
|
|
399
|
+
url,
|
|
400
|
+
method,
|
|
401
|
+
params: finalBody,
|
|
402
|
+
headers,
|
|
403
|
+
options,
|
|
404
|
+
response: jsonWrapper,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
catch (e) {
|
|
408
|
+
// 拦截器决定中断或修改
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
if (jsonWrapper.code === 0 || jsonWrapper.code === 200) {
|
|
412
|
+
// 缓存(仅 GET)
|
|
413
|
+
if (cacheTTL && method === "GET") {
|
|
414
|
+
setCache(cacheKey, jsonWrapper.data, cacheTTL);
|
|
415
|
+
}
|
|
416
|
+
if (debug)
|
|
417
|
+
console.info(`[http][response][ok] ${url}`, jsonWrapper);
|
|
418
|
+
if (unwrap)
|
|
419
|
+
return jsonWrapper.data;
|
|
420
|
+
return {
|
|
421
|
+
code: jsonWrapper.code,
|
|
422
|
+
msg: jsonWrapper.msg,
|
|
423
|
+
data: jsonWrapper.data,
|
|
424
|
+
raw: jsonWrapper,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
// 业务错误
|
|
429
|
+
throw new HttpBusinessError(jsonWrapper.code, jsonWrapper.msg || "Business error", jsonWrapper.data);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
catch (err) {
|
|
434
|
+
clearTimeout(timeoutId);
|
|
435
|
+
lastErr = err;
|
|
436
|
+
// 区分 AbortError(超时)和其他
|
|
437
|
+
if (err && err.name === "AbortError") {
|
|
438
|
+
lastErr = new HttpTimeoutError("Request timeout", err);
|
|
439
|
+
}
|
|
440
|
+
else if (err instanceof HttpBusinessError) {
|
|
441
|
+
// 业务错误,不触发重试(通常业务错误不会通过重试解决)
|
|
442
|
+
// 触发全局错误拦截器
|
|
443
|
+
for (const ei of globalConfig.errorInterceptors || []) {
|
|
444
|
+
try {
|
|
445
|
+
await ei(err);
|
|
446
|
+
}
|
|
447
|
+
catch { }
|
|
448
|
+
}
|
|
449
|
+
// 直接抛出
|
|
450
|
+
throw err;
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
// 网络错误(如断网、解析失败等)
|
|
454
|
+
lastErr = new HttpNetworkError(err?.message || "Network error", err);
|
|
455
|
+
}
|
|
456
|
+
// 执行错误拦截器
|
|
457
|
+
for (const ei of globalConfig.errorInterceptors || []) {
|
|
458
|
+
try {
|
|
459
|
+
await ei(lastErr);
|
|
460
|
+
}
|
|
461
|
+
catch { }
|
|
462
|
+
}
|
|
463
|
+
attempt++;
|
|
464
|
+
if (attempt > retry)
|
|
465
|
+
break;
|
|
466
|
+
// 等待 retryDelay
|
|
467
|
+
await new Promise((r) => setTimeout(r, retryDelay));
|
|
468
|
+
if (debug)
|
|
469
|
+
console.warn(`[http][retry] ${method} ${url} attempt ${attempt}/${retry}`);
|
|
470
|
+
}
|
|
100
471
|
}
|
|
472
|
+
// 全部失败后统一抛出最后的错误
|
|
473
|
+
if (debug)
|
|
474
|
+
console.error("[http][fail]", lastErr);
|
|
475
|
+
throw lastErr;
|
|
101
476
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hifun-tools",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"module": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -17,5 +17,8 @@
|
|
|
17
17
|
"devDependencies": {
|
|
18
18
|
"md5": "^2.3.0",
|
|
19
19
|
"typescript": "^5.6.3"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"crypto-js": "^4.2.0"
|
|
20
23
|
}
|
|
21
24
|
}
|