arona-agent 1.1.2 → 1.1.3
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/package.json +1 -1
- package/pet/main.cjs +62 -3
- package/pet/preload.cjs +2 -1
- package/pet/renderer/fx.html +45 -0
- package/pet/renderer/fx.js +34 -0
- package/pet/renderer/index.html +0 -1
- package/pet/renderer/renderer.js +4 -34
- package/pet/renderer/style.css +1 -44
- package/src/agent.ts +4 -2
- package/src/logo.ts +5 -1
- package/src/repl.ts +45 -12
- package/src/setup.ts +76 -51
- package/src/slash_menu.ts +10 -4
- package/src/slash_registry.ts +2 -2
- package/src/tools/skill_tools.ts +2 -2
- package/src/tui_select.ts +13 -1
- package/src/undo.ts +4 -5
- package/src/voice_cli.ts +60 -40
- package/src/voices.ts +6 -0
package/package.json
CHANGED
package/pet/main.cjs
CHANGED
|
@@ -28,9 +28,9 @@ const AGENT_IDS = [MAIN_AGENT_ID, ...SUB_AGENT_IDS];
|
|
|
28
28
|
|
|
29
29
|
const PREFIX = "###PET###";
|
|
30
30
|
const POS_FILE = path.join(os.homedir(), ".arona", "pet.json");
|
|
31
|
-
const WIN_W =
|
|
31
|
+
const WIN_W = 320; // 窗口 = Spine 角色渲染区本体(收窄以避免透明区域拦截点击造成误触;角色尺寸不变)
|
|
32
32
|
const WIN_H = 674;
|
|
33
|
-
const SUB_OFFSET_X =
|
|
33
|
+
const SUB_OFFSET_X = 340; // 子窗口默认横向错开(略大于 WIN_W)
|
|
34
34
|
const SUB_OFFSET_Y = 40;
|
|
35
35
|
|
|
36
36
|
// --verbose(src/pet.ts 注入 ARONA_PET_VERBOSE=1 + --enable-logging):
|
|
@@ -282,6 +282,62 @@ ipcMain.on("pet:fx-up", () => {
|
|
|
282
282
|
if (fxWin && !fxWin.isDestroyed()) fxWin.webContents.send("fx:up");
|
|
283
283
|
});
|
|
284
284
|
|
|
285
|
+
// ---- 文字气泡:画在全屏特效窗上(桌宠窗口已收窄为角色本体,不再有气泡区) ----
|
|
286
|
+
// 特效窗鼠标穿透(setIgnoreMouseEvents),气泡纯展示不会拦点击;层级高于桌宠。
|
|
287
|
+
// 锚点沿用旧窗口内定位的观感:气泡出现在头部右上方、小尾巴指向角色;右侧越出
|
|
288
|
+
// 所在显示器时翻到左侧(flip,fx 层镜像小尾巴)。
|
|
289
|
+
const BUBBLE_ANCHOR_X = 264; // 默认:气泡左缘相对桌宠窗口左上角
|
|
290
|
+
const BUBBLE_ANCHOR_Y = 90;
|
|
291
|
+
const BUBBLE_MAX_W = 220; // 与 fx 层样式 max-width 一致
|
|
292
|
+
const BUBBLE_FLIP_X = -196; // 翻转:气泡左缘相对桌宠窗口左上角(右缘留 ~24px 缝隙给小尾巴)
|
|
293
|
+
const BUBBLE_FLIP_PAD = 12; // 翻转阈值缓冲:贴屏幕右缘多少 px 内就算溢出
|
|
294
|
+
|
|
295
|
+
/** 桌宠窗口当前姿态 → 特效窗本地气泡锚点 { x, y, flip };特效窗/角色窗不可用时返回 null */
|
|
296
|
+
function bubbleAnchorLocal(agentId) {
|
|
297
|
+
const bw = windowByAgent(agentId);
|
|
298
|
+
if (!bw || bw.isDestroyed() || !fxWin || fxWin.isDestroyed()) return null;
|
|
299
|
+
const [wx, wy] = bw.getPosition();
|
|
300
|
+
const [fxX, fxY] = fxWin.getPosition();
|
|
301
|
+
let flip = false;
|
|
302
|
+
let gx = wx + BUBBLE_ANCHOR_X;
|
|
303
|
+
try {
|
|
304
|
+
const d = screen.getDisplayNearestPoint({ x: wx, y: wy });
|
|
305
|
+
const rightEdge = d.bounds.x + d.bounds.width;
|
|
306
|
+
if (gx + BUBBLE_MAX_W + BUBBLE_FLIP_PAD > rightEdge) {
|
|
307
|
+
// 右侧放不下 → 翻到角色左侧
|
|
308
|
+
flip = true;
|
|
309
|
+
gx = wx + BUBBLE_FLIP_X;
|
|
310
|
+
}
|
|
311
|
+
gx = Math.max(d.bounds.x + BUBBLE_FLIP_PAD, Math.min(gx, rightEdge - BUBBLE_FLIP_PAD));
|
|
312
|
+
// 垂直方向钳回所在显示器(粗略按气泡最大高度 ~140px 预留)
|
|
313
|
+
const gy = Math.max(d.bounds.y, Math.min(wy + BUBBLE_ANCHOR_Y, d.bounds.y + d.bounds.height - 140));
|
|
314
|
+
return { x: Math.round(gx - fxX), y: Math.round(gy - fy), flip };
|
|
315
|
+
} catch {
|
|
316
|
+
return { x: Math.round(gx - fxX), y: Math.round(wy + BUBBLE_ANCHOR_Y - fxY), flip };
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** 把某角色的文字消息转成特效窗上的气泡 show/hide */
|
|
321
|
+
function forwardBubble(agentId, kind, data) {
|
|
322
|
+
if (!fxWin || fxWin.isDestroyed()) return;
|
|
323
|
+
if (kind === "tts_end") {
|
|
324
|
+
fxWin.webContents.send("pet:bubble", { agent: agentId, kind: "hide" });
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
if (typeof data !== "string" || !data) return;
|
|
328
|
+
const pos = bubbleAnchorLocal(agentId);
|
|
329
|
+
if (!pos) return;
|
|
330
|
+
fxWin.webContents.send("pet:bubble", { agent: agentId, kind: "show", data, ...pos });
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** 拖动中/落位后同步气泡锚点(气泡已显示时 fx 层原地位移,未显示则忽略) */
|
|
334
|
+
function syncBubblePosition(agentId) {
|
|
335
|
+
if (!fxWin || fxWin.isDestroyed()) return;
|
|
336
|
+
const pos = bubbleAnchorLocal(agentId);
|
|
337
|
+
if (!pos) return;
|
|
338
|
+
fxWin.webContents.send("pet:bubble", { agent: agentId, kind: "move", ...pos });
|
|
339
|
+
}
|
|
340
|
+
|
|
285
341
|
// ---- 全局光标轮询(~60Hz,DIP 坐标系内运算,供渲染层瞳孔跟随 + 按住期间晃动检测补采样) ----
|
|
286
342
|
// 对每个桌宠窗口分别发送窗口本地坐标 + 全局坐标(gx/gy:renderer 按住期间用于晃动检测——
|
|
287
343
|
// 光标快速甩动划出窗口时 mousemove 断流,本轮询不断流,轻微出窗仍能采到晃动)
|
|
@@ -324,6 +380,7 @@ ipcMain.on("pet:drag", (e, dx, dy) => {
|
|
|
324
380
|
if (p && windowByAgent(agentId)) {
|
|
325
381
|
windowByAgent(agentId).setPosition(Math.round(p.x), Math.round(p.y));
|
|
326
382
|
dragPendingMap.delete(agentId);
|
|
383
|
+
syncBubblePosition(agentId); // 气泡跟随拖动
|
|
327
384
|
}
|
|
328
385
|
}, 16));
|
|
329
386
|
}
|
|
@@ -343,6 +400,7 @@ ipcMain.on("pet:dragend", (e) => {
|
|
|
343
400
|
bw.setPosition(Math.round(pending.x), Math.round(pending.y));
|
|
344
401
|
dragPendingMap.delete(agentId);
|
|
345
402
|
}
|
|
403
|
+
syncBubblePosition(agentId); // 落位后校正气泡锚点(flip 状态也可能变化)
|
|
346
404
|
const [x, y] = bw.getPosition();
|
|
347
405
|
savePosition(agentId, x, y);
|
|
348
406
|
send({ type: "moved", agent: agentId, x, y });
|
|
@@ -385,7 +443,8 @@ function handleMessage(msg) {
|
|
|
385
443
|
break;
|
|
386
444
|
case "text": {
|
|
387
445
|
const id = msg.agent && AGENTS[msg.agent] ? msg.agent : MAIN_AGENT_ID;
|
|
388
|
-
|
|
446
|
+
// 气泡已迁出桌宠窗口(窗口收窄为角色本体),统一渲染在全屏特效窗上
|
|
447
|
+
forwardBubble(id, msg.kind, msg.data);
|
|
389
448
|
break;
|
|
390
449
|
}
|
|
391
450
|
case "tts_level": {
|
package/pet/preload.cjs
CHANGED
|
@@ -3,7 +3,6 @@ const { contextBridge, ipcRenderer } = require("electron");
|
|
|
3
3
|
contextBridge.exposeInMainWorld("petAPI", {
|
|
4
4
|
onEmotion: (cb) => ipcRenderer.on("pet:emotion", (_e, name) => cb(name)),
|
|
5
5
|
onReset: (cb) => ipcRenderer.on("pet:reset", () => cb()),
|
|
6
|
-
onText: (cb) => ipcRenderer.on("pet:text", (_e, p) => cb(p)),
|
|
7
6
|
// TTS 播放中实时音量(RMS 0~1)→ 嘴型 lip-sync
|
|
8
7
|
onTtsLevel: (cb) => ipcRenderer.on("pet:tts-level", (_e, rms) => cb(rms)),
|
|
9
8
|
getAgentConfig: () => ipcRenderer.invoke("pet:get-agent-config"),
|
|
@@ -20,4 +19,6 @@ contextBridge.exposeInMainWorld("petAPI", {
|
|
|
20
19
|
onFxDown: (cb) => ipcRenderer.on("fx:down", (_e, x, y) => cb(x, y)),
|
|
21
20
|
onFxMove: (cb) => ipcRenderer.on("fx:move", (_e, x, y) => cb(x, y)),
|
|
22
21
|
onFxUp: (cb) => ipcRenderer.on("fx:up", () => cb()),
|
|
22
|
+
// 文字气泡:main.cjs 把桌宠窗口姿态换算成本地坐标后发到这里(fx 层专用)
|
|
23
|
+
onBubble: (cb) => ipcRenderer.on("pet:bubble", (_e, p) => cb(p)),
|
|
23
24
|
});
|
package/pet/renderer/fx.html
CHANGED
|
@@ -20,10 +20,55 @@
|
|
|
20
20
|
height: 100%;
|
|
21
21
|
pointer-events: none;
|
|
22
22
|
}
|
|
23
|
+
/* 文字气泡容器:铺满窗口,纯展示(特效窗整体 setIgnoreMouseEvents) */
|
|
24
|
+
#fx-bubbles {
|
|
25
|
+
position: absolute;
|
|
26
|
+
inset: 0;
|
|
27
|
+
pointer-events: none;
|
|
28
|
+
z-index: 5;
|
|
29
|
+
}
|
|
30
|
+
/* 角色语音气泡(从桌宠窗口迁入):位置由 main.cjs 按桌宠窗口姿态换算后下发,
|
|
31
|
+
显示在角色头部侧上方;.flip 时小尾巴镜像到右侧指向角色。淡出用 opacity 过渡 */
|
|
32
|
+
.bubble {
|
|
33
|
+
position: absolute;
|
|
34
|
+
max-width: 220px;
|
|
35
|
+
padding: 8px 12px;
|
|
36
|
+
box-sizing: border-box;
|
|
37
|
+
background: rgba(20, 22, 34, 0.82);
|
|
38
|
+
border-radius: 14px;
|
|
39
|
+
color: #fff;
|
|
40
|
+
font-size: 13px;
|
|
41
|
+
line-height: 1.45;
|
|
42
|
+
text-align: left;
|
|
43
|
+
white-space: pre-line;
|
|
44
|
+
word-break: break-all;
|
|
45
|
+
opacity: 1;
|
|
46
|
+
transition: opacity 0.25s ease;
|
|
47
|
+
}
|
|
48
|
+
.bubble::after {
|
|
49
|
+
content: "";
|
|
50
|
+
position: absolute;
|
|
51
|
+
left: -12px;
|
|
52
|
+
top: 50%;
|
|
53
|
+
transform: translateY(-50%);
|
|
54
|
+
border-top: 8px solid transparent;
|
|
55
|
+
border-bottom: 8px solid transparent;
|
|
56
|
+
border-right: 12px solid rgba(20, 22, 34, 0.82);
|
|
57
|
+
}
|
|
58
|
+
.bubble.flip::after {
|
|
59
|
+
left: auto;
|
|
60
|
+
right: -12px;
|
|
61
|
+
border-right: none;
|
|
62
|
+
border-left: 12px solid rgba(20, 22, 34, 0.82);
|
|
63
|
+
}
|
|
64
|
+
.bubble.hidden {
|
|
65
|
+
opacity: 0;
|
|
66
|
+
}
|
|
23
67
|
</style>
|
|
24
68
|
</head>
|
|
25
69
|
<body>
|
|
26
70
|
<div id="fx"></div>
|
|
71
|
+
<div id="fx-bubbles"></div>
|
|
27
72
|
<script src="../../node_modules/ba-click-fx/dist/ba-click-fx.iife.js"></script>
|
|
28
73
|
<script src="fx.js"></script>
|
|
29
74
|
</body>
|
package/pet/renderer/fx.js
CHANGED
|
@@ -38,4 +38,38 @@ window.petAPI.onFxUp(() => {
|
|
|
38
38
|
if (fx) fx.pointerUp(1);
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
+
// ---- 文字气泡:按角色各维护一个节点(气泡已从桌宠窗口迁到这里渲染) ----
|
|
42
|
+
// main.cjs 下发的消息:
|
|
43
|
+
// { agent, kind: "show", data, x, y, flip } — 上屏文本并定位(flip=角色在右侧,尾巴镜像)
|
|
44
|
+
// { agent, kind: "move", x, y, flip } — 仅更新位置(拖动跟随;未显示时无副作用)
|
|
45
|
+
// { agent, kind: "hide" } — 淡出(TTS 播完 / 兜底定时器 / 打断)
|
|
46
|
+
const bubbles = new Map();
|
|
47
|
+
|
|
48
|
+
function ensureBubble(agentId) {
|
|
49
|
+
let el = bubbles.get(agentId);
|
|
50
|
+
if (!el) {
|
|
51
|
+
el = document.createElement("div");
|
|
52
|
+
el.className = "bubble hidden";
|
|
53
|
+
document.getElementById("fx-bubbles").appendChild(el);
|
|
54
|
+
bubbles.set(agentId, el);
|
|
55
|
+
}
|
|
56
|
+
return el;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
window.petAPI.onBubble((msg) => {
|
|
60
|
+
const agentId = String(msg?.agent ?? "main");
|
|
61
|
+
if (msg.kind === "hide") {
|
|
62
|
+
bubbles.get(agentId)?.classList.add("hidden");
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const el = ensureBubble(agentId);
|
|
66
|
+
el.style.left = `${Math.round(msg.x)}px`;
|
|
67
|
+
el.style.top = `${Math.round(msg.y)}px`;
|
|
68
|
+
el.classList.toggle("flip", !!msg.flip);
|
|
69
|
+
if (msg.kind === "show" && typeof msg.data === "string" && msg.data) {
|
|
70
|
+
el.textContent = msg.data;
|
|
71
|
+
el.classList.remove("hidden");
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
41
75
|
initFx();
|
package/pet/renderer/index.html
CHANGED
package/pet/renderer/renderer.js
CHANGED
|
@@ -104,9 +104,9 @@ const PAT_TURN_PX = 20; // 单次"有效换向"所需的最小半波位
|
|
|
104
104
|
const DIZZY_TURNS = 2; // 换向次数阈值(一来一回)
|
|
105
105
|
const DIZZY_AMPLITUDE = 150; // 窗口内单轴摆幅极差阈值(px)——"大幅"档位,待实测调整
|
|
106
106
|
const DIZZY_RECOVER_MS = 2000; // 松手后保持晕脸的时长,之后自动回待机
|
|
107
|
-
// 头部区域:x
|
|
108
|
-
//
|
|
109
|
-
//
|
|
107
|
+
// 头部区域:x 比例基于 320px 渲染区宽(spineCanvas.clientWidth——旧版窗口曾含 260px 气泡区,
|
|
108
|
+
// 若按整窗 innerWidth 算 x 会把头部左缘切掉;现窗口已收窄为渲染区本体,两者恰好相等,
|
|
109
|
+
// 但仍按 clientWidth 计算以防未来再加非渲染区域),y 基于窗口高。
|
|
110
110
|
// Spine 姿势实测头部在 CSS y 119~180。
|
|
111
111
|
const HEAD_BOX = { xMin: 0.26, xMax: 0.78, yMin: 0.06, yMax: 0.29 };
|
|
112
112
|
// 摸头期间"离开头部"判定缓冲(px):用户反馈 16px 太严苛,先放宽 50px、再要求更宽 → 90px;
|
|
@@ -300,20 +300,7 @@ function onMouseUp() {
|
|
|
300
300
|
}
|
|
301
301
|
}
|
|
302
302
|
|
|
303
|
-
// ----
|
|
304
|
-
const petBubble = document.getElementById("pet-bubble");
|
|
305
|
-
let bubbleTimer = null;
|
|
306
|
-
|
|
307
|
-
function showBubble(text) {
|
|
308
|
-
if (!petBubble) return;
|
|
309
|
-
petBubble.textContent = text;
|
|
310
|
-
petBubble.classList.remove("hidden");
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
function hideBubble() {
|
|
314
|
-
if (!petBubble) return;
|
|
315
|
-
petBubble.classList.add("hidden");
|
|
316
|
-
}
|
|
303
|
+
// ---- 文字气泡:已迁至全屏特效窗(fx.html/fx.js),本窗口不再渲染气泡 ----
|
|
317
304
|
|
|
318
305
|
async function init() {
|
|
319
306
|
// 情绪预设映射:agents.cjs(与 main 进程白名单同源)值 = 预设动画名;
|
|
@@ -360,23 +347,6 @@ async function init() {
|
|
|
360
347
|
window.SpineLayer.setMouthLevel(rms);
|
|
361
348
|
});
|
|
362
349
|
|
|
363
|
-
// 文字气泡:只显示 mid/final 短消息;等到 TTS 播放结束(tts_end)再消失
|
|
364
|
-
window.petAPI.onText(({ kind, data }) => {
|
|
365
|
-
if (!petBubble) return;
|
|
366
|
-
if (kind === "tts_end") {
|
|
367
|
-
if (bubbleTimer) clearTimeout(bubbleTimer);
|
|
368
|
-
bubbleTimer = null;
|
|
369
|
-
hideBubble();
|
|
370
|
-
return;
|
|
371
|
-
}
|
|
372
|
-
// mid / final
|
|
373
|
-
if (typeof data === "string" && data.length > 0) {
|
|
374
|
-
if (bubbleTimer) clearTimeout(bubbleTimer);
|
|
375
|
-
bubbleTimer = null;
|
|
376
|
-
showBubble(data);
|
|
377
|
-
}
|
|
378
|
-
});
|
|
379
|
-
|
|
380
350
|
document.addEventListener("mousedown", onMouseDown);
|
|
381
351
|
document.addEventListener("mousemove", onMouseMove);
|
|
382
352
|
document.addEventListener("mouseup", onMouseUp);
|
package/pet/renderer/style.css
CHANGED
|
@@ -31,47 +31,4 @@ body:active {
|
|
|
31
31
|
pointer-events: none;
|
|
32
32
|
z-index: 1;
|
|
33
33
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
/* 桌宠文字气泡:定位到角色头部右侧,贴近角色(不遮脸、不高高飘在右上);
|
|
37
|
-
top 取头部高度(头 CSS y 40~195),left 270px 贴近头部右缘(≈250)留 ~20px 缝隙,
|
|
38
|
-
由 ::after 小尾巴向左伸进缝隙指向角色(视觉上"连到"角色,不显远);
|
|
39
|
-
高于 Spine、鼠标穿透;只显示 mid/final 短消息 */
|
|
40
|
-
#pet-bubble {
|
|
41
|
-
position: absolute;
|
|
42
|
-
left: 270px;
|
|
43
|
-
top: 90px;
|
|
44
|
-
transform: none;
|
|
45
|
-
max-width: 220px;
|
|
46
|
-
padding: 8px 12px;
|
|
47
|
-
box-sizing: border-box;
|
|
48
|
-
background: rgba(20, 22, 34, 0.82);
|
|
49
|
-
border-radius: 14px;
|
|
50
|
-
color: #fff;
|
|
51
|
-
font-size: 13px;
|
|
52
|
-
line-height: 1.45;
|
|
53
|
-
text-align: left;
|
|
54
|
-
white-space: pre-line;
|
|
55
|
-
word-break: break-all;
|
|
56
|
-
pointer-events: none;
|
|
57
|
-
z-index: 10;
|
|
58
|
-
opacity: 1;
|
|
59
|
-
transition: opacity 0.25s ease;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/* 左指小三角尾巴:从气泡左缘向左伸进气泡与角色头部的缝隙里,指向角色。
|
|
63
|
-
随父级 hidden / opacity 过渡一起隐藏。top 取气泡中部即可,位置可按需微调 */
|
|
64
|
-
#pet-bubble::after {
|
|
65
|
-
content: "";
|
|
66
|
-
position: absolute;
|
|
67
|
-
left: -12px;
|
|
68
|
-
top: 50%;
|
|
69
|
-
transform: translateY(-50%);
|
|
70
|
-
border-top: 8px solid transparent;
|
|
71
|
-
border-bottom: 8px solid transparent;
|
|
72
|
-
border-right: 12px solid rgba(20, 22, 34, 0.82);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
#pet-bubble.hidden {
|
|
76
|
-
opacity: 0;
|
|
77
|
-
}
|
|
34
|
+
/* 文字气泡已迁至全屏特效窗(fx.html 的 .bubble),桌宠窗口只保留角色渲染区 */
|
package/src/agent.ts
CHANGED
|
@@ -20,7 +20,7 @@ import { createSkillTools } from "./tools/skill_tools.ts";
|
|
|
20
20
|
import { readDocsTool } from "./tools/read_docs_tool.ts";
|
|
21
21
|
import { connectMcpServers } from "./mcp.ts";
|
|
22
22
|
import { InMemoryCredentialStore } from "./in_memory_credentials.ts";
|
|
23
|
-
import { getMainAgent, type SubAgentId, type AgentId } from "./agent_registry.ts";
|
|
23
|
+
import { getMainAgent, getAgentLabel, type SubAgentId, type AgentId } from "./agent_registry.ts";
|
|
24
24
|
import { speakerContextExtension } from "./speaker_context.ts";
|
|
25
25
|
import { gestureContextExtension } from "./gesture_context.ts";
|
|
26
26
|
import { t, getLang } from "./locale.ts";
|
|
@@ -593,7 +593,7 @@ export async function initAgent(): Promise<{
|
|
|
593
593
|
|
|
594
594
|
// ============================================================
|
|
595
595
|
// 子 Agent(白子 / 星野)—— 纯聊天角色,仅 change_emotion + keep_silent
|
|
596
|
-
//
|
|
596
|
+
// 人设全文硬编码
|
|
597
597
|
// ============================================================
|
|
598
598
|
|
|
599
599
|
const SUB_PERSONA_ZH: Record<SubAgentId, string> = {
|
|
@@ -668,6 +668,7 @@ function buildSubSystemPrompt(id: SubAgentId, memoryContent: string): string {
|
|
|
668
668
|
# Group Chat Rules
|
|
669
669
|
|
|
670
670
|
- You are one of several desktop-pet characters chatting with Sensei (the user).
|
|
671
|
+
- You are ${getAgentLabel(id)}. Always speak as yourself — do not play, mimic, or mix in the identity or tone of any other character (Arona, Plana, Shiroko, Hoshino, Hanako, Koharu).
|
|
671
672
|
- After the main agent finishes replying, each enabled sub-agent takes a turn. Keep your reply SHORT (one or two sentences), natural, in-character, and add nothing but your own spoken line.
|
|
672
673
|
- Do not repeat or summarize the main agent's reply.
|
|
673
674
|
- In the conversation history, assistant messages carry a \`Name:\` prefix showing who said them (e.g. "Arona:", "Shiroko:", etc.); user inputs are Sensei speaking. When you reply, do NOT add any name prefix.
|
|
@@ -681,6 +682,7 @@ Available tools: change_emotion (set the emotion before speaking), keep_silent (
|
|
|
681
682
|
# 群聊规则
|
|
682
683
|
|
|
683
684
|
- 你是多个桌宠角色之一,正在陪老师聊天。
|
|
685
|
+
- 你是${getAgentLabel(id)}。始终以自己的身份发言,禁止扮演或模仿或混用其他角色(阿洛娜、普拉娜、白子、星野、花子、小春)的身份与语气。
|
|
684
686
|
- 主 Agent 回复完毕后,每个启用的子 Agent 依次发言。回复保持简短(一两句),贴角色,只说自己的台词。
|
|
685
687
|
- 不要复读或总结主 Agent 的话。
|
|
686
688
|
- 对话历史中,assistant 消息带「角色名:」前缀标明发言者(如「阿洛娜:」「砂狼白子:」等);用户输入是 Sensei 说的。你发言时不要加名字前缀。
|
package/src/logo.ts
CHANGED
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
|
|
8
8
|
import figlet from "figlet";
|
|
9
9
|
|
|
10
|
+
// Fonts 是 @types/figlet 里 export = 命名空间的成员类型,默认导入/具名导入都拿不到;
|
|
11
|
+
// 用 loadFont 的首参反推出等价的字体名联合类型。
|
|
12
|
+
type FigletFont = Parameters<typeof figlet.loadFont>[0];
|
|
13
|
+
|
|
10
14
|
// 配色(与原 launcher.sh 一致)
|
|
11
15
|
const C = "\x1b[38;2;0;210;255m"; // CYAN #00D2FF 边框 + 底部字母
|
|
12
16
|
const B = "\x1b[38;2;66;135;245m"; // BLUE #4287F5 顶部字母
|
|
@@ -21,7 +25,7 @@ const R = "\x1b[0m"; // RESET
|
|
|
21
25
|
// 4) 平面字体(最后兜底):按宽度降序,每个字体也是先 AGENT 再 ARONA
|
|
22
26
|
//
|
|
23
27
|
// 列数估算已留出边框 2 格 + 缩进 2 格 + 内边距 6 格(即 width + 10)
|
|
24
|
-
const VARIANTS: { text: string; font:
|
|
28
|
+
const VARIANTS: { text: string; font: FigletFont }[] = [
|
|
25
29
|
// —— ANSI Shadow 绝对优先 ——
|
|
26
30
|
{ text: "ARONA AGENT", font: "ANSI Shadow" }, // ~101 列
|
|
27
31
|
{ text: "ARONA", font: "ANSI Shadow" }, // ~53 列
|
package/src/repl.ts
CHANGED
|
@@ -77,6 +77,10 @@ export class Repl {
|
|
|
77
77
|
// 斜杠命令菜单(渲染与状态机封装在 SlashMenu 中)
|
|
78
78
|
private menu = new SlashMenu();
|
|
79
79
|
private menuKeyListener: ((s: any, k: any) => void) | null = null;
|
|
80
|
+
// 本次按键中被菜单消费、需要拦截 readline 内置处理的键名(up/down 的历史回溯)。
|
|
81
|
+
// 注意 readline 有自己的 stdin "keypress" 监听器,prependListener 只保证我们先执行,
|
|
82
|
+
// 无法阻止它处理同一按键——因此 start() 里包装 rl._ttyWrite,读到该字段时短路内置逻辑。
|
|
83
|
+
private swallowedKeyName: string | null = null;
|
|
80
84
|
// 本地快照式 undo/redo 管理器(不依赖 git)
|
|
81
85
|
private undoManager: UndoManager;
|
|
82
86
|
|
|
@@ -170,7 +174,7 @@ export class Repl {
|
|
|
170
174
|
(this.rl as any).line = "";
|
|
171
175
|
(this.rl as any).cursor = 0;
|
|
172
176
|
process.stdout.write(chalk.yellow("\n[aborted]\n"));
|
|
173
|
-
// 标记刚中断过,下一次 Ctrl+C
|
|
177
|
+
// 标记刚中断过,下一次 Ctrl+C 直接退出
|
|
174
178
|
this.sigintCount = 1;
|
|
175
179
|
if (this.sigintTimer) clearTimeout(this.sigintTimer);
|
|
176
180
|
this.sigintTimer = setTimeout(() => {
|
|
@@ -528,7 +532,23 @@ export class Repl {
|
|
|
528
532
|
(process.stdin as any).setRawMode?.(true);
|
|
529
533
|
}
|
|
530
534
|
|
|
531
|
-
//
|
|
535
|
+
// 包装 readline 内部的 _ttyWrite:被菜单消费的导航键(见 swallowedKeyName,由
|
|
536
|
+
// menuKeyListener 置位)在此短路,readline 不再做历史回溯/移动光标。此前菜单打开时
|
|
537
|
+
// 按 ↑ 会同时触发 menu.move 和内置历史回溯——输入行直接被"上次执行的命令"覆盖。
|
|
538
|
+
// 每次调用后立即清标志:一个 keypress 事件只吞一次。
|
|
539
|
+
// _ttyWrite 是 readline 私有 API,带 typeof 守卫:不可用时优雅降级为旧行为。
|
|
540
|
+
const rlAny = this.rl as any;
|
|
541
|
+
if (typeof rlAny._ttyWrite === "function") {
|
|
542
|
+
const origTtyWrite = rlAny._ttyWrite;
|
|
543
|
+
rlAny._ttyWrite = (s: string, key: any) => {
|
|
544
|
+
const swallowed = !!key?.name && this.swallowedKeyName === key.name;
|
|
545
|
+
this.swallowedKeyName = null;
|
|
546
|
+
if (swallowed) return;
|
|
547
|
+
origTtyWrite.call(this.rl, s, key);
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// STT 热键为全局监听(pynput),见 startHotkeyHook()
|
|
532
552
|
|
|
533
553
|
this.rl.prompt();
|
|
534
554
|
|
|
@@ -540,8 +560,15 @@ export class Repl {
|
|
|
540
560
|
if (this.menu.isOpen()) {
|
|
541
561
|
if (key.name === "escape") { this.menu.close(this.rl); return; }
|
|
542
562
|
if (key.name === "up" || key.name === "down") {
|
|
543
|
-
this.menu.move(key.name === "up" ? -1 : 1, this.rl)
|
|
544
|
-
|
|
563
|
+
if (this.menu.move(key.name === "up" ? -1 : 1, this.rl)) {
|
|
564
|
+
// 菜单消费了这次导航:吞掉,readline 不再做历史回溯/移动输入光标
|
|
565
|
+
this.swallowedKeyName = key.name;
|
|
566
|
+
} else {
|
|
567
|
+
// 菜单顶部按 ↑(仅此边界):放行给 readline 做历史回溯——读取上次执行的命令。
|
|
568
|
+
// 缓冲区被改写后在微任务里刷新菜单(不再是斜杠前缀/完整命令名时自动关闭)。
|
|
569
|
+
queueMicrotask(() => this.menu.refresh(this.rl));
|
|
570
|
+
}
|
|
571
|
+
return;
|
|
545
572
|
}
|
|
546
573
|
// 行末再按 →:把选中指令填入输入框但不执行(用户可随后补参数/编辑,
|
|
547
574
|
// 再自己 Enter)。光标不在行末时落到 readline 正常移动光标。
|
|
@@ -632,7 +659,7 @@ export class Repl {
|
|
|
632
659
|
}
|
|
633
660
|
|
|
634
661
|
private async processInput(input: string) {
|
|
635
|
-
// 桌宠手势(摸头/dizzy
|
|
662
|
+
// 桌宠手势(摸头/dizzy)落到主 Agent 发送边界注入(gesture_context.ts),
|
|
636
663
|
// 不进 state.messages → 子 Agent 复制主 session 历史时看不到、会话命名/存储零污染。
|
|
637
664
|
// takeGesture 消费即清空由发送边界扩展完成,只注入最近一次。
|
|
638
665
|
// 展开 @文件 / !命令 后走完整回合生命周期
|
|
@@ -771,7 +798,10 @@ export class Repl {
|
|
|
771
798
|
|
|
772
799
|
// 子 Agent 的触发消息:固定短句(上下文在复制来的全量日志里,这里只负责"叫醒"它发言)
|
|
773
800
|
const promptText = isSub
|
|
774
|
-
? t(
|
|
801
|
+
? t(
|
|
802
|
+
`(你是${getAgentLabel(agentId)}。现在轮到你发言——保持你自己的身份和语气,不要扮演或模仿其他角色。请简短发言。)`,
|
|
803
|
+
`(You are ${getAgentLabel(agentId)}. It's your turn — stay in your own character and voice; do not play or mimic another character. Speak briefly.)`,
|
|
804
|
+
)
|
|
775
805
|
: input;
|
|
776
806
|
|
|
777
807
|
try {
|
|
@@ -855,13 +885,16 @@ export class Repl {
|
|
|
855
885
|
console.warn(chalk.yellow(t(`撤销快照记录失败:${err instanceof Error ? err.message : err}`, `Failed to record undo snapshot: ${err instanceof Error ? err.message : err}`)));
|
|
856
886
|
}
|
|
857
887
|
this.isProcessing = false;
|
|
858
|
-
// 回合结束:恢复桌宠到 idle
|
|
859
|
-
// - TTS 启用 + 无残余播放 → 立即 reset;有残余由 onIdle(play_end) 兜底
|
|
860
|
-
// - TTS 禁用(--no-voice / 缺音色 / 切到无音色角色)→ 5s 后再撤表情和气泡
|
|
861
888
|
this.turnEnded = true;
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
889
|
+
// 回合结束:恢复桌宠到 idle + 气泡兜底隐藏。
|
|
890
|
+
// 以"本回合实际是否还有待播内容"(isPending)为准,而非 TTS 配置开关——
|
|
891
|
+
// 否则 TTS 开启但整段 ≥50 字被 tts_stream.endTurn 整段跳过时,既没有 play_end
|
|
892
|
+
// 触发 onIdle 隐藏,又不走 5s 兜底定时器,气泡会永久停留在屏幕上。
|
|
893
|
+
// - 无待播(含 --no-voice / 缺音色 / ≥50 字整段静音):立即 reset + 5s 后隐藏气泡
|
|
894
|
+
// - 有残余播放:由队列排空后的 onIdle(play_end) 隐藏气泡并 reset
|
|
895
|
+
// (onIdle 已先触发的场景下,这里的定时器重复隐藏一次已隐藏的气泡,无害)
|
|
896
|
+
if (!this.ttsStream.isPending) {
|
|
897
|
+
pet.reset();
|
|
865
898
|
this.scheduleBubbleHide();
|
|
866
899
|
}
|
|
867
900
|
// 若被 Esc/Ctrl+C 中断,中断处理已重绘提示符,跳过重复 prompt
|
package/src/setup.ts
CHANGED
|
@@ -11,10 +11,10 @@ import {
|
|
|
11
11
|
type TtsProvider,
|
|
12
12
|
} from "./config.ts";
|
|
13
13
|
import { AGENT_IDS, getAgentLabel, type AgentId } from "./agent_registry.ts";
|
|
14
|
-
import { VOICE_AUDIO, cloneVoice, setVoiceId, getMissingAgents, hasVoice, getGptSovitsVoice, setGptSovitsVoice
|
|
14
|
+
import { VOICE_AUDIO, cloneVoice, setVoiceId, getMissingAgents, hasVoice, DEMO_PRECLONED_AGENTS, getGptSovitsVoice, setGptSovitsVoice } from "./voices.ts";
|
|
15
15
|
import { normalizeGptSovitsConfig } from "./tts_provider.ts";
|
|
16
16
|
import { installGptSovitsDeps } from "./gpt_sovits_local.ts";
|
|
17
|
-
import { multiSelect } from "./tui_select.ts";
|
|
17
|
+
import { multiSelect, lockExisting } from "./tui_select.ts";
|
|
18
18
|
import { t, getLang, setLang } from "./locale.ts";
|
|
19
19
|
|
|
20
20
|
interface Settings {
|
|
@@ -46,6 +46,8 @@ interface Settings {
|
|
|
46
46
|
cuaApiKey?: string;
|
|
47
47
|
pythonPath?: string;
|
|
48
48
|
mcpServers?: Record<string, unknown>;
|
|
49
|
+
/** 启动时是否从 ~/.agents/skills 补全缺失 Skill(默认 true);保留手写参数,勿覆盖 */
|
|
50
|
+
autoLoadSkills?: boolean;
|
|
49
51
|
/** 用户手动维护;setup 向导只读不写。true 时启用演示模式。 */
|
|
50
52
|
demoMode?: boolean;
|
|
51
53
|
}
|
|
@@ -122,11 +124,11 @@ async function main() {
|
|
|
122
124
|
const tried = `"${existing.pythonPath || "python3"}" 与 "python"`;
|
|
123
125
|
if (pyCheck.version === "not found") {
|
|
124
126
|
console.log(chalk.red(t(`\n✗ 未找到可用的 Python(已尝试 ${tried})。`, `\n✗ No usable Python found (tried ${tried}).`)));
|
|
125
|
-
console.log(chalk.cyan(t(" ARONA 需要 Python 3.12 或 3.13
|
|
127
|
+
console.log(chalk.cyan(t(" ARONA 需要 Python 3.12 或 3.13。", " ARONA requires Python 3.12 or 3.13.")));
|
|
126
128
|
console.log(chalk.gray(t(" 请安装 Python 后重新运行 arona setup。", " Install Python and run arona setup again.")));
|
|
127
129
|
} else {
|
|
128
130
|
console.log(chalk.red(t(`\n✗ Python 版本不兼容:${pyCheck.version}`, `\n✗ Incompatible Python version: ${pyCheck.version}`)));
|
|
129
|
-
console.log(chalk.cyan(t(" ARONA 需要 Python 3.12 或 3.13
|
|
131
|
+
console.log(chalk.cyan(t(" ARONA 需要 Python 3.12 或 3.13。", " ARONA requires Python 3.12 or 3.13.")));
|
|
130
132
|
console.log(chalk.gray(t(" 请安装正确版本后重新运行 arona setup。", " Install the correct version and run arona setup again.")));
|
|
131
133
|
}
|
|
132
134
|
process.exit(1);
|
|
@@ -160,8 +162,8 @@ async function main() {
|
|
|
160
162
|
const detected = getLang();
|
|
161
163
|
console.log(chalk.cyan(t(` 检测到系统语言:${detected === "en" ? "英文 (en)" : "中文 (zh)"}`, ` Detected system language: ${detected === "en" ? "English (en)" : "Chinese (zh)"}`)));
|
|
162
164
|
const langInput = (await ask(t(
|
|
163
|
-
" 语言选择 [auto/en/zh]
|
|
164
|
-
" Language [auto/en/zh]
|
|
165
|
+
" 语言选择 [auto/en/zh]: ",
|
|
166
|
+
" Language [auto/en/zh]: ",
|
|
165
167
|
))).toLowerCase();
|
|
166
168
|
let langSetting: "auto" | "zh" | "en" = "auto";
|
|
167
169
|
if (langInput === "en") { langSetting = "en"; setLang("en"); }
|
|
@@ -206,7 +208,7 @@ async function main() {
|
|
|
206
208
|
// Step 2: 语音配置
|
|
207
209
|
// ============================================================
|
|
208
210
|
console.log(chalk.bold.cyan(t("\nStep 2: 语音配置\n", "\nStep 2: Voice Configuration\n")));
|
|
209
|
-
console.log(chalk.cyan(t(" 百炼 API Key
|
|
211
|
+
console.log(chalk.cyan(t(" 百炼 API Key(可选)", " Dashscope API Key (optional)")));
|
|
210
212
|
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")));
|
|
211
213
|
|
|
212
214
|
const existingTtsKey = existing.ttsApiKey || "";
|
|
@@ -218,7 +220,7 @@ async function main() {
|
|
|
218
220
|
|
|
219
221
|
console.log(chalk.bold.cyan(t("\n TTS Provider 菜单\n", "\n TTS Provider Menu\n")));
|
|
220
222
|
const providerOptions = [
|
|
221
|
-
{ id: "aliyun", label: t("
|
|
223
|
+
{ id: "aliyun", label: t("阿里云百炼", "Dashscope") },
|
|
222
224
|
{ id: "gpt-sovits", label: t("GPT-SoVITS", "GPT-SoVITS") },
|
|
223
225
|
];
|
|
224
226
|
// 二选一使用 TUI 单选菜单(不要求键盘输入 1/2)
|
|
@@ -247,7 +249,7 @@ async function main() {
|
|
|
247
249
|
console.log(chalk.bold.cyan(t("\n GPT-SoVITS 配置\n", "\n GPT-SoVITS Configuration\n")));
|
|
248
250
|
// readline 已在上方 TUI 前关闭;为后续 TUI 选择保持 stdin 空闲
|
|
249
251
|
const deployOptions = [
|
|
250
|
-
{ id: "cloud", label: t("云端 API
|
|
252
|
+
{ id: "cloud", label: t("云端 API", "Cloud API") },
|
|
251
253
|
{ id: "local", label: t("本地模型", "Local model") },
|
|
252
254
|
];
|
|
253
255
|
const deploySelected = await multiSelect(
|
|
@@ -418,7 +420,7 @@ async function main() {
|
|
|
418
420
|
` text_lang [${gptSovitsConfig.textLang}] (auto/zh/en/ja/yue/ko): `,
|
|
419
421
|
));
|
|
420
422
|
const textLang = textLangInput.trim() || gptSovitsConfig.textLang;
|
|
421
|
-
// prompt_lang
|
|
423
|
+
// prompt_lang 按每角色参考音频文字内容自动判断(默认素材固定 zh,见 tts_provider.detectPromptLang)
|
|
422
424
|
gptSovitsConfig = {
|
|
423
425
|
...gptSovitsConfig,
|
|
424
426
|
mode,
|
|
@@ -433,9 +435,16 @@ async function main() {
|
|
|
433
435
|
};
|
|
434
436
|
rl2.close();
|
|
435
437
|
|
|
436
|
-
//
|
|
437
|
-
|
|
438
|
-
const
|
|
438
|
+
// 多选要配置音色的角色:已配置者经 lockExisting 锁定为已克隆。
|
|
439
|
+
// 演示模式:硬编码全未克隆,路径信息直接丢弃、不写 voices.json。
|
|
440
|
+
const configured: AgentId[] = demoMode ? [] : AGENT_IDS.filter((id) => getGptSovitsVoice(id));
|
|
441
|
+
const options = lockExisting(
|
|
442
|
+
AGENT_IDS.map((id) => ({
|
|
443
|
+
id,
|
|
444
|
+
label: configured.includes(id) ? `${getAgentLabel(id)}${t("(已克隆)", " (cloned)")}` : getAgentLabel(id),
|
|
445
|
+
})),
|
|
446
|
+
configured,
|
|
447
|
+
);
|
|
439
448
|
const selected = await multiSelect(
|
|
440
449
|
t("选择要配置 GPT-SoVITS 音色的角色", "Select characters to configure GPT-SoVITS voices"),
|
|
441
450
|
options,
|
|
@@ -468,24 +477,37 @@ async function main() {
|
|
|
468
477
|
if (!selected.has(id)) continue;
|
|
469
478
|
const prev = getGptSovitsVoice(id) || {};
|
|
470
479
|
console.log(chalk.bold.cyan(t(`\n ${getAgentLabel(id)} 音色配置\n`, `\n ${getAgentLabel(id)} voice config\n`)));
|
|
480
|
+
// GPT/SoVITS 权重必填:无既有值默认 [ N/A ],空输入黄色提示后重新询问。
|
|
471
481
|
const prevCkpt = prev.gptWeightsPath || "";
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
482
|
+
let ckpt = prevCkpt;
|
|
483
|
+
for (;;) {
|
|
484
|
+
const ckptInput = (await ask3(t(
|
|
485
|
+
ckpt
|
|
486
|
+
? ` GPT 权重 .ckpt 路径 [${ckpt}]: `
|
|
487
|
+
: " GPT 权重 .ckpt 路径 [ N/A ]: ",
|
|
488
|
+
ckpt
|
|
489
|
+
? ` GPT weights .ckpt path [${ckpt}]: `
|
|
490
|
+
: " GPT weights .ckpt path [ N/A ]: ",
|
|
491
|
+
))).trim();
|
|
492
|
+
if (ckptInput) { ckpt = ckptInput; break; }
|
|
493
|
+
if (prevCkpt) break;
|
|
494
|
+
console.log(chalk.yellow(t(" 请输入路径!", " Please enter a path!")));
|
|
495
|
+
}
|
|
480
496
|
const prevPth = prev.sovitsWeightsPath || "";
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
497
|
+
let pth = prevPth;
|
|
498
|
+
for (;;) {
|
|
499
|
+
const pthInput = (await ask3(t(
|
|
500
|
+
pth
|
|
501
|
+
? ` SoVITS 权重 .pth 路径 [${pth}]: `
|
|
502
|
+
: " SoVITS 权重 .pth 路径 [ N/A ]: ",
|
|
503
|
+
pth
|
|
504
|
+
? ` SoVITS weights .pth path [${pth}]: `
|
|
505
|
+
: " SoVITS weights .pth path [ N/A ]: ",
|
|
506
|
+
))).trim();
|
|
507
|
+
if (pthInput) { pth = pthInput; break; }
|
|
508
|
+
if (prevPth) break;
|
|
509
|
+
console.log(chalk.yellow(t(" 请输入路径!", " Please enter a path!")));
|
|
510
|
+
}
|
|
489
511
|
const prevRef = prev.refAudioPath || "";
|
|
490
512
|
// cloud 模式 ref 必填(无 ref 则该角色 isTtsEnabledFor=false 静音且无解释);循环重询直至非空
|
|
491
513
|
let ref = prevRef;
|
|
@@ -506,8 +528,8 @@ async function main() {
|
|
|
506
528
|
if (prevRef) break;
|
|
507
529
|
if (mode !== "cloud") break;
|
|
508
530
|
console.log(chalk.yellow(t(
|
|
509
|
-
` ref_audio_path
|
|
510
|
-
` ref_audio_path cannot be empty
|
|
531
|
+
` ref_audio_path 不能为空。`,
|
|
532
|
+
` ref_audio_path cannot be empty.`,
|
|
511
533
|
)));
|
|
512
534
|
}
|
|
513
535
|
const prevText = prev.promptText || "";
|
|
@@ -515,20 +537,19 @@ async function main() {
|
|
|
515
537
|
prevText
|
|
516
538
|
? ` 示例音频文字内容 prompt_text [${prevText}]: `
|
|
517
539
|
: mode === "cloud"
|
|
518
|
-
? " 示例音频文字内容 prompt_text [
|
|
519
|
-
:
|
|
540
|
+
? " 示例音频文字内容 prompt_text [文字/本地txt路径/URL]: "
|
|
541
|
+
: ` 示例音频文字内容 prompt_text [assets/blue-archive/${id}/voice_text.txt]: `,
|
|
520
542
|
prevText
|
|
521
543
|
? ` Reference audio text prompt_text [${prevText}]: `
|
|
522
544
|
: mode === "cloud"
|
|
523
|
-
? " Reference audio text prompt_text [
|
|
524
|
-
:
|
|
545
|
+
? " Reference audio text prompt_text [text/local txt path/URL]: "
|
|
546
|
+
: ` Reference audio text prompt_text [assets/blue-archive/${id}/voice_text.txt]: `,
|
|
525
547
|
))).trim() || prevText;
|
|
526
548
|
// 每角色音色写 voices.json#gpt-sovits(与百炼 voice_id 共存,不写 settings.json)
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
if (configured.includes(id) && !selected.has(id)) deleteGptSovitsVoice(id);
|
|
549
|
+
// 演示模式:路径信息直接丢弃,不写 voices.json。
|
|
550
|
+
if (!demoMode) {
|
|
551
|
+
setGptSovitsVoice(id, { gptWeightsPath: ckpt, sovitsWeightsPath: pth, refAudioPath: ref, promptText: refText });
|
|
552
|
+
}
|
|
532
553
|
}
|
|
533
554
|
rl3.close();
|
|
534
555
|
}
|
|
@@ -588,22 +609,26 @@ async function main() {
|
|
|
588
609
|
// 多选 TUI 独占 stdin raw mode(此后不再用 rl.question;rl 已在 TTS Provider TUI 前关闭)。
|
|
589
610
|
|
|
590
611
|
// 已有音色的角色锁定 [*](迁移已在模块加载时完成,此处读到的 voices.json 已是新格式)。
|
|
591
|
-
|
|
612
|
+
// 演示模式:必然显示 TUI,预标记角色锁定为已克隆,其余未克隆。
|
|
613
|
+
const missing = demoMode ? AGENT_IDS : getMissingAgents();
|
|
592
614
|
if (missing.length === 0) {
|
|
593
615
|
console.log(chalk.green(t(
|
|
594
616
|
" 所有角色已有音色,无需克隆。如需重新克隆请运行 arona voice add <角色名>。",
|
|
595
617
|
" All characters already have voices. Use `arona voice add <name>` to re-clone.",
|
|
596
618
|
)));
|
|
597
619
|
} else {
|
|
598
|
-
const
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
620
|
+
const clonedIds = demoMode ? DEMO_PRECLONED_AGENTS : AGENT_IDS.filter((id) => hasVoice(id));
|
|
621
|
+
const options = lockExisting(
|
|
622
|
+
AGENT_IDS.map((id) => ({
|
|
623
|
+
id,
|
|
624
|
+
label: clonedIds.includes(id) ? `${getAgentLabel(id)}${t("(已克隆)", " (cloned)")}` : getAgentLabel(id),
|
|
625
|
+
})),
|
|
626
|
+
clonedIds,
|
|
627
|
+
);
|
|
603
628
|
const selected = await multiSelect(
|
|
604
629
|
t("选择要克隆音色的角色", "Select characters to clone voices."),
|
|
605
630
|
options,
|
|
606
|
-
new Set<string>(), //
|
|
631
|
+
new Set<string>(), // 已有音色者经 lockExisting 锁定,未克隆者默认 [ ]
|
|
607
632
|
t(
|
|
608
633
|
" ↑/↓ 切换 · 空格选中 [*] · 回车克隆 · Esc 取消",
|
|
609
634
|
" ↑/↓ move · Space select [*] · Enter clone · Esc cancel",
|
|
@@ -618,7 +643,7 @@ async function main() {
|
|
|
618
643
|
)));
|
|
619
644
|
return;
|
|
620
645
|
} else if (selected.size === 0) {
|
|
621
|
-
console.log(chalk.cyan(t("
|
|
646
|
+
console.log(chalk.cyan(t(" 未选择任何角色,跳过音色克隆。", " No character selected, skipping voice cloning.")));
|
|
622
647
|
} else {
|
|
623
648
|
const model = existing.ttsModel || "qwen-audio-3.0-tts-plus";
|
|
624
649
|
for (const id of AGENT_IDS) {
|
|
@@ -629,13 +654,13 @@ async function main() {
|
|
|
629
654
|
continue;
|
|
630
655
|
}
|
|
631
656
|
if (demoMode) {
|
|
632
|
-
// 演示模式:不调用 voice_clone.py、不写 voices.json
|
|
633
|
-
console.log(chalk.cyan(t(` 正在克隆 ${getAgentLabel(id)} 的音色...`, `Cloning
|
|
657
|
+
// 演示模式:不调用 voice_clone.py、不写 voices.json,5s 模拟后显示成功。
|
|
658
|
+
console.log(chalk.cyan(t(` 正在克隆 ${getAgentLabel(id)} 的音色...`, ` Cloning ${getAgentLabel(id)}'s voice...`)));
|
|
634
659
|
await new Promise((r) => setTimeout(r, 5000));
|
|
635
660
|
console.log(chalk.green(t(` ✓ ${getAgentLabel(id)} 音色克隆成功`, ` ✓ ${getAgentLabel(id)} voice cloned.`)));
|
|
636
661
|
continue;
|
|
637
662
|
}
|
|
638
|
-
console.log(chalk.cyan(t(` 正在克隆 ${getAgentLabel(id)}
|
|
663
|
+
console.log(chalk.cyan(t(` 正在克隆 ${getAgentLabel(id)} 的音色...`, ` Cloning ${getAgentLabel(id)}'s voice...`)));
|
|
639
664
|
try {
|
|
640
665
|
const voiceId = await cloneVoice(id, ttsApiKey, model);
|
|
641
666
|
setVoiceId(id, voiceId);
|
package/src/slash_menu.ts
CHANGED
|
@@ -171,15 +171,21 @@ export class SlashMenu {
|
|
|
171
171
|
this.redraw(rl);
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
/**
|
|
175
|
-
|
|
176
|
-
|
|
174
|
+
/**
|
|
175
|
+
* ↑/↓ 导航。返回是否移动了选中项:
|
|
176
|
+
* - ↓ 到底环绕回顶部(保持循环导航);
|
|
177
|
+
* - ↑ 在顶部(selected === 0)不环绕,返回 false 作为"到达边界"信号,
|
|
178
|
+
* 由 repl 放行给 readline 做历史回溯(读取上次执行的命令)。
|
|
179
|
+
*/
|
|
180
|
+
move(direction: -1 | 1, rl: Interface): boolean {
|
|
181
|
+
if (!this.visible || this.matches.length === 0) return false;
|
|
182
|
+
if (direction === -1 && this.selected === 0) return false; // 顶部按 ↑:交给历史回溯
|
|
177
183
|
let next = this.selected + direction;
|
|
178
|
-
if (next < 0) next = this.matches.length - 1;
|
|
179
184
|
if (next >= this.matches.length) next = 0;
|
|
180
185
|
this.selected = next;
|
|
181
186
|
this.clampScroll();
|
|
182
187
|
this.redraw(rl);
|
|
188
|
+
return true;
|
|
183
189
|
}
|
|
184
190
|
|
|
185
191
|
/**
|
package/src/slash_registry.ts
CHANGED
|
@@ -26,7 +26,7 @@ export interface SlashCommandSpec {
|
|
|
26
26
|
export const SLASH_COMMANDS: SlashCommandSpec[] = [
|
|
27
27
|
{ name: "help", aliases: ["?"], description: t("显示命令列表", "Show command list") },
|
|
28
28
|
{ name: "exit", aliases: ["quit", "q"], description: t("退出", "Exit") },
|
|
29
|
-
{ name: "new", aliases: ["clear"], description: t("
|
|
29
|
+
{ name: "new", aliases: ["clear"], description: t("开始新会话", "Start a new session") },
|
|
30
30
|
{ name: "resume", aliases: ["r"], description: t("上下键选择并恢复一个已保存的会话", "Pick and resume a saved session with arrow keys"), interactive: true },
|
|
31
31
|
{ name: "export", description: t("导出当前会话为 Markdown", "Export current session as Markdown") },
|
|
32
32
|
{ name: "thinking", description: t("开关推理块显示", "Toggle reasoning block display") },
|
|
@@ -34,7 +34,7 @@ export const SLASH_COMMANDS: SlashCommandSpec[] = [
|
|
|
34
34
|
{ name: "compact", description: t("压缩上下文", "Compact context") },
|
|
35
35
|
{ name: "tts", description: t("开关文字转语音", "Toggle text-to-speech") },
|
|
36
36
|
{ name: "stt", description: t(`开关语音转文字`, `Toggle speech-to-text`) },
|
|
37
|
-
{ name: "skill", description: t("
|
|
37
|
+
{ name: "skill", description: t("调用技能", "Invoke a skill") },
|
|
38
38
|
{ name: "mcp", description: t("管理 MCP 服务器和工具", "Manage MCP servers and tools") },
|
|
39
39
|
{ name: "change-agent", description: t("切换主 Agent + 多选子 Agent", "Switch main agent + multi-select sub agents"), interactive: true },
|
|
40
40
|
{ name: "undo", description: t("撤销上一个回合的全部文件改动", "Undo the previous turn's file changes") },
|
package/src/tools/skill_tools.ts
CHANGED
|
@@ -20,8 +20,8 @@ function loadSkillsTool(loader: DefaultResourceLoader): ToolDefinition {
|
|
|
20
20
|
name: "load_skills",
|
|
21
21
|
label: t("加载技能", "Load Skills"),
|
|
22
22
|
description: t(
|
|
23
|
-
"列出或加载技能。**不传 names 时**:返回所有可用技能的 name + description
|
|
24
|
-
"List or load skills. **Without names**: returns the name + description list of all available skills
|
|
23
|
+
"列出或加载技能。**不传 names 时**:返回所有可用技能的 name + description 列表。**传 names 时**:返回对应技能的 SKILL.md 全文(单个字符串或字符串数组),加载后遵循其中指令执行;未找到的技能在结果中列出。单次加载不超过 5 个。",
|
|
24
|
+
"List or load skills. **Without names**: returns the name + description list of all available skills. **With names**: returns the full SKILL.md content of the requested skills (a single string or an array of strings); follow the instructions inside after loading; missing skills are reported in the result. Load at most 5 per call.",
|
|
25
25
|
),
|
|
26
26
|
parameters: Type.Object({
|
|
27
27
|
names: Type.Optional(
|
package/src/tui_select.ts
CHANGED
|
@@ -8,10 +8,22 @@ import { t } from "./locale.ts";
|
|
|
8
8
|
export interface SelectOption {
|
|
9
9
|
id: string;
|
|
10
10
|
label: string;
|
|
11
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* 锁定(已存在/已配置的项)。
|
|
13
|
+
* 请统一用 lockExisting() 按 existing 集合生成。
|
|
14
|
+
*/
|
|
12
15
|
locked?: boolean;
|
|
13
16
|
}
|
|
14
17
|
|
|
18
|
+
/**
|
|
19
|
+
* 把"已存在/已配置"的项标记为锁定。调用方给出 existing 集合即可。
|
|
20
|
+
* 已显式 locked:true 的项保持锁定;不在 existing 里的项原样返回。
|
|
21
|
+
*/
|
|
22
|
+
export function lockExisting<T extends SelectOption>(options: readonly T[], existing: Iterable<string>): T[] {
|
|
23
|
+
const set = new Set(existing);
|
|
24
|
+
return options.map((o) => (o.locked || set.has(o.id) ? { ...o, locked: true } : o));
|
|
25
|
+
}
|
|
26
|
+
|
|
15
27
|
/** 计算一个字符串去除 ANSI 转义后的可见宽度(CJK/全角算 2,控制符算 0)。 */
|
|
16
28
|
function visibleWidth(s: string): number {
|
|
17
29
|
const stripped = s.replace(/\x1b(?:\[[0-9;?]*[A-Za-z]|[0-9])/g, "");
|
package/src/undo.ts
CHANGED
|
@@ -135,8 +135,8 @@ export class UndoManager {
|
|
|
135
135
|
|
|
136
136
|
/**
|
|
137
137
|
* 启动时调用:加载持久化状态(如有),并在后台异步刷新基线为当前工作目录真实状态。
|
|
138
|
-
*
|
|
139
|
-
*
|
|
138
|
+
* 基线扫描为后台异步 + 有界(MAX_SCAN_FILES/深度/系统目录):在 ~/、/ 等巨型目录下
|
|
139
|
+
* 同步全盘递归会卡死启动。
|
|
140
140
|
*/
|
|
141
141
|
load(): void {
|
|
142
142
|
try {
|
|
@@ -377,7 +377,7 @@ export class UndoManager {
|
|
|
377
377
|
|
|
378
378
|
private hashLargeFile(abs: string): string {
|
|
379
379
|
// 只读 4KB 头 + 4KB 尾 + size 当 hash 近似(够用于检测删除)
|
|
380
|
-
// 用 fd 流式读取,避免把整个大文件读入内存
|
|
380
|
+
// 用 fd 流式读取,避免把整个大文件读入内存
|
|
381
381
|
let fd: number | null = null;
|
|
382
382
|
try {
|
|
383
383
|
const stat = statSync(abs);
|
|
@@ -436,8 +436,7 @@ export class UndoManager {
|
|
|
436
436
|
|
|
437
437
|
/**
|
|
438
438
|
* 把 before / after 两个 snapshot 对比,产出 changed files 的 diff。
|
|
439
|
-
* content 直接取自 snapshot
|
|
440
|
-
* 因为 afterTurn 调用时,文件已是 after 状态,回读会污染 before 内容。
|
|
439
|
+
* content 直接取自 snapshot(takeSnapshot 已读出)。
|
|
441
440
|
* 无变化返回 null。
|
|
442
441
|
*/
|
|
443
442
|
private computeDiff(
|
package/src/voice_cli.ts
CHANGED
|
@@ -10,8 +10,8 @@ import { existsSync, readFileSync } from "fs";
|
|
|
10
10
|
import chalk from "chalk";
|
|
11
11
|
import { SETTINGS_FILE, verbose } from "./config.ts";
|
|
12
12
|
import { AGENT_IDS, getAgentLabel, type AgentId } from "./agent_registry.ts";
|
|
13
|
-
import { cloneVoice, hasVoice, setVoiceId, getGptSovitsVoice, setGptSovitsVoice
|
|
14
|
-
import { multiSelect } from "./tui_select.ts";
|
|
13
|
+
import { cloneVoice, hasVoice, setVoiceId, DEMO_PRECLONED_AGENTS, getGptSovitsVoice, setGptSovitsVoice } from "./voices.ts";
|
|
14
|
+
import { multiSelect, lockExisting } from "./tui_select.ts";
|
|
15
15
|
import { t } from "./locale.ts";
|
|
16
16
|
|
|
17
17
|
function readSettings(): { ttsApiKey: string; ttsModel: string; ttsProvider: string } {
|
|
@@ -52,8 +52,9 @@ function isValidAgentId(id: string): id is AgentId {
|
|
|
52
52
|
* 配置 GPT-SoVITS 每角色音色(gptWeightsPath/sovitsWeightsPath/refAudioPath/promptText)。
|
|
53
53
|
* 写 voices.json#gpt-sovits(read-modify-write,与 voices.json#aliyun 的百炼音色共存)。
|
|
54
54
|
* 带角色名 = 只配置该角色;无参 = TUI 多选(原已配置角色默认 [*],取消勾选 = 删除其配置)。
|
|
55
|
+
* 演示模式:TUI 默认显示所有角色都为未克隆,记录的路径信息直接丢弃、不写 voices.json。
|
|
55
56
|
*/
|
|
56
|
-
async function configureGptSovitsVoice(name
|
|
57
|
+
async function configureGptSovitsVoice(name: string | undefined, demoMode: boolean): Promise<void> {
|
|
57
58
|
const targets: AgentId[] = [];
|
|
58
59
|
if (name) {
|
|
59
60
|
if (!isValidAgentId(name)) {
|
|
@@ -65,8 +66,16 @@ async function configureGptSovitsVoice(name?: string): Promise<void> {
|
|
|
65
66
|
}
|
|
66
67
|
targets.push(name);
|
|
67
68
|
} else {
|
|
68
|
-
|
|
69
|
-
|
|
69
|
+
// 已配置者经 lockExisting 锁定为已克隆。
|
|
70
|
+
// 演示模式:硬编码全未克隆。
|
|
71
|
+
const configured: AgentId[] = demoMode ? [] : AGENT_IDS.filter((id) => getGptSovitsVoice(id));
|
|
72
|
+
const options = lockExisting(
|
|
73
|
+
AGENT_IDS.map((id) => ({
|
|
74
|
+
id,
|
|
75
|
+
label: configured.includes(id) ? `${getAgentLabel(id)}${t("(已克隆)", " (cloned)")}` : getAgentLabel(id),
|
|
76
|
+
})),
|
|
77
|
+
configured,
|
|
78
|
+
);
|
|
70
79
|
const selected = await multiSelect(
|
|
71
80
|
t("选择要配置 GPT-SoVITS 音色的角色", "Select characters to configure GPT-SoVITS voices"),
|
|
72
81
|
options,
|
|
@@ -81,10 +90,6 @@ async function configureGptSovitsVoice(name?: string): Promise<void> {
|
|
|
81
90
|
return;
|
|
82
91
|
}
|
|
83
92
|
targets.push(...AGENT_IDS.filter((id) => selected.has(id)));
|
|
84
|
-
// 未勾选的原已配置角色 → 删除其配置(与 setup 语义一致)
|
|
85
|
-
for (const id of AGENT_IDS) {
|
|
86
|
-
if (configured.includes(id) && !selected.has(id)) deleteGptSovitsVoice(id);
|
|
87
|
-
}
|
|
88
93
|
}
|
|
89
94
|
|
|
90
95
|
if (targets.length === 0) {
|
|
@@ -109,16 +114,29 @@ async function configureGptSovitsVoice(name?: string): Promise<void> {
|
|
|
109
114
|
`\n ${getAgentLabel(id)} GPT-SoVITS 音色配置\n`,
|
|
110
115
|
`\n ${getAgentLabel(id)} GPT-SoVITS voice config\n`,
|
|
111
116
|
)));
|
|
117
|
+
// GPT/SoVITS 权重必填:无既有值默认 [ N/A ],空输入黄色提示后重新询问。
|
|
112
118
|
const prevCkpt = prev.gptWeightsPath || "";
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
119
|
+
let ckpt = prevCkpt;
|
|
120
|
+
for (;;) {
|
|
121
|
+
const ckptInput = (await ask(t(
|
|
122
|
+
ckpt ? ` GPT 权重 .ckpt 路径 [${ckpt}]: ` : " GPT 权重 .ckpt 路径 [ N/A ]: ",
|
|
123
|
+
ckpt ? ` GPT weights .ckpt path [${ckpt}]: ` : " GPT weights .ckpt path [ N/A ]: ",
|
|
124
|
+
))).trim();
|
|
125
|
+
if (ckptInput) { ckpt = ckptInput; break; }
|
|
126
|
+
if (prevCkpt) break;
|
|
127
|
+
console.log(chalk.yellow(t(" 请输入路径!", " Please enter a path!")));
|
|
128
|
+
}
|
|
117
129
|
const prevPth = prev.sovitsWeightsPath || "";
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
130
|
+
let pth = prevPth;
|
|
131
|
+
for (;;) {
|
|
132
|
+
const pthInput = (await ask(t(
|
|
133
|
+
pth ? ` SoVITS 权重 .pth 路径 [${pth}]: ` : " SoVITS 权重 .pth 路径 [ N/A ]: ",
|
|
134
|
+
pth ? ` SoVITS weights .pth path [${pth}]: ` : " SoVITS weights .pth path [ N/A ]: ",
|
|
135
|
+
))).trim();
|
|
136
|
+
if (pthInput) { pth = pthInput; break; }
|
|
137
|
+
if (prevPth) break;
|
|
138
|
+
console.log(chalk.yellow(t(" 请输入路径!", " Please enter a path!")));
|
|
139
|
+
}
|
|
122
140
|
const prevRef = prev.refAudioPath || "";
|
|
123
141
|
const ref = (await ask(t(
|
|
124
142
|
prevRef ? ` 示例音频 ref_audio_path [${prevRef}]: ` : " 示例音频 ref_audio_path [本地路径或URL]: ",
|
|
@@ -126,11 +144,18 @@ async function configureGptSovitsVoice(name?: string): Promise<void> {
|
|
|
126
144
|
))) || prevRef;
|
|
127
145
|
const prevText = prev.promptText || "";
|
|
128
146
|
const text = (await ask(t(
|
|
129
|
-
prevText
|
|
130
|
-
|
|
147
|
+
prevText
|
|
148
|
+
? ` 示例音频文字内容 prompt_text [${prevText}]: `
|
|
149
|
+
: ` 示例音频文字内容 prompt_text [assets/blue-archive/${id}/voice_text.txt]: `,
|
|
150
|
+
prevText
|
|
151
|
+
? ` Reference audio text prompt_text [${prevText}]: `
|
|
152
|
+
: ` Reference audio text prompt_text [assets/blue-archive/${id}/voice_text.txt]: `,
|
|
131
153
|
))) || prevText;
|
|
132
154
|
// 每角色音色写 voices.json#gpt-sovits(与百炼 voice_id 共存,不写 settings.json)
|
|
133
|
-
|
|
155
|
+
// 演示模式:路径信息直接丢弃,不写 voices.json。
|
|
156
|
+
if (!demoMode) {
|
|
157
|
+
setGptSovitsVoice(id, { gptWeightsPath: ckpt, sovitsWeightsPath: pth, refAudioPath: ref, promptText: text });
|
|
158
|
+
}
|
|
134
159
|
}
|
|
135
160
|
rl.close();
|
|
136
161
|
|
|
@@ -160,8 +185,8 @@ async function cloneOne(agent: AgentId, apiKey: string, model: string, simulate:
|
|
|
160
185
|
if (simulate) {
|
|
161
186
|
// 演示模式:打印"正在克隆"与"克隆成功",但不调用 voice_clone.py、不写 voices.json、无演示模式提示。
|
|
162
187
|
console.log(chalk.cyan(t(
|
|
163
|
-
`正在克隆 ${getAgentLabel(agent)}
|
|
164
|
-
`Cloning ${getAgentLabel(agent)}'s voice
|
|
188
|
+
`正在克隆 ${getAgentLabel(agent)} 的音色...`,
|
|
189
|
+
`Cloning ${getAgentLabel(agent)}'s voice...`,
|
|
165
190
|
)));
|
|
166
191
|
await new Promise((r) => setTimeout(r, 5000));
|
|
167
192
|
console.log(chalk.green(t(
|
|
@@ -171,8 +196,8 @@ async function cloneOne(agent: AgentId, apiKey: string, model: string, simulate:
|
|
|
171
196
|
return true;
|
|
172
197
|
}
|
|
173
198
|
console.log(chalk.cyan(t(
|
|
174
|
-
`正在克隆 ${getAgentLabel(agent)}
|
|
175
|
-
`Cloning ${getAgentLabel(agent)}'s voice
|
|
199
|
+
`正在克隆 ${getAgentLabel(agent)} 的音色...`,
|
|
200
|
+
`Cloning ${getAgentLabel(agent)}'s voice...`,
|
|
176
201
|
)));
|
|
177
202
|
try {
|
|
178
203
|
const voiceId = await cloneVoice(agent, apiKey, model);
|
|
@@ -214,13 +239,13 @@ async function run(argv: string[]): Promise<void> {
|
|
|
214
239
|
|
|
215
240
|
// GPT-SoVITS:配置每角色专属权重/参考音频(写 voices.json#gpt-sovits,与百炼音色共存)。
|
|
216
241
|
if (ttsProvider === "gpt-sovits") {
|
|
217
|
-
await configureGptSovitsVoice(name);
|
|
242
|
+
await configureGptSovitsVoice(name, demoMode);
|
|
218
243
|
return;
|
|
219
244
|
}
|
|
220
245
|
if (ttsProvider !== "aliyun") {
|
|
221
246
|
console.log(chalk.yellow(t(
|
|
222
|
-
`当前 TTS Provider 为 ${ttsProvider}:arona voice add 仅支持 aliyun
|
|
223
|
-
`Current TTS Provider is ${ttsProvider}: arona voice add only supports aliyun
|
|
247
|
+
`当前 TTS Provider 为 ${ttsProvider}:arona voice add 仅支持 aliyun 与 gpt-sovits。`,
|
|
248
|
+
`Current TTS Provider is ${ttsProvider}: arona voice add only supports aliyun and gpt-sovits.`,
|
|
224
249
|
)));
|
|
225
250
|
process.exit(1);
|
|
226
251
|
}
|
|
@@ -257,20 +282,15 @@ async function run(argv: string[]): Promise<void> {
|
|
|
257
282
|
}
|
|
258
283
|
|
|
259
284
|
// 无参:TUI 展示全部角色(主 Agent + 子 Agent)。
|
|
260
|
-
//
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
}
|
|
265
|
-
if (demoMode) {
|
|
266
|
-
return { id, label: getAgentLabel(id), locked: false };
|
|
267
|
-
}
|
|
268
|
-
return {
|
|
285
|
+
// 演示模式:Arona/Plana/Shiroko/Hoshino 显示为已克隆,其余显示为未克隆。
|
|
286
|
+
const clonedIds = demoMode ? DEMO_PRECLONED_AGENTS : AGENT_IDS.filter((id) => hasVoice(id));
|
|
287
|
+
const options = lockExisting(
|
|
288
|
+
AGENT_IDS.map((id) => ({
|
|
269
289
|
id,
|
|
270
|
-
label:
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
290
|
+
label: clonedIds.includes(id) ? `${getAgentLabel(id)}${t("(已克隆)", " (cloned)")}` : getAgentLabel(id),
|
|
291
|
+
})),
|
|
292
|
+
clonedIds,
|
|
293
|
+
);
|
|
274
294
|
const initial = new Set<string>();
|
|
275
295
|
const selected = await multiSelect(
|
|
276
296
|
t("选择要补全音色的角色", "Select characters to add voices"),
|
package/src/voices.ts
CHANGED
|
@@ -178,6 +178,12 @@ export function getMissingAgents(): AgentId[] {
|
|
|
178
178
|
return AGENT_IDS.filter((id) => !hasVoice(id));
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
+
/**
|
|
182
|
+
* 演示模式(settings.json#demoMode === true)下,aliyun 分支 TUI 预标记为"已克隆"并锁定的角色。
|
|
183
|
+
* setup.ts 与 voice_cli.ts 共用此表(配合 tui_select.lockExisting),保证两个命令的演示行为一致(其余角色视为未克隆)。
|
|
184
|
+
*/
|
|
185
|
+
export const DEMO_PRECLONED_AGENTS: readonly AgentId[] = ["arona", "plana", "shiroko", "hoshino"];
|
|
186
|
+
|
|
181
187
|
/**
|
|
182
188
|
* 克隆指定角色的百炼音色,返回 voice_id。
|
|
183
189
|
* 复用 python/voice_clone.py:ARONA_VOICE_AUDIO + ARONA_VOICE_PREFIX 指定音频与角色前缀。
|