张家口市网站建设_网站建设公司_导航易用性_seo优化
2026/1/15 17:44:35 网站建设 项目流程

「在地铁上用手机写代码」,这个念头最早是怎么蹦出来的,我已经记不清了。只记得那天加班到凌晨两点,拖着疲惫的身躯挤进末班地铁,手里还攥着一个没解决的 bug。要是这时候能掏出手机,让 AI 帮我把代码改了该多好?

于是,一个「远程驱动 AI 编程助手」的项目就这样诞生了。

听起来简单,做起来要命。

一、背景:当 AI 编程助手遇上「移动办公」

先说说痛点。

现在市面上的 AI 编程助手,无论是 Claude Code CLI、OpenAI Codex CLI,还是 GitHub Copilot CLI,都有一个共同的「硬伤」——它们都是命令行工具

这意味着什么?意味着你得有一台电脑,打开终端,敲命令。手机?平板?想都别想。

但问题是,我们这代程序员,已经被移动互联网惯坏了。微信能在手机上发消息,钉钉能在手机上审批,为什么写代码就必须坐在电脑前?

有没有一种可能,让浏览器成为 AI 编程助手的「遥控器」?

你在手机上输入需求,服务器上的 Claude Code 或 Codex 帮你执行,结果实时推送到你的屏幕上。不管你是在咖啡馆、地铁上,还是躺在沙发上——只要有浏览器,就能写代码。

这就是 WebCodeCli 要做的事情。

二、技术选型:为什么是 Blazor Server?

很多人第一反应可能是:「这不就是个 Web 终端吗?用 xterm.js 套个壳不就完了?」

我最初也是这么想的。但真正动手才发现,事情远没有那么简单。

2.1 流式输出的噩梦

AI 编程助手有一个显著特征——流式输出

它不是一次性返回结果,而是像打字机一样,一个字一个字地「敲」出来。这对用户体验至关重要:如果你发了一个需求,等 30 秒没任何反馈,你会以为程序挂了。但如果你能看到 AI 正在「思考」、正在「写代码」,就会安心很多。

问题在于,Claude Code 和 Codex 的流式输出格式完全不同。

Claude Code使用的是stream-json格式,输出类似这样:

