@guandata/guanwf 0.1.828 → 0.1.829

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## @guandata/guanwf 0.1.829 - 2026-08-27
4
+
5
+ - 新增 Python Runtime 查询与结构化上下文,生成脚本前可确认目标 Python 版本、运行模式和镜像来源;信息不足时按兼容规则保守处理。
6
+ - 新增 Python 输入规模与内存预检,普通运行会在提交前给出 `PASS`、`NO_GO` 或 `UNKNOWN`,高风险场景默认阻断。
7
+ - 新增输出绑定、改绑、解绑、已注册输出回填及能力查询命令,以 task 和 output slot 精确定位,支持 dry-run 与并发变更检查。
8
+ - 强化工作流保存和执行闭环:提供版本差异、冲突与 no-op 诊断,保存后 fresh readback;运行后逐项验证输出计算、注册、更新和可读性。
9
+ - 实例日志会明确完整性、缺失尝试和根因证据;实例状态与任务 ID 的处理更一致。
10
+ - 导出时保护含凭据的工作区文件,并保留未知扩展字段、孤立 sidecar 和 Python 节点往返信息,减少编辑已有工作流时的数据损失。
11
+ - 统一结构化错误与拼错子命令的非零退出行为。
12
+
3
13
  ## @guandata/guanwf 0.1.828 - 2026-08-25
4
14
 
5
15
  - 支持企业 OIDC 认证上下文,并同步升级底层请求兼容能力,改善受控运行环境中的工作流操作稳定性。
package/README.md CHANGED
@@ -19,6 +19,7 @@ guanwf create --name "我的数据流" --parent-dir <dirId>
19
19
  guanwf create --name "HTTP作业" --type HTTP --parent-dir <dirId>
20
20
  guanwf create --name "SQL作业" --type SQL --parent-dir <dirId>
21
21
  guanwf edit <parentWorkflowId>
22
+ guanwf python-runtime --dir <workdir> --format json
22
23
  guanwf export --dir <workdir>
23
24
  guanwf preview --dir <workdir>
24
25
  guanwf save --dir <workdir> --dry-run
@@ -47,6 +48,14 @@ guanwf install-skill
47
48
 
48
49
  ## 版本更新
49
50
 
51
+ ### @guandata/guanwf 0.1.829
52
+
53
+ - 新增 Python Runtime 上下文和输入内存预检,脚本生成与运行会依据目标环境能力保守判断。
54
+ - 新增输出绑定、改绑、解绑和已注册输出回填命令,支持 dry-run、精确槽位定位与并发变更检查。
55
+ - 保存后会回读目标字段,执行后逐项验证输出计算、注册、更新与可读性。
56
+ - 实例日志补充完整性和根因证据,并增强工作流往返编辑时的未知字段、sidecar 与敏感文件保护。
57
+ - 结构化错误和非零退出状态更适合自动化流程判断。
58
+
50
59
  ### @guandata/guanwf 0.1.828
51
60
 
52
61
  - 支持企业 OIDC 认证上下文,并升级底层请求兼容能力,改善受控环境中的工作流操作稳定性。
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guandata/guanwf",
3
- "version": "0.1.828",
3
+ "version": "0.1.829",
4
4
  "description": "观远工作流数据流编辑工具 - 创建、编辑、导出、预览、保存数据流",
