news 2026/10/27 5:56:50

我如何用FastAPI+React自建开源AI工作台替代workbuddy

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
我如何用FastAPI+React自建开源AI工作台替代workbuddy

我重度用了半年多的 workbuddy,笔记、任务、AI 问答全塞在里面,后来却越来越难受:数据锁在云端、技能没法自定义、免费额度老见底。被逼到一定程度,我干脆花几个周末自己写了一个开源版替代品,叫魔力工作台。这篇文章就是把我从方案选型、架构设计到实操落地的完整过程写出来,顺带把部署时踩过的坑也一并交代清楚。如果你也在找 workbuddy 的替代方案,或者想做一个属于自己的 AI 工作台,这篇应该能帮你省下不少试错时间。

1. 为什么我要亲手做一个开源版 workbuddy 替代

1.1 研究、学习和写代码的人,需要一个怎样的“工作台”

先说清楚一个概念:这里说的“工作台”,不是那种放满快捷键的桌面,也不是 CI/CD 那种流水线控制台,而是把“记录、检索、思考、执行”揉在一起的个人 AI 工作环境。我身边不少搞科研、做全栈开发、写教学案例的朋友,都习惯在 workbuddy 里攒项目笔记、整理文献、让 AI 帮忙跑一些固定流程。我在社科研究阶段就极度依赖这类工具,论文思路、访谈记录、数据脚本全堆在里面,久而久之,它几乎成了我的外置大脑。

但外置大脑最怕什么?最怕它哪一天不能用了,或者不让你的数据出去。workbuddy 在智能体编排和知识库管理上确实做得顺手,尤其是它的 Skill 机制,把一些高频诉求(翻译、润色、文献摘要、代码解释)封装成可复用模块,用起来很愉悦。我一开始也是看中这点才入坑的。可随着我往里塞的东西越来越多,问题就开始冒头了。

首先是数据主权问题。我用 workbuddy 记录了大量半成品想法和未发表的研究草稿,这些内容存在别人服务器上,始终觉得不稳妥。我尝试过导出,但它导出的格式是私有的 JSON,换工具后基本没法继续用。其次是扩展性天花板,我想做一个针对特定课题定制的技能,把本地文献库和模型调用串起来,workbuddy 的配置项不开放,写不了这种深度逻辑。后来我意识到:我不是要一个功能更多的 workbuddy,我要的是一个数据在自己手里、逻辑自己能改的工作台。

1.2 我为什么没继续用 workbuddy

做个开源替代品,并不是因为 workbuddy 不好,而是它的定位和我的需求出现了错位。具体拆开讲,有三点让我下定决心自己动手。

第一点是成本结构不透明。workbuddy 的免费额度对轻度用户够用,但我这种每天几十次问答轰炸的人,很快就撞上付费墙。它按模型调用量和存储空间双重计费,算下来一个月大几十块,倒也不是付不起,只是我不喜欢这种“用越多越贵”的模式。我自己有 OpenAI 兼容的 API,也有本地的 Ollama,为什么不让工具直接调用它们?

第二点是数据链路太长。我要的是“笔记写作 -> 知识库检索 -> 模型问答 -> 任务跟踪”在一个闭环里完成,workbuddy 能做到,但它中间加了很多层服务端逻辑,响应慢,出问题定位难。有一次我改了一条笔记,第二天发现问答里引用的还是旧版本,缓存策略完全不透明。这类问题社区里反馈很多,官方也没给彻底解释。

第三点,也是最重要的一点:我本身是写代码的。用着别人闭源的 AI 工具,就像住在一个好看但拿不到户型的公寓里——只能敲承重墙装饰,不能拆了重建。我需要的是能看完整源码、改一个字段就知道会改动哪里的工具。开源文化本来就该这样:发现问题自己修,想加功能自己写,不合意就 fork。

1.3 开源替代的边界:不做复刻,做“够用且可控”

定下目标之后,我一开始也想做一个像素级复刻 workbuddy 的东西。后来想明白了,这是自己给自己挖坑。任何成熟产品背后都是几十人团队填了几年的细节,我一个人在业余时间做复刻,既不可能也没必要。