{"type":"system","subtype":"init","session_id":"abc123","cwd":"/workspace"} {"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"我来帮你..."}]}} {"type":"tool_use","name":"Read","input":{"path":"src/main.ts"}}

Codex使用的是JSONL格式,结构又是另一套:

{"type":"thread.started","thread_id":"xyz789"} {"type":"item.started","item":{"type":"agent_message"}} {"type":"item.updated","item":{"type":"agent_message","text":"让我分析一下..."}} {"type":"turn.completed","usage":{"input_tokens":1234,"output_tokens":567}}

如果用传统的 REST API + 轮询方案,这种流式体验根本做不出来。用 WebSocket?可以,但状态管理会变得异常复杂。

最终我选择了Blazor Server

为什么?因为 Blazor Server 有一个杀手级特性——SignalR 长连接

服务端和客户端之间天然保持着一条实时通道,DOM 更新通过这条通道即时推送。这意味着我可以在服务端读取 CLI 进程的输出流,直接把结果「推」到用户浏览器上,延迟低到几乎感知不到

更爽的是,我不用自己处理 WebSocket 的连接管理、心跳检测、断线重连这些脏活累活——Blazor 全给我包了。

2.2 为什么不用 WebAssembly?

可能有人会问:「Blazor 有两种模式,为什么不用 WebAssembly?WASM 可是纯前端运行,还不用服务器!」

问题在于,这个项目的核心逻辑必须在服务端运行

想想看:Claude Code CLI 和 Codex CLI 是要安装在服务器上的,它们需要访问文件系统、需要执行命令、需要网络权限。这些事情,浏览器沙箱里的 WASM 根本做不了。

Blazor Server 正好满足我的需求:UI 在浏览器渲染,逻辑在服务端执行,两者通过 SignalR 实时同步。

说白了,浏览器只是个「皮」,真正干活的还是服务器。

三、架构设计:适配器模式的优雅与挣扎

确定技术栈后,第一个要解决的问题就是:如何统一处理不同 CLI 工具的差异?

Claude Code 和 Codex 就像两个性格迥异的人——一个喜欢用type: assistant表示回复,另一个偏要用item.type: agent_message;一个把会话 ID 叫session_id,另一个非得叫thread_id

如果每来一个新工具就写一坨 if-else,代码很快就会变成一锅粥。

于是我祭出了老朋友——适配器模式

3.1 接口设计:一个接口统一天下

首先,我定义了一个ICliToolAdapter接口:

public interface ICliToolAdapter { string[] SupportedToolIds { get; } bool SupportsStreamParsing { get; } bool CanHandle(CliToolConfig tool); string BuildArguments(CliToolConfig tool, string prompt, CliSessionContext context); CliOutputEvent? ParseOutputLine(string line); string? ExtractSessionId(CliOutputEvent outputEvent); string? ExtractAssistantMessage(CliOutputEvent outputEvent); string GetEventTitle(CliOutputEvent outputEvent); string GetEventBadgeClass(CliOutputEvent outputEvent); string GetEventBadgeLabel(CliOutputEvent outputEvent); }

看起来有点长,但每个方法都有它存在的意义:

  • BuildArguments:不同 CLI 工具的命令行参数格式不同。Claude Code 需要-p --verbose --output-format=stream-json,Codex 需要exec --json。这个方法负责「翻译」用户输入到具体命令。

  • ParseOutputLine:这是最核心的方法。每读到一行输出,就调用它把 JSON 字符串解析成统一的CliOutputEvent对象。

  • ExtractSessionId:AI 编程助手通常支持「会话恢复」功能。比如你中途断开,下次可以接着聊。但前提是你得保存住会话 ID。这个方法负责从输出中「揪」出会话 ID。

  • GetEventBadgeClass/GetEventBadgeLabel:纯粹为了 UI 显示。不同类型的事件用不同颜色标注,比如「工具调用」是蓝色,「错误」是红色。

3.2 Claude Code 适配器:细节里的魔鬼

以 Claude Code 适配器为例,来看看实际处理有多复杂。

Claude Code 的输出格式看起来规整,但实际上有好几种「方言」:

  1. 旧版格式type直接就是initmessagetool_use这些。

  2. 新版格式typesystemassistant,具体类型要看内嵌的subtypemessage.role

  3. 非 JSON 行:有时候 CLI 会吐出一些日志或错误信息,根本不是 JSON。

处理逻辑大概是这样的:

public CliOutputEvent? ParseOutputLine(string line) { var trimmed = line.Trim(); // 第一关:过滤非 JSON 行 if (!trimmed.StartsWith("{") && !trimmed.StartsWith("[")) { var isError = trimmed.StartsWith("Error:", StringComparison.OrdinalIgnoreCase); return new CliOutputEvent { EventType = isError ? "error" : "raw", IsError = isError, Title = isError ? "错误" : "输出", Content = trimmed }; } // 第二关:尝试 JSON 解析 try { using var document = JsonDocument.Parse(trimmed); var root = document.RootElement; var eventType = GetStringProperty(root, "type") ?? "unknown"; var outputEvent = new CliOutputEvent { EventType = eventType, RawJson = line }; switch (eventType) { case "init": ParseInitEvent(root, outputEvent); break; case "system": // 新版格式:检查 subtype if (root.TryGetProperty("subtype", out var subtypeEl) && subtypeEl.GetString() == "init") { outputEvent.EventType = "init"; ParseInitEvent(root, outputEvent); } else { ParseSystemEvent(root, outputEvent); } break; case "assistant": ParseAssistantOrUserEvent(root, outputEvent, isAssistant: true); break; // ... 更多 case } return outputEvent; } catch (JsonException) { // JSON 解析失败也不要慌,当普通输出处理 return new CliOutputEvent { EventType = "raw", Title = "输出", Content = trimmed }; } }

这里有个设计决策值得一提:绝不让解析失败破坏用户体验

早期版本里,我遇到解析不了的行就直接抛异常,结果整个输出流都断了。后来改成「兜底策略」——解析失败就当普通文本显示,至少用户能看到原始输出,而不是一脸懵逼对着空白屏幕。

3.3 工具调用的「待办列表」坑

还有一个让我头疼了整整两天的问题:待办列表(TodoWrite)的渲染

Claude Code 有个叫TodoWrite的工具,AI 会用它来记录任务清单。输出格式是这样的:

{ "type": "assistant", "message": { "content": [{ "type": "tool_use", "name": "TodoWrite", "input": { "todos": [ {"content": "分析需求", "status": "completed"}, {"content": "实现功能", "status": "in_progress"}, {"content": "编写测试", "status": "pending"} ] } }] } }

一开始我把它当普通的「工具调用」处理,UI 上显示的是一坨难看的 JSON。

后来专门加了一段逻辑,检测到是TodoWrite工具时,把 JSON 转成用户友好的格式:

✓ 分析需求 ◐ 实现功能 ○ 编写测试

这个细节花了不少时间,但效果立竿见影——用户终于能看懂 AI 在干什么了。

四、会话管理:IndexedDB + 防抖,小小的优化大大的提升

AI 编程助手的一个核心体验是会话连续性。你跟 AI 聊了半小时,中途刷新一下页面,之前的对话不能丢。

最直接的方案是存服务端数据库,但这样有两个问题:

  1. 读写频繁:每发一条消息就往数据库里存,对服务器压力很大。

  2. 隐私顾虑:用户可能不希望对话内容被服务器留存。

所以我选择了IndexedDB——浏览器内置的本地数据库。

4.1 Blazor 调用 IndexedDB 的「桥接」

Blazor Server 的代码跑在服务端,要操作浏览器的 IndexedDB,必须通过IJSRuntime做 JavaScript 互操作。

我在前端写了一套 IndexedDB 的封装:

window.webCliIndexedDB = { saveSession: async function(session) { const db = await openDatabase(); const tx = db.transaction('sessions', 'readwrite'); const store = tx.objectStore('sessions'); await store.put(session); return true; }, loadSessions: async function() { const db = await openDatabase(); const tx = db.transaction('sessions', 'readonly'); const store = tx.objectStore('sessions'); return await store.getAll(); }, deleteSession: async function(sessionId) { const db = await openDatabase(); const tx = db.transaction('sessions', 'readwrite'); const store = tx.objectStore('sessions'); await store.delete(sessionId); return true; } };

然后在 C# 里这样调用:

var success = await _jsRuntime.InvokeAsync<bool>("webCliIndexedDB.saveSession", session);

简单粗暴,但有效。

4.2 防抖:别让保存操作把浏览器干崩

问题来了。

AI 的流式输出是一个字一个字往外蹦的,如果每收到一点内容就存一次 IndexedDB,一条消息可能触发几十上百次写入。浏览器扛不住不说,还会严重影响渲染性能。

解决方案是防抖(Debounce)。

核心思想:收到保存请求后,不立即执行,而是等一小段时间(比如 500ms)。如果这段时间内又来了新请求,就重置计时器。只有「安静」了 500ms 后,才真正执行保存。

public Task SaveSessionAsync(SessionHistory session) { lock (_saveLock) { _hasPendingSave = true; _pendingSession = session; // 重置定时器 _saveTimer?.Dispose(); _saveTimer = new System.Threading.Timer(async _ => { await ExecuteSaveAsync(); }, null, SaveDebounceMs, Timeout.Infinite); } return Task.CompletedTask; }

这招一出,IndexedDB 写入次数直接从每秒几十次降到每秒一两次,浏览器瞬间丝滑。

4.3 存储空间的「优雅降级」

还有个细节:IndexedDB 虽然容量比 localStorage 大得多,但也不是无限的。如果用户存了太多会话,可能会触发QuotaExceededError

我的处理策略是:

  1. 限制单个会话的消息数量(上限 1000 条,超出就删除最早的)

  2. 捕获配额异常并友好提示

catch (JSException ex) when (ex.Message.Contains("QuotaExceededError")) { _logger.LogWarning(ex, "IndexedDB 空间不足"); throw new QuotaExceededException("存储空间不足,请删除一些旧会话以释放空间", ex); }

五、进程管理:一次性 vs 持久化,两种模式的抉择

接下来聊聊进程管理。

调用 CLI 工具,本质上就是启动一个子进程,把用户输入传进去,再把输出读出来。但怎么管理这个进程,大有讲究。

5.1 一次性进程模式

最简单的方案:每次用户发消息,就启动一个新进程,执行完就杀掉。

var process = new Process { StartInfo = new ProcessStartInfo { FileName = "claude", Arguments = "-p \"用户的问题\"", RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true } }; process.Start(); // 读取输出... process.WaitForExit(); process.Dispose();

优点是简单粗暴,每次都是干净的环境。

缺点也很明显:启动开销。每次启动 Claude Code CLI,它都要加载配置、初始化 MCP 服务器、连接 API……这套流程走下来,可能要好几秒。用户体验极差。

5.2 持久化进程模式

更聪明的做法是复用进程

进程启动后不杀掉,保持在后台运行。每次有新消息,就通过标准输入「喂」进去,然后读取标准输出。这样启动开销只有第一次,后续交互都是毫秒级。

但这带来了新的挑战:

  1. 进程生命周期管理:怎么知道进程还活着?挂了怎么办?

  2. 并发控制:多个用户同时使用,进程怎么隔离?

  3. 输出边界判断:一次性进程可以等WaitForExit(),持久化进程怎么知道「这轮回答结束了」?

我的方案是用一个PersistentProcessManager来统一管理:

public class PersistentProcessManager { private readonly ConcurrentDictionary<string, PersistentProcessInfo> _processes = new(); public PersistentProcessInfo GetOrCreateProcess( string sessionId, string toolId, CliToolConfig tool, string workingDirectory) { var key = $"{sessionId}_{toolId}"; return _processes.GetOrAdd(key, _ => { // 启动新进程 var process = StartProcess(tool, workingDirectory); return new PersistentProcessInfo { Process = process, SessionId = sessionId, ToolId = toolId }; }); } }

输出边界判断用的是「超时检测」:如果连续 2 秒没有新输出,就认为这轮回答结束了。

var noOutputTimeout = TimeSpan.FromSeconds(2); while (!cancellationToken.IsCancellationRequested) { bool hasNewOutput = false; if (outputReader.Peek() >= 0) { int bytesRead = await outputReader.ReadAsync(buffer); if (bytesRead > 0) { hasNewOutput = true; lastOutputTime = DateTime.UtcNow; yield return new StreamOutputChunk { Content = new string(buffer, 0, bytesRead) }; } } if (!hasNewOutput && (DateTime.UtcNow - lastOutputTime) > noOutputTimeout) { // 超时,认为输出结束 break; } await Task.Delay(50, cancellationToken); }

这个 2 秒的阈值是反复调优的结果——太短会误判(AI 思考中间可能停顿一下),太长用户等得难受。

六、会话恢复:让 AI 记住「上次聊到哪儿了」

AI 编程助手一个很爽的功能是「会话恢复」——你可以告诉它「继续上次的工作」,它就能接着之前的上下文继续执行。

但这需要保存「会话 ID」。Claude Code 叫session_id,Codex 叫thread_id,本质上是同一个东西。

难点在于:这个 ID 是 CLI 工具在运行时动态生成的,你得从输出流里「捞」出来。

我的做法是:

  1. 适配器在解析输出时,遇到包含会话 ID 的事件就提取出来

  2. 执行服务把提取到的 ID 存起来

  3. 下次执行时,把 ID 传给适配器,让它拼接到命令行参数里

// 适配器构建命令时,检查是否有会话 ID public string BuildArguments(CliToolConfig tool, string prompt, CliSessionContext context) { var argsBuilder = new StringBuilder(); argsBuilder.Append("-p --verbose --output-format=stream-json "); // 会话恢复参数 if (context.IsResume && !string.IsNullOrEmpty(context.CliThreadId)) { argsBuilder.Append($"--resume {context.CliThreadId} "); } argsBuilder.Append($"\"{escapedPrompt}\""); return argsBuilder.ToString(); }
// 执行服务保存会话 ID if (hasAdapter && string.IsNullOrEmpty(cliThreadId)) { var output = fullOutput.ToString(); var parsedThreadId = ParseCliThreadId(output, adapter); if (!string.IsNullOrEmpty(parsedThreadId)) { SetCliThreadId(sessionId, parsedThreadId); } }

这套机制跑通后,用户终于可以跨多次交互保持上下文了。比如让 AI 先写一个函数,然后再让它加个测试——AI 知道你说的是哪个函数。

七、移动端适配:44px 的触摸区域有多重要

说了这么多后端,来聊聊前端。

既然目标是「手机也能写代码」,移动端适配就是重中之重。

7.1 响应式布局

桌面端是左右分栏布局:左边是对话区,右边是预览区。

但手机屏幕那么窄,左右分栏根本不现实。我改成了上下布局,并加了一个「折叠预览区」的按钮:

<button @onclick="TogglePreviewPanel" class="lg:hidden fixed top-1/2 right-2 -translate-y-1/2 z-50 w-10 h-10 bg-gray-800 text-white rounded-full"> @if (_isPreviewCollapsed) { <span>▲</span> } else { <span>▼</span> } </button>

lg:hidden意味着这个按钮只在小屏幕上显示,大屏幕上自动隐藏。

7.2 触摸优化

移动端有个很容易被忽视的细节:手指比鼠标指针粗太多了

Apple 的人机界面指南建议,触摸目标至少要 44x44 像素。我最初没在意,结果测试时发现按钮根本点不准。

后来统一给交互元素加上了最小尺寸:

.min-h-[44px] .min-w-[44px]

还加了触摸反馈:

.active:scale-95 /* 按下时轻微缩小 */ .active:bg-gray-200 /* 按下时变色 */

7.3 虚拟键盘的坑

iOS Safari 有个臭名昭著的问题:虚拟键盘弹出时,视口高度会变化,但100vh还是按原来的高度算,导致页面布局乱掉。

解决方案是用 CSS 自定义属性动态更新视口高度:

function updateViewportHeight() { const vh = window.innerHeight * 0.01; document.documentElement.style.setProperty('--vh', `${vh}px`); } window.addEventListener('resize', updateViewportHeight);

然后在 CSS 里用calc(var(--vh, 1vh) * 100)代替100vh

八、工作区隔离:每个会话一个「沙盒」

AI 编程助手会生成文件、执行命令,必须做好隔离,不能让不同用户的文件混在一起。

我的方案是:每个会话一个独立的工作目录

private string GetOrCreateSessionWorkspace(string sessionId) { lock (_workspaceLock) { if (_sessionWorkspaces.TryGetValue(sessionId, out var existingWorkspace)) { return existingWorkspace; } var workspacePath = Path.Combine(workspaceRoot, sessionId); if (!Directory.Exists(workspacePath)) { Directory.CreateDirectory(workspacePath); } _sessionWorkspaces[sessionId] = workspacePath; // 创建标记文件,记录创建时间 var markerFile = Path.Combine(workspacePath, ".workspace_info"); File.WriteAllText(markerFile, $"Created: {DateTime.UtcNow:O}\nSessionId: {sessionId}"); return workspacePath; } }

启动 CLI 进程时,把工作目录设成这个隔离目录:

startInfo.WorkingDirectory = sessionWorkspace;

这样 AI 生成的文件都在各自的目录里,互不干扰。

8.1 过期清理

长期运行后,工作区目录会越积越多,磁盘迟早撑爆。

我加了一个定时清理的后台服务,默认 24 小时没访问的工作区自动删除:

public void CleanupExpiredWorkspaces() { var expirationTime = DateTime.UtcNow.AddHours(-_options.WorkspaceExpirationHours); var directories = Directory.GetDirectories(workspaceRoot); foreach (var dir in directories) { var markerFile = Path.Combine(dir, ".workspace_info"); var lastAccessTime = File.Exists(markerFile) ? File.GetLastWriteTimeUtc(markerFile) : Directory.GetLastWriteTimeUtc(dir); if (lastAccessTime < expirationTime) { Directory.Delete(dir, recursive: true); } } }

8.2 安全边界

另一个必须考虑的是路径穿越攻击

如果用户构造一个类似../../../etc/passwd的路径,可能会读到不该读的文件。

所有涉及文件操作的地方,我都加了路径校验:

var normalizedWorkspace = Path.GetFullPath(workspacePath); var normalizedFile = Path.GetFullPath(fullPath); if (!normalizedFile.StartsWith(normalizedWorkspace)) { _logger.LogWarning("尝试访问工作区外的文件: {File}", relativePath); return null; }

九、Markdown 渲染与代码高亮

AI 的回复里经常包含 Markdown 格式的内容,直接显示原始文本太丑了。

我用的是Markdig,一个高性能的 .NET Markdown 解析库:

private static readonly MarkdownPipeline _outputMarkdownPipeline = new MarkdownPipelineBuilder() .UseAdvancedExtensions() .DisableHtml() // 禁用原始 HTML,防止 XSS .Build(); private MarkupString RenderMarkdown(string? markdown) { if (string.IsNullOrWhiteSpace(markdown)) { return new MarkupString(string.Empty); } // 使用缓存避免重复渲染 if (_markdownCache.TryGetValue(markdown, out var cached)) { return cached; } var html = Markdown.ToHtml(markdown, _outputMarkdownPipeline); var result = new MarkupString(html); // 限制缓存大小 if (_markdownCache.Count > 100) { _markdownCache.Clear(); } _markdownCache[markdown] = result; return result; }

.DisableHtml()很重要——AI 生成的内容不可控,如果允许原始 HTML,可能被注入恶意脚本。

代码高亮用的是Monaco Editor(就是 VS Code 用的那个编辑器),配合前端的语法高亮渲染,效果相当不错。

十、国际化:从硬编码到动态切换

项目一开始,界面上的文字都是硬编码的中文。后来想着要支持海外用户,不得不补国际化。

我用的是 JSON 资源文件 + 动态加载:

// zh-CN.json { "codeAssistant.title": "AI 编程助手", "codeAssistant.newSession": "新建会话", "codeAssistant.sessionHistory": "会话历史" } // en-US.json { "codeAssistant.title": "AI Coding Assistant", "codeAssistant.newSession": "New Session", "codeAssistant.sessionHistory": "Session History" }

然后在 Blazor 组件里通过一个T()方法获取翻译:

<h2>@T("codeAssistant.sessionHistory")</h2>

语言切换时,重新加载对应的 JSON 文件,刷新缓存。

老实说,这套方案有点「土」,但胜在简单可控。等需求复杂了再考虑引入成熟的 i18n 库。

十一、踩过的坑,你可以绕过去

最后聊聊几个印象深刻的坑。

11.1 Codex 的 stderr 里有正常输出

大多数 CLI 工具,stderr 用来输出错误信息,stdout 用来输出正常内容。

但 Codex 不按套路出牌——它把 JSONL 日志全往 stderr 写。

一开始我只读 stdout,结果啥也读不到。查了半天才发现问题,改成同时读取两个流并合并输出。

11.2 Windows 上的只读属性

清理工作区目录时,偶尔会遇到删除失败。

排查后发现是某些文件被设成了只读属性(不知道是哪个 CLI 工具干的)。

解决方案是先递归清除只读属性,再删除:

private static void NormalizeDirectoryAttributes(string directoryPath) { foreach (var file in Directory.EnumerateFiles(directoryPath, "*", SearchOption.AllDirectories)) { try { File.SetAttributes(file, FileAttributes.Normal); } catch { } } }

11.3 JSON 解析的边界情况

你以为 CLI 的输出永远是规整的 JSON?太天真了。

有时候会混进一些非 JSON 的内容,比如:

  • 启动时的 banner 信息

  • 调试日志

  • ANSI 颜色码

如果直接扔给 JSON 解析器,必挂。

我的策略是先做一层过滤:

if (!trimmed.StartsWith("{") && !trimmed.StartsWith("[")) { // 不是 JSON,当普通文本处理 return new CliOutputEvent { EventType = "raw", Content = trimmed }; }

十二、未来的坑和机会

项目跑起来了,但还有很多可以优化的地方:

  1. 更多 CLI 工具支持:目前只适配了 Claude Code 和 Codex,后续可以加入 GitHub Copilot CLI、Qwen CLI、Gemini CLI 等。适配器模式的好处就是扩展方便,加个新类就行。

  2. 协作功能:多人同时编辑同一个项目?想想都兴奋,但实现起来是另一个量级的复杂度。

  3. AI 生成代码的即时预览:现在只能预览 HTML,如果能直接运行 React/Vue 组件就更爽了。可以考虑集成在线 IDE 的沙箱能力。

  4. 性能优化:Blazor Server 的 SignalR 连接是有状态的,服务器内存随用户数线性增长。如果要支持大规模并发,可能得考虑 Blazor WebAssembly + 独立 API 的架构。

写在最后

从一个「在地铁上写代码」的念头,到真正把 Claude Code 和 Codex 塞进浏览器,这一路踩了不少坑,也学到了很多东西。

如果你也在做类似的项目,希望这篇文章能给你一些启发。

如果你只是路过看个热闹,那就当听了一个程序员的深夜絮叨吧。

代码已开源,地址:https://github.com/xuzeyu91/WebCode

欢迎 Star、Fork、提 Issue。


更多AIGC文章

RAG技术全解:从原理到实战的简明指南

更多VibeCoding文章

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询