arona-agent 1.0.2 → 1.0.4

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.
Files changed (51) hide show
  1. package/README.md +16 -9
  2. package/assets/blue-archive/arona/voice_sovits.mp3 +0 -0
  3. package/assets/blue-archive/arona/voice_text.txt +1 -0
  4. package/assets/blue-archive/hoshino/voice_sovits.mp3 +0 -0
  5. package/assets/blue-archive/hoshino/voice_text.txt +1 -0
  6. package/assets/blue-archive/plana/voice_sovits.mp3 +0 -0
  7. package/assets/blue-archive/plana/voice_text.txt +1 -0
  8. package/assets/blue-archive/shiroko/voice_sovits.mp3 +0 -0
  9. package/assets/blue-archive/shiroko/voice_text.txt +1 -0
  10. package/bin/arona.mjs +3 -1
  11. package/bin/postinstall-fix.mjs +46 -0
  12. package/package.json +3 -2
  13. package/pet/renderer/gallery.js +2 -2
  14. package/pet/renderer/renderer.js +1 -1
  15. package/pet/renderer/style.css +16 -2
  16. package/pet/tools/gen_sway.cjs +1 -1
  17. package/pet/tools/visual_test.cjs +2 -2
  18. package/python/__pycache__/_i18n.cpython-314.pyc +0 -0
  19. package/python/__pycache__/computer_use.cpython-314.pyc +0 -0
  20. package/python/__pycache__/hotkey.cpython-314.pyc +0 -0
  21. package/python/__pycache__/oss_upload.cpython-314.pyc +0 -0
  22. package/python/__pycache__/stt.cpython-314.pyc +0 -0
  23. package/python/__pycache__/tts_say.cpython-314.pyc +0 -0
  24. package/python/hotkey.py +7 -1
  25. package/python/oss_upload.py +70 -0
  26. package/python/tts_say.py +182 -19
  27. package/src/agent.ts +155 -147
  28. package/src/commands.ts +17 -23
  29. package/src/config.ts +45 -2
  30. package/src/gesture_context.ts +48 -0
  31. package/src/gpt_sovits_local.ts +706 -0
  32. package/src/index.ts +13 -10
  33. package/src/memory.ts +3 -10
  34. package/src/oss_upload.ts +116 -0
  35. package/src/renderer.ts +17 -6
  36. package/src/repl.ts +70 -59
  37. package/src/setup.ts +349 -15
  38. package/src/skills.ts +32 -2
  39. package/src/slash_menu.ts +1 -1
  40. package/src/slash_registry.ts +6 -5
  41. package/src/tools/read_docs_tool.ts +84 -0
  42. package/src/tools/tavily_tools.ts +24 -10
  43. package/src/tools/voice_tools.ts +1 -1
  44. package/src/tts_provider.ts +501 -0
  45. package/src/tts_stream.ts +321 -31
  46. package/src/tui_select.ts +31 -2
  47. package/src/undo.ts +82 -18
  48. package/src/utils/python.ts +22 -0
  49. package/src/voice.ts +17 -7
  50. package/src/voice_cli.ts +117 -9
  51. package/src/voices.ts +197 -28
package/src/commands.ts CHANGED
@@ -3,7 +3,7 @@ import { execSync } from "child_process";
3
3
  import { writeFileSync } from "fs";
4
4
  import { join } from "path";
5
5
  import type { AgentSession, DefaultResourceLoader } from "@earendil-works/pi-coding-agent";
6
- import { config } from "./config.ts";
6
+ import { config, sttHotkeyLabel } from "./config.ts";
7
7
  import * as memory from "./memory.ts";
8
8
  import * as voice from "./voice.ts";
9
9
  import * as mcp from "./mcp.ts";
@@ -37,11 +37,12 @@ export interface CommandContext {
37
37
  undoManager?: UndoManager;
38
38
  // /change-agent 切换角色后重启 TTS 进程(音色随主 Agent,spawn 时固化,需重拉)
39
39
  restartTts?: () => void;
40
+ // /change-agent 只改子 Agent 组合时回收旧子 session 对象,下轮按新组合重新初始化
41
+ resetSubAgents?: () => void;
40
42
  }
41
43
 
