news 2026/10/13 22:53:02

本地优先的知识工作流插件集:从收藏到复盘的全流程自动化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
本地优先的知识工作流插件集:从收藏到复盘的全流程自动化

很多人问我,为什么别人用一个收藏工具就能完成的“稍后阅读”,我要写成一个插件集。这个问题其实戳中了一个长期被忽略的真相:知识工作者的效率瓶颈,从来不是某个单点工具的缺失,而是信息从“看过”到“用得起来”之间的断档。我维护的这套 knowledge-work-plugins 不是什么框架级的大项目,它更像是一组围绕“收集—清洗—归档—回看”这条个人工作流定制的插件集合,每个插件解决一个非常具体的动作,彼此之间靠统一的数据格式衔接。

这套插件集适合谁?适合像我这样每天要大量处理网页资料、论文、文档、聊天消息和下载文件的人;适合那些已经攒了上千条收藏、却发现检索比重新搜索还慢的人;也适合想动手改进自己的工作流,但不想被某个重型软件绑架的人。这篇文章我把项目的起因、整体架构、核心插件的实现逻辑、选型取舍和一次完整的踩坑排错过程都写清楚,希望能给你一些可复用的思路。

1. 收藏夹吃灰,问题不在“懒”,而在流程断点——项目的起点

1.1 知识工作者真实的信息处理路径

先还原一下我自己的工作状态。白天大部分时间在处理不同来源的信息:浏览器里开了十几个标签页,看到有参考价值的文章先丢进“稍后读”;文件下载目录里堆积着 PDF、表格、图片、代码压缩包,隔几天就乱成一锅粥;聊天工具里同事发来的关键截图、设计稿、会议纪要散落在各个会话里;自己的笔记软件里也存了一堆靠的临时草稿。

如果把这些动作用一条时间线串起来,你会发现真正的问题不是“信息来源多”,而是“每个环节之间的衔接全靠手动”。浏览器收藏夹只负责收集;下载文件夹只负责接收文件;笔记软件只负责存储;搜索工具只负责在已经存入的内容里找东西。这四个环节互相之间没有任何约定,没有统一的命名规则,没有自动清洗,也没有反馈回路。于是知识工作的日常变成了:花大量时间收集,花更多时间整理,最后在回顾时发现很多东西当时没记下出处、没存下要点,重新回头翻原始资料成本极高。

这个现象我身边很多人都有,大家一般归因于“自己懒”。但当我认真梳理了一遍信息处理路径之后,发现这不是懒,是流程断点太多。打个比方,就像厨房里有人负责买菜、有人负责洗碗,但中间没有切配台,做菜的人还得自己去菜堆里挑菜、洗菜、切菜,忙到一半发现少个调料还得再去趟超市。真正缺的不是某一把好刀,而是一条说得清楚的加工流水线。

1.2 为什么我决定自己写一套插件集而不是继续妥协

那时候市面上能用的通用类工具我基本都试过一轮。网页剪藏类工具体验顺滑,收集环节确实做到了“一键”,但它们对后来的归档和回顾很少操心,剪进来的内容就像扔进了一个永远不整理的文件柜。笔记类软件在结构化笔记、双链、标签这些概念上做得很好,可是一旦涉及自定义解析规则、批量处理、脚本化操作,就处处受限。搜索类工具能让“找到”这一点变快,但它救不了“当时根本没存进去”的东西。

最让我没法忍的一点是:大多数通用工具都以“产品形态”为中心,你得适应它的文件夹结构、它的模板、它的标签体系,而不是它来适应你的工作流。我自己的工作流里有些很具体的需求——比如从长文章中提取表格数据并按项目目录归档、把下载文件夹里的一堆文件按照“内容类型+项目名”自动分类、给剪藏笔记自动补全元信息并去重——这些需求在通用工具里要么没有,要么用得很别扭。

于是在某个周末,我开始动手写一个自己的插件集。当时定的原则很简单:第一,每个插件只做一件小事;第二,所有插件之间不直接互相调用,而是通过统一的中间格式衔接;第三,所有数据本地存储,纯文本优先,不依赖任何云服务;第四,插件集本身是可拆卸的,任何一个插件坏了或者不想要了,直接删掉,不影响其他功能。

