# ASR 实时关键词事件监控服务技术方案 > 基于当前已确认需求整理 > 技术栈:Java 21、Spring Boot 4.1.0、Spring MVC、Spring Data JPA、AC 自动机、React + Vite + shadcn/ui --- ## 1. 项目目标 建设一个独立的 ASR 实时事件监控服务。 坐席前端每收到一条新的 ASR Final,就调用后端实时检测接口。后端根据当前已生效的关键词规则进行匹配,并在同一通话内首次发现新的业务事件时返回提醒。 首期主要解决: - 潜在转接方向识别 - 实时关键词检测 - 同一通话内去重提醒 - 规则配置页面 - 规则保存后立即生效 - Demo 页面验证当前生效规则 后续可扩展: - 高频事件匹配 - 风险事件提醒 - 服务规范提醒 - 正则匹配 - 语义相似匹配 - Dify 二次确认 - Redis 多实例会话状态 - SSE 实时推送 --- ## 2. 首期范围 ### 2.1 首期实现内容 - Java 21 - Spring Boot 4.1.0 - Spring MVC REST 接口 - 前端主动提交 ASR Final - 只处理 Final,不处理 Partial - 只匹配市民侧文本 - AC 自动机关键词匹配 - 最近五条市民 ASR Final 拼接 - 同一通话、同一事件只提醒一次 - 规则通过页面编辑 - 规则保存到数据库 - 保存成功后重建 AC 自动机并立即生效 - 提供单页面配置与测试 - JVM 内存保存通话状态 - 提供通话主动关闭接口 - TTL 自动清理过期会话 - 单实例部署 ### 2.2 首期不实现内容 - Redis - Kafka、RabbitMQ、RocketMQ - Elasticsearch - 向量数据库 - Embedding 语义匹配 - Dify 实时确认 - WebSocket 或 SSE - 通话状态查询接口 - 独立 `eventId` - 多实例部署 - 复杂草稿、审核和发布流程 --- ## 3. 最终确定的接口边界 ### 3.1 实时检测接口 实时检测业务接口: ```http POST /api/v1/asr-events ``` 前端每收到一条新的 ASR Final,就调用该接口。 请求不再传 `eventId`,使用: ```text callId + seq ``` 唯一标识一条 ASR Final。 ### 3.2 不提供通话状态查询接口 不提供: ```http GET /api/v1/calls/{callId}/state ``` 每次实时检测响应同时返回: - 本次新增提醒:`newAlerts` - 当前通话累计结果:`currentResults` ### 3.3 提供通话关闭接口 通话结束时,前端应主动关闭会话,立即释放内存中的通话状态: ```http POST /api/v1/calls/{callId}/close ``` 同时保留 TTL 定时清理,作为兜底机制,处理前端未调用 close、进程异常退出等遗漏场景。 约束条件: - 每通电话的 `callId` 必须唯一 - 正常结束路径优先调用 close - 未关闭或异常遗留的会话由 TTL 自动清理 ### 3.4 保留每通电话独立锁 Spring MVC 会并发处理请求,同一通话的两条 ASR Final 可能同时进入后端。 每个 `callId` 使用独立锁,保护: ```text 幂等检查 → 更新上下文 → 关键词匹配 → 判断是否首次提醒 → 更新会话状态 → 组装响应 ``` 不同通话使用不同锁,不会互相阻塞。 --- ## 4. 总体架构 ```mermaid flowchart LR A[ASR 系统] --> B[坐席前端] B -->|ASR Final| C[POST /api/v1/asr-events] subgraph M[ASR 实时事件监控服务] C --> D[请求校验] D --> E[callId + seq 幂等] E --> F[更新最近 Final 上下文] F --> G[文本标准化] G --> H[AC 自动机匹配] H --> I[按事件聚合关键词] I --> J[会话级新增事件差分] J --> K[返回 newAlerts + currentResults] end L[JVM 内存会话状态] <--> E L <--> F L <--> J N[shadcn 配置页面] --> O[规则管理 API] O --> P[JPA 数据库] O --> Q[构建新 AC 自动机] Q --> R[AtomicReference 原子切换] R --> H K --> B ``` --- ## 5. 技术栈 | 类别 | 选型 | |---|---| | JDK | Java 21 | | 后端框架 | Spring Boot 4.1.0 | | Web | Spring MVC | | 参数校验 | Jakarta Validation | | 数据持久化 | Spring Data JPA | | 开发数据库 | H2 文件模式 | | 正式数据库 | PostgreSQL,可平滑替换 | | 关键词匹配 | `org.ahocorasick:ahocorasick` | | 会话状态 | `ConcurrentHashMap` | | 并发控制 | 每通话 `ReentrantLock` | | 规则热切换 | `AtomicReference` | | 定时清理 | `@Scheduled` | | 健康检查 | Spring Boot Actuator | | 前端 | React + TypeScript + Vite | | UI | Tailwind CSS + shadcn/ui | | 测试 | JUnit 5、MockMvc、Spring Boot Test Starter | --- ## 6. POM 调整 当前 POM 已包含: - `spring-boot-starter-webmvc` - `spring-boot-starter-validation` - `spring-boot-starter-actuator` - 对应测试 Starter 建议增加: ```xml org.ahocorasick ahocorasick 0.6.3 org.springframework.boot spring-boot-starter-data-jpa com.h2database h2 runtime org.springframework.boot spring-boot-configuration-processor true ``` 开发阶段使用 H2 文件数据库,规则配置不会因服务重启丢失。 正式部署时可将 H2 切换为 PostgreSQL,JPA 业务代码无需明显修改。 --- ## 7. 项目目录 ```text demo/ ├── pom.xml │ ├── frontend/ │ ├── package.json │ ├── vite.config.ts │ ├── components.json │ └── src/ │ ├── App.tsx │ ├── api/ │ │ └── rule-api.ts │ ├── components/ │ │ ├── RuleEditor.tsx │ │ ├── RuleDialog.tsx │ │ └── MatchTester.tsx │ └── components/ui/ │ └── src/ ├── main/ │ ├── java/com/example/demo/ │ │ ├── DemoApplication.java │ │ │ │ │ ├── api/ │ │ │ ├── AsrEventController.java │ │ │ ├── CallController.java │ │ │ └── RuleAdminController.java │ │ │ │ │ ├── application/ │ │ │ ├── AsrEventMonitorService.java │ │ │ ├── CallSessionService.java │ │ │ └── RuleManagementService.java │ │ │ │ │ ├── domain/ │ │ │ ├── AsrFinalEventRequest.java │ │ │ ├── MonitorResponse.java │ │ │ ├── CloseCallResponse.java │ │ │ ├── MatchResult.java │ │ │ ├── AlertResult.java │ │ │ ├── RuleDocument.java │ │ │ ├── RuleSet.java │ │ │ ├── EventRule.java │ │ │ ├── CallSession.java │ │ │ └── EventState.java │ │ │ │ │ ├── matcher/ │ │ │ ├── TextMatcher.java │ │ │ ├── AcKeywordMatcher.java │ │ │ ├── AcAutomatonFactory.java │ │ │ ├── AcAutomatonSnapshot.java │ │ │ └── KeywordPayload.java │ │ │ │ │ ├── repository/ │ │ │ ├── SessionStore.java │ │ │ ├── InMemorySessionStore.java │ │ │ ├── RuleVersionEntity.java │ │ │ └── RuleVersionRepository.java │ │ │ │ │ ├── config/ │ │ │ └── MonitorProperties.java │ │ │ │ │ ├── job/ │ │ │ └── SessionCleanupJob.java │ │ │ │ │ └── support/ │ │ ├── TextNormalizer.java │ │ ├── EventKey.java │ │ └── ApiExceptionHandler.java │ │ │ └── resources/ │ ├── application.yml │ └── static/ │ └── test/ ``` --- ## 8. ASR 实时检测接口 ### 8.1 请求 ```http POST /api/v1/asr-events Content-Type: application/json ``` ```json { "callId": "call-1001", "seq": 28, "speaker": "citizen", "text": "我这个顺丰快递一直没有收到", "final": true } ``` ### 8.2 DTO ```java public record AsrFinalEventRequest( @NotBlank @Size(max = 128) String callId, @PositiveOrZero long seq, @NotBlank @Pattern(regexp = "citizen|agent") String speaker, @NotBlank @Size(max = 4000) String text, @JsonProperty("final") boolean isFinal ) { } ``` ### 8.3 响应 ```json { "accepted": true, "duplicate": false, "callId": "call-1001", "acceptedSeq": 28, "revision": 1, "activeRuleVersion": 5, "newAlerts": [ { "eventType": "transfer", "eventId": "sf-express", "eventName": "顺丰速递绿色渠道服务热线", "status": "matched", "matchedKeywords": [ "顺丰快递", "顺丰" ], "evidence": "我这个顺丰快递一直没有收到", "firstMatchedSeq": 28 } ], "currentResults": [ { "eventType": "transfer", "eventId": "sf-express", "eventName": "顺丰速递绿色渠道服务热线", "status": "matched", "matchedKeywords": [ "顺丰快递", "顺丰" ], "evidence": "我这个顺丰快递一直没有收到", "firstMatchedSeq": 28 } ] } ``` 字段说明: | 字段 | 说明 | |---|---| | `accepted` | 请求是否被处理 | | `duplicate` | 是否为重复 Final | | `newAlerts` | 本次新增、需要前端提醒的事件 | | `currentResults` | 当前通话累计识别到的全部事件 | | `activeRuleVersion` | 当前实际参与匹配的规则版本 | --- ## 9. 幂等算法 删除独立 `eventId` 后,使用: ```text callId + seq ``` 标识 ASR Final。 每个通话保存最近已处理的序号: ```java private final Set processedSeqs = new LinkedHashSet<>(); ``` ```java public boolean markProcessed(long seq, int maxSize) { if (processedSeqs.contains(seq)) { return false; } processedSeqs.add(seq); while (processedSeqs.size() > maxSize) { Iterator iterator = processedSeqs.iterator(); iterator.next(); iterator.remove(); } return true; } ``` 使用有界集合而不是只判断: ```java seq <= lastSeq ``` 可以允许少量乱序请求继续参与匹配,降低漏判风险。 前提: - `callId` 每通电话唯一 - `seq` 在同一通电话内唯一 如果坐席和市民分别独立编号,应改成: ```text callId + speaker + seq ``` --- ## 10. 文本标准化算法 ```java public final class TextNormalizer { private static final Pattern REMOVABLE = Pattern.compile( "[\\s,。!?、;:,.!?;:\"'()()\\[\\]【】]+" ); private TextNormalizer() { } public static String normalize(String text) { if (text == null || text.isBlank()) { return ""; } String normalized = Normalizer.normalize( text, Normalizer.Form.NFKC ).toLowerCase(Locale.ROOT); return REMOVABLE .matcher(normalized) .replaceAll(""); } } ``` 示例: ```text EMS → ems E M S → ems 顺丰,快递 → 顺丰快递 ``` --- ## 11. 最近 Final 上下文 市民和坐席文本都参与匹配。同一次通话维护一个共享上下文窗口, 匹配文本使用该通话最近五条 ASR Final。 目的:降低 ASR 切段造成的漏判。 例如: ```text Final 27:我是在拼 Final 28:多多上买的 ``` 拼接后: ```text 我是在拼。多多上买的 ``` 标准化后: ```text 我是在拼多多上买的 ``` 从而命中“拼多多”。 会话中按 seq 保存: ```java private final NavigableMap recentFinals = new TreeMap<>(); ``` ```java public void appendFinal( long seq, String text, int windowSize ) { recentFinals.put(seq, text); while (recentFinals.size() > windowSize) { recentFinals.pollFirstEntry(); } } ``` --- ## 12. AC 自动机匹配 ### 12.1 Payload ```java public record RuleTarget( String ruleSetId, String ruleId, String ruleName ) { } public record KeywordPayload( String normalizedKeyword, String displayKeyword, List targets ) { } ``` ### 12.2 构建流程 ```text 读取完整规则 → 校验 → 标准化关键词 → 构建新的 PayloadTrie → 包装为不可变快照 → AtomicReference 原子替换 ``` 实时 ASR 请求只扫描内存中的自动机,不访问数据库。 ### 12.3 Matcher ```java @Component public class AcKeywordMatcher { private final AtomicReference currentSnapshot = new AtomicReference<>(); public List match(String text) { AcAutomatonSnapshot snapshot = currentSnapshot.get(); if (snapshot == null || text == null || text.isBlank()) { return List.of(); } String normalizedText = TextNormalizer.normalize(text); Collection> emits = snapshot.trie().parseText(normalizedText); return aggregate(emits, text); } public void replaceSnapshot( AcAutomatonSnapshot newSnapshot ) { currentSnapshot.set(newSnapshot); } } ``` ### 12.4 重叠关键词处理 规则可能同时包含: ```text 顺丰快递 顺丰 ``` 不使用 `ignoreOverlaps`。 AC 自动机返回全部命中,应用层按事件聚合: ```json { "eventId": "sf-express", "matchedKeywords": [ "顺丰快递", "顺丰" ] } ``` 同一方向只返回一个事件结果。 --- ## 13. 会话级新增提醒算法 每通电话维护: ```java private final Set alertedEventKeys = new HashSet<>(); ``` 统一事件键: ```text eventKey = ruleSetId + ":" + ruleId ``` 例如: ```text transfer:sf-express ``` 新增事件差分: ```text newEvents = currentMatchedEvents - alertedEvents ``` Java: ```java List newMatches = matches.stream() .filter(match -> !session.hasAlerted(match.eventKey()) ) .toList(); ``` 更新状态: ```java for (MatchResult match : matches) { session.mergeMatch(match, seq); } for (MatchResult match : newMatches) { session.markAlerted(match.eventKey()); } ``` 效果: ```text 第一次出现“顺丰” → 提醒一次 再次出现“顺丰” → 不重复提醒 后续出现“拼多多” → 新增提醒拼多多 ``` --- ## 14. 每通电话独立锁 ```java private final ConcurrentHashMap callLocks = new ConcurrentHashMap<>(); ``` ```java public ReentrantLock getLock(String callId) { return callLocks.computeIfAbsent( callId, ignored -> new ReentrantLock() ); } ``` 处理时: ```java ReentrantLock lock = sessionStore.getLock(request.callId()); lock.lock(); try { return processUnderLock(request); } finally { lock.unlock(); } ``` 锁保护: ```text 幂等检查 → 上下文更新 → 匹配 → 新事件判断 → 状态更新 → 响应构建 ``` 不同通话使用不同锁,可以并行处理。 --- ## 15. 会话关闭与 TTL 清理 会话释放采用 **主动关闭 + TTL 兜底** 双路径: ```text 通话结束 → 优先:POST /api/v1/calls/{callId}/close(立即释放) → 兜底:TTL 定时任务清理遗漏会话 ``` ### 15.1 通话关闭接口 ```http POST /api/v1/calls/{callId}/close ``` 响应示例: ```json { "callId": "call-1001", "closed": true, "existed": true } ``` | 字段 | 说明 | |---|---| | `callId` | 被关闭的通话 ID | | `closed` | 本次调用是否完成关闭处理(接口幂等,恒为 `true`) | | `existed` | 关闭前会话是否存在;重复关闭或不存在时为 `false` | 处理流程: ```text 获取 callId 独立锁 → 删除 CallSession → 删除对应 ReentrantLock → 返回 closed=true ``` 语义约定: - 接口幂等:同一 `callId` 多次 close 不报错 - 通话结束后应尽快调用,避免会话长期占用内存 - 由于 `callId` 必须全局唯一,关闭后不应再复用同一 `callId` 提交 ASR Final 服务层示意: ```java public CloseCallResponse close(String callId) { ReentrantLock lock = sessionStore.getLock(callId); lock.lock(); try { boolean existed = sessionStore.remove(callId); sessionStore.removeLock(callId); return new CloseCallResponse(callId, true, existed); } finally { lock.unlock(); } } ``` ### 15.2 TTL 兜底清理 即使提供 close,仍保留 TTL,用于清理以下遗留会话: - 前端未调用 close - 页面异常关闭、网络故障导致 close 未送达 - 进程重启前未及时释放的会话(单实例内存方案下重启后本就为空,TTL 主要覆盖运行期遗漏) ```yaml monitor: recent-final-window-size: 5 max-processed-seqs: 500 session-ttl: PT2H session-cleanup-interval: PT10M ``` ```java @Scheduled( fixedDelayString = "${monitor.session-cleanup-interval:PT10M}" ) public void cleanupExpiredSessions() { Instant threshold = Instant.now(clock) .minus(properties.sessionTtl()); sessionStore.removeExpired(threshold); } ``` TTL 清理时同样删除: - `CallSession` - 对应的 `ReentrantLock` close 与 TTL 互不冲突:已 close 的会话不存在,TTL 跳过;未 close 的会话到期后由 TTL 删除。 --- ## 16. 规则数据模型 首期使用一张版本表保存完整规则 JSON。 ```sql CREATE TABLE rule_version ( id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, version_no BIGINT NOT NULL, active BOOLEAN NOT NULL, content_json CLOB NOT NULL, rule_count INT NOT NULL, keyword_count INT NOT NULL, created_at TIMESTAMP NOT NULL ); ``` 规则文档: ```json { "ruleSets": [ { "id": "transfer", "name": "潜在转接", "enabled": true, "matcher": "ac-keyword", "rules": [ { "id": "sf-express", "name": "顺丰速递绿色渠道服务热线", "enabled": true, "keywords": [ "顺丰", "顺丰快递", "顺丰速运" ] } ] } ] } ``` 每次保存并生效生成新版本: ```text V1 active=false V2 active=false V3 active=true ``` --- ## 17. 规则管理接口 ### 17.1 获取当前生效规则 ```http GET /api/v1/admin/rules ``` 响应: ```json { "version": 5, "ruleCount": 14, "keywordCount": 39, "ruleSets": [] } ``` ### 17.2 保存并生效 ```http PUT /api/v1/admin/rules ``` 请求: ```json { "baseVersion": 5, "ruleSets": [] } ``` 使用 `baseVersion` 做乐观并发控制。 版本冲突: ```http 409 Conflict ``` ```json { "code": "RULE_VERSION_CONFLICT", "message": "规则已被其他页面修改,请刷新后重试", "currentVersion": 6 } ``` ### 17.3 查询历史版本 ```http GET /api/v1/admin/rules/versions?limit=20 ``` 按版本号倒序返回版本摘要,包括版本号、是否生效、规则数、关键词数和创建时间。 ### 17.4 回溯历史版本 ```http POST /api/v1/admin/rules/versions/{version}/restore ``` 请求: ```json { "baseVersion": 8 } ``` 回溯不会重新激活或修改旧记录。系统复制目标历史版本的规则内容, 创建并发布一个新的递增版本。例如当前 V8 回溯 V3,最终创建并生效 V9。 `baseVersion` 继续用于乐观并发控制。 ### 17.5 测试当前生效规则 ```http POST /api/v1/admin/rules/test ``` 请求: ```json { "text": "我这个顺丰快递一直没有收到" } ``` 响应: ```json { "activeVersion": 6, "normalizedText": "我这个顺丰快递一直没有收到", "matches": [ { "eventType": "transfer", "eventId": "sf-express", "eventName": "顺丰速递绿色渠道服务热线", "matchedKeywords": [ "顺丰快递", "顺丰" ] } ] } ``` 测试接口是无状态接口: - 不保存通话上下文 - 不执行同一通话去重 - 每次都返回当前文本的直接匹配结果 - 验证的是后端当前真正生效的内存 AC 自动机 --- ## 18. 规则保存后生效流程 ```text 页面提交完整规则和 baseVersion ↓ 检查 baseVersion ↓ 校验规则结构和关键词冲突 ↓ 预构建新的 AC 自动机 ↓ 数据库保存新规则版本 ↓ 事务提交成功 ↓ AtomicReference 原子切换 ↓ 后续 ASR 请求立即使用新规则 ``` 核心原则: > 新自动机先构建成功,数据库事务提交成功,之后才切换运行时规则。 事务内: ```java @Transactional public SaveRuleResponse saveAndActivate( SaveRuleRequest request ) { RuleDocument document = ruleValidator.validateAndNormalize(request); AcAutomatonSnapshot snapshot = automatonFactory.build(document); repository.deactivateCurrent(); RuleVersionEntity entity = repository.save( RuleVersionEntity.active( document, snapshot.ruleCount(), snapshot.keywordCount() ) ); eventPublisher.publishEvent( new RuleActivatedEvent( entity.getVersionNo(), snapshot ) ); return SaveRuleResponse.from(entity, snapshot); } ``` 事务提交后切换: ```java @TransactionalEventListener( phase = TransactionPhase.AFTER_COMMIT ) public void activate(RuleActivatedEvent event) { keywordMatcher.replaceSnapshot( event.snapshot() .withVersion(event.version()) ); } ``` 失败策略: - 校验失败:不保存、不切换 - AC 自动机构建失败:不保存、不切换 - 数据库提交失败:不切换 - 旧规则继续正常工作 --- ## 19. 服务启动后的规则恢复 服务启动时: ```text 查询数据库中 active=true 的最新规则 → 解析完整规则 JSON → 校验 → 构建 AC 自动机 → 设置为当前运行快照 ``` 如果数据库没有规则: ```text 读取内置初始配置 → 创建 V1 → 设置 active=true → 构建并加载自动机 ``` --- ## 20. 规则校验要求 保存前至少校验: 1. 规则集 ID 不重复 2. 事件 ID 由系统自动生成;导入或兼容旧数据携带 ID 时必须保持唯一 3. 事件名称不能为空 4. 启用事件至少有一个关键词 5. 标准化后关键词不能为空 6. 同一事件内关键词不能重复 7. 同一关键词不能指向多个转接方向 8. Matcher 类型必须存在 9. `baseVersion` 必须等于当前版本 对于“闪购”“邮政”“百度”等较宽泛关键词,可以作为 warning 返回,但首期允许保存。 前端新增规则时生成只读事件 ID,Excel 新增行允许将事件 ID 留空;后端保存时仍会为缺失 ID 生成 `event-`,作为绕过前端调用接口时的兜底。已有规则的事件 ID 保持不变,以保证同一通话内事件去重和历史版本回溯的稳定性。 --- ## 21. 单页面前端 前端采用: ```text React TypeScript Vite Tailwind CSS shadcn/ui ``` 页面不使用路由,只使用两个 Tab: ```text ┌─────────────────────────────────────────────┐ │ ASR 事件监控配置 当前生效版本 V6 │ ├─────────────────────────────────────────────┤ │ [规则配置] [匹配验证] │ ├─────────────────────────────────────────────┤ │ │ │ 当前 Tab 内容 │ │ │ └─────────────────────────────────────────────┘ ``` --- ## 22. 规则配置页面 页面功能: - 展示规则列表 - 新增规则 - 编辑规则 - 删除规则 - 启用或停用规则 - 增加或删除关键词 - 保存并生效 - 显示当前生效版本 - 显示规则数和关键词数 推荐 shadcn/ui 组件: | 功能 | 组件 | |---|---| | 页面切换 | `Tabs` | | 规则展示 | `Card` 或 `Table` | | 关键词 | `Badge` | | 启用状态 | `Switch` | | 新增与编辑 | `Dialog` | | 删除确认 | `AlertDialog` | | 按钮 | `Button` | | 文本输入 | `Input` | | 操作提示 | `Sonner` | 页面示意: ```text 潜在转接规则 共 14 条 ┌───────────────────────────────────────────┐ │ 顺丰速递绿色渠道服务热线 [启用] │ │ ID:sf-express │ │ │ │ [顺丰 ×] [顺丰快递 ×] [顺丰速运 ×] │ │ [编辑] [删除] │ └───────────────────────────────────────────┘ [新增规则] [保存并生效] ``` --- ## 23. Demo 匹配验证页面 Demo 页面用于验证当前后端真正生效的 AC 自动机。 ```text 匹配验证 生效版本 V6 测试文本 ┌───────────────────────────────────────────┐ │ 我这个顺丰快递一直没有收到 │ └───────────────────────────────────────────┘ [清空] [开始检测] 检测结果 ✓ 命中潜在转接方向 顺丰速递绿色渠道服务热线 事件 ID:sf-express 命中关键词:[顺丰快递] [顺丰] ``` 页面调用: ```http POST /api/v1/admin/rules/test ``` 保存并生效后,可自动切换到 Demo Tab 并验证新关键词。 --- ## 24. 前后端集成 开发阶段: ```text Vite:http://localhost:5173 Spring Boot:http://localhost:8080 ``` `vite.config.ts`: ```ts import react from "@vitejs/plugin-react" import tailwindcss from "@tailwindcss/vite" import { defineConfig } from "vite" export default defineConfig({ plugins: [react(), tailwindcss()], server: { proxy: { "/api": { target: "http://localhost:8080", changeOrigin: true, }, }, }, }) ``` 生产构建: ```bash cd frontend pnpm build ``` 将 `frontend/dist` 内容复制到: ```text src/main/resources/static/ ``` 最终前端和后端一起打包进同一个 Spring Boot JAR。 访问: ```text http://localhost:8080/ ``` 即可打开规则配置和 Demo 页面。 --- ## 25. application.yml 示例 ```yaml spring: application: name: asr-event-monitor datasource: url: jdbc:h2:file:./data/rules;DB_CLOSE_ON_EXIT=FALSE username: sa password: jpa: hibernate: ddl-auto: update open-in-view: false server: port: 8080 monitor: recent-final-window-size: 5 max-processed-seqs: 500 session-ttl: PT2H session-cleanup-interval: PT10M management: endpoints: web: exposure: include: - health - info - metrics ``` --- ## 26. 性能与部署 当前规模: ```text 坐席数:2 每坐席约 0~2 条 ASR Final/秒 总请求量:约 0~4 QPS ``` 推荐资源: ```text CPU:1 核 内存:512 MB~1 GB 实例数:1 ``` 启动: ```bash mvn clean package java \ -Xms256m \ -Xmx512m \ -jar target/demo-0.0.1-SNAPSHOT.jar ``` 由于通话状态保存在 JVM 内存中,首期只部署一个实例。 --- ## 27. 测试与验收 至少覆盖以下场景: 1. 首次出现“顺丰”产生提醒 2. 同一通话重复出现“顺丰”不重复提醒 3. 同一通话后续出现“拼多多”产生新增提醒 4. 同一个 `callId + seq` 重复提交返回 `duplicate=true` 5. 少量乱序请求仍可参与匹配 6. 市民和坐席文本均可触发匹配,并共享同一通话上下文窗口 7. Partial 文本不参与匹配 8. `EMS`、`ems`、`E M S` 均可命中 9. 相邻 Final 拼接后可命中被切分的品牌 10. 不同通话状态相互隔离 11. close 能立即释放通话会话,重复 close 幂等 12. TTL 能清理未 close 的过期会话 13. 保存新规则后 Demo 接口立即使用新版本 14. 保存失败时旧规则继续有效 15. 两个管理页面同时编辑时能够检测版本冲突 16. Actuator 健康检查正常 --- ## 28. 后续演进 ### 28.1 高频事件 增加规则集: ```json { "id": "high-frequency-event", "name": "高频事件", "enabled": true, "matcher": "ac-keyword", "rules": [] } ``` ### 28.2 Dify 二次确认 状态扩展: ```text matched → confirming → confirmed / rejected ``` Dify 调用异步执行,不阻塞 ASR 接口。 ### 28.3 语义匹配 新增: ```java public class SemanticMatcher implements TextMatcher { } ``` 保持统一 `MatchResult` 输出。 ### 28.4 Redis 与多实例 需要扩容时: ```text InMemorySessionStore → RedisSessionStore ``` Controller、规则管理、Matcher 和前端协议保持不变。 --- ## 29. 最终方案摘要 实时检测链路: ```text 前端收到 ASR Final ↓ POST /api/v1/asr-events ↓ callId + seq 幂等 ↓ 最近五条市民 Final 拼接 ↓ 文本标准化 ↓ 内存 AC 自动机匹配 ↓ 按业务事件聚合关键词 ↓ 与本通话已提醒事件集合做差 ↓ 返回 newAlerts + currentResults ``` 规则生效链路: ```text shadcn 单页面编辑完整规则 ↓ PUT /api/v1/admin/rules ↓ 校验规则 ↓ 预构建新 AC 自动机 ↓ 数据库保存新版本 ↓ 事务提交 ↓ AtomicReference 原子切换 ↓ Demo 页面立即验证 ``` 首期接口: ```text POST /api/v1/asr-events POST /api/v1/calls/{callId}/close GET /api/v1/admin/rules PUT /api/v1/admin/rules POST /api/v1/admin/rules/test ``` 会话清理: ```text 通话结束 → close 立即释放 遗漏会话 → TTL 定时兜底清理 ``` 最终采用: ```text Spring Boot 4.1.0 Java 21 Spring MVC Spring Data JPA H2 文件数据库 AC 自动机 JVM 内存会话状态 React + Vite + shadcn/ui 单实例部署 ```