arona-agent 1.0.0
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/LICENSE +21 -0
- package/README.md +149 -0
- package/assets/blue-archive/arona/spine/arona_spr.atlas.txt +1168 -0
- package/assets/blue-archive/arona/spine/arona_spr.png +0 -0
- package/assets/blue-archive/arona/spine/arona_spr.skel +0 -0
- package/assets/blue-archive/arona/voice.mp3 +0 -0
- package/assets/blue-archive/hoshino/hoshino_spr.atlas.txt +174 -0
- package/assets/blue-archive/hoshino/hoshino_spr.json +15385 -0
- package/assets/blue-archive/hoshino/hoshino_spr.png +0 -0
- package/assets/blue-archive/hoshino/hoshino_spr.skel +0 -0
- package/assets/blue-archive/hoshino/robber/hoshino_robber_spr.atlas.txt +111 -0
- package/assets/blue-archive/hoshino/robber/hoshino_robber_spr.png +0 -0
- package/assets/blue-archive/hoshino/robber/hoshino_robber_spr.skel +0 -0
- package/assets/blue-archive/hoshino/swimsuit/hoshino_swimsuit_spr.atlas.txt +251 -0
- package/assets/blue-archive/hoshino/swimsuit/hoshino_swimsuit_spr.png +0 -0
- package/assets/blue-archive/hoshino/swimsuit/hoshino_swimsuit_spr.skel +0 -0
- package/assets/blue-archive/hoshino/voice.mp3 +0 -0
- package/assets/blue-archive/plana/spine/plana_spr.atlas.txt +1266 -0
- package/assets/blue-archive/plana/spine/plana_spr.png +0 -0
- package/assets/blue-archive/plana/spine/plana_spr.skel +0 -0
- package/assets/blue-archive/plana/voice.mp3 +0 -0
- package/assets/blue-archive/shiroko/ridingsuit/shiroko_ridingsuit_spr.atlas.txt +97 -0
- package/assets/blue-archive/shiroko/ridingsuit/shiroko_ridingsuit_spr.png +0 -0
- package/assets/blue-archive/shiroko/ridingsuit/shiroko_ridingsuit_spr.skel +0 -0
- package/assets/blue-archive/shiroko/robber/shiroko_robber_spr.atlas.txt +174 -0
- package/assets/blue-archive/shiroko/robber/shiroko_robber_spr.png +0 -0
- package/assets/blue-archive/shiroko/robber/shiroko_robber_spr.skel +0 -0
- package/assets/blue-archive/shiroko/shiroko_spr.atlas.txt +167 -0
- package/assets/blue-archive/shiroko/shiroko_spr.json +10702 -0
- package/assets/blue-archive/shiroko/shiroko_spr.png +0 -0
- package/assets/blue-archive/shiroko/shiroko_spr.skel +0 -0
- package/assets/blue-archive/shiroko/voice.mp3 +0 -0
- package/bin/arona.mjs +43 -0
- package/package.json +37 -0
- package/pet/agents.cjs +197 -0
- package/pet/main.cjs +462 -0
- package/pet/preload.cjs +21 -0
- package/pet/renderer/fx.html +30 -0
- package/pet/renderer/fx.js +41 -0
- package/pet/renderer/gallery.html +18 -0
- package/pet/renderer/gallery.js +31 -0
- package/pet/renderer/index.html +18 -0
- package/pet/renderer/renderer.js +382 -0
- package/pet/renderer/spine_layer.js +609 -0
- package/pet/renderer/spinetest.html +24 -0
- package/pet/renderer/spinetest.js +295 -0
- package/pet/renderer/style.css +63 -0
- package/pet/tools/gallery_capture.cjs +267 -0
- package/pet/tools/gen_sway.cjs +170 -0
- package/pet/tools/meshify_region.cjs +105 -0
- package/pet/tools/skel_inspect.cjs +112 -0
- package/pet/tools/skel_to_json.cjs +410 -0
- package/pet/tools/spine_node.cjs +69 -0
- package/pet/tools/visual_test.cjs +262 -0
- package/pet/vendor/spine/spine-canvas.js +8472 -0
- package/pet/vendor/spine/spine-webgl.js +10839 -0
- package/python/__pycache__/_i18n.cpython-313.pyc +0 -0
- package/python/__pycache__/_i18n.cpython-314.pyc +0 -0
- package/python/__pycache__/computer_use.cpython-314.pyc +0 -0
- package/python/__pycache__/hotkey.cpython-313.pyc +0 -0
- package/python/__pycache__/hotkey.cpython-314.pyc +0 -0
- package/python/__pycache__/stt.cpython-313.pyc +0 -0
- package/python/__pycache__/stt.cpython-314.pyc +0 -0
- package/python/__pycache__/tts.cpython-313.pyc +0 -0
- package/python/__pycache__/tts.cpython-314.pyc +0 -0
- package/python/__pycache__/tts_stream.cpython-313.pyc +0 -0
- package/python/__pycache__/tts_stream.cpython-314.pyc +0 -0
- package/python/__pycache__/voice_clone.cpython-314.pyc +0 -0
- package/python/_i18n.py +18 -0
- package/python/computer_use.py +190 -0
- package/python/hotkey.py +196 -0
- package/python/stt.py +240 -0
- package/python/tts_say.py +177 -0
- package/python/voice_clone.py +98 -0
- package/requirements.txt +6 -0
- package/src/agent.ts +791 -0
- package/src/agent_registry.ts +109 -0
- package/src/commands.ts +641 -0
- package/src/config.ts +245 -0
- package/src/in_memory_credentials.ts +61 -0
- package/src/index.ts +78 -0
- package/src/locale.ts +131 -0
- package/src/logo.ts +120 -0
- package/src/mcp.ts +192 -0
- package/src/memory.ts +303 -0
- package/src/pet.ts +346 -0
- package/src/renderer.ts +350 -0
- package/src/repl.ts +902 -0
- package/src/setup.ts +371 -0
- package/src/skills.ts +96 -0
- package/src/slash_menu.ts +371 -0
- package/src/slash_registry.ts +53 -0
- package/src/speaker_context.ts +41 -0
- package/src/text_split.ts +79 -0
- package/src/tools/computer_use.ts +142 -0
- package/src/tools/emotion_tool.ts +55 -0
- package/src/tools/keep_silent_tool.ts +26 -0
- package/src/tools/memory_tool.ts +24 -0
- package/src/tools/skill_tools.ts +100 -0
- package/src/tools/tavily_tools.ts +431 -0
- package/src/tools/voice_tools.ts +28 -0
- package/src/tts_stream.ts +260 -0
- package/src/tui_select.ts +146 -0
- package/src/undo.ts +451 -0
- package/src/utils/python.ts +211 -0
- package/src/utils/spawn.ts +56 -0
- package/src/voice.ts +83 -0
- package/src/voice_cli.ts +198 -0
- package/src/voices.ts +135 -0
package/src/setup.ts
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import * as readline from "readline";
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
import {
|
|
7
|
+
ARONA_DIR,
|
|
8
|
+
SETTINGS_FILE,
|
|
9
|
+
PROJECT_ROOT,
|
|
10
|
+
resolveModelPrefix,
|
|
11
|
+
} from "./config.ts";
|
|
12
|
+
import { AGENT_IDS, getAgentLabel } from "./agent_registry.ts";
|
|
13
|
+
import { VOICE_AUDIO, cloneVoice, setVoiceId, getMissingAgents, hasVoice } from "./voices.ts";
|
|
14
|
+
import { multiSelect } from "./tui_select.ts";
|
|
15
|
+
import { t, getLang, setLang } from "./locale.ts";
|
|
16
|
+
|
|
17
|
+
interface Settings {
|
|
18
|
+
apiKey?: string;
|
|
19
|
+
apiBaseUrl?: string;
|
|
20
|
+
model?: string;
|
|
21
|
+
thinkingLevel?: string;
|
|
22
|
+
language?: "auto" | "zh" | "en";
|
|
23
|
+
/** 主 Agent(arona | plana):setup 保存时保留,不能覆盖 /change-agent 的选择 */
|
|
24
|
+
mainAgent?: string;
|
|
25
|
+
/** 子 Agent 列表(shiroko | hoshino):setup 保存时保留,不能覆盖 /change-agent 的选择 */
|
|
26
|
+
subAgents?: string[];
|
|
27
|
+
ttsEnabled?: boolean;
|
|
28
|
+
sttEnabled?: boolean;
|
|
29
|
+
/** @deprecated 已合并进 ttsEnabled,仅读兼容(旧配置 ttsAuto:false 仍生效) */
|
|
30
|
+
ttsAuto?: boolean;
|
|
31
|
+
workspaceId?: string;
|
|
32
|
+
ttsApiKey?: string;
|
|
33
|
+
ttsModel?: string;
|
|
34
|
+
ttsSampleRate?: number;
|
|
35
|
+
sttApiKey?: string;
|
|
36
|
+
sttModel?: string;
|
|
37
|
+
sttFormat?: string;
|
|
38
|
+
sttSampleRate?: number;
|
|
39
|
+
cuaApiKey?: string;
|
|
40
|
+
pythonPath?: string;
|
|
41
|
+
mcpServers?: Record<string, unknown>;
|
|
42
|
+
/** 用户手动维护;setup 向导只读不写。true 时启用演示模式。 */
|
|
43
|
+
demoMode?: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function loadExistingSettings(): Settings {
|
|
47
|
+
if (!existsSync(SETTINGS_FILE)) return {};
|
|
48
|
+
try {
|
|
49
|
+
return JSON.parse(readFileSync(SETTINGS_FILE, "utf-8"));
|
|
50
|
+
} catch {
|
|
51
|
+
return {};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Check that Python is 3.12 or 3.13 (3.14+ not supported by cua).
|
|
57
|
+
* Returns { ok, version } — version is the full version string.
|
|
58
|
+
*/
|
|
59
|
+
function checkPythonVersion(pythonPath: string): { ok: boolean; version: string } {
|
|
60
|
+
try {
|
|
61
|
+
const output = execSync(`${pythonPath} --version`, { stdio: "pipe" }).toString().trim();
|
|
62
|
+
const match = output.match(/Python\s+(\d+)\.(\d+)\.(\d+)/);
|
|
63
|
+
if (!match) return { ok: false, version: output || "unknown" };
|
|
64
|
+
const major = parseInt(match[1]);
|
|
65
|
+
const minor = parseInt(match[2]);
|
|
66
|
+
const version = `${major}.${minor}.${match[3]}`;
|
|
67
|
+
if (major === 3 && (minor === 12 || minor === 13)) {
|
|
68
|
+
return { ok: true, version };
|
|
69
|
+
}
|
|
70
|
+
return { ok: false, version };
|
|
71
|
+
} catch {
|
|
72
|
+
return { ok: false, version: "not found" };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function main() {
|
|
77
|
+
const verbose = process.argv.includes("--verbose");
|
|
78
|
+
|
|
79
|
+
// Load existing settings first so we can detect demoMode and surface its
|
|
80
|
+
// --verbose confirmation BEFORE any other output (banner, mkdir, etc.).
|
|
81
|
+
const existing = loadExistingSettings();
|
|
82
|
+
|
|
83
|
+
// demoMode: user-managed field, never written by the setup wizard itself.
|
|
84
|
+
// Strict === true so any other value (missing / false / "true" / 1) = normal mode.
|
|
85
|
+
// The activation notice + branch details only print under --verbose, so a
|
|
86
|
+
// plain `arona setup` run stays clean even when demoMode is on.
|
|
87
|
+
const demoMode = existing.demoMode === true;
|
|
88
|
+
if (verbose && demoMode) {
|
|
89
|
+
console.log(chalk.yellow(t(
|
|
90
|
+
"[demoMode] 已启用:Step 4 仍显示 TUI 选择界面,但跳过真实音色克隆与 settings.json/voices.json 写入。",
|
|
91
|
+
"[demoMode] enabled: Step 4 still shows the TUI, but skips real voice cloning and settings.json/voices.json writes.",
|
|
92
|
+
)));
|
|
93
|
+
console.log(chalk.gray(t(` [verbose] settings.json demoMode = ${JSON.stringify(existing.demoMode)} (类型 ${typeof existing.demoMode})`, ` [verbose] settings.json demoMode = ${JSON.stringify(existing.demoMode)} (type ${typeof existing.demoMode})`)));
|
|
94
|
+
console.log(chalk.gray(t(` [verbose] Step 3 将执行: pip3.13 install -r "${join(PROJECT_ROOT, "requirements.txt")}" -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple`, ` [verbose] Step 3 will run: pip3.13 install -r "${join(PROJECT_ROOT, "requirements.txt")}" -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple`)));
|
|
95
|
+
console.log(chalk.gray(t(" [verbose] Step 4 每个选中角色将模拟 5s 等待后显示成功,不调用 voice_clone.py、不写 voices.json", " [verbose] Step 4 simulates a 5s wait per selected character then reports success, without calling voice_clone.py or writing voices.json")));
|
|
96
|
+
console.log(chalk.gray(t(` [verbose] 末尾将跳过写入: ${SETTINGS_FILE}`, ` [verbose] will skip writing at the end: ${SETTINGS_FILE}`)));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
console.log(chalk.bold.cyan("\n=== ARONA Setup ===\n"));
|
|
100
|
+
|
|
101
|
+
// Ensure ~/.arona/ exists
|
|
102
|
+
if (!existsSync(ARONA_DIR)) mkdirSync(ARONA_DIR, { recursive: true });
|
|
103
|
+
|
|
104
|
+
// ---- Python version check (must be 3.12 or 3.13) ----
|
|
105
|
+
// 优先用 settings 里的 pythonPath;检查失败时静默回退到 "python"(Windows 端通常没有 python3 命令),
|
|
106
|
+
// 回退成功后 settings.json 里的 pythonPath 会写为 "python"。两个都失败才报错。
|
|
107
|
+
let pythonPath = existing.pythonPath || "python3";
|
|
108
|
+
let pyCheck = checkPythonVersion(pythonPath);
|
|
109
|
+
if (!pyCheck.ok) {
|
|
110
|
+
// 静默吞掉报错输出,自动回退到 "python"(不加 "3",兼容 Windows)
|
|
111
|
+
pythonPath = "python";
|
|
112
|
+
pyCheck = checkPythonVersion(pythonPath);
|
|
113
|
+
}
|
|
114
|
+
if (!pyCheck.ok) {
|
|
115
|
+
const tried = `"${existing.pythonPath || "python3"}" 与 "python"`;
|
|
116
|
+
if (pyCheck.version === "not found") {
|
|
117
|
+
console.log(chalk.red(t(`\n✗ 未找到可用的 Python(已尝试 ${tried})。`, `\n✗ No usable Python found (tried ${tried}).`)));
|
|
118
|
+
console.log(chalk.cyan(t(" ARONA 需要 Python 3.12 或 3.13(不支持 3.14)。", " ARONA requires Python 3.12 or 3.13 (3.14 is not supported).")));
|
|
119
|
+
console.log(chalk.gray(t(" 请安装 Python 后重新运行 arona setup。", " Install Python and run arona setup again.")));
|
|
120
|
+
} else {
|
|
121
|
+
console.log(chalk.red(t(`\n✗ Python 版本不兼容:${pyCheck.version}`, `\n✗ Incompatible Python version: ${pyCheck.version}`)));
|
|
122
|
+
console.log(chalk.cyan(t(" ARONA 需要 Python 3.12 或 3.13(不支持 3.14,因 pydantic-core 限制)。", " ARONA requires Python 3.12 or 3.13 (3.14 not supported due to pydantic-core).")));
|
|
123
|
+
console.log(chalk.gray(t(" 请安装正确版本后重新运行 arona setup。", " Install the correct version and run arona setup again.")));
|
|
124
|
+
}
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
console.log(chalk.gray(t(`Python ${pyCheck.version} ✓\n`, `Python ${pyCheck.version} ✓\n`)));
|
|
128
|
+
|
|
129
|
+
const rl = readline.createInterface({
|
|
130
|
+
input: process.stdin,
|
|
131
|
+
output: process.stdout,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Ctrl+C(SIGINT)强制退出:不保存任何配置,需重新运行 arona setup 填写。
|
|
135
|
+
// 不监听会走 readline 默认清行继续,最后把半成品写进 settings.json(历史踩坑)。
|
|
136
|
+
rl.on("SIGINT", () => {
|
|
137
|
+
console.log(chalk.yellow(t(
|
|
138
|
+
"\n 已取消,配置未保存。请重新运行 arona setup。",
|
|
139
|
+
"\n Cancelled, configuration not saved. Re-run arona setup.",
|
|
140
|
+
)));
|
|
141
|
+
process.exit(130);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const ask = (prompt: string): Promise<string> =>
|
|
145
|
+
new Promise((resolve) => {
|
|
146
|
+
rl.question(prompt, (answer) => resolve(answer.trim()));
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
// ============================================================
|
|
151
|
+
// Step 0: Language / 语言(显示检测结果,可手动指定,写回 settings)
|
|
152
|
+
// ============================================================
|
|
153
|
+
console.log(chalk.bold.cyan(t("\nStep 0: 语言 / Language\n", "\nStep 0: Language / 语言\n")));
|
|
154
|
+
const detected = getLang();
|
|
155
|
+
console.log(chalk.cyan(t(` 检测到系统语言:${detected === "en" ? "英文 (en)" : "中文 (zh)"}`, ` Detected system language: ${detected === "en" ? "English (en)" : "Chinese (zh)"}`)));
|
|
156
|
+
const langInput = (await ask(t(
|
|
157
|
+
" 语言选择 [auto/en/zh](默认 auto=按系统):",
|
|
158
|
+
" Language [auto/en/zh] (default auto = follow system): ",
|
|
159
|
+
))).toLowerCase();
|
|
160
|
+
let langSetting: "auto" | "zh" | "en" = "auto";
|
|
161
|
+
if (langInput === "en") { langSetting = "en"; setLang("en"); }
|
|
162
|
+
else if (langInput === "zh") { langSetting = "zh"; setLang("zh"); }
|
|
163
|
+
else if (langInput === "auto") { langSetting = "auto"; }
|
|
164
|
+
else {
|
|
165
|
+
// 空输入或其他值:保持自动(当前检测结果)
|
|
166
|
+
langSetting = "auto";
|
|
167
|
+
}
|
|
168
|
+
console.log(chalk.gray(t(
|
|
169
|
+
` 已选择:${langSetting === "en" ? "英文" : langSetting === "zh" ? "中文" : "自动(" + detected + ")"}\n`,
|
|
170
|
+
` Selected: ${langSetting === "en" ? "English" : langSetting === "zh" ? "Chinese" : "auto (" + detected + ")"}\n`,
|
|
171
|
+
)));
|
|
172
|
+
|
|
173
|
+
// ============================================================
|
|
174
|
+
// Step 1: LLM Configuration
|
|
175
|
+
// ============================================================
|
|
176
|
+
console.log(chalk.bold.cyan("Step 1: LLM Configuration\n"));
|
|
177
|
+
|
|
178
|
+
const existingBaseUrl = existing.apiBaseUrl || "";
|
|
179
|
+
const baseUrlPrompt = existingBaseUrl
|
|
180
|
+
? ` Base URL [${existingBaseUrl}]: `
|
|
181
|
+
: ` Base URL: `;
|
|
182
|
+
const apiBaseUrl = await ask(baseUrlPrompt);
|
|
183
|
+
|
|
184
|
+
const existingKey = existing.apiKey || "";
|
|
185
|
+
const keyPrompt = existingKey
|
|
186
|
+
? ` API Key [*** (keep existing)]: `
|
|
187
|
+
: ` API Key: `;
|
|
188
|
+
const apiKeyInput = await ask(keyPrompt);
|
|
189
|
+
const apiKey = apiKeyInput || existingKey;
|
|
190
|
+
|
|
191
|
+
const existingModel = existing.model || "openai/gpt-4o";
|
|
192
|
+
const modelInput = await ask(` Model [${existingModel}]: `);
|
|
193
|
+
const rawModel = modelInput || existingModel;
|
|
194
|
+
const resolvedModel = resolveModelPrefix(rawModel, apiBaseUrl || existingBaseUrl);
|
|
195
|
+
if (resolvedModel !== rawModel) {
|
|
196
|
+
console.log(chalk.gray(` → Resolved: ${resolvedModel}`));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ============================================================
|
|
200
|
+
// Step 2: 阿里云百炼 (TTS/STT) API Key
|
|
201
|
+
// ============================================================
|
|
202
|
+
console.log(chalk.bold.cyan(t("\nStep 2: 阿里云百炼 (TTS/STT) Configuration\n", "\nStep 2: Alibaba Cloud Bailian (TTS/STT) Configuration\n")));
|
|
203
|
+
console.log(chalk.cyan(t(" 阿里云有免费额度,请放心使用。", " Alibaba Cloud offers a free quota, feel free to use it.")));
|
|
204
|
+
console.log(chalk.cyan(t(" 获取 API Key: https://help.aliyun.com/zh/model-studio/get-api-key\n", " Get API Key: https://help.aliyun.com/zh/model-studio/get-api-key\n")));
|
|
205
|
+
|
|
206
|
+
const existingTtsKey = existing.ttsApiKey || "";
|
|
207
|
+
const ttsKeyPrompt = existingTtsKey
|
|
208
|
+
? t(" 百炼 API Key [*** (keep existing)]: ", " Bailian API Key [*** (keep existing)]: ")
|
|
209
|
+
: t(" 百炼 API Key: ", " Bailian API Key: ");
|
|
210
|
+
const ttsKeyInput = await ask(ttsKeyPrompt);
|
|
211
|
+
const ttsApiKey = ttsKeyInput || existingTtsKey;
|
|
212
|
+
|
|
213
|
+
// ============================================================
|
|
214
|
+
// Step 3: Install Python Dependencies
|
|
215
|
+
// ============================================================
|
|
216
|
+
console.log(chalk.bold.cyan("\nStep 3: Install Python Dependencies\n"));
|
|
217
|
+
|
|
218
|
+
const requirementsFile = join(PROJECT_ROOT, "requirements.txt");
|
|
219
|
+
const pipCmd = demoMode
|
|
220
|
+
? `pip3.13 install -r "${requirementsFile}" -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple`
|
|
221
|
+
: `${pythonPath} -m pip install -r "${requirementsFile}" -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple`;
|
|
222
|
+
let depsOk = false;
|
|
223
|
+
try {
|
|
224
|
+
execSync(pipCmd, { stdio: "inherit" });
|
|
225
|
+
console.log(chalk.green(t("\n ✓ Python 依赖安装完成", "\n ✓ Python dependencies installed")));
|
|
226
|
+
depsOk = true;
|
|
227
|
+
} catch {
|
|
228
|
+
console.log(chalk.red(t("\n ✗ Python 依赖安装失败", "\n ✗ Failed to install Python dependencies")));
|
|
229
|
+
console.log(chalk.gray(t(` 请手动运行: ${pipCmd}`, ` Please run manually: ${pipCmd}`)));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ============================================================
|
|
233
|
+
// Step 4: Voice Cloning(多角色多选)
|
|
234
|
+
// ============================================================
|
|
235
|
+
console.log(chalk.bold.cyan("\nStep 4: Voice Cloning\n"));
|
|
236
|
+
|
|
237
|
+
if (!demoMode && !ttsApiKey) {
|
|
238
|
+
console.log(chalk.yellow(t(" 跳过音色克隆:未提供百炼 API Key。", " Skipping voice cloning: no Bailian API Key provided.")));
|
|
239
|
+
console.log(chalk.gray(t(" 重新运行 arona setup 并填写百炼 API Key 以配置音色。", " Re-run arona setup and fill in the Bailian API Key to configure the voice.")));
|
|
240
|
+
} else if (!demoMode && !depsOk) {
|
|
241
|
+
console.log(chalk.yellow(t(" 跳过音色克隆:Python 依赖未安装成功。", " Skipping voice cloning: Python dependencies were not installed successfully.")));
|
|
242
|
+
console.log(chalk.gray(t(" 请先解决依赖安装问题,再重新运行 arona setup。", " Resolve the dependency installation issue, then re-run arona setup.")));
|
|
243
|
+
} else {
|
|
244
|
+
// 非 demoMode 才检查 dashscope;demoMode 直接进入 TUI 并模拟克隆。
|
|
245
|
+
let dashscopeOk = demoMode;
|
|
246
|
+
if (!demoMode) {
|
|
247
|
+
// Check dashscope package (safety fallback even after pip install)
|
|
248
|
+
try {
|
|
249
|
+
execSync(`${pythonPath} -c "import dashscope"`, { stdio: "pipe" });
|
|
250
|
+
dashscopeOk = true;
|
|
251
|
+
} catch {
|
|
252
|
+
console.log(chalk.yellow(t(" dashscope 包仍不可用。跳过音色克隆。", " The dashscope package is still unavailable. Skipping voice cloning.")));
|
|
253
|
+
console.log(chalk.gray(t(` 运行 ${pythonPath} -m pip install dashscope -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple 后重新执行 arona setup。`, ` Run ${pythonPath} -m pip install dashscope -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple and re-run arona setup.`)));
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (dashscopeOk) {
|
|
258
|
+
// 关闭 readline,让多选 TUI 独占 stdin raw mode(此后不再用 rl.question)。
|
|
259
|
+
rl.close();
|
|
260
|
+
|
|
261
|
+
// 已有音色的角色锁定 [*](迁移已在模块加载时完成,此处读到的 voices.json 已是新格式)。
|
|
262
|
+
const missing = getMissingAgents();
|
|
263
|
+
if (missing.length === 0) {
|
|
264
|
+
console.log(chalk.green(t(
|
|
265
|
+
" 所有角色已有音色,无需克隆。如需重新克隆请运行 arona voice add <角色名>。",
|
|
266
|
+
" All characters already have voices. Use `arona voice add <name>` to re-clone.",
|
|
267
|
+
)));
|
|
268
|
+
} else {
|
|
269
|
+
const options = AGENT_IDS.map((id) => ({
|
|
270
|
+
id,
|
|
271
|
+
label: hasVoice(id) ? `${getAgentLabel(id)}${t("(已克隆)", " (cloned)")}` : getAgentLabel(id),
|
|
272
|
+
locked: hasVoice(id),
|
|
273
|
+
}));
|
|
274
|
+
const selected = await multiSelect(
|
|
275
|
+
t("选择要克隆音色的角色(Arona/Plana/砂狼白子/小鸟游星野)", "Select characters to clone voices (Arona/Plana/Shiroko/Hoshino)"),
|
|
276
|
+
options,
|
|
277
|
+
new Set<string>(), // 已有音色者 locked 强制 [*],未克隆者默认 [ ]
|
|
278
|
+
t(
|
|
279
|
+
" ↑/↓ 切换 · 空格选中 [*] · 回车克隆 · Esc 取消",
|
|
280
|
+
" ↑/↓ move · Space select [*] · Enter clone · Esc cancel",
|
|
281
|
+
),
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
if (selected === null) {
|
|
285
|
+
// Esc / Ctrl+C:放弃整个 setup,不保存配置(半成品配置不能覆写 settings.json)
|
|
286
|
+
console.log(chalk.yellow(t(
|
|
287
|
+
" 已取消,配置未保存。请重新运行 arona setup。",
|
|
288
|
+
" Cancelled, configuration not saved. Re-run arona setup.",
|
|
289
|
+
)));
|
|
290
|
+
return;
|
|
291
|
+
} else if (selected.size === 0) {
|
|
292
|
+
console.log(chalk.cyan(t(" 未选择任何角色,跳过音色克隆(TTS 将保持静音)。", " No character selected, skipping voice cloning (TTS stays muted).")));
|
|
293
|
+
} else {
|
|
294
|
+
const model = existing.ttsModel || "qwen-audio-3.0-tts-plus";
|
|
295
|
+
for (const id of AGENT_IDS) {
|
|
296
|
+
if (!selected.has(id)) continue;
|
|
297
|
+
const voiceMp3 = VOICE_AUDIO[id];
|
|
298
|
+
if (!existsSync(voiceMp3)) {
|
|
299
|
+
console.log(chalk.yellow(t(` 未找到音色文件(${voiceMp3}),跳过 ${getAgentLabel(id)}。`, ` Voice file not found (${voiceMp3}), skipping ${getAgentLabel(id)}.`)));
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
if (demoMode) {
|
|
303
|
+
// 演示模式:不调用 voice_clone.py、不写 voices.json,静默 5s 后显示成功。
|
|
304
|
+
console.log(chalk.cyan(t(` 正在模拟克隆 ${getAgentLabel(id)} 的音色(演示模式)...`, ` Simulating voice clone for ${getAgentLabel(id)} (demo mode)...`)));
|
|
305
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
306
|
+
console.log(chalk.green(t(` ✓ ${getAgentLabel(id)} 音色克隆成功(演示模式,未写入)`, ` ✓ ${getAgentLabel(id)} voice cloned (demo mode, not persisted)`)));
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
console.log(chalk.cyan(t(` 正在克隆 ${getAgentLabel(id)} 的音色(可能需要 1-2 分钟)...`, ` Cloning ${getAgentLabel(id)}'s voice (may take 1-2 minutes)...`)));
|
|
310
|
+
try {
|
|
311
|
+
const voiceId = await cloneVoice(id, ttsApiKey, model);
|
|
312
|
+
setVoiceId(id, voiceId);
|
|
313
|
+
if (verbose) {
|
|
314
|
+
console.log(chalk.green(t(` ✓ ${getAgentLabel(id)} 音色克隆成功(voice_id: ${voiceId})`, ` ✓ ${getAgentLabel(id)} voice cloned (voice_id: ${voiceId})`)));
|
|
315
|
+
} else {
|
|
316
|
+
console.log(chalk.green(t(` ✓ ${getAgentLabel(id)} 音色克隆成功`, ` ✓ ${getAgentLabel(id)} voice cloned`)));
|
|
317
|
+
}
|
|
318
|
+
} catch (err) {
|
|
319
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
320
|
+
console.log(chalk.red(t(` ✗ ${getAgentLabel(id)} 音色克隆失败:${msg}`, ` ✗ ${getAgentLabel(id)} voice cloning failed: ${msg}`)));
|
|
321
|
+
console.log(chalk.gray(t(" 稍后可运行 arona voice add 重试。", " You can run `arona voice add` later to retry.")));
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ============================================================
|
|
330
|
+
// Save settings
|
|
331
|
+
// ============================================================
|
|
332
|
+
// demoMode: never write to settings.json. The user owns this field
|
|
333
|
+
// by direct file edit; the wizard only reads it.
|
|
334
|
+
if (!demoMode) {
|
|
335
|
+
const settings: Settings = {
|
|
336
|
+
apiKey: apiKey,
|
|
337
|
+
apiBaseUrl: apiBaseUrl || existingBaseUrl,
|
|
338
|
+
model: resolvedModel,
|
|
339
|
+
thinkingLevel: existing.thinkingLevel || "medium",
|
|
340
|
+
language: langSetting,
|
|
341
|
+
mainAgent: existing.mainAgent || "arona", // 保留 /change-agent 的选择,勿覆盖
|
|
342
|
+
subAgents: existing.subAgents || [], // 保留 /change-agent 的多选,勿覆盖
|
|
343
|
+
ttsEnabled: existing.ttsEnabled ?? existing.ttsAuto ?? true, // ttsAuto 已合并,旧配置兼容
|
|
344
|
+
sttEnabled: existing.sttEnabled ?? true,
|
|
345
|
+
workspaceId: existing.workspaceId || "",
|
|
346
|
+
ttsApiKey: ttsApiKey,
|
|
347
|
+
ttsModel: existing.ttsModel || "qwen-audio-3.0-tts-plus",
|
|
348
|
+
ttsSampleRate: existing.ttsSampleRate || 22050,
|
|
349
|
+
sttApiKey: ttsApiKey, // Same key for TTS and STT
|
|
350
|
+
sttModel: existing.sttModel || "qwen-audio-3.0-asr-flash-streaming",
|
|
351
|
+
sttFormat: existing.sttFormat || "pcm",
|
|
352
|
+
sttSampleRate: existing.sttSampleRate || 16000,
|
|
353
|
+
cuaApiKey: existing.cuaApiKey || "",
|
|
354
|
+
pythonPath: pythonPath,
|
|
355
|
+
mcpServers: existing.mcpServers || {},
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2) + "\n");
|
|
359
|
+
|
|
360
|
+
console.log(chalk.green(t(`\n✓ 配置已保存到 ${SETTINGS_FILE}`, `\n✓ Configuration saved to ${SETTINGS_FILE}`)));
|
|
361
|
+
}
|
|
362
|
+
console.log(chalk.cyan(t(" 运行 arona 启动 Agent。\n", " Run arona to start the Agent.\n")));
|
|
363
|
+
} finally {
|
|
364
|
+
rl.close();
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
main().catch((err) => {
|
|
369
|
+
console.error(chalk.red(t(`Setup error: ${err instanceof Error ? err.message : err}`, `Setup error: ${err instanceof Error ? err.message : err}`)));
|
|
370
|
+
process.exit(1);
|
|
371
|
+
});
|
package/src/skills.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { readFileSync, existsSync, readdirSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { SKILLS_DIR } from "./config.ts";
|
|
4
|
+
import type { DefaultResourceLoader } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
|
|
6
|
+
export interface SkillInfo {
|
|
7
|
+
name: string;
|
|
8
|
+
description: string;
|
|
9
|
+
path: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 从 SKILL.md 内容提取描述:优先解析 YAML frontmatter 的 description 主字段
|
|
14
|
+
* (忽略 description_en 等扩展字段),回退到第一个 "# " 标题;都没有则返回空串。
|
|
15
|
+
*/
|
|
16
|
+
function extractSkillDescription(content: string): string {
|
|
17
|
+
const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
18
|
+
if (fmMatch) {
|
|
19
|
+
const descMatch = fmMatch[1].match(/^description:\s*(.+)$/m);
|
|
20
|
+
if (descMatch) return descMatch[1].trim();
|
|
21
|
+
}
|
|
22
|
+
const headingMatch = content.match(/^#\s+(.+)$/m);
|
|
23
|
+
return headingMatch ? headingMatch[1].trim() : "";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* List skills discovered by the DefaultResourceLoader plus any in ~/.arona/skills/.
|
|
28
|
+
*/
|
|
29
|
+
export function listSkills(loader?: DefaultResourceLoader): SkillInfo[] {
|
|
30
|
+
const skills: SkillInfo[] = [];
|
|
31
|
+
|
|
32
|
+
// From ResourceLoader (Pi SDK discovery)
|
|
33
|
+
if (loader) {
|
|
34
|
+
try {
|
|
35
|
+
const { skills: loaderSkills } = loader.getSkills();
|
|
36
|
+
for (const s of loaderSkills) {
|
|
37
|
+
skills.push({
|
|
38
|
+
name: s.name,
|
|
39
|
+
description: s.description || "",
|
|
40
|
+
path: s.filePath,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
// Loader might not be initialized
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// From ~/.arona/skills/ directory
|
|
49
|
+
if (existsSync(SKILLS_DIR)) {
|
|
50
|
+
const entries = readdirSync(SKILLS_DIR, { withFileTypes: true });
|
|
51
|
+
for (const entry of entries) {
|
|
52
|
+
if (entry.isDirectory()) {
|
|
53
|
+
const skillMdPath = join(SKILLS_DIR, entry.name, "SKILL.md");
|
|
54
|
+
if (existsSync(skillMdPath)) {
|
|
55
|
+
// Avoid duplicates
|
|
56
|
+
if (!skills.find((s) => s.name === entry.name)) {
|
|
57
|
+
const content = readFileSync(skillMdPath, "utf-8");
|
|
58
|
+
skills.push({
|
|
59
|
+
name: entry.name,
|
|
60
|
+
description: extractSkillDescription(content),
|
|
61
|
+
path: skillMdPath,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return skills;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get the content of a skill by name.
|
|
74
|
+
*/
|
|
75
|
+
export function getSkillContent(name: string, loader?: DefaultResourceLoader): string | null {
|
|
76
|
+
// Check loader skills
|
|
77
|
+
if (loader) {
|
|
78
|
+
try {
|
|
79
|
+
const { skills } = loader.getSkills();
|
|
80
|
+
const skill = skills.find((s) => s.name === name);
|
|
81
|
+
if (skill && existsSync(skill.filePath)) {
|
|
82
|
+
return readFileSync(skill.filePath, "utf-8");
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
// ignore
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Check ~/.arona/skills/
|
|
90
|
+
const skillMdPath = join(SKILLS_DIR, name, "SKILL.md");
|
|
91
|
+
if (existsSync(skillMdPath)) {
|
|
92
|
+
return readFileSync(skillMdPath, "utf-8");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return null;
|
|
96
|
+
}
|