Add Markdown document processing support and enhance document handling

- Introduced `MarkdownDocumentProcessor` for handling markdown files, including reading and saving content.
- Updated `DocumentProcessorFactory` to include support for markdown file types.
- Enhanced existing document processors to utilize a shared initialization method for OllamaClient.
- Implemented chunking and mapping logic in `DocumentProcessor` for improved content processing and masking.
- Added utility class `LLMJsonExtractor` for extracting and parsing JSON from LLM outputs.
This commit is contained in:
oliviamn 2025-05-24 21:05:48 +08:00
parent caa4d6d2ef
commit 47e78c35bb
11 changed files with 511 additions and 44 deletions

View File

@ -0,0 +1,101 @@
# 北京市第三中级人民法院民事判决书
(2022)京 03 民终 3852 号
上诉人原审原告北京丰复久信营销科技有限公司住所地北京市海淀区北小马厂6 号1 号楼华天大厦1306 室。
法定代表人:郭东军,执行董事、经理。委托诉讼代理人:周大海,北京市康达律师事务所律师。委托诉讼代理人:王乃哲,北京市康达律师事务所律师。
被上诉人原审被告中研智创区块链技术有限公司住所地天津市津南区双港镇工业园区优谷产业园5 号楼-1505。
法定代表人:王欢子,总经理。
委托诉讼代理人:魏鑫,北京市昊衡律师事务所律师。
1.上诉人北京丰复久信营销科技有限公司以下简称丰复久信公司因与被上诉人中研智创区块链技术有限公司以下简称中研智创公司服务合同纠纷一案不服北京市朝阳区人民法院2020京0105 民初69754 号民事判决,向本院提起上诉。本院立案后,依法组成合议庭开庭进行了审理。上诉人丰复久信公司之委托诉讼代理人周大海、王乃哲,被上诉人中研智创公司之委托诉讼代理人魏鑫到庭参加诉讼。本案现已审理终结。
2.丰复久信公司上诉请求1.撤销一审判决发回重审或依法改判支持丰复久信公司一审全部诉讼请求2.或在维持原判的同时判令中研智创公司向丰复久信公司返还 1000 万元款项,并赔偿丰复久信公司因此支付的律师费 220 万元3.判令中研智创公司承担本案一审、二审全部诉讼费用。事实与理由一、根据2019 年的政策导向丰复久信公司的投资行为并无任何法律或政策瑕疵。丰复久信公司仅投资挖矿没有购买比特币故在当时国家、政府层面有相关政策支持甚至鼓励的前提下一审法院仅凭“挖矿”行为就得出丰复久信公司扰乱金融秩序的结论是错误的。二、一审法院没有全面、深入审查相关事实且遗漏了最核心的数据调查工作。三、本案一审判决适用法律错误。涉案合同成立及履行期间并无合同无效的情形当属有效。一审法院以挖矿活动耗能巨大、不利于我国产业结构调整为依据之一作出合同无效的判决实属牵强。最高人民法院发布的全国法院系统2020 年度优秀案例分析评选活动获奖名单中,由上海市第一中级人民法院刘江法官编写的“李圣艳、布兰登·斯密特诉闫向东、李敏等财产损害赔偿纠纷案— —比特币的法律属性及其司法救济”一案入选,该案同样发生在丰复久信公司与中研智创公司合同履行过程中,一审法院认定同时期同类型的涉案合同无效,与上述最高人民法院的优秀案例相悖。四、一审法院径行认定合同无效,未向丰复久信公司进行释明构成程序违法。
3.中研智创公司辩称,同意一审判决,不同意丰复久信公司的上诉请求。首先,一审法院曾在庭审中询问丰复久信公司关于机器返还的问题,一审法院进行了释明。其次,如二审法院对其该项上诉请求进行判决,会剥夺中研智创公司针对该部分请求再行上诉的权利。
4.丰复久信公司向一审法院起诉请求1.中研智创公司交付278.1654976 个比特币,或者按照 2021 年 1 月 25 日比特币的价格交付9550812.36 美元2.中研智创公司赔偿丰复久信公司服务期到期后占用微型存储空间服务器的损失(自2020 年7 月1日起至实际返还服务器时止按照bitinfocharts 网站公布的相关日产比特币数据计算应赔偿比特币数量或按照2021 年1 月25 日比特币的价格交付美元)。
5.一审法院查明事实2019 年5 月6 日,丰复久信公司作为甲方(买方)与乙方(卖方)中研智创公司签订《计算机设备采购合
同》约定货物名称为计算机设备型号规格及数量为T2T-30T 规格型号的微型存储空间服务器1542 台单价5040/ 台合同金额为 7 771 680 元;交货期 2019 年 8 月 31 日前;交货方式为乙方自行送货到甲方所在地,并提供安装服务,运输工具及运费由乙方负责;交货地点北京;签订购货合同,设备安装完毕后一次性支付项目总货款;乙方提供货物的质量保证期为自交货验收结束之日起不少于十二个月(具体按清单要求);乙方交货前应对产品作出全面检查和对验收文件进行整理,并列出清单,作为甲方收货验收和使用的技术条件依据,检验的结果应随货物交甲方,甲方对乙方提供的货物在使用前进行调试时,乙方协助甲方一起调试,直到符合技术要求,甲方才做最终验收,验收时乙方必须在现场,验收完毕后作出验收结果报告,并经双方签字生效。
6.同日丰复久信公司作为甲方客户方与乙方中研智创公司服务方签订《服务合同书》约定乙方同意就采购合同中的微型存储空间服务器向甲方提供特定服务服务的内容包括质保、维修、服务器设备代为运行管理、代为缴纳服务器相关用度花费如电费等详细内容见附件一如果乙方在工作中因自身过错而发生任何错误或遗漏应无条件更正不另外收费并对因此而对甲方造成的损失承担赔偿责任赔偿额以本合同约定的服务费为限若因甲方原因造成工作延误将由甲方承担相应的损失服务费总金额为2 228 320 元甲乙双方一致同意项目服务费以人民币形式于本合同签订后3 日内一次性支付甲方可以提前10 个工作日以书面形式要求变更或增加所提供的服务该等变更最终应由双方商定认可其中包括与该等变更有关的任何费用调整等。合同后附附件一以表格形式列明1.1542 台T2T-30T 微型存储空间服务器的质保、维修时限12 个月完成标准为完成甲方指定的运行量2.服务器的日常运行管理时限12 个月3.代扣代缴电费4.其他(空白)。
7.2019 年5 月双方签订《增值服务协议》约定甲方将自有的T2T-30 规格的微型存储空间服务器1542 台委托乙方管理由甲方向乙方支付一定管理费用由乙方向甲方提供相关数据增值服务对于增值服务产生的收益扣除运行成本后甲乙双方按照一定比例进行分配备注增值服务收益与微型存储空间服务的单位TH/s 相关分配收益方式不限于人民币支付甲方最多可将托管的云数据服务器的单位TH/s的 $50 \%$ 进行拆分委托乙方代为出售用户购买后的单位TH/s所产生的收益归购买用户所有结算价格按照当天实际的市场价格进行结算扣除市场销售成本后实时转入甲方提供的收益地址相关费用及支付数据增值服务的电费成本由甲方自行承担按日计算具体价格根据实际上架的数据中心的价格进行计算由后续的《数据增值服务电费计价协议作为补充》云数据服务器上架后2 天内甲方应当向乙方预付498 196 元用于预付部分云数据服务器的电费后续每日的电费支出按当天24 时云数据服务器的增值部分的价值扣除扣除完成后的增值服务收益部分当日划入甲方提供的收益地址单台云数据服务器的放置建设成本为300 元,由甲方承担;数据增值服务产生的收益,按照 $7 \%$ 的比例分配给乙方作为云数据服务器托管过程中乙方的管理和运营收益数据增值服务产生的收益当天进行结算转入甲方提供的接收地址乙方保证将按照厂家提供的环境标准包括但不限于电压、用电环境、温度、湿度、网络带宽、机房密度使用、维护本合同项下云数据服务器在正常使用过程中因不可归责于乙方的原因导致服务器损坏的乙方不承担责任乙方应协助甲方维修或更换服务器设备相关费用由甲方承担甲方云数据服务器根据机型按实测功耗计算电费各服务器机型到现场进行测量功耗后乙方告知甲方经双方认可后固定每月耗电量未经甲方同意乙方不得将托管的云数据服务器挪作他用且不得将同类设备进行调换如云数据服务器出现宕机或TH/s 为零的情况下,乙方必须 30分钟内对云数据服务器设备进行充气或其他处理以保障甲方的利益若检查发现系硬件原因无法解决乙方负责将故障设备进行打包、返场维修产生的费用由甲方承担如因突发情况如供电公司线路检修、机组维护、网络运营商意外断网等导致数据服务器中断运行乙方负责协调处理故障在乙方可控范围内甲方云数据服务器中断运行时间原则上每月不超过48 小时,如停电超出约定时间,乙方将在合同约定的管理时间基础上延长服务时间,并承担服务器的机器放置费用;乙方因自身原因导致托管的甲方服务器损害或者灭失的,应当向甲方承担赔偿责任;合同期限为 2019 年 6 月 30 日至 2020 年 6 月 30日。
8.上述合同签订后,中研智创公司购买并委托第三方矿场实际运营“矿机”。
019 年7 月15 日甲方中研智创公司与乙方成都毛球华数科技合伙企业有限合伙签订两份《矿机托管服务合同包运维约定甲方将其所拥有的“矿机”置于乙方算力服务中心乙方对甲方矿机提供运维管理服务“矿机”名称为芯动T2T数量分别为1350 台、502 台全新算力26T、30T功耗2200W托管期限以甲方矿机到达乙方算力服务中心并开始运行之日起算分别暂定自2019 年7 月15 日至2019 年10月 25 日止、自 2019 年 6 月 28 日至 2019 年 10 月 25 日止,乙方算力服务中心地址分别为四川省凉山州木里县水洛乡、沙湾乡;托管服务费计量方式均为,按照乙方上架运行机型实测功耗进行核算耗电量 $+ 3 \%$ 电损电费单价按0.239 元/度计算甲方应在本合同签订之日起两个工作日内向乙方支付半个月托管服务费作为本合同履约保证金分别为人民币251 242 元、
94000 元,履约保证金可用于抵扣协议最后一个结算周期的托管服务费,托管服务费支付周期为每半月支付。
10. 合同实际履行过程中2019 年5 月20 日丰复久信公司向中研智创公司支付1000 万元用途备注为货款。中研智创公司曾于2019 年向丰复久信公司交付了18.3463 个比特币。此后未再进行比特币交付,双方故此产生争议,并产生大量微信沟通记录。
微信聊天记录中关于核实设备及比特币产量情况。2019年11 月8 日丰复久信公司称“我们是应该自己有个矿池账号了吧这样是不是我们也可远程监控管理”“现在可以登不中研智创公司称“可以的”“之前走的是另外的体系我们看看怎样把矿池的账号直接对接给你这边吧”“现在不行因为所有机器都是统一管理放在同一个大的账号里面需要切割出来”“两天吧周一可以给你搞好”。11 月12 日,中研智创公司微信称“郭总,请你升级一下注册一下普惠矿场 App挖矿收益以后都在这里查看和提取原来的APP 不再更新了”丰复久信公司回复称“我不清楚你们要干什么现在不能你们让我干嘛我干嘛基本信任已经不存在了。所以任何一个动作我现在都需要做尽调”。11 月25 日双方微信群聊天记录显示中研智创公司员工介绍“这是我们在四川木里那边的现场管理人员”此后双方互相交换了联系电话并沟通丰复久信公司应以何种交通方式前往四川木里。11 月27 日丰复久信公司称“我到了矿场我要看下后台需要个链接”中研智创公司给出网页链接称“这是矿场的链接”。该链接地址网站名称为“币印”现点击链接显示“该观察者链接已失效请重新创建”。12 月7 日丰复久信公司称“什么时候可以提供你的原始资料抓紧核实”。12 月19 日双方沟通矿机搬动情况中研智创公司称“在昭通这边等通知进场”2020 年3 月20 日称矿机在“乐山这边”,丰复久信公司称“这几个月挖了多少币了?郭总的软件登不上去没有信息!什么时间通个电话”。中研智创公司未在微信聊天中明确答复。
12. 关于丰复久信公司向中研智创公司催要比特币情况。微信聊天记录显示2020 年4 月9 日,丰复久信公司询问中研智创公司,“我想知道一下我的机器到底挖了几个币,就这么难吗?”,中研智创公司回复“放心吧,我已经打电话给唐宇了,他会安排老潘落实,我今天没有联系到潘,我答应你的事情算数的”。
4 月 10 日、4 月 17 日、4 月 30 日、6 月 22 日、6 月 23 日、6 月27 日丰复久信公司分别再次询问称“还是这情况呀APP 还是 $0 ^ { \mathfrak { s } }$ “咱们这事您准备怎么收场啊币也不给钱也不还算力也不卖各种理由您是逼我报官了吧”等中研智创公司未回复6 月28 日回复称“稍晚一会给你打电话”。
13. 关于比特币等虚拟货币及“挖矿”活动的风险防范、整治,国家相关部门曾多次发布《通知》《公告》《风险提示》等政策文件:
1.2013 年12 月,中国人民银行等五部委发布《关于防范比特币风险的通知》指出,比特币不是由货币当局发行,不具有法偿性与强制性等货币属性,并不是真正意义的货币。从性质上看,比特币应当是一种特定的虚拟商品,不具有与货币等同的法律地位,不能且不应作为货币在市场上流通使用。各金融机构和支付机构不得以比特币为产品或服务定价,不得买卖或作为中央对手买卖比特币,不得承保与比特币相关的保险业务或将比特币纳入保险责任范围,不得直接或间接为客户提供其他与比特币相关的服务。
2.2017 年9 月,《中国人民银行、中央网信办、工业和信息化部、工商总局、银监会、证监会、保监会、关于防范代币发行融资风险的公告》,再次强调比特币不具有法偿性与强制性等货币属性,不具有与货币等同的法律地位,不能也不应作为货币在市场上流通使用,并提示,代币发行融资与交易存在多重风险,包括虚假资产风险、经营失败风险、投资炒作风险等,投资者须自行承担投资风险,希望广大投资者谨防上当受骗。
32018 年8 月《中国银行保险监督管理委员会、中央网络安全和信息化领导小组办公室、公安部、中国人民银行、国家市场监督管理总局关于防范以“虚拟货币”“区块链”名义进行非法集资的风险提示》也再次明确作出风险提示。
4.2021 年5 月18 日,中国互联网金融协会、中国银行业协会、中国支付清算协会联合发布《关于防范虚拟货币交易炒作风险的公告》,再次强调正确认识虚拟货币及相关业务活动的本质属性,有关机构不得开展与虚拟货币相关的业务,并特别指出,消费者要提高风险防范意识,“从我国现有司法实践看,虚拟货币交易合同不受法律保护,投资交易造成的后果和引发的损失由相关方自行承担”。
5.2021 年9 月3 日国家发展和改革委员会等部门发布《关于整治虚拟货币“挖矿”活动的通知》发改运行20211283号指出“虚拟货币挖矿活动指通过专用矿机计算生产虚拟货币的过程能源消耗和碳排放量大对国民经济贡献度低对产业发展、科技进步等带动作用有限加之虚拟货币生产、交易环节衍生的风险越发突出其盲目无序发展对推动经济社会高质量发展和节能减排带来不利影响。整治虚拟货币挖矿活动对促进我国产业结构优化、推动节能减排、如期实现碳达峰、碳中和目标具有重要意义。”“严禁投资建设增量项目禁止以任何名义发展虚拟货币挖矿项目加快有序退出存量项目。”
“严格执行有关法律法规和规章制度,严肃查处整治各地违规虚拟货币‘挖矿’活动”。
6.2021 年9 月15 日,中国人民银行、中央网信办、最高人民法院等部门联合发布《关于进一步防范和处置虚拟货币交易炒作风险的通知》指出,虚拟货币相关业务活动属于非法金融活动,境外虚拟货币交易所通过互联网向我国境内居民提供服务同样属于非法金融活动,并再次提示,参与虚拟货币投资交易活动存在法律风险,任何法人、非法人组织和自然人投资虚拟货币及相关衍生品,违背公序良俗的,相关民事法律行为无效,由此引发的损失由其自行承担。
14. 上述事实,有丰复久信公司提交的《计算机设备采购合同》《服务合同书》《增值服务协议》、银行转账记录、网页截图、微信聊天记录,有中研智创公司提交的《矿机托管服务合同(包运维)》、微信聊天记录等证据及当事人陈述等在案佐证。
一审法院认为,本案事实发生于民法典实施前,根据《最高人民法院关于适用<中华人民共和国民法典>时间效力的若干规定》,民法典施行前的法律事实引起的民事纠纷案件,适用当时的法律、司法解释的规定,因此本案应适用《中华人民共和国合同法》的相关规定。
15. 根据2021 年9 月3 日国家发展和改革委员会等部门《关于整治虚拟货币“挖矿”活动的通知》虚拟货币“挖矿”活动指通过专用“矿机”计算生产虚拟货币的过程。本案中丰复久信公司与中研智创公司签订《计算机设备采购合同》《服务合同书》《增值服务协议》约定丰复久信公司委托中研智创公司采购微型存储空间服务器并由中研智创公司对计算机服务器进行管理丰复久信公司向中研智创公司支付管理费用中研智创公司提供相关数据增值服务支付增值服务收益。诉讼中中研智创公司陈述其按照三份合同约定代丰复久信公司购买了“矿机”并与第三方公司即“矿场”签订委托合同将“矿机”在“矿场”运行并曾向丰复久信公司交付过18.3463 个比特币。根据上述履约过程及三份合同约定的主要内容,双方的交易模式实际上即为丰复久信公司委托中研智创公司购买并管理专用“矿机”计算生产比特币的“挖矿”行为。三份合同系有机整体,合同目的均系双方为了最终进行“挖矿”活动而签订,双方成立合同关系。该比特币“挖矿”的交易模式,属于国家相关行政机关管控范围,需要严格遵守相关法律法规和规章制度。
《中华人民共和国合同法》第七条规定,“当事人订立、履行合同,应当遵守法律、行政法规,尊重社会公德,不得扰乱社会经济秩序,损害社会公共利益”;第五十二条规定,“有下列情形之一的,合同无效:(一)一方以欺诈、胁迫的手段订立合同,损害国家利益;(二)恶意串通,损害国家、集体或者第三人利益;(三)以合法形式掩盖非法目的;(四)损害社会公共利益;(五)违反法律、行政法规的强制性规定。” 社会公共利益一般指关系到全体社会成员或者社会不特定多数人的利益,主要包括社会公共秩序以及社会善良风俗等,是明确国家和个人权利的行使边界、判断民事法律行为正当性和合法性的重要标准之一。能源安全、金融安全、经济安全等都是国家安全的重要组成部分,防范化解相关风险、深化整治相关市场乱象,均关系到我国的产业结构优化、金融秩序稳定、社会经济平稳运行和高质量发展,故社会经济秩序、金融秩序等均涉及社会公共利益。
17. 根据上述虚拟货币相关《通知》《公告》《风险提示》等文件,本案涉及的比特币为网络虚拟货币,并非国家有权机关发行的法定货币,不具有与法定货币等同的法律地位,不具有法偿性,不应且不能作为货币在市场上流通使用,相关部门多次发布《风险公告》《通知》等文件,提示消费者提高风险防范意识,投资交易虚拟货币造成的后果和引发的损失由相关方自行承担。且本案的交易模式系“挖矿”活动,随着虚拟货币交易的发展,“挖矿”行为的危害日渐凸显。
“挖矿”活动能源消耗和碳排放量大不利于我国产业结构优化、节能减排不利于我国实现碳达峰、碳中和目标。加之虚拟货币相关交易活动无真实价值支撑价格极易被操纵“挖矿”行为也进一步衍生虚假资产风险、经营失败风险、投资炒作风险等相关金融风险危害外汇管理秩序、金融秩序甚至容易引发违法犯罪活动、影响社会稳定。正因“挖矿”行为危害大、风险高其盲目无序发展对推动经济社会高质量发展和节能减排带来不利影响相关政策明确拟将虚拟货币“挖矿”活动增补列入《产业结构调整指导目录2019 年本)》“淘汰类”目录,要求采取有效措施,全面整治虚拟货币“挖矿”活动。本案中,丰复久信公司和中研智创公司在明知“挖矿”及比特币交易存在风险,且相关部门明确禁止比特币相关交易的情况下,仍然签订协议形成委托“挖矿”关系。“挖矿”活动及虚拟货币的相关交易行为存在上文论述的诸多风险和危害,干扰了正常的金融秩序、经济发展秩序,故该“挖矿”合同损害社会公共利益,应属无效。
18. 《中华人民共和国合同法》第五十八条规定,“合同无效或者被撤销后,因该合同取得的财产,应当予以返还;不能返还或者没有必要返还的,应当折价补偿。有过错的一方应当赔偿对方因此所受到的损失,双方都有过错的,应当各自承担相应的责任。”本案中,丰复久信公司第一项诉讼请求系基于合同项下权利义务要求中研智创公司支付比特币收益,因“挖矿” 合同自始无效,丰复久信公司通过履行无效合同主张获得的利益不应受到法律保护,对其相应诉讼请求,一审法院不予支持。丰复久信公司第二项诉讼请求主张占用“矿机”设备期间的比特币损失,该损失系丰复久信公司基于持续利用“矿机”从事 “挖矿”活动产生比特币的损失,不应受到法律保护,对其相应诉讼请求,一审法院亦不予支持。
19. 关于本案中“矿机”的处理,因现相关计算机设备仍由中研智创公司保管,但诉讼中,丰复久信公司明确表示其将另行主张,不在本案中要求处理“矿机”返还问题。故一审法院在本案中不再予以处理。但同时需要提醒双方当事人,均应遵守国家相关法律规定和产业政策,案涉计算机等设备不得继续用于比特币等虚拟货币“挖矿”活动,当事人应防范虚拟货币交易风险,自觉维护市场秩序和社会公共利益。
20. 综上,一审法院判决驳回北京丰复久信营销科技有限公司的全部诉讼请求。
21. 二审中,各方均未提交新的证据。本院对一审查明的事实予以确认。
22. 本院认为,比特币及相关经济活动是新型、复杂的,我国监管机构对比特币生产、交易等方面的监管措施建立在对其客观认识的基础上,并不断完善。本案双方挖矿合同从签订至履行后发生争议,纠纷延续至今,亦处于这一过程中。对合同效力的认定,应建立在当下对挖矿活动的客观认识基础上。
'、一·、( √DT4D H上23. 2013 年中国人民银行等五部委发布通知禁止金融机构对比特币进行定价不得买卖或作为中央对手买卖比特币不得直接或间接为客户提供其他与比特币相关的服务。2017 年中国人民银行等七部门联合发布《关于防范代币发行融资风险的公告》进一步提出任何所谓的代币融资交易平台不得从事法定货币与代币、“虚拟货币”相互之间的兑换业务不得买卖或作为中央对手方买卖代币或“虚拟货币”不得为代币或“虚拟货币”提供定价、信息中介等服务。上述两个文件实质上禁止了比特币在我国相关平台的兑付、交易。2021 年,中国人民银行等部门《关于进一步防范和处置虚拟货币交易炒作风险的通知》显示,虚拟货币交易炒作活动扰乱经济金融秩序,滋生赌博、非法集资、诈骗、传销、洗钱等违法犯罪活动,严重危害人民群众财产安全和国家金融安全。
24. 2021 年9 月3 日国家发展和改革委员会等部门《关于整治虚拟货币“挖矿”活动的通知》显示,虚拟货币挖矿活动能源消耗和碳排放量大,对国民经济贡献度低,对产业发展、科技进步等带动作用有限,加之虚拟货币生产、交易环节衍生的风险越发突出,其盲目无序发展对推动经济社会高质量发展和节能减排带来不利影响。故以电力资源、碳排放量为代价的“挖矿”行为,与经济社会高质量发展和碳达峰、碳中和目标相悖,与公共利益相悖。
5. 丰复久信公司主张双方合同签订时并无明确的法律规范禁止比特币“挖矿”活动,故应保障当事人的信赖利益,认定涉案合同有效一节,本院认为,当事人之间基于投资目的进行“挖矿”,并通过电子方式转让、储存以及交易的行为,实际经济追求是为了通过比特币与法定货币的兑换直接获取法定货币体系下的利益。丰复久信公司作为营利法人,在庭审中表示投资比特币仅系持有,本院难以采信。在监管机构禁止了比特币在我国相关平台的兑付、交易,且数次提示比特币投资风险的情况下,双方为获取高额利润,仍从事“挖矿”行为,现丰复久信公司以保障其信赖利益主张合同有效依据不足,本院不予采纳。
26. 综上,相关部门整治虚拟货币“挖矿”活动、认定虚拟货币相关业务活动属于非法金融活动,有利于保障我国发展利益和金融安全。从“挖矿”行为的高能耗以及比特币交易活动对国家金融秩序和社会秩序的影响来看,一审法院认定涉案合同无效是正确的。双方作为社会主义市场经济主体,既应遵守市场经济规则,亦应承担起相应的社会责任,推动经济社会高质量发展、可持续发展。
27. 关于合同无效后的返还问题,一审法院未予处理,双方可另行解决。
28. 综上所述,丰复久信公司的上诉请求不能成立,应予驳回;一审判决并无不当,应予维持。依照《中华人民共和国民事诉讼法》第一百七十七条第一款第一项规定,判决如下:
驳回上诉,维持原判。
二审案件受理费450892 元,由北京丰复久信营销科技有限公司负担(已交纳)。
29. 本判决为终审判决。
审 判 长 史晓霞审 判 员 邓青菁审 判 员 李 淼二〇二二年七月七日法 官 助 理 黎 铧书 记 员 郑海兴

