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.
Files changed (109) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +149 -0
  3. package/assets/blue-archive/arona/spine/arona_spr.atlas.txt +1168 -0
  4. package/assets/blue-archive/arona/spine/arona_spr.png +0 -0
  5. package/assets/blue-archive/arona/spine/arona_spr.skel +0 -0
  6. package/assets/blue-archive/arona/voice.mp3 +0 -0
  7. package/assets/blue-archive/hoshino/hoshino_spr.atlas.txt +174 -0
  8. package/assets/blue-archive/hoshino/hoshino_spr.json +15385 -0
  9. package/assets/blue-archive/hoshino/hoshino_spr.png +0 -0
  10. package/assets/blue-archive/hoshino/hoshino_spr.skel +0 -0
  11. package/assets/blue-archive/hoshino/robber/hoshino_robber_spr.atlas.txt +111 -0
  12. package/assets/blue-archive/hoshino/robber/hoshino_robber_spr.png +0 -0
  13. package/assets/blue-archive/hoshino/robber/hoshino_robber_spr.skel +0 -0
  14. package/assets/blue-archive/hoshino/swimsuit/hoshino_swimsuit_spr.atlas.txt +251 -0
  15. package/assets/blue-archive/hoshino/swimsuit/hoshino_swimsuit_spr.png +0 -0
  16. package/assets/blue-archive/hoshino/swimsuit/hoshino_swimsuit_spr.skel +0 -0
  17. package/assets/blue-archive/hoshino/voice.mp3 +0 -0
  18. package/assets/blue-archive/plana/spine/plana_spr.atlas.txt +1266 -0
  19. package/assets/blue-archive/plana/spine/plana_spr.png +0 -0
  20. package/assets/blue-archive/plana/spine/plana_spr.skel +0 -0
  21. package/assets/blue-archive/plana/voice.mp3 +0 -0
  22. package/assets/blue-archive/shiroko/ridingsuit/shiroko_ridingsuit_spr.atlas.txt +97 -0
  23. package/assets/blue-archive/shiroko/ridingsuit/shiroko_ridingsuit_spr.png +0 -0
  24. package/assets/blue-archive/shiroko/ridingsuit/shiroko_ridingsuit_spr.skel +0 -0
  25. package/assets/blue-archive/shiroko/robber/shiroko_robber_spr.atlas.txt +174 -0
  26. package/assets/blue-archive/shiroko/robber/shiroko_robber_spr.png +0 -0
  27. package/assets/blue-archive/shiroko/robber/shiroko_robber_spr.skel +0 -0
  28. package/assets/blue-archive/shiroko/shiroko_spr.atlas.txt +167 -0
  29. package/assets/blue-archive/shiroko/shiroko_spr.json +10702 -0
  30. package/assets/blue-archive/shiroko/shiroko_spr.png +0 -0
  31. package/assets/blue-archive/shiroko/shiroko_spr.skel +0 -0
  32. package/assets/blue-archive/shiroko/voice.mp3 +0 -0
  33. package/bin/arona.mjs +43 -0
  34. package/package.json +37 -0
  35. package/pet/agents.cjs +197 -0
  36. package/pet/main.cjs +462 -0
  37. package/pet/preload.cjs +21 -0
  38. package/pet/renderer/fx.html +30 -0
  39. package/pet/renderer/fx.js +41 -0
  40. package/pet/renderer/gallery.html +18 -0
  41. package/pet/renderer/gallery.js +31 -0
  42. package/pet/renderer/index.html +18 -0
  43. package/pet/renderer/renderer.js +382 -0
  44. package/pet/renderer/spine_layer.js +609 -0
  45. package/pet/renderer/spinetest.html +24 -0
  46. package/pet/renderer/spinetest.js +295 -0
  47. package/pet/renderer/style.css +63 -0
  48. package/pet/tools/gallery_capture.cjs +267 -0
  49. package/pet/tools/gen_sway.cjs +170 -0
  50. package/pet/tools/meshify_region.cjs +105 -0
  51. package/pet/tools/skel_inspect.cjs +112 -0
  52. package/pet/tools/skel_to_json.cjs +410 -0
  53. package/pet/tools/spine_node.cjs +69 -0
  54. package/pet/tools/visual_test.cjs +262 -0
  55. package/pet/vendor/spine/spine-canvas.js +8472 -0
  56. package/pet/vendor/spine/spine-webgl.js +10839 -0
  57. package/python/__pycache__/_i18n.cpython-313.pyc +0 -0
  58. package/python/__pycache__/_i18n.cpython-314.pyc +0 -0
  59. package/python/__pycache__/computer_use.cpython-314.pyc +0 -0
  60. package/python/__pycache__/hotkey.cpython-313.pyc +0 -0
  61. package/python/__pycache__/hotkey.cpython-314.pyc +0 -0
  62. package/python/__pycache__/stt.cpython-313.pyc +0 -0
  63. package/python/__pycache__/stt.cpython-314.pyc +0 -0
  64. package/python/__pycache__/tts.cpython-313.pyc +0 -0
  65. package/python/__pycache__/tts.cpython-314.pyc +0 -0
  66. package/python/__pycache__/tts_stream.cpython-313.pyc +0 -0
  67. package/python/__pycache__/tts_stream.cpython-314.pyc +0 -0
  68. package/python/__pycache__/voice_clone.cpython-314.pyc +0 -0
  69. package/python/_i18n.py +18 -0
  70. package/python/computer_use.py +190 -0
  71. package/python/hotkey.py +196 -0
  72. package/python/stt.py +240 -0
  73. package/python/tts_say.py +177 -0
  74. package/python/voice_clone.py +98 -0
  75. package/requirements.txt +6 -0
  76. package/src/agent.ts +791 -0
  77. package/src/agent_registry.ts +109 -0
  78. package/src/commands.ts +641 -0
  79. package/src/config.ts +245 -0
  80. package/src/in_memory_credentials.ts +61 -0
  81. package/src/index.ts +78 -0
  82. package/src/locale.ts +131 -0
  83. package/src/logo.ts +120 -0
  84. package/src/mcp.ts +192 -0
  85. package/src/memory.ts +303 -0
  86. package/src/pet.ts +346 -0
  87. package/src/renderer.ts +350 -0
  88. package/src/repl.ts +902 -0
  89. package/src/setup.ts +371 -0
  90. package/src/skills.ts +96 -0
  91. package/src/slash_menu.ts +371 -0
  92. package/src/slash_registry.ts +53 -0
  93. package/src/speaker_context.ts +41 -0
  94. package/src/text_split.ts +79 -0
  95. package/src/tools/computer_use.ts +142 -0
  96. package/src/tools/emotion_tool.ts +55 -0
  97. package/src/tools/keep_silent_tool.ts +26 -0
  98. package/src/tools/memory_tool.ts +24 -0
  99. package/src/tools/skill_tools.ts +100 -0
  100. package/src/tools/tavily_tools.ts +431 -0
  101. package/src/tools/voice_tools.ts +28 -0
  102. package/src/tts_stream.ts +260 -0
  103. package/src/tui_select.ts +146 -0
  104. package/src/undo.ts +451 -0
  105. package/src/utils/python.ts +211 -0
  106. package/src/utils/spawn.ts +56 -0
  107. package/src/voice.ts +83 -0
  108. package/src/voice_cli.ts +198 -0
  109. package/src/voices.ts +135 -0
