openfeel 1.3.2 → 1.3.4
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/README.en.md +1 -0
- package/README.md +2 -1
- package/README.zh-CN.md +1 -0
- package/dist/cli/deploy-check-output.d.ts.map +1 -1
- package/dist/cli/deploy-check-output.js +7 -1
- package/dist/cli/deploy-check-output.js.map +1 -1
- package/dist/commands/flow.d.ts.map +1 -1
- package/dist/commands/flow.js +98 -14
- package/dist/commands/flow.js.map +1 -1
- package/dist/commands/instructions.js +1 -1
- package/dist/commands/instructions.js.map +1 -1
- package/dist/commands/plan.d.ts.map +1 -1
- package/dist/commands/plan.js +9 -1
- package/dist/commands/plan.js.map +1 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +7 -0
- package/dist/commands/setup.js.map +1 -1
- package/dist/core/adapter-hash.d.ts +7 -0
- package/dist/core/adapter-hash.d.ts.map +1 -0
- package/dist/core/adapter-hash.js +64 -0
- package/dist/core/adapter-hash.js.map +1 -0
- package/dist/core/archive/merge.js +8 -8
- package/dist/core/archive/merge.js.map +1 -1
- package/dist/core/artifact-graph/resolver.d.ts.map +1 -1
- package/dist/core/artifact-graph/resolver.js +8 -0
- package/dist/core/artifact-graph/resolver.js.map +1 -1
- package/dist/core/config.d.ts +12 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +25 -3
- package/dist/core/config.js.map +1 -1
- package/dist/core/deployment-check.d.ts +17 -4
- package/dist/core/deployment-check.d.ts.map +1 -1
- package/dist/core/deployment-check.js +59 -7
- package/dist/core/deployment-check.js.map +1 -1
- package/dist/core/flow-manager.d.ts +26 -1
- package/dist/core/flow-manager.d.ts.map +1 -1
- package/dist/core/flow-manager.js +112 -9
- package/dist/core/flow-manager.js.map +1 -1
- package/dist/core/global-paths.d.ts +5 -0
- package/dist/core/global-paths.d.ts.map +1 -1
- package/dist/core/global-paths.js +7 -0
- package/dist/core/global-paths.js.map +1 -1
- package/dist/core/i18n-data/en.d.ts.map +1 -1
- package/dist/core/i18n-data/en.js +14 -0
- package/dist/core/i18n-data/en.js.map +1 -1
- package/dist/core/i18n-data/zh-CN.d.ts.map +1 -1
- package/dist/core/i18n-data/zh-CN.js +14 -0
- package/dist/core/i18n-data/zh-CN.js.map +1 -1
- package/dist/core/plan/stage.d.ts.map +1 -1
- package/dist/core/plan/stage.js +3 -1
- package/dist/core/plan/stage.js.map +1 -1
- package/dist/core/setup.d.ts.map +1 -1
- package/dist/core/setup.js +8 -3
- package/dist/core/setup.js.map +1 -1
- package/dist/core/template-loader.d.ts.map +1 -1
- package/dist/core/template-loader.js +38 -18
- package/dist/core/template-loader.js.map +1 -1
- package/dist/core/update-state.d.ts +20 -0
- package/dist/core/update-state.d.ts.map +1 -1
- package/dist/core/update-state.js +26 -0
- package/dist/core/update-state.js.map +1 -1
- package/dist/core/update.d.ts.map +1 -1
- package/dist/core/update.js +25 -5
- package/dist/core/update.js.map +1 -1
- package/dist/core/view/entry.d.ts +13 -4
- package/dist/core/view/entry.d.ts.map +1 -1
- package/dist/core/view/entry.js +40 -16
- package/dist/core/view/entry.js.map +1 -1
- package/dist/core/workspace/structure.d.ts +21 -1
- package/dist/core/workspace/structure.d.ts.map +1 -1
- package/dist/core/workspace/structure.js +30 -11
- package/dist/core/workspace/structure.js.map +1 -1
- package/dsh/ADAPTER.md +20 -8
- package/dsh/README.md +10 -5
- package/dsh/lib/adapters/dashboard.js +2 -2
- package/dsh/lib/adapters/host-state.js +111 -0
- package/dsh/lib/guard.js +78 -11
- package/dsh/lib/index.js +21 -0
- package/dsh/lib/tools/index.js +8 -3
- package/dsh/lib/tools/shared.js +28 -14
- package/dsh/package.json +2 -2
- package/package.json +1 -1
package/dsh/lib/guard.js
CHANGED
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
* @module openfeel-dsh/guard
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { readFileSync, statSync } from 'node:fs';
|
|
18
|
-
import { basename, join, resolve } from 'node:path';
|
|
17
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
18
|
+
import { basename, extname, join, resolve } from 'node:path';
|
|
19
19
|
|
|
20
20
|
import { isProjectWorkspace, OPENFEEL_DIR } from './adapters/workspace.js';
|
|
21
21
|
|
|
@@ -40,6 +40,35 @@ export function normalizeForComparison(value, platform = process.platform) {
|
|
|
40
40
|
return platform === 'win32' || platform === 'darwin' ? value.toLowerCase() : value;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/** 文档/笔记类扩展名(写门禁按产物类型分层:doc/note 不绑相位)。 */
|
|
44
|
+
export const DOC_EXTENSIONS = Object.freeze(['.md', '.mdx', '.txt', '.rst', '.adoc']);
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 是否为文档/笔记类目标(P0-4):扩展名命中,或位于**工作区根级 `docs/`** 下。
|
|
48
|
+
* v1.3.3 复核:`docs` 必须直接位于会话工作区下(`src/docs/x.ts` 不算文档,防源码借名绕过相位门)。
|
|
49
|
+
* 仅用于「放行 doc/note」;源代码仍按 `exec_running` 严格判定。
|
|
50
|
+
* @param {string} absolute 已归一化绝对路径
|
|
51
|
+
* @param {string} cwd 已归一化工作区
|
|
52
|
+
* @returns {boolean}
|
|
53
|
+
*/
|
|
54
|
+
function isDocPath(absolute, cwd) {
|
|
55
|
+
if (DOC_EXTENSIONS.includes(extname(absolute).toLowerCase())) return true;
|
|
56
|
+
const docsRoot = normalizeTarget(cwd, 'docs');
|
|
57
|
+
return absolute === docsRoot
|
|
58
|
+
|| absolute.startsWith(`${docsRoot}\\`)
|
|
59
|
+
|| absolute.startsWith(`${docsRoot}/`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 是否位于工作区内(bootstrap 例外边界;防 `..`/绝对路径写出工作区)。
|
|
64
|
+
* @param {string} absolute 已归一化绝对路径
|
|
65
|
+
* @param {string} cwd 已归一化工作区
|
|
66
|
+
* @returns {boolean}
|
|
67
|
+
*/
|
|
68
|
+
function isUnderWorkspace(absolute, cwd) {
|
|
69
|
+
return absolute === cwd || absolute.startsWith(`${cwd}\\`) || absolute.startsWith(`${cwd}/`);
|
|
70
|
+
}
|
|
71
|
+
|
|
43
72
|
/** flow.json 读取缓存(path → { mtimeMs, size, value })。 */
|
|
44
73
|
const flowCache = new Map();
|
|
45
74
|
|
|
@@ -120,22 +149,32 @@ function extractPatchText(args) {
|
|
|
120
149
|
}
|
|
121
150
|
|
|
122
151
|
/**
|
|
123
|
-
* 解析 apply_patch
|
|
152
|
+
* 解析 apply_patch 的全部目标路径与变更类型。
|
|
124
153
|
*
|
|
125
154
|
* 只认 patch 头,避免正文里出现 `.openfeel/` 被误判。支持:
|
|
126
155
|
* - `*** Update File:`, `*** Add File:`, `*** Delete File:`
|
|
127
156
|
* - `*** Move to:`(要求同块 `*** Update File:` 源,否则拒绝)
|
|
128
157
|
* - `--- a/path`, `+++ b/path`;忽略 `/dev/null`
|
|
129
158
|
*
|
|
159
|
+
* 返回 `entries`(路径 + kind)供 bootstrap 例外判定「是否纯新增」:
|
|
160
|
+
* kind ∈ `add | update | delete | move`;同一路径出现多种 kind 时从严记 `update`。
|
|
161
|
+
*
|
|
130
162
|
* @param {string} patch
|
|
131
|
-
* @returns {{ ok: true, paths: string[] } | { ok: false, reason: string }}
|
|
163
|
+
* @returns {{ ok: true, paths: string[], entries: Array<{ path: string, kind: string }> } | { ok: false, reason: string }}
|
|
132
164
|
*/
|
|
133
165
|
export function parsePatchTargets(patch) {
|
|
134
166
|
const paths = [];
|
|
167
|
+
const kinds = new Map();
|
|
135
168
|
const seen = new Set();
|
|
136
|
-
const add = (raw) => {
|
|
169
|
+
const add = (raw, kind) => {
|
|
137
170
|
const value = cleanPatchPath(raw);
|
|
138
171
|
if (value === '' || value === '/dev/null') return;
|
|
172
|
+
const prev = kinds.get(value);
|
|
173
|
+
if (prev === undefined) {
|
|
174
|
+
kinds.set(value, kind);
|
|
175
|
+
} else if (prev !== kind) {
|
|
176
|
+
kinds.set(value, 'update'); // 同路径混合 kind → 从严(bootstrap 例外不成立)
|
|
177
|
+
}
|
|
139
178
|
if (seen.has(value)) return;
|
|
140
179
|
seen.add(value);
|
|
141
180
|
paths.push(value);
|
|
@@ -147,19 +186,19 @@ export function parsePatchTargets(patch) {
|
|
|
147
186
|
const update = /^\*\*\* Update File:\s*(.+?)\s*$/.exec(line);
|
|
148
187
|
if (update !== null) {
|
|
149
188
|
currentBlock = 'update';
|
|
150
|
-
add(update[1]);
|
|
189
|
+
add(update[1], 'update');
|
|
151
190
|
continue;
|
|
152
191
|
}
|
|
153
192
|
const addFile = /^\*\*\* Add File:\s*(.+?)\s*$/.exec(line);
|
|
154
193
|
if (addFile !== null) {
|
|
155
194
|
currentBlock = 'add';
|
|
156
|
-
add(addFile[1]);
|
|
195
|
+
add(addFile[1], 'add');
|
|
157
196
|
continue;
|
|
158
197
|
}
|
|
159
198
|
const deleteFile = /^\*\*\* Delete File:\s*(.+?)\s*$/.exec(line);
|
|
160
199
|
if (deleteFile !== null) {
|
|
161
200
|
currentBlock = 'delete';
|
|
162
|
-
add(deleteFile[1]);
|
|
201
|
+
add(deleteFile[1], 'delete');
|
|
163
202
|
continue;
|
|
164
203
|
}
|
|
165
204
|
const move = /^\*\*\* Move to:\s*(.+?)\s*$/.exec(line);
|
|
@@ -167,7 +206,7 @@ export function parsePatchTargets(patch) {
|
|
|
167
206
|
if (currentBlock !== 'update') {
|
|
168
207
|
return { ok: false, reason: '*** Move to: 缺少同块 *** Update File: 源路径' };
|
|
169
208
|
}
|
|
170
|
-
add(move[1]);
|
|
209
|
+
add(move[1], 'move');
|
|
171
210
|
continue;
|
|
172
211
|
}
|
|
173
212
|
if (/^\*\*\* (?:Begin|End) Patch/.test(line)) {
|
|
@@ -175,13 +214,13 @@ export function parsePatchTargets(patch) {
|
|
|
175
214
|
continue;
|
|
176
215
|
}
|
|
177
216
|
const diff = /^(?:---|\+\+\+) [ab]\/(.+?)\s*$/.exec(line);
|
|
178
|
-
if (diff !== null) add(diff[1]);
|
|
217
|
+
if (diff !== null) add(diff[1], 'update');
|
|
179
218
|
}
|
|
180
219
|
|
|
181
220
|
if (paths.length === 0) {
|
|
182
221
|
return { ok: false, reason: '无法从 patch 头确定目标路径' };
|
|
183
222
|
}
|
|
184
|
-
return { ok: true, paths };
|
|
223
|
+
return { ok: true, paths, entries: paths.map((path) => ({ path, kind: kinds.get(path) })) };
|
|
185
224
|
}
|
|
186
225
|
|
|
187
226
|
/**
|
|
@@ -253,6 +292,8 @@ export function createOpenFeelGuard(options = {}) {
|
|
|
253
292
|
const fallbackCwd = typeof options.cwd === 'string' && options.cwd !== '' ? options.cwd : undefined;
|
|
254
293
|
|
|
255
294
|
const t = (zh, en) => (english ? en : zh);
|
|
295
|
+
/** bootstrap 例外提示去重(按工作区路径)。 */
|
|
296
|
+
const bootstrapHinted = new Set();
|
|
256
297
|
|
|
257
298
|
if (enforce === 'off') {
|
|
258
299
|
// 完全关闭:连 flow.json/status.md 也不拦。
|
|
@@ -270,6 +311,7 @@ export function createOpenFeelGuard(options = {}) {
|
|
|
270
311
|
const normalizedCwd = normalizeForComparison(resolvedCwd);
|
|
271
312
|
|
|
272
313
|
let rawTargets;
|
|
314
|
+
let patchEntries = null;
|
|
273
315
|
if (name === 'apply_patch') {
|
|
274
316
|
const patch = extractPatchText(execution?.arguments);
|
|
275
317
|
if (patch === undefined) {
|
|
@@ -286,6 +328,7 @@ export function createOpenFeelGuard(options = {}) {
|
|
|
286
328
|
);
|
|
287
329
|
}
|
|
288
330
|
rawTargets = parsed.paths;
|
|
331
|
+
patchEntries = parsed.entries;
|
|
289
332
|
} else {
|
|
290
333
|
rawTargets = extractArgumentPaths(execution?.arguments);
|
|
291
334
|
if (rawTargets.length === 0) {
|
|
@@ -297,6 +340,7 @@ export function createOpenFeelGuard(options = {}) {
|
|
|
297
340
|
}
|
|
298
341
|
|
|
299
342
|
const targets = rawTargets.map((target) => normalizeTarget(resolvedCwd, target));
|
|
343
|
+
const allDoc = targets.every((target) => isDocPath(target, normalizedCwd));
|
|
300
344
|
|
|
301
345
|
// 受保护路径:guard 启用时始终拒绝(enforce=off 已提前返回)。
|
|
302
346
|
if (targets.some((target) => isProtectedPath(target, normalizedCwd))) {
|
|
@@ -309,6 +353,25 @@ export function createOpenFeelGuard(options = {}) {
|
|
|
309
353
|
const initialized = isProjectWorkspace(resolvedCwd);
|
|
310
354
|
if (!initialized) {
|
|
311
355
|
if (enforce === 'always') {
|
|
356
|
+
// P0-1 bootstrap 例外:空工作区 + 全部为「新增文件」→ 放行(最高风险等级不成立)。
|
|
357
|
+
// apply_patch 额外要求全部 block 为 `*** Add File:`(Update/Delete/Move/混合一律从严)。
|
|
358
|
+
// `.openfeel/**` 不适用:流程产物仍要求先 init(B8)。
|
|
359
|
+
const allNew = targets.every((target) => !existsSync(target));
|
|
360
|
+
const anyInternal = targets.some((target) => isUnderOpenFeel(target, normalizedCwd));
|
|
361
|
+
const allInside = targets.every((target) => isUnderWorkspace(target, normalizedCwd));
|
|
362
|
+
const addOnly = patchEntries === null || patchEntries.every((entry) => entry.kind === 'add');
|
|
363
|
+
if (allNew && addOnly && !anyInternal && allInside) {
|
|
364
|
+
if (!bootstrapHinted.has(normalizedCwd)) {
|
|
365
|
+
bootstrapHinted.add(normalizedCwd);
|
|
366
|
+
process.stderr.write(
|
|
367
|
+
`${t(
|
|
368
|
+
'OpenFeel 写入门禁:[bootstrap] 工作区尚未初始化,已放行本次新增文件;建议稍后运行 `openfeel init` 补账本。',
|
|
369
|
+
'OpenFeel write guard: [bootstrap] workspace not initialized; this new-file write was allowed. Run `openfeel init` later to record it.',
|
|
370
|
+
)}\n`,
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
return undefined;
|
|
374
|
+
}
|
|
312
375
|
return t(
|
|
313
376
|
'OpenFeel 写入门禁:当前工作区尚未初始化 OpenFeel(无 `.openfeel/`)。请先运行 `openfeel init` 并建立需求/阶段;或让用户把 profile 的 `enforce` 改为 `project` / `off`。',
|
|
314
377
|
'OpenFeel write guard: this workspace is not OpenFeel-initialized (no `.openfeel/`). Run `openfeel init` and create a requirement/stage first; or ask the user to set `enforce` to `project` / `off`.',
|
|
@@ -331,6 +394,8 @@ export function createOpenFeelGuard(options = {}) {
|
|
|
331
394
|
const active = activeStages(flow.value);
|
|
332
395
|
if (active.length === 0) {
|
|
333
396
|
if (allowGeneral && hasGeneralRequirement(flow.value)) return undefined;
|
|
397
|
+
// P0-4:doc/note 类产物不要求活跃阶段(收口后仍可写文档);混合目标从严。
|
|
398
|
+
if (allDoc) return undefined;
|
|
334
399
|
return t(
|
|
335
400
|
'OpenFeel 写入门禁:当前无活跃阶段,不能修改源码/文档。请先 feel_req add → claim → feel_stage add 建立阶段。',
|
|
336
401
|
'OpenFeel write guard: no active stage, so source/docs are read-only. Create a stage via feel_req add → claim → feel_stage add.',
|
|
@@ -338,6 +403,8 @@ export function createOpenFeelGuard(options = {}) {
|
|
|
338
403
|
}
|
|
339
404
|
|
|
340
405
|
if (active.some((stage) => stage.phase === 'exec_running')) return undefined;
|
|
406
|
+
// P0-4:非 exec 相位仍允许 doc/note(研究记录、设计文档等);代码仍严格。
|
|
407
|
+
if (allDoc) return undefined;
|
|
341
408
|
|
|
342
409
|
const summary = active
|
|
343
410
|
.map((stage) => `${stage.id ?? '(unknown)'}=${stage.phase ?? '(unknown)'}`)
|
package/dsh/lib/index.js
CHANGED
|
@@ -22,8 +22,13 @@
|
|
|
22
22
|
* @module openfeel-dsh
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
+
import { readFileSync } from 'node:fs';
|
|
26
|
+
import { dirname } from 'node:path';
|
|
27
|
+
import { fileURLToPath } from 'node:url';
|
|
28
|
+
|
|
25
29
|
import { FeelCli } from './adapters/cli.js';
|
|
26
30
|
import { resolveWorkspace } from './adapters/workspace.js';
|
|
31
|
+
import { writeHostMarker } from './adapters/host-state.js';
|
|
27
32
|
import { createOpenFeelGuard } from './guard.js';
|
|
28
33
|
import { createFeelCommandHandler } from './surface/command.js';
|
|
29
34
|
import { FEEL_PROMPT_NAME, FEEL_PROMPT_ORDER, renderBootstrapPrompt, renderFeelPrompt } from './surface/prompt.js';
|
|
@@ -110,6 +115,22 @@ function currentAgent(ctx) {
|
|
|
110
115
|
*/
|
|
111
116
|
export function apply(ctx, rawConfig) {
|
|
112
117
|
const config = normalizeConfig(rawConfig);
|
|
118
|
+
|
|
119
|
+
// v1.3.4:记录「本宿主实际加载的适配器指纹」,供 `openfeel setup --check` 区分
|
|
120
|
+
// 「文件旧」与「文件已新但宿主未重启」。best-effort:失败不影响挂载。
|
|
121
|
+
try {
|
|
122
|
+
const adapterRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
123
|
+
let adapterVersion = '0.0.0';
|
|
124
|
+
try {
|
|
125
|
+
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
|
|
126
|
+
if (typeof pkg.version === 'string' && pkg.version !== '') adapterVersion = pkg.version;
|
|
127
|
+
} catch {
|
|
128
|
+
/* package.json 不可读时保持占位版本 */
|
|
129
|
+
}
|
|
130
|
+
writeHostMarker({ adapterRoot, version: adapterVersion });
|
|
131
|
+
} catch {
|
|
132
|
+
/* 标记写入失败 → CLI check 会提示 restart-required,可操作,不阻塞 */
|
|
133
|
+
}
|
|
113
134
|
const log = typeof ctx.logger === 'function'
|
|
114
135
|
? ctx.logger('openfeel-dsh')
|
|
115
136
|
: {
|
package/dsh/lib/tools/index.js
CHANGED
|
@@ -207,9 +207,9 @@ export function createTools(context) {
|
|
|
207
207
|
'操作。',
|
|
208
208
|
{ required: true },
|
|
209
209
|
),
|
|
210
|
-
stage: string('阶段 ID(advance/retry 必需;phases/mode 可选用于按阶段派生模式)。'),
|
|
210
|
+
stage: string('阶段 ID(advance/retry 必需;review_add 与 op 二选一;phases/mode 可选用于按阶段派生模式)。'),
|
|
211
211
|
to: stringEnum(PHASES, 'advance 的目标相位。'),
|
|
212
|
-
op: string('操作 ID(如 v1.2.1-stage-93.op-001
|
|
212
|
+
op: string('操作 ID(如 v1.2.1-stage-93.op-001);review_add 与 stage 二选一。'),
|
|
213
213
|
force: boolean('advance 时跳过相位合法性与跳跃检查(不能绕过 blocking REV 门)。'),
|
|
214
214
|
dry_run: boolean('仅校验/预览,不写盘。'),
|
|
215
215
|
review_tier: stringEnum(TIERS, '审查档位:full=零裁剪 / lean=裁 scheme_review / skip=再裁 plan_review。缺省不裁剪(=full)。'),
|
|
@@ -294,7 +294,12 @@ export function createTools(context) {
|
|
|
294
294
|
}
|
|
295
295
|
case 'review_add': {
|
|
296
296
|
const argv = ['flow', 'review', 'add'];
|
|
297
|
-
|
|
297
|
+
// v1.3.4:op / stage 二选一透传(fast 无 op 场景必须能挂阶段级 REV)
|
|
298
|
+
if (!args.op && !args.stage) {
|
|
299
|
+
throw new FeelError('review_add 需要 op 或 stage 之一', { kind: 'usage', code: 'FEEL_BAD_USAGE' });
|
|
300
|
+
}
|
|
301
|
+
if (args.op) pushOption(argv, '--op', args.op);
|
|
302
|
+
else pushOption(argv, '--stage', args.stage);
|
|
298
303
|
pushOption(argv, '--title', requireArg(args, 'title', action));
|
|
299
304
|
pushOption(argv, '--auto-fix', args.auto_fix);
|
|
300
305
|
if (args.blocking !== undefined) argv.push('--blocking', args.blocking === true ? 'true' : 'false');
|
package/dsh/lib/tools/shared.js
CHANGED
|
@@ -188,26 +188,40 @@ export function toWireEnvelope(action, payload) {
|
|
|
188
188
|
}
|
|
189
189
|
|
|
190
190
|
/**
|
|
191
|
-
*
|
|
191
|
+
* 安全序列化:只对**真环**(祖先路径上出现过的对象)写 `[循环引用]`;
|
|
192
|
+
* 同一对象的**重复引用**(DAG,如 focus.deps 与 stages[i].deps 指向同一数组)正常复制(P1-2)。
|
|
193
|
+
* 兼处理 BigInt / 不可序列化值,永不抛错。
|
|
192
194
|
* @param {unknown} value
|
|
193
195
|
* @returns {string}
|
|
194
196
|
*/
|
|
195
197
|
export function stringifySafe(value) {
|
|
196
198
|
try {
|
|
197
|
-
/**
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
199
|
+
/**
|
|
200
|
+
* @param {unknown} item
|
|
201
|
+
* @param {WeakSet<object>} ancestors
|
|
202
|
+
* @returns {unknown}
|
|
203
|
+
*/
|
|
204
|
+
const clone = (item, ancestors) => {
|
|
205
|
+
if (typeof item === 'bigint') return item.toString();
|
|
206
|
+
if (typeof item === 'function' || typeof item === 'symbol') return undefined;
|
|
207
|
+
if (typeof item !== 'object' || item === null) return item;
|
|
208
|
+
// 保留 Date / URL 等自带 toJSON 的语义(与 JSON.stringify 一致)
|
|
209
|
+
if (typeof item.toJSON === 'function') return item.toJSON();
|
|
210
|
+
if (ancestors.has(item)) return '[循环引用]';
|
|
211
|
+
ancestors.add(item);
|
|
212
|
+
let out;
|
|
213
|
+
if (Array.isArray(item)) {
|
|
214
|
+
out = item.map((entry) => clone(entry, ancestors));
|
|
215
|
+
} else {
|
|
216
|
+
out = {};
|
|
217
|
+
for (const [key, entry] of Object.entries(item)) {
|
|
218
|
+
out[key] = clone(entry, ancestors);
|
|
205
219
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
);
|
|
220
|
+
}
|
|
221
|
+
ancestors.delete(item);
|
|
222
|
+
return out;
|
|
223
|
+
};
|
|
224
|
+
return JSON.stringify(clone(value, new WeakSet()), null, 2);
|
|
211
225
|
} catch {
|
|
212
226
|
return String(value);
|
|
213
227
|
}
|
package/dsh/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openfeel-dsh",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.4",
|
|
4
4
|
"description": "OpenFeel × DSH — Feel 模式适配器:把 OpenFeel 的流水线、需求分发、版本归档与知识库接入 DeepSeek Harness",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": true,
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
},
|
|
33
33
|
"openfeel": {
|
|
34
34
|
"adapter": "dsh",
|
|
35
|
-
"requiresCli": ">=1.3.
|
|
35
|
+
"requiresCli": ">=1.3.4"
|
|
36
36
|
},
|
|
37
37
|
"keywords": [
|
|
38
38
|
"openfeel",
|