Skip to content

Commit 6b3690b

Browse files
committed
feat(onboarding): usage
1 parent 44d8d85 commit 6b3690b

8 files changed

Lines changed: 112 additions & 44 deletions

File tree

src-api/src/core/openclaw-workspace-markdown.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { mkdir, access } from "node:fs/promises";
22
import { constants as fsConstants } from "node:fs";
33
import { homedir } from "node:os";
44
import { join } from "node:path";
5+
import { BotService } from "./bot-service";
6+
import { readGatewaySettings } from "./openclaw-config-file";
57

68
export type WorkspaceTemplateId = "export-owner" | "equipment-rental" | "platform-ops";
79

@@ -53,6 +55,8 @@ export interface ApplyWorkspaceTemplateResult {
5355
success: true;
5456
assistantId: string;
5557
assistantName: string;
58+
botId: string;
59+
botName: string;
5660
workspacePath: string;
5761
writtenFiles: WrittenWorkspaceFile[];
5862
warnings: string[];
@@ -74,6 +78,12 @@ const GENERATED_FILES = [
7478
"assistants/profile.md",
7579
];
7680

81+
const TEMPLATE_BOT_EMOJI: Record<WorkspaceTemplateId, string> = {
82+
"export-owner": "🌍",
83+
"equipment-rental": "🏗️",
84+
"platform-ops": "📈",
85+
};
86+
7787
function slugify(value: string): string {
7888
const normalized = value
7989
.trim()
@@ -472,10 +482,30 @@ export async function applyWorkspaceTemplate(
472482
});
473483
}
474484