我的选择是:抓三件事做到及格线以上。一是知识库管理,能把 Markdown 笔记批量导入,向量化后支持语义检索;二是技能机制,通过简单的配置文件就能添加新工具,让模型按需调用;三是数据本地化,所有数据落在一个本机目录里,随时能备份、迁移、看懂。至于界面好看不好看、有没有移动端、能不能多人协作,第一版全部放弃。

这个思路也决定了这个项目能真正落地。开源项目最怕的就是野心太大,后面维护不住。我只承诺“个人 AI 工作台”这个场景,把单机模式下体验打磨顺,就已经能覆盖绝大多数研究型用户的需求了。所以魔力工作台的 README 第一句话就是:这是一个给研究者、写作者和独立开发者用的轻量 AI 工作台,不是团队协作平台。

2. 魔力工作台的整体架构与设计思路

2.1 产品定位:个人 AI 工作台,不是团队协作平台

确定不做协作功能之后,整个产品逻辑都清晰了:不需要用户体系、不需要多租户、不需要权限控制。我只需要一个单用户、本地运行、数据落盘的 Web 应用。从另一个角度看,这反而是最稳的起点,因为单用户架构省掉了我处理并发、隔离、审计这些大量精力,可以把全部时间花在检索质量和技能编排上。

实际使用场景是这样一个闭环:我在浏览器里打开魔力工作台,往笔记区贴想法,系统自动切分、向量化并存入本地;我有疑问时,在对话框里提问,系统先从知识库召回相关片段,再拼上下文给模型;要执行重复性动作(比如批量总结访谈记录),我写好或选好一个技能,点一下就跑起来。整个过程没有一条数据离开我的电脑,除非我自己配置了远程模型 API。

做个直观对比:

能力workbuddy(闭源)魔力工作台(开源)
数据存储云端,私有格式本地 SQLite + Markdown,明文可读
模型接入平台内置任意 OpenAI 兼容 API / 本地 Ollama
技能扩展受限配置JSON 声明 + Python 函数
离线使用部分功能依赖网络接本地模型即可全离线
二次开发不支持完整源码可改

这个定位还有一个好处:容易给新用户解释。任何人都能一眼看懂“你自己的电脑上长出一个 AI 工作台”是什么意思,不用去理解什么云原生、微服务、权限模型。

2.2 技术选型逻辑:FastAPI + React + SQLite 的组合

技术选型我反复纠结过,最终敲定这组快、稳、省心的组合。后端用 Python 的 FastAPI,因为 Python 在 AI 生态里的地位不可撼动,无论是接 OpenAI API 还是跑本地模型,都有大量现成代码参考;FastAPI 本身性能足够,异步支持完善,配一个 uvicorn 就能跑,开发效率极高。

前端我用 React + Vite + Tailwind CSS。本来也想用 Next.js,但对一个纯本地应用来说太浪费了,服务器渲染那套完全用不上。Vite 启动快、配置少,Tailwind 让我不需要写一堆 CSS 就能做出还能看的界面。这里分享一个经验:个人开源项目的 UI,千万不要追求花哨,朴素、清晰、能顺手改才是核心。

存储用 SQLite,很多人第一反应是太轻量,但这是我的刻意选择。SQLite + SQLAlchemy 足够支撑几十万条笔记和对话纪录,单文件备份方便到令人感动。向量检索我用了 sqlite-vec 扩展,它可以把向量直接存在 SQLite 里,省掉单独维护一个向量数据库的复杂度。等项目量级真成长到需要 PostgreSQL 再迁移也不迟,而且 SQLAlchemy 已经帮我抽象了一层,到时候改动成本可控。

2.3 项目目录结构与核心模块

整个仓库结构我尽量保持简单,新来的贡献者花五分钟就能找到对应功能:

magic-workbench/ ├── app/ │ ├── main.py # FastAPI 入口与路由注册 │ ├── config.py # 环境变量与运行配置 │ ├── models.py # SQLAlchemy 数据模型 │ ├── rag/ │ │ ├── embedding.py # 文本向量化 │ │ ├── store.py # sqlite-vec 存取 │ │ └── retriever.py # 召回与重排序 │ ├── skills/ │ │ ├── registry.py # 技能注册表 │ │ ├── loader.py # 从 JSON 加载技能声明 │ │ └── builtin/ # 内置技能实现 │ ├── api/ │ │ ├── chat.py # 对话接口 │ │ ├── notes.py # 笔记管理接口 │ │ └── skills.py # 技能管理接口 │ └── worker.py # 后台任务(索引构建等) ├── frontend/ # React 前端 ├── skills/ # 用户自定义技能存放目录 ├── notes/ # Markdown 笔记存放目录 └── data/ # SQLite 数据库与缓存文件

