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.
@@ -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
+ }
@@ -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 }: InitConfig): void;
22
+ InitConfig({ fileType }: {
23
+ fileType: string[];
24
+ }): Promise<any[]>;
26
25
  private LoadGatewayConfig;
27
26
  }
28
27
  declare const HF: InitCls;
@@ -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
- const xhr = new XMLHttpRequest();
37
- xhr.open("GET", "/lineTenants.txt?t=" + Date.now(), false);
38
- xhr.send(null);
39
- if (xhr.status === 200) {
40
- try {
41
- const rawData = atob(xhr.responseText);
42
- const data = JSON.parse(rawData);
43
- const host = location.host;
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
- catch (error) {
62
- console.error("解析 lineTenants.txt 失败:", error);
63
- if (location.host.includes("iggame") ||
64
- location.host.includes("localhost")) {
65
- console.info(`🏠 iggame匹配到默认租户`);
66
- return {
67
- tenant: "iggame",
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
- if (location.host.includes("iggame") ||
74
- location.host.includes("localhost")) {
75
- console.info(`🏠 iggame匹配到默认租户`);
76
- return {
77
- tenant: "iggame",
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(atob(configText));
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);
@@ -1,2 +1,174 @@
1
- /** 通用 HTTP 请求封装 */
2
- export declare function HttpServer<T = any>(endpoint: string, params?: Record<string, any>, method?: "GET" | "POST" | "PUT" | "DELETE"): Promise<T>;
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>;
@@ -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
- /** 生成随机 UDID */
5
- function generateUdid() {
6
- const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
7
- return Array.from({ length: 32 }, () => chars.charAt(Math.floor(Math.random() * chars.length))).join("");
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
- /** 获取或生成 Finger(UDID) */
10
- async function getFinger() {
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 newId = generateUdid();
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
- /** 通用 HTTP 请求封装 */
29
- export async function HttpServer(endpoint, params = {}, method = "GET") {
30
- const udid = await getFinger();
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 data = {
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
- // Header
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": data.sign,
195
+ "X-Sign": sign,
56
196
  "Accept-Language": lang,
57
197
  tenantSys: HF.getTenant(),
58
- "Content-Type": "application/json",
59
- os: data.os,
198
+ os: finalBody.os,
199
+ ...options.headers,
60
200
  };
61
- // 附加 URL 参数(如 puid、id、token
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
- const token = Local("token") || getParams("token");
71
- if (token) {
72
- headers.Authorization = `HSBox ${token}`;
73
- data.token = token;
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
- try {
77
- const url = method === "GET"
78
- ? `${fullUrl}${Object.keys(params).length
79
- ? "?" +
80
- new URLSearchParams(Object.entries(params).map(([k, v]) => [k, String(v)])).toString()
81
- : ""}`
82
- : fullUrl;
83
- const res = await fetch(url, {
84
- method,
85
- headers,
86
- ...(method !== "GET" && { body: JSON.stringify(data) }),
87
- });
88
- const text = await res.text();
89
- const json = JSON.parse(text);
90
- if (json.code === 0 || json.code === 200) {
91
- return json.data;
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
- else {
94
- throw new Error(json.msg || "Request failed");
269
+ catch (e) {
270
+ // 拦截器抛错则中断请求
271
+ throw e;
95
272
  }
96
273
  }
97
- catch (err) {
98
- console.error("[HttpServer Error]", err);
99
- throw err;
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.2.11",
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
  }