Эх сурвалжийг харах

feat: 工具界面中文化、模式切换记忆各自停留位置

工具名中文化(展示与调用分离):
- 「插件与技能 → 工具」、工具包编辑器与工具详情弹框一律显示中文名,复用
  utils/constants 的 TOOL_LABELS(已覆盖后端全部 64 个 @Tool,差集为空);
  界面上不再出现英文工具名,搜索仍同时匹配中英文。
- agent 调用不受影响:调用名仍是 @Tool(name) 的英文标识,页面显示名与它无关。

自主 ⇄ 智能 模式切换:
- 各模式记住自己的停留位置 —— 传统模式记路由、智能工作台记内部视图(sessionStorage),
  切回时恢复到原处,不再每次都掉回默认落点;工作台的会话与消息本来就由全局 store 保留。
- 传统模式下点自己那一侧仍只对齐偏好、不跳转。

技能详情弹框:只读输入框/文本域底纹置灰,与可编辑态区分。

同时带上在途改动:智能工作台移除「文件管理」视图(并删除 ai/views/aiCaseData 两个副本页)。
cc 3 өдөр өмнө
parent
commit
1f7ae248a9

+ 2 - 2
ai-electron/frontend/src/ai/components/AgentFormModal.vue

@@ -90,7 +90,7 @@
                 <div class="ae-form__card-desc">{{ p.description || '(无描述)' }}</div>
                 <div class="ae-form__chips">
                   <span>{{ p.toolCount }} 个工具</span>
-                  <span v-for="n in p.toolNames.slice(0, 2)" :key="n">{{ n }}</span>
+                  <span v-for="n in p.toolNames.slice(0, 2)" :key="n" :title="n">{{ toolLabel(n) }}</span>
                   <span v-if="p.toolNames.length > 2">+{{ p.toolNames.length - 2 }}</span>
                 </div>
               </div>
@@ -181,7 +181,7 @@
   import * as capabilityApi from '../api/capabilityApi';
   import * as toolPackApi from '../api/toolPackApi';
   import type { AiExpert, SaveExpertParams, Skill, ToolPack } from '../api/types';
-  import { toolGroupLabel } from '../utils/constants';
+  import { toolGroupLabel, toolLabel } from '../utils/constants';
 
   const AForm = Form;
   const AFormItem = FormItem;

+ 18 - 0
ai-electron/frontend/src/ai/components/SkillDetailModal.vue

@@ -213,6 +213,24 @@
       color: var(--ai-text-2, #656d76);
     }
 
