@zhin.js/adapter 1.0.1 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -0
- package/lib/adapter-index.d.ts +8 -1
- package/lib/adapter-index.js +103 -24
- package/lib/definition.d.ts +43 -0
- package/lib/definition.js +42 -0
- package/lib/endpoint-commands.d.ts +127 -0
- package/lib/endpoint-commands.js +385 -0
- package/lib/endpoint-lifecycle.d.ts +68 -0
- package/lib/endpoint-lifecycle.js +333 -0
- package/lib/endpoint-management.d.ts +53 -0
- package/lib/endpoint-management.js +30 -0
- package/lib/index.d.ts +5 -0
- package/lib/index.js +5 -0
- package/lib/provider.js +1 -0
- package/package.json +7 -5
- package/src/adapter-index.ts +124 -22
- package/src/definition.ts +107 -0
- package/src/endpoint-commands.ts +559 -0
- package/src/endpoint-lifecycle.ts +422 -0
- package/src/endpoint-management.ts +86 -0
- package/src/index.ts +5 -0
- package/src/provider.ts +1 -0
package/src/adapter-index.ts
CHANGED
|
@@ -10,9 +10,14 @@ import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
|
10
10
|
import type {
|
|
11
11
|
AdapterCapability,
|
|
12
12
|
AdapterDefinition,
|
|
13
|
+
AdapterSegmentPolicy,
|
|
13
14
|
EndpointInstance,
|
|
14
15
|
EndpointSendRequest,
|
|
15
16
|
} from './definition.js';
|
|
17
|
+
import {
|
|
18
|
+
listEndpointManagementCapabilities,
|
|
19
|
+
type EndpointManagementCapability,
|
|
20
|
+
} from './endpoint-management.js';
|
|
16
21
|
|
|
17
22
|
const logger = getLogger('Adapter');
|
|
18
23
|
|
|
@@ -29,6 +34,7 @@ export interface AdapterEndpointSummary extends AdapterDescriptor {
|
|
|
29
34
|
readonly connected: boolean;
|
|
30
35
|
readonly status: 'online' | 'offline';
|
|
31
36
|
readonly phase: AdapterEndpointPhase;
|
|
37
|
+
readonly managementCapabilities: readonly EndpointManagementCapability[];
|
|
32
38
|
}
|
|
33
39
|
|
|
34
40
|
export type AdapterEndpointPhase =
|
|
@@ -36,6 +42,7 @@ export type AdapterEndpointPhase =
|
|
|
36
42
|
|
|
37
43
|
interface AdapterRecord extends AdapterDescriptor {
|
|
38
44
|
readonly endpoint: EndpointInstance;
|
|
45
|
+
readonly segments?: AdapterSegmentPolicy;
|
|
39
46
|
readonly unconfigured: boolean;
|
|
40
47
|
started: boolean;
|
|
41
48
|
open: boolean;
|
|
@@ -79,23 +86,28 @@ export class AdapterIndex {
|
|
|
79
86
|
const unconfigured: string[] = [];
|
|
80
87
|
try {
|
|
81
88
|
for (const slot of [...slots].sort((left, right) => left.id.localeCompare(right.id))) {
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
89
|
+
for (const expansion of expandEndpointConfigs(slot, snapshot)) {
|
|
90
|
+
const endpoint = await createEndpointSoft(slot, snapshot, expansion);
|
|
91
|
+
if (endpoint.unconfigured) unconfigured.push(expansion.name);
|
|
92
|
+
records.push({
|
|
93
|
+
id: expansion.id,
|
|
94
|
+
owner: slot.owner,
|
|
95
|
+
// 展开模式下 record name 即 endpoint 名(entry.name),
|
|
96
|
+
// 保证 Console 展示与 resolve/instance 按 entry name 命中唯一 record
|
|
97
|
+
name: expansion.name,
|
|
98
|
+
source: slot.source,
|
|
99
|
+
capabilities: slot.definition.capabilities,
|
|
100
|
+
endpoint: endpoint.instance,
|
|
101
|
+
...(slot.definition.segments ? { segments: slot.definition.segments } : {}),
|
|
102
|
+
unconfigured: endpoint.unconfigured,
|
|
103
|
+
started: false,
|
|
104
|
+
open: false,
|
|
105
|
+
failed: false,
|
|
106
|
+
startAttempted: false,
|
|
107
|
+
// Unconfigured stubs skip start/open so kitchen-sink Roots stay quiet.
|
|
108
|
+
stopped: endpoint.unconfigured,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
99
111
|
}
|
|
100
112
|
if (unconfigured.length > 0) {
|
|
101
113
|
logger.info(formatCompact({
|
|
@@ -118,7 +130,8 @@ export class AdapterIndex {
|
|
|
118
130
|
list(): readonly AdapterDescriptor[] {
|
|
119
131
|
return this.#order.map(({ endpoint: _endpoint, unconfigured: _unconfigured,
|
|
120
132
|
started: _started, open: _open, stopped: _stopped, failed: _failed,
|
|
121
|
-
startAttempted: _startAttempted,
|
|
133
|
+
startAttempted: _startAttempted, segments: _segments,
|
|
134
|
+
...descriptor }) => Object.freeze(descriptor));
|
|
122
135
|
}
|
|
123
136
|
|
|
124
137
|
/** Endpoint rows for Console `endpoint.list` / `endpoint.info`. */
|
|
@@ -133,6 +146,7 @@ export class AdapterIndex {
|
|
|
133
146
|
connected: record.open && !record.stopped,
|
|
134
147
|
status: record.open && !record.stopped ? 'online' as const : 'offline' as const,
|
|
135
148
|
phase: endpointPhase(record),
|
|
149
|
+
managementCapabilities: listEndpointManagementCapabilities(record.endpoint),
|
|
136
150
|
})));
|
|
137
151
|
}
|
|
138
152
|
|
|
@@ -165,6 +179,14 @@ export class AdapterIndex {
|
|
|
165
179
|
return record.owner;
|
|
166
180
|
}
|
|
167
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Endpoint 的消息段能力声明(出站协商降级依据);
|
|
184
|
+
* 未声明或未知 id 返回 undefined(调用方按历史行为处理)。
|
|
185
|
+
*/
|
|
186
|
+
segmentPolicy(id: CapabilityId): AdapterSegmentPolicy | undefined {
|
|
187
|
+
return this.#records.get(id)?.segments;
|
|
188
|
+
}
|
|
189
|
+
|
|
168
190
|
async start(): Promise<void> {
|
|
169
191
|
// Soft-start in parallel with a short wait so kitchen-sink Roots do not
|
|
170
192
|
// stall generation. Configured platforms that need longer (QQ auth, Slack
|
|
@@ -320,8 +342,13 @@ function matchesEndpoint(
|
|
|
320
342
|
adapter: string,
|
|
321
343
|
endpointId: string,
|
|
322
344
|
): boolean {
|
|
345
|
+
// 消息上的 $adapter 是 CapabilityId 的 localName 段(多 endpoint 展开后形如
|
|
346
|
+
// `icqq~8596238`)。CapabilityId 段分隔符是 \0(owner\0feature\0localName),
|
|
347
|
+
// 不能用 `/` 去 endsWith,否则永远匹配不上(endpoint not found)。
|
|
348
|
+
const localName = record.id.split('\0').pop() ?? record.id;
|
|
323
349
|
const adapterOk = record.name === adapter
|
|
324
350
|
|| record.id === adapter
|
|
351
|
+
|| localName === adapter
|
|
325
352
|
|| record.id.endsWith(`/${adapter}`)
|
|
326
353
|
|| record.owner === adapter
|
|
327
354
|
|| record.owner.endsWith(`/${adapter}`);
|
|
@@ -366,16 +393,91 @@ function isUnconfiguredError(error: unknown): boolean {
|
|
|
366
393
|
);
|
|
367
394
|
}
|
|
368
395
|
|
|
396
|
+
/** 单个实例配置展开的 endpoint 描述(多账号适配器经 `endpoints` 数组声明)。 */
|
|
397
|
+
interface EndpointExpansion {
|
|
398
|
+
readonly id: CapabilityId;
|
|
399
|
+
readonly name: string;
|
|
400
|
+
readonly config?: Readonly<Record<string, unknown>>;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* 实例配置的 endpoint 展开:插件实例 config 含非空 `endpoints: [{name, ...覆盖}]` 时
|
|
405
|
+
* 按数组一一创建 endpoint(基础配置为实例 config 去掉 `endpoints` 键,逐项合并),
|
|
406
|
+
* 否则按实例 config 创建单个 endpoint(历史行为)。
|
|
407
|
+
*/
|
|
408
|
+
function expandEndpointConfigs(
|
|
409
|
+
slot: Readonly<CapabilitySlot<AdapterDefinition>>,
|
|
410
|
+
snapshot: RuntimeSnapshot,
|
|
411
|
+
): readonly EndpointExpansion[] {
|
|
412
|
+
const config = snapshot.config.get(slot.owner) as
|
|
413
|
+
| { endpoints?: unknown }
|
|
414
|
+
| undefined;
|
|
415
|
+
const raw = config?.endpoints;
|
|
416
|
+
const entries = Array.isArray(raw)
|
|
417
|
+
? raw.filter((entry): entry is Record<string, unknown> & { name: string } =>
|
|
418
|
+
!!entry && typeof entry === 'object'
|
|
419
|
+
&& typeof (entry as { name?: unknown }).name === 'string'
|
|
420
|
+
&& (entry as { name: string }).name.length > 0)
|
|
421
|
+
: [];
|
|
422
|
+
if (entries.length === 0) {
|
|
423
|
+
if (Array.isArray(raw) && raw.length > 0) {
|
|
424
|
+
logger.warn(formatCompact({
|
|
425
|
+
op: 'adapter_endpoints_entries_dropped',
|
|
426
|
+
id: slot.id,
|
|
427
|
+
reason: 'every endpoints entry is missing a non-empty string name',
|
|
428
|
+
}));
|
|
429
|
+
}
|
|
430
|
+
return Object.freeze([{ id: slot.id, name: slot.localName }]);
|
|
431
|
+
}
|
|
432
|
+
// `~` 是 record id 的分隔符、\0 是 CapabilityId 的分隔符,混入会破坏解析
|
|
433
|
+
const valid = entries.filter((entry) => {
|
|
434
|
+
if (/[~\0]/u.test(entry.name)) {
|
|
435
|
+
logger.warn(formatCompact({
|
|
436
|
+
op: 'adapter_endpoint_name_invalid',
|
|
437
|
+
id: slot.id,
|
|
438
|
+
name: entry.name,
|
|
439
|
+
}));
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
return true;
|
|
443
|
+
});
|
|
444
|
+
// 重名会让 #records 覆盖与 #order/resolve 三者不一致;保留首个并告警
|
|
445
|
+
const seen = new Set<string>();
|
|
446
|
+
const deduped = valid.filter((entry) => {
|
|
447
|
+
if (seen.has(entry.name)) {
|
|
448
|
+
logger.warn(formatCompact({
|
|
449
|
+
op: 'adapter_endpoint_name_duplicate',
|
|
450
|
+
id: slot.id,
|
|
451
|
+
name: entry.name,
|
|
452
|
+
}));
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
seen.add(entry.name);
|
|
456
|
+
return true;
|
|
457
|
+
});
|
|
458
|
+
if (deduped.length === 0) {
|
|
459
|
+
return Object.freeze([{ id: slot.id, name: slot.localName }]);
|
|
460
|
+
}
|
|
461
|
+
const { endpoints: _drop, ...base } = (config ?? {}) as Record<string, unknown>;
|
|
462
|
+
return Object.freeze(deduped.map((entry) => Object.freeze({
|
|
463
|
+
id: `${slot.id}~${entry.name}` as CapabilityId,
|
|
464
|
+
name: entry.name,
|
|
465
|
+
config: Object.freeze({ ...base, ...entry, name: entry.name }),
|
|
466
|
+
})));
|
|
467
|
+
}
|
|
468
|
+
|
|
369
469
|
async function createEndpointSoft(
|
|
370
470
|
slot: Readonly<CapabilitySlot<AdapterDefinition>>,
|
|
371
471
|
snapshot: RuntimeSnapshot,
|
|
472
|
+
expansion?: EndpointExpansion,
|
|
372
473
|
): Promise<{ readonly instance: EndpointInstance; readonly unconfigured: boolean }> {
|
|
373
474
|
let endpoint: unknown;
|
|
374
475
|
try {
|
|
375
476
|
endpoint = await slot.definition.create(
|
|
376
477
|
Object.freeze({
|
|
377
478
|
...createCapabilityContext(snapshot, slot.owner),
|
|
378
|
-
|
|
479
|
+
...(expansion?.config ? { config: expansion.config } : {}),
|
|
480
|
+
id: expansion?.id ?? slot.id,
|
|
379
481
|
name: slot.localName,
|
|
380
482
|
}),
|
|
381
483
|
);
|
|
@@ -388,8 +490,8 @@ async function createEndpointSoft(
|
|
|
388
490
|
const log = isUnconfiguredError(error) ? logger.debug.bind(logger) : logger.warn.bind(logger);
|
|
389
491
|
log(formatCompact({
|
|
390
492
|
op: 'adapter_create_soft_fail',
|
|
391
|
-
id: slot.id,
|
|
392
|
-
name: slot.localName,
|
|
493
|
+
id: expansion?.id ?? slot.id,
|
|
494
|
+
name: expansion?.name ?? slot.localName,
|
|
393
495
|
error: message,
|
|
394
496
|
}));
|
|
395
497
|
return {
|
|
@@ -400,7 +502,7 @@ async function createEndpointSoft(
|
|
|
400
502
|
// Programming errors (create() did not return an Endpoint) must surface:
|
|
401
503
|
// they propagate to AdapterIndex.create's catch, which disposes the records
|
|
402
504
|
// created so far instead of hiding the bug behind an unconfigured stub.
|
|
403
|
-
assertEndpoint(endpoint, slot.id);
|
|
505
|
+
assertEndpoint(endpoint, expansion?.id ?? slot.id);
|
|
404
506
|
return { instance: endpoint, unconfigured: false };
|
|
405
507
|
}
|
|
406
508
|
|
package/src/definition.ts
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import type { CapabilityId } from '@zhin.js/plugin-runtime';
|
|
2
2
|
import type { CapabilityContext } from '@zhin.js/feature-kit';
|
|
3
|
+
import type { EndpointManagement } from './endpoint-management.js';
|
|
3
4
|
|
|
4
5
|
const adapterBrand = 'zhin.adapter/1' as const;
|
|
5
6
|
|
|
6
7
|
export type AdapterCapability = 'inbound' | 'outbound';
|
|
7
8
|
|
|
9
|
+
/** 端点可消费的出站媒体来源形式。 */
|
|
10
|
+
export type AdapterOutboundMedia = 'url' | 'path' | 'base64' | 'upload';
|
|
11
|
+
|
|
12
|
+
/** 交互段(卡片/按钮等富交互)的端点消费方式。 */
|
|
13
|
+
export type AdapterInteractiveMode = 'native' | 'text';
|
|
14
|
+
|
|
8
15
|
export interface EndpointSendRequest {
|
|
9
16
|
readonly target: string;
|
|
10
17
|
readonly payload: unknown;
|
|
@@ -12,6 +19,8 @@ export interface EndpointSendRequest {
|
|
|
12
19
|
}
|
|
13
20
|
|
|
14
21
|
export interface EndpointInstance<TResult = unknown> {
|
|
22
|
+
/** Optional platform-neutral Console/Host management surface. */
|
|
23
|
+
readonly management?: EndpointManagement;
|
|
15
24
|
/** Allocates transport resources but must not admit inbound events yet. */
|
|
16
25
|
start?(): void | Promise<void>;
|
|
17
26
|
/** Opens admission after the candidate generation has committed. */
|
|
@@ -28,14 +37,62 @@ export interface AdapterContext<TConfig = unknown> extends CapabilityContext<TCo
|
|
|
28
37
|
readonly name: string;
|
|
29
38
|
}
|
|
30
39
|
|
|
40
|
+
/**
|
|
41
|
+
* html 段出站策略:
|
|
42
|
+
* - `direct`:端点直接消费 html 段(Console UI 类端点),核心不做任何转换;
|
|
43
|
+
* - `image`:经 html-renderer 渲染成 image 段,无渲染器时降级 text(缺省);
|
|
44
|
+
* - `text`:直接降级为 text 段。
|
|
45
|
+
*/
|
|
46
|
+
export type HtmlOutboundMode = 'direct' | 'image' | 'text';
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 端点消息段能力声明(出站协商降级的依据)。
|
|
50
|
+
* 缺省(未声明 `segments`)保持历史行为:仅 html 段按 image/text 处理,
|
|
51
|
+
* 其余段原样透传给端点。
|
|
52
|
+
*/
|
|
53
|
+
export interface AdapterSegmentPolicy {
|
|
54
|
+
/**
|
|
55
|
+
* 端点原生可消费的 wire 段类型(如 `['text', 'image', 'at']`)。
|
|
56
|
+
* 声明后,未列出的段由核心按 `formatSegmentPreview` 降级为 text 段;
|
|
57
|
+
* 不声明则不过滤(全部透传)。
|
|
58
|
+
*/
|
|
59
|
+
readonly supported?: readonly string[];
|
|
60
|
+
/** html 段处理策略,缺省 `image`。 */
|
|
61
|
+
readonly html?: HtmlOutboundMode;
|
|
62
|
+
/** 端点可消费的媒体来源形式;缺省表示不做媒体来源协商。 */
|
|
63
|
+
readonly outboundMedia?: readonly AdapterOutboundMedia[];
|
|
64
|
+
/**
|
|
65
|
+
* 交互段(卡片/按钮等富交互)消费方式:`native` 原生渲染 / `text` 降级纯文本。
|
|
66
|
+
* 目前仅为声明(供出站协商与门禁消费),`text` 的降级执行随 Wave 2 落地。
|
|
67
|
+
*/
|
|
68
|
+
readonly interactive?: AdapterInteractiveMode;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const HTML_OUTBOUND_MODES: readonly HtmlOutboundMode[] = ['direct', 'image', 'text'];
|
|
72
|
+
|
|
73
|
+
const OUTBOUND_MEDIA_FORMS: readonly AdapterOutboundMedia[] = [
|
|
74
|
+
'url', 'path', 'base64', 'upload',
|
|
75
|
+
];
|
|
76
|
+
|
|
31
77
|
export interface AdapterDefinition<TConfig = unknown, TResult = unknown> {
|
|
32
78
|
readonly $feature: typeof adapterBrand;
|
|
33
79
|
readonly capabilities: readonly AdapterCapability[];
|
|
80
|
+
/** 可选:端点消息段能力声明(出站协商降级挂载点)。 */
|
|
81
|
+
readonly segments?: AdapterSegmentPolicy;
|
|
34
82
|
create(
|
|
35
83
|
context: AdapterContext<TConfig>,
|
|
36
84
|
): EndpointInstance<TResult> | Promise<EndpointInstance<TResult>>;
|
|
37
85
|
}
|
|
38
86
|
|
|
87
|
+
declare module '@zhin.js/plugin-runtime' {
|
|
88
|
+
interface PluginSetupContext<TConfig> {
|
|
89
|
+
addAdapter<TResult = unknown>(
|
|
90
|
+
localName: string,
|
|
91
|
+
definition: AdapterDefinition<TConfig, TResult>,
|
|
92
|
+
): void;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
39
96
|
export function defineAdapter<TConfig = unknown, TResult = unknown>(
|
|
40
97
|
definition: Omit<AdapterDefinition<TConfig, TResult>, '$feature'>,
|
|
41
98
|
): Readonly<AdapterDefinition<TConfig, TResult>> {
|
|
@@ -49,10 +106,58 @@ export function defineAdapter<TConfig = unknown, TResult = unknown>(
|
|
|
49
106
|
) {
|
|
50
107
|
throw new TypeError('Adapter capabilities must contain inbound and/or outbound');
|
|
51
108
|
}
|
|
109
|
+
const segments = normalizeSegmentPolicy(definition.segments);
|
|
52
110
|
return Object.freeze({
|
|
53
111
|
...definition,
|
|
54
112
|
$feature: adapterBrand,
|
|
55
113
|
capabilities: Object.freeze(capabilities),
|
|
114
|
+
...(segments ? { segments } : {}),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function normalizeSegmentPolicy(
|
|
119
|
+
policy: AdapterSegmentPolicy | undefined,
|
|
120
|
+
): AdapterSegmentPolicy | undefined {
|
|
121
|
+
if (policy === undefined) return undefined;
|
|
122
|
+
if (!policy || typeof policy !== 'object') {
|
|
123
|
+
throw new TypeError('Adapter segments policy must be an object');
|
|
124
|
+
}
|
|
125
|
+
if (
|
|
126
|
+
policy.supported !== undefined
|
|
127
|
+
&& (!Array.isArray(policy.supported)
|
|
128
|
+
|| policy.supported.some((type) => typeof type !== 'string' || !type))
|
|
129
|
+
) {
|
|
130
|
+
throw new TypeError('Adapter segments.supported must be an array of segment type names');
|
|
131
|
+
}
|
|
132
|
+
if (policy.html !== undefined && !HTML_OUTBOUND_MODES.includes(policy.html)) {
|
|
133
|
+
throw new TypeError('Adapter segments.html must be direct, image or text');
|
|
134
|
+
}
|
|
135
|
+
if (
|
|
136
|
+
policy.outboundMedia !== undefined
|
|
137
|
+
&& (!Array.isArray(policy.outboundMedia)
|
|
138
|
+
|| policy.outboundMedia.length === 0
|
|
139
|
+
|| policy.outboundMedia.some(
|
|
140
|
+
(form) => !OUTBOUND_MEDIA_FORMS.includes(form as AdapterOutboundMedia),
|
|
141
|
+
))
|
|
142
|
+
) {
|
|
143
|
+
throw new TypeError(
|
|
144
|
+
"Adapter segments.outboundMedia must be a non-empty array of 'url' | 'path' | 'base64' | 'upload'",
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
if (
|
|
148
|
+
policy.interactive !== undefined
|
|
149
|
+
&& policy.interactive !== 'native'
|
|
150
|
+
&& policy.interactive !== 'text'
|
|
151
|
+
) {
|
|
152
|
+
throw new TypeError("Adapter segments.interactive must be 'native' or 'text'");
|
|
153
|
+
}
|
|
154
|
+
return Object.freeze({
|
|
155
|
+
...(policy.supported ? { supported: Object.freeze([...new Set(policy.supported)]) } : {}),
|
|
156
|
+
...(policy.html ? { html: policy.html } : {}),
|
|
157
|
+
...(policy.outboundMedia
|
|
158
|
+
? { outboundMedia: Object.freeze([...new Set(policy.outboundMedia)]) }
|
|
159
|
+
: {}),
|
|
160
|
+
...(policy.interactive ? { interactive: policy.interactive } : {}),
|
|
56
161
|
});
|
|
57
162
|
}
|
|
58
163
|
|
|
@@ -68,6 +173,8 @@ export function parseAdapterDefinition(value: unknown): AdapterDefinition {
|
|
|
68
173
|
(capability) => capability !== 'inbound' && capability !== 'outbound',
|
|
69
174
|
)
|
|
70
175
|
) throw invalidAdapter();
|
|
176
|
+
// defineAdapter 已校验过形状;外部手工构造的 definition 也在此兜底
|
|
177
|
+
normalizeSegmentPolicy(definition.segments);
|
|
71
178
|
return definition as AdapterDefinition;
|
|
72
179
|
}
|
|
73
180
|
|