改为运行时控制画板 Agent 入口
新增前端运行时配置接口下发画板 Agent 开关 将画板 Agent 入口改为读取后端运行时配置 把开关变量改为 GENARRATIVE_ENABLE_IMAGE_EDITOR_AGENT_SIDEBAR 补充前后端测试和生产环境变量文档
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# 微信小程序 web-view 登录配置。
|
# 微信小程序 web-view 登录配置。
|
||||||
# 留空时不覆盖已有微信网页 OAuth 配置;正式联调时再填小程序 AppID / AppSecret。
|
# 留空时不覆盖已有微信网页 OAuth 配置;正式联调时再填小程序 AppID / AppSecret。
|
||||||
VITE_ENABLE_IMAGE_EDITOR_AGENT_SIDEBAR=false
|
GENARRATIVE_ENABLE_IMAGE_EDITOR_AGENT_SIDEBAR=false
|
||||||
|
|
||||||
WECHAT_MINI_PROGRAM_APP_ID=""
|
WECHAT_MINI_PROGRAM_APP_ID=""
|
||||||
WECHAT_MINI_PROGRAM_APP_SECRET=""
|
WECHAT_MINI_PROGRAM_APP_SECRET=""
|
||||||
|
|||||||
+3
-3
@@ -199,9 +199,9 @@ VITE_LLM_DEBUG_LOG="false"
|
|||||||
# Set to "true" to expose local diagnostic panels, or "false" to hide them.
|
# Set to "true" to expose local diagnostic panels, or "false" to hide them.
|
||||||
VITE_DEBUG_MODE=""
|
VITE_DEBUG_MODE=""
|
||||||
|
|
||||||
# Optional: show the image editor right-side Agent entry.
|
# Optional: show the image editor right-side Agent entry at runtime.
|
||||||
# Keep this off by default outside local development.
|
# This is read by api-server and exposed through /api/runtime/frontend-config.
|
||||||
VITE_ENABLE_IMAGE_EDITOR_AGENT_SIDEBAR="false"
|
GENARRATIVE_ENABLE_IMAGE_EDITOR_AGENT_SIDEBAR="false"
|
||||||
|
|
||||||
# Optional: official VikingDB credentials for regenerating build-tag similarities
|
# Optional: official VikingDB credentials for regenerating build-tag similarities
|
||||||
# with the Python embedding script. The script auto-loads `.env.local` and uses
|
# with the Python embedding script. The script auto-loads `.env.local` and uses
|
||||||
|
|||||||
+1
-1
@@ -42,7 +42,7 @@ LLM_DEBUG_LOG="true"
|
|||||||
# 注意:不要在客户端启用调试日志,避免敏感数据泄露
|
# 注意:不要在客户端启用调试日志,避免敏感数据泄露
|
||||||
# VITE_LLM_DEBUG_LOG="false"
|
# VITE_LLM_DEBUG_LOG="false"
|
||||||
|
|
||||||
VITE_ENABLE_IMAGE_EDITOR_AGENT_SIDEBAR=true
|
GENARRATIVE_ENABLE_IMAGE_EDITOR_AGENT_SIDEBAR=true
|
||||||
|
|
||||||
ALIYUN_OSS_BUCKET="xushi-dev"
|
ALIYUN_OSS_BUCKET="xushi-dev"
|
||||||
ALIYUN_OSS_REGION="oss-cn-beijing"
|
ALIYUN_OSS_REGION="oss-cn-beijing"
|
||||||
|
|||||||
Vendored
+1
@@ -49,6 +49,7 @@ AUTH_REFRESH_COOKIE_PATH=/api/auth
|
|||||||
AUTH_REFRESH_COOKIE_SAME_SITE=Lax
|
AUTH_REFRESH_COOKIE_SAME_SITE=Lax
|
||||||
AUTH_REFRESH_COOKIE_SECURE=true
|
AUTH_REFRESH_COOKIE_SECURE=true
|
||||||
GENARRATIVE_DEV_PASSWORD_ENTRY_AUTO_REGISTER_ENABLED=false
|
GENARRATIVE_DEV_PASSWORD_ENTRY_AUTO_REGISTER_ENABLED=false
|
||||||
|
GENARRATIVE_ENABLE_IMAGE_EDITOR_AGENT_SIDEBAR=false
|
||||||
|
|
||||||
GENARRATIVE_SPACETIME_SERVER_URL=http://127.0.0.1:3101
|
GENARRATIVE_SPACETIME_SERVER_URL=http://127.0.0.1:3101
|
||||||
GENARRATIVE_SPACETIME_DATABASE=genarrative-prod
|
GENARRATIVE_SPACETIME_DATABASE=genarrative-prod
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ npm run check:server-rs-ddd
|
|||||||
- 平台基础能力:`/api/llm/*`、`/api/speech/volcengine/*`,只保留通用 LLM 和语音代理。
|
- 平台基础能力:`/api/llm/*`、`/api/speech/volcengine/*`,只保留通用 LLM 和语音代理。
|
||||||
- 资产基础能力:`/api/assets/direct-upload-tickets`、`/api/assets/sts-upload-credentials`、`/api/assets/objects/*`、`/api/assets/read-*`,负责直传、确认、绑定和读取。
|
- 资产基础能力:`/api/assets/direct-upload-tickets`、`/api/assets/sts-upload-credentials`、`/api/assets/objects/*`、`/api/assets/read-*`,负责直传、确认、绑定和读取。
|
||||||
- 外部 OpenAPI:`/api/external/v1/openapi.json`、`/api/external/v1/assets/direct-upload-tickets`、`/api/external/v1/assets/objects/confirm`、`/api/external/v1/assets/read-url`、`/api/external/v1/editor/*`,使用 Bearer API Key 鉴权;API Key 管理仍在登录态 `/api/profile/api-keys`,不进入外部 OpenAPI JSON。
|
- 外部 OpenAPI:`/api/external/v1/openapi.json`、`/api/external/v1/assets/direct-upload-tickets`、`/api/external/v1/assets/objects/confirm`、`/api/external/v1/assets/read-url`、`/api/external/v1/editor/*`,使用 Bearer API Key 鉴权;API Key 管理仍在登录态 `/api/profile/api-keys`,不进入外部 OpenAPI JSON。
|
||||||
- 创作 / 游玩支撑能力:`/api/creation-entry/config`、`/api/ai/tasks*`、`/api/runtime/chat/*`、`/api/runtime/settings`、`/api/runtime/save/snapshot`、`/api/profile/browse-history`、`/api/profile/save-archives*`、`/api/profile/play-stats`、`/api/assets/history`、`/api/assets/character-visual/*`、`/api/assets/character-animation/*`、`/api/assets/character-workflow-cache*`、`/api/assets/hyper3d/*`、`/api/runtime/custom-world/asset-studio/*`、`/api/editor/projects*`、`/api/editor/projects/{projectId}/agent-conversations`、`/api/editor/agent-conversations/{conversationId}*`。`/api/runtime/custom-world/asset-studio/*` 解析默认角色形象 / 动作提示词时可以在 OSS 缓存不可用或未配置时按无缓存返回默认提示;保存 workflow 缓存和真实素材读写仍必须要求 OSS 正常可用。
|
- 创作 / 游玩支撑能力:`/api/creation-entry/config`、`/api/ai/tasks*`、`/api/runtime/frontend-config`、`/api/runtime/chat/*`、`/api/runtime/settings`、`/api/runtime/save/snapshot`、`/api/profile/browse-history`、`/api/profile/save-archives*`、`/api/profile/play-stats`、`/api/assets/history`、`/api/assets/character-visual/*`、`/api/assets/character-animation/*`、`/api/assets/character-workflow-cache*`、`/api/assets/hyper3d/*`、`/api/runtime/custom-world/asset-studio/*`、`/api/editor/projects*`、`/api/editor/projects/{projectId}/agent-conversations`、`/api/editor/agent-conversations/{conversationId}*`。`/api/runtime/frontend-config` 由 `api-server` 从运行时环境变量下发非敏感 UI 开关;画板右侧 Agent 入口由 `GENARRATIVE_ENABLE_IMAGE_EDITOR_AGENT_SIDEBAR` 控制,默认关闭,前端不再读取 `VITE_*` 构建期变量决定生产显示。`/api/runtime/custom-world/asset-studio/*` 解析默认角色形象 / 动作提示词时可以在 OSS 缓存不可用或未配置时按无缓存返回默认提示;保存 workflow 缓存和真实素材读写仍必须要求 OSS 正常可用。
|
||||||
- 后台入口配置:`/admin/api/creation-entry/config`、`/admin/api/creation-entry/config/banners` 和 `/admin/api/creation-entry/config/interactions`。
|
- 后台入口配置:`/admin/api/creation-entry/config`、`/admin/api/creation-entry/config/banners` 和 `/admin/api/creation-entry/config/interactions`。
|
||||||
- 后台素材查询:`GET /admin/api/editor-assets` 通过 `admin_list_editor_assets_and_return` 后台只读 procedure 读取私有账号级 `editor_asset` 中 `source_type = 'generated'` 的素材,支持 `ownerUserId`、`keyword`、`createdAfter`、`createdBefore`、`cursor` 和 `limit`;返回缩略图 / Object Key、作者展示名、陶泥号、提示词、生成输入和生成成本,只用于查询,不提供分类筛选,也不执行精选审核、返还或展示状态修改,不通过后台 SQL 直查私有表。
|
- 后台素材查询:`GET /admin/api/editor-assets` 通过 `admin_list_editor_assets_and_return` 后台只读 procedure 读取私有账号级 `editor_asset` 中 `source_type = 'generated'` 的素材,支持 `ownerUserId`、`keyword`、`createdAfter`、`createdBefore`、`cursor` 和 `limit`;返回缩略图 / Object Key、作者展示名、陶泥号、提示词、生成输入和生成成本,只用于查询,不提供分类筛选,也不执行精选审核、返还或展示状态修改,不通过后台 SQL 直查私有表。
|
||||||
- 自定义世界 / RPG:`/api/runtime/custom-world*`、`/api/story/*`、`/api/runtime/chat/*`。
|
- 自定义世界 / RPG:`/api/runtime/custom-world*`、`/api/story/*`、`/api/runtime/chat/*`。
|
||||||
|
|||||||
@@ -234,6 +234,10 @@ npm run database:backup:oss -- --data-dir /stdb --stop-service spacetimedb.servi
|
|||||||
|
|
||||||
生产环境变量模板在 `deploy/env/api-server.env.example`:
|
生产环境变量模板在 `deploy/env/api-server.env.example`:
|
||||||
|
|
||||||
|
主站前端运行时配置由 `api-server` 下发;画板右侧 Agent 入口使用
|
||||||
|
`GENARRATIVE_ENABLE_IMAGE_EDITOR_AGENT_SIDEBAR=false` 默认关闭,需要开启时只改生产
|
||||||
|
api-server 环境变量并重启 `api-server`,不再通过 `VITE_*` 构建期变量控制。
|
||||||
|
|
||||||
```env
|
```env
|
||||||
GENARRATIVE_DATABASE_BACKUP_DATA_DIR=/stdb
|
GENARRATIVE_DATABASE_BACKUP_DATA_DIR=/stdb
|
||||||
GENARRATIVE_DATABASE_BACKUP_WORK_DIR=/var/lib/genarrative/database-backups
|
GENARRATIVE_DATABASE_BACKUP_WORK_DIR=/var/lib/genarrative/database-backups
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ pub fn build_router(state: AppState) -> Router {
|
|||||||
.merge(modules::auth::router(state.clone()))
|
.merge(modules::auth::router(state.clone()))
|
||||||
.merge(modules::profile::router(state.clone()))
|
.merge(modules::profile::router(state.clone()))
|
||||||
.merge(modules::external_api::router(state.clone()))
|
.merge(modules::external_api::router(state.clone()))
|
||||||
|
.merge(modules::frontend_runtime_config::router(state.clone()))
|
||||||
.merge(modules::assets::router(state.clone()))
|
.merge(modules::assets::router(state.clone()))
|
||||||
.merge(modules::editor_project::router(state.clone()))
|
.merge(modules::editor_project::router(state.clone()))
|
||||||
.merge(modules::platform::router(state.clone()))
|
.merge(modules::platform::router(state.clone()))
|
||||||
@@ -518,6 +519,51 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn frontend_runtime_config_defaults_agent_sidebar_to_disabled() {
|
||||||
|
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
|
||||||
|
|
||||||
|
let response = app
|
||||||
|
.oneshot(
|
||||||
|
Request::builder()
|
||||||
|
.uri("/api/runtime/frontend-config")
|
||||||
|
.body(Body::empty())
|
||||||
|
.expect("frontend config request should build"),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("frontend config request should succeed");
|
||||||
|
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
let payload = read_json_response(response).await;
|
||||||
|
assert_eq!(
|
||||||
|
payload["imageEditorAgentSidebarEnabled"],
|
||||||
|
Value::Bool(false)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn frontend_runtime_config_returns_agent_sidebar_env_flag() {
|
||||||
|
let config = AppConfig {
|
||||||
|
image_editor_agent_sidebar_enabled: true,
|
||||||
|
..AppConfig::default()
|
||||||
|
};
|
||||||
|
let app = build_router(AppState::new(config).expect("state should build"));
|
||||||
|
|
||||||
|
let response = app
|
||||||
|
.oneshot(
|
||||||
|
Request::builder()
|
||||||
|
.uri("/api/runtime/frontend-config")
|
||||||
|
.body(Body::empty())
|
||||||
|
.expect("frontend config request should build"),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("frontend config request should succeed");
|
||||||
|
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
let payload = read_json_response(response).await;
|
||||||
|
assert_eq!(payload["imageEditorAgentSidebarEnabled"], Value::Bool(true));
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn spacetime_unavailable_router_returns_service_unavailable_for_requests() {
|
async fn spacetime_unavailable_router_returns_service_unavailable_for_requests() {
|
||||||
let app =
|
let app =
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ pub struct AppConfig {
|
|||||||
pub editor_background_removal_base_url: String,
|
pub editor_background_removal_base_url: String,
|
||||||
pub editor_background_removal_token: Option<String>,
|
pub editor_background_removal_token: Option<String>,
|
||||||
pub editor_background_removal_request_timeout_ms: u64,
|
pub editor_background_removal_request_timeout_ms: u64,
|
||||||
|
pub image_editor_agent_sidebar_enabled: bool,
|
||||||
pub log_filter: String,
|
pub log_filter: String,
|
||||||
pub otel_enabled: bool,
|
pub otel_enabled: bool,
|
||||||
pub admin_username: Option<String>,
|
pub admin_username: Option<String>,
|
||||||
@@ -283,6 +284,7 @@ impl Default for AppConfig {
|
|||||||
editor_background_removal_token: None,
|
editor_background_removal_token: None,
|
||||||
editor_background_removal_request_timeout_ms:
|
editor_background_removal_request_timeout_ms:
|
||||||
DEFAULT_EDITOR_BACKGROUND_REMOVAL_REQUEST_TIMEOUT_MS,
|
DEFAULT_EDITOR_BACKGROUND_REMOVAL_REQUEST_TIMEOUT_MS,
|
||||||
|
image_editor_agent_sidebar_enabled: false,
|
||||||
log_filter: "info,tower_http=info".to_string(),
|
log_filter: "info,tower_http=info".to_string(),
|
||||||
otel_enabled: false,
|
otel_enabled: false,
|
||||||
admin_username: None,
|
admin_username: None,
|
||||||
@@ -473,6 +475,11 @@ impl AppConfig {
|
|||||||
]) {
|
]) {
|
||||||
config.editor_background_removal_request_timeout_ms = timeout_ms;
|
config.editor_background_removal_request_timeout_ms = timeout_ms;
|
||||||
}
|
}
|
||||||
|
if let Some(enabled) =
|
||||||
|
read_first_bool_env(&["GENARRATIVE_ENABLE_IMAGE_EDITOR_AGENT_SIDEBAR"])
|
||||||
|
{
|
||||||
|
config.image_editor_agent_sidebar_enabled = enabled;
|
||||||
|
}
|
||||||
if let Some(listen_backlog) =
|
if let Some(listen_backlog) =
|
||||||
read_first_positive_i32_env(&["GENARRATIVE_API_LISTEN_BACKLOG"])
|
read_first_positive_i32_env(&["GENARRATIVE_API_LISTEN_BACKLOG"])
|
||||||
{
|
{
|
||||||
@@ -2176,6 +2183,26 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn from_env_reads_image_editor_agent_sidebar_runtime_flag() {
|
||||||
|
let _guard = ENV_LOCK
|
||||||
|
.get_or_init(|| Mutex::new(()))
|
||||||
|
.lock()
|
||||||
|
.expect("env lock should not poison");
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
std::env::remove_var("GENARRATIVE_ENABLE_IMAGE_EDITOR_AGENT_SIDEBAR");
|
||||||
|
std::env::set_var("GENARRATIVE_ENABLE_IMAGE_EDITOR_AGENT_SIDEBAR", "true");
|
||||||
|
}
|
||||||
|
|
||||||
|
let config = AppConfig::from_env();
|
||||||
|
assert!(config.image_editor_agent_sidebar_enabled);
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
std::env::remove_var("GENARRATIVE_ENABLE_IMAGE_EDITOR_AGENT_SIDEBAR");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn from_env_reads_prefixed_character_animation_ffmpeg_paths() {
|
fn from_env_reads_prefixed_character_animation_ffmpeg_paths() {
|
||||||
let _guard = ENV_LOCK
|
let _guard = ENV_LOCK
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
use axum::{
|
||||||
|
Json,
|
||||||
|
extract::{Extension, State},
|
||||||
|
};
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::{api_response::json_success_body, request_context::RequestContext, state::AppState};
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct FrontendRuntimeConfigResponse {
|
||||||
|
pub image_editor_agent_sidebar_enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_frontend_runtime_config(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Extension(request_context): Extension<RequestContext>,
|
||||||
|
) -> Json<serde_json::Value> {
|
||||||
|
json_success_body(
|
||||||
|
Some(&request_context),
|
||||||
|
FrontendRuntimeConfigResponse {
|
||||||
|
image_editor_agent_sidebar_enabled: state.config.image_editor_agent_sidebar_enabled,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -52,6 +52,7 @@ mod external_editor_api;
|
|||||||
mod external_generation;
|
mod external_generation;
|
||||||
mod external_generation_worker;
|
mod external_generation_worker;
|
||||||
mod external_generation_worker_controller;
|
mod external_generation_worker_controller;
|
||||||
|
mod frontend_runtime_config;
|
||||||
pub(crate) mod generated_asset_sheets;
|
pub(crate) mod generated_asset_sheets;
|
||||||
mod generated_image_assets;
|
mod generated_image_assets;
|
||||||
mod health;
|
mod health;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ pub mod editor_project;
|
|||||||
pub mod edutainment;
|
pub mod edutainment;
|
||||||
pub mod external_api;
|
pub mod external_api;
|
||||||
pub mod external_generation;
|
pub mod external_generation;
|
||||||
|
pub mod frontend_runtime_config;
|
||||||
pub mod health;
|
pub mod health;
|
||||||
pub mod internal;
|
pub mod internal;
|
||||||
pub mod jump_hop;
|
pub mod jump_hop;
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
use axum::{Router, routing::get};
|
||||||
|
|
||||||
|
use crate::{frontend_runtime_config::get_frontend_runtime_config, state::AppState};
|
||||||
|
|
||||||
|
pub fn router(_state: AppState) -> Router<AppState> {
|
||||||
|
Router::new().route(
|
||||||
|
"/api/runtime/frontend-config",
|
||||||
|
get(get_frontend_runtime_config),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -41,7 +41,6 @@ type EditorAgentConversationSummary = Awaited<
|
|||||||
type EditorAgentConversationDetail = Awaited<
|
type EditorAgentConversationDetail = Awaited<
|
||||||
ReturnType<EditorAgentGetConversation>
|
ReturnType<EditorAgentGetConversation>
|
||||||
>;
|
>;
|
||||||
const EDITOR_AGENT_SIDEBAR_ENV = 'VITE_ENABLE_IMAGE_EDITOR_AGENT_SIDEBAR';
|
|
||||||
|
|
||||||
const listEditorAgentConversationsMock = vi.hoisted(() =>
|
const listEditorAgentConversationsMock = vi.hoisted(() =>
|
||||||
vi.fn<
|
vi.fn<
|
||||||
@@ -110,6 +109,7 @@ const loadOrCreateRecentEditorProjectMock = vi.hoisted(() => vi.fn());
|
|||||||
const renameEditorProjectMock = vi.hoisted(() => vi.fn());
|
const renameEditorProjectMock = vi.hoisted(() => vi.fn());
|
||||||
const saveEditorProjectLayoutMock = vi.hoisted(() => vi.fn());
|
const saveEditorProjectLayoutMock = vi.hoisted(() => vi.fn());
|
||||||
const getPlatformProfileDashboardMock = vi.hoisted(() => vi.fn());
|
const getPlatformProfileDashboardMock = vi.hoisted(() => vi.fn());
|
||||||
|
const loadFrontendRuntimeConfigMock = vi.hoisted(() => vi.fn());
|
||||||
|
|
||||||
vi.mock('../../services/image-editor/editorProjectClient', async () => {
|
vi.mock('../../services/image-editor/editorProjectClient', async () => {
|
||||||
const actual = await vi.importActual<
|
const actual = await vi.importActual<
|
||||||
@@ -140,6 +140,10 @@ vi.mock('../../services/platform-entry/platformProfileClient', () => ({
|
|||||||
getPlatformProfileDashboard: getPlatformProfileDashboardMock,
|
getPlatformProfileDashboard: getPlatformProfileDashboardMock,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../services/frontendRuntimeConfigService', () => ({
|
||||||
|
loadFrontendRuntimeConfig: loadFrontendRuntimeConfigMock,
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock('../../services/image-editor/editorAgentClient', () => ({
|
vi.mock('../../services/image-editor/editorAgentClient', () => ({
|
||||||
createEditorAgentConversation: createEditorAgentConversationMock,
|
createEditorAgentConversation: createEditorAgentConversationMock,
|
||||||
deleteEditorAgentConversation: deleteEditorAgentConversationMock,
|
deleteEditorAgentConversation: deleteEditorAgentConversationMock,
|
||||||
@@ -214,7 +218,7 @@ function openMinimap() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function ensureAgentConversationOpen() {
|
async function ensureAgentConversationOpen() {
|
||||||
const agentButton = screen.getByRole('button', { name: '画布 Agent' });
|
const agentButton = await screen.findByRole('button', { name: '画布 Agent' });
|
||||||
if (agentButton.getAttribute('aria-pressed') !== 'true') {
|
if (agentButton.getAttribute('aria-pressed') !== 'true') {
|
||||||
fireEvent.click(agentButton);
|
fireEvent.click(agentButton);
|
||||||
}
|
}
|
||||||
@@ -222,7 +226,9 @@ async function ensureAgentConversationOpen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function enableEditorAgentSidebarForTest() {
|
function enableEditorAgentSidebarForTest() {
|
||||||
vi.stubEnv(EDITOR_AGENT_SIDEBAR_ENV, 'true');
|
loadFrontendRuntimeConfigMock.mockResolvedValue({
|
||||||
|
imageEditorAgentSidebarEnabled: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('ImageCanvasEditorView', () => {
|
describe('ImageCanvasEditorView', () => {
|
||||||
@@ -246,7 +252,9 @@ describe('ImageCanvasEditorView', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.stubEnv(EDITOR_AGENT_SIDEBAR_ENV, 'false');
|
loadFrontendRuntimeConfigMock.mockResolvedValue({
|
||||||
|
imageEditorAgentSidebarEnabled: false,
|
||||||
|
});
|
||||||
listEditorAgentConversationsMock.mockResolvedValue([]);
|
listEditorAgentConversationsMock.mockResolvedValue([]);
|
||||||
createEditorAgentConversationMock.mockResolvedValue({
|
createEditorAgentConversationMock.mockResolvedValue({
|
||||||
...createEditorAgentConversationSummary({
|
...createEditorAgentConversationSummary({
|
||||||
@@ -277,7 +285,7 @@ describe('ImageCanvasEditorView', () => {
|
|||||||
deleteEditorAgentConversationMock.mockReset();
|
deleteEditorAgentConversationMock.mockReset();
|
||||||
streamEditorAgentMessageMock.mockReset();
|
streamEditorAgentMessageMock.mockReset();
|
||||||
getPlatformProfileDashboardMock.mockReset();
|
getPlatformProfileDashboardMock.mockReset();
|
||||||
vi.unstubAllEnvs();
|
loadFrontendRuntimeConfigMock.mockReset();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('loads the project from projectid query before falling back to recent project', async () => {
|
it('loads the project from projectid query before falling back to recent project', async () => {
|
||||||
@@ -1487,6 +1495,8 @@ describe('ImageCanvasEditorView', () => {
|
|||||||
enableEditorAgentSidebarForTest();
|
enableEditorAgentSidebarForTest();
|
||||||
render(<ImageCanvasEditorView />);
|
render(<ImageCanvasEditorView />);
|
||||||
|
|
||||||
|
await screen.findByRole('button', { name: '画布 Agent' });
|
||||||
|
|
||||||
const assetSidebar = openAssetSidebar();
|
const assetSidebar = openAssetSidebar();
|
||||||
expect(within(assetSidebar).getByText('素材')).toBeTruthy();
|
expect(within(assetSidebar).getByText('素材')).toBeTruthy();
|
||||||
|
|
||||||
@@ -1507,7 +1517,7 @@ describe('ImageCanvasEditorView', () => {
|
|||||||
screen.getByRole('complementary', { name: '画布任务列表' }),
|
screen.getByRole('complementary', { name: '画布任务列表' }),
|
||||||
).toBeTruthy();
|
).toBeTruthy();
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: '画布 Agent' }));
|
fireEvent.click(await screen.findByRole('button', { name: '画布 Agent' }));
|
||||||
expect(
|
expect(
|
||||||
screen.getByRole('complementary', { name: '图片资源栏' }),
|
screen.getByRole('complementary', { name: '图片资源栏' }),
|
||||||
).toBeTruthy();
|
).toBeTruthy();
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
loadEditorProject,
|
loadEditorProject,
|
||||||
loadEditorGenerationPricing,
|
loadEditorGenerationPricing,
|
||||||
} from '../../services/image-editor/editorProjectClient';
|
} from '../../services/image-editor/editorProjectClient';
|
||||||
|
import { loadFrontendRuntimeConfig } from '../../services/frontendRuntimeConfigService';
|
||||||
import { shouldShowRechargeEntry } from '../../services/payment/paymentPlatform';
|
import { shouldShowRechargeEntry } from '../../services/payment/paymentPlatform';
|
||||||
import { getPlatformProfileDashboard } from '../../services/platform-entry/platformProfileClient';
|
import { getPlatformProfileDashboard } from '../../services/platform-entry/platformProfileClient';
|
||||||
import { useAuthUi } from '../auth/AuthUiContext';
|
import { useAuthUi } from '../auth/AuthUiContext';
|
||||||
@@ -117,12 +118,6 @@ type ImageCanvasEditorViewProps = {
|
|||||||
onProjectAccessLost?: () => void;
|
onProjectAccessLost?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
function resolveEditorAgentSidebarEnabled(
|
|
||||||
rawValue = import.meta.env.VITE_ENABLE_IMAGE_EDITOR_AGENT_SIDEBAR,
|
|
||||||
): boolean {
|
|
||||||
return rawValue === 'true' || rawValue === '1';
|
|
||||||
}
|
|
||||||
|
|
||||||
function isCanvasStartupTool(value: string | null): value is CanvasStartupTool {
|
function isCanvasStartupTool(value: string | null): value is CanvasStartupTool {
|
||||||
return CANVAS_STARTUP_TOOLS.includes(value as CanvasStartupTool);
|
return CANVAS_STARTUP_TOOLS.includes(value as CanvasStartupTool);
|
||||||
}
|
}
|
||||||
@@ -327,6 +322,9 @@ export function ImageCanvasEditorView({
|
|||||||
const showRechargeEntry = shouldShowRechargeEntry();
|
const showRechargeEntry = shouldShowRechargeEntry();
|
||||||
const currentEditorUserId = authUi?.user?.id ?? null;
|
const currentEditorUserId = authUi?.user?.id ?? null;
|
||||||
const [isToolbarGuideVisible, setIsToolbarGuideVisible] = useState(false);
|
const [isToolbarGuideVisible, setIsToolbarGuideVisible] = useState(false);
|
||||||
|
const [isAgentConversationEnabled, setIsAgentConversationEnabled] =
|
||||||
|
useState(false);
|
||||||
|
const [isAgentConversationOpen, setIsAgentConversationOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let isMounted = true;
|
let isMounted = true;
|
||||||
@@ -345,6 +343,28 @@ export function ImageCanvasEditorView({
|
|||||||
isMounted = false;
|
isMounted = false;
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
useEffect(() => {
|
||||||
|
let isMounted = true;
|
||||||
|
void loadFrontendRuntimeConfig()
|
||||||
|
.then((config) => {
|
||||||
|
if (!isMounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const enabled = config.imageEditorAgentSidebarEnabled === true;
|
||||||
|
setIsAgentConversationEnabled(enabled);
|
||||||
|
setIsAgentConversationOpen(enabled);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (!isMounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsAgentConversationEnabled(false);
|
||||||
|
setIsAgentConversationOpen(false);
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
isMounted = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
const selectedLayerIdsRef = useRef<string[]>([]);
|
const selectedLayerIdsRef = useRef<string[]>([]);
|
||||||
const setQuickEditPanelRef = useRef<
|
const setQuickEditPanelRef = useRef<
|
||||||
Dispatch<SetStateAction<QuickEditPanelState | null>>
|
Dispatch<SetStateAction<QuickEditPanelState | null>>
|
||||||
@@ -1167,10 +1187,6 @@ export function ImageCanvasEditorView({
|
|||||||
applyProjectSnapshot: applyGeneratedProjectSnapshot,
|
applyProjectSnapshot: applyGeneratedProjectSnapshot,
|
||||||
onWalletBalanceMayHaveChanged: refreshEditorWalletBalance,
|
onWalletBalanceMayHaveChanged: refreshEditorWalletBalance,
|
||||||
});
|
});
|
||||||
const isAgentConversationEnabled = resolveEditorAgentSidebarEnabled();
|
|
||||||
const [isAgentConversationOpen, setIsAgentConversationOpen] = useState(
|
|
||||||
() => isAgentConversationEnabled,
|
|
||||||
);
|
|
||||||
const effectiveIsAgentConversationOpen =
|
const effectiveIsAgentConversationOpen =
|
||||||
isAgentConversationEnabled && isAgentConversationOpen;
|
isAgentConversationEnabled && isAgentConversationOpen;
|
||||||
const toggleAgentConversation = useCallback(() => {
|
const toggleAgentConversation = useCallback(() => {
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
const apiClientMocks = vi.hoisted(() => ({
|
||||||
|
requestJson: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('./apiClient', async () => {
|
||||||
|
const actual =
|
||||||
|
await vi.importActual<typeof import('./apiClient')>('./apiClient');
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
requestJson: apiClientMocks.requestJson,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
import { loadFrontendRuntimeConfig } from './frontendRuntimeConfigService';
|
||||||
|
|
||||||
|
describe('frontendRuntimeConfigService', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
apiClientMocks.requestJson.mockResolvedValue({
|
||||||
|
imageEditorAgentSidebarEnabled: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads public frontend runtime config without auth side effects', async () => {
|
||||||
|
await expect(loadFrontendRuntimeConfig()).resolves.toEqual({
|
||||||
|
imageEditorAgentSidebarEnabled: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(apiClientMocks.requestJson).toHaveBeenCalledWith(
|
||||||
|
'/api/runtime/frontend-config',
|
||||||
|
{ method: 'GET' },
|
||||||
|
'读取前端运行时配置失败',
|
||||||
|
{
|
||||||
|
skipAuth: true,
|
||||||
|
skipRefresh: true,
|
||||||
|
notifyAuthStateChange: false,
|
||||||
|
clearAuthOnUnauthorized: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { requestJson } from './apiClient';
|
||||||
|
|
||||||
|
const FRONTEND_RUNTIME_CONFIG_API = '/api/runtime/frontend-config';
|
||||||
|
|
||||||
|
export type FrontendRuntimeConfig = {
|
||||||
|
imageEditorAgentSidebarEnabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function loadFrontendRuntimeConfig() {
|
||||||
|
return requestJson<FrontendRuntimeConfig>(
|
||||||
|
FRONTEND_RUNTIME_CONFIG_API,
|
||||||
|
{ method: 'GET' },
|
||||||
|
'读取前端运行时配置失败',
|
||||||
|
{
|
||||||
|
skipAuth: true,
|
||||||
|
skipRefresh: true,
|
||||||
|
notifyAuthStateChange: false,
|
||||||
|
clearAuthOnUnauthorized: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
Vendored
-1
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
readonly VITE_DEBUG_MODE?: string;
|
readonly VITE_DEBUG_MODE?: string;
|
||||||
readonly VITE_ENABLE_IMAGE_EDITOR_AGENT_SIDEBAR?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Window {
|
interface Window {
|
||||||
|
|||||||
Reference in New Issue
Block a user