Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6eed6bd13f | |||
| b543a060b7 | |||
| e469831543 | |||
| 869abad879 | |||
| 280a2dc5d1 | |||
| edfd1f9143 | |||
| 423f2d7184 | |||
| aa9076c95c | |||
| 41366dd71d | |||
| f4b496caca | |||
| fc0fd846f9 | |||
| fa99d9ad62 | |||
| 64a52005f8 | |||
| 645d806fed | |||
| 0b9d16e8f1 | |||
| d5c4e0e780 | |||
| 30e27abc60 | |||
| b607501916 | |||
| 46e3cd9744 | |||
| 5fb3db662b | |||
| 87b798322e | |||
| ec286b3480 | |||
| 35733f0e33 | |||
| 3c7b02b9f8 | |||
| f2030a616f | |||
| b3b5d77990 | |||
| 1da106af7d | |||
| cd2edd6966 | |||
| 22e830ff93 | |||
| c266ae7b50 | |||
| a543b75cf7 | |||
| f213987f9a | |||
| 2c623bb577 | |||
| 3353906e6f | |||
| 17716347e2 | |||
| 9ad66a67a3 | |||
| ba3aa3ccdb | |||
| 2b38eaafba | |||
| 7d5b9071e7 | |||
| 9e83f1d88c | |||
| 7e35d7c344 | |||
| 5ec40c8b83 | |||
| 948a80fc49 | |||
| dfd6fadedf |
@@ -1,4 +1 @@
|
|||||||
# Git 在链接工作树里执行 Hook 时会注入 GIT_DIR 等仓库定位变量,优先级高于 cwd;
|
|
||||||
# 子进程(npm、lint-staged、测试夹具)会继承它们并写到真实仓库,故在入口统一清除。
|
|
||||||
unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_COMMON_DIR GIT_PREFIX GIT_CONFIG_PARAMETERS GIT_CEILING_DIRECTORIES
|
|
||||||
npm run format:staged
|
npm run format:staged
|
||||||
|
|||||||
@@ -1,4 +1 @@
|
|||||||
# Git 在链接工作树里执行 Hook 时会注入 GIT_DIR 等仓库定位变量,优先级高于 cwd;
|
|
||||||
# 钩子链(npm → check:repository-ci → 测试夹具)会继承它们并写到真实仓库,故在入口统一清除。
|
|
||||||
unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_COMMON_DIR GIT_PREFIX GIT_CONFIG_PARAMETERS GIT_CEILING_DIRECTORIES
|
|
||||||
npm run check:pre-push-master -- "$@"
|
npm run check:pre-push-master -- "$@"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@genarrative/ai-game-creator-shell",
|
"name": "@genarrative/ai-game-creator-shell",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.45",
|
"version": "0.1.29",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "node scripts/start-tauri-dev.mjs",
|
"dev": "node scripts/start-tauri-dev.mjs",
|
||||||
|
|||||||
+1
-1
@@ -1725,7 +1725,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "genarrative-ai-game-creator-shell"
|
name = "genarrative-ai-game-creator-shell"
|
||||||
version = "0.1.45"
|
version = "0.1.29"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"agent-runtime-core",
|
"agent-runtime-core",
|
||||||
"axum",
|
"axum",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "genarrative-ai-game-creator-shell"
|
name = "genarrative-ai-game-creator-shell"
|
||||||
version = "0.1.45"
|
version = "0.1.29"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
{
|
{
|
||||||
"$schema": "../gen/schemas/desktop-schema.json",
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
"identifier": "main",
|
"identifier": "main",
|
||||||
"description": "AI 游戏创作主窗口允许读写系统剪贴板,用于粘贴素材附件和复制生成文件路径;允许弹出原生打开/保存对话框用于素材上传与导出。",
|
"description": "AI 游戏创作主窗口允许读取系统剪贴板图片,用于粘贴素材附件;允许弹出原生打开/保存对话框用于素材上传与导出。",
|
||||||
"windows": ["client"],
|
"windows": ["client"],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"clipboard-manager:allow-read-image",
|
"clipboard-manager:allow-read-image",
|
||||||
"clipboard-manager:allow-read-text",
|
"clipboard-manager:allow-read-text",
|
||||||
"clipboard-manager:allow-write-text",
|
|
||||||
"core:image:allow-rgba",
|
"core:image:allow-rgba",
|
||||||
"core:image:allow-size",
|
"core:image:allow-size",
|
||||||
"core:resources:allow-close",
|
"core:resources:allow-close",
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ mod validation;
|
|||||||
mod wire;
|
mod wire;
|
||||||
|
|
||||||
pub(crate) use model::{
|
pub(crate) use model::{
|
||||||
DirectCodexUserContentPart, DirectCodexUserItem, DirectCodexUserMessageEnvelope,
|
DirectCodexUserAttachmentReferencePart, DirectCodexUserContentPart, DirectCodexUserItem,
|
||||||
DirectCodexUserMessageItem, DirectCodexUserRole, DirectCodexUserRuntimeRegionPart,
|
DirectCodexUserMessageEnvelope, DirectCodexUserMessageItem, DirectCodexUserRole,
|
||||||
|
DirectCodexUserRuntimeRegionPart,
|
||||||
};
|
};
|
||||||
pub(crate) use validation::validate_direct_codex_user_item;
|
pub(crate) use validation::validate_direct_codex_user_item;
|
||||||
pub(crate) use wire::{
|
pub(crate) use wire::{
|
||||||
|
|||||||
@@ -36,6 +36,21 @@ pub(crate) enum DirectCodexUserContentPart {
|
|||||||
AgcResourceReference { resource_id: String },
|
AgcResourceReference { resource_id: String },
|
||||||
#[serde(rename = "agc_runtime_region_reference")]
|
#[serde(rename = "agc_runtime_region_reference")]
|
||||||
AgcRuntimeRegionReference(DirectCodexUserRuntimeRegionPart),
|
AgcRuntimeRegionReference(DirectCodexUserRuntimeRegionPart),
|
||||||
|
/// Uploaded project attachment kept inline in canonical content.
|
||||||
|
#[serde(rename = "agc_attachment_reference")]
|
||||||
|
AgcAttachmentReference(DirectCodexUserAttachmentReferencePart),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
|
||||||
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||||
|
#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/features/project-workspace/generated/"))]
|
||||||
|
pub(crate) struct DirectCodexUserAttachmentReferencePart {
|
||||||
|
pub(crate) name: String,
|
||||||
|
pub(crate) media_type: String,
|
||||||
|
#[ts(type = "number")]
|
||||||
|
pub(crate) size: u64,
|
||||||
|
pub(crate) local_path: String,
|
||||||
|
pub(crate) status: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
|
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
|
||||||
|
|||||||
+69
-6
@@ -20,18 +20,16 @@ pub(crate) fn validate_direct_codex_user_item(
|
|||||||
if message.id.trim().is_empty() {
|
if message.id.trim().is_empty() {
|
||||||
return Err("DirectProject user item 缺少稳定 id".to_string());
|
return Err("DirectProject user item 缺少稳定 id".to_string());
|
||||||
}
|
}
|
||||||
if message.content.is_empty() {
|
// 有效输入只判一整条 content:单个纯空白 `input_text` 是合法 part —— 编辑器里的段落
|
||||||
|
// 分隔、软换行与 chip 后的分隔空格就是这样落进 canonical content 的,前端不为它过滤。
|
||||||
|
if !content_has_meaningful_input(&message.content) {
|
||||||
return Err("DirectProject user item content 不能为空".to_string());
|
return Err("DirectProject user item content 不能为空".to_string());
|
||||||
}
|
}
|
||||||
let manifest = read_manifest_for_project(root)?;
|
let manifest = read_manifest_for_project(root)?;
|
||||||
let mut reference_count = 0usize;
|
let mut reference_count = 0usize;
|
||||||
for part in &message.content {
|
for part in &message.content {
|
||||||
match part {
|
match part {
|
||||||
DirectCodexUserContentPart::InputText { text } => {
|
DirectCodexUserContentPart::InputText { .. } => {}
|
||||||
if text.trim().is_empty() {
|
|
||||||
return Err("DirectProject input_text 不能为空".to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DirectCodexUserContentPart::AgcResourceReference { resource_id } => {
|
DirectCodexUserContentPart::AgcResourceReference { resource_id } => {
|
||||||
reference_count = reference_count.saturating_add(1);
|
reference_count = reference_count.saturating_add(1);
|
||||||
validate_resource_id_and_manifest(&manifest, resource_id)?;
|
validate_resource_id_and_manifest(&manifest, resource_id)?;
|
||||||
@@ -40,6 +38,18 @@ pub(crate) fn validate_direct_codex_user_item(
|
|||||||
reference_count = reference_count.saturating_add(1);
|
reference_count = reference_count.saturating_add(1);
|
||||||
validate_runtime_region_reference(&manifest, reference)?;
|
validate_runtime_region_reference(&manifest, reference)?;
|
||||||
}
|
}
|
||||||
|
DirectCodexUserContentPart::AgcAttachmentReference(reference) => {
|
||||||
|
if reference.name.trim().is_empty() {
|
||||||
|
return Err("附件缺少文件名".to_string());
|
||||||
|
}
|
||||||
|
if !reference.local_path.trim().is_empty() {
|
||||||
|
sanitize_attachment_local_path(&reference.local_path)
|
||||||
|
.ok_or_else(|| "附件项目路径无效".to_string())?;
|
||||||
|
}
|
||||||
|
if !matches!(reference.status.trim(), "imported" | "failed") {
|
||||||
|
return Err("附件状态无效".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if reference_count > MAX_DIRECT_CODEX_REFERENCES {
|
if reference_count > MAX_DIRECT_CODEX_REFERENCES {
|
||||||
@@ -48,6 +58,14 @@ pub(crate) fn validate_direct_codex_user_item(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 整条 content 是否还有有效输入:任何一段非空白文本、或任何一个非文本 part 都算。
|
||||||
|
pub(crate) fn content_has_meaningful_input(content: &[DirectCodexUserContentPart]) -> bool {
|
||||||
|
content.iter().any(|part| match part {
|
||||||
|
DirectCodexUserContentPart::InputText { text } => !text.trim().is_empty(),
|
||||||
|
_ => true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn validate_resource_id_and_manifest(
|
pub(crate) fn validate_resource_id_and_manifest(
|
||||||
manifest: &GameCreationAppManifest,
|
manifest: &GameCreationAppManifest,
|
||||||
resource_id: &str,
|
resource_id: &str,
|
||||||
@@ -86,3 +104,48 @@ fn validate_runtime_region_reference(
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::content_has_meaningful_input;
|
||||||
|
use crate::agent::direct_codex_user_item::model::DirectCodexUserContentPart;
|
||||||
|
|
||||||
|
fn input_text(text: &str) -> DirectCodexUserContentPart {
|
||||||
|
DirectCodexUserContentPart::InputText {
|
||||||
|
text: text.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn only_all_blank_content_counts_as_empty_input() {
|
||||||
|
// 空数组与「整条只有空白」是同一种空输入。
|
||||||
|
assert!(!content_has_meaningful_input(&[]));
|
||||||
|
assert!(!content_has_meaningful_input(&[input_text(" \n ")]));
|
||||||
|
assert!(!content_has_meaningful_input(&[
|
||||||
|
input_text("\n"),
|
||||||
|
input_text(" "),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn whitespace_parts_are_valid_next_to_meaningful_input() {
|
||||||
|
// 段落分隔 / 软换行 / chip 后的分隔空格都是合法的单个 part。
|
||||||
|
assert!(content_has_meaningful_input(&[
|
||||||
|
input_text("\n"),
|
||||||
|
input_text("看"),
|
||||||
|
]));
|
||||||
|
assert!(content_has_meaningful_input(&[
|
||||||
|
input_text("看"),
|
||||||
|
input_text("\n\n"),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn non_text_parts_always_count_as_input() {
|
||||||
|
assert!(content_has_meaningful_input(&[
|
||||||
|
DirectCodexUserContentPart::AgcResourceReference {
|
||||||
|
resource_id: "asset-hero".to_string(),
|
||||||
|
},
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -100,6 +100,20 @@ pub(crate) fn direct_codex_user_item_to_wire_input(
|
|||||||
summary.push(']');
|
summary.push(']');
|
||||||
summary
|
summary
|
||||||
}
|
}
|
||||||
|
DirectCodexUserContentPart::AgcAttachmentReference(reference) => {
|
||||||
|
let mut summary = format!(
|
||||||
|
"[附件:名称={};类型={};大小={} 字节",
|
||||||
|
reference.name.trim(),
|
||||||
|
reference.media_type.trim(),
|
||||||
|
reference.size
|
||||||
|
);
|
||||||
|
if !reference.local_path.trim().is_empty() {
|
||||||
|
summary.push_str(&format!(";项目路径={}", reference.local_path.trim()));
|
||||||
|
}
|
||||||
|
summary.push_str(&format!(";状态={}", reference.status.trim()));
|
||||||
|
summary.push(']');
|
||||||
|
summary
|
||||||
|
}
|
||||||
};
|
};
|
||||||
input.push(serde_json::json!({ "type": "text", "text": text }));
|
input.push(serde_json::json!({ "type": "text", "text": text }));
|
||||||
}
|
}
|
||||||
@@ -130,7 +144,8 @@ pub(crate) fn direct_codex_user_item_to_prompt(
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::direct_codex_user_item_to_response_item;
|
use super::{direct_codex_user_item_to_response_item, direct_codex_user_item_to_wire_input};
|
||||||
|
use crate::agent::direct_codex_user_item::model::DirectCodexUserItem;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
@@ -174,4 +189,70 @@ mod tests {
|
|||||||
.expect_err("history item without type must fail");
|
.expect_err("history item without type must fail");
|
||||||
assert!(error.contains("缺少 type"), "{error}");
|
assert!(error.contains("缺少 type"), "{error}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn attachment_parts_remain_in_canonical_order_when_projected() {
|
||||||
|
let root = tempfile::tempdir().expect("temp project");
|
||||||
|
crate::init_local_game_project_at(root.path(), "wire-test", "wire 投影测试")
|
||||||
|
.expect("init project");
|
||||||
|
let item = json!({
|
||||||
|
"type": "message",
|
||||||
|
"role": "user",
|
||||||
|
"id": "turn-1:user",
|
||||||
|
"content": [
|
||||||
|
{"type": "input_text", "text": "先看"},
|
||||||
|
{"type": "agc_attachment_reference", "name": "notes.txt", "mediaType": "text/plain", "size": 4, "localPath": "assets/notes.txt", "status": "imported"}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
let projected = direct_codex_user_item_to_response_item(root.path(), &item)
|
||||||
|
.expect("user response item should project");
|
||||||
|
let content = projected["content"].as_array().expect("content array");
|
||||||
|
assert_eq!(content.len(), 2);
|
||||||
|
assert!(content[0]["text"].as_str().unwrap().contains("先看"));
|
||||||
|
assert!(content[1]["text"].as_str().unwrap().contains("notes.txt"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn whitespace_only_text_parts_survive_validation() {
|
||||||
|
let root = tempfile::tempdir().expect("temp project");
|
||||||
|
crate::init_local_game_project_at(root.path(), "wire-test", "wire 投影测试")
|
||||||
|
.expect("init project");
|
||||||
|
let item: DirectCodexUserItem = serde_json::from_value(json!({
|
||||||
|
"type": "message",
|
||||||
|
"role": "user",
|
||||||
|
"id": "turn-1:user",
|
||||||
|
"content": [
|
||||||
|
{"type": "input_text", "text": "先看"},
|
||||||
|
{"type": "input_text", "text": "\n"},
|
||||||
|
{"type": "input_text", "text": " "}
|
||||||
|
]
|
||||||
|
}))
|
||||||
|
.expect("deserialize user item");
|
||||||
|
let wire = direct_codex_user_item_to_wire_input(root.path(), &item)
|
||||||
|
.expect("whitespace-only part next to real text must pass");
|
||||||
|
let parts = wire.as_array().expect("wire input array");
|
||||||
|
assert_eq!(parts.len(), 3);
|
||||||
|
assert_eq!(parts[1]["text"].as_str(), Some("\n"));
|
||||||
|
assert_eq!(parts[2]["text"].as_str(), Some(" "));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn all_blank_content_is_rejected() {
|
||||||
|
let root = tempfile::tempdir().expect("temp project");
|
||||||
|
crate::init_local_game_project_at(root.path(), "wire-test", "wire 投影测试")
|
||||||
|
.expect("init project");
|
||||||
|
let item: DirectCodexUserItem = serde_json::from_value(json!({
|
||||||
|
"type": "message",
|
||||||
|
"role": "user",
|
||||||
|
"id": "turn-1:user",
|
||||||
|
"content": [
|
||||||
|
{"type": "input_text", "text": "\n"},
|
||||||
|
{"type": "input_text", "text": " "}
|
||||||
|
]
|
||||||
|
}))
|
||||||
|
.expect("deserialize user item");
|
||||||
|
let error = direct_codex_user_item_to_wire_input(root.path(), &item)
|
||||||
|
.expect_err("all-blank content must fail closed");
|
||||||
|
assert!(error.contains("不能为空"), "{error}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,10 +31,9 @@ pub(crate) fn normalize_direct_client_turn_id(
|
|||||||
pub(crate) async fn chat_with_game_creator_direct_codex(
|
pub(crate) async fn chat_with_game_creator_direct_codex(
|
||||||
project_path: String,
|
project_path: String,
|
||||||
prompt: String,
|
prompt: String,
|
||||||
mut user_item: DirectCodexUserItem,
|
user_item: DirectCodexUserItem,
|
||||||
creation_type: Option<String>,
|
creation_type: Option<String>,
|
||||||
client_turn_id: Option<String>,
|
client_turn_id: Option<String>,
|
||||||
attachments: Option<Vec<DirectCodexTurnAttachment>>,
|
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
let root = Path::new(project_path.trim());
|
let root = Path::new(project_path.trim());
|
||||||
let turn_id = normalize_direct_client_turn_id(client_turn_id.as_deref())?;
|
let turn_id = normalize_direct_client_turn_id(client_turn_id.as_deref())?;
|
||||||
@@ -43,24 +42,7 @@ pub(crate) async fn chat_with_game_creator_direct_codex(
|
|||||||
redact_agent_runtime_error(root, &format!("恢复上一轮陶泥儿整包事务失败:{error}"), 500)
|
redact_agent_runtime_error(root, &format!("恢复上一轮陶泥儿整包事务失败:{error}"), 500)
|
||||||
})?;
|
})?;
|
||||||
let turn_emitter = DirectGameCreatorTurnUpdateEmitter::new(root, turn_id.clone());
|
let turn_emitter = DirectGameCreatorTurnUpdateEmitter::new(root, turn_id.clone());
|
||||||
let mut audit = DirectCodexTurnAudit::start(
|
let mut audit = DirectCodexTurnAudit::start(root, &turn_id, &prompt, &[]);
|
||||||
root,
|
|
||||||
&turn_id,
|
|
||||||
&prompt,
|
|
||||||
attachments.as_deref().unwrap_or_default(),
|
|
||||||
);
|
|
||||||
let attachments = attachments.unwrap_or_default();
|
|
||||||
if !attachments.is_empty() {
|
|
||||||
let attachment_context =
|
|
||||||
render_direct_codex_user_prompt("", &attachments).map_err(|error| {
|
|
||||||
audit.finish(false);
|
|
||||||
error
|
|
||||||
})?;
|
|
||||||
let DirectCodexUserItem::Message(message) = &mut user_item;
|
|
||||||
message.content.push(DirectCodexUserContentPart::InputText {
|
|
||||||
text: attachment_context,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
validate_direct_codex_user_item(root, &user_item).map_err(|error| {
|
validate_direct_codex_user_item(root, &user_item).map_err(|error| {
|
||||||
audit.finish(false);
|
audit.finish(false);
|
||||||
error
|
error
|
||||||
|
|||||||
@@ -172,7 +172,6 @@ mod materialize {
|
|||||||
use crate::ui_editor::layout::node::{
|
use crate::ui_editor::layout::node::{
|
||||||
Node as LayoutNode, NodeMetadata, NodeSource, StageStatus,
|
Node as LayoutNode, NodeMetadata, NodeSource, StageStatus,
|
||||||
};
|
};
|
||||||
use crate::ui_editor::layout::offset::NodeOffset;
|
|
||||||
use crate::ui_editor::layout::transform::Transform;
|
use crate::ui_editor::layout::transform::Transform;
|
||||||
use crate::ui_editor::state::{State, UITree};
|
use crate::ui_editor::state::{State, UITree};
|
||||||
use crate::ui_editor::utils::{random_node_id, NodeId, UIDesignImageId};
|
use crate::ui_editor::utils::{random_node_id, NodeId, UIDesignImageId};
|
||||||
@@ -303,7 +302,6 @@ mod materialize {
|
|||||||
component: None,
|
component: None,
|
||||||
children_display_mode: ChildrenDisplayMode::Exclusive,
|
children_display_mode: ChildrenDisplayMode::Exclusive,
|
||||||
children: members.into_iter().map(|member| member.node).collect(),
|
children: members.into_iter().map(|member| member.node).collect(),
|
||||||
offset: NodeOffset::default(),
|
|
||||||
},
|
},
|
||||||
priority,
|
priority,
|
||||||
src_ui_design,
|
src_ui_design,
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ use crate::ui_editor::layout::children_display_mode::ChildrenDisplayMode;
|
|||||||
use crate::ui_editor::layout::control_layout::ControlLayout;
|
use crate::ui_editor::layout::control_layout::ControlLayout;
|
||||||
use crate::ui_editor::layout::dimension::UIRect;
|
use crate::ui_editor::layout::dimension::UIRect;
|
||||||
use crate::ui_editor::layout::node::{Node as LayoutNode, NodeMetadata, NodeSource, StageStatus};
|
use crate::ui_editor::layout::node::{Node as LayoutNode, NodeMetadata, NodeSource, StageStatus};
|
||||||
use crate::ui_editor::layout::offset::NodeOffset;
|
|
||||||
use crate::ui_editor::layout::transform::Transform;
|
use crate::ui_editor::layout::transform::Transform;
|
||||||
use crate::ui_editor::resource::ui_design_image::UIDesignImage;
|
use crate::ui_editor::resource::ui_design_image::UIDesignImage;
|
||||||
use crate::ui_editor::state::{State, UITree};
|
use crate::ui_editor::state::{State, UITree};
|
||||||
@@ -323,7 +322,6 @@ fn convert_node(
|
|||||||
component: source.component.clone().into_option(),
|
component: source.component.clone().into_option(),
|
||||||
children_display_mode: ChildrenDisplayMode::Stack,
|
children_display_mode: ChildrenDisplayMode::Stack,
|
||||||
children,
|
children,
|
||||||
offset: NodeOffset::default(),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -860,7 +858,6 @@ pub(crate) async fn recognize_ui_impl_with_provider(
|
|||||||
component: recognition_root.component.into_option(),
|
component: recognition_root.component.into_option(),
|
||||||
children_display_mode: ChildrenDisplayMode::Stack,
|
children_display_mode: ChildrenDisplayMode::Stack,
|
||||||
children,
|
children,
|
||||||
offset: NodeOffset::default(),
|
|
||||||
};
|
};
|
||||||
// Tree identity and root identity are assigned by Rust, never chosen by the model.
|
// Tree identity and root identity are assigned by Rust, never chosen by the model.
|
||||||
ui_trees.push(UITree {
|
ui_trees.push(UITree {
|
||||||
|
|||||||
@@ -2,5 +2,4 @@ pub mod children_display_mode;
|
|||||||
pub mod control_layout;
|
pub mod control_layout;
|
||||||
pub mod dimension;
|
pub mod dimension;
|
||||||
pub mod node;
|
pub mod node;
|
||||||
pub mod offset;
|
|
||||||
pub mod transform;
|
pub mod transform;
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
use crate::ui_editor::component::Component;
|
use crate::ui_editor::component::Component;
|
||||||
use crate::ui_editor::layout::children_display_mode::ChildrenDisplayMode;
|
use crate::ui_editor::layout::children_display_mode::ChildrenDisplayMode;
|
||||||
use crate::ui_editor::layout::control_layout::ControlLayout;
|
use crate::ui_editor::layout::control_layout::ControlLayout;
|
||||||
use crate::ui_editor::layout::offset::NodeOffset;
|
|
||||||
use crate::ui_editor::utils::NodeId;
|
use crate::ui_editor::utils::NodeId;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
@@ -15,7 +14,6 @@ pub struct Node {
|
|||||||
pub component: Option<Component>,
|
pub component: Option<Component>,
|
||||||
pub children_display_mode: ChildrenDisplayMode,
|
pub children_display_mode: ChildrenDisplayMode,
|
||||||
pub children: Vec<Node>,
|
pub children: Vec<Node>,
|
||||||
pub offset: NodeOffset,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, TS)]
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, TS)]
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use ts_rs::TS;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, TS)]
|
|
||||||
#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/features/ui-editor/types/"))]
|
|
||||||
pub struct NodeOffset {
|
|
||||||
pub min: [f32; 2],
|
|
||||||
pub max: [f32; 2],
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for NodeOffset {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
min: [0.0, 0.0],
|
|
||||||
max: [0.0, 0.0],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "陶泥儿",
|
"productName": "陶泥儿",
|
||||||
"version": "0.1.45",
|
"version": "0.1.29",
|
||||||
"identifier": "world.genarrative.ai-game-creator",
|
"identifier": "world.genarrative.ai-game-creator",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "npm --prefix ../.. run agc:serve",
|
"beforeDevCommand": "npm --prefix ../.. run agc:serve",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,102 @@
|
|||||||
|
import { LexicalComposer } from '@lexical/react/LexicalComposer';
|
||||||
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||||
|
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
|
||||||
|
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
|
||||||
|
import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin';
|
||||||
|
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
|
||||||
|
import {
|
||||||
|
COMMAND_PRIORITY_HIGH,
|
||||||
|
type EditorState,
|
||||||
|
KEY_ENTER_COMMAND,
|
||||||
|
type Klass,
|
||||||
|
type LexicalNode,
|
||||||
|
} from 'lexical';
|
||||||
|
import type { ReactElement, ReactNode, Ref } from 'react';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
type RichTextInputProps = {
|
||||||
|
namespace: string;
|
||||||
|
nodes: Klass<LexicalNode>[];
|
||||||
|
initialEditorState?: EditorState | null;
|
||||||
|
contentEditable?: ReactElement<typeof ContentEditable>;
|
||||||
|
placeholder?: ReactElement;
|
||||||
|
containerClassName?: string;
|
||||||
|
containerRef?: Ref<HTMLDivElement>;
|
||||||
|
disabled?: boolean;
|
||||||
|
onChange?: (editorState: EditorState) => void;
|
||||||
|
onEnter?: () => void;
|
||||||
|
children?: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
function SubmitOnEnter({ onEnter }: { onEnter?: () => void }) {
|
||||||
|
const [editor] = useLexicalComposerContext();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!onEnter) return undefined;
|
||||||
|
return editor.registerCommand(
|
||||||
|
KEY_ENTER_COMMAND,
|
||||||
|
(event) => {
|
||||||
|
if (!event || event.shiftKey || event.isComposing) return false;
|
||||||
|
event.preventDefault();
|
||||||
|
onEnter();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
COMMAND_PRIORITY_HIGH,
|
||||||
|
);
|
||||||
|
}, [editor, onEnter]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SetEditorEditable({ disabled }: { disabled: boolean }) {
|
||||||
|
const [editor] = useLexicalComposerContext();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
editor.setEditable(!disabled);
|
||||||
|
}, [disabled, editor]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RichTextInput({
|
||||||
|
namespace,
|
||||||
|
nodes,
|
||||||
|
initialEditorState,
|
||||||
|
contentEditable = <ContentEditable />,
|
||||||
|
placeholder,
|
||||||
|
containerClassName,
|
||||||
|
containerRef,
|
||||||
|
disabled = false,
|
||||||
|
onChange,
|
||||||
|
onEnter,
|
||||||
|
children,
|
||||||
|
}: RichTextInputProps) {
|
||||||
|
return (
|
||||||
|
<LexicalComposer
|
||||||
|
initialConfig={{
|
||||||
|
namespace,
|
||||||
|
nodes,
|
||||||
|
editorState: initialEditorState ?? undefined,
|
||||||
|
onError: (error) => {
|
||||||
|
throw error;
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className={containerClassName}
|
||||||
|
data-disabled={disabled ? 'true' : undefined}
|
||||||
|
>
|
||||||
|
<RichTextPlugin
|
||||||
|
contentEditable={contentEditable}
|
||||||
|
placeholder={placeholder}
|
||||||
|
ErrorBoundary={LexicalErrorBoundary}
|
||||||
|
/>
|
||||||
|
{children}
|
||||||
|
<SetEditorEditable disabled={disabled} />
|
||||||
|
<SubmitOnEnter onEnter={onEnter} />
|
||||||
|
{onChange ? <OnChangePlugin onChange={onChange} /> : null}
|
||||||
|
</div>
|
||||||
|
</LexicalComposer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
import type { RefObject } from 'react';
|
import type { RefObject } from 'react';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import type { GameCreationAppAssetManifestEntry } from '../../../../../packages/shared/src/contracts/gameCreationApp';
|
||||||
import { resolveTauriInvoke } from '../../app/tauri';
|
import { resolveTauriInvoke } from '../../app/tauri';
|
||||||
import type {
|
import type {
|
||||||
GameCreatorAppConfigView,
|
GameCreatorAppConfigView,
|
||||||
@@ -192,9 +193,12 @@ export function ComposerPendingAttachments({
|
|||||||
/** 队列 chip:回合运行中入队的消息,按 FIFO 顺序展示,可单条取消。 */
|
/** 队列 chip:回合运行中入队的消息,按 FIFO 顺序展示,可单条取消。 */
|
||||||
export function ComposerTurnQueue({
|
export function ComposerTurnQueue({
|
||||||
turns,
|
turns,
|
||||||
|
assets,
|
||||||
onCancel,
|
onCancel,
|
||||||
}: {
|
}: {
|
||||||
turns: readonly QueuedChatTurn[];
|
turns: readonly QueuedChatTurn[];
|
||||||
|
/** 与聊天消息渲染同源的素材清单:chip 文案里的 `@` 引用按它展开成显示名。 */
|
||||||
|
assets: readonly GameCreationAppAssetManifestEntry[];
|
||||||
onCancel: (id: string) => void;
|
onCancel: (id: string) => void;
|
||||||
}) {
|
}) {
|
||||||
if (turns.length === 0) {
|
if (turns.length === 0) {
|
||||||
@@ -211,11 +215,11 @@ export function ComposerTurnQueue({
|
|||||||
{index + 1}
|
{index + 1}
|
||||||
</span>
|
</span>
|
||||||
<span className="project-supervisor-composer-queue-text">
|
<span className="project-supervisor-composer-queue-text">
|
||||||
{queuedChatTurnLabel(turn)}
|
{queuedChatTurnLabel(turn, assets)}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={`取消排队消息 ${queuedChatTurnLabel(turn)}`}
|
aria-label={`取消排队消息 ${queuedChatTurnLabel(turn, assets)}`}
|
||||||
title="取消这条排队消息"
|
title="取消这条排队消息"
|
||||||
onClick={() => onCancel(turn.id)}
|
onClick={() => onCancel(turn.id)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ import {
|
|||||||
ResourceReferenceInput,
|
ResourceReferenceInput,
|
||||||
type ResourceReferenceInputHandle,
|
type ResourceReferenceInputHandle,
|
||||||
} from './ResourceReferenceInput';
|
} from './ResourceReferenceInput';
|
||||||
import type { ChatComposerDraft, ChatReference } from './resourceReferences';
|
import type { ChatComposerDraft } from './resourceReferences';
|
||||||
import { ToolCallGroup } from './ToolCallGroup';
|
import { ToolCallGroup } from './ToolCallGroup';
|
||||||
import {
|
import {
|
||||||
formatClockTime,
|
formatClockTime,
|
||||||
@@ -242,8 +242,6 @@ type ProjectSupervisorViewProps = RuntimePanelProps & {
|
|||||||
attachments?: DirectCodexTurnAttachment[];
|
attachments?: DirectCodexTurnAttachment[];
|
||||||
/** 上传/校验附件的提示文案(失败与成功都用它,空串不渲染)。 */
|
/** 上传/校验附件的提示文案(失败与成功都用它,空串不渲染)。 */
|
||||||
attachmentNotice?: string;
|
attachmentNotice?: string;
|
||||||
chatInput: string;
|
|
||||||
chatReferences: ChatReference[];
|
|
||||||
chatProjectAssets: import('../../../../../packages/shared/src/contracts/gameCreationApp').GameCreationAppAssetManifestEntry[];
|
chatProjectAssets: import('../../../../../packages/shared/src/contracts/gameCreationApp').GameCreationAppAssetManifestEntry[];
|
||||||
composerRef?: RefObject<ResourceReferenceInputHandle | null>;
|
composerRef?: RefObject<ResourceReferenceInputHandle | null>;
|
||||||
directCodex?: boolean;
|
directCodex?: boolean;
|
||||||
@@ -327,8 +325,6 @@ export function ProjectSupervisorView({
|
|||||||
activeVersionId = null,
|
activeVersionId = null,
|
||||||
attachments = [],
|
attachments = [],
|
||||||
attachmentNotice = '',
|
attachmentNotice = '',
|
||||||
chatInput,
|
|
||||||
chatReferences,
|
|
||||||
chatProjectAssets,
|
chatProjectAssets,
|
||||||
composerRef,
|
composerRef,
|
||||||
directCodex = false,
|
directCodex = false,
|
||||||
@@ -893,6 +889,7 @@ export function ProjectSupervisorView({
|
|||||||
>
|
>
|
||||||
<ComposerTurnQueue
|
<ComposerTurnQueue
|
||||||
turns={directCodex ? queuedTurns : []}
|
turns={directCodex ? queuedTurns : []}
|
||||||
|
assets={chatProjectAssets}
|
||||||
onCancel={(id) => onCancelQueuedTurn?.(id)}
|
onCancel={(id) => onCancelQueuedTurn?.(id)}
|
||||||
/>
|
/>
|
||||||
<ComposerPendingAttachments
|
<ComposerPendingAttachments
|
||||||
@@ -916,8 +913,6 @@ export function ProjectSupervisorView({
|
|||||||
Boolean(designView?.session.pendingClarification)
|
Boolean(designView?.session.pendingClarification)
|
||||||
}
|
}
|
||||||
rows={3}
|
rows={3}
|
||||||
value={chatInput}
|
|
||||||
references={chatReferences}
|
|
||||||
showTriggerButton={!directCodex}
|
showTriggerButton={!directCodex}
|
||||||
placeholder={
|
placeholder={
|
||||||
directCodex
|
directCodex
|
||||||
|
|||||||
+1
-7
@@ -55,7 +55,7 @@ import {
|
|||||||
ResourceReferenceInput,
|
ResourceReferenceInput,
|
||||||
type ResourceReferenceInputHandle,
|
type ResourceReferenceInputHandle,
|
||||||
} from './ResourceReferenceInput';
|
} from './ResourceReferenceInput';
|
||||||
import type { ChatComposerDraft, ChatReference } from './resourceReferences';
|
import type { ChatComposerDraft } from './resourceReferences';
|
||||||
|
|
||||||
type ProjectWorkspaceChatPaneProps = {
|
type ProjectWorkspaceChatPaneProps = {
|
||||||
activeVersionId?: string | null;
|
activeVersionId?: string | null;
|
||||||
@@ -65,8 +65,6 @@ type ProjectWorkspaceChatPaneProps = {
|
|||||||
cancelProjectCreateInNonEmptyFolder: () => void;
|
cancelProjectCreateInNonEmptyFolder: () => void;
|
||||||
cancelUiCommandConfirmation: () => void;
|
cancelUiCommandConfirmation: () => void;
|
||||||
chatAgentBusy: boolean;
|
chatAgentBusy: boolean;
|
||||||
chatInput: string;
|
|
||||||
chatReferences: ChatReference[];
|
|
||||||
chatProjectAssets: GameCreationAppAssetManifestEntry[];
|
chatProjectAssets: GameCreationAppAssetManifestEntry[];
|
||||||
composerRef?: Ref<ResourceReferenceInputHandle>;
|
composerRef?: Ref<ResourceReferenceInputHandle>;
|
||||||
chatInputRef: RefObject<HTMLDivElement | null>;
|
chatInputRef: RefObject<HTMLDivElement | null>;
|
||||||
@@ -215,8 +213,6 @@ export function ProjectWorkspaceChatPane({
|
|||||||
cancelProjectCreateInNonEmptyFolder,
|
cancelProjectCreateInNonEmptyFolder,
|
||||||
cancelUiCommandConfirmation,
|
cancelUiCommandConfirmation,
|
||||||
chatAgentBusy,
|
chatAgentBusy,
|
||||||
chatInput,
|
|
||||||
chatReferences,
|
|
||||||
chatProjectAssets,
|
chatProjectAssets,
|
||||||
composerRef,
|
composerRef,
|
||||||
chatInputRef,
|
chatInputRef,
|
||||||
@@ -962,8 +958,6 @@ export function ProjectWorkspaceChatPane({
|
|||||||
projectPath={projectPath}
|
projectPath={projectPath}
|
||||||
disabled={chatAgentBusy || projectSupervisorNeedsUserInput}
|
disabled={chatAgentBusy || projectSupervisorNeedsUserInput}
|
||||||
multiline={false}
|
multiline={false}
|
||||||
value={chatInput}
|
|
||||||
references={chatReferences}
|
|
||||||
placeholder="例如:像素风横版动作小游戏,或输入 @ 选择资源"
|
placeholder="例如:像素风横版动作小游戏,或输入 @ 选择资源"
|
||||||
onChange={onChatComposerChange}
|
onChange={onChatComposerChange}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+208
-149
File diff suppressed because it is too large
Load Diff
+1
-7
@@ -33,7 +33,7 @@ import {
|
|||||||
ResourceReferenceInput,
|
ResourceReferenceInput,
|
||||||
type ResourceReferenceInputHandle,
|
type ResourceReferenceInputHandle,
|
||||||
} from './ResourceReferenceInput';
|
} from './ResourceReferenceInput';
|
||||||
import type { ChatComposerDraft, ChatReference } from './resourceReferences';
|
import type { ChatComposerDraft } from './resourceReferences';
|
||||||
|
|
||||||
type RuntimeControlProps = ComponentProps<
|
type RuntimeControlProps = ComponentProps<
|
||||||
typeof ProjectSupervisorRuntimeControls
|
typeof ProjectSupervisorRuntimeControls
|
||||||
@@ -44,8 +44,6 @@ const CHAT_SCROLL_BOTTOM_THRESHOLD = 24;
|
|||||||
type SupervisorChatOnlyViewProps = {
|
type SupervisorChatOnlyViewProps = {
|
||||||
activeVersionId?: string | null;
|
activeVersionId?: string | null;
|
||||||
chatAgentBusy: boolean;
|
chatAgentBusy: boolean;
|
||||||
chatInput: string;
|
|
||||||
chatReferences: ChatReference[];
|
|
||||||
chatProjectAssets: import('../../../../../packages/shared/src/contracts/gameCreationApp').GameCreationAppAssetManifestEntry[];
|
chatProjectAssets: import('../../../../../packages/shared/src/contracts/gameCreationApp').GameCreationAppAssetManifestEntry[];
|
||||||
composerRef?: Ref<ResourceReferenceInputHandle>;
|
composerRef?: Ref<ResourceReferenceInputHandle>;
|
||||||
directCodex?: boolean;
|
directCodex?: boolean;
|
||||||
@@ -82,8 +80,6 @@ type SupervisorChatOnlyViewProps = {
|
|||||||
export function SupervisorChatOnlyView({
|
export function SupervisorChatOnlyView({
|
||||||
activeVersionId = null,
|
activeVersionId = null,
|
||||||
chatAgentBusy,
|
chatAgentBusy,
|
||||||
chatInput,
|
|
||||||
chatReferences,
|
|
||||||
chatProjectAssets,
|
chatProjectAssets,
|
||||||
composerRef,
|
composerRef,
|
||||||
directCodex = false,
|
directCodex = false,
|
||||||
@@ -328,8 +324,6 @@ export function SupervisorChatOnlyView({
|
|||||||
projectPath={projectPath}
|
projectPath={projectPath}
|
||||||
disabled={chatAgentBusy || needsUserInput}
|
disabled={chatAgentBusy || needsUserInput}
|
||||||
rows={3}
|
rows={3}
|
||||||
value={chatInput}
|
|
||||||
references={chatReferences}
|
|
||||||
placeholder={
|
placeholder={
|
||||||
directCodex ? '描述你的想法' : '给项目总控 Agent 发消息'
|
directCodex ? '描述你的想法' : '给项目总控 Agent 发消息'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,36 +4,33 @@
|
|||||||
* 回合运行中用户再次发送时,消息进入 FIFO 队列而不是被丢弃;当前回合结束后按入队顺序
|
* 回合运行中用户再次发送时,消息进入 FIFO 队列而不是被丢弃;当前回合结束后按入队顺序
|
||||||
* 依次发出。队列项能在输入盒上方单独取消。这里只放与 React 无关的纯逻辑,便于单测。
|
* 依次发出。队列项能在输入盒上方单独取消。这里只放与 React 无关的纯逻辑,便于单测。
|
||||||
*/
|
*/
|
||||||
import type { DirectCodexTurnAttachment } from '../app-shell/directCodexTurnAttachments';
|
import type { GameCreationAppAssetManifestEntry } from '../../../../../packages/shared/src/contracts/gameCreationApp';
|
||||||
import type { DirectCodexUserContentPart } from './generated';
|
import type { DirectCodexUserItem } from './generated';
|
||||||
import type { ChatReference } from './resourceReferences';
|
import { directCodexContentToPromptText } from './resourceReferences';
|
||||||
|
|
||||||
/** 队列上限:满了以后拒绝入队并给出可读提示,而不是静默丢消息。 */
|
/** 队列上限:满了以后拒绝入队并给出可读提示,而不是静默丢消息。 */
|
||||||
export const MAX_QUEUED_CHAT_TURNS = 5;
|
export const MAX_QUEUED_CHAT_TURNS = 5;
|
||||||
|
|
||||||
export type QueuedChatTurn = {
|
export type QueuedChatTurn = {
|
||||||
id: string;
|
id: string;
|
||||||
prompt: string;
|
clientTurnId: string;
|
||||||
attachments: DirectCodexTurnAttachment[];
|
userItem: DirectCodexUserItem;
|
||||||
references: ChatReference[];
|
|
||||||
content?: DirectCodexUserContentPart[];
|
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createQueuedChatTurn(input: {
|
export function createQueuedChatTurn(input: {
|
||||||
id: string;
|
id: string;
|
||||||
prompt: string;
|
clientTurnId: string;
|
||||||
attachments?: readonly DirectCodexTurnAttachment[];
|
userItem: DirectCodexUserItem;
|
||||||
references?: readonly ChatReference[];
|
|
||||||
content?: readonly DirectCodexUserContentPart[];
|
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
}): QueuedChatTurn {
|
}): QueuedChatTurn {
|
||||||
return {
|
return {
|
||||||
id: input.id,
|
id: input.id,
|
||||||
prompt: input.prompt,
|
clientTurnId: input.clientTurnId,
|
||||||
attachments: [...(input.attachments ?? [])],
|
userItem: {
|
||||||
references: [...(input.references ?? [])],
|
...input.userItem,
|
||||||
content: [...(input.content ?? [])],
|
content: [...input.userItem.content],
|
||||||
|
},
|
||||||
createdAt: input.createdAt,
|
createdAt: input.createdAt,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -77,16 +74,30 @@ export function chatQueueFullNotice(): string {
|
|||||||
return `队列已满(最多 ${MAX_QUEUED_CHAT_TURNS} 条),请等当前回合结束后再发送`;
|
return `队列已满(最多 ${MAX_QUEUED_CHAT_TURNS} 条),请等当前回合结束后再发送`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 队列 chip 上显示的文字:单行、有长度上限。 */
|
/**
|
||||||
export function queuedChatTurnLabel(turn: QueuedChatTurn): string {
|
* 队列 chip 上显示的文字:与真实消息同一个派生(`directCodexContentToPromptText`),
|
||||||
const text = turn.prompt.trim().replace(/\s+/gu, ' ');
|
* 再压成单行并限长。
|
||||||
|
*
|
||||||
|
* `assets` 与聊天消息渲染同源(`manifest.assets`)且必填:`@` 引用按显示名展开,chip
|
||||||
|
* 与消息正文逐字一致,不会露出 `@asset:…` 这种内部 id。
|
||||||
|
*/
|
||||||
|
export function queuedChatTurnLabel(
|
||||||
|
turn: QueuedChatTurn,
|
||||||
|
assets: readonly GameCreationAppAssetManifestEntry[],
|
||||||
|
): string {
|
||||||
|
const text = directCodexContentToPromptText(turn.userItem.content, assets)
|
||||||
|
.trim()
|
||||||
|
.replace(/\s+/gu, ' ');
|
||||||
if (text) {
|
if (text) {
|
||||||
return text.length > 24 ? `${text.slice(0, 24)}…` : text;
|
return text.length > 24 ? `${text.slice(0, 24)}…` : text;
|
||||||
}
|
}
|
||||||
if (turn.attachments.length > 0) {
|
const attachment = turn.userItem.content.find(
|
||||||
return `附件 · ${turn.attachments[0]?.name ?? '未命名'}`;
|
(part) => part.type === 'agc_attachment_reference',
|
||||||
|
);
|
||||||
|
if (attachment?.type === 'agc_attachment_reference') {
|
||||||
|
return `附件 · ${attachment.name || '未命名'}`;
|
||||||
}
|
}
|
||||||
if (turn.references.length > 0) {
|
if (turn.userItem.content.some((part) => part.type !== 'input_text')) {
|
||||||
return '素材引用';
|
return '素材引用';
|
||||||
}
|
}
|
||||||
return '未命名消息';
|
return '未命名消息';
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user