@ai-setting/roy-agent-core 1.7.53 → 1.7.55
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/dist/env/agent/index.js +3 -3
- package/dist/env/error-reply.d.ts +184 -0
- package/dist/env/event-source/event-source-handlers.d.ts +9 -23
- package/dist/env/event-source/index.js +6 -6
- package/dist/env/index.js +18 -18
- package/dist/env/llm/adapter-pi-ai.d.ts +147 -2
- package/dist/env/llm/adapter.d.ts +21 -1
- package/dist/env/llm/index.js +2 -2
- package/dist/env/llm/invoke.d.ts +41 -1
- package/dist/env/llm/llm.d.ts +25 -0
- package/dist/env/llm/reasoning-defense.d.ts +104 -0
- package/dist/env/llm/types.d.ts +26 -0
- package/dist/env/prompt/index.js +2 -2
- package/dist/env/prompt/prompts-index.d.ts +1 -1
- package/dist/env/session/index.js +4 -4
- package/dist/env/session/storage/index.js +3 -3
- package/dist/env/task/backend/index.js +0 -2
- package/dist/env/task/backend/types.d.ts +0 -11
- package/dist/env/task/delegate/index.js +2 -2
- package/dist/env/task/index.js +3 -3
- package/dist/env/task/plugins/card-chat-routing.d.ts +42 -89
- package/dist/env/task/plugins/index.js +4 -4
- package/dist/env/task/plugins/lark-card-patch-notifier.d.ts +0 -51
- package/dist/env/task/plugins/lark-cli-task-notify-plugin.d.ts +14 -31
- package/dist/env/task/plugins/per-task-group-chat-plugin.d.ts +4 -4
- package/dist/env/workflow/engine/engine.d.ts +65 -0
- package/dist/env/workflow/engine/index.js +2 -2
- package/dist/env/workflow/index.js +9 -9
- package/dist/env/workflow/nodes/index.js +1 -1
- package/dist/env/workflow/nodes/tool-node.d.ts +6 -0
- package/dist/env/workflow/plugins/index.js +2 -2
- package/dist/env/workflow/tools/index.js +6 -6
- package/dist/env/workflow/tools/workflow-validate-tool.d.ts +11 -2
- package/dist/env/workflow/types/run.d.ts +19 -0
- package/dist/env/workflow/utils/agent-session-metadata.d.ts +13 -0
- package/dist/env/workflow/utils/index.js +1 -1
- package/dist/env/workflow/utils/redact-command-secrets.d.ts +70 -0
- package/dist/env/workflow/utils/shell-dialect-lint.d.ts +75 -0
- package/dist/env/workflow/utils/validate-workflow-definition.d.ts +22 -0
- package/dist/index.js +20 -20
- package/dist/shared/@ai-setting/{roy-agent-core-stnddvx9.js → roy-agent-core-12d2xap8.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-pk1b3kvf.js → roy-agent-core-2b8jtv7m.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-x8n1r7h4.js → roy-agent-core-2bscfrr1.js} +188 -7
- package/dist/shared/@ai-setting/{roy-agent-core-gdsqp6j8.js → roy-agent-core-6masakz4.js} +45 -0
- package/dist/shared/@ai-setting/{roy-agent-core-vvxb9522.js → roy-agent-core-6qxh4xrm.js} +67 -6
- package/dist/shared/@ai-setting/{roy-agent-core-h93v6p6y.js → roy-agent-core-7qy3nffr.js} +2 -2
- package/dist/shared/@ai-setting/{roy-agent-core-4vpk40ss.js → roy-agent-core-8eqngasy.js} +2 -2
- package/dist/shared/@ai-setting/{roy-agent-core-brwzs8wf.js → roy-agent-core-9h72sk1n.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-na73mym1.js → roy-agent-core-a4vzsn0g.js} +212 -1
- package/dist/shared/@ai-setting/{roy-agent-core-35s1w7tc.js → roy-agent-core-aer5kw1j.js} +142 -70
- package/dist/shared/@ai-setting/{roy-agent-core-j8e92gj8.js → roy-agent-core-b5ew8t0k.js} +0 -59
- package/dist/shared/@ai-setting/{roy-agent-core-4sx6sdb2.js → roy-agent-core-cd62e9p1.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-39xejr39.js → roy-agent-core-e2z1etk4.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-hch2geta.js → roy-agent-core-g36jevrk.js} +14 -183
- package/dist/shared/@ai-setting/{roy-agent-core-ph5qbqcx.js → roy-agent-core-j2nvedt6.js} +465 -128
- package/dist/shared/@ai-setting/{roy-agent-core-p2r27era.js → roy-agent-core-mxdc756a.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-p8ak03s0.js → roy-agent-core-w527pcpe.js} +293 -42
- package/dist/shared/@ai-setting/{roy-agent-core-3tv1q6f2.js → roy-agent-core-ytr2w0vb.js} +2 -2
- package/dist/shared/@ai-setting/{roy-agent-core-yd21n6c7.js → roy-agent-core-zvemdfx5.js} +1 -1
- package/dist/test-utils/child-process-mock.d.ts +73 -0
- package/package.json +2 -2
- /package/dist/shared/@ai-setting/{roy-agent-core-zy3hqfwg.js → roy-agent-core-0hq21px2.js} +0 -0
|
@@ -1548,6 +1548,28 @@ workflow-creator 是**多轮对话式**协作。每个阶段都可能需要 ask
|
|
|
1548
1548
|
- ❌ **禁止**对 \`tool\` 节点写深层路径(如 \`{{nodes.bash-node.output.foo}}\`)—— tool 输出常是字符串,无法再取属性
|
|
1549
1549
|
- ✅ 存疑时一律退回 \`.output\`,或用 \`{{nodes.X.output}}\` 打印后**人肉核对**
|
|
1550
1550
|
|
|
1551
|
+
### 阶段 4.6: 单节点冒烟前置 (Per-node Standalone Smoke) \uD83E\uDDEA
|
|
1552
|
+
|
|
1553
|
+
> **依据 Task #4385 §3.4**:历史失败中,大量节点是**在整条 workflow 里第一次被执行**的 ——
|
|
1554
|
+
> 脚本名拼错、产物目录不存在、命令依赖 \`cd\`、参数没传进去…… 一个坏节点就烧掉整轮 run,
|
|
1555
|
+
> 然后你在整条链路的噪音里排障。**先把每个节点单独跑通,再拼成 DAG。**
|
|
1556
|
+
|
|
1557
|
+
**在把 YAML 组装/注册之前,对每一个 \`tool\` / \`pythonCall\` / \`skill\` 节点逐个做单节点冒烟:**
|
|
1558
|
+
|
|
1559
|
+
1. 用 \`bash\` 把该节点的 \`config.command\`(或 \`config.script\` / \`args\`)**原样**执行一次。
|
|
1560
|
+
- 变量先替换成真实值再跑(不要留着 \`{{input.x}}\` 跑,那验不出东西)
|
|
1561
|
+
- 逐个节点跑,**不是**跑整条链路
|
|
1562
|
+
2. 三项全绿才算该节点通过:
|
|
1563
|
+
- ✅ **exit code = 0**(非 0 立刻停下修,不要往后拼)
|
|
1564
|
+
- ✅ **产物/目录真实存在**(脚本声称会写出的文件、目录、wheel、日志,用 \`ls\` 核实)
|
|
1565
|
+
- ✅ **输出非空且含预期标记**(\`grep\` 你后面要引用的那个标记,命中才算通)
|
|
1566
|
+
3. 任何一项红了 → **先修这个节点**(改路径 / 改命令 / 改脚本),修好重跑,**不要**带着红灯继续组装。
|
|
1567
|
+
|
|
1568
|
+
**为什么不能省**:\`workflow_validate\` 只校验 DAG 结构 + 模板引用契约,它**看不到**
|
|
1569
|
+
"这个 shell 命令在这个 cwd 下到底能不能跑通"。单节点冒烟是唯一廉价、快速、可重复的补位。
|
|
1570
|
+
|
|
1571
|
+
**通过后**记录一张自检表(节点 id → exit code / 产物 / 标记),写进最终报告。
|
|
1572
|
+
|
|
1551
1573
|
### 阶段 5: 迭代验证 (Iterative Validate) \uD83D\uDD01
|
|
1552
1574
|
|
|
1553
1575
|
调用 \`workflow_validate\`,它返回 \`{ valid, errors, warnings, infos, errorCount, warningCount }\`。
|
|
@@ -1601,6 +1623,29 @@ workflow-creator 是**多轮对话式**协作。每个阶段都可能需要 ask
|
|
|
1601
1623
|
**未通过冒烟 → 禁止 \`task_complete\`**:回到阶段 4 改 YAML,重跑,直到通过;或明确报告失败原因
|
|
1602
1624
|
(**不得**用"应该没问题"之类的措辞声称通过)。
|
|
1603
1625
|
|
|
1626
|
+
#### \uD83D\uDEAB 手动补跑 ≠ 真跑(依据 Task #4385 §3.5)
|
|
1627
|
+
|
|
1628
|
+
workflow run 失败后,**手动**把某个节点的命令再敲一遍、看到它成功了,**不算**该节点被验证。
|
|
1629
|
+
|
|
1630
|
+
- ❌ 手动补跑的结果**不得计入** PASS 计数,也**不得**作为 "节点没问题" 的依据
|
|
1631
|
+
- ❌ 不得因为"手动跑通了"就跳过修 YAML / 重跑 workflow
|
|
1632
|
+
- ✅ 手动作业只能用来**定位原因**;结论必须靠**重跑 workflow** 得出
|
|
1633
|
+
- ✅ 若最终确实没能在 workflow 内验证某节点,结论里必须显式标注
|
|
1634
|
+
**「⚠️ 该节点未经 workflow run 验证」**,与真正通过的节点分开列
|
|
1635
|
+
|
|
1636
|
+
> 为什么:手动跑不经过模板解析、依赖排序、引擎超时与 fail-fast。
|
|
1637
|
+
> 一条在 shell 里通的命令,在节点里完全可能因为 \`{{}}\` 解析成空串而"成功但空转"。
|
|
1638
|
+
|
|
1639
|
+
#### \uD83D\uDD0D 归因:先分清「超时」与「别的节点先 fail-fast」(依据 Task #4385 §5)
|
|
1640
|
+
|
|
1641
|
+
说"某个节点超时了"之前,**必须先证明没有其他节点先失败**。
|
|
1642
|
+
|
|
1643
|
+
- 引擎是 **fail-fast** 的:任一节点失败会立刻收尾整个 run,**尚未跑完的节点看起来就像"卡住/超时"**
|
|
1644
|
+
- 因此**先排除**:\`roy-agent debug trace <trace_id>\` 里是否已有**其它节点**的失败 span / error
|
|
1645
|
+
- 只有在确认 "无其它节点先失败" 之后,才可以判断为真超时,再去看该节点自己的 span 耗时与 stdout
|
|
1646
|
+
- ❌ 不要把 fail-fast 的连带中止当成超时去"修超时参数" —— 这会让你在错误的节点上白改几轮
|
|
1647
|
+
(历史教训:#4361 / #4366 连续两轮误判)
|
|
1648
|
+
|
|
1604
1649
|
> \uD83D\uDEAB **绝不允许用 \`workflow update\` 绕过冒烟验证**(历史上真实节点版本 v1.1.0/v1.2.0 就是走
|
|
1605
1650
|
> \`workflow update\` 注册的,完全绕过了 validate 与冒烟)。update 后**同样必须**重跑阶段 5 + 5.5。
|
|
1606
1651
|
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
isValidSessionId
|
|
3
|
-
} from "./roy-agent-core-
|
|
3
|
+
} from "./roy-agent-core-zvemdfx5.js";
|
|
4
4
|
import {
|
|
5
5
|
TAG_MAX_COUNT,
|
|
6
6
|
TAG_MIN_COUNT
|
|
7
7
|
} from "./roy-agent-core-7evvpfwc.js";
|
|
8
8
|
import {
|
|
9
9
|
validateWorkflowDefinition
|
|
10
|
-
} from "./roy-agent-core-
|
|
10
|
+
} from "./roy-agent-core-a4vzsn0g.js";
|
|
11
11
|
import {
|
|
12
12
|
getCurrentBgTaskId
|
|
13
13
|
} from "./roy-agent-core-c1v263jn.js";
|
|
@@ -24,7 +24,8 @@ import {
|
|
|
24
24
|
init_logger
|
|
25
25
|
} from "./roy-agent-core-8seq65bp.js";
|
|
26
26
|
import {
|
|
27
|
-
__legacyDecorateClassTS
|
|
27
|
+
__legacyDecorateClassTS,
|
|
28
|
+
__require
|
|
28
29
|
} from "./roy-agent-core-fs0mn2jk.js";
|
|
29
30
|
|
|
30
31
|
// src/env/workflow/tools/ask-user-tool.ts
|
|
@@ -480,8 +481,25 @@ var logger5 = createLogger("workflow-validate-tool");
|
|
|
480
481
|
var WorkflowValidateInputSchema = z6.object({
|
|
481
482
|
definition: z6.any().optional().describe("Workflow definition object (alternative to definition_yaml)"),
|
|
482
483
|
definition_yaml: z6.string().optional().describe("Workflow definition as YAML or JSON string"),
|
|
483
|
-
yaml: z6.string().optional().describe("Alias for definition_yaml — workflow definition as YAML or JSON string")
|
|
484
|
+
yaml: z6.string().optional().describe("Alias for definition_yaml — workflow definition as YAML or JSON string"),
|
|
485
|
+
file: z6.string().optional().describe("Path to a YAML/JSON file to validate. Use this INSTEAD of passing " + "`$(cat /path)` — shell substitutions are NOT expanded by this tool.")
|
|
484
486
|
});
|
|
487
|
+
function isAllowedPath(absPath) {
|
|
488
|
+
const cwd = process.cwd();
|
|
489
|
+
const normalized = absPath.replace(/\\/g, "/");
|
|
490
|
+
if (normalized === cwd || normalized.startsWith(`${cwd}/`))
|
|
491
|
+
return true;
|
|
492
|
+
if (normalized === "/tmp" || normalized.startsWith("/tmp/"))
|
|
493
|
+
return true;
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
function detectShellSubstitution(text) {
|
|
497
|
+
const trimmed = text.trim();
|
|
498
|
+
const looksLikeSubstitution = /^\$\(.*\)$/.test(trimmed) || /^`.*`$/.test(trimmed) || /\$\(\s*cat\s+/.test(trimmed) || /`\s*cat\s+/.test(trimmed);
|
|
499
|
+
if (!looksLikeSubstitution)
|
|
500
|
+
return;
|
|
501
|
+
return "Shell substitutions such as `$(cat /path)` are NOT expanded by this tool — the " + "string is passed to the YAML parser verbatim, which is why you saw " + "`Parsed definition is not an object`. Pass the path directly instead: " + 'workflow_validate({ file: "/tmp/x.yaml" }).';
|
|
502
|
+
}
|
|
485
503
|
function createWorkflowValidateTool(_workflowService) {
|
|
486
504
|
return {
|
|
487
505
|
name: "workflow_validate",
|
|
@@ -493,13 +511,56 @@ function createWorkflowValidateTool(_workflowService) {
|
|
|
493
511
|
let definition;
|
|
494
512
|
if (params.definition && typeof params.definition === "object") {
|
|
495
513
|
definition = params.definition;
|
|
514
|
+
} else if (params.file && typeof params.file === "string") {
|
|
515
|
+
const fs = await import("node:fs/promises");
|
|
516
|
+
const nodePath = await import("node:path");
|
|
517
|
+
const abs = nodePath.resolve(params.file);
|
|
518
|
+
if (!isAllowedPath(abs)) {
|
|
519
|
+
return {
|
|
520
|
+
success: false,
|
|
521
|
+
output: "",
|
|
522
|
+
error: `Refusing to read outside the working directory or /tmp: ${abs}. Copy the file into CWD or /tmp first.`,
|
|
523
|
+
metadata: { execution_time_ms: 0 }
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
let raw;
|
|
527
|
+
try {
|
|
528
|
+
raw = await fs.readFile(abs, "utf-8");
|
|
529
|
+
} catch (readErr) {
|
|
530
|
+
const code = readErr?.code;
|
|
531
|
+
return {
|
|
532
|
+
success: false,
|
|
533
|
+
output: "",
|
|
534
|
+
error: code === "ENOENT" ? `Workflow file not found: ${abs}. Check the path (it is resolved against CWD: ${process.cwd()}).` : `Failed to read workflow file ${abs}: ${readErr instanceof Error ? readErr.message : String(readErr)}`,
|
|
535
|
+
metadata: { execution_time_ms: 0 }
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
try {
|
|
539
|
+
definition = yaml2.parse(raw);
|
|
540
|
+
} catch (yamlErr) {
|
|
541
|
+
return {
|
|
542
|
+
success: false,
|
|
543
|
+
output: "",
|
|
544
|
+
error: `Invalid YAML in ${abs}: ${yamlErr instanceof Error ? yamlErr.message : String(yamlErr)}`,
|
|
545
|
+
metadata: { execution_time_ms: 0 }
|
|
546
|
+
};
|
|
547
|
+
}
|
|
496
548
|
} else {
|
|
497
549
|
const yamlText = params.yaml ?? params.definition_yaml;
|
|
498
550
|
if (!yamlText || typeof yamlText !== "string") {
|
|
499
551
|
return {
|
|
500
552
|
success: false,
|
|
501
553
|
output: "",
|
|
502
|
-
error: "Must provide either 'definition' (object), 'definition_yaml', or 'yaml' (string)",
|
|
554
|
+
error: "Must provide either 'definition' (object), 'file' (path), 'definition_yaml', or 'yaml' (string)",
|
|
555
|
+
metadata: { execution_time_ms: 0 }
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
const substitutionHint = detectShellSubstitution(yamlText);
|
|
559
|
+
if (substitutionHint) {
|
|
560
|
+
return {
|
|
561
|
+
success: false,
|
|
562
|
+
output: "",
|
|
563
|
+
error: substitutionHint,
|
|
503
564
|
metadata: { execution_time_ms: 0 }
|
|
504
565
|
};
|
|
505
566
|
}
|
|
@@ -518,7 +579,7 @@ function createWorkflowValidateTool(_workflowService) {
|
|
|
518
579
|
return {
|
|
519
580
|
success: false,
|
|
520
581
|
output: "",
|
|
521
|
-
error:
|
|
582
|
+
error: `Parsed definition is not an object (got ${definition === null ? "null" : typeof definition}). If you passed a file path as a string, use workflow_validate({ file: "/path/to.yaml" }) ` + "instead of embedding `$(cat ...)` — shell substitutions are not expanded here.",
|
|
522
583
|
metadata: { execution_time_ms: 0 }
|
|
523
584
|
};
|
|
524
585
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
SessionMessageConverter
|
|
3
|
-
} from "./roy-agent-core-
|
|
3
|
+
} from "./roy-agent-core-2b8jtv7m.js";
|
|
4
4
|
import {
|
|
5
5
|
canonicalizeParts
|
|
6
|
-
} from "./roy-agent-core-
|
|
6
|
+
} from "./roy-agent-core-0hq21px2.js";
|
|
7
7
|
import {
|
|
8
8
|
createLogger,
|
|
9
9
|
init_logger
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
createWorkflowSearchTool,
|
|
14
14
|
createWorkflowTagListTool,
|
|
15
15
|
createWorkflowValidateTool
|
|
16
|
-
} from "./roy-agent-core-
|
|
16
|
+
} from "./roy-agent-core-6qxh4xrm.js";
|
|
17
17
|
import {
|
|
18
18
|
WorkflowService
|
|
19
19
|
} from "./roy-agent-core-7evvpfwc.js";
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
WorkflowEngine,
|
|
22
22
|
exports_engine,
|
|
23
23
|
init_engine
|
|
24
|
-
} from "./roy-agent-core-
|
|
24
|
+
} from "./roy-agent-core-aer5kw1j.js";
|
|
25
25
|
import {
|
|
26
26
|
BaseComponent
|
|
27
27
|
} from "./roy-agent-core-tdbv7ff4.js";
|
|
@@ -315,6 +315,167 @@ function formatTemplateContractFindings(findings) {
|
|
|
315
315
|
|
|
316
316
|
// src/env/workflow/utils/validate-workflow-definition.ts
|
|
317
317
|
init_dag_manager();
|
|
318
|
+
|
|
319
|
+
// src/env/workflow/utils/shell-dialect-lint.ts
|
|
320
|
+
var RULES = [
|
|
321
|
+
{
|
|
322
|
+
name: "pipefail",
|
|
323
|
+
re: /\bset\s+(?:-[A-Za-z]+\s+)*-?[A-Za-z]*o[A-Za-z]*\s+pipefail\b/,
|
|
324
|
+
fix: "dash 不支持 set -o pipefail(含 set -euo pipefail)。改用落文件再取退出码: " + "`cmd > /tmp/o 2>&1; RC=$?; cat /tmp/o; exit $RC`"
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
name: "PIPESTATUS",
|
|
328
|
+
re: /\$\{PIPESTATUS\[/,
|
|
329
|
+
fix: "dash 不支持 ${PIPESTATUS[n]}。改用把管道左端输出落文件,再单独取 $?: " + "`cmd > /tmp/o 2>&1; RC=$?`"
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
name: "substring-replace",
|
|
333
|
+
re: /\$\{[A-Za-z_][A-Za-z0-9_]*\/\//,
|
|
334
|
+
fix: "dash 不支持 ${VAR//a/b}。改用 tr 或 sed: " + '`V=$(echo "$V" | tr -d ".")`'
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
name: "BASH_SOURCE",
|
|
338
|
+
re: /\$\{BASH_SOURCE/,
|
|
339
|
+
fix: "dash 不支持 ${BASH_SOURCE[0]}。改用 $0(或显式写绝对路径)"
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
name: "process-substitution",
|
|
343
|
+
re: /(?:^|[\s;&|(])[<>]\([^)]/,
|
|
344
|
+
fix: "dash 不支持进程替换 <(cmd) / >(cmd)。改用显式 bash 或临时文件: " + "`bash -c 'diff <(a) <(b)'` 或 `a > /tmp/a; b > /tmp/b; diff /tmp/a /tmp/b`"
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
name: "here-string",
|
|
348
|
+
re: /(?<!<)<<<\s*[^\s<]/,
|
|
349
|
+
fix: "dash 不支持 here-string <<<。改用 printf 管道: " + '`printf "%s\\n" "$X" | cmd`'
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
name: "ansi-c-quote",
|
|
353
|
+
re: /\$'(?:[^'\\]|\\.)*\\/,
|
|
354
|
+
fix: "dash 不支持 ANSI-C 转义 $'...'(会静默保留反斜杠)。改用 printf: " + '`printf "a\\tb\\n"`'
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
name: "dbl-bracket",
|
|
358
|
+
re: /\[\[(?=[\s\-!$\u0027])/,
|
|
359
|
+
fix: 'dash 不支持 [[ ]]。改用 [ ](注意 quote 变量: `[ -f "$F" ]`)'
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
name: "source",
|
|
363
|
+
re: /(?:^|[\s;&|(])source\s+(?:~|\$|\.|\/)[^\s;|&]*|(?:^|[\s;&|(])source\s+[A-Za-z0-9_.-]+\.sh\b/,
|
|
364
|
+
fix: "dash 不支持 source。改用 POSIX 点命令: `. <file>`"
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
name: "array",
|
|
368
|
+
re: /(^|[\s;&|(])([A-Za-z_][A-Za-z0-9_]*)=\(/,
|
|
369
|
+
fix: "dash 不支持数组。改用空格分隔的字符串 + for 循环: " + '`PYS="3.12 3.13"; for p in $PYS; do ...; done`'
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
name: "function-keyword",
|
|
373
|
+
re: /(^|\n)[ \t]*function\s+[A-Za-z_]/,
|
|
374
|
+
fix: "dash 不支持 function 关键字。改用 `name() { ... }`"
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
name: "RANDOM",
|
|
378
|
+
re: /\$RANDOM\b/,
|
|
379
|
+
fix: "dash 不支持 $RANDOM(静默为空)。改用 " + "`awk -v s=$(date +%s%N) 'BEGIN{srand(s); print int(rand()*32768)}'`"
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
name: "echo-e",
|
|
383
|
+
re: /(^|[\s;&|(])echo\s+-[ne]*e[A-Za-z]*\b/,
|
|
384
|
+
fix: 'dash 的 echo 不解析 -e(会输出字面 "-e")。改用 printf: `printf "%s\\n" "$X"`'
|
|
385
|
+
}
|
|
386
|
+
];
|
|
387
|
+
function stripComments(command) {
|
|
388
|
+
const lines = command.split(`
|
|
389
|
+
`);
|
|
390
|
+
const out = [];
|
|
391
|
+
for (const line of lines) {
|
|
392
|
+
let inSingle = false;
|
|
393
|
+
let inDouble = false;
|
|
394
|
+
let cut = -1;
|
|
395
|
+
for (let i = 0;i < line.length; i++) {
|
|
396
|
+
const ch = line[i];
|
|
397
|
+
if (ch === "\\" && !inSingle) {
|
|
398
|
+
i++;
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
if (ch === "'" && !inDouble) {
|
|
402
|
+
inSingle = !inSingle;
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
if (ch === '"' && !inSingle) {
|
|
406
|
+
inDouble = !inDouble;
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
if (ch === "#" && !inSingle && !inDouble) {
|
|
410
|
+
const prev = i === 0 ? " " : line[i - 1];
|
|
411
|
+
if (/\s/.test(prev)) {
|
|
412
|
+
cut = i;
|
|
413
|
+
break;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
out.push(cut >= 0 ? line.slice(0, cut) : line);
|
|
418
|
+
}
|
|
419
|
+
return out.join(`
|
|
420
|
+
`);
|
|
421
|
+
}
|
|
422
|
+
function stripQuotedHeredocBodies(command) {
|
|
423
|
+
const lines = command.split(`
|
|
424
|
+
`);
|
|
425
|
+
const out = [];
|
|
426
|
+
let delim = null;
|
|
427
|
+
for (const line of lines) {
|
|
428
|
+
if (delim !== null) {
|
|
429
|
+
if (line.trim() === delim) {
|
|
430
|
+
out.push(line);
|
|
431
|
+
delim = null;
|
|
432
|
+
}
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
const m = /<<-?\s*(?:'([^']+)'|"([^"]+)")/.exec(line);
|
|
436
|
+
out.push(line);
|
|
437
|
+
if (m)
|
|
438
|
+
delim = (m[1] ?? m[2] ?? "").trim();
|
|
439
|
+
}
|
|
440
|
+
return out.join(`
|
|
441
|
+
`);
|
|
442
|
+
}
|
|
443
|
+
function stripForeignShellBodies(command) {
|
|
444
|
+
const re = /(\b(?:bash|sh)\s+-[a-z]*c[a-z]*\s+)(?:'[\s\S]*?'|"[\s\S]*?")/g;
|
|
445
|
+
return command.replace(re, "$1<foreign-shell-body>");
|
|
446
|
+
}
|
|
447
|
+
function isEntirelyBashInvocation(command) {
|
|
448
|
+
const re = /^(?:[A-Za-z_][A-Za-z0-9_]*=[^\s;|&]*\s+)*\/?(?:[\w./-]*\/)?bash\s+-(?:[a-z]*c[a-z]*)\s+(?:'[\s\S]*'|"[\s\S]*")\s*$/;
|
|
449
|
+
return re.test(command.trim());
|
|
450
|
+
}
|
|
451
|
+
function lintShellDialect(command) {
|
|
452
|
+
if (!command || typeof command !== "string")
|
|
453
|
+
return [];
|
|
454
|
+
const scannable = stripComments(stripForeignShellBodies(stripQuotedHeredocBodies(command)));
|
|
455
|
+
if (scannable.trim().length === 0)
|
|
456
|
+
return [];
|
|
457
|
+
if (isEntirelyBashInvocation(scannable))
|
|
458
|
+
return [];
|
|
459
|
+
const findings = [];
|
|
460
|
+
const seen = new Set;
|
|
461
|
+
for (const rule of RULES) {
|
|
462
|
+
if (seen.has(rule.name))
|
|
463
|
+
continue;
|
|
464
|
+
const m = rule.re.exec(scannable);
|
|
465
|
+
if (!m)
|
|
466
|
+
continue;
|
|
467
|
+
seen.add(rule.name);
|
|
468
|
+
const start = Math.max(0, m.index - 30);
|
|
469
|
+
const excerpt = scannable.slice(start, m.index + m[0].length + 40).replace(/\s+/g, " ").trim();
|
|
470
|
+
findings.push({ name: rule.name, fix: rule.fix, excerpt });
|
|
471
|
+
}
|
|
472
|
+
return findings;
|
|
473
|
+
}
|
|
474
|
+
function formatShellDialectFindings(nodeId, findings) {
|
|
475
|
+
return findings.map((f) => `[shell-dialect] node "${nodeId}" uses bash-only "${f.name}": ${f.fix} | near: ${f.excerpt}`);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// src/env/workflow/utils/validate-workflow-definition.ts
|
|
318
479
|
function validateWorkflowDefinition(definition, options = {}) {
|
|
319
480
|
if (!definition || typeof definition !== "object") {
|
|
320
481
|
return {
|
|
@@ -352,8 +513,17 @@ function validateWorkflowDefinition(definition, options = {}) {
|
|
|
352
513
|
const contractErrors = findings.filter((f) => f.severity === "error");
|
|
353
514
|
const contractWarnings = findings.filter((f) => f.severity === "warning");
|
|
354
515
|
const contractInfos = findings.filter((f) => f.severity === "info");
|
|
516
|
+
const dialectWarnings = [];
|
|
517
|
+
if (structural.valid && options.lintShellDialect !== false) {
|
|
518
|
+
try {
|
|
519
|
+
dialectWarnings.push(...collectShellDialectWarnings(definition));
|
|
520
|
+
} catch {}
|
|
521
|
+
}
|
|
355
522
|
const errors = [...structural.errors, ...formatTemplateContractFindings(contractErrors)];
|
|
356
|
-
const warnings =
|
|
523
|
+
const warnings = [
|
|
524
|
+
...formatTemplateContractFindings(contractWarnings),
|
|
525
|
+
...dialectWarnings
|
|
526
|
+
];
|
|
357
527
|
const infos = formatTemplateContractFindings(contractInfos);
|
|
358
528
|
return {
|
|
359
529
|
valid: structural.valid && contractErrors.length === 0,
|
|
@@ -363,5 +533,46 @@ function validateWorkflowDefinition(definition, options = {}) {
|
|
|
363
533
|
...findings.length > 0 ? { findings } : {}
|
|
364
534
|
};
|
|
365
535
|
}
|
|
536
|
+
function extractToolCommands(config) {
|
|
537
|
+
if (!config || typeof config !== "object")
|
|
538
|
+
return [];
|
|
539
|
+
const out = [];
|
|
540
|
+
const COMMAND_KEYS = ["command", "cmd"];
|
|
541
|
+
const CONTAINER_KEYS = ["input", "inputs", "args"];
|
|
542
|
+
for (const key of COMMAND_KEYS) {
|
|
543
|
+
const v = config[key];
|
|
544
|
+
if (typeof v === "string")
|
|
545
|
+
out.push(v);
|
|
546
|
+
}
|
|
547
|
+
for (const key of CONTAINER_KEYS) {
|
|
548
|
+
const container = config[key];
|
|
549
|
+
if (!container || typeof container !== "object" || Array.isArray(container))
|
|
550
|
+
continue;
|
|
551
|
+
for (const cmdKey of COMMAND_KEYS) {
|
|
552
|
+
const v = container[cmdKey];
|
|
553
|
+
if (typeof v === "string")
|
|
554
|
+
out.push(v);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return out;
|
|
558
|
+
}
|
|
559
|
+
function collectShellDialectWarnings(definition) {
|
|
560
|
+
const def = definition;
|
|
561
|
+
if (!def || !Array.isArray(def.nodes))
|
|
562
|
+
return [];
|
|
563
|
+
const warnings = [];
|
|
564
|
+
for (const node of def.nodes) {
|
|
565
|
+
if (node?.type !== "tool")
|
|
566
|
+
continue;
|
|
567
|
+
const nodeId = typeof node.id === "string" ? node.id : String(node.id ?? "<unnamed>");
|
|
568
|
+
for (const command of extractToolCommands(node.config)) {
|
|
569
|
+
const findings = lintShellDialect(command);
|
|
570
|
+
if (findings.length > 0) {
|
|
571
|
+
warnings.push(...formatShellDialectFindings(nodeId, findings));
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
return warnings;
|
|
576
|
+
}
|
|
366
577
|
|
|
367
578
|
export { VIRTUAL_OUTPUT_FIELD, NODE_OUTPUT_CONTRACTS, getNodeOutputContract, hasClosedOutputContract, RESULT_INVALID_NODE_TYPES, getAgentDeclaredJsonFields, validateTemplateContracts, getTemplateContractErrors, getTemplateContractWarnings, formatTemplateContractFindings, validateWorkflowDefinition };
|