From 44f5f5481c576af1df633bcbbf6722db0910b977 Mon Sep 17 00:00:00 2001 From: henyjone <39879909+henyjone@users.noreply.github.com> Date: Thu, 23 Jul 2026 01:38:36 +0800 Subject: [PATCH 1/4] Fix Studio reference media feedback --- apps/apsal-studio/electron/main.mjs | 5 ++++- apps/apsal-studio/index.html | 2 +- apps/apsal-studio/src/CreativeLibrary.tsx | 25 +++++++++++++++++------ apps/apsal-studio/src/styles.css | 9 +++++++- tests/test_studio_frontend.py | 16 +++++++++++++++ 5 files changed, 48 insertions(+), 9 deletions(-) diff --git a/apps/apsal-studio/electron/main.mjs b/apps/apsal-studio/electron/main.mjs index f4c9a9d..3f01878 100644 --- a/apps/apsal-studio/electron/main.mjs +++ b/apps/apsal-studio/electron/main.mjs @@ -334,7 +334,10 @@ async function callProtocol(message = {}) { if (!RENDERER_METHODS.has(method)) throw new Error(`Studio renderer is not allowed to call APSAL method: ${method}`) if (!currentProjectRoot && !PROJECT_OPTIONAL_METHODS.has(method)) throw new Error('请先新建或打开 APSAL 项目目录') const params = { ...(message.params || {}), project_root: currentProjectRoot || app.getPath('userData') } - const result = await getSidecar().call(method, params) + const sidecar = getSidecar() + const wasRunning = sidecar.status().running + const result = await sidecar.call(method, params) + if (!wasRunning) publish('apsal-protocol:status', await protocolStatus()) if (PROTOCOL_MUTATIONS.has(method) || method === 'studio.view.save') { publish('apsal-protocol:change', { method, result }) if (result?.snapshot) publish('apsal-protocol:snapshot', result.snapshot) diff --git a/apps/apsal-studio/index.html b/apps/apsal-studio/index.html index b78c589..03f26e1 100644 --- a/apps/apsal-studio/index.html +++ b/apps/apsal-studio/index.html @@ -3,7 +3,7 @@ - + APSAL Studio diff --git a/apps/apsal-studio/src/CreativeLibrary.tsx b/apps/apsal-studio/src/CreativeLibrary.tsx index 1928fd5..22a58cd 100644 --- a/apps/apsal-studio/src/CreativeLibrary.tsx +++ b/apps/apsal-studio/src/CreativeLibrary.tsx @@ -480,14 +480,14 @@ export function CreativeLibrary({ onOpenProject }: { onOpenProject: () => void } return () => window.clearTimeout(timer) }, [notice]) - const selectProject = async (project: LibraryProject) => { + const selectProjectById = async (projectId: string) => { if (!runtime) return - setSelectedId(project.project_id) + setSelectedId(projectId) setBusyAction('detail') try { const [value, lineage] = await Promise.all([ - runtime.call('library.get', { project_id: project.project_id }), - runtime.call>('library.lineage', { project_id: project.project_id }), + runtime.call('library.get', { project_id: projectId }), + runtime.call>('library.lineage', { project_id: projectId }), ]) setDetail({ ...value, lineage }) } catch (error) { @@ -497,6 +497,15 @@ export function CreativeLibrary({ onOpenProject }: { onOpenProject: () => void } } } + const selectProject = async (project: LibraryProject) => selectProjectById(project.project_id) + + const handleProjectCreated = async (snapshot: ApsalProjectSnapshot) => { + setCreating(false) + setNotice('参考图已安全入库并显示在项目详情中;下一步可启动 Codex 分析。') + await load() + await selectProjectById(snapshot.project.project_id) + } + const openProject = async (project: LibraryProject, switchView = true): Promise => { if (!runtime) return null const snapshot = await runtime.openProject(project.project_root) @@ -632,7 +641,9 @@ export function CreativeLibrary({ onOpenProject }: { onOpenProject: () => void } } } + const referenceAssets = useMemo(() => detail?.assets.filter((item) => item.kind === 'reference') ?? [], [detail]) const outputAssets = useMemo(() => detail?.assets.filter((item) => item.kind === 'output') ?? [], [detail]) + const latestAnalysis = detail?.analyses.at(-1) return (
@@ -657,7 +668,9 @@ export function CreativeLibrary({ onOpenProject }: { onOpenProject: () => void }
{detail.project.reference_count}参考图{detail.project.output_count}产出{detail.analyses.length}分析批次
{detail.project.parent_project_id &&
继承自父项目{detail.project.parent_project_id}
} {detail.lineage?.comparison.available &&

五层十三要素谱系比较

{detail.lineage.comparison.inherited.length}继承{detail.lineage.comparison.modified.length}修改{detail.lineage.comparison.added.length}新增
{detail.lineage.comparison.modified.length > 0 &&

修改:{detail.lineage.comparison.modified.map((role) => APSAL_ROLE_LABELS[role] || role).join('、')}

}{detail.lineage.comparison.added.length > 0 &&

新增:{detail.lineage.comparison.added.map((role) => APSAL_ROLE_LABELS[role] || role).join('、')}

}
} - {outputAssets.length > 0 &&
{outputAssets.slice(0, 6).map((item) => {`生成结果)}
} + {referenceAssets.length > 0 &&

参考图片

{referenceAssets.slice(0, 12).map((item, index) => {`参考图片)}
} + {outputAssets.length > 0 &&

生成结果

{outputAssets.slice(0, 6).map((item) => {`生成结果)}
} + {latestAnalysis &&
{latestAnalysis.status === 'completed' ? 'Codex 分析已完成' : '等待 Codex 分析'}{latestAnalysis.completed_job_count} / {latestAnalysis.job_count} 个结构化任务已回写
}
@@ -671,7 +684,7 @@ export function CreativeLibrary({ onOpenProject }: { onOpenProject: () => void } {busyAction === 'detail' &&
读取项目
} }
- {creating && setCreating(false)} onCreated={() => { setCreating(false); setNotice('根项目已创建并进入项目库。'); void load() }} />} + {creating && setCreating(false)} onCreated={(snapshot) => { void handleProjectCreated(snapshot) }} />} {creatingFork && detail && setCreatingFork(false)} onCreated={() => { setCreatingFork(false); setDetail(null); setSelectedId(null); setNotice('扩展子项目已创建;父项目保持不变。'); void load() }} />} {sharePreview && setSharePreview(null)} onComplete={(message) => { setSharePreview(null); setNotice(message); if (detail) void selectProject(detail.project) }} />}
diff --git a/apps/apsal-studio/src/styles.css b/apps/apsal-studio/src/styles.css index 3c96b8f..dce5867 100644 --- a/apps/apsal-studio/src/styles.css +++ b/apps/apsal-studio/src/styles.css @@ -321,8 +321,15 @@ button:disabled { .lineage-comparison > div span { padding: 7px; border-radius: 6px; background: var(--surface-soft); color: var(--muted); font-size: 8px; } .lineage-comparison strong { display: block; margin-bottom: 2px; color: var(--accent-soft); font-size: 13px; } .lineage-comparison p { margin: 7px 0 0; color: var(--muted); font-size: 8px; line-height: 1.5; } -.detail-gallery { display: grid; grid-template-columns: repeat(3, 1fr); gap: 5px; margin: 0 20px 14px; } +.detail-media { margin: 0 20px 14px; } +.detail-media h3 { margin: 0 0 8px; color: var(--text-soft); font-size: 10px; } +.detail-gallery { display: grid; grid-template-columns: repeat(3, 1fr); gap: 5px; } .detail-gallery img { width: 100%; aspect-ratio: 1; border-radius: 7px; object-fit: cover; background: #22180f; } +.analysis-progress { display: flex; align-items: center; gap: 10px; margin: 0 20px 14px; padding: 10px; border: 1px solid rgba(245, 165, 36, 0.18); border-radius: 9px; background: rgba(245, 165, 36, 0.05); } +.analysis-progress > svg { width: 18px; height: 18px; color: var(--accent-soft); } +.analysis-progress strong, .analysis-progress span { display: block; } +.analysis-progress strong { color: var(--text-soft); font-size: 10px; } +.analysis-progress span { margin-top: 3px; color: var(--muted); font-size: 8px; } .detail-actions { display: grid; gap: 7px; padding: 4px 20px 16px; } .detail-actions button { width: 100%; min-height: 40px; justify-content: center; } .library-tags { margin: 0 20px 16px; padding: 12px; border: 1px solid var(--border); border-radius: 10px; background: rgba(255, 255, 255, .015); } diff --git a/tests/test_studio_frontend.py b/tests/test_studio_frontend.py index 66baae0..3d07c21 100644 --- a/tests/test_studio_frontend.py +++ b/tests/test_studio_frontend.py @@ -79,6 +79,22 @@ def test_renderer_is_protocol_frontend_not_a_local_generation_app(self) -> None: self.assertNotIn("generation.record", renderer_contract) self.assertNotIn("qa.record", renderer_contract) + def test_private_reference_media_is_visible_only_through_the_safe_protocol(self) -> None: + index = (STUDIO / "index.html").read_text() + main = (STUDIO / "electron" / "main.mjs").read_text() + library = (STUDIO / "src" / "CreativeLibrary.tsx").read_text() + + csp = re.search(r'Content-Security-Policy" content="([^"]+)', index) + self.assertIsNotNone(csp) + self.assertIn("img-src 'self' data: apsal-media:", csp.group(1)) + self.assertNotIn("file:", csp.group(1)) + self.assertIn("protocol.handle('apsal-media'", main) + self.assertIn("isInside(mediaPath, join(apsalHome, 'vault'))", main) + self.assertIn("isInside(mediaPath, join(apsalHome, 'library', 'objects'))", main) + self.assertIn("if (!wasRunning) publish('apsal-protocol:status', await protocolStatus())", main) + self.assertIn("apsal-media://asset?path=", library) + self.assertIn("参考图已安全入库并显示在项目详情中", library) + def test_codex_bridge_keeps_full_domain_route_without_arbitrary_proxy(self) -> None: bridge = (STUDIO / "electron" / "apsal-link.mjs").read_text() self.assertIn("design.authoring_mode", bridge) From b14a7758c94313ae82fc5fa7b1bdba57548deaef Mon Sep 17 00:00:00 2001 From: henyjone <39879909+henyjone@users.noreply.github.com> Date: Thu, 23 Jul 2026 01:58:35 +0800 Subject: [PATCH 2/4] Show auditable Codex analysis progress --- apps/apsal-studio/src/App.tsx | 2 +- apps/apsal-studio/src/CreativeLibrary.tsx | 77 ++++++++++++++++++- apps/apsal-studio/src/styles.css | 32 ++++++-- .../apsal-studio/scripts/apsal_creative.py | 58 +++++++++++++- plugins/apsal-studio/scripts/apsal_mcp.py | 4 +- .../skills/apsal-theme-creator/SKILL.md | 2 +- .../references/INTERACTION.md | 2 + tests/test_creative_library.py | 7 ++ tests/test_studio_frontend.py | 3 + 9 files changed, 173 insertions(+), 14 deletions(-) diff --git a/apps/apsal-studio/src/App.tsx b/apps/apsal-studio/src/App.tsx index 0cb685a..3a08b79 100644 --- a/apps/apsal-studio/src/App.tsx +++ b/apps/apsal-studio/src/App.tsx @@ -949,7 +949,7 @@ export function App() { - {screen === 'library' ? setScreen('studio')} /> : <> + {screen === 'library' ? setScreen('studio')} linkStatus={linkStatus} /> : <> {snapshot?.read_only &&
只读模式:{snapshot.compatibility_error || '协议版本不兼容,请预览并复制迁移到 APSAL 0.16。'}{migrationError && {migrationError}}
}