5
5
  "bin": {
6
6
  "guanwf": "bin/run.js"
@@ -62,7 +62,7 @@ compatibility: "Requires Node.js 14+. Install via npm link --foreground-scripts
62
62
 
63
63
  ```
64
64
  <workdir>/
65
- workflow.go # 可编辑事实源:工作流 DAG(节点 + 依赖)
65
+ workflow.go # 可编辑事实源;含凭据时 export 自动收紧为 0600/当前用户 ACL 并加入 .gitignore
66
66
  nodes/<节点ID>/
67
67
  etl.go # 可编辑事实源:DATAFLOW/DB_DATAFLOW 节点(guanetl DSL)
68
68
  *.sql # 可编辑事实源:SQL 节点外置文件(ReadSQLFile 引用)
@@ -75,6 +75,7 @@ compatibility: "Requires Node.js 14+. Install via npm link --foreground-scripts
75
75
  body.json/body.txt # HTTP 请求体
76
76
  _input.json / meta.json # 导入快照/派生产物,不手改
77
77
  _layout.json # 画布坐标(edit 保留线上坐标;新节点自动布局补位)
78
+ _python_runtime_context.json # Python Runtime 结构化上下文(命令生成,只读)
78
79
  _wf_state.json # 系统状态,不手改
79
80
  _parent_snapshot.json # 脱敏服务端快照,不手改
80
81
  _parent_merge_snapshot.json # 受限权限的精确三方合并基线,不手改
@@ -127,6 +128,10 @@ guanwf instance latest --dir <workdir> --logs -f json
127
128
 
128
129
  `node add` 只生成节点目录和可粘贴 DSL 片段,不自动修改 `workflow.go` AST。节点真实字段、
129
130
  参数类型兼容和外置文件约定见 `references/TASK_NODE_CONTRACTS.md`。
131
+ `instance logs --full -f json` 固定返回 `complete/attemptsExpected/attemptsReturned/missingAttempts/fallbackUrl`,
132
+ 无法证明重跑/恢复历史完整时明确 `complete=false` 并非零退出;诊断同时返回
133
+ `firstFailure/rootCause/secondaryErrors` 及 task/log evidence refs。实例和任务 ID 全程按不透明字符串
134
+ 处理,状态统一输出 canonical `RUNNING_EXECUTION`,后端历史拼写仅保留在 `backendState`。
130
135
 
131
136
  ### 1. 缺上下文先查,不要猜
132
137
 
@@ -142,12 +147,38 @@ guancli workflow list --parent-dir <dirId> # 指定父目录
142
147
  guancli workflow get <processDefinitionId> # 查看工作流详情(含数据流节点详情)
143
148
  guancli workflow get <id> --raw # 输出原始 JSON
144
149
  guancli workflow get <id> -f json # JSON 格式输出
150
+ guanwf get <id> -f json # 原始定义 + dataflow sidecar 可达性
151
+ guanwf status <id> -f json # 轻量状态 + raw/reachable/orphan 分类
145
152
  ```
146
153
 
147
154
  `workflow tree` 调用 `/api/directory/MASTER_FLOW/authorized-tree` 获取工作流目录树。
148
155
 
149
156
  数据流以 SUB_PROCESS 节点形式内嵌在工作流中(`dataFlowNode=true`),不作为独立记录存在。
150
157
  `list` 默认展示内嵌数据流(遍历每个工作流并汇总),使用 `--show-embedded=false` 可关闭。
158
+ 顶层 `dataflowJson` 只是 raw sidecar 容器,不能用其 length 判断可执行节点数。只把当前
159
+ SUB_PROCESS task 引用的条目视为 `reachableDataflows`;其余为 `orphanSidecars`,get/edit/status
160
+ 会显式列出。保存会原样保留孤儿 sidecar 及未知扩展字段,但不会把它们放入执行拓扑,也不会默认清理。
161
+
162
+ 涉及 Python 节点时,在编写或修改 `script.py` **之前**查询目标环境的真实 Runtime:
163
+
164
+ ```bash
165
+ guanwf python-runtime --dir <workdir> --format json
166
+ ```
167
+
168
+ 命令返回默认 Runtime、完整镜像目录和工作区各 Python 节点使用的 `runtimeMode`、`pythonVersion`、
169
+ `pythonVersionSource`、`imageId` 及解析状态,并写入 `<workdir>/_python_runtime_context.json`。`guanwf edit` 检测到
170
+ Python 节点时也会自动获取并返回同一结构化上下文。生成代码时只以其中的
171
+ `pythonRuntime.nodes[].runtime.pythonVersion`(或 `pythonRuntime.selected`)为兼容目标;
172
+ 当前接口没有正式版本字段时使用产品兼容契约 Python 3.8,并明确标记
173
+ `pythonVersionSource: "compatibility_fallback"`;**禁止从镜像描述、名称或 URL 推断版本**。
174
+ 结构化上下文中的 `imageUrl` 会移除 userinfo、fragment 和敏感 query;落盘文件使用 0600 并自动忽略。
175
+ 固定 Runtime 输出 `runtimeMode: "FIXED_RUNTIME"`,配置镜像的动态 Pod 输出 `runtimeMode: "DYNAMIC_POD"`;
176
+ 目录接口不可用或镜像/版本字段缺失时仍保守生成 Python 3.8 兼容代码,同时保留 warning 和 fallback 来源。
177
+ Python 3.8 fallback 下禁止直接生成 `zoneinfo`、`dict[str, ...]` / `list[...]`、`X | None` 等
178
+ 3.9/3.10+ 写法;分别使用经验证可用的时区依赖、`typing.Dict` / `typing.List`、`typing.Optional`。
179
+ 不得用固定 `/tmp/...` 路径在 Python 节点之间交换中间结果:不同节点/并发实例可能运行在不同
180
+ UID、进程或 Pod。跨节点数据必须通过 `save_outputN` + 下游 `load_inputN` 的正式数据集/DAG 传递;
181
+ 单节点临时文件使用 `tempfile.TemporaryDirectory()` 并在上下文退出时自动清理。
151
182
 
152
183
  ### 2. 真正改文件前先读这些
153
184
 
@@ -182,6 +213,7 @@ guancli workflow get <id> -f json # JSON 格式输出
182
213
  ```bash
183
214
  guanwf create --name "我的工作流" --parent-dir <dirId> --dir <workdir> # 数据流节点骨架(默认)
184
215
  guanwf create --name "Python清洗" --type PYTHON --parent-dir <dirId> --dir <workdir> # Python 节点骨架
216
+ guanwf python-runtime --dir <workdir> --format json # 写 Python 前读取目标环境 Runtime
185
217
 
186
218
  # 编辑 workflow.go(节点 + 依赖)和 nodes/ 下源文件后:
187
219
  guanwf export --dir <workdir>
@@ -196,7 +228,7 @@ guanwf save --dir <workdir> --confirm # 正式发
196
228
 
197
229
  ```bash
198
230
  guanwf edit <workflowId> --dir <workdir>
199
- # 生成 workflow.go + nodes/<节点ID>/(数据流→etl.go,Python→script.py+python.json,其他→task.json)
231
+ # 生成 workflow.go + nodes/<节点ID>/;含 Python 节点时同时生成 _python_runtime_context.json
200
232
 
201
233
  # 修改源文件后与新建流程相同:export → preview/validate → save-draft/save
202
234
  ```
@@ -219,6 +251,7 @@ guanwf run --dir <workdir> --confirm # 触发运行(已发布版
219
251
  guanwf run --wait --dir <workdir> --dry-run # 查看执行计划
220
252
  guanwf run --wait --dir <workdir> --confirm # 确认并等待完成
221
253
  guanwf run --wait --timeout 600 --dir <workdir> --confirm # 自定义超时
254
+ guanwf run --wait --output-timeout 120 --dir <workdir> --confirm # 输出注册/更新独立超时
222
255
  guanwf run --wait --logs --dir <workdir> --confirm # 完成后输出任务日志
223
256
  guanwf run --node "<节点名>" --validate --dir <workdir> --confirm # 校验 Python 脚本(不创建输出数据集)
224
257
  guanwf run --node "<节点名>" --draft --wait --logs --dir <workdir> --confirm # 单节点真实运行草稿
@@ -229,12 +262,20 @@ guanwf instance resume-failed <instanceId> --dom-id <domId> --wait --timeout 900
229
262
  `--node` 按节点显示名只运行指定节点(TASK_ONLY);`--draft` 运行 save-draft 的草稿版本;
230
263
  `--logs` 输出任务实例日志(失败时自动拉取,失败的子工作流节点自动下钻子实例日志);
231
264
  `--recover` 从失败处续跑(见「失败恢复」一节)。
265
+ 普通 `run` 会在提交前执行 Python 输入规模/内存预检并返回 `PASS/NO_GO/UNKNOWN`;`NO_GO`
266
+ 默认阻断,`UNKNOWN` 只有在阅读 dry-run 缺失证据后显式追加 `--allow-unknown-memory` 才继续。
267
+ 预检只读取数据集元数据,不触发 preview 或全量物化。TASK_ONLY + CREATE_NEW 依据版本化能力矩阵
268
+ fail closed;用 `guanwf output capabilities` 查询当前矩阵。
269
+ 启动实例前会从同一份最终执行快照重新解析 `--param`、输出后置条件与 Python 内存门禁;
270
+ 确认期间发生参数、Python 节点或输出槽位漂移时会在 `start-process-instance` 前失败关闭。
232
271
  `run --recover` 和 `instance resume-failed` 的最外层实例只有传 `--wait` 才等待完成;嵌套
233
272
  PROCESS 子实例始终等待成功后才恢复父实例。长任务用 `--timeout` 调整等待上限。
234
273
 
235
- **`--validate` 是验证 Python 脚本的默认方法**:临时草稿中把 `save_outputN` 替换为只打印
236
- dtypes/head 的 stub,脚本完整执行但不写输出文件,服务端跳过数据集注册(不创建真实数据集),
237
- 结束后自动恢复原始草稿。日志中可看到输出 schema 预览。详见 `references/WORKFLOW_DSL.md`。
274
+ **`--validate` 是验证 Python 脚本的默认方法**:先对最终完整源码执行 WORKFLOW_PARAMS
275
+ 唯一入口、类型、引号上下文和残留检查(dry-run 只显示类型/来源,不显示值),再在临时草稿中
276
+ `save_outputN` 替换为 serializer stub。stub 会在任务唯一临时目录中用真实 PyArrow/Parquet
277
+ 完成 `Table.from_pandas` 和 `write_table`,随后清理目录且不注册数据集;结束后自动恢复原始草稿。
278
+ 日志中可看到 dtype、head、Arrow schema 和 `serializer=SUCCESS`。详见 `references/WORKFLOW_DSL.md`。
238
279
 
239
280
  ### 预览数据流节点
240
281
 
@@ -346,23 +387,62 @@ NEED_FAULT_TOLERANCE。适用于长链路编排中
346
387
  1. 读取本地 `_exported_workflow.json`(必须由 `export` 重新生成)
347
388
  2. 从服务端重新拉取工作流最新版本作为基底
348
389
  3. tasks 按节点 id 做字段级合并:本地定义的字段覆盖,服务端独有字段(dsId、运行时注册信息等)保留
390
+ ;Python 输出顶层 `dsId` 与 `dataSource.dsId` 同时存在时必须一致,冲突会在写请求前失败
391
+ ;本地显式绑定已有 dsId 的 Python 输出会 fresh 查询数据集并补齐 `parentDirId`、`parentDirName`、
392
+ `dirPath`,同时校验物理对象存在性、`DATA_SET_OFFLINE_DEV/MASTER_FLOW` 类型、producer
393
+ lineage 与输出槽身份;不存在、无权限、类型/血缘/目录/槽位冲突均在写请求前按维度失败。
394
+ 仅当 `_wf_state.json` 已记录同一 task/slot 的可信 placeholder lineage,或该 slot 是本次保存
395
+ 明确新增的输出时,服务端回填、`created=false` 且物理对象尚不存在的临时 dsId 才继续按
396
+ “待首次运行物化”处理。fresh `edit` 无法仅凭 404 区分 placeholder 与已删除的显式外部绑定,
397
+ 因而按显式绑定 fail closed;用户改绑后的新 ID 也不会继承旧 pending 来源
349
398
  4. 节点成员按三方判断(服务端最新 + 本地 `_parent_merge_snapshot.json` + 本地导出):
350
399
  本地快照里有、导出里删了的节点 → 删除;服务端有、本地快照不知道的节点(他人并发新增)→
351
400
  保留并打印提示
352
401
  5. dataflowJson 按 dataflow id 经 etlmerge 做 actions 字段级合并(成员判断同上)
353
- 6. 调用 `/process/save-draft` `/process/save`,成功后把**本次提交的定义**刷新为本地快照
402
+ 6. dry-run 固定输出 `baseVersion/currentVersion/changedFields/noOp/conflicts`;可用
403
+ `--require-change` 阻断静默 no-op,用 `--expected-base-version` 做 optimistic CAS。调用
404
+ `/process/save-draft` 或 `/process/save` 后必须 fresh readback 所有目标字段,否则命令非零退出;
405
+ 随后把**本次提交的定义**刷新为本地快照
354
406
  并同步进 `python.json`(不是服务端回写值——服务端在保存/运行后对输出 dataSource/dsId
355
407
  等字段的回写需要重新 `guanwf edit` 才能拿到),随后回读服务端做输出落位诊断:
356
408
  目录被纠正时提示 `save.output_dir_rectified`,输出尚未物化时提示
357
409
  `save.output_pending_materialization`(此时 dsId 是临时值,首次成功运行前每次保存都会
358
410
  重新生成,勿用于下游引用)。`run` 等待成功后会校验所有输出的数据集实体线上存在
359
411
  (`created=true` 直接通过;`created=false` 会查询数据集本身——绑定其他流程创建的
360
- 已有数据集时 DSL 要求保持 `Created=false`,该状态合法),数据集不存在的输出会直接
361
- 报错(后端存在"脚本未产出输出时静默跳过注册但任务 SUCCESS"的路径)。
412
+ 已有数据集时 DSL 要求保持 `Created=false`,该状态合法)。`run --wait` 会冻结每个 slot 的
413
+ CREATE_NEW/UPDATE_EXISTING 后置条件,逐项返回 compute/file/discovery/registerOrUpdate/readback;
414
+ CREATE_NEW 要求唯一 dsId、lineage 和可读实体,UPDATE_EXISTING 还要求版本/commit token/摘要
415
+ 发生变化。确认后、启动 POST 前的最后执行快照会重新生成输出和内存契约;并发新增/删除
416
+ task、slot 或资源漂移不会绕过 preflight。任一失败时 `businessResult != SUCCESS` 且命令非零退出。
362
417
 
363
418
  因此用户和 AI 都不需要手写服务端保存 payload。多人/多端并发修改同一工作流时,以服务端
364
419
  最新版本为基底合并,不会把别人新加的节点冲掉;但同一节点的并发修改仍是后保存者覆盖。
365
420
 
421
+ 只修改一个 Python 输出绑定时,不要编辑完整 DSL;先确保当前版本已有草稿,再使用精确命令:
422
+
423
+ ```bash
424
+ guanwf output bind <workflowId> --task-id <taskId> --slot-id <outputId> --dataset-id <dsId> --dry-run
425
+ guanwf output bind <workflowId> --task-id <taskId> --slot-id <outputId> --dataset-id <dsId> --confirm
426
+ guanwf output rebind <workflowId> --task-id <taskId> --slot-id <outputId> --expected-old <oldDsId> --dataset-id <newDsId> --confirm
427
+ guanwf output unbind <workflowId> --task-id <taskId> --slot-id <outputId> --expected-old <oldDsId> --confirm
428
+ guanwf output bind-registered-outputs <instanceId> --workflow-id <workflowId> --dom-id <domId> --dry-run
429
+ guanwf output bind-registered-outputs <instanceId> --workflow-id <workflowId> --dom-id <domId> --confirm
430
+ guanwf output capabilities -f json
431
+ guanwf output bootstrap-plan <workflowId> --task-id <taskId> --slot-id <outputId> --update-mode APPEND --primary-key id -f json
432
+ ```
433
+
434
+ 命令以 task id + output slot id 精确定位,只保存草稿;确认前再次 fresh read 并执行 optimistic
435
+ CAS,`expected-old` 不一致或在确认窗口中已检测到定义变化时不会发起写入。后端当前没有暴露原子
436
+ 条件写接口,最后一次 fresh read 到整份 `save-draft` POST 之间仍遵循上文的同节点后写覆盖语义;
437
+ 并发编辑同一工作流时应串行执行精确绑定命令。`unbind` 只清除定义中的绑定,不删除物理数据集。
438
+ `bind-registered-outputs` 只接受实例中带 `taskId + slotId + dsId` 的精确映射,拒绝按 `dsIds`
439
+ 数组顺序猜测;`processInstanceJson` 只用于识别 task/slot 定义形状,每个回写还必须有当前实例运行结果中同
440
+ task/slot/dsId 的成功状态或与 fresh 数据集精确一致的 commit token,但显式 `FAILURE` 终态永远优先于 token。
441
+ 所有 slot 先校验后用一份
442
+ save-draft 提交,缺失/重复/冲突时零写入,重复执行幂等。
443
+ 未绑定输出不能一步直接 APPEND/PK;先按 bootstrap plan 以 OVERWRITE 创建,fresh 取得并绑定唯一
444
+ dsId,再用 expected-old 切换增量模式和主键。
445
+
366
446
  若他人并发新增的节点依赖了本地这次删除/改名的节点,保存会检测到断链冲突并中止,
367
447
  此时按提示重新 `guanwf edit <工作流ID>` 同步最新版本后再修改。
368
448
 
@@ -7,11 +7,15 @@
7
7
  这是 guanwf 唯一的工作目录格式:单数据流工作流就是只含一个 `DataflowNode` 的 workflow.go,
8
8
  多节点工作流(多数据流、数据流 + Python、参数赋值等混合 DAG)按同样方式扩展节点列表。
9
9
 
10
+ 服务端顶层 `dataflowJson` 可能残留已无 task 引用的 raw sidecar。可执行性只由 `workflow.go` 对应的
11
+ SUB_PROCESS tasks 决定;`guanwf get/edit/status` 会分别输出 `rawSidecars`、
12
+ `reachableDataflows`、`orphanSidecars`。孤儿默认保留且不参与拓扑,不能用 raw sidecar 数量判断节点仍存在。
13
+
10
14
  ## 工作目录结构
11
15
 
12
16
  ```
13
17
  <workdir>/
14
- workflow.go # 工作流结构事实源(节点 + 依赖)
18
+ workflow.go # 工作流结构事实源;含凭据时 export 自动收紧权限并加入 .gitignore
15
19
  nodes/
16
20
  <节点ID>/
17
21
  etl.go # DATAFLOW/DB_DATAFLOW 节点:guanetl DSL(可附 *.sql、node_*.json)
@@ -26,6 +30,7 @@
26
30
  _parent_merge_snapshot.json # 0600/当前用户 ACL 的精确合并基线,不手改
27
31
  .gitignore # 自动忽略含原始凭据的精确合并基线
28
32
  _layout.json # 节点画布坐标(edit 时保留线上坐标),可不存在
33
+ _python_runtime_context.json # Python Runtime 结构化上下文(命令生成,只读)
29
34
  _exported_workflow.json # export 派生产物,不手改
30
35
  ```
31
36
 
@@ -101,6 +106,8 @@ return Workflow{
101
106
  `ParamRef("biz_date")`、`DynamicParamRef("system.biz")`、
102
107
  `DataDrivenParamRef("result")` 和 `BuiltinParamRef("looptimes")` 只生成平台引用表达式,不在本地求值。
103
108
  平台持久化类型为 STRING/NUMBER/DATE;BoolParam 兼容 STRING,TimeParam 兼容 DATE。
109
+ 需要保留超大整数或高精度十进制文本时使用 `NumberParam("amount", ExactNumber("1.2300000000000000001"))`;
110
+ `edit` 也会为服务端返回的精确 JSON 数值自动生成 `ExactNumber(...)`,避免经过 `int`/`float64` 丢失精度。
104
111
 
105
112
  完整 TaskNode 字段见 `TASK_NODE_CONTRACTS.md`。
106
113
 
@@ -135,6 +142,32 @@ DataflowConfig{
135
142
 
136
143
  ## Python 节点
137
144
 
145
+ ### 先解析目标 Runtime
146
+
147
+ 编写 `script.py` 前先读取目标环境的结构化 Runtime 上下文:
148
+
149
+ ```bash
150
+ guanwf python-runtime --dir <workdir> --format json
151
+ ```
152
+
153
+ `pythonRuntime.default` 是环境默认镜像,`pythonRuntime.catalog` 列出可见镜像,`pythonRuntime.selected` 是命令指定或默认选择,
154
+ `pythonRuntime.nodes[]` 是工作区各 Python 节点根据 `PythonConfig.ImageID` 解析出的实际选择。
155
+ 兼容性判断以 `runtimeMode` 和嵌套 `runtime.pythonVersion` 为准,并检查 `runtime.pythonVersionSource`。CLI 不输出
156
+ 服务端自由文本 `imageDescription`;经过凭证脱敏的 `imageUrl` 仅供排查,禁止从中推断版本。
157
+ CLI 会优先调用新版系统镜像 API,并自动兼容旧环境的 Python 镜像 API。
158
+
159
+ 短期兼容约定:镜像 API 尚未提供正式 `pythonVersion` 字段时,CLI 返回 Python 3.8,并标记
160
+ `pythonVersionSource: "compatibility_fallback"`;若未来 API 返回正式字段,则优先使用并标记为 `api_field`。
161
+
162
+ 固定 Runtime 标记 `runtimeMode: "FIXED_RUNTIME"`,配置镜像的动态 Pod 标记
163
+ `runtimeMode: "DYNAMIC_POD"`。目录接口不可用或版本字段缺失时,状态与 warning 会明确说明,
164
+ 但兼容目标仍保守落到 Python 3.8;这不是探测结果,不得描述为“平台返回 3.8”。
165
+ 以 3.8 为兼容目标时,不要使用标准库 `zoneinfo`、内置泛型 `dict[str, ...]` / `list[...]` 或
166
+ `X | None` 联合类型;使用目标环境已验证的时区方案以及 `typing.Dict` / `typing.List` / `typing.Optional`。
167
+
168
+ `guanwf edit` 遇到 Python 节点时会自动返回这段 JSON,并写入工作目录的
169
+ `_python_runtime_context.json`。也可用 `--image-id <id>` 单独解析某个镜像。
170
+
138
171
  ### PythonConfig
139
172
 
140
173
  ```go
@@ -142,8 +175,10 @@ PythonConfig{
142
175
  InputDatasets: []string{"dsId1", "dsId2"}, // 输入数据集 ID,顺序对应 input1, input2, ...
143
176
  Outputs: []PythonOutput{ // 输出数据集,顺序对应 output1, output2, ...
144
177
  {
178
+ SlotID: "", // edit 自动写入稳定 slot id;仅新建 slot 时留空
145
179
  Name: "输出数据集名", // 必填,目录内唯一
146
180
  DatasetID: "", // 新建时留空;更新已有数据集时填写其 dsId
181
+ ClearDatasetBinding: false, // 显式解除 edit 回读的已有 dsId 绑定
147
182
  Created: false, // true 仅表示数据集由当前工作流创建
148
183
  ParentDirID: "<数据集目录ID>", // 必填,guancli dir tree 可查
149
184
  UpdateMode: "OVERWRITE", // OVERWRITE(覆盖)/ APPEND(追加),默认 OVERWRITE
@@ -160,7 +195,20 @@ PythonConfig{
160
195
  新建输出数据集时不要填写 `DatasetID`:首次运行由 BI 服务端在 `ParentDirID` 下创建并注册。
161
196
  只有明确更新已有数据集时才填写该数据集的 dsId。更新其他流程创建的数据集时保持
162
197
  `Created: false`;仅当数据集确由当前工作流创建时使用 `Created: true`。`guanwf edit` 会把
163
- 服务端已有 dsId 和 created 状态回读,后续导出会继续保留。
198
+ 服务端已有 slot id、dsId 和 created 状态回读,后续导出只按 `SlotID` 继承绑定;显示名不会
199
+ 作为身份 fallback。手写新输出时可将直接字面量中的 `SlotID` 留空:首次 `guanwf export`
200
+ 会分配稳定 ID 并原子写回 `workflow.go`。如果输出由变量、循环或辅助函数动态构造,CLI 无法
201
+ 无歧义改写事实源,必须显式填写唯一 `SlotID`,否则 export 会 fail closed。
202
+ 因此,在 edit 工作区中仅把 `DatasetID` 改成空字符串不会删除已有绑定;需要让该输出重新由
203
+ 服务端创建数据集时,设置 `ClearDatasetBinding: true`,并保持 `DatasetID` 为空、`Created: false`。
204
+ 同一输出在服务端 JSON 中可能同时出现顶层 `dsId` 与 `dataSource.dsId`;两者非空时必须
205
+ 完全一致,guanwf 会在 edit/export/save 阶段拒绝冲突身份,禁止猜测应更新哪一个数据集。
206
+ save 与 run 还会 fresh 查询每个非空 dsId:对象必须存在且有权限、类型必须是工作流输出数据集、
207
+ 目录和输出槽身份必须一致;`Created: true` 时 producer lineage 必须包含当前工作流。
208
+ 同一 Python 节点内的输出 `Name` 必须唯一,以避免服务端目录名称与诊断结果歧义;slot 身份只认 `SlotID`。不同 Python 输出 slot 的非空 dsId
209
+ 必须全工作流唯一,并在保存或启动实例前检查。fresh `edit`
210
+ 遇到 `created=false` 且对象 404 时不会猜测它是首次运行 placeholder;只有现有 workspace lineage
211
+ 或本次明确新增 slot 能证明 placeholder 来源,否则按失效显式绑定 fail closed。
164
212
 
165
213
  ### script.py 沙箱约定
166
214
 
@@ -184,6 +232,8 @@ print("rows:", len(result)) # print 输出会出现在任务日志里,可用
184
232
  - 输出数据集的字段 schema 由首次运行时 `save_outputN` 写出的 DataFrame 推断,不需要预先声明。
185
233
  - 每个客户环境的 Python 版本和库可能不同,**生成脚本后先用单节点试运行验证**(见下节),
186
234
  不要假设任意第三方库可用;默认镜像保证 pandas 可用。
235
+ - 禁止用硬编码 `/tmp/...` 在节点之间交换文件。跨节点必须走输出/输入数据集;节点内部需要临时
236
+ 文件时用 `tempfile.TemporaryDirectory()`,确保任务唯一并自动清理。export 会静态拒绝固定 `/tmp` 字面量。
187
237
 
188
238
  ### python.json 透传
189
239
 
@@ -191,6 +241,13 @@ print("rows:", len(result)) # print 输出会出现在任务日志里,可用
191
241
  除脚本正文之外的完整参数(含服务端生成的 dsId、fieldMappings、k8sConfig 等)。导出时以它为
192
242
  基底,`PythonConfig` 中显式设置的字段覆盖其上,脚本正文始终来自 `script.py`。
193
243
 
244
+ 这个透传基底还负责保留输出槽位 id、目录名称/路径及当前 DSL 尚未结构化的服务端字段。因此,
245
+ 不修改 `workflow.go` 与 `script.py` 的 edit → export 往返会保持 Python 输出绑定语义不变。
246
+ 删除输出 slot 时 CLI 同步删除对应 pending/explicit provenance,后续复用相同 slot id 不会继承旧占位符身份。
247
+ 实例定义快照中的 output dsId 仅是身份形状,不是本次运行已完成注册的证据;精确回写和 `run --wait`
248
+ 只会在当前实例运行结果给出同 task/slot/dsId 的成功状态或匹配 fresh 实体的 commit token 时提升绑定来源;
249
+ 运行结果已给出显式失败终态时,即使 token 匹配也必须失败。
250
+
194
251
  不要手改 `python.json`。要改输入/输出/资源配置,改 `workflow.go` 的 `PythonConfig`。
195
252
 
196
253
  ## Python 脚本环境校验(run --validate)
@@ -206,12 +263,13 @@ guanwf run --dir <workdir> --node "<节点名>" --validate --confirm # 3. 校
206
263
 
207
264
  `--validate` 的工作机制:
208
265
 
209
- 1. 在临时草稿中给所有 Python 节点脚本头部注入 stub,把 `save_outputN` 替换为只
210
- `print(df.dtypes)` + `print(df.head(20))` 的实现(BI 服务端把真实 `save_outputN`
266
+ 1. 在临时草稿中给目标 Python 节点脚本头部注入 stub,把 `save_outputN` 替换为
267
+ `print(df.dtypes)` + `print(df.head(20))` 并执行真实 PyArrow/Parquet 序列化的实现
268
+ (BI 服务端把真实 `save_outputN`
211
269
  定义在用户脚本之前,用户脚本中的重定义会覆盖它)
212
270
  2. 单节点试运行草稿(TASK_ONLY + DRAFT),等待完成并拉取日志
213
- 3. stub 不写输出文件,服务端发现无 parquet 会跳过输出数据集注册——**不创建、不导入任何
214
- 真实数据集**
271
+ 3. stub 在任务唯一临时目录写入 `data.parquet` 验证真实 serializer,随后立即清理;它不写
272
+ 服务端约定的输出位置,因此服务端跳过输出数据集注册——**不创建、不导入任何真实数据集**
215
273
  4. 运行结束后自动把原始草稿(无 stub)重新保存回服务端
216
274
 
217
275
  校验日志中能看到(`--- 脚本输出 (DETAIL) ---` 段):
@@ -220,6 +278,7 @@ guanwf run --dir <workdir> --node "<节点名>" --validate --confirm # 3. 校
220
278
  - `[VALIDATE] save_outputN called: rows=..., cols=...`
221
279
  - `[VALIDATE] outputN dtypes:` 输出 DataFrame 的字段类型(即未来数据集的 schema 预览)
222
280
  - `[VALIDATE] outputN head(20):` 样例数据
281
+ - `[VALIDATE] outputN serializer=SUCCESS schema=...` 真实 Parquet serializer 校验成功及 schema
223
282
 
224
283
  AI 应根据 dtypes/head 检查输出 schema 是否符合预期,确认后用 `save` 发布并正式
225
284
  `run`(正式运行才会真实创建输出数据集)。
@@ -425,6 +484,8 @@ guanwf run --dir <workdir> --node "<节点名>" --draft --wait --logs --confirm
425
484
 
426
485
  `run` 会创建线上执行实例,升级后必须显式传 `--confirm`(兼容别名 `--yes`);
427
486
  自动化脚本可先执行 `--dry-run` 检查目标、参数和请求计划。
487
+ 正式启动实例前会从同一份最终执行快照重新解析参数覆盖、输出后置条件与 Python 内存门禁;
488
+ 确认期间发生参数、Python 节点或输出槽位漂移时会在 `start-process-instance` 前失败关闭。
428
489
 
429
490
  ### 预览数据流节点
430
491