目录设计上我遵循一个原则:数据与代码分离。data和notes不在app包里,它们就是纯数据目录,备份时直接把这俩文件夹拷走,就完成了全量备份。前端代码也不和后端混在一起,开发时开两个服务,生产环境由后端托管构建产物,互不干扰。

2.4 为什么说“数据本地化”是最大卖点

我在推广这个项目时,发现最打动人的不是一个又一个炫酷功能,而是一句话:你的笔记和对话记录,可以只存在你自己的硬盘上。对研究者来说,这意味着未发表的思路不会被任何中间环节看到;对开发者来说,这意味着我可以用 Lua、用 Rust、用什么语言写的脚本都无所谓,因为所有数据都是开放的,对接成本为零。

数据本地化的实现靠两层保障。第一层是存储层的明文策略:笔记文件本身就是标准 Markdown,数据库虽然有索引和向量,但我保留了每条记录与源文件之间的映射关系。第二层是服务层的隔离策略:所有 API 默认只监听127.0.0.1,没有特殊配置,外部网络根本访问不到这个工作台。需要远程访问时,我建议用户自己用 SSH 隧道或者反代工具去控制暴露范围,而不是让应用自己开一个裸奔的端口。

这里有个工程上的小坑,提一句:SQLite 虽然方便,但如果向量数据和文本索引都在同一个库文件里,写入并发稍微上来就容易锁库。我的做法是拆成两个库文件,一个存业务数据,一个存向量,这样索引构建任务在后台跑的时候,不会阻塞你正常写笔记。

3. 核心功能怎么实现的

3.1 技能(Skill)系统:工作台的学习机制

技能系统是魔力工作台里最值得讲的部分,它决定了这个工作台是“一个聊天窗口”还是“一个能干活的助手”。我的设计思路很简单:一个技能就是一份 JSON 声明加一个 Python 实现函数,系统启动时扫描skills/目录,把声明注册进技能表,模型在对话中根据用户意图决定要不要调用这些技能。

先看一份典型技能声明:

{ "name": "summary_file", "description": "摘要本地 Markdown 或文本文件的内容,输出结构化要点", "parameters": { "type": "object", "properties": { "path": { "type": "string", "description": "文件的绝对路径或相对于 notes 目录的路径" } }, "required": ["path"] } }

这份声明的作用,是告诉模型“什么时候该调用我、传什么参数”。也就是说,我自己不需要写意图识别逻辑,模型通过阅读理解技能描述,就能决定如何调用。真正干活的是 Python 侧的函数:

async def summary_file(path: str) -> str: full_path = resolve_path(path) text = read_file(full_path) return await call_llm( "请总结以下内容,输出三级要点:\n\n" + text[:8000] )

技能机制的好处有三个:一是扩展成本极低,我新加一个“把 CSV 转成 Markdown 表格”的技能,写一个函数加一份 JSON,重启后就能用;二是模型调用天然可靠,参数通过 JSON Schema 校验,传错了函数会直接报错,不会偷偷吞掉;三是贡献容易,别人想给你加功能,不需要理解整个项目结构,只要照着builtin/目录里某个技能的样子照猫画虎就行。

3.2 知识库与 RAG 检索:让 AI 记住你的笔记

工作台和普通聊天工具的本质区别,就是它知道你写过什么。魔力工作台对notes/目录做了监听,新文件出现或已有文件改动,后台会自动切分文本并向量化。切分时我设了 500 字符一个块、重叠 50 字符,这个参数是针对中文笔记调整过的。纯英文场景可以适当加大到 800 到 1000,中文如果切得太大,召回时会混入大量不相关内容,拉低回答质量。

