Merge pull request #20 from d0zingcat/feature/support_attachment

feat: Enable Feishu file and image uploads via webhook, supporting mu…
This commit is contained in:
2026-02-09 15:25:36 +08:00
committed by GitHub
14 changed files with 879 additions and 214 deletions

1
.husky/pre-commit Normal file
View File

@@ -0,0 +1 @@
bunx lint-staged

View File

@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
**CHANGELOG** | [简体中文](./CHANGELOG.zh-CN.md)
## [1.5.0] - 2026-02-09
### Added
- **File Attachment Support**: Users can now send files and images via the dashboard and API.
- **Multipart Form-Data Support**: Webhook endpoint now handles `multipart/form-data` for file uploads.
- **Git Pre-commit Hooks**: Automated linting and formatting on staged files using Husky and Biome.
- **Sequential Message Dispatch**: Support for sending text and attachments as multiple sequential messages in a single request.
- **SendAlertForm Component**: New UI component for sending alerts with attachments directly from the topic view.
### Fixed
- **Feishu SDK Bun Compatibility**: Resolved a crash when uploading files in the Bun environment by using temporary files and `fs.ReadStream`.
- **Drizzle ORM Prototype Error**: Fixed a `null is not an object` crash during database insertion by normalizing request bodies.
- **Attachment Precedence**: Fixed an issue where attachments were ignored if text content was also present.
## [1.4.0] - 2026-01-23
### Added

View File

