dsh-plugin-wallpaper-engine-media-bridge 0.0.0-stage → 1.4.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/cordis.patch.yml +17 -0
- package/icon.png +0 -0
- package/lib/audio-tap.swift +146 -0
- package/lib/backend.js +144 -0
- package/lib/client.media-bridge.js +835 -0
- package/lib/index.js +242 -0
- package/lib/legacy.js +469 -0
- package/lib/occlusion/index.js +292 -0
- package/lib/provision.js +301 -0
- package/lib/routes/inventory.js +142 -0
- package/lib/routes/live-frame.js +97 -0
- package/lib/routes/media-bytes.js +90 -0
- package/lib/routes/media-derived.js +250 -0
- package/lib/routes/now-playing.js +177 -0
- package/lib/routes/occlusion.js +78 -0
- package/lib/routes/props.js +81 -0
- package/lib/routes/scene-frame.js +373 -0
- package/lib/routes/scene-media.js +174 -0
- package/lib/routes/scene-serve.js +195 -0
- package/lib/routes/slides.js +68 -0
- package/lib/routes/upload.js +355 -0
- package/lib/routes/we-assets.js +172 -0
- package/lib/supervisor.js +765 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +33 -4
- package/README.md +0 -3
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# cordis.patch.yml — `media-bridge` 子包(P-D 阶段1 起承载壁纸域宿主/浏览器半边,包名不变)自己的行
|
|
2
|
+
# (ADR-0014 S4.1 / S4.2 / S4.4)。
|
|
3
|
+
#
|
|
4
|
+
# ⚠️ 一次安装形态(主包 patch 用 name 引用子包,ADR-0014 S4.1)下,这一行由**主包**的
|
|
5
|
+
# cordis.patch.yml 插入;只有把这个子包自己当成独立 bundle 时
|
|
6
|
+
# (各自 dsh.bundle.patch + 各自进 dsh.profile.bundles,**不采纳**)本文件才生效。
|
|
7
|
+
# 两处同时插同一个 id 会撞车 ⇒ 接线时以主包那份为准,本文件保持与它逐字一致(ADR-0014 S4.2)。
|
|
8
|
+
#
|
|
9
|
+
# 行名 = 真实包名(裸真名):本版 DSH 不传 bareModuleBaseUrl ⇒ 行名一律从 <profile>/cordis.yml
|
|
10
|
+
# 旁起算,裸名 → <profile>/node_modules/<name>;开发期靠 scripts/dev-link-subpackages.mjs 建的
|
|
11
|
+
# junction 让这个名字出现在 profile/node_modules 里(ADR-0014 S4.4),发布期靠传递依赖 hoist。
|
|
12
|
+
# ⚠️ P-D 阶段1:壁纸域的宿主路由族与浏览器半边并入本包,但**包名与行 id 维持
|
|
13
|
+
# `media-bridge` 不变**(upstream 1.4.0 已按此名发布 npm 八包集;ADR-0014 §S4.3 的
|
|
14
|
+
# id = `wallpaper-engine-<目录名>` 因此自然成立)。id 一旦定下不再改(ADR-0014 S4.3)。
|
|
15
|
+
- insert:
|
|
16
|
+
- id: wallpaper-engine-media-bridge
|
|
17
|
+
name: 'dsh-plugin-wallpaper-engine-media-bridge'
|
package/icon.png
ADDED
|
Binary file
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// dsh-wallpaper-engine — 系统音频频谱采集器(macOS 14.2+ CoreAudio Process Tap)
|
|
2
|
+
//
|
|
3
|
+
// 用途:把「系统正在播放的声音」(loopback,不经过麦克风)转成 64 段频谱,
|
|
4
|
+
// 20fps 写到 stdout(每帧固定 64 字节,0-255 整数)—— 供插件宿主喂给
|
|
5
|
+
// WebWallGL 渲染页,让壁纸的音频可视化 / registerAudioBuffers 跟随真实音乐。
|
|
6
|
+
//
|
|
7
|
+
// 权限:首次运行会请求「音频录制」(TCC)。未授权时 AudioHardwareCreateProcessTap
|
|
8
|
+
// 返回错误码,本程序以 stderr 一行 `tap-create-failed <code>` 退出 —— 宿主据此
|
|
9
|
+
// 在设置界面给出授权指引,并回落到模拟频谱。
|
|
10
|
+
//
|
|
11
|
+
// 设计取舍:
|
|
12
|
+
// - tap 的 IOProc 在实时线程:回调里只做「写环形缓冲」(无分配、无锁;单写单读
|
|
13
|
+
// 的环形缓冲最坏读到半帧数据,频谱略微跳动,音频可视化完全可接受)。
|
|
14
|
+
// - FFT / 分箱 / 输出放在主线程 20fps 定时器里做(2048 点 vDSP real FFT,微秒级)。
|
|
15
|
+
// - 立体声 → 单声道(多声道取平均):壁纸音条不强调声道差(上游对自带 BGM 也是
|
|
16
|
+
// 同值喂左右)。
|
|
17
|
+
import AudioToolbox
|
|
18
|
+
import CoreAudio
|
|
19
|
+
import Accelerate
|
|
20
|
+
import Foundation
|
|
21
|
+
|
|
22
|
+
let FFT_N = 2048
|
|
23
|
+
let LOG2N = vDSP_Length(11) // 2^11 = 2048
|
|
24
|
+
let BANDS = 64
|
|
25
|
+
let FPS = 20
|
|
26
|
+
let RING = 1 << 15 // 32768 样本(约 0.68s @48k),远大于 FFT 窗口
|
|
27
|
+
|
|
28
|
+
private var ring = [Float](repeating: 0, count: RING)
|
|
29
|
+
private var ringWrite = 0
|
|
30
|
+
|
|
31
|
+
private func fail(_ msg: String, _ code: Int32) -> Never {
|
|
32
|
+
FileHandle.standardError.write((msg + "\n").data(using: .utf8)!)
|
|
33
|
+
exit(code)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ── 1. Process Tap(系统全局音频;排除自身进程避免自采反馈)──────────────────
|
|
37
|
+
let tapDesc = CATapDescription(stereoGlobalTapButExcludeProcesses: [])
|
|
38
|
+
tapDesc.name = "dsh-we-audio-tap"
|
|
39
|
+
tapDesc.isPrivate = true
|
|
40
|
+
tapDesc.muteBehavior = .unmuted // 只监听,不影响系统输出
|
|
41
|
+
|
|
42
|
+
var tapID = AudioObjectID(kAudioObjectUnknown)
|
|
43
|
+
var err = AudioHardwareCreateProcessTap(tapDesc, &tapID)
|
|
44
|
+
if err != noErr { fail("tap-create-failed \(err)", 2) }
|
|
45
|
+
|
|
46
|
+
// ── 2. 聚合设备承载 tap ──────────────────────────────────────────────────────
|
|
47
|
+
let aggDesc: [String: Any] = [
|
|
48
|
+
kAudioAggregateDeviceNameKey: "dsh-we-audio-tap-agg",
|
|
49
|
+
kAudioAggregateDeviceUIDKey: UUID().uuidString,
|
|
50
|
+
kAudioAggregateDeviceIsPrivateKey: true,
|
|
51
|
+
kAudioAggregateDeviceTapListKey: [[
|
|
52
|
+
kAudioSubTapUIDKey: tapDesc.uuid.uuidString,
|
|
53
|
+
kAudioSubTapDriftCompensationKey: true,
|
|
54
|
+
]],
|
|
55
|
+
]
|
|
56
|
+
var aggID = AudioObjectID(kAudioObjectUnknown)
|
|
57
|
+
err = AudioHardwareCreateAggregateDevice(aggDesc as CFDictionary, &aggID)
|
|
58
|
+
if err != noErr { fail("agg-create-failed \(err)", 3) }
|
|
59
|
+
|
|
60
|
+
// ── 3. IOProc:只写环形缓冲 ─────────────────────────────────────────────────
|
|
61
|
+
var procID: AudioDeviceIOProcID?
|
|
62
|
+
err = AudioDeviceCreateIOProcIDWithBlock(&procID, aggID, nil) { _, inData, _, _, _ in
|
|
63
|
+
let abl = UnsafeMutableAudioBufferListPointer(UnsafeMutablePointer(mutating: inData))
|
|
64
|
+
guard abl.count > 0 else { return }
|
|
65
|
+
let buf = abl[0]
|
|
66
|
+
guard let mData = buf.mData else { return }
|
|
67
|
+
let ch = max(1, Int(buf.mNumberChannels))
|
|
68
|
+
let total = Int(buf.mDataByteSize) / MemoryLayout<Float>.size
|
|
69
|
+
let ptr = mData.bindMemory(to: Float.self, capacity: total)
|
|
70
|
+
if ch == 1 {
|
|
71
|
+
for i in 0..<total {
|
|
72
|
+
ring[ringWrite] = ptr[i]
|
|
73
|
+
ringWrite = (ringWrite + 1) % RING
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
var i = 0
|
|
77
|
+
while i + ch <= total {
|
|
78
|
+
var s: Float = 0
|
|
79
|
+
for c in 0..<ch { s += ptr[i + c] }
|
|
80
|
+
ring[ringWrite] = s / Float(ch)
|
|
81
|
+
ringWrite = (ringWrite + 1) % RING
|
|
82
|
+
i += ch
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if err != noErr { fail("ioproc-create-failed \(err)", 4) }
|
|
87
|
+
err = AudioDeviceStart(aggID, procID)
|
|
88
|
+
if err != noErr { fail("device-start-failed \(err)", 5) }
|
|
89
|
+
FileHandle.standardError.write("ready\n".data(using: .utf8)!)
|
|
90
|
+
|
|
91
|
+
// ── 4. 主线程 20fps:FFT → 64 段 → stdout ──────────────────────────────────
|
|
92
|
+
let setup = vDSP_create_fftsetup(LOG2N, FFTRadix(kFFTRadix2))!
|
|
93
|
+
var hann = [Float](repeating: 0, count: FFT_N)
|
|
94
|
+
vDSP_hann_window(&hann, vDSP_Length(FFT_N), Int32(vDSP_HANN_NORM))
|
|
95
|
+
var input = [Float](repeating: 0, count: FFT_N)
|
|
96
|
+
var real = [Float](repeating: 0, count: FFT_N / 2)
|
|
97
|
+
var imag = [Float](repeating: 0, count: FFT_N / 2)
|
|
98
|
+
var mags = [Float](repeating: 0, count: FFT_N / 2)
|
|
99
|
+
var out = [UInt8](repeating: 0, count: BANDS)
|
|
100
|
+
let usable = Float(FFT_N / 2) * 0.72 // 截到 ~16kHz@48k:更高频段能量极微
|
|
101
|
+
let lo = 2
|
|
102
|
+
|
|
103
|
+
func emitFrame() {
|
|
104
|
+
let w = ringWrite
|
|
105
|
+
let start = (w - FFT_N + RING) % RING
|
|
106
|
+
for i in 0..<FFT_N { input[i] = ring[(start + i) % RING] }
|
|
107
|
+
vDSP_vmul(input, 1, hann, 1, &input, 1, vDSP_Length(FFT_N))
|
|
108
|
+
real.withUnsafeMutableBufferPointer { rp in
|
|
109
|
+
imag.withUnsafeMutableBufferPointer { ip in
|
|
110
|
+
var split = DSPSplitComplex(realp: rp.baseAddress!, imagp: ip.baseAddress!)
|
|
111
|
+
input.withUnsafeBufferPointer { inp in
|
|
112
|
+
inp.baseAddress!.withMemoryRebound(to: DSPComplex.self, capacity: FFT_N / 2) { cp in
|
|
113
|
+
vDSP_ctoz(cp, 2, &split, 1, vDSP_Length(FFT_N / 2))
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
vDSP_fft_zrip(setup, &split, 1, LOG2N, FFTDirection(FFT_FORWARD))
|
|
117
|
+
vDSP_zvabs(&split, 1, &mags, 1, vDSP_Length(FFT_N / 2))
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
let norm = 1.0 / Float(FFT_N) // real FFT 的幅度归一(相对值,标定足够)
|
|
121
|
+
for b in 0..<BANDS {
|
|
122
|
+
let f0 = lo + Int(Float(usable) * pow(Float(b) / Float(BANDS), 2))
|
|
123
|
+
let f1 = max(f0 + 1, lo + Int(Float(usable) * pow(Float(b + 1) / Float(BANDS), 2)))
|
|
124
|
+
var peak: Float = 0
|
|
125
|
+
var f = f0
|
|
126
|
+
while f < f1 && f < mags.count {
|
|
127
|
+
let v = mags[f] * norm
|
|
128
|
+
if v > peak { peak = v }
|
|
129
|
+
f += 1
|
|
130
|
+
}
|
|
131
|
+
// -70dB..0dB → 0..255(对数观感,安静段落接近 0 而不是恒亮)
|
|
132
|
+
let db = 20 * log10(max(peak, 1e-7))
|
|
133
|
+
let n = max(0, min(1, (db + 70) / 70))
|
|
134
|
+
out[b] = UInt8(n * 255)
|
|
135
|
+
}
|
|
136
|
+
FileHandle.standardOutput.write(Data(out))
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let timer = Timer(timeInterval: 1.0 / Double(FPS), repeats: true) { _ in emitFrame() }
|
|
140
|
+
RunLoop.main.add(timer, forMode: .common)
|
|
141
|
+
|
|
142
|
+
// 信号兜底:宿主 kill 时安静退出(private 聚合设备随进程回收)
|
|
143
|
+
signal(SIGTERM) { _ in exit(0) }
|
|
144
|
+
signal(SIGINT) { _ in exit(0) }
|
|
145
|
+
|
|
146
|
+
RunLoop.main.run()
|
package/lib/backend.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* backend.js — 媒体桥的**媒体后端门面**:**首选 media-bridge 子进程,失败自动回落内置实现**。
|
|
3
|
+
*
|
|
4
|
+
* 对宿主只暴露一小组方法(start/stop/spectrum/nowPlaying/artworkFile/artworkMime/status
|
|
5
|
+
* 与内置实现同名,另有中间件专供的 usingBridge/bridgeInfo/control),所以同目录的路由族
|
|
6
|
+
* (`now-playing.js`)不需要知道底下是谁在供数 —— 这是刻意留的接缝:换后端不动路由形状,
|
|
7
|
+
* 回滚也不需要改路由。
|
|
8
|
+
*
|
|
9
|
+
* 选择逻辑(每次会话只决定一次,之后靠 bridge.ready() 判断中间件是否还活着):
|
|
10
|
+
* 1. `DSH_WE_MEDIA_LEGACY=1` → 直接用内置实现(排查/对比用)
|
|
11
|
+
* 2. 产物解析成功 + spawn + 握手通过 → 中间件(三平台同一套能力)
|
|
12
|
+
* 3. 任一步失败(取不到产物/起不来/协议不符/连续崩溃)→ 内置实现 + 原因进 status
|
|
13
|
+
*
|
|
14
|
+
* 日志注入契约:本目录只调用注入的 `log(msg, level?)`,**不自己写终端**(终端与落档的唯一
|
|
15
|
+
* 入口是 `lib/log.js`,见其文件头)。级别缺省 `info`;判据是"影响显示效果的非正常表现
|
|
16
|
+
* ⇒ `warn`":回落、启动失败、中间件报错与重启都是 `warn`,成功事实与运行信息是 `info`。
|
|
17
|
+
*/
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
import { createMediaBridge as createLegacy } from './legacy.js';
|
|
20
|
+
import { createBridgeSupervisor } from './supervisor.js';
|
|
21
|
+
import { provisionMediaBridge } from './provision.js';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {object} o
|
|
25
|
+
* @param {string} o.dataDir 插件数据目录(**不是**缓存根):中间件产物 `<dataDir>/bin/media-bridge/`
|
|
26
|
+
* 与 macOS 音频 tap 的编译产物 `<dataDir>/bin/we-audio-tap` 都落这里 —— 它们属**工具链**,
|
|
27
|
+
* 和 ffmpeg 一样不随「缓存位置」走(挪了要重下 / 重编译,那是另一件事)。
|
|
28
|
+
* @param {string|(() => string)} [o.cacheBaseDir] 缓存根(见 `lib/kernel/data-dir.js` 的 `cacheBaseDir()`):中间件的
|
|
29
|
+
* 运行时缓存(`--cache-dir`)与内置实现的封面缓存都在它下面。缺省 `<dataDir>/cache` ——
|
|
30
|
+
* 与默认布局逐字一致,所以只传 `dataDir` 的老调用点行为不变。**可以传访问器**(推荐,
|
|
31
|
+
* 路由就这么传):那样「缓存位置」改完,中间件下一次起动 / 下一张封面就走新根,不必重启宿主。
|
|
32
|
+
* @param {(text: any, level?: string) => void} [o.log] 日志被调用面(`log(msg, level?)`);缺省空实现。
|
|
33
|
+
* @param {(name:string, data?:any) => void} [o.diag] 诊断上报(宿主注入 `appendDiagLine` 包装)。
|
|
34
|
+
*/
|
|
35
|
+
export function createMediaBackend({ dataDir, cacheBaseDir = join(dataDir, 'cache'), log = () => {}, diag = () => {} }) {
|
|
36
|
+
// 缓存根**每次现问**(同 `cacheBaseDir()` 的理由):传进来是访问器就现算,是字符串就照用。
|
|
37
|
+
// 在这里存值快照会让"改完缓存位置、新缓存又写回旧盘"悄悄发生 —— 正是本轮要修的那类 bug。
|
|
38
|
+
const cacheRoot = () => (typeof cacheBaseDir === 'function' ? cacheBaseDir() : cacheBaseDir);
|
|
39
|
+
|
|
40
|
+
let started = false;
|
|
41
|
+
let useBridge = false; // 中间件可用(握手通过且没被判死)
|
|
42
|
+
let fallback = ''; // 回落原因(空 = 没回落)
|
|
43
|
+
let note = ''; // 准备中的说明(设置界面可见)
|
|
44
|
+
let booting = null;
|
|
45
|
+
let sup = null;
|
|
46
|
+
let legacy = null;
|
|
47
|
+
const optsRef = { audio: true, online: false };
|
|
48
|
+
const forceLegacy = String(process.env.DSH_WE_MEDIA_LEGACY || '').trim() === '1';
|
|
49
|
+
|
|
50
|
+
/** 内置实现按需构造(音频开关要跟着用户设置走,不能构造时就定死)。 */
|
|
51
|
+
function legacyBackend() {
|
|
52
|
+
if (!legacy) legacy = createLegacy({ dataDir, cacheBaseDir: cacheRoot, log, audio: optsRef.audio });
|
|
53
|
+
return legacy;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** 当前真正在供数的后端(中间件没就绪/已判死时都落到内置实现)。 */
|
|
57
|
+
function active() {
|
|
58
|
+
if (useBridge && sup && sup.ready()) return sup;
|
|
59
|
+
return legacyBackend();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function fallBackTo(reason) {
|
|
63
|
+
if (fallback) return;
|
|
64
|
+
fallback = reason || '中间件不可用';
|
|
65
|
+
useBridge = false;
|
|
66
|
+
note = '';
|
|
67
|
+
log('回落到内置媒体实现:' + fallback, 'warn');
|
|
68
|
+
diag('media-fallback', { reason: String(fallback).slice(0, 200) });
|
|
69
|
+
try { legacyBackend().start(); } catch (err) { log('内置媒体实现启动失败:' + err, 'warn'); }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function bootBridge() {
|
|
73
|
+
if (booting) return booting;
|
|
74
|
+
booting = (async () => {
|
|
75
|
+
note = '正在准备系统媒体中间件…';
|
|
76
|
+
const prov = await provisionMediaBridge({ dataDir, log });
|
|
77
|
+
if (!prov.path) { fallBackTo(prov.error || '中间件产物不可用'); return; }
|
|
78
|
+
sup = createBridgeSupervisor({
|
|
79
|
+
binPath: prov.path,
|
|
80
|
+
cacheDir: join(cacheRoot(), 'media-bridge'),
|
|
81
|
+
log,
|
|
82
|
+
diag,
|
|
83
|
+
onFatal: (why) => fallBackTo(why),
|
|
84
|
+
});
|
|
85
|
+
const ok = await sup.start({ audio: optsRef.audio, online: optsRef.online });
|
|
86
|
+
if (!ok) { fallBackTo(sup.lastError() || '中间件启动失败'); return; }
|
|
87
|
+
useBridge = true;
|
|
88
|
+
note = '';
|
|
89
|
+
log(`媒体数据源:系统媒体中间件(${prov.source}${prov.tag ? ', ' + prov.tag : ''})`, 'info');
|
|
90
|
+
})().catch((err) => {
|
|
91
|
+
fallBackTo('中间件准备出错:' + String(err && err.message ? err.message : err));
|
|
92
|
+
}).finally(() => { booting = null; });
|
|
93
|
+
return booting;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
/** 懒启动(由路由第一次被访问时触发;重复调用无副作用)。 */
|
|
98
|
+
start(opts = {}) {
|
|
99
|
+
if (started) return;
|
|
100
|
+
started = true;
|
|
101
|
+
Object.assign(optsRef, { audio: opts.audio !== false, online: opts.online === true });
|
|
102
|
+
if (forceLegacy) {
|
|
103
|
+
fallback = '按 DSH_WE_MEDIA_LEGACY=1 使用内置实现';
|
|
104
|
+
log('媒体数据源:内置实现(DSH_WE_MEDIA_LEGACY=1)', 'info');
|
|
105
|
+
legacyBackend().start();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
bootBridge();
|
|
109
|
+
},
|
|
110
|
+
stop() {
|
|
111
|
+
try { if (sup) sup.stop(); } catch { /* ignore */ }
|
|
112
|
+
try { if (legacy) legacy.stop(); } catch { /* ignore */ }
|
|
113
|
+
},
|
|
114
|
+
spectrum: () => active().spectrum(),
|
|
115
|
+
nowPlaying: () => active().nowPlaying(),
|
|
116
|
+
artworkFile: () => active().artworkFile(),
|
|
117
|
+
artworkMime: () => active().artworkMime(),
|
|
118
|
+
/**
|
|
119
|
+
* 反向控制(壁纸里的播放按钮 / 宿主面板 → 真实播放器):**只有中间件能做**
|
|
120
|
+
* —— 内置实现是纯读取。中间件没就绪(还没启动 / 已判死回落)时统一回不可用,
|
|
121
|
+
* 不在此处触发启动:控制是用户点一下的交互,不该顺带把产物下载与子进程
|
|
122
|
+
* 拉起来(启动仍由取数那四条路由的懒启动决定)。
|
|
123
|
+
*
|
|
124
|
+
* 返回值一律是 `{ ok, … }`(不抛),路由按它决定状态码。
|
|
125
|
+
*/
|
|
126
|
+
async control(action) {
|
|
127
|
+
if (useBridge && sup && sup.ready()) return sup.control(action);
|
|
128
|
+
return { ok: false, error: fallback ? 'control-unsupported' : 'media-backend-not-ready' };
|
|
129
|
+
},
|
|
130
|
+
status() {
|
|
131
|
+
const live = useBridge && sup && sup.ready();
|
|
132
|
+
const st = active().status();
|
|
133
|
+
return {
|
|
134
|
+
...st,
|
|
135
|
+
backend: live ? 'bridge' : st.backend,
|
|
136
|
+
fallback,
|
|
137
|
+
note,
|
|
138
|
+
};
|
|
139
|
+
},
|
|
140
|
+
/** 当前是否中间件在供数(自检与设置界面用)。 */
|
|
141
|
+
usingBridge: () => Boolean(useBridge && sup && sup.ready()),
|
|
142
|
+
bridgeInfo: () => (sup ? sup.info() : null),
|
|
143
|
+
};
|
|
144
|
+
}
|