@openbrt/audioctl 0.1.8 → 0.1.10

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 CHANGED
@@ -57,6 +57,11 @@ credentials, because the broker may require an exact MQTT client id.
57
57
  Agent-facing status is intentionally hardware-abstracted as
58
58
  `audio_voice_ambient_endpoint`; agents should not infer or search for a device
59
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.
60
65
 
61
66
  ## Commands
62
67
 
@@ -74,6 +79,7 @@ audioctl inspect --json
74
79
  audioctl feedback signal ready
75
80
  audioctl feedback flash cyan --repeat 2
76
81
  audioctl feedback volume 66
82
+ audioctl feedback effect music_beat --bpm 76 --color amber
77
83
  audioctl feedback beep
78
84
  audioctl feedback clear
79
85
  audioctl queue ./work-bgm.queue.json --play
@@ -98,18 +104,26 @@ prompt. Explicit LED controls are also available:
98
104
  audioctl feedback signal playing
99
105
  audioctl feedback flash '#00ffff' --repeat 2
100
106
  audioctl feedback volume 42
107
+ audioctl feedback effect music_beat --bpm 76 --color amber
101
108
  audioctl feedback beep
102
109
  audioctl feedback clear
103
110
  audioctl led flash amber --repeat 1
104
111
  ```
105
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. Beat-style names such as
117
+ `music_beat` are treated as effects, not as a signal that should be published in
118
+ a loop.
119
+
106
120
  Physical keys/touch surfaces are reported as VM capabilities such as
107
121
  `input.touch.read`, `input.gesture.read`, and `input.button.bind`. A VM app can
108
- declare those capabilities and bind local gestures to safe device actions; for a
109
- music endpoint the recommended default is center click = play/pause toggle,
110
- edge click = next, and clockwise/counter-clockwise = volume step up/down with a
111
- short beep and a volume LED indication. These bindings are device-local for low
112
- latency and do not require the agent to be online for every tap.
122
+ declare those capabilities and bind local gestures to safe device actions. The
123
+ agent should derive the binding from the user's goal and the reported
124
+ capabilities, instead of assuming a fixed product-specific layout. These
125
+ bindings are device-local for low latency and do not require the agent to be
126
+ online for every tap.
113
127
 
114
128
  `queue --play` is the generic agent-built music path. The agent uses the user's
115
129
  authorized music provider outside the speaker, writes a
@@ -168,6 +182,6 @@ Feedback messages use the same MQTT envelope:
168
182
  }
169
183
  ```
170
184
 
171
- Supported feedback commands are `signal`, `flash`, `volume`, `beep`, and
172
- `clear`. Devices that do not report a matching `feedback.*` capability should
173
- reject the command instead of pretending to support it.
185
+ Supported feedback commands are `signal`, `flash`, `volume`, `effect`, `beep`,
186
+ and `clear`. Devices that do not report a matching `feedback.*` capability
187
+ should reject the command instead of pretending to support it.
package/agent-plugin.json CHANGED
@@ -10,8 +10,28 @@
10
10
  "agent_facing_type": "audio_voice_ambient_endpoint",
11
11
  "rule": "agent 只通过 audioctl、VM manifest、capabilities 和控制回执理解设备;不要推断、询问或搜索品牌、型号、厂家资料、社区经验或拆机资料。"
12
12
  },
