diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent.rs b/apps/ai-game-creator-shell/src-tauri/src/agent.rs index 347ec3139..781aad21d 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent.rs @@ -29,6 +29,7 @@ mod direct_project_history; mod direct_project_turn_history; mod direct_runtime; mod direct_thread_manager; +mod direct_thread_queue; mod direct_thread_wire; mod direct_tool_bridge; mod direct_tool_calls; @@ -65,6 +66,7 @@ pub(crate) use direct_project_history::*; pub(crate) use direct_project_turn_history::*; pub(crate) use direct_runtime::*; pub(crate) use direct_thread_manager::*; +pub(crate) use direct_thread_queue::*; pub(crate) use direct_thread_wire::*; pub(crate) use direct_tool_bridge::*; pub(crate) use direct_tool_calls::*; diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_user_item/model.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_user_item/model.rs index b65f25ba4..2b537d5f6 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_user_item/model.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_user_item/model.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; use ts_rs::TS; /// DirectProject 本轮 user input 的唯一结构化入口。 -#[derive(Clone, Debug, Deserialize, Serialize, TS)] +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, TS)] #[serde(tag = "type", deny_unknown_fields)] #[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/chat/generated/"))] pub(crate) enum DirectCodexUserItem { @@ -10,7 +10,7 @@ pub(crate) enum DirectCodexUserItem { Message(DirectCodexUserMessageItem), } -#[derive(Clone, Debug, Deserialize, Serialize, TS)] +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, TS)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/chat/generated/"))] pub(crate) struct DirectCodexUserMessageItem { @@ -19,14 +19,14 @@ pub(crate) struct DirectCodexUserMessageItem { pub(crate) id: String, } -#[derive(Clone, Debug, Deserialize, Serialize, TS)] +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, TS)] #[serde(rename_all = "lowercase")] #[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/chat/generated/"))] pub(crate) enum DirectCodexUserRole { User, } -#[derive(Clone, Debug, Deserialize, Serialize, TS)] +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, TS)] #[serde(tag = "type", rename_all_fields = "camelCase", deny_unknown_fields)] #[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/chat/generated/"))] pub(crate) enum DirectCodexUserContentPart { @@ -43,7 +43,7 @@ pub(crate) enum DirectCodexUserContentPart { AgcAttachmentReference(DirectCodexUserAttachmentReferencePart), } -#[derive(Clone, Debug, Deserialize, Serialize, TS)] +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, TS)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/chat/generated/"))] pub(crate) struct DirectCodexUserAttachmentReferencePart { @@ -55,7 +55,7 @@ pub(crate) struct DirectCodexUserAttachmentReferencePart { pub(crate) status: String, } -#[derive(Clone, Debug, Deserialize, Serialize, TS)] +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, TS)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/chat/generated/"))] pub(crate) struct DirectCodexUserRuntimeRegionPart { diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_thread_manager.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_thread_manager.rs index 2b624df55..2e66f086c 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_thread_manager.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_thread_manager.rs @@ -428,6 +428,14 @@ impl DirectThreadManager { true } DirectThreadEvent::ItemDelta { .. } => true, + // 待发消息在队期间不可回收:队列的成员与顺序**就是**这条事件本身 + // (见 `direct_thread_queue`)。bootstrap 因此天然看得见当前队列。 + DirectThreadEvent::QueueEnqueued { .. } => false, + // 离开队列(取消 / 放行)才把同一条待发消息的入队事件一起转成可回收。 + DirectThreadEvent::QueueRemoved { client_turn_id, .. } => { + Self::mark_queue_events_cleanable(thread, client_turn_id); + true + } } } @@ -471,6 +479,15 @@ impl DirectThreadManager { } } + /// 一条待发消息离开队列:它和它对应的入队事件一起变成可回收。 + fn mark_queue_events_cleanable(thread: &mut ThreadState, client_turn_id: &str) { + for stored in &mut thread.events { + if stored.event.queue_client_turn_id() == Some(client_turn_id) { + stored.cleanable = true; + } + } + } + fn trim_prefix(thread: &mut ThreadState) { let min_cursor = thread .subscribers diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_thread_queue.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_thread_queue.rs new file mode 100644 index 000000000..2785c87cb --- /dev/null +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_thread_queue.rs @@ -0,0 +1,200 @@ +//! DirectProject 待发消息队列的条目与规则。 +//! +//! 队列的成员与顺序**就是 Thread Manager 的事件列表本身**:一条待发消息在队期间,它的 +//! `queue.enqueued` 事件不可回收;离开队列(取消或放行)时才转成可回收。新订阅者的 bootstrap 因此 +//! 天然看得见当前队列,不需要第二张队列表,也不会有"事件与队列不一致"的窗口。 +//! +//! 这个模块只放三件事:一条待发消息带走什么([`PendingDirectTurn`])、容量规则 +//! ([`MAX_PENDING_DIRECT_TURNS`])、以及它在线上长什么样([`PendingDirectTurn::enqueued_event`])。 +//! 它不碰锁、不碰 Tauri、不写盘:入队检查在命令侧,放行顺序在 Thread Manager。 + +use serde_json::Value; + +use crate::agent::{ + direct_codex_user_item_id_for_client_turn_id, DirectCodexUserItem, DirectThreadEvent, +}; + +/// 一个项目最多能同时排队的待发消息条数。 +/// +/// 只数**在队**条目,不算正在跑的那一轮。上限只落在宿主这一处:前端不再自己数,满队由命令返回 +/// typed 入队失败。 +pub(crate) const MAX_PENDING_DIRECT_TURNS: usize = 5; + +/// 一条已经通过入队检查、正在等放行的用户消息。 +/// +/// 只在内存里,进程重启即消失(与 ADR 记的边界一致)。它同时是**放行时要用的全部输入**:放行 +/// 没有失败出口,所以检查产物在入队时就地冻结,放行只搬运、不重算。 +#[derive(Clone, Debug)] +pub(crate) struct PendingDirectTurn { + /// 这条消息的回合身份;放行后同一轮的 `turn.started` / `turn.completed` 用它。 + pub(crate) client_turn_id: String, + /// canonical 用户条目:事件与界面 chip 都读它,Rust 不渲染展示形状。 + pub(crate) user_item: DirectCodexUserItem, + /// canonical 用户条目的 JSON 形状:入队时算好,放行时直接落盘。 + pub(crate) canonical_user_item: Value, + /// 入队检查产出的 prompt:放行不重算。 + pub(crate) prompt: String, + pub(crate) creation_type: Option, + /// 入队那一刻的宿主毫秒钟。 + pub(crate) at: u64, +} + +impl PendingDirectTurn { + /// 组一条待发消息:入队检查已经全部通过,这里只把放行要用的产物冻结下来。 + /// + /// 冻结是刻意的:放行没有失败出口,所以任何可能在放行时才失败的计算都必须提前到这里 + /// (canonical 形状与 prompt 都是)。 + pub(crate) fn prepare( + client_turn_id: String, + user_item: DirectCodexUserItem, + prompt: String, + creation_type: Option, + at: u64, + ) -> Result { + let canonical_user_item = serde_json::to_value(&user_item)?; + Ok(Self { + client_turn_id, + user_item, + canonical_user_item, + prompt, + creation_type, + at, + }) + } + + /// 入队事件的投影:带 canonical 用户条目与 `creationType`,**不带 prompt**(prompt 只留在宿主的 + /// 队列条目里,它不是要下发的展示形状)。 + pub(crate) fn enqueued_event(&self) -> DirectThreadEvent { + DirectThreadEvent::queue_enqueued( + self.client_turn_id.clone(), + self.user_item.clone(), + self.creation_type.clone(), + self.at, + ) + } + + /// 这条待发消息在历史里的用户条目 id:前端用它把 chip 与回合边界对上。 + pub(crate) fn user_item_id(&self) -> Option { + direct_codex_user_item_id_for_client_turn_id(&self.client_turn_id) + } +} + +/// 入队的结果。 +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum EnqueueOutcome { + /// 这次真的排进队尾了。 + Enqueued, + /// 同一 `clientTurnId` 已经在队(或正在跑):按幂等返回成功,不排第二条、不发事件。 + AlreadyKnown, +} + +/// 入队被检查挡下来的原因。满队之外的原因由入队半自己的 typed 错误表达。 +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum EnqueueRejection { + /// 在队条目已达 [`MAX_PENDING_DIRECT_TURNS`]。 + QueueFull, +} + +/// 队列还有没有位置。`pending_count` 只数在队条目。 +pub(crate) fn queue_has_room(pending_count: usize) -> Result<(), EnqueueRejection> { + if pending_count >= MAX_PENDING_DIRECT_TURNS { + return Err(EnqueueRejection::QueueFull); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + fn user_item(text: &str, id: &str) -> DirectCodexUserItem { + serde_json::from_value(json!({ + "type": "message", + "role": "user", + "content": [{"type": "input_text", "text": text}], + "id": id, + })) + .expect("canonical user item") + } + + fn pending(client_turn_id: &str, creation_type: Option<&str>) -> PendingDirectTurn { + PendingDirectTurn::prepare( + client_turn_id.to_string(), + user_item("生成一个游戏", "direct-codex:turn-1:user"), + "生成一个游戏".to_string(), + creation_type.map(str::to_string), + 1_700_000_000_123, + ) + .expect("prepare pending turn") + } + + /// 入队事件只带 canonical 用户条目与 `creationType`:prompt 是宿主的入队检查产物, + /// 不许顺着事件下发。 + #[test] + fn enqueued_event_carries_the_canonical_item_and_no_prompt() { + let event = pending("turn-1", Some("web-game")).enqueued_event(); + let value = serde_json::to_value(&event).expect("serialize queue.enqueued"); + assert_eq!( + value, + json!({ + "type": "queue.enqueued", + "clientTurnId": "turn-1", + "userItem": { + "type": "message", + "role": "user", + "content": [{"type": "input_text", "text": "生成一个游戏"}], + "id": "direct-codex:turn-1:user", + }, + "creationType": "web-game", + "at": 1_700_000_000_123u64, + }) + ); + assert!(value.get("prompt").is_none(), "{value}"); + + // 没有创建类型时不写字段,也不补 `null`。 + let bare = serde_json::to_value(pending("turn-2", None).enqueued_event()) + .expect("serialize queue.enqueued without creation type"); + assert!(bare.get("creationType").is_none(), "{bare}"); + + // 事件读得出自己的待发消息身份。 + assert_eq!(event.queue_client_turn_id(), Some("turn-1")); + assert_eq!(event.queue_removal_reason(), None); + } + + /// canonical 形状在入队时就冻结:放行时落盘的就是这一份,不再重算。 + #[test] + fn prepare_freezes_the_canonical_item() { + let turn = pending("turn-1", None); + assert_eq!( + turn.canonical_user_item["id"], + json!("direct-codex:turn-1:user") + ); + assert_eq!( + turn.canonical_user_item, + serde_json::to_value(&turn.user_item).expect("serialize user item") + ); + } + + /// 用户条目身份由 `clientTurnId` 派生,与落盘 / 下发用的是同一个函数。 + #[test] + fn user_item_id_derives_from_the_client_turn_id() { + assert_eq!( + pending("turn-1", None).user_item_id().as_deref(), + Some("direct-codex:turn-1:user") + ); + assert_eq!(pending(" ", None).user_item_id(), None); + } + + /// 容量只数在队条目,5 条封口;在跑的那一轮不算进去。 + #[test] + fn capacity_closes_at_five_pending_turns() { + for count in 0..MAX_PENDING_DIRECT_TURNS { + assert_eq!(queue_has_room(count), Ok(()), "{count} 条时仍该有位置"); + } + assert_eq!( + queue_has_room(MAX_PENDING_DIRECT_TURNS), + Err(EnqueueRejection::QueueFull) + ); + } +} diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_thread_wire.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_thread_wire.rs index 952dd3a24..a4649938e 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_thread_wire.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_thread_wire.rs @@ -13,6 +13,7 @@ use crate::agent::redact_secret_tokens; use crate::agent::sanitize_error_context; +use crate::agent::DirectCodexUserItem; use crate::agent::DirectTurnFailureKind; use crate::redact_absolute_path_tokens; use serde::{Deserialize, Serialize}; @@ -212,6 +213,33 @@ impl DirectThreadRequestKind { } } +/// 一条待发消息离开队列的原因。 +/// +/// typed 枚举,取值即语义:取消是用户在输入盒上撤掉这条消息,放行是它已经接单并成为回合 +/// (同一临界区里另有 `turn.started`)。界面按它分流,不解析字符串。 +#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/chat/generated/"))] +pub(crate) enum DirectQueueRemovalReason { + /// 用户取消了这条待发消息。 + Cancelled, + /// 放行:这条待发消息已经接单并成为回合。 + Dispatched, +} + +/// 取消一条待发消息的结果。 +#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/chat/generated/"))] +pub(crate) enum DirectQueueRemovalOutcome { + /// 已从队列移除。 + Removed, + /// 这条消息已经被放行(正在跑的那一轮就是它),不能按待发消息取消。 + AlreadyDispatched, + /// 队列里没有这个身份,也没有在跑的一轮是它。 + NotFound, +} + /// 失败终态的可下发载荷(`turn.completed.status == "failed"` 时必有,其余终态没有)。 /// /// `kind` 是稳定分类,只给界面选语气,不参与流程分支;`message` 是**已在宿主侧脱敏并截断**的 @@ -259,7 +287,7 @@ impl DirectTurnFailure { /// 不带回合身份,这个字段只用来把"这一轮的边界属于哪条用户消息"讲清楚:前端在只有生命周期锚点 /// + 历史切片、运行态一直为空时也能按身份认领开口条目,不必靠时间戳猜。缺失表示身份不可证明 /// (旧事件、没有开口用户条目、取消时拿不到 clientTurnId),此时前端不得补造。 -#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, TS)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS)] #[serde(tag = "type", rename_all_fields = "camelCase", deny_unknown_fields)] #[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/chat/generated/"))] pub(crate) enum DirectThreadEvent { @@ -321,6 +349,34 @@ pub(crate) enum DirectThreadEvent { #[serde(default)] request_id: Option, }, + /// 待发消息入队:数组顺序就是队首到队尾的顺序。 + /// + /// 这条事件在条目仍在队期间**不可回收**,离开队列(取消或放行)时才转成可回收——新订阅者 + /// 靠这一点在 bootstrap 里看到当前队列,`is_bootstrap_event` 不需要为它加特例。 + /// + /// 事件不带 prompt:prompt 是入队检查的产物,只留在宿主的队列条目里。 + #[serde(rename = "queue.enqueued")] + QueueEnqueued { + /// 这条待发消息的回合身份;放行后同一轮的 `turn.started` / `turn.completed` 用它。 + client_turn_id: String, + /// canonical 用户条目:前端据此派生 chip 文案,Rust 不渲染展示形状。 + user_item: DirectCodexUserItem, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + creation_type: Option, + /// 入队那一刻的宿主毫秒钟。 + #[ts(as = "f64")] + at: u64, + }, + /// 待发消息离开队列:`reason` 是取消还是放行。 + #[serde(rename = "queue.removed")] + QueueRemoved { + client_turn_id: String, + reason: DirectQueueRemovalReason, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional, as = "Option")] + at: Option, + }, } impl DirectThreadEvent { @@ -417,6 +473,51 @@ impl DirectThreadEvent { Self::Request { kind, request_id } } + /// 待发消息入队事件。 + pub(crate) fn queue_enqueued( + client_turn_id: String, + user_item: DirectCodexUserItem, + creation_type: Option, + at: u64, + ) -> Self { + Self::QueueEnqueued { + client_turn_id, + user_item, + creation_type, + at, + } + } + + /// 待发消息离开队列事件。 + pub(crate) fn queue_removed( + client_turn_id: String, + reason: DirectQueueRemovalReason, + at: u64, + ) -> Self { + Self::QueueRemoved { + client_turn_id, + reason, + at: Some(at), + } + } + + /// 这条事件属于哪条待发消息:只有队列事件有。 + pub(crate) fn queue_client_turn_id(&self) -> Option<&str> { + match self { + Self::QueueEnqueued { client_turn_id, .. } + | Self::QueueRemoved { client_turn_id, .. } => Some(client_turn_id), + _ => None, + } + } + + /// 待发消息离开队列的原因:只有 `queue.removed` 有。 + pub(crate) fn queue_removal_reason(&self) -> Option { + match self { + Self::QueueRemoved { reason, .. } => Some(*reason), + _ => None, + } + } + /// 事件级阶段时间(毫秒):只有四种生命周期事件有,其余事件返回 `None`。 /// /// 只读已存入事件的值,不在读取时取钟——重放要用的就是原事件的时间。 @@ -427,6 +528,8 @@ impl DirectThreadEvent { | Self::TurnCompleted { at, .. } | Self::ItemStarted { at, .. } | Self::ItemCompleted { at, .. } => *at, + Self::QueueEnqueued { at, .. } => Some(*at), + Self::QueueRemoved { at, .. } => *at, Self::ItemDelta { .. } | Self::Request { .. } => None, } } @@ -456,7 +559,7 @@ impl DirectThreadEvent { } } -#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, TS)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/chat/generated/"))] pub(crate) struct DirectThreadSubscriptionBootstrap { @@ -468,7 +571,7 @@ pub(crate) struct DirectThreadSubscriptionBootstrap { pub(crate) events: Vec, } -#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, TS)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/chat/generated/"))] pub(crate) struct DirectThreadConsumeResult { @@ -1466,4 +1569,61 @@ mod tests { .expect("failed turn without failure payload"); assert_eq!(sparse.failure(), None); } + + /// 待发消息离开队列:`reason` 是 typed 枚举(`cancelled` / `dispatched`),界面按取值分流, + /// 不解析字符串。`at` 缺省时反序列化仍是 `None`。 + #[test] + fn queue_removed_carries_a_typed_reason() { + let dispatched = DirectThreadEvent::queue_removed( + "turn-1".to_string(), + DirectQueueRemovalReason::Dispatched, + 2_000, + ); + assert_eq!( + serde_json::to_value(&dispatched).expect("serialize queue.removed"), + json!({ + "type": "queue.removed", + "clientTurnId": "turn-1", + "reason": "dispatched", + "at": 2_000u64, + }) + ); + assert_eq!( + serde_json::from_value::( + serde_json::to_value(&dispatched).expect("serialize") + ) + .expect("round trip"), + dispatched + ); + assert_eq!(dispatched.queue_client_turn_id(), Some("turn-1")); + assert_eq!( + dispatched.queue_removal_reason(), + Some(DirectQueueRemovalReason::Dispatched) + ); + + // 未识别的取值必须失败关闭:队列归属是宿主事实,不能让界面猜。 + serde_json::from_value::(json!({ + "type": "queue.removed", + "clientTurnId": "turn-1", + "reason": "timeout", + })) + .expect_err("unknown queue removal reason must fail closed"); + + // 没有 `at` 的老形状仍能反序列化,回写不补 `null`。 + let legacy: DirectThreadEvent = serde_json::from_value(json!({ + "type": "queue.removed", + "clientTurnId": "turn-1", + "reason": "cancelled", + })) + .expect("queue.removed without at"); + assert_eq!(legacy.at(), None); + assert_eq!( + serde_json::to_value(legacy).expect("serialize legacy"), + json!({ + "type": "queue.removed", + "clientTurnId": "turn-1", + "reason": "cancelled", + }) + ); + } } diff --git a/apps/ai-game-creator-shell/src/view/project-development/chat/generated/DirectQueueRemovalOutcome.ts b/apps/ai-game-creator-shell/src/view/project-development/chat/generated/DirectQueueRemovalOutcome.ts new file mode 100644 index 000000000..5a70720f9 --- /dev/null +++ b/apps/ai-game-creator-shell/src/view/project-development/chat/generated/DirectQueueRemovalOutcome.ts @@ -0,0 +1,9 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * 取消一条待发消息的结果。 + */ +export type DirectQueueRemovalOutcome = + | 'removed' + | 'alreadyDispatched' + | 'notFound'; diff --git a/apps/ai-game-creator-shell/src/view/project-development/chat/generated/DirectQueueRemovalReason.ts b/apps/ai-game-creator-shell/src/view/project-development/chat/generated/DirectQueueRemovalReason.ts new file mode 100644 index 000000000..90f8b8247 --- /dev/null +++ b/apps/ai-game-creator-shell/src/view/project-development/chat/generated/DirectQueueRemovalReason.ts @@ -0,0 +1,9 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * 一条待发消息离开队列的原因。 + * + * typed 枚举,取值即语义:取消是用户在输入盒上撤掉这条消息,放行是它已经接单并成为回合 + * (同一临界区里另有 `turn.started`)。界面按它分流,不解析字符串。 + */ +export type DirectQueueRemovalReason = 'cancelled' | 'dispatched'; diff --git a/apps/ai-game-creator-shell/src/view/project-development/chat/generated/DirectThreadEvent.ts b/apps/ai-game-creator-shell/src/view/project-development/chat/generated/DirectThreadEvent.ts index da0136295..630a41e95 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/chat/generated/DirectThreadEvent.ts +++ b/apps/ai-game-creator-shell/src/view/project-development/chat/generated/DirectThreadEvent.ts @@ -1,4 +1,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { DirectCodexUserItem } from './DirectCodexUserItem'; +import type { DirectQueueRemovalReason } from './DirectQueueRemovalReason'; import type { DirectThreadDeltaKind } from './DirectThreadDeltaKind'; import type { DirectThreadItem } from './DirectThreadItem'; import type { DirectThreadRequestKind } from './DirectThreadRequestKind'; @@ -84,8 +86,26 @@ export type DirectThreadEvent = kind: DirectThreadDeltaKind; delta: string; } + | { type: 'request'; kind: DirectThreadRequestKind; requestId: string | null } | { - type: 'request'; - kind: DirectThreadRequestKind; - requestId: string | null; + type: 'queue.enqueued'; + /** + * 这条待发消息的回合身份;放行后同一轮的 `turn.started` / `turn.completed` 用它。 + */ + clientTurnId: string; + /** + * canonical 用户条目:前端据此派生 chip 文案,Rust 不渲染展示形状。 + */ + userItem: DirectCodexUserItem; + creationType?: string; + /** + * 入队那一刻的宿主毫秒钟。 + */ + at: number; + } + | { + type: 'queue.removed'; + clientTurnId: string; + reason: DirectQueueRemovalReason; + at?: number; };