twikoo-func 2.0.8 → 2.0.10
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 +29 -3
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +165 -23
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +141 -24
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ Twikoo 2.0 服务端适配器。业务逻辑在 `@twikoojs/common`,本包仅
|
|
|
8
8
|
|
|
9
9
|
| | 1.x | 2.0 |
|
|
10
10
|
| ------------------------------------------- | ----------------------------------------- | ------------------------------------------ |
|
|
11
|
-
| 定位 | 公共逻辑 + CloudBase 入口(代码全在这里) | **仅 CloudBase 薄适配器**(< 150
|
|
11
|
+
| 定位 | 公共逻辑 + CloudBase 入口(代码全在这里) | **仅 CloudBase 薄适配器**(< 150 行 + 重依赖预热) |
|
|
12
12
|
| 公共逻辑位置 | `twikoo-func` 内部(`utils/`、`fn/` 等) | **`@twikoojs/common`** |
|
|
13
13
|
| 旧导出(`require('twikoo-func').utils` 等) | 可用 | **不再导出**(1.x 的整包转发已移除,见下) |
|
|
14
14
|
|
|
@@ -24,20 +24,46 @@ Twikoo 2.0 服务端适配器。业务逻辑在 `@twikoojs/common`,本包仅
|
|
|
24
24
|
|
|
25
25
|
## 部署(CloudBase 控制台)
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
> **前提:需要付费套餐(个人版及以上)**。免费体验版**无法**使用「HTTP 网关 - 跨域设置 - 添加跨域域名」
|
|
28
|
+
> (控制台提示「您当前版本暂不支持此功能」),网站域名加不进跨域白名单、评论框调不通云函数;
|
|
29
|
+
> 「自定义域名」为付费套餐权益(个人版 1 个),升级到个人版即可。详见 `docs/backend.md`
|
|
30
|
+
> 的 CloudBase 章节。
|
|
31
|
+
|
|
32
|
+
1. CloudBase 控制台 → 云函数 → 创建函数(模板 Node.js Hello World,运行时保持模板默认的 **Node 20.19**)
|
|
28
33
|
2. 在线编辑器粘贴:`exports.main = require("twikoo-func").main`
|
|
29
34
|
3. 在线装依赖:仅声明 `twikoo-func`(其余依赖随 npm 安装自动拉取)
|
|
30
35
|
4. 创建后状态「正常」即部署完成
|
|
31
36
|
|
|
32
|
-
|
|
37
|
+
> **运行环境别乱改**(实测 2026-09-24):`Node.js 24.11(公测中)` **无法完成在线安装依赖**,
|
|
38
|
+
> 函数会创建失败(`[ResourceNotFound.Package] BuildCodeViaSCF Failed resp:null: Dependency error`,
|
|
39
|
+
> 且代码为空);`Node.js 18.15` 不满足 `html-to-text` 等依赖的引擎要求(需 ≥ 20.19)。
|
|
40
|
+
|
|
41
|
+
## 平台核对清单(查阅 2026-09-17,真机复核 2026-09-24 / 09-28)
|
|
33
42
|
|
|
34
43
|
- [x] 函数入口 `exports.main(event, context)` 结构与返回体格式(docs.cloudbase.net 云函数章节)
|
|
35
44
|
- [x] 控制台「在线编辑器 + 在线装依赖」流程;CLI(tcb fn deploy)2.0 起不再支持
|
|
36
45
|
- [x] 运行时创建后不可改版本
|
|
46
|
+
- [x] **运行环境**:20.19 可在线装依赖;24.11(公测中)不可(见上方引用块)
|
|
47
|
+
- [x] **套餐前提**:免费体验版**无法**添加跨域域名(HTTP 网关 - 跨域设置)⇒ **需付费套餐**;
|
|
48
|
+
「自定义域名」为付费套餐权益(个人版 1 / 标准版 5 / 企业版 50),**个人版即可**
|
|
49
|
+
(据[官方资源点价格文档](https://cloud.tencent.com/document/product/876/127357),2026-09-15)
|
|
50
|
+
- [x] **套餐限制**:**免费体验版**的云函数**执行超时固定 3 秒、内存固定 256MB,均不可修改**
|
|
51
|
+
(控制台上该表单项禁用并带「个人版」标记 —— 该标记指「需个人版及以上」);
|
|
52
|
+
**个人版及以上超时最大 900 秒、内存可调整**
|
|
53
|
+
- [x] **冷启动开销**:`@cloudbase/node-sdk` 的加载与 jsdom 的**首次加载**都在 3 秒量级,
|
|
54
|
+
且原实现把它们放在**调用路径**里 ⇒ 冷实例发评论必然超时;现已由
|
|
55
|
+
`src/prewarm.ts` 同步预热到实例初始化阶段(实测冷调用 3000ms 超时 → 1078ms 成功,
|
|
56
|
+
`InitFunction` 594ms → 3574ms)
|
|
37
57
|
- [ ] 环境变量注入方式实测(人工项)
|
|
38
58
|
- [ ] MONGODB_URI 外接 Mongo 实测(人工项)
|
|
59
|
+
- [ ] 匿名登入 / 函数权限控制(legacy 安全规则)/ HTTP 网关路由(人工项)
|
|
60
|
+
- [ ] 跨域域名放通效果实测(**需付费套餐**,人工项)
|
|
61
|
+
- [ ] 邮件、IP 属地、图床、反垃圾、AI 各能力实测(人工项)
|
|
39
62
|
|
|
40
63
|
## 注意
|
|
41
64
|
|
|
42
65
|
- `exports.main` 导出名不可改(CloudBase 以此挂载)
|
|
43
66
|
- 环境变量:`MONGODB_URI`(可选外接 Mongo)、`TWIKOO_*` 业务配置存于数据库配置
|
|
67
|
+
- **`src/prewarm.ts` 是适配器唯一静态 `import` 重依赖的地方**:为把 jsdom / DOMPurify 的
|
|
68
|
+
首次加载挪出调用路径(理由与验证数据见该文件头注释)。增删它的依赖时必须同步
|
|
69
|
+
`package.json` 的 `dependencies`,改坏会表现为「冷调用重新付 3 秒」
|
package/dist/index.d.mts
CHANGED
|
@@ -75,7 +75,7 @@ export declare function fromTkResponse(response: TkResponse): Record<string, unk
|
|
|
75
75
|
//#endregion
|
|
76
76
|
//#region src/main.d.ts
|
|
77
77
|
/**
|
|
78
|
-
* 创建 CloudBase 请求处理器(database / app
|
|
78
|
+
* 创建 CloudBase 请求处理器(database / app 可注入供测试;缺省用模块级单例)。
|
|
79
79
|
* @param options 注入项
|
|
80
80
|
* @returns 逐请求处理器(第二参数为云函数上下文,供 IP 解析与递归自调用取函数名)
|
|
81
81
|
*/
|
package/dist/index.d.ts
CHANGED
|
@@ -75,7 +75,7 @@ export declare function fromTkResponse(response: TkResponse): Record<string, unk
|
|
|
75
75
|
//#endregion
|
|
76
76
|
//#region src/main.d.ts
|
|
77
77
|
/**
|
|
78
|
-
* 创建 CloudBase 请求处理器(database / app
|
|
78
|
+
* 创建 CloudBase 请求处理器(database / app 可注入供测试;缺省用模块级单例)。
|
|
79
79
|
* @param options 注入项
|
|
80
80
|
* @returns 逐请求处理器(第二参数为云函数上下文,供 IP 解析与递归自调用取函数名)
|
|
81
81
|
*/
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,112 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region \0rolldown/runtime.js
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
+
key = keys[i];
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
13
|
+
get: ((k) => from[k]).bind(null, key),
|
|
14
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
|
|
20
|
+
value: mod,
|
|
21
|
+
enumerable: true
|
|
22
|
+
}) : target, mod));
|
|
23
|
+
//#endregion
|
|
24
|
+
let _cloudbase_node_sdk = require("@cloudbase/node-sdk");
|
|
25
|
+
_cloudbase_node_sdk = __toESM(_cloudbase_node_sdk);
|
|
2
26
|
let _twikoojs_common = require("@twikoojs/common");
|
|
27
|
+
let dompurify = require("dompurify");
|
|
28
|
+
dompurify = __toESM(dompurify);
|
|
29
|
+
let jsdom = require("jsdom");
|
|
30
|
+
jsdom = __toESM(jsdom);
|
|
31
|
+
//#region src/prewarm.ts
|
|
32
|
+
/**
|
|
33
|
+
* 重依赖预热:把 jsdom / DOMPurify 的加载从「调用路径」挪到「实例初始化阶段」。
|
|
34
|
+
*
|
|
35
|
+
* ## 为什么必须预热
|
|
36
|
+
*
|
|
37
|
+
* `@twikoojs/common` 的重依赖一律经 `lib-loader` 的 `loadLib()` **动态** import,
|
|
38
|
+
* 于是 jsdom(4.2MB)的**首次**加载落在调用里。真机实测(2026-09-28,腾讯云 CloudBase
|
|
39
|
+
* 免费体验版):一个只做 `await import("jsdom")` + `new JSDOM("")` 的探针就会
|
|
40
|
+
* **打满 3 秒执行超时**;而同一实例紧接着的第二次调用只要 **13ms**(模块已在进程内)
|
|
41
|
+
* —— 即贵的是「首次加载」,与用不用 `import()` 无关。
|
|
42
|
+
*
|
|
43
|
+
* 而免费体验版与个人版的云函数**执行超时固定 3 秒、不可修改**(见 `docs/backend.md`),
|
|
44
|
+
* 这 3 秒直接决定「发评论」能否成功:`COMMENT_SUBMIT` 会经 `getDomPurify()` 加载 jsdom。
|
|
45
|
+
*
|
|
46
|
+
* ## 解法
|
|
47
|
+
*
|
|
48
|
+
* 在模块初始化阶段**同步**把 DOMPurify 装好,用 `setCustomLibs` 注入
|
|
49
|
+
* (1.x `lib.js` 的逃生舱范式,EdgeOne Makers 适配器同款做法)。
|
|
50
|
+
* `getDomPurify()` 里 `customLibs.DOMPurify` 优先级最高、直接返回,
|
|
51
|
+
* `loadLib` 根本不会被调用 —— 实测覆写生效后该调用耗时 **0ms**。
|
|
52
|
+
*
|
|
53
|
+
* ## 实测效果(真实 `COMMENT_SUBMIT`,同一环境同一函数)
|
|
54
|
+
*
|
|
55
|
+
* | 指标 | 预热前 | 预热后 |
|
|
56
|
+
* | --- | --- | --- |
|
|
57
|
+
* | `InitFunction` | 594ms | 3574ms |
|
|
58
|
+
* | 冷实例调用 | **3000ms 超时** | **1078ms 成功** |
|
|
59
|
+
* | 热实例调用 | — | 454 / 714 / 1933ms |
|
|
60
|
+
*
|
|
61
|
+
* 即这 3 秒开销**从「执行时长」挪进了「初始化时长」**,而平台接受 3.6 秒的初始化。
|
|
62
|
+
*
|
|
63
|
+
* ## 为什么不能写成「模块顶层的浮空 Promise」
|
|
64
|
+
*
|
|
65
|
+
* CJS 没有顶层 await,浮空预热与首次调用是**竞态**:handler 等不到它跑完就开始了,
|
|
66
|
+
* 首次请求照样超时(已实测)。所以这里的 import 必须是**同步**的。
|
|
67
|
+
*
|
|
68
|
+
* ## 破例说明与代价
|
|
69
|
+
*
|
|
70
|
+
* 本文件是全仓第二处**静态** import 重依赖的地方(另一处是
|
|
71
|
+
* `packages/pkg/src/bundled-libs.ts`,目的是把重依赖内联进 SEA 单文件产物)。
|
|
72
|
+
* AGENTS.md「重依赖全部 external + 动态加载」的禁令只约束
|
|
73
|
+
* `packages/server-common/src/**`(由 `test/utils/lib-loader-literals.test.ts` 断言),
|
|
74
|
+
* 适配器侧不受该断言约束;但为守住「薄适配器」,这里**只预热发评论路径上必需的
|
|
75
|
+
* jsdom + DOMPurify**,其余重依赖维持动态加载。
|
|
76
|
+
*
|
|
77
|
+
* 代价:冷启动 `InitFunction` 由 ~0.6s 涨到 ~3.6s(每次新建实例都付),常驻内存 +~36MB。
|
|
78
|
+
*/
|
|
79
|
+
/**
|
|
80
|
+
* 取 CJS/ESM 兼容的模块本体(ESM 命名空间的 `default` 优先)。
|
|
81
|
+
*
|
|
82
|
+
* `import * as X from "<CJS 包>"` 在 ESM 下拿到的是命名空间
|
|
83
|
+
* (`{ default: module.exports, ...具名导出 }`),本体不能直接调用;
|
|
84
|
+
* 而 CJS 产物里 rolldown 会合成 `default`。两种形态都要能取到本体 ——
|
|
85
|
+
* 与 `@twikoojs/common` 的 `pickDefault` 同一语义(那边是包内私有函数,故此处复刻)。
|
|
86
|
+
* @param namespace 模块命名空间
|
|
87
|
+
* @returns 模块本体
|
|
88
|
+
*/
|
|
89
|
+
function pickDefault(namespace) {
|
|
90
|
+
return namespace?.default ?? namespace;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* 预热好的 DOMPurify 实例(构造失败时为 `null`,交回 `getDomPurify` 的动态加载路径)。
|
|
94
|
+
*
|
|
95
|
+
* 用 try/catch 兜住构造:jsdom / dompurify 若因环境原因构造失败,应当退回
|
|
96
|
+
* 「按需动态加载」而不是让整个云函数起不来;但**必须留痕** ——
|
|
97
|
+
* 静默回退会让冷调用重新背上 3 秒级的 jsdom 首次加载(同 #1127 的教训)。
|
|
98
|
+
*/
|
|
99
|
+
const prewarmedDOMPurify = (() => {
|
|
100
|
+
try {
|
|
101
|
+
const { JSDOM } = pickDefault(jsdom);
|
|
102
|
+
return pickDefault(dompurify)(new JSDOM("").window);
|
|
103
|
+
} catch (e) {
|
|
104
|
+
console.warn("[twikoo] 重依赖预热失败,已回退按需加载(冷调用会重新付 jsdom 首次加载):", e instanceof Error ? e.message : String(e));
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
})();
|
|
108
|
+
if (prewarmedDOMPurify) (0, _twikoojs_common.setCustomLibs)({ DOMPurify: prewarmedDOMPurify });
|
|
109
|
+
//#endregion
|
|
3
110
|
//#region src/transform.ts
|
|
4
111
|
/**
|
|
5
112
|
* 取平台注入的上下文环境变量。
|
|
@@ -125,10 +232,11 @@ async dispatch(comment, ctx) {
|
|
|
125
232
|
//#region src/main.ts
|
|
126
233
|
/**
|
|
127
234
|
* twikoo-func 主逻辑(CloudBase 薄适配器)。
|
|
128
|
-
* 业务逻辑全部在 @twikoojs/common;平台核对:docs.cloudbase.net 云函数章节(查阅 2026-09-17
|
|
235
|
+
* 业务逻辑全部在 @twikoojs/common;平台核对:docs.cloudbase.net 云函数章节(查阅 2026-09-17、
|
|
236
|
+
* 真机复核 2026-09-24:运行环境 Node.js 20.19 可在线装依赖,24.11(公测中)不可)。
|
|
129
237
|
*
|
|
130
238
|
* 载荷转换见 `./transform.ts`,后置副作用派发见 `./dispatch.ts`,
|
|
131
|
-
*
|
|
239
|
+
* 重依赖预热见 `./prewarm.ts`(平台契约层),平台 SDK 结构面见 `./types.ts`。
|
|
132
240
|
*/
|
|
133
241
|
/** CloudBase 平台能力:全能力*/
|
|
134
242
|
const cloudbaseCapabilities = (0, _twikoojs_common.defineCapabilities)({
|
|
@@ -142,42 +250,76 @@ const cloudbaseCapabilities = (0, _twikoojs_common.defineCapabilities)({
|
|
|
142
250
|
ai: true
|
|
143
251
|
});
|
|
144
252
|
/**
|
|
145
|
-
*
|
|
253
|
+
* `@cloudbase/node-sdk` 的静态形态。
|
|
254
|
+
*
|
|
255
|
+
* **为什么是模块顶层静态 import,而不是调用内 `await import()`**:SDK 的模块加载在冷实例上
|
|
256
|
+
* 约 2.4s(本地对照:`require` 203ms / `init` 18ms,即开销几乎全在加载),而**调用内**的耗时
|
|
257
|
+
* 会算进函数执行时长、直接挤占「执行超时」预算。实测(2026-09-24,免费体验版
|
|
258
|
+
* `GET_FUNC_VERSION`):冷调用 **2477ms**、同实例后续调用 **66~85ms**;把加载与 `init()`
|
|
259
|
+
* 一并提到模块初始化阶段后,冷调用 **222ms**、`InitFunction` 112ms → 606ms —— 即这 2.4s
|
|
260
|
+
* 从「执行时长」挪进了「初始化时长」。CloudBase 免费体验版/个人版的执行超时**固定 3 秒且
|
|
261
|
+
* 不可修改**(见 `docs/backend.md`),这 2.4s 是决定性的。
|
|
262
|
+
*
|
|
263
|
+
* `@cloudbase/node-sdk` 不在 AGENTS.md「重依赖清单」内,且在 `dependencies` 中,
|
|
264
|
+
* 故静态导入合规(构建侧 `deps.neverBundle` 保证它仍是 external,不进产物)。
|
|
265
|
+
*
|
|
266
|
+
* v3 起 ESM 命名导出不可用(命名空间仅 default/module.exports/version),
|
|
267
|
+
* 统一回退取 default,兼容 v2 具名与 v3 default 两种形态。
|
|
268
|
+
*/
|
|
269
|
+
const tcbSdk = (() => {
|
|
270
|
+
const mod = _cloudbase_node_sdk;
|
|
271
|
+
return mod.default ?? mod;
|
|
272
|
+
})();
|
|
273
|
+
/**
|
|
274
|
+
* 模块级 app 单例(在**实例初始化阶段**构造)。
|
|
275
|
+
*
|
|
276
|
+
* 与 SDK 的官方写法一致(`const app = tcb.init({ env: tcb.SYMBOL_CURRENT_ENV })` 置于模块顶层),
|
|
277
|
+
* 好处同样是让 `init()` 的耗时落在初始化阶段而非执行阶段。
|
|
278
|
+
*
|
|
279
|
+
* 构造失败(例如在云函数环境之外被加载)时回落为 `null`,由首次调用兜底构造 ——
|
|
280
|
+
* 这样本地测试与注入路径(`options.app`)都不受影响。
|
|
281
|
+
*/
|
|
282
|
+
const sharedApp = (() => {
|
|
283
|
+
try {
|
|
284
|
+
return tcbSdk.init({ env: tcbSdk.SYMBOL_CURRENT_ENV });
|
|
285
|
+
} catch {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
})();
|
|
289
|
+
/**
|
|
290
|
+
* 创建 CloudBase 请求处理器(database / app 可注入供测试;缺省用模块级单例)。
|
|
146
291
|
* @param options 注入项
|
|
147
292
|
* @returns 逐请求处理器(第二参数为云函数上下文,供 IP 解析与递归自调用取函数名)
|
|
148
293
|
*/
|
|
149
294
|
function createTwikooFunc(options = {}) {
|
|
150
|
-
let app = options.app ??
|
|
295
|
+
let app = options.app ?? sharedApp;
|
|
151
296
|
let database = options.database ?? null;
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
"@cloudbase/node-sdk"
|
|
160
|
-
);
|
|
161
|
-
const tcb = mod.default ?? mod;
|
|
162
|
-
return tcb.init({ env: tcb.SYMBOL_CURRENT_ENV });
|
|
163
|
-
})();
|
|
164
|
-
app = await sdkPromise;
|
|
297
|
+
/**
|
|
298
|
+
* 取 app:注入优先 → 模块级单例 → 兜底构造。
|
|
299
|
+
* 只有模块级构造失败(`sharedApp === null`)才会走到最后一步。
|
|
300
|
+
* @returns CloudBase app 实例
|
|
301
|
+
*/
|
|
302
|
+
const getApp = () => {
|
|
303
|
+
app ??= tcbSdk.init({ env: tcbSdk.SYMBOL_CURRENT_ENV });
|
|
165
304
|
return app;
|
|
166
305
|
};
|
|
167
|
-
/**
|
|
168
|
-
|
|
169
|
-
|
|
306
|
+
/**
|
|
307
|
+
* 数据库实例(注入优先,否则取 app 的 `database()`)。
|
|
308
|
+
* app 已在初始化阶段就绪,故这里只是一次廉价的句柄构造。
|
|
309
|
+
* @returns 数据库句柄
|
|
310
|
+
*/
|
|
311
|
+
const getDatabase = () => {
|
|
312
|
+
database ??= getApp().database();
|
|
170
313
|
return database;
|
|
171
314
|
};
|
|
172
315
|
return async (event, context) => {
|
|
173
316
|
const request = toTkRequest(event, context);
|
|
174
|
-
const db = await getDatabase();
|
|
175
317
|
return fromTkResponse(await (0, _twikoojs_common.createHandler)({
|
|
176
318
|
request: {
|
|
177
319
|
/** 事件已在上游转换为统一请求 */
|
|
178
320
|
toTkRequest: () => request },
|
|
179
321
|
response: { fromTkResponse },
|
|
180
|
-
database: new _twikoojs_common.CloudBaseDatabase({ database:
|
|
322
|
+
database: new _twikoojs_common.CloudBaseDatabase({ database: getDatabase() }),
|
|
181
323
|
storage: {
|
|
182
324
|
challenges: {},
|
|
183
325
|
tokens: {}
|
|
@@ -188,7 +330,7 @@ send: async () => {} },
|
|
|
188
330
|
notifier: {
|
|
189
331
|
/** CloudBase 侧不直发推送(由 common 内部按需处理) */
|
|
190
332
|
notify: async () => {} },
|
|
191
|
-
postSubmit: createCloudBaseDispatcher(
|
|
333
|
+
postSubmit: createCloudBaseDispatcher(getApp(), context),
|
|
192
334
|
capabilities: cloudbaseCapabilities
|
|
193
335
|
})(request));
|
|
194
336
|
};
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["RECURSION_HEADER","getRecursionToken","defineCapabilities","createHandler","CloudBaseDatabase"],"sources":["../src/transform.ts","../src/dispatch.ts","../src/main.ts"],"sourcesContent":["/**\n * CloudBase 载荷转换(事件 ↔ 内部统一请求/响应)。\n *\n * 从 `main.ts` 抽出:入口只负责 SDK 懒加载与端口装配,平台载荷的形状知识\n * (含 IP 解析优先级)集中在本文件。\n */\nimport type { TkRequest, TkResponse } from \"@twikoojs/common\";\nimport type { TcbContextLike } from \"./types\";\n\n/**\n * 取平台注入的上下文环境变量。\n *\n * 与 `@cloudbase/node-sdk` 的 `getCloudbaseContext()` 同源:该函数从\n * `context.environment` / `context.environ` 与 `process.env` 中按\n * `TCB_CONTEXT_KEYS` 列表取值。\n * @param context 云函数调用上下文\n * @returns 上下文环境变量(无则为空对象)\n */\nfunction contextEnvOf(context?: TcbContextLike): Record<string, string> {\n return context?.environment ?? context?.environ ?? {};\n}\n\n/**\n * CloudBase 事件 → 内部统一请求(1.x 语义:事件本身即请求体)。\n *\n * **IP 解析优先级**(1.x `auth.getClientIP()` 直接读平台注入的 `TCB_SOURCE_IP`,\n * 即 CloudBase 云函数的权威来源):\n * 1. `TCB_SOURCE_IP`——上下文环境变量,`process.env` 兜底;\n * 2. `x-real-ip` 请求头(HTTP 访问服务场景);\n * 3. `x-forwarded-for` 首跳;\n * 4. `event.requestContext.http.sourceIp`。\n *\n * ⚠️ 本链必须用 `||` 而非 `??`:链上各中间项都是字符串,**空串不是 nullish**。\n * 曾写成 `a ?? (cond ? v : \"\") ?? b`——中间项在条件不成立时求值为 `\"\"`,\n * 把后面的来源全部短路掉,使 `sourceIp` 成为永远取不到的死代码。\n *\n * @param event 云函数事件\n * @param context 云函数调用上下文(取 TCB_SOURCE_IP)\n * @returns 内部统一请求\n */\nexport function toTkRequest(event: unknown, context?: TcbContextLike): TkRequest {\n const raw = (event && typeof event === \"object\" ? event : {}) as Record<string, unknown>;\n const headers = (raw.headers ?? {}) as Record<string, string>;\n const lowerHeaders: Record<string, string> = {};\n for (const [key, value] of Object.entries(headers)) {\n lowerHeaders[key.toLowerCase()] = String(value);\n }\n const tcbSourceIp = contextEnvOf(context).TCB_SOURCE_IP ?? process.env.TCB_SOURCE_IP ?? \"\";\n const forwarded = lowerHeaders[\"x-forwarded-for\"];\n const forwardedIp = forwarded ? forwarded.split(\",\")[0].trim() : \"\";\n const sourceIp = (raw.requestContext as { http?: { sourceIp?: string } } | undefined)?.http\n ?.sourceIp;\n const ip = tcbSourceIp || lowerHeaders[\"x-real-ip\"] || forwardedIp || sourceIp || \"\";\n // 事件本身即请求体(1.x 语义)\n const body = raw as TkRequest[\"body\"];\n return { method: \"POST\", path: \"/\", query: {}, body, headers: lowerHeaders, ip, raw: event };\n}\n\n/**\n * 内部统一响应 → 云函数返回体(网关层承载状态码/CORS)。\n * @param response 内部统一响应\n * @returns 云函数返回体\n */\nexport function fromTkResponse(response: TkResponse): Record<string, unknown> {\n return response.body;\n}\n","/**\n * CloudBase 的 POST_SUBMIT 派发实现(规范「后置副作用异步语义」)。\n *\n * **1.x 语义对齐**:`src/server/function/twikoo/index.js` 的 `commentSubmit`\n * 保存评论后执行\n *\n * ```js\n * await app.callFunction({ name: context.function_name,\n * data: { event: 'POST_SUBMIT', comment } }, { timeout: 300 })\n * ```\n *\n * ——用 **300ms 的极短 timeout** 把 `callFunction` 变成「异步」:请求已经发出,\n * 本实例不再等待,被调用的实例在自己的时间预算里跑完垃圾检测与通知。这样\n * 用户请求可以立即返回,也不会因为整条副作用链超出云函数超时上限而让\n * 「评论已入库」变成客户端错误。\n *\n * **为什么放在适配器**:`callFunction` 是 CloudBase 专有 API,common 不应感知;\n * 副作用链本身仍在 common 的 postSubmit 服务里。\n */\nimport { RECURSION_HEADER, getRecursionToken, type PostSubmitDispatcher } from \"@twikoojs/common\";\nimport type { TcbAppLike, TcbContextLike } from \"./types\";\n\n/** 递归调用的 timeout(毫秒)。1.x 取值,仅用于「不再等待」,不是副作用的上限 */\nconst CALL_TIMEOUT_MS = 300;\n\n/**\n * 创建 CloudBase 的派发实现。\n * @param app CloudBase SDK app 实例(提供 callFunction)\n * @param context 本次调用的云函数上下文(取 function_name)\n * @returns POST_SUBMIT 派发实现\n */\nexport function createCloudBaseDispatcher(\n app: TcbAppLike,\n context?: TcbContextLike,\n): PostSubmitDispatcher {\n return {\n /**\n * 递归自调用本云函数执行 POST_SUBMIT(不等副作用完成)。\n * @param comment 已入库的评论\n * @param ctx 当前请求上下文\n */\n async dispatch(comment, ctx): Promise<void> {\n const name = process.env.TCB_FUNCTION_NAME ?? context?.function_name;\n if (!name) {\n ctx.logger.warn(\n \"POST_SUBMIT 派发跳过:未取到云函数名(context.function_name / TCB_FUNCTION_NAME)\",\n );\n return;\n }\n try {\n await app.callFunction(\n {\n name,\n data: {\n event: \"POST_SUBMIT\",\n comment,\n /**\n * 转发原请求头(被调用实例据此解析客户端 IP,语义与用户请求一致),\n * 并附上内部派发令牌——POST_SUBMIT 处理器会校验它,拒绝外部调用。\n */\n headers: {\n ...ctx.request.headers,\n [RECURSION_HEADER]: getRecursionToken(ctx.config),\n },\n },\n },\n { timeout: CALL_TIMEOUT_MS },\n );\n } catch {\n // 1.x 语义:300ms 超时是「异步化」手段——超时说明目标实例已在执行,属预期\n ctx.logger.verbose(\"POST_SUBMIT 已派出(本实例不再等待)\");\n }\n },\n };\n}\n","/**\n * twikoo-func 主逻辑(CloudBase 薄适配器)。\n * 业务逻辑全部在 @twikoojs/common;平台核对:docs.cloudbase.net 云函数章节(查阅 2026-09-17)。\n *\n * 载荷转换见 `./transform.ts`,后置副作用派发见 `./dispatch.ts`,\n * 平台 SDK 结构面见 `./types.ts`。\n */\nimport {\n Capabilities,\n CloudBaseDatabase,\n createHandler,\n defineCapabilities,\n type CloudBaseDatabaseLike,\n} from \"@twikoojs/common\";\nimport { fromTkResponse, toTkRequest } from \"./transform\";\nimport { createCloudBaseDispatcher } from \"./dispatch\";\nimport type { TcbAppLike, TcbContextLike, TcbSdkStatic } from \"./types\";\n\nexport { fromTkResponse, toTkRequest } from \"./transform\";\n\n/** CloudBase 平台能力:全能力*/\nconst cloudbaseCapabilities: Capabilities = defineCapabilities({\n mail: true,\n domPurify: true,\n ip2region: true,\n akismet: true,\n tencentTms: true,\n imageUpload: true,\n qqAvatar: true,\n ai: true,\n});\n\n/**\n * 创建 CloudBase 请求处理器(database / app 可注入供测试;缺省懒加载 TCB SDK)。\n * @param options 注入项\n * @returns 逐请求处理器(第二参数为云函数上下文,供 IP 解析与递归自调用取函数名)\n */\nexport function createTwikooFunc(\n options: { database?: CloudBaseDatabaseLike; app?: TcbAppLike } = {},\n): (event: unknown, context?: TcbContextLike) => Promise<Record<string, unknown>> {\n let app: TcbAppLike | null = options.app ?? null;\n let database: CloudBaseDatabaseLike | null = options.database ?? null;\n let sdkPromise: Promise<TcbAppLike> | null = null;\n /** 注入优先;否则动态加载 @cloudbase/node-sdk(SYMBOL_CURRENT_ENV) */\n const getApp = async (): Promise<TcbAppLike> => {\n if (app) return app;\n sdkPromise ??= (async () => {\n const specifier = \"@cloudbase/node-sdk\";\n // v3 起 ESM 命名导出不可用(命名空间仅 default/module.exports/version),\n // 统一回退取 default,兼容 v2 具名与 v3 default 两种形态。\n const mod = (await import(/* @vite-ignore */ specifier)) as unknown as {\n default?: TcbSdkStatic;\n } & TcbSdkStatic;\n const tcb: TcbSdkStatic = mod.default ?? mod;\n return tcb.init({ env: tcb.SYMBOL_CURRENT_ENV });\n })();\n app = await sdkPromise;\n return app;\n };\n /** 数据库实例(注入优先,否则取 app 的 database()) */\n const getDatabase = async (): Promise<CloudBaseDatabaseLike> => {\n database ??= (await getApp()).database();\n return database;\n };\n return async (event: unknown, context?: TcbContextLike) => {\n const request = toTkRequest(event, context);\n const db = await getDatabase();\n const handler = createHandler({\n request: {\n /** 事件已在上游转换为统一请求 */\n toTkRequest: () => request,\n },\n response: { fromTkResponse },\n database: new CloudBaseDatabase({ database: db }),\n storage: { challenges: {} as never, tokens: {} as never },\n mailer: {\n /** CloudBase 侧不直发邮件(由 common 内部按需处理) */\n send: async () => {},\n },\n notifier: {\n /** CloudBase 侧不直发推送(由 common 内部按需处理) */\n notify: async () => {},\n },\n postSubmit: createCloudBaseDispatcher(await getApp(), context),\n capabilities: cloudbaseCapabilities,\n });\n return fromTkResponse(await handler(request));\n };\n}\n\n/** 装配缓存(main 懒加载语义) */\nlet mainFn:\n ((event: unknown, context?: TcbContextLike) => Promise<Record<string, unknown>>) | null = null;\n\n/**\n * 云函数入口(exports.main 导出名硬约束)。\n * @param event 云函数事件\n * @param context 云函数上下文(IP 解析 + 递归自调用取 function_name)\n * @returns 云函数返回体\n */\nexport async function main(\n event: unknown,\n context?: TcbContextLike,\n): Promise<Record<string, unknown>> {\n mainFn ??= createTwikooFunc();\n return mainFn(event, context);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["space","jsdomModule","dompurifyModule","RECURSION_HEADER","getRecursionToken","defineCapabilities","tcbSdkNamespace","createHandler","CloudBaseDatabase"],"sources":["../src/prewarm.ts","../src/transform.ts","../src/dispatch.ts","../src/main.ts"],"sourcesContent":["/**\n * 重依赖预热:把 jsdom / DOMPurify 的加载从「调用路径」挪到「实例初始化阶段」。\n *\n * ## 为什么必须预热\n *\n * `@twikoojs/common` 的重依赖一律经 `lib-loader` 的 `loadLib()` **动态** import,\n * 于是 jsdom(4.2MB)的**首次**加载落在调用里。真机实测(2026-09-28,腾讯云 CloudBase\n * 免费体验版):一个只做 `await import(\"jsdom\")` + `new JSDOM(\"\")` 的探针就会\n * **打满 3 秒执行超时**;而同一实例紧接着的第二次调用只要 **13ms**(模块已在进程内)\n * —— 即贵的是「首次加载」,与用不用 `import()` 无关。\n *\n * 而免费体验版与个人版的云函数**执行超时固定 3 秒、不可修改**(见 `docs/backend.md`),\n * 这 3 秒直接决定「发评论」能否成功:`COMMENT_SUBMIT` 会经 `getDomPurify()` 加载 jsdom。\n *\n * ## 解法\n *\n * 在模块初始化阶段**同步**把 DOMPurify 装好,用 `setCustomLibs` 注入\n * (1.x `lib.js` 的逃生舱范式,EdgeOne Makers 适配器同款做法)。\n * `getDomPurify()` 里 `customLibs.DOMPurify` 优先级最高、直接返回,\n * `loadLib` 根本不会被调用 —— 实测覆写生效后该调用耗时 **0ms**。\n *\n * ## 实测效果(真实 `COMMENT_SUBMIT`,同一环境同一函数)\n *\n * | 指标 | 预热前 | 预热后 |\n * | --- | --- | --- |\n * | `InitFunction` | 594ms | 3574ms |\n * | 冷实例调用 | **3000ms 超时** | **1078ms 成功** |\n * | 热实例调用 | — | 454 / 714 / 1933ms |\n *\n * 即这 3 秒开销**从「执行时长」挪进了「初始化时长」**,而平台接受 3.6 秒的初始化。\n *\n * ## 为什么不能写成「模块顶层的浮空 Promise」\n *\n * CJS 没有顶层 await,浮空预热与首次调用是**竞态**:handler 等不到它跑完就开始了,\n * 首次请求照样超时(已实测)。所以这里的 import 必须是**同步**的。\n *\n * ## 破例说明与代价\n *\n * 本文件是全仓第二处**静态** import 重依赖的地方(另一处是\n * `packages/pkg/src/bundled-libs.ts`,目的是把重依赖内联进 SEA 单文件产物)。\n * AGENTS.md「重依赖全部 external + 动态加载」的禁令只约束\n * `packages/server-common/src/**`(由 `test/utils/lib-loader-literals.test.ts` 断言),\n * 适配器侧不受该断言约束;但为守住「薄适配器」,这里**只预热发评论路径上必需的\n * jsdom + DOMPurify**,其余重依赖维持动态加载。\n *\n * 代价:冷启动 `InitFunction` 由 ~0.6s 涨到 ~3.6s(每次新建实例都付),常驻内存 +~36MB。\n */\nimport * as dompurifyModule from \"dompurify\";\nimport * as jsdomModule from \"jsdom\";\nimport { setCustomLibs } from \"@twikoojs/common\";\n\n/** jsdom 的最小结构面(与 `@twikoojs/common` 的 `JSDOMLike` 同构,不引入其类型依赖) */\ninterface JSDOMCtor {\n /**\n * @param html HTML 文本\n * @returns window 环境句柄\n */\n new (html: string): { window: unknown };\n}\n\n/** DOMPurify 工厂与实例的最小结构面(1.x `createDOMPurify(window)` 语义) */\ntype DOMPurifyFactory = (window: unknown) => {\n /**\n * 消毒 HTML\n * @param dirty 原始 HTML\n * @param config 消毒配置\n * @returns 消毒后的 HTML\n */\n sanitize(dirty: string, config?: unknown): string;\n};\n\n/**\n * 取 CJS/ESM 兼容的模块本体(ESM 命名空间的 `default` 优先)。\n *\n * `import * as X from \"<CJS 包>\"` 在 ESM 下拿到的是命名空间\n * (`{ default: module.exports, ...具名导出 }`),本体不能直接调用;\n * 而 CJS 产物里 rolldown 会合成 `default`。两种形态都要能取到本体 ——\n * 与 `@twikoojs/common` 的 `pickDefault` 同一语义(那边是包内私有函数,故此处复刻)。\n * @param namespace 模块命名空间\n * @returns 模块本体\n */\nfunction pickDefault<T>(namespace: unknown): T {\n const space = namespace as { default?: T } | null | undefined;\n return (space?.default ?? namespace) as T;\n}\n\n/**\n * 预热好的 DOMPurify 实例(构造失败时为 `null`,交回 `getDomPurify` 的动态加载路径)。\n *\n * 用 try/catch 兜住构造:jsdom / dompurify 若因环境原因构造失败,应当退回\n * 「按需动态加载」而不是让整个云函数起不来;但**必须留痕** ——\n * 静默回退会让冷调用重新背上 3 秒级的 jsdom 首次加载(同 #1127 的教训)。\n */\nconst prewarmedDOMPurify: ReturnType<DOMPurifyFactory> | null = (() => {\n try {\n const { JSDOM } = pickDefault<{ JSDOM: JSDOMCtor }>(jsdomModule);\n const createDOMPurify = pickDefault<DOMPurifyFactory>(dompurifyModule);\n return createDOMPurify(new JSDOM(\"\").window);\n } catch (e) {\n console.warn(\n \"[twikoo] 重依赖预热失败,已回退按需加载(冷调用会重新付 jsdom 首次加载):\",\n e instanceof Error ? e.message : String(e),\n );\n return null;\n }\n})();\n\nif (prewarmedDOMPurify) {\n setCustomLibs({ DOMPurify: prewarmedDOMPurify });\n}\n","/**\n * CloudBase 载荷转换(事件 ↔ 内部统一请求/响应)。\n *\n * 从 `main.ts` 抽出:入口只负责 SDK 懒加载与端口装配,平台载荷的形状知识\n * (含 IP 解析优先级)集中在本文件。\n */\nimport type { TkRequest, TkResponse } from \"@twikoojs/common\";\nimport type { TcbContextLike } from \"./types\";\n\n/**\n * 取平台注入的上下文环境变量。\n *\n * 与 `@cloudbase/node-sdk` 的 `getCloudbaseContext()` 同源:该函数从\n * `context.environment` / `context.environ` 与 `process.env` 中按\n * `TCB_CONTEXT_KEYS` 列表取值。\n * @param context 云函数调用上下文\n * @returns 上下文环境变量(无则为空对象)\n */\nfunction contextEnvOf(context?: TcbContextLike): Record<string, string> {\n return context?.environment ?? context?.environ ?? {};\n}\n\n/**\n * CloudBase 事件 → 内部统一请求(1.x 语义:事件本身即请求体)。\n *\n * **IP 解析优先级**(1.x `auth.getClientIP()` 直接读平台注入的 `TCB_SOURCE_IP`,\n * 即 CloudBase 云函数的权威来源):\n * 1. `TCB_SOURCE_IP`——上下文环境变量,`process.env` 兜底;\n * 2. `x-real-ip` 请求头(HTTP 访问服务场景);\n * 3. `x-forwarded-for` 首跳;\n * 4. `event.requestContext.http.sourceIp`。\n *\n * ⚠️ 本链必须用 `||` 而非 `??`:链上各中间项都是字符串,**空串不是 nullish**。\n * 曾写成 `a ?? (cond ? v : \"\") ?? b`——中间项在条件不成立时求值为 `\"\"`,\n * 把后面的来源全部短路掉,使 `sourceIp` 成为永远取不到的死代码。\n *\n * @param event 云函数事件\n * @param context 云函数调用上下文(取 TCB_SOURCE_IP)\n * @returns 内部统一请求\n */\nexport function toTkRequest(event: unknown, context?: TcbContextLike): TkRequest {\n const raw = (event && typeof event === \"object\" ? event : {}) as Record<string, unknown>;\n const headers = (raw.headers ?? {}) as Record<string, string>;\n const lowerHeaders: Record<string, string> = {};\n for (const [key, value] of Object.entries(headers)) {\n lowerHeaders[key.toLowerCase()] = String(value);\n }\n const tcbSourceIp = contextEnvOf(context).TCB_SOURCE_IP ?? process.env.TCB_SOURCE_IP ?? \"\";\n const forwarded = lowerHeaders[\"x-forwarded-for\"];\n const forwardedIp = forwarded ? forwarded.split(\",\")[0].trim() : \"\";\n const sourceIp = (raw.requestContext as { http?: { sourceIp?: string } } | undefined)?.http\n ?.sourceIp;\n const ip = tcbSourceIp || lowerHeaders[\"x-real-ip\"] || forwardedIp || sourceIp || \"\";\n // 事件本身即请求体(1.x 语义)\n const body = raw as TkRequest[\"body\"];\n return { method: \"POST\", path: \"/\", query: {}, body, headers: lowerHeaders, ip, raw: event };\n}\n\n/**\n * 内部统一响应 → 云函数返回体(网关层承载状态码/CORS)。\n * @param response 内部统一响应\n * @returns 云函数返回体\n */\nexport function fromTkResponse(response: TkResponse): Record<string, unknown> {\n return response.body;\n}\n","/**\n * CloudBase 的 POST_SUBMIT 派发实现(规范「后置副作用异步语义」)。\n *\n * **1.x 语义对齐**:`src/server/function/twikoo/index.js` 的 `commentSubmit`\n * 保存评论后执行\n *\n * ```js\n * await app.callFunction({ name: context.function_name,\n * data: { event: 'POST_SUBMIT', comment } }, { timeout: 300 })\n * ```\n *\n * ——用 **300ms 的极短 timeout** 把 `callFunction` 变成「异步」:请求已经发出,\n * 本实例不再等待,被调用的实例在自己的时间预算里跑完垃圾检测与通知。这样\n * 用户请求可以立即返回,也不会因为整条副作用链超出云函数超时上限而让\n * 「评论已入库」变成客户端错误。\n *\n * **为什么放在适配器**:`callFunction` 是 CloudBase 专有 API,common 不应感知;\n * 副作用链本身仍在 common 的 postSubmit 服务里。\n */\nimport { RECURSION_HEADER, getRecursionToken, type PostSubmitDispatcher } from \"@twikoojs/common\";\nimport type { TcbAppLike, TcbContextLike } from \"./types\";\n\n/** 递归调用的 timeout(毫秒)。1.x 取值,仅用于「不再等待」,不是副作用的上限 */\nconst CALL_TIMEOUT_MS = 300;\n\n/**\n * 创建 CloudBase 的派发实现。\n * @param app CloudBase SDK app 实例(提供 callFunction)\n * @param context 本次调用的云函数上下文(取 function_name)\n * @returns POST_SUBMIT 派发实现\n */\nexport function createCloudBaseDispatcher(\n app: TcbAppLike,\n context?: TcbContextLike,\n): PostSubmitDispatcher {\n return {\n /**\n * 递归自调用本云函数执行 POST_SUBMIT(不等副作用完成)。\n * @param comment 已入库的评论\n * @param ctx 当前请求上下文\n */\n async dispatch(comment, ctx): Promise<void> {\n const name = process.env.TCB_FUNCTION_NAME ?? context?.function_name;\n if (!name) {\n ctx.logger.warn(\n \"POST_SUBMIT 派发跳过:未取到云函数名(context.function_name / TCB_FUNCTION_NAME)\",\n );\n return;\n }\n try {\n await app.callFunction(\n {\n name,\n data: {\n event: \"POST_SUBMIT\",\n comment,\n /**\n * 转发原请求头(被调用实例据此解析客户端 IP,语义与用户请求一致),\n * 并附上内部派发令牌——POST_SUBMIT 处理器会校验它,拒绝外部调用。\n */\n headers: {\n ...ctx.request.headers,\n [RECURSION_HEADER]: getRecursionToken(ctx.config),\n },\n },\n },\n { timeout: CALL_TIMEOUT_MS },\n );\n } catch {\n // 1.x 语义:300ms 超时是「异步化」手段——超时说明目标实例已在执行,属预期\n ctx.logger.verbose(\"POST_SUBMIT 已派出(本实例不再等待)\");\n }\n },\n };\n}\n","/**\n * twikoo-func 主逻辑(CloudBase 薄适配器)。\n * 业务逻辑全部在 @twikoojs/common;平台核对:docs.cloudbase.net 云函数章节(查阅 2026-09-17、\n * 真机复核 2026-09-24:运行环境 Node.js 20.19 可在线装依赖,24.11(公测中)不可)。\n *\n * 载荷转换见 `./transform.ts`,后置副作用派发见 `./dispatch.ts`,\n * 重依赖预热见 `./prewarm.ts`(平台契约层),平台 SDK 结构面见 `./types.ts`。\n */\nimport * as tcbSdkNamespace from \"@cloudbase/node-sdk\";\nimport {\n Capabilities,\n CloudBaseDatabase,\n createHandler,\n defineCapabilities,\n type CloudBaseDatabaseLike,\n} from \"@twikoojs/common\";\n/**\n * 副作用导入:在实例初始化阶段就把 jsdom / DOMPurify 装好(见 ./prewarm.ts 的完整说明)。\n * 必须早于任何请求 —— 模块顶层的静态 import 天然满足,故放在类型导入之后的常规位置。\n */\nimport \"./prewarm\";\nimport { fromTkResponse, toTkRequest } from \"./transform\";\nimport { createCloudBaseDispatcher } from \"./dispatch\";\nimport type { TcbAppLike, TcbContextLike, TcbSdkStatic } from \"./types\";\n\nexport { fromTkResponse, toTkRequest } from \"./transform\";\n\n/** CloudBase 平台能力:全能力*/\nconst cloudbaseCapabilities: Capabilities = defineCapabilities({\n mail: true,\n domPurify: true,\n ip2region: true,\n akismet: true,\n tencentTms: true,\n imageUpload: true,\n qqAvatar: true,\n ai: true,\n});\n\n/**\n * `@cloudbase/node-sdk` 的静态形态。\n *\n * **为什么是模块顶层静态 import,而不是调用内 `await import()`**:SDK 的模块加载在冷实例上\n * 约 2.4s(本地对照:`require` 203ms / `init` 18ms,即开销几乎全在加载),而**调用内**的耗时\n * 会算进函数执行时长、直接挤占「执行超时」预算。实测(2026-09-24,免费体验版\n * `GET_FUNC_VERSION`):冷调用 **2477ms**、同实例后续调用 **66~85ms**;把加载与 `init()`\n * 一并提到模块初始化阶段后,冷调用 **222ms**、`InitFunction` 112ms → 606ms —— 即这 2.4s\n * 从「执行时长」挪进了「初始化时长」。CloudBase 免费体验版/个人版的执行超时**固定 3 秒且\n * 不可修改**(见 `docs/backend.md`),这 2.4s 是决定性的。\n *\n * `@cloudbase/node-sdk` 不在 AGENTS.md「重依赖清单」内,且在 `dependencies` 中,\n * 故静态导入合规(构建侧 `deps.neverBundle` 保证它仍是 external,不进产物)。\n *\n * v3 起 ESM 命名导出不可用(命名空间仅 default/module.exports/version),\n * 统一回退取 default,兼容 v2 具名与 v3 default 两种形态。\n */\nconst tcbSdk: TcbSdkStatic = (() => {\n const mod = tcbSdkNamespace as unknown as { default?: TcbSdkStatic } & TcbSdkStatic;\n return mod.default ?? mod;\n})();\n\n/**\n * 模块级 app 单例(在**实例初始化阶段**构造)。\n *\n * 与 SDK 的官方写法一致(`const app = tcb.init({ env: tcb.SYMBOL_CURRENT_ENV })` 置于模块顶层),\n * 好处同样是让 `init()` 的耗时落在初始化阶段而非执行阶段。\n *\n * 构造失败(例如在云函数环境之外被加载)时回落为 `null`,由首次调用兜底构造 ——\n * 这样本地测试与注入路径(`options.app`)都不受影响。\n */\nconst sharedApp: TcbAppLike | null = (() => {\n try {\n return tcbSdk.init({ env: tcbSdk.SYMBOL_CURRENT_ENV });\n } catch {\n return null;\n }\n})();\n\n/**\n * 创建 CloudBase 请求处理器(database / app 可注入供测试;缺省用模块级单例)。\n * @param options 注入项\n * @returns 逐请求处理器(第二参数为云函数上下文,供 IP 解析与递归自调用取函数名)\n */\nexport function createTwikooFunc(\n options: { database?: CloudBaseDatabaseLike; app?: TcbAppLike } = {},\n): (event: unknown, context?: TcbContextLike) => Promise<Record<string, unknown>> {\n let app: TcbAppLike | null = options.app ?? sharedApp;\n let database: CloudBaseDatabaseLike | null = options.database ?? null;\n /**\n * 取 app:注入优先 → 模块级单例 → 兜底构造。\n * 只有模块级构造失败(`sharedApp === null`)才会走到最后一步。\n * @returns CloudBase app 实例\n */\n const getApp = (): TcbAppLike => {\n app ??= tcbSdk.init({ env: tcbSdk.SYMBOL_CURRENT_ENV });\n return app;\n };\n /**\n * 数据库实例(注入优先,否则取 app 的 `database()`)。\n * app 已在初始化阶段就绪,故这里只是一次廉价的句柄构造。\n * @returns 数据库句柄\n */\n const getDatabase = (): CloudBaseDatabaseLike => {\n database ??= getApp().database();\n return database;\n };\n return async (event: unknown, context?: TcbContextLike) => {\n const request = toTkRequest(event, context);\n const handler = createHandler({\n request: {\n /** 事件已在上游转换为统一请求 */\n toTkRequest: () => request,\n },\n response: { fromTkResponse },\n database: new CloudBaseDatabase({ database: getDatabase() }),\n storage: { challenges: {} as never, tokens: {} as never },\n mailer: {\n /** CloudBase 侧不直发邮件(由 common 内部按需处理) */\n send: async () => {},\n },\n notifier: {\n /** CloudBase 侧不直发推送(由 common 内部按需处理) */\n notify: async () => {},\n },\n postSubmit: createCloudBaseDispatcher(getApp(), context),\n capabilities: cloudbaseCapabilities,\n });\n return fromTkResponse(await handler(request));\n };\n}\n\n/** 装配缓存(main 懒加载语义) */\nlet mainFn:\n ((event: unknown, context?: TcbContextLike) => Promise<Record<string, unknown>>) | null = null;\n\n/**\n * 云函数入口(exports.main 导出名硬约束)。\n * @param event 云函数事件\n * @param context 云函数上下文(IP 解析 + 递归自调用取 function_name)\n * @returns 云函数返回体\n */\nexport async function main(\n event: unknown,\n context?: TcbContextLike,\n): Promise<Record<string, unknown>> {\n mainFn ??= createTwikooFunc();\n return mainFn(event, context);\n}\n"],"mappings":"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"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,86 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as tcbSdkNamespace from "@cloudbase/node-sdk";
|
|
2
|
+
import { CloudBaseDatabase, RECURSION_HEADER, createHandler, defineCapabilities, getRecursionToken, setCustomLibs } from "@twikoojs/common";
|
|
3
|
+
import * as dompurifyModule from "dompurify";
|
|
4
|
+
import * as jsdomModule from "jsdom";
|
|
5
|
+
//#region src/prewarm.ts
|
|
6
|
+
/**
|
|
7
|
+
* 重依赖预热:把 jsdom / DOMPurify 的加载从「调用路径」挪到「实例初始化阶段」。
|
|
8
|
+
*
|
|
9
|
+
* ## 为什么必须预热
|
|
10
|
+
*
|
|
11
|
+
* `@twikoojs/common` 的重依赖一律经 `lib-loader` 的 `loadLib()` **动态** import,
|
|
12
|
+
* 于是 jsdom(4.2MB)的**首次**加载落在调用里。真机实测(2026-09-28,腾讯云 CloudBase
|
|
13
|
+
* 免费体验版):一个只做 `await import("jsdom")` + `new JSDOM("")` 的探针就会
|
|
14
|
+
* **打满 3 秒执行超时**;而同一实例紧接着的第二次调用只要 **13ms**(模块已在进程内)
|
|
15
|
+
* —— 即贵的是「首次加载」,与用不用 `import()` 无关。
|
|
16
|
+
*
|
|
17
|
+
* 而免费体验版与个人版的云函数**执行超时固定 3 秒、不可修改**(见 `docs/backend.md`),
|
|
18
|
+
* 这 3 秒直接决定「发评论」能否成功:`COMMENT_SUBMIT` 会经 `getDomPurify()` 加载 jsdom。
|
|
19
|
+
*
|
|
20
|
+
* ## 解法
|
|
21
|
+
*
|
|
22
|
+
* 在模块初始化阶段**同步**把 DOMPurify 装好,用 `setCustomLibs` 注入
|
|
23
|
+
* (1.x `lib.js` 的逃生舱范式,EdgeOne Makers 适配器同款做法)。
|
|
24
|
+
* `getDomPurify()` 里 `customLibs.DOMPurify` 优先级最高、直接返回,
|
|
25
|
+
* `loadLib` 根本不会被调用 —— 实测覆写生效后该调用耗时 **0ms**。
|
|
26
|
+
*
|
|
27
|
+
* ## 实测效果(真实 `COMMENT_SUBMIT`,同一环境同一函数)
|
|
28
|
+
*
|
|
29
|
+
* | 指标 | 预热前 | 预热后 |
|
|
30
|
+
* | --- | --- | --- |
|
|
31
|
+
* | `InitFunction` | 594ms | 3574ms |
|
|
32
|
+
* | 冷实例调用 | **3000ms 超时** | **1078ms 成功** |
|
|
33
|
+
* | 热实例调用 | — | 454 / 714 / 1933ms |
|
|
34
|
+
*
|
|
35
|
+
* 即这 3 秒开销**从「执行时长」挪进了「初始化时长」**,而平台接受 3.6 秒的初始化。
|
|
36
|
+
*
|
|
37
|
+
* ## 为什么不能写成「模块顶层的浮空 Promise」
|
|
38
|
+
*
|
|
39
|
+
* CJS 没有顶层 await,浮空预热与首次调用是**竞态**:handler 等不到它跑完就开始了,
|
|
40
|
+
* 首次请求照样超时(已实测)。所以这里的 import 必须是**同步**的。
|
|
41
|
+
*
|
|
42
|
+
* ## 破例说明与代价
|
|
43
|
+
*
|
|
44
|
+
* 本文件是全仓第二处**静态** import 重依赖的地方(另一处是
|
|
45
|
+
* `packages/pkg/src/bundled-libs.ts`,目的是把重依赖内联进 SEA 单文件产物)。
|
|
46
|
+
* AGENTS.md「重依赖全部 external + 动态加载」的禁令只约束
|
|
47
|
+
* `packages/server-common/src/**`(由 `test/utils/lib-loader-literals.test.ts` 断言),
|
|
48
|
+
* 适配器侧不受该断言约束;但为守住「薄适配器」,这里**只预热发评论路径上必需的
|
|
49
|
+
* jsdom + DOMPurify**,其余重依赖维持动态加载。
|
|
50
|
+
*
|
|
51
|
+
* 代价:冷启动 `InitFunction` 由 ~0.6s 涨到 ~3.6s(每次新建实例都付),常驻内存 +~36MB。
|
|
52
|
+
*/
|
|
53
|
+
/**
|
|
54
|
+
* 取 CJS/ESM 兼容的模块本体(ESM 命名空间的 `default` 优先)。
|
|
55
|
+
*
|
|
56
|
+
* `import * as X from "<CJS 包>"` 在 ESM 下拿到的是命名空间
|
|
57
|
+
* (`{ default: module.exports, ...具名导出 }`),本体不能直接调用;
|
|
58
|
+
* 而 CJS 产物里 rolldown 会合成 `default`。两种形态都要能取到本体 ——
|
|
59
|
+
* 与 `@twikoojs/common` 的 `pickDefault` 同一语义(那边是包内私有函数,故此处复刻)。
|
|
60
|
+
* @param namespace 模块命名空间
|
|
61
|
+
* @returns 模块本体
|
|
62
|
+
*/
|
|
63
|
+
function pickDefault(namespace) {
|
|
64
|
+
return namespace?.default ?? namespace;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* 预热好的 DOMPurify 实例(构造失败时为 `null`,交回 `getDomPurify` 的动态加载路径)。
|
|
68
|
+
*
|
|
69
|
+
* 用 try/catch 兜住构造:jsdom / dompurify 若因环境原因构造失败,应当退回
|
|
70
|
+
* 「按需动态加载」而不是让整个云函数起不来;但**必须留痕** ——
|
|
71
|
+
* 静默回退会让冷调用重新背上 3 秒级的 jsdom 首次加载(同 #1127 的教训)。
|
|
72
|
+
*/
|
|
73
|
+
const prewarmedDOMPurify = (() => {
|
|
74
|
+
try {
|
|
75
|
+
const { JSDOM } = pickDefault(jsdomModule);
|
|
76
|
+
return pickDefault(dompurifyModule)(new JSDOM("").window);
|
|
77
|
+
} catch (e) {
|
|
78
|
+
console.warn("[twikoo] 重依赖预热失败,已回退按需加载(冷调用会重新付 jsdom 首次加载):", e instanceof Error ? e.message : String(e));
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
})();
|
|
82
|
+
if (prewarmedDOMPurify) setCustomLibs({ DOMPurify: prewarmedDOMPurify });
|
|
83
|
+
//#endregion
|
|
2
84
|
//#region src/transform.ts
|
|
3
85
|
/**
|
|
4
86
|
* 取平台注入的上下文环境变量。
|
|
@@ -124,10 +206,11 @@ async dispatch(comment, ctx) {
|
|
|
124
206
|
//#region src/main.ts
|
|
125
207
|
/**
|
|
126
208
|
* twikoo-func 主逻辑(CloudBase 薄适配器)。
|
|
127
|
-
* 业务逻辑全部在 @twikoojs/common;平台核对:docs.cloudbase.net 云函数章节(查阅 2026-09-17
|
|
209
|
+
* 业务逻辑全部在 @twikoojs/common;平台核对:docs.cloudbase.net 云函数章节(查阅 2026-09-17、
|
|
210
|
+
* 真机复核 2026-09-24:运行环境 Node.js 20.19 可在线装依赖,24.11(公测中)不可)。
|
|
128
211
|
*
|
|
129
212
|
* 载荷转换见 `./transform.ts`,后置副作用派发见 `./dispatch.ts`,
|
|
130
|
-
*
|
|
213
|
+
* 重依赖预热见 `./prewarm.ts`(平台契约层),平台 SDK 结构面见 `./types.ts`。
|
|
131
214
|
*/
|
|
132
215
|
/** CloudBase 平台能力:全能力*/
|
|
133
216
|
const cloudbaseCapabilities = defineCapabilities({
|
|
@@ -141,42 +224,76 @@ const cloudbaseCapabilities = defineCapabilities({
|
|
|
141
224
|
ai: true
|
|
142
225
|
});
|
|
143
226
|
/**
|
|
144
|
-
*
|
|
227
|
+
* `@cloudbase/node-sdk` 的静态形态。
|
|
228
|
+
*
|
|
229
|
+
* **为什么是模块顶层静态 import,而不是调用内 `await import()`**:SDK 的模块加载在冷实例上
|
|
230
|
+
* 约 2.4s(本地对照:`require` 203ms / `init` 18ms,即开销几乎全在加载),而**调用内**的耗时
|
|
231
|
+
* 会算进函数执行时长、直接挤占「执行超时」预算。实测(2026-09-24,免费体验版
|
|
232
|
+
* `GET_FUNC_VERSION`):冷调用 **2477ms**、同实例后续调用 **66~85ms**;把加载与 `init()`
|
|
233
|
+
* 一并提到模块初始化阶段后,冷调用 **222ms**、`InitFunction` 112ms → 606ms —— 即这 2.4s
|
|
234
|
+
* 从「执行时长」挪进了「初始化时长」。CloudBase 免费体验版/个人版的执行超时**固定 3 秒且
|
|
235
|
+
* 不可修改**(见 `docs/backend.md`),这 2.4s 是决定性的。
|
|
236
|
+
*
|
|
237
|
+
* `@cloudbase/node-sdk` 不在 AGENTS.md「重依赖清单」内,且在 `dependencies` 中,
|
|
238
|
+
* 故静态导入合规(构建侧 `deps.neverBundle` 保证它仍是 external,不进产物)。
|
|
239
|
+
*
|
|
240
|
+
* v3 起 ESM 命名导出不可用(命名空间仅 default/module.exports/version),
|
|
241
|
+
* 统一回退取 default,兼容 v2 具名与 v3 default 两种形态。
|
|
242
|
+
*/
|
|
243
|
+
const tcbSdk = (() => {
|
|
244
|
+
const mod = tcbSdkNamespace;
|
|
245
|
+
return mod.default ?? mod;
|
|
246
|
+
})();
|
|
247
|
+
/**
|
|
248
|
+
* 模块级 app 单例(在**实例初始化阶段**构造)。
|
|
249
|
+
*
|
|
250
|
+
* 与 SDK 的官方写法一致(`const app = tcb.init({ env: tcb.SYMBOL_CURRENT_ENV })` 置于模块顶层),
|
|
251
|
+
* 好处同样是让 `init()` 的耗时落在初始化阶段而非执行阶段。
|
|
252
|
+
*
|
|
253
|
+
* 构造失败(例如在云函数环境之外被加载)时回落为 `null`,由首次调用兜底构造 ——
|
|
254
|
+
* 这样本地测试与注入路径(`options.app`)都不受影响。
|
|
255
|
+
*/
|
|
256
|
+
const sharedApp = (() => {
|
|
257
|
+
try {
|
|
258
|
+
return tcbSdk.init({ env: tcbSdk.SYMBOL_CURRENT_ENV });
|
|
259
|
+
} catch {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
})();
|
|
263
|
+
/**
|
|
264
|
+
* 创建 CloudBase 请求处理器(database / app 可注入供测试;缺省用模块级单例)。
|
|
145
265
|
* @param options 注入项
|
|
146
266
|
* @returns 逐请求处理器(第二参数为云函数上下文,供 IP 解析与递归自调用取函数名)
|
|
147
267
|
*/
|
|
148
268
|
function createTwikooFunc(options = {}) {
|
|
149
|
-
let app = options.app ??
|
|
269
|
+
let app = options.app ?? sharedApp;
|
|
150
270
|
let database = options.database ?? null;
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
"@cloudbase/node-sdk"
|
|
159
|
-
);
|
|
160
|
-
const tcb = mod.default ?? mod;
|
|
161
|
-
return tcb.init({ env: tcb.SYMBOL_CURRENT_ENV });
|
|
162
|
-
})();
|
|
163
|
-
app = await sdkPromise;
|
|
271
|
+
/**
|
|
272
|
+
* 取 app:注入优先 → 模块级单例 → 兜底构造。
|
|
273
|
+
* 只有模块级构造失败(`sharedApp === null`)才会走到最后一步。
|
|
274
|
+
* @returns CloudBase app 实例
|
|
275
|
+
*/
|
|
276
|
+
const getApp = () => {
|
|
277
|
+
app ??= tcbSdk.init({ env: tcbSdk.SYMBOL_CURRENT_ENV });
|
|
164
278
|
return app;
|
|
165
279
|
};
|
|
166
|
-
/**
|
|
167
|
-
|
|
168
|
-
|
|
280
|
+
/**
|
|
281
|
+
* 数据库实例(注入优先,否则取 app 的 `database()`)。
|
|
282
|
+
* app 已在初始化阶段就绪,故这里只是一次廉价的句柄构造。
|
|
283
|
+
* @returns 数据库句柄
|
|
284
|
+
*/
|
|
285
|
+
const getDatabase = () => {
|
|
286
|
+
database ??= getApp().database();
|
|
169
287
|
return database;
|
|
170
288
|
};
|
|
171
289
|
return async (event, context) => {
|
|
172
290
|
const request = toTkRequest(event, context);
|
|
173
|
-
const db = await getDatabase();
|
|
174
291
|
return fromTkResponse(await createHandler({
|
|
175
292
|
request: {
|
|
176
293
|
/** 事件已在上游转换为统一请求 */
|
|
177
294
|
toTkRequest: () => request },
|
|
178
295
|
response: { fromTkResponse },
|
|
179
|
-
database: new CloudBaseDatabase({ database:
|
|
296
|
+
database: new CloudBaseDatabase({ database: getDatabase() }),
|
|
180
297
|
storage: {
|
|
181
298
|
challenges: {},
|
|
182
299
|
tokens: {}
|
|
@@ -187,7 +304,7 @@ send: async () => {} },
|
|
|
187
304
|
notifier: {
|
|
188
305
|
/** CloudBase 侧不直发推送(由 common 内部按需处理) */
|
|
189
306
|
notify: async () => {} },
|
|
190
|
-
postSubmit: createCloudBaseDispatcher(
|
|
307
|
+
postSubmit: createCloudBaseDispatcher(getApp(), context),
|
|
191
308
|
capabilities: cloudbaseCapabilities
|
|
192
309
|
})(request));
|
|
193
310
|
};
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/transform.ts","../src/dispatch.ts","../src/main.ts"],"sourcesContent":["/**\n * CloudBase 载荷转换(事件 ↔ 内部统一请求/响应)。\n *\n * 从 `main.ts` 抽出:入口只负责 SDK 懒加载与端口装配,平台载荷的形状知识\n * (含 IP 解析优先级)集中在本文件。\n */\nimport type { TkRequest, TkResponse } from \"@twikoojs/common\";\nimport type { TcbContextLike } from \"./types\";\n\n/**\n * 取平台注入的上下文环境变量。\n *\n * 与 `@cloudbase/node-sdk` 的 `getCloudbaseContext()` 同源:该函数从\n * `context.environment` / `context.environ` 与 `process.env` 中按\n * `TCB_CONTEXT_KEYS` 列表取值。\n * @param context 云函数调用上下文\n * @returns 上下文环境变量(无则为空对象)\n */\nfunction contextEnvOf(context?: TcbContextLike): Record<string, string> {\n return context?.environment ?? context?.environ ?? {};\n}\n\n/**\n * CloudBase 事件 → 内部统一请求(1.x 语义:事件本身即请求体)。\n *\n * **IP 解析优先级**(1.x `auth.getClientIP()` 直接读平台注入的 `TCB_SOURCE_IP`,\n * 即 CloudBase 云函数的权威来源):\n * 1. `TCB_SOURCE_IP`——上下文环境变量,`process.env` 兜底;\n * 2. `x-real-ip` 请求头(HTTP 访问服务场景);\n * 3. `x-forwarded-for` 首跳;\n * 4. `event.requestContext.http.sourceIp`。\n *\n * ⚠️ 本链必须用 `||` 而非 `??`:链上各中间项都是字符串,**空串不是 nullish**。\n * 曾写成 `a ?? (cond ? v : \"\") ?? b`——中间项在条件不成立时求值为 `\"\"`,\n * 把后面的来源全部短路掉,使 `sourceIp` 成为永远取不到的死代码。\n *\n * @param event 云函数事件\n * @param context 云函数调用上下文(取 TCB_SOURCE_IP)\n * @returns 内部统一请求\n */\nexport function toTkRequest(event: unknown, context?: TcbContextLike): TkRequest {\n const raw = (event && typeof event === \"object\" ? event : {}) as Record<string, unknown>;\n const headers = (raw.headers ?? {}) as Record<string, string>;\n const lowerHeaders: Record<string, string> = {};\n for (const [key, value] of Object.entries(headers)) {\n lowerHeaders[key.toLowerCase()] = String(value);\n }\n const tcbSourceIp = contextEnvOf(context).TCB_SOURCE_IP ?? process.env.TCB_SOURCE_IP ?? \"\";\n const forwarded = lowerHeaders[\"x-forwarded-for\"];\n const forwardedIp = forwarded ? forwarded.split(\",\")[0].trim() : \"\";\n const sourceIp = (raw.requestContext as { http?: { sourceIp?: string } } | undefined)?.http\n ?.sourceIp;\n const ip = tcbSourceIp || lowerHeaders[\"x-real-ip\"] || forwardedIp || sourceIp || \"\";\n // 事件本身即请求体(1.x 语义)\n const body = raw as TkRequest[\"body\"];\n return { method: \"POST\", path: \"/\", query: {}, body, headers: lowerHeaders, ip, raw: event };\n}\n\n/**\n * 内部统一响应 → 云函数返回体(网关层承载状态码/CORS)。\n * @param response 内部统一响应\n * @returns 云函数返回体\n */\nexport function fromTkResponse(response: TkResponse): Record<string, unknown> {\n return response.body;\n}\n","/**\n * CloudBase 的 POST_SUBMIT 派发实现(规范「后置副作用异步语义」)。\n *\n * **1.x 语义对齐**:`src/server/function/twikoo/index.js` 的 `commentSubmit`\n * 保存评论后执行\n *\n * ```js\n * await app.callFunction({ name: context.function_name,\n * data: { event: 'POST_SUBMIT', comment } }, { timeout: 300 })\n * ```\n *\n * ——用 **300ms 的极短 timeout** 把 `callFunction` 变成「异步」:请求已经发出,\n * 本实例不再等待,被调用的实例在自己的时间预算里跑完垃圾检测与通知。这样\n * 用户请求可以立即返回,也不会因为整条副作用链超出云函数超时上限而让\n * 「评论已入库」变成客户端错误。\n *\n * **为什么放在适配器**:`callFunction` 是 CloudBase 专有 API,common 不应感知;\n * 副作用链本身仍在 common 的 postSubmit 服务里。\n */\nimport { RECURSION_HEADER, getRecursionToken, type PostSubmitDispatcher } from \"@twikoojs/common\";\nimport type { TcbAppLike, TcbContextLike } from \"./types\";\n\n/** 递归调用的 timeout(毫秒)。1.x 取值,仅用于「不再等待」,不是副作用的上限 */\nconst CALL_TIMEOUT_MS = 300;\n\n/**\n * 创建 CloudBase 的派发实现。\n * @param app CloudBase SDK app 实例(提供 callFunction)\n * @param context 本次调用的云函数上下文(取 function_name)\n * @returns POST_SUBMIT 派发实现\n */\nexport function createCloudBaseDispatcher(\n app: TcbAppLike,\n context?: TcbContextLike,\n): PostSubmitDispatcher {\n return {\n /**\n * 递归自调用本云函数执行 POST_SUBMIT(不等副作用完成)。\n * @param comment 已入库的评论\n * @param ctx 当前请求上下文\n */\n async dispatch(comment, ctx): Promise<void> {\n const name = process.env.TCB_FUNCTION_NAME ?? context?.function_name;\n if (!name) {\n ctx.logger.warn(\n \"POST_SUBMIT 派发跳过:未取到云函数名(context.function_name / TCB_FUNCTION_NAME)\",\n );\n return;\n }\n try {\n await app.callFunction(\n {\n name,\n data: {\n event: \"POST_SUBMIT\",\n comment,\n /**\n * 转发原请求头(被调用实例据此解析客户端 IP,语义与用户请求一致),\n * 并附上内部派发令牌——POST_SUBMIT 处理器会校验它,拒绝外部调用。\n */\n headers: {\n ...ctx.request.headers,\n [RECURSION_HEADER]: getRecursionToken(ctx.config),\n },\n },\n },\n { timeout: CALL_TIMEOUT_MS },\n );\n } catch {\n // 1.x 语义:300ms 超时是「异步化」手段——超时说明目标实例已在执行,属预期\n ctx.logger.verbose(\"POST_SUBMIT 已派出(本实例不再等待)\");\n }\n },\n };\n}\n","/**\n * twikoo-func 主逻辑(CloudBase 薄适配器)。\n * 业务逻辑全部在 @twikoojs/common;平台核对:docs.cloudbase.net 云函数章节(查阅 2026-09-17)。\n *\n * 载荷转换见 `./transform.ts`,后置副作用派发见 `./dispatch.ts`,\n * 平台 SDK 结构面见 `./types.ts`。\n */\nimport {\n Capabilities,\n CloudBaseDatabase,\n createHandler,\n defineCapabilities,\n type CloudBaseDatabaseLike,\n} from \"@twikoojs/common\";\nimport { fromTkResponse, toTkRequest } from \"./transform\";\nimport { createCloudBaseDispatcher } from \"./dispatch\";\nimport type { TcbAppLike, TcbContextLike, TcbSdkStatic } from \"./types\";\n\nexport { fromTkResponse, toTkRequest } from \"./transform\";\n\n/** CloudBase 平台能力:全能力*/\nconst cloudbaseCapabilities: Capabilities = defineCapabilities({\n mail: true,\n domPurify: true,\n ip2region: true,\n akismet: true,\n tencentTms: true,\n imageUpload: true,\n qqAvatar: true,\n ai: true,\n});\n\n/**\n * 创建 CloudBase 请求处理器(database / app 可注入供测试;缺省懒加载 TCB SDK)。\n * @param options 注入项\n * @returns 逐请求处理器(第二参数为云函数上下文,供 IP 解析与递归自调用取函数名)\n */\nexport function createTwikooFunc(\n options: { database?: CloudBaseDatabaseLike; app?: TcbAppLike } = {},\n): (event: unknown, context?: TcbContextLike) => Promise<Record<string, unknown>> {\n let app: TcbAppLike | null = options.app ?? null;\n let database: CloudBaseDatabaseLike | null = options.database ?? null;\n let sdkPromise: Promise<TcbAppLike> | null = null;\n /** 注入优先;否则动态加载 @cloudbase/node-sdk(SYMBOL_CURRENT_ENV) */\n const getApp = async (): Promise<TcbAppLike> => {\n if (app) return app;\n sdkPromise ??= (async () => {\n const specifier = \"@cloudbase/node-sdk\";\n // v3 起 ESM 命名导出不可用(命名空间仅 default/module.exports/version),\n // 统一回退取 default,兼容 v2 具名与 v3 default 两种形态。\n const mod = (await import(/* @vite-ignore */ specifier)) as unknown as {\n default?: TcbSdkStatic;\n } & TcbSdkStatic;\n const tcb: TcbSdkStatic = mod.default ?? mod;\n return tcb.init({ env: tcb.SYMBOL_CURRENT_ENV });\n })();\n app = await sdkPromise;\n return app;\n };\n /** 数据库实例(注入优先,否则取 app 的 database()) */\n const getDatabase = async (): Promise<CloudBaseDatabaseLike> => {\n database ??= (await getApp()).database();\n return database;\n };\n return async (event: unknown, context?: TcbContextLike) => {\n const request = toTkRequest(event, context);\n const db = await getDatabase();\n const handler = createHandler({\n request: {\n /** 事件已在上游转换为统一请求 */\n toTkRequest: () => request,\n },\n response: { fromTkResponse },\n database: new CloudBaseDatabase({ database: db }),\n storage: { challenges: {} as never, tokens: {} as never },\n mailer: {\n /** CloudBase 侧不直发邮件(由 common 内部按需处理) */\n send: async () => {},\n },\n notifier: {\n /** CloudBase 侧不直发推送(由 common 内部按需处理) */\n notify: async () => {},\n },\n postSubmit: createCloudBaseDispatcher(await getApp(), context),\n capabilities: cloudbaseCapabilities,\n });\n return fromTkResponse(await handler(request));\n };\n}\n\n/** 装配缓存(main 懒加载语义) */\nlet mainFn:\n ((event: unknown, context?: TcbContextLike) => Promise<Record<string, unknown>>) | null = null;\n\n/**\n * 云函数入口(exports.main 导出名硬约束)。\n * @param event 云函数事件\n * @param context 云函数上下文(IP 解析 + 递归自调用取 function_name)\n * @returns 云函数返回体\n */\nexport async function main(\n event: unknown,\n context?: TcbContextLike,\n): Promise<Record<string, unknown>> {\n mainFn ??= createTwikooFunc();\n return mainFn(event, context);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["space"],"sources":["../src/prewarm.ts","../src/transform.ts","../src/dispatch.ts","../src/main.ts"],"sourcesContent":["/**\n * 重依赖预热:把 jsdom / DOMPurify 的加载从「调用路径」挪到「实例初始化阶段」。\n *\n * ## 为什么必须预热\n *\n * `@twikoojs/common` 的重依赖一律经 `lib-loader` 的 `loadLib()` **动态** import,\n * 于是 jsdom(4.2MB)的**首次**加载落在调用里。真机实测(2026-09-28,腾讯云 CloudBase\n * 免费体验版):一个只做 `await import(\"jsdom\")` + `new JSDOM(\"\")` 的探针就会\n * **打满 3 秒执行超时**;而同一实例紧接着的第二次调用只要 **13ms**(模块已在进程内)\n * —— 即贵的是「首次加载」,与用不用 `import()` 无关。\n *\n * 而免费体验版与个人版的云函数**执行超时固定 3 秒、不可修改**(见 `docs/backend.md`),\n * 这 3 秒直接决定「发评论」能否成功:`COMMENT_SUBMIT` 会经 `getDomPurify()` 加载 jsdom。\n *\n * ## 解法\n *\n * 在模块初始化阶段**同步**把 DOMPurify 装好,用 `setCustomLibs` 注入\n * (1.x `lib.js` 的逃生舱范式,EdgeOne Makers 适配器同款做法)。\n * `getDomPurify()` 里 `customLibs.DOMPurify` 优先级最高、直接返回,\n * `loadLib` 根本不会被调用 —— 实测覆写生效后该调用耗时 **0ms**。\n *\n * ## 实测效果(真实 `COMMENT_SUBMIT`,同一环境同一函数)\n *\n * | 指标 | 预热前 | 预热后 |\n * | --- | --- | --- |\n * | `InitFunction` | 594ms | 3574ms |\n * | 冷实例调用 | **3000ms 超时** | **1078ms 成功** |\n * | 热实例调用 | — | 454 / 714 / 1933ms |\n *\n * 即这 3 秒开销**从「执行时长」挪进了「初始化时长」**,而平台接受 3.6 秒的初始化。\n *\n * ## 为什么不能写成「模块顶层的浮空 Promise」\n *\n * CJS 没有顶层 await,浮空预热与首次调用是**竞态**:handler 等不到它跑完就开始了,\n * 首次请求照样超时(已实测)。所以这里的 import 必须是**同步**的。\n *\n * ## 破例说明与代价\n *\n * 本文件是全仓第二处**静态** import 重依赖的地方(另一处是\n * `packages/pkg/src/bundled-libs.ts`,目的是把重依赖内联进 SEA 单文件产物)。\n * AGENTS.md「重依赖全部 external + 动态加载」的禁令只约束\n * `packages/server-common/src/**`(由 `test/utils/lib-loader-literals.test.ts` 断言),\n * 适配器侧不受该断言约束;但为守住「薄适配器」,这里**只预热发评论路径上必需的\n * jsdom + DOMPurify**,其余重依赖维持动态加载。\n *\n * 代价:冷启动 `InitFunction` 由 ~0.6s 涨到 ~3.6s(每次新建实例都付),常驻内存 +~36MB。\n */\nimport * as dompurifyModule from \"dompurify\";\nimport * as jsdomModule from \"jsdom\";\nimport { setCustomLibs } from \"@twikoojs/common\";\n\n/** jsdom 的最小结构面(与 `@twikoojs/common` 的 `JSDOMLike` 同构,不引入其类型依赖) */\ninterface JSDOMCtor {\n /**\n * @param html HTML 文本\n * @returns window 环境句柄\n */\n new (html: string): { window: unknown };\n}\n\n/** DOMPurify 工厂与实例的最小结构面(1.x `createDOMPurify(window)` 语义) */\ntype DOMPurifyFactory = (window: unknown) => {\n /**\n * 消毒 HTML\n * @param dirty 原始 HTML\n * @param config 消毒配置\n * @returns 消毒后的 HTML\n */\n sanitize(dirty: string, config?: unknown): string;\n};\n\n/**\n * 取 CJS/ESM 兼容的模块本体(ESM 命名空间的 `default` 优先)。\n *\n * `import * as X from \"<CJS 包>\"` 在 ESM 下拿到的是命名空间\n * (`{ default: module.exports, ...具名导出 }`),本体不能直接调用;\n * 而 CJS 产物里 rolldown 会合成 `default`。两种形态都要能取到本体 ——\n * 与 `@twikoojs/common` 的 `pickDefault` 同一语义(那边是包内私有函数,故此处复刻)。\n * @param namespace 模块命名空间\n * @returns 模块本体\n */\nfunction pickDefault<T>(namespace: unknown): T {\n const space = namespace as { default?: T } | null | undefined;\n return (space?.default ?? namespace) as T;\n}\n\n/**\n * 预热好的 DOMPurify 实例(构造失败时为 `null`,交回 `getDomPurify` 的动态加载路径)。\n *\n * 用 try/catch 兜住构造:jsdom / dompurify 若因环境原因构造失败,应当退回\n * 「按需动态加载」而不是让整个云函数起不来;但**必须留痕** ——\n * 静默回退会让冷调用重新背上 3 秒级的 jsdom 首次加载(同 #1127 的教训)。\n */\nconst prewarmedDOMPurify: ReturnType<DOMPurifyFactory> | null = (() => {\n try {\n const { JSDOM } = pickDefault<{ JSDOM: JSDOMCtor }>(jsdomModule);\n const createDOMPurify = pickDefault<DOMPurifyFactory>(dompurifyModule);\n return createDOMPurify(new JSDOM(\"\").window);\n } catch (e) {\n console.warn(\n \"[twikoo] 重依赖预热失败,已回退按需加载(冷调用会重新付 jsdom 首次加载):\",\n e instanceof Error ? e.message : String(e),\n );\n return null;\n }\n})();\n\nif (prewarmedDOMPurify) {\n setCustomLibs({ DOMPurify: prewarmedDOMPurify });\n}\n","/**\n * CloudBase 载荷转换(事件 ↔ 内部统一请求/响应)。\n *\n * 从 `main.ts` 抽出:入口只负责 SDK 懒加载与端口装配,平台载荷的形状知识\n * (含 IP 解析优先级)集中在本文件。\n */\nimport type { TkRequest, TkResponse } from \"@twikoojs/common\";\nimport type { TcbContextLike } from \"./types\";\n\n/**\n * 取平台注入的上下文环境变量。\n *\n * 与 `@cloudbase/node-sdk` 的 `getCloudbaseContext()` 同源:该函数从\n * `context.environment` / `context.environ` 与 `process.env` 中按\n * `TCB_CONTEXT_KEYS` 列表取值。\n * @param context 云函数调用上下文\n * @returns 上下文环境变量(无则为空对象)\n */\nfunction contextEnvOf(context?: TcbContextLike): Record<string, string> {\n return context?.environment ?? context?.environ ?? {};\n}\n\n/**\n * CloudBase 事件 → 内部统一请求(1.x 语义:事件本身即请求体)。\n *\n * **IP 解析优先级**(1.x `auth.getClientIP()` 直接读平台注入的 `TCB_SOURCE_IP`,\n * 即 CloudBase 云函数的权威来源):\n * 1. `TCB_SOURCE_IP`——上下文环境变量,`process.env` 兜底;\n * 2. `x-real-ip` 请求头(HTTP 访问服务场景);\n * 3. `x-forwarded-for` 首跳;\n * 4. `event.requestContext.http.sourceIp`。\n *\n * ⚠️ 本链必须用 `||` 而非 `??`:链上各中间项都是字符串,**空串不是 nullish**。\n * 曾写成 `a ?? (cond ? v : \"\") ?? b`——中间项在条件不成立时求值为 `\"\"`,\n * 把后面的来源全部短路掉,使 `sourceIp` 成为永远取不到的死代码。\n *\n * @param event 云函数事件\n * @param context 云函数调用上下文(取 TCB_SOURCE_IP)\n * @returns 内部统一请求\n */\nexport function toTkRequest(event: unknown, context?: TcbContextLike): TkRequest {\n const raw = (event && typeof event === \"object\" ? event : {}) as Record<string, unknown>;\n const headers = (raw.headers ?? {}) as Record<string, string>;\n const lowerHeaders: Record<string, string> = {};\n for (const [key, value] of Object.entries(headers)) {\n lowerHeaders[key.toLowerCase()] = String(value);\n }\n const tcbSourceIp = contextEnvOf(context).TCB_SOURCE_IP ?? process.env.TCB_SOURCE_IP ?? \"\";\n const forwarded = lowerHeaders[\"x-forwarded-for\"];\n const forwardedIp = forwarded ? forwarded.split(\",\")[0].trim() : \"\";\n const sourceIp = (raw.requestContext as { http?: { sourceIp?: string } } | undefined)?.http\n ?.sourceIp;\n const ip = tcbSourceIp || lowerHeaders[\"x-real-ip\"] || forwardedIp || sourceIp || \"\";\n // 事件本身即请求体(1.x 语义)\n const body = raw as TkRequest[\"body\"];\n return { method: \"POST\", path: \"/\", query: {}, body, headers: lowerHeaders, ip, raw: event };\n}\n\n/**\n * 内部统一响应 → 云函数返回体(网关层承载状态码/CORS)。\n * @param response 内部统一响应\n * @returns 云函数返回体\n */\nexport function fromTkResponse(response: TkResponse): Record<string, unknown> {\n return response.body;\n}\n","/**\n * CloudBase 的 POST_SUBMIT 派发实现(规范「后置副作用异步语义」)。\n *\n * **1.x 语义对齐**:`src/server/function/twikoo/index.js` 的 `commentSubmit`\n * 保存评论后执行\n *\n * ```js\n * await app.callFunction({ name: context.function_name,\n * data: { event: 'POST_SUBMIT', comment } }, { timeout: 300 })\n * ```\n *\n * ——用 **300ms 的极短 timeout** 把 `callFunction` 变成「异步」:请求已经发出,\n * 本实例不再等待,被调用的实例在自己的时间预算里跑完垃圾检测与通知。这样\n * 用户请求可以立即返回,也不会因为整条副作用链超出云函数超时上限而让\n * 「评论已入库」变成客户端错误。\n *\n * **为什么放在适配器**:`callFunction` 是 CloudBase 专有 API,common 不应感知;\n * 副作用链本身仍在 common 的 postSubmit 服务里。\n */\nimport { RECURSION_HEADER, getRecursionToken, type PostSubmitDispatcher } from \"@twikoojs/common\";\nimport type { TcbAppLike, TcbContextLike } from \"./types\";\n\n/** 递归调用的 timeout(毫秒)。1.x 取值,仅用于「不再等待」,不是副作用的上限 */\nconst CALL_TIMEOUT_MS = 300;\n\n/**\n * 创建 CloudBase 的派发实现。\n * @param app CloudBase SDK app 实例(提供 callFunction)\n * @param context 本次调用的云函数上下文(取 function_name)\n * @returns POST_SUBMIT 派发实现\n */\nexport function createCloudBaseDispatcher(\n app: TcbAppLike,\n context?: TcbContextLike,\n): PostSubmitDispatcher {\n return {\n /**\n * 递归自调用本云函数执行 POST_SUBMIT(不等副作用完成)。\n * @param comment 已入库的评论\n * @param ctx 当前请求上下文\n */\n async dispatch(comment, ctx): Promise<void> {\n const name = process.env.TCB_FUNCTION_NAME ?? context?.function_name;\n if (!name) {\n ctx.logger.warn(\n \"POST_SUBMIT 派发跳过:未取到云函数名(context.function_name / TCB_FUNCTION_NAME)\",\n );\n return;\n }\n try {\n await app.callFunction(\n {\n name,\n data: {\n event: \"POST_SUBMIT\",\n comment,\n /**\n * 转发原请求头(被调用实例据此解析客户端 IP,语义与用户请求一致),\n * 并附上内部派发令牌——POST_SUBMIT 处理器会校验它,拒绝外部调用。\n */\n headers: {\n ...ctx.request.headers,\n [RECURSION_HEADER]: getRecursionToken(ctx.config),\n },\n },\n },\n { timeout: CALL_TIMEOUT_MS },\n );\n } catch {\n // 1.x 语义:300ms 超时是「异步化」手段——超时说明目标实例已在执行,属预期\n ctx.logger.verbose(\"POST_SUBMIT 已派出(本实例不再等待)\");\n }\n },\n };\n}\n","/**\n * twikoo-func 主逻辑(CloudBase 薄适配器)。\n * 业务逻辑全部在 @twikoojs/common;平台核对:docs.cloudbase.net 云函数章节(查阅 2026-09-17、\n * 真机复核 2026-09-24:运行环境 Node.js 20.19 可在线装依赖,24.11(公测中)不可)。\n *\n * 载荷转换见 `./transform.ts`,后置副作用派发见 `./dispatch.ts`,\n * 重依赖预热见 `./prewarm.ts`(平台契约层),平台 SDK 结构面见 `./types.ts`。\n */\nimport * as tcbSdkNamespace from \"@cloudbase/node-sdk\";\nimport {\n Capabilities,\n CloudBaseDatabase,\n createHandler,\n defineCapabilities,\n type CloudBaseDatabaseLike,\n} from \"@twikoojs/common\";\n/**\n * 副作用导入:在实例初始化阶段就把 jsdom / DOMPurify 装好(见 ./prewarm.ts 的完整说明)。\n * 必须早于任何请求 —— 模块顶层的静态 import 天然满足,故放在类型导入之后的常规位置。\n */\nimport \"./prewarm\";\nimport { fromTkResponse, toTkRequest } from \"./transform\";\nimport { createCloudBaseDispatcher } from \"./dispatch\";\nimport type { TcbAppLike, TcbContextLike, TcbSdkStatic } from \"./types\";\n\nexport { fromTkResponse, toTkRequest } from \"./transform\";\n\n/** CloudBase 平台能力:全能力*/\nconst cloudbaseCapabilities: Capabilities = defineCapabilities({\n mail: true,\n domPurify: true,\n ip2region: true,\n akismet: true,\n tencentTms: true,\n imageUpload: true,\n qqAvatar: true,\n ai: true,\n});\n\n/**\n * `@cloudbase/node-sdk` 的静态形态。\n *\n * **为什么是模块顶层静态 import,而不是调用内 `await import()`**:SDK 的模块加载在冷实例上\n * 约 2.4s(本地对照:`require` 203ms / `init` 18ms,即开销几乎全在加载),而**调用内**的耗时\n * 会算进函数执行时长、直接挤占「执行超时」预算。实测(2026-09-24,免费体验版\n * `GET_FUNC_VERSION`):冷调用 **2477ms**、同实例后续调用 **66~85ms**;把加载与 `init()`\n * 一并提到模块初始化阶段后,冷调用 **222ms**、`InitFunction` 112ms → 606ms —— 即这 2.4s\n * 从「执行时长」挪进了「初始化时长」。CloudBase 免费体验版/个人版的执行超时**固定 3 秒且\n * 不可修改**(见 `docs/backend.md`),这 2.4s 是决定性的。\n *\n * `@cloudbase/node-sdk` 不在 AGENTS.md「重依赖清单」内,且在 `dependencies` 中,\n * 故静态导入合规(构建侧 `deps.neverBundle` 保证它仍是 external,不进产物)。\n *\n * v3 起 ESM 命名导出不可用(命名空间仅 default/module.exports/version),\n * 统一回退取 default,兼容 v2 具名与 v3 default 两种形态。\n */\nconst tcbSdk: TcbSdkStatic = (() => {\n const mod = tcbSdkNamespace as unknown as { default?: TcbSdkStatic } & TcbSdkStatic;\n return mod.default ?? mod;\n})();\n\n/**\n * 模块级 app 单例(在**实例初始化阶段**构造)。\n *\n * 与 SDK 的官方写法一致(`const app = tcb.init({ env: tcb.SYMBOL_CURRENT_ENV })` 置于模块顶层),\n * 好处同样是让 `init()` 的耗时落在初始化阶段而非执行阶段。\n *\n * 构造失败(例如在云函数环境之外被加载)时回落为 `null`,由首次调用兜底构造 ——\n * 这样本地测试与注入路径(`options.app`)都不受影响。\n */\nconst sharedApp: TcbAppLike | null = (() => {\n try {\n return tcbSdk.init({ env: tcbSdk.SYMBOL_CURRENT_ENV });\n } catch {\n return null;\n }\n})();\n\n/**\n * 创建 CloudBase 请求处理器(database / app 可注入供测试;缺省用模块级单例)。\n * @param options 注入项\n * @returns 逐请求处理器(第二参数为云函数上下文,供 IP 解析与递归自调用取函数名)\n */\nexport function createTwikooFunc(\n options: { database?: CloudBaseDatabaseLike; app?: TcbAppLike } = {},\n): (event: unknown, context?: TcbContextLike) => Promise<Record<string, unknown>> {\n let app: TcbAppLike | null = options.app ?? sharedApp;\n let database: CloudBaseDatabaseLike | null = options.database ?? null;\n /**\n * 取 app:注入优先 → 模块级单例 → 兜底构造。\n * 只有模块级构造失败(`sharedApp === null`)才会走到最后一步。\n * @returns CloudBase app 实例\n */\n const getApp = (): TcbAppLike => {\n app ??= tcbSdk.init({ env: tcbSdk.SYMBOL_CURRENT_ENV });\n return app;\n };\n /**\n * 数据库实例(注入优先,否则取 app 的 `database()`)。\n * app 已在初始化阶段就绪,故这里只是一次廉价的句柄构造。\n * @returns 数据库句柄\n */\n const getDatabase = (): CloudBaseDatabaseLike => {\n database ??= getApp().database();\n return database;\n };\n return async (event: unknown, context?: TcbContextLike) => {\n const request = toTkRequest(event, context);\n const handler = createHandler({\n request: {\n /** 事件已在上游转换为统一请求 */\n toTkRequest: () => request,\n },\n response: { fromTkResponse },\n database: new CloudBaseDatabase({ database: getDatabase() }),\n storage: { challenges: {} as never, tokens: {} as never },\n mailer: {\n /** CloudBase 侧不直发邮件(由 common 内部按需处理) */\n send: async () => {},\n },\n notifier: {\n /** CloudBase 侧不直发推送(由 common 内部按需处理) */\n notify: async () => {},\n },\n postSubmit: createCloudBaseDispatcher(getApp(), context),\n capabilities: cloudbaseCapabilities,\n });\n return fromTkResponse(await handler(request));\n };\n}\n\n/** 装配缓存(main 懒加载语义) */\nlet mainFn:\n ((event: unknown, context?: TcbContextLike) => Promise<Record<string, unknown>>) | null = null;\n\n/**\n * 云函数入口(exports.main 导出名硬约束)。\n * @param event 云函数事件\n * @param context 云函数上下文(IP 解析 + 递归自调用取 function_name)\n * @returns 云函数返回体\n */\nexport async function main(\n event: unknown,\n context?: TcbContextLike,\n): Promise<Record<string, unknown>> {\n mainFn ??= createTwikooFunc();\n return mainFn(event, context);\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "twikoo-func",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.10",
|
|
4
4
|
"description": "Twikoo comment system backend for CloudBase",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
"@cap.js/server": "^4.0.5",
|
|
25
25
|
"@cloudbase/node-sdk": "^3.18.3",
|
|
26
26
|
"@imaegoo/node-ip2region": "^2.1.1",
|
|
27
|
-
"@twikoojs/common": "2.0.
|
|
28
|
-
"@twikoojs/shared": "2.0.
|
|
27
|
+
"@twikoojs/common": "2.0.10",
|
|
28
|
+
"@twikoojs/shared": "2.0.10",
|
|
29
29
|
"@xsai/generate-text": "^0.5.0",
|
|
30
30
|
"akismet-api": "^6.0.0",
|
|
31
31
|
"bowser": "^2.14.1",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"jsdom": "~25.0.1",
|
|
36
36
|
"marked": "^18.0.13",
|
|
37
37
|
"nodemailer": "^10.0.10",
|
|
38
|
-
"pushoo": "^2.0.
|
|
38
|
+
"pushoo": "^2.0.10",
|
|
39
39
|
"tencentcloud-sdk-nodejs-tms": "^4.1.311",
|
|
40
40
|
"xml2js": "^0.6.2"
|
|
41
41
|
},
|