Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 38ad2ce256 | |||
| 65f8e44a88 | |||
| ade1b7803e | |||
| 71000b6df1 | |||
| c047543825 | |||
| 34b3af1d2a | |||
| f7cba30b6b | |||
| 273f12633c | |||
| 5f5aa17152 | |||
| 3a57d9fbdf | |||
| 382b925ab9 | |||
| c1482012c6 | |||
| c3a17a6efc | |||
| 1bfdc3a760 | |||
| 1e27cd229d | |||
| 774710452f | |||
| aaecb82622 | |||
| 64ad24ac77 | |||
| 75ec3361dc | |||
| bc1dc868a1 | |||
| 50204ff7aa | |||
| 70fa160819 |
@@ -174,6 +174,14 @@ _Avoid_: mock 先行堆积、前后端各自发散、先做排行榜 UI
|
|||||||
|
|
||||||
## 项目开发对话(DirectProject)
|
## 项目开发对话(DirectProject)
|
||||||
|
|
||||||
|
**DirectProject 专属聊天模块**:
|
||||||
|
AGC 普通项目聊天的独立容器,拥有 DirectProject 的聊天状态、运行态订阅、历史读取、发送队列、附件和中止交互,并把聊天投影交给专属表现层渲染;它不承接 Supervisor、Design Agent 或 Planning V2 的运行态。
|
||||||
|
_Avoid_: 把 DirectProject 作为项目总控聊天的一个布尔分支、把四种 Agent 会话抽象成同一事实源
|
||||||
|
|
||||||
|
**项目工作台布局**:
|
||||||
|
承载本地项目的资源工作区、项目级工具和独立聊天产品路径的外层界面;布局拥有跨面板的账户/钱包入口,聊天模块只负责项目对话,不嵌套账户展示。
|
||||||
|
_Avoid_: 把钱包入口塞进聊天设置、让聊天组件拥有工作台级账户状态
|
||||||
|
|
||||||
**项目对话历史**:
|
**项目对话历史**:
|
||||||
AGC 本地项目内 Codex 原始对话条目的持久集合,是聊天展示、工具卡片和线程恢复注入的唯一持久事实源。
|
AGC 本地项目内 Codex 原始对话条目的持久集合,是聊天展示、工具卡片和线程恢复注入的唯一持久事实源。
|
||||||
_Avoid_: 会话缓存、展示态历史、按 UI 需要另存的对话副本
|
_Avoid_: 会话缓存、展示态历史、按 UI 需要另存的对话副本
|
||||||
|
|||||||
@@ -2960,8 +2960,22 @@ impl CodexAppServerConnection {
|
|||||||
codex_app_server_text_prompt(&request)
|
codex_app_server_text_prompt(&request)
|
||||||
.map_err(platform_llm::LlmError::InvalidRequest)?
|
.map_err(platform_llm::LlmError::InvalidRequest)?
|
||||||
};
|
};
|
||||||
let input =
|
let input = if self.inner.workspace_mode == CodexAppServerWorkspaceMode::DirectProject {
|
||||||
codex_app_server_turn_input(&request, &prompt, &self.inner.workspace_path).await?;
|
if let Some(item) = direct_user_item {
|
||||||
|
let canonical: DirectCodexUserItem = serde_json::from_value(item.clone())
|
||||||
|
.map_err(|error| platform_llm::LlmError::InvalidRequest(error.to_string()))?;
|
||||||
|
direct_codex_user_item_to_codex_turn_input(
|
||||||
|
&self.inner.workspace_path,
|
||||||
|
&canonical,
|
||||||
|
self.inner._skill_roots.as_deref().unwrap_or_default(),
|
||||||
|
)
|
||||||
|
.map_err(platform_llm::LlmError::InvalidRequest)?
|
||||||
|
} else {
|
||||||
|
codex_app_server_turn_input(&request, &prompt, &self.inner.workspace_path).await?
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
codex_app_server_turn_input(&request, &prompt, &self.inner.workspace_path).await?
|
||||||
|
};
|
||||||
let _direct_tool_bridge_turn_guard =
|
let _direct_tool_bridge_turn_guard =
|
||||||
if self.inner.workspace_mode == CodexAppServerWorkspaceMode::DirectProject {
|
if self.inner.workspace_mode == CodexAppServerWorkspaceMode::DirectProject {
|
||||||
Some(
|
Some(
|
||||||
|
|||||||
@@ -10,6 +10,6 @@ pub(crate) use model::{
|
|||||||
};
|
};
|
||||||
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::{
|
||||||
direct_codex_user_item_to_prompt, direct_codex_user_item_to_response_item,
|
direct_codex_user_item_to_codex_turn_input, direct_codex_user_item_to_prompt,
|
||||||
direct_codex_user_item_to_wire_input,
|
direct_codex_user_item_to_response_item, direct_codex_user_item_to_wire_input,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ pub(crate) enum DirectCodexUserContentPart {
|
|||||||
InputText { text: String },
|
InputText { text: String },
|
||||||
#[serde(rename = "agc_resource_reference")]
|
#[serde(rename = "agc_resource_reference")]
|
||||||
AgcResourceReference { resource_id: String },
|
AgcResourceReference { resource_id: String },
|
||||||
|
#[serde(rename = "agc_skill_reference")]
|
||||||
|
AgcSkillReference { name: 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.
|
/// Uploaded project attachment kept inline in canonical content.
|
||||||
|
|||||||
+16
-2
@@ -14,7 +14,7 @@ pub(crate) const MAX_DIRECT_CODEX_REFERENCES: usize = 32;
|
|||||||
pub(crate) fn validate_direct_codex_user_item(
|
pub(crate) fn validate_direct_codex_user_item(
|
||||||
root: &Path,
|
root: &Path,
|
||||||
item: &DirectCodexUserItem,
|
item: &DirectCodexUserItem,
|
||||||
) -> Result<(), String> {
|
) -> Result<GameCreationAppManifest, String> {
|
||||||
let DirectCodexUserItem::Message(message) = item;
|
let DirectCodexUserItem::Message(message) = item;
|
||||||
if !matches!(message.role, DirectCodexUserRole::User) {
|
if !matches!(message.role, DirectCodexUserRole::User) {
|
||||||
return Err("DirectProject 只接受 user message item".to_string());
|
return Err("DirectProject 只接受 user message item".to_string());
|
||||||
@@ -37,6 +37,20 @@ pub(crate) fn validate_direct_codex_user_item(
|
|||||||
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)?;
|
||||||
}
|
}
|
||||||
|
DirectCodexUserContentPart::AgcSkillReference { name } => {
|
||||||
|
let name = name.trim();
|
||||||
|
if name.is_empty()
|
||||||
|
|| name.chars().count() > 120
|
||||||
|
|| matches!(name, "." | "..")
|
||||||
|
|| name.chars().any(|character| {
|
||||||
|
character.is_control()
|
||||||
|
|| character.is_whitespace()
|
||||||
|
|| matches!(character, '/' | '\\' | ':' | '$')
|
||||||
|
})
|
||||||
|
{
|
||||||
|
return Err("引用的 Skill 名称无效,请移除后重新选择".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
DirectCodexUserContentPart::AgcRuntimeRegionReference(reference) => {
|
DirectCodexUserContentPart::AgcRuntimeRegionReference(reference) => {
|
||||||
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)?;
|
||||||
@@ -84,7 +98,7 @@ pub(crate) fn validate_direct_codex_user_item(
|
|||||||
if reference_count > MAX_DIRECT_CODEX_REFERENCES {
|
if reference_count > MAX_DIRECT_CODEX_REFERENCES {
|
||||||
return Err(format!("一次最多引用 {MAX_DIRECT_CODEX_REFERENCES} 个素材"));
|
return Err(format!("一次最多引用 {MAX_DIRECT_CODEX_REFERENCES} 个素材"));
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(manifest)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 整条 content 是否还有有效输入:任何一段非空白文本、或任何一个非文本 part 都算。
|
/// 整条 content 是否还有有效输入:任何一段非空白文本、或任何一个非文本 part 都算。
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
use super::model::{DirectCodexUserContentPart, DirectCodexUserItem};
|
use super::model::{
|
||||||
|
DirectCodexUserContentPart, DirectCodexUserItem, DirectCodexUserRuntimeRegionPart,
|
||||||
|
};
|
||||||
use super::validation::validate_direct_codex_user_item;
|
use super::validation::validate_direct_codex_user_item;
|
||||||
use crate::agent::{
|
use crate::agent::{
|
||||||
read_manifest_for_project, sanitize_attachment_local_path, sanitize_attachment_media_type,
|
read_manifest_for_project, sanitize_attachment_local_path, sanitize_attachment_media_type,
|
||||||
sanitize_attachment_name,
|
sanitize_attachment_name,GameCreationAppManifest,
|
||||||
};
|
};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
@@ -54,6 +56,47 @@ fn direct_codex_user_item_to_response_content(
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn resource_reference_summary(
|
||||||
|
manifest: &GameCreationAppManifest,
|
||||||
|
resource_id: &str,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
let resource_id = resource_id.trim();
|
||||||
|
let asset = manifest
|
||||||
|
.assets
|
||||||
|
.iter()
|
||||||
|
.find(|asset| asset.id == resource_id)
|
||||||
|
.ok_or_else(|| "引用的素材已不存在,请移除后重新选择".to_string())?;
|
||||||
|
let path = sanitize_attachment_local_path(&asset.local_path)
|
||||||
|
.ok_or_else(|| "引用的素材路径无效,请移除后重新选择".to_string())?;
|
||||||
|
Ok(format!(
|
||||||
|
"[素材引用 resourceId={resource_id};项目路径={path}]"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn runtime_region_summary(reference: &DirectCodexUserRuntimeRegionPart) -> String {
|
||||||
|
let resources = reference
|
||||||
|
.resource_ids
|
||||||
|
.iter()
|
||||||
|
.map(|id| id.trim())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(",");
|
||||||
|
let mut summary = format!("[运行画面区域:名称={} ", reference.label.trim());
|
||||||
|
if let Some(run_id) = reference.run_id.as_deref() {
|
||||||
|
summary.push_str(&format!("运行标识={} ", run_id.trim()));
|
||||||
|
}
|
||||||
|
if let Some(role) = reference.element_role.as_deref() {
|
||||||
|
summary.push_str(&format!("角色={} ", role.trim()));
|
||||||
|
}
|
||||||
|
if let Some(text) = reference.text.as_deref() {
|
||||||
|
summary.push_str(&format!("文本={} ", text.trim()));
|
||||||
|
}
|
||||||
|
if !resources.is_empty() {
|
||||||
|
summary.push_str(&format!("关联素材={resources}"));
|
||||||
|
}
|
||||||
|
summary.push(']');
|
||||||
|
summary
|
||||||
|
}
|
||||||
|
|
||||||
/// 将 canonical user item 转为 app-server `turn/start.input` 可接受的文本数组。
|
/// 将 canonical user item 转为 app-server `turn/start.input` 可接受的文本数组。
|
||||||
/// AGC 私有 part 只在这里投影为安全摘要,canonical item 本身不被修改。
|
/// AGC 私有 part 只在这里投影为安全摘要,canonical item 本身不被修改。
|
||||||
pub(crate) fn direct_codex_user_item_to_wire_input(
|
pub(crate) fn direct_codex_user_item_to_wire_input(
|
||||||
@@ -68,40 +111,13 @@ pub(crate) fn direct_codex_user_item_to_wire_input(
|
|||||||
let text = match part {
|
let text = match part {
|
||||||
DirectCodexUserContentPart::InputText { text } => text.clone(),
|
DirectCodexUserContentPart::InputText { text } => text.clone(),
|
||||||
DirectCodexUserContentPart::AgcResourceReference { resource_id } => {
|
DirectCodexUserContentPart::AgcResourceReference { resource_id } => {
|
||||||
let asset = manifest
|
resource_reference_summary(&manifest, resource_id)?
|
||||||
.assets
|
}
|
||||||
.iter()
|
DirectCodexUserContentPart::AgcSkillReference { name } => {
|
||||||
.find(|asset| asset.id == resource_id.trim())
|
format!("${}", name.trim())
|
||||||
.ok_or_else(|| "引用的素材已不存在,请移除后重新选择".to_string())?;
|
|
||||||
let path = sanitize_attachment_local_path(&asset.local_path)
|
|
||||||
.ok_or_else(|| "引用的素材路径无效,请移除后重新选择".to_string())?;
|
|
||||||
format!(
|
|
||||||
"[素材引用 resourceId={};项目路径={path}]",
|
|
||||||
resource_id.trim()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
DirectCodexUserContentPart::AgcRuntimeRegionReference(reference) => {
|
DirectCodexUserContentPart::AgcRuntimeRegionReference(reference) => {
|
||||||
let resources = reference
|
runtime_region_summary(reference)
|
||||||
.resource_ids
|
|
||||||
.iter()
|
|
||||||
.map(|id| id.trim())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(",");
|
|
||||||
let mut summary = format!("[运行画面区域:名称={} ", reference.label.trim());
|
|
||||||
if let Some(run_id) = reference.run_id.as_deref() {
|
|
||||||
summary.push_str(&format!("运行标识={} ", run_id.trim()));
|
|
||||||
}
|
|
||||||
if let Some(role) = reference.element_role.as_deref() {
|
|
||||||
summary.push_str(&format!("角色={} ", role.trim()));
|
|
||||||
}
|
|
||||||
if let Some(text) = reference.text.as_deref() {
|
|
||||||
summary.push_str(&format!("文本={} ", text.trim()));
|
|
||||||
}
|
|
||||||
if !resources.is_empty() {
|
|
||||||
summary.push_str(&format!("关联素材={resources}"));
|
|
||||||
}
|
|
||||||
summary.push(']');
|
|
||||||
summary
|
|
||||||
}
|
}
|
||||||
DirectCodexUserContentPart::AgcAttachmentReference(reference) => {
|
DirectCodexUserContentPart::AgcAttachmentReference(reference) => {
|
||||||
let name = sanitize_attachment_name(&reference.name);
|
let name = sanitize_attachment_name(&reference.name);
|
||||||
@@ -124,6 +140,66 @@ pub(crate) fn direct_codex_user_item_to_wire_input(
|
|||||||
Ok(Value::Array(input))
|
Ok(Value::Array(input))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn direct_codex_user_item_to_codex_turn_input(
|
||||||
|
root: &Path,
|
||||||
|
item: &DirectCodexUserItem,
|
||||||
|
skill_roots: &[std::path::PathBuf],
|
||||||
|
) -> Result<Value, String> {
|
||||||
|
let manifest = validate_direct_codex_user_item(root, item)?;
|
||||||
|
let DirectCodexUserItem::Message(message) = item;
|
||||||
|
let mut input = Vec::with_capacity(message.content.len());
|
||||||
|
for part in &message.content {
|
||||||
|
match part {
|
||||||
|
DirectCodexUserContentPart::InputText { text } => {
|
||||||
|
input.push(serde_json::json!({ "type": "text", "text": text }));
|
||||||
|
}
|
||||||
|
DirectCodexUserContentPart::AgcResourceReference { resource_id } => {
|
||||||
|
input.push(serde_json::json!({
|
||||||
|
"type": "text",
|
||||||
|
"text": resource_reference_summary(&manifest, resource_id)?,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
DirectCodexUserContentPart::AgcSkillReference { name } => {
|
||||||
|
let name = name.trim();
|
||||||
|
let path = skill_roots
|
||||||
|
.iter()
|
||||||
|
.map(|root| root.join(name).join("SKILL.md"))
|
||||||
|
.find(|path| path.is_file())
|
||||||
|
.ok_or_else(|| "引用的 Skill 当前不可用,请重新选择".to_string())?;
|
||||||
|
input.push(serde_json::json!({
|
||||||
|
"type": "skill",
|
||||||
|
"name": name,
|
||||||
|
"path": path,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
DirectCodexUserContentPart::AgcRuntimeRegionReference(reference) => {
|
||||||
|
input.push(serde_json::json!({
|
||||||
|
"type": "text",
|
||||||
|
"text": runtime_region_summary(reference),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
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(']');
|
||||||
|
input.push(serde_json::json!({
|
||||||
|
"type": "text",
|
||||||
|
"text": summary,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Value::Array(input))
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn direct_codex_user_item_to_prompt(
|
pub(crate) fn direct_codex_user_item_to_prompt(
|
||||||
root: &Path,
|
root: &Path,
|
||||||
item: &DirectCodexUserItem,
|
item: &DirectCodexUserItem,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use serde::Deserialize;
|
use serde::{Deserialize, Serialize};
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
@@ -121,6 +121,13 @@ struct AgcSkillManifestEntry {
|
|||||||
sha256: String,
|
sha256: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub(crate) struct AgcSkillCatalogEntry {
|
||||||
|
pub(crate) name: String,
|
||||||
|
pub(crate) description: String,
|
||||||
|
}
|
||||||
|
|
||||||
fn is_safe_skill_relative_path(value: &str) -> bool {
|
fn is_safe_skill_relative_path(value: &str) -> bool {
|
||||||
let path = Path::new(value);
|
let path = Path::new(value);
|
||||||
!value.is_empty()
|
!value.is_empty()
|
||||||
@@ -234,6 +241,21 @@ pub(crate) fn agc_skill_pack_fingerprint() -> Result<String, String> {
|
|||||||
Ok(format!("{:x}", Sha256::digest(canonical_manifest.as_ref())))
|
Ok(format!("{:x}", Sha256::digest(canonical_manifest.as_ref())))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 返回当前客户端随 AGC 一起启用的内置 Skill 候选。
|
||||||
|
///
|
||||||
|
/// 前端不得复制审核清单;Skill 名称和描述统一从经过校验的资源 manifest 派生。
|
||||||
|
#[tauri::command]
|
||||||
|
pub(crate) fn list_agc_skill_catalog() -> Result<Vec<AgcSkillCatalogEntry>, String> {
|
||||||
|
Ok(validated_skill_pack_manifest()?
|
||||||
|
.skills
|
||||||
|
.into_iter()
|
||||||
|
.map(|entry| AgcSkillCatalogEntry {
|
||||||
|
name: entry.name,
|
||||||
|
description: entry.purpose,
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn render_agc_skill_pack_index() -> Result<String, String> {
|
pub(crate) fn render_agc_skill_pack_index() -> Result<String, String> {
|
||||||
let manifest = validated_skill_pack_manifest()?;
|
let manifest = validated_skill_pack_manifest()?;
|
||||||
let mut lines = vec![format!(
|
let mut lines = vec![format!(
|
||||||
@@ -328,6 +350,19 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn skill_catalog_is_derived_from_the_validated_manifest() {
|
||||||
|
let catalog = list_agc_skill_catalog().expect("skill catalog");
|
||||||
|
assert_eq!(catalog.len(), AGC_SKILL_PACK_EXPECTED_NAMES.len());
|
||||||
|
for expected_name in AGC_SKILL_PACK_EXPECTED_NAMES {
|
||||||
|
let entry = catalog
|
||||||
|
.iter()
|
||||||
|
.find(|entry| entry.name == expected_name)
|
||||||
|
.expect("expected bundled skill");
|
||||||
|
assert!(!entry.description.trim().is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn skill_content_digest_is_stable_across_lf_and_crlf() {
|
fn skill_content_digest_is_stable_across_lf_and_crlf() {
|
||||||
fn digest(bytes: &[u8]) -> String {
|
fn digest(bytes: &[u8]) -> String {
|
||||||
|
|||||||
@@ -482,43 +482,6 @@ fn automatic_local_game_projects_root(app: &tauri::AppHandle) -> Result<PathBuf,
|
|||||||
.map_err(|error| format!("无法读取 AGC 应用数据目录:{error}"))
|
.map_err(|error| format!("无法读取 AGC 应用数据目录:{error}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 校验用户选择的项目创建目录。
|
|
||||||
///
|
|
||||||
/// 目录必须已经存在(原生目录选择器返回的结果),并且先过 AGC 私有路径门禁:门禁失败时
|
|
||||||
/// 这里就拒绝,避免项目被建到 AGC 无法加固、后续无法打开的位置。
|
|
||||||
pub(crate) fn validate_requested_game_project_creation_root(
|
|
||||||
requested: &str,
|
|
||||||
) -> Result<PathBuf, String> {
|
|
||||||
let requested = requested.trim();
|
|
||||||
let root = Path::new(requested);
|
|
||||||
if requested.is_empty() || !root.is_absolute() {
|
|
||||||
return Err("项目创建目录必须是绝对路径".to_string());
|
|
||||||
}
|
|
||||||
if project_path_has_control_chars(root) {
|
|
||||||
return Err("项目创建目录不能包含控制字符".to_string());
|
|
||||||
}
|
|
||||||
let metadata = fs::symlink_metadata(root)
|
|
||||||
.map_err(|error| format!("读取项目创建目录失败:{}: {error}", root.display()))?;
|
|
||||||
if metadata.file_type().is_symlink() || !metadata.is_dir() {
|
|
||||||
return Err("项目创建目录必须是普通文件夹".to_string());
|
|
||||||
}
|
|
||||||
// 用户选择的外部目录仍走显式的项目根准备:保留 user-selected 范围的一次性修复,
|
|
||||||
// 同时不放弃 reparse point / 非普通目录的失败关闭。
|
|
||||||
prepare_game_creator_project_root_for_read(root, true, "项目创建目录")?;
|
|
||||||
Ok(root.to_path_buf())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 解析本次建项要使用的根目录:没选就用 AGC 管理的默认目录,选了就用用户指定的目录。
|
|
||||||
pub(crate) fn resolve_game_project_creation_root(
|
|
||||||
app: &tauri::AppHandle,
|
|
||||||
requested: Option<&str>,
|
|
||||||
) -> Result<PathBuf, String> {
|
|
||||||
match requested.map(str::trim).filter(|value| !value.is_empty()) {
|
|
||||||
Some(requested) => validate_requested_game_project_creation_root(requested),
|
|
||||||
None => automatic_local_game_projects_root(app),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn create_automatic_local_game_project_at(
|
pub(crate) fn create_automatic_local_game_project_at(
|
||||||
projects_root: &Path,
|
projects_root: &Path,
|
||||||
requested_name: Option<&str>,
|
requested_name: Option<&str>,
|
||||||
@@ -588,10 +551,9 @@ pub(crate) fn create_automatic_local_game_project(
|
|||||||
app: tauri::AppHandle,
|
app: tauri::AppHandle,
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
planning: Option<bool>,
|
planning: Option<bool>,
|
||||||
projects_root: Option<String>,
|
|
||||||
) -> Result<InitLocalProjectResult, String> {
|
) -> Result<InitLocalProjectResult, String> {
|
||||||
create_automatic_local_game_project_at(
|
create_automatic_local_game_project_at(
|
||||||
&resolve_game_project_creation_root(&app, projects_root.as_deref())?,
|
&automatic_local_game_projects_root(&app)?,
|
||||||
name.as_deref(),
|
name.as_deref(),
|
||||||
planning.unwrap_or(false),
|
planning.unwrap_or(false),
|
||||||
)
|
)
|
||||||
@@ -802,30 +764,13 @@ pub(crate) fn recent_game_creator_run_trace(root: &Path) -> Option<GameCreationA
|
|||||||
serde_json::from_str::<GameCreationAgentRunTrace>(&content).ok()
|
serde_json::from_str::<GameCreationAgentRunTrace>(&content).ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 目录选择器标题:调用方只能给短标题,其余(超长、含控制字符、空白)一律回退默认文案。
|
|
||||||
fn pick_project_directory_title(title: Option<&str>) -> &str {
|
|
||||||
const MAX_TITLE_CHARS: usize = 24;
|
|
||||||
title
|
|
||||||
.map(str::trim)
|
|
||||||
.filter(|value| {
|
|
||||||
!value.is_empty()
|
|
||||||
&& value.chars().count() <= MAX_TITLE_CHARS
|
|
||||||
&& !value.chars().any(char::is_control)
|
|
||||||
})
|
|
||||||
.unwrap_or("选择游戏项目目录")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub(crate) async fn pick_local_project_directory(
|
pub(crate) async fn pick_local_project_directory(
|
||||||
app: tauri::AppHandle,
|
app: tauri::AppHandle,
|
||||||
initial_path: Option<String>,
|
initial_path: Option<String>,
|
||||||
title: Option<String>,
|
|
||||||
) -> Result<Option<String>, String> {
|
) -> Result<Option<String>, String> {
|
||||||
let (sender, receiver) = tokio::sync::oneshot::channel();
|
let (sender, receiver) = tokio::sync::oneshot::channel();
|
||||||
let mut dialog = app
|
let mut dialog = app.dialog().file().set_title("选择游戏项目目录");
|
||||||
.dialog()
|
|
||||||
.file()
|
|
||||||
.set_title(pick_project_directory_title(title.as_deref()));
|
|
||||||
if let Some(initial_path) = initial_path
|
if let Some(initial_path) = initial_path
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.map(str::trim)
|
.map(str::trim)
|
||||||
|
|||||||
@@ -2518,6 +2518,7 @@ fn main() {
|
|||||||
pick_client_extension_file,
|
pick_client_extension_file,
|
||||||
pick_client_extension_directory,
|
pick_client_extension_directory,
|
||||||
list_client_extensions,
|
list_client_extensions,
|
||||||
|
list_agc_skill_catalog,
|
||||||
import_client_extension,
|
import_client_extension,
|
||||||
set_client_extension_enabled,
|
set_client_extension_enabled,
|
||||||
rename_client_extension,
|
rename_client_extension,
|
||||||
|
|||||||
@@ -880,9 +880,12 @@ pub(crate) async fn create_automatic_local_game_project_from_template(
|
|||||||
template_version: String,
|
template_version: String,
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
planning: Option<bool>,
|
planning: Option<bool>,
|
||||||
projects_root: Option<String>,
|
|
||||||
) -> Result<InitLocalProjectResult, String> {
|
) -> Result<InitLocalProjectResult, String> {
|
||||||
let projects_root = crate::resolve_game_project_creation_root(&app, projects_root.as_deref())?;
|
let projects_root = app
|
||||||
|
.path()
|
||||||
|
.app_data_dir()
|
||||||
|
.map(|root| root.join("projects"))
|
||||||
|
.map_err(|error| format!("无法读取 AGC 应用数据目录:{error}"))?;
|
||||||
let cache_root = template_cache_root(&app)?;
|
let cache_root = template_cache_root(&app)?;
|
||||||
ensure_game_creator_private_directory_tree(&cache_root, "模板库缓存目录")?;
|
ensure_game_creator_private_directory_tree(&cache_root, "模板库缓存目录")?;
|
||||||
let record =
|
let record =
|
||||||
|
|||||||
@@ -1549,63 +1549,6 @@ fn automatic_local_game_project_allocates_unique_initialized_workspaces() {
|
|||||||
fs::remove_dir_all(projects_root).ok();
|
fs::remove_dir_all(projects_root).ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn requested_project_creation_root_accepts_only_an_absolute_regular_directory() {
|
|
||||||
let root = unique_project_path();
|
|
||||||
fs::create_dir_all(&root).expect("create creation-root fixture");
|
|
||||||
let not_a_directory = root.join("not-a-directory.txt");
|
|
||||||
fs::write(¬_a_directory, b"x").expect("write file fixture");
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
validate_requested_game_project_creation_root(" ").expect_err("blank root is rejected"),
|
|
||||||
"项目创建目录必须是绝对路径"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
validate_requested_game_project_creation_root("relative/projects")
|
|
||||||
.expect_err("relative root is rejected"),
|
|
||||||
"项目创建目录必须是绝对路径"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
validate_requested_game_project_creation_root(&format!("{}\\pro\nject", root.display()))
|
|
||||||
.expect_err("control character is rejected"),
|
|
||||||
"项目创建目录不能包含控制字符"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
validate_requested_game_project_creation_root(¬_a_directory.to_string_lossy())
|
|
||||||
.expect_err("file root is rejected"),
|
|
||||||
"项目创建目录必须是普通文件夹"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
validate_requested_game_project_creation_root(&format!(" {}\n", root.display()))
|
|
||||||
.expect("trimmed directory root is accepted"),
|
|
||||||
root
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
validate_requested_game_project_creation_root(&root.join("missing").to_string_lossy())
|
|
||||||
.is_err(),
|
|
||||||
"a not-yet-existing creation root must fail instead of being created silently"
|
|
||||||
);
|
|
||||||
|
|
||||||
fs::remove_dir_all(root).ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn automatic_local_game_project_creates_inside_the_requested_creation_root() {
|
|
||||||
let projects_root = unique_project_path();
|
|
||||||
fs::create_dir_all(&projects_root).expect("create creation-root fixture");
|
|
||||||
|
|
||||||
let requested = validate_requested_game_project_creation_root(&projects_root.to_string_lossy())
|
|
||||||
.expect("valid creation root");
|
|
||||||
let result = create_automatic_local_game_project_at(&requested, None, false)
|
|
||||||
.expect("create workspace in requested root");
|
|
||||||
|
|
||||||
let project_root = PathBuf::from(&result.project_path);
|
|
||||||
assert_eq!(project_root.parent(), Some(projects_root.as_path()));
|
|
||||||
assert!(project_root.join(".agent/manifest.json").is_file());
|
|
||||||
|
|
||||||
fs::remove_dir_all(projects_root).ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn automatic_local_game_project_accepts_only_a_safe_custom_name() {
|
fn automatic_local_game_project_accepts_only_a_safe_custom_name() {
|
||||||
let projects_root = unique_project_path();
|
let projects_root = unique_project_path();
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -17,15 +17,10 @@ import type {
|
|||||||
ProjectAgentRuntimeSummary,
|
ProjectAgentRuntimeSummary,
|
||||||
} from '../../view/project-development';
|
} from '../../view/project-development';
|
||||||
import type { ProjectManifestSnapshotMetadata } from '../../view/project-development/projectResourceLiveUpdateModel';
|
import type { ProjectManifestSnapshotMetadata } from '../../view/project-development/projectResourceLiveUpdateModel';
|
||||||
import {
|
import { isAbsoluteProjectPath } from '../project-summary/projectSummary';
|
||||||
isAbsoluteProjectPath,
|
|
||||||
projectPathHasControlCharacter,
|
|
||||||
} from '../project-summary/projectSummary';
|
|
||||||
|
|
||||||
const RECENT_WORKSPACES_STORAGE_KEY =
|
const RECENT_WORKSPACES_STORAGE_KEY =
|
||||||
'genarrative-ai-game-creator.recent-workspaces.v1';
|
'genarrative-ai-game-creator.recent-workspaces.v1';
|
||||||
const PROJECT_CREATION_DIRECTORY_STORAGE_KEY =
|
|
||||||
'genarrative-ai-game-creator.project-creation-directory.v1';
|
|
||||||
const SUPERVISOR_CHAT_DRAFT_STORAGE_PREFIX =
|
const SUPERVISOR_CHAT_DRAFT_STORAGE_PREFIX =
|
||||||
'genarrative.supervisor-chat.draft';
|
'genarrative.supervisor-chat.draft';
|
||||||
|
|
||||||
@@ -161,57 +156,6 @@ export function removeRecentWorkspace(path: string) {
|
|||||||
return recent;
|
return recent;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 「项目创建目录」偏好:空串表示沿用 AGC 管理的默认位置(应用数据目录下的 projects)。
|
|
||||||
*
|
|
||||||
* 这里只保存用户意图,不是授权凭据:目录授权来自原生目录选择器,并由 Rust 侧私有路径门禁
|
|
||||||
* 在每次建项时重新复核,所以存储被改坏最坏只是退回默认位置或拿到一次可见的建项失败。
|
|
||||||
*/
|
|
||||||
export function normalizeProjectCreationDirectory(value: string) {
|
|
||||||
const trimmed = value.trim();
|
|
||||||
if (!trimmed || projectPathHasControlCharacter(trimmed)) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
const withoutTrailingSeparator = trimmed.replace(/[\\/]+$/, '');
|
|
||||||
// `C:\` 这类盘根只去掉分隔符会变成相对路径 `C:`,必须补回来。
|
|
||||||
return /^[a-zA-Z]:$/.test(withoutTrailingSeparator)
|
|
||||||
? `${withoutTrailingSeparator}\\`
|
|
||||||
: withoutTrailingSeparator;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function readProjectCreationDirectory() {
|
|
||||||
try {
|
|
||||||
const raw = window.localStorage.getItem(
|
|
||||||
PROJECT_CREATION_DIRECTORY_STORAGE_KEY,
|
|
||||||
);
|
|
||||||
const parsed: unknown = raw ? JSON.parse(raw) : '';
|
|
||||||
if (typeof parsed !== 'string') {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
const directory = normalizeProjectCreationDirectory(parsed);
|
|
||||||
return isAbsoluteProjectPath(directory) ? directory : '';
|
|
||||||
} catch {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function writeProjectCreationDirectory(path: string) {
|
|
||||||
const directory = normalizeProjectCreationDirectory(path);
|
|
||||||
try {
|
|
||||||
if (directory) {
|
|
||||||
window.localStorage.setItem(
|
|
||||||
PROJECT_CREATION_DIRECTORY_STORAGE_KEY,
|
|
||||||
JSON.stringify(directory),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
window.localStorage.removeItem(PROJECT_CREATION_DIRECTORY_STORAGE_KEY);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// WebView storage can be unavailable in restricted test shells.
|
|
||||||
}
|
|
||||||
return directory;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isTransientProjectOpenMessage(
|
export function isTransientProjectOpenMessage(
|
||||||
message: ChatMessage,
|
message: ChatMessage,
|
||||||
projectPath: string,
|
projectPath: string,
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ import {
|
|||||||
isAbsoluteProjectPath,
|
isAbsoluteProjectPath,
|
||||||
projectPathHasControlCharacter,
|
projectPathHasControlCharacter,
|
||||||
} from '../project-summary/projectSummary';
|
} from '../project-summary/projectSummary';
|
||||||
import { readProjectCreationDirectory } from './model';
|
|
||||||
import { resolveSessionPreviewOnProjectOpen } from './sessionPreview';
|
import { resolveSessionPreviewOnProjectOpen } from './sessionPreview';
|
||||||
|
|
||||||
/** 首页输入框当前的纯文本(Lexical 编辑器状态 -> 文本);没有输入就返回空串。 */
|
/** 首页输入框当前的纯文本(Lexical 编辑器状态 -> 文本);没有输入就返回空串。 */
|
||||||
@@ -829,9 +828,6 @@ export function useHomeProjectCreation({
|
|||||||
{
|
{
|
||||||
name: suggestedName,
|
name: suggestedName,
|
||||||
planning: startMode === 'planning',
|
planning: startMode === 'planning',
|
||||||
// 用户在首页选过「项目创建目录」就用它;没选传 null,由 Rust 侧回落到
|
|
||||||
// AGC 管理的默认位置(应用数据目录下的 projects)。
|
|
||||||
projectsRoot: readProjectCreationDirectory() || null,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
createdProjectPath = result.projectPath;
|
createdProjectPath = result.projectPath;
|
||||||
|
|||||||
@@ -1,81 +0,0 @@
|
|||||||
import { useCallback, useRef, useState } from 'react';
|
|
||||||
|
|
||||||
import { resolveTauriInvoke } from '../../app/tauri';
|
|
||||||
import {
|
|
||||||
readProjectCreationDirectory,
|
|
||||||
writeProjectCreationDirectory,
|
|
||||||
} from './model';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 「项目创建目录」用户偏好。
|
|
||||||
*
|
|
||||||
* 默认沿用 AGC 管理的应用数据目录(`<app_data>/projects`);用户改选时必须走原生目录
|
|
||||||
* 选择器,因为只有它构成"用户显式选择"边界:选择结果当场按用户选择范围加固,后续建项
|
|
||||||
* 再由 Rust 侧私有路径门禁复核一次。
|
|
||||||
*/
|
|
||||||
export function useProjectCreationDirectory() {
|
|
||||||
const [projectCreationDirectory, setProjectCreationDirectory] = useState(
|
|
||||||
readProjectCreationDirectory,
|
|
||||||
);
|
|
||||||
const [projectCreationDirectoryBusy, setProjectCreationDirectoryBusy] =
|
|
||||||
useState(false);
|
|
||||||
const [projectCreationDirectoryStatus, setProjectCreationDirectoryStatus] =
|
|
||||||
useState('');
|
|
||||||
const pickInFlightRef = useRef(false);
|
|
||||||
|
|
||||||
const pickProjectCreationDirectory = useCallback(async () => {
|
|
||||||
if (pickInFlightRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const invoke = resolveTauriInvoke();
|
|
||||||
if (!invoke) {
|
|
||||||
setProjectCreationDirectoryStatus('需要在陶泥儿客户端内运行');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
pickInFlightRef.current = true;
|
|
||||||
setProjectCreationDirectoryBusy(true);
|
|
||||||
setProjectCreationDirectoryStatus('正在选择项目创建目录');
|
|
||||||
try {
|
|
||||||
const selected = await invoke<string | null>(
|
|
||||||
'pick_local_project_directory',
|
|
||||||
{
|
|
||||||
title: '选择项目创建目录',
|
|
||||||
...(projectCreationDirectory
|
|
||||||
? { initialPath: projectCreationDirectory }
|
|
||||||
: {}),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (!selected) {
|
|
||||||
setProjectCreationDirectoryStatus('已取消');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setProjectCreationDirectory(writeProjectCreationDirectory(selected));
|
|
||||||
setProjectCreationDirectoryStatus('已更新项目创建目录');
|
|
||||||
} catch (error) {
|
|
||||||
setProjectCreationDirectoryStatus(
|
|
||||||
error instanceof Error ? error.message : String(error),
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
pickInFlightRef.current = false;
|
|
||||||
setProjectCreationDirectoryBusy(false);
|
|
||||||
}
|
|
||||||
}, [projectCreationDirectory]);
|
|
||||||
|
|
||||||
const resetProjectCreationDirectory = useCallback(() => {
|
|
||||||
writeProjectCreationDirectory('');
|
|
||||||
setProjectCreationDirectory('');
|
|
||||||
setProjectCreationDirectoryStatus('已恢复默认位置');
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return {
|
|
||||||
projectCreationDirectory,
|
|
||||||
projectCreationDirectoryBusy,
|
|
||||||
projectCreationDirectoryStatus,
|
|
||||||
pickProjectCreationDirectory,
|
|
||||||
resetProjectCreationDirectory,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ProjectCreationDirectoryController = ReturnType<
|
|
||||||
typeof useProjectCreationDirectory
|
|
||||||
>;
|
|
||||||
+1
-1
@@ -2,7 +2,7 @@ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext
|
|||||||
import { $getNodeByKey, type NodeKey } from 'lexical';
|
import { $getNodeByKey, type NodeKey } from 'lexical';
|
||||||
import { Paperclip, X } from 'lucide-react';
|
import { Paperclip, X } from 'lucide-react';
|
||||||
|
|
||||||
import type { DirectCodexUserAttachmentReferencePart } from './generated';
|
import type { DirectCodexUserAttachmentReferencePart } from '../../view/project-development/chat/generated/DirectCodexUserAttachmentReferencePart';
|
||||||
|
|
||||||
export function AttachmentReferenceChip({
|
export function AttachmentReferenceChip({
|
||||||
attachment,
|
attachment,
|
||||||
|
|||||||
+1
-1
@@ -9,8 +9,8 @@ import {
|
|||||||
} from 'lexical';
|
} from 'lexical';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
|
import type { DirectCodexUserAttachmentReferencePart } from '../../view/project-development/chat/generated/DirectCodexUserAttachmentReferencePart';
|
||||||
import { AttachmentReferenceChip } from './AttachmentReferenceChip';
|
import { AttachmentReferenceChip } from './AttachmentReferenceChip';
|
||||||
import type { DirectCodexUserAttachmentReferencePart } from './generated';
|
|
||||||
|
|
||||||
export type SerializedAttachmentReferenceNode = Spread<
|
export type SerializedAttachmentReferenceNode = Spread<
|
||||||
{
|
{
|
||||||
|
|||||||
+1
-17
@@ -1,5 +1,4 @@
|
|||||||
import { Settings2, ShieldCheck, Wallet, X } from 'lucide-react';
|
import { Settings2, ShieldCheck, X } from 'lucide-react';
|
||||||
import type { ReactNode } from 'react';
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -17,19 +16,16 @@ import { RuntimeConfigDialog } from '../runtime-config/RuntimeConfigDialog';
|
|||||||
* 组成:
|
* 组成:
|
||||||
* - 「运行配置」打开既有 `RuntimeConfigDialog`(独立浮层,自己有 backdrop);
|
* - 「运行配置」打开既有 `RuntimeConfigDialog`(独立浮层,自己有 backdrop);
|
||||||
* - 「操作权限」打开由 `ApprovalModeDialog` 提供的选择面板;
|
* - 「操作权限」打开由 `ApprovalModeDialog` 提供的选择面板;
|
||||||
* - 「泥点」直接渲染父级传入的 `walletEntry`(为空则整行不渲染)。
|
|
||||||
*/
|
*/
|
||||||
export function ProjectSupervisorSettingsDialog({
|
export function ProjectSupervisorSettingsDialog({
|
||||||
projectPath,
|
projectPath,
|
||||||
currentApprovalLabel,
|
currentApprovalLabel,
|
||||||
onOpenApproval,
|
onOpenApproval,
|
||||||
walletEntry,
|
|
||||||
onClose,
|
onClose,
|
||||||
}: {
|
}: {
|
||||||
projectPath: string;
|
projectPath: string;
|
||||||
currentApprovalLabel: string;
|
currentApprovalLabel: string;
|
||||||
onOpenApproval: () => void;
|
onOpenApproval: () => void;
|
||||||
walletEntry?: ReactNode;
|
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}) {
|
}) {
|
||||||
const [runtimeConfigOpen, setRuntimeConfigOpen] = useState(false);
|
const [runtimeConfigOpen, setRuntimeConfigOpen] = useState(false);
|
||||||
@@ -90,18 +86,6 @@ export function ProjectSupervisorSettingsDialog({
|
|||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
{walletEntry ? (
|
|
||||||
<div className="project-supervisor-settings-row is-static">
|
|
||||||
<span className="project-supervisor-settings-row-main">
|
|
||||||
<Wallet size={16} aria-hidden="true" />
|
|
||||||
<span>
|
|
||||||
<strong>泥点</strong>
|
|
||||||
<small>当前余额与充值入口</small>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<div className="game-workbench-chat-wallet">{walletEntry}</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+90
-474
File diff suppressed because it is too large
Load Diff
+19
-8
@@ -4,6 +4,16 @@ import { X } from 'lucide-react';
|
|||||||
|
|
||||||
import type { ChatReference } from './resourceReferences';
|
import type { ChatReference } from './resourceReferences';
|
||||||
|
|
||||||
|
function chipTitle(reference: ChatReference) {
|
||||||
|
if (reference.type === 'resource') {
|
||||||
|
return `${reference.label} · ${reference.kind}`;
|
||||||
|
}
|
||||||
|
if (reference.type === 'skill') {
|
||||||
|
return `${reference.name} · Skill`;
|
||||||
|
}
|
||||||
|
return `${reference.label} · 运行区域`;
|
||||||
|
}
|
||||||
|
|
||||||
export function ResourceReferenceChip({
|
export function ResourceReferenceChip({
|
||||||
reference,
|
reference,
|
||||||
nodeKey,
|
nodeKey,
|
||||||
@@ -21,18 +31,19 @@ export function ResourceReferenceChip({
|
|||||||
data-runtime-region-reference={
|
data-runtime-region-reference={
|
||||||
reference.type === 'runtime-region' ? 'true' : undefined
|
reference.type === 'runtime-region' ? 'true' : undefined
|
||||||
}
|
}
|
||||||
contentEditable={false}
|
data-skill-reference-name={
|
||||||
title={
|
reference.type === 'skill' ? reference.name : undefined
|
||||||
reference.type === 'resource'
|
|
||||||
? `${reference.label} · ${reference.kind}`
|
|
||||||
: `${reference.label} · 运行区域`
|
|
||||||
}
|
}
|
||||||
|
contentEditable={false}
|
||||||
|
title={chipTitle(reference)}
|
||||||
>
|
>
|
||||||
<span aria-hidden="true">@</span>
|
<span aria-hidden="true">{reference.type === 'skill' ? '$' : '@'}</span>
|
||||||
<span className="resource-reference-chip-label">{reference.label}</span>
|
<span className="resource-reference-chip-label">
|
||||||
|
{reference.type === 'skill' ? reference.name : reference.label}
|
||||||
|
</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={`移除引用 ${reference.label}`}
|
aria-label={`移除引用 ${reference.type === 'skill' ? reference.name : reference.label}`}
|
||||||
onMouseDown={(event) => event.preventDefault()}
|
onMouseDown={(event) => event.preventDefault()}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
editor.update(() => {
|
editor.update(() => {
|
||||||
|
|||||||
+170
-32
@@ -61,6 +61,7 @@ import {
|
|||||||
createProjectResourcePreviewRequestId,
|
createProjectResourcePreviewRequestId,
|
||||||
createProjectResourcePreviewScopeId,
|
createProjectResourcePreviewScopeId,
|
||||||
} from '../../services/projectResourcePreviewTransport';
|
} from '../../services/projectResourcePreviewTransport';
|
||||||
|
import type { DirectCodexUserContentPart } from '../../view/project-development/chat/generated/DirectCodexUserContentPart';
|
||||||
import {
|
import {
|
||||||
$createAttachmentReferenceNode,
|
$createAttachmentReferenceNode,
|
||||||
$isAttachmentReferenceNode,
|
$isAttachmentReferenceNode,
|
||||||
@@ -73,7 +74,6 @@ import {
|
|||||||
writeChatPromptPolishReminderDisabled,
|
writeChatPromptPolishReminderDisabled,
|
||||||
} from './chatPromptPolish';
|
} from './chatPromptPolish';
|
||||||
import { ChatPromptPolishReminder } from './ChatPromptPolishReminder';
|
import { ChatPromptPolishReminder } from './ChatPromptPolishReminder';
|
||||||
import type { DirectCodexUserContentPart } from './generated';
|
|
||||||
import {
|
import {
|
||||||
$createResourceReferenceNode,
|
$createResourceReferenceNode,
|
||||||
$isResourceReferenceNode,
|
$isResourceReferenceNode,
|
||||||
@@ -98,6 +98,7 @@ import {
|
|||||||
resourceReferenceMatchesTagSelection,
|
resourceReferenceMatchesTagSelection,
|
||||||
type ResourceReferenceScope,
|
type ResourceReferenceScope,
|
||||||
resourceReferenceTagLibrary,
|
resourceReferenceTagLibrary,
|
||||||
|
type SkillReference,
|
||||||
} from './resourceReferences';
|
} from './resourceReferences';
|
||||||
import { usePromptPolish } from './usePromptPolish';
|
import { usePromptPolish } from './usePromptPolish';
|
||||||
|
|
||||||
@@ -108,6 +109,7 @@ type ResourceReferenceInputProps = {
|
|||||||
onEditorStateChange?: (editorState: EditorState) => void;
|
onEditorStateChange?: (editorState: EditorState) => void;
|
||||||
initialContent?: DirectCodexUserContentPart[];
|
initialContent?: DirectCodexUserContentPart[];
|
||||||
assets: GameCreationAppAssetManifestEntry[];
|
assets: GameCreationAppAssetManifestEntry[];
|
||||||
|
skills?: SkillReference[];
|
||||||
projectPath: string;
|
projectPath: string;
|
||||||
/**
|
/**
|
||||||
* `@` 面板「当前版本素材」页签使用的版本 id。
|
* `@` 面板「当前版本素材」页签使用的版本 id。
|
||||||
@@ -181,12 +183,22 @@ class ResourceMentionOption extends MenuOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SkillMentionOption extends MenuOption {
|
||||||
|
skill: SkillReference;
|
||||||
|
|
||||||
|
constructor(skill: SkillReference) {
|
||||||
|
super(skill.name);
|
||||||
|
this.skill = skill;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 编辑器节点 → canonical content part,**原样透传**:段落分隔(root 子节点之间补的
|
* 编辑器节点 → canonical content part,**原样透传**:段落分隔(root 子节点之间补的
|
||||||
* `\n`)、软换行、chip 后的分隔空格都各自成 part,不做空白过滤、不与相邻 part 合并。
|
* `\n`)、软换行、chip 后的分隔空格都各自成 part,不做空白过滤、不与相邻 part 合并。
|
||||||
* 有效输入只在整条 content 上判定(`hasMeaningfulDirectCodexContent` 与 Rust
|
* 有效输入只在整条 content 上判定(`hasMeaningfulDirectCodexContent` 与 Rust
|
||||||
* `validate_direct_codex_user_item` 同口径),前端不替用户改写他输入了什么。
|
* `validate_direct_codex_user_item` 同口径),前端不替用户改写他输入了什么。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function collectDraftParts(
|
function collectDraftParts(
|
||||||
node: LexicalNode,
|
node: LexicalNode,
|
||||||
references: ChatReference[],
|
references: ChatReference[],
|
||||||
@@ -244,8 +256,12 @@ function readDraftFromNodes(): ChatComposerDraft {
|
|||||||
const projection = readDraftProjectionFromNodes();
|
const projection = readDraftProjectionFromNodes();
|
||||||
const labels = new Map(
|
const labels = new Map(
|
||||||
projection.references.map((reference) => [
|
projection.references.map((reference) => [
|
||||||
reference.type === 'resource' ? reference.resourceId : reference.label,
|
reference.type === 'resource'
|
||||||
`@${reference.label}`,
|
? reference.resourceId
|
||||||
|
: reference.type === 'skill'
|
||||||
|
? reference.name
|
||||||
|
: reference.label,
|
||||||
|
reference.type === 'skill' ? `$${reference.name}` : `@${reference.label}`,
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
const text = projection.content
|
const text = projection.content
|
||||||
@@ -256,7 +272,7 @@ function readDraftFromNodes(): ChatComposerDraft {
|
|||||||
? (labels.get(part.resourceId) ?? `@${part.resourceId}`)
|
? (labels.get(part.resourceId) ?? `@${part.resourceId}`)
|
||||||
: part.type === 'agc_runtime_region_reference'
|
: part.type === 'agc_runtime_region_reference'
|
||||||
? `@${part.label}`
|
? `@${part.label}`
|
||||||
: `@${part.name}`,
|
: `$${part.name}`,
|
||||||
)
|
)
|
||||||
.join('')
|
.join('')
|
||||||
.trim();
|
.trim();
|
||||||
@@ -308,7 +324,7 @@ function findDraftMentionToken(line: string, token: string, from: number) {
|
|||||||
* 把「文本 + 引用列表」这对外部表示切成编辑区要写的内容。
|
* 把「文本 + 引用列表」这对外部表示切成编辑区要写的内容。
|
||||||
*
|
*
|
||||||
* 不变量:写入编辑器后,编辑器读回的草稿就是唯一当前值。`collectDraftParts`
|
* 不变量:写入编辑器后,编辑器读回的草稿就是唯一当前值。`collectDraftParts`
|
||||||
* 会把每个 chip 读成一段 `@显示名` 文本,所以 chip 只能内联嵌在文本里同名 token 的位置上,
|
* 会把每个 chip 读成一段 `@显示名` / `$skill-name` 文本,所以 chip 只能内联嵌在文本里同名 token 的位置上,
|
||||||
* 不能另起一段堆在末尾;编辑器本身是唯一 authority,不再通过 props 比较后反复重建。
|
* 不能另起一段堆在末尾;编辑器本身是唯一 authority,不再通过 props 比较后反复重建。
|
||||||
*
|
*
|
||||||
* 文本里已经没有对应 token 的引用(例如 AI 润色整体改写了文本)内联补到最后一段末尾,
|
* 文本里已经没有对应 token 的引用(例如 AI 润色整体改写了文本)内联补到最后一段末尾,
|
||||||
@@ -320,7 +336,8 @@ function buildDraftSegments(
|
|||||||
): DraftBuildSegment[][] {
|
): DraftBuildSegment[][] {
|
||||||
const pending = references.map((reference) => ({
|
const pending = references.map((reference) => ({
|
||||||
reference,
|
reference,
|
||||||
token: `@${reference.label}`,
|
token:
|
||||||
|
reference.type === 'skill' ? `$${reference.name}` : `@${reference.label}`,
|
||||||
used: false,
|
used: false,
|
||||||
}));
|
}));
|
||||||
const lines: DraftBuildSegment[][] = [];
|
const lines: DraftBuildSegment[][] = [];
|
||||||
@@ -393,6 +410,9 @@ function referenceFromContentPart(
|
|||||||
const asset = assetsById.get(part.resourceId);
|
const asset = assetsById.get(part.resourceId);
|
||||||
return asset ? resourceReferenceFromAsset(asset, 'asset-picker') : null;
|
return asset ? resourceReferenceFromAsset(asset, 'asset-picker') : null;
|
||||||
}
|
}
|
||||||
|
if (part.type === 'agc_skill_reference') {
|
||||||
|
return { type: 'skill', name: part.name };
|
||||||
|
}
|
||||||
if (part.type === 'agc_runtime_region_reference') {
|
if (part.type === 'agc_runtime_region_reference') {
|
||||||
return {
|
return {
|
||||||
type: 'runtime-region',
|
type: 'runtime-region',
|
||||||
@@ -540,6 +560,7 @@ function ResourceReferenceEditor({
|
|||||||
onEditorStateChange,
|
onEditorStateChange,
|
||||||
initialContent,
|
initialContent,
|
||||||
assets,
|
assets,
|
||||||
|
skills = [],
|
||||||
projectPath,
|
projectPath,
|
||||||
activeVersionId = null,
|
activeVersionId = null,
|
||||||
versions,
|
versions,
|
||||||
@@ -556,14 +577,18 @@ function ResourceReferenceEditor({
|
|||||||
const [editor] = useLexicalComposerContext();
|
const [editor] = useLexicalComposerContext();
|
||||||
const skipInitialDraftChangeRef = useRef(false);
|
const skipInitialDraftChangeRef = useRef(false);
|
||||||
const [query, setQuery] = useState<string | null>(null);
|
const [query, setQuery] = useState<string | null>(null);
|
||||||
|
const [skillQuery, setSkillQuery] = useState<string | null>(null);
|
||||||
|
const [builtinSkills, setBuiltinSkills] = useState<SkillReference[]>([]);
|
||||||
|
const [clientSkills, setClientSkills] = useState<SkillReference[]>([]);
|
||||||
|
const skillCatalogRequestedRef = useRef(false);
|
||||||
const [pickerOpen, setPickerOpen] = useState(false);
|
const [pickerOpen, setPickerOpen] = useState(false);
|
||||||
// Enter 提交要避开候选浮层。用 ref 让命令处理器读到最新状态,不必因为
|
// Enter 提交要避开候选浮层。用 ref 让命令处理器读到最新状态,不必因为
|
||||||
// query / pickerOpen 变化反复重注册 HIGH 优先级命令。
|
// query / pickerOpen 变化反复重注册 HIGH 优先级命令。
|
||||||
const mentionMenuOpenRef = useRef(false);
|
const mentionMenuOpenRef = useRef(false);
|
||||||
const pickerVisibleRef = useRef(false);
|
const pickerVisibleRef = useRef(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
mentionMenuOpenRef.current = query !== null;
|
mentionMenuOpenRef.current = query !== null || skillQuery !== null;
|
||||||
}, [query]);
|
}, [query, skillQuery]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
pickerVisibleRef.current = pickerOpen;
|
pickerVisibleRef.current = pickerOpen;
|
||||||
}, [pickerOpen]);
|
}, [pickerOpen]);
|
||||||
@@ -577,6 +602,53 @@ function ResourceReferenceEditor({
|
|||||||
bottom: number;
|
bottom: number;
|
||||||
width: number;
|
width: number;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
// Skill 清单来自宿主:只在用户真正打开 `$` 候选时查一次。输入区挂载即发起
|
||||||
|
// Tauri 调用会让「工作区路径非法时不产生任何后端访问」的边界失效。
|
||||||
|
useEffect(() => {
|
||||||
|
if (skillQuery === null || skillCatalogRequestedRef.current) return;
|
||||||
|
const invoke = resolveTauriInvoke();
|
||||||
|
if (!invoke) return;
|
||||||
|
skillCatalogRequestedRef.current = true;
|
||||||
|
void invoke<Array<{ name: string; description: string }>>(
|
||||||
|
'list_agc_skill_catalog',
|
||||||
|
)
|
||||||
|
.then((items) => {
|
||||||
|
setBuiltinSkills(
|
||||||
|
items.map((item) => ({
|
||||||
|
type: 'skill' as const,
|
||||||
|
name: item.name,
|
||||||
|
description: item.description,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setBuiltinSkills([]);
|
||||||
|
});
|
||||||
|
void invoke<
|
||||||
|
Array<{
|
||||||
|
name: string;
|
||||||
|
extensionType: string;
|
||||||
|
enabled: boolean;
|
||||||
|
status: string;
|
||||||
|
}>
|
||||||
|
>('list_client_extensions')
|
||||||
|
.then((items) => {
|
||||||
|
setClientSkills(
|
||||||
|
items
|
||||||
|
.filter(
|
||||||
|
(item) =>
|
||||||
|
item.extensionType === 'skill' &&
|
||||||
|
item.enabled &&
|
||||||
|
item.status === 'enabled',
|
||||||
|
)
|
||||||
|
.map((item) => ({ type: 'skill' as const, name: item.name })),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setClientSkills([]);
|
||||||
|
});
|
||||||
|
}, [skillQuery]);
|
||||||
|
|
||||||
const assetsContentSignature = assetsSignature(assets);
|
const assetsContentSignature = assetsSignature(assets);
|
||||||
const versionsContentSignature = iterationsSignature(versions);
|
const versionsContentSignature = iterationsSignature(versions);
|
||||||
const assetsById = useMemo(
|
const assetsById = useMemo(
|
||||||
@@ -641,11 +713,35 @@ function ResourceReferenceEditor({
|
|||||||
.map((reference) => new ResourceMentionOption(reference));
|
.map((reference) => new ResourceMentionOption(reference));
|
||||||
}, [assetReferences, query]);
|
}, [assetReferences, query]);
|
||||||
|
|
||||||
|
const skillOptions = useMemo(() => {
|
||||||
|
if (skillQuery === null) return [];
|
||||||
|
const normalized = skillQuery.trim().toLowerCase();
|
||||||
|
const allSkills = [...builtinSkills, ...clientSkills, ...skills];
|
||||||
|
const seen = new Set<string>();
|
||||||
|
return allSkills
|
||||||
|
.filter((skill) => {
|
||||||
|
if (seen.has(skill.name)) return false;
|
||||||
|
seen.add(skill.name);
|
||||||
|
return (
|
||||||
|
!normalized ||
|
||||||
|
skill.name.toLowerCase().includes(normalized) ||
|
||||||
|
skill.description?.toLowerCase().includes(normalized)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.slice(0, 8)
|
||||||
|
.map((skill) => new SkillMentionOption(skill));
|
||||||
|
}, [builtinSkills, clientSkills, skillQuery, skills]);
|
||||||
|
|
||||||
const triggerFn = useBasicTypeaheadTriggerMatch('@', {
|
const triggerFn = useBasicTypeaheadTriggerMatch('@', {
|
||||||
minLength: 0,
|
minLength: 0,
|
||||||
maxLength: 64,
|
maxLength: 64,
|
||||||
allowWhitespace: false,
|
allowWhitespace: false,
|
||||||
});
|
});
|
||||||
|
const skillTriggerFn = useBasicTypeaheadTriggerMatch('$', {
|
||||||
|
minLength: 0,
|
||||||
|
maxLength: 64,
|
||||||
|
allowWhitespace: false,
|
||||||
|
});
|
||||||
|
|
||||||
const insertReferences = useCallback(
|
const insertReferences = useCallback(
|
||||||
(nextReferences: ChatReference[]) => {
|
(nextReferences: ChatReference[]) => {
|
||||||
@@ -771,13 +867,13 @@ function ResourceReferenceEditor({
|
|||||||
const currentRefKey = current.references
|
const currentRefKey = current.references
|
||||||
?.map(
|
?.map(
|
||||||
(reference) =>
|
(reference) =>
|
||||||
`${reference.type}:${reference.type === 'resource' ? reference.resourceId : reference.label}`,
|
`${reference.type}:${reference.type === 'resource' ? reference.resourceId : reference.type === 'skill' ? reference.name : reference.label}`,
|
||||||
)
|
)
|
||||||
.join('|');
|
.join('|');
|
||||||
const desiredRefKey = desiredRefs
|
const desiredRefKey = desiredRefs
|
||||||
.map(
|
.map(
|
||||||
(reference) =>
|
(reference) =>
|
||||||
`${reference.type}:${reference.type === 'resource' ? reference.resourceId : reference.label}`,
|
`${reference.type}:${reference.type === 'resource' ? reference.resourceId : reference.type === 'skill' ? reference.name : reference.label}`,
|
||||||
)
|
)
|
||||||
.join('|');
|
.join('|');
|
||||||
if (current.text === desiredValue && currentRefKey === desiredRefKey)
|
if (current.text === desiredValue && currentRefKey === desiredRefKey)
|
||||||
@@ -854,30 +950,47 @@ function ResourceReferenceEditor({
|
|||||||
[editor, pickerOpen],
|
[editor, pickerOpen],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const insertReferenceNode = useCallback(
|
||||||
|
(
|
||||||
|
reference: ChatReference,
|
||||||
|
textNodeContainingQuery: TextNode | null,
|
||||||
|
closeMenu: () => void,
|
||||||
|
) => {
|
||||||
|
textNodeContainingQuery?.remove();
|
||||||
|
const selection = $getSelection();
|
||||||
|
const node = $createResourceReferenceNode(reference);
|
||||||
|
if ($isRangeSelection(selection)) {
|
||||||
|
selection.insertNodes([node, $createTextNode(' ')]);
|
||||||
|
} else {
|
||||||
|
const paragraph = $createParagraphNode();
|
||||||
|
paragraph.append(node, $createTextNode(' '));
|
||||||
|
$getRoot().append(paragraph);
|
||||||
|
}
|
||||||
|
closeMenu();
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
const handleSelectMention = useCallback(
|
const handleSelectMention = useCallback(
|
||||||
(
|
(
|
||||||
option: ResourceMentionOption,
|
option: ResourceMentionOption,
|
||||||
textNodeContainingQuery: TextNode | null,
|
textNodeContainingQuery: TextNode | null,
|
||||||
closeMenu: () => void,
|
closeMenu: () => void,
|
||||||
) => {
|
) => {
|
||||||
textNodeContainingQuery?.remove();
|
insertReferenceNode(option.reference, textNodeContainingQuery, closeMenu);
|
||||||
const selection = $getSelection();
|
|
||||||
if ($isRangeSelection(selection)) {
|
|
||||||
selection.insertNodes([
|
|
||||||
$createResourceReferenceNode(option.reference),
|
|
||||||
$createTextNode(' '),
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
const paragraph = $createParagraphNode();
|
|
||||||
paragraph.append(
|
|
||||||
$createResourceReferenceNode(option.reference),
|
|
||||||
$createTextNode(' '),
|
|
||||||
);
|
|
||||||
$getRoot().append(paragraph);
|
|
||||||
}
|
|
||||||
closeMenu();
|
|
||||||
},
|
},
|
||||||
[],
|
[insertReferenceNode],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSelectSkill = useCallback(
|
||||||
|
(
|
||||||
|
option: SkillMentionOption,
|
||||||
|
textNodeContainingQuery: TextNode | null,
|
||||||
|
closeMenu: () => void,
|
||||||
|
) => {
|
||||||
|
insertReferenceNode(option.skill, textNodeContainingQuery, closeMenu);
|
||||||
|
},
|
||||||
|
[insertReferenceNode],
|
||||||
);
|
);
|
||||||
|
|
||||||
// —— C8 AI 润色与发送前提醒 ——
|
// —— C8 AI 润色与发送前提醒 ——
|
||||||
@@ -1017,7 +1130,9 @@ function ResourceReferenceEditor({
|
|||||||
acknowledgedDraftKeyRef.current = null;
|
acknowledgedDraftKeyRef.current = null;
|
||||||
}, [resetPromptPolish]);
|
}, [resetPromptPolish]);
|
||||||
|
|
||||||
const renderMentionMenu: MenuRenderFn<ResourceMentionOption> = useCallback(
|
const renderMentionMenu: MenuRenderFn<
|
||||||
|
ResourceMentionOption | SkillMentionOption
|
||||||
|
> = useCallback(
|
||||||
(_anchorElementRef, itemProps) => {
|
(_anchorElementRef, itemProps) => {
|
||||||
const inputRect = rootRef.current?.getBoundingClientRect();
|
const inputRect = rootRef.current?.getBoundingClientRect();
|
||||||
if (!inputRect || itemProps.options.length === 0) {
|
if (!inputRect || itemProps.options.length === 0) {
|
||||||
@@ -1052,7 +1167,7 @@ function ResourceReferenceEditor({
|
|||||||
<div
|
<div
|
||||||
className="resource-reference-menu"
|
className="resource-reference-menu"
|
||||||
role="listbox"
|
role="listbox"
|
||||||
aria-label="候选素材"
|
aria-label="候选引用"
|
||||||
style={{
|
style={{
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
top: `${top}px`,
|
top: `${top}px`,
|
||||||
@@ -1075,8 +1190,16 @@ function ResourceReferenceEditor({
|
|||||||
onMouseDown={(event) => event.preventDefault()}
|
onMouseDown={(event) => event.preventDefault()}
|
||||||
onClick={() => itemProps.selectOptionAndCleanUp(option)}
|
onClick={() => itemProps.selectOptionAndCleanUp(option)}
|
||||||
>
|
>
|
||||||
<span>{option.reference.label}</span>
|
<span>
|
||||||
<small>{option.reference.kind}</small>
|
{'reference' in option
|
||||||
|
? `@${option.reference.label}`
|
||||||
|
: `$${option.skill.name}`}
|
||||||
|
</span>
|
||||||
|
<small>
|
||||||
|
{'reference' in option
|
||||||
|
? option.reference.kind
|
||||||
|
: (option.skill.description ?? 'Skill')}
|
||||||
|
</small>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>,
|
</div>,
|
||||||
@@ -1201,7 +1324,22 @@ function ResourceReferenceEditor({
|
|||||||
onSelectOption={(option, textNode, closeMenu) =>
|
onSelectOption={(option, textNode, closeMenu) =>
|
||||||
handleSelectMention(option, textNode, closeMenu)
|
handleSelectMention(option, textNode, closeMenu)
|
||||||
}
|
}
|
||||||
menuRenderFn={renderMentionMenu}
|
menuRenderFn={
|
||||||
|
renderMentionMenu as unknown as MenuRenderFn<ResourceMentionOption>
|
||||||
|
}
|
||||||
|
anchorClassName="resource-reference-menu-anchor"
|
||||||
|
preselectFirstItem
|
||||||
|
/>
|
||||||
|
<LexicalTypeaheadMenuPlugin<SkillMentionOption>
|
||||||
|
options={skillOptions}
|
||||||
|
triggerFn={skillTriggerFn}
|
||||||
|
onQueryChange={setSkillQuery}
|
||||||
|
onSelectOption={(option, textNode, closeMenu) =>
|
||||||
|
handleSelectSkill(option, textNode, closeMenu)
|
||||||
|
}
|
||||||
|
menuRenderFn={
|
||||||
|
renderMentionMenu as unknown as MenuRenderFn<SkillMentionOption>
|
||||||
|
}
|
||||||
anchorClassName="resource-reference-menu-anchor"
|
anchorClassName="resource-reference-menu-anchor"
|
||||||
preselectFirstItem
|
preselectFirstItem
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
* 依次发出。队列项能在输入盒上方单独取消。这里只放与 React 无关的纯逻辑,便于单测。
|
* 依次发出。队列项能在输入盒上方单独取消。这里只放与 React 无关的纯逻辑,便于单测。
|
||||||
*/
|
*/
|
||||||
import type { GameCreationAppAssetManifestEntry } from '../../../../../packages/shared/src/contracts/gameCreationApp';
|
import type { GameCreationAppAssetManifestEntry } from '../../../../../packages/shared/src/contracts/gameCreationApp';
|
||||||
import type { DirectCodexUserItem } from './generated';
|
import type { DirectCodexUserItem } from '../../view/project-development/chat/generated/DirectCodexUserItem';
|
||||||
import { directCodexContentToPromptText } from './resourceReferences';
|
import { directCodexContentToPromptText } from './resourceReferences';
|
||||||
|
|
||||||
/** 队列上限:满了以后拒绝入队并给出可读提示,而不是静默丢消息。 */
|
/** 队列上限:满了以后拒绝入队并给出可读提示,而不是静默丢消息。 */
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { resolveTauriInvoke } from '../../app/tauri';
|
import { resolveTauriInvoke } from '../../app/tauri';
|
||||||
import type { DirectCodexUserContentPart } from './generated';
|
import type { DirectCodexUserContentPart } from '../../view/project-development/chat/generated/DirectCodexUserContentPart';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 「不再提醒」偏好存本机 localStorage,不进 manifest、不进后端。
|
* 「不再提醒」偏好存本机 localStorage,不进 manifest、不进后端。
|
||||||
|
|||||||
@@ -5,13 +5,11 @@
|
|||||||
* 入口转发:前端不再自己抄一份形状,字段增删必须改 Rust。
|
* 入口转发:前端不再自己抄一份形状,字段增删必须改 Rust。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type {
|
export type { DirectThreadConsumeResult } from '../../view/project-development/chat/generated/DirectThreadConsumeResult';
|
||||||
DirectThreadConsumeResult,
|
export type { DirectThreadDeltaKind } from '../../view/project-development/chat/generated/DirectThreadDeltaKind';
|
||||||
DirectThreadDeltaKind,
|
export type { DirectThreadEvent } from '../../view/project-development/chat/generated/DirectThreadEvent';
|
||||||
DirectThreadEvent,
|
export type { DirectThreadFileChange } from '../../view/project-development/chat/generated/DirectThreadFileChange';
|
||||||
DirectThreadFileChange,
|
export type { DirectThreadHistorySlice } from '../../view/project-development/chat/generated/DirectThreadHistorySlice';
|
||||||
DirectThreadHistorySlice,
|
export type { DirectThreadItem } from '../../view/project-development/chat/generated/DirectThreadItem';
|
||||||
DirectThreadItem,
|
export type { DirectThreadRequestKind } from '../../view/project-development/chat/generated/DirectThreadRequestKind';
|
||||||
DirectThreadRequestKind,
|
export type { DirectThreadSubscriptionBootstrap } from '../../view/project-development/chat/generated/DirectThreadSubscriptionBootstrap';
|
||||||
DirectThreadSubscriptionBootstrap,
|
|
||||||
} from './generated';
|
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
export type { DirectCodexUserContentPart } from './DirectCodexUserContentPart';
|
|
||||||
export type { DirectCodexUserAttachmentReferencePart } from './DirectCodexUserAttachmentReferencePart';
|
|
||||||
export type { DirectCodexUserItem } from './DirectCodexUserItem';
|
|
||||||
export type { DirectCodexUserMessageItem } from './DirectCodexUserMessageItem';
|
|
||||||
export type { DirectCodexUserRole } from './DirectCodexUserRole';
|
|
||||||
export type { DirectCodexUserRuntimeRegionPart } from './DirectCodexUserRuntimeRegionPart';
|
|
||||||
export type { DirectCodexUserMessageEnvelope } from './DirectCodexUserMessageEnvelope';
|
|
||||||
export type { DirectThreadConsumeResult } from './DirectThreadConsumeResult';
|
|
||||||
export type { DirectThreadDeltaKind } from './DirectThreadDeltaKind';
|
|
||||||
export type { DirectThreadEvent } from './DirectThreadEvent';
|
|
||||||
export type { DirectThreadFileChange } from './DirectThreadFileChange';
|
|
||||||
export type { DirectThreadHistorySlice } from './DirectThreadHistorySlice';
|
|
||||||
export type { DirectThreadItem } from './DirectThreadItem';
|
|
||||||
export type { DirectThreadRequestKind } from './DirectThreadRequestKind';
|
|
||||||
export type { DirectThreadSubscriptionBootstrap } from './DirectThreadSubscriptionBootstrap';
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user