twikoo-netlify 1.7.23 → 2.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -3
- package/dist/index.d.mts +46 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +155 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +150 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +41 -6
- package/netlify/functions/twikoo.js +0 -47
package/README.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
|
-
#
|
|
1
|
+
# twikoo-netlify
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Twikoo 2.0 服务端适配器。业务逻辑在 `@twikoojs/common`,本包仅做平台入口与注入。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## 部署(Netlify)
|
|
6
|
+
|
|
7
|
+
1. Netlify → New site → 导入仓库(Functions 目录 `netlify/functions`)
|
|
8
|
+
2. 环境变量:`MONGODB_URI`(必填)
|
|
9
|
+
3. 前端配置 API 地址为 `https://<site>.netlify.app/.netlify/functions/twikoo`
|
|
10
|
+
|
|
11
|
+
## 平台核对清单(查阅日期 2026-09-17)
|
|
12
|
+
|
|
13
|
+
- [x] Functions v1 形态(`exports.handler`,event/返回体字符串化)——docs.netlify.com/functions
|
|
14
|
+
- [x] IP 头 `x-nf-client-connection-ip`(适配器已按此头提取,回退 x-real-ip/x-forwarded-for)
|
|
15
|
+
- [ ] Functions v2 形态差异实测(人工项;2.0 按事件归一化设计,v2 需平台适配层)
|
|
16
|
+
- [ ] 构建期 Node 版本对函数运行时的影响实测(人工项)
|
|
17
|
+
|
|
18
|
+
## 注意
|
|
19
|
+
|
|
20
|
+
- 独立适配器:直接依赖 @twikoojs/common,**不依赖 twikoo-vercel**(用户决策)
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Database, PostSubmitDispatcher, TkRequest, TkResponse } from "@twikoojs/common";
|
|
2
|
+
//#region src/main.d.ts
|
|
3
|
+
/** Netlify Functions v1 事件的最小结构面 */
|
|
4
|
+
interface NetlifyEventLike {
|
|
5
|
+
httpMethod: string;
|
|
6
|
+
headers: Record<string, string | string[] | undefined>;
|
|
7
|
+
/** 请求体(Netlify 交付为字符串) */
|
|
8
|
+
body: string | null;
|
|
9
|
+
rawUrl?: string;
|
|
10
|
+
}
|
|
11
|
+
/** Netlify 返回体(v1 形态:body 必须为字符串) */
|
|
12
|
+
interface NetlifyResult {
|
|
13
|
+
statusCode: number;
|
|
14
|
+
headers: Record<string, string>;
|
|
15
|
+
/** 返回体(isBase64Encoded 缺省 false,JSON 直接字符串化) */
|
|
16
|
+
body: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Netlify 事件 → 内部统一请求(IP 优先 x-nf-client-connection-ip,1.x 对齐)。
|
|
20
|
+
* @param event Netlify 事件
|
|
21
|
+
* @returns 内部统一请求
|
|
22
|
+
*/
|
|
23
|
+
export declare function toTkRequest(event: NetlifyEventLike): TkRequest;
|
|
24
|
+
/** 内部统一响应 → Netlify 返回体(204 无体;业务体字符串化)。 */
|
|
25
|
+
export declare function fromTkResponse(tkRes: TkResponse): NetlifyResult;
|
|
26
|
+
/**
|
|
27
|
+
* 创建 Netlify 请求处理器(database 可注入;缺省按 MONGODB_URI 懒建连)。
|
|
28
|
+
* @param options 注入项(database:Database 实现;mongoUri:连接串覆写)
|
|
29
|
+
* @returns 逐事件处理器
|
|
30
|
+
*/
|
|
31
|
+
export declare function createNetlifyFunc(options?: {
|
|
32
|
+
database?: Database;
|
|
33
|
+
mongoUri?: string;
|
|
34
|
+
}): (event: NetlifyEventLike) => Promise<NetlifyResult>;
|
|
35
|
+
/**
|
|
36
|
+
* Netlify Function 入口(exports.handler 形态,v1 CJS)。
|
|
37
|
+
* @param event Netlify 事件
|
|
38
|
+
* @returns Netlify 返回体
|
|
39
|
+
*/
|
|
40
|
+
export declare function handler(event: NetlifyEventLike): Promise<NetlifyResult>;
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region src/dispatch.d.ts
|
|
43
|
+
/** Netlify 的 POST_SUBMIT 派发实现(HTTP 递归自调用) */
|
|
44
|
+
export declare const netlifyPostSubmitDispatcher: PostSubmitDispatcher;
|
|
45
|
+
//#endregion
|
|
46
|
+
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Database, PostSubmitDispatcher, TkRequest, TkResponse } from "@twikoojs/common";
|
|
2
|
+
//#region src/main.d.ts
|
|
3
|
+
/** Netlify Functions v1 事件的最小结构面 */
|
|
4
|
+
interface NetlifyEventLike {
|
|
5
|
+
httpMethod: string;
|
|
6
|
+
headers: Record<string, string | string[] | undefined>;
|
|
7
|
+
/** 请求体(Netlify 交付为字符串) */
|
|
8
|
+
body: string | null;
|
|
9
|
+
rawUrl?: string;
|
|
10
|
+
}
|
|
11
|
+
/** Netlify 返回体(v1 形态:body 必须为字符串) */
|
|
12
|
+
interface NetlifyResult {
|
|
13
|
+
statusCode: number;
|
|
14
|
+
headers: Record<string, string>;
|
|
15
|
+
/** 返回体(isBase64Encoded 缺省 false,JSON 直接字符串化) */
|
|
16
|
+
body: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Netlify 事件 → 内部统一请求(IP 优先 x-nf-client-connection-ip,1.x 对齐)。
|
|
20
|
+
* @param event Netlify 事件
|
|
21
|
+
* @returns 内部统一请求
|
|
22
|
+
*/
|
|
23
|
+
export declare function toTkRequest(event: NetlifyEventLike): TkRequest;
|
|
24
|
+
/** 内部统一响应 → Netlify 返回体(204 无体;业务体字符串化)。 */
|
|
25
|
+
export declare function fromTkResponse(tkRes: TkResponse): NetlifyResult;
|
|
26
|
+
/**
|
|
27
|
+
* 创建 Netlify 请求处理器(database 可注入;缺省按 MONGODB_URI 懒建连)。
|
|
28
|
+
* @param options 注入项(database:Database 实现;mongoUri:连接串覆写)
|
|
29
|
+
* @returns 逐事件处理器
|
|
30
|
+
*/
|
|
31
|
+
export declare function createNetlifyFunc(options?: {
|
|
32
|
+
database?: Database;
|
|
33
|
+
mongoUri?: string;
|
|
34
|
+
}): (event: NetlifyEventLike) => Promise<NetlifyResult>;
|
|
35
|
+
/**
|
|
36
|
+
* Netlify Function 入口(exports.handler 形态,v1 CJS)。
|
|
37
|
+
* @param event Netlify 事件
|
|
38
|
+
* @returns Netlify 返回体
|
|
39
|
+
*/
|
|
40
|
+
export declare function handler(event: NetlifyEventLike): Promise<NetlifyResult>;
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region src/dispatch.d.ts
|
|
43
|
+
/** Netlify 的 POST_SUBMIT 派发实现(HTTP 递归自调用) */
|
|
44
|
+
export declare const netlifyPostSubmitDispatcher: PostSubmitDispatcher;
|
|
45
|
+
//#endregion
|
|
46
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let _twikoojs_common = require("@twikoojs/common");
|
|
3
|
+
//#region src/dispatch.ts
|
|
4
|
+
/**
|
|
5
|
+
* Netlify 的 POST_SUBMIT 派发实现(规范「后置副作用异步语义」)。
|
|
6
|
+
*
|
|
7
|
+
* **与 1.x 的差异(修正)**:1.x 的 Netlify 适配器是 `require('twikoo-vercel')`
|
|
8
|
+
* 纯转发壳,于是继承了 vercel 的 HTTP 递归——而 vercel 取的是 `VERCEL_URL`,
|
|
9
|
+
* 在 Netlify 上并不存在,自调用实际发不出去(副作用静默丢失)。2.0 独立实现,
|
|
10
|
+
* 改用 Netlify 运行时可用的 `URL`(见下)。
|
|
11
|
+
*
|
|
12
|
+
* **平台事实**(docs.netlify.com「Environment variables and serverless functions」,
|
|
13
|
+
* 查阅 2026-09-18):Netlify Functions **运行时**可用的只读变量只有 `URL` /
|
|
14
|
+
* `SITE_NAME` / `SITE_ID`;`DEPLOY_URL` / `DEPLOY_PRIME_URL` 仅构建期可用。
|
|
15
|
+
* 函数调用路径为 `/.netlify/functions/<函数名>`。
|
|
16
|
+
*
|
|
17
|
+
* 副作用链本身仍在 common 的 postSubmit 服务里。
|
|
18
|
+
*/
|
|
19
|
+
/** 派发等待上限(毫秒),语义同 Vercel 实现(超时只表示本请求不再等) */
|
|
20
|
+
const DISPATCH_TIMEOUT_MS = 5e3;
|
|
21
|
+
/** Netlify Functions 的标准调用路径前缀(函数名为 twikoo) */
|
|
22
|
+
const FUNCTION_PATH = "/.netlify/functions/twikoo";
|
|
23
|
+
/**
|
|
24
|
+
* 解析自身函数地址:`TWIKOO_SELF_URL`(完整地址,显式覆写)→
|
|
25
|
+
* `${URL}/.netlify/functions/twikoo`(运行时只读变量)。
|
|
26
|
+
* @returns 绝对地址;URL 缺失时为空串
|
|
27
|
+
*/
|
|
28
|
+
function resolveSelfUrl() {
|
|
29
|
+
const explicit = process.env.TWIKOO_SELF_URL;
|
|
30
|
+
if (explicit) return explicit;
|
|
31
|
+
const siteUrl = process.env.URL;
|
|
32
|
+
return siteUrl ? `${siteUrl.replace(/\/$/, "")}${FUNCTION_PATH}` : "";
|
|
33
|
+
}
|
|
34
|
+
/** Netlify 的 POST_SUBMIT 派发实现(HTTP 递归自调用) */
|
|
35
|
+
const netlifyPostSubmitDispatcher = {
|
|
36
|
+
/**
|
|
37
|
+
* 自调用本函数执行 POST_SUBMIT(不等待副作用完成,只等有界竞速)。
|
|
38
|
+
* @param comment 已入库的评论
|
|
39
|
+
* @param ctx 当前请求上下文(用于取递归令牌与记录日志)
|
|
40
|
+
*/
|
|
41
|
+
async dispatch(comment, ctx) {
|
|
42
|
+
const url = resolveSelfUrl();
|
|
43
|
+
if (!url) {
|
|
44
|
+
ctx.logger.warn("POST_SUBMIT 派发跳过:未取到自身地址(TWIKOO_SELF_URL / URL)");
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const axios = await (0, _twikoojs_common.getAxios)();
|
|
48
|
+
await Promise.race([axios.post(url, {
|
|
49
|
+
event: "POST_SUBMIT",
|
|
50
|
+
comment
|
|
51
|
+
}, { headers: { [_twikoojs_common.RECURSION_HEADER]: (0, _twikoojs_common.getRecursionToken)(ctx.config) } }), new Promise((resolve) => setTimeout(resolve, DISPATCH_TIMEOUT_MS))]);
|
|
52
|
+
} };
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region src/main.ts
|
|
55
|
+
/**
|
|
56
|
+
* twikoo-netlify 主逻辑(Netlify 独立薄适配器——不再依赖 twikoo-vercel)。
|
|
57
|
+
* 业务逻辑全部在 @twikoojs/common;数据库 MONGODB_URI→Mongo。
|
|
58
|
+
* 平台核对:Functions v1 handler = async (event, context) =>
|
|
59
|
+
* { statusCode, headers, body(string) };IP 头 x-nf-client-connection-ip——
|
|
60
|
+
* docs.netlify.com/functions(查阅 2026-09-17)。1.x 的 TWIKOO_IP_HEADERS
|
|
61
|
+
* 环境变量机制由本适配器消化(公共库不感知该变量)。
|
|
62
|
+
*
|
|
63
|
+
* 后置副作用(垃圾检测 + 通知)经 {@link netlifyPostSubmitDispatcher} 以
|
|
64
|
+
* HTTP 递归自调用派发到独立执行单元,见 `./dispatch.ts`。
|
|
65
|
+
*/
|
|
66
|
+
/** Netlify 平台能力:全能力*/
|
|
67
|
+
const netlifyCapabilities = _twikoojs_common.FULL_CAPABILITIES;
|
|
68
|
+
/**
|
|
69
|
+
* Netlify 事件 → 内部统一请求(IP 优先 x-nf-client-connection-ip,1.x 对齐)。
|
|
70
|
+
* @param event Netlify 事件
|
|
71
|
+
* @returns 内部统一请求
|
|
72
|
+
*/
|
|
73
|
+
function toTkRequest(event) {
|
|
74
|
+
const headers = {};
|
|
75
|
+
for (const [key, value] of Object.entries(event.headers ?? {})) if (value !== void 0) headers[key.toLowerCase()] = Array.isArray(value) ? value[0] : value;
|
|
76
|
+
const forwarded = headers["x-forwarded-for"];
|
|
77
|
+
const ip = headers["x-nf-client-connection-ip"] ?? headers["x-real-ip"] ?? (forwarded ? forwarded.split(",")[0].trim() : "") ?? "";
|
|
78
|
+
let body = {};
|
|
79
|
+
try {
|
|
80
|
+
const parsed = event.body ? JSON.parse(event.body) : {};
|
|
81
|
+
if (parsed && typeof parsed === "object") body = parsed;
|
|
82
|
+
} catch {}
|
|
83
|
+
return {
|
|
84
|
+
method: event.httpMethod.toUpperCase(),
|
|
85
|
+
path: "/",
|
|
86
|
+
query: {},
|
|
87
|
+
body,
|
|
88
|
+
headers,
|
|
89
|
+
ip,
|
|
90
|
+
raw: event
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/** 内部统一响应 → Netlify 返回体(204 无体;业务体字符串化)。 */
|
|
94
|
+
function fromTkResponse(tkRes) {
|
|
95
|
+
if (tkRes.status === 204) return {
|
|
96
|
+
statusCode: 204,
|
|
97
|
+
headers: {},
|
|
98
|
+
body: ""
|
|
99
|
+
};
|
|
100
|
+
return {
|
|
101
|
+
statusCode: tkRes.status,
|
|
102
|
+
headers: {
|
|
103
|
+
...tkRes.headers,
|
|
104
|
+
"Content-Type": "application/json"
|
|
105
|
+
},
|
|
106
|
+
body: JSON.stringify(tkRes.body)
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* 创建 Netlify 请求处理器(database 可注入;缺省按 MONGODB_URI 懒建连)。
|
|
111
|
+
* @param options 注入项(database:Database 实现;mongoUri:连接串覆写)
|
|
112
|
+
* @returns 逐事件处理器
|
|
113
|
+
*/
|
|
114
|
+
function createNetlifyFunc(options = {}) {
|
|
115
|
+
let database = options.database ?? null;
|
|
116
|
+
/** 懒建连(inject 优先;init 幂等) */
|
|
117
|
+
const getDatabase = async () => {
|
|
118
|
+
database ??= new _twikoojs_common.MongoDatabase({ uri: options.mongoUri ?? process.env.MONGODB_URI ?? "" });
|
|
119
|
+
await database.init();
|
|
120
|
+
return database;
|
|
121
|
+
};
|
|
122
|
+
return async (event) => {
|
|
123
|
+
const request = toTkRequest(event);
|
|
124
|
+
return fromTkResponse(await (0, _twikoojs_common.createHandler)((0, _twikoojs_common.scaffoldAdapters)({
|
|
125
|
+
request: {
|
|
126
|
+
/** 请求恒等透传(归一化见 toTkRequest) */
|
|
127
|
+
toTkRequest: () => request },
|
|
128
|
+
response: {
|
|
129
|
+
/** TkResponse 恒等透传(序列化见 fromTkResponse) */
|
|
130
|
+
fromTkResponse: (r) => r },
|
|
131
|
+
database: await getDatabase(),
|
|
132
|
+
capabilities: netlifyCapabilities,
|
|
133
|
+
postSubmit: netlifyPostSubmitDispatcher
|
|
134
|
+
}))(request));
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/** 装配缓存(handler 懒加载语义) */
|
|
138
|
+
let handlerFn = null;
|
|
139
|
+
/**
|
|
140
|
+
* Netlify Function 入口(exports.handler 形态,v1 CJS)。
|
|
141
|
+
* @param event Netlify 事件
|
|
142
|
+
* @returns Netlify 返回体
|
|
143
|
+
*/
|
|
144
|
+
async function handler(event) {
|
|
145
|
+
handlerFn ??= createNetlifyFunc();
|
|
146
|
+
return handlerFn(event);
|
|
147
|
+
}
|
|
148
|
+
//#endregion
|
|
149
|
+
exports.createNetlifyFunc = createNetlifyFunc;
|
|
150
|
+
exports.fromTkResponse = fromTkResponse;
|
|
151
|
+
exports.handler = handler;
|
|
152
|
+
exports.netlifyPostSubmitDispatcher = netlifyPostSubmitDispatcher;
|
|
153
|
+
exports.toTkRequest = toTkRequest;
|
|
154
|
+
|
|
155
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["getAxios","RECURSION_HEADER","getRecursionToken","FULL_CAPABILITIES","MongoDatabase","createHandler","scaffoldAdapters"],"sources":["../src/dispatch.ts","../src/main.ts"],"sourcesContent":["/**\n * Netlify 的 POST_SUBMIT 派发实现(规范「后置副作用异步语义」)。\n *\n * **与 1.x 的差异(修正)**:1.x 的 Netlify 适配器是 `require('twikoo-vercel')`\n * 纯转发壳,于是继承了 vercel 的 HTTP 递归——而 vercel 取的是 `VERCEL_URL`,\n * 在 Netlify 上并不存在,自调用实际发不出去(副作用静默丢失)。2.0 独立实现,\n * 改用 Netlify 运行时可用的 `URL`(见下)。\n *\n * **平台事实**(docs.netlify.com「Environment variables and serverless functions」,\n * 查阅 2026-09-18):Netlify Functions **运行时**可用的只读变量只有 `URL` /\n * `SITE_NAME` / `SITE_ID`;`DEPLOY_URL` / `DEPLOY_PRIME_URL` 仅构建期可用。\n * 函数调用路径为 `/.netlify/functions/<函数名>`。\n *\n * 副作用链本身仍在 common 的 postSubmit 服务里。\n */\nimport {\n RECURSION_HEADER,\n getAxios,\n getRecursionToken,\n type PostSubmitDispatcher,\n} from \"@twikoojs/common\";\n\n/** 派发等待上限(毫秒),语义同 Vercel 实现(超时只表示本请求不再等) */\nconst DISPATCH_TIMEOUT_MS = 5000;\n\n/** Netlify Functions 的标准调用路径前缀(函数名为 twikoo) */\nconst FUNCTION_PATH = \"/.netlify/functions/twikoo\";\n\n/**\n * 解析自身函数地址:`TWIKOO_SELF_URL`(完整地址,显式覆写)→\n * `${URL}/.netlify/functions/twikoo`(运行时只读变量)。\n * @returns 绝对地址;URL 缺失时为空串\n */\nfunction resolveSelfUrl(): string {\n const explicit = process.env.TWIKOO_SELF_URL;\n if (explicit) return explicit;\n const siteUrl = process.env.URL;\n return siteUrl ? `${siteUrl.replace(/\\/$/, \"\")}${FUNCTION_PATH}` : \"\";\n}\n\n/** Netlify 的 POST_SUBMIT 派发实现(HTTP 递归自调用) */\nexport const netlifyPostSubmitDispatcher: PostSubmitDispatcher = {\n /**\n * 自调用本函数执行 POST_SUBMIT(不等待副作用完成,只等有界竞速)。\n * @param comment 已入库的评论\n * @param ctx 当前请求上下文(用于取递归令牌与记录日志)\n */\n async dispatch(comment, ctx): Promise<void> {\n const url = resolveSelfUrl();\n if (!url) {\n ctx.logger.warn(\"POST_SUBMIT 派发跳过:未取到自身地址(TWIKOO_SELF_URL / URL)\");\n return;\n }\n const axios = await getAxios();\n await Promise.race([\n axios.post(\n url,\n { event: \"POST_SUBMIT\", comment },\n { headers: { [RECURSION_HEADER]: getRecursionToken(ctx.config) } },\n ),\n new Promise((resolve) => setTimeout(resolve, DISPATCH_TIMEOUT_MS)),\n ]);\n },\n};\n","/**\n * twikoo-netlify 主逻辑(Netlify 独立薄适配器——不再依赖 twikoo-vercel)。\n * 业务逻辑全部在 @twikoojs/common;数据库 MONGODB_URI→Mongo。\n * 平台核对:Functions v1 handler = async (event, context) =>\n * { statusCode, headers, body(string) };IP 头 x-nf-client-connection-ip——\n * docs.netlify.com/functions(查阅 2026-09-17)。1.x 的 TWIKOO_IP_HEADERS\n * 环境变量机制由本适配器消化(公共库不感知该变量)。\n *\n * 后置副作用(垃圾检测 + 通知)经 {@link netlifyPostSubmitDispatcher} 以\n * HTTP 递归自调用派发到独立执行单元,见 `./dispatch.ts`。\n */\nimport {\n FULL_CAPABILITIES,\n MongoDatabase,\n createHandler,\n scaffoldAdapters,\n type Database,\n type TkRequest,\n type TkResponse,\n} from \"@twikoojs/common\";\nimport { netlifyPostSubmitDispatcher } from \"./dispatch\";\n\n/** Netlify 平台能力:全能力*/\nconst netlifyCapabilities = FULL_CAPABILITIES;\n\n/** Netlify Functions v1 事件的最小结构面 */\nexport interface NetlifyEventLike {\n httpMethod: string;\n headers: Record<string, string | string[] | undefined>;\n /** 请求体(Netlify 交付为字符串) */\n body: string | null;\n rawUrl?: string;\n}\n\n/** Netlify 返回体(v1 形态:body 必须为字符串) */\nexport interface NetlifyResult {\n statusCode: number;\n headers: Record<string, string>;\n /** 返回体(isBase64Encoded 缺省 false,JSON 直接字符串化) */\n body: string;\n}\n\n/**\n * Netlify 事件 → 内部统一请求(IP 优先 x-nf-client-connection-ip,1.x 对齐)。\n * @param event Netlify 事件\n * @returns 内部统一请求\n */\nexport function toTkRequest(event: NetlifyEventLike): TkRequest {\n const headers: Record<string, string> = {};\n for (const [key, value] of Object.entries(event.headers ?? {})) {\n if (value !== undefined) headers[key.toLowerCase()] = Array.isArray(value) ? value[0] : value;\n }\n const forwarded = headers[\"x-forwarded-for\"];\n const ip =\n headers[\"x-nf-client-connection-ip\"] ??\n headers[\"x-real-ip\"] ??\n (forwarded ? forwarded.split(\",\")[0].trim() : \"\") ??\n \"\";\n let body = {} as TkRequest[\"body\"];\n try {\n const parsed = event.body ? JSON.parse(event.body) : {};\n if (parsed && typeof parsed === \"object\") body = parsed as TkRequest[\"body\"];\n } catch {\n // 非法 JSON 按空体处理(1.x try/catch 对齐)\n }\n return {\n method: event.httpMethod.toUpperCase(),\n path: \"/\",\n query: {},\n body,\n headers,\n ip,\n raw: event,\n };\n}\n\n/** 内部统一响应 → Netlify 返回体(204 无体;业务体字符串化)。 */\nexport function fromTkResponse(tkRes: TkResponse): NetlifyResult {\n if (tkRes.status === 204) {\n return { statusCode: 204, headers: {}, body: \"\" };\n }\n return {\n statusCode: tkRes.status,\n headers: { ...tkRes.headers, \"Content-Type\": \"application/json\" },\n body: JSON.stringify(tkRes.body),\n };\n}\n\n/**\n * 创建 Netlify 请求处理器(database 可注入;缺省按 MONGODB_URI 懒建连)。\n * @param options 注入项(database:Database 实现;mongoUri:连接串覆写)\n * @returns 逐事件处理器\n */\nexport function createNetlifyFunc(\n options: { database?: Database; mongoUri?: string } = {},\n): (event: NetlifyEventLike) => Promise<NetlifyResult> {\n let database: Database | null = options.database ?? null;\n /** 懒建连(inject 优先;init 幂等) */\n const getDatabase = async (): Promise<Database> => {\n database ??= new MongoDatabase({ uri: options.mongoUri ?? process.env.MONGODB_URI ?? \"\" });\n await database.init();\n return database;\n };\n return async (event) => {\n const request = toTkRequest(event);\n const handler = createHandler(\n scaffoldAdapters({\n request: {\n /** 请求恒等透传(归一化见 toTkRequest) */\n toTkRequest: () => request,\n },\n response: {\n /** TkResponse 恒等透传(序列化见 fromTkResponse) */\n fromTkResponse: (r: TkResponse) => r,\n },\n database: await getDatabase(),\n capabilities: netlifyCapabilities,\n postSubmit: netlifyPostSubmitDispatcher,\n }),\n );\n return fromTkResponse(await handler(request));\n };\n}\n\n/** 装配缓存(handler 懒加载语义) */\nlet handlerFn: ((event: NetlifyEventLike) => Promise<NetlifyResult>) | null = null;\n\n/**\n * Netlify Function 入口(exports.handler 形态,v1 CJS)。\n * @param event Netlify 事件\n * @returns Netlify 返回体\n */\nexport async function handler(event: NetlifyEventLike): Promise<NetlifyResult> {\n handlerFn ??= createNetlifyFunc();\n return handlerFn(event);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAuBA,MAAM,sBAAsB;;AAG5B,MAAM,gBAAgB;;;;;;AAOtB,SAAS,iBAAyB;CAChC,MAAM,WAAW,QAAQ,IAAI;CAC7B,IAAI,UAAU,OAAO;CACrB,MAAM,UAAU,QAAQ,IAAI;CAC5B,OAAO,UAAU,GAAG,QAAQ,QAAQ,OAAO,EAAE,IAAI,kBAAkB;AACrE;;AAGA,MAAa,8BAAoD;;;;;;AAM/D,MAAM,SAAS,SAAS,KAAoB;CAC1C,MAAM,MAAM,eAAe;CAC3B,IAAI,CAAC,KAAK;EACR,IAAI,OAAO,KAAK,iDAAiD;EACjE;CACF;CACA,MAAM,QAAQ,OAAA,GAAMA,iBAAAA,SAAAA,CAAS;CAC7B,MAAM,QAAQ,KAAK,CACjB,MAAM,KACJ,KACA;EAAE,OAAO;EAAe;CAAQ,GAChC,EAAE,SAAS,GAAGC,iBAAAA,oBAAAA,GAAmBC,iBAAAA,kBAAAA,CAAkB,IAAI,MAAM,EAAE,EAAE,CACnE,GACA,IAAI,SAAS,YAAY,WAAW,SAAS,mBAAmB,CAAC,CACnE,CAAC;AACH,EACF;;;;;;;;;;;;;;;ACxCA,MAAM,sBAAsBC,iBAAAA;;;;;;AAwB5B,SAAgB,YAAY,OAAoC;CAC9D,MAAM,UAAkC,CAAC;CACzC,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,WAAW,CAAC,CAAC,GAC3D,IAAI,UAAU,KAAA,GAAW,QAAQ,IAAI,YAAY,KAAK,MAAM,QAAQ,KAAK,IAAI,MAAM,KAAK;CAE1F,MAAM,YAAY,QAAQ;CAC1B,MAAM,KACJ,QAAQ,gCACR,QAAQ,iBACP,YAAY,UAAU,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC,KAAK,IAAI,OAC9C;CACF,IAAI,OAAO,CAAC;CACZ,IAAI;EACF,MAAM,SAAS,MAAM,OAAO,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC;EACtD,IAAI,UAAU,OAAO,WAAW,UAAU,OAAO;CACnD,QAAQ,CAER;CACA,OAAO;EACL,QAAQ,MAAM,WAAW,YAAY;EACrC,MAAM;EACN,OAAO,CAAC;EACR;EACA;EACA;EACA,KAAK;CACP;AACF;;AAGA,SAAgB,eAAe,OAAkC;CAC/D,IAAI,MAAM,WAAW,KACnB,OAAO;EAAE,YAAY;EAAK,SAAS,CAAC;EAAG,MAAM;CAAG;CAElD,OAAO;EACL,YAAY,MAAM;EAClB,SAAS;GAAE,GAAG,MAAM;GAAS,gBAAgB;EAAmB;EAChE,MAAM,KAAK,UAAU,MAAM,IAAI;CACjC;AACF;;;;;;AAOA,SAAgB,kBACd,UAAsD,CAAC,GACF;CACrD,IAAI,WAA4B,QAAQ,YAAY;;CAEpD,MAAM,cAAc,YAA+B;EACjD,aAAa,IAAIC,iBAAAA,cAAc,EAAE,KAAK,QAAQ,YAAY,QAAQ,IAAI,eAAe,GAAG,CAAC;EACzF,MAAM,SAAS,KAAK;EACpB,OAAO;CACT;CACA,OAAO,OAAO,UAAU;EACtB,MAAM,UAAU,YAAY,KAAK;EAgBjC,OAAO,eAAe,OAAA,GAfNC,iBAAAA,cAAAA,EAAAA,GACdC,iBAAAA,iBAAAA,CAAiB;GACf,SAAS;;AAEP,mBAAmB,QACrB;GACA,UAAU;;AAER,iBAAiB,MAAkB,EACrC;GACA,UAAU,MAAM,YAAY;GAC5B,cAAc;GACd,YAAY;EACd,CAAC,CAE+B,CAAC,CAAC,OAAO,CAAC;CAC9C;AACF;;AAGA,IAAI,YAA0E;;;;;;AAO9E,eAAsB,QAAQ,OAAiD;CAC7E,cAAc,kBAAkB;CAChC,OAAO,UAAU,KAAK;AACxB"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { FULL_CAPABILITIES, MongoDatabase, RECURSION_HEADER, createHandler, getAxios, getRecursionToken, scaffoldAdapters } from "@twikoojs/common";
|
|
2
|
+
//#region src/dispatch.ts
|
|
3
|
+
/**
|
|
4
|
+
* Netlify 的 POST_SUBMIT 派发实现(规范「后置副作用异步语义」)。
|
|
5
|
+
*
|
|
6
|
+
* **与 1.x 的差异(修正)**:1.x 的 Netlify 适配器是 `require('twikoo-vercel')`
|
|
7
|
+
* 纯转发壳,于是继承了 vercel 的 HTTP 递归——而 vercel 取的是 `VERCEL_URL`,
|
|
8
|
+
* 在 Netlify 上并不存在,自调用实际发不出去(副作用静默丢失)。2.0 独立实现,
|
|
9
|
+
* 改用 Netlify 运行时可用的 `URL`(见下)。
|
|
10
|
+
*
|
|
11
|
+
* **平台事实**(docs.netlify.com「Environment variables and serverless functions」,
|
|
12
|
+
* 查阅 2026-09-18):Netlify Functions **运行时**可用的只读变量只有 `URL` /
|
|
13
|
+
* `SITE_NAME` / `SITE_ID`;`DEPLOY_URL` / `DEPLOY_PRIME_URL` 仅构建期可用。
|
|
14
|
+
* 函数调用路径为 `/.netlify/functions/<函数名>`。
|
|
15
|
+
*
|
|
16
|
+
* 副作用链本身仍在 common 的 postSubmit 服务里。
|
|
17
|
+
*/
|
|
18
|
+
/** 派发等待上限(毫秒),语义同 Vercel 实现(超时只表示本请求不再等) */
|
|
19
|
+
const DISPATCH_TIMEOUT_MS = 5e3;
|
|
20
|
+
/** Netlify Functions 的标准调用路径前缀(函数名为 twikoo) */
|
|
21
|
+
const FUNCTION_PATH = "/.netlify/functions/twikoo";
|
|
22
|
+
/**
|
|
23
|
+
* 解析自身函数地址:`TWIKOO_SELF_URL`(完整地址,显式覆写)→
|
|
24
|
+
* `${URL}/.netlify/functions/twikoo`(运行时只读变量)。
|
|
25
|
+
* @returns 绝对地址;URL 缺失时为空串
|
|
26
|
+
*/
|
|
27
|
+
function resolveSelfUrl() {
|
|
28
|
+
const explicit = process.env.TWIKOO_SELF_URL;
|
|
29
|
+
if (explicit) return explicit;
|
|
30
|
+
const siteUrl = process.env.URL;
|
|
31
|
+
return siteUrl ? `${siteUrl.replace(/\/$/, "")}${FUNCTION_PATH}` : "";
|
|
32
|
+
}
|
|
33
|
+
/** Netlify 的 POST_SUBMIT 派发实现(HTTP 递归自调用) */
|
|
34
|
+
const netlifyPostSubmitDispatcher = {
|
|
35
|
+
/**
|
|
36
|
+
* 自调用本函数执行 POST_SUBMIT(不等待副作用完成,只等有界竞速)。
|
|
37
|
+
* @param comment 已入库的评论
|
|
38
|
+
* @param ctx 当前请求上下文(用于取递归令牌与记录日志)
|
|
39
|
+
*/
|
|
40
|
+
async dispatch(comment, ctx) {
|
|
41
|
+
const url = resolveSelfUrl();
|
|
42
|
+
if (!url) {
|
|
43
|
+
ctx.logger.warn("POST_SUBMIT 派发跳过:未取到自身地址(TWIKOO_SELF_URL / URL)");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const axios = await getAxios();
|
|
47
|
+
await Promise.race([axios.post(url, {
|
|
48
|
+
event: "POST_SUBMIT",
|
|
49
|
+
comment
|
|
50
|
+
}, { headers: { [RECURSION_HEADER]: getRecursionToken(ctx.config) } }), new Promise((resolve) => setTimeout(resolve, DISPATCH_TIMEOUT_MS))]);
|
|
51
|
+
} };
|
|
52
|
+
//#endregion
|
|
53
|
+
//#region src/main.ts
|
|
54
|
+
/**
|
|
55
|
+
* twikoo-netlify 主逻辑(Netlify 独立薄适配器——不再依赖 twikoo-vercel)。
|
|
56
|
+
* 业务逻辑全部在 @twikoojs/common;数据库 MONGODB_URI→Mongo。
|
|
57
|
+
* 平台核对:Functions v1 handler = async (event, context) =>
|
|
58
|
+
* { statusCode, headers, body(string) };IP 头 x-nf-client-connection-ip——
|
|
59
|
+
* docs.netlify.com/functions(查阅 2026-09-17)。1.x 的 TWIKOO_IP_HEADERS
|
|
60
|
+
* 环境变量机制由本适配器消化(公共库不感知该变量)。
|
|
61
|
+
*
|
|
62
|
+
* 后置副作用(垃圾检测 + 通知)经 {@link netlifyPostSubmitDispatcher} 以
|
|
63
|
+
* HTTP 递归自调用派发到独立执行单元,见 `./dispatch.ts`。
|
|
64
|
+
*/
|
|
65
|
+
/** Netlify 平台能力:全能力*/
|
|
66
|
+
const netlifyCapabilities = FULL_CAPABILITIES;
|
|
67
|
+
/**
|
|
68
|
+
* Netlify 事件 → 内部统一请求(IP 优先 x-nf-client-connection-ip,1.x 对齐)。
|
|
69
|
+
* @param event Netlify 事件
|
|
70
|
+
* @returns 内部统一请求
|
|
71
|
+
*/
|
|
72
|
+
function toTkRequest(event) {
|
|
73
|
+
const headers = {};
|
|
74
|
+
for (const [key, value] of Object.entries(event.headers ?? {})) if (value !== void 0) headers[key.toLowerCase()] = Array.isArray(value) ? value[0] : value;
|
|
75
|
+
const forwarded = headers["x-forwarded-for"];
|
|
76
|
+
const ip = headers["x-nf-client-connection-ip"] ?? headers["x-real-ip"] ?? (forwarded ? forwarded.split(",")[0].trim() : "") ?? "";
|
|
77
|
+
let body = {};
|
|
78
|
+
try {
|
|
79
|
+
const parsed = event.body ? JSON.parse(event.body) : {};
|
|
80
|
+
if (parsed && typeof parsed === "object") body = parsed;
|
|
81
|
+
} catch {}
|
|
82
|
+
return {
|
|
83
|
+
method: event.httpMethod.toUpperCase(),
|
|
84
|
+
path: "/",
|
|
85
|
+
query: {},
|
|
86
|
+
body,
|
|
87
|
+
headers,
|
|
88
|
+
ip,
|
|
89
|
+
raw: event
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/** 内部统一响应 → Netlify 返回体(204 无体;业务体字符串化)。 */
|
|
93
|
+
function fromTkResponse(tkRes) {
|
|
94
|
+
if (tkRes.status === 204) return {
|
|
95
|
+
statusCode: 204,
|
|
96
|
+
headers: {},
|
|
97
|
+
body: ""
|
|
98
|
+
};
|
|
99
|
+
return {
|
|
100
|
+
statusCode: tkRes.status,
|
|
101
|
+
headers: {
|
|
102
|
+
...tkRes.headers,
|
|
103
|
+
"Content-Type": "application/json"
|
|
104
|
+
},
|
|
105
|
+
body: JSON.stringify(tkRes.body)
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* 创建 Netlify 请求处理器(database 可注入;缺省按 MONGODB_URI 懒建连)。
|
|
110
|
+
* @param options 注入项(database:Database 实现;mongoUri:连接串覆写)
|
|
111
|
+
* @returns 逐事件处理器
|
|
112
|
+
*/
|
|
113
|
+
function createNetlifyFunc(options = {}) {
|
|
114
|
+
let database = options.database ?? null;
|
|
115
|
+
/** 懒建连(inject 优先;init 幂等) */
|
|
116
|
+
const getDatabase = async () => {
|
|
117
|
+
database ??= new MongoDatabase({ uri: options.mongoUri ?? process.env.MONGODB_URI ?? "" });
|
|
118
|
+
await database.init();
|
|
119
|
+
return database;
|
|
120
|
+
};
|
|
121
|
+
return async (event) => {
|
|
122
|
+
const request = toTkRequest(event);
|
|
123
|
+
return fromTkResponse(await createHandler(scaffoldAdapters({
|
|
124
|
+
request: {
|
|
125
|
+
/** 请求恒等透传(归一化见 toTkRequest) */
|
|
126
|
+
toTkRequest: () => request },
|
|
127
|
+
response: {
|
|
128
|
+
/** TkResponse 恒等透传(序列化见 fromTkResponse) */
|
|
129
|
+
fromTkResponse: (r) => r },
|
|
130
|
+
database: await getDatabase(),
|
|
131
|
+
capabilities: netlifyCapabilities,
|
|
132
|
+
postSubmit: netlifyPostSubmitDispatcher
|
|
133
|
+
}))(request));
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/** 装配缓存(handler 懒加载语义) */
|
|
137
|
+
let handlerFn = null;
|
|
138
|
+
/**
|
|
139
|
+
* Netlify Function 入口(exports.handler 形态,v1 CJS)。
|
|
140
|
+
* @param event Netlify 事件
|
|
141
|
+
* @returns Netlify 返回体
|
|
142
|
+
*/
|
|
143
|
+
async function handler(event) {
|
|
144
|
+
handlerFn ??= createNetlifyFunc();
|
|
145
|
+
return handlerFn(event);
|
|
146
|
+
}
|
|
147
|
+
//#endregion
|
|
148
|
+
export { createNetlifyFunc, fromTkResponse, handler, netlifyPostSubmitDispatcher, toTkRequest };
|
|
149
|
+
|
|
150
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/dispatch.ts","../src/main.ts"],"sourcesContent":["/**\n * Netlify 的 POST_SUBMIT 派发实现(规范「后置副作用异步语义」)。\n *\n * **与 1.x 的差异(修正)**:1.x 的 Netlify 适配器是 `require('twikoo-vercel')`\n * 纯转发壳,于是继承了 vercel 的 HTTP 递归——而 vercel 取的是 `VERCEL_URL`,\n * 在 Netlify 上并不存在,自调用实际发不出去(副作用静默丢失)。2.0 独立实现,\n * 改用 Netlify 运行时可用的 `URL`(见下)。\n *\n * **平台事实**(docs.netlify.com「Environment variables and serverless functions」,\n * 查阅 2026-09-18):Netlify Functions **运行时**可用的只读变量只有 `URL` /\n * `SITE_NAME` / `SITE_ID`;`DEPLOY_URL` / `DEPLOY_PRIME_URL` 仅构建期可用。\n * 函数调用路径为 `/.netlify/functions/<函数名>`。\n *\n * 副作用链本身仍在 common 的 postSubmit 服务里。\n */\nimport {\n RECURSION_HEADER,\n getAxios,\n getRecursionToken,\n type PostSubmitDispatcher,\n} from \"@twikoojs/common\";\n\n/** 派发等待上限(毫秒),语义同 Vercel 实现(超时只表示本请求不再等) */\nconst DISPATCH_TIMEOUT_MS = 5000;\n\n/** Netlify Functions 的标准调用路径前缀(函数名为 twikoo) */\nconst FUNCTION_PATH = \"/.netlify/functions/twikoo\";\n\n/**\n * 解析自身函数地址:`TWIKOO_SELF_URL`(完整地址,显式覆写)→\n * `${URL}/.netlify/functions/twikoo`(运行时只读变量)。\n * @returns 绝对地址;URL 缺失时为空串\n */\nfunction resolveSelfUrl(): string {\n const explicit = process.env.TWIKOO_SELF_URL;\n if (explicit) return explicit;\n const siteUrl = process.env.URL;\n return siteUrl ? `${siteUrl.replace(/\\/$/, \"\")}${FUNCTION_PATH}` : \"\";\n}\n\n/** Netlify 的 POST_SUBMIT 派发实现(HTTP 递归自调用) */\nexport const netlifyPostSubmitDispatcher: PostSubmitDispatcher = {\n /**\n * 自调用本函数执行 POST_SUBMIT(不等待副作用完成,只等有界竞速)。\n * @param comment 已入库的评论\n * @param ctx 当前请求上下文(用于取递归令牌与记录日志)\n */\n async dispatch(comment, ctx): Promise<void> {\n const url = resolveSelfUrl();\n if (!url) {\n ctx.logger.warn(\"POST_SUBMIT 派发跳过:未取到自身地址(TWIKOO_SELF_URL / URL)\");\n return;\n }\n const axios = await getAxios();\n await Promise.race([\n axios.post(\n url,\n { event: \"POST_SUBMIT\", comment },\n { headers: { [RECURSION_HEADER]: getRecursionToken(ctx.config) } },\n ),\n new Promise((resolve) => setTimeout(resolve, DISPATCH_TIMEOUT_MS)),\n ]);\n },\n};\n","/**\n * twikoo-netlify 主逻辑(Netlify 独立薄适配器——不再依赖 twikoo-vercel)。\n * 业务逻辑全部在 @twikoojs/common;数据库 MONGODB_URI→Mongo。\n * 平台核对:Functions v1 handler = async (event, context) =>\n * { statusCode, headers, body(string) };IP 头 x-nf-client-connection-ip——\n * docs.netlify.com/functions(查阅 2026-09-17)。1.x 的 TWIKOO_IP_HEADERS\n * 环境变量机制由本适配器消化(公共库不感知该变量)。\n *\n * 后置副作用(垃圾检测 + 通知)经 {@link netlifyPostSubmitDispatcher} 以\n * HTTP 递归自调用派发到独立执行单元,见 `./dispatch.ts`。\n */\nimport {\n FULL_CAPABILITIES,\n MongoDatabase,\n createHandler,\n scaffoldAdapters,\n type Database,\n type TkRequest,\n type TkResponse,\n} from \"@twikoojs/common\";\nimport { netlifyPostSubmitDispatcher } from \"./dispatch\";\n\n/** Netlify 平台能力:全能力*/\nconst netlifyCapabilities = FULL_CAPABILITIES;\n\n/** Netlify Functions v1 事件的最小结构面 */\nexport interface NetlifyEventLike {\n httpMethod: string;\n headers: Record<string, string | string[] | undefined>;\n /** 请求体(Netlify 交付为字符串) */\n body: string | null;\n rawUrl?: string;\n}\n\n/** Netlify 返回体(v1 形态:body 必须为字符串) */\nexport interface NetlifyResult {\n statusCode: number;\n headers: Record<string, string>;\n /** 返回体(isBase64Encoded 缺省 false,JSON 直接字符串化) */\n body: string;\n}\n\n/**\n * Netlify 事件 → 内部统一请求(IP 优先 x-nf-client-connection-ip,1.x 对齐)。\n * @param event Netlify 事件\n * @returns 内部统一请求\n */\nexport function toTkRequest(event: NetlifyEventLike): TkRequest {\n const headers: Record<string, string> = {};\n for (const [key, value] of Object.entries(event.headers ?? {})) {\n if (value !== undefined) headers[key.toLowerCase()] = Array.isArray(value) ? value[0] : value;\n }\n const forwarded = headers[\"x-forwarded-for\"];\n const ip =\n headers[\"x-nf-client-connection-ip\"] ??\n headers[\"x-real-ip\"] ??\n (forwarded ? forwarded.split(\",\")[0].trim() : \"\") ??\n \"\";\n let body = {} as TkRequest[\"body\"];\n try {\n const parsed = event.body ? JSON.parse(event.body) : {};\n if (parsed && typeof parsed === \"object\") body = parsed as TkRequest[\"body\"];\n } catch {\n // 非法 JSON 按空体处理(1.x try/catch 对齐)\n }\n return {\n method: event.httpMethod.toUpperCase(),\n path: \"/\",\n query: {},\n body,\n headers,\n ip,\n raw: event,\n };\n}\n\n/** 内部统一响应 → Netlify 返回体(204 无体;业务体字符串化)。 */\nexport function fromTkResponse(tkRes: TkResponse): NetlifyResult {\n if (tkRes.status === 204) {\n return { statusCode: 204, headers: {}, body: \"\" };\n }\n return {\n statusCode: tkRes.status,\n headers: { ...tkRes.headers, \"Content-Type\": \"application/json\" },\n body: JSON.stringify(tkRes.body),\n };\n}\n\n/**\n * 创建 Netlify 请求处理器(database 可注入;缺省按 MONGODB_URI 懒建连)。\n * @param options 注入项(database:Database 实现;mongoUri:连接串覆写)\n * @returns 逐事件处理器\n */\nexport function createNetlifyFunc(\n options: { database?: Database; mongoUri?: string } = {},\n): (event: NetlifyEventLike) => Promise<NetlifyResult> {\n let database: Database | null = options.database ?? null;\n /** 懒建连(inject 优先;init 幂等) */\n const getDatabase = async (): Promise<Database> => {\n database ??= new MongoDatabase({ uri: options.mongoUri ?? process.env.MONGODB_URI ?? \"\" });\n await database.init();\n return database;\n };\n return async (event) => {\n const request = toTkRequest(event);\n const handler = createHandler(\n scaffoldAdapters({\n request: {\n /** 请求恒等透传(归一化见 toTkRequest) */\n toTkRequest: () => request,\n },\n response: {\n /** TkResponse 恒等透传(序列化见 fromTkResponse) */\n fromTkResponse: (r: TkResponse) => r,\n },\n database: await getDatabase(),\n capabilities: netlifyCapabilities,\n postSubmit: netlifyPostSubmitDispatcher,\n }),\n );\n return fromTkResponse(await handler(request));\n };\n}\n\n/** 装配缓存(handler 懒加载语义) */\nlet handlerFn: ((event: NetlifyEventLike) => Promise<NetlifyResult>) | null = null;\n\n/**\n * Netlify Function 入口(exports.handler 形态,v1 CJS)。\n * @param event Netlify 事件\n * @returns Netlify 返回体\n */\nexport async function handler(event: NetlifyEventLike): Promise<NetlifyResult> {\n handlerFn ??= createNetlifyFunc();\n return handlerFn(event);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAuBA,MAAM,sBAAsB;;AAG5B,MAAM,gBAAgB;;;;;;AAOtB,SAAS,iBAAyB;CAChC,MAAM,WAAW,QAAQ,IAAI;CAC7B,IAAI,UAAU,OAAO;CACrB,MAAM,UAAU,QAAQ,IAAI;CAC5B,OAAO,UAAU,GAAG,QAAQ,QAAQ,OAAO,EAAE,IAAI,kBAAkB;AACrE;;AAGA,MAAa,8BAAoD;;;;;;AAM/D,MAAM,SAAS,SAAS,KAAoB;CAC1C,MAAM,MAAM,eAAe;CAC3B,IAAI,CAAC,KAAK;EACR,IAAI,OAAO,KAAK,iDAAiD;EACjE;CACF;CACA,MAAM,QAAQ,MAAM,SAAS;CAC7B,MAAM,QAAQ,KAAK,CACjB,MAAM,KACJ,KACA;EAAE,OAAO;EAAe;CAAQ,GAChC,EAAE,SAAS,GAAG,mBAAmB,kBAAkB,IAAI,MAAM,EAAE,EAAE,CACnE,GACA,IAAI,SAAS,YAAY,WAAW,SAAS,mBAAmB,CAAC,CACnE,CAAC;AACH,EACF;;;;;;;;;;;;;;;ACxCA,MAAM,sBAAsB;;;;;;AAwB5B,SAAgB,YAAY,OAAoC;CAC9D,MAAM,UAAkC,CAAC;CACzC,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,WAAW,CAAC,CAAC,GAC3D,IAAI,UAAU,KAAA,GAAW,QAAQ,IAAI,YAAY,KAAK,MAAM,QAAQ,KAAK,IAAI,MAAM,KAAK;CAE1F,MAAM,YAAY,QAAQ;CAC1B,MAAM,KACJ,QAAQ,gCACR,QAAQ,iBACP,YAAY,UAAU,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC,KAAK,IAAI,OAC9C;CACF,IAAI,OAAO,CAAC;CACZ,IAAI;EACF,MAAM,SAAS,MAAM,OAAO,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC;EACtD,IAAI,UAAU,OAAO,WAAW,UAAU,OAAO;CACnD,QAAQ,CAER;CACA,OAAO;EACL,QAAQ,MAAM,WAAW,YAAY;EACrC,MAAM;EACN,OAAO,CAAC;EACR;EACA;EACA;EACA,KAAK;CACP;AACF;;AAGA,SAAgB,eAAe,OAAkC;CAC/D,IAAI,MAAM,WAAW,KACnB,OAAO;EAAE,YAAY;EAAK,SAAS,CAAC;EAAG,MAAM;CAAG;CAElD,OAAO;EACL,YAAY,MAAM;EAClB,SAAS;GAAE,GAAG,MAAM;GAAS,gBAAgB;EAAmB;EAChE,MAAM,KAAK,UAAU,MAAM,IAAI;CACjC;AACF;;;;;;AAOA,SAAgB,kBACd,UAAsD,CAAC,GACF;CACrD,IAAI,WAA4B,QAAQ,YAAY;;CAEpD,MAAM,cAAc,YAA+B;EACjD,aAAa,IAAI,cAAc,EAAE,KAAK,QAAQ,YAAY,QAAQ,IAAI,eAAe,GAAG,CAAC;EACzF,MAAM,SAAS,KAAK;EACpB,OAAO;CACT;CACA,OAAO,OAAO,UAAU;EACtB,MAAM,UAAU,YAAY,KAAK;EAgBjC,OAAO,eAAe,MAfN,cACd,iBAAiB;GACf,SAAS;;AAEP,mBAAmB,QACrB;GACA,UAAU;;AAER,iBAAiB,MAAkB,EACrC;GACA,UAAU,MAAM,YAAY;GAC5B,cAAc;GACd,YAAY;EACd,CAAC,CAE+B,CAAC,CAAC,OAAO,CAAC;CAC9C;AACF;;AAGA,IAAI,YAA0E;;;;;;AAO9E,eAAsB,QAAQ,OAAiD;CAC7E,cAAc,kBAAkB;CAChC,OAAO,UAAU,KAAK;AACxB"}
|
package/package.json
CHANGED
|
@@ -1,16 +1,51 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "twikoo-netlify",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
5
|
-
"author": "imaegoo <hello@imaegoo.com> (https://github.com/imaegoo)",
|
|
3
|
+
"version": "2.0.0-beta.1",
|
|
4
|
+
"description": "Twikoo comment system backend for Netlify",
|
|
6
5
|
"license": "MIT",
|
|
7
|
-
"main": "netlify/functions/twikoo.js",
|
|
8
6
|
"repository": {
|
|
9
7
|
"type": "git",
|
|
10
8
|
"url": "https://github.com/twikoojs/twikoo.git"
|
|
11
9
|
},
|
|
12
|
-
"
|
|
10
|
+
"main": "./dist/index.js",
|
|
11
|
+
"module": "./dist/index.mjs",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.mjs",
|
|
17
|
+
"require": "./dist/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsdown",
|
|
25
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
26
|
+
"test": "vitest run"
|
|
27
|
+
},
|
|
13
28
|
"dependencies": {
|
|
14
|
-
"
|
|
29
|
+
"@cap.js/server": "^4.0.5",
|
|
30
|
+
"@imaegoo/node-ip2region": "^2.1.1",
|
|
31
|
+
"@twikoojs/common": "workspace:*",
|
|
32
|
+
"@twikoojs/shared": "workspace:*",
|
|
33
|
+
"@xsai/generate-text": "^0.5.0",
|
|
34
|
+
"akismet-api": "^6.0.0",
|
|
35
|
+
"axios": "^1.20.0",
|
|
36
|
+
"bowser": "^2.14.1",
|
|
37
|
+
"dompurify": "^3.4.15",
|
|
38
|
+
"form-data": "^4.0.6",
|
|
39
|
+
"html-to-text": "^10.0.1",
|
|
40
|
+
"jsdom": "^30.1.0",
|
|
41
|
+
"marked": "^18.0.13",
|
|
42
|
+
"mongodb": "^7.6.0",
|
|
43
|
+
"nodemailer": "^10.0.10",
|
|
44
|
+
"pushoo": "workspace:^",
|
|
45
|
+
"tencentcloud-sdk-nodejs-tms": "^4.1.311",
|
|
46
|
+
"xml2js": "^0.6.2"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/node": "^26.6.1"
|
|
15
50
|
}
|
|
16
51
|
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
const twikoo = require('twikoo-vercel')
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Netlify 函数兼容 Vercel 函数实现
|
|
5
|
-
* 复用 Twikoo Vercel 函数代码
|
|
6
|
-
* Netlify functions doc:
|
|
7
|
-
* https://docs.netlify.com/functions/create/?fn-language=js
|
|
8
|
-
*/
|
|
9
|
-
exports.handler = async function (event, context) {
|
|
10
|
-
process.env.VERCEL_URL = event.rawUrl.replace(/^https?:\/\//, '')
|
|
11
|
-
process.env.TWIKOO_IP_HEADERS = JSON.stringify([
|
|
12
|
-
'headers.x-nf-client-connection-ip'
|
|
13
|
-
])
|
|
14
|
-
const result = {
|
|
15
|
-
statusCode: 204,
|
|
16
|
-
headers: {},
|
|
17
|
-
body: ''
|
|
18
|
-
}
|
|
19
|
-
const request = {
|
|
20
|
-
method: event.httpMethod,
|
|
21
|
-
headers: event.headers,
|
|
22
|
-
body: {}
|
|
23
|
-
}
|
|
24
|
-
try {
|
|
25
|
-
request.body = JSON.parse(event.body)
|
|
26
|
-
} catch (e) {}
|
|
27
|
-
const response = {
|
|
28
|
-
status: function (code) {
|
|
29
|
-
result.statusCode = code
|
|
30
|
-
return this
|
|
31
|
-
},
|
|
32
|
-
json: function (json) {
|
|
33
|
-
result.headers['Content-Type'] = 'application/json'
|
|
34
|
-
result.body = JSON.stringify(json)
|
|
35
|
-
return this
|
|
36
|
-
},
|
|
37
|
-
end: function () {
|
|
38
|
-
return this
|
|
39
|
-
},
|
|
40
|
-
setHeader: function (k, v) {
|
|
41
|
-
result.headers[k] = v
|
|
42
|
-
return this
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
await twikoo(request, response)
|
|
46
|
-
return result
|
|
47
|
-
}
|