feat: add Finance MCP workflow
Some checks failed
Some checks failed
This commit is contained in:
101
.gitea/workflows/deploy-mcp.yml
Normal file
101
.gitea/workflows/deploy-mcp.yml
Normal file
@@ -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)"
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -1,12 +1,23 @@
|
|||||||
{
|
{
|
||||||
"name": "@vben/finance-mcp-service",
|
"name": "@vben/finance-mcp-service",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "MCP service exposing Finwise Pro finance APIs",
|
"description": "MCP service exposing Finwise Pro finance APIs",
|
||||||
"scripts": {
|
"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": {},
|
"dependencies": {
|
||||||
"devDependencies": {}
|
"p-queue": "^9.0.0",
|
||||||
|
"pino": "^10.1.0",
|
||||||
|
"zod": "catalog:"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "catalog:",
|
||||||
|
"tsx": "^4.20.6",
|
||||||
|
"typescript": "catalog:"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
394
apps/finance-mcp-service/src/client/finance-client.ts
Normal file
394
apps/finance-mcp-service/src/client/finance-client.ts
Normal file
@@ -0,0 +1,394 @@
|
|||||||
|
import { Buffer } from 'node:buffer';
|
||||||
|
|
||||||
|
interface FinanceEnvelope<T> {
|
||||||
|
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<string, string> = {};
|
||||||
|
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<string, number | string> = {};
|
||||||
|
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<string, boolean | string> = {};
|
||||||
|
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<string, boolean | string> = {};
|
||||||
|
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<string, string> = { 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<FinanceEnvelope<unknown>>(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<string, unknown>) {
|
||||||
|
return this.request('GET', path, { query });
|
||||||
|
}
|
||||||
|
|
||||||
|
private async parseEnvelope(response: Response, path: string) {
|
||||||
|
const payload =
|
||||||
|
await this.safeParseEnvelope<FinanceEnvelope<unknown>>(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<string, unknown>;
|
||||||
|
} = {},
|
||||||
|
) {
|
||||||
|
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<T>(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');
|
||||||
57
apps/finance-mcp-service/src/config.ts
Normal file
57
apps/finance-mcp-service/src/config.ts
Normal file
@@ -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,
|
||||||
|
};
|
||||||
@@ -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');
|
|
||||||
@@ -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();
|
|
||||||
35
apps/finance-mcp-service/src/index.ts
Normal file
35
apps/finance-mcp-service/src/index.ts
Normal file
@@ -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();
|
||||||
12
apps/finance-mcp-service/src/logger.ts
Normal file
12
apps/finance-mcp-service/src/logger.ts
Normal file
@@ -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;
|
||||||
270
apps/finance-mcp-service/src/server/mcp-server.ts
Normal file
270
apps/finance-mcp-service/src/server/mcp-server.ts
Normal file
@@ -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<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Omit<McpToolDefinition, 'handler'>>;
|
||||||
|
|
||||||
|
private readonly options: McpServerOptions;
|
||||||
|
|
||||||
|
private readonly queue: PQueue;
|
||||||
|
|
||||||
|
private readonly tools: Map<string, McpToolDefinition>;
|
||||||
|
|
||||||
|
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<string, unknown>;
|
||||||
|
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<string, unknown>) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
789
apps/finance-mcp-service/src/tools/finance.ts
Normal file
789
apps/finance-mcp-service/src/tools/finance.ts
Normal file
@@ -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<string, unknown>;
|
||||||
|
|
||||||
|
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<string, unknown> = {};
|
||||||
|
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<string, unknown> = {};
|
||||||
|
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<string, unknown> = {};
|
||||||
|
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<string, unknown>,
|
||||||
|
options: CreateTransactionOptions = {},
|
||||||
|
) => {
|
||||||
|
const payload: Record<string, unknown> = {
|
||||||
|
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<string, unknown>) => {
|
||||||
|
const payload: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
44
apps/finance-mcp-service/src/types.ts
Normal file
44
apps/finance-mcp-service/src/types.ts
Normal file
@@ -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<string, unknown>;
|
||||||
|
|
||||||
|
export interface ToolContext {
|
||||||
|
logger: Logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type McpToolHandler = (
|
||||||
|
args: Record<string, unknown>,
|
||||||
|
context: ToolContext,
|
||||||
|
) => Promise<McpToolResult>;
|
||||||
|
|
||||||
|
export interface McpToolDefinition {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
inputSchema: JsonSchema;
|
||||||
|
outputSchema?: JsonSchema;
|
||||||
|
handler: McpToolHandler;
|
||||||
|
}
|
||||||
9
apps/finance-mcp-service/src/utils/mcp.ts
Normal file
9
apps/finance-mcp-service/src/utils/mcp.ts
Normal file
@@ -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 }],
|
||||||
|
});
|
||||||
94
apps/finance-mcp-service/src/utils/validation.ts
Normal file
94
apps/finance-mcp-service/src/utils/validation.ts
Normal file
@@ -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;
|
||||||
|
};
|
||||||
17
apps/finance-mcp-service/tsconfig.json
Normal file
17
apps/finance-mcp-service/tsconfig.json
Normal file
@@ -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"]
|
||||||
|
}
|
||||||
287
pnpm-lock.yaml
generated
287
pnpm-lock.yaml
generated
@@ -97,7 +97,7 @@ catalogs:
|
|||||||
specifier: ^4.3.9
|
specifier: ^4.3.9
|
||||||
version: 4.3.9
|
version: 4.3.9
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^22.16.0
|
specifier: 22.19.0
|
||||||
version: 22.19.0
|
version: 22.19.0
|
||||||
'@types/nprogress':
|
'@types/nprogress':
|
||||||
specifier: ^0.2.3
|
specifier: ^0.2.3
|
||||||
@@ -442,7 +442,7 @@ catalogs:
|
|||||||
specifier: ^2.5.4
|
specifier: ^2.5.4
|
||||||
version: 2.6.0
|
version: 2.6.0
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.8.3
|
specifier: 5.9.3
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
unbuild:
|
unbuild:
|
||||||
specifier: ^3.5.0
|
specifier: ^3.5.0
|
||||||
@@ -511,7 +511,7 @@ catalogs:
|
|||||||
specifier: ^1.6.2
|
specifier: ^1.6.2
|
||||||
version: 1.6.3
|
version: 1.6.3
|
||||||
zod:
|
zod:
|
||||||
specifier: ^3.25.67
|
specifier: 3.25.76
|
||||||
version: 3.25.76
|
version: 3.25.76
|
||||||
zod-defaults:
|
zod-defaults:
|
||||||
specifier: ^0.1.3
|
specifier: ^0.1.3
|
||||||
@@ -570,10 +570,10 @@ importers:
|
|||||||
version: link:scripts/vsh
|
version: link:scripts/vsh
|
||||||
'@vitejs/plugin-vue':
|
'@vitejs/plugin-vue':
|
||||||
specifier: 'catalog:'
|
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':
|
'@vitejs/plugin-vue-jsx':
|
||||||
specifier: 'catalog:'
|
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':
|
'@vue/test-utils':
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 2.4.6
|
version: 2.4.6
|
||||||
@@ -603,7 +603,7 @@ importers:
|
|||||||
version: 6.1.0
|
version: 6.1.0
|
||||||
tailwindcss:
|
tailwindcss:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 3.4.18(yaml@2.8.1)
|
version: 3.4.18(tsx@4.20.6)(yaml@2.8.1)
|
||||||
turbo:
|
turbo:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 2.6.0
|
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))
|
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:
|
vite:
|
||||||
specifier: 'catalog:'
|
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:
|
vitest:
|
||||||
specifier: 'catalog:'
|
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:
|
vue:
|
||||||
specifier: ^3.5.17
|
specifier: ^3.5.17
|
||||||
version: 3.5.22(typescript@5.9.3)
|
version: 3.5.22(typescript@5.9.3)
|
||||||
@@ -648,7 +648,27 @@ importers:
|
|||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 1.15.4
|
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:
|
apps/web-antd:
|
||||||
dependencies:
|
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))
|
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:
|
eslint-plugin-vitest:
|
||||||
specifier: 'catalog:'
|
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:
|
eslint-plugin-vue:
|
||||||
specifier: 'catalog:'
|
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)))
|
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)
|
version: 0.0.0-insiders.565cd3e(postcss@8.5.6)
|
||||||
'@tailwindcss/typography':
|
'@tailwindcss/typography':
|
||||||
specifier: 'catalog:'
|
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:
|
autoprefixer:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 10.4.21(postcss@8.5.6)
|
version: 10.4.21(postcss@8.5.6)
|
||||||
@@ -1120,10 +1140,10 @@ importers:
|
|||||||
version: 10.4.0(postcss@8.5.6)
|
version: 10.4.0(postcss@8.5.6)
|
||||||
tailwindcss:
|
tailwindcss:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 3.4.18(yaml@2.8.1)
|
version: 3.4.18(tsx@4.20.6)(yaml@2.8.1)
|
||||||
tailwindcss-animate:
|
tailwindcss-animate:
|
||||||
specifier: 'catalog:'
|
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:
|
devDependencies:
|
||||||
'@types/postcss-import':
|
'@types/postcss-import':
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
@@ -1136,7 +1156,7 @@ importers:
|
|||||||
version: link:../../packages/types
|
version: link:../../packages/types
|
||||||
vite:
|
vite:
|
||||||
specifier: 'catalog:'
|
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:
|
internal/vite-config:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -1166,10 +1186,10 @@ importers:
|
|||||||
version: 2.0.3
|
version: 2.0.3
|
||||||
vite-plugin-pwa:
|
vite-plugin-pwa:
|
||||||
specifier: 'catalog:'
|
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:
|
vite-plugin-vue-devtools:
|
||||||
specifier: 'catalog:'
|
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:
|
devDependencies:
|
||||||
'@pnpm/workspace.read-manifest':
|
'@pnpm/workspace.read-manifest':
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
@@ -1185,10 +1205,10 @@ importers:
|
|||||||
version: link:../node-utils
|
version: link:../node-utils
|
||||||
'@vitejs/plugin-vue':
|
'@vitejs/plugin-vue':
|
||||||
specifier: 'catalog:'
|
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':
|
'@vitejs/plugin-vue-jsx':
|
||||||
specifier: 'catalog:'
|
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:
|
dayjs:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 1.11.19
|
version: 1.11.19
|
||||||
@@ -1206,16 +1226,16 @@ importers:
|
|||||||
version: 1.93.3
|
version: 1.93.3
|
||||||
vite:
|
vite:
|
||||||
specifier: 'catalog:'
|
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:
|
vite-plugin-compression:
|
||||||
specifier: 'catalog:'
|
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:
|
vite-plugin-dts:
|
||||||
specifier: 'catalog:'
|
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:
|
vite-plugin-html:
|
||||||
specifier: 'catalog:'
|
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:
|
vite-plugin-lazy-import:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 1.0.7
|
version: 1.0.7
|
||||||
@@ -3839,6 +3859,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==}
|
resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==}
|
||||||
engines: {node: '>= 10.0.0'}
|
engines: {node: '>= 10.0.0'}
|
||||||
|
|
||||||
|
'@pinojs/redact@0.4.0':
|
||||||
|
resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==}
|
||||||
|
|
||||||
'@pkgjs/parseargs@0.11.0':
|
'@pkgjs/parseargs@0.11.0':
|
||||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
@@ -5058,6 +5081,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==}
|
resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==}
|
||||||
engines: {node: '>= 4.0.0'}
|
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:
|
atomically@2.1.0:
|
||||||
resolution: {integrity: sha512-+gDffFXRW6sl/HCwbta7zK4uNqbPjv4YJEAdz7Vu+FLQHe77eZ4bvbJGi4hE0QPeJlMYMA3piXEr1UL3dAwx7Q==}
|
resolution: {integrity: sha512-+gDffFXRW6sl/HCwbta7zK4uNqbPjv4YJEAdz7Vu+FLQHe77eZ4bvbJGi4hE0QPeJlMYMA3piXEr1UL3dAwx7Q==}
|
||||||
|
|
||||||
@@ -8040,6 +8067,10 @@ packages:
|
|||||||
ohash@2.0.11:
|
ohash@2.0.11:
|
||||||
resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==}
|
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:
|
on-finished@2.4.1:
|
||||||
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
|
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
@@ -8117,6 +8148,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==}
|
resolution: {integrity: sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==}
|
||||||
engines: {node: '>=18'}
|
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:
|
p-try@2.2.0:
|
||||||
resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
|
resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -8323,6 +8362,16 @@ packages:
|
|||||||
typescript:
|
typescript:
|
||||||
optional: true
|
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:
|
pirates@4.0.7:
|
||||||
resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
|
resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
@@ -8914,6 +8963,9 @@ packages:
|
|||||||
process-nextick-args@2.0.1:
|
process-nextick-args@2.0.1:
|
||||||
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
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:
|
process@0.11.10:
|
||||||
resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
|
resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
|
||||||
engines: {node: '>= 0.6.0'}
|
engines: {node: '>= 0.6.0'}
|
||||||
@@ -8965,6 +9017,9 @@ packages:
|
|||||||
queue-microtask@1.2.3:
|
queue-microtask@1.2.3:
|
||||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||||
|
|
||||||
|
quick-format-unescaped@4.0.4:
|
||||||
|
resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
|
||||||
|
|
||||||
radix-vue@1.9.17:
|
radix-vue@1.9.17:
|
||||||
resolution: {integrity: sha512-mVCu7I2vXt1L2IUYHTt0sZMz7s1K2ZtqKeTIxG3yC5mMFfLBG4FtE1FDeRMpDd+Hhg/ybi9+iXmAP1ISREndoQ==}
|
resolution: {integrity: sha512-mVCu7I2vXt1L2IUYHTt0sZMz7s1K2ZtqKeTIxG3yC5mMFfLBG4FtE1FDeRMpDd+Hhg/ybi9+iXmAP1ISREndoQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -9020,6 +9075,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
|
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
|
||||||
engines: {node: '>= 14.18.0'}
|
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:
|
redis-errors@1.2.0:
|
||||||
resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==}
|
resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -9224,6 +9283,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==}
|
resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
safe-stable-stringify@2.5.0:
|
||||||
|
resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
safer-buffer@2.1.2:
|
safer-buffer@2.1.2:
|
||||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||||
|
|
||||||
@@ -9392,6 +9455,9 @@ packages:
|
|||||||
smob@1.5.0:
|
smob@1.5.0:
|
||||||
resolution: {integrity: sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==}
|
resolution: {integrity: sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==}
|
||||||
|
|
||||||
|
sonic-boom@4.2.0:
|
||||||
|
resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==}
|
||||||
|
|
||||||
sortablejs@1.15.6:
|
sortablejs@1.15.6:
|
||||||
resolution: {integrity: sha512-aNfiuwMEpfBM/CN6LY0ibyhxPfPbyFeBTYJKCvzkJ2GkUpazIt3H+QIPAMHwqQ7tMKaHz1Qj+rJJCqljnf4p3A==}
|
resolution: {integrity: sha512-aNfiuwMEpfBM/CN6LY0ibyhxPfPbyFeBTYJKCvzkJ2GkUpazIt3H+QIPAMHwqQ7tMKaHz1Qj+rJJCqljnf4p3A==}
|
||||||
|
|
||||||
@@ -9798,6 +9864,9 @@ packages:
|
|||||||
thenify@3.3.1:
|
thenify@3.3.1:
|
||||||
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
|
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
|
||||||
|
|
||||||
|
thread-stream@3.1.0:
|
||||||
|
resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==}
|
||||||
|
|
||||||
throttle-debounce@5.0.2:
|
throttle-debounce@5.0.2:
|
||||||
resolution: {integrity: sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==}
|
resolution: {integrity: sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==}
|
||||||
engines: {node: '>=12.22'}
|
engines: {node: '>=12.22'}
|
||||||
@@ -9889,6 +9958,11 @@ packages:
|
|||||||
tslib@2.8.1:
|
tslib@2.8.1:
|
||||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
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:
|
tunnel-agent@0.6.0:
|
||||||
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
|
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
|
||||||
|
|
||||||
@@ -13101,6 +13175,8 @@ snapshots:
|
|||||||
'@parcel/watcher-win32-ia32': 2.5.1
|
'@parcel/watcher-win32-ia32': 2.5.1
|
||||||
'@parcel/watcher-win32-x64': 2.5.1
|
'@parcel/watcher-win32-x64': 2.5.1
|
||||||
|
|
||||||
|
'@pinojs/redact@0.4.0': {}
|
||||||
|
|
||||||
'@pkgjs/parseargs@0.11.0':
|
'@pkgjs/parseargs@0.11.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -13463,10 +13539,10 @@ snapshots:
|
|||||||
postcss: 8.5.6
|
postcss: 8.5.6
|
||||||
postcss-nested: 5.0.6(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:
|
dependencies:
|
||||||
postcss-selector-parser: 6.0.10
|
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': {}
|
'@tanstack/store@0.7.7': {}
|
||||||
|
|
||||||
@@ -13835,24 +13911,24 @@ snapshots:
|
|||||||
dependencies:
|
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)
|
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:
|
dependencies:
|
||||||
'@babel/core': 7.28.5
|
'@babel/core': 7.28.5
|
||||||
'@babel/plugin-transform-typescript': 7.28.5(@babel/core@7.28.5)
|
'@babel/plugin-transform-typescript': 7.28.5(@babel/core@7.28.5)
|
||||||
'@rolldown/pluginutils': 1.0.0-beta.46
|
'@rolldown/pluginutils': 1.0.0-beta.46
|
||||||
'@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.28.5)
|
'@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)
|
vue: 3.5.22(typescript@5.9.3)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- 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:
|
dependencies:
|
||||||
'@babel/core': 7.28.5
|
'@babel/core': 7.28.5
|
||||||
'@babel/plugin-transform-typescript': 7.28.5(@babel/core@7.28.5)
|
'@babel/plugin-transform-typescript': 7.28.5(@babel/core@7.28.5)
|
||||||
'@rolldown/pluginutils': 1.0.0-beta.46
|
'@rolldown/pluginutils': 1.0.0-beta.46
|
||||||
'@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.28.5)
|
'@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)
|
vue: 3.5.22(typescript@5.9.3)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- 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)
|
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)
|
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:
|
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)
|
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:
|
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)
|
vue: 3.5.22(typescript@5.9.3)
|
||||||
|
|
||||||
'@vitest/expect@3.2.4':
|
'@vitest/expect@3.2.4':
|
||||||
@@ -13880,13 +13956,13 @@ snapshots:
|
|||||||
chai: 5.3.3
|
chai: 5.3.3
|
||||||
tinyrainbow: 2.0.0
|
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:
|
dependencies:
|
||||||
'@vitest/spy': 3.2.4
|
'@vitest/spy': 3.2.4
|
||||||
estree-walker: 3.0.3
|
estree-walker: 3.0.3
|
||||||
magic-string: 0.30.21
|
magic-string: 0.30.21
|
||||||
optionalDependencies:
|
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':
|
'@vitest/pretty-format@3.2.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -13996,14 +14072,14 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@vue/devtools-kit': 7.7.7
|
'@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:
|
dependencies:
|
||||||
'@vue/devtools-kit': 7.7.7
|
'@vue/devtools-kit': 7.7.7
|
||||||
'@vue/devtools-shared': 7.7.7
|
'@vue/devtools-shared': 7.7.7
|
||||||
mitt: 3.0.1
|
mitt: 3.0.1
|
||||||
nanoid: 5.1.6
|
nanoid: 5.1.6
|
||||||
pathe: 2.0.3
|
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)
|
vue: 3.5.22(typescript@5.9.3)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- vite
|
- vite
|
||||||
@@ -14410,6 +14486,8 @@ snapshots:
|
|||||||
|
|
||||||
at-least-node@1.0.0: {}
|
at-least-node@1.0.0: {}
|
||||||
|
|
||||||
|
atomic-sleep@1.0.0: {}
|
||||||
|
|
||||||
atomically@2.1.0:
|
atomically@2.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
stubborn-fs: 2.0.0
|
stubborn-fs: 2.0.0
|
||||||
@@ -15831,13 +15909,13 @@ snapshots:
|
|||||||
optionalDependencies:
|
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)
|
'@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:
|
dependencies:
|
||||||
'@typescript-eslint/utils': 7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
|
'@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)
|
eslint: 9.39.1(jiti@2.6.1)
|
||||||
optionalDependencies:
|
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)
|
'@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:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
- typescript
|
- typescript
|
||||||
@@ -17666,6 +17744,8 @@ snapshots:
|
|||||||
|
|
||||||
ohash@2.0.11: {}
|
ohash@2.0.11: {}
|
||||||
|
|
||||||
|
on-exit-leak-free@2.1.2: {}
|
||||||
|
|
||||||
on-finished@2.4.1:
|
on-finished@2.4.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
ee-first: 1.1.1
|
ee-first: 1.1.1
|
||||||
@@ -17762,6 +17842,13 @@ snapshots:
|
|||||||
|
|
||||||
p-map@7.0.3: {}
|
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: {}
|
p-try@2.2.0: {}
|
||||||
|
|
||||||
package-json-from-dist@1.0.1: {}
|
package-json-from-dist@1.0.1: {}
|
||||||
@@ -17932,6 +18019,26 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 5.9.3
|
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: {}
|
pirates@4.0.7: {}
|
||||||
|
|
||||||
pkg-types@1.3.1:
|
pkg-types@1.3.1:
|
||||||
@@ -18139,12 +18246,13 @@ snapshots:
|
|||||||
'@csstools/utilities': 2.0.0(postcss@8.5.6)
|
'@csstools/utilities': 2.0.0(postcss@8.5.6)
|
||||||
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:
|
dependencies:
|
||||||
lilconfig: 3.1.3
|
lilconfig: 3.1.3
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
jiti: 1.21.7
|
jiti: 1.21.7
|
||||||
postcss: 8.5.6
|
postcss: 8.5.6
|
||||||
|
tsx: 4.20.6
|
||||||
yaml: 2.8.1
|
yaml: 2.8.1
|
||||||
|
|
||||||
postcss-logical@8.1.0(postcss@8.5.6):
|
postcss-logical@8.1.0(postcss@8.5.6):
|
||||||
@@ -18490,6 +18598,8 @@ snapshots:
|
|||||||
|
|
||||||
process-nextick-args@2.0.1: {}
|
process-nextick-args@2.0.1: {}
|
||||||
|
|
||||||
|
process-warning@5.0.0: {}
|
||||||
|
|
||||||
process@0.11.10: {}
|
process@0.11.10: {}
|
||||||
|
|
||||||
property-information@7.1.0: {}
|
property-information@7.1.0: {}
|
||||||
@@ -18538,6 +18648,8 @@ snapshots:
|
|||||||
|
|
||||||
queue-microtask@1.2.3: {}
|
queue-microtask@1.2.3: {}
|
||||||
|
|
||||||
|
quick-format-unescaped@4.0.4: {}
|
||||||
|
|
||||||
radix-vue@1.9.17(vue@3.5.22(typescript@5.9.3)):
|
radix-vue@1.9.17(vue@3.5.22(typescript@5.9.3)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@floating-ui/dom': 1.7.4
|
'@floating-ui/dom': 1.7.4
|
||||||
@@ -18626,6 +18738,8 @@ snapshots:
|
|||||||
|
|
||||||
readdirp@4.1.2: {}
|
readdirp@4.1.2: {}
|
||||||
|
|
||||||
|
real-require@0.2.0: {}
|
||||||
|
|
||||||
redis-errors@1.2.0: {}
|
redis-errors@1.2.0: {}
|
||||||
|
|
||||||
redis-parser@3.0.0:
|
redis-parser@3.0.0:
|
||||||
@@ -18844,6 +18958,8 @@ snapshots:
|
|||||||
es-errors: 1.3.0
|
es-errors: 1.3.0
|
||||||
is-regex: 1.2.1
|
is-regex: 1.2.1
|
||||||
|
|
||||||
|
safe-stable-stringify@2.5.0: {}
|
||||||
|
|
||||||
safer-buffer@2.1.2: {}
|
safer-buffer@2.1.2: {}
|
||||||
|
|
||||||
sass@1.93.3:
|
sass@1.93.3:
|
||||||
@@ -19052,6 +19168,10 @@ snapshots:
|
|||||||
|
|
||||||
smob@1.5.0: {}
|
smob@1.5.0: {}
|
||||||
|
|
||||||
|
sonic-boom@4.2.0:
|
||||||
|
dependencies:
|
||||||
|
atomic-sleep: 1.0.0
|
||||||
|
|
||||||
sortablejs@1.15.6: {}
|
sortablejs@1.15.6: {}
|
||||||
|
|
||||||
source-map-js@1.2.1: {}
|
source-map-js@1.2.1: {}
|
||||||
@@ -19439,11 +19559,11 @@ snapshots:
|
|||||||
|
|
||||||
tailwind-merge@2.6.0: {}
|
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:
|
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:
|
dependencies:
|
||||||
'@alloc/quick-lru': 5.2.0
|
'@alloc/quick-lru': 5.2.0
|
||||||
arg: 5.0.2
|
arg: 5.0.2
|
||||||
@@ -19462,7 +19582,7 @@ snapshots:
|
|||||||
postcss: 8.5.6
|
postcss: 8.5.6
|
||||||
postcss-import: 15.1.0(postcss@8.5.6)
|
postcss-import: 15.1.0(postcss@8.5.6)
|
||||||
postcss-js: 4.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-nested: 6.2.0(postcss@8.5.6)
|
||||||
postcss-selector-parser: 6.1.2
|
postcss-selector-parser: 6.1.2
|
||||||
resolve: 1.22.11
|
resolve: 1.22.11
|
||||||
@@ -19543,6 +19663,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
any-promise: 1.3.0
|
any-promise: 1.3.0
|
||||||
|
|
||||||
|
thread-stream@3.1.0:
|
||||||
|
dependencies:
|
||||||
|
real-require: 0.2.0
|
||||||
|
|
||||||
throttle-debounce@5.0.2: {}
|
throttle-debounce@5.0.2: {}
|
||||||
|
|
||||||
through@2.3.8: {}
|
through@2.3.8: {}
|
||||||
@@ -19609,6 +19733,13 @@ snapshots:
|
|||||||
|
|
||||||
tslib@2.8.1: {}
|
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:
|
tunnel-agent@0.6.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
safe-buffer: 5.2.1
|
safe-buffer: 5.2.1
|
||||||
@@ -19965,17 +20096,17 @@ snapshots:
|
|||||||
'@types/unist': 3.0.3
|
'@types/unist': 3.0.3
|
||||||
vfile-message: 4.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:
|
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:
|
dependencies:
|
||||||
cac: 6.7.14
|
cac: 6.7.14
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
es-module-lexer: 1.7.0
|
es-module-lexer: 1.7.0
|
||||||
pathe: 2.0.3
|
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:
|
transitivePeerDependencies:
|
||||||
- '@types/node'
|
- '@types/node'
|
||||||
- jiti
|
- jiti
|
||||||
@@ -19990,13 +20121,13 @@ snapshots:
|
|||||||
- tsx
|
- tsx
|
||||||
- yaml
|
- 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:
|
dependencies:
|
||||||
cac: 6.7.14
|
cac: 6.7.14
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
es-module-lexer: 1.7.0
|
es-module-lexer: 1.7.0
|
||||||
pathe: 2.0.3
|
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:
|
transitivePeerDependencies:
|
||||||
- '@types/node'
|
- '@types/node'
|
||||||
- jiti
|
- jiti
|
||||||
@@ -20012,16 +20143,16 @@ snapshots:
|
|||||||
- yaml
|
- yaml
|
||||||
optional: true
|
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:
|
dependencies:
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
fs-extra: 10.1.0
|
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:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- 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:
|
dependencies:
|
||||||
'@microsoft/api-extractor': 7.54.0(@types/node@24.10.0)
|
'@microsoft/api-extractor': 7.54.0(@types/node@24.10.0)
|
||||||
'@rollup/pluginutils': 5.3.0(rollup@4.52.5)
|
'@rollup/pluginutils': 5.3.0(rollup@4.52.5)
|
||||||
@@ -20034,13 +20165,13 @@ snapshots:
|
|||||||
magic-string: 0.30.21
|
magic-string: 0.30.21
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
optionalDependencies:
|
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:
|
transitivePeerDependencies:
|
||||||
- '@types/node'
|
- '@types/node'
|
||||||
- rollup
|
- rollup
|
||||||
- supports-color
|
- 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:
|
dependencies:
|
||||||
'@rollup/pluginutils': 4.2.1
|
'@rollup/pluginutils': 4.2.1
|
||||||
colorette: 2.0.20
|
colorette: 2.0.20
|
||||||
@@ -20054,9 +20185,9 @@ snapshots:
|
|||||||
html-minifier-terser: 6.1.0
|
html-minifier-terser: 6.1.0
|
||||||
node-html-parser: 5.4.2
|
node-html-parser: 5.4.2
|
||||||
pathe: 0.2.0
|
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:
|
dependencies:
|
||||||
'@antfu/utils': 0.7.10
|
'@antfu/utils': 0.7.10
|
||||||
'@rollup/pluginutils': 5.3.0(rollup@4.52.5)
|
'@rollup/pluginutils': 5.3.0(rollup@4.52.5)
|
||||||
@@ -20067,7 +20198,7 @@ snapshots:
|
|||||||
perfect-debounce: 1.0.0
|
perfect-debounce: 1.0.0
|
||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
sirv: 3.0.2
|
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:
|
transitivePeerDependencies:
|
||||||
- rollup
|
- rollup
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -20090,34 +20221,34 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- 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:
|
dependencies:
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
pretty-bytes: 6.1.1
|
pretty-bytes: 6.1.1
|
||||||
tinyglobby: 0.2.15
|
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-build: 7.3.0
|
||||||
workbox-window: 7.3.0
|
workbox-window: 7.3.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- 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:
|
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-kit': 7.7.7
|
||||||
'@vue/devtools-shared': 7.7.7
|
'@vue/devtools-shared': 7.7.7
|
||||||
execa: 9.6.0
|
execa: 9.6.0
|
||||||
sirv: 3.0.2
|
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)
|
||||||
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))
|
||||||
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))
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@nuxt/kit'
|
- '@nuxt/kit'
|
||||||
- rollup
|
- rollup
|
||||||
- supports-color
|
- supports-color
|
||||||
- vue
|
- 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:
|
dependencies:
|
||||||
'@babel/core': 7.28.5
|
'@babel/core': 7.28.5
|
||||||
'@babel/plugin-proposal-decorators': 7.28.0(@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
|
'@vue/compiler-dom': 3.5.22
|
||||||
kolorist: 1.8.0
|
kolorist: 1.8.0
|
||||||
magic-string: 0.30.21
|
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:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -20144,7 +20275,7 @@ snapshots:
|
|||||||
sass: 1.93.3
|
sass: 1.93.3
|
||||||
terser: 5.44.0
|
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:
|
dependencies:
|
||||||
esbuild: 0.25.3
|
esbuild: 0.25.3
|
||||||
fdir: 6.5.0(picomatch@4.0.3)
|
fdir: 6.5.0(picomatch@4.0.3)
|
||||||
@@ -20155,13 +20286,14 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 22.19.0
|
'@types/node': 22.19.0
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
jiti: 2.6.1
|
jiti: 1.21.7
|
||||||
less: 4.4.2
|
less: 4.4.2
|
||||||
sass: 1.93.3
|
sass: 1.93.3
|
||||||
terser: 5.44.0
|
terser: 5.44.0
|
||||||
|
tsx: 4.20.6
|
||||||
yaml: 2.8.1
|
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:
|
dependencies:
|
||||||
esbuild: 0.25.3
|
esbuild: 0.25.3
|
||||||
fdir: 6.5.0(picomatch@4.0.3)
|
fdir: 6.5.0(picomatch@4.0.3)
|
||||||
@@ -20176,6 +20308,7 @@ snapshots:
|
|||||||
less: 4.4.2
|
less: 4.4.2
|
||||||
sass: 1.93.3
|
sass: 1.93.3
|
||||||
terser: 5.44.0
|
terser: 5.44.0
|
||||||
|
tsx: 4.20.6
|
||||||
yaml: 2.8.1
|
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)):
|
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
|
- typescript
|
||||||
- universal-cookie
|
- 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:
|
dependencies:
|
||||||
'@types/chai': 5.2.3
|
'@types/chai': 5.2.3
|
||||||
'@vitest/expect': 3.2.4
|
'@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/pretty-format': 3.2.4
|
||||||
'@vitest/runner': 3.2.4
|
'@vitest/runner': 3.2.4
|
||||||
'@vitest/snapshot': 3.2.4
|
'@vitest/snapshot': 3.2.4
|
||||||
@@ -20259,8 +20392,8 @@ snapshots:
|
|||||||
tinyglobby: 0.2.15
|
tinyglobby: 0.2.15
|
||||||
tinypool: 1.1.1
|
tinypool: 1.1.1
|
||||||
tinyrainbow: 2.0.0
|
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: 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@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)
|
||||||
why-is-node-running: 2.3.0
|
why-is-node-running: 2.3.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 22.19.0
|
'@types/node': 22.19.0
|
||||||
@@ -20279,11 +20412,11 @@ snapshots:
|
|||||||
- tsx
|
- tsx
|
||||||
- yaml
|
- 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:
|
dependencies:
|
||||||
'@types/chai': 5.2.3
|
'@types/chai': 5.2.3
|
||||||
'@vitest/expect': 3.2.4
|
'@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/pretty-format': 3.2.4
|
||||||
'@vitest/runner': 3.2.4
|
'@vitest/runner': 3.2.4
|
||||||
'@vitest/snapshot': 3.2.4
|
'@vitest/snapshot': 3.2.4
|
||||||
@@ -20301,8 +20434,8 @@ snapshots:
|
|||||||
tinyglobby: 0.2.15
|
tinyglobby: 0.2.15
|
||||||
tinypool: 1.1.1
|
tinypool: 1.1.1
|
||||||
tinyrainbow: 2.0.0
|
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: 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)(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
|
why-is-node-running: 2.3.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 24.10.0
|
'@types/node': 24.10.0
|
||||||
|
|||||||
Reference in New Issue
Block a user