feat: add english readme and fix lint

Signed-off-by: d0zingcat <iamtangli42@gmail.com>
This commit is contained in:
2026-01-17 14:44:59 +08:00
parent 2ec8a9e7f7
commit 2f3325ad7f
9 changed files with 306 additions and 193 deletions

View File

@@ -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
View File

@@ -1,50 +1,48 @@
# Alert Message Center
**README** | [简体中文](./README.zh-CN.md)
[![Tech Stack](https://img.shields.io/badge/Stack-Bun%20%7C%20Hono%20%7C%20React-blue)](https://bun.sh)
[![Database](https://img.shields.io/badge/Database-PostgreSQL-blue)](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.
![Topics View](docs/images/topics_view.png)
除了个人订阅外,您可以将 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.
![Group Binding](docs/images/group_binding.png)
![Group Alert](docs/images/group_alert.png)
### 3. 管理员看板 (Live Stats)
实时追踪全系统的告警负载、分发成功率以及各话题的热度。
### 3. Admin Dashboard (Live Stats)
Real-time tracking of system alert load, dispatch success rates, and topic popularity.
![Admin Dashboard](docs/images/admin_dashboard.png)
---
## 🔥 核心特性
## 🔥 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
View File

@@ -0,0 +1,119 @@
# Alert Message Center
[English](./README.md) | **简体中文**
[![Tech Stack](https://img.shields.io/badge/Stack-Bun%20%7C%20Hono%20%7C%20React-blue)](https://bun.sh)
[![Database](https://img.shields.io/badge/Database-PostgreSQL-blue)](https://www.postgresql.org/)
**Alert Message Center** 是一个现代化、企业级的告警路由与分发中心。它旨在将纷繁复杂的告警源Prometheus, Grafana, 自建脚本等)与最终接收人解耦,通过 **飞书机器人私聊** 实现告警的精准触达。
---
## 📸 界面预览
### 1. 话题管理与个人信箱
支持通过 **Topic (主题)** 订阅模式分发告警,同时也提供 **Personal Inbox (个人信箱)** 功能,无需创建话题即可快速给自己推送消息。
![Topics View](docs/images/topics_view.png)
### 2. 群聊告警分发
支持将机器人加入飞书群聊,并将话题绑定到群聊中,实现告警的群组广播。
![Group Binding](docs/images/group_binding.png)
![Group Alert](docs/images/group_alert.png)
### 3. 管理员看板 (Live Stats)
实时追踪全系统的告警负载、分发成功率以及各话题的热度。
![Admin Dashboard](docs/images/admin_dashboard.png)
---
## 🔥 核心特性
- **🚀 极简推送 (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.*

View File

@@ -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]);

View File

@@ -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");
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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.

View File

@@ -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.