Просмотр исходного кода

feat:企微会话存档(添加发送人角色判断、调整媒体下载、调整语音转文字)

caoliqin 1 неделя назад
Родитель
Сommit
531c459be3

+ 40 - 1
fs-qw-api-msg/src/main/java/com/fs/app/msgarchives/QwMsgAuditIngestService.java

@@ -7,13 +7,16 @@ import com.fs.qw.domain.QwCompany;
 import com.fs.qw.domain.audit.QwMsgAuditMessage;
 import com.fs.qw.domain.audit.QwMsgAuditRaw;
 import com.fs.qw.domain.audit.QwMsgAuditSeq;
+import com.fs.qw.domain.QwUser;
 import com.fs.qw.mapper.QwCompanyMapper;
 import com.fs.qw.mapper.QwMsgAuditMessageMapper;
 import com.fs.qw.mapper.QwMsgAuditRawMapper;
 import com.fs.qw.mapper.QwMsgAuditSeqMapper;
+import com.fs.qw.mapper.QwUserMapper;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
 
 import java.util.*;
 
@@ -32,19 +35,26 @@ public class QwMsgAuditIngestService {
     private final QwMsgAuditMessageMapper messageMapper;
     private final QwMsgAuditPullService pullService;
     private final List<QwMsgAuditMsgHandler> handlers;
+    private final QwUserMapper qwUserMapper;
+
+    // 发送方角色,1-内部,2-外部
+    private static final int FROM_USER_ROLE_INTERNAL = 1;
+    private static final int FROM_USER_ROLE_EXTERNAL = 2;
 
     public QwMsgAuditIngestService(QwCompanyMapper qwCompanyMapper,
                                    QwMsgAuditSeqMapper seqMapper,
                                    QwMsgAuditRawMapper rawMapper,
                                    QwMsgAuditMessageMapper messageMapper,
                                    QwMsgAuditPullService pullService,
-                                   List<QwMsgAuditMsgHandler> handlers) {
+                                   List<QwMsgAuditMsgHandler> handlers,
+                                   QwUserMapper qwUserMapper) {
         this.qwCompanyMapper = qwCompanyMapper;
         this.seqMapper = seqMapper;
         this.rawMapper = rawMapper;
         this.messageMapper = messageMapper;
         this.pullService = pullService;
         this.handlers = handlers;
+        this.qwUserMapper = qwUserMapper;
     }
 
     @Transactional(rollbackFor = Exception.class)
@@ -128,6 +138,8 @@ public class QwMsgAuditIngestService {
                 continue;
             }
             QwMsgAuditMessage msg = handlerOpt.get().buildMessage(corpId, seq, rawId, plain);
+            // 添加发送人角色判断
+            fillFromUserRole(msg);
             messageMapper.insertQwMsgAuditMessage(msg);
         }
 
@@ -140,5 +152,32 @@ public class QwMsgAuditIngestService {
 
         seqMapper.advanceSeq(corpId, maxSeq);
     }
+
+    /**
+     * 发送人角色判断
+     * 判断前两位为 wm/wo/wb 则为企业外部;否则按 qw_user_id + corp_id 查 qw_user,查到为内部销售,否则外部。from 为空打日志不写角色。
+     */
+    private void fillFromUserRole(QwMsgAuditMessage msg) {
+        String from = msg.getFromUser();
+        String corpId = msg.getCorpId();
+        if (!StringUtils.hasText(from)) {
+            log.warn("消息发送方id有误, corpId={}, seq={}", corpId, msg.getSeq());
+            return;
+        }
+        String f = from.trim();
+        if (f.length() >= 2) {
+            String prefix = f.substring(0, 2).toLowerCase(Locale.ROOT);
+            if ("wm".equals(prefix) || "wo".equals(prefix) || "wb".equals(prefix)) {
+                msg.setFromUserRole(FROM_USER_ROLE_EXTERNAL);
+                return;
+            }
+        }
+        QwUser u = qwUserMapper.selectQwUserByIdByWeComeText2(f, corpId);
+        if (u != null) {
+            msg.setFromUserRole(FROM_USER_ROLE_INTERNAL);
+        } else {
+            msg.setFromUserRole(FROM_USER_ROLE_EXTERNAL);
+        }
+    }
 }
 

+ 87 - 0
fs-qw-api-msg/src/main/java/com/fs/app/msgarchives/QwMsgMediaFileService.java