这个项目就是现在的 knowledge-work-plugins。它不是一个像“生产力套件”那样听起来宏大的东西,它就是围绕我的真实工作场景长出来的一组小工具。这个过程中我发现,工作流类工具最大的成就感不是哪个插件写得多么炫酷,而是当你三个月后看到新收藏的内容自动带着规范的元信息落在正确的目录里时,那种“流程终于自己跑通了”的感觉。

2. 插件集整体架构:一个本地优先、可拆卸的“加工流水线”

2.1 插件分类与依赖关系设计

整套插件集按照知识处理的不同阶段分成四层:采集层、提炼层、归档层、复盘层。

表格化分类如下:

层级职责典型插件输入输出
采集层把外部信息变成本地中间格式网页剪藏、剪贴板收集、文件监听原始URL、剪贴板内容、新文件Markdown笔记或标准化文件
提炼层从中间格式里抽出有价值的信息速览卡片、去重、元信息补全已入库的Markdown笔记带结构化字段的强化笔记
归档层按规则整理命名和目录文件自动归类、命名规范、主动纠偏下载目录、导出文件规整后的项目目录
复盘层把积累的内容转成行动和回顾周报汇总、回顾清单、断链检查全部本地笔记每周小结、回顾提示

依赖关系上有一个很严格的约束:上层可以读取下层的输出,但下层绝对不反向依赖上层。比如归档层永远不会主动去调用提炼层的接口,复盘层也只需要读取归档后的文件目录。这保证了整个系统是单向流动的,任何一个环节升级或删除,最多影响它自己的输出格式,不会把上下游的数据搞乱。

2.2 数据格式的统一:一切向 Markdown 收口

如果只让我说一条这个项目最重要的设计决策,那就是“一切向 Markdown 收口”。无论是网页剪藏、聊天记录导出、PDF 摘录还是代码片段,最终落到本地磁盘上的都是 Markdown 文本文件。为什么选 Markdown 而不是 JSON 或者某个笔记软件的专有格式?原因有三。

第一,Markdown 是纯文本,十年后打开还能看,不会因为某个软件停维护就数据报废。第二,文本文件天然适合 diff 和版本管理,我要给笔记加标签、改标题、调整正文,用常见的文本对比工具就能看清楚改了什么。第三,Markdown 的渲染生态足够成熟,终端、网页、桌面编辑器都能看,我不必被绑定在某一个客户端里。

为了让插件之间能识别彼此产生的文件,我定义了一套固定的中间格式:每个笔记文件的开头是 YAML 格式的元信息区,后面是正文正文内容。字段里包含 id、type、source、created、tags 等,下面是一个示例。

--- id: 20250215-1842-article-001 type: web title: 关于本地优先工具的几点思考 source: https://example.com/path/to/article created: 2025-02-15 18:42 tags: [本地优先, 工具, markdown] status: unprocessed ---

正文区域没有任何额外约定,谁都会写。这套中间格式就是整个插件集的“共同语言”。新插件不需要理解其他插件内部怎么实现,只需要读这个 YAML 头部,处理正文,然后按同样规范输出新文件。我在项目文档里把它称为“所有插件的接口协议”,实际上它才是整套架构真正的地基。

2.3 插件的“三件套”约定:入口、配置、日志

每个插件不管功能多简单,都遵守一个“三件套”约定:一个统一入口、一份本地配置文件、一套独立的运行日志。目录结构大致如下:

knowledge-work-plugins/ ├── core/ │ ├── entry.py # 统一入口,负责路由到各插件 │ └── config.py # 读取全局配置 ├── plugins/ │ ├── fl-clip/ # 网页剪藏插件 │ │ ├── plugin.py # 实现的入口函数 │ │ ├── config.yaml # 插件级配置 │ │ └── log/ │ ├── ds-card/ # 速览卡片插件 │ │ └── ... ├── interim_storage/ # 中间态存储 └── scripts/

每个插件的入口函数签名都是统一的:接收一个文件路径或一组参数,返回处理结果。配置采用 YAML,因为人读起来方便,改起来也不用重新编译。日志独立存放,每个插件都有自己的日志文件,排查问题时不用在全局日志里大海捞针。

