cc 1 week ago
parent
commit
e20edeec17

+ 1 - 104
ai-electron/frontend/src/ai/components/ChatMessageItem.vue

@@ -45,6 +45,7 @@
             :steps="message.toolSteps"
             :trace="message.trace"
             :active="!!message.streaming"
+            :message-id="message.id"
           />
 
           <!-- 完全没有过程时才单独提示阶段,避免和时间线的汇总行重复 -->
@@ -75,25 +76,6 @@
               <div class="ai-skeleton" :style="{ height: '14px', width: '46%' }"></div>
             </div>
           </template>
-
-          <!-- 本条消息产出的 CSV 产物:产物栏收起时也能在消息里看到并下载 -->
-          <div v-if="artifacts.length" class="ai-msg__arts">
-            <button
-              v-for="a in artifacts"
-              :key="a.id"
-              class="ai-artifact"
-              type="button"
-              :title="`${a.fileName} · 点击预览前 200 行`"
-              @click="openArtifactPreview(a)"
-            >
-              <Icon icon="mdi:file-delimited-outline" :size="14" />
-              <span class="name">{{ a.fileName }}</span>
-              <span v-if="artifactRowsText(a)" class="rows">{{ artifactRowsText(a) }}</span>
-              <span class="dl" title="下载完整 CSV" @click.stop="downloadOne(a)">
-                <Icon icon="mdi:download-outline" :size="14" />
-              </span>
-            </button>
-          </div>
         </div>
 
         <!-- 追问建议 -->
@@ -131,14 +113,6 @@
   import { computed, ref } from 'vue';
   import type { ChatMessage } from '../store/chatStream';
   import Icon from '@/core/components/Icon/src/Icon.vue';
-  import { useMessage } from '@/core/hooks/web/useMessage';
-  import { downloadArtifactFile } from '../api/artifactApi';
-  import {
-    artifactRowsText,
-    extractMessageArtifacts,
-    openArtifactPreview,
-    type SessionArtifact,
-  } from '../utils/artifacts';
   import BlockRenderer from './BlockRenderer.vue';
   import ProcessTimeline from './ProcessTimeline.vue';
   import StreamStatusBar from './StreamStatusBar.vue';
@@ -169,21 +143,6 @@
   /** 是否存在过程记录(有思考或有工具调用)——决定是否显示时间线、是否还需要独立阶段提示 */
   const hasTrace = computed(() => !!props.message.thinking?.trim() || !!props.message.toolSteps?.length);
 
-  const { createMessage } = useMessage();
-
-  /** 本条消息产出的 CSV 产物(从工具结果的 artifact 字段提取) */
-  const artifacts = computed(() => extractMessageArtifacts(props.message));
-
-  /** 卡片上的下载按钮:全量 CSV(与产物栏同一条链路) */
-  async function downloadOne(artifact: SessionArtifact) {
-    try {
-      await downloadArtifactFile(artifact);
-      createMessage.success(`已开始下载 ${artifact.fileName}`);
-    } catch (err: any) {
-      createMessage.error(err?.message || '下载失败');
-    }
-  }
-
   const timeText = computed(() => {
     const raw = props.message.createAt;
     if (!raw) return '';
@@ -318,67 +277,5 @@
       flex-direction: column;
       gap: var(--ai-sp-2);
     }
-
-    /* 消息内产物卡片:文件名 + 行数 + 行内下载,点击进预览 */
-    &__arts {
-      display: flex;
-      flex-wrap: wrap;
-      gap: var(--ai-sp-2);
-      margin-top: var(--ai-sp-3);
-    }
-  }
-
-  .ai-artifact {
-    display: inline-flex;
-    align-items: center;
-    gap: 7px;
-    max-width: 100%;
-    height: 30px;
-    padding: 0 6px 0 10px;
-    font-size: 12.5px;
-    color: var(--ai-text);
-    background: var(--ai-bg-subtle);
-    border: 1px solid var(--ai-border);
-    border-radius: 99px;
-    cursor: pointer;
-    transition: border-color 0.15s ease;
-
-    &:hover {
-      border-color: var(--ai-primary);
-
-      .dl {
-        opacity: 1;
-      }
-    }
-
-    .name {
-      overflow: hidden;
-      max-width: 320px;
-      font-weight: 600;
-      white-space: nowrap;
-      text-overflow: ellipsis;
-    }
-
-    .rows {
-      font-variant-numeric: tabular-nums;
-      color: var(--ai-text-2);
-    }
-
-    .dl {
-      display: inline-flex;
-      align-items: center;
-      justify-content: center;
-      width: 22px;
-      height: 22px;
-      color: var(--ai-text-2);
-      background: transparent;
-      border-radius: 50%;
-      opacity: 0.7;
-      transition: opacity 0.12s ease;
-
-      &:hover {
-        color: var(--ai-primary);
-      }
-    }
   }
 </style>

