diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/skill_pack.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/skill_pack.rs index 5c5f9e602..1d5fc4861 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/skill_pack.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/skill_pack.rs @@ -1,4 +1,4 @@ -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use std::borrow::Cow; use std::collections::BTreeSet; @@ -121,6 +121,13 @@ struct AgcSkillManifestEntry { 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 { let path = Path::new(value); !value.is_empty() @@ -234,6 +241,21 @@ pub(crate) fn agc_skill_pack_fingerprint() -> Result { Ok(format!("{:x}", Sha256::digest(canonical_manifest.as_ref()))) } +/// 返回当前客户端随 AGC 一起启用的内置 Skill 候选。 +/// +/// 前端不得复制审核清单;Skill 名称和描述统一从经过校验的资源 manifest 派生。 +#[tauri::command] +pub(crate) fn list_agc_skill_catalog() -> Result, 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 { let manifest = validated_skill_pack_manifest()?; 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] fn skill_content_digest_is_stable_across_lf_and_crlf() { fn digest(bytes: &[u8]) -> String { diff --git a/apps/ai-game-creator-shell/src-tauri/src/main.rs b/apps/ai-game-creator-shell/src-tauri/src/main.rs index bec55a981..72f13acbb 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/main.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/main.rs @@ -2655,6 +2655,7 @@ fn main() { pick_client_extension_file, pick_client_extension_directory, list_client_extensions, + list_agc_skill_catalog, import_client_extension, set_client_extension_enabled, rename_client_extension, diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/ResourceReferenceInput.tsx b/apps/ai-game-creator-shell/src/features/project-workspace/ResourceReferenceInput.tsx index 35d9ccea1..e969e5e04 100644 --- a/apps/ai-game-creator-shell/src/features/project-workspace/ResourceReferenceInput.tsx +++ b/apps/ai-game-creator-shell/src/features/project-workspace/ResourceReferenceInput.tsx @@ -187,15 +187,6 @@ class SkillMentionOption extends MenuOption { } } -const BUILTIN_SKILL_REFERENCES: SkillReference[] = [ - 'agc-browser-playtest', - 'agc-client-projection', - 'agc-game-production-workflow', - 'agc-project-structure', - 'agc-web-game-development', - 'taonier-art-assets', -].map((name) => ({ type: 'skill', name })); - function appendInputText(content: DirectCodexUserContentPart[], text: string) { const previous = content[content.length - 1]; if (previous?.type === 'input_text') { @@ -491,6 +482,7 @@ function ResourceReferenceEditor({ const skipInitialDraftChangeRef = useRef(false); const [query, setQuery] = useState(null); const [skillQuery, setSkillQuery] = useState(null); + const [builtinSkills, setBuiltinSkills] = useState([]); const [clientSkills, setClientSkills] = useState([]); const [pickerOpen, setPickerOpen] = useState(false); // Enter 提交要避开候选浮层。用 ref 让命令处理器读到最新状态,不必因为 @@ -517,6 +509,22 @@ function ResourceReferenceEditor({ const invoke = resolveTauriInvoke(); if (!invoke) return; let cancelled = false; + void invoke>( + 'list_agc_skill_catalog', + ) + .then((items) => { + if (cancelled) return; + setBuiltinSkills( + items.map((item) => ({ + type: 'skill' as const, + name: item.name, + description: item.description, + })), + ); + }) + .catch(() => { + if (!cancelled) setBuiltinSkills([]); + }); void invoke< Array<{ name: string; @@ -607,7 +615,7 @@ function ResourceReferenceEditor({ const skillOptions = useMemo(() => { if (skillQuery === null) return []; const normalized = skillQuery.trim().toLowerCase(); - const allSkills = [...BUILTIN_SKILL_REFERENCES, ...clientSkills, ...skills]; + const allSkills = [...builtinSkills, ...clientSkills, ...skills]; const seen = new Set(); return allSkills .filter((skill) => { @@ -621,7 +629,7 @@ function ResourceReferenceEditor({ }) .slice(0, 8) .map((skill) => new SkillMentionOption(skill)); - }, [clientSkills, skillQuery, skills]); + }, [builtinSkills, clientSkills, skillQuery, skills]); const triggerFn = useBasicTypeaheadTriggerMatch('@', { minLength: 0, diff --git a/apps/ai-game-creator-shell/tests/resourceReferenceInput.test.tsx b/apps/ai-game-creator-shell/tests/resourceReferenceInput.test.tsx index a21ddbacd..cbf64c3bf 100644 --- a/apps/ai-game-creator-shell/tests/resourceReferenceInput.test.tsx +++ b/apps/ai-game-creator-shell/tests/resourceReferenceInput.test.tsx @@ -155,6 +155,36 @@ async function insertAssetThroughPicker(ariaLabel: string, optionName: RegExp) { afterEach(cleanup); describe('ResourceReferenceInput', () => { + test('从 Tauri command 读取内置 Skill catalog', async () => { + const invoke = vi.fn(async (command: string) => { + if (command === 'list_agc_skill_catalog') { + return [{ name: 'agc-test-skill', description: '测试 Skill' }]; + } + if (command === 'list_client_extensions') return []; + throw new Error(`unexpected invoke ${command}`); + }); + window.__TAURI__ = { core: { invoke: invoke as never } }; + + try { + render( + , + ); + + await waitFor(() => { + expect(invoke).toHaveBeenCalledWith('list_agc_skill_catalog'); + }); + } finally { + delete window.__TAURI__; + } + }); + test('运行画面引用的判别指纹带上了绑定素材、版本、元素角色与尺寸', () => { const base: RuntimeRegionReference = { type: 'runtime-region',