-
Notifications
You must be signed in to change notification settings - Fork 0
feat(audit): audit log for admin command operations #21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,243 @@ | ||
| # 审计日志 (Audit Log) 技术设计 | ||
|
|
||
| ## 1. 架构概览 | ||
|
|
||
| ``` | ||
| commands.ts --(成功后)--> AuditService.log() | ||
| | | ||
| v | ||
| audit_logs 表 | ||
| ^ | ||
| | | ||
| web-console.ts <-- listAuditLogs() -- AuditService.list() | ||
| main.ts --------> listAuditLogs 回调 -----> /operations/audit | ||
| ``` | ||
|
|
||
| ## 2. 数据库变更 | ||
|
|
||
| ### 2.1 audit_logs 表 | ||
|
|
||
| ```sql | ||
| CREATE TABLE IF NOT EXISTS audit_logs ( | ||
| id INTEGER PRIMARY KEY AUTOINCREMENT, | ||
| admin_id TEXT NOT NULL, | ||
| conversation_id INTEGER NOT NULL REFERENCES conversations(id), | ||
| action TEXT NOT NULL, | ||
| detail TEXT, | ||
| created_at TEXT NOT NULL | ||
| ); | ||
| CREATE INDEX IF NOT EXISTS audit_logs_conversation_idx ON audit_logs(conversation_id, created_at DESC); | ||
| CREATE INDEX IF NOT EXISTS audit_logs_admin_idx ON audit_logs(admin_id, created_at DESC); | ||
| ``` | ||
|
|
||
| 追加到 `0001_initial.ts` 的 `statements` 数组末尾(`app_settings` 之后)。无需 `addColumnIfMissing`,因为是全新表。 | ||
|
|
||
| ## 3. AuditService 设计 | ||
|
|
||
| ### 3.1 文件位置 | ||
| `src/domain/audit.ts` | ||
|
|
||
| ### 3.2 接口 | ||
|
|
||
| ```typescript | ||
| export interface AuditLogEntry { | ||
| id: number; | ||
| adminId: string; | ||
| conversationId: number; | ||
| action: string; | ||
| detail: string | null; | ||
| createdAt: string; | ||
| } | ||
|
|
||
| export interface AuditListOptions { | ||
| conversationId?: number; // 按会话筛选 | ||
| adminId?: string; // 按管理员筛选 | ||
| action?: string; // 按操作类型筛选 | ||
| limit: number; | ||
| offset: number; | ||
| } | ||
|
|
||
| export class AuditService { | ||
| constructor(private db: DatabaseSync) {} | ||
|
|
||
| log(entry: { | ||
| adminId: string; | ||
| conversationId: number; | ||
| action: string; | ||
| detail?: string; | ||
| }): void; | ||
|
|
||
| list(opts: AuditListOptions): { items: AuditLogEntry[]; total: number }; | ||
|
|
||
| listByConversation(conversationId: number, limit: number): AuditLogEntry[]; | ||
| } | ||
| ``` | ||
|
|
||
| ### 3.3 实现要点 | ||
|
|
||
| - `log()` 使用 `INSERT INTO audit_logs (admin_id, conversation_id, action, detail, created_at) VALUES (?, ?, ?, ?, ?)` | ||
| - `created_at` 使用 `new Date().toISOString()` | ||
| - `log()` 内部 try/catch,失败时只 console.error(不依赖 pino,因为 AuditService 在命令处理中调用,保持与 ConversationService 同级的纯 domain 服务) | ||
| - `list()` 动态拼接 WHERE 子句,按 `created_at DESC` 排序 | ||
| - `listByConversation()` 是 `list()` 的快捷方法,用于 `/audit` 命令 | ||
|
|
||
| ## 4. 命令埋点 | ||
|
|
||
| ### 4.1 CommandDeps 扩展 | ||
|
|
||
| ```typescript | ||
| export interface CommandDeps { | ||
| config: AppConfig; | ||
| conversations: ConversationService; | ||
| aiDrafts: AiDraftService; | ||
| deliveries: DeliveryService; | ||
| audit: AuditService; // 新增 | ||
| } | ||
| ``` | ||
|
|
||
| ### 4.2 埋点位置 | ||
|
|
||
| 在 `handleTopicCommand` 的各 case 分支中,命令执行成功后调用 `deps.audit.log()`。埋点清单: | ||
|
|
||
| | 命令 | action | detail | | ||
| |------|--------|--------| | ||
| | /ban | `ban` | 封禁原因(args 或 null) | | ||
| | /unban | `unban` | null | | ||
| | /assign | `assign` | 分配目标 user_id | | ||
| | /priority | `priority` | 新优先级值 | | ||
| | /close | `close` | null | | ||
| | /reopen | `reopen` | null | | ||
| | /mute | `mute` | 静音截止时间 | | ||
| | /delete confirm | `delete` | null | | ||
| | /reset confirm | `reset` | null | | ||
| | /expire | `expire` | 新策略(天数或 "never") | | ||
| | /ai_on | `ai_on` | null | | ||
| | /ai_off | `ai_off` | null | | ||
| | /draft send | `draft_send` | 草稿 ID | | ||
| | /draft discard | `draft_discard` | 草稿 ID | | ||
| | /tag | `tag` | 标签名 | | ||
| | /untag | `untag` | 标签名 | | ||
| | /note | `note` | null(备注内容不记入审计,保护隐私) | | ||
|
|
||
| ### 4.3 新增 /audit 命令 | ||
|
|
||
| 在 `handleTopicCommand` 中新增 case: | ||
|
|
||
| ```typescript | ||
| case "audit": { | ||
| const limit = parseLimit(args, 20, 50); | ||
| const logs = deps.audit.listByConversation(conversation.id, limit); | ||
| if (logs.length === 0) { | ||
| await ctx.reply("当前会话暂无审计记录。"); | ||
| return true; | ||
| } | ||
| const lines = logs.map(l => | ||
| `${l.createdAt} admin=${l.adminId} ${l.action}${l.detail ? ` (${l.detail})` : ""}` | ||
| ); | ||
| await ctx.reply([`最近 ${logs.length} 条审计记录:`, ...lines].join("\n")); | ||
| return true; | ||
| } | ||
| ``` | ||
|
|
||
| ### 4.4 菜单更新 | ||
|
|
||
| `topicHelpText()` 中 "安全与辅助" 区块新增: | ||
| ``` | ||
| /audit [数量] - 查看本会话最近审计记录,默认 20,最多 50 | ||
| ``` | ||
|
|
||
| ## 5. Web 控制台 | ||
|
|
||
| ### 5.1 路由 | ||
|
|
||
| - `GET /operations/audit` — 审计日志列表页 | ||
| - 查询参数:`page`(页码)、`admin_id`(管理员筛选)、`action`(操作类型筛选) | ||
|
|
||
| ### 5.2 WebConsoleOptions 扩展 | ||
|
|
||
| ```typescript | ||
| listAuditLogs: (opts: { | ||
| page: number; | ||
| adminId?: string; | ||
| action?: string; | ||
| pageSize: number; | ||
| }) => { items: AuditLogView[]; total: number }; | ||
| ``` | ||
|
|
||
| ### 5.3 AuditLogView 接口 | ||
|
|
||
| ```typescript | ||
| export interface AuditLogView { | ||
| id: number; | ||
| adminId: string; | ||
| conversationId: number; | ||
| action: string; | ||
| detail: string | null; | ||
| createdAt: string; | ||
| } | ||
| ``` | ||
|
|
||
| ### 5.4 渲染 | ||
|
|
||
| `renderAuditLogs(res, data, page, adminId, action)`: | ||
| - 顶部筛选表单:管理员 ID 输入框 + 操作类型下拉框 | ||
| - 表格列:ID、时间、管理员、会话 ID、操作、详情 | ||
| - 复用 `opsPage()`,activeNav 新增 `"audit"` | ||
|
|
||
| ### 5.5 导航 | ||
|
|
||
| `opsPage()` 的 header 新增 "审计" 链接: | ||
| ```html | ||
| <a href="/operations/audit" class="${activeNav === "audit" ? "active" : ""}">审计</a> | ||
| ``` | ||
|
|
||
| ## 6. main.ts 接入 | ||
|
|
||
| ### 6.1 import | ||
|
|
||
| ```typescript | ||
| import { AuditService } from "../domain/audit.js"; | ||
| ``` | ||
|
|
||
| ### 6.2 listAuditLogs 回调 | ||
|
|
||
| ```typescript | ||
| listAuditLogs: (opts) => { | ||
| const audit = new AuditService(handle.db); | ||
| const result = audit.list({ | ||
| adminId: opts.adminId || undefined, | ||
| action: opts.action || undefined, | ||
| limit: opts.pageSize, | ||
| offset: (opts.page - 1) * opts.pageSize, | ||
| }); | ||
| return { | ||
| items: result.items.map(a => ({ ...a })), | ||
| total: result.total, | ||
| }; | ||
| }, | ||
| ``` | ||
|
Comment on lines
+204
to
+218
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win 建议 Web 层复用 AuditService 单例 设计文档中 🤖 Prompt for AI Agents |
||
|
|
||
| ### 6.3 bot.ts 注入 | ||
|
|
||
| `createTelegramBot` 中创建 `AuditService` 实例并注入 `deps.audit`。 | ||
|
|
||
| ## 7. 测试计划 | ||
|
|
||
| 1. AuditService.log 写入并查询 | ||
| 2. AuditService.list 分页和筛选 | ||
| 3. AuditService.listByConversation | ||
| 4. /audit 命令返回审计记录 | ||
| 5. Web 审计页面认证重定向 | ||
| 6. Web 审计页面 HTML 渲染 | ||
|
|
||
| ## 8. 影响范围 | ||
|
|
||
| | 文件 | 变更类型 | | ||
| |------|----------| | ||
| | `src/storage/migrations/0001_initial.ts` | 新增 audit_logs 表和索引 | | ||
| | `src/domain/audit.ts` | 新建 | | ||
| | `src/channels/telegram/commands.ts` | CommandDeps 新增 audit、17 处埋点、新增 /audit 命令 | | ||
| | `src/channels/telegram/bot.ts` | createTelegramBot 注入 AuditService | | ||
| | `src/runtime/web-console.ts` | 新增 AuditLogView、listAuditLogs 回调、/operations/audit 路由、renderAuditLogs、导航 | | ||
| | `src/runtime/main.ts` | import AuditService、listAuditLogs 回调 | | ||
| | `test/core.test.ts` | 新增 AuditService 测试、/audit 命令测试、web 审计页面测试、所有 startWebConsole 调用加 stub | | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| # 审计日志 (Audit Log) 需求规格 | ||
|
|
||
| ## 1. 概述 | ||
|
|
||
| 持久化记录白名单管理员在 Topic 内执行的关键操作,便于多管理员协作场景下追溯谁在何时对哪个会话做了什么操作。 | ||
|
|
||
| ## 2. 背景 | ||
|
|
||
| 当前所有管理员命令(ban/assign/delete/reset/close/mute/priority 等)执行后仅回复一条 Telegram 消息,没有持久化记录。多管理员场景下无法回答"谁关闭了这个会话"、"谁重置了消息"、"谁封禁了这个联系人"等问题。 | ||
|
|
||
| ## 3. 范围 | ||
|
|
||
| ### 3.1 In Scope | ||
|
|
||
| - 新增 `audit_logs` 表,记录管理员操作 | ||
| - `AuditService`:写入、查询、统计 | ||
| - 在以下命令执行成功后埋点:ban/unban/assign/priority/close/reopen/mute/delete/reset/expire/ai_on/ai_off/draft send/draft discard/tag/untag/note | ||
| - `/audit [N]` 命令:在当前 Topic 内查看最近 N 条审计记录(默认 20,最多 50) | ||
| - Web 控制台 `/operations/audit` 页面:分页审计日志,按管理员 ID 和操作类型筛选 | ||
|
|
||
| ### 3.2 Out of Scope | ||
|
|
||
| - 审计日志的自动清理策略(后续随消息保留策略一起处理) | ||
| - 审计日志导出(后续随 export 功能扩展) | ||
| - 操作撤销/回滚 | ||
|
|
||
| ## 4. 需求 (EARS) | ||
|
|
||
| ### REQ-1: 审计日志持久化 | ||
| **WHEN** 管理员执行列入审计清单的命令 **AND** 命令执行成功时,**THE SYSTEM SHALL** 在 `audit_logs` 表中写入一条记录,包含管理员 ID、操作类型、目标会话 ID、操作详情和时间戳。 | ||
|
|
||
| ### REQ-2: 审计记录字段 | ||
| **THE audit_logs TABLE SHALL** 包含以下字段:id(自增主键)、admin_id(TEXT,执行操作的管理员 Telegram user_id)、conversation_id(INTEGER,目标会话 ID)、action(TEXT,操作类型枚举)、detail(TEXT,可空的附加信息如封禁原因/优先级值/分配目标)、created_at(TEXT,ISO 8601 时间戳)。 | ||
|
|
||
| ### REQ-3: /audit 命令 | ||
| **WHEN** 管理员在 Topic 内发送 `/audit` 或 `/audit <N>` **AND** 当前会话存在时,**THE SYSTEM SHALL** 返回该会话最近 N 条审计记录(默认 20,最多 50),按时间倒序排列,每条记录显示时间、管理员 ID、操作和详情。 | ||
|
|
||
| ### REQ-4: Web 审计页面认证 | ||
| **WHEN** 未认证用户访问 `/operations/audit` 时,**THE SYSTEM SHALL** 重定向到登录页,行为与 `/operations/overview` 一致。 | ||
|
|
||
| ### REQ-5: Web 审计页面查询 | ||
| **WHEN** 已认证管理员访问 `/operations/audit` 时,**THE SYSTEM SHALL** 展示分页审计日志(每页 50 条),支持按管理员 ID 和操作类型筛选,每条记录显示时间、管理员、会话 ID、操作、详情。 | ||
|
|
||
| ### REQ-6: 操作类型枚举 | ||
| **THE action field SHALL** 使用以下固定值:`ban`、`unban`、`assign`、`priority`、`close`、`reopen`、`mute`、`delete`、`reset`、`expire`、`ai_on`、`ai_off`、`draft_send`、`draft_discard`、`tag`、`untag`、`note`。 | ||
|
|
||
| ### REQ-7: 埋点不阻断主流程 | ||
| **WHEN** 审计日志写入失败时,**THE SYSTEM SHALL** 记录错误日志并继续执行,不阻断主命令流程,不向管理员暴露审计写入错误。 | ||
|
|
||
| ## 5. 非功能性需求 | ||
|
|
||
| - 性能:审计日志查询使用 `(conversation_id, created_at DESC)` 复合索引和 `(admin_id)` 索引 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value 索引描述与实际定义不一致 非功能性需求中写的 🤖 Prompt for AI Agents |
||
| - 审计日志写入使用同步 SQLite 操作,与命令在同一事务外独立写入 | ||
| - `/audit` 命令响应时间不超过 500ms(50 条记录内) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
审计写入失败时的错误处理与实现不一致
设计文档写明
log()失败时“只 console.error”,但src/domain/audit.ts的实际实现是空 catch(不记录任何信息)。这与 REQ-7“记录错误日志并继续执行”也存在偏差。请统一设计文档与实际实现,确保审计写入失败时至少输出错误日志以便排查。🤖 Prompt for AI Agents