MoreRSS

site iconGamea | 飘逝的风修改

游戏后台开发十多年, 目前就职于Tencent。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

Gamea | 飘逝的风的 RSS 预览

如何优雅的让Claude Code使用第三方模型?

2025-07-20 08:00:00

Featured image of post 如何优雅的让Claude Code使用第三方模型?

最近Claude Code爆火,很多人都说Cursor不香了。无奈原生的Claude Code使用对国人来说特别不便,我这里尝试了一些新的解决方案,希望对你流畅使用Claude Code有帮助。

背景

Claude模型的母公司Anthropic,对国内用户使用限制特别多,我曾经注册或购买的几个号没多久就阵亡了。但人家的这个工具确实不错,我们没账号怎么办?

听说可以使用一些第三方模型了,比如最近国内月之暗面推出的Kimi-K2,它们机智的直接支持了Anthropic的API。我们可以简单配置一下,就可以在Claude Code中使用Kimi-K2模型了。 但是有时我们想使用其他更强大的模型,比如Gemini,怎么办?

我又继续寻找到了最近刚开源的一个解决方案,claude-code-router,它支持了多个模型,包括Gemini,DeepSeek,GPT等。这里又产生一个问题,可以使用原生Gemini模型,但Google家模型不能精确控制预算,哪天你哐哐用,金钱也哗哗出的时候,看到账单傻眼了怎么办?

以上各个问题,本文都会尝试给一个解决方案,如果对你有帮助,请帮我点个赞吧!

本文假定你会Claude Code的安装和使用,我们直接进入主题:如何优雅的让Claude Code使用第三方模型?

初步试水:借助kimi-k2使用第三方模型

这块已经有不少文章介绍了,我简单说几个关键点:

  1. 设置两个环境变量:
1
2
export ANTHROPIC_API_KEY=`<你的API KEY>`
export ANTHROPIC_BASE_URL=https://api.moonshot.cn/anthropic
  1. 修改Claude Code的配置 ~/.claude.json 添加(你可能要先启动一次才会自动生成这个配置):
1
"hasCompletedOnboarding": true,

然后重启Claude Code,就可以使用Kimi-K2模型了。在里面似乎kimi做得足够兼容,你连模型都不用切换,直接就开箱即用了。我问了一句你是谁,看来Claude Code有被骗到:)

1
2
3
> 你是哪个模型

⏺ 我是 Claude Code,由 Anthropic 开发的官方 CLI 工具。我使用的是 Claude Sonnet 4 模型(具体版本 ID 是 claude-sonnet-4-20250514)。

kimi-k2

  1. 如果想更流畅使用,你可能至少需要充50元,不然那个限频等,体验应该会很差。

我感觉Kimi-K2这次挺“鸡贼”的,借了一波Claude Code的东风,应该引了不少新进,现在它官网开始提示繁忙起来了呢:)

我试着用了一阵Kimi-K2,有时候反应较慢,我在想是否可能把Gemini家的和OpenAI家的模型一起集成进来呢?方法当然是有的。

进阶集成:使用claude-code-router扩展你的模型库

在一两周前,我在寻找如何让Claude Code可使用更多种第三方模型。在搜索这个问题的解法,国外的Perplexity居然没有推荐这个项目,反倒是国内腾讯元宝给我介绍了有这样一个开源项目,claude-code-router(以下简称CCR)可能解决我的问题,一看到我甚为惊喜,我想莫不是这个项目是国人写的原因,咱离自己人更近。

A powerful tool to route Claude Code requests to different models and customize any request.

基本原理

在GitHub仓库中也写了项目的实现原理。简单的说,作者经过逆向分析后,发现在Claude Code中它在调用模型时,各个参数都是通过环境变量获取的,作者想到开发一个中间件,将各个环境变量替换掉,这样可以实现调用第三方模型。同时因为Claude Code使用Anthropic API的规范,我们需要将对第三方模型的调用转换成Anthropic API的格式。

安装

Claude Code类似通过npm即可安装。

1
2
3
4
5
# 安装
npm install -g @musistudio/claude-code-router

# 通过ccr启动claude code
ccr code

配置

你可以参考官方提供的示例,配置~/.claude-code-router/config.json

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
{
 "APIKEY": "your-secret-key",
 "PROXY_URL": "http://127.0.0.1:7890",
 "LOG": true,
 "Providers": [
 {
 "name": "openrouter",
 "api_base_url": "https://openrouter.ai/api/v1/chat/completions",
 "api_key": "sk-xxx",
 "models": [
 "google/gemini-2.5-pro-preview",
 "anthropic/claude-sonnet-4",
 "anthropic/claude-3.5-sonnet"
 ],
 "transformer": { "use": ["openrouter"] }
 },
 {
 "name": "deepseek",
 "api_base_url": "https://api.deepseek.com/chat/completions",
 "api_key": "sk-xxx",
 "models": ["deepseek-chat", "deepseek-reasoner"],
 "transformer": {
 "use": ["deepseek"],
 "deepseek-chat": { "use": ["tooluse"] }
 }
 },
 {
 "name": "ollama",
 "api_base_url": "http://localhost:11434/v1/chat/completions",
 "api_key": "ollama",
 "models": ["qwen2.5-coder:latest"]
 },
 {
 "name": "gemini",
 "api_base_url": "https://generativelanguage.googleapis.com/v1beta/models/",
 "api_key": "",
 "models": ["gemini-2.5-flash", "gemini-2.5-pro", "gemini-2.5-flash-lite-preview-06-17"],
 "transformer": {
 "use": ["gemini"]
 }
 },
 {
 "name": "kimi",
 "api_base_url": "https://api.moonshot.cn/v1/chat/completions",
 "api_key": "",
 "models": [
 "kimi-k2-0711-preview"
 ],
 "transformer": {
 "use": [
 "cleancache"
 ]
 }
 },
 ],
 "Router": {
 "default": "deepseek,deepseek-chat",
 "background": "ollama,qwen2.5-coder:latest",
 "think": "deepseek,deepseek-reasoner",
 "longContext": "openrouter,google/gemini-2.5-pro-preview"
 }
}

这里定义了不同的Provider,并且有一些模型可以有其设置。比如为了让DeepSeek模型更积极使用工具,有个tooluse的设置。比如为了转换Gemini模型,有个gemini的Transformer。 同时可看到,它还真是国人开发,很有本地化特色,比如显式支持PROXY设置方便你访问某些模型。

到这里配置好后,当你在使用Claude Code时,想切换模型时,可以输入/model命令,然后选择你想要的模型。比如: 在Claude Code Router中使用Kimi-K2

不过有点遗憾的是,当前通过CCR中还不支持Web搜索和图片上传,这离我们想要的完整体还是有点距离,但官方已经在计划中,并且这个项目最近更新很频繁,Star也涨得非常快。

折腾到这里就结束了吗?这里发生了一件小事,让我觉得有必要继续折腾一下。我使用OpenRouter来调用Claude模型,为了省钱,我已经很勤俭地只用 claude-3.7-sonnet 了,但几轮对话下来,发现账单还是有点夸张。我在一个不算太大的项目中进行了/init和简要对话而已。虽然OpenRouter提供了对每个KEY的费用限制(Credit limit),但是如Google的Gemini等模型,它就没有可以限制额度,那就只能等收到账单才后知后觉了?

问题不大,我想起来之前折腾过LiteLLM,它不仅能聚合LLM接口,还能像个贴心管家一样帮你控制预算。就决定是你了,继续折腾!

终极控制:使用LiteLLM统一LLM及控制支出

很早前想写一篇LiteLLM+Librechat的教程,但一直没时间,今天就让LiteLLM先出场吧。我继续在k8s中部署它,如果你是容器或其它方式,请参考官方文档,部署过程都是简单的。

配置LiteLLM

我们创建一个configmap定义了LiteLLM的配置文件config.yaml,大概内容如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
apiVersion: v1
kind: ConfigMap
metadata:
 name: litellm-config
 namespace: ai
data:
 config.yaml: |
 # LiteLLM配置
 model_list:
 # 添加Anthropic模型
 - model_name: "openrouter/anthropic/claude-3.7-sonnet"
 litellm_params:
 model: "openrouter/anthropic/claude-3.7-sonnet"
 api_key: os.environ/OPENROUTER_API_KEY
 - model_name: "openrouter/openai/gpt-4.1"
 litellm_params:
 model: "openrouter/openai/gpt-4.1"
 api_key: os.environ/OPENROUTER_API_KEY
 - model_name: "gemini/gemini-2.5-flash"
 litellm_params:
 model: "gemini/gemini-2.5-flash"
 api_key: os.environ/GEMINI_API_KEY

 # 服务器配置
 server_settings:
 port: 4000
 environment: production
 cors_allow_origins: ["*"]
 prometheus_metrics: true

 litellm_settings:
 # check_provider_endpoint: true # 👈 Enable checking provider endpoint for wildcard models
 # 禁用guardrails相关功能
 enable_guardrails: false
 enable_guardrails_in_db: false

部署LiteLLM

接着定义一个Deployment即可。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
apiVersion: apps/v1
kind: Deployment
metadata:
 name: litellm
 namespace: ai
spec:
 selector:
 matchLabels:
 app: litellm
 replicas: 1
 template:
 metadata:
 labels:
 app: litellm
 spec:
 volumes:
 - name: config-volume
 configMap:
 name: litellm-config
 nodeSelector:
 kubernetes.io/hostname: models
 containers:
 - name: litellm
 image: ghcr.io/berriai/litellm:v1.74.0-stable
 args:
 - "--config=/app/config.yaml"
 volumeMounts:
 - name: config-volume
 mountPath: /app/config.yaml
 subPath: config.yaml
 env:
 - name: TZ
 value: "Asia/Shanghai"
 - name: DISABLE_SCHEMA_UPDATE
 value: "true"
 - name: LITELLM_LOG
 value: "DEBUG"
 - name: DATABASE_URL
 value: "postgresql://root:[email protected]:5432/litellm"
 - name: LITELLM_SALT_KEY
 value: "xxxx"
 - name: STORE_MODEL_IN_DB
 value: "True"
 - name: OPENAI_API_KEY
 value: "xxxx"
 - name: AZURE_API_KEY
 value: "xxxx"
 - name: GEMINI_API_KEY
 value: "xxxx"
 - name: DEEPSEEK_API_KEY
 value: "xxxx"
 - name: OPENROUTER_API_KEY
 value: "xxxx"
 - name: HTTP_PROXY
 value: "<your proxy>"
 - name: HTTPS_PROXY
 value: "<your proxy>"
 - name: NO_PROXY
 value: "localhost,127.0.0.1,postgresql.base-system.svc.cluster.local,postgresql,10.43.140.217"
 ports:
 - containerPort: 4000
 livenessProbe:
 httpGet:
 path: /health/liveliness
 port: 4000
 initialDelaySeconds: 40
 periodSeconds: 30
 timeoutSeconds: 10
 failureThreshold: 3
 resources:
 requests:
 cpu: 100m
 memory: 128Mi
 limits:
 cpu: 500m
 memory: 512Mi
---
apiVersion: v1
kind: Service
metadata:
 name: litellm
 namespace: ai
spec:
 selector:
 app: litellm
 ports:
 - protocol: TCP
 port: 4000
 targetPort: 4000
 type: ClusterIP

要注意LiteLLM启动时,有时资源消耗会比较高,我的弱鸡k8s节点时不时会给搞得濒死,最好像上面限制一下资源。 我们可以测试一下LiteLLM对外的接口是否正常,比如:

1
2
3
4
5
6
7
curl https://litellm.mysite.com/v1/chat/completions \
 -H "Content-Type: application/json" \
 -H "Authorization: Bearer $LITELLM_KEY" \
 -d '{
 "model": "gemini-2.5-flash",
 "messages": [{"role": "user", "content": "What is the capital of France?"}]
 }'

正常返回后,说明我们的LiteLLM服务工作正常。接下来我们就可以在claude-code-router中统一使用litellm作为唯一的Provider了。

在claude-code-router中使用litellm

