feat:添加AI功能
This commit is contained in:
@@ -11,3 +11,12 @@ NOTIFICATION_ALLOWED_PACKAGES=com.eg.android.AlipayGphone,com.tencent.mm
|
||||
GEMINI_API_KEY=
|
||||
OCR_PROJECT_ID=
|
||||
FILE_STORAGE_BUCKET=
|
||||
|
||||
# DeepSeek AI (OpenAI compatible)
|
||||
DEEPSEEK_API_KEY=
|
||||
DEEPSEEK_BASE_URL=https://api.deepseek.com
|
||||
DEEPSEEK_MODEL=deepseek-chat
|
||||
|
||||
# Feature flags
|
||||
AI_ENABLE_CLASSIFICATION=false
|
||||
AI_AUTO_LEARN_RULES=false
|
||||
|
||||
@@ -15,6 +15,12 @@ NOTIFICATION_WEBHOOK_SECRET=bf921ce9ac433ba2fdfccbccb50e7d805ac89f166962e4b8437a
|
||||
NOTIFICATION_ALLOWED_PACKAGES=com.eg.android.AlipayGphone,com.tencent.mm
|
||||
|
||||
# Optional integrations (leave blank if unused)
|
||||
GEMINI_API_KEY=
|
||||
OCR_PROJECT_ID=
|
||||
FILE_STORAGE_BUCKET=
|
||||
|
||||
# DeepSeek AI (OpenAI compatible)
|
||||
DEEPSEEK_API_KEY=sk-aa152188952b4708a087693af9082721
|
||||
DEEPSEEK_BASE_URL=https://api.deepseek.com
|
||||
DEEPSEEK_MODEL=deepseek-chat
|
||||
|
||||
# Feature flags
|
||||
AI_ENABLE_CLASSIFICATION=false
|
||||
AI_AUTO_LEARN_RULES=false
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"express-rate-limit": "^7.1.5",
|
||||
"helmet": "^7.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"openai": "^4.58.1",
|
||||
"mongoose": "^7.6.0",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"pino": "^9.5.0",
|
||||
|
||||
@@ -28,6 +28,11 @@ const envSchema = z
|
||||
})
|
||||
.transform((values) => ({
|
||||
...values,
|
||||
DEEPSEEK_API_KEY: (values as any).DEEPSEEK_API_KEY as string | undefined,
|
||||
DEEPSEEK_BASE_URL: ((values as any).DEEPSEEK_BASE_URL as string | undefined) ?? 'https://api.deepseek.com',
|
||||
DEEPSEEK_MODEL: ((values as any).DEEPSEEK_MODEL as string | undefined) ?? 'deepseek-chat',
|
||||
AI_ENABLE_CLASSIFICATION: ((values as any).AI_ENABLE_CLASSIFICATION as string | undefined) === 'true',
|
||||
AI_AUTO_LEARN_RULES: ((values as any).AI_AUTO_LEARN_RULES as string | undefined) === 'true',
|
||||
isProduction: values.NODE_ENV === 'production',
|
||||
notificationPackageWhitelist: values.NOTIFICATION_ALLOWED_PACKAGES
|
||||
? values.NOTIFICATION_ALLOWED_PACKAGES.split(',').map((pkg) => pkg.trim()).filter(Boolean)
|
||||
|
||||
@@ -145,4 +145,3 @@ export async function applyNotificationRules(input: {
|
||||
|
||||
return { ignore: false, overrides: {} };
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,9 @@ import {
|
||||
ingestNotification
|
||||
} from './transactions.service.js';
|
||||
import type { CreateTransactionInput, UpdateTransactionInput } from './transactions.schema.js';
|
||||
import { aiClassifyByText, aiSuggestRuleForNotification } from '../../services/ai.service.js';
|
||||
import { createRule } from '../notifications/notification-rules.service.js';
|
||||
import { TransactionModel } from '../../models/transaction.model.js';
|
||||
|
||||
export const listTransactionsHandler = async (req: Request, res: Response) => {
|
||||
if (!req.user) {
|
||||
@@ -104,3 +107,91 @@ export const createTransactionFromNotificationHandler = async (req: Request, res
|
||||
data: transaction
|
||||
});
|
||||
};
|
||||
|
||||
export const aiClassifyTransactionHandler = async (req: Request, res: Response) => {
|
||||
if (!req.user) return res.status(401).json({ message: 'Authentication required' });
|
||||
const id = req.params.id;
|
||||
const txn = await getTransactionById(id, req.user.id);
|
||||
if (!txn) return res.status(404).json({ message: 'Transaction not found' });
|
||||
if (txn.source !== 'notification') return res.status(400).json({ message: 'Only notification transactions supported' });
|
||||
const bodyText = (txn.notes as string | undefined) ?? (txn.metadata as any)?.rawBody ?? '';
|
||||
const ai = await aiClassifyByText(txn.title, bodyText);
|
||||
if (!ai || (!ai.category && !ai.type && !ai.amount)) {
|
||||
return res.status(200).json({ data: txn, message: 'No AI classification available' });
|
||||
}
|
||||
const updated = await updateTransaction(id, {
|
||||
category: ai.category ?? txn.category,
|
||||
type: (ai.type as any) ?? txn.type,
|
||||
amount: ai.amount ?? txn.amount
|
||||
} as UpdateTransactionInput, req.user.id);
|
||||
return res.json({ data: updated });
|
||||
};
|
||||
|
||||
export const createBlacklistRuleFromTransactionHandler = async (req: Request, res: Response) => {
|
||||
if (!req.user) return res.status(401).json({ message: 'Authentication required' });
|
||||
const id = req.params.id;
|
||||
const txn = await getTransactionById(id, req.user.id);
|
||||
if (!txn) return res.status(404).json({ message: 'Transaction not found' });
|
||||
if (txn.source !== 'notification') return res.status(400).json({ message: 'Only notification transactions supported' });
|
||||
const pkg = (txn.metadata as any)?.packageName as string | undefined;
|
||||
const title = txn.title;
|
||||
if (!pkg || !title) return res.status(400).json({ message: 'Missing notification metadata' });
|
||||
const rule = await createRule({
|
||||
name: `黑名单-${pkg}-${title.substring(0, 10)}`,
|
||||
enabled: true,
|
||||
priority: 95,
|
||||
packageName: pkg,
|
||||
keywords: [title],
|
||||
action: 'ignore'
|
||||
} as any);
|
||||
return res.status(201).json({ data: rule });
|
||||
};
|
||||
|
||||
export const createRuleFromTransactionHandler = async (req: Request, res: Response) => {
|
||||
if (!req.user) return res.status(401).json({ message: 'Authentication required' });
|
||||
const id = req.params.id;
|
||||
const txn = await getTransactionById(id, req.user.id);
|
||||
if (!txn) return res.status(404).json({ message: 'Transaction not found' });
|
||||
if (txn.source !== 'notification') return res.status(400).json({ message: 'Only notification transactions supported' });
|
||||
const pkg = (txn.metadata as any)?.packageName as string | undefined;
|
||||
const title = txn.title;
|
||||
if (!pkg || !title) return res.status(400).json({ message: 'Missing notification metadata' });
|
||||
const rule = await createRule({
|
||||
name: `规则-${pkg}-${title.substring(0, 10)}`,
|
||||
enabled: true,
|
||||
priority: 80,
|
||||
packageName: pkg,
|
||||
keywords: [title],
|
||||
action: 'record',
|
||||
category: (txn as any).category,
|
||||
type: (txn as any).type
|
||||
} as any);
|
||||
return res.status(201).json({ data: rule });
|
||||
};
|
||||
|
||||
export const aiSuggestRuleFromTransactionHandler = async (req: Request, res: Response) => {
|
||||
if (!req.user) return res.status(401).json({ message: 'Authentication required' });
|
||||
const id = req.params.id;
|
||||
const txn = await getTransactionById(id, req.user.id);
|
||||
if (!txn) return res.status(404).json({ message: 'Transaction not found' });
|
||||
if (txn.source !== 'notification') return res.status(400).json({ message: 'Only notification transactions supported' });
|
||||
const pkg = (txn.metadata as any)?.packageName as string | undefined;
|
||||
const body = (txn.metadata as any)?.rawBody as string | undefined;
|
||||
const title = txn.title;
|
||||
if (!pkg) return res.status(400).json({ message: 'Missing notification metadata' });
|
||||
const suggestion = await aiSuggestRuleForNotification({ packageName: pkg, title: title ?? '', body: body ?? '' });
|
||||
if (!suggestion) return res.status(200).json({ message: 'No AI suggestion' });
|
||||
const rule = await createRule({
|
||||
name: suggestion.name ?? `AI规则-${pkg}-${Date.now()}`,
|
||||
enabled: suggestion.enabled ?? false,
|
||||
priority: suggestion.priority ?? 60,
|
||||
packageName: pkg,
|
||||
keywords: suggestion.keywords ?? [],
|
||||
pattern: suggestion.pattern ?? undefined,
|
||||
action: suggestion.action ?? 'record',
|
||||
category: suggestion.category ?? undefined,
|
||||
type: suggestion.type ?? undefined,
|
||||
amountPattern: suggestion.amountPattern ?? undefined
|
||||
} as any);
|
||||
return res.status(201).json({ data: rule });
|
||||
};
|
||||
|
||||
@@ -13,7 +13,11 @@ import {
|
||||
getTransactionHandler,
|
||||
updateTransactionHandler,
|
||||
deleteTransactionHandler,
|
||||
createTransactionFromNotificationHandler
|
||||
createTransactionFromNotificationHandler,
|
||||
aiClassifyTransactionHandler,
|
||||
createBlacklistRuleFromTransactionHandler,
|
||||
createRuleFromTransactionHandler,
|
||||
aiSuggestRuleFromTransactionHandler
|
||||
} from './transactions.controller.js';
|
||||
|
||||
const router = Router();
|
||||
@@ -27,5 +31,9 @@ router.post('/', validateRequest({ body: createTransactionSchema }), createTrans
|
||||
router.get('/:id', validateRequest({ params: transactionIdSchema }), getTransactionHandler);
|
||||
router.patch('/:id', validateRequest({ params: transactionIdSchema, body: updateTransactionSchema }), updateTransactionHandler);
|
||||
router.delete('/:id', validateRequest({ params: transactionIdSchema }), deleteTransactionHandler);
|
||||
router.post('/:id/ai-classify', validateRequest({ params: transactionIdSchema }), aiClassifyTransactionHandler);
|
||||
router.post('/:id/rules/blacklist', validateRequest({ params: transactionIdSchema }), createBlacklistRuleFromTransactionHandler);
|
||||
router.post('/:id/rules/from-notification', validateRequest({ params: transactionIdSchema }), createRuleFromTransactionHandler);
|
||||
router.post('/:id/rules/ai-suggest', validateRequest({ params: transactionIdSchema }), aiSuggestRuleFromTransactionHandler);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -7,7 +7,9 @@ import { getChannelByPackage } from '../notifications/notification-channels.serv
|
||||
import { getDefaultUserId } from '../../services/user-context.js';
|
||||
import type { CreateTransactionInput, UpdateTransactionInput } from './transactions.schema.js';
|
||||
import { parseNotificationPayload } from './notification.parser.js';
|
||||
import { applyNotificationRules } from '../notifications/notification-rules.service.js';
|
||||
import { applyNotificationRules, createRule } from '../notifications/notification-rules.service.js';
|
||||
import { aiClassifyByText, aiSuggestRuleForNotification } from '../../services/ai.service.js';
|
||||
import { env } from '../../config/env.js';
|
||||
|
||||
export interface TransactionDto {
|
||||
id: string;
|
||||
@@ -187,6 +189,14 @@ export async function ingestNotification(payload: NotificationPayload): Promise<
|
||||
|
||||
const parsed = parseNotificationPayload(payload.title ?? '', payload.body ?? '', mergedTemplate);
|
||||
|
||||
// Optionally enhance with AI classification
|
||||
if (env.AI_ENABLE_CLASSIFICATION && !parsed.category) {
|
||||
const ai = await aiClassifyByText(payload.title ?? '', payload.body ?? '');
|
||||
if (ai?.category) parsed.category = ai.category;
|
||||
if (ai?.type) parsed.type = ai.type;
|
||||
if (ai?.amount && (!parsed.amount || parsed.amount === 0)) parsed.amount = ai.amount;
|
||||
}
|
||||
|
||||
const hashSource = `${payload.packageName}|${payload.title}|${payload.body}|${now.getTime()}`;
|
||||
const notificationHash = createHash('sha256').update(hashSource).digest('hex');
|
||||
|
||||
@@ -219,5 +229,36 @@ export async function ingestNotification(payload: NotificationPayload): Promise<
|
||||
};
|
||||
|
||||
const userId = await getDefaultUserId();
|
||||
return createTransaction(transactionPayload, userId);
|
||||
const created = await createTransaction(transactionPayload, userId);
|
||||
|
||||
// Auto-learn rule (as draft) in background
|
||||
if (env.AI_AUTO_LEARN_RULES) {
|
||||
void (async () => {
|
||||
try {
|
||||
const suggestion = await aiSuggestRuleForNotification({
|
||||
packageName: payload.packageName,
|
||||
title: payload.title ?? '',
|
||||
body: payload.body ?? ''
|
||||
});
|
||||
if (suggestion && (suggestion.keywords?.length || suggestion.pattern)) {
|
||||
await createRule({
|
||||
name: suggestion.name ?? `AI规则-${payload.packageName}-${Date.now()}`,
|
||||
enabled: suggestion.enabled ?? false,
|
||||
priority: suggestion.priority ?? 60,
|
||||
packageName: payload.packageName,
|
||||
keywords: suggestion.keywords ?? [],
|
||||
pattern: suggestion.pattern ?? undefined,
|
||||
action: suggestion.action ?? 'record',
|
||||
category: suggestion.category ?? undefined,
|
||||
type: suggestion.type ?? undefined,
|
||||
amountPattern: suggestion.amountPattern ?? undefined
|
||||
} as any);
|
||||
}
|
||||
} catch (e) {
|
||||
// swallow errors
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
return created;
|
||||
}
|
||||
|
||||
102
apps/backend/src/services/ai.service.ts
Normal file
102
apps/backend/src/services/ai.service.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import OpenAI from 'openai';
|
||||
import { env } from '../config/env.js';
|
||||
import { logger } from '../config/logger.js';
|
||||
|
||||
const ai = env.DEEPSEEK_API_KEY
|
||||
? new OpenAI({ apiKey: env.DEEPSEEK_API_KEY, baseURL: env.DEEPSEEK_BASE_URL })
|
||||
: null;
|
||||
|
||||
export interface AiClassification {
|
||||
category?: string;
|
||||
type?: 'income' | 'expense';
|
||||
amount?: number;
|
||||
confidence?: number; // 0-1
|
||||
}
|
||||
|
||||
export interface AiRuleSuggestion {
|
||||
enabled?: boolean;
|
||||
action?: 'record' | 'ignore';
|
||||
name?: string;
|
||||
priority?: number;
|
||||
packageName?: string;
|
||||
keywords?: string[];
|
||||
pattern?: string;
|
||||
category?: string;
|
||||
type?: 'income' | 'expense';
|
||||
amountPattern?: string;
|
||||
confidence?: number; // 0-1
|
||||
}
|
||||
|
||||
function safeJson<T = unknown>(text: string): T | null {
|
||||
try {
|
||||
return JSON.parse(text) as T;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function aiClassifyByText(title: string, body?: string): Promise<AiClassification | null> {
|
||||
if (!ai) return null;
|
||||
const prompt = `你是一个财务助手。请从通知文本中判断账单类型与分类,并尽量提取金额。
|
||||
只返回JSON,不要任何多余解释。例如:{"category":"餐饮","type":"expense","amount":25.5,"confidence":0.8}
|
||||
常见分类:餐饮、交通、购物、生活缴费、医疗、教育、娱乐、转账、退款、理财、公益、收入、其他支出。
|
||||
如果无法判断,保持相应字段为空或省略,confidence 介于 0 到 1。
|
||||
|
||||
标题: ${title}\n正文: ${body ?? ''}`;
|
||||
|
||||
try {
|
||||
const res = await ai.chat.completions.create({
|
||||
model: env.DEEPSEEK_MODEL,
|
||||
messages: [
|
||||
{ role: 'system', content: 'You are a helpful assistant that outputs only JSON.' },
|
||||
{ role: 'user', content: prompt }
|
||||
]
|
||||
});
|
||||
const content = res.choices?.[0]?.message?.content ?? '';
|
||||
const parsed = safeJson<AiClassification>(content);
|
||||
return parsed ?? null;
|
||||
} catch (error) {
|
||||
logger.warn({ error }, 'AI classify failed');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function aiSuggestRuleForNotification(input: {
|
||||
packageName: string;
|
||||
title: string;
|
||||
body: string;
|
||||
}): Promise<AiRuleSuggestion | null> {
|
||||
if (!ai) return null;
|
||||
const prompt = `你是一个通知解析助手。根据通知标题与正文,生成一个用于匹配的规则建议,返回纯JSON:
|
||||
{
|
||||
"enabled": false,
|
||||
"action": "record" | "ignore",
|
||||
"name": "简短规则名",
|
||||
"priority": 60,
|
||||
"keywords": ["多个关键词可选"],
|
||||
"pattern": "可选的正则表达式(不要加分隔符)",
|
||||
"category": "可选 分类",
|
||||
"type": "income" | "expense",
|
||||
"amountPattern": "可选,金额捕获组",
|
||||
"confidence": 0.0-1.0
|
||||
}
|
||||
说明:keywords 与 pattern 二选一或都给;pattern 不要包含 /;若该通知应忽略,请 action 设为 "ignore"。
|
||||
标题: ${input.title}\n正文: ${input.body}\n包名: ${input.packageName}`;
|
||||
|
||||
try {
|
||||
const res = await ai.chat.completions.create({
|
||||
model: env.DEEPSEEK_MODEL,
|
||||
messages: [
|
||||
{ role: 'system', content: 'You are a helpful assistant that outputs only JSON.' },
|
||||
{ role: 'user', content: prompt }
|
||||
]
|
||||
});
|
||||
const content = res.choices?.[0]?.message?.content ?? '';
|
||||
const parsed = safeJson<AiRuleSuggestion>(content);
|
||||
return parsed ?? null;
|
||||
} catch (error) {
|
||||
logger.warn({ error }, 'AI rule suggestion failed');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import LucideIcon from '../../../components/common/LucideIcon.vue';
|
||||
import { useTransactionQuery } from '../../../composables/useTransaction';
|
||||
import { useUpdateTransactionMutation } from '../../../composables/useTransactions';
|
||||
import { PRESET_CATEGORIES } from '../../../lib/categories';
|
||||
import { apiClient } from '../../../lib/api/client';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -27,6 +28,53 @@ const saveCategory = async () => {
|
||||
await updateMutation.mutateAsync({ id: txn.value.id, payload: { category: editing.category || '未分类' } });
|
||||
await query.refetch();
|
||||
};
|
||||
|
||||
const feedback = ref<string | null>(null);
|
||||
const show = (msg: string) => {
|
||||
feedback.value = msg;
|
||||
setTimeout(() => (feedback.value = null), 2000);
|
||||
};
|
||||
|
||||
const useAiClassify = async () => {
|
||||
if (!txn.value?.id) return;
|
||||
try {
|
||||
await apiClient.post(`/transactions/${txn.value.id}/ai-classify`);
|
||||
await query.refetch();
|
||||
show('已使用 AI 分类');
|
||||
} catch (e) {
|
||||
show('AI 分类失败');
|
||||
}
|
||||
};
|
||||
|
||||
const createBlacklistRule = async () => {
|
||||
if (!txn.value?.id) return;
|
||||
try {
|
||||
await apiClient.post(`/transactions/${txn.value.id}/rules/blacklist`);
|
||||
show('已加入黑名单规则');
|
||||
} catch (e) {
|
||||
show('创建黑名单失败');
|
||||
}
|
||||
};
|
||||
|
||||
const createRuleFromNotification = async () => {
|
||||
if (!txn.value?.id) return;
|
||||
try {
|
||||
await apiClient.post(`/transactions/${txn.value.id}/rules/from-notification`);
|
||||
show('已根据该通知创建规则');
|
||||
} catch (e) {
|
||||
show('创建规则失败');
|
||||
}
|
||||
};
|
||||
|
||||
const aiSuggestRule = async () => {
|
||||
if (!txn.value?.id) return;
|
||||
try {
|
||||
await apiClient.post(`/transactions/${txn.value.id}/rules/ai-suggest`);
|
||||
show('已创建 AI 规则草案');
|
||||
} catch (e) {
|
||||
show('AI 规则分析失败');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -78,7 +126,7 @@ const saveCategory = async () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isNotification" class="bg-white rounded-2xl p-6 border border-gray-100">
|
||||
<div v-if="isNotification" class="bg-white rounded-2xl p-6 border border-gray-100 space-y-4">
|
||||
<h3 class="text-base font-semibold text-gray-900 mb-4">手工分类</h3>
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center space-x-3">
|
||||
@@ -97,6 +145,16 @@ const saveCategory = async () => {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="h-px bg-gray-100"></div>
|
||||
<h3 class="text-base font-semibold text-gray-900">快捷操作</h3>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<button class="px-3 py-2 border border-gray-200 rounded-xl text-sm" @click="useAiClassify">使用 AI 分类</button>
|
||||
<button class="px-3 py-2 border border-gray-200 rounded-xl text-sm" @click="createBlacklistRule">避免此类通知</button>
|
||||
<button class="px-3 py-2 border border-gray-200 rounded-xl text-sm" @click="createRuleFromNotification">使用该通知生成规则</button>
|
||||
<button class="px-3 py-2 border border-gray-200 rounded-xl text-sm" @click="aiSuggestRule">AI 分析规则</button>
|
||||
</div>
|
||||
<p v-if="feedback" class="text-xs text-emerald-600 text-right">{{ feedback }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="txn.metadata && Object.keys(txn.metadata).length" class="bg-white rounded-2xl p-6 border border-gray-100">
|
||||
|
||||
86
pnpm-lock.yaml
generated
86
pnpm-lock.yaml
generated
@@ -40,6 +40,9 @@ importers:
|
||||
multer:
|
||||
specifier: ^1.4.5-lts.1
|
||||
version: 1.4.5-lts.2
|
||||
openai:
|
||||
specifier: ^4.58.1
|
||||
version: 4.104.0(zod@3.25.76)
|
||||
pino:
|
||||
specifier: ^9.5.0
|
||||
version: 9.14.0
|
||||
@@ -1308,6 +1311,19 @@ packages:
|
||||
'@types/express': 4.17.25
|
||||
dev: true
|
||||
|
||||
/@types/node-fetch@2.6.13:
|
||||
resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==}
|
||||
dependencies:
|
||||
'@types/node': 24.9.2
|
||||
form-data: 4.0.4
|
||||
dev: false
|
||||
|
||||
/@types/node@18.19.130:
|
||||
resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==}
|
||||
dependencies:
|
||||
undici-types: 5.26.5
|
||||
dev: false
|
||||
|
||||
/@types/node@20.19.24:
|
||||
resolution: {integrity: sha512-FE5u0ezmi6y9OZEzlJfg37mqqf6ZDSF2V/NLjUyGrR9uTZ7Sb9F7bLNZ03S4XVUNRWGA7Ck4c1kK+YnuWjl+DA==}
|
||||
dependencies:
|
||||
@@ -1657,7 +1673,6 @@ packages:
|
||||
engines: {node: '>=6.5'}
|
||||
dependencies:
|
||||
event-target-shim: 5.0.1
|
||||
dev: true
|
||||
|
||||
/accepts@1.3.8:
|
||||
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
|
||||
@@ -1691,6 +1706,13 @@ packages:
|
||||
resolution: {integrity: sha512-qQLMr+8o0WC4FZGQTcJiKBVC59JylcPSrTtk6usvmIDFUOCKegapy1VHQwRbFMOFyb/inzUVqHs+eMYKDM1YeQ==}
|
||||
dev: true
|
||||
|
||||
/agentkeepalive@4.6.0:
|
||||
resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==}
|
||||
engines: {node: '>= 8.0.0'}
|
||||
dependencies:
|
||||
humanize-ms: 1.2.1
|
||||
dev: false
|
||||
|
||||
/aggregate-error@3.1.0:
|
||||
resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -2932,7 +2954,6 @@ packages:
|
||||
/event-target-shim@5.0.1:
|
||||
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
|
||||
engines: {node: '>=6'}
|
||||
dev: true
|
||||
|
||||
/events-universal@1.0.1:
|
||||
resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==}
|
||||
@@ -3145,6 +3166,10 @@ packages:
|
||||
cross-spawn: 7.0.6
|
||||
signal-exit: 4.1.0
|
||||
|
||||
/form-data-encoder@1.7.2:
|
||||
resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==}
|
||||
dev: false
|
||||
|
||||
/form-data@4.0.4:
|
||||
resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==}
|
||||
engines: {node: '>= 6'}
|
||||
@@ -3156,6 +3181,14 @@ packages:
|
||||
mime-types: 2.1.35
|
||||
dev: false
|
||||
|
||||
/formdata-node@4.4.1:
|
||||
resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==}
|
||||
engines: {node: '>= 12.20'}
|
||||
dependencies:
|
||||
node-domexception: 1.0.0
|
||||
web-streams-polyfill: 4.0.0-beta.3
|
||||
dev: false
|
||||
|
||||
/formidable@3.5.4:
|
||||
resolution: {integrity: sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
@@ -3489,6 +3522,12 @@ packages:
|
||||
statuses: 2.0.1
|
||||
toidentifier: 1.0.1
|
||||
|
||||
/humanize-ms@1.2.1:
|
||||
resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==}
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
dev: false
|
||||
|
||||
/iconv-lite@0.4.24:
|
||||
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -4279,6 +4318,12 @@ packages:
|
||||
resolution: {integrity: sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==}
|
||||
dev: true
|
||||
|
||||
/node-domexception@1.0.0:
|
||||
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
||||
engines: {node: '>=10.5.0'}
|
||||
deprecated: Use your platform's native DOMException instead
|
||||
dev: false
|
||||
|
||||
/node-fetch@2.7.0:
|
||||
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
|
||||
engines: {node: 4.x || >=6.0.0}
|
||||
@@ -4289,7 +4334,6 @@ packages:
|
||||
optional: true
|
||||
dependencies:
|
||||
whatwg-url: 5.0.0
|
||||
dev: true
|
||||
|
||||
/node-html-parser@5.4.2:
|
||||
resolution: {integrity: sha512-RaBPP3+51hPne/OolXxcz89iYvQvKOydaqoePpOgXcrOKZhjVIzmpKZz+Hd/RBO2/zN2q6CNJhQzucVz+u3Jyw==}
|
||||
@@ -4374,6 +4418,30 @@ packages:
|
||||
is-docker: 2.2.1
|
||||
is-wsl: 2.2.0
|
||||
|
||||
/openai@4.104.0(zod@3.25.76):
|
||||
resolution: {integrity: sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
ws: ^8.18.0
|
||||
zod: ^3.23.8
|
||||
peerDependenciesMeta:
|
||||
ws:
|
||||
optional: true
|
||||
zod:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@types/node': 18.19.130
|
||||
'@types/node-fetch': 2.6.13
|
||||
abort-controller: 3.0.0
|
||||
agentkeepalive: 4.6.0
|
||||
form-data-encoder: 1.7.2
|
||||
formdata-node: 4.4.1
|
||||
node-fetch: 2.7.0
|
||||
zod: 3.25.76
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
dev: false
|
||||
|
||||
/optionator@0.9.4:
|
||||
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -5646,7 +5714,6 @@ packages:
|
||||
|
||||
/tr46@0.0.3:
|
||||
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
||||
dev: true
|
||||
|
||||
/tr46@3.0.0:
|
||||
resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==}
|
||||
@@ -5788,6 +5855,10 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/undici-types@5.26.5:
|
||||
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
|
||||
dev: false
|
||||
|
||||
/undici-types@6.21.0:
|
||||
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
||||
|
||||
@@ -5962,9 +6033,13 @@ packages:
|
||||
'@vue/shared': 3.5.22
|
||||
typescript: 5.9.3
|
||||
|
||||
/web-streams-polyfill@4.0.0-beta.3:
|
||||
resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==}
|
||||
engines: {node: '>= 14'}
|
||||
dev: false
|
||||
|
||||
/webidl-conversions@3.0.1:
|
||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||
dev: true
|
||||
|
||||
/webidl-conversions@7.0.0:
|
||||
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
|
||||
@@ -5984,7 +6059,6 @@ packages:
|
||||
dependencies:
|
||||
tr46: 0.0.3
|
||||
webidl-conversions: 3.0.1
|
||||
dev: true
|
||||
|
||||
/which-module@2.0.1:
|
||||
resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==}
|
||||
|
||||
Reference in New Issue
Block a user