这个“三件套”最初是我自己觉得好维护,后来有朋友要往这个项目里加插件,只需要照着现有插件的目录结构复制一套,把逻辑填进去,半小时内就能跑通。这让我深刻体会到,“约定大于配置”不是一句空话,它能实打实降低插件的接入门槛。

3. 三个高频插件的实现逻辑与关键代码

3.1 剪藏插件:从网页到本地笔记的清洗管线

剪藏插件是整个插件集里用得最频繁的,也就是把浏览器里正在看的文章保存到本地笔记目录。这个过程看起来简单,真正做起来坑很多。网页上除了正文还有导航栏、页脚、相关推荐、广告、分享按钮、评论区,如果原样保存,笔记文件里会塞满噪音。我最初的版本就是直接把整个页面的内容丢进 Markdown,结果笔记又长又没法检索,基本等于没有剪藏。

后来我重新设计了清洗管线,流程如下:

  1. 抓取目标页面 HTML,先拿到完整的原始文档。
  2. 解析出主标题、作者、发布时间、主图等元信息。
  3. 定位正文容器,把容器内部的导航、侧栏、推荐模块排除掉。
  4. 对正文做结构化转换:标题、段落、列表、表格、代码块分别映射到 Markdown 对应语法。
  5. 把元信息写入 YAML 头部,正文写入 Markdown 正文区。
  6. 按配置生成目标文件名,写入本地目录,并在日志中记录处理情况。

核心逻辑用 Python 写的话,大概是下面这个思路。这里我不写完整实现,只描述关键流程。

def clean_html_to_markdown(html, url): title = extract_title(html) meta = extract_meta(html) main_content = locate_main_content(html) blocks = split_into_content_blocks(main_content) md_body = convert_blocks_to_markdown(blocks) note = wrap_with_yaml(title=title, meta=meta, source=url, body=md_body) return note

这个清洗管线和“选个模板直接保存”之间的差别,用一句总结就是:前者保存的是“文章”,后者保存的是“网页”。对于需要长期回看和检索的知识工作者来说,这个差别是决定性的。

3.2 速览卡片插件:把论文长句压缩成可回看的要点

知识工作的另一个高频场景是读论文和技术文章。很多内容当时读完觉得明白了,两周后回看笔记,发现笔记里全是原文段落,根本看不出重点。我需要一种把长内容压缩成“可回看要点”的插件,它做的事就是速览卡片化——不是做智能摘要,而是做结构化抽取。

我并没有用复杂的生成式模型,因为对于本地优先的插件集来说,重量级模型既不合适也不可控。我用的是几条规则加统计特征组合出来的方法:

  • 识别文章里加粗、斜体、引用块这些本身就代表强调的文本。
  • 提取每个段落的开头句,因为作者通常会把核心观点放在句首。
  • 如果文章包含小标题,就把小标题和它下面段落里的高频词组拼接成要点。
  • 对关键表格,抽取表头和第一行数据,形成“字段=值”的压缩形态。

输出结果会转成一个双栏表格,左边是原文位置,右边是压缩后的要点。例如:

原文位置要点
第 2 节,第三段作者认为本地优先工具的价值不在“离线”,而在“数据主权”
表 1样本量为 128,对照组误用率 18%,实验组 6%

这样一段内容回看起来非常高效。我做这个插件的体会是:摘要不是越“智能”越好,很多时候规则驱动的结构化抽取已经能覆盖 80% 的需求,而且可解释、可调整、不出意外。

3.3 文件整理插件:按项目语境自动归档下载目录

第 3 个高频插件是文件整理插件。下载目录大概是每个知识工作者心里的痛,一段时间不整理就会堆积几十个版本类似的 PDF、图片、压缩包和临时文件。我写的这个插件不会强制移动任何文件,它默认先做“模拟整理”,在配置正确并确认之后才真正执行移动操作。

它的核心是一个规则引擎,规则按“文件扩展名+来源站点+文件名关键词”三个维度组合成目标目录,示例如下:

rules: - pattern: "*.pdf" source: "research.example.com" dest: "archives/papers" - pattern: "IMG_*.png" dest: "archives/images/inbox" - pattern: "*.zip" dest: "archives/packages"

