codexpanel 0.1.8 → 0.1.10
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/README.md +4 -4
- package/README.zh.md +2 -2
- package/bin/codexpanel.cjs +69 -9
- package/docs/desktop-npx-install-flow.md +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# codexpanel
|
|
2
2
|
|
|
3
|
-
[
|
|
3
|
+
[涓枃鐗堟湰](README.zh.md)
|
|
4
4
|
|
|
5
5
|
codexpanel is a mobile control plane for managing local Codex work from a phone. It provides a Next.js web console, relay service, desktop agent, PostgreSQL-backed durable storage, shared protocol package, and reusable UI package in one monorepo.
|
|
6
6
|
|
|
@@ -59,7 +59,7 @@ Web/admin login uses a session cookie. Configure at least one of these before ex
|
|
|
59
59
|
```env
|
|
60
60
|
CODEXPANEL_ADMIN_USERNAME=admin
|
|
61
61
|
CODEXPANEL_ADMIN_USER_ID=admin
|
|
62
|
-
CODEXPANEL_ADMIN_PASSWORD=
|
|
62
|
+
CODEXPANEL_ADMIN_PASSWORD=Admin778899
|
|
63
63
|
CODEXPANEL_SESSION_SECRET=long_random_secret
|
|
64
64
|
CODEXPANEL_LOGIN_MAX_ATTEMPTS=8
|
|
65
65
|
CODEXPANEL_LOGIN_IP_MAX_ATTEMPTS=24
|
|
@@ -79,7 +79,7 @@ CODEXPANEL_REGISTRATION_ENABLED=false
|
|
|
79
79
|
|
|
80
80
|
Desktop setup uses browser login binding or a one-time terminal token. `CODEXPANEL_ADMIN_*` only bootstraps the first admin; additional users must be created through registration or admin operations.
|
|
81
81
|
|
|
82
|
-
`CODEXPANEL_REGISTRATION_ENABLED=true` opens self-service web registration. If the variable is omitted, development defaults to open and production defaults to closed; production onboarding must opt in explicitly. New users are stored in the existing relay users table with PBKDF2 password hashes
|
|
82
|
+
`CODEXPANEL_REGISTRATION_ENABLED=true` opens self-service web registration. If the variable is omitted, development defaults to open and production defaults to closed; production onboarding must opt in explicitly. New users are stored in the existing relay users table with PBKDF2 password hashes. Permissions are assigned through IAM permission groups, not account roles.
|
|
83
83
|
|
|
84
84
|
Desktop setup start requests are also rate-limited by source IP and capped by pending flow count. Tune `CODEXPANEL_DESKTOP_SETUP_*` only if installer traffic legitimately exceeds the defaults.
|
|
85
85
|
|
|
@@ -87,7 +87,7 @@ Desktop setup start requests are also rate-limited by source IP and capped by pe
|
|
|
87
87
|
|
|
88
88
|
Use `/admin/` with an admin session for first-batch testing:
|
|
89
89
|
|
|
90
|
-
- Enable/disable users,
|
|
90
|
+
- Enable/disable users, assign permission groups, and reset passwords. These actions invalidate existing local-user sessions.
|
|
91
91
|
- Recharge, gift, or deduct balance through the wallet adjustment action. Every change requires an idempotency key and is recorded in the user wallet ledger; do not edit `balance` directly.
|
|
92
92
|
- Grant, renew, or revoke monthly cards. Device quota is derived server-side from the user entitlement.
|
|
93
93
|
- Bind devices to users or disable devices from the admin device table. Ordinary users cannot operate another user's device, and disabled devices are blocked for non-admin operation.
|
package/README.zh.md
CHANGED
|
@@ -59,7 +59,7 @@ Web/admin 登录使用 session cookie。公网暴露 relay 前至少配置以下
|
|
|
59
59
|
```env
|
|
60
60
|
CODEXPANEL_ADMIN_USERNAME=admin
|
|
61
61
|
CODEXPANEL_ADMIN_USER_ID=admin
|
|
62
|
-
CODEXPANEL_ADMIN_PASSWORD=
|
|
62
|
+
CODEXPANEL_ADMIN_PASSWORD=Admin778899
|
|
63
63
|
CODEXPANEL_SESSION_SECRET=long_random_secret
|
|
64
64
|
CODEXPANEL_LOGIN_MAX_ATTEMPTS=8
|
|
65
65
|
CODEXPANEL_LOGIN_IP_MAX_ATTEMPTS=24
|
|
@@ -79,7 +79,7 @@ CODEXPANEL_REGISTRATION_ENABLED=false
|
|
|
79
79
|
|
|
80
80
|
电脑端 setup 支持浏览器登录绑定和终端一次性 token 登录。`CODEXPANEL_ADMIN_*` 只用于启动首个管理员;其他用户必须通过注册或后台运营创建。
|
|
81
81
|
|
|
82
|
-
`CODEXPANEL_REGISTRATION_ENABLED=true` 会打开自助注册。未配置该变量时,开发环境默认开放、生产环境默认关闭;生产环境如需开放注册必须显式设置为 `true`。新用户写入 relay 的 users 表,密码使用 PBKDF2 hash
|
|
82
|
+
`CODEXPANEL_REGISTRATION_ENABLED=true` 会打开自助注册。未配置该变量时,开发环境默认开放、生产环境默认关闭;生产环境如需开放注册必须显式设置为 `true`。新用户写入 relay 的 users 表,密码使用 PBKDF2 hash;后台权限只通过 IAM 权限组和 permission scopes 分配,不再使用账号角色。
|
|
83
83
|
|
|
84
84
|
电脑端 setup 启动请求会按来源 IP 限流,并受 pending flow 总量限制。只有安装流量确实超过默认值时,才需要调整 `CODEXPANEL_DESKTOP_SETUP_*`。
|
|
85
85
|
|
package/bin/codexpanel.cjs
CHANGED
|
@@ -11,7 +11,7 @@ const crypto = require("crypto");
|
|
|
11
11
|
const readline = require("readline");
|
|
12
12
|
const { spawn, spawnSync } = require("child_process");
|
|
13
13
|
|
|
14
|
-
const VERSION = "0.1.
|
|
14
|
+
const VERSION = "0.1.10";
|
|
15
15
|
const PROD_URL = "https://codexpanel.com";
|
|
16
16
|
const TEST_URL = "https://jd.6a.gs";
|
|
17
17
|
const LOCAL_HOST = "127.0.0.1";
|
|
@@ -34,6 +34,7 @@ Options:
|
|
|
34
34
|
--token-login Ask for a one-time terminal token from the setup page
|
|
35
35
|
--no-browser Do not open a browser; print the login URL instead
|
|
36
36
|
--no-autostart Do not enable Windows login autostart
|
|
37
|
+
--install-only Install/update the local agent and panel without binding
|
|
37
38
|
--dry-run Print resolved installer details without installing
|
|
38
39
|
--print-command Print the equivalent PowerShell bootstrap command after login approval
|
|
39
40
|
-h, --help Show help
|
|
@@ -59,6 +60,7 @@ function parseArgs(argv) {
|
|
|
59
60
|
autoStart: true,
|
|
60
61
|
dryRun: false,
|
|
61
62
|
printCommand: false,
|
|
63
|
+
installOnly: false,
|
|
62
64
|
};
|
|
63
65
|
|
|
64
66
|
const readValue = (flag) => {
|
|
@@ -80,6 +82,7 @@ function parseArgs(argv) {
|
|
|
80
82
|
else if (arg === "--token-login") options.tokenLogin = true;
|
|
81
83
|
else if (arg === "--no-browser") options.browser = false;
|
|
82
84
|
else if (arg === "--no-autostart") options.autoStart = false;
|
|
85
|
+
else if (arg === "--install-only") options.installOnly = true;
|
|
83
86
|
else if (arg === "--legacy-tunnel") throw new Error("--legacy-tunnel has been retired. Device traffic uses the server WSS gateway.");
|
|
84
87
|
else if (arg === "--no-tunnel") throw new Error("--no-tunnel has been retired. Device traffic uses the server WSS gateway.");
|
|
85
88
|
else if (arg === "--dry-run") options.dryRun = true;
|
|
@@ -364,6 +367,8 @@ async function preloadResources(relayUrl) {
|
|
|
364
367
|
let manifest = {};
|
|
365
368
|
try { manifest = JSON.parse(manifestText); }
|
|
366
369
|
catch { throw new Error("Server returned an invalid agent manifest."); }
|
|
370
|
+
const cacheKey = crypto.createHash("sha256").update(manifestText).digest("hex").slice(0, 16);
|
|
371
|
+
const preloadDir = path.join(localAgentRoot(), "preload", cacheKey);
|
|
367
372
|
const resources = [
|
|
368
373
|
manifest.hostRuntime,
|
|
369
374
|
manifest.hostRuntimeSchema,
|
|
@@ -376,9 +381,16 @@ async function preloadResources(relayUrl) {
|
|
|
376
381
|
const text = await downloadText(resourceUrl, 60000);
|
|
377
382
|
const hash = crypto.createHash("sha256").update(Buffer.from(text, "utf8")).digest("hex");
|
|
378
383
|
if (item.sha256 && hash !== item.sha256) throw new Error(`Checksum mismatch for ${item.fileName || item.path}`);
|
|
384
|
+
if (!item.dynamicAfterApproval && item.fileName) {
|
|
385
|
+
const target = path.join(preloadDir, ...String(item.fileName).split(/[\\/]+/));
|
|
386
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
387
|
+
fs.writeFileSync(target, text, "utf8");
|
|
388
|
+
}
|
|
379
389
|
}
|
|
380
390
|
console.log("CodexPanel: 资源已下载并校验完成 / Resources downloaded and verified.");
|
|
381
|
-
|
|
391
|
+
fs.mkdirSync(preloadDir, { recursive: true });
|
|
392
|
+
fs.writeFileSync(path.join(preloadDir, "manifest.json"), JSON.stringify(manifest, null, 2));
|
|
393
|
+
return { manifest, preloadDir };
|
|
382
394
|
}
|
|
383
395
|
|
|
384
396
|
function powershellCommand(url) {
|
|
@@ -427,14 +439,14 @@ async function tokenLogin(relayUrl, flowId, flowSecret) {
|
|
|
427
439
|
}
|
|
428
440
|
}
|
|
429
441
|
|
|
430
|
-
async function runBootstrap(relayUrl,
|
|
442
|
+
async function runBootstrap(relayUrl, profile, options, preloadDir = "") {
|
|
431
443
|
const url = new URL("/agent/bootstrap.ps1", relayUrl);
|
|
432
|
-
url.searchParams.set("setupToken", approval.setupToken);
|
|
433
444
|
url.searchParams.set("manualDeviceId", "1");
|
|
434
445
|
url.searchParams.set("manualDeviceName", "1");
|
|
435
|
-
url.searchParams.set("deviceId",
|
|
436
|
-
if (
|
|
446
|
+
url.searchParams.set("deviceId", profile.deviceId || stableDeviceId());
|
|
447
|
+
if (profile.deviceName) url.searchParams.set("deviceName", profile.deviceName);
|
|
437
448
|
if (options.workspace) url.searchParams.set("workspace", options.workspace);
|
|
449
|
+
if (preloadDir) url.searchParams.set("preloadDir", preloadDir);
|
|
438
450
|
url.searchParams.set("mode", "host-runtime");
|
|
439
451
|
url.searchParams.set("autoStart", options.autoStart ? "1" : "0");
|
|
440
452
|
|
|
@@ -453,9 +465,48 @@ async function runBootstrap(relayUrl, approval, options) {
|
|
|
453
465
|
if (result.status !== 0) throw new Error(`PowerShell installer exited with code ${result.status}`);
|
|
454
466
|
}
|
|
455
467
|
|
|
468
|
+
async function bindLocalAgent(approval) {
|
|
469
|
+
const root = localAgentRoot();
|
|
470
|
+
const script = path.join(root, "bind-agent.ps1");
|
|
471
|
+
if (!fs.existsSync(script)) throw new Error(`Local bind script not found: ${script}`);
|
|
472
|
+
const args = [
|
|
473
|
+
"-NoLogo", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", script,
|
|
474
|
+
"-SetupToken", approval.setupToken || "",
|
|
475
|
+
"-DeviceToken", approval.deviceToken || "",
|
|
476
|
+
"-UserId", approval.userId || "",
|
|
477
|
+
"-DeviceId", approval.deviceId || "",
|
|
478
|
+
];
|
|
479
|
+
if (approval.deviceName) args.push("-DeviceName", approval.deviceName);
|
|
480
|
+
const result = spawnSync(powershellPath(), args, { stdio: "inherit", windowsHide: false });
|
|
481
|
+
if (result.error) throw result.error;
|
|
482
|
+
if (result.status !== 0) throw new Error(`PowerShell bind script exited with code ${result.status}`);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function localProfileFromConfig(options, resolved) {
|
|
486
|
+
try {
|
|
487
|
+
const runtime = readLocalRuntimeConfig();
|
|
488
|
+
const config = runtime.config || {};
|
|
489
|
+
return {
|
|
490
|
+
...computerProfile(options, resolved),
|
|
491
|
+
deviceId: config.deviceId || config.agentId || stableDeviceId(),
|
|
492
|
+
deviceName: options.deviceName || config.deviceName || `${os.hostname()} / ${defaultUsername()} Codex Desktop Agent`,
|
|
493
|
+
workspace: options.workspace || config.workspace || process.env.USERPROFILE || process.cwd(),
|
|
494
|
+
};
|
|
495
|
+
} catch {
|
|
496
|
+
return computerProfile(options, resolved);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
456
500
|
async function install(options) {
|
|
501
|
+
if (options.command === "login" && !process.env.CODEXPANEL_SERVER) {
|
|
502
|
+
try {
|
|
503
|
+
const existing = readLocalRuntimeConfig().config;
|
|
504
|
+
if (existing.relayUrl && options.server === "production") options.server = existing.relayUrl;
|
|
505
|
+
} catch {}
|
|
506
|
+
}
|
|
457
507
|
const resolved = await resolveServer(options.server);
|
|
458
|
-
const
|
|
508
|
+
const loginOnly = options.command === "login";
|
|
509
|
+
const profile = loginOnly ? localProfileFromConfig(options, resolved) : computerProfile(options, resolved);
|
|
459
510
|
if (options.dryRun) {
|
|
460
511
|
console.log(JSON.stringify({
|
|
461
512
|
ok: true,
|
|
@@ -480,7 +531,16 @@ async function install(options) {
|
|
|
480
531
|
console.log(`Local relay port / 本地 relay 端口: ${resolved.localPort} (dynamic, not fixed)`);
|
|
481
532
|
}
|
|
482
533
|
|
|
483
|
-
|
|
534
|
+
let manifest = {};
|
|
535
|
+
if (!loginOnly) {
|
|
536
|
+
const preload = await preloadResources(resolved.relayUrl);
|
|
537
|
+
manifest = preload.manifest;
|
|
538
|
+
await runBootstrap(resolved.relayUrl, profile, options, preload.preloadDir);
|
|
539
|
+
}
|
|
540
|
+
if (options.installOnly) {
|
|
541
|
+
console.log("\nCodexPanel: local agent installed. Run `npx -y codexpanel login` to bind this device.");
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
484
544
|
const start = await requestJson("POST", resolved.relayUrl, "/api/desktop/setup/start", {
|
|
485
545
|
...profile,
|
|
486
546
|
agentVersion: manifest.agentVersion || "",
|
|
@@ -514,7 +574,7 @@ async function install(options) {
|
|
|
514
574
|
console.log("\nCodexPanel: 登录绑定成功 / Sign-in and binding approved.");
|
|
515
575
|
console.log(`User / 用户: ${approval.user?.name || approval.userId || "unknown"}`);
|
|
516
576
|
console.log(`Device / 设备: ${approval.deviceName || approval.deviceId}`);
|
|
517
|
-
await
|
|
577
|
+
await bindLocalAgent(approval);
|
|
518
578
|
if (approval.panelUrl) {
|
|
519
579
|
console.log(`CodexPanel local status panel / 本地状态面板: ${approval.panelUrl}`);
|
|
520
580
|
try { openBrowser(approval.panelUrl); } catch {}
|