mirror of
https://github.com/yynxxxxx/Codex-X.git
synced 2026-10-19 06:09:33 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
726de0fe63 | ||
|
|
7d0b2891b9 |
@@ -223,7 +223,7 @@ jobs:
|
||||
|
||||
- name: Test Windows filesystem compatibility
|
||||
if: matrix.platform == 'windows-latest'
|
||||
run: cargo test --manifest-path apps/desktop/src-tauri/Cargo.toml --lib --locked -- --test-threads=1
|
||||
run: cargo test --release --manifest-path apps/desktop/src-tauri/Cargo.toml --lib --locked -- --test-threads=1
|
||||
|
||||
- name: Import Apple signing certificate and notarization key
|
||||
if: matrix.macos
|
||||
|
||||
+3
-1
@@ -2,13 +2,15 @@
|
||||
|
||||
记录 Codex-X 的新功能、体验改进和问题修复。
|
||||
|
||||
## [v0.3.23] - 2026-10-01
|
||||
## [v0.3.24] - 2026-10-01
|
||||
|
||||
- 会话管理新增工作文件夹侧栏、文件夹与会话置顶,并使用虚拟列表减少大量会话的渲染开销。
|
||||
- 会话日志改为流式读取和磁盘快照处理,大日志及超长单条记录也能同步;读取、写入或复查失败时明确报错并执行安全回滚,避免日志、数据库索引与同步状态不一致。
|
||||
- 修复文件夹路径大小写混淆导致显示错误会话、置顶联动和缓存串用的问题,按实际文件系统身份区分目录或合并同目录别名。
|
||||
- 置顶按 Codex 配置目录分别保存,文件夹暂时不可读或重建后按原始路径保留置顶。旧 PR 预览版的 v1 置顶缓存不自动迁移,需重新置顶。
|
||||
|
||||
- 修复 Windows 下会话索引原子替换被文件句柄阻止的问题。
|
||||
|
||||
## [v0.3.22] - 2026-09-30
|
||||
|
||||
- 修复第三方模型被错误识别为“仅文本”而无法输入图片的问题,并自动升级旧版生成的模型目录。需供应商模型本身支持图片,更新菜单后重启 Codex 生效。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "codex-x",
|
||||
"version": "0.3.23",
|
||||
"version": "0.3.24",
|
||||
"private": true,
|
||||
"description": "Codex Switch & Instruct desktop manager",
|
||||
"type": "module",
|
||||
|
||||
Generated
+1
-1
@@ -492,7 +492,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "codex-x"
|
||||
version = "0.3.23"
|
||||
version = "0.3.24"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "codex-x"
|
||||
version = "0.3.23"
|
||||
version = "0.3.24"
|
||||
description = "Codex Switch & Instruct desktop manager"
|
||||
authors = ["yynxxxxx"]
|
||||
license = "MIT"
|
||||
|
||||
@@ -730,7 +730,7 @@ fn preflight_session_jsonl_cleanup(codex_dir: &Path) -> Result<()> {
|
||||
|
||||
fn commit_index_cleanup(
|
||||
path: &Path,
|
||||
source: &fs::File,
|
||||
source: fs::File,
|
||||
prepared: PreparedJsonlCleanup,
|
||||
) -> Result<usize> {
|
||||
if prepared.removed == 0 {
|
||||
@@ -738,7 +738,7 @@ fn commit_index_cleanup(
|
||||
}
|
||||
ensure_cleanup_source_unchanged(
|
||||
path,
|
||||
source,
|
||||
&source,
|
||||
&prepared.original_stamp,
|
||||
&prepared.original_hash,
|
||||
)?;
|
||||
@@ -750,11 +750,40 @@ fn commit_index_cleanup(
|
||||
.output
|
||||
.as_file()
|
||||
.set_permissions(permissions)
|
||||
.map_err(|error| io_err(path, error))?;
|
||||
prepared
|
||||
.output
|
||||
.persist(path)
|
||||
.map_err(|error| io_err(path, error.error))?;
|
||||
.map_err(|error| {
|
||||
io_err(
|
||||
path,
|
||||
std::io::Error::new(error.kind(), format!("设置临时会话索引权限失败: {error}")),
|
||||
)
|
||||
})?;
|
||||
// tempfile's Windows persist uses SetFileAttributesW followed by MoveFileExW.
|
||||
// Close both our destination and staging handles before that replacement;
|
||||
// the index has no history-style inode/append-lock requirement.
|
||||
drop(source);
|
||||
let output = prepared.output.into_temp_path();
|
||||
let verifier = fs::File::open(path).map_err(|error| {
|
||||
io_err(
|
||||
path,
|
||||
std::io::Error::new(error.kind(), format!("最终复核会话索引失败: {error}")),
|
||||
)
|
||||
})?;
|
||||
let unchanged = ensure_cleanup_source_unchanged(
|
||||
path,
|
||||
&verifier,
|
||||
&prepared.original_stamp,
|
||||
&prepared.original_hash,
|
||||
);
|
||||
drop(verifier);
|
||||
unchanged?;
|
||||
output.persist(path).map_err(|error| {
|
||||
io_err(
|
||||
path,
|
||||
std::io::Error::new(
|
||||
error.error.kind(),
|
||||
format!("原子替换会话索引失败: {}", error.error),
|
||||
),
|
||||
)
|
||||
})?;
|
||||
Ok(prepared.removed)
|
||||
}
|
||||
|
||||
@@ -769,7 +798,7 @@ fn remove_jsonl_session_entries(
|
||||
Err(error) => return Err(io_err(path, error)),
|
||||
};
|
||||
let prepared = prepare_jsonl_cleanup(path, &mut source, id_keys, session_ids, false)?;
|
||||
commit_index_cleanup(path, &source, prepared)
|
||||
commit_index_cleanup(path, source, prepared)
|
||||
}
|
||||
|
||||
fn remove_session_index_entries(codex_dir: &Path, session_ids: &HashSet<String>) -> Result<usize> {
|
||||
@@ -1777,22 +1806,100 @@ mod tests {
|
||||
drop(external);
|
||||
let grown = fs::read(&path).unwrap();
|
||||
let result = if history {
|
||||
commit_history_cleanup_with_writer(&path, &mut source, prepared, |_, _, _, _| {
|
||||
panic!("growth must be rejected before writing history")
|
||||
})
|
||||
let result = commit_history_cleanup_with_writer(
|
||||
&path,
|
||||
&mut source,
|
||||
prepared,
|
||||
|_, _, _, _| panic!("growth must be rejected before writing history"),
|
||||
);
|
||||
drop(source);
|
||||
result
|
||||
} else {
|
||||
commit_index_cleanup(&path, &source, prepared)
|
||||
commit_index_cleanup(&path, source, prepared)
|
||||
};
|
||||
assert!(result
|
||||
.expect_err("a stale staged result must not replace appended data")
|
||||
.to_string()
|
||||
.contains("发生变化"));
|
||||
assert_eq!(fs::read(&path).unwrap(), grown);
|
||||
drop(source);
|
||||
fs::remove_dir_all(codex_dir).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn index_replacement_closes_a_delete_denying_source_handle_and_preserves_all_kept_bytes() {
|
||||
use std::os::windows::fs::OpenOptionsExt;
|
||||
use windows_sys::Win32::Storage::FileSystem::{FILE_SHARE_READ, FILE_SHARE_WRITE};
|
||||
|
||||
let codex_dir = temp_codex_dir();
|
||||
let path = codex_dir.join("session_index.jsonl");
|
||||
fs::write(
|
||||
&path,
|
||||
b"{\"id\":\"selected\"}\r\nnot-json\r\n{\"id\":\"kept\"}",
|
||||
)
|
||||
.unwrap();
|
||||
let mut initial = fs::File::open(&path).unwrap();
|
||||
let prepared = prepare_jsonl_cleanup(
|
||||
&path,
|
||||
&mut initial,
|
||||
&["id"],
|
||||
&HashSet::from(["selected".to_string()]),
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
drop(initial);
|
||||
// Allow all verification reads while denying DELETE sharing, which is
|
||||
// specifically required for Windows rename/replacement operations.
|
||||
let source = fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE)
|
||||
.open(&path)
|
||||
.unwrap();
|
||||
assert_eq!(commit_index_cleanup(&path, source, prepared).unwrap(), 1);
|
||||
assert_eq!(fs::read(&path).unwrap(), b"not-json\r\n{\"id\":\"kept\"}");
|
||||
fs::remove_dir_all(codex_dir).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn index_replacement_rejects_same_length_modified_bytes_even_with_restored_mtime() {
|
||||
let codex_dir = temp_codex_dir();
|
||||
let path = codex_dir.join("session_index.jsonl");
|
||||
let original = b"{\"id\":\"selected\"}\n{\"id\":\"kept\"}\n";
|
||||
fs::write(&path, original).unwrap();
|
||||
let mut source = fs::File::open(&path).unwrap();
|
||||
let prepared = prepare_jsonl_cleanup(
|
||||
&path,
|
||||
&mut source,
|
||||
&["id"],
|
||||
&HashSet::from(["selected".to_string()]),
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
let mtime = prepared.original_stamp.modified.unwrap();
|
||||
let mut changed = original.to_vec();
|
||||
let last = changed.iter().rposition(|byte| *byte == b't').unwrap();
|
||||
changed[last] = b'x';
|
||||
let mut external = fs::OpenOptions::new().write(true).open(&path).unwrap();
|
||||
external.write_all(&changed).unwrap();
|
||||
external.sync_all().unwrap();
|
||||
drop(external);
|
||||
// Set the timestamp through a fresh handle after closing the data writer;
|
||||
// Windows may otherwise finalize a pending write time when it closes.
|
||||
let timestamp = fs::OpenOptions::new().write(true).open(&path).unwrap();
|
||||
timestamp
|
||||
.set_times(fs::FileTimes::new().set_modified(mtime))
|
||||
.unwrap();
|
||||
drop(timestamp);
|
||||
assert_eq!(fs::metadata(&path).unwrap().modified().unwrap(), mtime);
|
||||
assert_eq!(fs::metadata(&path).unwrap().len(), original.len() as u64);
|
||||
let error = commit_index_cleanup(&path, source, prepared)
|
||||
.expect_err("a matching length and timestamp must not bypass the source digest");
|
||||
assert!(error.to_string().contains("发生变化"));
|
||||
assert_eq!(fs::read(&path).unwrap(), changed);
|
||||
fs::remove_dir_all(codex_dir).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_commit_write_failure_restores_backup_and_keeps_its_lock_and_inode() {
|
||||
for fail_after_truncate in [false, true] {
|
||||
|
||||
@@ -366,7 +366,9 @@ mod tests {
|
||||
#[test]
|
||||
fn creation_generation_includes_epoch_sign_full_seconds_and_nanoseconds() {
|
||||
use std::time::Duration;
|
||||
let created = UNIX_EPOCH + Duration::new(1234, 17);
|
||||
// Windows SystemTime uses 100 ns intervals; use exactly representable
|
||||
// fractions while still testing that subsecond birth precision matters.
|
||||
let created = UNIX_EPOCH + Duration::new(1234, 100);
|
||||
let original = unix_directory_identity(1, 9, Some(created)).unwrap();
|
||||
assert!(original.birth_verified);
|
||||
assert_eq!(
|
||||
@@ -374,9 +376,9 @@ mod tests {
|
||||
unix_directory_identity(1, 9, Some(created)).unwrap()
|
||||
);
|
||||
for new_generation in [
|
||||
UNIX_EPOCH + Duration::new(1234, 18),
|
||||
UNIX_EPOCH + Duration::new(1234 + (1u64 << 32), 17),
|
||||
UNIX_EPOCH - Duration::new(1234, 17),
|
||||
UNIX_EPOCH + Duration::new(1234, 200),
|
||||
UNIX_EPOCH + Duration::new(1234 + (1u64 << 32), 100),
|
||||
UNIX_EPOCH - Duration::new(1234, 100),
|
||||
] {
|
||||
assert_ne!(
|
||||
original,
|
||||
@@ -387,7 +389,7 @@ mod tests {
|
||||
let file_id = [7; 16];
|
||||
assert_ne!(
|
||||
windows_directory_identity(1, &file_id, Some(created)),
|
||||
windows_directory_identity(1, &file_id, Some(created + Duration::from_nanos(1)))
|
||||
windows_directory_identity(1, &file_id, Some(created + Duration::from_nanos(100)))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Codex-X",
|
||||
"version": "0.3.23",
|
||||
"version": "0.3.24",
|
||||
"identifier": "com.yynxxxxx.codexx",
|
||||
"build": {
|
||||
"frontendDist": "../dist",
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "codex-x-workspace",
|
||||
"private": true,
|
||||
"version": "0.3.23",
|
||||
"version": "0.3.24",
|
||||
"scripts": {
|
||||
"dev": "pnpm --dir apps/desktop tauri dev",
|
||||
"build": "pnpm --dir apps/desktop tauri build",
|
||||
|
||||
Reference in New Issue
Block a user