现在,我们的config.json可以变得非常清爽,Providers里只留下litellm一个就行:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
 "Providers": [
 {
 "name": "litellm",
 "api_base_url": "https://litellm.mysite.com/v1/chat/completions",
 "api_key": "xxxx",
 "models": [ "deepseek/deepseek-chat","deepseek/deepseek-reasoner","moonshot/kimi-k2-0711-preview","gemini/gemini-2.5-flash-lite", "gemini/gemini-2.5-flash", "openrouter/google/gemini-2.5-flash","azure/gpt-4.1", "gemini/gemini-2.5-pro","openrouter/anthropic/claude-3.7-sonnet"],
 "transformer": {
 "deepseek/deepseek-chat": { "use": ["maxtoken", { "max_tokens": 8192 }] },
 "gemini/gemini-2.5-flash-lite": { "use": ["cleancache"] },
 "gemini/gemini-2.5-flash": { "use": ["cleancache"] },
 "moonshot/kimi-k2-0711-preview": { "use": ["cleancache"] }
 }
 }
 ],
 "Router": {
 "default": "litellm,moonshot/kimi-k2-0711-preview",
 "background": "litellm,deepseek/deepseek-chat",
 "think": "litellm,deepseek/deepseek-reasoner",
 "longContext": "litellm,gemini/gemini-2.5-pro"
 }
}

这里要注意,如果直接对接官方的Gemini模型,只需要配置Gemini的Transformer即可。但这里咱们是通过LiteLLM调用的,还需要配置use: cleancache的Transformer。不然会报类似下面这样的错误:

⎿ API Error: 400 {“error”:{“message”:“Error from provider: {"error":{"message":"litellm.BadRequestError: VertexAIException BadRequestError - {\n \"error\": {\n \"code\": 400,\n \"message\": \"* GenerateContentRequest.contents: contents is not specified\\n\",\n \"status\": \"INVALID_ARGUMENT\"\n }\n}\n. Received Model Group=gemini/gemini-2.5-pro\nAvailable Model Group Fallbacks=None","type":null,"param":null,"code":"400"}}”,“type”:“api_error”,“code”:“provider_response_error”}}

还好LiteLLM的日志相当给力,我通过排查请求体,很快就定位到问题出在 “cache_control” 这个字段上——删掉它就一切正常了。最后我们可以在LiteLLM的管理端看到每次Claude Code发出了哪些请求,使用了多少Token,花费了多少钱等。 LiteLLM管理端

我们也可以在LiteLLM中创建的API_KEY中定义它的额度,这样避免我们不小心超支。 LiteLLM API_KEY额度

现在,让我们开心的在Claude Code中使用各种模型吧!

总结

本文介绍了三种方式让你更好的基于第三方大语言模型来使用Claude Code,希望对你有所帮助。我们除了直接使用Kimi-K2外,还可以使用CCR来扩展模型库,最后通过LiteLLM来统一LLM的调用,这样也能让我们更精细化的观察Token的使用以及控制费用。

三种方案对比:

特性 方案一:Kimi 直连 方案二:CCR 方案三:CCR + LiteLLM
设置简易度 ⭐⭐⭐⭐⭐ (极简) ⭐⭐⭐⭐ (简单) ⭐⭐ (较复杂)
模型丰富度 ⭐ (仅 Kimi) ⭐⭐⭐⭐ (丰富) ⭐⭐⭐⭐⭐ (最丰富)
费用控制力 ⭐⭐ (依赖 Kimi 平台) ⭐⭐ (依赖上游) ⭐⭐⭐⭐⭐ (精准控制)
折腾系数 ⭐⭐⭐ ⭐⭐⭐⭐⭐

以上便是最近关于Claude Code的一点折腾经验,希望对你有帮助。欢迎点赞、收藏、分享,更欢迎分享你的使用经验。我们下篇文章见。

我是个爱折腾技术的工程师,也乐于分享。欢迎点赞、关注、分享,更欢迎一起探讨技术问题,共同学习,共同进步。为了获得更及时的文章推送,欢迎关注我的公众号:爱折腾的风

扫码关注公众号

打造属于孩子的有声绘本世界

2025-07-12 08:00:00

Featured image of post 打造属于孩子的有声绘本世界

经常从图书馆借一些书来给孩子看,有一些是他们感兴趣的可以反复阅读的,无奈借书终有归期,如何解决这个问题呢?作为一个程序员爸爸,何不将它“保留”下来,并且让老父亲可以给他们朗读呢?

背景

孩子的天性,对于喜欢的绘本,可以反复阅读,一遍又一遍。我还记得有一次带女儿去图书馆还书,有几本她不舍得还,于是我们在一个楼梯边上坐下,一起再读了一遍。之后才恋恋不舍地将书交给“机器人”(自动分检)。我也颇遗憾的是记忆中有几本书,每次读起来,娃们都笑逐颜开。于是我打算“留住”这些特别的书,那是他们的美妙回忆。

原本打算写个微信小程序,但想着优先把核心功能跑起来,还是以网站呈现比较简单些。花了一些闲暇时间,借助于 VideCoding 有个初步可用的版本了。

以下简要介绍一下我的一些探索和实现过程,代码没整理暂时没打算开源。如果你想看看效果,我已经部署在vercel了,可以访问:https://audiobook-web-brown.vercel.app 查看。

首页阅读页

技术实现解析

需求简要分析

显然我们这个网站有几个基本功能要支持一下,比如绘本管理、绘本语音生成、绘本阅读体验等基本功能,多用户管理也可以考虑。 在绘本管理上,比如:

  • 绘本列表,筛选,收藏,标星
  • 绘本添加,扫描录入,提交PDF/PPT录入
  • 绘本删除,需要有锁,避免小孩误删除
  • 阅读记录,可以看到最近读的绘本的阅读时间

在绘本语音生成上,我们之前也试过多种声音复刻了,今天会推荐另一个简单便宜的渠道(后文),我考虑这样:

  • AI 生成的语音
  • AI 复刻的语音
  • 手工录制的语音(咱可能演绎得惟妙惟肖)

在绘本阅读过程中,希望能提供沉浸式的阅读UI,同时需要支持:

  • 横屏还是坚屏展示(特别是在手机上时)
  • 倍速播放
  • 快速跳转页面

用户管理等就是区分不同的用户和阅读进度等啦,这个就不细说了。我们首先面临的是,绘本从何而来?

绘本录入

我们需要将纸质书迁移到线上,最直接的莫过于拿起手机直接对着绘本拍照了,这里有一点小技巧要注意。为了便于后续照片的处理,我们尽量让书本放在一个比较纯净的背景色下,比如下面垫一张大白纸等。之后是将我们拍的照片处理,这里涉及到一些图像校正,透视校正等,如果可以,一些锐化或超分技术也可以使用上。我尝试了几种方式,最后选择了一个简单的方式,但或许不是最终解:

  • 使用OpenCV等库直接对图片进行处理,这里照片里的内容如果对比比较强,容易识别异常,只识别一小部分等出来。比如下面这样: opencv 当然如果花一些时间仔细研究算法和以书本来定制化识别方案,或许是可以调好的。
  • 使用云厂商提供的图像增强功能,可以快速实现裁剪、锐化、提亮等功能。比如腾讯云的文本图像增强

文本图像增强(Text Image Enhancement)是面向文档类图片提供的图像增强处理能力,包括切边增强、图像矫正、阴影去除、摩尔纹去除等;可以有效优化文档类的图片质量,提升文字的清晰度。 它的效果整体还不错,不过对于一些需要先旋转再裁剪的图片,它不会智能处理,所以交给它前最好先预处理一下。

  • 使用免费的 App 来实现,这里推荐微软的 Lens。它不光可以将图片按我们需要处理,可以预览处理结果,还可以微调之。最后还可以直接整理输出为一份 PDF文档。

以上,对于希望免费使用,推荐使用 Lens来处理。而如果希望批量自动化处理,或许调用 API 是更便捷的。

提取文字

当我们已经准备好图片或绘本的 PDF 后,如何将绘本内的文字提取出来便是个问题。我试过传统的 OCR,老实说可读性和进一步处理都比较麻烦。幸运的是现在各种视觉模型都支持图像识别了,所以我们只需要借助他们之力便可以轻松实现目标。考虑到不同模型可能的识别的差异,这里我做了一个测试,使用如下Prompt:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
你是一个专业的儿童绘本分析助手。请仔细观察这张绘本页面,识别并提取出用于朗读的旁白文字内容。

请注意:
1. 只提取适合朗读的旁白文字,不包括对话气泡中的直接对话
2. 旁白通常是描述性的文字,用来推进故事情节或描述场景
3. 有一些页面是标题页等,需要返回标题的文字
4. 如果页面不是标题页,也没有旁白文字,只有对话,请返回"无旁白文字"
5. 请直接返回提取的文字,不需要额外说明

请提取这一页的旁白文字:

比如有如下这张图:example-page

📊 各模型结果对比:

模型 状态 识别结果
minimax-io/MiniMax-Text-01 ✅ 主模型 蓝色还是有点怀疑: “天空这么大,要不就留在这里?” 山谷中, 有歌声轻轻响起, 洒在她身上的, 是橘色的晨曦。
gemini/gemini-2.5-flash ❌ 备用模型 蓝色还是有点怀疑: 山谷中, 有歌声轻轻响起, 洒在她身上的, 是橘色的晨曦。
doubao/doubao-seed-1.6-flash ✅ 备用模型 蓝色还是有点怀疑:“天空这么大,要不就留在这里?” 山谷中, 有歌声轻轻响起, 洒在她身上的, 是橘色的晨曦。
moonshot/moonshot-v1-8k-vision-preview ✅ 备用模型 蓝色还是有点怀疑: “天空这么大,要不就留在这里?” 山谷中, 有歌声轻轻响起, 洒在她身上的, 是橘色的晨曦。
azure/gpt-4o ✅ 备用模型 蓝色还是有点怀疑: “天空这么大,要不就留在这里?” 山谷中, 有歌声轻轻响起, 落在她身上的, 是橘色的晨曦。
tencent/hunyuan-turbos-vision ✅ 备用模型 蓝色还是有点怀疑: “天空这么大,要不就留在这里?” 山谷中, 有歌声轻轻响起, 洒在她身上的, 是橘色的晨曦。

最终结果: 蓝色还是有点怀疑: “天空这么大,要不就留在这里?” 山谷中, 有歌声轻轻响起, 洒在她身上的, 是橘色的晨曦。

上面是写文章时再跑了一次数据,这次 gpt-4o正确了,过往它的错误率也很高。整体的模型测试挺有意思,发现gpt系列没那么准确时,我试了一下gemini 系列,准确性上升不少,但也有出错时。考虑到我是中文绘本的场景,我换成国内的豆包,居然正确率一下提高更多,豆包出错的地方,我就尝试了一下moonshot的模型,又一次给我惊喜,最后是minimax家模型,在我的几个绘本的识别中,准确率是最高的。咱国内模型在这块也是挺能打的嘛!!

我借用多个模型提取出结果,然后有一个模型来判断哪一个是更大概率正确的内容,这样避免单模型的问题,整体上提升识别成功率。 至此,绘本的文字已经准备好,我们开始着手给它配置语音吧。

语音生成

没有声音,再好的戏也出不来:) 小朋友需要一些声音来辅助读绘本。我在之前的让你的小智AI机器人起飞一文中介绍过如何在腾讯云平台复刻声音并且使用,还有豆包平台也有不少音色可选并且也支持声音复刻。但,等等……我找到一个相对更物美价廉的方案,使用 MiniMax 平台进行 TTS以及复刻。MiniMax平台国内注册即有 15 元可以体验,普通的通过 TTS 生成语音可以花一阵子。复刻一个声音只要 9.9 元,这比豆包和腾讯云上似乎便宜得多。同时我发现它的国际版本 minimax.io 可以申请开发者体验计划,我简单填写了一个表格,很快就到账 50刀,这绝对是很大方了,强烈推荐。 推荐它的原因不光是咱可能免费褥到羊毛,更重要的是它的效果很不错。

MiniMax 这家公司还很早提供了 MCP server,可以直接借助 MCP server测试一下它的能力,使用各种声音(复刻)来从文本生成语音(TTS)。我们甚至都不用写一行代码就可以玩起来了。除了使用 MCP 来复刻,也可以简单的使用几行curl命令来完成。

  • 上传样本声音文件 将准备好要复刻的人声文件放在本地文件中,并上传到平台。
