@tbox.cn/app-toolkit 0.2.0 → 0.4.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/index.d.ts +181 -33
- package/dist/index.js +8 -7
- package/package.json +2 -2
- package/src/assembly/app-manifest.ts +6 -0
- package/src/assembly/walk-manifests.ts +95 -28
- package/src/core/env-expansion.ts +14 -12
- package/src/core/module-schema.ts +3 -0
- package/src/dto.ts +59 -4
- package/src/factory.ts +58 -16
- package/src/index.ts +8 -2
- package/src/integrations/credentials.ts +1 -1
- package/src/integrations/evaluate-app.ts +41 -0
- package/src/integrations/predicate.ts +2 -1
- package/src/integrations/prune-bindings.ts +186 -0
- package/src/integrations/write.ts +15 -11
- package/src/views/app.ts +6 -2
- package/src/views/context.ts +20 -0
- package/src/views/integration-schemas.ts +197 -24
- package/src/views/modules.ts +42 -13
- package/src/views/providers.ts +39 -21
- package/src/views/service-detail.ts +5 -3
- package/tests/demo-app.ts +27 -3
- package/tests/dev-manifest-catalog.test.ts +81 -1
- package/tests/file-cache.test.ts +3 -1
- package/tests/module-schema.test.ts +15 -0
- package/tests/vendor-schema-keywords.test.ts +12 -0
- package/tests/views.test.ts +1098 -74
- package/tests/write-core.test.ts +26 -4
|
@@ -2,6 +2,7 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
2
2
|
import { join, resolve } from 'node:path';
|
|
3
3
|
import Ajv from 'ajv';
|
|
4
4
|
import { resolveContractsForApp } from '../core/contracts-resolver.js';
|
|
5
|
+
import { stemToFilePath } from '../core/credential-format.js';
|
|
5
6
|
import type { FileCache } from '../core/file-cache.js';
|
|
6
7
|
import { writeAtomic } from '../core/fskit.js';
|
|
7
8
|
import { SAFE_NAME_PATTERN } from '../core/module-schema.js';
|
|
@@ -36,8 +37,9 @@ import { AppToolkitError } from '../core/errors.js';
|
|
|
36
37
|
* 7 严格整文档校验(contracts parseIntegrationsConfig——触发 = !noDiff ∥ 有凭据输入)
|
|
37
38
|
* 8 凭据工件落盘(**后置于校验通过**——防「凭据已写 + zod 拒绝」不一致态;
|
|
38
39
|
* dryRun → writeCredentialArtifacts 零写盘 + existsSync 预览)
|
|
39
|
-
* 9 finalize
|
|
40
|
-
* restartScheduled = written && onApplied(凭据影响 boot
|
|
40
|
+
* 9 finalize(D35):planned = 将触碰(files 预览单源——dryRun 响应 ≡ 同 body 真实写,files 逐字对称);
|
|
41
|
+
* written = 实写(!dryRun 派生,D-B2);restartScheduled = written && onApplied(凭据影响 boot 装配——
|
|
42
|
+
* 重启正当);写后缓存失效
|
|
41
43
|
* 归一化:N1 模块节点 {} → 删 domains[键];N3 服务节点 {} 不归一化(显式通配激活 = 合法 TIER0 态);
|
|
42
44
|
* N2 已随 ?instance= 退役。dryRun(PUT only):复读完整管线不落盘不排程。
|
|
43
45
|
* DELETE:节点移除 + 孤儿凭据写后对账(覆盖 DELETE 与 PUT 替换两类来源,只报告不清理——D9)。
|
|
@@ -245,11 +247,13 @@ export function createWriteCore(appDir: string, opts: WriteCoreOptions = {}): Wr
|
|
|
245
247
|
appliedCredentials: AppliedCredential[],
|
|
246
248
|
issues: IntegrationServiceIssue[],
|
|
247
249
|
dryRun: boolean | undefined,
|
|
248
|
-
credentialsWritten: boolean,
|
|
249
250
|
): SaveResult {
|
|
250
|
-
//
|
|
251
|
-
// restartScheduled 跟随 written(凭据文件影响 boot 装配——重启正当)
|
|
252
|
-
const
|
|
251
|
+
// D35 双轴派生:planned = 将触碰(files 预览单源——dryRun 响应 ≡ 同 body 真实写,files 逐字对称);
|
|
252
|
+
// written = 实际落盘(!dryRun 派生——D-B2);restartScheduled 跟随 written(凭据文件影响 boot 装配——重启正当)
|
|
253
|
+
const integrationsPlanned = next !== null;
|
|
254
|
+
const credentialsPlanned = appliedCredentials.length > 0;
|
|
255
|
+
const integrationsWritten = !dryRun && integrationsPlanned;
|
|
256
|
+
const credentialsWritten = !dryRun && credentialsPlanned;
|
|
253
257
|
if (integrationsWritten) {
|
|
254
258
|
writeAtomic(join(appDir, INTEGRATIONS_FILE), `${JSON.stringify(next, null, 2)}\n`);
|
|
255
259
|
cache?.invalidate([join(appDir, INTEGRATIONS_FILE)]);
|
|
@@ -258,8 +262,8 @@ export function createWriteCore(appDir: string, opts: WriteCoreOptions = {}): Wr
|
|
|
258
262
|
const restartScheduled = written && onApplied !== undefined;
|
|
259
263
|
if (restartScheduled) onApplied?.();
|
|
260
264
|
const files: string[] = [];
|
|
261
|
-
if (
|
|
262
|
-
if (
|
|
265
|
+
if (integrationsPlanned) files.push(INTEGRATIONS_FILE);
|
|
266
|
+
if (credentialsPlanned) files.push(...appliedCredentials.map((a) => stemToFilePath(a.stem)));
|
|
263
267
|
return {
|
|
264
268
|
written,
|
|
265
269
|
restartScheduled,
|
|
@@ -361,7 +365,7 @@ export function createWriteCore(appDir: string, opts: WriteCoreOptions = {}): Wr
|
|
|
361
365
|
const dryRun = writeOpts.dryRun === true;
|
|
362
366
|
const applied = writeCredentialArtifacts(appDir, plan, dryRun);
|
|
363
367
|
issues.push(...registryCheck(next));
|
|
364
|
-
return finalize(noDiff ? null : next, applied, issues, writeOpts.dryRun
|
|
368
|
+
return finalize(noDiff ? null : next, applied, issues, writeOpts.dryRun);
|
|
365
369
|
},
|
|
366
370
|
|
|
367
371
|
applyModule(moduleId: string, node: Record<string, unknown>, writeOpts: WriteOptions = {}): SaveResult {
|
|
@@ -417,7 +421,7 @@ export function createWriteCore(appDir: string, opts: WriteCoreOptions = {}): Wr
|
|
|
417
421
|
}
|
|
418
422
|
runStrictValidation(next, issues, `domains.${domainKey}.`);
|
|
419
423
|
issues.push(...registryCheck(next));
|
|
420
|
-
return finalize(next, [], issues, writeOpts.dryRun
|
|
424
|
+
return finalize(next, [], issues, writeOpts.dryRun);
|
|
421
425
|
},
|
|
422
426
|
|
|
423
427
|
applyService(service: string, node: Record<string, unknown>, writeOpts: WriteOptions = {}): SaveResult {
|
|
@@ -474,7 +478,7 @@ export function createWriteCore(appDir: string, opts: WriteCoreOptions = {}): Wr
|
|
|
474
478
|
const dryRun = writeOpts.dryRun === true;
|
|
475
479
|
const applied = writeCredentialArtifacts(appDir, plan, dryRun);
|
|
476
480
|
issues.push(...registryCheck(next));
|
|
477
|
-
return finalize(noDiff ? null : next, applied, issues, writeOpts.dryRun
|
|
481
|
+
return finalize(noDiff ? null : next, applied, issues, writeOpts.dryRun);
|
|
478
482
|
},
|
|
479
483
|
|
|
480
484
|
deleteService(service: string): SaveResult {
|
package/src/views/app.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AppView, ServiceDemand } from '../dto.js';
|
|
2
2
|
import { APP_INTEGRATION_NODE_KEYS } from '../dto.js';
|
|
3
|
-
import { moduleOccupiedServices, type ViewSnapshot } from './context.js';
|
|
3
|
+
import { moduleOccupiedServices, resolveServiceDisplay, type ViewSnapshot } from './context.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* 应用静态投影(views/app;C4;FX-1c B10——root 节点投影修;v4.4 D29——services 双端点同型)。
|
|
@@ -14,9 +14,13 @@ import { moduleOccupiedServices, type ViewSnapshot } from './context.js';
|
|
|
14
14
|
|
|
15
15
|
export function loadAppView(snapshot: ViewSnapshot): AppView {
|
|
16
16
|
const occupied = moduleOccupiedServices(snapshot.walk);
|
|
17
|
+
// 平台条目展示名走链(B1):词汇 meta > demand meta > id——title 恒发(id 兜底内聚)
|
|
17
18
|
const services: ServiceDemand[] = snapshot.walk.vocabulary
|
|
18
19
|
.filter((s) => !occupied.has(s))
|
|
19
|
-
.map((s) =>
|
|
20
|
+
.map((s) => {
|
|
21
|
+
const display = resolveServiceDisplay(snapshot.walk, s);
|
|
22
|
+
return { service: s, title: display.title, ...(display.description !== undefined ? { description: display.description } : {}), required: false };
|
|
23
|
+
});
|
|
20
24
|
const config = snapshot.integrations;
|
|
21
25
|
let integration: AppView['integration'] = null;
|
|
22
26
|
if (config) {
|
package/src/views/context.ts
CHANGED
|
@@ -65,3 +65,23 @@ export function suppliedByVendors(catalog: WalkCatalog, service: string): string
|
|
|
65
65
|
}
|
|
66
66
|
return vendors.sort();
|
|
67
67
|
}
|
|
68
|
+
|
|
69
|
+
/** 服务展示名解析结果(title 恒有值——id 兜底内聚) */
|
|
70
|
+
export interface ServiceDisplay {
|
|
71
|
+
title: string;
|
|
72
|
+
description?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 服务展示名单源(B1):词汇 meta(slotMeta——contracts-mall 词汇锁保证 mall 域覆盖)
|
|
77
|
+
* > demand meta(catalog.services——域外槽兜底;首声明聚合)> service id。逐字段回落——
|
|
78
|
+
* 词汇条目仅含 description 时 demand title 仍可达(外部契约包部分 meta 前向容错)。
|
|
79
|
+
* 四视图唯一消费点(/modules/:m services、/app 平台条目、/services/:s、/providers/:p services)。
|
|
80
|
+
*/
|
|
81
|
+
export function resolveServiceDisplay(walk: WalkManifestsResult, service: string): ServiceDisplay {
|
|
82
|
+
const vocab = walk.slotMeta[service];
|
|
83
|
+
const demand = walk.catalog.services[service];
|
|
84
|
+
const title = vocab?.title ?? demand?.title ?? service;
|
|
85
|
+
const description = vocab?.description ?? demand?.description;
|
|
86
|
+
return { title, ...(description !== undefined ? { description } : {}) };
|
|
87
|
+
}
|
|
@@ -1,21 +1,32 @@
|
|
|
1
|
-
import type { LayerIntegrationSchemas } from '../dto.js';
|
|
1
|
+
import type { IntegrationFieldRegistryView, LayerIntegrationSchemas, ServiceIntegrationSchemas } from '../dto.js';
|
|
2
2
|
import { isSupplied, type ViewSnapshot } from './context.js';
|
|
3
|
-
import {
|
|
4
|
-
import { moduleDomainWritable, providerFootprint, resolveDomainBinding } from '../integrations/domain-binding.js';
|
|
3
|
+
import { resolveServiceConfigSchema } from './providers.js';
|
|
4
|
+
import { domainOfService, moduleDomainWritable, providerFootprint, resolveDomainBinding } from '../integrations/domain-binding.js';
|
|
5
|
+
import { isControlledInlineProvider } from '../integrations/predicate.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
|
-
*
|
|
8
|
-
* config
|
|
9
|
-
* /app、/modules/:m include)/ 供给轴批量(providers.ts include
|
|
8
|
+
* 层级/服务级 config schema 计算视图(v4.4 D32——静态轴富化,零 contracts 求值;v4.7 D38 服务级)。
|
|
9
|
+
* config 表单结构四形态同源(P10):槽级组合单体(providers.ts)/ 层级内嵌(本文件——
|
|
10
|
+
* /app、/modules/:m include)/ 供给轴批量(providers.ts include——切换态)/ 服务级生效绑定
|
|
11
|
+
* (本文件 loadServiceIntegrationSchemas——/services/:s include)。
|
|
10
12
|
*
|
|
11
13
|
* 计算规则(D32 裁决):禁交集——config 五层浅合并下「部分服务认识的字段」合法(谓词 #7
|
|
12
14
|
* 并集键集同依据);禁合成单一 schema——required/title 冲突的有损代数;产物 = 扁平映射,
|
|
13
15
|
* 分组归客户端纯呈现。层级 schema = 表单引导**非写入门禁**(P16——写路径校验权威恒在谓词
|
|
14
16
|
* #7 键集 + ajv 凭据链;root 级口径差:#7 = 引用厂商并集,表单 = 层生效厂商单厂商作用域)。
|
|
17
|
+
*
|
|
18
|
+
* v4.6(D36/D37 additive 精化):**原始投影**(loadApp/loadModuleIntegrationSchemas)原样保留
|
|
19
|
+
* 为诊断面 / registry 输入;**消费投影** = buildIntegrationFieldRegistry(字段级合并代数
|
|
20
|
+
* R1/R2/R3 服务端单源,conflict 折叠 schema:null);**轴向假想绑定** = formProviderOf
|
|
21
|
+
* (override ?? 实际解析——双投影同源同口径,formViews 装配层一次计算两投影)。
|
|
22
|
+
* 单源三锚:层级 schema 计算 = resolveLayerIntegrationSchemas / 生效厂商解析 = formProviderOf /
|
|
23
|
+
* 合并代数 = buildIntegrationFieldRegistry。
|
|
15
24
|
*/
|
|
16
25
|
|
|
17
|
-
/** 核心纯函数:显式作用域(provider null → 整字段 null;services 逐项
|
|
18
|
-
*
|
|
26
|
+
/** 核心纯函数:显式作用域(provider null → 整字段 null;services 逐项 resolveServiceConfigSchema
|
|
27
|
+
* 复合解析——v4.7 D38 收口,零新读取路径;供给在场未声明 = null,mock 合法) */
|
|
28
|
+
export function resolveLayerIntegrationSchemas(snapshot: ViewSnapshot, provider: string, services: readonly string[]): LayerIntegrationSchemas;
|
|
29
|
+
export function resolveLayerIntegrationSchemas(snapshot: ViewSnapshot, provider: string | null, services: readonly string[]): LayerIntegrationSchemas | null;
|
|
19
30
|
export function resolveLayerIntegrationSchemas(
|
|
20
31
|
snapshot: ViewSnapshot,
|
|
21
32
|
provider: string | null,
|
|
@@ -24,30 +35,192 @@ export function resolveLayerIntegrationSchemas(
|
|
|
24
35
|
if (provider === null) return null;
|
|
25
36
|
const out: LayerIntegrationSchemas['services'] = {};
|
|
26
37
|
for (const s of services) {
|
|
27
|
-
|
|
28
|
-
out[s] = { configSchema: entry ? readSchemaRef(snapshot.appDir, entry.packageDir, entry.configSchema) : null };
|
|
38
|
+
out[s] = { configSchema: resolveServiceConfigSchema(snapshot, provider, s) };
|
|
29
39
|
}
|
|
30
40
|
return { provider, services: out };
|
|
31
41
|
}
|
|
32
42
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (
|
|
39
|
-
|
|
43
|
+
// ── v4.6 D36/D37:合并代数助手(内联——深相等判定基)──
|
|
44
|
+
|
|
45
|
+
/** 稳定序列化(键序归一——R2 深相等 / R1 签名比较统一基;数组保序、对象键 sort) */
|
|
46
|
+
function stableSerialize(v: unknown): string {
|
|
47
|
+
if (v === null || typeof v !== 'object') return JSON.stringify(v ?? null) as string;
|
|
48
|
+
if (Array.isArray(v)) return `[${v.map(stableSerialize).join(',')}]`;
|
|
49
|
+
const keys = Object.keys(v as Record<string, unknown>).sort();
|
|
50
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${stableSerialize((v as Record<string, unknown>)[k])}`).join(',')}}`;
|
|
40
51
|
}
|
|
41
52
|
|
|
42
|
-
/**
|
|
43
|
-
|
|
44
|
-
|
|
53
|
+
/** enum 缺席哨兵(≠ 任何实际集签名——混布必不等) */
|
|
54
|
+
const NO_ENUM = '\u0000no-enum';
|
|
55
|
+
|
|
56
|
+
/** enum 集签名(去重排序——序不敏感) */
|
|
57
|
+
function enumSig(sub: Record<string, unknown>): string {
|
|
58
|
+
const e = sub.enum;
|
|
59
|
+
if (!Array.isArray(e)) return NO_ENUM;
|
|
60
|
+
return `[${[...new Set(e.map(stableSerialize))].sort().join(',')}]`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 顶层 type 签名(数组形态序敏感——严格全等);非对象(含布尔子 schema)/ type 缺席 → null(R3) */
|
|
64
|
+
function typeSig(sub: unknown): string | null {
|
|
65
|
+
if (!sub || typeof sub !== 'object' || Array.isArray(sub)) return null;
|
|
66
|
+
const t = (sub as Record<string, unknown>).type;
|
|
67
|
+
return t === undefined ? null : stableSerialize(t);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** D36 合并代数单点(R1/R2/R3——唯一扩展点;保守降级:不可调和 → null,客户端 RawJson 保真) */
|
|
71
|
+
function mergeFieldSources(sources: ReadonlyArray<{ subschema: unknown }>): object | null {
|
|
72
|
+
const sigs = sources.map((s) => typeSig(s.subschema));
|
|
73
|
+
if (sigs.some((s) => s === null) || new Set(sigs).size > 1) return null; // R3:非对象/type 缺席/type 不等
|
|
74
|
+
if (sigs[0] === '"object"') {
|
|
75
|
+
// R2:全源稳定序列化深相等 → 首源;异构 → null
|
|
76
|
+
const first = stableSerialize(sources[0].subschema);
|
|
77
|
+
return sources.every((s) => stableSerialize(s.subschema) === first) ? (sources[0].subschema as object) : null;
|
|
78
|
+
}
|
|
79
|
+
// R1:标量型——enum 集全等(含双无哨兵)→ 首源;不等/混布 → null
|
|
80
|
+
const es = new Set(sources.map((s) => enumSig(s.subschema as Record<string, unknown>)));
|
|
81
|
+
return es.size === 1 ? (sources[0].subschema as object) : null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** D36 registry 构造(纯函数——消费 resolveLayerIntegrationSchemas 产物,禁旁路直取 serviceSchemas)。
|
|
85
|
+
* fields 收集零过滤(布尔/非对象子 schema 入列,判定全权归 mergeFieldSources——S8 互证等式由
|
|
86
|
+
* 收集定义结构性闭合);schemaless 仅 configSchema === null(布尔/畸形整 schema 防御跳过——
|
|
87
|
+
* 不入 schemaless、零参与聚合;D32 运行时畸形,非契约面)。 */
|
|
88
|
+
export function buildIntegrationFieldRegistry(
|
|
89
|
+
layer: LayerIntegrationSchemas,
|
|
90
|
+
o?: { unsupplied?: string[] },
|
|
91
|
+
): IntegrationFieldRegistryView {
|
|
92
|
+
const schemaless: string[] = [];
|
|
93
|
+
let additionalPropertiesAllowed = true;
|
|
94
|
+
const buckets = new Map<string, Array<{ service: string; subschema: unknown; required: boolean }>>();
|
|
95
|
+
for (const [service, entry] of Object.entries(layer.services)) {
|
|
96
|
+
// 插入序 = scope 遍历序(app 字母序 / module demand 序)——fields 首声明序由此保证
|
|
97
|
+
const schema = entry.configSchema;
|
|
98
|
+
if (schema === null) {
|
|
99
|
+
schemaless.push(service);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (!schema || typeof schema !== 'object' || Array.isArray(schema)) continue;
|
|
103
|
+
const s = schema as Record<string, unknown>;
|
|
104
|
+
if (s.additionalProperties === false) additionalPropertiesAllowed = false;
|
|
105
|
+
const props = s.properties;
|
|
106
|
+
if (!props || typeof props !== 'object' || Array.isArray(props)) continue; // ≡ 谓词 collectPropertyKeys 守卫
|
|
107
|
+
const required = Array.isArray(s.required) ? s.required : [];
|
|
108
|
+
for (const key of Object.keys(props)) {
|
|
109
|
+
const bucket = buckets.get(key) ?? [];
|
|
110
|
+
bucket.push({ service, subschema: (props as Record<string, unknown>)[key], required: required.includes(key) });
|
|
111
|
+
buckets.set(key, bucket);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const fields: IntegrationFieldRegistryView['fields'] = [];
|
|
115
|
+
for (const [key, sources] of buckets) {
|
|
116
|
+
fields.push({
|
|
117
|
+
key,
|
|
118
|
+
schema: mergeFieldSources(sources),
|
|
119
|
+
sources: sources.map(({ service, required }) => ({ service, required })),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
provider: layer.provider,
|
|
124
|
+
schemaless,
|
|
125
|
+
unsupplied: [...(o?.unsupplied ?? [])],
|
|
126
|
+
additionalPropertiesAllowed,
|
|
127
|
+
fields,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** D37 生效厂商解析单源(实际/假想同径):override(非空串)?? 实际解析。
|
|
132
|
+
* 空串归一(I3)单点;module 轴 D34 不可写/不在册 → null(override 不豁免——预览服务于保存)。 */
|
|
133
|
+
export function formProviderOf(
|
|
134
|
+
axis: 'app' | 'module',
|
|
135
|
+
snapshot: ViewSnapshot,
|
|
136
|
+
moduleId?: string,
|
|
137
|
+
override?: string,
|
|
138
|
+
): string | null {
|
|
139
|
+
const ovr = typeof override === 'string' && override.length > 0 ? override : undefined;
|
|
140
|
+
if (axis === 'app') {
|
|
141
|
+
if (ovr !== undefined) return ovr;
|
|
142
|
+
const p = (snapshot.integrations as { provider?: unknown } | null)?.provider;
|
|
143
|
+
return typeof p === 'string' && p.length > 0 ? p : null;
|
|
144
|
+
}
|
|
45
145
|
const mod = snapshot.walk.modules.find((m) => m.id === moduleId);
|
|
46
146
|
if (!mod) return null; // 视图已先行 404,此处防御
|
|
47
147
|
const w = moduleDomainWritable(snapshot.walk, mod);
|
|
48
|
-
if (!w.writable) return null;
|
|
49
|
-
|
|
50
|
-
|
|
148
|
+
if (!w.writable) return null; // D34 不豁免
|
|
149
|
+
if (ovr !== undefined) return ovr;
|
|
150
|
+
return resolveDomainBinding(snapshot.integrations, w.domainKey).provider;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** D36/D37 表单层双投影(I1:同源同口径一次计算——双取单算 by construction) */
|
|
154
|
+
export interface LayerFormViews {
|
|
155
|
+
integrationSchemas: LayerIntegrationSchemas | null;
|
|
156
|
+
integrationFieldRegistry: IntegrationFieldRegistryView | null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** 防御双 null(module 不在册/不可写/未绑定、app 未绑定——F3) */
|
|
160
|
+
const NULL_FORM_VIEWS: LayerFormViews = { integrationSchemas: null, integrationFieldRegistry: null };
|
|
161
|
+
|
|
162
|
+
/** app 轴装配:scope = 供给足迹全量(≠ AppView.services——root.config 流向包括模块服务在内的所有服务);
|
|
163
|
+
* unsupplied 恒 [](结构性:足迹 ⊆ 供给)。未知厂商/零足迹 → 非 null 空集(镜像 D32 mock 语义)。 */
|
|
164
|
+
export function loadAppFormViews(snapshot: ViewSnapshot, appProvider?: string): LayerFormViews {
|
|
165
|
+
const provider = formProviderOf('app', snapshot, undefined, appProvider);
|
|
166
|
+
if (provider === null) return NULL_FORM_VIEWS;
|
|
167
|
+
const layer = resolveLayerIntegrationSchemas(snapshot, provider, providerFootprint(snapshot.catalog, provider));
|
|
168
|
+
return { integrationSchemas: layer, integrationFieldRegistry: buildIntegrationFieldRegistry(layer) };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** module 轴装配:仅 writable 单域(D34);scope = demand ∩ 该厂商供给;unsupplied 同点派生
|
|
172
|
+
* (demand − 供给,Set 去重保序——manifest demand 不去重防御)。 */
|
|
173
|
+
export function loadModuleFormViews(snapshot: ViewSnapshot, moduleId: string, moduleProvider?: string): LayerFormViews {
|
|
174
|
+
const provider = formProviderOf('module', snapshot, moduleId, moduleProvider);
|
|
175
|
+
const mod = snapshot.walk.modules.find((m) => m.id === moduleId);
|
|
176
|
+
if (provider === null || !mod) return NULL_FORM_VIEWS;
|
|
51
177
|
const demand = (mod.descriptor.contributes.services ?? []).map((s) => s.service);
|
|
52
|
-
|
|
178
|
+
const supplied = (s: string): boolean => isSupplied(snapshot.catalog, provider, s);
|
|
179
|
+
const layer = resolveLayerIntegrationSchemas(snapshot, provider, demand.filter(supplied));
|
|
180
|
+
return {
|
|
181
|
+
integrationSchemas: layer,
|
|
182
|
+
integrationFieldRegistry: buildIntegrationFieldRegistry(layer, {
|
|
183
|
+
unsupplied: [...new Set(demand.filter((s) => !supplied(s)))],
|
|
184
|
+
}),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** app 级 loader(v4.6 委托化——签名零改动,公开面保持;假想态归 loadAppFormViews override 参) */
|
|
189
|
+
export function loadAppIntegrationSchemas(snapshot: ViewSnapshot): LayerIntegrationSchemas | null {
|
|
190
|
+
return loadAppFormViews(snapshot).integrationSchemas;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** module 级 loader(v4.6 委托化——同上;多域/零域/共享键/未绑定 → 整字段 null,表单降级 = ExtraFields) */
|
|
194
|
+
export function loadModuleIntegrationSchemas(snapshot: ViewSnapshot, moduleId: string): LayerIntegrationSchemas | null {
|
|
195
|
+
return loadModuleFormViews(snapshot, moduleId).integrationSchemas;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** 服务级 loader(v4.7 D38):四分支穷举(node.provider = 文件 services[s].provider 逐字原值;
|
|
199
|
+
* 实例层无条件排除——node 级口径);非空三分支归约一次复合解析。
|
|
200
|
+
* ① undefined/'' → resolveDomainBinding 域级联(域 + root 双缺 → 整字段 null)
|
|
201
|
+
* ② 非空 string → bare:catalog miss(未知厂商/未供给)→ configSchema:null 不 404(富化非门控)
|
|
202
|
+
* ③ 受控内联(isControlledInlineProvider 单源)→ 名 = p.provider;catalog 复合键 > p.configSchema ref
|
|
203
|
+
* (H9 逃逸 400 照抛——readInlineSchema 共享路径)
|
|
204
|
+
* ④ 其余(null/数组/原始值/坏对象)→ 整字段 null(≡ 谓词 #5 INLINE_PROVIDER_INVALID 同判——
|
|
205
|
+
* 不放行域级联,防错误 schema 污染坏节点) */
|
|
206
|
+
export function loadServiceIntegrationSchemas(
|
|
207
|
+
snapshot: ViewSnapshot,
|
|
208
|
+
service: string,
|
|
209
|
+
): ServiceIntegrationSchemas | null {
|
|
210
|
+
const p = (snapshot.integrations as { services?: Record<string, { provider?: unknown }> } | null)
|
|
211
|
+
?.services?.[service]?.provider;
|
|
212
|
+
if (p === undefined || p === '') {
|
|
213
|
+
const binding = resolveDomainBinding(snapshot.integrations, domainOfService(service));
|
|
214
|
+
return binding.provider === null
|
|
215
|
+
? null
|
|
216
|
+
: { provider: binding.provider, configSchema: resolveServiceConfigSchema(snapshot, binding.provider, service) };
|
|
217
|
+
}
|
|
218
|
+
if (typeof p === 'string') {
|
|
219
|
+
return { provider: p, configSchema: resolveServiceConfigSchema(snapshot, p, service) };
|
|
220
|
+
}
|
|
221
|
+
if (isControlledInlineProvider(p)) {
|
|
222
|
+
const decl = p as { provider: string; configSchema?: string };
|
|
223
|
+
return { provider: decl.provider, configSchema: resolveServiceConfigSchema(snapshot, decl.provider, service, decl.configSchema) };
|
|
224
|
+
}
|
|
225
|
+
return null;
|
|
53
226
|
}
|
package/src/views/modules.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ModuleDeclaration, ModuleDetailView, ProviderBinding, ServiceDemand } from '../dto.js';
|
|
2
2
|
import { AppToolkitError } from '../core/errors.js';
|
|
3
3
|
import { moduleDomainWritable, resolveDomainBinding } from '../integrations/domain-binding.js';
|
|
4
|
-
import type
|
|
4
|
+
import { resolveServiceDisplay, type ViewSnapshot } from './context.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* 需求轴/模块静态投影(views/modules;C4)。静态端点结构性零求值(铁律 #3——
|
|
@@ -9,18 +9,34 @@ import type { ViewSnapshot } from './context.js';
|
|
|
9
9
|
* v4.4 D34:domainKeys/providerBindings/configEditable 显式化;integration 仅单域
|
|
10
10
|
* (writable)时在场——多域/零域/共享键无单键落点;多域回退兜底键机器拆除
|
|
11
11
|
* (派生统一 = domainOfService 去重集合,moduleDomainWritable 单源)。
|
|
12
|
+
* 2026-09-16 双缺陷批:loadModulesView 排除 provider 家族包(供给/实施载体非可配置
|
|
13
|
+
* 业务模块)——walk 声明集与 loadModuleDetailView 不变(装包仍合法,详情保持 200)。
|
|
12
14
|
*/
|
|
13
15
|
|
|
14
|
-
/**
|
|
16
|
+
/** 家族名归一(cli deriveModuleId 同源语义——依赖方向禁 import,故内联):
|
|
17
|
+
* `@tbox.cn/app-provider-x` / `@app/provider-y` / `provider-z` → `provider-*` */
|
|
18
|
+
function familyName(s: string): string {
|
|
19
|
+
return s.replace(/^@tbox\.cn\/app-/, '').replace(/^@[^/]+\//, '');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** provider 家族包判定(模块清单排除供给/实施载体;生态三先例同源:stage 排除判定 /
|
|
23
|
+
* guard-vendor-neutrality「provider-* 包」/ cli deriveModuleId 家族推导)。install id 与
|
|
24
|
+
* 包名 basename 双查 = 防 .tbox/app.json 登记漂移纵深;命名而非结构(provider-custom 零贡献
|
|
25
|
+
* 空壳与「零贡献合法」形态不可结构区分);未来收敛 = manifest 显式角色标记(届时整体退场)。 */
|
|
26
|
+
function isProviderFamily(mod: { id: string; pkg: string }): boolean {
|
|
27
|
+
return familyName(mod.id).startsWith('provider-') || familyName(mod.pkg).startsWith('provider-');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** GET /modules:纯模块清单(demand 不在——卡片展开 = GET /modules/:m;provider 家族包不在清单) */
|
|
15
31
|
export function loadModulesView(snapshot: ViewSnapshot): { modules: ModuleDeclaration[] } {
|
|
16
32
|
const modules: ModuleDeclaration[] = snapshot.walk.modules
|
|
33
|
+
.filter((mod) => !isProviderFamily(mod))
|
|
17
34
|
.map((mod) => ({
|
|
18
35
|
id: mod.id,
|
|
19
36
|
name: mod.descriptor.name,
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
description: '',
|
|
37
|
+
// 模块 meta(B1 additive):manifest title/description——兜底 name / ''(视图层灌值)
|
|
38
|
+
title: mod.descriptor.title ?? mod.descriptor.name,
|
|
39
|
+
description: mod.descriptor.description ?? '',
|
|
24
40
|
}))
|
|
25
41
|
.sort((a, b) => a.id.localeCompare(b.id));
|
|
26
42
|
return { modules };
|
|
@@ -32,12 +48,16 @@ export function loadModuleDetailView(snapshot: ViewSnapshot, moduleId: string):
|
|
|
32
48
|
if (!mod) {
|
|
33
49
|
throw new AppToolkitError('MODULE_NOT_FOUND', 404, `模块 ${moduleId} 非已装模块`);
|
|
34
50
|
}
|
|
35
|
-
const services: ServiceDemand[] = (mod.descriptor.contributes.services ?? []).map((need) =>
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
51
|
+
const services: ServiceDemand[] = (mod.descriptor.contributes.services ?? []).map((need) => {
|
|
52
|
+
// 展示名单源走链(B1):词汇 meta > demand meta > id——词汇主源防 manifest demand 漂移
|
|
53
|
+
const display = resolveServiceDisplay(snapshot.walk, need.service);
|
|
54
|
+
return {
|
|
55
|
+
service: need.service,
|
|
56
|
+
title: display.title,
|
|
57
|
+
...(display.description !== undefined ? { description: display.description } : {}),
|
|
58
|
+
required: need.optional === false,
|
|
59
|
+
};
|
|
60
|
+
});
|
|
41
61
|
const w = moduleDomainWritable(snapshot.walk, mod);
|
|
42
62
|
const domains = (snapshot.integrations as { domains?: Record<string, unknown> } | null)?.domains ?? {};
|
|
43
63
|
// integration = domains[换算键] 节点原值(D21 编辑预填源);未配置 = null;多域/零域/共享键 = null(D34)
|
|
@@ -46,5 +66,14 @@ export function loadModuleDetailView(snapshot: ViewSnapshot, moduleId: string):
|
|
|
46
66
|
const binding = resolveDomainBinding(snapshot.integrations, d);
|
|
47
67
|
return { domain: d, effective: binding.provider, layer: binding.layer };
|
|
48
68
|
});
|
|
49
|
-
return {
|
|
69
|
+
return {
|
|
70
|
+
// 模块 meta(B1 additive)——兜底与 /modules 同语义
|
|
71
|
+
title: mod.descriptor.title ?? mod.descriptor.name,
|
|
72
|
+
description: mod.descriptor.description ?? '',
|
|
73
|
+
domainKeys: [...w.domainKeys],
|
|
74
|
+
integration,
|
|
75
|
+
providerBindings,
|
|
76
|
+
configEditable: w.writable,
|
|
77
|
+
services,
|
|
78
|
+
};
|
|
50
79
|
}
|
package/src/views/providers.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { ProviderDetail, ProviderEntry, ProviderServiceDetail, ProvidersVie
|
|
|
2
2
|
import { AppToolkitError } from '../core/errors.js';
|
|
3
3
|
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
4
4
|
import { isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
5
|
-
import type
|
|
5
|
+
import { resolveServiceDisplay, type ViewSnapshot } from './context.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* 供给轴投影(views/providers;C4)。供给关系三切片(P5):
|
|
@@ -21,6 +21,9 @@ export function loadProvidersView(snapshot: ViewSnapshot): ProvidersView {
|
|
|
21
21
|
provider,
|
|
22
22
|
local: entries.every((e) => e.local === true),
|
|
23
23
|
owner: entries[0].owner,
|
|
24
|
+
// 厂商展示元数据(B1 walk 层传播——首条目 = 首声明,与 owner 同语义)
|
|
25
|
+
...(entries[0].title ? { title: entries[0].title } : {}),
|
|
26
|
+
...(entries[0].description ? { description: entries[0].description } : {}),
|
|
24
27
|
});
|
|
25
28
|
}
|
|
26
29
|
return { providers: providers.sort((a, b) => a.provider.localeCompare(b.provider)) };
|
|
@@ -40,12 +43,15 @@ export function loadProviderDetailView(snapshot: ViewSnapshot, provider: string,
|
|
|
40
43
|
bucket.implementations.push({ implementation: entry.implementation, credentialType: entry.credentialType });
|
|
41
44
|
}
|
|
42
45
|
}
|
|
46
|
+
// 服务展示名走链(B1):词汇 meta > demand meta > id(title 恒发)
|
|
47
|
+
for (const [service, bucket] of Object.entries(services)) {
|
|
48
|
+
const display = resolveServiceDisplay(snapshot.walk, service);
|
|
49
|
+
bucket.title = display.title;
|
|
50
|
+
if (display.description !== undefined) bucket.description = display.description;
|
|
51
|
+
}
|
|
43
52
|
if (include?.has('integrationSchemas')) {
|
|
44
53
|
for (const [service, bucket] of Object.entries(services)) {
|
|
45
|
-
|
|
46
|
-
bucket.integrationSchemas = {
|
|
47
|
-
configSchema: schemaEntry ? readSchemaRef(snapshot.appDir, schemaEntry.packageDir, schemaEntry.configSchema) : null,
|
|
48
|
-
};
|
|
54
|
+
bucket.integrationSchemas = { configSchema: resolveServiceConfigSchema(snapshot, provider, service) };
|
|
49
55
|
}
|
|
50
56
|
}
|
|
51
57
|
const entries = Object.values(impls);
|
|
@@ -53,6 +59,9 @@ export function loadProviderDetailView(snapshot: ViewSnapshot, provider: string,
|
|
|
53
59
|
provider,
|
|
54
60
|
local: entries.every((e) => e.local === true),
|
|
55
61
|
owner: entries[0].owner,
|
|
62
|
+
// 厂商展示元数据(B1 walk 层传播——首条目透传)
|
|
63
|
+
...(entries[0].title ? { title: entries[0].title } : {}),
|
|
64
|
+
...(entries[0].description ? { description: entries[0].description } : {}),
|
|
56
65
|
services,
|
|
57
66
|
};
|
|
58
67
|
}
|
|
@@ -95,22 +104,15 @@ export function loadProviderServiceDetailView(
|
|
|
95
104
|
implementations.push({ implementation: configured[service]?.implementation ?? `${provider}-inline@1`, credentialType: inlineDeclaration.credentialType });
|
|
96
105
|
}
|
|
97
106
|
|
|
98
|
-
// integrationSchemas:
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
// 内联声明 schema 解析:引用文件在 integrations 节点(provider.configSchema)——
|
|
108
|
-
// 形状同 config/schemas/ 约束(路径来源 = 节点声明,由服务端读应用侧)
|
|
109
|
-
const inlineSchemaRef = (inline as { configSchema?: string } | undefined)?.configSchema;
|
|
110
|
-
if (typeof inlineSchemaRef === 'string') {
|
|
111
|
-
configSchema = readInlineSchema(snapshot.appDir, inlineSchemaRef);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
107
|
+
// integrationSchemas:schema 解析收口 resolveServiceConfigSchema 单源(v4.7 D38——catalog 复合键 >
|
|
108
|
+
// 内联 ref,等价重构 O6/O7/O15 零改动即证);内联 ref 来源 = 节点声明(provider.configSchema,
|
|
109
|
+
// 仅受控内联声明匹配本厂商名时);路径约束 H9——resolve 落 config/schemas/ 内 + realpath 复核,违例 400
|
|
110
|
+
const configSchema = resolveServiceConfigSchema(
|
|
111
|
+
snapshot,
|
|
112
|
+
provider,
|
|
113
|
+
service,
|
|
114
|
+
inlineDeclaration ? (inline as { configSchema?: string } | undefined)?.configSchema : undefined,
|
|
115
|
+
);
|
|
114
116
|
|
|
115
117
|
return {
|
|
116
118
|
provider,
|
|
@@ -120,6 +122,22 @@ export function loadProviderServiceDetailView(
|
|
|
120
122
|
};
|
|
121
123
|
}
|
|
122
124
|
|
|
125
|
+
/** per-(provider, service) schema 复合解析单源(v4.7 D38):catalog 复合键(多 impl 声明序首个)
|
|
126
|
+
* > 内联 ref(readInlineSchema 保持私有——H9 双钉子不扩暴露);miss / 未声明 / 文件缺失 → null。
|
|
127
|
+
* views 层 \u0000 复合键唯一消费点(组合单体 / 供给轴批量 / 层级核心 / 服务级 include 同源)。
|
|
128
|
+
* (D36 registry 批若需跨包消费再升 index 导出——additive 零成本) */
|
|
129
|
+
export function resolveServiceConfigSchema(
|
|
130
|
+
snapshot: ViewSnapshot,
|
|
131
|
+
provider: string,
|
|
132
|
+
service: string,
|
|
133
|
+
inlineRef?: string,
|
|
134
|
+
): object | null {
|
|
135
|
+
const entry = snapshot.walk.serviceSchemas[`${provider}\u0000${service}`];
|
|
136
|
+
if (entry) return readSchemaRef(snapshot.appDir, entry.packageDir, entry.configSchema);
|
|
137
|
+
if (typeof inlineRef === 'string') return readInlineSchema(snapshot.appDir, inlineRef);
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
123
141
|
/** catalog schema 引用读取(相对包根;缺席 → null——SCHEMA_FILE_MISSING 归谓词 #6)。
|
|
124
142
|
* v4.4 导出共享:层级内嵌(integration-schemas.ts)/ 凭据结构清单(credential-types.ts)同源消费。 */
|
|
125
143
|
export function readSchemaRef(_appDir: string, packageDir: string, ref: string | undefined): object | null {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ServiceDetail } from '../dto.js';
|
|
2
2
|
import { AppToolkitError } from '../core/errors.js';
|
|
3
3
|
import { isKnownService } from '../integrations/read.js';
|
|
4
|
-
import { suppliedByVendors, type ViewSnapshot } from './context.js';
|
|
4
|
+
import { resolveServiceDisplay, suppliedByVendors, type ViewSnapshot } from './context.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* 服务静态单体投影(views/service-detail;C4)。求值数据不在此(→ /service-resolutions/:service)。
|
|
@@ -17,10 +17,12 @@ export function loadServiceDetailView(snapshot: ViewSnapshot, service: string):
|
|
|
17
17
|
}
|
|
18
18
|
// integration = 文件 services[s] 逐字原值(编辑预填源 D21);无声明 = null
|
|
19
19
|
const integration = (configured[service] as ServiceDetail['integration'] | undefined) ?? null;
|
|
20
|
+
// 展示名单源走链(B1):词汇 meta > demand meta > id——替换 demand 直拼(恒有值)
|
|
21
|
+
const display = resolveServiceDisplay(snapshot.walk, service);
|
|
20
22
|
return {
|
|
21
23
|
service,
|
|
22
|
-
|
|
23
|
-
...(
|
|
24
|
+
title: display.title,
|
|
25
|
+
...(display.description !== undefined ? { description: display.description } : {}),
|
|
24
26
|
required: demand ? !demand.optional : false,
|
|
25
27
|
// defaults 唯一落位(manifest 预填)
|
|
26
28
|
defaults: demand ? demand.defaults.map((d) => ({ ...d })) : [],
|