Compare commits

..
6 Commits
Author SHA1 Message Date
yyyyyynnn b03b9d881c Add startup update prompt 2026-07-04 12:56:57 +08:00
yyyyyynnn 10d4491f78 Add changelog and fix Windows console 2026-07-04 12:44:05 +08:00
yyyyyynnn 964151d479 Polish Windows installer branding 2026-07-04 12:37:20 +08:00
yyyyyynnn 5f1c510dd2 Fix about page external links 2026-07-04 12:32:31 +08:00
yyyyyynnn 686f2b4e18 Fix macOS icon and speed up releases 2026-07-04 12:21:54 +08:00
yyyyyynnn 7cd783b786 Improve README and release bundles 2026-07-04 12:09:50 +08:00
14 changed files with 310 additions and 77 deletions
+17 -3
View File
@@ -16,7 +16,13 @@ jobs:
strategy:
fail-fast: false
matrix:
platform: [macos-latest, ubuntu-22.04, windows-latest]
include:
- platform: macos-latest
args: --bundles dmg
- platform: ubuntu-22.04
args: --bundles deb,rpm
- platform: windows-latest
args: --bundles msi
steps:
- name: Checkout
@@ -47,6 +53,11 @@ jobs:
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Rust cache
uses: Swatinem/rust-cache@v2
with:
workspaces: apps/desktop/src-tauri
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -61,10 +72,13 @@ jobs:
releaseBody: |
Codex-X desktop release.
See `CHANGELOG.md` for highlights.
- macOS: `.dmg`
- Windows: `.msi` / `.exe`
- Linux: `.AppImage` / `.deb` / `.rpm`
- Windows: `.msi`
- Linux: `.deb` / `.rpm`
GitHub 会自动附带 Source code (zip/tar.gz)。
releaseDraft: false
prerelease: false
args: ${{ matrix.args }}
+30
View File
@@ -0,0 +1,30 @@
# Changelog
All notable changes to Codex-X will be documented here.
## [v0.2.5] - 2026-07-04
- Windows 版双击启动不再额外弹出终端窗口。
- 改进 Windows MSI 安装器品牌展示与图标。
- About 页面外部链接改为系统默认浏览器打开。
- Release 流程加入 Rust cache,后续构建更快。
## [v0.2.4] - 2026-07-04
- 美化 Windows MSI 安装器横幅与对话图。
- Windows 安装包加入应用图标。
## [v0.2.3] - 2026-07-04
- 修复 macOS DMG 中应用图标缺失问题。
- 补充 macOS 安装说明。
- Release 流程加入基础缓存优化。
## [v0.2.2] - 2026-07-04
- macOS / Linux / Windows 首次三平台 Release。
- Linux 产物改为 `deb` / `rpm`,避免 AppImage 图标问题。
## [v0.2.1] - 2026-07-04
- 首次加入应用图标与 GitHub Release 自动发布。
+129 -59
View File
@@ -1,22 +1,120 @@
# Codex-X
<div align="center">
<img src="apps/desktop/src-tauri/icons/icon.png" alt="Codex-X Logo" width="160" />
Codex-X 是一个面向 OpenAI Codex / Codex CLI 的跨平台桌面配置管理器,用来可视化管理 Codex 的供应商、`config.toml`、`auth.json` 和指令提示词。
# Codex-X
> 当前桌面端基于 **Tauri 2 + React + TypeScript + Rust + SQLite**。
**Codex 提示词注入与配置切换管理器**
## 功能
一款面向 **OpenAI Codex / Codex CLI** 的跨平台桌面工具,重点提供 `gpt5.5-unrestricted.md` 指令提示词注入、Provider 切换、官方 Auth 管理与 TOML 可视化编辑。
- **供应商管理**:添加、编辑、切换 Codex 第三方 API Provider。
- **官方配置**:查看/编辑 OpenAI Official 的 `auth.json` 与官方模型配置。
- **TOML 编辑**:在供应商编辑页直接编辑并保存完整 `config.toml`。
- **指令提示词管理**:管理多个 `model_instructions_file` 模板,支持自定义提示词、启用、禁用。
- **cc-switch 导入**:从本机 cc-switch SQLite 数据库导入 Codex Provider。
- **跨平台路径**:默认读取 `CODEX_HOME`,否则使用用户目录下的 `.codex`。
- **更新检查**:通过 GitHub Releases 检测最新版本。
<p>
<img src="https://img.shields.io/github/v/release/yynxxxxx/Codex-X?label=version&color=blue" alt="version" />
<img src="https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-555" alt="platform" />
<img src="https://img.shields.io/badge/built%20with-Tauri%202-24C8DB" alt="tauri" />
<img src="https://img.shields.io/badge/license-MIT-green" alt="license" />
</p>
## 配置文件位置
<p>
<img src="https://img.shields.io/badge/React-18-61DAFB?logo=react&logoColor=white" />
<img src="https://img.shields.io/badge/TypeScript-5-3178C6?logo=typescript&logoColor=white" />
<img src="https://img.shields.io/badge/Rust-stable-000000?logo=rust&logoColor=white" />
<img src="https://img.shields.io/badge/SQLite-Ready-003B57?logo=sqlite&logoColor=white" />
<img src="https://img.shields.io/badge/Vite-Ready-646CFF?logo=vite&logoColor=white" />
</p>
</div>
Codex-X 默认读写:
---
## Codex-X 是什么?
Codex-X 不是单纯的配置文件编辑器,而是一个更偏向「Codex 增强启动器」的桌面管理工具。
它可以把预设或自定义的 **指令提示词模板** 写入 Codex 配置,例如项目内置的:
```text
examples/gpt5.5-unrestricted.md
```
你可以在 UI 中直接启用、禁用、切换多个提示词模板,也可以同时管理不同第三方供应商 API 和官方 OpenAI Codex 登录配置。
## 核心亮点
### 1. 指令提示词注入
- 一键启用 `gpt5.5-unrestricted.md`
- 支持多个提示词模板并存
- 启用前自动记录状态,方便回退
- 支持禁用当前提示词,让 Codex 恢复普通配置
### 2. Provider 可视化切换
- 添加第三方 Codex API Provider
- 编辑 Base URL / API Key / Model / TOML 配置
- 当前 Provider 状态清晰可见
- 支持从 cc-switch 数据库导入 Codex Provider
### 3. 官方 Codex 配置管理
- 读取 Codex 官方 `auth.json`
- 支持官方 ChatGPT 登录态 Auth 查看与编辑
- 区分官方 Auth 与第三方 API Key,不混在一起
### 4. TOML 配置编辑
- 可视化查看当前 Codex `config.toml`
- 在 Provider 编辑页直接编辑完整 TOML
- 保存后立即同步到 Codex 配置目录
### 5. 跨平台桌面软件
- macOS `.dmg`
- Windows `.msi`
- Linux `.deb` / `.rpm`
- GitHub Releases 自动构建发布
## 软件预览
<details open>
<summary><b>主界面 / Provider / TOML / Auth 截图</b></summary>
> 这里可以放软件主界面、供应商切换页面、TOML 编辑页面、官方 Auth 页面截图。
<!-- 示例:
<p align="center">
<img src="docs/screenshots/overview.png" width="760" />
</p>
-->
</details>
<details>
<summary><b>提示词注入前后效果图</b></summary>
> 这里可以放启用 `gpt5.5-unrestricted.md` 前后的 Codex 效果对比图。
<!-- 示例:
<p align="center">
<img src="docs/screenshots/prompt-before.png" width="760" />
<img src="docs/screenshots/prompt-after.png" width="760" />
</p>
-->
</details>
## 技术栈
| 类型 | 技术 |
| --- | --- |
| 桌面框架 | Tauri 2 |
| 前端 | React 18 / TypeScript / Vite |
| 后端 | Rust |
| 本地数据 | SQLite / rusqlite |
| 配置编辑 | TOML / JSON |
| 发布 | GitHub Actions / GitHub Releases |
## 配置路径
Codex-X 默认读取 Codex 配置目录:
```text
~/.codex/config.toml
@@ -31,71 +129,43 @@ CODEXX_HOME=/path/to/codex-x-data
CC_SWITCH_HOME=/path/to/.cc-switch
```
Codex-X 自身数据默认存放在:
Codex-X 自身数据库默认位于:
```text
~/.codexx/codexx.db
```
## Windows 权限说明
## 下载
Windows 正常情况下 Codex 配置位于:
请前往 Releases 页面下载:
```text
%USERPROFILE%\.codex
```
https://github.com/yynxxxxx/Codex-X/releases
这是用户目录,不需要管理员权限。只有当你手动把 `CODEX_HOME` 指向 `C:\Program Files`、`C:\Windows`、系统盘根目录等受保护目录时,写入才可能因为权限不足失败。Codex-X 不会静默提权,会直接显示具体 IO 错误。
## 开发
## 开发运行
```bash
pnpm install
pnpm dev
```
类型检查:
```bash
pnpm typecheck
```
构建桌面端:
```bash
pnpm --dir apps/desktop tauri build
```
## Release
本仓库使用 GitHub Actions 在打 tag 时自动构建三平台安装包:
```bash
git tag v0.2.0
git push origin v0.2.0
```
Release 页面会包含:
- macOS `.dmg`
- Windows `.msi` / `.exe`
- Linux `.AppImage` / `.deb` / `.rpm`
- GitHub 自动生成的 Source code `.zip` / `.tar.gz`
## 技术栈
- Tauri 2
- React 18
- TypeScript
- Vite
- Rust
- SQLite / rusqlite
- toml_edit
## 项目地址
<https://github.com/yynxxxxx/Codex-X>
## License
MIT
MIT © yynxxxxx
## macOS 安装说明
如果你在未签名/未公证的 DMG 中看到“软件已损坏”提示,这是 macOS Gatekeeper 的正常行为。
- 最佳方式:使用 Apple Developer ID 签名并 notarize
- 仅本地测试:可手动移除 quarantine 属性
```bash
xattr -dr com.apple.quarantine /Applications/Codex-X.app
```
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "codex-x",
"version": "0.2.1",
"version": "0.2.6",
"private": true,
"description": "Codex Switch & Instruct desktop manager",
"type": "module",
+1 -1
View File
@@ -337,7 +337,7 @@ dependencies = [
[[package]]
name = "codex-x"
version = "0.2.0"
version = "0.2.3"
dependencies = [
"chrono",
"dirs 5.0.1",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "codex-x"
version = "0.2.1"
version = "0.2.6"
description = "Codex Switch & Instruct desktop manager"
authors = ["yynxxxxx"]
license = "MIT"
Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 KiB

+23
View File
@@ -1445,6 +1445,28 @@ fn restore_backup(config_dir: Option<String>, backup_id: String) -> Result<Actio
})
}
#[tauri::command]
fn open_url(url: String) -> std::result::Result<(), String> {
let trimmed = url.trim();
if trimmed.is_empty() {
return Err("URL 为空".to_string());
}
let status = if cfg!(target_os = "macos") {
Command::new("open").arg(trimmed).status()
} else if cfg!(target_os = "windows") {
Command::new("cmd").args(["/C", "start", "", trimmed]).status()
} else {
Command::new("xdg-open").arg(trimmed).status()
};
match status {
Ok(s) if s.success() => Ok(()),
Ok(s) => Err(format!("打开失败,退出码: {:?}", s.code())),
Err(e) => Err(format!("打开失败: {e}")),
}
}
pub fn run() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![
@@ -1468,6 +1490,7 @@ pub fn run() {
save_provider_toml_config,
list_backups,
restore_backup,
open_url,
])
.run(tauri::generate_context!())
.expect("error while running Codex-X");
+2
View File
@@ -1,3 +1,5 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
codexx_lib::run()
}
+15 -2
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Codex-X",
"version": "0.2.1",
"version": "0.2.6",
"identifier": "com.yynxxxxx.codexx",
"build": {
"frontendDist": "../dist",
@@ -28,6 +28,19 @@
},
"bundle": {
"active": true,
"targets": "all"
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"targets": "all",
"windows": {
"wix": {
"bannerPath": "icons/wix/banner.bmp",
"dialogImagePath": "icons/wix/dialog.bmp"
}
}
}
}
+51 -9
View File
@@ -660,6 +660,7 @@ function App() {
const [savedPrompts, setSavedPrompts] = React.useState<SavedPrompt[]>([]);
const [aboutInfo, setAboutInfo] = React.useState<AboutInfo | null>(null);
const [releaseInfo, setReleaseInfo] = React.useState<ReleaseInfo>({ status: "idle" });
const [updatePromptOpen, setUpdatePromptOpen] = React.useState(false);
const [state, setState] = React.useState<CodexState | null>(null);
const [backups, setBackups] = React.useState<BackupEntry[]>([]);
const [configDir, setConfigDir] = React.useState("");
@@ -671,6 +672,7 @@ function App() {
const [providerTomlDirty, setProviderTomlDirty] = React.useState(false);
const [promptForm, setPromptForm] = React.useState<SavedPrompt>(blankPromptForm);
const [officialForm, setOfficialForm] = React.useState({ model: "gpt-5.5", authJson: "" });
const autoUpdateCheckedRef = React.useRef(false);
const providerTomlPreview = React.useMemo(() => buildProviderTomlPreview(providerForm, state), [providerForm, state]);
const providerAuthPreview = React.useMemo(() => buildProviderAuthPreview(providerForm), [providerForm]);
const currentInstructionId = instructionIdFromPath(state?.instructionFile);
@@ -959,7 +961,7 @@ function App() {
const restoreBackup = (backupId: string) =>
call(() => invoke<ActionResult>("restore_backup", { configDir: configDir || null, backupId }), handleActionResult);
const checkForUpdates = async () => {
const checkForUpdates = React.useCallback(async ({ quiet = false }: { quiet?: boolean } = {}) => {
const repo = aboutInfo?.githubRepo || FALLBACK_GITHUB_REPO;
const appVersion = aboutInfo?.appVersion || "0.0.0";
setReleaseInfo({ status: "checking" });
@@ -979,24 +981,32 @@ function App() {
};
const latestVersion = release.tag_name || release.name || "";
const asset = releaseAssetForPlatform(release.assets || []);
const hasUpdate = compareVersions(latestVersion, appVersion) > 0;
setReleaseInfo({
status: "ok",
latestVersion,
htmlUrl: asset?.browser_download_url || release.html_url,
assetName: asset?.name,
body: release.body || "",
hasUpdate: compareVersions(latestVersion, appVersion) > 0,
message: compareVersions(latestVersion, appVersion) > 0
hasUpdate,
message: hasUpdate
? (lang === "zh" ? "发现新版本" : "Update available")
: (lang === "zh" ? "当前已是最新版本" : "You are up to date"),
});
if (hasUpdate) setUpdatePromptOpen(true);
} catch (e) {
setReleaseInfo({
status: "error",
message: lang === "zh" ? `检查失败:${String(e)}` : `Check failed: ${String(e)}`,
message: quiet ? (lang === "zh" ? "自动检查失败" : "Auto check failed") : (lang === "zh" ? `检查失败:${String(e)}` : `Check failed: ${String(e)}`),
});
}
};
}, [aboutInfo?.githubRepo, aboutInfo?.appVersion, lang]);
React.useEffect(() => {
if (!state || !aboutInfo || autoUpdateCheckedRef.current) return;
autoUpdateCheckedRef.current = true;
void checkForUpdates({ quiet: true });
}, [state, aboutInfo, checkForUpdates]);
const loadCcSwitchOfficialAuth = async (showToast = true) => {
const candidate = await invoke<OfficialAuthCandidate | null>("read_ccswitch_official_auth", { dbPath: null });
@@ -1144,6 +1154,38 @@ function App() {
</div>
)}
{error && <div className="toast error">{error}</div>}
{updatePromptOpen && releaseInfo.hasUpdate && (
<div className="update-mask" onClick={() => setUpdatePromptOpen(false)}>
<div className="update-dialog glass" onClick={(e) => e.stopPropagation()}>
<div className="update-head">
<div className="update-icon"><Sparkles size={22} /></div>
<div>
<p className="eyebrow">Codex-X</p>
<h3>{lang === "zh" ? "发现新版本" : "New version available"}</h3>
</div>
</div>
<div className="update-body">
<p>{lang === "zh" ? "检测到新版本,是否立即打开下载页?" : "A new version was found. Open the download page now?"}</p>
<div className="about-kv compact">
<div><span>{lang === "zh" ? "当前版本" : "Current"}</span><strong>{aboutInfo?.appVersion || "-"}</strong></div>
<div><span>{lang === "zh" ? "最新版本" : "Latest"}</span><strong>{releaseInfo.latestVersion || "-"}</strong></div>
<div><span>{lang === "zh" ? "资源" : "Asset"}</span><code>{releaseInfo.assetName || "-"}</code></div>
</div>
</div>
<div className="update-actions">
<button className="primary-btn" onClick={() => {
setUpdatePromptOpen(false);
if (releaseInfo.htmlUrl) void invoke("open_url", { url: releaseInfo.htmlUrl });
}}>
<Download size={16} /> {lang === "zh" ? "现在下载" : "Download now"}
</button>
<button className="secondary-btn" onClick={() => setUpdatePromptOpen(false)}>
{lang === "zh" ? "稍后" : "Later"}
</button>
</div>
</div>
</div>
)}
{!state ? (
<div className="panel glass center-panel">
@@ -1475,8 +1517,8 @@ function App() {
<div><span>{lang === "zh" ? "项目地址" : "Project"}</span><code>{aboutInfo?.projectUrl || `https://github.com/${FALLBACK_GITHUB_REPO}`}</code></div>
</div>
<div className="about-actions">
<button className="secondary-btn" onClick={() => window.open(aboutInfo?.projectUrl || `https://github.com/${FALLBACK_GITHUB_REPO}`, "_blank")}><ExternalLink size={16} /> {lang === "zh" ? "打开项目主页" : "Open project"}</button>
<button className="ghost-btn" onClick={() => window.open(`${aboutInfo?.projectUrl || `https://github.com/${FALLBACK_GITHUB_REPO}`}/issues`, "_blank")}><ExternalLink size={16} /> {lang === "zh" ? "反馈问题" : "Issues"}</button>
<button className="secondary-btn" onClick={() => void invoke("open_url", { url: aboutInfo?.projectUrl || `https://github.com/${FALLBACK_GITHUB_REPO}` })}><ExternalLink size={16} /> {lang === "zh" ? "打开项目主页" : "Open project"}</button>
<button className="ghost-btn" onClick={() => void invoke("open_url", { url: `${aboutInfo?.projectUrl || `https://github.com/${FALLBACK_GITHUB_REPO}`}/issues` })}><ExternalLink size={16} /> {lang === "zh" ? "反馈问题" : "Issues"}</button>
</div>
</section>
@@ -1493,8 +1535,8 @@ function App() {
</div>
{releaseInfo.body && <pre className="release-notes">{releaseInfo.body}</pre>}
<div className="about-actions">
<button className="primary-btn" onClick={checkForUpdates} disabled={releaseInfo.status === "checking"}><RefreshCw size={16} className={cx(releaseInfo.status === "checking" && "spin")} /> {lang === "zh" ? "检查更新" : "Check updates"}</button>
<button className="secondary-btn" onClick={() => releaseInfo.htmlUrl && window.open(releaseInfo.htmlUrl, "_blank")} disabled={!releaseInfo.htmlUrl}><Download size={16} /> {lang === "zh" ? "打开下载页" : "Open download"}</button>
<button className="primary-btn" onClick={() => void checkForUpdates()} disabled={releaseInfo.status === "checking"}><RefreshCw size={16} className={cx(releaseInfo.status === "checking" && "spin")} /> {lang === "zh" ? "检查更新" : "Check updates"}</button>
<button className="secondary-btn" onClick={() => releaseInfo.htmlUrl && void invoke("open_url", { url: releaseInfo.htmlUrl })} disabled={!releaseInfo.htmlUrl}><Download size={16} /> {lang === "zh" ? "打开下载页" : "Open download"}</button>
</div>
</section>
</section>
+39
View File
@@ -198,6 +198,45 @@ input, select, textarea {
.toast.ok { color: #dbfff3; background: rgba(12, 112, 81, .88); border: 1px solid rgba(31,242,178,.3); }
.toast.error { color: #fff; background: rgba(157, 38, 63, .92); border: 1px solid rgba(255,120,145,.35); animation: none; }
@keyframes toast { 0% { opacity: 0; transform: translateY(-8px); } 12%, 84% { opacity: 1; transform: translateY(0); } 100% { opacity: 0; transform: translateY(-8px); } }
.update-mask {
position: fixed;
inset: 0;
z-index: 40;
display: grid;
place-items: center;
background: rgba(3, 7, 16, .62);
backdrop-filter: blur(10px);
}
.update-dialog {
width: min(560px, calc(100vw - 40px));
border-radius: 26px;
padding: 22px;
box-shadow: 0 28px 90px rgba(0, 0, 0, .5);
}
.update-head {
display: flex;
align-items: center;
gap: 14px;
margin-bottom: 14px;
}
.update-icon {
width: 46px;
height: 46px;
border-radius: 16px;
display: grid;
place-items: center;
color: white;
background: linear-gradient(135deg, #7c5cff, #12d3ff);
}
.update-body { display: grid; gap: 14px; color: #dfe7f8; }
.update-body p { margin: 0; line-height: 1.6; }
.about-kv.compact { grid-template-columns: 1fr 1fr 1fr; }
.update-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 8px; }
.spin { animation: spin 1s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "codex-x-workspace",
"private": true,
"version": "0.2.1",
"version": "0.2.6",
"scripts": {
"dev": "pnpm --dir apps/desktop tauri dev",
"build": "pnpm --dir apps/desktop tauri build",