galaxy-opc 0.1.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 (2) hide show
  1. package/bin/cli.mjs +389 -0
  2. package/package.json +35 -0
package/bin/cli.mjs ADDED
@@ -0,0 +1,389 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 星环 Galaxy OPC — CLI 入口
4
+ * 用法:
5
+ * npx galaxy-opc # 安装并初始化
6
+ * galaxy-opc # 全局安装后运行
7
+ * galaxy-opc setup # 重新运行配置向导
8
+ * galaxy-opc start # 启动服务
9
+ */
10
+
11
+ import fs from "node:fs";
12
+ import path from "node:path";
13
+ import os from "node:os";
14
+ import readline from "node:readline";
15
+ import { execSync, spawn } from "node:child_process";
16
+ import crypto from "node:crypto";
17
+
18
+ const REPO_URL = "https://github.com/P3ngSaM/galaxy-opc.git";
19
+ const DEFAULT_INSTALL_DIR = path.join(os.homedir(), "galaxy-opc");
20
+
21
+ // ─── 颜色工具 ───────────────────────────────────────────────────────────────
22
+ const c = {
23
+ reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m",
24
+ cyan: "\x1b[36m", green: "\x1b[32m", yellow: "\x1b[33m",
25
+ red: "\x1b[31m", gray: "\x1b[90m",
26
+ };
27
+ const bold = (s) => `${c.bold}${s}${c.reset}`;
28
+ const cyan = (s) => `${c.cyan}${s}${c.reset}`;
29
+ const green = (s) => `${c.green}${s}${c.reset}`;
30
+ const yellow = (s) => `${c.yellow}${s}${c.reset}`;
31
+ const red = (s) => `${c.red}${s}${c.reset}`;
32
+ const gray = (s) => `${c.gray}${s}${c.reset}`;
33
+ const dim = (s) => `${c.dim}${s}${c.reset}`;
34
+
35
+ // ─── readline 工具 ──────────────────────────────────────────────────────────
36
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
37
+ const ask = (q) => new Promise((res) => rl.question(q, (a) => res(a.trim())));
38
+
39
+ async function askChoice(prompt, options) {
40
+ while (true) {
41
+ console.log(`\n${bold(prompt)}`);
42
+ options.forEach((opt, i) => {
43
+ const label = opt.recommended ? `${opt.label} ${dim("(推荐)")}` : opt.label;
44
+ console.log(` ${cyan(String(i + 1))}. ${label}`);
45
+ if (opt.desc) console.log(` ${gray(opt.desc)}`);
46
+ });
47
+ const ans = await ask(`\n请输入选项编号 [1-${options.length}]: `);
48
+ const n = parseInt(ans);
49
+ if (n >= 1 && n <= options.length) return n - 1;
50
+ console.log(red(" 无效选项,请重试"));
51
+ }
52
+ }
53
+
54
+ async function askYesNo(question, defaultYes = true) {
55
+ const hint = defaultYes ? "[Y/n]" : "[y/N]";
56
+ const ans = await ask(`${question} ${gray(hint)}: `);
57
+ if (!ans) return defaultYes;
58
+ return ans.toLowerCase().startsWith("y");
59
+ }
60
+
61
+ function separator(char = "─", len = 60) { console.log(gray(char.repeat(len))); }
62
+
63
+ // ─── 工具函数 ───────────────────────────────────────────────────────────────
64
+ function ensureDir(p) { fs.mkdirSync(p, { recursive: true }); }
65
+
66
+ function readJson(p) {
67
+ if (!fs.existsSync(p)) return {};
68
+ try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch { return {}; }
69
+ }
70
+
71
+ function writeJson(p, obj) {
72
+ fs.writeFileSync(p, JSON.stringify(obj, null, 2) + "\n", "utf8");
73
+ }
74
+
75
+ function readEnv(p) {
76
+ if (!fs.existsSync(p)) return {};
77
+ const env = {};
78
+ for (const line of fs.readFileSync(p, "utf8").split("\n")) {
79
+ const m = line.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/);
80
+ if (m) env[m[1]] = m[2].replace(/^["']|["']$/g, "");
81
+ }
82
+ return env;
83
+ }
84
+
85
+ function writeEnv(p, envObj) {
86
+ const lines = Object.entries(envObj)
87
+ .filter(([, v]) => v !== undefined && v !== "")
88
+ .map(([k, v]) => `${k}=${v}`);
89
+ fs.writeFileSync(p, lines.join("\n") + "\n", "utf8");
90
+ }
91
+
92
+ function deepMerge(target, source) {
93
+ const result = { ...target };
94
+ for (const key of Object.keys(source)) {
95
+ if (source[key] && typeof source[key] === "object" && !Array.isArray(source[key]) &&
96
+ target[key] && typeof target[key] === "object" && !Array.isArray(target[key])) {
97
+ result[key] = deepMerge(target[key], source[key]);
98
+ } else {
99
+ result[key] = source[key];
100
+ }
101
+ }
102
+ return result;
103
+ }
104
+
105
+ function runCommand(cmd, args, options = {}) {
106
+ return new Promise((resolve, reject) => {
107
+ const proc = spawn(cmd, args, { stdio: "inherit", shell: process.platform === "win32", ...options });
108
+ proc.on("close", (code) => code === 0 ? resolve() : reject(new Error(`exit ${code}`)));
109
+ proc.on("error", reject);
110
+ });
111
+ }
112
+
113
+ function checkTool(cmd) {
114
+ try { execSync(`${cmd} --version`, { stdio: "ignore" }); return true; } catch { return false; }
115
+ }
116
+
117
+ // ─── 环境检查 ───────────────────────────────────────────────────────────────
118
+ function checkNodeVersion() {
119
+ const [major] = process.versions.node.split(".").map(Number);
120
+ if (major < 22) {
121
+ console.error(red(`\n 需要 Node.js >= 22,当前版本 v${process.versions.node}`));
122
+ console.error(gray(" 下载: https://nodejs.org/\n"));
123
+ process.exit(1);
124
+ }
125
+ }
126
+
127
+ // ─── 命令路由 ───────────────────────────────────────────────────────────────
128
+ const args = process.argv.slice(2);
129
+ const command = args[0] || "install";
130
+
131
+ if (command === "start") {
132
+ await cmdStart();
133
+ } else if (command === "setup") {
134
+ const installDir = await findInstallDir();
135
+ await cmdSetup(installDir);
136
+ } else {
137
+ // 默认:install + setup
138
+ await cmdInstall();
139
+ }
140
+
141
+ rl.close();
142
+
143
+ // ─── install 命令:下载项目 + 运行 setup ────────────────────────────────────
144
+ async function cmdInstall() {
145
+ console.clear();
146
+ console.log(`
147
+ ${bold(cyan(" ╔══════════════════════════════════════════════╗"))}
148
+ ${bold(cyan(" ║"))} ${bold("星环 Galaxy OPC")} — 安装向导 ${bold(cyan("║"))}
149
+ ${bold(cyan(" ║"))} ${dim("一人公司孵化与赋能平台")} ${bold(cyan("║"))}
150
+ ${bold(cyan(" ╚══════════════════════════════════════════════╝"))}
151
+ `);
152
+
153
+ // ── 步骤 1:环境检查 ────────────────────────────────────────────────────
154
+ separator();
155
+ console.log(bold(" 步骤 1 / 5 环境检查"));
156
+ separator();
157
+
158
+ checkNodeVersion();
159
+ console.log(green(` ✓ Node.js v${process.versions.node}`));
160
+
161
+ if (!checkTool("git")) {
162
+ console.error(red("\n ✗ 未检测到 git,请先安装: https://git-scm.com/\n"));
163
+ process.exit(1);
164
+ }
165
+ console.log(green(" ✓ git 已安装"));
166
+
167
+ if (!checkTool("pnpm")) {
168
+ console.log(yellow(" ! pnpm 未安装,正在自动安装..."));
169
+ execSync("npm install -g pnpm", { stdio: "inherit" });
170
+ console.log(green(" ✓ pnpm 安装完成"));
171
+ } else {
172
+ console.log(green(" ✓ pnpm 已安装"));
173
+ }
174
+
175
+ // ── 步骤 2:选择安装目录 ────────────────────────────────────────────────
176
+ separator();
177
+ console.log(bold(" 步骤 2 / 5 选择安装目录"));
178
+ separator();
179
+ console.log(gray(` 默认目录: ${DEFAULT_INSTALL_DIR}`));
180
+ const dirInput = await ask(` 安装到哪里?${gray("(直接回车使用默认)")}: `);
181
+ const installDir = dirInput || DEFAULT_INSTALL_DIR;
182
+
183
+ if (fs.existsSync(path.join(installDir, "openclaw"))) {
184
+ console.log(yellow(`\n 检测到 ${installDir} 已存在项目文件`));
185
+ const skip = await askYesNo(" 跳过下载,直接进入配置?", true);
186
+ if (skip) {
187
+ await cmdSetup(installDir);
188
+ return;
189
+ }
190
+ }
191
+
192
+ // ── 步骤 3:下载项目 ────────────────────────────────────────────────────
193
+ separator();
194
+ console.log(bold(" 步骤 3 / 5 下载项目"));
195
+ separator();
196
+ console.log(dim(` 正在从 GitHub 下载...\n`));
197
+
198
+ ensureDir(installDir);
199
+ try {
200
+ // --depth 1 只拉最新一个 commit,速度快
201
+ await runCommand("git", ["clone", "--depth", "1", REPO_URL, installDir]);
202
+ } catch {
203
+ // 目录非空时用 pull
204
+ try {
205
+ await runCommand("git", ["-C", installDir, "pull", "--depth", "1"]);
206
+ } catch (e) {
207
+ console.error(red(`\n ✗ 下载失败: ${e.message}`));
208
+ process.exit(1);
209
+ }
210
+ }
211
+ console.log(green("\n ✓ 项目下载完成"));
212
+
213
+ // ── 步骤 4:安装依赖 ────────────────────────────────────────────────────
214
+ separator();
215
+ console.log(bold(" 步骤 4 / 5 安装依赖"));
216
+ separator();
217
+ console.log(dim(" 运行 pnpm install ...\n"));
218
+ await runCommand("pnpm", ["install"], { cwd: path.join(installDir, "openclaw") });
219
+ console.log(green("\n ✓ 依赖安装完成"));
220
+
221
+ // ── 步骤 5:配置模型 ────────────────────────────────────────────────────
222
+ await cmdSetup(installDir);
223
+ }
224
+
225
+ // ─── setup 命令:配置 AI 模型 + 写入配置文件 ────────────────────────────────
226
+ async function cmdSetup(installDir) {
227
+ const HOME = os.homedir();
228
+ const STATE_DIR = path.join(HOME, ".openclaw");
229
+ const CONFIG_PATH = path.join(STATE_DIR, "openclaw.json");
230
+ const ENV_PATH = path.join(STATE_DIR, ".env");
231
+ const OPENCLAW_DIR = path.join(installDir, "openclaw");
232
+
233
+ separator();
234
+ console.log(bold(" 步骤 5 / 5 配置 AI 模型"));
235
+ separator();
236
+
237
+ ensureDir(STATE_DIR);
238
+ let newConfig = readJson(CONFIG_PATH);
239
+ let newEnv = readEnv(ENV_PATH);
240
+
241
+ // 一级:国产 / 海外 / 跳过
242
+ const regionIdx = await askChoice("选择 AI 模型地区", [
243
+ { label: "国产模型", desc: "通义千问 / MiniMax / 豆包 / Kimi / 百度千帆 / DeepSeek", recommended: true },
244
+ { label: "海外模型", desc: "OpenAI / Anthropic / OpenRouter" },
245
+ { label: "稍后手动配置", desc: `编辑 ${CONFIG_PATH}` },
246
+ ]);
247
+
248
+ let defaultModel = null;
249
+
250
+ if (regionIdx === 0) {
251
+ const cnIdx = await askChoice("选择国产模型服务商", [
252
+ { label: "通义千问 Qwen", desc: "qwen-max / qwen-plus — 免费额度多,支持 OAuth 扫码", recommended: true },
253
+ { label: "MiniMax", desc: "MiniMax-M2.1 — 200K 上下文,支持 OAuth 扫码" },
254
+ { label: "豆包 Doubao(火山引擎)", desc: "doubao-seed-1-8 / GLM-4.7 / Kimi-K2.5" },
255
+ { label: "Kimi(Moonshot AI)", desc: "kimi-k2.5 — 256K 上下文" },
256
+ ]);
257
+
258
+ if (cnIdx === 0) {
259
+ const m = await askChoice("Qwen 登录方式", [
260
+ { label: "OAuth 扫码登录", desc: "浏览器扫码,无需 API Key", recommended: true },
261
+ { label: "DashScope API Key", desc: "从 dashscope.aliyun.com 获取" },
262
+ ]);
263
+ if (m === 0) {
264
+ console.log(gray("\n 浏览器即将打开,扫码登录通义千问...\n"));
265
+ const doLogin = await askYesNo(" 现在执行登录?", true);
266
+ if (doLogin) {
267
+ try {
268
+ await runCommand("node", [path.join(OPENCLAW_DIR, "openclaw.mjs"), "models", "auth", "login", "--provider", "qwen-portal"], { cwd: OPENCLAW_DIR });
269
+ console.log(green("\n ✓ Qwen OAuth 登录成功"));
270
+ } catch {
271
+ console.log(yellow("\n ! 登录失败,稍后可手动运行:"));
272
+ console.log(gray(` node ${path.join(OPENCLAW_DIR, "openclaw.mjs")} models auth login --provider qwen-portal`));
273
+ }
274
+ }
275
+ defaultModel = "qwen-max";
276
+ newConfig = deepMerge(newConfig, { agents: { defaults: { model: "qwen-max", provider: "qwen-portal" } } });
277
+ } else {
278
+ const key = await ask("\n 请输入 DashScope API Key (sk-...): ");
279
+ if (key) { newEnv["DASHSCOPE_API_KEY"] = key; defaultModel = "qwen-plus"; newConfig = deepMerge(newConfig, { agents: { defaults: { model: "qwen-plus" } } }); console.log(green(" ✓ 已保存")); }
280
+ }
281
+ } else if (cnIdx === 1) {
282
+ const m = await askChoice("MiniMax 登录方式", [
283
+ { label: "OAuth 扫码登录", desc: "浏览器扫码,无需 API Key", recommended: true },
284
+ { label: "API Key", desc: "从 minimaxi.com 获取" },
285
+ ]);
286
+ if (m === 0) {
287
+ const doLogin = await askYesNo(" 现在执行 MiniMax 登录?", true);
288
+ if (doLogin) {
289
+ try {
290
+ await runCommand("node", [path.join(OPENCLAW_DIR, "openclaw.mjs"), "models", "auth", "login", "--provider", "minimax"], { cwd: OPENCLAW_DIR });
291
+ console.log(green("\n ✓ MiniMax OAuth 登录成功"));
292
+ } catch {
293
+ console.log(yellow("\n ! 登录失败,稍后可手动运行:"));
294
+ console.log(gray(` node ${path.join(OPENCLAW_DIR, "openclaw.mjs")} models auth login --provider minimax`));
295
+ }
296
+ }
297
+ defaultModel = "MiniMax-M2.1";
298
+ newConfig = deepMerge(newConfig, { agents: { defaults: { model: "MiniMax-M2.1", provider: "minimax" } } });
299
+ } else {
300
+ const key = await ask("\n 请输入 MiniMax API Key: ");
301
+ if (key) { newEnv["MINIMAX_API_KEY"] = key; defaultModel = "MiniMax-M2.1"; newConfig = deepMerge(newConfig, { agents: { defaults: { model: "MiniMax-M2.1" } } }); console.log(green(" ✓ 已保存")); }
302
+ }
303
+ } else if (cnIdx === 2) {
304
+ const modelIdx = await askChoice("选择豆包模型", [
305
+ { label: "doubao-seed-1-8(推荐)", desc: "256K 上下文,支持图片", recommended: true },
306
+ { label: "GLM-4.7", desc: "智谱 GLM,200K 上下文" },
307
+ { label: "Kimi-K2.5(火山版)", desc: "256K 上下文" },
308
+ ]);
309
+ const modelMap = ["doubao-seed-1-8-251228", "glm-4-7-251222", "kimi-k2-5-260127"];
310
+ const key = await ask("\n 请输入火山引擎 API Key (从 console.volcengine.com 获取): ");
311
+ if (key) { newEnv["VOLC_ACCESSKEY"] = key; defaultModel = modelMap[modelIdx]; newConfig = deepMerge(newConfig, { agents: { defaults: { model: defaultModel } } }); console.log(green(" ✓ 已保存")); }
312
+ } else {
313
+ const key = await ask("\n 请输入 Moonshot API Key (从 platform.moonshot.ai 获取): ");
314
+ if (key) { newEnv["MOONSHOT_API_KEY"] = key; defaultModel = "kimi-k2.5"; newConfig = deepMerge(newConfig, { agents: { defaults: { model: "kimi-k2.5" } } }); console.log(green(" ✓ 已保存")); }
315
+ }
316
+
317
+ // 附加备用
318
+ const addExtra = await askYesNo("\n 是否额外配置备用模型(DeepSeek / 百度千帆)?", false);
319
+ if (addExtra) {
320
+ const extraIdx = await askChoice("选择备用模型", [
321
+ { label: "DeepSeek", desc: "deepseek-chat — platform.deepseek.com" },
322
+ { label: "百度千帆", desc: "deepseek-v3 / ERNIE — qianfan.baidu.com" },
323
+ ]);
324
+ const key = await ask(` 请输入 ${extraIdx === 0 ? "DeepSeek" : "百度千帆"} API Key: `);
325
+ if (key) { newEnv[extraIdx === 0 ? "DEEPSEEK_API_KEY" : "QIANFAN_API_KEY"] = key; console.log(green(" ✓ 已保存")); }
326
+ }
327
+ } else if (regionIdx === 1) {
328
+ const intlIdx = await askChoice("选择海外模型", [
329
+ { label: "OpenAI", desc: "gpt-4o-mini", recommended: true },
330
+ { label: "Anthropic", desc: "claude-3-5-haiku-latest" },
331
+ { label: "OpenRouter", desc: "聚合多家,一个 Key — openrouter.ai" },
332
+ ]);
333
+ const prompts = ["OpenAI API Key (sk-...)", "Anthropic API Key (sk-ant-...)", "OpenRouter API Key (sk-or-...)"];
334
+ const envKeys = ["OPENAI_API_KEY", "ANTHROPIC_API_KEY", "OPENROUTER_API_KEY"];
335
+ const models = ["gpt-4o-mini", "claude-3-5-haiku-latest", "openai/gpt-4o-mini"];
336
+ const key = await ask(`\n 请输入 ${prompts[intlIdx]}: `);
337
+ if (key) { newEnv[envKeys[intlIdx]] = key; defaultModel = models[intlIdx]; newConfig = deepMerge(newConfig, { agents: { defaults: { model: defaultModel } } }); console.log(green(" ✓ 已保存")); }
338
+ } else {
339
+ console.log(yellow(`\n 已跳过,稍后手动编辑: ${gray(CONFIG_PATH)}`));
340
+ }
341
+
342
+ // Gateway Token + 插件路径
343
+ if (!newEnv["OPENCLAW_GATEWAY_TOKEN"] || newEnv["OPENCLAW_GATEWAY_TOKEN"] === "change-me-to-a-long-random-token") {
344
+ newEnv["OPENCLAW_GATEWAY_TOKEN"] = crypto.randomBytes(32).toString("hex");
345
+ console.log(green("\n ✓ 已自动生成 Gateway 访问令牌"));
346
+ }
347
+
348
+ newConfig = deepMerge(newConfig, {
349
+ plugins: { load: { dirs: [path.join(OPENCLAW_DIR, "extensions", "opc-platform")] } },
350
+ });
351
+
352
+ writeJson(CONFIG_PATH, newConfig);
353
+ writeEnv(ENV_PATH, newEnv);
354
+
355
+ // ── 完成提示 ──────────────────────────────────────────────────────────────
356
+ separator("═");
357
+ console.log(`
358
+ ${bold(green("安装完成!"))}
359
+
360
+ 启动命令:
361
+ ${cyan(`cd ${path.join(installDir, "openclaw")} && npm start`)}
362
+
363
+ 管理后台:
364
+ ${cyan("http://localhost:18789/opc/admin")}
365
+ `);
366
+ if (defaultModel) console.log(` 当前模型: ${cyan(defaultModel)}\n`);
367
+ separator("═");
368
+ }
369
+
370
+ // ─── start 命令 ──────────────────────────────────────────────────────────────
371
+ async function cmdStart() {
372
+ const installDir = await findInstallDir();
373
+ const openclawDir = path.join(installDir, "openclaw");
374
+ if (!fs.existsSync(openclawDir)) {
375
+ console.error(red("\n ✗ 未找到项目,请先运行 npx galaxy-opc\n"));
376
+ process.exit(1);
377
+ }
378
+ console.log(cyan("\n 启动星环 Galaxy OPC...\n"));
379
+ await runCommand("node", ["scripts/run-node.mjs"], { cwd: openclawDir });
380
+ }
381
+
382
+ async function findInstallDir() {
383
+ // 优先检查常见位置
384
+ for (const dir of [DEFAULT_INSTALL_DIR, process.cwd()]) {
385
+ if (fs.existsSync(path.join(dir, "openclaw"))) return dir;
386
+ }
387
+ const ans = await ask(` 请输入安装目录 ${gray(`(默认 ${DEFAULT_INSTALL_DIR})`)}: `);
388
+ return ans || DEFAULT_INSTALL_DIR;
389
+ }
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "galaxy-opc",
3
+ "version": "0.1.0",
4
+ "description": "星环 Galaxy OPC — 一人公司孵化与赋能平台 AI 员工系统",
5
+ "keywords": [
6
+ "ai",
7
+ "opc",
8
+ "one-person-company",
9
+ "openclaw",
10
+ "qwen",
11
+ "llm",
12
+ "business",
13
+ "startup"
14
+ ],
15
+ "homepage": "https://github.com/P3ngSaM/galaxy-opc",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/P3ngSaM/galaxy-opc.git"
19
+ },
20
+ "bugs": {
21
+ "url": "https://github.com/P3ngSaM/galaxy-opc/issues"
22
+ },
23
+ "license": "MIT",
24
+ "author": "p3ngsam",
25
+ "type": "module",
26
+ "bin": {
27
+ "galaxy-opc": "./bin/cli.mjs"
28
+ },
29
+ "files": [
30
+ "bin/"
31
+ ],
32
+ "engines": {
33
+ "node": ">=22.0.0"
34
+ }
35
+ }