@@ -2,44 +2,57 @@
本项目的所有显著变更都将记录在此文件中。
文件的格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/)
并且本项目遵循 [语义化版本 (Semantic Versioning)](https://semver.org/lang/zh-CN/spec/v2.0.0.html)。
项目遵循 [Semantic Versioning](https://semver.org/spec/v2.0.0.html) 规范。
**更新日志** | [English](./CHANGELOG.md)
[English](./CHANGELOG.md) | **简体中文**
## [1.5.0] - 2026-02-09
### 新增
- **文件附件支持**:用户现在可以通过控制面板和 API 发送文件和图片。
- **Git Pre-commit 钩子**:使用 Husky 和 Biome 自动检查和格式化暂存代码。
- **Multipart Form-Data 支持**Webhook 端点现在支持处理用于文件上传的 `multipart/form-data`
- **顺序消息分发**:支持在单个请求中将文本和附件作为多个顺序消息发送。
- **SendAlertForm 组件**:新的 UI 组件,支持直接从话题视图发送带有附件的告警。
### 修复
- **飞书 SDK Bun 兼容性**:通过使用临时文件和 `fs.ReadStream`,解决了在 Bun 环境下上传文件时的崩溃问题。
- **Drizzle ORM 原型错误**:通过标准化请求体,修复了数据库插入过程中的 `null is not an object` 崩溃。
- **附件优先级**:修复了当同时存在文本内容时附件被忽略的问题。
## [1.4.0] - 2026-01-23
### 新增
- **全话题 (Global Topics)**: 引入了全新的话题类型,可自动向所有用户广播告警。
- **用户申请**: 现在所有用户在创建话题时都可以选择申请将其为“全局话题”。
- **管理员控制**: 管理员可以通过管理员后台将任何话题提升为“全局话题”,或直接创建新的全局话题
- **自动分发**: 发送到全局话题的告警将投递给每一位注册用户,无需用户手动订阅。
- **UI 标识**: 在话题列表和管理视图中增加了“全局”标识和专门的管理操作。
- **全话题 (Global Topics)**引入了一种自动向所有用户广播告警的新话题类型
- **用户申请**所有用户在创建话题时均可申请将其标记为“全”。
- **管理员控制**管理员可以通过后台将任何话题提升为“全量”模式
- **自动分发**:发送至全量话题的告警将自动推送给每一位注册用户,无需手动订阅。
- **UI 标识**在话题列表和管理视图中增加了“全量”徽章及专属管理操作。
## [1.3.3] - 2026-01-17
### 新增
- **多副本部署支持**: 增强了在负载均衡/多实例环境下的稳定性。
- **数据库锁机制**: 在 `db:migrate:deploy` 脚本中引入了 **Postgres Advisory Locks**,防止多个实例同时执行数据库迁移导致竞态问题
- **幂等性增强**: 验证并确保了飞书事件处理逻辑的幂等性,支持多副本安全接收重复事件。
- **多副本部署支持**增强了在负载均衡/多实例环境下的稳定性。
- **数据库锁**: 在 `db:migrate:deploy` 脚本中引入了 **Postgres Advisory Locks**,防止多个实例同时执行数据库迁移时产生竞态条件
- **幂等性增强**: 验证并确保了飞书事件处理逻辑的幂等性,支持多副本环境下安全地多次接收相同事件。
## [1.3.2] - 2026-01-17
### 新增
- **群聊搜索功能**: 在绑定群聊时增了实时搜索功能,解决了群过多时难以查找的问题。
- **后端支持**: `GET /groups` 接口现在支持 `q` 查询参数进行模糊搜索,并提了默认返回数量。
- **搜索前端**: 引入了带防抖逻辑的搜索输入框和自定义下拉列表,提升了用户体验。
- **群聊搜索**: 在绑定群聊时增了实时搜索功能,解决了群过多时难以定位的问题。
- **后端支持**: `GET /groups` 接口现在支持 `q` 查询参数,并提了默认返回数量。
- **搜索前端**: 引入了带防抖逻辑的搜索输入框和自定义下拉列表,提升了交互体验。
### 变更
- **UI 优化**: 改进了 `GroupBindingsModal` 的视觉设计,使用了更现代的列表样式、状态图标和加载动画。
- **文档优化**: 将 `README.md` 拆分为英文版 (`README.md`) 和中文版 (`README.zh-CN.md`),以更好地支持国际化。
- **UI 优化**: 改进了 `GroupBindingsModal` 的视觉设计,用了更现代的列表样式、状态图标和加载动画。
- **文档**: 将 `README.md` 拆分为英文版 (`README.md`) 和中文版 (`README.zh-CN.md`),以更好地支持国际化。
## [1.3.1] - 2026-01-16
### 新增
- **群聊绑定管理**: 增强了 Topic 飞书群聊绑定的安全性流程。
- **权限控制**: 仅 Topic 创建者或管理员允许执行群聊绑定/解绑操作。
- **审批流程**: 新增群聊绑定审批机制,非管理员/非信任用户的绑定请求需经过审批(`status` 追踪)。
- **群聊绑定管理**: 增强了 Topic 绑定到飞书群聊的安全性流程。
- **权限校验**: 仅 Topic 创建者或管理员执行绑定/解绑操作。
- **审批流程**: 非管理员/非信任用户的群聊绑定请求引入了审批机制(通过 `status` 字段追踪)。
- **管理员通知**: 引入 `admin-notifier.ts`,当有新的 Topic 或群聊绑定请求时,通过飞书卡片实时通知管理员。
- **信任用户系统**: 引入 `isTrusted` 标志。
- 信任用户创建 Topic 或绑定群聊时将自动通过审批。
@@ -48,6 +61,7 @@
### 变更
- **数据库架构**: `topic_group_chats` 表新增了 `status``created_by` 字段,以支持审批流和权限校验。
## [1.3.0] - 2026-01-16
### 新增
- **视觉品牌**: 引入了自定义图标和 Favicon。
@@ -143,6 +157,7 @@
### 变更
- **数据库**: 新增 `topic_group_chats``known_group_chats` 表。
- **底层架构**: 重构了飞书客户端 (`FeishuClient`) 和事件处理逻辑,统一了 Webhook 和 WebSocket 的事件分发。
## [1.1.1] - 2026-01-13
### 修复

View File

@@ -36,6 +36,8 @@ Real-time tracking of system alert load, dispatch success rates, and topic popul
- Full `Alert Task` logs for end-to-end traceability.
- **📊 Real-time Dashboard**: Grafana-style monitoring interface for system health visualization.
- **🔌 WebSocket Mode**: Supports Feishu Open Platform WebSocket for intranet deployments without public IP or domain.
- **📎 Attachment Support**: Send files and images directly via dashboard or API.
- **🏗️ Git Pre-commit Hooks**: Automated linting and formatting using Husky and Biome to ensure code quality.
- **⚡ High Performance**: Built on Bun + Hono for millisecond-level dispatch latency.
---
@@ -91,8 +93,37 @@ Automatically builds and pushes Docker images to GitHub Container Registry (GHCR
See [CHANGELOG.md](CHANGELOG.md) for version history.
## 📡 Webhook Usage
- **Personal Inbox**: `POST /api/webhook/:your_token/dm`
- **Topic**: `POST /api/webhook/:your_token/topic/:topic_slug`
### 1. Send to Personal Inbox (DM)
- **URL**: `POST /api/webhook/:your_token/dm`
- **Format**: JSON or Multipart Form-Data
### 2. Send to Topic
- **URL**: `POST /api/webhook/:your_token/topic/:topic_slug`
### Examples (using curl)
**Send Text (JSON):**
```bash
curl -X POST -H "Content-Type: application/json" \
-d '{"content":{"text":"Hello World"}}' \
http://localhost:3000/api/webhook/YOUR_TOKEN/dm
```
**Send File (Multipart):**
```bash
curl -X POST \
-F "content={\"text\":\"Check this file\"}" \
-F "file=@/path/to/report.pdf" \
http://localhost:3000/api/webhook/YOUR_TOKEN/dm
```
**Send Image:**
```bash
curl -X POST \
-F "image=@/path/to/screenshot.png" \
http://localhost:3000/api/webhook/YOUR_TOKEN/dm
```
## 📂 Project Structure
- `apps/server`: Core API service

View File

@@ -31,12 +31,14 @@
- **🚀 极简推送 (Personal Inbox)**: 每个用户拥有专属的 Webhook Token直接向 `/dm` 接口发送即可在飞书收到私聊,零配置成本。
- **📢 主题订阅 (Topic Model)**: 灵活的“发布-订阅”机制。告警发送至 Topic系统自动分发给所有订阅成员。
- **👥 群聊分发 (Group Support)**: 告警可同步分发至绑定的飞书群聊,支持机器人自动发现与解绑。
- **🛡️ 权限与审计**:
- **🛡️ 权限与审计**
- 话题创建需经过管理员审批。
- 记录完整的 `Alert Task` 日志,实现发送链路可追溯。
- **📊 实时看板**: Grafana 风格的监控界面,直观展示系统运行健壮性。
- **🔌 长连接模式 (WebSocket)**: 支持飞书开放平台长连接,无需公网 IP 或域名即可在内网环境接收事件回调。
- **⚡ 高性能架构**: 基于 Bun + Hono 的全异步架构,毫秒级分发延迟
- **📊 实时看板**Grafana 风格的监控界面,直观展示系统运行健壮性。
- **🔌 长连接模式 (WebSocket)**支持飞书开放平台长连接,无需公网 IP 或域名即可在内网环境接收事件回调。
- **📎 附件支持**:支持通过控制面板或 API 直接发送文件和图片
- **🏗️ Git Pre-commit 钩子**:使用 Husky 和 Biome 自动进行代码检查和格式化,确保代码质量。
- **⚡ 高性能**:基于 Bun + Hono 构建,毫秒级分发延迟。
---
@@ -102,10 +104,35 @@ docker-compose up -d
## 📡 Webhook 使用指南
### 1. 发送给个人 (Personal Inbox)
**URL**: `POST /api/webhook/:your_token/dm`
- **URL**: `POST /api/webhook/:your_token/dm`
- **格式**: JSON 或 Multipart Form-Data
### 2. 发送到主题 (Topic)
**URL**: `POST /api/webhook/:your_token/topic/:topic_slug`
- **URL**: `POST /api/webhook/:your_token/topic/:topic_slug`
### 使用示例 (curl)
**发送纯文字 (JSON):**
```bash
curl -X POST -H "Content-Type: application/json" \
-d '{"content":{"text":"你好,世界"}}' \
http://localhost:3000/api/webhook/YOUR_TOKEN/dm
```
**发送文件 (Multipart):**
```bash
curl -X POST \
-F "content={\"text\":\"请查看附件\"}" \
-F "file=@/path/to/report.pdf" \
http://localhost:3000/api/webhook/YOUR_TOKEN/dm
```
**发送图片:**
```bash
curl -X POST \
-F "image=@/path/to/screenshot.png" \
http://localhost:3000/api/webhook/YOUR_TOKEN/dm
```
---

View File

@@ -1,6 +1,6 @@
{
"name": "@alertmessagecenter/server",
"version": "1.4.0",
"version": "1.5.0",
"scripts": {
"dev": "bun run --env-file .env --watch src/index.ts",
"start": "bun run src/index.ts",

View File

@@ -1,3 +1,6 @@
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import * as lark from "@larksuiteoapi/node-sdk";
import { logger } from "./lib/logger";
@@ -64,6 +67,77 @@ export class FeishuClient {
}
}
async uploadFile(
fileType: "opus" | "mp4" | "pdf" | "doc" | "xls" | "ppt" | "stream",
fileName: string,
fileBuffer: Buffer,
): Promise<string> {
const tempPath = path.join(
os.tmpdir(),
`feishu_upload_${Date.now()}_${fileName}`,
);
try {
fs.writeFileSync(tempPath, fileBuffer);
const response = await this.client.im.file.create({
data: {
file_type: fileType,
file_name: fileName,
file: fs.createReadStream(tempPath),
},
});
if (!response || !response.file_key) {
logger.error({ response }, "Feishu upload file error: no file_key");
throw new Error(
"Failed to upload file to Feishu: no file_key returned",
);
}
return response.file_key;
} catch (e) {
console.error("Feishu upload file SDK error:", e);
throw e;
} finally {
// Clean up after a short delay to ensure stream is processed
setTimeout(() => {
if (fs.existsSync(tempPath)) {
fs.unlinkSync(tempPath);
}
}, 10000);
}
}
async uploadImage(imageBuffer: Buffer): Promise<string> {
const tempPath = path.join(os.tmpdir(), `feishu_upload_img_${Date.now()}`);
try {
fs.writeFileSync(tempPath, imageBuffer);
const response = await this.client.im.image.create({
data: {
image_type: "message",
image: fs.createReadStream(tempPath),
},
});
if (!response || !response.image_key) {
logger.error({ response }, "Feishu upload image error: no image_key");
throw new Error(
"Failed to upload image to Feishu: no image_key returned",
);
}
return response.image_key;
} catch (e) {
console.error("Feishu upload image SDK error:", e);
throw e;
} finally {
setTimeout(() => {
if (fs.existsSync(tempPath)) {
fs.unlinkSync(tempPath);
}
}, 10000);
}
}
async getUserAccessToken(
code: string,
): Promise<UserAccessTokenData | undefined> {

View File

@@ -1,5 +1,5 @@
import { eq } from "drizzle-orm";
import { Hono } from "hono";
import { type Context, Hono } from "hono";
import { db } from "./db";
import { alertLogs, alertTasks, topics, users } from "./db/schema";
import { feishuClient } from "./feishu";
@@ -15,18 +15,122 @@ interface Recipient {
idType: FeishuReceiveIdType;
}
interface Topic {
slug: string;
name: string;
isGlobal: boolean;
subscriptions?: { user: User }[];
groupChats?: { id: string; name: string; chatId: string; status: string }[];
}
interface User {
id: string;
name: string;
feishuUserId: string;
}
interface WebhookBody {
msg_type?: string;
content?: unknown;
card?: unknown;
post?: unknown;
image_key?: string;
file_key?: string;
audio_key?: string;
sticker_key?: string;
chat_id?: string;
user_id?: string;
uuid?: string;
token?: string;
file_type?: string;
file_name?: string;
[key: string]: unknown;
}
const webhook = new Hono();
const getRequestBody = async (c: Context): Promise<WebhookBody> => {
const contentType = c.req.header("Content-Type") || "";
let body: WebhookBody;
if (contentType.includes("application/json")) {
try {
body = await c.req.json();
} catch (_e) {
throw new Error("Invalid JSON body");
}
} else if (
contentType.includes("multipart/form-data") ||
contentType.includes("application/x-www-form-urlencoded")
) {
body = (await c.req.parseBody()) as unknown as WebhookBody;
// Handle stringified JSON fields in multipart
const complexFields: (keyof WebhookBody)[] = ["content", "card", "post"];
for (const field of complexFields) {
const val = body[field];
if (typeof val === "string") {
try {
body[field] = JSON.parse(val);
} catch {
// Not JSON, leave as is
}
}
}
} else {
// Fallback: try parsing as JSON
try {
const text = await c.req.text();
if (!text || text.trim() === "") {
throw new Error("Empty body");
}
body = JSON.parse(text);
} catch (_e) {
throw new Error("Invalid or missing request body");
}
}
// Proxy upload if files are present
const file = Array.isArray(body.file) ? (body.file[0] as unknown) : body.file;
if (file instanceof File) {
const buffer = Buffer.from(await file.arrayBuffer());
const fileType =
(body.file_type as
| "opus"
| "mp4"
| "pdf"
| "doc"
| "xls"
| "ppt"
| "stream") || "stream";
const fileName = (body.file_name as string) || file.name;
const fileKey = await feishuClient.uploadFile(fileType, fileName, buffer);
body.file_key = fileKey;
delete body.file;
}
const image = Array.isArray(body.image)
? (body.image[0] as unknown)
: body.image;
if (image instanceof File) {
const buffer = Buffer.from(await image.arrayBuffer());
const imageKey = await feishuClient.uploadImage(buffer);
body.image_key = imageKey;
delete body.image;
}
return { ...body };
};
const dispatchAlert = async (
c: any,
topic: any,
body: any,
user: any | null,
c: Context,
topic: Topic,
body: WebhookBody,
user: User | null,
) => {
// 2. Collect recipients
const userRecipients: Recipient[] = (topic.subscriptions || [])
.map((sub: any) => sub.user)
.map((u: any) => {
const userRecipients: (Recipient | null)[] = (topic.subscriptions || [])
.map((sub) => sub.user)
.map((u) => {
if (!u || !u.feishuUserId) return null;
return {
type: "user" as const,
@@ -37,12 +141,15 @@ const dispatchAlert = async (
? "open_id"
: "user_id") as FeishuReceiveIdType,
};
})
.filter((u: any): u is Recipient => u !== null);
});
const validUserRecipients: Recipient[] = userRecipients.filter(
(u): u is Recipient => u !== null,
);
const groupRecipients: Recipient[] = (topic.groupChats || [])
.filter((g: any) => g.status === "approved")
.map((g: any) => ({
.filter((g) => g.status === "approved")
.map((g) => ({
type: "group",
id: g.id, // Binding ID
name: g.name,
@@ -50,7 +157,10 @@ const dispatchAlert = async (
idType: "chat_id" as FeishuReceiveIdType,
}));
const allRecipients: Recipient[] = [...userRecipients, ...groupRecipients];
const allRecipients: Recipient[] = [
...validUserRecipients,
...groupRecipients,
];
const [task] = await db
.insert(alertTasks)
@@ -60,7 +170,8 @@ const dispatchAlert = async (
status: "processing",
recipientCount: allRecipients.length,
successCount: 0,
payload: body,
// biome-ignore lint/suspicious/noExplicitAny: Drizzle expects specific jsonb type
payload: body as any,
})
.returning();
@@ -80,7 +191,8 @@ const dispatchAlert = async (
logger.info(
{
taskId: task.id,
userCount: userRecipients.length,
slug: topic.slug,
userCount: validUserRecipients.length,
groupCount: groupRecipients.length,
},
"[Webhook] Dispatching alerts",
@@ -90,65 +202,127 @@ const dispatchAlert = async (
Promise.allSettled(
allRecipients.map(async (recipient) => {
try {
// Construct message content
let msgType = body.msg_type || "text";
let content = body.content;
// Construct messages list
const messagesToSend: {
type: string;
content: Record<string, unknown> | string;
}[] = [];
// 1. Text content
if (body.content) {
const content = JSON.parse(JSON.stringify(body.content));
const msgType = body.msg_type || "text";
// Add prefix for text
if (
msgType === "text" &&
content &&
typeof content === "object" &&
"text" in content
) {
(content as Record<string, unknown>).text = `[Direct Message]\n${
(content as Record<string, unknown>).text
}`;
}
// Add prefix for interactive
if (
msgType === "interactive" &&
content &&
typeof content === "object" &&
"header" in content
) {
const c = content as Record<
string,
Record<string, Record<string, unknown>>
>;
if (c.header?.title?.content) {
c.header.title.content = `[${topic.slug || topic.name}] ${
c.header.title.content
}`;
}
}
messagesToSend.push({
type: msgType,
content: content as Record<string, unknown> | string,
});
}
// 2. Image
if (body.image_key) {
messagesToSend.push({
type: "image",
content: { image_key: body.image_key },
});
}
// 3. File
if (body.file_key) {
messagesToSend.push({
type: "file",
content: { file_key: body.file_key },
});
}
// 4. Fallback for no explicit content/attachment keys
if (messagesToSend.length === 0) {
let msgType = body.msg_type || "text";
let content: unknown = body.content;
// Special handling for incomplete payloads (missing 'content')
if (!content) {
// 1. Special case: Unwrap 'card' if provided (convenience for user)
if (body.card) {
content = body.card;
if (!msgType) msgType = "interactive";
} else {
// 2. Pass-through strategy: Use rest of body as content
// Exclude keys that are definitely not part of content
const { msg_type, token, ...rest } = body;
const { msg_type: _msg_type, token: _token, ...rest } = body;
content = rest;
// 3. Infer msgType if missing
if (!msgType) {
if (body.post) msgType = "post";
else if (body.file_key && body.image_key)
msgType = "media"; // Media has both
else if (body.file_key && body.image_key) msgType = "media";
else if (body.image_key) msgType = "image";
else if (body.file_key) msgType = "file";
else if (body.audio_key) msgType = "audio";
else if (body.sticker_key) msgType = "sticker";
else if (body.chat_id) msgType = "share_chat";
else if (body.user_id) msgType = "share_user";
else if (body.header || body.elements)
msgType = "interactive"; // Unwrapped card
else if (body.header || body.elements) msgType = "interactive";
else {
// Fallback to text
msgType = "text";
// For text, content must be simple or stringified
content = { text: JSON.stringify(body, null, 2) };
}
}
}
} else {
// Deep clone content to avoid mutating shared object for parallel requests if we modify it
content = JSON.parse(JSON.stringify(content));
}
// Add metadata
if (msgType === "text" && content.text) {
content.text = `[${topic.name}]\n${content.text}`;
}
if (msgType === "interactive" && content.header) {
content.header.title.content = `[${topic.name}] ${content.header.title.content}`;
// Add prefix for inferred types
if (
msgType === "text" &&
content &&
typeof content === "object" &&
"text" in content
) {
(content as Record<string, unknown>).text = `[${topic.name}]\n${
(content as Record<string, unknown>).text
}`;
}
messagesToSend.push({
type: msgType,
content: content as Record<string, unknown> | string,
});
}
let successCount = 0;
for (const msg of messagesToSend) {
await feishuClient.sendMessage(
recipient.feishuId,
recipient.idType,
msgType,
content,
msg.type,
msg.content,
body.uuid,
);
successCount++;
}
return { recipientId: recipient.id, status: "sent", error: null };
return {
recipientId: recipient.id,
status: successCount > 0 ? "sent" : "failed",
error: null,
};
} catch (error: unknown) {
const errorMessage =
error instanceof Error ? error.message : String(error);
@@ -201,6 +375,7 @@ const dispatchAlert = async (
const recipient = allRecipients[index];
if (r.status === "fulfilled") {
const val = r.value as {
recipientId: string;
status: "sent" | "failed";
error: string | null;
};
@@ -211,14 +386,13 @@ const dispatchAlert = async (
status: val.status as "sent" | "failed",
error: val.error,
};
} else {
}
return {
taskId: task.id,
userId: recipient.type === "user" ? recipient.id : null,
status: "failed" as const,
error: r.status === "rejected" ? String(r.reason) : "Unknown error",
};
}
});
if (logs.length > 0) {
@@ -277,13 +451,9 @@ webhook.post("/topic/:slug", async (c) => {
// biome-ignore lint/suspicious/noExplicitAny: Webhook body can be any arbitrary JSON
let body: Record<string, any>;
try {
const rawBody = await c.req.text();
if (!rawBody || rawBody.trim() === "") {
return c.json({ error: "Empty body" }, 400);
}
body = JSON.parse(rawBody);
} catch (_e) {
return c.json({ error: "Invalid JSON body" }, 400);
body = await getRequestBody(c);
} catch (e) {
return c.json({ error: (e as Error).message }, 400);
}
return dispatchAlert(c, topic, body, null);
@@ -312,12 +482,13 @@ webhook.post("/:token/topic/:slug", async (c) => {
return c.json({ error: "Topic not found" }, 404);
}
let user: any = null;
let user: User | null = null;
if (!topic.isGlobal) {
// 0. Find the User by Token
user = await db.query.users.findFirst({
user =
(await db.query.users.findFirst({
where: eq(users.personalToken, token),
});
})) || null;
if (!user) {
logger.warn({ token }, "[Webhook] Invalid personal token");
@@ -328,13 +499,9 @@ webhook.post("/:token/topic/:slug", async (c) => {
// biome-ignore lint/suspicious/noExplicitAny: Webhook body can be any arbitrary JSON
let body: Record<string, any>;
try {
const rawBody = await c.req.text();
if (!rawBody || rawBody.trim() === "") {
return c.json({ error: "Empty body" }, 400);
}
body = JSON.parse(rawBody);
} catch (_e) {
return c.json({ error: "Invalid JSON body" }, 400);
body = await getRequestBody(c);
} catch (e) {
return c.json({ error: (e as Error).message }, 400);
}
return dispatchAlert(c, topic, body, user);
@@ -374,13 +541,9 @@ webhook.post("/:token/dm", async (c) => {
// biome-ignore lint/suspicious/noExplicitAny: Webhook body can be any arbitrary JSON
let body: Record<string, any>;
try {
const rawBody = await c.req.text();
if (!rawBody || rawBody.trim() === "") {
return c.json({ error: "Empty body" }, 400);
}
body = JSON.parse(rawBody);
} catch (_e) {
return c.json({ error: "Invalid JSON body" }, 400);
body = await getRequestBody(c);
} catch (e) {
return c.json({ error: (e as Error).message }, 400);
}
// 1. Create Task (topicSlug is null for DM)
@@ -399,107 +562,103 @@ webhook.post("/:token/dm", async (c) => {
// 2. Send Message
(async () => {
try {
const messagesToSend: {
type: string;
content: Record<string, unknown> | string;
}[] = [];
// Text content
if (body.content) {
const content = JSON.parse(JSON.stringify(body.content));
messagesToSend.push({ type: body.msg_type || "text", content });
}
// Image
if (body.image_key) {
messagesToSend.push({
type: "image",
content: { image_key: body.image_key },
});
}
// File
if (body.file_key) {
messagesToSend.push({
type: "file",
content: { file_key: body.file_key },
});
}
// Fallback: if no explicit content/attachment keys, check other fields
if (messagesToSend.length === 0) {
let msgType = body.msg_type || "text";
let content = body.content;
// Special handling for incomplete payloads (missing 'content')
if (!content) {
// 1. Interactive / Card
if ((msgType === "interactive" || !msgType) && body.card) {
msgType = "interactive";
content = body.card;
}
// 2. Post (Rich Text)
else if ((msgType === "post" || !msgType) && body.post) {
} else if ((msgType === "post" || !msgType) && body.post) {
msgType = "post";
content = { post: body.post };
}
// 3. Image
else if ((msgType === "image" || !msgType) && body.image_key) {
msgType = "image";
content = { image_key: body.image_key };
}
// 4. File
else if ((msgType === "file" || !msgType) && body.file_key) {
msgType = "file";
content = { file_key: body.file_key };
}
// 5. Audio
else if ((msgType === "audio" || !msgType) && body.audio_key) {
} else if ((msgType === "audio" || !msgType) && body.audio_key) {
msgType = "audio";
content = { file_key: body.audio_key };
}
// 6. Media (Video)
else if (
} else if (
(msgType === "media" || !msgType) &&
body.file_key &&
body.image_key
) {
msgType = "media";
content = { file_key: body.file_key, image_key: body.image_key };
}
// 7. Sticker
else if ((msgType === "sticker" || !msgType) && body.sticker_key) {
} else if ((msgType === "sticker" || !msgType) && body.sticker_key) {
msgType = "sticker";
content = { file_key: body.sticker_key };
}
// 8. Share Chat
else if ((msgType === "share_chat" || !msgType) && body.chat_id) {
} else if ((msgType === "share_chat" || !msgType) && body.chat_id) {
msgType = "share_chat";
content = { chat_id: body.chat_id };
}
// 9. Share User
else if ((msgType === "share_user" || !msgType) && body.user_id) {
} else if ((msgType === "share_user" || !msgType) && body.user_id) {
msgType = "share_user";
content = { user_id: body.user_id };
}
// Fallback
else {
} else {
const { msg_type: _msg_type, token: _token, ...rest } = body;
content = rest;
if (!msgType || msgType === "text") {
msgType = "text";
content = { text: JSON.stringify(body, null, 2) };
content = { text: JSON.stringify(rest, null, 2) };
}
}
} else {
// Deep clone content to avoid mutating shared object for parallel requests if we modify it
content = JSON.parse(JSON.stringify(content));
messagesToSend.push({ type: msgType, content });
}
// Add metadata
if (msgType === "text" && content.text) {
content.text = `[Direct Message]\n${content.text}`;
}
if (msgType === "interactive" && content.header) {
content.header.title.content = `[DM] ${content.header.title.content}`;
}
const idType = user.feishuUserId.startsWith("ou_")
? "open_id"
: "user_id";
const uuid = body.uuid || crypto.randomUUID();
let totalSuccess = 0;
for (const msg of messagesToSend) {
await feishuClient.sendMessage(
user.feishuUserId,
idType,
msgType,
content,
uuid,
"open_id",
msg.type,
msg.content,
body.uuid,
);
totalSuccess++;
}
const finalStatus = totalSuccess > 0 ? "completed" : "failed";
// Update Task
await db
.update(alertTasks)
.set({
status: "completed",
successCount: 1,
status: finalStatus,
successCount: totalSuccess === messagesToSend.length ? 1 : 0, // In DM case, 1 recipient
updatedAt: new Date(),
})
.where(eq(alertTasks.id, task.id));
// Insert Log
// Log Sent
await db.insert(alertLogs).values({
taskId: task.id,
userId: user.id,
status: "sent" as const,
status: totalSuccess > 0 ? "sent" : "failed",
});
} catch (error: unknown) {
const errorMessage =

View File

@@ -1,6 +1,6 @@
{
"name": "@alertmessagecenter/web",
"version": "1.4.0",
"version": "1.5.0",
"type": "module",
"scripts": {
"dev": "bun run --env-file .env vite",

View File

@@ -0,0 +1,195 @@
import {
AlertCircle,
CheckCircle2,
Loader2,
Paperclip,
Send,
X,
} from "lucide-react";
import { useRef, useState } from "react";
interface SendAlertFormProps {
webhookUrl: string;
onSuccess?: () => void;
placeholder?: string;
title?: string;
}
export default function SendAlertForm({
webhookUrl,
onSuccess,
placeholder = "Type your message here...",
title = "Send Quick Alert",
}: SendAlertFormProps) {
const [content, setContent] = useState("");
const [file, setFile] = useState<File | null>(null);
const [isSending, setIsSending] = useState(false);
const [status, setStatus] = useState<{
type: "success" | "error";
message: string;
} | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files?.[0]) {
setFile(e.target.files[0]);
setStatus(null);
}
};
const removeFile = () => {
setFile(null);
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!content.trim() && !file) return;
setIsSending(true);
setStatus(null);
try {
const formData = new FormData();
if (content.trim()) {
// We send content as a stringified JSON if it's elaborate,
// but for "Pure Proxy", simple text is just a field.
// Our backend getRequestBody handles both.
formData.append("content", JSON.stringify({ text: content }));
formData.append("msg_type", "text");
}
if (file) {
const isImage = file.type.startsWith("image/");
formData.append(isImage ? "image" : "file", file);
if (!content.trim()) {
formData.append("msg_type", isImage ? "image" : "file");
}
}
const response = await fetch(webhookUrl, {
method: "POST",
body: formData,
});
if (response.ok) {
setStatus({ type: "success", message: "Alert sent successfully!" });
setContent("");
setFile(null);
if (fileInputRef.current) fileInputRef.current.value = "";
onSuccess?.();
setTimeout(() => setStatus(null), 3000);
} else {
const error = await response.json();
setStatus({
type: "error",
message: error.error || "Failed to send alert",
});
}
} catch (_err) {
setStatus({ type: "error", message: "Network error. Please try again." });
} finally {
setIsSending(false);
}
};
return (
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
<div className="px-5 py-3 border-b border-gray-100 bg-gray-50 flex items-center justify-between">
<h3 className="text-sm font-bold text-gray-700 uppercase tracking-tight">
{title}
</h3>
</div>
<form onSubmit={handleSubmit} className="p-5">
<div className="relative">
<textarea
className="w-full min-h-[100px] p-3 text-sm text-gray-800 border border-gray-200 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent outline-none transition-all placeholder:text-gray-400 resize-none"
placeholder={placeholder}
value={content}
onChange={(e) => setContent(e.target.value)}
disabled={isSending}
/>
</div>
<div className="mt-4 space-y-3">
{file && (
<div className="flex items-center justify-between p-2 px-3 bg-indigo-50 rounded-lg border border-indigo-100 animate-in fade-in slide-in-from-top-2">
<div className="flex items-center">
<Paperclip className="w-4 h-4 text-indigo-500 mr-2" />
<span className="text-xs font-medium text-indigo-700 truncate max-w-[200px]">
{file.name}
</span>
<span className="ml-2 text-[10px] text-indigo-400">
({(file.size / 1024).toFixed(1)} KB)
</span>
</div>
<button
type="button"
onClick={removeFile}
className="text-indigo-400 hover:text-indigo-600 p-1 transition-colors"
>
<X className="w-4 h-4" />
</button>
</div>
)}
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-2">
<input
type="file"
ref={fileInputRef}
onChange={handleFileChange}
className="hidden"
id="alert-file-upload"
/>
<label
htmlFor="alert-file-upload"
className="cursor-pointer inline-flex items-center px-3 py-1.5 text-xs font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors border border-gray-200"
>
<Paperclip className="w-3.5 h-3.5 mr-1.5" />
Attach File
</label>
</div>
<button
type="submit"
disabled={isSending || (!content.trim() && !file)}
className="inline-flex items-center px-4 py-2 text-sm font-semibold text-white bg-indigo-600 hover:bg-indigo-700 rounded-lg transition-all disabled:opacity-50 disabled:cursor-not-allowed shadow-sm hover:shadow-indigo-500/20"
>
{isSending ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Sending...
</>
) : (
<>
<Send className="w-4 h-4 mr-2" />
Send Alert
</>
)}
</button>
</div>
</div>
{status && (
<div
className={`mt-4 p-3 rounded-lg flex items-start animate-in fade-in zoom-in-95 ${
status.type === "success"
? "bg-green-50 text-green-700 border border-green-100"
: "bg-red-50 text-red-700 border border-red-100"
}`}
>
{status.type === "success" ? (
<CheckCircle2 className="w-4 h-4 mr-2 mt-0.5 flex-shrink-0" />
) : (
<AlertCircle className="w-4 h-4 mr-2 mt-0.5 flex-shrink-0" />
)}
<span className="text-xs font-medium">{status.message}</span>
</div>
)}
</form>
</div>
);
}

View File

@@ -4,6 +4,7 @@ import {
Globe,
Lock,
Plus,
Send,
Settings,
ShieldCheck,
User,
@@ -14,6 +15,7 @@ import {
import { useCallback, useEffect, useState } from "react";
import GroupBindingsModal from "../components/GroupBindingsModal";
import Modal from "../components/Modal";
import SendAlertForm from "../components/SendAlertForm";
import { useAuth } from "../contexts/AuthContext";
import { client } from "../lib/client";
@@ -64,6 +66,8 @@ export default function TopicsView() {
type: "success" | "error";
message: string;
} | null>(null);
const [showPersonalSend, setShowPersonalSend] = useState(false);
const [activeSendTopic, setActiveSendTopic] = useState<string | null>(null);
const fetchTopics = useCallback(async () => {
setLoading(true);
@@ -398,17 +402,40 @@ export default function TopicsView() {
</div>
</div>
<div className="flex items-center gap-4 bg-white/10 p-4 rounded-xl backdrop-blur-sm border border-white/10">
<div className="bg-indigo-500/30 p-2.5 rounded-lg border border-white/20">
<Copy className="w-6 h-6" />
</div>
<div className="text-sm">
<div className="font-bold">Direct Push</div>
<div className="text-indigo-200 text-xs">
Always delivered to you
<div className="text-right">
<p className="text-xs text-indigo-300 uppercase tracking-widest font-bold mb-1">
Status
</p>
<div className="flex items-center text-white font-semibold">
<div className="w-2 h-2 bg-green-400 rounded-full mr-2 animate-pulse" />
Active
</div>
</div>
</div>
</div>
<div className="mt-6 pt-6 border-t border-white/10">
<button
type="button"
onClick={() => setShowPersonalSend(!showPersonalSend)}
className="inline-flex items-center text-sm font-bold text-white hover:text-indigo-200 transition-colors"
>
<Send className="w-4 h-4 mr-2" />
{showPersonalSend
? "Hide Send Form"
: "Send Quick Message to Myself"}
</button>
{showPersonalSend && (
<div className="mt-4 text-gray-900 max-w-2xl">
<SendAlertForm
webhookUrl={getDmWebhookUrl()}
title="Send to Personal Inbox"
placeholder="What would you like to notify yourself about?"
/>
</div>
)}
</div>
</div>
</div>
@@ -436,7 +463,7 @@ export default function TopicsView() {
<div className="flex items-center justify-between">
<div className="flex-1">
<div className="flex items-center justify-between">
<p className="text-sm font-medium text-indigo-600 truncate flex items-center">
<div className="text-sm font-medium text-indigo-600 truncate flex items-center">
{topic.name}
{topic.isGlobal ? (
<span className="ml-2 inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold bg-purple-100 text-purple-700 border border-purple-200 uppercase tracking-tight">
@@ -449,7 +476,7 @@ export default function TopicsView() {
Private
</span>
)}
</p>
</div>
<div className="flex items-center space-x-2">
<button
type="button"
@@ -498,8 +525,8 @@ export default function TopicsView() {
)}
</div>
</div>
<div className="mt-2 sm:flex sm:justify-between">
<div className="sm:flex flex-col">
<div className="mt-2">
<div className="flex flex-col w-full">
<p className="flex items-center text-sm text-gray-500">
Slug:{" "}
<span className="font-mono ml-1 bg-gray-100 px-1 rounded">
@@ -533,6 +560,7 @@ export default function TopicsView() {
</div>
)}
</div>
{currentUser && (
<div
className={`mt-3 ${topic.isGlobal ? "grid grid-cols-1 md:grid-cols-2 gap-4" : "space-y-3"}`}
@@ -543,6 +571,7 @@ export default function TopicsView() {
<span className="text-[10px] font-bold text-gray-500 uppercase tracking-widest">
Your Personal Webhook
</span>
<div className="flex items-center space-x-2">
<button
type="button"
onClick={() =>
@@ -565,6 +594,27 @@ export default function TopicsView() {
</>
)}
</button>
<button
type="button"
onClick={() =>
setActiveSendTopic(
activeSendTopic === topic.id
? null
: topic.id,
)
}
className={`flex items-center text-xs font-semibold px-2 py-0.5 rounded border transition-all hover:shadow hover:translate-y-[-1px] ${
activeSendTopic === topic.id
? "bg-indigo-600 text-white border-indigo-700 shadow-inner"
: "bg-white text-indigo-600 border-gray-200 shadow-sm"
}`}
>
<Send className="w-3 h-3 mr-1" />
{activeSendTopic === topic.id
? "Close"
: "Send Message"}
</button>
</div>
</div>
<div className="text-[11px] font-mono text-gray-600 break-all select-all bg-white/60 p-1.5 rounded border border-gray-100/50 leading-relaxed">
{getWebhookUrl(topic.slug)}
@@ -624,6 +674,17 @@ export default function TopicsView() {
)}
</div>
)}
{activeSendTopic === topic.id && (
<div className="mt-4 animate-in fade-in slide-in-from-top-2 max-w-2xl bg-white p-4 rounded-lg border border-indigo-100 shadow-md">
<SendAlertForm
webhookUrl={getWebhookUrl(topic.slug)}
title={`Send Message to ${topic.name}`}
placeholder={`Enter alert content for ${topic.name}...`}
onSuccess={() => setActiveSendTopic(null)}
/>
</div>
)}
</div>
</div>
</div>

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.3.11/schema.json",
"$schema": "https://biomejs.dev/schemas/2.3.14/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",

View File

@@ -4,12 +4,15 @@
"": {
"name": "alert-manager",
"devDependencies": {
"@biomejs/biome": "^2.3.14",
"bun-types": "latest",
"husky": "^9.1.7",
"lint-staged": "^16.2.7",
},
},
"apps/server": {
"name": "@alertmessagecenter/server",
"version": "1.2.0",
"version": "1.5.0",
"dependencies": {
"@hono/zod-validator": "^0.7.6",
"@larksuiteoapi/node-sdk": "^1.56.1",
@@ -28,7 +31,7 @@
},
"apps/web": {
"name": "@alertmessagecenter/web",
"version": "1.2.0",
"version": "1.5.0",
"dependencies": {
"clsx": "^2.0.0",
"hono": "^4.11.3",
@@ -98,6 +101,24 @@
"@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
"@biomejs/biome": ["@biomejs/biome@2.3.14", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.14", "@biomejs/cli-darwin-x64": "2.3.14", "@biomejs/cli-linux-arm64": "2.3.14", "@biomejs/cli-linux-arm64-musl": "2.3.14", "@biomejs/cli-linux-x64": "2.3.14", "@biomejs/cli-linux-x64-musl": "2.3.14", "@biomejs/cli-win32-arm64": "2.3.14", "@biomejs/cli-win32-x64": "2.3.14" }, "bin": { "biome": "bin/biome" } }, "sha512-QMT6QviX0WqXJCaiqVMiBUCr5WRQ1iFSjvOLoTk6auKukJMvnMzWucXpwZB0e8F00/1/BsS9DzcKgWH+CLqVuA=="],
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.14", "", { "os": "darwin", "cpu": "arm64" }, "sha512-UJGPpvWJMkLxSRtpCAKfKh41Q4JJXisvxZL8ChN1eNW3m/WlPFJ6EFDCE7YfUb4XS8ZFi3C1dFpxUJ0Ety5n+A=="],
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.14", "", { "os": "darwin", "cpu": "x64" }, "sha512-PNkLNQG6RLo8lG7QoWe/hhnMxJIt1tEimoXpGQjwS/dkdNiKBLPv4RpeQl8o3s1OKI3ZOR5XPiYtmbGGHAOnLA=="],
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-KT67FKfzIw6DNnUNdYlBg+eU24Go3n75GWK6NwU4+yJmDYFe9i/MjiI+U/iEzKvo0g7G7MZqoyrhIYuND2w8QQ=="],
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-LInRbXhYujtL3sH2TMCH/UBwJZsoGwfQjBrMfl84CD4hL/41C/EU5mldqf1yoFpsI0iPWuU83U+nB2TUUypWeg=="],
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.14", "", { "os": "linux", "cpu": "x64" }, "sha512-ZsZzQsl9U+wxFrGGS4f6UxREUlgHwmEfu1IrXlgNFrNnd5Th6lIJr8KmSzu/+meSa9f4rzFrbEW9LBBA6ScoMA=="],
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.14", "", { "os": "linux", "cpu": "x64" }, "sha512-KQU7EkbBBuHPW3/rAcoiVmhlPtDSGOGRPv9js7qJVpYTzjQmVR+C9Rfcz+ti8YCH+zT1J52tuBybtP4IodjxZQ=="],
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.14", "", { "os": "win32", "cpu": "arm64" }, "sha512-+IKYkj/pUBbnRf1G1+RlyA3LWiDgra1xpS7H2g4BuOzzRbRB+hmlw0yFsLprHhbbt7jUzbzAbAjK/Pn0FDnh1A=="],
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.14", "", { "os": "win32", "cpu": "x64" }, "sha512-oizCjdyQ3WJEswpb3Chdngeat56rIdSYK12JI3iI11Mt5T5EXcZ7WLuowzEaFPNJ3zmOQFliMN8QY1Pi+qsfdQ=="],
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="],
"@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="],
@@ -264,6 +285,12 @@
"@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="],
"ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="],
"ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
"ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
"any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="],
"anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
@@ -312,13 +339,17 @@
"chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="],
"cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="],
"cli-truncate": ["cli-truncate@5.1.1", "", { "dependencies": { "slice-ansi": "^7.1.0", "string-width": "^8.0.0" } }, "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A=="],
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
"colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="],
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
"commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],
"commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
@@ -350,8 +381,12 @@
"electron-to-chromium": ["electron-to-chromium@1.5.267", "", {}, "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw=="],
"emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
"end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
"environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
@@ -366,6 +401,8 @@
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="],
"expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="],
"fast-copy": ["fast-copy@4.0.2", "", {}, "sha512-ybA6PDXIXOXivLJK/z9e+Otk7ve13I4ckBvGO5I2RRmBU1gMHLVDJYEuJYhGwez7YNlYji2M2DvVU+a9mSFDlw=="],
@@ -396,6 +433,8 @@
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
"get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="],
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
@@ -418,6 +457,8 @@
"hono": ["hono@4.11.3", "", {}, "sha512-PmQi306+M/ct/m5s66Hrg+adPnkD5jiO6IjA7WhWw0gSBSo1EcRegwuI1deZ+wd5pzCGynCcn2DprnE4/yEV4w=="],
"husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="],
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
@@ -430,6 +471,8 @@
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
"is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="],
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
@@ -448,12 +491,18 @@
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
"lint-staged": ["lint-staged@16.2.7", "", { "dependencies": { "commander": "^14.0.2", "listr2": "^9.0.5", "micromatch": "^4.0.8", "nano-spawn": "^2.0.0", "pidtree": "^0.6.0", "string-argv": "^0.3.2", "yaml": "^2.8.1" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-lDIj4RnYmK7/kXMya+qJsmkRFkGolciXjrsZ6PC25GdTfWOAWetR0ZbsNXRAj1EHHImRSalc+whZFg56F5DVow=="],
"listr2": ["listr2@9.0.5", "", { "dependencies": { "cli-truncate": "^5.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^6.1.0", "rfdc": "^1.4.1", "wrap-ansi": "^9.0.0" } }, "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g=="],
"lodash.identity": ["lodash.identity@3.0.0", "", {}, "sha512-AupTIzdLQxJS5wIYUQlgGyk2XRTfGXA+MCghDHqZk0pzUNYvd3EESS6dkChNauNYVIutcb0dfHw1ri9Q1yPV8Q=="],
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
"lodash.pickby": ["lodash.pickby@4.6.0", "", {}, "sha512-AZV+GsS/6ckvPOVQPXSiFFacKvKB4kOQu6ynt9wz0F3LO4R9Ij4K1ddYsIytDpSgLz88JHd9P+oaLeej5/Sl7Q=="],
"log-update": ["log-update@6.1.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w=="],
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
@@ -472,6 +521,8 @@
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="],
"mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="],
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
@@ -482,6 +533,8 @@
"mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="],
"nano-spawn": ["nano-spawn@2.0.0", "", {}, "sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="],
@@ -502,12 +555,16 @@
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
"onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"pidtree": ["pidtree@0.6.0", "", { "bin": { "pidtree": "bin/pidtree.js" } }, "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g=="],
"pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="],
"pino": ["pino@10.1.1", "", { "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^3.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^4.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-3qqVfpJtRQUCAOs4rTOEwLH6mwJJ/CSAlbis8fKOiMzTtXh0HN/VLsn3UWVTJ7U8DsWmxeNon2IpGb+wORXH4g=="],
@@ -570,8 +627,12 @@
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
"restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="],
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
"rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
"rollup": ["rollup@4.54.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.54.0", "@rollup/rollup-android-arm64": "4.54.0", "@rollup/rollup-darwin-arm64": "4.54.0", "@rollup/rollup-darwin-x64": "4.54.0", "@rollup/rollup-freebsd-arm64": "4.54.0", "@rollup/rollup-freebsd-x64": "4.54.0", "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", "@rollup/rollup-linux-arm-musleabihf": "4.54.0", "@rollup/rollup-linux-arm64-gnu": "4.54.0", "@rollup/rollup-linux-arm64-musl": "4.54.0", "@rollup/rollup-linux-loong64-gnu": "4.54.0", "@rollup/rollup-linux-ppc64-gnu": "4.54.0", "@rollup/rollup-linux-riscv64-gnu": "4.54.0", "@rollup/rollup-linux-riscv64-musl": "4.54.0", "@rollup/rollup-linux-s390x-gnu": "4.54.0", "@rollup/rollup-linux-x64-gnu": "4.54.0", "@rollup/rollup-linux-x64-musl": "4.54.0", "@rollup/rollup-openharmony-arm64": "4.54.0", "@rollup/rollup-win32-arm64-msvc": "4.54.0", "@rollup/rollup-win32-ia32-msvc": "4.54.0", "@rollup/rollup-win32-x64-gnu": "4.54.0", "@rollup/rollup-win32-x64-msvc": "4.54.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw=="],
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
@@ -594,10 +655,14 @@
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="],
"simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="],
"slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="],
"sonic-boom": ["sonic-boom@4.2.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww=="],
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
@@ -608,8 +673,14 @@
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
"string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="],
"string-width": ["string-width@8.1.1", "", { "dependencies": { "get-east-asian-width": "^1.3.0", "strip-ansi": "^7.1.0" } }, "sha512-KpqHIdDL9KwYk22wEOg/VIqYbrnLeSApsKT/bSj6Ez7pn3CftUiLAv2Lccpq1ALcpLV9UX1Ppn92npZWu2w/aw=="],
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
"strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
"strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="],
"sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="],
@@ -648,12 +719,16 @@
"vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="],
"wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="],
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="],
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@alertmessagecenter/server/bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="],
@@ -668,10 +743,14 @@
"rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="],
"sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],
"tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"vite/esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="],
"wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="],

View File

@@ -1,6 +1,6 @@
{
"name": "alertmessagecenter",
"version": "1.4.0",
"version": "1.5.0",
"workspaces": [
"apps/*"
],
@@ -10,9 +10,18 @@
"start": "bun run --filter '@alertmessagecenter/server' start",
"lint": "bunx @biomejs/biome lint .",
"format": "bunx @biomejs/biome format . --write",
"check": "bunx @biomejs/biome check --write ."
"check": "bunx @biomejs/biome check --write .",
"prepare": "husky"
},
"devDependencies": {
"bun-types": "latest"
"@biomejs/biome": "^2.3.14",
"bun-types": "latest",
"husky": "^9.1.7",
"lint-staged": "^16.2.7"
},
"lint-staged": {
"**/*.{ts,tsx,js,jsx,json}": [
"bunx --bun biome check --write --no-errors-on-unmatched --files-ignore-unknown=true"
]
}
}