twikoo-vercel 2.0.0-beta.3 → 2.0.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -14,6 +14,10 @@ interface VercelResponseLike {
14
14
  setHeader(name: string, value: string): VercelResponseLike;
15
15
  json(body: unknown): VercelResponseLike;
16
16
  end(): VercelResponseLike;
17
+ /** Node 响应头已发出(异常兜底时避免二次写入) */
18
+ headersSent?: boolean;
19
+ /** Node 响应已结束(异常兜底时避免二次写入) */
20
+ writableEnded?: boolean;
17
21
  }
18
22
  /**
19
23
  * Vercel 请求 → 内部统一请求(headers 小写化;query String 化;x-real-ip/转发首跳取 IP)。
package/dist/index.d.ts CHANGED
@@ -14,6 +14,10 @@ interface VercelResponseLike {
14
14
  setHeader(name: string, value: string): VercelResponseLike;
15
15
  json(body: unknown): VercelResponseLike;
16
16
  end(): VercelResponseLike;
17
+ /** Node 响应头已发出(异常兜底时避免二次写入) */
18
+ headersSent?: boolean;
19
+ /** Node 响应已结束(异常兜底时避免二次写入) */
20
+ writableEnded?: boolean;
17
21
  }
18
22
  /**
19
23
  * Vercel 请求 → 内部统一请求(headers 小写化;query String 化;x-real-ip/转发首跳取 IP)。
package/dist/index.js CHANGED
@@ -61,6 +61,9 @@ async dispatch(comment, ctx) {
61
61
  *
62
62
  * 后置副作用(垃圾检测 + 通知)经 {@link vercelPostSubmitDispatcher} 以
63
63
  * HTTP 递归自调用派发到独立执行单元,见 `./dispatch.ts`。
64
+ *
65
+ * **异常不外抛**(1.x 语义):请求处理中的任何异常都转成 HTTP 200 + `code: 1000`。
66
+ * 一旦抛给平台,Vercel 会回 500,前端只能看到 FUNCTION_INVOCATION_FAILED。
64
67
  */
65
68
  /** Vercel 平台能力:全能力(能力矩阵)*/
66
69
  const vercelCapabilities = _twikoojs_common.FULL_CAPABILITIES;
