@xdfnet/ispeak 1.6.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/Docs/ARCHITECTURE.md +177 -0
- package/Docs/HTTP Chunked:SSE/345/215/225/345/220/221/346/265/201/345/274/217-V3.md" +896 -0
- package/Docs//345/243/260/351/237/263/345/244/215/345/210/273API-V3.md +873 -0
- package/Docs//351/237/263/350/211/262/345/210/227/350/241/250.md +998 -0
- package/LICENSE +21 -0
- package/README.md +194 -0
- package/configs/com.iSpeak.plist +20 -0
- package/configs/config.example.json +18 -0
- package/configs/hook-speak.sh +76 -0
- package/go.mod +5 -0
- package/go.sum +4 -0
- package/main.go +858 -0
- package/npm/postinstall.js +134 -0
- package/package.json +46 -0
- package/scripts/ispeak +58 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const os = require("os");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const { spawnSync } = require("child_process");
|
|
8
|
+
|
|
9
|
+
const root = path.resolve(__dirname, "..");
|
|
10
|
+
const home = os.homedir();
|
|
11
|
+
const binDir = path.join(home, ".local", "bin");
|
|
12
|
+
const configDir = path.join(home, ".config", "iSpeak");
|
|
13
|
+
const plistPath = path.join(home, "Library", "LaunchAgents", "com.iSpeak.plist");
|
|
14
|
+
const socketPath = path.join(configDir, "ispeak.sock");
|
|
15
|
+
const binaryPath = path.join(binDir, "ispeakd");
|
|
16
|
+
const cliPath = path.join(binDir, "ispeak");
|
|
17
|
+
const buildPath = path.join(root, "build", "ispeakd");
|
|
18
|
+
|
|
19
|
+
function run(cmd, args, options = {}) {
|
|
20
|
+
const result = spawnSync(cmd, args, {
|
|
21
|
+
cwd: root,
|
|
22
|
+
stdio: options.stdio || "inherit",
|
|
23
|
+
encoding: "utf8",
|
|
24
|
+
});
|
|
25
|
+
if (result.error) {
|
|
26
|
+
throw result.error;
|
|
27
|
+
}
|
|
28
|
+
if (result.status !== 0 && !options.allowFailure) {
|
|
29
|
+
throw new Error(`${cmd} ${args.join(" ")} failed with exit code ${result.status}`);
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function ensureDir(dir) {
|
|
35
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function copyExecutable(src, dst) {
|
|
39
|
+
fs.copyFileSync(src, dst);
|
|
40
|
+
fs.chmodSync(dst, 0o755);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function symlinkForce(target, linkPath) {
|
|
44
|
+
try {
|
|
45
|
+
fs.rmSync(linkPath, { force: true });
|
|
46
|
+
} catch (_) {
|
|
47
|
+
// Ignore stale link cleanup failures; copyExecutable below will surface real errors.
|
|
48
|
+
}
|
|
49
|
+
fs.symlinkSync(target, linkPath);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function copyIfMissing(src, dst, mode) {
|
|
53
|
+
if (fs.existsSync(dst)) {
|
|
54
|
+
console.log(`配置文件已存在: ${dst}`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
fs.copyFileSync(src, dst);
|
|
58
|
+
if (mode) {
|
|
59
|
+
fs.chmodSync(dst, mode);
|
|
60
|
+
}
|
|
61
|
+
console.log(`配置文件已创建: ${dst}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function sleep(ms) {
|
|
65
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function waitForSocket(timeoutMs) {
|
|
69
|
+
const deadline = Date.now() + timeoutMs;
|
|
70
|
+
while (Date.now() < deadline) {
|
|
71
|
+
try {
|
|
72
|
+
if (fs.statSync(socketPath).isSocket()) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
} catch (_) {
|
|
76
|
+
// Socket not ready yet.
|
|
77
|
+
}
|
|
78
|
+
sleep(100);
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function main() {
|
|
84
|
+
if (process.platform !== "darwin") {
|
|
85
|
+
throw new Error("@xdfnet/ispeak currently supports macOS only.");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const goCheck = spawnSync("go", ["version"], { stdio: "ignore" });
|
|
89
|
+
if (goCheck.status !== 0) {
|
|
90
|
+
throw new Error("Go is required to install @xdfnet/ispeak from npm. Install Go first, then rerun npm i -g @xdfnet/ispeak.");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
ensureDir(path.dirname(buildPath));
|
|
94
|
+
ensureDir(binDir);
|
|
95
|
+
ensureDir(configDir);
|
|
96
|
+
ensureDir(path.dirname(plistPath));
|
|
97
|
+
|
|
98
|
+
console.log("编译 ispeakd...");
|
|
99
|
+
run("go", ["build", "-ldflags=-s -w", "-o", buildPath, "."]);
|
|
100
|
+
|
|
101
|
+
console.log("停止旧服务...");
|
|
102
|
+
run("launchctl", ["unload", plistPath], { allowFailure: true, stdio: "ignore" });
|
|
103
|
+
|
|
104
|
+
copyExecutable(buildPath, binaryPath);
|
|
105
|
+
copyExecutable(path.join(root, "scripts", "ispeak"), cliPath);
|
|
106
|
+
symlinkForce(cliPath, path.join(binDir, "ispeak-claude"));
|
|
107
|
+
symlinkForce(cliPath, path.join(binDir, "ispeak-codex"));
|
|
108
|
+
|
|
109
|
+
copyIfMissing(path.join(root, "configs", "config.example.json"), path.join(configDir, "config.json"));
|
|
110
|
+
copyIfMissing(path.join(root, "configs", "hook-speak.sh"), path.join(configDir, "hook-speak.sh"), 0o755);
|
|
111
|
+
|
|
112
|
+
const plist = fs
|
|
113
|
+
.readFileSync(path.join(root, "configs", "com.iSpeak.plist"), "utf8")
|
|
114
|
+
.replaceAll("BINARY_PATH_PLACEHOLDER", binaryPath);
|
|
115
|
+
fs.writeFileSync(plistPath, plist);
|
|
116
|
+
|
|
117
|
+
console.log("启动服务...");
|
|
118
|
+
run("launchctl", ["load", plistPath]);
|
|
119
|
+
|
|
120
|
+
if (!waitForSocket(5000)) {
|
|
121
|
+
throw new Error(`service started but socket is not ready: ${socketPath}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const status = run(cliPath, ["status"], { stdio: "pipe" });
|
|
125
|
+
process.stdout.write(status.stdout);
|
|
126
|
+
console.log("\n安装成功!");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
main();
|
|
131
|
+
} catch (err) {
|
|
132
|
+
console.error(`ispeak npm install failed: ${err.message}`);
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xdfnet/ispeak",
|
|
3
|
+
"version": "1.6.1",
|
|
4
|
+
"description": "Local macOS TTS daemon for AI coding assistants, powered by Volcengine streaming TTS.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://github.com/xdfnet/iSpeak#readme",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/xdfnet/iSpeak.git"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/xdfnet/iSpeak/issues"
|
|
13
|
+
},
|
|
14
|
+
"os": [
|
|
15
|
+
"darwin"
|
|
16
|
+
],
|
|
17
|
+
"cpu": [
|
|
18
|
+
"arm64",
|
|
19
|
+
"x64"
|
|
20
|
+
],
|
|
21
|
+
"bin": {
|
|
22
|
+
"ispeak": "scripts/ispeak",
|
|
23
|
+
"ispeak-claude": "scripts/ispeak",
|
|
24
|
+
"ispeak-codex": "scripts/ispeak"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "go build -ldflags=\"-s -w\" -o build/ispeakd .",
|
|
28
|
+
"test": "go test ./...",
|
|
29
|
+
"postinstall": "node npm/postinstall.js",
|
|
30
|
+
"pack:dry-run": "npm pack --dry-run"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"main.go",
|
|
34
|
+
"go.mod",
|
|
35
|
+
"go.sum",
|
|
36
|
+
"scripts/",
|
|
37
|
+
"configs/",
|
|
38
|
+
"npm/",
|
|
39
|
+
"Docs/",
|
|
40
|
+
"README.md",
|
|
41
|
+
"LICENSE"
|
|
42
|
+
],
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=18"
|
|
45
|
+
}
|
|
46
|
+
}
|
package/scripts/ispeak
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ispeak — iSpeak 控制命令
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
VERSION="1.6.1"
|
|
6
|
+
SOCK="$HOME/.config/iSpeak/ispeak.sock"
|
|
7
|
+
PLIST="$HOME/Library/LaunchAgents/com.iSpeak.plist"
|
|
8
|
+
CMD_NAME="$(basename "$0")"
|
|
9
|
+
SOURCE_PREFIX=""
|
|
10
|
+
|
|
11
|
+
case "$CMD_NAME" in
|
|
12
|
+
ispeak-claude) SOURCE_PREFIX="{source:claude}" ;;
|
|
13
|
+
ispeak-codex) SOURCE_PREFIX="{source:codex}" ;;
|
|
14
|
+
esac
|
|
15
|
+
|
|
16
|
+
cmd_status() {
|
|
17
|
+
echo "== iSpeak =="
|
|
18
|
+
if pgrep ispeakd > /dev/null 2>&1; then
|
|
19
|
+
echo " 进程: $(pgrep ispeakd) ✓"
|
|
20
|
+
else
|
|
21
|
+
echo " 进程: 未运行 ✗"
|
|
22
|
+
fi
|
|
23
|
+
if [[ -S "$SOCK" ]]; then
|
|
24
|
+
echo " Socket: ✓"
|
|
25
|
+
else
|
|
26
|
+
echo " Socket: ✗"
|
|
27
|
+
fi
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
cmd_restart() {
|
|
31
|
+
launchctl unload "$PLIST" 2>/dev/null || true
|
|
32
|
+
launchctl load "$PLIST"
|
|
33
|
+
sleep 0.5
|
|
34
|
+
if [[ -S "$SOCK" ]]; then
|
|
35
|
+
echo "ispeak: 已重启"
|
|
36
|
+
else
|
|
37
|
+
echo "ispeak: 重启失败" >&2
|
|
38
|
+
exit 1
|
|
39
|
+
fi
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
case "${1:-}" in
|
|
43
|
+
status) cmd_status ;;
|
|
44
|
+
restart) cmd_restart ;;
|
|
45
|
+
version|-v|--version) echo "iSpeak $VERSION" ;;
|
|
46
|
+
"")
|
|
47
|
+
echo "iSpeak $VERSION"
|
|
48
|
+
echo ""
|
|
49
|
+
echo "用法:"
|
|
50
|
+
echo " ispeak \"文本\" 播报"
|
|
51
|
+
echo " ispeak status 服务状态"
|
|
52
|
+
echo " ispeak restart 重启服务"
|
|
53
|
+
echo " ispeak version 版本"
|
|
54
|
+
;;
|
|
55
|
+
*)
|
|
56
|
+
printf "%s%s" "$SOURCE_PREFIX" "$*" | nc -U -w5 "$SOCK" 2>/dev/null || echo "ispeak: socket 不可用" >&2
|
|
57
|
+
;;
|
|
58
|
+
esac
|