Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions MiniApp/Skills/miniapp-dev/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -422,3 +422,17 @@ BuiltinApp {

- 重构计划: `.cursor/plans/miniapp_v2_full_refactor_*.plan.md`
- 架构说明见 plan 内「MiniApp V2 一步到位重构计划」

## 发布到市场

用户明确要求把 MiniApp 发布/上架到市场时,用 `PublishMiniApp` 工具:

- 传 `app_id` 和 1–5 张截图路径(PNG/JPEG/WebP,单张 ≤ 5 MiB)。
没有截图时先向用户要,或请用户在「市场 → 我的投稿」用「截取当前画面」生成。
- 名称、描述、图标、分类、标签自动取自 `meta.json`;slug 和版本号自动推导。
发布前确认 `meta.json` 的 `description` 非空、权限是最小集
(市场会拒绝 `node.enabled=true`、宽泛 fs scope 等)。
- 未登录时工具会返回 GitHub 授权链接:把链接给用户,等用户完成授权后
用相同参数再调用一次即可继续。
- 提交后进入人工审核;用户可在「市场 → 我的投稿」查看状态。
- 这是对外动作:只在用户明确要求发布时调用,不要主动发布。
90 changes: 13 additions & 77 deletions src/apps/desktop/src/api/miniapp_market_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use bitfun_product_domains::miniapp::market::{
MarketSubmission, MarketSubmissionDraftRequest,
};
use bitfun_services_integrations::miniapp_market::{
build_market_package, validate_market_package, DesktopAuthPollRequest, DesktopAuthPollResponse,
submit_installed_app, validate_market_package, DesktopAuthPollRequest, DesktopAuthPollResponse,
DesktopAuthStart, FavoriteAggregate, MarketBrowseRequest, MarketClient, MarketMe,
RatingAggregate, ValidatedMarketPackage,
};
Expand Down Expand Up @@ -517,63 +517,26 @@ pub async fn miniapp_market_submit_installed(
state: State<'_, AppState>,
request: MarketSubmitInstalledRequest,
) -> Result<MarketSubmission, String> {
if request.screenshot_paths.is_empty() || request.screenshot_paths.len() > 5 {
return Err("Choose between 1 and 5 screenshots.".to_string());
}
let installed = state
.miniapp_manager
.get(&request.app_id)
.await
.map_err(|error| error.to_string())?;
// The reviewed listing metadata is part of the immutable submission
// snapshot. Build the package from the selected app's source and
// permissions, but use the values the author entered in the submission
// form so the package and review record cannot disagree.
let mut package_app = installed.clone();
package_app.name = request.draft.name.clone();
package_app.description = request.draft.description.clone();
package_app.icon = request.draft.icon.clone();
package_app.category = request.draft.category.clone();
package_app.tags = request.draft.tags.clone();
let package = build_market_package(&package_app).map_err(|error| error.to_string())?;
emit_upload_progress(&app, None, "validating", 1, 1);

let mut client = MarketClient::from_environment()
.await
.map_err(market_error)?;
let submission = client
.create_submission(&request.draft)
.await
.map_err(market_error)?;
let submission_id = submission.submission_id.clone();
emit_upload_progress(&app, Some(&submission_id), "package", 0, 1);
client
.upload_submission_package(&submission_id, package)
.await
.map_err(market_error)?;
emit_upload_progress(&app, Some(&submission_id), "package", 1, 1);

let screenshot_total = request.screenshot_paths.len() as u32;
for (position, path) in request.screenshot_paths.iter().enumerate() {
let path = PathBuf::from(path);
let (media_type, bytes) = read_screenshot(&path).await?;
client
.upload_submission_screenshot(&submission_id, position as u32, media_type, bytes)
.await
.map_err(market_error)?;
emit_upload_progress(
&app,
Some(&submission_id),
"screenshots",
position as u32 + 1,
screenshot_total,
);
}
let submission = client
.submit_submission(&submission_id)
.await
.map_err(market_error)?;
emit_upload_progress(&app, Some(&submission_id), "submitted", 1, 1);
let mut progress = |submission_id: Option<&str>, phase: &'static str, completed, total| {
emit_upload_progress(&app, submission_id, phase, completed, total);
};
let submission = submit_installed_app(
&mut client,
&installed,
&request.draft,
&request.screenshot_paths,
&mut progress,
)
.await
.map_err(market_error)?;
remove_owned_capture_files(&app, &request.screenshot_paths).await;
Ok(submission)
}
Expand Down Expand Up @@ -697,33 +660,6 @@ fn validate_minimum_bitfun_version(minimum: &str) -> Result<(), String> {
Ok(())
}

