@actiondock/core 2.12.0 → 2.13.0
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/input/describe.d.ts +12 -1
- package/dist/input/describe.js +55 -1
- package/dist/input/flat-materializer.d.ts +24 -0
- package/dist/input/flat-materializer.js +51 -14
- package/dist/input/index.d.ts +1 -0
- package/dist/input/index.js +1 -0
- package/dist/input/input-resolver.d.ts +10 -2
- package/dist/input/input-resolver.js +123 -4
- package/dist/input/output-contract.d.ts +72 -0
- package/dist/input/output-contract.js +274 -0
- package/dist/input/stdin-input.d.ts +11 -1
- package/dist/input/stdin-input.js +15 -3
- package/dist/project/index.d.ts +1 -1
- package/dist/project/index.js +2 -2
- package/dist/project/init.js +2 -2
- package/dist/runtime/action-collection.js +3 -0
- package/dist/runtime/standalone-commands.js +10 -1
- package/dist/runtime/standalone-help.js +2 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/dist/input/describe.d.ts
CHANGED
|
@@ -88,11 +88,22 @@ export declare function buildActionDescribePayload(spec: {
|
|
|
88
88
|
* 扁平输入通用语法速查行列表。
|
|
89
89
|
*/
|
|
90
90
|
export declare const ACTION_DESCRIBE_SYNTAX_REFERENCE: readonly string[];
|
|
91
|
+
/**
|
|
92
|
+
* 格式化 Action 详情的可选配置项。
|
|
93
|
+
*/
|
|
94
|
+
export interface FormatActionDetailOptions {
|
|
95
|
+
/**
|
|
96
|
+
* 是否支持显式与声明式正文选择运行选项(普通 CLI 为 true,目录型 Standalone 默认为 false)。
|
|
97
|
+
* @default true
|
|
98
|
+
*/
|
|
99
|
+
supportsTextField?: boolean;
|
|
100
|
+
}
|
|
91
101
|
/**
|
|
92
102
|
* 格式化渲染 Action 详情人类可读文本。
|
|
93
103
|
* 普通 CLI 与 Standalone 统一复用该函数,避免长期排版漂移。
|
|
94
104
|
*
|
|
95
105
|
* @param input Action 描述载荷或 Action 规范
|
|
106
|
+
* @param options 可选格式化配置项
|
|
96
107
|
* @returns 格式化后的说明文本
|
|
97
108
|
*/
|
|
98
109
|
export declare function formatActionDetail(input: ActionDescribePayload | (Partial<ActionSpec> & {
|
|
@@ -109,4 +120,4 @@ export declare function formatActionDetail(input: ActionDescribePayload | (Parti
|
|
|
109
120
|
uses?: string[];
|
|
110
121
|
entry?: string;
|
|
111
122
|
[key: string]: any;
|
|
112
|
-
}): string;
|
|
123
|
+
}, options?: FormatActionDetailOptions): string;
|
package/dist/input/describe.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { buildCliInputAdviceV1 } from "./advice.js";
|
|
2
2
|
import { isForbiddenActionInputPropertyName } from "./flat-predicates.js";
|
|
3
|
+
import { analyzeActionOutputContract } from "./output-contract.js";
|
|
3
4
|
/**
|
|
4
5
|
* 计算 Action 输入建议。
|
|
5
6
|
*
|
|
@@ -134,9 +135,10 @@ export const ACTION_DESCRIBE_SYNTAX_REFERENCE = [
|
|
|
134
135
|
* 普通 CLI 与 Standalone 统一复用该函数,避免长期排版漂移。
|
|
135
136
|
*
|
|
136
137
|
* @param input Action 描述载荷或 Action 规范
|
|
138
|
+
* @param options 可选格式化配置项
|
|
137
139
|
* @returns 格式化后的说明文本
|
|
138
140
|
*/
|
|
139
|
-
export function formatActionDetail(input) {
|
|
141
|
+
export function formatActionDetail(input, options) {
|
|
140
142
|
const payload = "inputAdvice" in input && input.inputAdvice
|
|
141
143
|
? input
|
|
142
144
|
: buildActionDescribePayload(input);
|
|
@@ -197,5 +199,57 @@ export function formatActionDetail(input) {
|
|
|
197
199
|
lines.push(` - ${issue.path}: ${issue.code}`);
|
|
198
200
|
}
|
|
199
201
|
}
|
|
202
|
+
const supportsTextField = options?.supportsTextField ?? true;
|
|
203
|
+
const outputContract = analyzeActionOutputContract({
|
|
204
|
+
annotations: payload.annotations,
|
|
205
|
+
outputSchema: payload.outputSchema,
|
|
206
|
+
});
|
|
207
|
+
const isAnnotationInvalid = outputContract.diagnostics.some((d) => d.code === "INVALID_ANNOTATION");
|
|
208
|
+
lines.push("");
|
|
209
|
+
lines.push("Output Selection:");
|
|
210
|
+
if (supportsTextField) {
|
|
211
|
+
if (isAnnotationInvalid) {
|
|
212
|
+
lines.push(` Invalid annotation: ${outputContract.errors.join("; ")}`);
|
|
213
|
+
lines.push(" Default run: Synchronous execution will be rejected before invocation due to invalid annotation");
|
|
214
|
+
lines.push(" Full result: Pass '--json' to bypass default annotation and receive the complete structured envelope");
|
|
215
|
+
lines.push(" Explicit text field: Pass '--text-field <field>' to bypass default annotation and select a specific field");
|
|
216
|
+
}
|
|
217
|
+
else if (outputContract.declared && outputContract.textField) {
|
|
218
|
+
lines.push(` Default text field: ${outputContract.textField}`);
|
|
219
|
+
if (outputContract.errors.length > 0) {
|
|
220
|
+
lines.push(` Contract conflict: ${outputContract.errors.join("; ")}`);
|
|
221
|
+
}
|
|
222
|
+
lines.push(` stdout: Raw text content of '${outputContract.textField}' (synchronous execution only)`);
|
|
223
|
+
lines.push(" stderr: Remaining fields as JSON metadata, diagnostics, and logs");
|
|
224
|
+
lines.push(" Full result: Pass '--json' to receive the complete structured envelope");
|
|
225
|
+
lines.push(" Override: Pass '--text-field <field>' to select another top-level string field");
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
lines.push(" Default: Raw string for string results; formatted JSON for objects and arrays");
|
|
229
|
+
lines.push(" Full result: Pass '--json' to receive the complete structured envelope");
|
|
230
|
+
lines.push(" Custom field: Pass '--text-field <field>' to extract a specific top-level string field");
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
if (isAnnotationInvalid) {
|
|
235
|
+
lines.push(` Invalid annotation: ${outputContract.errors.join("; ")} (note: ignored in standalone runtime; standalone extracts 'content', 'text', or 'message', falling back to formatted JSON)`);
|
|
236
|
+
lines.push(" stdout: Raw string for scalars; extracts 'content', 'text', or 'message' from objects");
|
|
237
|
+
lines.push(" stderr: Remaining fields as metadata (for extracted fields), diagnostics, and logs");
|
|
238
|
+
lines.push(" Full result: Pass '--json' to receive the complete structured envelope");
|
|
239
|
+
}
|
|
240
|
+
else if (outputContract.declared && outputContract.textField) {
|
|
241
|
+
lines.push(` Default text field: ${outputContract.textField} (note: ignored in standalone runtime; standalone extracts 'content', 'text', or 'message', falling back to formatted JSON)`);
|
|
242
|
+
if (outputContract.errors.length > 0) {
|
|
243
|
+
lines.push(` Contract conflict: ${outputContract.errors.join("; ")}`);
|
|
244
|
+
}
|
|
245
|
+
lines.push(" stdout: Raw string for scalars; extracts 'content', 'text', or 'message' from objects");
|
|
246
|
+
lines.push(" stderr: Remaining fields as metadata (for extracted fields), diagnostics, and logs");
|
|
247
|
+
lines.push(" Full result: Pass '--json' to receive the complete structured envelope");
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
lines.push(" Default: Raw string for scalars; extracts 'content', 'text', or 'message' from objects (with metadata on stderr), falling back to formatted JSON");
|
|
251
|
+
lines.push(" Full result: Pass '--json' to receive the complete structured envelope");
|
|
252
|
+
}
|
|
253
|
+
}
|
|
200
254
|
return lines.join("\n");
|
|
201
255
|
}
|
|
@@ -16,6 +16,30 @@ export type NodeState = (typeof NodeState)[keyof typeof NodeState];
|
|
|
16
16
|
export interface FlatMaterializerOptions {
|
|
17
17
|
maxMaterializedSizeBytes?: number;
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* 断言顶层字符串属性写入不与既有字段结构冲突。
|
|
21
|
+
*
|
|
22
|
+
* 作为扁平赋值与 stdin 字段绑定共享的路径冲突判定单一事实源:
|
|
23
|
+
* - 重复赋值同名属性:DUPLICATE_ASSIGNMENT;
|
|
24
|
+
* - 既有同名对象容器:LEAF_CONTAINER_CONFLICT;
|
|
25
|
+
* - 既有同名数组容器:OBJECT_ARRAY_CONFLICT。
|
|
26
|
+
*
|
|
27
|
+
* @param target 目标对象(仅检测自有属性,不触及原型链)
|
|
28
|
+
* @param field 待写入的顶层属性名
|
|
29
|
+
*/
|
|
30
|
+
export declare function assertTopLevelLeafAvailable(target: object, field: string): void;
|
|
31
|
+
/**
|
|
32
|
+
* 校验物化后的最终入参值:结构合法性与实体化总大小上限。
|
|
33
|
+
*
|
|
34
|
+
* 作为扁平赋值与 stdin 字段绑定共享的最终校验单一事实源:
|
|
35
|
+
* - 结构非法或超深:FLAT_INPUT_LIMIT_EXCEEDED(MAX_MATERIALIZED_JSON_DEPTH / INVALID_JSON_VALUE);
|
|
36
|
+
* - 序列化后字节超出 maxMaterializedSizeBytes(默认 10MiB):
|
|
37
|
+
* FLAT_INPUT_LIMIT_EXCEEDED(MAX_MATERIALIZED_BYTES)。
|
|
38
|
+
*
|
|
39
|
+
* @param result 待校验的最终入参对象
|
|
40
|
+
* @param options 物化配置选项(可选 maxMaterializedSizeBytes)
|
|
41
|
+
*/
|
|
42
|
+
export declare function assertMaterializedInputWithinLimits(result: JsonValue, options?: FlatMaterializerOptions): void;
|
|
19
43
|
/**
|
|
20
44
|
* 将扁平赋值表达式列表物化为标准 JSON 值对象。
|
|
21
45
|
*
|
|
@@ -11,6 +11,56 @@ export const NodeState = {
|
|
|
11
11
|
ARRAY: "ARRAY",
|
|
12
12
|
VALUE: "VALUE",
|
|
13
13
|
};
|
|
14
|
+
/**
|
|
15
|
+
* 断言顶层字符串属性写入不与既有字段结构冲突。
|
|
16
|
+
*
|
|
17
|
+
* 作为扁平赋值与 stdin 字段绑定共享的路径冲突判定单一事实源:
|
|
18
|
+
* - 重复赋值同名属性:DUPLICATE_ASSIGNMENT;
|
|
19
|
+
* - 既有同名对象容器:LEAF_CONTAINER_CONFLICT;
|
|
20
|
+
* - 既有同名数组容器:OBJECT_ARRAY_CONFLICT。
|
|
21
|
+
*
|
|
22
|
+
* @param target 目标对象(仅检测自有属性,不触及原型链)
|
|
23
|
+
* @param field 待写入的顶层属性名
|
|
24
|
+
*/
|
|
25
|
+
export function assertTopLevelLeafAvailable(target, field) {
|
|
26
|
+
if (!Object.hasOwn(target, field))
|
|
27
|
+
return;
|
|
28
|
+
const existing = target[field];
|
|
29
|
+
if (Array.isArray(existing)) {
|
|
30
|
+
throw inputPathConflict(`Path conflict at "${field}": expected object but found array`, { path: field, reason: "OBJECT_ARRAY_CONFLICT" });
|
|
31
|
+
}
|
|
32
|
+
if (existing !== null && typeof existing === "object") {
|
|
33
|
+
throw inputPathConflict(`Path conflict at "${field}": cannot assign value to existing container (object)`, { path: field, reason: "LEAF_CONTAINER_CONFLICT" });
|
|
34
|
+
}
|
|
35
|
+
throw inputPathConflict(`Duplicate assignment to leaf path "${field}"`, { path: field, reason: "DUPLICATE_ASSIGNMENT" });
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* 校验物化后的最终入参值:结构合法性与实体化总大小上限。
|
|
39
|
+
*
|
|
40
|
+
* 作为扁平赋值与 stdin 字段绑定共享的最终校验单一事实源:
|
|
41
|
+
* - 结构非法或超深:FLAT_INPUT_LIMIT_EXCEEDED(MAX_MATERIALIZED_JSON_DEPTH / INVALID_JSON_VALUE);
|
|
42
|
+
* - 序列化后字节超出 maxMaterializedSizeBytes(默认 10MiB):
|
|
43
|
+
* FLAT_INPUT_LIMIT_EXCEEDED(MAX_MATERIALIZED_BYTES)。
|
|
44
|
+
*
|
|
45
|
+
* @param result 待校验的最终入参对象
|
|
46
|
+
* @param options 物化配置选项(可选 maxMaterializedSizeBytes)
|
|
47
|
+
*/
|
|
48
|
+
export function assertMaterializedInputWithinLimits(result, options) {
|
|
49
|
+
const check = validateJsonValue(result);
|
|
50
|
+
if (!check.valid) {
|
|
51
|
+
throw flatInputLimitExceeded(`Materialized input validation failed: ${check.reason}`, {
|
|
52
|
+
reason: check.code === "MAX_JSON_DEPTH"
|
|
53
|
+
? "MAX_MATERIALIZED_JSON_DEPTH"
|
|
54
|
+
: "INVALID_JSON_VALUE",
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
const maxMaterializedSizeBytes = options?.maxMaterializedSizeBytes ?? DEFAULT_MAX_MATERIALIZED_SIZE_BYTES;
|
|
58
|
+
const serialized = JSON.stringify(result);
|
|
59
|
+
const byteLength = Buffer.byteLength(serialized, "utf8");
|
|
60
|
+
if (byteLength > maxMaterializedSizeBytes) {
|
|
61
|
+
throw flatInputLimitExceeded(`Materialized input size (${byteLength} bytes) exceeds limit (${maxMaterializedSizeBytes} bytes)`, { byteLength, maxMaterializedSizeBytes, reason: "MAX_MATERIALIZED_BYTES" });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
14
64
|
function createNode(pathStr, state = NodeState.UNSET) {
|
|
15
65
|
return {
|
|
16
66
|
state,
|
|
@@ -159,19 +209,6 @@ export function materializeFlatInput(assignments, options) {
|
|
|
159
209
|
}
|
|
160
210
|
}
|
|
161
211
|
const result = materializeNode(root);
|
|
162
|
-
|
|
163
|
-
if (!check.valid) {
|
|
164
|
-
throw flatInputLimitExceeded(`Materialized input validation failed: ${check.reason}`, {
|
|
165
|
-
reason: check.code === "MAX_JSON_DEPTH"
|
|
166
|
-
? "MAX_MATERIALIZED_JSON_DEPTH"
|
|
167
|
-
: "INVALID_JSON_VALUE",
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
const maxMaterializedSizeBytes = options?.maxMaterializedSizeBytes ?? DEFAULT_MAX_MATERIALIZED_SIZE_BYTES;
|
|
171
|
-
const serialized = JSON.stringify(result);
|
|
172
|
-
const byteLength = Buffer.byteLength(serialized, "utf8");
|
|
173
|
-
if (byteLength > maxMaterializedSizeBytes) {
|
|
174
|
-
throw flatInputLimitExceeded(`Materialized input size (${byteLength} bytes) exceeds limit (${maxMaterializedSizeBytes} bytes)`, { byteLength, maxMaterializedSizeBytes, reason: "MAX_MATERIALIZED_BYTES" });
|
|
175
|
-
}
|
|
212
|
+
assertMaterializedInputWithinLimits(result, options);
|
|
176
213
|
return result;
|
|
177
214
|
}
|
package/dist/input/index.d.ts
CHANGED
package/dist/input/index.js
CHANGED
|
@@ -16,6 +16,8 @@ export interface InputResolutionPolicy {
|
|
|
16
16
|
sanitizeInputErrors?: boolean;
|
|
17
17
|
/** 是否仅允许字节流输入(默认 false) */
|
|
18
18
|
byteStreamOnly?: boolean;
|
|
19
|
+
/** 原始 stdin 文本模式:保留开头 BOM,UTF-8 编码失败视为读取失败(默认 false,仅作用于 stdinField) */
|
|
20
|
+
stdinRawText?: boolean;
|
|
19
21
|
}
|
|
20
22
|
/**
|
|
21
23
|
* 从标准输入流中完整读取全部数据并转换为 UTF-8 字符串(保持历史接口兼容)。
|
|
@@ -30,6 +32,8 @@ export interface ResolveActionInputOptions {
|
|
|
30
32
|
input?: string;
|
|
31
33
|
inputFile?: string;
|
|
32
34
|
flatArgs?: string[];
|
|
35
|
+
/** 将 stdin 完整原始文本绑定为指定顶层入参字段的字符串值(与 input / inputFile 互斥,可与 flatArgs 组合) */
|
|
36
|
+
stdinField?: string;
|
|
33
37
|
stdin?: NodeJS.ReadableStream;
|
|
34
38
|
flatOptions?: FlatParserOptions & FlatMaterializerOptions;
|
|
35
39
|
policy?: InputResolutionPolicy;
|
|
@@ -51,11 +55,15 @@ export declare function parseJson(text: string, source: string, policy?: InputRe
|
|
|
51
55
|
* - effectiveFlat: flatArgs 存在且 length > 0
|
|
52
56
|
* - hasInlineJson: options.input !== undefined(即使为 "")
|
|
53
57
|
* - hasFileJson: options.inputFile !== undefined(包含 "-")
|
|
54
|
-
* -
|
|
55
|
-
* -
|
|
58
|
+
* - hasStdinField: options.stdinField !== undefined
|
|
59
|
+
* - flat / inline JSON / file JSON 三者仍保持原有严格互斥;
|
|
60
|
+
* - stdinField 可与 flatArgs 组合(正文绑定到字段 + 扁平补充其他字段),
|
|
61
|
+
* 但与 inline JSON、file JSON(含 inputFile="-")任何来源严格互斥;
|
|
62
|
+
* - 若四者均为 false,返回默认 {}。
|
|
56
63
|
* - flatArgs = [] 视为未指定。
|
|
57
64
|
* - options.input === "" 为显式 Full JSON 模式,进入解析并得到 INVALID_JSON / SYNTAX_ERROR,不可退化为 {}。
|
|
58
65
|
* - 显式空 JSON 输入(--input ""、0 字节文件、空 stdin、纯空白、纯 BOM)统一返回 INVALID_JSON / SYNTAX_ERROR。
|
|
66
|
+
* - stdinField 的 stdin 读取为原始文本绑定:不做 JSON 解析、不剥 BOM、保留首尾空白,空 stdin 得到空字符串。
|
|
59
67
|
*
|
|
60
68
|
* @param options 输入解析选项
|
|
61
69
|
* @returns 解析后的 JSON 对象
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { inputConflict, inputLimitExceeded, invalidJson, InputError, FlatInputError, } from "./flat-errors.js";
|
|
1
|
+
import { inputConflict, inputLimitExceeded, inputPathConflict, invalidFlatArgument, invalidJson, InputError, FlatInputError, } from "./flat-errors.js";
|
|
2
|
+
import { isFlatPathPropertyName, isForbiddenActionInputPropertyName, } from "./flat-predicates.js";
|
|
2
3
|
import { decodeFlatInput } from "./flat-decode.js";
|
|
4
|
+
import { assertTopLevelLeafAvailable, assertMaterializedInputWithinLimits, } from "./flat-materializer.js";
|
|
3
5
|
import { validateJsonValue, DEFAULT_MAX_JSON_DEPTH, } from "../value-validator.js";
|
|
4
6
|
import { scanJsonDepth } from "./json-depth-scanner.js";
|
|
5
7
|
import { decodeUtf8Strict, stripBom } from "./utf8.js";
|
|
@@ -103,6 +105,62 @@ function sanitizeError(err, source) {
|
|
|
103
105
|
}
|
|
104
106
|
return err;
|
|
105
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* 检测单个扁平 token 是否占用 stdin 绑定目标字段。
|
|
110
|
+
*
|
|
111
|
+
* 判定范围(在读 stdin 与执行业务之前):
|
|
112
|
+
* - 目标字段本身:重复赋值(DUPLICATE_ASSIGNMENT);
|
|
113
|
+
* - 目标字段的任意嵌套路径:叶/容器冲突(LEAF_CONTAINER_CONFLICT)。
|
|
114
|
+
*
|
|
115
|
+
* @param token 扁平赋值 token
|
|
116
|
+
* @param field stdin 绑定的目标字段名
|
|
117
|
+
*/
|
|
118
|
+
function detectFlatTokenFieldConflict(token, field) {
|
|
119
|
+
const colonEqIndex = token.indexOf(":=");
|
|
120
|
+
const eqIndex = token.indexOf("=");
|
|
121
|
+
let opIndex = -1;
|
|
122
|
+
if (colonEqIndex !== -1 && (eqIndex === -1 || colonEqIndex < eqIndex)) {
|
|
123
|
+
opIndex = colonEqIndex;
|
|
124
|
+
}
|
|
125
|
+
else if (eqIndex !== -1) {
|
|
126
|
+
opIndex = eqIndex;
|
|
127
|
+
}
|
|
128
|
+
const rawPath = opIndex === -1 ? token : token.slice(0, opIndex);
|
|
129
|
+
if (rawPath !== field && !rawPath.startsWith(`${field}.`))
|
|
130
|
+
return null;
|
|
131
|
+
return inputPathConflict(`Input path conflict at "${rawPath}": field "${field}" is already bound to stdin text via --stdin-field`, { path: rawPath, reason: rawPath === field ? "DUPLICATE_ASSIGNMENT" : "LEAF_CONTAINER_CONFLICT" });
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* 校验 stdin 字段绑定名是否为合法安全顶层属性名。
|
|
135
|
+
*
|
|
136
|
+
* 拒绝空、纯空白、危险属性(`__proto__`、`constructor`、`prototype`)与
|
|
137
|
+
* 不符合扁平命名段规范(含点分嵌套路径)的表达,错误为 INVALID_FLAT_ARGUMENT。
|
|
138
|
+
*
|
|
139
|
+
* @param field 用户提供的字段名
|
|
140
|
+
*/
|
|
141
|
+
function assertStdinFieldName(field) {
|
|
142
|
+
const problem = (() => {
|
|
143
|
+
if (field.length === 0)
|
|
144
|
+
return "empty";
|
|
145
|
+
if (field.trim() !== field || field.trim().length === 0)
|
|
146
|
+
return "whitespace";
|
|
147
|
+
if (isForbiddenActionInputPropertyName(field))
|
|
148
|
+
return "forbidden";
|
|
149
|
+
if (field.includes("."))
|
|
150
|
+
return "dotted";
|
|
151
|
+
if (!isFlatPathPropertyName(field))
|
|
152
|
+
return "invalid";
|
|
153
|
+
return null;
|
|
154
|
+
})();
|
|
155
|
+
if (problem === null)
|
|
156
|
+
return;
|
|
157
|
+
const reason = problem === "forbidden"
|
|
158
|
+
? "FORBIDDEN_PROPERTY"
|
|
159
|
+
: problem === "dotted"
|
|
160
|
+
? "INVALID_DOT_NOTATION"
|
|
161
|
+
: "INVALID_SEGMENT";
|
|
162
|
+
throw invalidFlatArgument(`Invalid --stdin-field name (length: ${field.length}): must be a non-empty top-level property name without dot notation`, { length: field.length, reason });
|
|
163
|
+
}
|
|
106
164
|
/**
|
|
107
165
|
* 解析完整 JSON 文档字符串。
|
|
108
166
|
* 若解析失败抛出 INVALID_JSON 异常。
|
|
@@ -152,11 +210,15 @@ export function parseJson(text, source, policy) {
|
|
|
152
210
|
* - effectiveFlat: flatArgs 存在且 length > 0
|
|
153
211
|
* - hasInlineJson: options.input !== undefined(即使为 "")
|
|
154
212
|
* - hasFileJson: options.inputFile !== undefined(包含 "-")
|
|
155
|
-
* -
|
|
156
|
-
* -
|
|
213
|
+
* - hasStdinField: options.stdinField !== undefined
|
|
214
|
+
* - flat / inline JSON / file JSON 三者仍保持原有严格互斥;
|
|
215
|
+
* - stdinField 可与 flatArgs 组合(正文绑定到字段 + 扁平补充其他字段),
|
|
216
|
+
* 但与 inline JSON、file JSON(含 inputFile="-")任何来源严格互斥;
|
|
217
|
+
* - 若四者均为 false,返回默认 {}。
|
|
157
218
|
* - flatArgs = [] 视为未指定。
|
|
158
219
|
* - options.input === "" 为显式 Full JSON 模式,进入解析并得到 INVALID_JSON / SYNTAX_ERROR,不可退化为 {}。
|
|
159
220
|
* - 显式空 JSON 输入(--input ""、0 字节文件、空 stdin、纯空白、纯 BOM)统一返回 INVALID_JSON / SYNTAX_ERROR。
|
|
221
|
+
* - stdinField 的 stdin 读取为原始文本绑定:不做 JSON 解析、不剥 BOM、保留首尾空白,空 stdin 得到空字符串。
|
|
160
222
|
*
|
|
161
223
|
* @param options 输入解析选项
|
|
162
224
|
* @returns 解析后的 JSON 对象
|
|
@@ -165,6 +227,22 @@ export async function resolveActionInput(options) {
|
|
|
165
227
|
const effectiveFlat = (options.flatArgs?.length ?? 0) > 0;
|
|
166
228
|
const hasInlineJson = options.input !== undefined;
|
|
167
229
|
const hasFileJson = options.inputFile !== undefined;
|
|
230
|
+
const hasStdinField = options.stdinField !== undefined;
|
|
231
|
+
// stdin 字段绑定与完整 JSON 来源(内联 / 文件 / stdin JSON)严格互斥
|
|
232
|
+
if (hasStdinField && (hasInlineJson || hasFileJson)) {
|
|
233
|
+
const err = inputConflict("Input options conflict: --stdin-field binds raw stdin text to an action input field and cannot be combined with inline JSON (-i/--input) or file input (-f/--input-file).", {
|
|
234
|
+
reason: "MULTIPLE_INPUT_MODES",
|
|
235
|
+
hasFlatArgs: effectiveFlat,
|
|
236
|
+
hasInput: hasInlineJson,
|
|
237
|
+
hasInputFile: hasFileJson,
|
|
238
|
+
hasStdinField,
|
|
239
|
+
});
|
|
240
|
+
if (options.policy?.sanitizeInputErrors) {
|
|
241
|
+
throw sanitizeError(err, "stdin");
|
|
242
|
+
}
|
|
243
|
+
throw err;
|
|
244
|
+
}
|
|
245
|
+
// 原有三种完整输入模式之间的严格互斥保持不变
|
|
168
246
|
let modeCount = 0;
|
|
169
247
|
if (effectiveFlat)
|
|
170
248
|
modeCount++;
|
|
@@ -184,11 +262,52 @@ export async function resolveActionInput(options) {
|
|
|
184
262
|
}
|
|
185
263
|
throw err;
|
|
186
264
|
}
|
|
187
|
-
if (modeCount === 0) {
|
|
265
|
+
if (modeCount === 0 && !hasStdinField) {
|
|
188
266
|
return {};
|
|
189
267
|
}
|
|
190
268
|
const policy = options.policy;
|
|
191
269
|
const maxBytes = policy?.maxInputBytes ?? DEFAULT_MAX_INPUT_BYTES;
|
|
270
|
+
// 0. stdin 原始文本字段绑定模式(可与 flatArgs 组合)
|
|
271
|
+
if (hasStdinField) {
|
|
272
|
+
const field = options.stdinField;
|
|
273
|
+
try {
|
|
274
|
+
assertStdinFieldName(field);
|
|
275
|
+
// 冲突检测与扁平完整解码均先于 stdin 读取:
|
|
276
|
+
// 目标字段占用冲突与任何已知的 flat 语法/路径/字面量错误在读 stdin 前拒绝,
|
|
277
|
+
// 不因等待上游 EOF 而挂起;解码结果复用,读取后不再重复解码。
|
|
278
|
+
let result = {};
|
|
279
|
+
if (effectiveFlat) {
|
|
280
|
+
for (const token of options.flatArgs) {
|
|
281
|
+
const conflict = detectFlatTokenFieldConflict(token, field);
|
|
282
|
+
if (conflict) {
|
|
283
|
+
throw conflict;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
result = decodeFlatInput(options.flatArgs, options.flatOptions);
|
|
287
|
+
assertTopLevelLeafAvailable(result, field);
|
|
288
|
+
}
|
|
289
|
+
const text = await readStdinBounded(options.stdin || process.stdin, {
|
|
290
|
+
maxInputBytes: maxBytes,
|
|
291
|
+
signal: options.signal,
|
|
292
|
+
byteStreamOnly: policy?.byteStreamOnly,
|
|
293
|
+
strictUtf8: policy?.strictUtf8 ?? true,
|
|
294
|
+
rawText: policy?.stdinRawText ?? true,
|
|
295
|
+
});
|
|
296
|
+
// 绑定后对最终入参执行既有实体化结构/总大小校验:
|
|
297
|
+
// stdin 源字节上限(maxInputBytes)与最终对象序列化上限
|
|
298
|
+
// (flatOptions.maxMaterializedSizeBytes,默认 10MiB)各自独立生效,
|
|
299
|
+
// 正文中的引号/反斜杠转义放大亦被最终上限拦截。
|
|
300
|
+
result[field] = text;
|
|
301
|
+
assertMaterializedInputWithinLimits(result, options.flatOptions);
|
|
302
|
+
return result;
|
|
303
|
+
}
|
|
304
|
+
catch (err) {
|
|
305
|
+
if (policy?.sanitizeInputErrors) {
|
|
306
|
+
throw sanitizeError(err, "stdin");
|
|
307
|
+
}
|
|
308
|
+
throw err;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
192
311
|
// 1. 扁平入参模式
|
|
193
312
|
if (effectiveFlat) {
|
|
194
313
|
try {
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Action 输出正文字段与 Schema 契约静态分析工具。
|
|
3
|
+
*
|
|
4
|
+
* 核心设计原则:
|
|
5
|
+
* - 纯数据分析:仅基于 Action 清单与 Schema 纯元数据推导,无数据库、无模块加载、无业务执行副作用。
|
|
6
|
+
* - 静态契约门禁:区分确定矛盾(错误)与无法证明安全(风险警告),不将不确定误判为合法,亦不为证明安全而试跑动作。
|
|
7
|
+
* - 单一事实源:声明解析、展示描述与门禁校验共用同一套底层纯规则。
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* 契约诊断项。
|
|
11
|
+
*/
|
|
12
|
+
export interface OutputContractDiagnostic {
|
|
13
|
+
type: "error" | "warning";
|
|
14
|
+
code: string;
|
|
15
|
+
message: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* 注解解析与检验结果。
|
|
19
|
+
*/
|
|
20
|
+
export interface AnnotationInspectionResult {
|
|
21
|
+
declared: boolean;
|
|
22
|
+
valid: boolean;
|
|
23
|
+
textField?: string;
|
|
24
|
+
error?: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* 输出契约分析输入项。
|
|
28
|
+
*/
|
|
29
|
+
export interface ActionOutputContractInput {
|
|
30
|
+
annotations?: unknown;
|
|
31
|
+
outputSchema?: unknown;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* 输出契约全量分析结果。
|
|
35
|
+
*/
|
|
36
|
+
export interface ActionOutputContractAnalysis {
|
|
37
|
+
declared: boolean;
|
|
38
|
+
valid: boolean;
|
|
39
|
+
textField?: string;
|
|
40
|
+
errors: string[];
|
|
41
|
+
warnings: string[];
|
|
42
|
+
diagnostics: OutputContractDiagnostic[];
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* 纯静态解析并校验 Action annotations 中的 actiondock.cli 声明。
|
|
46
|
+
*
|
|
47
|
+
* 契约规范:
|
|
48
|
+
* - 注解键名固定为 actiondock.cli,值必须为非空非数组对象。
|
|
49
|
+
* - 正文字段键名为 textField,值必须为非空字符串。
|
|
50
|
+
* - 无相关注解或未声明 textField 时返回 declared: false, valid: true。
|
|
51
|
+
* - 注解存在但类型非法时返回 valid: false 与对应错误描述。
|
|
52
|
+
*/
|
|
53
|
+
export declare function inspectActionTextFieldAnnotation(annotations?: unknown): AnnotationInspectionResult;
|
|
54
|
+
/**
|
|
55
|
+
* 静态分析 Action 输出契约(textField 注解与 outputSchema 的兼容性)。
|
|
56
|
+
*
|
|
57
|
+
* 错误判断(确定矛盾):
|
|
58
|
+
* - 注解结构非法或 textField 不是非空字符串;
|
|
59
|
+
* - Schema 明确拒绝所有结果(boolean false);
|
|
60
|
+
* - Schema 明确只允许非对象根类型;
|
|
61
|
+
* - Schema 明确禁止目标属性(property schema 为 false);
|
|
62
|
+
* - Schema 明确要求目标属性为非字符串类型;
|
|
63
|
+
* - 目标属性不存在且 Schema 明确禁止额外属性(additionalProperties 为 false)。
|
|
64
|
+
*
|
|
65
|
+
* 风险提示(无法完全静态证明安全):
|
|
66
|
+
* - 目标字段未声明为 required;
|
|
67
|
+
* - 目标字段允许 null 等非字符串类型;
|
|
68
|
+
* - outputSchema 缺失或为布尔 true;
|
|
69
|
+
* - 未声明 properties 但允许额外属性,或未声明目标属性但允许额外属性;
|
|
70
|
+
* - Schema 包含 oneOf/anyOf/allOf/$ref 复杂组合无法简单静态推导。
|
|
71
|
+
*/
|
|
72
|
+
export declare function analyzeActionOutputContract(input: ActionOutputContractInput): ActionOutputContractAnalysis;
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Action 输出正文字段与 Schema 契约静态分析工具。
|
|
3
|
+
*
|
|
4
|
+
* 核心设计原则:
|
|
5
|
+
* - 纯数据分析:仅基于 Action 清单与 Schema 纯元数据推导,无数据库、无模块加载、无业务执行副作用。
|
|
6
|
+
* - 静态契约门禁:区分确定矛盾(错误)与无法证明安全(风险警告),不将不确定误判为合法,亦不为证明安全而试跑动作。
|
|
7
|
+
* - 单一事实源:声明解析、展示描述与门禁校验共用同一套底层纯规则。
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* 纯静态解析并校验 Action annotations 中的 actiondock.cli 声明。
|
|
11
|
+
*
|
|
12
|
+
* 契约规范:
|
|
13
|
+
* - 注解键名固定为 actiondock.cli,值必须为非空非数组对象。
|
|
14
|
+
* - 正文字段键名为 textField,值必须为非空字符串。
|
|
15
|
+
* - 无相关注解或未声明 textField 时返回 declared: false, valid: true。
|
|
16
|
+
* - 注解存在但类型非法时返回 valid: false 与对应错误描述。
|
|
17
|
+
*/
|
|
18
|
+
export function inspectActionTextFieldAnnotation(annotations) {
|
|
19
|
+
if (!annotations || typeof annotations !== "object" || Array.isArray(annotations)) {
|
|
20
|
+
return { declared: false, valid: true };
|
|
21
|
+
}
|
|
22
|
+
const record = annotations;
|
|
23
|
+
if (!Object.hasOwn(record, "actiondock.cli")) {
|
|
24
|
+
return { declared: false, valid: true };
|
|
25
|
+
}
|
|
26
|
+
const cli = record["actiondock.cli"];
|
|
27
|
+
if (cli === undefined) {
|
|
28
|
+
return { declared: false, valid: true };
|
|
29
|
+
}
|
|
30
|
+
if (typeof cli !== "object" || cli === null || Array.isArray(cli)) {
|
|
31
|
+
return {
|
|
32
|
+
declared: true,
|
|
33
|
+
valid: false,
|
|
34
|
+
error: `Invalid 'actiondock.cli' annotation: expected an object, but received ${cli === null ? "null" : Array.isArray(cli) ? "an array" : typeof cli}.`,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const cliRecord = cli;
|
|
38
|
+
if (!Object.hasOwn(cliRecord, "textField") || cliRecord.textField === undefined) {
|
|
39
|
+
return { declared: false, valid: true };
|
|
40
|
+
}
|
|
41
|
+
const textField = cliRecord.textField;
|
|
42
|
+
if (typeof textField !== "string") {
|
|
43
|
+
return {
|
|
44
|
+
declared: true,
|
|
45
|
+
valid: false,
|
|
46
|
+
error: `Invalid 'actiondock.cli.textField' annotation: expected a string, but received ${textField === null ? "null" : Array.isArray(textField) ? "an array" : typeof textField}.`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (textField.trim() === "") {
|
|
50
|
+
return {
|
|
51
|
+
declared: true,
|
|
52
|
+
valid: false,
|
|
53
|
+
error: "Invalid 'actiondock.cli.textField' annotation: cannot be an empty string.",
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
declared: true,
|
|
58
|
+
valid: true,
|
|
59
|
+
textField,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* 静态分析 Action 输出契约(textField 注解与 outputSchema 的兼容性)。
|
|
64
|
+
*
|
|
65
|
+
* 错误判断(确定矛盾):
|
|
66
|
+
* - 注解结构非法或 textField 不是非空字符串;
|
|
67
|
+
* - Schema 明确拒绝所有结果(boolean false);
|
|
68
|
+
* - Schema 明确只允许非对象根类型;
|
|
69
|
+
* - Schema 明确禁止目标属性(property schema 为 false);
|
|
70
|
+
* - Schema 明确要求目标属性为非字符串类型;
|
|
71
|
+
* - 目标属性不存在且 Schema 明确禁止额外属性(additionalProperties 为 false)。
|
|
72
|
+
*
|
|
73
|
+
* 风险提示(无法完全静态证明安全):
|
|
74
|
+
* - 目标字段未声明为 required;
|
|
75
|
+
* - 目标字段允许 null 等非字符串类型;
|
|
76
|
+
* - outputSchema 缺失或为布尔 true;
|
|
77
|
+
* - 未声明 properties 但允许额外属性,或未声明目标属性但允许额外属性;
|
|
78
|
+
* - Schema 包含 oneOf/anyOf/allOf/$ref 复杂组合无法简单静态推导。
|
|
79
|
+
*/
|
|
80
|
+
export function analyzeActionOutputContract(input) {
|
|
81
|
+
const inspection = inspectActionTextFieldAnnotation(input.annotations);
|
|
82
|
+
if (!inspection.valid) {
|
|
83
|
+
const errorMsg = inspection.error;
|
|
84
|
+
return {
|
|
85
|
+
declared: true,
|
|
86
|
+
valid: false,
|
|
87
|
+
errors: [errorMsg],
|
|
88
|
+
warnings: [],
|
|
89
|
+
diagnostics: [
|
|
90
|
+
{
|
|
91
|
+
type: "error",
|
|
92
|
+
code: "INVALID_ANNOTATION",
|
|
93
|
+
message: errorMsg,
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
if (!inspection.declared || !inspection.textField) {
|
|
99
|
+
return {
|
|
100
|
+
declared: false,
|
|
101
|
+
valid: true,
|
|
102
|
+
errors: [],
|
|
103
|
+
warnings: [],
|
|
104
|
+
diagnostics: [],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const textField = inspection.textField;
|
|
108
|
+
const errors = [];
|
|
109
|
+
const warnings = [];
|
|
110
|
+
const diagnostics = [];
|
|
111
|
+
function addError(code, message) {
|
|
112
|
+
errors.push(message);
|
|
113
|
+
diagnostics.push({ type: "error", code, message });
|
|
114
|
+
}
|
|
115
|
+
function addWarning(code, message) {
|
|
116
|
+
warnings.push(message);
|
|
117
|
+
diagnostics.push({ type: "warning", code, message });
|
|
118
|
+
}
|
|
119
|
+
const schema = input.outputSchema;
|
|
120
|
+
if (schema === undefined) {
|
|
121
|
+
addWarning("OUTPUT_SCHEMA_ABSENT", `Output schema is absent; cannot statically verify text field '${textField}'. Runtime validation will apply.`);
|
|
122
|
+
return {
|
|
123
|
+
declared: true,
|
|
124
|
+
valid: true,
|
|
125
|
+
textField,
|
|
126
|
+
errors,
|
|
127
|
+
warnings,
|
|
128
|
+
diagnostics,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
if (schema === false) {
|
|
132
|
+
addError("OUTPUT_SCHEMA_REJECTS_ALL", `Output schema explicitly rejects all outputs (schema is false); cannot produce object containing text field '${textField}'.`);
|
|
133
|
+
return {
|
|
134
|
+
declared: true,
|
|
135
|
+
valid: false,
|
|
136
|
+
textField,
|
|
137
|
+
errors,
|
|
138
|
+
warnings,
|
|
139
|
+
diagnostics,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
if (schema === true) {
|
|
143
|
+
addWarning("OUTPUT_SCHEMA_ARBITRARY", `Output schema allows arbitrary values (schema is true); text field '${textField}' is not guaranteed to exist or be a string.`);
|
|
144
|
+
return {
|
|
145
|
+
declared: true,
|
|
146
|
+
valid: true,
|
|
147
|
+
textField,
|
|
148
|
+
errors,
|
|
149
|
+
warnings,
|
|
150
|
+
diagnostics,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
if (typeof schema !== "object" || schema === null || Array.isArray(schema)) {
|
|
154
|
+
addError("INVALID_OUTPUT_SCHEMA", "Invalid outputSchema: expected a schema object or boolean.");
|
|
155
|
+
return {
|
|
156
|
+
declared: true,
|
|
157
|
+
valid: false,
|
|
158
|
+
textField,
|
|
159
|
+
errors,
|
|
160
|
+
warnings,
|
|
161
|
+
diagnostics,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
const s = schema;
|
|
165
|
+
// 1. 检查根节点类型约束
|
|
166
|
+
if (s.type === undefined) {
|
|
167
|
+
addWarning("ROOT_TYPE_UNSPECIFIED", `Output schema does not explicitly specify root type as 'object'; non-object outputs cannot satisfy text field '${textField}'.`);
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
const rootTypes = Array.isArray(s.type) ? s.type : [s.type];
|
|
171
|
+
if (!rootTypes.includes("object")) {
|
|
172
|
+
addError("ROOT_TYPE_NOT_OBJECT", `Output schema root type is '${String(s.type)}', which does not allow an object required for text field '${textField}'.`);
|
|
173
|
+
return {
|
|
174
|
+
declared: true,
|
|
175
|
+
valid: false,
|
|
176
|
+
textField,
|
|
177
|
+
errors,
|
|
178
|
+
warnings,
|
|
179
|
+
diagnostics,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
const nonObjectTypes = rootTypes.filter((t) => t !== "object");
|
|
183
|
+
if (nonObjectTypes.length > 0) {
|
|
184
|
+
addWarning("ROOT_TYPE_ALLOWS_NON_OBJECT", `Output schema root type allows non-object types (${nonObjectTypes.join(", ")}); text output requires an object at runtime.`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// 2. 检查复杂组合关键字(oneOf, anyOf, allOf, $ref)
|
|
188
|
+
if (s.oneOf || s.anyOf || s.allOf || s.$ref) {
|
|
189
|
+
addWarning("COMPLEX_SCHEMA_COMPOSITION", `Output schema contains complex composition keywords (oneOf/anyOf/allOf/$ref); text field '${textField}' cannot be fully verified statically.`);
|
|
190
|
+
}
|
|
191
|
+
const hasPatternProps = Boolean(s.patternProperties &&
|
|
192
|
+
typeof s.patternProperties === "object" &&
|
|
193
|
+
!Array.isArray(s.patternProperties) &&
|
|
194
|
+
Object.keys(s.patternProperties).length > 0);
|
|
195
|
+
// 3. 检查属性定义与目标字段
|
|
196
|
+
if (s.properties && typeof s.properties === "object" && !Array.isArray(s.properties)) {
|
|
197
|
+
const props = s.properties;
|
|
198
|
+
if (Object.hasOwn(props, textField)) {
|
|
199
|
+
const fieldSchema = props[textField];
|
|
200
|
+
if (fieldSchema === false) {
|
|
201
|
+
addError("TEXT_FIELD_FORBIDDEN", `Output schema property '${textField}' is explicitly forbidden (property schema is false).`);
|
|
202
|
+
}
|
|
203
|
+
else if (fieldSchema === true) {
|
|
204
|
+
addWarning("TEXT_FIELD_UNTYPED", `Property '${textField}' does not specify a type; cannot guarantee it will be a string.`);
|
|
205
|
+
}
|
|
206
|
+
else if (typeof fieldSchema === "object" &&
|
|
207
|
+
fieldSchema !== null &&
|
|
208
|
+
!Array.isArray(fieldSchema)) {
|
|
209
|
+
const propObj = fieldSchema;
|
|
210
|
+
if (propObj.oneOf || propObj.anyOf || propObj.allOf || propObj.$ref) {
|
|
211
|
+
addWarning("TEXT_FIELD_COMPLEX_COMPOSITION", `Property '${textField}' contains complex composition keywords (oneOf/anyOf/allOf/$ref); text output cannot be fully verified statically.`);
|
|
212
|
+
}
|
|
213
|
+
if (propObj.type === undefined) {
|
|
214
|
+
addWarning("TEXT_FIELD_UNTYPED", `Property '${textField}' does not specify a type; cannot guarantee it will be a string.`);
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
const fieldTypes = Array.isArray(propObj.type) ? propObj.type : [propObj.type];
|
|
218
|
+
if (!fieldTypes.includes("string")) {
|
|
219
|
+
addError("TEXT_FIELD_NOT_STRING", `Output schema property '${textField}' has type '${String(propObj.type)}', which does not allow string.`);
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
const nonStringTypes = fieldTypes.filter((t) => t !== "string");
|
|
223
|
+
if (nonStringTypes.length > 0) {
|
|
224
|
+
addWarning("TEXT_FIELD_ALLOWS_NON_STRING", `Property '${textField}' allows non-string types (${nonStringTypes.join(", ")}); text output requires a string at runtime.`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
// 字段未在 properties 中明确定义
|
|
232
|
+
if (s.additionalProperties === false) {
|
|
233
|
+
if (hasPatternProps) {
|
|
234
|
+
addWarning("TEXT_FIELD_PATTERN_PROPERTIES", `Text field '${textField}' is not declared in properties; presence may be governed by patternProperties and cannot be fully verified statically.`);
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
addError("TEXT_FIELD_DISALLOWED_ADDITIONAL", `Text field '${textField}' is not declared in outputSchema properties and additionalProperties is false.`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
addWarning("TEXT_FIELD_UNDECLARED_ADDITIONAL", `Text field '${textField}' is not declared in outputSchema properties (allowed via additionalProperties); existence and type cannot be statically verified.`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
// 未定义 properties
|
|
247
|
+
if (s.additionalProperties === false) {
|
|
248
|
+
if (hasPatternProps) {
|
|
249
|
+
addWarning("TEXT_FIELD_PATTERN_PROPERTIES", `Text field '${textField}' is not declared in properties; presence may be governed by patternProperties and cannot be fully verified statically.`);
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
addError("TEXT_FIELD_DISALLOWED_NO_PROPERTIES", `Output schema has no properties declared and additionalProperties is false; text field '${textField}' cannot exist.`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
addWarning("TEXT_FIELD_NO_PROPERTIES", `Output schema does not define properties; text field '${textField}' existence cannot be statically verified.`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// 4. 检查 required 约束(若无确定阻止字段存在的错误)
|
|
260
|
+
if (errors.length === 0) {
|
|
261
|
+
const isRequired = Array.isArray(s.required) && s.required.includes(textField);
|
|
262
|
+
if (!isRequired) {
|
|
263
|
+
addWarning("TEXT_FIELD_NOT_REQUIRED", `Text field '${textField}' is not marked as required in outputSchema; if absent at runtime, text extraction will fail with OUTPUT_FORMAT_ERROR (business execution remains successful in run records).`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
declared: true,
|
|
268
|
+
valid: errors.length === 0,
|
|
269
|
+
textField,
|
|
270
|
+
errors,
|
|
271
|
+
warnings,
|
|
272
|
+
diagnostics,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
@@ -10,6 +10,15 @@ export interface ReadStdinBoundedOptions {
|
|
|
10
10
|
byteStreamOnly?: boolean;
|
|
11
11
|
/** 是否执行严格 UTF-8 校验与解码(默认 true) */
|
|
12
12
|
strictUtf8?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* 原始文本模式(默认 false)。
|
|
15
|
+
*
|
|
16
|
+
* 语义差异(仅在 strictUtf8 下生效):
|
|
17
|
+
* - 保留开头 BOM(U+FEFF)作为正文一部分,不执行剥离;
|
|
18
|
+
* - 编码失败映射为 INPUT_FILE_READ_FAILED(reason: "INVALID_UTF8")读取失败,
|
|
19
|
+
* 而不是完整 JSON 场景的 INVALID_JSON 解析失败。
|
|
20
|
+
*/
|
|
21
|
+
rawText?: boolean;
|
|
13
22
|
}
|
|
14
23
|
/**
|
|
15
24
|
* 以有界大小限制从标准输入流中安全读取全部内容。
|
|
@@ -17,7 +26,8 @@ export interface ReadStdinBoundedOptions {
|
|
|
17
26
|
* 核心契约:
|
|
18
27
|
* - 纯字节流策略拦截:若启用 byteStreamOnly 且流发射 string chunk,抛出 INPUT_FILE_READ_FAILED(reason: "INVALID_STREAM_CHUNK_TYPE")。
|
|
19
28
|
* - 终态竞态裁决(First Observed Terminal Event Wins):EOF、limit、abort 谁先观测谁生效,后续事件仅执行清理,不篡改已确定的错误与结果。
|
|
20
|
-
* - 严格 UTF-8 校验:在 strictUtf8 下通过 decodeUtf8Strict 校验并抛出结构化 INVALID_JSON
|
|
29
|
+
* - 严格 UTF-8 校验:在 strictUtf8 下通过 decodeUtf8Strict 校验并抛出结构化 INVALID_JSON 异常;
|
|
30
|
+
* rawText 模式改为保留 BOM 的 fatal 解码,编码失败抛出 INPUT_FILE_READ_FAILED(reason: "INVALID_UTF8")。
|
|
21
31
|
*
|
|
22
32
|
* @param stream 标准输入可读流,默认为 process.stdin
|
|
23
33
|
* @param options 读取配置选项
|
|
@@ -7,7 +7,8 @@ import { DEFAULT_MAX_INPUT_BYTES } from "./file-input.js";
|
|
|
7
7
|
* 核心契约:
|
|
8
8
|
* - 纯字节流策略拦截:若启用 byteStreamOnly 且流发射 string chunk,抛出 INPUT_FILE_READ_FAILED(reason: "INVALID_STREAM_CHUNK_TYPE")。
|
|
9
9
|
* - 终态竞态裁决(First Observed Terminal Event Wins):EOF、limit、abort 谁先观测谁生效,后续事件仅执行清理,不篡改已确定的错误与结果。
|
|
10
|
-
* - 严格 UTF-8 校验:在 strictUtf8 下通过 decodeUtf8Strict 校验并抛出结构化 INVALID_JSON
|
|
10
|
+
* - 严格 UTF-8 校验:在 strictUtf8 下通过 decodeUtf8Strict 校验并抛出结构化 INVALID_JSON 异常;
|
|
11
|
+
* rawText 模式改为保留 BOM 的 fatal 解码,编码失败抛出 INPUT_FILE_READ_FAILED(reason: "INVALID_UTF8")。
|
|
11
12
|
*
|
|
12
13
|
* @param stream 标准输入可读流,默认为 process.stdin
|
|
13
14
|
* @param options 读取配置选项
|
|
@@ -18,6 +19,7 @@ export async function readStdinBounded(stream = process.stdin, options) {
|
|
|
18
19
|
const signal = options?.signal;
|
|
19
20
|
const byteStreamOnly = options?.byteStreamOnly ?? false;
|
|
20
21
|
const strictUtf8 = options?.strictUtf8 ?? true;
|
|
22
|
+
const rawText = options?.rawText ?? false;
|
|
21
23
|
// 启动前检查 abort
|
|
22
24
|
if (signal?.aborted) {
|
|
23
25
|
throw signal.reason ?? new DOMException("The operation was aborted", "AbortError");
|
|
@@ -101,8 +103,18 @@ export async function readStdinBounded(stream = process.stdin, options) {
|
|
|
101
103
|
try {
|
|
102
104
|
const fullBuffer = Buffer.concat(chunks);
|
|
103
105
|
if (strictUtf8) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
+
if (rawText) {
|
|
107
|
+
// 原始文本模式:保留开头 BOM,编码失败视为读取失败而非 JSON 解析失败
|
|
108
|
+
try {
|
|
109
|
+
resolve(new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(fullBuffer));
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
reject(inputFileReadFailed("stdin", new Error(`Invalid UTF-8 encoding: ${err instanceof Error ? err.message : String(err)}`), { reason: "INVALID_UTF8" }));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
resolve(decodeUtf8Strict(fullBuffer, "full-json-stdin"));
|
|
117
|
+
}
|
|
106
118
|
}
|
|
107
119
|
else {
|
|
108
120
|
resolve(fullBuffer.toString("utf8"));
|
package/dist/project/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export * from "./types-generator.js";
|
|
|
9
9
|
export { assertPathWithinRoot, isPathOutsideBoundary, getPackageSlug, parseDuration, traverseDirectory, type TraverseDirectoryEntry, type TraverseDirectoryOptions, } from "../utils/index.js";
|
|
10
10
|
export { isSecretConfigKey, maskSecretValue, sanitizeConfigDefinitions, } from "../storage/mask.js";
|
|
11
11
|
export { runDoctorChecks, } from "../doctor/doctor.js";
|
|
12
|
-
export { resolveActionInput, buildActionDescribePayload, ACTION_DESCRIBE_SYNTAX_REFERENCE, mapInputValidationFailure, stripBom, readStdinBounded, type ResolveActionInputOptions, type ReadStdinBoundedOptions, } from "../input/index.js";
|
|
12
|
+
export { resolveActionInput, buildActionDescribePayload, formatActionDetail, type FormatActionDetailOptions, ACTION_DESCRIBE_SYNTAX_REFERENCE, inspectActionTextFieldAnnotation, analyzeActionOutputContract, type AnnotationInspectionResult, type ActionOutputContractInput, type ActionOutputContractAnalysis, type OutputContractDiagnostic, mapInputValidationFailure, stripBom, readStdinBounded, type ResolveActionInputOptions, type ReadStdinBoundedOptions, } from "../input/index.js";
|
|
13
13
|
export { validateActionInputValue } from "../value-validator.js";
|
|
14
14
|
export { parseJson } from "../input/input-resolver.js";
|
|
15
15
|
export { InputError, FlatInputError, } from "../input/flat-errors.js";
|
package/dist/project/index.js
CHANGED
|
@@ -9,8 +9,8 @@ export * from "./types-generator.js";
|
|
|
9
9
|
export { assertPathWithinRoot, isPathOutsideBoundary, getPackageSlug, parseDuration, traverseDirectory, } from "../utils/index.js";
|
|
10
10
|
export { isSecretConfigKey, maskSecretValue, sanitizeConfigDefinitions, } from "../storage/mask.js";
|
|
11
11
|
export { runDoctorChecks, } from "../doctor/doctor.js";
|
|
12
|
-
//
|
|
13
|
-
export { resolveActionInput, buildActionDescribePayload, ACTION_DESCRIBE_SYNTAX_REFERENCE, mapInputValidationFailure, stripBom, readStdinBounded, } from "../input/index.js";
|
|
12
|
+
// 动作入参解析、输出契约分析、校验与格式化(含 BOM 剥离与标准输入有界读取的单一事实源转引)
|
|
13
|
+
export { resolveActionInput, buildActionDescribePayload, formatActionDetail, ACTION_DESCRIBE_SYNTAX_REFERENCE, inspectActionTextFieldAnnotation, analyzeActionOutputContract, mapInputValidationFailure, stripBom, readStdinBounded, } from "../input/index.js";
|
|
14
14
|
export { validateActionInputValue } from "../value-validator.js";
|
|
15
15
|
export { parseJson } from "../input/input-resolver.js";
|
|
16
16
|
export { InputError, FlatInputError, } from "../input/flat-errors.js";
|
package/dist/project/init.js
CHANGED
|
@@ -86,10 +86,10 @@ export function initProject(targetDir, options = {}) {
|
|
|
86
86
|
node: ">=24.12.0",
|
|
87
87
|
},
|
|
88
88
|
dependencies: {
|
|
89
|
-
"@actiondock/sdk": "^2.
|
|
89
|
+
"@actiondock/sdk": "^2.13.0",
|
|
90
90
|
},
|
|
91
91
|
devDependencies: {
|
|
92
|
-
"@actiondock/testing": "^2.
|
|
92
|
+
"@actiondock/testing": "^2.13.0",
|
|
93
93
|
"@types/node": "^22.13.0",
|
|
94
94
|
"tsx": "^4.19.0",
|
|
95
95
|
"typescript": "^5.7.0",
|
|
@@ -22,6 +22,7 @@ export function normalizeActionCollection(rawActions) {
|
|
|
22
22
|
description: v.description,
|
|
23
23
|
inputSchema: v.inputSchema,
|
|
24
24
|
outputSchema: v.outputSchema,
|
|
25
|
+
annotations: v.annotations,
|
|
25
26
|
};
|
|
26
27
|
}
|
|
27
28
|
return { actionsMap, actionSpecs };
|
|
@@ -37,6 +38,7 @@ export function normalizeActionCollection(rawActions) {
|
|
|
37
38
|
description: item.description ?? act.description,
|
|
38
39
|
inputSchema: item.inputSchema ?? act.inputSchema,
|
|
39
40
|
outputSchema: item.outputSchema ?? act.outputSchema,
|
|
41
|
+
annotations: item.annotations ?? act.annotations,
|
|
40
42
|
};
|
|
41
43
|
}
|
|
42
44
|
}
|
|
@@ -50,6 +52,7 @@ export function normalizeActionCollection(rawActions) {
|
|
|
50
52
|
description: v.description,
|
|
51
53
|
inputSchema: v.inputSchema,
|
|
52
54
|
outputSchema: v.outputSchema,
|
|
55
|
+
annotations: v.annotations,
|
|
53
56
|
};
|
|
54
57
|
}
|
|
55
58
|
}
|
|
@@ -72,7 +72,7 @@ export async function handleDescribe(ctx, subArgs) {
|
|
|
72
72
|
ctx.writeOut(JSON.stringify(payload, null, 2));
|
|
73
73
|
}
|
|
74
74
|
else {
|
|
75
|
-
ctx.writeOut(formatActionDetail(payload));
|
|
75
|
+
ctx.writeOut(formatActionDetail(payload, { supportsTextField: false }));
|
|
76
76
|
}
|
|
77
77
|
return ExitCode.SUCCESS;
|
|
78
78
|
}
|
|
@@ -84,6 +84,7 @@ export async function handleRun(ctx, subArgs, actionArgs, signal) {
|
|
|
84
84
|
}
|
|
85
85
|
let inputStr;
|
|
86
86
|
let inputFile;
|
|
87
|
+
let stdinField;
|
|
87
88
|
let timeoutMs;
|
|
88
89
|
const recognizedFlags = new Set(["--json", "--async"]);
|
|
89
90
|
for (let i = 0; i < subArgs.length; i++) {
|
|
@@ -131,6 +132,12 @@ export async function handleRun(ctx, subArgs, actionArgs, signal) {
|
|
|
131
132
|
else if (arg.startsWith("-f=")) {
|
|
132
133
|
inputFile = arg.slice(3);
|
|
133
134
|
}
|
|
135
|
+
else if (arg === "--stdin-field" && i + 1 < subArgs.length) {
|
|
136
|
+
stdinField = subArgs[++i];
|
|
137
|
+
}
|
|
138
|
+
else if (arg.startsWith("--stdin-field=")) {
|
|
139
|
+
stdinField = arg.slice(14);
|
|
140
|
+
}
|
|
134
141
|
else if (arg.startsWith("-")) {
|
|
135
142
|
const hint = "Hint: Separate action inputs from CLI options using '--', e.g.: ad run <id> [options] -- <param>=<val> or <param>:=<json>.";
|
|
136
143
|
return ctx.emitError(isJson, "INVALID_ARGUMENT", `error: unknown option '${arg}'`, ExitCode.INVALID_ARGUMENT, {
|
|
@@ -144,8 +151,10 @@ export async function handleRun(ctx, subArgs, actionArgs, signal) {
|
|
|
144
151
|
input = await resolveActionInput({
|
|
145
152
|
input: inputStr,
|
|
146
153
|
inputFile,
|
|
154
|
+
stdinField,
|
|
147
155
|
flatArgs: actionArgs.length > 0 ? actionArgs : undefined,
|
|
148
156
|
stdin: process.stdin,
|
|
157
|
+
signal,
|
|
149
158
|
});
|
|
150
159
|
const check = validateActionInputValue(input);
|
|
151
160
|
if (!check.valid) {
|
|
@@ -42,6 +42,8 @@ export function printHelp(writeOut, options) {
|
|
|
42
42
|
writeOut(" <cmd> describe <id> [--json] Show action details and schemas");
|
|
43
43
|
writeOut(" <cmd> run <id> [--input '<json>'] Execute action (raw text output by default)");
|
|
44
44
|
writeOut(" <cmd> run <id> [--json] Output standard execution result in JSON format");
|
|
45
|
+
writeOut(" <cmd> run <id> [--stdin-field <field>] [-- <assignments...>]");
|
|
46
|
+
writeOut(" Bind raw stdin text to an input field");
|
|
45
47
|
writeOut(" <cmd> config list/get/set/delete Manage package configuration");
|
|
46
48
|
writeOut(" <cmd> state list/get/set/delete Manage shared state store");
|
|
47
49
|
writeOut("\nGlobal options:");
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@actiondock/core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.13.0",
|
|
4
4
|
"description": "ActionDock Core Engine - Project loader, runtime execution, SQLite storage, standalone builder, and skill exporter",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"test": "node ../../scripts/run-tests.ts"
|
|
65
65
|
},
|
|
66
66
|
"dependencies": {
|
|
67
|
-
"@actiondock/sdk": "^2.
|
|
67
|
+
"@actiondock/sdk": "^2.13.0",
|
|
68
68
|
"ajv": "^8.17.1",
|
|
69
69
|
"ajv-formats": "^3.0.1",
|
|
70
70
|
"undici": "^7.29.1"
|