Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ bin/
- **Hub→Edge request-response 通用模式** — `src/hub/pending.ts` 提供 `PendingRequests<T>` 泛型工具,基于 token 匹配 WS 异步请求与响应。spawn 和目录浏览(browse)均使用此模式
- **目录浏览 API** — `GET /api/edges/:edgeId/browse?path=xxx`,Hub 转发给 Edge 执行 readdir。Edge 解析 parent/prefix(路径以 `/` 结尾列全部,否则以末段为前缀过滤),仅返回子目录,默认隐藏 dotfiles(前缀以 `.` 开头时显示),最多 200 条(截断时标记 `truncated`)。前端据此实现类 VS Code 的路径补全选择器
- **bind 地址与安全** — Hub 和 Edge 默认 bind `127.0.0.1`(仅本机)。`--host` 可指定;非 loopback 时 CLI 和日志都会警告
- **子路径部署(Base Path)** — Hub 支持通过 `--base-path /paimon` 部署到反向代理子路径下。优先级:`PAIMON_BASE_PATH` 环境变量 > `--base-path` 参数 > hub.json 继承 > 默认 `/`。Vite 构建使用 `base: './'`(相对路径),产物路径无关;Hub 运行时在返回 index.html 时动态注入 `<base href>` 和 `window.__BASE_PATH__`,前端通过 `src/web/src/utils/basePath.ts` 读取并用于 React Router basename、API/WS 路径前缀。Hub 自身会 strip basePath 前缀(路径以 basePath 开头则移除,否则保持原样),因此无需依赖反向代理 strip,直接访问和经 nginx 转发均可工作
- **Access Token 认证** — Hub 启动时生成或接收 access token(优先级:`PAIMON_ACCESS_TOKEN` 环境变量 > `--token` 参数 > 自动生成),写入 `hub.json`。Edge/Browser/HTTP API 连接 Hub 时必须携带 token(WS 通过 `?token=xxx`,HTTP 通过 `Authorization: Bearer xxx`)。`/api/health` 不需认证。`PAIMON_AUTH_DISABLED=1` 可关闭认证(仅开发调试)
- **Token 生命周期** — token 存储于 `hub.json`,随 `paimon hub stop` 删除而失效。`paimon hub restart` 默认继承旧 token(显示来源为 `inherited`)。Pi Extension → Edge 不需认证(Edge 仅 bind loopback,天然同机信任)
- **Edge token 来源** — 优先级:`PAIMON_ACCESS_TOKEN` 环境变量 > `--token` 参数 > 同机 hub.json fallback
Expand Down
16 changes: 15 additions & 1 deletion src/cli/commands/hub/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,18 @@ export function registerHubCommand(program: Command): void {
.option("--port <port>", "port number", String(DEFAULTS.PORT))
.option("--host <host>", "bind address", DEFAULTS.HOST)
.option("--token <token>", "access token (default: auto-generate)")
.option(
"--base-path <path>",
"base path for sub-path deployment (e.g. /paimon)",
)
.action(async (opts) => {
const { handleStart } = await import("./start");
await handleStart(parseInt(opts.port), opts.host, opts.token);
await handleStart(
parseInt(opts.port),
opts.host,
opts.token,
opts.basePath,
);
});

hub
Expand All @@ -31,12 +40,17 @@ export function registerHubCommand(program: Command): void {
.option("--port <port>", "port number")
.option("--host <host>", "bind address")
.option("--token <token>", "access token (default: auto-generate)")
.option(
"--base-path <path>",
"base path for sub-path deployment (e.g. /paimon)",
)
.action(async (opts) => {
const { handleRestart } = await import("./restart");
await handleRestart(
opts.port ? parseInt(opts.port) : undefined,
opts.host,
opts.token,
opts.basePath,
);
});

Expand Down
4 changes: 3 additions & 1 deletion src/cli/commands/hub/restart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ export async function handleRestart(
port: number | undefined,
host: string | undefined,
token?: string,
basePath?: string,
): Promise<void> {
// 先读取当前状态(stop 会清理状态文件,必须先读)
const prevState = await readHubState();

// 未指定则继承之前的值,兜底用默认值
const finalPort = port ?? prevState?.port ?? DEFAULTS.PORT;
const finalHost = host ?? prevState?.host ?? DEFAULTS.HOST;
const finalBasePath = basePath ?? prevState?.basePath;

if (isNaN(finalPort) || finalPort < 1 || finalPort > 65535) {
console.error("Invalid port number");
Expand All @@ -35,5 +37,5 @@ export async function handleRestart(
} else if (prevState?.accessToken) {
tokenOption = { token: prevState.accessToken, source: "inherited" };
}
await startDaemon(finalPort, finalHost, tokenOption);
await startDaemon(finalPort, finalHost, tokenOption, finalBasePath);
}
3 changes: 2 additions & 1 deletion src/cli/commands/hub/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export async function handleStart(
port: number,
host: string,
token?: string,
basePath?: string,
): Promise<void> {
if (isNaN(port) || port < 1 || port > 65535) {
console.error("Invalid port number");
Expand All @@ -14,5 +15,5 @@ export async function handleStart(
const tokenOption: TokenOption | undefined = token
? { token, source: "--token" }
: undefined;
await startDaemon(port, host, tokenOption);
await startDaemon(port, host, tokenOption, basePath);
}
4 changes: 3 additions & 1 deletion src/cli/commands/hub/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ export async function handleStatus(): Promise<void> {
console.log(`Hub is running`);
console.log(` PID: ${state.pid}`);
console.log(` Bind: ${state.host}:${state.port}`);
console.log(` URL: http://${displayHost}:${state.port}`);
console.log(
` URL: http://${displayHost}:${state.port}${state.basePath ?? ""}`,
);
console.log(` Token: ${maskToken(state.accessToken)}`);
} else {
console.log("Hub is not running");
Expand Down
13 changes: 12 additions & 1 deletion src/cli/daemon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { join, resolve } from "node:path";
import { mkdir, unlink, rename } from "node:fs/promises";
import { openSync, closeSync } from "node:fs";
import { normalizeBasePath } from "../utils/basePath";
import { DEFAULTS } from "../protocol/types";
import type { HubState } from "../protocol/types";
import {
Expand Down Expand Up @@ -90,7 +91,15 @@ export async function startDaemon(
port: number,
host: string,
tokenOption?: TokenOption,
rawBasePath?: string,
): Promise<void> {
let basePath: string | undefined;
try {
basePath = normalizeBasePath(rawBasePath);
} catch (err) {
console.error((err as Error).message);
process.exit(1);
}
// 检查是否已在运行
const existing = await readHubState();
if (existing && isProcessAlive(existing.pid)) {
Expand Down Expand Up @@ -128,6 +137,7 @@ export async function startDaemon(
PAIMON_PORT: String(port),
PAIMON_HOST: host,
PAIMON_ACCESS_TOKEN: accessToken,
...(basePath ? { PAIMON_BASE_PATH: basePath } : {}),
},
stdin: "ignore",
// stdout/stderr 仅作为 crash 兜底,正常结构化日志走 rotating-file-stream
Expand Down Expand Up @@ -180,10 +190,11 @@ export async function startDaemon(
host,
startedAt: new Date().toISOString(),
accessToken,
...(basePath ? { basePath } : {}),
});

console.log(`Hub started (PID: ${child.pid}, port: ${port}, host: ${host})`);
console.log(` Web UI: http://${healthHost}:${port}`);
console.log(` Web UI: http://${healthHost}:${port}${basePath ?? ""}`);
// 有意打印完整 token:用户首次启动时需要复制 token 用于 Web 登录和 Edge 配置
console.log(` Token: ${accessToken} (${tokenSource})`);
console.log(` Logs: ${getMainLogPath(DEFAULTS.HUB_LOG_NAME)}`);
Expand Down
Loading