1
2
3
4
5
6
7
8
9
export MINIMAX_API_KEY=`你的MINIMAX_API_KEY`
export GROUP_ID=`你的 GROUPID`
export FILE_PATH="/Users/kevin/Downloads/luoluo-voice/output.m4a"
curl --location "https://api.minimaxi.com/v1/files/upload?GroupId=${GROUP_ID}" \
--header 'authority: api.minimaxi.com' \
--header 'content-type: multipart/form-data' \
--header "Authorization: Bearer ${MINIMAX_API_KEY}" \
--form 'purpose="voice_clone"' \
--form "file=@${FILE_PATH}"

返回:

{“file”:{“file_id”:281211669533090,“bytes”:259501,“created_at”:1750210557,“filename”:“output.m4a”,“purpose”:“voice_clone”},“base_resp”:{“status_code”:0,“status_msg”:“success”}}

  • 使用上传声音复刻
1
2
3
4
5
6
7
8
9
export GROUP_ID=`你的 GROUPID`
curl --location "https://api.minimaxi.com/v1/voice_clone?GroupId=${GROUP_ID}" \
--header 'authority: api.minimaxi.com' \
--header "Authorization: Bearer ${MINIMAX_API_KEY}" \
--header 'content-type: application/json' \
--data '{
 "file_id": 281211669533090,
 "voice_id": "luoluo-2025-0618"
}'

返回:

{“input_sensitive”:false,“input_sensitive_type”:0,“demo_audio”:"",“base_resp”:{“status_code”:0,“status_msg”:“success”}}

  • 查询音色列表
1
2
3
4
5
6
curl --location 'https://api.minimax.chat/v1/get_voice' \
--header 'Content-Type: application/json' \
--header "Authorization: Bearer $MINIMAX_API_KEY" \
--data '{
 "voice_type":"voice_cloning"
}'

注意,在没有使用此音色时,上面查询不到这个,只有在首次使用后才扣费和能够被查询到

欲查询音色类型,支持以下取值:
“system”(系统音色),
“voice_cloning”(快速复刻的音色),
“voice_generation”(文生音色接口生成的音色),
“music_generation”(音乐生成产生的人声或者伴奏音色),
“all”(以上全部)。

  • 使用复刻声音
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
export GROUP_ID=`你的 GROUPID`
curl --location "https://api.minimax.chat/v1/t2a_v2?GroupId=${GROUP_ID}" \
--header "Authorization: Bearer $MINIMAX_API_KEY" \
--header 'Content-Type: application/json' \
--data '{
 "model":"speech-02-turbo",
 "text":"真正的危险不是计算机开始像人一样思考,而是人开始像计算机一样思考。计算机只是可以帮我们处理一些简单事务。",
 "stream":false,
 "voice_setting":{
 "voice_id":"luoluo-2025-0618",
 "speed":1,
 "vol":1,
 "pitch":0,
 "emotion":"happy"
 },
 "pronunciation_dict":{
 "tone":["处理/(chu3)(li3)", "危险/dangerous"]
 },
 "audio_setting":{
 "sample_rate":32000,
 "bitrate":128000,
 "format":"mp3",
 "channel":1
 }
 }' > resp.json | jq -r '.data.audio' | xxd -r -p > test1.mp3

这样这个复刻的音色就可以后面随时使用了。它在你第一次使用时会扣除 9.9 元(国内)或 3 刀(国际)费用。

音效生成

在我和 Gemini 探讨如何更好的设计这个绘本功能时,它提出了一个很不错的建议:

在准备配音文案时,我建议你创建一个“配音脚本”,不仅仅是抄录文字,而是像一个导演一样,设计整个声音的蓝图。你可以按照这个格式来整理:

页面 角色/旁白 文字内容 声音特效/拟声词 背景音乐建议
P1-2 旁白 在一个安静的小镇上,住着一只名叫乐乐的小狗。 (远处几声狗叫) (风轻轻吹过的声音) 舒缓、宁静的钢琴曲
P3 旁白 一天早上,他被一阵“叩叩叩”的声音吵醒了。 叩叩叩 (清晰有力的敲门声) 音乐暂停或减弱
P3 乐乐 “是谁呀?” (乐乐迷糊的、带点奶气的声音) (无)

总结一下,要成为声音的东西,不仅仅是“文字”,而是整个“故事场景”。你需要从一个“阅读者”转变为一个“声音导演”。在看每一页图画时,都可以问自己这几个问题:

  • “这里有什么文字?” -> 变成旁白和对话。
  • “这里能听到什么声音?” -> 变成拟声词和音效。
  • “这里是什么感觉/情绪?” -> 变成背景音乐。

通过这样的思考和准备,你最终创作出的有声绘本,将不仅仅是“读”出来的故事,而是一个生动、立体、能让孩子沉浸其中的声音世界。

如果我们可以在绘本期间追加一些声效,以及控制各个角色的声音情绪表现,那必然是更沉浸的体验。这块我寻找了一些方案:

  • ElevenLabs

    • 地址:https://elevenlabs.io/app/sound-effects/generate
    • 提供强大的AI音效生成API,支持通过文本描述生成音效。
    • 可自定义音效时长、提示影响力等参数。
    • 支持多语言,适合集成到游戏、视频制作等开发流程中。
    • 适合开发者通过API调用实现自动化音效生成
  • Stability AI

    • 地址:https://stableaudio.com/generate
    • 提供Stable Audio系列模型API,支持文本到音频、音频到音频的转换。
    • 适合需要高质量、长音频(如3分钟)生成的应用场景。

测试效果两者效果差不多,从费用上来说,似乎ElevenLabs 更便宜一些。暂时因为时间问题,我还没有将这块和上面 TTS 整合。它需要重新编排识别到的文本,在恰到好处的地方插入音效。未来有时间,我会尝试将这块整合到一起。

部署

这个应用是前后端分离设计的,前端基于 Vue 3 + TypeScript + Vite 构建,提供沉浸式阅读体验和智能用户管理。后端使用 golang来开发,同时将数据保存在对象存储中,这样我们的一些图片音频等可以借助对象存储的 CDN 有更快的响应速度。

我试着让 AI 帮我将前端对接到 Vercel 上,它帮我生成了一个 vercel.json 文件,我试着将它部署到 Vercel 上,结果居然一次成功咯,可以开心的用免费服务了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
{
 "buildCommand": "npm run build:skip-check",
 "rewrites": [
 {
 "source": "/((?!api|static|_next|favicon.ico).*)",
 "destination": "/index.html"
 }
 ],
 "headers": [
 {
 "source": "/(.*)",
 "headers": [
 {
 "key": "X-Content-Type-Options",
 "value": "nosniff"
 },
 {
 "key": "X-Frame-Options",
 "value": "DENY"
 },
 {
 "key": "X-XSS-Protection",
 "value": "1; mode=block"
 }
 ]
 },
 {
 "source": "/static/(.*)",
 "headers": [
 {
 "key": "Cache-Control",
 "value": "public, max-age=31536000, immutable"
 }
 ]
 }
 ]
}

后端就自己将程序打包为镜像,部署在我原来的k8s集群上了。

后记

在初步完成后,我给孩子试用了几次,效果还不错,他们惊讶于识别到这是爸爸的声音,这是他自己的声音等,也能认真的听完一本又一本绘本,作为老父亲,几个晚上的熬夜也没有白费了。 我突然想起来,有几本英文绘本,碍于我自己的英文水平和发音,都没怎么给孩子读,那真是误自己子弟了:)不过,现在有这个玩意,我可算找到救星了!文字就搞个双语吧,那发音到底是英式还是美式好呢?

我是个爱折腾技术的工程师,也乐于分享。欢迎点赞、关注、分享,更欢迎一起探讨技术问题,共同学习,共同进步。为了获得更及时的文章推送,欢迎关注我的公众号:爱折腾的风

扫码关注公众号

AI编码时代:如何从零构建血常规追踪应用

2025-06-01 08:00:00

Featured image of post AI编码时代:如何从零构建血常规追踪应用

我对现今的前端技术几乎完全不懂,但借助于AI,几乎不需要什么背景知识就可以轻松构建这样一个还不错的应用。本文除了简要介绍一下这个网站的功能外,同时也谈谈开发过程的一些经验感悟,希望对你有所帮助,更期待一起交流经验。

背景

在医院看到不少爸妈都很认真负责地记录着孩子的各项检测数值,我也曾经将一些数据记录在笔记本中。那些牵动家长们内心的数值,一旦离开了纸质记录本,就很难说清楚,也不便于和医生沟通交流,更难发现其中的变化规律。某天在从医院回家的路上,灵机一动:何不构建一个血常规追踪应用,让整个治疗过程清晰直观呢?

本文会简单介绍一下这个网站,更重要的会分享一下在使用Lovable平台和Cursor中的一些独家的感受。

成果展示

我的Lovable会员每个月有100次AI对话额度,打算物尽其用,“强行”让它值回票价,便计划用它来搭建网站雏形。经历了几天的迭代、趟过一些坑、也用光了Cursor的次数后,我总算以网站的形式,捣鼓出了一个比较完整的血常规追踪功能。先来看看基础功能,后面再谈谈开发过程中的一些经验。

我们可以快速预览到最近一次检测有哪些指标异常: 概览-最新检测

也可以通过单指标或多指标的选择进一步分析变化趋势,同时可以结合期间的治疗及手术情况,分析指标变化的原因: 概览-指标趋势图

然后可以通过医疗日历,查看到每天的用药情况等: 概览-医疗日历

在检测数据的录入中,除了传统的手术填表外,借助于AI识图,可以通过上传图片,系统会自动识别出检测数据,包括采样时间等以及各项指标的数值,并自动填充到表单中: 检测数据录入

我们可以添加期间的用药及手术类型,针对不同的病种,可以自定义自己的用药以及手术,可以用不同颜色区分: 用药记录手术记录 同时也提供了一些预设,可以选择性导入: 预设导入

同时,除了单个日期的添加记录,支持批量选择日期,通过Shift等常用区间选择的交互方式,可以快速添加一整段时间的记录,这样对于一些需要长期用药,这会更便捷一些。

有时找医生沟通,我们需要一个表格呈现一些数据,网站提供了将选定区间的信息导出的功能,同时会标注期间的一些关键事项。 导出数据

做完这些后,为了让初次使用的人可以快速上手,我们可能还需要一个使用指南,借助于现在便捷的网页生成能力,一个简洁美观的说明页也快速生成了: 使用指南

开发过程

和AI一起明晰目标——反复折腾多缘于目标不清

尽管头脑中已经有不少这个应用的功能点,但在让AI开始正式编码前,我们还需要细化一些东西,于是我和Gemini 2.5 Pro聊了起来。像一个倾诉对象一样,我和它谈我的目标,也让它反过来询问我一些事项,最后我们敲定了方案,Gemini 2.5 Pro 在我得到有首肯后,很快就帮我生成了一份需求文档。这期间,AI会帮助我们完善一些可能欠考虑的细节,比如数据的隐私性,它强调:

数据安全:对病人的血常规数据进行加密存储,确保数据库中的数据没有明文可读内容。加密算法建议采用行业常用的对称加密算法如AES(高级加密标准),以保障数据在存储过程中的安全性。

最终形成了一个初版的提示词,它将交给Lovable平台:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
根据你提供的新信息,我们可以进一步完善开发提示词,以下是目前整理的详细提示词,涵盖了之前和新确认的需求,可用于在 https://lovable.dev/ 创建应用:

### 应用概述
开发一个用于跟进病人血常规变化的APP,主要具备两大核心功能:一是允许病人录入血常规情况,支持文字和图片录入方式,图片录入时使用OCR工具识别内容,录入的数据会整理后记录到数据库;二是通过图表(主要是趋势图)展示相关数据,支持选择展示某一个指标的数值,图表横轴为按真实日期连续排列的日期,纵轴为指标对应的数值,同时要考虑每个指标的参考范围并在图中展示,根据数值低于或高于参考范围,对应点展示不同颜色。

### 功能需求

