mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-03 11:10:23 +02:00
* refactor(onboarding): remove starter-content kit, unify install-runtime issue across mark-onboarded paths (MUL-2438) Drops the post-onboarding ImportStarterContent / DismissStarterContent flow (handler + routes + StarterContentPrompt + templates + locale strings + analytics event). The bug — web onboarding seeding 6+ starter issues without a runtime — only existed through that path; with it gone the source disappears. The "install a runtime" issue from BootstrapOnboardingNoRuntime is now the canonical no-runtime onboarding seed. The title/description and a LockAndFindActiveDuplicate-deduped seeder move to handler/no_runtime_issue.go, and CompleteOnboarding / CreateWorkspace / AcceptInvitation seed it whenever the workspace has no runtime yet, so every mark-onboarded entry point lands the user on a concrete next step. starter_content_state column is kept and continues to be claimed as 'imported' in all five entry points so older desktop builds (which still render the legacy dialog on NULL) don't surface it to accounts created after this change. Co-authored-by: multica-agent <github@multica.ai> * fix(onboarding): backfill starter_content_state for in-window NULL users (MUL-2438) 054 only covered pre-feature users. Anyone onboarded between then and the starter-content kit removal could still sit at NULL, and old desktop clients gate the legacy StarterContentPrompt on `starter_content_state IS NULL`. The import/dismiss routes are gone, so leaving these rows NULL would surface a dialog whose buttons 404. Mark them 'imported' to match the new helper's claim semantics. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai>
200 lines
7.0 KiB
Go
200 lines
7.0 KiB
Go
package handler
|
||
|
||
import (
|
||
"context"
|
||
"strings"
|
||
|
||
"github.com/jackc/pgx/v5/pgtype"
|
||
|
||
"github.com/multica-ai/multica/server/internal/issueguard"
|
||
db "github.com/multica-ai/multica/server/pkg/db/generated"
|
||
)
|
||
|
||
// noRuntimeIssueTitle and noRuntimeIssueDescription are the canonical
|
||
// "install your first runtime" issue. The text lives here so every mark-
|
||
// onboarded entry point (BootstrapOnboardingNoRuntime, CompleteOnboarding,
|
||
// CreateWorkspace, AcceptInvitation) seeds the same body.
|
||
const noRuntimeIssueTitle = "Connect a runtime to start using agents"
|
||
|
||
func noRuntimeIssueDescription(language pgtype.Text) string {
|
||
if language.Valid && strings.HasPrefix(language.String, "zh") {
|
||
return zhNoRuntimeIssueDescription()
|
||
}
|
||
return enNoRuntimeIssueDescription()
|
||
}
|
||
|
||
func enNoRuntimeIssueDescription() string {
|
||
return strings.Join([]string{
|
||
"Welcome to Multica.",
|
||
"",
|
||
"Agents need a runtime before they can execute work. You can still use Multica as a lightweight project-management workspace while you install one.",
|
||
"",
|
||
"## Try Multica first",
|
||
"",
|
||
"Before the runtime is ready, you can:",
|
||
"",
|
||
"1. Create a project for your current work.",
|
||
"2. Create a few issues and move them across backlog, todo, in_progress, and done.",
|
||
"3. Add priorities, labels, comments, and subscriptions.",
|
||
"4. Use Inbox to track assignments and mentions.",
|
||
"",
|
||
"That gives you the project-management layer first. Once a runtime is connected, agents can start working from the same issues.",
|
||
"",
|
||
"## Install your first agent runtime",
|
||
"",
|
||
"Full guide: https://multica.ai/docs/install-agent-runtime",
|
||
"",
|
||
"For English users, the fastest first path is Codex:",
|
||
"",
|
||
"1. Make sure Node.js is installed.",
|
||
"2. Install Codex:",
|
||
" npm i -g @openai/codex",
|
||
"3. Sign in:",
|
||
" codex",
|
||
"4. Confirm your terminal can find it:",
|
||
" which codex",
|
||
" codex --version",
|
||
"5. Restart the Multica daemon:",
|
||
" multica daemon restart",
|
||
" If you use the desktop app, restarting the app is enough.",
|
||
"6. Return to Runtimes and refresh. You should see a Codex runtime online.",
|
||
"7. Create your first agent from that runtime, then assign an issue to the agent and set status to todo.",
|
||
"",
|
||
"Codex reference: https://developers.openai.com/codex/cli",
|
||
"",
|
||
"When the runtime is connected, you can create Multica Helper for a guided first run.",
|
||
}, "\n")
|
||
}
|
||
|
||
func zhNoRuntimeIssueDescription() string {
|
||
return strings.Join([]string{
|
||
"欢迎来到 Multica。",
|
||
"",
|
||
"智能体需要先连上运行时才能执行工作。运行时还没准备好时,你也可以先把 Multica 当作轻量项目管理工具体验起来。",
|
||
"",
|
||
"## 先体验项目管理功能",
|
||
"",
|
||
"运行时安装前,你可以先做这些事:",
|
||
"",
|
||
"1. 为当前工作创建一个项目。",
|
||
"2. 新建几个 issue,并在 backlog、todo、in_progress、done 之间流转。",
|
||
"3. 给 issue 加优先级、标签、评论和订阅。",
|
||
"4. 用收件箱追踪分配给你的事项和 @mention。",
|
||
"",
|
||
"这样你先熟悉项目管理层。连上运行时后,智能体会直接在这些 issue 上开始工作。",
|
||
"",
|
||
"## 安装第一个 Agent 运行时",
|
||
"",
|
||
"完整文档:https://multica.ai/docs/install-agent-runtime",
|
||
"",
|
||
"中文用户建议先装 Kimi CLI:",
|
||
"",
|
||
"1. 在 macOS / Linux 终端安装 Kimi CLI:",
|
||
" curl -LsSf https://code.kimi.com/install.sh | bash",
|
||
" Windows PowerShell:",
|
||
" Invoke-RestMethod https://code.kimi.com/install.ps1 | Invoke-Expression",
|
||
"2. 确认终端能找到 Kimi:",
|
||
" kimi --version",
|
||
"3. 在你想让 Kimi 工作的项目目录里启动一次:",
|
||
" kimi",
|
||
"4. 首次启动后输入 /login,按提示完成 Kimi Code 或 API key 配置。",
|
||
"5. 重启 Multica 守护进程:",
|
||
" multica daemon restart",
|
||
" 如果你用桌面端,重启 app 即可。",
|
||
"6. 回到 Runtimes 页面刷新。你应该能看到一个在线的 Kimi 运行时。",
|
||
"7. 用这个运行时创建第一个智能体,再把一个 issue 分配给它,并把状态切到 todo。",
|
||
"",
|
||
"Kimi CLI 官方文档:https://moonshotai.github.io/kimi-cli/zh/guides/getting-started.html",
|
||
"",
|
||
"运行时连上后,你就可以创建 Multica Helper,开始一次有智能体参与的上手引导。",
|
||
}, "\n")
|
||
}
|
||
|
||
// seedInstallRuntimeIssue creates the install-runtime issue, deduping against
|
||
// existing active issues with the same title via pg_advisory_xact_lock so
|
||
// concurrent callers can't produce two copies. Must run inside a transaction.
|
||
func seedInstallRuntimeIssue(
|
||
ctx context.Context,
|
||
q *db.Queries,
|
||
workspaceID pgtype.UUID,
|
||
userID pgtype.UUID,
|
||
language pgtype.Text,
|
||
) (db.Issue, bool, error) {
|
||
var emptyUUID pgtype.UUID
|
||
existing, foundIssue, err := issueguard.LockAndFindActiveDuplicate(
|
||
ctx, q, workspaceID, emptyUUID, emptyUUID, noRuntimeIssueTitle, false,
|
||
)
|
||
if err != nil {
|
||
return db.Issue{}, false, err
|
||
}
|
||
if foundIssue {
|
||
return existing, false, nil
|
||
}
|
||
|
||
issueNumber, err := q.IncrementIssueCounter(ctx, workspaceID)
|
||
if err != nil {
|
||
return db.Issue{}, false, err
|
||
}
|
||
issue, err := q.CreateIssue(ctx, db.CreateIssueParams{
|
||
WorkspaceID: workspaceID,
|
||
Title: noRuntimeIssueTitle,
|
||
Description: strOrNullText(noRuntimeIssueDescription(language)),
|
||
Status: "todo",
|
||
Priority: "high",
|
||
AssigneeType: pgtype.Text{String: "member", Valid: true},
|
||
AssigneeID: userID,
|
||
CreatorType: "member",
|
||
CreatorID: userID,
|
||
ParentIssueID: emptyUUID,
|
||
Position: 0,
|
||
Number: issueNumber,
|
||
ProjectID: emptyUUID,
|
||
})
|
||
if err != nil {
|
||
return db.Issue{}, false, err
|
||
}
|
||
return issue, true, nil
|
||
}
|
||
|
||
// ensureNoRuntimeOnboardingIssue is the side-door wrapper used by
|
||
// CompleteOnboarding / CreateWorkspace / AcceptInvitation: it only seeds the
|
||
// install-runtime issue when the workspace has no agent_runtime yet.
|
||
// BootstrapOnboardingNoRuntime is the explicit "I skipped the runtime step"
|
||
// signal and bypasses this gate via seedInstallRuntimeIssue directly.
|
||
func ensureNoRuntimeOnboardingIssue(
|
||
ctx context.Context,
|
||
q *db.Queries,
|
||
workspaceID pgtype.UUID,
|
||
userID pgtype.UUID,
|
||
language pgtype.Text,
|
||
) (db.Issue, bool, error) {
|
||
runtimes, err := q.ListAgentRuntimes(ctx, workspaceID)
|
||
if err != nil {
|
||
return db.Issue{}, false, err
|
||
}
|
||
if len(runtimes) > 0 {
|
||
return db.Issue{}, false, nil
|
||
}
|
||
return seedInstallRuntimeIssue(ctx, q, workspaceID, userID, language)
|
||
}
|
||
|
||
// claimStarterContentStateIfUnset transitions starter_content_state from NULL
|
||
// to 'imported'. Kept after the starter-kit removal so older desktop builds —
|
||
// which still render the legacy import dialog when this column is NULL — skip
|
||
// the dialog on accounts created after the removal.
|
||
func claimStarterContentStateIfUnset(
|
||
ctx context.Context,
|
||
q *db.Queries,
|
||
userID pgtype.UUID,
|
||
current pgtype.Text,
|
||
) error {
|
||
if current.Valid {
|
||
return nil
|
||
}
|
||
_, err := q.SetStarterContentState(ctx, db.SetStarterContentStateParams{
|
||
ID: userID,
|
||
StarterContentState: pgtype.Text{String: "imported", Valid: true},
|
||
})
|
||
return err
|
||
}
|