@vectorx/functions-framework 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/error.js CHANGED
@@ -30,18 +30,26 @@ function decorateErrorStack(err) {
30
30
  return err.stack.split("\n").join("\n");
31
31
  }
32
32
  function newSysErr(err) {
33
- return new AppError(ErrorCode.SYS_ERR, "System Error.", { cause: err });
33
+ return new AppError(ErrorCode.SYS_ERR, "System Error.", {
34
+ cause: err,
35
+ });
34
36
  }
35
37
  function newBadRequestErr(err) {
36
38
  const message = err instanceof Error ? err.message : err;
37
39
  return new AppError(ErrorCode.BAD_REQUEST, message, { cause: err });
38
40
  }
39
41
  function newTimeoutErr() {
40
- return new AppError(ErrorCode.TIMEOUT, "Request timeout", { cause: new Error("Request timeout") });
42
+ return new AppError(ErrorCode.TIMEOUT, "Request timeout", {
43
+ cause: new Error("Request timeout"),
44
+ });
41
45
  }
42
46
  function newTooManyRequestsErr(err) {
43
- return new AppError(ErrorCode.TOO_MANY_REQUESTS, "Too many requests", { cause: err });
47
+ return new AppError(ErrorCode.TOO_MANY_REQUESTS, "Too many requests", {
48
+ cause: err,
49
+ });
44
50
  }
45
51
  function newUnauthorizedErr(err) {
46
- return new AppError(ErrorCode.UNAUTHORIZED, "Unauthorized", { cause: err });
52
+ return new AppError(ErrorCode.UNAUTHORIZED, "Unauthorized", {
53
+ cause: err,
54
+ });
47
55
  }
package/lib/framework.js CHANGED
@@ -28,7 +28,9 @@ const apm_1 = require("./utils/apm");
28
28
  const console_intercept_1 = require("./utils/console-intercept");
