mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-21 14:44:30 +02:00
Compare commits
5 Commits
chore/remo
...
feat/cloud
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fdf44a453b | ||
|
|
afb199d7bf | ||
|
|
86d309dbb8 | ||
|
|
b604c6d9b0 | ||
|
|
6cd03f24a0 |
49
.env.example
49
.env.example
@@ -40,12 +40,11 @@ JWT_SECRET=change-me-in-production
|
||||
# MULTICA_APP_URL=http://localhost:3000
|
||||
# Public URL the API is reachable at from the open internet (no trailing
|
||||
# slash). Used to mint absolute webhook URLs for autopilot webhook
|
||||
# triggers and to show correct daemon setup commands in the web UI. Leave
|
||||
# unset behind a same-origin reverse proxy or for plain localhost dev —
|
||||
# the frontend will compose the URL from window.origin + webhook_path in
|
||||
# that case. Headers are intentionally not used to derive this value, to
|
||||
# avoid Host / X-Forwarded-Host spoofing when a self-hosted reverse proxy
|
||||
# is not hardened.
|
||||
# triggers. Leave unset behind a same-origin reverse proxy or for plain
|
||||
# localhost dev — the frontend will compose the URL from
|
||||
# window.origin + webhook_path in that case. Headers are intentionally
|
||||
# not used to derive this value, to avoid Host / X-Forwarded-Host
|
||||
# spoofing when a self-hosted reverse proxy is not hardened.
|
||||
MULTICA_PUBLIC_URL=
|
||||
# Comma-separated CIDR list of reverse proxies whose X-Forwarded-For /
|
||||
# X-Real-IP headers the per-IP webhook rate limiter is allowed to trust.
|
||||
@@ -89,22 +88,11 @@ RESEND_FROM_EMAIL=noreply@multica.ai
|
||||
# Takes priority over Resend when SMTP_HOST is set.
|
||||
# Supports unauthenticated relay (leave SMTP_USERNAME empty) and authenticated SMTP.
|
||||
# Set SMTP_TLS_INSECURE=true only for private CA or self-signed certificates.
|
||||
# SMTP_TLS controls the TLS mode:
|
||||
# - unset / "starttls" (default): plaintext connect, upgrade via STARTTLS.
|
||||
# - "implicit" (aliases: "smtps", "ssl"): TLS handshake on connect (SMTPS).
|
||||
# Required by providers that only offer port 465 and do not advertise
|
||||
# STARTTLS (e.g. Aliyun enterprise mail). Auto-enabled when SMTP_PORT=465
|
||||
# and SMTP_TLS is unset.
|
||||
# SMTP_EHLO_NAME is the EHLO/HELO name announced to the relay. Defaults to the
|
||||
# machine hostname; set a real FQDN when a strict relay (e.g. Google Workspace
|
||||
# smtp-relay.gmail.com) rejects the default and the connection drops as an EOF.
|
||||
SMTP_HOST=
|
||||
SMTP_PORT=25
|
||||
SMTP_USERNAME=
|
||||
SMTP_PASSWORD=
|
||||
SMTP_TLS_INSECURE=false
|
||||
SMTP_TLS=
|
||||
SMTP_EHLO_NAME=
|
||||
|
||||
# Google OAuth
|
||||
# The web login page reads GOOGLE_CLIENT_ID from /api/config at runtime, so
|
||||
@@ -122,13 +110,6 @@ GOOGLE_CLIENT_SECRET=
|
||||
# from S3_BUCKET + S3_REGION. S3_REGION must match the bucket's real region.
|
||||
S3_BUCKET=
|
||||
S3_REGION=us-west-2
|
||||
# AWS_ENDPOINT_URL — optional S3-compatible endpoint (MinIO, RustFS, R2, etc.).
|
||||
# For internal Docker/VPC hosts such as http://rustfs:9000, leave
|
||||
# ATTACHMENT_DOWNLOAD_MODE=auto or set proxy explicitly so browsers/CLI do
|
||||
# not need direct access to the object store.
|
||||
AWS_ENDPOINT_URL=
|
||||
ATTACHMENT_DOWNLOAD_MODE=auto
|
||||
ATTACHMENT_DOWNLOAD_URL_TTL=30m
|
||||
CLOUDFRONT_KEY_PAIR_ID=
|
||||
CLOUDFRONT_PRIVATE_KEY_SECRET=multica/cloudfront-signing-key
|
||||
CLOUDFRONT_PRIVATE_KEY=
|
||||
@@ -200,18 +181,6 @@ CORS_ALLOWED_ORIGINS=
|
||||
GITHUB_APP_SLUG=
|
||||
GITHUB_WEBHOOK_SECRET=
|
||||
|
||||
# Lark / Feishu bot integration (Settings → Integrations "Bind to Lark")
|
||||
# Off until MULTICA_LARK_SECRET_KEY is set — a base64-encoded 32-byte key
|
||||
# that encrypts each Bot's app secret at rest. Leave empty to disable.
|
||||
# Generate one with: openssl rand -base64 32
|
||||
MULTICA_LARK_SECRET_KEY=
|
||||
# The two base URLs default to the mainland host (open.feishu.cn). For
|
||||
# international Lark tenants, set BOTH to https://open.larksuite.com:
|
||||
# HTTP drives outbound Open Platform API calls, CALLBACK drives the
|
||||
# inbound long-conn callback bootstrap. See docs/lark-bot-integration.
|
||||
MULTICA_LARK_HTTP_BASE_URL=
|
||||
MULTICA_LARK_CALLBACK_BASE_URL=
|
||||
|
||||
# Frontend
|
||||
FRONTEND_PORT=3000
|
||||
# Derived by docker-compose.selfhost.yml / local scripts from FRONTEND_PORT.
|
||||
@@ -239,14 +208,6 @@ ALLOWED_EMAIL_DOMAINS=
|
||||
# Optional: Only allow these exact email addresses (comma-separated)
|
||||
ALLOWED_EMAILS=
|
||||
|
||||
# Set to "true" to disable workspace creation for every caller on this
|
||||
# instance (#3433). Operators usually leave this unset, bootstrap the
|
||||
# shared workspace, then flip this to "true" and restart so subsequent
|
||||
# users join only via invitations and the entire deployment is visible to
|
||||
# the platform admin. The web UI reads this from /api/config at runtime,
|
||||
# so toggling requires a backend restart but not a frontend rebuild.
|
||||
DISABLE_WORKSPACE_CREATION=
|
||||
|
||||
# ==================== Analytics (PostHog) ====================
|
||||
# Product analytics events feed the acquisition → activation → expansion funnel.
|
||||
# Leave POSTHOG_API_KEY empty for local dev / self-hosted instances; the server
|
||||
|
||||
9
.github/workflows/mobile-verify.yml
vendored
9
.github/workflows/mobile-verify.yml
vendored
@@ -13,9 +13,8 @@ name: Mobile Verify
|
||||
# - pnpm-workspace.yaml — catalog versions
|
||||
# - turbo.json — turbo task pipeline
|
||||
#
|
||||
# Mobile's vitest suite is intentionally narrow (Node env, pure-function
|
||||
# tests under apps/mobile/lib/*.test.ts — see apps/mobile/vitest.config.ts).
|
||||
# RN component-level rendering is not exercised here.
|
||||
# Mobile has no vitest suite today; if one lands, add `test` to the turbo
|
||||
# task list below.
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -62,5 +61,5 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Type check, lint, and test
|
||||
run: pnpm exec turbo typecheck lint test --filter=@multica/mobile
|
||||
- name: Type check and lint
|
||||
run: pnpm exec turbo typecheck lint --filter=@multica/mobile
|
||||
|
||||
@@ -176,7 +176,6 @@ make start-worktree # Start using .env.worktree
|
||||
- Avoid broad refactors unless required by the task.
|
||||
- New global (pre-workspace) routes MUST use a single word (`/login`, `/inbox`) or a `/{noun}/{verb}` pair (`/workspaces/new`). NEVER add hyphenated word-group root routes (`/new-workspace`, `/create-team`) — they collide with common user workspace names and force endless reserved-slug audits. Reserving the noun (`workspaces`) automatically protects the entire `/workspaces/*` subtree.
|
||||
- The reserved-slug list lives in **one** place: `server/internal/handler/reserved_slugs.json`. The Go side embeds the JSON; `packages/core/paths/reserved-slugs.ts` is generated from it by `pnpm generate:reserved-slugs`. Edit the JSON, run the generator, commit both. CI re-runs the generator and fails on any drift, so a stale TS file cannot land.
|
||||
- When you change a CLI command or flag, an API request/response field, or product behavior that a built-in skill documents (`server/internal/service/builtin_skills/*`), update that skill's `SKILL.md` **and** its `references/*-source-map.md` in the same PR. The built-in skills are source-traced contracts shipped to agents — if the code moves and the skill doesn't, it silently teaches stale behavior.
|
||||
|
||||
### API Response Compatibility
|
||||
|
||||
|
||||
@@ -655,7 +655,7 @@ multica autopilot update <id> --description "New prompt"
|
||||
multica autopilot delete <id>
|
||||
```
|
||||
|
||||
`--mode` accepts `create_issue` (creates a new issue on each run and assigns it to the agent) or `run_only` (enqueues a direct agent task without creating an issue). `--agent` accepts either a name or UUID.
|
||||
`--mode` currently only accepts `create_issue` (creates a new issue on each run and assigns it to the agent). The server data model also defines `run_only`, but the daemon task path doesn't yet resolve a workspace for runs without an issue, so it's not exposed by the CLI. `--agent` accepts either a name or UUID.
|
||||
|
||||
### Manual Trigger
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ Open http://localhost:3000 in your browser. The Docker self-host stack defaults
|
||||
- **Without email configured:** the verification code is generated server-side and printed to the backend container logs (look for `[DEV] Verification code for ...:`). Useful for one-off testing on a single machine.
|
||||
- **Deterministic local/private testing:** set `APP_ENV=development` and `MULTICA_DEV_VERIFICATION_CODE=888888` in `.env`, then restart the backend. This fixed code is ignored when `APP_ENV=production`.
|
||||
|
||||
Changes to `ALLOW_SIGNUP`, `DISABLE_WORKSPACE_CREATION`, and `GOOGLE_CLIENT_ID` also take effect after restarting the backend / compose stack. The web UI reads all three from `/api/config` at runtime, so no web rebuild is needed. See [Advanced Configuration → Signup Controls](SELF_HOSTING_ADVANCED.md#signup-controls-optional) for the recommended sequence to lock down workspace creation.
|
||||
Changes to `ALLOW_SIGNUP` and `GOOGLE_CLIENT_ID` also take effect after restarting the backend / compose stack. The web UI reads both from `/api/config` at runtime, so no web rebuild is needed.
|
||||
|
||||
> **Warning:** do **not** set `MULTICA_DEV_VERIFICATION_CODE` on a publicly reachable instance — anyone who knows an email address can then log in with that fixed code.
|
||||
|
||||
@@ -144,7 +144,7 @@ If you already run a Kubernetes cluster, you can deploy Multica there instead of
|
||||
The chart creates the following resources in the target namespace:
|
||||
|
||||
- `multica-postgres` — `pgvector/pgvector:pg17` backed by a 10Gi PVC
|
||||
- `multica-backend` — Go API/WS server. Backed by a 5Gi `ReadWriteOnce` uploads PVC by default; set `backend.uploads.persistence.enabled=false` when you have configured S3 (`backend.config.s3Bucket`) and don't want the chart to declare the PVC at all.
|
||||
- `multica-backend` — Go API/WS server backed by a 5Gi uploads PVC
|
||||
- `multica-frontend` — Next.js standalone server
|
||||
- Two `Ingress` resources: one for the web host, one for the backend host
|
||||
- `multica-config` ConfigMap (rendered from `values.yaml`)
|
||||
@@ -268,7 +268,7 @@ The chart defaults to `APP_ENV=production` (set in `values.yaml` under `backend.
|
||||
kubectl -n multica rollout restart deploy/multica-backend
|
||||
```
|
||||
|
||||
`ALLOW_SIGNUP`, `DISABLE_WORKSPACE_CREATION`, and `GOOGLE_CLIENT_ID` likewise live under `backend.config.*` in `values.yaml` (as `allowSignup`, `disableWorkspaceCreation`, and `googleClientId`). After `helm upgrade`, the backend pod will roll automatically because the ConfigMap hash changes; the web UI reads all three from `/api/config` at runtime, so no web rebuild is needed.
|
||||
`ALLOW_SIGNUP` and `GOOGLE_CLIENT_ID` likewise live under `backend.config.*` in `values.yaml`. After `helm upgrade`, the backend pod will roll automatically because the ConfigMap hash changes; the web UI reads both from `/api/config` at runtime, so no web rebuild is needed.
|
||||
|
||||
> **Warning:** do **not** set `MULTICA_DEV_VERIFICATION_CODE` on a publicly reachable instance — anyone who knows an email address can then log in with that fixed code.
|
||||
|
||||
|
||||
@@ -44,11 +44,9 @@ Use this option when your deployment cannot reach the public internet or you alr
|
||||
| `SMTP_PORT` | SMTP port | `25` |
|
||||
| `SMTP_USERNAME` | SMTP username (leave empty for unauthenticated relay) | - |
|
||||
| `SMTP_PASSWORD` | SMTP password | - |
|
||||
| `SMTP_TLS` | TLS mode. `implicit` (aliases `smtps`, `ssl`) forces SMTPS on connect; port `465` auto-enables it. Unset / `starttls` upgrades via STARTTLS | `starttls` |
|
||||
| `SMTP_TLS_INSECURE` | Set `true` to skip TLS certificate verification (self-signed / private CA certs) | `false` |
|
||||
| `SMTP_EHLO_NAME` | EHLO/HELO name announced to the relay. Set a real FQDN when a strict relay (e.g. Google Workspace) rejects the default greeting from a public IP | machine hostname |
|
||||
|
||||
STARTTLS is used automatically when advertised by the server. Port 465 (SMTPS / implicit TLS) is supported and auto-enables implicit TLS; set `SMTP_TLS=implicit` (aliases `smtps`, `ssl`) to force it on a non-standard port.
|
||||
STARTTLS is used automatically when advertised by the server. Port 465 (SMTPS / implicit TLS) is not currently supported - use ports 25 or 587 with STARTTLS.
|
||||
|
||||
> **Note:** If neither Resend nor SMTP is configured, generated verification codes are printed to backend logs — copy them from there to log in. A fixed local testing code (e.g. `888888`) is **opt-in only**: set `MULTICA_DEV_VERIFICATION_CODE=888888` in `.env` and keep `APP_ENV` non-production. The Docker self-host stack pins `APP_ENV=production`, so the shortcut is ignored there. **Never enable a fixed code on a publicly reachable instance.**
|
||||
|
||||
@@ -69,20 +67,8 @@ Changes take effect after restarting the backend / compose stack. The web UI rea
|
||||
| `ALLOW_SIGNUP` | Set to `false` to disable new user signups on a private instance |
|
||||
| `ALLOWED_EMAIL_DOMAINS` | Optional comma-separated allowlist of email domains |
|
||||
| `ALLOWED_EMAILS` | Optional comma-separated allowlist of exact email addresses |
|
||||
| `DISABLE_WORKSPACE_CREATION` | Set to `true` to make `POST /api/workspaces` return 403 for every caller — users can only join workspaces they were invited to |
|
||||
|
||||
Changes take effect after restarting the backend / compose stack. The web UI reads `ALLOW_SIGNUP` and `DISABLE_WORKSPACE_CREATION` from `/api/config` at runtime, so no web rebuild is needed.
|
||||
|
||||
#### Locking down workspace creation
|
||||
|
||||
`ALLOW_SIGNUP=false` blocks new accounts from being created, but it does **not** block an already-signed-in user from creating another workspace via `POST /api/workspaces`. On a self-hosted instance where every issue/repo/agent must be visible to the platform admin, set `DISABLE_WORKSPACE_CREATION=true` to close that gap. The recommended bootstrap sequence is:
|
||||
|
||||
1. Start the instance with `DISABLE_WORKSPACE_CREATION=false` (the default).
|
||||
2. Sign in as the admin and create the shared workspace.
|
||||
3. Set `DISABLE_WORKSPACE_CREATION=true` and restart the backend. Optionally set `ALLOW_SIGNUP=false` at the same time if you also want to block new account creation.
|
||||
4. Going forward, additional users join via invitation only — the "Create workspace" affordance is hidden in the UI and any direct API call returns 403.
|
||||
|
||||
> Note: setting `ALLOW_SIGNUP=false` blocks **all** new account creation, including users who already have a pending invitation. If you need invited users to be able to sign up but not create their own workspaces, keep `ALLOW_SIGNUP=true` (optionally combined with `ALLOWED_EMAIL_DOMAINS` / `ALLOWED_EMAILS`) and only flip `DISABLE_WORKSPACE_CREATION=true`.
|
||||
Changes take effect after restarting the backend / compose stack. The web UI reads `ALLOW_SIGNUP` from `/api/config` at runtime, so no web rebuild is needed.
|
||||
|
||||
### File Storage (Optional)
|
||||
|
||||
@@ -94,8 +80,6 @@ For file uploads and attachments, configure S3 and (optionally) CloudFront:
|
||||
| `S3_REGION` | AWS region (default: `us-west-2`). Must match the bucket's actual region — used for both SDK signing and public URLs |
|
||||
| `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` | Static credentials. When both are unset, the AWS SDK default credential chain is used |
|
||||
| `AWS_ENDPOINT_URL` | Custom S3-compatible endpoint (e.g. MinIO, R2, B2). Setting this switches to path-style URLs |
|
||||
| `ATTACHMENT_DOWNLOAD_MODE` | Attachment download behavior: `auto` (default), `cloudfront`, `presign`, or `proxy`. Use `proxy` for private buckets behind Docker/VPC-only endpoints such as `http://rustfs:9000` |
|
||||
| `ATTACHMENT_DOWNLOAD_URL_TTL` | TTL for CloudFront signed URLs and S3 presigned download URLs (default: `30m`) |
|
||||
| `CLOUDFRONT_DOMAIN` | CloudFront distribution domain — when set, public URLs use this host instead of the S3 host |
|
||||
| `CLOUDFRONT_KEY_PAIR_ID` | CloudFront key pair ID for signed URLs |
|
||||
| `CLOUDFRONT_PRIVATE_KEY` | CloudFront private key (PEM format) |
|
||||
@@ -115,7 +99,7 @@ The `Secure` flag on session cookies is derived automatically from the scheme of
|
||||
| `PORT` | `8080` | Backend server port |
|
||||
| `METRICS_ADDR` | empty | Optional Prometheus metrics listener, for example `127.0.0.1:9090` |
|
||||
| `FRONTEND_PORT` | `3000` | Frontend port |
|
||||
| `CORS_ALLOWED_ORIGINS` | Value of `FRONTEND_ORIGIN` | Comma-separated list of allowed origins. Governs **both** the HTTP CORS allowlist **and** the WebSocket `Origin` check. A browser origin that isn't listed here (and isn't `localhost`) has its real-time WebSocket upgrade rejected with `403`, so live updates stop working until a manual refresh. |
|
||||
| `CORS_ALLOWED_ORIGINS` | Value of `FRONTEND_ORIGIN` | Comma-separated list of allowed origins |
|
||||
| `LOG_LEVEL` | `info` | Log level: `debug`, `info`, `warn`, `error` |
|
||||
|
||||
### CLI / Daemon
|
||||
@@ -340,8 +324,6 @@ multica.example.com {
|
||||
}
|
||||
```
|
||||
|
||||
> Even on a single domain, set `FRONTEND_ORIGIN` / `CORS_ALLOWED_ORIGINS` to that public origin (e.g. `https://multica.example.com`) on the backend. The backend's default origin allowlist is `localhost` only, so without this it rejects the WebSocket upgrade from the public URL with `403` and real-time updates silently stop working. See [LAN / Non-localhost Access](#lan--non-localhost-access).
|
||||
|
||||
**Separate-domain layout** — frontend and backend on different hostnames:
|
||||
|
||||
```
|
||||
@@ -461,8 +443,6 @@ HTTP requests (issues, comments, uploads) work on LAN out of the box — Next.js
|
||||
|
||||
`NEXT_PUBLIC_WS_URL` is a build-time variable (see `Dockerfile.web`), so setting it only in `environment:` on the pre-built image has no effect — you must use the `selfhost.build.yml` override that rebuilds the image.
|
||||
|
||||
**Also required: allowlist the browser origin.** The two options above fix the WebSocket *upgrade proxying*, but a second, independent setting gates the connection: the backend validates the WebSocket `Origin` header against an allowlist that defaults to `localhost` only. When you open Multica from any other origin — a LAN IP **or a public domain behind a reverse proxy** — set `CORS_ALLOWED_ORIGINS` (or `FRONTEND_ORIGIN`) on the backend to that exact origin and restart, exactly as shown under [LAN / Non-localhost Access](#lan--non-localhost-access) above. Otherwise the upgrade is refused with `403`: the backend logs `websocket: request origin not allowed by Upgrader.CheckOrigin` and the browser console loops `disconnected, reconnecting in 3s`, while HTTP requests (and manual page refreshes) keep working because they are same-origin to the page. The single value covers both HTTP CORS and the WebSocket origin check.
|
||||
|
||||
> **Note:** If you need to hard-code a different public API / WebSocket endpoint into the web image for any other reason, use the same source-build override: `docker compose -f docker-compose.selfhost.yml -f docker-compose.selfhost.build.yml up -d --build`.
|
||||
|
||||
## Health Check
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { classifyAuthProbe, isAuthStatusError } from "./daemon-auth-probe";
|
||||
|
||||
describe("classifyAuthProbe", () => {
|
||||
it("treats a 401 as expired login", () => {
|
||||
expect(classifyAuthProbe({ status: 401 })).toBe("auth_expired");
|
||||
});
|
||||
|
||||
it("treats a missing token as expired login", () => {
|
||||
expect(classifyAuthProbe({ noToken: true })).toBe("auth_expired");
|
||||
});
|
||||
|
||||
it("treats a 2xx as a valid token (failure is non-auth)", () => {
|
||||
expect(classifyAuthProbe({ status: 200 })).toBe("ok");
|
||||
expect(classifyAuthProbe({ status: 204 })).toBe("ok");
|
||||
});
|
||||
|
||||
// The headline guard: a network failure must never be reported as an auth
|
||||
// problem — the daemon is just as unreachable for non-auth reasons.
|
||||
it("does NOT classify a network error as expired login", () => {
|
||||
expect(classifyAuthProbe({ networkError: true })).toBe("unknown");
|
||||
});
|
||||
|
||||
it("leaves 5xx and other statuses inconclusive", () => {
|
||||
expect(classifyAuthProbe({ status: 500 })).toBe("unknown");
|
||||
expect(classifyAuthProbe({ status: 503 })).toBe("unknown");
|
||||
expect(classifyAuthProbe({ status: 403 })).toBe("unknown");
|
||||
});
|
||||
|
||||
it("is inconclusive when nothing is known", () => {
|
||||
expect(classifyAuthProbe({})).toBe("unknown");
|
||||
});
|
||||
});
|
||||
|
||||
describe("isAuthStatusError", () => {
|
||||
it("is true only for a 401-tagged error (session token is dead)", () => {
|
||||
expect(isAuthStatusError(Object.assign(new Error("x"), { status: 401 }))).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
// The reviewer's must-fix: transient failures must NOT be treated as auth
|
||||
// failures (which would log the user out). A 5xx mint, a thrown fetch, a
|
||||
// file-write error — none carry status 401.
|
||||
it("is false for transient / non-401 failures", () => {
|
||||
expect(isAuthStatusError(Object.assign(new Error("x"), { status: 503 }))).toBe(
|
||||
false,
|
||||
);
|
||||
expect(isAuthStatusError(new Error("network down"))).toBe(false);
|
||||
expect(isAuthStatusError(new Error("EACCES: write failed"))).toBe(false);
|
||||
expect(isAuthStatusError(undefined)).toBe(false);
|
||||
expect(isAuthStatusError(null)).toBe(false);
|
||||
expect(isAuthStatusError("401")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,58 +0,0 @@
|
||||
/**
|
||||
* Pure classification for the daemon auth probe. Kept free of Electron imports
|
||||
* so it can be unit-tested in jsdom.
|
||||
*
|
||||
* When the local daemon fails to reach "running" shortly after a start, the
|
||||
* main process probes the daemon's token against the backend (GET /api/me) to
|
||||
* tell "the daemon can't authenticate" apart from "the daemon is slow / the
|
||||
* network is down / it crashed for another reason". Misclassifying a network
|
||||
* blip as an auth failure would be worse than the original silent-Starting bug,
|
||||
* so the rules below are deliberately conservative: only an explicit 401 (or a
|
||||
* missing credential) is treated as auth-expired.
|
||||
*/
|
||||
|
||||
export interface AuthProbeOutcome {
|
||||
/** HTTP status code returned by the probe request, if one completed. */
|
||||
status?: number;
|
||||
/** The daemon profile has no token at all — there is nothing to validate. */
|
||||
noToken?: boolean;
|
||||
/** The probe request threw (timeout, connection refused, DNS, TLS). */
|
||||
networkError?: boolean;
|
||||
}
|
||||
|
||||
export type AuthProbeResult = "auth_expired" | "ok" | "unknown";
|
||||
|
||||
/**
|
||||
* Whether an error represents a genuine auth rejection (HTTP 401) as opposed to
|
||||
* a transient failure (5xx, network, local I/O). Used by the re-authenticate
|
||||
* flow so that only a real 401 — the session token itself is dead — forces a
|
||||
* full re-login; transient failures keep the user signed in to retry.
|
||||
*
|
||||
* `mintPat` attaches the response status to the error it throws, so a 401
|
||||
* surfaces here as `{ status: 401 }`. Everything else (no status, 5xx, a thrown
|
||||
* fetch, a file-write error) is treated as non-auth.
|
||||
*/
|
||||
export function isAuthStatusError(err: unknown): boolean {
|
||||
return (
|
||||
typeof err === "object" &&
|
||||
err !== null &&
|
||||
(err as { status?: unknown }).status === 401
|
||||
);
|
||||
}
|
||||
|
||||
export function classifyAuthProbe(outcome: AuthProbeOutcome): AuthProbeResult {
|
||||
// No credential to validate → the user must sign in.
|
||||
if (outcome.noToken) return "auth_expired";
|
||||
// Couldn't reach the server → this is a network problem, not an auth one.
|
||||
// Stay "unknown" so the caller keeps showing "starting"/"stopped" instead of
|
||||
// wrongly prompting for re-login.
|
||||
if (outcome.networkError) return "unknown";
|
||||
// The server explicitly rejected the token.
|
||||
if (outcome.status === 401) return "auth_expired";
|
||||
// The token is accepted — the daemon is failing for some other reason.
|
||||
if (outcome.status !== undefined && outcome.status >= 200 && outcome.status < 300) {
|
||||
return "ok";
|
||||
}
|
||||
// 5xx and everything else are inconclusive about the token's validity.
|
||||
return "unknown";
|
||||
}
|
||||
@@ -15,26 +15,16 @@ import {
|
||||
type StatsListener,
|
||||
} from "fs";
|
||||
import { join } from "path";
|
||||
import { homedir, hostname } from "os";
|
||||
import { homedir } from "os";
|
||||
import type { DaemonStatus, DaemonPrefs } from "../shared/daemon-types";
|
||||
import { ensureManagedCli, managedCliPath } from "./cli-bootstrap";
|
||||
import { decideVersionAction } from "./version-decision";
|
||||
import {
|
||||
classifyAuthProbe,
|
||||
isAuthStatusError,
|
||||
type AuthProbeResult,
|
||||
} from "./daemon-auth-probe";
|
||||
|
||||
const DEFAULT_HEALTH_PORT = 19514;
|
||||
const POLL_INTERVAL_MS = 5_000;
|
||||
const PREFS_PATH = join(homedir(), ".multica", "desktop_prefs.json");
|
||||
const LOG_TAIL_RETRY_MS = 2_000;
|
||||
const LOG_TAIL_MAX_RETRIES = 5;
|
||||
// How long a start may sit in "starting" (with no /health) before we probe the
|
||||
// token to find out whether login expired. The daemon's own startup can legitimately
|
||||
// take a while (it renews the PAT and lists workspaces before serving /health), so we
|
||||
// wait past the common case to avoid probing healthy-but-slow starts.
|
||||
const AUTH_PROBE_GRACE_MS = 10_000;
|
||||
|
||||
const DEFAULT_PREFS: DaemonPrefs = { autoStart: true, autoStop: false };
|
||||
|
||||
@@ -58,15 +48,6 @@ let pendingVersionRestart = false;
|
||||
let targetApiBaseUrl: string | null = null;
|
||||
let activeProfile: ActiveProfile | null = null;
|
||||
|
||||
// Auth-probe state for the current start attempt. When a start fails to reach
|
||||
// "running", we probe the daemon's token once (after AUTH_PROBE_GRACE_MS) to
|
||||
// decide whether the cause is an expired/invalid login. `authExpired` is sticky
|
||||
// until the next start attempt or a successful /health, so the UI keeps showing
|
||||
// the re-login prompt instead of flapping back to "starting". See #3512.
|
||||
let startingSince: number | null = null;
|
||||
let authProbeDone = false;
|
||||
let authExpired = false;
|
||||
|
||||
// Serialize all writes to any profile config file. Multiple paths
|
||||
// (syncToken, resolveActiveProfile, clearToken, watch/unwatch handlers)
|
||||
// may try to write concurrently; chaining them avoids interleaved writes
|
||||
@@ -180,36 +161,6 @@ async function fetchHealthAtPort(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the daemon profile's token against the backend to find out whether
|
||||
* a stuck start is an auth problem. Hits the same endpoint `multica auth status`
|
||||
* uses (GET /api/me) with the exact token the daemon loads from config.json, so
|
||||
* the verdict matches what the daemon itself would get from the server.
|
||||
*
|
||||
* Only the HTTP status is inspected (never the body) so a future change to the
|
||||
* /api/me response shape can't break this — a 401 means the token is rejected,
|
||||
* a 2xx means it's fine, and a thrown request means the network is the problem,
|
||||
* not auth. See classifyAuthProbe for the full rule set.
|
||||
*/
|
||||
async function probeTokenValidity(profile: string): Promise<AuthProbeResult> {
|
||||
if (!targetApiBaseUrl) return "unknown";
|
||||
const cfg = await readProfileConfig(profile);
|
||||
const token = typeof cfg.token === "string" ? cfg.token : "";
|
||||
if (!token) return classifyAuthProbe({ noToken: true });
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 4_000);
|
||||
const res = await fetch(`${targetApiBaseUrl.replace(/\/+$/, "")}/api/me`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
signal: controller.signal,
|
||||
});
|
||||
clearTimeout(timeout);
|
||||
return classifyAuthProbe({ status: res.status });
|
||||
} catch {
|
||||
return classifyAuthProbe({ networkError: true });
|
||||
}
|
||||
}
|
||||
|
||||
// Desktop owns a dedicated CLI profile named after the target API host, so it
|
||||
// never reads or writes the user's hand-configured profiles. Profile dir:
|
||||
// ~/.multica/profiles/desktop-<host>/
|
||||
@@ -298,40 +249,12 @@ async function fetchHealth(): Promise<DaemonStatus> {
|
||||
const data = await fetchHealthAtPort(active.port);
|
||||
|
||||
if (!data || data.status !== "running") {
|
||||
// A start that never reaches "running" is the symptom; an expired/invalid
|
||||
// login is the most common cause and the one with no other signal (the
|
||||
// daemon exits before it can serve /health, so we can't read the reason
|
||||
// from it). Probe the token once per attempt, after a grace period, to
|
||||
// surface a re-login prompt instead of spinning on "starting" forever.
|
||||
if (
|
||||
currentState === "starting" &&
|
||||
!authExpired &&
|
||||
!authProbeDone &&
|
||||
startingSince !== null &&
|
||||
Date.now() - startingSince >= AUTH_PROBE_GRACE_MS
|
||||
) {
|
||||
authProbeDone = true;
|
||||
if ((await probeTokenValidity(active.name)) === "auth_expired") {
|
||||
authExpired = true;
|
||||
}
|
||||
}
|
||||
// Sticky: once login is known-expired, keep reporting it (even after
|
||||
// currentState flips away from "starting") until the next start attempt or
|
||||
// a successful /health clears the flag.
|
||||
if (authExpired) {
|
||||
return { state: "auth_expired", profile: active.name };
|
||||
}
|
||||
return {
|
||||
state: currentState === "starting" ? "starting" : "stopped",
|
||||
profile: active.name,
|
||||
};
|
||||
}
|
||||
|
||||
// A live, authenticated daemon clears any prior auth-failure verdict so the
|
||||
// re-login prompt disappears once the user reconnects.
|
||||
authExpired = false;
|
||||
startingSince = null;
|
||||
|
||||
// Safety: if we have a target URL and the daemon on our port reports a
|
||||
// different server_url, it's not "our" daemon — drop it and re-resolve.
|
||||
if (
|
||||
@@ -592,13 +515,7 @@ async function mintPat(jwt: string): Promise<string> {
|
||||
});
|
||||
if (!res.ok) {
|
||||
const body = await res.text().catch(() => "");
|
||||
// Attach the status so callers can tell a genuine auth rejection (401 — the
|
||||
// session token is dead) apart from a transient failure (5xx, etc.) without
|
||||
// string-matching the message.
|
||||
throw Object.assign(
|
||||
new Error(`mint PAT failed: ${res.status} ${res.statusText} ${body}`),
|
||||
{ status: res.status },
|
||||
);
|
||||
throw new Error(`mint PAT failed: ${res.status} ${res.statusText} ${body}`);
|
||||
}
|
||||
const data = (await res.json()) as { token?: unknown };
|
||||
if (typeof data.token !== "string" || !data.token.startsWith("mul_")) {
|
||||
@@ -703,52 +620,6 @@ async function clearToken(): Promise<void> {
|
||||
await removeProfileUserId(active.name);
|
||||
}
|
||||
|
||||
// Result of a user-initiated daemon re-authentication. The distinction matters:
|
||||
// only `session_invalid` justifies signing the user out of the whole app; a
|
||||
// `transient` failure must keep them logged in so they can retry.
|
||||
export type ReauthResult =
|
||||
| { ok: true }
|
||||
| { ok: false; reason: "session_invalid" }
|
||||
| { ok: false; reason: "transient"; message: string };
|
||||
|
||||
function errorMessage(err: unknown): string {
|
||||
return err instanceof Error ? err.message : String(err);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recover the local daemon from the "auth_expired" state. Drops the stale
|
||||
* cached PAT, mints a fresh one from the current session token, and restarts
|
||||
* the daemon so it loads the new credential.
|
||||
*
|
||||
* Failures are classified rather than collapsed: a 401 from the mint means the
|
||||
* session token itself is dead (`session_invalid` → the renderer drives a full
|
||||
* re-login); anything else — mint 5xx, a network blip, a config write error, a
|
||||
* restart hiccup — is `transient`, leaving the user signed in so they can retry.
|
||||
* This mirrors the conservative classification the startup probe already uses.
|
||||
*/
|
||||
async function reauthenticate(
|
||||
token: string,
|
||||
userId: string,
|
||||
): Promise<ReauthResult> {
|
||||
try {
|
||||
await clearToken();
|
||||
// syncToken mints a fresh PAT because clearToken just removed any cache.
|
||||
await syncToken(token, userId);
|
||||
} catch (err) {
|
||||
if (isAuthStatusError(err)) return { ok: false, reason: "session_invalid" };
|
||||
return { ok: false, reason: "transient", message: errorMessage(err) };
|
||||
}
|
||||
const restart = await restartDaemon();
|
||||
if (!restart.success) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: "transient",
|
||||
message: restart.error ?? "failed to restart daemon",
|
||||
};
|
||||
}
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
async function withGuard<T>(fn: () => Promise<T>): Promise<T | { success: false; error: string }> {
|
||||
if (operationInProgress) {
|
||||
return { success: false, error: "Another daemon operation is in progress" };
|
||||
@@ -786,10 +657,6 @@ async function startDaemon(): Promise<{ success: boolean; error?: string }> {
|
||||
}
|
||||
|
||||
currentState = "starting";
|
||||
// Begin a fresh auth-probe window for this attempt.
|
||||
startingSince = Date.now();
|
||||
authProbeDone = false;
|
||||
authExpired = false;
|
||||
sendStatus({ state: "starting" });
|
||||
|
||||
const args = ["daemon", "start", ...profileArgs(active)];
|
||||
@@ -822,9 +689,6 @@ async function stopDaemon(): Promise<{ success: boolean; error?: string }> {
|
||||
|
||||
const active = await ensureActiveProfile();
|
||||
currentState = "stopping";
|
||||
// An explicit stop is a clean reset — drop any pending auth-failure verdict.
|
||||
authExpired = false;
|
||||
startingSince = null;
|
||||
sendStatus({ state: "stopping" });
|
||||
|
||||
const args = ["daemon", "stop", ...profileArgs(active)];
|
||||
@@ -1000,20 +864,11 @@ export function setupDaemonManager(
|
||||
ipcMain.handle("daemon:stop", () => withGuard(() => stopDaemon()));
|
||||
ipcMain.handle("daemon:restart", () => withGuard(() => restartDaemon()));
|
||||
ipcMain.handle("daemon:get-status", () => fetchHealth());
|
||||
// The host's OS name, available regardless of daemon state. The Runtimes
|
||||
// page uses it as a fallback identity for "this machine" when no
|
||||
// app-managed daemon is reporting a device name (e.g. the daemon runs
|
||||
// out-of-band in WSL2). See desktop-runtimes-page.tsx.
|
||||
ipcMain.handle("daemon:get-host-name", () => hostname());
|
||||
ipcMain.handle(
|
||||
"daemon:sync-token",
|
||||
(_event, token: string, userId: string) => syncToken(token, userId),
|
||||
);
|
||||
ipcMain.handle("daemon:clear-token", () => clearToken());
|
||||
ipcMain.handle(
|
||||
"daemon:reauthenticate",
|
||||
(_event, token: string, userId: string) => reauthenticate(token, userId),
|
||||
);
|
||||
ipcMain.handle("daemon:is-cli-installed", async () => {
|
||||
const bin = await resolveCliBinary();
|
||||
return bin !== null;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { app, BrowserWindow, dialog, ipcMain, nativeImage, Notification } from "electron";
|
||||
import { app, BrowserWindow, ipcMain, nativeImage, Notification } from "electron";
|
||||
import { homedir } from "os";
|
||||
import { join } from "path";
|
||||
import { electronApp, optimizer, is } from "@electron-toolkit/utils";
|
||||
@@ -13,11 +13,6 @@ import { installNavigationGestures } from "./navigation-gestures";
|
||||
import { getAppVersion } from "./app-version";
|
||||
import { loadRuntimeConfig } from "./runtime-config-loader";
|
||||
import type { RuntimeConfigResult } from "../shared/runtime-config";
|
||||
import {
|
||||
createElectronReloadPrompt,
|
||||
installRendererRecoveryHandlers,
|
||||
type RendererRecoveryWindow,
|
||||
} from "./renderer-recovery";
|
||||
|
||||
// Bundled icon used for dock/taskbar branding. macOS/Windows production
|
||||
// builds let the OS pick up the icon from the .app bundle / .exe resources,
|
||||
@@ -229,6 +224,13 @@ function createWindow(): void {
|
||||
log(level, `${message} (${sourceId}:${lineNumber})`);
|
||||
});
|
||||
|
||||
// Fires when the renderer process dies for any reason (OOM, crash,
|
||||
// killed). `details.reason` is the discriminator: "crashed", "oom",
|
||||
// "killed", "abnormal-exit", "launch-failed", etc.
|
||||
mainWindow.webContents.on("render-process-gone", (_event, details) => {
|
||||
log("process-gone", JSON.stringify(details));
|
||||
});
|
||||
|
||||
// Fires when loadURL / loadFile can't reach its target (dev server
|
||||
// not up yet, network blip, file missing). errorCode is a Chromium
|
||||
// net error number; -3 = ABORTED is normal during HMR and skipped.
|
||||
@@ -243,15 +245,14 @@ function createWindow(): void {
|
||||
},
|
||||
);
|
||||
|
||||
// Fires when the preload script throws before the renderer can boot.
|
||||
// This is the one error class that NEVER reaches DevTools (preload
|
||||
// runs before any window) — without this listener it's invisible.
|
||||
mainWindow.webContents.on("preload-error", (_event, preloadPath, error) => {
|
||||
log("preload-error", `path=${preloadPath} err=${error?.stack ?? error}`);
|
||||
});
|
||||
}
|
||||
|
||||
installRendererRecoveryHandlers(mainWindow as unknown as RendererRecoveryWindow, {
|
||||
isDev: is.dev,
|
||||
showReloadPrompt: createElectronReloadPrompt((options) =>
|
||||
dialog.showMessageBox(mainWindow!, options),
|
||||
),
|
||||
});
|
||||
|
||||
installContextMenu(mainWindow.webContents);
|
||||
installNavigationGestures(mainWindow);
|
||||
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
||||
import { installRendererRecoveryHandlers } from "./renderer-recovery";
|
||||
|
||||
type Handler = (...args: unknown[]) => void;
|
||||
|
||||
function makeWindow() {
|
||||
const windowHandlers = new Map<string, Handler>();
|
||||
const webContentsHandlers = new Map<string, Handler>();
|
||||
const reload = vi.fn();
|
||||
return {
|
||||
window: {
|
||||
on: vi.fn((event: string, handler: Handler) => windowHandlers.set(event, handler)),
|
||||
isDestroyed: vi.fn(() => false),
|
||||
webContents: {
|
||||
on: vi.fn((event: string, handler: Handler) => webContentsHandlers.set(event, handler)),
|
||||
reload,
|
||||
},
|
||||
},
|
||||
windowHandlers,
|
||||
webContentsHandlers,
|
||||
reload,
|
||||
};
|
||||
}
|
||||
|
||||
describe("installRendererRecoveryHandlers", () => {
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
afterEach(() => vi.useRealTimers());
|
||||
|
||||
it("registers production reload prompts for renderer death and preload failure without auto reloading", async () => {
|
||||
const fixture = makeWindow();
|
||||
const showReloadPrompt = vi.fn(async () => "reload" as const);
|
||||
|
||||
installRendererRecoveryHandlers(fixture.window, { isDev: false, showReloadPrompt });
|
||||
|
||||
expect(fixture.webContentsHandlers.has("render-process-gone")).toBe(true);
|
||||
expect(fixture.webContentsHandlers.has("preload-error")).toBe(true);
|
||||
expect(fixture.windowHandlers.has("unresponsive")).toBe(true);
|
||||
expect(fixture.windowHandlers.has("responsive")).toBe(true);
|
||||
|
||||
fixture.webContentsHandlers.get("render-process-gone")?.({}, { reason: "crashed" });
|
||||
fixture.webContentsHandlers.get("preload-error")?.({}, "/preload.js", new Error("boom"));
|
||||
|
||||
expect(fixture.reload).not.toHaveBeenCalled();
|
||||
await Promise.resolve();
|
||||
|
||||
expect(showReloadPrompt).toHaveBeenCalledTimes(2);
|
||||
expect(fixture.reload).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("does not prompt when the renderer exits cleanly", async () => {
|
||||
const fixture = makeWindow();
|
||||
const showReloadPrompt = vi.fn(async () => "reload" as const);
|
||||
|
||||
installRendererRecoveryHandlers(fixture.window, { isDev: false, showReloadPrompt });
|
||||
|
||||
fixture.webContentsHandlers.get("render-process-gone")?.({}, { reason: "clean-exit" });
|
||||
await Promise.resolve();
|
||||
|
||||
expect(showReloadPrompt).not.toHaveBeenCalled();
|
||||
expect(fixture.reload).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("cancels an unresponsive prompt when the window becomes responsive again", async () => {
|
||||
vi.useFakeTimers();
|
||||
const fixture = makeWindow();
|
||||
const showReloadPrompt = vi.fn(async () => "reload" as const);
|
||||
|
||||
installRendererRecoveryHandlers(fixture.window, {
|
||||
isDev: false,
|
||||
showReloadPrompt,
|
||||
unresponsivePromptDelayMs: 100,
|
||||
});
|
||||
|
||||
fixture.windowHandlers.get("unresponsive")?.();
|
||||
fixture.windowHandlers.get("responsive")?.();
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
|
||||
expect(showReloadPrompt).not.toHaveBeenCalled();
|
||||
expect(fixture.reload).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("prompts for sustained unresponsive windows and only reloads after user confirmation", async () => {
|
||||
vi.useFakeTimers();
|
||||
const fixture = makeWindow();
|
||||
const showReloadPrompt = vi.fn(async () => "dismiss" as const);
|
||||
|
||||
installRendererRecoveryHandlers(fixture.window, {
|
||||
isDev: false,
|
||||
showReloadPrompt,
|
||||
unresponsivePromptDelayMs: 100,
|
||||
});
|
||||
|
||||
fixture.windowHandlers.get("unresponsive")?.();
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
|
||||
expect(showReloadPrompt).toHaveBeenCalledWith({ kind: "unresponsive", context: {} });
|
||||
expect(fixture.reload).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps dev diagnostics non-prompting", async () => {
|
||||
const fixture = makeWindow();
|
||||
const showReloadPrompt = vi.fn(async () => "reload" as const);
|
||||
|
||||
installRendererRecoveryHandlers(fixture.window, { isDev: true, showReloadPrompt, log: vi.fn() });
|
||||
|
||||
fixture.webContentsHandlers.get("render-process-gone")?.({}, { reason: "crashed" });
|
||||
await Promise.resolve();
|
||||
|
||||
expect(showReloadPrompt).not.toHaveBeenCalled();
|
||||
expect(fixture.reload).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,135 +0,0 @@
|
||||
export type RendererRecoveryWindow = {
|
||||
isDestroyed: () => boolean;
|
||||
on: (event: "unresponsive" | "responsive", handler: () => void) => unknown;
|
||||
webContents: {
|
||||
on: (event: string, handler: (...args: any[]) => void) => unknown;
|
||||
reload: () => void;
|
||||
};
|
||||
};
|
||||
|
||||
type ReloadPromptPayload = {
|
||||
kind: "render-process-gone" | "preload-error" | "unresponsive";
|
||||
context: Record<string, unknown>;
|
||||
};
|
||||
|
||||
type ReloadPromptResult = "reload" | "dismiss";
|
||||
|
||||
type RendererRecoveryOptions = {
|
||||
isDev: boolean;
|
||||
showReloadPrompt: (payload: ReloadPromptPayload) => Promise<ReloadPromptResult>;
|
||||
log?: (tag: string, ...args: unknown[]) => void;
|
||||
unresponsivePromptDelayMs?: number;
|
||||
};
|
||||
|
||||
export function installRendererRecoveryHandlers(
|
||||
window: RendererRecoveryWindow,
|
||||
{
|
||||
isDev,
|
||||
showReloadPrompt,
|
||||
log = defaultDevLog,
|
||||
unresponsivePromptDelayMs = 1500,
|
||||
}: RendererRecoveryOptions,
|
||||
) {
|
||||
let unresponsivePromptTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
const maybePromptReload = (payload: ReloadPromptPayload) => {
|
||||
if (isDev) return;
|
||||
void showReloadPrompt(payload).then((result) => {
|
||||
if (result === "reload" && !window.isDestroyed()) {
|
||||
window.webContents.reload();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
window.webContents.on("render-process-gone", (_event, details) => {
|
||||
if (isDev) log("process-gone", JSON.stringify(details));
|
||||
if (!isRecoverableRendererExit(details)) return;
|
||||
maybePromptReload({ kind: "render-process-gone", context: { details } });
|
||||
});
|
||||
|
||||
window.webContents.on("preload-error", (_event, preloadPath, error) => {
|
||||
if (isDev) log("preload-error", `path=${preloadPath} err=${formatError(error)}`);
|
||||
maybePromptReload({
|
||||
kind: "preload-error",
|
||||
context: { preloadPath, error: formatError(error) },
|
||||
});
|
||||
});
|
||||
|
||||
window.on("unresponsive", () => {
|
||||
if (isDev || unresponsivePromptTimer) return;
|
||||
unresponsivePromptTimer = setTimeout(() => {
|
||||
unresponsivePromptTimer = null;
|
||||
maybePromptReload({ kind: "unresponsive", context: {} });
|
||||
}, unresponsivePromptDelayMs);
|
||||
});
|
||||
|
||||
window.on("responsive", () => {
|
||||
if (!unresponsivePromptTimer) return;
|
||||
clearTimeout(unresponsivePromptTimer);
|
||||
unresponsivePromptTimer = null;
|
||||
});
|
||||
}
|
||||
|
||||
export function createElectronReloadPrompt(
|
||||
showMessageBox: (options: {
|
||||
type: "warning";
|
||||
buttons: string[];
|
||||
defaultId: number;
|
||||
cancelId: number;
|
||||
title: string;
|
||||
message: string;
|
||||
detail: string;
|
||||
}) => Promise<{ response: number }>,
|
||||
) {
|
||||
return async (payload: ReloadPromptPayload): Promise<ReloadPromptResult> => {
|
||||
const result = await showMessageBox({
|
||||
type: "warning",
|
||||
buttons: ["Reload", "Dismiss"],
|
||||
defaultId: 0,
|
||||
cancelId: 1,
|
||||
title: "Multica needs to reload",
|
||||
message: rendererRecoveryMessage(payload.kind),
|
||||
detail: rendererRecoveryDetail(payload),
|
||||
});
|
||||
return result.response === 0 ? "reload" : "dismiss";
|
||||
};
|
||||
}
|
||||
|
||||
function isRecoverableRendererExit(details: unknown) {
|
||||
if (!details || typeof details !== "object") return false;
|
||||
const reason = (details as { reason?: unknown }).reason;
|
||||
return (
|
||||
reason === "crashed" ||
|
||||
reason === "oom" ||
|
||||
reason === "abnormal-exit" ||
|
||||
reason === "launch-failed" ||
|
||||
reason === "integrity-failure"
|
||||
);
|
||||
}
|
||||
|
||||
function rendererRecoveryMessage(kind: ReloadPromptPayload["kind"]) {
|
||||
switch (kind) {
|
||||
case "render-process-gone":
|
||||
return "The desktop renderer process stopped responding or crashed.";
|
||||
case "preload-error":
|
||||
return "The desktop preload script failed before the app could start.";
|
||||
case "unresponsive":
|
||||
return "The desktop window is not responding.";
|
||||
}
|
||||
}
|
||||
|
||||
function rendererRecoveryDetail(payload: ReloadPromptPayload) {
|
||||
return [
|
||||
"Reloading is the safest recovery path for this window.",
|
||||
"",
|
||||
`kind: ${payload.kind}`,
|
||||
`context: ${JSON.stringify(payload.context)}`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function defaultDevLog(tag: string, ...args: unknown[]) {
|
||||
process.stderr.write(`[renderer ${tag}] ${args.map(String).join(" ")}\n`);
|
||||
}
|
||||
|
||||
function formatError(error: unknown) {
|
||||
return error instanceof Error ? (error.stack ?? error.message) : String(error);
|
||||
}
|
||||
19
apps/desktop/src/preload/index.d.ts
vendored
19
apps/desktop/src/preload/index.d.ts
vendored
@@ -74,14 +74,7 @@ interface DesktopAPI {
|
||||
}
|
||||
|
||||
interface DaemonStatus {
|
||||
state:
|
||||
| "running"
|
||||
| "stopped"
|
||||
| "starting"
|
||||
| "stopping"
|
||||
| "installing_cli"
|
||||
| "cli_not_found"
|
||||
| "auth_expired";
|
||||
state: "running" | "stopped" | "starting" | "stopping" | "installing_cli" | "cli_not_found";
|
||||
pid?: number;
|
||||
uptime?: string;
|
||||
daemonId?: string;
|
||||
@@ -97,25 +90,15 @@ interface DaemonPrefs {
|
||||
autoStop: boolean;
|
||||
}
|
||||
|
||||
type DaemonReauthResult =
|
||||
| { ok: true }
|
||||
| { ok: false; reason: "session_invalid" }
|
||||
| { ok: false; reason: "transient"; message: string };
|
||||
|
||||
interface DaemonAPI {
|
||||
start: () => Promise<{ success: boolean; error?: string }>;
|
||||
stop: () => Promise<{ success: boolean; error?: string }>;
|
||||
restart: () => Promise<{ success: boolean; error?: string }>;
|
||||
getStatus: () => Promise<DaemonStatus>;
|
||||
getHostName: () => Promise<string>;
|
||||
onStatusChange: (callback: (status: DaemonStatus) => void) => () => void;
|
||||
setTargetApiUrl: (url: string) => Promise<void>;
|
||||
syncToken: (token: string, userId: string) => Promise<void>;
|
||||
clearToken: () => Promise<void>;
|
||||
reauthenticate: (
|
||||
token: string,
|
||||
userId: string,
|
||||
) => Promise<DaemonReauthResult>;
|
||||
isCliInstalled: () => Promise<boolean>;
|
||||
getPrefs: () => Promise<DaemonPrefs>;
|
||||
setPrefs: (prefs: Partial<DaemonPrefs>) => Promise<DaemonPrefs>;
|
||||
|
||||
@@ -165,14 +165,7 @@ const desktopAPI = {
|
||||
};
|
||||
|
||||
interface DaemonStatus {
|
||||
state:
|
||||
| "running"
|
||||
| "stopped"
|
||||
| "starting"
|
||||
| "stopping"
|
||||
| "installing_cli"
|
||||
| "cli_not_found"
|
||||
| "auth_expired";
|
||||
state: "running" | "stopped" | "starting" | "stopping" | "installing_cli" | "cli_not_found";
|
||||
pid?: number;
|
||||
uptime?: string;
|
||||
daemonId?: string;
|
||||
@@ -183,11 +176,6 @@ interface DaemonStatus {
|
||||
serverUrl?: string;
|
||||
}
|
||||
|
||||
type DaemonReauthResult =
|
||||
| { ok: true }
|
||||
| { ok: false; reason: "session_invalid" }
|
||||
| { ok: false; reason: "transient"; message: string };
|
||||
|
||||
const daemonAPI = {
|
||||
start: (): Promise<{ success: boolean; error?: string }> =>
|
||||
ipcRenderer.invoke("daemon:start"),
|
||||
@@ -197,8 +185,6 @@ const daemonAPI = {
|
||||
ipcRenderer.invoke("daemon:restart"),
|
||||
getStatus: (): Promise<DaemonStatus> =>
|
||||
ipcRenderer.invoke("daemon:get-status"),
|
||||
getHostName: (): Promise<string> =>
|
||||
ipcRenderer.invoke("daemon:get-host-name"),
|
||||
onStatusChange: (callback: (status: DaemonStatus) => void) => {
|
||||
const handler = (_: unknown, status: DaemonStatus) => callback(status);
|
||||
ipcRenderer.on("daemon:status", handler);
|
||||
@@ -210,11 +196,6 @@ const daemonAPI = {
|
||||
ipcRenderer.invoke("daemon:sync-token", token, userId),
|
||||
clearToken: (): Promise<void> =>
|
||||
ipcRenderer.invoke("daemon:clear-token"),
|
||||
reauthenticate: (
|
||||
token: string,
|
||||
userId: string,
|
||||
): Promise<DaemonReauthResult> =>
|
||||
ipcRenderer.invoke("daemon:reauthenticate", token, userId),
|
||||
isCliInstalled: (): Promise<boolean> =>
|
||||
ipcRenderer.invoke("daemon:is-cli-installed"),
|
||||
getPrefs: (): Promise<{ autoStart: boolean; autoStop: boolean }> =>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { CoreProvider } from "@multica/core/platform";
|
||||
import { pickLocale, type SupportedLocale } from "@multica/core/i18n";
|
||||
import { pickLocale } from "@multica/core/i18n";
|
||||
import { useAuthStore } from "@multica/core/auth";
|
||||
import { useWelcomeStore } from "@multica/core/onboarding";
|
||||
import { workspaceKeys, workspaceListOptions } from "@multica/core/workspace/queries";
|
||||
@@ -21,18 +21,6 @@ import { useDaemonIPCBridge } from "./platform/daemon-ipc-bridge";
|
||||
import { createDesktopLocaleAdapter } from "./platform/i18n-adapter";
|
||||
import { RESOURCES } from "@multica/views/locales";
|
||||
|
||||
// BCP-47 region tags for the <html lang> attribute, mirroring
|
||||
// apps/web/app/layout.tsx HTML_LANG. index.html ships a static lang="en";
|
||||
// we sync it to the resolved locale at boot so screen readers announce the
|
||||
// right language AND the Japanese-scoped CJK font override in globals.css
|
||||
// (`html[lang|="ja"]`) can take effect.
|
||||
const HTML_LANG: Record<SupportedLocale, string> = {
|
||||
en: "en",
|
||||
"zh-Hans": "zh-CN",
|
||||
ko: "ko-KR",
|
||||
ja: "ja-JP",
|
||||
};
|
||||
|
||||
|
||||
function AppContent() {
|
||||
const user = useAuthStore((s) => s.user);
|
||||
@@ -191,7 +179,6 @@ function AppContent() {
|
||||
return undefined;
|
||||
}, [user, workspaceListFetched, wsCount, workspaces, hasOnboarded, qc]);
|
||||
|
||||
|
||||
// Validate persisted tab state against the current user's workspace list,
|
||||
// and pick an active workspace if none is set. Runs in useLayoutEffect
|
||||
// (synchronously after render, before paint) rather than the render
|
||||
@@ -316,15 +303,6 @@ export default function App() {
|
||||
[locale],
|
||||
);
|
||||
|
||||
// Keep <html lang> in sync with the resolved locale (index.html hardcodes
|
||||
// "en"). Drives the lang-scoped Japanese CJK font override and a11y.
|
||||
// useLayoutEffect (not useEffect) so lang is committed before the first
|
||||
// paint — otherwise Japanese users would see one frame of Kanji rendered
|
||||
// with the Chinese-first fallback stack before the override kicks in.
|
||||
useLayoutEffect(() => {
|
||||
document.documentElement.lang = HTML_LANG[locale];
|
||||
}, [locale]);
|
||||
|
||||
// React to OS-level language changes detected by main on focus regain.
|
||||
// Only act when the user is following the system signal (no explicit
|
||||
// Settings choice) — otherwise their preference wins. Cross-device sync
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
RotateCw,
|
||||
Activity,
|
||||
ScrollText,
|
||||
LogIn,
|
||||
} from "lucide-react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useWorkspaceId } from "@multica/core/hooks";
|
||||
@@ -23,7 +22,6 @@ import {
|
||||
} from "@multica/ui/components/ui/dialog";
|
||||
import { toast } from "sonner";
|
||||
import { DaemonPanel } from "./daemon-panel";
|
||||
import { reauthenticateDaemon } from "../platform/daemon-reauth";
|
||||
import type { DaemonStatus } from "../../../shared/daemon-types";
|
||||
import { DAEMON_STATE_LABELS } from "../../../shared/daemon-types";
|
||||
|
||||
@@ -117,18 +115,9 @@ export function DaemonRuntimeActions() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleReauth = useCallback(async () => {
|
||||
setActionLoading(true);
|
||||
await reauthenticateDaemon();
|
||||
// onStatusChange resets actionLoading on the next status push; reset here
|
||||
// too in case reauth logged out (unmount) or produced no status change.
|
||||
setActionLoading(false);
|
||||
}, []);
|
||||
|
||||
const isRunning = status.state === "running";
|
||||
const isStopped = status.state === "stopped";
|
||||
const isCliMissing = status.state === "cli_not_found";
|
||||
const isAuthExpired = status.state === "auth_expired";
|
||||
const isTransitioning =
|
||||
status.state === "starting" || status.state === "stopping";
|
||||
const isInstalling = status.state === "installing_cli";
|
||||
@@ -186,23 +175,6 @@ export function DaemonRuntimeActions() {
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{isAuthExpired && (
|
||||
<>
|
||||
<span className="inline-flex items-center gap-1.5 text-xs text-destructive">
|
||||
<AlertCircle className="size-3.5 shrink-0" />
|
||||
Sign-in expired
|
||||
</span>
|
||||
<Button size="sm" onClick={handleReauth} disabled={actionLoading}>
|
||||
{actionLoading ? (
|
||||
<Activity className="size-3.5 mr-1.5 animate-pulse" />
|
||||
) : (
|
||||
<LogIn className="size-3.5 mr-1.5" />
|
||||
)}
|
||||
Sign in again
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{(isTransitioning || isInstalling) && (
|
||||
<Button size="sm" variant="outline" disabled>
|
||||
<Activity className="size-3.5 mr-1.5 animate-pulse" />
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { useState, useEffect, useCallback, type ReactNode } from "react";
|
||||
import { AlertCircle, LogIn } from "lucide-react";
|
||||
import { Button } from "@multica/ui/components/ui/button";
|
||||
import { Switch } from "@multica/ui/components/ui/switch";
|
||||
import { cn } from "@multica/ui/lib/utils";
|
||||
import { reauthenticateDaemon } from "../platform/daemon-reauth";
|
||||
import type { DaemonPrefs, DaemonStatus } from "../../../shared/daemon-types";
|
||||
import {
|
||||
DAEMON_STATE_COLORS,
|
||||
@@ -63,7 +61,6 @@ export function DaemonSettingsTab() {
|
||||
const [cliInstalled, setCliInstalled] = useState<boolean | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [status, setStatus] = useState<DaemonStatus>({ state: "stopped" });
|
||||
const [reauthLoading, setReauthLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
window.daemonAPI.getPrefs().then(setPrefs);
|
||||
@@ -72,12 +69,6 @@ export function DaemonSettingsTab() {
|
||||
return window.daemonAPI.onStatusChange(setStatus);
|
||||
}, []);
|
||||
|
||||
const handleReauth = useCallback(async () => {
|
||||
setReauthLoading(true);
|
||||
await reauthenticateDaemon();
|
||||
setReauthLoading(false);
|
||||
}, []);
|
||||
|
||||
const updatePref = useCallback(
|
||||
async (key: keyof DaemonPrefs, value: boolean) => {
|
||||
setSaving(true);
|
||||
@@ -95,30 +86,6 @@ export function DaemonSettingsTab() {
|
||||
Configure how the local agent daemon behaves with the desktop app.
|
||||
</p>
|
||||
|
||||
{status.state === "auth_expired" && (
|
||||
<div className="mt-4 flex items-start gap-3 rounded-lg border border-destructive/40 bg-destructive/5 px-4 py-3">
|
||||
<AlertCircle className="mt-0.5 size-4 shrink-0 text-destructive" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-medium text-destructive">
|
||||
Sign-in expired
|
||||
</p>
|
||||
<p className="mt-0.5 text-sm text-muted-foreground">
|
||||
The local daemon couldn't authenticate, so this device
|
||||
can't take tasks. Sign in again to restore it.
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
className="shrink-0"
|
||||
onClick={handleReauth}
|
||||
disabled={reauthLoading}
|
||||
>
|
||||
<LogIn className="size-3.5 mr-1.5" />
|
||||
Sign in again
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-6 divide-y">
|
||||
<SettingRow
|
||||
label="Auto-start on launch"
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { AgentsPage } from "@multica/views/agents";
|
||||
import type { DaemonStatus } from "../../../shared/daemon-types";
|
||||
|
||||
/**
|
||||
* Desktop wrapper around the shared `AgentsPage`. Bridges the Electron
|
||||
* `daemonAPI` (main-process daemon state) into the page so the runtime
|
||||
* machine filter can render the Local section the same way the Runtimes
|
||||
* page does — without these props the page falls back to grouping
|
||||
* every local-mode runtime under "Remote" with a generic title, which
|
||||
* breaks the "drill from a machine into its agents" promise of the
|
||||
* filter.
|
||||
*
|
||||
* Mirrors `DesktopRuntimesPage`: we cache the last seen daemon
|
||||
* identity so the Local row doesn't get reclassified as Remote when
|
||||
* the daemon is stopped (which would null out `status.daemonId`), and
|
||||
* we fall back to the OS hostname so the section label stays useful
|
||||
* even when the app doesn't manage the running daemon (WSL2 etc.).
|
||||
*/
|
||||
export function DesktopAgentsPage() {
|
||||
const [status, setStatus] = useState<DaemonStatus>({ state: "stopped" });
|
||||
const [lastIdentity, setLastIdentity] = useState<{
|
||||
daemonId: string | null;
|
||||
deviceName: string | null;
|
||||
}>({ daemonId: null, deviceName: null });
|
||||
const [hostName, setHostName] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const apply = (s: DaemonStatus) => {
|
||||
setStatus(s);
|
||||
if (s.daemonId) {
|
||||
setLastIdentity({
|
||||
daemonId: s.daemonId,
|
||||
deviceName: s.deviceName ?? null,
|
||||
});
|
||||
}
|
||||
};
|
||||
window.daemonAPI.getStatus().then(apply);
|
||||
window.daemonAPI.getHostName().then((name) => setHostName(name || null));
|
||||
return window.daemonAPI.onStatusChange(apply);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AgentsPage
|
||||
localDaemonId={status.daemonId ?? lastIdentity.daemonId}
|
||||
localMachineName={status.deviceName ?? lastIdentity.deviceName ?? hostName}
|
||||
// Desktop owns a local machine for the lifetime of the app, even
|
||||
// while the daemon is stopped or hasn't registered yet. The shared
|
||||
// page synthesizes a placeholder local row so the filter dropdown
|
||||
// still has a Local option to pick in the empty window.
|
||||
hasLocalMachine
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -28,11 +28,6 @@ export function DesktopRuntimesPage() {
|
||||
daemonId: string | null;
|
||||
deviceName: string | null;
|
||||
}>({ daemonId: null, deviceName: null });
|
||||
// The host's OS hostname, independent of any daemon. Used as the last
|
||||
// fallback for the local machine name so consolidation still works when
|
||||
// the app doesn't manage the running daemon (e.g. it lives in WSL2) and
|
||||
// thus never reports a device name.
|
||||
const [hostName, setHostName] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const apply = (s: DaemonStatus) => {
|
||||
@@ -45,7 +40,6 @@ export function DesktopRuntimesPage() {
|
||||
}
|
||||
};
|
||||
window.daemonAPI.getStatus().then(apply);
|
||||
window.daemonAPI.getHostName().then((name) => setHostName(name || null));
|
||||
return window.daemonAPI.onStatusChange(apply);
|
||||
}, []);
|
||||
|
||||
@@ -57,7 +51,7 @@ export function DesktopRuntimesPage() {
|
||||
return (
|
||||
<RuntimesPage
|
||||
localDaemonId={status.daemonId ?? lastIdentity.daemonId}
|
||||
localMachineName={status.deviceName ?? lastIdentity.deviceName ?? hostName}
|
||||
localMachineName={status.deviceName ?? lastIdentity.deviceName}
|
||||
localMachineActions={<DaemonRuntimeActions />}
|
||||
// Desktop owns a local machine for the lifetime of the app, even
|
||||
// while the daemon is stopped or hasn't registered yet. The shared
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { createMemoryRouter, RouterProvider } from "react-router-dom";
|
||||
|
||||
const openModal = vi.fn();
|
||||
const reloadActiveTab = vi.fn();
|
||||
const closeActiveTab = vi.fn();
|
||||
|
||||
vi.mock("@multica/core/modals", () => ({
|
||||
useModalStore: {
|
||||
getState: () => ({ open: openModal }),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/stores/tab-store", () => ({
|
||||
useTabStore: {
|
||||
getState: () => ({ reloadActiveTab, closeActiveTab }),
|
||||
},
|
||||
}));
|
||||
|
||||
import { DesktopRouteErrorPage, formatRouteErrorReport } from "./route-error-page";
|
||||
|
||||
function Boom(): null {
|
||||
throw new Error("route render exploded");
|
||||
return null;
|
||||
}
|
||||
|
||||
describe("DesktopRouteErrorPage", () => {
|
||||
beforeEach(() => {
|
||||
openModal.mockReset();
|
||||
reloadActiveTab.mockReset();
|
||||
closeActiveTab.mockReset();
|
||||
vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("brands React Router route errors and offers tab reload", async () => {
|
||||
const router = createMemoryRouter(
|
||||
[{ path: "/", element: <Boom />, errorElement: <DesktopRouteErrorPage /> }],
|
||||
{ initialEntries: ["/"] },
|
||||
);
|
||||
|
||||
render(<RouterProvider router={router} />);
|
||||
|
||||
expect(await screen.findByRole("alert")).toHaveTextContent(
|
||||
"Something went wrong in this tab",
|
||||
);
|
||||
fireEvent.click(screen.getByRole("button", { name: /reload tab/i }));
|
||||
expect(reloadActiveTab).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("offers Close tab as the always-safe escape from a crashing route", async () => {
|
||||
const router = createMemoryRouter(
|
||||
[{ path: "/acme/issues/1", element: <Boom />, errorElement: <DesktopRouteErrorPage /> }],
|
||||
{ initialEntries: ["/acme/issues/1"] },
|
||||
);
|
||||
|
||||
render(<RouterProvider router={router} />);
|
||||
|
||||
fireEvent.click(await screen.findByRole("button", { name: /close tab/i }));
|
||||
expect(closeActiveTab).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("opens the existing feedback modal with a structured markdown report only after click", async () => {
|
||||
const router = createMemoryRouter(
|
||||
[{ path: "/acme/issues", element: <Boom />, errorElement: <DesktopRouteErrorPage /> }],
|
||||
{ initialEntries: ["/acme/issues"] },
|
||||
);
|
||||
|
||||
render(<RouterProvider router={router} />);
|
||||
|
||||
expect(openModal).not.toHaveBeenCalled();
|
||||
fireEvent.click(await screen.findByRole("button", { name: /report error/i }));
|
||||
|
||||
expect(openModal).toHaveBeenCalledWith(
|
||||
"feedback",
|
||||
expect.objectContaining({
|
||||
initialMessage: expect.stringContaining("kind: desktop_route_error"),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("documents the structured kind/context follow-up debt in the report template", () => {
|
||||
const report = formatRouteErrorReport({
|
||||
error: new Error("bad route"),
|
||||
url: "app://desktop/acme/issues",
|
||||
appInfo: { version: "1.2.3", os: "macos" },
|
||||
trigger: "route-errorElement",
|
||||
});
|
||||
|
||||
expect(report).toContain("kind: desktop_route_error");
|
||||
expect(report).toContain("trigger: route-errorElement");
|
||||
expect(report).toContain("TODO: promote kind/context to structured feedback fields");
|
||||
});
|
||||
});
|
||||
@@ -1,140 +0,0 @@
|
||||
import { useMemo } from "react";
|
||||
import { useLocation, useNavigate, useRouteError } from "react-router-dom";
|
||||
import { AlertTriangle, RotateCw, Send, X } from "lucide-react";
|
||||
import { Button } from "@multica/ui/components/ui/button";
|
||||
import { useModalStore } from "@multica/core/modals";
|
||||
import { useTabStore } from "@/stores/tab-store";
|
||||
|
||||
type DesktopAppInfo = {
|
||||
version?: string;
|
||||
os?: string;
|
||||
};
|
||||
|
||||
export function formatRouteErrorReport({
|
||||
error,
|
||||
url,
|
||||
appInfo,
|
||||
trigger,
|
||||
}: {
|
||||
error: unknown;
|
||||
url: string;
|
||||
appInfo?: DesktopAppInfo;
|
||||
trigger: string;
|
||||
}) {
|
||||
const normalized = normalizeError(error);
|
||||
return [
|
||||
"kind: desktop_route_error",
|
||||
`trigger: ${trigger}`,
|
||||
`url: ${url}`,
|
||||
`app_version: ${appInfo?.version ?? "unknown"}`,
|
||||
`runtime_os: ${appInfo?.os ?? "unknown"}`,
|
||||
"",
|
||||
"context:",
|
||||
`- name: ${normalized.name}`,
|
||||
`- message: ${normalized.message}`,
|
||||
"",
|
||||
"stack:",
|
||||
"```",
|
||||
normalized.stack ?? "<no stack>",
|
||||
"```",
|
||||
"",
|
||||
"TODO: promote kind/context to structured feedback fields once the feedback API supports them.",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
export function DesktopRouteErrorPage() {
|
||||
const error = useRouteError();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const workspaceSlug = location.pathname.split("/").filter(Boolean)[0];
|
||||
const safeRoute = workspaceSlug ? `/${workspaceSlug}/issues` : null;
|
||||
const report = useMemo(
|
||||
() =>
|
||||
formatRouteErrorReport({
|
||||
error,
|
||||
url:
|
||||
typeof window !== "undefined"
|
||||
? `${window.location.origin}${location.pathname}${location.search}${location.hash}`
|
||||
: location.pathname,
|
||||
appInfo: typeof window !== "undefined" ? window.desktopAPI?.appInfo : undefined,
|
||||
trigger: "route-errorElement",
|
||||
}),
|
||||
[error, location.hash, location.pathname, location.search],
|
||||
);
|
||||
const message = normalizeError(error).message;
|
||||
|
||||
return (
|
||||
<div
|
||||
role="alert"
|
||||
className="flex h-full min-h-[20rem] flex-col items-center justify-center gap-4 p-8 text-center"
|
||||
>
|
||||
<div className="rounded-full bg-destructive/10 p-3 text-destructive">
|
||||
<AlertTriangle className="h-6 w-6" aria-hidden="true" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-lg font-semibold">Something went wrong in this tab</h2>
|
||||
<p className="max-w-lg text-sm text-muted-foreground">
|
||||
A route-level renderer error was contained before it could take down the
|
||||
desktop shell. Reload this tab, or send the report if it keeps happening.
|
||||
</p>
|
||||
<p className="max-w-lg truncate text-xs text-muted-foreground">{message}</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => useTabStore.getState().reloadActiveTab()}
|
||||
>
|
||||
<RotateCw className="mr-2 h-4 w-4" aria-hidden="true" />
|
||||
Reload tab
|
||||
</Button>
|
||||
{safeRoute ? (
|
||||
<Button type="button" variant="outline" onClick={() => navigate(safeRoute, { replace: true })}>
|
||||
Go to issues
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => useTabStore.getState().closeActiveTab()}
|
||||
>
|
||||
<X className="mr-2 h-4 w-4" aria-hidden="true" />
|
||||
Close tab
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
useModalStore.getState().open("feedback", {
|
||||
initialMessage: report,
|
||||
})
|
||||
}
|
||||
>
|
||||
<Send className="mr-2 h-4 w-4" aria-hidden="true" />
|
||||
Report error
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeError(error: unknown): { name: string; message: string; stack?: string } {
|
||||
if (error instanceof Error) {
|
||||
return {
|
||||
name: error.name || "Error",
|
||||
message: error.message || "Unknown route error",
|
||||
stack: error.stack,
|
||||
};
|
||||
}
|
||||
if (typeof error === "string") {
|
||||
return { name: "Error", message: error };
|
||||
}
|
||||
return { name: "Error", message: "Unknown route error", stack: safeJson(error) };
|
||||
}
|
||||
|
||||
function safeJson(value: unknown) {
|
||||
try {
|
||||
return JSON.stringify(value, null, 2);
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||
import { render } from "@testing-library/react";
|
||||
import { MemoryRouter, Route, Routes } from "react-router-dom";
|
||||
|
||||
// vi.hoisted shared state for all the stores / hooks the layout consumes.
|
||||
const state = vi.hoisted(() => ({
|
||||
user: null as { id: string } | null,
|
||||
isAuthLoading: false,
|
||||
overlay: null as { type: string } | null,
|
||||
workspace: null as { id: string; slug: string } | null,
|
||||
listFetched: true,
|
||||
wsList: [] as { id: string; slug: string }[],
|
||||
workspaceSeen: true,
|
||||
modalRenders: 0,
|
||||
modalAriaLabel: "source-backfill-modal-marker",
|
||||
}));
|
||||
|
||||
vi.mock("@multica/core/auth", () => {
|
||||
const useAuthStore = (selector: (s: typeof state) => unknown) => {
|
||||
if (selector.toString().includes("isLoading"))
|
||||
return state.isAuthLoading;
|
||||
return state.user;
|
||||
};
|
||||
return { useAuthStore };
|
||||
});
|
||||
|
||||
vi.mock("@multica/core/platform", () => ({
|
||||
setCurrentWorkspace: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@multica/core/workspace", async () => {
|
||||
const actual = await vi.importActual<typeof import("@multica/core/workspace")>(
|
||||
"@multica/core/workspace",
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
workspaceBySlugOptions: () => ({
|
||||
queryKey: ["workspace-by-slug"],
|
||||
queryFn: async () => state.workspace,
|
||||
}),
|
||||
workspaceListOptions: () => ({
|
||||
queryKey: ["workspace-list"],
|
||||
queryFn: async () => state.wsList,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@multica/core/paths", async () => {
|
||||
const actual = await vi.importActual<typeof import("@multica/core/paths")>(
|
||||
"@multica/core/paths",
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
WorkspaceSlugProvider: ({ children }: { children: React.ReactNode }) => (
|
||||
<>{children}</>
|
||||
),
|
||||
paths: {
|
||||
...actual.paths,
|
||||
login: () => "/login",
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@multica/views/workspace/use-workspace-seen", () => ({
|
||||
useWorkspaceSeen: () => state.workspaceSeen,
|
||||
}));
|
||||
|
||||
vi.mock("@multica/views/workspace/welcome-after-onboarding", () => ({
|
||||
WelcomeAfterOnboarding: () => null,
|
||||
}));
|
||||
|
||||
vi.mock("@multica/views/layout", () => ({
|
||||
WorkspacePresencePrefetch: () => null,
|
||||
}));
|
||||
|
||||
// The point of this whole test: assert the desktop layout mounts the
|
||||
// SourceBackfillModal. We stub the real component with a marker that
|
||||
// renders only when the layout actually rendered it (and not e.g.
|
||||
// suppressed by overlayActive).
|
||||
vi.mock("@multica/views/onboarding", () => ({
|
||||
SourceBackfillModal: () => {
|
||||
state.modalRenders += 1;
|
||||
return <div data-testid={state.modalAriaLabel} />;
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/stores/tab-store", () => ({
|
||||
useTabStore: Object.assign(() => null, {
|
||||
getState: () => ({ validateWorkspaceSlugs: vi.fn() }),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/stores/window-overlay-store", () => {
|
||||
const useWindowOverlayStore = (selector: (s: typeof state) => unknown) =>
|
||||
selector(state);
|
||||
return { useWindowOverlayStore };
|
||||
});
|
||||
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { WorkspaceRouteLayout } from "./workspace-route-layout";
|
||||
|
||||
function renderLayout() {
|
||||
const qc = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false, gcTime: 0 } },
|
||||
});
|
||||
// Seed the workspace queries so the gate inside the layout passes
|
||||
// synchronously — the real hook reads from cache.
|
||||
qc.setQueryData(["workspace-by-slug"], state.workspace);
|
||||
qc.setQueryData(["workspace-list"], state.wsList);
|
||||
return render(
|
||||
<QueryClientProvider client={qc}>
|
||||
<MemoryRouter initialEntries={["/acme/issues"]}>
|
||||
<Routes>
|
||||
<Route path=":workspaceSlug/*" element={<WorkspaceRouteLayout />}>
|
||||
<Route path="*" element={<div data-testid="outlet" />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
state.user = { id: "u1" };
|
||||
state.isAuthLoading = false;
|
||||
state.overlay = null;
|
||||
state.workspace = { id: "ws-1", slug: "acme" };
|
||||
state.listFetched = true;
|
||||
state.wsList = [{ id: "ws-1", slug: "acme" }];
|
||||
state.workspaceSeen = true;
|
||||
state.modalRenders = 0;
|
||||
});
|
||||
|
||||
describe("WorkspaceRouteLayout", () => {
|
||||
it("mounts SourceBackfillModal when no WindowOverlay is active", () => {
|
||||
const { queryByTestId } = renderLayout();
|
||||
expect(queryByTestId(state.modalAriaLabel)).not.toBeNull();
|
||||
expect(state.modalRenders).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("suppresses SourceBackfillModal while a WindowOverlay is active", () => {
|
||||
state.overlay = { type: "new-workspace" };
|
||||
const { queryByTestId } = renderLayout();
|
||||
expect(queryByTestId(state.modalAriaLabel)).toBeNull();
|
||||
expect(state.modalRenders).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -11,7 +11,6 @@ import { useAuthStore } from "@multica/core/auth";
|
||||
import { useWorkspaceSeen } from "@multica/views/workspace/use-workspace-seen";
|
||||
import { WelcomeAfterOnboarding } from "@multica/views/workspace/welcome-after-onboarding";
|
||||
import { WorkspacePresencePrefetch } from "@multica/views/layout";
|
||||
import { SourceBackfillModal } from "@multica/views/onboarding";
|
||||
import { useTabStore } from "@/stores/tab-store";
|
||||
import { useWindowOverlayStore } from "@/stores/window-overlay-store";
|
||||
|
||||
@@ -105,13 +104,6 @@ export function WorkspaceRouteLayout() {
|
||||
* Modal — unless the store signal has already been consumed, in
|
||||
* which case the hook renders null. */}
|
||||
{!overlayActive && <WelcomeAfterOnboarding />}
|
||||
{/* Source-attribution backfill: same Dialog the web shell mounts
|
||||
* inside DashboardLayout. Desktop's WorkspaceRouteLayout doesn't
|
||||
* wrap DashboardLayout, so the modal has to be wired in directly
|
||||
* here. Same overlay-suppression rule as WelcomeAfterOnboarding —
|
||||
* a portal-rendered Dialog at z-50 would otherwise sit above an
|
||||
* active pre-workspace overlay. */}
|
||||
{!overlayActive && <SourceBackfillModal />}
|
||||
</WorkspaceSlugProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const chineseFonts = ["PingFang SC", "Microsoft YaHei", "Noto Sans CJK SC"];
|
||||
const koreanFonts = ["Apple SD Gothic Neo", "Malgun Gothic", "Noto Sans CJK KR"];
|
||||
const japaneseFonts = ["Hiragino Sans", "Yu Gothic", "Noto Sans CJK JP"];
|
||||
|
||||
function expectChineseFontsBeforeKoreanFonts(source: string) {
|
||||
const chineseIndexes = chineseFonts.map((font) => source.indexOf(font));
|
||||
const koreanIndexes = koreanFonts.map((font) => source.indexOf(font));
|
||||
|
||||
expect(chineseIndexes).not.toContain(-1);
|
||||
expect(koreanIndexes).not.toContain(-1);
|
||||
|
||||
for (const chineseIndex of chineseIndexes) {
|
||||
for (const koreanIndex of koreanIndexes) {
|
||||
expect(chineseIndex).toBeLessThan(koreanIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Japanese Kanji share the Han Unicode block with Chinese, so the global
|
||||
// Chinese-first stack must stay Chinese-first (no zh regression) while a
|
||||
// Japanese-first CJK stack is scoped to html[lang|="ja"]. App.tsx syncs
|
||||
// document.documentElement.lang so the selector matches at runtime.
|
||||
function expectJapaneseScopedOverride(source: string) {
|
||||
expect(source).toContain('html[lang|="ja"]');
|
||||
|
||||
const japaneseIndexes = japaneseFonts.map((font) => source.indexOf(font));
|
||||
expect(japaneseIndexes).not.toContain(-1);
|
||||
|
||||
const firstJapanese = Math.min(...japaneseIndexes);
|
||||
const lastChinese = Math.max(
|
||||
...chineseFonts.map((font) => source.lastIndexOf(font)),
|
||||
);
|
||||
expect(firstJapanese).toBeLessThan(lastChinese);
|
||||
}
|
||||
|
||||
describe("CJK font fallback order", () => {
|
||||
it("keeps desktop Chinese font fallbacks before Korean font fallbacks", () => {
|
||||
const desktopCss = readFileSync(
|
||||
resolve(process.cwd(), "src/renderer/src/globals.css"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
expectChineseFontsBeforeKoreanFonts(desktopCss);
|
||||
});
|
||||
|
||||
it("scopes the Japanese-first CJK stack to html[lang|='ja']", () => {
|
||||
const desktopCss = readFileSync(
|
||||
resolve(process.cwd(), "src/renderer/src/globals.css"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
expectJapaneseScopedOverride(desktopCss);
|
||||
});
|
||||
});
|
||||
@@ -6,15 +6,16 @@
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
/* Font stack: Inter for Latin UI text + system CJK fonts for localized content.
|
||||
/* Font stack: Inter for Latin UI text + system Chinese fonts for zh content.
|
||||
Web app uses the same stack via next/font/google in apps/web/app/layout.tsx —
|
||||
keep the CJK fallback tail in sync across both files. The Inter primary family
|
||||
differs by design: next/font produces `__Inter_xxx` (with a synthetic size-adjusted
|
||||
fallback face to prevent FOUT layout shift); desktop uses fontsource's "Inter Variable".
|
||||
Both resolve to Inter glyphs, so rendering is identical in practice.
|
||||
Per-character fallback: Latin chars render with Inter, CJK chars render with
|
||||
the platform-native Chinese/Korean fallback when needed. Chinese fonts must stay
|
||||
before Korean fonts so zh users do not receive Korean Hanja glyph shapes.
|
||||
Currently covers English + Simplified Chinese. When ja/ko i18n lands, extend
|
||||
the tail with Hiragino Kaku Gothic ProN / Yu Gothic / Apple SD Gothic Neo / Malgun Gothic.
|
||||
Per-character fallback: Latin chars render with Inter, Chinese chars with
|
||||
PingFang SC (macOS) / Microsoft YaHei (Windows) / Noto Sans CJK SC (Linux).
|
||||
|
||||
Mono font has no explicit CJK fallback: CJK chars in code blocks are inherently
|
||||
non-aligned with a mono grid (Chinese is proportional), so listing CJK fonts
|
||||
@@ -22,35 +23,14 @@
|
||||
the rare mixed case correctly. */
|
||||
:root {
|
||||
--font-sans: "Inter Variable", "Inter", -apple-system, BlinkMacSystemFont,
|
||||
"Segoe UI", "PingFang SC", "Microsoft YaHei",
|
||||
"Noto Sans CJK SC", "Apple SD Gothic Neo", "Malgun Gothic",
|
||||
"Noto Sans CJK KR", sans-serif;
|
||||
"Segoe UI", "PingFang SC", "Microsoft YaHei", "Noto Sans CJK SC",
|
||||
sans-serif;
|
||||
--font-serif: "Source Serif 4 Variable", "Source Serif 4", "Iowan Old Style",
|
||||
"Apple Garamond", Baskerville, "Times New Roman", serif;
|
||||
--font-mono: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Consolas,
|
||||
monospace;
|
||||
}
|
||||
|
||||
/* Japanese-scoped CJK override. Japanese Kanji share the Han Unicode block
|
||||
with Chinese, and CSS font-fallback order is not changed by <html lang> —
|
||||
so the global Chinese-first stack above would give Japanese users Chinese
|
||||
glyph shapes for shared ideographs. We keep the global stack Chinese-first
|
||||
(no regression for zh users) and promote Japanese fonts ahead of the
|
||||
Chinese/Korean families only when the locale is Japanese. App.tsx syncs
|
||||
document.documentElement.lang to the active locale so this selector
|
||||
matches. Mirrors the lang-scoped override in apps/web/app/layout.tsx.
|
||||
`[lang|="ja"]` is the BCP-47 language-range selector: it matches exactly
|
||||
`ja` or `ja-<region>` (App.tsx sets `ja-JP`), never unrelated subtags
|
||||
such as `jam`. */
|
||||
html[lang|="ja"] {
|
||||
--font-sans: "Inter Variable", "Inter", "Hiragino Sans",
|
||||
"Hiragino Kaku Gothic ProN", "Yu Gothic", "YuGothic", "Meiryo",
|
||||
"Noto Sans CJK JP", "Noto Sans JP", -apple-system,
|
||||
BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei",
|
||||
"Noto Sans CJK SC", "Apple SD Gothic Neo", "Malgun Gothic",
|
||||
"Noto Sans CJK KR", sans-serif;
|
||||
}
|
||||
|
||||
@source "../../../../../packages/ui/**/*.tsx";
|
||||
@source "../../../../../packages/core/**/*.{ts,tsx}";
|
||||
@source "../../../../../packages/views/**/*.{ts,tsx}";
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { IssueDetail } from "@multica/views/issues/components";
|
||||
import { ErrorBoundary } from "@multica/ui/components/common/error-boundary";
|
||||
import { useWorkspaceId } from "@multica/core/hooks";
|
||||
import { issueDetailOptions } from "@multica/core/issues/queries";
|
||||
import { useDocumentTitle } from "@/hooks/use-document-title";
|
||||
@@ -13,8 +14,9 @@ export function IssueDetailPage() {
|
||||
useDocumentTitle(issue ? `${issue.identifier}: ${issue.title}` : "Issue");
|
||||
|
||||
if (!id) return null;
|
||||
// Render errors bubble to the root route errorElement (DesktopRouteErrorPage),
|
||||
// which contains the crash inside the tab content pane. No page-level boundary
|
||||
// here — a whole-page wrapper duplicates the route-level error UI.
|
||||
return <IssueDetail issueId={id} />;
|
||||
return (
|
||||
<ErrorBoundary resetKeys={[id]}>
|
||||
<IssueDetail issueId={id} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,14 +11,7 @@ import type { AgentRuntime } from "@multica/core/types";
|
||||
* to the desktop preload typings (which live in apps/desktop/src/preload).
|
||||
*/
|
||||
interface DaemonStatusLike {
|
||||
state:
|
||||
| "running"
|
||||
| "stopped"
|
||||
| "starting"
|
||||
| "stopping"
|
||||
| "installing_cli"
|
||||
| "cli_not_found"
|
||||
| "auth_expired";
|
||||
state: "running" | "stopped" | "starting" | "stopping" | "installing_cli" | "cli_not_found";
|
||||
daemonId?: string;
|
||||
}
|
||||
|
||||
@@ -32,11 +25,7 @@ interface DaemonStatusLike {
|
||||
* within 75s.
|
||||
*/
|
||||
function mergeDaemonStatus(rt: AgentRuntime, status: DaemonStatusLike): AgentRuntime {
|
||||
if (
|
||||
status.state === "stopped" ||
|
||||
status.state === "stopping" ||
|
||||
status.state === "auth_expired"
|
||||
) {
|
||||
if (status.state === "stopped" || status.state === "stopping") {
|
||||
return { ...rt, status: "offline" };
|
||||
}
|
||||
if (status.state === "running") {
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { mockGetState, logout } = vi.hoisted(() => ({
|
||||
mockGetState: vi.fn(),
|
||||
logout: vi.fn(),
|
||||
}));
|
||||
|
||||
const { toastError } = vi.hoisted(() => ({ toastError: vi.fn() }));
|
||||
|
||||
vi.mock("@multica/core/auth", () => ({
|
||||
useAuthStore: { getState: mockGetState },
|
||||
}));
|
||||
|
||||
vi.mock("sonner", () => ({
|
||||
toast: { error: toastError },
|
||||
}));
|
||||
|
||||
import { reauthenticateDaemon } from "./daemon-reauth";
|
||||
|
||||
const daemonAPI = {
|
||||
reauthenticate: vi.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
localStorage.clear();
|
||||
(window as unknown as { daemonAPI: typeof daemonAPI }).daemonAPI = daemonAPI;
|
||||
mockGetState.mockReturnValue({ user: { id: "user-1" }, logout });
|
||||
});
|
||||
|
||||
describe("reauthenticateDaemon", () => {
|
||||
it("re-mints + restarts the daemon when signed in, without logging out", async () => {
|
||||
localStorage.setItem("multica_token", "jwt-abc");
|
||||
daemonAPI.reauthenticate.mockResolvedValue({ ok: true });
|
||||
|
||||
await reauthenticateDaemon();
|
||||
|
||||
expect(daemonAPI.reauthenticate).toHaveBeenCalledWith("jwt-abc", "user-1");
|
||||
expect(logout).not.toHaveBeenCalled();
|
||||
expect(toastError).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("logs out only when the session token itself is rejected (401)", async () => {
|
||||
localStorage.setItem("multica_token", "jwt-abc");
|
||||
daemonAPI.reauthenticate.mockResolvedValue({
|
||||
ok: false,
|
||||
reason: "session_invalid",
|
||||
});
|
||||
|
||||
await reauthenticateDaemon();
|
||||
|
||||
expect(logout).toHaveBeenCalledOnce();
|
||||
expect(toastError).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// The reviewer's must-fix: a non-401 (transient) failure must NOT log the
|
||||
// user out — they stay signed in and can retry.
|
||||
it("does NOT log out on a transient failure; shows a retryable toast", async () => {
|
||||
localStorage.setItem("multica_token", "jwt-abc");
|
||||
daemonAPI.reauthenticate.mockResolvedValue({
|
||||
ok: false,
|
||||
reason: "transient",
|
||||
message: "mint PAT failed: 503 Service Unavailable",
|
||||
});
|
||||
|
||||
await reauthenticateDaemon();
|
||||
|
||||
expect(logout).not.toHaveBeenCalled();
|
||||
expect(toastError).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("does NOT log out when the IPC call itself throws unexpectedly", async () => {
|
||||
localStorage.setItem("multica_token", "jwt-abc");
|
||||
daemonAPI.reauthenticate.mockRejectedValue(new Error("ipc boom"));
|
||||
|
||||
await reauthenticateDaemon();
|
||||
|
||||
expect(logout).not.toHaveBeenCalled();
|
||||
expect(toastError).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("routes to login when there is no session token", async () => {
|
||||
await reauthenticateDaemon();
|
||||
|
||||
expect(logout).toHaveBeenCalledOnce();
|
||||
expect(daemonAPI.reauthenticate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("routes to login when there is no signed-in user", async () => {
|
||||
localStorage.setItem("multica_token", "jwt-abc");
|
||||
mockGetState.mockReturnValue({ user: null, logout });
|
||||
|
||||
await reauthenticateDaemon();
|
||||
|
||||
expect(logout).toHaveBeenCalledOnce();
|
||||
expect(daemonAPI.reauthenticate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,48 +0,0 @@
|
||||
import { useAuthStore } from "@multica/core/auth";
|
||||
import { toast } from "sonner";
|
||||
|
||||
/**
|
||||
* Re-establish the local daemon's credentials after it failed to authenticate
|
||||
* (daemon state "auth_expired", surfaced by daemon-manager's token probe — see
|
||||
* #3512).
|
||||
*
|
||||
* The desktop owns the daemon's PAT: it mints one from the user's session token
|
||||
* and caches it per profile. A stale/revoked cached PAT is the common cause (and
|
||||
* merely restarting the app reuses the same bad PAT), so the main process drops
|
||||
* the cached token, mints a fresh one, and restarts the daemon.
|
||||
*
|
||||
* Failure handling is deliberately conservative — we only force a full re-login
|
||||
* when the session token itself is rejected (a real 401). A transient failure
|
||||
* (mint 5xx, network blip, config write error, restart hiccup) keeps the user
|
||||
* signed in and shows a retryable toast, so a momentary glitch never logs them
|
||||
* out. The 401-vs-transient classification happens in the main process where the
|
||||
* real HTTP status is available; here we just act on the verdict.
|
||||
*/
|
||||
export async function reauthenticateDaemon(): Promise<void> {
|
||||
const user = useAuthStore.getState().user;
|
||||
const token = localStorage.getItem("multica_token");
|
||||
if (!user || !token) {
|
||||
// No usable session at all — the standard recovery is the login page.
|
||||
useAuthStore.getState().logout();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await window.daemonAPI.reauthenticate(token, user.id);
|
||||
if (result.ok) return; // daemon restarting; status flips via onStatusChange
|
||||
if (result.reason === "session_invalid") {
|
||||
// The session token itself is rejected (401) — full re-login.
|
||||
useAuthStore.getState().logout();
|
||||
return;
|
||||
}
|
||||
// Transient failure — keep the user signed in and let them retry.
|
||||
toast.error("Couldn't reconnect the daemon", {
|
||||
description: result.message || "Please try again in a moment.",
|
||||
});
|
||||
} catch (err) {
|
||||
// An unexpected IPC error is not an auth failure — never log out on it.
|
||||
toast.error("Couldn't reconnect the daemon", {
|
||||
description: err instanceof Error ? err.message : "Please try again.",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -21,16 +21,16 @@ import { AutopilotsPage } from "@multica/views/autopilots/components";
|
||||
import { MyIssuesPage } from "@multica/views/my-issues";
|
||||
import { SkillsPage } from "@multica/views/skills";
|
||||
import { DesktopRuntimesPage } from "./components/desktop-runtimes-page";
|
||||
import { DesktopAgentsPage } from "./components/desktop-agents-page";
|
||||
import { AgentsPage } from "@multica/views/agents";
|
||||
import { SquadsPage, SquadDetailPage as SquadDetailPageView } from "@multica/views/squads/components";
|
||||
import { InboxPage } from "@multica/views/inbox";
|
||||
import { SettingsPage } from "@multica/views/settings";
|
||||
import { useT } from "@multica/views/i18n";
|
||||
import { ErrorBoundary } from "@multica/ui/components/common/error-boundary";
|
||||
import { Download, Server } from "lucide-react";
|
||||
import { DaemonSettingsTab } from "./components/daemon-settings-tab";
|
||||
import { UpdatesSettingsTab } from "./components/updates-settings-tab";
|
||||
import { WorkspaceRouteLayout } from "./components/workspace-route-layout";
|
||||
import { DesktopRouteErrorPage } from "./components/route-error-page";
|
||||
|
||||
/**
|
||||
* Wraps `SettingsPage` so the desktop-only extra tabs can pull their labels
|
||||
@@ -109,7 +109,6 @@ function PageShell() {
|
||||
export const appRoutes: RouteObject[] = [
|
||||
{
|
||||
element: <PageShell />,
|
||||
errorElement: <DesktopRouteErrorPage />,
|
||||
children: [
|
||||
{ index: true, element: null },
|
||||
{
|
||||
@@ -119,7 +118,11 @@ export const appRoutes: RouteObject[] = [
|
||||
{ index: true, element: <Navigate to="issues" replace /> },
|
||||
{
|
||||
path: "issues",
|
||||
element: <IssuesPage />,
|
||||
element: (
|
||||
<ErrorBoundary>
|
||||
<IssuesPage />
|
||||
</ErrorBoundary>
|
||||
),
|
||||
handle: { title: "Issues" },
|
||||
},
|
||||
{
|
||||
@@ -168,7 +171,7 @@ export const appRoutes: RouteObject[] = [
|
||||
element: <SkillDetailPage />,
|
||||
handle: { title: "Skill" },
|
||||
},
|
||||
{ path: "agents", element: <DesktopAgentsPage />, handle: { title: "Agents" } },
|
||||
{ path: "agents", element: <AgentsPage />, handle: { title: "Agents" } },
|
||||
{
|
||||
path: "agents/:id",
|
||||
element: <AgentDetailPage />,
|
||||
|
||||
@@ -259,47 +259,6 @@ describe("useTabStore actions", () => {
|
||||
expect(s.activeWorkspaceSlug).toBeNull();
|
||||
});
|
||||
|
||||
it("validateWorkspaceSlugs seeds the first valid workspace when no group exists", () => {
|
||||
const store = useTabStore.getState();
|
||||
store.validateWorkspaceSlugs(new Set(["acme", "butter"]));
|
||||
const s = useTabStore.getState();
|
||||
expect(s.activeWorkspaceSlug).toBe("acme");
|
||||
expect(s.byWorkspace.acme.tabs).toHaveLength(1);
|
||||
expect(s.byWorkspace.acme.tabs[0].path).toBe("/acme/issues");
|
||||
});
|
||||
|
||||
it("validateWorkspaceSlugs reactivates an existing valid group before seeding", () => {
|
||||
const store = useTabStore.getState();
|
||||
store.switchWorkspace("acme");
|
||||
const existingTabId = useTabStore.getState().byWorkspace.acme.tabs[0].id;
|
||||
|
||||
useTabStore.setState({ activeWorkspaceSlug: null });
|
||||
store.validateWorkspaceSlugs(new Set(["acme"]));
|
||||
|
||||
const s = useTabStore.getState();
|
||||
expect(s.activeWorkspaceSlug).toBe("acme");
|
||||
expect(s.byWorkspace.acme.tabs).toHaveLength(1);
|
||||
expect(s.byWorkspace.acme.tabs[0].id).toBe(existingTabId);
|
||||
});
|
||||
|
||||
it("validateWorkspaceSlugs seeds a fresh tab for a valid slug after dropping all stale groups", () => {
|
||||
const store = useTabStore.getState();
|
||||
// The only persisted group points at a workspace the user has lost access
|
||||
// to — the stale-tab heal path WorkspaceRouteLayout drives.
|
||||
store.switchWorkspace("stale");
|
||||
const staleRouter = useTabStore.getState().byWorkspace.stale.tabs[0].router;
|
||||
|
||||
store.validateWorkspaceSlugs(new Set(["acme"]));
|
||||
|
||||
const s = useTabStore.getState();
|
||||
expect(Object.keys(s.byWorkspace)).toEqual(["acme"]);
|
||||
expect(s.activeWorkspaceSlug).toBe("acme");
|
||||
expect(s.byWorkspace.acme.tabs).toHaveLength(1);
|
||||
expect(s.byWorkspace.acme.tabs[0].path).toBe("/acme/issues");
|
||||
// The dropped stale group's router must be disposed, not leaked.
|
||||
expect(staleRouter.dispose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("reset wipes the whole store", () => {
|
||||
const store = useTabStore.getState();
|
||||
store.switchWorkspace("acme");
|
||||
|
||||
@@ -86,16 +86,6 @@ interface TabStore {
|
||||
updateTab: (tabId: string, patch: Partial<Pick<Tab, "path" | "title" | "icon">>) => void;
|
||||
/** Patch history tracking of a tab. Finds across groups. */
|
||||
updateTabHistory: (tabId: string, historyIndex: number, historyLength: number) => void;
|
||||
/** Recreate the active tab's router at the same path after a route-level crash. */
|
||||
reloadActiveTab: () => void;
|
||||
/**
|
||||
* Close the active tab. The always-safe escape from a route-level crash:
|
||||
* unlike reloadActiveTab (recreates the same crashing path) or navigating
|
||||
* to a "safe" route (which may itself be the route that crashed), closing
|
||||
* destroys the crashing router entirely and falls back to a sibling tab
|
||||
* (or a reseeded default if it was the last tab).
|
||||
*/
|
||||
closeActiveTab: () => void;
|
||||
/**
|
||||
* Reorder within the active workspace's group only. Clamped so a tab can
|
||||
* never cross the pinned / unpinned boundary — a drag that would move a
|
||||
@@ -485,38 +475,6 @@ export const useTabStore = create<TabStore>()(
|
||||
});
|
||||
},
|
||||
|
||||
reloadActiveTab() {
|
||||
const { activeWorkspaceSlug, byWorkspace } = get();
|
||||
if (!activeWorkspaceSlug) return;
|
||||
const group = byWorkspace[activeWorkspaceSlug];
|
||||
if (!group) return;
|
||||
const index = group.tabs.findIndex((t) => t.id === group.activeTabId);
|
||||
if (index < 0) return;
|
||||
const current = group.tabs[index];
|
||||
const nextTabs = [...group.tabs];
|
||||
nextTabs[index] = {
|
||||
...current,
|
||||
router: createTabRouter(current.path),
|
||||
historyIndex: 0,
|
||||
historyLength: 1,
|
||||
};
|
||||
set({
|
||||
byWorkspace: {
|
||||
...byWorkspace,
|
||||
[activeWorkspaceSlug]: { ...group, tabs: nextTabs },
|
||||
},
|
||||
});
|
||||
window.setTimeout(() => current.router.dispose(), 0);
|
||||
},
|
||||
|
||||
closeActiveTab() {
|
||||
const { activeWorkspaceSlug, byWorkspace, closeTab } = get();
|
||||
if (!activeWorkspaceSlug) return;
|
||||
const group = byWorkspace[activeWorkspaceSlug];
|
||||
if (!group) return;
|
||||
closeTab(group.activeTabId);
|
||||
},
|
||||
|
||||
moveTab(fromIndex, toIndex) {
|
||||
if (fromIndex === toIndex) return;
|
||||
const { activeWorkspaceSlug, byWorkspace } = get();
|
||||
@@ -599,24 +557,6 @@ export const useTabStore = create<TabStore>()(
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (!nextActive) {
|
||||
nextActive = Object.keys(nextByWorkspace)[0] ?? null;
|
||||
if (nextActive) changed = true;
|
||||
}
|
||||
|
||||
if (!nextActive) {
|
||||
const fallbackSlug = validSlugs.values().next().value;
|
||||
if (fallbackSlug) {
|
||||
const fresh = defaultTabFor(fallbackSlug);
|
||||
nextByWorkspace[fallbackSlug] = {
|
||||
tabs: [fresh],
|
||||
activeTabId: fresh.id,
|
||||
};
|
||||
nextActive = fallbackSlug;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!changed) return;
|
||||
set({ byWorkspace: nextByWorkspace, activeWorkspaceSlug: nextActive });
|
||||
},
|
||||
|
||||
@@ -4,11 +4,7 @@ export type DaemonState =
|
||||
| "starting"
|
||||
| "stopping"
|
||||
| "installing_cli"
|
||||
| "cli_not_found"
|
||||
// The daemon can't start because the server rejected its credentials (the
|
||||
// cached PAT expired / was revoked, or the session token is dead). Without
|
||||
// this, an auth failure silently sticks at "starting" forever — see #3512.
|
||||
| "auth_expired";
|
||||
| "cli_not_found";
|
||||
|
||||
export interface DaemonStatus {
|
||||
state: DaemonState;
|
||||
@@ -36,7 +32,6 @@ export const DAEMON_STATE_COLORS: Record<DaemonState, string> = {
|
||||
stopping: "bg-amber-500 animate-pulse",
|
||||
installing_cli: "bg-sky-500 animate-pulse",
|
||||
cli_not_found: "bg-red-500",
|
||||
auth_expired: "bg-red-500",
|
||||
};
|
||||
|
||||
export const DAEMON_STATE_LABELS: Record<DaemonState, string> = {
|
||||
@@ -46,7 +41,6 @@ export const DAEMON_STATE_LABELS: Record<DaemonState, string> = {
|
||||
stopping: "Stopping…",
|
||||
installing_cli: "Setting up…",
|
||||
cli_not_found: "Setup Failed",
|
||||
auth_expired: "Sign-in required",
|
||||
};
|
||||
|
||||
export function formatUptime(uptime?: string): string {
|
||||
@@ -87,7 +81,5 @@ export function daemonStateDescription(state: DaemonState, runtimeCount: number)
|
||||
return "Setting up the runtime for the first time. Only happens once.";
|
||||
case "cli_not_found":
|
||||
return "Setup failed · couldn't download the runtime. Check your network.";
|
||||
case "auth_expired":
|
||||
return "Sign-in expired · sign in again to bring this device back online.";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import type { Metadata } from "next";
|
||||
import { docsAlternates } from "@/lib/site";
|
||||
import { i18n, type Lang } from "@/lib/i18n";
|
||||
import { DocsLocaleProvider, LocaleLink } from "@/components/locale-link";
|
||||
import { docsSlugStaticParams } from "@/lib/static-params";
|
||||
|
||||
function asLang(lang: string): Lang {
|
||||
return (i18n.languages as readonly string[]).includes(lang)
|
||||
@@ -23,11 +22,11 @@ export default async function Page(props: {
|
||||
params: Promise<{ lang: string; slug: string[] }>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const lang = asLang(params.lang);
|
||||
const page = source.getPage(params.slug, lang);
|
||||
const page = source.getPage(params.slug, params.lang);
|
||||
if (!page) notFound();
|
||||
|
||||
const MDX = page.data.body;
|
||||
const lang = asLang(params.lang);
|
||||
|
||||
return (
|
||||
<DocsPage toc={page.data.toc}>
|
||||
@@ -43,15 +42,14 @@ export default async function Page(props: {
|
||||
}
|
||||
|
||||
export function generateStaticParams() {
|
||||
return docsSlugStaticParams(source.generateParams());
|
||||
return source.generateParams().filter((p) => p.slug.length > 0);
|
||||
}
|
||||
|
||||
export async function generateMetadata(props: {
|
||||
params: Promise<{ lang: string; slug: string[] }>;
|
||||
}): Promise<Metadata> {
|
||||
const params = await props.params;
|
||||
const lang = asLang(params.lang);
|
||||
const page = source.getPage(params.slug, lang);
|
||||
const page = source.getPage(params.slug, params.lang);
|
||||
if (!page) notFound();
|
||||
|
||||
return {
|
||||
|
||||
@@ -11,13 +11,18 @@ import { i18n, type Lang } from "@/lib/i18n";
|
||||
import { uiTranslations, localeLabels } from "@/lib/translations";
|
||||
import { DocsSettings } from "@/components/docs-settings";
|
||||
|
||||
// Inter (Latin UI face) is exposed under `--font-inter`. The full `--font-sans`
|
||||
// stack — Inter + the per-locale CJK fallback chain, including the Japanese-first
|
||||
// override scoped to `<html lang="ja">` — is composed in static CSS in
|
||||
// ./global.css (CSP-safe, no inline <style>). Mirrors apps/web/app/layout.tsx.
|
||||
const inter = Inter({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-inter",
|
||||
variable: "--font-sans",
|
||||
fallback: [
|
||||
"-apple-system",
|
||||
"BlinkMacSystemFont",
|
||||
"Segoe UI",
|
||||
"PingFang SC",
|
||||
"Microsoft YaHei",
|
||||
"Noto Sans CJK SC",
|
||||
"sans-serif",
|
||||
],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
|
||||
@@ -17,42 +17,8 @@ function tokenizeCJK(raw: string): string[] {
|
||||
return tokens;
|
||||
}
|
||||
|
||||
// Japanese mixes Hiragana, Katakana and Kanji; the English regex strips them
|
||||
// all, and the zh tokenizer only keeps Han (Kanji), dropping kana entirely.
|
||||
// Tokenize each kana/Kanji codepoint on its own and keep Latin/digit runs
|
||||
// whole — same character-level recall strategy as tokenizeCJK, extended to
|
||||
// the Hiragana (\u3040-\u309f) and Katakana (\u30a0-\u30ff) blocks, plus the
|
||||
// ideographic iteration mark \u3005 which sits just below the kana blocks and
|
||||
// recurs in common words (e.g. the JP for "various", "daily", "individual").
|
||||
function tokenizeJapanese(raw: string): string[] {
|
||||
const tokens: string[] = [];
|
||||
const regex = /[\u3005\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff]|[A-Za-z0-9]+/g;
|
||||
const lower = raw.toLowerCase();
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = regex.exec(lower)) !== null) {
|
||||
tokens.push(match[0]);
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
export const { GET } = createFromSource(source, {
|
||||
localeMap: {
|
||||
ko: {
|
||||
components: {
|
||||
tokenizer: {
|
||||
language: "english",
|
||||
},
|
||||
},
|
||||
},
|
||||
ja: {
|
||||
components: {
|
||||
tokenizer: {
|
||||
language: "english",
|
||||
normalizationCache: new Map(),
|
||||
tokenize: tokenizeJapanese,
|
||||
},
|
||||
},
|
||||
},
|
||||
zh: {
|
||||
components: {
|
||||
tokenizer: {
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const chineseFonts = ["PingFang SC", "Microsoft YaHei", "Noto Sans CJK SC"];
|
||||
const koreanFonts = ["Apple SD Gothic Neo", "Malgun Gothic", "Noto Sans CJK KR"];
|
||||
const japaneseFonts = ["Hiragino Sans", "Yu Gothic", "Noto Sans CJK JP"];
|
||||
|
||||
function expectChineseFontsBeforeKoreanFonts(source: string) {
|
||||
const chineseIndexes = chineseFonts.map((font) => source.indexOf(font));
|
||||
const koreanIndexes = koreanFonts.map((font) => source.indexOf(font));
|
||||
|
||||
expect(chineseIndexes).not.toContain(-1);
|
||||
expect(koreanIndexes).not.toContain(-1);
|
||||
|
||||
for (const chineseIndex of chineseIndexes) {
|
||||
for (const koreanIndex of koreanIndexes) {
|
||||
expect(chineseIndex).toBeLessThan(koreanIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Japanese Kanji share the Han Unicode block with Chinese, so the docs
|
||||
// Japanese-first CJK stack must be scoped to html[lang|="ja"] (zh/en keep
|
||||
// Chinese-first) and order Japanese fonts before the Chinese families.
|
||||
function expectJapaneseScopedOverride(source: string) {
|
||||
expect(source).toContain('html[lang|="ja"]');
|
||||
|
||||
const japaneseIndexes = japaneseFonts.map((font) => source.indexOf(font));
|
||||
expect(japaneseIndexes).not.toContain(-1);
|
||||
|
||||
const firstJapanese = Math.min(...japaneseIndexes);
|
||||
const lastChinese = Math.max(
|
||||
...chineseFonts.map((font) => source.lastIndexOf(font)),
|
||||
);
|
||||
expect(firstJapanese).toBeLessThan(lastChinese);
|
||||
}
|
||||
|
||||
describe("CJK font fallback order", () => {
|
||||
it("keeps docs Chinese font fallbacks before Korean font fallbacks", () => {
|
||||
const cssSource = readFileSync(
|
||||
resolve(process.cwd(), "app/global.css"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
expectChineseFontsBeforeKoreanFonts(cssSource);
|
||||
});
|
||||
|
||||
it("scopes the Japanese-first CJK stack to html[lang|='ja']", () => {
|
||||
const cssSource = readFileSync(
|
||||
resolve(process.cwd(), "app/global.css"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
expectJapaneseScopedOverride(cssSource);
|
||||
});
|
||||
});
|
||||
@@ -6,36 +6,6 @@
|
||||
|
||||
@source "../../../packages/ui/**/*.{ts,tsx}";
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* Font stack. `--font-inter` is the next/font Inter family (+ synthetic
|
||||
* size-adjusted fallback), set on <html> by inter.variable in app/[lang]/layout.tsx.
|
||||
* `--font-sans` is composed here in static CSS so it can be overridden per
|
||||
* `<html lang>` and stays CSP-safe (no inline <style>). Tailwind's `font-sans`
|
||||
* utility resolves `var(--font-sans)`. Mirrors apps/web/app/globals.css.
|
||||
*
|
||||
* Default (en / zh / ko): Latin → Inter, CJK → Chinese then Korean. Chinese MUST
|
||||
* stay before Korean so zh users don't get Korean Hanja glyph shapes.
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
:root {
|
||||
--font-sans: var(--font-inter), -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||
"PingFang SC", "Microsoft YaHei", "Noto Sans CJK SC", "Apple SD Gothic Neo",
|
||||
"Malgun Gothic", "Noto Sans CJK KR", sans-serif;
|
||||
}
|
||||
|
||||
/* Japanese: Kanji share the Han Unicode block with Chinese and CSS fallback
|
||||
order is not affected by `<html lang>`, so promote a Japanese-first CJK chain
|
||||
only for Japanese docs (`<html lang="ja">`). `[lang|="ja"]` is the BCP-47
|
||||
language-range selector — matches exactly `ja` or `ja-<region>`, never
|
||||
unrelated subtags like `jam`. Inter still leads for Latin. */
|
||||
html[lang|="ja"] {
|
||||
--font-sans: var(--font-inter), "Hiragino Sans", "Hiragino Kaku Gothic ProN",
|
||||
"Yu Gothic", "YuGothic", "Meiryo", "Noto Sans CJK JP", "Noto Sans JP",
|
||||
-apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
|
||||
"Microsoft YaHei", "Noto Sans CJK SC", "Apple SD Gothic Neo", "Malgun Gothic",
|
||||
"Noto Sans CJK KR", sans-serif;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* Multica Docs — editorial visual identity (v2)
|
||||
*
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
---
|
||||
title: エージェントの作成と構成
|
||||
description: エージェントを作成するために必要な最小限のフィールドと、すべての任意設定 — システム指示、環境変数、公開範囲、同時実行制限、アーカイブ。
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
|
||||
[エージェント](/agents)を作成するのに必要なのは 2 つだけです。**名前**と **[AI コーディングツール](/providers)の選択**です。それ以外はすべて任意です — システム指示、モデル、環境変数、CLI 引数、公開範囲、同時実行制限 — デフォルト値でも問題なく動作します。まず動かしてから後で調整しましょう。すべてのフィールドはいつでも変更できます。
|
||||
|
||||
## エージェントを作成する
|
||||
|
||||
前提条件: 使用中のマシンにサポートされている [AI コーディングツール](/providers)が少なくとも 1 つインストールされており(Claude Code、Codex など)、[デーモン](/daemon-runtimes)が実行中であること。まだそこまで準備できていない場合は、[Cloud クイックスタート](/cloud-quickstart)または[セルフホストクイックスタート](/self-host-quickstart)から始めてください。
|
||||
|
||||
準備が整ったら、ワークスペースの **Agents** ページに移動して **+ New** をクリックするか、CLI を使用します。
|
||||
|
||||
```bash
|
||||
multica agent create
|
||||
```
|
||||
|
||||
このフォームには必須フィールドが 2 つだけあります。**name**(ワークスペース内で一意であること)と **runtime**(= AI コーディングツールの選択)です。それ以外のすべてのフィールドは、以下でセクションごとに扱います。
|
||||
|
||||
## AI コーディングツールを選ぶ
|
||||
|
||||
各ランタイムは特定の AI コーディングツールを基盤としています。Multica はそのうち 12 個をサポートします。最も一般的な選択肢は次のとおりです。
|
||||
|
||||
| ツール | 適している場合 |
|
||||
|---|---|
|
||||
| **Claude Code** | Anthropic の公式ツールで、最も完成度の高い機能セットを提供します。**最初の選択として最適です** |
|
||||
| **Codex** | OpenAI 製で、主流の代替手段です |
|
||||
| **Cursor** | Cursor エディターのエコシステムを使うユーザー |
|
||||
| **Copilot** | GitHub アカウントの権限を活用するチーム |
|
||||
| **Gemini** | Google エコシステムのユーザー |
|
||||
|
||||
残りの 7 個(Antigravity、Hermes、Kimi、Kiro CLI、OpenCode、Pi、OpenClaw)と、各ツールの完全な機能比較表(セッション再開、MCP、スキル注入パス、モデル選択)は、[AI コーディングツール比較](/providers)で扱います。
|
||||
|
||||
## システム指示を書く
|
||||
|
||||
**システム指示**(`instructions`)はすべてのタスクの先頭に追加され、エージェントがどんな役割を担い、どんなルールに従うべきかを伝えます。
|
||||
|
||||
```text
|
||||
You're a frontend code-review agent. When an issue comes in, read the diff first. Focus only on:
|
||||
- Styling issues (tailwind class names, box model)
|
||||
- Accessibility (a11y)
|
||||
Don't change code — leave suggestions in a comment.
|
||||
```
|
||||
|
||||
空のままにすると(デフォルト)、エージェントは追加の制約なしに、基盤となる AI コーディングツールのネイティブな動作を使用します。
|
||||
|
||||
## モデルを選ぶ
|
||||
|
||||
ほとんどの AI コーディングツールはモデル選択をサポートしています(例えば Claude Code では Sonnet と Opus のどちらかを選べます)。空のままにするとツール自体のデフォルト値が使われ、明示的に 1 つを選ぶとそのモデルが実行されます。各ツールがサポートするモデルは、[AI コーディングツール比較](/providers)にまとめられています。
|
||||
|
||||
モデルの変更は**新しいタスクにのみ適用されます**。すでにディスパッチされたタスクは、ディスパッチ時点で固定されたモデルで実行を続けます。
|
||||
|
||||
## カスタム環境変数 (custom_env)
|
||||
|
||||
**カスタム環境変数**(`custom_env`)を使うと、タスク実行時に追加の環境変数を注入できます。代表的な用途は API キーの設定やアップストリームエンドポイントの切り替えです。
|
||||
|
||||
```
|
||||
ANTHROPIC_API_KEY = sk-...
|
||||
ANTHROPIC_BASE_URL = https://my-proxy.example.com
|
||||
```
|
||||
|
||||
システムにとって重要な変数は上書きできません。`PATH`、`HOME`、`USER`、`SHELL`、`TERM`、`CODEX_HOME`、そして `MULTICA_*` で始まるすべてのキーは、デーモンが静かに無視します(警告ログは残しますが、エラーは発生しません)。
|
||||
|
||||
<Callout type="warning">
|
||||
**`custom_env` の値は Multica サーバーのデータベースに平文で保存されます。** エージェントの list/get レスポンスには環境変数の値がまったく含まれなくなり、不透明な個数だけが返されます。実際の値を読み取るには、ワークスペースの owner または admin が、専用で監査される `GET /api/agents/{id}/env` エンドポイント(CLI: `multica agent env get <id>`)を呼び出す必要があります。タスクを実行中のエージェントは、ホストの owner 資格情報を使って他のエージェントの環境変数を明らかにすることはできません。このエンドポイントはエージェントアクターのセッションを拒否します。
|
||||
|
||||
**価値の高いシークレットは `custom_env` に入れないでください**(本番データベースのパスワード、root レベルのトークンなど)。エージェントには**権限範囲が限定された専用の資格情報**(読み取り専用 API キー、単一スコープの PAT)を使用し、定期的にローテーションしてください。データベースのバックアップと DB 監査は、依然として意味のある露出面として残ります。
|
||||
</Callout>
|
||||
|
||||
## カスタム CLI 引数 (custom_args)
|
||||
|
||||
**カスタム CLI 引数**(`custom_args`)は、AI コーディングツールのコマンドラインに 1 つずつ順に付け足される文字列配列です。
|
||||
|
||||
```json
|
||||
["--max-turns", "100", "--append-system-prompt", "always respond in Chinese"]
|
||||
```
|
||||
|
||||
最終的なコマンドは次のように生成されます。
|
||||
|
||||
```bash
|
||||
claude --model <model> --max-turns 100 --append-system-prompt "always respond in Chinese" [...]
|
||||
```
|
||||
|
||||
引数はシェルを介さずそのまま渡されるため(注入リスクなし)、特定のフラグが認識されるかどうかは AI コーディングツール自体に依存します。この部分はツールによって大きな差があります。
|
||||
|
||||
<Callout type="tip">
|
||||
`custom_env` と `custom_args` には厳格な上限はありませんが、実際には**それぞれ 10 個以内に抑えてください**。多すぎるとコマンドラインが長くなり、起動が遅くなり、メンテナンスも難しくなります。
|
||||
</Callout>
|
||||
|
||||
## 公開範囲
|
||||
|
||||
- **ワークスペース**(`workspace`) — ワークスペースのすべてのメンバーが割り当てできます
|
||||
- **非公開**(`private`) — ワークスペースの owner、admin、またはエージェントの作成者だけが割り当てできます
|
||||
|
||||
新しいエージェントはデフォルトで `private` です。
|
||||
|
||||
**非公開だからといって隠されるわけではありません** — すべてのメンバーが一覧で非公開エージェントの名前と説明を見ることができ、ただし機微な構成は読み取れません(環境変数の値はエージェントの list/get レスポンスに決して現れず、MCP 構成は owner 以外のユーザーにはマスキングされます)。詳しい意味は[エージェント → 誰がエージェントを割り当てられるか](/agents#who-can-assign-an-agent)を参照してください。
|
||||
|
||||
## 同時実行制限
|
||||
|
||||
**同時実行制限**(`max_concurrent_tasks`)は、このエージェントが一度に並列で実行できるタスク数を制御します。デフォルト値は **6** です。上限に達した新しいタスクは拒否されず、キューで待機します。
|
||||
|
||||
これは 2 段階の制限のうち「エージェント層」にすぎません。デーモン自体がより広い上限(デフォルト値 20)を適用し、2 つのうちより厳しい方が優先されます。詳しくは[デーモンとランタイム → 並列で何個のタスクを実行できるか](/daemon-runtimes#how-many-tasks-can-run-in-parallel)にあります。
|
||||
|
||||
この値を変更しても**すでに実行中のタスクはキャンセルされず**、次に処理されるタスクからのみ適用されます。
|
||||
|
||||
## ドメインの専門性をつなぐ: スキル
|
||||
|
||||
作成したエージェントには**スキル**をアタッチできます — タスク実行時に AI コーディングツールへ自動的に届けられる**ナレッジパック**(`SKILL.md` + 補助ファイル)です。新しいスキルを作成したり、GitHub または ClawHub からインポートしたり、マシン上の既存のスキルディレクトリからスキャンしたりできます。[スキル](/skills)を参照してください。
|
||||
|
||||
## アーカイブと復元
|
||||
|
||||
もう使わないエージェントは**アーカイブ**できます — 日常的な画面からは消えますが、履歴データ(実行したタスク、投稿したコメント)はすべてそのまま保持されます。いつでも**復元**して再び作業に投入できます。
|
||||
|
||||
<Callout type="warning">
|
||||
**アーカイブは、そのエージェントに属する未完了のすべてのタスクを即座にキャンセルします** — 実行中、ディスパッチ済み、キュー待ちのタスクがすべて `cancelled` としてマークされ、続行されません。進行中の重要なタスクがある場合は、アーカイブする前に最後まで完了させてください。
|
||||
</Callout>
|
||||
|
||||
アーカイブ済みのエージェントには新しいタスクを割り当てられません。
|
||||
|
||||
## 次のステップ
|
||||
|
||||
- [スキル](/skills) — エージェントにナレッジパックをアタッチする
|
||||
- [AI コーディングツール比較](/providers) — 12 個のツール全体の機能比較表
|
||||
- [エージェントへのイシューの割り当て](/assigning-issues) — 新しく作ったエージェントを作業に投入する
|
||||
@@ -1,127 +0,0 @@
|
||||
---
|
||||
title: 에이전트 생성 및 구성
|
||||
description: 에이전트를 생성하는 데 필요한 최소 필드와 모든 선택적 설정 — 시스템 지침, 환경 변수, 공개 범위, 동시 실행 제한, 보관.
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
|
||||
[에이전트](/agents)를 생성하는 데는 단 두 가지만 필요합니다. **이름** 하나와 **[AI 코딩 도구](/providers) 선택** 하나입니다. 나머지는 모두 선택 사항입니다 — 시스템 지침, 모델, 환경 변수, CLI 인자, 공개 범위, 동시 실행 제한 — 기본값으로도 문제없이 작동합니다. 먼저 실행해 보고 나중에 조정하세요. 모든 필드는 언제든지 변경할 수 있습니다.
|
||||
|
||||
## 에이전트 생성
|
||||
|
||||
전제 조건: 사용 중인 기기에 지원되는 [AI 코딩 도구](/providers)가 최소 하나는 설치되어 있고(Claude Code, Codex 등) [데몬](/daemon-runtimes)이 실행 중이어야 합니다. 아직 여기까지 준비되지 않았다면 [Cloud 빠른 시작](/cloud-quickstart)이나 [자체 호스팅 빠른 시작](/self-host-quickstart)부터 시작하세요.
|
||||
|
||||
준비가 끝나면 워크스페이스의 **에이전트** 페이지로 이동해 **+ New**를 클릭하거나 CLI를 사용하세요.
|
||||
|
||||
```bash
|
||||
multica agent create
|
||||
```
|
||||
|
||||
이 폼에는 필수 필드가 두 개뿐입니다. **이름**(워크스페이스 내에서 고유해야 함)과 **런타임**(= AI 코딩 도구 선택)입니다. 나머지 모든 필드는 아래에서 섹션별로 다룹니다.
|
||||
|
||||
## AI 코딩 도구 선택
|
||||
|
||||
각 런타임은 특정 AI 코딩 도구를 기반으로 합니다. Multica는 그중 12개를 지원합니다. 가장 일반적인 선택지는 다음과 같습니다.
|
||||
|
||||
| 도구 | 적합한 경우 |
|
||||
|---|---|
|
||||
| **Claude Code** | Anthropic의 공식 도구로, 가장 완성도 높은 기능 집합을 제공합니다. **첫 선택으로 가장 좋습니다** |
|
||||
| **Codex** | OpenAI 제품으로, 주류 대안입니다 |
|
||||
| **Cursor** | Cursor 에디터 생태계 사용자 |
|
||||
| **Copilot** | GitHub 계정 권한을 활용하는 팀 |
|
||||
| **Gemini** | Google 생태계 사용자 |
|
||||
|
||||
나머지 7개(Antigravity, Hermes, Kimi, Kiro CLI, OpenCode, Pi, OpenClaw)와 각 도구의 전체 기능 비교표(세션 재개, MCP, 스킬 주입 경로, 모델 선택)는 [AI 코딩 도구 비교](/providers)에서 다룹니다.
|
||||
|
||||
## 시스템 지침 작성
|
||||
|
||||
**시스템 지침**(`instructions`)은 모든 작업 앞에 추가되어, 에이전트가 어떤 역할을 맡고 어떤 규칙을 따라야 하는지 알려줍니다.
|
||||
|
||||
```text
|
||||
You're a frontend code-review agent. When an issue comes in, read the diff first. Focus only on:
|
||||
- Styling issues (tailwind class names, box model)
|
||||
- Accessibility (a11y)
|
||||
Don't change code — leave suggestions in a comment.
|
||||
```
|
||||
|
||||
비워 두면(기본값) 에이전트는 추가 제약 없이 기반이 되는 AI 코딩 도구의 기본 동작을 사용합니다.
|
||||
|
||||
## 모델 선택
|
||||
|
||||
대부분의 AI 코딩 도구는 모델 선택을 지원합니다(예를 들어 Claude Code는 Sonnet과 Opus 중에서 고를 수 있습니다). 비워 두면 도구 자체의 기본값이 사용되고, 명시적으로 하나를 선택하면 그 모델이 실행됩니다. 각 도구가 지원하는 모델은 [AI 코딩 도구 비교](/providers)에 정리되어 있습니다.
|
||||
|
||||
모델 변경은 **새 작업에만 적용됩니다**. 이미 디스패치된 작업은 디스패치 시점에 고정된 모델로 계속 실행됩니다.
|
||||
|
||||
## 사용자 지정 환경 변수 (custom_env)
|
||||
|
||||
**사용자 지정 환경 변수**(`custom_env`)를 사용하면 작업 실행 시점에 추가 환경 변수를 주입할 수 있습니다. 대표적인 용도는 API 키 설정이나 업스트림 엔드포인트 전환입니다.
|
||||
|
||||
```
|
||||
ANTHROPIC_API_KEY = sk-...
|
||||
ANTHROPIC_BASE_URL = https://my-proxy.example.com
|
||||
```
|
||||
|
||||
시스템에 핵심적인 변수는 재정의할 수 없습니다. `PATH`, `HOME`, `USER`, `SHELL`, `TERM`, `CODEX_HOME`, 그리고 `MULTICA_*`로 시작하는 모든 키는 데몬이 조용히 무시합니다(경고 로그는 남기지만 오류는 발생하지 않습니다).
|
||||
|
||||
<Callout type="warning">
|
||||
**`custom_env`의 값은 Multica 서버 데이터베이스에 평문으로 저장됩니다.** 에이전트 list/get 응답은 더 이상 환경 변수 값을 전혀 포함하지 않으며, 불투명한 개수만 반환합니다. 실제 값을 읽으려면 워크스페이스 owner 또는 admin이 전용으로 감사되는 `GET /api/agents/{id}/env` 엔드포인트(CLI: `multica agent env get <id>`)를 호출해야 합니다. 작업을 실행 중인 에이전트는 호스트의 owner 자격 증명을 이용해 다른 에이전트의 환경 변수를 드러낼 수 없습니다. 이 엔드포인트는 에이전트 액터 세션을 거부합니다.
|
||||
|
||||
**가치가 높은 secret은 `custom_env`에 넣지 마세요**(운영 데이터베이스 비밀번호, root 수준 토큰 등). 에이전트에는 **권한 범위가 제한된 전용 자격 증명**(읽기 전용 API 키, 단일 스코프 PAT)을 사용하고 정기적으로 교체하세요. 데이터베이스 백업과 DB 감사는 여전히 의미 있는 노출 표면으로 남아 있습니다.
|
||||
</Callout>
|
||||
|
||||
## 사용자 지정 CLI 인자 (custom_args)
|
||||
|
||||
**사용자 지정 CLI 인자**(`custom_args`)는 AI 코딩 도구의 명령줄에 하나씩 차례로 덧붙는 문자열 배열입니다.
|
||||
|
||||
```json
|
||||
["--max-turns", "100", "--append-system-prompt", "always respond in Chinese"]
|
||||
```
|
||||
|
||||
최종 명령은 다음과 같이 만들어집니다.
|
||||
|
||||
```bash
|
||||
claude --model <model> --max-turns 100 --append-system-prompt "always respond in Chinese" [...]
|
||||
```
|
||||
|
||||
인자는 셸을 거치지 않고 있는 그대로 전달되므로(주입 위험 없음), 특정 플래그가 인식되는지 여부는 AI 코딩 도구 자체에 달려 있습니다. 이 부분은 도구마다 상당한 차이가 있습니다.
|
||||
|
||||
<Callout type="tip">
|
||||
`custom_env`와 `custom_args`에는 엄격한 상한이 없지만, 실제로는 **각각 10개 이내로 유지하세요**. 너무 많으면 명령줄이 길어지고 시작이 느려지며 유지 관리도 어려워집니다.
|
||||
</Callout>
|
||||
|
||||
## 공개 범위
|
||||
|
||||
- **워크스페이스**(`workspace`) — 워크스페이스의 모든 멤버가 할당할 수 있습니다
|
||||
- **비공개**(`private`) — 워크스페이스 owner, admin, 또는 에이전트 생성자만 할당할 수 있습니다
|
||||
|
||||
새 에이전트는 기본적으로 `private`입니다.
|
||||
|
||||
**비공개라고 해서 숨겨지는 것은 아닙니다** — 모든 멤버가 목록에서 비공개 에이전트의 이름과 설명을 볼 수 있으며, 다만 민감한 구성은 읽을 수 없습니다(환경 변수 값은 에이전트 list/get 응답에 절대 나타나지 않으며, MCP 구성은 owner가 아닌 사용자에게는 마스킹됩니다). 자세한 의미는 [에이전트 → 누가 에이전트를 할당할 수 있나요](/agents#who-can-assign-an-agent)를 참고하세요.
|
||||
|
||||
## 동시 실행 제한
|
||||
|
||||
**동시 실행 제한**(`max_concurrent_tasks`)은 이 에이전트가 한 번에 병렬로 실행할 수 있는 작업 수를 제어합니다. 기본값은 **6**입니다. 상한에 도달한 새 작업은 거부되지 않고 대기열에서 대기합니다.
|
||||
|
||||
이것은 두 단계 제한 중 "에이전트 계층"에 불과합니다. 데몬 자체가 더 넓은 상한(기본값 20)을 적용하며, 둘 중 더 빡빡한 쪽이 우선합니다. 자세한 내용은 [데몬과 런타임 → 병렬로 몇 개의 작업을 실행할 수 있나요](/daemon-runtimes#how-many-tasks-can-run-in-parallel)에 있습니다.
|
||||
|
||||
이 값을 변경해도 **이미 실행 중인 작업은 취소되지 않으며**, 다음에 처리될 작업부터만 적용됩니다.
|
||||
|
||||
## 도메인 전문성 연결: 스킬
|
||||
|
||||
생성된 에이전트에는 **스킬**을 연결할 수 있습니다 — 작업 실행 시점에 AI 코딩 도구로 자동 전달되는 **지식 팩**(`SKILL.md` + 보조 파일)입니다. 새 스킬을 만들거나, GitHub 또는 ClawHub에서 가져오거나, 기기에 있는 기존 스킬 디렉터리에서 스캔할 수 있습니다. [스킬](/skills)을 참고하세요.
|
||||
|
||||
## 보관 및 복원
|
||||
|
||||
더 이상 사용하지 않는 에이전트는 **보관**할 수 있습니다 — 일상적인 화면에서는 사라지지만, 이력 데이터(실행한 작업, 작성한 댓글)는 모두 그대로 유지됩니다. 언제든지 **복원**하여 다시 작업에 투입할 수 있습니다.
|
||||
|
||||
<Callout type="warning">
|
||||
**보관은 해당 에이전트에 속한 완료되지 않은 모든 작업을 즉시 취소합니다** — 실행 중, 디스패치됨, 대기 중인 작업이 모두 `cancelled`로 표시되며 계속 진행되지 않습니다. 진행 중인 중요한 작업이 있다면 보관하기 전에 끝까지 완료되도록 두세요.
|
||||
</Callout>
|
||||
|
||||
보관된 에이전트에는 새 작업을 할당할 수 없습니다.
|
||||
|
||||
## 다음 단계
|
||||
|
||||
- [스킬](/skills) — 에이전트에 지식 팩 연결하기
|
||||
- [AI 코딩 도구 비교](/providers) — 12개 도구 전체의 기능 비교표
|
||||
- [에이전트에게 이슈 할당하기](/assigning-issues) — 새로 만든 에이전트를 작업에 투입하기
|
||||
@@ -1,49 +0,0 @@
|
||||
---
|
||||
title: エージェント
|
||||
description: "エージェントは Multica ワークスペースの一級メンバーです — イシューを割り当てられ、コメントを投稿し、@ でメンションされることができます。人間との核心的な違いは、エージェントは自分から作業を始め、通知を受け取らない点です。"
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
|
||||
エージェントは Multica [ワークスペース](/workspaces)の **一級メンバー** です — 人間と同じように、[イシューを割り当てられ](/assigning-issues)、[コメント](/comments)で発言し、[`@` でメンションされ](/mentioning-agents)、[プロジェクト](/projects)をリードできます。核心的な違いはこれです。すべてのエージェントの背後には、あなたのマシンで動作する [AI コーディングツール](/providers)があります。エージェントにタスクを割り当てると、特に促さなくても **数秒以内に自分から作業を始めます** — 急かす必要も、オフラインになることもなく、24時間いつでも利用できます。
|
||||
|
||||
## エージェントができること
|
||||
|
||||
エージェントは人間と同じ「メンバー」の表面を使っており、UI ではほとんど区別されません。
|
||||
|
||||
- **[イシューを割り当てられる](/assigning-issues)** — 担当者に設定された瞬間、自動的に作業を始めます
|
||||
- **[`@` でメンションされる](/mentioning-agents)** — コメントに `@agent-name` と書くと、目覚めてそのコメントを読みます
|
||||
- **[コメント](/comments)を投稿する** — イシューの下で進捗を報告し、人々に返信します
|
||||
- **[プロジェクト](/projects)をリードする** — 人間と同じように、プロジェクトリードに設定できます
|
||||
- **自分で[イシュー](/issues)を開く** — タスクを実行している間に関連する問題を見つけると、直接新しいイシューを作成できます
|
||||
|
||||
協業ビューから見ると、エージェントはただのワークスペースのメンバーです — 人間と同じメンバー一覧に名前が並び、通常はその前に小さなロボットアイコンが付きます。
|
||||
|
||||
## 人間との違い
|
||||
|
||||
いくつかの重要な違いは、実際にエージェントを使い始めて初めて見えてきます。
|
||||
|
||||
- **自分から始めます** — イシューを割り当てたり `@` でメンションしたりすると、Multica が即座にそのタスクをエージェントのランタイムにディスパッチします。人間のようにメッセージを見て応答するまで待つことはありません。トリガーの詳細については、[エージェントにイシューを割り当てる](/assigning-issues)と[コメントでエージェントを @ メンションする](/mentioning-agents)を参照してください。
|
||||
- **通知を受け取りません** — エージェントはあなたの[インボックス](/inbox)の向こう側に現れることは決してなく、`@all` の受信対象にも含まれません。エージェントは「メッセージを読む受信者」ではなく「タスクを実行するためにトリガーされる作業の単位」です。
|
||||
- **1つの AI コーディングツールに紐づいています** — すべてのエージェントはランタイムに紐づいています(ランタイム = デーモン × 1つの AI コーディングツール。[デーモンとランタイム](/daemon-runtimes)を参照)。ツールがオフラインだとエージェントは作業できず、新しいタスクはランタイムが戻るまで待機します。
|
||||
- **アーカイブできます** — もう使わないエージェントをアーカイブすると日常的なビューから消えます。いつでも好きなときに復元できます。アーカイブすると、現在実行中のタスクはすべてキャンセルされます。
|
||||
|
||||
## 誰がエージェントを割り当てられるか
|
||||
|
||||
エージェントを作成するとき、誰がそのエージェントをイシューに割り当てたりプロジェクトリードに設定したりできるかを制御する **可視性(visibility)** を選択します。
|
||||
|
||||
- **Workspace** — ワークスペースの任意のメンバーが割り当てられます
|
||||
- **Private** — ワークスペースの owner、admin、またはエージェントの作成者だけが割り当てられます
|
||||
|
||||
新しいエージェントはデフォルトで **private** です。ワークスペース全体で利用できるようにするには、作成時に可視性を `workspace` に設定するか、後でエージェントの設定で変更してください。役割と権限の完全なマトリクスについては、[メンバーと役割](/members-roles)を参照してください。
|
||||
|
||||
<Callout type="info">
|
||||
**private は「誰が割り当てられるかを制限する」という意味であって、「他の全員から隠す」という意味ではありません。** ワークスペースのすべてのメンバーは、エージェント一覧で private エージェントの名前と説明を見ることができます — 見えないのは設定の詳細だけです(カスタム環境変数、MCP 設定、その他の機密フィールドはマスクされます)。「1人だけに見える」ようにしたい場合、現時点では実現できません。
|
||||
</Callout>
|
||||
|
||||
## 次のステップ
|
||||
|
||||
- [エージェントの作成と構成](/agents-create) — エージェントを作る方法
|
||||
- [スキル](/skills) — エージェントに知識パックを添付する
|
||||
- [スクワッド](/squads) — 適切なエージェントが適切なイシューを担当するよう、リーダーの下にエージェントをグループ化する
|
||||
- [デーモンとランタイム](/daemon-runtimes) — エージェントが実際に動作するために必要なもの
|
||||
@@ -1,49 +0,0 @@
|
||||
---
|
||||
title: 에이전트
|
||||
description: "에이전트는 Multica 워크스페이스의 일급 멤버입니다 — 이슈를 할당받고, 댓글을 달고, @로 멘션될 수 있습니다. 사람과의 핵심 차이는, 에이전트는 스스로 작업을 시작하며 알림을 받지 않는다는 점입니다."
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
|
||||
에이전트는 Multica [워크스페이스](/workspaces)의 **일급 멤버**입니다 — 사람과 마찬가지로 [이슈를 할당받고](/assigning-issues), [댓글](/comments)에서 발언하고, [`@`로 멘션되며](/mentioning-agents), [프로젝트](/projects)를 이끌 수 있습니다. 핵심 차이는 이것입니다. 모든 에이전트 뒤에는 여러분의 기기에서 실행되는 [AI 코딩 도구](/providers)가 있습니다. 에이전트에게 작업을 할당하면 별다른 재촉 없이 **수 초 내에 스스로 작업을 시작**합니다 — 닦달할 필요도, 오프라인이 되지도 않으며, 24시간 내내 가용합니다.
|
||||
|
||||
## 에이전트가 할 수 있는 일
|
||||
|
||||
에이전트는 사람과 동일한 "멤버" 표면을 사용하며, UI에서는 거의 구분되지 않습니다.
|
||||
|
||||
- **[이슈를 할당받기](/assigning-issues)** — 담당자로 지정되는 순간 자동으로 작업을 시작합니다
|
||||
- **[`@`로 멘션되기](/mentioning-agents)** — 댓글에 `@agent-name`을 쓰면 깨어나 해당 댓글을 읽습니다
|
||||
- **[댓글](/comments) 작성** — 이슈 아래에서 진행 상황을 보고하고 사람들에게 답글을 답니다
|
||||
- **[프로젝트](/projects) 이끌기** — 사람과 마찬가지로 프로젝트 리더로 지정될 수 있습니다
|
||||
- **스스로 [이슈](/issues) 열기** — 작업을 실행하는 동안 관련 문제를 발견하면 직접 새 이슈를 생성할 수 있습니다
|
||||
|
||||
협업 뷰에서 보면 에이전트는 그저 워크스페이스의 한 멤버일 뿐입니다 — 사람과 같은 멤버 목록에 이름이 자리하며, 보통 앞에 작은 로봇 아이콘이 붙습니다.
|
||||
|
||||
## 사람과 다른 점
|
||||
|
||||
몇 가지 핵심 차이는 실제로 에이전트를 사용하기 시작해야 비로소 드러납니다.
|
||||
|
||||
- **스스로 시작합니다** — 이슈를 할당하거나 `@`로 멘션하면 Multica가 즉시 해당 작업을 에이전트의 런타임에 디스패치합니다. 사람처럼 메시지를 보고 응답할 때까지 기다리지 않습니다. 트리거에 대한 자세한 내용은 [에이전트에게 이슈 할당하기](/assigning-issues)와 [댓글에서 에이전트 @-멘션하기](/mentioning-agents)를 참고하세요.
|
||||
- **알림을 받지 않습니다** — 에이전트는 여러분의 [인박스](/inbox) 건너편에 결코 나타나지 않으며, `@all`의 수신 대상에도 포함되지 않습니다. 에이전트는 "메시지를 읽는 수신자"가 아니라 "작업을 실행하도록 트리거되는 작업 단위"입니다.
|
||||
- **하나의 AI 코딩 도구에 묶여 있습니다** — 모든 에이전트는 런타임에 묶여 있습니다(런타임 = 데몬 × 하나의 AI 코딩 도구. [데몬과 런타임](/daemon-runtimes) 참고). 도구가 오프라인이면 에이전트는 작업할 수 없으며, 새 작업은 런타임이 돌아올 때까지 대기합니다.
|
||||
- **보관할 수 있습니다** — 더 이상 사용하지 않는 에이전트를 보관하면 일상적인 뷰에서 사라지며, 원하면 언제든지 복원할 수 있습니다. 보관하면 현재 실행 중인 작업은 모두 취소됩니다.
|
||||
|
||||
## 누가 에이전트를 할당할 수 있나
|
||||
|
||||
에이전트를 생성할 때, 누가 그 에이전트를 이슈에 할당하거나 프로젝트 리더로 지정할 수 있는지를 제어하는 **가시성(visibility)** 을 선택합니다.
|
||||
|
||||
- **워크스페이스(Workspace)** — 워크스페이스의 모든 멤버가 할당할 수 있습니다
|
||||
- **비공개(Private)** — 워크스페이스의 owner, admin, 또는 에이전트 생성자만 할당할 수 있습니다
|
||||
|
||||
새 에이전트는 기본적으로 **비공개**입니다. 전체 워크스페이스에서 사용할 수 있게 하려면, 생성 시 가시성을 `workspace`로 설정하거나 이후에 에이전트 설정에서 변경하세요. 전체 역할-권한 매트릭스는 [멤버와 역할](/members-roles)을 참고하세요.
|
||||
|
||||
<Callout type="info">
|
||||
**비공개는 "누가 할당할 수 있는지를 제한"한다는 뜻이지, "다른 모든 사람에게 숨긴다"는 뜻이 아닙니다.** 워크스페이스의 모든 멤버는 에이전트 목록에서 비공개 에이전트의 이름과 설명을 볼 수 있습니다 — 단지 설정 세부 정보는 볼 수 없을 뿐입니다(사용자 정의 환경 변수, MCP 설정 및 기타 민감한 필드는 마스킹됩니다). "단 한 사람에게만 보이게" 하고 싶다면, 현재로서는 불가능합니다.
|
||||
</Callout>
|
||||
|
||||
## 다음 단계
|
||||
|
||||
- [에이전트 생성 및 구성](/agents-create) — 에이전트를 만드는 방법
|
||||
- [스킬](/skills) — 에이전트에 지식 팩 연결하기
|
||||
- [스쿼드](/squads) — 적합한 에이전트가 적합한 이슈를 맡도록 리더 아래 에이전트를 그룹으로 묶기
|
||||
- [데몬과 런타임](/daemon-runtimes) — 에이전트가 실제로 실행되기 위해 필요한 것
|
||||
@@ -1,83 +0,0 @@
|
||||
---
|
||||
title: エージェントにイシューを割り当てる
|
||||
description: イシューをエージェントに渡すと、作業が終わるまで公式の担当者として引き継ぎます — 完全なコンテキストを持ち、イシューのステータスやフィールドを変更できます。
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
|
||||
[イシュー](/issues)を[エージェント](/agents)に割り当てると、作業が終わるまで**公式の担当者**として働きます — イシューの完全なコンテキスト(説明 + すべての[コメント](/comments))を読み、ステータスを変更し、コメントを投稿し、フィールドを編集できます。これは Multica の 4 つのトリガー経路の中で**最も一般的で、最も重い**方式です。同じフローは[スクワッド](/squads)を担当者として受け付けることもできます — その場合、Multica は代わりにスクワッドの**リーダーエージェント**をトリガーします。
|
||||
|
||||
| 経路 | 使う場面 | イシューの変更 | コンテキスト | 優先度 | 自動リトライ |
|
||||
|---|---|---|---|---|---|
|
||||
| **割り当て** | エージェントに所有権を渡す | 担当者を変更 | イシュー + すべてのコメント | イシューから継承 | ✓ |
|
||||
| [**@メンション**](/mentioning-agents) | ちょっと見てもらうために呼び込む | 変更なし | イシュー + トリガーコメント | イシューから継承 | ✓ |
|
||||
| [**チャット**](/chat) | イシューと無関係な 1 対 1 の会話 | イシューは関与しない | 現在の会話履歴 | 固定で medium | ✓ |
|
||||
| [**オートパイロット**](/autopilots) | スケジュールまたは手動の自動化 | モードによる | モードによる | オートパイロットが設定 | ✗ |
|
||||
|
||||
「自動リトライ」とは、インフラ障害(ランタイムのオフライン、タイムアウト)後のリトライを指します。エージェント側のビジネスエラー(たとえばモデルがエラーを報告する場合)はリトライされません。詳しくは [**タスク**](/tasks)を参照してください。
|
||||
|
||||
## UI から割り当てる
|
||||
|
||||
イシュー詳細ページで、**担当者**ピッカーをクリックしてください。ワークスペースのすべてのメンバー、アーカイブされていないすべてのエージェント、アーカイブされていないすべての[スクワッド](/squads)が一覧表示されます。エージェント(またはスクワッド)を選ぶと、イシューはすぐに割り当てられます。
|
||||
|
||||
いくつかのルールがあります。
|
||||
|
||||
- **ワークスペースエージェント**はどのメンバーでも割り当てられます。**プライベートエージェント**はその owner またはワークスペースの admin のみが割り当てられます。
|
||||
- **オンラインのランタイムを持つ**エージェントにのみ割り当てられます — 誰も実行していないエージェントはピッカーで利用不可と表示されます。
|
||||
- イシューのステータスが **Backlog** のとき、割り当てても**エージェントはトリガーされません** — Backlog は一時保管所であり、イシューを Todo または In Progress に移して初めてエージェントがキューに入ります。
|
||||
|
||||
## CLI から割り当てる
|
||||
|
||||
コマンドラインでの同等の操作です。
|
||||
|
||||
```bash
|
||||
multica issue assign MUL-42 --to alice
|
||||
multica issue assign MUL-42 --to-id 5fb87ac7-23b5-4a7a-81fa-ed295a54545d
|
||||
```
|
||||
|
||||
`--to` はメンバーのユーザー名またはエージェント名(あいまい一致)を受け付けます。名前が重複するとき — たとえばエージェント `J` の隣に `Cursor - J` がある場合 — は、代わりに `--to-id <uuid>` を渡してください。このとき `multica workspace member list --output json` の `user_id`(メンバー)または `multica agent list --output json` の `id`(エージェント)を使います。UUID 一致は厳密かつ曖昧さがないため、スクリプトや CLI を駆動するエージェントに適しています。`--to` と `--to-id` は同時に使えません。
|
||||
|
||||
割り当て解除:
|
||||
|
||||
```bash
|
||||
multica issue assign MUL-42 --unassign
|
||||
```
|
||||
|
||||
## 割り当て後に起こること
|
||||
|
||||
Backlog ではないイシューがエージェントに割り当てられると、Multica はすぐにバックグラウンドで次のことを行います。
|
||||
|
||||
1. イシューから継承した優先度で `queued` 状態の `task` をキューに入れ、エージェントが存在するランタイムへルーティングします。
|
||||
2. エージェントのデーモンが次のポーリング時に `task` を取得し、`dispatched` に遷移させます。
|
||||
3. エージェントが作業を開始すると `task` が `running` に移ります。完了すると `completed` または `failed` になります。
|
||||
4. 実行中、エージェントはイシューのステータスを変更し、コメントを投稿し、フィールドを編集できます — これらの操作はエージェントの ID で表示されます。
|
||||
|
||||
**エージェントがオフラインの場合**、`task` はキューで待機します — **5 分後に `runtime_offline` の理由でタイムアウトして失敗します**。リトライ可能なソース(割り当て、@メンション、チャット)については、Multica が自動的に再度キューに入れます。完全なリトライルールは [**タスク**](/tasks)を参照してください。
|
||||
|
||||
割り当てると、エージェントはイシューに自動的に購読されます — ただし Multica では**エージェントはインボックス通知を受け取りません**(メンバーのみ受け取ります)。この購読は内部的な記録管理にすぎず、ユーザーに見える副作用はありません。
|
||||
|
||||
## 再割り当てまたは割り当て解除
|
||||
|
||||
担当者をエージェント A からエージェント B に変更すると、
|
||||
|
||||
1. **A が進行中だったものはすべてキャンセルされます** — `queued`、`dispatched`、`running` 状態のすべての `task` が `cancelled` と表示されます。
|
||||
2. **B にはすぐに新しい `task` がキューに入ります**(イシューが Backlog でなく、B にオンラインのランタイムがある場合)。
|
||||
|
||||
<Callout type="warning">
|
||||
**再割り当てはこのイシューのすべてのアクティブな `task` をキャンセルします — 以前の担当者のものだけではありません。** 別のエージェントが @メンションによってこのイシューで作業中の場合、その `task` も一緒にキャンセルされます。現在のところ、単一のエージェントの `task` だけを個別にキャンセルする UI 操作はありません。
|
||||
</Callout>
|
||||
|
||||
割り当て解除(`--unassign` またはピッカーで「none」を選択)は、すべてのアクティブな `task` 項目を `cancelled` と表示し、**新しい項目をキューに入れません**。既存の購読は自動的にクリアされません — 以前の担当者は購読リストに残ります(ただし依然としてインボックス通知は受け取りません)。
|
||||
|
||||
## イシューごとエージェントごとにアクティブな `task` が 1 つだけの理由
|
||||
|
||||
**単一のエージェントは、同じイシューで任意の時点に最大 1 つの `queued` または `dispatched` の `task` しか持てません。** データベースレベルの一意インデックスとクレームロジックがこれを強制します — 重複したキュー登録と、同時実行が互いを上書きすることを防ぎます。
|
||||
|
||||
しかし**異なるエージェントは同じイシューで並列に作業できます** — たとえばエージェント A が担当者で、エージェント B が @メンションされた場合、2 つの `task` 項目がそれぞれ自分のランタイムで実行されながら共存できます。完全な直列・並列ルールは [**タスク**](/tasks)を参照してください。
|
||||
|
||||
## 次へ
|
||||
|
||||
- [**コメントでエージェントを @メンションする**](/mentioning-agents) — 担当者とステータスを変えない、より軽いトリガー
|
||||
- [**スクワッド**](/squads) — エージェントのグループに割り当て、リーダーに誰が引き受けるかを決めさせる
|
||||
- [**チャット**](/chat) — イシューと無関係な 1 対 1 の会話
|
||||
- [**オートパイロット**](/autopilots) — エージェントがスケジュールに沿って自動的に作業を開始するようにする
|
||||
@@ -1,83 +0,0 @@
|
||||
---
|
||||
title: 에이전트에게 이슈 할당하기
|
||||
description: 이슈를 에이전트에게 넘기면 작업이 끝날 때까지 공식 담당자로 인계받습니다 — 전체 컨텍스트를 갖고 이슈 상태와 필드를 변경할 수 있습니다.
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
|
||||
[이슈](/issues)를 [에이전트](/agents)에게 할당하면, 작업이 끝날 때까지 **공식 담당자**로서 일합니다 — 이슈의 전체 컨텍스트(설명 + 모든 [댓글](/comments))를 읽을 수 있고, 상태를 변경하고, 댓글을 남기고, 필드를 수정할 수 있습니다. 이것은 Multica의 네 가지 트리거 경로 중 **가장 일반적이고 가장 무거운** 방식입니다. 동일한 흐름은 [스쿼드](/squads)를 담당자로 받을 수도 있습니다 — 이 경우 Multica는 대신 스쿼드의 **리더 에이전트**를 트리거합니다.
|
||||
|
||||
| 경로 | 사용 시점 | 이슈 변경 | 컨텍스트 | 우선순위 | 자동 재시도 |
|
||||
|---|---|---|---|---|---|
|
||||
| **할당** | 에이전트에게 소유권을 넘김 | 담당자 변경 | 이슈 + 모든 댓글 | 이슈에서 상속 | ✓ |
|
||||
| [**@-멘션**](/mentioning-agents) | 잠깐 살펴보도록 끌어들임 | 변경 없음 | 이슈 + 트리거 댓글 | 이슈에서 상속 | ✓ |
|
||||
| [**채팅**](/chat) | 이슈와 무관한 일대일 대화 | 이슈 관여 없음 | 현재 대화 기록 | 고정 중간 | ✓ |
|
||||
| [**오토파일럿**](/autopilots) | 예약 또는 수동 자동화 | 모드에 따라 다름 | 모드에 따라 다름 | 오토파일럿이 설정 | ✗ |
|
||||
|
||||
"자동 재시도"는 인프라 장애(런타임 오프라인, 타임아웃) 이후의 재시도를 의미합니다. 에이전트 쪽의 비즈니스 오류(예: 모델이 오류를 보고하는 경우)는 재시도되지 않습니다. 자세한 내용은 [**작업**](/tasks)을 참고하세요.
|
||||
|
||||
## UI에서 할당하기
|
||||
|
||||
이슈 상세 페이지에서 **담당자** 선택기를 클릭하세요. 워크스페이스의 모든 멤버, 보관되지 않은 모든 에이전트, 보관되지 않은 모든 [스쿼드](/squads)가 목록에 표시됩니다. 에이전트(또는 스쿼드)를 선택하면 이슈가 즉시 할당됩니다.
|
||||
|
||||
몇 가지 규칙이 있습니다.
|
||||
|
||||
- **워크스페이스 에이전트**는 어떤 멤버든 할당할 수 있습니다. **프라이빗 에이전트**는 owner 또는 워크스페이스 admin만 할당할 수 있습니다.
|
||||
- **온라인 런타임이 있는** 에이전트에게만 할당할 수 있습니다 — 아무도 실행하고 있지 않은 에이전트는 선택기에서 사용 불가로 표시됩니다.
|
||||
- 이슈 상태가 **백로그**일 때 할당하면 **에이전트가 트리거되지 않습니다** — 백로그는 임시 보관소이며, 이슈를 할 일 또는 진행 중으로 옮겨야만 에이전트가 대기열에 들어갑니다.
|
||||
|
||||
## CLI에서 할당하기
|
||||
|
||||
명령줄에서의 동등한 작업입니다.
|
||||
|
||||
```bash
|
||||
multica issue assign MUL-42 --to alice
|
||||
multica issue assign MUL-42 --to-id 5fb87ac7-23b5-4a7a-81fa-ed295a54545d
|
||||
```
|
||||
|
||||
`--to`는 멤버 사용자 이름 또는 에이전트 이름(퍼지 매칭)을 받습니다. 이름이 겹칠 때 — 예를 들어 에이전트 `J` 옆에 `Cursor - J`가 있을 때 — 대신 `--to-id <uuid>`를 전달하세요. 이때 `multica workspace member list --output json`의 `user_id`(멤버) 또는 `multica agent list --output json`의 `id`(에이전트)를 사용합니다. UUID 매칭은 엄격하고 모호하지 않으므로, 스크립트나 CLI를 구동하는 에이전트에게 적합합니다. `--to`와 `--to-id`는 함께 쓸 수 없습니다.
|
||||
|
||||
할당 해제:
|
||||
|
||||
```bash
|
||||
multica issue assign MUL-42 --unassign
|
||||
```
|
||||
|
||||
## 할당 이후에 일어나는 일
|
||||
|
||||
백로그가 아닌 이슈가 에이전트에게 할당되면, Multica는 즉시 백그라운드에서 다음을 수행합니다.
|
||||
|
||||
1. 이슈에서 상속한 우선순위로 `queued` 상태의 `task`를 대기열에 넣고, 에이전트가 있는 런타임으로 라우팅합니다.
|
||||
2. 에이전트의 데몬이 다음 폴링 시 `task`를 가져가 `dispatched`로 전환합니다.
|
||||
3. 에이전트가 작업을 시작하면 `task`가 `running`으로 이동합니다. 완료되면 `completed` 또는 `failed`가 됩니다.
|
||||
4. 실행 중에 에이전트는 이슈의 상태를 변경하고, 댓글을 남기고, 필드를 수정할 수 있습니다 — 이러한 동작은 에이전트의 신원으로 표시됩니다.
|
||||
|
||||
**에이전트가 오프라인인 경우**, `task`는 대기열에서 기다립니다 — **5분 후 `runtime_offline` 사유로 타임아웃되어 실패합니다**. 재시도 가능한 소스(할당, @-멘션, 채팅)에 대해서는 Multica가 자동으로 다시 대기열에 넣습니다. 전체 재시도 규칙은 [**작업**](/tasks)을 참고하세요.
|
||||
|
||||
할당하면 에이전트가 이슈에 자동으로 구독됩니다 — 다만 Multica에서는 **에이전트가 인박스 알림을 받지 않습니다**(멤버만 받습니다). 이 구독은 내부 기록 관리일 뿐이며 사용자에게 보이는 부작용은 없습니다.
|
||||
|
||||
## 재할당 또는 할당 해제
|
||||
|
||||
담당자를 에이전트 A에서 에이전트 B로 변경하면:
|
||||
|
||||
1. **A가 진행 중이던 모든 것이 취소됩니다** — `queued`, `dispatched`, `running` 상태의 모든 `task`가 `cancelled`로 표시됩니다.
|
||||
2. **B에게 즉시 새 `task`가 대기열에 들어갑니다**(이슈가 백로그가 아니고 B에게 온라인 런타임이 있는 경우).
|
||||
|
||||
<Callout type="warning">
|
||||
**재할당은 이 이슈의 모든 활성 `task`를 취소합니다 — 이전 담당자의 것만이 아닙니다.** 다른 에이전트가 @-멘션 때문에 이 이슈에서 작업 중이라면, 그 `task`도 함께 취소됩니다. 현재로서는 단일 에이전트의 `task`만 따로 취소하는 UI 동작이 없습니다.
|
||||
</Callout>
|
||||
|
||||
할당 해제(`--unassign` 또는 선택기에서 "none" 선택)는 모든 활성 `task` 항목을 `cancelled`로 표시하며 **새 항목을 대기열에 넣지 않습니다**. 기존 구독은 자동으로 정리되지 않습니다 — 이전 담당자는 구독 목록에 남아 있습니다(다만 여전히 인박스 알림은 받지 않습니다).
|
||||
|
||||
## 이슈당 에이전트당 활성 `task`가 하나뿐인 이유
|
||||
|
||||
**단일 에이전트는 같은 이슈에서 어느 시점에든 최대 하나의 `queued` 또는 `dispatched` `task`만 가질 수 있습니다.** 데이터베이스 수준의 고유 인덱스와 클레임 로직이 이를 강제합니다 — 중복 대기열 등록과 동시 실행이 서로를 덮어쓰는 것을 방지합니다.
|
||||
|
||||
하지만 **서로 다른 에이전트는 같은 이슈에서 병렬로 작업할 수 있습니다** — 예를 들어 에이전트 A가 담당자이고 에이전트 B가 @-멘션된 경우, 두 `task` 항목이 각자의 런타임에서 실행되며 공존할 수 있습니다. 전체 직렬/동시 실행 규칙은 [**작업**](/tasks)을 참고하세요.
|
||||
|
||||
## 다음 단계
|
||||
|
||||
- [**댓글에서 에이전트를 @-멘션하기**](/mentioning-agents) — 담당자와 상태를 건드리지 않는 더 가벼운 트리거
|
||||
- [**스쿼드**](/squads) — 에이전트 그룹에게 할당하고 리더가 누가 맡을지 결정하도록 함
|
||||
- [**채팅**](/chat) — 이슈와 무관한 일대일 대화
|
||||
- [**오토파일럿**](/autopilots) — 에이전트가 예약된 일정에 따라 자동으로 작업을 시작하도록 함
|
||||
@@ -1,186 +0,0 @@
|
||||
---
|
||||
title: ログインとサインアップの構成
|
||||
description: メール + 認証コードログイン、Google OAuth、サインアップ許可リスト、ローカルテストコードを構成します。
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
import { Mermaid } from "@/components/mermaid";
|
||||
|
||||
Multica は 2 つのログイン方式をサポートしています。**メール + 認証コード**(デフォルト)と **Google OAuth**(オプション)です。ログインに成功すると、サーバーは 30 日間有効な JWT クッキーを発行します。このページでは、各方式の構成方法、誰がサインアップできるかを制限する方法、そしてセルフホストのデプロイで最も陥りやすい落とし穴を 1 つ取り上げます。
|
||||
|
||||
以下で参照する環境変数の一覧は[環境変数](/environment-variables)を参照してください。トークンの使い方とライフサイクルの詳細は[認証とトークン](/auth-tokens)を参照してください。
|
||||
|
||||
## メール + 認証コードログインの仕組み
|
||||
|
||||
ユーザーがログインページでメールを入力します → サーバーが 6 桁のコードを送信します → ユーザーがコードを入力します → サーバーがコードを検証します → JWT クッキーが発行されます。標準的なフローです。2 つの送信バックエンドがサポートされているので、デプロイ環境に合うほうを選んでください。
|
||||
|
||||
### オプション A: Resend(クラウド / 公開インターネットのデプロイに推奨)
|
||||
|
||||
1. [Resend](https://resend.com/) アカウントを作成し、ドメインを認証します
|
||||
2. API キーを作成します
|
||||
3. 環境変数を設定します:
|
||||
|
||||
```bash
|
||||
RESEND_API_KEY=re_xxxxxxxxxxxxxxxx
|
||||
RESEND_FROM_EMAIL=noreply@yourdomain.com # must be a domain verified in Resend
|
||||
```
|
||||
|
||||
4. サーバーを再起動します
|
||||
|
||||
### オプション B: SMTP relay(セルフホスト / オンプレミスのデプロイ用)
|
||||
|
||||
デプロイ環境から `api.resend.com` に到達できない場合や、すでに内部メール relay(Microsoft Exchange、Postfix、オンプレミスの SendGrid など)がある場合に使用してください。両方が設定されている場合は `SMTP_HOST` が `RESEND_API_KEY` より優先されます。`SMTP_HOST` が空でなければ、`RESEND_API_KEY` も併せて構成されていても、サーバーは常に SMTP を経由するため、認証メールと招待メールが内部ネットワークの外に出ることは決してありません。
|
||||
|
||||
SMTP 経路は、ほとんどのオンプレミスメールサーバー(特に Microsoft Exchange の receive connector)が公開する 3 つの relay モードをサポートします。
|
||||
|
||||
| モード | ポート | 認証 | TLS |
|
||||
|---|---|---|---|
|
||||
| 匿名内部 relay | `25` | なし — IP / サブネットで送信を信頼 | 伝送経路上はなし(内部セグメント専用) |
|
||||
| 認証付き送信(submission) | `587` | `SMTP_USERNAME` + `SMTP_PASSWORD` | STARTTLS、自動アップグレード |
|
||||
| 暗黙的 TLS(SMTPS) | `465` | 任意(`SMTP_USERNAME` + `SMTP_PASSWORD`) | 接続時に TLS ハンドシェイク — ポート `465` で自動的に有効化、非標準ポートでは `SMTP_TLS=implicit` で強制 |
|
||||
|
||||
**ポート 25 の匿名 Exchange relay** — 認証情報なしで信頼されたサブネットからのメールを受け入れる、典型的な「internal SMTP relay」/ Exchange 匿名 receive connector:
|
||||
|
||||
```bash
|
||||
SMTP_HOST=exchange.internal.example.com
|
||||
SMTP_PORT=25
|
||||
SMTP_USERNAME=
|
||||
SMTP_PASSWORD=
|
||||
SMTP_TLS_INSECURE=false
|
||||
RESEND_FROM_EMAIL=noreply@yourdomain.com # reused as the From: header
|
||||
```
|
||||
|
||||
**ポート 587 の認証付き送信** — サービスアカウントを必要とする relay 用。サーバーが STARTTLS のサポートを通知すると自動的にアップグレードされます:
|
||||
|
||||
```bash
|
||||
SMTP_HOST=smtp.internal.example.com
|
||||
SMTP_PORT=587
|
||||
SMTP_USERNAME=multica
|
||||
SMTP_PASSWORD=...
|
||||
SMTP_TLS_INSECURE=false # set true only for self-signed / private CA
|
||||
RESEND_FROM_EMAIL=noreply@yourdomain.com
|
||||
```
|
||||
|
||||
**ポート 465 の暗黙的 TLS(SMTPS)** — SMTPS のみを提供し STARTTLS を通知しないプロバイダー(例: Aliyun / Tencent のエンタープライズメール)向け。ポート `465` は暗黙的 TLS を自動的に有効化します。`SMTP_TLS=implicit`(別名: `smtps`、`ssl`)は非標準の SMTPS ポートでこれを強制します:
|
||||
|
||||
```bash
|
||||
SMTP_HOST=smtp.qiye.aliyun.com
|
||||
SMTP_PORT=465 # implicit TLS auto-enabled on 465
|
||||
SMTP_USERNAME=multica@yourdomain.com
|
||||
SMTP_PASSWORD=...
|
||||
SMTP_TLS=implicit # optional on 465; required on a non-standard SMTPS port
|
||||
RESEND_FROM_EMAIL=noreply@yourdomain.com
|
||||
```
|
||||
|
||||
**厳格な公開 relay(例: Google Workspace `smtp-relay.gmail.com`)** はさらに有効な EHLO 名を必要とします。これらの relay は公開 IP からのデフォルトの `localhost` 挨拶を拒否し、relay が接続を切断します — これは挨拶の時点ではなく、後続のコマンドで不明瞭な `EOF`(`smtp auth: EOF`)として表面化します。`SMTP_EHLO_NAME` を relay が期待する FQDN に設定してください。デフォルトはマシンのホスト名で、コンテナ内では通常は有効な FQDN ではありません。
|
||||
|
||||
```bash
|
||||
SMTP_HOST=smtp-relay.gmail.com
|
||||
SMTP_PORT=587
|
||||
SMTP_EHLO_NAME=mail.yourdomain.com # FQDN the relay accepts; defaults to the (non-FQDN) container hostname
|
||||
RESEND_FROM_EMAIL=noreply@yourdomain.com
|
||||
```
|
||||
|
||||
起動時に、サーバーは選択したプロバイダーを、ネゴシエートされた TLS モードも含めて出力します。例えば `EmailService: SMTP relay exchange.internal.example.com:25 (starttls) from=noreply@example.com` や `… smtp.qiye.aliyun.com:465 (implicit-tls) from=…`(または `Resend API` / `DEV mode`)のように表示されます。パスワードがログに記録されることは決してありません。再起動後に SMTP の行が見えない場合は `SMTP_HOST` がプロセスに届いていないので、コンテナ環境(`docker compose -f docker-compose.selfhost.yml exec backend env | grep SMTP`)を確認してください。
|
||||
|
||||
**どちらも設定しない場合**: サーバーはエラーを出しませんが、**送信されるはずだったすべてのメールがサーバーの stdout にのみ書き出されます**。ローカル開発には便利ですが(ログからコードをコピーできます)、プロダクションではブラックホールになります。
|
||||
|
||||
## 固定ローカルテストコード
|
||||
|
||||
<Callout type="warning">
|
||||
**公開アクセス可能なインスタンスでは固定の認証コードを有効にしないでください。**
|
||||
|
||||
非プロダクションのインスタンスがデフォルトで `888888` を受け入れていた従来の動作は削除されました。明示的に構成しない限り、`888888` の入力は他の誤ったコードと同じように扱われます。
|
||||
|
||||
メールバックエンドをまったく構成していない(Resend も SMTP もない)ローカル開発では、サーバーログに出力される生成されたコードを使用してください。決定論的なローカル / プライベートの自動化が必要な場合は、`MULTICA_DEV_VERIFICATION_CODE` を `888888` のような 6 桁の値に設定し、`APP_ENV` を非プロダクションに保ってください:
|
||||
|
||||
```bash
|
||||
APP_ENV=development
|
||||
MULTICA_DEV_VERIFICATION_CODE=888888
|
||||
```
|
||||
|
||||
このショートカットは `APP_ENV=production` のときは無視されます。
|
||||
</Callout>
|
||||
|
||||
プロダクションのデプロイでは `MULTICA_DEV_VERIFICATION_CODE` を空のままにし、`APP_ENV=production` に設定してください。`make selfhost` / `docker-compose.selfhost.yml` でデプロイする場合、`APP_ENV` はデフォルトで `production` です。
|
||||
|
||||
## Google OAuth の構成
|
||||
|
||||
オプションです。構成しないとメール + 認証コードのみが利用可能で、構成するとログインページに「Sign in with Google」ボタンが追加されます。
|
||||
|
||||
1. [Google Cloud Console](https://console.cloud.google.com/) で OAuth 2.0 クライアントを作成します
|
||||
2. **Authorized redirect URIs** を Multica フロントエンドのアドレスに `/auth/callback` を加えた値に設定します。例:
|
||||
|
||||
```text
|
||||
https://multica.yourdomain.com/auth/callback
|
||||
```
|
||||
|
||||
3. クライアント ID とクライアント secret を取得したら、3 つの環境変数を設定します:
|
||||
|
||||
```bash
|
||||
GOOGLE_CLIENT_ID=xxxxx.apps.googleusercontent.com
|
||||
GOOGLE_CLIENT_SECRET=GOCSPX-xxxxxxxxxxxxxxx
|
||||
GOOGLE_REDIRECT_URI=https://multica.yourdomain.com/auth/callback
|
||||
```
|
||||
|
||||
4. サーバーを再起動します。
|
||||
|
||||
**ランタイムで反映されます**: フロントエンドは `/api/config` を通じてランタイムにこれらの設定を読み込みます — 変更後にサーバーを再起動すると、フロントエンドはリビルドや再デプロイなしで新しい値を取得します。
|
||||
|
||||
<Callout type="warning">
|
||||
**リダイレクト URI は Google Console と `GOOGLE_REDIRECT_URI` の両方で完全に一致している必要があります** — プロトコル(`http` と `https`)、末尾のスラッシュ、ポートを含みます。少しでも一致しないと Google は OAuth フロー全体を拒否し、ユーザーに表示されるエラーは `redirect_uri_mismatch` です。
|
||||
</Callout>
|
||||
|
||||
## 誰がサインアップできるかを制限する
|
||||
|
||||
3 つの環境変数が優先順位に従って組み合わされます。
|
||||
|
||||
<Mermaid chart={`
|
||||
graph TD
|
||||
Start[New user first sign-in] --> A{Email in<br/>ALLOWED_EMAILS?}
|
||||
A -- Yes --> Allow[Allow signup]
|
||||
A -- No --> B{Domain in<br/>ALLOWED_EMAIL_DOMAINS?}
|
||||
B -- Yes --> Allow
|
||||
B -- No --> C{Any allowlist<br/>non-empty?}
|
||||
C -- Yes --> Block[Reject]
|
||||
C -- No --> D{ALLOW_SIGNUP<br/>= true?}
|
||||
D -- Yes --> Allow
|
||||
D -- No --> Block
|
||||
`} />
|
||||
|
||||
**既存のユーザーはいつでも再ログインできます** — サインアップ許可リストは**初回サインアップ**にのみ適用され、戻ってくるユーザーは妨げられません。
|
||||
|
||||
- **`ALLOWED_EMAILS`**(最高優先度) — 明示的なメール許可リスト、カンマ区切り。**空でない場合、リストにあるメールのみがサインアップできます。**
|
||||
- **`ALLOWED_EMAIL_DOMAINS`** — ドメイン許可リスト、カンマ区切り(例: `company.io,partner.com`)。
|
||||
- **`ALLOW_SIGNUP`** — マスタースイッチ、デフォルト `true`。`false` に設定するとサインアップが完全に無効になります。
|
||||
|
||||
<Callout type="warning">
|
||||
**3 つの層は OR ではなく AND のセマンティクスです。** よくある誤った直感は、`ALLOWED_EMAIL_DOMAINS=company.io` + `ALLOW_SIGNUP=true` が「company.io に加えて他の全員を許可する」という意味だと考えることです。そうでは**ありません**。いずれかの層に空でない値があると、**それに一致しないメールはただちに拒否され**、`ALLOW_SIGNUP=true` はそれを無効にできません。
|
||||
|
||||
実際に「全員を許可」するには、3 つの変数をすべて空のままにしてください(または `ALLOW_SIGNUP=true` を維持してください)。
|
||||
</Callout>
|
||||
|
||||
**典型的な構成**:
|
||||
|
||||
| 目的 | 構成 |
|
||||
|---|---|
|
||||
| 内部専用、`company.io` の従業員のみ | `ALLOWED_EMAIL_DOMAINS=company.io` |
|
||||
| 内部 + 少数の外部コラボレーター | `ALLOWED_EMAIL_DOMAINS=company.io` + コラボレーターのアドレスを `ALLOWED_EMAILS` に追加 |
|
||||
| セルフサービスのサインアップを完全に無効化、招待のみ | `ALLOW_SIGNUP=false` |
|
||||
| 開放型サインアップ(プロダクションには非推奨) | 3 つすべて空 |
|
||||
|
||||
## サインアップを無効にしても人を招待できますか?
|
||||
|
||||
**すでに Multica アカウントを持っている人のみ可能です。** 招待の受諾はサインアップ許可リストをチェックしません — 招待された人がすでにサインアップ済み(例えば別のワークスペースで)であれば、招待リンクをクリックしてログインすれば受諾できます。
|
||||
|
||||
**しかし一度もサインアップしていない人は招待で救うことはできません。** 受諾する前にまずログインする必要があり、ログインの最初のステップ(認証コードの要求)はサインアップ許可リストのチェックを通過します。`ALLOW_SIGNUP=false` であるか、そのメールが `ALLOWED_EMAILS` / `ALLOWED_EMAIL_DOMAINS` にない場合、**サインアップを完了できず**、したがって招待を受諾することもできません。
|
||||
|
||||
まだサインアップしていない外部コラボレーターを招待するには: そのメールを `ALLOWED_EMAILS` に一時的に追加し、その人がサインアップして招待を受諾するのを待ってから、エントリを削除してください。
|
||||
|
||||
招待の作成と使用方法については[メンバーとロール](/members-roles)を参照してください。
|
||||
|
||||
## 次に
|
||||
|
||||
- [環境変数](/environment-variables) — このページで使用するすべての変数の完全な定義
|
||||
- [認証とトークン](/auth-tokens) — JWT / PAT / デーモントークンの分類と使い方
|
||||
- [トラブルシューティング](/troubleshooting) — 認証コードが届かない、OAuth `redirect_uri_mismatch`、サインアップ拒否
|
||||
@@ -1,186 +0,0 @@
|
||||
---
|
||||
title: 로그인 및 회원가입 구성
|
||||
description: 이메일 + 인증 코드 로그인, Google OAuth, 회원가입 허용 목록, 로컬 테스트 코드를 구성합니다.
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
import { Mermaid } from "@/components/mermaid";
|
||||
|
||||
Multica는 두 가지 로그인 방식을 지원합니다. **이메일 + 인증 코드**(기본값)와 **Google OAuth**(선택). 로그인에 성공하면 서버가 30일 수명의 JWT 쿠키를 발급합니다. 이 페이지에서는 각 방식을 구성하는 방법, 누가 회원가입할 수 있는지 제한하는 방법, 그리고 자체 호스팅 배포에서 가장 빠지기 쉬운 함정 하나를 다룹니다.
|
||||
|
||||
아래에서 참조하는 환경 변수 목록은 [환경 변수](/environment-variables)를 참고하세요. 토큰 사용법과 수명 주기 세부 사항은 [인증 및 토큰](/auth-tokens)을 참고하세요.
|
||||
|
||||
## 이메일 + 인증 코드 로그인의 작동 방식
|
||||
|
||||
사용자가 로그인 페이지에서 이메일을 입력합니다 → 서버가 6자리 코드를 보냅니다 → 사용자가 코드를 입력합니다 → 서버가 코드를 검증합니다 → JWT 쿠키가 발급됩니다. 표준 흐름입니다. 두 가지 전송 백엔드가 지원되므로 배포 환경에 맞는 쪽을 선택하세요.
|
||||
|
||||
### 옵션 A: Resend (클라우드 / 공용 인터넷 배포에 권장)
|
||||
|
||||
1. [Resend](https://resend.com/) 계정을 만들고 도메인을 인증합니다
|
||||
2. API 키를 생성합니다
|
||||
3. 환경 변수를 설정합니다:
|
||||
|
||||
```bash
|
||||
RESEND_API_KEY=re_xxxxxxxxxxxxxxxx
|
||||
RESEND_FROM_EMAIL=noreply@yourdomain.com # must be a domain verified in Resend
|
||||
```
|
||||
|
||||
4. 서버를 재시작합니다
|
||||
|
||||
### 옵션 B: SMTP relay (자체 호스팅 / 온프레미스 배포용)
|
||||
|
||||
배포 환경에서 `api.resend.com`에 접근할 수 없거나 이미 내부 메일 relay(Microsoft Exchange, Postfix, 온프레미스 SendGrid 등)가 있는 경우에 사용하세요. 둘 다 설정된 경우 `SMTP_HOST`가 `RESEND_API_KEY`보다 우선합니다. `SMTP_HOST`가 비어 있지 않으면 `RESEND_API_KEY`도 함께 구성되어 있더라도 서버는 항상 SMTP를 통하므로, 인증 및 초대 메일이 내부 네트워크를 벗어나는 일이 결코 없습니다.
|
||||
|
||||
SMTP 경로는 대부분의 온프레미스 메일 서버(특히 Microsoft Exchange의 receive connector)가 노출하는 세 가지 relay 모드를 지원합니다:
|
||||
|
||||
| 모드 | 포트 | 인증 | TLS |
|
||||
|---|---|---|---|
|
||||
| 익명 내부 relay | `25` | 없음 — IP / 서브넷으로 제출을 신뢰 | 전송 경로상 없음(내부 세그먼트 전용) |
|
||||
| 인증된 제출(submission) | `587` | `SMTP_USERNAME` + `SMTP_PASSWORD` | STARTTLS, 자동 업그레이드 |
|
||||
| 암묵적 TLS (SMTPS) | `465` | 선택 사항(`SMTP_USERNAME` + `SMTP_PASSWORD`) | 연결 시 TLS 핸드셰이크 — 포트 `465`에서 자동 활성화, 비표준 포트에서는 `SMTP_TLS=implicit`로 강제 |
|
||||
|
||||
**포트 25의 익명 Exchange relay** — 자격 증명 없이 신뢰된 서브넷에서 오는 메일을 받아들이는 일반적인 "internal SMTP relay" / Exchange 익명 receive connector:
|
||||
|
||||
```bash
|
||||
SMTP_HOST=exchange.internal.example.com
|
||||
SMTP_PORT=25
|
||||
SMTP_USERNAME=
|
||||
SMTP_PASSWORD=
|
||||
SMTP_TLS_INSECURE=false
|
||||
RESEND_FROM_EMAIL=noreply@yourdomain.com # reused as the From: header
|
||||
```
|
||||
|
||||
**포트 587의 인증된 제출** — 서비스 계정이 필요한 relay용. 서버가 STARTTLS 지원을 알리면 자동으로 업그레이드됩니다:
|
||||
|
||||
```bash
|
||||
SMTP_HOST=smtp.internal.example.com
|
||||
SMTP_PORT=587
|
||||
SMTP_USERNAME=multica
|
||||
SMTP_PASSWORD=...
|
||||
SMTP_TLS_INSECURE=false # set true only for self-signed / private CA
|
||||
RESEND_FROM_EMAIL=noreply@yourdomain.com
|
||||
```
|
||||
|
||||
**포트 465의 암묵적 TLS(SMTPS)** — SMTPS만 제공하고 STARTTLS를 알리지 않는 제공자(예: Aliyun / Tencent 엔터프라이즈 메일)용. 포트 `465`는 암묵적 TLS를 자동으로 활성화하며, `SMTP_TLS=implicit`(별칭: `smtps`, `ssl`)는 비표준 SMTPS 포트에서 이를 강제합니다:
|
||||
|
||||
```bash
|
||||
SMTP_HOST=smtp.qiye.aliyun.com
|
||||
SMTP_PORT=465 # implicit TLS auto-enabled on 465
|
||||
SMTP_USERNAME=multica@yourdomain.com
|
||||
SMTP_PASSWORD=...
|
||||
SMTP_TLS=implicit # optional on 465; required on a non-standard SMTPS port
|
||||
RESEND_FROM_EMAIL=noreply@yourdomain.com
|
||||
```
|
||||
|
||||
**엄격한 공개 relay(예: Google Workspace `smtp-relay.gmail.com`)** 는 추가로 유효한 EHLO 이름을 요구합니다. 이들은 공개 IP에서 보내는 기본 `localhost` greeting을 거부하며, relay가 연결을 끊습니다 — 이는 greeting 단계가 아니라 이후 명령에서 불투명한 `EOF`(`smtp auth: EOF`)로 나타납니다. relay가 기대하는 FQDN으로 `SMTP_EHLO_NAME`을 설정하세요. 기본값은 머신 호스트명이며, 컨테이너 안에서는 보통 유효한 FQDN이 아닙니다:
|
||||
|
||||
```bash
|
||||
SMTP_HOST=smtp-relay.gmail.com
|
||||
SMTP_PORT=587
|
||||
SMTP_EHLO_NAME=mail.yourdomain.com # FQDN the relay accepts; defaults to the (non-FQDN) container hostname
|
||||
RESEND_FROM_EMAIL=noreply@yourdomain.com
|
||||
```
|
||||
|
||||
시작 시 서버는 협상된 TLS 모드를 포함하여 선택한 제공자를 출력합니다. 예를 들어 `EmailService: SMTP relay exchange.internal.example.com:25 (starttls) from=noreply@example.com` 또는 `… smtp.qiye.aliyun.com:465 (implicit-tls) from=…`(또는 `Resend API` / `DEV mode`)와 같이 표시됩니다. 비밀번호는 절대 로그에 기록되지 않습니다. 재시작 후 SMTP 줄이 보이지 않는다면 `SMTP_HOST`가 프로세스에 도달하지 못한 것이므로, 컨테이너 환경(`docker compose -f docker-compose.selfhost.yml exec backend env | grep SMTP`)을 확인하세요.
|
||||
|
||||
**둘 다 설정하지 않으면**: 서버는 오류를 내지 않지만, **전송되어야 했던 모든 이메일이 서버의 stdout에만 기록됩니다**. 로컬 개발에는 편리하지만(로그에서 코드를 복사하면 됩니다), 프로덕션에서는 블랙홀이 됩니다.
|
||||
|
||||
## 고정 로컬 테스트 코드
|
||||
|
||||
<Callout type="warning">
|
||||
**공용으로 접근 가능한 인스턴스에서는 고정 인증 코드를 활성화하지 마세요.**
|
||||
|
||||
프로덕션이 아닌 인스턴스가 기본적으로 `888888`을 받아들이던 기존 동작은 제거되었습니다. 명시적으로 구성하지 않는 한 `888888`을 입력하는 것은 다른 잘못된 코드와 동일하게 처리됩니다.
|
||||
|
||||
이메일 백엔드를 전혀 구성하지 않은(Resend도 SMTP도 없는) 로컬 개발에서는 서버 로그에 출력되는 생성된 코드를 사용해야 합니다. 결정적인 로컬/사설 자동화가 필요하다면 `MULTICA_DEV_VERIFICATION_CODE`를 `888888` 같은 6자리 값으로 설정하고 `APP_ENV`를 프로덕션이 아닌 값으로 유지하세요:
|
||||
|
||||
```bash
|
||||
APP_ENV=development
|
||||
MULTICA_DEV_VERIFICATION_CODE=888888
|
||||
```
|
||||
|
||||
이 단축키는 `APP_ENV=production`일 때 무시됩니다.
|
||||
</Callout>
|
||||
|
||||
프로덕션 배포에서는 `MULTICA_DEV_VERIFICATION_CODE`를 비워 두고 `APP_ENV=production`으로 설정해야 합니다. `make selfhost` / `docker-compose.selfhost.yml`로 배포하는 경우 `APP_ENV`는 기본적으로 `production`입니다.
|
||||
|
||||
## Google OAuth 구성
|
||||
|
||||
선택 사항입니다. 구성하지 않으면 이메일 + 인증 코드만 사용할 수 있고, 구성하면 로그인 페이지에 "Google로 로그인" 버튼이 추가됩니다.
|
||||
|
||||
1. [Google Cloud Console](https://console.cloud.google.com/)에서 OAuth 2.0 클라이언트를 생성합니다
|
||||
2. **승인된 리디렉션 URI**(Authorized redirect URIs)를 Multica 프런트엔드 주소에 `/auth/callback`을 더한 값으로 설정합니다. 예:
|
||||
|
||||
```text
|
||||
https://multica.yourdomain.com/auth/callback
|
||||
```
|
||||
|
||||
3. 클라이언트 ID와 클라이언트 secret을 얻은 후 세 개의 환경 변수를 설정합니다:
|
||||
|
||||
```bash
|
||||
GOOGLE_CLIENT_ID=xxxxx.apps.googleusercontent.com
|
||||
GOOGLE_CLIENT_SECRET=GOCSPX-xxxxxxxxxxxxxxx
|
||||
GOOGLE_REDIRECT_URI=https://multica.yourdomain.com/auth/callback
|
||||
```
|
||||
|
||||
4. 서버를 재시작합니다.
|
||||
|
||||
**런타임에 적용됨**: 프런트엔드는 `/api/config`를 통해 런타임에 이 설정을 읽습니다. 변경 후 서버를 재시작하면 프런트엔드가 다시 빌드하거나 다시 배포할 필요 없이 새 값을 가져옵니다.
|
||||
|
||||
<Callout type="warning">
|
||||
**리디렉션 URI는 Google Console과 `GOOGLE_REDIRECT_URI` 양쪽에서 정확히 일치해야 합니다** — 프로토콜(`http` vs `https`), 끝의 슬래시, 포트까지 포함합니다. 조금이라도 일치하지 않으면 Google이 전체 OAuth 흐름을 거부하며, 사용자에게 표시되는 오류는 `redirect_uri_mismatch`입니다.
|
||||
</Callout>
|
||||
|
||||
## 누가 회원가입할 수 있는지 제한하기
|
||||
|
||||
세 개의 환경 변수가 우선순위에 따라 조합됩니다:
|
||||
|
||||
<Mermaid chart={`
|
||||
graph TD
|
||||
Start[New user first sign-in] --> A{Email in<br/>ALLOWED_EMAILS?}
|
||||
A -- Yes --> Allow[Allow signup]
|
||||
A -- No --> B{Domain in<br/>ALLOWED_EMAIL_DOMAINS?}
|
||||
B -- Yes --> Allow
|
||||
B -- No --> C{Any allowlist<br/>non-empty?}
|
||||
C -- Yes --> Block[Reject]
|
||||
C -- No --> D{ALLOW_SIGNUP<br/>= true?}
|
||||
D -- Yes --> Allow
|
||||
D -- No --> Block
|
||||
`} />
|
||||
|
||||
**기존 사용자는 언제든 다시 로그인할 수 있습니다** — 회원가입 허용 목록은 **최초 회원가입**에만 적용되며, 돌아오는 사용자는 막지 않습니다.
|
||||
|
||||
- **`ALLOWED_EMAILS`** (최고 우선순위) — 명시적 이메일 허용 목록, 쉼표로 구분합니다. **비어 있지 않으면 목록에 있는 이메일만 회원가입할 수 있습니다.**
|
||||
- **`ALLOWED_EMAIL_DOMAINS`** — 도메인 허용 목록, 쉼표로 구분합니다(예: `company.io,partner.com`).
|
||||
- **`ALLOW_SIGNUP`** — 마스터 스위치, 기본값 `true`. `false`로 설정하면 회원가입이 완전히 비활성화됩니다.
|
||||
|
||||
<Callout type="warning">
|
||||
**세 계층은 OR가 아니라 AND 의미입니다.** 흔한 잘못된 직관은 `ALLOWED_EMAIL_DOMAINS=company.io` + `ALLOW_SIGNUP=true`가 "company.io에 더해 다른 모든 사람을 허용"한다는 것입니다. 그렇지 **않습니다**. 어느 계층이든 비어 있지 않은 값이 있으면 **그에 일치하지 않는 이메일은 곧바로 거부되며**, `ALLOW_SIGNUP=true`는 그것을 무효로 만들지 못합니다.
|
||||
|
||||
실제로 "모두 허용"하려면 세 변수를 모두 비워 두세요(또는 `ALLOW_SIGNUP=true`를 유지하세요).
|
||||
</Callout>
|
||||
|
||||
**일반적인 구성**:
|
||||
|
||||
| 목표 | 구성 |
|
||||
|---|---|
|
||||
| 내부 전용, `company.io` 직원만 | `ALLOWED_EMAIL_DOMAINS=company.io` |
|
||||
| 내부 + 소수의 외부 협업자 | `ALLOWED_EMAIL_DOMAINS=company.io` + 협업자 주소를 `ALLOWED_EMAILS`에 추가 |
|
||||
| 셀프서비스 회원가입을 완전히 비활성화, 초대 전용 | `ALLOW_SIGNUP=false` |
|
||||
| 개방형 회원가입(프로덕션에는 권장하지 않음) | 셋 다 비움 |
|
||||
|
||||
## 회원가입을 비활성화해도 사람을 초대할 수 있나요?
|
||||
|
||||
**이미 Multica 계정이 있는 사람만 가능합니다.** 초대 수락은 회원가입 허용 목록을 확인하지 않습니다. 초대받은 사람이 이미 회원가입한 상태라면(예: 다른 워크스페이스에서), 초대 링크를 클릭하고 로그인하면 수락할 수 있습니다.
|
||||
|
||||
**하지만 한 번도 회원가입하지 않은 사람은 초대로 구제할 수 없습니다.** 수락하기 전에 먼저 로그인해야 하고, 로그인의 첫 단계(인증 코드 요청)는 회원가입 허용 목록 검사를 거칩니다. `ALLOW_SIGNUP=false`이거나 그들의 이메일이 `ALLOWED_EMAILS` / `ALLOWED_EMAIL_DOMAINS`에 없으면 **회원가입을 완료할 수 없으며**, 따라서 초대도 수락할 수 없습니다.
|
||||
|
||||
아직 회원가입하지 않은 외부 협업자를 초대하려면: 그들의 이메일을 `ALLOWED_EMAILS`에 임시로 추가하고, 그들이 회원가입하고 초대를 수락하기를 기다린 다음 항목을 제거하세요.
|
||||
|
||||
초대를 만들고 사용하는 방법은 [멤버 및 역할](/members-roles)을 참고하세요.
|
||||
|
||||
## 다음
|
||||
|
||||
- [환경 변수](/environment-variables) — 이 페이지에서 사용하는 모든 변수의 전체 정의
|
||||
- [인증 및 토큰](/auth-tokens) — JWT / PAT / 데몬 토큰의 분류와 사용법
|
||||
- [문제 해결](/troubleshooting) — 인증 코드 미수신, OAuth `redirect_uri_mismatch`, 회원가입 거부
|
||||
@@ -37,7 +37,7 @@ The SMTP path supports the three relay modes most on-premise mail servers (notab
|
||||
|---|---|---|---|
|
||||
| Anonymous internal relay | `25` | none — submission is trusted by IP / subnet | none on the wire (internal segment only) |
|
||||
| Authenticated submission | `587` | `SMTP_USERNAME` + `SMTP_PASSWORD` | STARTTLS, upgraded automatically |
|
||||
| Implicit TLS (SMTPS) | `465` | optional (`SMTP_USERNAME` + `SMTP_PASSWORD`) | TLS handshake on connect — auto-enabled on port `465`, or force on a non-standard port with `SMTP_TLS=implicit` |
|
||||
| Implicit TLS (SMTPS) | `465` | — | **not supported yet** — use port 25 or 587 |
|
||||
|
||||
**Anonymous Exchange relay on port 25** — the typical "internal SMTP relay" / Exchange anonymous receive connector that accepts mail from a trusted subnet without credentials:
|
||||
|
||||
@@ -61,27 +61,7 @@ SMTP_TLS_INSECURE=false # set true only for self-signed / private CA
|
||||
RESEND_FROM_EMAIL=noreply@yourdomain.com
|
||||
```
|
||||
|
||||
**Implicit TLS (SMTPS) on port 465** — for providers that only offer SMTPS and don't advertise STARTTLS (e.g. Aliyun / Tencent enterprise mail). Port `465` auto-enables implicit TLS; `SMTP_TLS=implicit` (aliases: `smtps`, `ssl`) forces it on a non-standard SMTPS port:
|
||||
|
||||
```bash
|
||||
SMTP_HOST=smtp.qiye.aliyun.com
|
||||
SMTP_PORT=465 # implicit TLS auto-enabled on 465
|
||||
SMTP_USERNAME=multica@yourdomain.com
|
||||
SMTP_PASSWORD=...
|
||||
SMTP_TLS=implicit # optional on 465; required on a non-standard SMTPS port
|
||||
RESEND_FROM_EMAIL=noreply@yourdomain.com
|
||||
```
|
||||
|
||||
**Strict public relays (e.g. Google Workspace `smtp-relay.gmail.com`)** additionally require a valid EHLO name. They reject the default `localhost` greeting from a public IP, and the relay drops the connection — which surfaces as an opaque `EOF` on a later command (`smtp auth: EOF`) rather than at the greeting. Set `SMTP_EHLO_NAME` to the FQDN the relay expects; it defaults to the machine hostname, which inside a container is usually not a valid FQDN:
|
||||
|
||||
```bash
|
||||
SMTP_HOST=smtp-relay.gmail.com
|
||||
SMTP_PORT=587
|
||||
SMTP_EHLO_NAME=mail.yourdomain.com # FQDN the relay accepts; defaults to the (non-FQDN) container hostname
|
||||
RESEND_FROM_EMAIL=noreply@yourdomain.com
|
||||
```
|
||||
|
||||
At startup the server prints which provider it picked, including the negotiated TLS mode — for example `EmailService: SMTP relay exchange.internal.example.com:25 (starttls) from=noreply@example.com` or `… smtp.qiye.aliyun.com:465 (implicit-tls) from=…` (or `Resend API` / `DEV mode`). The password is never logged. If you don't see the SMTP line after restart, `SMTP_HOST` didn't reach the process — check the container env (`docker compose -f docker-compose.selfhost.yml exec backend env | grep SMTP`).
|
||||
At startup the server prints which provider it picked — for example `EmailService: SMTP relay exchange.internal.example.com:25 from=noreply@example.com` (or `Resend API` / `DEV mode`). The password is never logged. If you don't see the SMTP line after restart, `SMTP_HOST` didn't reach the process — check the container env (`docker compose -f docker-compose.selfhost.yml exec backend env | grep SMTP`).
|
||||
|
||||
**What happens if you set neither**: the server doesn't error, but **every email that should have been sent is written to the server's stdout only**. Handy for local development (copy the code from the logs); in production it's a black hole.
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ SMTP 路径覆盖大多数本地邮件服务器(特别是 Microsoft Exchange
|
||||
|---|---|---|---|
|
||||
| 匿名内部 relay | `25` | 无 —— 按 IP / 子网信任 | 链路上无 TLS(仅限内网段) |
|
||||
| 认证提交(submission) | `587` | `SMTP_USERNAME` + `SMTP_PASSWORD` | STARTTLS,自动升级 |
|
||||
| 隐式 TLS(SMTPS) | `465` | 可选(`SMTP_USERNAME` + `SMTP_PASSWORD`) | 连上即 TLS 握手 —— 端口 `465` 自动启用;非标准端口设 `SMTP_TLS=implicit` 强制开启 |
|
||||
| 隐式 TLS(SMTPS) | `465` | —— | **暂不支持** —— 请用 25 或 587 |
|
||||
|
||||
**匿名 Exchange relay,端口 25** —— 经典的 "internal SMTP relay" / Exchange 匿名 receive connector,按可信子网放行,不要求凭据:
|
||||
|
||||
@@ -61,27 +61,7 @@ SMTP_TLS_INSECURE=false # 仅在私有 CA / 自签证书时改成 true
|
||||
RESEND_FROM_EMAIL=noreply@yourdomain.com
|
||||
```
|
||||
|
||||
**隐式 TLS(SMTPS),端口 465** —— 适用于只提供 SMTPS、不广告 STARTTLS 的服务商(例如阿里云 / 腾讯企业邮箱)。端口 `465` 会自动启用隐式 TLS;非标准 SMTPS 端口设置 `SMTP_TLS=implicit`(别名:`smtps`、`ssl`)即可强制开启:
|
||||
|
||||
```bash
|
||||
SMTP_HOST=smtp.qiye.aliyun.com
|
||||
SMTP_PORT=465 # 465 自动启用隐式 TLS
|
||||
SMTP_USERNAME=multica@yourdomain.com
|
||||
SMTP_PASSWORD=...
|
||||
SMTP_TLS=implicit # 465 上可省略;在非标准 SMTPS 端口上必填
|
||||
RESEND_FROM_EMAIL=noreply@yourdomain.com
|
||||
```
|
||||
|
||||
**严格公网 relay(例如 Google Workspace `smtp-relay.gmail.com`)**还要求一个合法的 EHLO 名称。它们会拒绝来自公网 IP 的默认 `localhost` 问候,relay 随即断开连接——这不会在问候阶段报错,而是在后续某条命令上表现为一个不知所云的 `EOF`(`smtp auth: EOF`)。把 `SMTP_EHLO_NAME` 设成 relay 期望的 FQDN;它默认取机器主机名,而在容器内这通常不是合法的 FQDN:
|
||||
|
||||
```bash
|
||||
SMTP_HOST=smtp-relay.gmail.com
|
||||
SMTP_PORT=587
|
||||
SMTP_EHLO_NAME=mail.yourdomain.com # relay 接受的 FQDN;默认取(非 FQDN 的)容器主机名
|
||||
RESEND_FROM_EMAIL=noreply@yourdomain.com
|
||||
```
|
||||
|
||||
启动时 server 会打印当前选择的 provider 和协商出的 TLS 模式,比如 `EmailService: SMTP relay exchange.internal.example.com:25 (starttls) from=noreply@example.com` 或 `… smtp.qiye.aliyun.com:465 (implicit-tls) from=…`(或 `Resend API` / `DEV mode`),密码不会出现在日志里。重启后没看到 SMTP 这行,说明 `SMTP_HOST` 没进到进程,确认下容器环境(`docker compose -f docker-compose.selfhost.yml exec backend env | grep SMTP`)。
|
||||
启动时 server 会打印当前选择的 provider,比如 `EmailService: SMTP relay exchange.internal.example.com:25 from=noreply@example.com`(或 `Resend API` / `DEV mode`),密码不会出现在日志里。重启后没看到 SMTP 这行,说明 `SMTP_HOST` 没进到进程,确认下容器环境(`docker compose -f docker-compose.selfhost.yml exec backend env | grep SMTP`)。
|
||||
|
||||
**两种都不配**:server 不报错,但所有本该发出去的邮件**只打到 server 的 stdout**。本地开发方便(你从日志抄验证码),生产环境等于黑洞。
|
||||
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
---
|
||||
title: 認証とトークン
|
||||
description: Multica には 3 種類のトークンがあります — ブラウザ、CLI、デーモンにそれぞれ 1 つずつ。どの場面でどれを使うかを解説します。
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
|
||||
Multica には 3 種類のトークンがあり、それぞれが 1 つのコンテキストに対応します。ブラウザの Web UI、コマンドラインとスクリプト、そしてデーモンです。3 つとも同じあなたを表しますが、スコープと有効期間が異なります。
|
||||
|
||||
## 3 つのトークン
|
||||
|
||||
| トークン | 形式 | 使われる場所 | 有効期間 |
|
||||
|---|---|---|---|
|
||||
| **JWT クッキー** | `multica_auth` クッキー (HttpOnly) | Web ブラウザ | 30 日 |
|
||||
| **個人アクセストークン (PAT)** | `mul_` プレフィックス | CLI、スクリプト、直接の API 呼び出し | デフォルトでは期限なし。API で作成する際に `expires_in_days` を渡せます |
|
||||
| **デーモントークン** | `mdt_` プレフィックス | デーモンとサーバー間の通信 | デーモン自体が管理 |
|
||||
|
||||
日常的な利用では、最初の 2 つだけを直接扱うことになります。**[デーモン](/daemon-runtimes)トークン**は `multica daemon login` が自動的に作成・更新するため、気にする必要はありません。
|
||||
|
||||
## 各トークンがアクセスできるもの
|
||||
|
||||
| API ルート | JWT クッキー | PAT | デーモントークン |
|
||||
|---|---|---|---|
|
||||
| `/api/user/*` (ユーザーレベルの操作) | ✓ | ✓ | ✗ |
|
||||
| `/api/workspaces/:id/*` (ワークスペースレベル) | ✓ | ✓ | ✗ |
|
||||
| `/api/daemon/*` (デーモン専用) | ✗ | ✓ | ✓ |
|
||||
| WebSocket `/ws` (リアルタイムプッシュ) | ✓ (クッキー) | ✓ (最初のメッセージで認証) | ✗ |
|
||||
|
||||
**PAT はほぼすべてにアクセスできます** — これは「完全なあなた」を表します。デーモントークンはデーモンに必要なこと、つまりタスクを取得して結果を報告することしかできません。
|
||||
|
||||
**どちらも `/api/daemon/*` にアクセスできますが、スコープが異なります。** PAT は**ユーザー全体**を表し、一度認証されると、あなたが所属するすべてのワークスペースを見ることができます。デーモントークンは作成時点で単一のワークスペースに固定され、そのワークスペースのリソースにしかアクセスできません。本番環境では、デーモンはデーモントークンで実行してください。手軽さのために PAT を使う近道を選ばないでください。そうしないと、デーモンに必要な以上にはるかに大きな権限を与えてしまいます。
|
||||
|
||||
## ログイン
|
||||
|
||||
### メール + 認証コード
|
||||
|
||||
1. メールアドレスを入力すると、サーバーが 6 桁のコードを送信します。
|
||||
2. コードを入力すると、サーバーが JWT クッキーを発行(ブラウザ)するか、PAT に交換(CLI)します。
|
||||
|
||||
<Callout type="warning">
|
||||
**セルフホストの運用者は注意してください**: 公開デプロイでは `MULTICA_DEV_VERIFICATION_CODE` を空のままにしておいてください。固定のローカルテストコードを有効にすると、`APP_ENV` が production 以外の間は、コードをリクエストできる人なら誰でもその値でサインインできてしまいます。[セルフホスト認証の構成](/auth-setup)を参照してください。
|
||||
</Callout>
|
||||
|
||||
### Google OAuth
|
||||
|
||||
**Sign in with Google** をクリックして、標準の OAuth コールバックを通過してください。セルフホストには `GOOGLE_CLIENT_ID`、`GOOGLE_CLIENT_SECRET`、そしてリダイレクト URI を構成する必要があります — [セルフホスト認証の構成](/auth-setup)を参照してください。
|
||||
|
||||
## PAT の作成、表示、失効
|
||||
|
||||
PAT の**作成**は 2 つの方法で行えます。
|
||||
|
||||
- **Web UI**: 設定 → 個人アクセストークン → 新しいトークン
|
||||
- **CLI**: `multica login` は、まだローカル PAT がない場合に自動的に 1 つ作成します
|
||||
|
||||
<Callout type="warning">
|
||||
**完全な PAT は作成時に正確に 1 回だけ表示されます。** 更新したりダイアログを閉じたりした後は、二度と見ることができません。
|
||||
|
||||
Multica はデータベースに PAT のハッシュだけを保存します — サーバーでさえ元の値を取得できません。すぐにコピーして保存してください。紛失した場合の唯一の手段は、失効させて新しく作り直すことです。
|
||||
</Callout>
|
||||
|
||||
既存の PAT の**表示**(名前、作成時刻、最終使用時刻 — 完全なトークンは**含みません**)は、設定 → 個人アクセストークンにあります。
|
||||
|
||||
PAT の**失効**: 一覧で Revoke をクリックしてください。失効はすぐに反映されます — その PAT で送られる次のリクエストは 401 で拒否されます。
|
||||
|
||||
## ログアウトはローカルトークンを削除するだけ
|
||||
|
||||
`multica auth logout` を実行するか、Web UI でログアウトをクリックすると、
|
||||
|
||||
- **ローカルトークンが消去されます** — CLI は `~/.multica/config.json` から PAT を削除し、ブラウザはクッキーを削除します。
|
||||
- **PAT はサーバー上では依然として有効です** — ログアウトする前に誰かがあなたの PAT を入手していた場合(たとえば別のマシンにコピーしていた場合)、その人は**依然としてそれを使用できます**。
|
||||
|
||||
<Callout type="warning">
|
||||
**PAT が漏洩したと疑われる場合は、単にログアウトするだけにしないでください。** 設定 → 個人アクセストークンに進み、そのトークンを**失効**させてください。失効だけが、漏洩したトークンを即座に無効化します。
|
||||
</Callout>
|
||||
|
||||
## 次のステップ
|
||||
|
||||
- [CLI コマンドリファレンス](/cli) — すべての CLI コマンドの認証は自動です
|
||||
- [セルフホスト認証の構成](/auth-setup) — セルフホスト時にメール、OAuth、サインアップ許可リストを構成する方法
|
||||
- [デーモンとランタイム](/daemon-runtimes) — デーモントークンがどこから来るのか
|
||||
@@ -1,80 +0,0 @@
|
||||
---
|
||||
title: 인증과 토큰
|
||||
description: Multica에는 세 종류의 토큰이 있습니다 — 브라우저, CLI, 데몬에 각각 하나씩. 어떤 상황에 무엇을 쓰는지 설명합니다.
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
|
||||
Multica에는 세 종류의 토큰이 있으며, 각각 하나의 컨텍스트에 대응합니다: 브라우저 Web UI, 명령줄과 스크립트, 그리고 데몬입니다. 세 가지 모두 동일한 당신을 나타내지만, 범위와 수명이 다릅니다.
|
||||
|
||||
## 세 가지 토큰
|
||||
|
||||
| 토큰 | 형식 | 사용되는 곳 | 수명 |
|
||||
|---|---|---|---|
|
||||
| **JWT 쿠키** | `multica_auth` 쿠키 (HttpOnly) | 웹 브라우저 | 30일 |
|
||||
| **개인 액세스 토큰 (PAT)** | `mul_` 접두사 | CLI, 스크립트, 직접 API 호출 | 기본적으로 만료 없음. API로 생성할 때 `expires_in_days`를 전달할 수 있습니다 |
|
||||
| **데몬 토큰** | `mdt_` 접두사 | 데몬-서버 통신 | 데몬 자체가 관리 |
|
||||
|
||||
일상적인 사용에서는 앞의 두 가지만 직접 다루게 됩니다. **[데몬](/daemon-runtimes) 토큰**은 `multica daemon login`이 자동으로 생성하고 갱신하므로, 신경 쓸 필요가 없습니다.
|
||||
|
||||
## 각 토큰이 접근할 수 있는 것
|
||||
|
||||
| API 라우트 | JWT 쿠키 | PAT | 데몬 토큰 |
|
||||
|---|---|---|---|
|
||||
| `/api/user/*` (사용자 수준 작업) | ✓ | ✓ | ✗ |
|
||||
| `/api/workspaces/:id/*` (워크스페이스 수준) | ✓ | ✓ | ✗ |
|
||||
| `/api/daemon/*` (데몬 전용) | ✗ | ✓ | ✓ |
|
||||
| WebSocket `/ws` (실시간 푸시) | ✓ (쿠키) | ✓ (첫 메시지로 인증) | ✗ |
|
||||
|
||||
**PAT는 거의 모든 것에 접근할 수 있습니다** — 이는 "완전한 당신"을 나타냅니다. 데몬 토큰은 데몬에 필요한 일, 즉 작업을 가져오고 결과를 보고하는 것만 할 수 있습니다.
|
||||
|
||||
**둘 다 `/api/daemon/*`에 접근할 수 있지만, 범위가 다릅니다.** PAT는 **사용자 전체**를 나타내며 — 일단 인증되면 당신이 속한 모든 워크스페이스를 볼 수 있습니다. 데몬 토큰은 생성 시점에 단일 워크스페이스에 고정되며 해당 워크스페이스의 리소스에만 접근할 수 있습니다. 프로덕션에서는 데몬을 데몬 토큰으로 실행하세요. 편의를 위해 PAT를 사용하는 지름길을 택하지 마세요. 그렇지 않으면 데몬에 필요한 것보다 훨씬 많은 권한을 부여하게 됩니다.
|
||||
|
||||
## 로그인
|
||||
|
||||
### Email + 인증 코드
|
||||
|
||||
1. 이메일을 입력하면 서버가 6자리 코드를 보냅니다.
|
||||
2. 코드를 입력하면 서버가 JWT 쿠키를 발급(브라우저)하거나 PAT로 교환(CLI)합니다.
|
||||
|
||||
<Callout type="warning">
|
||||
**자체 호스팅 운영자는 주의하세요**: 공개 배포에서는 `MULTICA_DEV_VERIFICATION_CODE`를 비워 두세요. 고정된 로컬 테스트 코드를 활성화하면, `APP_ENV`가 production이 아닌 동안 코드를 요청할 수 있는 누구나 그 값으로 로그인할 수 있습니다. [자체 호스팅 인증 구성](/auth-setup)을 참고하세요.
|
||||
</Callout>
|
||||
|
||||
### Google OAuth
|
||||
|
||||
**Sign in with Google**을 클릭하고 표준 OAuth 콜백을 거치세요. 자체 호스팅에는 `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, 그리고 리디렉션 URI를 구성해야 합니다 — [자체 호스팅 인증 구성](/auth-setup)을 참고하세요.
|
||||
|
||||
## PAT 생성, 조회, 취소
|
||||
|
||||
PAT **생성**은 두 가지 방법으로 할 수 있습니다:
|
||||
|
||||
- **Web UI**: 설정 → API 토큰 → 새 토큰
|
||||
- **CLI**: `multica login`은 아직 로컬 PAT가 없으면 자동으로 하나를 생성합니다
|
||||
|
||||
<Callout type="warning">
|
||||
**전체 PAT는 생성될 때 정확히 한 번만 표시됩니다.** 새로 고침하거나 대화상자를 닫은 후에는 다시 볼 수 없습니다.
|
||||
|
||||
Multica는 데이터베이스에 PAT의 해시만 저장합니다 — 서버조차 원본을 가져올 수 없습니다. 즉시 복사하여 저장하세요. 분실하면 유일한 방법은 취소하고 새로 생성하는 것입니다.
|
||||
</Callout>
|
||||
|
||||
기존 PAT **조회**(이름, 생성 시각, 마지막 사용 시각 — 전체 토큰은 **포함하지 않음**)는 설정 → API 토큰에 있습니다.
|
||||
|
||||
PAT **취소**: 목록에서 Revoke를 클릭하세요. 취소는 즉시 적용됩니다 — 그 PAT로 보낸 다음 요청은 401로 거부됩니다.
|
||||
|
||||
## 로그아웃은 로컬 토큰만 삭제합니다
|
||||
|
||||
`multica auth logout`을 실행하거나 Web UI에서 로그아웃을 클릭하면:
|
||||
|
||||
- **로컬 토큰이 지워집니다** — CLI는 `~/.multica/config.json`에서 PAT를 제거하고, 브라우저는 쿠키를 삭제합니다.
|
||||
- **PAT는 서버에서 여전히 유효합니다** — 로그아웃하기 전에 누군가 당신의 PAT를 입수했다면(예를 들어 다른 기기에 복사했다면), 그들은 **여전히 그것을 사용할 수 있습니다**.
|
||||
|
||||
<Callout type="warning">
|
||||
**PAT가 유출되었다고 의심되면, 단순히 로그아웃하지 마세요.** 설정 → API 토큰로 가서 그 토큰을 **취소**하세요. 취소만이 유출된 토큰을 즉시 무효화합니다.
|
||||
</Callout>
|
||||
|
||||
## 다음 단계
|
||||
|
||||
- [CLI 명령어 레퍼런스](/cli) — 모든 CLI 명령어의 인증은 자동입니다
|
||||
- [자체 호스팅 인증 구성](/auth-setup) — 자체 호스팅 시 이메일, OAuth, 가입 허용 목록을 구성하는 방법
|
||||
- [데몬과 런타임](/daemon-runtimes) — 데몬 토큰이 어디서 오는지
|
||||
@@ -1,239 +0,0 @@
|
||||
---
|
||||
title: オートパイロット
|
||||
description: エージェントが cron スケジュールやインバウンド webhook で作業を開始したり、UI や CLI で一度だけ手動でトリガーしたりできるようにします。
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
|
||||
オートパイロットは、[エージェント](/agents)が**スケジュールに従って自動的に作業を開始**できるようにします — cron 式とタイムゾーンを設定すると、あなたが何もトリガーしなくても Multica が自ら [`task`](/tasks) をディスパッチします。定期点検、繰り返しのレポート、夜間のクリーンアップ作業など、「常設指示(standing order)」の形の作業に適しています。残りの 3 つのトリガー経路([割り当て](/assigning-issues)、[@-メンション](/mentioning-agents)、[チャット](/chat) — いずれもあなた自身が起点となる方式)と比べたとき、オートパイロットの核心的な違いは**時間駆動**であることです。
|
||||
|
||||
## オートパイロットを構成する
|
||||
|
||||
ワークスペースの**オートパイロット**ページで新しいオートパイロットを作成します。次の項目を設定します。
|
||||
|
||||
- **名前(Name)** — 表示名
|
||||
- **エージェント(Agent)** — 実行をディスパッチする対象
|
||||
- **優先度(Priority)** — 生成される `task` に継承されます(イシューの優先度と同じ意味)
|
||||
- **説明 / プロンプト(Description / prompt)** — 実行のたびにエージェントが受け取る作業説明
|
||||
- **実行モード(Execution mode)** — 以下を参照
|
||||
- **トリガー(Triggers)** — `schedule`(cron + タイムゾーン)または `webhook` のうち少なくとも 1 つ
|
||||
|
||||
## 実行モードを選ぶ
|
||||
|
||||
オートパイロットには 2 つの実行モードがあります。**「イシュー作成」モードから始めてください。**
|
||||
|
||||
- **イシュー作成モード(Create issue mode)**(`create_issue`) — デフォルトであり、**推奨**されます。各トリガーはまずワークスペースにイシューを作成し(タイトルには現在、単一のプレースホルダー `{{date}}` のみがサポートされ、これは `YYYY-MM-DD` 形式の UTC 日付に補間されます。それ以外の `{{...}}` トークンは作成時点で拒否されるため、タイプミスがイシュータイトルにリテラル文字列として静かに紛れ込むことを防ぎます)、通常の割り当てフローを通じてそのイシューをエージェントに割り当てます。すべての作業は、手動で割り当てたイシューと同じ履歴、コメント、ステータスを持った状態でイシューボードに上がります。
|
||||
- **実行専用モード(Run-only mode)**(`run_only`) — イシュー作成をスキップし、`task` を直接キューに入れます。この実行はボードには表示されません — オートパイロットの実行履歴でのみ確認できます。
|
||||
|
||||
## スケジュールに従って実行する
|
||||
|
||||
すべてのオートパイロットには少なくとも 1 つの `schedule` トリガーが必要です。Cron は**標準の 5 フィールド形式**(分 時 日 月 曜日)を使用し、最小単位は **1 分**です(秒単位はありません)。タイムゾーンは IANA 形式(例: `Asia/Shanghai`)で、cron 式がどのタイムゾーンで解釈されるかを決定します。
|
||||
|
||||
いくつかの例:
|
||||
|
||||
- `0 9 * * 1-5`, `Asia/Shanghai` — 平日の北京時間午前 9 時
|
||||
- `*/30 * * * *`, `UTC` — 30 分ごと
|
||||
- `0 3 * * *`, `UTC` — 毎日 UTC 午前 3 時
|
||||
|
||||
Multica サーバーは**30 秒**ごとに期限が来たトリガーをスキャンします — **実際の発火時刻は最大 30 秒まで遅れる可能性があり**、秒単位で正確ではありません。発火時刻のあたりでサーバーが再起動された場合、起動時に逃したトリガーを追いつきます(何も失われませんが、すぐに発火します)。
|
||||
|
||||
## 一度だけ手動でトリガーする
|
||||
|
||||
オートパイロットのデバッグ中に cron を待たないためには、手動でトリガーしてください。
|
||||
|
||||
- UI: オートパイロット詳細ページで「Run now」をクリック
|
||||
- CLI:
|
||||
|
||||
```bash
|
||||
multica autopilot trigger <autopilot-id>
|
||||
```
|
||||
|
||||
手動トリガーは `schedule` トリガーとまったく同じ実行フローを通り — 実行レコードの `source` フィールドのみが `manual` とマークされます。
|
||||
|
||||
## webhook からトリガーする
|
||||
|
||||
オートパイロットはインバウンドの HTTP webhook でも発火できます。オートパイロット詳細ページで **Webhook** トリガーを追加すると、Multica は次のような形の一意の URL を生成します。
|
||||
|
||||
```
|
||||
https://<your-multica-host>/api/webhooks/autopilots/awt_…
|
||||
```
|
||||
|
||||
その URL に任意の JSON を POST してください — Multica は `source = webhook` で実行を記録し、本文を実行の `trigger_payload` として保存し、schedule トリガーとまったく同じ方法でエージェントをディスパッチします。
|
||||
|
||||
```bash
|
||||
curl -X POST "$MULTICA_WEBHOOK_URL" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"event":"demo.received","eventPayload":{"message":"hello"}}'
|
||||
```
|
||||
|
||||
**イシュー作成モード**では、インバウンドの payload が新しいイシューの説明に追記され、エージェントがインラインで読めるようになります。**実行専用モード**では、payload はデーモンがエージェントに渡す実行コンテキストの一部になります。
|
||||
|
||||
### Payload の形
|
||||
|
||||
独自のエンベロープ(envelope)を送れます。
|
||||
|
||||
```json
|
||||
{ "event": "github.pull_request.opened", "eventPayload": { } }
|
||||
```
|
||||
|
||||
…または任意の JSON オブジェクト / 配列を送ることもできます。Multica はこれを内部エンベロープに正規化します。
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "<inferred>",
|
||||
"eventPayload": <your body>,
|
||||
"request": { "receivedAt": "<rfc3339>", "contentType": "application/json" }
|
||||
}
|
||||
```
|
||||
|
||||
`event` フィールドを指定しない場合、Multica は一般的なヘッダーと本文フィールドからこれを推論します(`X-GitHub-Event` + 本文 `action`、`X-Gitlab-Event`、`X-Event-Type`、本文の `event`/`type`/`action`)。どれも一致しない場合、イベントは `webhook.received` になります。
|
||||
|
||||
GitHub のようなソースを構成するときは、content type を `application/json` に設定してください — フォームエンコードされた webhook payload は受け付けられません。
|
||||
|
||||
### イベントフィルター
|
||||
|
||||
新しい webhook トリガーはインバウンドの POST ごとに発火します。単一用途の URL には問題ありませんが、多数のイベントタイプをファンアウトするソース(GitHub が代表的です — 単一のリポジトリ webhook 一つが `push`、`pull_request`、`workflow_run`、`check_suite` などを配信できます)にはノイズになります。webhook トリガーの**イベントフィルター(Event filters)**セクションを使うと、実際に実行をディスパッチするイベントを制限でき、それ以外のすべては `status = ignored`、`reason = event_filtered` で配信履歴に記録され、実行もイシューも作成されません。
|
||||
|
||||
各行は 1 つのルールです。**イベント名(event name)**と、任意でカンマ区切りの **action** リストで構成されます。Multica は**いずれか 1 つ**の行でも一致すれば webhook を許可します。セクションを空のままにすると、すべてを受け付けます(フィルタリング以前の動作)。
|
||||
|
||||
例:
|
||||
|
||||
| イベント名 | Actions | 一致対象 |
|
||||
| -------------- | ------------------- | ------------------------------------------------------------------------ |
|
||||
| `workflow_run` | `completed, failed` | `action: completed` または `action: failed` の `workflow_run` イベントのみ |
|
||||
| `workflow_run` | _(空)_ | action に関係なくすべての `workflow_run` イベント |
|
||||
| `push` | _(空)_ | すべての `push` イベント |
|
||||
|
||||
#### イベント名と action の出所
|
||||
|
||||
Multica は次の順序でインバウンドリクエストから `event` 名と `action` を導き出します — **最初に一致したものが優先されます**。
|
||||
|
||||
**1. 本文エンベロープ(Body envelope)。** 本文が文字列の `event` フィールドを持つ JSON オブジェクトであれば、その値がそのままイベント名になります。任意の `eventPayload` オブジェクトは、自身の `action` / `state` / `conclusion` / `status` フィールドから action 候補を提供します。
|
||||
|
||||
```bash
|
||||
curl -X POST "$MULTICA_WEBHOOK_URL" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"event":"trigger","eventPayload":{"action":"true"}}'
|
||||
# inferred: event = trigger, action candidate = true
|
||||
```
|
||||
|
||||
**2. ヘッダー(Headers)。** 本文エンベロープがない場合、Multica は次のよく知られたプロバイダーヘッダーを読みます。
|
||||
|
||||
- `X-GitHub-Event: <event>` — (存在する場合)最上位の本文 `action` フィールドと組み合わされて `github.<event>.<action>` を形成します。
|
||||
- `X-Gitlab-Event: <event>` — `gitlab.<event>` になります。
|
||||
- `X-Event-Type: <event>` — そのまま通過します。
|
||||
|
||||
```bash
|
||||
# GitHub-style: header gives the event name, body gives the action.
|
||||
curl -X POST "$MULTICA_WEBHOOK_URL" \
|
||||
-H 'X-GitHub-Event: workflow_run' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"action":"completed"}'
|
||||
# inferred: event = github.workflow_run.completed
|
||||
# → matches a filter row of workflow_run / completed
|
||||
|
||||
# Generic event-type header — no body fields needed.
|
||||
curl -X POST "$MULTICA_WEBHOOK_URL" \
|
||||
-H 'X-Event-Type: trigger.true' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{}'
|
||||
# inferred: event = trigger.true → matches trigger / true
|
||||
```
|
||||
|
||||
**3. 本文フォールバック(Body fallback)。** 本文エンベロープも既知のヘッダーもない場合、Multica は次の順序で最上位の本文文字列フィールドにフォールバックします: `event` → `type` → `action`。
|
||||
|
||||
```bash
|
||||
curl -X POST "$MULTICA_WEBHOOK_URL" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"type":"trigger","action":"true"}'
|
||||
# inferred: event = trigger (from `type`), action candidate = true
|
||||
```
|
||||
|
||||
**4. デフォルト(Default)。** 上記のいずれも一致しない場合、イベントは `webhook.received` で、action 候補はありません。
|
||||
|
||||
**action 候補、全リスト。** イベントが決定されると、Multica は以下のすべての値を可能な action 一致対象として考慮します。
|
||||
|
||||
- イベントが `provider.event.<action>` の形のときのイベント名のサフィックス(例: `github.workflow_run.completed` → `completed`)。
|
||||
- 本文フィールド `action`、`state`、`conclusion`、`status` — **JSON 文字列のときのみ該当します**。ブール値(`{"action": true}`)や数値は資格がないため、`event=trigger, action=true` を期待するフィルターは `{"trigger": true}` の本文とは決して一致しません。`true` は文字列ではなく bool だからです。
|
||||
|
||||
**よくある落とし穴。** `Event name: trigger` / `Actions: true` のようなフィルター行は、「本文に `trigger: true` があれば発火せよ」という意味では**ありません** — イベントフィルターは任意の本文フィールドではなく、*推論されたイベントと action* に一致させます。これにヒットさせるには、`X-Event-Type` で `trigger.true` を送るか(または上に示した本文エンベロープを使ってください)。保存されたフィルター行の周囲の空白(`" workflow_run "`)はそのまま保存され、決して一致しないため — 保存する前に trim してください。
|
||||
|
||||
#### クイックテスト
|
||||
|
||||
フィルターを構成したら、`curl` で両方の分岐を確認できます。
|
||||
|
||||
```bash
|
||||
# Allowed — header drives event=workflow_run, body drives action=completed
|
||||
curl -X POST "$MULTICA_WEBHOOK_URL" \
|
||||
-H 'X-GitHub-Event: workflow_run' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"action":"completed"}'
|
||||
# → 200 {"status":"accepted", ...}
|
||||
|
||||
# Filtered — same event, action not in allowlist
|
||||
curl -X POST "$MULTICA_WEBHOOK_URL" \
|
||||
-H 'X-GitHub-Event: workflow_run' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"action":"in_progress"}'
|
||||
# → 200 {"status":"ignored","reason":"event_filtered"}
|
||||
```
|
||||
|
||||
### URL は bearer secret です
|
||||
|
||||
生成された URL **そのものが**認証情報です。それを持っている人は誰でもオートパイロットを発火できます。トークンのように扱ってください。
|
||||
|
||||
- **公開のイシュースレッド、スクリーンショット、チャット履歴に貼り付けないでください。**
|
||||
- **漏洩したら交換してください** — トリガー行で「Rotate URL」をクリックするか、`multica autopilot trigger-rotate-url <autopilot-id> <trigger-id>` を実行してください。古い URL はただちに動作を停止します。
|
||||
- 強力なソース認証が必要なソースの場合は、トリガーごとの HMAC 署名検証を待ってください。この v1 URL は bearer 方式のみをサポートします。
|
||||
- 現時点では、オートパイロットを閲覧できるワークスペースメンバーであればその webhook URL を読めます — 役割ごとのより厳格な secret の可視性は後続作業です。
|
||||
|
||||
### ステータスコードの意味
|
||||
|
||||
Multica は正常な no-op の結果に対して `status` フィールド付きで `200 OK` を返すため、プロバイダーの webhook 再試行メカニズムが URL を叩き続けることはありません。
|
||||
|
||||
- `{"status":"accepted","run_id":"…","autopilot_id":"…","trigger_id":"…"}` — 実行がディスパッチされました。
|
||||
- `{"status":"skipped","run_id":"…","reason":"agent runtime is offline at dispatch time"}` — 割り当て先のランタイムがオフラインで、`skipped` の実行として記録されます。
|
||||
- `{"status":"ignored","reason":"trigger_disabled"}` — トリガーが無効になっています。
|
||||
- `{"status":"ignored","reason":"autopilot_paused"}` — オートパイロットが一時停止しています。
|
||||
- `{"status":"ignored","reason":"autopilot_archived"}` — オートパイロットがアーカイブされています。
|
||||
|
||||
2xx 以外の応答は実際の失敗を扱います。
|
||||
|
||||
- `400` — 無効な JSON、スカラー本文、空の本文。
|
||||
- `404` — 不明なトークン(`{"error":"webhook not found"}`)。
|
||||
- `413` — payload が 256 KiB を超えました。
|
||||
- `429` — トークンごとのレート制限超過(デフォルトは 60 req/min)。
|
||||
|
||||
### セルフホスト: 公開 URL を構成する
|
||||
|
||||
サーバーに `MULTICA_PUBLIC_URL` が設定されている場合(例: `https://multica.example.com`)、トリガー応答に絶対パスの `webhook_url` が含まれ、UI にはすぐにコピーできる URL が表示されます。設定しない場合、UI はクライアントの API origin から URL を構成します — デスクトップと同一オリジンの Web には問題ありませんが、カスタムのセルフホストリバースプロキシには適しません。Multica は、誤って構成されたリバースプロキシが攻撃者の制御するホストを指す webhook URL をサーバーに発行させて欺くことができないよう、`Host` / `X-Forwarded-Host` ヘッダーから公開ホストを導出しないよう意図的に設計されています。
|
||||
|
||||
## 実行履歴を見る
|
||||
|
||||
すべてのトリガーは**実行レコード(run record)**を生成し、オートパイロット詳細ページの「History」タブで確認できます。
|
||||
|
||||
- トリガーソース(`schedule` / `manual` / `webhook`)
|
||||
- 開始時刻、完了時刻
|
||||
- ステータス(`issue_created` / `running` / `completed` / `failed` / `skipped`)
|
||||
- 連携したイシュー(イシュー作成モード)または `task`(実行専用モード)
|
||||
- 失敗理由(失敗またはスキップした場合)
|
||||
|
||||
## オートパイロットが失敗したらどうなるか
|
||||
|
||||
<Callout type="warning">
|
||||
**オートパイロットの失敗は自動的に再試行されず、インボックス通知も送られません。** 失敗は実行履歴に `failed` のエントリを残すだけで — 割り当てや @-メンションのようなシステムレベルの再キューイングもなく、誰にも通知が行きません。オートパイロットが定期的な場合、**次の cron 発火が新しい実行をトリガー**しますが、失敗した作業が自動的に再実行されることはありません。
|
||||
|
||||
オートパイロットが重要な場合は、独自のモニタリングを設計してください — 例えば、エージェントに成功時にコメントを残させ、コメントの欠落に気づくことで失敗を検出する、といった具合です。
|
||||
</Callout>
|
||||
|
||||
自動再試行がない理由: オートパイロットはすでに定期的であるため、システムレベルの再試行を追加すると次の予定実行の上に重なり、重複した実行を生み出します。スケジューリングを完全に cron に任せることで、すっきりと保てます。
|
||||
|
||||
## まだ提供されていない機能
|
||||
|
||||
**API 種類のトリガーはまだ接続されていません。** トリガースキーマは `api` 種類を予約していますが、それを発火させるイングレスルートはありません。UI は既存の行に Deprecated バッジを表示し、コピー / 交換の操作は提供しません。トリガーごとの HMAC 署名検証、IP 許可リスト、プロバイダー固有のイベントプリセットは後続作業として追跡されており、v1 URL は bearer 方式のみをサポートします。
|
||||
|
||||
## 次へ
|
||||
|
||||
- [**エージェントにイシューを割り当てる**](/assigning-issues) — イシューをエージェントに一回限りで引き渡す
|
||||
- [**コメントでエージェントを @-メンションする**](/mentioning-agents) — コメントからエージェントを呼んで一度見てもらう
|
||||
- [**チャット**](/chat) — イシューの外での一対一の会話
|
||||
@@ -1,239 +0,0 @@
|
||||
---
|
||||
title: 오토파일럿
|
||||
description: 에이전트가 cron 스케줄, 인바운드 webhook으로 작업을 시작하거나, UI나 CLI로 한 번 수동 트리거하게 합니다.
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
|
||||
오토파일럿은 [에이전트](/agents)가 **스케줄에 따라 자동으로 작업을 시작**하게 합니다 — cron 표현식과 타임존을 설정하면, 여러분이 아무것도 트리거하지 않아도 Multica가 알아서 [`task`](/tasks)를 디스패치합니다. 정기 점검, 반복 보고서, 야간 정리 작업 등 "상시 지시(standing order)" 형태의 작업에 잘 맞습니다. 나머지 세 가지 트리거 경로([할당](/assigning-issues), [@-멘션](/mentioning-agents), [채팅](/chat) — 모두 여러분이 직접 시작하는 방식)와 비교했을 때, 오토파일럿의 핵심 차이는 **시간 기반**이라는 점입니다.
|
||||
|
||||
## 오토파일럿 구성하기
|
||||
|
||||
워크스페이스의 **오토파일럿** 페이지에서 새 오토파일럿을 만듭니다. 다음 항목을 설정합니다.
|
||||
|
||||
- **이름(Name)** — 표시 이름
|
||||
- **에이전트(Agent)** — 실행을 디스패치할 대상
|
||||
- **우선순위(Priority)** — 생성되는 `task`에 상속됩니다(이슈 우선순위와 동일한 의미)
|
||||
- **설명 / 프롬프트(Description / prompt)** — 매 실행마다 에이전트가 받는 작업 설명
|
||||
- **실행 모드(Execution mode)** — 아래 참고
|
||||
- **트리거(Triggers)** — `schedule`(cron + 타임존) 또는 `webhook` 중 최소 하나
|
||||
|
||||
## 실행 모드 선택하기
|
||||
|
||||
오토파일럿에는 두 가지 실행 모드가 있습니다. **"이슈 생성" 모드부터 시작하세요.**
|
||||
|
||||
- **이슈 생성 모드(Create issue mode)**(`create_issue`) — 기본값이며 **권장**됩니다. 각 트리거는 먼저 워크스페이스에 이슈를 생성한 다음(제목에는 현재 단일 플레이스홀더 `{{date}}` 하나만 지원되며, 이는 `YYYY-MM-DD` 형식의 UTC 날짜로 보간됩니다. 그 외의 `{{...}}` 토큰은 생성 시점에 거부되므로, 오타가 이슈 제목에 리터럴 문자열로 조용히 들어가는 일을 막습니다), 일반 할당 흐름을 통해 그 이슈를 에이전트에게 할당합니다. 모든 작업은 수동으로 할당한 이슈와 동일한 히스토리, 댓글, 상태를 가진 채 이슈 보드에 올라갑니다.
|
||||
- **실행 전용 모드(Run-only mode)**(`run_only`) — 이슈 생성을 건너뛰고 `task`를 곧바로 대기열에 넣습니다. 이 실행은 보드에 표시되지 않으며 — 오토파일럿의 실행 히스토리에서만 확인할 수 있습니다.
|
||||
|
||||
## 스케줄에 따라 실행하기
|
||||
|
||||
모든 오토파일럿에는 최소 하나의 `schedule` 트리거가 필요합니다. Cron은 **표준 5필드 형식**(분 시 일 월 요일)을 사용하며, 최소 단위는 **1분**입니다(초 단위는 없음). 타임존은 IANA 형식(예: `Asia/Shanghai`)이며, cron 표현식이 어느 타임존으로 해석될지를 결정합니다.
|
||||
|
||||
몇 가지 예시입니다.
|
||||
|
||||
- `0 9 * * 1-5`, `Asia/Shanghai` — 평일 베이징 시간 오전 9시
|
||||
- `*/30 * * * *`, `UTC` — 30분마다
|
||||
- `0 3 * * *`, `UTC` — 매일 UTC 오전 3시
|
||||
|
||||
Multica 서버는 **30초**마다 만료된 트리거를 스캔합니다 — **실제 발화 시각은 최대 30초까지 지연될 수 있으며**, 초 단위로 정확하지는 않습니다. 발화 시각 즈음에 서버가 재시작되면, 시작 시 놓친 트리거를 따라잡습니다(아무것도 유실되지 않지만 즉시 발화됩니다).
|
||||
|
||||
## 수동으로 한 번 트리거하기
|
||||
|
||||
오토파일럿을 디버깅하는 동안 cron을 기다리지 않으려면 수동으로 트리거하세요.
|
||||
|
||||
- UI: 오토파일럿 상세 페이지에서 "Run now" 클릭
|
||||
- CLI:
|
||||
|
||||
```bash
|
||||
multica autopilot trigger <autopilot-id>
|
||||
```
|
||||
|
||||
수동 트리거는 `schedule` 트리거와 완전히 동일한 실행 흐름을 거치며 — 실행 레코드의 `source` 필드만 `manual`로 표시됩니다.
|
||||
|
||||
## Webhook으로 트리거하기
|
||||
|
||||
오토파일럿은 인바운드 HTTP webhook으로도 발화할 수 있습니다. 오토파일럿 상세 페이지에서 **Webhook** 트리거를 추가하면, Multica가 다음과 같은 형태의 고유 URL을 생성합니다.
|
||||
|
||||
```
|
||||
https://<your-multica-host>/api/webhooks/autopilots/awt_…
|
||||
```
|
||||
|
||||
그 URL로 아무 JSON이나 POST하세요 — Multica는 `source = webhook`로 실행을 기록하고, 본문을 실행의 `trigger_payload`로 저장한 다음, schedule 트리거와 정확히 동일한 방식으로 에이전트를 디스패치합니다.
|
||||
|
||||
```bash
|
||||
curl -X POST "$MULTICA_WEBHOOK_URL" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"event":"demo.received","eventPayload":{"message":"hello"}}'
|
||||
```
|
||||
|
||||
**이슈 생성 모드**에서는 인바운드 payload가 새 이슈의 설명에 덧붙여져 에이전트가 인라인으로 읽을 수 있습니다. **실행 전용 모드**에서는 payload가 데몬이 에이전트에게 넘기는 실행 컨텍스트의 일부가 됩니다.
|
||||
|
||||
### Payload 형태
|
||||
|
||||
직접 만든 봉투(envelope)를 보낼 수 있습니다.
|
||||
|
||||
```json
|
||||
{ "event": "github.pull_request.opened", "eventPayload": { } }
|
||||
```
|
||||
|
||||
…또는 임의의 JSON 객체/배열을 보낼 수도 있습니다. Multica는 이를 내부 봉투로 정규화합니다.
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "<inferred>",
|
||||
"eventPayload": <your body>,
|
||||
"request": { "receivedAt": "<rfc3339>", "contentType": "application/json" }
|
||||
}
|
||||
```
|
||||
|
||||
`event` 필드를 제공하지 않으면, Multica는 일반적인 헤더와 본문 필드로부터 이를 추론합니다(`X-GitHub-Event` + 본문 `action`, `X-Gitlab-Event`, `X-Event-Type`, 본문의 `event`/`type`/`action`). 어느 것도 일치하지 않으면 이벤트는 `webhook.received`가 됩니다.
|
||||
|
||||
GitHub 같은 소스를 구성할 때는 content type을 `application/json`으로 설정하세요 — 폼 인코딩된 webhook payload는 허용되지 않습니다.
|
||||
|
||||
### 이벤트 필터
|
||||
|
||||
새 webhook 트리거는 인바운드 POST마다 발화합니다. 단일 용도 URL에는 괜찮지만, 여러 이벤트 타입을 팬아웃하는 소스(GitHub가 대표적입니다 — 단일 저장소 webhook 하나가 `push`, `pull_request`, `workflow_run`, `check_suite` 등을 전달할 수 있습니다)에는 시끄럽습니다. webhook 트리거의 **이벤트 필터(Event filters)** 섹션을 사용하면 실제로 실행을 디스패치할 이벤트를 제한할 수 있으며, 그 외의 모든 것은 `status = ignored`, `reason = event_filtered`로 전달 히스토리에 기록되고 실행이나 이슈는 생성되지 않습니다.
|
||||
|
||||
각 행은 하나의 규칙입니다. **이벤트 이름(event name)**과 선택적으로 쉼표로 구분한 **action** 목록으로 구성됩니다. Multica는 **어느 한** 행이라도 일치하면 webhook을 허용합니다. 섹션을 비워 두면 모든 것을 수락합니다(필터링 이전 동작).
|
||||
|
||||
예시:
|
||||
|
||||
| 이벤트 이름 | Actions | 일치 대상 |
|
||||
| -------------- | ------------------- | ------------------------------------------------------------------------ |
|
||||
| `workflow_run` | `completed, failed` | `action: completed` 또는 `action: failed`인 `workflow_run` 이벤트만 |
|
||||
| `workflow_run` | _(비어 있음)_ | action에 상관없이 모든 `workflow_run` 이벤트 |
|
||||
| `push` | _(비어 있음)_ | 모든 `push` 이벤트 |
|
||||
|
||||
#### 이벤트 이름과 action의 출처
|
||||
|
||||
Multica는 다음 순서로 인바운드 요청에서 `event` 이름과 `action`을 도출하며 — **첫 번째로 일치하는 것이 우선합니다**.
|
||||
|
||||
**1. 본문 봉투(Body envelope).** 본문이 문자열 `event` 필드를 가진 JSON 객체이면, 그 값이 곧 이벤트 이름입니다. 선택적인 `eventPayload` 객체는 자신의 `action` / `state` / `conclusion` / `status` 필드에서 action 후보를 제공합니다.
|
||||
|
||||
```bash
|
||||
curl -X POST "$MULTICA_WEBHOOK_URL" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"event":"trigger","eventPayload":{"action":"true"}}'
|
||||
# inferred: event = trigger, action candidate = true
|
||||
```
|
||||
|
||||
**2. 헤더(Headers).** 본문 봉투가 없을 때, Multica는 다음의 잘 알려진 제공자 헤더를 읽습니다.
|
||||
|
||||
- `X-GitHub-Event: <event>` — (있는 경우) 최상위 본문 `action` 필드와 결합해 `github.<event>.<action>`을 형성합니다.
|
||||
- `X-Gitlab-Event: <event>` — `gitlab.<event>`가 됩니다.
|
||||
- `X-Event-Type: <event>` — 그대로 통과합니다.
|
||||
|
||||
```bash
|
||||
# GitHub-style: header gives the event name, body gives the action.
|
||||
curl -X POST "$MULTICA_WEBHOOK_URL" \
|
||||
-H 'X-GitHub-Event: workflow_run' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"action":"completed"}'
|
||||
# inferred: event = github.workflow_run.completed
|
||||
# → matches a filter row of workflow_run / completed
|
||||
|
||||
# Generic event-type header — no body fields needed.
|
||||
curl -X POST "$MULTICA_WEBHOOK_URL" \
|
||||
-H 'X-Event-Type: trigger.true' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{}'
|
||||
# inferred: event = trigger.true → matches trigger / true
|
||||
```
|
||||
|
||||
**3. 본문 폴백(Body fallback).** 본문 봉투도 알려진 헤더도 없으면, Multica는 다음 순서로 최상위 본문 문자열 필드로 폴백합니다: `event` → `type` → `action`.
|
||||
|
||||
```bash
|
||||
curl -X POST "$MULTICA_WEBHOOK_URL" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"type":"trigger","action":"true"}'
|
||||
# inferred: event = trigger (from `type`), action candidate = true
|
||||
```
|
||||
|
||||
**4. 기본값(Default).** 위 어느 것도 일치하지 않으면, 이벤트는 `webhook.received`이고 action 후보는 없습니다.
|
||||
|
||||
**action 후보, 전체 목록.** 이벤트가 결정되면, Multica는 아래의 모든 값을 가능한 action 일치 대상으로 고려합니다.
|
||||
|
||||
- 이벤트가 `provider.event.<action>` 형태일 때의 이벤트 이름 접미사(예: `github.workflow_run.completed` → `completed`).
|
||||
- 본문 필드 `action`, `state`, `conclusion`, `status` — **JSON 문자열일 때만 해당됩니다**. 불리언(`{"action": true}`)이나 숫자는 자격이 없으므로, `event=trigger, action=true`를 기대하는 필터는 `{"trigger": true}` 본문과 절대 일치하지 않습니다. `true`는 문자열이 아니라 bool이기 때문입니다.
|
||||
|
||||
**흔한 함정.** `Event name: trigger` / `Actions: true` 같은 필터 행은 "본문에 `trigger: true`가 있으면 발화하라"는 뜻이 **아닙니다** — 이벤트 필터는 임의의 본문 필드가 아니라 *추론된 이벤트와 action*에 일치시킵니다. 이를 적중시키려면 `X-Event-Type`로 `trigger.true`를 보내거나(또는 위에 표시된 본문 봉투를 사용하세요). 저장된 필터 행에서 주변 공백(`" workflow_run "`)은 그대로 저장되어 절대 일치하지 않으므로 — 저장하기 전에 trim하세요.
|
||||
|
||||
#### 빠른 테스트
|
||||
|
||||
필터를 구성한 후에는 `curl`로 두 분기를 모두 확인할 수 있습니다.
|
||||
|
||||
```bash
|
||||
# Allowed — header drives event=workflow_run, body drives action=completed
|
||||
curl -X POST "$MULTICA_WEBHOOK_URL" \
|
||||
-H 'X-GitHub-Event: workflow_run' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"action":"completed"}'
|
||||
# → 200 {"status":"accepted", ...}
|
||||
|
||||
# Filtered — same event, action not in allowlist
|
||||
curl -X POST "$MULTICA_WEBHOOK_URL" \
|
||||
-H 'X-GitHub-Event: workflow_run' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"action":"in_progress"}'
|
||||
# → 200 {"status":"ignored","reason":"event_filtered"}
|
||||
```
|
||||
|
||||
### URL은 bearer secret입니다
|
||||
|
||||
생성된 URL **자체가** 자격 증명입니다. 그것을 가진 사람은 누구나 오토파일럿을 발화할 수 있습니다. 토큰처럼 다루세요.
|
||||
|
||||
- **공개된 이슈 스레드, 스크린샷, 채팅 히스토리에 붙여넣지 마세요.**
|
||||
- **유출되면 교체하세요** — 트리거 행에서 "Rotate URL"을 클릭하거나 `multica autopilot trigger-rotate-url <autopilot-id> <trigger-id>`를 실행하세요. 기존 URL은 즉시 작동을 멈춥니다.
|
||||
- 강력한 소스 인증이 필요한 소스의 경우, 트리거별 HMAC 서명 검증을 기다리세요. 이 v1 URL은 bearer 방식만 지원합니다.
|
||||
- 현재로서는 오토파일럿을 볼 수 있는 워크스페이스 멤버라면 그 webhook URL을 읽을 수 있습니다 — 역할별로 더 엄격한 secret 가시성은 후속 작업입니다.
|
||||
|
||||
### 상태 코드 의미
|
||||
|
||||
Multica는 정상적인 no-op 결과에 대해 `status` 필드와 함께 `200 OK`를 반환하므로, 제공자의 webhook 재시도 메커니즘이 URL을 계속 두드리지 않습니다.
|
||||
|
||||
- `{"status":"accepted","run_id":"…","autopilot_id":"…","trigger_id":"…"}` — 실행이 디스패치되었습니다.
|
||||
- `{"status":"skipped","run_id":"…","reason":"agent runtime is offline at dispatch time"}` — 수신 대상의 런타임이 오프라인이며, `skipped` 실행으로 기록됩니다.
|
||||
- `{"status":"ignored","reason":"trigger_disabled"}` — 트리거가 비활성화되어 있습니다.
|
||||
- `{"status":"ignored","reason":"autopilot_paused"}` — 오토파일럿이 일시 정지되어 있습니다.
|
||||
- `{"status":"ignored","reason":"autopilot_archived"}` — 오토파일럿이 보관되어 있습니다.
|
||||
|
||||
2xx가 아닌 응답은 실제 실패를 다룹니다.
|
||||
|
||||
- `400` — 유효하지 않은 JSON, 스칼라 본문, 빈 본문.
|
||||
- `404` — 알 수 없는 토큰(`{"error":"webhook not found"}`).
|
||||
- `413` — payload가 256 KiB를 초과했습니다.
|
||||
- `429` — 토큰별 속도 제한 초과(기본값 60 req/min).
|
||||
|
||||
### 자체 호스팅: 공개 URL 구성하기
|
||||
|
||||
서버에 `MULTICA_PUBLIC_URL`이 설정되어 있으면(예: `https://multica.example.com`), 트리거 응답에 절대 경로 `webhook_url`이 포함되고 UI에는 복사 가능한 URL이 바로 표시됩니다. 설정하지 않으면 UI는 클라이언트의 API origin으로부터 URL을 구성합니다 — 데스크톱과 동일 출처 웹에는 괜찮지만, 커스텀 자체 호스팅 리버스 프록시에는 적합하지 않습니다. Multica는 잘못 구성된 리버스 프록시가 공격자가 제어하는 호스트를 가리키는 webhook URL을 서버가 발행하도록 속이지 못하게, `Host` / `X-Forwarded-Host` 헤더로부터 공개 호스트를 도출하지 않도록 의도적으로 설계되었습니다.
|
||||
|
||||
## 실행 히스토리 보기
|
||||
|
||||
모든 트리거는 **실행 레코드(run record)**를 생성하며, 오토파일럿 상세 페이지의 "History" 탭에서 볼 수 있습니다.
|
||||
|
||||
- 트리거 소스(`schedule` / `manual` / `webhook`)
|
||||
- 시작 시각, 완료 시각
|
||||
- 상태(`issue_created` / `running` / `completed` / `failed` / `skipped`)
|
||||
- 연결된 이슈(이슈 생성 모드) 또는 `task`(실행 전용 모드)
|
||||
- 실패 사유(실패하거나 건너뛴 경우)
|
||||
|
||||
## 오토파일럿이 실패하면 어떻게 되나요
|
||||
|
||||
<Callout type="warning">
|
||||
**오토파일럿 실패는 자동으로 재시도되지 않으며 인박스 알림도 보내지 않습니다.** 실패는 실행 히스토리에 `failed` 항목을 남길 뿐 — 할당이나 @-멘션처럼 시스템 수준에서 다시 대기열에 넣는 일도 없고, 누구에게도 알림이 가지 않습니다. 오토파일럿이 주기적이라면 **다음 cron 발화가 새 실행을 트리거**하지만, 실패한 작업이 자동으로 다시 실행되지는 않습니다.
|
||||
|
||||
오토파일럿이 중요하다면 직접 모니터링을 설계하세요 — 예를 들어 에이전트가 성공 시 댓글을 남기게 하고, 댓글이 누락된 것을 알아채 실패를 잡아내는 식입니다.
|
||||
</Callout>
|
||||
|
||||
자동 재시도가 없는 이유: 오토파일럿은 이미 주기적이므로, 시스템 수준의 재시도를 추가하면 다음 예약된 실행 위에 겹쳐 중복 실행을 만들어 냅니다. 스케줄링을 전적으로 cron에 맡기면 깔끔하게 유지됩니다.
|
||||
|
||||
## 아직 제공되지 않는 기능
|
||||
|
||||
**API 종류의 트리거는 아직 연결되어 있지 않습니다.** 트리거 스키마는 `api` 종류를 예약해 두었지만, 그것을 발화하는 인그레스 라우트가 없습니다. UI는 기존 행에 Deprecated 배지를 표시하며 복사/교체 기능을 제공하지 않습니다. 트리거별 HMAC 서명 검증, IP 허용 목록, 제공자별 이벤트 프리셋은 후속 작업으로 추적되고 있으며, v1 URL은 bearer 방식만 지원합니다.
|
||||
|
||||
## 다음
|
||||
|
||||
- [**에이전트에게 이슈 할당하기**](/assigning-issues) — 이슈를 에이전트에게 일회성으로 넘기기
|
||||
- [**댓글에서 에이전트 @-멘션하기**](/mentioning-agents) — 댓글에서 에이전트를 불러 한번 살펴보게 하기
|
||||
- [**채팅**](/chat) — 이슈 밖에서의 일대일 대화
|
||||
@@ -1,63 +0,0 @@
|
||||
---
|
||||
title: チャット
|
||||
description: どのイシューにも属さない、エージェントとの一対一の会話 — 完全にサンドボックス化されています。エージェントはイシューを見たり変更したりできず、他の誰もこの会話を見ることはできません。
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
|
||||
**チャットはあなたと[エージェント](/agents)との一対一の会話です** — [イシュー](/issues)ボードから外に出るものです。エージェントはどのイシューも見られず、どのイシューも変更できず、会話全体は**完全に非公開**です([ワークスペース](/workspaces)内の他の誰も、admin を含めて、この会話を見ることはできません)。エージェントとアプローチを議論したり、ブレインストーミングをしたり、どのイシューにも属さない質問をしたりするのに適しています。
|
||||
|
||||
## エージェントを @-メンションするだけではだめなのですか?
|
||||
|
||||
[@-メンション](/mentioning-agents)はエージェントをイシューのコンテキスト**の中へ引き入れます** — エージェントはイシューの説明とすべての過去のコメントを読み、イシューを変更できます。チャットはこれを逆転させます。**あなたをイシューの外へ引き出します** — エージェントはこの単一の会話のみを見られ、どのイシューの存在も認識せず、イシューを変更する入口もありません。
|
||||
|
||||
2 つの判断基準:
|
||||
|
||||
- 特定のイシューのコンテキストに基づくフィードバックがほしいとき → [@-メンション](/mentioning-agents)
|
||||
- どのイシューとも無関係なトピックを議論したいとき(または他の誰にも議論を見られたくないとき) → チャット
|
||||
|
||||
## 会話を始める
|
||||
|
||||
サイドバーから**チャット**を開き、エージェントを選んで、新しい会話を始めてください。インターフェースはどのメッセージングアプリとも似ています。メッセージを送るとエージェントが返信します。各メッセージはバックグラウンドで実行をトリガーするため(キューに入れられた `task`)、返信には数秒かかることがあります。
|
||||
|
||||
## チャットでエージェントができることとできないこと
|
||||
|
||||
エージェントは会話の中で**完全にサンドボックス化された**モードで実行されます。
|
||||
|
||||
**できること:**
|
||||
|
||||
- 現在のメッセージに含まれる質問に答える
|
||||
- 構成された[スキル](/skills)と MCP を使う
|
||||
- 自身の作業ディレクトリでファイルを読み書きする
|
||||
- イシューコンテキストを必要としない `multica` CLI コマンドを呼び出す(例: 基本的なワークスペース情報の照会)
|
||||
|
||||
**できないこと:**
|
||||
|
||||
- **どのイシューも見ること** — エージェントが受け取るプロンプトにはイシュー ID がなく、`multica issue list` のようなコマンドは空の結果を返します
|
||||
- **どのイシューも変更すること** — イシューコンテキストがなければ、権限チェックによって API 呼び出しがブロックされます
|
||||
- **他の会話を見ること** — 会話は完全に隔離されています
|
||||
- **誰かや任意のエージェントを @-メンションすること** — チャットは他者に通知する経路のない非公開の空間です
|
||||
|
||||
## 複数ターンのコンテキストが保持される仕組み
|
||||
|
||||
チャットは**プロバイダーセッションの再開**を通じて複数ターンのコンテキストを維持します — エージェントは最初の返信でプロバイダーセッションを確立し(例: Claude セッション)、そのセッション ID が保存されます。次のメッセージでは、タスクのディスパッチがその ID を渡し直すため、エージェントは毎回履歴を読み直すことなく**中断したところから再開**します。
|
||||
|
||||
もし**1 つのターンが失敗した**場合、Multica はセッション ID を確立していた以前のタスク(そのタスクが成功したか失敗したかにかかわらず)を探し、再開を試みます — 途中で一度失敗したからといって、会話全体の記憶が失われることはありません。
|
||||
|
||||
注: すべてのプロバイダーが実際にセッション再開を実装しているわけではありません — サポート状況は[**プロバイダーマトリックス**](/providers)を参照してください。
|
||||
|
||||
## 会話をアーカイブする
|
||||
|
||||
もう見たくない会話はアーカイブできます — 会話一覧で右クリックするか、詳細ページの「アーカイブ」ボタンを使ってください。アーカイブ後は次のようになります。
|
||||
|
||||
- 会話がアクティブな一覧から消えます(「アーカイブ済み」ビューで引き続き見つけられます)
|
||||
- 過去のメッセージ、セッション ID、作業ディレクトリはすべて保持されます — 何も削除されません
|
||||
|
||||
<Callout type="warning">
|
||||
**アーカイブ後には「復元」ボタンがありません。** 現在、アーカイブされた会話を再びアクティブな状態に戻す入口はありません。後でそのスレッドを続けたい場合は、新しい会話を始める必要があります。アーカイブされた会話の内容を再び見るには、「アーカイブ済み」ビューを開いて履歴を読んでください。
|
||||
</Callout>
|
||||
|
||||
## 次へ
|
||||
|
||||
- [**オートパイロット**](/autopilots) — エージェントがスケジュールに従って自動的に作業を開始できるようにします
|
||||
- [**エージェントにイシューを割り当てる**](/assigning-issues) — トピックをイシューボードに戻します
|
||||
@@ -1,63 +0,0 @@
|
||||
---
|
||||
title: 채팅
|
||||
description: 어떤 이슈에도 속하지 않는, 에이전트와의 일대일 대화 — 완전히 샌드박스화되어 있습니다. 에이전트는 이슈를 보거나 변경할 수 없으며, 다른 누구도 이 대화를 볼 수 없습니다.
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
|
||||
**채팅은 당신과 [에이전트](/agents) 사이의 일대일 대화입니다** — [이슈](/issues) 보드에서 벗어나는 것입니다. 에이전트는 어떤 이슈도 보지 못하고 어떤 이슈도 변경할 수 없으며, 대화 전체가 **완전히 비공개**입니다([워크스페이스](/workspaces) 내의 다른 누구도, admin을 포함해서, 이 대화를 볼 수 없습니다). 에이전트와 접근 방식을 논의하거나, 브레인스토밍을 하거나, 어떤 이슈에도 속하지 않는 질문을 하기에 적합합니다.
|
||||
|
||||
## 그냥 에이전트를 @-멘션하면 안 되나요?
|
||||
|
||||
[@-멘션](/mentioning-agents)은 에이전트를 이슈의 컨텍스트 **안으로 끌어들입니다** — 에이전트는 이슈 설명과 모든 과거 댓글을 읽고, 이슈를 변경할 수 있습니다. 채팅은 이를 뒤집습니다. **당신을 이슈 밖으로 끌어냅니다** — 에이전트는 이 단일 대화만 볼 수 있고, 어떤 이슈의 존재도 인지하지 못하며, 이슈를 수정할 진입점도 없습니다.
|
||||
|
||||
두 가지 판단 기준:
|
||||
|
||||
- 특정 이슈의 컨텍스트에 기반한 피드백을 원할 때 → [@-멘션](/mentioning-agents)
|
||||
- 어떤 이슈와도 무관한 주제를 논의하고 싶을 때(또는 다른 누구에게도 논의를 보이고 싶지 않을 때) → 채팅
|
||||
|
||||
## 대화 시작하기
|
||||
|
||||
사이드바에서 **채팅**을 열고, 에이전트를 선택한 다음, 새 대화를 시작하세요. 인터페이스는 여느 메시징 앱과 비슷합니다. 메시지를 보내면 에이전트가 답장합니다. 각 메시지는 백그라운드에서 실행을 트리거하므로(대기열에 들어간 `task`), 답장에는 몇 초가 걸릴 수 있습니다.
|
||||
|
||||
## 채팅에서 에이전트가 할 수 있는 일과 할 수 없는 일
|
||||
|
||||
에이전트는 대화 안에서 **완전히 샌드박스화된** 모드로 실행됩니다.
|
||||
|
||||
**할 수 있는 일:**
|
||||
|
||||
- 현재 메시지에 담긴 질문에 답하기
|
||||
- 구성된 [스킬](/skills)과 MCP 사용하기
|
||||
- 자신의 작업 디렉터리에서 파일 읽기 및 쓰기
|
||||
- 이슈 컨텍스트가 필요 없는 `multica` CLI 명령 호출하기(예: 기본 워크스페이스 정보 조회)
|
||||
|
||||
**할 수 없는 일:**
|
||||
|
||||
- **어떤 이슈도 보기** — 에이전트가 받는 프롬프트에는 이슈 ID가 없으며, `multica issue list` 같은 명령은 빈 결과를 반환합니다
|
||||
- **어떤 이슈도 변경하기** — 이슈 컨텍스트가 없으면 권한 검사에 의해 API 호출이 차단됩니다
|
||||
- **다른 대화 보기** — 대화는 완전히 격리되어 있습니다
|
||||
- **누구도, 어떤 에이전트도 @-멘션하기** — 채팅은 다른 사람에게 알릴 경로가 없는 비공개 공간입니다
|
||||
|
||||
## 여러 턴에 걸친 컨텍스트가 보존되는 방식
|
||||
|
||||
채팅은 **제공자 세션 재개**를 통해 여러 턴에 걸친 컨텍스트를 유지합니다 — 에이전트는 첫 답장에서 제공자 세션을 설정하고(예: Claude 세션), 그 세션 ID가 저장됩니다. 다음 메시지에서는 작업 디스패치가 그 ID를 다시 전달하므로, 에이전트는 매번 기록을 다시 읽지 않고도 **중단했던 지점부터 이어서 재개**합니다.
|
||||
|
||||
만약 **한 턴이 실패하면**, Multica는 세션 ID를 설정했던 이전 작업(그 작업이 성공했든 실패했든)을 찾아 재개를 시도합니다 — 중간에 한 번 실패한다고 해서 대화 전체의 기억이 사라지지는 않습니다.
|
||||
|
||||
참고: 모든 제공자가 실제로 세션 재개를 구현하는 것은 아닙니다 — 지원 현황은 [**제공자 매트릭스**](/providers)를 참고하세요.
|
||||
|
||||
## 대화 보관하기
|
||||
|
||||
더 이상 보고 싶지 않은 대화는 보관할 수 있습니다 — 대화 목록에서 우클릭하거나 상세 페이지의 "보관" 버튼을 사용하세요. 보관 후에는:
|
||||
|
||||
- 대화가 활성 목록에서 사라집니다("보관됨" 보기에서 여전히 찾을 수 있습니다)
|
||||
- 과거 메시지, 세션 ID, 작업 디렉터리가 모두 보존됩니다 — 아무것도 삭제되지 않습니다
|
||||
|
||||
<Callout type="warning">
|
||||
**보관 후에는 "복원" 버튼이 없습니다.** 현재 보관된 대화를 다시 활성 상태로 되돌리는 진입점이 없습니다. 나중에 그 스레드를 계속 이어가고 싶다면 새 대화를 시작해야 합니다. 보관된 대화의 내용을 다시 보려면 "보관됨" 보기를 열고 기록을 읽어 보세요.
|
||||
</Callout>
|
||||
|
||||
## 다음
|
||||
|
||||
- [**오토파일럿**](/autopilots) — 에이전트가 일정에 따라 자동으로 작업을 시작하도록 하세요
|
||||
- [**에이전트에게 이슈 할당하기**](/assigning-issues) — 주제를 이슈 보드로 다시 가져오세요
|
||||
@@ -1,147 +0,0 @@
|
||||
---
|
||||
title: CLI コマンドリファレンス
|
||||
description: すべてのトップレベル Multica CLI コマンドを 1 ページにまとめた概要です。完全な使い方は `multica <command> --help` を実行してください。
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
|
||||
Multica CLI は、Web UI でできるほぼすべての操作をそのまま提供します([イシュー](/issues)の作成、[エージェント](/agents)の割り当て、[デーモン](/daemon-runtimes)の起動など)。このページでは、すべてのトップレベルコマンドを 1 行の説明とともに一覧します。フラグや例の完全な一覧は `multica <command> --help` を実行してください。
|
||||
|
||||
## 認証する
|
||||
|
||||
CLI を初めて使うときにこのコマンドを実行して、**パーソナルアクセストークン(PAT)**を取得します。
|
||||
|
||||
```bash
|
||||
multica login
|
||||
```
|
||||
|
||||
ブラウザが自動的に開きます。Web アプリで承認すると、CLI が PAT(`mul_` プレフィックス付き)を `~/.multica/config.json` に保存します。これ以降のすべてのコマンドはこの PAT で認証されます。
|
||||
|
||||
<Callout type="tip">
|
||||
CI やヘッドレス環境では、ブラウザフローをスキップできます。Web アプリの **Settings → Personal Access Tokens** で PAT を作成し、`multica login --token <mul_...>` で直接渡してください。
|
||||
</Callout>
|
||||
|
||||
トークンの種類による違いについては、[認証とトークン](/auth-tokens)を参照してください。
|
||||
|
||||
## 認証とセットアップ
|
||||
|
||||
| コマンド | 用途 |
|
||||
|---|---|
|
||||
| `multica login` | ログインして PAT を保存 |
|
||||
| `multica auth status` | 現在のログイン状態、ユーザー、ワークスペースを表示 |
|
||||
| `multica auth logout` | ローカルの PAT を削除 |
|
||||
| `multica setup cloud` | Multica Cloud のワンショットセットアップ(ログイン + デーモンのインストール) |
|
||||
| `multica setup self-host` | セルフホストバックエンドのワンショットセットアップ |
|
||||
|
||||
## ワークスペースとメンバー
|
||||
|
||||
| コマンド | 用途 |
|
||||
|---|---|
|
||||
| `multica workspace list` | アクセスできるすべてのワークスペースを一覧 |
|
||||
| `multica workspace get <slug>` | 1 つのワークスペースの詳細を表示 |
|
||||
| `multica workspace member list` | 現在のワークスペースのメンバーを一覧 |
|
||||
| `multica workspace update <id> --name "..." [--description "..."] [--context "..."] [--issue-prefix "..."]` | ワークスペースのメタデータを更新(admin/owner)。長いフィールドは `--description-stdin` / `--context-stdin` を使用できます。 |
|
||||
|
||||
## イシューとプロジェクト
|
||||
|
||||
<Callout type="info">
|
||||
`list` 系のコマンド(`multica issue list`、`autopilot list`、`project list` など)は、デフォルトで短く**そのままコピー&ペーストできる** ID を出力します。イシューは `MUL-123` のようなイシューキー、それ以外のリソースは短い UUID プレフィックスです。以下の後続コマンドの `<id>` 引数は短い ID と完全な UUID のどちらも受け取るため、一般的な流れは `multica issue list` → キーをコピー → `multica issue get MUL-123` となります。正式な UUID が必要なときは `list` コマンドに `--full-id` を渡してください。
|
||||
</Callout>
|
||||
|
||||
| コマンド | 用途 |
|
||||
|---|---|
|
||||
| `multica issue list` | イシューを一覧(コピー&ペーストできるイシューキーを出力) |
|
||||
| `multica issue get <id>` | 単一のイシューを表示(イシューキーまたは UUID を受け取る) |
|
||||
| `multica issue create --title "..."` | 新しいイシューを作成 |
|
||||
| `multica issue update <id> ...` | イシューを更新(ステータス、優先度、担当者など) |
|
||||
| `multica issue assign <id> --agent <slug>` | エージェントに割り当て(即座にタスクをトリガー) |
|
||||
| `multica issue status <id> --set <status>` | ステータス変更のショートカット |
|
||||
| `multica issue search <query>` | キーワード検索 |
|
||||
| `multica issue runs <id>` | イシュー上のエージェント実行を表示 |
|
||||
| `multica issue rerun <id>` | イシューの現在のエージェント担当者向けに新しいタスクを再キューイング |
|
||||
| `multica issue comment <id> ...` | ネスト: コメントの表示 / 投稿 |
|
||||
| `multica issue subscriber <id> ...` | ネスト: 購読 / 購読解除 |
|
||||
| `multica project list/get/create/update/delete/status` | プロジェクトの CRUD |
|
||||
|
||||
## エージェントとスキル
|
||||
|
||||
| コマンド | 用途 |
|
||||
|---|---|
|
||||
| `multica agent list` | ワークスペースのエージェントを一覧 |
|
||||
| `multica agent get <slug>` | エージェントの構成を表示 |
|
||||
| `multica agent create ...` | エージェントを作成 |
|
||||
| `multica agent update <slug> ...` | エージェントを更新 |
|
||||
| `multica agent archive <slug>` | アーカイブ |
|
||||
| `multica agent restore <slug>` | アーカイブ済みのエージェントを復元 |
|
||||
| `multica agent tasks <slug>` | エージェントのタスク履歴を表示 |
|
||||
| `multica agent skills ...` | ネスト: スキルのアタッチ / デタッチ |
|
||||
| `multica skill list/get/create/update/delete` | スキルの CRUD |
|
||||
| `multica skill import ...` | GitHub、ClawHub、またはローカルマシンからスキルをインポート |
|
||||
| `multica skill files ...` | ネスト: スキルのファイルを管理 |
|
||||
|
||||
## スクワッド
|
||||
|
||||
| コマンド | 用途 |
|
||||
|---|---|
|
||||
| `multica squad list` | ワークスペースのスクワッドを一覧 |
|
||||
| `multica squad get <id>` | 単一のスクワッドを表示 |
|
||||
| `multica squad create --name "..." --leader <agent>` | スクワッドを作成(owner / admin) |
|
||||
| `multica squad update <id> ...` | 名前、説明、指示、リーダー、またはアバターを更新 |
|
||||
| `multica squad delete <id>` | アーカイブ(ソフト削除) — 割り当て済みのイシューをリーダーに移管 |
|
||||
| `multica squad member list/add/remove <squad-id>` | スクワッドメンバーを管理 |
|
||||
| `multica squad activity <issue-id> <action\|no_action\|failed> --reason "..."` | スクワッドリーダーエージェントがターンごとに評価を記録するために使用 |
|
||||
|
||||
完全なモデルについては[スクワッド](/squads)を参照してください。
|
||||
|
||||
## オートパイロット
|
||||
|
||||
| コマンド | 用途 |
|
||||
|---|---|
|
||||
| `multica autopilot list` | ワークスペースのすべてのオートパイロットを一覧 |
|
||||
| `multica autopilot get <id>` | 単一のオートパイロットを表示 |
|
||||
| `multica autopilot create ...` | オートパイロットを作成 |
|
||||
| `multica autopilot update <id> ...` | 更新 |
|
||||
| `multica autopilot delete <id>` | 削除 |
|
||||
| `multica autopilot runs <id>` | 実行履歴を表示 |
|
||||
| `multica autopilot trigger <id>` | 手動で実行をトリガー |
|
||||
|
||||
## デーモンとランタイム
|
||||
|
||||
| コマンド | 用途 |
|
||||
|---|---|
|
||||
| `multica daemon start` | デーモンを起動(デフォルトはバックグラウンド。`--foreground` を追加するとフォアグラウンドで実行) |
|
||||
| `multica daemon stop` | デーモンを停止 |
|
||||
| `multica daemon restart` | デーモンを再起動 |
|
||||
| `multica daemon status` | デーモンがオンラインかどうかと同時実行数を確認 |
|
||||
| `multica daemon logs` | デーモンのログを表示 |
|
||||
| `multica runtime list` | 現在のワークスペースのランタイムを一覧 |
|
||||
| `multica runtime usage` | リソース使用量を表示 |
|
||||
| `multica runtime activity` | 最近のアクティビティログ |
|
||||
| `multica runtime update <id> ...` | ランタイムの構成を更新 |
|
||||
|
||||
## その他
|
||||
|
||||
| コマンド | 用途 |
|
||||
|---|---|
|
||||
| `multica repo checkout <url>` | エージェントが使用できるようにリポジトリをローカルにクローン |
|
||||
| `multica config` | ローカルの CLI 構成を表示または編集 |
|
||||
| `multica version` | CLI のバージョンを出力 |
|
||||
| `multica update` | CLI を最新のリリースにアップグレード |
|
||||
| `multica attachment download <id>` | イシューまたはコメントから添付ファイルをダウンロード |
|
||||
|
||||
## 完全なフラグを確認する
|
||||
|
||||
すべてのコマンドが `--help` をサポートしています。
|
||||
|
||||
```bash
|
||||
multica issue create --help
|
||||
multica agent update --help
|
||||
```
|
||||
|
||||
v2 では、各コマンドごとに専用の詳細なリファレンスページを提供する予定です。
|
||||
|
||||
## 次のステップ
|
||||
|
||||
- [認証とトークン](/auth-tokens) — PAT vs. JWT vs. デーモントークン
|
||||
- [デーモンとランタイム](/daemon-runtimes) — `daemon` コマンドが内部でどう動作するか
|
||||
- [エージェントの作成と構成](/agents-create) — `multica agent create` のすべてのオプション
|
||||
@@ -1,147 +0,0 @@
|
||||
---
|
||||
title: CLI 명령어 레퍼런스
|
||||
description: "모든 최상위 Multica CLI 명령어를 한 페이지로 정리한 개요입니다. 전체 사용법은 `multica <command> --help`를 실행하세요."
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
|
||||
Multica CLI는 Web UI에서 할 수 있는 거의 모든 작업을 그대로 제공합니다([이슈](/issues) 생성, [에이전트](/agents) 할당, [데몬](/daemon-runtimes) 시작 등). 이 페이지는 모든 최상위 명령어를 한 줄 설명과 함께 나열합니다. 전체 플래그와 예제는 `multica <command> --help`를 실행하세요.
|
||||
|
||||
## 인증하기
|
||||
|
||||
CLI를 처음 사용할 때 이 명령을 실행해 **개인 액세스 토큰(personal access token, PAT)**을 발급받으세요:
|
||||
|
||||
```bash
|
||||
multica login
|
||||
```
|
||||
|
||||
브라우저가 자동으로 열립니다. 웹 앱에서 승인하면 CLI가 PAT(`mul_` 접두사)를 `~/.multica/config.json`에 저장합니다. 이후 모든 명령은 이 PAT로 인증됩니다.
|
||||
|
||||
<Callout type="tip">
|
||||
CI나 headless 환경에서는 브라우저 플로우를 건너뛰세요. 웹 앱의 **설정 → API 토큰**에서 PAT를 만든 뒤 `multica login --token <mul_...>`로 직접 전달하면 됩니다.
|
||||
</Callout>
|
||||
|
||||
토큰 유형 간의 차이는 [인증과 토큰](/auth-tokens)을 참고하세요.
|
||||
|
||||
## 인증과 설정
|
||||
|
||||
| 명령어 | 용도 |
|
||||
|---|---|
|
||||
| `multica login` | 로그인하고 PAT 저장 |
|
||||
| `multica auth status` | 현재 로그인 상태, 사용자, 워크스페이스 표시 |
|
||||
| `multica auth logout` | 로컬 PAT 삭제 |
|
||||
| `multica setup cloud` | Multica Cloud용 원샷 설정(로그인 + 데몬 설치) |
|
||||
| `multica setup self-host` | 자체 호스팅 백엔드용 원샷 설정 |
|
||||
|
||||
## 워크스페이스와 멤버
|
||||
|
||||
| 명령어 | 용도 |
|
||||
|---|---|
|
||||
| `multica workspace list` | 접근할 수 있는 모든 워크스페이스 나열 |
|
||||
| `multica workspace get <slug>` | 워크스페이스 하나의 상세 정보 표시 |
|
||||
| `multica workspace member list` | 현재 워크스페이스의 멤버 나열 |
|
||||
| `multica workspace update <id> --name "..." [--description "..."] [--context "..."] [--issue-prefix "..."]` | 워크스페이스 메타데이터 업데이트(admin/owner). 긴 필드는 `--description-stdin` / `--context-stdin`을 사용할 수 있습니다. |
|
||||
|
||||
## 이슈와 프로젝트
|
||||
|
||||
<Callout type="info">
|
||||
`list` 계열 명령(`multica issue list`, `autopilot list`, `project list` 등)은 기본적으로 짧고 **바로 복사해 붙여 넣을 수 있는** ID를 출력합니다. 이슈는 `MUL-123` 같은 이슈 키이고, 나머지 리소스는 짧은 UUID 접두사입니다. 아래 후속 명령의 `<id>` 인자는 짧은 ID와 전체 UUID를 모두 받으므로, 일반적인 흐름은 `multica issue list` → 키 복사 → `multica issue get MUL-123`입니다. 정식 UUID가 필요할 때는 `list` 명령에 `--full-id`를 전달하세요.
|
||||
</Callout>
|
||||
|
||||
| 명령어 | 용도 |
|
||||
|---|---|
|
||||
| `multica issue list` | 이슈 나열(복사해 붙여 넣을 수 있는 이슈 키 출력) |
|
||||
| `multica issue get <id>` | 단일 이슈 표시(이슈 키 또는 UUID를 받음) |
|
||||
| `multica issue create --title "..."` | 새 이슈 생성 |
|
||||
| `multica issue update <id> ...` | 이슈 업데이트(상태, 우선순위, 담당자 등) |
|
||||
| `multica issue assign <id> --agent <slug>` | 에이전트에게 할당(즉시 작업을 트리거) |
|
||||
| `multica issue status <id> --set <status>` | 상태 변경 단축 명령 |
|
||||
| `multica issue search <query>` | 키워드 검색 |
|
||||
| `multica issue runs <id>` | 이슈의 에이전트 실행 표시 |
|
||||
| `multica issue rerun <id>` | 이슈의 현재 에이전트 담당자에게 새 작업을 다시 큐에 넣기 |
|
||||
| `multica issue comment <id> ...` | 중첩: 댓글 보기 / 작성 |
|
||||
| `multica issue subscriber <id> ...` | 중첩: 구독 / 구독 취소 |
|
||||
| `multica project list/get/create/update/delete/status` | 프로젝트 CRUD |
|
||||
|
||||
## 에이전트와 스킬
|
||||
|
||||
| 명령어 | 용도 |
|
||||
|---|---|
|
||||
| `multica agent list` | 워크스페이스의 에이전트 나열 |
|
||||
| `multica agent get <slug>` | 에이전트의 구성 표시 |
|
||||
| `multica agent create ...` | 에이전트 생성 |
|
||||
| `multica agent update <slug> ...` | 에이전트 업데이트 |
|
||||
| `multica agent archive <slug>` | 보관 |
|
||||
| `multica agent restore <slug>` | 보관된 에이전트 복원 |
|
||||
| `multica agent tasks <slug>` | 에이전트의 작업 기록 표시 |
|
||||
| `multica agent skills ...` | 중첩: 스킬 연결 / 분리 |
|
||||
| `multica skill list/get/create/update/delete` | 스킬 CRUD |
|
||||
| `multica skill import ...` | GitHub, ClawHub, 또는 로컬 기기에서 스킬 가져오기 |
|
||||
| `multica skill files ...` | 중첩: 스킬의 파일 관리 |
|
||||
|
||||
## 스쿼드
|
||||
|
||||
| 명령어 | 용도 |
|
||||
|---|---|
|
||||
| `multica squad list` | 워크스페이스의 스쿼드 나열 |
|
||||
| `multica squad get <id>` | 단일 스쿼드 표시 |
|
||||
| `multica squad create --name "..." --leader <agent>` | 스쿼드 생성(owner / admin) |
|
||||
| `multica squad update <id> ...` | 이름, 설명, 지침, 리더, 또는 아바타 업데이트 |
|
||||
| `multica squad delete <id>` | 보관(소프트 삭제) — 할당된 이슈를 리더에게 이관 |
|
||||
| `multica squad member list/add/remove/set-role <squad-id>` | 스쿼드 멤버 관리 및 역할 직접 업데이트 |
|
||||
| `multica squad activity <issue-id> <action\|no_action\|failed> --reason "..."` | 스쿼드 리더 에이전트가 매 턴마다 평가를 기록할 때 사용 |
|
||||
|
||||
전체 모델은 [스쿼드](/squads)를 참고하세요.
|
||||
|
||||
## 오토파일럿
|
||||
|
||||
| 명령어 | 용도 |
|
||||
|---|---|
|
||||
| `multica autopilot list` | 워크스페이스의 모든 오토파일럿 나열 |
|
||||
| `multica autopilot get <id>` | 단일 오토파일럿 표시 |
|
||||
| `multica autopilot create ...` | 오토파일럿 생성 |
|
||||
| `multica autopilot update <id> ...` | 업데이트 |
|
||||
| `multica autopilot delete <id>` | 삭제 |
|
||||
| `multica autopilot runs <id>` | 실행 기록 표시 |
|
||||
| `multica autopilot trigger <id>` | 수동으로 실행 트리거 |
|
||||
|
||||
## 데몬과 런타임
|
||||
|
||||
| 명령어 | 용도 |
|
||||
|---|---|
|
||||
| `multica daemon start` | 데몬 시작(기본은 백그라운드; `--foreground`를 추가하면 포그라운드 실행) |
|
||||
| `multica daemon stop` | 데몬 중지 |
|
||||
| `multica daemon restart` | 데몬 재시작 |
|
||||
| `multica daemon status` | 데몬의 온라인 여부와 동시 실행 수 확인 |
|
||||
| `multica daemon logs` | 데몬 로그 보기 |
|
||||
| `multica runtime list` | 현재 워크스페이스의 런타임 나열 |
|
||||
| `multica runtime usage` | 리소스 사용량 표시 |
|
||||
| `multica runtime activity` | 최근 활동 로그 |
|
||||
| `multica runtime update <id> ...` | 런타임의 구성 업데이트 |
|
||||
|
||||
## 기타
|
||||
|
||||
| 명령어 | 용도 |
|
||||
|---|---|
|
||||
| `multica repo checkout <url>` | 에이전트가 사용할 수 있도록 리포지토리를 로컬에 클론 |
|
||||
| `multica config` | 로컬 CLI 구성 보기 또는 편집 |
|
||||
| `multica version` | CLI 버전 출력 |
|
||||
| `multica update` | CLI를 최신 릴리스로 업그레이드 |
|
||||
| `multica attachment download <id>` | 이슈나 댓글에서 첨부 파일 다운로드 |
|
||||
|
||||
## 전체 플래그 확인하기
|
||||
|
||||
모든 명령은 `--help`를 지원합니다:
|
||||
|
||||
```bash
|
||||
multica issue create --help
|
||||
multica agent update --help
|
||||
```
|
||||
|
||||
v2에서는 각 명령마다 전용 상세 레퍼런스 페이지를 제공할 예정입니다.
|
||||
|
||||
## 다음 단계
|
||||
|
||||
- [인증과 토큰](/auth-tokens) — PAT vs. JWT vs. 데몬 토큰
|
||||
- [데몬과 런타임](/daemon-runtimes) — `daemon` 명령이 내부적으로 동작하는 방식
|
||||
- [에이전트 생성과 구성](/agents-create) — `multica agent create`의 모든 옵션
|
||||
@@ -88,7 +88,7 @@ For the difference between token types, see [Authentication and tokens](/auth-to
|
||||
| `multica squad create --name "..." --leader <agent>` | Create a squad (owner / admin) |
|
||||
| `multica squad update <id> ...` | Update name, description, instructions, leader, or avatar |
|
||||
| `multica squad delete <id>` | Archive (soft-delete) — transfers assigned issues to the leader |
|
||||
| `multica squad member list/add/remove/set-role <squad-id>` | Manage squad members and update roles in place |
|
||||
| `multica squad member list/add/remove <squad-id>` | Manage squad members |
|
||||
| `multica squad activity <issue-id> <action\|no_action\|failed> --reason "..."` | Used by squad leader agents to record an evaluation per turn |
|
||||
|
||||
See [Squads](/squads) for the full model.
|
||||
|
||||
@@ -88,7 +88,7 @@ Token 类型的详细区分见 [认证与令牌](/auth-tokens)。
|
||||
| `multica squad create --name "..." --leader <agent>` | 创建小队(owner / admin)|
|
||||
| `multica squad update <id> ...` | 修改名字、描述、instructions、队长、头像 |
|
||||
| `multica squad delete <id>` | 归档(软删除)—— 同时把分配给小队的 issue 转给队长 |
|
||||
| `multica squad member list/add/remove/set-role <squad-id>` | 管理小队成员并原地更新 role |
|
||||
| `multica squad member list/add/remove <squad-id>` | 管理小队成员 |
|
||||
| `multica squad activity <issue-id> <action\|no_action\|failed> --reason "..."` | 队长智能体每轮结束时调用,记录 evaluation |
|
||||
|
||||
完整模型见 [小队](/squads)。
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
---
|
||||
title: Cloud クイックスタート
|
||||
description: サインアップからエージェントへの最初のタスク割り当てまで 5 分で。
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
|
||||
このページは Multica Cloud を最初から最後まで案内します — **サインアップ → [CLI](/cli) のインストール → [デーモン](/daemon-runtimes)の起動 → [エージェント](/agents)の作成 → 最初の[タスク](/tasks)の割り当て**。約 5 分かかります。
|
||||
|
||||
前提条件は 1 つだけです: ローカルに [AI コーディングツール](/providers)([Antigravity](/providers#antigravity)、[Claude Code](/providers#claude-code)、[Codex](/providers#codex)、[Cursor](/providers#cursor)、[Copilot](/providers#copilot)、[Gemini](/providers#gemini)、[Hermes](/providers#hermes)、[Kimi](/providers#kimi)、[Kiro CLI](/providers#kiro-cli)、[OpenCode](/providers#opencode)、[OpenClaw](/providers#openclaw)、[Pi](/providers#pi) のいずれか)を少なくとも 1 つ、すでにインストールしておくこと。デーモンは起動時にこれらを自動検出し、1 つもなければ起動を拒否します。
|
||||
|
||||
## 1. アカウントを作成する
|
||||
|
||||
[multica.ai](https://multica.ai) でサインアップしてください。メール(6 桁の確認コード)または Google でログインできます。
|
||||
|
||||
サインアップ後は(アカウント名から生成された)デフォルトのワークスペースに自動的に配置されます。後で名前を変更したり、新しいワークスペースを作成したりできます。
|
||||
|
||||
## 2. Multica CLI をインストールする
|
||||
|
||||
**macOS / Linux(Homebrew 推奨)**:
|
||||
|
||||
```bash
|
||||
brew install multica-ai/tap/multica
|
||||
```
|
||||
|
||||
**macOS / Linux(Homebrew なし)**:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.sh | bash
|
||||
```
|
||||
|
||||
**Windows(PowerShell)**:
|
||||
|
||||
```powershell
|
||||
irm https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.ps1 | iex
|
||||
```
|
||||
|
||||
インストールを確認します。
|
||||
|
||||
```bash
|
||||
multica version
|
||||
```
|
||||
|
||||
## 3. ログイン + デーモンの起動
|
||||
|
||||
コマンド 1 つでログインとデーモンの起動を処理します。
|
||||
|
||||
```bash
|
||||
multica setup
|
||||
```
|
||||
|
||||
`multica setup` は次を実行します。
|
||||
|
||||
1. CLI が Multica Cloud に接続するよう構成します
|
||||
2. ログインのためにブラウザを開きます(Web と同じメール確認コード / Google OAuth)
|
||||
3. 生成された PAT を `~/.multica/config.json` に保存します
|
||||
4. **デーモンを自動的に起動します** — 3 秒ごとにタスクをポーリングし、15 秒ごとにハートビートを送信し始めます
|
||||
|
||||
<Callout type="info">
|
||||
**デスクトップアプリを使用していますか?** デスクトップアプリは起動時に**デーモンを自動的に起動します** — `multica setup` を手動で実行する必要はありません。[デスクトップアプリ](/desktop-app)を参照してください。
|
||||
</Callout>
|
||||
|
||||
デーモンが実行中かどうかを確認します。
|
||||
|
||||
```bash
|
||||
multica daemon status
|
||||
```
|
||||
|
||||
`online` はサーバーに登録されたことを意味します。
|
||||
|
||||
## 4. ランタイムがオンラインか確認する
|
||||
|
||||
Web UI で **Settings → Runtimes** に移動します。先ほど起動したデーモンが、1 つ以上のアクティブなランタイムとして表示されるはずです — ローカルにインストールされた AI コーディングツールごとに 1 つです。
|
||||
|
||||
オフラインと表示されても慌てないでください — [トラブルシューティング → デーモンがサーバーに接続できない](/troubleshooting#daemon-cant-connect-to-the-server)を参照してください。
|
||||
|
||||
## 5. エージェントを作成する
|
||||
|
||||
Web UI で **Settings → Agents** に移動し、**New Agent** をクリックします。
|
||||
|
||||
- **Name** — ボードやコメントでこのエージェントに表示される名前です。好きな名前を選んでください
|
||||
- **Provider** — ローカルにインストールした AI コーディングツールを選択します(ドロップダウンにはランタイムで検出されたツールのみが表示されます)
|
||||
- **Model**(任意) — そのツール内部のモデル選択(プロバイダーによって静的な一覧または動的探索)
|
||||
- **Instructions**(任意) — このエージェントのためのシステムプロンプト
|
||||
|
||||
作成されると、エージェントはワークスペースのメンバー一覧に表示され、人間のメンバーと同じように作業を割り当てられます。
|
||||
|
||||
## 6. 最初のタスクを割り当てる
|
||||
|
||||
Web UI でイシューを作成するか、CLI から作成します。
|
||||
|
||||
```bash
|
||||
multica issue create --title "Add an ASCII architecture diagram to the README"
|
||||
```
|
||||
|
||||
先ほど作成したエージェントにイシューを割り当てます — Web UI でアバターをクリックするか、CLI を使用します。
|
||||
|
||||
```bash
|
||||
multica issue assign MUL-1 --to my-agent-name
|
||||
```
|
||||
|
||||
`--to` はエージェントまたはメンバーの**名前**を受け取ります。部分文字列の一致も機能します — エージェント名が `my-code-reviewer` なら、`reviewer` でそれに解決されます。ワークスペースに名前が重複している場合は、代わりに `--to-id <uuid>`(`--to` と相互排他)を渡してください。UUID は `multica agent list --output json` または `multica workspace member list --output json` で調べられます。
|
||||
|
||||
**次にデーモンで起きること**:
|
||||
|
||||
1. 3 秒以内にタスクを取得します(ステータスが `queued` から `dispatched` に変わります)
|
||||
2. 一致する AI コーディングツールを呼び出して作業を開始します(ステータスが `running` になります)
|
||||
3. AI がローカルで作業します — コードディレクトリを読んだり、コマンドを実行したり、ファイルを編集したりできます
|
||||
4. 完了すると結果を Multica に報告します(自動リトライが作動するかどうかに応じて、ステータスが `completed` または `failed` になります)
|
||||
|
||||
Web UI は**リアルタイムで**(WebSocket を通じて)更新されます — 再読み込みは不要です。
|
||||
|
||||
## 次のステップ
|
||||
|
||||
- [デーモンとランタイム](/daemon-runtimes) — デーモンがどう動作するかとランタイムの意味
|
||||
- [タスク](/tasks) — タスクのライフサイクルとリトライルール
|
||||
- [AI コーディングツール比較](/providers) — 12 個のツール間の機能差
|
||||
- [デスクトップアプリ](/desktop-app) — デーモンを自分で実行したくない場合
|
||||
- [セルフホストクイックスタート](/self-host-quickstart) — 自前のバックエンドを実行する
|
||||
@@ -1,119 +0,0 @@
|
||||
---
|
||||
title: Cloud 빠른 시작
|
||||
description: 가입부터 에이전트에게 첫 작업을 할당하기까지 5분 만에.
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
|
||||
이 페이지는 Multica Cloud를 처음부터 끝까지 안내합니다 — **가입 → [CLI](/cli) 설치 → [데몬](/daemon-runtimes) 시작 → [에이전트](/agents) 생성 → 첫 [작업](/tasks) 할당**. 약 5분이 걸립니다.
|
||||
|
||||
전제 조건은 하나뿐입니다: 로컬에 [AI 코딩 도구](/providers)([Antigravity](/providers#antigravity), [Claude Code](/providers#claude-code), [Codex](/providers#codex), [Cursor](/providers#cursor), [Copilot](/providers#copilot), [Gemini](/providers#gemini), [Hermes](/providers#hermes), [Kimi](/providers#kimi), [Kiro CLI](/providers#kiro-cli), [OpenCode](/providers#opencode), [OpenClaw](/providers#openclaw), [Pi](/providers#pi) 중 하나)를 이미 최소 하나는 설치해 두어야 합니다. 데몬은 시작할 때 이들을 자동으로 감지하며, 하나도 없으면 시작을 거부합니다.
|
||||
|
||||
## 1. 계정 만들기
|
||||
|
||||
[multica.ai](https://multica.ai)에서 가입하세요. 이메일(6자리 인증 코드) 또는 Google로 로그인할 수 있습니다.
|
||||
|
||||
가입 후에는 (계정 이름으로 생성된) 기본 워크스페이스에 자동으로 배치됩니다. 나중에 이름을 변경하거나 새 워크스페이스를 만들 수 있습니다.
|
||||
|
||||
## 2. Multica CLI 설치하기
|
||||
|
||||
**macOS / Linux (Homebrew 권장)**:
|
||||
|
||||
```bash
|
||||
brew install multica-ai/tap/multica
|
||||
```
|
||||
|
||||
**macOS / Linux (Homebrew 없이)**:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.sh | bash
|
||||
```
|
||||
|
||||
**Windows (PowerShell)**:
|
||||
|
||||
```powershell
|
||||
irm https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.ps1 | iex
|
||||
```
|
||||
|
||||
설치를 확인하세요:
|
||||
|
||||
```bash
|
||||
multica version
|
||||
```
|
||||
|
||||
## 3. 로그인 + 데몬 시작하기
|
||||
|
||||
명령어 하나로 로그인과 데몬 시작을 처리합니다:
|
||||
|
||||
```bash
|
||||
multica setup
|
||||
```
|
||||
|
||||
`multica setup`은 다음을 수행합니다:
|
||||
|
||||
1. CLI가 Multica Cloud에 연결하도록 구성합니다
|
||||
2. 로그인을 위해 브라우저를 엽니다(웹과 동일한 이메일 인증 코드 / Google OAuth)
|
||||
3. 생성된 PAT를 `~/.multica/config.json`에 저장합니다
|
||||
4. **데몬을 자동으로 시작합니다** — 3초마다 작업을 폴링하고 15초마다 하트비트를 전송하기 시작합니다
|
||||
|
||||
<Callout type="info">
|
||||
**데스크톱 앱을 사용 중이신가요?** 데스크톱 앱은 실행 시 **데몬을 자동으로 시작합니다** — `multica setup`을 직접 실행할 필요가 없습니다. [데스크톱 앱](/desktop-app)을 참고하세요.
|
||||
</Callout>
|
||||
|
||||
데몬이 실행 중인지 확인하세요:
|
||||
|
||||
```bash
|
||||
multica daemon status
|
||||
```
|
||||
|
||||
`online`은 서버에 등록되었음을 의미합니다.
|
||||
|
||||
## 4. 런타임이 온라인인지 확인하기
|
||||
|
||||
웹 UI에서 **설정 → 런타임**으로 이동하세요. 방금 시작한 데몬이 하나 이상의 활성 런타임으로 표시되어야 합니다 — 로컬에 설치된 AI 코딩 도구당 하나씩입니다.
|
||||
|
||||
오프라인으로 표시되더라도 당황하지 마세요 — [문제 해결 → 데몬이 서버에 연결할 수 없음](/troubleshooting#daemon-cant-connect-to-the-server)을 참고하세요.
|
||||
|
||||
## 5. 에이전트 생성하기
|
||||
|
||||
웹 UI에서 **설정 → 에이전트**로 이동하여 **새 에이전트**를 클릭하세요:
|
||||
|
||||
- **이름** — 보드와 댓글에서 이 에이전트에 표시되는 이름입니다. 원하는 이름을 고르세요
|
||||
- **제공자** — 로컬에 설치한 AI 코딩 도구를 선택하세요(드롭다운에는 런타임에서 감지된 도구만 나열됩니다)
|
||||
- **모델**(선택) — 해당 도구 내부의 모델 선택(제공자에 따라 정적 목록 또는 동적 발견)
|
||||
- **지침**(선택) — 이 에이전트를 위한 시스템 프롬프트
|
||||
|
||||
생성되면 에이전트는 워크스페이스 멤버 목록에 나타나며, 사람 멤버처럼 작업을 할당할 수 있습니다.
|
||||
|
||||
## 6. 첫 작업 할당하기
|
||||
|
||||
웹 UI에서 이슈를 만들거나, CLI에서 만드세요:
|
||||
|
||||
```bash
|
||||
multica issue create --title "Add an ASCII architecture diagram to the README"
|
||||
```
|
||||
|
||||
방금 만든 에이전트에게 이슈를 할당하세요 — 웹 UI에서 아바타를 클릭하거나, CLI를 사용하세요:
|
||||
|
||||
```bash
|
||||
multica issue assign MUL-1 --to my-agent-name
|
||||
```
|
||||
|
||||
`--to`는 에이전트 또는 멤버의 **이름**을 받습니다. 부분 문자열 일치도 동작합니다 — 에이전트 이름이 `my-code-reviewer`라면 `reviewer`로 해석됩니다. 워크스페이스에 이름이 겹치는 경우, 대신 `--to-id <uuid>`(`--to`와 상호 배타적)를 전달하세요. UUID는 `multica agent list --output json` 또는 `multica workspace member list --output json`으로 조회하세요.
|
||||
|
||||
**다음으로 데몬에서 일어나는 일**:
|
||||
|
||||
1. 3초 이내에 작업을 가져갑니다(상태가 `queued`에서 `dispatched`로 바뀝니다)
|
||||
2. 일치하는 AI 코딩 도구를 호출하여 작업을 시작합니다(상태가 `running`이 됩니다)
|
||||
3. AI가 로컬에서 작업합니다 — 코드 디렉터리를 읽고, 명령을 실행하고, 파일을 편집할 수 있습니다
|
||||
4. 완료되면 결과를 Multica로 다시 보고합니다(자동 재시도 동작 여부에 따라 상태가 `completed` 또는 `failed`가 됩니다)
|
||||
|
||||
웹 UI는 **실시간으로**(WebSocket을 통해) 업데이트됩니다 — 새로 고침이 필요 없습니다.
|
||||
|
||||
## 다음 단계
|
||||
|
||||
- [데몬과 런타임](/daemon-runtimes) — 데몬이 어떻게 동작하는지와 런타임의 의미
|
||||
- [작업](/tasks) — 작업 생명주기와 재시도 규칙
|
||||
- [AI 코딩 도구 비교](/providers) — 12개 도구 간의 기능 차이
|
||||
- [데스크톱 앱](/desktop-app) — 데몬을 직접 실행하고 싶지 않은 경우
|
||||
- [자체 호스팅 빠른 시작](/self-host-quickstart) — 자체 백엔드 실행하기
|
||||
@@ -1,81 +0,0 @@
|
||||
---
|
||||
title: コメントとメンション
|
||||
description: イシューの下での共同作業 — コメント、返信、`@` メンション、リアクション、そしてコメントからエージェントをトリガーする方法。
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
|
||||
すべての[イシュー](/issues)にはコメントスレッドがあります。コメントを投稿し、誰かに返信し、[メンバー](/members-roles)や[エージェント](/agents)を `@` でメンションし、リアクションを追加する — これまで使ってきたどのタスク管理ツールでも行ってきたのと同じ操作です。唯一の違いは、**`@` でエージェントをメンションすると、そのエージェントが作業を開始するようトリガーされる**ことです。
|
||||
|
||||
## コメントを投稿する
|
||||
|
||||
イシュー詳細ページ下部の入力欄に内容を入力し、**送信**を押してください。コメントはすぐにスレッドに表示されます。コメントは Markdown に対応しています — 見出し、リスト、コードブロック、リンクがすべて使えます。
|
||||
|
||||
## コメントに返信する
|
||||
|
||||
任意のコメントの右上にある**返信**をクリックすると、その下にネストされた入力欄が開きます。返信はそのコメントの子要素として表示され、会話スレッドを形成します。返信にもさらに返信を付けられ、必要なだけ深くネストできます。
|
||||
|
||||
イシュー一覧にはトップレベルのコメント数だけが表示され、イシューを開くと会話ツリー全体が見えます。
|
||||
|
||||
## リアクション
|
||||
|
||||
各コメントの右上には、素早く意思を伝えるためのリアクションボタンがあります(👍、👀、🎉)— 同意を示すために「+1」コメントをわざわざ投稿する必要はありません。
|
||||
|
||||
## `@` メンション
|
||||
|
||||
コメントに `@` を入力するとピッカーが開きます。メンバーまたはエージェントを選ぶと、`@` と対象のスラッグが挿入されます(`@alice` や `@reviewer-bot`)。メンションされた相手は自分の[インボックス](/inbox)に通知を受け取ります。
|
||||
|
||||
**エージェントをメンションすると自動的にトリガーされます** — [コメントでエージェントをメンションする](/mentioning-agents)を参照してください。
|
||||
|
||||
1 つのコメントで同じ人を複数回メンションしても、通知は**1 つだけ**発生します。
|
||||
|
||||
### `@all` はワークスペース全体に通知する
|
||||
|
||||
`@all` は特別な対象です。ワークスペースのすべてのメンバーに通知を送ります。人もエージェントも `@all` を使えます — つまり進捗を報告するエージェントも `@all` できるので、エージェントの指示には控えめに使うよう伝えておきましょう。
|
||||
|
||||
<Callout type="warning">
|
||||
**`@all` は慎重に使ってください。** 規模の大きいワークスペースでは、たった 1 回の `@all` がその人数分のインボックス通知を瞬時に生成します。全員が本当に知る必要があることだけに使い、日常的な更新には使わないでください。
|
||||
</Callout>
|
||||
|
||||
## イシューを参照する
|
||||
|
||||
別のイシューをリンクするには、`MUL-123` のようにそのイシューキーを入力してください。Multica はコメント内で実在するイシューキーを解決し、内部的に `mention://issue/<uuid>` リンクとして保存します。イシューリンクは単なる相互参照にすぎません。人に通知を送ることはなく、エージェントをトリガーすることもありません。
|
||||
|
||||
通常は `[MUL-123](mention://issue/<uuid>)` を手で書く必要はありません。その形式は、Multica がキーを解決した後に使う標準的な内部表現です。
|
||||
|
||||
<Callout type="info">
|
||||
Markdown の強調は CommonMark のルールに従います。太字テキストが句読点や閉じ引用符で終わり、その直後に韓国語の助詞が続く場合、閉じの `**` が認識されないことがあります。
|
||||
|
||||
引用符を太字の範囲の外に出すことをおすすめします。
|
||||
|
||||
```markdown
|
||||
"**무엇을 먼저 정해두고 시작할지**"가
|
||||
```
|
||||
|
||||
次の代わりに:
|
||||
|
||||
```markdown
|
||||
**"무엇을 먼저 정해두고 시작할지"**가
|
||||
```
|
||||
</Callout>
|
||||
|
||||
## コメントの編集と削除
|
||||
|
||||
コメントは作成者のみが編集または削除できます。
|
||||
|
||||
コメントを削除すると、その下の**すべての返信も一緒に削除されます**(返信への返信も含む)。内容だけを変えたい場合は、削除ではなく編集を使ってください。
|
||||
|
||||
<Callout type="warning">
|
||||
**コメントを編集して `@` を追加しても、エージェントはトリガーされません。** トリガーはコメントが**作成された**その瞬間に発生します — 後から編集して新しい `@` を追加したり、対象を変えたりしても、新しい通知は送られず、エージェントも起きません。見逃したエージェントを呼び出すには、そのエージェントを `@` する**新しいコメントを投稿**してください。
|
||||
</Callout>
|
||||
|
||||
---
|
||||
|
||||
ここまで扱ってきた内容はすべて「人の世界」です — ワークスペース、メンバー、イシュー、プロジェクト、コメント。Linear や Jira を使ったことがあれば、これまでの内容はまったく目新しくないはずです。
|
||||
|
||||
しかし Multica の決定的な特徴はまだ登場していません。**エージェントをワークスペースの一級メンバーとして扱うこと**です。次はまさにこの話に移ります。
|
||||
|
||||
## 次へ
|
||||
|
||||
- [エージェント](/agents) — 何であり、人とどう違うのか
|
||||
- [コメントでエージェントをメンションする](/mentioning-agents) — コメントで `@` を使ってエージェントを起動する
|
||||
@@ -1,81 +0,0 @@
|
||||
---
|
||||
title: 댓글과 멘션
|
||||
description: 이슈 아래에서 협업하기 — 댓글, 답글, `@` 멘션, 반응, 그리고 댓글에서 에이전트를 트리거하기.
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
|
||||
모든 [이슈](/issues)에는 댓글 스레드가 있습니다. 댓글을 작성하고, 다른 사람에게 답글을 달고, [멤버](/members-roles)나 [에이전트](/agents)를 `@`로 멘션하고, 반응을 추가하세요 — 지금까지 써온 어떤 작업 관리 도구에서나 하던 것과 똑같은 동작입니다. 단 한 가지 다른 점은: **`@`로 에이전트를 멘션하면 에이전트가 작업을 시작하도록 트리거된다**는 것입니다.
|
||||
|
||||
## 댓글 작성하기
|
||||
|
||||
이슈 상세 페이지 하단의 입력란에 내용을 입력하고 **보내기**를 누르세요. 댓글이 스레드에 즉시 나타납니다. 댓글은 Markdown을 지원합니다 — 제목, 목록, 코드 블록, 링크를 모두 사용할 수 있습니다.
|
||||
|
||||
## 댓글에 답글 달기
|
||||
|
||||
어떤 댓글이든 오른쪽 상단의 **답글**을 클릭하면 그 아래에 중첩된 입력란이 열립니다. 작성한 답글은 해당 댓글의 하위 항목으로 표시되어 대화 스레드를 이룹니다. 답글에도 다시 답글을 달 수 있으며, 필요한 만큼 깊이 중첩됩니다.
|
||||
|
||||
이슈 목록에는 최상위 댓글 수만 표시되며, 이슈를 열어야 전체 대화 트리를 볼 수 있습니다.
|
||||
|
||||
## 반응
|
||||
|
||||
각 댓글의 오른쪽 상단에는 빠른 신호를 보내기 위한 반응 버튼이 있습니다(👍, 👀, 🎉) — 동의를 표시하려고 "+1" 댓글을 따로 달 필요가 없습니다.
|
||||
|
||||
## `@` 멘션
|
||||
|
||||
댓글에 `@`를 입력하면 선택 창이 열립니다. 멤버나 에이전트를 고르면 `@`와 대상의 slug가 함께 삽입됩니다(`@alice` 또는 `@reviewer-bot`). 멘션된 대상은 자신의 [인박스](/inbox)에서 알림을 받습니다.
|
||||
|
||||
**에이전트를 멘션하면 자동으로 트리거됩니다** — [댓글에서 에이전트 멘션하기](/mentioning-agents)를 참고하세요.
|
||||
|
||||
하나의 댓글에서 같은 사람을 여러 번 멘션해도 알림은 **하나만** 발생합니다.
|
||||
|
||||
### `@all`은 워크스페이스 전체에 알립니다
|
||||
|
||||
`@all`은 특별한 대상입니다. 워크스페이스의 모든 멤버에게 알림을 보냅니다. 사람과 에이전트 모두 `@all`을 사용할 수 있습니다 — 즉 진행 상황을 보고하는 에이전트도 `@all`을 할 수 있으므로, 에이전트의 지침에 이를 아껴서 사용하라고 일러두세요.
|
||||
|
||||
<Callout type="warning">
|
||||
**`@all`은 신중하게 사용하세요.** 규모가 큰 워크스페이스에서는 단 한 번의 `@all`이 그만큼의 인박스 알림을 순식간에 생성합니다. 모두가 정말로 알아야 하는 일에만 사용하고 — 일상적인 업데이트에는 쓰지 마세요.
|
||||
</Callout>
|
||||
|
||||
## 이슈 참조하기
|
||||
|
||||
다른 이슈를 링크하려면 `MUL-123`처럼 이슈 키를 입력하세요. Multica는 댓글에서 실제 존재하는 이슈 키를 해석하여 내부적으로 `mention://issue/<uuid>` 링크로 저장합니다. 이슈 링크는 단순한 상호 참조일 뿐입니다. 사람에게 알림을 보내지 않으며 에이전트를 트리거하지도 않습니다.
|
||||
|
||||
보통은 `[MUL-123](mention://issue/<uuid>)`을 직접 손으로 작성할 필요가 없습니다. 그 형식은 Multica가 키를 해석한 뒤에 사용하는 표준 내부 표현입니다.
|
||||
|
||||
<Callout type="info">
|
||||
Markdown 강조는 CommonMark 규칙을 따릅니다. 굵은 텍스트가 문장 부호나 닫는 따옴표로 끝나고 그 뒤에 한국어 조사가 바로 이어지면, 닫는 `**`가 인식되지 않을 수 있습니다.
|
||||
|
||||
따옴표를 굵게 표시 범위 밖으로 옮기는 것을 권장합니다:
|
||||
|
||||
```markdown
|
||||
"**무엇을 먼저 정해두고 시작할지**"가
|
||||
```
|
||||
|
||||
다음 대신:
|
||||
|
||||
```markdown
|
||||
**"무엇을 먼저 정해두고 시작할지"**가
|
||||
```
|
||||
</Callout>
|
||||
|
||||
## 댓글 편집과 삭제
|
||||
|
||||
댓글은 작성자만 편집하거나 삭제할 수 있습니다.
|
||||
|
||||
댓글을 삭제하면 그 아래의 **모든 답글도 함께 삭제됩니다**(답글의 답글 포함). 내용만 바꾸려면 삭제 대신 편집을 사용하세요.
|
||||
|
||||
<Callout type="warning">
|
||||
**댓글을 편집하면서 `@`를 추가해도 에이전트는 트리거되지 않습니다.** 트리거는 댓글이 **생성되는** 그 순간에 발생합니다 — 나중에 편집해서 새로운 `@`를 추가하거나 대상을 바꿔도 새 알림이 발송되지 않고 에이전트가 깨어나지도 않습니다. 놓친 에이전트를 불러오려면 그 에이전트를 `@`하는 **새 댓글을 작성**하세요.
|
||||
</Callout>
|
||||
|
||||
---
|
||||
|
||||
여기까지 다룬 내용은 모두 "사람의 세계"입니다 — 워크스페이스, 멤버, 이슈, 프로젝트, 댓글. Linear나 Jira를 써본 적이 있다면 지금까지의 내용은 전혀 낯설지 않을 것입니다.
|
||||
|
||||
하지만 Multica의 결정적인 특징은 아직 등장하지 않았습니다: **에이전트를 워크스페이스의 일급 멤버로 대우하는 것**입니다. 다음 장에서 바로 이 이야기를 다룹니다.
|
||||
|
||||
## 다음
|
||||
|
||||
- [에이전트](/agents) — 무엇이며, 사람과 어떻게 다른지
|
||||
- [댓글에서 에이전트 멘션하기](/mentioning-agents) — 댓글에서 `@`로 에이전트를 시작시키기
|
||||
@@ -37,28 +37,6 @@ Mentioning the same person multiple times in one comment still produces **only o
|
||||
**Use `@all` carefully.** In a larger workspace, a single `@all` generates that many inbox notifications instantly. Reserve it for things everyone genuinely needs to know — not day-to-day updates.
|
||||
</Callout>
|
||||
|
||||
## Referencing issues
|
||||
|
||||
To link another issue, type its issue key, such as `MUL-123`. Multica resolves real issue keys in comments and stores them as an internal `mention://issue/<uuid>` link. Issue links are cross-references only: they do not notify people and they do not trigger agents.
|
||||
|
||||
You normally do not need to write `[MUL-123](mention://issue/<uuid>)` by hand. That format is the canonical internal representation after Multica has resolved the key.
|
||||
|
||||
<Callout type="info">
|
||||
Markdown emphasis follows CommonMark rules. When bold text ends with punctuation or a closing quote and is immediately followed by a Korean particle, the closing `**` may not be recognized.
|
||||
|
||||
Prefer moving the quote outside the bold span:
|
||||
|
||||
```markdown
|
||||
"**무엇을 먼저 정해두고 시작할지**"가
|
||||
```
|
||||
|
||||
instead of:
|
||||
|
||||
```markdown
|
||||
**"무엇을 먼저 정해두고 시작할지"**가
|
||||
```
|
||||
</Callout>
|
||||
|
||||
## Editing and deleting a comment
|
||||
|
||||
Only the author of a comment can edit or delete it.
|
||||
|
||||
@@ -37,28 +37,6 @@ import { Callout } from "fumadocs-ui/components/callout";
|
||||
**谨慎使用 `@all`**。工作区人数较多时,一条 `@all` 的评论会瞬间生成同等数量的收件箱通知。只在确实需要全员知晓的重大事项上使用——不是日常琐事。
|
||||
</Callout>
|
||||
|
||||
## 引用 issue
|
||||
|
||||
要链接另一个 issue,直接输入它的 issue key,例如 `MUL-123`。Multica 会在评论中解析真实存在的 issue key,并把它存成内部的 `mention://issue/<uuid>` 链接。Issue 链接只是交叉引用:不会通知成员,也不会触发智能体。
|
||||
|
||||
通常不需要手写 `[MUL-123](mention://issue/<uuid>)`。这是 Multica 解析 key 之后使用的内部规范格式。
|
||||
|
||||
<Callout type="info">
|
||||
Markdown 加粗遵循 CommonMark 规则。当加粗文本以标点或闭引号结尾,并且后面紧跟韩语助词时,结尾的 `**` 可能不会被识别。
|
||||
|
||||
推荐把引号放到加粗范围外:
|
||||
|
||||
```markdown
|
||||
"**무엇을 먼저 정해두고 시작할지**"가
|
||||
```
|
||||
|
||||
而不是:
|
||||
|
||||
```markdown
|
||||
**"무엇을 먼저 정해두고 시작할지"**가
|
||||
```
|
||||
</Callout>
|
||||
|
||||
## 编辑和删除评论
|
||||
|
||||
只有评论的作者能编辑或删除自己的评论。
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
---
|
||||
title: デーモンとランタイム
|
||||
description: エージェントは Multica のサーバーでは実行されません — あなた自身のマシンで実行されます。
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
import { Mermaid } from "@/components/mermaid";
|
||||
|
||||
Multica では、[エージェント](/agents)は私たちのサーバーでは実行され**ません** — ローカルにインストールされた [AI コーディングツール](/providers)を呼び出す**デーモン**という小さなプログラムが駆動し、あなた自身のマシンで実行されます。Multica サーバーは調整役に徹します。[イシュー](/issues)を保存し、[タスク](/tasks)をキューに入れ、適切な**ランタイム**へ分配します(ランタイム = デーモン × AI コーディングツール 1 つ)。
|
||||
|
||||
この構造が Multica と Linear / Jira の最大の違いです。**あなたの API キー、ツールチェーン、コードディレクトリはすべてあなたのマシンに残り**、Multica サーバーはそのどれも見ることはありません。つまり「自分のエージェントが動かない」はほとんど常にローカルの問題です。デーモンが実行されていない、AI ツールがインストールされていない、キーが期限切れになっている、といったことです。まずローカルを確認してください。案内は[トラブルシューティング](/troubleshooting)を参照してください。
|
||||
|
||||
## デーモンを起動する
|
||||
|
||||
デーモンは Multica CLI の一部です。[Multica CLI](/cli) をインストールしたら、あなた自身のマシンで実行してください。
|
||||
|
||||
```bash
|
||||
multica daemon start
|
||||
```
|
||||
|
||||
起動時にデーモンは 4 つのことを行います。
|
||||
|
||||
1. ログイン時に保存された認証情報を読み込みます
|
||||
2. `PATH` にインストールされた AI コーディングツールを検出します(内蔵 12 種: [Antigravity](/providers#antigravity)、[Claude Code](/providers#claude-code)、[Codex](/providers#codex)、[Cursor](/providers#cursor)、[Copilot](/providers#copilot)、[Gemini](/providers#gemini)、[Hermes](/providers#hermes)、[Kimi](/providers#kimi)、[Kiro CLI](/providers#kiro-cli)、[OpenCode](/providers#opencode)、[OpenClaw](/providers#openclaw)、[Pi](/providers#pi))
|
||||
3. 検出した各ツールに対するランタイムとともに、自身をサーバーに登録します
|
||||
4. **3 秒ごと**に取得すべきタスクがないかポーリングし、**15 秒ごとにハートビートを送信**し続けます
|
||||
|
||||
よく使うコマンド:
|
||||
|
||||
| コマンド | 用途 |
|
||||
|---|---|
|
||||
| `multica daemon start` | 起動(デフォルトはバックグラウンド。フォアグラウンドで実行するには `--foreground` を追加) |
|
||||
| `multica daemon stop` | 停止 |
|
||||
| `multica daemon restart` | 再起動 |
|
||||
| `multica daemon status` | ステータス表示 |
|
||||
| `multica daemon logs` | ログ表示(追従するには `-f` を追加) |
|
||||
|
||||
完全な CLI リファレンスは [CLI コマンド](/cli)を確認してください。
|
||||
|
||||
**デスクトップアプリにはデーモンが同梱されています。** [デスクトップアプリ](/desktop-app)を使う場合、`multica daemon start` を手動で実行する必要はありません。起動時にデーモンを自動的に立ち上げます。あなたのワークフローにどの方式が合うかは、[デスクトップアプリ](/desktop-app)ページを参照してください。
|
||||
|
||||
## 1 つのマシンに複数のランタイムができる理由
|
||||
|
||||
ランタイムはサーバーでもコンテナでもありません。「**デーモン × AI コーディングツール 1 つ**」の組み合わせです。たとえば、Claude Code と Codex の両方がインストールされた MacBook でデーモンを起動し、あなたが 2 つのワークスペースのメンバーだとします。すると Multica は 4 つのランタイムを登録します。
|
||||
|
||||
<Mermaid chart={`
|
||||
graph TD
|
||||
D["あなたのデーモン<br/>MacBook"]
|
||||
D --> R1["ランタイム<br/>ワークスペース A × Claude Code"]
|
||||
D --> R2["ランタイム<br/>ワークスペース A × Codex"]
|
||||
D --> R3["ランタイム<br/>ワークスペース B × Claude Code"]
|
||||
D --> R4["ランタイム<br/>ワークスペース B × Codex"]
|
||||
`} />
|
||||
|
||||
要点:
|
||||
|
||||
- **1 つのデーモンは複数のランタイムにマッピングされ得ます** — インストールされたツールと、あなたが所属するワークスペースの組み合わせごとに 1 つできます
|
||||
- **同じデーモン、ワークスペース、ツールは、ちょうど 1 つのランタイムを作ります** — デーモンを再起動しても重複レコードは生まれません
|
||||
- Multica UI の**ランタイム**ページがこれらの行を一覧表示します
|
||||
|
||||
<Callout type="info">
|
||||
**クラウドランタイムが近日提供されます。** 現在は順番待ちリストの段階です。提供が始まれば、ローカルのデーモンを実行せずに Multica Cloud 上で直接エージェントタスクを実行できるようになります。[ダウンロードページ](https://multica.ai/download)でメールアドレスを登録すると通知を受け取れます。
|
||||
</Callout>
|
||||
|
||||
## ランタイムがオフラインと表示される時点
|
||||
|
||||
Multica はハートビートでランタイムがオンラインかどうかを判断します。3 つの重要な数値があります。
|
||||
|
||||
| イベント | しきい値 |
|
||||
|---|---|
|
||||
| デーモンのハートビート頻度 | **15 秒**ごと |
|
||||
| 欠落として表示 | **45 秒**間ハートビートなし(3 回欠落) |
|
||||
| 自動削除 | 関連するエージェントがない状態で **7 日**以上欠落 |
|
||||
|
||||
欠落は永続的ではありません。デーモンが再びハートビートを送った瞬間にオンラインに戻り、ランタイムレコードも保持されます。デーモンを再起動してもランタイムは失われません。
|
||||
|
||||
<Callout type="warning">
|
||||
**欠落したランタイムで実行中だったタスクは失敗として表示されます**(失敗理由 `runtime_offline`)。リトライ可能なソース(イシュー、チャット)については、Multica が自動的に再度キューに入れます。オートパイロットがトリガーしたタスクは自動的にはリトライされません。[タスク → どの失敗が自動リトライされるか](/tasks#which-failures-retry-automatically-which-dont)を参照してください。
|
||||
</Callout>
|
||||
|
||||
## いくつのタスクを並列に実行できるか
|
||||
|
||||
Multica は 2 つの層で同時実行数の制限を適用します。
|
||||
|
||||
- **デーモン層**: デフォルトで**同時タスク 20 個**(環境変数 `MULTICA_DAEMON_MAX_CONCURRENT_TASKS` で調整可能)
|
||||
- **エージェント層**: デフォルトで**エージェントあたり同時タスク 6 個**(エージェントごとに設定)
|
||||
|
||||
2 つのうち厳しい方が適用されます。デーモンがすでにタスク 20 個を実行中なら、あるエージェントに余裕が残っていても新しいタスクは待機します。
|
||||
|
||||
タスクが `dispatched` に進めず `queued` で止まっている場合、通常はこの 2 つの制限のいずれかが飽和しています。
|
||||
|
||||
## デーモンのクラッシュ後、進行中だったタスクはどうなるか
|
||||
|
||||
デーモンがクラッシュしたり強制終了されたりすると、デーモンが取得していたタスクは `dispatched` または `running` 状態に残ります。次回の起動時、デーモンはサーバーに「これらのタスクはもう私のものではないので、失敗として表示してください」と伝えます。サーバーはそれを理由 `runtime_recovery` とともに `failed` に切り替えます。リトライ可能なソースについては、タスクが自動的に再度キューに入ります。
|
||||
|
||||
この手順がネットワークの問題で失敗しても、バックアップとして**30 秒ごと**にサーバー側のスキャンが回ります。45 秒以上ハートビートのないランタイムは欠落として表示され、その上のタスクも一緒に回収されます。
|
||||
|
||||
## 動かないエージェントのトラブルシューティング
|
||||
|
||||
「自分のエージェントが動かない」という問題に遭遇したら、まずこの 3 ステップのチェックリストを進めてください。
|
||||
|
||||
1. `multica daemon status` を実行し、デーモンが実行中でオンラインかを確認します
|
||||
2. `multica daemon logs -f` を実行し、エラーがないかを確認します
|
||||
3. Multica UI の**ランタイム**ページを開き、ランタイムが「オンライン」と表示されているかを確認します
|
||||
|
||||
より多くのシナリオは[トラブルシューティング](/troubleshooting)を参照してください。
|
||||
|
||||
## 次へ
|
||||
|
||||
- [タスク](/tasks) — デーモンがタスクを取得した後の全ライフサイクル
|
||||
- [プロバイダー対照表](/providers) — 12 種の AI コーディングツールの機能の違い
|
||||
@@ -1,111 +0,0 @@
|
||||
---
|
||||
title: 데몬과 런타임
|
||||
description: 에이전트는 Multica 서버에서 실행되지 않습니다. 여러분의 기기에서 실행됩니다.
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
import { Mermaid } from "@/components/mermaid";
|
||||
|
||||
Multica에서 [에이전트](/agents)는 우리 서버에서 실행되지 **않습니다**. 로컬에 설치된 [AI 코딩 도구](/providers)를 호출하는 **데몬**이라는 작은 프로그램이 구동하여, 여러분 자신의 기기에서 실행됩니다. Multica 서버는 조율만 담당합니다. [이슈](/issues)를 저장하고, [작업](/tasks)을 대기열에 넣고, 알맞은 **런타임**으로 분배합니다(런타임 = 데몬 × AI 코딩 도구 하나).
|
||||
|
||||
이 구조가 Multica와 Linear / Jira의 가장 큰 차이점입니다. **여러분의 API 키, 툴체인, 코드 디렉터리는 모두 여러분의 기기에 남아 있으며**, Multica 서버는 그중 어느 것도 보지 못합니다. 따라서 "내 에이전트가 동작하지 않는다"는 거의 항상 로컬 문제입니다. 데몬이 실행 중이 아니거나, AI 도구가 설치되어 있지 않거나, 키가 만료되었을 수 있습니다. 먼저 로컬을 확인하세요. 안내는 [문제 해결](/troubleshooting)을 참고하세요.
|
||||
|
||||
## 데몬 시작하기
|
||||
|
||||
데몬은 Multica CLI의 일부입니다. [Multica CLI](/cli)를 설치한 뒤, 여러분의 기기에서 실행하세요.
|
||||
|
||||
```bash
|
||||
multica daemon start
|
||||
```
|
||||
|
||||
시작 시 데몬은 네 가지 일을 합니다.
|
||||
|
||||
1. 로그인할 때 저장된 인증 정보를 읽습니다
|
||||
2. `PATH`에 설치된 AI 코딩 도구를 감지합니다(내장 12종: [Antigravity](/providers#antigravity), [Claude Code](/providers#claude-code), [Codex](/providers#codex), [Cursor](/providers#cursor), [Copilot](/providers#copilot), [Gemini](/providers#gemini), [Hermes](/providers#hermes), [Kimi](/providers#kimi), [Kiro CLI](/providers#kiro-cli), [OpenCode](/providers#opencode), [OpenClaw](/providers#openclaw), [Pi](/providers#pi))
|
||||
3. 감지된 각 도구에 대한 런타임과 함께 자신을 서버에 등록합니다
|
||||
4. **3초마다** 가져올 작업이 있는지 폴링하고, **15초마다 하트비트를 전송**합니다
|
||||
|
||||
자주 쓰는 명령:
|
||||
|
||||
| 명령 | 용도 |
|
||||
|---|---|
|
||||
| `multica daemon start` | 시작(기본은 백그라운드. 포그라운드로 실행하려면 `--foreground` 추가) |
|
||||
| `multica daemon stop` | 중지 |
|
||||
| `multica daemon restart` | 재시작 |
|
||||
| `multica daemon status` | 상태 표시 |
|
||||
| `multica daemon logs` | 로그 표시(따라가려면 `-f` 추가) |
|
||||
|
||||
전체 CLI 참고는 [CLI 명령](/cli)을 확인하세요.
|
||||
|
||||
**데스크톱 앱에는 데몬이 포함되어 있습니다.** [데스크톱 앱](/desktop-app)을 사용한다면 `multica daemon start`를 수동으로 실행할 필요가 없습니다. 시작할 때 데몬을 자동으로 띄웁니다. 여러분의 워크플로에 어떤 방식이 맞는지는 [데스크톱 앱](/desktop-app) 페이지를 참고하세요.
|
||||
|
||||
## 한 기기에 여러 런타임이 생기는 이유
|
||||
|
||||
런타임은 서버도 아니고 컨테이너도 아닙니다. "**데몬 × AI 코딩 도구 하나**"의 조합입니다. 예를 들어, Claude Code와 Codex가 모두 설치된 MacBook에서 데몬을 시작하고, 여러분이 두 개의 워크스페이스 멤버라고 합시다. 그러면 Multica는 4개의 런타임을 등록합니다.
|
||||
|
||||
<Mermaid chart={`
|
||||
graph TD
|
||||
D["여러분의 데몬<br/>MacBook"]
|
||||
D --> R1["런타임<br/>워크스페이스 A × Claude Code"]
|
||||
D --> R2["런타임<br/>워크스페이스 A × Codex"]
|
||||
D --> R3["런타임<br/>워크스페이스 B × Claude Code"]
|
||||
D --> R4["런타임<br/>워크스페이스 B × Codex"]
|
||||
`} />
|
||||
|
||||
핵심 포인트:
|
||||
|
||||
- **하나의 데몬은 여러 런타임에 매핑될 수 있습니다.** 설치된 도구와 가입한 워크스페이스의 조합마다 하나씩 생깁니다
|
||||
- **같은 데몬, 워크스페이스, 도구는 정확히 하나의 런타임을 만듭니다.** 데몬을 재시작해도 중복 레코드가 생기지 않습니다
|
||||
- Multica UI의 **런타임** 페이지가 이 행들을 나열합니다
|
||||
|
||||
<Callout type="info">
|
||||
**클라우드 런타임이 곧 제공됩니다.** 현재는 대기자 명단 단계입니다. 제공이 시작되면 로컬 데몬을 실행하지 않고도 Multica Cloud에서 직접 에이전트 작업을 실행할 수 있습니다. [다운로드 페이지](https://multica.ai/download)에서 이메일로 등록하면 알림을 받을 수 있습니다.
|
||||
</Callout>
|
||||
|
||||
## 런타임이 오프라인으로 표시되는 시점
|
||||
|
||||
Multica는 하트비트로 런타임이 온라인인지 판단합니다. 세 가지 핵심 수치:
|
||||
|
||||
| 이벤트 | 임곗값 |
|
||||
|---|---|
|
||||
| 데몬 하트비트 빈도 | **15초**마다 |
|
||||
| 누락으로 표시 | **45초** 동안 하트비트 없음(3회 누락) |
|
||||
| 자동 삭제 | 연결된 에이전트 없이 **7일** 넘게 누락 상태 |
|
||||
|
||||
누락은 영구적이지 않습니다. 데몬이 다시 하트비트를 보내는 즉시 온라인으로 돌아오며, 런타임 레코드도 보존됩니다. 데몬을 재시작해도 런타임은 사라지지 않습니다.
|
||||
|
||||
<Callout type="warning">
|
||||
**누락된 런타임에서 실행 중이던 작업은 실패로 표시됩니다**(실패 사유 `runtime_offline`). 재시도 가능한 출처(이슈, 채팅)에 대해서는 Multica가 자동으로 다시 대기열에 넣습니다. 오토파일럿이 트리거한 작업은 자동으로 재시도되지 않습니다. [작업 → 어떤 실패가 자동 재시도되는지](/tasks#which-failures-retry-automatically-which-dont)를 참고하세요.
|
||||
</Callout>
|
||||
|
||||
## 동시에 실행할 수 있는 작업 수
|
||||
|
||||
Multica는 두 계층에서 동시성 제한을 적용합니다.
|
||||
|
||||
- **데몬 계층**: 기본 **동시 작업 20개**(환경 변수 `MULTICA_DAEMON_MAX_CONCURRENT_TASKS`로 조정 가능)
|
||||
- **에이전트 계층**: 기본 **에이전트당 동시 작업 6개**(에이전트별로 설정)
|
||||
|
||||
둘 중 더 엄격한 쪽이 적용됩니다. 데몬이 이미 작업 20개를 실행 중이라면, 어떤 에이전트에 여유가 남아 있어도 새 작업은 대기합니다.
|
||||
|
||||
작업이 `dispatched`로 넘어가지 못하고 `queued`에 멈춰 있다면, 보통 이 두 제한 중 하나가 포화 상태인 것입니다.
|
||||
|
||||
## 데몬 충돌 후 진행 중이던 작업은 어떻게 되나
|
||||
|
||||
데몬이 충돌하거나 강제 종료되면, 데몬이 가져갔던 작업은 `dispatched` 또는 `running` 상태에 남습니다. 다음 시작 시 데몬은 서버에 "이 작업들은 더 이상 제 것이 아니니, 실패로 표시해 주세요"라고 알립니다. 서버는 이를 사유 `runtime_recovery`와 함께 `failed`로 전환합니다. 재시도 가능한 출처에 대해서는 작업이 자동으로 다시 대기열에 들어갑니다.
|
||||
|
||||
이 단계가 네트워크 문제로 실패하더라도, 백업으로 **30초마다** 서버 측 스캔이 돕니다. 45초 넘게 하트비트가 없는 런타임은 누락으로 표시되며, 그 위의 작업도 함께 회수됩니다.
|
||||
|
||||
## 동작하지 않는 에이전트 문제 해결
|
||||
|
||||
"내 에이전트가 동작하지 않는다"는 문제를 만나면, 먼저 이 세 단계 체크리스트를 진행하세요.
|
||||
|
||||
1. `multica daemon status`를 실행해 데몬이 실행 중이고 온라인인지 확인하세요
|
||||
2. `multica daemon logs -f`를 실행해 오류가 있는지 확인하세요
|
||||
3. Multica UI의 **런타임** 페이지를 열어 런타임이 "온라인"으로 표시되는지 확인하세요
|
||||
|
||||
더 많은 시나리오는 [문제 해결](/troubleshooting)을 참고하세요.
|
||||
|
||||
## 다음
|
||||
|
||||
- [작업](/tasks) — 데몬이 작업을 가져온 뒤의 전체 생애 주기
|
||||
- [제공자 대조표](/providers) — 12종 AI 코딩 도구의 기능 차이
|
||||
@@ -1,99 +0,0 @@
|
||||
---
|
||||
title: デスクトップアプリ
|
||||
description: Multica Desktop とは何か、Web アプリとどう違うのか、そしてどんなときに使う価値があるのかを解説します。
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
|
||||
Multica Desktop は macOS、Windows、Linux 向けのネイティブデスクトップアプリです。設定された環境に対して、Web アプリと同じバックエンドに接続し、同じデータを表示します。Desktop はデフォルトで Multica Cloud を使用しますが、セルフホスト環境はローカルのランタイム設定ファイルで構成できます。Desktop はブラウザにはできないいくつかの機能も追加で提供します。**[ワークスペース](/workspaces)ごとの独立したタブグループ**、**[デーモン](/daemon-runtimes)の自動起動**、**ワンクリックアップグレード**です。
|
||||
|
||||
## Desktop か Web か — どちらを選ぶか
|
||||
|
||||
| | Web | Desktop |
|
||||
|---|---|---|
|
||||
| アクセス方法 | ブラウザで URL を開く | ネイティブアプリをインストール |
|
||||
| 複数タブ | ブラウザ自体のタブ(ワークスペースの区別なし) | **ワークスペースごとに独立したタブグループ 1 つ** |
|
||||
| デーモン | `multica daemon start` を自分で実行 | 起動時に**自動的に開始** |
|
||||
| アップグレード | 更新すると最新版になる | アプリがバックグラウンドで確認し、次回起動時にインストール |
|
||||
| ログイン後のデータ | 同一 | 同一 |
|
||||
|
||||
**Web を選ぶ**: 一度きりの利用、他人のマシンでの作業、何もインストールしたくないとき。
|
||||
**Desktop を選ぶ**: 毎日の利用、複数のワークスペースを同時に扱うとき、デーモンを手動で管理したくないとき。
|
||||
|
||||
## 複数タブ: ワークスペースを切り替えるとどうなるか
|
||||
|
||||
Desktop は**参加しているすべてのワークスペース**ごとに独立したタブグループを保持します。ワークスペースを切り替えると、現在のワークスペースのタブが 1 つの単位として非表示になり、以前のワークスペースのタブは離れたときのまま復元されます — VSCode のマルチワークスペースの挙動や、Slack でワークスペースを切り替えるのに似ています。
|
||||
|
||||
例: ワークスペース A でイシューのタブを 3 つ開いた状態でワークスペース B に切り替えます。A のタブ 3 つは消え、B には B で最後に開いていたものが表示されます。再び A に切り替えると、その 3 つのタブが以前の状態そのままに戻ってきます。**タブはワークスペース間で決して漏れ出しません。**
|
||||
|
||||
ログアウトすると**すべてのワークスペースのタブ状態が消去される**ため、複数のユーザーでマシンを共有していてもデータが漏れることはありません。
|
||||
|
||||
## Desktop の自動更新の仕組み
|
||||
|
||||
起動時に Desktop は GitHub Releases でより新しいバージョンがないかを確認します。新しいバージョンが見つかると、
|
||||
|
||||
1. バックグラウンドで新しいバージョンを静かにダウンロードします。
|
||||
2. 「準備完了 — 次回起動時にインストールされます」と通知します。
|
||||
3. 終了時(または次回の再起動時)に、アプリが閉じる前に更新をインストールします。
|
||||
4. 次回起動時に新しいバージョンが実行されます。
|
||||
|
||||
このプロセス全体は**作業中の内容を妨げません**。
|
||||
|
||||
<Callout type="warning">
|
||||
**Windows では ARM64 と x64 は別々の更新チャンネルです** — 間違ったアーキテクチャをインストールすると更新が検出されません。ダウンロードする際は、マシンに合った `.exe` を選んでください(ARM ビルドには `arm64` のサフィックスが付いています)。
|
||||
</Callout>
|
||||
|
||||
macOS ビルドは署名・公証されているため、初回起動時に「未確認の開発者」の警告は表示されません。Linux ビルドは `.AppImage` です — 自動更新は electron-updater に依存しており、一部のディストリビューションでは不安定になることがあります。**自動更新が動作しない場合は、新しいバージョンを手動でダウンロードして古いファイルを置き換えてください。**
|
||||
|
||||
## 単体の CLI とデーモンはまだ必要ですか?
|
||||
|
||||
**いいえ。** Desktop には同じ `multica` CLI バイナリが内蔵されており、起動時に独自のデーモンプロファイルを起動します(ターミナルから手動で実行しているデーモンとは隔離されます)。
|
||||
|
||||
すでに CLI をインストールして `multica daemon start` を手動で実行していても、Desktop はそのデーモンを乗っ取りません — 別のプロファイルで独自のデーモンを開始します。両者は**異なるランタイム**として登録され、UI では 2 つの独立したランタイムが表示されます。
|
||||
|
||||
ターミナルで CLI コマンドを実行したい場合、Desktop は特別な経路を提供しません — 別途インストールした CLI を使うか、アプリのリソースディレクトリ内 `resources/bin/multica` にあるバンドル済みのコピーを実行してください。
|
||||
|
||||
## ダウンロードとインストール
|
||||
|
||||
[Multica ダウンロードページ](https://multica.ai/download)から、使用するプラットフォームのインストーラーを入手してください。
|
||||
|
||||
| プラットフォーム | ファイル |
|
||||
|---|---|
|
||||
| macOS (Intel または Apple Silicon) | `.dmg` |
|
||||
| Windows x64 | `.exe`(標準) |
|
||||
| Windows ARM64 | `.exe`(`arm64` サフィックス付き) |
|
||||
| Linux | `.AppImage` |
|
||||
|
||||
初回起動時にはログインが必要です — Web アプリと同じメール + 認証コードのフローです。ログインすると、Desktop はワークスペース一覧を自動的に同期します。
|
||||
|
||||
<Callout type="info">
|
||||
**Desktop はデフォルトで Multica Cloud を使用しますが、ローカルの設定ファイルでセルフホスト環境を指すように設定できます。** アプリ内には依然として「セルフホストに接続」を選ぶピッカーはありません。Desktop はレンダラーが起動する前に `~/.multica/desktop.json` を読み込みます。ファイルがない場合は Cloud のデフォルト値を使用します。
|
||||
|
||||
最小構成のセルフホスト設定:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"apiUrl": "https://api.your-domain"
|
||||
}
|
||||
```
|
||||
|
||||
`apiUrl` は必須で、`http` または `https` を使用する必要があります。Desktop は同一オリジン上で `wsUrl` を `/ws` として導出し(`https` なら `wss`、`http` なら `ws`)、API オリジンから `appUrl` を導出します。デプロイ環境が異なるオリジンを使用する場合は明示的に設定してください。
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"apiUrl": "https://api.your-domain",
|
||||
"wsUrl": "wss://api.your-domain/ws",
|
||||
"appUrl": "https://your-domain"
|
||||
}
|
||||
```
|
||||
|
||||
`desktop.json` は存在するが無効な場合、Desktop は安全側に倒して動作を停止し、Cloud に静かにフォールバックする代わりにブロック型の設定エラーを表示します。開発ビルドの場合、`electron-vite dev` 中は依然として `VITE_API_URL` / `VITE_WS_URL` / `VITE_APP_URL` が優先されます。ランタイムでの Desktop セルフホスト構成は [issue #1371](https://github.com/multica-ai/multica/issues/1371) で実装されました。
|
||||
</Callout>
|
||||
|
||||
## 次のステップ
|
||||
|
||||
- [Cloud Quickstart](/cloud-quickstart) — Desktop 向けの Cloud オンボーディングフロー
|
||||
- [Self-Host Quickstart](/self-host-quickstart) — 自前のバックエンドを実行し、CLI または Desktop のランタイム設定で接続する
|
||||
- [デーモンとランタイム](/daemon-runtimes) — デーモンの仕組み(Desktop が代わりに起動してくれますが、動作は同じです)
|
||||
@@ -1,99 +0,0 @@
|
||||
---
|
||||
title: 데스크톱 앱
|
||||
description: Multica Desktop이 무엇인지, 웹 앱과 어떻게 다른지, 언제 쓸 만한지 알아봅니다.
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
|
||||
Multica Desktop은 macOS, Windows, Linux용 네이티브 데스크톱 앱입니다. 구성된 환경에 대해 웹 앱과 동일한 백엔드에 연결되며 동일한 데이터를 보여줍니다. 기본적으로 Desktop은 Multica Cloud를 사용하며, 자체 호스팅 인스턴스는 로컬 런타임 설정 파일로 구성할 수 있습니다. Desktop은 브라우저가 할 수 없는 몇 가지 기능도 추가로 제공합니다: **[워크스페이스](/workspaces)별 독립적인 탭 그룹**, **[데몬](/daemon-runtimes) 자동 시작**, **원클릭 업그레이드**입니다.
|
||||
|
||||
## Desktop 또는 웹 — 무엇을 선택할까
|
||||
|
||||
| | 웹 | Desktop |
|
||||
|---|---|---|
|
||||
| 접근 방식 | 브라우저에서 URL 열기 | 네이티브 앱 설치 |
|
||||
| 다중 탭 | 브라우저 자체 탭 (워크스페이스 구분 없음) | **워크스페이스별 독립 탭 그룹 한 개** |
|
||||
| 데몬 | `multica daemon start`를 직접 실행 | 실행 시 **자동으로 시작** |
|
||||
| 업그레이드 | 새로고침하면 최신 버전 | 앱이 백그라운드에서 확인하고 다음 실행 시 설치 |
|
||||
| 로그인 후 데이터 | 동일 | 동일 |
|
||||
|
||||
**웹을 선택**: 일회성으로 사용하거나, 다른 사람의 기기에서 작업하거나, 아무것도 설치하고 싶지 않을 때.
|
||||
**Desktop을 선택**: 매일 사용하거나, 여러 워크스페이스를 동시에 다루거나, 데몬을 수동으로 관리하고 싶지 않을 때.
|
||||
|
||||
## 다중 탭: 워크스페이스를 전환하면 어떻게 되나
|
||||
|
||||
Desktop은 **참여한 모든 워크스페이스**마다 독립적인 탭 그룹을 유지합니다. 워크스페이스를 전환하면 현재 워크스페이스의 탭이 하나의 단위로 숨겨지고, 이전 워크스페이스의 탭은 떠났던 그대로 복원됩니다 — VSCode의 멀티 워크스페이스 동작이나 Slack에서 워크스페이스를 전환하는 것과 비슷합니다.
|
||||
|
||||
예시: 워크스페이스 A에서 이슈 탭 3개를 열어 둔 상태로 워크스페이스 B로 전환합니다. A의 탭 3개는 사라지고, B에는 B에서 마지막으로 열어 두었던 것이 표시됩니다. 다시 A로 전환하면 그 3개의 탭이 정확히 이전 상태 그대로 돌아옵니다. **탭은 워크스페이스 간에 절대 새어 나가지 않습니다.**
|
||||
|
||||
로그아웃하면 **모든 워크스페이스의 탭 상태가 지워지므로**, 여러 사용자가 기기를 공유하더라도 데이터가 새어 나가지 않습니다.
|
||||
|
||||
## Desktop의 자동 업데이트 방식
|
||||
|
||||
실행 시 Desktop은 GitHub Releases에서 더 최신 버전이 있는지 확인합니다. 새 버전이 발견되면:
|
||||
|
||||
1. 백그라운드에서 새 버전을 조용히 다운로드합니다.
|
||||
2. "준비 완료 — 다음 실행 시 설치됩니다"라고 알려 줍니다.
|
||||
3. 종료(또는 다음 재시작) 시, 앱이 닫히기 전에 업데이트를 설치합니다.
|
||||
4. 다음 실행 시 새 버전이 실행됩니다.
|
||||
|
||||
이 전체 과정은 **작업 중인 내용을 방해하지 않습니다**.
|
||||
|
||||
<Callout type="warning">
|
||||
**Windows에서는 ARM64와 x64가 별도의 업데이트 채널입니다** — 잘못된 아키텍처를 설치하면 업데이트가 감지되지 않습니다. 다운로드할 때 기기에 맞는 `.exe`를 선택하세요 (ARM 빌드에는 `arm64` 접미사가 붙어 있습니다).
|
||||
</Callout>
|
||||
|
||||
macOS 빌드는 서명 및 공증되어 있으므로, 첫 실행 시 "확인되지 않은 개발자" 경고가 표시되지 않습니다. Linux 빌드는 `.AppImage`입니다 — 자동 업데이트는 electron-updater에 의존하는데, 일부 배포판에서는 불안정할 수 있습니다. **자동 업데이트가 작동하지 않으면, 새 버전을 수동으로 다운로드하여 기존 파일을 교체하세요.**
|
||||
|
||||
## 별도의 CLI와 데몬이 여전히 필요한가요?
|
||||
|
||||
**아닙니다.** Desktop에는 동일한 `multica` CLI 바이너리가 내장되어 있으며, 시작 시 자체 데몬 프로필을 실행합니다 (터미널에서 수동으로 실행 중인 데몬과는 격리됩니다).
|
||||
|
||||
이미 CLI를 설치하고 `multica daemon start`를 직접 실행했더라도, Desktop은 그 데몬을 가로채지 않습니다 — 별도의 프로필로 자체 데몬을 시작합니다. 둘은 **서로 다른 런타임**으로 등록되며, UI에서 두 개의 독립된 런타임을 볼 수 있습니다.
|
||||
|
||||
터미널에서 CLI 명령을 실행하고 싶다면, Desktop이 특별한 경로를 제공하지는 않습니다 — 별도로 설치한 CLI를 사용하거나, 앱의 리소스 디렉터리 안 `resources/bin/multica`에 있는 번들 사본을 실행하세요.
|
||||
|
||||
## 다운로드 및 설치
|
||||
|
||||
[Multica 다운로드 페이지](https://multica.ai/download)에서 사용하는 플랫폼의 설치 프로그램을 받으세요:
|
||||
|
||||
| 플랫폼 | 파일 |
|
||||
|---|---|
|
||||
| macOS (Intel 또는 Apple Silicon) | `.dmg` |
|
||||
| Windows x64 | `.exe` (표준) |
|
||||
| Windows ARM64 | `.exe` (`arm64` 접미사 포함) |
|
||||
| Linux | `.AppImage` |
|
||||
|
||||
첫 실행 시 로그인이 필요합니다 — 웹 앱과 동일한 이메일 + 인증 코드 흐름입니다. 로그인하면 Desktop이 워크스페이스 목록을 자동으로 동기화합니다.
|
||||
|
||||
<Callout type="info">
|
||||
**Desktop은 기본적으로 Multica Cloud를 사용하지만, 로컬 설정 파일로 자체 호스팅 인스턴스를 가리키도록 할 수 있습니다.** 앱 안에는 여전히 "자체 호스팅에 연결" 선택기가 없습니다. Desktop은 렌더러가 시작되기 전에 `~/.multica/desktop.json`을 읽습니다. 파일이 없으면 Cloud 기본값을 사용합니다.
|
||||
|
||||
최소 자체 호스팅 설정:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"apiUrl": "https://api.your-domain"
|
||||
}
|
||||
```
|
||||
|
||||
`apiUrl`은 필수이며 `http` 또는 `https`를 사용해야 합니다. Desktop은 동일 오리진에서 `wsUrl`을 `/ws`로 유도하고(`https`이면 `wss`, `http`이면 `ws`), API 오리진에서 `appUrl`을 유도합니다. 배포 환경이 서로 다른 오리진을 사용한다면 명시적으로 설정하세요:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"apiUrl": "https://api.your-domain",
|
||||
"wsUrl": "wss://api.your-domain/ws",
|
||||
"appUrl": "https://your-domain"
|
||||
}
|
||||
```
|
||||
|
||||
`desktop.json`이 존재하지만 유효하지 않으면, Desktop은 안전하게 동작을 차단하여 Cloud로 조용히 되돌아가는 대신 차단형 설정 오류를 표시합니다. 개발 빌드의 경우, `electron-vite dev` 중에는 여전히 `VITE_API_URL` / `VITE_WS_URL` / `VITE_APP_URL`이 우선합니다. 런타임 Desktop 자체 호스팅 구성은 [issue #1371](https://github.com/multica-ai/multica/issues/1371)에서 구현되었습니다.
|
||||
</Callout>
|
||||
|
||||
## 다음 단계
|
||||
|
||||
- [Cloud Quickstart](/cloud-quickstart) — Desktop을 위한 Cloud 온보딩 흐름
|
||||
- [Self-Host Quickstart](/self-host-quickstart) — 자체 백엔드를 실행하고 CLI 또는 Desktop 런타임 설정으로 연결하기
|
||||
- [데몬 및 런타임](/daemon-runtimes) — 데몬의 작동 방식 (Desktop이 대신 시작해 주지만 동작은 동일합니다)
|
||||
@@ -1,302 +0,0 @@
|
||||
---
|
||||
title: 規約
|
||||
description: コードのネーミング、i18n 翻訳用語集、中国語ボイスガイドの単一の信頼できる情報源。
|
||||
---
|
||||
|
||||
このページは、コードのネーミング、i18n 翻訳用語集、中国語ボイスガイドの単一の信頼できる情報源です。かつて `packages/views/locales/glossary.md` やあちこちに散らばったコメントにあった内容は、現在すべてここに集約されています。
|
||||
|
||||
Multica のコードを書く、翻訳を変更する、あるいは中国語の製品コピーを書く場合は、このページを参照してください。
|
||||
|
||||
---
|
||||
|
||||
## 1. コードのネーミング
|
||||
|
||||
### ルート
|
||||
|
||||
ワークスペース進入前のルート(ユーザーがワークスペースに入る前から存在するルート)は、必ず単一の単語または `/{noun}/{verb}` パターンを使用しなければなりません。
|
||||
|
||||
- ✅ `/login`, `/inbox`, `/workspaces/new`
|
||||
- ❌ `/new-workspace`, `/create-team`, `/accept-invite`
|
||||
|
||||
ルート直下のハイフンで連結した単語のまとまりは、ユーザーが自分で選んだワークスペースの slug と衝突し、際限のない予約 slug の監査を強いることになります。名詞(`workspaces`)を予約しておけば、`/workspaces/*` のサブツリー全体が自動的に保護されます。
|
||||
|
||||
### ワークスペーススコープのルート
|
||||
|
||||
常に `/{slug}/{section}` の下に置きます — `/{slug}/issues`、`/{slug}/agents`、`/{slug}/settings`。ワークスペースのルーティングロジックを絶対に重複させず、共有コードではフレームワーク固有の link API ではなく `useNavigation().push()` を使用してください。
|
||||
|
||||
### パッケージとモジュール
|
||||
|
||||
モノレポは厳格なパッケージ境界を強制します。
|
||||
|
||||
| パッケージ | 依存可能 | 依存禁止 |
|
||||
| --- | --- | --- |
|
||||
| `packages/core` | アプリ固有でないもののみ | `react-dom`, `localStorage`, `process.env`, `next/*`, UI ライブラリ |
|
||||
| `packages/ui` | なし | `@multica/core`, ビジネスロジック |
|
||||
| `packages/views` | `core/`, `ui/` | `next/*`, `react-router-dom`, stores |
|
||||
| `apps/web/platform/` | `next/*` | 他のアプリ |
|
||||
| `apps/desktop/.../platform/` | `react-router-dom`, electron | 他のアプリ |
|
||||
|
||||
両方のアプリに同じロジックが現れる場合は、必ず共有パッケージに抽出しなければなりません。「ささいな」重複という例外はありません。
|
||||
|
||||
### ファイルとコンポーネント
|
||||
|
||||
- ファイル: `kebab-case.tsx` / `kebab-case.ts`(例: `agent-row-actions.tsx`)
|
||||
- コンポーネント: `PascalCase`(例: `AgentRowActions`)
|
||||
- フック: `useCamelCase`(例: `useWorkspaceId`)
|
||||
- テスト: `<file>.test.ts(x)` として同じ場所に配置
|
||||
- ストア(Zustand): `<feature>-store.ts`、`use<Feature>Store` として export
|
||||
|
||||
### データベース(Go + sqlc)
|
||||
|
||||
- テーブル: `snake_case` の単数形(`user`, `workspace`, `agent_runtime`)
|
||||
- カラム: `snake_case`(`workspace_id`, `created_at`, `last_seen_at`)
|
||||
- 外部キー: `<table>_id`
|
||||
- ブール値: `is_<state>` または `<state>_at`(状態変更にはタイムスタンプ形式を推奨)
|
||||
- マイグレーションファイル: `NNN_descriptive_name.up.sql` + `.down.sql` — 常に双方向を提供する
|
||||
|
||||
### Go
|
||||
|
||||
- 標準の `gofmt` + `go vet`。例外なし。
|
||||
- Handler ファイルはドメインを反映する: `agent.go`, `auth.go`, `runtime.go`
|
||||
- テスト: `<file>_test.go` を同じ場所に配置
|
||||
- handler での UUID パースは、ルートの `CLAUDE.md` のルールに従ってください — 境界の入力には `parseUUIDOrBadRequest`、信頼できる往復には `parseUUID`(panic 版)を使い、error を確認せずに `util.ParseUUID` を直接使用しないでください。
|
||||
|
||||
### TypeScript
|
||||
|
||||
- ネットワーク上の API レスポンスは `snake_case` で、api client が境界で `camelCase` に変換します。TS コード内部では**常に camelCase**。
|
||||
- 型: `PascalCase`(`Issue`, `AgentRuntime`);`IPrefix` は禁止、`_t` サフィックスも禁止。
|
||||
- 列挙: string literal union を推奨し、ランタイムで反復処理が必要な場合にのみ `enum` を使用。
|
||||
- TanStack Query のキー: `<feature>/queries.ts` 内のファクトリ関数、例: `issueKeys.detail(id)`。
|
||||
|
||||
### イシューキー
|
||||
|
||||
すべてのイシューには `MUL-123` のような人が読めるキーがあります。ワークスペースの `issue_prefix`(大文字と数字、通常 3 文字、最大 10 文字)+ 連番です。ワークスペースの admin は Settings → General で接頭辞を変更できますが、変更すると既存のすべてのイシューが番号を振り直されるため、古い接頭辞が埋め込まれた外部参照(PR タイトル、ブランチ名、ドキュメントやチャット内のリンク)は解決されなくなります。
|
||||
|
||||
### コード内のコメント
|
||||
|
||||
英語のみです。リポジトリは Go と TypeScript の両方でこれを強制します。コード内に中国語のコメントを見つけたら、それはバグなので置き換えてください。
|
||||
|
||||
### コミットメッセージ
|
||||
|
||||
Conventional 形式: `feat(scope)`, `fix(scope)`, `refactor(scope)`, `docs`, `test(scope)`, `chore(scope)`。意図ごとにまとめられたアトミックなコミット。
|
||||
|
||||
---
|
||||
|
||||
## 2. i18n 翻訳用語集
|
||||
|
||||
これは、すべての翻訳 PR が**必ず**守らなければならない用語集です。かつては `packages/views/locales/glossary.md` にありましたが、そのファイルは現在ここを指す stub です。
|
||||
|
||||
### 中核となる区別: エンティティ vs 概念
|
||||
|
||||
Multica の製品名詞は 2 つのカテゴリに分かれます。
|
||||
|
||||
- **エンティティ(Entity)** — URL、データベースの row、API の型を持ちます。中国語のテキストでは**小文字の英語**で表記し、視覚的に型名のように読めて「これは Multica のシステムエンティティだ」というシグナルを与えます。
|
||||
- **概念(Concept)** — 一般名詞であり、データベースのエンティティではありません。**完全に翻訳**し、中国語ユーザーが流れるテキストの中にギザギザの英語を見ないようにします。
|
||||
|
||||
このルールは `apps/docs/content/docs/*.zh.mdx` と整合しています — これらのドキュメントは事実上の中国語ボイス標準であり、20 ページ以上で実戦検証されています。
|
||||
|
||||
### エンティティ — 混合ルール(`issue` / `skill` / `task`)
|
||||
|
||||
`issue` / `skill` / `task` は Multica の中核エンティティです。スキーマのカラム、API のフィールド、製品 UI のラベルがすべて英語です。中国語のテキストでは**混合ルール**に従い、何を使うかは単語がどこに現れるかによって変わります。
|
||||
|
||||
| 文脈 | 表記 | 例 |
|
||||
| --- | --- | --- |
|
||||
| **UI 文字列、状態名、コード参照** | 小文字の英語 | "排队中的 task"、"创建子 issue"、"为智能体注入 skill" |
|
||||
| **ドキュメントのタイトル / セクション見出し** | Title-case の英語 **または** 中国語の用語 | "Issue 与 project"、"Skills"、"执行任务" |
|
||||
| **長文ドキュメントの本文で、エンティティが文の主語になっている場合** | 中国語の用語、初出時に括弧内に英語 | "**执行任务**(task)是智能体每一次工作的单位" |
|
||||
| **API / DB フィールド** | 常に `task` / `issue` / `skill` | `task_id`, `issue_status`, `skill_uuid` |
|
||||
|
||||
中国語の用語の参考:
|
||||
|
||||
- `task` ↔ `执行任务`(文脈が明確になれば `任务` に短縮)
|
||||
- `issue` には定着した中国語訳がありません — 英語のまま;タイトルでは `Issue` のように大文字にできます
|
||||
- `skill` には定着した中国語訳がありません — 英語のまま;タイトルでは `Skills` のように大文字にできます
|
||||
|
||||
**`issue` / `skill` / `task` が `project` / `autopilot` のように中国語へ強制的に翻訳されない理由**:
|
||||
|
||||
- **`issue` / `task`**: 開発チームは英語で会話します。中国語の候補("任务" — あいまいすぎて "工作" とほぼ同義;"工单" — IT チケットのニュアンス;"议题" — GitHub 風だが製品の感覚に合わない)はいずれも `issue` よりも読みづらくなります。**ただし**、長文ドキュメントの本文で小文字の `task` を 50 回繰り返すとリズムが崩れるため、本文では `执行任务` を許容しつつ、UI 文字列と状態名は小文字の英語のままにします。
|
||||
- **`skill`**: 定着した中国語の用語がない Multica 固有の概念です。
|
||||
- **`project` → "项目"**: 定着した主流の中国語の単語です。Feishu / Tower / Teambition / PingCode / GitHub Projects — すべての中国語製品がこれを翻訳します。中国語の文脈で `project` をそのまま残す製品はありません。
|
||||
- **`autopilot` → "自动化"**: 中国語で "autopilot" は Tesla の "自动驾驶" を連想させ、この機能が行うこと(スケジュールに従ってタスクを実行する)と合いません。Notion も Feishu も "自动化" を使っており、それが業界の合意です。
|
||||
|
||||
### 翻訳しない — ブランドと頭字語
|
||||
|
||||
| カテゴリ | 用語 |
|
||||
| --- | --- |
|
||||
| ブランド | **Multica**, GitHub, Slack, Google, Anthropic, OpenAI, Claude, Codex, Cursor, Linear, Jira |
|
||||
| 頭字語 | API, CLI, URL, SDK, OAuth, JWT, SSO, WebSocket, HTTP, JSON, YAML, SQL |
|
||||
|
||||
### 完全に翻訳する — 概念
|
||||
|
||||
| English | Chinese |
|
||||
| --- | --- |
|
||||
| Workspace | **工作区** |
|
||||
| Agent | **智能体** |
|
||||
| Project | **项目** |
|
||||
| Autopilot | **自动化** |
|
||||
| Daemon | **守护进程** |
|
||||
| Runtime | **运行时** |
|
||||
| Inbox | **收件箱** |
|
||||
| Comment | **评论** |
|
||||
| Reply | **回复** |
|
||||
| Notifications | **通知** |
|
||||
| Member | **成员** |
|
||||
| Label | **标签** |
|
||||
| Settings | **设置** |
|
||||
| Onboarding | **上手引导** |
|
||||
|
||||
### 完全に翻訳する — 一般的な UI 用語
|
||||
|
||||
| English | Chinese |
|
||||
| --- | --- |
|
||||
| Invite / Invitation | 邀请 |
|
||||
| Search | 搜索 |
|
||||
| Email | 邮箱 (label) / 邮件 (action) |
|
||||
| Password | 密码 |
|
||||
| Sign in / Log in | 登录 |
|
||||
| Sign up | 注册 |
|
||||
| Sign out / Log out | 退出登录 |
|
||||
| Save / Cancel / Delete | 保存 / 取消 / 删除 |
|
||||
| Confirm / Continue / Back | 确认 / 继续 / 返回 |
|
||||
| Edit / New / Create / Add | 编辑 / 新建 / 创建 / 添加 |
|
||||
| Remove / Send / Open / Close | 移除 / 发送 / 打开 / 关闭 |
|
||||
| Preview / Download / Upload | 预览 / 下载 / 上传 |
|
||||
| Done / Loading... | 完成 / 加载中... |
|
||||
| Profile / Account / Appearance | 个人资料 / 账号 / 外观 |
|
||||
| Theme / Language | 主题 / 语言 |
|
||||
| Light / Dark / System | 浅色 / 深色 / 跟随系统 |
|
||||
| Active / Archived | 活跃 (or 启用) / 已归档 |
|
||||
| Status / Priority | 状态 / 优先级 |
|
||||
| Assignee / Reporter | 负责人 / 报告人 |
|
||||
| Description / Title | 描述 / 标题 |
|
||||
| Date / Time | 日期 / 时间 |
|
||||
| Today / Yesterday / Tomorrow | 今天 / 昨天 / 明天 |
|
||||
| Empty / Failed / Success | 空 / 失败 / 成功 |
|
||||
| Error / Warning | 错误 / 警告 |
|
||||
|
||||
### ロールと状態の列挙型(小文字の英語、翻訳しない)
|
||||
|
||||
これらはスキーマレベルの識別子です。中国語の文脈でも小文字の英語で表記します。
|
||||
|
||||
- ロール: `owner` / `admin` / `member`
|
||||
- イシューの状態: `backlog` / `todo` / `in_progress` / `in_review` / `done` / `blocked` / `cancelled`
|
||||
|
||||
UI ではこれらの値を英語で表示します(必要に応じて `code-style` で囲む)。
|
||||
|
||||
- "你需要 owner 权限"
|
||||
- "已切换到 in_progress"
|
||||
|
||||
### 単語の組み合わせルール
|
||||
|
||||
英語の単語(エンティティ / ブランド / 頭字語)と周囲の中国語の間には、常に**単一の空白**を入れます。
|
||||
|
||||
- "Create new issue" → "新建 issue"
|
||||
- "Assign to agent" → "分配给智能体"
|
||||
- "Configure runtime" → "配置运行时"
|
||||
- "Stop daemon" → "停止守护进程"
|
||||
|
||||
### 複数形と数
|
||||
|
||||
i18next は `_one` / `_other` を使います。中国語には文法的な数がないため、`_other` のみを埋めます。
|
||||
|
||||
```json
|
||||
// en/issues.json
|
||||
{
|
||||
"issue_count_one": "{{count}} issue",
|
||||
"issue_count_other": "{{count}} issues"
|
||||
}
|
||||
|
||||
// zh-Hans/issues.json
|
||||
{
|
||||
"issue_count_other": "{{count}} 个 issue"
|
||||
}
|
||||
```
|
||||
|
||||
一般的な数の形式:
|
||||
|
||||
- `{{count}} issues` → `{{count}} 个 issue`
|
||||
- `{{count}} agents` → `{{count}} 个智能体`
|
||||
- `{{count}} workspaces` → `{{count}} 个工作区`
|
||||
- `{{count}} comments` → `{{count}} 条评论`
|
||||
- `{{count}} members` → `{{count}} 位成员`
|
||||
- `{{count}} skills` → `{{count}} 个 skill`
|
||||
|
||||
### 補間
|
||||
|
||||
`{{var}}` を使います。中国語の翻訳では、自然な文章の流れのために順序を並べ替えてもかまいません。
|
||||
|
||||
```json
|
||||
// en
|
||||
{ "welcome_message": "Welcome back, {{name}}!" }
|
||||
|
||||
// zh-Hans
|
||||
{ "welcome_message": "欢迎回来,{{name}}!" }
|
||||
```
|
||||
|
||||
### 翻訳キーのネーミング
|
||||
|
||||
3 階層のネスト: `feature.component.action`。
|
||||
|
||||
```json
|
||||
{
|
||||
"feature_or_component": {
|
||||
"subcomponent_or_section": {
|
||||
"action_or_label": "..."
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
例:
|
||||
|
||||
- `issues.toolbar.batch_update_success`
|
||||
- `issues.detail.comment_form.placeholder`
|
||||
- `inbox.empty.title`
|
||||
- `settings.preferences.language.title`
|
||||
|
||||
### Web 専用 / Desktop 専用のコピー
|
||||
|
||||
- 共有コピー: namespace JSON の最上位
|
||||
- Web 専用: `web` セクション
|
||||
- Desktop 専用: `desktop` セクション
|
||||
|
||||
正式な例は `auth.json` を参照してください(`web` セクションに `prefer_desktop` / `desktop_handoff.*` が含まれます)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 中国語のボイスとスタイル
|
||||
|
||||
### 句読点
|
||||
|
||||
- 中国語では全角の句読点を使用: `,。:;!?`
|
||||
- 引用符: 英語の原文に合わせて、まっすぐな二重引用符 `"..."` を使用。`「」` や丸い引用符は使わないでください。
|
||||
- 省略記号: 単一文字の `…` ではなく、3 つの点 `...`。英語の原文に合わせてください。
|
||||
- 中国語と英語の混在: 英語の単語の両側にそれぞれ単一の空白(単語の組み合わせルールを参照)。
|
||||
|
||||
### スタイルの原則
|
||||
|
||||
- **簡潔かつ直接的に。** 翻訳調を避ける: "对于 X 来说"、"作为 X"、"我们的"。
|
||||
- **エラーメッセージ**: 穏やかだが明確に。"无法保存修改" は "保存修改失败了!" よりも優れています。
|
||||
- **ボタン**: 動詞を先頭に、2〜4 文字。"取消"、"保存修改"、"立即同步"。
|
||||
- **ツールチップ**: 完結した短い文。"复制链接到剪贴板"。
|
||||
- **プレースホルダー**: 例の形式。"输入 issue 标题..."。
|
||||
|
||||
### 迷ったときに参照する場所
|
||||
|
||||
用語集が特定の用語を扱っていない場合は、次を参照してください。
|
||||
|
||||
1. `apps/docs/content/docs/*.zh.mdx` — 事実上の中国語ボイス標準、一貫した翻訳が 20 ページ以上
|
||||
2. `packages/views/locales/zh-Hans/auth.json` と `editor.json` — JSON 構造 + selector API パターン
|
||||
3. `packages/views/auth/login-page.tsx` — コンポーネントレベルの selector API 呼び出し箇所
|
||||
4. `packages/views/settings/components/preferences-tab.tsx` — 言語切り替えの参考
|
||||
|
||||
---
|
||||
|
||||
## このページを更新するとき
|
||||
|
||||
ここのルールを変更した場合は、次も併せて行ってください。
|
||||
|
||||
1. 関連する locale JSON / CLAUDE.md / ドキュメントページに適用する
|
||||
2. PR の説明に変更点を記録し、レビュアーが下流の一括対応を確認できるようにする
|
||||
|
||||
このページが契約です。他の何ものもこれを上書きできません。
|
||||
@@ -1,302 +0,0 @@
|
||||
---
|
||||
title: 규약
|
||||
description: 코드 네이밍, i18n 번역 용어집, 중국어 보이스 가이드의 단일 진실 공급원.
|
||||
---
|
||||
|
||||
이 페이지는 코드 네이밍, i18n 번역 용어집, 중국어 보이스 가이드의 단일 진실 공급원입니다. 예전에 `packages/views/locales/glossary.md`나 여기저기 흩어진 주석에 있던 내용은 이제 모두 이곳에 모여 있습니다.
|
||||
|
||||
Multica 코드를 작성하거나, 번역을 변경하거나, 중국어 제품 카피를 작성한다면 이 페이지를 참고하세요.
|
||||
|
||||
---
|
||||
|
||||
## 1. 코드 네이밍
|
||||
|
||||
### 라우트
|
||||
|
||||
워크스페이스 진입 전 라우트(사용자가 워크스페이스에 들어가기 전에 존재하는 라우트)는 반드시 단일 단어 또는 `/{noun}/{verb}` 패턴을 사용해야 합니다.
|
||||
|
||||
- ✅ `/login`, `/inbox`, `/workspaces/new`
|
||||
- ❌ `/new-workspace`, `/create-team`, `/accept-invite`
|
||||
|
||||
루트에 하이픈으로 연결된 단어 묶음은 사용자가 직접 고른 워크스페이스 slug와 충돌하며, 끝없는 예약어 slug 점검을 강요합니다. 명사(`workspaces`)를 예약해 두면 `/workspaces/*` 하위 트리 전체가 자동으로 보호됩니다.
|
||||
|
||||
### 워크스페이스 범위 라우트
|
||||
|
||||
항상 `/{slug}/{section}` 아래에 둡니다 — `/{slug}/issues`, `/{slug}/agents`, `/{slug}/settings`. 워크스페이스 라우팅 로직을 절대 중복하지 말고, 공유 코드에서는 프레임워크별 link API 대신 `useNavigation().push()`를 사용하세요.
|
||||
|
||||
### 패키지와 모듈
|
||||
|
||||
모노레포는 엄격한 패키지 경계를 강제합니다:
|
||||
|
||||
| 패키지 | 의존 가능 | 의존 금지 |
|
||||
| --- | --- | --- |
|
||||
| `packages/core` | 앱 종속적이지 않은 것만 | `react-dom`, `localStorage`, `process.env`, `next/*`, UI 라이브러리 |
|
||||
| `packages/ui` | 없음 | `@multica/core`, 비즈니스 로직 |
|
||||
| `packages/views` | `core/`, `ui/` | `next/*`, `react-router-dom`, stores |
|
||||
| `apps/web/platform/` | `next/*` | 다른 앱 |
|
||||
| `apps/desktop/.../platform/` | `react-router-dom`, electron | 다른 앱 |
|
||||
|
||||
두 앱 모두에 동일한 로직이 나타난다면 반드시 공유 패키지로 추출해야 합니다. "사소한" 중복이라는 예외는 없습니다.
|
||||
|
||||
### 파일과 컴포넌트
|
||||
|
||||
- 파일: `kebab-case.tsx` / `kebab-case.ts` (예: `agent-row-actions.tsx`)
|
||||
- 컴포넌트: `PascalCase` (예: `AgentRowActions`)
|
||||
- Hook: `useCamelCase` (예: `useWorkspaceId`)
|
||||
- 테스트: `<file>.test.ts(x)`로 같은 위치에 배치
|
||||
- Store (Zustand): `<feature>-store.ts`, `use<Feature>Store`로 export
|
||||
|
||||
### 데이터베이스 (Go + sqlc)
|
||||
|
||||
- 테이블: `snake_case` 단수 (`user`, `workspace`, `agent_runtime`)
|
||||
- 컬럼: `snake_case` (`workspace_id`, `created_at`, `last_seen_at`)
|
||||
- 외래 키: `<table>_id`
|
||||
- 불리언: `is_<state>` 또는 `<state>_at` (상태 변경에는 타임스탬프 형태 선호)
|
||||
- 마이그레이션 파일: `NNN_descriptive_name.up.sql` + `.down.sql` — 항상 양방향을 모두 제공
|
||||
|
||||
### Go
|
||||
|
||||
- 표준 `gofmt` + `go vet`. 예외 없음.
|
||||
- Handler 파일은 도메인을 반영: `agent.go`, `auth.go`, `runtime.go`
|
||||
- 테스트: `<file>_test.go`로 같은 위치에 배치
|
||||
- handler에서의 UUID 파싱은 루트 `CLAUDE.md`의 규칙을 따릅니다 — 경계 입력에는 `parseUUIDOrBadRequest`, 신뢰할 수 있는 왕복에는 `parseUUID`(panic 버전), 절대 error를 확인하지 않고 `util.ParseUUID`를 직접 사용하지 마세요.
|
||||
|
||||
### TypeScript
|
||||
|
||||
- 네트워크상의 API 응답은 `snake_case`이며, api client가 경계에서 `camelCase`로 변환합니다. TS 코드 내부에서는 **항상 camelCase**.
|
||||
- 타입: `PascalCase` (`Issue`, `AgentRuntime`); `IPrefix` 금지, `_t` 접미사 금지.
|
||||
- 열거형: string literal union을 선호하고, 런타임 순회가 필요한 경우에만 `enum`을 사용.
|
||||
- TanStack Query 키: `<feature>/queries.ts` 안의 팩토리 함수, 예: `issueKeys.detail(id)`.
|
||||
|
||||
### 이슈 키
|
||||
|
||||
모든 이슈에는 `MUL-123` 같은 사람이 읽을 수 있는 키가 있습니다: 워크스페이스 `issue_prefix`(대문자와 숫자, 보통 3자, 최대 10자) + 일련번호. 워크스페이스 admin은 Settings → General에서 접두사를 변경할 수 있는데, 변경하면 기존의 모든 이슈가 다시 번호 매겨지므로 옛 접두사가 박혀 있는 외부 참조(PR 제목, 브랜치 이름, 문서와 채팅 속 링크)는 더 이상 연결되지 않습니다.
|
||||
|
||||
### 코드 내 주석
|
||||
|
||||
영어만 사용합니다. 레포는 Go와 TypeScript 모두에 이를 강제합니다. 코드에서 중국어 주석을 발견하면 그것은 버그이니 교체하세요.
|
||||
|
||||
### 커밋 메시지
|
||||
|
||||
Conventional 형식: `feat(scope)`, `fix(scope)`, `refactor(scope)`, `docs`, `test(scope)`, `chore(scope)`. 의도별로 묶인 원자적 커밋.
|
||||
|
||||
---
|
||||
|
||||
## 2. i18n 번역 용어집
|
||||
|
||||
이것은 모든 번역 PR이 **반드시** 지켜야 하는 용어집입니다. 예전에는 `packages/views/locales/glossary.md`에 있었는데, 그 파일은 이제 이곳을 가리키는 stub입니다.
|
||||
|
||||
### 핵심 구분: 엔티티 vs 개념
|
||||
|
||||
Multica의 제품 명사는 두 가지 범주로 나뉩니다:
|
||||
|
||||
- **엔티티(Entity)** — URL, 데이터베이스 row, API 타입을 가집니다. 중국어 텍스트에서는 **소문자 영문**으로 표기하여 시각적으로 타입 이름처럼 읽히게 하고 "이것은 Multica 시스템 엔티티다"라는 신호를 줍니다.
|
||||
- **개념(Concept)** — 일반 명사이며, 데이터베이스 엔티티가 아닙니다. **완전히 번역**하여 중국어 사용자가 흐르는 텍스트 속에 들쭉날쭉한 영문을 보지 않도록 합니다.
|
||||
|
||||
이 규칙은 `apps/docs/content/docs/*.zh.mdx`와 정렬되어 있습니다 — 이 문서들은 사실상의 중국어 보이스 표준이며 20개 이상의 페이지에서 실전 검증되었습니다.
|
||||
|
||||
### 엔티티 — 혼합 규칙 (`issue` / `skill` / `task`)
|
||||
|
||||
`issue` / `skill` / `task`는 Multica의 핵심 엔티티입니다. 스키마 컬럼, API 필드, 제품 UI 레이블이 모두 영어입니다. 중국어 텍스트에서는 **혼합 규칙**을 따르며 — 무엇을 사용할지는 단어가 어디에 나타나는지에 따라 달라집니다:
|
||||
|
||||
| 맥락 | 표기 | 예시 |
|
||||
| --- | --- | --- |
|
||||
| **UI 문자열, 상태 이름, 코드 참조** | 소문자 영문 | "排队中的 task"、"创建子 issue"、"为智能体注入 skill" |
|
||||
| **문서 제목 / 섹션 헤딩** | Title-case 영문 **또는** 중국어 용어 | "Issue 与 project"、"Skills"、"执行任务" |
|
||||
| **장문 문서 산문에서 엔티티가 진행 중인 주어일 때** | 중국어 용어, 첫 언급 시 괄호 안에 영문 | "**执行任务**(task)是智能体每一次工作的单位" |
|
||||
| **API / DB 필드** | 항상 `task` / `issue` / `skill` | `task_id`, `issue_status`, `skill_uuid` |
|
||||
|
||||
중국어 용어 참고:
|
||||
|
||||
- `task` ↔ `执行任务` (맥락이 분명해지면 `任务`로 줄여서 사용)
|
||||
- `issue`에는 정착된 중국어 번역이 없습니다 — 영문 유지; 제목에서는 `Issue`처럼 대문자로 표기 가능
|
||||
- `skill`에는 정착된 중국어 번역이 없습니다 — 영문 유지; 제목에서는 `Skills`처럼 대문자로 표기 가능
|
||||
|
||||
**`issue` / `skill` / `task`가 `project` / `autopilot`처럼 중국어로 강제 번역되지 않는 이유**:
|
||||
|
||||
- **`issue` / `task`**: 개발 팀은 영어로 대화합니다. 중국어 후보들("任务" — 너무 모호하고 "工作"과 거의 동의어; "工单" — IT 티켓 어감; "议题" — GitHub 스타일이지만 제품 느낌과 맞지 않음)은 모두 `issue`보다 못하게 읽힙니다. **하지만** 장문 문서 산문에서 소문자 `task`를 50번 반복하면 리듬이 깨지므로 — 산문에서는 `执行任务`를 허용하되, UI 문자열과 상태 이름은 소문자 영문으로 유지합니다.
|
||||
- **`skill`**: 정착된 중국어 용어가 없는 Multica 고유 개념입니다.
|
||||
- **`project` → "项目"**: 정착된 주류 중국어 단어. Feishu / Tower / Teambition / PingCode / GitHub Projects — 모든 중국어 제품이 이를 번역합니다. 중국어 맥락에서 `project`를 그대로 두는 제품은 없습니다.
|
||||
- **`autopilot` → "自动化"**: 중국어에서 "autopilot"은 Tesla의 "自动驾驶"를 연상시키며, 이 기능이 하는 일(일정에 따라 task 실행)과 맞지 않습니다. Notion과 Feishu 모두 "自动化"를 사용하며, 그것이 업계 합의입니다.
|
||||
|
||||
### 번역하지 않음 — 브랜드와 약어
|
||||
|
||||
| 범주 | 용어 |
|
||||
| --- | --- |
|
||||
| 브랜드 | **Multica**, GitHub, Slack, Google, Anthropic, OpenAI, Claude, Codex, Cursor, Linear, Jira |
|
||||
| 약어 | API, CLI, URL, SDK, OAuth, JWT, SSO, WebSocket, HTTP, JSON, YAML, SQL |
|
||||
|
||||
### 완전히 번역 — 개념
|
||||
|
||||
| English | Chinese |
|
||||
| --- | --- |
|
||||
| Workspace | **工作区** |
|
||||
| Agent | **智能体** |
|
||||
| Project | **项目** |
|
||||
| Autopilot | **自动化** |
|
||||
| Daemon | **守护进程** |
|
||||
| Runtime | **运行时** |
|
||||
| Inbox | **收件箱** |
|
||||
| Comment | **评论** |
|
||||
| Reply | **回复** |
|
||||
| Notifications | **通知** |
|
||||
| Member | **成员** |
|
||||
| Label | **标签** |
|
||||
| Settings | **设置** |
|
||||
| Onboarding | **上手引导** |
|
||||
|
||||
### 완전히 번역 — 일반 UI 단어
|
||||
|
||||
| English | Chinese |
|
||||
| --- | --- |
|
||||
| Invite / Invitation | 邀请 |
|
||||
| Search | 搜索 |
|
||||
| Email | 邮箱 (label) / 邮件 (action) |
|
||||
| Password | 密码 |
|
||||
| Sign in / Log in | 登录 |
|
||||
| Sign up | 注册 |
|
||||
| Sign out / Log out | 退出登录 |
|
||||
| Save / Cancel / Delete | 保存 / 取消 / 删除 |
|
||||
| Confirm / Continue / Back | 确认 / 继续 / 返回 |
|
||||
| Edit / New / Create / Add | 编辑 / 新建 / 创建 / 添加 |
|
||||
| Remove / Send / Open / Close | 移除 / 发送 / 打开 / 关闭 |
|
||||
| Preview / Download / Upload | 预览 / 下载 / 上传 |
|
||||
| Done / Loading... | 完成 / 加载中... |
|
||||
| Profile / Account / Appearance | 个人资料 / 账号 / 外观 |
|
||||
| Theme / Language | 主题 / 语言 |
|
||||
| Light / Dark / System | 浅色 / 深色 / 跟随系统 |
|
||||
| Active / Archived | 活跃 (or 启用) / 已归档 |
|
||||
| Status / Priority | 状态 / 优先级 |
|
||||
| Assignee / Reporter | 负责人 / 报告人 |
|
||||
| Description / Title | 描述 / 标题 |
|
||||
| Date / Time | 日期 / 时间 |
|
||||
| Today / Yesterday / Tomorrow | 今天 / 昨天 / 明天 |
|
||||
| Empty / Failed / Success | 空 / 失败 / 成功 |
|
||||
| Error / Warning | 错误 / 警告 |
|
||||
|
||||
### 역할과 상태 열거형 (소문자 영문, 번역하지 않음)
|
||||
|
||||
이것들은 스키마 수준의 식별자입니다; 중국어 맥락에서도 소문자 영문으로 표기합니다.
|
||||
|
||||
- 역할: `owner` / `admin` / `member`
|
||||
- 이슈 상태: `backlog` / `todo` / `in_progress` / `in_review` / `done` / `blocked` / `cancelled`
|
||||
|
||||
UI에서는 이 값들을 영어로 표시합니다(필요 시 `code-style`로 감쌈):
|
||||
|
||||
- "你需要 owner 权限"
|
||||
- "已切换到 in_progress"
|
||||
|
||||
### 단어 조합 규칙
|
||||
|
||||
영문 단어(엔티티 / 브랜드 / 약어)와 주변 중국어 사이에는 항상 **단일 공백**을 둡니다:
|
||||
|
||||
- "Create new issue" → "新建 issue"
|
||||
- "Assign to agent" → "分配给智能体"
|
||||
- "Configure runtime" → "配置运行时"
|
||||
- "Stop daemon" → "停止守护进程"
|
||||
|
||||
### 복수형과 개수
|
||||
|
||||
i18next는 `_one` / `_other`를 사용합니다; 중국어에는 문법적 수가 없으므로 `_other`만 채웁니다.
|
||||
|
||||
```json
|
||||
// en/issues.json
|
||||
{
|
||||
"issue_count_one": "{{count}} issue",
|
||||
"issue_count_other": "{{count}} issues"
|
||||
}
|
||||
|
||||
// zh-Hans/issues.json
|
||||
{
|
||||
"issue_count_other": "{{count}} 个 issue"
|
||||
}
|
||||
```
|
||||
|
||||
일반적인 개수 형식:
|
||||
|
||||
- `{{count}} issues` → `{{count}} 个 issue`
|
||||
- `{{count}} agents` → `{{count}} 个智能体`
|
||||
- `{{count}} workspaces` → `{{count}} 个工作区`
|
||||
- `{{count}} comments` → `{{count}} 条评论`
|
||||
- `{{count}} members` → `{{count}} 位成员`
|
||||
- `{{count}} skills` → `{{count}} 个 skill`
|
||||
|
||||
### 보간
|
||||
|
||||
`{{var}}`를 사용합니다. 중국어 번역은 자연스러운 문장 흐름을 위해 순서를 재배치할 수 있습니다.
|
||||
|
||||
```json
|
||||
// en
|
||||
{ "welcome_message": "Welcome back, {{name}}!" }
|
||||
|
||||
// zh-Hans
|
||||
{ "welcome_message": "欢迎回来,{{name}}!" }
|
||||
```
|
||||
|
||||
### 번역 키 네이밍
|
||||
|
||||
3단계 중첩: `feature.component.action`.
|
||||
|
||||
```json
|
||||
{
|
||||
"feature_or_component": {
|
||||
"subcomponent_or_section": {
|
||||
"action_or_label": "..."
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
예시:
|
||||
|
||||
- `issues.toolbar.batch_update_success`
|
||||
- `issues.detail.comment_form.placeholder`
|
||||
- `inbox.empty.title`
|
||||
- `settings.preferences.language.title`
|
||||
|
||||
### Web 전용 / Desktop 전용 카피
|
||||
|
||||
- 공유 카피: namespace JSON의 최상위
|
||||
- Web 전용: `web` 섹션
|
||||
- Desktop 전용: `desktop` 섹션
|
||||
|
||||
정식 예시는 `auth.json`을 참고하세요(`web` 섹션에 `prefer_desktop` / `desktop_handoff.*`가 포함됨).
|
||||
|
||||
---
|
||||
|
||||
## 3. 중국어 보이스와 스타일
|
||||
|
||||
### 구두점
|
||||
|
||||
- 중국어에서는 전각 구두점 사용: `,。:;!?`
|
||||
- 따옴표: 영어 원문과 맞추기 위해 곧은 큰따옴표 `"..."`를 사용. `「」`나 둥근 따옴표는 사용하지 마세요.
|
||||
- 줄임표: 단일 문자 `…`가 아닌 세 개의 점 `...`. 영어 원문과 일치시키세요.
|
||||
- 중국어-영어 혼용: 영문 단어 양옆에 각각 단일 공백(단어 조합 규칙 참고).
|
||||
|
||||
### 스타일 원칙
|
||||
|
||||
- **간결하고 직접적으로.** 번역투 회피: "对于 X 来说"、"作为 X"、"我们的".
|
||||
- **오류 메시지**: 부드럽지만 명확하게. "无法保存修改"가 "保存修改失败了!"보다 낫습니다.
|
||||
- **버튼**: 동사를 먼저, 2~4자. "取消"、"保存修改"、"立即同步".
|
||||
- **툴팁**: 완결된 짧은 문장. "复制链接到剪贴板".
|
||||
- **플레이스홀더**: 예시 형태. "输入 issue 标题...".
|
||||
|
||||
### 막힐 때 참고할 곳
|
||||
|
||||
용어집이 특정 용어를 다루지 않을 때는 다음을 참고하세요:
|
||||
|
||||
1. `apps/docs/content/docs/*.zh.mdx` — 사실상의 중국어 보이스 표준, 일관된 번역 20개 이상 페이지
|
||||
2. `packages/views/locales/zh-Hans/auth.json`과 `editor.json` — JSON 구조 + selector API 패턴
|
||||
3. `packages/views/auth/login-page.tsx` — 컴포넌트 수준 selector API 호출 지점
|
||||
4. `packages/views/settings/components/preferences-tab.tsx` — 언어 전환기 참고
|
||||
|
||||
---
|
||||
|
||||
## 이 페이지를 업데이트할 때
|
||||
|
||||
이곳의 규칙을 변경하면 다음도 함께 수행하세요:
|
||||
|
||||
1. 관련 locale JSON / CLAUDE.md / 문서 페이지에 적용
|
||||
2. PR 설명에 변경 사항을 기록하여 리뷰어가 다운스트림 정리를 살펴보도록 알리기
|
||||
|
||||
이 페이지가 계약입니다; 다른 어떤 것도 이를 무시할 수 없습니다.
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"title": "開発者",
|
||||
"pages": ["conventions"]
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"title": "Developers",
|
||||
"pages": ["conventions"]
|
||||
}
|
||||
@@ -1,230 +0,0 @@
|
||||
---
|
||||
title: 環境変数
|
||||
description: セルフホストの Multica サーバーを実行するための環境変数の完全な一覧です。
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
|
||||
セルフホストの Multica [サーバー](/self-host-quickstart)は、起動時に環境変数から設定を読み込みます — データベース、サインイン、メール、ストレージ、サインアップ許可リストはすべてここにあります。このページでは、すべての変数を用途別にグループ化しています。各セクションでは、**設定しないと何が起きるか**、そして**プロダクションで必ず設定すべきものはどれか**を明確に説明します。auth 関連の変数を実際にどう設定するかについては、[サインインとサインアップの設定](/auth-setup)を参照してください。
|
||||
|
||||
## コアサーバー変数
|
||||
|
||||
デプロイ前に必ず検討すべきコア変数です — 一部はサーバーを起動できるようにするデフォルト値を持っていますが、プロダクションでは必須項目を明示的に設定すべきです。
|
||||
|
||||
| 変数 | デフォルト | プロダクションで必須? |
|
||||
|---|---|---|
|
||||
| `DATABASE_URL` | `postgres://multica:multica@localhost:5432/multica?sslmode=disable` | **はい** |
|
||||
| `PORT` | `8080` | いいえ(ポートを変更する場合を除く) |
|
||||
| `JWT_SECRET` | `multica-dev-secret-change-in-production` | **はい**(デフォルトは安全ではありません) |
|
||||
| `APP_ENV` | 空 | **はい**(`production` である必要があります) |
|
||||
| `FRONTEND_ORIGIN` | 空 | **はい**(セルフホストは自身のドメインを設定する必要があります) |
|
||||
| `MULTICA_DEV_VERIFICATION_CODE` | 空 | いいえ(プロダクションでは必ず空のままにしてください) |
|
||||
|
||||
<Callout type="warning">
|
||||
**プロダクションでは `MULTICA_DEV_VERIFICATION_CODE` を空のままにしてください。** 固定のローカルテストコードはデフォルトで無効になっていますが、`MULTICA_DEV_VERIFICATION_CODE=888888` で有効にすると、`APP_ENV` が production 以外の間は、コードを要求できる誰もがその固定値でサインインできてしまいます。このショートカットは `APP_ENV=production` のときには無視されます。
|
||||
</Callout>
|
||||
|
||||
### データベース接続プール
|
||||
|
||||
| 変数 | デフォルト | 説明 |
|
||||
|---|---|---|
|
||||
| `DATABASE_MAX_CONNS` | `25` | pgxpool の最大接続数。デーモンは頻繁に(3 秒ごとに)ポーリングして接続を使用するため、規模の大きいデプロイではより高い値が必要になる場合があります |
|
||||
| `DATABASE_MIN_CONNS` | `5` | 最小アイドル接続数 |
|
||||
|
||||
**設定しない場合**、上記の値が使われます — 以前プロダクションでプール枯渇を引き起こした pgx 組み込みの 4/NumCPU デフォルトでは**ありません**。
|
||||
|
||||
## メール設定
|
||||
|
||||
Multica は 2 つの配信バックエンドをサポートします — クラウドデプロイ向けの [Resend](https://resend.com/) と、内部 / オンプレミスネットワーク向けの SMTP relay です。両方が設定されている場合は `SMTP_HOST` が `RESEND_API_KEY` より優先されます。
|
||||
|
||||
### Resend
|
||||
|
||||
| 変数 | デフォルト | 説明 |
|
||||
|---|---|---|
|
||||
| `RESEND_API_KEY` | 空 | Resend API key |
|
||||
| `RESEND_FROM_EMAIL` | `noreply@multica.ai` | 送信元アドレス(Resend アカウントで検証済みのドメインである必要があり、SMTP を使用する場合も `From:` ヘッダーとして再利用されます) |
|
||||
|
||||
### SMTP relay
|
||||
|
||||
| 変数 | デフォルト | 説明 |
|
||||
|---|---|---|
|
||||
| `SMTP_HOST` | 空 | SMTP relay のホスト名。これを設定すると SMTP モードが有効になり、Resend を上書きします |
|
||||
| `SMTP_PORT` | `25` | SMTP ポート。STARTTLS サブミッションには `587` を、SMTPS(暗黙的 TLS、自動有効化)には `465` を使用します |
|
||||
| `SMTP_USERNAME` | 空 | SMTP ユーザー名。認証なしの relay の場合は空のままにしてください |
|
||||
| `SMTP_PASSWORD` | 空 | SMTP パスワード |
|
||||
| `SMTP_TLS` | `starttls` | TLS モード。`implicit`(別名 `smtps`、`ssl`)は接続時に即座に TLS ハンドシェイクを行います(SMTPS)。`465` ポートでは自動的に有効になります。未設定 / `starttls` の場合は接続後に STARTTLS でアップグレードします |
|
||||
| `SMTP_TLS_INSECURE` | `false` | TLS 証明書の検証をスキップするには `true` に設定(プライベート CA / 自己署名証明書のみ) |
|
||||
| `SMTP_EHLO_NAME` | マシンのホスト名 | relay に通知する EHLO/HELO 名。厳格な relay(例: Google Workspace `smtp-relay.gmail.com`)が公開 IP からのデフォルトの挨拶を拒否する場合は、実際の FQDN を設定してください — そうしないと relay が接続を切断し、後続のコマンドで不明瞭な `EOF` として表面化します |
|
||||
|
||||
サーバーが STARTTLS を通知すると自動的にアップグレードされます。dial タイムアウトは 10 秒で、SMTP セッション全体には 30 秒のデッドラインがあるため、ブラックホール化した relay が auth ハンドラーをハングさせることはできません。
|
||||
|
||||
**どちらも設定していない場合の動作**: サーバーはエラーを出しませんが、送信されるはずだったすべてのメール(検証コード、招待リンク)は**サーバーの stdout にのみ記録されます**。ローカル開発には便利です — サーバーログからコードをコピーして使ってください。**プロダクションでこれを設定し忘れると、静かなブラックホールが生まれ**、ユーザーはメールをまったく受け取れず、エラーも一切表面化しません。
|
||||
|
||||
## Google OAuth 設定
|
||||
|
||||
任意です。メール + 検証コードのみを使用する場合は設定しないままにし、サインインページに「Sign in with Google」を追加する場合は設定してください。
|
||||
|
||||
| 変数 | デフォルト | 説明 |
|
||||
|---|---|---|
|
||||
| `GOOGLE_CLIENT_ID` | 空 | Google Cloud OAuth client ID |
|
||||
| `GOOGLE_CLIENT_SECRET` | 空 | Google Cloud OAuth secret |
|
||||
| `GOOGLE_REDIRECT_URI` | `http://localhost:3000/auth/callback` | OAuth コールバック URL(セルフホスト: 自身のフロントエンドドメインに置き換えてください) |
|
||||
|
||||
**ランタイムで適用されます**: フロントエンドはこれらの設定をランタイムに `/api/config` 経由で読み込むため、**変更してもフロントエンドのリビルドや再デプロイは不要です** — サーバーを再起動すれば適用されます。
|
||||
|
||||
完全なセットアップ(Google Cloud Console の手順を含む)は[サインインとサインアップの設定](/auth-setup#google-oauth-configuration)にあります。
|
||||
|
||||
## ファイルストレージ設定
|
||||
|
||||
Multica はユーザーがアップロードした添付ファイル(コメント内の画像やファイル)を保存します。**S3 が推奨されます**。S3 が設定されていない場合はローカルディスクにフォールバックします。
|
||||
|
||||
### S3 / S3 互換ストレージ
|
||||
|
||||
| 変数 | デフォルト | 説明 |
|
||||
|---|---|---|
|
||||
| `S3_BUCKET` | 空 | **バケット名のみ**(例: `my-bucket`)。`.s3.<region>.amazonaws.com` サフィックスは含め**ないでください** — サーバーが `S3_BUCKET` + `S3_REGION` から公開ホストを構築します。これを設定すると S3 ストレージが有効になります |
|
||||
| `S3_REGION` | `us-west-2` | AWS リージョン。バケットの実際のリージョンと一致する必要があります — SDK 署名と公開 URL の構築の両方に使われます |
|
||||
| `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` | 空 | 静的な認証情報。両方を設定しない場合は AWS SDK のデフォルト認証情報チェーン(IAM role / 環境認証情報)が使われます |
|
||||
| `AWS_ENDPOINT_URL` | 空 | カスタムの S3 互換エンドポイント(例: [MinIO](https://min.io/))。これを設定すると path-style URL に切り替わります |
|
||||
| `ATTACHMENT_DOWNLOAD_MODE` | `auto` | 添付ファイルのダウンロード方式: `auto`、`cloudfront`、`presign`、`proxy`。`auto` では CloudFront が完全に設定されている場合は優先し、内部/プライベート endpoint host は server proxy、公開 S3 互換 endpoint は対応時に presigned GET を使います |
|
||||
| `ATTACHMENT_DOWNLOAD_URL_TTL` | `30m` | CloudFront signed URL と S3 presigned download URL の有効期間。Go duration 形式を受け付けます |
|
||||
|
||||
**`S3_BUCKET` を設定しない場合**: サーバーは起動時に `"S3_BUCKET not set, cloud upload disabled"` をログに記録し、すべてのアップロードはローカルディスクにフォールバックします。
|
||||
|
||||
**保存されるオブジェクト URL** は次の優先順位で構築されます。
|
||||
|
||||
1. `CLOUDFRONT_DOMAIN` が設定されている場合は `https://<CLOUDFRONT_DOMAIN>/<key>`。
|
||||
2. `AWS_ENDPOINT_URL` が設定されている場合は `<AWS_ENDPOINT_URL>/<S3_BUCKET>/<key>`(path-style)。
|
||||
3. `https://<S3_BUCKET>.s3.<S3_REGION>.amazonaws.com/<key>`(virtual-hosted-style)。`S3_BUCKET` にドットが含まれる場合、AWS が発行するワイルドカード TLS 証明書がドットを含むバケットホストを検証できないため、サーバーは `https://s3.<S3_REGION>.amazonaws.com/<S3_BUCKET>/<key>`(path-style)にフォールバックします。
|
||||
|
||||
API の `download_url` は、CloudFront 署名が設定されていない場合 `GET /api/attachments/{id}/download` を使います。この endpoint は安全な場合 CloudFront/S3 presigned URL にリダイレクトし、`http://rustfs:9000` のようなプライベート/内部 endpoint では server がストリーミングします。Docker/VPC 内部のオブジェクトストアでは `ATTACHMENT_DOWNLOAD_MODE=proxy` を明示できます。
|
||||
|
||||
### ローカルディスク(S3 が設定されていない場合)
|
||||
|
||||
| 変数 | デフォルト | 説明 |
|
||||
|---|---|---|
|
||||
| `LOCAL_UPLOAD_DIR` | `./data/uploads` | ローカルストレージのディレクトリ |
|
||||
| `LOCAL_UPLOAD_BASE_URL` | 空(相対パスを返します) | 公開 base URL — 設定しないとフロントエンドが添付ファイルの完全な URL を解決できません |
|
||||
|
||||
### CloudFront(任意)
|
||||
|
||||
S3 の前段に CloudFront を置く場合、3 つの変数が適用されます: `CLOUDFRONT_DOMAIN`、`CLOUDFRONT_KEY_PAIR_ID`、`CLOUDFRONT_PRIVATE_KEY`(または Secrets Manager から読み込むには `CLOUDFRONT_PRIVATE_KEY_SECRET`)。CloudFront を使わない場合はスキップしてください — S3 設定とは競合しません。
|
||||
|
||||
### Cookie ドメイン
|
||||
|
||||
| 変数 | デフォルト | 説明 |
|
||||
|---|---|---|
|
||||
| `COOKIE_DOMAIN` | 空 | セッション cookie のスコープ |
|
||||
|
||||
- **空**: cookie は訪問した正確なホストでのみ有効です(単一ホストのデプロイに適切)
|
||||
- **`.example.com` に設定**: cookie がサブドメイン間で共有されます(そのため `app.example.com` と `api.example.com` がサインインセッションを共有します)
|
||||
- 警告: IP アドレスにはできません(ブラウザは無視します)
|
||||
|
||||
## 誰がサインアップできるかを制限する
|
||||
|
||||
3 つの許可リストの層が優先順位に従って組み合わされます。**いずれか 1 つの層でも空でない値に設定されると、一致しないメールは拒否されます** — `ALLOW_SIGNUP=true` でさえこれを上書きできません。
|
||||
|
||||
| 変数 | デフォルト | 説明 |
|
||||
|---|---|---|
|
||||
| `ALLOWED_EMAILS` | 空 | 明示的なメール許可リスト(カンマ区切り)。空でない場合、リストにあるメールのみがサインアップできます |
|
||||
| `ALLOWED_EMAIL_DOMAINS` | 空 | ドメイン許可リスト(カンマ区切り)。空でない場合、リストにあるドメインのみがサインアップできます |
|
||||
| `ALLOW_SIGNUP` | `true` | サインアップのマスタースイッチ。サインアップを完全に無効にするには `false` に設定 |
|
||||
|
||||
**直感に反する部分**: `ALLOWED_EMAIL_DOMAINS=company.io` + `ALLOW_SIGNUP=true` は「company.io または全員を許可」という意味では**なく**、**company.io のみを許可**という意味です。許可リストの層は AND セマンティクスです — 完全な決定木は[サインインとサインアップの設定 → サインアップ許可リスト](/auth-setup#restricting-who-can-sign-up)にあります。
|
||||
|
||||
**招待フロー自体はサインアップ許可リストをチェックしません** — ただし、招待された人は招待を承諾する前に依然として**サインイン**できる必要があります。すでに Multica アカウントを持っている場合(例: 別のワークスペースから)、許可リストの影響を受けずに直接承諾できます。**一度もサインアップしたことがない場合**、サインインの最初のステップ(検証コードの要求)は依然として許可リストのチェックを通過し、`ALLOW_SIGNUP=false` や `ALLOWED_EMAILS` / `ALLOWED_EMAIL_DOMAINS` によって拒否されたメールは**サインアップを完了できず、したがって招待を承諾できません**。
|
||||
|
||||
## ワークスペース作成をロックダウンする
|
||||
|
||||
`ALLOW_SIGNUP=false` は新しいアカウントをブロックしますが、すでにサインイン済みのユーザーが `POST /api/workspaces` 経由で別のワークスペースを作成することは**ブロックしません**。すべてのイシュー、リポジトリ、エージェントがプラットフォーム管理者に見えなければならないセルフホストインスタンスでは、そのギャップを塞ぐために `DISABLE_WORKSPACE_CREATION=true` を設定してください。
|
||||
|
||||
| 変数 | デフォルト | 説明 |
|
||||
|---|---|---|
|
||||
| `DISABLE_WORKSPACE_CREATION` | `false` | `true` の場合、`POST /api/workspaces` へのすべての呼び出しが `403 workspace creation is disabled for this instance` を返します。Web UI は `/api/config` 経由ですべての「ワークスペース作成」要素を非表示にします。役割 / owner の例外はありません — このゲートはインスタンス単位で全体に適用されます |
|
||||
|
||||
推奨されるブートストラップ手順:
|
||||
|
||||
1. `DISABLE_WORKSPACE_CREATION` を設定しないまま(デフォルト)インスタンスを起動します。
|
||||
2. 管理者としてサインインし、共有ワークスペースを作成します。
|
||||
3. `DISABLE_WORKSPACE_CREATION=true` を設定してバックエンドを再起動します。この時点から、ユーザーは招待によってのみ参加できます。
|
||||
|
||||
招待されたユーザーが最初の検証コードでサインアップを完了できるよう `ALLOW_SIGNUP=true` を維持したい場合は、`DISABLE_WORKSPACE_CREATION=true` を `ALLOWED_EMAIL_DOMAINS` / `ALLOWED_EMAILS` と組み合わせて、どのアドレスがサインアップできるかの範囲を指定してください。`ALLOW_SIGNUP=false` を設定すると、保留中の招待対象者がアカウントを作成すること自体も追加でブロックされます — すべてのメンバーがすでに Multica アカウントを持っているインスタンスでのみ有用です。
|
||||
|
||||
## レート制限(任意の Redis)
|
||||
|
||||
公開 auth エンドポイント — `/auth/send-code`、`/auth/verify-code`、`/auth/google` — の前段には、IP ごとの固定ウィンドウのレート制限があります。リミッターは Redis によって支えられています。`REDIS_URL` を設定しない場合、ミドルウェアは **no-op**(fail-open)になり、バックエンドは起動時に `rate limiting disabled: REDIS_URL not configured` をログに記録します。
|
||||
|
||||
| 変数 | デフォルト | 説明 |
|
||||
|---|---|---|
|
||||
| `REDIS_URL` | 空 | Redis 接続 URL(例: `redis://localhost:6379/0`)。設定しないと auth エンドポイントのレート制限が無効になります。同じ Redis はリアルタイムハブの fan-out、PAT キャッシュ、デーモントークンキャッシュでも使われます — 設定しない場合はすべてインメモリ / 直接 DB モードにフォールバックします |
|
||||
| `RATE_LIMIT_AUTH` | `5` | `/auth/send-code` および `/auth/google` に対する IP あたり毎分の最大リクエスト数 |
|
||||
| `RATE_LIMIT_AUTH_VERIFY` | `20` | `/auth/verify-code` に対する IP あたり毎分の最大リクエスト数 |
|
||||
| `RATE_LIMIT_TRUSTED_PROXIES` | 空 | リミッターがその `X-Forwarded-For` ヘッダーを信頼することを許可する、カンマ区切りの CIDR。空(デフォルト)は **XFF を決して信頼しない**ことを意味します — リミッターは直接接続の `RemoteAddr` のみを使用します |
|
||||
|
||||
リクエストが制限を超えると、サーバーは `429 Too Many Requests`、`Retry-After: 60`、そして本文 `{"error":"too many requests"}` で応答します。
|
||||
|
||||
<Callout type="warning">
|
||||
**リバースプロキシの背後では `RATE_LIMIT_TRUSTED_PROXIES` を必ず設定する必要があります。** そうしないと、バックエンドの観点ではすべての実際のユーザーがプロキシの IP を共有することになり、デプロイ全体が 1 つのバケットに入り、`/auth/send-code` がサイト全体で毎分 5 リクエストになってしまいます。一般的な値: 同一ホストの Caddy / Nginx には `127.0.0.1/32,::1/128`、Cloudflare / ALB / CloudFront には該当 CDN が公開している IP 範囲。`RemoteAddr` がこれらの CIDR のいずれかに含まれる IP のみが、`X-Forwarded-For` を使ってクライアントを識別できます。
|
||||
</Callout>
|
||||
|
||||
この独立した `RATE_LIMIT_TRUSTED_PROXIES` は、オートパイロット webhook リミッター(`/api/webhooks/autopilots/{token}`)を制御する `MULTICA_TRUSTED_PROXIES` とは**異なります**。各リミッターは自身のリストをパースするため、プロキシの背後にあるデプロイは両方を設定すべきです。
|
||||
|
||||
## デーモンのチューニングパラメータ
|
||||
|
||||
デーモンはユーザーのローカルマシン上で実行され、その設定もローカル環境変数から読み込まれます。一般的なものは次のとおりです。
|
||||
|
||||
| 変数 | デフォルト | 説明 |
|
||||
|---|---|---|
|
||||
| `MULTICA_SERVER_URL` | `ws://localhost:8080/ws` | サーバーアドレス(セルフホスト: 自身のドメインに置き換えてください) |
|
||||
| `MULTICA_DAEMON_HEARTBEAT_INTERVAL` | `15s` | ハートビート間隔 |
|
||||
| `MULTICA_DAEMON_POLL_INTERVAL` | `3s` | タスクのポーリング間隔 |
|
||||
| `MULTICA_DAEMON_MAX_CONCURRENT_TASKS` | `20` | 最大同時タスク数 |
|
||||
| `MULTICA_<PROVIDER>_PATH` | CLI 名に一致 | 各 AI コーディングツールの実行ファイルへのパス(例: `MULTICA_CLAUDE_PATH`) |
|
||||
| `MULTICA_<PROVIDER>_MODEL` | 空 | 各 AI コーディングツールのデフォルトモデル |
|
||||
|
||||
各パラメータがデーモンの動作にどう影響するかの完全な説明は、[デーモンとランタイム](/daemon-runtimes)を参照してください。
|
||||
|
||||
## フロントエンドのアクセス制御
|
||||
|
||||
| 変数 | デフォルト | 説明 |
|
||||
|---|---|---|
|
||||
| `FRONTEND_ORIGIN` | 空 | フロントエンドアドレス。招待メールのリンク、CORS 許可リスト、cookie ドメインはすべてこの値から派生します。設定しない場合、招待メールのリンクはホスト型ドメイン `https://app.multica.ai` にフォールバックします — セルフホストはこれを明示的に設定する必要があります |
|
||||
| `CORS_ALLOWED_ORIGINS` | 空 | 追加で許可する CORS origin(カンマ区切り) |
|
||||
| `ALLOWED_ORIGINS` | 空 | WebSocket 専用の origin 許可リスト(カンマ区切り)。設定しない場合、フォールバック順序は `CORS_ALLOWED_ORIGINS` → `FRONTEND_ORIGIN` → `localhost:3000/5173/5174` です |
|
||||
|
||||
<Callout type="warning">
|
||||
**`FRONTEND_ORIGIN` を設定しないと 2 つの静かな失敗が発生します**: (1) 招待メールのリンクが `https://app.multica.ai`(ホスト型ドメイン)を指し、クリックしてもユーザーがセルフホストインスタンスに戻ってこない。(2) WebSocket の Origin チェックが `localhost:3000 / 5173 / 5174` にフォールバックするため、プロダクションデプロイのすべての WebSocket 接続が拒否され、フロントエンドが「リアルタイム更新を受け取れない」ように見える。
|
||||
</Callout>
|
||||
|
||||
## GitHub 連携
|
||||
|
||||
[GitHub PR ↔ イシュー連携](/github-integration)には 2 つの変数が必要です。設定で Connect GitHub を有効にし、受信 webhook を受け付けるには両方を設定してください。
|
||||
|
||||
| 変数 | デフォルト | 説明 |
|
||||
|---|---|---|
|
||||
| `GITHUB_APP_SLUG` | 空 | GitHub App の slug(`https://github.com/apps/<slug>` の末尾部分)。設定 → GitHub のインストールボタン URL を構成します |
|
||||
| `GITHUB_WEBHOOK_SECRET` | 空 | GitHub App に設定した Webhook secret。すべての `pull_request` / `installation` delivery の HMAC-SHA256 検証に使われ、setup コールバックの state token の HMAC キーとしても使われます |
|
||||
|
||||
**どちらかが設定されていない場合の動作:**
|
||||
|
||||
- 設定 → GitHub の `Connect GitHub` が**無効**になり、admin に「not configured」というヒントを表示します。
|
||||
- `/api/webhooks/github` エンドポイントは **`503 github webhooks not configured`** を返します — Multica はすべての署名を有効として扱うのではなく、secret なしではイベント処理を拒否します。
|
||||
|
||||
**注:** `GITHUB_WEBHOOK_SECRET` はインストールフローの state token の署名キーとして再利用されるため、運用者は secret を 1 つだけ管理すればよいです。これは GitHub App の *Client* secret では**ありません** — Client secret は OAuth 関連であり、この連携では使われません。完全な手順は [GitHub 連携 → セルフホストのセットアップ](/github-integration#self-host-setup)を参照してください。
|
||||
|
||||
## 使用量分析
|
||||
|
||||
デフォルトでは、サーバーは Multica の公式 PostHog インスタンスにレポートします。オプトアウトするには `ANALYTICS_DISABLED=true` を設定してください。
|
||||
|
||||
| 変数 | デフォルト | 説明 |
|
||||
|---|---|---|
|
||||
| `ANALYTICS_DISABLED` | `false` | バックエンド分析を完全に無効にするには `true` に設定 |
|
||||
| `POSTHOG_API_KEY` | 組み込みのデフォルトキー | 自身の PostHog インスタンスを指す場合に設定 |
|
||||
| `POSTHOG_HOST` | `https://us.i.posthog.com` | PostHog をセルフホストする場合は自身のホストに変更 |
|
||||
|
||||
## 次へ
|
||||
|
||||
- [サインインとサインアップの設定](/auth-setup) — 上記の auth 関連変数を実際にどう設定するか、そして落とし穴がどこにあるか
|
||||
- [GitHub 連携](/github-integration) — `GITHUB_APP_SLUG` / `GITHUB_WEBHOOK_SECRET` を支える GitHub App をどうセットアップするか
|
||||
- [トラブルシューティング](/troubleshooting) — よくある設定ミスの症状と対処法
|
||||
- [デーモンとランタイム](/daemon-runtimes) — `MULTICA_DAEMON_*` パラメータが実際に何をするか
|
||||
@@ -1,230 +0,0 @@
|
||||
---
|
||||
title: 환경 변수
|
||||
description: 자체 호스팅 Multica 서버를 실행하기 위한 환경 변수 전체 목록입니다.
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
|
||||
자체 호스팅 Multica [서버](/self-host-quickstart)는 시작 시 환경 변수에서 설정을 읽습니다 — 데이터베이스, 로그인, 이메일, 스토리지, 가입 허용 목록이 모두 여기에 있습니다. 이 페이지는 모든 변수를 용도별로 그룹화합니다: 각 섹션은 **설정하지 않으면 어떻게 되는지**, 그리고 **프로덕션에서 반드시 설정해야 하는 변수가 무엇인지**를 명확히 설명합니다. auth 관련 변수를 실제로 어떻게 설정하는지는 [로그인 및 가입 설정](/auth-setup)을 참고하세요.
|
||||
|
||||
## 핵심 서버 변수
|
||||
|
||||
배포 전에 반드시 고려해야 하는 핵심 변수입니다 — 일부는 서버를 시작할 수 있게 해주는 기본값을 가지고 있지만, 프로덕션에서는 필수 항목을 명시적으로 설정해야 합니다.
|
||||
|
||||
| 변수 | 기본값 | 프로덕션 필수? |
|
||||
|---|---|---|
|
||||
| `DATABASE_URL` | `postgres://multica:multica@localhost:5432/multica?sslmode=disable` | **예** |
|
||||
| `PORT` | `8080` | 아니오 (포트를 변경하는 경우 제외) |
|
||||
| `JWT_SECRET` | `multica-dev-secret-change-in-production` | **예** (기본값은 안전하지 않음) |
|
||||
| `APP_ENV` | 비어 있음 | **예** (`production`이어야 함) |
|
||||
| `FRONTEND_ORIGIN` | 비어 있음 | **예** (자체 호스팅은 자체 도메인을 설정해야 함) |
|
||||
| `MULTICA_DEV_VERIFICATION_CODE` | 비어 있음 | 아니오 (프로덕션에서는 반드시 비워 두어야 함) |
|
||||
|
||||
<Callout type="warning">
|
||||
**프로덕션에서는 `MULTICA_DEV_VERIFICATION_CODE`를 비워 두세요.** 고정된 로컬 테스트 코드는 기본적으로 비활성화되어 있지만, `MULTICA_DEV_VERIFICATION_CODE=888888`로 활성화하면 `APP_ENV`가 production이 아닌 동안에는 코드를 요청할 수 있는 누구나 그 고정 값으로 로그인할 수 있습니다. 이 단축 코드는 `APP_ENV=production`일 때 무시됩니다.
|
||||
</Callout>
|
||||
|
||||
### 데이터베이스 커넥션 풀
|
||||
|
||||
| 변수 | 기본값 | 설명 |
|
||||
|---|---|---|
|
||||
| `DATABASE_MAX_CONNS` | `25` | pgxpool 최대 연결 수. 데몬은 자주(3초마다) 폴링하며 연결을 사용하므로, 규모가 큰 배포에서는 더 높은 값이 필요할 수 있습니다 |
|
||||
| `DATABASE_MIN_CONNS` | `5` | 최소 유휴 연결 수 |
|
||||
|
||||
**설정하지 않으면** 위 값이 사용됩니다 — 이전에 프로덕션에서 풀 고갈을 일으켰던 pgx 내장 기본값 4/NumCPU가 **아닙니다**.
|
||||
|
||||
## 이메일 설정
|
||||
|
||||
Multica는 두 가지 전송 백엔드를 지원합니다 — 클라우드 배포용 [Resend](https://resend.com/), 또는 내부 / 온프레미스 네트워크용 SMTP relay. 둘 다 설정된 경우 `SMTP_HOST`가 `RESEND_API_KEY`보다 우선합니다.
|
||||
|
||||
### Resend
|
||||
|
||||
| 변수 | 기본값 | 설명 |
|
||||
|---|---|---|
|
||||
| `RESEND_API_KEY` | 비어 있음 | Resend API key |
|
||||
| `RESEND_FROM_EMAIL` | `noreply@multica.ai` | 발신 주소 (Resend 계정에서 검증된 도메인이어야 하며, SMTP를 사용할 때도 `From:` 헤더로 재사용됨) |
|
||||
|
||||
### SMTP relay
|
||||
|
||||
| 변수 | 기본값 | 설명 |
|
||||
|---|---|---|
|
||||
| `SMTP_HOST` | 비어 있음 | SMTP relay 호스트명. 이를 설정하면 SMTP 모드가 활성화되고 Resend를 덮어씁니다 |
|
||||
| `SMTP_PORT` | `25` | SMTP 포트. STARTTLS 제출에는 `587`을, SMTPS(암묵적 TLS, 자동 활성화)에는 `465`를 사용하세요 |
|
||||
| `SMTP_USERNAME` | 비어 있음 | SMTP 사용자명. 인증 없는 relay의 경우 비워 두세요 |
|
||||
| `SMTP_PASSWORD` | 비어 있음 | SMTP 비밀번호 |
|
||||
| `SMTP_TLS` | `starttls` | TLS 모드. `implicit`(별칭 `smtps`, `ssl`)은 연결 시 즉시 TLS 핸드셰이크를 수행합니다(SMTPS). `465` 포트에서는 자동으로 활성화됩니다. 미설정 / `starttls`는 연결 후 STARTTLS로 업그레이드합니다 |
|
||||
| `SMTP_TLS_INSECURE` | `false` | TLS 인증서 검증을 건너뛰려면 `true`로 설정 (사설 CA / 자체 서명 인증서만 해당) |
|
||||
| `SMTP_EHLO_NAME` | 머신 호스트명 | relay에 알리는 EHLO/HELO 이름. 엄격한 relay(예: Google Workspace `smtp-relay.gmail.com`)가 공개 IP에서 보내는 기본 greeting을 거부하는 경우 실제 FQDN을 설정하세요 — 그렇지 않으면 relay가 연결을 끊고, 이는 이후 명령에서 불투명한 `EOF`로 나타납니다 |
|
||||
|
||||
서버가 STARTTLS를 알리면 자동으로 업그레이드됩니다. dial 타임아웃은 10초이고 전체 SMTP 세션에는 30초 데드라인이 있어, 블랙홀이 된 relay가 auth 핸들러를 멈추게 할 수 없습니다.
|
||||
|
||||
**둘 다 설정하지 않았을 때의 동작**: 서버는 오류를 내지 않지만, 전송되어야 했던 모든 이메일(인증 코드, 초대 링크)은 **서버의 stdout에만 기록됩니다**. 로컬 개발에는 편리합니다 — 서버 로그에서 코드를 복사해 사용하세요; **프로덕션에서는 이를 설정하는 것을 잊으면 조용한 블랙홀이 되어**, 사용자는 이메일을 전혀 받지 못하고 아무런 오류도 드러나지 않습니다.
|
||||
|
||||
## Google OAuth 설정
|
||||
|
||||
선택 사항입니다. 이메일 + 인증 코드만 사용하려면 설정하지 않은 채로 두고, 로그인 페이지에 "Sign in with Google"을 추가하려면 설정하세요.
|
||||
|
||||
| 변수 | 기본값 | 설명 |
|
||||
|---|---|---|
|
||||
| `GOOGLE_CLIENT_ID` | 비어 있음 | Google Cloud OAuth client ID |
|
||||
| `GOOGLE_CLIENT_SECRET` | 비어 있음 | Google Cloud OAuth secret |
|
||||
| `GOOGLE_REDIRECT_URI` | `http://localhost:3000/auth/callback` | OAuth 콜백 URL (자체 호스팅: 자신의 프론트엔드 도메인으로 교체) |
|
||||
|
||||
**런타임에 적용됨**: 프론트엔드는 런타임에 `/api/config`를 통해 이 설정을 읽으므로, **변경해도 프론트엔드 리빌드나 재배포가 필요 없습니다** — 서버를 재시작하면 적용됩니다.
|
||||
|
||||
전체 설정(Google Cloud Console 단계 포함)은 [로그인 및 가입 설정](/auth-setup#google-oauth-configuration)에 있습니다.
|
||||
|
||||
## 파일 스토리지 설정
|
||||
|
||||
Multica는 사용자가 업로드한 첨부 파일(댓글의 이미지와 파일)을 저장합니다. **S3가 권장됩니다**; S3가 설정되어 있지 않으면 로컬 디스크로 폴백합니다.
|
||||
|
||||
### S3 / S3 호환 스토리지
|
||||
|
||||
| 변수 | 기본값 | 설명 |
|
||||
|---|---|---|
|
||||
| `S3_BUCKET` | 비어 있음 | **버킷 이름만** (예: `my-bucket`). `.s3.<region>.amazonaws.com` 접미사를 포함하지 **마세요** — 서버가 `S3_BUCKET` + `S3_REGION`으로 공개 호스트를 구성합니다. 이를 설정하면 S3 스토리지가 활성화됩니다 |
|
||||
| `S3_REGION` | `us-west-2` | AWS 리전. 버킷의 실제 리전과 일치해야 합니다 — SDK 서명과 공개 URL 구성 모두에 사용됩니다 |
|
||||
| `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` | 비어 있음 | 정적 자격 증명. 둘 다 설정하지 않으면 AWS SDK 기본 자격 증명 체인(IAM role / 환경 자격 증명)이 사용됩니다 |
|
||||
| `AWS_ENDPOINT_URL` | 비어 있음 | 사용자 정의 S3 호환 엔드포인트 (예: [MinIO](https://min.io/)). 이를 설정하면 path-style URL로 전환됩니다 |
|
||||
| `ATTACHMENT_DOWNLOAD_MODE` | `auto` | 첨부 파일 다운로드 방식: `auto`, `cloudfront`, `presign`, `proxy`. `auto`에서는 CloudFront가 완전히 설정되어 있으면 우선 사용하고, 내부/프라이빗 endpoint host는 server proxy를, 공개 S3 호환 endpoint는 지원되는 경우 presigned GET을 사용합니다 |
|
||||
| `ATTACHMENT_DOWNLOAD_URL_TTL` | `30m` | CloudFront signed URL 및 S3 presigned download URL의 유효 기간. Go duration 형식을 받습니다 |
|
||||
|
||||
**`S3_BUCKET`을 설정하지 않으면**: 서버는 시작 시 `"S3_BUCKET not set, cloud upload disabled"`를 로깅하고, 모든 업로드는 로컬 디스크로 폴백합니다.
|
||||
|
||||
**저장된 객체 URL**은 다음 우선순위 순서로 구성됩니다:
|
||||
|
||||
1. `CLOUDFRONT_DOMAIN`이 설정된 경우 `https://<CLOUDFRONT_DOMAIN>/<key>`.
|
||||
2. `AWS_ENDPOINT_URL`이 설정된 경우 `<AWS_ENDPOINT_URL>/<S3_BUCKET>/<key>` (path-style).
|
||||
3. `https://<S3_BUCKET>.s3.<S3_REGION>.amazonaws.com/<key>` (virtual-hosted-style). `S3_BUCKET`에 점이 포함된 경우, AWS가 발급한 와일드카드 TLS 인증서가 점이 포함된 버킷 호스트를 검증하지 못하므로 서버는 `https://s3.<S3_REGION>.amazonaws.com/<S3_BUCKET>/<key>` (path-style)로 폴백합니다.
|
||||
|
||||
API `download_url` 값은 CloudFront 서명이 설정되지 않은 경우 `GET /api/attachments/{id}/download`를 사용합니다. 이 endpoint는 안전한 경우 CloudFront/S3 presigned URL로 리디렉션하고, `http://rustfs:9000` 같은 프라이빗/내부 endpoint에서는 server가 스트리밍합니다. Docker/VPC 내부 객체 저장소에서는 `ATTACHMENT_DOWNLOAD_MODE=proxy`를 명시할 수 있습니다.
|
||||
|
||||
### 로컬 디스크 (S3가 설정되지 않은 경우)
|
||||
|
||||
| 변수 | 기본값 | 설명 |
|
||||
|---|---|---|
|
||||
| `LOCAL_UPLOAD_DIR` | `./data/uploads` | 로컬 스토리지 디렉터리 |
|
||||
| `LOCAL_UPLOAD_BASE_URL` | 비어 있음 (상대 경로 반환) | 공개 base URL — 설정하지 않으면 프론트엔드가 첨부 파일의 전체 URL을 확인할 수 없습니다 |
|
||||
|
||||
### CloudFront (선택)
|
||||
|
||||
S3 앞에 CloudFront를 두는 경우 세 가지 변수가 적용됩니다: `CLOUDFRONT_DOMAIN`, `CLOUDFRONT_KEY_PAIR_ID`, `CLOUDFRONT_PRIVATE_KEY` (또는 Secrets Manager에서 읽으려면 `CLOUDFRONT_PRIVATE_KEY_SECRET`). CloudFront를 사용하지 않으면 건너뛰세요 — S3 설정과 충돌하지 않습니다.
|
||||
|
||||
### Cookie 도메인
|
||||
|
||||
| 변수 | 기본값 | 설명 |
|
||||
|---|---|---|
|
||||
| `COOKIE_DOMAIN` | 비어 있음 | 세션 cookie의 범위 |
|
||||
|
||||
- **비어 있음**: cookie는 방문한 정확한 호스트에서만 유효합니다 (단일 호스트 배포에 적합)
|
||||
- **`.example.com`으로 설정**: cookie가 하위 도메인 간에 공유됩니다 (그래서 `app.example.com`과 `api.example.com`이 로그인 세션을 공유함)
|
||||
- 경고: IP 주소일 수 없습니다 (브라우저가 무시함)
|
||||
|
||||
## 누가 가입할 수 있는지 제한하기
|
||||
|
||||
세 개의 허용 목록 계층이 우선순위에 따라 결합됩니다. **어느 한 계층이라도 비어 있지 않은 값으로 설정되면, 일치하지 않는 이메일은 거부됩니다** — `ALLOW_SIGNUP=true`조차 이를 무시할 수 없습니다.
|
||||
|
||||
| 변수 | 기본값 | 설명 |
|
||||
|---|---|---|
|
||||
| `ALLOWED_EMAILS` | 비어 있음 | 명시적 이메일 허용 목록 (쉼표로 구분). 비어 있지 않으면 목록에 있는 이메일만 가입할 수 있습니다 |
|
||||
| `ALLOWED_EMAIL_DOMAINS` | 비어 있음 | 도메인 허용 목록 (쉼표로 구분). 비어 있지 않으면 목록에 있는 도메인만 가입할 수 있습니다 |
|
||||
| `ALLOW_SIGNUP` | `true` | 가입 마스터 스위치. 가입을 완전히 비활성화하려면 `false`로 설정 |
|
||||
|
||||
**직관에 반하는 부분**: `ALLOWED_EMAIL_DOMAINS=company.io` + `ALLOW_SIGNUP=true`는 "company.io 또는 모두를 허용"한다는 뜻이 **아니라** **company.io만 허용**한다는 뜻입니다. 허용 목록 계층은 AND 시맨틱입니다 — 전체 결정 트리는 [로그인 및 가입 설정 → 가입 허용 목록](/auth-setup#restricting-who-can-sign-up)에 있습니다.
|
||||
|
||||
**초대 흐름 자체는 가입 허용 목록을 확인하지 않습니다** — 하지만 초대받은 사람은 초대를 수락하기 전에 여전히 **로그인**할 수 있어야 합니다. 이미 Multica 계정이 있다면(예: 다른 워크스페이스에서) 허용 목록의 영향을 받지 않고 바로 수락할 수 있습니다; **한 번도 가입한 적이 없다면**, 로그인의 첫 단계(인증 코드 요청)는 여전히 허용 목록 확인을 거치며, `ALLOW_SIGNUP=false`나 `ALLOWED_EMAILS` / `ALLOWED_EMAIL_DOMAINS`에 의해 거부된 이메일은 **가입을 완료할 수 없고, 따라서 초대를 수락할 수 없습니다**.
|
||||
|
||||
## 워크스페이스 생성 잠그기
|
||||
|
||||
`ALLOW_SIGNUP=false`는 새 계정을 차단하지만, 이미 로그인한 사용자가 `POST /api/workspaces`를 통해 또 다른 워크스페이스를 생성하는 것은 **차단하지 않습니다**. 모든 이슈, 저장소, 에이전트가 플랫폼 관리자에게 보여야 하는 자체 호스팅 인스턴스에서는, 그 틈을 막기 위해 `DISABLE_WORKSPACE_CREATION=true`로 설정하세요.
|
||||
|
||||
| 변수 | 기본값 | 설명 |
|
||||
|---|---|---|
|
||||
| `DISABLE_WORKSPACE_CREATION` | `false` | `true`이면 `POST /api/workspaces`에 대한 모든 호출이 `403 workspace creation is disabled for this instance`를 반환합니다. 웹 UI는 `/api/config`를 통해 모든 "워크스페이스 생성" 요소를 숨깁니다. 역할/owner 예외는 없습니다 — 이 게이트는 인스턴스 단위로 전역 적용됩니다 |
|
||||
|
||||
권장 부트스트랩 순서:
|
||||
|
||||
1. `DISABLE_WORKSPACE_CREATION`을 설정하지 않은 채로(기본값) 인스턴스를 시작합니다.
|
||||
2. 관리자로 로그인하여 공유 워크스페이스를 생성합니다.
|
||||
3. `DISABLE_WORKSPACE_CREATION=true`로 설정하고 백엔드를 재시작합니다. 이 시점부터 사용자는 초대를 통해서만 참여할 수 있습니다.
|
||||
|
||||
초대받은 사용자가 첫 인증 코드로 가입을 완료할 수 있도록 `ALLOW_SIGNUP=true`를 유지하고 싶다면, `DISABLE_WORKSPACE_CREATION=true`를 `ALLOWED_EMAIL_DOMAINS` / `ALLOWED_EMAILS`와 결합하여 어떤 주소가 가입할 수 있는지 범위를 지정하세요. `ALLOW_SIGNUP=false`로 설정하면 대기 중인 초대 대상자가 계정을 만드는 것조차 추가로 차단됩니다 — 모든 멤버가 이미 Multica 계정을 가지고 있는 인스턴스에서만 유용합니다.
|
||||
|
||||
## 속도 제한 (선택적 Redis)
|
||||
|
||||
공개 auth 엔드포인트 — `/auth/send-code`, `/auth/verify-code`, `/auth/google` — 앞에는 IP별 고정 윈도우 속도 제한이 있습니다. 리미터는 Redis로 뒷받침됩니다. `REDIS_URL`을 설정하지 않으면 미들웨어는 **no-op**(fail-open)이 되고, 백엔드는 시작 시 `rate limiting disabled: REDIS_URL not configured`를 로깅합니다.
|
||||
|
||||
| 변수 | 기본값 | 설명 |
|
||||
|---|---|---|
|
||||
| `REDIS_URL` | 비어 있음 | Redis 연결 URL (예: `redis://localhost:6379/0`). 설정하지 않으면 auth 엔드포인트의 속도 제한이 비활성화됩니다. 동일한 Redis는 실시간 허브 fan-out, PAT 캐시, 데몬 토큰 캐시에서도 사용됩니다 — 설정하지 않으면 모두 인메모리 / 직접 DB 모드로 폴백합니다 |
|
||||
| `RATE_LIMIT_AUTH` | `5` | `/auth/send-code` 및 `/auth/google`에 대한 IP당 분당 최대 요청 수 |
|
||||
| `RATE_LIMIT_AUTH_VERIFY` | `20` | `/auth/verify-code`에 대한 IP당 분당 최대 요청 수 |
|
||||
| `RATE_LIMIT_TRUSTED_PROXIES` | 비어 있음 | 리미터가 그 `X-Forwarded-For` 헤더를 신뢰하도록 허용하는, 쉼표로 구분된 CIDR. 비어 있음(기본값)은 **XFF를 절대 신뢰하지 않음**을 의미합니다 — 리미터는 직접 연결의 `RemoteAddr`만 사용합니다 |
|
||||
|
||||
요청이 제한을 초과하면 서버는 `429 Too Many Requests`, `Retry-After: 60`, 그리고 본문 `{"error":"too many requests"}`로 응답합니다.
|
||||
|
||||
<Callout type="warning">
|
||||
**리버스 프록시 뒤에서는 `RATE_LIMIT_TRUSTED_PROXIES`를 반드시 설정해야 합니다.** 그렇지 않으면 백엔드 관점에서 모든 실제 사용자가 프록시의 IP를 공유하게 되어, 배포 전체가 하나의 버킷에 들어가고, `/auth/send-code`가 사이트 전체에 대해 분당 5회가 됩니다. 일반적인 값: 같은 호스트의 Caddy / Nginx에는 `127.0.0.1/32,::1/128`; Cloudflare / ALB / CloudFront에는 해당 CDN의 공개된 IP 범위. `RemoteAddr`이 이 CIDR 중 하나에 속하는 IP만 `X-Forwarded-For`를 사용해 클라이언트를 식별할 수 있습니다.
|
||||
</Callout>
|
||||
|
||||
이 별도의 `RATE_LIMIT_TRUSTED_PROXIES`는 오토파일럿 webhook 리미터(`/api/webhooks/autopilots/{token}`)를 제어하는 `MULTICA_TRUSTED_PROXIES`와는 **다릅니다**. 각 리미터는 자체 목록을 파싱하므로, 프록시 뒤에 있는 배포는 둘 다 설정해야 합니다.
|
||||
|
||||
## 데몬 튜닝 파라미터
|
||||
|
||||
데몬은 사용자의 로컬 기기에서 실행되며, 그 설정도 로컬 환경 변수에서 읽습니다. 일반적으로 사용되는 것들:
|
||||
|
||||
| 변수 | 기본값 | 설명 |
|
||||
|---|---|---|
|
||||
| `MULTICA_SERVER_URL` | `ws://localhost:8080/ws` | 서버 주소 (자체 호스팅: 자신의 도메인으로 교체) |
|
||||
| `MULTICA_DAEMON_HEARTBEAT_INTERVAL` | `15s` | 하트비트 간격 |
|
||||
| `MULTICA_DAEMON_POLL_INTERVAL` | `3s` | 작업 폴링 간격 |
|
||||
| `MULTICA_DAEMON_MAX_CONCURRENT_TASKS` | `20` | 최대 동시 작업 수 |
|
||||
| `MULTICA_<PROVIDER>_PATH` | CLI 이름과 일치 | 각 AI 코딩 도구 실행 파일의 경로 (예: `MULTICA_CLAUDE_PATH`) |
|
||||
| `MULTICA_<PROVIDER>_MODEL` | 비어 있음 | 각 AI 코딩 도구의 기본 모델 |
|
||||
|
||||
각 파라미터가 데몬 동작에 어떻게 영향을 미치는지에 대한 전체 설명은 [데몬과 런타임](/daemon-runtimes)을 참고하세요.
|
||||
|
||||
## 프론트엔드 액세스 제어
|
||||
|
||||
| 변수 | 기본값 | 설명 |
|
||||
|---|---|---|
|
||||
| `FRONTEND_ORIGIN` | 비어 있음 | 프론트엔드 주소. 초대 이메일 링크, CORS 허용 목록, cookie 도메인이 모두 이 값에서 파생됩니다. 설정하지 않으면 초대 이메일 링크는 호스팅 도메인 `https://app.multica.ai`로 폴백합니다 — 자체 호스팅은 이를 명시적으로 설정해야 합니다 |
|
||||
| `CORS_ALLOWED_ORIGINS` | 비어 있음 | 추가로 허용할 CORS origin (쉼표로 구분) |
|
||||
| `ALLOWED_ORIGINS` | 비어 있음 | WebSocket 전용 origin 허용 목록 (쉼표로 구분); 설정하지 않으면 폴백 순서는 `CORS_ALLOWED_ORIGINS` → `FRONTEND_ORIGIN` → `localhost:3000/5173/5174`입니다 |
|
||||
|
||||
<Callout type="warning">
|
||||
**`FRONTEND_ORIGIN`을 설정하지 않으면 두 가지 조용한 실패가 발생합니다**: (1) 초대 이메일 링크가 `https://app.multica.ai`(호스팅 도메인)를 가리켜, 클릭해도 사용자가 자체 호스팅 인스턴스로 돌아오지 않습니다; (2) WebSocket Origin 검사가 `localhost:3000 / 5173 / 5174`로 폴백하여, 프로덕션 배포의 모든 WebSocket 연결이 거부되고 프론트엔드가 "실시간 업데이트를 받지 못하는" 것처럼 보입니다.
|
||||
</Callout>
|
||||
|
||||
## GitHub 연동
|
||||
|
||||
[GitHub PR ↔ 이슈 연동](/github-integration)에는 두 개의 변수가 필요합니다. 설정에서 Connect GitHub를 활성화하고 들어오는 webhook을 수락하려면 둘 다 설정하세요.
|
||||
|
||||
| 변수 | 기본값 | 설명 |
|
||||
|---|---|---|
|
||||
| `GITHUB_APP_SLUG` | 비어 있음 | GitHub App의 slug (`https://github.com/apps/<slug>`의 끝부분). 설정 → GitHub 설치 버튼 URL을 구성합니다 |
|
||||
| `GITHUB_WEBHOOK_SECRET` | 비어 있음 | GitHub App에 설정한 Webhook secret. 모든 `pull_request` / `installation` delivery의 HMAC-SHA256 검증에 사용되며, setup 콜백 state token의 HMAC 키로도 사용됩니다 |
|
||||
|
||||
**둘 중 하나라도 설정하지 않았을 때의 동작:**
|
||||
|
||||
- 설정 → GitHub의 `Connect GitHub`가 **비활성화**되고 admin에게 "not configured" 힌트를 표시합니다.
|
||||
- `/api/webhooks/github` 엔드포인트는 **`503 github webhooks not configured`**를 반환합니다 — Multica는 모든 서명을 유효한 것으로 취급하기보다, secret 없이는 이벤트 처리를 거부합니다.
|
||||
|
||||
**참고:** `GITHUB_WEBHOOK_SECRET`은 설치 흐름 state token의 서명 키로 재사용되므로, 운영자는 secret 하나만 관리하면 됩니다. 이것은 GitHub App의 *Client* secret이 **아닙니다** — Client secret은 OAuth 관련이며 이 연동에서는 사용되지 않습니다. 전체 안내는 [GitHub 연동 → 자체 호스팅 설정](/github-integration#self-host-setup)을 참고하세요.
|
||||
|
||||
## 사용량 분석
|
||||
|
||||
기본적으로 서버는 Multica의 공식 PostHog 인스턴스로 보고합니다. 옵트아웃하려면 `ANALYTICS_DISABLED=true`로 설정하세요.
|
||||
|
||||
| 변수 | 기본값 | 설명 |
|
||||
|---|---|---|
|
||||
| `ANALYTICS_DISABLED` | `false` | 백엔드 분석을 완전히 비활성화하려면 `true`로 설정 |
|
||||
| `POSTHOG_API_KEY` | 내장 기본 키 | 자신의 PostHog 인스턴스를 가리킬 때 설정 |
|
||||
| `POSTHOG_HOST` | `https://us.i.posthog.com` | PostHog를 자체 호스팅하는 경우 자신의 호스트로 변경 |
|
||||
|
||||
## 다음
|
||||
|
||||
- [로그인 및 가입 설정](/auth-setup) — 위의 auth 관련 변수를 실제로 어떻게 설정하는지, 그리고 함정이 어디에 있는지
|
||||
- [GitHub 연동](/github-integration) — `GITHUB_APP_SLUG` / `GITHUB_WEBHOOK_SECRET`을 뒷받침하는 GitHub App을 어떻게 설정하는지
|
||||
- [문제 해결](/troubleshooting) — 흔한 설정 오류의 증상과 해결책
|
||||
- [데몬과 런타임](/daemon-runtimes) — `MULTICA_DAEMON_*` 파라미터가 실제로 하는 일
|
||||
@@ -49,12 +49,10 @@ Multica supports two delivery backends — [Resend](https://resend.com/) for clo
|
||||
| Variable | Default | Description |
|
||||
|---|---|---|
|
||||
| `SMTP_HOST` | empty | SMTP relay hostname. Setting this activates SMTP mode and overrides Resend |
|
||||
| `SMTP_PORT` | `25` | SMTP port. Use `587` for STARTTLS submission, or `465` for SMTPS (implicit TLS, auto-enabled) |
|
||||
| `SMTP_PORT` | `25` | SMTP port. Use `587` for STARTTLS submission; **port 465 (SMTPS / implicit TLS) is not supported** |
|
||||
| `SMTP_USERNAME` | empty | SMTP username. Leave empty for unauthenticated relay |
|
||||
| `SMTP_PASSWORD` | empty | SMTP password |
|
||||
| `SMTP_TLS` | `starttls` | TLS mode. `implicit` (aliases `smtps`, `ssl`) forces an immediate TLS handshake on connect (SMTPS); port `465` auto-enables it. Unset / `starttls` upgrades via STARTTLS after connect |
|
||||
| `SMTP_TLS_INSECURE` | `false` | Set `true` to skip TLS certificate verification (private CA / self-signed only) |
|
||||
| `SMTP_EHLO_NAME` | machine hostname | EHLO/HELO name announced to the relay. Set a real FQDN when a strict relay (e.g. Google Workspace `smtp-relay.gmail.com`) rejects the default greeting from a public IP — otherwise the relay drops the connection and it surfaces as an opaque `EOF` on a later command |
|
||||
|
||||
STARTTLS is upgraded automatically when the server advertises it. The dial timeout is 10s and the whole SMTP session has a 30s deadline, so a black-holed relay can't hang the auth handler.
|
||||
|
||||
@@ -86,19 +84,15 @@ Multica stores user-uploaded attachments (images and files in comments). **S3 is
|
||||
| `S3_REGION` | `us-west-2` | AWS region. Must match the bucket's actual region — it is used both for SDK signing and for building the public URL |
|
||||
| `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` | empty | Static credentials. When both are unset, the AWS SDK default credential chain is used (IAM role / environment credentials) |
|
||||
| `AWS_ENDPOINT_URL` | empty | Custom S3-compatible endpoint (for example [MinIO](https://min.io/)). Setting this switches to path-style URLs |
|
||||
| `ATTACHMENT_DOWNLOAD_MODE` | `auto` | Attachment download path: `auto`, `cloudfront`, `presign`, or `proxy`. In `auto`, CloudFront is preferred when fully configured; internal/private endpoint hosts use the server proxy; public S3-compatible endpoints use presigned GET URLs when supported |
|
||||
| `ATTACHMENT_DOWNLOAD_URL_TTL` | `30m` | TTL for CloudFront signed URLs and S3 presigned download URLs. Accepts Go duration strings |
|
||||
|
||||
**When `S3_BUCKET` is unset**: the server logs `"S3_BUCKET not set, cloud upload disabled"` at startup, and all uploads fall back to local disk.
|
||||
|
||||
**Stored object URLs** are constructed in this order of priority:
|
||||
**Public URLs** are constructed in this order of priority:
|
||||
|
||||
1. `https://<CLOUDFRONT_DOMAIN>/<key>` if `CLOUDFRONT_DOMAIN` is set.
|
||||
2. `<AWS_ENDPOINT_URL>/<S3_BUCKET>/<key>` (path-style) if `AWS_ENDPOINT_URL` is set.
|
||||
3. `https://<S3_BUCKET>.s3.<S3_REGION>.amazonaws.com/<key>` (virtual-hosted-style). When `S3_BUCKET` contains dots, the server falls back to `https://s3.<S3_REGION>.amazonaws.com/<S3_BUCKET>/<key>` (path-style) because the AWS-issued wildcard TLS certificate does not validate dotted bucket hosts.
|
||||
|
||||
API `download_url` values use `GET /api/attachments/{id}/download` unless CloudFront signing is configured. The endpoint redirects to CloudFront/S3 presigned URLs when safe, or streams through the server for private/internal endpoints such as `http://rustfs:9000`. For Docker/VPC-only object stores, set `ATTACHMENT_DOWNLOAD_MODE=proxy` if auto detection is not conservative enough for your network.
|
||||
|
||||
### Local disk (when S3 is not configured)
|
||||
|
||||
| Variable | Default | Description |
|
||||
@@ -134,22 +128,6 @@ Three allowlist layers combine by priority. **If any layer is set to a non-empty
|
||||
|
||||
**Invite flows themselves do not check the signup allowlist** — but the invitee must still be able to **sign in** before accepting the invite. If they already have a Multica account (for example from another workspace), they can accept directly, unaffected by the allowlist; **if they have never signed up**, the first step of sign-in (requesting a verification code) still passes through the allowlist check, and an email rejected by `ALLOW_SIGNUP=false` or by `ALLOWED_EMAILS` / `ALLOWED_EMAIL_DOMAINS` **cannot finish signup, and therefore cannot accept the invite**.
|
||||
|
||||
## Locking down workspace creation
|
||||
|
||||
`ALLOW_SIGNUP=false` blocks new accounts, but it does **not** block an already-signed-in user from creating another workspace via `POST /api/workspaces`. On a self-hosted instance where every issue, repo, and agent must be visible to the platform admin, set `DISABLE_WORKSPACE_CREATION=true` to close that gap.
|
||||
|
||||
| Variable | Default | Description |
|
||||
|---|---|---|
|
||||
| `DISABLE_WORKSPACE_CREATION` | `false` | When `true`, every call to `POST /api/workspaces` returns `403 workspace creation is disabled for this instance`. The web UI hides every "Create workspace" affordance via `/api/config`. There is no role/owner exception — the gate is global per instance |
|
||||
|
||||
Recommended bootstrap sequence:
|
||||
|
||||
1. Start the instance with `DISABLE_WORKSPACE_CREATION` unset (the default).
|
||||
2. Sign in as the admin and create the shared workspace.
|
||||
3. Set `DISABLE_WORKSPACE_CREATION=true` and restart the backend. From this point on, users join via invitation only.
|
||||
|
||||
If you also want to keep `ALLOW_SIGNUP=true` so invited users can finish signup with their first verification code, combine `DISABLE_WORKSPACE_CREATION=true` with `ALLOWED_EMAIL_DOMAINS` / `ALLOWED_EMAILS` to scope which addresses can sign up. Setting `ALLOW_SIGNUP=false` will additionally block pending invitees from creating their account at all — useful only on instances where every member already has a Multica account.
|
||||
|
||||
## Rate limiting (optional Redis)
|
||||
|
||||
Public auth endpoints — `/auth/send-code`, `/auth/verify-code`, `/auth/google` — have per-IP fixed-window rate limiting in front of them. The limiter is backed by Redis. When `REDIS_URL` is unset the middleware is a **no-op** (fail-open) and the backend logs `rate limiting disabled: REDIS_URL not configured` at startup.
|
||||
@@ -189,8 +167,6 @@ For a full explanation of how each parameter affects daemon behavior, see [Daemo
|
||||
| Variable | Default | Description |
|
||||
|---|---|---|
|
||||
| `FRONTEND_ORIGIN` | empty | Frontend address. Invite email links, the CORS allowlist, and the cookie domain are all derived from this. When unset, invite email links fall back to the hosted domain `https://app.multica.ai` — self-host must set this explicitly |
|
||||
| `MULTICA_APP_URL` | empty | Frontend URL for CLI login flow. Also used by the web UI to show self-host daemon setup commands with your app domain; for same-origin deployments this is also used as daemon `server_url` when `MULTICA_PUBLIC_URL` is unset |
|
||||
| `MULTICA_PUBLIC_URL` | empty | Public API URL, without trailing slash. Used for autopilot webhook URLs and by the web UI as the daemon `server_url` |
|
||||
| `CORS_ALLOWED_ORIGINS` | empty | Additional allowed CORS origins (comma-separated) |
|
||||
| `ALLOWED_ORIGINS` | empty | WebSocket-specific origin allowlist (comma-separated); when unset, fallback order is `CORS_ALLOWED_ORIGINS` → `FRONTEND_ORIGIN` → `localhost:3000/5173/5174` |
|
||||
|
||||
|
||||
@@ -49,12 +49,10 @@ Multica 支持两种邮件发送通道——[Resend](https://resend.com/) 适合
|
||||
| 环境变量 | 默认值 | 说明 |
|
||||
|---|---|---|
|
||||
| `SMTP_HOST` | 空 | SMTP relay 主机名。设置后即启用 SMTP 模式并覆盖 Resend |
|
||||
| `SMTP_PORT` | `25` | SMTP 端口。STARTTLS 提交端口用 `587`;SMTPS(隐式 TLS,自动启用)用 `465` |
|
||||
| `SMTP_PORT` | `25` | SMTP 端口。STARTTLS 提交端口用 `587`;**暂不支持 465(SMTPS / 隐式 TLS)** |
|
||||
| `SMTP_USERNAME` | 空 | SMTP 用户名。留空表示未认证 relay |
|
||||
| `SMTP_PASSWORD` | 空 | SMTP 密码 |
|
||||
| `SMTP_TLS` | `starttls` | TLS 模式。`implicit`(别名 `smtps`、`ssl`)在连接时立即进行 TLS 握手(SMTPS);`465` 端口会自动启用。未设置 / `starttls` 则在连接后通过 STARTTLS 升级 |
|
||||
| `SMTP_TLS_INSECURE` | `false` | 设为 `true` 跳过 TLS 证书校验(仅限私有 CA / 自签证书)|
|
||||
| `SMTP_EHLO_NAME` | 机器主机名 | 向 relay 通告的 EHLO/HELO 名称。当严格的 relay(例如 Google Workspace `smtp-relay.gmail.com`)拒绝来自公网 IP 的默认问候时,填一个真实的 FQDN——否则 relay 会直接断开连接,并在后续某条命令上表现为一个不知所云的 `EOF` |
|
||||
|
||||
服务端 advertise STARTTLS 时会自动升级。dial 超时 10s,整个 SMTP 会话有 30s deadline,避免 relay 黑洞把 auth handler 挂死。
|
||||
|
||||
@@ -86,19 +84,15 @@ Multica 存储用户上传的附件(评论里的图片、文件等)。**优
|
||||
| `S3_REGION` | `us-west-2` | AWS 区域。必须和 bucket 所在区域一致——SDK 签名和公开 URL 都用它 |
|
||||
| `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` | 空 | 静态凭证。全未设时用 AWS SDK 默认凭证链(IAM role / 环境凭证)|
|
||||
| `AWS_ENDPOINT_URL` | 空 | 自定义 S3 兼容端点(例如 [MinIO](https://min.io/))。设了会切到 path-style URL |
|
||||
| `ATTACHMENT_DOWNLOAD_MODE` | `auto` | 附件下载路径:`auto`、`cloudfront`、`presign` 或 `proxy`。`auto` 下 CloudFront 配完整时优先 CloudFront;内网/私有 endpoint host 走 server proxy;公网 S3 兼容 endpoint 在支持时走 presigned GET |
|
||||
| `ATTACHMENT_DOWNLOAD_URL_TTL` | `30m` | CloudFront signed URL 和 S3 presigned download URL 的有效期。使用 Go duration 格式 |
|
||||
|
||||
**`S3_BUCKET` 未设时**:server 启动时打 info 日志 `"S3_BUCKET not set, cloud upload disabled"`,所有上传回落到本地磁盘。
|
||||
|
||||
**对象存储 URL** 按优先级拼装:
|
||||
**公开 URL** 按优先级拼装:
|
||||
|
||||
1. 设了 `CLOUDFRONT_DOMAIN` → `https://<CLOUDFRONT_DOMAIN>/<key>`
|
||||
2. 设了 `AWS_ENDPOINT_URL` → `<AWS_ENDPOINT_URL>/<S3_BUCKET>/<key>`(path-style)
|
||||
3. 默认走 AWS S3 → `https://<S3_BUCKET>.s3.<S3_REGION>.amazonaws.com/<key>`(virtual-hosted-style)。bucket 名含点时会回落到 `https://s3.<S3_REGION>.amazonaws.com/<S3_BUCKET>/<key>`(path-style),因为 AWS 通配证书无法覆盖含点 host。
|
||||
|
||||
API 返回的 `download_url` 在未配置 CloudFront 签名时会指向 `GET /api/attachments/{id}/download`。这个端点会在安全时跳转到 CloudFront/S3 presigned URL;遇到 `http://rustfs:9000` 这类私有或内网 endpoint 时则由 server 流式转发。Docker/VPC 内部对象存储建议显式设置 `ATTACHMENT_DOWNLOAD_MODE=proxy`。
|
||||
|
||||
### 本地磁盘(S3 未配时)
|
||||
|
||||
| 环境变量 | 默认值 | 说明 |
|
||||
@@ -134,22 +128,6 @@ API 返回的 `download_url` 在未配置 CloudFront 签名时会指向 `GET /ap
|
||||
|
||||
**邀请流程本身不检查 signup 白名单**——但被邀请人必须先能**登录**才能接受邀请。如果对方已经有 Multica 账号(比如在其他工作区注册过),可以直接接受,不受白名单影响;**如果对方还没注册过**,他们登录的第一步(发送验证码)仍然会过白名单检查,被 `ALLOW_SIGNUP=false` 或 `ALLOWED_EMAILS` / `ALLOWED_EMAIL_DOMAINS` 拒绝的邮箱**无法完成注册,也就没法接受邀请**。
|
||||
|
||||
## 锁死工作区创建
|
||||
|
||||
`ALLOW_SIGNUP=false` 能挡住新注册,但**挡不住**已经登录的用户继续 `POST /api/workspaces` 自助开新工作区。在希望平台管理员能看到全部 issue / 仓库 / 智能体的自部署实例里,把 `DISABLE_WORKSPACE_CREATION=true` 打开以堵上这个口子。
|
||||
|
||||
| 环境变量 | 默认值 | 说明 |
|
||||
|---|---|---|
|
||||
| `DISABLE_WORKSPACE_CREATION` | `false` | 设为 `true` 时,对 `POST /api/workspaces` 的任何调用都返回 `403 workspace creation is disabled for this instance`。Web UI 通过 `/api/config` 隐藏所有「创建工作区」入口。该开关是实例级的,没有 owner / admin 例外 |
|
||||
|
||||
推荐的开服流程:
|
||||
|
||||
1. 启动实例时保持 `DISABLE_WORKSPACE_CREATION` 未设置(默认)。
|
||||
2. 管理员登录并创建共享工作区。
|
||||
3. 把 `DISABLE_WORKSPACE_CREATION=true` 设上并重启 backend。之后新用户只能通过邀请加入。
|
||||
|
||||
如果还想让被邀请的新用户能完成首次注册,保留 `ALLOW_SIGNUP=true`(必要时用 `ALLOWED_EMAIL_DOMAINS` / `ALLOWED_EMAILS` 限定可注册邮箱),只把 `DISABLE_WORKSPACE_CREATION=true` 打开即可;如果同时设 `ALLOW_SIGNUP=false`,连有 pending invite 的邮箱都无法完成首次注册,仅适合所有成员都已有 Multica 账号的实例。
|
||||
|
||||
## 速率限制(可选 Redis)
|
||||
|
||||
公开认证端点——`/auth/send-code`、`/auth/verify-code`、`/auth/google`——前面挂了按 IP 的固定窗口限流。限流器后端是 Redis。`REDIS_URL` 不设时中间件**直通**(fail-open),后端启动会打日志 `rate limiting disabled: REDIS_URL not configured`。
|
||||
@@ -189,8 +167,6 @@ API 返回的 `download_url` 在未配置 CloudFront 签名时会指向 `GET /ap
|
||||
| 环境变量 | 默认值 | 说明 |
|
||||
|---|---|---|
|
||||
| `FRONTEND_ORIGIN` | 空 | 前端地址。邀请邮件里的链接、CORS 白名单、cookie domain 都从这里推导。邮件链接在不设时会 fallback 到托管版域名 `https://app.multica.ai`——self-host 必须显式填 |
|
||||
| `MULTICA_APP_URL` | 空 | CLI 登录流程使用的前端 URL。Web UI 也会用它显示带你自己 app domain 的 self-host 守护进程 setup 命令;同源部署中,如果 `MULTICA_PUBLIC_URL` 未设置,它也会作为守护进程的 `server_url` |
|
||||
| `MULTICA_PUBLIC_URL` | 空 | 公开 API URL,不带结尾 slash。用于 autopilot webhook URL,也会被 Web UI 用作守护进程的 `server_url` |
|
||||
| `CORS_ALLOWED_ORIGINS` | 空 | 额外的 CORS 允许来源(逗号分隔)|
|
||||
| `ALLOWED_ORIGINS` | 空 | WebSocket 专用的 origin 白名单(逗号分隔);不设就按 `CORS_ALLOWED_ORIGINS` → `FRONTEND_ORIGIN` → `localhost:3000/5173/5174` 顺序回落 |
|
||||
|
||||
|
||||
@@ -1,183 +0,0 @@
|
||||
---
|
||||
title: GitHub 連携
|
||||
description: GitHub App を一度連携すれば、ブランチ・タイトル・本文にイシュー識別子を含む PR が該当イシューに自動で紐づきます。そして PR をマージするとイシューが Done に移動します。
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
|
||||
**設定 → GitHub** で GitHub アカウントまたは組織を一度だけ連携してください。その後は、ブランチ名・タイトル・本文にイシュー識別子(例: `MUL-123`)を含むあらゆる pull request が該当する[イシュー](/issues)に**自動で紐づき**、イシューサイドバーの **Pull requests** に表示され、PR がマージされるとイシューが **Done** に移動します。
|
||||
|
||||
イシューごとの設定はありません。フロー全体が識別子で駆動されます。
|
||||
|
||||
## 連携が行うこと
|
||||
|
||||
| 場所 | 動作 |
|
||||
|---|---|
|
||||
| **設定 → GitHub** | ワークスペースの admin には、マスタートグル、**Connect GitHub** ボタン、機能スイッチ(PR サイドバー、Co-authored-by、自動紐づけ)を備えた GitHub タブが表示されます。インストール後は GitHub タブに戻ります。 |
|
||||
| **イシューサイドバー → Pull requests** | このイシューに自動で紐づいたすべての PR が、タイトル、リポジトリ、状態(`Open` / `Draft` / `Merged` / `Closed`)、作成者とともに表示されます。行をクリックすると GitHub の該当 PR に移動します。 |
|
||||
| **Webhook(バックグラウンド)** | すべての `pull_request` イベントで、Multica は PR 行を upsert し、PR からイシュー識別子をスキャンして、紐づけ行を(再)構築します。冪等性があり、同じ delivery を再送しても変化はありません。 |
|
||||
| **マージ時のステータス自動変更** | PR が `merged` に遷移すると、まだ `Done` でも `Cancelled` でもない、紐づいたすべてのイシューが `Done` に移動します。ステータス変更は source `github_pr_merged` でタイムラインに記録されます。 |
|
||||
|
||||
ミラーリングされるのは PR 自体のみです。コミット、オープンな PR のないブランチ ref、CI チェックの状態はモデル化され**ません**。この連携は意図的に狭く設計されています。
|
||||
|
||||
## 識別子のマッチング方法
|
||||
|
||||
Webhook は次の順序で 3 つのフィールドから識別子を抽出します: **PR head ブランチ**、**PR タイトル**、**PR 本文**。マッチャーは次のとおりです。
|
||||
|
||||
- 大文字小文字を区別しません — `mul-123`、`MUL-123`、`Mul-123` はすべてマッチします。
|
||||
- 境界があります — 左側の `\b` と右側の数字アンカーにより、`v1.2-3` のようなバージョン番号やメール形式の文字列を誤って拾わないようにしています。
|
||||
- ワークスペーススコープに限定されます — そのワークスペース固有の[イシュー prefix](/workspaces)にのみマッチします。prefix が `MUL` のワークスペースでは、整数が別のイシューと一致しても `FOO-1` は無視されます。
|
||||
- 重複が除去されます — 本文に `MUL-1, MUL-1` と並べても、イシューは一度だけ紐づきます。
|
||||
|
||||
1 つの PR で**複数のイシュー**を参照できます。`Closes MUL-1, MUL-2` は PR を両方のイシューに紐づけ、マージすると両方が `Done` に進みます。
|
||||
|
||||
## マージ時の Done 自動変更ルール
|
||||
|
||||
PR の `merged` フィールドが `true` に切り替わると、紐づいたすべてのイシューが評価されます。
|
||||
|
||||
| イシューの現在のステータス | 結果 |
|
||||
|---|---|
|
||||
| `done` | 変化なし(すでに終了状態)。 |
|
||||
| `cancelled` | **変化なし** — cancelled はユーザーが作業を明示的に放棄したことを意味するため、連携はこのシグナルを上書きしません。 |
|
||||
| それ以外すべて(`todo`、`in_progress`、`in_review`、`blocked`、`backlog`) | `done` に移動。 |
|
||||
|
||||
PR をマージ**せずに**クローズした場合は、PR カードの状態が `Closed` に更新されるだけです。紐づいたイシューはそのまま維持されます — マージせずにクローズすることが何を意味するかはユーザーが決めるからです。
|
||||
|
||||
<Callout type="info">
|
||||
この動作はタイムライン上で `system` アクターに帰属します。イシューの購読者は、人がステータスを移動したときと同じように、ステータス変更に関するインボックス通知を受け取ります。
|
||||
</Callout>
|
||||
|
||||
## 自動で紐づかないもの
|
||||
|
||||
- **コミットメッセージ内の識別子** — ブランチ / タイトル / 本文のみがスキャンされます。`MUL-123: fix login` というタイトルのコミットは、同じ文字列が PR タイトルや本文にも現れない限り自動では紐づきません。
|
||||
- **PR コメント内の識別子** — PR 自体のメタデータのみがスキャンされ、後から付いた GitHub コメントは無視されます。
|
||||
- **App がインストールされていないリポジトリの PR** — App がなければ、Multica は webhook をまったく受け取りません。
|
||||
- **PR をイシューに手動で紐づける** — まだこのための UI はありません。チームの慣習で識別子を Multica が読まない場所に置いている場合は、PR タイトルや本文に追加してください。
|
||||
|
||||
## 連携解除
|
||||
|
||||
**設定 → GitHub** にはインストール一覧はありません — 既存のインストールは GitHub から直接管理します。
|
||||
|
||||
- **GitHub から** — `https://github.com/settings/installations`(個人)または `https://github.com/organizations/<org>/settings/installations`(組織)で Multica GitHub App をアンインストールします。Multica は `installation.deleted` webhook を受け取ってリアルタイムで行を削除し、開いている Settings タブはリロードなしで更新されます。
|
||||
- **Multica 内部からの連携解除は admin 専用です** — GitHub タブの連携解除コントロールは、admin 以外のユーザーには非表示です。マスター GitHub スイッチがオフでも利用可能なままなので、admin はワンクリックで機能を無効化した後でも、古いインストールを取り消せます。
|
||||
|
||||
連携解除後も、ミラーリングされた PR 行はデータベースに残り、過去のイシューサイドバーで何が紐づいていたかを引き続き表示しますが、そのインストールから新たに入ってくる webhook イベントは受理されなくなります。
|
||||
|
||||
## 権限と可視性
|
||||
|
||||
- **連携 / 連携解除**にはワークスペースの **owner または admin** が必要です。member にはカードの説明は見えますが、Connect ボタンは見えません。
|
||||
- イシューの **Pull requests** サイドバーは、そのイシューを閲覧できる誰にでも表示されます — イシュー詳細の他の部分と同じ権限です。
|
||||
- GitHub App は pull request とメタデータへの**読み取り専用**アクセスを要求します。Multica はコミット、コメント、ステータスチェックを GitHub に書き戻すことはありません。
|
||||
|
||||
## セルフホストのセットアップ
|
||||
|
||||
Multica Cloud で Multica を実行している場合、連携はすでに構成済みです — このセクションは飛ばしてください。
|
||||
|
||||
セルフホストの場合は、GitHub App を 1 つ作成し、サーバーを指すように設定し、環境変数を 2 つ設定します。フロー全体は以下のとおりです。
|
||||
|
||||
### 1. GitHub App を作成する
|
||||
|
||||
次のいずれかにアクセスしてください。
|
||||
|
||||
- 個人アカウント → `https://github.com/settings/apps/new`
|
||||
- 組織 → `https://github.com/organizations/<org>/settings/apps/new`
|
||||
|
||||
次を入力します。
|
||||
|
||||
| フィールド | 値 |
|
||||
|---|---|
|
||||
| **GitHub App name** | 見分けやすい名前、例: `Multica` または `Multica (staging)`。 |
|
||||
| **Homepage URL** | Multica フロントエンド、例: `https://multica.example.com`。 |
|
||||
| **Callback URL** | 空欄のままにしてください — Multica は OAuth ユーザー ID を使用しません。 |
|
||||
| **Setup URL** | `https://<api-host>/api/github/setup`。**「Redirect on update」をチェックしてください。** |
|
||||
| **Webhook → Active** | 有効。 |
|
||||
| **Webhook URL** | `https://<api-host>/api/webhooks/github`。 |
|
||||
| **Webhook secret** | 長いランダム文字列を生成してください(例: `openssl rand -hex 32`)。手順 2 で同じ値を Multica の env に貼り付けます。 |
|
||||
| **Permissions → Repository → Pull requests** | **Read-only**。 |
|
||||
| **Permissions → Repository → Metadata** | Read-only(必須)。 |
|
||||
| **Subscribe to events** | **Pull request** をチェックしてください。 |
|
||||
| **Where can this GitHub App be installed?** | お好みで。単一組織のセットアップなら `Only on this account` で十分です。 |
|
||||
|
||||
**Create GitHub App** の後、App の詳細ページから 2 つのことを控えておいてください。
|
||||
|
||||
- 上部の **public link** — その末尾が slug です。`https://github.com/apps/multica-acme` → slug = `multica-acme`。
|
||||
- 先ほど生成した **webhook secret**(後で GitHub から読み戻すことはできません — 今すぐ保存してください)。
|
||||
|
||||
<Callout type="warning">
|
||||
**Webhook secret ≠ Client secret。** App 設定ページには両方のフィールドが並んで配置されています。**Webhook secret** は `pull_request` の payload に署名する値で、Multica が必要とするものです。**Client secret** は OAuth 用で、この連携では使用しません。この 2 つを混同すると、すべての webhook delivery で紛らわしい `401 invalid signature` が発生します。
|
||||
</Callout>
|
||||
|
||||
### 2. 環境変数を設定する
|
||||
|
||||
API サーバーで:
|
||||
|
||||
```dotenv
|
||||
GITHUB_APP_SLUG=multica-acme
|
||||
GITHUB_WEBHOOK_SECRET=<the webhook secret you generated>
|
||||
```
|
||||
|
||||
両方の変数が必須です。どちらかが欠けていると:
|
||||
|
||||
- Settings の `Connect GitHub` が**無効**になり、「not configured」のヒントが表示されます。
|
||||
- `/api/webhooks/github` エンドポイントが **`503 github webhooks not configured`** を返します — Multica は secret なしでイベントを処理することを拒否し、すべての署名を黙って有効として扱うことはありません。
|
||||
|
||||
`FRONTEND_ORIGIN` も設定されている必要があります(どのプロダクションのセルフホストでもすでに設定されています)。インストール後、setup コールバックがユーザーを `<FRONTEND_ORIGIN>/settings?tab=github` に戻します。
|
||||
|
||||
env 変数を設定した後は API を再起動してください。
|
||||
|
||||
### 3. マイグレーションを実行する
|
||||
|
||||
この連携はテーブルをマイグレーション `079_github_integration` で提供します。古いデプロイをアップグレードする場合:
|
||||
|
||||
```bash
|
||||
make migrate-up
|
||||
```
|
||||
|
||||
3 つのテーブルが作成されます: `github_installation`、`github_pull_request`、`issue_pull_request`。これらはワークスペースとともに cascade-delete されるため、ワークスペースを削除すると自動的にクリーンアップされます。
|
||||
|
||||
### 4. UI から連携する
|
||||
|
||||
Multica で:
|
||||
|
||||
1. owner または admin 権限で **設定 → GitHub** を開きます。
|
||||
2. **Connect GitHub** をクリックします。GitHub が新しいタブで開きます。
|
||||
3. アクセスを付与するリポジトリを選び、**Install** します。
|
||||
4. GitHub が `<api-host>/api/github/setup` にリダイレクトしてインストールを記録し、`<FRONTEND_ORIGIN>/settings?tab=github&github_connected=1` に戻します。
|
||||
|
||||
その後、ブランチ / タイトル / 本文にイシュー識別子を含む PR を開いてみてください — 数秒以内に、そのイシューの詳細ページに Pull requests ブロックが表示されます。
|
||||
|
||||
### 5. curl プローブで検証する
|
||||
|
||||
インストール後に GitHub の **Recent Deliveries** ページで `401 invalid signature` が報告される場合、両側の secret が異なっています。どちらが間違っているかを最も速く突き止める方法は、GitHub を迂回することです。
|
||||
|
||||
```bash
|
||||
SECRET="<the value you put in GITHUB_WEBHOOK_SECRET>"
|
||||
BODY='{"zen":"test"}'
|
||||
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')
|
||||
curl -i -X POST https://<api-host>/api/webhooks/github \
|
||||
-H "X-Hub-Signature-256: sha256=$SIG" \
|
||||
-H "X-GitHub-Event: ping" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$BODY"
|
||||
```
|
||||
|
||||
| HTTP ステータス | 意味 | 解決方法 |
|
||||
|---|---|---|
|
||||
| `200` `{"ok":"pong"}` | サーバーがロードした secret が `$SECRET` と一致します。不一致は GitHub 側にあります。 | App → Webhook secret を編集 → **同じ値を貼り付け** → **Save changes**(保存せずにフィールドの外をクリックすると古い secret が維持されます)。再送してください。 |
|
||||
| `401 invalid signature` | サーバーがロードした secret が思っている値で**ありません**。 | env 変数が実行中のプロセスに反映されたか確認してください(例: `kubectl exec` → `echo -n "$GITHUB_WEBHOOK_SECRET" \| wc -c`)。再デプロイしてください。 |
|
||||
| `503 github webhooks not configured` | プロセスで `GITHUB_WEBHOOK_SECRET` が空です。 | env 変数を設定し、API を再起動してください。 |
|
||||
|
||||
## 制限事項
|
||||
|
||||
現時点で知っておくべき、いくつかの粗い部分があります。
|
||||
|
||||
- **まだ手動の紐づけ UI はありません** — PR を紐づける唯一の方法は、ブランチ、タイトル、本文に識別子を置くことです。
|
||||
- **CI / チェック状態はありません** — PR 自体のみがミラーリングされます。ビルド状態、レビューコメント、レビュアーは Multica には表示されません。
|
||||
- マージ → Done ルールに対する**ワークスペースレベルの設定はありません** — 固定のデフォルトです(`cancelled` でない限り `merged → done`)。ワークスペースでカスタマイズできるマッピングは将来の追加予定です。
|
||||
- **1 つのイシューに複数の PR が紐づく場合、マージは保守的です** — 2 つの PR がどちらも `MUL-123` を参照していて最初の 1 つがマージされると、イシューはただちに `Done` に移動します。進める前に紐づいたすべての PR が解決されるのを待つ後続の変更が進行中です。
|
||||
|
||||
## 次に
|
||||
|
||||
- [イシュー](/issues) — PR から参照されるイシュー識別子(`MUL-123`)
|
||||
- [ワークスペース](/workspaces) — ワークスペース固有のイシュー prefix を設定する場所
|
||||
- [環境変数](/environment-variables) — 上記の GitHub 変数を含む env の完全なリファレンス
|
||||
@@ -1,183 +0,0 @@
|
||||
---
|
||||
title: GitHub 연동
|
||||
description: GitHub App을 한 번만 연결하면, 브랜치·제목·본문에 이슈 식별자가 들어간 PR이 해당 이슈에 자동으로 연결됩니다. 그리고 PR을 머지하면 이슈가 완료로 이동합니다.
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
|
||||
**설정 → GitHub**에서 GitHub 계정 또는 조직을 한 번만 연결하세요. 그 후에는 브랜치 이름, 제목, 본문에 이슈 식별자(예: `MUL-123`)가 들어 있는 모든 pull request가 해당 [이슈](/issues)에 **자동으로 연결**되고, 이슈 사이드바의 **Pull requests** 아래에 표시되며, PR이 머지되면 이슈가 **완료**로 이동합니다.
|
||||
|
||||
이슈별 설정은 없습니다. 전체 흐름은 식별자로 동작합니다.
|
||||
|
||||
## 연동이 하는 일
|
||||
|
||||
| 위치 | 동작 |
|
||||
|---|---|
|
||||
| **설정 → GitHub** | 워크스페이스 admin에게는 마스터 토글, **Connect GitHub** 버튼, 기능 스위치(PR 사이드바, Co-authored-by, 자동 연결)가 있는 GitHub 탭이 보입니다. 설치 후에는 GitHub 탭으로 다시 돌아옵니다. |
|
||||
| **이슈 사이드바 → Pull requests** | 이 이슈에 자동 연결된 모든 PR이 제목, 저장소, 상태(`Open` / `Draft` / `Merged` / `Closed`), 작성자와 함께 표시됩니다. 행을 클릭하면 GitHub의 해당 PR로 이동합니다. |
|
||||
| **Webhook(백그라운드)** | 모든 `pull_request` 이벤트에서 Multica는 PR 행을 upsert하고, PR에서 이슈 식별자를 스캔한 뒤, 연결 행을 (다시) 구성합니다. 멱등성이 있어 동일 delivery를 재전송해도 변화가 없습니다. |
|
||||
| **머지 시 상태 자동 변경** | PR이 `merged`로 전환되면, 아직 `Done`이나 `Cancelled`가 아닌 모든 연결된 이슈가 `Done`으로 이동합니다. 상태 변경은 source `github_pr_merged`로 타임라인에 기록됩니다. |
|
||||
|
||||
미러링되는 것은 PR 자체뿐입니다. 커밋, 열린 PR이 없는 브랜치 ref, CI 체크 상태는 모델링되지 **않습니다**. 이 연동은 의도적으로 좁게 설계되었습니다.
|
||||
|
||||
## 식별자 매칭 방식
|
||||
|
||||
Webhook은 다음 순서로 세 필드에서 식별자를 추출합니다: **PR head 브랜치**, **PR 제목**, **PR 본문**. 매처는 다음과 같습니다.
|
||||
|
||||
- 대소문자를 구분하지 않습니다 — `mul-123`, `MUL-123`, `Mul-123`이 모두 매칭됩니다.
|
||||
- 경계가 있습니다 — 왼쪽의 `\b`와 오른쪽의 숫자 앵커 덕분에 `v1.2-3` 같은 버전 번호나 이메일 형식 문자열을 잘못 잡지 않습니다.
|
||||
- 워크스페이스 범위로 제한됩니다 — 해당 워크스페이스 고유의 [이슈 prefix](/workspaces)에만 매칭됩니다. prefix가 `MUL`인 워크스페이스에서는 정수가 다른 이슈와 일치하더라도 `FOO-1`이 무시됩니다.
|
||||
- 중복이 제거됩니다 — 본문에 `MUL-1, MUL-1`을 나열해도 이슈는 한 번만 연결됩니다.
|
||||
|
||||
하나의 PR에서 **여러 이슈**를 참조할 수 있습니다. `Closes MUL-1, MUL-2`는 PR을 두 이슈에 모두 연결하고, 머지하면 두 이슈 모두 `Done`으로 진행됩니다.
|
||||
|
||||
## 머지 시 완료 자동 변경 규칙
|
||||
|
||||
PR의 `merged` 필드가 `true`로 바뀌면, 연결된 모든 이슈가 평가됩니다.
|
||||
|
||||
| 이슈 현재 상태 | 결과 |
|
||||
|---|---|
|
||||
| `done` | 변화 없음(이미 종료 상태). |
|
||||
| `cancelled` | **변화 없음** — 취소됨은 사용자가 작업을 명시적으로 포기했다는 의미이므로, 연동이 이 신호를 덮어쓰지 않습니다. |
|
||||
| 그 외 모두(`todo`, `in_progress`, `in_review`, `blocked`, `backlog`) | `done`으로 이동. |
|
||||
|
||||
PR을 머지하지 **않고** 닫으면 PR 카드의 상태만 `Closed`로 업데이트됩니다. 연결된 이슈는 그대로 유지됩니다 — 머지 없이 닫는 것이 무엇을 의미하는지는 사용자가 결정합니다.
|
||||
|
||||
<Callout type="info">
|
||||
이 동작은 타임라인에서 `system` 액터에게 귀속됩니다. 이슈 구독자는 사람이 상태를 옮겼을 때와 동일하게 상태 변경에 대한 인박스 알림을 받습니다.
|
||||
</Callout>
|
||||
|
||||
## 자동 연결되지 않는 것
|
||||
|
||||
- **커밋 메시지의 식별자** — 브랜치 / 제목 / 본문만 스캔됩니다. `MUL-123: fix login`이라는 제목의 커밋은 동일한 문자열이 PR 제목이나 본문에도 나타나지 않는 한 자동 연결되지 않습니다.
|
||||
- **PR 댓글의 식별자** — PR 자체의 메타데이터만 스캔되며, 이후의 GitHub 댓글은 무시됩니다.
|
||||
- **App이 설치되지 않은 저장소의 PR** — App이 없으면 Multica는 webhook을 전혀 받지 못합니다.
|
||||
- **PR을 이슈에 수동으로 연결하기** — 아직 이를 위한 UI는 없습니다. 팀의 규칙상 식별자를 Multica가 읽지 않는 위치에 둔다면, PR 제목이나 본문에 추가하세요.
|
||||
|
||||
## 연결 해제
|
||||
|
||||
**설정 → GitHub**에는 설치 목록이 없습니다 — 기존 설치는 GitHub에서 직접 관리합니다.
|
||||
|
||||
- **GitHub에서** — `https://github.com/settings/installations`(개인) 또는 `https://github.com/organizations/<org>/settings/installations`(조직)에서 Multica GitHub App을 제거합니다. Multica는 `installation.deleted` webhook을 받아 실시간으로 행을 삭제하며, 열려 있는 Settings 탭은 새로고침 없이 업데이트됩니다.
|
||||
- **Multica 내부에서의 연결 해제는 admin 전용입니다** — GitHub 탭의 연결 해제 컨트롤은 admin이 아닌 사용자에게는 숨겨집니다. 마스터 GitHub 스위치가 꺼져 있어도 계속 사용할 수 있어, admin이 원클릭으로 기능을 비활성화한 후에도 오래된 설치를 해제할 수 있습니다.
|
||||
|
||||
연결 해제 후에도 미러링된 PR 행은 데이터베이스에 남아 과거 이슈 사이드바에서 무엇이 연결되어 있었는지 계속 보여주지만, 해당 설치에서 새로 들어오는 webhook 이벤트는 더 이상 수락되지 않습니다.
|
||||
|
||||
## 권한 및 가시성
|
||||
|
||||
- **연결 / 연결 해제**에는 워크스페이스 **owner 또는 admin**이 필요합니다. member에게는 카드 설명은 보이지만 Connect 버튼은 보이지 않습니다.
|
||||
- 이슈의 **Pull requests** 사이드바는 해당 이슈를 읽을 수 있는 모든 사람에게 보입니다 — 이슈 상세의 나머지 부분과 동일한 권한입니다.
|
||||
- GitHub App은 pull request와 메타데이터에 대한 **읽기 전용** 액세스를 요청합니다. Multica는 커밋, 댓글, 상태 체크를 GitHub로 다시 푸시하지 않습니다.
|
||||
|
||||
## 자체 호스팅 설정
|
||||
|
||||
Multica Cloud에서 Multica를 실행 중이라면 연동이 이미 구성되어 있습니다 — 이 섹션은 건너뛰세요.
|
||||
|
||||
자체 호스팅의 경우, GitHub App을 하나 만들고, 서버를 가리키게 한 뒤, 환경 변수 두 개를 설정합니다. 전체 흐름은 아래와 같습니다.
|
||||
|
||||
### 1. GitHub App 만들기
|
||||
|
||||
다음 중 하나로 이동하세요.
|
||||
|
||||
- 개인 계정 → `https://github.com/settings/apps/new`
|
||||
- 조직 → `https://github.com/organizations/<org>/settings/apps/new`
|
||||
|
||||
다음을 입력하세요.
|
||||
|
||||
| 필드 | 값 |
|
||||
|---|---|
|
||||
| **GitHub App name** | 알아보기 쉬운 이름, 예: `Multica` 또는 `Multica (staging)`. |
|
||||
| **Homepage URL** | Multica 프론트엔드, 예: `https://multica.example.com`. |
|
||||
| **Callback URL** | 비워 두세요 — Multica는 OAuth 사용자 신원을 사용하지 않습니다. |
|
||||
| **Setup URL** | `https://<api-host>/api/github/setup`. **"Redirect on update"를 체크하세요.** |
|
||||
| **Webhook → Active** | 활성화. |
|
||||
| **Webhook URL** | `https://<api-host>/api/webhooks/github`. |
|
||||
| **Webhook secret** | 긴 무작위 문자열을 생성하세요(예: `openssl rand -hex 32`). 2단계에서 동일한 값을 Multica의 env에 붙여넣게 됩니다. |
|
||||
| **Permissions → Repository → Pull requests** | **Read-only**. |
|
||||
| **Permissions → Repository → Metadata** | Read-only(필수). |
|
||||
| **Subscribe to events** | **Pull request**를 체크하세요. |
|
||||
| **Where can this GitHub App be installed?** | 선택 사항. 단일 조직 설정에는 `Only on this account`로 충분합니다. |
|
||||
|
||||
**Create GitHub App** 후, App 상세 페이지에서 두 가지를 기록해 두세요.
|
||||
|
||||
- 상단의 **public link** — 그 꼬리가 slug입니다. `https://github.com/apps/multica-acme` → slug = `multica-acme`.
|
||||
- 방금 생성한 **webhook secret**(나중에 GitHub에서 다시 읽을 수 없으니 — 지금 저장하세요).
|
||||
|
||||
<Callout type="warning">
|
||||
**Webhook secret ≠ Client secret.** App 설정 페이지에는 두 필드가 함께 쌓여 있습니다. **Webhook secret**은 `pull_request` payload에 서명하는 값으로, Multica가 필요로 하는 것입니다. **Client secret**은 OAuth용이며 이 연동에서는 사용되지 않습니다. 이 둘을 혼동하면 모든 webhook delivery에서 혼란스러운 `401 invalid signature`가 발생합니다.
|
||||
</Callout>
|
||||
|
||||
### 2. 환경 변수 설정
|
||||
|
||||
API 서버에서:
|
||||
|
||||
```dotenv
|
||||
GITHUB_APP_SLUG=multica-acme
|
||||
GITHUB_WEBHOOK_SECRET=<the webhook secret you generated>
|
||||
```
|
||||
|
||||
두 변수 모두 필수입니다. 둘 중 하나라도 누락되면:
|
||||
|
||||
- Settings의 `Connect GitHub`이 **비활성화**되고 "not configured" 힌트가 표시됩니다.
|
||||
- `/api/webhooks/github` 엔드포인트가 **`503 github webhooks not configured`**를 반환합니다 — Multica는 secret 없이 이벤트를 처리하기를 거부하며, 모든 서명을 조용히 유효한 것으로 취급하지 않습니다.
|
||||
|
||||
`FRONTEND_ORIGIN`도 설정되어 있어야 합니다(어떤 프로덕션 자체 호스팅이든 이미 설정되어 있습니다). 설치 후 setup 콜백이 사용자를 `<FRONTEND_ORIGIN>/settings?tab=github`으로 다시 돌려보냅니다.
|
||||
|
||||
env 변수를 설정한 후 API를 재시작하세요.
|
||||
|
||||
### 3. 마이그레이션 실행
|
||||
|
||||
이 연동은 테이블을 마이그레이션 `079_github_integration`으로 제공합니다. 기존 배포를 업그레이드하는 경우:
|
||||
|
||||
```bash
|
||||
make migrate-up
|
||||
```
|
||||
|
||||
세 개의 테이블이 생성됩니다: `github_installation`, `github_pull_request`, `issue_pull_request`. 이들은 워크스페이스와 함께 cascade-delete되므로, 워크스페이스를 제거하면 자동으로 정리됩니다.
|
||||
|
||||
### 4. UI에서 연결
|
||||
|
||||
Multica에서:
|
||||
|
||||
1. owner 또는 admin 권한으로 **설정 → GitHub**를 엽니다.
|
||||
2. **Connect GitHub**를 클릭합니다. GitHub가 새 탭에서 열립니다.
|
||||
3. 액세스를 부여할 저장소를 선택하고 **Install**합니다.
|
||||
4. GitHub가 `<api-host>/api/github/setup`으로 리디렉션하여 설치를 기록한 뒤, `<FRONTEND_ORIGIN>/settings?tab=github&github_connected=1`로 돌려보냅니다.
|
||||
|
||||
그 후, 브랜치 / 제목 / 본문에 이슈 식별자가 들어 있는 PR을 열어 보세요 — 몇 초 내에 해당 이슈의 상세 페이지에 Pull requests 블록이 나타납니다.
|
||||
|
||||
### 5. curl 프로브로 검증
|
||||
|
||||
설치 후 GitHub의 **Recent Deliveries** 페이지에서 `401 invalid signature`가 보고된다면, 양쪽의 secret이 다른 것입니다. 어느 쪽이 잘못되었는지 가장 빠르게 찾는 방법은 GitHub를 우회하는 것입니다.
|
||||
|
||||
```bash
|
||||
SECRET="<the value you put in GITHUB_WEBHOOK_SECRET>"
|
||||
BODY='{"zen":"test"}'
|
||||
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')
|
||||
curl -i -X POST https://<api-host>/api/webhooks/github \
|
||||
-H "X-Hub-Signature-256: sha256=$SIG" \
|
||||
-H "X-GitHub-Event: ping" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$BODY"
|
||||
```
|
||||
|
||||
| HTTP 상태 | 의미 | 해결 방법 |
|
||||
|---|---|---|
|
||||
| `200` `{"ok":"pong"}` | 서버가 로드한 secret이 `$SECRET`과 일치합니다. 불일치는 GitHub 쪽에 있습니다. | App → Webhook secret 편집 → **동일한 값을 붙여넣기** → **Save changes**(저장하지 않고 필드 밖을 클릭하면 이전 secret이 유지됩니다). 재전송하세요. |
|
||||
| `401 invalid signature` | 서버가 로드한 secret이 생각하는 값이 **아닙니다**. | env 변수가 실행 중인 프로세스에 적용되었는지 확인하세요(예: `kubectl exec` → `echo -n "$GITHUB_WEBHOOK_SECRET" \| wc -c`). 재배포하세요. |
|
||||
| `503 github webhooks not configured` | 프로세스에서 `GITHUB_WEBHOOK_SECRET`이 비어 있습니다. | env 변수를 설정하고 API를 재시작하세요. |
|
||||
|
||||
## 제한 사항
|
||||
|
||||
현재 알아 둬야 할 몇 가지 거친 부분이 있습니다.
|
||||
|
||||
- **아직 수동 연결 UI가 없습니다** — PR을 연결하는 유일한 방법은 브랜치, 제목, 본문에 식별자를 두는 것입니다.
|
||||
- **CI / 체크 상태가 없습니다** — PR 자체만 미러링됩니다. 빌드 상태, 리뷰 댓글, 리뷰어는 Multica에 표시되지 않습니다.
|
||||
- 머지 → 완료 규칙에 대한 **워크스페이스 수준 설정이 없습니다** — 고정된 기본값입니다(`cancelled`가 아닌 한 `merged → done`). 워크스페이스에서 커스터마이즈할 수 있는 매핑은 향후 추가될 예정입니다.
|
||||
- **하나의 이슈에 여러 PR이 연결된 경우 머지가 보수적입니다** — 두 PR이 모두 `MUL-123`을 참조하고 첫 번째가 머지되면, 이슈는 즉시 `Done`으로 이동합니다. 진행하기 전에 연결된 모든 PR이 해결되기를 기다리는 후속 변경이 진행 중입니다.
|
||||
|
||||
## 다음
|
||||
|
||||
- [이슈](/issues) — PR에서 참조하는 이슈 식별자(`MUL-123`)
|
||||
- [워크스페이스](/workspaces) — 워크스페이스별 이슈 prefix를 설정하는 곳
|
||||
- [환경 변수](/environment-variables) — 위의 GitHub 변수를 포함한 전체 env 참조
|
||||
@@ -1,54 +0,0 @@
|
||||
---
|
||||
title: Multica の仕組み
|
||||
description: 3 つのコア構成要素(サーバー / デーモン / AI コーディングツール)がどのように連携してエージェントの作業を実行するかを説明します。
|
||||
---
|
||||
|
||||
import { ArchitectureDiagram } from "@/components/architecture-diagram";
|
||||
|
||||
Multica は**分散型**プラットフォームです。あなたが目にする Web インターフェースは表に見えている部分にすぎず、実際の作業は 3 つの構成要素が処理します。**Multica サーバー**はデータを保持します([ワークスペース](/workspaces)、[イシュー](/issues)、[メンバー](/members-roles)、[タスク](/tasks)キューなど)。**[デーモン](/daemon-runtimes)**はあなた自身のマシンで実行され、タスクを取得して AI コーディングツールを駆動します。そして **[AI コーディングツール](/providers)**(Claude Code、Codex、その他のローカル CLI)が、実際にコードを書く構成要素です。これが Multica と Linear や Jira との最大の違いです。**[エージェント](/agents)は当社のサーバーではなく、あなたのマシンで実行されます。**
|
||||
|
||||
## 3 つのコア構成要素
|
||||
|
||||
<ArchitectureDiagram />
|
||||
|
||||
- **Multica サーバー** — あなたが目にするワークスペース、イシュー一覧、コメントスレッドは、すべてここのデータベースに保存されます。また、あなたと同僚の間でリアルタイム更新をプッシュする WebSocket ハブでもあります。エージェントのタスクは**実行しません**。
|
||||
- **デーモン** — Multica CLI の一部であり、あなた自身のマシンで実行されます。起動時にローカルにインストールされた AI コーディングツールを検出し、サーバーに登録したうえで、3 秒ごとにタスクをポーリングし、15 秒ごとにハートビートを送信し始めます。
|
||||
- **AI コーディングツール** — 次の 12 種類のうちの 1 つ(または複数を並列で): [Antigravity](/providers#antigravity)、[Claude Code](/providers#claude-code)、[Codex](/providers#codex)、[Cursor](/providers#cursor)、[Copilot](/providers#copilot)、[Gemini](/providers#gemini)、[Hermes](/providers#hermes)、[Kimi](/providers#kimi)、[Kiro CLI](/providers#kiro-cli)、[OpenCode](/providers#opencode)、[OpenClaw](/providers#openclaw)、[Pi](/providers#pi)。デーモンがタスクを取得した後は、これらのツールを使って実際の作業を行います。
|
||||
|
||||
ツールチェーンがローカルに留まるため、**あなたの API キー、コードディレクトリ、認可されたツール**は、あなたのマシン上でのみ使用されます。Multica サーバーはそのいずれも目にすることはありません。これはセルフホストでも Cloud でも同じように適用されます。
|
||||
|
||||
## タスクのライフサイクル
|
||||
|
||||
最も一般的なシナリオである、イシューをエージェントに割り当てる場合を見てみましょう。
|
||||
|
||||
1. あなたが Web UI で割り当てをクリックします。ブラウザが Multica サーバーへ HTTP リクエストを送ります。
|
||||
2. サーバーがそのイシューの担当者をエージェントに設定し、同時にタスクキューに状態 `queued` の実行タスクを作成します。
|
||||
3. あなたのマシンにあるデーモンが、次のポーリング(3 秒以内)でタスクを取得します。タスクの状態が `dispatched` に変わります。
|
||||
4. デーモンがローカルに隔離された作業ディレクトリを作成し、該当する AI コーディングツールを呼び出します。タスクの状態が `running` に変わります。
|
||||
5. AI がローカルでコードを書き、テストを実行し、コメントをサーバーへ投稿します。
|
||||
6. 実行が終了します。デーモンが結果(成功 / 失敗)をサーバーに報告し、タスクの状態が `completed` または `failed` に変わります。あなたは Web UI で進捗の更新をリアルタイムに(WebSocket を通じて)確認します。
|
||||
|
||||
詳しい動作の仕組みは、[デーモンとランタイム](/daemon-runtimes)および[タスク](/tasks)を参照してください。
|
||||
|
||||
## エージェントを動かす 4 つの方法
|
||||
|
||||
「イシューの割り当て」だけではありません。Multica にはコラボレーションのスタイルごとに 1 つずつ、4 つのトリガーがあります。
|
||||
|
||||
| 方法 | 一般的なシナリオ | ドキュメント |
|
||||
|---|---|---|
|
||||
| **イシューの割り当て** | 最も一般的な方法。イシューをエージェントに割り当てると、自分で作業を始めます | [イシューの割り当て](/assigning-issues) |
|
||||
| **コメントでエージェントを @メンション** | 「これちょっと見てくれる?」— 担当者や状態を変えず、コメント 1 つで実行を開始します | [エージェントのメンション](/mentioning-agents) |
|
||||
| **ダイレクトチャット** | イシューに紐づかない独立した会話 — 質問したり、イシューの下書きを作らせたりします | [チャット](/chat) |
|
||||
| **オートパイロット(スケジュール)** | 常時の指示 — 「毎週月曜の朝にスタンドアップのまとめをして」のようなもの | [オートパイロット](/autopilots) |
|
||||
|
||||
## ランタイム: どこで実行され、ツールは何個か
|
||||
|
||||
**ランタイム**とは「デーモン × 1 つの AI コーディングツール」の組み合わせです。あるマシンのデーモンに Claude Code と Codex の両方がインストールされており、2 つのワークスペースに参加している場合、Multica は 4 つの独立したランタイム(ワークスペース 2 個 × ツール 2 個)を登録します。
|
||||
|
||||
現在は**ローカルデーモン**のランタイムモデルのみがサポートされています。クラウドランタイム(自分のマシンを起動しておく必要がない方式)は**近日提供予定**で、現在はウェイトリストの登録のみを受け付けています。[ダウンロード](https://multica.ai/download)ページでお申し込みください。
|
||||
|
||||
## 次のステップ
|
||||
|
||||
- [Cloud Quickstart](/cloud-quickstart) — 5 分で Multica Cloud に接続する
|
||||
- [Self-Host Quickstart](/self-host-quickstart) — 自前のバックエンドを実行する
|
||||
- [デーモンとランタイム](/daemon-runtimes) — アーキテクチャが依存する構成要素を深掘りする
|
||||
@@ -1,54 +0,0 @@
|
||||
---
|
||||
title: Multica의 작동 방식
|
||||
description: 세 가지 핵심 구성 요소(서버 / 데몬 / AI 코딩 도구)가 어떻게 협력하여 에이전트의 작업을 실행하는지 설명합니다.
|
||||
---
|
||||
|
||||
import { ArchitectureDiagram } from "@/components/architecture-diagram";
|
||||
|
||||
Multica는 **분산형** 플랫폼입니다. 여러분이 보는 웹 인터페이스는 겉으로 드러난 부분일 뿐이고, 실제 작업은 세 가지 구성 요소가 처리합니다. **Multica 서버**는 데이터를 소유합니다([워크스페이스](/workspaces), [이슈](/issues), [멤버](/members-roles), [작업](/tasks) 대기열 등). **[데몬](/daemon-runtimes)**은 여러분 자신의 기기에서 실행되며 작업을 가져와 AI 코딩 도구를 구동합니다. 그리고 **[AI 코딩 도구](/providers)**(Claude Code, Codex, 그 밖의 로컬 CLI)는 실제로 코드를 작성하는 구성 요소입니다. 이것이 Multica와 Linear 또는 Jira의 가장 큰 차이입니다. **[에이전트](/agents)는 우리 서버가 아니라 여러분의 기기에서 실행됩니다.**
|
||||
|
||||
## 세 가지 핵심 구성 요소
|
||||
|
||||
<ArchitectureDiagram />
|
||||
|
||||
- **Multica 서버** — 여러분이 보는 워크스페이스, 이슈 목록, 댓글 스레드는 모두 이곳의 데이터베이스에 저장됩니다. 또한 여러분과 동료 사이의 실시간 업데이트를 푸시하는 WebSocket 허브이기도 합니다. 에이전트 작업은 **실행하지 않습니다.**
|
||||
- **데몬** — Multica CLI의 일부로, 여러분 자신의 기기에서 실행됩니다. 시작 시 로컬에 설치된 AI 코딩 도구를 감지하고, 서버에 등록한 다음, 3초마다 작업을 폴링하고 15초마다 하트비트를 전송하기 시작합니다.
|
||||
- **AI 코딩 도구** — 다음 열두 가지 중 하나(또는 여러 개를 병렬로): [Antigravity](/providers#antigravity), [Claude Code](/providers#claude-code), [Codex](/providers#codex), [Cursor](/providers#cursor), [Copilot](/providers#copilot), [Gemini](/providers#gemini), [Hermes](/providers#hermes), [Kimi](/providers#kimi), [Kiro CLI](/providers#kiro-cli), [OpenCode](/providers#opencode), [OpenClaw](/providers#openclaw), [Pi](/providers#pi). 데몬이 작업을 가져온 뒤에는 이러한 도구를 사용해 실제 작업을 수행합니다.
|
||||
|
||||
도구 체인이 로컬에 유지되므로 **여러분의 API 키, 코드 디렉터리, 인증된 도구**는 오직 여러분의 기기에서만 사용됩니다. Multica 서버는 그중 어떤 것도 보지 못합니다. 이는 자체 호스팅을 하든 Cloud를 사용하든 동일하게 적용됩니다.
|
||||
|
||||
## 작업의 수명 주기
|
||||
|
||||
가장 흔한 시나리오인, 이슈를 에이전트에게 할당하는 경우를 살펴보겠습니다.
|
||||
|
||||
1. 여러분이 웹 UI에서 할당을 클릭합니다. 브라우저가 Multica 서버로 HTTP 요청을 보냅니다.
|
||||
2. 서버가 해당 이슈의 담당자를 에이전트로 설정하고, 동시에 작업 대기열에 상태 `queued`인 실행 작업을 생성합니다.
|
||||
3. 여러분의 기기에 있는 데몬이 다음 폴링(3초 이내) 때 작업을 가져옵니다. 작업 상태가 `dispatched`로 바뀝니다.
|
||||
4. 데몬이 로컬에 격리된 작업 디렉터리를 생성하고 해당 AI 코딩 도구를 호출합니다. 작업 상태가 `running`으로 바뀝니다.
|
||||
5. AI가 로컬에서 코드를 작성하고, 테스트를 실행하고, 댓글을 서버로 다시 게시합니다.
|
||||
6. 실행이 종료됩니다. 데몬이 결과(성공 / 실패)를 서버에 보고하고, 작업 상태가 `completed` 또는 `failed`로 바뀝니다. 여러분은 웹 UI에서 진행 상황 업데이트를 실시간으로(WebSocket을 통해) 확인합니다.
|
||||
|
||||
자세한 동작 원리는 [데몬과 런타임](/daemon-runtimes) 및 [작업](/tasks)을 참조하세요.
|
||||
|
||||
## 에이전트를 작동시키는 네 가지 방법
|
||||
|
||||
"이슈 할당"만 있는 것은 아닙니다. Multica에는 협업 스타일별로 하나씩, 4가지 트리거가 있습니다.
|
||||
|
||||
| 방법 | 일반적인 시나리오 | 문서 |
|
||||
|---|---|---|
|
||||
| **이슈 할당** | 가장 흔한 방법. 이슈를 에이전트에게 할당하면 스스로 작업을 시작합니다 | [이슈 할당하기](/assigning-issues) |
|
||||
| **댓글에서 에이전트 @멘션** | "이거 한번 봐 줘" — 담당자나 상태를 바꾸지 않고 댓글 하나로 실행을 시작합니다 | [에이전트 멘션하기](/mentioning-agents) |
|
||||
| **다이렉트 채팅** | 이슈에 묶이지 않은 독립적인 대화 — 질문하거나, 이슈 초안을 작성하게 합니다 | [채팅](/chat) |
|
||||
| **오토파일럿(예약)** | 상시 지시 — "매주 월요일 아침에 스탠드업 요약을 해 줘" 같은 것 | [오토파일럿](/autopilots) |
|
||||
|
||||
## 런타임: 어디서 실행되고, 도구는 몇 개인가
|
||||
|
||||
**런타임**은 "데몬 × 하나의 AI 코딩 도구"의 조합입니다. 한 기기의 데몬에 Claude Code와 Codex가 모두 설치되어 있고 두 개의 워크스페이스에 참여해 있다면, Multica는 4개의 독립적인 런타임(워크스페이스 2개 × 도구 2개)을 등록합니다.
|
||||
|
||||
현재는 **로컬 데몬** 런타임 모델만 지원됩니다. 클라우드 런타임(여러분 자신의 기기를 켜 둘 필요가 없는 방식)은 **곧 제공될 예정**이며, 현재는 대기자 명단 등록만 받고 있습니다. [다운로드](https://multica.ai/download) 페이지에서 신청하세요.
|
||||
|
||||
## 다음 단계
|
||||
|
||||
- [Cloud Quickstart](/cloud-quickstart) — 5분 만에 Multica Cloud에 연결하기
|
||||
- [Self-Host Quickstart](/self-host-quickstart) — 자체 백엔드 실행하기
|
||||
- [데몬과 런타임](/daemon-runtimes) — 아키텍처가 의존하는 구성 요소에 대한 심층 분석
|
||||
@@ -1,65 +0,0 @@
|
||||
---
|
||||
title: インボックスと購読
|
||||
description: Multica がいつ通知を送るか、そして関心のないイシューをミュートする方法。
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
|
||||
インボックスは Multica があなたを**割り込む**場所です。あなたに割り当てられた[イシュー](/issues)、[`@` メンション](/comments)、そしてあなたが購読しているイシューのアクティビティがすべてここに届きます。
|
||||
|
||||
あなたは**購読**と**購読解除**を通じて、どのイシューのアクティビティが自分に届くかを制御します。
|
||||
|
||||
## インボックスに表示されるもの
|
||||
|
||||
次のイベントがあなたのインボックスに通知を届けます。
|
||||
|
||||
- **イシューの割り当て / 割り当て解除 / 再割り当て** — あなたが新しい担当者(または以前の担当者)になると通知を受け取ります
|
||||
- **あなたが購読しているイシューのステータス、優先度、期限の変更**
|
||||
- **あなたが購読しているイシューの新しいコメント**
|
||||
- **あなたがコメントで `@` メンションされた** — 購読しているかどうかに関係なく届きます
|
||||
- **誰かがあなたのイシューやコメントにリアクションした**
|
||||
- **あなたが割り当てたエージェントの[タスク](/tasks)が失敗した**
|
||||
|
||||
## `@all` はワークスペース全体に通知します
|
||||
|
||||
`@all` は特殊な対象です。ワークスペースの**すべてのメンバー**に通知をプッシュします。
|
||||
|
||||
<Callout type="warning">
|
||||
**`@all` は控えめに使ってください。** 50 人規模のワークスペースでは、`@all` コメント 1 つで瞬時に 50 件のインボックス通知が生成されます。日常的な議論ではなく、重大な事案(プロダクション障害、マイルストーンの告知)にのみ使ってください。
|
||||
</Callout>
|
||||
|
||||
## エージェントは通知を受け取りません
|
||||
|
||||
エージェントは**決して**インボックス通知を受け取りません。担当者や作成者であるときも、コメントで `@` メンションされたときも受け取りません。
|
||||
|
||||
これはバグではありません。エージェントはインボックスを読みません。エージェントは[**即時トリガー**](/assigning-issues)方式で動作します。イシューを割り当てたり、コメントでエージェントを `@` メンションしたりすると、ただちにそのエージェント向けのタスクが始まります。インボックスは人間のためのリマインダーの仕組みであり、エージェントにとっては何の意味も持ちません。
|
||||
|
||||
## 購読のルール
|
||||
|
||||
次の 4 つの状況で、あなたはイシューに**自動購読**されます。
|
||||
|
||||
- あなたがそのイシューを**作成**した場合
|
||||
- あなたがそのイシューに**割り当て**られた場合
|
||||
- あなたがそのイシューに**コメント**した場合
|
||||
- あなたがそのイシューまたはそのコメントで **`@` メンション**された場合
|
||||
|
||||
自動購読は一度だけ起こります。作成者であり同時にメンション対象でもあっても、2 回購読されることはありません。
|
||||
|
||||
<Callout type="warning">
|
||||
**再割り当ては自動で購読を解除しません。** あなたが以前は担当者だったのに交代させられた場合でも、**そのイシューの更新を引き続き受け取ります** — 自動購読がデータベースにそのまま残っているためです。
|
||||
|
||||
通知を受け取らないようにするには、イシューを開いて手動で購読を解除してください。
|
||||
</Callout>
|
||||
|
||||
また、どのイシュー(無関係なイシューでも)でも**手動で購読**したり、どの自動購読でも**手動で購読解除**したりできます。UI ではイシューページの右パネルを使い、CLI では `multica issue subscriber add/remove` を使ってください。
|
||||
|
||||
## 子イシューのステータス変更は親イシューに伝播します
|
||||
|
||||
子イシューの**ステータス**が変更されると、親イシューの購読者も通知を受け取ります。たとえ子イシューを購読していなくても同様です。
|
||||
|
||||
これは**ステータスにのみ**適用されます。子イシューのコメント、優先度、期限の変更は親イシューに伝播**しません**。
|
||||
|
||||
## 次へ
|
||||
|
||||
- [コメントとメンション](/comments) — `@` メンションの仕組みと注意点
|
||||
- [エージェントにイシューを割り当てる](/assigning-issues) — エージェントがトリガーされる仕組み(そしてエージェントがインボックスを読まない理由)
|
||||
@@ -1,65 +0,0 @@
|
||||
---
|
||||
title: 인박스와 구독
|
||||
description: Multica가 언제 알림을 보내는지, 그리고 관심 없는 이슈를 음소거하는 방법.
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
|
||||
인박스는 Multica가 여러분을 **방해**하는 곳입니다. 여러분에게 할당된 [이슈](/issues), [`@` 멘션](/comments), 그리고 여러분이 구독한 이슈의 활동이 모두 여기에 도착합니다.
|
||||
|
||||
여러분은 **구독**과 **구독 취소**를 통해 어떤 이슈 활동이 여러분에게 도달할지 제어합니다.
|
||||
|
||||
## 인박스에 표시되는 것
|
||||
|
||||
다음 이벤트가 여러분의 인박스로 알림을 전달합니다.
|
||||
|
||||
- **이슈 할당 / 할당 해제 / 재할당** — 여러분이 새 담당자(또는 이전 담당자)가 되면 알림을 받습니다
|
||||
- **여러분이 구독한 이슈의 상태, 우선순위, 마감일 변경**
|
||||
- **여러분이 구독한 이슈의 새 댓글**
|
||||
- **여러분이 댓글에서 `@`로 멘션됨** — 구독 여부와 관계없이 전달됩니다
|
||||
- **누군가 여러분의 이슈나 댓글에 반응함**
|
||||
- **여러분이 할당한 에이전트 [작업](/tasks)이 실패함**
|
||||
|
||||
## `@all`은 워크스페이스 전체에 알립니다
|
||||
|
||||
`@all`은 특수한 대상입니다. 워크스페이스의 **모든 멤버**에게 알림을 푸시합니다.
|
||||
|
||||
<Callout type="warning">
|
||||
**`@all`은 아껴서 사용하세요.** 50명 규모의 워크스페이스에서 `@all` 댓글 하나는 즉시 50개의 인박스 알림을 생성합니다. 일상적인 논의가 아니라 중대한 사안(프로덕션 장애, 마일스톤 공지)에만 사용하세요.
|
||||
</Callout>
|
||||
|
||||
## 에이전트는 알림을 받지 않습니다
|
||||
|
||||
에이전트는 **절대** 인박스 알림을 받지 않습니다. 담당자나 생성자일 때도, 댓글에서 `@`로 멘션될 때도 받지 않습니다.
|
||||
|
||||
이것은 버그가 아닙니다. 에이전트는 인박스를 읽지 않습니다. 에이전트는 [**즉시 트리거**](/assigning-issues) 방식으로 동작합니다. 이슈를 할당하거나 댓글에서 에이전트를 `@`로 멘션하면 곧바로 해당 에이전트를 위한 작업이 시작됩니다. 인박스는 사람을 위한 알림 메커니즘이며, 에이전트에게는 아무런 의미가 없습니다.
|
||||
|
||||
## 구독 규칙
|
||||
|
||||
다음 네 가지 상황에서 여러분은 이슈에 **자동 구독**됩니다.
|
||||
|
||||
- 여러분이 이슈를 **생성**한 경우
|
||||
- 여러분이 이슈에 **할당**된 경우
|
||||
- 여러분이 이슈에 **댓글**을 단 경우
|
||||
- 여러분이 이슈 또는 그 댓글에서 **`@`로 멘션**된 경우
|
||||
|
||||
자동 구독은 한 번만 일어납니다. 생성자이면서 동시에 멘션 대상이더라도 두 번 구독되지는 않습니다.
|
||||
|
||||
<Callout type="warning">
|
||||
**재할당은 자동으로 구독을 취소하지 않습니다.** 여러분이 예전에 담당자였다가 교체되었더라도, **그 이슈의 업데이트를 계속 받게 됩니다.** 자동 구독이 데이터베이스에 그대로 남아 있기 때문입니다.
|
||||
|
||||
더 이상 알림을 받지 않으려면 이슈를 열어 직접 구독을 취소하세요.
|
||||
</Callout>
|
||||
|
||||
또한 어떤 이슈든(관련 없는 이슈라도) **직접 구독**하거나, 어떤 자동 구독이든 **직접 구독을 취소**할 수 있습니다. UI에서는 이슈 페이지의 오른쪽 패널을 사용하고, CLI에서는 `multica issue subscriber add/remove`를 사용하세요.
|
||||
|
||||
## 하위 이슈의 상태 변경은 상위 이슈로 전파됩니다
|
||||
|
||||
하위 이슈의 **상태**가 변경되면, 상위 이슈의 구독자도 알림을 받습니다. 그들이 하위 이슈를 구독하지 않았더라도 마찬가지입니다.
|
||||
|
||||
이것은 **상태에만** 적용됩니다. 하위 이슈의 댓글, 우선순위, 마감일 변경은 상위 이슈로 전파되지 **않습니다**.
|
||||
|
||||
## 다음
|
||||
|
||||
- [댓글과 멘션](/comments) — `@` 멘션의 작동 방식과 주의할 점
|
||||
- [에이전트에게 이슈 할당하기](/assigning-issues) — 에이전트가 트리거되는 방식(그리고 에이전트가 인박스를 읽지 않는 이유)
|
||||
@@ -1,50 +0,0 @@
|
||||
---
|
||||
title: ようこそ
|
||||
description: 人間と AI エージェントが同じワークスペースで一緒に働く、タスクコラボレーションプラットフォーム。
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
|
||||
Multica は、人間と AI [エージェント](/agents)が同じ[ワークスペース](/workspaces)で一緒に働くタスクコラボレーションプラットフォームです。同僚に仕事を渡すのと同じように[エージェントにイシューを割り当てる](/assigning-issues)ことができ、エージェントは作業を実行し、進捗を報告し、コメントで返信します。また、[チャットウィンドウを開いて直接対話](/chat)し、イシューの下書き作成、質問への回答、単発のリクエスト処理を任せることもできます。
|
||||
|
||||
このページでは、エージェントがどこで実行されるか、そして Multica を使い始めるさまざまな方法を説明します。
|
||||
|
||||
## エージェントが実行される場所
|
||||
|
||||
エージェントは Multica のサーバー上でタスクを実行**しません**。現在 Multica は 1 つのランタイムモデルをサポートしています。
|
||||
|
||||
- **ローカル[デーモン](/daemon-runtimes)** — 自分のマシンで `multica daemon` を実行すると、デーモンがローカルにインストールされた [AI コーディングツール](/providers)を駆動します。現在 12 種類が標準で組み込まれています: [Antigravity](/providers#antigravity)、[Claude Code](/providers#claude-code)、[Codex](/providers#codex)、[Cursor](/providers#cursor)、[Copilot](/providers#copilot)、[Gemini](/providers#gemini)、[Hermes](/providers#hermes)、[Kimi](/providers#kimi)、[Kiro CLI](/providers#kiro-cli)、[OpenCode](/providers#opencode)、[OpenClaw](/providers#openclaw)、[Pi](/providers#pi)。API キー、ツールチェーン、コードディレクトリはすべて自分のマシンに留まります。
|
||||
|
||||
<Callout type="info">
|
||||
**クラウドランタイムが近日提供予定です。** 現在はウェイトリストのみで運用されています。提供が開始されればローカルデーモンは不要になり、エージェントのタスクは Multica Cloud 上で直接実行されます。[ダウンロード](https://multica.ai/download)ページで登録すると通知を受け取れます。
|
||||
</Callout>
|
||||
|
||||
## Multica を使う 3 つの方法
|
||||
|
||||
最初の 2 つのカードは**バックエンドの選択肢**で、Multica サーバーがどこで実行されるかを決めます。3 つ目は**クライアントの選択肢**で、どのインターフェースを使うかを決めます。デスクトップアプリはどちらのバックエンドとも組み合わせて使えます。
|
||||
|
||||
<NumberedCards>
|
||||
<NumberedCard number="01" title="Multica Cloud" href="/cloud-quickstart" tag="ウェイトリスト">
|
||||
マネージドバックエンド。CLI をインストールし、ローカルでデーモンを実行してから、Multica がホスティングするサーバーに接続します。約 5 分で完了します。
|
||||
</NumberedCard>
|
||||
<NumberedCard number="02" title="セルフホスト" href="/self-host-quickstart" tag="Docker · Helm">
|
||||
Docker Compose を使って自分のサーバーでバックエンド全体を実行します。データベース、サーバー、ストレージがすべて自分のインフラ上に配置されます。
|
||||
</NumberedCard>
|
||||
<NumberedCard number="03" title="デスクトップアプリ" href="/desktop-app" tag="推奨">
|
||||
ネイティブのマルチタブウィンドウ。CLI が内蔵されており、起動時にデーモンを自動的に開始します。インストール後に実行するコマンドは一切ありません。Multica Cloud またはセルフホストのバックエンドに接続します。
|
||||
</NumberedCard>
|
||||
</NumberedCards>
|
||||
|
||||
## 次のステップ
|
||||
|
||||
<NumberedSteps>
|
||||
<Step number="01" title="ランタイムモデルから理解する">
|
||||
[Multica の仕組み](/how-multica-works) — 30 秒で読めて、「サーバーはエージェントを実行せず、エージェントはユーザーのマシンで実行される」という点をしっかり押さえられます。
|
||||
</Step>
|
||||
<Step number="02" title="始める方法を選ぶ">
|
||||
上記の 3 つから 1 つを選びましょう。ほとんどの方は[デスクトップアプリ](/desktop-app)から始めます。CLI のセットアップが不要で、5 分で動き出します。
|
||||
</Step>
|
||||
<Step number="03" title="最初のイシューを割り当てる">
|
||||
[イシュー](/issues)を作成し、担当者として同僚の代わりにエージェントを選びましょう。エージェントが結果を届けるのを待つだけです。
|
||||
</Step>
|
||||
</NumberedSteps>
|
||||
@@ -1,50 +0,0 @@
|
||||
---
|
||||
title: 환영합니다
|
||||
description: 인간과 AI 에이전트가 같은 워크스페이스에서 함께 일하는 작업 협업 플랫폼.
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
|
||||
Multica는 인간과 AI [에이전트](/agents)가 같은 [워크스페이스](/workspaces)에서 함께 일하는 작업 협업 플랫폼입니다. 동료에게 일을 넘기듯이 [에이전트에게 이슈를 할당](/assigning-issues)할 수 있으며 — 에이전트는 작업을 실행하고, 진행 상황을 보고하며, 댓글로 답합니다. 또한 [채팅 창을 열어 직접 대화](/chat)하면서 이슈 초안 작성, 질문 답변, 일회성 요청 처리를 맡길 수도 있습니다.
|
||||
|
||||
이 페이지에서는 에이전트가 어디에서 실행되는지, 그리고 Multica를 사용하기 시작하는 여러 방법을 설명합니다.
|
||||
|
||||
## 에이전트가 실행되는 곳
|
||||
|
||||
에이전트는 Multica 서버에서 작업을 실행하지 **않습니다**. 현재 Multica는 하나의 런타임 모델을 지원합니다:
|
||||
|
||||
- **로컬 [데몬](/daemon-runtimes)** — 자신의 기기에서 `multica daemon`을 실행하면, 데몬이 로컬에 설치된 [AI 코딩 도구](/providers)를 구동합니다. 현재 열두 가지가 기본 내장되어 있습니다: [Antigravity](/providers#antigravity), [Claude Code](/providers#claude-code), [Codex](/providers#codex), [Cursor](/providers#cursor), [Copilot](/providers#copilot), [Gemini](/providers#gemini), [Hermes](/providers#hermes), [Kimi](/providers#kimi), [Kiro CLI](/providers#kiro-cli), [OpenCode](/providers#opencode), [OpenClaw](/providers#openclaw), [Pi](/providers#pi). API 키, 툴체인, 코드 디렉터리는 모두 자신의 기기에 머뭅니다.
|
||||
|
||||
<Callout type="info">
|
||||
**클라우드 런타임이 곧 제공됩니다.** 현재는 대기 명단으로만 운영됩니다. 출시되면 로컬 데몬이 필요 없어지며 — 에이전트 작업이 Multica Cloud에서 직접 실행됩니다. [다운로드](https://multica.ai/download) 페이지에서 등록하면 알림을 받을 수 있습니다.
|
||||
</Callout>
|
||||
|
||||
## Multica를 사용하는 세 가지 방법
|
||||
|
||||
처음 두 카드는 **백엔드 선택지** — Multica 서버가 어디에서 실행되는지를 정합니다. 세 번째는 **클라이언트 선택지** — 어떤 인터페이스를 사용할지를 정합니다. 데스크톱 앱은 두 백엔드 중 어느 쪽과도 함께 사용할 수 있습니다.
|
||||
|
||||
<NumberedCards>
|
||||
<NumberedCard number="01" title="Multica Cloud" href="/cloud-quickstart" tag="대기 명단">
|
||||
관리형 백엔드. CLI를 설치하고 로컬에서 데몬을 실행한 뒤, Multica가 호스팅하는 서버에 연결합니다. 약 5분이면 됩니다.
|
||||
</NumberedCard>
|
||||
<NumberedCard number="02" title="자체 호스팅" href="/self-host-quickstart" tag="Docker · Helm">
|
||||
Docker Compose로 자신의 서버에서 전체 백엔드를 실행합니다. 데이터베이스, 서버, 스토리지가 모두 자신의 인프라에 위치합니다.
|
||||
</NumberedCard>
|
||||
<NumberedCard number="03" title="데스크톱 앱" href="/desktop-app" tag="추천">
|
||||
네이티브 멀티탭 창. CLI가 내장되어 있고 실행 시 데몬을 자동으로 시작합니다 — 설치 후 실행할 명령이 전혀 없습니다. Multica Cloud 또는 자체 호스팅 백엔드에 연결합니다.
|
||||
</NumberedCard>
|
||||
</NumberedCards>
|
||||
|
||||
## 다음 단계
|
||||
|
||||
<NumberedSteps>
|
||||
<Step number="01" title="런타임 모델부터 이해하기">
|
||||
[Multica는 어떻게 작동하나요](/how-multica-works) — 30초면 읽을 수 있으며, "서버는 에이전트를 실행하지 않고 에이전트는 사용자의 기기에서 실행된다"는 점을 확실히 짚어줍니다.
|
||||
</Step>
|
||||
<Step number="02" title="시작할 방법 고르기">
|
||||
위의 세 가지 중 하나를 선택하세요 — 대부분은 [데스크톱 앱](/desktop-app)으로 시작합니다. CLI 설정이 필요 없고 5분이면 실행됩니다.
|
||||
</Step>
|
||||
<Step number="03" title="첫 이슈 할당하기">
|
||||
[이슈](/issues)를 만들고 담당자로 동료 대신 에이전트를 선택하세요. 에이전트가 결과를 가져올 때까지 기다리면 됩니다.
|
||||
</Step>
|
||||
</NumberedSteps>
|
||||
@@ -1,180 +0,0 @@
|
||||
---
|
||||
title: エージェントランタイムをインストールする
|
||||
description: Multica はあなたのマシンにインストールされている AI コーディングツールを駆動します。このページでは、デーモンがそれらを検出できるように、サポートされている 12 種のツールをそれぞれインストールする方法を説明します。
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
|
||||
Multica における**ランタイム**とは、あなたのマシンのデーモンと、デーモンが `PATH` で見つけた AI コーディングツール 1 つが組になったものです。オンボーディングの「ランタイムを接続」ステップで **No supported tools detected** と表示される場合、それはデーモンが `PATH` をスキャンしたものの、駆動方法を知っている 12 種のツールのいずれも見つけられなかったことを意味します。以下のツールのいずれか(または複数)をインストールしてから、そのステップに戻って再スキャンしてください — 数秒以内にランタイムが表示されます。
|
||||
|
||||
このページは次のドキュメントのインストール側の補完ドキュメントです。
|
||||
|
||||
- [デーモンとランタイム](/daemon-runtimes) — 検出の仕組み
|
||||
- [AI コーディングツールマトリクス](/providers) — 各ツールができることとできないこと(セッション再開、MCP、モデル選択)
|
||||
|
||||
<Callout type="info">
|
||||
Multica サーバーがあなたの API キーやツール自体を見ることは決してありません。以下のすべて — インストール、認証、モデルアクセス — はあなたのローカルマシン上に存在します。何かが失敗する場合、それはほぼ常にローカルの問題です。
|
||||
</Callout>
|
||||
|
||||
## 始める前に
|
||||
|
||||
以下の**すべての**ツールに 2 つの前提条件が適用されます。
|
||||
|
||||
1. **Multica デーモンが実行中である必要があります。** [Multica CLI](/cli) をインストールした後に `multica daemon start` を実行するか、デーモンを自動的に起動する [Multica デスクトップアプリ](/desktop-app)を使用してください。デーモンが実行されていなければ、ツールを検出する主体がありません。
|
||||
2. **ツールのバイナリが `PATH` で到達可能である必要があります。** デーモンは各ツールを名前で呼び出して実行します(各セクションの**デーモンが探す名前**の列を参照)。ターミナルで `which <name>` で見つからなければ、デーモンも見つけられません。インストール後は、新しいターミナルを開く(またはデーモンを再起動する)ことで、新しい `PATH` エントリが反映されるようにしてください。
|
||||
|
||||
ツールをインストールした後は、デーモンを再起動してください。
|
||||
|
||||
```bash
|
||||
multica daemon restart
|
||||
```
|
||||
|
||||
または、デスクトップアプリではアプリを再起動するだけで構いません。デーモンは起動するたびに `PATH` を再スキャンします。
|
||||
|
||||
## サポートされている 12 種のツール
|
||||
|
||||
おおよそ利用者の多い順に並べています。すでに認証情報を持っているものを選んで使ってください — 12 種すべてをインストールする必要はありません。
|
||||
|
||||
### Claude Code (Anthropic)
|
||||
|
||||
最も完全な連携です。セッション再開が動作し、MCP が動作し、**11 種のうちエージェントの `mcp_config` フィールドを実際に読み込む唯一のツール**です(詳しくは[マトリクス](/providers#mcp-configuration-only-claude-code-actually-reads-it)を参照)。
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| デーモンが探す名前 | `claude` |
|
||||
| インストール | [claude.com/claude-code](https://www.claude.com/claude-code) の公式ガイドに従ってください。標準的な方法は npm パッケージ `@anthropic-ai/claude-code` です(Node.js 18+ が必要)。 |
|
||||
| 認証 | `claude` を一度実行して CLI 内のログイン手順に従うか、`ANTHROPIC_API_KEY` を設定してください。 |
|
||||
| 備考 | 新しいユーザーに最初に推奨する選択肢です。 |
|
||||
|
||||
### Codex (OpenAI)
|
||||
|
||||
よりきめ細かい承認ゲートを備えた JSON-RPC 2.0 のトランスポートです。**セッション再開のコードは存在しますが、現在は到達できません** — 再開が必要な場合は Claude Code か ACP 系列のいずれかを選んでください。
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| デーモンが探す名前 | `codex` |
|
||||
| インストール | [github.com/openai/codex](https://github.com/openai/codex) の公式ガイドに従ってください。標準的な方法は npm パッケージ `@openai/codex` です。 |
|
||||
| 認証 | `codex login`(ブラウザベース)または `OPENAI_API_KEY`。 |
|
||||
|
||||
### Cursor (Anysphere)
|
||||
|
||||
Cursor エディタに対応する CLI です。**セッション再開は動作しません** — Cursor の CLI がセッション id を返さないため、再開時に渡す値は常に無効です。
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| デーモンが探す名前 | `cursor-agent` |
|
||||
| インストール | [Cursor エディタ](https://cursor.com/)をインストールしてから、[docs.cursor.com](https://docs.cursor.com/) のドキュメントに従って CLI をインストールしてください。バイナリ名は `cursor` ではなく `cursor-agent` です。 |
|
||||
| 認証 | Cursor エディタを通じてログインすると、CLI がそのセッションを再利用します。 |
|
||||
|
||||
### GitHub Copilot
|
||||
|
||||
モデルのルーティングはあなたの GitHub アカウントのエンタイトルメント(entitlement)を通じて行われます — ツールが自分でモデルを選ぶのではなく、どのモデルを受け取るかは GitHub が決めます。
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| デーモンが探す名前 | `copilot` |
|
||||
| インストール | GitHub の CLI ドキュメント [github.com/github/copilot-cli](https://github.com/github/copilot-cli) を参照してください。 |
|
||||
| 認証 | CLI を通じたブラウザベースの GitHub ログイン。 |
|
||||
| 備考 | ログインしているアカウントに有効な GitHub Copilot サブスクリプションが必要です。 |
|
||||
|
||||
### Gemini (Google)
|
||||
|
||||
Gemini 2.5 および 3 シリーズをサポートします。セッション再開と MCP はありません — 単発のタスクに適しています。
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| デーモンが探す名前 | `gemini` |
|
||||
| インストール | [github.com/google-gemini/gemini-cli](https://github.com/google-gemini/gemini-cli) の公式ガイドに従ってください。標準的な方法は npm パッケージ `@google/gemini-cli` です。 |
|
||||
| 認証 | `gemini` を実行すると Google アカウントのログインを求められるか、`GEMINI_API_KEY` を設定してください。 |
|
||||
|
||||
### OpenCode (SST)
|
||||
|
||||
オープンソースの CLI エージェントです。独自の設定ファイルから利用可能なモデルを動的に発見します — 自分のモデルカタログを持ち込みたいユーザーによく合います。
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| デーモンが探す名前 | `opencode` |
|
||||
| インストール | [opencode.ai](https://opencode.ai/) の公式ガイド、または GitHub リポジトリ [github.com/sst/opencode](https://github.com/sst/opencode) に従ってください。一般的な方法はインストールスクリプトまたは npm パッケージです。 |
|
||||
| 認証 | OpenCode のドキュメントに従ってモデルプロバイダー(Anthropic、OpenAI など)を構成してください。 |
|
||||
|
||||
### Kiro CLI (Amazon)
|
||||
|
||||
ACP-over-stdio のトランスポートです。セッション再開は ACP `session/load` を通じて動作し、スキルは `.kiro/skills/` にコピーされます。
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| デーモンが探す名前 | `kiro-cli` |
|
||||
| インストール | [kiro.dev](https://kiro.dev/) の Kiro ドキュメントを参照してください。バイナリ名は `kiro` ではなく `kiro-cli` です。 |
|
||||
| 認証 | AWS アカウントベースで、Kiro 独自のオンボーディングに従ってください。 |
|
||||
|
||||
### Kimi (Moonshot)
|
||||
|
||||
ACP プロトコルのエージェントで、主に中国市場を対象としています。スキルは `.kimi/skills/` 配下に置かれます(ネイティブ発見)。
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| デーモンが探す名前 | `kimi` |
|
||||
| インストール | [github.com/MoonshotAI/kimi-cli](https://github.com/MoonshotAI/kimi-cli) の公式ガイドに従ってください。 |
|
||||
| 認証 | Moonshot API キーで、ベンダーのドキュメントに従って構成します。 |
|
||||
|
||||
### Hermes (Nous Research)
|
||||
|
||||
ACP プロトコルのエージェントです(Kimi とトランスポートを共有)。セッション再開が動作します。スキル注入のパスは汎用の `.agent_context/skills/` にフォールバックします — 依存する前に、スキルが正しくロードされているか確認してください。
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| デーモンが探す名前 | `hermes` |
|
||||
| インストール | 最新の CLI ディストリビューションは Nous Research のリポジトリ [github.com/NousResearch](https://github.com/NousResearch) を参照してください。 |
|
||||
| 認証 | ベンダーのドキュメントに従います。 |
|
||||
|
||||
### OpenClaw
|
||||
|
||||
オープンソースの CLI エージェントオーケストレーターです。**モデルはエージェント層にバインドされます**(`openclaw agents add --model`) — タスクごとに上書きすることはできず、Multica から `--model` や `--system-prompt` を渡すこともできません。
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| デーモンが探す名前 | `openclaw` |
|
||||
| インストール | プロジェクト [github.com/openclaw-org/openclaw](https://github.com/openclaw-org/openclaw) を参照してください(コミュニティによる保守)。 |
|
||||
| 認証 | OpenClaw のドキュメントに従って、基盤となるモデルプロバイダーを構成してください。 |
|
||||
|
||||
### Pi (Inflection AI)
|
||||
|
||||
ミニマルです。**セッション再開の方式が特殊です** — 再開 id が文字列 id ではなく、ディスク上のセッションファイルへのパスです。
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| デーモンが探す名前 | `pi` |
|
||||
| インストール | Inflection の CLI ドキュメント [pi.ai](https://pi.ai/) を参照してください。 |
|
||||
| 認証 | ベンダーのドキュメントに従います。 |
|
||||
|
||||
### Antigravity (Google)
|
||||
|
||||
Google の Antigravity CLI(`agy`)です。Google の Antigravity サービスと組になり、Gemini ベースのモデルを実行します。セッション再開は `--conversation <id>` を通じて動作し、デーモンが CLI のログファイルからこれをキャプチャします。モデル選択は Antigravity CLI 自体の内部で管理されます — Multica はこのプロバイダーに対してエージェントごとのモデルピッカーを無効にします。スキルは `.agents/skills/` に書き込まれます(CLI が Gemini CLI のワークスペーススキルレイアウトを継承します — [Antigravity ドキュメント](https://antigravity.google/docs/gcli-migration)を参照)。
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| デーモンが探す名前 | `agy` |
|
||||
| インストール | [antigravity.google/docs/cli-overview](https://antigravity.google/docs/cli-overview) の公式ガイドに従ってください。CLI はあらかじめビルドされて提供されます — `agy install` を一度実行して PATH とシェルエイリアスを設定してください。 |
|
||||
| 認証 | `agy` を対話的に一度実行して Google アカウントのログインを完了するか、Antigravity デスクトップアプリを通じてログインしてください — CLI は GUI が書き込んだ keyring エントリを再利用します。 |
|
||||
| 備考 | CLI は構造化されたイベントストリームではなく、stdout に通常のアシスタントテキストを出力します。途中の「I will run X」の行と最終的な応答の両方がテキストとして Multica に中継されます。 |
|
||||
|
||||
## インストールした後
|
||||
|
||||
1. **バイナリが `PATH` にあるか確認してください。** 新しいターミナルを開いて `which <name>`(例: `which claude`、`which cursor-agent`、`which kiro-cli`、`which agy`)を実行してください。パスが出力されれば、デーモンが見つけられます。何も出力されない場合は、まずシェルの `PATH` を修正してください(典型的な原因は、リロードされていないシェルごとの rc ファイルです)。
|
||||
2. **デーモンを再起動してください。** `multica daemon restart` を実行するか、デスクトップアプリを再起動してください。デーモンは起動時にのみ `PATH` をスキャンします。
|
||||
3. **ランタイムページを確認してください。** Multica UI の**ランタイム**ページに、`(ワークスペース × ツール)` の組み合わせごとに 1 行ずつ表示されるはずです。行に「offline」と表示される場合は、[デーモンとランタイム → ランタイムがオフラインと表示されるとき](/daemon-runtimes#when-a-runtime-is-marked-offline)を参照してください。
|
||||
4. **オンボーディングに戻ってください。** 「ランタイムを接続」ステップはポーリングを行い、数秒以内に新しいランタイムを認識します — リロードは不要です。
|
||||
|
||||
## トラブルシューティング
|
||||
|
||||
- **`which` はバイナリを見つけるのにデーモンは見つけません。** デーモンが古い `PATH` で起動されています。再起動してください。
|
||||
- **バイナリは存在するのに起動に失敗します。** ターミナルからツール自体の `--version` や `--help` を一度実行してください — ここで発生する失敗のほとんどは、認証の欠落、期限切れのトークン、または Node.js / ランタイムの不一致です。
|
||||
- **ランタイムページに行は表示されるのに、タスクがすぐに失敗します。** タスクをトリガーしながら `multica daemon logs -f` を確認してください。デーモンはツール自体のエラー出力をそのまま表示します。
|
||||
|
||||
より広範な症状については、[トラブルシューティングガイド](/troubleshooting)を参照してください。
|
||||
|
||||
## 次に
|
||||
|
||||
- [デーモンとランタイム](/daemon-runtimes) — 検出、ハートビート、オフライン処理の仕組み
|
||||
- [AI コーディングツールマトリクス](/providers) — ツールが接続された後の機能差
|
||||
- [エージェントの作成と構成](/agents-create) — エージェントに使うツールを選び、タスクの実行を開始する
|
||||
@@ -1,180 +0,0 @@
|
||||
---
|
||||
title: 에이전트 런타임 설치하기
|
||||
description: Multica는 사용자 기기에 설치된 AI 코딩 도구를 구동합니다. 이 페이지에서는 데몬이 도구를 감지할 수 있도록 지원되는 12종의 도구를 각각 설치하는 방법을 설명합니다.
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
|
||||
Multica에서 **런타임**이란 사용자 기기의 데몬과, 데몬이 `PATH`에서 찾아낸 AI 코딩 도구 하나가 짝을 이룬 것입니다. 온보딩의 "런타임 연결" 단계에서 **지원되는 도구를 감지하지 못했습니다**라고 표시된다면, 데몬이 `PATH`를 스캔했지만 구동 방법을 아는 12종의 도구 중 어느 것도 찾지 못했다는 뜻입니다. 아래 도구 중 하나(또는 여러 개)를 설치한 다음 해당 단계로 돌아와 다시 스캔하세요. 몇 초 안에 런타임이 나타납니다.
|
||||
|
||||
이 페이지는 다음 문서의 설치 측면 동반 문서입니다.
|
||||
|
||||
- [데몬과 런타임](/daemon-runtimes) — 감지가 작동하는 방식
|
||||
- [AI 코딩 도구 매트릭스](/providers) — 각 도구가 할 수 있는 것과 할 수 없는 것(세션 재개, MCP, 모델 선택)
|
||||
|
||||
<Callout type="info">
|
||||
Multica 서버는 사용자의 API 키나 도구 자체를 결코 보지 못합니다. 아래의 모든 것 — 설치, 인증, 모델 접근 — 은 사용자의 로컬 기기에 존재합니다. 무언가 실패한다면 거의 항상 로컬 문제입니다.
|
||||
</Callout>
|
||||
|
||||
## 시작하기 전에
|
||||
|
||||
아래 **모든** 도구에 두 가지 사전 조건이 적용됩니다.
|
||||
|
||||
1. **Multica 데몬이 실행 중이어야 합니다.** [Multica CLI](/cli)를 설치한 후 `multica daemon start`를 실행하거나, 데몬을 자동으로 시작하는 [Multica 데스크톱 앱](/desktop-app)을 사용하세요. 데몬이 실행 중이지 않으면 도구를 감지할 주체가 없습니다.
|
||||
2. **도구의 바이너리가 `PATH`에서 접근 가능해야 합니다.** 데몬은 각 도구를 이름으로 호출하여 실행합니다(각 섹션의 **데몬이 찾는 이름** 열 참고). 터미널에서 `which <name>`으로 찾을 수 없다면 데몬도 찾지 못합니다. 설치한 후에는 새 터미널을 열거나(또는 데몬을 재시작하여) 새 `PATH` 항목이 반영되도록 하세요.
|
||||
|
||||
도구를 설치한 후에는 데몬을 재시작하세요.
|
||||
|
||||
```bash
|
||||
multica daemon restart
|
||||
```
|
||||
|
||||
또는 데스크톱 앱에서는 앱을 다시 실행하기만 하면 됩니다. 데몬은 시작될 때마다 `PATH`를 다시 스캔합니다.
|
||||
|
||||
## 지원되는 12종의 도구
|
||||
|
||||
대략 많이 쓰이는 순서대로 나열했습니다. 이미 자격 증명을 갖고 있는 것을 골라 사용하세요. 12종을 모두 설치할 필요는 없습니다.
|
||||
|
||||
### Claude Code (Anthropic)
|
||||
|
||||
가장 완전한 연동입니다. 세션 재개가 작동하고, MCP가 작동하며, 에이전트의 `mcp_config` 필드를 소비합니다(자세한 내용은 [매트릭스](/providers) 참고).
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| 데몬이 찾는 이름 | `claude` |
|
||||
| 설치 | [claude.com/claude-code](https://www.claude.com/claude-code)의 공식 가이드를 따르세요. 일반적인 방법은 npm 패키지 `@anthropic-ai/claude-code`입니다(Node.js 18+ 필요). |
|
||||
| 인증 | `claude`를 한 번 실행하고 CLI 내 로그인 절차를 따르거나, `ANTHROPIC_API_KEY`를 설정하세요. |
|
||||
| 비고 | 새 사용자에게 가장 먼저 권장하는 선택지입니다. |
|
||||
|
||||
### Codex (OpenAI)
|
||||
|
||||
더 세분화된 승인 게이트를 갖춘 JSON-RPC 2.0 전송 방식입니다. MCP 구성은 작업별 `$CODEX_HOME/config.toml`에 기록됩니다. **세션 재개 코드는 존재하지만 현재 도달할 수 없습니다** — 재개가 필요하다면 Claude Code 또는 ACP 계열 중 하나를 선택하세요.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| 데몬이 찾는 이름 | `codex` |
|
||||
| 설치 | [github.com/openai/codex](https://github.com/openai/codex)의 공식 가이드를 따르세요. 일반적인 방법은 npm 패키지 `@openai/codex`입니다. |
|
||||
| 인증 | `codex login`(브라우저 기반) 또는 `OPENAI_API_KEY`. |
|
||||
|
||||
### Cursor (Anysphere)
|
||||
|
||||
Cursor 에디터에 대응하는 CLI입니다. **세션 재개가 작동하지 않습니다** — Cursor의 CLI가 세션 id를 반환하지 않으므로 재개 시 전달하는 값은 항상 유효하지 않습니다.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| 데몬이 찾는 이름 | `cursor-agent` |
|
||||
| 설치 | [Cursor 에디터](https://cursor.com/)를 설치한 다음 [docs.cursor.com](https://docs.cursor.com/)의 문서에 따라 CLI를 설치하세요. 바이너리 이름은 `cursor`가 아니라 `cursor-agent`입니다. |
|
||||
| 인증 | Cursor 에디터를 통해 로그인하면 CLI가 해당 세션을 재사용합니다. |
|
||||
|
||||
### GitHub Copilot
|
||||
|
||||
모델 라우팅은 사용자의 GitHub 계정 권한(entitlement)을 통해 이루어집니다 — 도구가 직접 모델을 고르지 않고, 어떤 모델을 받을지는 GitHub가 결정합니다.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| 데몬이 찾는 이름 | `copilot` |
|
||||
| 설치 | GitHub의 CLI 문서 [github.com/github/copilot-cli](https://github.com/github/copilot-cli)를 참고하세요. |
|
||||
| 인증 | CLI를 통한 브라우저 기반 GitHub 로그인. |
|
||||
| 비고 | 로그인한 계정에 활성화된 GitHub Copilot 구독이 필요합니다. |
|
||||
|
||||
### Gemini (Google)
|
||||
|
||||
Gemini 2.5 및 3 시리즈를 지원합니다. 세션 재개와 MCP는 없습니다 — 단발성 작업에 적합합니다.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| 데몬이 찾는 이름 | `gemini` |
|
||||
| 설치 | [github.com/google-gemini/gemini-cli](https://github.com/google-gemini/gemini-cli)의 공식 가이드를 따르세요. 일반적인 방법은 npm 패키지 `@google/gemini-cli`입니다. |
|
||||
| 인증 | `gemini`를 실행하면 Google 계정 로그인을 요청하거나, `GEMINI_API_KEY`를 설정하세요. |
|
||||
|
||||
### OpenCode (SST)
|
||||
|
||||
오픈 소스 CLI 에이전트입니다. 자체 설정 파일에서 사용 가능한 모델을 동적으로 발견합니다 — 자신만의 모델 카탈로그를 직접 가져오려는 사용자에게 잘 맞습니다. `OPENCODE_CONFIG_CONTENT`를 통해 에이전트의 `mcp_config` 필드도 소비합니다.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| 데몬이 찾는 이름 | `opencode` |
|
||||
| 설치 | [opencode.ai](https://opencode.ai/)의 공식 가이드 또는 GitHub 저장소 [github.com/sst/opencode](https://github.com/sst/opencode)를 따르세요. 일반적인 방법은 설치 스크립트 또는 npm 패키지입니다. |
|
||||
| 인증 | OpenCode의 문서에 따라 모델 제공자(Anthropic, OpenAI 등)를 구성하세요. |
|
||||
|
||||
### Kiro CLI (Amazon)
|
||||
|
||||
ACP-over-stdio 전송 방식입니다. 세션 재개는 ACP `session/load`를 통해 작동하며, MCP 구성은 ACP `mcpServers`로 전달되고, 스킬은 `.kiro/skills/`로 복사됩니다.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| 데몬이 찾는 이름 | `kiro-cli` |
|
||||
| 설치 | [kiro.dev](https://kiro.dev/)의 Kiro 문서를 참고하세요. 바이너리 이름은 `kiro`가 아니라 `kiro-cli`입니다. |
|
||||
| 인증 | AWS 계정 기반이며, Kiro 자체 온보딩을 따르세요. |
|
||||
|
||||
### Kimi (Moonshot)
|
||||
|
||||
ACP 프로토콜 에이전트로, 주로 중국 시장을 겨냥합니다. MCP 구성은 ACP `mcpServers`로 전달되며, 스킬은 `.kimi/skills/` 아래에 위치합니다(네이티브 발견).
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| 데몬이 찾는 이름 | `kimi` |
|
||||
| 설치 | [github.com/MoonshotAI/kimi-cli](https://github.com/MoonshotAI/kimi-cli)의 공식 가이드를 따르세요. |
|
||||
| 인증 | Moonshot API 키이며, 공급사 문서에 따라 구성합니다. |
|
||||
|
||||
### Hermes (Nous Research)
|
||||
|
||||
ACP 프로토콜 에이전트입니다(Kimi와 전송 방식을 공유). 세션 재개가 작동하고, MCP 구성은 ACP `mcpServers`로 전달됩니다. 스킬 주입 경로는 일반적인 `.agent_context/skills/`로 폴백됩니다 — 의존하기 전에 스킬이 제대로 로드되는지 확인하세요.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| 데몬이 찾는 이름 | `hermes` |
|
||||
| 설치 | 최신 CLI 배포본은 Nous Research의 저장소 [github.com/NousResearch](https://github.com/NousResearch)를 참고하세요. |
|
||||
| 인증 | 공급사 문서에 따릅니다. |
|
||||
|
||||
### OpenClaw
|
||||
|
||||
오픈 소스 CLI 에이전트 오케스트레이터입니다. MCP 구성은 Multica의 작업별 config wrapper를 통해 기록됩니다. **모델은 에이전트 계층에 바인딩됩니다**(`openclaw agents add --model`) — 작업별로 재정의할 수 없으며, Multica에서 `--model`이나 `--system-prompt`를 전달할 수 없습니다.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| 데몬이 찾는 이름 | `openclaw` |
|
||||
| 설치 | 프로젝트 [github.com/openclaw-org/openclaw](https://github.com/openclaw-org/openclaw)를 참고하세요(커뮤니티 유지 관리). |
|
||||
| 인증 | OpenClaw의 문서에 따라 기반 모델 제공자를 구성하세요. |
|
||||
|
||||
### Pi (Inflection AI)
|
||||
|
||||
미니멀합니다. **세션 재개 방식이 특이합니다** — 재개 id가 문자열 id가 아니라 디스크에 있는 세션 파일의 경로입니다.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| 데몬이 찾는 이름 | `pi` |
|
||||
| 설치 | Inflection의 CLI 문서 [pi.ai](https://pi.ai/)를 참고하세요. |
|
||||
| 인증 | 공급사 문서에 따릅니다. |
|
||||
|
||||
### Antigravity (Google)
|
||||
|
||||
Google의 Antigravity CLI(`agy`)입니다. Google의 Antigravity 서비스와 짝을 이루며 Gemini 기반 모델을 실행합니다. 세션 재개는 `--conversation <id>`를 통해 작동하며, 데몬이 CLI 로그 파일에서 이를 캡처합니다. 모델 선택은 Antigravity CLI 자체 내부에서 관리됩니다 — Multica는 이 제공자에 대해 에이전트별 모델 선택기를 비활성화합니다. 스킬은 `.agents/skills/`에 기록됩니다(CLI가 Gemini CLI의 워크스페이스 스킬 레이아웃을 상속함 — [Antigravity 문서](https://antigravity.google/docs/gcli-migration) 참고).
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| 데몬이 찾는 이름 | `agy` |
|
||||
| 설치 | [antigravity.google/docs/cli-overview](https://antigravity.google/docs/cli-overview)의 공식 가이드를 따르세요. CLI는 미리 빌드되어 제공됩니다 — `agy install`을 한 번 실행하여 PATH와 셸 별칭을 설정하세요. |
|
||||
| 인증 | `agy`를 대화형으로 한 번 실행하여 Google 계정 로그인을 완료하거나, Antigravity 데스크톱 앱을 통해 로그인하세요 — CLI는 GUI가 기록한 keyring 항목을 재사용합니다. |
|
||||
| 비고 | CLI는 구조화된 이벤트 스트림이 아니라 stdout에 일반 어시스턴트 텍스트를 출력합니다. 중간의 "I will run X" 줄과 최종 응답 모두 텍스트로 Multica에 전달됩니다. |
|
||||
|
||||
## 설치한 후에
|
||||
|
||||
1. **바이너리가 `PATH`에 있는지 확인하세요.** 새 터미널을 열고 `which <name>`(예: `which claude`, `which cursor-agent`, `which kiro-cli`, `which agy`)을 실행하세요. 경로가 출력되면 데몬이 찾을 수 있습니다. 아무것도 출력되지 않으면 먼저 셸의 `PATH`를 수정하세요(전형적인 원인은 다시 로드되지 않은 셸별 rc 파일입니다).
|
||||
2. **데몬을 재시작하세요.** `multica daemon restart`를 실행하거나 데스크톱 앱을 다시 실행하세요. 데몬은 시작 시에만 `PATH`를 스캔합니다.
|
||||
3. **런타임 페이지를 확인하세요.** Multica UI의 **런타임** 페이지에 이제 `(워크스페이스 × 도구)` 조합별로 한 행씩 나열되어야 합니다. 행에 "offline"이라고 표시되면 [데몬과 런타임 → 런타임이 오프라인으로 표시될 때](/daemon-runtimes#when-a-runtime-is-marked-offline)를 참고하세요.
|
||||
4. **온보딩으로 돌아가세요.** "런타임 연결" 단계는 폴링을 수행하며 몇 초 안에 새 런타임을 인식합니다 — 새로 고칠 필요가 없습니다.
|
||||
|
||||
## 문제 해결
|
||||
|
||||
- **`which`는 바이너리를 찾는데 데몬은 찾지 못합니다.** 데몬이 예전 `PATH`로 시작되었습니다. 재시작하세요.
|
||||
- **바이너리는 존재하지만 실행에 실패합니다.** 터미널에서 도구 자체의 `--version`이나 `--help`를 한 번 실행하세요 — 여기서 발생하는 대부분의 실패는 인증 누락, 만료된 토큰, 또는 Node.js / 런타임 불일치입니다.
|
||||
- **런타임 페이지에 행은 표시되지만 작업이 즉시 실패합니다.** 작업을 트리거하면서 `multica daemon logs -f`를 확인하세요. 데몬은 도구 자체의 오류 출력을 그대로 보여줍니다.
|
||||
|
||||
더 광범위한 증상은 [문제 해결 가이드](/troubleshooting)를 참고하세요.
|
||||
|
||||
## 다음
|
||||
|
||||
- [데몬과 런타임](/daemon-runtimes) — 감지, 하트비트, 오프라인 처리가 작동하는 방식
|
||||
- [AI 코딩 도구 매트릭스](/providers) — 도구가 연결된 후의 기능 차이
|
||||
- [에이전트 생성 및 구성](/agents-create) — 에이전트에 사용할 도구를 선택하고 작업 실행 시작하기
|
||||
@@ -37,7 +37,7 @@ Listed roughly from most to least common. Pick whichever ones you already have c
|
||||
|
||||
### Claude Code (Anthropic)
|
||||
|
||||
The most complete integration. Session resumption works, MCP works, and it consumes the `mcp_config` field on agents (see the [matrix](/providers#mcp-configuration-provider-specific-support)).
|
||||
The most complete integration. Session resumption works, MCP works, and it's the **only one of the 11 that actually consumes the `mcp_config` field** on agents (see the [matrix](/providers#mcp-configuration-only-claude-code-actually-reads-it)).
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
@@ -48,7 +48,7 @@ The most complete integration. Session resumption works, MCP works, and it consu
|
||||
|
||||
### Codex (OpenAI)
|
||||
|
||||
JSON-RPC 2.0 transport with finer-grained approval gates. MCP config is written into the per-task `$CODEX_HOME/config.toml`. **Session resumption code exists but is currently unreachable** — pick Claude Code or one of the ACP family if you need resume.
|
||||
JSON-RPC 2.0 transport with finer-grained approval gates. **Session resumption code exists but is currently unreachable** — pick Claude Code or one of the ACP family if you need resume.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
@@ -89,7 +89,7 @@ Supports the Gemini 2.5 and 3 series. No session resumption, no MCP — suitable
|
||||
|
||||
### OpenCode (SST)
|
||||
|
||||
Open-source CLI agent. Dynamically discovers available models from its own configuration file — good fit for users who want to bring their own model catalog. Consumes the agent's `mcp_config` field through `OPENCODE_CONFIG_CONTENT`.
|
||||
Open-source CLI agent. Dynamically discovers available models from its own configuration file — good fit for users who want to bring their own model catalog.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
@@ -99,7 +99,7 @@ Open-source CLI agent. Dynamically discovers available models from its own confi
|
||||
|
||||
### Kiro CLI (Amazon)
|
||||
|
||||
ACP-over-stdio transport. Session resumption works through ACP `session/load`; MCP config is passed through ACP `mcpServers`; skills are copied into `.kiro/skills/`.
|
||||
ACP-over-stdio transport. Session resumption works through ACP `session/load`; skills are copied into `.kiro/skills/`.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
@@ -109,7 +109,7 @@ ACP-over-stdio transport. Session resumption works through ACP `session/load`; M
|
||||
|
||||
### Kimi (Moonshot)
|
||||
|
||||
ACP-protocol agent, primarily aimed at the Chinese market. MCP config is passed through ACP `mcpServers`; skills live under `.kimi/skills/` (native discovery).
|
||||
ACP-protocol agent, primarily aimed at the Chinese market. Skills live under `.kimi/skills/` (native discovery).
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
@@ -119,7 +119,7 @@ ACP-protocol agent, primarily aimed at the Chinese market. MCP config is passed
|
||||
|
||||
### Hermes (Nous Research)
|
||||
|
||||
ACP-protocol agent (shares the transport with Kimi). Session resumption works, and MCP config is passed through ACP `mcpServers`. The skill injection path falls back to the generic `.agent_context/skills/` — verify your skills are loading before relying on them.
|
||||
ACP-protocol agent (shares the transport with Kimi). Session resumption works. The skill injection path falls back to the generic `.agent_context/skills/` — verify your skills are loading before relying on them.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
@@ -129,7 +129,7 @@ ACP-protocol agent (shares the transport with Kimi). Session resumption works, a
|
||||
|
||||
### OpenClaw
|
||||
|
||||
Open-source CLI agent orchestrator. MCP config is materialized through Multica's per-task config wrapper. **Model is bound at the agent layer** (`openclaw agents add --model`) — it can't be overridden per task, and you can't pass `--model` or `--system-prompt` from Multica.
|
||||
Open-source CLI agent orchestrator. **Model is bound at the agent layer** (`openclaw agents add --model`) — it can't be overridden per task, and you can't pass `--model` or `--system-prompt` from Multica.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
|
||||
@@ -37,7 +37,7 @@ multica daemon restart
|
||||
|
||||
### Claude Code(Anthropic)
|
||||
|
||||
集成最完整的一款。会话续接好用,MCP 好用,会消费 agent 配置里的 `mcp_config` 字段(见[矩阵](/zh/providers))。
|
||||
集成最完整的一款。会话续接好用,MCP 好用,而且 **12 款里只有它真正会读 agent 配置里的 `mcp_config` 字段**(见[矩阵](/zh/providers))。
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
@@ -48,7 +48,7 @@ multica daemon restart
|
||||
|
||||
### Codex(OpenAI)
|
||||
|
||||
JSON-RPC 2.0 传输,审批粒度更细。MCP 配置会写入单次任务的 `$CODEX_HOME/config.toml`。**会话续接的代码在,但调不到** —— 要续接的话选 Claude Code 或 ACP 系列。
|
||||
JSON-RPC 2.0 传输,审批粒度更细。**会话续接的代码在,但调不到** —— 要续接的话选 Claude Code 或 ACP 系列。
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
@@ -89,7 +89,7 @@ Cursor 编辑器的 CLI 对应物。**会话续接是坏的** —— Cursor CLI
|
||||
|
||||
### OpenCode(SST)
|
||||
|
||||
开源 CLI agent。会从自己的配置文件里动态发现可用模型 —— 适合想自己掌控模型清单的用户。会通过 `OPENCODE_CONFIG_CONTENT` 消费 agent 配置里的 `mcp_config` 字段。
|
||||
开源 CLI agent。会从自己的配置文件里动态发现可用模型 —— 适合想自己掌控模型清单的用户。
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
@@ -99,7 +99,7 @@ Cursor 编辑器的 CLI 对应物。**会话续接是坏的** —— Cursor CLI
|
||||
|
||||
### Kiro CLI(Amazon)
|
||||
|
||||
ACP-over-stdio 传输。会话续接通过 ACP `session/load` 工作;MCP 配置通过 ACP `mcpServers` 传入;skills 拷到 `.kiro/skills/`。
|
||||
ACP-over-stdio 传输。会话续接通过 ACP `session/load` 工作;skills 拷到 `.kiro/skills/`。
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
@@ -109,7 +109,7 @@ ACP-over-stdio 传输。会话续接通过 ACP `session/load` 工作;MCP 配
|
||||
|
||||
### Kimi(Moonshot)
|
||||
|
||||
ACP 协议 agent,主要面向中国市场。MCP 配置通过 ACP `mcpServers` 传入;skills 放在 `.kimi/skills/`(原生发现路径)。
|
||||
ACP 协议 agent,主要面向中国市场。Skills 放在 `.kimi/skills/`(原生发现路径)。
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
@@ -119,7 +119,7 @@ ACP 协议 agent,主要面向中国市场。MCP 配置通过 ACP `mcpServers`
|
||||
|
||||
### Hermes(Nous Research)
|
||||
|
||||
ACP 协议 agent(和 Kimi 共享传输层)。会话续接可用,MCP 配置通过 ACP `mcpServers` 传入。Skill 注入用的是通用回退路径 `.agent_context/skills/` —— 用之前先验证 skills 真的被加载了。
|
||||
ACP 协议 agent(和 Kimi 共享传输层)。会话续接可用。Skill 注入用的是通用回退路径 `.agent_context/skills/` —— 用之前先验证 skills 真的被加载了。
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
@@ -129,7 +129,7 @@ ACP 协议 agent(和 Kimi 共享传输层)。会话续接可用,MCP 配置
|
||||
|
||||
### OpenClaw
|
||||
|
||||
开源 CLI agent 编排器。MCP 配置通过 Multica 的单次任务配置 wrapper 写入。**模型绑在 agent 层**(`openclaw agents add --model`)—— 不能按任务覆盖,从 Multica 也传不了 `--model` / `--system-prompt`。
|
||||
开源 CLI agent 编排器。**模型绑在 agent 层**(`openclaw agents add --model`)—— 不能按任务覆盖,从 Multica 也传不了 `--model` / `--system-prompt`。
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
---
|
||||
title: イシューとプロジェクト
|
||||
description: 人またはエージェントに割り当てられる、Multica の中心的な作業単位。
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
|
||||
イシューは Multica における独立した作業単位です — バグ、新機能、対応が必要なことなら何でも構いません。すべてのイシューには **タイトル**、**説明**(Markdown 対応)、**ステータス**、**優先度**、**担当者** があり、任意で **プロジェクト** に属することもできます。Linear や Jira を使ったことがあれば、同じ形だと分かるはずです。
|
||||
|
||||
**Multica の最大の特徴は、イシューの担当者が人でもエージェントでもよいという点です** — [エージェント](/agents) — ここから始めましょう。
|
||||
|
||||
## エージェントにイシューを割り当てる
|
||||
|
||||
イシューをエージェントに[割り当てる](/assigning-issues)と、その作業をエージェントに引き渡すことになります。エージェントは **自動的に開始します** — 数秒以内に実行を始め、コメントで進捗を報告し、完了するとステータスを done に切り替えます。同僚に仕事を渡すのとの唯一の違いは、エージェントはオフラインにならず、リマインドも要らず、24時間365日いつでも対応できることです。
|
||||
|
||||
<Callout type="info">
|
||||
エージェントのアイデンティティ、設定、実行場所については [エージェント](/agents) を参照してください。
|
||||
</Callout>
|
||||
|
||||
非公開エージェントをイシューに割り当てられるのは、ワークスペースの owner と admin だけです。ロールの権限については [メンバーとロール](/members-roles) を参照してください。
|
||||
|
||||
## ステータス
|
||||
|
||||
Multica には7つのステータスがあります。**どのステータスからでも、ほかのどのステータスへも直接移動できます** — Multica はワークフローを強制せず、`backlog` から `done` へ一気に飛んでも止めません。
|
||||
|
||||
| ステータス | 意味 |
|
||||
|---|---|
|
||||
| `backlog` | まだ予定に入っていない |
|
||||
| `todo` | 予定が決まり、着手できる |
|
||||
| `in_progress` | 作業中 |
|
||||
| `in_review` | レビュー待ち |
|
||||
| `done` | 完了 |
|
||||
| `blocked` | 外部要因で止まっている |
|
||||
| `cancelled` | キャンセル済み |
|
||||
|
||||
イシューがエージェントに割り当てられると、エージェントは自動的にステータスを `backlog` / `todo` から `in_progress` に移し、完了すると `done` にします。いつでも手動で変更することもできます。
|
||||
|
||||
## 優先度
|
||||
|
||||
優先度には5段階があり、デフォルトのイシュー一覧の並び替えに使われます:
|
||||
|
||||
| 優先度 | 用途 |
|
||||
|---|---|
|
||||
| `No priority` | まだ決めていない(デフォルト) |
|
||||
| `Urgent` | 緊急 |
|
||||
| `High` | 高 |
|
||||
| `Medium` | 中 |
|
||||
| `Low` | 低 |
|
||||
|
||||
## イシュー番号
|
||||
|
||||
すべてのイシューには、ワークスペース内で一意の番号が `<prefix>-<digits>` 形式で付きます — 例えば `MUL-123` のように。番号は作成時にシステムが付与し、**決して変わりません**。[ワークスペース → イシュー番号](/workspaces#issue-numbers) を参照してください。
|
||||
|
||||
## コメント
|
||||
|
||||
イシューの下のコメントスレッドは、協業が行われる場所です — コメントに返信し、人やエージェントを `@` でメンションし、リアクションを追加できます。
|
||||
|
||||
コメントでエージェントを `@` でメンションすると **自動的にトリガーされます** — これは「割り当て」と並ぶ、エージェントを起動する2つ目の方法です。[コメントとメンション](/comments) と [コメントでエージェントをメンションする](/mentioning-agents) を参照してください。
|
||||
|
||||
## イシューを削除する
|
||||
|
||||
<Callout type="warning">
|
||||
イシューを削除すると、その下のすべてのコメント、リアクション、添付ファイルと、キューに入っているエージェントのタスクが **即座に** 消えます(実行中のタスクはキャンセルされます)。**元に戻せません。**
|
||||
|
||||
単にイシューを見えないようにしたいだけなら、**ステータスを `cancelled` に変更するほうが削除より安全です** — データは残り、後から戻すことができます。
|
||||
</Callout>
|
||||
|
||||
## プロジェクト
|
||||
|
||||
プロジェクトは、複数のイシューをまとめるコンテナです。イシューは最大1つのプロジェクトに属するか、どのプロジェクトにも属さないかのいずれかです。
|
||||
|
||||
プロジェクトには独自の **リード** がいます — **イシューの担当者と同じように、リードも人でもエージェントでもかまいません**。
|
||||
|
||||
プロジェクトを削除しても **その中のイシューは削除されません**: それらのイシューはプロジェクトから切り離されるだけで、ワークスペースにそのまま残ります。
|
||||
|
||||
## 次に読む
|
||||
|
||||
- [コメントとメンション](/comments) — イシューの下で協業する
|
||||
- [エージェント](/agents) — 「エージェントに割り当てる」が実際にどう動くのかを理解する
|
||||
@@ -1,79 +0,0 @@
|
||||
---
|
||||
title: 이슈와 프로젝트
|
||||
description: 사람 또는 에이전트에게 할당할 수 있는 Multica의 핵심 작업 단위.
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
|
||||
이슈는 Multica에서 독립적인 작업 단위입니다 — 버그, 새 기능, 처리해야 할 어떤 일이든 될 수 있습니다. 모든 이슈에는 **제목**, **설명**(Markdown 지원), **상태**, **우선순위**, **담당자**가 있으며, 선택적으로 **프로젝트**에 속할 수 있습니다. Linear나 Jira를 사용해 보셨다면 동일한 형태입니다.
|
||||
|
||||
**Multica의 가장 큰 특징은 이슈의 담당자가 사람일 수도, [에이전트](/agents)일 수도 있다는 점입니다** — 여기서부터 시작하겠습니다.
|
||||
|
||||
## 에이전트에게 이슈 할당하기
|
||||
|
||||
이슈를 에이전트에게 [할당](/assigning-issues)하면 그 작업을 에이전트에게 넘기는 것입니다. 에이전트는 **자동으로 시작합니다** — 몇 초 안에 실행을 시작하고, 댓글로 진행 상황을 보고하며, 완료되면 상태를 done으로 전환합니다. 동료에게 일을 넘기는 것과의 유일한 차이는 에이전트는 오프라인이 되지 않고, 알림이 필요 없으며, 연중무휴 24시간 사용할 수 있다는 점입니다.
|
||||
|
||||
<Callout type="info">
|
||||
에이전트의 정체성, 구성, 실행 위치에 대해서는 [에이전트](/agents)를 참고하세요.
|
||||
</Callout>
|
||||
|
||||
비공개 에이전트는 워크스페이스 owner와 admin만 이슈에 할당할 수 있습니다. 역할 권한에 대해서는 [멤버와 역할](/members-roles)을 참고하세요.
|
||||
|
||||
## 상태
|
||||
|
||||
Multica에는 일곱 가지 상태가 있습니다. **어떤 상태든 다른 어떤 상태로도 바로 이동할 수 있습니다** — Multica는 워크플로를 강제하지 않으며, `backlog`에서 곧바로 `done`으로 건너뛰어도 막지 않습니다.
|
||||
|
||||
| 상태 | 의미 |
|
||||
|---|---|
|
||||
| `backlog` | 아직 일정에 없음 |
|
||||
| `todo` | 일정이 잡혔고, 시작할 준비됨 |
|
||||
| `in_progress` | 작업 중 |
|
||||
| `in_review` | 리뷰 대기 중 |
|
||||
| `done` | 완료됨 |
|
||||
| `blocked` | 외부 요인으로 막힘 |
|
||||
| `cancelled` | 취소됨 |
|
||||
|
||||
이슈가 에이전트에게 할당되면, 에이전트는 자동으로 상태를 `backlog` / `todo`에서 `in_progress`로 옮기고, 완료 시 `done`으로 옮깁니다. 언제든지 직접 변경할 수도 있습니다.
|
||||
|
||||
## 우선순위
|
||||
|
||||
우선순위에는 다섯 단계가 있으며, 기본 이슈 목록의 정렬에 사용됩니다:
|
||||
|
||||
| 우선순위 | 용도 |
|
||||
|---|---|
|
||||
| `No priority` | 아직 결정되지 않음 (기본값) |
|
||||
| `Urgent` | 긴급 |
|
||||
| `High` | 높음 |
|
||||
| `Medium` | 중간 |
|
||||
| `Low` | 낮음 |
|
||||
|
||||
## 이슈 번호
|
||||
|
||||
모든 이슈에는 워크스페이스 내에서 고유한 번호가 `<prefix>-<digits>` 형식으로 부여됩니다 — 예를 들어 `MUL-123`처럼요. 번호는 생성 시점에 시스템이 부여하며 **절대 변하지 않습니다**. [워크스페이스 → 이슈 번호](/workspaces#issue-numbers)를 참고하세요.
|
||||
|
||||
## 댓글
|
||||
|
||||
이슈 아래의 댓글 스레드는 협업이 일어나는 곳입니다 — 댓글에 답글을 달고, 사람이나 에이전트를 `@`로 멘션하고, 반응을 추가할 수 있습니다.
|
||||
|
||||
댓글에서 에이전트를 `@`로 멘션하면 **자동으로 트리거됩니다** — 이것은 "할당"과 더불어 에이전트를 시작하는 두 번째 방법입니다. [댓글과 멘션](/comments)과 [댓글에서 에이전트 멘션하기](/mentioning-agents)를 참고하세요.
|
||||
|
||||
## 이슈 삭제하기
|
||||
|
||||
<Callout type="warning">
|
||||
이슈를 삭제하면 그 아래의 모든 댓글, 반응, 첨부 파일과 대기 중인 에이전트 작업이 **즉시** 삭제됩니다(실행 중인 작업은 취소됩니다). **되돌릴 수 없습니다.**
|
||||
|
||||
이슈를 단지 보이지 않게 하고 싶을 뿐이라면, **상태를 `cancelled`로 변경하는 것이 삭제보다 안전합니다** — 데이터가 남아 있어 나중에 다시 가져올 수 있습니다.
|
||||
</Callout>
|
||||
|
||||
## 프로젝트
|
||||
|
||||
프로젝트는 여러 이슈를 하나로 묶는 컨테이너입니다. 이슈는 최대 하나의 프로젝트에 속하거나, 아예 어떤 프로젝트에도 속하지 않을 수 있습니다.
|
||||
|
||||
프로젝트에는 자체 **리더**가 있습니다 — **이슈의 담당자와 마찬가지로, 리더도 사람일 수도, 에이전트일 수도 있습니다**.
|
||||
|
||||
프로젝트를 삭제해도 **그 안의 이슈는 삭제되지 않습니다**: 해당 이슈들은 단지 프로젝트에서 분리되어 워크스페이스에 그대로 남습니다.
|
||||
|
||||
## 다음 단계
|
||||
|
||||
- [댓글과 멘션](/comments) — 이슈 아래에서 협업하기
|
||||
- [에이전트](/agents) — "에이전트에게 할당"이 실제로 어떻게 작동하는지 이해하기
|
||||
@@ -1,95 +0,0 @@
|
||||
---
|
||||
title: Lark Bot 連携
|
||||
description: Multica エージェントを Lark(飞书)Bot に紐づければ、Lark の DM やグループからそのまま対話できます——@ でメンションして自然に話しかけたり、/issue と入力して Lark を離れずに Multica イシューを起票したりできます。
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
|
||||
任意の[エージェント](/agents)を Lark(飞书)Bot に紐づければ、チームは Lark の中から直接それを使えます——Bot に DM したり、グループで @ メンションしたり、`/issue` と入力してアプリを開かずに [Multica イシュー](/issues)を起票したりできます。エージェントの返信は、作業の進行に合わせて更新されるライブカードとしてチャットに戻ってきます。
|
||||
|
||||
各 Bot は 1 つの Multica エージェントと **1 対 1** で紐づきます。2 つ目のエージェントを紐づけると 2 つ目の Bot が作られます。1 つのエージェントが 2 つの Bot を持つことはありません。
|
||||
|
||||
## この連携でできること
|
||||
|
||||
| 場所 | 動作 |
|
||||
|---|---|
|
||||
| **エージェント → 連携** | エージェント詳細ページには **連携(Integrations)** タブがあります(左サイドバーにも対応する区画があります)。owner と admin はそこに **Lark に紐づける** が表示され、紐づけると **Lark に接続済み** バッジと **Lark で管理** リンクに切り替わります。 |
|
||||
| **Bot に DM** | ワークスペースメンバーが Lark の中で Bot に直接メッセージを送ります。各会話はそのエージェントとの Multica [chat](/chat) セッションになり、エージェントはスレッド内で返信します。 |
|
||||
| **グループで @ メンション** | Bot を Lark グループに追加して @ メンションします。読み取られるのはメンションしたメッセージだけで、Bot はグループ全体を聞いているわけではありません。 |
|
||||
| **`/issue` コマンド** | `/issue <タイトル>`(本文を続けてもよい)と入力すると、ワークスペースに新しい Multica イシューが作られ、あなたの名義になります。 |
|
||||
| **ライブ返信カード** | Bot はインタラクティブなカードを投稿し、エージェントの実行に合わせて更新し続けます——進捗、最終的な回答、あるいはエラーが反映されます。 |
|
||||
|
||||
## エージェントを紐づける(owner / admin)
|
||||
|
||||
紐づけはスキャンしてインストールするフローです——アプリのシークレットをコピーする必要も、開発者コンソールでの操作も不要です。
|
||||
|
||||
1. **Agents → あなたのエージェント** からそのエージェントを開きます。
|
||||
2. **連携(Integrations)** タブ(または左サイドバーの **連携** 区画)を開き、**Lark に紐づける** をクリックします。
|
||||
3. QR コードが表示されます。スマートフォンで **Lark → スキャン** を開き、新しい PersonalAgent Bot を認可します。
|
||||
4. スキャンが完了するとダイアログが閉じ、エージェントに **Lark に接続済み** と表示されます。あなた自身の Lark アイデンティティは自動であなたの Multica アカウントに紐づくので、すぐに Bot と対話を始められます。
|
||||
|
||||
<Callout type="info">
|
||||
QR は使い切りで、短い時間が過ぎると失効します。認可する前に失効してしまったら、**もう一度スキャン** をクリックして新しいコードを取得してください。
|
||||
</Callout>
|
||||
|
||||
エージェントが接続されると、**Lark に紐づける** ボタンは **Lark で管理** リンクに置き換わります。スコープの調整、名前の変更、追加の権限の申請が必要なときは、これを使って Lark 内の Bot のアプリページを開いてください——再スキャンは意図的に無効化されており、既存の Bot を取り残してしまわないようにしています。
|
||||
|
||||
## Bot を使う(メンバー)
|
||||
|
||||
### 最初のメッセージ:Lark アイデンティティを紐づける
|
||||
|
||||
初めて Bot にメッセージを送ると、Bot は **Lark アイデンティティを紐づける** よう促すカードで返信します。リンクをタップして Multica にサインインすると、あなたの Lark アカウントがあなたの Multica メンバーシップに紐づきます。これによって、エージェントがあなたとして振る舞えるようになります——たとえば `/issue` はあなたの名義でイシューを起票します。
|
||||
|
||||
<Callout type="warning">
|
||||
Bot を使えるのは **ワークスペースのメンバー** だけです。メンバーでない場合や、アイデンティティの紐づけをスキップした場合、Bot は返信しません——あなたのメッセージは破棄されます(内容は保存せず、監査のために記録されます)。
|
||||
</Callout>
|
||||
|
||||
### 対話と `/issue`
|
||||
|
||||
- **エージェントに何でも聞く** —— Bot に DM するか、グループで @ メンションします。会話は通常のエージェント chat セッションで、エージェントはカードの中で返信します。
|
||||
- **イシューを起票する** —— `/issue Fix the login redirect` と送れば、Multica は新しいイシューを作るのと同じやり方でそのイシューをワークスペースに作ります。タイトルの後ろに行を足せば、それが説明になります。
|
||||
- **作業を見守る** —— 返信カードはエージェントの実行に合わせて自身を更新するので、進捗と結果がその場で見えます。
|
||||
|
||||
エージェントが **オフライン**(ランタイムが接続されていない)または **アーカイブ済み** の場合、Bot はメッセージを黙って破棄するのではなく、短いステータス通知で返信します。
|
||||
|
||||
## 管理と切断
|
||||
|
||||
ワークスペース全体の管理は **設定 → 連携** にあります。
|
||||
|
||||
- **接続済みの Bot** は、ワークスペース内のすべての Bot と、それぞれが紐づくエージェントを一覧表示します。この一覧はすべてのメンバーから見えます。
|
||||
- **切断** は **owner / admin 専用** です。切断すると Bot は Lark メッセージの受信を停止し、その接続が破棄されます。インストール記録は監査のために保持され、あとで同じエージェントを再び紐づけられます。
|
||||
|
||||
## 権限
|
||||
|
||||
- **紐づけ / 切断** にはワークスペースの **owner** または **admin** が必要です。member には接続済み Bot 一覧は見えますが、紐づけや切断の操作は見えません。
|
||||
- **Bot との対話** には、Lark アイデンティティを紐づけたワークスペースメンバーであることが必要です。それ以外の人のメッセージは一律に破棄されます。
|
||||
- この連携は破棄されたメッセージの本文を保存することはありません——監査のために破棄理由だけを記録します。
|
||||
|
||||
## セルフホストのセットアップ
|
||||
|
||||
Multica Cloud では連携はすでに利用可能です——このセクションは飛ばしてください。
|
||||
|
||||
セルフホストの場合、**保存時の暗号化キーを設定するまで Lark はオフ** です。このキーは、各 Bot の app secret がデータベースに触れる前にそれを暗号化します。
|
||||
|
||||
1. 32 バイトのキーを生成し、API サーバーに設定します。
|
||||
|
||||
```dotenv
|
||||
MULTICA_LARK_SECRET_KEY=<base64-encoded 32-byte key>
|
||||
```
|
||||
|
||||
2. API を再起動します。キーを設定するまで、**設定 → 連携** には「Lark integration not enabled」という通知が表示され、**Lark に紐づける** のエントリポイントは非表示のままになります。
|
||||
|
||||
<Callout type="info">
|
||||
**国際版テナント。** 連携はデフォルトで中国大陸のホスト(`open.feishu.cn`)を使います。組織が Lark の国際版テナントにある場合は、トランスポートをそちらに向けてください。
|
||||
|
||||
```dotenv
|
||||
MULTICA_LARK_HTTP_BASE_URL=https://open.larksuite.com
|
||||
```
|
||||
</Callout>
|
||||
|
||||
## 次に
|
||||
|
||||
- [エージェント](/agents) — 各 Bot はちょうど 1 つのエージェントに紐づきます
|
||||
- [Chat](/chat) — Bot の会話が Multica 内で対応するもの
|
||||
- [イシュー](/issues) — `/issue` が作るもの
|
||||
- [環境変数](/environment-variables) — セルフホスト構成の完全なリファレンス
|
||||
@@ -1,95 +0,0 @@
|
||||
---
|
||||
title: Lark Bot 연동
|
||||
description: Multica 에이전트를 Lark(飞书) 봇에 바인딩하면, Lark에서 직접 대화할 수 있습니다 — 개인 메시지나 그룹에서 @로 멘션하거나, 자연스럽게 대화하거나, /issue를 입력해 Lark를 벗어나지 않고 Multica 이슈를 생성하세요.
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
|
||||
아무 [에이전트](/agents)나 Lark(飞书) 봇에 바인딩하면, 팀이 Lark 안에서 바로 그 에이전트를 사용할 수 있습니다 — 봇에게 개인 메시지를 보내거나, 그룹에서 `@`로 멘션하거나, `/issue`를 입력해 앱을 열지 않고도 [Multica 이슈](/issues)를 생성하세요. 에이전트의 답변은 실시간 카드로 채팅에 돌아오며, 작업이 진행되는 동안 계속 업데이트됩니다.
|
||||
|
||||
각 봇은 하나의 Multica 에이전트와 **일대일**로 바인딩됩니다. 두 번째 에이전트를 바인딩하면 두 번째 봇이 생성되며, 하나의 에이전트가 두 개의 봇을 갖는 일은 없습니다.
|
||||
|
||||
## 연동이 하는 일
|
||||
|
||||
| 위치 | 동작 |
|
||||
|---|---|
|
||||
| **에이전트 → Integrations** | 에이전트 상세 페이지에 **Integrations** 탭이 있습니다(왼쪽 사이드바에도 대응하는 섹션이 있습니다). owner와 admin에게는 여기에 **Bind to Lark**가 보이며, 바인딩되면 **Connected to Lark** 배지와 **Manage in Lark** 링크로 바뀝니다. |
|
||||
| **봇에게 개인 메시지** | 워크스페이스 멤버가 Lark에서 봇에게 직접 메시지를 보냅니다. 각 대화는 그 에이전트와의 Multica [chat](/chat) 세션이 되며, 에이전트는 해당 스레드에서 답변합니다. |
|
||||
| **그룹에서 `@` 멘션** | 봇을 Lark 그룹에 추가하고 `@`로 멘션하세요. 멘션한 메시지만 읽으며, 봇이 그룹 전체를 듣지는 않습니다. |
|
||||
| **`/issue` 명령** | `/issue <제목>`(본문 추가 가능)을 입력하면 워크스페이스에 새 Multica 이슈가 생성되고, 당신 이름으로 귀속됩니다. |
|
||||
| **실시간 답변 카드** | 봇은 인터랙티브 카드를 게시하고 에이전트가 실행되는 동안 계속 갱신합니다 — 진행 상황, 최종 답변, 또는 오류. |
|
||||
|
||||
## 에이전트 바인딩하기 (owner / admin)
|
||||
|
||||
바인딩은 스캔하여 설치하는 방식입니다 — 복사할 앱 시크릿도, 개발자 콘솔 작업도 없습니다.
|
||||
|
||||
1. **Agents → _당신의 에이전트_**에서 에이전트를 엽니다.
|
||||
2. **Integrations** 탭으로 이동하거나(또는 왼쪽 사이드바의 **Integrations** 섹션 사용) **Bind to Lark**를 클릭합니다.
|
||||
3. QR 코드가 나타납니다. 휴대폰에서 **Lark → 스캔**을 열고, 새로 생긴 PersonalAgent 봇을 인증하세요.
|
||||
4. 스캔이 완료되면 대화상자가 닫히고 에이전트에 **Connected to Lark**가 표시됩니다. 당신의 Lark 신원이 자동으로 Multica 계정에 바인딩되므로, 곧바로 봇과 대화를 시작할 수 있습니다.
|
||||
|
||||
<Callout type="info">
|
||||
QR 코드는 일회용이며 짧은 시간 후에 만료됩니다. 인증하기 전에 만료되면 **Scan again**을 클릭해 새 코드를 받으세요.
|
||||
</Callout>
|
||||
|
||||
에이전트가 연결되면 **Bind to Lark** 버튼이 **Manage in Lark** 링크로 바뀝니다. 권한 범위를 조정하거나, 이름을 바꾸거나, 추가 권한을 요청해야 할 때 이 링크로 Lark에서 봇의 앱 페이지를 여세요 — 기존 봇이 고아가 되지 않도록 재스캔은 의도적으로 비활성화되어 있습니다.
|
||||
|
||||
## 봇 사용하기 (멤버)
|
||||
|
||||
### 첫 메시지: Lark 신원 바인딩하기
|
||||
|
||||
봇에게 처음 메시지를 보내면, **Lark 신원을 바인딩**하라는 카드로 답합니다. 링크를 탭하고 Multica에 로그인하면, 당신의 Lark 계정이 Multica 멤버십에 연결됩니다. 바로 이 단계가 에이전트로 하여금 당신을 대신해 행동하게 합니다 — 예를 들어 `/issue`는 이슈를 당신 이름으로 생성합니다.
|
||||
|
||||
<Callout type="warning">
|
||||
**워크스페이스 멤버**만 봇을 사용할 수 있습니다. 멤버가 아니거나 신원 바인딩을 건너뛰면 봇은 응답하지 않으며, 메시지는 폐기됩니다(감사 목적으로 기록되며, 내용은 저장하지 않습니다).
|
||||
</Callout>
|
||||
|
||||
### 대화와 `/issue`
|
||||
|
||||
- **무엇이든 에이전트에게 물어보기** — 봇에게 개인 메시지를 보내거나 그룹에서 `@`로 멘션하세요. 이 대화는 일반적인 에이전트 chat 세션이며, 에이전트는 카드에서 답변합니다.
|
||||
- **이슈 생성** — `/issue 로그인 리디렉션 수정`을 보내면 Multica가 워크스페이스에 그 이슈를 생성하며, 새 이슈가 으레 할당되는 방식 그대로 처리됩니다. 제목 뒤에 줄을 더 추가하면 설명이 됩니다.
|
||||
- **작업 지켜보기** — 답변 카드는 에이전트가 실행되는 동안 스스로 갱신되므로, 진행 상황과 결과를 그 자리에서 볼 수 있습니다.
|
||||
|
||||
에이전트가 **오프라인**(런타임이 연결되지 않음)이거나 **보관됨** 상태라면, 봇은 메시지를 조용히 폐기하는 대신 짧은 상태 안내로 답합니다.
|
||||
|
||||
## 관리 및 연결 해제
|
||||
|
||||
워크스페이스 전체 관리는 **설정 → Integrations**에 있습니다.
|
||||
|
||||
- **Connected bots**는 워크스페이스 내 모든 봇과 각 봇이 바인딩된 에이전트를 나열합니다. 이 목록은 모든 멤버에게 보입니다.
|
||||
- **Disconnect**는 **owner / admin 전용**입니다. 연결을 해제하면 봇이 Lark 메시지 수신을 멈추고 연결이 해체됩니다. 설치 기록은 감사용으로 유지되며, 이후 같은 에이전트를 다시 바인딩할 수 있습니다.
|
||||
|
||||
## 권한
|
||||
|
||||
- **바인딩 / 연결 해제**에는 워크스페이스 **owner** 또는 **admin**이 필요합니다. 멤버에게는 connected-bots 목록은 보이지만 바인딩이나 연결 해제 컨트롤은 보이지 않습니다.
|
||||
- **봇과 대화하기**에는 Lark 신원이 바인딩된 워크스페이스 멤버여야 합니다. 그 외의 사람은 모두 폐기됩니다.
|
||||
- 연동은 폐기된 메시지의 본문을 절대 저장하지 않으며 — 감사용 폐기 사유만 기록합니다.
|
||||
|
||||
## 자체 호스팅 설정
|
||||
|
||||
Multica Cloud에서는 연동이 이미 사용 가능합니다 — 이 섹션은 건너뛰세요.
|
||||
|
||||
자체 호스팅의 경우, **at-rest 암호화 키를 설정하기 전까지 Lark는 꺼져 있습니다**. 이 키는 각 봇의 앱 시크릿이 데이터베이스에 닿기 전에 암호화합니다.
|
||||
|
||||
1. 32바이트 키를 생성해 API 서버에 설정합니다.
|
||||
|
||||
```dotenv
|
||||
MULTICA_LARK_SECRET_KEY=<base64-encoded 32-byte key>
|
||||
```
|
||||
|
||||
2. API를 재시작하세요. 키가 설정되기 전까지 **설정 → Integrations**에는 "Lark integration not enabled" 안내가 표시되고, **Bind to Lark** 진입점은 숨겨진 채로 유지됩니다.
|
||||
|
||||
<Callout type="info">
|
||||
**국제판 테넌트.** 연동은 기본적으로 중국 본토 호스트(`open.feishu.cn`)를 사용합니다. 당신의 조직이 Lark 국제판 테넌트에 있다면, 전송 계층을 그쪽으로 가리키게 하세요.
|
||||
|
||||
```dotenv
|
||||
MULTICA_LARK_HTTP_BASE_URL=https://open.larksuite.com
|
||||
```
|
||||
</Callout>
|
||||
|
||||
## 다음
|
||||
|
||||
- [에이전트](/agents) — 각 봇은 정확히 하나의 에이전트에 바인딩됩니다
|
||||
- [Chat](/chat) — 봇 대화가 Multica 내부에서 무엇에 대응하는지
|
||||
- [이슈](/issues) — `/issue`가 생성하는 것
|
||||
- [환경 변수](/environment-variables) — 전체 자체 호스팅 구성 참조
|
||||
@@ -1,95 +0,0 @@
|
||||
---
|
||||
title: Lark Bot integration
|
||||
description: Bind a Multica agent to a Lark (飞书) Bot, then talk to it from a Lark DM or group — @-mention it, chat naturally, or type /issue to file a Multica issue without leaving Lark.
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
|
||||
Bind any [agent](/agents) to a Lark (飞书) Bot and your team can work with it from inside Lark — DM the Bot, @-mention it in a group, or type `/issue` to file a [Multica issue](/issues) without opening the app. The agent's replies stream back into the chat as a live card that updates while it works.
|
||||
|
||||
Each Bot is bound **one-to-one** to a single Multica agent. Binding a second agent creates a second Bot; one agent never has two Bots.
|
||||
|
||||
## What the integration does
|
||||
|
||||
| Surface | Behavior |
|
||||
|---|---|
|
||||
| **Agent → Integrations** | The agent detail page has an **Integrations** tab (and a matching section in the left sidebar). Owners and admins see **Bind to Lark** there; once bound it flips to a **Connected to Lark** badge with a **Manage in Lark** link. |
|
||||
| **DM the Bot** | A workspace member messages the Bot directly in Lark. Each conversation becomes a Multica [chat](/chat) session with the agent; the agent answers in-thread. |
|
||||
| **@-mention in a group** | Add the Bot to a Lark group and @-mention it. Only the mentioning message is read — the Bot does not listen to the whole group. |
|
||||
| **`/issue` command** | Typing `/issue <title>` (optionally with a body) creates a new Multica issue in the workspace, attributed to you. |
|
||||
| **Live reply card** | The Bot posts an interactive card and keeps patching it as the agent runs — progress, the final answer, or an error. |
|
||||
|
||||
## Bind an agent (owner / admin)
|
||||
|
||||
Binding uses a scan-to-install flow — no app secrets to copy, no developer console steps.
|
||||
|
||||
1. Open the agent in **Agents → _your agent_**.
|
||||
2. Go to the **Integrations** tab (or use the **Integrations** section in the left sidebar) and click **Bind to Lark**.
|
||||
3. A QR code appears. On your phone, open **Lark → Scan**, then authorize the new PersonalAgent Bot.
|
||||
4. When the scan completes the dialog closes and the agent shows **Connected to Lark**. Your own Lark identity is bound to your Multica account automatically, so you can start chatting with the Bot right away.
|
||||
|
||||
<Callout type="info">
|
||||
The QR is single-use and expires after a short window. If it lapses before you authorize, click **Scan again** for a fresh code.
|
||||
</Callout>
|
||||
|
||||
Once an agent is connected, the **Bind to Lark** button is replaced by a **Manage in Lark** link. Use it to open the Bot's app page in Lark when you need to adjust scopes, rename it, or request additional permissions — re-scanning is intentionally disabled so you don't strand the existing Bot.
|
||||
|
||||
## Use the Bot (members)
|
||||
|
||||
### First message: bind your Lark identity
|
||||
|
||||
The first time you message the Bot, it replies with a card asking you to **bind your Lark identity**. Tap the link, sign in to Multica, and your Lark account is linked to your Multica membership. This is what lets the agent act as you — for example, `/issue` files the issue under your name.
|
||||
|
||||
<Callout type="warning">
|
||||
Only people who are **members of the workspace** can use the Bot. If you aren't a member, or you skip the identity bind, the Bot won't respond — your message is dropped (and recorded for audit, without its contents).
|
||||
</Callout>
|
||||
|
||||
### Chat and `/issue`
|
||||
|
||||
- **Ask the agent anything** — DM the Bot or @-mention it in a group. The conversation is a normal agent chat session; the agent replies in the card.
|
||||
- **File an issue** — send `/issue Fix the login redirect` and Multica creates that issue in the workspace, assigned the way any new issue would be. Add more lines after the title for a description.
|
||||
- **Watch it work** — the reply card patches itself while the agent runs, so you see progress and the result in place.
|
||||
|
||||
If the agent is **offline** (its runtime isn't connected) or **archived**, the Bot replies with a short status notice instead of silently dropping your message.
|
||||
|
||||
## Manage and disconnect
|
||||
|
||||
Workspace-wide management lives in **Settings → Integrations**:
|
||||
|
||||
- **Connected bots** lists every Bot in the workspace and the agent each one is bound to. This list is visible to all members.
|
||||
- **Disconnect** is **owner / admin only**. Disconnecting stops the Bot from receiving Lark messages and tears down its connection; the installation record is kept for audit, and you can re-bind the same agent later.
|
||||
|
||||
## Permissions
|
||||
|
||||
- **Bind / disconnect** require workspace **owner** or **admin**. Members see the connected-bots list but no bind or disconnect controls.
|
||||
- **Talking to the Bot** requires being a workspace member with a bound Lark identity. Everyone else is dropped.
|
||||
- The integration never stores message bodies for dropped messages — only a drop reason, for audit.
|
||||
|
||||
## Self-host setup
|
||||
|
||||
On Multica Cloud the integration is already available — skip this section.
|
||||
|
||||
For self-host, Lark is **off until you set an at-rest encryption key**. The key encrypts each Bot's app secret before it touches the database.
|
||||
|
||||
1. Generate a 32-byte key and set it on the API server:
|
||||
|
||||
```dotenv
|
||||
MULTICA_LARK_SECRET_KEY=<base64-encoded 32-byte key>
|
||||
```
|
||||
|
||||
2. Restart the API. Until the key is set, **Settings → Integrations** shows a "Lark integration not enabled" notice and the **Bind to Lark** entry points stay hidden.
|
||||
|
||||
<Callout type="info">
|
||||
**International tenants.** The integration defaults to the mainland host (`open.feishu.cn`). If your organization is on Lark's international tenant, point the transport at it:
|
||||
|
||||
```dotenv
|
||||
MULTICA_LARK_HTTP_BASE_URL=https://open.larksuite.com
|
||||
```
|
||||
</Callout>
|
||||
|
||||
## Next
|
||||
|
||||
- [Agents](/agents) — each Bot is bound to exactly one agent
|
||||
- [Chat](/chat) — what a Bot conversation maps to inside Multica
|
||||
- [Issues](/issues) — what `/issue` creates
|
||||
- [Environment variables](/environment-variables) — full self-host configuration reference
|
||||
@@ -1,95 +0,0 @@
|
||||
---
|
||||
title: 飞书 Bot 接入
|
||||
description: 把 Multica 智能体绑定到飞书(Lark)Bot,就能直接在飞书里和它对话——私聊、群里 @ 它,或者输入 /issue 直接创建 Multica issue,全程不用离开飞书。
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
|
||||
把任意[智能体](/agents)绑定到飞书 Bot,团队就能在飞书里直接使用它——私聊 Bot、在群里 @ 它,或者输入 `/issue` 直接创建一个 [Multica issue](/issues),不用打开应用。智能体的回复会以一张实时卡片的形式回到聊天里,随着它干活不断更新。
|
||||
|
||||
每个 Bot 与一个 Multica 智能体**一对一**绑定。再绑定一个智能体会创建另一个 Bot;一个智能体永远不会有两个 Bot。
|
||||
|
||||
## 这个集成能做什么
|
||||
|
||||
| 入口 | 行为 |
|
||||
|---|---|
|
||||
| **智能体 → 集成** | 智能体详情页有一个 **集成(Integrations)** tab(左侧栏也有对应的区块)。所有者和管理员能在这里看到 **绑定到飞书**;绑定后会变成 **已连接到飞书** 徽标,并带一个 **在飞书中管理** 链接。 |
|
||||
| **私聊 Bot** | 工作区成员在飞书里直接给 Bot 发消息。每段对话都会成为该智能体的一个 Multica [chat](/chat) 会话,智能体在会话里回复。 |
|
||||
| **群里 @ 它** | 把 Bot 加进飞书群再 @ 它。Bot 只读取 @ 它的那条消息,不会监听整个群。 |
|
||||
| **`/issue` 命令** | 输入 `/issue <标题>`(可附正文)会在工作区创建一个新的 Multica issue,记在你名下。 |
|
||||
| **实时回复卡片** | Bot 会发出一张可交互卡片,并随着智能体运行不断更新——进度、最终答复或报错。 |
|
||||
|
||||
## 绑定智能体(所有者 / 管理员)
|
||||
|
||||
绑定走的是扫码安装流程——不用复制任何应用密钥,也不用进开发者后台。
|
||||
|
||||
1. 在 **Agents → 你的智能体** 打开该智能体。
|
||||
2. 进入 **集成(Integrations)** tab(或使用左侧栏的 **集成** 区块),点击 **绑定到飞书**。
|
||||
3. 弹出一个二维码。用手机打开 **飞书 → 扫一扫**,然后授权这个新的 PersonalAgent Bot。
|
||||
4. 扫码完成后弹窗关闭,智能体显示 **已连接到飞书**。你自己的飞书身份会自动绑定到你的 Multica 账号,绑完即可开始和 Bot 对话。
|
||||
|
||||
<Callout type="info">
|
||||
二维码是一次性的,并且会在较短时间后过期。如果在授权前就过期了,点 **重新扫码** 获取新码即可。
|
||||
</Callout>
|
||||
|
||||
智能体连接后,**绑定到飞书** 按钮会替换成 **在飞书中管理** 链接。需要调整权限范围、改名或申请更多权限时,用它打开 Bot 在飞书里的应用页面——重新扫码被刻意禁用,以免把已有的 Bot 弄成孤儿。
|
||||
|
||||
## 使用 Bot(成员)
|
||||
|
||||
### 第一条消息:绑定你的飞书身份
|
||||
|
||||
第一次给 Bot 发消息时,它会回一张卡片,让你 **绑定飞书身份**。点开链接、登录 Multica,你的飞书账号就会关联到你的 Multica 成员身份。正是这一步让智能体能以你的身份行事——比如 `/issue` 会把 issue 记在你名下。
|
||||
|
||||
<Callout type="warning">
|
||||
只有**工作区成员**才能使用 Bot。如果你不是成员,或者跳过了身份绑定,Bot 不会回复——你的消息会被丢弃(仅出于审计目的记录,不保存消息内容)。
|
||||
</Callout>
|
||||
|
||||
### 对话与 `/issue`
|
||||
|
||||
- **随便问智能体** —— 私聊 Bot,或在群里 @ 它。对话就是一段普通的智能体 chat 会话,智能体在卡片里回复。
|
||||
- **创建 issue** —— 发送 `/issue 修复登录跳转`,Multica 会在工作区创建这个 issue,和新建任何 issue 一样。标题后面再加几行就是描述。
|
||||
- **看它干活** —— 回复卡片会随着智能体运行不断更新,进度和结果都在原处呈现。
|
||||
|
||||
如果智能体**离线**(运行时未连接)或**已归档**,Bot 会回一条简短的状态提示,而不是悄悄丢掉你的消息。
|
||||
|
||||
## 管理与断开
|
||||
|
||||
工作区级别的管理在 **设置 → 集成**:
|
||||
|
||||
- **已连接的 Bot** 列出工作区里每个 Bot 以及它绑定的智能体。这个列表所有成员都能看到。
|
||||
- **断开连接** 仅限 **所有者 / 管理员**。断开后 Bot 停止接收飞书消息、连接被销毁;安装记录会保留以便审计,之后你可以重新绑定同一个智能体。
|
||||
|
||||
## 权限
|
||||
|
||||
- **绑定 / 断开** 需要工作区**所有者**或**管理员**。成员能看到已连接 Bot 列表,但看不到绑定或断开的操作。
|
||||
- **和 Bot 对话** 需要你是工作区成员且已绑定飞书身份。其余的人一律被丢弃。
|
||||
- 对于被丢弃的消息,集成不会保存消息内容——只记录一个丢弃原因,用于审计。
|
||||
|
||||
## 自部署配置
|
||||
|
||||
在 Multica Cloud 上这个集成已经可用——可跳过本节。
|
||||
|
||||
自部署时,**在你设置好静态加密密钥之前,飞书集成是关闭的**。这个密钥会在每个 Bot 的 app secret 落库之前对其加密。
|
||||
|
||||
1. 生成一个 32 字节的密钥并设置到 API 服务器:
|
||||
|
||||
```dotenv
|
||||
MULTICA_LARK_SECRET_KEY=<base64 编码的 32 字节密钥>
|
||||
```
|
||||
|
||||
2. 重启 API。在密钥设置好之前,**设置 → 集成** 会显示「未启用飞书集成」提示,**绑定到飞书** 入口也会保持隐藏。
|
||||
|
||||
<Callout type="info">
|
||||
**国际版租户。** 集成默认走中国大陆主机(`open.feishu.cn`)。如果你的组织在飞书国际版(Lark)租户上,把传输层指过去:
|
||||
|
||||
```dotenv
|
||||
MULTICA_LARK_HTTP_BASE_URL=https://open.larksuite.com
|
||||
```
|
||||
</Callout>
|
||||
|
||||
## 下一步
|
||||
|
||||
- [智能体](/agents) —— 每个 Bot 都绑定到恰好一个智能体
|
||||
- [Chat](/chat) —— 一段 Bot 对话在 Multica 里对应什么
|
||||
- [Issues](/issues) —— `/issue` 创建的是什么
|
||||
- [环境变量](/environment-variables) —— 完整的自部署配置参考
|
||||
@@ -1,60 +0,0 @@
|
||||
---
|
||||
title: メンバーと役割
|
||||
description: ワークスペースの3つの役割(owner、admin、member)がそれぞれ何をできるのか、そして人をどのように招待するのかを説明します。
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
|
||||
[ワークスペース](/workspaces)に属するすべての人は役割を持ち、その役割によって何ができるかが決まります。Multica には3つの役割があります。**owner**(ワークスペースのオーナー)、**admin**、**member** です。[イシュー](/issues)の作成、[コメント](/comments)の作成、[エージェント](/agents)の利用といった日常的な作業のほとんどは、3つの役割すべてで利用できます。**違いはチーム管理の領域に集中しています。**
|
||||
|
||||
## 権限の一覧
|
||||
|
||||
以下の表は、チーム管理アクションにおける最も重要な違いをまとめたものです。
|
||||
|
||||
| アクション | owner | admin | member |
|
||||
|---|---|---|---|
|
||||
| 新しい admin または member を招待 | ✓ | ✓ | ✗ |
|
||||
| **新しい owner を招待** | ✓ | ✗ | ✗ |
|
||||
| admin または member を降格 / 削除 | ✓ | ✓ | ✗ |
|
||||
| **別の owner を降格 / 削除** | ✓ | ✗ | ✗ |
|
||||
| ワークスペースの削除 | ✓ | ✗ | ✗ |
|
||||
|
||||
**member は誰も招待できません** — 招待は admin 層の権限です。**owner だけが他の人を owner に昇格できます** — admin は member や他の admin を昇格・降格できますが、新しい owner を作成することはできません。同様に、admin は member や他の admin を削除できますが、**既存の owner には手を出せません**。要点は、最上位の層をすでに保有している人だけがその層を付与できるようにすることです — 権限は上方向に漏れません。
|
||||
|
||||
<Callout type="info">
|
||||
エージェントの可視性には「workspace」と「private」の2種類があります。private エージェントは owner と admin だけがイシューに割り当てられます — これは特定の人だけが利用するように作られた構成を保護するためです。[エージェント](/agents)を参照してください。
|
||||
</Callout>
|
||||
|
||||
## 新しいメンバーを招待する
|
||||
|
||||
Multica はメールで新しいメンバーを招待します。
|
||||
|
||||
1. ワークスペース設定ページで **メンバーを招待** をクリックし、メールアドレスを入力して役割を選択します。
|
||||
2. Multica が一意のリンクを含む招待メールを送信します。
|
||||
3. 受信者がリンクをクリックしてログイン(または登録)し、**招待を承諾** するとワークスペースに参加します。
|
||||
|
||||
招待されるメールアドレスは **あらかじめ Multica に登録されている必要はありません** — アカウントがなければ、招待を承諾した時点で自動的に作成されます。
|
||||
|
||||
招待メールの配信に失敗しても(誤ったアドレス、メールサービスの不具合など)、招待レコードはそのまま保持されます。ワークスペース設定からメールを再送するか、招待リンクを別の経路で共有できます。
|
||||
|
||||
招待は **7日間有効** です。それ以降にリンクをクリックすると「期限切れ」のメッセージが表示され、招待した人が新しく送り直す必要があります。
|
||||
|
||||
## 常に最低1人の owner を維持する
|
||||
|
||||
すべてのワークスペースには **常に最低1人の owner が存在しなければなりません**。この制約により、2つの操作が自動的にブロックされます。
|
||||
|
||||
- 最後の owner は自分自身を降格できません。
|
||||
- 他の owner や admin は、最後の owner を削除できません。
|
||||
|
||||
<Callout type="warning">
|
||||
あなたが最後の owner でチームを離れようとしている場合は、**まず owner の役割を別のメンバーに譲渡してから**、ワークスペースを離れるか引き継いでください。そうしないと操作が拒否されます。
|
||||
</Callout>
|
||||
|
||||
## メンバーを削除する
|
||||
|
||||
owner と admin は、ワークスペースから他のメンバーを削除できます。削除されたメンバーは即座にアクセス権を失います。そのメンバーが作成したイシュー、コメントなどのコンテンツは、ワークスペースにそのまま保持されます。
|
||||
|
||||
## 次へ
|
||||
|
||||
- [イシューとプロジェクト](/issues) — メンバーが取り組む対象
|
||||
- [コメントとメンション](/comments) — イシューの下で協業する
|
||||
@@ -1,60 +0,0 @@
|
||||
---
|
||||
title: 멤버와 역할
|
||||
description: 워크스페이스의 세 가지 역할 — owner, admin, member — 가 각각 무엇을 할 수 있는지, 그리고 사람을 어떻게 초대하는지 설명합니다.
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
|
||||
[워크스페이스](/workspaces)에 속한 모든 사람은 역할을 가지며, 역할에 따라 무엇을 할 수 있는지가 결정됩니다. Multica에는 세 가지 역할이 있습니다. **owner**(워크스페이스의 소유자), **admin**, **member**입니다. [이슈](/issues) 생성, [댓글](/comments) 작성, [에이전트](/agents) 사용 같은 대부분의 일상 작업은 세 역할 모두 사용할 수 있습니다. **차이는 팀 관리 영역에 집중되어 있습니다.**
|
||||
|
||||
## 권한 한눈에 보기
|
||||
|
||||
아래 표는 팀 관리 작업에서 나타나는 가장 중요한 차이를 정리한 것입니다.
|
||||
|
||||
| 작업 | owner | admin | member |
|
||||
|---|---|---|---|
|
||||
| 새 admin 또는 member 초대 | ✓ | ✓ | ✗ |
|
||||
| **새 owner 초대** | ✓ | ✗ | ✗ |
|
||||
| admin 또는 member 강등 / 제거 | ✓ | ✓ | ✗ |
|
||||
| **다른 owner 강등 / 제거** | ✓ | ✗ | ✗ |
|
||||
| 워크스페이스 삭제 | ✓ | ✗ | ✗ |
|
||||
|
||||
**member는 누구도 초대할 수 없습니다** — 초대는 admin 등급의 권한입니다. **owner만 다른 사람을 owner로 승격할 수 있습니다** — admin은 member나 다른 admin을 승격하거나 강등할 수 있지만, 새 owner를 만들 수는 없습니다. 마찬가지로 admin은 member나 다른 admin을 제거할 수 있지만 **기존 owner는 건드릴 수 없습니다**. 핵심은 최고 등급을 이미 보유한 사람만이 그 등급을 부여할 수 있도록 하는 것입니다 — 권한은 위쪽으로 새어 나가지 않습니다.
|
||||
|
||||
<Callout type="info">
|
||||
에이전트 가시성에는 "workspace"와 "private" 두 가지가 있습니다. private 에이전트는 owner와 admin만 이슈에 할당할 수 있습니다 — 이는 특정 사람들만 사용하도록 만든 구성을 보호하기 위함입니다. [에이전트](/agents)를 참고하세요.
|
||||
</Callout>
|
||||
|
||||
## 새 멤버 초대하기
|
||||
|
||||
Multica는 이메일로 새 멤버를 초대합니다.
|
||||
|
||||
1. 워크스페이스 설정 페이지에서 **멤버 초대**를 클릭하고, 이메일을 입력한 뒤 역할을 선택합니다.
|
||||
2. Multica가 고유 링크가 담긴 초대 이메일을 보냅니다.
|
||||
3. 수신자가 링크를 클릭하고 로그인(또는 가입)한 뒤 **초대를 수락**하면 워크스페이스에 합류합니다.
|
||||
|
||||
초대받는 이메일은 **미리 Multica에 등록되어 있을 필요가 없습니다** — 계정이 없으면 초대를 수락할 때 자동으로 생성됩니다.
|
||||
|
||||
초대 이메일 전송이 실패하더라도(잘못된 주소, 메일 서비스 장애 등) 초대 기록은 그대로 유지됩니다. 워크스페이스 설정에서 이메일을 다시 보내거나, 초대 링크를 다른 경로로 공유할 수 있습니다.
|
||||
|
||||
초대는 **7일간 유효합니다**. 그 이후에 링크를 클릭하면 "만료됨" 메시지가 표시되며, 초대한 사람이 새로 보내야 합니다.
|
||||
|
||||
## 항상 최소 한 명의 owner 유지
|
||||
|
||||
모든 워크스페이스에는 **항상 최소 한 명의 owner가 있어야 합니다**. 이 제약은 두 가지 작업을 자동으로 차단합니다.
|
||||
|
||||
- 마지막 owner는 자신을 강등할 수 없습니다.
|
||||
- 다른 owner나 admin은 마지막 owner를 제거할 수 없습니다.
|
||||
|
||||
<Callout type="warning">
|
||||
당신이 마지막 owner이고 팀을 떠나려 한다면, **먼저 owner 역할을 다른 멤버에게 양도한 뒤** 워크스페이스를 떠나거나 넘기세요. 그렇지 않으면 작업이 거부됩니다.
|
||||
</Callout>
|
||||
|
||||
## 멤버 제거하기
|
||||
|
||||
owner와 admin은 워크스페이스에서 다른 멤버를 제거할 수 있습니다. 제거된 멤버는 즉시 접근 권한을 잃습니다. 그 멤버가 만든 이슈, 댓글 등의 콘텐츠는 워크스페이스에 그대로 유지됩니다.
|
||||
|
||||
## 다음
|
||||
|
||||
- [이슈와 프로젝트](/issues) — 멤버가 작업하는 대상
|
||||
- [댓글과 멘션](/comments) — 이슈 아래에서 협업하기
|
||||
@@ -1,63 +0,0 @@
|
||||
---
|
||||
title: "コメントでエージェントを @メンションする"
|
||||
description: コメントで @ を使ってエージェントをメンションし、ちょっと見てもらいましょう — 担当者の変更も、ステータスの変更もなく、割り当てより軽い操作です。
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
|
||||
[コメント](/comments)で[エージェント](/agents)を `@`メンションするのは、より軽いトリガーです — **担当者の変更も、ステータスの変更もなく**、ただエージェントに現在の[イシュー](/issues)を見てもらうよう軽く促すだけです。[**割り当て**](/assigning-issues)(エージェントを担当者にしてイシューを渡すこと)と比べると、@メンションは「この部分をちょっと見て」「別の角度から分析して」「ちょっと呼び込んで一緒に議論しよう」といった場面に向いています。
|
||||
|
||||
## コメントでエージェントをメンションする
|
||||
|
||||
メンバーをメンションするのと同じです — `@` を入力してピッカーを開き、エージェントを選んでください。コメントが投稿されると、Multica はメンションされた各エージェントに対して**そのコメント**をトリガーコンテキストとして、すぐに `task` をキューに入れます。エージェントがタスクを受け取ると、次のものを読めます。
|
||||
|
||||
- イシュー全体(説明 + すべての過去のコメント)
|
||||
- トリガーコメント自体 — 今回の実行の起点として
|
||||
|
||||
`@mention` の Markdown 構文、ピッカー、そして `@all` のセマンティクスは [**コメント**](/comments)で扱います。
|
||||
|
||||
<Callout type="info">
|
||||
**コメントで[スクワッド](/squads)を `@`メンションすることもできます。** 同じピッカーがメンバーやエージェントと並べてスクワッドも表示します。スクワッドを選ぶと `[@SquadName](mention://squad/<uuid>)` が挿入され、スクワッドの**リーダーエージェント**が応答を調整するようトリガーされます — 担当者とステータスはそのまま維持されます。
|
||||
</Callout>
|
||||
|
||||
## 割り当てとどう違うか
|
||||
|
||||
どちらもエージェントを働かせますが、仕組みはまったく異なります。
|
||||
|
||||
| 観点 | 割り当て | @メンション |
|
||||
|---|---|---|
|
||||
| `assignee` の変更 | ✓ | ✗ |
|
||||
| `status` の変更 | ✗ | ✗ |
|
||||
| `task` のキュー追加 | すぐに(Backlog 以外) | すぐに |
|
||||
| トリガーコメント ID | 任意 | 常に現在のコメントを含む |
|
||||
| 1 回の操作あたりの対象エージェント | 1(担当者 1 名) | 複数(1 つのコメントで複数を @ 可能) |
|
||||
| 優先度 | イシューから継承 | イシューから継承 |
|
||||
|
||||
判断の目安は単純です。**エージェントに「これからこのイシューを所有してほしい」なら割り当てを、「現在のコンテキストをちょっと見てほしい」なら @メンションを使ってください。**
|
||||
|
||||
## 複数のエージェントを @ するとどうなるか
|
||||
|
||||
1 つのコメントで複数のエージェントを @メンションすると、各エージェントは自分のランタイムで独立した `task` をキューに受け取ります — **互いをブロックすることなく並列に実行されます**。
|
||||
|
||||
同じイシューで、あるエージェントがすでに `queued` または `dispatched` 状態の `task` を持っている場合(たとえば、たった今メンションされてまだ開始していない場合)、今回のメンションは**重複排除**され、重複した `task` はキューに追加されません。重複排除は**単一のコメント単位で**適用されます — 数秒間隔で投稿された別々の 2 つのコメントが両方とも同じエージェントを @ すると、どちらも `task` をキューに入れます。
|
||||
|
||||
<Callout type="warning">
|
||||
**コメントを編集して @ を追加しても、再トリガーされません。** 投稿した後で `@agent` を追加しなければと気づいた場合、編集で入れた `@` は表示される内容を変えるだけで、そのエージェントに新しい `task` を**届けません**。トリガーするには、新しいコメントを投稿するか、イシューをそのエージェントに割り当ててください。
|
||||
</Callout>
|
||||
|
||||
## `@all` はどのエージェントもトリガーしない
|
||||
|
||||
`@all` で全員を呼ぶとき、**ワークスペースのメンバーだけがインボックスに入り、エージェントは `@all` の展開に含まれません。** これは意図された設計です。エージェントはインボックス通知を受け取らないため、`@all` はエージェントにとって意味を持ちません。エージェントを働かせるには、名前で直接メンションしてください。
|
||||
|
||||
## エージェントが自分自身を @メンションしてもループしない
|
||||
|
||||
エージェントは実行中にコメントを投稿でき、そのコメントには `@mention` が含まれることがあります。Multica にはハードコードされたガードがあります。**コメントの作成者が `@` メンションの対象エージェントと同じ場合、そのメンションはスキップされます** — 「エージェント A がエージェント A を @ → 新しい task → 再びエージェント A を @」のような無限ループは発生しません。
|
||||
|
||||
このガードは**直接的な自己参照だけをブロックします。** エージェント A がエージェント B を @メンションするのは正常に動作し、その後 B が応答で A を @メンションすると A が再びトリガーされます — つまり**間接的な再帰はブロックされません**。エージェントの指示を書くときは、複数のエージェントが互いを @メンションして循環を作らないよう注意してください。
|
||||
|
||||
## 次へ
|
||||
|
||||
- [**スクワッド**](/squads) — スクワッドを `@`メンションすると、リーダーが質問を適切なメンバーにルーティングします
|
||||
- [**チャット**](/chat) — イシューと無関係な 1 対 1 の会話
|
||||
- [**オートパイロット**](/autopilots) — エージェントがスケジュールに沿って自動的に作業を開始するようにする
|
||||
- [**コメント**](/comments) — `@mention` の構文、ピッカー、そして `@all` のセマンティクス
|
||||
@@ -1,63 +0,0 @@
|
||||
---
|
||||
title: "댓글에서 에이전트 @-멘션하기"
|
||||
description: 댓글에서 @로 에이전트를 멘션해 살펴보게 하세요 — 담당자 변경도, 상태 변경도 없이, 할당보다 가볍습니다.
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
|
||||
[댓글](/comments)에서 [에이전트](/agents)를 `@`-멘션하는 것은 더 가벼운 트리거입니다 — **담당자 변경도, 상태 변경도 없이**, 그저 에이전트가 현재 [이슈](/issues)를 살펴보도록 살짝 알리는 것입니다. [**할당**](/assigning-issues)(에이전트를 담당자로 만들고 이슈를 넘기는 것)과 비교하면, @-멘션은 "이 부분 좀 봐줘", "다른 관점으로 분석해줘", "잠깐 끌어들여서 같이 논의하자" 같은 상황에 잘 맞습니다.
|
||||
|
||||
## 댓글에서 에이전트 멘션하기
|
||||
|
||||
멤버를 멘션하는 것과 동일합니다 — `@`를 입력해 피커를 열고 에이전트를 선택하세요. 댓글이 게시되면 Multica는 멘션된 각 에이전트에게 **그 댓글**을 트리거 컨텍스트로 삼아 즉시 `task`를 대기열에 넣습니다. 에이전트가 작업을 받으면 다음을 읽을 수 있습니다.
|
||||
|
||||
- 이슈 전체(설명 + 모든 과거 댓글)
|
||||
- 트리거 댓글 자체 — 이번 실행의 시작점으로
|
||||
|
||||
`@mention` Markdown 문법, 피커, 그리고 `@all` 의미는 [**댓글**](/comments)에서 다룹니다.
|
||||
|
||||
<Callout type="info">
|
||||
**댓글에서 [스쿼드](/squads)도 `@`-멘션할 수 있습니다.** 동일한 피커가 멤버 및 에이전트와 함께 스쿼드도 보여줍니다. 스쿼드를 선택하면 `[@SquadName](mention://squad/<uuid>)`이 삽입되고, 스쿼드의 **리더 에이전트**가 응답을 조율하도록 트리거됩니다 — 담당자와 상태는 그대로 유지됩니다.
|
||||
</Callout>
|
||||
|
||||
## 할당과 어떻게 다른가
|
||||
|
||||
둘 다 에이전트를 일하게 만들지만, 동작 방식은 완전히 다릅니다.
|
||||
|
||||
| 항목 | 할당 | @-멘션 |
|
||||
|---|---|---|
|
||||
| `assignee` 변경 | ✓ | ✗ |
|
||||
| `status` 변경 | ✗ | ✗ |
|
||||
| `task` 대기열 추가 | 즉시(백로그가 아닌 경우) | 즉시 |
|
||||
| 트리거 댓글 ID | 선택 사항 | 항상 현재 댓글을 포함 |
|
||||
| 한 번의 동작당 대상 에이전트 | 1명(담당자 한 명) | 다수(댓글 하나에서 여러 명을 @ 가능) |
|
||||
| 우선순위 | 이슈에서 상속 | 이슈에서 상속 |
|
||||
|
||||
판단 기준은 간단합니다. **에이전트가 "지금부터 이 이슈를 책임지길" 원하면 할당을, "현재 컨텍스트를 한번 살펴보길" 원하면 @-멘션을 사용하세요.**
|
||||
|
||||
## 여러 에이전트를 @ 하면 어떻게 되는가
|
||||
|
||||
댓글 하나에서 여러 에이전트를 @-멘션하면, 각 에이전트는 자신의 런타임에서 독립적인 `task`를 대기열에 받습니다 — **서로를 막지 않고 병렬로 실행됩니다**.
|
||||
|
||||
같은 이슈에서 어떤 에이전트가 이미 `queued` 또는 `dispatched` 상태의 `task`를 가지고 있다면(예: 방금 멘션되었고 아직 시작하지 않은 경우), 이번 멘션은 **중복 제거**되어 중복 `task`가 대기열에 추가되지 않습니다. 중복 제거는 **단일 댓글 단위로** 적용됩니다 — 몇 초 간격으로 게시된 서로 다른 두 댓글이 모두 같은 에이전트를 @ 하면, 둘 다 `task`를 대기열에 넣습니다.
|
||||
|
||||
<Callout type="warning">
|
||||
**댓글을 편집해 @를 추가해도 다시 트리거되지 않습니다.** 게시한 뒤에야 `@agent`를 추가해야겠다고 떠올렸다면, 편집으로 넣은 `@`는 표시되는 내용만 바꿀 뿐 — 그 에이전트에게 새 `task`를 **전달하지 않습니다**. 트리거하려면 새 댓글을 게시하거나 이슈를 그 에이전트에게 할당하세요.
|
||||
</Callout>
|
||||
|
||||
## `@all`은 어떤 에이전트도 트리거하지 않는다
|
||||
|
||||
`@all`로 전체를 호출할 때, **워크스페이스 멤버만 인박스에 들어가며 — 에이전트는 `@all` 확장에 포함되지 않습니다.** 이는 의도된 설계입니다. 에이전트는 인박스 알림을 받지 않으므로 `@all`은 에이전트에게 아무 의미가 없습니다. 에이전트를 일하게 하려면 이름으로 직접 멘션하세요.
|
||||
|
||||
## 에이전트가 자기 자신을 @-멘션해도 루프에 빠지지 않는다
|
||||
|
||||
에이전트는 실행 중에 댓글을 게시할 수 있으며, 그 댓글에는 `@mention`이 포함될 수 있습니다. Multica에는 하드코딩된 가드가 있습니다. **댓글 작성자가 `@` 멘션의 대상 에이전트와 동일하다면, 그 멘션은 건너뜁니다** — "에이전트 A가 에이전트 A를 @ → 새 task → 다시 에이전트 A를 @" 같은 무한 루프는 발생하지 않습니다.
|
||||
|
||||
이 가드는 **직접적인 자기 참조만 차단합니다.** 에이전트 A가 에이전트 B를 @-멘션하는 것은 정상적으로 동작하며, 그 후 B가 응답에서 A를 @-멘션하면 A가 다시 트리거됩니다 — 다시 말해 **간접 재귀는 차단되지 않습니다**. 에이전트 지침을 작성할 때 여러 에이전트가 서로를 @-멘션해 순환을 이루지 않도록 주의하세요.
|
||||
|
||||
## 다음
|
||||
|
||||
- [**스쿼드**](/squads) — 스쿼드를 `@`-멘션하면 리더가 질문을 적절한 멤버에게 라우팅합니다
|
||||
- [**채팅**](/chat) — 이슈 밖에서의 일대일 대화
|
||||
- [**오토파일럿**](/autopilots) — 에이전트가 일정에 따라 자동으로 작업을 시작하게 하세요
|
||||
- [**댓글**](/comments) — `@mention` 문법, 피커, 그리고 `@all` 의미
|
||||
@@ -1,47 +0,0 @@
|
||||
{
|
||||
"title": "ドキュメント",
|
||||
"pages": [
|
||||
"index",
|
||||
"how-multica-works",
|
||||
"cloud-quickstart",
|
||||
"self-host-quickstart",
|
||||
"---ワークスペース & チーム---",
|
||||
"workspaces",
|
||||
"members-roles",
|
||||
"issues",
|
||||
"projects",
|
||||
"comments",
|
||||
"project-resources",
|
||||
"---エージェント---",
|
||||
"agents",
|
||||
"agents-create",
|
||||
"skills",
|
||||
"squads",
|
||||
"---エージェントの実行方法---",
|
||||
"daemon-runtimes",
|
||||
"install-agent-runtime",
|
||||
"tasks",
|
||||
"providers",
|
||||
"---エージェントとの協業---",
|
||||
"assigning-issues",
|
||||
"mentioning-agents",
|
||||
"chat",
|
||||
"autopilots",
|
||||
"---インボックス---",
|
||||
"inbox",
|
||||
"---連携---",
|
||||
"github-integration",
|
||||
"lark-bot-integration",
|
||||
"---セルフホスト & 運用---",
|
||||
"environment-variables",
|
||||
"auth-setup",
|
||||
"troubleshooting",
|
||||
"---リファレンス---",
|
||||
"cli",
|
||||
"auth-tokens",
|
||||
"desktop-app",
|
||||
"mobile-app",
|
||||
"---開発者---",
|
||||
"developers"
|
||||
]
|
||||
}
|
||||
@@ -31,7 +31,6 @@
|
||||
"inbox",
|
||||
"---Integrations---",
|
||||
"github-integration",
|
||||
"lark-bot-integration",
|
||||
"---Self-hosting & ops---",
|
||||
"environment-variables",
|
||||
"auth-setup",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user