@usethink/cf-core 0.3.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/README.md +88 -0
- package/dist/features/anti-abuse/index.d.ts +62 -0
- package/dist/features/anti-abuse/index.d.ts.map +1 -0
- package/dist/features/anti-abuse/index.js +139 -0
- package/dist/features/anti-abuse/index.js.map +1 -0
- package/dist/features/email/index.d.ts +68 -0
- package/dist/features/email/index.d.ts.map +1 -0
- package/dist/features/email/index.js +120 -0
- package/dist/features/email/index.js.map +1 -0
- package/dist/features/payment/fetch-utils.d.ts +17 -0
- package/dist/features/payment/fetch-utils.d.ts.map +1 -0
- package/dist/features/payment/fetch-utils.js +56 -0
- package/dist/features/payment/fetch-utils.js.map +1 -0
- package/dist/features/payment/index.d.ts +20 -0
- package/dist/features/payment/index.d.ts.map +1 -0
- package/dist/features/payment/index.js +21 -0
- package/dist/features/payment/index.js.map +1 -0
- package/dist/features/payment/providers/alipay.d.ts +30 -0
- package/dist/features/payment/providers/alipay.d.ts.map +1 -0
- package/dist/features/payment/providers/alipay.js +149 -0
- package/dist/features/payment/providers/alipay.js.map +1 -0
- package/dist/features/payment/providers/stripe.d.ts +34 -0
- package/dist/features/payment/providers/stripe.d.ts.map +1 -0
- package/dist/features/payment/providers/stripe.js +168 -0
- package/dist/features/payment/providers/stripe.js.map +1 -0
- package/dist/features/payment/providers/trc20.d.ts +24 -0
- package/dist/features/payment/providers/trc20.d.ts.map +1 -0
- package/dist/features/payment/providers/trc20.js +96 -0
- package/dist/features/payment/providers/trc20.js.map +1 -0
- package/dist/features/payment/registry.d.ts +43 -0
- package/dist/features/payment/registry.d.ts.map +1 -0
- package/dist/features/payment/registry.js +65 -0
- package/dist/features/payment/registry.js.map +1 -0
- package/dist/features/payment/types.d.ts +72 -0
- package/dist/features/payment/types.d.ts.map +1 -0
- package/dist/features/payment/types.js +8 -0
- package/dist/features/payment/types.js.map +1 -0
- package/dist/features/thompson-router/index.d.ts +101 -0
- package/dist/features/thompson-router/index.d.ts.map +1 -0
- package/dist/features/thompson-router/index.js +186 -0
- package/dist/features/thompson-router/index.js.map +1 -0
- package/dist/features/webhook/index.d.ts +76 -0
- package/dist/features/webhook/index.d.ts.map +1 -0
- package/dist/features/webhook/index.js +127 -0
- package/dist/features/webhook/index.js.map +1 -0
- package/dist/src/audit.d.ts +45 -0
- package/dist/src/audit.d.ts.map +1 -0
- package/dist/src/audit.js +40 -0
- package/dist/src/audit.js.map +1 -0
- package/dist/src/auth/jwt.d.ts +33 -0
- package/dist/src/auth/jwt.d.ts.map +1 -0
- package/dist/src/auth/jwt.js +87 -0
- package/dist/src/auth/jwt.js.map +1 -0
- package/dist/src/auth/password.d.ts +26 -0
- package/dist/src/auth/password.d.ts.map +1 -0
- package/dist/src/auth/password.js +52 -0
- package/dist/src/auth/password.js.map +1 -0
- package/dist/src/bootstrap.d.ts +74 -0
- package/dist/src/bootstrap.d.ts.map +1 -0
- package/dist/src/bootstrap.js +231 -0
- package/dist/src/bootstrap.js.map +1 -0
- package/dist/src/cache.d.ts +52 -0
- package/dist/src/cache.d.ts.map +1 -0
- package/dist/src/cache.js +76 -0
- package/dist/src/cache.js.map +1 -0
- package/dist/src/config.d.ts +83 -0
- package/dist/src/config.d.ts.map +1 -0
- package/dist/src/config.js +96 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/crypto.d.ts +33 -0
- package/dist/src/crypto.d.ts.map +1 -0
- package/dist/src/crypto.js +87 -0
- package/dist/src/crypto.js.map +1 -0
- package/dist/src/db/connection.d.ts +53 -0
- package/dist/src/db/connection.d.ts.map +1 -0
- package/dist/src/db/connection.js +104 -0
- package/dist/src/db/connection.js.map +1 -0
- package/dist/src/db/index.d.ts +6 -0
- package/dist/src/db/index.d.ts.map +1 -0
- package/dist/src/db/index.js +6 -0
- package/dist/src/db/index.js.map +1 -0
- package/dist/src/db/schema.d.ts +649 -0
- package/dist/src/db/schema.d.ts.map +1 -0
- package/dist/src/db/schema.js +76 -0
- package/dist/src/db/schema.js.map +1 -0
- package/dist/src/error.d.ts +47 -0
- package/dist/src/error.d.ts.map +1 -0
- package/dist/src/error.js +94 -0
- package/dist/src/error.js.map +1 -0
- package/dist/src/http.d.ts +83 -0
- package/dist/src/http.d.ts.map +1 -0
- package/dist/src/http.js +116 -0
- package/dist/src/http.js.map +1 -0
- package/dist/src/idempotency.d.ts +78 -0
- package/dist/src/idempotency.d.ts.map +1 -0
- package/dist/src/idempotency.js +84 -0
- package/dist/src/idempotency.js.map +1 -0
- package/dist/src/index.d.ts +31 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +45 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/logger.d.ts +31 -0
- package/dist/src/logger.d.ts.map +1 -0
- package/dist/src/logger.js +45 -0
- package/dist/src/logger.js.map +1 -0
- package/dist/src/middleware/admin-auth.d.ts +38 -0
- package/dist/src/middleware/admin-auth.d.ts.map +1 -0
- package/dist/src/middleware/admin-auth.js +55 -0
- package/dist/src/middleware/admin-auth.js.map +1 -0
- package/dist/src/middleware/api-key-auth.d.ts +42 -0
- package/dist/src/middleware/api-key-auth.d.ts.map +1 -0
- package/dist/src/middleware/api-key-auth.js +104 -0
- package/dist/src/middleware/api-key-auth.js.map +1 -0
- package/dist/src/middleware/index.d.ts +3 -0
- package/dist/src/middleware/index.d.ts.map +1 -0
- package/dist/src/middleware/index.js +3 -0
- package/dist/src/middleware/index.js.map +1 -0
- package/dist/src/rate-limit.d.ts +54 -0
- package/dist/src/rate-limit.d.ts.map +1 -0
- package/dist/src/rate-limit.js +134 -0
- package/dist/src/rate-limit.js.map +1 -0
- package/dist/src/security.d.ts +78 -0
- package/dist/src/security.d.ts.map +1 -0
- package/dist/src/security.js +175 -0
- package/dist/src/security.js.map +1 -0
- package/dist/src/types.d.ts +64 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +8 -0
- package/dist/src/types.js.map +1 -0
- package/features/anti-abuse/index.ts +180 -0
- package/features/anti-abuse/tests/index.test.ts +50 -0
- package/features/email/index.ts +172 -0
- package/features/email/tests/index.test.ts +44 -0
- package/features/payment/fetch-utils.ts +65 -0
- package/features/payment/index.ts +39 -0
- package/features/payment/providers/alipay.ts +171 -0
- package/features/payment/providers/stripe.ts +192 -0
- package/features/payment/providers/trc20.ts +115 -0
- package/features/payment/registry.ts +87 -0
- package/features/payment/tests/index.test.ts +506 -0
- package/features/payment/types.ts +93 -0
- package/features/telegram-miniapp/index.ts +109 -0
- package/features/telegram-miniapp/tests/index.test.ts +11 -0
- package/features/thompson-router/index.ts +243 -0
- package/features/thompson-router/tests/index.test.ts +93 -0
- package/features/webhook/index.ts +183 -0
- package/features/webhook/tests/index.test.ts +21 -0
- package/package.json +202 -0
- package/src/audit.ts +70 -0
- package/src/auth/jwt.ts +114 -0
- package/src/auth/password.ts +75 -0
- package/src/bootstrap.ts +322 -0
- package/src/cache.ts +78 -0
- package/src/config.ts +134 -0
- package/src/crypto.ts +106 -0
- package/src/db/connection.ts +127 -0
- package/src/db/index.ts +6 -0
- package/src/db/schema.ts +90 -0
- package/src/error.ts +125 -0
- package/src/http.ts +150 -0
- package/src/idempotency.ts +127 -0
- package/src/index.ts +85 -0
- package/src/logger.ts +63 -0
- package/src/middleware/admin-auth.ts +71 -0
- package/src/middleware/api-key-auth.ts +164 -0
- package/src/middleware/index.ts +2 -0
- package/src/rate-limit.ts +167 -0
- package/src/security.ts +219 -0
- package/src/types.ts +70 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 支付功能模块 — Provider 注册表
|
|
3
|
+
*
|
|
4
|
+
* per-request 工厂模式:根据 env 中配置的凭证,自动实例化已配置的渠道。
|
|
5
|
+
* 支持从 DB 加载加密配置(通过 dbConfigs 参数),数据库中的配置优先于环境变量。
|
|
6
|
+
* 优先级由调用方传入 factory 数组的顺序决定。
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { createProviderRegistry, stripeFactory, alipayFactory } from "@usethink/cf-core/features/payment";
|
|
11
|
+
*
|
|
12
|
+
* // 纯环境变量模式(向后兼容)
|
|
13
|
+
* const registry = createProviderRegistry(env, [stripeFactory, alipayFactory]);
|
|
14
|
+
*
|
|
15
|
+
* // 混合模式:DB 配置优先于 env var
|
|
16
|
+
* const registry = createProviderRegistry(env, factories, dbConfigs);
|
|
17
|
+
* const provider = registry.selectOnline();
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* 创建 per-request 的 Provider 注册表。
|
|
22
|
+
*
|
|
23
|
+
* 配置加载优先级(高到低):
|
|
24
|
+
* 1. dbConfigs[factory.name].config — 数据库中的支付配置(解密后)
|
|
25
|
+
* 2. env — 环境变量(兜底)
|
|
26
|
+
*
|
|
27
|
+
* 当 dbConfigs 存在且 enabled=true 时,DB 配置覆盖 env 中同名的键。
|
|
28
|
+
*/
|
|
29
|
+
export function createProviderRegistry(env, factories, dbConfigs) {
|
|
30
|
+
const registry = new Map();
|
|
31
|
+
for (const factory of factories) {
|
|
32
|
+
// 检查是否有 DB 配置
|
|
33
|
+
const dbEntry = dbConfigs?.[factory.name];
|
|
34
|
+
if (dbEntry?.enabled && dbEntry.config) {
|
|
35
|
+
// DB 配置优先:将 DB 配置合入 env(DB 值覆盖 env 同名键)
|
|
36
|
+
const mergedEnv = { ...env, ...dbEntry.config };
|
|
37
|
+
if (factory.fromDbConfig) {
|
|
38
|
+
// 工厂有专用方法则直接调用
|
|
39
|
+
registry.set(factory.name, factory.fromDbConfig(dbEntry.config));
|
|
40
|
+
}
|
|
41
|
+
else if (factory.isAvailable(mergedEnv)) {
|
|
42
|
+
// 否则合入 env 后走标准 create 路径
|
|
43
|
+
registry.set(factory.name, factory.create(mergedEnv));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
else if (factory.isAvailable(env)) {
|
|
47
|
+
// 无 DB 配置,走环境变量路径
|
|
48
|
+
registry.set(factory.name, factory.create(env));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const sorted = [...factories].sort((a, b) => a.priority - b.priority);
|
|
52
|
+
return {
|
|
53
|
+
get(name) { return registry.get(name); },
|
|
54
|
+
selectOnline() {
|
|
55
|
+
for (const f of sorted) {
|
|
56
|
+
const p = registry.get(f.name);
|
|
57
|
+
if (p)
|
|
58
|
+
return p;
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
},
|
|
62
|
+
list() { return [...registry.keys()]; },
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../../../features/payment/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAkBH;;;;;;;;GAQG;AACH,MAAM,UAAU,sBAAsB,CACpC,GAA4B,EAC5B,SAA4B,EAC5B,SAA+B;IAE/B,MAAM,QAAQ,GAAG,IAAI,GAAG,EAA2B,CAAC;IAEpD,KAAK,MAAM,OAAO,IAAI,SAAS,EAAE,CAAC;QAChC,cAAc;QACd,MAAM,OAAO,GAAG,SAAS,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAE1C,IAAI,OAAO,EAAE,OAAO,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;YACvC,wCAAwC;YACxC,MAAM,SAAS,GAAG,EAAE,GAAG,GAAG,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;YAEhD,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;gBACzB,eAAe;gBACf,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,YAAY,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;YACnE,CAAC;iBAAM,IAAI,OAAO,CAAC,WAAW,CAAC,SAAS,CAAC,EAAE,CAAC;gBAC1C,0BAA0B;gBAC1B,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC;YACxD,CAAC;QACH,CAAC;aAAM,IAAI,OAAO,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC;YACpC,kBAAkB;YAClB,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;QAClD,CAAC;IACH,CAAC;IAED,MAAM,MAAM,GAAG,CAAC,GAAG,SAAS,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC;IAEtE,OAAO;QACL,GAAG,CAAC,IAAI,IAAI,OAAO,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACxC,YAAY;YACV,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;gBACvB,MAAM,CAAC,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;gBAC/B,IAAI,CAAC;oBAAE,OAAO,CAAC,CAAC;YAClB,CAAC;YACD,OAAO,IAAI,CAAC;QACd,CAAC;QACD,IAAI,KAAK,OAAO,CAAC,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;KACxC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 支付功能模块 — 类型定义
|
|
3
|
+
*
|
|
4
|
+
* 纯接口文件,零运行时依赖。
|
|
5
|
+
* 所有支付 Provider 实现都必须实现 PaymentProvider 接口。
|
|
6
|
+
*/
|
|
7
|
+
export interface CreatePaymentInput {
|
|
8
|
+
orderNo: string;
|
|
9
|
+
amountCents: number;
|
|
10
|
+
currency: string;
|
|
11
|
+
notifyUrl: string;
|
|
12
|
+
returnUrl?: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
metadata?: Record<string, string>;
|
|
15
|
+
}
|
|
16
|
+
export interface CreatePaymentResult {
|
|
17
|
+
providerTradeNo?: string;
|
|
18
|
+
qrCode?: string;
|
|
19
|
+
redirectUrl?: string;
|
|
20
|
+
raw?: Record<string, unknown>;
|
|
21
|
+
}
|
|
22
|
+
export interface CallbackResult {
|
|
23
|
+
orderNo: string;
|
|
24
|
+
providerTradeNo: string;
|
|
25
|
+
amountCents: number;
|
|
26
|
+
currency: string;
|
|
27
|
+
paidAt: string;
|
|
28
|
+
raw?: Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
export interface QueryStatusResult {
|
|
31
|
+
paid: boolean;
|
|
32
|
+
providerTradeNo?: string;
|
|
33
|
+
}
|
|
34
|
+
export interface RefundInput {
|
|
35
|
+
providerTradeNo: string;
|
|
36
|
+
refundCents: number;
|
|
37
|
+
reason?: string;
|
|
38
|
+
refundNo?: string;
|
|
39
|
+
}
|
|
40
|
+
export interface RefundResult {
|
|
41
|
+
success: boolean;
|
|
42
|
+
providerRefundNo?: string;
|
|
43
|
+
status: string;
|
|
44
|
+
}
|
|
45
|
+
export interface PaymentProvider {
|
|
46
|
+
readonly name: string;
|
|
47
|
+
readonly displayName: string;
|
|
48
|
+
readonly supportedCurrencies: string[];
|
|
49
|
+
createPayment(input: CreatePaymentInput): Promise<CreatePaymentResult>;
|
|
50
|
+
verifyCallback(params: Record<string, string>): Promise<CallbackResult>;
|
|
51
|
+
queryStatus?(tradeNo: string): Promise<QueryStatusResult>;
|
|
52
|
+
refund?(input: RefundInput): Promise<RefundResult>;
|
|
53
|
+
}
|
|
54
|
+
export interface ProviderRegistry {
|
|
55
|
+
get(name: string): PaymentProvider | undefined;
|
|
56
|
+
selectOnline(): PaymentProvider | null;
|
|
57
|
+
list(): string[];
|
|
58
|
+
}
|
|
59
|
+
export interface ProviderFactory {
|
|
60
|
+
name: string;
|
|
61
|
+
priority: number;
|
|
62
|
+
isAvailable(env: Record<string, unknown>): boolean;
|
|
63
|
+
create(env: Record<string, unknown>): PaymentProvider;
|
|
64
|
+
/**
|
|
65
|
+
* 从数据库解密后的配置创建 Provider。
|
|
66
|
+
* 可选方法——未实现时,注册表会将 config 合入 env 再调用 create()。
|
|
67
|
+
*
|
|
68
|
+
* @param config - 解密后的扁平配置对象(键名与 env var 一致,如 { ZPAY_PID: "xxx" })
|
|
69
|
+
*/
|
|
70
|
+
fromDbConfig?(config: Record<string, unknown>): PaymentProvider;
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../features/payment/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACnC;AAED,MAAM,WAAW,mBAAmB;IAClC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,OAAO,CAAC;IACd,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,WAAW;IAC1B,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAC;CAChB;AAMD,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,mBAAmB,EAAE,MAAM,EAAE,CAAC;IACvC,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC;IACvE,cAAc,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;IACxE,WAAW,CAAC,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAC1D,MAAM,CAAC,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;CACpD;AAMD,MAAM,WAAW,gBAAgB;IAC/B,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAAC;IAC/C,YAAY,IAAI,eAAe,GAAG,IAAI,CAAC;IACvC,IAAI,IAAI,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC;IACnD,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,eAAe,CAAC;IAEtD;;;;;OAKG;IACH,YAAY,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,eAAe,CAAC;CACjE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../features/payment/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* thompson-router — Thompson Sampling 智能路由插件
|
|
3
|
+
*
|
|
4
|
+
* 基于贝叶斯多臂老虎机算法的渠道选择器:
|
|
5
|
+
* - 自动探索-利用平衡(新渠道多尝试,好渠道多使用)
|
|
6
|
+
* - Beta 分布建模成功率(α=成功次数, β=失败次数)
|
|
7
|
+
* - 自动故障转移(选中渠道失败时切换到下一候选)
|
|
8
|
+
* - 可选多维度评分(成功率+价格+延迟+容量+趋势)
|
|
9
|
+
*
|
|
10
|
+
* 来源:vcode src/services/channel-router.ts + channel-scorer.ts
|
|
11
|
+
*/
|
|
12
|
+
export interface ChannelCandidate {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
/** Beta 分布参数 α(成功次数 + 先验) */
|
|
16
|
+
alpha: number;
|
|
17
|
+
/** Beta 分布参数 β(失败次数 + 先验) */
|
|
18
|
+
beta: number;
|
|
19
|
+
/** 是否启用 */
|
|
20
|
+
enabled: boolean;
|
|
21
|
+
/** 优先级权重(越小越优先,用于同分时排序) */
|
|
22
|
+
priority?: number;
|
|
23
|
+
/** 扩展元数据 */
|
|
24
|
+
metadata?: Record<string, unknown>;
|
|
25
|
+
}
|
|
26
|
+
export interface RouteDecision {
|
|
27
|
+
/** 选中的渠道 ID */
|
|
28
|
+
channelId: string;
|
|
29
|
+
/** 渠道名称 */
|
|
30
|
+
channelName: string;
|
|
31
|
+
/** 采样得分 */
|
|
32
|
+
score: number;
|
|
33
|
+
/** 候选数量 */
|
|
34
|
+
candidateCount: number;
|
|
35
|
+
/** 决策时间 */
|
|
36
|
+
timestamp: string;
|
|
37
|
+
}
|
|
38
|
+
export interface ThompsonRouterConfig {
|
|
39
|
+
/** Beta 分布先验参数(默认 α=1, β=1,即均匀分布) */
|
|
40
|
+
priorAlpha?: number;
|
|
41
|
+
priorBeta?: number;
|
|
42
|
+
/** 最小样本量(低于此值的渠道会被优先探索) */
|
|
43
|
+
minSamples?: number;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Thompson Sampling 渠道路由器
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```ts
|
|
50
|
+
* import { ThompsonRouter } from "@usethink/cf-core/plugins/thompson-router";
|
|
51
|
+
*
|
|
52
|
+
* const router = new ThompsonRouter();
|
|
53
|
+
*
|
|
54
|
+
* // 从数据库加载候选渠道
|
|
55
|
+
* const candidates: ChannelCandidate[] = [
|
|
56
|
+
* { id: "5sim", name: "5sim.net", alpha: 15, beta: 3, enabled: true },
|
|
57
|
+
* { id: "grizzly", name: "GrizzlySMS", alpha: 10, beta: 5, enabled: true },
|
|
58
|
+
* ];
|
|
59
|
+
*
|
|
60
|
+
* // 选择最佳渠道
|
|
61
|
+
* const decision = router.select(candidates);
|
|
62
|
+
* console.log(decision.channelName); // 大概率选中 5sim(成功率更高)
|
|
63
|
+
*
|
|
64
|
+
* // 反馈结果(更新 Beta 参数)
|
|
65
|
+
* router.recordSuccess(candidates, "5sim"); // α += 1
|
|
66
|
+
* router.recordFailure(candidates, "5sim"); // β += 1
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
export declare class ThompsonRouter {
|
|
70
|
+
readonly name = "thompson-router";
|
|
71
|
+
readonly version = "0.1.0";
|
|
72
|
+
private config;
|
|
73
|
+
constructor(config?: ThompsonRouterConfig);
|
|
74
|
+
/**
|
|
75
|
+
* Thompson Sampling 选择最佳渠道
|
|
76
|
+
*/
|
|
77
|
+
select(candidates: ChannelCandidate[]): RouteDecision;
|
|
78
|
+
/**
|
|
79
|
+
* 选择渠道并支持自动故障转移
|
|
80
|
+
*/
|
|
81
|
+
selectWithFailover(candidates: ChannelCandidate[], maxAttempts?: number): RouteDecision[];
|
|
82
|
+
/**
|
|
83
|
+
* 记录成功(更新 α 参数)
|
|
84
|
+
*/
|
|
85
|
+
recordSuccess(candidates: ChannelCandidate[], channelId: string): void;
|
|
86
|
+
/**
|
|
87
|
+
* 记录失败(更新 β 参数)
|
|
88
|
+
*/
|
|
89
|
+
recordFailure(candidates: ChannelCandidate[], channelId: string): void;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Wilson Score Interval — 小样本成功率估计
|
|
93
|
+
*
|
|
94
|
+
* 比简单 success/total 更可靠,被 Reddit/Google 等用于排序。
|
|
95
|
+
*
|
|
96
|
+
* @param successes 成功次数
|
|
97
|
+
* @param total 总次数
|
|
98
|
+
* @param z 置信水平(默认 1.96 = 95%)
|
|
99
|
+
*/
|
|
100
|
+
export declare function wilsonScore(successes: number, total: number, z?: number): number;
|
|
101
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../features/thompson-router/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAMH,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,6BAA6B;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,6BAA6B;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW;IACX,OAAO,EAAE,OAAO,CAAC;IACjB,2BAA2B;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,aAAa;IAC5B,eAAe;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW;IACX,KAAK,EAAE,MAAM,CAAC;IACd,WAAW;IACX,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW;IACX,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,oBAAoB;IACnC,qCAAqC;IACrC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2BAA2B;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AA8CD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,qBAAa,cAAc;IACzB,QAAQ,CAAC,IAAI,qBAAqB;IAClC,QAAQ,CAAC,OAAO,WAAW;IAE3B,OAAO,CAAC,MAAM,CAAiC;gBAEnC,MAAM,GAAE,oBAAyB;IAQ7C;;OAEG;IACH,MAAM,CAAC,UAAU,EAAE,gBAAgB,EAAE,GAAG,aAAa;IA8CrD;;OAEG;IACH,kBAAkB,CAChB,UAAU,EAAE,gBAAgB,EAAE,EAC9B,WAAW,SAAI,GACd,aAAa,EAAE;IAgBlB;;OAEG;IACH,aAAa,CAAC,UAAU,EAAE,gBAAgB,EAAE,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;IAKtE;;OAEG;IACH,aAAa,CAAC,UAAU,EAAE,gBAAgB,EAAE,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;CAIvE;AAMD;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,SAAO,GAAG,MAAM,CAO9E"}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* thompson-router — Thompson Sampling 智能路由插件
|
|
3
|
+
*
|
|
4
|
+
* 基于贝叶斯多臂老虎机算法的渠道选择器:
|
|
5
|
+
* - 自动探索-利用平衡(新渠道多尝试,好渠道多使用)
|
|
6
|
+
* - Beta 分布建模成功率(α=成功次数, β=失败次数)
|
|
7
|
+
* - 自动故障转移(选中渠道失败时切换到下一候选)
|
|
8
|
+
* - 可选多维度评分(成功率+价格+延迟+容量+趋势)
|
|
9
|
+
*
|
|
10
|
+
* 来源:vcode src/services/channel-router.ts + channel-scorer.ts
|
|
11
|
+
*/
|
|
12
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
13
|
+
// 统计采样工具
|
|
14
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
15
|
+
/** Marsaglia-Tsang 方法生成 Gamma 分布随机数 */
|
|
16
|
+
function sampleGamma(shape) {
|
|
17
|
+
if (shape < 1) {
|
|
18
|
+
return sampleGamma(shape + 1) * Math.pow(Math.random(), 1 / shape);
|
|
19
|
+
}
|
|
20
|
+
const d = shape - 1 / 3;
|
|
21
|
+
const c = 1 / Math.sqrt(9 * d);
|
|
22
|
+
while (true) {
|
|
23
|
+
let x;
|
|
24
|
+
let v;
|
|
25
|
+
do {
|
|
26
|
+
x = gaussianRandom();
|
|
27
|
+
v = 1 + c * x;
|
|
28
|
+
} while (v <= 0);
|
|
29
|
+
v = v * v * v;
|
|
30
|
+
const u = Math.random();
|
|
31
|
+
if (u < 1 - 0.0331 * x * x * x * x)
|
|
32
|
+
return d * v;
|
|
33
|
+
if (Math.log(u) < 0.5 * x * x + d * (1 - v + Math.log(v)))
|
|
34
|
+
return d * v;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Box-Muller 变换生成标准正态分布 */
|
|
38
|
+
function gaussianRandom() {
|
|
39
|
+
let u = 0, v = 0;
|
|
40
|
+
while (u === 0)
|
|
41
|
+
u = Math.random();
|
|
42
|
+
while (v === 0)
|
|
43
|
+
v = Math.random();
|
|
44
|
+
return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
|
|
45
|
+
}
|
|
46
|
+
/** 从 Beta(α, β) 分布采样 */
|
|
47
|
+
function sampleBeta(alpha, beta) {
|
|
48
|
+
const x = sampleGamma(alpha);
|
|
49
|
+
const y = sampleGamma(beta);
|
|
50
|
+
return x / (x + y);
|
|
51
|
+
}
|
|
52
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
53
|
+
// ThompsonRouter
|
|
54
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
55
|
+
/**
|
|
56
|
+
* Thompson Sampling 渠道路由器
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```ts
|
|
60
|
+
* import { ThompsonRouter } from "@usethink/cf-core/plugins/thompson-router";
|
|
61
|
+
*
|
|
62
|
+
* const router = new ThompsonRouter();
|
|
63
|
+
*
|
|
64
|
+
* // 从数据库加载候选渠道
|
|
65
|
+
* const candidates: ChannelCandidate[] = [
|
|
66
|
+
* { id: "5sim", name: "5sim.net", alpha: 15, beta: 3, enabled: true },
|
|
67
|
+
* { id: "grizzly", name: "GrizzlySMS", alpha: 10, beta: 5, enabled: true },
|
|
68
|
+
* ];
|
|
69
|
+
*
|
|
70
|
+
* // 选择最佳渠道
|
|
71
|
+
* const decision = router.select(candidates);
|
|
72
|
+
* console.log(decision.channelName); // 大概率选中 5sim(成功率更高)
|
|
73
|
+
*
|
|
74
|
+
* // 反馈结果(更新 Beta 参数)
|
|
75
|
+
* router.recordSuccess(candidates, "5sim"); // α += 1
|
|
76
|
+
* router.recordFailure(candidates, "5sim"); // β += 1
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
export class ThompsonRouter {
|
|
80
|
+
name = "thompson-router";
|
|
81
|
+
version = "0.1.0";
|
|
82
|
+
config;
|
|
83
|
+
constructor(config = {}) {
|
|
84
|
+
this.config = {
|
|
85
|
+
priorAlpha: config.priorAlpha ?? 1,
|
|
86
|
+
priorBeta: config.priorBeta ?? 1,
|
|
87
|
+
minSamples: config.minSamples ?? 5,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Thompson Sampling 选择最佳渠道
|
|
92
|
+
*/
|
|
93
|
+
select(candidates) {
|
|
94
|
+
const eligible = candidates.filter((c) => c.enabled);
|
|
95
|
+
if (eligible.length === 0) {
|
|
96
|
+
throw new Error("No eligible channel candidates");
|
|
97
|
+
}
|
|
98
|
+
if (eligible.length === 1) {
|
|
99
|
+
return {
|
|
100
|
+
channelId: eligible[0].id,
|
|
101
|
+
channelName: eligible[0].name,
|
|
102
|
+
score: 1,
|
|
103
|
+
candidateCount: 1,
|
|
104
|
+
timestamp: new Date().toISOString(),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
// 优先探索样本不足的渠道
|
|
108
|
+
const underExplored = eligible.filter((c) => (c.alpha + c.beta) < this.config.minSamples + this.config.priorAlpha + this.config.priorBeta);
|
|
109
|
+
const pool = underExplored.length > 0 ? underExplored : eligible;
|
|
110
|
+
// 对每个候选渠道从 Beta(α, β) 采样
|
|
111
|
+
let bestCandidate = pool[0];
|
|
112
|
+
let bestScore = -1;
|
|
113
|
+
for (const candidate of pool) {
|
|
114
|
+
const alpha = candidate.alpha + this.config.priorAlpha;
|
|
115
|
+
const beta = candidate.beta + this.config.priorBeta;
|
|
116
|
+
const score = sampleBeta(alpha, beta);
|
|
117
|
+
if (score > bestScore || (score === bestScore && (candidate.priority ?? 100) < (bestCandidate.priority ?? 100))) {
|
|
118
|
+
bestScore = score;
|
|
119
|
+
bestCandidate = candidate;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
channelId: bestCandidate.id,
|
|
124
|
+
channelName: bestCandidate.name,
|
|
125
|
+
score: bestScore,
|
|
126
|
+
candidateCount: eligible.length,
|
|
127
|
+
timestamp: new Date().toISOString(),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* 选择渠道并支持自动故障转移
|
|
132
|
+
*/
|
|
133
|
+
selectWithFailover(candidates, maxAttempts = 3) {
|
|
134
|
+
const decisions = [];
|
|
135
|
+
const remaining = [...candidates];
|
|
136
|
+
for (let i = 0; i < Math.min(maxAttempts, remaining.length); i++) {
|
|
137
|
+
const decision = this.select(remaining);
|
|
138
|
+
decisions.push(decision);
|
|
139
|
+
// 移除已选渠道,下次选择时跳过
|
|
140
|
+
const idx = remaining.findIndex((c) => c.id === decision.channelId);
|
|
141
|
+
if (idx >= 0)
|
|
142
|
+
remaining.splice(idx, 1);
|
|
143
|
+
if (remaining.length === 0)
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
return decisions;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* 记录成功(更新 α 参数)
|
|
150
|
+
*/
|
|
151
|
+
recordSuccess(candidates, channelId) {
|
|
152
|
+
const channel = candidates.find((c) => c.id === channelId);
|
|
153
|
+
if (channel)
|
|
154
|
+
channel.alpha += 1;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* 记录失败(更新 β 参数)
|
|
158
|
+
*/
|
|
159
|
+
recordFailure(candidates, channelId) {
|
|
160
|
+
const channel = candidates.find((c) => c.id === channelId);
|
|
161
|
+
if (channel)
|
|
162
|
+
channel.beta += 1;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
166
|
+
// Wilson Score 成功率估计(小样本更可靠)
|
|
167
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
168
|
+
/**
|
|
169
|
+
* Wilson Score Interval — 小样本成功率估计
|
|
170
|
+
*
|
|
171
|
+
* 比简单 success/total 更可靠,被 Reddit/Google 等用于排序。
|
|
172
|
+
*
|
|
173
|
+
* @param successes 成功次数
|
|
174
|
+
* @param total 总次数
|
|
175
|
+
* @param z 置信水平(默认 1.96 = 95%)
|
|
176
|
+
*/
|
|
177
|
+
export function wilsonScore(successes, total, z = 1.96) {
|
|
178
|
+
if (total === 0)
|
|
179
|
+
return 0;
|
|
180
|
+
const p = successes / total;
|
|
181
|
+
const denominator = 1 + z * z / total;
|
|
182
|
+
const centre = p + z * z / (2 * total);
|
|
183
|
+
const adjustment = z * Math.sqrt((p * (1 - p) + z * z / (4 * total)) / total);
|
|
184
|
+
return (centre - adjustment) / denominator;
|
|
185
|
+
}
|
|
186
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../features/thompson-router/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* webhook — Webhook 通知插件
|
|
3
|
+
*
|
|
4
|
+
* 支持:
|
|
5
|
+
* - 多 URL 通知(逗号分隔)
|
|
6
|
+
* - HMAC-SHA256 签名
|
|
7
|
+
* - 5s 超时 + 最多 2 次重试
|
|
8
|
+
* - 事件类型过滤
|
|
9
|
+
*
|
|
10
|
+
* 来源:xtools src/services/webhook.ts
|
|
11
|
+
*/
|
|
12
|
+
export interface WebhookConfig {
|
|
13
|
+
/** 通知 URL(支持多个,逗号分隔) */
|
|
14
|
+
urls: string;
|
|
15
|
+
/** HMAC-SHA256 签名密钥(可选) */
|
|
16
|
+
secret?: string;
|
|
17
|
+
/** 请求超时(毫秒,默认 5000) */
|
|
18
|
+
timeoutMs?: number;
|
|
19
|
+
/** 最大重试次数(默认 2) */
|
|
20
|
+
maxRetries?: number;
|
|
21
|
+
}
|
|
22
|
+
export interface WebhookPayload {
|
|
23
|
+
/** 事件类型 */
|
|
24
|
+
event: string;
|
|
25
|
+
/** 事件时间 ISO 8601 */
|
|
26
|
+
timestamp: string;
|
|
27
|
+
/** 事件数据 */
|
|
28
|
+
data: Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
export interface WebhookResult {
|
|
31
|
+
ok: boolean;
|
|
32
|
+
url: string;
|
|
33
|
+
status?: number;
|
|
34
|
+
error?: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Webhook 通知服务
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* import { WebhookService } from "@usethink/cf-core/plugins/webhook";
|
|
42
|
+
*
|
|
43
|
+
* const webhook = new WebhookService({
|
|
44
|
+
* urls: "https://hooks.example.com/a,https://hooks.example.com/b",
|
|
45
|
+
* secret: "my-secret",
|
|
46
|
+
* });
|
|
47
|
+
*
|
|
48
|
+
* await webhook.notify("order.paid", { orderId: "123", amount: 9900 });
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export declare class WebhookService {
|
|
52
|
+
readonly name = "webhook";
|
|
53
|
+
readonly version = "0.1.0";
|
|
54
|
+
private urls;
|
|
55
|
+
private secret?;
|
|
56
|
+
private timeoutMs;
|
|
57
|
+
private maxRetries;
|
|
58
|
+
constructor(config: WebhookConfig);
|
|
59
|
+
/**
|
|
60
|
+
* 发送通知到所有 URL
|
|
61
|
+
*/
|
|
62
|
+
notify(event: string, data: Record<string, unknown>): Promise<WebhookResult[]>;
|
|
63
|
+
/**
|
|
64
|
+
* 发送通知到单个 URL
|
|
65
|
+
*/
|
|
66
|
+
private sendToUrl;
|
|
67
|
+
/**
|
|
68
|
+
* HMAC-SHA256 签名
|
|
69
|
+
*/
|
|
70
|
+
private sign;
|
|
71
|
+
/**
|
|
72
|
+
* 是否有可用的 URL
|
|
73
|
+
*/
|
|
74
|
+
get isConfigured(): boolean;
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../features/webhook/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAMH,MAAM,WAAW,aAAa;IAC5B,wBAAwB;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,2BAA2B;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uBAAuB;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mBAAmB;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,cAAc;IAC7B,WAAW;IACX,KAAK,EAAE,MAAM,CAAC;IACd,oBAAoB;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW;IACX,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,OAAO,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAMD;;;;;;;;;;;;;;GAcG;AACH,qBAAa,cAAc;IACzB,QAAQ,CAAC,IAAI,aAAa;IAC1B,QAAQ,CAAC,OAAO,WAAW;IAE3B,OAAO,CAAC,IAAI,CAAW;IACvB,OAAO,CAAC,MAAM,CAAC,CAAS;IACxB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,UAAU,CAAS;gBAEf,MAAM,EAAE,aAAa;IAUjC;;OAEG;IACG,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC;IAmBpF;;OAEG;YACW,SAAS;IAuDvB;;OAEG;YACW,IAAI;IAYlB;;OAEG;IACH,IAAI,YAAY,IAAI,OAAO,CAE1B;CACF"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* webhook — Webhook 通知插件
|
|
3
|
+
*
|
|
4
|
+
* 支持:
|
|
5
|
+
* - 多 URL 通知(逗号分隔)
|
|
6
|
+
* - HMAC-SHA256 签名
|
|
7
|
+
* - 5s 超时 + 最多 2 次重试
|
|
8
|
+
* - 事件类型过滤
|
|
9
|
+
*
|
|
10
|
+
* 来源:xtools src/services/webhook.ts
|
|
11
|
+
*/
|
|
12
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
13
|
+
// WebhookService
|
|
14
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
15
|
+
/**
|
|
16
|
+
* Webhook 通知服务
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* import { WebhookService } from "@usethink/cf-core/plugins/webhook";
|
|
21
|
+
*
|
|
22
|
+
* const webhook = new WebhookService({
|
|
23
|
+
* urls: "https://hooks.example.com/a,https://hooks.example.com/b",
|
|
24
|
+
* secret: "my-secret",
|
|
25
|
+
* });
|
|
26
|
+
*
|
|
27
|
+
* await webhook.notify("order.paid", { orderId: "123", amount: 9900 });
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export class WebhookService {
|
|
31
|
+
name = "webhook";
|
|
32
|
+
version = "0.1.0";
|
|
33
|
+
urls;
|
|
34
|
+
secret;
|
|
35
|
+
timeoutMs;
|
|
36
|
+
maxRetries;
|
|
37
|
+
constructor(config) {
|
|
38
|
+
this.urls = config.urls
|
|
39
|
+
.split(",")
|
|
40
|
+
.map((u) => u.trim())
|
|
41
|
+
.filter((u) => u.startsWith("http"));
|
|
42
|
+
this.secret = config.secret;
|
|
43
|
+
this.timeoutMs = config.timeoutMs ?? 5000;
|
|
44
|
+
this.maxRetries = config.maxRetries ?? 2;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* 发送通知到所有 URL
|
|
48
|
+
*/
|
|
49
|
+
async notify(event, data) {
|
|
50
|
+
if (this.urls.length === 0)
|
|
51
|
+
return [];
|
|
52
|
+
const payload = {
|
|
53
|
+
event,
|
|
54
|
+
timestamp: new Date().toISOString(),
|
|
55
|
+
data,
|
|
56
|
+
};
|
|
57
|
+
const results = [];
|
|
58
|
+
for (const url of this.urls) {
|
|
59
|
+
const result = await this.sendToUrl(url, payload);
|
|
60
|
+
results.push(result);
|
|
61
|
+
}
|
|
62
|
+
return results;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* 发送通知到单个 URL
|
|
66
|
+
*/
|
|
67
|
+
async sendToUrl(url, payload) {
|
|
68
|
+
const body = JSON.stringify(payload);
|
|
69
|
+
const headers = {
|
|
70
|
+
"Content-Type": "application/json",
|
|
71
|
+
"X-Webhook-Event": payload.event,
|
|
72
|
+
"X-Webhook-Timestamp": payload.timestamp,
|
|
73
|
+
};
|
|
74
|
+
if (this.secret) {
|
|
75
|
+
const signature = await this.sign(body);
|
|
76
|
+
headers["X-Webhook-Signature"] = signature;
|
|
77
|
+
}
|
|
78
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
79
|
+
try {
|
|
80
|
+
const controller = new AbortController();
|
|
81
|
+
const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
82
|
+
const res = await fetch(url, {
|
|
83
|
+
method: "POST",
|
|
84
|
+
headers,
|
|
85
|
+
body,
|
|
86
|
+
signal: controller.signal,
|
|
87
|
+
});
|
|
88
|
+
clearTimeout(timeout);
|
|
89
|
+
if (res.ok) {
|
|
90
|
+
return { ok: true, url, status: res.status };
|
|
91
|
+
}
|
|
92
|
+
if (res.status >= 400 && res.status < 500) {
|
|
93
|
+
return { ok: false, url, status: res.status, error: `HTTP ${res.status}` };
|
|
94
|
+
}
|
|
95
|
+
if (attempt < this.maxRetries) {
|
|
96
|
+
await new Promise((r) => setTimeout(r, 1000 * (attempt + 1)));
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
return { ok: false, url, status: res.status, error: `HTTP ${res.status} after ${this.maxRetries + 1} attempts` };
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
103
|
+
if (attempt < this.maxRetries) {
|
|
104
|
+
await new Promise((r) => setTimeout(r, 1000 * (attempt + 1)));
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
return { ok: false, url, error: errMsg };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return { ok: false, url, error: "unexpected" };
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* HMAC-SHA256 签名
|
|
114
|
+
*/
|
|
115
|
+
async sign(body) {
|
|
116
|
+
const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(this.secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
117
|
+
const signature = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(body));
|
|
118
|
+
return [...new Uint8Array(signature)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* 是否有可用的 URL
|
|
122
|
+
*/
|
|
123
|
+
get isConfigured() {
|
|
124
|
+
return this.urls.length > 0;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=index.js.map
|