Forráskód Böngészése

fix: CSV 产物文件名改为「工具组中文名_工具中文名_时间戳_随机串.csv」

- ArtifactService 增加具体 @Tool 名到中文工具名的映射(与前端 TOOL_LABELS 同步),展示/下载文件名改为中文;未登记工具回退原始 @Tool 名。- 文件名中的中文片段做 Windows 非法字符与空白清洗,磁盘 id 仍保持 ASCII。- 更新 ArtifactServiceTest 断言。
cc 1 napja
szülő
commit
521b1a1de9

+ 13 - 2
ai-electron/frontend/src/ai/components/ArtifactFileCard.vue

@@ -50,8 +50,19 @@
 
   const { createMessage } = useMessage();
 
-  /** 工具中文名:文件名里是具体 @Tool 名(英文),这里补一个可读中文标签 */
-  const toolNameText = computed(() => (props.toolName ? toolLabel(props.toolName) : ''));
+  /**
+   * 工具中文名 chip:
+   * - 新产物文件名已经是「工具组中文名_工具中文名_...」,不再重复展示;
+   * - 历史产物文件名只有工具组,补一个中文工具名便于识别。
+   */
+  const toolNameText = computed(() => {
+    if (!props.toolName) return '';
+    const label = toolLabel(props.toolName);
+    if (!label || label === props.toolName) return '';
+    const compactFileName = String(props.artifact.fileName || '').replace(/\s+/g, '');
+    const compactLabel = label.replace(/\s+/g, '');
+    return compactLabel && compactFileName.includes(compactLabel) ? '' : label;
+  });
 
   function toCount(value: unknown): number | undefined {
     if (typeof value === 'number' && Number.isFinite(value)) return value;

+ 105 - 5
ai-server/src/main/java/com/zsjz/ai/module/agent/artifact/ArtifactService.java

@@ -80,6 +80,81 @@ public class ArtifactService {
             "execute_sql", "SQL查询",
             "query_table_data", "基础数据");
 
+    /**
+     * 具体 {@code @Tool} 名 → 中文工具名。
+     *
+     * <p>文件名格式:{@code 工具组中文名_工具中文名_时间戳_随机串.csv}。
+     * 本表与前端 {@code ai/utils/constants.ts#TOOL_LABELS} 保持同步;新工具忘记登记时
+     * 回退原始 {@code @Tool} 名,仍能区分是哪个工具,只是显示为英文。
+     */
+    private static final Map<String, String> ARTIFACT_ACTION_LABELS = Map.ofEntries(
+            Map.entry("execute_sql", "执行 SQL 查询"),
+            Map.entry("query_table_data", "查询表格数据"),
+
+            // 人员画像与亲密度
+            Map.entry("list_persons", "列出案件人员"),
+            Map.entry("get_person_call_profile", "通话数据画像"),
+            Map.entry("stat_person_call_station", "通话基站分布"),
+            Map.entry("stat_person_call_top10", "最常联系人 TOP10"),
+            Map.entry("stat_call_record_summary", "通话记录汇总"),
+            Map.entry("get_person_trans_profile", "交易数据画像"),
+            Map.entry("stat_person_trans_top10", "最常交易对手 TOP10"),
+            Map.entry("stat_trans_record_summary", "交易记录汇总"),
+            Map.entry("get_trans_bank_card_info", "银行卡交易汇总"),
+            Map.entry("stat_trans_amount_trend_month", "交易金额月度趋势"),
+            Map.entry("stat_trans_amount_trend_year", "交易金额年度趋势"),
+            Map.entry("stat_intimacy_summary", "亲密度总览"),
+
+            // 通话明细
+            Map.entry("get_call_records", "查询通话明细"),
+            Map.entry("stat_call_night_summary", "夜间通话汇总"),
+            Map.entry("stat_call_night_detail", "夜间通话明细"),
+            Map.entry("stat_call_continuous_summary", "持续通话汇总"),
+            Map.entry("stat_call_continuous_detail", "持续通话明细"),
+            Map.entry("stat_call_sensitive_summary", "敏感日期通话汇总"),
+            Map.entry("stat_call_sensitive_detail", "敏感日期通话明细"),
+
+            // 交易与资金
+            Map.entry("get_trans_records", "查询交易明细"),
+            Map.entry("stat_trans_big_summary", "大额交易汇总"),
+            Map.entry("stat_trans_big_by_date", "大额交易按日统计"),
+            Map.entry("stat_trans_big_detail", "大额交易明细"),
+            Map.entry("stat_trans_together", "共同交易分析"),
+            Map.entry("stat_trans_card_hold", "代持卡分析"),
+            Map.entry("stat_trans_cash_flow", "交易现金流汇总"),
+            Map.entry("stat_trans_continuous_summary", "持续交易汇总"),
+            Map.entry("stat_trans_continuous_detail", "持续交易明细"),
+            Map.entry("stat_trans_fast_fund_flow", "资金快转分析"),
+            Map.entry("stat_trans_financial", "金融交易筛选"),
+            Map.entry("stat_trans_fixed_deposit", "定期存款汇总"),
+            Map.entry("stat_trans_fixed_deposit_detail", "定期存款明细"),
+            Map.entry("stat_trans_frequency", "交易频次统计"),
+            Map.entry("stat_trans_sensitive_summary", "敏感日期交易汇总"),
+            Map.entry("stat_trans_sensitive_detail", "敏感日期交易明细"),
+
+            // 轨迹 / 出行 / 快递
+            Map.entry("stat_track_cell_tower_summary", "基站轨迹汇总"),
+            Map.entry("stat_track_cell_tower_detail", "基站轨迹明细"),
+            Map.entry("stat_track_en_local_summary", "境外昼夜活动汇总"),
+            Map.entry("stat_track_en_local_detail", "境外昼夜活动明细"),
+            Map.entry("stat_express_summary", "快递收寄汇总"),
+            Map.entry("stat_express_by_phone", "按号码查快递"),
+            Map.entry("get_express_records", "查询快递明细"),
+            Map.entry("stat_track_meet_summary", "人员碰面汇总"),
+            Map.entry("stat_track_meet_detail", "人员碰面明细"),
+            Map.entry("stat_together_live_summary", "同住关系汇总"),
+            Map.entry("stat_together_live_detail", "同住关系明细"),
+            Map.entry("get_hotel_stay_detail", "旅馆住宿明细"),
+            Map.entry("get_together_live_info_detail", "同住服务记录明细"),
+            Map.entry("stat_together_travel_summary", "同行出行汇总"),
+            Map.entry("stat_together_travel_detail", "同行出行明细"),
+            Map.entry("get_travel_record_detail", "出行票务明细"),
+
+            // 非表格材料档案
+            Map.entry("list_file_profiles", "检索材料清单"),
+            Map.entry("stat_file_profiles_by_category", "材料分类统计"),
+            Map.entry("get_file_sample", "读取材料正文片段"));
+
     private final ArtifactProperties props;
 
     /** 随工具结果回传的预览行数(模型与前端都不看全量,全量在 CSV 里) */
@@ -367,21 +442,25 @@ public class ArtifactService {
     private record Names(String id, String fileName) {
     }
 
-    /** 文件名 = {工具域}_{具体工具名}_{时间戳}_{8位随机}.csv */
+    /** 文件名 = {工具组中文名}_{工具中文名}_{时间戳}_{8位随机}.csv */
     private Names buildNames(String toolName) {
         String domain = safeToolPart(toolName, "query");
         String specific = currentToolName();
         String specificSafe = safeToolPart(specific, null);
-        // execute_sql / query_table_data 这类工具:域标识本身就是具体工具名,避免重复
+        // execute_sql / query_table_data 这类工具:域标识本身就是具体工具名,磁盘 id 避免重复
         boolean appendSpecific = specificSafe != null && !specificSafe.equalsIgnoreCase(domain);
 
         String uuid = UUID.randomUUID().toString().replace("-", "");
         String suffix = "_" + LocalDateTime.now().format(ID_TS) + "_"
                 + uuid.substring(0, 8) + ".csv";
         String idBase = appendSpecific ? domain + "_" + specificSafe : domain;
-        String displayBase = artifactLabel(toolName);
-        if (appendSpecific) {
-            displayBase = displayBase + "_" + specificSafe;
+
+        // 展示名:工具组中文名 + 工具中文名(未登记的工具回退原始 @Tool 名)
+        String domainLabel = artifactLabel(toolName);
+        String actionLabel = displayToolPart(artifactActionLabel(specific));
+        String displayBase = domainLabel;
+        if (!actionLabel.isEmpty() && !actionLabel.equals(domainLabel)) {
+            displayBase = displayBase + "_" + actionLabel;
         }
         return new Names(idBase + suffix, displayBase + suffix);
     }
@@ -443,6 +522,27 @@ public class ArtifactService {
         return safe.isEmpty() ? fallback : safe.toString();
     }
 
+    /** 具体工具的中文名;未登记时回退原始 @Tool 名(至少还能区分) */
+    private static String artifactActionLabel(String toolName) {
+        if (toolName == null) {
+            return "";
+        }
+        String trimmed = toolName.trim();
+        return ARTIFACT_ACTION_LABELS.getOrDefault(trimmed, trimmed);
+    }
+
+    /** 展示文件名片段清洗:去掉 Windows 非法字符与空白,最长 40 字符 */
+    private static String displayToolPart(String value) {
+        if (value == null || value.isBlank()) {
+            return "";
+        }
+        String cleaned = value.trim()
+                .replaceAll("[\\\\/:*?\"<>|\\r\\n\\t]", "_")
+                .replaceAll("\\s+", "")
+                .replaceAll("_+", "_");
+        return cleaned.length() > 40 ? cleaned.substring(0, 40) : cleaned;
+    }
+
     /**
      * 工具标识 → 产物文件名的中文前缀。
      *

+ 2 - 2
ai-server/src/test/java/com/zsjz/ai/module/agent/artifact/ArtifactServiceTest.java

@@ -153,8 +153,8 @@ class ArtifactServiceTest {
         assertTrue(id.matches("call_analysis_stat_call_night_detail_\\d{14}_[0-9a-f]{8}\\.csv"),
                 "磁盘 id 应带具体工具名且保持 ASCII: " + id);
         String fileName = (String) meta.get("fileName");
-        assertTrue(fileName.startsWith("通话分析_stat_call_night_detail_"),
-                "展示/下载名应带具体工具名: " + fileName);
+        assertTrue(fileName.startsWith("通话分析_夜间通话明细_"),
+                "展示/下载名应为「工具组中文名_工具中文名」: " + fileName);
         track(id);
     }