mirror of
https://github.com/yynxxxxx/Codex-X.git
synced 2026-10-18 05:59:23 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b03b9d881c | ||
|
|
10d4491f78 | ||
|
|
964151d479 | ||
|
|
5f1c510dd2 | ||
|
|
686f2b4e18 |
@@ -53,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
|
||||
|
||||
@@ -67,6 +72,8 @@ jobs:
|
||||
releaseBody: |
|
||||
Codex-X desktop release.
|
||||
|
||||
See `CHANGELOG.md` for highlights.
|
||||
|
||||
- macOS: `.dmg`
|
||||
- Windows: `.msi`
|
||||
- Linux: `.deb` / `.rpm`
|
||||
|
||||
@@ -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 自动发布。
|
||||
@@ -157,3 +157,15 @@ pnpm --dir apps/desktop tauri build
|
||||
## License
|
||||
|
||||
MIT © yynxxxxx
|
||||
|
||||
|
||||
## macOS 安装说明
|
||||
|
||||
如果你在未签名/未公证的 DMG 中看到“软件已损坏”提示,这是 macOS Gatekeeper 的正常行为。
|
||||
|
||||
- 最佳方式:使用 Apple Developer ID 签名并 notarize
|
||||
- 仅本地测试:可手动移除 quarantine 属性
|
||||
|
||||
```bash
|
||||
xattr -dr com.apple.quarantine /Applications/Codex-X.app
|
||||
```
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "codex-x",
|
||||
"version": "0.2.2",
|
||||
"version": "0.2.6",
|
||||
"private": true,
|
||||
"description": "Codex Switch & Instruct desktop manager",
|
||||
"type": "module",
|
||||
|
||||
Generated
+1
-1
@@ -337,7 +337,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "codex-x"
|
||||
version = "0.2.2"
|
||||
version = "0.2.3"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"dirs 5.0.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "codex-x"
|
||||
version = "0.2.2"
|
||||
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 |
@@ -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");
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
codexx_lib::run()
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Codex-X",
|
||||
"version": "0.2.2",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "codex-x-workspace",
|
||||
"private": true,
|
||||
"version": "0.2.2",
|
||||
"version": "0.2.6",
|
||||
"scripts": {
|
||||
"dev": "pnpm --dir apps/desktop tauri dev",
|
||||
"build": "pnpm --dir apps/desktop tauri build",
|
||||
|
||||
Reference in New Issue
Block a user