From 076b9fac5f1575f8ae6090cbc6257143028921d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BD=A0=E7=9A=84=E7=94=A8=E6=88=B7=E5=90=8D?= <你的邮箱> Date: Sat, 8 Nov 2025 19:39:10 +0800 Subject: [PATCH] feat: add Finance MCP workflow --- .gitea/workflows/deploy-mcp.yml | 101 ++ DEPLOYMENT_LOG.md | 23 +- apps/finance-mcp-service/package.json | 19 +- .../src/client/finance-client.ts | 394 ++++++++ apps/finance-mcp-service/src/config.ts | 57 ++ .../finance-mcp-service/src/finance-client.js | 285 ------ apps/finance-mcp-service/src/index.js | 901 ------------------ apps/finance-mcp-service/src/index.ts | 35 + apps/finance-mcp-service/src/logger.ts | 12 + .../src/server/mcp-server.ts | 270 ++++++ apps/finance-mcp-service/src/tools/finance.ts | 789 +++++++++++++++ apps/finance-mcp-service/src/types.ts | 44 + apps/finance-mcp-service/src/utils/mcp.ts | 9 + .../src/utils/validation.ts | 94 ++ apps/finance-mcp-service/tsconfig.json | 17 + pnpm-lock.yaml | 287 ++++-- 16 files changed, 2069 insertions(+), 1268 deletions(-) create mode 100644 .gitea/workflows/deploy-mcp.yml create mode 100644 apps/finance-mcp-service/src/client/finance-client.ts create mode 100644 apps/finance-mcp-service/src/config.ts delete mode 100644 apps/finance-mcp-service/src/finance-client.js delete mode 100644 apps/finance-mcp-service/src/index.js create mode 100644 apps/finance-mcp-service/src/index.ts create mode 100644 apps/finance-mcp-service/src/logger.ts create mode 100644 apps/finance-mcp-service/src/server/mcp-server.ts create mode 100644 apps/finance-mcp-service/src/tools/finance.ts create mode 100644 apps/finance-mcp-service/src/types.ts create mode 100644 apps/finance-mcp-service/src/utils/mcp.ts create mode 100644 apps/finance-mcp-service/src/utils/validation.ts create mode 100644 apps/finance-mcp-service/tsconfig.json diff --git a/.gitea/workflows/deploy-mcp.yml b/.gitea/workflows/deploy-mcp.yml new file mode 100644 index 00000000..532fdfe7 --- /dev/null +++ b/.gitea/workflows/deploy-mcp.yml @@ -0,0 +1,101 @@ +name: Deploy Finance MCP Service + +on: + push: + branches: + - main + paths: + - 'apps/finance-mcp-service/**' + - 'pnpm-lock.yaml' + - 'pnpm-workspace.yaml' + workflow_dispatch: + +env: + DEPLOY_PATH: /home/atai/kt-financial-system + MCP_PACKAGE: '@vben/finance-mcp-service' + +jobs: + build-mcp: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '20' + + - name: Setup pnpm + uses: pnpm/action-setup@v2 + with: + version: 9 + + - name: Get pnpm store directory + id: pnpm-cache + run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT + + - name: Setup pnpm cache + uses: actions/cache@v3 + with: + path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} + key: ${{ runner.os }}-mcp-pnpm-${{ hashFiles('pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-mcp-pnpm- + + - name: Install dependencies (MCP only) + run: pnpm install --filter ${MCP_PACKAGE}... --frozen-lockfile + + - name: Typecheck MCP service + run: pnpm --filter ${MCP_PACKAGE} typecheck + + - name: Build MCP service + run: pnpm --filter ${MCP_PACKAGE} build + + deploy-mcp: + runs-on: ubuntu-latest + needs: build-mcp + + steps: + - name: Deploy MCP artifacts to server + uses: appleboy/ssh-action@v1.0.0 + with: + host: ${{ secrets.SERVER_HOST || '172.16.74.149' }} + username: ${{ secrets.SERVER_USER || 'atai' }} + password: ${{ secrets.SERVER_PASSWORD || 'wengewudi666808' }} + port: ${{ secrets.SERVER_PORT || '22' }} + command_timeout: 30m + script: | + set -e + + echo "🚀 部署 Finance MCP 服务" + cd /home/atai + + if [ ! -d "kt-financial-system" ]; then + echo "📥 首次部署,正在克隆仓库..." + git clone https://gitea.ktyun.cc/chenjiangjiang/kt-financial-system.git + fi + + cd ${DEPLOY_PATH} + git fetch origin main + git reset --hard origin/main + + echo "🧱 使用容器化 Node 环境构建..." + sudo docker run --rm \ + -v $(pwd):/workspace \ + -w /workspace \ + node:20-bullseye bash -lc "npm install -g pnpm@9 && pnpm install --filter ${MCP_PACKAGE}... --frozen-lockfile && pnpm --filter ${MCP_PACKAGE} build" + + echo "🗂 生成运行入口,方便手动或自动触发 MCP 服务" + cat <<'EOF' | sudo tee /home/atai/run-finance-mcp.sh >/dev/null + #!/bin/bash + set -e + cd /home/atai/kt-financial-system + exec pnpm --filter @vben/finance-mcp-service start + EOF + sudo chmod +x /home/atai/run-finance-mcp.sh + + echo "✅ MCP 服务代码已更新至 $(git rev-parse --short HEAD)" diff --git a/DEPLOYMENT_LOG.md b/DEPLOYMENT_LOG.md index 4bdabd84..4b6835df 100644 --- a/DEPLOYMENT_LOG.md +++ b/DEPLOYMENT_LOG.md @@ -51,4 +51,25 @@ --- -最后更新时间: 2025-11-06 21:30 +## 2025-11-08 部署记录 + +### Finance MCP Service 独立 CI/CD + +- **时间**: 2025-11-08 18:50 +- **版本**: main@latest +- **内容**: 新增 `.gitea/workflows/deploy-mcp.yml`,专门构建并下发 `@vben/finance-mcp-service`,不再触碰主应用容器。 + +#### 核心变更 + +1. `build-mcp` 仅安装/构建 MCP 包(`pnpm --filter @vben/finance-mcp-service`),包含 typecheck 与产物生成。 +2. `deploy-mcp` 通过 `appleboy/ssh-action` 拉取服务器最新代码,并在容器化 Node 20 环境里构建 MCP 服务,避免污染宿主 Node。 +3. 自动生成 `/home/atai/run-finance-mcp.sh`,可直接执行 `pnpm --filter @vben/finance-mcp-service start`,便于 Codex/Claude 通过 SSH 调用。 + +#### 验证 + +- `pnpm --filter @vben/finance-mcp-service build` 在 CI 与服务器双端通过。 +- 服务器路径 `/home/atai/kt-financial-system/apps/finance-mcp-service/dist` 更新至最新提交,可随时执行 `./run-finance-mcp.sh` 启动 MCP。 + +--- + +最后更新时间: 2025-11-08 18:50 diff --git a/apps/finance-mcp-service/package.json b/apps/finance-mcp-service/package.json index 56157845..8d0d04f5 100644 --- a/apps/finance-mcp-service/package.json +++ b/apps/finance-mcp-service/package.json @@ -1,12 +1,23 @@ { "name": "@vben/finance-mcp-service", - "version": "0.1.0", + "version": "0.2.0", "private": true, "type": "module", "description": "MCP service exposing Finwise Pro finance APIs", "scripts": { - "start": "node src/index.js" + "dev": "tsx watch src/index.ts", + "start": "node dist/index.js", + "build": "tsc -p tsconfig.json", + "typecheck": "tsc -p tsconfig.json --noEmit" }, - "dependencies": {}, - "devDependencies": {} + "dependencies": { + "p-queue": "^9.0.0", + "pino": "^10.1.0", + "zod": "catalog:" + }, + "devDependencies": { + "@types/node": "catalog:", + "tsx": "^4.20.6", + "typescript": "catalog:" + } } diff --git a/apps/finance-mcp-service/src/client/finance-client.ts b/apps/finance-mcp-service/src/client/finance-client.ts new file mode 100644 index 00000000..44a08965 --- /dev/null +++ b/apps/finance-mcp-service/src/client/finance-client.ts @@ -0,0 +1,394 @@ +import { Buffer } from 'node:buffer'; + +interface FinanceEnvelope { + code: number; + message?: string; + data: T; +} + +interface BasicCredentials { + username: string; + password: string; +} + +export interface FinanceClientConfig { + baseUrl: string; + apiKey?: string; + basicAuth?: BasicCredentials; + timeoutMs?: number; +} + +export interface ListTransactionsParams { + type?: string; + statuses?: string[]; + includeDeleted?: boolean; +} + +export interface ListReimbursementsParams { + type?: string; + statuses?: string[]; + includeDeleted?: boolean; +} + +export interface ListMediaParams { + limit?: number; + fileTypes?: string[]; +} + +export interface ListExchangeRatesParams { + fromCurrency?: string; + toCurrency?: string; + date?: string; +} + +export interface TelegramConfigPayload { + name?: string; + botToken?: string; + chatId?: string; + notificationTypes?: string[]; + isEnabled?: boolean; +} + +export class FinanceClient { + private readonly apiKey?: string; + + private readonly baseUrl: string; + + private readonly basicAuth?: BasicCredentials; + + private readonly timeoutMs?: number; + + constructor(config: FinanceClientConfig) { + if (!config?.baseUrl) { + throw new Error('FinanceClient requires a baseUrl'); + } + + this.baseUrl = config.baseUrl.replace(/\/$/, ''); + this.apiKey = config.apiKey; + this.basicAuth = validateBasicAuth(config.basicAuth); + this.timeoutMs = config.timeoutMs; + } + + createBudget(payload: unknown) { + return this.post('/api/finance/budgets', payload); + } + + createCategory(payload: unknown) { + return this.post('/api/finance/categories', payload); + } + + createReimbursement(payload: unknown) { + return this.post('/api/finance/reimbursements', payload); + } + + createReimbursementMedia(payload: unknown) { + return this.post('/api/finance/media', payload); + } + + createTelegramConfig(payload: TelegramConfigPayload) { + return this.post('/api/telegram/notifications', payload); + } + + createTransaction(payload: unknown) { + return this.post('/api/finance/transactions', payload); + } + + deleteBudget(id: number) { + return this.delete(`/api/finance/budgets/${id}`); + } + + deleteCategory(id: number) { + return this.delete(`/api/finance/categories/${id}`); + } + + deleteTelegramConfig(id: number) { + return this.delete(`/api/telegram/notifications/${id}`); + } + + deleteTransaction(id: number) { + return this.delete(`/api/finance/transactions/${id}`); + } + + downloadMedia(id: number) { + return this.download(`/api/finance/media/${id}/download`); + } + + getMediaById(id: number) { + return this.get(`/api/finance/media/${id}`); + } + + listAccounts(params: { currency?: string } = {}) { + return this.get('/api/finance/accounts', params); + } + + listBudgets() { + return this.get('/api/finance/budgets'); + } + + listCategories(params: { type?: string } = {}) { + return this.get('/api/finance/categories', params); + } + + listCurrencies() { + return this.get('/api/finance/currencies'); + } + + listExchangeRates(params: ListExchangeRatesParams = {}) { + const query: Record = {}; + if (params.fromCurrency) query.from = params.fromCurrency; + if (params.toCurrency) query.to = params.toCurrency; + if (params.date) query.date = params.date; + return this.get('/api/finance/exchange-rates', query); + } + + listMedia(params: ListMediaParams = {}) { + const query: Record = {}; + if (typeof params.limit === 'number') query.limit = params.limit; + if (params.fileTypes?.length) query.types = params.fileTypes.join(','); + return this.get('/api/finance/media', query); + } + + listReimbursements(params: ListReimbursementsParams = {}) { + const query: Record = {}; + if (params.type) query.type = params.type; + if (params.statuses?.length) query.statuses = params.statuses.join(','); + if (params.includeDeleted !== undefined) + query.includeDeleted = params.includeDeleted; + return this.get('/api/finance/reimbursements', query); + } + + listTelegramConfigs() { + return this.get('/api/telegram/notifications'); + } + + listTransactions(params: ListTransactionsParams = {}) { + const query: Record = {}; + if (params.type) query.type = params.type; + if (params.statuses?.length) query.statuses = params.statuses.join(','); + if (params.includeDeleted !== undefined) + query.includeDeleted = params.includeDeleted; + return this.get('/api/finance/transactions', query); + } + + testTelegramConfig(payload: { botToken: string; chatId: string }) { + return this.post('/api/telegram/test', payload); + } + + updateBudget(id: number, payload: unknown) { + return this.put(`/api/finance/budgets/${id}`, payload); + } + + updateCategory(id: number, payload: unknown) { + return this.put(`/api/finance/categories/${id}`, payload); + } + + updateReimbursement(id: number, payload: unknown) { + return this.put(`/api/finance/reimbursements/${id}`, payload); + } + + updateTelegramConfig(id: number, payload: TelegramConfigPayload) { + return this.put(`/api/telegram/notifications/${id}`, payload); + } + + updateTransaction(id: number, payload: unknown) { + return this.put(`/api/finance/transactions/${id}`, payload); + } + + private buildHeaders(json: boolean) { + const headers: Record = { Accept: 'application/json' }; + if (json) headers['Content-Type'] = 'application/json'; + if (this.apiKey) headers.Authorization = `Bearer ${this.apiKey}`; + else if (this.basicAuth) + headers.Authorization = `Basic ${createBasicToken(this.basicAuth)}`; + return headers; + } + + private createUrl(path: string) { + const normalized = path.startsWith('/') ? path : `/${path}`; + return new URL(normalized, this.baseUrl); + } + + private async delete(path: string) { + return this.request('DELETE', path); + } + + private async download(path: string) { + const url = this.createUrl(path); + const response = await this.performFetch(url, { + method: 'GET', + headers: this.buildHeaders(false), + }); + + if (!response.ok) { + const payload = + await this.safeParseEnvelope>(response); + if (payload) { + throw new Error(payload.message || 'Failed to download media file'); + } + throw new Error( + `Failed to download media file (HTTP ${response.status})`, + ); + } + + const arrayBuffer = await response.arrayBuffer(); + const buffer = Buffer.from(arrayBuffer); + + return { + fileName: this.extractFileName( + response.headers.get('content-disposition'), + ), + mimeType: + response.headers.get('content-type') ?? 'application/octet-stream', + size: buffer.byteLength, + base64: buffer.toString('base64'), + }; + } + + private extractFileName(contentDisposition: null | string) { + if (!contentDisposition) return undefined; + + const filenameStar = contentDisposition.match(/filename\*=([^;]+)/i); + if (filenameStar?.[1]) { + const value = filenameStar[1].replace(/^UTF-8''/, ''); + try { + return decodeURIComponent(value); + } catch { + return value; + } + } + + const filename = contentDisposition.match(/filename="?([^";]+)"?/i); + return filename?.[1]; + } + + private async get(path: string, query?: Record) { + return this.request('GET', path, { query }); + } + + private async parseEnvelope(response: Response, path: string) { + const payload = + await this.safeParseEnvelope>(response); + + if (!payload) { + const text = await response.text(); + throw new Error( + `Unexpected response from ${path}: ${text || response.statusText}`, + ); + } + + if (!response.ok) { + throw new Error( + payload.message || + `Finance API request failed (HTTP ${response.status})`, + ); + } + + return payload; + } + + private async performFetch(url: URL, init: RequestInit) { + const controller = this.timeoutMs ? new AbortController() : undefined; + let timer: NodeJS.Timeout | undefined; + + if (controller) { + init.signal = controller.signal; + timer = setTimeout(() => controller.abort(), this.timeoutMs); + } + + try { + return await fetch(url, init); + } catch (error: unknown) { + if ((error as Error)?.name === 'AbortError') { + throw new Error(`Request to ${url.pathname} timed out`); + } + throw error; + } finally { + if (timer) clearTimeout(timer); + } + } + + private async post(path: string, body?: unknown) { + return this.request('POST', path, { body }); + } + + private async put(path: string, body?: unknown) { + return this.request('PUT', path, { body }); + } + + private async request( + method: 'DELETE' | 'GET' | 'POST' | 'PUT', + path: string, + options: { + body?: unknown; + query?: Record; + } = {}, + ) { + const url = this.createUrl(path); + + if (options.query) { + for (const [key, value] of Object.entries(options.query)) { + if (value === undefined || value === null) continue; + if (Array.isArray(value)) { + if (value.length > 0) url.searchParams.set(key, value.join(',')); + } else if (typeof value === 'boolean') { + url.searchParams.set(key, value ? 'true' : 'false'); + } else { + url.searchParams.set(key, String(value)); + } + } + } + + const response = await this.performFetch(url, { + method, + headers: this.buildHeaders(method !== 'GET' && method !== 'DELETE'), + body: options.body ? JSON.stringify(options.body) : undefined, + }); + + const payload = await this.parseEnvelope(response, path); + if (payload.code !== 0) { + throw new Error(payload.message || 'Finance API returned an error'); + } + + return payload.data; + } + + private async safeParseEnvelope(response: Response) { + const contentType = response.headers.get('content-type') || ''; + if (!contentType.includes('application/json')) return null; + + try { + return (await response.clone().json()) as T; + } catch { + return null; + } + } +} + +type BasicAuthLike = Partial< + BasicCredentials & { + login: string; + pass: string; + user: string; + } +>; + +const validateBasicAuth = ( + credentials?: BasicAuthLike | null, +): BasicCredentials | undefined => { + if (!credentials) return undefined; + const username = + credentials.username ?? credentials.user ?? credentials.login; + const password = credentials.password ?? credentials.pass; + + if (!username && !password) return undefined; + if (!username || !password) { + throw new Error( + 'FinanceClient basicAuth requires both username and password', + ); + } + + return { username: String(username), password: String(password) }; +}; + +const createBasicToken = ({ username, password }: BasicCredentials) => + Buffer.from(`${username}:${password}`, 'utf8').toString('base64'); diff --git a/apps/finance-mcp-service/src/config.ts b/apps/finance-mcp-service/src/config.ts new file mode 100644 index 00000000..ea222034 --- /dev/null +++ b/apps/finance-mcp-service/src/config.ts @@ -0,0 +1,57 @@ +import process from 'node:process'; + +import { z } from 'zod'; + +const DEFAULT_BASE_URL = 'http://172.16.74.149:5666'; + +const EnvSchema = z.object({ + FINANCE_API_BASE_URL: z.string().trim().optional(), + FINANCE_API_KEY: z.string().trim().optional(), + FINANCE_API_TIMEOUT: z.string().trim().optional(), + FINANCE_BASIC_USERNAME: z.string().optional(), + FINANCE_BASIC_PASSWORD: z.string().optional(), + FINANCE_BASIC_USER: z.string().optional(), + FINANCE_USERNAME: z.string().optional(), + FINANCE_PASSWORD: z.string().optional(), + FINANCE_MCP_MAX_CONCURRENCY: z.string().trim().optional(), +}); + +const parsed = EnvSchema.parse(process.env); + +const baseUrl = + parsed.FINANCE_API_BASE_URL && parsed.FINANCE_API_BASE_URL.length > 0 + ? parsed.FINANCE_API_BASE_URL + : DEFAULT_BASE_URL; + +const timeoutMs = parsed.FINANCE_API_TIMEOUT + ? Number.parseInt(parsed.FINANCE_API_TIMEOUT, 10) + : undefined; + +const maxConcurrencyRaw = parsed.FINANCE_MCP_MAX_CONCURRENCY + ? Number.parseInt(parsed.FINANCE_MCP_MAX_CONCURRENCY, 10) + : undefined; + +const maxConcurrency = + Number.isFinite(maxConcurrencyRaw ?? Number.NaN) && + (maxConcurrencyRaw ?? 0) > 0 + ? (maxConcurrencyRaw as number) + : 4; + +const username = + parsed.FINANCE_BASIC_USERNAME ?? + parsed.FINANCE_BASIC_USER ?? + parsed.FINANCE_USERNAME ?? + null; +const password = + parsed.FINANCE_BASIC_PASSWORD ?? parsed.FINANCE_PASSWORD ?? null; + +export const config = { + baseUrl, + apiKey: parsed.FINANCE_API_KEY, + timeoutMs: + Number.isFinite(timeoutMs ?? Number.NaN) && (timeoutMs ?? 0) > 0 + ? (timeoutMs as number) + : undefined, + maxConcurrency, + basicAuth: username && password ? { username, password } : undefined, +}; diff --git a/apps/finance-mcp-service/src/finance-client.js b/apps/finance-mcp-service/src/finance-client.js deleted file mode 100644 index 59f39e37..00000000 --- a/apps/finance-mcp-service/src/finance-client.js +++ /dev/null @@ -1,285 +0,0 @@ -import { Buffer } from 'node:buffer'; - -export class FinanceClient { - constructor(config) { - if (!config?.baseUrl) { - throw new Error('FinanceClient requires a baseUrl'); - } - - this.baseUrl = config.baseUrl.replace(/\/$/, ''); - this.apiKey = config.apiKey; - this.basicAuth = validateBasicAuth(config.basicAuth); - this.timeoutMs = config.timeoutMs; - } - - async listAccounts(params = {}) { - return this.get('/api/finance/accounts', params); - } - - async listBudgets() { - return this.get('/api/finance/budgets'); - } - - async createBudget(payload) { - return this.post('/api/finance/budgets', payload); - } - - async updateBudget(id, payload) { - return this.put(`/api/finance/budgets/${id}`, payload); - } - - async deleteBudget(id) { - return this.delete(`/api/finance/budgets/${id}`); - } - - async listCategories(params = {}) { - return this.get('/api/finance/categories', params); - } - - async createCategory(payload) { - return this.post('/api/finance/categories', payload); - } - - async updateCategory(id, payload) { - return this.put(`/api/finance/categories/${id}`, payload); - } - - async deleteCategory(id) { - return this.delete(`/api/finance/categories/${id}`); - } - - async listCurrencies() { - return this.get('/api/finance/currencies'); - } - - async listExchangeRates(params = {}) { - const query = {}; - if (params.fromCurrency) query.from = params.fromCurrency; - if (params.toCurrency) query.to = params.toCurrency; - if (params.date) query.date = params.date; - return this.get('/api/finance/exchange-rates', query); - } - - async listTransactions(params = {}) { - const query = {}; - if (params.type) query.type = params.type; - if (params.statuses?.length) { - query.statuses = params.statuses.join(','); - } - if (params.includeDeleted !== undefined) { - query.includeDeleted = params.includeDeleted; - } - return this.get('/api/finance/transactions', query); - } - - async createTransaction(payload) { - return this.post('/api/finance/transactions', payload); - } - - async updateTransaction(id, payload) { - return this.put(`/api/finance/transactions/${id}`, payload); - } - - async deleteTransaction(id) { - return this.delete(`/api/finance/transactions/${id}`); - } - - async listReimbursements(params = {}) { - const query = {}; - if (params.type) query.type = params.type; - if (params.statuses?.length) { - query.statuses = params.statuses.join(','); - } - if (params.includeDeleted !== undefined) { - query.includeDeleted = params.includeDeleted; - } - return this.get('/api/finance/reimbursements', query); - } - - async createReimbursement(payload) { - return this.post('/api/finance/reimbursements', payload); - } - - async updateReimbursement(id, payload) { - return this.put(`/api/finance/reimbursements/${id}`, payload); - } - - async listMedia(params = {}) { - const query = {}; - if (params.limit !== undefined) query.limit = params.limit; - if (params.fileTypes?.length) query.types = params.fileTypes.join(','); - return this.get('/api/finance/media', query); - } - - async getMediaById(id) { - return this.get(`/api/finance/media/${id}`); - } - - async downloadMedia(id) { - const url = this.createUrl(`/api/finance/media/${id}/download`); - const response = await this.performFetch(url, { - method: 'GET', - headers: this.buildHeaders(false), - }); - - if (!response.ok) { - const payload = await this.safeParseEnvelope(response); - if (payload) { - throw new Error(payload.message || 'Failed to download media file'); - } - throw new Error(`Failed to download media file (HTTP ${response.status})`); - } - - const arrayBuffer = await response.arrayBuffer(); - const buffer = Buffer.from(arrayBuffer); - - return { - fileName: this.extractFileName(response.headers.get('content-disposition')), - mimeType: response.headers.get('content-type') ?? 'application/octet-stream', - size: buffer.byteLength, - base64: buffer.toString('base64'), - }; - } - - async get(path, query) { - return this.request('GET', path, { query }); - } - - async post(path, body) { - return this.request('POST', path, { body }); - } - - async put(path, body) { - return this.request('PUT', path, { body }); - } - - async delete(path) { - return this.request('DELETE', path); - } - - async request(method, path, options = {}) { - const url = this.createUrl(path); - - if (options.query) { - for (const [key, value] of Object.entries(options.query)) { - if (value === undefined || value === null) continue; - if (Array.isArray(value)) { - if (value.length > 0) url.searchParams.set(key, value.join(',')); - } else if (typeof value === 'boolean') { - url.searchParams.set(key, value ? 'true' : 'false'); - } else { - url.searchParams.set(key, String(value)); - } - } - } - - const response = await this.performFetch(url, { - method, - headers: this.buildHeaders(method !== 'GET' && method !== 'DELETE'), - body: options.body ? JSON.stringify(options.body) : undefined, - }); - - const payload = await this.parseEnvelope(response, path); - - if (payload.code !== 0) { - throw new Error(payload.message || 'Finance API returned an error'); - } - - return payload.data; - } - - createUrl(path) { - if (!path.startsWith('/')) { - path = `/${path}`; - } - return new URL(path, this.baseUrl); - } - - buildHeaders(json) { - const headers = { Accept: 'application/json' }; - if (json) headers['Content-Type'] = 'application/json'; - if (this.apiKey) headers.Authorization = `Bearer ${this.apiKey}`; - else if (this.basicAuth) headers.Authorization = `Basic ${createBasicToken(this.basicAuth)}`; - return headers; - } - - async performFetch(url, init) { - const controller = this.timeoutMs ? new AbortController() : undefined; - let timer; - - if (controller) { - init.signal = controller.signal; - timer = setTimeout(() => controller.abort(), this.timeoutMs); - } - - try { - return await fetch(url, init); - } catch (error) { - if (error?.name === 'AbortError') { - throw new Error(`Request to ${url.pathname} timed out`); - } - throw error; - } finally { - if (timer) clearTimeout(timer); - } - } - - async parseEnvelope(response, path) { - const payload = await this.safeParseEnvelope(response); - - if (!payload) { - const text = await response.text(); - throw new Error(`Unexpected response from ${path}: ${text || response.statusText}`); - } - - if (!response.ok) { - throw new Error(payload.message || `Finance API request failed (HTTP ${response.status})`); - } - - return payload; - } - - async safeParseEnvelope(response) { - const contentType = response.headers.get('content-type') || ''; - if (!contentType.includes('application/json')) return null; - - try { - return await response.clone().json(); - } catch { - return null; - } - } - - extractFileName(contentDisposition) { - if (!contentDisposition) return undefined; - - const filenameStar = contentDisposition.match(/filename\*=([^;]+)/i); - if (filenameStar?.[1]) { - const value = filenameStar[1].replace(/^UTF-8''/, ''); - try { - return decodeURIComponent(value); - } catch { - return value; - } - } - - const filename = contentDisposition.match(/filename="?([^";]+)"?/i); - return filename?.[1]; - } -} - -const validateBasicAuth = (credentials) => { - if (!credentials) return undefined; - const username = credentials.username ?? credentials.user ?? credentials.login; - const password = credentials.password ?? credentials.pass; - - if (!username && !password) return undefined; - if (!username || !password) { - throw new Error('FinanceClient basicAuth requires both username and password'); - } - - return { username: String(username), password: String(password) }; -}; - -const createBasicToken = ({ username, password }) => - Buffer.from(`${username}:${password}`, 'utf8').toString('base64'); diff --git a/apps/finance-mcp-service/src/index.js b/apps/finance-mcp-service/src/index.js deleted file mode 100644 index a1e4c15b..00000000 --- a/apps/finance-mcp-service/src/index.js +++ /dev/null @@ -1,901 +0,0 @@ -import process from 'node:process'; - -import { FinanceClient } from './finance-client.js'; - -process.on('exit', (code) => { - process.stderr.write(`[finwise-finance] process exit with code ${code}\n`); -}); -process.on('uncaughtException', (error) => { - process.stderr.write(`[finwise-finance] uncaughtException: ${error.stack ?? error.message}\n`); -}); -process.on('unhandledRejection', (reason) => { - process.stderr.write(`[finwise-finance] unhandledRejection: ${reason}\n`); -}); - -class McpServer { - constructor(options) { - this.options = options; - this.tools = new Map(); - this.metadata = []; - this.buffer = ''; - this.expectedLength = null; - this.initialized = false; - - for (const tool of options.tools) { - if (this.tools.has(tool.name)) { - throw new Error(`Duplicate MCP tool name: ${tool.name}`); - } - this.tools.set(tool.name, tool); - this.metadata.push({ - name: tool.name, - description: tool.description, - inputSchema: tool.inputSchema, - ...(tool.outputSchema ? { outputSchema: tool.outputSchema } : {}), - }); - } - } - - start() { - process.stdin.setEncoding('utf8'); - process.stdin.on('data', (chunk) => { - this.buffer += chunk; - void this.drain(); - }); - process.stdin.on('end', () => { - this.log('stdin ended'); - }); - process.stdin.on('close', () => { - this.log('stdin closed'); - }); - process.stdin.resume(); - this.log('MCP service ready'); - } - - write(payload) { - const json = JSON.stringify(payload); - const frame = `Content-Length: ${Buffer.byteLength(json, 'utf8')}\r\n\r\n${json}`; - process.stdout.write(frame); - } - - respond(id, result) { - this.log(`responding to ${id} with result`); - if (id === undefined) return; - this.write({ jsonrpc: '2.0', id, result }); - } - - respondError(id, code, message) { - this.log(`responding error to ${id}: [${code}] ${message}`); - if (id === undefined) return; - this.write({ jsonrpc: '2.0', id, error: { code, message } }); - } - - notify(method, params) { - this.log(`notifying ${method}`); - this.write({ jsonrpc: '2.0', method, params }); - } - - async drain() { - while (true) { - if (this.expectedLength === null) { - const headerEnd = this.buffer.indexOf('\r\n\r\n'); - if (headerEnd === -1) return; - const header = this.buffer.slice(0, headerEnd); - const match = header.match(/content-length:\s*(\d+)/i); - if (!match) { - this.buffer = this.buffer.slice(headerEnd + 4); - continue; - } - this.expectedLength = Number.parseInt(match[1], 10); - this.buffer = this.buffer.slice(headerEnd + 4); - } - - if (this.buffer.length < (this.expectedLength ?? 0)) return; - - const body = this.buffer.slice(0, this.expectedLength ?? 0); - this.buffer = this.buffer.slice(this.expectedLength ?? 0); - this.expectedLength = null; - - await this.handleMessage(body); - } - } - - async handleMessage(payload) { - this.log(`received payload: ${payload}`); - let request; - try { - request = JSON.parse(payload); - } catch { - this.respondError(null, -32700, 'Parse error'); - return; - } - - if (!request || request.jsonrpc !== '2.0' || typeof request.method !== 'string') { - this.respondError(request?.id, -32600, 'Invalid Request'); - return; - } - - try { - await this.dispatch(request); - } catch (error) { - this.log(`Unexpected error: ${error.message}`); - this.respondError(request.id, -32000, error.message); - } - } - - async dispatch(request) { - switch (request.method) { - case 'initialize': { - if (this.initialized) { - this.respondError(request.id, -32600, 'Already initialized'); - return; - } - this.initialized = true; - this.respond(request.id, { - protocolVersion: '2024-10-07', - capabilities: { tools: { list: true, call: true } }, - service: { - name: this.options.name, - version: this.options.version, - description: this.options.description, - }, - }); - this.notify('notifications/ready', {}); - return; - } - - case 'tools/list': { - this.assertInitialized('tools/list'); - this.respond(request.id, { tools: this.metadata }); - return; - } - - case 'tools/call': { - this.assertInitialized('tools/call'); - const params = request.params ?? {}; - const toolName = params.name; - if (!toolName || typeof toolName !== 'string') { - this.respondError(request.id, -32602, 'Tool name is required'); - return; - } - const tool = this.tools.get(toolName); - if (!tool) { - this.respondError(request.id, -32601, `Unknown tool: ${toolName}`); - return; - } - try { - const result = await tool.handler(params.arguments ?? {}); - this.respond(request.id, result); - } catch (error) { - this.respondError(request.id, -32001, error.message); - } - return; - } - - case 'ping': { - this.respond(request.id, 'pong'); - return; - } - - case 'shutdown': { - this.respond(request.id, null); - process.nextTick(() => process.exit(0)); - return; - } - - default: { - this.respondError(request.id, -32601, `Method not found: ${request.method}`); - } - } - } - - assertInitialized(method) { - if (!this.initialized) { - throw new Error(`Received ${method} before initialize`); - } - } - - log(message) { - process.stderr.write(`[${this.options.name}] ${message}\n`); - } -} - -const jsonResult = (data) => ({ - content: [ - { - type: 'application/json', - data, - }, - ], -}); - -const ensureNumber = (value, field) => { - if (typeof value === 'number' && Number.isFinite(value)) return value; - if (typeof value === 'string' && value.trim()) { - const parsed = Number(value); - if (!Number.isNaN(parsed)) return parsed; - } - throw new Error(`${field} must be a number`); -}; - -const optionalNumber = (value, field) => { - if (value === undefined || value === null) return undefined; - return ensureNumber(value, field); -}; - -const optionalNullableNumber = (value, field) => { - if (value === undefined) return undefined; - if (value === null) return null; - if (typeof value === 'string') { - const normalized = value.trim().toLowerCase(); - if (!normalized || normalized === 'null') return null; - } - return ensureNumber(value, field); -}; - -const ensureString = (value, field) => { - if (typeof value === 'string') { - const trimmed = value.trim(); - if (!trimmed) throw new Error(`${field} cannot be empty`); - return trimmed; - } - if (value === undefined || value === null) throw new Error(`${field} is required`); - return ensureString(String(value), field); -}; - -const optionalString = (value) => { - if (value === undefined || value === null) return undefined; - return String(value); -}; - -const optionalNullableString = (value) => { - if (value === undefined) return undefined; - if (value === null) return null; - const normalized = String(value).trim(); - if (normalized.toLowerCase() === 'null') return null; - return normalized; -}; - -const optionalBoolean = (value, field) => { - if (value === undefined || value === null) return undefined; - if (typeof value === 'boolean') return value; - if (typeof value === 'number') return value !== 0; - if (typeof value === 'string') { - const normalized = value.trim().toLowerCase(); - if (['true', '1', 'yes', 'y'].includes(normalized)) return true; - if (['false', '0', 'no', 'n'].includes(normalized)) return false; - } - throw new Error(`${field} must be boolean`); -}; - -const parseStringArray = (value) => { - if (value === undefined || value === null) return undefined; - let items = []; - if (Array.isArray(value)) { - items = value.map((item) => String(item).trim()).filter(Boolean); - } else if (typeof value === 'string') { - items = value.split(',').map((item) => item.trim()).filter(Boolean); - } else { - items = [String(value).trim()].filter(Boolean); - } - return items.length ? items : undefined; -}; - -const buildTransactionCreatePayload = (args, options = {}) => { - const payload = { - type: optionalString(args?.type) ?? options.defaultType ?? ensureString(args?.type, 'type'), - amount: ensureNumber(args?.amount, 'amount'), - currency: optionalString(args?.currency) ?? 'CNY', - transactionDate: ensureString(args?.transactionDate, 'transactionDate'), - }; - - const categoryId = optionalNullableNumber(args?.categoryId, 'categoryId'); - if (categoryId !== undefined) payload.categoryId = categoryId; - - const accountId = optionalNullableNumber(args?.accountId, 'accountId'); - if (accountId !== undefined) payload.accountId = accountId; - - const description = optionalString(args?.description); - if (description !== undefined) payload.description = description; - - const project = optionalNullableString(args?.project); - if (project !== undefined) payload.project = project; - - const memo = optionalNullableString(args?.memo); - if (memo !== undefined) payload.memo = memo; - - const status = optionalString(args?.status); - if (status !== undefined) payload.status = status; - - const reimbursementBatch = optionalNullableString(args?.reimbursementBatch); - if (reimbursementBatch !== undefined) payload.reimbursementBatch = reimbursementBatch; - - const reviewNotes = optionalNullableString(args?.reviewNotes); - if (reviewNotes !== undefined) payload.reviewNotes = reviewNotes; - - const submittedBy = optionalNullableString(args?.submittedBy); - if (submittedBy !== undefined) payload.submittedBy = submittedBy; - - const approvedBy = optionalNullableString(args?.approvedBy); - if (approvedBy !== undefined) payload.approvedBy = approvedBy; - - const approvedAt = optionalNullableString(args?.approvedAt); - if (approvedAt !== undefined) payload.approvedAt = approvedAt; - - const statusUpdatedAt = optionalNullableString(args?.statusUpdatedAt); - if (statusUpdatedAt !== undefined) payload.statusUpdatedAt = statusUpdatedAt; - - const isDeleted = optionalBoolean(args?.isDeleted, 'isDeleted'); - if (isDeleted !== undefined) payload.isDeleted = isDeleted; - - return payload; -}; - -const buildTransactionUpdatePayload = (args) => { - const payload = {}; - - if (args?.type !== undefined) payload.type = ensureString(args.type, 'type'); - if (args?.amount !== undefined) payload.amount = ensureNumber(args.amount, 'amount'); - if (args?.currency !== undefined) payload.currency = ensureString(args.currency, 'currency'); - if (args?.transactionDate !== undefined) payload.transactionDate = ensureString(args.transactionDate, 'transactionDate'); - if (args?.categoryId !== undefined) payload.categoryId = optionalNullableNumber(args.categoryId, 'categoryId'); - if (args?.accountId !== undefined) payload.accountId = optionalNullableNumber(args.accountId, 'accountId'); - if (args?.description !== undefined) payload.description = args.description === null ? '' : String(args.description); - if (args?.project !== undefined) payload.project = optionalNullableString(args.project) ?? null; - if (args?.memo !== undefined) payload.memo = optionalNullableString(args.memo) ?? null; - if (args?.status !== undefined) payload.status = ensureString(args.status, 'status'); - if (args?.statusUpdatedAt !== undefined) payload.statusUpdatedAt = ensureString(args.statusUpdatedAt, 'statusUpdatedAt'); - if (args?.reimbursementBatch !== undefined) payload.reimbursementBatch = optionalNullableString(args.reimbursementBatch) ?? null; - if (args?.reviewNotes !== undefined) payload.reviewNotes = optionalNullableString(args.reviewNotes) ?? null; - if (args?.submittedBy !== undefined) payload.submittedBy = optionalNullableString(args.submittedBy) ?? null; - if (args?.approvedBy !== undefined) payload.approvedBy = optionalNullableString(args.approvedBy) ?? null; - if (args?.approvedAt !== undefined) payload.approvedAt = optionalNullableString(args.approvedAt) ?? null; - const isDeleted = optionalBoolean(args?.isDeleted, 'isDeleted'); - if (isDeleted !== undefined) payload.isDeleted = isDeleted; - - return payload; -}; - -const createFinanceTools = (client) => { - const tools = []; - - tools.push({ - name: 'finance_list_accounts', - description: '列出账户,可选货币过滤', - inputSchema: { - type: 'object', - additionalProperties: false, - properties: { - currency: { type: 'string', description: 'ISO 4217 货币代码' }, - }, - }, - handler: async (args) => { - const currency = optionalString(args?.currency); - return jsonResult(await client.listAccounts(currency ? { currency } : {})); - }, - }); - - tools.push({ - name: 'finance_list_budgets', - description: '查询预算列表', - inputSchema: { type: 'object', additionalProperties: false, properties: {} }, - handler: async () => jsonResult(await client.listBudgets()), - }); - - tools.push({ - name: 'finance_create_budget', - description: '创建预算', - inputSchema: { - type: 'object', - additionalProperties: false, - required: ['category', 'categoryId', 'limit', 'currency', 'period'], - properties: { - category: { type: 'string' }, - categoryId: { type: 'number' }, - emoji: { type: 'string' }, - limit: { type: 'number' }, - spent: { type: 'number' }, - remaining: { type: 'number' }, - percentage: { type: 'number' }, - currency: { type: 'string' }, - period: { type: 'string' }, - alertThreshold: { type: 'number' }, - description: { type: 'string' }, - autoRenew: { type: 'boolean' }, - overspendAlert: { type: 'boolean' }, - dailyReminder: { type: 'boolean' }, - monthlyTrend: { type: 'number' }, - isDeleted: { type: 'boolean' }, - }, - }, - handler: async (args) => { - const payload = { - category: ensureString(args?.category, 'category'), - categoryId: ensureNumber(args?.categoryId, 'categoryId'), - emoji: optionalString(args?.emoji), - limit: ensureNumber(args?.limit, 'limit'), - spent: optionalNumber(args?.spent, 'spent'), - remaining: optionalNumber(args?.remaining, 'remaining'), - percentage: optionalNumber(args?.percentage, 'percentage'), - currency: ensureString(args?.currency, 'currency'), - period: ensureString(args?.period, 'period'), - alertThreshold: optionalNumber(args?.alertThreshold, 'alertThreshold'), - description: optionalString(args?.description), - autoRenew: optionalBoolean(args?.autoRenew, 'autoRenew'), - overspendAlert: optionalBoolean(args?.overspendAlert, 'overspendAlert'), - dailyReminder: optionalBoolean(args?.dailyReminder, 'dailyReminder'), - monthlyTrend: optionalNumber(args?.monthlyTrend, 'monthlyTrend'), - isDeleted: optionalBoolean(args?.isDeleted, 'isDeleted'), - }; - return jsonResult(await client.createBudget(payload)); - }, - }); - - tools.push({ - name: 'finance_update_budget', - description: '更新预算', - inputSchema: { - type: 'object', - additionalProperties: false, - required: ['id'], - properties: { - id: { type: 'number' }, - category: { type: 'string' }, - categoryId: { type: 'number' }, - emoji: { type: 'string' }, - limit: { type: 'number' }, - spent: { type: 'number' }, - remaining: { type: 'number' }, - percentage: { type: 'number' }, - currency: { type: 'string' }, - period: { type: 'string' }, - alertThreshold: { type: 'number' }, - description: { type: 'string' }, - autoRenew: { type: 'boolean' }, - overspendAlert: { type: 'boolean' }, - dailyReminder: { type: 'boolean' }, - monthlyTrend: { type: 'number' }, - isDeleted: { type: 'boolean' }, - }, - }, - handler: async (args) => { - const id = ensureNumber(args?.id, 'id'); - const payload = {}; - if (args?.category !== undefined) payload.category = ensureString(args.category, 'category'); - if (args?.categoryId !== undefined) payload.categoryId = ensureNumber(args.categoryId, 'categoryId'); - if (args?.emoji !== undefined) payload.emoji = optionalString(args.emoji); - if (args?.limit !== undefined) payload.limit = ensureNumber(args.limit, 'limit'); - if (args?.spent !== undefined) payload.spent = ensureNumber(args.spent, 'spent'); - if (args?.remaining !== undefined) payload.remaining = ensureNumber(args.remaining, 'remaining'); - if (args?.percentage !== undefined) payload.percentage = ensureNumber(args.percentage, 'percentage'); - if (args?.currency !== undefined) payload.currency = ensureString(args.currency, 'currency'); - if (args?.period !== undefined) payload.period = ensureString(args.period, 'period'); - if (args?.alertThreshold !== undefined) payload.alertThreshold = ensureNumber(args.alertThreshold, 'alertThreshold'); - if (args?.description !== undefined) payload.description = optionalString(args.description); - const autoRenew = optionalBoolean(args?.autoRenew, 'autoRenew'); - if (autoRenew !== undefined) payload.autoRenew = autoRenew; - const overspendAlert = optionalBoolean(args?.overspendAlert, 'overspendAlert'); - if (overspendAlert !== undefined) payload.overspendAlert = overspendAlert; - const dailyReminder = optionalBoolean(args?.dailyReminder, 'dailyReminder'); - if (dailyReminder !== undefined) payload.dailyReminder = dailyReminder; - if (args?.monthlyTrend !== undefined) payload.monthlyTrend = ensureNumber(args.monthlyTrend, 'monthlyTrend'); - const isDeleted = optionalBoolean(args?.isDeleted, 'isDeleted'); - if (isDeleted !== undefined) payload.isDeleted = isDeleted; - return jsonResult(await client.updateBudget(id, payload)); - }, - }); - - tools.push({ - name: 'finance_delete_budget', - description: '删除预算(软删)', - inputSchema: { - type: 'object', - additionalProperties: false, - required: ['id'], - properties: { id: { type: 'number' } }, - }, - handler: async (args) => jsonResult(await client.deleteBudget(ensureNumber(args?.id, 'id'))), - }); - - tools.push({ - name: 'finance_list_categories', - description: '查询分类,可按类型过滤', - inputSchema: { - type: 'object', - additionalProperties: false, - properties: { - type: { type: 'string', description: 'expense / income' }, - }, - }, - handler: async (args) => { - const type = optionalString(args?.type); - return jsonResult(await client.listCategories(type ? { type } : {})); - }, - }); - - tools.push({ - name: 'finance_create_category', - description: '创建分类', - inputSchema: { - type: 'object', - additionalProperties: false, - required: ['name', 'type'], - properties: { - name: { type: 'string' }, - type: { type: 'string' }, - icon: { type: 'string' }, - color: { type: 'string' }, - }, - }, - handler: async (args) => jsonResult( - await client.createCategory({ - name: ensureString(args?.name, 'name'), - type: ensureString(args?.type, 'type'), - icon: optionalString(args?.icon), - color: optionalString(args?.color), - }), - ), - }); - - tools.push({ - name: 'finance_update_category', - description: '更新分类', - inputSchema: { - type: 'object', - additionalProperties: false, - required: ['id'], - properties: { - id: { type: 'number' }, - name: { type: 'string' }, - icon: { type: 'string' }, - color: { type: 'string' }, - isActive: { type: 'boolean' }, - }, - }, - handler: async (args) => { - const id = ensureNumber(args?.id, 'id'); - const payload = {}; - if (args?.name !== undefined) payload.name = ensureString(args.name, 'name'); - if (args?.icon !== undefined) payload.icon = optionalString(args.icon); - if (args?.color !== undefined) payload.color = optionalString(args.color); - const isActive = optionalBoolean(args?.isActive, 'isActive'); - if (isActive !== undefined) payload.isActive = isActive; - return jsonResult(await client.updateCategory(id, payload)); - }, - }); - - tools.push({ - name: 'finance_delete_category', - description: '删除分类(软删)', - inputSchema: { - type: 'object', - additionalProperties: false, - required: ['id'], - properties: { id: { type: 'number' } }, - }, - handler: async (args) => jsonResult(await client.deleteCategory(ensureNumber(args?.id, 'id'))), - }); - - tools.push({ - name: 'finance_list_currencies', - description: '列出可用货币', - inputSchema: { type: 'object', additionalProperties: false, properties: {} }, - handler: async () => jsonResult(await client.listCurrencies()), - }); - - tools.push({ - name: 'finance_list_exchange_rates', - description: '查询汇率', - inputSchema: { - type: 'object', - additionalProperties: false, - properties: { - fromCurrency: { type: 'string' }, - toCurrency: { type: 'string' }, - date: { type: 'string' }, - }, - }, - handler: async (args) => { - const params = {}; - if (args?.fromCurrency !== undefined) params.fromCurrency = ensureString(args.fromCurrency, 'fromCurrency'); - if (args?.toCurrency !== undefined) params.toCurrency = ensureString(args.toCurrency, 'toCurrency'); - if (args?.date !== undefined) params.date = ensureString(args.date, 'date'); - return jsonResult(await client.listExchangeRates(params)); - }, - }); - - tools.push({ - name: 'finance_list_transactions', - description: '查询交易列表', - inputSchema: { - type: 'object', - additionalProperties: false, - properties: { - type: { type: 'string' }, - statuses: { type: ['array', 'string'], items: { type: 'string' } }, - includeDeleted: { type: 'boolean' }, - }, - }, - handler: async (args) => { - const type = optionalString(args?.type); - const statuses = parseStringArray(args?.statuses); - const includeDeleted = optionalBoolean(args?.includeDeleted, 'includeDeleted'); - return jsonResult( - await client.listTransactions({ - ...(type ? { type } : {}), - ...(statuses ? { statuses } : {}), - ...(includeDeleted !== undefined ? { includeDeleted } : {}), - }), - ); - }, - }); - - tools.push({ - name: 'finance_create_transaction', - description: '创建交易', - inputSchema: { - type: 'object', - additionalProperties: false, - required: ['type', 'amount', 'transactionDate'], - properties: { - type: { type: 'string' }, - amount: { type: 'number' }, - currency: { type: 'string' }, - categoryId: { type: ['number', 'null'] }, - accountId: { type: ['number', 'null'] }, - transactionDate: { type: 'string' }, - description: { type: 'string' }, - project: { type: ['string', 'null'] }, - memo: { type: ['string', 'null'] }, - status: { type: 'string' }, - reimbursementBatch: { type: ['string', 'null'] }, - reviewNotes: { type: ['string', 'null'] }, - submittedBy: { type: ['string', 'null'] }, - approvedBy: { type: ['string', 'null'] }, - approvedAt: { type: ['string', 'null'] }, - statusUpdatedAt: { type: ['string', 'null'] }, - isDeleted: { type: 'boolean' }, - }, - }, - handler: async (args) => jsonResult(await client.createTransaction(buildTransactionCreatePayload(args))), - }); - - tools.push({ - name: 'finance_update_transaction', - description: '更新交易', - inputSchema: { - type: 'object', - additionalProperties: false, - required: ['id'], - properties: { - id: { type: 'number' }, - type: { type: 'string' }, - amount: { type: 'number' }, - currency: { type: 'string' }, - categoryId: { type: ['number', 'null'] }, - accountId: { type: ['number', 'null'] }, - transactionDate: { type: 'string' }, - description: { type: ['string', 'null'] }, - project: { type: ['string', 'null'] }, - memo: { type: ['string', 'null'] }, - status: { type: 'string' }, - statusUpdatedAt: { type: 'string' }, - reimbursementBatch: { type: ['string', 'null'] }, - reviewNotes: { type: ['string', 'null'] }, - submittedBy: { type: ['string', 'null'] }, - approvedBy: { type: ['string', 'null'] }, - approvedAt: { type: ['string', 'null'] }, - isDeleted: { type: 'boolean' }, - }, - }, - handler: async (args) => jsonResult( - await client.updateTransaction(ensureNumber(args?.id, 'id'), buildTransactionUpdatePayload(args)), - ), - }); - - tools.push({ - name: 'finance_delete_transaction', - description: '删除交易(软删)', - inputSchema: { - type: 'object', - additionalProperties: false, - required: ['id'], - properties: { id: { type: 'number' } }, - }, - handler: async (args) => jsonResult(await client.deleteTransaction(ensureNumber(args?.id, 'id'))), - }); - - tools.push({ - name: 'finance_list_reimbursements', - description: '查询报销单', - inputSchema: { - type: 'object', - additionalProperties: false, - properties: { - type: { type: 'string' }, - statuses: { type: ['array', 'string'], items: { type: 'string' } }, - includeDeleted: { type: 'boolean' }, - }, - }, - handler: async (args) => { - const type = optionalString(args?.type); - const statuses = parseStringArray(args?.statuses); - const includeDeleted = optionalBoolean(args?.includeDeleted, 'includeDeleted'); - return jsonResult( - await client.listReimbursements({ - ...(type ? { type } : {}), - ...(statuses ? { statuses } : {}), - ...(includeDeleted !== undefined ? { includeDeleted } : {}), - }), - ); - }, - }); - - tools.push({ - name: 'finance_create_reimbursement', - description: '创建报销单', - inputSchema: { - type: 'object', - additionalProperties: false, - required: ['amount', 'transactionDate'], - properties: { - type: { type: 'string' }, - amount: { type: 'number' }, - currency: { type: 'string' }, - categoryId: { type: ['number', 'null'] }, - accountId: { type: ['number', 'null'] }, - transactionDate: { type: 'string' }, - description: { type: 'string' }, - project: { type: ['string', 'null'] }, - memo: { type: ['string', 'null'] }, - status: { type: 'string' }, - reimbursementBatch: { type: ['string', 'null'] }, - reviewNotes: { type: ['string', 'null'] }, - submittedBy: { type: ['string', 'null'] }, - approvedBy: { type: ['string', 'null'] }, - approvedAt: { type: ['string', 'null'] }, - statusUpdatedAt: { type: ['string', 'null'] }, - isDeleted: { type: 'boolean' }, - }, - }, - handler: async (args) => jsonResult( - await client.createReimbursement(buildTransactionCreatePayload(args, { defaultType: 'expense' })), - ), - }); - - tools.push({ - name: 'finance_update_reimbursement', - description: '更新报销单', - inputSchema: { - type: 'object', - additionalProperties: false, - required: ['id'], - properties: { - id: { type: 'number' }, - type: { type: 'string' }, - amount: { type: 'number' }, - currency: { type: 'string' }, - categoryId: { type: ['number', 'null'] }, - accountId: { type: ['number', 'null'] }, - transactionDate: { type: 'string' }, - description: { type: ['string', 'null'] }, - project: { type: ['string', 'null'] }, - memo: { type: ['string', 'null'] }, - status: { type: 'string' }, - statusUpdatedAt: { type: 'string' }, - reimbursementBatch: { type: ['string', 'null'] }, - reviewNotes: { type: ['string', 'null'] }, - submittedBy: { type: ['string', 'null'] }, - approvedBy: { type: ['string', 'null'] }, - approvedAt: { type: ['string', 'null'] }, - isDeleted: { type: 'boolean' }, - }, - }, - handler: async (args) => jsonResult( - await client.updateReimbursement( - ensureNumber(args?.id, 'id'), - buildTransactionUpdatePayload(args), - ), - ), - }); - - tools.push({ - name: 'finance_list_media', - description: '查询媒体消息', - inputSchema: { - type: 'object', - additionalProperties: false, - properties: { - limit: { type: 'number' }, - fileTypes: { type: ['array', 'string'], items: { type: 'string' } }, - }, - }, - handler: async (args) => { - const limit = optionalNumber(args?.limit, 'limit'); - const fileTypes = parseStringArray(args?.fileTypes); - return jsonResult( - await client.listMedia({ - ...(limit !== undefined ? { limit } : {}), - ...(fileTypes ? { fileTypes } : {}), - }), - ); - }, - }); - - tools.push({ - name: 'finance_get_media', - description: '根据 ID 获取媒体详情', - inputSchema: { - type: 'object', - additionalProperties: false, - required: ['id'], - properties: { id: { type: 'number' } }, - }, - handler: async (args) => jsonResult(await client.getMediaById(ensureNumber(args?.id, 'id'))), - }); - - tools.push({ - name: 'finance_download_media', - description: '下载媒体文件并返回 Base64', - inputSchema: { - type: 'object', - additionalProperties: false, - required: ['id'], - properties: { - id: { type: 'number' }, - includeMetadata: { type: 'boolean', default: true }, - }, - }, - outputSchema: { - type: 'object', - properties: { - fileName: { type: ['string', 'null'] }, - mimeType: { type: 'string' }, - size: { type: 'number' }, - base64: { type: 'string' }, - metadata: { type: ['object', 'null'] }, - }, - }, - handler: async (args) => { - const id = ensureNumber(args?.id, 'id'); - const includeMetadata = optionalBoolean(args?.includeMetadata, 'includeMetadata'); - const file = await client.downloadMedia(id); - const metadata = includeMetadata === false ? null : await client.getMediaById(id); - return jsonResult({ ...file, metadata }); - }, - }); - - return tools; -}; - -const createServer = () => { - const baseUrl = - process.env.FINANCE_API_BASE_URL ?? 'http://172.16.74.149:5666'; - const apiKey = process.env.FINANCE_API_KEY; - const timeoutEnv = process.env.FINANCE_API_TIMEOUT; - const timeout = timeoutEnv ? Number.parseInt(timeoutEnv, 10) : undefined; - const basicUsername = - process.env.FINANCE_BASIC_USERNAME ?? - process.env.FINANCE_BASIC_USER ?? - process.env.FINANCE_USERNAME; - const basicPassword = - process.env.FINANCE_BASIC_PASSWORD ?? - process.env.FINANCE_PASSWORD; - const basicAuth = - basicUsername && basicPassword ? { username: basicUsername, password: basicPassword } : undefined; - - const client = new FinanceClient({ - baseUrl, - apiKey, - basicAuth, - timeoutMs: Number.isFinite(timeout ?? NaN) ? timeout : undefined, - }); - - return new McpServer({ - name: 'finwise-finance', - version: '0.1.0', - description: 'Finwise Pro 财务接口 MCP 服务', - tools: createFinanceTools(client), - }); -}; - -createServer().start(); diff --git a/apps/finance-mcp-service/src/index.ts b/apps/finance-mcp-service/src/index.ts new file mode 100644 index 00000000..22c1d527 --- /dev/null +++ b/apps/finance-mcp-service/src/index.ts @@ -0,0 +1,35 @@ +import process from 'node:process'; + +import { FinanceClient } from './client/finance-client.js'; +import { config } from './config.js'; +import { logger } from './logger.js'; +import { McpServer } from './server/mcp-server.js'; +import { createFinanceTools } from './tools/finance.js'; + +process.on('exit', (code) => { + logger.info({ code }, 'process exit'); +}); +process.on('uncaughtException', (error) => { + logger.error({ err: error }, 'uncaught exception'); +}); +process.on('unhandledRejection', (reason) => { + logger.error({ reason }, 'unhandled rejection'); +}); + +const client = new FinanceClient({ + baseUrl: config.baseUrl, + apiKey: config.apiKey, + basicAuth: config.basicAuth, + timeoutMs: config.timeoutMs, +}); + +const server = new McpServer({ + name: 'finwise-finance', + version: '0.2.0', + description: 'Finwise Pro 财务接口 MCP 服务', + tools: createFinanceTools(client), + logger, + concurrency: config.maxConcurrency, +}); + +server.start(); diff --git a/apps/finance-mcp-service/src/logger.ts b/apps/finance-mcp-service/src/logger.ts new file mode 100644 index 00000000..7a5fa69e --- /dev/null +++ b/apps/finance-mcp-service/src/logger.ts @@ -0,0 +1,12 @@ +import process from 'node:process'; +import pino from 'pino'; + +const level = + process.env.FINANCE_MCP_LOG_LEVEL ?? process.env.LOG_LEVEL ?? 'info'; + +export const logger = pino({ + name: 'finwise-finance', + level, +}); + +export type Logger = typeof logger; diff --git a/apps/finance-mcp-service/src/server/mcp-server.ts b/apps/finance-mcp-service/src/server/mcp-server.ts new file mode 100644 index 00000000..679fcc1b --- /dev/null +++ b/apps/finance-mcp-service/src/server/mcp-server.ts @@ -0,0 +1,270 @@ +import type { Logger } from '../logger.js'; +import type { McpToolDefinition, ToolContext } from '../types.js'; + +import { Buffer } from 'node:buffer'; +import process from 'node:process'; + +import PQueue from 'p-queue'; + +interface JsonRpcRequest { + jsonrpc: '2.0'; + id?: null | number | string; + method: string; + params?: Record; +} + +interface JsonRpcSuccess { + jsonrpc: '2.0'; + id: null | number | string; + result: unknown; +} + +interface JsonRpcError { + jsonrpc: '2.0'; + id: null | number | string; + error: { + code: number; + message: string; + }; +} + +export interface McpServerOptions { + name: string; + version: string; + description: string; + tools: McpToolDefinition[]; + logger: Logger; + concurrency?: number; +} + +export class McpServer { + private buffer = ''; + + private expectedLength: null | number = null; + + private initialized = false; + + private readonly metadata: Array>; + + private readonly options: McpServerOptions; + + private readonly queue: PQueue; + + private readonly tools: Map; + + constructor(options: McpServerOptions) { + this.options = options; + this.tools = new Map(); + this.metadata = []; + + for (const tool of options.tools) { + if (this.tools.has(tool.name)) { + throw new Error(`Duplicate MCP tool name: ${tool.name}`); + } + this.tools.set(tool.name, tool); + this.metadata.push({ + name: tool.name, + description: tool.description, + inputSchema: tool.inputSchema, + ...(tool.outputSchema ? { outputSchema: tool.outputSchema } : {}), + }); + } + + this.queue = new PQueue({ concurrency: options.concurrency ?? 4 }); + } + + start() { + process.stdin.setEncoding('utf8'); + process.stdin.on('data', (chunk) => { + this.buffer += chunk; + void this.drain(); + }); + process.stdin.on('end', () => { + this.log('stdin ended'); + }); + process.stdin.on('close', () => { + this.log('stdin closed'); + }); + process.stdin.resume(); + this.log('MCP service ready'); + } + + private assertInitialized(method: string) { + if (!this.initialized) { + throw new Error(`Received ${method} before initialize`); + } + } + + private async dispatch(request: JsonRpcRequest) { + switch (request.method) { + case 'initialize': { + if (this.initialized) { + this.respondError(request.id ?? null, -32_600, 'Already initialized'); + return; + } + this.initialized = true; + this.respond(request.id ?? null, { + protocolVersion: '2024-10-07', + capabilities: { tools: { list: true, call: true } }, + service: { + name: this.options.name, + version: this.options.version, + description: this.options.description, + }, + }); + this.notify('notifications/ready', {}); + return; + } + + case 'ping': { + this.respond(request.id ?? null, 'pong'); + return; + } + + case 'shutdown': { + this.respond(request.id ?? null, null); + process.exitCode = 0; + process.nextTick(() => { + process.stdin.pause(); + this.log('shutdown signal received'); + }); + return; + } + + case 'tools/call': { + this.assertInitialized('tools/call'); + const params = request.params ?? {}; + const toolName = params.name; + + if (!toolName || typeof toolName !== 'string') { + this.respondError( + request.id ?? null, + -32_602, + 'Tool name is required', + ); + return; + } + + const tool = this.tools.get(toolName); + if (!tool) { + this.respondError( + request.id ?? null, + -32_601, + `Unknown tool: ${toolName}`, + ); + return; + } + + await this.queue.add(async () => { + try { + const args = (params.arguments ?? {}) as Record; + const context: ToolContext = { logger: this.options.logger }; + const result = await tool.handler(args, context); + this.respond(request.id ?? null, result); + } catch (error) { + const message = + error instanceof Error ? error.message : String(error); + this.respondError(request.id ?? null, -32_001, message); + } + }); + + return; + } + + case 'tools/list': { + this.assertInitialized('tools/list'); + this.respond(request.id ?? null, { tools: this.metadata }); + return; + } + + default: { + this.respondError( + request.id ?? null, + -32_601, + `Method not found: ${request.method}`, + ); + } + } + } + + private async drain() { + while (true) { + if (this.expectedLength === null) { + const headerEnd = this.buffer.indexOf('\r\n\r\n'); + if (headerEnd === -1) return; + const header = this.buffer.slice(0, headerEnd); + const match = header.match(/content-length:\s*(\d+)/i); + if (!match) { + this.buffer = this.buffer.slice(headerEnd + 4); + continue; + } + const lengthHeader = match[1]; + if (!lengthHeader) { + this.buffer = this.buffer.slice(headerEnd + 4); + continue; + } + this.expectedLength = Number.parseInt(lengthHeader, 10); + this.buffer = this.buffer.slice(headerEnd + 4); + } + + if (this.buffer.length < (this.expectedLength ?? 0)) return; + + const body = this.buffer.slice(0, this.expectedLength ?? 0); + this.buffer = this.buffer.slice(this.expectedLength ?? 0); + this.expectedLength = null; + + await this.handleMessage(body); + } + } + + private async handleMessage(payload: string) { + let request: JsonRpcRequest | null = null; + try { + request = JSON.parse(payload) as JsonRpcRequest; + } catch { + this.respondError(null, -32_700, 'Parse error'); + return; + } + + if ( + !request || + request.jsonrpc !== '2.0' || + typeof request.method !== 'string' + ) { + this.respondError(request?.id ?? null, -32_600, 'Invalid Request'); + return; + } + + try { + await this.dispatch(request); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + this.log(`Unexpected error: ${message}`); + this.respondError(request.id ?? null, -32_000, message); + } + } + + private log(message: string) { + this.options.logger.debug({ scope: 'mcp-server' }, message); + } + + private notify(method: string, params: Record) { + this.write({ jsonrpc: '2.0', method, params }); + } + + private respond(id: JsonRpcSuccess['id'], result: unknown) { + if (id === undefined) return; + this.write({ jsonrpc: '2.0', id, result }); + } + + private respondError(id: JsonRpcError['id'], code: number, message: string) { + if (id === undefined) return; + this.write({ jsonrpc: '2.0', id, error: { code, message } }); + } + + private write(payload: JsonRpcError | JsonRpcRequest | JsonRpcSuccess) { + const json = JSON.stringify(payload); + const frame = `Content-Length: ${Buffer.byteLength(json, 'utf8')}\r\n\r\n${json}`; + process.stdout.write(frame); + } +} diff --git a/apps/finance-mcp-service/src/tools/finance.ts b/apps/finance-mcp-service/src/tools/finance.ts new file mode 100644 index 00000000..80dd8286 --- /dev/null +++ b/apps/finance-mcp-service/src/tools/finance.ts @@ -0,0 +1,789 @@ +import type { + FinanceClient, + ListExchangeRatesParams, +} from '../client/finance-client.js'; +import type { McpToolDefinition, ToolContext } from '../types.js'; + +import { jsonResult } from '../utils/mcp.js'; +import { + ensureNumber, + ensureString, + optionalBoolean, + optionalNullableNumber, + optionalNullableString, + optionalNumber, + optionalString, + parseStringArray, +} from '../utils/validation.js'; + +type ToolArgs = Record; + +interface CreateTransactionOptions { + defaultType?: string; +} + +export const createFinanceTools = ( + client: FinanceClient, +): McpToolDefinition[] => { + const tools: McpToolDefinition[] = []; + + tools.push( + { + name: 'finance_list_accounts', + description: '列出账户,可选货币过滤', + inputSchema: { + type: 'object', + additionalProperties: false, + properties: { + currency: { type: 'string', description: 'ISO 4217 货币代码' }, + }, + }, + handler: async (args: ToolArgs, _context: ToolContext) => { + const currency = optionalString(args?.currency); + return jsonResult( + await client.listAccounts(currency ? { currency } : {}), + ); + }, + }, + { + name: 'finance_list_budgets', + description: '查询预算列表', + inputSchema: { + type: 'object', + additionalProperties: false, + properties: {}, + }, + handler: async () => jsonResult(await client.listBudgets()), + }, + { + name: 'finance_create_budget', + description: '创建预算', + inputSchema: { + type: 'object', + additionalProperties: false, + required: ['name', 'amount', 'currency', 'startDate', 'endDate'], + properties: { + name: { type: 'string' }, + amount: { type: 'number' }, + currency: { type: 'string' }, + startDate: { type: 'string' }, + endDate: { type: 'string' }, + description: { type: 'string' }, + categoryId: { type: 'number' }, + project: { type: 'string' }, + owner: { type: 'string' }, + }, + }, + handler: async (args: ToolArgs, _context: ToolContext) => + jsonResult( + await client.createBudget({ + name: ensureString(args?.name, 'name'), + amount: ensureNumber(args?.amount, 'amount'), + currency: ensureString(args?.currency, 'currency'), + startDate: ensureString(args?.startDate, 'startDate'), + endDate: ensureString(args?.endDate, 'endDate'), + description: optionalString(args?.description), + categoryId: optionalNumber(args?.categoryId, 'categoryId'), + project: optionalString(args?.project), + owner: optionalString(args?.owner), + }), + ), + }, + { + name: 'finance_update_budget', + description: '更新预算', + inputSchema: { + type: 'object', + additionalProperties: false, + required: ['id'], + properties: { + id: { type: 'number' }, + name: { type: 'string' }, + amount: { type: 'number' }, + currency: { type: 'string' }, + startDate: { type: 'string' }, + endDate: { type: 'string' }, + description: { type: 'string' }, + categoryId: { type: 'number' }, + project: { type: 'string' }, + owner: { type: 'string' }, + }, + }, + handler: async (args: ToolArgs, _context: ToolContext) => { + const id = ensureNumber(args?.id, 'id'); + const payload: Record = {}; + if (args?.name !== undefined) + payload.name = ensureString(args.name, 'name'); + if (args?.amount !== undefined) + payload.amount = ensureNumber(args.amount, 'amount'); + if (args?.currency !== undefined) + payload.currency = ensureString(args.currency, 'currency'); + if (args?.startDate !== undefined) + payload.startDate = ensureString(args.startDate, 'startDate'); + if (args?.endDate !== undefined) + payload.endDate = ensureString(args.endDate, 'endDate'); + if (args?.description !== undefined) + payload.description = optionalString(args.description); + if (args?.categoryId !== undefined) + payload.categoryId = optionalNumber(args.categoryId, 'categoryId'); + if (args?.project !== undefined) + payload.project = optionalString(args.project); + if (args?.owner !== undefined) + payload.owner = optionalString(args.owner); + return jsonResult(await client.updateBudget(id, payload)); + }, + }, + { + name: 'finance_delete_budget', + description: '删除预算', + inputSchema: { + type: 'object', + additionalProperties: false, + required: ['id'], + properties: { id: { type: 'number' } }, + }, + handler: async (args: ToolArgs, _context: ToolContext) => + jsonResult(await client.deleteBudget(ensureNumber(args?.id, 'id'))), + }, + { + name: 'finance_list_categories', + description: '查询分类,可按类型过滤', + inputSchema: { + type: 'object', + additionalProperties: false, + properties: { + type: { + type: 'string', + enum: ['expense', 'income', 'transfer'], + }, + }, + }, + handler: async (args: ToolArgs, _context: ToolContext) => { + const type = optionalString(args?.type); + return jsonResult(await client.listCategories(type ? { type } : {})); + }, + }, + { + name: 'finance_create_category', + description: '创建分类', + inputSchema: { + type: 'object', + additionalProperties: false, + required: ['name', 'type'], + properties: { + name: { type: 'string' }, + type: { type: 'string', enum: ['expense', 'income', 'transfer'] }, + icon: { type: 'string' }, + color: { type: 'string' }, + userId: { type: 'number' }, + isActive: { type: 'boolean' }, + }, + }, + handler: async (args: ToolArgs, _context: ToolContext) => + jsonResult( + await client.createCategory({ + name: ensureString(args?.name, 'name'), + type: ensureString(args?.type, 'type'), + icon: optionalString(args?.icon), + color: optionalString(args?.color), + userId: optionalNumber(args?.userId, 'userId'), + isActive: optionalBoolean(args?.isActive, 'isActive') ?? true, + }), + ), + }, + { + name: 'finance_update_category', + description: '更新分类', + inputSchema: { + type: 'object', + additionalProperties: false, + required: ['id'], + properties: { + id: { type: 'number' }, + name: { type: 'string' }, + type: { type: 'string' }, + icon: { type: 'string' }, + color: { type: 'string' }, + userId: { type: 'number' }, + isActive: { type: 'boolean' }, + }, + }, + handler: async (args: ToolArgs, _context: ToolContext) => { + const id = ensureNumber(args?.id, 'id'); + const payload: Record = {}; + if (args?.name !== undefined) + payload.name = ensureString(args.name, 'name'); + if (args?.type !== undefined) + payload.type = ensureString(args.type, 'type'); + if (args?.icon !== undefined) payload.icon = optionalString(args.icon); + if (args?.color !== undefined) + payload.color = optionalString(args.color); + if (args?.userId !== undefined) + payload.userId = optionalNumber(args.userId, 'userId'); + if (args?.isActive !== undefined) + payload.isActive = optionalBoolean(args.isActive, 'isActive'); + return jsonResult(await client.updateCategory(id, payload)); + }, + }, + { + name: 'finance_delete_category', + description: '删除分类', + inputSchema: { + type: 'object', + additionalProperties: false, + required: ['id'], + properties: { id: { type: 'number' } }, + }, + handler: async (args: ToolArgs, _context: ToolContext) => + jsonResult(await client.deleteCategory(ensureNumber(args?.id, 'id'))), + }, + { + name: 'finance_list_currencies', + description: '查询货币列表', + inputSchema: { + type: 'object', + additionalProperties: false, + properties: {}, + }, + handler: async () => jsonResult(await client.listCurrencies()), + }, + { + name: 'finance_list_exchange_rates', + description: '查询汇率,可按货币/日期过滤', + inputSchema: { + type: 'object', + additionalProperties: false, + properties: { + fromCurrency: { type: 'string' }, + toCurrency: { type: 'string' }, + date: { type: 'string' }, + }, + }, + handler: async (args: ToolArgs, _context: ToolContext) => { + const params: ListExchangeRatesParams = {}; + if (args?.fromCurrency) + params.fromCurrency = ensureString(args.fromCurrency, 'fromCurrency'); + if (args?.toCurrency) + params.toCurrency = ensureString(args.toCurrency, 'toCurrency'); + if (args?.date) params.date = ensureString(args.date, 'date'); + return jsonResult(await client.listExchangeRates(params)); + }, + }, + { + name: 'finance_list_transactions', + description: '查询交易记录', + inputSchema: { + type: 'object', + additionalProperties: false, + properties: { + type: { type: 'string', enum: ['expense', 'income', 'transfer'] }, + statuses: { + type: ['array', 'string'], + items: { type: 'string' }, + }, + includeDeleted: { type: 'boolean' }, + }, + }, + handler: async (args: ToolArgs, _context: ToolContext) => { + const type = optionalString(args?.type); + const statuses = parseStringArray(args?.statuses); + const includeDeleted = optionalBoolean( + args?.includeDeleted, + 'includeDeleted', + ); + return jsonResult( + await client.listTransactions({ + ...(type ? { type } : {}), + ...(statuses ? { statuses } : {}), + ...(includeDeleted === undefined ? {} : { includeDeleted }), + }), + ); + }, + }, + { + name: 'finance_create_transaction', + description: '创建交易记录', + inputSchema: { + type: 'object', + additionalProperties: false, + required: ['type', 'amount', 'currency', 'transactionDate'], + properties: { + type: { type: 'string', enum: ['expense', 'income', 'transfer'] }, + amount: { type: 'number' }, + currency: { type: 'string' }, + transactionDate: { type: 'string', description: 'ISO 日期' }, + categoryId: { type: ['number', 'null'] }, + accountId: { type: ['number', 'null'] }, + description: { type: 'string' }, + project: { type: ['string', 'null'] }, + memo: { type: ['string', 'null'] }, + status: { type: 'string' }, + statusUpdatedAt: { type: 'string' }, + reimbursementBatch: { type: ['string', 'null'] }, + reviewNotes: { type: ['string', 'null'] }, + submittedBy: { type: ['string', 'null'] }, + approvedBy: { type: ['string', 'null'] }, + approvedAt: { type: ['string', 'null'] }, + isDeleted: { type: 'boolean' }, + }, + }, + handler: async (args: ToolArgs, _context: ToolContext) => + jsonResult( + await client.createTransaction(buildTransactionCreatePayload(args)), + ), + }, + { + name: 'finance_update_transaction', + description: '更新交易记录', + inputSchema: { + type: 'object', + additionalProperties: false, + required: ['id'], + properties: { + id: { type: 'number' }, + type: { type: 'string' }, + amount: { type: 'number' }, + currency: { type: 'string' }, + transactionDate: { type: 'string' }, + categoryId: { type: ['number', 'null'] }, + accountId: { type: ['number', 'null'] }, + description: { type: ['string', 'null'] }, + project: { type: ['string', 'null'] }, + memo: { type: ['string', 'null'] }, + status: { type: 'string' }, + statusUpdatedAt: { type: 'string' }, + reimbursementBatch: { type: ['string', 'null'] }, + reviewNotes: { type: ['string', 'null'] }, + submittedBy: { type: ['string', 'null'] }, + approvedBy: { type: ['string', 'null'] }, + approvedAt: { type: ['string', 'null'] }, + isDeleted: { type: 'boolean' }, + }, + }, + handler: async (args: ToolArgs, _context: ToolContext) => + jsonResult( + await client.updateTransaction( + ensureNumber(args?.id, 'id'), + buildTransactionUpdatePayload(args), + ), + ), + }, + { + name: 'finance_delete_transaction', + description: '删除交易记录', + inputSchema: { + type: 'object', + additionalProperties: false, + required: ['id'], + properties: { id: { type: 'number' } }, + }, + handler: async (args: ToolArgs, _context: ToolContext) => + jsonResult( + await client.deleteTransaction(ensureNumber(args?.id, 'id')), + ), + }, + { + name: 'finance_list_reimbursements', + description: '查询报销记录', + inputSchema: { + type: 'object', + additionalProperties: false, + properties: { + type: { type: 'string' }, + statuses: { + type: ['array', 'string'], + items: { type: 'string' }, + }, + includeDeleted: { type: 'boolean' }, + }, + }, + handler: async (args: ToolArgs, _context: ToolContext) => { + const type = optionalString(args?.type); + const statuses = parseStringArray(args?.statuses); + const includeDeleted = optionalBoolean( + args?.includeDeleted, + 'includeDeleted', + ); + return jsonResult( + await client.listReimbursements({ + ...(type ? { type } : {}), + ...(statuses ? { statuses } : {}), + ...(includeDeleted === undefined ? {} : { includeDeleted }), + }), + ); + }, + }, + { + name: 'finance_create_reimbursement', + description: '创建报销记录', + inputSchema: { + type: 'object', + additionalProperties: false, + required: ['amount', 'currency', 'transactionDate'], + properties: { + type: { type: 'string', default: 'expense' }, + amount: { type: 'number' }, + currency: { type: 'string' }, + transactionDate: { type: 'string' }, + categoryId: { type: ['number', 'null'] }, + accountId: { type: ['number', 'null'] }, + description: { type: 'string' }, + project: { type: ['string', 'null'] }, + memo: { type: ['string', 'null'] }, + status: { type: 'string' }, + statusUpdatedAt: { type: 'string' }, + reimbursementBatch: { type: ['string', 'null'] }, + reviewNotes: { type: ['string', 'null'] }, + submittedBy: { type: ['string', 'null'] }, + approvedBy: { type: ['string', 'null'] }, + approvedAt: { type: ['string', 'null'] }, + isDeleted: { type: 'boolean' }, + }, + }, + handler: async (args: ToolArgs, _context: ToolContext) => + jsonResult( + await client.createReimbursement( + buildTransactionCreatePayload(args, { defaultType: 'expense' }), + ), + ), + }, + { + name: 'finance_update_reimbursement', + description: '更新报销记录', + inputSchema: { + type: 'object', + additionalProperties: false, + required: ['id'], + properties: { + id: { type: 'number' }, + type: { type: 'string' }, + amount: { type: 'number' }, + currency: { type: 'string' }, + transactionDate: { type: 'string' }, + categoryId: { type: ['number', 'null'] }, + accountId: { type: ['number', 'null'] }, + description: { type: ['string', 'null'] }, + project: { type: ['string', 'null'] }, + memo: { type: ['string', 'null'] }, + status: { type: 'string' }, + statusUpdatedAt: { type: 'string' }, + reimbursementBatch: { type: ['string', 'null'] }, + reviewNotes: { type: ['string', 'null'] }, + submittedBy: { type: ['string', 'null'] }, + approvedBy: { type: ['string', 'null'] }, + approvedAt: { type: ['string', 'null'] }, + isDeleted: { type: 'boolean' }, + }, + }, + handler: async (args: ToolArgs, _context: ToolContext) => + jsonResult( + await client.updateReimbursement( + ensureNumber(args?.id, 'id'), + buildTransactionUpdatePayload(args), + ), + ), + }, + { + name: 'finance_list_media', + description: '查询媒体消息', + inputSchema: { + type: 'object', + additionalProperties: false, + properties: { + limit: { type: 'number' }, + fileTypes: { type: ['array', 'string'], items: { type: 'string' } }, + }, + }, + handler: async (args: ToolArgs, _context: ToolContext) => { + const limit = optionalNumber(args?.limit, 'limit'); + const fileTypes = parseStringArray(args?.fileTypes); + return jsonResult( + await client.listMedia({ + ...(limit === undefined ? {} : { limit }), + ...(fileTypes ? { fileTypes } : {}), + }), + ); + }, + }, + { + name: 'finance_get_media', + description: '根据 ID 获取媒体详情', + inputSchema: { + type: 'object', + additionalProperties: false, + required: ['id'], + properties: { id: { type: 'number' } }, + }, + handler: async (args: ToolArgs, _context: ToolContext) => + jsonResult(await client.getMediaById(ensureNumber(args?.id, 'id'))), + }, + { + name: 'finance_download_media', + description: '下载媒体文件并返回 Base64', + inputSchema: { + type: 'object', + additionalProperties: false, + required: ['id'], + properties: { + id: { type: 'number' }, + includeMetadata: { type: 'boolean', default: true }, + }, + }, + outputSchema: { + type: 'object', + properties: { + fileName: { type: ['string', 'null'] }, + mimeType: { type: 'string' }, + size: { type: 'number' }, + base64: { type: 'string' }, + metadata: { type: ['object', 'null'] }, + }, + }, + handler: async (args: ToolArgs, _context: ToolContext) => { + const id = ensureNumber(args?.id, 'id'); + const includeMetadata = optionalBoolean( + args?.includeMetadata, + 'includeMetadata', + ); + const file = await client.downloadMedia(id); + const metadata = + includeMetadata === false ? null : await client.getMediaById(id); + return jsonResult({ ...file, metadata }); + }, + }, + { + name: 'finance_list_telegram_configs', + description: '列出 Telegram 通知配置', + inputSchema: { + type: 'object', + additionalProperties: false, + properties: {}, + }, + handler: async () => jsonResult(await client.listTelegramConfigs()), + }, + { + name: 'finance_create_telegram_config', + description: '创建 Telegram 通知配置并自动测试', + inputSchema: { + type: 'object', + additionalProperties: false, + required: ['name', 'botToken', 'chatId'], + properties: { + name: { type: 'string' }, + botToken: { type: 'string' }, + chatId: { type: 'string' }, + notificationTypes: { + type: ['array', 'string'], + items: { type: 'string' }, + }, + isEnabled: { type: 'boolean', default: true }, + }, + }, + handler: async (args: ToolArgs, _context: ToolContext) => + jsonResult( + await client.createTelegramConfig({ + name: ensureString(args?.name, 'name'), + botToken: ensureString(args?.botToken, 'botToken'), + chatId: ensureString(args?.chatId, 'chatId'), + notificationTypes: parseStringArray(args?.notificationTypes) ?? [ + 'transaction', + ], + isEnabled: optionalBoolean(args?.isEnabled, 'isEnabled') ?? true, + }), + ), + }, + { + name: 'finance_update_telegram_config', + description: '更新 Telegram 通知配置,并在 Token/Chat 变更时重测', + inputSchema: { + type: 'object', + additionalProperties: false, + required: ['id'], + properties: { + id: { type: 'number' }, + name: { type: 'string' }, + botToken: { type: 'string' }, + chatId: { type: 'string' }, + notificationTypes: { + type: ['array', 'string'], + items: { type: 'string' }, + }, + isEnabled: { type: 'boolean' }, + }, + }, + handler: async (args: ToolArgs, _context: ToolContext) => { + const id = ensureNumber(args?.id, 'id'); + const payload: Record = {}; + if (args?.name !== undefined) + payload.name = ensureString(args.name, 'name'); + if (args?.botToken !== undefined) + payload.botToken = ensureString(args.botToken, 'botToken'); + if (args?.chatId !== undefined) + payload.chatId = ensureString(args.chatId, 'chatId'); + if (args?.notificationTypes !== undefined) { + payload.notificationTypes = + parseStringArray(args.notificationTypes) ?? []; + } + if (args?.isEnabled !== undefined) + payload.isEnabled = optionalBoolean(args.isEnabled, 'isEnabled'); + + if (Object.keys(payload).length === 0) { + throw new Error('No fields to update'); + } + + return jsonResult(await client.updateTelegramConfig(id, payload)); + }, + }, + { + name: 'finance_delete_telegram_config', + description: '删除 Telegram 通知配置', + inputSchema: { + type: 'object', + additionalProperties: false, + required: ['id'], + properties: { id: { type: 'number' } }, + }, + handler: async (args: ToolArgs, _context: ToolContext) => + jsonResult( + await client.deleteTelegramConfig(ensureNumber(args?.id, 'id')), + ), + }, + { + name: 'finance_test_telegram_config', + description: '实时测试 Telegram Bot 是否可发送消息', + inputSchema: { + type: 'object', + additionalProperties: false, + required: ['botToken', 'chatId'], + properties: { + botToken: { type: 'string' }, + chatId: { type: 'string' }, + }, + }, + handler: async (args: ToolArgs, _context: ToolContext) => + jsonResult( + await client.testTelegramConfig({ + botToken: ensureString(args?.botToken, 'botToken'), + chatId: ensureString(args?.chatId, 'chatId'), + }), + ), + }, + ); + + return tools; +}; + +const buildTransactionCreatePayload = ( + args: Record, + options: CreateTransactionOptions = {}, +) => { + const payload: Record = { + type: + optionalString(args?.type) ?? + options.defaultType ?? + ensureString(args?.type, 'type'), + amount: ensureNumber(args?.amount, 'amount'), + currency: optionalString(args?.currency) ?? 'CNY', + transactionDate: ensureString(args?.transactionDate, 'transactionDate'), + }; + + const categoryId = optionalNullableNumber(args?.categoryId, 'categoryId'); + if (categoryId !== undefined) payload.categoryId = categoryId; + + const accountId = optionalNullableNumber(args?.accountId, 'accountId'); + if (accountId !== undefined) payload.accountId = accountId; + + const description = optionalString(args?.description); + if (description !== undefined) payload.description = description; + + const project = optionalNullableString(args?.project); + if (project !== undefined) payload.project = project; + + const memo = optionalNullableString(args?.memo); + if (memo !== undefined) payload.memo = memo; + + const status = optionalString(args?.status); + if (status !== undefined) payload.status = status; + + const reimbursementBatch = optionalNullableString(args?.reimbursementBatch); + if (reimbursementBatch !== undefined) + payload.reimbursementBatch = reimbursementBatch; + + const reviewNotes = optionalNullableString(args?.reviewNotes); + if (reviewNotes !== undefined) payload.reviewNotes = reviewNotes; + + const submittedBy = optionalNullableString(args?.submittedBy); + if (submittedBy !== undefined) payload.submittedBy = submittedBy; + + const approvedBy = optionalNullableString(args?.approvedBy); + if (approvedBy !== undefined) payload.approvedBy = approvedBy; + + const approvedAt = optionalNullableString(args?.approvedAt); + if (approvedAt !== undefined) payload.approvedAt = approvedAt; + + const statusUpdatedAt = optionalNullableString(args?.statusUpdatedAt); + if (statusUpdatedAt !== undefined) payload.statusUpdatedAt = statusUpdatedAt; + + const isDeleted = optionalBoolean(args?.isDeleted, 'isDeleted'); + if (isDeleted !== undefined) payload.isDeleted = isDeleted; + + return payload; +}; + +const buildTransactionUpdatePayload = (args: Record) => { + const payload: Record = {}; + + if (args?.type !== undefined) payload.type = ensureString(args.type, 'type'); + if (args?.amount !== undefined) + payload.amount = ensureNumber(args.amount, 'amount'); + if (args?.currency !== undefined) + payload.currency = ensureString(args.currency, 'currency'); + if (args?.transactionDate !== undefined) { + payload.transactionDate = ensureString( + args.transactionDate, + 'transactionDate', + ); + } + if (args?.categoryId !== undefined) { + payload.categoryId = optionalNullableNumber(args.categoryId, 'categoryId'); + } + if (args?.accountId !== undefined) { + payload.accountId = optionalNullableNumber(args.accountId, 'accountId'); + } + if (args?.description !== undefined) { + payload.description = + args.description === null ? '' : String(args.description); + } + if (args?.project !== undefined) + payload.project = optionalNullableString(args.project) ?? null; + if (args?.memo !== undefined) + payload.memo = optionalNullableString(args.memo) ?? null; + if (args?.status !== undefined) + payload.status = ensureString(args.status, 'status'); + if (args?.statusUpdatedAt !== undefined) { + payload.statusUpdatedAt = ensureString( + args.statusUpdatedAt, + 'statusUpdatedAt', + ); + } + if (args?.reimbursementBatch !== undefined) { + payload.reimbursementBatch = + optionalNullableString(args.reimbursementBatch) ?? null; + } + if (args?.reviewNotes !== undefined) { + payload.reviewNotes = optionalNullableString(args.reviewNotes) ?? null; + } + if (args?.submittedBy !== undefined) { + payload.submittedBy = optionalNullableString(args.submittedBy) ?? null; + } + if (args?.approvedBy !== undefined) { + payload.approvedBy = optionalNullableString(args.approvedBy) ?? null; + } + if (args?.approvedAt !== undefined) { + payload.approvedAt = optionalNullableString(args.approvedAt) ?? null; + } + const isDeleted = optionalBoolean(args?.isDeleted, 'isDeleted'); + if (isDeleted !== undefined) payload.isDeleted = isDeleted; + + return payload; +}; diff --git a/apps/finance-mcp-service/src/types.ts b/apps/finance-mcp-service/src/types.ts new file mode 100644 index 00000000..48b3938d --- /dev/null +++ b/apps/finance-mcp-service/src/types.ts @@ -0,0 +1,44 @@ +import type { Logger } from 'pino'; + +export type JsonValue = + | boolean + | JsonValue[] + | null + | number + | string + | { [key: string]: JsonValue }; + +export interface JsonContent { + type: 'application/json'; + data: unknown; +} + +export interface TextContent { + type: 'text'; + text: string; +} + +export type McpContent = JsonContent | TextContent; + +export interface McpToolResult { + content: McpContent[]; +} + +export type JsonSchema = Record; + +export interface ToolContext { + logger: Logger; +} + +export type McpToolHandler = ( + args: Record, + context: ToolContext, +) => Promise; + +export interface McpToolDefinition { + name: string; + description: string; + inputSchema: JsonSchema; + outputSchema?: JsonSchema; + handler: McpToolHandler; +} diff --git a/apps/finance-mcp-service/src/utils/mcp.ts b/apps/finance-mcp-service/src/utils/mcp.ts new file mode 100644 index 00000000..3e8943c5 --- /dev/null +++ b/apps/finance-mcp-service/src/utils/mcp.ts @@ -0,0 +1,9 @@ +import type { McpToolResult } from '../types.js'; + +export const jsonResult = (data: unknown): McpToolResult => ({ + content: [{ type: 'application/json', data }], +}); + +export const textResult = (text: string): McpToolResult => ({ + content: [{ type: 'text', text }], +}); diff --git a/apps/finance-mcp-service/src/utils/validation.ts b/apps/finance-mcp-service/src/utils/validation.ts new file mode 100644 index 00000000..b4b161dc --- /dev/null +++ b/apps/finance-mcp-service/src/utils/validation.ts @@ -0,0 +1,94 @@ +const isNil = (value: unknown): value is null | undefined => + value === null || value === undefined; + +export const ensureNumber = (value: unknown, field: string): number => { + if (typeof value === 'number' && Number.isFinite(value)) return value; + if (typeof value === 'string') { + const trimmed = value.trim(); + if (trimmed) { + const parsed = Number(trimmed); + if (!Number.isNaN(parsed)) return parsed; + } + } + throw new Error(`${field} must be a number`); +}; + +export const optionalNumber = ( + value: unknown, + field: string, +): number | undefined => { + if (isNil(value)) return undefined; + return ensureNumber(value, field); +}; + +export const optionalNullableNumber = ( + value: unknown, + field: string, +): null | number | undefined => { + if (value === undefined) return undefined; + if (value === null) return null; + if (typeof value === 'string') { + const normalized = value.trim().toLowerCase(); + if (!normalized || normalized === 'null') return null; + } + return ensureNumber(value, field); +}; + +export const ensureString = (value: unknown, field: string): string => { + if (typeof value === 'string') { + const trimmed = value.trim(); + if (!trimmed) throw new Error(`${field} cannot be empty`); + return trimmed; + } + if (isNil(value)) throw new Error(`${field} is required`); + return ensureString(String(value), field); +}; + +export const optionalString = (value: unknown): string | undefined => { + if (isNil(value)) return undefined; + return String(value); +}; + +export const optionalNullableString = ( + value: unknown, +): null | string | undefined => { + if (value === undefined) return undefined; + if (value === null) return null; + const normalized = String(value).trim(); + if (!normalized || normalized.toLowerCase() === 'null') return null; + return normalized; +}; + +export const optionalBoolean = ( + value: unknown, + field: string, +): boolean | undefined => { + if (isNil(value)) return undefined; + if (typeof value === 'boolean') return value; + if (typeof value === 'number') return value !== 0; + if (typeof value === 'string') { + const normalized = value.trim().toLowerCase(); + if (['1', 'true', 'y', 'yes'].includes(normalized)) return true; + if (['0', 'false', 'n', 'no'].includes(normalized)) return false; + } + throw new Error(`${field} must be boolean`); +}; + +export const parseStringArray = (value: unknown): string[] | undefined => { + if (isNil(value)) return undefined; + const toItem = (item: unknown) => String(item).trim(); + let items: string[] = []; + + if (Array.isArray(value)) { + items = value.map((item) => toItem(item)).filter(Boolean); + } else if (typeof value === 'string') { + items = value + .split(',') + .map((item) => toItem(item)) + .filter(Boolean); + } else { + items = [toItem(value)].filter(Boolean); + } + + return items.length > 0 ? items : undefined; +}; diff --git a/apps/finance-mcp-service/tsconfig.json b/apps/finance-mcp-service/tsconfig.json new file mode 100644 index 00000000..8227e517 --- /dev/null +++ b/apps/finance-mcp-service/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "module": "NodeNext", + "moduleResolution": "NodeNext", + "target": "ES2022", + "lib": ["ES2022", "DOM"], + "strict": true, + "noUncheckedIndexedAccess": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "rootDir": "src", + "outDir": "dist", + "types": ["node"], + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c7eba614..a725c6a9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -97,7 +97,7 @@ catalogs: specifier: ^4.3.9 version: 4.3.9 '@types/node': - specifier: ^22.16.0 + specifier: 22.19.0 version: 22.19.0 '@types/nprogress': specifier: ^0.2.3 @@ -442,7 +442,7 @@ catalogs: specifier: ^2.5.4 version: 2.6.0 typescript: - specifier: ^5.8.3 + specifier: 5.9.3 version: 5.9.3 unbuild: specifier: ^3.5.0 @@ -511,7 +511,7 @@ catalogs: specifier: ^1.6.2 version: 1.6.3 zod: - specifier: ^3.25.67 + specifier: 3.25.76 version: 3.25.76 zod-defaults: specifier: ^0.1.3 @@ -570,10 +570,10 @@ importers: version: link:scripts/vsh '@vitejs/plugin-vue': specifier: 'catalog:' - version: 5.2.4(vite@6.4.1(@types/node@22.19.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3)) + version: 5.2.4(vite@6.4.1(@types/node@22.19.0)(jiti@1.21.7)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3)) '@vitejs/plugin-vue-jsx': specifier: 'catalog:' - version: 4.2.0(vite@6.4.1(@types/node@22.19.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3)) + version: 4.2.0(vite@6.4.1(@types/node@22.19.0)(jiti@1.21.7)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3)) '@vue/test-utils': specifier: 'catalog:' version: 2.4.6 @@ -603,7 +603,7 @@ importers: version: 6.1.0 tailwindcss: specifier: 'catalog:' - version: 3.4.18(yaml@2.8.1) + version: 3.4.18(tsx@4.20.6)(yaml@2.8.1) turbo: specifier: 'catalog:' version: 2.6.0 @@ -615,10 +615,10 @@ importers: version: 3.6.1(sass@1.93.3)(typescript@5.9.3)(vue-tsc@2.2.10(typescript@5.9.3))(vue@3.5.22(typescript@5.9.3)) vite: specifier: 'catalog:' - version: 6.4.1(@types/node@22.19.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1) + version: 6.4.1(@types/node@22.19.0)(jiti@1.21.7)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) vitest: specifier: 'catalog:' - version: 3.2.4(@types/node@22.19.0)(happy-dom@17.6.3)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1) + version: 3.2.4(@types/node@22.19.0)(happy-dom@17.6.3)(jiti@1.21.7)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) vue: specifier: ^3.5.17 version: 3.5.22(typescript@5.9.3) @@ -648,7 +648,27 @@ importers: specifier: 'catalog:' version: 1.15.4 - apps/finance-mcp-service: {} + apps/finance-mcp-service: + dependencies: + p-queue: + specifier: ^9.0.0 + version: 9.0.0 + pino: + specifier: ^10.1.0 + version: 10.1.0 + zod: + specifier: 'catalog:' + version: 3.25.76 + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 22.19.0 + tsx: + specifier: ^4.20.6 + version: 4.20.6 + typescript: + specifier: 'catalog:' + version: 5.9.3 apps/web-antd: dependencies: @@ -978,7 +998,7 @@ importers: version: 4.3.0(@typescript-eslint/eslint-plugin@8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-vitest: specifier: 'catalog:' - version: 0.5.4(@typescript-eslint/eslint-plugin@8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)(vitest@3.2.4(@types/node@24.10.0)(happy-dom@17.6.3)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)) + version: 0.5.4(@typescript-eslint/eslint-plugin@8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)(vitest@3.2.4(@types/node@24.10.0)(happy-dom@17.6.3)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) eslint-plugin-vue: specifier: 'catalog:' version: 10.5.1(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(vue-eslint-parser@10.2.0(eslint@9.39.1(jiti@2.6.1))) @@ -1099,7 +1119,7 @@ importers: version: 0.0.0-insiders.565cd3e(postcss@8.5.6) '@tailwindcss/typography': specifier: 'catalog:' - version: 0.5.19(tailwindcss@3.4.18(yaml@2.8.1)) + version: 0.5.19(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1)) autoprefixer: specifier: 'catalog:' version: 10.4.21(postcss@8.5.6) @@ -1120,10 +1140,10 @@ importers: version: 10.4.0(postcss@8.5.6) tailwindcss: specifier: 'catalog:' - version: 3.4.18(yaml@2.8.1) + version: 3.4.18(tsx@4.20.6)(yaml@2.8.1) tailwindcss-animate: specifier: 'catalog:' - version: 1.0.7(tailwindcss@3.4.18(yaml@2.8.1)) + version: 1.0.7(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1)) devDependencies: '@types/postcss-import': specifier: 'catalog:' @@ -1136,7 +1156,7 @@ importers: version: link:../../packages/types vite: specifier: 'catalog:' - version: 6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1) + version: 6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) internal/vite-config: dependencies: @@ -1166,10 +1186,10 @@ importers: version: 2.0.3 vite-plugin-pwa: specifier: 'catalog:' - version: 1.1.0(vite@6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))(workbox-build@7.3.0)(workbox-window@7.3.0) + version: 1.1.0(vite@6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(workbox-build@7.3.0)(workbox-window@7.3.0) vite-plugin-vue-devtools: specifier: 'catalog:' - version: 7.7.7(rollup@4.52.5)(vite@6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3)) + version: 7.7.7(rollup@4.52.5)(vite@6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3)) devDependencies: '@pnpm/workspace.read-manifest': specifier: 'catalog:' @@ -1185,10 +1205,10 @@ importers: version: link:../node-utils '@vitejs/plugin-vue': specifier: 'catalog:' - version: 5.2.4(vite@6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3)) + version: 5.2.4(vite@6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3)) '@vitejs/plugin-vue-jsx': specifier: 'catalog:' - version: 4.2.0(vite@6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3)) + version: 4.2.0(vite@6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3)) dayjs: specifier: 'catalog:' version: 1.11.19 @@ -1206,16 +1226,16 @@ importers: version: 1.93.3 vite: specifier: 'catalog:' - version: 6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1) + version: 6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) vite-plugin-compression: specifier: 'catalog:' - version: 0.5.1(vite@6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)) + version: 0.5.1(vite@6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) vite-plugin-dts: specifier: 'catalog:' - version: 4.5.4(@types/node@24.10.0)(rollup@4.52.5)(typescript@5.9.3)(vite@6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)) + version: 4.5.4(@types/node@24.10.0)(rollup@4.52.5)(typescript@5.9.3)(vite@6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) vite-plugin-html: specifier: 'catalog:' - version: 3.2.2(vite@6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)) + version: 3.2.2(vite@6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) vite-plugin-lazy-import: specifier: 'catalog:' version: 1.0.7 @@ -3839,6 +3859,9 @@ packages: resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} engines: {node: '>= 10.0.0'} + '@pinojs/redact@0.4.0': + resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -5058,6 +5081,10 @@ packages: resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} engines: {node: '>= 4.0.0'} + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + atomically@2.1.0: resolution: {integrity: sha512-+gDffFXRW6sl/HCwbta7zK4uNqbPjv4YJEAdz7Vu+FLQHe77eZ4bvbJGi4hE0QPeJlMYMA3piXEr1UL3dAwx7Q==} @@ -8040,6 +8067,10 @@ packages: ohash@2.0.11: resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -8117,6 +8148,14 @@ packages: resolution: {integrity: sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==} engines: {node: '>=18'} + p-queue@9.0.0: + resolution: {integrity: sha512-KO1RyxstL9g1mK76530TExamZC/S2Glm080Nx8PE5sTd7nlduDQsAfEl4uXX+qZjLiwvDauvzXavufy3+rJ9zQ==} + engines: {node: '>=20'} + + p-timeout@7.0.1: + resolution: {integrity: sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==} + engines: {node: '>=20'} + p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} @@ -8323,6 +8362,16 @@ packages: typescript: optional: true + pino-abstract-transport@2.0.0: + resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} + + pino-std-serializers@7.0.0: + resolution: {integrity: sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==} + + pino@10.1.0: + resolution: {integrity: sha512-0zZC2ygfdqvqK8zJIr1e+wT1T/L+LF6qvqvbzEQ6tiMAoTqEVK9a1K3YRu8HEUvGEvNqZyPJTtb2sNIoTkB83w==} + hasBin: true + pirates@4.0.7: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} @@ -8914,6 +8963,9 @@ packages: process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + process-warning@5.0.0: + resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + process@0.11.10: resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} engines: {node: '>= 0.6.0'} @@ -8965,6 +9017,9 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + radix-vue@1.9.17: resolution: {integrity: sha512-mVCu7I2vXt1L2IUYHTt0sZMz7s1K2ZtqKeTIxG3yC5mMFfLBG4FtE1FDeRMpDd+Hhg/ybi9+iXmAP1ISREndoQ==} peerDependencies: @@ -9020,6 +9075,10 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + redis-errors@1.2.0: resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} engines: {node: '>=4'} @@ -9224,6 +9283,10 @@ packages: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} engines: {node: '>= 0.4'} + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -9392,6 +9455,9 @@ packages: smob@1.5.0: resolution: {integrity: sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==} + sonic-boom@4.2.0: + resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==} + sortablejs@1.15.6: resolution: {integrity: sha512-aNfiuwMEpfBM/CN6LY0ibyhxPfPbyFeBTYJKCvzkJ2GkUpazIt3H+QIPAMHwqQ7tMKaHz1Qj+rJJCqljnf4p3A==} @@ -9798,6 +9864,9 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + thread-stream@3.1.0: + resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} + throttle-debounce@5.0.2: resolution: {integrity: sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==} engines: {node: '>=12.22'} @@ -9889,6 +9958,11 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsx@4.20.6: + resolution: {integrity: sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==} + engines: {node: '>=18.0.0'} + hasBin: true + tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} @@ -13101,6 +13175,8 @@ snapshots: '@parcel/watcher-win32-ia32': 2.5.1 '@parcel/watcher-win32-x64': 2.5.1 + '@pinojs/redact@0.4.0': {} + '@pkgjs/parseargs@0.11.0': optional: true @@ -13463,10 +13539,10 @@ snapshots: postcss: 8.5.6 postcss-nested: 5.0.6(postcss@8.5.6) - '@tailwindcss/typography@0.5.19(tailwindcss@3.4.18(yaml@2.8.1))': + '@tailwindcss/typography@0.5.19(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))': dependencies: postcss-selector-parser: 6.0.10 - tailwindcss: 3.4.18(yaml@2.8.1) + tailwindcss: 3.4.18(tsx@4.20.6)(yaml@2.8.1) '@tanstack/store@0.7.7': {} @@ -13835,24 +13911,24 @@ snapshots: dependencies: vite-plugin-pwa: 1.1.0(vite@5.4.21(@types/node@24.10.0)(less@4.4.2)(sass@1.93.3)(terser@5.44.0))(workbox-build@7.3.0)(workbox-window@7.3.0) - '@vitejs/plugin-vue-jsx@4.2.0(vite@6.4.1(@types/node@22.19.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))': + '@vitejs/plugin-vue-jsx@4.2.0(vite@6.4.1(@types/node@22.19.0)(jiti@1.21.7)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))': dependencies: '@babel/core': 7.28.5 '@babel/plugin-transform-typescript': 7.28.5(@babel/core@7.28.5) '@rolldown/pluginutils': 1.0.0-beta.46 '@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.28.5) - vite: 6.4.1(@types/node@22.19.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1) + vite: 6.4.1(@types/node@22.19.0)(jiti@1.21.7)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) vue: 3.5.22(typescript@5.9.3) transitivePeerDependencies: - supports-color - '@vitejs/plugin-vue-jsx@4.2.0(vite@6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))': + '@vitejs/plugin-vue-jsx@4.2.0(vite@6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))': dependencies: '@babel/core': 7.28.5 '@babel/plugin-transform-typescript': 7.28.5(@babel/core@7.28.5) '@rolldown/pluginutils': 1.0.0-beta.46 '@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.28.5) - vite: 6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1) + vite: 6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) vue: 3.5.22(typescript@5.9.3) transitivePeerDependencies: - supports-color @@ -13862,14 +13938,14 @@ snapshots: vite: 5.4.21(@types/node@24.10.0)(less@4.4.2)(sass@1.93.3)(terser@5.44.0) vue: 3.5.22(typescript@5.9.3) - '@vitejs/plugin-vue@5.2.4(vite@6.4.1(@types/node@22.19.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))': + '@vitejs/plugin-vue@5.2.4(vite@6.4.1(@types/node@22.19.0)(jiti@1.21.7)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))': dependencies: - vite: 6.4.1(@types/node@22.19.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1) + vite: 6.4.1(@types/node@22.19.0)(jiti@1.21.7)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) vue: 3.5.22(typescript@5.9.3) - '@vitejs/plugin-vue@5.2.4(vite@6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))': + '@vitejs/plugin-vue@5.2.4(vite@6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))': dependencies: - vite: 6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1) + vite: 6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) vue: 3.5.22(typescript@5.9.3) '@vitest/expect@3.2.4': @@ -13880,13 +13956,13 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@6.4.1(@types/node@22.19.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))': + '@vitest/mocker@3.2.4(vite@6.4.1(@types/node@22.19.0)(jiti@1.21.7)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 6.4.1(@types/node@22.19.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1) + vite: 6.4.1(@types/node@22.19.0)(jiti@1.21.7)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) '@vitest/pretty-format@3.2.4': dependencies: @@ -13996,14 +14072,14 @@ snapshots: dependencies: '@vue/devtools-kit': 7.7.7 - '@vue/devtools-core@7.7.7(vite@6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))': + '@vue/devtools-core@7.7.7(vite@6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))': dependencies: '@vue/devtools-kit': 7.7.7 '@vue/devtools-shared': 7.7.7 mitt: 3.0.1 nanoid: 5.1.6 pathe: 2.0.3 - vite-hot-client: 2.1.0(vite@6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)) + vite-hot-client: 2.1.0(vite@6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) vue: 3.5.22(typescript@5.9.3) transitivePeerDependencies: - vite @@ -14410,6 +14486,8 @@ snapshots: at-least-node@1.0.0: {} + atomic-sleep@1.0.0: {} + atomically@2.1.0: dependencies: stubborn-fs: 2.0.0 @@ -15831,13 +15909,13 @@ snapshots: optionalDependencies: '@typescript-eslint/eslint-plugin': 8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - eslint-plugin-vitest@0.5.4(@typescript-eslint/eslint-plugin@8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)(vitest@3.2.4(@types/node@24.10.0)(happy-dom@17.6.3)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)): + eslint-plugin-vitest@0.5.4(@typescript-eslint/eslint-plugin@8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)(vitest@3.2.4(@types/node@24.10.0)(happy-dom@17.6.3)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)): dependencies: '@typescript-eslint/utils': 7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) optionalDependencies: '@typescript-eslint/eslint-plugin': 8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - vitest: 3.2.4(@types/node@24.10.0)(happy-dom@17.6.3)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1) + vitest: 3.2.4(@types/node@24.10.0)(happy-dom@17.6.3)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) transitivePeerDependencies: - supports-color - typescript @@ -17666,6 +17744,8 @@ snapshots: ohash@2.0.11: {} + on-exit-leak-free@2.1.2: {} + on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -17762,6 +17842,13 @@ snapshots: p-map@7.0.3: {} + p-queue@9.0.0: + dependencies: + eventemitter3: 5.0.1 + p-timeout: 7.0.1 + + p-timeout@7.0.1: {} + p-try@2.2.0: {} package-json-from-dist@1.0.1: {} @@ -17932,6 +18019,26 @@ snapshots: optionalDependencies: typescript: 5.9.3 + pino-abstract-transport@2.0.0: + dependencies: + split2: 4.2.0 + + pino-std-serializers@7.0.0: {} + + pino@10.1.0: + dependencies: + '@pinojs/redact': 0.4.0 + atomic-sleep: 1.0.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 2.0.0 + pino-std-serializers: 7.0.0 + process-warning: 5.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.0 + thread-stream: 3.1.0 + pirates@4.0.7: {} pkg-types@1.3.1: @@ -18139,12 +18246,13 @@ snapshots: '@csstools/utilities': 2.0.0(postcss@8.5.6) postcss: 8.5.6 - postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.6)(yaml@2.8.1): + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.20.6)(yaml@2.8.1): dependencies: lilconfig: 3.1.3 optionalDependencies: jiti: 1.21.7 postcss: 8.5.6 + tsx: 4.20.6 yaml: 2.8.1 postcss-logical@8.1.0(postcss@8.5.6): @@ -18490,6 +18598,8 @@ snapshots: process-nextick-args@2.0.1: {} + process-warning@5.0.0: {} + process@0.11.10: {} property-information@7.1.0: {} @@ -18538,6 +18648,8 @@ snapshots: queue-microtask@1.2.3: {} + quick-format-unescaped@4.0.4: {} + radix-vue@1.9.17(vue@3.5.22(typescript@5.9.3)): dependencies: '@floating-ui/dom': 1.7.4 @@ -18626,6 +18738,8 @@ snapshots: readdirp@4.1.2: {} + real-require@0.2.0: {} + redis-errors@1.2.0: {} redis-parser@3.0.0: @@ -18844,6 +18958,8 @@ snapshots: es-errors: 1.3.0 is-regex: 1.2.1 + safe-stable-stringify@2.5.0: {} + safer-buffer@2.1.2: {} sass@1.93.3: @@ -19052,6 +19168,10 @@ snapshots: smob@1.5.0: {} + sonic-boom@4.2.0: + dependencies: + atomic-sleep: 1.0.0 + sortablejs@1.15.6: {} source-map-js@1.2.1: {} @@ -19439,11 +19559,11 @@ snapshots: tailwind-merge@2.6.0: {} - tailwindcss-animate@1.0.7(tailwindcss@3.4.18(yaml@2.8.1)): + tailwindcss-animate@1.0.7(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1)): dependencies: - tailwindcss: 3.4.18(yaml@2.8.1) + tailwindcss: 3.4.18(tsx@4.20.6)(yaml@2.8.1) - tailwindcss@3.4.18(yaml@2.8.1): + tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1): dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -19462,7 +19582,7 @@ snapshots: postcss: 8.5.6 postcss-import: 15.1.0(postcss@8.5.6) postcss-js: 4.1.0(postcss@8.5.6) - postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.6)(yaml@2.8.1) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.20.6)(yaml@2.8.1) postcss-nested: 6.2.0(postcss@8.5.6) postcss-selector-parser: 6.1.2 resolve: 1.22.11 @@ -19543,6 +19663,10 @@ snapshots: dependencies: any-promise: 1.3.0 + thread-stream@3.1.0: + dependencies: + real-require: 0.2.0 + throttle-debounce@5.0.2: {} through@2.3.8: {} @@ -19609,6 +19733,13 @@ snapshots: tslib@2.8.1: {} + tsx@4.20.6: + dependencies: + esbuild: 0.25.3 + get-tsconfig: 4.13.0 + optionalDependencies: + fsevents: 2.3.3 + tunnel-agent@0.6.0: dependencies: safe-buffer: 5.2.1 @@ -19965,17 +20096,17 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite-hot-client@2.1.0(vite@6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)): + vite-hot-client@2.1.0(vite@6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)): dependencies: - vite: 6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1) + vite: 6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) - vite-node@3.2.4(@types/node@22.19.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1): + vite-node@3.2.4(@types/node@22.19.0)(jiti@1.21.7)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 6.4.1(@types/node@22.19.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1) + vite: 6.4.1(@types/node@22.19.0)(jiti@1.21.7)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) transitivePeerDependencies: - '@types/node' - jiti @@ -19990,13 +20121,13 @@ snapshots: - tsx - yaml - vite-node@3.2.4(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1): + vite-node@3.2.4(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1) + vite: 6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) transitivePeerDependencies: - '@types/node' - jiti @@ -20012,16 +20143,16 @@ snapshots: - yaml optional: true - vite-plugin-compression@0.5.1(vite@6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)): + vite-plugin-compression@0.5.1(vite@6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)): dependencies: chalk: 4.1.2 debug: 4.4.3 fs-extra: 10.1.0 - vite: 6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1) + vite: 6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) transitivePeerDependencies: - supports-color - vite-plugin-dts@4.5.4(@types/node@24.10.0)(rollup@4.52.5)(typescript@5.9.3)(vite@6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)): + vite-plugin-dts@4.5.4(@types/node@24.10.0)(rollup@4.52.5)(typescript@5.9.3)(vite@6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)): dependencies: '@microsoft/api-extractor': 7.54.0(@types/node@24.10.0) '@rollup/pluginutils': 5.3.0(rollup@4.52.5) @@ -20034,13 +20165,13 @@ snapshots: magic-string: 0.30.21 typescript: 5.9.3 optionalDependencies: - vite: 6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1) + vite: 6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) transitivePeerDependencies: - '@types/node' - rollup - supports-color - vite-plugin-html@3.2.2(vite@6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)): + vite-plugin-html@3.2.2(vite@6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)): dependencies: '@rollup/pluginutils': 4.2.1 colorette: 2.0.20 @@ -20054,9 +20185,9 @@ snapshots: html-minifier-terser: 6.1.0 node-html-parser: 5.4.2 pathe: 0.2.0 - vite: 6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1) + vite: 6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) - vite-plugin-inspect@0.8.9(rollup@4.52.5)(vite@6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)): + vite-plugin-inspect@0.8.9(rollup@4.52.5)(vite@6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)): dependencies: '@antfu/utils': 0.7.10 '@rollup/pluginutils': 5.3.0(rollup@4.52.5) @@ -20067,7 +20198,7 @@ snapshots: perfect-debounce: 1.0.0 picocolors: 1.1.1 sirv: 3.0.2 - vite: 6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1) + vite: 6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) transitivePeerDependencies: - rollup - supports-color @@ -20090,34 +20221,34 @@ snapshots: transitivePeerDependencies: - supports-color - vite-plugin-pwa@1.1.0(vite@6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))(workbox-build@7.3.0)(workbox-window@7.3.0): + vite-plugin-pwa@1.1.0(vite@6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(workbox-build@7.3.0)(workbox-window@7.3.0): dependencies: debug: 4.4.3 pretty-bytes: 6.1.1 tinyglobby: 0.2.15 - vite: 6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1) + vite: 6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) workbox-build: 7.3.0 workbox-window: 7.3.0 transitivePeerDependencies: - supports-color - vite-plugin-vue-devtools@7.7.7(rollup@4.52.5)(vite@6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3)): + vite-plugin-vue-devtools@7.7.7(rollup@4.52.5)(vite@6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3)): dependencies: - '@vue/devtools-core': 7.7.7(vite@6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3)) + '@vue/devtools-core': 7.7.7(vite@6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3)) '@vue/devtools-kit': 7.7.7 '@vue/devtools-shared': 7.7.7 execa: 9.6.0 sirv: 3.0.2 - vite: 6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1) - vite-plugin-inspect: 0.8.9(rollup@4.52.5)(vite@6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)) - vite-plugin-vue-inspector: 5.3.2(vite@6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)) + vite: 6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite-plugin-inspect: 0.8.9(rollup@4.52.5)(vite@6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + vite-plugin-vue-inspector: 5.3.2(vite@6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) transitivePeerDependencies: - '@nuxt/kit' - rollup - supports-color - vue - vite-plugin-vue-inspector@5.3.2(vite@6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)): + vite-plugin-vue-inspector@5.3.2(vite@6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)): dependencies: '@babel/core': 7.28.5 '@babel/plugin-proposal-decorators': 7.28.0(@babel/core@7.28.5) @@ -20128,7 +20259,7 @@ snapshots: '@vue/compiler-dom': 3.5.22 kolorist: 1.8.0 magic-string: 0.30.21 - vite: 6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1) + vite: 6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) transitivePeerDependencies: - supports-color @@ -20144,7 +20275,7 @@ snapshots: sass: 1.93.3 terser: 5.44.0 - vite@6.4.1(@types/node@22.19.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1): + vite@6.4.1(@types/node@22.19.0)(jiti@1.21.7)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): dependencies: esbuild: 0.25.3 fdir: 6.5.0(picomatch@4.0.3) @@ -20155,13 +20286,14 @@ snapshots: optionalDependencies: '@types/node': 22.19.0 fsevents: 2.3.3 - jiti: 2.6.1 + jiti: 1.21.7 less: 4.4.2 sass: 1.93.3 terser: 5.44.0 + tsx: 4.20.6 yaml: 2.8.1 - vite@6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1): + vite@6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): dependencies: esbuild: 0.25.3 fdir: 6.5.0(picomatch@4.0.3) @@ -20176,6 +20308,7 @@ snapshots: less: 4.4.2 sass: 1.93.3 terser: 5.44.0 + tsx: 4.20.6 yaml: 2.8.1 vitepress-plugin-group-icons@1.6.5(vite@5.4.21(@types/node@24.10.0)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)): @@ -20237,11 +20370,11 @@ snapshots: - typescript - universal-cookie - vitest@3.2.4(@types/node@22.19.0)(happy-dom@17.6.3)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1): + vitest@3.2.4(@types/node@22.19.0)(happy-dom@17.6.3)(jiti@1.21.7)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@6.4.1(@types/node@22.19.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)) + '@vitest/mocker': 3.2.4(vite@6.4.1(@types/node@22.19.0)(jiti@1.21.7)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -20259,8 +20392,8 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 6.4.1(@types/node@22.19.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1) - vite-node: 3.2.4(@types/node@22.19.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1) + vite: 6.4.1(@types/node@22.19.0)(jiti@1.21.7)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite-node: 3.2.4(@types/node@22.19.0)(jiti@1.21.7)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.19.0 @@ -20279,11 +20412,11 @@ snapshots: - tsx - yaml - vitest@3.2.4(@types/node@24.10.0)(happy-dom@17.6.3)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1): + vitest@3.2.4(@types/node@24.10.0)(happy-dom@17.6.3)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@6.4.1(@types/node@22.19.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)) + '@vitest/mocker': 3.2.4(vite@6.4.1(@types/node@22.19.0)(jiti@1.21.7)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -20301,8 +20434,8 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1) - vite-node: 3.2.4(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1) + vite: 6.4.1(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite-node: 3.2.4(@types/node@24.10.0)(jiti@2.6.1)(less@4.4.2)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.10.0