@openbrt/audioctl 0.1.11 → 0.1.12

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
@@ -83,6 +83,8 @@ audioctl light set all --color amber --brightness 0.35
83
83
  audioctl light timeline ./hero.light.json
84
84
  audioctl light bind ./hero.light-bindings.json
85
85
  audioctl light clear
86
+ audioctl firmware check --json
87
+ audioctl firmware update --yes --json
86
88
  audioctl feedback signal ready
87
89
  audioctl feedback flash cyan --repeat 2
88
90
  audioctl feedback volume 66
@@ -97,8 +99,25 @@ audioctl prompt 123456 --agent codex --alias 视听房音箱
97
99
 
98
100
  `inspect` asks the device for a redacted configuration/status snapshot over the
99
101
  existing MQTT channel. Agent scripts can read Wi-Fi connection state, BLE
100
- provisioning state, wake-word capability, playback, VM/app status, and health
101
- without receiving Wi-Fi passwords, MQTT credentials, or music-platform secrets.
102
+ provisioning state, wake-word capability, playback, VM/app status, firmware
103
+ runtime version, and health without receiving Wi-Fi passwords, MQTT
104
+ credentials, or music-platform secrets.
105
+
106
+ `firmware check` reads the official firmware index from
107
+ `https://weclawbot.link/firmware/audio/manifest.json`, compares it with
108
+ `inspect.firmware.version`, and reports whether a host/runtime update is
109
+ available. `firmware update` is intentionally gated: without `--yes` it only
110
+ prints the update plan and `confirmation_required`. An agent must show the
111
+ version, source and notes to the user first, then run `audioctl firmware update
112
+ --yes --json` only after the user explicitly confirms. Firmware updates are for
113
+ the device host/runtime and VM manager; they do not erase Wi-Fi, MQTT binding,
114
+ music-platform settings, queues, rules, or installed VM app slots.
115
+
116
+ ```bash
117
+ audioctl firmware check --json
118
+ audioctl firmware update --json # dry plan, no mutation
119
+ audioctl firmware update --yes --json # runs only after explicit user approval
120
+ ```
102
121
 
103
122
  `light` controls expose programmable visual surfaces. Agents should use
104
123
  `audioctl light describe --json` to inspect surfaces such as `all`, `ring`, and
@@ -265,3 +284,33 @@ Light messages use a distinct command envelope:
265
284
 
266
285
  Supported light commands are `describe`, `set`, `timeline`, `bind`, and
267
286
  `clear`. Continuous light timing and event reactions run on the device VM.
287
+
288
+ Firmware update messages also use the same MQTT envelope, but carry only an
289
+ official release descriptor. The device downloads the artifact itself from
290
+ `weclawbot.link`, enforces HTTPS, checks SHA-256 and byte size, installs from a
291
+ staging directory, restarts local services, and reports health through
292
+ `inspect`.
293
+
294
+ ```json
295
+ {
296
+ "schema": "weclawbot.control.v1",
297
+ "id": "firmware_<uuid>",
298
+ "kind": "firmware_update",
299
+ "firmware": {
300
+ "schema": "weclawbot.firmware.update.v1",
301
+ "confirmed_by_user": true,
302
+ "release": {
303
+ "schema": "weclawbot.firmware.release.v1",
304
+ "device_class": "audio_voice_ambient_endpoint",
305
+ "runtime": "rokid_music_runtime",
306
+ "version": "0.1.1",
307
+ "channel": "stable",
308
+ "artifact": {
309
+ "url": "https://weclawbot.link/firmware/audio/rokid-music-runtime-0.1.1.tar",
310
+ "sha256": "<64 hex chars>",
311
+ "bytes": 123456
312
+ }
313
+ }
314
+ }
315
+ }
316
+ ```
package/agent-plugin.json CHANGED
@@ -18,7 +18,8 @@
18
18
  "不要读取或解析 audioctl 本地 MQTT 凭据文件",
19
19
  "不要直接 import mqtt、打开或保持 MQTT 长连接来控制设备",
20
20
  "不要写 agent 端 while/sleep/BPM loop 来反复发布 feedback/LED 命令",
