Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion docs/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
<SiteHeader/>
<main class="flex-1">
<RouterView v-slot="{ Component, route }">
<DocLayout v-if="route.meta.doc">
<DocLayout v-if="route.meta.doc && route.meta.docType === 'release'">
<component :is="Component"/>
</DocLayout>
<BlogLayout v-else-if="route.meta.doc && route.meta.docType === 'blog'">
<component :is="Component"/>
</BlogLayout>
<component :is="Component" v-else/>
</RouterView>
</main>
Expand All @@ -17,4 +20,5 @@
import SiteHeader from './components/SiteHeader.vue'
import SiteFooter from './components/SiteFooter.vue'
import DocLayout from './layouts/DocLayout.vue'
import BlogLayout from './layouts/BlogLayout.vue'
</script>
1 change: 1 addition & 0 deletions docs/src/components/SiteFooter.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<div class="flex items-center gap-6">
<a class="hover:text-brand-600 dark:hover:text-brand-400 transition-colors" href="https://github.com/devlive-community/codeforge" target="_blank" rel="noopener">GitHub</a>
<RouterLink class="hover:text-brand-600 dark:hover:text-brand-400 transition-colors" to="/download">{{ t('footer.download') }}</RouterLink>
<RouterLink class="hover:text-brand-600 dark:hover:text-brand-400 transition-colors" to="/blog">{{ t('footer.blog') }}</RouterLink>
<RouterLink class="hover:text-brand-600 dark:hover:text-brand-400 transition-colors" to="/release">{{ t('footer.releases') }}</RouterLink>
</div>
</div>
Expand Down
17 changes: 16 additions & 1 deletion docs/src/components/SiteHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,20 @@

<div class="hidden sm:flex items-center gap-1 text-[15px]">
<RouterLink to="/download" class="px-3 py-2 rounded-lg text-slate-600 dark:text-slate-300 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-slate-800/60 transition-colors" active-class="!text-brand-600 dark:!text-brand-400">{{ t('nav.download') }}</RouterLink>
<RouterLink to="/release" class="px-3 py-2 rounded-lg text-slate-600 dark:text-slate-300 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-slate-800/60 transition-colors" active-class="!text-brand-600 dark:!text-brand-400">{{ t('nav.releases') }}</RouterLink>
<RouterLink to="/blog"
:class="[
'px-3 py-2 rounded-lg text-slate-600 dark:text-slate-300 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-slate-800/60 transition-colors',
route.path.startsWith('/blog') && '!text-brand-600 dark:!text-brand-400'
]">
{{ t('nav.blog') }}
</RouterLink>
<RouterLink to="/release"
:class="[
'px-3 py-2 rounded-lg text-slate-600 dark:text-slate-300 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-slate-800/60 transition-colors',
route.path.startsWith('/release') && '!text-brand-600 dark:!text-brand-400'
]">
{{ t('nav.releases') }}
</RouterLink>
</div>

<div class="ml-auto flex items-center gap-1">
Expand All @@ -32,8 +45,10 @@

<script setup lang="ts">
import {useI18n} from 'vue-i18n'
import {useRoute} from 'vue-router'
import ThemeToggle from './ThemeToggle.vue'
import LangToggle from './LangToggle.vue'

const {t} = useI18n()
const route = useRoute()
</script>
151 changes: 151 additions & 0 deletions docs/src/content/blog/welcome.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
---
title: 我用 Tauri 2 做了一个能跑 30 多种语言的代码运行器,聊聊插件化架构是怎么扛住的
date: 2026-06-06
author: CodeForge Team
description: 我想要的东西其实很简单:打开就能写、写完一键就能跑、还能顺手把跑出来的 JSON 看明白。最好是个桌面应用,本地、快、不依赖网络。
tags: ['公告', '开始']
---

> 这是一篇实现向的文章。如果你只想看产品,项目在这里:<https://github.com/devlive-community/codeforge>。如果你也在用 Tauri 折腾桌面工具,下面这些架构上的取舍和坑,也许能帮你少走点弯路。

## 起因:我想要一个「代码草稿本」,但没找到顺手的

