dsh-any-agent 0.3.3 → 0.3.5
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 +628 -0
- package/DESIGN.md +2 -2
- package/README.md +75 -22
- package/README.zh.md +46 -25
- package/USAGE-MIGRATION.md +9 -9
- package/USAGE-NOTES.en.md +4 -4
- package/USAGE-NOTES.md +7 -7
- package/bin/dsh-any-agent-workspaces.mjs +22 -3
- package/bin/dsh-any-agent.mjs +3 -2
- package/package.json +2 -1
- package/src/acp.mjs +24 -3
- package/src/bootstrap.mjs +73 -64
- package/src/clients.mjs +10 -15
- package/src/config.mjs +59 -8
- package/src/hub.mjs +124 -12
- package/src/tools.mjs +16 -8
- package/src/workspace.mjs +137 -39
- package/src/yaml-lite.mjs +424 -0
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 极简 YAML 读写器(只覆盖 DSH profile patch 用到的子集)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么不用 js-yaml:本包承诺**零运行时依赖**,且发布包不带 `runtime/`
|
|
5
|
+
* (见 package.json 的 files 白名单)。首启 bootstrap 只需要读取
|
|
6
|
+
* `cordis.patch.yml` 里的块映射/块序列、引号、注释、null/bool/number
|
|
7
|
+
* 和少量行内 `[]`/`{}`,自己实现比引入一个 YAML 解析器更可控。
|
|
8
|
+
*
|
|
9
|
+
* 明确**不支持**:锚点/别名、多文档合并、复杂标签、跨行 flow 集合、制表符缩进。
|
|
10
|
+
* 遇到不支持的结构会抛错,由调用方决定是跳过还是报错 —— 绝不"猜一个值"。
|
|
11
|
+
*
|
|
12
|
+
* 导出的 API 与 js-yaml 的 load/dump 同名,bootstrap 只需替换 import。
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** 去掉行内注释;引号内的 # 不算注释。 */
|
|
16
|
+
function stripComment(s) {
|
|
17
|
+
let quote = null;
|
|
18
|
+
let escaped = false;
|
|
19
|
+
for (let i = 0; i < s.length; i++) {
|
|
20
|
+
const c = s[i];
|
|
21
|
+
if (quote === "'") {
|
|
22
|
+
if (c === "'") {
|
|
23
|
+
if (s[i + 1] === "'") i++;
|
|
24
|
+
else quote = null;
|
|
25
|
+
}
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (quote === '"') {
|
|
29
|
+
if (escaped) escaped = false;
|
|
30
|
+
else if (c === '\\') escaped = true;
|
|
31
|
+
else if (c === '"') quote = null;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (c === "'" || c === '"') {
|
|
35
|
+
quote = c;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (c === '#' && (i === 0 || /\s/.test(s[i - 1]))) return s.slice(0, i);
|
|
39
|
+
}
|
|
40
|
+
return s;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** 解析带引号的标量;单引号只处理 '' 转义,双引号优先走 JSON。 */
|
|
44
|
+
function parseQuoted(s) {
|
|
45
|
+
if (s.startsWith('"')) {
|
|
46
|
+
try {
|
|
47
|
+
return JSON.parse(s);
|
|
48
|
+
} catch {
|
|
49
|
+
return s.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return s.slice(1, -1).replace(/''/g, "'");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const NULLS = new Set(['', '~', 'null', 'Null', 'NULL']);
|
|
56
|
+
const TRUES = new Set(['true', 'True', 'TRUE', 'yes', 'Yes', 'YES', 'on', 'On', 'ON']);
|
|
57
|
+
const FALSES = new Set(['false', 'False', 'FALSE', 'no', 'No', 'NO', 'off', 'Off', 'OFF']);
|
|
58
|
+
|
|
59
|
+
/** 标量:引号、null/bool、数字、行内集合、普通字符串。 */
|
|
60
|
+
function parseScalar(raw) {
|
|
61
|
+
const s = String(raw ?? '').trim();
|
|
62
|
+
if (NULLS.has(s)) return null;
|
|
63
|
+
if (TRUES.has(s)) return true;
|
|
64
|
+
if (FALSES.has(s)) return false;
|
|
65
|
+
if (s.startsWith('"') || s.startsWith("'")) {
|
|
66
|
+
if (!(s.endsWith(s[0]) && s.length >= 2)) throw new Error(`引号未闭合: ${s}`);
|
|
67
|
+
return parseQuoted(s);
|
|
68
|
+
}
|
|
69
|
+
if (s.startsWith('[') || s.startsWith('{')) return parseFlow(s);
|
|
70
|
+
// 标签与锚点:只处理最常见的一层,不展开别名。
|
|
71
|
+
if (s.startsWith('!!')) {
|
|
72
|
+
const sp = s.indexOf(' ');
|
|
73
|
+
return sp < 0 ? null : parseScalar(s.slice(sp + 1));
|
|
74
|
+
}
|
|
75
|
+
if (s.startsWith('&')) {
|
|
76
|
+
const sp = s.indexOf(' ');
|
|
77
|
+
return sp < 0 ? null : parseScalar(s.slice(sp + 1));
|
|
78
|
+
}
|
|
79
|
+
if (/^[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?$/.test(s)) {
|
|
80
|
+
// 保留前导 0(例如 08)为字符串,避免 YAML 1.1 的八进制歧义。
|
|
81
|
+
if (!/^[-+]?0\d/.test(s)) return Number(s);
|
|
82
|
+
}
|
|
83
|
+
return s;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** 行内 flow 集合:`[a, b]` / `{k: v}`,支持嵌套与引号。 */
|
|
87
|
+
function parseFlow(text) {
|
|
88
|
+
let i = 0;
|
|
89
|
+
const ws = () => {
|
|
90
|
+
while (i < text.length && /\s/.test(text[i])) i++;
|
|
91
|
+
};
|
|
92
|
+
const readQuoted = () => {
|
|
93
|
+
const q = text[i];
|
|
94
|
+
let j = i + 1;
|
|
95
|
+
let escaped = false;
|
|
96
|
+
for (; j < text.length; j++) {
|
|
97
|
+
const c = text[j];
|
|
98
|
+
if (q === "'") {
|
|
99
|
+
if (c === "'") {
|
|
100
|
+
if (text[j + 1] === "'") j++;
|
|
101
|
+
else break;
|
|
102
|
+
}
|
|
103
|
+
} else if (escaped) escaped = false;
|
|
104
|
+
else if (c === '\\') escaped = true;
|
|
105
|
+
else if (c === '"') break;
|
|
106
|
+
}
|
|
107
|
+
if (j >= text.length) throw new Error(`flow 集合里的引号未闭合: ${text}`);
|
|
108
|
+
const out = parseQuoted(text.slice(i, j + 1));
|
|
109
|
+
i = j + 1;
|
|
110
|
+
return out;
|
|
111
|
+
};
|
|
112
|
+
const readKey = () => {
|
|
113
|
+
ws();
|
|
114
|
+
if (text[i] === '"' || text[i] === "'") return readQuoted();
|
|
115
|
+
const start = i;
|
|
116
|
+
while (i < text.length && text[i] !== ':') i++;
|
|
117
|
+
return text.slice(start, i).trim();
|
|
118
|
+
};
|
|
119
|
+
const value = () => {
|
|
120
|
+
ws();
|
|
121
|
+
if (i >= text.length) throw new Error(`flow 集合不完整: ${text}`);
|
|
122
|
+
if (text[i] === '[') {
|
|
123
|
+
i++;
|
|
124
|
+
const arr = [];
|
|
125
|
+
ws();
|
|
126
|
+
if (text[i] === ']') {
|
|
127
|
+
i++;
|
|
128
|
+
return arr;
|
|
129
|
+
}
|
|
130
|
+
for (;;) {
|
|
131
|
+
arr.push(value());
|
|
132
|
+
ws();
|
|
133
|
+
if (text[i] === ',') {
|
|
134
|
+
i++;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (text[i] === ']') {
|
|
138
|
+
i++;
|
|
139
|
+
return arr;
|
|
140
|
+
}
|
|
141
|
+
throw new Error(`flow 数组缺少 , 或 ]: ${text}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (text[i] === '{') {
|
|
145
|
+
i++;
|
|
146
|
+
const obj = {};
|
|
147
|
+
ws();
|
|
148
|
+
if (text[i] === '}') {
|
|
149
|
+
i++;
|
|
150
|
+
return obj;
|
|
151
|
+
}
|
|
152
|
+
for (;;) {
|
|
153
|
+
const k = readKey();
|
|
154
|
+
ws();
|
|
155
|
+
if (text[i] !== ':') throw new Error(`flow 对象缺少 : ${text}`);
|
|
156
|
+
i++;
|
|
157
|
+
obj[k] = value();
|
|
158
|
+
ws();
|
|
159
|
+
if (text[i] === ',') {
|
|
160
|
+
i++;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (text[i] === '}') {
|
|
164
|
+
i++;
|
|
165
|
+
return obj;
|
|
166
|
+
}
|
|
167
|
+
throw new Error(`flow 对象缺少 , 或 }: ${text}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (text[i] === '"' || text[i] === "'") return readQuoted();
|
|
171
|
+
const start = i;
|
|
172
|
+
while (i < text.length && !/[,\]}]/.test(text[i])) i++;
|
|
173
|
+
return parseScalar(text.slice(start, i));
|
|
174
|
+
};
|
|
175
|
+
const out = value();
|
|
176
|
+
ws();
|
|
177
|
+
if (i !== text.length) throw new Error(`flow 集合尾部有多余内容: ${text}`);
|
|
178
|
+
return out;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** 找 `key: value` 的分隔冒号(引号外、冒号后是空白或行尾)。 */
|
|
182
|
+
function splitKey(text) {
|
|
183
|
+
let quote = null;
|
|
184
|
+
let escaped = false;
|
|
185
|
+
for (let i = 0; i < text.length; i++) {
|
|
186
|
+
const c = text[i];
|
|
187
|
+
if (quote === "'") {
|
|
188
|
+
if (c === "'") {
|
|
189
|
+
if (text[i + 1] === "'") i++;
|
|
190
|
+
else quote = null;
|
|
191
|
+
}
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (quote === '"') {
|
|
195
|
+
if (escaped) escaped = false;
|
|
196
|
+
else if (c === '\\') escaped = true;
|
|
197
|
+
else if (c === '"') quote = null;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (c === "'" || c === '"') {
|
|
201
|
+
quote = c;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (c === ':' && (i + 1 === text.length || /\s/.test(text[i + 1]))) {
|
|
205
|
+
let key = text.slice(0, i).trim();
|
|
206
|
+
if ((key.startsWith('"') && key.endsWith('"')) || (key.startsWith("'") && key.endsWith("'"))) {
|
|
207
|
+
key = parseQuoted(key);
|
|
208
|
+
}
|
|
209
|
+
return { key, value: text.slice(i + 1).trim() };
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const isSeq = (text) => text === '-' || /^-\s/.test(text);
|
|
216
|
+
|
|
217
|
+
function parseBlock(tokens, index, indent) {
|
|
218
|
+
if (index >= tokens.length || tokens[index].indent !== indent) {
|
|
219
|
+
throw new Error(`第 ${tokens[index]?.line ?? '?'} 行缩进不一致`);
|
|
220
|
+
}
|
|
221
|
+
return isSeq(tokens[index].text)
|
|
222
|
+
? parseSequence(tokens, index, indent)
|
|
223
|
+
: parseMapping(tokens, index, indent);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function parseSequence(tokens, index, indent) {
|
|
227
|
+
const arr = [];
|
|
228
|
+
let i = index;
|
|
229
|
+
while (i < tokens.length && tokens[i].indent === indent && isSeq(tokens[i].text)) {
|
|
230
|
+
const t = tokens[i];
|
|
231
|
+
const rest = t.text.replace(/^-\s?/, '').trim();
|
|
232
|
+
if (!rest) {
|
|
233
|
+
if (i + 1 < tokens.length && tokens[i + 1].indent > indent) {
|
|
234
|
+
const r = parseBlock(tokens, i + 1, tokens[i + 1].indent);
|
|
235
|
+
arr.push(r.value);
|
|
236
|
+
i = r.next;
|
|
237
|
+
} else {
|
|
238
|
+
arr.push(null);
|
|
239
|
+
i++;
|
|
240
|
+
}
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
const kv = splitKey(rest);
|
|
244
|
+
if (!kv) {
|
|
245
|
+
arr.push(parseScalar(rest));
|
|
246
|
+
i++;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
const obj = {};
|
|
250
|
+
if (kv.value === '') {
|
|
251
|
+
if (i + 1 < tokens.length && tokens[i + 1].indent > indent) {
|
|
252
|
+
const r = parseBlock(tokens, i + 1, tokens[i + 1].indent);
|
|
253
|
+
obj[kv.key] = r.value;
|
|
254
|
+
i = r.next;
|
|
255
|
+
} else {
|
|
256
|
+
obj[kv.key] = null;
|
|
257
|
+
i++;
|
|
258
|
+
}
|
|
259
|
+
} else if (kv.value === '|' || kv.value === '>') {
|
|
260
|
+
const folded = kv.value === '>';
|
|
261
|
+
const parts = [];
|
|
262
|
+
i++;
|
|
263
|
+
while (i < tokens.length && tokens[i].indent > indent) {
|
|
264
|
+
parts.push(tokens[i].text);
|
|
265
|
+
i++;
|
|
266
|
+
}
|
|
267
|
+
obj[kv.key] = parts.join(folded ? ' ' : '\n') + '\n';
|
|
268
|
+
continue;
|
|
269
|
+
} else {
|
|
270
|
+
obj[kv.key] = parseScalar(kv.value);
|
|
271
|
+
i++;
|
|
272
|
+
}
|
|
273
|
+
// `- key: value` 后面的同项字段(缩进大于序列缩进)属于同一个对象。
|
|
274
|
+
if (i < tokens.length && tokens[i].indent > indent && !isSeq(tokens[i].text)) {
|
|
275
|
+
const r = parseMapping(tokens, i, tokens[i].indent);
|
|
276
|
+
Object.assign(obj, r.value);
|
|
277
|
+
i = r.next;
|
|
278
|
+
}
|
|
279
|
+
arr.push(obj);
|
|
280
|
+
}
|
|
281
|
+
return { value: arr, next: i };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function parseMapping(tokens, index, indent) {
|
|
285
|
+
const obj = {};
|
|
286
|
+
let i = index;
|
|
287
|
+
while (i < tokens.length && tokens[i].indent === indent && !isSeq(tokens[i].text)) {
|
|
288
|
+
const t = tokens[i];
|
|
289
|
+
const kv = splitKey(t.text);
|
|
290
|
+
if (!kv) throw new Error(`第 ${t.line} 行不是 key: value:${t.text}`);
|
|
291
|
+
if (kv.value === '') {
|
|
292
|
+
if (i + 1 < tokens.length && tokens[i + 1].indent > indent) {
|
|
293
|
+
const r = parseBlock(tokens, i + 1, tokens[i + 1].indent);
|
|
294
|
+
obj[kv.key] = r.value;
|
|
295
|
+
i = r.next;
|
|
296
|
+
} else {
|
|
297
|
+
obj[kv.key] = null;
|
|
298
|
+
i++;
|
|
299
|
+
}
|
|
300
|
+
} else if (kv.value === '|' || kv.value === '>') {
|
|
301
|
+
const folded = kv.value === '>';
|
|
302
|
+
const parts = [];
|
|
303
|
+
i++;
|
|
304
|
+
while (i < tokens.length && tokens[i].indent > indent) {
|
|
305
|
+
parts.push(tokens[i].text);
|
|
306
|
+
i++;
|
|
307
|
+
}
|
|
308
|
+
obj[kv.key] = parts.join(folded ? ' ' : '\n') + '\n';
|
|
309
|
+
} else {
|
|
310
|
+
obj[kv.key] = parseScalar(kv.value);
|
|
311
|
+
i++;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return { value: obj, next: i };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* 解析一个 YAML 文档。
|
|
319
|
+
* @param {string} text
|
|
320
|
+
* @returns {any}
|
|
321
|
+
*/
|
|
322
|
+
export function load(text) {
|
|
323
|
+
const lines = String(text ?? '')
|
|
324
|
+
.replace(/^/, '')
|
|
325
|
+
.split(/\r?\n/);
|
|
326
|
+
const tokens = [];
|
|
327
|
+
for (let i = 0; i < lines.length; i++) {
|
|
328
|
+
const raw = lines[i];
|
|
329
|
+
if (/^\s*(?:#.*)?$/.test(raw)) continue;
|
|
330
|
+
if (/^\s*(?:---|\.\.\.)\s*(?:#.*)?$/.test(raw)) {
|
|
331
|
+
// 只支持单文档;遇到第二个文档分隔符就停止(DSH patch 只有一个文档)。
|
|
332
|
+
if (tokens.length) break;
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
if (/^\s*%/.test(raw)) continue;
|
|
336
|
+
const indentText = raw.match(/^[ \t]*/)[0];
|
|
337
|
+
if (indentText.includes('\t')) {
|
|
338
|
+
throw new Error(`第 ${i + 1} 行使用了制表符缩进(YAML 不允许)`);
|
|
339
|
+
}
|
|
340
|
+
const content = stripComment(raw.slice(indentText.length)).trimEnd();
|
|
341
|
+
if (!content) continue;
|
|
342
|
+
tokens.push({ indent: indentText.length, text: content, line: i + 1 });
|
|
343
|
+
}
|
|
344
|
+
if (!tokens.length) return null;
|
|
345
|
+
const r = parseBlock(tokens, 0, tokens[0].indent);
|
|
346
|
+
if (r.next !== tokens.length) {
|
|
347
|
+
throw new Error(`第 ${tokens[r.next]?.line ?? '?'} 行无法归入任何结构`);
|
|
348
|
+
}
|
|
349
|
+
return r.value;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const RESERVED = new Set([
|
|
353
|
+
'null', 'Null', 'NULL', '~', 'true', 'True', 'TRUE', 'false', 'False', 'FALSE',
|
|
354
|
+
'yes', 'Yes', 'YES', 'no', 'No', 'NO', 'on', 'On', 'ON', 'off', 'Off', 'OFF',
|
|
355
|
+
]);
|
|
356
|
+
|
|
357
|
+
/** 键名尽量保持可读;含特殊字符时用 JSON 字符串。 */
|
|
358
|
+
function dumpKey(k) {
|
|
359
|
+
const s = String(k);
|
|
360
|
+
if (/^[A-Za-z0-9_.-]+$/.test(s) && !RESERVED.has(s) && !/^[-+]?\d/.test(s)) return s;
|
|
361
|
+
return JSON.stringify(s);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** 标量:简单标识符/URL 用裸串,其余用 JSON 双引号(YAML 兼容)。 */
|
|
365
|
+
function dumpScalar(v) {
|
|
366
|
+
if (v === null || v === undefined) return 'null';
|
|
367
|
+
if (typeof v === 'boolean' || typeof v === 'number') return String(v);
|
|
368
|
+
const s = String(v);
|
|
369
|
+
if (
|
|
370
|
+
s &&
|
|
371
|
+
!/^\s|\s$/.test(s) &&
|
|
372
|
+
!RESERVED.has(s) &&
|
|
373
|
+
!/^[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?$/.test(s) &&
|
|
374
|
+
/^[A-Za-z0-9_./:@+-]+$/.test(s) &&
|
|
375
|
+
!/^[@`%&*!|>'"#]/.test(s)
|
|
376
|
+
) {
|
|
377
|
+
return s;
|
|
378
|
+
}
|
|
379
|
+
return JSON.stringify(s);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const isPlainObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
383
|
+
|
|
384
|
+
function dumpNode(v, indent) {
|
|
385
|
+
const pad = ' '.repeat(indent);
|
|
386
|
+
if (Array.isArray(v)) {
|
|
387
|
+
if (!v.length) return `${pad}[]`;
|
|
388
|
+
return v
|
|
389
|
+
.map((item) => {
|
|
390
|
+
if (!Array.isArray(item) && !isPlainObject(item)) return `${pad}- ${dumpScalar(item)}`;
|
|
391
|
+
if (Array.isArray(item)) return `${pad}-\n${dumpNode(item, indent + 2)}`;
|
|
392
|
+
const entries = Object.entries(item);
|
|
393
|
+
if (!entries.length) return `${pad}- {}`;
|
|
394
|
+
const [k, val] = entries[0];
|
|
395
|
+
let out = isPlainObject(val) || Array.isArray(val)
|
|
396
|
+
? `${pad}- ${dumpKey(k)}:\n${dumpNode(val, indent + 4)}`
|
|
397
|
+
: `${pad}- ${dumpKey(k)}: ${dumpScalar(val)}`;
|
|
398
|
+
for (const [k2, v2] of entries.slice(1)) {
|
|
399
|
+
out += isPlainObject(v2) || Array.isArray(v2)
|
|
400
|
+
? `\n${pad} ${dumpKey(k2)}:\n${dumpNode(v2, indent + 4)}`
|
|
401
|
+
: `\n${pad} ${dumpKey(k2)}: ${dumpScalar(v2)}`;
|
|
402
|
+
}
|
|
403
|
+
return out;
|
|
404
|
+
})
|
|
405
|
+
.join('\n');
|
|
406
|
+
}
|
|
407
|
+
if (isPlainObject(v)) {
|
|
408
|
+
const entries = Object.entries(v);
|
|
409
|
+
if (!entries.length) return `${pad}{}`;
|
|
410
|
+
return entries
|
|
411
|
+
.map(([k, val]) =>
|
|
412
|
+
isPlainObject(val) || Array.isArray(val)
|
|
413
|
+
? `${pad}${dumpKey(k)}:\n${dumpNode(val, indent + 2)}`
|
|
414
|
+
: `${pad}${dumpKey(k)}: ${dumpScalar(val)}`,
|
|
415
|
+
)
|
|
416
|
+
.join('\n');
|
|
417
|
+
}
|
|
418
|
+
return `${pad}${dumpScalar(v)}`;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** 序列化为块风格 YAML(根节点为数组/对象时)。 */
|
|
422
|
+
export function dump(value) {
|
|
423
|
+
return dumpNode(value, 0) + '\n';
|
|
424
|
+
}
|