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
47 changes: 42 additions & 5 deletions src/crates/assembly/core/builtin_skills/miniapp-dev/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,41 @@ description: 'Generate and refine BitFun MiniApps. Use when the user wants a new
- 需要 npm 依赖
- 需要较长链路或较复杂的后台逻辑

### 4. 用 `InitMiniApp` 创建骨架
### 4. 选择正确的编辑流程

先判断当前任务属于哪一种,不要混用:

#### 新建 MiniApp

1. 调用 `InitMiniApp`,保存返回的 `app_id` 和根目录。
2. 只在返回的根目录中编辑 `source/index.html`、`source/style.css`、
`source/ui.js`、按需编辑 `source/worker.js`,以及确有必要的 `meta.json`
产品字段。
3. 编辑完成后必须调用 `FinalizeMiniApp`,传入刚才的 `app_id`。
4. 只有 `FinalizeMiniApp` 成功后才算交付完成。

#### 更新已有 MiniApp

1. 复用已有应用的 `app_id` 和根目录,不要再次调用 `InitMiniApp` 创建副本。
2. 在已有根目录中完成修改。
3. 每一批文件修改完成后必须调用一次 `FinalizeMiniApp`。
4. 如果预期有修改但返回 `changed: false`,检查文件是否写进了正确的应用根目录;
不要靠手动增加版本号掩盖问题。

`FinalizeMiniApp` 会重新从磁盘读取源码、编译 `compiled.html`、持久化内容修订,
并通知已经打开的 MiniApp 刷新。版本号由它管理;不要手改 `version`、
`created_at`、`updated_at`、`runtime` 或 `compiled.html`。

#### 定制草稿

如果当前提示明确给出了 `Draft root` / 草稿目录:

- 只编辑草稿目录
- 不调用 `InitMiniApp`
- 不调用 `FinalizeMiniApp`
- 由定制面板负责“刷新草稿预览”和“应用草稿”

### 5. 用 `InitMiniApp` 创建骨架

创建后,围绕这些文件工作:

Expand All @@ -115,7 +149,7 @@ description: 'Generate and refine BitFun MiniApps. Use when the user wants a new
- `ui.js` 负责状态、渲染、事件、i18n
- `worker.js` 只承载真正需要后台执行的逻辑

### 5. 只使用真实存在的宿主能力
### 6. 只使用真实存在的宿主能力

MiniApp 里可用的是 `window.app`。

Expand All @@ -140,7 +174,7 @@ MiniApp 里可用的是 `window.app`。

- [`api-reference.md`](api-reference.md)

### 6. 不要假设这些 API 存在
### 7. 不要假设这些 API 存在

默认**不要**写这些不存在的接口:

Expand All @@ -163,7 +197,7 @@ await app.shell.exec('git ...', { cwd: app.workspaceDir })
await app.fs.readFile(...)
```

### 7. 从第一版就带上 i18n 和 theme
### 8. 从第一版就带上 i18n 和 theme

不要把多语言和主题适配留到最后。

Expand All @@ -175,7 +209,7 @@ await app.fs.readFile(...)
- 样式优先使用 `--bitfun-*`
- 测试 light/dark + zh/en

### 8. 先做核心体验,不补假内容
### 9. 先做核心体验,不补假内容

如果缺素材、图标、真实数据:

Expand Down Expand Up @@ -249,6 +283,9 @@ await app.fs.readFile(...)
- i18n 至少覆盖 `zh-CN` / `en-US`
- light/dark 没有明显样式问题
- 没有遗留 “TODO / 占位 / Lorem ipsum”
- 普通新建/更新流程已成功调用 `FinalizeMiniApp`
- 预期有改动时 `FinalizeMiniApp` 返回 `changed: true`
- 没有手动修改生命周期字段或 `compiled.html`

## 发布到市场

Expand Down
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(),
"FinalizeMiniApp".to_string(),
"PublishMiniApp".to_string(),
"PageDeploy".to_string(),
"PagePublish".to_string(),
Expand Down Expand Up @@ -96,9 +97,10 @@ mod tests {
use bitfun_agent_runtime::prompt::UserContextSection;

#[test]
fn claw_mode_includes_init_miniapp_in_default_tools() {
fn claw_mode_includes_miniapp_lifecycle_tools_in_defaults() {
let tools = ClawMode::new().default_tools();
assert!(tools.contains(&"InitMiniApp".to_string()));
assert!(tools.contains(&"FinalizeMiniApp".to_string()));
assert!(tools.contains(&"ListModels".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(),
"FinalizeMiniApp".to_string(),
"PublishMiniApp".to_string(),
],
}
Expand Down Expand Up @@ -108,9 +109,10 @@ mod tests {
use crate::agentic::agents::Agent;

#[test]
fn cowork_mode_includes_init_miniapp_in_default_tools() {
fn cowork_mode_includes_miniapp_lifecycle_tools_in_defaults() {
let tools = CoworkMode::new().default_tools();
assert!(tools.contains(&"InitMiniApp".to_string()));
assert!(tools.contains(&"FinalizeMiniApp".to_string()));
assert!(tools.contains(&"ListModels".to_string()));
}
}
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(),
"FinalizeMiniApp".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 @@ -44,6 +44,7 @@ Notes:
| `GenerativeUI` | Deferred | None | - |
| `Git` | Deferred | `ReviewFixer`, `ReviewWorker`, `ReviewJudge` | Direct |
| `InitMiniApp` | Direct | None | - |
| `FinalizeMiniApp` | Direct | None | - |
| `PublishMiniApp` | Direct | None | - |
| `ControlHub` | Deferred | `ComputerUse` | Direct |
| `ComputerUse` | Deferred | `ComputerUse` | Direct |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
//! FinalizeMiniApp tool — compile direct MiniApp file edits and publish the
//! resulting runtime revision to every open product surface.

use crate::agentic::tools::framework::{PermissionIntent, Tool, ToolResult, ToolUseContext};
use crate::infrastructure::events::{emit_global_event, BackendEvent};
use crate::miniapp::lifecycle::miniapp_runtime_event_payload;
use crate::miniapp::try_get_global_miniapp_manager;
use crate::util::errors::{BitFunError, BitFunResult};
use async_trait::async_trait;
use serde_json::{json, Value};

pub struct FinalizeMiniAppTool;

impl FinalizeMiniAppTool {
pub fn new() -> Self {
Self
}
}

impl Default for FinalizeMiniAppTool {
fn default() -> Self {
Self::new()
}
}

#[async_trait]
impl Tool for FinalizeMiniAppTool {
fn name(&self) -> &str {
"FinalizeMiniApp"
}

async fn description(&self) -> BitFunResult<String> {
Ok(r#"Finalize source files edited for an installed BitFun MiniApp.

Call this after every successful Read/Write/Edit pass under a MiniApp root returned by InitMiniApp, and after modifying an existing MiniApp. It:
- reloads source files from disk;
- recompiles and persists compiled.html;
- increments the MiniApp version only when user-controlled content changed;
- emits runtime update events so an already-open MiniApp reloads.

Input: app_id and optional theme ('dark' or 'light').
Do not call this for a customization draft root; the customization host owns draft sync and apply.
Do not edit meta.json version fields manually; this tool owns version transitions.
Returns app_id, version, changed, content_hash, and source_revision."#
.to_string())
}

fn short_description(&self) -> String {
"Finalize MiniApp file edits and refresh open runtimes.".to_string()
}

fn input_schema(&self) -> Value {
json!({
"type": "object",
"additionalProperties": false,
"required": ["app_id"],
"properties": {
"app_id": {
"type": "string",
"description": "Installed MiniApp id returned by InitMiniApp."
},
"theme": {
"type": "string",
"enum": ["dark", "light"],
"description": "Theme used for the persisted compiled preview. Defaults to dark."
}
}
})
}

fn is_readonly(&self) -> bool {
false
}

fn permission_intents(
&self,
input: &Value,
_context: &ToolUseContext,
) -> BitFunResult<Vec<PermissionIntent>> {
let app_id = input
.get("app_id")
.and_then(Value::as_str)
.unwrap_or("<missing>")
.trim();
Ok(vec![PermissionIntent::new(
"custom_tool",
vec![format!("miniapp:FinalizeMiniApp:{app_id}")],
)])
}

async fn call_impl(
&self,
input: &Value,
context: &ToolUseContext,
) -> BitFunResult<Vec<ToolResult>> {
let manager = try_get_global_miniapp_manager()
.ok_or_else(|| BitFunError::tool("MiniAppManager not initialized".to_string()))?;
let app_id = input
.get("app_id")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.ok_or_else(|| BitFunError::validation("Missing required field: app_id"))?;
let theme = input.get("theme").and_then(Value::as_str).unwrap_or("dark");

let previous = manager
.get_meta(app_id)
.await
.map_err(|error| BitFunError::tool(format!("Failed to load MiniApp: {error}")))?;
let app = manager
.sync_from_fs(app_id, theme, context.workspace_root())
.await
.map_err(|error| BitFunError::tool(format!("Failed to finalize MiniApp: {error}")))?;
let changed = app.version != previous.version;
let reason = if changed {
"agent-finalize"
} else {
"agent-finalize-noop"
};

for event_name in ["miniapp-recompiled", "miniapp-updated"] {
let _ = emit_global_event(BackendEvent::Custom {
event_name: event_name.to_string(),
payload: miniapp_runtime_event_payload(&app, reason),
})
.await;
}

let result_text = if changed {
format!(
"MiniApp '{}' finalized at version {}. Open runtimes were notified to reload.",
app.name, app.version
)
} else {
format!(
"MiniApp '{}' was recompiled with no content change; version remains {}. Open runtimes were notified to reload.",
app.name, app.version
)
};

Ok(vec![ToolResult::Result {
data: json!({
"app_id": app.id,
"version": app.version,
"changed": changed,
"content_hash": app.runtime.content_hash,
"source_revision": app.runtime.source_revision,
}),
result_for_assistant: Some(result_text),
image_attachments: None,
}])
}
}

#[cfg(test)]
mod tests {
use super::FinalizeMiniAppTool;
use crate::agentic::tools::framework::{Tool, ToolExposure, ToolUseContext};
use serde_json::json;

#[test]
fn finalize_miniapp_stays_expanded_for_assistant_updates() {
let tool = FinalizeMiniAppTool::new();
assert_eq!(tool.default_exposure(), ToolExposure::Direct);
}

#[test]
fn finalize_miniapp_emits_stable_permission_identity() {
let tool = FinalizeMiniAppTool::new();
let context = ToolUseContext::for_tool_listing(None, None);
let intents = tool
.permission_intents(&json!({ "app_id": "demo-app" }), &context)
.expect("permission intent");

assert_eq!(intents.len(), 1);
assert_eq!(intents[0].action, "custom_tool");
assert_eq!(
intents[0].resources,
["miniapp:FinalizeMiniApp:demo-app".to_string()]
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ Input: name, description, icon, category. The tool creates the app directory and
- manifest (meta.json), source/index.html, source/style.css, source/ui.js, source/worker.js,
package.json, storage.json.

Returns app_id and the app root directory. Use the root directory and file names above with Read/Write/Edit to implement the app. The MiniApp uses window.app (app.fs, app.call, app.dialog, etc.) — see miniapp-dev skill for API reference."#
Returns app_id and the app root directory. Use the root directory and file names above with Read/Write/Edit to implement the app, then call FinalizeMiniApp with the returned app_id. FinalizeMiniApp recompiles the edited files, persists the new content revision, and refreshes already-open runtimes. The MiniApp uses window.app (app.fs, app.call, app.dialog, etc.) — see miniapp-dev skill for API reference."#
.to_string())
}

Expand Down Expand Up @@ -214,7 +214,7 @@ Returns app_id and the app root directory. Use the root directory and file names
.await;

let result_text = format!(
"MiniApp '{}' skeleton created. app_id: {}. Root directory: {}. Use Read/Write/Edit tools with files under this root, then open in Toolbox to run.",
"MiniApp '{}' skeleton created. app_id: {}. Root directory: {}. Use Read/Write/Edit tools with files under this root, then call FinalizeMiniApp with this app_id before opening it.",
app.name, app.id, app_dir_str
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ pub mod grep_tool;
pub mod list_models_tool;
pub mod ls_tool;
pub mod mcp_tools;
pub mod miniapp_finalize_tool;
pub mod miniapp_init_tool;
pub mod miniapp_publish_tool;
pub mod page_deploy_tool;
Expand Down Expand Up @@ -77,6 +78,7 @@ pub use ls_tool::LSTool;
pub use mcp_tools::{
GetMCPPromptTool, ListMCPPromptsTool, ListMCPResourcesTool, ReadMCPResourceTool,
};
pub use miniapp_finalize_tool::FinalizeMiniAppTool;
pub use miniapp_init_tool::InitMiniAppTool;
pub use miniapp_publish_tool::PublishMiniAppTool;
pub use page_deploy_tool::PageDeployTool;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ impl StaticToolProviderFactory<dyn Tool> for ProductConcreteToolFactory {
"Worktree" => Some(Arc::new(WorktreeTool::new())),
"ReviewPlatform" => Some(Arc::new(ReviewPlatformTool::new())),
"InitMiniApp" => Some(Arc::new(InitMiniAppTool::new())),
"FinalizeMiniApp" => Some(Arc::new(FinalizeMiniAppTool::new())),
"PublishMiniApp" => Some(Arc::new(PublishMiniAppTool::new())),
"PageDeploy" => Some(Arc::new(PageDeployTool::new())),
"PagePublish" => Some(Arc::new(PagePublishTool::new())),
Expand Down
2 changes: 2 additions & 0 deletions src/crates/assembly/core/src/agentic/tools/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,7 @@ mod tests {
"Worktree",
"ReviewPlatform",
"InitMiniApp",
"FinalizeMiniApp",
"PublishMiniApp",
"PageDeploy",
"PagePublish",
Expand Down Expand Up @@ -700,6 +701,7 @@ mod tests {
assert!(registry.is_tool_deferred("Worktree"));
assert!(registry.is_tool_deferred("ReviewPlatform"));
assert!(!registry.is_tool_deferred("InitMiniApp"));
assert!(!registry.is_tool_deferred("FinalizeMiniApp"));
assert!(!registry.is_tool_deferred("PublishMiniApp"));
}

Expand Down
Loading