UNPKG

imean-service-engine

Version:

microservice engine

1,387 lines (1,374 loc) 43.3 kB
'use strict'; var zod = require('zod'); var ejson4 = require('ejson'); var lruCache = require('lru-cache'); var nodeServer = require('@hono/node-server'); var etcd3 = require('etcd3'); var fs = require('fs-extra'); var hono = require('hono'); var api = require('@opentelemetry/api'); var winston = require('winston'); var prettier = require('prettier'); var crypto2 = require('crypto'); var zlib = require('zlib'); var nodeWs = require('@hono/node-ws'); var mcp_js = require('@modelcontextprotocol/sdk/server/mcp.js'); var types_js = require('@modelcontextprotocol/sdk/types.js'); var streaming = require('hono/streaming'); var ulid = require('ulid'); var dayjs = require('dayjs'); function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; } var ejson4__default = /*#__PURE__*/_interopDefault(ejson4); var fs__default = /*#__PURE__*/_interopDefault(fs); var winston__default = /*#__PURE__*/_interopDefault(winston); var prettier__default = /*#__PURE__*/_interopDefault(prettier); var crypto2__default = /*#__PURE__*/_interopDefault(crypto2); var dayjs__default = /*#__PURE__*/_interopDefault(dayjs); // mod.ts var CacheAdapter = class { }; var RedisCacheAdapter = class extends CacheAdapter { constructor(redis) { super(); this.redis = redis; } async get(key) { const value = await this.redis.get(key); return value ? ejson4__default.default.parse(value) : null; } async set(key, value, ttl) { return this.redis.set(key, ejson4__default.default.stringify(value), "EX", ttl / 1e3); } }; var MemoryCacheAdapter = class extends CacheAdapter { cache; constructor(options) { super(); this.cache = new lruCache.LRUCache({ max: 1e3, ttl: 1e3 * 60 * 10, ...options }); } async get(key) { return this.cache.get(key); } async set(key, value, ttl) { this.cache.set(key, value, { ttl }); } }; var ACTION_METADATA = Symbol("action:metadata"); function Action(options) { options.params = options.params || []; options.returns = options.returns || zod.z.void(); options.cache = options.cache ?? false; options.ttl = options.ttl ?? 0; options.stream = options.stream ?? false; return function(target, context) { const methodName = context.name; context.addInitializer(function() { const prototype = this.constructor.prototype; const func = target.toString().split("\n")[0]; const params = func.slice(func.indexOf("(") + 1, func.indexOf(")")).split(",").map((p) => p.split("=").shift()).filter((param) => !!param?.trim()); params.forEach((param, index) => { options.params[index] = options.params[index]?.describe( param.trim() ); }); const existingMetadata = prototype[ACTION_METADATA] || {}; existingMetadata[methodName] = { name: methodName, description: options.description || "", params: options.params, returns: options.returns, idempotence: options.idempotence ?? false, printError: options.printError ?? false, cache: options.cache, ttl: options.ttl, stream: options.stream, mcp: options.mcp }; prototype[ACTION_METADATA] = existingMetadata; }); }; } function getActionMetadata(target) { return target.constructor.prototype[ACTION_METADATA] ?? {}; } // decorators/module.ts var moduleMetadataMap = /* @__PURE__ */ new WeakMap(); function Module(name, options = {}) { return function(target, _context) { const metadata = { name, options: { name: options.name || name, version: options.version || "1.0.0", description: options.description || "", printError: options.printError ?? false } }; moduleMetadataMap.set(target, metadata); }; } function getModuleMetadata(target) { return moduleMetadataMap.get(target); } // core/types.ts var ScheduleMode = /* @__PURE__ */ ((ScheduleMode2) => { ScheduleMode2["FIXED_RATE"] = "FIXED_RATE"; ScheduleMode2["FIXED_DELAY"] = "FIXED_DELAY"; return ScheduleMode2; })(ScheduleMode || {}); var Plugin = class { }; var logger = winston__default.default.createLogger({ level: "info", transports: [ new winston__default.default.transports.Console({ level: "info", format: winston.format.combine( winston.format.colorize(), winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss.SSS" }), winston.format.printf((info) => { const splat = info[Symbol.for("splat")]; const msg = `${info.level} ${info.timestamp}: ${info.message} ${!!splat ? JSON.stringify(splat) : ""}`; return msg; }) ) }) ] }); var logger_default = logger; // decorators/schedule.ts var tracer = api.trace.getTracer("scheduler"); var SCHEDULE_METADATA = Symbol("schedule:metadata"); function Schedule(options) { return function(_originalMethod, context) { const methodName = context.name.toString(); context.addInitializer(function() { const prototype = this.constructor.prototype; const moduleMetadata = getModuleMetadata(this.constructor); if (!moduleMetadata) { throw new Error( `Class ${this.constructor.name} is not decorated with @Module decorator` ); } const existingMetadata = prototype[SCHEDULE_METADATA] || {}; existingMetadata[methodName] = { name: methodName, interval: options.interval, mode: options.mode || "FIXED_RATE" /* FIXED_RATE */ }; prototype[SCHEDULE_METADATA] = existingMetadata; }); }; } function getScheduleMetadata(target) { return target.constructor.prototype[SCHEDULE_METADATA] ?? {}; } var Scheduler = class { constructor(etcdClient) { this.etcdClient = etcdClient; } campaigns = /* @__PURE__ */ new Map(); timers = /* @__PURE__ */ new Map(); isLeader = /* @__PURE__ */ new Map(); /** * 启动调度任务 */ async startSchedule(serviceId, moduleName, methodName, electionKey, metadata, method) { const election = this.etcdClient.election(electionKey, 10); const observe = await election.observe(); observe.on("change", (leader) => { const isLeader = leader === serviceId; this.isLeader.set(serviceId, isLeader); if (!isLeader) { this.stopTimer(serviceId); } }); const campaign = election.campaign(serviceId); this.campaigns.set(serviceId, campaign); campaign.on("error", (error) => { logger_default.error(`Error in campaign for ${moduleName}.${methodName}:`, error); }); campaign.on("elected", () => { this.isLeader.set(serviceId, true); this.startTimer(serviceId, metadata, moduleName, method); logger_default.info(`become leader for ${moduleName}.${methodName}`); }); } /** * 启动定时器 */ startTimer(serviceId, metadata, moduleName, method) { this.stopTimer(serviceId); const wrappedMethod = async () => { tracer.startActiveSpan( `ScheduleTask ${moduleName}.${metadata.name}`, { root: true }, async (span) => { span.setAttribute("serviceId", serviceId); span.setAttribute("methodName", metadata.name); span.setAttribute("moduleName", moduleName); span.setAttribute("interval", metadata.interval); span.setAttribute("mode", metadata.mode); try { await method(); } catch (error) { span.setStatus({ code: api.SpanStatusCode.ERROR, message: error.message }); } finally { span.setStatus({ code: api.SpanStatusCode.OK }); span.end(); } } ); }; if (metadata.mode === "FIXED_DELAY" /* FIXED_DELAY */) { const runTask = async () => { if (!this.isLeader.get(serviceId)) return; try { await wrappedMethod(); } finally { this.timers.set(serviceId, setTimeout(runTask, metadata.interval)); } }; runTask(); } else { this.timers.set( serviceId, setInterval(async () => { if (!this.isLeader.get(serviceId)) return; await wrappedMethod(); }, metadata.interval) ); } } /** * 停止定时器 */ stopTimer(serviceId) { const timer = this.timers.get(serviceId); if (timer) { clearTimeout(timer); clearInterval(timer); this.timers.delete(serviceId); } } /** * 停止所有调度任务 */ async stop() { for (const serviceId of this.timers.keys()) { this.stopTimer(serviceId); } for (const [serviceId, campaign] of this.campaigns.entries()) { try { await campaign.resign().catch(() => { }); } catch (error) { console.error(`Error stopping schedule ${serviceId}:`, error); } finally { this.campaigns.delete(serviceId); this.isLeader.delete(serviceId); } } } }; async function formatCode(code) { try { return prettier__default.default.format(code, { parser: "typescript" }); } catch { return code; } } function getZodTypeString(schema, defaultOptional = false) { function processType(type) { const def = type._def; if (def.typeName === "ZodNullable") { return `${processType(def.innerType)} | null`; } if (def.typeName === "ZodOptional") { return processType(def.innerType); } if (def.typeName === "ZodEffects" && def.schema?._def.typeName !== "ZodAny") { return processType(def.schema); } switch (def.typeName) { case "ZodString": { return "string"; } case "ZodNumber": { return "number"; } case "ZodBigInt": { return "bigint"; } case "ZodBoolean": { return "boolean"; } case "ZodArray": { const elementType = processType(def.type); return `${elementType}[]`; } case "ZodDate": { return "Date"; } case "ZodObject": { const shape = def.shape(); const props = Object.entries(shape).map(([key, value]) => { if (key.includes("-")) { key = `'${key}'`; } const fieldDef = value._def; const isOptional = fieldDef.typeName === "ZodOptional"; const isDefault = defaultOptional && fieldDef.typeName === "ZodDefault"; const fieldType = processType( isOptional ? fieldDef.innerType : value ); return `${key}${isOptional || isDefault ? "?" : ""}: ${fieldType}`; }).join("; "); return `{ ${props} }`; } case "ZodUnion": { return def.options.map((opt) => processType(opt)).join(" | "); } case "ZodNull": { return "null"; } case "ZodPromise": { return `Promise<${processType(def.type)}>`; } case "ZodVoid": { return "void"; } case "ZodRecord": { return `Record<${processType(def.keyType)}, ${processType( def.valueType )}>`; } case "ZodMap": { return `Map<${processType(def.keyType)}, ${processType( def.valueType )}>`; } case "ZodAny": { return "any"; } case "ZodUnknown": { return "unknown"; } case "ZodEnum": { return "(" + def.values.map((opt) => `"${opt}"`).join(" | ") + ")"; } case "ZodDefault": { return processType(def.innerType); } default: { if (type.safeParse(new Uint8Array()).success) { return "Uint8Array"; } return "unknown"; } } } return processType(schema); } async function generateClientCode(modules) { const timestamp = (/* @__PURE__ */ new Date()).toISOString(); const imports = [ "// \u8FD9\u4E2A\u6587\u4EF6\u662F\u81EA\u52A8\u751F\u6210\u7684\uFF0C\u8BF7\u4E0D\u8981\u624B\u52A8\u4FEE\u6539", `// Generated at ${timestamp}`, "", 'import { MicroserviceClient as BaseMicroserviceClient } from "imean-service-client";', 'export * from "imean-service-client";', "" ].join("\n"); const interfaces = Object.entries(modules).map(([name, module]) => { const methods = Object.entries(module.actions).map(([actionName, action]) => { if (!action.params) { throw new Error(`Missing params for action ${actionName}`); } const params = action.params.map((param, index) => { const name2 = param.description || `arg${index}`; return `${name2}${param.isOptional() ? "?" : ""}: ${getZodTypeString( param, true )}`; }).join(", "); const returnType = action.returns ? getZodTypeString(action.returns) : "void"; return ` /** * ${action.description} */ ${actionName}: (${params}) => Promise<${action.stream ? `AsyncIterable<${returnType}>` : returnType}>;`; }).join("\n "); const capitalizedName = name.charAt(0).toUpperCase() + name.slice(1); return `export interface ${capitalizedName}Module { ${methods} }`; }).join("\n\n"); const clientClass = `export class MicroserviceClient extends BaseMicroserviceClient { ${Object.entries(modules).map(([name, module]) => { const methods = Object.entries(module.actions).map(([actionName, action]) => { return `${actionName}: { idempotent: ${!!action.idempotence}, stream: ${!!action.stream} }`; }).join(",\n "); return `public readonly ${name} = this.registerModule<${name.charAt(0).toUpperCase() + name.slice(1)}Module>("${name}", { ${methods} });`; }).join("\n\n ")} }`; return await formatCode([imports, interfaces, clientClass].join("\n\n")); } function hashText(text) { const hash = crypto2__default.default.createHash("sha256"); hash.update(text); return hash.digest("hex"); } var brotli = { compress: (data, quality = 6) => { return new Promise((resolve, reject) => { zlib.brotliCompress( data, { params: { [zlib.constants.BROTLI_PARAM_QUALITY]: quality } }, (err, compressed) => { if (err) reject(err); else resolve(compressed); } ); }); }, decompress: (data) => { return new Promise((resolve, reject) => { zlib.brotliDecompress(data, (err, decompressed) => { if (err) reject(err); else resolve(decompressed); }); }); } }; // core/handler.ts var tracer2 = api.trace.getTracer("action-handler"); var ActionHandler = class { constructor(moduleInstance, actionName, metadata, microservice, moduleName) { this.moduleInstance = moduleInstance; this.actionName = actionName; this.metadata = metadata; this.microservice = microservice; this.moduleName = moduleName; } async handle(req) { return await tracer2.startActiveSpan( `handle ${this.moduleName}.${this.actionName}`, async (span) => { span.setAttribute("module", this.moduleName); span.setAttribute("action", this.actionName); try { return await this._handle(req); } catch (error) { span.recordException(error); span.setStatus({ code: api.SpanStatusCode.ERROR, message: error.message }); throw error; } finally { span.end(); } } ); } async _validate(req) { const span = tracer2.startSpan("validate"); try { let args; if (typeof req === "string") { try { args = Object.values(ejson4__default.default.parse(req)); } catch (error) { throw new Error(`Invalid request body: ${error.message}`); } } else { args = req; } if (this.metadata.params) { args = args.map((arg, index) => { try { return this.metadata.params[index].parse(arg); } catch (error) { throw new Error( `Invalid argument ${index}: ${error.message}` ); } }); } const requestHash = hashText(ejson4__default.default.stringify(args)); span.setAttribute("requestHash", requestHash); return { args, requestHash }; } finally { span.end(); } } async _handle(req) { const span = api.trace.getActiveSpan(); const { args, requestHash } = await this._validate(req); const cacheHash = typeof this.metadata.cache === "function" ? this.metadata.cache(...args) : requestHash; const cacheKey = `${this.microservice.options.name}:cache:${this.moduleName}:${this.actionName}:${hashText(ejson4__default.default.stringify(cacheHash))}`; span?.setAttribute("args", JSON.stringify(args)); if (!this.metadata.stream && this.metadata.cache && !this.microservice.options.disableCache) { const cached = await this.microservice.cache.get(cacheKey); const now = Date.now(); if (cached !== null && (!this.metadata.ttl || cached?.expireAt > now)) { span?.setAttribute("cacheHit", true); span?.setAttribute("cacheKey", cacheKey); return cached.data; } } try { const result = await this.moduleInstance[this.actionName].apply( this.moduleInstance, args ); if (this.metadata.stream) { span?.setAttribute("stream", true); if (!isAsyncIterable(result)) { throw new Error("Stream action must return AsyncIterator"); } let count = 0; return { [Symbol.asyncIterator]() { const iterator = result[Symbol.asyncIterator](); return { async next() { const { value, done } = await iterator.next(); span?.addEvent("stream.next"); if (!done) count++; if (done) { span?.setAttribute("streamCount", count); span?.addEvent("stream.end"); } return { value, done }; } }; } }; } let parsedResult = result; if (this.metadata.returns) { try { parsedResult = this.metadata.returns.parse(result); } catch (error) { throw new Error(`Invalid return value: ${error.message}`); } } if (this.metadata.cache && !this.microservice.options.disableCache) { const now = Date.now(); this.microservice.cache.set( cacheKey, { data: parsedResult, expireAt: this.metadata.ttl ? now + this.metadata.ttl : void 0 }, this.metadata.ttl ?? 1e3 ); } return parsedResult; } catch (error) { if (this.metadata.printError !== false && this.microservice.options.printError !== false) { console.error(`Error in ${this.moduleName}.${this.actionName}:`, error); } throw error; } } }; function isAsyncIterable(obj) { return obj != null && typeof obj[Symbol.asyncIterator] === "function"; } var WebSocketHandler = class { constructor(microservice, options) { this.microservice = microservice; this.options = { timeout: options?.timeout || 3e4 }; } sockets = /* @__PURE__ */ new Set(); pendingRequests = /* @__PURE__ */ new Map(); options; onOpen(ws) { this.sockets.add(ws); } async encodeMessage(message) { const jsonStr = ejson4__default.default.stringify(message); const data = new TextEncoder().encode(jsonStr); const compressed = await brotli.compress(data, 6); return compressed; } async decodeMessage(data) { try { const decompressed = await brotli.decompress(new Uint8Array(data)); const jsonStr = new TextDecoder().decode(decompressed); return ejson4__default.default.parse(jsonStr); } catch (error) { console.error("Error decoding message", error); throw error; } } async sendMessage(ws, message) { const encoded = await this.encodeMessage(message); ws.send(encoded); } async onMessage(event, ws) { let messageId = ""; try { const message = await this.decodeMessage(event.data); messageId = message.id || ""; if (message.type === "ping") { await this.sendMessage(ws, { type: "pong" }); return; } const response = await this.handleRequest(ws, message); if (response) { await this.sendMessage(ws, response); } } catch (error) { if (messageId) { await this.sendMessage(ws, { id: messageId, success: false, error: error.message }); } console.error("Failed to handle WebSocket message:", error); } } onClose(ws) { this.sockets.delete(ws); for (const [id, pending] of this.pendingRequests.entries()) { if (pending) { clearTimeout(pending.timer); this.pendingRequests.delete(id); } } } onError(_error, ws) { this.onClose(ws); } async handleRequest(ws, message) { if (!message.id || !message.module || !message.action) { throw new Error("Invalid request message"); } const timer = setTimeout(() => { const pending = this.pendingRequests.get(message.id); if (pending) { this.pendingRequests.delete(message.id); this.microservice.updateMethodStats( pending.module, pending.action, 0, false ); ws.send( ejson4__default.default.stringify({ id: message.id, success: false, error: "Request timeout" }) ); } }, this.options.timeout); this.pendingRequests.set(message.id, { timer, module: message.module, action: message.action }); return (async () => { const startTime = Date.now(); try { const handler = this.microservice.getActionHandler( message.module, message.action ); const args = message.args ? Object.values(message.args) : []; const result = await handler.handle(args); if (handler.metadata.stream) { try { for await (const value of result) { await this.sendMessage(ws, { id: message.id, type: "stream", data: value, done: false }); } await this.sendMessage(ws, { id: message.id, type: "stream", done: true }); return null; } catch (error) { throw error; } } const responseTime = Date.now() - startTime; this.microservice.updateMethodStats( message.module, message.action, responseTime, true ); return { id: message.id, success: true, data: result }; } catch (error) { this.microservice.updateMethodStats( message.module, message.action, 0, false ); return { id: message.id, success: false, error: error.message }; } finally { const pending = this.pendingRequests.get(message.id); if (pending) { clearTimeout(pending.timer); this.pendingRequests.delete(message.id); } } })(); } close() { for (const socket of this.sockets) { socket.close(); } for (const pending of this.pendingRequests.values()) { clearTimeout(pending.timer); } this.sockets.clear(); this.pendingRequests.clear(); } }; // core/core.ts var ServiceContext = {}; var Microservice = class { app; nodeWebSocket; codeCache; waitingInitialization; etcdClient; serviceKey; statsMap = /* @__PURE__ */ new Map(); lease; scheduler; abortController; isShuttingDown = false; statisticsTimer; wsHandler; actionHandlers = /* @__PURE__ */ new Map(); modules = /* @__PURE__ */ new Map(); fetch; options; cache; serviceId; constructor(options) { this.app = new hono.Hono(); this.nodeWebSocket = nodeWs.createNodeWebSocket({ app: this.app }); this.serviceId = crypto.randomUUID(); this.options = { prefix: "/api", name: "microservice", version: "0.0.1", env: "default", printError: false, disableCache: false, generateClient: false, etcd: false, events: {}, websocket: { enabled: false }, cacheAdapter: new MemoryCacheAdapter(), plugins: [], ...options }; this.cache = this.options.cacheAdapter; this.fetch = this.app.request; this.waitingInitialization = this.initialize(); ServiceContext.service = this; } async initialize() { if (this.options.etcd) { this.initEtcd(this.options.etcd); } await this.initModules(); this.initRoutes(); this.initShutdownHandlers(); if (this.options.events?.onStats) { this.initStatsEventManager(); } await this.registerService(true); await this.initPlugins(); } async initModules() { for (const ModuleClass of this.options.modules) { const moduleInstance = new ModuleClass(); const metadata = getModuleMetadata(ModuleClass); if (!metadata) { throw new Error( `Module ${ModuleClass.name} is not decorated with @Module` ); } const moduleName = metadata.name; logger_default.info(`[ \u6CE8\u518C\u6A21\u5757 ] ${moduleName} ${metadata.options.description}`); this.modules.set(moduleName, moduleInstance); const actions = getActionMetadata(ModuleClass.prototype); for (const [actionName, actionMetadata] of Object.entries(actions)) { const handler = new ActionHandler( moduleInstance, actionName, actionMetadata, this, moduleName ); this.actionHandlers.set(`${moduleName}.${actionName}`, handler); logger_default.info( `[ \u6CE8\u518C\u52A8\u4F5C ] ${moduleName}.${actionName} ${actionMetadata.description} ${actionMetadata.mcp ? "MCP:" + actionMetadata.mcp?.type : ""}` ); } const schedules = getScheduleMetadata(ModuleClass.prototype); if (schedules && Object.keys(schedules).length > 0) { if (!this.scheduler && this.etcdClient) { this.scheduler = new Scheduler(this.etcdClient); } for (const [methodName, metadata2] of Object.entries(schedules)) { const electionKey = `${this.options.name}/${moduleName}/schedules/${metadata2.name}`; this.scheduler.startSchedule( this.serviceId, moduleName, methodName, electionKey, metadata2, ModuleClass.prototype[methodName].bind(moduleInstance) ); logger_default.info( `[ \u542F\u52A8\u8C03\u5EA6\u4EFB\u52A1 ] ${moduleName}.${metadata2.name} interval = ${metadata2.interval} mode = ${metadata2.mode}` ); } } } if (this.options.generateClient) { logger_default.info(`[ \u751F\u6210\u5BA2\u6237\u7AEF\u4EE3\u7801 ] ${this.options.generateClient}`); await this.generateClientCode(); } } initRoutes() { const startTime = Date.now(); const prefix = this.options.prefix || "/api"; this.app.get(prefix, (ctx) => { const name = this.options.name ?? "Microservice"; const version = this.options.version ?? "1.0.0"; return ctx.text(`${name} is running. version: ${version}`); }); this.app.get(`${prefix}/health`, (ctx) => { return ctx.json({ status: "ok", uptime: Date.now() - startTime, timestamp: Date.now() }); }); this.app.get(`${prefix}/status`, (ctx) => { return ctx.json({ id: this.serviceId, name: this.options.name, version: this.options.version, env: this.options.env, modules: this.getModules(), stats: Object.fromEntries(this.statsMap) }); }); this.app.get(`${prefix}/client.ts`, async (ctx) => { ctx.header("Content-Type", "application/typescript; charset=utf-8"); ctx.header("Content-Disposition", "attachment; filename=client.ts"); ctx.header("Cache-Control", "public, max-age=3600"); return ctx.body(await this.clientCode()); }); this.app.post(`${prefix}/:moduleName/:actionName`, this.handleRequest); if (this.options.websocket?.enabled) { this.wsHandler = new WebSocketHandler(this); this.app.get( `${prefix}/ws`, this.nodeWebSocket.upgradeWebSocket((_ctx) => { const wsHandler = new WebSocketHandler(this, { timeout: this.options.websocket?.timeout }); return { onOpen(_event, ws) { wsHandler.onOpen(ws); }, onMessage(message, ws) { wsHandler.onMessage(message, ws); }, onClose(_evt, ws) { wsHandler.onClose(ws); }, onError(error, ws) { wsHandler.onError(error, ws); } }; }) ); } } async generateClientCode() { if (typeof this.options.generateClient === "string" || this.options.generateClient instanceof URL) { const code = await this.clientCode(); await fs__default.default.writeFile(this.options.generateClient, code); } } async clientCode() { if (!this.codeCache) { this.codeCache = await formatCode( await generateClientCode(this.getModules(true)) ); } return this.codeCache; } initEtcd(config) { this.etcdClient = new etcd3.Etcd3({ hosts: config.hosts, auth: config.auth }); const serviceName = this.options.name || "microservice"; const namespace = config.namespace ? `${config.namespace}/` : ""; this.serviceKey = `${namespace}services/${serviceName}/${this.serviceId}`; const ttl = config.ttl || 10; this.lease = this.etcdClient.lease(ttl); ServiceContext.lease = this.lease; ServiceContext.etcdClient = this.etcdClient; } /** * 注册服务 */ async registerService(update = false) { const serviceInfo = { id: this.serviceId, name: this.options.name, version: this.options.version, prefix: this.options.prefix, env: this.options.env, modules: this.getModules(false) }; if (update) { await this.options.events?.onRegister?.(serviceInfo)?.catch((e) => { logger_default.error(`Failed to emit register event: ${e}`); }); } } /** * 更新方法统计信息 */ updateMethodStats(moduleName, methodName, responseTime, success, cacheHit = false) { const key = `${moduleName}.${methodName}`; let stats = this.statsMap.get(key); if (!stats) { stats = { totalCalls: 0, successCalls: 0, failureCalls: 0, avgResponseTime: 0, maxResponseTime: 0, minResponseTime: Number.MAX_SAFE_INTEGER, lastUpdateTime: Date.now(), cacheHit: 0 }; this.statsMap.set(key, stats); } stats.totalCalls++; if (success) { stats.successCalls++; } else { stats.failureCalls++; } stats.avgResponseTime = (stats.avgResponseTime * (stats.totalCalls - 1) + responseTime) / stats.totalCalls; stats.maxResponseTime = Math.max(stats.maxResponseTime, responseTime); stats.minResponseTime = Math.min(stats.minResponseTime, responseTime); stats.lastUpdateTime = Date.now(); stats.cacheHit += cacheHit ? 1 : 0; } /** * 获取 Hono 应用实例 */ getApp() { return this.app; } /** * 启动服务 */ async start(port = 3e3, silent = false) { await this.waitingInitialization; const prefix = this.options.prefix ?? "/api"; this.abortController = new AbortController(); !silent && console.log(""); const server = nodeServer.serve({ fetch: this.app.fetch, port }); this.nodeWebSocket.injectWebSocket(server); if (!silent) { console.log( `\u{1F680} Microservice is running on http://localhost:${port}${prefix}` ); console.log( `\u{1F4DA} Client SDK available at http://localhost:${port}${prefix}/client.ts` ); this.options.websocket?.enabled && console.info( `\u{1F517} WebSocket available at http://localhost:${port}${prefix}/ws` ); } } /** * 获取所有模块的元数据 */ getModules(withTypes = false) { const modules = {}; for (const [moduleName, moduleInstance] of this.modules) { const metadata = getModuleMetadata(moduleInstance.constructor); if (!metadata) continue; modules[moduleName] = { name: metadata.name, version: metadata.options.version, description: metadata.options.description, printError: metadata.options.printError, actions: Object.fromEntries( Array.from(this.actionHandlers.entries()).filter(([key]) => key.startsWith(moduleName + ".")).map(([key, handler]) => { const metadata2 = { ...handler.metadata }; if (!withTypes) { delete metadata2.params; delete metadata2.returns; } return [key.slice(moduleName.length + 1), metadata2]; }) ) }; } return modules; } /** * 停止服务 */ async stop() { if (this.wsHandler) { this.wsHandler.close(); } if (this.abortController) { this.abortController.abort(); } if (this.scheduler) { await this.scheduler.stop(); } if (this.options.events?.onStats && this.options.events.forceEmitOnShutdown) { await this.updateStats().catch((e) => { logger_default.error("Failed to emit final stats:", e); }); } if (this.etcdClient && this.serviceKey) { await this.etcdClient.delete().key(this.serviceKey); this.etcdClient.close(); } } /** * 初始化停机处理 */ initShutdownHandlers() { process.on("SIGINT", () => { logger_default.info(` Received SIGINT signal`); this.gracefulShutdown(); }); process.on("unhandledrejection", (event) => { logger_default.error("Unhandled rejection:", event.reason); }); process.on("error", (event) => { logger_default.error("Uncaught error:", event.error); }); } /** * 优雅停机 */ async gracefulShutdown() { if (this.isShuttingDown) return; this.isShuttingDown = true; logger_default.info("\nGraceful shutdown initiated..."); if (this.statisticsTimer) clearInterval(this.statisticsTimer); try { await this.stop(); logger_default.info("Graceful shutdown completed"); } catch (error) { logger_default.error("Error during shutdown:", error); process.exit(1); } finally { process.exit(0); } } initStatsEventManager() { this.statisticsTimer = setInterval(async () => { await this.updateStats(); }, this.options.events.interval ?? 1e4); } async updateStats() { const now = Date.now(); const { onStats } = this.options.events; this.registerService(true); for (const [key, stats] of this.statsMap.entries()) { if (stats) { const [moduleName, actionName] = key.split("."); try { await onStats?.({ service: { env: this.options.env, id: this.serviceId, name: this.options.name, version: this.options.version }, module: moduleName, action: actionName, stats, startTime: stats.lastUpdateTime, endTime: now }); this.statsMap.delete(key); } catch (error) { logger_default.error(`Failed to emit stats event for ${key}:`, error); } } } } getActionHandler(moduleName, actionName) { const key = `${moduleName}.${actionName}`; const handler = this.actionHandlers.get(key); if (!handler) { throw new Error(`Action ${actionName} not found in module ${moduleName}`); } return handler; } handleRequest = async (ctx) => { const { moduleName, actionName } = ctx.req.param(); const handler = this.getActionHandler(moduleName, actionName); try { const paramsText = await ctx.req.text(); const result = await handler.handle(paramsText); if (handler.metadata.stream) { const encoder = new TextEncoder(); const stream = new ReadableStream({ async start(controller) { try { for await (const value of result) { const response = { value, done: false }; const chunk = encoder.encode(ejson4__default.default.stringify(response) + "\n"); controller.enqueue(chunk); } controller.enqueue( encoder.encode( ejson4__default.default.stringify({ done: true }) + "\n" ) ); controller.close(); } catch (error) { const response = { error: error.message, done: true }; controller.enqueue( encoder.encode(ejson4__default.default.stringify(response) + "\n") ); controller.close(); } } }); return new Response(stream, { headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" } }); } return ctx.text(ejson4__default.default.stringify({ success: true, data: result })); } catch (error) { return ctx.json({ success: false, error: error.message }); } }; async initPlugins() { for (const plugin of this.options.plugins) { logger_default.info(`[ \u521D\u59CB\u5316\u63D2\u4EF6 ] ${plugin.constructor.name}`); await plugin.initialize(this).catch((e) => { logger_default.error(`[ \u521D\u59CB\u5316\u63D2\u4EF6\u5931\u8D25 ] ${plugin.constructor.name}: ${e}`); }); } } async init() { await this.waitingInitialization; } }; // utils/checker.ts async function startCheck(checkers, pass) { logger_default.info("[ \u9884\u68C0\u5F00\u59CB ]"); for (const [index, checker] of checkers.entries()) { const seq = index + 1; logger_default.info(`${seq}. ${checker.name}`); try { if (checker.skip) { logger_default.warn(`${seq}. ${checker.name} [\u8DF3\u8FC7]`); continue; } await checker.check(); logger_default.info(`${seq}. ${checker.name} [\u6210\u529F]`); } catch (error) { logger_default.error(`${seq}. ${checker.name} [\u5931\u8D25]`); throw error; } } logger_default.info("[ \u9884\u68C0\u5B8C\u6210 ]"); if (pass) await pass(); } var HonoTransport = class { constructor(url, stream, closeStream) { this.url = url; this.stream = stream; this.closeStream = closeStream; this.sessionId = ulid.ulid(); } sessionId; onclose; onerror; onmessage; async start() { await this.stream.writeSSE({ event: "endpoint", data: `${encodeURI(this.url)}?sessionId=${this.sessionId}` }); this.stream.onAbort(() => { this.close(); }); } async handleMessage(message) { let parsedMessage; try { parsedMessage = types_js.JSONRPCMessageSchema.parse(message); } catch (error) { this.onerror?.(error); throw error; } this.onmessage?.(parsedMessage); } async send(message) { await this.stream.writeln( `event: message data: ${JSON.stringify(message)} ` ); } async close() { this.onclose?.(); this.closeStream(); } }; var ModelContextProtocolPlugin = class extends Plugin { mcpServer; transports = {}; registerMcpTools(engine) { const modules = engine.getModules(true); for (const module of Object.values(modules)) { for (const action of Object.values(module.actions)) { if (action.mcp?.type !== "tool") { continue; } const args = {}; const argsIndex = {}; for (const [index, param] of (action.params ?? []).entries()) { args[param.description] = param; argsIndex[param.description] = index; } this.mcpServer.tool( `${module.name}.${action.name}`, action.description ?? "", args, async (params) => { const argsList = []; for (const [key, value] of Object.entries(params)) { argsList[argsIndex[key]] = value; } const result = await engine.getActionHandler(module.name, action.name).handle(argsList); return { content: [{ type: "text", text: JSON.stringify(result) }] }; } ); } } } initialize = async (engine) => { const app = engine.getApp(); this.mcpServer = new mcp_js.McpServer({ name: engine.options.name, version: engine.options.version }); this.registerMcpTools(engine); app.get(`${engine.options.prefix}/mcp_sse`, async (ctx) => { return streaming.streamSSE(ctx, async (stream) => { return new Promise(async (resolve) => { const transport = new HonoTransport( `${engine.options.prefix}/mcp_messages`, stream, () => { delete this.transports[transport.sessionId]; resolve(); } ); this.transports[transport.sessionId] = transport; await this.mcpServer.connect(transport); }); }); }); app.post(`${engine.options.prefix}/mcp_messages`, async (ctx) => { const sessionId = ctx.req.query("sessionId"); if (!sessionId) { return ctx.text("No transport found for sessionId", 400); } const transport = this.transports[sessionId]; const message = await ctx.req.json(); await transport.handleMessage(message); return ctx.text("Accepted", 202); }); logger_default.info( `ModelContextProtocolPlugin endpoint: ${engine.options.prefix}/mcp_sse` ); }; }; Object.defineProperty(exports, "dayjs", { enumerable: true, get: function () { return dayjs__default.default; } }); exports.Action = Action; exports.CacheAdapter = CacheAdapter; exports.MemoryCacheAdapter = MemoryCacheAdapter; exports.Microservice = Microservice; exports.ModelContextProtocolPlugin = ModelContextProtocolPlugin; exports.Module = Module; exports.Plugin = Plugin; exports.RedisCacheAdapter = RedisCacheAdapter; exports.Schedule = Schedule; exports.ScheduleMode = ScheduleMode; exports.ServiceContext = ServiceContext; exports.logger = logger_default; exports.startCheck = startCheck; Object.keys(zod).forEach(function (k) { if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, { enumerable: true, get: function () { return zod[k]; } }); });