Bladeren bron

docs: refresh bundled metadata and workflow specs

mzp 3 maanden geleden
bovenliggende
commit
b722ed9518

+ 26 - 3
Theme/Theme.vth

@@ -35,7 +35,7 @@ intent.success.intentSubtleBg:#ECFDF5
 intent.warning.intentFg:#F59E0B
 intent.warning.intentBg:#F59E0B
 intent.warning.intentBorder:#F59E0B
-intent.warning.intentOnBg:#0F172A
+intent.warning.intentOnBg:#FFFFFF
 intent.warning.intentFocusRing:0 0 0 3px rgba(245,158,11,0.24)
 intent.warning.intentSubtleBg:#FEF3C7
 
@@ -243,6 +243,31 @@ textRole.contrast.textRoleBgImage:none
 textRole.contrast.textRoleBgClip:border-box
 textRole.contrast.textRoleFillColor:none
 
+// size (all 3 points x 7 slots)
+size.sm.sizePaddingY:4px
+size.sm.sizePaddingX:12px
+size.sm.sizeFontSize:13px
+size.sm.sizeFontWeight:500
+size.sm.sizeLineHeight:1.4
+size.sm.sizeMinHeight:32px
+size.sm.sizeGap:6px
+
+size.md.sizePaddingY:8px
+size.md.sizePaddingX:16px
+size.md.sizeFontSize:14px
+size.md.sizeFontWeight:500
+size.md.sizeLineHeight:1.5
+size.md.sizeMinHeight:40px
+size.md.sizeGap:8px
+
+size.lg.sizePaddingY:12px
+size.lg.sizePaddingX:24px
+size.lg.sizeFontSize:16px
+size.lg.sizeFontWeight:500
+size.lg.sizeLineHeight:1.5
+size.lg.sizeMinHeight:48px
+size.lg.sizeGap:10px
+
 // state (all 6 points x 7 slots)
 state.rest.stateOverlay:none
 state.rest.stateShadow:none
@@ -292,6 +317,4 @@ state.invalid.stateTransitionDuration:120ms
 state.invalid.stateTransitionTiming:ease-out
 state.invalid.stateTransitionProperty:box-shadow, border-color, background-color
 
-# Overrides
-
 </Theme-Enterprise-6.6>

+ 376 - 0
docs/vl-metadata-spec-3.1.md

@@ -0,0 +1,376 @@
+# VL Metadata Spec 3.1
+
+> Status: canonical metadata schema for VLCode-Lite and DocCenter Path `4`
+>
+> Scope:
+> - Defines the canonical `ProjectMeta` JSON consumed by metadata diff, workflow regeneration, project context, and DocCenter-backed tooling
+> - Normalizes legacy extractor output into one stable shape
+> - Aligns Theme metadata with VL 3.7 / THEME 6.6
+> - Clarifies that DocCenter / workflow document bindings live in project config as `Doc ID`, not inside `ProjectMeta`
+
+## 1. Purpose
+
+`ProjectMeta` is the machine-readable project model for a VL workspace.
+
+It exists to support:
+
+- deterministic workflow regeneration
+- project-wide diff and impact analysis
+- DocCenter-backed prompt context
+- IDE visualization and debug tooling
+- stable references across `Apps/`, `Sections/`, `ExtComponents/`, `Services/`, `Database/`, and `Theme/`
+
+This spec defines the canonical schema. Legacy field names may still be accepted by importers, but they are not canonical output.
+
+## 2. Canonical Rules
+
+### 2.1 Root schema
+
+Canonical root object:
+
+```json
+{
+  "$schema": "VL-ProjectMeta/3.0",
+  "projectName": "SmartCampus",
+  "projectDescription": "optional",
+  "vlVersion": "3.7",
+  "config": {
+    "deviceTarget": "web",
+    "screenResolution": "1440x900"
+  },
+  "fileManifest": [],
+  "valueDomains": {},
+  "apps": [],
+  "sections": [],
+  "components": [],
+  "services": [],
+  "dataSchema": {
+    "tables": [],
+    "relations": []
+  },
+  "theme": null,
+  "dependencyGraph": null
+}
+```
+
+### 2.2 Identifier policy
+
+- `apps[*].id` is the canonical app identifier
+- `sections[*].id` is the canonical section identifier
+- `components[*].id` is the canonical component identifier
+- `services[*].domainId` is the canonical service-domain identifier
+- `services[*].methods[*].id` is the canonical method identifier
+- `services[*].methods[*].serviceId` is the canonical fully-qualified service identifier, normally `domainId.methodId`
+- `dataSchema.tables[*].id` is the canonical table identifier
+- `theme.id` is the canonical theme identifier
+
+Legacy aliases such as `appId`, `sectionId`, `componentId`, `tableName`, `serviceDomains`, `servicesUsed`, and `componentRefs` are compatibility inputs only.
+
+### 2.3 File path policy
+
+Canonical default file locations:
+
+- app: `Apps/<id>.vx`
+- section: `Sections/<id>.sc`
+- component: `ExtComponents/<id>.cp`
+- service: `Services/<domainId>.vs`
+- database: `Database/<projectName>.vdb` or explicit per-table/file-level mapping when available
+- theme: `Theme/Theme.vth`
+
+If `filePath` is omitted during normalization, tooling may synthesize the conventional path above.
+
+### 2.4 Doc binding exclusion
+
+`ProjectMeta` must not inline DocCenter binding configuration for core specs or workflow prompts.
+
+The following values belong to IDE / project profile configuration, not metadata schema:
+
+- `coreDocIds`
+- `docIdOverrides`
+- any UI-level `Doc Ref` or viewer URL such as `vl://doc/16` or `/doc-center.html?docId=16`
+
+Reason:
+
+- `ProjectMeta` models the project itself
+- doc bindings model external tooling configuration
+- keeping them separate prevents schema drift when official docs are republished or remapped
+
+## 3. Entity Schemas
+
+### 3.1 App
+
+```json
+{
+  "id": "AdminApp",
+  "filePath": "Apps/AdminApp.vx",
+  "vlVersion": "3.7",
+  "device": "web",
+  "resolution": "1440x900",
+  "description": "",
+  "globalVars": [],
+  "pages": [
+    {
+      "id": "Dashboard",
+      "route": "/dashboard",
+      "sections": ["DashboardMain"],
+      "sectionRefs": [
+        {
+          "sectionId": "DashboardMain",
+          "instanceId": "mainSection",
+          "layoutProps": {}
+        }
+      ],
+      "componentRefs": [],
+      "layout": []
+    }
+  ],
+  "routeMap": {
+    "/dashboard": "DashboardMain"
+  },
+  "homeRoute": "/dashboard",
+  "wiring": [],
+  "navSectionInstanceId": null,
+  "navEventName": null
+}
+```
+
+Rules:
+
+- `pages[*].id` must be stable inside the app
+- `pages[*].sections` contains section IDs only
+- `pages[*].sectionRefs[*].sectionId` must resolve to an existing section
+- `routeMap` is a convenience index, not a second source of truth
+
+### 3.2 Section
+
+```json
+{
+  "id": "DashboardMain",
+  "filePath": "Sections/DashboardMain.sc",
+  "vlVersion": "3.7",
+  "previewSize": null,
+  "publicProps": [],
+  "publicEvents": [],
+  "publicMethods": [],
+  "globalVars": [],
+  "derivedVars": [],
+  "consumesServices": [
+    "CampusService.getOverview",
+    "CampusService.listAlerts"
+  ],
+  "usesComponents": [
+    "StatCard",
+    "AlertTable"
+  ],
+  "interactiveElements": [],
+  "internalMethods": [],
+  "pipeFuncs": [],
+  "isNavSection": false,
+  "navMenuItems": [],
+  "navItemInstanceId": null,
+  "keyStates": {},
+  "description": ""
+}
+```
+
+Rules:
+
+- `consumesServices` is an array of service ID strings, not nested service objects
+- `usesComponents` is an array of component ID strings, not component ref objects
+- `consumesServices[*]` must resolve to an existing `services[*].methods[*].serviceId`
+- `usesComponents[*]` must resolve to an existing `components[*].id`
+
+### 3.3 Component
+
+```json
+{
+  "id": "StatCard",
+  "filePath": "ExtComponents/StatCard.cp",
+  "vlVersion": "3.7",
+  "previewSize": null,
+  "publicProps": [],
+  "publicEvents": [],
+  "derivedVars": [],
+  "interactiveElements": [],
+  "internalMethods": [],
+  "pipeFuncs": [],
+  "description": ""
+}
+```
+
+### 3.4 Service domain
+
+```json
+{
+  "domainId": "CampusService",
+  "filePath": "Services/CampusService.vs",
+  "vlVersion": "3.7",
+  "envVars": [],
+  "methods": [
+    {
+      "id": "getOverview",
+      "serviceId": "CampusService.getOverview",
+      "type": null,
+      "params": [],
+      "returns": {},
+      "expose": null,
+      "sig": null
+    }
+  ],
+  "virtualTables": [
+    {
+      "id": "OverviewView",
+      "source": "campus_overview",
+      "fields": ["id", "title"],
+      "extraSpecs": {}
+    }
+  ],
+  "transactions": [],
+  "backendComponents": []
+}
+```
+
+Rules:
+
+- `domainId` is canonical; `id`/`name` are compatibility inputs only
+- `methods[*].id` is canonical; `methods[*].name` is compatibility input only
+- `methods[*].serviceId` should be emitted even when it can be derived
+- `virtualTables[*].source` must reference an existing table ID when it points to a physical table
+
+### 3.5 Database schema
+
+```json
+{
+  "dataSchema": {
+    "tables": [
+      {
+        "id": "campus_overview",
+        "name": "campus_overview",
+        "filePath": "",
+        "fields": [
+          {
+            "name": "id",
+            "type": "STRING",
+            "notNull": true,
+            "default": null,
+            "enumRef": null,
+            "sourceField": null
+          }
+        ],
+        "indexes": [],
+        "seedData": null
+      }
+    ],
+    "relations": [
+      {
+        "id": "overview_to_building",
+        "from": "campus_overview",
+        "to": "building",
+        "cardinality": "N:1"
+      }
+    ]
+  }
+}
+```
+
+Rules:
+
+- canonical table key is `id`, not `tableName`
+- canonical column collection is `fields`; `columns` is compatibility input only
+- `enumRef` must resolve into `valueDomains.enums[*].name` when used
+
+### 3.6 Theme
+
+```json
+{
+  "theme": {
+    "id": "Theme",
+    "name": "Theme",
+    "filePath": "Theme/Theme.vth",
+    "vlVersion": "3.7",
+    "rootTag": "Theme-Enterprise-6.6",
+    "meta": {
+      "mode": "light",
+      "version": "6.6.0",
+      "styleSpaceVersion": "1.6",
+      "base_theme": "Platform/Theme-Default-Light@1",
+      "profile": "enterprise"
+    },
+    "slots": {
+      "intent.primary.intentBg": "#2563EB",
+      "size.md.sizeMinHeight": "40px",
+      "state.focus.stateShadow": "@intent.intentFocusRing"
+    },
+    "designTokens": [],
+    "componentVariants": [],
+    "bindingRules": [],
+    "overrides": []
+  }
+}
+```
+
+Rules:
+
+- for VL 3.7 / THEME 6.6, the canonical theme model is `# Meta` plus `# Point Slot Values`
+- `theme.slots` is the canonical compiled metadata view of `.vth` point-slot assignments
+- `designTokens`, `componentVariants`, `bindingRules`, and `overrides` are compatibility carry-through fields only; new tooling should not require them
+
+## 4. Consistency Constraints
+
+Validation must reject or flag at least the following:
+
+- section consumes a non-existent service ID
+- section uses a non-existent component ID
+- service virtual table points at a non-existent table
+- app wiring references an instance ID not present in layout refs
+- field `enumRef` points at a non-existent enum
+
+## 5. Legacy Compatibility Mapping
+
+Normalizers may accept the following legacy inputs:
+
+| Legacy input | Canonical output |
+| --- | --- |
+| `project.name` / `project.projectName` | `projectName` |
+| `database` | `dataSchema` |
+| `serviceDomains` | `services` |
+| `appId` | `apps[*].id` |
+| `sectionId` | `sections[*].id` |
+| `componentId` | `components[*].id` |
+| `servicesUsed` / `services` / `serviceDomains` on section | `consumesServices` |
+| `componentRefs` / `components` on section | `usesComponents` |
+| `tableName` / `entityId` | `dataSchema.tables[*].id` |
+| `columns` | `fields` |
+| `methods[*].name` | `methods[*].id` |
+| `theme.pointSlotValues` / `theme.pointSlots` | `theme.slots` |
+
+Compatibility input does not change canonical output names.
+
+## 6. Producer Requirements
+
+Any producer that claims compliance with Metadata Spec 3.1 must emit:
+
+- `$schema: "VL-ProjectMeta/3.0"`
+- canonical IDs and field names
+- `sections[*].consumesServices` as string IDs
+- `sections[*].usesComponents` as string IDs
+- `services[*].methods[*].id`
+- `dataSchema.tables[*].id`
+- `theme.slots` when theme slot data is available
+
+## 7. Consumer Requirements
+
+Consumers should:
+
+- read canonical fields first
+- optionally normalize known legacy aliases on import
+- never write new metadata using legacy key names
+- treat DocCenter path references and workflow prompts as external configuration, not as metadata schema fields
+
+## 8. Canonical Schema String
+
+Use exactly:
+
+```text
+VL-ProjectMeta/3.0
+```

+ 3620 - 0
docs/vl-workflow-spec-3.19.md

