从想法到实践:在无序的生活里,试图用代码敲出一点秩序
<blockquote><p>该内容由 RSS 渲染生成,最佳阅读体验请前往:<a href="https://blog.grtsinry43.com/posts/from-think-to-code-in-2025">https://blog.grtsinry43.com/posts/from-think-to-code-in-2025</a></p></blockquote><blockquote>
<p>其实标题应该是 于痛苦中和解,或者说,只是为了让自己别停下来罢了</p>
</blockquote>
<p>回头看了一眼,距离无法正常行动到现在,已经过去三个月了,直到月底,我也不知道我还能不能正常行走。</p>
<p>:::link-card{href="/moments/2025/09/16/some-dark-days" title="命运开了个无情的玩笑" desc="人生如戏,跌宕起伏间尽是沧桑;命运弄人,笑泪交织中暗藏微光。在伤痛中觉醒,于绝望里寻觅重生。" newtab="true"}</p>
<p>:::</p>
<p>那段时间的生活,怎么说呢,确实是烂透了。直到现在,我也依然无法摆脱每日的痛苦,夜里依然常常失眠,从那天开始就噩梦不断</p>
<p>但人嘛,总不能真就在泥坑里躺平了。</p>
<p>这三个月,虽然腿脚不便,但我强迫自己的脑子动起来,毕竟生活,时间都在继续,学期结束越来越近,秋招也越来越近了。既然生活里全是不可控的 Exception,那我至少要在 IDE 里找回一点能跑通的逻辑。这三个月折腾了一堆东西,但是始终没有精力,也没有心情打磨一个好的产品,之前好多计划的,还有合作的项目,都被我无奈 delay 了。</p>
<p>这篇文章可能很需要 AI 总结来导读,又长又流水帐,可能我现在没有力气来慢慢打磨了。</p>
<h3>PureFlow:先试试手还在不在</h3>
<p>最开始是 <strong>PureFlow</strong>(这个之前水过文章了,就不细说了)。</p>
<p>其实当时写这个没别的想法,就是学了 kmp 始终没写过啥成型的东西嘛,正好出不了门,在学校全天狠狠写了一周多,然后终于证明跨平台没那么简单,之后慢慢学,然后填坑吧。</p>
<p><a href="https://github.com/grtsinry43/PureFlow">PureFlow</a></p>
<h3>Tangyuan 社区:帮别人点缀,顺便治愈自己</h3>
<p>那时候自己其实挺迷茫的,也没什么好的 idea。不过恰逢我负责的学生部门招新,遇到了@XianlitiCN 同学。他有一群志同道合的伙伴,有共同的爱好,还是文科相关专业。</p>
<p><a href="https://qingshuige.ink/">清水阁</a></p>
<p><a href="https://qingshuige.ink/archives/1794">线粒体同学的帖子,也是我为是什么想帮助他</a></p>
<p>我想到我小时候很喜欢文学,当时还去过什么汉字听写大会的市级海选,还很喜欢诗词大会。...然后发现风花雪月并给不了我生活的底气,所以还是选择作为爱好了。并且我一直以来还有一个想要维护一个社区的想法...<del>(你怎么恰好这么多想法)</del> 线粒体同学一直想做 <strong>Tangyuan 社区</strong>,而旧版用命令式写的 UI 有点过时并且不好维护,我想着,行吧,既然我自己也是一团乱麻,不如帮别人把想法落地。</p>
<p>其实核心就是用 Compose 构建 UI,通过 ViewModel 组织数据并传递到 UI 线程</p>
<ul>
<li><strong>UI 层:声明式构建 (Jetpack Compose)</strong>
摒弃了传统的 View/XML 体系,全线采用 Compose 构建界面。作为声明式 UI 框架,Compose 允许通过 Kotlin 代码直接描述界面状态。这种“Code as UI”的方式,极大地减少了 findViewById 和手动操作 View 状态的代码量,让视图层的代码更加直观、紧凑。</li>
<li><strong>逻辑层:ViewModel 托管数据与状态</strong>
为了实现 UI 与 逻辑的解耦,这里引入了 <strong>ViewModel</strong>。所有的业务逻辑、网络请求(Retrofit)、数据清洗都严格限制在 ViewModel 内部进行。ViewModel 的生命周期感知特性,确保了数据在配置更改时不会丢失。</li>
<li><strong>数据流:从 ViewModel 到 UI 线程的单向传递</strong>
这是整个架构中最关键的一环。
<ol>
<li><strong>数据获取</strong>:ViewModel 利用 viewModelScope 启动协程,在 IO 线程进行耗时的网络或数据库操作。</li>
<li><strong>状态暴露</strong>:将处理后的结果封装在 StateFlow 或 LiveData 中,作为一个可观察的单一数据源(SSOT)。</li>
<li><strong>UI 渲染</strong>:在 Compose 界面中,通过 collectAsState() 监听数据流。一旦数据发生变化,Compose 会自动在 <strong>主线程(UI Thread)</strong> 触发重组(Recomposition),刷新界面。</li>
</ol>
</li>
</ul>
<p>具体的项目结构是这样的:</p>
<pre><code class="language-bash">❯ tree -I build .
.
├── build.gradle.kts
├── proguard-rules.pro
├── release // 编译产物
└── src
├── androidTest // 测试
├── main
│ ├── AndroidManifest.xml // Manifest
│ ├── java
│ │ └── com
│ │ └── qingshuige
│ │ └── tangyuan
│ │ ├── analytics // 分析上报
│ │ ├── api
│ │ │ └── ApiInterface.kt
│ │ ├── App.kt
│ │ ├── di // 依赖注入
│ │ │ ├── NetworkModule.kt
│ │ │ └── RepositoryModule.kt
│ │ ├── MainActivity.kt
│ │ ├── model // 数据模型
│ │ │ ├── Category.kt
│ │ │ ├── CommentCard.kt
│ │ │ └── ...
│ │ ├── navigation // 导航栈
│ │ │ └── Screen.kt
│ │ ├── network // 网络相关
│ │ │ ├── JwtAuthenticator.kt
│ │ │ ├── JwtInterceptor.kt
│ │ │ ├── NetworkClient.kt
│ │ │ └── TokenManager.kt
│ │ ├── repository // 数据操作封装
│ │ │ ├── CategoryRepository.kt
│ │ │ ├── CommentRepository.kt
│ │ │ └── ...
│ │ ├── TangyuanApplication.kt
│ │ ├── ui
│ │ │ ├── animation // 动画配置
│ │ │ │ ├── AnimationConfig.kt
│ │ │ │ ├── ImagePreloader.kt
│ │ │ │ └── SmartSharedElementManager.kt
│ │ │ ├── components // 复用组件
│ │ │ │ ├── AuroraBackground.kt
│ │ │ │ ├── BottomBar.kt
│ │ │ │ └── ...
│ │ │ ├── screens // ui屏幕
│ │ │ │ ├── AboutScreen.kt
│ │ │ │ ├── CategoryScreen.kt
│ │ │ │ └── ...
│ │ │ └── theme // 主题和设计系统
│ │ │ ├── Color.kt
│ │ │ ├── Theme.kt
│ │ │ └── Type.kt
│ │ ├── utils // 工具类
│ │ │ ├── DeviceIdentifier.kt
│ │ │ ├── FlowExtensions.kt
│ │ │ └── ...
│ │ └── viewmodel // 视图数据绑定
│ │ ├── CategoryViewModel.kt
│ │ ├── CommentViewModel.kt
│ │ └── ...
│ └── res // 资源等等
</code></pre>
<p>看着大家在里面发帖交流,那种“被需要”的感觉,在当时真的是一剂良药。虽然现在回看代码可能还是堆了不少 <!-- raw HTML omitted --> 新鲜热乎的屎山 <!-- raw HTML omitted -->,但至少它跑起来了,还挺像模像样的。</p>
<h3>AI 原型生成器:稍微膨胀了一下的野心,折腾的开始</h3>
<p>到这个时候就是国庆假期了,身体稍微恢复一点,想搞什么东西的想法又上来了。这个 <code>ai-proto-generator</code> 还是有点东西可以讲的。这里我选择了 nextjs ktor 来构建这个项目,我们生态内有一个好用的工具:<a href="https://start.ktor.io/p/koog">Koog</a></p>
<p>我们可以去看一下市面上的这种 ai 生成工具,原理就是通过对话,toolcall,在右侧打开一个 iframe,将远程开发服务器的网页传回来,然后通过命令不断更改即可看到效果。</p>
<p>首先是,为了构建一个项目,agent 需要在一个开发目录运行开发服务器,比如 <code>nextjs</code> <code>vite</code> 等,然后输出代码,我们可以为它提供比如 <code>websearch </code> <code>shell</code> <code>read</code> <code>write</code> 工具,为了我们环境的绝对隔离,这里最好的方法是使用容器技术,这里我为了方便使用了抽象程度最高的 docker(其实可以用低一层的 containerd,更轻量一些)。</p>
<p>为了方便管理,我们可以使用 go 写一个单独管理容器的工具(用 go 是因为 docker 所在的生态还是 go 最方便,我 kt 搞了半天都是很麻烦),导入包直接开干</p>
<pre><code class="language-go">package handler
import (
// ...其他依赖
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
)
// SandboxHandler 结构体,持有 Docker 客户端
type SandboxHandler struct {
DockerClient *client.Client
}
type CodeInjectionPayload struct {
Filename string `json:"filename" binding:"required"`
Content string `json:"content" binding:"required"`
}
// 构造函数,方便 gin 那边用
func NewSandboxHandler(dockerClient *client.Client) *SandboxHandler {
return &SandboxHandler{DockerClient: dockerClient}
}
func (h *SandboxHandler) CreateSandbox(c *gin.Context) {
// 1. 绑定 JSON 数据
var payload CodeInjectionPayload
if err := c.ShouldBindJSON(&payload); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request payload: " + err.Error()})
return
}
ctx := context.Background()
containerName := "sandbox-" + generateRandomId() // 生成短随机容器名
config := &container.Config{
Image: "sandbox-template:latest",
Cmd: []string{"tail", "-f", "/dev/null"},
User: "appuser",
Labels: map[string]string{
"traefik.enable": "true",
"traefik.http.routers." + containerName + ".rule": "Host(`" + containerName + ".sandbox.localhost`)",
"traefik.http.services." + containerName + ".loadbalancer.server.port": "3000",
},
}
hostConfig := &container.HostConfig{
AutoRemove: true,
Resources: container.Resources{
Memory: 512 * 1024 * 1024,
CPUShares: 512,
},
}
// 指定网络接入
networkingConfig := &network.NetworkingConfig{
EndpointsConfig: map[string]*network.EndpointSettings{
"sandbox-manager_sandbox-net": {},
},
}
// 2. 创建并启动容器
resp, err := h.DockerClient.ContainerCreate(ctx, config, hostConfig, networkingConfig, nil, containerName)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建容器失败: " + err.Error()})
return
}
if err := h.DockerClient.ContainerStart(ctx, resp.ID, container.StartOptions{}); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "启动容器失败: " + err.Error()})
return
}
// 3. 创建 Tar 存档(为了方便一次性放入初始文件)
tarReader, err := createTarArchive(payload.Filename, payload.Content)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create tar archive: " + err.Error()})
return
}
// 4. 复制文件到容器的工作目录
if err := h.DockerClient.CopyToContainer(ctx, resp.ID, "/home/appuser/project", tarReader, container.CopyToContainerOptions{}); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to copy code to container: " + err.Error()})
return
}
// 5. 在容器中执行 pnpm run dev
execConfig := container.ExecOptions{
Cmd: []string{"pnpm", "run", "dev"},
WorkingDir: "/home/appuser/project",
AttachStdout: true,
AttachStderr: true,
}
execResp, err := h.DockerClient.ContainerExecCreate(ctx, resp.ID, execConfig)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create exec: " + err.Error()})
return
}
if err := h.DockerClient.ContainerExecStart(ctx, execResp.ID, container.ExecStartOptions{Detach: true}); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to start exec: " + err.Error()})
return
}
log.Printf("成功创建、启动容器并注入代码,已执行 pnpm run dev %s", resp.ID[:12])
sandboxURL := "http://" + containerName + ".sandbox.localhost"
c.JSON(http.StatusOK, gin.H{
"message": "Sandbox created and code injected successfully",
"containerId": resp.ID,
"url": sandboxURL,
})
}
func (h *SandboxHandler) ListSandboxes(c *gin.Context) {
containers, err := h.DockerClient.ContainerList(context.Background(), container.ListOptions{})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "列出容器失败: " + err.Error()})
return
}
type simpleContainer struct {
ID string
Image string
Status string
}
var result []simpleContainer
for _, c := range containers {
result = append(result, simpleContainer{
ID: c.ID[:12],
Image: c.Image,
Status: c.Status,
})
}
if result == nil {
result = []simpleContainer{}
}
c.JSON(http.StatusOK, result)
}
func (h *SandboxHandler) DeleteSandbox(c *gin.Context) {
// 从 URL 路径中获取容器 ID
containerID := c.Param("id")
ctx := context.Background()
log.Printf("Attempting to stop and remove container %s", containerID)
// 1. 停止容器
// 第三个参数可以设置超时时间,nil 表示使用默认超时
if err := h.DockerClient.ContainerStop(ctx, containerID, container.StopOptions{}); err != nil {
// 如果容器已经不存在,Docker 会报错,我们需要优雅地处理
if strings.Contains(err.Error(), "No such container") {
c.JSON(http.StatusNotFound, gin.H{"error": "Container not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to stop container: " + err.Error()})
return
}
// 2. 移除容器
// 因为我们在创建时使用了 --rm (AutoRemove: true),
// 所以容器在停止后会自动被删除。这一步严格来说不是必须的,
// 但是为了兜底,这里可以放一下
err := h.DockerClient.ContainerRemove(ctx, containerID, container.RemoveOptions{Force: true})
if err != nil {
if strings.Contains(err.Error(), "No such container") {
c.JSON(http.StatusNotFound, gin.H{"error": "Container not found"})
return
}
}
log.Printf("Successfully stopped container %s", containerID)
c.JSON(http.StatusOK, gin.H{"message": "Sandbox deleted successfully"})
}
func createTarArchive(filename, content string) (io.Reader, error) {
buf := new(bytes.Buffer)
tw := tar.NewWriter(buf)
hdr := &tar.Header{
Name: filename,
Mode: 0644,
Size: int64(len(content)),
}
if err := tw.WriteHeader(hdr); err != nil {
return nil, err
}
if _, err := tw.Write([]byte(content)); err != nil {
return nil, err
}
if err := tw.Close(); err != nil {
return nil, err
}
return buf, nil
}
// 一个简单的随机 ID 生成函数
func generateRandomId() string {
const letters = "abcdefghijklmnopqrstuvwxyz0123456789"
b := make([]byte, 8)
r := rand.New(rand.NewSource(time.Now().UnixNano()))
for i := range b {
b[i] = letters[r.Intn(len(letters))]
}
return string(b)
}
</code></pre>
<p>太长不看版:就是我们实现了一个快速创建容器的抽象</p>
<pre><code class="language-go">package handler
func (h *SandboxHandler) CreateSandbox(c *gin.Context) {
}
func (h *SandboxHandler) ListSandboxes(c *gin.Context) {
}
func (h *SandboxHandler) DeleteSandbox(c *gin.Context) {
}
</code></pre>
<p>而后我们可以让主服务去调用这个啦,就像这样:</p>
<pre><code class="language-bash">┌─────────────────┐ HTTP ┌─────────────────┐ gRPC ┌─────────────────┐
│ │ Request │ │ Call │ │
│ Frontend │ ──────────→ │ Ktor Backend │ ──────────→ │ Go Container │
│ (Next.js) │ │ (Business) │ │ Manager │
│ │ │ │ │ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ SandboxService │ │ Docker Container│
│ (Code Gen AI) │ │ (Isolated) │
└─────────────────┘ └─────────────────┘
</code></pre>
<p>为了管理 docker 的流量转到前端,我们引入 traefik,这里就不多赘述了。待到写好基本的后端结构,然后我们便可以使用 koog 来顺畅调用 llm 的 api 了。</p>
<p>市面上的对话使用 sse 来实现流式输出,然后后端维护对话上下文,并且通过提取回应的字符串来实现 toolcall(也就是 MCP),借用 koog,我们可以方便的流式调用</p>
<pre><code class="language-kotlin"> // 执行LLM流式调用
val llmResponse = StringBuilder()
val flow = llm().executeStreaming(chatPrompt, GoogleModels.Gemini2_0FlashLite)
flow.collect { chunk ->
llmResponse.append(chunk)
writeSseData("token", mapOf("token" to chunk))
}
val fullResponse = llmResponse.toString().trim()
logger.info("LLM response: $fullResponse")
// 检查是否包含函数调用
val functionCalls = extractFunctionCalls(fullResponse)
</code></pre>
<p>在之前,我们写代码是为了人,基建调用,而这里我们写的都是为 llm 服务:我们可以维护一个工具集合,方便注册,根据项目切换,还有管理</p>
<pre><code class="language-kotlin">package com.grtsinry43.ai
import kotlinx.serialization.json.JsonObject
import org.slf4j.LoggerFactory
import java.util.concurrent.ConcurrentHashMap
/**
* 默认的工具注册表实现
*/
class DefaultToolRegistry : ToolRegistry {
private val logger = LoggerFactory.getLogger(DefaultToolRegistry::class.java)
private val tools = ConcurrentHashMap<String, FunctionTool>()
override fun register(tool: FunctionTool) {
tools[tool.name] = tool
logger.info("Registered tool: ${tool.name}")
}
override fun unregister(name: String) {
tools.remove(name)
logger.info("Unregistered tool: $name")
}
override fun getTool(name: String): FunctionTool? {
return tools[name]
}
override fun getAllTools(): List<FunctionTool> {
return tools.values.toList()
}
override fun getToolDefinitions(): List<JsonObject> {
return tools.values.map { tool ->
JsonObject(mapOf(
"type" to kotlinx.serialization.json.JsonPrimitive("function"),
"function" to JsonObject(mapOf(
"name" to kotlinx.serialization.json.JsonPrimitive(tool.name),
"description" to kotlinx.serialization.json.JsonPrimitive(tool.description),
"parameters" to tool.parameters
))
))
}
}
}
</code></pre>
<p>随后处理对话中的工具调用相关,来拿到我们想要的工具调用</p>
<pre><code class="language-kotlin"> // 从响应中提取函数调用
fun extractFunctionCalls(response: String): List<FunctionCall> {
val json = Json { ignoreUnknownKeys = true }
return try {
val jsonResponse = json.parseToJsonElement(response).jsonObject
val functionCallsArray = jsonResponse["function_calls"]?.jsonArray ?: return emptyList()
functionCallsArray.mapNotNull { element ->
try {
val callObj = element.jsonObject
val name = callObj["name"]?.jsonPrimitive?.content ?: return@mapNotNull null
val arguments = callObj["arguments"]?.jsonObject ?: JsonObject(emptyMap())
FunctionCall(name, arguments)
} catch (e: Exception) {
logger.warn("Failed to parse function call: $element", e)
null
}
}
} catch (e: Exception) {
// 尝试从文本中提取JSON块
val jsonPattern = Regex("""``` json\s *(\{.*?\})\s*```""", RegexOption.DOT_MATCHES_ALL)
val matches = jsonPattern.findAll(response)
matches.mapNotNull { match ->
try {
val jsonText = match.groupValues [1]
extractFunctionCalls(jsonText)
} catch (e: Exception) {
emptyList()
}
}.flatten().toList()
}
}
</code></pre>
<p>有了工具调用,接下来就是编写大量的工具集,然后<strong>不要一次性塞给AI</strong>,因为选择工具经常出现问题,我们需要的是根据项目类型自动推荐,然后分好类,比如我们这里有的 <code>websearch </code> <code>shell</code> <code>read</code> <code>write</code> 等等。</p>
<p>可惜理想是美好的,现实是残酷的,想实现这些效果,需要付出高昂的 tokens 成本,在 claude 小号被封之后,我的项目就搁置了,如果你恰巧财力雄厚,等我完善完我就开源出去可以调 api 慢慢玩。</p>
<h3>Github Overview & UI 的“滑铁卢”</h3>
<p>进入10月中旬,生活开始多线运行,虽然身体不行,但是空闲时间反而越来越少了,我开始转向轻量项目。在钱包受伤之后,紧接着,只能玩一玩比如api这种现成的,于是方向转向了 <strong>Github 仓库分析工具 (Overview)</strong>。</p>
<p>这里我首次用fastify写大项目,也是首次尝试cc接管一切,配置好提示词,eslint规则,并且设置commit钩子,这种强制执行的限制对于llm还是挺有用的。</p>
<p>后端逻辑写得飞起,数据抓取也没问题。结果到了前端展示环节,<strong>UI 设计彻底把我整不会了</strong>。</p>
<p>我是真的尽力了,但画出来的界面怎么看怎么丑,那种“脑子里有画面但手残画不出来”的挫败感,真的让人想砸键盘。这就好比当时的我,里子虽然还在,但面子上已经挂不住了。最后这个项目只能含泪鸽置,<!-- raw HTML omitted --> 实在太丑了没眼看 <!-- raw HTML omitted -->。</p>
<p>不过最近Gemini 3 Pro 让我燃起了希望啊,这个可能近期我会写完。</p>
<p>总结了一个文档,希望能帮到你,如果你也在写相关的:</p>
<p><a href="https://github.com/grtsinry43/proj-dash-backend/blob/main/GITHUB_API_FEASIBILITY.md">https://github.com/grtsinry43/proj-dash-backend/blob/main/GITHUB_API_FEASIBILITY.md</a></p>
<h3>ELK 日志系统:既然脸不要了,那就搞内脏</h3>
<p>在 UI 上碰得满头包之后,我产生了逆反心理:<strong>行,既然我画不好皮,那我就去搞最底层、最枯燥的后端基建。</strong></p>
<p>于是我开始折腾 <strong>ELK (Elasticsearch, Logstash, Kibana)</strong> 日志系统。</p>
<p>这是一个相当“重”的项目,<del>主要是java太吃内存了</del>。两天时间配完,搓完bff,看着成千上万条杂乱无章的日志被 Logstash 吞进去,然后整整齐齐地吐出来,有点治愈的哈哈哈。</p>
<p><img src="https://blog.grtsinry43.com/uploads/2025/11/23/f754f23e304d1c534d41e5a675560c8d_720.jpg_ba72dd3b-0296-4e41-a26c-a9d957d99426.jpg" alt=""></p>
<p><img src="https://blog.grtsinry43.com/uploads/2025/11/23/image.png_f379043f-b86e-458a-9a98-7b59ee8da7c4.png" alt=""></p>
<p>折腾也很简单,一个compose加上自己设计bff收集就行了,前后端都可以的。</p>
<pre><code class="language-yml">services:
elasticsearch:
image: elasticsearch: 8.11.0
container_name: elasticsearch
environment:
- discovery.type = single-node
- xpack.security.enabled = false # 开发时关闭安全验证,简化操作
- "ES_JAVA_OPTS =-Xms1g -Xmx1g" # 建议分配 1G 内存
ports:
- "9200:9200"
volumes:
- es_data:/usr/share/elasticsearch/data
logstash:
image: logstash: 8.11.0
container_name: logstash
# 将我们本地的配置文件挂载到容器里
volumes:
- ./logstash/pipeline:/usr/share/logstash/pipeline/
ports:
- "5044:5044" # 这是我们稍后要从 Spring Boot 发送日志的端口!!
depends_on:
- elasticsearch
kibana:
image: kibana: 8.11.0
container_name: kibana
ports:
- "5601:5601"
environment:
# 告诉 Kibana 去哪里找 Elasticsearch
- ELASTICSEARCH_HOSTS = http://elasticsearch: 9200
- XPACK_ENCRYPTEDSAVEDOBJECTS_ENCRYPTIONKEY = 4d1b36625bb7a2e0f8fc41c7bb9a1dbf
depends_on:
- elasticsearch
zookeeper:
image: confluentinc/cp-zookeeper: 7.5.3
container_name: zookeeper
environment:
ZOOKEEPER_CLIENT_PORT: 2181
ZOOKEEPER_TICK_TIME: 2000
kafka:
image: confluentinc/cp-kafka: 7.5.3
container_name: kafka
ports:
- "9092:9092"
depends_on:
- zookeeper
environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: 'zookeeper: 2181'
# 1. 定义两个监听器:
# - PLAINTEXT: 用于容器间通信,监听在 29092 端口
# - PLAINTEXT_HOST: 用于外部通信 (比如你的 Spring Boot 应用),监听在 9092 端口
KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:29092, PLAINTEXT_HOST://0.0.0.0:9092
# 2. 定义这两个监听器分别对外广播什么地址:
# - 如果从 PLAINTEXT 进来,就告诉对方我的地址是 kafka: 29092
# - 如果从 PLAINTEXT_HOST 进来,就告诉对方我的地址是 localhost: 9092
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka: 29092, PLAINTEXT_HOST://localhost: 9092
# 3. 将监听器名称映射到安全协议
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT: PLAINTEXT, PLAINTEXT_HOST: PLAINTEXT
# 4. 指定 Broker 之间通信使用哪个监听器
KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
volumes:
es_data: # 创建一个 Docker volume 来持久化 ES 数据
driver: local
</code></pre>
<p><img src="https://blog.grtsinry43.com/uploads/2025/11/23/mermaid-diagram-2025-11-23-011441.png_72c506a3-ad56-4571-bb7b-aaff3f3e7cdc.png" alt=""></p>
<h3>Vespera LightMonitor:回归极简,来点 Rust 哲学</h3>
<p>折腾完沉重的 ELK,再看看我手里那几台配置感人的小鸡(VPS),我又感觉自己有点好笑,这ELK根本没地方部署。</p>
<p>于是看着市面上眼花缭乱的服务器探针, <strong>Vespera LightMonitor</strong> 诞生了。</p>
<p>这个用了Axum sqlx 已经上线了,bug慢慢修,等我用了一段时间稳定就开源然后写文档。</p>
<p><a href="https://status.grtsinry43.com/">Verpera | grtsinry43's Server Monitor</a></p>
<p><img src="https://blog.grtsinry43.com/uploads/2025/11/23/image.png_6e4b8c9d-c719-4027-8766-1eb3d30a1c74.png" alt=""></p>
<h3>Design System:试图建立秩序</h3>
<p>经历了这几个月的胡搞瞎搞:从社区到 AI,从 UI 碰壁到沉迷日志后端,再到极简监控...</p>
<p>我也发现了,我做的东西太碎了。就像那个死掉的 Github Overview 一样,我每次都在重复造轮子,还在纠结圆角是 4px 还是 8px 这种无聊的问题。</p>
<p>所以,最近我在研究和创造一套属于自己的 <strong>设计系统 (Design System)</strong>。</p>
<h3>碎碎念</h3>
<p>三个月里,我的每个项目,都是挤时间,在难受的时候,在无聊的时候,在实在感觉不想继续下去的时候,就连这篇文章也一样,流水帐的就像我的生活一样,其实这里的每个项目都可以展开为一篇文章,都有很多可以讲的,但是我还是等有余力将它们打磨好一个好的产品再汇报给每一个人吧,写下这些文字也算是一种解脱,至少证明我的生活还在继续,在这个重要的节点依然在输出,当然也有更多的输入。</p>
<blockquote>
<p>回头看看这三个月,痛苦消失了吗?
害,其实也没有。深夜破防的时候该 emo 还是会 emo。</p>
</blockquote>
<p>但好在,我没停下来。
从想法到实践,这中间的距离,大概就是我与自己和解的过程吧。</p>
<p>代码还得写,生活还得过,只要键盘还在响,就不算太糟糕。希望重新健康的日子,能早一点来吧。</p>