检索链路分三步:摘要召回、相关度排序、上下文拼装。先根据用户提问生成 query 向量,在 sqlite-vec 里做余弦相似度召回前 20 个块;然后我用一个轻量的重排策略,基于关键词重叠与位置新鲜度做分数加权;最后取前 4 到 6 个块拼进 System Prompt,约束模型只能基于这些内容回答。这样既控制了 token 消耗,也减少了幻觉概率。

想特别说一下为什么不用现成的 LangChain。LangChain 功能确实全,但对这种单机小项目来说过于笨重,依赖一大堆,版本升级还经常 breaking change。我自己用 httpx 直接调用模型 API、自己写向量存储操作,核心逻辑不超过一百行,出了问题一眼就能看穿。这也是开源项目该有的特质:少一点黑盒,多一点透明。

3.3 上下文记忆与缓存目录设计

用 AI 工作台最烦的一件事,就是它记不住你刚才说过的话。我在第一版就踩了这个坑,对话超过几轮就答非所问。后面我设计了一套分层的记忆机制:短期记忆存当前对话窗口内的消息 ID,中期记忆提取对话摘要写入一张 summary 表,长期记忆则定期从笔记里抽取标签和主题词。

关于缓存目录,这是一个从 workbuddy 时代就困扰我的问题。它默认把模型响应缓存塞在系统临时目录里,用户想清理都不知道去哪。魔力工作台把缓存放在data/cache/下,按天分文件夹,每天的模型响应、中间检索结果、技能执行日志都留在对应日期文件里。这样既方便排查问题,也方便用户自己清理。我还加了一个配置项 CACHE_RETENTION_DAYS,默认 7 天,超期的缓存自动清除。

设置这个分层缓存的意义不只是省点 token 钱。更重要的是,当你发现某次回答质量不对劲时,可以把缓存翻出来,看模型到底接收到了什么上下文,哪些检索块影响了回答。没有这种可观测性,AI 应用调试起来和碰运气没什么区别。

3.4 任务看板与日程规划的轻量实现

任务管理是后来加的功能,因为光有笔记和问答还不够,写论文也好、做项目也好,都需要把目标拆解成可执行事项。魔力工作台的看板只有三个状态:待处理、进行中、已完成。每个任务可以关联到一篇或多篇笔记,任务完成时关联笔记里的状态检查项也会同步打勾。

这个设计意图是:让“思考”和“执行”不要脱节。比如我在笔记里写了一个研究计划,列出了五步;我可以通过一条斜杠命令/todo把这五步直接导入看板。AI 甚至可以根据笔记内容用技能自动生成任务清单,我只需要确认。

技术上就是一张简单的任务表,外键关联笔记 ID,没有任何复杂算法。但我刻意保留了“关联”,因为人在看板面前容易只顾着打勾,忘了回头看看这些任务最初为什么存在。保留笔记链接,等于随时能顺着任务回到原始上下文。

4. 安装部署与旧数据迁移实录

4.1 环境准备:Python 3.11 + Node 18 + Ollama

下载项目后第一步是准备环境。我建议直接用 Python 3.11 以上版本,低版本会有依赖兼容问题。前端要 Node 18 或 20,主要给 Vite 用。如果你打算完全离线跑,额外装一个 Ollama 并拉取qwen2.5:7b或llama3.1:8b这类模型,本机就能推理。

克隆项目之后按顺序执行:

git clone https://github.com/yourname/magic-workbench.git cd magic-workbench python -m venv .venv source .venv/bin/activate # Windows 下用 .venv\Scripts\activate pip install -r requirements.txt

后端依赖装完,再装前端依赖:

cd frontend npm install npm run build cd ..

生产模式下我让 FastAPI 直接托管前端构建产物,所以只需要在frontend里构建一次,后面改前端才需要重新执行上述命令。开发模式下则是开两个终端分别跑,前端用 Vite 的代理转发 API 请求,体验会更顺。

4.2 配置模型接入:一份 .env 文件说清楚

模型接入是整个部署里最需要说明白的地方。我把所有外部依赖收拢到一份.env文件里,避免用户到处找配置。默认支持任意 OpenAI 兼容 API,你只需要改三个变量:

# 模型 API 配置 OPENAI_BASE_URL=https://api.openai.com/v1 OPENAI_API_KEY=sk-xxxx DEFAULT_MODEL=gpt-4o-mini # 若使用本地 Ollama # OPENAI_BASE_URL=http://localhost:11434/v1 # OPENAI_API_KEY=ollama # DEFAULT_MODEL=qwen2.5:7b # 数据与缓存目录 DATA_DIR=./data NOTES_DIR=./notes CACHE_DIR=./data/cache CACHE_RETENTION_DAYS=7

这份.env放在项目根目录,会被start.sh自动加载。值得提一句:有很多人问我为什么不用 LangChain 的OPENAI_API_KEY那一套标准命名,我直接用了名字更长的OPENAI_BASE_URL,就是为了防止用户被各种中间层概念绕晕。记住一条原则:base_url、api_key、model 三者齐了,就能跑。

我实测过把 base_url 指向http://localhost:11434/v1(Ollama 的兼容端点),连 api_key 随便填一个字符串都行,模型换成qwen2.5:7b,整个工作台立刻变成本地离线版。如果你有私有化部署的 vLLM 服务,同样改这三个变量就完事。

4.3 首次启动与导入存量笔记

配置完成后执行启动脚本:

chmod +x start.sh ./start.sh

脚本内部做了三件事:激活虚拟环境、做数据库迁移、启动 uvicorn。默认监听 127.0.0.1:8000,浏览器打开 http://127.0.0.1:8000 就能看到工作台界面。

第一次使用,魔力工作台会引导你指定notes/目录的路径,也可以沿用默认的./notes。我迁移旧数据时,直接把以前存在 workbuddy 里的几百篇 Markdown 笔记拷进这个目录,重启服务后系统自动开始建索引。这里要提醒一下:如果笔记量超过一千篇,首次索引构建可能要几分钟,期间界面可以看到进度条,不用一直盯着。

说说从 workbuddy 迁移数据的具体操作。workbuddy 的私有导出格式我没有直接兼容,但我发现绝大多数用户都是把笔记当作 Markdown 维护的,导出后无非是带一层 JSON 包装。我在项目里写了一个scripts/import_notes.py脚本,可以递归扫描一个目录下所有.md、.txt文件,提取 frontmatter 元数据(标题、标签、创建时间),导入魔力工作台并触发索引。脚本用法:

python scripts/import_notes.py /path/to/old/notes

这个脚本是我从自己迁移经验里提炼出来的,我用 15 分钟处理完了几百篇笔记,后续没有发现漏文或乱码。对话历史那个部分我直接放弃了导入,因为 AI 对话的上下文强绑定原平台会话结构,硬迁反而容易出现答非所问。

5. 我踩过的坑和排查经验

5.1 常见问题速查表

开发和使用过程中,我在 issue 区和社区里收集了不少问题,挑几个高频率的整理成一张陷阱清单:

症状直接原因解决办法
对话加载慢检索到的知识块过多,拼入上下文后 token 超限检查重排后取出块数,默认 6 块以内
回答完全偏离笔记内容向量索引过期,新改的笔记未被重新切分确认 watcher 是否运行,手动触发重建索引
本地模型回答质量差7B 模型能力有限,且中文知识库切块过大换 14B 以上模型,或把切块调到 300 字符
sqlite 提示 database is locked索引构建线程与写入线程争锁确认已把向量库拆到单独文件,减少并发写
技能调用不生效描述写得太泛,模型无法判断何时调用在 description 里写清楚触发场景和反面例子
前端页面白屏后端地址被修改,前端没配置代理检查 VITE_API_TARGET 是否指向正确的后端端口

这些问题的共同规律是:大部分不是“功能坏了”,而是“配置没对齐”。我的调试建议是先看/data/cache/里当天的日志文件,每次模型调用和技能执行都有记录,从日志基本能定位到 90% 的问题。

5.2 性能优化:从 3 秒到 800ms 的取舍

魔力工作台早期版本一个问题平均响应 3 秒,其中一半时间消耗在检索和重排上。我做了三轮优化,这里分享最有用的两个动作。

第一个动作是向量检索前置过滤。我在做召回时先按标签和目录过滤掉明显无关的笔记块,只在候选集里做余弦相似度计算。举个例子,笔记总量五万块,但用户提问时没提到 tag A 和 tag B,我可以直接把这两类块排除,计算量能减少一半以上。这一点对中文场景特别明显,因为中文语义分散,直接全量召回很容易被无关文档干扰。