@@ -0,0 +1,3620 @@
+# VL Workflow Spec 3.19
+
+> **版本说明**
+>
+> * **Spec 版本**:3.19(文档版本)
+> * **Workflow JSON 格式版本**:过渡期兼容 `3.16`、`3.17` 与 `3.18`;当前已发布的 VLCode-Lite JS 引擎仍以 `"version": "3.16"` 为主
+> * **兼容性规则(3.19 延续)**:规范升级先于所有运行时升级。只要 workflow 未使用更高版本独占能力,现有 `3.16` workflow 应继续可运行;运行时应优先按 capability 判断,而不是只按版本字符串拒绝加载
+> * **3.19 相比 3.18 的主要差异**:
+>   * 明确 **IDE 输入值语义**:界面可以展示 `Doc Ref` 或 viewer URL 方便复制和打开,但真正落盘与传给运行时的配置值仍然是 **Doc ID**
+>   * 明确 **DocCenter 引用优先级**:workflow 中 `registry.docs` 与 step `in.docs` 仍以保留 path 号表达语义,但运行时必须优先使用用户配置的 **Doc ID** 覆盖,再退回 path→docId 目录解析
+>   * 明确 **官方文档与用户文档解耦**:官方核心 spec / workflow docs 通过稳定 `Doc ID` 固定;path 仅作为逻辑别名与默认槽位
+>   * 补充 **IDE Settings 配置位约束**:`VL Syntax`、`Theme` 与 workflow docs 可由用户替换其 Doc ID;`Meta Spec` 与 `Workflow Spec` 可由 IDE/平台锁定
+>   * 新增 **`Tool_*` 标准扩展剖面**:workflow 可直接调用本地/宿主工具,支持 `tool` / `toolName`、`input` / `in`、`timeout`、`allowError` 等字段
+>   * 新增 **`Subflow_*` 显示/运行别名**:在 editor 与运行时中可作为 `WorkflowRun` 的语义化别名使用,用于突出“子工作流”而不引入新的底层执行原语
+>   * 新增 **工具运行时事件**:`tool_start` / `tool_message` / `tool_done` / `tool_error`,要求向外层窗口、SSE 消费方、日志系统完整暴露
+>   * 新增 **稳定运行标识 `clientRunToken`**:在真实 `runID` 尚未分配前,即可对并行 run、pause/resume、rerun 做稳定归属
+>   * 明确 **checkpoint 续跑 / rerunFromStep** 的 fork、loop、overrides 语义,允许从复杂分支中的指定节点向下继续执行
+>   * 补充 **workflow-of-workflows** 编排模式:父 workflow 可通过工具或宿主能力同步/异步调度子 workflow,并透传结构化事件
+
+## 0.3 DocCenter 文档解析优先级(3.19 延续)
+
+对 `registry.docs`、step `in.docs`、以及任何 workflow 内部文档引用,运行时必须按以下顺序解析:
+
+1. 当前项目或 IDE settings 中显式配置的 `Doc ID`
+2. 平台内置官方默认 `Doc ID`
+3. DocCenter 目录中 `path -> latest docId` 的回退解析
+
+约束:
+
+* workflow JSON 内保留的 `"1"`, `"2"`, `"141"` 等值,语义上是 **保留 path 槽位**
+* 真正读取文档内容时,必须优先落到具体 `Doc ID`
+* 当某个 path 配置了显式 `Doc ID` 时,运行时不得再用本地旧文档或 path fallback 覆盖
+* UI 中允许展示 `vl://doc/<id>` 或 `/doc-center.html?docId=<id>` 作为辅助引用,但运行时配置序列化结果必须仍然是数值型 `Doc ID`
+
+---
+# 第一章:Workflow 总纲
+
+## 1.1 定位与目标
+
+Workflow 是 VL 平台中的一种**通用流程编排机制**,用于以结构化、可执行的方式描述和运行多步骤流程。它既可以用于开发阶段的流程约束,也可以作为应用运行时的一部分,对外提供能力。
+
+Workflow 本身是一种**程序化定义的流程**,可被前端、后端服务或工具环境触发执行,并根据使用场景的不同,呈现为不同的承载形式与生命周期。
+
+在 VL 平台中,Workflow 主要覆盖以下四类用途:
+
+| 场景                            | 承载位置                | 使用对象   | 是否项目资产 | 是否参与编译 | 调用方式              | 主要作用                                               |
+| ------------------------------- | ----------------------- | ---------- | ------------ | ------------ | --------------------- | ------------------------------------------------------ |
+| IDE 内 agent flow(无服务调用) | Process/                | 开发者自用 | 是           | 否           | IDE 内调用            | 约束 IDE 内的开发流程,支持基于项目上下文的 Agent 执行 |
+| 业务 workflow(可以调用服务)   | Workflows/              | 应用用户   | 是           | 是           | 前端调用 / 服务内调用 | 编排后端流程,作为应用能力对外提供 API 或自动化服务    |
+| 审批流(可以调用服务)          | Workflows/              | 应用用户   | 是           | 是           | 前端调用 / 服务内调用 | 支持长流程执行,允许 pause / resume 与人工介入         |
+| 本地自用 workflow               | LocalWorkflows/(建议) | 开发者自用 | 否           | 否           | 本地工具调用          | 作为用户本地或个人使用的自动化工具                     |
+
+### IDE 场景强约束(3.9 新增,3.10 延续)
+
+当 workflow 用于 IDE 内 agent flow(典型承载路径 `Process/`)时,必须满足:
+
+* `steps` 中不得出现 `Service_*` 节点;出现即编译/加载报错
+* `registry.services` 必须为空数组(或在实现允许时省略)
+* 该约束仅适用于 IDE workflow;业务 workflow(如 `Workflows/`)不受此限制
+
+**Workflow 的整体设计目标是**:
+
+* 用统一的流程编排模型覆盖**开发流程、业务流程与自动化工具**等不同场景
+* 通过承载位置与编译参与度,明确 Workflow 的**生命周期与责任边界**
+* 同时支持前端触发与服务内调用,避免将 Workflow 限定为单一调用入口
+* 区分"开发者自用流程"与"作为应用能力对外提供的流程",避免语义混淆
+* 保持流程定义的结构化、可执行性与长期可维护性
+
+## 1.2 Workflow 的状态
+
+Workflow 在 VL 平台中的运行模型遵循 **"执行无状态,状态外置"** 的原则。
+
+从定义层面看,Workflow 本身不包含持久化状态,也不描述状态如何存储或演进。
+
+同一个 Workflow 定义可以被多次、并行地执行,而不会因历史运行而产生语义差异。
+
+在运行层面,Workflow **可以在执行时选择性地挂载一个外部状态空间**,用于承载执行过程中所需的上下文或产物,例如文件、运行中间结果或流程进度信息。
+
+该状态空间具有以下特征:
+
+* 是否挂载是可选的
+* 状态空间在 Workflow 运行时通过参数指定
+* 状态空间不属于 Workflow 定义的一部分
+* 不同运行实例可以挂载不同的状态空间
+
+当未挂载状态空间时,Workflow 仍可正常执行,其行为仅依赖运行参数与即时计算结果。
+
+当挂载状态空间时,Workflow 可以在该空间中读写数据,但仍保持执行引擎本身的无状态特性。
+
+通过将状态与执行解耦,VL Workflow 既能够支持短时、一次性的流程执行,也能够支撑需要持续上下文的复杂场景,而无需引入重量级的内置状态机机制。
+
+## 1.3 多步输出
+
+Workflow 支持在执行过程中产生**多步输出(Multi-step Output)**,用于对外暴露流程的中间进度、阶段性结果或执行事件。
+
+多步输出的核心目标是: **在不阻塞流程执行的前提下,使外部系统能够感知并响应 Workflow 的执行过程。**
+
+Workflow 的多步输出具有以下特性:
+
+* 输出以**事件流**的形式产生,而不是一次性返回最终结果
+* 每一步输出均对应 Workflow 执行过程中的某个阶段或节点
+* 对于串行执行的节点,输出顺序与 Workflow 的实际执行顺序保持一致
+* 对于并行执行的节点(如 `children` 并行分支或 `Loop_* mode:"parallel"`),输出事件的顺序取决于各节点的实际完成时间,不做确定性顺序保证
+
+多步输出的消费方式不限定于前端场景,可根据运行环境选择不同的承载方式:
+
+* **前端消费** 多步输出可以通过流式接口推送至前端,用于展示实时进度、阶段结果或交互提示。
+* **后端消费** 多步输出也可以发送至消息队列(MQ)或类似的异步通道,由后端系统监听并据此触发后续处理逻辑。
+
+Workflow 本身不关心多步输出的最终消费方,也不依赖具体的传输机制。
+
+无论输出被前端直接展示,还是被后端服务订阅处理,其语义均保持一致。
+
+通过多步输出机制,Workflow 可以自然支持:
+
+* 长时间执行的流程
+* 需要外部系统按阶段响应的任务
+* 前后端解耦的异步处理模式
+
+## 1.4 控制流模型(Control Flow)
+
+控制流由两部分组成:
+
+### A 节点属性(Node Properties)
+
+用于描述"节点之间如何连接、并发以及是否执行":
+
+* **`next`**:串行后继节点(显式继续信号)
+* **`children`**:并行子分支入口列表(并行扇出 + 汇合)
+* **`if`**:节点执行条件(条件不满足时跳过节点本体及其 children 子树)
+
+规则取向:
+
+* **`next` 必须显式写出,支持特殊语义如"RETURN"和"BREAK"**
+* **除 `Stop_*`节点,其他节点必须有 `next` ,否则spec编译报错**
+* **`if` 为 false 时:**
+  * 不执行该节点的实际动作(如不调用 service/llm/component、不写 out)
+  * **同时跳过该节点的所有 `children` 子树**
+  * 直接进入 `next`
+
+说明:
+
+* `next / children / if` 都是所有节点可拥有的**通用属性**,不属于某个特定节点类型。
+* next / children 在任何节点上都表示控制流出口,而不是数据端口
+
+### B 控制节点类型(Control Node Types)
+
+用于表达结构化控制语义:
+
+* `branch`:条件分支(选择性执行某一条分支)
+* `loop`:循环(对集合数据或条件执行重复步骤,支持数组遍历模式与 while 条件模式)
+
+> 说明:`branch` / `loop` 是"节点类型(kind)",它们本身也是 steps 中的一种节点。
+
+### C 入口与终止(Entry & Termination)
+
+不设置显式的"开始节点"。入口规则与终止语义如下:
+
+#### 入口节点(Entry Nodes)
+
+**入口节点 = `steps` 中不被任何其他节点的 `next`、`children`、`cases` 引用的节点。**
+
+* 引擎在启动 workflow 时,自动识别所有入口节点
+* 若入口节点有且仅有一个:workflow 从该节点开始执行
+* 若入口节点有多个:这些入口节点被视为**并行起点**,引擎同时启动它们(等同于隐含一个虚拟的 `children` 扇出)
+* 若入口节点为零(所有节点都被其他节点引用):workflow 定义非法,引擎应报错(存在环路或结构错误)
+
+> 不设显式 Start 节点的原因:入口点可由结构自动推断,额外的 Start 节点只会增加图上的冗余方块,不携带任何执行语义。
+
+#### 终止、暂停与失败语义
+
+workflow 存在三种**互斥的结束状态**,其语义明确且不可混用:
+
+| 结束方式        | 触发条件                | 状态        | 是否可恢复   | 调用方含义                    |
+| --------------- | ----------------------- | ----------- | ------------ | ----------------------------- |
+| 完成(stopped) | 执行到达 `Stop_*`节点 | `stopped` | 否           | 流程正常完成,可获取最终结果  |
+| 暂停(paused)  | 执行到达 `Pause_*` 节点 | `paused`  | 是           | 流程等待外部输入,需要 resume |
+| 失败(failed)  | 节点执行出错            | `failed`  | 否(需重跑) | 流程异常中断                  |
+
+补充说明:
+
+* `Stop_*` 用于表示 **workflow 的正常完成**,到达即终止,不可恢复。
+* `Pause_*` 节点用于表示 **可恢复的执行中断**,不等价于流程结束。
+* `failed` 表示执行异常,不属于正常控制流的一部分。
+
+约束规则:
+
+* 每个 workflow **至少应存在一个 `Stop_*` 节点**,以保证存在明确的正常完成路径。
+* 一个 workflow **可以包含多个 `Stop_*` 节点**,用于表示不同分支或条件下的提前完成路径。
+
+## 1.5 变量
+
+变量用于承载**轻量级、结构化的数据**,例如参数、标识、状态标志或小规模结果。
+
+变量具有以下特征:
+
+* 以键值形式存在
+* 生命周期限定在单次 Workflow 运行过程中
+* 适合传递控制信息与小体量数据
+* 不适合承载大文本或二进制内容
+
+变量主要用于节点之间的数据传递与执行控制,而不是作为持久化产物存在。
+
+#### 全局变量(Workflow Globals)
+
+* 使用 `$xxx` 表示工作流全局变量
+* `$vars` 在 registry 中声明
+* 支持在步骤间传递数据,并作为最终输出的一部分
+
+#### 局部变量(Locals)
+
+* 使用 `_xxx` 表示局部/循环变量
+* 不需要声明,用于循环体与临时计算
+
+#### 系统变量(User Space Config)
+
+* 使用 `SYSVAR.xxx` 引用用户预先配置在系统空间里的通用变量
+* `SYSVAR` 为只读,常用于密钥、默认配置、偏好参数等
+
+## 1.6 文件与工作空间
+
+Workflow 在运行过程中可以通过 `out` 将结果写入文件空间。文件用于承载**中间产物或结果性输出**,例如代码文件、配置文件、文档等,其生命周期和管理方式由\*\*工作空间(Workspace)\*\*决定。
+
+#### 工作空间的基本概念
+
+工作空间是 Workflow 运行时可选挂载的**外部文件与状态承载空间**。当 Workflow 运行时指定了 `workspaceId`,其所有文件写入行为将发生在该工作空间中;未指定时,系统会为本次运行创建临时文件空间。
+
+工作空间的存在与否、以及具体使用哪个工作空间,均由**运行时参数**决定,而不是 Workflow 定义的一部分。
+
+#### 工作空间的组成
+
+一个工作空间在逻辑上分为两个部分:
+
+1. **系统空间(System Space)**
+2. **产物空间(Artifacts Space)**
+
+---
+
+##### 系统空间(System Space)
+
+系统空间用于存储与工作空间自身相关的**系统级与流程级元数据**,例如:
+
+* Workflow 的运行状态
+* 暂停 / 恢复相关的信息
+* 中间状态标识、执行记录等
+
+系统空间由平台自动管理:
+
+* 不提供给用户直接写入接口
+* 不作为业务产物参与编译或交付
+* 类似于数据库系统中自动维护的系统表
+
+Workflow 在运行过程中可以使用系统空间承载必要的运行状态,但不直接暴露其结构与内容。
+
+---
+
+##### 产物空间(Artifacts Space)
+
+产物空间用于存储 Workflow 运行过程中产生或修改的**业务文件与结果文件**,例如:
+
+* 代码文件
+* 配置文件
+* 文档与资源文件
+
+通过 `out` 写入的文件路径(以 `/` 开头),最终都会落入产物空间。
+
+产物空间具有以下特征:
+
+* 文件内容对用户可见
+* 文件可以参与项目编译或交付
+* 文件的管理策略独立于 Workflow 本身
+
+---
+
+#### 产物空间的版本管理能力
+
+针对产物空间,工作空间本身提供**版本管理能力**,包括但不限于:
+
+* 提交(commit)
+* 回滚(rollback)
+
+这些能力属于**工作空间自身的能力**,而不是 Workflow 的职责:
+
+* Workflow 不自动触发 commit
+* Workflow 不负责回滚策略
+* 是否提交、何时回滚由用户或外部系统决定
+
+Workflow 的职责仅限于"在当前工作空间状态下生成或修改文件"。
+
+---
+
+#### 工作空间的锁定机制
+
+工作空间支持加锁机制,用于控制并发访问与写入冲突。
+
+* 在 Workflow 运行期间,可对工作空间加锁
+* 锁的粒度与策略由平台统一管理
+* 加锁期间,其他写入操作需等待或被拒绝
+
+该机制用于保证:
+
+* Workflow 执行期间文件状态的一致性
+* 多次运行或多用户操作之间的安全隔离
+
+---
+
+#### 设计原则
+
+* Workflow 关注的是**文件如何生成或修改**
+* 工作空间负责**文件的存储、版本与并发控制**
+* 系统空间与产物空间职责清晰,避免混用
+* 文件状态管理与 Workflow 执行解耦
+
+通过引入工作空间机制,VL Workflow 能够在保持执行模型无状态的前提下,支持复杂的文件生成、修改与长期演进场景。
+
+## 1.7 运行时参数
+
+Workflow 在定义完成后,可以在每一次运行(run)时通过**运行时参数**对本次执行行为进行控制。运行时参数不属于 Workflow 定义本身,而是**每次 run 的附加上下文**,用于让同一份 Workflow 在不同场景下以不同方式执行。
+
+运行时参数主要包含以下几类:
+
+---
+
+#### 1) `params`(可选:业务入参)
+
+用于向 workflow 传入**业务数据**。
+
+* 入参名称与类型在 `registry.params` 中声明
+* 无默认值的入参为必填,缺失时引擎报错
+* 有默认值的入参可省略(使用声明中的默认值)
+* 入参在 workflow 内部以只读变量形式存在
+
+---
+
+#### 2) `workspaceId`(可选:挂载文件空间)
+
+用于指定本次运行所使用的外部文件空间。
+
+* **未指定**:系统为本次 run 创建临时文件空间;节点通过 `out` 写入的文件路径(以 `/` 开头)将落到该临时空间;运行结束后按 TTL 自动清理。
+* **指定**:节点通过 `out` 写入的文件路径将落到该 `workspaceId` 对应的文件空间中,文件持续保留,供后续运行或人工操作使用。
+
+> `workspaceId` 只影响本次 run 的文件落点,不改变 Workflow 定义。
+
+---
+
+#### 3) `nodes`(可选:部分执行 / 重跑)
+
+用于指定本次运行中**实际参与执行的节点列表**。
+
+* **未指定**:按 Workflow 定义从起始节点执行完整流程。
+* **指定**:仅执行给定列表中的节点(引擎会按 Workflow 定义的顺序与依赖关系进行调度)。
+
+该参数用于支持:
+
+* 增量执行(只跑某些生成/修复节点)
+* 失败后的局部重试
+* 规划阶段生成"执行指令"后按指令选择性运行
+
+---
+
+#### 4) `mode`(可选:运行模式)
+
+用于指定本次运行的整体执行模式。该模式为引擎提供统一的运行策略,用于影响节点的行为(尤其是文件写入相关行为)。
+
+常见模式包括:
+
+* `create`:用于初始化或新增产物
+* `patch`:用于增量修改
+* `regenerate`:用于重建/重写
+* `validate`:仅校验,不产生文件写入
+
+> 运行模式是 run 级别的策略参数,不属于 `out` 的一部分。
+
+---
+
+#### 设计原则
+
+* 运行时参数是**执行级控制**,与 Workflow 定义解耦
+* 不同 run 的参数相互独立,可并行运行
+* `params` 传入业务数据,`workspaceId`、`nodes`、`mode` 控制执行行为与文件落点
+* Workflow 通过 `out` 的一层映射同时支持变量与文件输出:
+  * `$...` 写入变量
+  * `/...` 写入文件空间(临时或 workspace)
+
+通过运行时参数机制,Workflow 能在保持结构稳定的同时,支持新项目生成、增量修改、局部重跑与校验等多种运行方式。
+
+#### 示例
+
+```JSON
+// 示例:Workflow 的运行时参数(runParams)
+// 说明:
+// - 该示例展示一次"增量修改"的运行方式
+// - 同一份 Workflow 定义,在不同 runParams 下可以表现出完全不同的执行行为
+
+{
+  // 要执行的 workflow 定义
+  "workflowId": "codegen_apply_v1",
+
+  // 本次 workflow run 的运行时参数
+  "runParams": {
+    // 可选:业务入参(对应 registry.params 中声明的入参)
+    "params": {
+      "userRequest": "帮我生成一个登录页面",
+      "targetLang": "zh-CN"
+    },
+
+    // 可选:指定外部文件空间
+    // - 不指定:使用临时文件空间(有 TTL)
+    // - 指定:文件写入落到该 workspace 中
+    "workspaceId": "project_workspace_123",
+
+    // 可选:仅执行部分节点
+    // 用于增量执行、失败后重跑、或按规划指令执行
+    "nodes": [
+      "blueprint",
+      "contract",
+      "file_backend"
+    ],
+
+    // 可选:运行模式(run 级别策略)
+    // - create      初始化 / 新建
+    // - patch       增量修改
+    // - regenerate 重建 / 重写
+    // - validate    仅校验,不写文件
+    "mode": "patch"
+  }
+}
+```
+
+## 1.8 节点就绪即执行与并行调度(Eager Execution)
+
+Workflow 的执行引擎采用 **"就绪即执行"(Eager Execution)** 的调度策略:
+
+**核心规则:当一个节点的所有输入条件(前置依赖)已经全部满足时,该节点即可立即开始执行,无需等待与其无依赖关系的其他节点完成。**
+
+这意味着 Workflow 的实际执行顺序不是简单的线性序列,而是由 **数据依赖关系** 决定的偏序执行图(Partial Order)。多个彼此无依赖的节点可以被引擎同时调度和执行。
+
+---
+
+#### 对 workflow.json 生成的指导意义
+
+在设计和生成 `workflow.json` 时,应 **尽量利用并行性**,具体原则如下:
+
+1. **能并行就并行**:如果多个节点之间不存在数据依赖关系(即节点 B 不需要读取节点 A 的输出),则应将它们设计为可并行执行的结构(如使用 `children` 扇出,或在 `Loop_*` 中使用 `mode:"parallel"`)。
+2. **避免不必要的串行**:不要仅因为"书写顺序"而将无依赖关系的节点串联为 `next` 链。只有当节点 B 确实依赖节点 A 的输出(如 B 的 `in` 引用了 A 写入的 `$vars`)时,才应将 A → B 设为串行。
+3. **依赖判定标准**:节点 B 依赖节点 A,当且仅当:
+   * B 的 `in` 中引用了 A 通过 `out` 或 `Set_*` 写入的全局变量(`$xxx`)
+   * B 需要读取 A 通过 `Write_*` 或 `out` 写入的文件
+   * B 在控制流上显式依赖 A(如 A 的 `next` 指向 B)
+4. **推荐结构模式**:
+   * 对于多个独立的 LLM 调用或 Service 调用,优先使用 `children` 并行扇出
+   * 对于集合数据的处理,优先使用 `Loop_* mode:"parallel"`
+   * 仅在确有顺序依赖时使用 `next` 串行连接
+
+---
+
+#### 示例
+
+```JSON
+// ❌ 不推荐:无依赖关系却串行排列
+{
+  "id": "LLM_GenFrontend",
+  "in": { "messages": [{ "role": "user", "content": "=$prd" }] },
+  "out": { "$frontendCode": "=_result" },
+  "next": "LLM_GenBackend"   // GenBackend 并不依赖 GenFrontend 的输出
+}
+
+// ✅ 推荐:使用 children 并行执行无依赖节点
+{
+  "id": "Set_StartGen",
+  "target": "$status",
+  "value": "\"generating\"",
+  "children": ["LLM_GenFrontend", "LLM_GenBackend"],
+  "next": "Service_Merge"
+}
+```
+
+---
+
+#### 引擎行为约束
+
+* 引擎在调度时,应根据节点的实际依赖关系构建 DAG(有向无环图),并尽可能并行执行就绪节点
+* `children` 中列出的节点天然并行;但即使是通过 `next` 串联的节点,如果引擎检测到无实际数据依赖,也 **允许** 提前调度(但这属于引擎优化,不改变语义正确性)
+* `steps` 数组中的顺序不影响执行顺序,执行顺序完全由 `next / children / branch / loop` 等控制流结构以及数据依赖关系决定
+
+通过就绪即执行策略,Workflow 能在保持语义正确性的同时,最大化执行并行度,显著提升整体吞吐效率。
+
+## 1.9 错误处理
+
+Workflow 在执行过程中可能遇到各类异常情况,包括但不限于:
+
+* 调用型节点执行失败(如 Service 调用超时、LLM 返回异常、Component 执行错误)
+* 表达式求值失败(如引用了不存在的变量或字段)
+* 文件写入失败(如路径越界、IO 错误)
+* 类型错误(如 Loop source 求值结果不是数组)
+
+---
+
+#### 1.9.1 默认错误行为
+
+当节点执行发生不可恢复的错误时,workflow 进入 **`failed`** 状态:
+
+* `failed` 不同于 `paused`(由 `Pause_*` 节点触发的可恢复挂起)和 `stopped`(正常终止)
+* 引擎停止后续节点的执行
+* 已启动的并行分支(children / parallel loop 迭代)由引擎决定是否等待其完成或立即中止
+
+---
+
+#### 1.9.2 Failed 返回结构
+
+当 workflow 进入 `failed` 状态时,引擎返回:
+
+* `status: "failed"`
+* `failedAt: "<stepId>"`(失败节点的 id)
+* `error: { code: "<errorCode>", message: "<errorMessage>" }`
+* `vars: {...}`(失败时的全局变量快照,用于调试与重跑)
+
+---
+
+#### 1.9.3 与运行时参数的关系
+
+`failed` 状态下,可通过运行时参数 `nodes` 指定失败节点及其后续节点进行局部重跑,实现失败恢复。
+
+---
+
+#### 1.9.4 设计原则
+
+* 错误处理采用 **fail-fast** 策略:节点失败即终止整个 workflow,不做隐式重试
+* 重试与容错由外部系统(调用方)决定,不内建于 workflow 定义中
+* 支持节点级 `onError`:节点失败时,若配置了 `onError`,则转入指定失败处理节点;未配置时保持 fail-fast
+
+---
+
+## 1.10 流程图简洁性规则(Graph Simplicity)
+
+Workflow 在可视化时表现为流程图(Graph),每个节点对应图上的一个方块,每条 `next / children` 对应一条连线。为了保持流程图的 **可读性与稳定性**,在设计和生成 `workflow.json` 时应遵循以下简洁性原则:
+
+---
+
+#### 1.10.1 核心原则:能内联就不外拆
+
+**调用型节点(Service\_\* / API\_\* / Component\_\* / LLM\_\*)自身已具备通过 `out` 写入变量和文件的能力。** 当变量写入或文件写入的内容来源于当前节点的 `_result` 时,应直接在该节点的 `out` 中完成,**不应** 额外创建独立的 `Set_*` 或 `Write_*` 节点。
+
+```JSON
+// ❌ 不推荐:多余的 Set 节点,增加了图上的方块和连线
+{ "id": "LLM_GenCode", "in": {...}, "out": "$rawResult", "next": "Set_ExtractStatus" }
+{ "id": "Set_ExtractStatus", "target": "$status", "value": "=$rawResult.status", "next": "Stop_End" }
+
+// ✅ 推荐:在 out 中直接完成映射,图上只有一个节点
+{ "id": "LLM_GenCode", "in": {...}, "out": { "$rawResult": "=_result", "$status": "=_result.status" }, "next": "Stop_End" }
+```
+
+---
+
+#### 1.10.2 何时使用独立的 `Set_*` / `Write_*` 节点
+
+独立状态写入节点仅在以下场景中使用:
+
+1. **写入内容不来自任何节点的 `_result`** — 例如需要拼接多个变量、写入常量、或进行纯计算
+2. **需要在控制流的特定位置写入** — 例如在 `Branch_*` 之前设置分支条件变量,或在 `Loop_*` 之前初始化计数器
+3. **需要特殊写入策略** — 例如 `Write_*` 的 `mode:"append"` 或 `mode:"failIfExists"`,这些是 `out` 文件写入不支持的
+4. **需要在并行扇出前设置共享状态** — 例如 `Set_*` 后接 `children` 并行启动
+
+---
+
+#### 1.10.3 控制节点的合并原则
+
+* **避免空壳节点**:如果一个 `Set_*` 节点的唯一作用是设置变量然后立刻 `next` 到下一个节点,且该变量可以在前一个调用型节点的 `out` 中完成,则该 `Set_*` 节点应被合并
+* **Branch / Loop 保持独立**:控制节点(`Branch_*` / `Loop_*`)本身代表流程结构语义,应始终作为独立节点存在
+* **Stop 保持独立**:`Stop_*` 代表明确的终止语义,应始终作为独立节点存在
+
+---
+
+#### 1.10.4 并行结构的简化
+
+* 如果两个节点可以并行执行且无依赖关系,优先使用 `children` 扇出,而不是串行排列后依赖引擎优化
+* 如果并行扇出的父节点不需要做任何计算或写入,可以使用一个轻量的 `Set_*` 节点作为扇出锚点(设置状态标志),避免为此创建一个空的 `Service_*` 调用
+
+---
+
+#### 1.10.5 节点数量参考基准
+
+* 一个典型的业务流程应以 **5–15 个节点** 为目标
+* 如果节点数超过 20 个,应检查是否存在可合并的 Set/Write 节点
+* 嵌套的 Loop / Branch 内部节点不计入上层计数
+
+---
+
+#### 1.10.6 总结
+
+| 场景                  | 推荐做法                       | 避免做法                               |
+| --------------------- | ------------------------------ | -------------------------------------- |
+| 调用节点输出写变量    | 在 `out`中直接映射           | 创建额外 `Set_*`                     |
+| 调用节点输出写文件    | 在 `out`中使用 `/path`写入 | 创建额外 `Write_*`                   |
+| 纯计算 / 拼接写入     | 使用独立 `Set_*`             | 无法内联,独立节点是正确选择           |
+| 特殊写入策略 (append) | 使用独立 `Write_*`           | `out`不支持 mode,独立节点是正确选择 |
+| 多个无依赖调用        | `children`并行扇出           | `next`串行排列                       |
+| 条件判断              | 独立 `Branch_*`节点          | 不应合并到其他节点                     |
+
+---
+
+# 第二章:Spec JSON 顶层结构
+
+工作流以一个 JSON 对象表示,顶层结构固定如下:
+
+```JSON
+{
+  "version": "当前版本号",
+  "name": "string",
+  "registry": { },
+  "steps": [ ]
+}
+```
+
+## 2.1 字段定义
+
+### 2.1.1 `version`(必填)
+
+* 固定值:`"当前版本号"`
+
+### 2.1.2 `name`(必填)
+
+* 工作流名称(用于展示、检索、日志定位)
+* 类型:`string`
+
+### 2.1.3 `registry`(必填)
+
+* 外部资源与全局边界声明区(先注册后使用)
+* 类型:`object`
+* 结构在第三章定义
+
+### 2.1.4 `steps`(必填)
+
+* 节点列表(workflow 的主体)
+* 类型:`array<Step>`
+* `Step` 的结构在后续章节定义
+
+## 2.2 顶层约束
+
+* `registry` 与 `steps` 必须同时存在
+* `steps` 必须非空
+* 节点的执行与连接关系不依赖数组顺序,只依赖节点的 `next / children / branch / loop` 等显式结构(因此数组顺序仅用于阅读与 diff)
+
+## 2.3 图结构校验规则(Graph Validation)
+
+workflow 的 `steps` 在结构上构成一个有向图(Directed Graph)。为确保所有节点都有意义且可执行,引擎在加载 workflow 时应执行以下校验:
+
+---
+
+#### 2.3.1 入口节点校验
+
+* **入口节点** = 不被任何其他节点的 `next`、`children`、`cases` 引用的节点(见 1.4 C 节)
+* 入口节点数量必须 ≥ 1,否则 workflow 非法(存在环路或结构错误)
+
+---
+
+#### 2.3.2 可达性校验(Reachability)
+
+* **每个节点必须从至少一个入口节点可达**
+* 可达的定义:从入口节点出发,沿 `next`、`children`、`cases`(含 ELSE)的引用链能够到达该节点
+* 不可达的节点视为死代码,引擎应报错或发出警告
+
+---
+
+#### 2.3.3 引用完整性校验
+
+* 所有 `next`、`children`、`cases` 中引用的 step id 必须在 `steps` 中存在
+* 不允许引用不存在的节点
+* 不允许节点 id 重复
+
+---
+
+#### 2.3.4 `Stop_*` 校验
+
+* 推荐至少存在一个 `Stop_*` 节点(缺失时引擎发出警告)
+* `Stop_*` 不允许有 `next` 或 `children`(若存在应报错)
+
+---
+
+#### 2.3.5 校验级别建议
+
+| 校验项                    | 级别    | 说明                |
+| ------------------------- | ------- | ------------------- |
+| 入口节点 ≥ 1             | ERROR   | 无入口则无法启动    |
+| 所有节点可达              | ERROR   | 不可达节点是死代码  |
+| 引用 id 存在              | ERROR   | 断链导致运行时崩溃  |
+| id 不重复                 | ERROR   | 控制流歧义          |
+| 至少一个 Stop\_\*         | WARNING | 纯审批流可能无 Stop |
+| Stop\_\* 无 next/children | ERROR   | 语义冲突            |
+
+## 2.4 3.16 增量编译检查
+
+| 检查项 | 规则 | 级别 |
+|------|------|------|
+| while 与 source 互斥 | 同一 `Loop_*` 同时声明 `while` 与 `source` | ERROR |
+| while 必填项 | `while` 模式缺少 `maxIterations` | ERROR |
+| maxIterations 范围 | `maxIterations < 1` | ERROR |
+| while mode 约束 | `while` + `mode:"parallel"` | ERROR |
+| BREAK 作用域 | `BREAK` 出现在非 `Loop_*` children 子树 | ERROR |
+| BREAK 保留字 | `BREAK` 被用作 stepId | ERROR |
+| LLM model 空值 | `LLM_*` 节点 `model` 为空字符串 | ERROR |
+| LLM model 分段 | `model` 含 `/` 但任一侧为空 | ERROR |
+
+# 第三章:Registry
+
+`registry` 用于声明 workflow 运行所需的**外部依赖**与**全局边界**,并强制执行"**先注册后使用**"。workflow 只负责**执行逻辑**,不内置任何特定业务闭环(如 git commit、审批提交、触发下一个流程等),这些均由 workflow 外部系统完成。
+
+---
+
+## 3.1 Registry 顶层结构
+
+```JSON
+{
+  "params": [ ... ],
+  "services": [ ... ],
+  "apis": [ ... ],
+  "components": [ ... ],
+  "vars": [ ... ],
+  "files": {
+    "inputs": [ ... ],
+    "artifacts": [ ... ]
+  },
+  "docs": { ... },
+  "schemas": { ... }
+}
+```
+
+## 3.2 `params`(选填)
+
+声明 workflow 接受的**运行时入参**。入参由调用方在 `runParams.params` 中传入,workflow 内部以只读变量形式访问。
+
+### 3.2.1 结构
+
+`params` 是字符串数组,每个元素声明一个入参名称与类型,可带默认值:
+
+```Plain
+"params": [
+  "userRequest(STRING)",
+  "targetLang(STRING)",
+  "maxRetries(INT) = 3"
+]
+```
+
+### 3.2.2 规则
+
+* 入参声明**不带 `$` 前缀**,与 VL 语法中服务/方法的入参风格对齐
+* 有默认值的入参在 `runParams.params` 中可省略;无默认值的入参为必填,缺失时引擎报错
+* 入参在 workflow 内部以**只读变量**形式存在,步骤中直接用参数名引用(如 `=userRequest`),不可出现在 `out` 左侧或 `Set_*` 的 `target` 中
+* 入参名称不得与 `registry.vars` 中的全局变量名冲突
+
+---
+
+## 3.3 `services`(必填)
+
+声明 workflow 会调用的**项目内服务**,并在 registry 中明确其**入参/出参契约**(用于静态校验与 AI 生成约束)。
+
+### 3.3.1 结构(VL 单行签名)
+
+`services` 是字符串数组,每个元素是一条服务签名:
+
+```Plain
+"services": [
+  "PlannerService(prd(STRING), rulesFile(FILE_REF)) RETURN plan(OBJECT)",
+  "WriteFileService(path(STRING), content(STRING)) RETURN ok(BOOL)",
+  "ApprovalService(form(OBJECT), policy(STRING)) RETURN decision(STRING), comment(STRING)"
+]
+```
+
+### 3.3.2 规则
+
+* `ServiceName(...) RETURN ...` 为固定格式
+* 入参用 `paramName(Type)` 表示,可多个
+* 出参用 `resultName(Type)` 表示,可多个
+* `ServiceName` 必须唯一
+* `Service_*` 节点的 id 中 `Service_` 后缀部分必须匹配 `registry.services` 中的某个 ServiceName
+* service 节点传入参数必须满足签名约束;返回结果应满足 RETURN 声明
+
+---
+
+## 3.4 `apis`(选填)
+
+声明 workflow 会调用的**第三方外部 API**,并在 registry 中明确其**端点、方法与认证方式**。
+
+`API_*` 节点与 `Service_*` 的区别在于:`Service_*` 调用的是 VL 项目内部的服务(由项目自身定义和部署),而 `API_*` 调用的是项目外部的第三方 HTTP 接口(如支付网关、地图服务、天气 API、SaaS 平台 API 等)。
+
+### 3.4.1 结构
+
+`apis` 是对象数组,每个元素声明一个第三方 API 端点:
+
+```JSON
+"apis": [
+  {
+    "id": "StripeCreateCharge",
+    "method": "POST",
+    "url": "https://api.stripe.com/v1/charges",
+    "auth": "SYSVAR.stripeApiKey",
+    "headers": { "Content-Type": "application/x-www-form-urlencoded" },
+    "desc": "Stripe 创建收费"
+  },
+  {
+    "id": "WeatherQuery",
+    "method": "GET",
+    "url": "https://api.weatherapi.com/v1/current.json",
+    "auth": "SYSVAR.weatherKey",
+    "desc": "查询当前天气"
+  },
+  {
+    "id": "SlackPostMessage",
+    "method": "POST",
+    "url": "https://slack.com/api/chat.postMessage",
+    "auth": "SYSVAR.slackBotToken",
+    "headers": { "Content-Type": "application/json" },
+    "desc": "发送 Slack 消息"
+  }
+]
+```
+
+### 3.4.2 字段说明
+
+| 字段    | 必填 | 类型   | 说明                                                            |
+| ------- | ---- | ------ | --------------------------------------------------------------- |
+| id      | 是   | string | API 唯一标识,`API_*`节点 id 的 `API_`后缀部分必须匹配此 id |
+| method  | 是   | string | HTTP 方法:`GET / POST / PUT / PATCH / DELETE`                |
+| url     | 是   | string | API 端点 URL(可包含路径参数占位符,如 `{orderId}`)          |
+| auth    | 否   | string | 认证凭据来源(通常引用 `SYSVAR.xxx`),引擎负责注入到请求头   |
+| headers | 否   | object | 固定请求头(键值对),每次调用自动携带                          |
+| desc    | 否   | string | 人类可读的用途说明                                              |
+
+### 3.4.3 规则
+
+* `id` 必须唯一
+* `API_*` 节点的 id 后缀必须匹配 `registry.apis` 中的某个 `id`
+* `auth` 引用的凭据建议存放在 `SYSVAR` 中(系统级密钥管理),不应明文写入 workflow 定义
+* `url` 中的路径参数(如 `{orderId}`)在运行时由节点 `in.pathParams` 替换
+* API 的实际调用由引擎层执行,workflow 节点只声明请求参数,不直接发起 HTTP 请求
+
+---
+
+## 3.5 `components`(必填)
+
+声明 workflow 会调用的**系统内置组件能力**(例如 MCP、文件能力等)。
+
+```Plain
+"components": ["FileOps", "MCP_Search", "MCP_VectorDB"]
+```
+
+规则:
+
+* `componentId` 必须唯一
+* `Component_*` 节点引用的 `componentId` 必须在该列表中
+* component 的入参/出参由系统内置定义,不要求在 registry 中重复声明
+
+---
+
+## 3.6 `vars`(必填)
+
+声明 workflow 可读写的**全局变量集合**(VL 风格)。
+
+```Bash
+"vars": ["$keyword(STRING)", "$items([OBJECT])", "$result(OBJECT)", "$count(INT)"]
+```
+
+规则:
+
+* 变量名必须以 `$` 开头
+* 变量名必须唯一
+* workflow 中出现的任何 `$xxx` 必须在此声明
+* `set.target` 与节点 `out` 只能写入已声明的 `$xxx`
+
+---
+
+## 3.7 `files`(必填)
+
+声明 workflow 的文件读写边界(只读输入 + 临时产物)。
+
+```JSON
+"files": {
+  "inputs": ["Process/PRD.json", "Process/Rules/*"],
+  "artifacts": ["Process/Artifacts/*"]
+}
+```
+
+### 3.7.1 `inputs`(只读)
+
+规则:
+
+* workflow 中所有读取文件地址的行为必须落在 `inputs` 范围内
+* inputs 在 workflow 运行期间视为只读,不允许修改
+
+### 3.7.2 `artifacts`(临时可写)
+
+规则:
+
+* workflow 中所有写入文件地址的行为必须落在 `artifacts` 范围内
+* artifacts 是 run scope 的临时文件空间,不承诺长期可见性或稳定读取入口
+
+## 3.8 `docs`(语义文档引用)
+
+`registry.docs` 用于声明 **workflow 可能引用的语义文档标识**。这些文档不以"文件路径"形式暴露给 workflow,而是通过 **稳定的 `docId`** 引用。
+
+---
+
+### 3.8.1 定位与原则
+
+* `docs` 是 **语义级引用表**,不是文件系统
+* workflow **只感知 docId,不感知路径**
+* 文档 **只读**
+* 文档的真实存储位置、加载方式由系统管理,不属于 workflow 规范
+* `docs` 的作用是:**为 LLM / service 提供稳定、可复用的背景知识引用**
+
+---
+
+### 3.8.2 结构
+
+`docs` 是一个对象,**key 为 docId(字符串或数字)**,value 为该文档的语义说明:
+
+```Plain
+"docs": {
+  "11": "VL 语法与表达式规则",
+  "12": "Workflow v2.x 设计约束说明",
+  "20": "前端组件生成规范"
+}
+```
+
+说明:
+
+* `docId` 必须在 workflow 内唯一
+* value 是**人类可读的用途描述**,用于:
+  * 理解 workflow
+  * 辅助 AI 生成
+  * 审计与维护
+
+---
+
+### 3.8.3 使用方式
+
+在 workflow 中,节点**只引用 docId**,不引用路径或内容。
+
+常见用法(示意):
+
+```Plain
+{
+  "id": "LLM_Generate",
+  "in": {
+    "docs": ["11", "20"],
+    "messages": [
+      { "role": "system", "content": "请严格遵守相关规范。" }
+    ]
+  }
+}
+```
+
+语义:
+
+* `"11"` 表示「VL 语法与表达式规则」
+* `"20"` 表示「前端组件生成规范」
+* 文档的实际内容如何注入 prompt,由系统实现决定,不属于workflow规范
+
+---
+
+### 3.8.4 约束规则
+
+* workflow 中引用的 docId **必须已在 `registry.docs` 中声明**
+* workflow **不能修改 docs**
+* docs **不参与表达式计算**
+* docs **不影响控制流**
+
+---
+
+### 3.8.5 与 `files.inputs` 的关系
+
+| 对比项       | files.inputs        | docs               |
+| ------------ | ------------------- | ------------------ |
+| 暴露形式     | 文件路径            | docId              |
+| 是否 IO      | 是                  | 否                 |
+| 是否参与计算 | 是                  | 否                 |
+| 主要使用者   | Service / Component | LLM(为主)        |
+| 关注点       | 数据                | 语义 / 规则 / 规范 |
+
+## 3.9 Registry 通用约束(强制)
+
+1. **先注册后使用**
+   1. service / api / component / 全局变量 / 入参 / 文件地址引用均必须先在 registry 声明
+   2. `LLM_*` 节点是例外:不需要在 registry 中注册。其模型通过节点 `model` 字段与运行环境默认规则解析
+2. **唯一性**
+   1. services.id、apis.id、components、params、vars 均不允许重复
+   2. params 与 vars 之间不允许同名
+3. **只读与可写边界明确**
+   1. params 只读(入参不可被 workflow 修改)
+   2. inputs 只读
+   3. docs 只读
+   4. artifacts 可写(临时产物)
+4. **保持逻辑纯净**
+   1. registry 只描述运行所需依赖与边界,不表达业务闭环语义(如 commit、发布、审批提交等)
+
+## 3.10 `schemas`(JSON Schema 复用,选填)
+
+`registry.schemas` 用于集中声明可复用的 JSON Schema,供 `LLM_*` 节点在 `output_config.format.schemaRef` 中引用。
+
+```JSON
+"schemas": {
+  "SpecSchema": { "type": "object", "additionalProperties": false, "properties": { ... } },
+  "PlanSchema": { "type": "object", "additionalProperties": false, "properties": { ... } }
+}
+```
+
+规则:
+
+* `schemaId`(key)在 workflow 内唯一
+* `schemaRef` 必须能在 `registry.schemas` 中找到
+* 若同时声明 `schema` 与 `schemaRef`,建议视为错误(避免歧义)
+* 执行前引擎需将 `schemaRef` 归一化展开为 `format.schema`
+
+## 3.11 模型配置边界
+
+本规范采用“本地 SDK 内置”模式:LLM provider 由引擎内置 SDK 调用,不经由统一 `LLM_URL` 网关。
+
+规则:
+
+* 不在 `registry` 中声明模型列表或 API key
+* `LLM_*` 节点通过 `model` 字段声明 provider/model;`model` 允许以下三种写法:
+  * 不写 `model`:使用运行环境全局默认 `LLM_MODEL`(格式为 `<provider>/<modelId>`)
+  * `model: "<provider>"`:仅声明 provider,`modelId` 取运行环境 `<PROVIDER>_MODEL`
+  * `model: "<provider>/<modelId>"`:节点内完全指定
+* 运行环境仅负责提供 provider 级凭据(如 `OPENAI_API_KEY`、`ANTHROPIC_API_KEY`)与默认模型值
+* `workflow` 只声明流程逻辑与输出约束(如 `output_config`、`schemaRef`),不声明密钥
+
+最小 `.env` 示例:
+
+```env
+LLM_MODEL=anthropic/claude-sonnet-4-5-20250929
+ANTHROPIC_API_KEY=sk-ant-xxx
+OPENAI_API_KEY=sk-openai-xxx
+ANTHROPIC_MODEL=claude-sonnet-4-5-20250929
+OPENAI_MODEL=gpt-4.1
+```
+
+
+# 第四章:节点类型(Step Types)
+
+`steps` 中所有节点分为三大类:**调用型节点**、**状态写入节点**、**控制节点**。
+
+---
+
+## 4.1 调用型节点(Call Nodes)
+
+用于调用外部能力并产出结果(可流式输出)。
+
+* **`Service_*`**:调用项目内服务(需在 `registry.services` 注册并声明签名)
+* **`API_*`**:调用第三方外部 HTTP API(需在 `registry.apis` 注册端点与认证信息)
+* **`Component_*`**:调用系统内置组件能力(需在 `registry.components` 注册)
+* **`LLM_*`**:调用大模型推理能力(可流式输出;不需要在 registry 中注册,模型通过节点 `model` 字段选择)
+* **`Download_*`**:从外部来源下载单文件并落盘到 artifacts(流式,不经过 `$vars` 承载正文)
+* **`Unzip_*`**:读取 zip 并逐 entry 解压到 artifacts(流式分发,支持后缀路由)
+
+---
+
+## 4.2 状态写入节点(State Nodes)
+
+用于把数据写入 workflow 内部状态空间(变量 / 临时文件),作为步骤间数据传递与过程记录手段。
+
+* **`Set_*`**:写入/更新全局变量(`$vars`)
+* **`Write_*`**:写入 workflow 空间内的临时文件(artifacts)
+
+---
+
+## 4.3 控制节点(Control Nodes)
+
+用于表达流程结构与执行策略。
+
+* **`Branch_*`**:条件分支(选择性执行某一条分支)
+* **`Loop_*`**:循环节点(对集合执行重复步骤或按条件循环;支持 `source` 与 `while` 两种互斥模式)
+* **`Stop_*`**:终止节点(明确结束 workflow)
+* **`Pause_*`**:暂停节点(挂起 workflow 等待外部 resume,可恢复)
+
+---
+
+# 第五章:节点属性(Step Properties)
+
+## 5.1 属性概览(总表)
+
+| 属性     | 一句话简介                                          | 适用节点类型 | Service\_\* | API\_\* | Component\_\* | LLM\_\* | Download\_\* | Unzip\_\* | Set\_\* | Write\_\* | Branch\_\* | Loop\_\* | Stop\_\* | Pause\_\* |
+| -------- | --------------------------------------------------- | ------------ | ----------- | ------- | ------------- | ------- | ------------ | --------- | ------- | --------- | ---------- | -------- | -------- | -------- |
+| id       | 节点唯一标识(同时编码类型,如 Service\_xxx)       | 全部         | 必填        | 必填    | 必填          | 必填    | 必填         | 必填      | 必填    | 必填      | 必填       | 必填     | 必填     | 必填       |
+| if       | 条件表达式;为 false 时跳过该节点及其 children 子树 | 除Stop_*     | 选填        | 选填    | 选填          | 选填    | 选填         | 选填      | 选填    | 选填      | 选填       | 选填     | 不适用   | 选填       |
+| in       | 调用入参对象(由服务/组件/LLM/API 定义)            | 调用型节点   | 必填        | 必填    | 必填          | 必填    | 不适用       | 不适用    | 不适用  | 不适用    | 不适用     | 不适用   | 不适用   | 不适用     |
+| out      | 将本节点输出\_result 映射写入 \$vars 或文件         | 调用型节点   | 选填        | 选填    | 选填          | 选填    | 选填         | 选填      | 不适用  | 不适用    | 不适用     | 不适用   | 不适用   | 不适用     |
+| model    | LLM 选择字段(支持 `<provider>/<modelId>` / `<provider>` / 省略) | LLM\_\*      | 不适用      | 不适用  | 不适用        | 选填    | 不适用       | 不适用    | 不适用  | 不适用    | 不适用     | 不适用   | 不适用   | 不适用     |
+| source   | 下载来源 / zip 来源 / loop 数据源(与 `while` 互斥) | Download/Unzip/Loop | 不适用 | 不适用 | 不适用    | 不适用  | 必填         | 必填      | 不适用  | 不适用    | 不适用     | 条件必填 | 不适用   | 不适用     |
+| while    | 条件循环表达式(仅 Loop\_\*,与 `source` 互斥)      | Loop\_\*     | 不适用      | 不适用  | 不适用        | 不适用  | 不适用       | 不适用    | 不适用  | 不适用    | 不适用     | 条件必填 | 不适用   | 不适用     |
+| maxIterations | 迭代上限(while 时必填,source 时选填)         | Loop\_\*     | 不适用      | 不适用  | 不适用        | 不适用  | 不适用       | 不适用    | 不适用  | 不适用    | 不适用     | 选填     | 不适用   | 不适用     |
+| routeByExt | 按后缀路由目录映射                                | Download/Unzip | 不适用    | 不适用  | 不适用        | 不适用  | 选填         | 必填      | 不适用  | 不适用    | 不适用     | 不适用   | 不适用   | 不适用     |
+| defaultDir | 路由未命中时的兜底目录                            | Download/Unzip | 不适用    | 不适用  | 不适用        | 不适用  | 选填         | 选填      | 不适用  | 不适用    | 不适用     | 不适用   | 不适用   | 不适用     |
+| overwrite | 解压覆盖策略(默认 true)                          | Unzip\_\*     | 不适用    | 不适用  | 不适用        | 不适用  | 不适用       | 选填      | 不适用  | 不适用    | 不适用     | 不适用   | 不适用   | 不适用     |
+| target   | 状态写入目标(变量路径或文件路径)                  | 状态写入节点 | 不适用      | 不适用  | 不适用        | 不适用  | 选填         | 不适用    | 必填    | 必填      | 不适用     | 不适用   | 不适用   | 不适用     |
+| value    | 写入内容/值(表达式)                               | 状态写入节点 | 不适用      | 不适用  | 不适用        | 不适用  | 不适用       | 不适用    | 必填    | 必填      | 不适用     | 不适用   | 不适用   | 不适用     |
+| children | 并行子分支入口列表(join 后再走 next)              | 除Stop_*     | 选填        | 选填    | 选填          | 选填    | 选填         | 选填      | 选填    | 选填      | 选填       | 必填     | 不适用   | 不适用     |
+| next     | 串行后继节点, 除Stop_*外必填                        | 除Stop_*     | 必填        | 必填    | 必填          | 必填    | 必填         | 必填      | 必填    | 必填      | 必填       | 必填     | 不适用   | 必填       |
+| cases    | 分支规则列表                                        | Branch\_\*   | 不适用      | 不适用  | 不适用        | 不适用  | 不适用       | 不适用    | 不适用  | 不适用    | 必填       | 不适用   | 不适用   | 不适用     |
+| mode     | 执行/写入模式                                       | Loop / Write | 不适用      | 不适用  | 不适用        | 不适用  | 不适用       | 不适用    | 不适用  | 选填      | 不适用     | 必填     | 不适用   | 不适用     |
+| meta     | 元信息(展示/追踪,不参与执行语义)                 | 全部         | 选填        | 选填    | 选填          | 选填    | 选填         | 选填      | 选填    | 选填      | 选填       | 选填     | 选填     | 选填       |
+| print    | 节点结束后输出自定义消息(流式事件)                | 除Stop_*     | 选填        | 选填    | 选填          | 选填    | 选填         | 选填      | 选填    | 选填      | 选填       | 选填     | 不适用   | 不适用     |
+| reason   | 暂停等待原因文案(前端/通知/日志展示)             | Pause\_\*    | 不适用      | 不适用  | 不适用        | 不适用  | 不适用       | 不适用    | 不适用  | 不适用    | 不适用     | 不适用   | 不适用   | 选填       |
+| resumeResultTarget | resume payload 写入的 \$vars 路径  | Pause\_\*    | 不适用      | 不适用  | 不适用        | 不适用  | 不适用       | 不适用    | 不适用  | 不适用    | 不适用     | 不适用   | 不适用   | 必填       |
+| timeout  | 超时配置(sec: 秒数, on: 超时后继节点)            | Pause\_\*    | 不适用      | 不适用  | 不适用        | 不适用  | 不适用       | 不适用    | 不适用  | 不适用    | 不适用     | 不适用   | 不适用   | 选填       |
+
+\* `in` 对 `Service_* / API_* / Component_* / LLM_*`:语法上可选,但调用型节点通常应填写(建议实现侧强制)。
+
+> `Stop_*`:不需要 `next/children`,到达即终止;若出现也应视为非法或忽略(建议非法)。
+
+## 5.2 属性详解
+
+本节逐个定义step 的属性语义、约束与推荐写法。
+
+### 5.2.1 `id`
+
+* **类型**:`string`
+* **必填**:是(每个 step 必须有)
+* **作用**:节点唯一标识,用于 `next / children` 等引用;同时用前缀表达节点类型,因此 **不再需要 `kind`**
+
+**前缀约束(强制):**
+
+id 前缀是引擎识别节点类型的 **唯一依据**。不符合以下已定义前缀的 id 视为非法,引擎应报错:
+
+* `Service_xxx`
+* `API_xxx`
+* `Component_xxx`
+* `LLM_xxx`
+* `Download_xxx`
+* `Unzip_xxx`
+* `Set_xxx`
+* `Write_xxx`
+* `Branch_xxx`
+* `Loop_xxx`
+* `Stop_xxx`
+* `Pause_xxx`
+
+**示例:**
+
+`{ "id": "Service_Approval", "next": "Branch_CheckAmount" }`
+
+### 5.2.2 `if`
+
+* **类型**:`string`(表达式)
+* **必填**:否
+* **作用**:控制"是否执行该节点及其子树(children)"
+
+**规则:**
+
+* `if` 不写 ⇒ 默认执行
+* `if` 求值为 `true` ⇒ 执行该节点本体,并按正常规则执行 `children`,最后进入 `next`
+* `if` 求值为 `false` ⇒ **跳过该节点本体 + 跳过该节点的所有 `children` 子树**,然后直接进入next
+
+**示例:**
+
+```Plain
+{
+  "id": "Service_SendNotify",
+  "if": "=$needNotify == true",
+  "in": { "msg": "=$text" },
+  "next": "Stop_End"
+}
+```
+
+### 5.2.3 `in`
+
+* **类型**:`object`
+* **必填**:对 `Loop_*` 为必填;对 `Service_* / API_* / Component_* / LLM_*` 通常必填
+* **作用**:节点输入参数。不同节点类型的 `in` 结构不同。
+
+---
+
+#### 规则(通用)
+
+* `in` 是一个键值对象:`{ key: value }`
+* `value` 可以是:
+  * 常量(字符串/数字/布尔/对象/数组)
+  * 全局变量引用:`$xxx`
+  * 系统变量引用:`SYSVAR.xxx`
+  * 局部变量引用:`_item / _index / _result / _meta / _error`
+  * 表达式(按表达式规则求值)
+* 若节点被 `if=false` 跳过,则该节点的 `in` 不会被执行/求值
+
+---
+
+#### `Service_*` 的 `in`
+
+* `in` 的字段必须匹配 `registry.services` 中该服务的入参签名
+* 允许少字段(若服务入参允许缺省),但建议引擎做静态校验
+
+示例:
+
+```Plain
+{
+  "id": "Service_PlannerService",
+  "in": { "prd": "=$prdText", "rulesFile": "Process/Rules/rules.txt" },
+  "out": "$plan",
+  "next": "LLM_Generate"
+}
+```
+
+---
+
+#### `Component_*` 的 `in`
+
+* `in` 结构由系统组件定义(不在 registry 重复声明)
+* 同样允许引用 `$vars / SYSVAR / _item / _index`
+
+示例:
+
+```Plain
+{
+  "id": "Component_MCP_Search",
+  "in": { "query": "=$keyword", "topK": 5 },
+  "out": "$hits"
+}
+```
+
+---
+
+#### `LLM_*` 的 `model`(选填,3.16 更新)
+
+`LLM_*` 节点通过 `model` 字段选择 provider/model,支持三种写法:
+
+* 不写 `model`:使用环境变量 `LLM_MODEL`(格式 `<provider>/<modelId>`)
+* `model: "<provider>"`:仅指定 provider,`modelId` 从环境变量 `<PROVIDER>_MODEL` 获取
+* `model: "<provider>/<modelId>"`:节点内完全指定
+
+校验规则:
+
+* `model` 写了但为空字符串 → 编译错误
+* `model` 包含 `/` 但左右任一侧为空(如 `/gpt-4.1`、`openai/`)→ 编译错误
+* 解析出的 provider 不受支持 → 运行时错误
+* 对应 provider 的 API key 缺失 → 运行时错误
+* 仅写 provider 但无 `<PROVIDER>_MODEL` → 运行时错误
+* 不写 model 且无 `LLM_MODEL` → 运行时错误
+
+---
+
+#### `LLM_*` 的 `in`
+
+* `in` 结构由 LLM 调用协议定义(如 `messages / stream / output_config`)
+* 支持结构化输出(`output_config`):通过 `output_config.format.type = "json_schema"` 配合 `format.schema`,可强制 LLM 输出严格符合指定 JSON Schema 的内容。引擎自动将返回的 JSON parse 为对象绑定到 `_result`
+* `output_config` 为可选字段。workflow 统一写 `in.output_config`,引擎按 provider 做参数映射,不要求作者写各厂商原生字段
+* 支持流式输出(`in.stream: true`)
+
+##### `output_config` 字段定义
+
+`in.output_config` 为可选字段,仅适用于 `LLM_*` 节点。该字段用于声明输出约束;其中 `format` 是规范定义字段。
+
+跨 provider 兼容规则:
+
+* workflow 只写统一 `in.output_config`
+* `openai/*`:引擎映射到 OpenAI 的 `response_format`
+* `anthropic/*`:引擎映射到 Anthropic 对应输入结构(保持 `output_config.format` 语义)
+* 若目标 provider 不支持对应能力或无法映射,运行时错误(不得静默降级)
+
+统一字段结构:
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `output_config` | `object` | 否 | 指定 LLM 输出格式约束。不填则为自由文本输出 |
+| `output_config.format` | `object` | 是(当 `output_config` 存在时) | 输出格式配置对象 |
+| `output_config.format.type` | `string` | 是 | 支持 `"text"`、`"json_object"`、`"json_schema"` 三个值。`"text"` 为默认行为(自由文本),无需显式声明 |
+| `output_config.format.schema` | `object` | 是(当 `type` 为 `"json_schema"` 时) | 标准 JSON Schema 对象,定义输出的结构约束。**必须包含 `"additionalProperties": false`** |
+| `output_config.format.schemaRef` | `string` | 是(当 `type` 为 `"json_schema"` 且未提供 `schema` 时) | 引用 `registry.schemas` 中已声明的 schemaId。执行前引擎必须展开为 `format.schema`;若找不到引用应报错 |
+
+`json_object` 模式新增约束:
+
+* `type = "json_object"` 时,不得提供 `schema` 或 `schemaRef`
+* 若 `type = "json_object"` 且同时提供 `schema` / `schemaRef`,引擎应报 `bad_request`
+* `json_object` 与 `json_schema` 语义互斥(由 `type` 单选表达)
+
+##### LLM 节点运行时上下文(3.10)
+
+`LLM_*` 节点执行时,引擎统一提供三元组上下文:
+
+| 字段 | 语义 | 成功时 | 失败时 |
+|------|------|--------|--------|
+| `_result` | 业务内容本体 | 必有 | 不参与正常流程 |
+| `_meta` | 调用元信息(usage/model/latency/request_id 等) | 必有 | 可有 |
+| `_error` | 失败信息(标准化错误对象) | 无 | 必有 |
+
+其中 `_result` 规则固定如下:
+
+| 场景 | `_result` 类型 | 说明 |
+|------|---------------|------|
+| 无 `output_config`(或 `format.type: "text"`) | `string` | 自由文本输出,正文直接存于 `_result` |
+| `output_config.format.type = "json_object"` | **已解析的 JSON 对象/数组** | 引擎 parse 后直接绑定,不做 schema 校验。`out` 可路径访问,但不保证字段存在性/类型 |
+| `output_config.format.type = "json_schema"` | **已解析且通过校验的 JSON 对象/数组** | `out` 中可直接路径访问(如 `=_result.score`) |
+
+引擎保证:
+
+* 当 `type = "json_object"` 时,`_result` 一定是可解析 JSON;parse 失败时 `_result = null` 且 `_error.type = "json_parse_error"`
+* `json_object` 模式不保证字段存在性;下游路径访问前应做防御性判断,或使用 `onError` 兜底
+* 当 `type = "json_schema"` 时,`_result` 一定是合法且符合 schema 的对象(或数组),无需下游再次 parse 或校验
+
+##### 与 `stream` 的兼容性
+
+`output_config` 与 `in.stream: true` 可同时使用:
+
+- 流式传输期间,引擎逐步接收 JSON 片段
+- 流式完成后,引擎将完整 JSON 文本 parse 为对象,再绑定到 `_result`
+- `out` 映射始终在流式完成后执行,因此 `_result/_meta/_error` 在 `out` 求值时均为最终态
+
+##### 引擎映射规则
+
+引擎对 `in.output_config` 执行统一抽象映射:
+
+* workflow 层统一写 `in.output_config`
+* OpenAI provider:映射到 `response_format`
+* Anthropic provider:映射到对应输入结构(保持 `output_config.format` 语义)
+* 无法映射时,运行时错误(不得静默降级)
+
+示例(结构化输出 — Anthropic Claude):
+
+```json
+{
+  "id": "LLM_Generate",
+  "in": {
+    "stream": true,
+    "messages": [
+      { "role": "system", "content": "Generate a project plan based on the PRD." },
+      { "role": "user", "content": "=$prdText" }
+    ],
+    "output_config": {
+      "format": {
+        "type": "json_schema",
+        "schema": {
+          "type": "object",
+          "properties": {
+            "title": { "type": "string" },
+            "phases": {
+              "type": "array",
+              "items": {
+                "type": "object",
+                "properties": {
+                  "name": { "type": "string" },
+                  "tasks": { "type": "array", "items": { "type": "string" } }
+                },
+                "required": ["name", "tasks"],
+                "additionalProperties": false
+              }
+            }
+          },
+          "required": ["title", "phases"],
+          "additionalProperties": false
+        }
+      }
+    }
+  },
+  "out": {
+    "$planTitle": "=_result.title",
+    "$planPhases": "=_result.phases"
+  },
+  "next": "Service_Validate"
+}
+```
+
+示例(Loop 中使用结构化输出):
+
+```json
+{
+  "id": "LLM_ExtractEntity",
+  "in": {
+    "messages": [
+      { "role": "system", "content": "Extract structured entity information from the given text." },
+      { "role": "user", "content": "=_item.rawText" }
+    ],
+    "output_config": {
+      "format": {
+        "type": "json_schema",
+        "schema": {
+          "type": "object",
+          "properties": {
+            "name": { "type": "string" },
+            "type": { "type": "string", "enum": ["person", "org", "location", "other"] },
+            "confidence": { "type": "number" }
+          },
+          "required": ["name", "type", "confidence"],
+          "additionalProperties": false
+        }
+      }
+    }
+  },
+  "out": {
+    "$entities[_index]": "=_result"
+  },
+  "next": "Write_SaveEntity"
+}
+```
+
+示例(自由文本输出,与 3.6 行为一致):
+
+```json
+{
+  "id": "LLM_FreeChat",
+  "in": {
+    "stream": true,
+    "messages": [
+      { "role": "user", "content": "=$userQuestion" }
+    ]
+  },
+  "out": "$answer",
+  "next": "Stop_End"
+}
+```
+
+示例(`json_object`:要求合法 JSON,不约束结构):
+
+```json
+{
+  "id": "LLM_Analyze",
+  "in": {
+    "messages": [
+      { "role": "system", "content": "Analyze and return JSON with your own structure." },
+      { "role": "user", "content": "=$inputText" }
+    ],
+    "output_config": {
+      "format": { "type": "json_object" }
+    }
+  },
+  "out": { "$analysis": "=_result" },
+  "next": "Stop_End"
+}
+```
+
+不写 `output_config` 即表示自由文本输出,`_result` 为文本正文字符串。
+
+三种模式对比:
+
+```json
+// 自由文本(默认,不写 output_config)
+{ "id": "LLM_Chat", "in": { "messages": [...] } }
+
+// JSON Object 模式(有结构但不约束 schema)
+{ "id": "LLM_Analyze", "in": { "messages": [...], "output_config": { "format": { "type": "json_object" } } } }
+
+// JSON Schema 模式(强约束结构)
+{ "id": "LLM_Extract", "in": { "messages": [...], "output_config": { "format": { "type": "json_schema", "schema": { ... } } } } }
+```
+
+**选型指南:**
+
+| 场景 | 推荐模式 |
+|------|----------|
+| 对话、摘要、生成类任务,下游直接展示 | `text`(默认) |
+| 需要 JSON 输出,但结构灵活或由 prompt 约定 | `json_object` |
+| 需要 JSON 输出,且下游强依赖特定字段(如 `=_result.score`) | `json_schema` |
+| 下游需要类型安全的路径访问,且字段稳定 | `json_schema` |
+
+---
+
+示例(`onError` + `_meta` + `_error` 联合使用):
+
+```json
+{
+  "id": "LLM_GenSpec",
+  "in": {
+    "messages": [{ "role": "user", "content": "..." }],
+    "output_config": { "format": { "type": "json_schema", "schemaRef": "SpecSchema" } }
+  },
+  "out": {
+    "$spec": "=_result",
+    "$title": "=_result.title",
+    "$tokens": "=_meta.usage.total_tokens"
+  },
+  "onError": "Set_LogError",
+  "next": "Stop_End"
+}
+```
+
+```json
+{ "id": "Set_LogError", "target": "$runState.lastError", "value": "=_error", "next": "Stop_End" }
+```
+
+> 说明:`onError` 指向的节点在当前节点失败时执行,此时 `_error` 可在该节点的 `value` 表达式中读取。成功路径走 `next`,失败路径走 `onError`,两者互斥。
+
+---
+
+#### `API_*` 的 `in`
+
+`API_*` 节点的 `in` 用于描述 HTTP 请求的参数。引擎根据 `registry.apis` 中声明的端点信息与节点 `in` 的内容,构造并发起 HTTP 请求。
+
+`in` 支持以下字段:
+
+| 字段       | 类型            | 说明                                                                  |
+| ---------- | --------------- | --------------------------------------------------------------------- |
+| body       | object / string | 请求体(POST/PUT/PATCH 时使用)                                       |
+| query      | object          | URL 查询参数(键值对)                                                |
+| pathParams | object          | 路径参数(替换 URL 中的 `{xxx}`占位符)                             |
+| headers    | object          | 本次请求额外的请求头(与 registry 中声明的 headers 合并,节点级优先) |
+| timeout    | number          | 本次请求超时时间(毫秒),可选                                        |
+
+示例:
+
+```Plain
+{
+  "id": "API_StripeCreateCharge",
+  "in": {
+    "body": {
+      "amount": "=$orderAmount",
+      "currency": "usd",
+      "source": "=$paymentToken"
+    }
+  },
+  "out": { "$chargeId": "=_result.id", "$chargeStatus": "=_result.status" },
+  "next": "Branch_CheckPayment"
+}
+```
+
+```Plain
+{
+  "id": "API_WeatherQuery",
+  "in": {
+    "query": { "q": "=$cityName", "key": "=SYSVAR.weatherKey" }
+  },
+  "out": "$weather",
+  "next": "LLM_AnalyzeWeather"
+}
+```
+
+```Plain
+{
+  "id": "API_SlackPostMessage",
+  "in": {
+    "body": {
+      "channel": "=$slackChannel",
+      "text": "=$notifyMessage"
+    }
+  },
+  "out": "$slackResult",
+  "next": "Stop_End"
+}
+```
+
+**执行语义:**
+
+* 引擎根据 `registry.apis` 中对应 id 的 `method / url / auth / headers` 构造请求
+* `in.pathParams` 用于替换 URL 中的路径占位符(如 `https://api.example.com/orders/{orderId}` → `https://api.example.com/orders/12345`)
+* `in.query` 拼接为 URL 查询字符串
+* `in.body` 作为请求体发送(JSON 序列化或按 Content-Type 处理)
+* `in.headers` 与 registry 声明的固定 headers 合并(节点级优先)
+* `auth` 凭据由引擎自动注入到请求头(如 `Authorization: Bearer xxx`),节点无需手动处理
+* 请求完成后,HTTP 响应体(JSON 解析后)作为 `_result` 供 `out` 映射使用
+* HTTP 状态码非 2xx 时,节点视为执行失败,workflow 进入 `failed` 状态(引擎可按实现提供更细粒度的错误码判断)
+
+---
+
+### 5.2.4 `out`
+
+`out` 用于定义节点执行完成后的**输出映射**。
+
+---
+
+#### 两种写法
+
+`out` 支持两种写法:
+
+**A) 简写形式(string)**
+
+`"out": "$plan"` — 等价于 `{ "$plan": "=_result" }`,即将完整 `_result` 写入指定变量。
+
+适用于节点只需要把完整输出存入一个变量的简单场景。
+
+**B) 完整形式(object)**
+
+`"out": { "$plan.status": "=_result.ok", "/path/file": "=_result" }` — 逐字段映射。
+
+每一项都是一个映射规则:**key 表示写入目标**,**value 表示取值来源**(通常来自 `_result`、`_item` 等运行时上下文)。
+
+---
+
+#### 变量写入(以 `$` 开头)
+
+当 `out` 的 key 以 `$` 开头时,表示将值写入变量空间。
+
+* 变量名支持层级路径(如 `$draft.content`)
+* 支持数组/索引写入(如 `$generated[_index].name`)
+* 常用于节点间传参、控制信息、统计信息等
+
+---
+
+#### 文件写入(以 `/` 开头)
+
+当 `out` 的 key 以 `/` 开头时,表示将值写入文件空间中的某个文件路径。
+
+* 文件路径可使用运行时上下文变量进行插值(例如包含 `_item`、`_index` 等)
+* 适用于 loop 场景下按条目生成/修改多份文件
+* 文件最终写入位置取决于本次 run 是否指定了 workspace:
+  * 未指定:写入系统创建的临时文件空间(有 TTL)
+  * 指定:写入该 workspace 文件空间
+* 文件写入路径必须落在 `registry.files.artifacts` 范围内
+
+> 重要:文件写入由**引擎层**执行。节点只通过 `out` 声明"写入结果",不直接对文件系统进行写操作。
+
+---
+
+#### `out` 文件写入与 `Write_*` 节点的使用边界
+
+两者功能有重叠,但适用场景不同:
+
+* **`out` 文件写入**:适用于调用型节点直接将 `_result` 中的某部分落盘,路径与内容在同一步完成
+* **`Write_*`**:适用于需要独立控制写入时机、写入策略(append / failIfExists)或写入内容来自非 `_result` 的场景(如从 `$vars` 中取值写入文件)
+
+---
+
+#### value 取值来源(右值表达式)
+
+`out` 的 value 采用路径引用方式,从运行时上下文中取值,常见来源包括:
+
+* `_result.*`:当前节点的执行结果
+* `_item.*`:loop 场景下当前迭代项
+* `_index`:loop 场景下当前迭代下标
+* 以及其他已存在的运行时上下文对象(按平台约定)
+
+---
+
+#### 执行语义与约束
+
+* `out` 只描述"输出映射规则",不描述执行顺序
+* 当 key 为文件路径时,引擎负责:
+  * 在写入前做必要的安全与一致性校验
+  * 执行实际落盘(临时空间或 workspace)
+  * 记录必要的变更信息用于调试与审计
+* 若同一次节点输出对同一变量或同一路径发生冲突,按平台的冲突处理规则执行(建议默认以本节点输出为准或直接报错,具体策略由引擎统一定义)
+
+#### 示例:
+
+```JSON
+{
+  "id": "LLM_GenFilesInLoop",
+  "in": {
+    "messages": [
+      { "role": "system", "content": "Generate file content for the given spec." },
+      { "role": "user", "content": "=_item.specText" }
+    ]
+  },
+  "out": {
+    // ===== 变量写入,以$开始 =====
+    "$generated[_index].name": "=_item.name",
+    "$generated[_index].tokens": "=_meta.usage.total_tokens",
+
+    // ===== 文件写入,以/开始(key 用文件路径,支持 _index / _item 变量插值)=====
+    // 表示:把本次 _item 对应的文件内容写入到 /vsc 目录下
+    "/vsc/{_item.path}": "=_result"
+
+    // 例:若 _item.path = "files/fileA.ts",
+    // 则引擎会把 _result 写入 /vsc/files/fileA.ts
+  },
+  "next": "API_CustomLogic"
+}
+```
+
+### 5.2.5 `Loop_*` 的 `source` 与 `while`
+
+`Loop_*` 支持两种互斥模式:
+
+* `source` 模式:数组遍历
+* `while` 模式:条件循环
+
+互斥与约束:
+
+* `source` 与 `while` 不得同时出现(编译错误)
+* `while` 模式下 `maxIterations` 必填且 `mode` 必须为 `"serial"`
+* `source` 模式下 `maxIterations` 选填(若填写,迭代上限 `min(len(source), maxIterations)`)
+
+#### A) `source` 模式
+
+* `source`:循环数据源表达式,求值必须是数组
+
+```Plain
+{
+  "id": "Loop_ForItems",
+  "mode": "parallel",
+  "source": "=$items",
+  "children": ["LLM_GenOne", "Write_One"],
+  "next": "Stop_End"
+}
+```
+
+#### B) `while` 模式(3.16 新增)
+
+* `while`:每轮 children 执行前求值;true 继续,false 退出
+* `maxIterations`:循环上限(integer,>=1)
+* while 模式局部变量:`_index`、`_iterDir` 可用;`_item` 不可用
+* `if` 先于 while 求值:`if=false` 时整个 Loop 跳过,不执行 while 判定
+
+```json
+{
+  "id": "Loop_review",
+  "while": "=$review.approved != true",
+  "maxIterations": 5,
+  "mode": "serial",
+  "children": ["LLM_Generate"],
+  "next": "Write_final"
+}
+```
+
+#### 5.2.5.C `Download_*`/`Unzip_*` 路径与来源约束
+
+`Download_*`:
+
+* `source` 必填,可为 URL 字符串或对象
+* `target` 与 `routeByExt` 二选一
+* `routeByExt` 未命中时可用 `defaultDir` 兜底
+* `source` 为对象时建议包含:`url`(必填)/`headers`/`auth`/`timeout`/`checksum`
+
+`Unzip_*`:
+
+* `source` 必填,必须是 zip 文件路径(表达式字符串)
+* `routeByExt` 必填,按后缀分流到目标目录
+* `defaultDir` 选填,`overwrite` 选填(默认 `true`)
+
+通用约束:
+
+* 必须流式处理(下载流式、解压逐 entry)
+* 必须防止 zip-slip(`../`、绝对路径、盘符跳转)
+* 文件正文不得整体落入 `$vars`,变量仅保存路径或摘要
+* 路径必须落在 `registry.files.artifacts` 范围
+
+与 `Write_*` 的边界与协作:
+
+* `Write_*` 仅负责将 `value` 写入 `target`,不负责外部下载与解压
+* 单文件下载场景:`Download_*` 可直接按后缀分流落盘;如需补丁写入,再接 `Write_*`
+* zip 场景:先 `Download_*` 到 artifacts(建议 `.tmp/...`),再 `Unzip_*` 分流;若需二次加工,再接 `Write_*`
+
+### 5.2.6 `target` & `value`
+
+
+`target` 与 `value` 是\*\*状态写入节点(State Nodes)\*\*的核心属性,适用于:
+
+* `Set_*`(写全局变量)
+* `Write_*`(写临时文件)
+
+---
+
+#### `target`
+
+* **类型**:`string`
+* **必填**:是(`Set_* / Write_*` 必填)
+* **作用**:写入目标
+
+##### 对 `Set_*`
+
+* `target` 必须是全局变量路径:
+  * `$var`
+  * `$var.field`
+  * `$arr[_index]`
+  * `$arr[_index].field`
+
+示例:
+
+`{ "id": "Set_Status", "target": "$plan.status", "value": "\"draft\"" }`
+
+##### 对 `Write_*`
+
+* `target` 必须是临时文件路径(字符串或表达式字符串)
+* 写入目标必须落在 `registry.files.artifacts` 范围内
+
+示例:
+
+```Plain
+{
+  "id": "Write_ComponentFile",
+  "target": "=\"Process/Artifacts/components/\" + _item.name + \".tsx\"",
+  "value": "=$componentCode"
+}
+```
+
+---
+
+#### `value`
+
+* **类型**:`string`(表达式)
+* **必填**:是
+* **作用**:写入内容/值
+
+##### 对 `Set_*`
+
+* `value` 求值后写入 `target` 指向的变量路径
+
+示例:
+
+```Plain
+{
+  "id": "Set_Count",
+  "target": "$count",
+  "value": "=$items.length"
+}
+```
+
+##### 对 `Write_*`
+
+* `value` 求值后作为文件内容写入 `target` 指向的临时文件
+
+示例:
+
+```Plain
+{
+  "id": "Write_PlanJson",
+  "target": "\"Process/Artifacts/plan.json\"",
+  "value": "=$plan"
+}
+```
+
+> 说明:若写入 JSON 文件,推荐 `value` 为对象/数组(由引擎序列化),或由调用方显式 `toJson(...)` 后写入文本(取决于表达式系统是否内置该函数)。
+
+### **5.2.7 `children`**
+
+* **类型**:`string[]`(step `id` 列表)
+* **必填**:否(但对 `Loop_*` 为必填)
+* **作用**:声明当前节点的并行子分支入口(并行扇出)。
+
+#### 规则
+
+##### 1) 并行执行(fan-out)
+
+* 当执行到包含 `children` 的节点时,引擎会**并行启动** `children` 中列出的所有入口节点。
+* `children` 仅表达**控制流并行**,不隐含数据传递语义。
+
+##### 2) 子分支完成与 `RETURN`
+
+* 每个 `children` 分支从其入口节点开始,沿自身的 `next / children / Branch_* / Loop_*` 执行完整控制流链。
+* 当某个分支执行到 **`next: "RETURN"`** 时:
+  * 表示该分支 **正常完成**
+  * 控制流返回到父节点的 `children` join
+
+##### 3) join 汇合规则
+
+* 父节点会等待 **所有 `children` 分支均完成(RETURN)** 后,才进入父节点自身的 `next`。
+* `children` 的完成顺序不做保证。
+
+##### 4) 异常与终止传播
+
+在任一 `children` 分支中发生以下情况时,行为如下:
+
+* 到达 `Stop_*`
+  → 整个 workflow 立即终止(`stopped`)
+* 节点执行失败
+  → 整个 workflow 进入 `failed`
+* 执行到 `Pause_*` 节点
+  → 整个 workflow 进入 `paused`,等待 resume
+
+##### 5) 与 `if` 的关系
+
+* 若父节点 `if=false`:
+  * 父节点及其整个 `children` 子树均被跳过
+  * 直接进入父节点的 `next`
+* `children` 内部节点的 `if`:
+  * 仅影响该分支内部执行
+  * 不影响其他分支
+
+#### 示例(并行分支完成后返回 join)
+
+```
+{
+  "id": "Service_GenerateParts",
+  "children": ["LLM_GenA", "LLM_GenB"],
+  "next": "Service_Merge"
+}
+
+{
+  "id": "LLM_GenA",
+  "next": "RETURN"
+}
+
+{
+  "id": "LLM_GenB",
+  "next": "RETURN"
+}
+```
+
+### **5.2.8 `next`**
+
+* 类型:`string`
+* 必填性:**必填(除 `Stop_*` 节点外)**
+* 作用:显式声明该节点执行完成后的控制流行为。
+
+#### 规则
+
+##### 单后继
+
+* `next` 只能有一个,表示唯一的后继控制流。
+* 多路径控制需使用 `Branch_*` 或 `children`。
+
+##### 合法取值
+
+`next` 的取值必须显式表达控制流语义,合法取值包括:
+
+* `"<stepId>"`:线性推进到指定后继节点。
+* `"RETURN"`:结束当前执行分支,返回最近一层父执行上下文(join)。
+* `"BREAK"`:结束当前迭代并退出整个 `Loop_*`,跳转到 `Loop_*.next`(仅在 `Loop_*` children 子树内有效)。
+
+`"RETURN"` 与 `"BREAK"` 为保留关键字,不得作为 stepId 使用。
+
+`BREAK` 规则:
+
+* 仅可用于 `Loop_*` 的 children 子树;在 Loop 外使用为编译错误
+* 适用于 `source` 与 `while` 两种模式
+* 在 `mode:"parallel"` 下,触发 `BREAK` 后未启动迭代不再启动,已启动迭代继续到自然结束,然后进入 `Loop_*.next`
+
+> 可恢复的暂停不通过 `next` 的魔法值表达,而是通过节点类型 `Pause_*` 显式声明(见第十一章)。
+
+##### 控制流完整性
+
+* **除 `Stop_*` 节点外,所有节点必须显式声明 `next`。**
+* 不允许通过缺失 `next` 推断 pause、return 或 stop 语义。
+* 非 `Stop_*` 节点缺失 `next`,属于 Spec 校验错误。
+
+#### 示例
+
+```Plain
+{
+  "id": "Service_PlannerService",
+  "next": "LLM_Generate"
+}
+```
+
+### 5.2.9 `cases`(仅 `Branch_*`)
+
+* **类型**:`array`
+* **必填**:是(仅适用于 `Branch_*`)
+* **作用**:定义条件分支入口;运行时只会选择其中一个入口节点执行。
+
+---
+
+#### 格式
+
+`cases` 是一个二维数组,每一项是:
+
+`["<whenExpr>", "<stepId>"]`
+
+* `<whenExpr>`:条件表达式字符串,或固定字符串 `"ELSE"`
+* `<stepId>`:该分支的入口节点 `id`
+
+示例:
+
+```Plain
+"cases": [
+  ["=$amount < 500", "Service_ManagerApprove"],
+  ["ELSE", "Service_ManagerAndDirectorApprove"]
+]
+```
+
+---
+
+#### 规则
+
+1. **单入口规则** 每个规则只能指定 **一个入口节点**(一个 stepId)。
+2. **选择规则** 按顺序判断:
+
+* 第一个 `<whenExpr>` 求值为 `true` 的分支被选中
+* `"ELSE"` 必须放最后,作为兜底分支
+
+3. **执行语义(重要)** 命中某个 case 后:
+
+* 从该 case 的入口节点开始执行一整条分支链(沿该入口节点自己的 `next/children/branch/loop` 继续执行)
+* 只有当该分支链执行结束(到达 `Stop_*` 或遇到 `next: "RETURN"`)才算该 case 完成
+* case 完成后,才进入 `Branch_*` 节点自身的 `next`
+
+4. **未命中的 case 不执行** 其它 case 完全不执行。
+
+### 5.2.10 `mode`
+
+* **类型**:`string`
+* **必填**:对 `Loop_*` 必填;对 `Write_*` 选填;其他节点不适用
+* **作用**:声明该节点的执行/写入策略(不同节点含义不同)。
+
+---
+
+#### `Loop_*` 的 `mode`(必填)
+
+用于指定循环的执行方式:
+
+* `"parallel"`:各次迭代可并行执行
+* `"serial"`:按迭代顺序串行执行
+* 若使用 `while` 模式,则 `mode` 必须为 `"serial"`
+
+示例:
+
+```Plain
+{
+  "id": "Loop_ForItems",
+  "mode": "parallel",
+  "source": "=$items",
+  "children": ["Service_HandleOne"],
+  "next": "Stop_End"
+}
+```
+
+---
+
+#### `Write_*` 的 `mode`(选填)
+
+用于指定写文件时的写入策略:
+
+* `"overwrite"`:覆盖写(默认)
+* `"failIfExists"`:若已存在则报错
+* `"append"`:追加写(新内容拼接到末尾)
+* `"prepend"`:前置写(新内容拼接到开头)
+
+约束:
+
+* `append/prepend` 属于读改写(read-modify-write)语义,适用于文本内容(或实现明确支持的字节拼接)
+* 目标文件不存在时,`append/prepend` 均应按新建文件处理
+* 同一路径并发 `append/prepend` 不保证顺序;需由上层流程避免并发冲突
+
+示例:
+
+```Plain
+{
+  "id": "Write_Log",
+  "mode": "append",
+  "target": "\"Process/Artifacts/log.txt\"",
+  "value": "=$line"
+}
+```
+
+### 5.2.11 `meta`
+
+* **类型**:`object`
+* **必填**:否
+* **作用**:用于展示/调试/追踪的元信息,不参与执行语义。
+
+示例:
+
+```Plain
+{
+  "id": "LLM_GenerateUI",
+  "meta": { "title": "Generate UI Map", "tags": ["gen", "ui"], "owner": "agent-ui" },
+  "in": { "messages": [ { "role": "user", "content": "=$prdText" } ] },
+  "out": { "$uiMap": "=_result" }
+}
+```
+
+### 5.2.12 `print`
+
+* **类型**:`string`(表达式或字面字符串)
+* **必填**:否
+* **作用**:在节点执行完成后输出一条自定义消息,并由引擎发出 `run_events.type = "step_print"` 事件。
+
+规则:
+
+* `print` 仅在节点**实际执行且成功完成**时触发
+* 若节点因 `if=false` 被跳过,不触发 `print`
+* 若节点执行失败,不触发 `print`(仅发 `step_error`)
+* `Stop_*` 不允许 `print`
+* `print` 的求值结果应为字符串;若结果非字符串,引擎应按 JSON 序列化为字符串后输出
+
+建议:
+
+* 用于进度提示、阶段摘要、关键指标展示
+* 不用于传递最终业务结果(最终结果应通过 `out` / workflow 输出返回)
+
+示例:
+
+```json
+{
+  "id": "LLM_CheckVersion",
+  "in": {
+    "messages": [
+      { "role": "user", "content": "请返回文档版本号" }
+    ]
+  },
+  "out": { "$version": "=_result" },
+  "print": "=\"版本检查完成: \" + $version",
+  "next": "Stop_End"
+}
+```
+
+# 第六章:变量与作用域(Variables & Scope)
+
+## 6.1 变量类型总览
+
+workflow 中可使用的"数据引用"分为 7 类:
+
+| 写法 | 类型 | 可写 | 声明位置 |
+|------|------|------|----------|
+| `paramName` | 入参(Params) | 否 | `registry.params` |
+| `$xxx` | 全局变量(Global Vars) | 是 | `registry.vars` |
+| `SYSVAR.xxx` | 系统变量(System Vars) | 否 | 系统预配置 |
+| `_item` / `_index` | 循环局部变量(Loop Locals) | 否 | 自动注入 |
+| `_result` | 节点输出变量(Step Local Result) | 否 | 自动注入 |
+| `_meta` | 节点元信息变量(Step Local Meta) | 否 | 自动注入(LLM 节点) |
+| `_error` | 节点错误变量(Step Local Error) | 否 | 自动注入(节点失败时) |
+
+---
+
+## 6.2 入参 `params`
+
+### 6.2.1 定义方式
+
+入参在 `registry.params` 中声明,不带 `$` 前缀:
+
+`"params": ["userRequest(STRING)", "targetLang(STRING)", "maxRetries(INT) = 3"]`
+
+### 6.2.2 读写规则
+
+* **读取**:任何表达式位置都可以直接引用参数名(如 `=userRequest`)
+* **只读**:入参不可被 workflow 修改。不可出现在 `out` 左侧或 `Set_*` 的 `target` 中
+* 入参名不带 `$`,与 VL 语法中服务/方法入参的风格对齐
+
+---
+
+## 6.3 全局变量 `$vars`
+
+### 6.3.1 定义方式
+
+全局变量必须在 `registry.vars` 中声明(VL 风格):
+
+`"vars": ["$plans([OBJECT])", "$generated([OBJECT])", "$summary(STRING)"]`
+
+### 6.3.2 读写规则
+
+* **读取**:任何表达式位置都可以读 `$xxx`
+* **写入**:
+  * 调用型节点通过 `out` 写入(字段映射)
+  * 状态节点通过 `Set_*` 写入(`target/value`)
+* `$vars` 是 workflow 的主要数据传递方式(节点连线只表示控制流)
+
+### 6.3.3 并发注意事项(Loop parallel)
+
+当 `Loop_*` 使用 `mode:"parallel"` 时:
+
+* 允许写 `$vars`,但应避免多个迭代写入同一位置导致竞态
+* 推荐写法:按 `_index` 分槽写入,例如:
+  * `$generated[_index] = ...`
+  * `$files[_index].path = ...`
+
+---
+
+## 6.4 系统变量 `SYSVAR.xxx`
+
+### 6.4.1 定义
+
+`SYSVAR.xxx` 表示用户在系统空间预先配置的通用变量。
+
+### 6.4.2 规则
+
+* **只读**
+* 不需要在 `registry.vars` 声明
+* 可在任何表达式位置引用
+
+---
+
+## 6.5 Loop 局部变量 `_item` / `_index` / `_iterDir`
+
+当进入 `Loop_*` 的循环体(其 `children` 子树)时,引擎会为每次迭代提供:
+
+* `_item`:当前迭代元素
+* `_index`:当前迭代索引(从 0 开始)
+* `_iterDir`:当前迭代临时目录名,格式 `{loopNodeId}_{_index}`
+
+规则:
+
+* 仅在 loop 的子树范围内可用
+* 不需要声明
+* `_iterDir` 建议用于并行迭代下的临时路径隔离(例如 `.tmp/{_iterDir}/bundle.zip`)
+* 在 `mode:"serial"` 与 `mode:"parallel"` 下语义一致
+
+---
+
+## 6.6 `_result`(调用型节点输出)
+
+### 6.6.1 定义
+
+调用型节点(`Service_* / API_* / Component_* / LLM_*`)执行完成后,引擎将内容输出绑定为:
+
+* `_result`
+
+### 6.6.2 可用范围
+
+* `_result` 只在该节点的 `out` 映射求值阶段可用
+* 仅表示"当前节点的输出",不会跨节点存在
+
+### 6.6.3 `LLM_*` 结构化输出时的 `_result`
+
+`LLM_*` 节点中,`_result` 永远表示业务内容本体:
+
+* 文本输出:`_result` 为字符串正文
+* `json_schema` 输出:`_result` 为引擎自动解析并校验后的 JSON 对象/数组
+
+即使是文本场景,也不再要求通过 `=_result.content` 取正文。
+
+示例:
+
+```Plain
+{
+  "id": "Service_PlannerService",
+  "in": { "prd": "=$prdText" },
+  "out": { "$plan": "=_result", "$planId": "=_result.id" }
+}
+```
+
+## 6.7 `_meta`(LLM 节点元信息)
+
+### 6.7.1 定义
+
+`_meta` 为 `LLM_*` 节点的元信息对象,承载调用层信息,不承载业务正文。
+
+### 6.7.2 可用范围
+
+* `_meta` 只在当前节点 `out` 求值阶段可用
+* 仅表示当前节点执行元信息,不跨节点存在
+
+### 6.7.3 LiteLLM 推荐结构
+
+建议至少包含:
+
+* `provider`、`model`、`model_resolved`
+* `request_id`、`response_id`
+* `latency_ms`、`finish_reason`
+* `usage.input_tokens`、`usage.output_tokens`、`usage.total_tokens`
+* `cost`(若启用成本计算)
+
+约束:
+
+* usage 统一命名为 `input_tokens / output_tokens / total_tokens`
+* 厂商特有 usage 字段统一放在 `usage.raw`
+
+取值示例:
+
+* `=_meta.usage.total_tokens`
+* `=_meta.model_resolved`
+
+## 6.8 `_error`(节点失败信息)
+
+### 6.8.1 定义
+
+节点失败时,引擎将标准错误对象绑定到 `_error`。
+
+### 6.8.2 可用范围
+
+* `_error` 在失败路径中可读(如 `onError` 分支或失败处理节点)
+* 成功执行时 `_error` 不存在
+
+### 6.8.3 LiteLLM 推荐结构
+
+建议至少包含:
+
+* `type`(标准分类)
+* `code`、`message`、`retryable`、`status_code`
+* `provider`、`model`、`request_id`
+* `litellm_exception`
+* `details`、`raw`
+
+`type` 建议使用以下标准枚举:
+
+* `auth_error`
+* `bad_request`
+* `rate_limit`
+* `timeout`
+* `context_length_exceeded`
+* `content_policy_violation`
+* `service_unavailable`
+* `connection_error`
+* `json_parse_error`
+* `schema_validation_error`
+* `internal_error`
+* `unknown_error`
+
+# 第七章:表达式与路径语法(Expressions & Paths)
+
+## 7.1 表达式前缀规则(`=` 前缀)
+
+Workflow JSON 中的所有字符串值遵循统一规则:
+
+* **以 `=` 开头**:表达式,引擎对 `=` 后的内容进行求值
+* **不以 `=` 开头**:字面字符串,原样使用
+* **以 `==` 开头**:字面字符串,内容为 `=` 后的部分(用于极少数需要以 `=` 开头的字面字符串)
+
+示例:
+
+```Plain
+"msg": "hello"              → 字面字符串 "hello"
+"msg": "=$userRequest"      → 表达式,求值 userRequest 入参
+"msg": "=$text + ' world'"  → 表达式,字符串拼接
+"msg": "==formula"          → 字面字符串 "=formula"
+```
+
+该规则适用于所有 JSON 字符串值位置。引擎不再需要区分"哪些字段是表达式位置"——统一由 `=` 前缀决定。
+
+---
+
+## 7.2 表达式出现的位置
+
+以下属性中的字符串值通常需要使用 `=` 前缀:
+
+* `if`:条件表达式(如 `"=$count > 0"`)
+* 调用型节点 `in` 的字段值(引用变量时需 `=`,字面值不需要)
+* 调用型节点 `out` 的右侧取值表达式(如 `"=_result.id"`)
+* `Set_*` 的 `value`(如 `"=$plan.status"`)
+* `Write_*` 的 `target`(当为动态路径时,如 `"=\"Process/\" + _item.name"`)
+* `Write_*` 的 `value`(如 `"=$componentCode"`)
+* `Loop_*` 的 `source`(如 `"=$items"`)
+* `Branch_*` 的 `cases[*][0]`(when 表达式,如 `"=$amount < 500"`)
+* `print`(如 `"=\"当前阶段: \" + $phase"`)
+
+以下位置**不使用 `=` 前缀**(它们是目标路径或标识符,非求值表达式):
+
+* `out` 的左侧(写入目标路径,如 `"$plan"`、`"$generated[_index].name"`)
+* `out` 简写形式(如 `"out": "$plan"`,表示目标变量)
+* `Set_*` 的 `target`(写入目标路径,如 `"$plan.status"`)
+* `next` / `children` / `cases[*][1]`(步骤 ID)
+* `mode`、`model` 等配置值
+
+---
+
+## 7.3 表达式的最小能力集合
+
+表达式建议至少支持:
+
+* 入参引用:`userRequest`、`targetLang`(params,无 `$` 前缀,只读)
+* 全局变量引用:`$xxx`(vars,可读写)
+* 系统变量引用:`SYSVAR.xxx`(只读)
+* 上下文变量:`_item`、`_index`、`_result`、`_meta`、`_error`
+* 布尔逻辑:`&&`、`||`、`!`
+* 比较:`==`、`!=`、`>`、`>=`、`<`、`<=`
+* 字符串拼接:`+`
+* 数字运算:`+ - * /`
+* 括号:`(...)`
+* 基础属性访问:`.field`
+* 数组/字典索引:`[expr]`
+
+> 说明:表达式语言不需要复杂到脚本语言,保持可静态分析与可预测即可。
+
+---
+
+## 7.3 路径语法(关键规则)
+
+里"路径"既用于读(表达式),也用于写(`out` 左侧、`Set.target`)。
+
+### 7.3.1 点号 `.`:永远是字面字段名
+
+例如:
+
+* `$plan.status` 表示字段名就是 `"status"`
+* `_meta.usage.total_tokens` 表示逐级字段访问
+
+点号后面的内容**不会被当作变量解析**。
+
+---
+
+### 7.3.2 方括号 `[...]`:永远是表达式索引
+
+方括号内部必须按表达式解析。
+
+* `$generated[_index]`:`_index` 是变量(表达式求值)
+* `$arr[0]`:数字常量索引
+* `$obj[_item.name]`:表达式求值为 key
+
+---
+
+### 7.3.3 字面 key 的写法:`["literal"]`
+
+如果确实要访问 key 名为 `"_index"` 的字段(而不是变量),必须写成:
+
+* `$obj["_index"]`
+
+这样解析器完全不需要猜测。
+
+---
+
+## 7.4 `out` 的路径写入语义(deep-set)
+
+当 `out` 左侧是路径(如 `$plan.status`)时:
+
+* 引擎执行 deep-set 写入
+* 若中间对象不存在,可自动创建(实现可选,但建议支持)
+
+示例:
+
+`"out": { "$plan.status": "=_result.success" }`
+
+---
+
+## 7.5 Loop 中路径写入推荐
+
+Loop 并行场景推荐按 `_index` 分槽写入,避免覆盖:
+
+`"out": { "$generated[_index]": "=_result" }`
+
+或:
+
+`"out": { "$generated[_index].name": "=_item.name" }`
+
+# 第八章:临时文件 Artifacts(读写规则)
+
+本章定义workflow 空间内的\*\*临时文件(artifacts)\*\*的用途、路径约束、读写方式与生命周期规则。
+
+Artifacts 仅用于 workflow 内部的数据临时存储与步骤间传递,不等同于业务最终产出文件。
+
+---
+
+## 8.1 基本定义
+
+* **Artifacts**:workflow 运行时产生的临时文件,属于 workflow 当前运行实例的隔离空间。
+* Artifacts 的存在目的是:
+  * 存放大文本/大对象(避免全局变量膨胀)
+  * 存放多文件中间产物(如 `components/` 目录下 N 个文件)
+  * 支持 pause/resume 时仅保存"文件地址引用",而不是保存完整内容
+
+---
+
+## 8.2 路径声明与边界(Registry 约束)
+
+### 8.2.1 `registry.files.artifacts`
+
+workflow 允许写入的临时文件路径范围必须在 `registry.files.artifacts` 中声明:
+
+```Plain
+"files": {
+  "inputs": ["Process/Rules/*", "Process/PRD.json"],
+  "artifacts": ["Process/Artifacts/*"]
+}
+```
+
+### 8.2.2 先注册后使用
+
+* **所有 artifacts 写入路径必须落在声明的范围内**
+* 超出范围的写入属于非法(运行时报错)
+
+---
+
+## 8.3 写入规则(Write\_\*)
+
+### 8.3.1 写入节点
+
+写入 artifacts 必须使用状态写入节点:
+
+* `Write_*`
+
+### 8.3.2 写入目标
+
+`Write_*` 的 `target` 表示写入路径(可为表达式字符串):
+
+```Plain
+{
+  "id": "Write_ComponentFile",
+  "target": "=\"Process/Artifacts/components/\" + _item.name + \".tsx\"",
+  "value": "=$componentCode"
+}
+```
+
+### 8.3.3 写入内容
+
+`value` 表示文件内容(表达式求值结果):
+
+* 文本(STRING)
+* JSON 对象/数组(由引擎序列化为 JSON 文本,或按实现约定保存为结构化对象)
+
+---
+
+## 8.4 写入策略(Write.mode)
+
+`Write_*` 支持 `mode`(可选):
+
+* `"overwrite"`:覆盖写(默认)
+* `"failIfExists"`:若文件已存在则报错
+* `"append"`:追加写(适用于日志类文件)
+* `"prepend"`:前置写(将新内容写在文件开头)
+
+补充约束:
+
+* `append/prepend` 属于读改写语义(read-modify-write)
+* 目标文件不存在时按新建文件处理
+* 并发写入同一路径时不保证顺序,建议避免并发冲突
+
+示例:
+
+```Plain
+{
+  "id": "Write_Log",
+  "mode": "append",
+  "target": "\"Process/Artifacts/run.log\"",
+  "value": "=$line + \"\\n\""
+}
+```
+
+---
+
+## 8.5 读取规则(Artifacts Read)
+
+当前版本不强制提供"专门的读文件节点"。Artifacts 的读取建议通过以下方式完成:
+
+### 8.5.1 作为路径引用传递给调用型节点
+
+Artifacts 文件路径可以作为普通字符串存入 `$vars`,并作为调用型节点 `in` 的参数传递:
+
+`{ "id": "Service_UseArtifact", "in": { "filePath": "=$artifactPath" } }`
+
+### 8.5.2 读取能力的来源
+
+Artifacts 的实际读取由以下之一承担:
+
+* `Component_*`(系统内置能力,例如 FileOps.Read)
+* 或项目内 `Service_*`(自定义的读取服务)
+
+> 当前版本的核心原则是:workflow 只定义编排,不绑定具体 IO 实现细节;因此"读文件"属于能力节点的职责。
+
+---
+
+## 8.6 Artifacts 与变量的关系
+
+* Artifacts 文件地址(path/uri)可以作为普通数据写入 `$vars`
+* `$vars` 可存"文件引用",而 artifacts 存"文件内容"
+* 推荐模式:
+  * 大内容写入 artifacts
+  * 变量只保存 artifacts 路径、摘要、hash、索引信息
+
+---
+
+## 8.7 临时目录(.tmp)与生命周期
+
+### 8.7.1 `.tmp/` 语义
+
+* 路径以 `.tmp/` 开头时,视为临时文件路径(例如 `.tmp/bundle.zip`)
+* workflow 定义中不感知 `runId`,只写相对路径
+
+### 8.7.2 引擎解析与隔离
+
+* 引擎将 `.tmp/xxx` 解析为 `<artifactsRoot>/.tmp/{runId}/xxx`
+* 隔离维度:`workspaceId + runId`
+* 不同 run 的临时文件互不可见
+* 懒创建:仅在本次 run 首次写入 `.tmp/` 路径时创建 `.tmp/{runId}/`
+* 若本次 run 未使用 `.tmp/`,则不创建 `.tmp/{runId}/` 目录
+
+### 8.7.3 迭代级隔离(`_iterDir`)
+
+* 在 `Loop_*` 并行场景下,建议使用 `.tmp/{_iterDir}/...` 防止迭代间冲突
+* 完整解析路径:`<artifactsRoot>/.tmp/{runId}/{_iterDir}/...`
+
+### 8.7.4 生命周期
+
+* run 进入终态(`stopped` / `failed` / `cancelled`)后,引擎应清理 `.tmp/{runId}/`
+* 需提供 TTL 兜底清理,处理异常中断后的遗留目录
+
+### 8.7.5 可见性约束
+
+* `.tmp/` 仅作为中间态目录,不作为业务可读目录
+* 对外可见目录应仅暴露已发布完成的正式文件
+
+---
+
+## 8.8 Download/Unzip 执行约束
+
+* `Download_*` 必须流式下载,避免将完整正文加载到内存
+* `Unzip_*` 必须逐 entry 解压,并做 zip-slip 防护
+* `routeByExt` 路由目录建议与组件工厂目录约定一致(如 `ExtComponents/Sections/Services`)
+* 大文件正文不落 `$vars`,仅将路径、摘要、统计信息写入变量
+
+---
+
+## 8.9 并发与冲突约束(重要)
+
+当 workflow 存在并行执行(`children` 并行、`Loop_* mode:"parallel"`)时:
+
+* 多个节点写入同一路径会导致冲突
+* 建议规则:
+  * 引擎检测到同路径并发写入时应报错(或按最后写入覆盖,但不推荐)
+* 推荐写法:
+  * 路径按 `_index` 或 `_item.name` 分片,保证每轮写不同文件
+
+示例:
+
+```Plain
+{
+  "id": "Write_ComponentFile",
+  "target": "=\"Process/Artifacts/components/\" + _item.name + \".tsx\"",
+  "value": "=$code"
+}
+```
+
+# 第九章:Loop 完整执行语义(`Loop_*`)
+
+本章定义  `Loop_*` 节点的完整执行规则,包括数据源求值、并行/串行模式、循环体执行、join 汇合、局部变量作用域,以及与 `if/children/next` 的交互。
+
+---
+
+## 9.1 节点结构
+
+`Loop_*` 是控制节点,支持 `source` 与 `while` 两种互斥模式。
+
+`source` 模式:
+```Plain
+{
+  "id": "Loop_xxx",
+  "mode": "parallel | serial",
+  "source": "<expr>",
+  "children": ["<stepId>"],
+  "next": "<stepId>"
+}
+```
+
+`while` 模式:
+
+```Plain
+{
+  "id": "Loop_xxx",
+  "mode": "serial",
+  "while": "<expr>",
+  "maxIterations": 5,
+  "children": ["<stepId>"],
+  "next": "<stepId>"
+}
+```
+
+约束:
+
+* `mode` 必填;`while` 模式必须为 `"serial"`
+* `source` 与 `while` 二选一
+* `source` 模式:`source` 必填且求值为数组,`maxIterations` 选填
+* `while` 模式:`while` 与 `maxIterations` 必填
+* `children` 必填:循环体入口节点列表(推荐仅 1 个入口;若允许多个则表示并行体)
+
+---
+
+## 9.2 数据源求值(`source`)
+
+执行到 `Loop_*` 时,引擎首先求值:
+
+* `items = eval(source)`
+
+规则:
+
+* `items` 必须是数组
+* 若求值结果为 `null/undefined`:
+  * 建议视为 `[]`(空循环)
+* 若不是数组:
+  * 运行时报错(Loop 输入类型错误),workflow 进入 `failed` 状态
+
+## 9.2.A while 模式求值(3.16 新增)
+
+当 `Loop_*` 使用 `while` 模式时:
+
+* 每轮执行 children 前先求值 `while`
+* 第 0 轮若为 false,children 零次执行并直接进入 `next`
+* 每轮后 `_index++`,达到 `maxIterations` 后强制退出
+* `_item` 不存在(while 无数据源),`_index` 与 `_iterDir` 可用
+
+---
+
+## 9.3 空循环行为
+
+若 `items.length == 0`:
+
+* loop 不执行任何循环体
+* 直接进入 `Loop_*` 的 `next`
+* `Loop_*` 必须声明 `next`;空循环时直接进入该 `next`
+
+---
+
+## 9.4 循环体(Loop Body)
+
+### 9.4.1 循环体入口
+
+`children` 定义循环体入口节点(stepId 列表)。
+
+推荐约束(更清晰):
+
+* `Loop_*` 的 `children` 建议限制为 **1 个入口节点**
+* 如果循环体需要并行分叉,在该入口节点上再写 `children`
+
+(如果允许 `Loop_* children` 多入口,则它们在每次迭代中并行启动)
+
+---
+
+### 9.4.2 局部变量绑定
+
+每次迭代 i(从 0 开始)都会在循环体子树范围内注入:
+
+* `_item = items[i]`
+* `_index = i`
+
+作用域规则:
+
+* `_item/_index` 仅在该次迭代的循环体子树内可用
+* 不会泄露到 loop 外部
+* 不同迭代的 `_item/_index` 相互隔离
+
+---
+
+## 9.5 执行模式(`mode`)
+
+### 9.5.1 `mode: "serial"`
+
+语义:按顺序逐个迭代执行。
+
+执行流程:
+
+1. 执行迭代 0 的循环体(从 `children` 入口开始跑完整链)
+2. 迭代 0 完成后,执行迭代 1
+3. …直到最后一个迭代完成
+4. loop 完成后进入 `Loop_* next`
+
+特点:
+
+* 迭代间严格顺序
+* 适用于有依赖关系、写同一份状态、或需要限流的场景
+
+---
+
+### 9.5.2 `mode: "parallel"`
+
+语义:所有迭代可并行执行。
+
+执行流程:
+
+1. 引擎为每个 i 启动一个迭代任务(并行)
+2. 每个迭代从 `children` 入口开始执行完整链
+3. 所有迭代都完成后,loop 才算完成(join)
+4. loop 完成后进入 `Loop_* next`
+
+特点:
+
+* 高吞吐
+* 必须注意共享状态冲突(尤其是 `$vars` 与 artifacts 同路径写入)
+
+### 9.5.3 while 模式的 mode 约束(3.16 新增)
+
+* `while` 模式下 `mode` 必须为 `"serial"`
+* `while + mode:"parallel"` 属于编译错误
+
+## 9.6 迭代的“完成”定义
+
+一次迭代从其入口节点开始执行,沿自身的 `next / children / Branch_* / Loop_*` 推进,并在以下任一情况发生时结束:
+
+* 迭代中的某个节点执行到 **`next: "RETURN"`**,表示该迭代**正常完成**,控制流返回到 `Loop_*` 的 join。
+* 迭代中的某个节点执行到 **`next: "BREAK"`**,表示该迭代完成并退出整个 Loop,跳转到 `Loop_*.next`。
+* 迭代中的某个节点为 **`Pause_*`**,workflow 进入 `paused` 状态,等待 resume;loop 未完成。
+* 迭代中的某个节点执行到 **`Stop_*`**,workflow 立即终止(`stopped`)。
+* 迭代中的某个节点执行失败,workflow 进入 `failed` 状态。
+
+---
+
+## 9.7 Loop 的 join 汇合规则
+
+无论 `mode` 为 `serial` 还是 `parallel`,`Loop_*` 的汇合规则一致:
+
+* `Loop_*` 仅在 **所有迭代均正常完成(`RETURN`)** 后,才会进入自身的 `next`。
+* 若任一迭代触发 `BREAK`:退出循环并进入 `Loop_*.next`;在 parallel 模式下已启动迭代继续执行至自然结束。
+* 若任一迭代遇到 `Pause_*` 节点进入 `paused` 状态,整个 workflow 进入 `paused`,loop 不再继续。
+* 若任一迭代到达 `Stop_*`,整个 workflow 立即终止(`stopped`)。
+* 若任一迭代执行失败,整个 workflow 进入 `failed` 状态。
+
+---
+
+## 9.8 与 `if` 的交互
+
+## 9.8.1 Loop 自身的 `if`
+
+* 若 `Loop_* if=false`:
+  * 整个 loop 节点及其循环体子树被跳过
+  * 直接进入 `Loop_* next`
+
+### 9.8.2 循环体内部节点的 `if`
+
+* 每个迭代中节点 `if` 独立求值
+* `if=false` 会跳过该节点及其子树,但不影响其他迭代
+
+---
+
+## 9.9 与 `$vars` / artifacts 的交互建议
+
+### 9.9.1 `$vars` 写入建议
+
+* serial:可安全写共享变量(仍建议结构化)
+* parallel:推荐按 `_index` 分槽写入,例如:
+  * `$generated[_index] = ...`
+
+### 9.9.2 artifacts 写入建议
+
+* parallel 模式下必须保证不同迭代写入不同路径,例如:
+  * `Process/Artifacts/components/<n>.tsx`
+
+---
+
+## 9.10 Loop 的输出与数据汇总(推荐方式)
+
+Loop 本身不直接产出 `_result`。循环结果通常通过:
+
+* 循环体内调用型节点 `out` 写入 `$vars[_index]`
+* 循环体内 `Write_*` 写入 artifacts,并把路径写入 `$vars[_index]`
+
+示例(概念):
+
+* `$files[_index].path = "Process/Artifacts/components/xxx.tsx"`
+
+# 第十章:Branch 完整执行语义(`Branch_*`)
+
+本章定义 `Branch_*` 节点的完整执行规则,包括 case 选择、分支链执行、汇合(join)与 `next` 的关系,以及与 `if/children` 的交互。
+
+---
+
+## 10.1 节点结构
+
+`Branch_*` 是控制节点,基本结构如下:
+
+```Plain
+{
+  "id": "Branch_xxx",
+  "cases": [
+    ["<whenExpr1>", "<stepId1>"],
+    ["<whenExpr2>", "<stepId2>"],
+    ["ELSE", "<stepIdElse>"]
+  ],
+  "next": "<stepId>"
+}
+```
+
+约束:
+
+* `cases` 必填
+* 每个 case 必须是二元组:`[whenExpr, stepId]`
+* `whenExpr` 为表达式字符串,或固定字符串 `"ELSE"`
+* `"ELSE"` 必须存在且必须放最后(推荐强约束)
+
+---
+
+## 10.2 分支选择规则(Case Selection)
+
+执行到 `Branch_*` 时,引擎按顺序对 `cases` 进行判断:
+
+1. 对每个 `["<whenExpr>", "<stepId>"]`:
+   1. 若 `<whenExpr>` 不是 `"ELSE"`:
+      * 计算 `eval(<whenExpr>)`
+      * 若结果为 `true`,该 case 命中,停止继续判断后续 case
+2. 若前面都未命中,则命中 `"ELSE"` case
+
+---
+
+## 10.3 单入口规则
+
+每个 case 只能指定 **一个入口节点**(一个 stepId)。该入口节点代表"该分支路径的起点"。
+
+---
+
+## 10.4 分支执行语义(执行完整链)
+
+命中某个 case 后,引擎将从该 case 的入口节点开始执行:
+
+* 沿入口节点自己的 `next / children / Branch_* / Loop_*` 继续推进
+* 这条链路可能包含嵌套分支、嵌套循环、并行 children 等
+
+重要:**Branch 并不是只执行入口节点一次就返回**,而是执行"从入口节点开始的一整条分支链"。
+
+## 10.5 分支的“完成”定义
+
+分支链从被选中的 case 入口节点开始执行,沿其自身的控制流推进,并在以下任一情况发生时结束:
+
+1. 分支链中的某个节点执行到 **`next: "RETURN"`**,表示该分支**正常完成**,控制流返回到 `Branch_*` 的 join。
+2. 分支链中的某个节点为 **`Pause_*`**,workflow 进入 `paused` 状态,等待 resume。
+3. 分支链中的某个节点执行到 **`Stop_*`**,workflow 立即终止(`stopped`)。
+4. 分支链中的某个节点执行失败,workflow 进入 `failed` 状态。
+
+## 10.6 Branch 的汇合(Join)与 `next`
+
+当且仅当:
+
+* 命中的分支链 **正常完成**(即执行到 `next: "RETURN"`)
+
+`Branch_*` 节点才会进入其自身的 `next`。
+
+执行规则如下:
+
+* `Branch_*` 的 `next`**必须显式声明**。
+* 当分支链完成后,控制流从 `Branch_* next` 指向的节点继续执行。
+
+异常情况:
+
+* 若分支链执行过程中遇到 `Pause_*` 节点,workflow 进入 `paused` 状态,`Branch_*` 不再继续推进。
+* 若分支链执行过程中到达 `Stop_*`,workflow 立即终止(`stopped`)。
+* 若分支链执行失败,workflow 进入 `failed` 状态。
+
+## 10.7 未命中的分支不执行
+
+所有未命中的 case 对应的入口节点及其子树完全不执行。
+
+## 10.8 与 `if` 的交互
+
+### 10.8.1 Branch 自身的 `if`
+
+若 `Branch_* if=false`:
+
+* 整个 Branch 节点被跳过(不会判断 cases)
+* 直接进入 `Branch_* next`
+
+### 10.8.2 分支链内部节点的 `if`
+
+* 仅影响该分支链内部执行
+* 不影响 Branch 的 case 选择逻辑(case 选择发生在进入分支链之前)
+
+---
+
+## 10.9 Branch 与并行(children)的关系
+
+Branch 本身是"互斥选择",一次只会选择一个 case。如果某个 case 的入口节点内部包含 `children`,则该分支链内部仍可产生并行执行,并遵循 `children` 的 join 规则。
+
+---
+
+## 10.10 ELSE 缺失时的行为(不推荐)
+
+如果实现允许 `"ELSE"` 缺失(不推荐):
+
+* 所有 when 都不命中时:
+  * Branch 视为"不执行任何分支"
+  * 直接进入 `Branch_* next`
+
+> 推荐强制要求 ELSE 存在,以保证分支行为可预测。
+
+# 第十一章:Pause_* 节点与暂停/恢复协议
+
+本章定义 `Pause_*` 节点的结构、执行语义与配套的 `resume` 恢复协议,用于在 workflow 中显式表达"等待外部结果再继续"的语义,适配用户输入与审批流等场景。引擎保持通用,不将审批领域规则内置到引擎。
+
+---
+
+## 11.1 定位
+
+`Pause_*` 用于在流程中间等待外部系统输入结果。
+
+引擎执行到该节点时进入 `paused` 状态,仅在收到合法 `resume` 请求后继续执行 `next`。
+
+> `Pause_*` ≠ `Stop_*`:`Stop_*` 是终止(不可恢复),`Pause_*` 是可恢复的挂起。  
+> `Pause_*` ≠ `failed`:`failed` 是异常中断,`Pause_*` 是设计意图内的等待。
+
+---
+
+## 11.2 节点结构
+
+### 11.2.1 必填与选填字段
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `id` | string | 是 | 节点标识,须以 `Pause_` 开头 |
+| `reason` | string | 否 | 给前端/通知/日志展示的等待原因文案 |
+| `resumeResultTarget` | string | 是 | `resume(payload)` 写入的 `$vars` 目标路径 |
+| `timeout` | object | 否 | 超时配置(见下) |
+| `next` | string | 是 | 恢复成功后的后继节点 |
+
+### 11.2.2 `timeout` 结构
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `sec` | number | 是 | 超时秒数(必须 > 0) |
+| `on` | string | 是 | 超时后跳转的节点(通常为 `Stop_*` 或异常处理链路) |
+
+### 11.2.3 示例
+
+```json
+{
+  "id": "Pause_UserInput_Address",
+  "reason": "请补充收货地址",
+  "resumeResultTarget": "$vars.userInput.address",
+  "timeout": {
+    "sec": 86400,
+    "on": "Stop_Timeout"
+  },
+  "next": "Service_CreateOrder"
+}
+```
+
+---
+
+## 11.3 执行语义
+
+### 11.3.1 进入暂停
+
+执行到 `Pause_*` 时,引擎必须:
+
+1. 将 run 状态置为 `paused`
+2. 持久化等待上下文(至少包含 `runId`、`pauseNodeId`、`waitToken`(hash)、`expireAt`、`resumeResultTarget`)
+3. 发出 `pause_start` 事件
+
+### 11.3.2 恢复执行
+
+收到 `resume` 请求后,引擎必须:
+
+1. 校验 run 当前为 `paused`
+2. 校验 `waitToken` 与 `pauseNodeId` 对应关系
+3. 将 `payload` 写入 `resumeResultTarget`
+4. 发出 `pause_resumed` 事件
+5. 从该 `Pause_*` 节点的 `next` 继续执行
+
+### 11.3.3 超时处理
+
+若到达 `timeout.sec` 仍未恢复:
+
+1. 发出 `pause_timeout` 事件
+2. 跳转到 `timeout.on` 执行
+3. 若未配置 `timeout`,实现可按平台策略保持 `paused` 或转 `failed`(需实现侧明确文档化)
+
+---
+
+## 11.4 恢复接口协议
+
+### 11.4.1 请求体最小字段
+
+```json
+{
+  "runId": "run_xxx",
+  "token": "wait_token_xxx",
+  "payload": {
+    "approved": true,
+    "comment": "ok"
+  },
+  "requestId": "req_xxx"
+}
+```
+
+### 11.4.2 约束
+
+1. `resume` 必须幂等(建议基于 `requestId` 去重)
+2. 同一个等待 token 只能成功恢复一次
+3. run 非 `paused` 时应拒绝恢复并返回当前状态
+4. 不允许外部指定 `nextStep`,恢复路径固定为 `Pause_*` 的 `next`
+
+---
+
+## 11.5 事件定义
+
+### 11.5.1 `pause_start`
+
+触发:进入 `Pause_*` 时。
+
+关键字段:`runId`、`nodeId`、`reason`、`waitToken`、`expireAt`、`resumeResultTarget`
+
+### 11.5.2 `pause_resumed`
+
+触发:`resume` 成功且已写入 `resumeResultTarget` 后。
+
+关键字段:`runId`、`nodeId`、`requestId`、`resumedAt`
+
+### 11.5.3 `pause_timeout`
+
+触发:等待超时。
+
+关键字段:`runId`、`nodeId`、`expiredAt`、`timeoutAction`
+
+### 11.5.4 `pause_rejected`
+
+触发:恢复请求被拒绝(token 无效、状态错误、schema 不通过)。
+
+关键字段:`runId`、`nodeId`、`requestId`、`reasonCode`
+
+---
+
+## 11.6 与 Service_* 的职责边界
+
+1. workflow 负责"何时等待/何时继续"
+2. 业务 Service 负责"谁来处理/规则如何计算/何时形成最终结果"
+3. 审批会签/或签/回退等复杂逻辑不内置到 workflow 引擎
+4. 业务 Service 在形成"可推进最终结果"时调用 `resume`
+
+---
+
+## 11.7 典型编排示例(三层审批)
+
+推荐结构(每层 2 节点):
+
+1. `Service_StartL1Approval` → `Pause_L1` → `Branch_L1Result`
+2. `Service_StartL2Approval` → `Pause_L2` → `Branch_L2Result`
+3. `Service_StartL3Approval` → `Pause_L3` → `Branch_L3Result`
+
+说明:
+
+1. 每层 `Pause_*` 使用独立 `resumeResultTarget`(如 `$vars.approval.l1`、`$vars.approval.l2`、`$vars.approval.l3`)
+2. 每层审批发起可复用同一个通用 service(如 `approval.create`),通过 `stage` 参数区分
+
+---
+
+## 11.8 约束与非目标
+
+1. 本章不要求引擎实现审批规则引擎能力
+2. 不引入 `next: "PAUSE"` 魔法值;暂停由 `Pause_*` 节点显式表达
+3. 不要求外部回传完整引擎状态快照;推荐由引擎服务端持久化 run/pause 状态
+
+# 第十二章:IDE 内 Workflow 交互说明
+
+> 说明
+>
+> * Workflow 是项目的一部分,不需要用户选择,存储在项目内 /config/workflow.json 中
+> * 新建项目时自动生成默认 workflow
+> * IDE 始终运行在已知 `tenantId + projectId` 上下文中
+> * workflow 的执行策略由 Agent 决定,前端不提供配置面板
+
+---
+
+## 12.1 前端核心交互窗口
+
+| 窗口区域          | 主要内容                     | 用户操作          | 说明                         |
+| ----------------- | ---------------------------- | ----------------- | ---------------------------- |
+| 对话窗口          | 用户自然语言输入             | 输入需求          | 所有 workflow 执行的唯一入口 |
+| Workflow 运行视图 | 当前项目 workflow 的执行过程 | 无                | 自动展示当前 run 状态        |
+| 输出控制台        | 日志 / 节点状态 / 错误       | 查看              | 由后台 run 自动推送          |
+| 工作空间视图      | 当前 workspace 文件状态      | commit / rollback | 与 workflow 解耦             |
+| 状态提示区        | run 状态                     | 查看              | running / finished / failed  |
+
+---
+
+## 12.2 后台接口(IDE 调用)
+
+| 接口名称           | 接口用途(使用方)    | 入参                                                  | 出参                                                                                       | 重点说明                   |
+| ------------------ | --------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------ | -------------------------- |
+| LoadProjectContext | 加载项目上下文(IDE) | tenantId, projectId                                   | 项目信息、默认 workspaceId                                                                 | IDE 初始化                 |
+| LoadWorkspace      | 加载工作空间          | tenantId, projectId, workspaceId                      | 文件树摘要、workspace 状态                                                                 | 判断是否初始状态           |
+| AnalyzeIntent      | 生成运行入参(Agent) | tenantId, projectId, workspaceId, userInput           | runMaps(mode / nodes),notes(用来在会话窗口展示当前修改计划供用户查看,但不会中断流程) | 仅非初始 workspace 调用    |
+| RunProjectWorkflow | 运行项目 workflow     | tenantId, projectId, workspaceId, userInput, runMaps? | runId + SSE 连接                                                                           | 自动建立 SSE,返回执行状态 |
+| GetRunStatus       | 查询运行状态          | tenantId, projectId, runId                            | run 状态、错误信息                                                                         | 用于断线恢复               |
+| CommitWorkspace    | 提交产物              | tenantId, projectId, workspaceId                      | commitId                                                                                   | 与 workflow 无关           |
+| RollbackWorkspace  | 回滚产物              | tenantId, projectId, workspaceId                      | 回滚结果                                                                                   | 与 workflow 无关           |
+
+---
+
+## 12.3 前端功能实现核心要点(含对话驱动主流程)
+
+| 功能点              | 实现逻辑                             | 设计约束                   |
+| ------------------- | ------------------------------------ | -------------------------- |
+| 对话驱动执行        | 用户输入即触发 workflow              | 无显式按钮                 |
+| 初始 workspace 判断 | LoadWorkspace 后判断是否初始状态     | 前端只判断,不决策         |
+| 新项目执行          | 初始 workspace → 直接 run           | 不生成 runMaps             |
+| 修改型执行          | 非初始 workspace → 先 AnalyzeIntent | 由 Agent 决定 mode / nodes |
+| 运行参数生成        | AnalyzeIntent 输出 runMaps           | 前端不理解语义             |
+| 执行启动            | RunProjectWorkflow                   | 自动 SSE                   |
+| 执行反馈            | 后台推送运行状态                     | 前端被动展示               |
+| 产物管理            | commit / rollback workspace          | workflow 不参与            |
+| 错误处理            | run 失败即终止                       | 支持后续再次对话           |
+
+---
+
+### 对话驱动执行主逻辑(摘要)
+
+| workspace 状态 | 执行流程                                         |
+| -------------- | ------------------------------------------------ |
+| 初始状态       | userInput → RunProjectWorkflow                  |
+| 非初始状态     | userInput → AnalyzeIntent → RunProjectWorkflow |
+
+# 第十三章:运行时消息规范(`run_events`)
+
+## 13.1 概述
+
+Workflow 执行过程中,引擎向 `run_events` 事件流写入结构化消息,用于向调用方(前端、MQ 消费者、日志系统)传递执行进度和阶段结果。
+
+设计原则:
+
+* `run_events` 只描述过程,最终结论以 `workflow_runs` 为准
+* 每条事件独立可解析,不依赖前序事件状态
+* 事件流为尽力而为输出,消费侧异常不影响 workflow 执行
+
+## 13.2 事件基础结构
+
+```json
+{
+  "run_id": "run_xxx",
+  "seq": 1,
+  "ts": "2026-02-21T10:00:00.123Z",
+  "type": "step_start",
+  "step_id": "LLM_Answer",
+  "payload": {}
+}
+```
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `run_id` | `string` | 本次 workflow 执行唯一 ID |
+| `seq` | `integer` | run 内单调递增序号,从 1 开始 |
+| `ts` | `string` | ISO 8601 时间戳(毫秒精度) |
+| `type` | `string` | 事件类型 |
+| `step_id` | `string \\| null` | 触发事件的节点 ID;workflow 级事件为 `null` |
+| `payload` | `object` | 事件负载 |
+
+## 13.3 事件类型
+
+### Workflow 级
+
+* `workflow_start`
+* `workflow_done`
+* `workflow_failed`
+* `workflow_cancelled`
+
+### Step 级
+
+* `step_start`
+* `step_print`
+* `step_done`
+* `step_error`
+* `step_skipped`
+
+### LLM 专属
+
+* `llm_token`(仅当节点 `in.stream: true`)
+* `llm_done`
+
+### 文件状态专属
+
+* `file_start`(步骤开始时声明将写入某路径的文件)
+* `file_done`(文件内容已完整写入对应路径)
+
+### Pause 专属
+
+* `pause_start`(进入 `Pause_*` 节点,workflow 进入 `paused` 状态)
+* `pause_resumed`(resume 成功,已写入 `resumeResultTarget`,即将继续执行)
+* `pause_timeout`(等待超时,即将跳转到 `timeout.on`)
+* `pause_rejected`(resume 请求被拒绝,token 无效或状态不符)
+
+顺序约束:
+
+* `step_start` → `llm_token`(xN) → `llm_done` → `step_print`(0..1) → `step_done`
+* `llm_done` 必在所有 `llm_token` 之后
+* 同一步骤的所有 `file_start` 在 `step_start` 之后立即批量发出,早于任何 `llm_token`
+* 所有 `file_done` 在 `llm_done` 之后、`step_print`/`step_done` 之前发出
+
+## 13.4 事件 payload 规范(最小集)
+
+### `workflow_start`
+
+```json
+{ "params": {} }
+```
+
+### `workflow_done`
+
+```json
+{ "stop_id": "Stop_End", "duration_ms": 4821 }
+```
+
+### `workflow_failed`
+
+```json
+{
+  "failed_step_id": "LLM_Analyze",
+  "error": { "type": "json_parse_error", "message": "Unexpected token", "retryable": true },
+  "duration_ms": 1203
+}
+```
+
+### `workflow_cancelled`
+
+```json
+{
+  "reason": "user_request",
+  "duration_ms": 800
+}
+```
+
+### `step_start`
+
+```json
+{ "step_type": "LLM_*" }
+```
+
+### `step_done`
+
+```json
+{ "step_type": "LLM_*", "duration_ms": 2341 }
+```
+
+`step_done` 的 payload 不包含 `_result`。
+
+### `step_print`
+
+节点执行完成后发出,用于承载节点级自定义打印内容(来自该节点 `print` 属性)。
+
+```json
+{ "message": "版本检查完成: 3.13" }
+```
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `message` | `string` | `print` 求值后的输出文本 |
+
+### `step_error`
+
+```json
+{
+  "step_type": "LLM_*",
+  "error": { "type": "rate_limit", "message": "Rate limit exceeded", "retryable": true, "status_code": 429 },
+  "duration_ms": 312
+}
+```
+
+### `step_skipped`
+
+```json
+{ "step_type": "Set_*", "reason": "if_false" }
+```
+
+### `file_start`
+
+步骤开始时批量发出,声明本步骤将写入的文件路径。此时文件内容尚未生成。
+
+```json
+{
+  "path": "src/components/Header.tsx"
+}
+```
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `path` | `string` | 文件路径(相对于 workspace 根目录) |
+
+### `file_done`
+
+文件内容已完整写入对应路径后发出,在 `llm_done` 之后、`step_done` 之前。
+
+```json
+{
+  "path": "src/components/Header.tsx",
+  "size_bytes": 2048
+}
+```
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `path` | `string` | 与对应 `file_start` 事件中的 `path` 一致 |
+| `size_bytes` | `integer` | 写入后文件的字节数 |
+
+### `llm_token`
+
+```json
+{ "delta": "这是" }
+```
+
+`delta` 为原始文本增量,不做 JSON parse。
+
+### `llm_done`
+
+```json
+{
+  "finish_reason": "stop",
+  "usage": { "input_tokens": 312, "output_tokens": 428, "total_tokens": 740 },
+  "model": "claude-sonnet-4-5",
+  "latency_ms": 2180
+}
+```
+
+### `pause_start`
+
+```json
+{
+  "nodeId": "Pause_UserInput_Address",
+  "reason": "请补充收货地址",
+  "waitToken": "tok_abc123",
+  "expireAt": "2026-03-03T10:00:00.000Z",
+  "resumeResultTarget": "$vars.userInput.address"
+}
+```
+
+### `pause_resumed`
+
+```json
+{
+  "nodeId": "Pause_UserInput_Address",
+  "requestId": "req_xyz",
+  "resumedAt": "2026-03-02T11:30:00.000Z"
+}
+```
+
+### `pause_timeout`
+
+```json
+{
+  "nodeId": "Pause_UserInput_Address",
+  "expiredAt": "2026-03-03T10:00:00.000Z",
+  "timeoutAction": "Stop_Timeout"
+}
+```
+
+### `pause_rejected`
+
+```json
+{
+  "nodeId": "Pause_UserInput_Address",
+  "requestId": "req_bad",
+  "reasonCode": "INVALID_TOKEN"
+}
+```
+
+## 13.5 并行与顺序保证
+
+* 串行节点:事件顺序与执行顺序一致
+* 并行节点:同一节点内事件有序;不同节点间不保证顺序
+* 消费方应使用 `step_id` + `seq` 进行重组
+
+## 13.6 完整事件流示例
+
+### 示例 A:包含单个 `LLM_*` 节点(`in.stream: true`,无文件写入)
+
+```json
+{ "run_id": "run_abc", "seq": 1,  "ts": "...", "type": "workflow_start",  "step_id": null,         "payload": { "params": {} } }
+{ "run_id": "run_abc", "seq": 2,  "ts": "...", "type": "step_start",      "step_id": "LLM_Answer", "payload": { "step_type": "LLM_*" } }
+{ "run_id": "run_abc", "seq": 3,  "ts": "...", "type": "llm_token",       "step_id": "LLM_Answer", "payload": { "delta": "这是" } }
+{ "run_id": "run_abc", "seq": 4,  "ts": "...", "type": "llm_token",       "step_id": "LLM_Answer", "payload": { "delta": "一个" } }
+{ "run_id": "run_abc", "seq": 5,  "ts": "...", "type": "llm_token",       "step_id": "LLM_Answer", "payload": { "delta": "回答。" } }
+{ "run_id": "run_abc", "seq": 6,  "ts": "...", "type": "llm_done",        "step_id": "LLM_Answer", "payload": { "finish_reason": "stop", "usage": { "input_tokens": 80, "output_tokens": 12, "total_tokens": 92 }, "model": "claude-sonnet-4-5", "latency_ms": 1340 } }
+{ "run_id": "run_abc", "seq": 7,  "ts": "...", "type": "step_print",      "step_id": "LLM_Answer", "payload": { "message": "版本检查完成: 3.13" } }
+{ "run_id": "run_abc", "seq": 8,  "ts": "...", "type": "step_done",       "step_id": "LLM_Answer", "payload": { "step_type": "LLM_*", "duration_ms": 1355 } }
+{ "run_id": "run_abc", "seq": 9,  "ts": "...", "type": "workflow_done",   "step_id": null,         "payload": { "stop_id": "Stop_End", "duration_ms": 1400 } }
+```
+
+### 示例 B:包含文件写入的 workflow(一个步骤生成多个文件)
+
+以一个代码生成 workflow 为例,`LLM_GenCode` 节点生成两个文件。两个 `file_start` 在步骤开始时批量发出,两个 `file_done` 在 `llm_done` 之后发出:
+
+```json
+{ "run_id": "run_xyz", "seq": 1,  "ts": "...", "type": "workflow_start",  "step_id": null,           "payload": { "params": {} } }
+{ "run_id": "run_xyz", "seq": 2,  "ts": "...", "type": "step_start",      "step_id": "LLM_GenCode",  "payload": { "step_type": "LLM_*" } }
+{ "run_id": "run_xyz", "seq": 3,  "ts": "...", "type": "file_start",      "step_id": "LLM_GenCode",  "payload": { "path": "src/components/Header.tsx" } }
+{ "run_id": "run_xyz", "seq": 4,  "ts": "...", "type": "file_start",      "step_id": "LLM_GenCode",  "payload": { "path": "src/components/Header.module.css" } }
+{ "run_id": "run_xyz", "seq": 5,  "ts": "...", "type": "llm_token",       "step_id": "LLM_GenCode",  "payload": { "delta": "import React" } }
+{ "run_id": "run_xyz", "seq": 6,  "ts": "...", "type": "llm_token",       "step_id": "LLM_GenCode",  "payload": { "delta": " from 'react'..." } }
+{ "run_id": "run_xyz", "seq": 7,  "ts": "...", "type": "llm_done",        "step_id": "LLM_GenCode",  "payload": { "finish_reason": "stop", "usage": { "input_tokens": 210, "output_tokens": 560, "total_tokens": 770 }, "model": "claude-sonnet-4-5", "latency_ms": 3100 } }
+{ "run_id": "run_xyz", "seq": 8,  "ts": "...", "type": "file_done",       "step_id": "LLM_GenCode",  "payload": { "path": "src/components/Header.tsx", "size_bytes": 1872 } }
+{ "run_id": "run_xyz", "seq": 9,  "ts": "...", "type": "file_done",       "step_id": "LLM_GenCode",  "payload": { "path": "src/components/Header.module.css", "size_bytes": 634 } }
+{ "run_id": "run_xyz", "seq": 10, "ts": "...", "type": "step_print",      "step_id": "LLM_GenCode",  "payload": { "message": "代码文件生成完成: 2 个文件" } }
+{ "run_id": "run_xyz", "seq": 11, "ts": "...", "type": "step_done",       "step_id": "LLM_GenCode",  "payload": { "step_type": "LLM_*", "duration_ms": 3280 } }
+{ "run_id": "run_xyz", "seq": 12, "ts": "...", "type": "workflow_done",   "step_id": null,            "payload": { "stop_id": "Stop_End", "duration_ms": 3350 } }
+```
+# 第十四章:3.17 扩展与兼容性
+
+## 14.1 目标
+
+3.17 的目标不是推翻 3.16,而是把过去分散在实现层里的高价值能力收敛成一套可公开引用的规范:
+
+* workflow 调用宿主 tools
+* 外层窗口可感知完整工具事件
+* 并行运行从第一步起具备稳定归属
+* 从 checkpoint 在复杂 fork / loop 中继续执行
+
+本章允许这些能力以“标准扩展剖面”的形式存在。实现方可以分阶段支持,但一旦声明支持 3.17 扩展剖面,就应满足本章的字段、事件与兼容性要求。
+
+## 14.2 `Tool_*` 节点
+
+### 14.2.1 定位
+
+`Tool_*` 用于调用 workflow 宿主环境提供的工具能力,例如文件、检索、编译、浏览器控制、DocCenter、子 workflow 调度等。
+
+`Tool_*` 属于 **3.17 标准扩展剖面**。不支持该剖面的运行时,应在加载期或执行期显式报错,而不是静默跳过。
+
+### 14.2.2 推荐结构
+
+```json
+{
+  "id": "Tool_ReadBlueprint",
+  "tool": "ReadFile",
+  "input": { "file_path": "docs/blueprint.md" },
+  "timeout": 15000,
+  "allowError": false,
+  "out": {
+    "$blueprint": "_result",
+    "$readMeta": "_toolResult"
+  },
+  "next": "LLM_AnalyzeBlueprint"
+}
+```
+
+字段约定:
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `tool` / `toolName` | `string` | 要调用的宿主工具名;两者二选一,推荐 `tool` |
+| `input` / `in` | `object` | 传给工具的输入对象;两者二选一,推荐 `input` |
+| `timeout` | `integer` | 可选,毫秒级超时 |
+| `allowError` | `boolean` | 为 `true` 时,工具失败不会直接使 workflow failed,而是将错误信息作为结果暴露给后续步骤 |
+| `out` | `object` | 输出映射,语义与其他节点保持一致 |
+
+### 14.2.3 输出映射
+
+`Tool_*` 执行完成后,至少应向 `out` 提供以下两个临时值:
+
+* `_result`:工具的主结果;若工具返回 envelope 且存在 `result` 字段,可优先解包为主结果
+* `_toolResult`:工具的完整原始返回对象,保留元数据、diff、undoId、usage 等附加字段
+
+### 14.2.4 错误语义
+
+* `allowError = false`:工具异常应导致当前节点报错,并使 workflow 进入 `failed`
+* `allowError = true`:运行时应发出 `tool_error`,同时允许后续节点读取错误 envelope 并自行决策
+
+### 14.2.5 `Subflow_*` 语义化别名
+
+当 workflow 需要显式表达“此节点会调度一个子 workflow”时,可以使用 `Subflow_*` 作为 `WorkflowRun` 的语义化别名。
+
+约束:
+
+* `Subflow_*` 不引入新的底层执行原语
+* 运行时可以将其降级为对宿主 `WorkflowRun` 能力的调用
+* editor 应将 `Subflow_*` 与普通 `Tool_*` 在视觉上区分开,便于识别子流程边界
+* 推荐字段包括:`workflow_path`、`params`、`mode`、`work_dir`、`emit_events`
+
+## 14.3 工具运行时事件
+
+3.17 在第十三章基础上新增以下事件类型:
+
+* `tool_start`
+* `tool_message`
+* `tool_done`
+* `tool_error`
+
+### 14.3.1 最小 payload
+
+#### `tool_start`
+
+```json
+{
+  "nodeId": "Tool_ReadBlueprint",
+  "name": "ReadFile",
+  "input": { "file_path": "docs/blueprint.md" }
+}
+```
+
+#### `tool_message`
+
+```json
+{
+  "nodeId": "Tool_ReadBlueprint",
+  "name": "ReadFile",
+  "level": "info",
+  "message": "Reading file",
+  "data": { "file_path": "docs/blueprint.md" }
+}
+```
+
+#### `tool_done`
+
+```json
+{
+  "nodeId": "Tool_ReadBlueprint",
+  "name": "ReadFile",
+  "output": { "result": "...file content..." }
+}
+```
+
+#### `tool_error`
+
+```json
+{
+  "nodeId": "Tool_ReadBlueprint",
+  "name": "ReadFile",
+  "error": "ENOENT: file not found"
+}
+```
+
+### 14.3.2 抛出要求
+
+工具相关事件必须满足以下要求:
+
+* 必须进入 workflow 的标准事件流
+* 必须可被外层窗口、SSE 客户端、日志系统、嵌套 workflow 调用方感知
+* 不得只在节点内部吞掉 message/progress/warn/error
+* 并行 run 下,必须带上足以归属 run 的标识(见 14.4)
+
+## 14.4 运行标识与并行归属
+
+### 14.4.1 `runID` 与 `clientRunToken`
+
+`runID` 是运行时正式分配的执行实例标识;但在部分实现中,它可能直到 workflow_start 之后或首次 checkpoint 之后才稳定。
+
+3.17 引入 `clientRunToken` 作为前置、稳定、由调用方或前端先生成的运行归属标识。
+
+要求:
+
+* `clientRunToken` 应从 `workflow_start` 开始就随事件一起抛出
+* 一次新的 `Run`、`rerunFromStep`、`executeFrom`,都应拥有新的 `clientRunToken`
+* 即使底层 `runID` 被复用,外层 UI 也不应因此把不同 session 混成同一条 run
+
+### 14.4.2 并行场景
+
+对于同一个 workflow 图上的多次并行运行:
+
+* `pause` / `resume` / `cancel` 应按选定 run 生效
+* `tool_*`、`llm_*`、`checkpoint`、`node_*` 事件都应能按 `runID` 或 `clientRunToken` 正确归属
+* “从第一步高亮到结束”的 run 轨迹应保持独立
+
+## 14.5 Checkpoint、断点续跑与复杂分支
+
+3.17 明确要求:基于 checkpoint 的继续执行不仅适用于简单串行流,也应覆盖 fork / loop 等复杂结构。
+
+### 14.5.1 `rerunFromStep` / `executeFrom`
+
+当调用方指定某个节点作为继续执行起点时,运行时应:
+
+* 保留本次继续执行仍然依赖的 artifacts 与变量
+* 对已完成分支信息、loop 进度、当前 step 指针进行归一化
+* 允许调用方提供 `overrides` 覆盖输入变量
+* 防止旧 checkpoint 中的已完成 sibling branch 错误地阻止新的下游执行
+
+### 14.5.2 Fork / Loop 最低要求
+
+* fork 续跑:允许从某个 fork 后继节点向下继续,且不会错误复用无关 sibling branch 的完成态
+* loop 续跑:允许从中途 iteration 之后继续,必要时重建 loop progress
+* parallel loop 续跑:允许在并行 loop 中基于 checkpoint 继续剩余迭代
+
+## 14.6 Workflow-of-Workflows
+
+3.17 认可“workflow 调度 workflow”作为推荐模式之一。
+
+典型形式包括:
+
+* 父 workflow 通过 `Tool_*` 调用宿主 `WorkflowRun` 能力
+* 父 workflow 并行调度多个子 workflow
+* 子 workflow 的 `node_*` / `tool_*` / `checkpoint` 摘要通过 `tool_message` 或宿主桥接向父级外抛
+
+该模式特别适合:
+
+* 多 worker 软件工程流水线
+* 数学/研究问题的分而治之探索
+* 带人工 gate 的审查与返工链路
+
+## 14.7 迁移建议
+
+从 3.16 迁移到 3.17 时,建议按以下顺序进行:
+
+1. 先发布文档与 capability 说明
+2. 保持现有 workflow JSON 继续使用 `"version": "3.16"`,避免一次性打断旧运行时
+3. 对支持 `Tool_*`、`tool_message`、`clientRunToken`、checkpoint 续跑的实现,显式标注支持 3.17 扩展剖面
+4. 等所有关键运行时完成升级后,再逐步切换到 `"version": "3.17"`
+
+---
+
+本规范的 canonical copy 发布于 **DocCenter Path 3**。若本地镜像与文档中心不一致,以文档中心最新版本为准。