async fn read_screenshot(path: &Path) -> Result<(&'static str, Vec<u8>), String> {
let metadata = tokio::fs::metadata(path)
.await
.map_err(|error| format!("Could not read screenshot metadata: {error}"))?;
if !metadata.is_file()
|| metadata.len() > bitfun_product_domains::miniapp::market::MARKET_MAX_SCREENSHOT_BYTES
{
return Err("Each screenshot must be a file no larger than 5 MiB.".to_string());
}
let media_type = match path
.extension()
.and_then(|extension| extension.to_str())
.unwrap_or_default()
.to_ascii_lowercase()
.as_str()
{
"png" => "image/png",
"jpg" | "jpeg" => "image/jpeg",
"webp" => "image/webp",
_ => return Err("Screenshots must be PNG, JPEG, or WebP.".to_string()),
};
let bytes = tokio::fs::read(path)
.await
.map_err(|error| format!("Could not read screenshot: {error}"))?;
Ok((media_type, bytes))
}

fn emit_upload_progress(
app: &AppHandle,
submission_id: Option<&str>,
Expand Down
1 change: 1 addition & 0 deletions src/crates/assembly/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ product-domains = [
"plugin-source",
"bitfun-services-integrations/function-agents",
"bitfun-services-integrations/miniapp-runtime",
"bitfun-services-integrations/miniapp-market",
"bitfun-product-domains/product-full",
]
canvas-runtime = [
Expand Down
14 changes: 14 additions & 0 deletions src/crates/assembly/core/builtin_skills/miniapp-dev/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,3 +249,17 @@ await app.fs.readFile(...)
- i18n 至少覆盖 `zh-CN` / `en-US`
- light/dark 没有明显样式问题
- 没有遗留 “TODO / 占位 / Lorem ipsum”

## 发布到市场

用户明确要求把 MiniApp 发布/上架到市场时,用 `PublishMiniApp` 工具:

- 传 `app_id` 和 1–5 张截图路径(PNG/JPEG/WebP,单张 ≤ 5 MiB)。
没有截图时先向用户要,或请用户在「市场 → 我的投稿」用「截取当前画面」生成。
- 名称、描述、图标、分类、标签自动取自 `meta.json`;slug 和版本号自动推导。
发布前确认 `meta.json` 的 `description` 非空、权限是最小集
(市场会拒绝 `node.enabled=true`、宽泛 fs scope 等)。
- 未登录时工具会返回 GitHub 授权链接:把链接给用户,等用户完成授权后
用相同参数再调用一次即可继续。
- 提交后进入人工审核;用户可在「市场 → 我的投稿」查看状态。
- 这是对外动作:只在用户明确要求发布时调用,不要主动发布。
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ impl ClawMode {
// agent/tool instead of being surfaced as a ControlHub domain.
"ControlHub".to_string(),
"InitMiniApp".to_string(),
"PublishMiniApp".to_string(),
"PageDeploy".to_string(),
"PagePublish".to_string(),
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ impl CoworkMode {
"WebFetch".to_string(),
"ControlHub".to_string(),
"InitMiniApp".to_string(),
"PublishMiniApp".to_string(),
],
}
}
Expand Down
1 change: 1 addition & 0 deletions src/crates/assembly/core/src/agentic/agents/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ pub fn shared_coding_mode_tools() -> Vec<String> {
"ReviewPlatform".to_string(),
"ControlHub".to_string(),
"InitMiniApp".to_string(),
"PublishMiniApp".to_string(),
"PageDeploy".to_string(),
"PagePublish".to_string(),
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ Notes:
| `GetMCPPrompt` | Deferred | None | - |
| `GenerativeUI` | Deferred | None | - |
| `Git` | Deferred | `ReviewFixer`, `ReviewWorker`, `ReviewJudge` | Direct |
| `InitMiniApp` | Deferred | None | - |
| `InitMiniApp` | Direct | None | - |
| `PublishMiniApp` | Direct | None | - |
| `ControlHub` | Deferred | `ComputerUse` | Direct |
| `ComputerUse` | Deferred | `ComputerUse` | Direct |
| `Playbook` | Deferred | None | - |
Expand Down
Loading