关键的安全设计是“默认不移动”:插件先生成一个整理预览报告,列出每个文件当前路径和目标路径,人工确认后才执行。我之前吃过亏——自动移动脚本跑了一次,把一个我还需要引用的旧版本文档从原目录带走了,之后好几天没找到。后来我把所有文件操作都加上了“dry-run”模式,只打印报告不实际操作。这个教训让我意识到,工具处理的是珍贵的知识资产,宁可慢一步,不能错一次。

4. 选型时的几个硬性取舍:为什么不用云端服务和重型客户端

4.1 本地优先是“数据主权”问题,不是“偏执”

很多朋友看到我折腾这套插件集,第一反应是“为什么不上云?同步多方便”。我理解云同步的价值,但在这个项目上,我坚持本地优先。理由其实不复杂:我需要确保所有笔记、剪藏和归档文件在任何时候都可以被直接访问和复制,不依赖网络、不依赖某个第三方服务是否继续运营、不依赖某个传输通道是否通畅。

云服务在我看来更像一个“同步层”,而不是“存储层”。数据的主副本应该始终在自己能直接控制的磁盘上,云端只是多一份异地备份的渠道。我用过一个很形象的类比:本地磁盘是家里的文件柜,云同步是把重要文件复印一份放到银行保险柜里备用,但你不应该把唯一原件放在快递途中。知识资产里很多内容带有个人积累和未公开的信息,让它们的主副本始终留在本地,既是为了格式可控,也是为了避免依赖。

4.2 插件之间只通过文件系统通信,而不是进程间耦合

整套插件集里所有的数据交换都通过文件系统完成,插件之间没有进程内通信、没有数据库、没有 HTTP 服务。每个插件读文件,处理完写文件。这个选择看起来“低技术含量”,实际效果却出奇地稳。

文件系统作为通信中介有四个好处。第一,可调试性强:随便打开一个文件就能看到上下游交换了什么数据,不用通过抓包或断点去猜。第二,可回滚:插件处理完发现不对劲,直接看旧文件就能恢复。第三,进程崩溃不容易影响数据:写得再差的脚本最多留下一堆中间文件,不会把整个系统带崩。第四,没有常驻进程:每个插件都可以按需运行,不需要维护一个服务集群。

我在目录里用一套“inbox / 工作区 / archive”式的中间态存储来模拟异步通信。如果某个插件失败了,它对应的输入文件就留在待处理目录里,日志里会有记录,下次运行重新处理即可。实际使用中,这种“文件即消息”的设计足够简单,也足够可靠。

4.3 为什么不做一个大而全的“All-in-One”

最初我也想过要不要把所有功能揉进一个大工具里,做一个界面精美的单应用。后来想明白一个道理:插件集的组合价值远大于单体价值。单应用的问题在于,任何一个功能模块的变化都可能影响整个应用的行为,升级一个模块往往要重新验证所有功能。而插件集像是一组独立的刀具,哪把刀钝了磨哪把,哪把刀不适合当前任务就先收起来,不会影响其他刀的使用。

再加上维护成本。我现在可以很坦率地说,从长期维护的角度,单体应用的心理负担比插件集大得多。单人项目最难的是持续投入,如果每改一个功能都要进入“庞大代码库”的上下文,很容易被劝退。插件集的边界清晰,能让我每次只需要关注一个目录、一个入口、一份日志,这种“小步维护”的节奏,才是个人项目能持续更久的关键。

5. 踩坑实录:剪藏插件在长文页面上的字段串位问题

5.1 问题表现与排查链路

实际上手之后踩得最深的一个坑,出现在剪藏插件的元信息提取环节。当时现象是这样的:某天我剪藏一篇很长的行业分析文章,完成后打开本地笔记,发现 YAML 头部里的 tags 字段不是我预期的标签,而是一串表格数据;正文也不完整,后半段被截断了。最麻烦的是,这条笔记在“采集成功”的日志里显示一切正常,没有任何报错。

我先按正常的排查链路走了一遍。第一步,回到原始网页,重新手工保存一份 HTML,确认页面上确实有完整的表格数据。第二步,用解析工具把 HTML 内部结构打印出来,重点看这些表格数据到底出现在文档的哪个区域。第三步,对比插件里元信息提取规则扫描到的位置和预期位置。这一步很快发现问题:元信息提取规则匹配到了页面侧栏的一个“相关参数表格”里的标签文本,而这些内容在 DOM 顺序上竟然排在正文之前。

