docs: Pixelle 连接凭证管理实现计划(5 任务)
serde_yaml+config路径 → service(settings读写+yaml同步+restart) → admin端点(掩码) → 前端连接管理卡片 → 部署联调。写文件+重启容器同步机制。 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,503 @@
|
||||
# Pixelle 连接凭证纳入 BoosAPI 管理 实现计划
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 在 BoosAPI 连接管理页加「Pixelle 连接」卡片,管 runninghub_api_key / comfyui_url / comfyui_api_key 三个凭证,保存即写入 settings 表 + 同步 Pixelle config.yaml + 重启 pixelle 容器生效。
|
||||
|
||||
**Architecture:** 新增 service `pixelle_connection.rs`(读写 settings 表 + 改 config.yaml YAML 三键 + docker restart)+ admin 路由(GET/PUT,密钥掩码)+ 前端 Accounts 页卡片。settings 表为真相源,config.yaml 是同步目标。
|
||||
|
||||
**Tech Stack:** Rust(axum, sqlx, serde_yaml<新增依赖>, tokio::process)、React/TS。
|
||||
|
||||
> **构建/测试:** `cargo build -j 2`(-j2 防 OOM)、`cargo test -p boosapi --lib pixelle_connection`、`npm run typecheck:web`。分支 `feat/comfy-cloud-adapter`。Pixelle compose 目录 `/opt/Pixelle-Video`,config `/opt/Pixelle-Video/config.yaml`。
|
||||
|
||||
---
|
||||
|
||||
## 文件结构
|
||||
|
||||
- Modify `Cargo.toml` — 加 `serde_yaml` 依赖。
|
||||
- Modify `src/config.rs` — `PixelleVideoConfig` 加 `config_path` / `compose_dir` 两字段(默认 `/opt/Pixelle-Video/config.yaml`、`/opt/Pixelle-Video`)。
|
||||
- Create `src/services/pixelle_connection.rs` — read/save settings + sync_to_pixelle(改 yaml)+ restart。
|
||||
- Modify `src/services/mod.rs` — `pub mod pixelle_connection;`。
|
||||
- Create `src/routes/admin/pixelle_connection.rs` — GET/PUT handler(掩码)。
|
||||
- Modify `src/routes/admin/mod.rs` — 声明子模块 + 挂 2 条路由。
|
||||
- Modify `src/web/api.ts` — adminApi 加 getPixelleConnection / savePixelleConnection。
|
||||
- Modify `src/web/pages/Accounts.tsx` — 加「Pixelle 连接」卡片。
|
||||
|
||||
---
|
||||
|
||||
<!-- TASKS -->
|
||||
|
||||
## Task 1: 加 serde_yaml 依赖 + config 路径字段
|
||||
|
||||
**Files:**
|
||||
- Modify: `Cargo.toml`
|
||||
- Modify: `src/config.rs`
|
||||
|
||||
- [ ] **Step 1: Cargo.toml 加 serde_yaml**
|
||||
|
||||
在 `[dependencies]` 段加(版本对齐项目 serde 生态,用 0.9):
|
||||
```toml
|
||||
serde_yaml = "0.9"
|
||||
```
|
||||
|
||||
- [ ] **Step 2: PixelleVideoConfig 加两字段(src/config.rs,struct 内 timeout_secs 后)**
|
||||
|
||||
在 `pub timeout_secs: u64,` 之后加:
|
||||
```rust
|
||||
#[serde(default = "default_pixelle_config_path")]
|
||||
pub config_path: String,
|
||||
#[serde(default = "default_pixelle_compose_dir")]
|
||||
pub compose_dir: String,
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 加默认值函数(src/config.rs,default_pixelle_backend_provider 附近)**
|
||||
|
||||
```rust
|
||||
fn default_pixelle_config_path() -> String {
|
||||
"/opt/Pixelle-Video/config.yaml".to_string()
|
||||
}
|
||||
fn default_pixelle_compose_dir() -> String {
|
||||
"/opt/Pixelle-Video".to_string()
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: PixelleVideoConfig::default() 补两字段**
|
||||
|
||||
找到 `impl Default for ...` 或 `PixelleVideoConfig::default()`(约 line 207 区域,有 `api_base_url: Some(...)` 那个),在其字段列表末尾加:
|
||||
```rust
|
||||
config_path: default_pixelle_config_path(),
|
||||
compose_dir: default_pixelle_compose_dir(),
|
||||
```
|
||||
|
||||
- [ ] **Step 5: 构建确认**
|
||||
|
||||
Run: `cd /root/metapi && cargo build -j 2 2>&1 | tail -3`
|
||||
Expected: `Finished`。
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
cd /root/metapi && git add Cargo.toml Cargo.lock src/config.rs
|
||||
git commit -m "feat(pixelle): 加 serde_yaml + Pixelle config 路径配置项
|
||||
|
||||
Co-Authored-By: Claude <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
<!-- T2 -->
|
||||
|
||||
## Task 2: pixelle_connection service(settings + yaml 同步)
|
||||
|
||||
**Files:**
|
||||
- Create: `src/services/pixelle_connection.rs`
|
||||
- Modify: `src/services/mod.rs`
|
||||
|
||||
- [ ] **Step 1: src/services/mod.rs 加模块声明**
|
||||
|
||||
```rust
|
||||
pub mod pixelle_connection;
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 写失败测试 `src/services/pixelle_connection.rs`(含结构 + sync 测试)**
|
||||
|
||||
```rust
|
||||
use crate::error::{AppError, AppResult};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PixelleConnection {
|
||||
pub runninghub_api_key: Option<String>,
|
||||
pub comfyui_url: Option<String>,
|
||||
pub comfyui_api_key: Option<String>,
|
||||
}
|
||||
|
||||
const KEY_RH: &str = "pixelle.runninghub_api_key";
|
||||
const KEY_URL: &str = "pixelle.comfyui_url";
|
||||
const KEY_CK: &str = "pixelle.comfyui_api_key";
|
||||
|
||||
pub async fn read_connection(db: &SqlitePool) -> AppResult<PixelleConnection> {
|
||||
async fn get(db: &SqlitePool, k: &str) -> AppResult<Option<String>> {
|
||||
Ok(sqlx::query_scalar("SELECT value FROM settings WHERE key = ?")
|
||||
.bind(k).fetch_optional(db).await?)
|
||||
}
|
||||
Ok(PixelleConnection {
|
||||
runninghub_api_key: get(db, KEY_RH).await?,
|
||||
comfyui_url: get(db, KEY_URL).await?,
|
||||
comfyui_api_key: get(db, KEY_CK).await?,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn save_connection(db: &SqlitePool, conn: &PixelleConnection) -> AppResult<()> {
|
||||
async fn put(db: &SqlitePool, k: &str, v: &Option<String>) -> AppResult<()> {
|
||||
if let Some(val) = v {
|
||||
sqlx::query("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)")
|
||||
.bind(k).bind(val).execute(db).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
put(db, KEY_RH, &conn.runninghub_api_key).await?;
|
||||
put(db, KEY_URL, &conn.comfyui_url).await?;
|
||||
put(db, KEY_CK, &conn.comfyui_api_key).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 改 config.yaml 的 comfyui.{runninghub_api_key,comfyui_url,comfyui_api_key} 三键,其余保留。
|
||||
/// 改前备份到 <path>.bak-<ts>。返回备份路径。
|
||||
pub fn sync_to_pixelle(config_path: &str, conn: &PixelleConnection) -> AppResult<String> {
|
||||
let content = std::fs::read_to_string(config_path)
|
||||
.map_err(|e| AppError::ServiceUnavailable(format!("读取 Pixelle config 失败: {e}")))?;
|
||||
let mut doc: serde_yaml::Value = serde_yaml::from_str(&content)
|
||||
.map_err(|e| AppError::ServiceUnavailable(format!("解析 config.yaml 失败: {e}")))?;
|
||||
let comfyui = doc.get_mut("comfyui")
|
||||
.ok_or_else(|| AppError::ServiceUnavailable("config.yaml 缺少 comfyui 段".into()))?;
|
||||
let map = comfyui.as_mapping_mut()
|
||||
.ok_or_else(|| AppError::ServiceUnavailable("comfyui 段不是映射".into()))?;
|
||||
if let Some(v) = &conn.runninghub_api_key {
|
||||
map.insert("runninghub_api_key".into(), serde_yaml::Value::String(v.clone()));
|
||||
}
|
||||
if let Some(v) = &conn.comfyui_url {
|
||||
map.insert("comfyui_url".into(), serde_yaml::Value::String(v.clone()));
|
||||
}
|
||||
if let Some(v) = &conn.comfyui_api_key {
|
||||
map.insert("comfyui_api_key".into(), serde_yaml::Value::String(v.clone()));
|
||||
}
|
||||
let ts = chrono::Utc::now().format("%Y%m%d%H%M%S").to_string();
|
||||
let backup = format!("{config_path}.bak-{ts}");
|
||||
std::fs::copy(config_path, &backup)
|
||||
.map_err(|e| AppError::ServiceUnavailable(format!("备份失败: {e}")))?;
|
||||
let out = serde_yaml::to_string(&doc)
|
||||
.map_err(|e| AppError::Internal(anyhow::anyhow!(e)))?;
|
||||
std::fs::write(config_path, out)
|
||||
.map_err(|e| AppError::ServiceUnavailable(format!("写入 config 失败: {e}")))?;
|
||||
Ok(backup)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn sync_updates_three_keys_preserves_rest() {
|
||||
let dir = std::env::temp_dir().join(format!("pxtest_{}", uuid::Uuid::new_v4().simple()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let path = dir.join("config.yaml");
|
||||
std::fs::write(&path,
|
||||
"project_name: Pixelle-Video\ncomfyui:\n comfyui_url: https://old\n runninghub_api_key: oldkey\n comfyui_api_key: oldck\n runninghub_concurrent_limit: 1\n").unwrap();
|
||||
let conn = PixelleConnection {
|
||||
runninghub_api_key: Some("newkey".into()),
|
||||
comfyui_url: Some("http://new".into()),
|
||||
comfyui_api_key: Some("newck".into()),
|
||||
};
|
||||
let backup = sync_to_pixelle(path.to_str().unwrap(), &conn).unwrap();
|
||||
assert!(std::path::Path::new(&backup).exists());
|
||||
let after = std::fs::read_to_string(&path).unwrap();
|
||||
assert!(after.contains("newkey"));
|
||||
assert!(after.contains("http://new"));
|
||||
assert!(after.contains("newck"));
|
||||
// 保留其余键
|
||||
assert!(after.contains("runninghub_concurrent_limit"));
|
||||
assert!(after.contains("project_name"));
|
||||
std::fs::remove_dir_all(&dir).ok();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 运行测试,确认失败**
|
||||
|
||||
Run: `cd /root/metapi && cargo test -p boosapi --lib services::pixelle_connection 2>&1 | tail -15`
|
||||
Expected: FAIL —— 模块/函数未定义(首次编译错误)。
|
||||
|
||||
- [ ] **Step 4: 实现**
|
||||
|
||||
Step 2 的代码即实现(测试与实现同文件)。确保 mod.rs 已声明。
|
||||
|
||||
- [ ] **Step 5: 运行测试,确认通过**
|
||||
|
||||
Run: `cd /root/metapi && cargo test -p boosapi --lib services::pixelle_connection 2>&1 | tail -10`
|
||||
Expected: PASS(1 个测试)。
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
cd /root/metapi && git add src/services/mod.rs src/services/pixelle_connection.rs
|
||||
git commit -m "feat(pixelle): 连接凭证 service(settings 读写 + config.yaml 同步)
|
||||
|
||||
Co-Authored-By: Claude <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
<!-- T3 -->
|
||||
|
||||
## Task 3: restart helper + admin 路由(掩码)
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/services/pixelle_connection.rs`(加 restart)
|
||||
- Create: `src/routes/admin/pixelle_connection.rs`
|
||||
- Modify: `src/routes/admin/mod.rs`
|
||||
|
||||
- [ ] **Step 1: 加 restart_pixelle(pixelle_connection.rs,sync_to_pixelle 之后)**
|
||||
|
||||
```rust
|
||||
pub async fn restart_pixelle(compose_dir: &str) -> AppResult<()> {
|
||||
let output = tokio::process::Command::new("docker")
|
||||
.args(["compose", "restart", "api"])
|
||||
.current_dir(compose_dir)
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| AppError::ServiceUnavailable(format!("docker compose restart 启动失败: {e}")))?;
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(AppError::ServiceUnavailable(format!(
|
||||
"Pixelle 重启失败: {}", stderr.trim()
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 写 admin 路由 `src/routes/admin/pixelle_connection.rs`**
|
||||
|
||||
```rust
|
||||
use crate::error::AppResult;
|
||||
use crate::services::pixelle_connection::{self, PixelleConnection};
|
||||
use crate::AppState;
|
||||
use axum::{extract::State, Json};
|
||||
use serde::Deserialize;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
const MASK_SENTINEL: &str = "••••";
|
||||
|
||||
fn mask(v: &Option<String>) -> Value {
|
||||
match v {
|
||||
Some(s) if s.len() > 4 => json!(format!("{}{}", MASK_SENTINEL, &s[s.len()-4..])),
|
||||
Some(s) if !s.is_empty() => json!(MASK_SENTINEL),
|
||||
_ => Value::Null,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_connection(State(state): State<AppState>) -> AppResult<Json<Value>> {
|
||||
let c = pixelle_connection::read_connection(&state.db).await?;
|
||||
Ok(Json(json!({
|
||||
"success": true,
|
||||
"connection": {
|
||||
// url 不算敏感,明文回;两个 key 掩码
|
||||
"comfyuiUrl": c.comfyui_url,
|
||||
"runninghubApiKey": mask(&c.runninghub_api_key),
|
||||
"comfyuiApiKey": mask(&c.comfyui_api_key),
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdatePayload {
|
||||
runninghub_api_key: Option<String>,
|
||||
comfyui_url: Option<String>,
|
||||
comfyui_api_key: Option<String>,
|
||||
}
|
||||
|
||||
fn keep_if_not_mask(incoming: Option<String>) -> Option<String> {
|
||||
// 掩码值(以 •••• 开头)表示"未修改",跳过
|
||||
match incoming {
|
||||
Some(s) if s.starts_with(MASK_SENTINEL) => None,
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn put_connection(
|
||||
State(state): State<AppState>,
|
||||
Json(p): Json<UpdatePayload>,
|
||||
) -> AppResult<Json<Value>> {
|
||||
let conn = PixelleConnection {
|
||||
runninghub_api_key: keep_if_not_mask(p.runninghub_api_key),
|
||||
comfyui_url: p.comfyui_url, // url 明文,不掩码
|
||||
comfyui_api_key: keep_if_not_mask(p.comfyui_api_key),
|
||||
};
|
||||
pixelle_connection::save_connection(&state.db, &conn).await?;
|
||||
let backup = pixelle_connection::sync_to_pixelle(&state.config.pixelle_video.config_path, &conn)?;
|
||||
let restart = pixelle_connection::restart_pixelle(&state.config.pixelle_video.compose_dir).await;
|
||||
match restart {
|
||||
Ok(()) => Ok(Json(json!({ "success": true, "backup": backup, "restarted": true }))),
|
||||
Err(e) => Ok(Json(json!({
|
||||
"success": true, "backup": backup, "restarted": false,
|
||||
"warning": format!("配置已写入,但 Pixelle 重启失败,请手动重启: {e}")
|
||||
}))),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: src/routes/admin/mod.rs 声明子模块 + 挂路由**
|
||||
|
||||
模块声明区(`mod users;` 附近)加:
|
||||
```rust
|
||||
mod pixelle_connection;
|
||||
```
|
||||
在 `routes()` 的路由链里(`/api/admin/user-balances` 那一带)加:
|
||||
```rust
|
||||
.route(
|
||||
"/api/admin/pixelle/connection",
|
||||
get(pixelle_connection::get_connection).put(pixelle_connection::put_connection),
|
||||
)
|
||||
```
|
||||
确认 `get`/`put` 已从 axum::routing 引入(文件顶部应已 `use axum::routing::{get, post, ...}`;若缺 put 则补)。
|
||||
|
||||
- [ ] **Step 4: 构建确认**
|
||||
|
||||
Run: `cd /root/metapi && cargo build -j 2 2>&1 | tail -3`
|
||||
Expected: `Finished`。
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
cd /root/metapi && git add src/services/pixelle_connection.rs src/routes/admin/pixelle_connection.rs src/routes/admin/mod.rs
|
||||
git commit -m "feat(pixelle): 连接凭证 admin 端点(GET/PUT 掩码 + 重启)
|
||||
|
||||
Co-Authored-By: Claude <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
<!-- T4 -->
|
||||
|
||||
## Task 4: 前端连接管理卡片
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/web/api.ts`
|
||||
- Modify: `src/web/pages/Accounts.tsx`
|
||||
|
||||
- [ ] **Step 1: api.ts adminApi 加方法**
|
||||
|
||||
在 adminApi 对象里(找 `request(` 范式的 admin 方法群)加:
|
||||
```typescript
|
||||
getPixelleConnection: () =>
|
||||
request<{ success: boolean; connection: { comfyuiUrl?: string | null; runninghubApiKey?: string | null; comfyuiApiKey?: string | null } }>("/api/admin/pixelle/connection"),
|
||||
savePixelleConnection: (data: { runninghubApiKey?: string; comfyuiUrl?: string; comfyuiApiKey?: string }) =>
|
||||
request<{ success: boolean; backup?: string; restarted?: boolean; warning?: string }>("/api/admin/pixelle/connection", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Accounts.tsx 加「Pixelle 连接」卡片**
|
||||
|
||||
先读 `src/web/pages/Accounts.tsx` 顶部,确认 `api`/`adminApi` 的导入名和 toast 用法。在页面主体某处(如页尾)加一个卡片组件块。最小实现(state + 加载 + 保存):
|
||||
```tsx
|
||||
// —— Pixelle 连接(加在组件内 state 区)——
|
||||
const [pxConn, setPxConn] = useState({ comfyuiUrl: '', runninghubApiKey: '', comfyuiApiKey: '' });
|
||||
const [pxSaving, setPxSaving] = useState(false);
|
||||
useEffect(() => {
|
||||
adminApi.getPixelleConnection()
|
||||
.then((r) => setPxConn({
|
||||
comfyuiUrl: r.connection?.comfyuiUrl || '',
|
||||
runninghubApiKey: r.connection?.runninghubApiKey || '',
|
||||
comfyuiApiKey: r.connection?.comfyuiApiKey || '',
|
||||
}))
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
const savePxConn = async () => {
|
||||
setPxSaving(true);
|
||||
try {
|
||||
const res = await adminApi.savePixelleConnection({
|
||||
runninghubApiKey: pxConn.runninghubApiKey || undefined,
|
||||
comfyuiUrl: pxConn.comfyuiUrl || undefined,
|
||||
comfyuiApiKey: pxConn.comfyuiApiKey || undefined,
|
||||
});
|
||||
if (res.warning) toast.error(res.warning);
|
||||
else toast.success('已保存并重启 Pixelle,约数秒生效');
|
||||
} catch (e: any) {
|
||||
toast.error(e?.message || '保存失败');
|
||||
} finally {
|
||||
setPxSaving(false);
|
||||
}
|
||||
};
|
||||
```
|
||||
卡片 JSX(加到 return 的合适位置,样式对齐页面现有 card):
|
||||
```tsx
|
||||
<div className="card" style={{ marginTop: 16, padding: 16 }}>
|
||||
<h3>Pixelle 连接</h3>
|
||||
<label style={{ display: 'block', marginBottom: 8 }}>
|
||||
<span>ComfyUI URL</span>
|
||||
<input value={pxConn.comfyuiUrl} onChange={(e) => setPxConn((s) => ({ ...s, comfyuiUrl: e.target.value }))} placeholder="http://172.17.0.1:4000/comfy-cloud" style={{ width: '100%' }} />
|
||||
</label>
|
||||
<label style={{ display: 'block', marginBottom: 8 }}>
|
||||
<span>RunningHub API Key</span>
|
||||
<input value={pxConn.runninghubApiKey} onChange={(e) => setPxConn((s) => ({ ...s, runninghubApiKey: e.target.value }))} placeholder="留空••••表示不改" style={{ width: '100%' }} />
|
||||
</label>
|
||||
<label style={{ display: 'block', marginBottom: 8 }}>
|
||||
<span>Comfy Cloud API Key</span>
|
||||
<input value={pxConn.comfyuiApiKey} onChange={(e) => setPxConn((s) => ({ ...s, comfyuiApiKey: e.target.value }))} placeholder="留空••••表示不改" style={{ width: '100%' }} />
|
||||
</label>
|
||||
<button type="button" className="btn btn-primary" disabled={pxSaving} onClick={savePxConn}>
|
||||
{pxSaving ? '保存中...' : '保存并重启 Pixelle'}
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
> 注:`useState`/`useEffect` 若未导入需补;`adminApi`/`api` 用 Accounts.tsx 实际的导入名(读文件确认是 `api` 还是 `adminApi`)。若提交掩码值(`••••...`)后端会跳过不改,符合预期。
|
||||
|
||||
- [ ] **Step 3: typecheck**
|
||||
|
||||
Run: `cd /root/metapi && npm run typecheck:web 2>&1 | tail -5`
|
||||
Expected: 无 TS 错误。
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
cd /root/metapi && git add src/web/api.ts src/web/pages/Accounts.tsx
|
||||
git commit -m "feat(pixelle): 连接管理页加 Pixelle 连接卡片
|
||||
|
||||
Co-Authored-By: Claude <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
<!-- T5 -->
|
||||
|
||||
## Task 5: 部署 + 端到端验证
|
||||
|
||||
**Files:** 无(部署 + 运行时)
|
||||
|
||||
- [ ] **Step 1: 重建并重启 BoosAPI(含前后端改动)**
|
||||
|
||||
```bash
|
||||
cd /root/metapi && ./build-and-run.sh --fast-release 2>&1 | tail -15
|
||||
```
|
||||
Expected: 构建成功,BoosAPI 起在 :4000。动线上服务,确认时机。
|
||||
|
||||
- [ ] **Step 2: 连接管理页确认「Pixelle 连接」卡片**
|
||||
|
||||
硬刷新管理控制台连接管理页(`/accounts`),确认出现「Pixelle 连接」卡片,
|
||||
ComfyUI URL 显示当前值,两个 key 显示掩码(`••••xxxx`)。
|
||||
|
||||
- [ ] **Step 3: 改一个值测试同步**
|
||||
|
||||
把 RunningHub API Key 改成一个新值,保存。确认:
|
||||
```bash
|
||||
grep runninghub_api_key /opt/Pixelle-Video/config.yaml | sed 's/[a-f0-9]\{20\}/.../'
|
||||
ls -t /opt/Pixelle-Video/config.yaml.bak-* | head -1
|
||||
rtk proxy docker ps --filter name=pixelle --format '{{.Names}} {{.Status}}'
|
||||
```
|
||||
Expected: config.yaml 的 key 变成新值;有新备份文件;容器重启后 healthy。
|
||||
|
||||
- [ ] **Step 4: 掩码不改测试**
|
||||
|
||||
只改 ComfyUI URL、两个 key 保持掩码原值,保存。确认 config.yaml 里两个 key **没变**
|
||||
(掩码值被后端跳过),URL 变了。
|
||||
|
||||
- [ ] **Step 5: 成功标准**
|
||||
|
||||
UI 改 Pixelle 凭证 → settings 表存值 + config.yaml 同步 + 容器重启,不再需要手动编辑文件。
|
||||
|
||||
---
|
||||
|
||||
## 完成标准
|
||||
|
||||
- 后端:pixelle_connection service(读写 settings + sync_to_pixelle + restart)+ admin GET/PUT 端点,`cargo build -j 2` 通过,service 单测通过。
|
||||
- 前端:连接管理页有「Pixelle 连接」卡片,typecheck 干净。
|
||||
- 端到端:UI 改凭证 → config.yaml 同步 + 容器重启生效;掩码值不误改。
|
||||
- 向后兼容:不改 Pixelle 源码;config.yaml 其余键保留;每次改前备份。
|
||||
|
||||
## 已知风险 / 备注
|
||||
|
||||
- serde_yaml 重序列化会丢 config.yaml 注释(已接受;每次有时间戳备份兜底)。
|
||||
- BoosAPI 进程需有 docker compose 执行权限 + config.yaml 读写权限(同宿主 root 运行,满足)。
|
||||
- Task 5 重启 BoosAPI 动线上服务,确认时机。
|
||||
- 备份文件会累积(config.yaml.bak-*),长期可加清理,本期不做。
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user