arona-agent 1.0.7 → 1.0.9
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 -4
- package/README_en.md +13 -4
- package/package.json +1 -1
- package/python/__pycache__/oss_upload.cpython-314.pyc +0 -0
- package/python/__pycache__/voice_clone.cpython-314.pyc +0 -0
- package/python/oss_upload.py +14 -0
- package/python/voice_clone.py +36 -1
- package/src/config.ts +10 -0
- package/src/index.ts +38 -4
- package/src/locale.ts +10 -0
- package/src/setup.ts +6 -1
- package/src/utils/python.ts +8 -1
package/README.md
CHANGED
|
@@ -17,12 +17,21 @@
|
|
|
17
17
|
```bash
|
|
18
18
|
npm i -g arona-agent
|
|
19
19
|
|
|
20
|
-
#
|
|
21
|
-
arona setup
|
|
22
|
-
|
|
23
|
-
# 初始化后可直接启动
|
|
20
|
+
# 首次运行自动进入初始化向导
|
|
24
21
|
arona
|
|
25
22
|
|
|
23
|
+
# 或着如果想先上手玩玩
|
|
24
|
+
npx arona-agent
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 扩展指令
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# 重新初始化(可选)
|
|
33
|
+
arona setup
|
|
34
|
+
|
|
26
35
|
# 更新
|
|
27
36
|
npm u -g arona-agent
|
|
28
37
|
|
package/README_en.md
CHANGED
|
@@ -15,12 +15,21 @@ A terminal-based conversational AI Agent built on the [Pi SDK](https://www.npmjs
|
|
|
15
15
|
```bash
|
|
16
16
|
npm i -g arona-agent
|
|
17
17
|
|
|
18
|
-
#
|
|
19
|
-
arona setup
|
|
20
|
-
|
|
21
|
-
# Launch after setup
|
|
18
|
+
# First run auto-starts the setup wizard, then launches ARONA
|
|
22
19
|
arona
|
|
23
20
|
|
|
21
|
+
# Quick Start
|
|
22
|
+
npx arona-agent
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Optional commands
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# Re-run setup manually (optional)
|
|
31
|
+
arona setup
|
|
32
|
+
|
|
24
33
|
# Upgrade
|
|
25
34
|
npm u -g arona-agent
|
|
26
35
|
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
package/python/oss_upload.py
CHANGED
|
@@ -17,8 +17,22 @@ stdout:
|
|
|
17
17
|
|
|
18
18
|
import json
|
|
19
19
|
import os
|
|
20
|
+
import socket
|
|
20
21
|
import sys
|
|
21
22
|
|
|
23
|
+
# ---- IPv4 优先解析(同 tts_say.py / voice_clone.py)----
|
|
24
|
+
# macOS 家宽坑:有全局 IPv6 但出口路由不通,python 无 Happy Eyeballs →
|
|
25
|
+
# getaddrinfo v6 在前,每次请求先卡满 v6 超时再回落 v4。重排 AF_INET 在前根治。
|
|
26
|
+
_orig_getaddrinfo = socket.getaddrinfo
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _ipv4_first_getaddrinfo(*args, **kwargs):
|
|
30
|
+
res = _orig_getaddrinfo(*args, **kwargs)
|
|
31
|
+
return sorted(res, key=lambda r: 0 if r[0] == socket.AF_INET else 1)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
socket.getaddrinfo = _ipv4_first_getaddrinfo
|
|
35
|
+
|
|
22
36
|
|
|
23
37
|
def fail(msg):
|
|
24
38
|
print(json.dumps({"error": msg}))
|
package/python/voice_clone.py
CHANGED
|
@@ -16,9 +16,32 @@ All logging goes to stderr so stdout stays clean JSON.
|
|
|
16
16
|
import json
|
|
17
17
|
import os
|
|
18
18
|
import re
|
|
19
|
+
import socket
|
|
19
20
|
import sys
|
|
20
21
|
import time
|
|
21
22
|
|
|
23
|
+
from _i18n import t
|
|
24
|
+
|
|
25
|
+
# ---- IPv4 优先解析(同 tts_say.py)----
|
|
26
|
+
# macOS 家宽坑:系统有全局 IPv6 地址但出口路由不通,python 无 Happy Eyeballs,
|
|
27
|
+
# getaddrinfo 返回 v6 在前 → 每次请求先卡满 v6 超时才回落 v4(上传/轮询多请求叠加=分钟级卡死,
|
|
28
|
+
# 表现为"正在克隆"卡住;开代理可用是因为代理直连 127.0.0.1 恰好绕开 v6)。
|
|
29
|
+
# 重排 AF_INET 在前根治;IPv4 不可用时仍按序回落 IPv6。
|
|
30
|
+
_orig_getaddrinfo = socket.getaddrinfo
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _ipv4_first_getaddrinfo(*args, **kwargs):
|
|
34
|
+
res = _orig_getaddrinfo(*args, **kwargs)
|
|
35
|
+
return sorted(res, key=lambda r: 0 if r[0] == socket.AF_INET else 1)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
socket.getaddrinfo = _ipv4_first_getaddrinfo
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def log(msg_zh, msg_en):
|
|
42
|
+
# 阶段诊断只走 stderr:Node 侧 --verbose 会逐行转发实时可见,stdout 保持纯 JSON。
|
|
43
|
+
print(f"[voice_clone] {t(msg_zh, msg_en)}", file=sys.stderr, flush=True)
|
|
44
|
+
|
|
22
45
|
|
|
23
46
|
def fail(msg):
|
|
24
47
|
print(json.dumps({"error": msg}))
|
|
@@ -52,29 +75,39 @@ def main():
|
|
|
52
75
|
|
|
53
76
|
# 1. Upload audio to DashScope hosted OSS
|
|
54
77
|
try:
|
|
78
|
+
log(f"上传音频 {os.path.basename(audio_file)} 到 DashScope 托管 OSS...",
|
|
79
|
+
f"Uploading {os.path.basename(audio_file)} to DashScope OSS...")
|
|
55
80
|
resp = Files.upload(file_path=audio_file, purpose="voice_clone")
|
|
56
81
|
file_id = resp.output["uploaded_files"][0]["file_id"]
|
|
82
|
+
log(f"上传完成 file_id={file_id}", f"Upload complete file_id={file_id}")
|
|
57
83
|
except Exception as e:
|
|
58
84
|
fail(f"Upload failed: {e}")
|
|
59
85
|
|
|
60
86
|
# 2. Get the Alibaba Cloud internal URL (must use internal address)
|
|
61
87
|
try:
|
|
88
|
+
log("获取 OSS 内部地址...", "Getting OSS internal URL...")
|
|
62
89
|
oss_url = Files.get(file_id).output["url"]
|
|
90
|
+
log("OSS 内部地址获取成功", "OSS internal URL acquired")
|
|
63
91
|
except Exception as e:
|
|
64
92
|
fail(f"Failed to get OSS URL: {e}")
|
|
65
93
|
|
|
66
94
|
# 3. Submit voice cloning
|
|
67
95
|
try:
|
|
96
|
+
log(f"提交音色克隆(model={target_model},prefix={prefix})...",
|
|
97
|
+
f"Submitting voice creation (model={target_model}, prefix={prefix})...")
|
|
68
98
|
svc = VoiceEnrollmentService()
|
|
69
99
|
voice_id = svc.create_voice(target_model=target_model, prefix=prefix, url=oss_url)
|
|
100
|
+
log(f"已提交,voice_id={voice_id}", f"Submitted, voice_id={voice_id}")
|
|
70
101
|
except Exception as e:
|
|
71
102
|
fail(f"create_voice failed: {e}")
|
|
72
103
|
|
|
73
104
|
# 4. Wait for voice to be ready (max 5 minutes)
|
|
74
105
|
try:
|
|
75
|
-
for
|
|
106
|
+
for i in range(30):
|
|
76
107
|
info = svc.query_voice(voice_id=voice_id)
|
|
77
108
|
status = info.get("status")
|
|
109
|
+
log(f"查询克隆状态:{status}(第 {i + 1}/30 次,每 10s)",
|
|
110
|
+
f"Voice status: {status} ({i + 1}/30, every 10s)")
|
|
78
111
|
if status == "OK":
|
|
79
112
|
break
|
|
80
113
|
if status == "UNDEPLOYED":
|
|
@@ -87,10 +120,12 @@ def main():
|
|
|
87
120
|
|
|
88
121
|
# 5. Delete the uploaded file (best-effort cleanup)
|
|
89
122
|
try:
|
|
123
|
+
log("删除已上传文件(尽力清理)...", "Deleting uploaded file (best-effort)...")
|
|
90
124
|
Files.delete(file_id)
|
|
91
125
|
except Exception:
|
|
92
126
|
pass # Cleanup is best-effort
|
|
93
127
|
|
|
128
|
+
log(f"音色克隆完成 voice_id={voice_id}", f"Voice clone complete voice_id={voice_id}")
|
|
94
129
|
print(json.dumps({"voice_id": voice_id}))
|
|
95
130
|
|
|
96
131
|
|
package/src/config.ts
CHANGED
|
@@ -277,6 +277,16 @@ function loadConfig(): AronaConfig {
|
|
|
277
277
|
|
|
278
278
|
export const config = loadConfig();
|
|
279
279
|
|
|
280
|
+
/**
|
|
281
|
+
* 重新加载 settings.json 并就地更新 config 单例(首次运行引导 setup 完成后调用)。
|
|
282
|
+
* ESM 模块缓存无法重建模块级 const,只能改对象字段;所有消费方均通过 config.xxx
|
|
283
|
+
* 运行时引用属性,就地覆盖即全局生效(无顶层解构,见 index.ts 引导注释)。
|
|
284
|
+
*/
|
|
285
|
+
export function reloadConfig(): AronaConfig {
|
|
286
|
+
Object.assign(config, loadConfig());
|
|
287
|
+
return config;
|
|
288
|
+
}
|
|
289
|
+
|
|
280
290
|
/**
|
|
281
291
|
* STT 全局热键键名(pynput 命名,供 repl.ts 注入 ARONA_HOTKEY_KEY):
|
|
282
292
|
* macOS 右 Cmd(pynput 有 Key.cmd_r),Windows/Linux 右 Ctrl(pynput Key.ctrl_r 三平台皆有效)。
|
package/src/index.ts
CHANGED
|
@@ -1,17 +1,51 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { join } from "node:path";
|
|
1
3
|
import { initAgent } from "./agent.ts";
|
|
2
4
|
import { Repl } from "./repl.ts";
|
|
3
5
|
import { resetConversationFlag, loadSession } from "./memory.ts";
|
|
4
6
|
import { startPet } from "./pet.ts";
|
|
5
|
-
import { config, settingsExist } from "./config.ts";
|
|
7
|
+
import { config, settingsExist, reloadConfig, PROJECT_ROOT } from "./config.ts";
|
|
6
8
|
import { preloadGptSovitsLocal } from "./tts_provider.ts";
|
|
7
9
|
import { syncSkillsFromAgentsDir } from "./skills.ts";
|
|
8
|
-
import { t } from "./locale.ts";
|
|
10
|
+
import { t, refreshLanguage } from "./locale.ts";
|
|
9
11
|
import chalk from "chalk";
|
|
10
12
|
|
|
13
|
+
/**
|
|
14
|
+
* 首次运行引导:以子进程拉起 setup 向导(stdio inherit 复用当前终端交互)。
|
|
15
|
+
* 传 ARONA_AUTO_SETUP=1 让向导末尾提示"正在启动"而非"运行 arona"。setup 在
|
|
16
|
+
* 独立进程写 settings.json,主进程随后 reloadConfig 即可(ESM 缓存无法重建单例)。
|
|
17
|
+
*/
|
|
18
|
+
function runSetupWizard(): Promise<number> {
|
|
19
|
+
const tsxBin = process.platform === "win32"
|
|
20
|
+
? join(PROJECT_ROOT, "node_modules", ".bin", "tsx.cmd")
|
|
21
|
+
: join(PROJECT_ROOT, "node_modules", ".bin", "tsx");
|
|
22
|
+
return new Promise((resolve) => {
|
|
23
|
+
const child = spawn(tsxBin, [join(PROJECT_ROOT, "src", "setup.ts"), ...process.argv.slice(2)], {
|
|
24
|
+
cwd: process.cwd(),
|
|
25
|
+
stdio: "inherit",
|
|
26
|
+
shell: process.platform === "win32",
|
|
27
|
+
env: { ...process.env, ARONA_AUTO_SETUP: "1" },
|
|
28
|
+
});
|
|
29
|
+
child.on("exit", (code) => resolve(code ?? 0));
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
11
33
|
async function main() {
|
|
34
|
+
// 首次运行引导:无 settings.json 时直接进入 setup 向导,配置完成后继续 REPL
|
|
12
35
|
if (!settingsExist()) {
|
|
13
|
-
console.log(chalk.yellow(t("
|
|
14
|
-
|
|
36
|
+
console.log(chalk.yellow(t("未找到配置文件,正在进入初始化向导…", "Config file not found. Starting the setup wizard…")));
|
|
37
|
+
const setupCode = await runSetupWizard();
|
|
38
|
+
if (setupCode !== 0) {
|
|
39
|
+
console.log(chalk.yellow(t("初始化已中止,配置未保存。", "Setup aborted, configuration not saved.")));
|
|
40
|
+
process.exit(setupCode);
|
|
41
|
+
}
|
|
42
|
+
if (!settingsExist()) {
|
|
43
|
+
console.log(chalk.yellow(t("配置未保存,无法启动。请重新运行 arona 完成初始化。", "Configuration was not saved. Re-run arona to initialize.")));
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
// setup 在子进程内写配置:就地刷新 config 单例与 UI 语言后继续
|
|
47
|
+
reloadConfig();
|
|
48
|
+
refreshLanguage();
|
|
15
49
|
}
|
|
16
50
|
|
|
17
51
|
if (!config.apiKey) {
|
package/src/locale.ts
CHANGED
|
@@ -129,3 +129,13 @@ export function getLang(): Language {
|
|
|
129
129
|
export function setLang(l: Language): void {
|
|
130
130
|
lang = l;
|
|
131
131
|
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 按 settings.json 的 language 字段重新定型语言(首次运行引导 setup 完成后调用;
|
|
135
|
+
* 不重新探测系统/环境,仅尊重向导写入的显式配置)。auto/缺失保持现状返回当前值。
|
|
136
|
+
*/
|
|
137
|
+
export function refreshLanguage(): Language {
|
|
138
|
+
const setting = readSettingsLanguage();
|
|
139
|
+
if (setting === "zh" || setting === "en") lang = setting;
|
|
140
|
+
return lang;
|
|
141
|
+
}
|
package/src/setup.ts
CHANGED
|
@@ -694,7 +694,12 @@ async function main() {
|
|
|
694
694
|
|
|
695
695
|
console.log(chalk.green(t(`\n✓ 配置已保存到 ${SETTINGS_FILE}`, `\n✓ Configuration saved to ${SETTINGS_FILE}`)));
|
|
696
696
|
}
|
|
697
|
-
|
|
697
|
+
if (process.env.ARONA_AUTO_SETUP === "1") {
|
|
698
|
+
// 首次运行自动引导(index.ts spawn 传入):配置完成后主进程直接续跑 REPL
|
|
699
|
+
console.log(chalk.cyan(t(" 配置完成,正在启动 ARONA...\n", " Configuration saved. Starting ARONA...\n")));
|
|
700
|
+
} else {
|
|
701
|
+
console.log(chalk.cyan(t(" 运行 arona 启动 Agent。\n", " Run arona to start the Agent.\n")));
|
|
702
|
+
}
|
|
698
703
|
} finally {
|
|
699
704
|
rl.close();
|
|
700
705
|
}
|
package/src/utils/python.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ChildProcessWithoutNullStreams } from "child_process";
|
|
2
2
|
import { join } from "path";
|
|
3
|
-
import { PYTHON_DIR, config } from "../config.ts";
|
|
3
|
+
import { PYTHON_DIR, config, verbose } from "../config.ts";
|
|
4
4
|
import { t, getLang } from "../locale.ts";
|
|
5
5
|
import { spawnCompat, stripProxyEnv } from "./spawn.ts";
|
|
6
6
|
|
|
@@ -42,6 +42,13 @@ export async function runPython(
|
|
|
42
42
|
});
|
|
43
43
|
proc.stderr.on("data", (data) => {
|
|
44
44
|
stderr += data.toString();
|
|
45
|
+
// --verbose:逐行实时转发 python stderr,可见 voice_clone 上传/轮询等阶段进度
|
|
46
|
+
if (verbose) {
|
|
47
|
+
for (const line of data.toString().split(/[\r\n]+/)) {
|
|
48
|
+
const msg = line.trim();
|
|
49
|
+
if (msg) console.error(`[python:${scriptName}]`, msg);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
45
52
|
});
|
|
46
53
|
proc.on("close", (code) => {
|
|
47
54
|
clearTimeout(timer);
|