+ 3 - 1
ai-electron/frontend/src/ai/components/ProcessTimeline.vue

@@ -54,7 +54,7 @@
           </button>
 
           <div v-if="isStepOpen(item.key)" class="ai-trace__detail">
-            <ToolCallPanel :step="item.step" />
+            <ToolCallPanel :step="item.step" :message-id="messageId" />
           </div>
         </template>
       </template>
@@ -102,6 +102,8 @@
       trace?: TraceNode[];
       /** 所属消息是否仍在流式输出 */
       active?: boolean;
+      /** 所属消息 ID(产物卡片预览/定位用) */
+      messageId?: string;
     }>(),
     { thinking: '', thinkingActive: false, steps: () => [], trace: () => [], active: false },
   );

+ 111 - 12
ai-electron/frontend/src/ai/components/ToolCallPanel.vue

@@ -28,27 +28,31 @@
       <pre v-else class="ai-tool-detail__pre ai-scroll-area"><code>{{ pretty(step.input) }}</code></pre>
     </div>
 
-    <!-- 输出:表格类结果已自动导出为产物文件,这里只给指引;其余结果仍结构化渲染 -->
+    <!-- 输出:表格类结果与「产物文件卡片」成对出现(原表格位置就是产物位置) -->
     <div v-if="step.result" class="ai-tool-detail__block">
       <div class="ai-tool-detail__label">
         <span>输出</span>
-        <span v-if="artifact" class="ai-tool-detail__meta">
-          已导出为产物文件<template v-if="artifactRowsText">({{ artifactRowsText }})</template>
-        </span>
-        <span v-else-if="table && table.totalRows != null" class="ai-tool-detail__meta">
-          共 {{ table.totalRows.toLocaleString() }} 行
-        </span>
-        <span v-else-if="table && table.rows.length" class="ai-tool-detail__meta">{{ table.rows.length }} 条</span>
-        <span v-if="truncated" class="ai-tool-detail__badge">结果过大已截断</span>
+        <span v-if="truncated" class="ai-tool-detail__badge">仅回传头部预览行</span>
         <span class="ai-tool-detail__spacer"></span>
         <button v-if="table" class="ai-tool-detail__btn" type="button" @click="showRaw = !showRaw">
           {{ showRaw ? '表格' : '原始 JSON' }}
         </button>
       </div>
 
+      <!-- 产物文件卡片:全量 CSV,可预览前 200 行 / 下载完整文件 -->
       <div v-if="artifact && !showRaw" class="ai-tool-detail__artifact">
-        <Icon icon="mdi:file-delimited-outline" :size="14" />
-        <span>结果已导出为 CSV 产物文件,在下方产物卡片或右侧产物栏中预览 / 下载</span>
+        <span class="ai-tool-detail__artifact-icon">
+          <Icon icon="mdi:file-delimited-outline" :size="15" />
+        </span>
+        <span class="ai-tool-detail__artifact-name" :title="artifact.fileName">{{ artifact.fileName }}</span>
+        <span v-if="artifactRowsText" class="ai-tool-detail__artifact-meta">{{ artifactRowsText }}</span>
+        <span class="ai-tool-detail__artifact-sp"></span>
+        <button type="button" title="预览前 200 行" @click="onPreview">
+          <Icon icon="mdi:eye-outline" :size="14" />
+        </button>
+        <button type="button" title="下载完整 CSV" @click="onDownload">
+          <Icon icon="mdi:download-outline" :size="14" />
+        </button>
       </div>
 
       <a-table
@@ -93,8 +97,11 @@
   import { Table } from 'ant-design-vue';
   import type { TableColumnsType, TablePaginationConfig } from 'ant-design-vue';
   import Icon from '@/core/components/Icon/src/Icon.vue';
+  import { useMessage } from '@/core/hooks/web/useMessage';
   import type { ToolStep } from '../api/types';
   import { parseJsonPayload } from '../utils/parseMessageContent';
