@tbox.cn/app-toolkit 0.5.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/src/dto.ts CHANGED
@@ -38,6 +38,9 @@ export interface ModuleDeclaration {
38
38
  /** 展示名(v7 词汇统一:manifest name ?? id——恒有值) */
39
39
  name: string;
40
40
  description: string;
41
+ /** 隐藏模块(additive——manifest hidden === true 时携带;默认清单已过滤,本字段仅供
42
+ * ?includeHidden=true 可见时徽标呈现) */
43
+ hidden?: boolean;
41
44
  }
42
45
 
43
46
  /** 服务需求声明(模块 demand 单元 / 平台服务条目——v4.4 D29 双端点同型;defaults 唯一落位 ServiceDetail) */
@@ -58,11 +61,17 @@ export interface ModuleDetailView {
58
61
  name: string;
59
62
  /** B1 additive:模块 meta 描述(manifest description;兜底 '') */
60
63
  description: string;
64
+ /** 隐藏模块(additive——manifest hidden === true 时携带;详情恒 200 不受清单过滤影响,
65
+ * 本字段仅供面板徽标呈现) */
66
+ hidden?: boolean;
61
67
  /** v6 所有权模型:模块归属服务(declare.owns 展平,排序;零归属 = [])——
62
68
  * v5 domainKeys 退役(破坏性更名;面板 presence-detection 双读义务见 api.md §3) */
63
69
  ownedServices: string[];
64
- /** modules[install id] 集成位原值(provider/config;resources 不投影);未配置 = null */
70
+ /** modules[install id] 集成位原值(provider/config;resources 不投影——见 resources 字段);未配置 = null */
65
71
  integration: ModuleIntegrationNode | null;
72
+ /** 模块资源只读镜像(N2 读半边——写侧 resources 恒保全;含 ref 台账展开态 resolvedDatasetId);
73
+ * 无模块键/无资源节 = null */
74
+ resources?: ModuleResourcesEcho | null;
66
75
  /** v6 单数化(v5 providerBindings[] 退役):模块级厂商绑定解析(resolveModuleBinding 单源;
67
76
  * 单值作用于全部 ownedServices) */
68
77
  binding: ModuleBinding | null;
@@ -147,14 +156,17 @@ export interface ServiceDetail extends ServiceDemand {
147
156
  * 绑定无可计算 schema / 含 schema;malformed 节点 → null——不放行域级联) */
148
157
  integrationSchemas?: ServiceIntegrationSchemas | null;
149
158
  /** G 批:include=integrationFieldRegistry 服务轴注册表(loadServiceFieldRegistry——
150
- * 四分支链推导复用 D38;fields = 该 schema 全部字段带 global 旗标;null 语义镜像 D38) */
159
+ * 四分支链推导复用 D38;fields = 该 schema 全部字段带 global 旗标;null 语义镜像 D38。
160
+ * v5.1 D39:serviceProvider 轴参 = 假想重定向——仅表单投影按 X 计算,带非空轴参时
161
+ * registry 恒非 null(豁免链③不镜像实际态 null);同构 ≡ PUT 改写 provider 后实读) */
151
162
  integrationFieldRegistry?: IntegrationFieldRegistryView | null;
152
163
  }
153
164
 
154
165
  /** 服务级生效绑定 schema(v4.7 D38 静态轴富化——零 contracts,永不 503;实例层无条件排除:
155
166
  * node 级口径 ≠ resolution 默认实例口径,api.md §4.4 注) */