+ 96 - 6
public/doc-center.html

@@ -85,6 +85,7 @@ header .spacer { flex:1; }
 .doc-item.active { background:var(--bg3); border-left-color:var(--accent); }
 .doc-item .doc-name { font-size:12px; color:var(--text); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
 .doc-item .doc-meta { display:flex; gap:6px; flex-wrap:wrap; }
+.doc-item .doc-ref { font-size:10px; color:var(--accent); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
 .doc-item .doc-tags { display:flex; gap:3px; margin-top:2px; flex-wrap:wrap; }
 .doc-badge {
   font-size:10px; padding:2px 6px; border-radius:999px; border:1px solid var(--border);
@@ -112,6 +113,7 @@ header .spacer { flex:1; }
 }
 .toolbar .doc-title { font-size:13px; font-weight:600; color:var(--text); flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
 .toolbar .doc-id { font-size:10px; color:var(--text2); margin-right:8px; }
+.toolbar .doc-ref { font-size:10px; color:var(--accent); max-width:240px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
 .mode-tabs { display:flex; gap:2px; }
 .mode-tab {
   padding:4px 12px; border-radius:4px; cursor:pointer; font-size:11px;
@@ -231,14 +233,14 @@ header .spacer { flex:1; }
       <input type="text" class="search-box" id="searchInput" placeholder="Search name / path / ID..." oninput="filterDocs()">
       <div class="sidebar-controls">
         <select id="sortSelect" onchange="renderVisibleDocs()">
+          <option value="path">Sort: Slot ↑ / ID ↓</option>
           <option value="name">Sort: Name</option>
-          <option value="path">Sort: Path</option>
           <option value="id">Sort: Doc ID</option>
           <option value="updated">Sort: Updated</option>
           <option value="version">Sort: Version</option>
         </select>
       </div>
-      <div class="sidebar-note">Find the official doc, copy its <code>Doc ID</code>, then fill it into VLCode's <code>Official Doc IDs</code>.</div>
+      <div class="sidebar-note">这里先看 <code>Slot / Path</code>,再填右侧显示的 <code>Doc ID</code>。真正配置到 VLCode 里的永远是 Doc ID,不是 Path。</div>
     </div>
     <div class="sidebar-actions">
       <div class="sa-btn" onclick="showCreateDialog()">+ New Doc</div>
@@ -253,7 +255,10 @@ header .spacer { flex:1; }
   <div class="main-content">
     <div class="toolbar" id="toolbar" style="display:none;">
       <span class="doc-id" id="docIdLabel"></span>
+      <span class="doc-ref" id="docRefLabel"></span>
       <span class="doc-title" id="docTitleLabel"></span>
+      <button class="hdr-btn" id="btnCopyDocId" onclick="copyCurrentDocId()" disabled>Copy ID</button>
+      <button class="hdr-btn" id="btnCopyDocLink" onclick="copyCurrentDocLink()" disabled>Copy Link</button>
       <div class="mode-tabs">
         <div class="mode-tab active" data-mode="view" onclick="setMode('view')">View</div>
         <div class="mode-tab" data-mode="edit" onclick="setMode('edit')">Edit</div>
@@ -315,6 +320,52 @@ let originalContent = '';  // track if modified
 let mode = 'view';         // 'view' | 'edit'
 const urlParams = new URLSearchParams(window.location.search);
 const embedMode = urlParams.get('embed') || '';
+const DOC_REF_PREFIX = 'vl://doc/';
+let pendingDocId = normalizeDocRefInput(urlParams.get('docId') || urlParams.get('ref'));
+
+function normalizeDocRefInput(value) {
+  if (typeof value === 'number') {
+    return Number.isInteger(value) && value > 0 ? value : null;
+  }
+  if (typeof value !== 'string') return null;
+  const trimmed = value.trim();
+  if (!trimmed) return null;
+  if (/^\d+$/.test(trimmed)) {
+    const n = parseInt(trimmed, 10);
+    return Number.isInteger(n) && n > 0 ? n : null;
+  }
+  if (trimmed.toLowerCase().startsWith(DOC_REF_PREFIX)) {
+    return normalizeDocRefInput(trimmed.slice(DOC_REF_PREFIX.length));
+  }
+  const inlineMatch = trimmed.match(/(?:^|[?&#])(?:docId|id|ref)=([^&#]+)/i);
+  if (inlineMatch?.[1]) {
+    try {
+      return normalizeDocRefInput(decodeURIComponent(inlineMatch[1]));
+    } catch {
+      return normalizeDocRefInput(inlineMatch[1]);
+    }
+  }
+  try {
+    const parsed = new URL(trimmed, window.location.origin);
+    const docId = parsed.searchParams.get('docId')
+      || parsed.searchParams.get('id')
+      || parsed.searchParams.get('ref');
+    if (docId) return normalizeDocRefInput(docId);
+  } catch {}
+  return null;
+}
+
+function formatDocRef(value) {
+  const docId = normalizeDocRefInput(value);
+  return docId ? `${DOC_REF_PREFIX}${docId}` : '';
+}
+
+function formatDocHref(value, { absolute = false } = {}) {
+  const docId = normalizeDocRefInput(value);
+  if (!docId) return '';
+  const path = `/doc-center.html?docId=${docId}`;
+  return absolute ? `${window.location.origin}${path}` : path;
+}
 
 function isOfficialDoc(doc = {}) {
   const id = parseInt(doc.id ?? doc._id, 10);
@@ -338,11 +389,12 @@ function parseDocVersion(doc = {}) {
 }
 
 function sortDocs(docs) {
-  const sortKey = document.getElementById('sortSelect')?.value || 'name';
+  const sortKey = document.getElementById('sortSelect')?.value || 'path';
   const items = [...docs];
   if (sortKey === 'path') {
     return items.sort((a, b) =>
       (parseInt(a.path || '0', 10) || Number.MAX_SAFE_INTEGER) - (parseInt(b.path || '0', 10) || Number.MAX_SAFE_INTEGER)
+      || (parseInt(b.id || b._id || '0', 10) || 0) - (parseInt(a.id || a._id || '0', 10) || 0)
       || String(a.name || '').localeCompare(String(b.name || ''), 'zh-CN', { numeric: true, sensitivity: 'base' })
     );
   }
@@ -425,8 +477,14 @@ async function refreshList() {
   setStatus('Fetching document list...');
   try {
     const res = await dcApi('list', { keyword: '', tagId: 0, page: 1, pageSize: 200 });
-    allDocs = res?.data?.list || res?.list || [];
+    const list = res?.data?.list || res?.list || (Array.isArray(res?.data) ? res.data : []);
+    allDocs = list;
     renderVisibleDocs();
+    if (pendingDocId) {
+      const targetDocId = pendingDocId;
+      pendingDocId = null;
+      await selectDoc(targetDocId, { updateHistory: false });
+    }
     setStatus(`${allDocs.length} documents loaded`);
   } catch (e) {
     listEl.innerHTML = `<div class="list-status" style="color:var(--red)">Error: ${esc(e.message)}</div>`;
@@ -449,10 +507,11 @@ function renderDocList(docs) {
     const latestVersion = d.latestVersion || d.latest_version || d.version || 0;
     return `<div class="doc-item${isActive ? ' active' : ''}" data-id="${docId}" onclick="selectDoc(${docId})">
       <span class="doc-name">${esc(d.name || 'Untitled')}</span>
+      <div class="doc-ref">${esc(formatDocRef(docId))}</div>
       <div class="doc-meta">
         <span class="doc-badge ${scope.className}">${scope.label}</span>
         <span class="doc-badge doc-badge-id">ID ${docId}</span>
-        <span class="doc-badge doc-badge-path">Path ${d.path ?? docId}</span>
+        <span class="doc-badge doc-badge-path">Slot ${d.path ?? docId}</span>
         <span class="doc-badge doc-badge-version">v${latestVersion}</span>
       </div>
       <div class="doc-tags">${tags}${formatDate(d.updatedAt || d.createdAt) ? `<span class="doc-tag">${esc(formatDate(d.updatedAt || d.createdAt))}</span>` : ''}</div>
@@ -465,7 +524,7 @@ function filterDocs() {
 }
 
 // --- Select & Load Document ---
-async function selectDoc(docId) {
+async function selectDoc(docId, { updateHistory = true } = {}) {
   if (currentDoc && currentDoc.id === docId) return;
   if (currentDoc && isModified()) {
     if (!confirm('You have unsaved changes. Discard them?')) return;
@@ -483,6 +542,7 @@ async function selectDoc(docId) {
     };
     originalContent = currentDoc.content;
     showDocument();
+    if (updateHistory) updateDocLocation(currentDoc.id);
     updateButtons();
     renderDocList(allDocs); // refresh active highlight
     setStatus(`Loaded: ${currentDoc.name}`);
@@ -496,7 +556,10 @@ function showDocument() {
   const toolbar = document.getElementById('toolbar');
   toolbar.style.display = 'flex';
   document.getElementById('docIdLabel').textContent = `Doc ID ${currentDoc.id}`;
+  document.getElementById('docRefLabel').textContent = formatDocRef(currentDoc.id);
   document.getElementById('docTitleLabel').textContent = currentDoc.name;
+  document.getElementById('btnCopyDocId').disabled = false;
+  document.getElementById('btnCopyDocLink').disabled = false;
   renderContent();
 }
 
@@ -542,6 +605,33 @@ function updateStatusChars() {
   }
 }
 
+function updateDocLocation(docId) {
+  if (!window.history?.replaceState) return;
+  const url = new URL(window.location.href);
+  if (embedMode) url.searchParams.set('embed', embedMode);
+  else url.searchParams.delete('embed');
+  url.searchParams.delete('t');
+  if (docId) url.searchParams.set('docId', String(docId));
+  else url.searchParams.delete('docId');
+  url.searchParams.delete('ref');
+  window.history.replaceState({}, '', `${url.pathname}?${url.searchParams.toString()}`);
+}
+
+function copyCurrentDocId() {
+  if (!currentDoc?.id) return;
+  navigator.clipboard.writeText(String(currentDoc.id))
+    .then(() => toast('Doc ID copied', 'success'))
+    .catch(() => toast('Failed to copy doc ID', 'error'));
+}
+
+function copyCurrentDocLink() {
+  if (!currentDoc?.id) return;
+  const value = formatDocHref(currentDoc.id, { absolute: true });
+  navigator.clipboard.writeText(value)
+    .then(() => toast('Doc link copied', 'success'))
+    .catch(() => toast('Failed to copy doc link', 'error'));
+}
+
 // --- Mode switching ---
 function setMode(m) {
   if (embedMode === 'landing' && m !== 'view') return;

+ 173 - 25
public/index.html

@@ -163,6 +163,15 @@ main { flex:1; display:flex; overflow:hidden; }
 .doc-id-section-toggle:hover { color:var(--text); }
 .settings-doc-card.is-locked { opacity:0.78; }
 .settings-doc-card.is-locked input { opacity:0.65; cursor:not-allowed; }
+.settings-doc-header { display:flex; align-items:flex-start; justify-content:space-between; gap:8px; }
+.settings-doc-card .settings-doc-title { font-size:11px; color:var(--text); font-weight:600; }
+.settings-doc-card .settings-doc-meta { font-size:9px; color:var(--text2); text-transform:uppercase; letter-spacing:0.4px; }
+.settings-doc-card .settings-doc-ref { font-size:10px; color:var(--accent); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
+.settings-doc-card .settings-doc-link { font-size:9px; color:var(--text2); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
+.settings-doc-actions { display:flex; gap:6px; margin-top:2px; }
+.settings-doc-action { flex:1; background:var(--bg2); border:1px solid var(--border); color:var(--text2); border-radius:4px; cursor:pointer; font-size:10px; padding:4px 6px; font-family:var(--font); }
+.settings-doc-action:hover { color:var(--accent); border-color:var(--accent); }
+.settings-doc-action:disabled { opacity:0.5; cursor:not-allowed; }
 .pc-doc-toggle { font-size:9px; opacity:0.5; cursor:pointer; padding:1px 4px; border-radius:3px; background:none; border:none; color:var(--text2); }
 .pc-doc-toggle:hover { opacity:1; }
 .pc-doc-toggle.active { color:var(--green); opacity:1; }
@@ -769,8 +778,6 @@ main { flex:1; display:flex; overflow:hidden; }
 .settings-provider-hint { font-size:10px; color:var(--text2); margin:-2px 0 10px; line-height:1.5; }
 .settings-doc-grid { display:grid; grid-template-columns:repeat(2, minmax(0, 1fr)); gap:8px; margin-top:8px; }
 .settings-doc-card { display:flex; flex-direction:column; gap:4px; padding:10px; border:1px solid var(--border); border-radius:8px; background:var(--bg); }
-.settings-doc-card .settings-doc-title { font-size:11px; color:var(--text); font-weight:600; }
-.settings-doc-card .settings-doc-meta { font-size:9px; color:var(--text2); text-transform:uppercase; letter-spacing:0.4px; }
 .settings-doc-card input { margin-top:2px; }
 .settings-doc-hint { font-size:10px; color:var(--text2); line-height:1.5; margin-top:8px; }
 .mode-tab[data-mode="docs"] { color:var(--yellow); }
@@ -954,11 +961,11 @@ main { flex:1; display:flex; overflow:hidden; }
       <div class="landing-docs-head">
         <div>
           <h2>Official DocCenter</h2>
-          <div class="landing-docs-copy">Search official docs, copy the stable <code>Doc ID</code>, then fill it into VLCode's settings after login.</div>
+          <div class="landing-docs-copy">Search official docs, confirm the slot/path, then copy the numeric <code>Doc ID</code> into VLCode after login. The page also shows the matching ref and viewer link for debugging.</div>
         </div>
         <button class="hdr-btn" onclick="refreshLandingDocsFrame()">Refresh</button>
       </div>
-      <div class="landing-docs-note">Workflow execution resolves official specs and workflow prompts by <code>Doc ID</code> first. Paths stay as reserved aliases; IDs are the runtime source of truth.</div>
+      <div class="landing-docs-note">Workflow execution resolves official specs and workflow prompts by <code>Doc ID</code>. Slots stay as reserved aliases; viewer links and <code>Doc Ref</code> are only debug helpers.</div>
       <iframe id="landingDocsFrame" class="landing-docs-frame" src="/doc-center.html?embed=landing" title="VLCode DocCenter"></iframe>
     </div>
   </div>
@@ -1075,14 +1082,14 @@ main { flex:1; display:flex; overflow:hidden; }
         </span>
       </h4>
       <div id="docIdConfigBody">
-        <div class="doc-id-panel-note">Path 是文档类别,真正执行引用的是 Doc ID。官方文档默认小于 1000,用户文档建议从 1000 以后开始。</div>
+        <div class="doc-id-panel-note">这里真正填写的是 <code>Doc ID</code>。也支持直接粘贴 <code>vl://doc/&lt;id&gt;</code> 或 <code>/doc-center.html?docId=&lt;id&gt;</code>,保存时会自动归一成 Doc ID。Slot 只保留给 runtime 内部映射。</div>
         <div class="doc-id-section-title">Core Runtime</div>
         <div class="doc-id-grid" id="docIdCoreGrid"></div>
         <div class="doc-id-section-title doc-id-section-toggle" onclick="toggleDocWorkflowGrid()">
           <span>Workflow Docs</span>
-          <span id="docWorkflowToggle">&#9654;</span>
+          <span id="docWorkflowToggle">&#9660;</span>
         </div>
-        <div class="doc-id-grid" id="docIdWorkflowGrid" style="display:none;"></div>
+        <div class="doc-id-grid" id="docIdWorkflowGrid" style="display:flex;"></div>
         <div class="doc-id-section-title">Locked By Tooling</div>
         <div class="doc-id-grid" id="docIdLockedGrid"></div>
       </div>
@@ -1348,7 +1355,7 @@ main { flex:1; display:flex; overflow:hidden; }
     <input type="text" id="settingsWorkDir" disabled style="opacity:0.6">
     <div style="margin-top:12px;padding-top:12px;border-top:1px solid var(--border);">
       <label>Official Doc IDs</label>
-      <div class="settings-doc-hint">`VL / Theme / workflow docs` 可改;`Meta Spec / Workflow Spec` 只读。后端会优先按这些 Doc ID 取文档。</div>
+      <div class="settings-doc-hint">输入框里填的是 <code>Doc ID</code>。也支持直接粘贴 <code>vl://doc/&lt;id&gt;</code> 或 <code>/doc-center.html?docId=&lt;id&gt;</code>,保存时会自动归一成稳定 Doc ID;<code>Meta Spec / Workflow Spec</code> 仍保持只读。</div>
       <div class="settings-doc-grid" id="settingsDocIdCoreGrid"></div>
       <div class="settings-doc-hint">Workflow Docs</div>
       <div class="settings-doc-grid" id="settingsDocIdWorkflowGrid"></div>
@@ -2452,7 +2459,7 @@ async function doLandingTokenLogin() {
 function refreshLandingDocsFrame() {
   const frame = $('landingDocsFrame');
   if (!frame) return;
-  frame.src = `/doc-center.html?embed=landing&t=${Date.now()}`;
+  frame.src = buildDocCenterEmbedSrc({ embed: 'landing', force: true });
 }
 
 async function enterIDE() {
@@ -3339,6 +3346,57 @@ async function restartBackend() {
 }
 
 // ===================== SETTINGS =====================
+const DOC_REF_PREFIX = 'vl://doc/';
+let _docCenterFocusDocId = null;
+
+function normalizeDocRefInput(value) {
+  if (typeof value === 'number') {
+    return Number.isInteger(value) && value > 0 ? value : null;
+  }
+  if (typeof value !== 'string') return null;
+  const trimmed = value.trim();
+  if (!trimmed) return null;
+  if (/^\d+$/.test(trimmed)) {
+    const n = parseInt(trimmed, 10);
+    return Number.isInteger(n) && n > 0 ? n : null;
+  }
+  if (trimmed.toLowerCase().startsWith(DOC_REF_PREFIX)) {
+    return normalizeDocRefInput(trimmed.slice(DOC_REF_PREFIX.length));
+  }
+  const inlineMatch = trimmed.match(/(?:^|[?&#])(?:docId|id|ref)=([^&#]+)/i);
+  if (inlineMatch?.[1]) {
+    try {
+      return normalizeDocRefInput(decodeURIComponent(inlineMatch[1]));
+    } catch {
+      return normalizeDocRefInput(inlineMatch[1]);
+    }
+  }
+  try {
+    const parsed = new URL(trimmed, window.location.origin);
+    const docId = parsed.searchParams.get('docId')
+      || parsed.searchParams.get('id')
+      || parsed.searchParams.get('ref');
+    if (docId) return normalizeDocRefInput(docId);
+  } catch {}
+  return null;
+}
+
+function formatDocRef(value) {
+  const docId = normalizeDocRefInput(value);
+  return docId ? `${DOC_REF_PREFIX}${docId}` : '';
+}
+
+function formatDocHref(value) {
+  const docId = normalizeDocRefInput(value);
+  return docId ? `/doc-center.html?docId=${docId}` : '';
+}
+
+function escapeAttr(s) {
+  return escapeHtml(String(s || ''))
+    .replace(/"/g, '&quot;')
+    .replace(/'/g, '&#39;');
+}
+
 const DOC_ID_CORE_FIELDS = [
   { alias: 'vlSyntax', label: 'VL Syntax', path: 1 },
   { alias: 'theme', label: 'Theme', path: 2 },
@@ -3366,23 +3424,107 @@ function docIdInputId(prefix, alias) {
 }
 
 function getDocIdValue(settings, alias) {
-  const editable = parseInt(settings?.docIdOverrides?.[alias], 10);
-  if (Number.isInteger(editable) && editable > 0) return editable;
-  const locked = parseInt(settings?.coreDocIds?.[alias], 10);
-  if (Number.isInteger(locked) && locked > 0) return locked;
+  const editable = normalizeDocRefInput(settings?.docIdOverrides?.[alias]);
+  if (editable) return editable;
+  const locked = normalizeDocRefInput(settings?.coreDocIds?.[alias]);
+  if (locked) return locked;
   return '';
 }
 
+function readDocBindingInput(prefix, alias) {
+  return $(docIdInputId(prefix, alias))?.value?.trim?.() || '';
+}
+
+function copyTextValue(value, successText) {
+  if (!value) {
+    setStatus('Document reference is empty', 'red');
+    return;
+  }
+  if (navigator.clipboard?.writeText) {
+    navigator.clipboard.writeText(value)
+      .then(() => setStatus(successText, 'green'))
+      .catch(() => setStatus('Copy failed', 'red'));
+    return;
+  }
+  const temp = document.createElement('textarea');
+  temp.value = value;
+  document.body.appendChild(temp);
+  temp.select();
+  try {
+    document.execCommand('copy');
+    setStatus(successText, 'green');
+  } catch {
+    setStatus('Copy failed', 'red');
+  } finally {
+    temp.remove();
+  }
+}
+
+function copyConfiguredDocRef(prefix, alias, kind = 'ref') {
+  const raw = readDocBindingInput(prefix, alias);
+  const docId = normalizeDocRefInput(raw);
+  if (!docId) {
+    setStatus('Invalid document reference', 'red');
+    return;
+  }
+  const value = kind === 'link'
+    ? `${window.location.origin}${formatDocHref(docId)}`
+    : String(docId);
+  copyTextValue(value, kind === 'link' ? 'Doc link copied' : 'Doc ID copied');
+}
+
+function openConfiguredDoc(prefix, alias) {
+  const raw = readDocBindingInput(prefix, alias);
+  const docId = normalizeDocRefInput(raw);
+  if (!docId) {
+    setStatus('Invalid document reference', 'red');
+    return;
+  }
+  _docCenterFocusDocId = docId;
+  if (prefix === 'settings') closeSettings();
+  switchMode('docs');
+}
+
+function syncDocBindingCard(input) {
+  const card = input?.closest?.('.settings-doc-card');
+  if (!card) return;
+  const docId = normalizeDocRefInput(input.value);
+  const docRef = formatDocRef(docId);
+  const docHref = formatDocHref(docId);
+  const refEl = card.querySelector('.settings-doc-ref');
+  const linkEl = card.querySelector('.settings-doc-link');
+  const openBtn = card.querySelector('.settings-doc-action[data-action="open"]');
+  const copyBtn = card.querySelector('.settings-doc-action[data-action="copy-id"]');
+  const inputEl = card.querySelector('input');
+  if (inputEl && docId && /^\s*(vl:\/\/doc\/|\/doc-center\.html\?docId=|https?:)/i.test(input.value)) {
+    inputEl.value = String(docId);
+  }
+  if (refEl) refEl.textContent = docRef || 'Not set';
+  if (linkEl) linkEl.textContent = docHref || 'No viewer link yet';
+  if (openBtn) openBtn.disabled = !docId;
+  if (copyBtn) copyBtn.disabled = !docId;
+}
+
 function renderDocIdFieldGroup(containerId, prefix, fields, settings, { locked = false } = {}) {
   const container = $(containerId);
   if (!container) return;
   container.innerHTML = fields.map((field) => {
     const value = getDocIdValue(settings, field.alias);
+    const docRef = formatDocRef(value);
+    const docHref = formatDocHref(value);
     return `
       <label class="settings-doc-card${locked ? ' is-locked' : ''}">
-        <span class="settings-doc-title">${escapeHtml(field.label)}</span>
-        <span class="settings-doc-meta">Path ${field.path}</span>
-        <input type="number" id="${docIdInputId(prefix, field.alias)}" min="1" placeholder="Doc ID" value="${value}" ${locked ? 'disabled' : ''}>
+        <span class="settings-doc-header">
+          <span class="settings-doc-title">${escapeHtml(field.label)}</span>
+          <span class="settings-doc-meta">Slot ${field.path}</span>
+        </span>
+        <input type="text" id="${docIdInputId(prefix, field.alias)}" placeholder="Doc ID / doc link" value="${escapeAttr(value ? String(value) : '')}" oninput="syncDocBindingCard(this)" ${locked ? 'disabled' : ''}>
+        <span class="settings-doc-ref">${docRef ? escapeHtml(docRef) : 'Not set'}</span>
+        <span class="settings-doc-link">${docHref ? escapeHtml(docHref) : 'No viewer link yet'}</span>
+        <div class="settings-doc-actions">
+          <button type="button" class="settings-doc-action" data-action="open" onclick="event.preventDefault();event.stopPropagation();openConfiguredDoc('${prefix}','${field.alias}')" ${value ? '' : 'disabled'}>Open</button>
+          <button type="button" class="settings-doc-action" data-action="copy-id" onclick="event.preventDefault();event.stopPropagation();copyConfiguredDocRef('${prefix}','${field.alias}')" ${value ? '' : 'disabled'}>Copy ID</button>
+        </div>
       </label>
     `;
   }).join('');
@@ -3404,9 +3546,8 @@ function applyDocIdSettings(settings = {}) {
 function collectDocIdSettings(prefix) {
   const out = {};
   for (const field of DOC_ID_EDITABLE_FIELDS) {
-    const raw = $(docIdInputId(prefix, field.alias))?.value?.trim?.() || '';
-    const value = parseInt(raw, 10);
-    out[field.alias] = Number.isInteger(value) && value > 0 ? value : null;
+    const raw = readDocBindingInput(prefix, field.alias);
+    out[field.alias] = normalizeDocRefInput(raw);
   }
   return out;
 }
@@ -8133,13 +8274,20 @@ function _hasRenderableMetadata(meta) {
   return false;
 }
 
-async function resolveDocCenterEmbedSrc({ force = false } = {}) {
-  return force
-    ? `/doc-center.html?embed=ide&t=${Date.now()}`
-    : '/doc-center.html?embed=ide';
+function buildDocCenterEmbedSrc({ embed = 'ide', docId = null, force = false } = {}) {
+  const params = new URLSearchParams();
+  if (embed) params.set('embed', embed);
+  const normalizedDocId = normalizeDocRefInput(docId);
+  if (normalizedDocId) params.set('docId', String(normalizedDocId));
+  if (force) params.set('t', String(Date.now()));
+  return `/doc-center.html?${params.toString()}`;
+}
+
+async function resolveDocCenterEmbedSrc({ force = false, embed = 'ide', docId = _docCenterFocusDocId } = {}) {
+  return buildDocCenterEmbedSrc({ embed, docId, force });
 }
 
-async function showDocCenterMode() {
+async function showDocCenterMode(docId = _docCenterFocusDocId) {
   $('editorTabs').style.display = 'none';
   $('cmEditorWrap').style.display = 'none';
   $('editor').style.display = 'none';
@@ -8147,7 +8295,7 @@ async function showDocCenterMode() {
   $('mdPreview').style.display = 'none';
   $('editorPlaceholder').style.display = 'none';
   $('iframeContainer').style.display = 'block';
-  const src = await resolveDocCenterEmbedSrc();
+  const src = await resolveDocCenterEmbedSrc({ docId });
   showModeIframe('docs', src, async () => null);
   setStatus('Documentation ready', 'green');
 }

+ 3 - 3
scripts/publish-core-docs.js

@@ -114,14 +114,14 @@ async function buildManifest(catalog) {
       name: `VL Workflow Spec ${WORKFLOW_SPEC_DOC_VERSION}`,
       description: 'Canonical workflow specification with Doc ID priority, Tool_* runtime semantics, and checkpoint rerun compatibility.',
       currentContent: fs.readFileSync(path.join(ROOT, 'docs', `vl-workflow-spec-${WORKFLOW_SPEC_DOC_VERSION}.md`), 'utf-8'),
-      changeNote: `v${WORKFLOW_SPEC_DOC_VERSION} — document Doc ID priority, locked core spec slots, and workflow runtime compatibility`,
+      changeNote: `v${WORKFLOW_SPEC_DOC_VERSION} — clarify Doc ID as persisted workflow binding, keep path as slot alias, and align workflow runtime compatibility`,
     },
     {
       path: '4',
       name: 'VL Metadata Spec',
       description: 'Canonical VL ProjectMeta schema for normalization, diff, workflow regeneration, and IDE tooling.',
-      currentContent: fs.readFileSync(path.join(ROOT, 'docs', 'vl-metadata-spec-3.0.md'), 'utf-8'),
-      changeNote: 'v3.0 — define canonical ProjectMeta schema, ID rules, theme slot metadata, and legacy normalization mapping',
+      currentContent: fs.readFileSync(path.join(ROOT, 'docs', 'vl-metadata-spec-3.1.md'), 'utf-8'),
+      changeNote: 'v3.1 — clarify Doc ID bindings stay outside ProjectMeta while keeping canonical schema and theme slot metadata rules',
     },
   ];
 }

+ 5 - 5
scripts/publish-workflow-spec.js

@@ -6,7 +6,7 @@ import path from 'path';
 import { DOCCENTER_API_URL } from '../src/data/doc-paths.js';
 
 const ROOT = '/Users/ivx/Documents/VLCode-Lite';
-const SPEC_PATH = path.join(ROOT, 'docs', 'vl-workflow-spec-3.18.md');
+const SPEC_PATH = path.join(ROOT, 'docs', 'vl-workflow-spec-3.19.md');
 const AUTH_PATH = path.join(os.homedir(), '.vl-code', 'auth.json');
 const APPLY = process.argv.includes('--apply');
 
@@ -65,21 +65,21 @@ async function main() {
   const remoteContent = remote?.data?.currentContent || '';
 
   if (remoteContent === currentContent) {
-    console.log(`unchanged path 3  VL Workflow Spec 3.18  (docId ${docId})`);
+    console.log(`unchanged path 3  VL Workflow Spec 3.19  (docId ${docId})`);
     return;
   }
 
   if (!APPLY) {
-    console.log(`publish path 3  VL Workflow Spec 3.18  (docId ${docId})`);
+    console.log(`publish path 3  VL Workflow Spec 3.19  (docId ${docId})`);
     return;
   }
 
   const result = await dcPost('SERVICE_DocCenter_SaveAsVersion', {
     path: '3',
-    name: 'VL Workflow Spec 3.18',
+    name: 'VL Workflow Spec 3.19',
     description: 'Canonical workflow specification with Doc ID resolution priority, Tool_* extension profiles, tool events, clientRunToken, and checkpoint rerun semantics.',
     currentContent,
-    changeNote: 'v3.18 — document Doc ID priority, locked core spec slots, Tool_* runtime semantics, and workflow-of-workflows compatibility',
+    changeNote: 'v3.19 — clarify persisted Doc ID bindings, path slot aliases, and runtime DocCenter resolution priority',
   });
 
   console.log(JSON.stringify({

+ 55 - 2
src/data/doc-paths.js

@@ -18,12 +18,18 @@ export const DOCCENTER_API_URL = 'https://v4pre.visuallogic.ai/api/12027022';
 /** Local DocCenter API base URL (fallback when available) */
 export const DOCCENTER_LOCAL_URL = 'http://localhost:9300';
 
+/** Stable doc reference prefix exposed in UI */
+export const DOC_REF_PREFIX = 'vl://doc/';
+
+/** Embedded DocCenter viewer route used by VLCode */
+export const DOCCENTER_VIEW_PATH = '/doc-center.html';
+
 // ─── Core Specs (paths 1-9, RESERVED) ──────────────────────────
 export const CORE_DOCS = {
   vlSyntax:       { path: 1,   docId: 1,    desc: 'VL Syntax 3.7' },
   theme:          { path: 2,   docId: 4,    desc: 'THEME 6.6 Enterprise' },
-  metaSpec:       { path: 4,   docId: 16,   desc: 'VL Metadata Spec 3.0' },
-  workflowSpec:   { path: 3,   docId: 2,    desc: 'Workflow Spec 3.18' },
+  metaSpec:       { path: 4,   docId: 16,   desc: 'VL Metadata Spec 3.1' },
+  workflowSpec:   { path: 3,   docId: 2,    desc: 'Workflow Spec 3.19' },
   componentFactory: { path: 6, docId: 15,   desc: 'VL Component Factory' },
   docCenterAPI:   { path: 7,   docId: 95,   desc: 'DocCenter API' },
   workspaceAPI:   { path: 8,   docId: 94,   desc: 'Workspace Doc API' },
@@ -162,10 +168,57 @@ export function pathFor(alias) {
 }
 
 export function normalizeDocId(value) {
+  if (typeof value === 'number') {
+    return Number.isInteger(value) && value > 0 ? value : null;
+  }
+  if (typeof value !== 'string') return null;
+  const trimmed = value.trim();
+  if (!trimmed) return null;
+
+  if (/^\d+$/.test(trimmed)) {
+    const n = Number.parseInt(trimmed, 10);
+    return Number.isInteger(n) && n > 0 ? n : null;
+  }
+
+  if (trimmed.toLowerCase().startsWith(DOC_REF_PREFIX)) {
+    return normalizeDocId(trimmed.slice(DOC_REF_PREFIX.length));
+  }
+
+  const inlineMatch = trimmed.match(/(?:^|[?&#])(?:docId|id|ref)=([^&#]+)/i);
+  if (inlineMatch?.[1]) {
+    try {
+      return normalizeDocId(decodeURIComponent(inlineMatch[1]));
+    } catch {
+      return normalizeDocId(inlineMatch[1]);
+    }
+  }
+
+  try {
+    const parsed = new URL(trimmed, 'http://vl.local');
+    const docId = parsed.searchParams.get('docId')
+      || parsed.searchParams.get('id')
+      || parsed.searchParams.get('ref');
+    if (docId) return normalizeDocId(docId);
+  } catch {
+    return null;
+  }
+
   const n = Number.parseInt(value, 10);
   return Number.isInteger(n) && n > 0 ? n : null;
 }
 
+export function docRefForId(docId) {
+  const normalized = normalizeDocId(docId);
+  return normalized ? `${DOC_REF_PREFIX}${normalized}` : '';
+}
+
+export function docHrefForId(docId, { origin = '' } = {}) {
+  const normalized = normalizeDocId(docId);
+  if (!normalized) return '';
+  const prefix = origin ? String(origin).replace(/\/+$/, '') : '';
+  return `${prefix}${DOCCENTER_VIEW_PATH}?docId=${normalized}`;
+}
+
 export function getDefaultCoreDocIds() {
   return Object.fromEntries(
     CORE_DOC_ALIASES.map((alias) => [alias, normalizeDocId(DOC_REGISTRY[alias]?.docId)])

+ 1 - 1
src/data/versions.js

@@ -34,7 +34,7 @@ export const THEME_PROFILE = 'enterprise';
 export const WORKFLOW_VERSION = '3.16';
 
 /** Canonical workflow spec document version in DocCenter Path 3 */
-export const WORKFLOW_SPEC_DOC_VERSION = '3.18';
+export const WORKFLOW_SPEC_DOC_VERSION = '3.19';
 
 /** Parser API URL */
 export const PARSEVL_URL = 'https://blaonkhbvcgvmcgk3tkrxo2cc40ojwpn.lambda-url.us-west-2.on.aws';