21
- "不要把 weclawbot.link 当作实时灯效、音频分析或本地交互的数据面"
21
+ "不要把 weclawbot.link 当作实时灯效、音频分析或本地交互的数据面",
22
+ "不要在未获得用户明确确认时执行固件更新;agent 只能先发现新版并说明变更、版本和来源"
22
23
  ],
23
24
  "continuous_feedback": "连续灯效、节拍灯、呼吸灯、动画和按键反馈必须通过 audioctl light timeline/bind、VM app 或兼容的 feedback effect 一次下发,在设备本地运行;停止使用 audioctl light clear 或 feedback clear。"
24
25
  },
@@ -28,7 +29,8 @@
28
29
  "把用户自然语言目标拆成需要的 I/O、媒体、反馈、输入绑定和持久运行需求",
29
30
  "在用户已授权的平台、连接器、OAuth 或 secret store 中选择合法数据来源",
30
31
  "决定使用 queue、rule、light timeline/bind、feedback、input binding 或 VM app install 的组合",
31
- "在设备 VM 内放置需要离线、低延迟或持续运行的逻辑"
32
+ "在设备 VM 内放置需要离线、低延迟或持续运行的逻辑",
33
+ "运行 audioctl firmware check 发现官网固件新版,并在用户确认后运行 audioctl firmware update --yes"
32
34
  ],
33
35
  "not_prescribed": "插件不会也无法穷举所有 agent 方案;agent 应读取 capabilities 后自己想办法达到目标。"
34
36
  },
@@ -83,6 +85,7 @@
83
85
  "event.bind",
84
86
  "iot.control",
85
87
  "device.inspect",
88
+ "firmware.update",
86
89
  "music.autopilot",
87
90
  "music.set_rule",
88
91
  "music.set_queue",
@@ -117,6 +120,13 @@
117
120
  "intent": "让音箱作为语音遥控、家庭智能或语音互动端点",
118
121
  "primary_command": "audioctl app install MANIFEST.json APP.wasm",
119
122
  "agent_strategy": "不要把设备假定为音乐播放器。先运行 audioctl inspect --json 读取设备能力,再生成声明最小 capabilities 的 VM app。语音、TTS、家居控制等能力必须通过 manifest capabilities 显式声明;设备侧只执行授权范围内的 I/O。"
123
+ },
124
+ {
125
+ "intent": "检查并升级设备固件",
126
+ "primary_command": "audioctl firmware check --json",
127
+ "agent_strategy": "先运行 audioctl firmware check --json,从 https://weclawbot.link/firmware/audio/manifest.json 发现最新官方固件和当前设备版本。只向用户展示版本、来源、release notes 和风险提示;不得自行执行更新。只有用户明确确认后,才运行 audioctl firmware update --yes --json。固件更新是设备 host/runtime 更新,不等同于 VM app install;更新过程中不要读取 MQTT 凭据或私钥。",
128
+ "if_update_available": "向用户确认:官网发现新固件,是否现在更新?说明版本、预计会重启部分本地服务、不会清除 Wi-Fi/MQTT/音乐平台/VM app 配置。",
129
+ "if_user_confirms": "运行 audioctl firmware update --yes --json,并在完成后运行 audioctl inspect --json 验证 firmware.version、health 和 vm 状态。"
120
130
  }
121
131
  ],
122
132
  "commands": [
@@ -164,6 +174,10 @@
164
174
  "name": "app install",
165
175
  "usage": "audioctl app install MANIFEST.json APP.wasm"
166
176
  },