5.2 根因:表格化数据和正文文本的同名标签

根因在于元信息提取规则写得太宽泛。当时我用的选择规则是“匹配页面中所有含某些关键词的文本节点,取第一个作为标签”,本意是抓取某些 SEO 关键词或者作者自带的标签字段。但在长文章页面中,这类关键词不只出现在文档头部元信息里,还出现在侧栏的“相关参数表格”“热点标签”“文章子分类”中。由于这些侧栏模块在页面模板里的位置比正文更靠前,插件就匹配到了它们,导致 YAML 头里的 tags 被污染。正文截断也是类似原因——当正文容器定位规则遇到侧栏表格的特殊结构时,容器边界计算错误,后半段正文被误判为页脚。

为什么只有长文才触发?因为短文页面侧栏的“相关模块”通常没有足够的相似标签,而长文章页面为了露出更多附加信息,往往会在头部区域直接渲染一套完整的参数表格,让提取规则有了可乘之机。查出这个原因后,我一度想通过排除规则把“表格区域”直接过滤掉,但很快意识到这治标不治本:换个站点模板就又可能出问题。

5.3 修复方案与回归验证

最终修复思路是改变提取策略:不再从全页范围“从头找标签”,而是先定位正文容器,再从正文容器内部反向提取字段信息。换句话说,先把页面最关键的部分锁定,再去读元信息,而不是先读元信息再想办法找正文。正文容器定位也加入了多层校验:容器长度超过阈值才视为正文;容器内链接占比过高则排除;容器结尾必须匹配标题对应结构或其他结束标志。

修复之后,我在脚本里加了一个数据完整性校验函数,每次剪藏完成就跑一遍:

def validate_note(yaml_head, md_body): errors = [] if len(yaml_head.get("tags", [])) > 20: errors.append("tags field too large, possible field-misalignment") if len(md_body) < 200: errors.append("body too short, possible truncated extraction") if "table" in yaml_head.get("tags", []) and "相关" in md_body[:100]: errors.append("possible contamination from sidebar table") return errors

然后我把过去踩过坑的十几类页面整理成一个回归样本集,每次修改剪藏插件都全部跑一遍,确保不再犯同类错误。这次事故还有一个后续收益:我学会了在插件里尽量暴露中间过程。剪藏插件会把“提取到的元信息”和“定位到的正文区域”单独存一份调试文件,出问题时不用重新抓页面,直接看这份调试文件就能定位。

6. 扩展与维护:插件集的复利来自“格式自律”

6.1 新插件接入成本如何控制在半小时内

项目走到现在,最常被问的是“我要加一个新插件,难不难”。我设计的目标是让接入成本控制在半小时内。因为有统一的中间格式和三件套约定,新插件只需要做一件事:读 Markdown 的 YAML 头部,处理正文,再写回一个新的 Markdown 文件。

拿我最近加的一个“重复笔记检测插件”举例:这个插件的功能是扫描本地笔记,找出标题或正文相似度较高的条目,输出一个风险列表。它完全不需要理解剪藏插件怎么抓页面、不需要理解速览卡片怎么压缩要点,只需要读所有笔记文件的标题和正文哈希,计算相似度,生成一份报告。一两百行代码以内就完成了。这种低接入成本让“根据需求随时加一个工具”变成了现实,也让插件集的整体生态能够随实际使用慢慢长出来。

6.2 维护计划:每周的“格式体检”

单一插件简单,合起来之后数据质量却容易漂移。我养成了一个习惯:每周跑一次“格式体检”脚本。它会统计所有笔记中 YAML 字段的完备率、正文长度分布、失效链接数量、标签使用频次、待处理文件数量等指标,生成一份简短报表。

表格里大致长这样:

指标本周数值上周数值说明
笔记总数12601228新增 32 条
元信息完备率94.2%93.1%字段校验通过比例
断链数量1713外部链接已失效
待处理文件数69下载目录积压

格式体检的核心目的不是做监控仪表盘好看,而是把“格式自律”变成一项自动化的护栏。当某个网站的页面结构变化导致所有新笔记的标题提取异常时,体检报表里元信息完备率会立刻掉下来,我就能在数据污染扩散之前处理掉。没有这套护栏,插件集用上两三个月,中间格式就会慢慢被各种异常夹杂破坏。

