Skip to content

Commit 48bf91e

Browse files
committed
Add localized runtime launcher and CLI entrypoint
1 parent 169511d commit 48bf91e

38 files changed

Lines changed: 1784 additions & 200 deletions

README.md

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ The codebase is feature-rich and the repository gate is now green locally. As of
1111
Latest local evidence on March 12, 2026 (Windows, Node `v20.11.0` with engine warning):
1212

1313
- `pnpm run build`: pass
14-
- `pnpm run test`: pass (`33` test files, `217` tests)
14+
- `pnpm run test`: pass (`36` test files, `225` tests)
1515
- `pnpm run test:integration`: pass (`6` test files, `20` tests)
1616
- `pnpm run verify`: pass
1717

@@ -94,6 +94,42 @@ Notes:
9494
- The gateway API runs on `http://localhost:4100` by default.
9595
- The standalone scheduler health endpoint runs on `http://localhost:4500/health` by default.
9696

97+
## One-Click Local Runtime
98+
99+
For a local all-in-one SenClaw session, use the launcher scripts:
100+
101+
```bash
102+
# Windows
103+
scripts\\start-senclaw.cmd
104+
scripts\\stop-senclaw.cmd
105+
106+
# Linux
107+
./scripts/start-senclaw.sh
108+
./scripts/stop-senclaw.sh
109+
```
110+
111+
The launcher bootstraps the local runtime under `.tmp/live-run`, starts the gateway and web console, reuses a persistent bootstrap admin key, and prints a startup banner with:
112+
113+
- current model ID
114+
- admin key
115+
- web console URL
116+
- gateway URL
117+
- runtime log directory
118+
119+
The web console header includes an `EN / 中文` locale toggle. The selected language is persisted and applied in two places:
120+
121+
- immediately for web console copy via browser storage
122+
- on the next launcher start via `.tmp/live-run/runtime-settings.json`
123+
124+
If you are already in the repository root on Windows `cmd`, you can use the shorter wrapper command directly:
125+
126+
```bat
127+
senclaw start
128+
senclaw stop
129+
```
130+
131+
This wrapper forwards to the local CLI in `packages/cli` and behaves like the launcher scripts.
132+
97133
## Authentication
98134

99135
Gateway API routes under `/api/v1/*` require a bearer API key by default.

README.zh-CN.md

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@
44

55
Senclaw 是一个 AI Agent 编排平台,具备持久化存储、API Key 鉴权、Web Console、连接器接入、任务调度以及沙箱化工具执行能力。
66

7-
当前代码库功能已经比较完整,本地仓库门禁也已经恢复为绿色。截至 2026 年 3 月 12 日,最新一次 Windows 本地验证中,`build``test``test:integration``verify` 都已通过。RabbitMQ 与 Redis Streams 队列驱动已经在仓库内实现,并带有单元测试覆盖及默认的 gateway 接线,但基于真实 broker 的发布级验证证据仍未补齐。同时,项目也已经记录了真实 OpenAI-compatible provider 的 smoke 结果,因此当前剩余的基础上线工作主要收敛为:在 Node 22 环境复验,以及补齐受保护 Web Console 的验收记录。
7+
当前代码库功能已经比较完整,仓库门禁也已在本地恢复为绿色。截止 2026 年 3 月 12 日,最新一次 Windows 本地验证中,`build``test``test:integration``verify` 均已通过。RabbitMQ 与 Redis Streams 队列驱动已经在仓库内实现,并带有单元测试覆盖和默认 gateway 接线,但基于真实 broker 的发布级验证证据仍待补齐。与此同时,项目也已经记录了真实 OpenAI-compatible provider 的 smoke 结果,因此当前剩余的基础上线工作主要收敛为:在 Node 22 环境复验,以及补齐受保护 Web Console 的验收记录。
88

99
## 就绪度快照
1010

1111
截至 2026 年 3 月 12 日的最新本地证据(Windows,Node `v20.11.0`,因此会出现 engine warning):
1212

1313
- `pnpm run build`:通过
14-
- `pnpm run test`:通过(`33` 个测试文件,`217` 个测试)
14+
- `pnpm run test`:通过(`36` 个测试文件,`225` 个测试)
1515
- `pnpm run test:integration`:通过(`6` 个测试文件,`20` 个测试)
1616
- `pnpm run verify`:通过
1717

