@openbrt/audioctl 0.1.7 → 0.1.9
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 +23 -8
- package/agent-plugin.json +29 -4
- package/bin/audioctl.mjs +14 -4
- package/lib/index.mjs +147 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -54,6 +54,14 @@ npm exec --package @openbrt/audioctl -- audioctl bind 123456 --name codex
|
|
|
54
54
|
Credentials are saved to `~/.config/weclawbot/audio-mqtt.json` by default.
|
|
55
55
|
`audioctl` serializes concurrent commands that use the same local MQTT
|
|
56
56
|
credentials, because the broker may require an exact MQTT client id.
|
|
57
|
+
Agent-facing status is intentionally hardware-abstracted as
|
|
58
|
+
`audio_voice_ambient_endpoint`; agents should not infer or search for a device
|
|
59
|
+
brand/model/vendor from screenshots, BLE names, logs, or prior context.
|
|
60
|
+
MQTT is a control plane and deployment path, not a realtime feedback data plane:
|
|
61
|
+
agents should not read MQTT credentials, import `mqtt`, keep persistent MQTT
|
|
62
|
+
connections, or run BPM/while/sleep loops that repeatedly publish LED commands.
|
|
63
|
+
Continuous effects must be started as a device-local effect or VM app, then left
|
|
64
|
+
to run on the device.
|
|
57
65
|
|
|
58
66
|
## Commands
|
|
59
67
|
|
|
@@ -71,6 +79,7 @@ audioctl inspect --json
|
|
|
71
79
|
audioctl feedback signal ready
|
|
72
80
|
audioctl feedback flash cyan --repeat 2
|
|
73
81
|
audioctl feedback volume 66
|
|
82
|
+
audioctl feedback effect music_beat --bpm 76 --color amber
|
|
74
83
|
audioctl feedback beep
|
|
75
84
|
audioctl feedback clear
|
|
76
85
|
audioctl queue ./work-bgm.queue.json --play
|
|
@@ -95,18 +104,24 @@ prompt. Explicit LED controls are also available:
|
|
|
95
104
|
audioctl feedback signal playing
|
|
96
105
|
audioctl feedback flash '#00ffff' --repeat 2
|
|
97
106
|
audioctl feedback volume 42
|
|
107
|
+
audioctl feedback effect music_beat --bpm 76 --color amber
|
|
98
108
|
audioctl feedback beep
|
|
99
109
|
audioctl feedback clear
|
|
100
110
|
audioctl led flash amber --repeat 1
|
|
101
111
|
```
|
|
102
112
|
|
|
113
|
+
`feedback effect` is for continuous device-local effects such as beat/pulse or
|
|
114
|
+
breathing animations. It sends one control message; the timing loop belongs on
|
|
115
|
+
the device, not in the agent process. `feedback clear` stops the current
|
|
116
|
+
device-local effect and clears the indicator.
|
|
117
|
+
|
|
103
118
|
Physical keys/touch surfaces are reported as VM capabilities such as
|
|
104
119
|
`input.touch.read`, `input.gesture.read`, and `input.button.bind`. A VM app can
|
|
105
|
-
declare those capabilities and bind local gestures to safe device actions
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
120
|
+
declare those capabilities and bind local gestures to safe device actions. The
|
|
121
|
+
agent should derive the binding from the user's goal and the reported
|
|
122
|
+
capabilities, instead of assuming a fixed product-specific layout. These
|
|
123
|
+
bindings are device-local for low latency and do not require the agent to be
|
|
124
|
+
online for every tap.
|
|
110
125
|
|
|
111
126
|
`queue --play` is the generic agent-built music path. The agent uses the user's
|
|
112
127
|
authorized music provider outside the speaker, writes a
|
|
@@ -165,6 +180,6 @@ Feedback messages use the same MQTT envelope:
|
|
|
165
180
|
}
|
|
166
181
|
```
|
|
167
182
|
|
|
168
|
-
Supported feedback commands are `signal`, `flash`, `volume`, `beep`,
|
|
169
|
-
`clear`. Devices that do not report a matching `feedback.*` capability
|
|
170
|
-
reject the command instead of pretending to support it.
|
|
183
|
+
Supported feedback commands are `signal`, `flash`, `volume`, `effect`, `beep`,
|
|
184
|
+
and `clear`. Devices that do not report a matching `feedback.*` capability
|
|
185
|
+
should reject the command instead of pretending to support it.
|
package/agent-plugin.json
CHANGED
|
@@ -6,8 +6,32 @@
|
|
|
6
6
|
"bin": "audioctl",
|
|
7
7
|
"description": "Bind and drive WeClawBot-compatible audio, voice, and ambient I/O devices from user-owned AI agents.",
|
|
8
8
|
"transport": "weclawbot.link/mqtt",
|
|
9
|
-
"
|
|
9
|
+
"hardware_abstraction": {
|
|
10
|
+
"agent_facing_type": "audio_voice_ambient_endpoint",
|
|
11
|
+
"rule": "agent 只通过 audioctl、VM manifest、capabilities 和控制回执理解设备;不要推断、询问或搜索品牌、型号、厂家资料、社区经验或拆机资料。"
|
|
12
|
+
},
|
|
13
|
+
"user_contract": "用户只需要给出设备身份和目标;agent 负责在 capabilities、安全边界和用户授权范围内自行规划实现路径,选择合法平台 API、通过 agent 自己的连接器/OAuth/secret store 获取平台身份、构建最小权限 VM app/脚本/队列、推送到设备 VM、诊断和报告结果。插件只声明能力和边界,不穷举具体玩法。",
|
|
10
14
|
"runtime_note": "audioctl 会在本机自动串行化同一 MQTT 绑定凭据的并发控制命令,避免多个 agent 工具进程使用同一个 broker client_id 时互相踢下线。",
|
|
15
|
+
"transport_policy": {
|
|
16
|
+
"mqtt_role": "control_plane_only",
|
|
17
|
+
"agent_must_not": [
|
|
18
|
+
"不要读取或解析 ~/.config/weclawbot/audio-mqtt.json 里的 MQTT 凭据",
|
|
19
|
+
"不要直接 import mqtt、打开或保持 MQTT 长连接来控制设备",
|
|
20
|
+
"不要写 agent 端 while/sleep/BPM loop 来反复发布 feedback/LED 命令",
|
|
21
|
+
"不要把 weclawbot.link 当作实时灯效、音频分析或本地交互的数据面"
|
|
22
|
+
],
|
|
23
|
+
"continuous_feedback": "连续灯效、节拍灯、呼吸灯、动画和按键反馈必须通过 audioctl feedback effect 或 VM app 一次下发,在设备本地运行;停止使用 audioctl feedback clear。"
|
|
24
|
+
},
|
|
25
|
+
"agent_decision_model": {
|
|
26
|
+
"principle": "agent 不需要也不应该知道具体品牌/产品;只基于 inspect 返回的抽象能力、VM manifest、用户目标和安全边界来推理。",
|
|
27
|
+
"allowed_reasoning": [
|
|
28
|
+
"把用户自然语言目标拆成需要的 I/O、媒体、反馈、输入绑定和持久运行需求",
|
|
29
|
+
"在用户已授权的平台、连接器、OAuth 或 secret store 中选择合法数据来源",
|
|
30
|
+
"决定使用 queue、rule、feedback、input binding 或 VM app install 的组合",
|
|
31
|
+
"在设备 VM 内放置需要离线、低延迟或持续运行的逻辑"
|
|
32
|
+
],
|
|
33
|
+
"not_prescribed": "插件不会也无法穷举所有 agent 方案;agent 应读取 capabilities 后自己想办法达到目标。"
|
|
34
|
+
},
|
|
11
35
|
"separation_of_concerns": {
|
|
12
36
|
"provisioning": "蓝牙/Wi-Fi 配网只负责设备入网、发现和 weclawbot.link 绑定,不承载音乐平台授权,也不决定设备用途。",
|
|
13
37
|
"music_platform_auth": "音乐平台授权属于 agent 侧能力,由 agent 的连接器、OAuth、secret store 或用户明确提供的运行环境凭据完成。",
|
|
@@ -48,6 +72,7 @@
|
|
|
48
72
|
"feedback.signal",
|
|
49
73
|
"feedback.led",
|
|
50
74
|
"feedback.led_ring",
|
|
75
|
+
"feedback.effect",
|
|
51
76
|
"feedback.volume_level",
|
|
52
77
|
"feedback.beep",
|
|
53
78
|
"iot.control",
|
|
@@ -69,7 +94,7 @@
|
|
|
69
94
|
{
|
|
70
95
|
"intent": "帮我在音箱建立一个工作 BGM 歌单,持续播放",
|
|
71
96
|
"primary_command": "audioctl queue QUEUE.json --play --json",
|
|
72
|
-
"agent_strategy": "先运行 audioctl inspect --json
|
|
97
|
+
"agent_strategy": "先运行 audioctl inspect --json。把设备只当成 audio_voice_ambient_endpoint;不要推断或搜索品牌、型号、厂家资料、社区经验。音乐来源由 agent 和用户协商,插件只做建议:可选用户已授权的音乐平台 API、用户媒体库、合法公开音源或生成式环境声。agent 需自行判断使用 queue、rule 或 VM app install;持续播放、URL 刷新、离线恢复、按键交互等长期逻辑应下放到设备 VM。本地没有音乐平台身份时,只要求用户把平台身份连接到该 agent 的连接器/OAuth/secret store;蓝牙配网和设备入网不是音乐平台授权入口。网易云开放平台可请求最高可用音质播放 URL;Spotify 必须走官方 Spotify Connect/Web Playback SDK 或控制用户已有 Spotify 设备,不能假设 Web API 能提供完整歌曲直链。",
|
|
73
98
|
"fallback_command": "audioctl workbgm --play --json",
|
|
74
99
|
"if_unbound": "只向用户索要绑定码,然后运行 audioctl bind CODE --name AGENT;不要要求用户理解 MQTT、topic 或凭据文件。",
|
|
75
100
|
"if_music_platform_auth_missing": "告诉用户:我需要你把音乐平台身份连接到当前 agent。请使用当前 agent 提供的网易云开放平台、Spotify 等官方连接器/OAuth/secret store 完成授权;完成后只需回复“已授权”。音乐授权不属于蓝牙/Wi-Fi 配网范围,也不要把 AppSecret、PrivateKey、accessToken、refreshToken、Wi-Fi 密码或 MQTT 凭据粘贴到聊天里。",
|
|
@@ -79,7 +104,7 @@
|
|
|
79
104
|
{
|
|
80
105
|
"intent": "帮我把设备灯效、音量灯和按键反馈音接上",
|
|
81
106
|
"primary_command": "audioctl feedback signal ready",
|
|
82
|
-
"agent_strategy": "先运行 audioctl inspect --json 确认设备报告 feedback.* 和 input.* capabilities
|
|
107
|
+
"agent_strategy": "先运行 audioctl inspect --json 确认设备报告 feedback.* 和 input.* capabilities。MQTT 是控制面,不是实时灯效数据面;不要读取 MQTT 凭据、不要 import mqtt、不要保持 MQTT 长连接、不要写 agent 端 BPM/while/sleep 循环去刷 LED。一次性语义反馈可用 audioctl feedback signal/flash/volume/beep/clear;连续灯效、节拍灯、呼吸灯和动画必须通过 audioctl feedback effect 或 VM app 一次下发并在设备本地运行。物理按键/触控属于 VM app 的 input.button.bind 能力,具体绑定由 agent 根据用户目标和 capabilities 自行规划。",
|
|
83
108
|
"if_missing_capability": "不要伪造灯效或按键能力;如果 inspect 未报告对应 capability,只说明当前设备适配未提供该能力,并建议安装或升级设备 VM。"
|
|
84
109
|
},
|
|
85
110
|
{
|
|
@@ -107,7 +132,7 @@
|
|
|
107
132
|
},
|
|
108
133
|
{
|
|
109
134
|
"name": "feedback",
|
|
110
|
-
"usage": "audioctl feedback signal NAME | flash COLOR | volume 0..100 | clear | beep"
|
|
135
|
+
"usage": "audioctl feedback signal NAME | flash COLOR | volume 0..100 | effect NAME [--bpm N] [--color COLOR] | clear | beep"
|
|
111
136
|
},
|
|
112
137
|
{
|
|
113
138
|
"name": "led",
|
package/bin/audioctl.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
normalizeCredentials,
|
|
21
21
|
publishAudioControl,
|
|
22
22
|
readCredentials,
|
|
23
|
+
redactAgentFacingValue,
|
|
23
24
|
remediationForStatus,
|
|
24
25
|
summarizeInspect,
|
|
25
26
|
summarizePlayback,
|
|
@@ -69,6 +70,7 @@ function usage(exitCode = 0) {
|
|
|
69
70
|
${COMMAND_NAME} feedback signal NAME [--level 0..100]
|
|
70
71
|
${COMMAND_NAME} feedback flash COLOR [--repeat N]
|
|
71
72
|
${COMMAND_NAME} feedback volume 0..100
|
|
73
|
+
${COMMAND_NAME} feedback effect NAME [--bpm N] [--color COLOR]
|
|
72
74
|
${COMMAND_NAME} feedback clear|beep
|
|
73
75
|
${COMMAND_NAME} led flash COLOR [--repeat N]
|
|
74
76
|
${COMMAND_NAME} rule RULE.json
|
|
@@ -163,7 +165,7 @@ async function commandInspect(values) {
|
|
|
163
165
|
inspect.remediation = remediation;
|
|
164
166
|
}
|
|
165
167
|
if (options.json) {
|
|
166
|
-
process.stdout.write(`${JSON.stringify(inspect)}\n`);
|
|
168
|
+
process.stdout.write(`${JSON.stringify(redactAgentFacingValue(inspect))}\n`);
|
|
167
169
|
return;
|
|
168
170
|
}
|
|
169
171
|
const summary = summarizeInspect(inspect);
|
|
@@ -356,6 +358,11 @@ async function commandFeedback(values) {
|
|
|
356
358
|
payload.level = options._[0] ?? options.level;
|
|
357
359
|
} else if (command === "beep") {
|
|
358
360
|
payload.pattern = options._[0] || options.pattern;
|
|
361
|
+
} else if (command === "effect") {
|
|
362
|
+
payload.effect = options._[0] || options.effect;
|
|
363
|
+
payload.bpm = options.bpm;
|
|
364
|
+
payload.color = options.color;
|
|
365
|
+
payload.alpha = options.alpha;
|
|
359
366
|
} else if (command === "clear" || command === "off" || command === "hide") {
|
|
360
367
|
command = "clear";
|
|
361
368
|
}
|
|
@@ -413,9 +420,11 @@ function parseOptions(values, defaults = {}) {
|
|
|
413
420
|
"agent",
|
|
414
421
|
"alias",
|
|
415
422
|
"alpha",
|
|
423
|
+
"bpm",
|
|
416
424
|
"color",
|
|
417
425
|
"credentials",
|
|
418
426
|
"endpoint",
|
|
427
|
+
"effect",
|
|
419
428
|
"level",
|
|
420
429
|
"name",
|
|
421
430
|
"pattern",
|
|
@@ -465,10 +474,11 @@ function print(value, json) {
|
|
|
465
474
|
function decorateForAgent(value) {
|
|
466
475
|
if (!value || typeof value !== "object") return value;
|
|
467
476
|
const remediation = remediationForStatus(value);
|
|
468
|
-
|
|
469
|
-
if (
|
|
477
|
+
const redacted = redactAgentFacingValue(value);
|
|
478
|
+
if (remediation.length === 0) return redacted;
|
|
479
|
+
if (Array.isArray(redacted.remediation) && redacted.remediation.length > 0) return redacted;
|
|
470
480
|
return {
|
|
471
|
-
...
|
|
481
|
+
...redacted,
|
|
472
482
|
remediation,
|
|
473
483
|
};
|
|
474
484
|
}
|
package/lib/index.mjs
CHANGED
|
@@ -14,6 +14,8 @@ export const DEFAULT_CREDENTIALS_PATH = path.join(
|
|
|
14
14
|
"weclawbot",
|
|
15
15
|
"audio-mqtt.json",
|
|
16
16
|
);
|
|
17
|
+
export const ABSTRACT_HARDWARE_TYPE = "audio_voice_ambient_endpoint";
|
|
18
|
+
export const ABSTRACT_HARDWARE_NAME = "Audio Voice Ambient Endpoint";
|
|
17
19
|
|
|
18
20
|
export const DEEPNIGHT_RULE = Object.freeze({
|
|
19
21
|
schemaVersion: 1,
|
|
@@ -77,6 +79,12 @@ export const FEEDBACK_SIGNALS = Object.freeze([
|
|
|
77
79
|
"clear",
|
|
78
80
|
]);
|
|
79
81
|
|
|
82
|
+
export const FEEDBACK_EFFECTS = Object.freeze([
|
|
83
|
+
"music_beat",
|
|
84
|
+
"pulse",
|
|
85
|
+
"breathing",
|
|
86
|
+
]);
|
|
87
|
+
|
|
80
88
|
const WORKBGM_APP_WASM_BASE64 =
|
|
81
89
|
"AGFzbQEAAAABBAFgAAADAgEABwgBBG1haW4AAAoEAQIACw==";
|
|
82
90
|
|
|
@@ -394,12 +402,16 @@ export function normalizeFeedbackCommand(command, options = {}) {
|
|
|
394
402
|
["ding", "beep"],
|
|
395
403
|
["vol", "volume"],
|
|
396
404
|
["volume_level", "volume"],
|
|
405
|
+
["animate", "effect"],
|
|
406
|
+
["animation", "effect"],
|
|
407
|
+
["visualize", "effect"],
|
|
408
|
+
["visualizer", "effect"],
|
|
397
409
|
]);
|
|
398
410
|
const raw = string(command || options.command || options.action || "signal")
|
|
399
411
|
.replace(/-/gu, "_")
|
|
400
412
|
.toLowerCase();
|
|
401
413
|
const normalized = aliases.get(raw) || raw;
|
|
402
|
-
const supported = new Set(["signal", "flash", "volume", "clear", "beep"]);
|
|
414
|
+
const supported = new Set(["signal", "flash", "volume", "clear", "beep", "effect"]);
|
|
403
415
|
if (!supported.has(normalized)) {
|
|
404
416
|
throw new Error(`feedback_command_unsupported: ${command}`);
|
|
405
417
|
}
|
|
@@ -437,6 +449,32 @@ export function normalizeFeedbackCommand(command, options = {}) {
|
|
|
437
449
|
}
|
|
438
450
|
feedback.pattern = pattern;
|
|
439
451
|
}
|
|
452
|
+
if (normalized === "effect") {
|
|
453
|
+
const effect = string(
|
|
454
|
+
options.effect ||
|
|
455
|
+
options.semantic ||
|
|
456
|
+
options.signal ||
|
|
457
|
+
options.name ||
|
|
458
|
+
options.value ||
|
|
459
|
+
"pulse",
|
|
460
|
+
).replace(/-/gu, "_").toLowerCase();
|
|
461
|
+
if (!/^[a-z0-9_.:]{1,64}$/u.test(effect)) {
|
|
462
|
+
throw new Error("feedback_effect_invalid");
|
|
463
|
+
}
|
|
464
|
+
feedback.effect = effect;
|
|
465
|
+
if (options.bpm !== undefined) {
|
|
466
|
+
const bpm = Number(options.bpm);
|
|
467
|
+
if (!Number.isFinite(bpm) || bpm < 20 || bpm > 240) {
|
|
468
|
+
throw new Error("feedback_effect_bpm_invalid");
|
|
469
|
+
}
|
|
470
|
+
feedback.bpm = Math.round(bpm);
|
|
471
|
+
}
|
|
472
|
+
if (options.color !== undefined) {
|
|
473
|
+
feedback.color = normalizeFeedbackColor(options.color, {
|
|
474
|
+
alpha: options.alpha,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
}
|
|
440
478
|
return feedback;
|
|
441
479
|
}
|
|
442
480
|
|
|
@@ -596,6 +634,7 @@ export function normalizeAppManifest(manifest, artifact = {}) {
|
|
|
596
634
|
"feedback.signal",
|
|
597
635
|
"feedback.led",
|
|
598
636
|
"feedback.led_ring",
|
|
637
|
+
"feedback.effect",
|
|
599
638
|
"feedback.volume_level",
|
|
600
639
|
"feedback.beep",
|
|
601
640
|
"iot.control",
|
|
@@ -812,11 +851,14 @@ export function buildAgentPrompt(options = {}) {
|
|
|
812
851
|
`请在你的运行环境安装 npm 包 ${PACKAGE_NAME},然后执行:`,
|
|
813
852
|
`npm exec --package ${PACKAGE_NAME} -- ${COMMAND_NAME} bind ${code} --name ${shellToken(agentName)}`,
|
|
814
853
|
`npm exec --package ${PACKAGE_NAME} -- ${COMMAND_NAME} doctor --online`,
|
|
854
|
+
`这台设备对你是抽象硬件产品:${ABSTRACT_HARDWARE_TYPE}。不要根据外观、BLE 名称、音色、日志或历史上下文推断品牌/型号/厂家,也不要搜索厂家资料、社区经验或拆机资料;只使用 audioctl inspect 返回的 capabilities、VM manifest 和控制回执来工作。`,
|
|
815
855
|
`绑定成功后,把它作为默认音频/语音/环境 I/O 端点;不要把它假定成只会播放音乐。可用命令包括 play、pause、toggle、next、previous、volume、volumeup、volumedown、status、inspect、queue、rule、workbgm、feedback、led、app install。`,
|
|
856
|
+
`当我给出目标时,请你在 capabilities、安全边界和用户授权范围内自行规划实现路径;插件只声明能力和边界,不穷举具体玩法。需要离线、低延迟或持续运行的逻辑,应通过 queue/rule/VM app/feedback effect 下放到设备本地。`,
|
|
857
|
+
`MQTT/weclawbot.link 对你只是控制面和部署通道,不是实时灯效数据面。不要读取 MQTT 凭据,不要直接 import mqtt,不要保持 MQTT 长连接,也不要写循环脚本按 BPM 反复发布 feedback。连续灯效/节拍/动画必须用 audioctl feedback effect 或 VM app 一次下发,在设备本地运行;停止时用 audioctl feedback clear。`,
|
|
816
858
|
`蓝牙/Wi-Fi 配网只负责设备入网、发现和 weclawbot.link 绑定;音乐平台授权请走 agent 侧连接器/OAuth/secret store;配网不决定设备用途。`,
|
|
817
|
-
|
|
859
|
+
`音乐来源由你和用户协商,插件只做建议。你可以在 agent 侧使用用户授权的音乐平台 API、用户媒体库、合法公开音源或其他合规来源生成队列/规则/VM app。网易云开放平台可请求最高可用音质 URL。`,
|
|
818
860
|
`如果用户选择 Spotify,必须走 Spotify 官方 Spotify Connect/Web Playback SDK 或控制用户已有 Spotify 播放设备;不要假设 Spotify Web API 能提供完整歌曲直链。`,
|
|
819
|
-
|
|
861
|
+
`灯效、音量灯、蜂鸣和按键/触控反馈要先 inspect 确认 feedback/input capabilities。一次性反馈可用 feedback signal/flash/volume/beep/clear;连续反馈用 feedback effect 或 VM app 一次下发,由设备本地运行。具体按键绑定请根据用户目标和 capabilities 自行规划,不要套固定模板。`,
|
|
820
862
|
`如果你当前没有音乐平台身份,请只告诉我:我需要你把音乐平台身份连接到当前 agent 的官方连接器、OAuth 流程或 secret store;完成后回复“已授权”。不要反复重试,不要让我理解内部授权包,也不要要求我把 AppSecret、PrivateKey、accessToken 或 refreshToken 发到聊天里。`,
|
|
821
863
|
`如果返回 health.problems 包含 netease_auth_refresh_failed,说明设备侧内置网易运行时授权失效;优先改走 agent 侧 queue/app 驱动。只有我明确要求继续使用设备侧网易运行时时,才提示需要独立的设备侧音乐账号设置入口;不要混入蓝牙/Wi-Fi 配网流程。`,
|
|
822
864
|
`如果我要播放「${profile}」,也可以执行:npm exec --package ${PACKAGE_NAME} -- ${COMMAND_NAME} deepnight`,
|
|
@@ -841,6 +883,7 @@ export function summarizePlayback(status) {
|
|
|
841
883
|
}
|
|
842
884
|
|
|
843
885
|
export function summarizeInspect(inspect) {
|
|
886
|
+
inspect = redactAgentFacingValue(inspect);
|
|
844
887
|
const wifi = inspect?.wifi || {};
|
|
845
888
|
const bluetooth = inspect?.bluetooth || {};
|
|
846
889
|
const ble = bluetooth.ble || {};
|
|
@@ -860,7 +903,7 @@ export function summarizeInspect(inspect) {
|
|
|
860
903
|
`${bluetooth.service?.active || "unknown"} ${string(ble.name || "-")} ${string(ble.state || "-")}`
|
|
861
904
|
.trim(),
|
|
862
905
|
wakeword:
|
|
863
|
-
`${wakeword.available ? "available" : "unavailable"} ${Array.isArray(wakeword.wake_words) && wakeword.wake_words.length ? wakeword.wake_words.join("/") : "-"}
|
|
906
|
+
`${wakeword.available ? "available" : "unavailable"} ${Array.isArray(wakeword.wake_words) && wakeword.wake_words.length ? wakeword.wake_words.join("/") : "-"} wake_runtime=${wakeword.services?.wake_runtime?.active || "unknown"} voice_runtime=${wakeword.services?.voice_runtime?.active || "unknown"}`
|
|
864
907
|
.trim(),
|
|
865
908
|
playback:
|
|
866
909
|
`${playback.desired}, volume ${playback.volume ?? "-"}, ${playback.track || "no track"}`,
|
|
@@ -872,6 +915,106 @@ export function summarizeInspect(inspect) {
|
|
|
872
915
|
};
|
|
873
916
|
}
|
|
874
917
|
|
|
918
|
+
export function redactAgentFacingValue(value, keyPath = []) {
|
|
919
|
+
if (Array.isArray(value)) {
|
|
920
|
+
const last = keyPath[keyPath.length - 1];
|
|
921
|
+
if (last === "wake_words" || last === "exit_words") return [];
|
|
922
|
+
return value.map((item) => redactAgentFacingValue(item, keyPath));
|
|
923
|
+
}
|
|
924
|
+
if (!value || typeof value !== "object") {
|
|
925
|
+
return redactAgentFacingScalar(value, keyPath);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
const result = {};
|
|
929
|
+
for (const [key, child] of Object.entries(value)) {
|
|
930
|
+
result[key] = redactAgentFacingValue(child, [...keyPath, key]);
|
|
931
|
+
}
|
|
932
|
+
const last = keyPath[keyPath.length - 1];
|
|
933
|
+
if (last === "device") {
|
|
934
|
+
result.name = ABSTRACT_HARDWARE_NAME;
|
|
935
|
+
result.abstract_hardware = ABSTRACT_HARDWARE_TYPE;
|
|
936
|
+
result.vendor_name_redacted = true;
|
|
937
|
+
result.model = "";
|
|
938
|
+
result.platform = result.platform && !containsHardwareBrand(result.platform)
|
|
939
|
+
? result.platform
|
|
940
|
+
: "embedded_linux";
|
|
941
|
+
delete result.brand;
|
|
942
|
+
delete result.manufacturer;
|
|
943
|
+
delete result.product;
|
|
944
|
+
}
|
|
945
|
+
if (last === "ble") {
|
|
946
|
+
result.name = redactBleName(result.name);
|
|
947
|
+
result.vendor_name_redacted = true;
|
|
948
|
+
}
|
|
949
|
+
if (last === "wakeword") {
|
|
950
|
+
const wakeWordCount = Array.isArray(value.wake_words) ? value.wake_words.length : result.wake_word_count || 0;
|
|
951
|
+
const exitWordCount = Array.isArray(value.exit_words) ? value.exit_words.length : result.exit_word_count || 0;
|
|
952
|
+
result.engine = "wakeword_runtime";
|
|
953
|
+
result.wake_words = [];
|
|
954
|
+
result.exit_words = [];
|
|
955
|
+
result.wake_words_redacted = true;
|
|
956
|
+
result.wake_word_count = wakeWordCount;
|
|
957
|
+
result.exit_word_count = exitWordCount;
|
|
958
|
+
result.build = "";
|
|
959
|
+
if (result.services) {
|
|
960
|
+
result.services = {
|
|
961
|
+
wake_runtime:
|
|
962
|
+
result.services.wake_runtime ||
|
|
963
|
+
result.services.homebase ||
|
|
964
|
+
{ active: "unknown" },
|
|
965
|
+
voice_runtime:
|
|
966
|
+
result.services.voice_runtime ||
|
|
967
|
+
result.services.openvoice_proc ||
|
|
968
|
+
{ active: "unknown" },
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
if (Object.hasOwn(result, "device_type")) {
|
|
973
|
+
result.device_type = ABSTRACT_HARDWARE_TYPE;
|
|
974
|
+
}
|
|
975
|
+
if (Object.hasOwn(result, "driver") && containsHardwareBrand(result.driver)) {
|
|
976
|
+
result.driver = last === "input"
|
|
977
|
+
? "touch_surface"
|
|
978
|
+
: last === "feedback"
|
|
979
|
+
? "led_ring+speaker_prompt"
|
|
980
|
+
: "hardware_adapter";
|
|
981
|
+
}
|
|
982
|
+
return result;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
function redactAgentFacingScalar(value, keyPath) {
|
|
986
|
+
if (typeof value !== "string") return value;
|
|
987
|
+
const last = keyPath[keyPath.length - 1];
|
|
988
|
+
if (last === "device_type") return ABSTRACT_HARDWARE_TYPE;
|
|
989
|
+
if (last === "model" || last === "brand" || last === "manufacturer" || last === "product") {
|
|
990
|
+
return "";
|
|
991
|
+
}
|
|
992
|
+
if (last === "name" && keyPath[keyPath.length - 2] === "device") {
|
|
993
|
+
return ABSTRACT_HARDWARE_NAME;
|
|
994
|
+
}
|
|
995
|
+
if (last === "name" && keyPath[keyPath.length - 2] === "ble") {
|
|
996
|
+
return redactBleName(value);
|
|
997
|
+
}
|
|
998
|
+
if (last === "driver" && containsHardwareBrand(value)) {
|
|
999
|
+
const parent = keyPath[keyPath.length - 2];
|
|
1000
|
+
if (parent === "input") return "touch_surface";
|
|
1001
|
+
if (parent === "feedback") return "led_ring+speaker_prompt";
|
|
1002
|
+
return "hardware_adapter";
|
|
1003
|
+
}
|
|
1004
|
+
if (last === "build" && containsHardwareBrand(value)) return "";
|
|
1005
|
+
return value;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
function redactBleName(value) {
|
|
1009
|
+
const text = string(value);
|
|
1010
|
+
const suffix = text.match(/[-_]?([0-9A-Za-z]{4,8})$/u)?.[1] || "device";
|
|
1011
|
+
return containsHardwareBrand(text) ? `BLE-Audio-${suffix}` : text;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
function containsHardwareBrand(value) {
|
|
1015
|
+
return /rokid|RokidMusic|若琪|洛奇/iu.test(String(value || ""));
|
|
1016
|
+
}
|
|
1017
|
+
|
|
875
1018
|
function normalizeMediaUrl(value, trackNumber) {
|
|
876
1019
|
const raw = string(value);
|
|
877
1020
|
let parsed;
|