#### 数据录入功能
1. **文字录入**
 - 提供简洁的文字录入界面,病人可按照规范格式录入血常规数据,数据应包含日期(精确到上午或下午)以及各项血常规指标数值(如C反应蛋白、中性粒细胞绝对值、血小板、血红蛋白、红细胞、白细胞等)和标准单位。
 - 当用户录入同一天同一时间(上午或下午视为不同时间)的指标时,新录入的数据将覆盖之前的记录。
 - 提供对某一次血常规结果数据进行单独修改的功能,允许更新或删除某一项指标。
2. **图片录入**
 - 支持病人上传血常规检查报告的图片,APP自动调用OCR工具识别图片中的内容。
 - 展示OCR识别结果供病人确认,病人确认无误后数据存入数据库;若识别结果有误,病人可手动修正。

#### 数据存储功能
1. **数据库记录**:将录入的血常规数据整理后记录到数据库中,数据结构应包含日期、时间、各项血常规指标数值及单位。
2. **数据安全**:对病人的血常规数据进行加密存储,确保数据库中的数据没有明文可读内容。加密算法建议采用行业常用的对称加密算法如AES(高级加密标准),以保障数据在存储过程中的安全性。

#### 图表展示功能
1. **趋势图展示**
 - 以趋势图形式展示血常规指标数据,用户可选择展示某一个指标的数值。
 - 图表横轴为按真实日期连续排列的日期,纵轴为指标对应的数值。
 - 在图表中展示每个指标的参考范围,根据数值低于或高于参考范围,对应点展示不同颜色,以便用户直观了解指标情况。
2. **可选功能(预留)**
 - 未来考虑支持其他类型的图表(如柱状图、饼图等)展示数据。
 - 支持对数据进行筛选,例如只展示某一段时间内的数据。
 - 支持同时对比多个指标的趋势图,以便更直观地观察数据之间的关系。

#### 多用户管理功能(预留)
预留多用户管理功能,为未来支持多个病人使用APP做准备,包括用户的注册、登录和数据隔离等功能。

省略...

在Lovable平台创建应用——选对工具,事半功倍

虽然作为开发者,手上有不少编程工具可以使用,我这次还是选择了Lovable平台,除了我恰好有会员外,还有几个因素也是比较重要的:

  • 一站式的开发与发布平台,这样初期的开发与部署可以无缝衔接,不需要再额外考虑部署的问题。
  • 结合Supabase等开放数据库,提供了多用户的认证以及可以方便地进行数据存储与管理。
  • 它和GitHub的集成,可以方便地进行版本管理,以及未来若有必要,可以随时转到其它开发工具中继续开发。

和Lovable寒暄几句(你好)后便开始制造我们应用的初始版本,没一会,几个页面便出来了,整体雏形便有了。页面美观程度嘛,也还不错? Lovable开发体验 但在和Lovable深入接触后(也就是真正开始开发),我有几点不太爽的感受:

  • 它对于一些稍微复杂点的算法,就容易陷入死循环——反复修改,反复出错,简直让人头大。不知道啥时我居然和AI和编辑器生闷气:)比如我在想让趋势图支持多指标的选择相关功能时,在Lovable平台中反复好多次没有成功。我琢磨着是不是背后的模型还不够强大,在我切换到Cursor中启用Claude 3.5 Sonnet后很快搞定。
  • 它对于指令的遵从还不错,但是缺少一些设计,有些人说用大模型开发容易搞出屎山,大概是这种感觉。并且它写代码是重复重写一个个文件,导致差异上我们也不容易看出来,虽然它经常有文件过大时的重构提示,但这显然治标不治本。
  • 它的输出和修改还是过慢了,对于一些较小的修改,等的我有点忧伤,这不是我想要的"氛围"(Vibe Coding)。

这让我像一个渣男,打算"移情别恋"转投Cursor的怀抱。但更渣的是,我时不时还得回来蹭一下它的发布能力——毕竟人家有打通GitHub一键部署的神通。

在Cursor中精细化开发——真正的战斗才刚刚开始

我早前写过一篇Cursor的开发技巧及思考,感兴趣可以翻看过往文章,也可以在我博客找到:AI编程之Cursor使用技巧及一些思考。在Lovable那儿碰了几鼻子灰后,我不得不把代码克隆下来,开始进行“真刀真枪”的开发。

借助于Cursor,我们可以这样更有效的推进工作。为了让它知道我在干啥,也为了它不要反复乱改一些设定,我们先让Cursor通读一下整个项目,并生成Cursor rules。没一会就生成了好几个文件,看起来满满当当,也有模有样的,为了更好的使用,你可别全信,咱继续定制:

  • 提炼最基础的原则,将它作为一个Always规则,使每次沟通可以带上。反正Cursor又不会基于Token计费(不过Max版本说会收费,你要更小心),这里规则三五百行是没问题的。注意上面默认生成的里面,一般是不会给你设定Always规则的。你每次得自己主动添加上相关文件,这对擅长遗忘的我们容易造成伤害。
  • 删除不需要或模糊的规则。比如像这个工程我没打算让它写测试,所以测试相关说明可以删除。我也不喜欢它在做一件事后马上去更新文档(我还没确认最终是不是这个方案呢),所以文档的更新我也会限制。
  • 添加一些新的规则。我发现现在AI真的有点智能了,居然学会偷懒!有时候一堆lint报错都没解决,就开始给我汇报工作成果了,这就想邀功?未免太早了吧,瞧您这干的啥事。于是我得给它上个"紧箍咒",需要它每次提交时,先检查一下lint,确保没有问题。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# 业务逻辑和数据处理规范较

## 血常规数据管理

### 数据说明
本工程数据区分本地demo数据和数据库supabase存储的数据,它们完全隔离。
* 未登录用户使用的是本地demo数据。
* 登录用户使用其自身数据。

### 数据模型
参考类型定义:@types/blood-test.ts

#### 血常规指标
指标区分基础指标和其它指标。
* 基础指标的参考: @constants/basicIndicators.ts
* 全部指标定义在:@constants/indicators.ts

还有一些规则,我们要一边开发,根据AI的表现为它量身定制。比如我们已经npm run dev启动了项目,它每次提交完又想运行一次,你知道这显然不必,咱已经能动态热更新,所以得限制它。有时我们会沉迷于Vibe Coding的便捷,执着于像个甲方一样反复让它修改实现,就是不想思考如何优化规则,这时AI就会反复提醒你:)所以我感觉,在一个较复杂的项目上,想要快,必须得慢下来,琢磨一下如何去定义适合自己的规则。

在不断的调整和修改下,Cursor的月次数告急。我观察了一下Cursor的计数规则,它不是以你每次的交互多少来计数,而是差不多按完成一项工作来计算。于是假如你是想一出是一出,让它把东边修缮一下,西边再改改,那次数就哗哗的没了。可以考虑以一次将要修改的任务整理好,甚至一些关联的地方也提醒它注意修改,这样可以更充分的利用好次数。比如我这里的用药管理和手术管理经常是类似的逻辑,我就让它修改用药管理界面的A、B、C处,同时在手术管理中也需要类似的逻辑。这次只需要消耗一次计数,哗啦啦一大堆代码和功能就完善了。不过这招也别滥用,不建议一次性塞给它太多不相关的修改任务。我的经验是,把类似的功能打包一次性修改,独立的功能分开处理。改完一部分,自己确认没问题了就赶紧提交到版本库,免得一次改太多,到头来想挑挑拣拣哪些要哪些不要的时候,那叫一个尴尬!虽然Cursor和Cline等都有Checkpoint,其实都不好解决这个问题,我们得自己规范流程。

在使用Cursor过程中,过往我对模型的选择没有太在意,但这次我特意观察了一下,发现不同模型在Cursor中的表现还是有些差异的。比如在Cursor中,我试着用了一段时间那传说中被各大自媒体吹爆的Gemini 2.5 Pro,结果发现这家伙有点“高冷范儿”——思维链一开始还好好说中文,搞着搞着就中英混杂,甚至直接飙英文了,跟它沟通像在考六级。同时感觉它可能太过思维活跃了,动不动自我否定又来推敲一次,导致产出效率其实不高。而当我切换到Claude 3.5 Sonnet模型时,它甚至都表现更好一些,这或许和不少人的感知略有不同,我不确定是否Cursor未对它调教好(默认Prompt未能很好适配?)。但也不要迷恋Claude 3.5 Sonnet,在一次调整中,它也陷入了无尽的自我怀疑与纠结中,还是多花一倍代价(2x)切换到它哥(Claude 3.7 Sonnet)后问题很快得以解决。这告诉我一个道理,必要时启用最强大模型有时是更经济的事情。这个道理很快又被证明也不靠谱,因为没几天Claude 4 Sonnet出来了,号称更强大的编码能力,同时在Cursor中更便宜(0.5x)。我赶紧用上,没想到它似乎还是一匹没有驯服的野马,时有脱缰的时候。事后我在想为啥Cursor给这些优惠呢,在几天后费用升到0.75x时,它渐渐好了起来,或许前面就是鼓励我们使用,有足够数据偷偷调教呢?

借助MCP让代码感知DB——不再做知识的搬运工

我是在使用Lovable才知道有Supabase这个数据库的。最开始惊讶于原来居然可以在保障安全性的同时,不需要借助后端,前端直接就可以和DB交互。它借助了Postgresql的RLS机制,用户的权限被严格限制了。在开发期间,我有一次需要调整和迁移DB。最开始我频繁将DB Schema以及现有数据等截图贴给它,然后Cursor又为我生成一些SQL语句,说可以让我在Supabase平台SQL Editor中执行。我意识到这不是我想要的方式,作为一个现代化的BaaS服务(后端即服务),它也提供了mcp server,果断添加到Cursor中,然后就可以在Cursor中直接操作数据库了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
 "mcpServers": {
 "supabase": {
 "command": "npx",
 "args": [
 "-y",
 "@supabase/mcp-server-supabase@latest",
 "--access-token",
 "<YOUR_SUPABASE_ACCESS_TOKEN>"
 ]
 }
 }
}

如下图,对于DB的修改及查询,甚至迁移等工作,都可以借助这个MCP工具完全在Cursor中完成,我终于不用当搬运工了。 MCP工具

专属网址——入口必须简单直接

整体网站的功能基本就绪后,要是想给其他人用,我寻思着得给它配个好记的网址才行。在NameCheap上找了一个并不太Cheap的com域名: blood-track.com。然后在Lovable平台直接绑定这个域名就可以通过新的自有域名访问啦。我看了一下,域名解析到了一个IPv4地址,应该是Lovable的接入层,因为我在平台登记了项目的自有域名,请求中header又有这个域名信息,故让Lovable路由到它所部署的代码实例上并不困难。

后续规划

开发的初衷嘛,主要是想解决自己记录、整理和洞察(或‘分析’)这些医疗数据时遇到的一些麻烦。因为所知有限,不一定适配于其它病种,虽然基础完备,但也有一些工作后续有空会继续考虑完善:

  • 一些关键事件的记录与呈现。当前虽然有一些手术记录,但对于比如输血、输血小板等,或者感染事件,它也会引发血常规直接变化。
  • 如果使用者变多,Supabase的免费Plan就不够使用了,而要每个月额外几十刀的话,可能自托管supabase或切换到其它DB都可能要提上议程。
  • 多语言版本也可以考虑,如果有呼声的话(我怎么能听到这里的呼声)。
  • 支持更多端,比如微信小程序或APP等。

本站提供的是公益免费的服务,域名、开发维护等成本较低,包括AI的识别等,整体花费不多暂时能够承担。(话说大模型识别一次检查数据费用不到一分钱,感谢这个时代。)

后记

如果你完整看完,不知道期间的感触你是否也有共鸣?如何在AI时代更高效的开发,这是现今乃至未来几年传统开发者都需要面临和思考的问题。 你可能有疑问为什么不开发一个微信小程序等,其实我也有此意。所以期间也尝试用全新的开发范式来建立微信小程序,比如从UI的设计开始,借助AI来设计交互图,然后我也试着回归VSCode + Cline看整体开发一个微信小程序的成本如何,初步完成一个Demo来看,成本尚可接受。未来有空会进一步聊一聊。有Cline后咱最新的模型或免费的渠道羊毛都可以褥一下,成本可能更低(注意也可能时间成本高得多)。

