///|
/// 操作语法与参数解析:CLI(apply-*-mbt-op-b64)、WASM SDK、Studio 共用的
/// 唯一实现。此前只在 cli 包,WASM 目标(只依赖 core/decl)无法触达。
///
/// 声明转义还原也移到这里:操作参数经 split_op_args 保留转义原文,由
/// unescape_value 统一还原,两半必须成对存在。
///|
/// CLI/engine 边界的公共值还原:把 `\" \\ \, \( \) \n \r \t` 还原为字面字符。
pub fn unescape_value(s : String) -> String {
unescape_decl(s)
}
///|
/// 还原声明转义:\" \\ \, \( \) \n \r \t,以及 json_escape 的 C0 控制符
/// 编码段 \u00xx——decl_escape 经 json_escape 把控制符写成 \u00xx,解码端
/// 必须对称还原,否则控制符文本往返后变成字面 backslash-u 序列。
fn unescape_decl(s : String) -> String {
let sb = StringBuilder::new()
let it = s.iter()
let mut running = true
while running {
match it.next() {
None => running = false
Some('\\') =>
match it.next() {
None => sb.write_char('\\')
Some('"') => sb.write_char('"')
Some('\\') => sb.write_char('\\')
Some(',') => sb.write_char(',')
Some('(') => sb.write_char('(')
Some(')') => sb.write_char(')')
Some('n') => sb.write_char('\n')
Some('r') => sb.write_char('\r')
Some('t') => sb.write_char('\t')
Some('u') =>
match hex4(it) {
Some(code) => sb.write_char(Char::from_int(code))
None => sb.write_string("\\u")
}
Some(other) => {
sb.write_char('\\')
sb.write_char(other)
}
}
Some(c) => sb.write_char(c)
}
}
sb.to_string()
}
///|
/// 从迭代器读 4 个十六进制字符(\uXXXX);任一非法即 None(调用方按原文回填)。
fn hex4(it : Iter[Char]) -> Int? {
let mut code = 0
for _ in 0..<4 {
match it.next() {
None => return None
Some(h) => {
let d = match h {
'0'..='9' => h.to_int() - 48
'a'..='f' => h.to_int() - 87
'A'..='F' => h.to_int() - 55
_ => return None
}
code = code * 16 + d
}
}
}
Some(code)
}
///|
pub fn split_cmd(line : String) -> Array[String] {
// 支持双引号包裹带空格的参数:update ab n text="Hello World"
let out : Array[String] = []
let buf = StringBuilder::new()
let mut in_q = false
let mut esc = false
for c in line.iter() {
if esc {
// 保留转义序列原文(含 \" \, 等),由 k=v 解析端统一还原。
buf.write_char('\\')
buf.write_char(c)
esc = false
continue
}
if c == '\\' && in_q {
esc = true
} else if c == '"' {
in_q = !in_q
} else if (c == ' ' || c == '\t' || c == '\r') && !in_q {
if buf.to_string().length() > 0 {
out.push(buf.to_string())
buf.reset()
}
} else {
buf.write_char(c)
}
}
if buf.to_string().length() > 0 {
out.push(buf.to_string())
}
out
}
///|
/// 查找子串首次出现位置;未找到返回 -1
pub fn index_of_str(s : String, sub : String) -> Int {
let sl = s.length()
let subl = sub.length()
if subl == 0 || subl > sl {
return -1
}
let mut i = 0
while i + subl <= sl {
if s.substring(start=i, end=i + subl) == sub {
return i
}
i = i + 1
}
-1
}
///|
pub fn parse_op_double(s : String) -> Double {
let mut result = 0.0
let mut frac = 0.0
let mut div = 10.0
let mut seen_dot = false
let mut neg = false
for c in s.iter() {
let code = c.to_int()
if code == 45 {
neg = true
} else if code >= 48 && code <= 57 {
if seen_dot {
frac = frac + (code - 48).to_double() / div
div = div * 10.0
} else {
result = result * 10.0 + (code - 48).to_double()
}
} else if code == 46 {
seen_dot = true
}
}
if neg {
-(result + frac)
} else {
result + frac
}
}
///|
/// 对由 MBT 重新构建的临时 Project 执行一个人类或 Agent 操作。
///
/// 这个函数不负责持久化。调用方随后必须经 commit_and_render() 把变更写成
/// canonical MBT,再从 MBT 重建画布,避免 Project/命令日志成为第二事实源。
///|
/// place 的 w/h 位置 token 是否数字(首字符 0-9;"-" 单独出现表示默认)。
fn place_token_is_number(t : String) -> Bool {
t.length() > 0 &&
({
let c0 = t.substring(start=0, end=1)
c0 >= "0" && c0 <= "9"
})
}
///|
/// place 参数解析结果(variant/x/y/w/h 槽 + 尾部 k=v 用户参数槽)。
pub(all) struct PlaceArgs {
variant : String
x : Double
y : Double
w : Double
h : Double
args : Map[String, String]
error : String // 非空 = 解析失败(place_invalid_arg:)
}
///|
/// place 尾部参数解析(issue #18 w/h 槽):CLI(cli_exec 的 place 命令)
/// 与 apply_operation 的 place 臂共用此实现——「双路线解析必须一致」
/// 由单一实现结构性兑现,不再各持副本互相漂移。
/// parts 为 split_cmd 产物,布局:
/// [place, ab, comp, id, variant?, x?, y?, w?, h?, k=v...]。
pub fn parse_place_args(parts : Array[String]) -> PlaceArgs {
let variant = if parts.length() >= 5 && parts[4] != "-" {
parts[4]
} else {
""
}
let x = if parts.length() >= 6 {
parse_op_double(parts[5])
} else {
0.0
}
let y = if parts.length() >= 7 {
parse_op_double(parts[6])
} else {
0.0
}
// 位置参数 w/h(issue #18):place 一步到位指定最终尺寸,
// AgentGate 在最终 bbox 上评估,消除中间态拒绝
let mut w = 0.0
let mut h = 0.0
let mut kv_error = ""
let mut kv_start = 7
if parts.length() >= 8 && parts[7].find("=") is None {
if parts[7] == "-" {
kv_start = 8
} else if place_token_is_number(parts[7]) {
w = parse_op_double(parts[7])
kv_start = 8
if parts.length() >= 9 && parts[8].find("=") is None {
if parts[8] == "-" {
kv_start = 9
} else if place_token_is_number(parts[8]) {
h = parse_op_double(parts[8])
kv_start = 9
} else {
kv_error = "place_invalid_arg:" + parts[8]
}
}
} else {
kv_error = "place_invalid_arg:" + parts[7]
}
}
// 尾部 k=v 对为用户组件参数槽
let kv : Map[String, String] = Map::new()
for i = kv_start; i < parts.length(); i = i + 1 {
if kv_error != "" {
break
}
match parts[i].find("=") {
None =>
if kv_error == "" {
kv_error = "place_invalid_arg:" + parts[i]
}
Some(pos) => {
let k = parts[i].substring(start=0, end=pos)
// k=v 值反转义(与 update 路径一致,兑现 split_cmd 注释协议)
let v = unescape_value(parts[i].substring(start=pos + 1))
if k != "" {
kv.set(k, v)
}
}
}
}
{
variant,
x,
y,
w,
h,
args: kv,
error: kv_error,
}
}
///|
pub fn apply_operation(
project : Project,
operation : String,
) -> Result[Unit, String] {
let parts = split_cmd(operation)
if parts.length() == 0 {
return Err("mbt_operation_empty")
}
match parts[0] {
"move" =>
if parts.length() < 5 {
Err("mbt_operation_usage:move ")
} else {
match
project.move_node(
artboard=parts[1],
node_id=parts[2],
x=parse_op_double(parts[3]),
y=parse_op_double(parts[4]),
) {
Ok(_) => Ok(())
Err(error) => Err(error)
}
}
"update" =>
if parts.length() < 4 {
Err("mbt_operation_usage:update k=v")
} else {
let kvs : Array[(String, String)] = []
let mut bad_token = ""
let mut i = 3
while i < parts.length() {
let kv = parts[i]
let eq = index_of_str(kv, "=")
if eq > 0 {
kvs.push(
(
kv.substring(start=0, end=eq),
unescape_value(kv.substring(start=eq + 1)),
),
)
} else if bad_token == "" {
// 漏写 "=" 的 token 静默跳过 = Agent 收到成功却什么都没改
//(与 restyle 的 no_kv_pairs 防御对称)
bad_token = kv
}
i = i + 1
}
if bad_token != "" {
Err("mbt_operation_invalid_kv:" + bad_token)
} else {
match project.edit_node(artboard=parts[1], node_id=parts[2], kvs~) {
Ok(_) => Ok(())
Err(error) => Err(error)
}
}
}
"delete" =>
if parts.length() < 3 {
Err("mbt_operation_usage:delete ")
} else {
match project.delete_node(artboard=parts[1], node_id=parts[2]) {
Ok(_) => Ok(())
Err(error) => Err(error)
}
}
"copy" =>
if parts.length() < 4 {
Err("mbt_operation_usage:copy [dx] [dy]")
} else {
let dx = if parts.length() >= 5 {
parse_op_double(parts[4])
} else {
24.0
}
let dy = if parts.length() >= 6 {
parse_op_double(parts[5])
} else {
24.0
}
match
project.copy_node(
artboard=parts[1],
node_id=parts[2],
new_id=parts[3],
dx~,
dy~,
) {
Ok(_) => Ok(())
Err(error) => Err(error)
}
}
"duplicate" =>
if parts.length() < 3 {
Err("mbt_operation_usage:duplicate ")
} else {
match project.duplicate_artboard(src=parts[1], new_name=parts[2]) {
Ok(_) => Ok(())
Err(error) => Err(error)
}
}
"delete-artboard" =>
if parts.length() < 2 {
Err("mbt_operation_usage:delete-artboard ")
} else {
match project.delete_artboard(id=parts[1]) {
Ok(_) => Ok(())
Err(error) => Err(error)
}
}
"reorder" =>
if parts.length() < 4 {
Err("mbt_operation_usage:reorder front|back|up|down")
} else {
match
project.reorder_node(
artboard=parts[1],
node_id=parts[2],
order=parts[3],
) {
Ok(_) => Ok(())
Err(error) => Err(error)
}
}
"flip" =>
if parts.length() < 4 {
Err("mbt_operation_usage:flip h|v|both|none")
} else {
match
project.flip_node(artboard=parts[1], node_id=parts[2], axis=parts[3]) {
Ok(_) => Ok(())
Err(error) => Err(error)
}
}
"create" =>
if parts.length() < 2 {
Err("mbt_operation_usage:create [width] [height]")
} else {
let width = if parts.length() >= 3 {
parse_op_double(parts[2])
} else {
390.0
}
let height = if parts.length() >= 4 {
parse_op_double(parts[3])
} else {
844.0
}
let _ = project.create_artboard(name=parts[1], width~, height~)
Ok(())
}
"template" =>
if parts.length() < 3 {
Err("mbt_operation_usage:template [width] [height]")
} else {
let width = if parts.length() >= 4 {
parse_op_double(parts[3])
} else {
0.0
}
let height = if parts.length() >= 5 {
parse_op_double(parts[4])
} else {
0.0
}
match
project.apply_template(
template_id=parts[1],
name=parts[2],
width~,
height~,
) {
Ok(_) => Ok(())
Err(error) => Err(error)
}
}
"place" =>
if parts.length() < 4 {
Err(
"mbt_operation_usage:place [variant|-] [x] [y] [w] [h] [k=v ...]",
)
} else {
// 参数解析走 parse_place_args——与 cli_exec 的 place 命令同一实现
let pa = parse_place_args(parts)
if pa.error != "" {
Err(pa.error)
} else {
match
project.place_component(
artboard=parts[1],
component_id=parts[2],
instance_id=parts[3],
variant=pa.variant,
x=pa.x,
y=pa.y,
w=pa.w,
h=pa.h,
args=pa.args,
) {
Ok(_) => Ok(())
Err(error) => Err(error)
}
}
}
"group" =>
if parts.length() < 5 {
Err(
"mbt_operation_usage:group [n3 ...]",
)
} else {
let ids : Array[String] = []
let mut i = 3
while i < parts.length() {
ids.push(parts[i])
i = i + 1
}
match
project.group_nodes(
artboard=parts[1],
node_ids=ids,
group_id=parts[2],
) {
Ok(_) => Ok(())
Err(error) => Err(error)
}
}
"ungroup" =>
if parts.length() < 3 {
Err("mbt_operation_usage:ungroup ")
} else {
match project.ungroup_node(artboard=parts[1], group_id=parts[2]) {
Ok(_) => Ok(())
Err(error) => Err(error)
}
}
"align" =>
if parts.length() < 5 {
Err("mbt_operation_usage:align [n3 ...]")
} else {
let ids : Array[String] = []
let mut i = 3
while i < parts.length() {
ids.push(parts[i])
i = i + 1
}
match
project.align_nodes(artboard=parts[1], mode=parts[2], node_ids=ids) {
Ok(_) => Ok(())
Err(error) => Err(error)
}
}
"resize-canvas" =>
if parts.length() < 4 {
Err("mbt_operation_usage:resize-canvas ")
} else {
match
project.resize_canvas(
artboard=parts[1],
width=parse_op_double(parts[2]),
height=parse_op_double(parts[3]),
) {
Ok(_) => Ok(())
Err(error) => Err(error)
}
}
"responsive" =>
if parts.length() < 2 {
Err("mbt_operation_usage:responsive ")
} else {
match project.generate_responsive(artboard=parts[1]) {
Ok(_) => Ok(())
Err(error) => Err(error)
}
}
"restyle" =>
if parts.length() < 4 {
Err(
"mbt_operation_usage:restyle k=v [k=v ...]",
)
} else {
let kvs : Array[(String, String)] = []
let mut i = 3
while i < parts.length() {
let eq = index_of_str(parts[i], "=")
if eq > 0 {
kvs.push(
(
parts[i].substring(start=0, end=eq),
unescape_value(parts[i].substring(start=eq + 1)),
),
)
}
i = i + 1
}
if kvs.length() == 0 {
Err("mbt_operation_no_kv_pairs")
} else {
match
project.restyle_component(
artboard=parts[1],
component_id=parts[2],
kvs~,
) {
Ok(_) => Ok(())
Err(error) => Err(error)
}
}
}
"interact" =>
if parts.length() < 5 {
Err(
"mbt_operation_usage:interact ",
)
} else {
match
project.attach_interaction(
artboard=parts[1],
node_id=parts[2],
trigger=parts[3],
action_spec=parts[4],
) {
Ok(_) => Ok(())
Err(error) => Err(error)
}
}
"uninteract" =>
if parts.length() < 3 {
Err("mbt_operation_usage:uninteract ")
} else {
match project.detach_interaction(artboard=parts[1], node_id=parts[2]) {
Ok(_) => Ok(())
Err(error) => Err(error)
}
}
"state" =>
if parts.length() < 5 {
Err(
"mbt_operation_usage:state k=v [k=v ...]",
)
} else {
let mut fill : String? = None
let mut text_color : String? = None
let mut stroke : String? = None
let mut text : String? = None
let mut radius : Double? = None
let mut font_size : Double? = None
// 与 update 臂同防御:漏 "=" 的 token 与未识别 key 显式报错,
// 不静默吞掉(Agent 否则收到成功却没定义任何字段)
let mut bad_token = ""
let mut unknown_key = ""
let mut i = 4
while i < parts.length() {
let eq = index_of_str(parts[i], "=")
if eq <= 0 {
if bad_token == "" {
bad_token = parts[i]
}
} else {
let k = parts[i].substring(start=0, end=eq)
let v = unescape_value(parts[i].substring(start=eq + 1))
let d = parse_op_double(v)
match k {
"fill" => fill = Some(v)
"text_color" => text_color = Some(v)
"stroke" => stroke = Some(v)
"text" => text = Some(v)
"radius" => radius = Some(d)
"font_size" => font_size = Some(d)
_ => if unknown_key == "" { unknown_key = k }
}
}
i = i + 1
}
if bad_token != "" {
Err("mbt_operation_invalid_kv:" + bad_token)
} else if unknown_key != "" {
Err("mbt_operation_unknown_key:" + unknown_key)
} else {
let patch : NodePatch = {
..NodePatch::empty(),
fill,
text_color,
stroke,
text,
corner_radius: radius,
font_size,
}
match
project.define_component_state(
artboard=parts[1],
node_id=parts[2],
state_name=parts[3],
patch~,
) {
Ok(_) => Ok(())
Err(error) => Err(error)
}
}
}
"set-state" =>
if parts.length() < 3 {
Err("mbt_operation_usage:set-state [toggle]")
} else {
let toggle = parts.length() >= 4 && parts[3] == "toggle"
match
project.set_node_state(node_id=parts[1], state_name=parts[2], toggle~) {
Ok(_) => Ok(())
Err(error) => Err(error)
}
}
"flow" =>
if parts.length() < 4 {
Err("mbt_operation_usage:flow ")
} else {
project.add_flow(from=parts[1], to=parts[2], trigger="tap:" + parts[3])
project.revision = project.revision + 1
Ok(())
}
"unflow" =>
if parts.length() < 4 {
Err("mbt_operation_usage:unflow ")
} else {
match
project.remove_flow(
from=parts[1],
to=parts[2],
trigger="tap:" + parts[3],
) {
Ok(_) => {
project.revision = project.revision + 1
Ok(())
}
Err(error) => Err(error)
}
}
"theme" =>
if parts.length() < 2 {
Err("mbt_operation_usage:theme ")
} else {
match project.apply_theme(parts[1]) {
Ok(_) => Ok(())
Err(error) => Err(error)
}
}
"token" =>
if parts.length() < 3 {
Err("mbt_operation_usage:token ")
} else {
match project.set_token(parts[1], parts[2]) {
Ok(_) => Ok(())
Err(error) => Err(error)
}
}
"autofill" =>
if parts.length() < 2 {
Err("mbt_operation_usage:autofill ")
} else {
match project.autofill_missing(artboard=parts[1]) {
Ok(_) => Ok(())
Err(error) => Err(error)
}
}
"fix" =>
if parts.length() < 2 {
Err("mbt_operation_usage:fix ")
} else {
match project.auto_fix(artboard=parts[1]) {
Ok(_) => Ok(())
Err(error) => Err(error)
}
}
_ => Err("mbt_operation_unsupported:" + parts[0])
}
}
///|
/// 批量执行上限:防滥用/防失控循环;64 条足够覆盖一次精修轮。
pub const OPS_BATCH_LIMIT : Int = 64
///|
/// 批量执行 mutating ops(生成式执行策略第二级:确定的精修一次提交,
/// 替代 N 次单调用往返)。顺序执行、遇错即停——与逐条执行语义完全一致
/// (错误处之后的 op 不落,索引随错误返回供断点续批)。
pub fn apply_operations(
project : Project,
operations : Array[String],
) -> Result[Int, (Int, String)] {
if operations.length() > OPS_BATCH_LIMIT {
return Err((-1, "mbt_ops_batch_too_many:\{OPS_BATCH_LIMIT}"))
}
let mut i = 0
while i < operations.length() {
match apply_operation(project, operations[i]) {
Err(error) => return Err((i, error))
Ok(_) => i = i + 1
}
}
Ok(operations.length())
}