156
167
  export interface ServiceIntegrationSchemas {
157
- /** 生效厂商:显式(含内联声明名)?? modules[ownerOf].provider ?? root.provider */
168
+ /** 生效厂商:显式(含内联声明名)?? modules[ownerOf].provider ?? root.provider;
169
+ * v5.1 D39:serviceProvider 轴参在场时 = 假想短路值(非链解析产物) */
158
170
  provider: string;
159
171
  /** catalog 复合键(多 impl 声明序首个)> 内联 ref;miss(未知厂商/未供给/内联缺 ref)→ null(富化非门控) */
160
172
  configSchema: object | null;
@@ -244,6 +256,26 @@ export interface ModuleIntegrationNode {
244
256
  * (键集外 400;resources 恒随既有节点保全)+ views/modules 投影(GET ≡ PUT,三同构)) */
245
257
  export const MODULE_INTEGRATION_NODE_KEYS = ['provider', 'config'] as const;
246
258
 
259
+ /** 模块资源回显(只读投影——N2 读半边:写侧仍仅 provider/config,resources 恒保全)。
260
+ * 原值文件镜像(L1)+ ref 台账展开态:`ref` 在场且同目录 knowledge.json 命中时
261
+ * `resolvedDatasetId` = 台账 datasetId(镜像 SDK resolveKnowledge 展开语义),未命中/台账缺失 = null。
262
+ * 路径锚定边界:视图恒读 `<appDir>/config/`(与 toolkit readIntegrationsConfig 同源——
263
+ * `TBOX_INTEGRATIONS_FILE` 为 SDK 运行时逃生舱,视图不跟随,既有分叉)。 */
264
+ export interface ModuleResourceEcho {
265
+ type: 'knowledge';
266
+ /** 文件原值直填 datasetId(与 ref 互斥使用——并存时 SDK 侧 datasetId 优先) */
267
+ datasetId?: string;
268
+ /** 文件原值 ref(台账引用——knowledge.json knowledge_bases 键) */
269
+ ref?: string;
270
+ /** 展开态:datasetId 直填时 = 该值;ref 命中台账 = 台账 datasetId;ref 未命中/台账缺失 = null */
271
+ resolvedDatasetId: string | null;
272
+ topK?: number;
273
+ scoreThreshold?: number;
274
+ }
275
+
276
+ /** modules[install id].resources 只读镜像(键 = 资源 id;无模块键/无资源 = null) */
277
+ export type ModuleResourcesEcho = Record<string, ModuleResourceEcho>;
278
+
247
279
  /** services[s] 整节点 */
248
280
  export interface ServiceIntegrationNode {
249
281
  /** 对象 = 自定义 API(仅槽级,P2) */
package/src/factory.ts CHANGED
@@ -45,7 +45,8 @@ import type {
45
45
  * createAppToolkit(C4 工厂;v4.4——24 方法 = 11 读(含 credentials/credential-types 双端点)
46
46
  * + 4 写 + 9 迁移面;loadApp/loadModule/loadProvider 增 include 选项;v4.6——loadApp/loadModule
47
47
  * += 轴向假想绑定选项(appProvider/moduleProvider,D37);表单双投影经 formViews 单点装配;
48
- * v4.7——loadService 增 include 选项(服务级生效绑定 schema 单体))。
48
+ * v4.7——loadService 增 include 选项(服务级生效绑定 schema 单体);v5.1 D39——loadService
49
+ * += serviceProvider 轴参(服务轴向假想绑定,三轴收口))。
49
50
  * AppToolkit = AppToolkitRead & AppToolkitWrite & AppToolkitInternals——
50
51
  * Read+Write = HTTP 直通面 additive 冻结(L1 端点↔方法 1:1,L3 命名镜像:Integration=节点级 /
51
52
  * Config=文件级);Internals = CLI/doctor/桥消费(自由演进)。
@@ -98,9 +99,10 @@ function validateInclude(kind: keyof typeof INCLUDE_WHITELIST, include?: string[
98
99
  const FORM_INCLUDE_KEYS = ['integrationSchemas', 'integrationFieldRegistry'] as const;
99
100
 
100
101
  /** v4.6 D37 轴参严检(400 先于 404——镜像输入校验先行):非空轴参需与表单 include 键同现;
101
- * 缺席/空串 ≡ 按实际配置解析(I3——不触发严检);错轴参数由壳层按名读取,天然静默忽略。 */
102
+ * 缺席/空串 ≡ 按实际配置解析(I3——不触发严检);错轴参数由壳层按名读取,天然静默忽略。
103
+ * v5.1 D39:联合类型 += serviceProvider(service 轴同律)。 */
102
104
  function assertAxisProviderUsable(
103
- paramName: 'appProvider' | 'moduleProvider',
105
+ paramName: 'appProvider' | 'moduleProvider' | 'serviceProvider',
104
106
  value: string | undefined,
105
107
  include: ReadonlySet<string>,
106
108
  ): void {
@@ -119,13 +121,16 @@ function assertAxisProviderUsable(
119
121
  * **双键契约(v7 词汇统一)**:资源路径参数双键寻址(id 优先、唯一展示名兜底——resolve-key
120
122
  * 单点;多命中 404 带候选);query 过滤参数(loadServiceResolutions services?)恒 id-only。 */
121
123
  export interface AppToolkitRead {
122
- loadModules(): Promise<ModulesView>;
124
+ /** GET /modules:hidden 模块默认过滤(includeHidden = true 可见——对齐 ?dryRun 布尔参数先例) */
125
+ loadModules(o?: { includeHidden?: boolean }): Promise<ModulesView>;
123
126
  /** v4.6 D37:moduleProvider = 轴向假想绑定(缺席/空串 ≡ 按实际配置解析;需与表单 include 键同现)。
124
127
  * moduleId = 双键(id 优先、唯一展示名兜底)。 */
125
128
  loadModule(moduleId: string, o?: { include?: string[]; moduleProvider?: string }): Promise<ModuleDetailView>;
126
129
  /** v4.6 D37:appProvider = 轴向假想绑定(缺席/空串 ≡ 按实际配置解析;需与表单 include 键同现) */
127
130
  loadApp(o?: { include?: string[]; appProvider?: string }): Promise<AppView>;
128
- loadService(service: string, o?: { include?: string[] }): Promise<ServiceDetail>;
131
+ /** v5.1 D39:serviceProvider = 服务轴向假想绑定(缺席/空串 ≡ 按实际配置解析;需与表单 include
132
+ * 键同现)——假想重定向仅表单双投影,integration/suppliedBy/defaults 恒实际态(I2) */
133
+ loadService(service: string, o?: { include?: string[]; serviceProvider?: string }): Promise<ServiceDetail>;
129
134
  loadServiceResolutions(services?: string[]): Promise<ServiceResolutionsView>;
130
135
  loadServiceResolution(service: string): Promise<ServiceResolutionDetail>;
131
136
  loadProviders(): Promise<ProvidersView>;
@@ -242,9 +247,9 @@ export function createAppToolkit(appDir: string, options: AppToolkitOptions = {}
242
247
  }
243
248
 
244
249
  const read = {
245
- async loadModules() {
250
+ async loadModules(o?: { includeHidden?: boolean }) {
246
251
  const contracts = await resolveContractsForApp(appDir);
247
- return loadModulesView(snapshot(contracts));
252
+ return loadModulesView(snapshot(contracts), o);
248
253
  },
249
254
  // v4.4 D31:门控与内嵌分离——静态方法零门控分支;include=serviceResolutions 按
250
255
  // evaluation && strictValidation 双条件装配(<0.9/形态异常 → null,永不 503——技术方案 §2.4:
@@ -291,18 +296,21 @@ export function createAppToolkit(appDir: string, options: AppToolkitOptions = {}
291
296
  },
292
297
  // v4.7 D38:include 镜像 loadModule 三段式——① 输入校验 400 先于 404 ② 视图 ③ 静态富化(永不 503)
293
298
  // G 批:service 轴 += integrationFieldRegistry(loadServiceFieldRegistry——D38 四分支链复用)
294
- async loadService(service: string, o?: { include?: string[] }): Promise<ServiceDetail> {
299
+ // v5.1 D39:loadService += serviceProvider 轴参(三段式序对齐 loadModule——①' 严检先于 resolveContracts,
300
+ // 即先于 404 也先于 503;I1 双投影同源一次计算经单源链透传)
301
+ async loadService(service: string, o?: { include?: string[]; serviceProvider?: string }): Promise<ServiceDetail> {
295
302
  const include = validateInclude('service', o?.include);
303
+ assertAxisProviderUsable('serviceProvider', o?.serviceProvider, include); // ①' D39 轴参严检(先于 404)
296
304
  const contracts = await resolveContractsForApp(appDir);
297
305
  const snap = snapshot(contracts); // 单快照——detail 与 include 装配共享
298
306
  // 双键寻址(v7):id 优先、唯一展示名兜底 → 真实 service id(404 面)
299
307
  const serviceId = resolveServiceKey(snap.walk, configuredServiceKeys(snap), service);
300
308
  const view = loadServiceDetailView(snap, serviceId);
301
309
  if (include.has('integrationSchemas')) {
302
- view.integrationSchemas = loadServiceIntegrationSchemas(snap, serviceId);
310
+ view.integrationSchemas = loadServiceIntegrationSchemas(snap, serviceId, o?.serviceProvider);
303
311
  }
304
312
  if (include.has('integrationFieldRegistry')) {
305
- view.integrationFieldRegistry = loadServiceFieldRegistry(snap, serviceId);
313
+ view.integrationFieldRegistry = loadServiceFieldRegistry(snap, serviceId, o?.serviceProvider);
306
314
  }
307
315
  return view;
308
316
  },
package/src/index.ts CHANGED
@@ -129,6 +129,8 @@ export type {
129
129
  ServiceResolutionDetail,
130
130
  AppIntegrationNode,
131
131
  ModuleIntegrationNode,
132
+ ModuleResourceEcho,
133
+ ModuleResourcesEcho,
132
134
  ServiceIntegrationNode,
133
135
  ServiceInstanceNode,
134
136
  InlineVendorDeclaration,
@@ -261,6 +261,10 @@ export function createWriteCore(appDir: string, opts: WriteCoreOptions = {}): Wr
261
261
  cache?.invalidate([join(appDir, INTEGRATIONS_FILE)]);
262
262
  }
263
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));
264
268
  const restartScheduled = written && onApplied !== undefined;
265
269
  if (restartScheduled) onApplied?.();
266
270
  const files: string[] = [];
@@ -19,6 +19,8 @@ export interface ViewSnapshot {
19
19
  catalog: WalkCatalog;
20
20
  integrations: Record<string, unknown> | null;
21
21
  contracts: ResolvedContracts;
22
+ /** 文件读原语透传(可选——resources 台账惰性读等 stat 指纹缓存消费;缺席 = 调用方直读) */
23
+ cache?: FileCache;
22
24
  }
23
25
 
24
26
  /** 读取视图输入快照(每方法调用独立——Freshness Contract 单位 = 方法调用) */
@@ -27,14 +29,14 @@ export async function loadViewSnapshot(ctx: ViewContext): Promise<ViewSnapshot>
27
29
  const integrations = await readIntegrationsConfig(ctx.appDir, ctx.cache);
28
30
  const { resolveContractsForApp } = await import('../core/contracts-resolver.js');
29
31
  const contracts = await resolveContractsForApp(ctx.appDir);
30
- 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 } : {}) };
31
33
  }
32
34
 
33
35
  /** 同步快照(调用方已持有 contracts 时——避免重复解析) */
34
36
  export function loadViewSnapshotSync(ctx: ViewContext, contracts: ResolvedContracts): ViewSnapshot {
35
37
  const walk = walkManifests(ctx.appDir, ctx.cache);
36
38
  const integrations = readIntegrationsConfig(ctx.appDir, ctx.cache);
37
- 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 } : {}) };
38
40
  }
39
41
 
40
42
  /** 模块占用服务集(v6——demand ∪ supply ∪ owns:归属模型下「平台服务」= 无主服务;
@@ -229,11 +229,22 @@ function serviceLevelBindingProvider(snapshot: ViewSnapshot, service: string): s
229
229
  * ③ 受控内联(isControlledInlineProvider 单源)→ 名 = p.provider;catalog 复合键 > p.configSchema ref
230
230
  * (H9 逃逸 400 照抛——readInlineSchema 共享路径)
231
231
  * ④ 其余(null/数组/原始值/坏对象)→ 整字段 null(≡ 谓词 #5 INLINE_PROVIDER_INVALID 同判——
232
- * 不放行域级联,防错误 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 轴零归属对应物)。 */
233
239
  export function loadServiceIntegrationSchemas(
234
240
  snapshot: ViewSnapshot,
235
241
  service: string,
242
+ serviceProvider?: string,
236
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
+ }
237
248
  const p = (snapshot.integrations as { services?: Record<string, { provider?: unknown }> } | null)
238
249
  ?.services?.[service]?.provider;
239
250
  if (p === undefined || p === '') {
@@ -255,12 +266,15 @@ export function loadServiceIntegrationSchemas(
255
266
  /** G 批:服务轴字段注册表(D38 四分支链复用——消费 loadServiceIntegrationSchemas 单源;
256
267
  * 伪层 {provider, services: {[s]: {configSchema}}} → buildIntegrationFieldRegistry——
257
268
  * fields = 该 schema 全部字段带 global 旗标;configSchema null → schemaless=[service]
258
- * 非 null 空集;schemas 整体 null(全链未绑定/坏节点)→ registry null——镜像 D38 语义) */
269
+ * 非 null 空集;schemas 整体 null(全链未绑定/坏节点)→ registry null——镜像 D38 语义)。
270
+ * v5.1 D39:serviceProvider = 服务轴向假想绑定,纯透传(I1 单源——归一与短路都在链函数头部;
271
+ * 轴参仅重定向表单层投影,global 旗标全量透传语义不变)。 */
259
272
  export function loadServiceFieldRegistry(
260
273
  snapshot: ViewSnapshot,
261
274
  service: string,
275
+ serviceProvider?: string,
262
276
  ): IntegrationFieldRegistryView | null {
263
- const schemas = loadServiceIntegrationSchemas(snapshot, service);
277
+ const schemas = loadServiceIntegrationSchemas(snapshot, service, serviceProvider);
264
278
  if (schemas === null) return null;
265
279
  const layer: LayerIntegrationSchemas = {
266
280
  provider: schemas.provider,
@@ -1,4 +1,6 @@
1
- import type { ModuleDeclaration, ModuleBinding, ModuleDetailView, ServiceDemand } from '../dto.js';
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
5
  import { moduleOwnershipWritable, resolveModuleBinding } from '../integrations/module-binding.js';
4
6
  import { resolveServiceDisplay, type ViewSnapshot } from './context.js';
@@ -8,11 +10,62 @@ import { resolveServiceDisplay, type ViewSnapshot } from './context.js';
8
10
  * 无 resolution/issues/contracts 键,永不 503)。
9
11
  * v6 所有权模型:ownedServices(declare.owns 展平)/ binding 单数(resolveModuleBinding)/
10
12
  * configEditable = owns>0(moduleOwnershipWritable 单源);integration = modules[install id]
11
- * 集成位投影(provider/config;resources 不投影——N2)。v5 domainKeys/providerBindings 退役。
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,15 +80,21 @@ 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
- export function loadModulesView(snapshot: ViewSnapshot): { modules: ModuleDeclaration[] } {
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
94
  // v7 词汇统一:name = 展示名(manifest name;兜底 id——恒有值)
37
95
  name: mod.descriptor.name ?? mod.descriptor.id,
38
96
  description: mod.descriptor.description ?? '',
97
+ ...(mod.descriptor.hidden === true ? { hidden: true } : {}),
39
98
  }))
40
99
  .sort((a, b) => a.id.localeCompare(b.id));
41
100
  return { modules };
@@ -75,9 +134,12 @@ export function loadModuleDetailView(snapshot: ViewSnapshot, moduleId: string):
75
134
  // v7 词汇统一:name = 展示名(兜底与 /modules 同语义——manifest name ?? id)
76
135
  name: mod.descriptor.name ?? mod.descriptor.id,
77
136
  description: mod.descriptor.description ?? '',
137
+ ...(mod.descriptor.hidden === true ? { hidden: true } : {}),
78
138
  // v6:ownedServices(declare.owns 展平排序)——N13:展示名走 resolveServiceDisplay 链,不重复携带
79
139
  ownedServices: w.ownedServices,
80
140
  integration,
141
+ // 模块资源只读镜像(N2 读半边——knowledge.json 台账 ref 展开态随行)
142
+ resources: resourcesOf(snapshot, moduleId, modulesNode),
81
143
  binding: { provider: binding.provider, layer: binding.layer },
82
144
  configEditable: w.writable,
83
145
  services,
@@ -0,0 +1,148 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { join } from 'node:path';
3
+ import { mkdirSync } from 'node:fs';
4
+ import { createAppToolkit, type AppToolkit } from '../src/factory.js';
5
+ import { invalidateContractsResolver } from '../src/core/contracts-resolver.js';
6
+ import { MODULE_INTEGRATION_NODE_KEYS } from '../src/dto.js';
7
+ import type { ModuleDetailView } from '../src/dto.js';
8
+ import { createTempApp, writeFakeContractsPackage, linkContracts, writeAppFile, type TempApp } from './demo-app.js';
9
+
10
+ /**
11
+ * 模块资源只读投影矩阵(N2 读半边):GET /modules/:m `resources` 四态(直填 datasetId /
12
+ * ref 命中台账 / ref 未命中(台账缺失·坏文件·键 miss 同归 null)/ 无 modules 节 = null)
13
+ * + 写侧保全不变量(MODULE_INTEGRATION_NODE_KEYS 白名单不变——resources 不进写位)。
14
+ * 展开语义镜像 SDK resolveKnowledge:datasetId 直填优先、ref 台账命中/miss-warn 降级。
15
+ */
16
+
17
+ interface Harness {
18
+ app: TempApp;
19
+ tk: AppToolkit;
20
+ }
21
+
22
+ /** 自足基座:module-cs(declare.resources 声明对齐真实客服 manifest)+ integrations + 可选台账 */
23
+ function resourcesHarness(integrations: object, knowledge: object | null): Harness {
24
+ const app = createTempApp('tbox-res-');
25
+ invalidateContractsResolver();
26
+ const store = join(app.appDir, 'contracts');
27
+ writeFakeContractsPackage(store, { version: '0.9.0' });
28
+ linkContracts(app.appDir, store, { subdir: 'apps/server' });
29
+ mkdirSync(join(app.appDir, 'packages', 'module-cs'), { recursive: true });
30
+ writeAppFile(
31
+ app.appDir,
32
+ 'packages/module-cs/tbox.module.json',
33
+ JSON.stringify({
34
+ schemaVersion: 1,
35
+ id: 'module-cs',
36
+ version: '0.1.0',
37
+ kind: 'business',
38
+ declare: {
39
+ owns: [],
40
+ consumes: [],
41
+ resources: [
42
+ { id: 'customer-service-faq-kb', type: 'knowledge' },
43
+ { id: 'knowledge-assistance-kb', type: 'knowledge' },
44
+ ],
45
+ },
46
+ dependencies: { modules: [] },
47
+ env: [],
48
+ }),
49
+ );
50
+ writeAppFile(app.appDir, 'config/integrations.json', JSON.stringify(integrations));
51
+ if (knowledge !== null) {
52
+ writeAppFile(app.appDir, 'config/knowledge.json', JSON.stringify(knowledge));
53
+ }
54
+ const tk = createAppToolkit(app.appDir);
55
+ return { app, tk };
56
+ }
57
+
58
+ const FAQ_DIRECT = {
59
+ provider: 'mock',
60
+ modules: {
61
+ 'module-cs': {
62
+ resources: { 'customer-service-faq-kb': { type: 'knowledge', datasetId: '20260101direct00000000000001', topK: 5, scoreThreshold: 0.5 } },
63
+ },
64
+ },
65
+ };
66
+
67
+ const FAQ_REF = {
68
+ provider: 'mock',
69
+ modules: {
70
+ 'module-cs': {
71
+ resources: { 'customer-service-faq-kb': { type: 'knowledge', ref: 'customer-service-faq-generic' } },
72
+ },
73
+ },
74
+ };
75
+
76
+ const KB_REGISTRY = {
77
+ version: '1.0',
78
+ knowledge_bases: {
79
+ 'customer-service-faq-generic': { datasetId: '20260921999def645305kQj02050918', retrieve_config: { topK: 5, scoreThreshold: 0.5 } },
80
+ },
81
+ };
82
+
83
+ async function detailOf(h: Harness): Promise<ModuleDetailView> {
84
+ return h.tk.loadModule('module-cs');
85
+ }
86
+
87
+ describe('module resources 只读投影(N2 读半边)', () => {
88
+ it('T1 直填 datasetId:resolvedDatasetId = 原值(无台账读取依赖)', async () => {
89
+ const h = resourcesHarness(FAQ_DIRECT, null); // 台账缺失——直填态不受影响
90
+ const v = await detailOf(h);
91
+ expect(v.resources).not.toBeNull();
92
+ expect(v.resources!['customer-service-faq-kb']).toEqual({
93
+ type: 'knowledge',
94
+ datasetId: '20260101direct00000000000001',
95
+ resolvedDatasetId: '20260101direct00000000000001',
96
+ topK: 5,
97
+ scoreThreshold: 0.5,
98
+ });
99
+ h.app.dispose();
100
+ });
101
+
102
+ it('T2 ref 命中台账:resolvedDatasetId = 台账 datasetId;文件原值 ref 保留', async () => {
103
+ const h = resourcesHarness(FAQ_REF, KB_REGISTRY);
104
+ const v = await detailOf(h);
105
+ expect(v.resources!['customer-service-faq-kb']).toEqual({
106
+ type: 'knowledge',
107
+ ref: 'customer-service-faq-generic',
108
+ resolvedDatasetId: '20260921999def645305kQj02050918',
109
+ });
110
+ h.app.dispose();
111
+ });
112
+
113
+ it('T3 ref 未命中键:resolvedDatasetId = null(原值 ref 保留)', async () => {
114
+ const h = resourcesHarness(FAQ_REF, { knowledge_bases: { 'other-kb': { datasetId: 'x' } } });
115
+ const v = await detailOf(h);
116
+ expect(v.resources!['customer-service-faq-kb']).toEqual({
117
+ type: 'knowledge',
118
+ ref: 'customer-service-faq-generic',
119
+ resolvedDatasetId: null,
120
+ });
121
+ h.app.dispose();
122
+ });
123
+
124
+ it('T3b 台账文件缺失 / 坏 JSON:同归 resolvedDatasetId = null(fail-soft 镜像 SDK)', async () => {
125
+ const missing = resourcesHarness(FAQ_REF, null);
126
+ expect((await detailOf(missing)).resources!['customer-service-faq-kb']?.resolvedDatasetId).toBeNull();
127
+ missing.app.dispose();
128
+
129
+ const broken = resourcesHarness(FAQ_REF, KB_REGISTRY);
130
+ writeAppFile(broken.app.appDir, 'config/knowledge.json', '{not-json');
131
+ expect((await detailOf(broken)).resources!['customer-service-faq-kb']?.resolvedDatasetId).toBeNull();
132
+ broken.app.dispose();
133
+ });
134
+
135
+ it('T4 无 modules 节 / 模块键无 resources:resources = null(null 语义:无资源 ≠ 空表)', async () => {
136
+ const noModules = resourcesHarness({ provider: 'mock' }, null);
137
+ expect((await detailOf(noModules)).resources).toBeNull();
138
+ noModules.app.dispose();
139
+
140
+ const noResourcesKey = resourcesHarness({ provider: 'mock', modules: { 'module-cs': { config: { k: 1 } } } }, KB_REGISTRY);
141
+ expect((await detailOf(noResourcesKey)).resources).toBeNull();
142
+ noResourcesKey.app.dispose();
143
+ });
144
+
145
+ it('T5 写侧保全不变量:MODULE_INTEGRATION_NODE_KEYS 白名单恒不含 resources(N2 写半边不变)', () => {
146
+ expect(MODULE_INTEGRATION_NODE_KEYS).toEqual(['provider', 'config']);
147
+ });
148
+ });
@@ -128,6 +128,20 @@ describe('module-schema(descriptor zod 单源)', () => {
128
128
  }
129
129
  });