本篇文章就先到这里,感谢你的耐心阅读。期待未来有更多的成果和经验可以继续和你聊聊,欢迎一起交流探讨。这一块领域,全世界都还在探索,我们保持在路上。

我是个爱折腾技术的工程师,也乐于分享。欢迎点赞、关注、分享,更欢迎一起探讨技术问题,共同学习,共同进步。为了获得更及时的文章推送,欢迎关注我的公众号:爱折腾的风

扫码关注公众号

给内网穿透FRP套上坚固的盾牌

2025-05-10 08:00:00

Featured image of post 给内网穿透FRP套上坚固的盾牌

背景

有一天,我突然发现无法从外部连接家里的NAS了。我开始慌了,预感到不妙。莫非是公网IP被运营商回收了?我也成为一个大内网用户了。所幸已经有不少成熟的方案,而FRP就是其中之一。它开源免费,易上手,并且支持的协议还比较多(当然,部署服务器的费用得另算)。晚上回到家,我决定面对现实,好好折腾一番。虽然网上现有的FRP教程多数只完成了‘能用’的第一步,但距离‘好用易用’还有点距离。

本文简要描述一下我使用FRP的过程,并且看一下我们如何给FRP套上坚固的盾牌,配上易用的武器。我假定你已经知道FRP是什么,并且最基本的FRP使用已经了解。不了解也没关系,继续看你也大概能懂。

虽然咱数据或许对别人而言也没那么重要,但自我保护意识也不可松懈。

目标制定

frp 是一个专注于内网穿透的高性能的反向代理应用,支持 TCP、UDP、HTTP、HTTPS 等多种协议,且支持 P2P 通信。可以将内网服务以安全、便捷的方式通过具有公网 IP 节点的中转暴露到公网。

为了方便迁移和管理,在使用FRP时我首推容器化方案。不过似乎没看到官方的镜像,但dockerhub上有一个社区广泛使用且下载量很高的镜像,大抵错不了:snowdreamtech/frpcsnowdreamtech/frps

FRP是分客户端和服务端的,需要在不同的机器分别配置。frpc一般部署在内网,用于将内部需要对外暴露的服务定义出来。而frps一般部署在有公网IP的服务器上,用于接收外部连接并转发到内部服务。这里有几个安全事项需要关注:

  1. 内网frpc和公网frps之间需要建立安全的连接。
  2. 公网frps暴露的端口需要进一步限制连接来源。
  3. 公网frps暴露的端口仅在必要时开放。

很多分享的方案里基本不启用TLS,也对暴露的端口没有进一步的限制,这其实是不安全的。秉承这些目标,我们开始行动吧。

部署服务

内网部署frpc端

这里我直接使用docker-compose部署,并使用snowdreamtech/frpc镜像。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
version: '3.8'
services:
 frpc:
 image: snowdreamtech/frpc:debian
 container_name: frpc
 restart: always
 network_mode: host
 volumes:
 - ./frpc.toml:/etc/frp/frpc.toml
 - ./client.crt:/etc/frp/client.crt
 - ./client.key:/etc/frp/client.key
 - ./ca.crt:/etc/frp/ca.crt
 environment:
 - TZ=Asia/Shanghai
 env_file:
 - ./.env

这里需要的TLS证书一会我们再生成。这里的.env文件内容如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 服务端连接信息
FRPC_SERVER_ADDR=<your-server-address>
FRPC_SERVER_PORT=<your-server-port>

# 服务器域名
FRPC_SERVER_NAME="<your-domain-for-tls>"

# 认证令牌 - 必须与服务端一致
FRPC_AUTH_TOKEN=<your-auth-token>

# 客户端仪表盘配置
FRPC_DASHBOARD_USER=admin
FRPC_DASHBOARD_PWD=admin

为了方便修改和对齐,我们将frpc.toml文件中的一部分配置放在.env文件中定义。而frpc.toml文件内容如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
user = "kevin"

serverAddr = "{{ .Envs.FRPC_SERVER_ADDR }}"
serverPort = {{ .Envs.FRPC_SERVER_PORT }}

loginFailExit = true

log.to = "./frpc.log"
log.level = "trace"
log.maxDays = 3
log.disablePrintColor = false

auth.method = "token"
auth.token = "{{ if .Envs.FRPC_AUTH_TOKEN }}{{ .Envs.FRPC_AUTH_TOKEN }}{{ else }}{{ .Envs.FRP_AUTH_TOKEN }}{{ end }}"

transport.poolCount = 5
transport.protocol = "tcp"
transport.connectServerLocalIP = "0.0.0.0"

transport.tls.enable = true
transport.tls.certFile = "/etc/frp/client.crt"
transport.tls.keyFile = "/etc/frp/client.key"
transport.tls.trustedCaFile = "/etc/frp/ca.crt"
transport.tls.serverName = "{{ .Envs.FRPC_SERVER_NAME }}"

udpPacketSize = 1500

webServer.addr = "127.0.0.1"
webServer.port = 7400
webServer.user = "{{ .Envs.FRPC_DASHBOARD_USER }}"
webServer.password = "{{ .Envs.FRPC_DASHBOARD_PWD }}"

[[proxies]]
name = "router-web"
type = "tcp"
localIP = "192.168.1.1"
localPort = 80
remotePort = 17603

[[proxies]]
name = "external-http"
type = "tcp"
localIP = "192.168.50.96"
localPort = 8080
remotePort = 9443

公网部署frps端

我们的服务端一般是部署在一台有公网IP的服务器上,用于我们从任何地方通过它连接回家里内网。这个服务器可以从国内国外各种云上买一台或者找机会白嫖一台。我是在腾讯云上有一台机器,安装好docker以及docker-compose后,使用snowdreamtech/frps镜像部署。部署在公网的服务,基于安全性考虑,我们希望即使frps被攻击,其它服务也是安全的,所以除了放在容器中,把网络也隔离出来。这里便不再使用network_mode: host,而是使用默认的network_mode: bridge,同时预留一些端口用于后续我们的服务暴露。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
version: '3.8'
services:
 frps:
 image: snowdreamtech/frps:debian
 container_name: frps
 restart: always
 ports:
 - "9443:9443"
 - "17600-17610:17600-17610" # TCP/UDP 代理端口范围 (allowPorts),视你需要开放一些端口
 volumes:
 - ./frps.toml:/etc/frp/frps.toml
 - ./server.crt:/etc/frp/server.crt
 - ./server.key:/etc/frp/server.key
 - ./ca.crt:/etc/frp/ca.crt
 environment:
 - TZ=Asia/Shanghai
 env_file:
 - ./.env

同样的,为了配置修改方便,我们将frps.toml文件中的一部分配置放在.env文件中定义:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 仪表盘访问凭证
FRPS_DASHBOARD_USER=<your-dashboard-username>
FRPS_DASHBOARD_PWD=<your-dashboard-password>

# 认证令牌 - 必须与客户端一致
FRPS_AUTH_TOKEN=<your-auth-token>

# 绑定地址和端口配置
FRPS_BIND_ADDR=0.0.0.0
FRPS_BIND_PORT=17600
FRPS_KCP_BIND_PORT=17600
FRPS_DASHBOARD_PORT=17601

而frps.toml文件内容如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
bindAddr = "{{ .Envs.FRPS_BIND_ADDR }}"
bindPort = {{ .Envs.FRPS_BIND_PORT }}

kcpBindPort = {{ .Envs.FRPS_KCP_BIND_PORT }}

transport.maxPoolCount = 5
transport.tls.force = true
transport.tls.certFile = "/etc/frp/server.crt"
transport.tls.keyFile = "/etc/frp/server.key"
transport.tls.trustedCaFile = "/etc/frp/ca.crt"

webServer.addr = "0.0.0.0"
webServer.port = {{ .Envs.FRPS_DASHBOARD_PORT }}
webServer.user = "{{ .Envs.FRPS_DASHBOARD_USER }}"
webServer.password = "{{ .Envs.FRPS_DASHBOARD_PWD }}"
webServer.pprofEnable = false

# 开放的端口范围,这里可以配置适大一些,更多的映射(限制)在docker-compose中
allowPorts = [
 { start = 17000, end = 17999 },
 { single = 9443 }
]


enablePrometheus = true

log.to = "./frps.log"
log.level = "trace"
log.maxDays = 3
log.disablePrintColor = false

detailedErrorsToClient = true

auth.method = "token"
auth.token = "{{ if .Envs.FRPS_AUTH_TOKEN }}{{ .Envs.FRPS_AUTH_TOKEN }}{{ else }}{{ .Envs.FRP_AUTH_TOKEN }}{{ end }}"
auth.oidc.issuer = ""
auth.oidc.audience = ""
auth.oidc.skipExpiryCheck = false
auth.oidc.skipIssuerCheck = false

maxPortsPerClient = 0
udpPacketSize = 1500
natholeAnalysisDataReserveHours = 168

生成TLS证书

为了让FRP的连接更安全,我们使用TLS证书来加密连接。它确保我们的frpc和frps之间的连接是安全的,并且防止中间人攻击。我们使用自签名证书来生成TLS证书。以下提供了一段脚本,方便一键生成我们需要的证书。

 1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
#!/bin/bash
# 脚本用于生成 FRP 配置所需的自签名证书

set -e # 任何命令失败立即退出

echo "开始生成 FRP 通信证书..."

# 设置默认值,但要求用户至少提供一个
SERVER_DOMAIN=""
SERVER_IP=""

# 仅重新生成服务器证书的选项
REGENERATE_SERVER_ONLY=false

# 显示帮助信息
show_help() {
 echo "用法: $0 [选项]"
 echo ""
 echo "选项:"
 echo " --server-only 仅重新生成服务器证书,保留现有CA证书"
 echo " --domain=<域名> 指定服务器域名"
 echo " --ip=<IP地址> 指定服务器IP地址"
 echo " --help 显示此帮助信息"
 echo ""
 echo "至少需要指定域名或IP地址中的一个"
 exit 1
}

# 解析命令行参数
while [[ $# -gt 0 ]]; do
 case $1 in
 --server-only)
 REGENERATE_SERVER_ONLY=true
 shift
 ;;
 --domain=*)
 SERVER_DOMAIN="${1#*=}"
 shift
 ;;
 --ip=*)
 SERVER_IP="${1#*=}"
 shift
 ;;
 --help)
 show_help
 ;;
 *)
 echo "未知选项: $1"
 show_help
 ;;
 esac
done

# 检查是否提供了至少一个参数
if [ -z "$SERVER_DOMAIN" ] && [ -z "$SERVER_IP" ]; then
 echo "错误: 必须至少指定域名(--domain)或IP地址(--ip)中的一个"
 show_help
fi

# 显示配置信息
echo "证书配置:"
if [ -n "$SERVER_DOMAIN" ]; then
 echo "- 域名: $SERVER_DOMAIN"
fi
if [ -n "$SERVER_IP" ]; then
 echo "- IP地址: $SERVER_IP"
fi

if [ "$REGENERATE_SERVER_ONLY" = false ]; then
 # 1. 生成 CA 根证书
 echo "生成 CA 根证书..."
 openssl genrsa -out ca.key 4096
 openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 -out ca.crt \
 -subj "/C=CN/ST=Beijing/L=Beijing/O=FRP-Private/OU=DevOps/CN=frp-ca"
else
 echo "跳过 CA 证书生成,使用现有 CA 证书..."
 # 检查CA证书是否存在
 if [ ! -f "ca.key" ] || [ ! -f "ca.crt" ]; then
 echo "错误: CA证书文件不存在。请先运行不带 --server-only 参数的脚本生成完整证书集。"
 exit 1
 fi
fi

# 2. 生成服务端证书
echo "生成服务端证书..."
openssl genrsa -out frps/server.key 4096

# 设置默认CN
CN_VALUE="${SERVER_DOMAIN}"
if [ -z "$CN_VALUE" ]; then
 CN_VALUE="frps"
fi

# 创建OpenSSL配置文件
cat > frps/openssl.cnf << EOF
[ req ]
default_bits = 4096
prompt = no
default_md = sha256
req_extensions = req_ext
distinguished_name = dn

[ dn ]
C = CN
ST = Beijing
L = Beijing
O = FRP-Private
OU = DevOps
CN = ${CN_VALUE}

[ req_ext ]
subjectAltName = @alt_names

