feat(themes): 添加可编辑的站点外观和皮肤主题(#408)

This commit is contained in:
Ray
2026-09-03 21:13:41 +08:00
committed by GitHub
parent c768fccbc2
commit 5e66cb7c45
70 changed files with 3654 additions and 327 deletions

View File

@@ -8,6 +8,7 @@ import "@/lib/plugins/builtin";
import { createEagleAssetSource, EAGLE_DEFAULT_BASE_URL, eagleAssetPlugin } from "@/lib/plugins/builtin/eagle";
import type { ExternalAssetFolder, ExternalAssetItem } from "@/lib/plugins/plugin-types";
import type { Asset } from "@/stores/use-asset-store";
import { useAppearanceStore } from "@/stores/use-appearance-store";
import { usePluginStore } from "@/stores/use-plugin-store";
import { CollectionGrid, ListToolbar, PageHeader, PaginationBar, WorkspacePage } from "@/components/layout/workspace-page";
import { AssetLibraryCard, AssetLibraryCardMedia } from "@/components/assets/asset-library-card";
@@ -16,6 +17,7 @@ import "./eagle.css";
export default function EagleLibraryPage() {
const navigate = useNavigate();
const { message } = App.useApp();
const brandName = useAppearanceStore((state) => state.appearance.brandName);
const installations = usePluginStore((state) => state.installations);
const hydrated = usePluginStore((state) => state.hydrated);
const ensurePlugin = usePluginStore((state) => state.ensurePlugin);
@@ -133,13 +135,13 @@ export default function EagleLibraryPage() {
<WorkspacePage grid className="library-page eagle-library-page">
<PageHeader
title="Eagle 素材库"
description="把 Eagle 作为影策的外部素材来源,直接浏览和管理 Eagle 原始文件。"
actions={<Button icon={<ArrowLeft className="size-3.5" />} onClick={() => navigate("/assets")}>返回影策素材库</Button>}
description={`把 Eagle 作为${brandName}的外部素材来源,直接浏览和管理 Eagle 原始文件。`}
actions={<Button icon={<ArrowLeft className="size-3.5" />} onClick={() => navigate("/assets")}>返回{brandName}素材库</Button>}
/>
<section className="mt-4 library-card-surface flex min-h-72 flex-col items-center justify-center rounded-[var(--r-xl)] px-6 py-10 text-center">
<span className="grid size-14 place-items-center rounded-[var(--r-lg)] bg-[var(--workspace-accent-soft)] text-[var(--workspace-accent)]"><FolderOpen className="size-7" aria-hidden="true" /></span>
<h2 className="mt-4 text-base font-semibold">先启用 Eagle 素材来源</h2>
<p className="mt-2 max-w-md text-sm leading-6 text-foreground/55">启用后,这里会直接显示 Eagle 原本的文件夹和文件,不会把素材复制成影策本地素材。</p>
<p className="mt-2 max-w-md text-sm leading-6 text-foreground/55">启用后,这里会直接显示 Eagle 原本的文件夹和文件,不会把素材复制成{brandName}本地素材。</p>
<Button type="primary" className="mt-5" icon={<Settings2 className="size-4" />} onClick={() => navigate("/plugins")}>去插件中心启用</Button>
</section>
</WorkspacePage>
@@ -152,13 +154,13 @@ export default function EagleLibraryPage() {
<div className="studio-band">
<PageHeader
title="Eagle 素材库"
description="Eagle 是影策的外部素材来源;这里复用影策素材库的浏览方式,直接读取和写入 Eagle 原始文件。"
description={`Eagle 是${brandName}的外部素材来源;这里复用${brandName}素材库的浏览方式,直接读取和写入 Eagle 原始文件。`}
meta={<span className="app-projects-header-meta assets-header-meta">{error ? "Eagle · 连接异常" : "Eagle · 已连接"}</span>}
actions={(
<div className="assets-header-actions">
<div className="assets-header-action-buttons">
<Button className="library-primary-action" type="primary" icon={<Upload className="size-3.5" />} onClick={() => fileInputRef.current?.click()} disabled={working}>写入素材</Button>
<Button icon={<ArrowLeft className="size-3.5" />} onClick={() => navigate("/assets")}>影策素材库</Button>
<Button icon={<ArrowLeft className="size-3.5" />} onClick={() => navigate("/assets")}>{brandName}素材库</Button>
<Button icon={<Settings2 className="size-3.5" />} onClick={() => navigate("/plugins")}>插件设置</Button>
</div>
</div>
@@ -210,7 +212,7 @@ export default function EagleLibraryPage() {
<h2 className="text-base font-semibold">{currentFolder?.name || "全部素材"}</h2>
<span className="app-projects-header-meta">{items.length} 个素材</span>
</div>
<p className="mt-1 text-xs text-foreground/48">{currentFolder ? "当前文件夹由 Eagle 管理,影策只负责展示和调用。" : "当前展示 Eagle 素材库中的全部文件。"}</p>
<p className="mt-1 text-xs text-foreground/48">{currentFolder ? `当前文件夹由 Eagle 管理,${brandName}只负责展示和调用。` : "当前展示 Eagle 素材库中的全部文件。"}</p>
</div>
<div className="flex flex-wrap items-center gap-2">
<Button icon={<FolderPlus className="size-3.5" />} onClick={() => setFolderName((value) => value ? "" : "新文件夹")}>新建文件夹</Button>
@@ -348,4 +350,4 @@ function externalFolderPath(folders: ExternalAssetFolder[], folderId: string) {
current = current.parentId ? byId.get(current.parentId) : undefined;
}
return result;
}
}

View File

@@ -12,6 +12,7 @@ import { ART_CRITIQUE_PLUGIN_ID } from "@/lib/art-critique/contracts";
import type { PluginManifest, RegisteredPlugin } from "@/lib/plugins/plugin-types";
import { getEagleLibrary, type EagleFolder } from "@/services/api/eagle";
import { fetchPlugins, setUserPluginEnabled, type BackendPlugin, type PluginState } from "@/services/api/plugins";
import { useAppearanceStore } from "@/stores/use-appearance-store";
import { usePluginStore } from "@/stores/use-plugin-store";
import { useUserStore } from "@/stores/use-user-store";
@@ -64,6 +65,7 @@ const protocolSectionMeta = [
export default function PluginsPage() {
const { message } = App.useApp();
const navigate = useNavigate();
const brandName = useAppearanceStore((state) => state.appearance.brandName);
const user = useUserStore((state) => state.user);
const features = useUserStore((state) => state.features);
const installations = usePluginStore((state) => state.installations);
@@ -516,14 +518,14 @@ export default function PluginsPage() {
<div className="min-w-0">
<label htmlFor="eagle-base-url">Eagle 本地 API 地址</label>
<Input id="eagle-base-url" aria-label="Eagle 本地 API 地址" value={eagleBaseUrl} onChange={(event) => setEagleBaseUrl(event.target.value)} placeholder="http://localhost:41595" />
<p>Eagle 必须在本机运行;影策通过插件直接读取和写入 Eagle 原始文件。</p>
<p>Eagle 必须在本机运行;{brandName}通过插件直接读取和写入 Eagle 原始文件。</p>
</div>
<div className="min-w-0">
<div className="plugin-setting-label-row">
<label htmlFor="eagle-auto-upload-generated">自动归档生成结果</label>
<Switch id="eagle-auto-upload-generated" checked={eagleAutoUploadGenerated} onChange={setEagleAutoUploadGenerated} aria-label="自动归档生成结果到 Eagle" />
</div>
<p>图片、视频和音频生成成功后,自动写入 Eagle;影策本地素材仍会保留。</p>
<p>图片、视频和音频生成成功后,自动写入 Eagle;{brandName}本地素材仍会保留。</p>
</div>
<div className="min-w-0">
<div className="plugin-setting-label-row">

View File

@@ -1,6 +1,6 @@
# 上传统一插件包
影策只有一种插件:一个版本化 Manifest 描述一个插件包可以向宿主贡献的全部能力。Provider、工作流、画布节点、媒体转换、素材源、Agent 和命令都写在同一个 `contributes` 下;运行时可以不同,但不会再出现“协议插件”和“UI 插件”两套清单。
宿主只有一种插件:一个版本化 Manifest 描述一个插件包可以向宿主贡献的全部能力。Provider、工作流、画布节点、媒体转换、素材源、Agent 和命令都写在同一个 `contributes` 下;运行时可以不同,但不会再出现“协议插件”和“UI 插件”两套清单。
## 1. 包格式
@@ -65,9 +65,9 @@ my-plugin.yingce-plugin
## 4. Provider 与工作流
请求字段右侧只能引用影策统一请求,例如 `request.prompt`、`request.model`、`request.duration`、`request.extra`。声明式字段也支持安全的对象/数组路径(例如 `request.images.0.url`)和通用字符串变换(`|trim`、`|lower`、`|upper`),不执行插件代码。渠道声明的枚举值由画布完整保留,插件可使用通用大小写变换适配上游,不应要求宿主增加渠道专用归一化。字段解析为空时会省略该字段,适合映射可选参考素材。Provider 如果把 `requiresPublicMediaUrls` 设为 `true`,宿主会在请求前把用户资源转换为短期签名公网 URL。响应映射支持对象路径和数组下标,并可用 `errorPaths` 声明上游错误码路径。同步 provider 只需要 `create` 和 `response`;异步 provider 再声明 `poll`。
请求字段右侧只能引用宿主统一请求,例如 `request.prompt`、`request.model`、`request.duration`、`request.extra`。声明式字段也支持安全的对象/数组路径(例如 `request.images.0.url`)和通用字符串变换(`|trim`、`|lower`、`|upper`),不执行插件代码。渠道声明的枚举值由画布完整保留,插件可使用通用大小写变换适配上游,不应要求宿主增加渠道专用归一化。字段解析为空时会省略该字段,适合映射可选参考素材。Provider 如果把 `requiresPublicMediaUrls` 设为 `true`,宿主会在请求前把用户资源转换为短期签名公网 URL。响应映射支持对象路径和数组下标,并可用 `errorPaths` 声明上游错误码路径。同步 provider 只需要 `create` 和 `response`;异步 provider 再声明 `poll`。
结果 URL 如果是短期地址,必须设置 `resultEphemeral: true`。宿主会在任务完成后立即下载并保存资源,画布和任务记录只保存影策资源引用。
结果 URL 如果是短期地址,必须设置 `resultEphemeral: true`。宿主会在任务完成后立即下载并保存资源,画布和任务记录只保存宿主资源引用。
## 5. 安全与运行边界

View File

@@ -60,7 +60,7 @@ export function UploadPluginModal({ open, onClose, onUpload }: UploadPluginModal
<span className="plugin-upload-panel-icon"><CloudUpload className="size-5" /></span>
<div>
<h2>安装插件包</h2>
<p>选择统一影策插件包,安装后会立即进入插件中心。</p>
<p>选择统一站点插件包,安装后会立即进入插件中心。</p>
</div>
</div>
<div