@@ -8,8 +8,13 @@ import com.tencent.wework.Finance;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
 
 import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
 import java.util.Locale;
 
 /**
@@ -22,12 +27,19 @@ import java.util.Locale;
 @Slf4j
 public class QwMsgMediaFileService {
 
+    private static final Object FINANCE_NATIVE_LOCK = new Object();
+    private static volatile boolean financeNativeLoaded;
+
     @Value("${qw.msg-audit.ffmpeg-path}")
     private String ffmpegPath;
 
     @Value("${qw.msg-audit.voice-amr-to-mp3:true}")
     private boolean voiceAmrToMp3Enabled;
 
+    /** 与 QwMsgAuditWxCpFactory 一致,调用 Finance JNI 前必须先按此路径 loadingLibraries */
+    @Value("${qw.msg-audit.sdk-lib-path:}")
+    private String msgAuditSdkLibPath;
+
     /**
      * 下载后上传 OSS。后缀为 .amr 且开启转换时,会先转为 mp3 再上传,返回的 URL 指向 mp3。
      *
@@ -40,6 +52,8 @@ public class QwMsgMediaFileService {
         if (sdkfileid == null || sdkfileid.isEmpty()) {
             return null;
         }
+        // 先要加载 Finance SDK 原生库,才能NewSdk 然后下载媒体文件
+        ensureFinanceNativeLoaded(msgAuditSdkLibPath);
         long sdk = Finance.NewSdk();
         long ret = Finance.Init(sdk, company.getCorpId(), company.getMsgSecret());
         if (ret != 0) {
@@ -77,6 +91,79 @@ public class QwMsgMediaFileService {
         }
     }
 
+    /**
+     * Finance JNI 须先 loadingLibraries 再 NewSdk;仅走媒体下载、未先跑 WxJava 拉取时也要加载。
+     * 配置格式与 qw.msg-audit.sdk-lib-path 一致:目录、单文件路径,或逗号分隔多 DLL(首项为第一个库的绝对路径)。
+     */
+    private static void ensureFinanceNativeLoaded(String sdkLibPathConfig) {
+        if (financeNativeLoaded) {
+            return;
+        }
+        synchronized (FINANCE_NATIVE_LOCK) {
+            if (financeNativeLoaded) {
+                return;
+            }
+            if (!StringUtils.hasText(sdkLibPathConfig)) {
+                throw new IllegalStateException("未配置 qw.msg-audit.sdk-lib-path,无法加载企微 Finance 原生库");
+            }
+            String raw = sdkLibPathConfig.trim();
+            try {
+                if (raw.contains(",")) {
+                    loadFinanceNativeMulti(raw);
+                } else {
+                    loadFinanceNativeSingle(raw);
+                }
+            } catch (UnsatisfiedLinkError e) {
+                String msg = e.getMessage();
+                if (msg != null && msg.contains("already loaded")) {
+                    log.debug("Finance SDK 原生库已加载: {}", msg);
+                } else {
+                    throw e;
+                }
+            }
+            financeNativeLoaded = true;
+        }
+    }
+
+    private static void loadFinanceNativeMulti(String raw) {
+        String[] segments = raw.split(",");
+        List<String> trimmed = new ArrayList<>();
+        for (String s : segments) {
+            if (StringUtils.hasText(s)) {
+                trimmed.add(s.trim());
+            }
+        }
+        if (trimmed.isEmpty()) {
+            throw new IllegalStateException("qw.msg-audit.sdk-lib-path 多文件配置为空");
+        }
+        String anchor = trimmed.get(0);
+        File anchorFile = new File(anchor);
+        List<String> libs = new ArrayList<>();
+        if (anchorFile.isAbsolute() && anchor.contains(".")) {
+            libs.add(anchorFile.getName());
+            for (int i = 1; i < trimmed.size(); i++) {
+                libs.add(trimmed.get(i));
+            }
+        } else {
+            libs.addAll(trimmed);
+        }
+        Finance.loadingLibraries(libs, anchor);
+    }
+
+    private static void loadFinanceNativeSingle(String raw) {
+        String os = System.getProperty("os.name", "").toLowerCase(Locale.ROOT);
+        boolean win = os.contains("win");
+        String lower = raw.toLowerCase(Locale.ROOT);
+        boolean looksLikeFile = lower.endsWith(".dll") || lower.endsWith(".so");
+        File f = new File(raw);
+        if (looksLikeFile || f.isFile()) {
+            Finance.loadingLibraries(Collections.singletonList(f.getName()), raw);
+            return;
+        }
+        String name = win ? "WeWorkFinanceSdk.dll" : "libWeWorkFinanceSdk.so";
+        Finance.loadingLibraries(Collections.singletonList(name), raw);
+    }
+
     /**
      * 循环分片拉取媒体二进制数据,直到 isFinish
      */