130
130
 
131
+ it('hidden 展示元数据:boolean 可选(true/false 通过、缺席合法、非 boolean 拒——清单过滤语义归视图层)', () => {
132
+ const hidden = parseModuleDescriptor({ ...BASE_MANIFEST, hidden: true });
133
+ expect(hidden.ok).toBe(true);
134
+ if (hidden.ok) expect(hidden.value.hidden).toBe(true);
135
+ const unhidden = parseModuleDescriptor({ ...BASE_MANIFEST, hidden: false });
136
+ expect(unhidden.ok).toBe(true);
137
+ if (unhidden.ok) expect(unhidden.value.hidden).toBe(false);
138
+ const absent = moduleDescriptorSchema.safeParse(BASE_MANIFEST);
139
+ expect(absent.success).toBe(true);
140
+ if (absent.success) expect(absent.data.hidden).toBeUndefined();
141
+ const bad = parseModuleDescriptor({ ...BASE_MANIFEST, hidden: 'yes' });
142
+ expect(bad.ok).toBe(false);
143
+ });
144
+
131
145
  // ── v7 词汇统一:legacy 四象限矩阵(旧态/半迁移定向拒;v7 两形态放行)──
132
146
 
133
147
  it('v7 legacy ①:v6 旧态(name 标识 + title 展示、无 id)→ 定向拒 + legacy 标志 + 双键指引', () => {