29
29
  function getDependencyVersion(packageName, searchPath) {
30
30
  try {
31
- const packagePath = require.resolve(packageName, { paths: [searchPath] });
31
+ const packagePath = require.resolve(packageName, {
32
+ paths: [searchPath],
33
+ });
32
34
  let currentPath = path_1.default.dirname(packagePath);
33
35
  let packageJsonPath = path_1.default.join(currentPath, "package.json");
34
36
  while (!(0, fs_1.existsSync)(packageJsonPath) && currentPath !== path_1.default.parse(currentPath).root) {
@@ -85,10 +87,14 @@ class AgentServerFramework {
85
87
  const stageEnvFilePath = path_1.default.join(process.cwd(), `.env.${stage}`);
86
88
  let dotEnvContent;
87
89
  if ((0, fs_1.existsSync)(defaultEnvFilePath)) {
88
- dotEnvContent = dotenv_1.default.config({ path: path_1.default.resolve(defaultEnvFilePath) });
90
+ dotEnvContent = dotenv_1.default.config({
91
+ path: path_1.default.resolve(defaultEnvFilePath),
92
+ });
89
93
  }
90
94
  else if ((0, fs_1.existsSync)(stageEnvFilePath)) {
91
- dotEnvContent = dotenv_1.default.config({ path: path_1.default.resolve(stageEnvFilePath) });
95
+ dotEnvContent = dotenv_1.default.config({
96
+ path: path_1.default.resolve(stageEnvFilePath),
97
+ });
92
98
  }
93
99
  if (dotEnvContent && dotEnvContent.parsed) {
94
100
  const envKeys = Object.keys(dotEnvContent.parsed);
@@ -30,7 +30,10 @@ function loadFunctionsConfig(configPath) {
30
30
  throw new Error(`Route '${fn.triggerPath}->${fn.name}' conflict with '${fn.triggerPath}->${existsTriggerFnName}', please check your functions config`);
31
31
  }
32
32
  }
33
- config.routes.push({ functionName: fn.name, path: fn.triggerPath });
33
+ config.routes.push({
34
+ functionName: fn.name,
35
+ path: fn.triggerPath,
36
+ });
34
37
  routeMaps.set(fn.triggerPath, fn.name);
35
38
  }
36
39
  }
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.buildFnId = buildFnId;
4
4
  exports.registerFunction = registerFunction;
5
5
  exports.getRegisteredFunction = getRegisteredFunction;
6
+ exports.getAllRegisteredFunctions = getAllRegisteredFunctions;
6
7
  exports.clearRegistry = clearRegistry;
7
8
  const functionRegistry = new Map();
8
9
  function buildFnId(fnName, functionName) {
@@ -15,6 +16,13 @@ function registerFunction(fnName, fn) {
15
16
  function getRegisteredFunction(fnId) {
16
17
  return functionRegistry.get(fnId);
17
18
  }
19
+ function getAllRegisteredFunctions() {
20
+ const result = [];
21
+ functionRegistry.forEach((fn, fnId) => {
22
+ result.push({ name: fnId, userFunction: fn });
23
+ });
24
+ return result;
25
+ }
18
26
  function clearRegistry() {
19
27
  functionRegistry.clear();
20
28
  }
package/lib/index.js CHANGED
@@ -20,5 +20,6 @@ __exportStar(require("./function-loader"), exports);
20
20
  __exportStar(require("./function-registry"), exports);
21
21
  __exportStar(require("./sse"), exports);
22
22
  __exportStar(require("./server"), exports);
23
+ __exportStar(require("./wss"), exports);
23
24
  __exportStar(require("./logger"), exports);
24
25
  __exportStar(require("./async-context"), exports);
@@ -25,7 +25,7 @@ function loggerMiddleware() {
25
25
  yield next();
26
26
  return;
27
27
  }
28
- process.stdout.write(chalk_1.default.greenBright(`▶️ ${chalk_1.default.gray(`[${new Date().toLocaleString()} - ${ctx.method}:${eventId}]`)} ${ctx.url}`));
28
+ process.stdout.write(chalk_1.default.greenBright(`▶️ ${chalk_1.default.gray(`[${new Date().toLocaleString()} - ${ctx.method}:${eventId}]`)} ${ctx.url}\n`));
29
29
  try {
30
30
  const accessLog = {
31
31
  type: "HTTP_ACCESS",
package/lib/request.js CHANGED
@@ -78,7 +78,7 @@ class Request {
78
78
  }
79
79
  request(options_1) {
80
80
  return __awaiter(this, arguments, void 0, function* (options, enableAbort = false) {
81
- const { url, headers: _headers = {}, data, responseType, withCredentials, body, method: _method } = options, headers = Object.assign(Object.assign({}, this.defaultHeaders), (0, helper_1.obj2StrRecord)(_headers)), method = String(_method).toLowerCase() || "get";
81
+ const { url, headers: _headers = {}, data, responseType, withCredentials, body, method: _method, } = options, headers = Object.assign(Object.assign({}, this.defaultHeaders), (0, helper_1.obj2StrRecord)(_headers)), method = String(_method).toLowerCase() || "get";
82
82
  let queryParams = {};
83
83
  if (method === "get" || method === "head") {
84
84
  queryParams = Object.assign(Object.assign({}, (data || {})), (body && typeof body === "object" ? body : {}));
@@ -151,7 +151,11 @@ class Request {
151
151
  return Promise.reject(x);
152
152
  });
153
153
  const ret = {
154
- data: stream ? (res.body ? stream_1.Readable.toWeb(res.body) : res.body) : yield res.json(),
154
+ data: stream
155
+ ? res.body
156
+ ? stream_1.Readable.toWeb(res.body)
157
+ : res.body
158
+ : yield res.json(),
155
159
  statusCode: res.status,
156
160
  header: res.headers,
157
161
  };
package/lib/server.js CHANGED
@@ -17,8 +17,10 @@ const http_1 = __importDefault(require("http"));
17
17
  const koa_1 = __importDefault(require("koa"));
18
18
  const koa_body_1 = require("koa-body");
19
19
  const error_1 = require("./error");
20
+ const function_registry_1 = require("./function-registry");
20
21
  const middlewares_1 = require("./middlewares");
21
22
  const unified_responder_1 = require("./unified-responder");
23
+ const wss_1 = require("./wss");
22
24
  function corsMiddleware() {
23
25
  return (ctx, next) => __awaiter(this, void 0, void 0, function* () {
24
26
  ctx.set("Access-Control-Allow-Origin", "*");
@@ -35,6 +37,19 @@ function corsMiddleware() {
35
37
  function createServer(options, router, projectConfig) {
36
38
  const app = new koa_1.default();
37
39
  app.use(corsMiddleware());
40
+ app.use((ctx, next) => __awaiter(this, void 0, void 0, function* () {
41
+ if (ctx.path === "/@meta") {
42
+ const functions = (0, function_registry_1.getAllRegisteredFunctions)().map((fn) => ({
43
+ name: fn.name,
44
+ features: {
45
+ websocket: typeof fn.userFunction.handleUpgrade === "function",
46
+ },
47
+ }));
48
+ ctx.body = { functions };
49
+ return;
50
+ }
51
+ yield next();
52
+ }));
38
53
  app.use((0, middlewares_1.logsQueryMiddleware)());
39
54
  app.use((0, middlewares_1.contextInjectionMiddleware)(options, projectConfig));
40
55
  app.use((0, middlewares_1.envVarsInjectionMiddleware)());
@@ -77,5 +92,6 @@ function createServer(options, router, projectConfig) {
77
92
  app.use((0, middlewares_1.openGwRequestMiddleware)(openGwAgentId, { mode: options.mode }));
78
93
  app.use((0, middlewares_1.functionRouteMiddleware)(router, projectConfig));
79
94
  const httpServer = http_1.default.createServer(app.callback());
95
+ (0, wss_1.createWebSocketServer)(httpServer, app, router, options, projectConfig);
80
96
  return httpServer;
81
97
  }
package/lib/sse.js CHANGED
@@ -90,7 +90,9 @@ class ServerSentEvent extends events_1.EventEmitter {
90
90
  const openId = ctx.headers["open-id"] || undefined;
91
91
  const eventID = (_d = ctx.state) === null || _d === void 0 ? void 0 : _d.eventID;
92
92
  const agentInfo = ((_e = ctx.state) === null || _e === void 0 ? void 0 : _e.agentInfo) || {};
93
- const name = [(agentInfo === null || agentInfo === void 0 ? void 0 : agentInfo.agentName) || (agentInfo === null || agentInfo === void 0 ? void 0 : agentInfo.name), agentInfo === null || agentInfo === void 0 ? void 0 : agentInfo.version].filter(Boolean).join("@");
93
+ const name = [(agentInfo === null || agentInfo === void 0 ? void 0 : agentInfo.agentName) || (agentInfo === null || agentInfo === void 0 ? void 0 : agentInfo.name), agentInfo === null || agentInfo === void 0 ? void 0 : agentInfo.version]
94
+ .filter(Boolean)
95
+ .join("@");
94
96
  const metadata = {
95
97
  http: { method: ctx.method, path: ctx.path },
96
98
  eventID,
@@ -108,7 +110,11 @@ class ServerSentEvent extends events_1.EventEmitter {
108
110
  });
109
111
  if (trace) {
110
112
  ctx.state.langfuseTrace = trace;
111
- (_g = trace.event) === null || _g === void 0 ? void 0 : _g.call(trace, { name: "sse:start", input: (_j = (_h = ctx.state) === null || _h === void 0 ? void 0 : _h.event) !== null && _j !== void 0 ? _j : null, metadata: { path: ctx.path } });
113
+ (_g = trace.event) === null || _g === void 0 ? void 0 : _g.call(trace, {
114
+ name: "sse:start",
115
+ input: (_j = (_h = ctx.state) === null || _h === void 0 ? void 0 : _h.event) !== null && _j !== void 0 ? _j : null,
116
+ metadata: { path: ctx.path },
117
+ });
112
118
  }
113
119
  }
114
120
  catch (_k) { }
@@ -43,11 +43,10 @@ class Interceptor {
43
43
  anotherConsole[method] = (message, ...optionalParams) => {
44
44
  if ((0, async_context_1.isInFrameworkAsyncContext)()) {
45
45
  callAddUserCodeLog(method, message, ...optionalParams);
46
- process.stdout.write((0, util_1.formatWithOptions)({ colors: false }, message, ...optionalParams));
46
+ process.stdout.write(`${(0, util_1.formatWithOptions)({ colors: false }, message, ...optionalParams)}\n`);
47
47
  }
48
48
  else {
49
49
  originalConsole[method](message, ...optionalParams);
50
- process.stdout.write((0, util_1.formatWithOptions)({ colors: false }, message, ...optionalParams));
51
50
  }
52
51
  };
53
52
  }
@@ -13,13 +13,13 @@ exports.getDependencyVersion = getDependencyVersion;
13
13
  const fs_1 = __importDefault(require("fs"));
14
14
  const path_1 = __importDefault(require("path"));
15
15
  function deepFreeze(obj) {
16
- if (obj === null || typeof obj !== "object") {
16
+ if (!isPlainObject(obj)) {
17
17
  return obj;
18
18
  }
19
- Object.keys(obj).forEach((prop) => {
20
- const value = obj[prop];
21
- if (typeof value === "object" && value !== null) {
22
- deepFreeze(value);
19
+ Object.getOwnPropertyNames(obj).forEach((name) => {
20
+ const prop = obj[name];
21
+ if (typeof prop === "object" && prop !== null && !Object.isFrozen(prop)) {
22
+ deepFreeze(prop);
23
23
  }
24
24
  });
25
25
  return Object.freeze(obj);
@@ -69,7 +69,9 @@ function formatUrl(PROTOCOL, url, query) {
69
69
  }
70
70
  function getDependencyVersion(packageName, searchPath) {
71
71
  try {
72
- const packagePath = require.resolve(packageName, { paths: [searchPath] });
72
+ const packagePath = require.resolve(packageName, {
73
+ paths: [searchPath],
74
+ });
73
75
  let currentPath = path_1.default.dirname(packagePath);
74
76
  let packageJsonPath = path_1.default.join(currentPath, "package.json");
75
77
  while (!fs_1.default.existsSync(packageJsonPath) && currentPath !== path_1.default.parse(currentPath).root) {
package/lib/wss.js ADDED
@@ -0,0 +1,305 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
36
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
37
+ return new (P || (P = Promise))(function (resolve, reject) {
38
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
39
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
40
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
41
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
42
+ });
43
+ };
44
+ var __importDefault = (this && this.__importDefault) || function (mod) {
45
+ return (mod && mod.__esModule) ? mod : { "default": mod };
46
+ };
47
+ Object.defineProperty(exports, "__esModule", { value: true });
48
+ exports.VectorxWebSocket = void 0;
49
+ exports.createWebSocketServer = createWebSocketServer;
50
+ const events = __importStar(require("events"));
51
+ const http = __importStar(require("http"));
52
+ const koa_compose_1 = __importDefault(require("koa-compose"));
53
+ const ws_1 = require("ws");
54
+ const error_1 = require("./error");
55
+ const function_registry_1 = require("./function-registry");
56
+ const logger_1 = require("./logger");
57
+ const middle_async_context_1 = require("./middlewares/middle-async-context");
58
+ const middle_common_logger_1 = require("./middlewares/middle-common-logger");
59
+ const middle_context_injection_1 = require("./middlewares/middle-context-injection");
60
+ const middle_event_id_1 = require("./middlewares/middle-event-id");
61
+ const router_1 = require("./router");
62
+ const helper_1 = require("./utils/helper");
63
+ const WS_ERROR_CODE = {
64
+ SERVICE_RESTART: 1012,
65
+ };
66
+ function respondError(socket, e) {
67
+ respond(socket, 500, `Internal Server Error: ${e && e.message ? e.message : e}`);
68
+ }
69
+ function respond(socket, statusCode, body, type = "text/plain") {
70
+ const statusMessage = http.STATUS_CODES[statusCode];
71
+ const head = [
72
+ `HTTP/1.1 ${statusCode} ${statusMessage}`,
73
+ `Content-Type: ${type}`,
74
+ `Content-Length: ${Buffer.byteLength(Buffer.from(body))}`,
75
+ "Content-Encoding: utf-8",
76
+ "Connection: close",
77
+ ].join("\r\n");
78
+ socket.write(head);
79
+ socket.write("\r\n\r\n");
80
+ socket.end(body);
81
+ }
82
+ function setupWebSocketsHeartbeat(wss, interval = 10000) {
83
+ const SYM_WS_IS_ALIVE = Symbol("WS_IS_ALIVE");
84
+ let checking = false;
85
+ let wsNotAliveCount = 0;
86
+ function doCheck() {
87
+ if (checking)
88
+ return;
89
+ checking = true;
90
+ wsNotAliveCount = 0;
91
+ wss.clients.forEach((ws) => {
92
+ if (Reflect.has(ws, SYM_WS_IS_ALIVE) === false) {
93
+ ws[SYM_WS_IS_ALIVE] = true;
94
+ ws.on("pong", function onPong() {
95
+ this[SYM_WS_IS_ALIVE] = true;
96
+ });
97
+ }
98
+ if (ws.readyState !== ws.OPEN)
99
+ return;
100
+ if (ws[SYM_WS_IS_ALIVE] === false) {
101
+ wsNotAliveCount += 1;
102
+ ws.close(1001, "websocket client not alive");
103
+ return ws.terminate();
104
+ }
105
+ ws[SYM_WS_IS_ALIVE] = false;
106
+ ws.ping();
107
+ });
108
+ logger_1.functionsLogger.logError(`wss heartbeat finish, ws not alive count: ${wsNotAliveCount}, total: ${wss.clients.size}`);
109
+ checking = false;
110
+ }
111
+ const aliveCheckTimer = setInterval(doCheck, interval);
112
+ wss.once("close", () => {
113
+ clearInterval(aliveCheckTimer);
114
+ });
115
+ }
116
+ function setupGracefulShutdown(wss) {
117
+ for (const signal of ["SIGINT", "SIGTERM"]) {
118
+ process.on(signal, () => __awaiter(this, void 0, void 0, function* () {
119
+ logger_1.functionsLogger.logError(`received ${signal} signal, shutting down websocket server, ${wss.clients.size} ws clients need to be closed`);
120
+ wss.close();
121
+ wss.clients.forEach((ws) => {
122
+ ws.close(WS_ERROR_CODE.SERVICE_RESTART, "websocket server is shutting down, please try to reconnect");
123
+ });
124
+ }));
125
+ }
126
+ }
127
+ class VectorxWebSocket extends events.EventEmitter {
128
+ constructor(ws, eventCtx) {
129
+ super();
130
+ this.ws = ws;
131
+ this.ws.onopen = (openEvent) => {
132
+ this.emit("open", {
133
+ ctx: eventCtx,
134
+ type: openEvent.type,
135
+ });
136
+ };
137
+ this.ws.onerror = (errorEvent) => {
138
+ this.emit("error", {
139
+ ctx: eventCtx,
140
+ type: errorEvent.type,
141
+ message: errorEvent.message,
142
+ error: errorEvent.error,
143
+ });
144
+ };
145
+ this.ws.onclose = (closeEvent) => {
146
+ this.emit("close", {
147
+ ctx: eventCtx,
148
+ type: closeEvent.type,
149
+ code: closeEvent.code,
150
+ reason: closeEvent.reason,
151
+ wasClean: closeEvent.wasClean,
152
+ });
153
+ };
154
+ this.ws.onmessage = (messageEvent) => {
155
+ this.emit("message", {
156
+ ctx: eventCtx,
157
+ type: messageEvent.type,
158
+ data: messageEvent.data,
159
+ });
160
+ };
161
+ Object.defineProperty(this, "ws", {
162
+ configurable: false,
163
+ enumerable: false,
164
+ writable: false,
165
+ });
166
+ }
167
+ send(buf) {
168
+ this.ws.send(buf);
169
+ }
170
+ close(code, reason) {
171
+ this.ws.close(code, reason);
172
+ }
173
+ }
174
+ exports.VectorxWebSocket = VectorxWebSocket;
175
+ function createWebSocketServer(httpServer, koaApp, router, options, projectConfig) {
176
+ const hasExportHandleUpgradeFunctions = [];
177
+ (0, function_registry_1.getAllRegisteredFunctions)().forEach((fn) => {
178
+ if (typeof fn.userFunction.handleUpgrade === "function") {
179
+ hasExportHandleUpgradeFunctions.push(fn.name);
180
+ }
181
+ });
182
+ if (hasExportHandleUpgradeFunctions.length === 0) {
183
+ return;
184
+ }
185
+ process.stdout.write(`[vectorx-ff] WebSocket feature is enabled. Enabled functions: ${hasExportHandleUpgradeFunctions.join(", ")}\n`);
186
+ const wss = new ws_1.WebSocketServer({
187
+ noServer: true,
188
+ clientTracking: true,
189
+ perMessageDeflate: {
190
+ zlibDeflateOptions: {
191
+ chunkSize: 1024,
192
+ memLevel: 7,
193
+ level: 3,
194
+ },
195
+ zlibInflateOptions: {
196
+ chunkSize: 10 * 1024,
197
+ },
198
+ clientNoContextTakeover: true,
199
+ serverNoContextTakeover: true,
200
+ serverMaxWindowBits: 10,
201
+ concurrencyLimit: 10,
202
+ threshold: 1024,
203
+ },
204
+ });
205
+ setupGracefulShutdown(wss);
206
+ setupWebSocketsHeartbeat(wss);
207
+ wss.on("error", (err) => {
208
+ logger_1.functionsLogger.logError(err, { scene: "ws_server_error" });
209
+ });
210
+ wss.on("close", () => {
211
+ logger_1.functionsLogger.logError("ws server has been closed");
212
+ });
213
+ const composedMiddleware = (0, koa_compose_1.default)([
214
+ (0, middle_event_id_1.eventIdMiddleware)(),
215
+ (0, middle_async_context_1.asyncContextMiddleware)(),
216
+ (0, middle_common_logger_1.loggerMiddleware)(),
217
+ (ctx, next) => __awaiter(this, void 0, void 0, function* () {
218
+ try {
219
+ yield next();
220
+ }
221
+ catch (err) {
222
+ logger_1.functionsLogger.logError(err, { scene: "ws_middleware_error" });
223
+ }
224
+ }),
225
+ (0, middle_context_injection_1.contextInjectionMiddleware)(options, projectConfig),
226
+ ]);
227
+ httpServer.on("upgrade", (request, socket, head) => __awaiter(this, void 0, void 0, function* () {
228
+ const { handleFunction } = (0, router_1.routeFunction)(router, request.url || "/");
229
+ const userFunction = handleFunction;
230
+ if (typeof userFunction === "undefined" || typeof userFunction.handleUpgrade !== "function") {
231
+ socket.destroy();
232
+ return;
233
+ }
234
+ request.socket.setKeepAlive(true);
235
+ request.socket.setNoDelay(true);
236
+ request.socket.setTimeout(0);
237
+ try {
238
+ const ctx = koaApp.createContext(request, {});
239
+ yield composedMiddleware(ctx, () => {
240
+ return new Promise((resolve) => {
241
+ if (typeof userFunction.handleUpgrade !== "function") {
242
+ respond(socket, 404, "Not Found");
243
+ return;
244
+ }
245
+ const upgradeContext = {
246
+ ctxId: ctx.state.eventID,
247
+ eventID: ctx.state.eventID,
248
+ eventType: "websocket.upgrade",
249
+ timestamp: new Date().toISOString(),
250
+ httpContext: {
251
+ url: request.url,
252
+ httpMethod: request.method,
253
+ headers: request.headers,
254
+ },
255
+ };
256
+ ctx.state.contextInjected = upgradeContext;
257
+ try {
258
+ Promise.resolve(userFunction.handleUpgrade(upgradeContext))
259
+ .then((result) => {
260
+ if (!result.allowWebSocket) {
261
+ respond(socket, result.statusCode || 404, result.body || "Not Found", result.contentType);
262
+ return;
263
+ }
264
+ wss.handleUpgrade(request, socket, head, (ws) => __awaiter(this, void 0, void 0, function* () {
265
+ const context = {
266
+ ctxId: ctx.state.eventID,
267
+ eventID: ctx.state.eventID,
268
+ eventType: "websocket.connection",
269
+ timestamp: new Date().toISOString(),
270
+ httpContext: {
271
+ url: request.url,
272
+ httpMethod: request.method,
273
+ headers: request.headers,
274
+ },
275
+ ws: new VectorxWebSocket(ws, { ctxId: ctx.state.eventID }),
276
+ };
277
+ (0, helper_1.deepFreeze)(context);
278
+ process.nextTick(() => {
279
+ try {
280
+ userFunction(null, Object.assign({}, context));
281
+ }
282
+ catch (_e) {
283
+ }
284
+ });
285
+ resolve();
286
+ }));
287
+ })
288
+ .catch((e) => {
289
+ logger_1.functionsLogger.logError(`handleUpgrade promise-catch error: ${(0, error_1.decorateErrorStack)(e)}`);
290
+ respondError(socket, e);
291
+ });
292
+ }
293
+ catch (e) {
294
+ logger_1.functionsLogger.logError(`handleUpgrade try-catch error: ${(0, error_1.decorateErrorStack)(e)}`);
295
+ respondError(socket, e);
296
+ }
297
+ });
298
+ });
299
+ }
300
+ catch (e) {
301
+ logger_1.functionsLogger.logError(e, { scene: "ws_upgrade_error" });
302
+ respondError(socket, e);
303
+ }
304
+ }));
305
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vectorx/functions-framework",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "VectorX Functions Framework",
5
5
  "main": "lib/index.js",
6
6
  "types": "types/index.d.ts",
@@ -22,16 +22,15 @@
22
22
  "author": "",
23
23
  "license": "ISC",
24
24
  "engines": {
25
- "node": ">=18.0.0"
25
+ "node": ">=20.0.0"
26
26
  },
27
27
  "dependencies": {
28
- "@vectorx/ai-types": "1.0.1",
29
- "@vectorx/endpoints": "1.0.1",
30
28
  "async_hooks": "^1.0.0",
31
29
  "chalk": "4",
32
30
  "commander": "^12.1.0",
33
31
  "dotenv": "^16.5.0",
34
32
  "koa": "^2.14.2",
33
+ "koa-compose": "^4.1.0",
35
34
  "koa-body": "^6.0.1",
36
35
  "koa-bodyparser": "^4.4.1",
37
36
  "koa-router": "^12.0.1",
@@ -42,8 +41,11 @@
42
41
  "radix3": "^1.1.2",
43
42
  "raw-body": "^2.5.2",
44
43
  "uuid": "^9.0.1",
44
+ "ws": "^8.18.0",
45
45
  "winston": "^3.11.0",
46
- "winston-daily-rotate-file": "^4.7.1"
46
+ "winston-daily-rotate-file": "^4.7.1",
47
+ "@vectorx/endpoints": "1.1.0",
48
+ "@vectorx/ai-types": "1.1.0"
47
49
  },
48
50
  "devDependencies": {
49
51
  "@types/jest": "^29.5.12",
@@ -53,6 +55,7 @@
53
55
  "@types/node": "^20.11.24",
54
56
  "@types/supertest": "^6.0.2",
55
57
  "@types/uuid": "^9.0.8",
58
+ "@types/ws": "^8.5.12",
56
59
  "@typescript-eslint/eslint-plugin": "^7.1.0",
57
60
  "@typescript-eslint/parser": "^7.1.0",
58
61
  "eslint": "^8.57.0",
@@ -72,6 +75,5 @@
72
75
  "test:ci": "jest --ci --coverage",
73
76
  "lint": "eslint src --ext .ts",
74
77
  "run:demo": "node ./bin/rcb-ff.js"
75
- },
76
- "readme": "# VectorX Functions Framework\n\nVectorX Functions Framework 是一个用于构建和运行云函数的框架,提供了完整的日志系统支持。\n\n## 日志系统\n\n框架内置了完整的日志系统,支持请求日志记录和用户代码日志记录。\n\n### 配置\n\n在初始化框架时,可以配置日志系统:\n\n```typescript\nimport { createAgentServerFramework } from '@vectorx/functions-framework';\n\nconst framework = createAgentServerFramework({\n port: 3000,\n logging: {\n dirname: '/path/to/logs', // 日志存储目录\n maxSize: '20m', // 单个日志文件最大大小\n maxFiles: 14 // 保留的日志文件数量\n }\n});\n```\n\n### 日志类型\n\n框架支持两种类型的日志:\n\n- `ACCESS`: 访问日志,记录所有 HTTP 请求\n- `USERCODE`: 用户代码日志,记录用户函数中的日志\n\n### 日志查询 API\n\n框架提供了日志查询 API,用于查看和调试日志:\n\n```\nGET /@logs\n```\n\n#### 查询参数\n\n| 参数名 | 类型 | 必填 | 说明 |\n|--------|------|------|------|\n| type | string | 否 | 日志类型,可选值:ACCESS/USERCODE,默认 ACCESS |\n| limit | number | 否 | 返回的日志条数限制,默认 100 |\n| eventId | string | 否 | 按 eventId 过滤日志 |\n\n#### 示例\n\n1. 查询所有访问日志:\n```bash\ncurl 'http://localhost:3000/@logs?type=ACCESS'\n```\n\n2. 查询特定 eventId 的日志:\n```bash\ncurl 'http://localhost:3000/@logs?eventId=550e8400-e29b-41d4-a716-446655440000'\n```\n\n3. 限制返回条数:\n```bash\ncurl 'http://localhost:3000/@logs?limit=10'\n```\n\n4. 组合查询:\n```bash\ncurl 'http://localhost:3000/@logs?type=USERCODE&eventId=550e8400-e29b-41d4-a716-446655440000&limit=20'\n```\n\n#### 响应格式\n\n成功响应:\n```json\n{\n \"success\": true,\n \"data\": [\n {\n \"@timestamp\": \"2024-03-21T10:30:00.000Z\",\n \"level\": \"info\",\n \"eventId\": \"550e8400-e29b-41d4-a716-446655440000\",\n \"message\": \"Request started\",\n \"method\": \"GET\",\n \"url\": \"/api/example\"\n }\n ]\n}\n```\n\n错误响应:\n```json\n{\n \"success\": false,\n \"error\": \"错误信息\"\n}\n```\n\n### 在代码中使用日志\n\n```typescript\nimport { functionsLogger } from '@vectorx/functions-framework';\nimport { LogLevel } from '@vectorx/functions-framework';\n\n// 记录访问日志\nfunctionsLogger.logAccesslog(LogLevel.INFO, 'Custom message', {\n customField: 'value'\n});\n\n// 记录用户代码日志\nfunctionsLogger.logUserCodelog([\n { message: 'User log message', level: 'info' }\n]);\n```\n\n### 日志格式\n\n每条日志都包含以下字段:\n\n- `@timestamp`: 日志时间戳\n- `level`: 日志级别\n- `eventId`: 请求 ID\n- `message`: 日志消息\n- 其他自定义字段\n\n## 开发\n\n### 运行测试\n\n```bash\nnpm test\n```\n\n### 构建\n\n```bash\nnpm run build\n``` \n\n### 环境管理\n读取 .env 文件,校验 env 文件的可用性,获取当前运行环境\n\n\n## rcb-ff 自动检测项目类型\n\n当直接使用 `rcb-ff` 命令启动服务时,如果未指定 `--mode` 参数,会自动检测项目类型:\n\n### 检测规则\n\n1. **Agent 项目检测**:\n - 如果存在 `project.config.json` 文件且包含 `agentId` 字段,则判定为 `agent` 项目\n\n2. **Fun 项目检测**:\n - 如果不存在 `project.config.json` 或 `project.config.json` 中不存在 `agentId` 字段\n - 且存在 `agent-cloudbase-functions.json` 配置文件\n - 则判定为 `fun` 项目\n\n3. **默认行为**:\n - 如果无法确定项目类型,默认使用 `agent` 模式(保持向后兼容)\n\n### 使用示例\n\n```bash\n# 自动检测项目类型(推荐)\nrcb-ff --directory ./my-project\n\n# 明确指定模式(覆盖自动检测)\nrcb-ff --directory ./my-project --mode fun\nrcb-ff --directory ./my-project --mode agent\n```\n"
78
+ }
77
79
  }
@@ -1,4 +1,8 @@
1
1
  export declare function buildFnId(fnName: string, functionName: string): string;
2
2
  export declare function registerFunction(fnName: string, fn: Function): void;
3
3
  export declare function getRegisteredFunction(fnId: string): Function | undefined;
4
+ export declare function getAllRegisteredFunctions(): Array<{
5
+ name: string;
6
+ userFunction: Function;
7
+ }>;
4
8
  export declare function clearRegistry(): void;
package/types/index.d.ts CHANGED
@@ -4,5 +4,6 @@ export * from "./function-loader";
4
4
  export * from "./function-registry";
5
5
  export * from "./sse";
6
6
  export * from "./server";
7
+ export * from "./wss";
7
8
  export * from "./logger";
8
9
  export * from "./async-context";
package/types/server.d.ts CHANGED
@@ -4,6 +4,7 @@ import type { ProjectConfig } from "./function-loader";
4
4
  import type { functionsLogger } from "./logger";
5
5
  import type { Request } from "./request";
6
6
  import type { Router } from "./router";
7
+ import type { VectorxWebSocket } from "./wss";
7
8
  declare module "koa" {
8
9
  interface DefaultState {
9
10
  isTimeout?: boolean;
@@ -41,5 +42,6 @@ export interface RcbContext {
41
42
  setCookie: (name: string, value: string, options?: any) => void;
42
43
  clearCookie: (name: string, options?: any) => void;
43
44
  sse: () => any;
45
+ ws?: VectorxWebSocket;
44
46
  }
45
47
  export declare function createServer(options: FrameworkOptions, router: Router, projectConfig: ProjectConfig): http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>;
package/types/wss.d.ts ADDED
@@ -0,0 +1,56 @@
1
+ import * as events from "events";
2
+ import * as http from "http";
3
+ import type Koa from "koa";
4
+ import type { WebSocket as RawWebSocket } from "ws";
5
+ import type { FrameworkOptions } from "./framework";
6
+ import type { ProjectConfig } from "./function-loader";
7
+ import type { Router } from "./router";
8
+ export interface WsEventContext {
9
+ ctxId: string;
10
+ }
11
+ export interface WsOpenEvent {
12
+ ctx: WsEventContext;
13
+ type: string;
14
+ }
15
+ export interface WsErrorEvent {
16
+ ctx: WsEventContext;
17
+ type: string;
18
+ message: string;
19
+ error: any;
20
+ }
21
+ export interface WsCloseEvent {
22
+ ctx: WsEventContext;
23
+ type: string;
24
+ code: number;
25
+ reason: string;
26
+ wasClean: boolean;
27
+ }
28
+ export interface WsMessageEvent {
29
+ ctx: WsEventContext;
30
+ type: string;
31
+ data: any;
32
+ }
33
+ export interface HandleUpgradeResult {
34
+ allowWebSocket: boolean;
35
+ statusCode?: number;
36
+ body?: string;
37
+ contentType?: string;
38
+ }
39
+ export interface WsUpgradeContext {
40
+ ctxId: string;
41
+ eventID: string;
42
+ eventType: "websocket.upgrade";
43
+ timestamp: string;
44
+ httpContext: {
45
+ url: string | undefined;
46
+ httpMethod: string | undefined;
47
+ headers: http.IncomingHttpHeaders;
48
+ };
49
+ }
50
+ export declare class VectorxWebSocket extends events.EventEmitter {
51
+ private ws;
52
+ constructor(ws: RawWebSocket, eventCtx: WsEventContext);
53
+ send(buf: any): void;
54
+ close(code?: number, reason?: string): void;
55
+ }
56
+ export declare function createWebSocketServer(httpServer: http.Server, koaApp: Koa, router: Router, options: FrameworkOptions, projectConfig: ProjectConfig): void;