事情的起点很朴素。

我经常需要快速验证一小段代码——可能是一段 Python 的正则、一个 Go 的并发写法、一段 Rust 的所有权实验。问题是,为了跑这几行临时代码,要么得新建一个项目、配一遍环境,要么开浏览器找在线 playground,但在线的那些通常只支持一两种语言,还动不动连不上。

我想要的东西其实很简单:**打开就能写、写完一键就能跑、还能顺手把跑出来的 JSON 看明白**。最好是个桌面应用,本地、快、不依赖网络。

找了一圈没有完全合手的,于是我自己做了一个,叫 CodeForge。做到现在它支持 30 多种语言的运行,从 Python、Node、Go、Rust 到 Swift、Haskell,甚至 AppleScript。这篇文章我想重点聊的不是功能清单,而是支撑这一切的那个东西——**插件化的语言系统**,以及它在工程上是怎么落地的。

## 第一个决定:为什么是 Tauri 2,而不是 Electron

做桌面应用,绕不开的第一个问题就是用什么壳。

Electron 当然是最省心的选择,生态成熟、文档全、遇到问题基本都能搜到答案。我一开始也确实往那个方向想过。但越想越觉得它有两个我不愿意接受的代价:**包体积**和**内存占用**。一个定位「随手打开的代码草稿本」,如果装完一百多兆、空载就吃掉几百兆内存,那它从第一秒起就背离了我做它的初衷——那种「轻、快、即开即用」的手感,本身就是这个产品的核心体验。

Tauri 2 用系统自带的 WebView 渲染前端,后端是 Rust,最终产物可以做得很小,常驻内存也低得多。对一个主打轻量的工具来说,这个差别不是「优化项」,而是「成立与否」的问题。

代价也是有的,我得诚实地说:

- WebView 在不同平台上行为不完全一致(macOS 的 WKWebView、Windows 的 WebView2、Linux 的 WebKitGTK),偶尔要写平台分支;
- Rust 的学习曲线比 Node 陡,前期开发明显更慢;
- 遇到问题时能搜到的现成答案比 Electron 少很多,不少坑得自己啃文档和 issue。

但对这个项目来说,这些代价换来的轻量和性能,我认为是值的。最终的技术栈是:前端 Vue 3 + TypeScript + CodeMirror 6,后端 Rust + Tauri 2,执行历史、AI 对话、代码片段、应用配置统一进 SQLite。

## 核心难题:30 多种语言,怎么才能不写成 30 个 if-else

支持一种语言的运行很简单:找到解释器/编译器,拼一条命令,起个子进程,把输出抓回来。

但当语言数量往上涨,这种天真的写法会迅速崩坏成一坨巨大的分支判断——「如果是 Python 就这样、如果是 Go 就那样、如果是 Rust 还要先编译再运行……」。每加一种语言都要回头改核心逻辑,每一种语言的特殊性都在污染主流程。这是典型的、会随规模膨胀而失控的设计。

所以从一开始我就把它设计成**插件化**的。但我想强调的是它的灵魂不在「插件」这个词,而在于——**它是配置驱动的**。核心引擎不认识任何具体语言,它只认识一份描述「这门语言该怎么跑」的配置;绝大多数通用逻辑(拼命令、起进程、抓输出、清理)都写在统一的地方,而每个语言插件本身薄得惊人。

### 一个语言插件,薄到什么程度

落到 Rust 里,所有语言实现同一个 trait:

```rust
pub trait LanguagePlugin: Send + Sync {
fn get_language_name(&self) -> &'static str; // 显示名,如 "Python 3"
fn get_language_key(&self) -> &'static str; // 唯一键,如 "python3"
fn get_file_extension(&self) -> String; // 扩展名
fn get_version_args(&self) -> Vec<&'static str>; // 版本探测参数
fn get_path_command(&self) -> String; // 探测工具链是否可用
fn get_default_config(&self) -> PluginConfig; // 默认配置(核心)
fn get_default_command(&self) -> String;
// …还有一组带默认实现的钩子,下面会讲
}
```

