@tbox.cn/app-toolkit 0.4.0 → 0.6.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/chunk-WUULQDOV.js +1 -0
- package/dist/contracts-resolver-PS2UH2G6.js +1 -0
- package/dist/index.d.ts +374 -150
- package/dist/index.js +9 -9
- package/package.json +2 -2
- package/src/assembly/provider-catalog.ts +1 -1
- package/src/assembly/resolve-key.ts +75 -0
- package/src/assembly/walk-manifests.ts +138 -85
- package/src/core/contracts-expected.ts +2 -2
- package/src/core/contracts-resolver.ts +12 -1
- package/src/core/module-schema.ts +90 -14
- package/src/dto.ts +80 -33
- package/src/factory.ts +78 -26
- package/src/index.ts +16 -7
- package/src/integrations/mock-bindings.ts +1 -1
- package/src/integrations/module-binding.ts +60 -0
- package/src/integrations/predicate-io.ts +26 -5
- package/src/integrations/predicate.ts +97 -57
- package/src/integrations/prune-bindings.ts +33 -14
- package/src/integrations/write.ts +47 -27
- package/src/views/app.ts +4 -4
- package/src/views/context.ts +15 -14
- package/src/views/integration-schemas.ts +76 -18
- package/src/views/modules.ts +93 -25
- package/src/views/providers.ts +5 -5
- package/src/views/service-detail.ts +1 -1
- package/src/views/service-resolutions.ts +14 -13
- package/tests/credentials-view.test.ts +2 -2
- package/tests/demo-app.ts +11 -6
- package/tests/dev-manifest-catalog.test.ts +14 -14
- package/tests/file-cache.test.ts +5 -5
- package/tests/module-resources.test.ts +148 -0
- package/tests/module-schema.test.ts +120 -20
- package/tests/resolve-key.test.ts +154 -0
- package/tests/vendor-schema-keywords.test.ts +74 -0
- package/tests/views.test.ts +585 -247
- package/tests/write-core.test.ts +117 -0
- package/dist/chunk-KPD3LUH4.js +0 -1
- package/dist/contracts-resolver-E4SECBOG.js +0 -1
- package/src/integrations/domain-binding.ts +0 -81
|
@@ -19,8 +19,9 @@ import {
|
|
|
19
19
|
import type { IntegrationServiceIssue } from './predicate.js';
|
|
20
20
|
import { checkInstanceKeys } from './predicate.js';
|
|
21
21
|
import { isKnownService } from './read.js';
|
|
22
|
-
import {
|
|
22
|
+
import { moduleOwnershipWritable } from './module-binding.js';
|
|
23
23
|
import { AppToolkitError } from '../core/errors.js';
|
|
24
|
+
import { MODULE_INTEGRATION_NODE_KEYS } from '../dto.js';
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* 写核心(C3-4 write;FX-1c 管线重排——F3 九步定序 + D-B2 语义 + F4 输入扩位):
|
|
@@ -231,7 +232,8 @@ export interface WriteCore {
|
|
|
231
232
|
warmup(): Promise<void>;
|
|
232
233
|
/** PUT /app/integration(root 节点全集) */
|
|
233
234
|
applyApp(node: Record<string, unknown>, opts?: WriteOptions): SaveResult;
|
|
234
|
-
/** PUT /modules/:module/integration(
|
|
235
|
+
/** PUT /modules/:module/integration(v6 modules[owner] 集成位;键集纪律见 MODULE_INTEGRATION_NODE_KEYS;
|
|
236
|
+
* N1 {} 清 provider/config 保 resources;moduleOwnershipWritable 写守卫单源) */
|
|
235
237
|
applyModule(moduleId: string, node: Record<string, unknown>, opts?: WriteOptions): SaveResult;
|
|
236
238
|
/** PUT /services/:service/integration(整节点含 instances map;N3 {} 不归一化;v4.4 A2 身份判据) */
|
|
237
239
|
applyService(service: string, node: Record<string, unknown>, opts?: WriteOptions): SaveResult;
|
|
@@ -259,6 +261,10 @@ export function createWriteCore(appDir: string, opts: WriteCoreOptions = {}): Wr
|
|
|
259
261
|
cache?.invalidate([join(appDir, INTEGRATIONS_FILE)]);
|
|
260
262
|
}
|
|
261
263
|
const written = integrationsWritten || credentialsWritten;
|
|
264
|
+
// 孤儿凭据写后对账(api.md §4.13 承诺追平——「覆盖 DELETE 与 PUT 替换两类来源」;三 PUT 写路径
|
|
265
|
+
// finalize 单点收口,未来第四写方法自动继承)。dryRun(written 蕴含 !dryRun)与 no-diff
|
|
266
|
+
// (finalize 前短路 return)天然跳过;applyModule modules 节无凭据键——对账恒零新增,无害捎带。
|
|
267
|
+
if (written) issues.push(...reconcileOrphanCredentials(appDir));
|
|
262
268
|
const restartScheduled = written && onApplied !== undefined;
|
|
263
269
|
if (restartScheduled) onApplied?.();
|
|
264
270
|
const files: string[] = [];
|
|
@@ -336,7 +342,7 @@ export function createWriteCore(appDir: string, opts: WriteCoreOptions = {}): Wr
|
|
|
336
342
|
message: 'config/integrations.json 缺席——已物化骨架(写时自动初始化;R13 语义翻转提示)',
|
|
337
343
|
});
|
|
338
344
|
}
|
|
339
|
-
// root 全量替换(D14 无例外语义):缺席根键删除(services/
|
|
345
|
+
// root 全量替换(D14 无例外语义):缺席根键删除(services/modules 域不在根键集——随 base 保留;v6 domains 键已退役),
|
|
340
346
|
// node 全键置入(未知键交严格 zod 拒——fail-visible;B10:原实现 spread+merge 残留 stale 根键)
|
|
341
347
|
const next: Record<string, unknown> = { ...base };
|
|
342
348
|
for (const k of APP_INTEGRATION_NODE_KEYS) if (!(k in node)) delete next[k];
|
|
@@ -370,37 +376,27 @@ export function createWriteCore(appDir: string, opts: WriteCoreOptions = {}): Wr
|
|
|
370
376
|
|
|
371
377
|
applyModule(moduleId: string, node: Record<string, unknown>, writeOpts: WriteOptions = {}): SaveResult {
|
|
372
378
|
assertSafeNames(node, 'root');
|
|
373
|
-
//
|
|
374
|
-
//
|
|
375
|
-
//
|
|
376
|
-
const { modules, catalog } = walkManifests(appDir, cache);
|
|
379
|
+
// v6 写守卫(moduleOwnershipWritable 单源):404(模块在册)→ 400(zero-ownership——
|
|
380
|
+
// 模块级集成位作用于 declare.owns 服务,零归属模块无配置位)→ 既有管线。
|
|
381
|
+
// 守卫先于 no-op 短路——零归属模块 PUT {} 清位同 400(「写路径拒绝歧义操作」含清除)。
|
|
382
|
+
const { modules, catalog, ownership } = walkManifests(appDir, cache);
|
|
377
383
|
const mod = modules.find((m) => m.id === moduleId);
|
|
378
384
|
if (!mod) {
|
|
379
385
|
throw new AppToolkitError('MODULE_NOT_FOUND', 404, `模块 ${moduleId} 非已装模块`);
|
|
380
386
|
}
|
|
381
|
-
const w =
|
|
387
|
+
const w = moduleOwnershipWritable({ modules, catalog, ownership }, mod);
|
|
382
388
|
if (!w.writable) {
|
|
383
|
-
if (w.reason === 'multi-domain') {
|
|
384
|
-
throw new AppToolkitError(
|
|
385
|
-
'VALIDATION_FAILED',
|
|
386
|
-
400,
|
|
387
|
-
`模块 ${moduleId} 跨 ${w.domainKeys.length} 个域(${w.domainKeys.join('、')})——模块级统一配置仅支持单域模块,请按服务级配置(编辑各服务行)`,
|
|
388
|
-
);
|
|
389
|
-
}
|
|
390
|
-
if (w.reason === 'zero-domain') {
|
|
391
|
-
throw new AppToolkitError('VALIDATION_FAILED', 400, `模块 ${moduleId} 无服务需求声明,无域配置位`);
|
|
392
|
-
}
|
|
393
389
|
throw new AppToolkitError(
|
|
394
390
|
'VALIDATION_FAILED',
|
|
395
391
|
400,
|
|
396
|
-
|
|
392
|
+
`模块 ${moduleId} 未声明任何 declare.owns——无模块级配置位(模块级 provider/config 作用于归属服务)`,
|
|
397
393
|
);
|
|
398
394
|
}
|
|
399
|
-
const domainKey = w.domainKey;
|
|
400
395
|
const issues: IntegrationServiceIssue[] = [];
|
|
401
396
|
const current = loadConfig(appDir, cache);
|
|
397
|
+
// N2 语义(v6):空节点 = 清 provider/config 两键,resources 随既有节点保全;
|
|
398
|
+
// 文件缺席 + 空节点 = no-op(幂等)
|
|
402
399
|
if (current === null && Object.keys(node).length === 0) {
|
|
403
|
-
// 空文件清空域 = no-op(N1 幂等)
|
|
404
400
|
return { written: false, restartScheduled: false, files: [], credentials: [], issues };
|
|
405
401
|
}
|
|
406
402
|
if (current === null) {
|
|
@@ -411,15 +407,39 @@ export function createWriteCore(appDir: string, opts: WriteCoreOptions = {}): Wr
|
|
|
411
407
|
});
|
|
412
408
|
}
|
|
413
409
|
const base: Record<string, unknown> = current ?? { services: {} };
|
|
414
|
-
const
|
|
415
|
-
const
|
|
416
|
-
//
|
|
417
|
-
|
|
418
|
-
|
|
410
|
+
const modulesNode = { ...((base.modules as Record<string, unknown>) ?? {}) };
|
|
411
|
+
const existing = { ...((modulesNode[moduleId] as Record<string, unknown> | undefined) ?? {}) };
|
|
412
|
+
// 键集纪律:node 内仅接受 MODULE_INTEGRATION_NODE_KEYS 成员(未知键交严格 zod 拒——fail-visible)
|
|
413
|
+
for (const k of Object.keys(node)) {
|
|
414
|
+
if (!(MODULE_INTEGRATION_NODE_KEYS as readonly string[]).includes(k)) {
|
|
415
|
+
throw new AppToolkitError(
|
|
416
|
+
'VALIDATION_FAILED',
|
|
417
|
+
400,
|
|
418
|
+
`模块集成位仅接受 ${MODULE_INTEGRATION_NODE_KEYS.join('/')} 键(收到 ${k})`,
|
|
419
|
+
[k],
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
// 全量替换(键集内):缺席键删除;空对象/空串值 = 删除(≡ 缺席——模块节点不留空键);
|
|
424
|
+
// PUT {} = 清 provider/config,resources 恒随 existing 保全(N2:清集成位不杀资源绑定)
|
|
425
|
+
const nextNode: Record<string, unknown> = { ...existing };
|
|
426
|
+
for (const k of MODULE_INTEGRATION_NODE_KEYS) {
|
|
427
|
+
const v = node[k];
|
|
428
|
+
const isEmptyish = v === undefined || v === null || v === ''
|
|
429
|
+
|| (typeof v === 'object' && !Array.isArray(v) && Object.keys(v as object).length === 0);
|
|
430
|
+
if (isEmptyish) delete nextNode[k];
|
|
431
|
+
else nextNode[k] = v;
|
|
432
|
+
}
|
|
433
|
+
if (Object.keys(nextNode).length === 0) {
|
|
434
|
+
delete modulesNode[moduleId];
|
|
435
|
+
} else {
|
|
436
|
+
modulesNode[moduleId] = nextNode;
|
|
437
|
+
}
|
|
438
|
+
const next: Record<string, unknown> = { ...base, modules: modulesNode };
|
|
419
439
|
if (deepEqual(current, next)) {
|
|
420
440
|
return { written: false, restartScheduled: false, files: [], credentials: [], issues };
|
|
421
441
|
}
|
|
422
|
-
runStrictValidation(next, issues, `
|
|
442
|
+
runStrictValidation(next, issues, `modules.${moduleId}.`);
|
|
423
443
|
issues.push(...registryCheck(next));
|
|
424
444
|
return finalize(next, [], issues, writeOpts.dryRun);
|
|
425
445
|
},
|
package/src/views/app.ts
CHANGED
|
@@ -5,21 +5,21 @@ import { moduleOccupiedServices, resolveServiceDisplay, type ViewSnapshot } from
|
|
|
5
5
|
/**
|
|
6
6
|
* 应用静态投影(views/app;C4;FX-1c B10——root 节点投影修;v4.4 D29——services 双端点同型)。
|
|
7
7
|
* /app = 平台服务 + root 节点原值(纯静态):services = 服务词汇 − 模块占用
|
|
8
|
-
* (ServiceDemand[]——平台条目 required 恒 false、
|
|
8
|
+
* (ServiceDemand[]——平台条目 required 恒 false、name/description 缺席,D29);
|
|
9
9
|
* 平台服务状态不在此(→ /service-resolutions);模块服务不在此(→ /modules/:m)。
|
|
10
10
|
* integration = **root 节点投影**(APP_INTEGRATION_NODE_KEYS——D21/L1:GET 预填 ≡ PUT body;
|
|
11
|
-
* 文件另含 services/
|
|
11
|
+
* 文件另含 services/modules 域键,不进 root 节点投影——原实现整文件透传,
|
|
12
12
|
* PUT 回会级联写回域键);文件缺席 = null,文件在场 → 投影对象(可为空)。
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
export function loadAppView(snapshot: ViewSnapshot): AppView {
|
|
16
16
|
const occupied = moduleOccupiedServices(snapshot.walk);
|
|
17
|
-
// 平台条目展示名走链(B1):词汇 meta > demand meta > id——
|
|
17
|
+
// 平台条目展示名走链(B1):词汇 meta > demand meta > id——name 恒发(id 兜底内聚)
|
|
18
18
|
const services: ServiceDemand[] = snapshot.walk.vocabulary
|
|
19
19
|
.filter((s) => !occupied.has(s))
|
|
20
20
|
.map((s) => {
|
|
21
21
|
const display = resolveServiceDisplay(snapshot.walk, s);
|
|
22
|
-
return { service: s,
|
|
22
|
+
return { service: s, name: display.name, ...(display.description !== undefined ? { description: display.description } : {}), required: false };
|
|
23
23
|
});
|
|
24
24
|
const config = snapshot.integrations;
|
|
25
25
|
let integration: AppView['integration'] = null;
|
package/src/views/context.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { readIntegrationsConfig } from '../integrations/read.js';
|
|
2
|
-
import { domainOfService } from '../integrations/domain-binding.js';
|
|
3
2
|
import { walkManifests, type WalkCatalog, type WalkManifestsResult } from '../assembly/walk-manifests.js';
|
|
4
3
|
import type { FileCache } from '../core/file-cache.js';
|
|
5
4
|
import type { ResolvedContracts } from '../core/contracts-resolver.js';
|
|
6
5
|
|
|
7
6
|
/**
|
|
8
7
|
* 视图装配共享上下文(五投影共用;目录枚举零缓存——walkManifests 每次调用执行)。
|
|
9
|
-
*
|
|
8
|
+
* v6:域前缀换算退役(domainOfService 随归属模型退场)——模块占用判定改 declare 三键直读。
|
|
10
9
|
*/
|
|
11
10
|
|
|
12
11
|
export interface ViewContext {
|
|
@@ -20,32 +19,34 @@ export interface ViewSnapshot {
|
|
|
20
19
|
catalog: WalkCatalog;
|
|
21
20
|
integrations: Record<string, unknown> | null;
|
|
22
21
|
contracts: ResolvedContracts;
|
|
22
|
+
/** 文件读原语透传(可选——resources 台账惰性读等 stat 指纹缓存消费;缺席 = 调用方直读) */
|
|
23
|
+
cache?: FileCache;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
export { domainOfService };
|
|
26
|
-
|
|
27
26
|
/** 读取视图输入快照(每方法调用独立——Freshness Contract 单位 = 方法调用) */
|
|
28
27
|
export async function loadViewSnapshot(ctx: ViewContext): Promise<ViewSnapshot> {
|
|
29
28
|
const walk = walkManifests(ctx.appDir, ctx.cache);
|
|
30
29
|
const integrations = await readIntegrationsConfig(ctx.appDir, ctx.cache);
|
|
31
30
|
const { resolveContractsForApp } = await import('../core/contracts-resolver.js');
|
|
32
31
|
const contracts = await resolveContractsForApp(ctx.appDir);
|
|
33
|
-
return { appDir: ctx.appDir, walk, catalog: walk.catalog, integrations, contracts };
|
|
32
|
+
return { appDir: ctx.appDir, walk, catalog: walk.catalog, integrations, contracts, ...(ctx.cache ? { cache: ctx.cache } : {}) };
|
|
34
33
|
}
|
|
35
34
|
|
|
36
35
|
/** 同步快照(调用方已持有 contracts 时——避免重复解析) */
|
|
37
36
|
export function loadViewSnapshotSync(ctx: ViewContext, contracts: ResolvedContracts): ViewSnapshot {
|
|
38
37
|
const walk = walkManifests(ctx.appDir, ctx.cache);
|
|
39
38
|
const integrations = readIntegrationsConfig(ctx.appDir, ctx.cache);
|
|
40
|
-
return { appDir: ctx.appDir, walk, catalog: walk.catalog, integrations, contracts };
|
|
39
|
+
return { appDir: ctx.appDir, walk, catalog: walk.catalog, integrations, contracts, ...(ctx.cache ? { cache: ctx.cache } : {}) };
|
|
41
40
|
}
|
|
42
41
|
|
|
43
|
-
/** 模块占用服务集(demand
|
|
42
|
+
/** 模块占用服务集(v6——demand ∪ supply ∪ owns:归属模型下「平台服务」= 无主服务;
|
|
43
|
+
* mall-core owns auth.alipay-login → 移出 AppView.services 平台条目,入其模块页 ownedServices) */
|
|
44
44
|
export function moduleOccupiedServices(walk: WalkManifestsResult): Set<string> {
|
|
45
45
|
const occupied = new Set<string>();
|
|
46
46
|
for (const mod of walk.modules) {
|
|
47
|
-
for (const
|
|
48
|
-
for (const
|
|
47
|
+
for (const own of mod.descriptor.declare.owns ?? []) occupied.add(own.service);
|
|
48
|
+
for (const need of mod.descriptor.declare.consumes ?? []) occupied.add(need.service);
|
|
49
|
+
for (const slot of mod.descriptor.declare.providers?.slots ?? []) occupied.add(slot.service);
|
|
49
50
|
}
|
|
50
51
|
return occupied;
|
|
51
52
|
}
|
|
@@ -66,22 +67,22 @@ export function suppliedByVendors(catalog: WalkCatalog, service: string): string
|
|
|
66
67
|
return vendors.sort();
|
|
67
68
|
}
|
|
68
69
|
|
|
69
|
-
/** 服务展示名解析结果(
|
|
70
|
+
/** 服务展示名解析结果(name 恒有值——id 兜底内聚;v7 词汇统一 title→name) */
|
|
70
71
|
export interface ServiceDisplay {
|
|
71
|
-
|
|
72
|
+
name: string;
|
|
72
73
|
description?: string;
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
/**
|
|
76
77
|
* 服务展示名单源(B1):词汇 meta(slotMeta——contracts-mall 词汇锁保证 mall 域覆盖)
|
|
77
78
|
* > demand meta(catalog.services——域外槽兜底;首声明聚合)> service id。逐字段回落——
|
|
78
|
-
* 词汇条目仅含 description 时 demand
|
|
79
|
+
* 词汇条目仅含 description 时 demand name 仍可达(外部契约包部分 meta 前向容错)。
|
|
79
80
|
* 四视图唯一消费点(/modules/:m services、/app 平台条目、/services/:s、/providers/:p services)。
|
|
80
81
|
*/
|
|
81
82
|
export function resolveServiceDisplay(walk: WalkManifestsResult, service: string): ServiceDisplay {
|
|
82
83
|
const vocab = walk.slotMeta[service];
|
|
83
84
|
const demand = walk.catalog.services[service];
|
|
84
|
-
const
|
|
85
|
+
const name = vocab?.name ?? demand?.name ?? service;
|
|
85
86
|
const description = vocab?.description ?? demand?.description;
|
|
86
|
-
return {
|
|
87
|
+
return { name, ...(description !== undefined ? { description } : {}) };
|
|
87
88
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { IntegrationFieldRegistryView, LayerIntegrationSchemas, ServiceIntegrationSchemas } from '../dto.js';
|
|
2
2
|
import { isSupplied, type ViewSnapshot } from './context.js';
|
|
3
3
|
import { resolveServiceConfigSchema } from './providers.js';
|
|
4
|
-
import {
|
|
4
|
+
import { moduleOwnershipWritable, providerFootprint, resolveModuleBinding } from '../integrations/module-binding.js';
|
|
5
5
|
import { isControlledInlineProvider } from '../integrations/predicate.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -117,6 +117,12 @@ export function buildIntegrationFieldRegistry(
|
|
|
117
117
|
key,
|
|
118
118
|
schema: mergeFieldSources(sources),
|
|
119
119
|
sources: sources.map(({ service, required }) => ({ service, required })),
|
|
120
|
+
// G 批:x-tbox-global 聚合旗标(sources.some——subschema 属性标记直读,零新增 IO;
|
|
121
|
+
// 任一来源声明即 true——并集口径与 required 聚合同族)
|
|
122
|
+
global: sources.some(
|
|
123
|
+
(s) => !!s.subschema && typeof s.subschema === 'object'
|
|
124
|
+
&& (s.subschema as Record<string, unknown>)['x-tbox-global'] === true,
|
|
125
|
+
),
|
|
120
126
|
});
|
|
121
127
|
}
|
|
122
128
|
return {
|
|
@@ -129,7 +135,7 @@ export function buildIntegrationFieldRegistry(
|
|
|
129
135
|
}
|
|
130
136
|
|
|
131
137
|
/** D37 生效厂商解析单源(实际/假想同径):override(非空串)?? 实际解析。
|
|
132
|
-
* 空串归一(I3)单点;module 轴
|
|
138
|
+
* 空串归一(I3)单点;module 轴 v6 零归属 → null(override 不豁免——预览服务于保存)。 */
|
|
133
139
|
export function formProviderOf(
|
|
134
140
|
axis: 'app' | 'module',
|
|
135
141
|
snapshot: ViewSnapshot,
|
|
@@ -144,10 +150,10 @@ export function formProviderOf(
|
|
|
144
150
|
}
|
|
145
151
|
const mod = snapshot.walk.modules.find((m) => m.id === moduleId);
|
|
146
152
|
if (!mod) return null; // 视图已先行 404,此处防御
|
|
147
|
-
const w =
|
|
148
|
-
if (!w.writable) return null; //
|
|
153
|
+
const w = moduleOwnershipWritable(snapshot.walk, mod);
|
|
154
|
+
if (!w.writable) return null; // v6 零归属不豁免
|
|
149
155
|
if (ovr !== undefined) return ovr;
|
|
150
|
-
return
|
|
156
|
+
return resolveModuleBinding(snapshot.integrations, mod.id).provider;
|
|
151
157
|
}
|
|
152
158
|
|
|
153
159
|
/** D36/D37 表单层双投影(I1:同源同口径一次计算——双取单算 by construction) */
|
|
@@ -159,29 +165,39 @@ export interface LayerFormViews {
|
|
|
159
165
|
/** 防御双 null(module 不在册/不可写/未绑定、app 未绑定——F3) */
|
|
160
166
|
const NULL_FORM_VIEWS: LayerFormViews = { integrationSchemas: null, integrationFieldRegistry: null };
|
|
161
167
|
|
|
168
|
+
/** G 批表单收窄(消费投影单点):app/module 轴 registry.fields 只留 global===true——
|
|
169
|
+
* 未声明键落 ExtraFields 语义(P16 复合可见面:layer.properties 仍全量,字段级代数不受扰)。
|
|
170
|
+
* schemaless/unsupplied/additionalPropertiesAllowed 描述服务/Schema 面,不随字段过滤。 */
|
|
171
|
+
function narrowToGlobal(registry: IntegrationFieldRegistryView): IntegrationFieldRegistryView {
|
|
172
|
+
return { ...registry, fields: registry.fields.filter((f) => f.global) };
|
|
173
|
+
}
|
|
174
|
+
|
|
162
175
|
/** app 轴装配:scope = 供给足迹全量(≠ AppView.services——root.config 流向包括模块服务在内的所有服务);
|
|
163
176
|
* unsupplied 恒 [](结构性:足迹 ⊆ 供给)。未知厂商/零足迹 → 非 null 空集(镜像 D32 mock 语义)。 */
|
|
164
177
|
export function loadAppFormViews(snapshot: ViewSnapshot, appProvider?: string): LayerFormViews {
|
|
165
178
|
const provider = formProviderOf('app', snapshot, undefined, appProvider);
|
|
166
179
|
if (provider === null) return NULL_FORM_VIEWS;
|
|
167
180
|
const layer = resolveLayerIntegrationSchemas(snapshot, provider, providerFootprint(snapshot.catalog, provider));
|
|
168
|
-
return { integrationSchemas: layer, integrationFieldRegistry: buildIntegrationFieldRegistry(layer) };
|
|
181
|
+
return { integrationSchemas: layer, integrationFieldRegistry: narrowToGlobal(buildIntegrationFieldRegistry(layer)) };
|
|
169
182
|
}
|
|
170
183
|
|
|
171
|
-
/** module 轴装配:仅 writable
|
|
172
|
-
*
|
|
184
|
+
/** module 轴装配:仅 writable(v6 owns>0);scope = ownedServices ∩ 该厂商供给(v6——模块
|
|
185
|
+
* config 流向归属服务);unsupplied 同点派生(ownedServices − 供给,Set 去重保序——
|
|
186
|
+
* declare.owns 不去重防御)。 */
|
|
173
187
|
export function loadModuleFormViews(snapshot: ViewSnapshot, moduleId: string, moduleProvider?: string): LayerFormViews {
|
|
174
188
|
const provider = formProviderOf('module', snapshot, moduleId, moduleProvider);
|
|
175
189
|
const mod = snapshot.walk.modules.find((m) => m.id === moduleId);
|
|
176
190
|
if (provider === null || !mod) return NULL_FORM_VIEWS;
|
|
177
|
-
const
|
|
191
|
+
const owned = (mod.descriptor.declare.owns ?? []).map((o) => o.service);
|
|
178
192
|
const supplied = (s: string): boolean => isSupplied(snapshot.catalog, provider, s);
|
|
179
|
-
const layer = resolveLayerIntegrationSchemas(snapshot, provider,
|
|
193
|
+
const layer = resolveLayerIntegrationSchemas(snapshot, provider, owned.filter(supplied));
|
|
180
194
|
return {
|
|
181
195
|
integrationSchemas: layer,
|
|
182
|
-
integrationFieldRegistry:
|
|
183
|
-
|
|
184
|
-
|
|
196
|
+
integrationFieldRegistry: narrowToGlobal(
|
|
197
|
+
buildIntegrationFieldRegistry(layer, {
|
|
198
|
+
unsupplied: [...new Set(owned.filter((s) => !supplied(s)))],
|
|
199
|
+
}),
|
|
200
|
+
),
|
|
185
201
|
};
|
|
186
202
|
}
|
|
187
203
|
|
|
@@ -195,25 +211,47 @@ export function loadModuleIntegrationSchemas(snapshot: ViewSnapshot, moduleId: s
|
|
|
195
211
|
return loadModuleFormViews(snapshot, moduleId).integrationSchemas;
|
|
196
212
|
}
|
|
197
213
|
|
|
214
|
+
/** 服务级生效绑定(v6 级联):显式 ?? modules[ownerOf[s]].provider ?? root.provider。
|
|
215
|
+
* 服务无主 → 仅 root(平台槽语义)。 */
|
|
216
|
+
function serviceLevelBindingProvider(snapshot: ViewSnapshot, service: string): string | null {
|
|
217
|
+
const owner = snapshot.walk.ownership[service];
|
|
218
|
+
if (owner !== undefined) {
|
|
219
|
+
return resolveModuleBinding(snapshot.integrations, owner).provider;
|
|
220
|
+
}
|
|
221
|
+
const p = (snapshot.integrations as { provider?: unknown } | null)?.provider;
|
|
222
|
+
return typeof p === 'string' && p.length > 0 ? p : null;
|
|
223
|
+
}
|
|
224
|
+
|
|
198
225
|
/** 服务级 loader(v4.7 D38):四分支穷举(node.provider = 文件 services[s].provider 逐字原值;
|
|
199
226
|
* 实例层无条件排除——node 级口径);非空三分支归约一次复合解析。
|
|
200
|
-
* ① undefined/'' →
|
|
227
|
+
* ① undefined/'' → 模块级联(v6:modules[ownerOf[s]] → root 双缺 → 整字段 null)
|
|
201
228
|
* ② 非空 string → bare:catalog miss(未知厂商/未供给)→ configSchema:null 不 404(富化非门控)
|
|
202
229
|
* ③ 受控内联(isControlledInlineProvider 单源)→ 名 = p.provider;catalog 复合键 > p.configSchema ref
|
|
203
230
|
* (H9 逃逸 400 照抛——readInlineSchema 共享路径)
|
|
204
231
|
* ④ 其余(null/数组/原始值/坏对象)→ 整字段 null(≡ 谓词 #5 INLINE_PROVIDER_INVALID 同判——
|
|
205
|
-
* 不放行域级联,防错误 schema 污染坏节点)
|
|
232
|
+
* 不放行域级联,防错误 schema 污染坏节点)
|
|
233
|
+
* v5.1 D39:serviceProvider = 服务轴向假想绑定(缺席/空串 ≡ 按实际解析——I3 归一单点在此头部,
|
|
234
|
+
* registry 侧不重复归一);非空轴参 → 短路 bare 解析(镜像 formProviderOf「输入替换非分支复制」先例:
|
|
235
|
+
* 任一原分支改写 provider 为 X 后实读都进 bare 分支,同构承诺 by construction)。短路在节点读取之前——
|
|
236
|
+
* 豁免链三分支(D39 与 D38 四分支的有意分歧:预览与坏文件解耦):malformed(分支④)不污染假想 /
|
|
237
|
+
* 内联 ref 不读取(假想态无 H9 逃逸 400;catalog 命中的 manifest 声明路径照读,与节点声明的
|
|
238
|
+
* 用户可控路径不同性)/ 全链未绑定时假想仍非 null(服务轴恒可写,无 module 轴零归属对应物)。 */
|
|
206
239
|
export function loadServiceIntegrationSchemas(
|
|
207
240
|
snapshot: ViewSnapshot,
|
|
208
241
|
service: string,
|
|
242
|
+
serviceProvider?: string,
|
|
209
243
|
): ServiceIntegrationSchemas | null {
|
|
244
|
+
const ovr = typeof serviceProvider === 'string' && serviceProvider.length > 0 ? serviceProvider : undefined;
|
|
245
|
+
if (ovr !== undefined) {
|
|
246
|
+
return { provider: ovr, configSchema: resolveServiceConfigSchema(snapshot, ovr, service) };
|
|
247
|
+
}
|
|
210
248
|
const p = (snapshot.integrations as { services?: Record<string, { provider?: unknown }> } | null)
|
|
211
249
|
?.services?.[service]?.provider;
|
|
212
250
|
if (p === undefined || p === '') {
|
|
213
|
-
const
|
|
214
|
-
return
|
|
251
|
+
const provider = serviceLevelBindingProvider(snapshot, service);
|
|
252
|
+
return provider === null
|
|
215
253
|
? null
|
|
216
|
-
: { provider
|
|
254
|
+
: { provider, configSchema: resolveServiceConfigSchema(snapshot, provider, service) };
|
|
217
255
|
}
|
|
218
256
|
if (typeof p === 'string') {
|
|
219
257
|
return { provider: p, configSchema: resolveServiceConfigSchema(snapshot, p, service) };
|
|
@@ -224,3 +262,23 @@ export function loadServiceIntegrationSchemas(
|
|
|
224
262
|
}
|
|
225
263
|
return null;
|
|
226
264
|
}
|
|
265
|
+
|
|
266
|
+
/** G 批:服务轴字段注册表(D38 四分支链复用——消费 loadServiceIntegrationSchemas 单源;
|
|
267
|
+
* 伪层 {provider, services: {[s]: {configSchema}}} → buildIntegrationFieldRegistry——
|
|
268
|
+
* fields = 该 schema 全部字段带 global 旗标;configSchema null → schemaless=[service]
|
|
269
|
+
* 非 null 空集;schemas 整体 null(全链未绑定/坏节点)→ registry null——镜像 D38 语义)。
|
|
270
|
+
* v5.1 D39:serviceProvider = 服务轴向假想绑定,纯透传(I1 单源——归一与短路都在链函数头部;
|
|
271
|
+
* 轴参仅重定向表单层投影,global 旗标全量透传语义不变)。 */
|
|
272
|
+
export function loadServiceFieldRegistry(
|
|
273
|
+
snapshot: ViewSnapshot,
|
|
274
|
+
service: string,
|
|
275
|
+
serviceProvider?: string,
|
|
276
|
+
): IntegrationFieldRegistryView | null {
|
|
277
|
+
const schemas = loadServiceIntegrationSchemas(snapshot, service, serviceProvider);
|
|
278
|
+
if (schemas === null) return null;
|
|
279
|
+
const layer: LayerIntegrationSchemas = {
|
|
280
|
+
provider: schemas.provider,
|
|
281
|
+
services: { [service]: { configSchema: schemas.configSchema } },
|
|
282
|
+
};
|
|
283
|
+
return buildIntegrationFieldRegistry(layer);
|
|
284
|
+
}
|
package/src/views/modules.ts
CHANGED
|
@@ -1,18 +1,71 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import type { ModuleDeclaration, ModuleBinding, ModuleDetailView, ModuleResourceEcho, ModuleResourcesEcho, ServiceDemand } from '../dto.js';
|
|
2
4
|
import { AppToolkitError } from '../core/errors.js';
|
|
3
|
-
import {
|
|
5
|
+
import { moduleOwnershipWritable, resolveModuleBinding } from '../integrations/module-binding.js';
|
|
4
6
|
import { resolveServiceDisplay, type ViewSnapshot } from './context.js';
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* 需求轴/模块静态投影(views/modules;C4)。静态端点结构性零求值(铁律 #3——
|
|
8
10
|
* 无 resolution/issues/contracts 键,永不 503)。
|
|
9
|
-
*
|
|
10
|
-
* (
|
|
11
|
-
*
|
|
11
|
+
* v6 所有权模型:ownedServices(declare.owns 展平)/ binding 单数(resolveModuleBinding)/
|
|
12
|
+
* configEditable = owns>0(moduleOwnershipWritable 单源);integration = modules[install id]
|
|
13
|
+
* 集成位投影(provider/config;resources 走独立只读字段,见投影 resourcesOf——N2 读半边)。
|
|
14
|
+
* v5 domainKeys/providerBindings 退役。
|
|
12
15
|
* 2026-09-16 双缺陷批:loadModulesView 排除 provider 家族包(供给/实施载体非可配置
|
|
13
16
|
* 业务模块)——walk 声明集与 loadModuleDetailView 不变(装包仍合法,详情保持 200)。
|
|
14
17
|
*/
|
|
15
18
|
|
|
19
|
+
/** knowledge.json 台账路径(与 toolkit readIntegrationsConfig 同源锚定 appDir/config/——
|
|
20
|
+
* TBOX_INTEGRATIONS_FILE 为 SDK 运行时逃生舱,视图不跟随,既有分叉见 dto ModuleResourceEcho 注) */
|
|
21
|
+
const KNOWLEDGE_REGISTRY_FILE = join('config', 'knowledge.json');
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* modules[install id].resources 只读投影(N2 读半边;文件镜像 L1 + ref 台账展开态):
|
|
25
|
+
* - 无模块键/无资源节 → null(DTO null 语义:无资源 ≠ 空表);
|
|
26
|
+
* - `datasetId` 直填 → resolvedDatasetId = 该值(镜像 SDK resolveKnowledge:并存时 datasetId 优先);
|
|
27
|
+
* - `ref` → 同目录 knowledge.json 台账命中 = 台账 datasetId;未命中/台账缺失/解析失败 = null
|
|
28
|
+
* (镜像 SDK resolveKnowledge miss-warn 降级语义,资源缺席走模块占位回退);
|
|
29
|
+
* - 非 knowledge 型资源(未来 additive)原样透传 type,resolvedDatasetId = null。
|
|
30
|
+
*/
|
|
31
|
+
function resourcesOf(snapshot: ViewSnapshot, moduleId: string, modulesNode: Record<string, unknown>): ModuleResourcesEcho | null {
|
|
32
|
+
const raw = modulesNode[moduleId] as { resources?: Record<string, unknown> } | undefined;
|
|
33
|
+
const resources = raw?.resources;
|
|
34
|
+
if (!resources || typeof resources !== 'object' || Array.isArray(resources)) return null;
|
|
35
|
+
const registryPath = join(snapshot.appDir, KNOWLEDGE_REGISTRY_FILE);
|
|
36
|
+
// 台账惰性读(stat 指纹缓存;缺席/坏文件 = 空表——展开态 null,与 SDK loadKnowledgeRegistry fail-soft 同语义)
|
|
37
|
+
let bases: Record<string, { datasetId?: unknown }> = {};
|
|
38
|
+
if (existsSync(registryPath)) {
|
|
39
|
+
try {
|
|
40
|
+
const parsed = (snapshot.cache?.read(registryPath, (raw: string) => JSON.parse(raw) as unknown) ?? JSON.parse(readFileSync(registryPath, 'utf8'))) as {
|
|
41
|
+
knowledge_bases?: Record<string, { datasetId?: unknown }>;
|
|
42
|
+
};
|
|
43
|
+
bases = parsed.knowledge_bases ?? {};
|
|
44
|
+
} catch {
|
|
45
|
+
bases = {};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const out: ModuleResourcesEcho = {};
|
|
49
|
+
for (const [id, spec] of Object.entries(resources)) {
|
|
50
|
+
if (!spec || typeof spec !== 'object' || Array.isArray(spec)) continue;
|
|
51
|
+
const s = spec as Record<string, unknown>;
|
|
52
|
+
const type = s.type === 'knowledge' ? 'knowledge' : String(s.type ?? 'knowledge');
|
|
53
|
+
const direct = typeof s.datasetId === 'string' && s.datasetId !== '' ? s.datasetId : undefined;
|
|
54
|
+
const ref = typeof s.ref === 'string' && s.ref !== '' ? s.ref : undefined;
|
|
55
|
+
const resolved = direct ?? (ref !== undefined && typeof bases[ref]?.datasetId === 'string' ? (bases[ref].datasetId as string) : null);
|
|
56
|
+
const echo: ModuleResourceEcho = {
|
|
57
|
+
type: type as ModuleResourceEcho['type'],
|
|
58
|
+
...(direct !== undefined ? { datasetId: direct } : {}),
|
|
59
|
+
...(ref !== undefined ? { ref } : {}),
|
|
60
|
+
resolvedDatasetId: resolved,
|
|
61
|
+
...(typeof s.topK === 'number' ? { topK: s.topK } : {}),
|
|
62
|
+
...(typeof s.scoreThreshold === 'number' ? { scoreThreshold: s.scoreThreshold } : {}),
|
|
63
|
+
};
|
|
64
|
+
out[id] = echo;
|
|
65
|
+
}
|
|
66
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
16
69
|
/** 家族名归一(cli deriveModuleId 同源语义——依赖方向禁 import,故内联):
|
|
17
70
|
* `@tbox.cn/app-provider-x` / `@app/provider-y` / `provider-z` → `provider-*` */
|
|
18
71
|
function familyName(s: string): string {
|
|
@@ -27,52 +80,67 @@ function isProviderFamily(mod: { id: string; pkg: string }): boolean {
|
|
|
27
80
|
return familyName(mod.id).startsWith('provider-') || familyName(mod.pkg).startsWith('provider-');
|
|
28
81
|
}
|
|
29
82
|
|
|
30
|
-
/** GET /modules:纯模块清单(demand 不在——卡片展开 = GET /modules/:m;provider
|
|
31
|
-
|
|
83
|
+
/** GET /modules:纯模块清单(demand 不在——卡片展开 = GET /modules/:m;provider 家族包不在清单;
|
|
84
|
+
* hidden 模块默认过滤——includeHidden = true 可见并携带 hidden: true 徽标) */
|
|
85
|
+
export function loadModulesView(
|
|
86
|
+
snapshot: ViewSnapshot,
|
|
87
|
+
opts: { includeHidden?: boolean } = {},
|
|
88
|
+
): { modules: ModuleDeclaration[] } {
|
|
32
89
|
const modules: ModuleDeclaration[] = snapshot.walk.modules
|
|
33
90
|
.filter((mod) => !isProviderFamily(mod))
|
|
91
|
+
.filter((mod) => opts.includeHidden === true || mod.descriptor.hidden !== true)
|
|
34
92
|
.map((mod) => ({
|
|
35
93
|
id: mod.id,
|
|
36
|
-
name
|
|
37
|
-
|
|
38
|
-
title: mod.descriptor.title ?? mod.descriptor.name,
|
|
94
|
+
// v7 词汇统一:name = 展示名(manifest name;兜底 id——恒有值)
|
|
95
|
+
name: mod.descriptor.name ?? mod.descriptor.id,
|
|
39
96
|
description: mod.descriptor.description ?? '',
|
|
97
|
+
...(mod.descriptor.hidden === true ? { hidden: true } : {}),
|
|
40
98
|
}))
|
|
41
99
|
.sort((a, b) => a.id.localeCompare(b.id));
|
|
42
100
|
return { modules };
|
|
43
101
|
}
|
|
44
102
|
|
|
45
|
-
/** GET /modules/:module:模块静态一站式(
|
|
103
|
+
/** GET /modules/:module:模块静态一站式(modules[install id] 集成位原值 + demand;PUT 读对偶) */
|
|
46
104
|
export function loadModuleDetailView(snapshot: ViewSnapshot, moduleId: string): ModuleDetailView {
|
|
47
105
|
const mod = snapshot.walk.modules.find((m) => m.id === moduleId);
|
|
48
106
|
if (!mod) {
|
|
49
107
|
throw new AppToolkitError('MODULE_NOT_FOUND', 404, `模块 ${moduleId} 非已装模块`);
|
|
50
108
|
}
|
|
51
|
-
const services: ServiceDemand[] = (mod.descriptor.
|
|
109
|
+
const services: ServiceDemand[] = (mod.descriptor.declare.consumes ?? []).map((need) => {
|
|
52
110
|
// 展示名单源走链(B1):词汇 meta > demand meta > id——词汇主源防 manifest demand 漂移
|
|
53
111
|
const display = resolveServiceDisplay(snapshot.walk, need.service);
|
|
54
112
|
return {
|
|
55
113
|
service: need.service,
|
|
56
|
-
|
|
114
|
+
name: display.name,
|
|
57
115
|
...(display.description !== undefined ? { description: display.description } : {}),
|
|
58
116
|
required: need.optional === false,
|
|
59
117
|
};
|
|
60
118
|
});
|
|
61
|
-
const w =
|
|
62
|
-
const
|
|
63
|
-
// integration =
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
119
|
+
const w = moduleOwnershipWritable(snapshot.walk, mod);
|
|
120
|
+
const modulesNode = (snapshot.integrations as { modules?: Record<string, unknown> } | null)?.modules ?? {};
|
|
121
|
+
// integration = modules[install id] 集成位投影(D21 编辑预填源;provider/config 两键——
|
|
122
|
+
// resources 不投影不写,N2);未配置 = null
|
|
123
|
+
const rawNode = modulesNode[moduleId] as Record<string, unknown> | undefined;
|
|
124
|
+
const integration: ModuleDetailView['integration'] = rawNode
|
|
125
|
+
? {
|
|
126
|
+
...(typeof rawNode.provider === 'string' ? { provider: rawNode.provider } : {}),
|
|
127
|
+
...(rawNode.config && typeof rawNode.config === 'object' && !Array.isArray(rawNode.config)
|
|
128
|
+
? { config: rawNode.config as Record<string, unknown> }
|
|
129
|
+
: {}),
|
|
130
|
+
}
|
|
131
|
+
: null;
|
|
132
|
+
const binding: ModuleBinding = resolveModuleBinding(snapshot.integrations, moduleId);
|
|
69
133
|
return {
|
|
70
|
-
//
|
|
71
|
-
|
|
134
|
+
// v7 词汇统一:name = 展示名(兜底与 /modules 同语义——manifest name ?? id)
|
|
135
|
+
name: mod.descriptor.name ?? mod.descriptor.id,
|
|
72
136
|
description: mod.descriptor.description ?? '',
|
|
73
|
-
|
|
137
|
+
...(mod.descriptor.hidden === true ? { hidden: true } : {}),
|
|
138
|
+
// v6:ownedServices(declare.owns 展平排序)——N13:展示名走 resolveServiceDisplay 链,不重复携带
|
|
139
|
+
ownedServices: w.ownedServices,
|
|
74
140
|
integration,
|
|
75
|
-
|
|
141
|
+
// 模块资源只读镜像(N2 读半边——knowledge.json 台账 ref 展开态随行)
|
|
142
|
+
resources: resourcesOf(snapshot, moduleId, modulesNode),
|
|
143
|
+
binding: { provider: binding.provider, layer: binding.layer },
|
|
76
144
|
configEditable: w.writable,
|
|
77
145
|
services,
|
|
78
146
|
};
|
package/src/views/providers.ts
CHANGED
|
@@ -22,7 +22,7 @@ export function loadProvidersView(snapshot: ViewSnapshot): ProvidersView {
|
|
|
22
22
|
local: entries.every((e) => e.local === true),
|
|
23
23
|
owner: entries[0].owner,
|
|
24
24
|
// 厂商展示元数据(B1 walk 层传播——首条目 = 首声明,与 owner 同语义)
|
|
25
|
-
...(entries[0].
|
|
25
|
+
...(entries[0].name ? { name: entries[0].name } : {}),
|
|
26
26
|
...(entries[0].description ? { description: entries[0].description } : {}),
|
|
27
27
|
});
|
|
28
28
|
}
|
|
@@ -43,10 +43,10 @@ export function loadProviderDetailView(snapshot: ViewSnapshot, provider: string,
|
|
|
43
43
|
bucket.implementations.push({ implementation: entry.implementation, credentialType: entry.credentialType });
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
|
-
// 服务展示名走链(B1):词汇 meta > demand meta > id(
|
|
46
|
+
// 服务展示名走链(B1):词汇 meta > demand meta > id(name 恒发)
|
|
47
47
|
for (const [service, bucket] of Object.entries(services)) {
|
|
48
48
|
const display = resolveServiceDisplay(snapshot.walk, service);
|
|
49
|
-
bucket.
|
|
49
|
+
bucket.name = display.name;
|
|
50
50
|
if (display.description !== undefined) bucket.description = display.description;
|
|
51
51
|
}
|
|
52
52
|
if (include?.has('integrationSchemas')) {
|
|
@@ -59,8 +59,8 @@ export function loadProviderDetailView(snapshot: ViewSnapshot, provider: string,
|
|
|
59
59
|
provider,
|
|
60
60
|
local: entries.every((e) => e.local === true),
|
|
61
61
|
owner: entries[0].owner,
|
|
62
|
-
// 厂商展示元数据(B1 walk
|
|
63
|
-
...(entries[0].
|
|
62
|
+
// 厂商展示元数据(B1 walk 层传播——首条目透传;v7 title→name)
|
|
63
|
+
...(entries[0].name ? { name: entries[0].name } : {}),
|
|
64
64
|
...(entries[0].description ? { description: entries[0].description } : {}),
|
|
65
65
|
services,
|
|
66
66
|
};
|
|
@@ -21,7 +21,7 @@ export function loadServiceDetailView(snapshot: ViewSnapshot, service: string):
|
|
|
21
21
|
const display = resolveServiceDisplay(snapshot.walk, service);
|
|
22
22
|
return {
|
|
23
23
|
service,
|
|
24
|
-
|
|
24
|
+
name: display.name,
|
|
25
25
|
...(display.description !== undefined ? { description: display.description } : {}),
|
|
26
26
|
required: demand ? !demand.optional : false,
|
|
27
27
|
// defaults 唯一落位(manifest 预填)
|