@openbrt/audioctl 0.1.0 → 0.1.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 +13 -0
- package/agent-plugin.json +17 -1
- package/bin/audioctl.mjs +104 -3
- package/lib/index.mjs +215 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,6 +20,7 @@ run doctor, then use it as my default BGM/music output device.
|
|
|
20
20
|
npm install -g @openbrt/audioctl
|
|
21
21
|
audioctl bind 123456 --name codex
|
|
22
22
|
audioctl doctor --online
|
|
23
|
+
audioctl inspect
|
|
23
24
|
```
|
|
24
25
|
|
|
25
26
|
One-shot usage:
|
|
@@ -42,11 +43,23 @@ audioctl volume 48
|
|
|
42
43
|
audioctl volumeup
|
|
43
44
|
audioctl volumedown
|
|
44
45
|
audioctl status
|
|
46
|
+
audioctl inspect --json
|
|
47
|
+
audioctl workbgm --play
|
|
45
48
|
audioctl rule ./my-playlist-rule.json
|
|
46
49
|
audioctl deepnight
|
|
47
50
|
audioctl prompt 123456 --agent codex --alias 视听房音箱
|
|
48
51
|
```
|
|
49
52
|
|
|
53
|
+
`inspect` asks the device for a redacted configuration/status snapshot over the
|
|
54
|
+
existing MQTT channel. Agent scripts can read Wi-Fi connection state, BLE
|
|
55
|
+
provisioning state, wake-word capability, playback, VM/app status, and health
|
|
56
|
+
without receiving Wi-Fi passwords, MQTT credentials, or music-platform secrets.
|
|
57
|
+
|
|
58
|
+
`workbgm --play` installs a persistent `work-bgm-autopilot` VM app on the audio
|
|
59
|
+
device, writes the bundled 工作 BGM playlist rule through the device-side music
|
|
60
|
+
adapter, requests a refresh, and starts playback once. The app expresses a
|
|
61
|
+
continuous BGM intent and respects later user-initiated pause/resume controls.
|
|
62
|
+
|
|
50
63
|
`deepnight` pushes the bundled “深夜工作” playlist rule and asks the device to
|
|
51
64
|
refresh/play according to its local runtime. The package never contains music
|
|
52
65
|
platform private keys.
|
package/agent-plugin.json
CHANGED
|
@@ -27,8 +27,12 @@
|
|
|
27
27
|
"audio.previous",
|
|
28
28
|
"audio.seek",
|
|
29
29
|
"audio.set_volume",
|
|
30
|
+
"device.inspect",
|
|
31
|
+
"music.autopilot",
|
|
30
32
|
"music.set_rule",
|
|
31
|
-
"music.
|
|
33
|
+
"music.work_bgm",
|
|
34
|
+
"music.status",
|
|
35
|
+
"app.install"
|
|
32
36
|
],
|
|
33
37
|
"pairing": {
|
|
34
38
|
"endpoint": "https://weclawbot.link/byoa",
|
|
@@ -45,6 +49,10 @@
|
|
|
45
49
|
"name": "doctor",
|
|
46
50
|
"usage": "audioctl doctor --online"
|
|
47
51
|
},
|
|
52
|
+
{
|
|
53
|
+
"name": "inspect",
|
|
54
|
+
"usage": "audioctl inspect --json"
|
|
55
|
+
},
|
|
48
56
|
{
|
|
49
57
|
"name": "control",
|
|
50
58
|
"usage": "audioctl play|pause|toggle|next|previous|volumeup|volumedown|status"
|
|
@@ -56,6 +64,14 @@
|
|
|
56
64
|
{
|
|
57
65
|
"name": "rule",
|
|
58
66
|
"usage": "audioctl rule RULE.json"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"name": "app install",
|
|
70
|
+
"usage": "audioctl app install MANIFEST.json APP.wasm"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"name": "workbgm",
|
|
74
|
+
"usage": "audioctl workbgm --play"
|
|
59
75
|
}
|
|
60
76
|
]
|
|
61
77
|
}
|
package/bin/audioctl.mjs
CHANGED
|
@@ -10,12 +10,15 @@ import {
|
|
|
10
10
|
DEEPNIGHT_RULE,
|
|
11
11
|
PACKAGE_NAME,
|
|
12
12
|
bindAgent,
|
|
13
|
+
buildAppInstallControl,
|
|
13
14
|
buildAgentPrompt,
|
|
14
15
|
buildAudioControl,
|
|
16
|
+
buildWorkBgmAppInstallControl,
|
|
15
17
|
expandPath,
|
|
16
18
|
normalizeCredentials,
|
|
17
19
|
publishAudioControl,
|
|
18
20
|
readCredentials,
|
|
21
|
+
summarizeInspect,
|
|
19
22
|
summarizePlayback,
|
|
20
23
|
testConnection,
|
|
21
24
|
validateRule,
|
|
@@ -37,9 +40,12 @@ if (!command || new Set(["help", "-h", "--help"]).has(command)) {
|
|
|
37
40
|
async function run(name, values) {
|
|
38
41
|
if (name === "bind") return commandBind(values);
|
|
39
42
|
if (name === "doctor") return commandDoctor(values);
|
|
43
|
+
if (name === "inspect") return commandInspect(values);
|
|
40
44
|
if (name === "prompt") return commandPrompt(values);
|
|
41
45
|
if (name === "manifest") return commandManifest();
|
|
46
|
+
if (name === "app") return commandApp(values);
|
|
42
47
|
if (name === "rule") return commandRule(values);
|
|
48
|
+
if (name === "workbgm") return commandWorkBgm(values);
|
|
43
49
|
if (name === "deepnight") return commandDeepnight(values);
|
|
44
50
|
if (name === "volume") return commandVolume(values);
|
|
45
51
|
if (name === "seek") return commandSeek(values);
|
|
@@ -50,11 +56,14 @@ function usage(exitCode = 0) {
|
|
|
50
56
|
process.stdout.write(`Usage:
|
|
51
57
|
${COMMAND_NAME} bind CODE [--name AGENT]
|
|
52
58
|
${COMMAND_NAME} doctor [--online] [--json]
|
|
59
|
+
${COMMAND_NAME} inspect [--json]
|
|
53
60
|
${COMMAND_NAME} prompt CODE [--agent AGENT] [--alias NAME]
|
|
54
61
|
${COMMAND_NAME} play|pause|toggle|next|previous|volumeup|volumedown|status
|
|
55
62
|
${COMMAND_NAME} volume 0..100
|
|
56
63
|
${COMMAND_NAME} seek MILLISECONDS
|
|
57
64
|
${COMMAND_NAME} rule RULE.json
|
|
65
|
+
${COMMAND_NAME} app install MANIFEST.json APP.wasm
|
|
66
|
+
${COMMAND_NAME} workbgm [--play]
|
|
58
67
|
${COMMAND_NAME} deepnight
|
|
59
68
|
${COMMAND_NAME} manifest
|
|
60
69
|
|
|
@@ -110,8 +119,47 @@ async function commandDoctor(values) {
|
|
|
110
119
|
if (options.online) {
|
|
111
120
|
await testConnection(credentials, { timeoutMs: Number(options.timeout) * 1000 });
|
|
112
121
|
status.online = true;
|
|
122
|
+
const delivery = await publishAudioControl(credentials, buildAudioControl("status"), {
|
|
123
|
+
timeoutMs: Number(options.timeout) * 1000,
|
|
124
|
+
});
|
|
125
|
+
status.device_status = delivery.status || null;
|
|
126
|
+
status.healthy = delivery.status?.health?.ok !== false;
|
|
127
|
+
status.ok = status.healthy;
|
|
113
128
|
}
|
|
114
129
|
print(status, options.json);
|
|
130
|
+
if (options.online && status.healthy === false) process.exitCode = 1;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function commandInspect(values) {
|
|
134
|
+
const options = parseOptions(values, {
|
|
135
|
+
credentials: credentialsPath(),
|
|
136
|
+
timeout: 12,
|
|
137
|
+
json: false,
|
|
138
|
+
});
|
|
139
|
+
const credentials = await readCredentials(options.credentials);
|
|
140
|
+
const delivery = await publishAudioControl(credentials, buildAudioControl("status"), {
|
|
141
|
+
timeoutMs: Number(options.timeout) * 1000,
|
|
142
|
+
});
|
|
143
|
+
if (delivery.status?.kind === "rejected") {
|
|
144
|
+
print(delivery.status, options.json);
|
|
145
|
+
process.exitCode = 1;
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const inspect = delivery.status?.inspect || delivery.status || {};
|
|
149
|
+
if (options.json) {
|
|
150
|
+
process.stdout.write(`${JSON.stringify(inspect)}\n`);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const summary = summarizeInspect(inspect);
|
|
154
|
+
process.stdout.write(
|
|
155
|
+
`device: ${summary.device}\n` +
|
|
156
|
+
`wifi: ${summary.wifi}\n` +
|
|
157
|
+
`bluetooth: ${summary.bluetooth}\n` +
|
|
158
|
+
`wakeword: ${summary.wakeword}\n` +
|
|
159
|
+
`playback: ${summary.playback}\n` +
|
|
160
|
+
`vm: ${summary.vm}\n` +
|
|
161
|
+
`health: ${summary.health}\n`,
|
|
162
|
+
);
|
|
115
163
|
}
|
|
116
164
|
|
|
117
165
|
async function commandPrompt(values) {
|
|
@@ -148,6 +196,35 @@ async function commandRule(values) {
|
|
|
148
196
|
await send("set_rule", options, { rule });
|
|
149
197
|
}
|
|
150
198
|
|
|
199
|
+
async function commandApp(values) {
|
|
200
|
+
const subcommand = String(values[0] || "");
|
|
201
|
+
if (subcommand !== "install") {
|
|
202
|
+
throw new Error("app_subcommand_unsupported");
|
|
203
|
+
}
|
|
204
|
+
const options = parseOptions(values.slice(1), {
|
|
205
|
+
credentials: credentialsPath(),
|
|
206
|
+
timeout: 12,
|
|
207
|
+
json: false,
|
|
208
|
+
nowait: false,
|
|
209
|
+
});
|
|
210
|
+
const manifestFile = String(options._[0] || "");
|
|
211
|
+
const wasmFile = String(options._[1] || "");
|
|
212
|
+
if (!manifestFile || !wasmFile) {
|
|
213
|
+
throw new Error("app_install_requires_manifest_and_wasm");
|
|
214
|
+
}
|
|
215
|
+
const control = buildAppInstallControl({
|
|
216
|
+
manifest: JSON.parse(await fs.readFile(manifestFile, "utf8")),
|
|
217
|
+
wasm: await fs.readFile(wasmFile),
|
|
218
|
+
});
|
|
219
|
+
const credentials = await readCredentials(options.credentials);
|
|
220
|
+
const delivery = await publishAudioControl(credentials, control, {
|
|
221
|
+
timeoutMs: Number(options.timeout) * 1000,
|
|
222
|
+
wait: !options.nowait,
|
|
223
|
+
});
|
|
224
|
+
print(delivery.status || delivery, options.json);
|
|
225
|
+
if (delivery.status?.kind === "rejected") process.exitCode = 1;
|
|
226
|
+
}
|
|
227
|
+
|
|
151
228
|
async function commandDeepnight(values) {
|
|
152
229
|
const options = parseOptions(values, {
|
|
153
230
|
credentials: credentialsPath(),
|
|
@@ -158,6 +235,27 @@ async function commandDeepnight(values) {
|
|
|
158
235
|
await send("set_rule", options, { rule: DEEPNIGHT_RULE });
|
|
159
236
|
}
|
|
160
237
|
|
|
238
|
+
async function commandWorkBgm(values) {
|
|
239
|
+
const options = parseOptions(values, {
|
|
240
|
+
credentials: credentialsPath(),
|
|
241
|
+
timeout: 20,
|
|
242
|
+
json: false,
|
|
243
|
+
nowait: false,
|
|
244
|
+
play: false,
|
|
245
|
+
});
|
|
246
|
+
const credentials = await readCredentials(options.credentials);
|
|
247
|
+
const delivery = await publishAudioControl(
|
|
248
|
+
credentials,
|
|
249
|
+
buildWorkBgmAppInstallControl({ play: options.play }),
|
|
250
|
+
{
|
|
251
|
+
timeoutMs: Number(options.timeout) * 1000,
|
|
252
|
+
wait: !options.nowait,
|
|
253
|
+
},
|
|
254
|
+
);
|
|
255
|
+
print(delivery.status || delivery, options.json);
|
|
256
|
+
if (delivery.status?.kind === "rejected") process.exitCode = 1;
|
|
257
|
+
}
|
|
258
|
+
|
|
161
259
|
async function commandVolume(values) {
|
|
162
260
|
const options = parseOptions(values, {
|
|
163
261
|
credentials: credentialsPath(),
|
|
@@ -211,7 +309,7 @@ function parseOptions(values, defaults = {}) {
|
|
|
211
309
|
continue;
|
|
212
310
|
}
|
|
213
311
|
const key = value.slice(2);
|
|
214
|
-
if (key === "json" || key === "online" || key === "nowait") {
|
|
312
|
+
if (key === "json" || key === "online" || key === "nowait" || key === "play") {
|
|
215
313
|
options[key] = true;
|
|
216
314
|
continue;
|
|
217
315
|
}
|
|
@@ -248,12 +346,15 @@ function print(value, json) {
|
|
|
248
346
|
if (value?.kind === "applied" || value?.kind === "rejected") {
|
|
249
347
|
const playback = summarizePlayback(value);
|
|
250
348
|
const track = playback.track || "no track";
|
|
349
|
+
const problems = Array.isArray(value.health?.problems)
|
|
350
|
+
? value.health.problems
|
|
351
|
+
: [];
|
|
251
352
|
process.stdout.write(
|
|
252
353
|
`${value.kind}: ${value.detail || ""}\n` +
|
|
253
|
-
`playback: ${playback.desired}, volume ${playback.volume ?? "-"}, ${track}\n
|
|
354
|
+
`playback: ${playback.desired}, volume ${playback.volume ?? "-"}, ${track}\n` +
|
|
355
|
+
(problems.length > 0 ? `health: ${problems.join(", ")}\n` : "health: ok\n"),
|
|
254
356
|
);
|
|
255
357
|
return;
|
|
256
358
|
}
|
|
257
359
|
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
258
360
|
}
|
|
259
|
-
|
package/lib/index.mjs
CHANGED
|
@@ -30,6 +30,24 @@ export const DEEPNIGHT_RULE = Object.freeze({
|
|
|
30
30
|
refreshHintSeconds: 21600,
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
+
export const WORKBGM_RULE = Object.freeze({
|
|
34
|
+
schemaVersion: 1,
|
|
35
|
+
title: "工作 BGM",
|
|
36
|
+
source: "网易云音乐官方歌单:深夜刷题党|轻缓律动伴你专注学习",
|
|
37
|
+
sourcePlaylist: Object.freeze({
|
|
38
|
+
originalId: "8383869866",
|
|
39
|
+
encryptedId: "661561242F7B3CE80B5AB760DB171EB4",
|
|
40
|
+
}),
|
|
41
|
+
fetchLimit: 80,
|
|
42
|
+
targetTrackCount: 24,
|
|
43
|
+
minimumTrackCount: 8,
|
|
44
|
+
qualityPolicy: "highest-available",
|
|
45
|
+
refreshHintSeconds: 21600,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const WORKBGM_APP_WASM_BASE64 =
|
|
49
|
+
"AGFzbQEAAAABBAFgAAADAgEABwgBBG1haW4AAAoEAQIACw==";
|
|
50
|
+
|
|
33
51
|
export function validateBindingCode(code) {
|
|
34
52
|
const value = string(code).replace(/\s/gu, "");
|
|
35
53
|
if (!/^[A-Za-z0-9_-]{6,64}$/u.test(value)) {
|
|
@@ -156,6 +174,74 @@ export function buildAudioControl(command, options = {}) {
|
|
|
156
174
|
};
|
|
157
175
|
}
|
|
158
176
|
|
|
177
|
+
export function buildAppInstallControl(options = {}) {
|
|
178
|
+
const wasm =
|
|
179
|
+
Buffer.isBuffer(options.wasm) || options.wasm instanceof Uint8Array
|
|
180
|
+
? Buffer.from(options.wasm)
|
|
181
|
+
: Buffer.from(String(options.wasm || ""), "utf8");
|
|
182
|
+
const sha256 = crypto.createHash("sha256").update(wasm).digest("hex");
|
|
183
|
+
const manifest = normalizeAppManifest(options.manifest, {
|
|
184
|
+
sha256,
|
|
185
|
+
wasmBytes: wasm.length,
|
|
186
|
+
});
|
|
187
|
+
return {
|
|
188
|
+
schema: "weclawbot.control.v1",
|
|
189
|
+
id: string(options.id) || `app_${crypto.randomUUID()}`,
|
|
190
|
+
kind: "app_install",
|
|
191
|
+
app: {
|
|
192
|
+
manifest,
|
|
193
|
+
wasm_base64: wasm.toString("base64"),
|
|
194
|
+
activate: options.activate !== false,
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function buildWorkBgmAppInstallControl(options = {}) {
|
|
200
|
+
const play = options.play === true || options.autoplay === true;
|
|
201
|
+
return buildAppInstallControl({
|
|
202
|
+
id: string(options.id) || `workbgm_${crypto.randomUUID()}`,
|
|
203
|
+
wasm: Buffer.from(WORKBGM_APP_WASM_BASE64, "base64"),
|
|
204
|
+
manifest: {
|
|
205
|
+
schema: "weclawbot.app.v1",
|
|
206
|
+
app_id: "work-bgm-autopilot",
|
|
207
|
+
version: "0.1.0",
|
|
208
|
+
host_api: "weclawbot.host.v0",
|
|
209
|
+
entrypoints: ["main"],
|
|
210
|
+
device_classes: ["music_player", "audio_output"],
|
|
211
|
+
capabilities: [
|
|
212
|
+
"clock.read",
|
|
213
|
+
"log.emit",
|
|
214
|
+
"music.state.read",
|
|
215
|
+
"music.rule.write",
|
|
216
|
+
"music.playback.write",
|
|
217
|
+
],
|
|
218
|
+
limits: {
|
|
219
|
+
wasm_bytes: 4096,
|
|
220
|
+
linear_memory_bytes: 65_536,
|
|
221
|
+
callback_wall_ms: 500,
|
|
222
|
+
instruction_budget: 1_000_000,
|
|
223
|
+
kv_bytes: 8192,
|
|
224
|
+
},
|
|
225
|
+
artifact: { sha256: "0".repeat(64) },
|
|
226
|
+
install: {
|
|
227
|
+
persist_on_reboot: true,
|
|
228
|
+
source: "audioctl",
|
|
229
|
+
},
|
|
230
|
+
autopilot: {
|
|
231
|
+
schema: "weclawbot.music_autopilot.v1",
|
|
232
|
+
profile: "work-bgm",
|
|
233
|
+
objective: "建立工作 BGM 歌单,持续播放,并尊重用户手工暂停。",
|
|
234
|
+
continuous: true,
|
|
235
|
+
respect_user_pause: true,
|
|
236
|
+
autoplay: play,
|
|
237
|
+
play_request_id: play ? `play_${crypto.randomUUID()}` : "",
|
|
238
|
+
refresh: true,
|
|
239
|
+
rule: WORKBGM_RULE,
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
159
245
|
export function normalizeAudioCommand(command, options = {}) {
|
|
160
246
|
const aliases = new Map([
|
|
161
247
|
["resume", "play"],
|
|
@@ -236,6 +322,100 @@ export function validateRule(rule) {
|
|
|
236
322
|
};
|
|
237
323
|
}
|
|
238
324
|
|
|
325
|
+
export function normalizeAppManifest(manifest, artifact = {}) {
|
|
326
|
+
if (
|
|
327
|
+
!manifest ||
|
|
328
|
+
manifest.schema !== "weclawbot.app.v1" ||
|
|
329
|
+
!/^[a-z0-9][a-z0-9_.-]{0,63}$/u.test(manifest.app_id || "") ||
|
|
330
|
+
!/^[A-Za-z0-9][A-Za-z0-9_.+-]{0,63}$/u.test(manifest.version || "") ||
|
|
331
|
+
manifest.host_api !== "weclawbot.host.v0"
|
|
332
|
+
) {
|
|
333
|
+
throw new Error("app_manifest_invalid");
|
|
334
|
+
}
|
|
335
|
+
const allowedCapabilities = new Set([
|
|
336
|
+
"clock.read",
|
|
337
|
+
"timer.schedule",
|
|
338
|
+
"kv.app",
|
|
339
|
+
"log.emit",
|
|
340
|
+
"music.state.read",
|
|
341
|
+
"music.rule.write",
|
|
342
|
+
"music.playback.write",
|
|
343
|
+
]);
|
|
344
|
+
const capabilities = Array.isArray(manifest.capabilities)
|
|
345
|
+
? manifest.capabilities.map((capability) => string(capability)).filter(Boolean)
|
|
346
|
+
: [];
|
|
347
|
+
for (const capability of capabilities) {
|
|
348
|
+
if (!allowedCapabilities.has(capability)) {
|
|
349
|
+
throw new Error(`app_capability_unsupported: ${capability}`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
const wasmBytes = Number(artifact.wasmBytes) || Number(manifest.artifact?.wasm_bytes) || 0;
|
|
353
|
+
const sha256 = string(artifact.sha256 || manifest.artifact?.sha256);
|
|
354
|
+
if (!/^[0-9a-f]{64}$/u.test(sha256)) {
|
|
355
|
+
throw new Error("app_sha256_invalid");
|
|
356
|
+
}
|
|
357
|
+
const wasmLimit = clamp(Number(manifest.limits?.wasm_bytes) || 1_048_576, 1, 1_048_576);
|
|
358
|
+
if (wasmBytes < 1 || wasmBytes > wasmLimit) {
|
|
359
|
+
throw new Error("app_wasm_size_invalid");
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
schema: "weclawbot.app.v1",
|
|
363
|
+
app_id: string(manifest.app_id),
|
|
364
|
+
version: string(manifest.version),
|
|
365
|
+
host_api: "weclawbot.host.v0",
|
|
366
|
+
entrypoints: Array.isArray(manifest.entrypoints)
|
|
367
|
+
? manifest.entrypoints.map((entrypoint) => string(entrypoint)).filter(Boolean).slice(0, 8)
|
|
368
|
+
: [],
|
|
369
|
+
device_classes: Array.isArray(manifest.device_classes)
|
|
370
|
+
? manifest.device_classes.map((deviceClass) => string(deviceClass)).filter(Boolean).slice(0, 8)
|
|
371
|
+
: [],
|
|
372
|
+
capabilities,
|
|
373
|
+
limits: {
|
|
374
|
+
wasm_bytes: wasmLimit,
|
|
375
|
+
linear_memory_bytes: clamp(
|
|
376
|
+
Number(manifest.limits?.linear_memory_bytes) || 16_777_216,
|
|
377
|
+
65_536,
|
|
378
|
+
16_777_216,
|
|
379
|
+
),
|
|
380
|
+
callback_wall_ms: clamp(Number(manifest.limits?.callback_wall_ms) || 500, 1, 2000),
|
|
381
|
+
instruction_budget: clamp(
|
|
382
|
+
Number(manifest.limits?.instruction_budget) || 10_000_000,
|
|
383
|
+
1,
|
|
384
|
+
10_000_000,
|
|
385
|
+
),
|
|
386
|
+
kv_bytes: clamp(Number(manifest.limits?.kv_bytes) || 262_144, 0, 262_144),
|
|
387
|
+
},
|
|
388
|
+
artifact: {
|
|
389
|
+
sha256,
|
|
390
|
+
wasm_bytes: wasmBytes,
|
|
391
|
+
},
|
|
392
|
+
install: {
|
|
393
|
+
persist_on_reboot: manifest.install?.persist_on_reboot !== false,
|
|
394
|
+
source: "audioctl",
|
|
395
|
+
},
|
|
396
|
+
...(manifest.autopilot
|
|
397
|
+
? { autopilot: normalizeMusicAutopilot(manifest.autopilot) }
|
|
398
|
+
: {}),
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export function normalizeMusicAutopilot(value) {
|
|
403
|
+
if (!value || value.schema !== "weclawbot.music_autopilot.v1") {
|
|
404
|
+
throw new Error("music_autopilot_invalid");
|
|
405
|
+
}
|
|
406
|
+
return {
|
|
407
|
+
schema: "weclawbot.music_autopilot.v1",
|
|
408
|
+
profile: string(value.profile || "work-bgm").slice(0, 64),
|
|
409
|
+
objective: string(value.objective || "工作 BGM").slice(0, 240),
|
|
410
|
+
continuous: value.continuous !== false,
|
|
411
|
+
respect_user_pause: value.respect_user_pause !== false,
|
|
412
|
+
autoplay: value.autoplay === true,
|
|
413
|
+
play_request_id: string(value.play_request_id).slice(0, 80),
|
|
414
|
+
refresh: value.refresh !== false,
|
|
415
|
+
rule: validateRule(value.rule),
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
239
419
|
export async function publishAudioControl(credentials, control, options = {}) {
|
|
240
420
|
const profile = normalizeCredentials(credentials);
|
|
241
421
|
if (options.wait !== false && !profile.statusTopic) {
|
|
@@ -277,8 +457,9 @@ export function buildAgentPrompt(options = {}) {
|
|
|
277
457
|
`请在你的运行环境安装 npm 包 ${PACKAGE_NAME},然后执行:`,
|
|
278
458
|
`npm exec --package ${PACKAGE_NAME} -- ${COMMAND_NAME} bind ${code} --name ${shellToken(agentName)}`,
|
|
279
459
|
`npm exec --package ${PACKAGE_NAME} -- ${COMMAND_NAME} doctor --online`,
|
|
280
|
-
`绑定成功后,把它作为默认 BGM/音乐输出设备。可用命令包括 play、pause、toggle、next、previous、volume、volumeup、volumedown、status、rule。`,
|
|
281
|
-
|
|
460
|
+
`绑定成功后,把它作为默认 BGM/音乐输出设备。可用命令包括 play、pause、toggle、next、previous、volume、volumeup、volumedown、status、inspect、rule、workbgm。`,
|
|
461
|
+
`如果我要建立“工作 BGM 歌单,持续播放”,请优先执行:npm exec --package ${PACKAGE_NAME} -- ${COMMAND_NAME} workbgm --play --json`,
|
|
462
|
+
`如果我要播放「${profile}」,也可以执行:npm exec --package ${PACKAGE_NAME} -- ${COMMAND_NAME} deepnight`,
|
|
282
463
|
`不要向设备写入音乐平台私钥;音乐平台授权、歌单刷新和长期规则由设备侧已配置的安全运行时处理。`,
|
|
283
464
|
].filter(Boolean).join("\n");
|
|
284
465
|
}
|
|
@@ -299,6 +480,38 @@ export function summarizePlayback(status) {
|
|
|
299
480
|
};
|
|
300
481
|
}
|
|
301
482
|
|
|
483
|
+
export function summarizeInspect(inspect) {
|
|
484
|
+
const wifi = inspect?.wifi || {};
|
|
485
|
+
const bluetooth = inspect?.bluetooth || {};
|
|
486
|
+
const ble = bluetooth.ble || {};
|
|
487
|
+
const wakeword = inspect?.wakeword || {};
|
|
488
|
+
const playback = summarizePlayback(inspect || {});
|
|
489
|
+
const vm = inspect?.vm || {};
|
|
490
|
+
const health = inspect?.health || {};
|
|
491
|
+
const problems = Array.isArray(health.problems) ? health.problems : [];
|
|
492
|
+
return {
|
|
493
|
+
device:
|
|
494
|
+
string(inspect?.device?.name || "audio-device") +
|
|
495
|
+
(inspect?.device?.platform ? `/${inspect.device.platform}` : ""),
|
|
496
|
+
wifi:
|
|
497
|
+
`${wifi.connected ? "connected" : "disconnected"} ${string(wifi.ssid || "-")} ${string(wifi.ip || "-")}`
|
|
498
|
+
.trim(),
|
|
499
|
+
bluetooth:
|
|
500
|
+
`${bluetooth.service?.active || "unknown"} ${string(ble.name || "-")} ${string(ble.state || "-")}`
|
|
501
|
+
.trim(),
|
|
502
|
+
wakeword:
|
|
503
|
+
`${wakeword.available ? "available" : "unavailable"} ${Array.isArray(wakeword.wake_words) && wakeword.wake_words.length ? wakeword.wake_words.join("/") : "-"} homebase=${wakeword.services?.homebase?.active || "unknown"} openvoice=${wakeword.services?.openvoice_proc?.active || "unknown"}`
|
|
504
|
+
.trim(),
|
|
505
|
+
playback:
|
|
506
|
+
`${playback.desired}, volume ${playback.volume ?? "-"}, ${playback.track || "no track"}`,
|
|
507
|
+
vm:
|
|
508
|
+
vm
|
|
509
|
+
? `${vm.status || "unknown"} ${vm.app_id || "-"}@${vm.version || "-"}`
|
|
510
|
+
: "none",
|
|
511
|
+
health: problems.length > 0 ? problems.join(", ") : "ok",
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
|
|
302
515
|
function connectMqtt(profile, options = {}) {
|
|
303
516
|
const timeoutMs = Math.max(1000, Number(options.timeoutMs) || 12_000);
|
|
304
517
|
return mqtt.connect(profile.url, {
|