[ alt_names ]
EOF

# 动态添加DNS和IP到配置文件
DNS_COUNT=1
if [ -n "$SERVER_DOMAIN" ]; then
 echo "DNS.${DNS_COUNT} = ${SERVER_DOMAIN}" >> frps/openssl.cnf
 DNS_COUNT=$((DNS_COUNT+1))
fi
echo "DNS.${DNS_COUNT} = localhost" >> frps/openssl.cnf

IP_COUNT=1
if [ -n "$SERVER_IP" ]; then
 echo "IP.${IP_COUNT} = ${SERVER_IP}" >> frps/openssl.cnf
 IP_COUNT=$((IP_COUNT+1))
fi
echo "IP.${IP_COUNT} = 127.0.0.1" >> frps/openssl.cnf

# 使用配置文件生成CSR
openssl req -new -key frps/server.key -out frps/server.csr -config frps/openssl.cnf

# 创建扩展配置文件 - 正确的格式
cat > frps/v3.ext << EOF
subjectAltName = @alt_names
[alt_names]
EOF

# 添加DNS和IP到扩展配置
if [ -n "$SERVER_DOMAIN" ]; then
 echo "DNS.1 = ${SERVER_DOMAIN}" >> frps/v3.ext
 echo "DNS.2 = localhost" >> frps/v3.ext
else
 echo "DNS.1 = localhost" >> frps/v3.ext
fi

if [ -n "$SERVER_IP" ]; then
 echo "IP.1 = ${SERVER_IP}" >> frps/v3.ext
 echo "IP.2 = 127.0.0.1" >> frps/v3.ext
else
 echo "IP.1 = 127.0.0.1" >> frps/v3.ext
fi

# 签署证书,应用SAN扩展
openssl x509 -req -in frps/server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
 -out frps/server.crt -days 3650 -sha256 -extfile frps/v3.ext

if [ "$REGENERATE_SERVER_ONLY" = false ]; then
 # 3. 生成客户端证书
 echo "生成客户端证书..."
 openssl genrsa -out frpc/client.key 4096
 openssl req -new -key frpc/client.key -out frpc/client.csr \
 -subj "/C=CN/ST=Beijing/L=Beijing/O=FRP-Private/OU=DevOps/CN=frpc"
 openssl x509 -req -in frpc/client.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
 -out frpc/client.crt -days 3650 -sha256

 # 4. 分发 CA 证书到 frps 和 frpc 目录
 echo "分发 CA 证书到各个目录..."
 cp ca.crt frps/ca.crt
 cp ca.crt frpc/ca.crt
else
 echo "跳过客户端证书生成,仅更新服务器证书..."
fi

# 5. 清理中间文件
echo "清理临时文件..."
rm -f frps/server.csr ca.srl frps/openssl.cnf frps/v3.ext
if [ "$REGENERATE_SERVER_ONLY" = false ]; then
 rm -f frpc/client.csr
fi

echo "设置文件权限..."
chmod 600 ca.key frps/server.key
if [ "$REGENERATE_SERVER_ONLY" = false ]; then
 chmod 600 frpc/client.key
fi

echo "证书生成完成!"
if [ "$REGENERATE_SERVER_ONLY" = true ]; then
 echo "已使用现有CA证书重新生成服务器证书"
else
 echo "注意:ca.key 是敏感文件,请妥善保管,建议不要上传到代码仓库。"
fi

# 输出证书信息和提示
echo ""
echo "===== 证书信息 ====="
if [ -n "$SERVER_DOMAIN" ]; then
 echo "服务器证书包含域名: ${SERVER_DOMAIN}"
 echo "如果使用域名连接,请在frpc.toml中设置: transport.tls.serverName = \"${SERVER_DOMAIN}\""
fi
if [ -n "$SERVER_IP" ]; then
 echo "服务器证书包含IP地址: ${SERVER_IP}"
fi

# 提示下一步操作
echo ""
echo "===== 下一步操作 ====="
echo "1. 复制证书文件到相应位置"
echo "2. 更新frpc.toml中的服务器地址和相关配置"
echo "3. 使用docker-compose restart重启服务"

# 验证证书内容
echo ""
echo "===== 验证证书 ====="
echo "查看证书内容(包括SAN扩展):"
openssl x509 -in frps/server.crt -text -noout | grep -A1 "Subject Alternative Name"

使用方式:

1
./generate-frp-certs.sh --domain=<your-domain> --ip=<your-ip>

这样我们的服务器与客户端双向认证,谁都不可被冒充。

安全加固

到这里,你的FRP连接已经通过TLS和双向认证得到了很好的保护,即使token不慎泄露,没有匹配的证书也无法建立连接。接下来,我们在此基础上更进一步,结合云主机的防火墙(安全组)策略,实现更精细的访问控制。我们希望达成:

  • 仅允许必要的端口放通
  • 仅允许必要的IP连接

相信很多人都明白这个道理,但手动操作实在繁琐,所以我们需要一个工具来帮忙。在我看来,最便捷的莫过于再次祭出alfred workflow来实现。于是我抽空写了一个,并开源在github上,感兴趣的可以看这里:alfred-workflow-sg-manager

我们基于frpc.toml中的一部分配置[[proxies]],来动态开启与关闭相关的端口。一点点前置工作还需要你做的,便是将你的服务器绑定一个安全组,至于安全组后续规则添加维护等就交给这个工具了。 比如先查看一下当前列表: frp list 你上面可以看到这些内网服务还未开放,我们选择一个,比如想从外部访问家里的Mac mini了,我们在alfred框中输入frp open可以看到可开放的列表: frp open 选中Mac mini,然后回车,这个通路便打开了。再次查看可以看到它开放并且绑定了本机的出口IP: frp list

现在你可以开心的从外部VNC到你家内网的Mac mini了,并且安全得多了。 用过之后想关闭的一些端口,比如Mac mini的VNC端口,我们输入frp close,然后选择对应的要关闭的服务回车即可: frp close

现在咱是不是对于FRP的安全使用更有信心了。有些人可能会说:何苦呢,有谁会看中咱攻击咱呢?或许可能Maybe:我们就是控制欲作祟而已:)

一些技巧

还有两个小窍门我也想让你知道,看在这么认真的份上小手点点赞不过份吧!

第一:如上面截图出现了external-http,我们可以借助于将内网服务统一在一个ingress服务(反向代理)下,然后通过这个ingress服务进一步路由,这样我们只需要穿透一个端口即可访问内网的各种服务,免去了配置FRP的手续。也通过让ingress服务走TLS,或者后端服务对接OAuth2等更安全的认证方式,可以进一步保护我们的内网服务。

第二:FRPS除了默认有Dashboard外,还支持Prometheus。我们可以复用这种生态。Prometheus用于收集监控数据,Grafana用于可视化数据和配置告警。比如我们可监控FRP的连接访问情况,并且添加告警,这样某个敏感服务有连接,我们便可以及时收到通知。

后记

本文整体到这里就结束了。我们尝试用docker来部署了FRP的客户端和服务端,并且基于安全的考虑,我们启用了TLS和创建了一个便捷的工具来快速修改云上的安全策略。这犹如一块坚固的盾牌,避免我们可能受到的攻击。

当我折腾好FRP,并且安全地将它保护起来后。有一天,我查看我家的外网IP,发现它居然是一个公网IP。我的天啦,我这折腾一番可是为了啥!我要不要重新回归到公网IP的路线呢?可是我却放不下这份安全了呢。

注:题图来自于互联网,我觉得画得挺棒,若有侵权请联系我删除。

我是个爱折腾技术的工程师,也乐于分享。欢迎点赞、关注、分享,更欢迎一起探讨技术问题,共同学习,共同进步。为了获得更及时的文章推送,欢迎关注我的公众号:爱折腾的风

扫码关注公众号

让你的小智AI机器人起飞

2025-05-04 08:00:00

Featured image of post 让你的小智AI机器人起飞

前阵子B站刷到小智AI机器人,入手了一个玩玩,官方的服务器显然不足以满足我的幻想,于是搭建了第三方服务器,顺便给它功能扩展了一下,到底做了点啥呢,似乎有一点意思了。

上手初体验

这个机器人似乎前阵子比较火,有不少硬件形态,有比较小巧可爱的,也有硬核直接的,但内核是不变的,如果你从来没玩过,好像长什么样,可以B站看看或淘宝啥的搜索一下。我家的比较抽象,长得像这样: 小智AI机器人

跟着电路图或者引脚说明接线,这步不是有手就行,还得有力气:)因为ESP32的针脚和面包板孔不完全匹配,需要用力强行按进去。看过B站有个哥们用锤子敲,也真是乐坏我了。供电的话我们可以随便拿个充电宝,倒也是很容易搞定、也有一定移动性。

最开始体验时,参考官方的DIY教程将设备刷好官方固件后,整体响应较快,对话流畅,不过模型数量有限,声音复刻收费,特别是如果这个不能联网做点啥,光和模型过时的数据聊天略有点无聊啊。结合当前较火的MCP功能,于是有了两个扩展目标:

  • 复刻个人的声音让AI用我的语气说话
  • 扩展MCP功能,让AI具备连接更多外界能力

消耗了一点闲暇时间,目前已经跑起来咯,下面简要分享一下过程。如果你也想折腾或者有好点子,欢迎一起交流。

部署专属服务器

小智官方并没有把服务器开源,但提供了交互协议等,有第三方开源服务器实现 xiaozhi-esp32-server,可供使用。要完成可以使用第三方服务器,我们做两件事:

  1. 将服务部署到自己的机器上。
  2. 修改小智的固件的两个地址,重新编译并烧录到设备上。

其实都很简单,我们需要动脑的机会不多,像我这样一步步来即可。

部署服务器

参考xiaozhi-esp32-server的Docker运行全模块教程。我推荐使用docker部署,主要是为了保持本地的干净。

准备配置文件.config.yaml,在这个服务器中,使用了多层config来互相覆盖,我们定义的.config.yaml会覆盖默认的config.yaml。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# 在开发中,请在项目根目录创建data目录,然后在data目录创建名称为【.config.yaml】的空文件
# 然后你想修改覆盖修改什么配置,就修改【.config.yaml】文件,而不是修改【config.yaml】文件
# 系统会优先读取【data/.config.yaml】文件的配置,如果【.config.yaml】文件里的配置不存在,系统会自动去读取【config.yaml】文件的配置。
# 这样做,可以最简化配置,保护您的密钥安全。
read_config_from_api: True

manager-api:
 # 你的manager-api的地址,最好使用局域网ip
 url: http://xiaozhi-esp32-server-web:8002/xiaozhi
 # 你的manager-api的token,就是刚才复制出来的server.secret
 secret: 8e0ee7db-3928-400f-9e93-261275f91cf0

# #####################################################################################
# #############################以下是服务器基本运行配置####################################
server:
 # 服务器监听地址和端口(Server listening address and port)
 ip: 0.0.0.0
 port: 8000

这里的配置不用太多,因为它的配置还有一层可以动态通过manager-api来下发更新,所以这里我们只需要配置一个manager-api的地址和secret即可。

