@openbrt/audioctl 0.1.6 → 0.1.7
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 +2 -0
- package/agent-plugin.json +1 -0
- package/lib/index.mjs +60 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -52,6 +52,8 @@ npm exec --package @openbrt/audioctl -- audioctl bind 123456 --name codex
|
|
|
52
52
|
```
|
|
53
53
|
|
|
54
54
|
Credentials are saved to `~/.config/weclawbot/audio-mqtt.json` by default.
|
|
55
|
+
`audioctl` serializes concurrent commands that use the same local MQTT
|
|
56
|
+
credentials, because the broker may require an exact MQTT client id.
|
|
55
57
|
|
|
56
58
|
## Commands
|
|
57
59
|
|
package/agent-plugin.json
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
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
|
"user_contract": "用户只需要给出设备身份和目标;agent 负责选择合法平台 API、通过 agent 自己的连接器/OAuth/secret store 获取平台身份、构建最小权限脚本/队列、推送到设备 VM、诊断和报告结果。",
|
|
10
|
+
"runtime_note": "audioctl 会在本机自动串行化同一 MQTT 绑定凭据的并发控制命令,避免多个 agent 工具进程使用同一个 broker client_id 时互相踢下线。",
|
|
10
11
|
"separation_of_concerns": {
|
|
11
12
|
"provisioning": "蓝牙/Wi-Fi 配网只负责设备入网、发现和 weclawbot.link 绑定,不承载音乐平台授权,也不决定设备用途。",
|
|
12
13
|
"music_platform_auth": "音乐平台授权属于 agent 侧能力,由 agent 的连接器、OAuth、secret store 或用户明确提供的运行环境凭据完成。",
|
package/lib/index.mjs
CHANGED
|
@@ -719,24 +719,26 @@ export async function publishAudioControl(credentials, control, options = {}) {
|
|
|
719
719
|
throw new Error("status_topic_missing");
|
|
720
720
|
}
|
|
721
721
|
const timeoutMs = Math.max(1000, Number(options.timeoutMs) || 12_000);
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
722
|
+
return withMqttClientLock(profile.clientId, timeoutMs, async () => {
|
|
723
|
+
const client = connectMqtt(profile, { timeoutMs });
|
|
724
|
+
try {
|
|
725
|
+
await onceConnected(client, timeoutMs);
|
|
726
|
+
if (options.wait === false) {
|
|
727
|
+
await publishJson(client, profile.controlTopic, control);
|
|
728
|
+
return { ok: true, published: true, applied: null };
|
|
729
|
+
}
|
|
730
|
+
await subscribe(client, profile.statusTopic);
|
|
731
|
+
const statusPromise = waitForStatus(client, {
|
|
732
|
+
correlationId: control.id,
|
|
733
|
+
timeoutMs,
|
|
734
|
+
});
|
|
726
735
|
await publishJson(client, profile.controlTopic, control);
|
|
727
|
-
|
|
736
|
+
const status = await statusPromise;
|
|
737
|
+
return { ok: status.kind === "applied", published: true, applied: true, status };
|
|
738
|
+
} finally {
|
|
739
|
+
client.end(true);
|
|
728
740
|
}
|
|
729
|
-
|
|
730
|
-
const statusPromise = waitForStatus(client, {
|
|
731
|
-
correlationId: control.id,
|
|
732
|
-
timeoutMs,
|
|
733
|
-
});
|
|
734
|
-
await publishJson(client, profile.controlTopic, control);
|
|
735
|
-
const status = await statusPromise;
|
|
736
|
-
return { ok: status.kind === "applied", published: true, applied: true, status };
|
|
737
|
-
} finally {
|
|
738
|
-
client.end(true);
|
|
739
|
-
}
|
|
741
|
+
});
|
|
740
742
|
}
|
|
741
743
|
|
|
742
744
|
export function remediationForStatus(status) {
|
|
@@ -993,6 +995,48 @@ function waitForStatus(client, options) {
|
|
|
993
995
|
});
|
|
994
996
|
}
|
|
995
997
|
|
|
998
|
+
async function withMqttClientLock(clientId, timeoutMs, task) {
|
|
999
|
+
const lockRoot = path.join(os.tmpdir(), "openbrt-audioctl-locks");
|
|
1000
|
+
await fs.mkdir(lockRoot, { recursive: true, mode: 0o700 });
|
|
1001
|
+
const lockName = crypto
|
|
1002
|
+
.createHash("sha256")
|
|
1003
|
+
.update(String(clientId || "default"))
|
|
1004
|
+
.digest("hex")
|
|
1005
|
+
.slice(0, 24);
|
|
1006
|
+
const lockDirectory = path.join(lockRoot, `${lockName}.lock`);
|
|
1007
|
+
const started = Date.now();
|
|
1008
|
+
while (true) {
|
|
1009
|
+
try {
|
|
1010
|
+
await fs.mkdir(lockDirectory, { mode: 0o700 });
|
|
1011
|
+
break;
|
|
1012
|
+
} catch (error) {
|
|
1013
|
+
if (error?.code !== "EEXIST") throw error;
|
|
1014
|
+
try {
|
|
1015
|
+
const stats = await fs.stat(lockDirectory);
|
|
1016
|
+
if (Date.now() - stats.mtimeMs > Math.max(60_000, timeoutMs * 3)) {
|
|
1017
|
+
await fs.rm(lockDirectory, { recursive: true, force: true });
|
|
1018
|
+
continue;
|
|
1019
|
+
}
|
|
1020
|
+
} catch {
|
|
1021
|
+
// Race with another process removing the lock. Try again.
|
|
1022
|
+
}
|
|
1023
|
+
if (Date.now() - started > timeoutMs) {
|
|
1024
|
+
throw new Error("mqtt_client_busy");
|
|
1025
|
+
}
|
|
1026
|
+
await sleep(120);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
try {
|
|
1030
|
+
return await task();
|
|
1031
|
+
} finally {
|
|
1032
|
+
await fs.rm(lockDirectory, { recursive: true, force: true });
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
function sleep(milliseconds) {
|
|
1037
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
1038
|
+
}
|
|
1039
|
+
|
|
996
1040
|
function shellToken(value) {
|
|
997
1041
|
const text = string(value) || "agent";
|
|
998
1042
|
if (/^[A-Za-z0-9_./:@+-]+$/u.test(text)) return text;
|