+  import { downloadArtifactFile } from '../api/artifactApi';
+  import { openArtifactPreview } from '../utils/artifacts';
 
   const ATable = Table;
 
@@ -103,7 +110,16 @@
   /** 入参里的 SQL 超过这个行数默认折叠 */
   const SQL_COLLAPSE_LINES = 4;
 
-  const props = defineProps<{ step: ToolStep }>();
+  const props = withDefaults(
+    defineProps<{
+      step: ToolStep;
+      /** 所属消息 ID(产物预览/定位用;时间线详情里可为空) */
+      messageId?: string;
+    }>(),
+    { messageId: '' },
+  );
+
+  const { createMessage } = useMessage();
 
   const showRaw = ref(false);
   const showInputRaw = ref(false);
@@ -138,6 +154,26 @@
     typeof artifact.value?.totalRows === 'number' ? `${artifact.value.totalRows.toLocaleString()} 行` : '',
   );
 
+  /** 与本步骤成对的产物对象(预览/下载用) */
+  const sessionArtifact = computed(() => {
+    if (!artifact.value) return null;
+    return { ...artifact.value, messageId: props.messageId, toolName: props.step.toolName };
+  });
+
+  function onPreview() {
+    if (sessionArtifact.value) openArtifactPreview(sessionArtifact.value);
+  }
+
+  async function onDownload() {
+    if (!sessionArtifact.value) return;
+    try {
+      await downloadArtifactFile(sessionArtifact.value);
+      createMessage.success(`已开始下载 ${sessionArtifact.value.fileName}`);
+    } catch (err: any) {
+      createMessage.error(err?.message || '下载失败');
+    }
+  }
+
   const table = computed<DetailTable | null>(() => {
     if (!props.step.result || payload.value === undefined || artifact.value) return null;
     return buildTable(payload.value);
@@ -316,6 +352,69 @@
       font-size: 12px;
     }
 
+    /* 与工具结果成对的产物文件卡片(原表格位置) */
+    &__artifact {
+      display: flex;
+      align-items: center;
+      gap: 8px;
+      padding: 7px 8px;
+      font-size: 12.5px;
+      color: var(--ai-text);
+      background: var(--ai-bg-subtle);
+      border: 1px solid var(--ai-border);
+      border-radius: 8px;
+
+      &-icon {
+        display: inline-flex;
+        align-items: center;
+        justify-content: center;
+        flex: 0 0 auto;
+        width: 26px;
+        height: 26px;
+        color: var(--ai-primary);
+        background: var(--ai-primary-soft);
+        border-radius: 6px;
+      }
+
+      &-name {
+        overflow: hidden;
+        min-width: 0;
+        font-weight: 600;
+        white-space: nowrap;
+        text-overflow: ellipsis;
+      }
+
+      &-meta {
+        flex: 0 0 auto;
+        font-variant-numeric: tabular-nums;
+        color: var(--ai-text-2);
+      }
+
+      &-sp {
+        flex: 1 1 auto;
+      }
+
+      button {
+        display: inline-flex;
+        align-items: center;
+        justify-content: center;
+        flex: 0 0 auto;
+        width: 24px;
+        height: 24px;
+        padding: 0;
+        color: var(--ai-text-2);
+        background: transparent;
+        border: none;
+        border-radius: 6px;
+        cursor: pointer;
+
+        &:hover {
+          color: var(--ai-primary);
+          background: var(--ai-bg-page);
+        }
+      }
+    }
+
     &__empty {
       font-size: 11px;
       color: var(--ai-text-2);

+ 6 - 1
ai-electron/frontend/src/ai/utils/artifacts.ts

@@ -93,7 +93,12 @@ export function artifactSizeText(artifact: SessionArtifact): string {
 export const previewArtifact = ref<SessionArtifact | null>(null);
 
 export function openArtifactPreview(artifact: SessionArtifact): void {
-  previewArtifact.value = artifact;
+  previewArtifact.value = normalizeArtifact(artifact);
+}
+
+/** 数值字段兜底归一化(调用方传原始 artifact 时保证展示层拿到 number) */
+function normalizeArtifact(artifact: SessionArtifact): SessionArtifact {
+  return { ...artifact, totalRows: toCount(artifact.totalRows), sizeBytes: toCount(artifact.sizeBytes) };
 }
 
 export function closeArtifactPreview(): void {

+ 9 - 4
ai-server/.agentscope/users/1/workspaces/5/agents/1/default/MEMORY.md

@@ -11,15 +11,20 @@ Earlier sessions (~03:25–04:00) returned code=400 on all three data entry laye
 
 **Workspace context:** workspaceId=5, name "AI 对话测试" (AI conversation test), description field empty — signature consistent with an un-loaded formal case.
 
-### Data channel RESTORED (≈04:08)
-Unlike prior 400-error sessions, `get_call_records` succeeded and real call-detail data was retrieved. Prior gate appears resolved or session-specific.
+### Data channel RESTORED (≈04:08) — channel stable
+Unlike prior 400-error sessions, `get_call_records` succeeded and real call-detail data was retrieved. Prior gate appears resolved or session-specific. **Data-read channel remains accessible** — a repeat query at ~05:51 (`pageSize=20`, `callDateTime` DESC) succeeded without the code=400 gate, confirming channel stability across sessions.
 
-**Retrieval specifics:** query completed for `pageSize=20`, sorted by `callDateTime` DESC; total case records = **38,263 rows across 1914 pages**; CSV artifact `call_analysis_20260928120148_76eedc24.csv` (~5.4MB / 5,437,351 bytes); resultId `9060ac11-d267-4a88-ad50-5eee736d9386`.
+**Retrieval specifics:** query completed for `pageSize=20`, sorted by `callDateTime` DESC; total case records = **38,263 rows across 1914 pages**; CSV artifacts:
+- `call_analysis_20260928120148_76eedc24.csv` (~5.4MB / 5,437,351 bytes, timestamp 12:01)
+- `call_analysis_20260928134916_5d851f61.csv` (~5.4MB, timestamp 13:49:16) — distinct from the prior export
+
+resultId `9060ac11-d267-4a88-ad50-5eee736d9386`.
 
 ### Actual subjects (from live data — not metadata guesses)
 - **吴得福** — 机主/primary subject, 中国移动; phones 13948567895 & 13648652680; certNo 513126197412174415
 - **刘术** — 四川电信; phones 13888856054 & 13861904573
 - **许天** — 中国移动; phone 13832954621
+- **周海** — NEW fourth subject identified in the ~05:51 repeat query's first 20 rows (previously only three subjects documented); specifics not yet detailed.
 
 ### Anomaly patterns (first 20 rows, dates 2025-12-26 → 12-30)
 - Three-person closed-loop interplay.
@@ -38,6 +43,6 @@ Layered error responses expose system package structure — useful for future de
 ### Recommended next drill-downs (case now loaded)
 1. Night-call专项 for 吴得福 — deep-night window analysis.
 2. Base-station (基站) trajectory collision analysis.
-3. Financial/transaction tracing定位 of 刘术 & 许天.
+3. Financial/transaction tracing定位 of 刘术 & 许天 (and 周海 once details established).
 - Operational principle: per-subject queries over full-case blind scan to reduce load; confirm subject list via `list_persons` before drilling.
 - User awaiting decision on which drill-down to continue ("需要我继续下钻哪一项?") — no further instruction in this session.

File diff suppressed because it is too large
+ 0 - 0
ai-server/.agentscope/users/1/workspaces/5/agents/1/default/agents/数刃/sessions/4311687975235309032.jsonl


File diff suppressed because it is too large
+ 0 - 0
ai-server/.agentscope/users/1/workspaces/5/agents/1/default/agents/数刃/sessions/4311687975235309032.log.jsonl


+ 2 - 2
ai-server/.agentscope/users/1/workspaces/5/agents/1/default/agents/数刃/sessions/sessions.json

@@ -1,8 +1,8 @@
 {
   "sessions" : {
     "4311687975235309032" : {
-      "summary" : "transcript updated (9 entries)",
-      "updatedAt" : "2026-09-28T05:49:57.026003900Z"
+      "summary" : "transcript updated (33 entries)",
+      "updatedAt" : "2026-09-28T06:12:48.751344300Z"
     }
   },
   "version" : 1

+ 1 - 1
ai-server/.agentscope/users/1/workspaces/5/agents/1/default/memory/.consolidation_state

@@ -1 +1 @@
-2026-09-28T05:49:57.002465700Z
+2026-09-28T06:12:48.727290100Z

+ 7 - 0
ai-server/.agentscope/users/1/workspaces/5/agents/1/default/memory/2026-09-28.md

@@ -29,3 +29,10 @@
 - New call-detail retrieval completed this session: `get_call_records` with pageSize=20, sorted by callDateTime DESC; total case records = 38,263 rows; **new CSV artifact** `call_analysis_20260928134916_5d851f61.csv` (~5.4MB) — distinct from prior export `call_analysis_20260928120148_76eedc24.csv` (earlier timestamp 12:01 vs this one's 13:49:16)
 - **New subject identified**: first 20 rows now cover four subjects — 吴得福, 许天, 刘术, and **周海** (previously only three subjects were documented in prior sessions); 周海's specifics not yet detailed
 - Call-detail query succeeded without the code=400 gate this session (repeat of the same query as prior restored-channel session), confirming data-read channel remains accessible
+
+## Memory Flush — 2026-09-28T06:14:56.258949100Z
+- Call-record query window (first 20 rows, callDateTime DESC): assistant repeatedly invoked `get_person_call_profile` (person-group tool) for subjects 吴得福/刘术/许天 before finally executing the requested `get_call_records` (call-group tool) query with pageSize=20 at session end — tool-routing mismatch worth noting for future sessions
+- `get_person_call_profile` output structure: single-row CSV artifacts (~235–251 bytes); personName field is **null** in results, keyed instead by operator phone map (operCountMap); columns include phoneCount, callCount, otherCount, outCount, inCount, callRatioMap, callDateStart/callDateEnd
+- Call-count aggregates per subject from person profile: 吴得福 = 954 calls (out 554 / in 400, other 19); 刘术 = 924 (out 515 / in 409, other 187); 许天 = 489 (out 248 / in 241, other 24) — new aggregate data not documented in prior sessions
+- 吴得福's callDateStart shows placeholder date "1970-01-01" in person-profile output — new confirmation of placeholder-data anomaly beyond earlier test/placeholder-number findings in call-detail records
+- Repeated `get_person_call_profile` calls generated distinct tiny CSV artifacts each time (IDs: person_analysis_20260928141029, 141132, 141137, 141146, 141153, 141159, 141205, 141212, 141218, 141224)

+ 2 - 3
ai-server/src/main/java/com/zsjz/ai/module/agent/service/AgentService.java

@@ -310,9 +310,8 @@ public class AgentService extends ServiceImpl<AgentMapper, AgentEntity> {
                 
                 ## 回复输出格式契约(前端渲染依赖,必须遵守)
                 聊天前端只把以下两种围栏代码块渲染为交互组件,其余代码块按普通文本展示:
-                1. 查询/分析出的数据明细**不要**在回复里罗列,也**不要**把工具返回的 JSON(全部或片段)放进任何围栏块或代码块:
-                   表格类工具(execute_sql 与各业务分析工具)的结果已自动导出为 CSV 产物文件,前端会在「产物栏」展示,用户可预览前 200 行、下载全量。
-                   回复正文用文字总结关键结论(总数、排名、趋势、异常点等),需要举例时引用个别行的关键字段即可(一两行、几个字段,不要整表搬运)。
+                1. 表格类工具(execute_sql 与各业务分析工具)的数据结果会自动导出为 CSV 产物文件,与该次工具调用**成对**展示在「过程」详情里,用户可预览前 200 行、下载全量。
+                   报告正文如需呈现数据,直接写 Markdown 表格(第一行是表头,用 | 分隔列,前端会渲染为表格):把你要展示的行**完整给出**,不要省略列、不要用"……"截断;全量数据以 CSV 产物为准。
                    ★ 业务口径优先用专用工具:它们的统计口径(未知对手/未知卡号、现金关键词、夜间/连续/敏感通话、大额/共同/相互交易、碰面时间窗口、同住推导、同行比对等)已固化,不要用 execute_sql 手写 SQL 重算同一口径;只有专用工具覆盖不到的场景才自己写 SQL。
                    ★ 专用工具按业务域分成 6 组:person(人员画像与亲密度)、call(通话明细)、trans(交易明细与资金)、track(轨迹/出行/快递)、otg(全文检索与行为时间轴)、graph(关系图谱)。当前会话默认只装备 person 组。
                      当你要用的工具不在当前可见工具列表里时,先调用 reset_equipped_tools 把需要的组名一起写进 to_activate —— 它是**全量替换**语义,不在列表里的组会被卸载,所以要么把当前还要用的组一并列出,要么用完立刻切回。不要把无关组一起打开,会浪费上下文。

Some files were not shown because too many files changed in this diff