真正承载「这门语言怎么跑」的,是它返回的那份 `PluginConfig`:

```rust
pub struct PluginConfig {
pub enabled: bool,
pub extension: String,
pub language: String,
pub run_command: Option<String>, // 命令模板,如 "python3 $filename"
pub before_compile: Option<String>, // 运行前的准备命令
pub after_compile: Option<String>, // 运行后的清理命令
pub template: Option<String>, // 新建文件时的初始模板
pub timeout: Option<u64>, // 超时(默认 30s)
pub execute_home: Option<String>, // 自定义工具链路径
// …
}
```

于是一个解释型语言的插件,几乎就是「填一张表」。这是 Python 3 插件的全部核心——没有任何执行逻辑,只是声明「我叫什么、扩展名是啥、用 `python3 $filename` 跑」:

```rust
fn get_default_config(&self) -> PluginConfig {
PluginConfig {
enabled: true,
language: "python3".into(),
extension: "py".into(),
run_command: Some("python3 $filename".into()), // $filename 运行时被替换
before_compile: None,
after_compile: None,
template: Some("# 在这里输入 Python 3 代码".into()),
timeout: Some(30),
// …
}
}
```

核心引擎拿到文件后,从配置里取出 `run_command`,把 `$filename` 替换成真实路径,剩下的——起进程、流式抓输出、算耗时——全是**与语言无关**的通用逻辑。加一门解释型语言,本质上就是再填一张这样的表。

### 编译型语言怎么塞进同一套抽象

解释型一步到位,麻烦的是 Rust、C/C++、Java 这类「先编译、再运行」的两步语言。我没有为它们单独造一套机制,而是用了两个朴素但好使的办法。

**第一招:用 shell 的 `&&` 把两步串成一条命令。** Rust 插件就是这么干的——它的运行命令实际展开成:

```text
rustc /path/to/main.rs -o /tmp/main && /tmp/main
```

编译和运行被 `&&` 连成一个整体交给 shell。编译失败,`&&` 短路,编译器的报错就成了本次运行的输出——而这恰恰是「验证代码」场景里用户最想看到的东西,根本不需要单独搞一套错误展示。跑完之后,`after_compile` 配了 `rm -f /tmp/main` 把产物清掉。Windows 上则换成 `cmd /c "rustc ... -o main.exe && main.exe"`,平台差异用 `#[cfg(target_os)]` 分支隔开。

**第二招:`before_compile` / `after_compile` 两个钩子。** Go 插件的 `before_compile` 是 `go mod init temp 2>/dev/null || true`——临时跑一段 Go 片段得先有个 module,这个钩子就替用户把它补上了,`|| true` 保证已经初始化过时不报错。运行命令本身用 `go run $filename`,一步编译加运行。

Java 我直接用了 `java $filename`:Java 11 起支持单文件源码模式,一条命令就能编译加运行,连 `javac` 那一步都省了;再挂一个 `after_compile: rm -f *.class` 兜底清理可能残留的字节码。

你看,编译型语言的全部「特殊性」,最后都收敛成了配置里的几个字符串字段,没有一行渗进核心引擎。

### 这套设计最大的红利:加语言常常不用写代码

因为「怎么跑一门语言」已经被完整地编码进了 `PluginConfig`(命令模板 + 编译前后钩子 + 扩展名 + 超时),所以新增语言很多时候根本不需要写 Rust——填一份配置就行。我把这个能力直接开放给了用户:内置插件之外,还有一类 `CustomPlugin`,启动时从用户配置里加载,只要语言键不和内置的冲突,用户就能纯靠填配置给 CodeForge 加一门新语言。配置驱动设计在这里兑现了它最大的价值。

## 运行时的几个真实工程细节

把代码跑起来,魔鬼都在细节里。几个我觉得值得一提的:

**工具链探测。** 用户机器上不一定装了对应工具链——没人会为了跑段 Haskell 恰好装了 GHC。所以每个插件都要给出探测自己的方式:`get_version_args` 提供版本参数(Python 是 `--version`、Java 是 `-version`、Go 是 `version`,各不相同),`get_path_command` 给出定位工具的命令——有些直接得很巧,比如 Python 用 `import sys; print(sys.executable)` 反查自己的解释器路径。探测不到就给一句明确的「未检测到 xxx」,而不是甩给用户一坨 `command not found`。