package/src/config.ts ADDED
@@ -0,0 +1,245 @@
1
+ import { homedir } from "os";
2
+ import { join, resolve } from "path";
3
+ import { existsSync, mkdirSync, readFileSync } from "fs";
4
+ import type { LanguageSetting } from "./locale.ts";
5
+
6
+ export const ARONA_DIR = join(homedir(), ".arona");
7
+ export const MEMORY_FILE = join(ARONA_DIR, "MEMORY.md");
8
+ export const SESSIONS_DIR = join(ARONA_DIR, "sessions");
9
+ export const SKILLS_DIR = join(ARONA_DIR, "skills");
10
+ export const SETTINGS_FILE = join(ARONA_DIR, "settings.json");
11
+ export const PROJECT_ROOT = resolve(import.meta.dirname, "..");
12
+ export const PYTHON_DIR = join(PROJECT_ROOT, "python");
13
+ export const PET_DIR = join(PROJECT_ROOT, "pet");
14
+
15
+ // Ensure directories exist
16
+ function ensureDir(path: string) {
17
+ if (!existsSync(path)) mkdirSync(path, { recursive: true });
18
+ }
19
+ ensureDir(ARONA_DIR);
20
+ ensureDir(SESSIONS_DIR);
21
+ ensureDir(SKILLS_DIR);
22
+
23
+ // Parse --no-voice flag from CLI args
24
+ const noVoice = process.argv.includes("--no-voice");
25
+
26
+ // Parse --verbose flag from CLI args(桌宠/子进程详细日志;Electron 加 --enable-logging 转发 renderer 日志)
27
+ export const verbose = process.argv.includes("--verbose");
28
+
29
+ export interface McpServerConfig {
30
+ command?: string;
31
+ args?: string[];
32
+ env?: Record<string, string>;
33
+ url?: string;
34
+ /** 仅 url 型(Streamable HTTP)server 生效:自定义请求头,如 Authorization */
35
+ headers?: Record<string, string>;
36
+ }
37
+
38
+ interface AronaConfig {
39
+ // LLM
40
+ apiKey: string;
41
+ apiBaseUrl: string;
42
+ model: string;
43
+ thinkingLevel: string;
44
+ // 上下文窗口(token 数):用于压缩阈值推导。settings.json 直接填数字(如 1000000),不填默认 1M。
45
+ contextWindow: number;
46
+ // Voice (Qwen / 阿里云百炼 DashScope)
47
+ noVoice: boolean;
48
+ // 百炼业务空间 ID(可选;留空走旧域名 dashscope.aliyuncs.com)
49
+ workspaceId: string;
50
+ ttsApiKey: string;
51
+ ttsModel: string;
52
+ // 系统音色名 或 自定义音色(声音复刻)ID —— 填声音复刻得到的 voice_id 即用自定义音色
53
+ ttsVoice: string;
54
+ ttsSampleRate: number;
55
+ sttApiKey: string;
56
+ sttModel: string;
57
+ sttFormat: string;
58
+ sttSampleRate: number;
59
+ ttsEnabled: boolean;
60
+ sttEnabled: boolean;
61
+ // Cua
62
+ cuaApiKey: string;
63
+ // Tavily(网页搜索;留空走 keyless 共享池,免费限流)
64
+ tavilyApiKey: string;
65
+ // Misc
66
+ pythonPath: string;
67
+ mcpServers: Record<string, McpServerConfig>;
68
+ }
69
+
70
+ interface Settings {
71
+ apiKey?: string;
72
+ apiBaseUrl?: string;
73
+ model?: string;
74
+ thinkingLevel?: string;
75
+ // 上下文窗口(token 数),默认 1M。用于压缩阈值推导,不填 SDK 走模型注册表。
76
+ contextWindow?: number;
77
+ language?: LanguageSetting;
78
+ // 主 Agent(arona | plana):桌宠形象 + 人格,由 agent_registry.ts 读写
79
+ mainAgent?: string;
80
+ // 启用的子 Agent 列表(shiroko | hoshino),由 agent_registry.ts 读写
81
+ subAgents?: string[];
82
+ ttsEnabled?: boolean;
83
+ sttEnabled?: boolean;
84
+ /** @deprecated 已合并进 ttsEnabled,仅读兼容(旧配置 ttsAuto:false 仍生效) */
85
+ ttsAuto?: boolean;
86
+ workspaceId?: string;
87
+ ttsApiKey?: string;
88
+ ttsModel?: string;
89
+ ttsVoice?: string;
90
+ ttsSampleRate?: number;
91
+ sttApiKey?: string;
92
+ sttModel?: string;
93
+ sttFormat?: string;
94
+ sttSampleRate?: number;
95
+ cuaApiKey?: string;
96
+ // Tavily API Key(可选):不填则 web_search/web_extract 走 keyless 共享池
97
+ tavilyApiKey?: string;
98
+ pythonPath?: string;
99
+ mcpServers?: Record<string, McpServerConfig>;
100
+ }
101
+
102
+ /**
103
+ * Check if ~/.arona/settings.json exists.
104
+ */
105
+ export function settingsExist(): boolean {
106
+ return existsSync(SETTINGS_FILE);
107
+ }
108
+
109
+ /**
110
+ * Load settings from ~/.arona/settings.json.
111
+ * Returns {} if the file is missing or unparseable.
112
+ */
113
+ function loadSettings(): Settings {
114
+ if (!existsSync(SETTINGS_FILE)) return {};
115
+ try {
116
+ return JSON.parse(readFileSync(SETTINGS_FILE, "utf-8"));
117
+ } catch {
118
+ console.warn("Failed to parse settings.json, using defaults");
119
+ return {};
120
+ }
121
+ }
122
+
123
+ // ============================================================
124
+ // Model prefix auto-detection
125
+ // ============================================================
126
+
127
+ /**
128
+ * Model name patterns → provider prefix.
129
+ * Checked in order; first match wins. Higher priority than Base URL.
130
+ */
131
+ const MODEL_PATTERNS: Array<[RegExp, string]> = [
132
+ [/^gpt[-_]/i, "openai"],
133
+ [/^o[134][-_]/i, "openai"],
134
+ [/^claude[-_]/i, "anthropic"],
135
+ [/^deepseek[-_]/i, "deepseek"],
136
+ [/^gemini[-_]/i, "google"],
137
+ [/^grok[-_]/i, "xai"],
138
+ [/^minimax[-_]/i, "minimax-cn"],
139
+ [/^kimi[-_]/i, "moonshotai-cn"],
140
+ [/^glm[-_]/i, "zai-coding-cn"],
141
+ [/^qwen/i, "qwen-token-plan-cn"],
142
+ [/^mistral[-_]/i, "mistral"],
143
+ [/^codestral[-_]/i, "mistral"],
144
+ [/^devstral[-_]/i, "mistral"],
145
+ [/^mimo[-_]/i, "xiaomi-token-plan-cn"],
146
+ [/^llama[-_]/i, "openrouter"],
147
+ ];
148
+
149
+ /**
150
+ * Base URL domain → provider prefix.
151
+ * Checked in order; first match wins. Lower priority than model name.
152
+ */
153
+ const URL_DOMAINS: Array<[string, string]> = [
154
+ ["api.openai.com", "openai"],
155
+ ["api.anthropic.com", "anthropic"],
156
+ ["api.deepseek.com", "deepseek"],
157
+ ["generativelanguage.googleapis.com", "google"],
158
+ ["api.x.ai", "xai"],
159
+ ["api.minimax.chat", "minimax-cn"],
160
+ ["api.moonshot.cn", "moonshotai-cn"],
161
+ ["open.bigmodel.cn", "zai-coding-cn"],
162
+ ["dashscope.aliyuncs.com", "qwen-token-plan-cn"],
163
+ ["api.mistral.ai", "mistral"],
164
+ ["openrouter.ai", "openrouter"],
165
+ ["api.groq.com", "groq"],
166
+ ];
167
+
168
+ /**
169
+ * Auto-detect the provider prefix for a model name.
170
+ *
171
+ * - If the model already contains "/", return it as-is.
172
+ * - Otherwise, detect from the model name (higher priority), then Base URL.
173
+ * - Defaults to "openai" if nothing matches.
174
+ */
175
+ export function resolveModelPrefix(model: string, apiBaseUrl: string): string {
176
+ // Already has a prefix
177
+ if (model.includes("/")) return model;
178
+
179
+ // 1. Model name patterns (higher priority)
180
+ for (const [pattern, provider] of MODEL_PATTERNS) {
181
+ if (pattern.test(model)) {
182
+ return `${provider}/${model}`;
183
+ }
184
+ }
185
+
186
+ // 2. Base URL domain (lower priority)
187
+ if (apiBaseUrl) {
188
+ const urlLower = apiBaseUrl.toLowerCase();
189
+ for (const [domain, provider] of URL_DOMAINS) {
190
+ if (urlLower.includes(domain)) {
191
+ return `${provider}/${model}`;
192
+ }
193
+ }
194
+ }
195
+
196
+ // 3. Default
197
+ return `openai/${model}`;
198
+ }
199
+
200
+ function loadConfig(): AronaConfig {
201
+ const s = loadSettings();
202
+
203
+ const apiKey = s.apiKey || "";
204
+ const apiBaseUrl = s.apiBaseUrl || "";
205
+
206
+ // Set OPENAI_BASE_URL for OpenAI-compatible endpoints
207
+ if (apiBaseUrl) {
208
+ process.env.OPENAI_BASE_URL = apiBaseUrl;
209
+ }
210
+
211
+ // Auto-detect model prefix
212
+ const rawModel = s.model || "openai/gpt-4o";
213
+ const model = resolveModelPrefix(rawModel, apiBaseUrl);
214
+
215
+ return {
216
+ apiKey,
217
+ apiBaseUrl,
218
+ model,
219
+ thinkingLevel: s.thinkingLevel || "medium",
220
+ contextWindow: typeof s.contextWindow === "number" && s.contextWindow > 0 ? s.contextWindow : 1000000,
221
+
222
+ noVoice,
223
+ // Skip TTS/STT fields when --no-voice is set
224
+ workspaceId: noVoice ? "" : (s.workspaceId || ""),
225
+ ttsApiKey: noVoice ? "" : (s.ttsApiKey || ""),
226
+ ttsModel: noVoice ? "" : (s.ttsModel || "qwen-audio-3.0-tts-plus"),
227
+ ttsVoice: noVoice ? "" : (s.ttsVoice || ""),
228
+ ttsSampleRate: noVoice ? 22050 : (s.ttsSampleRate || 22050),
229
+ sttApiKey: noVoice ? "" : (s.sttApiKey || ""),
230
+ sttModel: noVoice ? "" : (s.sttModel || "qwen-audio-3.0-asr-flash-streaming"),
231
+ sttFormat: noVoice ? "pcm" : (s.sttFormat || "pcm"),
232
+ sttSampleRate: noVoice ? 16000 : (s.sttSampleRate || 16000),
233
+ // ttsAuto 已合并进 ttsEnabled(旧配置兼容:ttsAuto ?? true)
234
+ ttsEnabled: noVoice ? false : (s.ttsEnabled ?? s.ttsAuto ?? true),
235
+ sttEnabled: noVoice ? false : (s.sttEnabled ?? true),
236
+
237
+ cuaApiKey: s.cuaApiKey || "",
238
+ tavilyApiKey: s.tavilyApiKey || "",
239
+
240
+ pythonPath: s.pythonPath || (process.platform === "win32" ? "python" : "python3"),
241
+ mcpServers: s.mcpServers || {},
242
+ };
243
+ }
244
+
245
+ export const config = loadConfig();
@@ -0,0 +1,61 @@
1
+ /**
2
+ * 内存版凭证存储 —— 替代 Pi SDK 默认的 ~/.arona/auth.json。
3
+ *
4
+ * ARONA 的 API key 由 settings.json → setRuntimeApiKey() 在启动时注入
5
+ * RuntimeCredentials 的内存 overrides,从不落盘。给 ModelRuntime.create()
6
+ * 传入本存储,可让 SDK 跳过 FileAuthStorageBackend,彻底不再创建 auth.json。
7
+ *
8
+ * 代价:OAuth 订阅制登录(radius 网关等)的凭证无法跨会话持久化。
9
+ * 仅 type-only 导入,运行时零依赖,不受 pi-ai 版本分裂影响。
10
+ */
11
+ import type {
12
+ AuthOperationOptions,
13
+ Credential,
14
+ CredentialInfo,
15
+ CredentialStore,
16
+ } from "@earendil-works/pi-ai";
17
+
18
+ /** 按 provider 串行化的写队列:保证 modify/delete 互斥,语义与 SDK 内存版一致。 */
19
+ export class InMemoryCredentialStore implements CredentialStore {
20
+ private credentials = new Map<string, Credential>();
21
+ private chains = new Map<string, Promise<unknown>>();
22
+
23
+ private enqueue<T>(providerId: string, task: () => Promise<T>): Promise<T> {
24
+ const prev = this.chains.get(providerId) ?? Promise.resolve();
25
+ const next = prev.then(task, task);
26
+ this.chains.set(providerId, next.catch(() => undefined));
27
+ return next;
28
+ }
29
+
30
+ async read(providerId: string): Promise<Credential | undefined> {
31
+ return this.credentials.get(providerId);
32
+ }
33
+
34
+ async list(): Promise<readonly CredentialInfo[]> {
35
+ return [...this.credentials.entries()].map(([providerId, credential]) => ({
36
+ providerId,
37
+ type: credential.type,
38
+ }));
39
+ }
40
+
41
+ modify(
42
+ providerId: string,
43
+ fn: (current: Credential | undefined) => Promise<Credential | undefined>,
44
+ ): Promise<Credential | undefined> {
45
+ return this.enqueue(providerId, async () => {
46
+ const current = this.credentials.get(providerId);
47
+ const next = await fn(current);
48
+ // SDK 契约:fn 返回 undefined 表示"保持不变"(返回当前值),而非删除凭证
49
+ if (next !== undefined) {
50
+ this.credentials.set(providerId, next);
51
+ }
52
+ return next ?? current;
53
+ });
54
+ }
55
+
56
+ delete(providerId: string): Promise<void> {
57
+ return this.enqueue(providerId, async () => {
58
+ this.credentials.delete(providerId);
59
+ });
60
+ }
61
+ }
package/src/index.ts ADDED
@@ -0,0 +1,78 @@
1
+ import { initAgent } from "./agent.ts";
2
+ import { Repl } from "./repl.ts";
3
+ import { resetConversationFlag, loadSession } from "./memory.ts";
4
+ import { startPet } from "./pet.ts";
5
+ import { config, settingsExist } from "./config.ts";
6
+ import { t } from "./locale.ts";
7
+ import chalk from "chalk";
8
+
9
+ async function main() {
10
+ // Require settings.json — if missing, tell user to run arona setup
11
+ if (!settingsExist()) {
12
+ console.log(chalk.yellow(t("未找到配置文件。", "Config file not found.")));
13
+ console.log(chalk.cyan(t("请先运行 arona setup 进行初始化配置。", "Please run arona setup to initialize first.")));
14
+ process.exit(1);
15
+ }
16
+
17
+ // Check for required environment
18
+ if (!config.apiKey) {
19
+ console.log(chalk.yellow(t("警告:未找到 LLM API Key。", "Warning: LLM API Key not found.")));
20
+ console.log(chalk.cyan(t("请运行 arona setup 进行配置。", "Please run arona setup to configure.")));
21
+ console.log();
22
+ }
23
+
24
+ if (config.noVoice) {
25
+ console.log(chalk.cyan(t("语音功能已禁用(--no-voice)。", "Voice features disabled (--no-voice).")));
26
+ }
27
+
28
+ // Initialize agent
29
+ console.log(chalk.cyan(t("正在初始化 ARONA...", "Initializing ARONA...")));
30
+
31
+ let { session, modelRuntime, loader } = await initAgent();
32
+
33
+ // 启动桌宠(首次需下载 Electron 二进制,须等下载完成再进入 REPL,避免卡在后台静默下载)
34
+ await startPet();
35
+
36
+ // Handle session resume from command line arg
37
+ // 只加载消息到 session,渲染交给 Repl.start() 处理(确保 logo 在历史记录之前)
38
+ // initialSessionPath 传给 Repl,退出时覆盖保存回原文件而非另存为新文件
39
+ let resumedMessages: any[] | null = null;
40
+ let initialSessionPath: string | null = null;
41
+ const resumeArg = process.argv.find((a) => a.startsWith("--resume="));
42
+ if (resumeArg) {
43
+ const sessionPath = resumeArg.split("=")[1];
44
+ try {
45
+ resumedMessages = loadSession(sessionPath);
46
+ session.agent.state.messages = resumedMessages;
47
+ initialSessionPath = sessionPath;
48
+ } catch (err) {
49
+ console.warn(chalk.yellow(t("恢复会话失败:", "Failed to resume: ") + (err instanceof Error ? err.message : err)));
50
+ }
51
+ }
52
+
53
+ // Start REPL
54
+ const repl = new Repl(
55
+ session,
56
+ modelRuntime,
57
+ loader,
58
+ () => {
59
+ // onExit - cleanup is handled in repl
60
+ },
61
+ async () => {
62
+ // onNewSession - dispose old session, recreate, and return it so
63
+ // the Repl can rebind its reference + renderer subscription.
64
+ session.dispose();
65
+ resetConversationFlag();
66
+ return await initAgent();
67
+ },
68
+ resumedMessages,
69
+ initialSessionPath,
70
+ );
71
+
72
+ await repl.start();
73
+ }
74
+
75
+ main().catch((err) => {
76
+ console.error(chalk.red(t("致命错误:", "Fatal error: ") + (err instanceof Error ? err.message : err)));
77
+ process.exit(1);
78
+ });
package/src/locale.ts ADDED
@@ -0,0 +1,131 @@
1
+ import { homedir } from "os";
2
+ import { join } from "path";
3
+ import { existsSync, readFileSync } from "fs";
4
+ import { execFileSync } from "child_process";
5
+
6
+ type Language = "zh" | "en";
7
+ export type LanguageSetting = "auto" | "zh" | "en";
8
+
9
+ const SETTINGS_FILE = join(homedir(), ".arona", "settings.json");
10
+
11
+ /**
12
+ * 读取 ~/.arona/settings.json 中的 language 字段("auto"|"zh"|"en")。
13
+ * 不 import config.ts —— config 在模块加载时读 settings,locale 再依赖它会成环。
14
+ * 解析失败或字段缺失返回 "auto"。
15
+ */
16
+ function readSettingsLanguage(): LanguageSetting {
17
+ try {
18
+ if (!existsSync(SETTINGS_FILE)) return "auto";
19
+ const s = JSON.parse(readFileSync(SETTINGS_FILE, "utf-8")) as { language?: unknown };
20
+ const v = s.language;
21
+ if (v === "zh" || v === "en") return v;
22
+ return "auto";
23
+ } catch {
24
+ return "auto";
25
+ }
26
+ }
27
+
28
+ /** 统一英文判定:locale 以 "en" 开头(大小写不敏感)。 */
29
+ function isEnglishLocale(locale: string): boolean {
30
+ return /^en/i.test(locale.trim());
31
+ }
32
+
33
+ /** 平台系统偏好检测(仅当环境变量缺失/未命中时调用一次)。失败静默返回 null。 */
34
+ function detectSystemLocale(): string | null {
35
+ const platform = process.platform;
36
+ try {
37
+ if (platform === "darwin") {
38
+ // macOS: 系统语言列表,如 ["en-US", "zh-Hans"],取首项
39
+ const out = execFileSync("defaults", ["read", "-g", "AppleLanguages"], {
40
+ encoding: "utf-8",
41
+ timeout: 3000,
42
+ }).trim();
43
+ const match = out.match(/["']([^"']+)["']/);
44
+ return match ? match[1] : null;
45
+ }
46
+ if (platform === "win32") {
47
+ // Windows: 注册表用户语言列表(分号分隔),取首个
48
+ try {
49
+ const out = execFileSync(
50
+ "reg",
51
+ ["query", "HKCU\\Control Panel\\International\\User Profile", "/v", "Languages"],
52
+ { encoding: "utf-8", timeout: 3000 },
53
+ );
54
+ const m = out.match(/LANGUAGES\s+REG_SZ\s+([^\r\n]+)/i);
55
+ if (m) {
56
+ const first = m[1].trim().split(";")[0];
57
+ if (first) return first;
58
+ }
59
+ } catch {
60
+ // 回退 PowerShell
61
+ }
62
+ const ps = execFileSync(
63
+ "powershell",
64
+ ["-NoProfile", "-Command", "[System.Globalization.CultureInfo]::CurrentUICulture.Name"],
65
+ { encoding: "utf-8", timeout: 5000 },
66
+ ).trim();
67
+ return ps || null;
68
+ }
69
+ // Linux 等:LANG 已在环境变量步骤覆盖,无需额外命令
70
+ return null;
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * 检测当前语言。优先级链:
78
+ * 1. settings.json 手动配置(zh/en 直接定型;auto 继续下探)
79
+ * 2. 环境变量 LANG / LC_ALL(/^en/i 命中 → en)
80
+ * 3. 平台系统偏好(macOS defaults / Windows 注册表 + CurrentUICulture)
81
+ * 4. Intl 兜底
82
+ * 5. 默认 zh
83
+ */
84
+ function detectLanguage(): Language {
85
+ // 1) 手动配置
86
+ const setting = readSettingsLanguage();
87
+ if (setting === "zh" || setting === "en") return setting;
88
+
89
+ // 2) 环境变量
90
+ const envLocale = process.env.LANG || process.env.LC_ALL || process.env.LC_MESSAGES || "";
91
+ if (envLocale && isEnglishLocale(envLocale)) return "en";
92
+ // 环境变量命中中文则直接定型,避免中文系统上再跑系统命令
93
+ if (/^zh/i.test(envLocale)) return "zh";
94
+
95
+ // 3) 平台系统偏好
96
+ const systemLocale = detectSystemLocale();
97
+ if (systemLocale) {
98
+ if (isEnglishLocale(systemLocale)) return "en";
99
+ if (/^zh/i.test(systemLocale)) return "zh";
100
+ }
101
+
102
+ // 4) Intl 兜底
103
+ try {
104
+ const intlLocale = Intl.DateTimeFormat().resolvedOptions().locale || "";
105
+ if (isEnglishLocale(intlLocale)) return "en";
106
+ if (/^zh/i.test(intlLocale)) return "zh";
107
+ } catch {
108
+ // ignore
109
+ }
110
+
111
+ // 5) 默认
112
+ return "zh";
113
+ }
114
+
115
+ // 模块加载时检测一次,全局缓存。运行时语言不热切换(符合"启动检测一次"的设计)。
116
+ let lang: Language = detectLanguage();
117
+
118
+ /** 按当前语言返回中英两版文案之一。 */
119
+ export function t(zh: string, en: string): string {
120
+ return lang === "en" ? en : zh;
121
+ }
122
+
123
+ /** 当前语言(只读)。 */
124
+ export function getLang(): Language {
125
+ return lang;
126
+ }
127
+
128
+ /** 手动切换语言(setup 向导改语言后即时生效)。 */
129
+ export function setLang(l: Language): void {
130
+ lang = l;
131
+ }
package/src/logo.ts ADDED
@@ -0,0 +1,120 @@
1
+ /**
2
+ * 开屏 ASCII 艺术字(动态生成)。
3
+ *
4
+ * 使用 figlet 根据终端宽度自动选择合适的字体,确保在各种终端宽度下
5
+ * 都能显示立体艺术字。配色保留 SCHALE 风格的蓝→浅蓝→青三段行级渐变。
6
+ */
7
+
8
+ import figlet from "figlet";
9
+
10
+ // 配色(与原 launcher.sh 一致)
11
+ const C = "\x1b[38;2;0;210;255m"; // CYAN #00D2FF 边框 + 底部字母
12
+ const B = "\x1b[38;2;66;135;245m"; // BLUE #4287F5 顶部字母
13
+ const L = "\x1b[38;2;100;180;255m"; // LIGHT_BLUE #64B4FF 中部字母
14
+ const R = "\x1b[0m"; // RESET
15
+
16
+ // 字体降级策略:ANSI Shadow 绝对优先,剩下立体优先、平面兜底
17
+ //
18
+ // 1) ANSI Shadow + 完整 "ARONA AGENT" (用户最想要的效果,~101 列)
19
+ // 2) ANSI Shadow + 缩短 "ARONA" (~53 列)
20
+ // 3) 其他 3D 立体字体(按宽度降序):每个字体都先试 "ARONA AGENT",装不下再试 "ARONA"
21
+ // 4) 平面字体(最后兜底):按宽度降序,每个字体也是先 AGENT 再 ARONA
22
+ //
23
+ // 列数估算已留出边框 2 格 + 缩进 2 格 + 内边距 6 格(即 width + 10)
24
+ const VARIANTS: { text: string; font: figlet.Fonts }[] = [
25
+ // —— ANSI Shadow 绝对优先 ——
26
+ { text: "ARONA AGENT", font: "ANSI Shadow" }, // ~101 列
27
+ { text: "ARONA", font: "ANSI Shadow" }, // ~53 列
28
+
29
+ // —— 其他 3D 立体字体(按宽度降序,AGENT → ARONA 配对) ——
30
+ { text: "ARONA AGENT", font: "Line Blocks" }, // ~100
31
+ { text: "ARONA", font: "Line Blocks" }, // ~55
32
+ { text: "ARONA AGENT", font: "Larry 3D 2" }, // ~96
33
+ { text: "ARONA", font: "Larry 3D 2" }, // ~54
34
+ { text: "ARONA AGENT", font: "Larry 3D" }, // ~96
35
+ { text: "ARONA", font: "Larry 3D" }, // ~54
36
+ { text: "ARONA AGENT", font: "Speed" }, // ~96
37
+ { text: "ARONA", font: "Speed" }, // ~49
38
+ { text: "ARONA AGENT", font: "Banner" }, // ~94
39
+ { text: "ARONA", font: "Banner" }, // ~51
40
+ { text: "ARONA AGENT", font: "Old Banner" }, // ~93
41
+ { text: "ARONA", font: "Old Banner" }, // ~50
42
+ { text: "ARONA AGENT", font: "Shadow" }, // ~91
43
+ { text: "ARONA", font: "Shadow" }, // ~50
44
+ { text: "ARONA AGENT", font: "JS Block Letters" }, // ~83
45
+ { text: "ARONA", font: "JS Block Letters" }, // ~46
46
+ { text: "ARONA AGENT", font: "Broadway KB" }, // ~79
47
+ { text: "ARONA", font: "Broadway KB" }, // ~43
48
+ { text: "ARONA AGENT", font: "Small Shadow" }, // ~75
49
+ { text: "ARONA", font: "Small Shadow" }, // ~42
50
+ { text: "ARONA AGENT", font: "Pyramid" }, // ~63
51
+ { text: "ARONA", font: "Pyramid" }, // ~35
52
+
53
+ // —— 平面字体兜底(按宽度降序,AGENT → ARONA 配对) ——
54
+ { text: "ARONA AGENT", font: "Big" }, // ~88
55
+ { text: "ARONA", font: "Big" }, // ~50
56
+ { text: "ARONA AGENT", font: "Slant" }, // ~79
57
+ { text: "ARONA", font: "Slant" }, // ~43
58
+ { text: "ARONA AGENT", font: "Standard" }, // ~79
59
+ { text: "ARONA", font: "Standard" }, // ~43
60
+ { text: "ARONA AGENT", font: "Small" }, // ~66
61
+ { text: "ARONA", font: "Small" }, // ~39
62
+ { text: "ARONA", font: "Mini" }, // ~34
63
+ { text: "ARONA", font: "Bubble" }, // ~32
64
+ { text: "ARONA", font: "Digital" }, // ~22
65
+ ];
66
+
67
+ /**
68
+ * 输出开屏 logo。根据终端宽度自动选择 figlet 字体,
69
+ * 确保艺术字不会超出终端宽度导致换行错乱。
70
+ */
71
+ export function printLogo(): void {
72
+ const cols = process.stdout.columns ?? 80;
73
+ const indent = " ";
74
+ const pad = 3; // 边框内 padding
75
+
76
+ for (const { text, font } of VARIANTS) {
77
+ try {
78
+ const rendered = figlet.textSync(text, { font });
79
+ let lines = rendered.split("\n");
80
+ // 去除首尾全空行
81
+ lines = lines.filter((l) => l.trim().length > 0);
82
+ if (lines.length === 0) continue;
83
+
84
+ const maxLen = Math.max(...lines.map((l) => l.length));
85
+ const innerWidth = maxLen + pad * 2; // 内容区宽度(不含 ║)
86
+ const totalWidth = innerWidth + 2 + indent.length; // +2 for ║║
87
+
88
+ if (totalWidth > cols) continue;
89
+
90
+ // 三段渐变:上 1/3 蓝、中 1/3 浅蓝、下 1/3 青
91
+ const n = lines.length;
92
+ const bEnd = Math.floor(n / 3);
93
+ const lEnd = Math.floor((n * 2) / 3);
94
+
95
+ const border = "═".repeat(innerWidth);
96
+ const blank = " ".repeat(innerWidth);
97
+ const padStr = " ".repeat(pad);
98
+
99
+ const out: string[] = [];
100
+ out.push(`${C}${indent}╔${border}╗${R}`);
101
+ out.push(`${C}${indent}║${blank}║${R}`);
102
+ lines.forEach((line, i) => {
103
+ const color = i < bEnd ? B : i < lEnd ? L : C;
104
+ const padded = line.padEnd(maxLen, " ");
105
+ out.push(`${C}${indent}║${padStr}${color}${padded}${C}${padStr}║${R}`);
106
+ });
107
+ out.push(`${C}${indent}║${blank}║${R}`);
108
+ out.push(`${C}${indent}╚${border}╝${R}`);
109
+
110
+ process.stdout.write("\n" + out.join("\n") + "\n\n");
111
+ return;
112
+ } catch {
113
+ // 字体不存在或渲染失败,尝试下一个
114
+ continue;
115
+ }
116
+ }
117
+
118
+ // 所有字体都放不下,退化为单行文字
119
+ process.stdout.write(`${C} ARONA AGENT${R}\n\n`);
120
+ }