第二个动作是重排策略的“预算控制”。我最初的重排逻辑会对 20 个召回块每个都做一次文本评分模型调用,算力开销很大。后来我把这个环节改成两阶段:先用关键词重叠和位置新鲜度做一次轻量粗排,只让前 8 个块进入模型评分阶段,最终取前 4 个。这样回答质量几乎没有下降,单次请求时间从 3 秒降到 800ms 左右。

性能优化的原则,我一直坚持“先测量,再动手”。不要一上来就上缓存、上 Redis,先把耗时分布打出来看清楚,资源花在刀刃上。开源工具的好处是你自己就能加指标埋点,我最后在日志里加了耗时记录,每条请求都能看到检索耗时、模型调用耗时、总耗时三段。

5.3 给同样想动手做开源替代的朋友的三个建议

第一个建议:不要一上来就写代码,先梳理你自己最痛的三件事。我一开始差点去做一个“更智能的对话引擎”,后来发现最痛的是笔记检索不准。把精力放到检索质量上之后,整个工具的价值一下子体现出来了,对话能力反而只是锦上添花。做开源替代品,永远先解决自己的问题,再考虑普适性。

第二个建议:Skill 系统的格式越早稳定越好。我在早期改过三次技能声明格式,每次都要手动迁移已有的技能文件和文档,非常折磨。如果你也打算做类似的功能,一开始就写好 JSON Schema,发布 1.0 之前尽量冻结格式。插件生态是所有这类工作台的生命线,格式不稳定,贡献者不会愿意来。

第三个建议:一定要给自己留一个“隔离环境”做测试。魔力工作台支持用环境变量切换数据目录,我专门建了一个>

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

Anaconda环境恢复全攻略:从误删到系统崩溃的解决方案

1. 项目概述作为一名长期使用Python进行数据分析和机器学习的开发者,我深知Anaconda环境的重要性。它不仅仅是一个Python发行版,更是我们日常工作的重要基础设施。然而,当环境突然崩溃或误删时,那种绝望感我深有体会。本文将分享我…

作者头像 李华
网站建设 2026/10/27 5:56:32

工业设备预测性维护实战:从单台泵到全厂落地的分步路径

1. 这不是“AI概念秀”,而是一套可落地的工业设备预测性维护实战路径2026年,工厂里那台服役八年的数控龙门铣床突然停机,维修班花了三天排查,最后发现是主轴轴承早期微裂纹引发的渐进式失效——这种事我见过太多次。它不轰动&…

作者头像 李华
网站建设 2026/10/27 5:56:28

智元周将启AI模型解析与多模态技术实践

1. AI领域最新动态:2024年4月3日关键进展2024年4月3日,全球AI领域迎来了一轮密集的产品发布与技术突破。国内外多家头部企业相继推出新一代AI模型,其中智元科技的"周将启"系列尤为引人注目。作为从业十余年的技术观察者&#xff0c…

作者头像 李华
网站建设 2026/10/27 5:55:51

AISNS 2026:人工智能与社交网络的技术融合与安全创新

1. 会议背景与核心议题解析2026年人工智能与社交网络系统国际学术会议(简称AISNS 2026)作为跨学科领域的年度盛会,今年特别聚焦智能技术与网络安全的融合创新。会议吸引了来自全球37个国家的600余位学者、企业技术负责人和行业监管者&#xf…

作者头像 李华
网站建设 2026/10/27 5:51:44

Tiger_Rory创意项目:跨文化设计与参数化建模实践

1. 项目概述:Tiger_Rory的创意探索第一次听到"Tiger_Rory"这个组合词时,我的直觉告诉我这背后一定藏着有趣的创意。作为混迹创意圈十多年的老手,我习惯性地开始拆解这个看似随意的命名——"Tiger"代表力量与野性&#xf…

作者头像 李华
网站建设 2026/10/27 5:50:50

高效批量添加文件名前缀的跨平台解决方案

1. 为什么我们需要批量给文件名加前缀?在日常工作中,我经常遇到需要给大量文件统一添加前缀的情况。比如上周整理客户资料时,收到了市场部发来的300多张产品图片,文件名都是零散的"IMG_001.jpg"、"DSC1234.png&quo…

作者头像 李华