Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b43a663fe2 |
@@ -15,8 +15,6 @@ const PROJECT_COMMAND_MAX_ARGUMENT_BYTES: usize = 8 * 1024;
|
|||||||
const PROJECT_COMMAND_MIN_TIMEOUT_SECONDS: u64 = 1;
|
const PROJECT_COMMAND_MIN_TIMEOUT_SECONDS: u64 = 1;
|
||||||
const PROJECT_COMMAND_MAX_TIMEOUT_SECONDS: u64 = 300;
|
const PROJECT_COMMAND_MAX_TIMEOUT_SECONDS: u64 = 300;
|
||||||
const PROJECT_COMMAND_OUTPUT_MAX_BYTES: usize = 24 * 1024;
|
const PROJECT_COMMAND_OUTPUT_MAX_BYTES: usize = 24 * 1024;
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
const PROJECT_COMMAND_CLEANUP_TIMEOUT: Duration = Duration::from_secs(5);
|
|
||||||
const PROJECT_COMMAND_FINGERPRINT_MAX_ENTRIES: usize = 20_000;
|
const PROJECT_COMMAND_FINGERPRINT_MAX_ENTRIES: usize = 20_000;
|
||||||
const PROJECT_COMMAND_FINGERPRINT_MAX_FILES: usize = 10_000;
|
const PROJECT_COMMAND_FINGERPRINT_MAX_FILES: usize = 10_000;
|
||||||
const PROJECT_COMMAND_FINGERPRINT_MAX_BYTES: u64 = 512 * 1024 * 1024;
|
const PROJECT_COMMAND_FINGERPRINT_MAX_BYTES: u64 = 512 * 1024 * 1024;
|
||||||
@@ -224,32 +222,16 @@ impl ProjectCommandTree {
|
|||||||
}
|
}
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
{
|
{
|
||||||
let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
|
|
||||||
let requested = self.request_owned_group_termination();
|
let requested = self.request_owned_group_termination();
|
||||||
let _ = child.start_kill();
|
let _ = child.start_kill();
|
||||||
let waited = tokio::time::timeout_at(deadline, child.wait()).await;
|
let waited = tokio::time::timeout(Duration::from_secs(5), child.wait()).await;
|
||||||
|
requested?;
|
||||||
waited
|
waited
|
||||||
.map_err(|_| "等待受控命令主进程退出超时")?
|
.map_err(|_| "等待受控命令主进程退出超时")?
|
||||||
.map_err(|_| "受控命令主进程退出未确认")?;
|
.map_err(|_| "受控命令主进程退出未确认")?;
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
{
|
|
||||||
let Self::Group { pid, .. } = self;
|
|
||||||
// leader 可能与取消同时退出;只要组已停止便无需补发信号。
|
|
||||||
if let Err(error) = wait_linux_project_command_group_exit(*pid, deadline).await {
|
|
||||||
return Err(match requested {
|
|
||||||
Ok(_) => error,
|
|
||||||
Err(request_error) => format!("{request_error};{error}"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return Ok("主进程已回收,受控进程组已无活成员;完整子树状态未证明".into());
|
|
||||||
}
|
|
||||||
#[cfg(not(target_os = "linux"))]
|
|
||||||
{
|
|
||||||
requested?;
|
|
||||||
Ok("已请求终止受控进程组并回收主进程,完整子树状态未证明".into())
|
Ok("已请求终止受控进程组并回收主进程,完整子树状态未证明".into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
async fn after_main_exit(&self, child: &mut tokio::process::Child) -> Result<(), String> {
|
async fn after_main_exit(&self, child: &mut tokio::process::Child) -> Result<(), String> {
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
@@ -262,39 +244,17 @@ impl ProjectCommandTree {
|
|||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
{
|
{
|
||||||
let Self::Group { pid, .. } = self;
|
let Self::Group { pid, .. } = self;
|
||||||
// wait 已回收 leader,不能再用旧 PID 授权发信号。
|
// 容器 PID 1 可能不回收 bwrap 的孤儿僵尸;它们不再执行,也无法被信号终止。
|
||||||
// namespace 后代可能仍在退出,容器 PID 1 也可能保留孤儿僵尸。
|
// 仅在确认没有存活成员时免除清理,存活成员仍须通过 leader 身份核对。
|
||||||
return wait_linux_project_command_group_exit(
|
if !linux_project_command_group_has_live_members(*pid)? {
|
||||||
*pid,
|
return Ok(());
|
||||||
tokio::time::Instant::now() + PROJECT_COMMAND_CLEANUP_TIMEOUT,
|
}
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
}
|
||||||
#[cfg(not(target_os = "linux"))]
|
|
||||||
self.request_owned_group_termination().map(|_| ())
|
self.request_owned_group_termination().map(|_| ())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
async fn wait_linux_project_command_group_exit(
|
|
||||||
group: u32,
|
|
||||||
deadline: tokio::time::Instant,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
while linux_project_command_group_has_live_members(group)? {
|
|
||||||
if tokio::time::Instant::now() >= deadline {
|
|
||||||
return Err(
|
|
||||||
"受控进程组仍有存活成员,退出未确认;不得向身份未确认的进程组补发信号".into(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
tokio::time::sleep_until(
|
|
||||||
deadline.min(tokio::time::Instant::now() + Duration::from_millis(20)),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
fn linux_project_command_group_has_live_members(group: u32) -> Result<bool, String> {
|
fn linux_project_command_group_has_live_members(group: u32) -> Result<bool, String> {
|
||||||
let inspect = || -> std::io::Result<bool> {
|
let inspect = || -> std::io::Result<bool> {
|
||||||
@@ -1882,17 +1842,6 @@ where
|
|||||||
),
|
),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
// 目标放行后可能立即退出,必须趁 ready gate 仍持有 launcher 时记录归属。
|
|
||||||
let tree = match ProjectCommandTree::attach(&child) {
|
|
||||||
Ok(tree) => tree,
|
|
||||||
Err(error) => {
|
|
||||||
let termination = terminate_project_command_process_group(&mut child).await;
|
|
||||||
return Err(ProjectCommandError::new(
|
|
||||||
ProjectCommandErrorStage::Preflight,
|
|
||||||
project_command_launch_error_with_termination(error, termination),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if let Err(error) = durable_commit() {
|
if let Err(error) = durable_commit() {
|
||||||
let termination = terminate_project_command_process_group(&mut child).await;
|
let termination = terminate_project_command_process_group(&mut child).await;
|
||||||
return Err(ProjectCommandError::new(
|
return Err(ProjectCommandError::new(
|
||||||
@@ -1911,7 +1860,12 @@ where
|
|||||||
// cancelled future must not erase the launch-unknown decision window.
|
// cancelled future must not erase the launch-unknown decision window.
|
||||||
let exec = gate.wait_target_exec(Duration::from_secs(3));
|
let exec = gate.wait_target_exec(Duration::from_secs(3));
|
||||||
match exec {
|
match exec {
|
||||||
Ok(TargetExecState::Established) => Ok(EstablishedProjectCommand { tree, child, gate }),
|
Ok(TargetExecState::Established) => {
|
||||||
|
let tree = ProjectCommandTree::attach(&child).map_err(|error| {
|
||||||
|
ProjectCommandError::new(ProjectCommandErrorStage::LaunchUnknown, error)
|
||||||
|
})?;
|
||||||
|
Ok(EstablishedProjectCommand { tree, child, gate })
|
||||||
|
}
|
||||||
Ok(TargetExecState::Failed { errno }) => {
|
Ok(TargetExecState::Failed { errno }) => {
|
||||||
let termination = terminate_project_command_process_group_after_commit(&mut child);
|
let termination = terminate_project_command_process_group_after_commit(&mut child);
|
||||||
Err(ProjectCommandError::new(
|
Err(ProjectCommandError::new(
|
||||||
@@ -2023,11 +1977,7 @@ async fn terminate_project_command_process_group(
|
|||||||
.ok_or_else(|| "请求终止受控进程组失败:子进程缺少 pid".to_string())?;
|
.ok_or_else(|| "请求终止受控进程组失败:子进程缺少 pid".to_string())?;
|
||||||
let group_result = request_unix_project_command_process_group_termination(process_id);
|
let group_result = request_unix_project_command_process_group_termination(process_id);
|
||||||
let child_kill_error = child.start_kill().err();
|
let child_kill_error = child.start_kill().err();
|
||||||
let deadline = tokio::time::Instant::now() + PROJECT_COMMAND_CLEANUP_TIMEOUT;
|
let wait_result = child.wait().await;
|
||||||
let wait_result = tokio::time::timeout_at(deadline, child.wait())
|
|
||||||
.await
|
|
||||||
.map_err(|_| "等待受控命令主进程退出超时".to_string())?
|
|
||||||
.map_err(|error| error.to_string());
|
|
||||||
if let Err(error) = &group_result {
|
if let Err(error) = &group_result {
|
||||||
let fallback = match (&child_kill_error, &wait_result) {
|
let fallback = match (&child_kill_error, &wait_result) {
|
||||||
(_, Ok(_)) => "主进程已回收,但无法确认其余组内进程".to_string(),
|
(_, Ok(_)) => "主进程已回收,但无法确认其余组内进程".to_string(),
|
||||||
@@ -2039,7 +1989,6 @@ async fn terminate_project_command_process_group(
|
|||||||
return Err(format!("{error};{fallback}"));
|
return Err(format!("{error};{fallback}"));
|
||||||
}
|
}
|
||||||
wait_result.map_err(|error| format!("请求终止受控进程组后等待主进程失败:{error}"))?;
|
wait_result.map_err(|error| format!("请求终止受控进程组后等待主进程失败:{error}"))?;
|
||||||
wait_linux_project_command_group_exit(process_id, deadline).await?;
|
|
||||||
Ok(format!(
|
Ok(format!(
|
||||||
"{}并完成主进程回收",
|
"{}并完成主进程回收",
|
||||||
group_result.expect("group termination result checked")
|
group_result.expect("group termination result checked")
|
||||||
@@ -2271,7 +2220,7 @@ where
|
|||||||
let (exit_code, timed_out, termination_summary) = match wait {
|
let (exit_code, timed_out, termination_summary) = match wait {
|
||||||
ProjectCommandWait::Exited(Ok(status)) => {
|
ProjectCommandWait::Exited(Ok(status)) => {
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
let terminal = wait_established_project_command_terminal(gate).await;
|
let _terminal = wait_established_project_command_terminal(gate).await?;
|
||||||
if let Err(error) = tree.after_main_exit(&mut child).await {
|
if let Err(error) = tree.after_main_exit(&mut child).await {
|
||||||
stdout_task.abort();
|
stdout_task.abort();
|
||||||
stderr_task.abort();
|
stderr_task.abort();
|
||||||
@@ -2280,12 +2229,6 @@ where
|
|||||||
format!("command.exec 主进程退出后进程树未确认回收,需要人工核对:{error}"),
|
format!("command.exec 主进程退出后进程树未确认回收,需要人工核对:{error}"),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
if let Err(error) = terminal {
|
|
||||||
stdout_task.abort();
|
|
||||||
stderr_task.abort();
|
|
||||||
return Err(error);
|
|
||||||
}
|
|
||||||
(status.code(), false, None)
|
(status.code(), false, None)
|
||||||
}
|
}
|
||||||
ProjectCommandWait::Exited(Err(error)) => {
|
ProjectCommandWait::Exited(Err(error)) => {
|
||||||
@@ -2598,23 +2541,11 @@ mod tests {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
assert_eq!(unsafe { libc::prctl(libc::PR_SET_CHILD_SUBREAPER, 1) }, 0);
|
assert_eq!(unsafe { libc::prctl(libc::PR_SET_CHILD_SUBREAPER, 1) }, 0);
|
||||||
struct DescendantGuard(i32);
|
|
||||||
impl Drop for DescendantGuard {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
unsafe {
|
|
||||||
libc::kill(self.0, libc::SIGKILL);
|
|
||||||
libc::waitpid(self.0, std::ptr::null_mut(), 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 正常退出与取消/超时共用的 terminate 都覆盖 leader 已回收的窗口。
|
|
||||||
for terminate in [false, true] {
|
|
||||||
let mut command = tokio::process::Command::new("/bin/sh");
|
let mut command = tokio::process::Command::new("/bin/sh");
|
||||||
command
|
command
|
||||||
.args(["-c", "sleep 60 & echo $!; read release"])
|
.args(["-c", "sleep 60 & echo $!; read release"])
|
||||||
.stdin(Stdio::piped())
|
.stdin(Stdio::piped())
|
||||||
.stdout(Stdio::piped())
|
.stdout(Stdio::piped());
|
||||||
.kill_on_drop(true);
|
|
||||||
command.as_std_mut().process_group(0);
|
command.as_std_mut().process_group(0);
|
||||||
let mut child = command.spawn().unwrap();
|
let mut child = command.spawn().unwrap();
|
||||||
let tree = ProjectCommandTree::attach(&child).unwrap();
|
let tree = ProjectCommandTree::attach(&child).unwrap();
|
||||||
@@ -2624,33 +2555,9 @@ mod tests {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let descendant: i32 = line.trim().parse().unwrap();
|
let descendant: i32 = line.trim().parse().unwrap();
|
||||||
let descendant_guard = DescendantGuard(descendant);
|
|
||||||
drop(child.stdin.take());
|
drop(child.stdin.take());
|
||||||
child.wait().await.unwrap();
|
child.wait().await.unwrap();
|
||||||
let ProjectCommandTree::Group { pid, .. } = &tree;
|
let live_result = tree.after_main_exit(&mut child).await;
|
||||||
let error = tree
|
|
||||||
.request_owned_group_termination()
|
|
||||||
.expect_err("存活成员缺少 leader 身份时必须拒绝发送信号");
|
|
||||||
assert!(error.contains("leader 身份未确认"), "{error}");
|
|
||||||
let error = wait_linux_project_command_group_exit(*pid, tokio::time::Instant::now())
|
|
||||||
.await
|
|
||||||
.expect_err("持续存活成员必须在预算用尽时失败");
|
|
||||||
assert!(error.contains("仍有存活成员"), "{error}");
|
|
||||||
{
|
|
||||||
let cleanup = async {
|
|
||||||
if terminate {
|
|
||||||
tree.terminate(&mut child).await.map(|_| ())
|
|
||||||
} else {
|
|
||||||
tree.after_main_exit(&mut child).await
|
|
||||||
}
|
|
||||||
};
|
|
||||||
tokio::pin!(cleanup);
|
|
||||||
// 先 poll 生产清理,确认它确实等待,才让后代进入僵尸态。
|
|
||||||
tokio::select! {
|
|
||||||
biased;
|
|
||||||
result = &mut cleanup => panic!("后代仍存活时提前结束清理:{result:?}"),
|
|
||||||
_ = tokio::task::yield_now() => {}
|
|
||||||
}
|
|
||||||
assert_eq!(unsafe { libc::kill(descendant, libc::SIGKILL) }, 0);
|
assert_eq!(unsafe { libc::kill(descendant, libc::SIGKILL) }, 0);
|
||||||
let mut info = unsafe { std::mem::zeroed::<libc::siginfo_t>() };
|
let mut info = unsafe { std::mem::zeroed::<libc::siginfo_t>() };
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -2664,18 +2571,16 @@ mod tests {
|
|||||||
},
|
},
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
cleanup.await.expect("leader 消失后,组成员停止应完成清理");
|
let zombie_result = tree.after_main_exit(&mut child).await;
|
||||||
}
|
assert_eq!(
|
||||||
tree.after_main_exit(&mut child)
|
unsafe { libc::waitpid(descendant, std::ptr::null_mut(), 0) },
|
||||||
.await
|
descendant
|
||||||
.expect("僵尸不应阻止完成");
|
);
|
||||||
wait_linux_project_command_group_exit(*pid, tokio::time::Instant::now())
|
let error = live_result.expect_err("存活成员缺少 leader 身份时必须拒绝清理");
|
||||||
.await
|
assert!(error.contains("leader 身份未确认"), "{error}");
|
||||||
.expect("无活成员时不消耗等待预算");
|
zombie_result.expect("已回收 leader 的进程组只剩僵尸时不应要求人工核对");
|
||||||
drop(descendant_guard);
|
|
||||||
tree.after_main_exit(&mut child).await.unwrap();
|
tree.after_main_exit(&mut child).await.unwrap();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn owned_process_group_refuses_missing_or_reused_leader_identity() {
|
fn owned_process_group_refuses_missing_or_reused_leader_identity() {
|
||||||
@@ -3521,9 +3426,6 @@ raise SystemExit(code)'
|
|||||||
.expect("run timeout test");
|
.expect("run timeout test");
|
||||||
assert!(timed_out.timed_out);
|
assert!(timed_out.timed_out);
|
||||||
assert_eq!(timed_out.status, "failed");
|
assert_eq!(timed_out.status, "failed");
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
assert!(timed_out.output.contains("主进程已回收"));
|
|
||||||
#[cfg(not(target_os = "linux"))]
|
|
||||||
assert!(timed_out.output.contains("请求终止受控进程组"));
|
assert!(timed_out.output.contains("请求终止受控进程组"));
|
||||||
assert!(timed_out.output.contains("不等同完整 OS sandbox"));
|
assert!(timed_out.output.contains("不等同完整 OS sandbox"));
|
||||||
|
|
||||||
|
|||||||
@@ -2884,7 +2884,7 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
|||||||
## 2026-06-22 编辑器生成扣费与新用户赠送收口
|
## 2026-06-22 编辑器生成扣费与新用户赠送收口
|
||||||
|
|
||||||
- 背景:画板多个生成按钮已经展示泥点消耗,但部分图片、图标、UI 提取、视频、角色动作或音频链路只校验 / 展示价格,没有统一进入钱包预扣;新用户注册送泥点也需要与当前生成价格匹配。
|
- 背景:画板多个生成按钮已经展示泥点消耗,但部分图片、图标、UI 提取、视频、角色动作或音频链路只校验 / 展示价格,没有统一进入钱包预扣;新用户注册送泥点也需要与当前生成价格匹配。
|
||||||
- 决策:编辑器所有外部生成入口不再从前端请求接收 `priceMudPoints`,后端按运行时模型定价配置计算价格后统一进入 `execute_billable_asset_operation_with_cost` 或等价音频发布扣费链路;角色动作和视频使用真实登录用户作为扣费 owner。~~新用户注册赠送固定为 `100` 泥点。~~(2026-09-24 更正:注册赠送金额不是固定值,由线上 `profile_wallet_config.initial_mud_points` 配置决定,后台通过 `/admin/api/profile/wallet-config` 与钱包配置页随时调整;代码内常量仅为未写入配置时的兜底默认,本地编译行为不代表线上实际赠送金额,线上数值以配置表当前值为准。契约说明见 [`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`](../../【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md)。)
|
- 决策:编辑器所有外部生成入口不再从前端请求接收 `priceMudPoints`,后端按运行时模型定价配置计算价格后统一进入 `execute_billable_asset_operation_with_cost` 或等价音频发布扣费链路;角色动作和视频使用真实登录用户作为扣费 owner。新用户注册赠送固定为 `100` 泥点。
|
||||||
- 影响范围:编辑器图片 / 图片修改 / 图标 spritesheet / UI 提取 / 视频 / 角色动作 / 音频生成 BFF,前端画板生成提交模型,外部 OpenAPI,`module-runtime` 钱包注册奖励。
|
- 影响范围:编辑器图片 / 图片修改 / 图标 spritesheet / UI 提取 / 视频 / 角色动作 / 音频生成 BFF,前端画板生成提交模型,外部 OpenAPI,`module-runtime` 钱包注册奖励。
|
||||||
- 验证方式:运行编辑器图片、图标、UI 提取、视频、角色动作、音频扣费结构性测试,前端生成提交和 API client 测试,`module-runtime` 注册奖励测试。
|
- 验证方式:运行编辑器图片、图标、UI 提取、视频、角色动作、音频扣费结构性测试,前端生成提交和 API client 测试,`module-runtime` 注册奖励测试。
|
||||||
- 关联文档:`docs/【编辑器】模型定价配置管理方案-2026-06-22.md`、`docs/【编辑器】生成类面板Lovart统一改造方案-2026-06-17.md`、`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`。
|
- 关联文档:`docs/【编辑器】模型定价配置管理方案-2026-06-22.md`、`docs/【编辑器】生成类面板Lovart统一改造方案-2026-06-17.md`、`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`。
|
||||||
@@ -9578,3 +9578,11 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
|||||||
- 验证:`cargo test -p module-runtime --lib agc_models::`(4 passed)、`cargo test -p api-server --bin api-server agc` 与 `llm::`、AGC 客户端 `configuration::`、admin-web 页面定向 Vitest 与 typecheck、两套 workspace 的 `cargo fmt -- --check`、`check:encoding`/`check:doc-index`/`check:spacetime-schema`/`git diff --check`。
|
- 验证:`cargo test -p module-runtime --lib agc_models::`(4 passed)、`cargo test -p api-server --bin api-server agc` 与 `llm::`、AGC 客户端 `configuration::`、admin-web 页面定向 Vitest 与 typecheck、两套 workspace 的 `cargo fmt -- --check`、`check:encoding`/`check:doc-index`/`check:spacetime-schema`/`git diff --check`。
|
||||||
- 验证(真实上游 smoke,本地 dev DB):清空 `agc_model_catalog` 后启动 api-server → 日志 `已按上游模型列表初始化 AGC 模型目录 revision=1 model_count=6`;登录后 `GET /api/llm/models` 返回同一批模型、`displayName` 即上游原名、默认项为排序后第一项;上游不可达/非 2xx 时启动只记录 error、AGC 接口 `503` 且目录保持未初始化;目录已存在时重启不重写。
|
- 验证(真实上游 smoke,本地 dev DB):清空 `agc_model_catalog` 后启动 api-server → 日志 `已按上游模型列表初始化 AGC 模型目录 revision=1 model_count=6`;登录后 `GET /api/llm/models` 返回同一批模型、`displayName` 即上游原名、默认项为排序后第一项;上游不可达/非 2xx 时启动只记录 error、AGC 接口 `503` 且目录保持未初始化;目录已存在时重启不重写。
|
||||||
- 边界(未验证/残留):上游在售模型超过 32 条时同步会失败(目录项上限未改);`qwen-image-3.0` 这类图像模型会一起进入目录,是否对 AGC 隐藏由 owner 在后台停用;混合版本期间未升级的 api-server 会把自己的 AGC 接口打到 `503`,module 与 api-server 必须同批发布/回滚。
|
- 边界(未验证/残留):上游在售模型超过 32 条时同步会失败(目录项上限未改);`qwen-image-3.0` 这类图像模型会一起进入目录,是否对 AGC 隐藏由 owner 在后台停用;混合版本期间未升级的 api-server 会把自己的 AGC 接口打到 `503`,module 与 api-server 必须同批发布/回滚。
|
||||||
|
|
||||||
|
## 2026-09-24 移动端主站顶栏收成单行、个人中心弹窗统一走页面级 portal
|
||||||
|
|
||||||
|
- 背景:移动端主站(`PlatformEntryActiveFlowShell` 工作台壳层)顶栏在 `e3682fd06` 加入「下载客户端」后,`flex-wrap` 让品牌与下载/泥点/账户入口折成两行;同一壳层里 `.platform-desktop-layout` 带 `z-index: 1` 形成 stacking context,把 `portal={false}` 的弹窗封在这个 context 内,低于 `z-index: 60` 的固定底部主菜单——头像裁剪弹窗的「取消 / 上传」正好被「游戏 / 我的」压住点不到。
|
||||||
|
- 决策(顶栏):顶栏在所有断点都保持单行(`flex-nowrap`),窄屏靠两个降级点让位:`< 640px` 下载入口收起为 44×44 图标按钮(保留 `aria-label="下载客户端"`),`< 480px` 顶栏品牌只留 IP 标识(`platform-desktop-topbar__brand` 内隐藏 `platform-brand-logo__copy`)。品牌字标、下载文案的完整形态在 `>= 640px` / `>= 480px` 恢复,桌面端外观不变。
|
||||||
|
- 决策(弹窗层级):个人中心的独立弹窗(`SquareImageCropModal`、`PlatformProfileModalShell` 两个壳层)改回 `UnifiedModal` 默认的页面级 portal(挂到 `document.body`),不再用 `portal={false}` 内联渲染;层级由现有 `z-[80]` 决定,仍在鉴权 / 法务 / 预览等 `z-[110]` 以上弹窗之下。`PlatformStatusDialog` 保持内联(承载运行态内嵌遮罩,不在本次范围)。
|
||||||
|
- 影响范围:`src/components/platform-entry/PlatformEntryActiveFlowShell.tsx`、`src/components/creation-home/ClientDownloadEntry.tsx`、`src/components/common/SquareImageCropModal.tsx`、`src/components/platform-entry/PlatformProfileModalShell.tsx`、`src/index.css`。
|
||||||
|
- 验证:本地 Vite + Chromium 真机尺寸复核(320/360/375/390/414/480/639/640/768/1024):顶栏在 320–768 全部单行、右侧留白 16px;弹窗内「上传 / 保存」`elementFromPoint` 命中自身而非底部菜单,overlay 父节点为 `BODY`。定向 Vitest(`ClientDownloadEntry`、`PlatformProfileModalShell`、`PlatformActiveProfileView`、`PlatformEntryActiveFlowShell`、`UnifiedModalPortalTheme`)与 `prettier --check`、`eslint`、`tsc`、`check:encoding` 通过。
|
||||||
|
|||||||
@@ -5994,15 +5994,18 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
|
|||||||
- **验证**:宿主 `the_opening_user_item_is_emitted_before_anything_that_can_fail_in_the_turn`、前端 `本轮用户条目没到时,失败说明按身份挂回自己那一轮,本地气泡不再自成假回合` 与 `收口早退不吞掉还没写进界面的失败说明(订阅重建只回放生命周期锚点)`。
|
- **验证**:宿主 `the_opening_user_item_is_emitted_before_anything_that_can_fail_in_the_turn`、前端 `本轮用户条目没到时,失败说明按身份挂回自己那一轮,本地气泡不再自成假回合` 与 `收口早退不吞掉还没写进界面的失败说明(订阅重建只回放生命周期锚点)`。
|
||||||
- **关联**:`apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs`、`.../agent/direct_runtime/user_input.rs`、`.../chat/conversation/{directThreadChat.ts,directTurnPresentation.ts}`、`docs/adr/【ADR】DirectProject命令接单化-2026-09-23.md`。
|
- **关联**:`apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs`、`.../agent/direct_runtime/user_input.rs`、`.../chat/conversation/{directThreadChat.ts,directTurnPresentation.ts}`、`docs/adr/【ADR】DirectProject命令接单化-2026-09-23.md`。
|
||||||
|
|
||||||
## Linux command.exec 的 leader 回收与后代退出存在时序差
|
|
||||||
|
|
||||||
- bwrap 主进程已经 `wait` 回收时,namespace 后代仍可能短暂处于退出过程;一次 `/proc` 扫描发现活成员后再读取 leader 身份,会把正常退出误报为需人工核对。容器 PID 1 未回收的 `Z / X` 成员也不能当作活进程。
|
|
||||||
- 正常退出与取消/超时共用有界的组退出确认,组内无活成员立即返回;缺失或变化的 leader 身份不能授权补发信号,持续活成员或读取失败仍报错。启动身份在 ready 后、commit 前记录;terminal 协议错误也不能跳过清理与 reader 取消。
|
|
||||||
- 回归使用独立 subreaper 夹具:先 poll 清理并确认仍在等待,再让同组后代退出,覆盖正常收尾和取消/超时的共同清理路径;保持现有 CI 分片与并行,不靠取消并行或失败重试消除竞态。
|
|
||||||
|
|
||||||
## 2026-09-24 DirectProject「接单窗口里看不到自己刚发的话」是设计,不是丢消息
|
## 2026-09-24 DirectProject「接单窗口里看不到自己刚发的话」是设计,不是丢消息
|
||||||
|
|
||||||
- **现象**:按下发送后聊天区里不会立刻出现自己那句话;宿主还在接单 / 落盘的那段时间只能看到 composer 忙态、状态行与「陶泥儿正在处理」卡片(卡片这一段不读秒——起点要等宿主的 `turn.started.at`),滚动也停在原地。订阅重建的窗口同理。容易被读成"消息丢了 / 没发出去"。
|
- **现象**:按下发送后聊天区里不会立刻出现自己那句话;宿主还在接单 / 落盘的那段时间只能看到 composer 忙态、状态行与「陶泥儿正在处理」卡片(卡片这一段不读秒——起点要等宿主的 `turn.started.at`),滚动也停在原地。订阅重建的窗口同理。容易被读成"消息丢了 / 没发出去"。
|
||||||
- **原因**:本地乐观用户气泡已删(ADR「DirectProject命令接单化」后续更新 2026-09-24)。用户气泡的唯一来源是宿主下发的开口条目(发点=接单成立 + 用户条目落盘成功 + 起 codex 之前)。删它的收益是"回合归属只认身份"不再需要给本地消息一份同名身份,投影也少一个展示态(`awaiting-start`)。
|
- **原因**:本地乐观用户气泡已删(ADR「DirectProject命令接单化」后续更新 2026-09-24)。用户气泡的唯一来源是宿主下发的开口条目(发点=接单成立 + 用户条目落盘成功 + 起 codex 之前)。删它的收益是"回合归属只认身份"不再需要给本地消息一份同名身份,投影也少一个展示态(`awaiting-start`)。
|
||||||
- **排查提示**:窗口期不要拿"有没有本地气泡"当发送成功的证据;证据是 `invoke` 返回 `Ok`(接单成立)与随后到达的 `turn.started` / 开口条目。显示时间与耗时也全以宿主事件为准:起点 `turn.started.at`、终点 `turn.completed.at`;重进项目读回来的历史回合两边都空,整条「本轮结束于 … 」直接隐藏(不再出现 0.0 秒)。
|
- **排查提示**:窗口期不要拿"有没有本地气泡"当发送成功的证据;证据是 `invoke` 返回 `Ok`(接单成立)与随后到达的 `turn.started` / 开口条目。显示时间与耗时也全以宿主事件为准:起点 `turn.started.at`、终点 `turn.completed.at`;重进项目读回来的历史回合两边都空,整条「本轮结束于 … 」直接隐藏(不再出现 0.0 秒)。
|
||||||
- **关联**:`apps/ai-game-creator-shell/src/view/project-development/chat/conversation/directTurnPresentation.ts`、`.../chat/controller/useDirectProjectChatController.ts`、`.../chat/components/DirectProjectConversation/DirectProjectTurn.tsx`、`docs/adr/【ADR】DirectProject命令接单化-2026-09-23.md`。
|
- **关联**:`apps/ai-game-creator-shell/src/view/project-development/chat/conversation/directTurnPresentation.ts`、`.../chat/controller/useDirectProjectChatController.ts`、`.../chat/components/DirectProjectConversation/DirectProjectTurn.tsx`、`docs/adr/【ADR】DirectProject命令接单化-2026-09-23.md`。
|
||||||
|
|
||||||
|
## 2026-09-24 移动端固定底部菜单会压住「内联」弹窗:`z-index` 高也点不到
|
||||||
|
|
||||||
|
- **现象**:手机端主站从头像入口上传头像,裁剪弹窗的「取消 / 上传」被底部「游戏 / 我的」菜单盖住,点按钮命中底部菜单;弹窗遮罩、面板本身都正常渲染,只有底部一条区域失去交互。
|
||||||
|
- **原因**:`.platform-desktop-shell--workbench .platform-desktop-layout` 带 `position: relative; z-index: 1`,**自成一个 stacking context**。底部主菜单(`.platform-mobile-bottom-dock`,`z-index: 60`)是 layout 的兄弟节点,而弹窗用 `portal={false}` 内联渲染在 layout 内部——弹窗自己的 `z-[80]` 只在 layout 的局部层叠里比较,整体被限制在 `z-index: 1` 之上、`z-index: 60` 之下。**在祖先 stacking context 内抬高子元素 `z-index` 无效,必须让弹窗跳出该 context**。
|
||||||
|
- **处理(现行口径)**:独立弹窗一律走 `UnifiedModal` 默认的页面级 portal(挂 `document.body`),不要用 `portal={false}` 内联渲染。目前只剩 `PlatformStatusDialog` 保留内联(运行态内嵌遮罩,且它渲染位置本就在 layout 之外)。
|
||||||
|
- **同时**:主站顶栏(`platform-desktop-topbar`)在窄屏必须保持单行——加 `flex-wrap` 后新增一个动作入口就会让品牌折到第二行;窄屏降级靠 `< 640px` 图标化下载入口、`< 480px` 只留品牌 IP 标识。
|
||||||
|
- **验证**:真机尺寸下用 `document.elementFromPoint(按钮中心)` 断言命中的是按钮自身而不是底部菜单(`overlay.parentElement === document.body`);顶栏断点矩阵(320–768)断言单行且无横向溢出。
|
||||||
|
- **关联**:`src/components/common/SquareImageCropModal.tsx`、`src/components/platform-entry/PlatformProfileModalShell.tsx`、`src/components/platform-entry/PlatformEntryActiveFlowShell.tsx`、`src/index.css`。
|
||||||
|
|||||||
@@ -582,8 +582,6 @@ V1.11 把命令安全边界从“固定 program + argv 规则 + 隔离环境变
|
|||||||
|
|
||||||
### V1.11.1 可信 launch 握手
|
### V1.11.1 可信 launch 握手
|
||||||
|
|
||||||
Linux 一次性命令的退出确认按进程组最终状态判断:正常退出、取消与超时在同一有界清理预算内等待组内非 `Z / X` 成员消失,空组立即返回。bwrap leader 已回收而 namespace 后代尚在退出时,不凭一次瞬时扫描报错,也不向无法确认 leader 启动身份的进程组发送信号;持续存活、不可读或归属不明仍失败关闭。启动身份在放行目标前记录,terminal 协议错误也必须完成清理并停止输出 reader。回归用独立 subreaper 夹具控制后代退出时序,保留 CI 的现有并行与分片,不增加测试失败重试。
|
|
||||||
|
|
||||||
V1.11.1 必须把 `prepared -> child-created -> sandbox-ready -> commit-persisted -> exec-established -> running/exited` 做成 launcher 状态机,不能再把 bwrap 进程 spawn 或 `--json-status-fd` 的 `child-pid` 当作 sandbox-ready。实测 `child-pid` 会在 `--block-fd` 放行前出现,此时目标程序尚未执行;它只能证明 namespace child 已创建。关闭 block writer 也不能作为 abort,因为 bwrap 会把 EOF 当作可读并继续执行,失败关闭必须显式 kill + wait/reap。
|
V1.11.1 必须把 `prepared -> child-created -> sandbox-ready -> commit-persisted -> exec-established -> running/exited` 做成 launcher 状态机,不能再把 bwrap 进程 spawn 或 `--json-status-fd` 的 `child-pid` 当作 sandbox-ready。实测 `child-pid` 会在 `--block-fd` 放行前出现,此时目标程序尚未执行;它只能证明 namespace child 已创建。关闭 block writer 也不能作为 abort,因为 bwrap 会把 EOF 当作可读并继续执行,失败关闭必须显式 kill + wait/reap。
|
||||||
|
|
||||||
- Linux 最终 `COMMAND` 必须先进入受信任 trampoline,而不是直接进入用户目标。trampoline 通过与 PTY/transcript 分离的私有控制通道发送带随机 nonce 的 `SANDBOX_READY`,等待 Runtime 完成 revision / verification gate / process record 的 durable commit 后接收 `COMMIT_EXEC`,再用 exec-error pipe 启动目标并回报 `EXEC_ESTABLISHED` 或 `TARGET_EXEC_FAILED`。commit 前的 EOF、错 nonce、协议错误和持久化失败都必须杀死并回收整个 bwrap 树,目标零执行。
|
- Linux 最终 `COMMAND` 必须先进入受信任 trampoline,而不是直接进入用户目标。trampoline 通过与 PTY/transcript 分离的私有控制通道发送带随机 nonce 的 `SANDBOX_READY`,等待 Runtime 完成 revision / verification gate / process record 的 durable commit 后接收 `COMMIT_EXEC`,再用 exec-error pipe 启动目标并回报 `EXEC_ESTABLISHED` 或 `TARGET_EXEC_FAILED`。commit 前的 EOF、错 nonce、协议错误和持久化失败都必须杀死并回收整个 bwrap 树,目标零执行。
|
||||||
|
|||||||
@@ -319,7 +319,6 @@ export function SquareImageCropModal({
|
|||||||
closeLabel={labels.close}
|
closeLabel={labels.close}
|
||||||
closeVariant="profileCompact"
|
closeVariant="profileCompact"
|
||||||
closeIcon="×"
|
closeIcon="×"
|
||||||
portal={false}
|
|
||||||
zIndexClassName="z-[80]"
|
zIndexClassName="z-[80]"
|
||||||
panelClassName="platform-remap-surface max-w-sm rounded-[1.4rem]"
|
panelClassName="platform-remap-surface max-w-sm rounded-[1.4rem]"
|
||||||
headerClassName="border-white/10 px-5 py-4"
|
headerClassName="border-white/10 px-5 py-4"
|
||||||
|
|||||||
@@ -75,14 +75,16 @@ export function ClientDownloadEntry() {
|
|||||||
tone="secondary"
|
tone="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
shape="pill"
|
shape="pill"
|
||||||
className="min-h-11 shrink-0 whitespace-nowrap"
|
aria-label="下载客户端"
|
||||||
|
title="下载客户端"
|
||||||
|
className="min-h-11 shrink-0 gap-0 whitespace-nowrap max-sm:h-11 max-sm:w-11 max-sm:px-0 sm:gap-2 sm:px-4"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setState({ status: 'loading' });
|
setState({ status: 'loading' });
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Download className="h-4 w-4" aria-hidden="true" />
|
<Download className="h-4 w-4 shrink-0" aria-hidden="true" />
|
||||||
下载客户端
|
<span className="hidden sm:inline">下载客户端</span>
|
||||||
</PlatformActionButton>
|
</PlatformActionButton>
|
||||||
<UnifiedModal
|
<UnifiedModal
|
||||||
open={open}
|
open={open}
|
||||||
|
|||||||
@@ -208,6 +208,9 @@ describe('CreationLandingView', () => {
|
|||||||
expect(subtitle.textContent).toContain('美术 Agent');
|
expect(subtitle.textContent).toContain('美术 Agent');
|
||||||
expect(subtitle.textContent).toContain('无限画布');
|
expect(subtitle.textContent).toContain('无限画布');
|
||||||
expect(subtitle.textContent).toContain('角色、场景、UI 与宣发素材');
|
expect(subtitle.textContent).toContain('角色、场景、UI 与宣发素材');
|
||||||
|
expect(
|
||||||
|
screen.getByText('登录即送 100 泥点,可以免费制作 50 个素材'),
|
||||||
|
).toBeTruthy();
|
||||||
expect(
|
expect(
|
||||||
screen.getByRole('heading', {
|
screen.getByRole('heading', {
|
||||||
level: 2,
|
level: 2,
|
||||||
|
|||||||
@@ -1418,6 +1418,9 @@ export function CreationLandingView({
|
|||||||
<br />
|
<br />
|
||||||
用美术 Agent 与无限画布,快速制作角色、场景、UI 与宣发素材。
|
用美术 Agent 与无限画布,快速制作角色、场景、UI 与宣发素材。
|
||||||
</p>
|
</p>
|
||||||
|
<p className="creation-landing__hero-benefit">
|
||||||
|
登录即送 100 泥点,可以免费制作 50 个素材
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="creation-landing__hero-actions">
|
<div className="creation-landing__hero-actions">
|
||||||
<PlatformActionButton
|
<PlatformActionButton
|
||||||
|
|||||||
@@ -618,8 +618,8 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<div className="platform-desktop-main flex min-w-0 flex-1 flex-col">
|
<div className="platform-desktop-main flex min-w-0 flex-1 flex-col">
|
||||||
<header className="platform-desktop-topbar flex min-h-16 shrink-0 flex-wrap items-center justify-between gap-1 gap-y-2 px-4 py-2 sm:flex-nowrap sm:gap-3 sm:px-6 sm:py-0">
|
<header className="platform-desktop-topbar flex min-h-16 shrink-0 flex-nowrap items-center justify-between gap-1 px-4 py-2 sm:gap-3 sm:px-6 sm:py-0">
|
||||||
<div className="min-w-0 shrink-0 lg:hidden">
|
<div className="platform-desktop-topbar__brand min-w-0 shrink-0 lg:hidden">
|
||||||
<ActivePlatformBrand />
|
<ActivePlatformBrand />
|
||||||
</div>
|
</div>
|
||||||
<div className="platform-desktop-topbar__brand-search hidden min-w-0 flex-1 items-center lg:flex">
|
<div className="platform-desktop-topbar__brand-search hidden min-w-0 flex-1 items-center lg:flex">
|
||||||
@@ -662,7 +662,7 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="platform-desktop-topbar__actions flex max-w-full shrink-0 flex-wrap items-center gap-1 sm:flex-nowrap sm:gap-2 lg:gap-3">
|
<div className="platform-desktop-topbar__actions flex min-w-0 shrink-0 items-center gap-1 sm:gap-2 lg:gap-3">
|
||||||
<ClientDownloadEntry />
|
<ClientDownloadEntry />
|
||||||
{isAuthenticated ? (
|
{isAuthenticated ? (
|
||||||
<PlatformMudPointWalletEntry
|
<PlatformMudPointWalletEntry
|
||||||
|
|||||||
@@ -84,7 +84,6 @@ export function PlatformProfileModalShell({
|
|||||||
showCloseButton={showCloseButton}
|
showCloseButton={showCloseButton}
|
||||||
closeOnBackdrop={false}
|
closeOnBackdrop={false}
|
||||||
closeOnEscape={false}
|
closeOnEscape={false}
|
||||||
portal={false}
|
|
||||||
size={size}
|
size={size}
|
||||||
zIndexClassName={zIndexClassName}
|
zIndexClassName={zIndexClassName}
|
||||||
overlayClassName={PROFILE_MODAL_OVERLAY_CLASS}
|
overlayClassName={PROFILE_MODAL_OVERLAY_CLASS}
|
||||||
@@ -128,7 +127,6 @@ export function PlatformProfileSecondaryModalShell({
|
|||||||
showCloseButton={false}
|
showCloseButton={false}
|
||||||
closeOnBackdrop={false}
|
closeOnBackdrop={false}
|
||||||
closeOnEscape={false}
|
closeOnEscape={false}
|
||||||
portal={false}
|
|
||||||
size={size}
|
size={size}
|
||||||
zIndexClassName={zIndexClassName}
|
zIndexClassName={zIndexClassName}
|
||||||
overlayClassName={
|
overlayClassName={
|
||||||
|
|||||||
+31
-1
@@ -2945,7 +2945,8 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
|
|||||||
gap: 0.42rem;
|
gap: 0.42rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.creation-landing__hero-subtitle {
|
.creation-landing__hero-subtitle,
|
||||||
|
.creation-landing__hero-benefit {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #8a7466;
|
color: #8a7466;
|
||||||
text-indent: 0;
|
text-indent: 0;
|
||||||
@@ -2958,6 +2959,14 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
|
|||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.creation-landing__hero-benefit {
|
||||||
|
color: #9a6f56;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 680;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
.creation-landing__hero-actions {
|
.creation-landing__hero-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -3925,6 +3934,11 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
|
|||||||
line-height: 1.62;
|
line-height: 1.62;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.creation-landing__hero-benefit {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
.creation-landing__hero-actions {
|
.creation-landing__hero-actions {
|
||||||
gap: 0.72rem;
|
gap: 0.72rem;
|
||||||
}
|
}
|
||||||
@@ -12612,6 +12626,22 @@ button.image-canvas-editor__reference-chip:disabled {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding-inline: 0.12rem;
|
padding-inline: 0.12rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.platform-desktop-shell--workbench
|
||||||
|
.platform-desktop-topbar
|
||||||
|
.platform-desktop-topbar__actions {
|
||||||
|
gap: 0.38rem !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 窄屏顶栏必须保持单行:品牌只保留 IP 标识,字标让位给下载、泥点与账户入口。 */
|
||||||
|
@media (max-width: 479px) {
|
||||||
|
.platform-desktop-shell--workbench
|
||||||
|
.platform-desktop-topbar
|
||||||
|
.platform-desktop-topbar__brand
|
||||||
|
.platform-brand-logo__copy {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.platform-desktop-shell--workbench .platform-desktop-rail {
|
.platform-desktop-shell--workbench .platform-desktop-rail {
|
||||||
|
|||||||
Reference in New Issue
Block a user