mirror of
https://github.com/d0zingcat/alert-message-center.git
synced 2026-05-13 15:09:19 +00:00
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:
1
.husky/pre-commit
Normal file
1
.husky/pre-commit
Normal file
@@ -0,0 +1 @@
|
||||
bunx lint-staged
|
||||
14
CHANGELOG.md
14
CHANGELOG.md
@@ -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
|
||||
|
||||
@@ -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) | **简体中文**
|
||||
|
||||
## [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` 崩溃。
|
||||
- **附件优先级**:修复了当同时存在文本内容时附件被忽略的问题。
|
||||
|
||||
**更新日志** | [English](./CHANGELOG.md)
|
||||
|
||||
## [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。
|
||||
@@ -69,7 +83,7 @@
|
||||
- **用户 Token**:将用户的 `personalToken` 从 32 位 UUID 缩短为 8 位十六进制字符串,提升易用性。
|
||||
- **数据库迁移**:完善了数据库迁移流程,在 `db:migrate:deploy` 中集成了存量用户 Token 的自动缩短逻辑,确保线上环境数据的一致性。
|
||||
- **AI 规范**:更新了 `copilot-context.md`,明确要求 AI 在每次修改代码后必须进行代码风格和 Lint 检查。
|
||||
|
||||
|
||||
## [1.2.5] - 2026-01-15
|
||||
|
||||
### 修复
|
||||
@@ -143,6 +157,7 @@
|
||||
### 变更
|
||||
- **数据库**: 新增 `topic_group_chats` 和 `known_group_chats` 表。
|
||||
- **底层架构**: 重构了飞书客户端 (`FeishuClient`) 和事件处理逻辑,统一了 Webhook 和 WebSocket 的事件分发。
|
||||
|
||||
## [1.1.1] - 2026-01-13
|
||||
|
||||
### 修复
|
||||
|
||||
35
README.md
35
README.md
@@ -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
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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 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,
|
||||
});
|
||||
}
|
||||
|
||||
// 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}`;
|
||||
let successCount = 0;
|
||||
for (const msg of messagesToSend) {
|
||||
await feishuClient.sendMessage(
|
||||
recipient.feishuId,
|
||||
recipient.idType,
|
||||
msg.type,
|
||||
msg.content,
|
||||
body.uuid,
|
||||
);
|
||||
successCount++;
|
||||
}
|
||||
|
||||
await feishuClient.sendMessage(
|
||||
recipient.feishuId,
|
||||
recipient.idType,
|
||||
msgType,
|
||||
content,
|
||||
body.uuid,
|
||||
);
|
||||
|
||||
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",
|
||||
};
|
||||
}
|
||||
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({
|
||||
where: eq(users.personalToken, token),
|
||||
});
|
||||
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 {
|
||||
let msgType = body.msg_type || "text";
|
||||
let content = body.content;
|
||||
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}`;
|
||||
let totalSuccess = 0;
|
||||
for (const msg of messagesToSend) {
|
||||
await feishuClient.sendMessage(
|
||||
user.feishuUserId,
|
||||
"open_id",
|
||||
msg.type,
|
||||
msg.content,
|
||||
body.uuid,
|
||||
);
|
||||
totalSuccess++;
|
||||
}
|
||||
|
||||
const idType = user.feishuUserId.startsWith("ou_")
|
||||
? "open_id"
|
||||
: "user_id";
|
||||
const uuid = body.uuid || crypto.randomUUID();
|
||||
await feishuClient.sendMessage(
|
||||
user.feishuUserId,
|
||||
idType,
|
||||
msgType,
|
||||
content,
|
||||
uuid,
|
||||
);
|
||||
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 =
|
||||
|
||||
@@ -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",
|
||||
|
||||
195
apps/web/src/components/SendAlertForm.tsx
Normal file
195
apps/web/src/components/SendAlertForm.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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,28 +571,50 @@ export default function TopicsView() {
|
||||
<span className="text-[10px] font-bold text-gray-500 uppercase tracking-widest">
|
||||
Your Personal Webhook
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
copyToClipboard(
|
||||
getWebhookUrl(topic.slug),
|
||||
topic.id,
|
||||
)
|
||||
}
|
||||
className="text-indigo-600 hover:text-indigo-800 flex items-center text-xs font-semibold bg-white px-2 py-0.5 rounded border border-gray-200 shadow-sm transition-all hover:shadow hover:translate-y-[-1px]"
|
||||
>
|
||||
{copiedId === topic.id ? (
|
||||
<>
|
||||
<Check className="w-3 h-3 mr-1" />
|
||||
Copied
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="w-3 h-3 mr-1" />
|
||||
Copy URL
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
copyToClipboard(
|
||||
getWebhookUrl(topic.slug),
|
||||
topic.id,
|
||||
)
|
||||
}
|
||||
className="text-indigo-600 hover:text-indigo-800 flex items-center text-xs font-semibold bg-white px-2 py-0.5 rounded border border-gray-200 shadow-sm transition-all hover:shadow hover:translate-y-[-1px]"
|
||||
>
|
||||
{copiedId === topic.id ? (
|
||||
<>
|
||||
<Check className="w-3 h-3 mr-1" />
|
||||
Copied
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="w-3 h-3 mr-1" />
|
||||
Copy URL
|
||||
</>
|
||||
)}
|
||||
</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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
85
bun.lock
85
bun.lock
@@ -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=="],
|
||||
|
||||
15
package.json
15
package.json
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user