@twinsearth/dsh-master 3.3.0 → 3.3.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 +2 -1
- package/client.js +1 -1
- package/dist-host/contract/client.js +7 -0
- package/dist-host/contract/constants.js +16 -2
- package/dist-host/contract/index.js +1 -0
- package/dist-host/contract/relay.js +239 -0
- package/dist-host/cost/fang-relay.js +329 -0
- package/dist-host/cost/index.js +35 -2
- package/dist-host/cost/milestones.js +44 -6
- package/dist-host/kernel.js +1 -1
- package/dist-host/plugins/cost-optimizer.js +57 -5
- package/dist-host/plugins/superagent-router.js +18 -2
- package/docs/ARCHITECTURE.md +8 -5
- package/docs/CHANGELOG.md +16 -0
- package/docs/DESIGN-v3.3.0-cost-optimization.md +24 -19
- package/docs/EXPERIMENT-M5-fang-relay.md +117 -0
- package/docs/EXPERIMENT-M5-real-ab.md +6 -1
- package/package.json +2 -2
- package/src/contract/client.js +7 -0
- package/src/contract/constants.js +17 -2
- package/src/contract/index.js +1 -0
- package/src/contract/relay.js +223 -0
- package/src/cost/fang-relay.ts +318 -0
- package/src/cost/index.ts +34 -2
- package/src/cost/milestones.ts +41 -5
- package/src/kernel.ts +1 -1
- package/src/plugins/cost-optimizer.ts +48 -6
- package/src/plugins/superagent-router.ts +16 -2
package/src/cost/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { createAuditChain } from './audit.ts'
|
|
|
17
17
|
import { createAttribution } from './attribution.ts'
|
|
18
18
|
import { createCostBus } from './bus.ts'
|
|
19
19
|
import { createHostSurfaceAdapter } from './host-surface.ts'
|
|
20
|
+
import { createFangRelay } from './fang-relay.ts'
|
|
20
21
|
import { buildSavingsReport, classifyEntry } from './report.ts'
|
|
21
22
|
import { buildMetricsReport, METRIC_KEYS, SUCCESS_REGRESSION_PP } from './metrics.ts'
|
|
22
23
|
import { MODULE_MANIFESTS, MODULE_IDS, validateManifests } from './modules/manifests.ts'
|
|
@@ -24,8 +25,9 @@ import { buildLedgerReport, formatLedgerReport, parseLedgerJsonl } from './ledge
|
|
|
24
25
|
import { buildToolFaceReport, planToolFace } from './toolface.ts'
|
|
25
26
|
import { buildReuseReport, formatReuseReport } from './reuse-report.ts'
|
|
26
27
|
|
|
27
|
-
export { createLedger, createBudget, createPolicy, createAuditChain, createAttribution, createCostBus, createHostSurfaceAdapter, buildSavingsReport, classifyEntry, buildMetricsReport, METRIC_KEYS, SUCCESS_REGRESSION_PP, MODULE_MANIFESTS, MODULE_IDS, validateManifests, buildLedgerReport, formatLedgerReport, parseLedgerJsonl, buildToolFaceReport, planToolFace, buildReuseReport, formatReuseReport }
|
|
28
|
+
export { createLedger, createBudget, createPolicy, createAuditChain, createAttribution, createCostBus, createHostSurfaceAdapter, createFangRelay, buildSavingsReport, classifyEntry, buildMetricsReport, METRIC_KEYS, SUCCESS_REGRESSION_PP, MODULE_MANIFESTS, MODULE_IDS, validateManifests, buildLedgerReport, formatLedgerReport, parseLedgerJsonl, buildToolFaceReport, planToolFace, buildReuseReport, formatReuseReport }
|
|
28
29
|
export * from './types.ts'
|
|
30
|
+
export { RELAY_PEERS, RELAY_DEFAULTS, createFangRelayFromContext, createRelayClient } from './fang-relay.ts'
|
|
29
31
|
|
|
30
32
|
/**
|
|
31
33
|
* @param {{
|
|
@@ -34,6 +36,7 @@ export * from './types.ts'
|
|
|
34
36
|
* budgetLimits?: Array<{unit:string, limitTokens?:number, limitCost?:number, onExhaust?:string, degradeTo?:string}>,
|
|
35
37
|
* degradeModels?: string[],
|
|
36
38
|
* now?: () => number, maxEntries?: number, maxRecords?: number,
|
|
39
|
+
* relay?: object, relayClient?: object|null, relayOptions?: object, relayAutoAttach?: boolean,
|
|
37
40
|
* }} [options]
|
|
38
41
|
*/
|
|
39
42
|
export function createCostSystem(options = {}) {
|
|
@@ -50,6 +53,18 @@ export function createCostSystem(options = {}) {
|
|
|
50
53
|
})
|
|
51
54
|
const attribution = createAttribution({ ledger, now })
|
|
52
55
|
const bus = createCostBus({ now, maxEvents: options.maxBusEvents, relays: options.relays })
|
|
56
|
+
/**
|
|
57
|
+
* §5.6 / M5:瓮城回流出口。
|
|
58
|
+
* 传入 `relay`(已有适配器)则复用;否则用 `relayClient` 现造一个。
|
|
59
|
+
* 没有客户端时出口状态是 `relay-client-absent` —— 显式不可用,不是“探测中”。
|
|
60
|
+
*/
|
|
61
|
+
const relay = options.relay && typeof options.relay.status === 'function'
|
|
62
|
+
? options.relay
|
|
63
|
+
: createFangRelay({ client: options.relayClient ?? null, now, ...(options.relayOptions && typeof options.relayOptions === 'object' ? options.relayOptions : {}), logger: options.logger })
|
|
64
|
+
// 默认把出口挂到总线;测试可关掉自动挂载,手动投递。
|
|
65
|
+
if (options.relayAutoAttach !== false && typeof relay.attach === 'function') {
|
|
66
|
+
try { relay.attach(bus) } catch { /* 回流挂载失败不影响记账 */ }
|
|
67
|
+
}
|
|
53
68
|
/** 宿主会话工具面适配器(可选;默认关闭,不依赖任何第三方插件)。 */
|
|
54
69
|
const hostSurface = createHostSurfaceAdapter({
|
|
55
70
|
ledger,
|
|
@@ -270,6 +285,21 @@ export function createCostSystem(options = {}) {
|
|
|
270
285
|
return bus.subscribe(listener)
|
|
271
286
|
}
|
|
272
287
|
|
|
288
|
+
/** §5.6:探测回流对端(hello 协商 + relay 状态),结果并入 `status().relay`。 */
|
|
289
|
+
async function relayProbe(input = {}) {
|
|
290
|
+
return await relay.probe({ force: input.force === true })
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** §5.6:投递当前队列里的回流事件;返回回执摘要(不吞错,不谎报送达)。 */
|
|
294
|
+
async function relayFlush() {
|
|
295
|
+
if (typeof relay.flush !== 'function') return { ok: false, code: 'relay-flush-unsupported' }
|
|
296
|
+
return await relay.flush()
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function relayStatus() {
|
|
300
|
+
return relay.status()
|
|
301
|
+
}
|
|
302
|
+
|
|
273
303
|
function status() {
|
|
274
304
|
const ledgerSnapshot = ledger.snapshot()
|
|
275
305
|
const auditVerify = audit.verify()
|
|
@@ -293,6 +323,8 @@ export function createCostSystem(options = {}) {
|
|
|
293
323
|
unattributedProjectedTotal: attributionSnapshot.unattributedProjectedTotal,
|
|
294
324
|
},
|
|
295
325
|
bus: { count: busSnapshot.count, dropped: busSnapshot.dropped, subscribers: busSnapshot.subscribers, relays: busSnapshot.relays },
|
|
326
|
+
/** §5.6 / M5:瓮城回流出口的真实状态(可达则 available:true + 对端自报 peer)。 */
|
|
327
|
+
relay: relay.status(),
|
|
296
328
|
degraded: {
|
|
297
329
|
modules: [...degradedByModule.values()].map((item) => ({ ...item, phases: { ...item.phases } })),
|
|
298
330
|
occurrences: [...degradedByModule.values()].reduce((sum, item) => sum + item.occurrences, 0),
|
|
@@ -312,5 +344,5 @@ export function createCostSystem(options = {}) {
|
|
|
312
344
|
})
|
|
313
345
|
}
|
|
314
346
|
|
|
315
|
-
return Object.freeze({ ledger, budget, policy, audit, attribution, bus, hostSurface, recordUsage, authorize, attribute, attributeTotal, substitute, trace, degraded, query, replay, savingsReport, metrics, ledgerReport, subscribe, status })
|
|
347
|
+
return Object.freeze({ ledger, budget, policy, audit, attribution, bus, hostSurface, relay, recordUsage, authorize, attribute, attributeTotal, substitute, trace, degraded, query, replay, savingsReport, metrics, ledgerReport, subscribe, status, relayProbe, relayFlush, relayStatus })
|
|
316
348
|
}
|
package/src/cost/milestones.ts
CHANGED
|
@@ -28,6 +28,13 @@ export const M3_MIN_SAMPLES = 20
|
|
|
28
28
|
/** M4 放行前要求的最小守卫准确率(与 ENFORCE_GATE.minAccuracy 一致)。 */
|
|
29
29
|
export const M3_MIN_ACCURACY = 0.98
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* M5 回流放行条件:至少 3 条事件拿到对端回执,且对端自报身份在白名单内。
|
|
33
|
+
* 自环(同一进程里服务端+客户端)与未知 peer 一律不算达标。
|
|
34
|
+
*/
|
|
35
|
+
export const M5_MIN_RELAY_EVENTS = 3
|
|
36
|
+
export const M5_RELAY_PEERS = Object.freeze(['dsh-fang-bundle', 'contract-server'])
|
|
37
|
+
|
|
31
38
|
export const MILESTONES = Object.freeze([
|
|
32
39
|
Object.freeze({
|
|
33
40
|
id: 'M1',
|
|
@@ -79,13 +86,16 @@ export const MILESTONES = Object.freeze([
|
|
|
79
86
|
* files?: string[], // 仓库里实际存在的文件(相对路径)
|
|
80
87
|
* guard?: {samples?: number, accuracy?: number}, // M3 的守卫准确率数据(来自 shadow 报告)
|
|
81
88
|
* forkExperiment?: {ok?: boolean}, // M4 的成功率对照(来自 gate 的 G5)
|
|
82
|
-
* fangRelay?: {available?: boolean
|
|
89
|
+
* fangRelay?: {available?: boolean, peer?: string, events?: number,
|
|
90
|
+
* sameProcess?: boolean, transport?: string, contractVersion?: string,
|
|
91
|
+
* reason?: string}, // M5 的回流对端证据(独立进程实测)
|
|
83
92
|
* }} evidence
|
|
84
93
|
*/
|
|
85
94
|
export function evaluateMilestones(evidence = {}) {
|
|
86
95
|
const files = new Set(Array.isArray(evidence.files) ? evidence.files : [])
|
|
87
96
|
const rows = []
|
|
88
97
|
const done = new Set()
|
|
98
|
+
let relayEvidence = null
|
|
89
99
|
|
|
90
100
|
for (const milestone of MILESTONES) {
|
|
91
101
|
const missingImplementation = milestone.implementation.filter((file) => !files.has(file))
|
|
@@ -118,10 +128,30 @@ export function evaluateMilestones(evidence = {}) {
|
|
|
118
128
|
if (evidence?.forkExperiment?.ok === true) effect = 'verified'
|
|
119
129
|
else reasons.push('effect-needs-fork-success-comparison')
|
|
120
130
|
} else if (milestone.id === 'M5') {
|
|
121
|
-
//
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
131
|
+
// 回流必须有**对端回执**才算达标:可用 + 非自环 + 对端自报身份在白名单 + 至少 3 条事件。
|
|
132
|
+
const relay = evidence?.fangRelay && typeof evidence.fangRelay === 'object' ? evidence.fangRelay : null
|
|
133
|
+
const available = relay?.available === true
|
|
134
|
+
const peer = String(relay?.peer ?? '')
|
|
135
|
+
const events = Math.max(0, Number(relay?.events ?? 0) || 0)
|
|
136
|
+
if (!available) {
|
|
137
|
+
reasons.push(`fang-relay-unavailable:${String(relay?.reason ?? 'not-provided')}`)
|
|
138
|
+
} else if (relay?.sameProcess === true) {
|
|
139
|
+
reasons.push('fang-relay-self-loop')
|
|
140
|
+
} else if (!M5_RELAY_PEERS.includes(peer)) {
|
|
141
|
+
reasons.push(`fang-relay-peer-invalid:${peer || 'missing'}`)
|
|
142
|
+
} else if (events < M5_MIN_RELAY_EVENTS) {
|
|
143
|
+
reasons.push(`fang-relay-no-receipts(events=${events})`)
|
|
144
|
+
} else {
|
|
145
|
+
effect = 'verified'
|
|
146
|
+
}
|
|
147
|
+
relayEvidence = Object.freeze({
|
|
148
|
+
available,
|
|
149
|
+
peer: peer || null,
|
|
150
|
+
events,
|
|
151
|
+
sameProcess: relay?.sameProcess === true,
|
|
152
|
+
transport: relay?.transport ?? null,
|
|
153
|
+
contractVersion: relay?.contractVersion ?? relay?.peerContractVersion ?? null,
|
|
154
|
+
})
|
|
125
155
|
}
|
|
126
156
|
}
|
|
127
157
|
|
|
@@ -141,6 +171,12 @@ export function evaluateMilestones(evidence = {}) {
|
|
|
141
171
|
effect,
|
|
142
172
|
status,
|
|
143
173
|
reasons,
|
|
174
|
+
/** M5:回流对端证据(peer 自报 + 事件数 + 是否自环);其它阶段为 null。 */
|
|
175
|
+
relay: relayEvidence,
|
|
176
|
+
/** 剩余缺口说明(不隐藏)。 */
|
|
177
|
+
notes: milestone.id === 'M5' && relayEvidence?.peer === 'contract-server'
|
|
178
|
+
? ['peer=contract-server:契约对端独立进程实测,dsh-fang-bundle 采用 contract 1.1 后应变为 peer=dsh-fang-bundle']
|
|
179
|
+
: [],
|
|
144
180
|
}))
|
|
145
181
|
if (status === 'done') done.add(milestone.id)
|
|
146
182
|
}
|
package/src/kernel.ts
CHANGED
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
roleAllowed,
|
|
29
29
|
safeGet,
|
|
30
30
|
} from '../kernel.ts'
|
|
31
|
-
import { createCostSystem, DEFAULT_BUDGET_LIMITS } from '../cost/index.ts'
|
|
31
|
+
import { createCostSystem, createFangRelayFromContext, DEFAULT_BUDGET_LIMITS } from '../cost/index.ts'
|
|
32
32
|
|
|
33
33
|
export const name = 'dsh-master-cost-optimizer'
|
|
34
34
|
export const inject = ['dshMaster']
|
|
@@ -53,6 +53,10 @@ function finiteNonNegative(value) {
|
|
|
53
53
|
* enabled?: boolean, mode?: string, targetSavingsRatio?: number,
|
|
54
54
|
* budget?: Array<{unit: string, limitTokens: number, onExhaust?: string}>,
|
|
55
55
|
* enforceAllowlist?: string[],
|
|
56
|
+
* relay?: {enabled?: boolean, httpBaseUrl?: string, httpToken?: string, fileDir?: string,
|
|
57
|
+
* timeoutMs?: number, maxBatch?: number, maxQueue?: number, probeTtlMs?: number,
|
|
58
|
+
* autoFlushMs?: number},
|
|
59
|
+
* dispatch?: {motherUrl?: string, motherToken?: string, contractFileDir?: string},
|
|
56
60
|
* }} [config]
|
|
57
61
|
*/
|
|
58
62
|
export function apply(ctx, config = {}) {
|
|
@@ -70,6 +74,12 @@ export function apply(ctx, config = {}) {
|
|
|
70
74
|
}
|
|
71
75
|
|
|
72
76
|
const targetSavingsRatio = finiteNonNegative(config.targetSavingsRatio)
|
|
77
|
+
/**
|
|
78
|
+
* §5.6 / M5:瓮城回流出口。
|
|
79
|
+
* 传输按「插件配置 relay → dispatch 配置 → 环境变量 DSH_FANG_*」装配;
|
|
80
|
+
* 一个都没有时不造客户端,出口状态显式 `relay-client-absent`。
|
|
81
|
+
*/
|
|
82
|
+
const relayWiring = createFangRelayFromContext(ctx, config, { logger: ctx?.logger })
|
|
73
83
|
const system = createCostSystem({
|
|
74
84
|
mode: config.mode,
|
|
75
85
|
allowlist: config.enforceAllowlist ?? config.allowlist,
|
|
@@ -80,9 +90,21 @@ export function apply(ctx, config = {}) {
|
|
|
80
90
|
hostSurface: config.hostSurface && typeof config.hostSurface === 'object'
|
|
81
91
|
? { enabled: config.hostSurface.enabled === true, source: config.hostSurface.source }
|
|
82
92
|
: undefined,
|
|
93
|
+
relay: relayWiring.relay,
|
|
94
|
+
logger: ctx?.logger,
|
|
83
95
|
})
|
|
84
96
|
const policy = system.policy.describe()
|
|
85
97
|
|
|
98
|
+
// M5:加载后异步探测一次回流对端(不阻塞 apply;没有客户端时立即返回 absent)。
|
|
99
|
+
try { void Promise.resolve(system.relayProbe()).catch(() => {}) } catch { /* 探测失败不影响主流程 */ }
|
|
100
|
+
/** M5:回流自动投递;没有客户端或 autoFlushMs=0 时不开定时器。 */
|
|
101
|
+
const autoFlushMs = Math.max(0, Number(config.relay?.autoFlushMs ?? 2_000) || 0)
|
|
102
|
+
let relayTimer = null
|
|
103
|
+
if (relayWiring.client && autoFlushMs > 0) {
|
|
104
|
+
relayTimer = setInterval(() => { try { void system.relayFlush().catch(() => {}) } catch { /* ignore */ } }, autoFlushMs)
|
|
105
|
+
try { relayTimer.unref?.() } catch { /* ignore */ }
|
|
106
|
+
}
|
|
107
|
+
|
|
86
108
|
// 3.2.x 兼容计数器(任务级实测汇总;系统层另有账本,两者口径不同、互不覆盖)。
|
|
87
109
|
const metrics = {
|
|
88
110
|
tasks: 0,
|
|
@@ -95,7 +117,7 @@ export function apply(ctx, config = {}) {
|
|
|
95
117
|
|
|
96
118
|
const service = {
|
|
97
119
|
id: SERVICE.cost,
|
|
98
|
-
version: '3.3.
|
|
120
|
+
version: '3.3.1',
|
|
99
121
|
role,
|
|
100
122
|
get mode() {
|
|
101
123
|
return system.policy.describe().mode
|
|
@@ -315,12 +337,24 @@ export function apply(ctx, config = {}) {
|
|
|
315
337
|
substitutions(filter = {}) {
|
|
316
338
|
return system.audit.substitutions(filter)
|
|
317
339
|
},
|
|
340
|
+
/** M5:瓮城回流出口状态(只读;不可用时必须带具体原因)。 */
|
|
341
|
+
relayStatus() {
|
|
342
|
+
return system.relayStatus()
|
|
343
|
+
},
|
|
344
|
+
/** M5:立即探测回流对端(hello 协商 + relay 状态),返回最新状态。 */
|
|
345
|
+
async relayProbe(input = {}) {
|
|
346
|
+
return await system.relayProbe(input)
|
|
347
|
+
},
|
|
348
|
+
/** M5:立即投递队列中的回流事件;回执里的数字才是送达证据。 */
|
|
349
|
+
async relayFlush() {
|
|
350
|
+
return await system.relayFlush()
|
|
351
|
+
},
|
|
318
352
|
status() {
|
|
319
353
|
const current = system.policy.describe()
|
|
320
354
|
return {
|
|
321
355
|
ok: true,
|
|
322
356
|
id: 'cost-optimizer',
|
|
323
|
-
version: '3.3.
|
|
357
|
+
version: '3.3.1',
|
|
324
358
|
role,
|
|
325
359
|
mode: current.mode,
|
|
326
360
|
requestedMode: current.requestedMode,
|
|
@@ -340,8 +374,10 @@ export function apply(ctx, config = {}) {
|
|
|
340
374
|
attribution: system.attribution.snapshot(),
|
|
341
375
|
attributionSummary: system.status().attributionSummary,
|
|
342
376
|
bus: system.bus.snapshot(1),
|
|
343
|
-
|
|
344
|
-
|
|
377
|
+
/** §5.6 / M5:瓮城回流出口(可达 ⇒ available:true + 对端自报 peer;否则带原因)。 */
|
|
378
|
+
relay: system.relayStatus(),
|
|
379
|
+
/** §5.6:内部事件类型(冒号命名)与对外出口状态。 */
|
|
380
|
+
events: { types: ['cost:event', 'cost:degraded', 'cost:substitution'], relays: system.bus.snapshot(1).relays },
|
|
345
381
|
degraded: system.status().degraded,
|
|
346
382
|
surfaces: system.status().surfaces,
|
|
347
383
|
hostSurface: system.hostSurface.status(),
|
|
@@ -360,12 +396,18 @@ export function apply(ctx, config = {}) {
|
|
|
360
396
|
doubleCountGuard: report.doubleCountGuard,
|
|
361
397
|
}
|
|
362
398
|
})(),
|
|
363
|
-
note: '3.3.
|
|
399
|
+
note: '3.3.1:系统层记账/预算/灰度/审计 + A1 工具输出紧凑化(默认 shadow)+ 瓮城回流出口(contract 1.1 真实投递);工具面 A2–A3 与决策复用 B1–B4 已接入(shadow 默认)。',
|
|
364
400
|
}
|
|
365
401
|
},
|
|
366
402
|
}
|
|
367
403
|
|
|
368
404
|
try { ctx.effect(() => ctx.provide(SERVICE.cost, service)) } catch { /* fake ctx */ }
|
|
405
|
+
try {
|
|
406
|
+
ctx.effect(() => () => {
|
|
407
|
+
if (relayTimer) clearInterval(relayTimer)
|
|
408
|
+
relayTimer = null
|
|
409
|
+
})
|
|
410
|
+
} catch { /* fake ctx */ }
|
|
369
411
|
// 3.3.0 §11:可选子服务(主服务形态不变;消费方必须探测,不能假定存在)。
|
|
370
412
|
const optionalFaces = {
|
|
371
413
|
[OPTIONAL_SERVICE.costLedger]: Object.freeze({
|
|
@@ -312,6 +312,20 @@ export function apply(ctx, config = {}) {
|
|
|
312
312
|
} catch { return null }
|
|
313
313
|
})()
|
|
314
314
|
const tickets = id === null ? [...workerEvents.keys()] : [id]
|
|
315
|
+
/**
|
|
316
|
+
* §5.6 / M5:回流出口状态来自成本服务(3.3.1 起是真实探测,不再是硬编码)。
|
|
317
|
+
* 没有成本服务时如实给 `cost-service-unavailable`,不编造“未就绪”原因。
|
|
318
|
+
*/
|
|
319
|
+
const relayBlock = (() => {
|
|
320
|
+
try {
|
|
321
|
+
const cost = safeGet(ctx, SERVICE.cost)
|
|
322
|
+
if (cost && typeof cost.relayStatus === 'function') {
|
|
323
|
+
const relay = cost.relayStatus()
|
|
324
|
+
if (relay && typeof relay === 'object') return relay
|
|
325
|
+
}
|
|
326
|
+
} catch { /* 回流状态读取失败不阻塞成本摘要 */ }
|
|
327
|
+
return { target: 'fang', available: false, reason: 'cost-service-unavailable' }
|
|
328
|
+
})()
|
|
315
329
|
const rows = []
|
|
316
330
|
const totals = { events: 0, degraded: 0, substitutions: 0, estimated: 0, savedTokens: 0 }
|
|
317
331
|
for (const key of tickets) {
|
|
@@ -345,8 +359,8 @@ export function apply(ctx, config = {}) {
|
|
|
345
359
|
totals,
|
|
346
360
|
/* §11:只读成本块(A 单价项 / B 跳过调用项分列 + 命中率),面板只读消费。 */
|
|
347
361
|
service: serviceBlock,
|
|
348
|
-
/* §5.6
|
|
349
|
-
relay:
|
|
362
|
+
/* §5.6 / M5:瓮城回流出口真实状态(不可用必须显式带原因)。 */
|
|
363
|
+
relay: relayBlock,
|
|
350
364
|
}
|
|
351
365
|
}
|
|
352
366
|
|