新增 direct-codex 回合终止与推理档写入命令
新增 cancel_direct_codex_turn:按项目登记正在运行的 direct-codex 回合句柄,终止时复用 CodexTurnStartCancellation 只发 turn/interrupt;无活动回合、clientTurnId 不匹配、进程已退出都返回可读原因。 新增 select_game_creator_reasoning_effort:按 select_game_creator_model 的既有配置写入模式,只改 llm.reasoningEffort 后落盘,读接口语义不变。 codex_app_server.rs 追加 DirectCodexActiveTurnTable 注册表与回合登记/注销 guard,并补 2 个单测(选中/注销哪一轮、注册键归一化);既有事件与命令语义不动。
This commit is contained in:
@@ -35,7 +35,8 @@ mod runtime_tools;
|
||||
mod skill_pack;
|
||||
use codex_app_server::*;
|
||||
pub(crate) use codex_app_server::{
|
||||
direct_game_creator_codex_chat_at, direct_game_creator_home_codex_chat,
|
||||
cancel_direct_codex_turn_at, direct_game_creator_codex_chat_at,
|
||||
direct_game_creator_home_codex_chat,
|
||||
};
|
||||
use codex_cli::*;
|
||||
pub(crate) use codex_cli::{
|
||||
|
||||
@@ -216,6 +216,12 @@ impl CodexTurnStartCancellation {
|
||||
self.maybe_interrupt();
|
||||
}
|
||||
|
||||
/// app-server 连接是否还活着:句柄只剩 Weak 时说明进程已被回收,此时"终止"必须
|
||||
/// 明确报错,而不是静默成功让界面以为回合已经停了。
|
||||
fn app_server_alive(&self) -> bool {
|
||||
self.inner.strong_count() > 0
|
||||
}
|
||||
|
||||
fn cancel(&self) {
|
||||
self.cancelled.store(true, Ordering::Release);
|
||||
self.maybe_interrupt();
|
||||
@@ -2752,6 +2758,17 @@ impl CodexAppServerConnection {
|
||||
}
|
||||
let turn_start_cancellation =
|
||||
Arc::new(CodexTurnStartCancellation::new(&self.inner, &thread_id));
|
||||
// Direct 回合登记为"可终止":终止命令只作用在这一轮上,回合结束时自动注销。
|
||||
let _active_turn_guard = direct_tool_call_turn_id
|
||||
.as_deref()
|
||||
.filter(|_| self.inner.workspace_mode == CodexAppServerWorkspaceMode::DirectProject)
|
||||
.map(|turn_id| {
|
||||
register_active_direct_codex_turn(
|
||||
direct_codex_active_turn_key(history_root),
|
||||
turn_id,
|
||||
Arc::clone(&turn_start_cancellation),
|
||||
)
|
||||
});
|
||||
let mut turn_start_guard = CodexTurnStartGuard {
|
||||
cancellation: Arc::clone(&turn_start_cancellation),
|
||||
armed: true,
|
||||
@@ -3059,6 +3076,135 @@ impl Drop for CodexTurnStartGuard {
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct 回合中断表:与具体取消句柄解耦的最小实现,"选哪一轮 / 注销哪一轮"可单测。
|
||||
struct DirectCodexActiveTurnTable<T> {
|
||||
entries: HashMap<std::path::PathBuf, (String, T)>,
|
||||
}
|
||||
|
||||
impl<T> DirectCodexActiveTurnTable<T> {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
entries: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn register(&mut self, key: std::path::PathBuf, client_turn_id: &str, value: T) {
|
||||
self.entries
|
||||
.insert(key, (client_turn_id.to_string(), value));
|
||||
}
|
||||
|
||||
/// 只有当前登记项仍是本回合的句柄时才注销,避免旧回合的收尾清掉后来注册的回合。
|
||||
fn unregister(&mut self, key: &Path, is_same: impl Fn(&T) -> bool) {
|
||||
if self
|
||||
.entries
|
||||
.get(key)
|
||||
.is_some_and(|(_, value)| is_same(value))
|
||||
{
|
||||
self.entries.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
/// 选中要终止的回合:没有活动回合、或前端给的 clientTurnId 与活动回合不一致时都返回
|
||||
/// 可读原因,绝不误伤另一个回合。
|
||||
fn select(&self, key: &Path, client_turn_id: Option<&str>) -> Result<&(String, T), String> {
|
||||
let active = self
|
||||
.entries
|
||||
.get(key)
|
||||
.ok_or_else(|| "当前项目没有正在运行的陶泥儿回合,无法终止".to_string())?;
|
||||
if let Some(expected) = client_turn_id
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
if active.0 != expected {
|
||||
return Err("正在运行的是另一个陶泥儿回合,已拒绝终止".to_string());
|
||||
}
|
||||
}
|
||||
Ok(active)
|
||||
}
|
||||
}
|
||||
|
||||
/// 正在运行的 Direct 回合中断句柄,按项目根(canonical,去掉 Windows `\\?\` 前缀)索引。
|
||||
///
|
||||
/// `CodexTurnStartCancellation` 本身已经能在 turn/start 响应到达**前后**发出
|
||||
/// `turn/interrupt`;这里只是把它留一个 Tauri 命令取得到的引用,回合结束后由
|
||||
/// [`DirectCodexActiveTurnGuard`] 移除。只做新增:不改既有事件、命令语义。
|
||||
static GAME_CREATOR_DIRECT_CODEX_ACTIVE_TURNS: OnceLock<
|
||||
std::sync::Mutex<DirectCodexActiveTurnTable<Arc<CodexTurnStartCancellation>>>,
|
||||
> = OnceLock::new();
|
||||
|
||||
fn direct_codex_active_turns(
|
||||
) -> &'static std::sync::Mutex<DirectCodexActiveTurnTable<Arc<CodexTurnStartCancellation>>> {
|
||||
GAME_CREATOR_DIRECT_CODEX_ACTIVE_TURNS
|
||||
.get_or_init(|| std::sync::Mutex::new(DirectCodexActiveTurnTable::new()))
|
||||
}
|
||||
|
||||
/// 注册键:与 Direct 回合用的 `codex_root` 同一形态(canonical 且去掉 `\\?\` 前缀),
|
||||
/// 这样前端传进来的项目路径与注册时的路径一定落到同一个键上。
|
||||
fn direct_codex_active_turn_key(root: &Path) -> std::path::PathBuf {
|
||||
let canonical = std::fs::canonicalize(root).unwrap_or_else(|_| root.to_path_buf());
|
||||
match canonical
|
||||
.to_str()
|
||||
.and_then(|value| value.strip_prefix("\\\\?\\"))
|
||||
{
|
||||
Some(stripped) => std::path::PathBuf::from(stripped),
|
||||
None => canonical,
|
||||
}
|
||||
}
|
||||
|
||||
struct DirectCodexActiveTurnGuard {
|
||||
key: std::path::PathBuf,
|
||||
cancellation: Arc<CodexTurnStartCancellation>,
|
||||
}
|
||||
|
||||
impl Drop for DirectCodexActiveTurnGuard {
|
||||
fn drop(&mut self) {
|
||||
let Some(active_turns) = GAME_CREATOR_DIRECT_CODEX_ACTIVE_TURNS.get() else {
|
||||
return;
|
||||
};
|
||||
let Ok(mut entries) = active_turns.lock() else {
|
||||
return;
|
||||
};
|
||||
let cancellation = Arc::clone(&self.cancellation);
|
||||
entries.unregister(&self.key, |current| Arc::ptr_eq(current, &cancellation));
|
||||
}
|
||||
}
|
||||
|
||||
/// 把一个 Direct 回合登记为"可终止",返回的 guard 在回合结束时注销它。
|
||||
fn register_active_direct_codex_turn(
|
||||
key: std::path::PathBuf,
|
||||
client_turn_id: &str,
|
||||
cancellation: Arc<CodexTurnStartCancellation>,
|
||||
) -> DirectCodexActiveTurnGuard {
|
||||
if let Ok(mut entries) = direct_codex_active_turns().lock() {
|
||||
entries.register(key.clone(), client_turn_id, Arc::clone(&cancellation));
|
||||
}
|
||||
DirectCodexActiveTurnGuard { key, cancellation }
|
||||
}
|
||||
|
||||
/// 终止当前项目正在运行的 Direct 回合。
|
||||
///
|
||||
/// 只向正在跑的 Codex app-server 回合发 `turn/interrupt`(app-server 随后回
|
||||
/// `turn/completed status=interrupted`,正在 await 的那个回合命令会带着可读原因返回),
|
||||
/// 不动任何既有事件或命令语义。
|
||||
pub(crate) fn cancel_direct_codex_turn_at(
|
||||
root: &Path,
|
||||
client_turn_id: Option<&str>,
|
||||
) -> Result<(), String> {
|
||||
let key = direct_codex_active_turn_key(root);
|
||||
let cancellation = {
|
||||
let entries = direct_codex_active_turns()
|
||||
.lock()
|
||||
.map_err(|_| "Direct 回合中断表已损坏,无法终止".to_string())?;
|
||||
let (_, cancellation) = entries.select(&key, client_turn_id)?;
|
||||
if !cancellation.app_server_alive() {
|
||||
return Err("陶泥儿执行进程已退出,无法终止本轮;请重新发送这条消息".to_string());
|
||||
}
|
||||
Arc::clone(cancellation)
|
||||
};
|
||||
cancellation.cancel();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct CodexThreadLease {
|
||||
connection: CodexAppServerConnection,
|
||||
key: CodexNodeThreadKey,
|
||||
@@ -4071,6 +4217,52 @@ pub(crate) fn build_direct_codex_history_prompt(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// 终止只作用在"当前项目正在跑的那一轮"上:没有活动回合 / clientTurnId 不匹配都要
|
||||
/// 返回可读原因,不能误伤别人;注销也只注销本回合自己的句柄。
|
||||
#[test]
|
||||
fn direct_codex_active_turn_table_selects_only_the_running_turn() {
|
||||
let mut table: DirectCodexActiveTurnTable<u8> = DirectCodexActiveTurnTable::new();
|
||||
let key = std::path::PathBuf::from("C:/projects/direct-turn-demo");
|
||||
assert_eq!(
|
||||
table.select(&key, None).expect_err("no active turn"),
|
||||
"当前项目没有正在运行的陶泥儿回合,无法终止"
|
||||
);
|
||||
|
||||
table.register(key.clone(), "turn-a", 1);
|
||||
assert_eq!(table.select(&key, None).expect("active turn").0, "turn-a");
|
||||
assert_eq!(table.select(&key, Some("turn-a")).expect("same turn").1, 1);
|
||||
assert_eq!(
|
||||
table
|
||||
.select(&key, Some("turn-b"))
|
||||
.expect_err("another running turn"),
|
||||
"正在运行的是另一个陶泥儿回合,已拒绝终止"
|
||||
);
|
||||
|
||||
// 句柄已被后来的回合替换:旧回合收尾不得注销新回合。
|
||||
table.register(key.clone(), "turn-b", 2);
|
||||
table.unregister(&key, |value| *value == 1);
|
||||
assert_eq!(table.select(&key, None).expect("newer turn").0, "turn-b");
|
||||
table.unregister(&key, |value| *value == 2);
|
||||
assert!(table.select(&key, None).is_err());
|
||||
}
|
||||
|
||||
/// 注册键:前端传的项目路径与回合注册时的路径必须归一化成同一个键(Windows 上
|
||||
/// `canonicalize` 会带 `\\?\` 前缀,去掉后两边才相等)。
|
||||
#[test]
|
||||
fn direct_codex_active_turn_key_normalizes_windows_prefix() {
|
||||
let root = tempfile::tempdir().expect("temp dir");
|
||||
let canonical = std::fs::canonicalize(root.path()).expect("canonical root");
|
||||
let expected = canonical
|
||||
.to_str()
|
||||
.and_then(|value| value.strip_prefix("\\\\?\\"))
|
||||
.map(std::path::PathBuf::from)
|
||||
.unwrap_or(canonical);
|
||||
let key = direct_codex_active_turn_key(root.path());
|
||||
assert_eq!(key, expected);
|
||||
// 归一化后的键不再带 Windows 扩展长度前缀:前端传进来的普通路径才能命中同一个键。
|
||||
assert!(!key.to_string_lossy().starts_with("\\\\?\\"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direct_item_activities_are_closed_safe_categories() {
|
||||
let allowed = [
|
||||
|
||||
@@ -1989,6 +1989,29 @@ pub(crate) fn write_game_creator_app_config(
|
||||
persist_game_creator_app_config(config, overlays, false)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) fn cancel_direct_codex_turn(
|
||||
project_path: String,
|
||||
client_turn_id: Option<String>,
|
||||
) -> Result<(), String> {
|
||||
let root = Path::new(project_path.trim());
|
||||
enforce_project_permission_policy(root, "agent.kill")?;
|
||||
cancel_direct_codex_turn_at(root, client_turn_id.as_deref())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) fn select_game_creator_reasoning_effort(
|
||||
effort: String,
|
||||
) -> Result<GameCreatorAppConfigView, String> {
|
||||
let _guard = GAME_CREATOR_CONFIG_WRITE_LOCK
|
||||
.lock()
|
||||
.map_err(|_| "配置写入锁不可用")?;
|
||||
let effort = game_creator_llm_reasoning_effort_name(&effort, "llm.reasoningEffort")?;
|
||||
let (mut config, overlays) = load_game_creator_app_config_for_write()?;
|
||||
config.llm.reasoning_effort = effort;
|
||||
persist_game_creator_app_config(config, overlays, false)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) fn select_game_creator_model(
|
||||
model_id: String,
|
||||
|
||||
@@ -2632,6 +2632,8 @@ fn main() {
|
||||
chat_with_game_creator_role_agent,
|
||||
chat_with_game_creator_role_agent_stream,
|
||||
chat_with_game_creator_direct_codex,
|
||||
cancel_direct_codex_turn,
|
||||
select_game_creator_reasoning_effort,
|
||||
start_planning_session_v2,
|
||||
continue_planning_session_v2,
|
||||
decide_planning_artifact_v2,
|
||||
|
||||
Reference in New Issue
Block a user