42
44
  const HELP_TEXT = t(
43
- `
44
- ${chalk.bold.cyan("ARONA Agent - 命令列表")}
45
+ `${chalk.bold.cyan("ARONA Agent - 命令列表")}
45
46
 
46
47
  ${chalk.bold("会话")}
47
48
  /new, /clear 开始新会话(清空上下文)
@@ -50,9 +51,9 @@ ${chalk.bold("会话")}
50
51
  /export 导出当前会话为 Markdown
51
52
 
52
53
  ${chalk.bold("显示")}
53
- /thinking 开关思考/推理块显示
54
+ /thinking 开关推理块显示
54
55
  /details 开关工具执行详情显示
55
- /compact 压缩当前上下文以节省 token
56
+ /compact 压缩上下文
56
57
 
57
58
  ${chalk.bold("语音")}
58
59
  /tts 开关文字转语音
@@ -67,13 +68,8 @@ ${chalk.bold("其他")}
67
68
  /undo 撤销上一个回合的全部文件改动
68
69
  /redo 重做已撤销的改动
69
70
  /help 显示本帮助
70
-
71
- ${chalk.cyan("提示:")}
72
- ${chalk.cyan("• @文件名 把文件内容插入到消息中")}
73
- ${chalk.cyan("• !命令 执行 shell 命令并把输出加入消息")}
74
71
  `,
75
- `
76
- ${chalk.bold.cyan("ARONA Agent - Commands")}
72
+ `${chalk.bold.cyan("ARONA Agent - Commands")}
77
73
 
78
74
  ${chalk.bold("Session")}
79
75
  /new, /clear Start a new session (clear context)
@@ -99,10 +95,6 @@ ${chalk.bold("Other")}
99
95
  /undo Undo the previous turn's file changes
100
96
  /redo Redo undone changes
101
97
  /help Show this help
102
-
103
- ${chalk.cyan("Tips:")}
104
- ${chalk.cyan("• @filename inserts the file content into your message")}
105
- ${chalk.cyan("• !command runs a shell command and appends its output")}
106
98
  `,
107
99
  );
108
100
 
@@ -184,7 +176,7 @@ export async function handleCommand(input: string, ctx: CommandContext): Promise
184
176
  voice.setSttEnabled(!voice.isSttEnabled());
185
177
  console.log(chalk.cyan(t(`STT:${voice.isSttEnabled() ? "开" : "关"}`, `STT: ${voice.isSttEnabled() ? "on" : "off"}`)));
186
178
  if (voice.isSttEnabled()) {
187
- console.log(chalk.cyan(t("长按右 Cmd ≥2秒录音(任务中按会先中断再录音)。", "Hold right Cmd ≥2s to record (press during a task to interrupt it first).")));
179
+ console.log(chalk.cyan(t(`长按${sttHotkeyLabel()} ≥2秒录音。`, `Hold ${sttHotkeyLabel()} ≥2s to record.`)));
188
180
  }
189
181
  ctx.setSttHookEnabled?.(voice.isSttEnabled());
190
182
  return true;
@@ -198,11 +190,11 @@ export async function handleCommand(input: string, ctx: CommandContext): Promise
198
190
  return true;
199
191
 
200
192
  case "undo":
201
- handleUndo(ctx);
193
+ await handleUndo(ctx);
202
194
  return true;
203
195
 
204
196
  case "redo":
205
- handleRedo(ctx);
197
+ await handleRedo(ctx);
206
198
  return true;
207
199
 
208
200
  case "skill":
@@ -409,22 +401,22 @@ function handleExport(ctx: CommandContext) {
409
401
  }
410
402
  }
411
403
 