**在配置里设环境变量。** `before_compile` 不只能跑准备命令,还能写 `export KEY=value`(Unix)或 `set KEY=value`(Windows),插件会解析它、跨平台地设进环境,还会帮你展开 `$PATH` / `%PATH%`。这样一些需要特定环境变量才能跑的语言,不用改代码、配置里写一行就解决了。

**多标签并发与流式输出。** 每次运行请求都带一个 `task_id`,用它做事件路由:Rust 端读到一段子进程输出就带着 `task_id` 发事件,前端按 `task_id` 把输出投递到对应的标签页。于是多个标签可以同时各跑各的,输出实时流式刷新、互不串台,而不是憋到进程结束才一次性吐出来。

**「成功但没输出」也是一种结果。** 有个小细节我自己挺得意:运行成功、但 stdout/stderr 都为空时,后执行钩子会填一个 `END-NO-OUTPUT` 的哨兵值。否则用户点了运行、界面一片空白,根本分不清是「跑成功了但本来就没输出」还是「卡住了」。一个哨兵值,把这两种状态明确区分开。

**每语言可配的超时。** 默认 30 秒,但写死在配置里、可按语言调整,避免一段死循环把应用拖住。

## 顺手做的一件事:把结构化数据「看明白」

跑代码经常会吐出 JSON,而一坨压扁的 JSON 是很难读的。

所以我顺手给 JSON / XML / YAML 做了两种可视化:可折叠的**层级树**,以及把字段关系画成卡片 + 连线的**关系图**。Markdown 会实时渲染预览(内嵌 HTML 用 DOMPurify 净化,防 XSS)。还有一个我自己很喜欢的小功能——识别到文件是 GitHub Actions 的 workflow 时,自动把它渲染成 **Jobs 依赖的 DAG 图**:触发事件、各个 Job、它们之间的依赖和每个 Job 的 Steps,一目了然,比对着 YAML 数缩进强多了。

## 写在最后

做到现在,CodeForge 大概是我「最想自己天天用」的一个项目——它解决的就是我自己每天都会遇到的麻烦。配置驱动的插件化在前期看起来有点「过度设计」,但当语言数量从 5 种涨到 30 多种时,它的价值才真正显现:核心一直很干净,加语言变成了一件愉快的、甚至能交给用户自己完成的事。

如果你对实现感兴趣,或者也想要这么一个本地的多语言代码草稿本,欢迎来看看、点个 star、提 issue:

**👉 <https://github.com/devlive-community/codeforge>**

技术栈:Vue 3 + TypeScript + CodeMirror 6(前端)· Rust + Tauri 2(后端)· SQLite(存储)· 配置驱动的插件化语言系统(架构)。

对了,**你最希望它支持哪种语言、或者最想要什么功能?** 评论区告诉我,下一个版本说不定就给你安排上。
47 changes: 47 additions & 0 deletions docs/src/content/blogs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// 技术博客:扫描 blog/*.md,自动生成博客列表与路由组件。

export interface BlogMeta {
slug: string
title: string
date?: string
author?: string
description?: string
tags?: string[]
}

const slugFromPath = (p: string): string => (p.match(/([^/]+)\.md$/)?.[1]) ?? p

// 按日期降序排序
const compareDateDesc = (a: string | undefined, b: string | undefined): number => {
if (!a && !b) return 0
if (!a) return 1
if (!b) return -1
return b.localeCompare(a)
}

// 导入所有 markdown 文件
const modules = import.meta.glob<any>('./blog/*.md', { eager: true })

const blogsData: BlogMeta[] = Object.entries(modules).map(([path, module]) => {
const slug = slugFromPath(path)
// 日期格式为 ISO 8601 字符串,提取 YYYY-MM-DD 部分
const date = module.date
const dateStr = typeof date === 'string' ? date.split('T')[0] : date
return {
slug,
title: module.title || slug,
date: dateStr,
author: module.author,
description: module.description,
tags: module.tags || []
}
})