6.3 后续想做的方向与个人体会

到目前为止,knowledge-work-plugins 已经覆盖了从采集到复盘的完整流程,但还有几个方向我准备了很久没动手。一个是“回看提醒”插件:根据笔记的创建时间和上次打开时间,每周生成一份“很久没碰的笔记”清单,防止收藏完就被遗忘。另一个是“标题联想”插件:在新笔记创建时,扫描已有笔记标题,提示可能相关的旧笔记,帮助建立知识连接。还有一个是“来源信誉标记”插件:给常用的信息源打一个可信度标签,剪藏时自动标注。

最后说说我在维护这个项目过程中的个人体会。做这类个人工具,真正难的不是功能实现,而是“克制”。一开始我总想做得更快、更全、更智能,结果复杂度上来之后,维护意愿反而下降。后来我慢慢调整了策略:每个插件都保持小而稳,一个阶段只重点打磨一两个最常用的功能。工具的价值在于它能不能一直长在你手上、让你愿意持续用,而不是功能清单有多长。这个判断标准,我觉得比任何技术选型都重要。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 22:48:55

客户流失预测不靠玄学:从数据清洗到MLP的完整流程

简介&#xff1a;这是一份面向大数据技术类专业师生的《Python机器学习编程与实战》第8章教学教案&#xff0c;针对通信运营商客户流失分析与预测场景&#xff0c;讲解从数据预处理到分类建模的完整流程。教案覆盖数据去重、PCA降维、缺失值与异常值处理、独热编码、训练集与测…

作者头像 李华
网站建设 2026/10/13 22:48:24

制造业进销存选型:适配性比功能全更重要

制造业老板们聊起进销存软件&#xff0c;第一句话往往是“我想要一套功能全的”。但这几年我越来越觉得&#xff0c;选型真正该盯着的不是功能全不全&#xff0c;而是适配性够不够。同样是进销存&#xff0c;贸易商拿来管一买一卖&#xff0c;制造企业要管的是采购计划、生产领…

作者头像 李华
网站建设 2026/10/13 22:47:59

HDwiki 6.0 部署实战:PHP+MySQL 中文维基系统落地指南

简介&#xff1a;互动百科HDwiki 6.0 UTF8 是一套面向中文站长与PHP开发者开源的维基建站系统&#xff0c;专为构建专业级中文百科、知识库或企业内部Wiki平台设计&#xff0c;兼顾二次开发灵活性与生产环境安全性。资源为完整可部署的PHP源码包&#xff0c;含核心程序、后台管…

作者头像 李华
网站建设 2026/10/13 22:47:28

Vue3+ExcelJS前端导出Excel:样式控制与常见坑位全解析

做后台管理系统开发的朋友应该都有同感&#xff0c;"把当前表格导出成Excel"这个需求&#xff0c;几乎在每个项目里都会撞见。以前我习惯把活甩给后端&#xff0c;后端拼文件、返回地址、前端触发下载&#xff0c;一条链路看似顺畅&#xff0c;但一旦碰样式就难受了。…

作者头像 李华
网站建设 2026/10/13 22:46:45

AI搜索时代,官网内容优化与企业级RAG谁先做?

企业官网的定位&#xff0c;最近两年其实一直在被重新定义。过去我们说官网是企业对外展示的门面&#xff0c;做得好一点能带来线索&#xff0c;做得平庸一点就只是个“在线宣传册”。但到了AI搜索逐渐成为用户获取信息主入口的今天&#xff0c;很多企业开始发现&#xff0c;官…

作者头像 李华
网站建设 2026/10/13 22:46:23

COMSOL超表面仿真:从S参数相位到GH位移计算的完整流程

做超表面仿真的人迟早会遇到这样一个场景&#xff1a;透过率、反射率的谱线调得很顺眼&#xff0c;但对方突然一句“相位响应呢”&#xff0c;整个思路就开始冒汗。我最近在做一个光子晶体超表面的透反射分析时&#xff0c;被问到了一个更具体的量——光束斜入射后反射光在界面…

作者头像 李华