177
+ {
178
+ "name": "firmware",
179
+ "usage": "audioctl firmware check [--manifest URL] | firmware update [--manifest URL] --yes"
180
+ },
167
181
  {
168
182
  "name": "workbgm",
169
183
  "usage": "audioctl workbgm --play"
package/bin/audioctl.mjs CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  COMMAND_NAME,
8
8
  DEFAULT_CREDENTIALS_PATH,
9
9
  DEFAULT_ENDPOINT,
10
+ DEFAULT_FIRMWARE_MANIFEST_URL,
10
11
  DEEPNIGHT_RULE,
11
12
  PACKAGE_NAME,
12
13
  bindAgent,
@@ -14,12 +15,15 @@ import {
14
15
  buildAgentPrompt,
15
16
  buildAudioControl,
16
17
  buildFeedbackControl,
18
+ buildFirmwareUpdateControl,
17
19
  buildLightControl,
18
20
  buildQueueControl,
19
21
  buildWorkBgmAppInstallControl,
20
22
  expandPath,
21
23
  normalizeCredentials,
24
+ planFirmwareUpdate,
22
25
  publishAudioControl,
26
+ readFirmwareIndex,
23
27
  readCredentials,
24
28
  redactAgentFacingValue,
25
29
  remediationForStatus,
@@ -48,6 +52,7 @@ async function run(name, values) {
48
52
  if (name === "inspect") return commandInspect(values);
49
53
  if (name === "prompt") return commandPrompt(values);
50
54
  if (name === "manifest") return commandManifest();
55
+ if (name === "firmware") return commandFirmware(values);
51
56
  if (name === "app") return commandApp(values);
52
57
  if (name === "queue") return commandQueue(values);
53
58
  if (name === "rule") return commandRule(values);
@@ -80,6 +85,8 @@ function usage(exitCode = 0) {
80
85
  ${COMMAND_NAME} light timeline TIMELINE.json
81
86
  ${COMMAND_NAME} light bind BINDINGS.json
82
87
  ${COMMAND_NAME} light clear
88
+ ${COMMAND_NAME} firmware check [--manifest URL]
89
+ ${COMMAND_NAME} firmware update [--manifest URL] [--yes]
83
90
  ${COMMAND_NAME} rule RULE.json
84
91
  ${COMMAND_NAME} queue QUEUE.json [--play]
85
92
  ${COMMAND_NAME} app install MANIFEST.json APP.wasm
@@ -90,6 +97,7 @@ function usage(exitCode = 0) {
90
97
  Options:
91
98
  --credentials FILE override audioctl-managed local credential file
92
99
  --endpoint URL default: ${DEFAULT_ENDPOINT}
100
+ --manifest URL default: ${DEFAULT_FIRMWARE_MANIFEST_URL}
93
101
  --timeout SECONDS default: 12
94
102
  --json machine-readable output
95
103
  --nowait publish without waiting for device status
@@ -208,6 +216,78 @@ async function commandManifest() {
208
216
  process.stdout.write(`${JSON.stringify(manifest, null, 2)}\n`);
209
217
  }
210
218
 
219
+ async function commandFirmware(values) {
220
+ const subcommand = String(values[0] || "check").replace(/-/gu, "_").toLowerCase();
221
+ if (!new Set(["check", "update", "upgrade"]).has(subcommand)) {
222
+ throw new Error(`firmware_subcommand_unsupported: ${subcommand}`);
223
+ }
224
+ const options = parseOptions(values.slice(1), {
225
+ credentials: credentialsPath(),
226
+ manifest: process.env.WEC_FIRMWARE_MANIFEST || DEFAULT_FIRMWARE_MANIFEST_URL,
227
+ channel: "stable",
228
+ timeout: subcommand === "check" ? 20 : 120,
229
+ json: false,
230
+ nowait: false,
231
+ yes: false,
232
+ force: false,
233
+ });
234
+ const credentials = await readCredentials(options.credentials);
235
+ const firmwareIndex = await readFirmwareIndex(options.manifest, {
236
+ timeoutMs: Number(options.timeout) * 1000,
237
+ });
238
+ const statusDelivery = await publishAudioControl(credentials, buildAudioControl("status"), {
239
+ timeoutMs: Number(options.timeout) * 1000,
240
+ });
241
+ if (statusDelivery.status?.kind === "rejected") {
242
+ print(statusDelivery.status, options.json);
243
+ process.exitCode = 1;
244
+ return;
245
+ }
246
+ const inspect = statusDelivery.status?.inspect || statusDelivery.status || {};
247
+ const plan = planFirmwareUpdate(firmwareIndex, inspect, {
248
+ channel: options.channel,
249
+ force: options.force,
250
+ });
251
+ if (subcommand === "check") {
252
+ print(plan, options.json);
253
+ return;
254
+ }
255
+ if (!plan.supported) {
256
+ print({
257
+ ...plan,
258
+ update_started: false,
259
+ reason: "device_does_not_advertise_firmware.update",
260
+ }, options.json);
261
+ process.exitCode = 1;
262
+ return;
263
+ }
264
+ if (!plan.release || !plan.update_available) {
265
+ print({
266
+ ...plan,
267
+ update_started: false,
268
+ }, options.json);
269
+ return;
270
+ }
271
+ if (!options.yes) {
272
+ print({
273
+ ...plan,
274
+ update_started: false,
275
+ confirmation_required: true,
276
+ next_step: `Ask the user to confirm, then run: ${COMMAND_NAME} firmware update --yes`,
277
+ }, options.json);
278
+ return;
279
+ }
280
+ const control = buildFirmwareUpdateControl(plan.release, {
281
+ yes: true,
282
+ });
283
+ const delivery = await publishAudioControl(credentials, control, {
284
+ timeoutMs: Number(options.timeout) * 1000,
285
+ wait: !options.nowait,
286
+ });
287
+ print(delivery.status || delivery, options.json);
288
+ if (delivery.status?.kind === "rejected") process.exitCode = 1;
289
+ }
290
+
211
291
  async function commandRule(values) {
212
292
  const options = parseOptions(values, {
213
293
  credentials: credentialsPath(),
@@ -485,7 +565,7 @@ function parseOptions(values, defaults = {}) {
485
565
  continue;
486
566
  }
487
567
  const key = value.slice(2);
488
- if (key === "json" || key === "online" || key === "nowait" || key === "play") {
568
+ if (key === "json" || key === "online" || key === "nowait" || key === "play" || key === "yes" || key === "force") {
489
569
  options[key] = true;
490
570
  continue;
491
571
  }
@@ -499,7 +579,9 @@ function parseOptions(values, defaults = {}) {
499
579
  "credentials",
500
580
  "endpoint",
501
581
  "effect",
582
+ "channel",
502
583
  "level",
584
+ "manifest",
503
585
  "name",
504
586
  "pattern",
505
587
  "profile",
package/lib/index.mjs CHANGED
@@ -8,6 +8,8 @@ import mqtt from "mqtt";
8
8
  export const PACKAGE_NAME = "@openbrt/audioctl";
9
9
  export const COMMAND_NAME = "audioctl";
10
10
  export const DEFAULT_ENDPOINT = "https://weclawbot.link/byoa";
11
+ export const DEFAULT_FIRMWARE_MANIFEST_URL =
12
+ "https://weclawbot.link/firmware/audio/manifest.json";
11
13
  export const DEFAULT_CREDENTIALS_PATH = path.join(
12
14
  os.homedir(),
13
15
  ".config",
@@ -50,6 +52,8 @@ export const WORKBGM_RULE = Object.freeze({
50
52
  export const PLAYABLE_QUEUE_SCHEMA = "weclawbot.playable_queue.v1";
51
53
  export const LIGHT_TIMELINE_SCHEMA = "weclawbot.light.timeline.v1";
52
54
  export const LIGHT_BINDINGS_SCHEMA = "weclawbot.light.bind.v1";
55
+ export const FIRMWARE_INDEX_SCHEMA = "weclawbot.firmware.index.v1";
56
+ export const FIRMWARE_RELEASE_SCHEMA = "weclawbot.firmware.release.v1";
53
57
 
54
58
  export const FEEDBACK_COLOR_NAMES = Object.freeze({
55
59
  red: Object.freeze({ red: 255, green: 0, blue: 0, alpha: 255 }),
@@ -284,6 +288,20 @@ export function buildAppInstallControl(options = {}) {
284
288
  };
285
289
  }
286
290
 
291
+ export function buildFirmwareUpdateControl(release, options = {}) {
292
+ const firmwareRelease = normalizeFirmwareRelease(release);
293
+ return {
294
+ schema: "weclawbot.control.v1",
295
+ id: string(options.id) || `firmware_${crypto.randomUUID()}`,
296
+ kind: "firmware_update",
297
+ firmware: {
298
+ schema: "weclawbot.firmware.update.v1",
299
+ release: firmwareRelease,
300
+ confirmed_by_user: options.confirmedByUser === true || options.yes === true,
301
+ },
302
+ };
303
+ }
304
+
287
305
  export function buildWorkBgmAppInstallControl(options = {}) {
288
306
  const play = options.play === true || options.autoplay === true;
289
307
  return buildAppInstallControl({
@@ -701,6 +719,142 @@ function normalizeLightEvent(value) {
701
719
  return event;
702
720
  }
703
721
 
722
+ export async function readFirmwareIndex(source = DEFAULT_FIRMWARE_MANIFEST_URL, options = {}) {
723
+ const location = string(source) || DEFAULT_FIRMWARE_MANIFEST_URL;
724
+ let payload;
725
+ if (/^https?:\/\//iu.test(location)) {
726
+ const response = await fetch(location, {
727
+ headers: { accept: "application/json" },
728
+ signal: AbortSignal.timeout(Math.max(1000, Number(options.timeoutMs) || 15_000)),
729
+ });
730
+ if (!response.ok) {
731
+ throw new Error(`firmware_manifest_http_${response.status}`);
732
+ }
733
+ payload = await response.json();
734
+ } else if (location.startsWith("file://")) {
735
+ payload = JSON.parse(await fs.readFile(new URL(location), "utf8"));
736
+ } else {
737
+ payload = JSON.parse(await fs.readFile(expandPath(location), "utf8"));
738
+ }
739
+ return normalizeFirmwareIndex(payload, location);
740
+ }
741
+
742
+ export function normalizeFirmwareIndex(value, source = DEFAULT_FIRMWARE_MANIFEST_URL) {
743
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
744
+ throw new Error("firmware_manifest_invalid");
745
+ }
746
+ const releasesInput = Array.isArray(value.releases) ? value.releases : [];
747
+ if (value.schema !== FIRMWARE_INDEX_SCHEMA || releasesInput.length < 1) {
748
+ throw new Error("firmware_manifest_invalid");
749
+ }
750
+ return {
751
+ schema: FIRMWARE_INDEX_SCHEMA,
752
+ device_class: string(value.device_class || ABSTRACT_HARDWARE_TYPE).slice(0, 80),
753
+ channel: string(value.channel || "stable").slice(0, 40),
754
+ generated_at: string(value.generated_at).slice(0, 80),
755
+ source: string(source).slice(0, 240),
756
+ releases: releasesInput.map(normalizeFirmwareRelease),
757
+ };
758
+ }
759
+
760
+ export function normalizeFirmwareRelease(value) {
761
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
762
+ throw new Error("firmware_release_invalid");
763
+ }
764
+ const artifact = value.artifact && typeof value.artifact === "object" ? value.artifact : {};
765
+ const url = string(artifact.url || value.url);
766
+ if (!/^https:\/\/(?:www\.)?weclawbot\.link\/firmware\//iu.test(url)) {
767
+ throw new Error("firmware_artifact_url_invalid");
768
+ }
769
+ const sha256 = string(artifact.sha256 || value.sha256).toLowerCase();
770
+ if (!/^[0-9a-f]{64}$/u.test(sha256)) {
771
+ throw new Error("firmware_artifact_sha256_invalid");
772
+ }
773
+ const bytes = Math.floor(Number(artifact.bytes || artifact.size || value.bytes || value.size) || 0);
774
+ if (bytes < 1 || bytes > 96 * 1024 * 1024) {
775
+ throw new Error("firmware_artifact_size_invalid");
776
+ }
777
+ const version = string(value.version).replace(/^v/iu, "");
778
+ if (!/^[0-9]+(?:\.[0-9]+){1,3}(?:[-+][A-Za-z0-9_.-]+)?$/u.test(version)) {
779
+ throw new Error("firmware_version_invalid");
780
+ }
781
+ return {
782
+ schema: FIRMWARE_RELEASE_SCHEMA,
783
+ device_class: string(value.device_class || ABSTRACT_HARDWARE_TYPE).slice(0, 80),
784
+ runtime: string(value.runtime || "rokid_music_runtime").slice(0, 80),
785
+ version,
786
+ channel: string(value.channel || "stable").slice(0, 40),
787
+ host_api: string(value.host_api || "weclawbot.host.v0").slice(0, 80),
788
+ released_at: string(value.released_at).slice(0, 80),
789
+ min_current_version: string(value.min_current_version || "0.0.0").replace(/^v/iu, "").slice(0, 40),
790
+ requires_user_confirmation: value.requires_user_confirmation !== false,
791
+ notes: Array.isArray(value.notes)
792
+ ? value.notes.map((note) => string(note).slice(0, 180)).filter(Boolean).slice(0, 12)
793
+ : [],
794
+ artifact: {
795
+ url,
796
+ sha256,
797
+ bytes,
798
+ format: string(artifact.format || "tar").slice(0, 32),
799
+ },
800
+ };
801
+ }
802
+
803
+ export function compareVersions(a, b) {
804
+ const parse = (value) => string(value)
805
+ .replace(/^v/iu, "")
806
+ .split(/[-+]/u)[0]
807
+ .split(".")
808
+ .map((part) => Number.parseInt(part, 10) || 0);
809
+ const left = parse(a);
810
+ const right = parse(b);
811
+ const length = Math.max(left.length, right.length, 3);
812
+ for (let index = 0; index < length; index += 1) {
813
+ const difference = (left[index] || 0) - (right[index] || 0);
814
+ if (difference !== 0) return difference > 0 ? 1 : -1;
815
+ }
816
+ return 0;
817
+ }
818
+
819
+ export function planFirmwareUpdate(index, inspect = {}, options = {}) {
820
+ const firmwareIndex = normalizeFirmwareIndex(index, index?.source || DEFAULT_FIRMWARE_MANIFEST_URL);
821
+ const deviceClass =
822
+ string(inspect.device_type) ||
823
+ string(inspect.device?.abstract_hardware) ||
824
+ ABSTRACT_HARDWARE_TYPE;
825
+ const channel = string(options.channel || firmwareIndex.channel || "stable");
826
+ const currentVersion =
827
+ string(inspect.firmware?.version || inspect.device?.firmware_version || "0.0.0")
828
+ .replace(/^v/iu, "") || "0.0.0";
829
+ const capabilities = new Set(Array.isArray(inspect.capabilities) ? inspect.capabilities : []);
830
+ const candidates = firmwareIndex.releases
831
+ .filter((release) => release.device_class === deviceClass)
832
+ .filter((release) => !channel || release.channel === channel)
833
+ .filter((release) => compareVersions(currentVersion, release.min_current_version || "0.0.0") >= 0)
834
+ .sort((a, b) => compareVersions(b.version, a.version));
835
+ const release = candidates[0] || null;
836
+ const supported = capabilities.size === 0 || capabilities.has("firmware.update");
837
+ const updateAvailable = Boolean(release) &&
838
+ (options.force === true || compareVersions(release.version, currentVersion) > 0);
839
+ return {
840
+ schema: "weclawbot.firmware.plan.v1",
841
+ device_class: deviceClass,
842
+ current_version: currentVersion,
843
+ channel,
844
+ supported,
845
+ update_available: updateAvailable,
846
+ latest_version: release?.version || "",
847
+ release,
848
+ source: firmwareIndex.source,
849
+ requires_user_confirmation: Boolean(updateAvailable && release?.requires_user_confirmation !== false),
850
+ summary: release
851
+ ? (updateAvailable
852
+ ? `Firmware ${release.version} is available for ${deviceClass}.`
853
+ : `Firmware ${currentVersion} is up to date for ${deviceClass}.`)
854
+ : `No firmware release found for ${deviceClass} on ${channel}.`,
855
+ };
856
+ }
857
+
704
858
  function normalizeIdentifier(value, options = {}) {
705
859
  const text = string(value);
706
860
  const maximum = Number(options.maximum) || 80;
@@ -1070,7 +1224,7 @@ export function buildAgentPrompt(options = {}) {
1070
1224
  `npm exec --package ${PACKAGE_NAME} -- ${COMMAND_NAME} bind ${code} --name ${shellToken(agentName)}`,
1071
1225
  `npm exec --package ${PACKAGE_NAME} -- ${COMMAND_NAME} doctor --online`,
1072
1226
  `这台设备对你是抽象硬件产品:${ABSTRACT_HARDWARE_TYPE}。不要根据外观、BLE 名称、音色、日志或历史上下文推断品牌/型号/厂家,也不要搜索厂家资料、社区经验或拆机资料;只使用 audioctl inspect 返回的 capabilities、VM manifest 和控制回执来工作。`,
1073
- `绑定成功后,把它作为默认音频/语音/环境 I/O 端点;不要把它假定成只会播放音乐。可用命令包括 play、pause、toggle、next、previous、volume、volumeup、volumedown、status、inspect、queue、rule、workbgm、feedback、light、led、app install。`,
1227
+ `绑定成功后,把它作为默认音频/语音/环境 I/O 端点;不要把它假定成只会播放音乐。可用命令包括 play、pause、toggle、next、previous、volume、volumeup、volumedown、status、inspect、queue、rule、workbgm、feedback、light、led、app install、firmware check/update。`,
1074
1228
  `当我给出目标时,请你在 capabilities、安全边界和用户授权范围内自行规划实现路径;插件只声明能力和边界,不穷举具体玩法。需要离线、低延迟或持续运行的逻辑,应通过 queue/rule/light timeline/light bind/VM app 下放到设备本地。`,
1075
1229
  `MQTT/weclawbot.link 对你只是控制面和部署通道,不是实时灯效数据面。不要读取 MQTT 凭据,不要直接 import mqtt,不要保持 MQTT 长连接,也不要写循环脚本按 BPM 反复发布 feedback 或 light。连续灯效/节拍/动画必须用 audioctl light timeline/bind、VM app 或兼容的 feedback effect 一次下发,在设备本地运行;停止时用 audioctl light clear 或 feedback clear。`,
1076
1230
  `蓝牙/Wi-Fi 配网只负责设备入网、发现和 weclawbot.link 绑定;音乐平台授权请走 agent 侧连接器/OAuth/secret store;配网不决定设备用途。`,
@@ -1079,6 +1233,7 @@ export function buildAgentPrompt(options = {}) {
1079
1233
  `灯效、音量灯、蜂鸣和按键/触控反馈要先 inspect 或 light describe 确认 feedback/input/light capabilities。一次性语义反馈可用 feedback signal/flash/volume/beep/clear;复杂灯效请优先生成 light timeline 和 light bind,把外圈、中心等 surface 与 media/input 事件绑定到设备本地。feedback effect 只作为兼容/demo 入口,不要把 music_beat/pulse/breathing 当成能力边界。具体按键绑定请根据用户目标和 capabilities 自行规划,不要套固定模板。`,
1080
1234
  `如果你当前没有音乐平台身份,请只告诉我:我需要你把音乐平台身份连接到当前 agent 的官方连接器、OAuth 流程或 secret store;完成后回复“已授权”。不要反复重试,不要让我理解内部授权包,也不要要求我把 AppSecret、PrivateKey、accessToken 或 refreshToken 发到聊天里。`,
1081
1235
  `如果返回 health.problems 包含 netease_auth_refresh_failed,说明设备侧内置网易运行时授权失效;优先改走 agent 侧 queue/app 驱动。只有我明确要求继续使用设备侧网易运行时时,才提示需要独立的设备侧音乐账号设置入口;不要混入蓝牙/Wi-Fi 配网流程。`,
1236
+ `固件更新由 agent 端发现新版、用户确认后执行:先运行 audioctl firmware check --json,只展示官网来源、当前版本、新版本和 release notes;只有用户明确同意升级后,才运行 audioctl firmware update --yes --json。固件只从 weclawbot.link 官方 manifest 下载,更新设备 host/runtime,不等同于 VM app,也不要用固件更新去实现普通应用需求。`,
1082
1237
  `如果我要播放「${profile}」,也可以执行:npm exec --package ${PACKAGE_NAME} -- ${COMMAND_NAME} deepnight`,
1083
1238
  `不要向设备写入全局音乐平台私钥;平台授权优先保存在 agent 运行环境。只有在我明确授权时,才把最小范围、可轮换、可撤销的 app-scoped 凭据随 VM app 写入设备。`,
1084
1239
  ].filter(Boolean).join("\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openbrt/audioctl",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "Generic WeClawBot audio/voice endpoint control CLI and AI-agent plugin.",
5
5
  "type": "module",
6
6
  "license": "MIT",