@@ -104,25 +107,35 @@ function fromTkResponse(res, tkRes) {
104
107
  */
105
108
  function createVercelFunc(options = {}) {
106
109
  let database = options.database ?? null;
107
- /** 懒建连(inject 优先;init 幂等) */
108
- const getDatabase = async () => {
110
+ /**
111
+ * 懒建数据库实例(inject 优先)。**不在这里 init**:pipeline 自己会 init 且在同一处 catch 内,
112
+ * 提前 init 会让「连不上数据库」在 pipeline 之外抛错(1.x 连不上也是 200 + code 1000)。
113
+ */
114
+ const getDatabase = () => {
109
115
  database ??= new _twikoojs_common.MongoDatabase({ uri: options.mongoUri ?? process.env.MONGODB_URI ?? "" });
110
- await database.init();
111
116
  return database;
112
117
  };
113
118
  return async (req, res) => {
114
- const request = toTkRequest(req);
115
- fromTkResponse(res, await (0, _twikoojs_common.createHandler)((0, _twikoojs_common.scaffoldAdapters)({
116
- request: {
117
- /** 事件即请求体(闭包透传,见 toTkRequest) */
119
+ try {
120
+ const request = toTkRequest(req);
121
+ fromTkResponse(res, await (0, _twikoojs_common.createHandler)((0, _twikoojs_common.scaffoldAdapters)({
122
+ request: {
123
+ /** 事件即请求体(闭包透传,见 toTkRequest) */
118
124
  toTkRequest: () => request },
119
- response: {
120
- /** TkResponse 恒等透传(平台写入见 fromTkResponse) */
125
+ response: {
126
+ /** TkResponse 恒等透传(平台写入见 fromTkResponse) */
121
127
  fromTkResponse: (r) => r },
122
- database: await getDatabase(),
123
- capabilities: vercelCapabilities,
124
- postSubmit: vercelPostSubmitDispatcher
125
- }))(request));
128
+ database: getDatabase(),
129
+ capabilities: vercelCapabilities,
130
+ postSubmit: vercelPostSubmitDispatcher
131
+ }))(request));
132
+ } catch (e) {
133
+ if (res.headersSent || res.writableEnded) return;
134
+ res.status(200).json({
135
+ code: _twikoojs_common.RES_CODE.FAIL,
136
+ message: e instanceof Error ? e.message : String(e)
137
+ });
138
+ }
126
139
  };
127
140
  }
128
141
  /** 装配缓存(default handler 懒加载语义) */
package/dist/index.js.map CHANGED
@@ -1 +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 * Vercel 的 POST_SUBMIT 派发实现(规范「后置副作用异步语义」)。\n *\n * **1.x 语义对齐**:COMMENT_SUBMIT 保存评论后,用 **HTTP 递归自调用**把\n * POST_SUBMIT 送到另一个函数实例执行——垃圾检测与邮件/IM 通知因此拥有\n * 独立的执行时间预算,既不会拖慢用户请求,也不会因为用户请求的超时上限\n * 而整条失败(1.x 实现见 `src/server/vercel/api/index.js` 的 `commentSubmit`)。\n *\n * **为什么放在适配器**:这是平台专有机制,common 不应感知。平台相关部分只有\n * 两处:\n * - 自身地址:`TWIKOO_SELF_URL`(显式覆写)→ `VERCEL_URL`(平台注入);\n * - 内部派发令牌:走 `x-twikoo-recursion` 请求头(1.x 同名,跨版本可互认)。\n *\n * 副作用链本身仍在 common 的 postSubmit 服务里。\n */\nimport {\n RECURSION_HEADER,\n getAxios,\n getRecursionToken,\n type PostSubmitDispatcher,\n} from \"@twikoojs/common\";\n\n/**\n * 派发等待上限(毫秒)。1.x 为 `Promise.race` 5 秒:超时只表示「本请求不再\n * 继续等」,被调用的实例仍在自己的预算内跑完副作用。\n */\nconst DISPATCH_TIMEOUT_MS = 5000;\n\n/**\n * 解析自身函数地址(显式覆写优先,其次平台注入的 VERCEL_URL)。\n * @returns 绝对地址;两者皆缺失时为空串\n */\nfunction resolveSelfUrl(): string {\n const explicit = process.env.TWIKOO_SELF_URL;\n if (explicit) return explicit;\n const vercelUrl = process.env.VERCEL_URL;\n return vercelUrl ? `https://${vercelUrl}` : \"\";\n}\n\n/** Vercel 的 POST_SUBMIT 派发实现(HTTP 递归自调用) */\nexport const vercelPostSubmitDispatcher: 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 / VERCEL_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-vercel 主逻辑(Vercel 薄适配器,规范)。\n * 业务逻辑全部在 @twikoojs/common;平台核对:vercel.com/docs/functions(查阅 2026-09-17)。\n *\n * 后置副作用(垃圾检测 + 通知)经 {@link vercelPostSubmitDispatcher} 以\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 { vercelPostSubmitDispatcher } from \"./dispatch\";\n\n/** Vercel 平台能力:全能力(能力矩阵)*/\nconst vercelCapabilities = FULL_CAPABILITIES;\n\n/** Vercel 请求的最小结构面(Node IncomingMessage + Vercel 扩展) */\nexport interface VercelRequestLike {\n method?: string;\n headers: Record<string, string | string[] | undefined>;\n query?: Record<string, string | string[] | undefined>;\n body?: unknown;\n url?: string;\n}\n\n/** Vercel 响应的最小结构面(Node ServerResponse) */\nexport interface VercelResponseLike {\n status(code: number): VercelResponseLike;\n setHeader(name: string, value: string): VercelResponseLike;\n json(body: unknown): VercelResponseLike;\n end(): VercelResponseLike;\n}\n\n/**\n * Vercel 请求 → 内部统一请求(headers 小写化;query String 化;x-real-ip/转发首跳取 IP)。\n * @param req Vercel/Node 请求\n * @returns 内部统一请求\n */\nexport function toTkRequest(req: VercelRequestLike): TkRequest {\n const headers: Record<string, string> = {};\n for (const [key, value] of Object.entries(req.headers ?? {})) {\n if (value !== undefined) headers[key.toLowerCase()] = Array.isArray(value) ? value[0] : value;\n }\n const query: Record<string, string> = {};\n for (const [key, value] of Object.entries(req.query ?? {})) {\n query[key] = Array.isArray(value) ? String(value[0]) : String(value ?? \"\");\n }\n const forwarded = headers[\"x-forwarded-for\"];\n const ip = headers[\"x-real-ip\"] ?? (forwarded ? forwarded.split(\",\")[0].trim() : \"\") ?? \"\";\n const body = (req.body && typeof req.body === \"object\" ? req.body : {}) as TkRequest[\"body\"];\n return {\n method: String(req.method ?? \"POST\").toUpperCase(),\n path: String(req.url ?? \"/\"),\n query,\n body,\n headers,\n ip,\n raw: req,\n };\n}\n\n/** 内部统一响应 → Vercel 响应(状态码 + CORS 头 + JSON 体;204 无体)。 */\nexport function fromTkResponse(res: VercelResponseLike, tkRes: TkResponse): void {\n res.status(tkRes.status);\n for (const [name, value] of Object.entries(tkRes.headers)) {\n res.setHeader(name, value);\n }\n if (tkRes.status === 204) {\n res.end();\n return;\n }\n res.json(tkRes.body);\n}\n\n/**\n * 创建 Vercel 请求处理器(database 可注入;缺省按 MONGODB_URI 懒建连,1.x 缓存语义)。\n * @param options 注入项(database:Database 实现;mongoUri:连接串覆写)\n * @returns 逐请求处理器 `(req, res) => Promise<void>`\n */\nexport function createVercelFunc(\n options: { database?: Database; mongoUri?: string } = {},\n): (req: VercelRequestLike, res: VercelResponseLike) => Promise<void> {\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 (req, res) => {\n const request = toTkRequest(req);\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: vercelCapabilities,\n postSubmit: vercelPostSubmitDispatcher,\n }),\n );\n fromTkResponse(res, await handler(request));\n };\n}\n\n/** 装配缓存(default handler 懒加载语义) */\nlet handlerFn: ((req: VercelRequestLike, res: VercelResponseLike) => Promise<void>) | null = null;\n\n/**\n * Vercel Serverless Function 默认入口。\n * @param req Vercel/Node 请求\n * @param res Vercel/Node 响应\n */\nexport default async function vercelHandler(\n req: VercelRequestLike,\n res: VercelResponseLike,\n): Promise<void> {\n handlerFn ??= createVercelFunc();\n await handlerFn(req, res);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["getAxios","RECURSION_HEADER","getRecursionToken","FULL_CAPABILITIES","MongoDatabase","createHandler","scaffoldAdapters","RES_CODE"],"sources":["../src/dispatch.ts","../src/main.ts"],"sourcesContent":["/**\n * Vercel 的 POST_SUBMIT 派发实现(规范「后置副作用异步语义」)。\n *\n * **1.x 语义对齐**:COMMENT_SUBMIT 保存评论后,用 **HTTP 递归自调用**把\n * POST_SUBMIT 送到另一个函数实例执行——垃圾检测与邮件/IM 通知因此拥有\n * 独立的执行时间预算,既不会拖慢用户请求,也不会因为用户请求的超时上限\n * 而整条失败(1.x 实现见 `src/server/vercel/api/index.js` 的 `commentSubmit`)。\n *\n * **为什么放在适配器**:这是平台专有机制,common 不应感知。平台相关部分只有\n * 两处:\n * - 自身地址:`TWIKOO_SELF_URL`(显式覆写)→ `VERCEL_URL`(平台注入);\n * - 内部派发令牌:走 `x-twikoo-recursion` 请求头(1.x 同名,跨版本可互认)。\n *\n * 副作用链本身仍在 common 的 postSubmit 服务里。\n */\nimport {\n RECURSION_HEADER,\n getAxios,\n getRecursionToken,\n type PostSubmitDispatcher,\n} from \"@twikoojs/common\";\n\n/**\n * 派发等待上限(毫秒)。1.x 为 `Promise.race` 5 秒:超时只表示「本请求不再\n * 继续等」,被调用的实例仍在自己的预算内跑完副作用。\n */\nconst DISPATCH_TIMEOUT_MS = 5000;\n\n/**\n * 解析自身函数地址(显式覆写优先,其次平台注入的 VERCEL_URL)。\n * @returns 绝对地址;两者皆缺失时为空串\n */\nfunction resolveSelfUrl(): string {\n const explicit = process.env.TWIKOO_SELF_URL;\n if (explicit) return explicit;\n const vercelUrl = process.env.VERCEL_URL;\n return vercelUrl ? `https://${vercelUrl}` : \"\";\n}\n\n/** Vercel 的 POST_SUBMIT 派发实现(HTTP 递归自调用) */\nexport const vercelPostSubmitDispatcher: 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 / VERCEL_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-vercel 主逻辑(Vercel 薄适配器,规范)。\n * 业务逻辑全部在 @twikoojs/common;平台核对:vercel.com/docs/functions(查阅 2026-09-17)。\n *\n * 后置副作用(垃圾检测 + 通知)经 {@link vercelPostSubmitDispatcher} 以\n * HTTP 递归自调用派发到独立执行单元,见 `./dispatch.ts`。\n *\n * **异常不外抛**(1.x 语义):请求处理中的任何异常都转成 HTTP 200 + `code: 1000`。\n * 一旦抛给平台,Vercel 会回 500,前端只能看到 FUNCTION_INVOCATION_FAILED。\n */\nimport {\n FULL_CAPABILITIES,\n MongoDatabase,\n RES_CODE,\n createHandler,\n scaffoldAdapters,\n type Database,\n type TkRequest,\n type TkResponse,\n} from \"@twikoojs/common\";\nimport { vercelPostSubmitDispatcher } from \"./dispatch\";\n\n/** Vercel 平台能力:全能力(能力矩阵)*/\nconst vercelCapabilities = FULL_CAPABILITIES;\n\n/** Vercel 请求的最小结构面(Node IncomingMessage + Vercel 扩展) */\nexport interface VercelRequestLike {\n method?: string;\n headers: Record<string, string | string[] | undefined>;\n query?: Record<string, string | string[] | undefined>;\n body?: unknown;\n url?: string;\n}\n\n/** Vercel 响应的最小结构面(Node ServerResponse) */\nexport interface VercelResponseLike {\n status(code: number): VercelResponseLike;\n setHeader(name: string, value: string): VercelResponseLike;\n json(body: unknown): VercelResponseLike;\n end(): VercelResponseLike;\n /** Node 响应头已发出(异常兜底时避免二次写入) */\n headersSent?: boolean;\n /** Node 响应已结束(异常兜底时避免二次写入) */\n writableEnded?: boolean;\n}\n\n/**\n * Vercel 请求 → 内部统一请求(headers 小写化;query String 化;x-real-ip/转发首跳取 IP)。\n * @param req Vercel/Node 请求\n * @returns 内部统一请求\n */\nexport function toTkRequest(req: VercelRequestLike): TkRequest {\n const headers: Record<string, string> = {};\n for (const [key, value] of Object.entries(req.headers ?? {})) {\n if (value !== undefined) headers[key.toLowerCase()] = Array.isArray(value) ? value[0] : value;\n }\n const query: Record<string, string> = {};\n for (const [key, value] of Object.entries(req.query ?? {})) {\n query[key] = Array.isArray(value) ? String(value[0]) : String(value ?? \"\");\n }\n const forwarded = headers[\"x-forwarded-for\"];\n const ip = headers[\"x-real-ip\"] ?? (forwarded ? forwarded.split(\",\")[0].trim() : \"\") ?? \"\";\n const body = (req.body && typeof req.body === \"object\" ? req.body : {}) as TkRequest[\"body\"];\n return {\n method: String(req.method ?? \"POST\").toUpperCase(),\n path: String(req.url ?? \"/\"),\n query,\n body,\n headers,\n ip,\n raw: req,\n };\n}\n\n/** 内部统一响应 → Vercel 响应(状态码 + CORS 头 + JSON 体;204 无体)。 */\nexport function fromTkResponse(res: VercelResponseLike, tkRes: TkResponse): void {\n res.status(tkRes.status);\n for (const [name, value] of Object.entries(tkRes.headers)) {\n res.setHeader(name, value);\n }\n if (tkRes.status === 204) {\n res.end();\n return;\n }\n res.json(tkRes.body);\n}\n\n/**\n * 创建 Vercel 请求处理器(database 可注入;缺省按 MONGODB_URI 懒建连,1.x 缓存语义)。\n * @param options 注入项(database:Database 实现;mongoUri:连接串覆写)\n * @returns 逐请求处理器 `(req, res) => Promise<void>`\n */\nexport function createVercelFunc(\n options: { database?: Database; mongoUri?: string } = {},\n): (req: VercelRequestLike, res: VercelResponseLike) => Promise<void> {\n let database: Database | null = options.database ?? null;\n /**\n * 懒建数据库实例(inject 优先)。**不在这里 init**:pipeline 自己会 init 且在同一处 catch 内,\n * 提前 init 会让「连不上数据库」在 pipeline 之外抛错(1.x 连不上也是 200 + code 1000)。\n */\n const getDatabase = (): Database => {\n database ??= new MongoDatabase({ uri: options.mongoUri ?? process.env.MONGODB_URI ?? \"\" });\n return database;\n };\n return async (req, res) => {\n try {\n const request = toTkRequest(req);\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: getDatabase(),\n capabilities: vercelCapabilities,\n postSubmit: vercelPostSubmitDispatcher,\n }),\n );\n fromTkResponse(res, await handler(request));\n } catch (e) {\n // 1.x 语义:函数内异常不外抛 —— 抛出去 Vercel 直接回 500,前端只看到\n // FUNCTION_INVOCATION_FAILED,拿不到 code/message\n if (res.headersSent || res.writableEnded) return;\n res.status(200).json({\n code: RES_CODE.FAIL,\n message: e instanceof Error ? e.message : String(e),\n });\n }\n };\n}\n\n/** 装配缓存(default handler 懒加载语义) */\nlet handlerFn: ((req: VercelRequestLike, res: VercelResponseLike) => Promise<void>) | null = null;\n\n/**\n * Vercel Serverless Function 默认入口。\n * @param req Vercel/Node 请求\n * @param res Vercel/Node 响应\n */\nexport default async function vercelHandler(\n req: VercelRequestLike,\n res: VercelResponseLike,\n): Promise<void> {\n handlerFn ??= createVercelFunc();\n await handlerFn(req, res);\n}\n"],"mappings":"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"}
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { FULL_CAPABILITIES, MongoDatabase, RECURSION_HEADER, createHandler, getAxios, getRecursionToken, scaffoldAdapters } from "@twikoojs/common";
1
+ import { FULL_CAPABILITIES, MongoDatabase, RECURSION_HEADER, RES_CODE, createHandler, getAxios, getRecursionToken, scaffoldAdapters } from "@twikoojs/common";
2
2
  //#region src/dispatch.ts
3
3
  /**
4
4
  * Vercel 的 POST_SUBMIT 派发实现(规范「后置副作用异步语义」)。
@@ -57,6 +57,9 @@ async dispatch(comment, ctx) {
57
57
  *
58
58
  * 后置副作用(垃圾检测 + 通知)经 {@link vercelPostSubmitDispatcher} 以
59
59
  * HTTP 递归自调用派发到独立执行单元,见 `./dispatch.ts`。
60
+ *
61
+ * **异常不外抛**(1.x 语义):请求处理中的任何异常都转成 HTTP 200 + `code: 1000`。
62
+ * 一旦抛给平台,Vercel 会回 500,前端只能看到 FUNCTION_INVOCATION_FAILED。
60
63
  */
61
64
  /** Vercel 平台能力:全能力(能力矩阵)*/
62
65
  const vercelCapabilities = FULL_CAPABILITIES;
@@ -100,25 +103,35 @@ function fromTkResponse(res, tkRes) {
100
103
  */
101
104
  function createVercelFunc(options = {}) {
102
105
  let database = options.database ?? null;
103
- /** 懒建连(inject 优先;init 幂等) */
104
- const getDatabase = async () => {
106
+ /**
107
+ * 懒建数据库实例(inject 优先)。**不在这里 init**:pipeline 自己会 init 且在同一处 catch 内,
108
+ * 提前 init 会让「连不上数据库」在 pipeline 之外抛错(1.x 连不上也是 200 + code 1000)。
109
+ */
110
+ const getDatabase = () => {
105
111
  database ??= new MongoDatabase({ uri: options.mongoUri ?? process.env.MONGODB_URI ?? "" });
106
- await database.init();
107
112
  return database;
108
113
  };
109
114
  return async (req, res) => {
110
- const request = toTkRequest(req);
111
- fromTkResponse(res, await createHandler(scaffoldAdapters({
112
- request: {
113
- /** 事件即请求体(闭包透传,见 toTkRequest) */
115
+ try {
116
+ const request = toTkRequest(req);
117
+ fromTkResponse(res, await createHandler(scaffoldAdapters({
118
+ request: {
119
+ /** 事件即请求体(闭包透传,见 toTkRequest) */
114
120
  toTkRequest: () => request },
115
- response: {
116
- /** TkResponse 恒等透传(平台写入见 fromTkResponse) */
121
+ response: {
122
+ /** TkResponse 恒等透传(平台写入见 fromTkResponse) */
117
123
  fromTkResponse: (r) => r },
118
- database: await getDatabase(),
119
- capabilities: vercelCapabilities,
120
- postSubmit: vercelPostSubmitDispatcher
121
- }))(request));
124
+ database: getDatabase(),
125
+ capabilities: vercelCapabilities,
126
+ postSubmit: vercelPostSubmitDispatcher
127
+ }))(request));
128
+ } catch (e) {
129
+ if (res.headersSent || res.writableEnded) return;
130
+ res.status(200).json({
131
+ code: RES_CODE.FAIL,
132
+ message: e instanceof Error ? e.message : String(e)
133
+ });
134
+ }
122
135
  };
123
136
  }
124
137
  /** 装配缓存(default handler 懒加载语义) */
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/dispatch.ts","../src/main.ts"],"sourcesContent":["/**\n * Vercel 的 POST_SUBMIT 派发实现(规范「后置副作用异步语义」)。\n *\n * **1.x 语义对齐**:COMMENT_SUBMIT 保存评论后,用 **HTTP 递归自调用**把\n * POST_SUBMIT 送到另一个函数实例执行——垃圾检测与邮件/IM 通知因此拥有\n * 独立的执行时间预算,既不会拖慢用户请求,也不会因为用户请求的超时上限\n * 而整条失败(1.x 实现见 `src/server/vercel/api/index.js` 的 `commentSubmit`)。\n *\n * **为什么放在适配器**:这是平台专有机制,common 不应感知。平台相关部分只有\n * 两处:\n * - 自身地址:`TWIKOO_SELF_URL`(显式覆写)→ `VERCEL_URL`(平台注入);\n * - 内部派发令牌:走 `x-twikoo-recursion` 请求头(1.x 同名,跨版本可互认)。\n *\n * 副作用链本身仍在 common 的 postSubmit 服务里。\n */\nimport {\n RECURSION_HEADER,\n getAxios,\n getRecursionToken,\n type PostSubmitDispatcher,\n} from \"@twikoojs/common\";\n\n/**\n * 派发等待上限(毫秒)。1.x 为 `Promise.race` 5 秒:超时只表示「本请求不再\n * 继续等」,被调用的实例仍在自己的预算内跑完副作用。\n */\nconst DISPATCH_TIMEOUT_MS = 5000;\n\n/**\n * 解析自身函数地址(显式覆写优先,其次平台注入的 VERCEL_URL)。\n * @returns 绝对地址;两者皆缺失时为空串\n */\nfunction resolveSelfUrl(): string {\n const explicit = process.env.TWIKOO_SELF_URL;\n if (explicit) return explicit;\n const vercelUrl = process.env.VERCEL_URL;\n return vercelUrl ? `https://${vercelUrl}` : \"\";\n}\n\n/** Vercel 的 POST_SUBMIT 派发实现(HTTP 递归自调用) */\nexport const vercelPostSubmitDispatcher: 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 / VERCEL_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-vercel 主逻辑(Vercel 薄适配器,规范)。\n * 业务逻辑全部在 @twikoojs/common;平台核对:vercel.com/docs/functions(查阅 2026-09-17)。\n *\n * 后置副作用(垃圾检测 + 通知)经 {@link vercelPostSubmitDispatcher} 以\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 { vercelPostSubmitDispatcher } from \"./dispatch\";\n\n/** Vercel 平台能力:全能力(能力矩阵)*/\nconst vercelCapabilities = FULL_CAPABILITIES;\n\n/** Vercel 请求的最小结构面(Node IncomingMessage + Vercel 扩展) */\nexport interface VercelRequestLike {\n method?: string;\n headers: Record<string, string | string[] | undefined>;\n query?: Record<string, string | string[] | undefined>;\n body?: unknown;\n url?: string;\n}\n\n/** Vercel 响应的最小结构面(Node ServerResponse) */\nexport interface VercelResponseLike {\n status(code: number): VercelResponseLike;\n setHeader(name: string, value: string): VercelResponseLike;\n json(body: unknown): VercelResponseLike;\n end(): VercelResponseLike;\n}\n\n/**\n * Vercel 请求 → 内部统一请求(headers 小写化;query String 化;x-real-ip/转发首跳取 IP)。\n * @param req Vercel/Node 请求\n * @returns 内部统一请求\n */\nexport function toTkRequest(req: VercelRequestLike): TkRequest {\n const headers: Record<string, string> = {};\n for (const [key, value] of Object.entries(req.headers ?? {})) {\n if (value !== undefined) headers[key.toLowerCase()] = Array.isArray(value) ? value[0] : value;\n }\n const query: Record<string, string> = {};\n for (const [key, value] of Object.entries(req.query ?? {})) {\n query[key] = Array.isArray(value) ? String(value[0]) : String(value ?? \"\");\n }\n const forwarded = headers[\"x-forwarded-for\"];\n const ip = headers[\"x-real-ip\"] ?? (forwarded ? forwarded.split(\",\")[0].trim() : \"\") ?? \"\";\n const body = (req.body && typeof req.body === \"object\" ? req.body : {}) as TkRequest[\"body\"];\n return {\n method: String(req.method ?? \"POST\").toUpperCase(),\n path: String(req.url ?? \"/\"),\n query,\n body,\n headers,\n ip,\n raw: req,\n };\n}\n\n/** 内部统一响应 → Vercel 响应(状态码 + CORS 头 + JSON 体;204 无体)。 */\nexport function fromTkResponse(res: VercelResponseLike, tkRes: TkResponse): void {\n res.status(tkRes.status);\n for (const [name, value] of Object.entries(tkRes.headers)) {\n res.setHeader(name, value);\n }\n if (tkRes.status === 204) {\n res.end();\n return;\n }\n res.json(tkRes.body);\n}\n\n/**\n * 创建 Vercel 请求处理器(database 可注入;缺省按 MONGODB_URI 懒建连,1.x 缓存语义)。\n * @param options 注入项(database:Database 实现;mongoUri:连接串覆写)\n * @returns 逐请求处理器 `(req, res) => Promise<void>`\n */\nexport function createVercelFunc(\n options: { database?: Database; mongoUri?: string } = {},\n): (req: VercelRequestLike, res: VercelResponseLike) => Promise<void> {\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 (req, res) => {\n const request = toTkRequest(req);\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: vercelCapabilities,\n postSubmit: vercelPostSubmitDispatcher,\n }),\n );\n fromTkResponse(res, await handler(request));\n };\n}\n\n/** 装配缓存(default handler 懒加载语义) */\nlet handlerFn: ((req: VercelRequestLike, res: VercelResponseLike) => Promise<void>) | null = null;\n\n/**\n * Vercel Serverless Function 默认入口。\n * @param req Vercel/Node 请求\n * @param res Vercel/Node 响应\n */\nexport default async function vercelHandler(\n req: VercelRequestLike,\n res: VercelResponseLike,\n): Promise<void> {\n handlerFn ??= createVercelFunc();\n await handlerFn(req, res);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AA0BA,MAAM,sBAAsB;;;;;AAM5B,SAAS,iBAAyB;CAChC,MAAM,WAAW,QAAQ,IAAI;CAC7B,IAAI,UAAU,OAAO;CACrB,MAAM,YAAY,QAAQ,IAAI;CAC9B,OAAO,YAAY,WAAW,cAAc;AAC9C;;AAGA,MAAa,6BAAmD;;;;;;AAM9D,MAAM,SAAS,SAAS,KAAoB;CAC1C,MAAM,MAAM,eAAe;CAC3B,IAAI,CAAC,KAAK;EACR,IAAI,OAAO,KAAK,wDAAwD;EACxE;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;;;;;;;;;;;AC3CA,MAAM,qBAAqB;;;;;;AAwB3B,SAAgB,YAAY,KAAmC;CAC7D,MAAM,UAAkC,CAAC;CACzC,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,IAAI,WAAW,CAAC,CAAC,GACzD,IAAI,UAAU,KAAA,GAAW,QAAQ,IAAI,YAAY,KAAK,MAAM,QAAQ,KAAK,IAAI,MAAM,KAAK;CAE1F,MAAM,QAAgC,CAAC;CACvC,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,IAAI,SAAS,CAAC,CAAC,GACvD,MAAM,OAAO,MAAM,QAAQ,KAAK,IAAI,OAAO,MAAM,EAAE,IAAI,OAAO,SAAS,EAAE;CAE3E,MAAM,YAAY,QAAQ;CAC1B,MAAM,KAAK,QAAQ,iBAAiB,YAAY,UAAU,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC,KAAK,IAAI,OAAO;CACxF,MAAM,OAAQ,IAAI,QAAQ,OAAO,IAAI,SAAS,WAAW,IAAI,OAAO,CAAC;CACrE,OAAO;EACL,QAAQ,OAAO,IAAI,UAAU,MAAM,CAAC,CAAC,YAAY;EACjD,MAAM,OAAO,IAAI,OAAO,GAAG;EAC3B;EACA;EACA;EACA;EACA,KAAK;CACP;AACF;;AAGA,SAAgB,eAAe,KAAyB,OAAyB;CAC/E,IAAI,OAAO,MAAM,MAAM;CACvB,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,MAAM,OAAO,GACtD,IAAI,UAAU,MAAM,KAAK;CAE3B,IAAI,MAAM,WAAW,KAAK;EACxB,IAAI,IAAI;EACR;CACF;CACA,IAAI,KAAK,MAAM,IAAI;AACrB;;;;;;AAOA,SAAgB,iBACd,UAAsD,CAAC,GACa;CACpE,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,KAAK,QAAQ;EACzB,MAAM,UAAU,YAAY,GAAG;EAgB/B,eAAe,KAAK,MAfJ,cACd,iBAAiB;GACf,SAAS;;AAEP,mBAAmB,QACrB;GACA,UAAU;;AAER,iBAAiB,MAAkB,EACrC;GACA,UAAU,MAAM,YAAY;GAC5B,cAAc;GACd,YAAY;EACd,CAAC,CAE6B,CAAC,CAAC,OAAO,CAAC;CAC5C;AACF;;AAGA,IAAI,YAAyF;;;;;;AAO7F,eAA8B,cAC5B,KACA,KACe;CACf,cAAc,iBAAiB;CAC/B,MAAM,UAAU,KAAK,GAAG;AAC1B"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/dispatch.ts","../src/main.ts"],"sourcesContent":["/**\n * Vercel 的 POST_SUBMIT 派发实现(规范「后置副作用异步语义」)。\n *\n * **1.x 语义对齐**:COMMENT_SUBMIT 保存评论后,用 **HTTP 递归自调用**把\n * POST_SUBMIT 送到另一个函数实例执行——垃圾检测与邮件/IM 通知因此拥有\n * 独立的执行时间预算,既不会拖慢用户请求,也不会因为用户请求的超时上限\n * 而整条失败(1.x 实现见 `src/server/vercel/api/index.js` 的 `commentSubmit`)。\n *\n * **为什么放在适配器**:这是平台专有机制,common 不应感知。平台相关部分只有\n * 两处:\n * - 自身地址:`TWIKOO_SELF_URL`(显式覆写)→ `VERCEL_URL`(平台注入);\n * - 内部派发令牌:走 `x-twikoo-recursion` 请求头(1.x 同名,跨版本可互认)。\n *\n * 副作用链本身仍在 common 的 postSubmit 服务里。\n */\nimport {\n RECURSION_HEADER,\n getAxios,\n getRecursionToken,\n type PostSubmitDispatcher,\n} from \"@twikoojs/common\";\n\n/**\n * 派发等待上限(毫秒)。1.x 为 `Promise.race` 5 秒:超时只表示「本请求不再\n * 继续等」,被调用的实例仍在自己的预算内跑完副作用。\n */\nconst DISPATCH_TIMEOUT_MS = 5000;\n\n/**\n * 解析自身函数地址(显式覆写优先,其次平台注入的 VERCEL_URL)。\n * @returns 绝对地址;两者皆缺失时为空串\n */\nfunction resolveSelfUrl(): string {\n const explicit = process.env.TWIKOO_SELF_URL;\n if (explicit) return explicit;\n const vercelUrl = process.env.VERCEL_URL;\n return vercelUrl ? `https://${vercelUrl}` : \"\";\n}\n\n/** Vercel 的 POST_SUBMIT 派发实现(HTTP 递归自调用) */\nexport const vercelPostSubmitDispatcher: 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 / VERCEL_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-vercel 主逻辑(Vercel 薄适配器,规范)。\n * 业务逻辑全部在 @twikoojs/common;平台核对:vercel.com/docs/functions(查阅 2026-09-17)。\n *\n * 后置副作用(垃圾检测 + 通知)经 {@link vercelPostSubmitDispatcher} 以\n * HTTP 递归自调用派发到独立执行单元,见 `./dispatch.ts`。\n *\n * **异常不外抛**(1.x 语义):请求处理中的任何异常都转成 HTTP 200 + `code: 1000`。\n * 一旦抛给平台,Vercel 会回 500,前端只能看到 FUNCTION_INVOCATION_FAILED。\n */\nimport {\n FULL_CAPABILITIES,\n MongoDatabase,\n RES_CODE,\n createHandler,\n scaffoldAdapters,\n type Database,\n type TkRequest,\n type TkResponse,\n} from \"@twikoojs/common\";\nimport { vercelPostSubmitDispatcher } from \"./dispatch\";\n\n/** Vercel 平台能力:全能力(能力矩阵)*/\nconst vercelCapabilities = FULL_CAPABILITIES;\n\n/** Vercel 请求的最小结构面(Node IncomingMessage + Vercel 扩展) */\nexport interface VercelRequestLike {\n method?: string;\n headers: Record<string, string | string[] | undefined>;\n query?: Record<string, string | string[] | undefined>;\n body?: unknown;\n url?: string;\n}\n\n/** Vercel 响应的最小结构面(Node ServerResponse) */\nexport interface VercelResponseLike {\n status(code: number): VercelResponseLike;\n setHeader(name: string, value: string): VercelResponseLike;\n json(body: unknown): VercelResponseLike;\n end(): VercelResponseLike;\n /** Node 响应头已发出(异常兜底时避免二次写入) */\n headersSent?: boolean;\n /** Node 响应已结束(异常兜底时避免二次写入) */\n writableEnded?: boolean;\n}\n\n/**\n * Vercel 请求 → 内部统一请求(headers 小写化;query String 化;x-real-ip/转发首跳取 IP)。\n * @param req Vercel/Node 请求\n * @returns 内部统一请求\n */\nexport function toTkRequest(req: VercelRequestLike): TkRequest {\n const headers: Record<string, string> = {};\n for (const [key, value] of Object.entries(req.headers ?? {})) {\n if (value !== undefined) headers[key.toLowerCase()] = Array.isArray(value) ? value[0] : value;\n }\n const query: Record<string, string> = {};\n for (const [key, value] of Object.entries(req.query ?? {})) {\n query[key] = Array.isArray(value) ? String(value[0]) : String(value ?? \"\");\n }\n const forwarded = headers[\"x-forwarded-for\"];\n const ip = headers[\"x-real-ip\"] ?? (forwarded ? forwarded.split(\",\")[0].trim() : \"\") ?? \"\";\n const body = (req.body && typeof req.body === \"object\" ? req.body : {}) as TkRequest[\"body\"];\n return {\n method: String(req.method ?? \"POST\").toUpperCase(),\n path: String(req.url ?? \"/\"),\n query,\n body,\n headers,\n ip,\n raw: req,\n };\n}\n\n/** 内部统一响应 → Vercel 响应(状态码 + CORS 头 + JSON 体;204 无体)。 */\nexport function fromTkResponse(res: VercelResponseLike, tkRes: TkResponse): void {\n res.status(tkRes.status);\n for (const [name, value] of Object.entries(tkRes.headers)) {\n res.setHeader(name, value);\n }\n if (tkRes.status === 204) {\n res.end();\n return;\n }\n res.json(tkRes.body);\n}\n\n/**\n * 创建 Vercel 请求处理器(database 可注入;缺省按 MONGODB_URI 懒建连,1.x 缓存语义)。\n * @param options 注入项(database:Database 实现;mongoUri:连接串覆写)\n * @returns 逐请求处理器 `(req, res) => Promise<void>`\n */\nexport function createVercelFunc(\n options: { database?: Database; mongoUri?: string } = {},\n): (req: VercelRequestLike, res: VercelResponseLike) => Promise<void> {\n let database: Database | null = options.database ?? null;\n /**\n * 懒建数据库实例(inject 优先)。**不在这里 init**:pipeline 自己会 init 且在同一处 catch 内,\n * 提前 init 会让「连不上数据库」在 pipeline 之外抛错(1.x 连不上也是 200 + code 1000)。\n */\n const getDatabase = (): Database => {\n database ??= new MongoDatabase({ uri: options.mongoUri ?? process.env.MONGODB_URI ?? \"\" });\n return database;\n };\n return async (req, res) => {\n try {\n const request = toTkRequest(req);\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: getDatabase(),\n capabilities: vercelCapabilities,\n postSubmit: vercelPostSubmitDispatcher,\n }),\n );\n fromTkResponse(res, await handler(request));\n } catch (e) {\n // 1.x 语义:函数内异常不外抛 —— 抛出去 Vercel 直接回 500,前端只看到\n // FUNCTION_INVOCATION_FAILED,拿不到 code/message\n if (res.headersSent || res.writableEnded) return;\n res.status(200).json({\n code: RES_CODE.FAIL,\n message: e instanceof Error ? e.message : String(e),\n });\n }\n };\n}\n\n/** 装配缓存(default handler 懒加载语义) */\nlet handlerFn: ((req: VercelRequestLike, res: VercelResponseLike) => Promise<void>) | null = null;\n\n/**\n * Vercel Serverless Function 默认入口。\n * @param req Vercel/Node 请求\n * @param res Vercel/Node 响应\n */\nexport default async function vercelHandler(\n req: VercelRequestLike,\n res: VercelResponseLike,\n): Promise<void> {\n handlerFn ??= createVercelFunc();\n await handlerFn(req, res);\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "twikoo-vercel",
3
- "version": "2.0.0-beta.3",
3
+ "version": "2.0.0-beta.4",
4
4
  "description": "Twikoo comment system backend for Vercel",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -23,8 +23,8 @@
23
23
  "dependencies": {
24
24
  "@cap.js/server": "^4.0.5",
25
25
  "@imaegoo/node-ip2region": "^2.1.1",
26
- "@twikoojs/common": "2.0.0-beta.3",
27
- "@twikoojs/shared": "2.0.0-beta.3",
26
+ "@twikoojs/common": "2.0.0-beta.4",
27
+ "@twikoojs/shared": "2.0.0-beta.4",
28
28
  "@xsai/generate-text": "^0.5.0",
29
29
  "akismet-api": "^6.0.0",
30
30
  "axios": "^1.20.0",
@@ -36,7 +36,7 @@
36
36
  "marked": "^18.0.13",
37
37
  "mongodb": "^7.6.0",
38
38
  "nodemailer": "^10.0.10",
39
- "pushoo": "^2.0.0-beta.3",
39
+ "pushoo": "^2.0.0-beta.4",
40
40
  "tencentcloud-sdk-nodejs-tms": "^4.1.311",
41
41
  "xml2js": "^0.6.2"
42
42
  },