485+
const gatewaySettings = await readGatewaySettings().catch(() => null);
486+
const defaultGatewayPort = gatewaySettings?.port ?? 18789;
487+
const defaultGatewayUrl = `ws://localhost:${defaultGatewayPort}/ws`;
488+
const defaultGatewayToken =
489+
gatewaySettings?.authMode === "token" ? gatewaySettings.authToken ?? undefined : undefined;
490+
const bot = BotService.create({
491+
name: assistantName,
492+
avatar_emoji: TEMPLATE_BOT_EMOJI[input.templateId],
493+
description: assistantGoal,
494+
skills_config: [],
495+
mcp_config: {},
496+
llm_config: {},
497+
openclaw_ws_url: defaultGatewayUrl,
498+
openclaw_ws_token: defaultGatewayToken,
499+
openclaw_agent_id: "main",
500+
is_active: true,
501+
});
502+
475503
return {
476504
success: true,
477505
assistantId,
478506
assistantName,
507+
botId: bot.id,
508+
botName: bot.name,
479509
workspacePath,
480510
writtenFiles,
481511
warnings: [],

src/components/StartupGuard.tsx

Lines changed: 31 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,48 +2,45 @@ import { useEffect, useState } from "react";
22
import { Navigate } from "react-router-dom";
33
import { apiClient } from "../shared/api-client";
44
import { setOnboardingRuntimeState } from "../shared/store/onboarding-runtime-store.ts";
5-
import { getOnboardingProgress, useWizardStore } from "../shared/store/wizard-store";
5+
import { getOnboardingProgress, isOnboardingComplete, useWizardStore } from "../shared/store/wizard-store";
66

7-
type CheckState = "loading" | "has-openclaw" | "no-openclaw" | "error";
7+
type CheckState = "loading" | "installed" | "resume-onboarding" | "start-onboarding" | "error";
88

99
export function StartupGuard() {
1010
const [state, setState] = useState<CheckState>("loading");
11-
const [hasProgress, setHasProgress] = useState(false);
11+
const [targetStep, setTargetStep] = useState("welcome");
1212

1313
useEffect(() => {
1414
let cancelled = false;
1515

16-
const progress = getOnboardingProgress();
17-
if (progress) {
18-
useWizardStore.getState().goToStep(progress.lastStepId);
19-
setHasProgress(true);
20-
setOnboardingRuntimeState({
21-
startupCheck: "unknown",
22-
hasOpenClaw: false,
23-
selectedTemplateId: null,
24-
initializedAssistantAt: null,
25-
assistantName: null,
26-
assistantWorkspacePath: null,
27-
});
28-
setState("no-openclaw");
29-
return () => {
30-
cancelled = true;
31-
};
32-
}
33-
3416
apiClient
3517
.get<{ hasOpenClaw: boolean }>("/openclaw/check-environment")
3618
.then((res) => {
3719
if (cancelled) return;
20+
21+
const progress = getOnboardingProgress();
22+
const completed = isOnboardingComplete();
23+
const nextStep = progress?.lastStepId ?? "welcome";
24+
3825
setOnboardingRuntimeState({
3926
startupCheck: "ready",
4027
hasOpenClaw: res.hasOpenClaw,
4128
selectedTemplateId: null,
4229
initializedAssistantAt: null,
4330
assistantName: null,
4431
assistantWorkspacePath: null,
32+
createdBotId: null,
33+
createdBotName: null,
4534
});
46-
setState(res.hasOpenClaw ? "has-openclaw" : "no-openclaw");
35+
36+
if (res.hasOpenClaw) {
37+
setState("installed");
38+
return;
39+
}
40+
41+
useWizardStore.getState().goToStep(nextStep);
42+
setTargetStep(nextStep);
43+
setState(progress || completed ? "resume-onboarding" : "start-onboarding");
4744
})
4845
.catch(() => {
4946
if (cancelled) return;
@@ -54,6 +51,8 @@ export function StartupGuard() {
5451
initializedAssistantAt: null,
5552
assistantName: null,
5653
assistantWorkspacePath: null,
54+
createdBotId: null,
55+
createdBotName: null,
5756
});
5857
setState("error");
5958
});
@@ -108,16 +107,19 @@ export function StartupGuard() {
108107
);
109108
}
110109

111-
if (state === "no-openclaw" || state === "has-openclaw" || state === "error") {
112-
if (!hasProgress) {
113-
useWizardStore.getState().goToStep("welcome");
114-
return <Navigate to="/onboarding/welcome" replace />;
115-
}
116-
const progress = getOnboardingProgress();
117-
const targetStep = progress?.lastStepId ?? "welcome";
110+
if (state === "installed") {
111+
return <Navigate to="/bots" replace />;
112+
}
113+
114+
if (state === "resume-onboarding") {
118115
return <Navigate to={`/onboarding/${targetStep}`} replace />;
119116
}
120117

118+
if (state === "start-onboarding") {
119+
useWizardStore.getState().goToStep("welcome");
120+
return <Navigate to="/onboarding/welcome" replace />;
121+
}
122+
121123
if (state === "error") {
122124
useWizardStore.getState().goToStep("welcome");
123125
return <Navigate to="/onboarding/welcome" replace />;

src/pages/OnboardingV2/Page1Welcome.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import { useMemo } from "react";
22
import { useNavigate } from "react-router-dom";
33
import { getOnboardingRuntimeState } from "../../shared/store/onboarding-runtime-store";
4+
import { isOnboardingComplete } from "../../shared/store/wizard-store";
45
import { OnboardingPageShell } from "./OnboardingPageShell";
56

67
export function OnboardingPage() {
78
const navigate = useNavigate();
89
const runtimeState = useMemo(() => getOnboardingRuntimeState(), []);
910
const hasOpenClaw = runtimeState.hasOpenClaw;
11+
const hadCompletedOnboarding = useMemo(() => isOnboardingComplete(), []);
12+
const needsReinstall = hadCompletedOnboarding && !hasOpenClaw;
1013
const footer = {
1114
hint: "你之后仍可从设置重新进入这条向导。",
1215
actions: [
@@ -86,16 +89,20 @@ export function OnboardingPage() {
8689
<section className="rounded-[24px] border border-[#E2E8F0] bg-[linear-gradient(180deg,#F8FAFC_0%,#FFFFFF_100%)] p-6 shadow-[0_18px_48px_rgba(15,23,42,0.06)]">
8790
<div className="text-[13px] font-semibold text-[#2563EB]">下一步</div>
8891
<div className="mt-3 text-[24px] font-semibold leading-[1.3] text-[#0F172A]">
89-
{hasOpenClaw ? "已检测到 OpenClaw" : "开始安装 OpenClaw"}
92+
{hasOpenClaw ? "已检测到 OpenClaw" : needsReinstall ? "需要重新安装 OpenClaw" : "开始安装 OpenClaw"}
9093
</div>
9194
<p className="mt-3 text-[14px] leading-7 text-[#64748B]">
9295
{hasOpenClaw
9396
? "你可以直接退出向导,或者重新进入安装流程。"
94-
: "当前还没有检测到 OpenClaw,建议先完成安装再继续。"}
97+
: needsReinstall
98+
? "检测到你之前已经完成过初始化,但当前没有找到 OpenClaw,可能已被手动卸载,建议先重新安装。"
99+
: "当前还没有检测到 OpenClaw,建议先完成安装再继续。"}
95100
</p>
96101

97102
<div className="mt-6 rounded-2xl border border-[#E2E8F0] bg-white px-4 py-4 text-[13px] leading-6 text-[#64748B]">
98-
这一步只做一个决定:继续安装,或者在已安装时直接退出向导。
103+
{needsReinstall
104+
? "这一步会引导你修复当前缺失的 OpenClaw 环境,完成后再继续后续配置。"
105+
: "这一步只做一个决定:继续安装,或者在已安装时直接退出向导。"}
99106
</div>
100107
</section>
101108
</OnboardingPageShell>

src/pages/OnboardingV2/Page4Template.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ export function OnboardingPage() {
6666
initializedAssistantAt: null,
6767
assistantName: runtimeState.assistantName,
6868
assistantWorkspacePath: null,
69+
createdBotId: null,
70+
createdBotName: null,
6971
});
7072
navigate("/onboarding/assistant");
7173
}

src/pages/OnboardingV2/Page5Assistant.tsx

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ import {
44
applyWorkspaceTemplate,
55
fetchWorkspaceTemplateSchema,
66
} from "../../shared/hooks/useOnboardingWorkspace";
7-
import { getTemplateMeta } from "../Onboarding/views/template-meta";
87
import {
98
getOnboardingRuntimeState,
109
setOnboardingRuntimeState,
1110
} from "../../shared/store/onboarding-runtime-store";
1211
import type { WorkspaceTemplateSchema } from "../../shared/types";
12+
import { getTemplateMeta } from "../Onboarding/views/template-meta";
1313
import { OnboardingPageShell } from "./OnboardingPageShell";
1414

1515
export function OnboardingPage() {
@@ -61,6 +61,8 @@ export function OnboardingPage() {
6161
initializedAssistantAt: Date.now(),
6262
assistantName: result.assistantName,
6363
assistantWorkspacePath: result.workspacePath,
64+
createdBotId: result.botId,
65+
createdBotName: result.botName,
6466
});
6567

6668
navigate("/onboarding/ready", { state: result });
@@ -87,13 +89,21 @@ export function OnboardingPage() {
8789
},
8890
variant: "primary" as const,
8991
disabled:
90-
isSaving || !assistantName.trim() || !assistantGoal.trim() || !toneStyle.trim() || !schema,
92+
isSaving ||
93+
!assistantName.trim() ||
94+
!assistantGoal.trim() ||
95+
!toneStyle.trim() ||
96+
!schema,
9197
},
9298
],
9399
};
94100

95101
return (
96-
<OnboardingPageShell footer={footer} mainClassName="items-start" contentClassName="max-w-[760px]">
102+
<OnboardingPageShell
103+
footer={footer}
104+
mainClassName="items-start"
105+
contentClassName="max-w-[760px]"
106+
>
97107
<section>
98108
<div className="inline-flex items-center rounded-full bg-[#EFF6FF] px-3 py-1 text-[11px] font-semibold text-[#2563EB]">
99109
Step 5 / 创建助手
@@ -103,7 +113,10 @@ export function OnboardingPage() {
103113
</h2>
104114
<p className="mt-3 text-[14px] leading-7 text-[#64748B]">
105115
当前模板为
106-
<span className="font-semibold text-[#0F172A]"> {templateMeta.icon} {templateMeta.name}</span>
116+
<span className="font-semibold text-[#0F172A]">
117+
{" "}
118+
{templateMeta.icon} {templateMeta.name}
119+
</span>
107120
。补齐助手名称、目标和协作风格后,系统会为它创建独立 workspace 并写入初始化 markdown。
108121
</p>
109122

@@ -123,7 +136,9 @@ export function OnboardingPage() {
123136
</div>
124137

125138
<div>
126-
<label className="block text-[13px] font-semibold text-[#0F172A]">你最希望它帮你推进什么</label>
139+
<label className="block text-[13px] font-semibold text-[#0F172A]">
140+
你最希望它帮你推进什么
141+
</label>
127142
<textarea
128143
value={assistantGoal}
129144
onChange={(event) => setAssistantGoal(event.target.value)}
@@ -134,7 +149,9 @@ export function OnboardingPage() {
134149
</div>
135150

136151
<div>
137-
<label className="block text-[13px] font-semibold text-[#0F172A]">你希望它的协作风格</label>
152+
<label className="block text-[13px] font-semibold text-[#0F172A]">
153+
你希望它的协作风格
154+
</label>
138155
<textarea
139156
value={toneStyle}
140157
onChange={(event) => setToneStyle(event.target.value)}
@@ -145,10 +162,6 @@ export function OnboardingPage() {
145162
</div>
146163
</div>
147164

148-
<div className="mt-4 rounded-2xl border border-[#E2E8F0] bg-[#F8FAFC] px-4 py-4 text-[13px] leading-7 text-[#64748B]">
149-
创建后会生成:AGENTS.md、SOUL.md、TOOLS.md、MEMORY.md 和助手 profile markdown。
150-
</div>
151-
152165
{error ? (
153166
<div className="mt-4 rounded-2xl border border-[#FECACA] bg-[#FEF2F2] px-4 py-3 text-[13px] leading-6 text-[#B91C1C]">
154167
{error}

src/pages/OnboardingV2/Page5Ready.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,13 @@ export function OnboardingPage() {
2222
Step 6 / 开始对话
2323
</div>
2424
<h2 className="mt-4 text-[28px] font-semibold leading-[1.15] tracking-[-0.03em] text-[#0F172A]">
25-
第一个助手已经初始化完成
25+
第一个助手和默认 Bot 已创建完成
2626
</h2>
2727
<p className="mt-3 text-[14px] leading-7 text-[#64748B]">
2828
<span className="font-semibold text-[#0F172A]">{result?.assistantName ?? runtimeState.assistantName ?? "我的助手"}</span>
29-
的专属 workspace 已创建完成,现在可以直接进入对话,也可以先查看生成结果。
29+
的专属 workspace 已创建完成,系统也已经自动生成第一个 Bot:
30+
<span className="font-semibold text-[#0F172A]"> {result?.botName ?? runtimeState.createdBotName ?? "默认 Bot"}</span>
31+
。现在可以直接进入对话,也可以先查看生成结果。
3032
</p>
3133

3234
<div className="mt-6 rounded-3xl border border-[#E2E8F0] bg-white p-5 shadow-[0_12px_30px_rgba(15,23,42,0.05)]">
@@ -36,6 +38,10 @@ export function OnboardingPage() {
3638
Workspace 路径:{result?.workspacePath ?? runtimeState.assistantWorkspacePath ?? "未记录"}
3739
</div>
3840

41+
<div className="mt-4 rounded-2xl border border-[#DCFCE7] bg-[#F0FDF4] px-4 py-4 text-[13px] leading-7 text-[#166534]">
42+
已创建 Bot:{result?.botName ?? runtimeState.createdBotName ?? "默认 Bot"}
43+
</div>
44+
3945
<div className="mt-4 rounded-2xl border border-[#E2E8F0] bg-[#F8FAFC] px-4 py-4 text-[13px] leading-7 text-[#64748B]">
4046
已生成文件:
4147
<div className="mt-2 space-y-1 text-[#0F172A]">

src/shared/store/onboarding-runtime-store.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ export interface OnboardingRuntimeState {
77
initializedAssistantAt: number | null;
88
assistantName: string | null;
99
assistantWorkspacePath: string | null;
10+
createdBotId: string | null;
11+
createdBotName: string | null;
1012
}
1113

1214
const STORAGE_KEY = "onboarding_runtime_state_v1";
@@ -18,6 +20,8 @@ const defaultState: OnboardingRuntimeState = {
1820
initializedAssistantAt: null,
1921
assistantName: null,
2022
assistantWorkspacePath: null,
23+
createdBotId: null,
24+
createdBotName: null,
2125
};
2226

2327
function canUseStorage() {
@@ -48,6 +52,8 @@ export function getOnboardingRuntimeState(): OnboardingRuntimeState {
4852
assistantName: typeof parsed.assistantName === "string" ? parsed.assistantName : null,
4953
assistantWorkspacePath:
5054
typeof parsed.assistantWorkspacePath === "string" ? parsed.assistantWorkspacePath : null,
55+
createdBotId: typeof parsed.createdBotId === "string" ? parsed.createdBotId : null,
56+
createdBotName: typeof parsed.createdBotName === "string" ? parsed.createdBotName : null,
5157
};
5258
} catch {
5359
return defaultState;

src/shared/types/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,8 @@ export interface WorkspaceInitResult {
233233
success: true;
234234
assistantId: string;
235235
assistantName: string;
236+
botId: string;
237+
botName: string;
236238
workspacePath: string;
237239
writtenFiles: Array<{
238240
kind: "agents" | "soul" | "tools" | "memory" | "assistant-profile";

0 commit comments

Comments
 (0)