412
- function handleUndo(ctx: CommandContext) {
404
+ async function handleUndo(ctx: CommandContext) {
413
405
  if (!ctx.undoManager) {
414
406
  console.log(chalk.yellow(t("撤销系统未初始化。", "Undo system is not initialized.")));
415
407
  return;
416
408
  }
417
- const r = ctx.undoManager.undo();
409
+ const r = await ctx.undoManager.undo();
418
410
  if (r.ok) console.log(chalk.green(r.message));
419
411
  else console.log(chalk.yellow(r.message));
420
412
  }
421
413
 
422
- function handleRedo(ctx: CommandContext) {
414
+ async function handleRedo(ctx: CommandContext) {
423
415
  if (!ctx.undoManager) {
424
416
  console.log(chalk.yellow(t("撤销系统未初始化。", "Undo system is not initialized.")));
425
417
  return;
426
418
  }
427
- const r = ctx.undoManager.redo();
419
+ const r = await ctx.undoManager.redo();
428
420
  if (r.ok) console.log(chalk.green(r.message));
429
421
  else console.log(chalk.yellow(r.message));
430
422
  }
@@ -535,8 +527,10 @@ async function handleChangeAgent(ctx: CommandContext) {
535
527
  // 1. 持久化(settings.json mainAgent + subAgents 字段)
536
528
  setMainAgent(main);
537
529
  setSubAgents(subs);
538
- // 2. 桌宠多窗口按新组合重建(旧进程退出后自动拉起)
530
+ // 2. 桌宠多窗口按新组合重建
539
531
  pet.restartWithSelection(main, subs);
532
+ // 2.5 子 Agent 组合变化:回收旧子 session 对象;主 Agent 切换时 newSession 也会回收
533
+ if (subsChanged) ctx.resetSubAgents?.();
540
534
  // 3. 主 Agent 切换才需要重启 TTS(音色随主 Agent 变化)与重建会话(人格变化)
541
535
  if (mainChanged) {
542
536
  ctx.restartTts?.();
package/src/config.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { homedir } from "os";
2
2
  import { join, resolve } from "path";
3
3
  import { existsSync, mkdirSync, readFileSync } from "fs";
4
- import type { LanguageSetting } from "./locale.ts";
4
+ import { t, type LanguageSetting } from "./locale.ts";
5
5
 
6
6
  export const ARONA_DIR = join(homedir(), ".arona");
7
7
  export const MEMORY_FILE = join(ARONA_DIR, "MEMORY.md");
@@ -35,6 +35,22 @@ export interface McpServerConfig {
35
35
  headers?: Record<string, string>;
36
36
  }
37
37
 
38
+ /** 已实现的 TTS Provider。 */
39
+ export type TtsProvider = "aliyun" | "gpt-sovits";
40
+
41
+ export const TTS_PROVIDER_IDS: readonly TtsProvider[] = ["aliyun", "gpt-sovits"];
42
+
43
+ /** settings.json 中 ttsProvider 的原始值解析:未知/未实现回退 aliyun 并警告一次。 */
44
+ function resolveTtsProvider(raw: unknown): TtsProvider {
45
+ if (typeof raw === "string" && (TTS_PROVIDER_IDS as readonly string[]).includes(raw)) {
46
+ return raw as TtsProvider;
47
+ }
48
+ if (typeof raw === "string" && raw) {
49
+ console.warn(t(`[tts] 未知 ttsProvider "${raw}",已回退 aliyun`, `[tts] unknown ttsProvider "${raw}", falling back to aliyun`));
50
+ }
51
+ return "aliyun";
52
+ }
53
+
38
54
  interface AronaConfig {
39
55
  // LLM
40
56
  apiKey: string;
@@ -45,6 +61,10 @@ interface AronaConfig {
45
61
  contextWindow: number;
46
62
  // Voice (Qwen / 阿里云百炼 DashScope)
47
63
  noVoice: boolean;
64
+ // TTS 后端:aliyun(默认,百炼) | gpt-sovits(本地 api_v2 服务)
65
+ ttsProvider: TtsProvider;
66
+ // 各 Provider 专属配置(键 = provider id)。百炼沿用顶层 ttsApiKey/ttsModel/workspaceId,不入此表。
67
+ ttsConfig: Record<string, unknown>;
48
68
  // 百炼业务空间 ID(可选;留空走旧域名 dashscope.aliyuncs.com)
49
69
  workspaceId: string;
50
70
  ttsApiKey: string;
@@ -83,6 +103,10 @@ interface Settings {
83
103
  sttEnabled?: boolean;
84
104
  /** @deprecated 已合并进 ttsEnabled,仅读兼容(旧配置 ttsAuto:false 仍生效) */
85
105
  ttsAuto?: boolean;
106
+ /** TTS 后端(aliyun | gpt-sovits) */
107
+ ttsProvider?: string;
108
+ /** 各 Provider 专属配置(键 = provider id;百炼沿用顶层 TTS 字段) */
109
+ ttsConfig?: Record<string, unknown>;
86
110
  workspaceId?: string;
87
111
  ttsApiKey?: string;
88
112
  ttsModel?: string;
@@ -115,7 +139,7 @@ function loadSettings(): Settings {
115
139
  try {
116
140
  return JSON.parse(readFileSync(SETTINGS_FILE, "utf-8"));
117
141
  } catch {
118
- console.warn("Failed to parse settings.json, using defaults");
142
+ console.warn(t("settings.json 解析失败,使用默认配置", "Failed to parse settings.json, using defaults"));
119
143
  return {};
120
144
  }
121
145
  }
@@ -219,6 +243,9 @@ function loadConfig(): AronaConfig {
219
243
  thinkingLevel: s.thinkingLevel || "medium",
220
244
  contextWindow: typeof s.contextWindow === "number" && s.contextWindow > 0 ? s.contextWindow : 1000000,
221
245
 
246
+ ttsProvider: resolveTtsProvider(s.ttsProvider),
247
+ ttsConfig: s.ttsConfig && typeof s.ttsConfig === "object" && !Array.isArray(s.ttsConfig) ? s.ttsConfig : {},
248
+
222
249
  noVoice,
223
250
  // Skip TTS/STT fields when --no-voice is set
224
251
  workspaceId: noVoice ? "" : (s.workspaceId || ""),
@@ -243,3 +270,19 @@ function loadConfig(): AronaConfig {
243
270
  }
244
271
 
245
272
  export const config = loadConfig();
273
+
274
+ /**
275
+ * STT 全局热键键名(pynput 命名,供 repl.ts 注入 ARONA_HOTKEY_KEY):
276
+ * macOS 右 Cmd(pynput 有 Key.cmd_r),Windows/Linux 右 Ctrl(pynput Key.ctrl_r 三平台皆有效)。
277
+ * Windows 没有 Key.cmd_r,默认 cmd_r 在那会是静默失效,故按平台自动选。
278
+ */
279
+ export function sttHotkeyKey(): string {
280
+ return process.platform === "darwin" ? "cmd_r" : "ctrl_r";
281
+ }
282
+
283
+ /** STT 全局热键显示名(用户可见,双语简洁)。右 Ctrl 提示一句话即可,不追加文案。 */
284
+ export function sttHotkeyLabel(): string {
285
+ return process.platform === "darwin"
286
+ ? t("右 Cmd", "right Cmd")
287
+ : t("右 Ctrl", "right Ctrl");
288
+ }
@@ -0,0 +1,48 @@
1
+ // 桌宠手势上下文注入扩展:摸头/dizzy 触发后,在**主 Agent** 的发送边界(context 事件)
2
+ // 追加一条 user 消息,描述"刚才发生了什么",引导主 Agent 在回复中自然流露出确实感受到了。
3
+ //
4
+ // 为什么用发送边界而不是拼进用户输入:repl.ts 的 processInput 会把文本写进主 session 的
5
+ // state.messages,会污染会话命名/存储;拼进输入仍可能被主 session 落盘/导出,且子 Agent 每轮
6
+ // 复制主 session 全量(stateless)时会一并看到。发送边界注入只改本次请求的消息数组、不进
7
+ // state.messages → 仅主 Agent 本轮看到、零存储污染、会话命名天然干净。
8
+ //
9
+ // 只注册在**主 Agent** 的 loader 扩展列表(agent.ts initAgent),子 Agent(initSubAgent)
10
+ // 的 loader 不含本扩展 → 不会收到。消费经 pet.takeGesture()(消费即清空,只注入最近一次)。
11
+
12
+ import type { ContextEvent, ExtensionAPI, InlineExtension } from "@earendil-works/pi-coding-agent";
13
+ import { pet } from "./pet.ts";
14
+ import { t } from "./locale.ts";
15
+
16
+ /** 桌宠手势 → user 消息的上下文场景文本(双语)。 */
17
+ function petGestureScene(type: "pat" | "dizzy"): string {
18
+ if (type === "dizzy") {
19
+ return t(
20
+ "(Sensei刚才拖着你到处晃来晃去,你感觉有些头晕。请在回复里自然地体现出这份晕眩感。)",
21
+ "(Sensei just swung you around, and you feel a bit dizzy. Let that dizziness show naturally in your reply.)",
22
+ );
23
+ }
24
+ return t(
25
+ "(Sensei刚才摸了摸你的头。请在回复里自然地体现出被摸头的感受。)",
26
+ "(Sensei just petted your head. Let that show naturally in your reply.)",
27
+ );
28
+ }
29
+
30
+ export const gestureContextExtension: InlineExtension = {
31
+ name: "arona-gesture-context",
32
+ hidden: true, // 不显示在启动扩展列表
33
+ factory: (pi: ExtensionAPI) => {
34
+ pi.on("context", (event: ContextEvent) => {
35
+ // 有手势时才注入;takeGesture 消费即清空 → 一次 LLM 调用只注入一次、无手势时原样返回
36
+ const gesture = pet.takeGesture();
37
+ if (!gesture) return;
38
+ const scene = petGestureScene(gesture);
39
+ if (!scene) return;
40
+ return {
41
+ messages: [
42
+ ...event.messages,
43
+ { role: "user" as const, content: [{ type: "text" as const, text: scene }], timestamp: Date.now() },
44
+ ],
45
+ };
46
+ });
47
+ },
48
+ };