然后我们使用docker-compose部署,在此之前你需要自行下载语音识别模型文件到models/SenseVoiceSmall/model.pt,这些细节上面链接都有提,不啰嗦了,最终yaml如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# Docker安装全模块
services:
 # Server模块
 xiaozhi-esp32-server:
 image: ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:server_latest
 container_name: xiaozhi-esp32-server
 depends_on:
 - xiaozhi-esp32-server-db
 - xiaozhi-esp32-server-redis
 restart: always
 networks:
 - default
 ports:
 # ws服务端
 - "8000:8000"
 security_opt:
 - seccomp:unconfined
 environment:
 - TZ=Asia/Shanghai
 volumes:
 # 配置文件目录
 - ./data:/opt/xiaozhi-esp32-server/data
 # 模型文件挂接,很重要
 - ./models/SenseVoiceSmall/model.pt:/opt/xiaozhi-esp32-server/models/SenseVoiceSmall/model.pt

 # manager-api和manager-web模块
 xiaozhi-esp32-server-web:
 image: ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:web_latest
 container_name: xiaozhi-esp32-server-web
 restart: always
 networks:
 - default
 depends_on:
 xiaozhi-esp32-server-db:
 condition: service_healthy
 xiaozhi-esp32-server-redis:
 condition: service_healthy
 ports:
 # 智控台
 - "8002:8002"
 environment:
 - TZ=Asia/Shanghai
 - SPRING_DATASOURCE_DRUID_URL=jdbc:mysql://xiaozhi-esp32-server-db:3306/xiaozhi_esp32_server?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true&connectTimeout=30000&socketTimeout=30000&autoReconnect=true&failOverReadOnly=false&maxReconnects=10
 - SPRING_DATASOURCE_DRUID_USERNAME=root
 - SPRING_DATASOURCE_DRUID_PASSWORD=123456
 - SPRING_DATA_REDIS_HOST=xiaozhi-esp32-server-redis
 - SPRING_DATA_REDIS_PORT=6379
 xiaozhi-esp32-server-db:
 # image: mysql/mysql-server:latest
 image: mysql:latest
 container_name: xiaozhi-esp32-server-db
 healthcheck:
 test: [ "CMD", "mysqladmin" ,"ping", "-h", "localhost" ]
 timeout: 45s
 interval: 10s
 retries: 10
 restart: always
 networks:
 - default
 ports:
 - "3306:3306"
 volumes:
 - ./mysql/data:/var/lib/mysql
 environment:
 - TZ=Asia/Shanghai
 - MYSQL_ROOT_PASSWORD=123456
 - MYSQL_DATABASE=xiaozhi_esp32_server
 - MYSQL_INITDB_ARGS="--character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci"
 xiaozhi-esp32-server-redis:
 image: redis
 expose:
 - 6379
 container_name: xiaozhi-esp32-server-redis
 healthcheck:
 test: ["CMD", "redis-cli", "ping"]
 interval: 10s
 timeout: 5s
 retries: 3
 networks:
 - default
networks:
 default:

使用命令启动:

1
docker-compose up -d

等服务起来了之后,我们可以使用仓库自带的test页面测试,浏览器打开main/xiaozhi-server/test/test_page.html,可以看到如下页面: 测试页面 你最好是在这个网页测试通过,聊天、语音等都符合你的预期,再进行下一步。话说这个页面也是调试利器了,效率高不少。

这里还有个小技巧,因为我们接下来需要将ota/websocket地址烧录在固件上,如果你不确定服务未来会部署在哪台机器上,可以通过一个反向代理来转发。给个示意了解一下即可:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
 name: to-xiaozhi-web
spec:
 entryPoints:
 - websecure
 routes:
 - match: Host(`web-xiaozhi.mrlin.cc`)
 kind: Rule
 services:
 - name: web-xiaozhi
 port: 80
---
apiVersion: v1
kind: Service
metadata:
 name: web-xiaozhi
spec:
 ports:
 - protocol: TCP
 port: 80
 targetPort: 8002
---
apiVersion: v1
kind: Endpoints
metadata:
 name: web-xiaozhi
subsets:
 - addresses:
 - ip: 192.168.50.190
 ports:
 - port: 8002
---
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
 name: to-xiaozhi-api
spec:
 entryPoints:
 - websecure
 routes:
 - match: Host(`api-xiaozhi.mrlin.cc`)
 kind: Rule
 services:
 - name: api-xiaozhi
 port: 80
---
apiVersion: v1
kind: Service
metadata:
 name: api-xiaozhi
spec:
 ports:
 - protocol: TCP
 port: 80
 targetPort: 8000
---
apiVersion: v1
kind: Endpoints
metadata:
 name: api-xiaozhi
subsets:
 - addresses:
 - ip: 192.168.50.190
 ports:
 - port: 8000

上面我将api-xiaozhiweb-xiaozhi都指向了我的本地IP,这样我就可以通过api-xiaozhi.mrlin.ccweb-xiaozhi.mrlin.cc来访问了,未来迁移到其他机器上,只需要修改这个yaml文件即可。你不搞这一步一点问题也没有,继续用IP,注意那样就使用http/ws协议。

修改固件&编译&烧录

这里我们要进入到官方固件仓库来继续我们的操作。并且编译烧录等需要你在Windows下进行了。 我们可以参考这篇文章Windows搭建 ESP IDF 5.3.3开发环境以及编译小智大概了解整个过程;接着看这里简要而关键的固件构建过程。

简单来说,当前最新版本我们仅需要修改OTA_URL地址即可,以往要修改websocket地址,现在它已经通过OTA下发了。

当天我正在照着教程编译,发现不找到websocket的定义了,定睛一看刚好有提交重构了这一块,于是顺便向server那边也提了个PR,居然很快就被合并了。不过当时字段名取得不太好,我延用了固件那边的websocket_url,后面又被修改为server.websocket,这确实更符合项目的命名规范一些。

之后构建就几个命令:

1
2
3
4
5
6
# 终端命令行进入xiaozhi-esp32的根目录
cd xiaozhi-esp32
# 例如我使用的板子是esp32s3,所以设置编译目标为esp32s3,如果你的板子是其他型号,请替换成对应的型号
idf.py set-target esp32s3
# 进入菜单配置
idf.py menuconfig

之后是烧录,将你的小智AI设备通过串口连接到电脑,然后使用idf.py build构建,接着使用idf.py -p PORT flash烧录。 这里的PORT根据你连接的设备不同,可能有所不同,在电脑中我的设备查看串口,我的是COM4。如果嫌烧录慢,可以添加参数-b 2000000来加速。

一切成功后,同时也在上一步骤的服务器的server.websocket中填入wss://api-xiaozhi.mrlin.cc/xiaozhi/v1/,然后就可以打开设备连接自己的服务器开玩了。 server websocket修改 现在你可能发现有了更多的LLM选择,有了更多的TTS(声音)等,似乎是更开放了,或许可以玩得更嗨了。

个人声音复刻

之前在玩火山引擎时,送了几次声音复刻给我,体验了一下,当我想用在小智上时,发现已经过期了,居然只有十天有效期,这也太抠了。我去腾讯云找了一下,也有免费的额度,申请下来有3个月,那么就玩玩吧。不像火山引擎的声音复刻在网页上提交声音即可,腾讯云的复刻要原始得多,需要自己使用API,所幸相关功能也在它的tccli中,那么也就是敲命令的事,也不可能难倒我们。

命令行体验

我们可以参考文档声音复刻相关接口,使用命令行来把过程体验一下。至于如何安装tccli,可以参考腾讯云命令行工具

1. 获取训练文本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
❯ tccli vrs GetTrainingText --TaskType 5 --Domain 1
{
 "Data": {
 "TrainingTextList": [
 {
 "TextId": "5f3ef960-4f11-4f4b-84e6-514692ebe0c5",
 "Text": "亲 默认是发韵达快递哦 您这边可以收到韵达的货吗 "
 }
 ]
 },
 "RequestId": "8c758bac-19fc-474d-9b12-2e813e834102"
}

这一步要我们根据提供的文本来提供一段音频,我盲猜是不是要规避用别人的声音来复刻呢?

2. 环境检测和音频质量检测

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
❯ tccli vrs DetectEnvAndSoundQuality --cli-unfold-argument --TextId "5f3ef960-4f11-4f4b-84e6-514692ebe0c5" --AudioData "$(base64 -i vrs.m4a | tr -d '\n')" --TypeId 2 --Codec m4a --TaskType 5


{
 "Data": {
 "AudioId": "audio0de472d6d96d488ba2159d83c10b440f",
 "DetectionCode": 0,
 "DetectionMsg": "Success",
 "DetectionTip": []
 },
 "RequestId": "6218aa6d-9d45-492c-9c18-dfa342d3bd5e"
}

这一步看名称感觉多余,但是下一步创建任务需要这个AudioId,所以还是得来一下。

3. 创建声音复刻任务

1
2
3
4
5
6
7
❯ tccli vrs CreateVRSTask --SessionId 1234 --VoiceName kevin --VoiceGender 1 --VoiceLanguage 1 --AudioIdList "[\"audio0de472d6d96d488ba2159d83c10b440f\"]" --TaskType 5
{
 "Data": {
 "TaskId": "fast7feffdeb04ca401583a317002dd14e7a"
 },
 "RequestId": "127ad8c6-a27d-470f-8dab-b9940c9594cb"
}

我们得到了一个任务ID。原因或许是这一步是异步的,服务器训练这个声音也需要时间。

4. 查询声音复刻任务结果

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
❯ tccli vrs DescribeVRSTaskStatus --TaskId fast7feffdeb04ca401583a317002dd14e7a
{
 "Data": {
 "TaskId": "fast7feffdeb04ca401583a317002dd14e7a",
 "Status": 2,
 "StatusStr": "Success",
 "VoiceType": 200000000,
 "ErrorMsg": "",
 "ExpireTime": "2025-08-02T00:00:00+08:00",
 "FastVoiceType": "WCHN-869b22949cbe4db6a344f1da8cf8a68c"
 },
 "RequestId": "3eb52b2d-3fa1-42f5-84f0-abf71bef32e2"
}

查到的结果里有几个比较关键的,VoiceTypeFastVoiceType,前者是音色ID,后者是快速复刻时使用的音色ID,请用小本本记录下来备用。

5. 查询复刻音色信息

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
❯ tccli vrs GetVRSVoiceTypeInfo --VoiceType 200000000 --TaskType 5 --FastVoiceType WCHN-869b22949cbe4db6a344f1da8cf8a68c
{
 "Data": {
 "VoiceType": 200000000,
 "VoiceName": "kevin",
 "VoiceGender": 1,
 "TaskType": 5,
 "TaskID": "fast7feffdeb04ca401583a317002dd14e7a",
 "DateCreated": "2025-05-03T15:17:35+08:00",
 "IsDeployed": true,
 "ExpireTime": "2025-08-02T00:00:00+08:00",
 "FastVoiceType": "WCHN-869b22949cbe4db6a344f1da8cf8a68c"
 },
 "RequestId": "db5709d2-4e2c-432d-8bd3-ed2eddcb6c21"
}

这一步可能真没啥用了,我们要的信息前面也有了,说有用的话,你大概知道这是一个男人名叫kevin的声音。

6. 使用复刻声音来合成语音

1
2
3
4
5
❯ tccli tts TextToVoice \
--Text "我今天带娃在meland玩,这家伙玩疯了呢" \
--SessionId 123 \
--VoiceType 200000000 \
--FastVoiceType WCHN-869b22949cbe4db6a344f1da8cf8a68c > result.json

返回的内容包括base64后的音频数据,我们使用jq来提取并保存到文件:

1
jq -r '.Audio' result.json | base64 -d > tts.wav

播放来听听吧,感受一下自己声音的魔力或者惊吓。

小智server支持使用复刻声音

当我使用复刻声音时,发现小智server并不支持,于是我给它添加了支持,并提交了PRfeat: 支持腾讯TTS声音一句话复刻后的合成,这次没有很快被合并,因为官方说某个字段要废弃了,又没说最新用什么方式,明儿就就腾讯云提工单。

如PR的修改所见,仅是增加了上述fast_voice_type参数的传递即可。不管上游合不合修改,反正我本地已经用上了。有个小发现,这里server中处理不同语音合成模型的参数,UI是基于fields的字段及其定义动态生成展示的,这还不错。顺手update一个DB,对应的UI就有新字段出来了。

1
2
3
4
UPDATE `ai_model_provider`
SET `fields` = '[{"key":"appid","label":"应用ID","type":"string"},{"key":"secret_id","label":"Secret ID","type":"string"},{"key":"secret_key","label":"Secret Key","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"},{"key":"region","label":"区域","type":"string"},{"key":"voice","label":"音色ID","type":"string"},{"key":"fast_voice_type","label":"FastVoiceType","type":"string"}]',
 `update_date` = NOW()
WHERE id = 'SYSTEM_TTS_TencentTTS';

不知道当你发现你自己在和自己说话时是什么感受,反正我挺惊奇的,感觉自己像个神经病。明儿我还是复刻一个我孩子的声音吧,我就可以随时亲子教育了:D

MCP扩展

在这个第三方服务器的实现中,它支持了一部分MCP的能力,也即stdio模式的mcp server。这会因为使用容器的模式,限制了它的使用。容器内可没有npx/uvx啥的,更没有chrome了。我就一个小小愿望,问它谁是最美丽的人,不对,问它昨天曼联输了个几比几,它不能实时联网找到正确答案我就不满意了。

