mirror of
https://github.com/d0zingcat/alert-message-center.git
synced 2026-05-13 15:09:19 +00:00
feat: add english readme and fix lint
Signed-off-by: d0zingcat <iamtangli42@gmail.com>
This commit is contained in:
11
CHANGELOG.md
11
CHANGELOG.md
@@ -5,6 +5,17 @@
|
||||
本文件的格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),
|
||||
并且本项目遵循 [语义化版本 (Semantic Versioning)](https://semver.org/lang/zh-CN/spec/v2.0.0.html)。
|
||||
|
||||
## [1.3.2] - 2026-01-17
|
||||
|
||||
### 新增
|
||||
- **群聊搜索功能**: 在绑定群聊时新增了实时搜索功能,解决了群聊过多时难以查找的问题。
|
||||
- **后端支持**: `GET /groups` 接口现在支持 `q` 查询参数进行模糊搜索,并提高了默认返回数量。
|
||||
- **搜索前端**: 引入了带防抖逻辑的搜索输入框和自定义下拉列表,提升了用户体验。
|
||||
|
||||
### 变更
|
||||
- **UI 优化**: 改进了 `GroupBindingsModal` 的视觉设计,使用了更现代的列表样式、状态图标和加载动画。
|
||||
- **文档优化**: 将 `README.md` 拆分为英文版 (`README.md`) 和中文版 (`README.zh-CN.md`),以更好地支持国际化。
|
||||
|
||||
## [1.3.1] - 2026-01-16
|
||||
|
||||
### 新增
|
||||
|
||||
121
README.md
121
README.md
@@ -1,50 +1,48 @@
|
||||
# Alert Message Center
|
||||
|
||||
**README** | [简体中文](./README.zh-CN.md)
|
||||
|
||||
[](https://bun.sh)
|
||||
[](https://www.postgresql.org/)
|
||||
|
||||
**Alert Message Center** 是一个现代化、企业级的告警路由与分发中心。它旨在将纷繁复杂的告警源(Prometheus, Grafana, 自建脚本等)与最终接收人解耦,通过 **飞书机器人私聊** 实现告警的精准触达。
|
||||
**Alert Message Center** is a modern, enterprise-grade alert routing and dispatching hub. It decouples complex alert sources (Prometheus, Grafana, custom scripts, etc.) from end recipients, ensuring precise alert delivery via **Feishu (Lark) private messages and group chats.**
|
||||
|
||||
---
|
||||
|
||||
## 📸 界面预览
|
||||
## 📸 Preview
|
||||
|
||||
### 1. 话题管理与个人信箱
|
||||
支持通过 **Topic (主题)** 订阅模式分发告警,同时也提供 **Personal Inbox (个人信箱)** 功能,无需创建话题即可快速给自己推送消息。
|
||||
### 1. Topic Management & Personal Inbox
|
||||
Supports alert distribution through the **Topic** subscription model, and also provides a **Personal Inbox** feature for quick self-notifications without creating a topic.
|
||||

|
||||
|
||||
除了个人订阅外,您可以将 Topic 绑定至多个**飞书群聊**。
|
||||
> [!TIP]
|
||||
> **群聊发现**:请先将机器人邀请进入目标群聊。机器人入群后会触发自动感应,此时刷新管理页面即可在下拉菜单中看到并绑定该群组。
|
||||
|
||||
### 2. 群聊告警分发
|
||||
支持将机器人加入飞书群聊,并将话题绑定到群聊中,实现告警的群组广播。
|
||||
### 2. Group Chat Alert Dispatch
|
||||
Supports adding the bot to Feishu group chats and binding topics to these groups for group-wide broadcasting.
|
||||

|
||||

|
||||
|
||||
### 3. 管理员看板 (Live Stats)
|
||||
实时追踪全系统的告警负载、分发成功率以及各话题的热度。
|
||||
### 3. Admin Dashboard (Live Stats)
|
||||
Real-time tracking of system alert load, dispatch success rates, and topic popularity.
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 🔥 核心特性
|
||||
## 🔥 Key Features
|
||||
|
||||
- **🚀 极简推送 (Personal Inbox)**: 每个用户拥有专属的 Webhook Token,直接向 `/dm` 接口发送即可在飞书收到私聊,零配置成本。
|
||||
- **📢 主题订阅 (Topic Model)**: 灵活的“发布-订阅”机制。告警发送至 Topic,系统自动分发给所有订阅成员。
|
||||
- **👥 群聊分发 (Group Support)**: 告警可同步分发至绑定的飞书群聊,支持机器人自动发现与解绑。
|
||||
- **🛡️ 权限与审计**:
|
||||
- 话题创建需经过管理员审批。
|
||||
- 记录完整的 `Alert Task` 日志,实现发送链路可追溯。
|
||||
- **📊 实时看板**: Grafana 风格的监控界面,直观展示系统运行健壮性。
|
||||
- **🔌 长连接模式 (WebSocket)**: 支持飞书开放平台长连接,无需公网 IP 或域名即可在内网环境接收事件回调。
|
||||
- **⚡ 高性能架构**: 基于 Bun + Hono 的全异步架构,毫秒级分发延迟。
|
||||
- **🚀 Zero-Config Personal Inbox**: Every user has a unique Webhook Token. Send directly to the `/dm` interface to receive messages on Feishu with zero configuration.
|
||||
- **📢 Topic Subscription Model**: Flexible "Publish-Subscribe" mechanism. Alerts sent to a Topic are automatically distributed to all subscribers.
|
||||
- **👥 Group Chat Distribution**: Alerts can be simultaneously dispatched to bound Feishu group chats, supporting automatic discovery and unbinding.
|
||||
- **🛡️ Permissions & Auditing**:
|
||||
- Topic creation requires admin approval.
|
||||
- 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.
|
||||
- **⚡ High Performance**: Built on Bun + Hono for millisecond-level dispatch latency.
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 技术栈
|
||||
## 🛠️ Tech Stack
|
||||
|
||||
- **Runtime**: [Bun](https://bun.sh/) (高性能 TS 运行时)
|
||||
- **Runtime**: [Bun](https://bun.sh/) (High-performance TS runtime)
|
||||
- **Backend**: [Hono](https://hono.dev/) (Web Standards Based)
|
||||
- **Frontend**: [React](https://react.dev/) + [Vite](https://vitejs.dev/) + [Tailwind CSS](https://tailwindcss.com/)
|
||||
- **Database**: [PostgreSQL](https://www.postgresql.org/) + [Drizzle ORM](https://orm.drizzle.team/)
|
||||
@@ -52,84 +50,53 @@
|
||||
|
||||
---
|
||||
|
||||
## 🚀 快速开始
|
||||
## 🚀 Quick Start
|
||||
|
||||
### 1. 飞书应用配置
|
||||
1. 登录 [飞书开放平台](https://open.feishu.cn/) 创建一个 **企业自建应用**。
|
||||
2. 在“应用能力”中开启 **机器人**。
|
||||
3. 在“权限管理”中申请 `im:message:send_as_bot` (以应用身份发送消息)。
|
||||
4. 获取 `App ID` 和 `App Secret`。
|
||||
### 1. Feishu App Configuration
|
||||
1. Login to the [Feishu Open Platform](https://open.feishu.cn/) and create an **Enterprise Custom App**.
|
||||
2. Enable **Bot** capability in "App Capabilities".
|
||||
3. Apply for `im:message:send_as_bot` permission in "Permission Management".
|
||||
4. Get your `App ID` and `App Secret`.
|
||||
|
||||
### 2. 部署运行
|
||||
### 2. Deployment
|
||||
```bash
|
||||
# 安装依赖
|
||||
# Install dependencies
|
||||
bun install
|
||||
|
||||
# 配置环境变量 (apps/server/.env)
|
||||
# Configure environment variables (apps/server/.env)
|
||||
DATABASE_URL="postgresql://user:pass@localhost:5432/db"
|
||||
FEISHU_APP_ID="cli_xxx"
|
||||
FEISHU_APP_SECRET="xxx"
|
||||
ADMIN_EMAILS="user1@example.com,user2@example.com" # 管理员列表
|
||||
ADMIN_EMAILS="user1@example.com,user2@example.com"
|
||||
|
||||
# 数据库推送/迁移
|
||||
# Database migration
|
||||
cd apps/server && bun run db:migrate:deploy
|
||||
|
||||
# 启动开发环境
|
||||
# Start development
|
||||
bun run dev
|
||||
```
|
||||
|
||||
### 3. Docker 部署
|
||||
项目支持使用 Docker Compose 快速部署,且**数据库会自动进行初始化与迁移**:
|
||||
|
||||
### 3. Docker Deployment
|
||||
```bash
|
||||
# 复制并填写环境变量
|
||||
cp apps/server/.env.example .env
|
||||
|
||||
# 启动所有服务 (Postgres + Server + Web)
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ CI/CD
|
||||
Automatically builds and pushes Docker images to GitHub Container Registry (GHCR) on every push to `main`.
|
||||
|
||||
项目通过 GitHub Actions 实现了自动化流水线:
|
||||
## 📜 Changelog
|
||||
See [CHANGELOG.md](CHANGELOG.md) for version history.
|
||||
|
||||
- **自动化构建**: 每次推送至 `main` 分支或提交 Pull Request 时,会自动触发 Docker 镜像构建。
|
||||
- **镜像仓库**: 构建生成的镜像会同步推送到 GitHub Container Registry (GHCR)。
|
||||
- **镜像路径**: `ghcr.io/${USER}/alert-message-center` (包含前后端的统一镜像)
|
||||
## 📡 Webhook Usage
|
||||
- **Personal Inbox**: `POST /api/webhook/:your_token/dm`
|
||||
- **Topic**: `POST /api/webhook/:your_token/topic/:topic_slug`
|
||||
|
||||
---
|
||||
|
||||
## 📜 更新日志
|
||||
|
||||
所有版本的详细变更记录请查看 [CHANGELOG.md](CHANGELOG.md)。
|
||||
|
||||
---
|
||||
|
||||
## 📡 Webhook 使用指南
|
||||
|
||||
### 1. 发送给个人 (Personal Inbox)
|
||||
**URL**: `POST /api/webhook/:your_token/dm`
|
||||
**Body**:
|
||||
```json
|
||||
{
|
||||
"msg_type": "text",
|
||||
"content": { "text": "这是一条私有告警" }
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 发送到主题 (Topic)
|
||||
**URL**: `POST /api/webhook/:your_token/topic/:topic_slug`
|
||||
**Body**: 同上。系统会自动根据该 Topic 的订阅列表进行广播。
|
||||
|
||||
---
|
||||
|
||||
## 📂 项目结构
|
||||
|
||||
- `apps/server`: 核心 API 服务,处理 OAuth、Webhook 解析与飞书分发。
|
||||
- `apps/web`: 响应式管理后台。
|
||||
- `docs/copilot-context.md`: 为 AI 辅助编程提供的深层架构背景。
|
||||
## 📂 Project Structure
|
||||
- `apps/server`: Core API service
|
||||
- `apps/web`: Responsive management dashboard
|
||||
|
||||
---
|
||||
*Created with ❤️ by the Alert Message Center Team.*
|
||||
|
||||
119
README.zh-CN.md
Normal file
119
README.zh-CN.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# Alert Message Center
|
||||
|
||||
[English](./README.md) | **简体中文**
|
||||
|
||||
[](https://bun.sh)
|
||||
[](https://www.postgresql.org/)
|
||||
|
||||
**Alert Message Center** 是一个现代化、企业级的告警路由与分发中心。它旨在将纷繁复杂的告警源(Prometheus, Grafana, 自建脚本等)与最终接收人解耦,通过 **飞书机器人私聊** 实现告警的精准触达。
|
||||
|
||||
---
|
||||
|
||||
## 📸 界面预览
|
||||
|
||||
### 1. 话题管理与个人信箱
|
||||
支持通过 **Topic (主题)** 订阅模式分发告警,同时也提供 **Personal Inbox (个人信箱)** 功能,无需创建话题即可快速给自己推送消息。
|
||||

|
||||
|
||||
### 2. 群聊告警分发
|
||||
支持将机器人加入飞书群聊,并将话题绑定到群聊中,实现告警的群组广播。
|
||||

|
||||

|
||||
|
||||
### 3. 管理员看板 (Live Stats)
|
||||
实时追踪全系统的告警负载、分发成功率以及各话题的热度。
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 🔥 核心特性
|
||||
|
||||
- **🚀 极简推送 (Personal Inbox)**: 每个用户拥有专属的 Webhook Token,直接向 `/dm` 接口发送即可在飞书收到私聊,零配置成本。
|
||||
- **📢 主题订阅 (Topic Model)**: 灵活的“发布-订阅”机制。告警发送至 Topic,系统自动分发给所有订阅成员。
|
||||
- **👥 群聊分发 (Group Support)**: 告警可同步分发至绑定的飞书群聊,支持机器人自动发现与解绑。
|
||||
- **🛡️ 权限与审计**:
|
||||
- 话题创建需经过管理员审批。
|
||||
- 记录完整的 `Alert Task` 日志,实现发送链路可追溯。
|
||||
- **📊 实时看板**: Grafana 风格的监控界面,直观展示系统运行健壮性。
|
||||
- **🔌 长连接模式 (WebSocket)**: 支持飞书开放平台长连接,无需公网 IP 或域名即可在内网环境接收事件回调。
|
||||
- **⚡ 高性能架构**: 基于 Bun + Hono 的全异步架构,毫秒级分发延迟。
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 技术栈
|
||||
|
||||
- **Runtime**: [Bun](https://bun.sh/) (高性能 TS 运行时)
|
||||
- **Backend**: [Hono](https://hono.dev/) (Web Standards Based)
|
||||
- **Frontend**: [React](https://react.dev/) + [Vite](https://vitejs.dev/) + [Tailwind CSS](https://tailwindcss.com/)
|
||||
- **Database**: [PostgreSQL](https://www.postgresql.org/) + [Drizzle ORM](https://orm.drizzle.team/)
|
||||
- **Messaging**: [Feishu (Lark) Open Platform](https://open.feishu.cn/)
|
||||
|
||||
---
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 1. 飞书应用配置
|
||||
1. 登录 [飞书开放平台](https://open.feishu.cn/) 创建一个 **企业自建应用**。
|
||||
2. 在“应用能力”中开启 **机器人**。
|
||||
3. 在“权限管理”中申请 `im:message:send_as_bot` (以应用身份发送消息)。
|
||||
4. 获取 `App ID` 和 `App Secret`。
|
||||
|
||||
### 2. 部署运行
|
||||
```bash
|
||||
# 安装依赖
|
||||
bun install
|
||||
|
||||
# 配置环境变量 (apps/server/.env)
|
||||
DATABASE_URL="postgresql://user:pass@localhost:5432/db"
|
||||
FEISHU_APP_ID="cli_xxx"
|
||||
FEISHU_APP_SECRET="xxx"
|
||||
ADMIN_EMAILS="user1@example.com,user2@example.com" # 管理员列表
|
||||
|
||||
# 数据库推送/迁移
|
||||
cd apps/server && bun run db:migrate:deploy
|
||||
|
||||
# 启动开发环境
|
||||
bun run dev
|
||||
```
|
||||
|
||||
### 3. Docker 部署
|
||||
项目支持使用 Docker Compose 快速部署,且**数据库会自动进行初始化与迁移**:
|
||||
|
||||
```bash
|
||||
# 复制并填写环境变量
|
||||
cp apps/server/.env.example .env
|
||||
|
||||
# 启动所有服务 (Postgres + Server + Web)
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ CI/CD
|
||||
|
||||
项目通过 GitHub Actions 实现了自动化流水线。
|
||||
|
||||
## 📜 更新日志
|
||||
|
||||
所有版本的详细变更记录请查看 [CHANGELOG.md](CHANGELOG.md)。
|
||||
|
||||
---
|
||||
|
||||
## 📡 Webhook 使用指南
|
||||
|
||||
### 1. 发送给个人 (Personal Inbox)
|
||||
**URL**: `POST /api/webhook/:your_token/dm`
|
||||
|
||||
### 2. 发送到主题 (Topic)
|
||||
**URL**: `POST /api/webhook/:your_token/topic/:topic_slug`
|
||||
|
||||
---
|
||||
|
||||
## 📂 项目结构
|
||||
|
||||
- `apps/server`: 核心 API 服务,处理 OAuth、Webhook 解析与飞书分发。
|
||||
- `apps/web`: 响应式管理后台。
|
||||
- `docs/copilot-context.md`: 为 AI 辅助编程提供的深层架构背景。
|
||||
|
||||
---
|
||||
*Created with ❤️ by the Alert Message Center Team.*
|
||||
@@ -11,8 +11,8 @@ import {
|
||||
topics,
|
||||
users,
|
||||
} from "./db/schema";
|
||||
import { type AuthSession, requireAdmin, requireAuth } from "./middleware";
|
||||
import { notifyAdminsOfNewTopic } from "./lib/admin-notifier";
|
||||
import { type AuthSession, requireAdmin, requireAuth } from "./middleware";
|
||||
|
||||
const api = new Hono<{ Variables: { session: AuthSession } }>();
|
||||
|
||||
@@ -273,10 +273,9 @@ api.get("/groups", requireAuth, async (c) => {
|
||||
const query = c.req.query("q")?.trim();
|
||||
const limit = Math.min(Number(c.req.query("limit") || 100), 200);
|
||||
|
||||
let whereClause = undefined;
|
||||
if (query) {
|
||||
whereClause = sql`${knownGroupChats.name} ilike ${`%${query}%`}`;
|
||||
}
|
||||
const whereClause = query
|
||||
? sql`${knownGroupChats.name} ilike ${`%${query}%`}`
|
||||
: undefined;
|
||||
|
||||
// Return recent active groups
|
||||
const groups = await db
|
||||
@@ -319,10 +318,14 @@ api.post(
|
||||
}
|
||||
|
||||
if (topic.createdBy !== session.id && !session.isAdmin) {
|
||||
return c.json({ error: "Only topic owner or admin can bind groups" }, 403);
|
||||
return c.json(
|
||||
{ error: "Only topic owner or admin can bind groups" },
|
||||
403,
|
||||
);
|
||||
}
|
||||
|
||||
const status = session.isAdmin || session.isTrusted ? "approved" : "pending";
|
||||
const status =
|
||||
session.isAdmin || session.isTrusted ? "approved" : "pending";
|
||||
|
||||
const result = await db
|
||||
.insert(topicGroupChats)
|
||||
@@ -345,7 +348,7 @@ api.post(
|
||||
// Metadata passed to notifier for better context
|
||||
isGroupBinding: true,
|
||||
groupName: body.name,
|
||||
} as any);
|
||||
});
|
||||
}
|
||||
|
||||
return c.json(result[0]);
|
||||
|
||||
@@ -1,85 +1,85 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import { db } from "../db";
|
||||
import { users } from "../db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { feishuClient } from "../feishu";
|
||||
import { logger } from "./logger";
|
||||
|
||||
export async function notifyAdminsOfNewTopic(topic: {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
createdBy: string | null;
|
||||
isGroupBinding?: boolean;
|
||||
groupName?: string;
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
createdBy: string | null;
|
||||
isGroupBinding?: boolean;
|
||||
groupName?: string;
|
||||
}) {
|
||||
try {
|
||||
// 1. Get all admins
|
||||
const admins = await db.query.users.findMany({
|
||||
where: eq(users.isAdmin, true),
|
||||
});
|
||||
try {
|
||||
// 1. Get all admins
|
||||
const admins = await db.query.users.findMany({
|
||||
where: eq(users.isAdmin, true),
|
||||
});
|
||||
|
||||
if (admins.length === 0) {
|
||||
logger.warn("No admins found to notify");
|
||||
return;
|
||||
}
|
||||
if (admins.length === 0) {
|
||||
logger.warn("No admins found to notify");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Get creator name
|
||||
let creatorName = "Unknown";
|
||||
if (topic.createdBy) {
|
||||
const creator = await db.query.users.findFirst({
|
||||
where: eq(users.id, topic.createdBy),
|
||||
});
|
||||
if (creator) creatorName = creator.name;
|
||||
}
|
||||
// 2. Get creator name
|
||||
let creatorName = "Unknown";
|
||||
if (topic.createdBy) {
|
||||
const creator = await db.query.users.findFirst({
|
||||
where: eq(users.id, topic.createdBy),
|
||||
});
|
||||
if (creator) creatorName = creator.name;
|
||||
}
|
||||
|
||||
// 3. Prepare message content
|
||||
const title = topic.isGroupBinding
|
||||
? "🔗 新的群聊绑定申请"
|
||||
: "🆕 新的 Topic 申请";
|
||||
const detailContent = topic.isGroupBinding
|
||||
? `**Topic:** ${topic.name}\n**群聊:** ${topic.groupName}\n**创建者:** ${creatorName}`
|
||||
: `**名称:** ${topic.name}\n**Slug:** ${topic.slug}\n**创建者:** ${creatorName}`;
|
||||
// 3. Prepare message content
|
||||
const title = topic.isGroupBinding
|
||||
? "🔗 新的群聊绑定申请"
|
||||
: "🆕 新的 Topic 申请";
|
||||
const detailContent = topic.isGroupBinding
|
||||
? `**Topic:** ${topic.name}\n**群聊:** ${topic.groupName}\n**创建者:** ${creatorName}`
|
||||
: `**名称:** ${topic.name}\n**Slug:** ${topic.slug}\n**创建者:** ${creatorName}`;
|
||||
|
||||
const content = {
|
||||
config: { wide_screen_mode: true },
|
||||
header: {
|
||||
template: topic.isGroupBinding ? "blue" : "orange",
|
||||
title: { content: title, tag: "plain_text" },
|
||||
},
|
||||
elements: [
|
||||
{
|
||||
tag: "div",
|
||||
text: {
|
||||
content: detailContent,
|
||||
tag: "lark_md",
|
||||
},
|
||||
},
|
||||
{
|
||||
tag: "action",
|
||||
actions: [
|
||||
{
|
||||
tag: "button",
|
||||
text: { content: "前往审批", tag: "plain_text" },
|
||||
type: "primary",
|
||||
url: `${process.env.FRONTEND_URL || "http://localhost:5173"}/admin/topics`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const content = {
|
||||
config: { wide_screen_mode: true },
|
||||
header: {
|
||||
template: topic.isGroupBinding ? "blue" : "orange",
|
||||
title: { content: title, tag: "plain_text" },
|
||||
},
|
||||
elements: [
|
||||
{
|
||||
tag: "div",
|
||||
text: {
|
||||
content: detailContent,
|
||||
tag: "lark_md",
|
||||
},
|
||||
},
|
||||
{
|
||||
tag: "action",
|
||||
actions: [
|
||||
{
|
||||
tag: "button",
|
||||
text: { content: "前往审批", tag: "plain_text" },
|
||||
type: "primary",
|
||||
url: `${process.env.FRONTEND_URL || "http://localhost:5173"}/admin/topics`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// 4. Send notification to each admin
|
||||
for (const admin of admins) {
|
||||
if (admin.feishuUserId) {
|
||||
await feishuClient.sendMessage(
|
||||
admin.feishuUserId,
|
||||
"open_id",
|
||||
"interactive",
|
||||
content,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error({ err: error, topicId: topic.id }, "Failed to notify admins");
|
||||
}
|
||||
// 4. Send notification to each admin
|
||||
for (const admin of admins) {
|
||||
if (admin.feishuUserId) {
|
||||
await feishuClient.sendMessage(
|
||||
admin.feishuUserId,
|
||||
"open_id",
|
||||
"interactive",
|
||||
content,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error({ err: error, topicId: topic.id }, "Failed to notify admins");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,12 +238,13 @@ export default function GroupBindingsModal({
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
className={`ml-3 inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-bold tracking-wider uppercase ${binding.status === "approved"
|
||||
className={`ml-3 inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-bold tracking-wider uppercase ${
|
||||
binding.status === "approved"
|
||||
? "bg-green-100 text-green-700"
|
||||
: binding.status === "rejected"
|
||||
? "bg-red-100 text-red-700"
|
||||
: "bg-amber-100 text-amber-700"
|
||||
}`}
|
||||
}`}
|
||||
>
|
||||
{binding.status}
|
||||
</span>
|
||||
@@ -268,8 +269,8 @@ export default function GroupBindingsModal({
|
||||
Add Group Binding
|
||||
</h4>
|
||||
<p className="text-xs text-gray-500 mb-4 leading-relaxed">
|
||||
Search and select a group where the <strong>Alert Messenger</strong> bot
|
||||
has been added.
|
||||
Search and select a group where the <strong>Alert Messenger</strong>{" "}
|
||||
bot has been added.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
@@ -288,7 +289,9 @@ export default function GroupBindingsModal({
|
||||
placeholder="Search for a group name..."
|
||||
value={searchQuery}
|
||||
onChange={handleSearchChange}
|
||||
onFocus={() => knownGroups.length > 0 && setShowDropdown(true)}
|
||||
onFocus={() =>
|
||||
knownGroups.length > 0 && setShowDropdown(true)
|
||||
}
|
||||
disabled={loading}
|
||||
/>
|
||||
{searchQuery && (
|
||||
@@ -364,10 +367,11 @@ export default function GroupBindingsModal({
|
||||
|
||||
{status && (
|
||||
<div
|
||||
className={`mt-4 p-3 rounded-lg flex items-start gap-2 ${status.type === "success"
|
||||
className={`mt-4 p-3 rounded-lg flex items-start gap-2 ${
|
||||
status.type === "success"
|
||||
? "bg-green-50 text-green-700 border border-green-100"
|
||||
: "bg-red-50 text-red-700 border border-red-100"
|
||||
}`}
|
||||
}`}
|
||||
>
|
||||
<div className="text-sm font-medium">{status.message}</div>
|
||||
</div>
|
||||
|
||||
@@ -35,40 +35,44 @@ export default function AdminView() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveTab("load")}
|
||||
className={`${activeTab === "load"
|
||||
className={`${
|
||||
activeTab === "load"
|
||||
? "border-indigo-500 text-indigo-600"
|
||||
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
|
||||
} whitespace-nowrap pb-4 px-1 border-b-2 font-medium text-sm`}
|
||||
} whitespace-nowrap pb-4 px-1 border-b-2 font-medium text-sm`}
|
||||
>
|
||||
System Load
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveTab("requests")}
|
||||
className={`${activeTab === "requests"
|
||||
className={`${
|
||||
activeTab === "requests"
|
||||
? "border-indigo-500 text-indigo-600"
|
||||
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
|
||||
} whitespace-nowrap pb-4 px-1 border-b-2 font-medium text-sm`}
|
||||
} whitespace-nowrap pb-4 px-1 border-b-2 font-medium text-sm`}
|
||||
>
|
||||
Topic Requests
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveTab("group-requests")}
|
||||
className={`${activeTab === "group-requests"
|
||||
className={`${
|
||||
activeTab === "group-requests"
|
||||
? "border-indigo-500 text-indigo-600"
|
||||
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
|
||||
} whitespace-nowrap pb-4 px-1 border-b-2 font-medium text-sm`}
|
||||
} whitespace-nowrap pb-4 px-1 border-b-2 font-medium text-sm`}
|
||||
>
|
||||
Group Bindings
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveTab("topics")}
|
||||
className={`${activeTab === "topics"
|
||||
className={`${
|
||||
activeTab === "topics"
|
||||
? "border-indigo-500 text-indigo-600"
|
||||
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
|
||||
} whitespace-nowrap pb-4 px-1 border-b-2 font-medium text-sm`}
|
||||
} whitespace-nowrap pb-4 px-1 border-b-2 font-medium text-sm`}
|
||||
>
|
||||
All Topics
|
||||
</button>
|
||||
@@ -102,7 +106,7 @@ function GroupRequestsList() {
|
||||
const fetchRequests = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// @ts-ignore - groups requests might not be in the generated client yet
|
||||
// @ts-expect-error - groups requests might not be in the generated client yet
|
||||
const res = await client.api.topics.groups.requests.$get(undefined, {
|
||||
init: { credentials: "include" },
|
||||
});
|
||||
@@ -133,7 +137,7 @@ function GroupRequestsList() {
|
||||
action: "approve" | "reject",
|
||||
) => {
|
||||
try {
|
||||
// @ts-ignore
|
||||
// @ts-expect-error
|
||||
await client.api.topics[":id"].groups[":bindingId"][action].$post(
|
||||
{ param: { id: req.topicId, bindingId: req.id } },
|
||||
{ init: { credentials: "include" } },
|
||||
@@ -164,15 +168,13 @@ function GroupRequestsList() {
|
||||
Group: <span className="text-indigo-600">{req.name}</span>
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
Topic: <span className="font-semibold">{req.topic?.name}</span> (
|
||||
{req.topic?.slug})
|
||||
Topic: <span className="font-semibold">{req.topic?.name}</span>{" "}
|
||||
({req.topic?.slug})
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
Requested by: {req.creator?.name || "Unknown"}
|
||||
</p>
|
||||
<p className="text-xs text-gray-400 mt-1">
|
||||
ID: {req.chatId}
|
||||
</p>
|
||||
<p className="text-xs text-gray-400 mt-1">ID: {req.chatId}</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
@@ -291,12 +293,13 @@ function TopicsManagement() {
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span
|
||||
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${topic.status === "approved"
|
||||
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
||||
topic.status === "approved"
|
||||
? "bg-green-100 text-green-800"
|
||||
: topic.status === "rejected"
|
||||
? "bg-red-100 text-red-800"
|
||||
: "bg-yellow-100 text-yellow-800"
|
||||
}`}
|
||||
}`}
|
||||
>
|
||||
{topic.status}
|
||||
</span>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Project Context for GitHub Copilot (v1.3.1)
|
||||
# Project Context for GitHub Copilot (v1.3.2)
|
||||
|
||||
This document provides technical context, architectural decisions, and code conventions for the **Alert Message Center** project. It is intended to help AI assistants understand the codebase.
|
||||
|
||||
@@ -134,6 +134,7 @@ The database schema is defined in `apps/server/src/db/schema.ts`.
|
||||
- When the bot is removed, the cached group is deleted from `known_group_chats`.
|
||||
- **Auto-Unbind**: All bindings in `topic_group_chats` for that `chat_id` are automatically deleted to ensure data consistency.
|
||||
- **Binding**: Users/Admins bind a Topic to a known Feishu Group in the UI.
|
||||
- **Search**: The binding UI supports real-time, server-side debounced search by group name.
|
||||
- **Security**: Only the Topic Creator or an Admin can bind/unbind groups to a Topic.
|
||||
- **Approval**:
|
||||
- Normal users: Binding status is `pending` upon creation. Admins receive notification.
|
||||
@@ -180,7 +181,7 @@ The database schema is defined in `apps/server/src/db/schema.ts`.
|
||||
- `GET /api/users`: List users (Admin only).
|
||||
|
||||
### Feishu Group Management
|
||||
- `GET /api/groups`: List known groups (cached from bot events).
|
||||
- `GET /api/groups`: List known groups (cached from bot events). Supports `q` for search and `limit` parameters.
|
||||
- `GET /api/topics/:id/groups`: List group bindings for a topic.
|
||||
- `POST /api/topics/:id/groups`: Bind a group to a topic.
|
||||
- `DELETE /api/topics/:id/groups/:bindingId`: Unbind a group.
|
||||
@@ -206,13 +207,15 @@ The database schema is defined in `apps/server/src/db/schema.ts`.
|
||||
- **Imports**: Use relative imports.
|
||||
- **Styling**: Use Tailwind utility classes directly in JSX.
|
||||
- **Async/Await**: Prefer `async/await` over `.then()`.
|
||||
- **Type Safety**: strict TypeScript usage. Backend and Frontend share types via Hono RPC or shared interfaces. **Elimination of `any`** is a priority; use explicit interfaces (e.g., `WebhookBody`, `UserAccessTokenData`) for all externally sourced data.
|
||||
- **Linter & Formatter**:
|
||||
- Framework: [Biome](https://biomejs.dev/).
|
||||
- **Rules**: Strict configuration for `a11y`, `suspicious`, `style`, and `correctness`.
|
||||
- **Tailwind**: `noUnknownAtRules` is configured to ignore Tailwind directives (`@tailwind`, `@apply`, etc.).
|
||||
- **Enforcement**: CI/CD runs `biome check` to ensure compliance. **AI assistants MUST run `bun x biome check --write .` (or equivalent) in the respective app directory after every code modification to verify and fix lint/formatting issues before finalizing.** Avoid Use of `as any` is strictly prohibited except for specialized cases like `import.meta as any` (for Vite env) or very complex JSON spread operations. In those rare cases, use `// biome-ignore` with a clear explanation.
|
||||
- **Vite Env Access**: When accessing Vite environment variables via `import.meta.env` (or casting `import.meta as any`), **always use optional chaining** (e.g., `meta.env?.VITE_...`). This prevents crashes if the environment is not initialized or if the code runs in a non-browser context during pre-rendering/testing.
|
||||
- **Strict Type Safety & `any` Prohibition**:
|
||||
> [!IMPORTANT]
|
||||
> **The usage of `any` is strictly prohibited.** This has been a recurring issue and must be avoided at all costs.
|
||||
- **Explicit Interfaces**: Always define clear interfaces or types for API responses, webhook payloads, and complex objects.
|
||||
- **Type Inference**: Leverage TypeScript's type inference. If a variable is initialized later, provide an explicit type during declaration (e.g., `let whereClause: SQL | undefined;`) instead of leaving it implicit.
|
||||
- **Hono RPC**: Utilize the type-safe client (`client.api...`) to ensure end-to-end type safety between backend and frontend.
|
||||
- **No Type Casting**: Avoid `as any` or `<any>` casts. Use type guards (`if`, `switch`, `instanceof`) or Zod schema validation to narrow types safely.
|
||||
- **AI Responsibility**: AI assistants MUST ensure every new or modified piece of code passes strict TypeScript and Biome checks. If a type is unknown, research the schema rather than defaulting to `any`.
|
||||
- **Vite Env Access**: When accessing Vite environment variables via `import.meta.env` (or casting `import.meta as any`), **always use optional chaining** (e.g., `meta.env?.VITE_...`). This prevents crashes if the environment is not initialized or if the code runs in a non-browser context during pre-rendering/testing.
|
||||
- **Frontend Resilience**:
|
||||
- Always check `res.ok` before attempting to parse or use API responses.
|
||||
- Use `Array.isArray()` to verify that data expected to be a list actually is one, especially when mapping over it in JSX. This prevents "white page" crashes when the backend returns error objects instead of arrays.
|
||||
@@ -240,6 +243,7 @@ The database schema is defined in `apps/server/src/db/schema.ts`.
|
||||
|
||||
## 8. Core Documents
|
||||
|
||||
- **[README.md](file:///Users/lilithgames/Workspace/play/alert-message-center/README.md)**: Main project documentation, including quick start, tech stack overview, and Webhook usage guide.
|
||||
- **[CHANGELOG.md](file:///Users/lilithgames/Workspace/play/alert-message-center/CHANGELOG.md)**: Record of version changes, following the Keep a Changelog specification.
|
||||
- **[todo.md](file:///Users/lilithgames/Workspace/play/alert-message-center/todo.md)**: Task tracking and upcoming features.
|
||||
- **[README.md](file:///Users/lilithgames/Workspace/play/alert-message-center/README.md)**: Main project documentation (English version).
|
||||
- **[README.zh-CN.md](file:///Users/lilithgames/Workspace/play/alert-message-center/README.zh-CN.md)**: Simplified Chinese version of the documentation.
|
||||
- **[CHANGELOG.md](file:///Users/lilithgames/Workspace/play/alert-message-center/CHANGELOG.md)**: Record of version changes.
|
||||
- **[todo.md](file:///Users/lilithgames/Workspace/play/alert-message-center/todo.md)**: Task tracking.
|
||||
|
||||
2
todo.md
2
todo.md
@@ -35,4 +35,6 @@
|
||||
- [x] **Migration Robustness**: Fixed migration failures in Docker by un-ignoring the drizzle meta directory.
|
||||
- [x] **Scalability & Security**: Implemented Trusted User system, ownership-based group binding, and Admin notification for topic requests.
|
||||
- [x] **User Management UI**: Added "Admin" badges and a "Trusted" toggle in the User Management view.
|
||||
- [x] **Searchable Group Binding**: Implemented server-side search and searchable dropdown for smoother group chat management.
|
||||
- [x] **Bilingual Documentation**: Split README into English and Chinese versions for international outreach.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user