export const blogs: BlogMeta[] = blogsData.sort((a, b) => compareDateDesc(a.date, b.date))

// 路由:每篇博客一个静态路由,便于 vite-ssg 全量预渲染
const componentLoaders = import.meta.glob('./blog/*.md')
export const blogRoutes = Object.entries(componentLoaders).map(([path, loader]) => ({
path: `/blog/${slugFromPath(path)}`,
component: loader
}))
2 changes: 2 additions & 0 deletions docs/src/content/release/25.0.0.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
---
title: 25.0.0
date: 2025-08-11
description: 首个正式版本发布
---

我们激动地宣布 **CodeForge v25.0.0** 正式发布!这是 CodeForge 项目的首个正式版本,标志着这款轻量级、高性能桌面代码执行器正式与大家见面。
Expand Down
2 changes: 2 additions & 0 deletions docs/src/content/release/25.0.1.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
---
title: 25.0.1
date: 2025-08-18
description: 首个版本的问题修复
---

CodeForge v25.0.1 正式发布!这是一个重要的功能更新版本,我们在短短几天内就为用户带来了令人振奋的新特性。本次更新重点加强了多语言支持,升级了编辑器体验,并优化了整体性能表现。
Expand Down
2 changes: 2 additions & 0 deletions docs/src/content/release/25.0.2.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
---
title: 25.0.2
date: 2025-08-25
description: 性能优化和错误修复
---

CodeForge v25.0.2 隆重登场!本次更新是我们迄今为止最具雄心的版本,大幅扩展了编程语言支持范围,并对编辑器体验进行了全方位优化。从主流语言到小众语言,从界面美化到功能增强,我们致力于为每一位开发者提供更完善的代码执行环境。
Expand Down
2 changes: 2 additions & 0 deletions docs/src/content/release/25.0.3.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
---
title: 25.0.3
date: 2025-09-01
description: UI 优化和用户体验改进
---

CodeForge v25.0.3 重磅发布!本次更新带来了突破性的 Web 技术栈支持和前所未有的编辑器个性化配置能力。我们不仅新增了 12 种编程语言和技术栈,更引入了革命性的 Web 渲染模式,让 CodeForge 真正成为一个全栈开发者的理想工具。
Expand Down
2 changes: 2 additions & 0 deletions docs/src/content/release/25.0.4.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
---
title: 25.0.4
date: 2025-10-09
description: 稳定性提升和 Bug 修复
---

CodeForge v25.0.4 正式发布!本次更新专注于 Apple 生态系统的完善和函数式编程能力的增强。我们新增了 4 种重要编程语言,特别是 Objective-C 家族的加入,让 CodeForge 成为 iOS/macOS 开发者验证代码的得力工具。
Expand Down
4 changes: 3 additions & 1 deletion docs/src/content/release/25.0.5.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
---
title: 25.0.4
title: 25.0.5
date: 2025-12-28
description: JVM 生态和 Go 语言支持、环境管理优化
---

CodeForge v25.0.5 正式发布!本次更新带来了 JVM 生态和 Go 语言的完整支持,全面优化了环境管理、网络配置和用户体验。我们新增了 3 种重要编程语言,重构了版本管理系统,让 CodeForge 成为更强大、更易用的代码执行工具。
Expand Down
2 changes: 2 additions & 0 deletions docs/src/content/release/26.0.0.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
---
title: 26.0.0
date: 2026-06-04
description: 全新架构、性能优化、UI 改版
---

CodeForge v26.0.0 是一次里程碑式的更新:CodeForge 从"按语言运行代码片段"的工具,进化为以**文件 / 项目为中心**的轻量编辑器,并首次内置 **AI 助手**。本次更新带来多标签编辑、文件树侧栏、就地运行、超大文件只读查看、可自定义快捷键,以及接入 Claude / OpenAI / DeepSeek 的 AI 对话与代码生成能力。
Expand Down
Loading
Loading