2
data/test.sh Executable file
View File

@ -0,0 +1,2 @@
rm ./doc_src/*.md
cp ./doc/*.md ./doc_src/

View File

@ -4,7 +4,8 @@ from document_handlers.document_processor import DocumentProcessor
from document_handlers.processors import (
TxtDocumentProcessor,
DocxDocumentProcessor,
PdfDocumentProcessor
PdfDocumentProcessor,
MarkdownDocumentProcessor
)
class DocumentProcessorFactory:
@ -16,7 +17,9 @@ class DocumentProcessorFactory:
'.txt': TxtDocumentProcessor,
'.docx': DocxDocumentProcessor,
'.doc': DocxDocumentProcessor,
'.pdf': PdfDocumentProcessor
'.pdf': PdfDocumentProcessor,
'.md': MarkdownDocumentProcessor,
'.markdown': MarkdownDocumentProcessor
}
processor_class = processors.get(file_extension)

View File

@ -1,16 +1,188 @@
from abc import ABC, abstractmethod
from typing import Any
from typing import Any, Dict
from prompts.masking_prompts import get_masking_mapping_prompt
import logging
import json
from services.ollama_client import OllamaClient
from config.settings import settings
from utils.json_extractor import LLMJsonExtractor
logger = logging.getLogger(__name__)
class DocumentProcessor(ABC):
def __init__(self):
self.ollama_client = OllamaClient(model_name=settings.OLLAMA_MODEL, base_url=settings.OLLAMA_API_URL)
self.max_chunk_size = 1000 # Maximum number of characters per chunk
self.max_retries = 3 # Maximum number of retries for mapping generation
@abstractmethod
def read_content(self) -> str:
"""Read document content"""
pass
@abstractmethod
def _split_into_chunks(self, sentences: list[str]) -> list[str]:
"""Split sentences into chunks that don't exceed max_chunk_size"""
chunks = []
current_chunk = ""
for sentence in sentences:
if not sentence.strip():
continue
# If adding this sentence would exceed the limit, save current chunk and start new one
if len(current_chunk) + len(sentence) > self.max_chunk_size and current_chunk:
chunks.append(current_chunk)
current_chunk = sentence
else:
if current_chunk:
current_chunk += "" + sentence
else:
current_chunk = sentence
# Add the last chunk if it's not empty
if current_chunk:
chunks.append(current_chunk)
return chunks
def _validate_mapping_format(self, mapping: Dict[str, Any]) -> bool:
"""
Validate that the mapping follows the required format:
{
"原文1": "脱敏后1",
"原文2": "脱敏后2",
...
}
"""
if not isinstance(mapping, dict):
logger.warning("Mapping is not a dictionary")
return False
# Check if any key or value is not a string
for key, value in mapping.items():
if not isinstance(key, str) or not isinstance(value, str):
logger.warning(f"Invalid mapping format - key or value is not a string: {key}: {value}")
return False
# Check if the mapping has any nested structures
if any(isinstance(v, (dict, list)) for v in mapping.values()):
logger.warning("Invalid mapping format - contains nested structures")
return False
return True
def _build_mapping(self, chunk: str) -> Dict[str, str]:
"""Build mapping for a single chunk of text with retry logic"""
for attempt in range(self.max_retries):
try:
formatted_prompt = get_masking_mapping_prompt(chunk)
logger.info(f"Calling ollama to generate mapping for chunk (attempt {attempt + 1}/{self.max_retries}): {formatted_prompt}")
response = self.ollama_client.generate(formatted_prompt)
logger.info(f"Raw response from LLM: {response}")
# Parse the JSON response into a dictionary
mapping = LLMJsonExtractor.parse_raw_json_str(response)
logger.info(f"Parsed mapping: {mapping}")
if mapping and self._validate_mapping_format(mapping):
return mapping
else:
logger.warning(f"Invalid mapping format received on attempt {attempt + 1}, retrying...")
except Exception as e:
logger.error(f"Error generating mapping on attempt {attempt + 1}: {e}")
if attempt < self.max_retries - 1:
logger.info("Retrying...")
else:
logger.error("Max retries reached, returning empty mapping")
return {}
def _apply_mapping(self, text: str, mapping: Dict[str, str]) -> str:
"""Apply the mapping to replace sensitive information"""
masked_text = text
for original, masked in mapping.items():
# Ensure masked value is a string
if isinstance(masked, dict):
# If it's a dict, use the first value or a default
masked = next(iter(masked.values()), "")
elif not isinstance(masked, str):
# If it's not a string, convert to string or use default
masked = str(masked) if masked is not None else ""
masked_text = masked_text.replace(original, masked)
return masked_text
def _get_next_suffix(self, value: str) -> str:
"""Get the next available suffix for a value that already has a suffix"""
# Define the sequence of suffixes
suffixes = ['', '', '', '', '', '', '', '', '', '']
# Check if the value already has a suffix
for suffix in suffixes:
if value.endswith(suffix):
# Find the next suffix in the sequence
current_index = suffixes.index(suffix)
if current_index + 1 < len(suffixes):
return value[:-1] + suffixes[current_index + 1]
else:
# If we've used all suffixes, start over with the first one
return value[:-1] + suffixes[0]
# If no suffix found, return the value with the first suffix
return value + ''
def _merge_mappings(self, existing: Dict[str, str], new: Dict[str, str]) -> Dict[str, str]:
"""
Merge two mappings following the rules:
1. If key exists in existing, keep existing value
2. If value exists in existing:
- If value ends with a suffix (甲乙丙丁...), add next suffix
- If no suffix, add ''
"""
result = existing.copy()
# Get all existing values
existing_values = set(result.values())
for key, value in new.items():
if key in result:
# Rule 1: Keep existing value if key exists
continue
if value in existing_values:
# Rule 2: Handle duplicate values
new_value = self._get_next_suffix(value)
result[key] = new_value
existing_values.add(new_value)
else:
# No conflict, add as is
result[key] = value
existing_values.add(value)
return result
def process_content(self, content: str) -> str:
"""Process document content"""
pass
"""Process document content by masking sensitive information"""
# Split content into sentences
sentences = content.split("")
# Split sentences into manageable chunks
chunks = self._split_into_chunks(sentences)
logger.info(f"Split content into {len(chunks)} chunks")
# Build mapping for each chunk
combined_mapping = {}
for i, chunk in enumerate(chunks):
logger.info(f"Processing chunk {i+1}/{len(chunks)}")
chunk_mapping = self._build_mapping(chunk)
if chunk_mapping: # Only update if we got a valid mapping
combined_mapping = self._merge_mappings(combined_mapping, chunk_mapping)
else:
logger.warning(f"Failed to generate mapping for chunk {i+1}")
# Apply the combined mapping to the entire content
masked_content = self._apply_mapping(content, combined_mapping)
logger.info("Successfully masked content")
return masked_content
@abstractmethod
def save_content(self, content: str) -> None:

View File

@ -1,5 +1,6 @@
from document_handlers.processors.txt_processor import TxtDocumentProcessor
from document_handlers.processors.docx_processor import DocxDocumentProcessor
from document_handlers.processors.pdf_processor import PdfDocumentProcessor
from document_handlers.processors.md_processor import MarkdownDocumentProcessor
__all__ = ['TxtDocumentProcessor', 'DocxDocumentProcessor', 'PdfDocumentProcessor']
__all__ = ['TxtDocumentProcessor', 'DocxDocumentProcessor', 'PdfDocumentProcessor', 'MarkdownDocumentProcessor']

View File

@ -13,6 +13,7 @@ logger = logging.getLogger(__name__)
class DocxDocumentProcessor(DocumentProcessor):
def __init__(self, input_path: str, output_path: str):
super().__init__() # Call parent class's __init__
self.input_path = input_path
self.output_path = output_path
self.output_dir = os.path.dirname(output_path)
@ -44,21 +45,21 @@ class DocxDocumentProcessor(DocumentProcessor):
logger.error(f"Error converting DOCX to MD: {e}")
raise
def process_content(self, content: str) -> str:
logger.info("Processing DOCX content")
# def process_content(self, content: str) -> str:
# logger.info("Processing DOCX content")
# Split content into sentences and apply masking
sentences = content.split("")
final_md = ""
for sentence in sentences:
if sentence.strip(): # Only process non-empty sentences
formatted_prompt = get_masking_mapping_prompt(sentence)
logger.info("Calling ollama to generate response, prompt: %s", formatted_prompt)
response = self.ollama_client.generate(formatted_prompt)
logger.info(f"Response generated: {response}")
final_md += response + ""
# # Split content into sentences and apply masking
# sentences = content.split("。")
# final_md = ""
# for sentence in sentences:
# if sentence.strip(): # Only process non-empty sentences
# formatted_prompt = get_masking_mapping_prompt(sentence)
# logger.info("Calling ollama to generate response, prompt: %s", formatted_prompt)
# response = self.ollama_client.generate(formatted_prompt)
# logger.info(f"Response generated: {response}")
# final_md += response + "。"
return final_md
# return final_md
def save_content(self, content: str) -> None:
# Ensure output path has .md extension

View File

@ -0,0 +1,39 @@
import os
from document_handlers.document_processor import DocumentProcessor
from services.ollama_client import OllamaClient
import logging
from config.settings import settings
logger = logging.getLogger(__name__)
class MarkdownDocumentProcessor(DocumentProcessor):
def __init__(self, input_path: str, output_path: str):
super().__init__() # Call parent class's __init__
self.input_path = input_path
self.output_path = output_path
self.ollama_client = OllamaClient(model_name=settings.OLLAMA_MODEL, base_url=settings.OLLAMA_API_URL)
def read_content(self) -> str:
"""Read markdown content from file"""
try:
with open(self.input_path, 'r', encoding='utf-8') as file:
content = file.read()
logger.info(f"Successfully read markdown content from {self.input_path}")
return content
except Exception as e:
logger.error(f"Error reading markdown file {self.input_path}: {e}")
raise
def save_content(self, content: str) -> None:
"""Save processed markdown content"""
try:
# Ensure output directory exists
output_dir = os.path.dirname(self.output_path)
os.makedirs(output_dir, exist_ok=True)
with open(self.output_path, 'w', encoding='utf-8') as file:
file.write(content)
logger.info(f"Successfully saved masked content to {self.output_path}")
except Exception as e:
logger.error(f"Error saving content to {self.output_path}: {e}")
raise

View File

@ -14,6 +14,7 @@ logger = logging.getLogger(__name__)
class PdfDocumentProcessor(DocumentProcessor):
def __init__(self, input_path: str, output_path: str):
super().__init__() # Call parent class's __init__
self.input_path = input_path
self.output_path = output_path
self.output_dir = os.path.dirname(output_path)
@ -37,13 +38,12 @@ class PdfDocumentProcessor(DocumentProcessor):
os.makedirs(self.work_local_image_dir, exist_ok=True)
self.ollama_client = OllamaClient(model_name=settings.OLLAMA_MODEL, base_url=settings.OLLAMA_API_URL)
def read_content(self) -> bytes:
with open(self.input_path, 'rb') as file:
return file.read()
def process_content(self, content: bytes) -> dict:
def read_content(self) -> str:
logger.info("Starting PDF content processing")
# Read the PDF file
with open(self.input_path, 'rb') as file:
content = file.read()
# Initialize writers
image_writer = FileBasedDataWriter(self.work_local_image_dir)
@ -78,19 +78,21 @@ class PdfDocumentProcessor(DocumentProcessor):
middle_json = pipe_result.get_middle_json()
pipe_result.dump_middle_json(md_writer, f'{self.name_without_suff}_middle.json')
logger.info("Masking content")
sentences = md_content.split("")
final_md = ""
for sentence in sentences:
formatted_prompt = get_masking_mapping_prompt(sentence)
logger.info("Calling ollama to generate response, prompt: %s", formatted_prompt)
response = self.ollama_client.generate(formatted_prompt)
logger.info(f"Response generated: {response}")
final_md += response + ""
return final_md
return md_content
# def process_content(self, content: str) -> str:
# logger.info("Starting content masking process")
# sentences = content.split("。")
# final_md = ""
# for sentence in sentences:
# if not sentence.strip(): # Skip empty sentences
# continue
# formatted_prompt = get_masking_mapping_prompt(sentence)
# logger.info("Calling ollama to generate response, prompt: %s", formatted_prompt)
# response = self.ollama_client.generate(formatted_prompt)
# logger.info(f"Response generated: {response}")
# final_md += response + "。"
# return final_md
def save_content(self, content: str) -> None:
# Ensure output path has .md extension

View File

@ -7,6 +7,7 @@ from config.settings import settings
logger = logging.getLogger(__name__)
class TxtDocumentProcessor(DocumentProcessor):
def __init__(self, input_path: str, output_path: str):
super().__init__()
self.input_path = input_path
self.output_path = output_path
self.ollama_client = OllamaClient(model_name=settings.OLLAMA_MODEL, base_url=settings.OLLAMA_API_URL)
@ -15,12 +16,12 @@ class TxtDocumentProcessor(DocumentProcessor):
with open(self.input_path, 'r', encoding='utf-8') as file:
return file.read()
def process_content(self, content: str) -> str:
# def process_content(self, content: str) -> str:
formatted_prompt = get_masking_prompt(content)
response = self.ollama_client.generate(formatted_prompt)
logger.debug(f"Processed content: {response}")
return response
# formatted_prompt = get_masking_prompt(content)
# response = self.ollama_client.generate(formatted_prompt)
# logger.debug(f"Processed content: {response}")
# return response
def save_content(self, content: str) -> None:
with open(self.output_path, 'w', encoding='utf-8') as file:

View File

@ -57,10 +57,12 @@ def get_masking_mapping_prompt(text: str) -> str:
2. 公司名映射规则
- 保留地理位置信息北京上海等
- 保留公司类型有限公司股份公司等
- ""替换核心名称
- ""替换核心名称,但保留首尾字(北京智慧科技有限公司 北京智某科技有限公司)
- 对于多个相似公司名使用字母区分
北京智慧科技有限公司 北京某科技有限公司
北京智能科技有限公司 北京某科技有限公司A
3. 公权机关不做脱敏处理公安局法院检察院中国人民银行银监会及其他未列明的公权机关
请分析以下文本并生成一个JSON格式的映射表包含所有需要脱敏的名称及其对应的脱敏后的形式
@ -72,6 +74,8 @@ def get_masking_mapping_prompt(text: str) -> str:
"原文2": "脱敏后2",
...
}}
如无需要输出的映射请输出空json如下:
{{}}
""")
return prompt.format(text=text)

141
src/utils/json_extractor.py Normal file
View File

@ -0,0 +1,141 @@
import json
import re
from typing import Any, Optional, Dict, TypeVar, Type
T = TypeVar('T')
class LLMJsonExtractor:
"""Utility class for extracting and parsing JSON from LLM outputs"""
@staticmethod
def extract_json(text: str) -> Optional[str]:
"""
Extracts JSON string from text using regex pattern matching.
Handles both single and multiple JSON objects in text.
Args:
text (str): Raw text containing JSON
Returns:
Optional[str]: Extracted JSON string or None if no valid JSON found
"""
# Pattern to match JSON objects with balanced braces
pattern = r'{[^{}]*(?:{[^{}]*}[^{}]*)*}'
matches = re.findall(pattern, text)
if not matches:
return None
# Return the first valid JSON match
for match in matches:
try:
# Verify it's valid JSON
json.loads(match)
return match
except json.JSONDecodeError:
continue
return None
@staticmethod
def parse_json(text: str) -> Optional[Dict[str, Any]]:
"""
Extracts and parses JSON from text into a Python dictionary.
Args:
text (str): Raw text containing JSON
Returns:
Optional[Dict[str, Any]]: Parsed JSON as dictionary or None if parsing fails
"""
try:
json_str = LLMJsonExtractor.extract_json(text)
if json_str:
return json.loads(json_str)
return None
except json.JSONDecodeError:
return None
@staticmethod
def parse_to_dataclass(text: str, dataclass_type: Type[T]) -> Optional[T]:
"""
Extracts JSON and converts it to a specified dataclass type.
Args:
text (str): Raw text containing JSON
dataclass_type (Type[T]): Target dataclass type
Returns:
Optional[T]: Instance of specified dataclass or None if conversion fails
"""
try:
data = LLMJsonExtractor.parse_json(text)
if data:
return dataclass_type(**data)
return None
except (json.JSONDecodeError, TypeError):
return None
@staticmethod
def parse_raw_json_str(text: str) -> Optional[Dict[str, Any]]:
"""
Extracts and parses JSON from text into a Python dictionary.
Args:
text (str): Raw text containing JSON
Returns:
Optional[Dict[str, Any]]: Parsed JSON as dictionary or None if parsing fails
"""
try:
json_str = LLMJsonExtractor.extract_json_max(text)
if json_str:
return json.loads(json_str)
return None
except json.JSONDecodeError:
return None
@staticmethod
def extract_json_max(text: str) -> Optional[str]:
"""
Extracts the maximum valid JSON object from text using stack-based brace matching.
Args:
text (str): Raw text containing JSON
Returns:
Optional[str]: Maximum valid JSON object as string or None if no valid JSON found
"""
max_json = None
max_length = 0
# Iterate through each character as a potential start of JSON
for start in range(len(text)):
if text[start] != '{':
continue
stack = []
for end in range(start, len(text)):
if text[end] == '{':
stack.append(end)
elif text[end] == '}':
if not stack: # Unmatched closing brace
break
opening_pos = stack.pop()
# If stack is empty, we have a complete JSON object
if not stack:
json_candidate = text[opening_pos:end + 1]
try:
# Verify it's valid JSON
json.loads(json_candidate)
if len(json_candidate) > max_length:
max_length = len(json_candidate)
max_json = json_candidate
except json.JSONDecodeError:
continue
return max_json