@@ -24,7 +24,7 @@ Senclaw 是一个 AI Agent 编排平台,具备持久化存储、API Key 鉴权
2424

2525
## 仓库结构
2626

27-
应用
27+
Applications
2828

2929
- `@senclaw/gateway`:Fastify API 与管理入口
3030
- `@senclaw/agent-runner`:Agent 执行运行时
@@ -94,11 +94,47 @@ corepack pnpm --filter @senclaw/web dev
9494
- Gateway API 默认运行在 `http://localhost:4100`
9595
- 独立 scheduler 健康检查默认运行在 `http://localhost:4500/health`
9696

97+
## 一键本地运行
98+
99+
如果你想快速启动一套本地可用的 Senclaw,可直接使用启动脚本:
100+
101+
```bash
102+
# Windows
103+
scripts\start-senclaw.cmd
104+
scripts\stop-senclaw.cmd
105+
106+
# Linux
107+
./scripts/start-senclaw.sh
108+
./scripts/stop-senclaw.sh
109+
```
110+
111+
启动脚本会在 `.tmp/live-run` 下初始化本地运行目录,启动 gateway 和 web console,复用持久化 bootstrap admin key,并在终端打印启动摘要,包含:
112+
113+
- 当前模型 ID
114+
- admin key
115+
- Web Console 地址
116+
- gateway 地址
117+
- 运行日志目录
118+
119+
Web Console 头部右上角提供 `EN / 中文` 语言切换按钮。所选语言会持久化,并分别作用于:
120+
121+
- 当前浏览器里的 Web Console 文案
122+
- 下一次启动脚本运行时的终端输出文案(通过 `.tmp/live-run/runtime-settings.json`
123+
124+
如果你已经在仓库根目录的 Windows `cmd` 里,也可以直接使用更短的命令:
125+
126+
```bat
127+
senclaw start
128+
senclaw stop
129+
```
130+
131+
这个包装器会转发到 `packages/cli` 里的本地 CLI,行为和启动脚本一致。
132+
97133
## 鉴权
98134

99-
默认情况下,Gateway `/api/v1/*` 路由都需要 Bearer API Key。
135+
默认情况下,Gateway `/api/v1/*` 路由都需要 Bearer API Key。
100136

101-
创建或引导一个 key:
137+
创建或引导一把 key:
102138

103139
```bash
104140
corepack pnpm run auth:bootstrap-admin
@@ -108,7 +144,7 @@ Web Console 当前支持轻量级 API Key session。使用 agents、runs 或 tas
108144

109145
## 真实 Provider Smoke 测试
110146

111-
Senclaw 提供了一个不把密钥写入仓库的 OpenAI-compatible provider smoke 路径。在本地设置以下环境变量
147+
Senclaw 提供了一条不把密钥写入仓库的 OpenAI-compatible provider smoke 路径。先在本地设置以下环境变量
112148

113149
```bash
114150
SENCLAW_OPENAI_API_KEY=<你的 key>
@@ -150,7 +186,7 @@ corepack pnpm run verify
150186

151187
项目从“本地全绿”到“可以正式宣称部署就绪”之间,还剩这些事项:
152188

153-
- 在受支持的 Node 22 环境中重新跑一遍 readiness matrix
189+
- 在受支持的 Node 22 环境中重跑一遍 readiness matrix
154190
- 记录一次开启鉴权后的 Web Console 验收
155191
- 如果要对外宣称 broker-backed queue 已具备发布级支持,则需要补齐 RabbitMQ 和 Redis 的真实 broker 验证
156192
- 如果要对外宣称 level 4 native enforcement,则需要补齐 Windows 和 Linux 上基于证据的 Rust 沙箱验证

apps/gateway/src/plugins/auth.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ import type { ApiKeyService } from "../auth/api-key-service.js";
99
function isPublicRoute(url: string): boolean {
1010
const path = url.split("?")[0] ?? url;
1111
return (
12-
path === "/health" || path === "/metrics" || path.startsWith("/webhooks/")
12+
path === "/health" ||
13+
path === "/metrics" ||
14+
path === "/api/runtime/settings" ||
15+
path.startsWith("/webhooks/")
1316
);
1417
}
1518

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { ConsoleLocaleSchema } from "@senclaw/config";
2+
import type { FastifyInstance } from "fastify";
3+
import type { RuntimeSettingsStore } from "../runtime-settings.js";
4+
5+
export async function runtimeSettingsRoutes(
6+
app: FastifyInstance,
7+
opts: { store: RuntimeSettingsStore },
8+
): Promise<void> {
9+
const { store } = opts;
10+
11+
app.get("/", async () => {
12+
return store.get();
13+
});
14+
15+
app.put("/", async (request, reply) => {
16+
const body = request.body as Record<string, unknown> | undefined;
17+
const parsed = ConsoleLocaleSchema.safeParse(body?.locale);
18+
if (!parsed.success) {
19+
reply.status(400).send({
20+
error: "VALIDATION_ERROR",
21+
message: "Invalid runtime settings",
22+
details: parsed.error.issues.map((issue) => ({
23+
path: issue.path.join("."),
24+
message: issue.message,
25+
})),
26+
});
27+
return;
28+
}
29+
30+
return store.update({ locale: parsed.data });
31+
});
32+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import {
2+
readRuntimeSettings,
3+
type RuntimeSettings,
4+
writeRuntimeSettings,
5+
} from "@senclaw/config";
6+
7+
export interface RuntimeSettingsStore {
8+
get(): RuntimeSettings;
9+
update(next: Partial<RuntimeSettings>): RuntimeSettings;
10+
}
11+
12+
export function createRuntimeSettingsStore(
13+
settingsFile: string,
14+
): RuntimeSettingsStore {
15+
return {
16+
get() {
17+
return readRuntimeSettings(settingsFile);
18+
},
19+
update(next) {
20+
return writeRuntimeSettings(settingsFile, next);
21+
},
22+
};
23+
}

apps/gateway/src/server.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
InMemoryMessageRepository,
55
InMemoryRunRepository,
66
} from "@senclaw/agent-runner";
7-
import { loadConfig } from "@senclaw/config";
7+
import { loadConfig, resolveLocalRuntimeFiles } from "@senclaw/config";
88
import {
99
createChildLogger,
1010
createLogger,
@@ -45,12 +45,14 @@ import { healthRoutes } from "./routes/health.js";
4545
import { jobRoutes } from "./routes/jobs.js";
4646
import { keyRoutes } from "./routes/keys.js";
4747
import { runRoutes } from "./routes/runs.js";
48+
import { runtimeSettingsRoutes } from "./routes/runtime-settings.js";
4849
import { taskRoutes } from "./routes/tasks.js";
4950
import { webhookRoutes } from "./routes/webhooks.js";
5051
import {
5152
InMemoryExecutionRepository,
5253
InMemoryJobRepository,
5354
} from "./scheduler-repositories.js";
55+
import { createRuntimeSettingsStore } from "./runtime-settings.js";
5456

5557
function resolveMetricPath(request: import("fastify").FastifyRequest): string {
5658
const routePath =
@@ -100,6 +102,7 @@ export interface CreateServerOptions {
100102
loggerDestination?: DestinationStream;
101103
queueDriver?: QueueDriverLike;
102104
pollingFetcher?: PollingFetcherLike;
105+
runtimeSettingsPath?: string;
103106
}
104107

105108
const HIGH_VOLUME_PATHS = new Set(["/health", "/metrics"]);
@@ -223,6 +226,10 @@ export async function createServer(options: CreateServerOptions = {}): Promise<{
223226
const app = Fastify({
224227
logger: false,
225228
});
229+
const runtimeSettingsStore = createRuntimeSettingsStore(
230+
options.runtimeSettingsPath ??
231+
resolveLocalRuntimeFiles(process.cwd()).settingsFile,
232+
);
226233

227234
const requestSpans = new WeakMap<
228235
import("fastify").FastifyRequest,
@@ -580,6 +587,11 @@ export async function createServer(options: CreateServerOptions = {}): Promise<{
580587
webhookConnector,
581588
});
582589

590+
await app.register(runtimeSettingsRoutes, {
591+
prefix: "/api/runtime/settings",
592+
store: runtimeSettingsStore,
593+
});
594+
583595
await app.register(keyRoutes, {
584596
prefix: "/api/v1/keys",
585597
apiKeyService,
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { mkdtempSync, rmSync } from "node:fs";
2+
import { tmpdir } from "node:os";
3+
import { dirname, join } from "node:path";
4+
import type { FastifyInstance } from "fastify";
5+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
6+
import { createServer } from "../src/server.js";
7+
8+
describe("Gateway runtime settings", () => {
9+
let app: FastifyInstance;
10+
let runtimeSettingsPath: string;
11+
12+
beforeAll(async () => {
13+
const tempDir = mkdtempSync(join(tmpdir(), "senclaw-runtime-settings-"));
14+
runtimeSettingsPath = join(tempDir, "runtime-settings.json");
15+
16+
const server = await createServer({
17+
runtimeSettingsPath,
18+
});
19+
app = server.app;
20+
await app.ready();
21+
});
22+
23+
afterAll(async () => {
24+
await app.close();
25+
rmSync(dirname(runtimeSettingsPath), { recursive: true, force: true });
26+
});
27+
28+
it("returns the default locale when no runtime settings file exists", async () => {
29+
const response = await app.inject({
30+
method: "GET",
31+
url: "/api/runtime/settings",
32+
});
33+
34+
expect(response.statusCode).toBe(200);
35+
expect(response.json()).toEqual({
36+
locale: "en",
37+
});
38+
});
39+
40+
it("persists the selected locale for later startup scripts", async () => {
41+
const putResponse = await app.inject({
42+
method: "PUT",
43+
url: "/api/runtime/settings",
44+
payload: {
45+
locale: "zh-CN",
46+
},
47+
});
48+
49+
expect(putResponse.statusCode).toBe(200);
50+
expect(putResponse.json()).toEqual({
51+
locale: "zh-CN",
52+
});
53+
54+
const getResponse = await app.inject({
55+
method: "GET",
56+
url: "/api/runtime/settings",
57+
});
58+
59+
expect(getResponse.statusCode).toBe(200);
60+
expect(getResponse.json()).toEqual({
61+
locale: "zh-CN",
62+
});
63+
});
64+
});

apps/web/src/App.tsx

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Routes, Route } from "react-router-dom";
22
import { AppLayout } from "./components/AppLayout";
3+
import { LocaleProvider } from "./components/LocaleProvider";
34
import { AgentsList } from "./pages/AgentsList";
45
import { AgentDetail } from "./pages/AgentDetail";
56
import { AgentCreate } from "./pages/AgentCreate";
@@ -10,17 +11,19 @@ import { HealthDashboard } from "./pages/HealthDashboard";
1011

1112
export default function App() {
1213
return (
13-
<AppLayout>
14-
<Routes>
15-
<Route path="/" element={<AgentsList />} />
16-
<Route path="/agents" element={<AgentsList />} />
17-
<Route path="/agents/new" element={<AgentCreate />} />
18-
<Route path="/agents/:id" element={<AgentDetail />} />
19-
<Route path="/runs" element={<RunsList />} />
20-
<Route path="/runs/:id" element={<RunDetail />} />
21-
<Route path="/tasks/new" element={<TaskSubmit />} />
22-
<Route path="/health" element={<HealthDashboard />} />
23-
</Routes>
24-
</AppLayout>
14+
<LocaleProvider>
15+
<AppLayout>
16+
<Routes>
17+
<Route path="/" element={<AgentsList />} />
18+
<Route path="/agents" element={<AgentsList />} />
19+
<Route path="/agents/new" element={<AgentCreate />} />
20+
<Route path="/agents/:id" element={<AgentDetail />} />
21+
<Route path="/runs" element={<RunsList />} />
22+
<Route path="/runs/:id" element={<RunDetail />} />
23+
<Route path="/tasks/new" element={<TaskSubmit />} />
24+
<Route path="/health" element={<HealthDashboard />} />
25+
</Routes>
26+
</AppLayout>
27+
</LocaleProvider>
2528
);
2629
}

0 commit comments

Comments
 (0)