同样的,让我们扩展它,只需要几行代码的修改就可以啦,详见PRfeat: MCP server支持使用sse模式。一般来说宿主机上使用npx/uvx/chrome等,容器内我们通过http协议使用mcp-server还是更简单的。可是有一些mcp server人家就没实现sse咋办?一点都不困难,比如我们借助supergateway就可以让几个模式互转。

使用supergateway

Supergateway runs MCP stdio-based servers over SSE (Server-Sent Events) or WebSockets (WS) with one command. This is useful for remote access, debugging, or connecting to clients when your MCP server only supports stdio.

比如我有这样一个perplexity-ask的mcp server,它只支持stdio模式,那么我就可以使用supergateway来让它支持sse模式:

1
2
3
4
5
6
7
 "perplexity-ask": {
 "command": "/opt/homebrew/bin/node",
 "args": ["/Users/kevinlin/Source/modelcontextprotocol/perplexity-ask/dist/index.js"],
 "env": {
 "PERPLEXITY_API_KEY": "pplx-..."
 }
 }

使用supergateway来启动:

1
2
3
4
export PERPLEXITY_API_KEY=pplx-... &&
npx -y supergateway \
 --stdio "/opt/homebrew/bin/node .../perplexity-ask/dist/index.js" \
 --port 8123

于是我们给小智server的mcp config就可以这么简单了:

1
2
3
4
5
{
 "perplexity-ask": {
 "url": "http://your-ip:8123"
 }
}

现在你的小智就可以更好的使用MCP server了,我试着问了一下,它说曼联最近欧联13场不败!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
 "role": "assistant",
 "tool_calls": [
 {
 "id": "call_202505041955402e0c441c996740fb_0",
 "function": {
 "arguments": "{\"messages\": [{\"role\": \"user\", \"content\": \"曼联过去几场欧联杯比赛的对手和结果?\"}]}",
 "name": "perplexity_ask"
 },
 "type": "function",
 "index": 0
 }
 ]
 },
 {
 "role": "tool",
 "tool_call_id": "call_202505041955402e0c441c996740fb_0",
 "content": "曼联在2024-2025赛季欧联杯中表现出色,以下是他们最近几场欧联杯比赛的对手和结果:\n\n最近的欧联杯比赛,曼联在半决赛首回合客场3-0完胜西甲第四名的毕尔巴鄂竞技[1][2]。这场比赛中,卡塞米罗为曼联首开纪录,随后布鲁诺·费尔南德斯梅开二度,全部进球都是在上半场完成的[2]。比赛中,毕尔巴鄂竞技的后卫丹尼尔·维维安被罚下场[2]。\n\n在四分之一决赛中,曼联对阵法国球队里昂。首回合比赛两队2-2战平[5]。次回合在老特拉福德球场进行的比赛异常激烈,经过常规时间和加时赛的鏖战,曼联最终以5-4战胜里昂,总比分7-6晋级半决赛[5]。在这场比赛中,达洛特建功,布鲁诺·费尔南德斯点射破门,最后时刻马奎尔上演读秒绝杀[5]。\n\n值得一提的是,曼联本赛季在欧联杯中保持着13场不败的强势表现[1],与他们在英超联赛中的低迷形成了鲜明对比。新帅阿莫林有望复制穆里尼奥在2016-17赛季率领曼联首个赛季就夺得欧联杯冠军的成就[2]。\n\n曼联已经一只脚踏入欧联杯决赛,决赛将在本月下旬在毕尔巴鄂的圣马梅斯球场举行[2]。\n\nCitations:\n[1] https://www.163.com/dy/article/JUKFHV4C0530UTM3.html\n[2] https://www.163.com/dy/article/JUHUQC8L05567SBP.html\n[3] https://zh.wikipedia.org/zh-cn/%E6%9B%BC%E8%81%AF2024%E5%B9%B4%E8%87%B32025%E5%B9%B4%E7%90%83%E5%AD%A3\n[4] https://m.dongqiudi.com/article/5071872.html\n[5] https://news.sina.com.cn/zx/2025-04-18/doc-inetppfi2898601.shtml\n"
 },
 {
 "role": "assistant",
 "content": "曼联在2024-2025赛季欧联杯中的表现非常出色。以下是他们最近几场欧联杯比赛的对手和结果:\n\n- 在半决赛首回合,曼联客场以3-0完胜西甲第四名的毕尔巴鄂竞技。卡塞米罗为曼联首开纪录,随后布鲁诺·费尔南德斯梅开二度,全部进球都是在上半场完成的。\n- 在四分之一决赛中,曼联对阵法国球队里昂。首回合比赛两队2-2战平。次回合在老特拉福德球场进行的比赛异常激烈,经过常规时间和加时赛的鏖战,曼联最终以5-4战胜里昂,总比分7-6晋级半决赛。\n\n曼联在本赛季欧联杯中保持着13场不败的强势表现,与他们在英超联赛中的低迷形成了鲜明对比。他们已经一只脚踏入欧联杯决赛,决赛将在本月下旬在毕尔巴鄂的圣马梅斯球场举行。"
 }

使用云函数包装mcp server

像上面这样,它需要依赖机器安装相关的服务,换一台机器就歇菜了,并且为了转换为sse还是比较麻烦。前阵子在试用阿里云百炼平台时,发现有个有意思的扩展,对于它不支持的mcp server,可以通过云函数来自定义扩展它。我想何不借助于它来包装一下mcp server,这样就可以在任何地方使用它了。

我们可以打开https://cap.console.aliyun.com/explore?lang=MCP+Server,这里以高德地图为例,我们只要在云端部署它之后。 使用云函数部署MCP server 本地只需要一个url(填上面公网访问地址)即可使用这个mcp server了,它自动提供了sse接口。至此,你居然就将mcp server “上云”了:)审计和日志也一应俱全呢。现在你可以试一下再和小智聊天,对于地图信息,它比我们还清楚了。小声的说,地图API都提供了天气查询能力,咱也不用再找一个天气服务了呢。

尾声

这个机器人应该还有不少待开发的东西,无论软件还是硬件上。比如如果能再添加几个外设,预期会更有意思了。它本身也支持如Home Assistant等智能家居平台,可以实现如开关灯、开关窗帘等操作。 技术上也有不少地方可以进一步聊聊,比如MCP的使用姿势问题等,以及如何进一步提升对话的响应速度等。 记录一张图留念,本想放视频的,第一不好录制,第二声音这东西,你懂的。

记录一张图留念

考虑到篇幅问题,今天就到这里吧。如果你对某些地方感兴趣,欢迎留言讨论。如果你没什么想说的,点赞、分享、关注,都是对我最好的鼓励。我努力创作有价值的内容,期待未来与你再会。

我是个爱折腾技术的工程师,也乐于分享。欢迎点赞、关注、分享,更欢迎一起探讨技术问题,共同学习,共同进步。为了获得更及时的文章推送,欢迎关注我的公众号:

扫码关注公众号

PaintBot Hub: 一站式AI绘画平台大集成

2025-04-24 08:00:00

Featured image of post PaintBot Hub: 一站式AI绘画平台大集成

背景

在ChatGPT出现后,聊天相关的开源工具多如牛毛,但是一直以来没找到一个能集成多平台绘图能力的开源项目。有时为了给博客文章配一个图,辗转于多个平台,这也太难受了。于是闲暇时间我在lovable.dev平台上搭建了一个集成式网页。

今天恰逢强大到令人瞠目的gpt-image-1模型发布,我想是时候把这个小小项目给介绍一下,方便大家填上API就可以直接使用。使用它你不需要安装任何软件,即用即走,我保证。但你不妨点个赞再走,我感谢:)

功能介绍

这个叫PaintBot Hub的项目,旨在提供一个统一的操作界面,集成多个AI绘画平台的API,让你的创意更加触手可及。当前算是个小Demo,但未来会持续添加更多平台,并增加图片编辑、图生图、提示词辅助等功能。

我们看一下它长得什么样:

PaintBot Hub

​还是挺简洁的是不?当前支持的AI平台:

  • OpenAI 文生图
    • GPT-Image-1 (高/中/低质量)
    • DALL·E 3 (HD/标准质量)
    • DALL·E 2
  • 智谱AI CogView
    • CogView-4-250304
    • CogView-4
    • CogView-3-Flash
    • CogView-3
  • 百度千帆
    • irag-1.0
    • flux.1-schnell
  • 火山引擎豆包(开发中)
  • 阿里云通义万相(卡壳中)
  • 腾讯云文生图(计划中)

核心功能

  • 多平台支持:目前已集成OpenAI、智谱AI CogView、百度千帆等主流AI绘画平台。
  • 文本生成图片:支持中英文等多语言提示词。提示词如果有长度限制,下面会提示你用量。
  • 自定义图片尺寸和比例:支持多种常用比例。如果模型有限制,也会温馨提示你。
  • 批量生成:一次可生成最多6张图片。随意挑选吧,也要注意成本哟。
  • 历史记录:查看和复用历史生成记录。你可以跳回去修改提示词,或者回顾一下过去的创作。

开箱即用

你可以直接使用在线版本,只需要配置自己的API密钥(安全存储在本地浏览器中),即可立即开始创作。

访问链接:https://paintbot-hub.lovable.app/

为了解决使用者的后顾之忧,所有的交互都在你本地浏览器完成的,数据不会上传到任何地方。当然你的文生图提供商可能是知道的:) 你需要填上自己使用对应平台的API密钥等信息,这只能你自己去申请了。抱歉,这个我帮不了你呢:)

好消息是智谱AI CogView3模型生图还免费呢,不妨一试,更好一点的模型就要收钱啦。作为用户,每张图大概多少钱,UI中都给你标注了,作为参考咯。我可是太贴心了,会不会是我成本意识太强,说人话(太抠了哇)。

一键部署你自己的实例

或许访问上面链接还不够快,又或者你更喜欢折腾可控,项目提供了私有化部署方案。私有化部署版本也提供了额外的好处:

  • 通过环境变量预设API密钥,用户无需手动输入
  • 后续计划增加鉴权功能,支持多用户管理和访问控制

使用Docker快速部署

你只需要在本地安装Docker,然后执行以下命令:

1
docker run -d -p 8080:8080 kevinlin86/paintbot-hub:latest

接着打开浏览器访问 http://localhost:8080 即可。

使用docker-compose

你可以本地编辑一个这样的文件,或者直接打开Github代码仓库中的docker-compose.yml文件并复制过来。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
version: '3'

services:
 paintbot:
 image: kevinlin86/paintbot-hub:latest
 container_name: paintbot
 env_file:
 - .env
 ports:
 - "${PORT:-8080}:8080"
 restart: unless-stopped

然后执行命令docker-compose up -d,接着打开浏览器访问 http://localhost:8080 即可。 更多的使用以及未来可能的更新,可以参考Github项目

后记

PaintBot Hub是一个开源项目,欢迎大家参与共建,让这个工具变得更加强大和完善。如果你有好用的类似工具,不妨推荐给我,感谢啦~

这个项目最开始是基于lovable.dev平台创建和开发的,不过随着功能变多,似乎lovable不容易驾驭,我都生气了,仿佛会员白充了,那些吹这个平台有多厉害的人,你们到底有没有深度用过啊(假装发怒)。最后更多的功能还是在Cursor指导下开发完成。但不得不说平台提供免费的托管和一键的发布,这还是很方便的。上面公开的网站就是平台托管的,我没有额外成本付出。

最后,因为个人时间和精力有限,这个项目当前只支持了三四个平台,未来希望能将主流平台都集成进来。

  • 如果你是个开发者,有兴趣的话期待你也参与贡献。我考虑在UI中放个贡献者列表,你的名字将会出现在那里。
  • 如果你是个使用者,祝你玩得愉快,有任何意见都可以在Github提issue。

GitHub项目源码,千万别给我Star哦。啊哈~~~

我是个爱折腾技术的工程师,也乐于分享。欢迎点赞、关注、分享,更欢迎一起探讨技术问题,共同学习,共同进步。为了获得更及时的文章推送,欢迎关注我的公众号:

扫码关注公众号