13
- "user_contract": "用户只需要给出设备身份和目标;agent 负责选择合法平台 API、通过 agent 自己的连接器/OAuth/secret store 获取平台身份、构建最小权限脚本/队列、推送到设备 VM、诊断和报告结果。",
13
+ "user_contract": "用户只需要给出设备身份和目标;agent 负责在 capabilities、安全边界和用户授权范围内自行规划实现路径,选择合法平台 API、通过 agent 自己的连接器/OAuth/secret store 获取平台身份、构建最小权限 VM app/脚本/队列、推送到设备 VM、诊断和报告结果。插件只声明能力和边界,不穷举具体玩法。",
14
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
+ },
15
35
  "separation_of_concerns": {
16
36
  "provisioning": "蓝牙/Wi-Fi 配网只负责设备入网、发现和 weclawbot.link 绑定,不承载音乐平台授权,也不决定设备用途。",
17
37
  "music_platform_auth": "音乐平台授权属于 agent 侧能力,由 agent 的连接器、OAuth、secret store 或用户明确提供的运行环境凭据完成。",
@@ -52,6 +72,7 @@
52
72
  "feedback.signal",
53
73
  "feedback.led",
54
74
  "feedback.led_ring",
75
+ "feedback.effect",
55
76
  "feedback.volume_level",
56
77
  "feedback.beep",
57
78
  "iot.control",
@@ -73,7 +94,7 @@
73
94
  {
74
95
  "intent": "帮我在音箱建立一个工作 BGM 歌单,持续播放",
75
96
  "primary_command": "audioctl queue QUEUE.json --play --json",
76
- "agent_strategy": "先运行 audioctl inspect --json。把设备只当成 audio_voice_ambient_endpoint;不要推断或搜索品牌、型号、厂家资料、社区经验。音乐来源由 agent 和用户协商,插件只做建议:可选用户已授权的音乐平台 API、用户媒体库、合法公开音源或生成式环境声。选定来源后,在 agent 侧生成 QUEUE.json,再运行 audioctl queue QUEUE.json --play --json。网易云开放平台可请求最高可用音质播放 URL;Spotify 必须走官方 Spotify Connect/Web Playback SDK 或控制用户已有 Spotify 设备,不能假设 Web API 能提供完整歌曲直链。如果 agent 当前没有音乐平台身份,只要求用户把音乐平台连接到该 agent 的连接器/OAuth/secret store;蓝牙配网和设备入网不是音乐平台授权入口。",
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 能提供完整歌曲直链。",
77
98
  "fallback_command": "audioctl workbgm --play --json",
78
99
  "if_unbound": "只向用户索要绑定码,然后运行 audioctl bind CODE --name AGENT;不要要求用户理解 MQTT、topic 或凭据文件。",
79
100
  "if_music_platform_auth_missing": "告诉用户:我需要你把音乐平台身份连接到当前 agent。请使用当前 agent 提供的网易云开放平台、Spotify 等官方连接器/OAuth/secret store 完成授权;完成后只需回复“已授权”。音乐授权不属于蓝牙/Wi-Fi 配网范围,也不要把 AppSecret、PrivateKey、accessToken、refreshToken、Wi-Fi 密码或 MQTT 凭据粘贴到聊天里。",
@@ -83,7 +104,7 @@
83
104
  {
84
105
  "intent": "帮我把设备灯效、音量灯和按键反馈音接上",
85
106
  "primary_command": "audioctl feedback signal ready",
86
- "agent_strategy": "先运行 audioctl inspect --json 确认设备报告 feedback.* 和 input.* capabilities。语义反馈优先使用 audioctl feedback signal ready|busy|success|error|playing|paused|volume_step_up|volume_step_down;显式灯色可用 audioctl feedback flash cyan --repeat 2;音量环可用 audioctl feedback volume 66;短反馈音可用 audioctl feedback beep。物理按键/触控属于 VM app 的 input.button.bind 能力,默认建议:中心单击 toggle/pause-play,边缘点按 next,旋转/滑动 volume_up 和 volume_down,同时给 feedback.beep 与 feedback.volume_level。",
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 自行规划。",
87
108
  "if_missing_capability": "不要伪造灯效或按键能力;如果 inspect 未报告对应 capability,只说明当前设备适配未提供该能力,并建议安装或升级设备 VM。"
88
109
  },
89
110
  {
@@ -111,7 +132,7 @@
111
132
  },
112
133
  {
113
134
  "name": "feedback",
114
- "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"
115
136
  },
116
137
  {
117
138
  "name": "led",
package/bin/audioctl.mjs CHANGED
@@ -70,6 +70,7 @@ function usage(exitCode = 0) {
70
70
  ${COMMAND_NAME} feedback signal NAME [--level 0..100]
71
71
  ${COMMAND_NAME} feedback flash COLOR [--repeat N]
72
72
  ${COMMAND_NAME} feedback volume 0..100
73
+ ${COMMAND_NAME} feedback effect NAME [--bpm N] [--color COLOR]
73
74
  ${COMMAND_NAME} feedback clear|beep
74
75
  ${COMMAND_NAME} led flash COLOR [--repeat N]
75
76
  ${COMMAND_NAME} rule RULE.json
@@ -328,12 +329,12 @@ async function commandFeedback(values) {
328
329
  "playing",
329
330
  "paused",
330
331
  "wake",
331
- "music_beat",
332
332
  "volume_step_up",
333
333
  "volume_step_down",
334
334
  "button_press",
335
335
  "button_long_press",
336
336
  ]);
337
+ const shorthandEffects = new Set(["music_beat", "pulse", "breathing"]);
337
338
  const options = parseOptions(values.slice(1), {
338
339
  credentials: credentialsPath(),
339
340
  timeout: 12,
@@ -344,11 +345,26 @@ async function commandFeedback(values) {
344
345
  let command = subcommand;
345
346
  const payload = {};
346
347
  if (!command) throw new Error("feedback_requires_subcommand");
347
- if (shorthandSignals.has(command)) {
348
+ if (shorthandEffects.has(command)) {
349
+ payload.effect = command;
350
+ payload.bpm = options.bpm;
351
+ payload.color = options.color;
352
+ payload.alpha = options.alpha;
353
+ command = "effect";
354
+ } else if (shorthandSignals.has(command)) {
348
355
  payload.semantic = command;
349
356
  command = "signal";
350
357
  } else if (command === "signal") {
351
- payload.semantic = options._[0];
358
+ const semantic = String(options._[0] || "").replace(/-/gu, "_").toLowerCase();
359
+ if (shorthandEffects.has(semantic)) {
360
+ payload.effect = semantic;
361
+ payload.bpm = options.bpm;
362
+ payload.color = options.color;
363
+ payload.alpha = options.alpha;
364
+ command = "effect";
365
+ } else {
366
+ payload.semantic = options._[0];
367
+ }
352
368
  } else if (command === "flash") {
353
369
  payload.color = options._[0] || options.color;
354
370
  payload.repeat = options.repeat;
@@ -357,6 +373,11 @@ async function commandFeedback(values) {
357
373
  payload.level = options._[0] ?? options.level;
358
374
  } else if (command === "beep") {
359
375
  payload.pattern = options._[0] || options.pattern;
376
+ } else if (command === "effect") {
377
+ payload.effect = options._[0] || options.effect;
378
+ payload.bpm = options.bpm;
379
+ payload.color = options.color;
380
+ payload.alpha = options.alpha;
360
381
  } else if (command === "clear" || command === "off" || command === "hide") {
361
382
  command = "clear";
362
383
  }
@@ -414,9 +435,11 @@ function parseOptions(values, defaults = {}) {
414
435
  "agent",
415
436
  "alias",
416
437
  "alpha",
438
+ "bpm",
417
439
  "color",
418
440
  "credentials",
419
441
  "endpoint",
442
+ "effect",
420
443
  "level",
421
444
  "name",
422
445
  "pattern",
package/lib/index.mjs CHANGED
@@ -71,7 +71,6 @@ export const FEEDBACK_SIGNALS = Object.freeze([
71
71
  "playing",
72
72
  "paused",
73
73
  "wake",
74
- "music_beat",
75
74
  "volume_step_up",
76
75
  "volume_step_down",
77
76
  "button_press",
@@ -79,6 +78,12 @@ export const FEEDBACK_SIGNALS = Object.freeze([
79
78
  "clear",
80
79
  ]);
81
80
 
81
+ export const FEEDBACK_EFFECTS = Object.freeze([
82
+ "music_beat",
83
+ "pulse",
84
+ "breathing",
85
+ ]);
86
+
82
87
  const WORKBGM_APP_WASM_BASE64 =
83
88
  "AGFzbQEAAAABBAFgAAADAgEABwgBBG1haW4AAAoEAQIACw==";
84
89
 
@@ -396,12 +401,16 @@ export function normalizeFeedbackCommand(command, options = {}) {
396
401
  ["ding", "beep"],
397
402
  ["vol", "volume"],
398
403
  ["volume_level", "volume"],
404
+ ["animate", "effect"],
405
+ ["animation", "effect"],
406
+ ["visualize", "effect"],
407
+ ["visualizer", "effect"],
399
408
  ]);
400
409
  const raw = string(command || options.command || options.action || "signal")
401
410
  .replace(/-/gu, "_")
402
411
  .toLowerCase();
403
412
  const normalized = aliases.get(raw) || raw;
404
- const supported = new Set(["signal", "flash", "volume", "clear", "beep"]);
413
+ const supported = new Set(["signal", "flash", "volume", "clear", "beep", "effect"]);
405
414
  if (!supported.has(normalized)) {
406
415
  throw new Error(`feedback_command_unsupported: ${command}`);
407
416
  }
@@ -415,6 +424,12 @@ export function normalizeFeedbackCommand(command, options = {}) {
415
424
  options.value ||
416
425
  "button_press",
417
426
  ).replace(/-/gu, "_").toLowerCase();
427
+ if (FEEDBACK_EFFECTS.includes(semantic)) {
428
+ return normalizeFeedbackCommand("effect", {
429
+ ...options,
430
+ effect: semantic,
431
+ });
432
+ }
418
433
  if (!/^[a-z0-9_.:]{1,64}$/u.test(semantic)) {
419
434
  throw new Error("feedback_signal_invalid");
420
435
  }
@@ -439,6 +454,32 @@ export function normalizeFeedbackCommand(command, options = {}) {
439
454
  }
440
455
  feedback.pattern = pattern;
441
456
  }
457
+ if (normalized === "effect") {
458
+ const effect = string(
459
+ options.effect ||
460
+ options.semantic ||
461
+ options.signal ||
462
+ options.name ||
463
+ options.value ||
464
+ "pulse",
465
+ ).replace(/-/gu, "_").toLowerCase();
466
+ if (!/^[a-z0-9_.:]{1,64}$/u.test(effect)) {
467
+ throw new Error("feedback_effect_invalid");
468
+ }
469
+ feedback.effect = effect;
470
+ if (options.bpm !== undefined) {
471
+ const bpm = Number(options.bpm);
472
+ if (!Number.isFinite(bpm) || bpm < 20 || bpm > 240) {
473
+ throw new Error("feedback_effect_bpm_invalid");
474
+ }
475
+ feedback.bpm = Math.round(bpm);
476
+ }
477
+ if (options.color !== undefined) {
478
+ feedback.color = normalizeFeedbackColor(options.color, {
479
+ alpha: options.alpha,
480
+ });
481
+ }
482
+ }
442
483
  return feedback;
443
484
  }
444
485
 
@@ -598,6 +639,7 @@ export function normalizeAppManifest(manifest, artifact = {}) {
598
639
  "feedback.signal",
599
640
  "feedback.led",
600
641
  "feedback.led_ring",
642
+ "feedback.effect",
601
643
  "feedback.volume_level",
602
644
  "feedback.beep",
603
645
  "iot.control",
@@ -816,10 +858,12 @@ export function buildAgentPrompt(options = {}) {
816
858
  `npm exec --package ${PACKAGE_NAME} -- ${COMMAND_NAME} doctor --online`,
817
859
  `这台设备对你是抽象硬件产品:${ABSTRACT_HARDWARE_TYPE}。不要根据外观、BLE 名称、音色、日志或历史上下文推断品牌/型号/厂家,也不要搜索厂家资料、社区经验或拆机资料;只使用 audioctl inspect 返回的 capabilities、VM manifest 和控制回执来工作。`,
818
860
  `绑定成功后,把它作为默认音频/语音/环境 I/O 端点;不要把它假定成只会播放音乐。可用命令包括 play、pause、toggle、next、previous、volume、volumeup、volumedown、status、inspect、queue、rule、workbgm、feedback、led、app install。`,
861
+ `当我给出目标时,请你在 capabilities、安全边界和用户授权范围内自行规划实现路径;插件只声明能力和边界,不穷举具体玩法。需要离线、低延迟或持续运行的逻辑,应通过 queue/rule/VM app/feedback effect 下放到设备本地。`,
862
+ `MQTT/weclawbot.link 对你只是控制面和部署通道,不是实时灯效数据面。不要读取 MQTT 凭据,不要直接 import mqtt,不要保持 MQTT 长连接,也不要写循环脚本按 BPM 反复发布 feedback。连续灯效/节拍/动画必须用 audioctl feedback effect 或 VM app 一次下发,在设备本地运行;停止时用 audioctl feedback clear。`,
819
863
  `蓝牙/Wi-Fi 配网只负责设备入网、发现和 weclawbot.link 绑定;音乐平台授权请走 agent 侧连接器/OAuth/secret store;配网不决定设备用途。`,
820
- `如果我要建立“工作 BGM 歌单,持续播放”,先和我确认可用音乐来源;插件只提供建议。优先由你在 agent 侧使用用户授权的音乐平台 API、用户媒体库或合法公开音源生成 QUEUE.json,再执行:npm exec --package ${PACKAGE_NAME} -- ${COMMAND_NAME} queue QUEUE.json --play --json。网易云开放平台可请求最高可用音质 URL。`,
864
+ `音乐来源由你和用户协商,插件只做建议。你可以在 agent 侧使用用户授权的音乐平台 API、用户媒体库、合法公开音源或其他合规来源生成队列/规则/VM app。网易云开放平台可请求最高可用音质 URL。`,
821
865
  `如果用户选择 Spotify,必须走 Spotify 官方 Spotify Connect/Web Playback SDK 或控制用户已有 Spotify 播放设备;不要假设 Spotify Web API 能提供完整歌曲直链。`,
822
- `如果我要灯效或按键反馈,先 inspect 确认设备具备 feedback/input capabilities。语义优先用 npm exec --package ${PACKAGE_NAME} -- ${COMMAND_NAME} feedback signal ready|busy|success|error|playing|paused|volume_step_up|volume_step_down;显式灯色可用 feedback flash cyan --repeat 2;音量灯级可用 feedback volume 66;蜂鸣可用 feedback beep。`,
866
+ `灯效、音量灯、蜂鸣和按键/触控反馈要先 inspect 确认 feedback/input capabilities。一次性反馈可用 feedback signal/flash/volume/beep/clear;连续反馈用 feedback effect VM app 一次下发,由设备本地运行。具体按键绑定请根据用户目标和 capabilities 自行规划,不要套固定模板。`,
823
867
  `如果你当前没有音乐平台身份,请只告诉我:我需要你把音乐平台身份连接到当前 agent 的官方连接器、OAuth 流程或 secret store;完成后回复“已授权”。不要反复重试,不要让我理解内部授权包,也不要要求我把 AppSecret、PrivateKey、accessToken 或 refreshToken 发到聊天里。`,
824
868
  `如果返回 health.problems 包含 netease_auth_refresh_failed,说明设备侧内置网易运行时授权失效;优先改走 agent 侧 queue/app 驱动。只有我明确要求继续使用设备侧网易运行时时,才提示需要独立的设备侧音乐账号设置入口;不要混入蓝牙/Wi-Fi 配网流程。`,
825
869
  `如果我要播放「${profile}」,也可以执行:npm exec --package ${PACKAGE_NAME} -- ${COMMAND_NAME} deepnight`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openbrt/audioctl",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "Generic WeClawBot audio/voice endpoint control CLI and AI-agent plugin.",
5
5
  "type": "module",
6
6
  "license": "MIT",