+ 3 - 0
fs-qw-api-msg/src/main/java/com/fs/app/msgarchives/handler/QwMsgAuditMsgHandler.java

@@ -33,6 +33,9 @@ public interface QwMsgAuditMsgHandler {
             msg.setMsgId(plain.getString("msgid"));
             msg.setMsgTime(plain.getLong("msgtime"));
             msg.setMsgType(plain.getString("msgtype"));
+            msg.setFromUser(plain.getString("from"));
+            msg.setToList(plain.getJSONArray("tolist") == null ? null : plain.getJSONArray("tolist").toJSONString());
+            msg.setRoomId(plain.getString("roomid"));
         }
     }
 

+ 8 - 7
fs-service/src/main/java/com/fs/aiSoundReplication/service/impl/AsrServiceImpl.java

@@ -14,6 +14,7 @@ import okhttp3.Response;
 import org.springframework.stereotype.Service;
 import org.springframework.util.StringUtils;
 
+import java.nio.charset.StandardCharsets;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.UUID;
@@ -87,7 +88,7 @@ public class AsrServiceImpl implements AsrService {
         String json = objectMapper.writeValueAsString(body);
         Request.Builder builder = new Request.Builder()
                 .url(config.getSubmitUrl())
-                .post(RequestBody.create(json, JSON))
+                .post(RequestBody.create(JSON, json.getBytes(StandardCharsets.UTF_8)))
                 .addHeader("Content-Type", "application/json");
 
         applySubmitHeaders(builder, taskId);
@@ -95,10 +96,10 @@ public class AsrServiceImpl implements AsrService {
         try (Response response = okHttpClient.newCall(builder.build()).execute()) {
             String code = headerStatus(response, "X-Api-Status-Code");
             if (STATUS_OK.equals(code)) {
-                log.info("ASR submit 成功, taskId={}, logId={}", taskId, response.header("X-Tt-Logid"));
+                log.info("ASR submit成功, taskId={}, logId={}", taskId, response.header("X-Tt-Logid"));
                 return true;
             }
-            log.error("ASR submit 失败, taskId={}, status={}, message={}, body={}",
+            log.error("ASR submit失败, taskId={}, status={}, message={}, body={}",
                     taskId, code, response.header("X-Api-Message"), bodyString(response));
             return false;
         }
@@ -107,7 +108,7 @@ public class AsrServiceImpl implements AsrService {
     private String pollQuery(String taskId) throws Exception {
         Request.Builder builder = new Request.Builder()
                 .url(config.getQueryUrl())
-                .post(RequestBody.create("{}", JSON))
+                .post(RequestBody.create(JSON, "{}".getBytes(StandardCharsets.UTF_8)))
                 .addHeader("Content-Type", "application/json");
 
         applyQueryHeaders(builder, taskId);
@@ -123,19 +124,19 @@ public class AsrServiceImpl implements AsrService {
                     if (StringUtils.hasText(text)) {
                         return text.trim();
                     }
-                    log.warn("ASR query 成功但 result.text 为空, taskId={}", taskId);
+                    log.warn("ASR query成功但 result.text 为空, taskId={}", taskId);
                     return null;
                 }
                 if (STATUS_PROCESSING.equals(code) || STATUS_QUEUED.equals(code)) {
                     Thread.sleep(config.getPollIntervalMs());
                     continue;
                 }
-                log.error("ASR query 失败, taskId={}, status={}, message={}, body={}",
+                log.error("ASR query失败, taskId={}, status={}, message={}, body={}",
                         taskId, code, response.header("X-Api-Message"), bodyStr);
                 return null;
             }
         }
-        log.error("ASR query 超时, taskId={}, attempts={}", taskId, config.getMaxQueryAttempts());
+        log.error("ASR query超时, taskId={}, attempts={}", taskId, config.getMaxQueryAttempts());
         return null;
     }
 

+ 2 - 1
fs-service/src/main/resources/application-config-druid-ddgy.yml

@@ -100,7 +100,8 @@ doubao:
     access-token: ahvjLYU_CX86otk0ffpkQTGjQPz_iOvw
     # 新版控制台:仅使用 API Key 时配置此项,并留空 app-key、access-token
     # api-key: ''
-    resource-id: volc.seedasr.auc
+    # 旧版
+    resource-id: volc.bigasr.auc
     default-audio-format: mp3
     default-language: zh-CN