+    /* 只读态:输入框 / 文本域底纹置灰(沿用项目的输入框底色 token),
+       一眼区分「看」与「改」;hover / focus 也不再变色,避免像可编辑。 */
+    :deep(.ant-input) {
+      &[readonly] {
+        color: var(--ai-text, #24292f);
+        cursor: default;
+        background: var(--ai-bg-subtle, #f5f5f5);
+        border-color: var(--ai-border, #eaecef);
+
+        &:hover,
+        &:focus {
+          background: var(--ai-bg-subtle, #f5f5f5);
+          border-color: var(--ai-border, #eaecef);
+          box-shadow: none;
+        }
+      }
+    }
+
     &__badge {
       margin-left: 4px;
       padding: 0 5px;

+ 0 - 109
ai-electron/frontend/src/ai/views/aiCaseData/AiCaseDataPage.vue

@@ -1,109 +0,0 @@
-<template>
-  <div class="ai-casedata">
-    <ImportList :active="true" />
-  </div>
-</template>
-
-<script lang="ts" setup name="AiCaseDataPage">
-  /**
-   * 智能工作台「文件管理」视图:只承载「导入文件管理」表格
-   * (操作列仅 删除/下载,见 CaseImportList.vue 智能侧副本)。
-   *
-   * 与传统侧 case/views/data/index.vue 的整页(导入数据树 / 清洗数据 / 数据治理 / 治理设置)
-   * 无关——传统侧保持原样,这里是独立的智能侧副本。表格直接铺开,不做卡片包裹。
-   */
-  import ImportList from './CaseImportList.vue';
-</script>
-
-<style lang="less" scoped>
-  .ai-casedata {
-    display: flex;
-    flex-direction: column;
-    height: 100%;
-    min-height: 0;
-    padding: var(--ai-sp-2);
-    background: var(--ai-bg-page);
-    overflow: hidden;
-
-    :deep(.case-import-list) {
-      flex: 1 1 auto;
-      min-height: 0;
-    }
-  }
-</style>
-
-<style lang="less">
-  /* 子组件内部类名只能用全局选择器,全部限定在本视图根下 */
-
-  /* 查询表单顶部 1px 描边来自核心 BasicTable 的 !important 规则,这里同强度覆盖(仅本视图) */
-  .ai-casedata .jeesite-basic-table-form-container .ant-form {
-    border-top: 0 !important;
-    background: #fff !important;
-  }
-
-  /* 表头工具条容器(当前页/已选 + 隐藏 + 设置齿轮)整体移除 */
-  .ai-casedata .jeesite-basic-table-header-container {
-    display: none;
-  }
-
-  /* 表格观感:外层不套卡片(无白底/描边/圆角);
-     表格本体 / 表头 / 滚动表体 保持组件原色,不做透明化 */
-  .ai-casedata .ant-table-wrapper {
-    background: transparent !important;
-    border: none;
-    border-radius: 0;
-    display: flex;
-    flex-direction: column;
-    flex: 1 1 auto;
-    min-height: 0;
-  }
-
-  /*
-   * 表体吃满表格区剩余高度:组件按传统布局算死的内联 max-height 在工作台里矮一截,
-   * 会在尾部留出空带 —— 用弹性高度中和(!important 盖内联样式),滚动只发生在表体内。
-   */
-  .ai-casedata .ant-table-container {
-    flex: 1 1 auto;
-    min-height: 0;
-    display: flex;
-    flex-direction: column;
-  }
-
-  .ai-casedata .ant-table-body {
-    flex: 1 1 auto;
-    min-height: 0;
-    max-height: none !important;
-    overflow: auto !important;
-    /* 底部横向滚动条不显示(表格总宽略有溢出时也不露滚动条) */
-    scrollbar-width: none;
-  }
-
-  .ai-casedata .ant-table-body::-webkit-scrollbar {
-    height: 0;
-    width: 0;
-    display: none;
-  }
-
-  /*
-   * 空态(暂无数据):占位行要铺满整个表体——组件给 expanded-row-fixed 写死了内联高度,
-   * 会矮一截,行尾留分割线、行下留白块。这里用 :has 定位空态表体,撑满高度、
-   * 去掉行尾分割线,并把空态提示垂直居中。
-   */
-  .ai-casedata .ant-table-body table:has(.ant-table-placeholder) {
-    height: 100% !important;
-  }
-
-  .ai-casedata .ant-table-tbody:has(.ant-table-placeholder),
-  .ai-casedata .ant-table-placeholder > td {
-    height: 100% !important;
-    border-bottom: none !important;
-  }
-
-  .ai-casedata .ant-table-placeholder .ant-table-expanded-row-fixed {
-    height: 100% !important;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    overflow: visible !important;
-  }
-</style>

+ 0 - 698
ai-electron/frontend/src/ai/views/aiCaseData/CaseImportList.vue

@@ -1,698 +0,0 @@
-<template>
-  <div :class="prefixCls" class="case-import-list">
-    <BasicTable class="import-list-table" @register="registerTable" @fetch-success="handleFetchSuccess">
-      <!-- 多选删除:选中行后出现在查询表单「重置」按钮后面 -->
-      <template #form-advanceAfter>
-        <Popconfirm title="是否确认删除选中的导入记录?" @confirm="handleDeleteSelected()">
-          <a-button v-if="selectedRowKeysRef.length > 0" danger class="batch-delete-btn" type="default">
-            <Icon icon="i-ant-design:delete-outlined" color="error" /> 删除 ({{ selectedRowKeysRef.length }})
-          </a-button>
-        </Popconfirm>
-      </template>
-    </BasicTable>
-
-    <div class="pagination-bar">
-      <AnalysisPagination
-        :total="pagination.total"
-        :current="pagination.current"
-        :page-size="pagination.pageSize"
-        :page-size-options="pageSizeOptions"
-        :loading="paginationLoading"
-        @change="handlePaginationChange"
-      />
-    </div>
-
-    <CleanLogModal v-model:open="logModalOpen" title="导入失败日志" :file-infos="currentLogFileInfos" />
-  </div>
-</template>
-<script lang="ts" setup name="ViewsSysEmpUserList">
-  import { h, onBeforeUnmount, onMounted, ref, watch } from 'vue';
-  import type { PropType } from 'vue';
-  import { Popconfirm } from 'ant-design-vue';
-  import AnalysisPagination from '@/core/components/AnalysisPagination/src/AnalysisPagination.vue';
-  import { Icon } from '@/core/components/Icon';
-  import { BasicTable, BasicColumn, useTable } from '@/core/components/Table';
-  import { useDesign } from '@/core/hooks/web/useDesign';
-  import { useGo } from '@/core/hooks/web/usePage';
-  import { useMessage } from '@/core/hooks/web/useMessage';
-  import { useEmitter } from '@/core/store/modules/case';
-  import { PageEnum } from '@/core/enums/pageEnum';
-  import type { FormProps } from '@/core/components/Form';
-  import type { FileInfo } from '@/types';
-  import { importFileList, deletedFileList, preFile } from '@/case/api/govern/governApi';
-  import { getOpenOfficeFileErrorMessage, downloadOfficeFileById } from '@/core/utils/openOfficeFile';
-  import { FileStatusEnum } from '@/case/types/enum';
-  import componentSetting from '@/core/settings/componentSetting';
-  import CleanLogModal from '@/case/views/data/components/CleanLogModal.vue';
-  import { resetImportProgressSession } from '@/case/views/data/importProgressSession';
-
-  const props = defineProps({
-    treeCodes: Array as PropType<string[]>,
-    treeName: String,
-    active: {
-      type: Boolean,
-      default: false,
-    },
-  });
-
-  const { prefixCls } = useDesign('case-import-list');
-  const go = useGo();
-  const emitter = useEmitter();
-  const { createMessage } = useMessage();
-  const CASE_DATA_REFRESH_EVENT = 'case-data:refresh';
-  const CASE_IMPORT_FILE_REFRESH_EVENT = 'case-import-file:refresh';
-  const FILE_INFO_STORAGE_KEY = 'case_clean_file_infos';
-  const IMPORT_FILE_INFO_STORAGE_KEY = 'case_clean_import_file_infos';
-  const RECLEAN_CONTEXT_STORAGE_KEY = 'case_clean_reclean_context';
-  const PRE_FILE_EMPTY_MESSAGE = '未识别到可处理文件,可能是文件重复、所有文件不存在,或文件格式不正确';
-  const ctrlPermi = ref('');
-  const currentPageFileCount = ref(0);
-  const logModalOpen = ref(false);
-  const currentLogFileInfos = ref<FileInfo[]>([]);
-  const hasLoadedOnActivate = ref(false);
-  const recleaningFileIds = ref<string[]>([]);
-  const paginationLoading = ref(false);
-  const pagination = ref({
-    current: 1,
-    pageSize: Number(componentSetting.table.defaultPageSize),
-    total: 0,
-  });
-  const pageSizeOptions = componentSetting.table.pageSizeOptions.map((value) => ({
-    label: value,
-    value: Number(value),
-  }));
-
-  const normalizeSearchParams = (params: Recordable = {}) => {
-    const nextParams: Recordable = { ...params };
-    Object.keys(nextParams).forEach((key) => {
-      const value = nextParams[key];
-      if (value === '' || value === undefined || value === null || (Array.isArray(value) && value.length === 0)) {
-        delete nextParams[key];
-      }
-    });
-    return nextParams;
-  };
-
-  const handleSearchSubmit = async () => {
-    const formValues = tableAction.getForm()?.getFieldsValue?.() || {};
-    await tableAction.reload({
-      searchInfo: normalizeSearchParams(formValues),
-      page: 1,
-    });
-  };
-
-  const searchForm: FormProps<FileInfo> = {
-    baseColProps: { md: 12, lg: 8, xl: 6 },
-    labelWidth: 90,
-    showAdvancedButton: false,
-    submitFunc: handleSearchSubmit,
-    schemas: [
-      {
-        label: '文件名称:',
-        field: 'fileName',
-        component: 'Input',
-        componentProps: {
-          placeholder: '请输入文件名称',
-        },
-      },
-      {
-        label: '上传时间:',
-        field: 'createDateTime',
-        component: 'DatePicker',
-        componentProps: {
-          format: 'YYYY-MM-DD',
-          valueFormat: 'YYYY-MM-DD',
-          placeholder: '请选择上传时间',
-        },
-      },
-    ],
-  };
-
-  const getText = (value: unknown, fallback = '-') => {
-    const text = String(value ?? '').trim();
-    return text ? text : fallback;
-  };
-
-  const getNumberText = (value: unknown) => {
-    const num = Number(value ?? 0);
-    return Number.isFinite(num) ? String(num) : '0';
-  };
-
-  const fileStatusLabelMap = Object.entries(FileStatusEnum).reduce(
-    (acc, [key, label]) => {
-      acc[key] = label;
-      return acc;
-    },
-    {} as Record<string, string>,
-  );
-
-  const getGovernStatusLabel = (status?: string | null) => {
-    const normalized = String(status || '').trim();
-    if (!normalized) return '未治理';
-    return fileStatusLabelMap[normalized] || normalized;
-  };
-
-  const getErrorMessage = (error: any, fallback = '操作失败') =>
-    error?.response?.data?.message || error?.message || fallback;
-
-  const tableColumns: BasicColumn<FileInfo>[] = [
-    {
-      title: '序号',
-      key: 'sort',
-      width: 76,
-      align: 'center',
-      customRender: ({ index }: { index: number }) => h('span', { class: 'table-cell-center' }, String(index + 1)),
-    },
-    {
-      title: '文件名称',
-      dataIndex: 'fileName',
-      key: 'fileName',
-
-      align: 'center',
-      ellipsis: true,
-      customRender: ({ record }: { record: FileInfo }) =>
-        h(
-          'span',
-          {
-            class: 'file-name-text',
-            title: getText(record.fileName || record.sheetName, '未命名文件'),
-          },
-          getText(record.fileName || record.sheetName, '未命名文件'),
-        ),
-    },
-    {
-      title: '导入成功',
-      dataIndex: 'successNum',
-      key: 'successNum',
-      // 表头文字实测 56px + 单元格内边距 16px,76 足够完整展示数字与表头
-      width: 76,
-      align: 'center',
-      customRender: ({ value }: { value?: string | number }) =>
-        h('span', { class: 'table-cell-center' }, getNumberText(value)),
-    },
-    {
-      title: '导入失败',
-      dataIndex: 'failNum',
-      key: 'failNum',
-      width: 76,
-      align: 'center',
-      customRender: ({ record }: { record: FileInfo }) =>
-        h(
-          'a',
-          {
-            class: 'fail-link',
-            onClick: (event: MouseEvent) => handleViewFailLog(event, record),
-          },
-          getNumberText(record.failNum),
-        ),
-    },
-    {
-      title: '治理状态',
-      dataIndex: 'fileStatus',
-      key: 'fileStatus',
-      width: 120,
-      align: 'center',
-      customRender: ({ value }: { value?: string | null }) =>
-        h('span', { class: 'govern-status-text' }, getGovernStatusLabel(value)),
-    },
-    {
-      title: '上传时间',
-      dataIndex: 'createDateTime',
-      key: 'createDateTime',
-      // 时间戳文本实测 109px + 单元格内边距/拖拽柄 ≈ 24px,148 保证完整展示
-      width: 148,
-      align: 'center',
-      customRender: ({ value }: { value?: string }) => h('span', { class: 'table-cell-center' }, getText(value)),
-    },
-  ];
-
-  const actionColumn: BasicColumn<FileInfo> = {
-    title: '操作',
-    width: 132,
-    // 不固定右侧(核心表格默认会给操作列 fixed:'right',这里显式覆盖为不固定)
-    fixed: undefined,
-    actions: (record: FileInfo) => [
-      {
-        label: '删除',
-        title: '删除',
-        color: 'error',
-        popConfirm: {
-          title: '是否确认删除文件',
-          confirm: handleDelete.bind(this, record),
-        },
-      },
-      {
-        label: '下载',
-        title: '下载',
-        onClick: handleDownloadFile.bind(this, record),
-      },
-    ],
-  };
-
-  const selectedRowKeysRef = ref<string[]>([]);
-  const rowSelection = {
-    type: 'checkbox',
-    columnWidth: 48,
-    onChange: (selectedRowKeys: string[]) => {
-      selectedRowKeysRef.value = selectedRowKeys;
-    },
-  };
-  const [registerTable, tableAction] = useTable<FileInfo>({
-    api: importFileList,
-    beforeFetch: (params) => {
-      const nextParams = normalizeSearchParams(params);
-      nextParams.ctrlPermi = ctrlPermi.value;
-      return nextParams;
-    },
-    afterFetch: (items: FileInfo[]) => {
-      currentPageFileCount.value = Array.isArray(items) ? items.length : 0;
-      return items;
-    },
-    immediate: false,
-    columns: tableColumns,
-    actionColumn,
-    formConfig: searchForm,
-    showTableSetting: true,
-    useSearchForm: true,
-    showSearchForm: true,
-    canResize: true,
-    isCanResizeParent: true,
-    resizeHeightOffset: 32,
-    clickToRowSelect: false,
-    rowKey: 'id',
-    rowSelection,
-    defaultRowSelection: rowSelection,
-    pagination: { position: ['none'] },
-  });
-  const { clearSelectedRowKeys, reload, redoHeight } = tableAction;
-
-  const handleFetchSuccess = ({ total }: { total?: number }) => {
-    const tablePagination = tableAction.getPagination();
-    if (typeof tablePagination === 'boolean') return;
-
-    pagination.value.current = Number(tablePagination.current) || 1;
-    pagination.value.pageSize = Number(tablePagination.pageSize || tablePagination.defaultPageSize) || 1;
-    pagination.value.total = Number(total ?? tablePagination.total) || 0;
-  };
-
-  const handlePaginationChange = async (page: number, pageSize: number) => {
-    if (paginationLoading.value || (page === pagination.value.current && pageSize === pagination.value.pageSize))
-      return;
-
-    const nextPage = pageSize === pagination.value.pageSize ? page : 1;
-    paginationLoading.value = true;
-    try {
-      tableAction.setPagination({ current: nextPage, pageSize });
-      await reload({ page: nextPage });
-    } finally {
-      paginationLoading.value = false;
-    }
-  };
-
-  const syncTableLayout = () => {
-    if (!props.active) return;
-    requestAnimationFrame(() => {
-      redoHeight();
-    });
-  };
-
-  const handleImportListRefresh = () => {
-    hasLoadedOnActivate.value = true;
-    syncTableLayout();
-    void reload();
-  };
-
-  watch(
-    () => props.active,
-    (active) => {
-      if (!active) return;
-      if (!hasLoadedOnActivate.value) {
-        handleImportListRefresh();
-        return;
-      }
-      syncTableLayout();
-    },
-    { flush: 'post' },
-  );
-
-  onMounted(() => {
-    emitter.on(CASE_DATA_REFRESH_EVENT, handleImportListRefresh);
-    emitter.on(CASE_IMPORT_FILE_REFRESH_EVENT, handleImportListRefresh);
-    // 智能侧嵌入时 active 初始即为 true,watch 只在变化时触发——挂载时直接做首查,
-    // 进入「文件管理」即默认查出数据(与传统页签激活时同一条加载路径)
-    if (props.active) {
-      handleImportListRefresh();
-    }
-  });
-
-  onBeforeUnmount(() => {
-    emitter.off(CASE_DATA_REFRESH_EVENT, handleImportListRefresh, true);
-    emitter.off(CASE_IMPORT_FILE_REFRESH_EVENT, handleImportListRefresh, true);
-  });
-
-  async function handleDelete(record: Partial<FileInfo>) {
-    const id = String(record.id ?? '').trim();
-    if (!id) {
-      createMessage.warning('未获取到文件ID,暂时无法删除');
-      return;
-    }
-    try {
-      await deletedFileList({ ids: [id] });
-      await handleSuccess('文件删除成功');
-    } catch (error: any) {
-      createMessage.error(getErrorMessage(error, '文件删除失败'));
-    }
-  }
-
-  async function handleRecognizeImport(record: Partial<FileInfo>) {
-    const filePath = String(record.filePath ?? '').trim();
-    const fileId = String(record.id ?? '').trim();
-    if (!filePath) {
-      createMessage.warning('未获取到文件路径,暂时无法重新清洗');
-      return;
-    }
-    if (!fileId) {
-      createMessage.warning('未获取到文件ID,暂时无法重新清洗');
-      return;
-    }
-    if (recleaningFileIds.value.includes(fileId)) {
-      return;
-    }
-
-    try {
-      recleaningFileIds.value = [...recleaningFileIds.value, fileId];
-      const response = await preFile({
-        filePath: [filePath],
-        isDir: false,
-        reClean: 1,
-      });
-      const fileInfos = Array.isArray(response) ? response : ((response as any)?.data ?? []);
-      if (!Array.isArray(fileInfos) || !fileInfos.length) {
-        createMessage.warning(PRE_FILE_EMPTY_MESSAGE);
-        return;
-      }
-
-      sessionStorage.setItem(FILE_INFO_STORAGE_KEY, JSON.stringify(fileInfos));
-      sessionStorage.removeItem(IMPORT_FILE_INFO_STORAGE_KEY);
-      sessionStorage.setItem(
-        RECLEAN_CONTEXT_STORAGE_KEY,
-        JSON.stringify({
-          reClean: 1,
-          fileId,
-        }),
-      );
-      resetImportProgressSession();
-      await go({
-        path: PageEnum.BASE_CLEANING,
-        query: {
-          focusKey: fileId,
-        },
-      });
-    } catch (error: any) {
-      createMessage.error(getErrorMessage(error, '进入重新清洗失败'));
-    } finally {
-      recleaningFileIds.value = recleaningFileIds.value.filter((id) => id !== fileId);
-    }
-  }
-
-  /** 下载该文件(Web 化:只传文件ID,服务端查库定位文件流,浏览器另存) */
-  async function handleDownloadFile(record: Partial<FileInfo>) {
-    const fileId = String(record.id ?? '').trim();
-    if (!fileId) {
-      createMessage.warning('未获取到文件ID,暂时无法下载文件');
-      return;
-    }
-
-    try {
-      const fileName = String(record.fileName ?? '').trim();
-      await downloadOfficeFileById(fileId, fileName || '文件');
-    } catch (error: any) {
-      createMessage.error(getOpenOfficeFileErrorMessage(error, '文件下载失败'));
-    }
-  }
-
-  function handleViewFailLog(event: MouseEvent, record: Partial<FileInfo>) {
-    event.preventDefault();
-    event.stopPropagation();
-    const failCount = Number(record.failNum ?? 0);
-    if (!Number.isFinite(failCount) || failCount <= 0) {
-      createMessage.info('当前文件暂无失败记录');
-      return;
-    }
-    const logFileInfo = { ...(record as FileInfo), pid: '0', children: [] as FileInfo[] };
-    delete (logFileInfo as Recordable).batchId;
-    currentLogFileInfos.value = [logFileInfo];
-    logModalOpen.value = true;
-  }
-
-  async function handleDeleteSelected() {
-    if (!selectedRowKeysRef.value.length) {
-      createMessage.warning('请先选择需要删除的文件');
-      return;
-    }
-    try {
-      const ids = selectedRowKeysRef.value.map((item) => String(item)).filter(Boolean);
-      await deletedFileList({ ids });
-      await handleSuccess(`已删除 ${ids.length} 条导入记录`);
-    } catch (error: any) {
-      createMessage.error(getErrorMessage(error, '批量删除失败'));
-    }
-  }
-
-  async function handleSuccess(message?: string) {
-    selectedRowKeysRef.value = [];
-    clearSelectedRowKeys();
-    if (message) {
-      createMessage.success(message);
-    }
-    await reload();
-  }
-</script>
-
-<style lang="less">
-  @prefix-cls: ~'jeesite-case-import-list';
-
-  .@{prefix-cls}.case-import-list {
-    height: 100%;
-    min-height: 0;
-    display: flex;
-    flex-direction: column;
-
-    .import-list-table .jeesite-basic-table-header__title {
-      display: none;
-    }
-
-    .import-list-table .jeesite-basic-table-header__toolbar {
-      width: 100%;
-      justify-content: space-between;
-      margin-top: -2px;
-      margin-bottom: 8px;
-
-      > * {
-        margin-right: 0;
-      }
-    }
-
-    .import-list-table .jeesite-basic-table-header__toolbar > .ant-btn {
-      display: none;
-    }
-
-    .table-toolbar {
-      display: flex;
-      align-items: center;
-      flex: 1 1 auto;
-      gap: 10px;
-      min-width: 0;
-      flex-wrap: wrap;
-    }
-
-    .table-title-meta {
-      display: flex;
-      align-items: center;
-      gap: 8px;
-      flex-wrap: wrap;
-    }
-
-    .table-meta-chip {
-      display: inline-flex;
-      align-items: center;
-      height: 24px;
-      padding: 0 10px;
-      border-radius: 999px;
-      background: #f3f6fb;
-      color: #475569;
-      font-size: 12px;
-      line-height: 24px;
-
-      &.is-selected {
-        background: #eef4ff;
-        color: #315efb;
-      }
-    }
-
-    .batch-delete-btn {
-      height: 34px;
-      padding: 0 14px;
-      border-radius: 8px;
-    }
-
-    .import-list-table,
-    .import-list-table.jeesite-basic-table {
-      flex: 1 1 auto;
-      min-height: 0;
-      height: 100%;
-      overflow: hidden;
-      display: flex;
-      flex-direction: column;
-
-      .jeesite-basic-table {
-        height: 100%;
-        display: flex;
-        flex-direction: column;
-      }
-    }
-
-    .import-list-table .ant-table-wrapper {
-      flex: 1 1 auto;
-      min-height: 0;
-      display: flex;
-      flex-direction: column;
-      padding: 0 6px 14px !important;
-      box-sizing: border-box;
-    }
-
-    .import-list-table .ant-spin-nested-loading,
-    .import-list-table .ant-spin-container,
-    .import-list-table .ant-table,
-    .import-list-table .ant-table-container {
-      flex: 1 1 auto;
-      min-height: 0;
-      display: flex;
-      flex-direction: column;
-    }
-
-    .import-list-table .ant-table-body,
-    .import-list-table .ant-table-content {
-      flex: 1 1 auto;
-      min-height: 0;
-    }
-
-    .table-cell-center,
-    .file-name-text,
-    .govern-status-text {
-      display: inline-block;
-      max-width: 100%;
-      color: #374151;
-      font-size: 12px;
-      line-height: 1.5;
-      overflow: hidden;
-      text-overflow: ellipsis;
-      white-space: nowrap;
-      vertical-align: middle;
-    }
-
-    .fail-link {
-      color: #315efb;
-      font-size: 12px;
-      line-height: 1.5;
-      text-decoration: underline;
-
-      &:hover {
-        color: #1d4ed8;
-      }
-    }
-
-    .ant-form {
-      padding: 12px 16px 0;
-      /* 查询表单:白底、不描边、不留底缝(智能侧视觉要求) */
-      border: none;
-      border-radius: 0;
-      background: #fff;
-      margin-bottom: 0;
-
-      .ant-form-item {
-        margin-bottom: 12px;
-      }
-
-      .ant-input,
-      .ant-picker,
-      .ant-input-affix-wrapper {
-        border-radius: 8px;
-      }
-    }
-
-    .ant-table-wrapper {
-      border-radius: 12px;
-      box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04);
-    }
-
-    .ant-table-container {
-      border-radius: 12px;
-      overflow: hidden;
-    }
-
-    .ant-table-thead > tr > th {
-      background: #f8fafc;
-      color: #374151;
-      font-weight: 600;
-      border-bottom-color: #e5e7eb;
-    }
-
-    .ant-table-tbody > tr > td {
-      padding-top: 10px;
-      padding-bottom: 10px;
-      color: #4b5563;
-      border-bottom-color: #eef2f7;
-      text-align: center;
-    }
-
-    .ant-table-tbody > tr:hover > td {
-      background: #f8fbff;
-    }
-
-    .jeesite-basic-table-action {
-      justify-content: center;
-      gap: 10px;
-      flex-wrap: nowrap;
-
-      .ant-btn-link,
-      .ant-btn-link.ant-btn-sm {
-        height: auto;
-        padding: 0;
-        color: #374151;
-        font-size: 12px;
-        line-height: 1.5;
-
-        &:hover {
-          color: #1d4ed8;
-        }
-      }
-
-      .ant-btn-link.ant-btn-error,
-      .ant-btn-link.ant-btn-error.ant-btn-sm {
-        color: @error-color;
-
-        &:hover,
-        &:focus {
-          color: lighten(@error-color, 10%);
-        }
-
-        &:active {
-          color: darken(@error-color, 10%);
-        }
-      }
-    }
-
-    .pagination-bar {
-      display: flex;
-      align-items: center;
-      justify-content: flex-end;
-      flex: 0 0 auto;
-      min-height: 40px;
-      /* 上下对称内边距:内容在条内垂直居中,与表格/视图底各留 8px */
-      padding: 8px 6px;
-      overflow: hidden;
-      box-sizing: border-box;
-    }
-  }
-</style>

+ 15 - 13
ai-electron/frontend/src/ai/views/aiPlugin/index.vue

@@ -122,10 +122,12 @@
               <span class="ai-plugin__pack-section-sp"></span>
               <label class="ai-plugin__search">
                 <Icon icon="mdi:magnify" :size="14" />
-                <input v-model="packEditor.keyword" type="text" placeholder="搜索工具名,如 stat_trans_big" />
+                <input v-model="packEditor.keyword" type="text" placeholder="搜索工具,如 大额交易" />
               </label>
             </div>
-            <div class="ai-plugin__pack-hint">点卡片勾选工具;基础工具(如 execute_sql)始终可用、不进工具包。</div>
+            <div class="ai-plugin__pack-hint"
+              >点卡片勾选工具;基础工具(如「执行 SQL 查询」)始终可用、不进工具包。</div
+            >
 
             <div v-for="g in packToolGroups" :key="g.label" class="ai-plugin__pack-group">
               <div class="ai-plugin__pack-group-title">
@@ -151,7 +153,7 @@
                   @click="packEditor.checked[t.toolName] = !packEditor.checked[t.toolName]"
                 >
                   <span class="ai-plugin__pack-ck"><Icon icon="mdi:check" :size="11" /></span>
-                  <div class="ai-plugin__pack-card-name">{{ t.toolName }}</div>
+                  <div class="ai-plugin__pack-card-name">{{ toolLabel(t.toolName) }}</div>
                   <div class="ai-plugin__pack-card-desc">{{ t.description || '(无说明)' }}</div>
                 </div>
               </div>
@@ -179,9 +181,7 @@
               </div>
               <p class="ai-plugin__card-desc">{{ p.description || '(无描述)' }}</p>
               <div class="ai-plugin__card-tools">
-                <span v-for="n in p.toolNames.slice(0, 4)" :key="n" class="ai-plugin__chip ai-plugin__mono">{{
-                  n
-                }}</span>
+                <span v-for="n in p.toolNames.slice(0, 4)" :key="n" class="ai-plugin__chip">{{ toolLabel(n) }}</span>
                 <span v-if="p.toolNames.length > 4" class="ai-plugin__chip">+{{ p.toolNames.length - 4 }}</span>
               </div>
               <div class="ai-plugin__card-ops" @click.stop>
@@ -219,7 +219,7 @@
             v-model:value="toolKeyword"
             class="ai-plugin__search"
             allow-clear
-            placeholder="搜索工具名,如 stat_call_night"
+            placeholder="搜索工具,如 夜间通话"
           >
             <template #prefix><Icon icon="mdi:magnify" :size="14" /></template>
           </a-input>
@@ -238,11 +238,10 @@
               v-for="tool in filteredTools"
               :key="tool.toolName"
               class="ai-plugin__card is-clickable"
-              :title="tool.toolName"
               @click="openTool(tool)"
             >
               <div class="ai-plugin__card-head">
-                <span class="ai-plugin__mono ai-plugin__tool-name ai-plugin__ellipsis">{{ tool.toolName }}</span>
+                <span class="ai-plugin__tool-name ai-plugin__ellipsis">{{ toolLabel(tool.toolName) }}</span>
                 <span v-if="tool.readOnly" class="ai-plugin__chip">内置</span>
               </div>
 
@@ -265,7 +264,7 @@
     <!-- 弹框:工具详情(说明 + 所属工具组说明) -->
     <a-modal
       v-model:open="toolOpen"
-      :title="selectedTool?.toolName || '工具详情'"
+      :title="selectedTool ? toolLabel(selectedTool.toolName) : '工具详情'"
       :width="760"
       :footer="null"
       destroy-on-close
@@ -313,7 +312,7 @@
           <div class="ai-plugin__pack-tools">
             <div v-for="t in packTools(selectedPack)" :key="t.toolName" class="ai-plugin__pack-tool">
               <div class="ai-plugin__pack-tool-head">
-                <span class="ai-plugin__mono ai-plugin__pack-tool-name">{{ t.toolName }}</span>
+                <span class="ai-plugin__pack-tool-name">{{ toolLabel(t.toolName) }}</span>
                 <span v-if="t.groupLabel" class="ai-plugin__chip">{{ t.groupLabel }}</span>
               </div>
               <div class="ai-plugin__pack-tool-desc">{{ t.description || '(无说明)' }}</div>
@@ -358,6 +357,8 @@
   import { useUserStore } from '@/core/store/modules/case';
   import { deleteSkill, installSkillZip, listSkills, listTools, toggleSkill } from '../../api/capabilityApi';
   import { createToolPack, deleteToolPack, listToolPacks, updateToolPack } from '../../api/toolPackApi';
+  // 工具名 → 中文展示名。工具名是模型调用用的标识(英文),页面只显示中文,两者互不影响。
+  import { toolLabel } from '../../utils/constants';
   import SkillDetailModal from '../../components/SkillDetailModal.vue';
   import SkillEditorModal from '../../components/SkillEditorModal.vue';
   import type { AgentTool, SaveToolPackParams, Skill, ToolPack } from '../../api/types';
@@ -436,13 +437,13 @@
     ];
   });
 
-  /** 列表筛选:分类 + 工具名关键字(工具名搜索,与需求一致) */
+  /** 列表筛选:分类 + 关键字(中文展示名与英文调用名都能命中) */
   const filteredTools = computed(() => {
     const keyword = toolKeyword.value.trim().toLowerCase();
     return tools.value.filter((tool) => {
       if (groupFilter.value && (tool.groupCode || 'base') !== groupFilter.value) return false;
       if (!keyword) return true;
-      return tool.toolName.toLowerCase().includes(keyword);
+      return tool.toolName.toLowerCase().includes(keyword) || toolLabel(tool.toolName).toLowerCase().includes(keyword);
     });
   });
 
@@ -595,6 +596,7 @@
       const hitKw =
         !keyword ||
         tool.toolName.toLowerCase().includes(keyword) ||
+        toolLabel(tool.toolName).toLowerCase().includes(keyword) ||
         String(tool.description || '')
           .toLowerCase()
           .includes(keyword);

+ 78 - 41
ai-electron/frontend/src/ai/views/aiWorkbench/index.vue

@@ -2,12 +2,6 @@
   <div class="awb-page">
     <!-- 工作台顶栏:与布局壳共用 AppTopBar(结构/模式切换/头像/窗口控制都在里面) -->
     <AppTopBar :case-name="caseName" :logo-title="'清鉴 · 智能工作台'" show-case-exit @exit-case="onExitCase">
-      <!-- 工作台特有状态 chip:当前会话 -->
-      <template #info>
-        <span v-if="activeTitle" class="awb-chip" :title="activeTitle">
-          {{ activeTitle }}
-        </span>
-      </template>
       <!-- 下载管理:AI 会话的工具结果导出(CSV 等下载产物)都经它落到本机,入口与传统壳顶栏一致 -->
       <template #actions>
         <AppDownloadManager />
@@ -24,7 +18,7 @@
             type="button"
             class="awb-rm"
             :class="{ 'is-on': view === item.key }"
-            @click="view = item.key"
+            @click="onRail(item.key)"
           >
             <Icon :icon="item.icon" :size="15" />
             {{ item.label }}
@@ -71,7 +65,6 @@
 
         <!-- ④ 插件与技能 -->
         <AiPluginPage v-else-if="view === 'plugin'" />
-        <AiCaseDataPage v-else-if="view === 'casedata'" />
       </main>
 
       <!-- 右栏:本会话产物(工具结果导出的 CSV)。可收起/拖宽,首次产出自动展开 -->
@@ -87,20 +80,21 @@
   /**
    * 智能工作台(智能模式的主界面)。
    *
-   * 不套传统 LAYOUT 的全屏路由:AI 能力集中呈现——对话 / 模型管理 / 插件与技能
-   * 三个视图经左栏菜单切换,交互逻辑与 aiAnalysis 完全同源(useAiChatPage),
-   * 案件上下文也一致(useUserStore().getCaseInfo → workspaceId)。
-   * 切换案件必须退出后从案件管理重进。
+   * 不套传统 LAYOUT 的全屏路由:AI 能力集中呈现——研判方案 / 插件与技能 / 模型管理
+   * 三个视图经左栏菜单切换(「对话」不在菜单里,只能由会话列表或「新建对话」带入),
+   * 交互逻辑与 aiAnalysis 完全同源(useAiChatPage),案件上下文也一致
+   * (useUserStore().getCaseInfo → workspaceId)。切换案件必须退出后从案件管理重进。
    *
    * 组件级复用:会话侧栏 / 对话面板 / 模型页 / 插件页全部来自 ai 模块现有实现,
    * 本文件只做装配与工作台外壳,不含协议逻辑。
    */
-  import { computed, nextTick, ref } from 'vue';
+  import { computed, nextTick, onMounted, ref, watch } from 'vue';
   import { useRoute } from 'vue-router';
   import Icon from '@/core/components/Icon/src/Icon.vue';
   import { AppDownloadManager, AppTopBar } from '@/core/components/Application';
   import { useMessage } from '@/core/hooks/web/useMessage';
   import { exitCaseConfirmOptions } from '@/core/logics/exitCaseConfirm';
+  import { getLastWorkbenchView, setLastWorkbenchView } from '@/core/logics/workbenchMode';
   import { useUserStore } from '@/core/store/modules/case';
   import SessionList from '../../components/SessionList.vue';
   import ChatPanel from '../../components/ChatPanel.vue';
@@ -109,7 +103,6 @@
   import AiModelPage from '../aiModel/index.vue';
   import AiExpertPage from '../aiExpert/index.vue';
   import AiPluginPage from '../aiPlugin/index.vue';
-  import AiCaseDataPage from '../aiCaseData/AiCaseDataPage.vue';
   import { useAiChatPage } from '../../composables/useAiChatPage';
   import { extractArtifacts } from '../../utils/artifacts';
   import type { AiExpert } from '../../api/types';
@@ -117,29 +110,46 @@
   const userStore = useUserStore();
   const { createConfirm } = useMessage();
 
-  type WorkbenchView = 'chat' | 'casedata' | 'model' | 'agent' | 'plugin';
-  const WORKBENCH_VIEWS: WorkbenchView[] = ['chat', 'casedata', 'model', 'agent', 'plugin'];
+  type WorkbenchView = 'chat' | 'model' | 'agent' | 'plugin';
+  const WORKBENCH_VIEWS: WorkbenchView[] = ['chat', 'model', 'agent', 'plugin'];
 
   /**
-   * 视图恢复:从上传流程等外部全屏页返回工作台时(/?view=xxx),按参数恢复所在视图;
-   * 无参数或非法值仍默认「对话」。只在挂载时读一次,页内切换不回写 URL。
+   * 视图恢复,两个来源,前者优先:
+   * 1. ?view=xxx —— 从上传流程等外部全屏页返回工作台时带回来的视图;
+   * 2. 上次停留的视图 —— 「自主 ⇄ 智能」来回切时工作台会重新挂载,靠它回到原来那一屏
+   *    (工作台是单路由全屏壳,视图不落 URL,只能自己记,见 core/logics/workbenchMode)。
+   * 两者都没有才交给下面的「进入工作台默认落点」决定。
    */
   const route = useRoute();
   const initialView = String(route.query.view ?? '');
-  const view = ref<WorkbenchView>(
-    WORKBENCH_VIEWS.includes(initialView as WorkbenchView) ? (initialView as WorkbenchView) : 'chat',
-  );
+  const restoredView = WORKBENCH_VIEWS.includes(initialView as WorkbenchView) ? (initialView as WorkbenchView) : null;
+  const lastView = getLastWorkbenchView();
+  const rememberedView = WORKBENCH_VIEWS.includes(lastView as WorkbenchView) ? (lastView as WorkbenchView) : null;
+  const view = ref<WorkbenchView>(restoredView ?? rememberedView ?? 'chat');
+
+  /**
+   * 用户是否已经自己动过视图(点菜单 / 选会话 / 新建对话 / 研判方案去对话)。
+   * 进入工作台的默认落点只在没人动过时生效,避免会话列表加载慢半拍时把用户拽走;
+   * 从记忆里恢复出来的视图同样算「已定」,否则默认落点会把用户上次待的那一屏换掉。
+   */
+  const userChoseView = ref(!!rememberedView);
+
+  /** 视图一变就记下:切到传统模式时本组件会卸载,只有持续写才能保证切回来还是这一屏 */
+  watch(view, (v) => setLastWorkbenchView(v));
 
   const RAIL_ITEMS: Array<{ key: WorkbenchView; icon: string; label: string }> = [
-    { key: 'chat', icon: 'mdi:robot-outline', label: '对话' },
     // 研判方案沿用原「模型管理」的 brain 图标;模型管理改用芯片图标(与输入框的模型 chip 同形)
     { key: 'agent', icon: 'mdi:brain', label: '研判方案' },
     { key: 'plugin', icon: 'mdi:puzzle-outline', label: '插件与技能' },
     { key: 'model', icon: 'mdi:chip', label: '模型管理' },
-    // 文件管理:把传统侧「导入文件管理」表格复制进智能侧(副本见 ai/views/aiCaseData),传统页不动
-    { key: 'casedata', icon: 'mdi:database-outline', label: '文件管理' },
   ];
 
+  /** 左栏菜单切视图:聊天视图不在菜单里,切走即离开对话 */
+  function onRail(key: WorkbenchView) {
+    userChoseView.value = true;
+    view.value = key;
+  }
+
   const {
     store,
     panelRef,
@@ -172,6 +182,7 @@
    * 发送第一条消息时才创建会话并绑定该研判方案(绑定后不可更换)。
    */
   async function onChatWithExpert(expert: AiExpert) {
+    userChoseView.value = true;
     store.startDraft({ id: expert.id, name: expert.name });
     view.value = 'chat';
     await nextTick();
@@ -179,24 +190,66 @@
   }
 
   /**
-   * 左栏会话列表在三个视图下都常驻,所以「选会话 / 新建会话」必须先把主区切回对话视图:
+   * 左栏会话列表在各视图下都常驻,所以「选会话 / 新建会话」必须先把主区切回对话视图:
    * 否则在模型管理或插件页点会话,选中的会话在别处看不见,用户会以为点击没生效。
    *
    * 先切视图再 await nextTick() 的原因:对话面板是 v-if 渲染的,切换后要等它挂载出来,
    * 接下来的「回到底部 / 聚焦输入框」才有目标(面板未挂载时 panelRef 还是空)。
    */
   async function onSelectSession(id: string) {
+    userChoseView.value = true;
     view.value = 'chat';
     await nextTick();
     await onSelect(id);
   }
 
+  /** 「新建对话」:落点永远是对话视图的新草稿(菜单里没有「对话」,这是唯一的入口之一) */
   async function onCreateSession() {
+    userChoseView.value = true;
     view.value = 'chat';
     await nextTick();
     await onCreate();
   }
 
+  /**
+   * 进入智能模式的默认落点(只算一次):
+   * 会话列表就绪后,有历史 → 打开第一条历史会话(对话视图);没有历史 → 研判方案页。
+   *
+   * 时机放在「首帧会话列表加载结束」而不是 setup:列表是 useAiChatPage 的 onMounted 里
+   * 异步拉的,此刻还不知道有没有历史。用户先动过视图(菜单 / 选会话 / 新建对话)就不接管,
+   * ?view=xxx 返回跳转恢复的视图同理,那条路径根本不会走到这里。
+   */
+  let defaultViewApplied = false;
+  async function applyDefaultView() {
+    // ?view= 带回的是非对话页(如上传流程回 ?view=agent),那个视图才是用户要回的地方
+    if (defaultViewApplied || (restoredView && restoredView !== 'chat')) return;
+    // 记忆恢复的视图(或用户自己点的)不被默认落点覆盖;但「对话」要靠会话上下文才成立 ——
+    // 刷新后 store 是新的、会话还没选,那种情况仍要选一条,免得落在一屏空白欢迎页上
+    if (userChoseView.value && (view.value !== 'chat' || hasConversation.value)) return;
+    defaultViewApplied = true;
+    // 列表展示顺序是「置顶在前、其余按接口顺序」,取展示中的第一行
+    const first = store.pinnedSessions[0] ?? store.normalSessions[0];
+    if (first) {
+      await onSelectSession(first.id);
+    } else {
+      view.value = 'agent';
+    }
+  }
+
+  watch(
+    [() => store.initialized, () => store.loadingSessions],
+    async () => {
+      if (!store.initialized || store.loadingSessions) return;
+      await applyDefaultView();
+    },
+    { immediate: true },
+  );
+
+  // 没有案件上下文时 store.init 不会跑,上面的 watcher 永远不触发;按「无历史」兜底
+  onMounted(() => {
+    if (!workspaceId.value) applyDefaultView();
+  });
+
   /**
    * 退出案件:回案件管理页重新选择(工作台不允许直接切案件)。
    * 破坏性动作(当前会话上下文、工作台状态都会离开)先弹确认框。
@@ -223,22 +276,6 @@
     background: var(--ai-bg-page);
   }
 
-  .awb-chip {
-    display: inline-flex;
-    align-items: center;
-    gap: 6px;
-    flex: 0 0 auto;
-    max-width: 260px;
-    padding: 3px 11px;
-    overflow: hidden;
-    font-size: 12.5px;
-    color: var(--ai-text-2);
-    white-space: nowrap;
-    text-overflow: ellipsis;
-    background: var(--ai-bg-subtle);
-    border-radius: 99px;
-  }
-
   .awb-body {
     display: flex;
     flex: 1;

+ 4 - 0
ai-electron/frontend/src/core/enums/cacheEnum.ts

@@ -10,6 +10,10 @@ export const MULTIPLE_TABS_KEY = 'MULTIPLE_TABS__';
 export const LOCALE_KEY = 'LOCALE__';
 export const APP_DARK_MODE_KEY = 'APP_DARK_MODE__';
 export const APP_WORKBENCH_MODE_KEY = 'APP_WORKBENCH_MODE__';
+/** 传统(自主)模式最后停留的路由:切到智能再切回来时恢复,避免每次掉回首页 */
+export const APP_WORKBENCH_PATH_KEY = 'APP_WORKBENCH_PATH__';
+/** 智能工作台最后停留的视图(chat/model/agent/plugin):切到传统再切回来时恢复 */
+export const APP_WORKBENCH_VIEW_KEY = 'APP_WORKBENCH_VIEW__';
 export const APP_LOCAL_CACHE_KEY = 'COMMON_LOCAL__';
 export const APP_SESSION_CACHE_KEY = 'COMMON_SESSION__';
 

+ 73 - 6
ai-electron/frontend/src/core/logics/workbenchMode.ts

@@ -1,6 +1,7 @@
 import { useRouter } from 'vue-router';
 import { useAppStoreWithOut, type WorkbenchMode } from '@/core/store/modules/app';
 import { PageEnum } from '@/core/enums/pageEnum';
+import { APP_WORKBENCH_PATH_KEY, APP_WORKBENCH_VIEW_KEY } from '@/core/enums/cacheEnum';
 
 /** 智能工作台的全屏路由地址(守卫与切换器同口径) */
 export const WORKBENCH_PATH = '/workbench';
@@ -23,22 +24,88 @@ export function getModeHomePath(): string {
   return useAppStoreWithOut().getWorkbenchMode === 'smart' ? WORKBENCH_PATH : PageEnum.BASE_HOME;
 }
 
+/* ------------------------------------------------------------------ *
+ * 各模式的「停留位置」记忆
+ *
+ * 模式来回切不该每次都掉回默认落点:传统模式记路由地址,智能模式记工作台内部视图
+ * (工作台是单路由全屏壳,视图不落 URL,只有组件自己知道当前在哪一屏)。
+ * 用 sessionStorage 而非 localStorage:只在本次运行内有效,重启后各模式回默认落点,
+ * 免得上次遗留的页面在案件上下文变化后变成一次莫名其妙的跳转。
+ * ------------------------------------------------------------------ */
+
+/** 传统(自主)模式最后停留的路由;没停留过或记录不可用时返回 null */
+export function getLastTraditionalPath(): string | null {
+  return readSession(APP_WORKBENCH_PATH_KEY);
+}
+
+/** 记下传统模式当前停留的路由;工作台地址不属于传统模式,直接忽略 */
+export function setLastTraditionalPath(path: string): void {
+  if (!path || WORKBENCH_PATHS.includes(path)) {
+    return;
+  }
+  writeSession(APP_WORKBENCH_PATH_KEY, path);
+}
+
+/** 智能工作台最后停留的视图(chat / model / agent / plugin) */
+export function getLastWorkbenchView(): string | null {
+  return readSession(APP_WORKBENCH_VIEW_KEY);
+}
+
+export function setLastWorkbenchView(view: string): void {
+  if (!view) {
+    return;
+  }
+  writeSession(APP_WORKBENCH_VIEW_KEY, view);
+}
+
+/** sessionStorage 兜底:隐私模式 / 存储被禁用时退化成「不记忆」,不影响切换本身 */
+function readSession(key: string): string | null {
+  try {
+    return sessionStorage.getItem(key) || null;
+  } catch {
+    return null;
+  }
+}
+
+function writeSession(key: string, value: string): void {
+  try {
+    sessionStorage.setItem(key, value);
+  } catch {
+    // 写不进去就算了:下次切回来走默认落点
+  }
+}
+
 /**
- * 主界面模式切换的唯一实现:写持久化状态 + 只在「当前页与目标模式不匹配」时跳转。
+ * 主界面模式切换的唯一实现:写持久化状态 + 跳到目标模式的「上次停留处」。
  *
  * 传统布局壳(顶栏里切换)与智能工作台共用,避免两处各写一份跳转口径而后漂移。
+ *
+ * 切走前先把当前所在页记下来,来回切才不会每次都掉回默认页(传统模式记路由、
+ * 智能模式记工作台视图,后者由工作台组件自己写);目标位置就是当前页时不跳转 ——
+ * 在传统壳里点「传统」只是把偏好对齐,不该把人从当前页挪走。
  */
 export function useWorkbenchModeSwitch() {
   const router = useRouter();
   const appStore = useAppStoreWithOut();
 
   function setMode(mode: WorkbenchMode) {
-    appStore.setWorkbenchMode(mode);
     const path = router.currentRoute.value.path;
-    if (mode === 'smart' && path !== WORKBENCH_PATH) {
-      router.replace(WORKBENCH_PATH);
-    } else if (mode === 'traditional' && path === WORKBENCH_PATH) {
-      router.replace(PageEnum.BASE_HOME);
+    const inWorkbench = WORKBENCH_PATHS.includes(path);
+
+    // 人在传统壳就把这一页记下(智能模式的停留位置由工作台组件自己写)
+    if (!inWorkbench) {
+      setLastTraditionalPath(path);
+    }
+
+    appStore.setWorkbenchMode(mode);
+
+    if (mode === 'smart') {
+      if (!inWorkbench) {
+        router.replace(WORKBENCH_PATH);
+      }
+    } else if (inWorkbench) {
+      // 切回传统:有记忆回记忆,没记忆才回首页
+      router.replace(getLastTraditionalPath() || PageEnum.BASE_HOME);
     }
   }