mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-21 14:44:30 +02:00
Compare commits
1 Commits
chore/remo
...
agent/engi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6d3079183 |
23
.env.example
23
.env.example
@@ -95,16 +95,12 @@ RESEND_FROM_EMAIL=noreply@multica.ai
|
||||
# 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 +118,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 +189,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.
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -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`)
|
||||
|
||||
@@ -46,7 +46,6 @@ Use this option when your deployment cannot reach the public internet or you alr
|
||||
| `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.
|
||||
|
||||
@@ -94,8 +93,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 +112,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 +337,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 +456,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";
|
||||
}
|
||||
@@ -19,22 +19,12 @@ import { homedir, hostname } 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)];
|
||||
@@ -1010,10 +874,6 @@ export function setupDaemonManager(
|
||||
(_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);
|
||||
}
|
||||
18
apps/desktop/src/preload/index.d.ts
vendored
18
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,11 +90,6 @@ 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 }>;
|
||||
@@ -112,10 +100,6 @@ interface DaemonAPI {
|
||||
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"),
|
||||
@@ -210,11 +198,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,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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ 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));
|
||||
@@ -20,23 +19,6 @@ function expectChineseFontsBeforeKoreanFonts(source: string) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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(
|
||||
@@ -46,13 +28,4 @@ describe("CJK font fallback order", () => {
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -31,26 +31,6 @@
|
||||
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.",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -26,11 +26,11 @@ import { SquadsPage, SquadDetailPage as SquadDetailPageView } from "@multica/vie
|
||||
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" },
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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,13 +11,21 @@ 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",
|
||||
"Apple SD Gothic Neo",
|
||||
"Malgun Gothic",
|
||||
"Noto Sans CJK KR",
|
||||
"sans-serif",
|
||||
],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
|
||||
@@ -17,24 +17,6 @@ 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: {
|
||||
@@ -44,15 +26,6 @@ export const { GET } = createFromSource(source, {
|
||||
},
|
||||
},
|
||||
},
|
||||
ja: {
|
||||
components: {
|
||||
tokenizer: {
|
||||
language: "english",
|
||||
normalizationCache: new Map(),
|
||||
tokenize: tokenizeJapanese,
|
||||
},
|
||||
},
|
||||
},
|
||||
zh: {
|
||||
components: {
|
||||
tokenizer: {
|
||||
|
||||
@@ -4,7 +4,6 @@ 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));
|
||||
@@ -20,38 +19,13 @@ function expectChineseFontsBeforeKoreanFonts(source: string) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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"),
|
||||
const layoutSource = readFileSync(
|
||||
resolve(process.cwd(), "app/[lang]/layout.tsx"),
|
||||
"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);
|
||||
expectChineseFontsBeforeKoreanFonts(layoutSource);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,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,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,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`、サインアップ拒否
|
||||
@@ -37,7 +37,7 @@ SMTP 경로는 대부분의 온프레미스 메일 서버(특히 Microsoft Excha
|
||||
|---|---|---|---|
|
||||
| 익명 내부 relay | `25` | 없음 — IP / 서브넷으로 제출을 신뢰 | 전송 경로상 없음(내부 세그먼트 전용) |
|
||||
| 인증된 제출(submission) | `587` | `SMTP_USERNAME` + `SMTP_PASSWORD` | STARTTLS, 자동 업그레이드 |
|
||||
| 암묵적 TLS (SMTPS) | `465` | 선택 사항(`SMTP_USERNAME` + `SMTP_PASSWORD`) | 연결 시 TLS 핸드셰이크 — 포트 `465`에서 자동 활성화, 비표준 포트에서는 `SMTP_TLS=implicit`로 강제 |
|
||||
| 암묵적 TLS (SMTPS) | `465` | — | **아직 지원하지 않음** — 포트 25 또는 587을 사용하세요 |
|
||||
|
||||
**포트 25의 익명 Exchange relay** — 자격 증명 없이 신뢰된 서브넷에서 오는 메일을 받아들이는 일반적인 "internal SMTP relay" / Exchange 익명 receive connector:
|
||||
|
||||
@@ -61,27 +61,7 @@ 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`)을 확인하세요.
|
||||
시작 시 서버는 선택한 제공자를 출력합니다. 예를 들어 `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`)을 확인하세요.
|
||||
|
||||
**둘 다 설정하지 않으면**: 서버는 오류를 내지 않지만, **전송되어야 했던 모든 이메일이 서버의 stdout에만 기록됩니다**. 로컬 개발에는 편리하지만(로그에서 코드를 복사하면 됩니다), 프로덕션에서는 블랙홀이 됩니다.
|
||||
|
||||
|
||||
@@ -72,15 +72,6 @@ SMTP_TLS=implicit # optional on 465; required on a non-standard SMT
|
||||
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`).
|
||||
|
||||
**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.
|
||||
|
||||
@@ -72,15 +72,6 @@ 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 不报错,但所有本该发出去的邮件**只打到 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,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,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,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` のすべてのオプション
|
||||
@@ -88,7 +88,7 @@ CI나 headless 환경에서는 브라우저 플로우를 건너뛰세요. 웹
|
||||
| `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 member list/add/remove <squad-id>` | 스쿼드 멤버 관리 |
|
||||
| `multica squad activity <issue-id> <action\|no_action\|failed> --reason "..."` | 스쿼드 리더 에이전트가 매 턴마다 평가를 기록할 때 사용 |
|
||||
|
||||
전체 모델은 [스쿼드](/squads)를 참고하세요.
|
||||
|
||||
@@ -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,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,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,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,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,4 +0,0 @@
|
||||
{
|
||||
"title": "開発者",
|
||||
"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_*` パラメータが実際に何をするか
|
||||
@@ -49,12 +49,10 @@ Multica는 두 가지 전송 백엔드를 지원합니다 — 클라우드 배
|
||||
| 변수 | 기본값 | 설명 |
|
||||
|---|---|---|
|
||||
| `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` | 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 핸들러를 멈추게 할 수 없습니다.
|
||||
|
||||
@@ -86,19 +84,15 @@ Multica는 사용자가 업로드한 첨부 파일(댓글의 이미지와 파일
|
||||
| `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**은 다음 우선순위 순서로 구성됩니다:
|
||||
**공개 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가 설정되지 않은 경우)
|
||||
|
||||
| 변수 | 기본값 | 설명 |
|
||||
|
||||
@@ -54,7 +54,6 @@ Multica supports two delivery backends — [Resend](https://resend.com/) for clo
|
||||
| `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 +85,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 |
|
||||
|
||||
@@ -54,7 +54,6 @@ Multica 支持两种邮件发送通道——[Resend](https://resend.com/) 适合
|
||||
| `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 +85,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 未配时)
|
||||
|
||||
| 环境变量 | 默认值 | 说明 |
|
||||
|
||||
@@ -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,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,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,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,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,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,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,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,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",
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
"inbox",
|
||||
"---연동---",
|
||||
"github-integration",
|
||||
"lark-bot-integration",
|
||||
"---자체 호스팅 & 운영---",
|
||||
"environment-variables",
|
||||
"auth-setup",
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
"inbox",
|
||||
"---集成---",
|
||||
"github-integration",
|
||||
"lark-bot-integration",
|
||||
"---自部署运维---",
|
||||
"environment-variables",
|
||||
"auth-setup",
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
---
|
||||
title: モバイルアプリ (iOS)
|
||||
description: まだ App Store にないオープンソースの Multica iOS アプリを、自分の iPhone に自分でビルドする方法。
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
|
||||
Multica の iOS クライアントはオープンソースで、web、desktop、バックエンドとともに[メインリポジトリ](https://github.com/multica-ai/multica)に含まれています。まだ App Store にはなく、その状況が変わるまでは、iPhone で使いたい人がソースから自分でビルドします。ビルドは初回は約 10〜20 分、それ以降は約 2 分かかり、[multica.ai](https://multica.ai) と同じバックエンドと通信するため、既存のアカウントがそのまま使えます。
|
||||
|
||||
<Callout type="info">
|
||||
このページは**個人利用**向けです。アプリ開発者はリポジトリの [`apps/mobile/README.md`](https://github.com/multica-ai/multica/blob/main/apps/mobile/README.md) を読んでください — dev / staging のバリアントと全スクリプト一覧を扱っています。
|
||||
</Callout>
|
||||
|
||||
## 必要なもの
|
||||
|
||||
- Xcode がインストールされた **Mac**(App Store から無料で入手できます)。
|
||||
- Xcode → Settings → Accounts に追加した無料の **Apple ID**。有料の Apple Developer Program アカウントは任意で、7 日間の署名期間を 1 年に延長するだけです — 下記の [7 日制限](#7-day-signing-limit)を参照してください。
|
||||
- USB ケーブルで接続され、[Developer Mode が有効になった](https://docs.expo.dev/guides/ios-developer-mode/) **iPhone**(設定 → プライバシーとセキュリティ → デベロッパモード)。
|
||||
- チェックアウトした Multica のソースコード:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/multica-ai/multica.git
|
||||
cd multica
|
||||
pnpm install
|
||||
```
|
||||
|
||||
この一覧に欠けているものがあれば、Expo の [Set up your environment](https://docs.expo.dev/get-started/set-up-your-environment/) に沿って進めてください(**Development build → iOS Device** を選択)。リポジトリのチェックアウトを除くすべてに関する公式のセットアップガイドです。
|
||||
|
||||
## ビルドする
|
||||
|
||||
コマンドは 1 つ:
|
||||
|
||||
```bash
|
||||
pnpm ios:mobile:device:prod:release
|
||||
```
|
||||
|
||||
Xcode は Apple ID が自動的に所有する "Personal Team" でビルドに署名します — この team はどの Apple ID であれ初めて Xcode にサインインしたときに静かに作成されるため、何かを設定した記憶がなくてもすでに存在しています。これは **Release ビルド**です。Metro への依存がなく、スプラッシュ画面 → アプリへとつながり、App Store からインストールしたものとまったく同じです。
|
||||
|
||||
初回ビルドは CocoaPods をダウンロードし、React Native をソースからコンパイルします — 10〜20 分ほど見込んでください。以降のビルドは Xcode のキャッシュを再利用します。
|
||||
|
||||
一般的な手順はこれで終わりです。署名が失敗したら [トラブルシューティング](#troubleshooting)に進んでください。
|
||||
|
||||
## 7 日間の署名制限
|
||||
|
||||
無料の Apple ID はビルドを **7 日間**署名します。それを過ぎると、アプリは iPhone での起動を拒否し、「untrusted developer」エラーを表示します。Mac に再び接続して同じコマンドを再実行し、再署名してください — データはアプリではなくバックエンドにあるため、そのまま保持されます。
|
||||
|
||||
これを延長する唯一の方法は **Apple Developer Program アカウント**です([developer.apple.com](https://developer.apple.com) で年間 $99)。すると署名は更新の間 1 年間有効になり、TestFlight を通じて他のデバイスに配布することもできます。
|
||||
|
||||
## 更新
|
||||
|
||||
まだ自動更新はありません。Multica のコードベースが進んだら、pull して再ビルドしてください。
|
||||
|
||||
```bash
|
||||
git pull
|
||||
pnpm install
|
||||
pnpm ios:mobile:device:prod:release
|
||||
```
|
||||
|
||||
Xcode がネイティブコンパイルをキャッシュするため、以降のビルドは高速です。
|
||||
|
||||
## なぜまだ App Store にないのか
|
||||
|
||||
iOS アプリはまだ速いペースで動いています — チームは今のところ App Store の審査サイクルよりも、リリースして反復改善することを好んでいます。正式な App Store リリースの前に TestFlight ベータが最も可能性の高い次のステップです。それまでは、上記の自前ビルドが iOS で Multica を使う唯一の方法です。
|
||||
|
||||
TestFlight が公開されたときに通知を受け取りたい場合は、[GitHub リポジトリ](https://github.com/multica-ai/multica)を watch してください。
|
||||
|
||||
## トラブルシューティング
|
||||
|
||||
**「No matching provisioning profiles found」** — Xcode がデフォルトのバンドル id `ai.multica.mobile` をあなたの Apple ID で署名することを拒否しています。まれですが、誰かが Apple の開発者ポータルでそのプレフィックスを登録していると発生します。自分が管理する任意の逆引きドメイン(`com.yourname.multica` で十分です)を選んで export し、再実行してください。
|
||||
|
||||
```bash
|
||||
export EXPO_BUNDLE_IDENTIFIER_PROD=com.yourname.multica
|
||||
pnpm ios:mobile:device:prod:release
|
||||
```
|
||||
|
||||
id 自体に意味がある必要はありません — Apple は単に他の team に占有されていないことだけを求めています。
|
||||
|
||||
**「Could not launch <app>」/「Untrusted Developer」** — 7 日制限に達したか(ビルドを再実行してください)、iPhone で開発者プロファイルを手動で信頼する必要があります。設定 → 一般 → VPN とデバイス管理 → あなたの Apple ID をタップ → 信頼。
|
||||
|
||||
**`Pod install` でビルドが止まる、または延々とコンパイルが続く** — 初回ビルドは CocoaPods が依存関係をダウンロードし、Xcode が React Native をソースからコンパイルするため、実際に 10〜20 分かかります。以降のビルドははるかに高速です。
|
||||
|
||||
**アプリがバックエンドに接続できない** — `apps/mobile/.env.production` が変更されていないか確認してください(デフォルトでは `EXPO_PUBLIC_API_URL=https://api.multica.ai` が同梱されています)。変更した場合は `git checkout apps/mobile/.env.production` で復元してください。
|
||||
@@ -1,262 +0,0 @@
|
||||
---
|
||||
title: プロジェクトリソース
|
||||
description: 型付きのポインター(Git リポジトリ、ローカルディレクトリ、今後さらに増える種類)をプロジェクトに添付し、エージェントが範囲を限定したコンテキストとして取り込めるようにします。
|
||||
---
|
||||
|
||||
**プロジェクトリソース(Project Resource)** は型付きのポインターです — Git リポジトリの URL、自分のマシン上のパス、いずれは Notion ページまで — これを[プロジェクト](/workspaces)に添付します。[エージェント](/agents)がそのプロジェクト内のイシューに対して実行されると、デーモンはプロジェクトのリソース一覧をエージェントの作業ディレクトリと[メタスキル](/skills)プロンプトに自動的に書き込みます。
|
||||
|
||||
その結果、エージェントは、どのリポジトリをチェックアウトすべきか(またはどのローカルディレクトリで作業すべきか)、そしてこのプロジェクトの「主要な参照資料」が何かを、誰もコンテキストをイシュー本文にコピー&ペーストしなくても把握できます。
|
||||
|
||||
## メンタルモデル
|
||||
|
||||
プロジェクトはもはや単なるラベルではありません。小さな **リソースコンテナ** です。
|
||||
|
||||
- プロジェクトは 0..N 個の **リソース** を持ちます。
|
||||
- リソースは `resource_type`(例: `github_repo`、`local_directory`)と `resource_ref`(`resource_type` によって型が定まる JSON ペイロード)を持ちます。
|
||||
- 新しいリソースタイプを追加するには、文字列1つ + ハンドラー1つを追加するだけです。**スキーマのマイグレーションも、フロントエンドの書き直しも不要です。**
|
||||
|
||||
この形は意図的なものです — Multica がエージェントプロバイダーですでに使っているのと同じパターンです: `type` の判別子1つと型付きのペイロード。スキーマを安定させるため、後で「Notion ページ」「Google Doc」「アップロードされたファイル」「外部 URL」を追加するのは、小さく追加的な変更で済みます。
|
||||
|
||||
現在、2つのリソースタイプが提供されています: [`github_repo`](#resource-type-github_repo)(タスクごとに隔離されたワークツリーへクローン)と [`local_directory`](#resource-type-local_directory)(特定のデーモンのマシン上のフォルダ内で直接実行)です。
|
||||
|
||||
## リソースタイプ: `github_repo`
|
||||
|
||||
デフォルトのリソースタイプです — タスクごとに隔離されたワークツリーへチェックアウトされます。
|
||||
|
||||
```json
|
||||
{
|
||||
"resource_type": "github_repo",
|
||||
"resource_ref": {
|
||||
"url": "https://github.com/owner/repo",
|
||||
"default_branch_hint": "main"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`default_branch_hint` は任意です — 指定すると、デーモンがこれをメタスキルに出すため、エージェントはどのブランチを基準に作業すべきかを把握できます。
|
||||
|
||||
## リソースタイプ: `local_directory`
|
||||
|
||||
タスクごとに再クローンするのが現実的でないリポジトリ — 数ギガバイトのゲームのチェックアウト、大規模な monorepo、またはタスクごとのワークツリーモデルが煩わしいあらゆるプロジェクト — の場合、プロジェクトは代わりに **特定の[デーモン](/daemon-runtimes)のマシン上にある既存のディレクトリ** を指すことができます。エージェントは、クローンもコピーもワークツリーもなしに **そのフォルダ内で直接** 実行されます。
|
||||
|
||||
```json
|
||||
{
|
||||
"resource_type": "local_directory",
|
||||
"resource_ref": {
|
||||
"local_path": "/Users/me/code/big-game",
|
||||
"daemon_id": "0001234e-…",
|
||||
"label": "main checkout"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`github_repo` と比べたトレードオフは意図的です。バインドされたデーモンだけがそのディレクトリに対するタスクを取得でき、同じディレクトリに対するタスクは並列ではなく **直列で** 実行されます。その代わり、既存のチェックアウト、既存のブランチ、既存のダーティな状態をそのまま保てます — Multica は決して再クローンしません。
|
||||
|
||||
### `github_repo` より `local_directory` を選ぶとき
|
||||
|
||||
| 検討事項 | `github_repo`(ワークツリー) | `local_directory` |
|
||||
| --- | --- | --- |
|
||||
| タスクごとのチェックアウトコスト | 新規クローン + ワークツリー | なし — エージェントがその場で実行 |
|
||||
| 同じリポジトリでの並行性 | 複数タスクを並列に | ディレクトリごとに一度に1つ |
|
||||
| ブランチ / ダーティな状態 | タスクごとにデフォルトから新しいブランチを取得 | ディレクトリが現在持っているそのまま |
|
||||
| 実行できる場所 | 任意のデーモン | ちょうど1つのデーモン(バインドされたもの) |
|
||||
| ディスク使用量 | タスクごとにワークツリー1つ | オーバーヘッド0 — 既存のフォルダ |
|
||||
|
||||
次のいずれか **1つでも** 当てはまる場合は `local_directory` を選んでください。
|
||||
|
||||
1. **再クローンのコストが法外に大きい場合** — 数ギガバイトのゲームのチェックアウト、重い LFS アセットを持つ monorepo、またはタスクごとの `git clone` が実際の作業を圧倒するあらゆる場合。並行性をクローン不要の実行と引き換えにすることになります。
|
||||
2. **変更がきめ細かく、変更が起きるそばからローカルでレビューしたい場合** — 単一のコンポーネントを繰り返し磨いていて、数分ごとにエージェントの編集と自分のエディターを行き来したく、`~/multica_workspaces/` から掘り出さなければならないタスクごとのワークツリーよりも、既存のチェックアウトを信頼できる情報源にしたい場合です。
|
||||
|
||||
どちらの場合でも受け入れるトレードオフは同じです: **このバージョンはファイル単位の書き込みロックを提供しません。** ディレクトリごとの直列ゲート(同じフォルダで一度に1つのタスク)が、別々の2つのイシューのエージェントが同時に同じファイルを触るのを防ぐ唯一の保護手段です。2つのイシューのエージェントを同じ `local_directory` に向けると、それらのタスクは並列化されずにキューに入ります — これは意図された動作です。同じコードベースで本物の並列性が必要なら、`github_repo` を使い続けてください。
|
||||
|
||||
### ローカルディレクトリを添付する
|
||||
|
||||
フォルダピッカーは **Desktop アプリ** にのみあります — Web アプリは OS のパスを読み取る方法がないため、「ローカルディレクトリを追加」ボタンはそこでは非表示になっています。Desktop では:
|
||||
|
||||
1. プロジェクトを開く → **Resources** パネル。
|
||||
2. **ローカルディレクトリを追加** をクリックします。ネイティブのフォルダピッカーが開きます。
|
||||
3. フォルダを選択します。そのパスは **この Desktop インストールが現在登録しているデーモン** にバインドされます — リソースレコードにはパスとそのデーモンの ID が一緒に保存されます。
|
||||
|
||||
Desktop では、このマシンのデーモンがオフラインのとき、またはプロジェクトにすでにこのデーモンへバインドされた `local_directory` があるとき、ボタンは表示されたまま **ヒントとともに無効化** されます — そのため *なぜ* 使えないのかが分かります。(Web アプリではそもそもフォルダピッカーが一切ないため、ボタンは完全に非表示になります。)別のマシンのディレクトリをバインドするには、そのマシンに Desktop をインストールし、そこからリソースを追加してください。
|
||||
|
||||
CLI からも可能です(デーモン ID を自分で指定すれば、Web 専用環境でも動作します)。
|
||||
|
||||
```bash
|
||||
multica project resource add <project-id> \
|
||||
--type local_directory \
|
||||
--local-path /Users/me/code/big-game \
|
||||
--daemon-id <daemon-uuid> \
|
||||
--ref-label "main checkout" # optional
|
||||
|
||||
multica project resource update <project-id> <resource-id> \
|
||||
--local-path /Users/me/code/big-game-new
|
||||
```
|
||||
|
||||
`--daemon-id` は `multica daemon list` から取得できます。CLI は、ペイロードを直接渡したい場合のための汎用的な `--ref '<json>'` の脱出口も受け付けます。
|
||||
|
||||
### パスのルール
|
||||
|
||||
添付するパスは、添付時の検証とタスクごとの検証の両方を通過しなければなりません。どちらもリソースを所有するデーモンが強制します — サーバーは JSON を保存するだけです。いずれかのルールに違反するパスは、型付きのエラーとともにタスクを失敗させ、あなたのディレクトリには手を付けずに残します。
|
||||
|
||||
- 必ず **絶対パス** でなければなりません。
|
||||
- 必ず **存在** し、**ディレクトリ** でなければなりません(ファイル、ファイルへの symlink、デバイスノードではなく)。
|
||||
- デーモンプロセスが **読み書き可能** でなければなりません。
|
||||
- システムルートやユーザープロファイル全体であってはいけません — `/`、`/Users`、`/home`、`/root`、`/etc`、`/tmp`、`/var`、`/usr`、`/opt`、`/Users/Shared`、自分の `$HOME`、任意の Windows ドライブルート(`C:\`、`D:\`、…)、または `C:\Users` / `C:\ProgramData` / `C:\Program Files` / `C:\Program Files (x86)` / `C:\Windows`。
|
||||
- 上記のいずれかに解決される symlink は拒否され、OS がエイリアス処理するパスの正規形(canonical form)も同様です(例: macOS で `/private/tmp` を入力するのは `/tmp` と同じように拒否されます)。
|
||||
|
||||
このブラックリストは意図的に攻撃的です — ホームディレクトリを選ぶと Multica のランタイムファイルがアカウントのルートに置かれることになりますが、これは決して望ましい結果ではありません。代わりにサブフォルダ(通常は実際のプロジェクトのチェックアウト)を選んでください。
|
||||
|
||||
### (プロジェクト、デーモン)ごとに1つ
|
||||
|
||||
プロジェクトは **デーモンごとに最大1つの `local_directory`** しか持てません。同じデーモンに2つ目を追加しようとすると、API は `409` を返します。Desktop のボタンは上限にすでに達すると自動的に非表示になり、理由を説明するツールチップを表示します。
|
||||
|
||||
異なるデーモンは独立しています — 共有プロジェクトはチームメイトのマシンごとに1つずつ `local_directory` を持つことができ、それぞれが同じプロジェクトを別々のホストの別々のフォルダにバインドします。デーモンがタスクを取得するときは、自分の ID に一致する行を選び、残りは無視します。
|
||||
|
||||
### リソースタイプの混在、および複数の `local_directory` リソース
|
||||
|
||||
実際に登場する2つの横断的なリソース構成があります。
|
||||
|
||||
- **同じプロジェクトに `github_repo` + `local_directory`。** 一致する `local_directory` バインディングを持つデーモンでは、ローカルディレクトリが **優先** されます。エージェントはあなたのフォルダで実行され、デーモンはそのタスクのために `github_repo` ワークツリーを作成も使用もしません。(ワークスペースごとのリポジトリキャッシュは平常どおり同期される場合がありますが、これはこのタスクの作業ツリーとは無関係なバックグラウンドの動作です。)`github_repo` の URL は参照用として `.multica/project/resources.json` とエージェントの `## Repositories` セクションに依然として表示されますが、エージェントが編集する作業ツリーはワークツリーではなく、あなたのローカルなものです。このプロジェクトに対する `local_directory` 行を **持たない** デーモン(別のマシン、またはそのチームメイトが1つを添付する前)では、タスクは通常の `github_repo` ワークツリーのフローにフォールバックします。実質的に、ローカルディレクトリはワークツリーパスに対するデーモンごとのオーバーライドです。
|
||||
- **同じプロジェクトに2つの `local_directory` リソース。** 各 `local_directory` はちょうど1つのデーモンにバインドされるため、これは別々の2つのマシンの間でのみ発生します(API は添付時に同じデーモンへの2つを拒否します。上記参照)。タスクは、どのデーモンがローカルディレクトリを持っているかではなく、エージェントのランタイム割り当てによってルーティングされます。タスクは受信するエージェントのランタイムを所有するデーモンに届き、そのデーモンは自分の ID に一致する `local_directory` 行を選び、残りは無視します。ロードバランシングはありません — 特定のマシンにタスクを実行させたい場合は、そのマシンのランタイムにバインドされたエージェントをディスパッチしてください。
|
||||
|
||||
別の場所に1つがバインドされているプロジェクトに対して `local_directory` 行を持たないデーモンは **ブロックされません** — そのタスクは単に、プロジェクトの他のリソース(通常は `github_repo` のフォールバック)を通じて進みます。`local_directory` は、それがバインドされたデーモンに対してのみ意味があります。
|
||||
|
||||
### ローカルディレクトリに対してタスクを実行する
|
||||
|
||||
プロジェクトが受信デーモンにバインドされた `local_directory` を持つイシューでタスクがディスパッチされると、デーモンは次のことを行います。
|
||||
|
||||
1. パスを再検証します(上記のルール)。
|
||||
2. symlink を解決した実際のパスをキーとして、ディレクトリごとのロックを取得します — そのため、同じフォルダに向かう2つの経路(1つは symlink 経由、1つは直接)も依然として直列化されます。
|
||||
3. エージェントの `CLAUDE.md` / `AGENTS.md`(および `.multica/project/resources.json`)を **ユーザーのディレクトリ内に** 書き込みます。エージェントは、あなたが自分でそのフォルダを開いたのとまったく同じように、そこで作業します。
|
||||
4. Multica のランタイム成果物(`output/`、`logs/`、`.gc_meta.json`)は、ユーザーのディレクトリの **外側** の別の envRoot に置きます。
|
||||
|
||||
同じディレクトリに対する2つ目のタスクが、1つ目のタスクの実行中に届くと、ステータス **ローカルディレクトリ待ち(Waiting for local directory)** で待機します。このステータスは、タスクがあるあらゆる場所で見えます — チャットのタスクピル、エージェントのバナー、実行ログ、アクティビティインジケーター — そして待機中のタスクはエージェントの「キュー済み」のプレゼンスにカウントされます。待機中のタスクをキャンセルすると、そのスロットが即座に解放されます。実行中のタスクをキャンセルすると、次のタスクが昇格します。
|
||||
|
||||
この待機はタイムアウトではありません — 待機中のタスクは、ロックが解放されるか、ユーザー / エージェントがキャンセルするまで待機し続けます。
|
||||
|
||||
### Multica があなたのディレクトリで触れるものと触れないもの
|
||||
|
||||
- **書き込みます**: `CLAUDE.md` / `AGENTS.md`(またはエージェントのプロバイダーに対応する同等物)と `.multica/project/resources.json` をディレクトリのルートに。そのためエージェントはメタスキルとリソース一覧を持ちます。コミットされたくない場合は、これらを `.gitignore` に追加してください。
|
||||
- **書き込みます**: エージェントが行うと判断したあらゆるコード編集を — あなたが自分でローカルでエージェントを実行したのとまったく同じ方法で。
|
||||
- **物理的に削除することは決してありません**: ディレクトリやその中の何も。ガベージコレクションはパスを認識します: `local_directory` の envRoot の場合、`workspacesRoot` の下にある自身の `output/` と `logs/` だけをクリーンアップし、ユーザーのディレクトリは立ち入り禁止として扱います。
|
||||
|
||||
### v1 の制限事項(後続作業で狭まる予定)
|
||||
|
||||
最初のリリースは意図的に `github_repo` より鋭い角を持って出荷されます。この一覧は時間とともに縮小していくと考えてください — ここに記載されているのは今日時点で事実の内容です。
|
||||
|
||||
- **自動ブランチ切り替えなし。** エージェントは、あなたがチェックアウトしているブランチで実行されます。重要なら、ディスパッチ前にブランチを切り替えてください。
|
||||
- **ダーティツリーの保護や自動コミットなし。** コミットされていない変更はエージェントから見え、その場で変更される可能性があり、stash されません。ディレクトリを実際の作業ツリーとして扱い、危険な実行の前にコミットしてください。
|
||||
- **自動 PR なし。** タスクが終わると、変更は作られたブランチのまま残ります — 何も push されず、PR も開かれません。準備ができたら、自分で push して PR を開いてください。
|
||||
- **`waiting_local_directory` はステータスを示しますが、保有者は示しません。** バッジはタスクが待機していることを伝えます。どのタスクやどのファイルパスが現在ディレクトリを保有しているかは表示しません。
|
||||
|
||||
これらはローカルディレクトリ作業のエージェントタスクライフサイクルの後続項目として追跡されています。それが出荷されるまでは、`local_directory` を「エージェントがあなたのフォルダで、あなたがするのと同じ方法で実行する」ものとして扱ってください。
|
||||
|
||||
## プロジェクト作成時にリポジトリを添付する
|
||||
|
||||
**Web** または **Desktop** アプリで *新規プロジェクト* を開くと、ステータス / 優先度 / リードの横に **Repos** ピルが表示されるようになりました。ワークスペースにバインドされたリポジトリを選択する(またはアドホックな URL を貼り付ける)と、プロジェクトが作成される瞬間にそれらが `github_repo` リソースとして添付されます。
|
||||
|
||||
**CLI** から:
|
||||
|
||||
```bash
|
||||
# Create + attach in one shot. The server attaches resources in the same
|
||||
# transaction as the project create — invalid resources roll back the whole
|
||||
# operation, so you never end up with a project that has half its resources.
|
||||
multica project create \
|
||||
--title "Agent UX 2026" \
|
||||
--repo https://github.com/multica-ai/multica
|
||||
|
||||
# Manage resources later
|
||||
multica project resource list <project-id>
|
||||
multica project resource add <project-id> --type github_repo --url <url>
|
||||
multica project resource remove <project-id> <resource-id>
|
||||
|
||||
# Generic escape hatch for any resource_type the server understands —
|
||||
# no CLI change needed when a new type ships:
|
||||
multica project resource add <project-id> \
|
||||
--type notion_page \
|
||||
--ref '{"page_id":"…","title":"…"}'
|
||||
```
|
||||
|
||||
`--repo` は繰り返し指定できます。各値は別々の `github_repo` リソースとして添付されます。
|
||||
|
||||
## ランタイムにエージェントが見るもの
|
||||
|
||||
デーモンがプロジェクト内のイシューのためにエージェントを生成すると、2つのことが起こります。
|
||||
|
||||
### 1. `.multica/project/resources.json`
|
||||
|
||||
API レスポンスの構造化されたパススルー(pass-through)で、エージェントの作業ディレクトリに書き込まれます。
|
||||
|
||||
```json
|
||||
{
|
||||
"project_id": "…",
|
||||
"project_title": "Agent UX 2026",
|
||||
"resources": [
|
||||
{
|
||||
"id": "…",
|
||||
"resource_type": "github_repo",
|
||||
"resource_ref": {
|
||||
"url": "https://github.com/multica-ai/multica",
|
||||
"default_branch_hint": "main"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
スキル、ヘルパースクリプト、またはエージェント自身が、この実行のための *正確な* リソースの集合が必要なときに、このファイルをパースできます。
|
||||
|
||||
### 2. メタスキルプロンプトの「Project Context」セクション
|
||||
|
||||
エージェントの `CLAUDE.md` / `AGENTS.md`(プロバイダーによって異なる)には、人間が読める要約が含まれるようになりました。
|
||||
|
||||
```
|
||||
## Project Context
|
||||
|
||||
This issue belongs to **Agent UX 2026**.
|
||||
|
||||
Project resources (also written to `.multica/project/resources.json`):
|
||||
|
||||
- **GitHub repo**: https://github.com/multica-ai/multica (default branch: `main`)
|
||||
|
||||
Resources are pointers — open them only when relevant to the task. For
|
||||
`github_repo` resources, use `multica repo checkout <url>` to fetch the code.
|
||||
```
|
||||
|
||||
このテキストは意図的に最小限です。完全なペイロードはディスク上にあり、プロンプトはエージェントがプロジェクトが存在することと何が添付されているかを把握できるように方向付けるだけです。
|
||||
|
||||
### 失敗モード
|
||||
|
||||
リソースの取得は **best-effort** です。API 呼び出しが失敗すると、プロンプトからプロジェクトセクションが省略され、ファイルも書き込まれませんが、タスクは依然として開始されます。エージェントは、欠落したプロジェクトコンテキストのために止まることは決してありません。
|
||||
|
||||
## 新しいリソースタイプを追加する
|
||||
|
||||
この抽象化の要点は、新しいタイプが安価だということです。完全な経路は:
|
||||
|
||||
1. **サーバーの検証器**(`server/internal/handler/project_resource.go`)— `validateAndNormalizeResourceRef` に、新しいペイロードをパースして正規化する case を追加します。
|
||||
2. **デーモンのメタスキルフォーマッター**(`server/internal/daemon/execenv/runtime_config.go`)— `formatProjectResource` に case を追加し、エージェントのプロンプトが新しいタイプを読みやすい箇条書きとしてレンダリングするようにします。
|
||||
3. **TypeScript の型**(`packages/core/types/project.ts`)— `ProjectResourceType` を拡張し、ペイロードのインターフェースを追加します。
|
||||
4. **UI レンダラー**(`packages/views/projects/components/project-resources-section.tsx`)— `ResourceRow` に新しいタイプのための case を追加します。
|
||||
|
||||
**スキーマのマイグレーションも、新しい sqlc クエリも、新しいエンドポイントも、そして CLI の変更もありません** — CLI の汎用的な `--ref '<json>'` フラグが、検証器が理解するあらゆるペイロードを受け付けるため、新しいタイプの初日サポートは純粋に上記の4ステップだけです。(後でタイプごとの CLI ショートカットを *任意で* 追加できますが、必須ではありません。)
|
||||
|
||||
同じ `project_resource` テーブルと同じ3つの CRUD 呼び出しが、すべてのタイプを処理します。
|
||||
|
||||
## ワークスペースのリポジトリ vs. プロジェクトのリポジトリ
|
||||
|
||||
エージェントに表示されるリポジトリ一覧(`CLAUDE.md` / `AGENTS.md` の `## Repositories` ブロック)は、デーモンのクレームハンドラーが次の優先順位で選びます。
|
||||
|
||||
- **プロジェクトが最低1つの `github_repo` リソースを持つ** → そのリポジトリだけがエージェントに出されます。ワークスペースにバインドされたリポジトリは、エージェントがこのイシューにどれが属するかを推測しなくて済むように、意図的に隠されます。
|
||||
- **プロジェクトが `github_repo` リソースを持たない(またはイシューがプロジェクトに属さない)** → 従来どおりワークスペースのリポジトリ一覧にフォールバックします。
|
||||
|
||||
これによりエージェントの作業セットが引き締まります: プロジェクトがリポジトリについて明示的であれば、それが権威ある答えです。`.multica/project/resources.json` の構造化されたリソース一覧は常に完全な集合を運ぶため、すべてを検査したいスキルは依然としてそうできます。
|
||||
|
||||
デーモンはチェックアウト側でもこれを反映します: プロジェクト範囲の `github_repo` URL を持つタスクが届くと、それらの URL はエージェントが生成される前に、ワークスペースごとの許可リストにマージされ *同時に* ローカルのリポジトリキャッシュに同期されます。そのため、ワークスペースレベルでバインドされていないプロジェクトのリポジトリ URL も、依然として `multica repo checkout` の有効な引数になります — デーモンはそれを「構成されていない」として拒否しません。許可リストの分割は内部的なものです: ワークスペースにバインドされた URL とタスク範囲の URL は別々に追跡されるため、ワークスペースリポジトリの再読み込みが実行の途中でプロジェクト URL を誤って取り消すことはありません。
|
||||
|
||||
## ここで意図的に範囲に **含めなかった** もの
|
||||
|
||||
- **プロジェクト間の共有。** 今日時点で、各リソースはちょうど1つのプロジェクトにのみ存在します。
|
||||
- **スキルごとのリソース範囲指定。** すべてのリソースは、エージェント実行のすべてのスキルから見えます。タイプを認識したフィルタリングは後続作業です。
|
||||
- **キャッシュ / 同期。** `github_repo` は単なるメタデータです — チェックアウトは依然として必要に応じて `multica repo checkout` を通じて行われます。Notion / Google Docs のキャッシュされた文書テキストは、それらのタイプとともに提供される予定です。
|
||||
|
||||
これらは意図的な省略です — 最初のカットの目標は、可動部分を最小限にしてこの抽象化を検証することです。
|
||||
@@ -1,49 +0,0 @@
|
||||
---
|
||||
title: プロジェクト
|
||||
description: 関連するイシューをまとめて1つの単位として追跡します — 優先度、ステータス、進捗、担当者とともに。
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
|
||||
Multica の **プロジェクト** は、関連する[イシュー](/issues)をまとめるコンテナです。作業の分量がイシュー1つよりは大きいが、ワークスペース全体よりは小さいときに使ってください — リリース、マイグレーション、複数の部分に分かれる機能、いくつもの枝に分岐する調査などです。
|
||||
|
||||
各プロジェクトには、名前、アイコン、説明、**リード**(メンバーまたは[エージェント](/agents))、**ステータス**(`planned` / `in_progress` / `paused` / `completed` / `cancelled`)、**優先度**(`urgent` / `high` / `medium` / `low` / `none`)、そして紐づくイシューのステータスから自動的に算出される **進捗** の百分率があります。
|
||||
|
||||
## プロジェクトとイシューの関係
|
||||
|
||||
プロジェクトとイシューは独立したオブジェクトで、多対一の関係です。1つのイシューは **最大1つの** プロジェクトに属することができ、1つのプロジェクトは **任意の数の** イシューを保持できます。紐づけと紐づけ解除はいつでも元に戻せます — ボードビューでドラッグするか、イシュー右側のプロパティパネルにあるプロジェクトピッカーを使ってください。
|
||||
|
||||
プロジェクトの進捗バーは、紐づくイシューから計算されます — `done` に到達したイシューが多いほど、バーがより満たされます。`cancelled` のイシューは集計から除外されます。`backlog` のイシューは分母にはカウントされますが、分子にはカウントされません。
|
||||
|
||||
## サイドバーにピン留めする
|
||||
|
||||
プロジェクト右上のピンアイコンをクリックすると、サイドバーのピン留めリストに追加されます。ピン留めされたプロジェクトは、ワークスペースのどこにいてもワンクリックでアクセスできます。チームの全員がそれぞれ独立してピン留めできます — ピン留めは個人ごとの設定です。
|
||||
|
||||
サイドバーの **ワークスペース → プロジェクト** リンクは、常にワークスペースのすべてのプロジェクトを表示します。ピン留めはその上に重ねる個人用のショートカットにすぎません。
|
||||
|
||||
## リソースを添付する
|
||||
|
||||
各プロジェクトには、GitHub リポジトリを添付する **Resources** セクションがあります。添付すると、このプロジェクトのイシューに割り当てられた[エージェント](/agents)は、タスクを実行する際にそれらのリポジトリを読み書きできます — Multica がリポジトリ URL をコンテキストとして[デーモン](/daemon-runtimes)に渡します。
|
||||
|
||||
リソースはプロジェクト単位です。複数のプロジェクトが同じリポジトリを共有する場合は、それぞれに添付してください。
|
||||
|
||||
## プロジェクトを削除する
|
||||
|
||||
プロジェクトを削除しても **イシューは削除されません**。紐づくイシューは単に紐づけが解除され、ワークスペースのフラットなイシュー一覧に戻ります。これは意図された動作です — プロジェクトの枠組みが変わっても、プロジェクトの範囲として定められた作業が使い捨てになることはまれだからです。
|
||||
|
||||
<Callout type="info">
|
||||
作業も一緒に削除したい場合は、まずイシューをアーカイブまたは削除してから、プロジェクトを削除してください。
|
||||
</Callout>
|
||||
|
||||
## プロジェクトリード
|
||||
|
||||
リードは、プロジェクトに対して責任を負う人 — またはエージェント — です。これはアクセス制御ではなく、弱いシグナルです。誰がリードであるかに関わらず、ワークスペースのすべてのメンバーがプロジェクトを編集できます。プロジェクトのリードには次のものを設定できます。
|
||||
|
||||
- ワークスペースのメンバー(人間のチームメイト)
|
||||
- [エージェント](/agents) — プロジェクトの作業の大半をエージェントに委任する場合に便利です(例: 「週次のバグトリアージ」をトリアージ用エージェントがリードする)
|
||||
|
||||
## 次へ
|
||||
|
||||
- [イシュー](/issues) — プロジェクトの中に存在する作業の単位
|
||||
- [プロジェクトリードとしてのエージェント](/agents) — エージェントが適切な担当者となる場合
|
||||
- [Multica の仕組み](/how-multica-works) — より広い全体像
|
||||
@@ -1,127 +0,0 @@
|
||||
---
|
||||
title: AI コーディングツール対応表
|
||||
description: Multica は 12 個の AI コーディングツールをサポートしています。すべて同じインターフェースを実装していますが、機能の詳細は大きく異なります。
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
|
||||
Multica は **12 個の AI コーディングツール**を標準でサポートしています。これらはすべて同じインターフェース(キューへの投入、ディスパッチ、実行、結果の返却)を実装しているため、同じ Multica ボードからどれでも動かすことができます。**しかし機能の詳細は大きく異なります**: セッション再開が実際に動作するか、MCP をサポートするか、スキルファイルがどこに置かれるか、モデルをどう選択するか。このページがその完全な対応表です。
|
||||
|
||||
エージェントを作成するときにツールを選ぶ際のガイダンスは、[エージェントの作成と構成](/agents-create)を参照してください。
|
||||
|
||||
## 機能対応マトリクス
|
||||
|
||||
| ツール | ベンダー | セッション再開 | MCP | スキル注入パス | モデル選択 |
|
||||
|---|---|---|---|---|---|
|
||||
| **Antigravity** | Google | ✅ (`--conversation <id>`) | ❌ | `.agents/skills/` | Antigravity CLI 自体の内部で管理 |
|
||||
| **Claude Code** | Anthropic | ✅ | **✅(実際に使用する唯一のツール)** | `.claude/skills/` | 静的 + flag |
|
||||
| **Codex** | OpenAI | ⚠️ コードは存在するが到達不可 | ❌ | `$CODEX_HOME/skills/` | 静的 |
|
||||
| **Copilot** | GitHub | ✅ | ❌ | `.github/skills/` | 静的(アカウントの権限で決定) |
|
||||
| **Cursor** | Anysphere | ⚠️ コードは存在するが使用不可 | ❌ | `.cursor/skills/` | 動的探索 |
|
||||
| **Gemini** | Google | ❌ | ❌ | `.agent_context/skills/` | 静的 |
|
||||
| **Hermes** | Nous Research | ✅ | ❌ | `.agent_context/skills/`(フォールバック) | 動的探索 |
|
||||
| **Kimi** | Moonshot | ✅ | ❌ | `.kimi/skills/` | 動的探索 |
|
||||
| **Kiro CLI** | Amazon | ✅ | ❌ | `.kiro/skills/` | 動的探索 |
|
||||
| **OpenCode** | SST | ✅ | ❌ | `.opencode/skills/` | 動的探索 |
|
||||
| **OpenClaw** | オープンソース | ✅ | ❌ | `.agent_context/skills/`(フォールバック) | エージェントにバインドされ、タスクごとに切り替え不可 |
|
||||
| **Pi** | Inflection AI | ✅(セッションがファイルパス) | ❌ | `.pi/skills/` | 動的探索 |
|
||||
|
||||
## 各ツールの用途
|
||||
|
||||
### Antigravity
|
||||
|
||||
Google が提供します。CLI バイナリ名は `agy` です。Google の Antigravity サービスと連携し、Gemini ベースのデフォルトモデルが付属しています。**セッション再開が動作します** — `--conversation <id>` を通じて行われ、stdout が構造化されたイベントストリームではなくプレーンテキストであるため、デーモンが CLI のログファイルから conversation UUID をキャプチャします。`--model` flag はありません — モデル選択は Antigravity CLI の設定内にあるため、Multica はこのプロバイダーに対してエージェントごとのモデルピッカーを無効にします。スキルは `.agents/skills/` に配置されます(CLI が Gemini CLI のワークスペーススキルレイアウトをそのまま継承します — [Antigravity 移行ドキュメント](https://antigravity.google/docs/gcli-migration)を参照)。
|
||||
|
||||
### Claude Code
|
||||
|
||||
Anthropic が提供します。**新規ユーザーにとって第一の選択肢**であり、最も完成度の高い機能セットを備えています: セッション再開が実際に動作し、**11 個の中で MCP 構成を本当に読み取る唯一のツール**であり、`--max-turns` や `--append-system-prompt` のような細かな調整 flag をサポートします。Anthropic API キーが必要です。
|
||||
|
||||
### Codex
|
||||
|
||||
OpenAI が提供します。JSON-RPC 2.0 を使用し、ステートフルな能力がより強く、よりきめ細かい承認メカニズム(`exec_command` および `patch_apply` に対する手動承認)を備えています。**セッション再開のコードは存在しますが、現在は到達できません** — 再開が必要なら、Claude Code または ACP 系のいずれかを選んでください。
|
||||
|
||||
### Copilot
|
||||
|
||||
GitHub が提供します。モデルルーティングは GitHub アカウントの権限を経由します — ツールが直接モデルを選択するのではなく、GitHub がどのモデルを提供するかを決定します。`.github/skills/` にスキルを置くのは GitHub CLI のネイティブな探索メカニズムです。
|
||||
|
||||
### Cursor
|
||||
|
||||
Anysphere が提供し、Cursor エディターに対応する CLI です。**セッション再開のコードは存在しますが、実際には動作しません** — Cursor CLI のイベントストリームがセッション ID を返さないため、渡す再開値は常に無効です。再開が必要なら、別のものを選んでください。
|
||||
|
||||
### Gemini
|
||||
|
||||
Google が提供し、Gemini 2.5 および 3 シリーズをサポートします。**セッション再開も MCP もサポートしません** — 長いコンテキストの記憶が不要なワンショットタスクに適しています。
|
||||
|
||||
### Hermes
|
||||
|
||||
Nous Research が提供します。ACP プロトコルを使用します(Kimi とトランスポート層を共有します)。セッション再開が動作します。しかし**スキル注入パスは専用のものではなく汎用のフォールバック**(`.agent_context/skills/`)です — Hermes CLI 自体がこのパスを読み取らない場合、スキルが適用されないことがあります。テストで確認してください。
|
||||
|
||||
### Kimi
|
||||
|
||||
Moonshot が提供し、中国市場を対象としています。Hermes と ACP プロトコルを共有しますが、スキルパス `.kimi/skills/` は Kimi CLI のネイティブな探索メカニズムであり、Hermes のフォールバックとは異なります。
|
||||
|
||||
### Kiro CLI
|
||||
|
||||
Amazon が提供します。`kiro-cli acp` を通じて stdio 上で ACP を使用します。セッション再開は ACP `session/load` で動作し、モデル選択は `session/set_model` で動作し、スキルはプロジェクトレベルのネイティブ探索のために `.kiro/skills/` にコピーされます。
|
||||
|
||||
### OpenCode
|
||||
|
||||
SST が提供するオープンソースです。利用可能なモデルを動的に探索します(CLI の構成ファイルをスキャン)。セッション再開が動作します。**自分のモデルカタログをカスタマイズしたい、いじるのが好きなユーザーに適しています。**
|
||||
|
||||
### OpenClaw
|
||||
|
||||
オープンソースプロジェクトであり、CLI エージェントオーケストレーターです。**モデルはエージェント層にバインドされます**(`openclaw agents add --model`) — タスクごとに上書きできません。構成は厳格に制御されます: ユーザーは `--model` や `--system-prompt` を渡せず、エージェント登録時の構成が決定します。
|
||||
|
||||
### Pi
|
||||
|
||||
Inflection AI が提供し、ミニマルです。**セッション再開の方式が独特です** — セッション ID が文字列 ID ではなく、ディスク上のファイルパス(`~/.pi/...`)です。他のツールでは再開 id は CLI が返す文字列ですが、Pi では再開 id はセッションファイルそのものです。
|
||||
|
||||
## セッション再開: 実際にサポートするツール
|
||||
|
||||
セッション再開のメカニズムは[タスク](/tasks#can-a-task-continue-from-the-previous-context)で扱います。以下はツールごとの**正確な現在の状態**です。
|
||||
|
||||
| 状態 | ツール | 意味 |
|
||||
|---|---|---|
|
||||
| ✅ 実際に動作 | Antigravity, Claude Code, Copilot, Hermes, Kimi, Kiro CLI, OpenCode, OpenClaw, Pi | 再開 id を渡すと以前のコンテキストから続行します |
|
||||
| ⚠️ コードは存在するが到達不可 | Codex, Cursor | コードに再開パスがありますが、実際には到達しません(Codex は静かにフォールバックし、Cursor はセッション id を返しません) — **未サポートとみなしてください** |
|
||||
| ❌ なし | Gemini | CLI に再開メカニズムがありません |
|
||||
|
||||
**意思決定のために**: ワークフローでエージェントがタスク間でコンテキストを保持する必要がある場合(失敗時のリトライ、手動の再実行、対話的な反復)、✅ の行にあるツールだけを選んでください。
|
||||
|
||||
## MCP 構成: Claude Code だけが実際に読み取る
|
||||
|
||||
**12 個のツールのうち、`mcp_config` を実際に消費するのは Claude Code だけです**。残りの 11 個はこのフィールドを受け取りますが、**完全に無視します** — エラーも警告もなく、構成はただ効果を発揮しません。
|
||||
|
||||
<Callout type="warning">
|
||||
エージェント構成で `mcp_config` を設定しても、Claude Code 以外のツールを選んだ場合、MCP サーバーはそのエージェントに**何の効果**も及ぼしません。現在、MCP 連携は Claude Code のみをカバーしています。
|
||||
</Callout>
|
||||
|
||||
## スキルファイルが置かれる場所
|
||||
|
||||
各ツールは**それぞれ独自の**スキル探索パスを使用します。タスクが実行される前に、Multica デーモンがワークスペースのスキルファイルを対応するパスにコピーします。
|
||||
|
||||
| ツール | パス | ネイティブ探索か |
|
||||
|---|---|---|
|
||||
| Claude Code | `.claude/skills/` | ✅ ネイティブ |
|
||||
| Codex | `$CODEX_HOME/skills/` | ✅ ネイティブ |
|
||||
| Copilot | `.github/skills/` | ✅ ネイティブ |
|
||||
| Cursor | `.cursor/skills/` | ✅ ネイティブ |
|
||||
| Kimi | `.kimi/skills/` | ✅ ネイティブ |
|
||||
| Kiro CLI | `.kiro/skills/` | ✅ ネイティブ |
|
||||
| OpenCode | `.opencode/skills/` | ✅ ネイティブ |
|
||||
| Pi | `.pi/skills/` | ✅ ネイティブ |
|
||||
| Antigravity | `.agents/skills/` | ✅ ネイティブ(Gemini CLI のワークスペースレイアウトを継承 — [Antigravity ドキュメント](https://antigravity.google/docs/gcli-migration)を参照) |
|
||||
| Gemini | `.agent_context/skills/` | ⚠️ 汎用フォールバック |
|
||||
| Hermes | `.agent_context/skills/` | ⚠️ 汎用フォールバック |
|
||||
| OpenClaw | `.agent_context/skills/` | ⚠️ 汎用フォールバック |
|
||||
|
||||
フォールバックパスを使うツールが実際にこのディレクトリを読み取るかどうかは、そのツール自体のドキュメントによって異なり、保証されません。Gemini / Hermes / OpenClaw でスキルが適用されない場合は、まずこの点を確認してください。
|
||||
|
||||
スキルの作成と使用については、[スキル](/skills)を参照してください。
|
||||
|
||||
## 次へ
|
||||
|
||||
- [エージェントの作成と構成](/agents-create) — エージェントに使うツールを選ぶ
|
||||
- [タスク](/tasks) — タスクのライフサイクルとセッション再開のメカニズム
|
||||
- [デーモンとランタイム](/daemon-runtimes) — ツールが実行される場所と Multica への接続方法
|
||||
- [エージェントランタイムのインストール](/install-agent-runtime) — サポートされる 12 個のツールそれぞれのインストールと認証
|
||||
@@ -22,7 +22,7 @@ Multica는 **12개의 AI 코딩 도구**를 기본 지원합니다. 이들은
|
||||
| **Hermes** | Nous Research | ✅ | ✅ | `.agent_context/skills/` (fallback) | 동적 탐색 |
|
||||
| **Kimi** | Moonshot | ✅ | ✅ | `.kimi/skills/` | 동적 탐색 |
|
||||
| **Kiro CLI** | Amazon | ✅ | ✅ | `.kiro/skills/` | 동적 탐색 |
|
||||
| **OpenCode** | SST | ✅ | ✅ | `.opencode/skills/` | 동적 탐색 + variant |
|
||||
| **OpenCode** | SST | ✅ | ✅ | `.opencode/skills/` | 동적 탐색 |
|
||||
| **OpenClaw** | 오픈소스 | ✅ | ✅ | `.agent_context/skills/` (fallback) | 에이전트에 바인딩되어 작업마다 전환 불가 |
|
||||
| **Pi** | Inflection AI | ✅ (세션이 파일 경로) | ❌ | `.pi/skills/` | 동적 탐색 |
|
||||
|
||||
@@ -66,7 +66,7 @@ Amazon에서 제공합니다. `kiro-cli acp`를 통해 stdio 위에서 ACP를
|
||||
|
||||
### OpenCode
|
||||
|
||||
SST에서 제공하는 오픈소스입니다. 사용 가능한 모델과 모델 variant를 동적으로 탐색합니다(CLI의 구성 파일을 스캔). 세션 재개가 동작하고, 에이전트의 `mcp_config` 필드를 소비합니다. Multica는 `OPENCODE_CONFIG_CONTENT` 환경 변수를 통해 이를 인라인으로 주입하므로, 에이전트의 MCP 서버가 작업 디렉터리의 `opencode.json`(에이전트 또는 사용자가 소유하는 파일)을 건드리지 않고 OpenCode에 전달됩니다. 모델이 variant를 노출하면 Multica는 이를 에이전트 thinking selector로 표시하고 선택한 값을 `opencode run --variant`로 전달합니다. **자신의 모델 카탈로그를 커스터마이징하고 싶은, 만지작거리기 좋아하는 사용자에게 적합합니다.**
|
||||
SST에서 제공하는 오픈소스입니다. 사용 가능한 모델을 동적으로 탐색합니다(CLI의 구성 파일을 스캔). 세션 재개가 동작하고, 에이전트의 `mcp_config` 필드를 소비합니다. Multica는 `OPENCODE_CONFIG_CONTENT` 환경 변수를 통해 이를 인라인으로 주입하므로, 에이전트의 MCP 서버가 작업 디렉터리의 `opencode.json`(에이전트 또는 사용자가 소유하는 파일)을 건드리지 않고 OpenCode에 전달됩니다. **자신의 모델 카탈로그를 커스터마이징하고 싶은, 만지작거리기 좋아하는 사용자에게 적합합니다.**
|
||||
|
||||
### OpenClaw
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ For guidance on picking a tool when creating an agent, see [Creating and configu
|
||||
| **Hermes** | Nous Research | ✅ | ✅ | `.agent_context/skills/` (fallback) | Dynamic discovery |
|
||||
| **Kimi** | Moonshot | ✅ | ✅ | `.kimi/skills/` | Dynamic discovery |
|
||||
| **Kiro CLI** | Amazon | ✅ | ✅ | `.kiro/skills/` | Dynamic discovery |
|
||||
| **OpenCode** | SST | ✅ | ✅ | `.opencode/skills/` | Dynamic discovery + variants |
|
||||
| **OpenCode** | SST | ✅ | ✅ | `.opencode/skills/` | Dynamic discovery |
|
||||
| **OpenClaw** | Open source | ✅ | ✅ | `.agent_context/skills/` (fallback) | Bound to the agent, can't be switched per task |
|
||||
| **Pi** | Inflection AI | ✅ (session is a file path) | ❌ | `.pi/skills/` | Dynamic discovery |
|
||||
|
||||
@@ -66,7 +66,7 @@ From Amazon. Uses ACP over stdio via `kiro-cli acp`. Session resumption works th
|
||||
|
||||
### OpenCode
|
||||
|
||||
From SST, open source. Dynamically discovers available models and model variants (scans the CLI's configuration file). Session resumption works, and it consumes the agent's `mcp_config` field — Multica injects it inline through the `OPENCODE_CONFIG_CONTENT` environment variable, so the agent's MCP servers reach OpenCode without writing anything into the task workdir's `opencode.json` (the agent or the user keep ownership of that file). When a model exposes variants, Multica shows them as the agent thinking selector and passes the selected value through `opencode run --variant`. **Suitable for tinkerers who want to customize their model catalog.**
|
||||
From SST, open source. Dynamically discovers available models (scans the CLI's configuration file). Session resumption works, and it consumes the agent's `mcp_config` field — Multica injects it inline through the `OPENCODE_CONFIG_CONTENT` environment variable, so the agent's MCP servers reach OpenCode without writing anything into the task workdir's `opencode.json` (the agent or the user keep ownership of that file). **Suitable for tinkerers who want to customize their model catalog.**
|
||||
|
||||
### OpenClaw
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ Multica 内置支持 **12 款 AI 编程工具**。它们都实现了同一套接
|
||||
| **Hermes** | Nous Research | ✅ | ✅ | `.agent_context/skills/` (fallback)| 动态发现 |
|
||||
| **Kimi** | Moonshot | ✅ | ✅ | `.kimi/skills/` | 动态发现 |
|
||||
| **Kiro CLI** | Amazon | ✅ | ✅ | `.kiro/skills/` | 动态发现 |
|
||||
| **OpenCode** | SST | ✅ | ✅ | `.opencode/skills/` | 动态发现 + variant |
|
||||
| **OpenCode** | SST | ✅ | ✅ | `.opencode/skills/` | 动态发现 |
|
||||
| **OpenClaw** | 开源项目 | ✅ | ✅ | `.agent_context/skills/` (fallback)| 绑定在智能体上,不能在任务里切换 |
|
||||
| **Pi** | Inflection AI | ✅(session 为文件路径)| ❌ | `.pi/skills/` | 动态发现 |
|
||||
|
||||
@@ -66,7 +66,7 @@ Amazon 出品。通过 `kiro-cli acp` 使用 ACP stdio 协议。会话恢复走
|
||||
|
||||
### OpenCode
|
||||
|
||||
SST 出品,开源。动态发现可用模型和模型 variant(扫 CLI 的配置文件)。会话恢复真用,会消费智能体的 `mcp_config` 字段——Multica 通过 `OPENCODE_CONFIG_CONTENT` 环境变量内联注入,让智能体的 MCP server 直接到达 OpenCode,**不会去碰任务工作目录里的 `opencode.json`**(那个文件归智能体或用户所有)。当模型暴露 variant 时,Multica 会把它显示成智能体的思考强度选择,并通过 `opencode run --variant` 传给 OpenCode。**适合爱折腾、想自定义模型目录**的开发者。
|
||||
SST 出品,开源。动态发现可用模型(扫 CLI 的配置文件)。会话恢复真用,会消费智能体的 `mcp_config` 字段——Multica 通过 `OPENCODE_CONFIG_CONTENT` 环境变量内联注入,让智能体的 MCP server 直接到达 OpenCode,**不会去碰任务工作目录里的 `opencode.json`**(那个文件归智能体或用户所有)。**适合爱折腾、想自定义模型目录**的开发者。
|
||||
|
||||
### OpenClaw
|
||||
|
||||
|
||||
@@ -1,295 +0,0 @@
|
||||
---
|
||||
title: セルフホスティングのクイックスタート
|
||||
description: Docker で自分のサーバーやマシン上に Multica を実行します(Kubernetes では Helm が利用できます)。所要時間は約10分です。
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
|
||||
このページでは、Docker を使って Multica **サーバー**(バックエンド + フロントエンド + PostgreSQL)を自分のマシンやサーバー上で実行する手順を案内します。完了すると、[ワークスペース](/workspaces)、[イシュー](/issues)、[コメント](/comments)、[エージェント](/agents)の構成を含むデータが、完全にあなたの管理下に置かれます。
|
||||
|
||||
エージェントの **実行** は、依然としてローカルで動かす[デーモン](/daemon-runtimes)と、そのマシンにインストールされた [AI コーディングツール](/providers)に依存します — Cloud とまったく同じです。セルフホスティングはサーバー層を置き換えるだけで、実行層を置き換えるわけではありません。
|
||||
|
||||
## 前提条件
|
||||
|
||||
- **Docker** がインストールされ、`docker compose` を実行できること
|
||||
- **Git**(任意ですが、ソースを取得できるので推奨)
|
||||
- 常時稼働させられるマシン(ローカル / 内部ネットワーク / クラウドホストのいずれでも可)
|
||||
- **デーモンを実行するマシン** に AI コーディングツールが最低1つインストールされていること(サーバーを実行するマシンである必要はなく、開発用ノートパソコンでも構いません)
|
||||
|
||||
## 1. プロジェクトを取得してバックエンドを起動する
|
||||
|
||||
<Callout type="info">
|
||||
**すでに Kubernetes を使っていますか?** Docker をスキップして代わりに Helm チャートを使ってください — 下記の [Kubernetes デプロイ](#kubernetes-deployment-alternative)へ移動し、初回ログインのために [ステップ4](#4-first-login--create-a-workspace)に戻ってきてください。
|
||||
</Callout>
|
||||
|
||||
```bash
|
||||
git clone https://github.com/multica-ai/multica.git
|
||||
cd multica
|
||||
make selfhost
|
||||
```
|
||||
|
||||
`make selfhost` は次のことを行います。
|
||||
|
||||
1. `.env` がなければ `.env.example` から生成し、**ランダムな JWT_SECRET** を併せて作成します
|
||||
2. 公式の Docker イメージ(PostgreSQL、Multica backend、Multica frontend)を取得します
|
||||
3. `docker-compose.selfhost.yml` を使ってすべてのサービスを起動します
|
||||
4. バックエンドの `/health` エンドポイントが準備できるまで待機します
|
||||
|
||||
起動後の継続的なプロダクションプローブには、データベースや migration の問題でチェックが失敗するようにしたい場合は `/readyz` を使ってください。
|
||||
|
||||
バックエンドのコンテナは起動時に **データベースの migration を自動で実行します**(`docker/entrypoint.sh` がサーバー起動前に `./migrate up` を実行)— バックエンドのログで migration の出力を確認できます。バージョンアップグレードも同じ方法で処理されます。
|
||||
|
||||
<Callout type="info">
|
||||
**イメージがまだ公開されていませんか?** `make selfhost` がイメージを取得できない場合、まだリリースされていないバージョンタグにいる可能性があります。安定リリースに切り替えるか、ソースからビルドしてください: `make selfhost-build`。
|
||||
</Callout>
|
||||
|
||||
起動すると次のようになります。
|
||||
|
||||
- **フロントエンド**: [http://localhost:3000](http://localhost:3000)
|
||||
- **バックエンド**: [http://localhost:8080](http://localhost:8080)
|
||||
|
||||
<Callout type="info">
|
||||
**ポートは `127.0.0.1` でのみ待ち受けます。** `docker-compose.selfhost.yml` は公開されるすべてのポートを loopback にバインドします — `ss -tlnp` には `0.0.0.0:8080` は表示されず、設計上、他のマシンからはサービスにアクセスできません。デフォルトの `JWT_SECRET` と Postgres の認証情報は、公開インターネット上に置いては絶対にいけません。マシン間アクセスが必要な場合は、TLS を終端するリバースプロキシをスタックの前に置いてください — [ステップ5b — マシン間: リバースプロキシを前に置く](#5b-cross-machine-front-with-a-reverse-proxy)を参照してください。
|
||||
</Callout>
|
||||
|
||||
## 2. 重要: プロダクションの安全設定を維持する
|
||||
|
||||
<Callout type="warning">
|
||||
**`docker-compose.selfhost.yml` はデフォルトで `APP_ENV` を `production` に設定し**、`MULTICA_DEV_VERIFICATION_CODE` を空のままにするため、公開インスタンスには固定コードがありません。
|
||||
|
||||
`MULTICA_DEV_VERIFICATION_CODE` はローカルまたは非公開のテスト自動化でのみ設定してください。`APP_ENV` が non-production のときに固定コードが有効になっていると、コードをリクエストできる誰もがその固定値でサインインできてしまいます。[認証設定 → 固定のローカルテストコード](/auth-setup#fixed-local-testing-codes)を参照してください。
|
||||
|
||||
公開デプロイの前には、`.env` に `APP_ENV=production` が設定され、`MULTICA_DEV_VERIFICATION_CODE` が空であることを必ず確認してください。
|
||||
</Callout>
|
||||
|
||||
## 3. メールサービスを構成する(任意ですが推奨)
|
||||
|
||||
メールを構成しないと、ユーザーはメールで認証コードを受け取れず、サーバーは生成したコードを代わりに stdout に出力します。
|
||||
|
||||
2種類の配信バックエンドがサポートされています — ネットワークに合うものを選んでください。
|
||||
|
||||
**オプション A — Resend(クラウド / 公開インターネットのデプロイ):**
|
||||
|
||||
1. [Resend](https://resend.com/) にサインアップして API key を取得します
|
||||
2. 自分が管理する送信用ドメインを認証します
|
||||
3. `.env` に次を設定します。
|
||||
|
||||
```bash
|
||||
RESEND_API_KEY=re_xxxxxxxxxxxx
|
||||
RESEND_FROM_EMAIL=noreply@yourdomain.com
|
||||
```
|
||||
|
||||
**オプション B — SMTP relay(内部ネットワーク / オンプレミス):**
|
||||
|
||||
デプロイ環境が `api.resend.com` に到達できない場合や、すでに内部メールリレー(Microsoft Exchange、Postfix、オンプレミスの SendGrid など)がある場合に使ってください。両方が設定されている場合は `SMTP_HOST` が Resend より優先されるため、認証メールと招待メールは内部リレーにとどまります。STARTTLS は広告されると自動的にアップグレードされます。ポート `465`(SMTPS / 暗黙的 TLS)は接続直後の TLS ハンドシェイクを自動的に有効化し、`SMTP_TLS=implicit`(別名: `smtps`、`ssl`)は非標準の SMTPS ポートで強制的に有効化します。
|
||||
|
||||
**匿名 Exchange 内部リレー(ポート 25)** — ホストが IP で信頼され、認証情報なしで送信する場合:
|
||||
|
||||
```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、STARTTLS)** — リレーがサービスアカウントを必要とし、STARTTLS が広告されると自動的にアップグレードされる場合:
|
||||
|
||||
```bash
|
||||
SMTP_HOST=smtp.internal.example.com
|
||||
SMTP_PORT=587
|
||||
SMTP_USERNAME=multica
|
||||
SMTP_PASSWORD=...
|
||||
SMTP_TLS_INSECURE=false # set true only for private CA / self-signed
|
||||
RESEND_FROM_EMAIL=noreply@yourdomain.com
|
||||
```
|
||||
|
||||
**暗黙的 TLS / SMTPS(ポート 465)** — STARTTLS を広告しないアリババクラウド / テンセントの法人メールなどのプロバイダー向け。ポート `465` は暗黙的 TLS を自動的に有効化するため、ここでは `SMTP_TLS` は省略可能です:
|
||||
|
||||
```bash
|
||||
SMTP_HOST=smtp.qiye.aliyun.com
|
||||
SMTP_PORT=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`)** が公開 IP からのデフォルトの `localhost` 挨拶を拒否する場合は、`SMTP_EHLO_NAME` を relay が期待する FQDN に設定してください — そうしないと接続が切断され、後続のコマンドで不明瞭な `EOF` として表面化します。デフォルトはコンテナのホスト名で、これは通常は有効な 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
|
||||
```
|
||||
|
||||
その後、再起動します: `docker compose -f docker-compose.selfhost.yml restart backend`。再起動時、バックエンドはどのプロバイダーを選んだかを出力します(`EmailService: SMTP relay …` / `Resend API` / `DEV mode`)— 認証情報は決してログに残らないため、この行はヘルプを求めるときに共有しても安全です。
|
||||
|
||||
追加の認証構成(OAuth、サインアップの許可リスト)と SMTP 変数の完全なリファレンスは、[認証設定](/auth-setup)と[環境変数 → メール](/environment-variables#email-configuration)を参照してください。
|
||||
|
||||
## 4. 初回ログイン + ワークスペースの作成
|
||||
|
||||
[http://localhost:3000](http://localhost:3000) を開きます。
|
||||
|
||||
- メールアドレスを入力します
|
||||
- 構成したメールバックエンド(Resend または SMTP relay)から認証コードを受け取ります。どちらも構成していない場合は、サーバーコンテナの stdout からコピーしてください — `[DEV] Verification code` の行を探します
|
||||
- non-production の非公開インスタンスで `MULTICA_DEV_VERIFICATION_CODE=888888` を明示的に設定した場合を除き、`888888` を使わないでください
|
||||
- ログインして最初のワークスペースを作成します
|
||||
|
||||
## 5. CLI を自分のサーバーに向ける
|
||||
|
||||
CLI のインストールは [Cloud クイックスタート → 2. CLI をインストールする](/cloud-quickstart#2-install-the-multica-cli) と同じです — Homebrew / スクリプト / PowerShell のいずれかを選んでください。
|
||||
|
||||
### 5a. 同じマシン
|
||||
|
||||
CLI とサーバーが同じホストで動作している場合、デフォルト値ですでに動作します。
|
||||
|
||||
```bash
|
||||
multica setup self-host
|
||||
```
|
||||
|
||||
これにより CLI が `http://localhost:8080`(バックエンド)と `http://localhost:3000`(フロントエンド)を指すようになり、ブラウザログインを案内し、PAT をローカルに保存して、**デーモンを自動的に起動します**。
|
||||
|
||||
### 5b. マシン間: リバースプロキシを前に置く
|
||||
|
||||
compose スタックは `127.0.0.1` でのみ待ち受けるため、別のマシンにあるデーモンは `http://<server-ip>:8080` に直接接続できません — そして、そうなることを望むべきでもありません。さもなければデフォルトの `JWT_SECRET` が公開インターネットから到達可能になってしまうからです。TLS を終端し、`127.0.0.1:8080`(バックエンド)と `127.0.0.1:3000`(フロントエンド)へ転送するリバースプロキシをサーバーに置き、CLI を公開 HTTPS URL に向けてください。
|
||||
|
||||
```bash
|
||||
multica setup self-host \
|
||||
--server-url https://<your-domain> \
|
||||
--app-url https://<your-domain>
|
||||
```
|
||||
|
||||
単一のホスト名でフロントエンドとバックエンドの両方を前段に置く(デーモンと Web アプリの両方に必要な WebSocket サポートを含む)最小限の Caddyfile は次のとおりです。
|
||||
|
||||
```nginx
|
||||
multica.example.com {
|
||||
# WebSocket route — must come before the catch-all
|
||||
@ws path /ws /ws/*
|
||||
handle @ws {
|
||||
reverse_proxy 127.0.0.1:8080 {
|
||||
flush_interval -1
|
||||
}
|
||||
}
|
||||
|
||||
# Backend API
|
||||
handle /api/* {
|
||||
reverse_proxy 127.0.0.1:8080
|
||||
}
|
||||
|
||||
# Everything else → frontend
|
||||
reverse_proxy 127.0.0.1:3000
|
||||
}
|
||||
```
|
||||
|
||||
プロキシを立ち上げたら、サーバーの `.env` に `FRONTEND_ORIGIN=https://multica.example.com` を設定してバックエンドを再起動してください — そうしないと WebSocket の origin チェックがブラウザを拒否します([トラブルシューティング → WebSocket が接続できない](/troubleshooting#websocket-cant-connect))。
|
||||
|
||||
[Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) も堅実な選択肢です — ホストにポートを一切公開せずに TLS と公開ホスト名を提供してくれます。Nginx で同等に構成する方法(`app.` / `api.` を別々のホスト名に分離、WebSocket 用の `proxy_set_header Upgrade`)も同じくらいうまく動作します。重要な要件は、TLS の終端と `/ws` での `Upgrade` ヘッダーの転送です。
|
||||
|
||||
## 6. エージェントの作成 + 最初のタスクの割り当て
|
||||
|
||||
Cloud と同じ流れです — [Cloud クイックスタート → ステップ5-6](/cloud-quickstart#5-create-an-agent)を参照してください。
|
||||
|
||||
## 7. 使用量ロールアップのスケジューリング(使用量ダッシュボードに必須)
|
||||
|
||||
<Callout type="warning">
|
||||
使用量 / ランタイムのダッシュボードは、`rollup_task_usage_hourly()` が埋める派生テーブル `task_usage_hourly` からデータを読み取ります。バンドルされた `pgvector/pgvector:pg17` の Postgres イメージには **`pg_cron` が含まれておらず**、バックエンドもロールアップをインプロセスで実行しません。`rollup_task_usage_hourly()` をスケジューリングするものが何もないと、生の `task_usage` 行は届き続けるのに、ダッシュボードは永遠にゼロのままになります。
|
||||
</Callout>
|
||||
|
||||
サポートされているオプションのいずれか1つを選んでください — 1つあれば十分です。
|
||||
|
||||
**オプション A — 外部 cron / systemd-timer(最もシンプル)。** 任意の帯域外スケジューラから5分ごとにロールアップを実行します。冪等でウォーターマーク駆動なので、取りこぼしたティックは追いつきます。
|
||||
|
||||
```bash
|
||||
# /etc/cron.d/multica-rollup — every 5 minutes
|
||||
*/5 * * * * root docker compose -f /path/to/multica/docker-compose.selfhost.yml \
|
||||
exec -T postgres psql -U multica -d multica \
|
||||
-c "SELECT rollup_task_usage_hourly();" >/dev/null
|
||||
```
|
||||
|
||||
**オプション B — Postgres を `pg_cron` を同梱したイメージに置き換える。** `docker-compose.selfhost.yml` の `pgvector/pgvector:pg17` を、`pgvector` と `pg_cron` の両方を備えたイメージ(`supabase/postgres`、またはカスタムビルド)に置き換え、`shared_preload_libraries=pg_cron` を設定して再起動してから、ジョブを一度登録します。
|
||||
|
||||
```sql
|
||||
CREATE EXTENSION IF NOT EXISTS pg_cron;
|
||||
SELECT cron.schedule(
|
||||
'rollup_task_usage_hourly',
|
||||
'*/5 * * * *',
|
||||
$$SELECT rollup_task_usage_hourly()$$
|
||||
);
|
||||
```
|
||||
|
||||
**オプション C — まず履歴をバックフィルする(アップグレード経路)。** `v0.3.4 → v0.3.5+` へアップグレード中で、既存の `task_usage` 行がある場合、migration `103` は hourly テーブルがシードされるまで `refusing to drop legacy daily rollups: ...` とともに `migrate up` を中断します。バンドルされたバックフィルを一度実行してから、オプション A または B を設定してください。
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.selfhost.yml exec backend \
|
||||
./backfill_task_usage_hourly --sleep-between-slices=2s
|
||||
```
|
||||
|
||||
`--sleep-between-slices=2s` は、忙しい DB での読み取り負荷を調整します。完了後、バックエンドのコンテナを再起動すると(起動時に migration が実行されます)アップグレードが完了します。
|
||||
|
||||
完全なリファレンス — Kubernetes の `CronJob` テンプレートとアップグレード順序を含む — は、リポジトリの [`SELF_HOSTING_ADVANCED.md → Usage Dashboard Rollup`](https://github.com/multica-ai/multica/blob/main/SELF_HOSTING_ADVANCED.md#usage-dashboard-rollup) にあります。
|
||||
|
||||
## Kubernetes デプロイ(代替手段)
|
||||
|
||||
すでに Kubernetes クラスターを運用している場合、リポジトリには `deploy/helm/multica/` に Helm チャートも同梱されています。k8s 用の `make selfhost` に相当します — 同じバックエンドイメージ、フロントエンドイメージ、`pgvector/pgvector:pg17` の Postgres を Deployment / Service / Ingress としてパッケージングし、`values.yaml` からレンダリングされた1つの `ConfigMap` を併せて提供します。k3s + Traefik + `local-path` を基準に作成されており、Ingress コントローラーとデフォルトの `ReadWriteOnce` StorageClass があるあらゆるクラスターで動作するはずです。
|
||||
|
||||
このチャートは **シークレット値をテンプレート化しません**。`multica-secrets` という名前の Secret を名前で参照するため、実際の JWT / DB / Resend / Google キーが git や `values.yaml` に置かれる必要はまったくありません。ネームスペースと Secret を kubectl で一度作成してください。
|
||||
|
||||
```bash
|
||||
kubectl create namespace multica
|
||||
|
||||
kubectl -n multica create secret generic multica-secrets \
|
||||
--from-literal=JWT_SECRET="$(openssl rand -hex 32)" \
|
||||
--from-literal=POSTGRES_PASSWORD="$(openssl rand -hex 16)" \
|
||||
--from-literal=RESEND_API_KEY="" \
|
||||
--from-literal=GOOGLE_CLIENT_SECRET="" \
|
||||
--from-literal=CLOUDFRONT_PRIVATE_KEY="" \
|
||||
--from-literal=MULTICA_DEV_VERIFICATION_CODE=""
|
||||
```
|
||||
|
||||
その後、チャートをインストールします。
|
||||
|
||||
```bash
|
||||
git clone https://github.com/multica-ai/multica.git
|
||||
cd multica
|
||||
helm install multica deploy/helm/multica -n multica
|
||||
```
|
||||
|
||||
デフォルト値はホスト名 `multica.dev.lan`(web)と `api.multica.dev.lan`(バックエンド)を想定しています。これらを `/etc/hosts`(またはローカル DNS)に追加し、Ingress に到達可能な任意のノード IP を指すようにしてください。別のホスト名を使うには、`deploy/helm/multica/values.yaml` をコピーして `ingress.frontend.host` / `ingress.backend.host` と、それに対応する `backend.config.appUrl` / `frontendOrigin` / `localUploadBaseUrl` / `googleRedirectUri` を編集し、`-f my-values.yaml` でインストールしてください。
|
||||
|
||||
コールドクラスターでは、バックエンドが Postgres を待ち、migration を実行する間、数分間 `Running` 状態だが `Ready` ではないことがあります — startupProbe がこれを吸収するため、ポッドは再起動されないはずです。`Ready` になったら:
|
||||
|
||||
```bash
|
||||
curl -H "Host: api.multica.dev.lan" http://<ingress-ip>/healthz
|
||||
# {"status":"ok","checks":{"db":"ok","migrations":"ok"}}
|
||||
```
|
||||
|
||||
その後 `http://multica.dev.lan` を開き、上記の [ステップ4 — 初回ログイン](#4-first-login--create-a-workspace)から続けてください。CLI を Ingress のホスト名に向けます。
|
||||
|
||||
```bash
|
||||
multica setup self-host \
|
||||
--server-url http://api.multica.dev.lan \
|
||||
--app-url http://multica.dev.lan
|
||||
```
|
||||
|
||||
チャートを変更せずに最新のイメージだけを取得するには、`kubectl -n multica rollout restart deploy/multica-backend deploy/multica-frontend` を実行してください。特定の Multica リリースに固定するには、values ファイルで `images.backend.tag` / `images.frontend.tag` を設定して `helm upgrade` を実行してください。`helm -n multica uninstall multica` はワークロードを削除しますが、PVC と Secret は保持します。`kubectl delete namespace multica` はすべてを消去します。
|
||||
|
||||
完全なリファレンス — 3つのログインモード、web イメージにビルド時に焼き込まれた `REMOTE_API_URL` に対する `backend` ExternalName の回避策、リソース制限、TLS — は、リポジトリの [`SELF_HOSTING.md`](https://github.com/multica-ai/multica/blob/main/SELF_HOSTING.md#kubernetes-deployment-alternative) にあります。
|
||||
|
||||
## よくある問題
|
||||
|
||||
- **バックエンドが起動しない**: `docker compose -f docker-compose.selfhost.yml logs backend` でコンテナのログを確認してください。たいていは `.env` の不正な `DATABASE_URL` または `JWT_SECRET` が原因です
|
||||
- **認証コードが届かない**: メールバックエンドが構成されていない場合(Resend も SMTP もない)→ `docker compose logs backend` で `[DEV] Verification code` を探してください
|
||||
- **WebSocket が接続できない**: 公開デプロイでは、`FRONTEND_ORIGIN` を実際のフロントエンドのドメインに必ず設定する必要があります。[トラブルシューティング → WebSocket が接続できない](/troubleshooting#websocket-wont-connect)を参照してください
|
||||
- **使用量 / ランタイムのダッシュボードがゼロのまま**: `rollup_task_usage_hourly()` がスケジューリングされていません — 上記の [ステップ7](#7-schedule-the-usage-rollup-required-for-the-usage-dashboard)と[トラブルシューティング → 使用量ダッシュボードがゼロと表示される](/troubleshooting#usage-dashboard-stays-at-zero)を参照してください
|
||||
- **`migrate up` が `refusing to drop legacy daily rollups` で失敗する**: `v0.3.4 → v0.3.5+` のアップグレード経路ガードです。まず `backfill_task_usage_hourly` を実行してください — [ステップ7 → オプション C](#7-schedule-the-usage-rollup-required-for-the-usage-dashboard)を参照してください
|
||||
|
||||
## 次のステップ
|
||||
|
||||
- [環境変数](/environment-variables) — 完全な env リファレンス
|
||||
- [認証設定](/auth-setup) — Resend / OAuth / サインアップ許可リストの詳細
|
||||
- [GitHub 連携](/github-integration) — GitHub App を接続して、PR がイシューに自動でリンクされ、マージするとイシューが閉じられるように設定する
|
||||
- [トラブルシューティング](/troubleshooting) — うまくいかないときはここから始めてください
|
||||
- [デスクトップアプリ](/desktop-app) — `~/.multica/desktop.json` を通じた任意のデスクトップ設定。Web フロントエンド + CLI が依然として最速のセルフホスティング経路です
|
||||
@@ -81,7 +81,7 @@ make selfhost
|
||||
|
||||
**옵션 B — SMTP relay(내부 네트워크 / 온프레미스):**
|
||||
|
||||
배포 환경이 `api.resend.com`에 접근할 수 없거나, 이미 내부 메일 릴레이(Microsoft Exchange, Postfix, 온프레미스 SendGrid 등)가 있는 경우에 사용하세요. 둘 다 설정된 경우 `SMTP_HOST`가 Resend보다 우선하므로, 인증 및 초대 메일이 내부 릴레이에 머무릅니다. STARTTLS는 광고될 때 자동으로 업그레이드됩니다. `465` 포트(SMTPS / 암묵적 TLS)는 연결 직후의 TLS 핸드셰이크를 자동으로 활성화하며, `SMTP_TLS=implicit`(별칭: `smtps`, `ssl`)는 비표준 SMTPS 포트에서 강제로 활성화합니다.
|
||||
배포 환경이 `api.resend.com`에 접근할 수 없거나, 이미 내부 메일 릴레이(Microsoft Exchange, Postfix, 온프레미스 SendGrid 등)가 있는 경우에 사용하세요. 둘 다 설정된 경우 `SMTP_HOST`가 Resend보다 우선하므로, 인증 및 초대 메일이 내부 릴레이에 머무릅니다. 465 포트(SMTPS / 암묵적 TLS)는 현재 지원하지 않습니다 — 25 또는 587을 사용하세요.
|
||||
|
||||
**익명 Exchange 내부 릴레이(포트 25)** — 호스트가 IP로 신뢰되며 자격 증명 없이 제출하는 경우:
|
||||
|
||||
@@ -105,26 +105,6 @@ SMTP_TLS_INSECURE=false # 비공개 CA / 자체 서명 인증서일 때
|
||||
RESEND_FROM_EMAIL=noreply@yourdomain.com
|
||||
```
|
||||
|
||||
**암묵적 TLS / SMTPS(포트 465)** — STARTTLS를 광고하지 않는 알리바바 클라우드 / 텐센트 기업 메일 같은 제공자용. 포트 `465`는 암묵적 TLS를 자동으로 활성화하므로, 여기서 `SMTP_TLS`는 생략할 수 있습니다:
|
||||
|
||||
```bash
|
||||
SMTP_HOST=smtp.qiye.aliyun.com
|
||||
SMTP_PORT=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
|
||||
```
|
||||
|
||||
공개 IP에서 보내는 기본 `localhost` greeting을 거부하는 **엄격한 공개 relay(예: Google Workspace `smtp-relay.gmail.com`)** 의 경우, relay가 기대하는 FQDN으로 `SMTP_EHLO_NAME`을 설정하세요 — 그렇지 않으면 연결이 끊기고, 이는 이후 명령에서 불투명한 `EOF`로 나타납니다. 기본값은 컨테이너 호스트명이며, 보통 유효한 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
|
||||
```
|
||||
|
||||
그런 다음 재시작합니다: `docker compose -f docker-compose.selfhost.yml restart backend`. 재시작 시 백엔드는 어떤 제공자를 선택했는지 출력합니다(`EmailService: SMTP relay …` / `Resend API` / `DEV mode`) — 자격 증명은 절대 로그에 남지 않으므로, 이 줄은 도움을 요청할 때 공유해도 안전합니다.
|
||||
|
||||
추가 인증 구성(OAuth, 가입 허용 목록)과 전체 SMTP 변수 레퍼런스는 [인증 설정](/auth-setup)과 [환경 변수 → 이메일](/environment-variables#email-configuration)을 참고하세요.
|
||||
|
||||
@@ -117,15 +117,6 @@ SMTP_TLS=implicit # optional on 465; required on a non-standard SMT
|
||||
RESEND_FROM_EMAIL=noreply@yourdomain.com
|
||||
```
|
||||
|
||||
For **strict public relays (e.g. Google Workspace `smtp-relay.gmail.com`)** that reject the default `localhost` greeting from a public IP, set `SMTP_EHLO_NAME` to the FQDN the relay expects — otherwise the connection is dropped and surfaces as an opaque `EOF` on a later command. It defaults to the container hostname, which 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
|
||||
```
|
||||
|
||||
Then restart: `docker compose -f docker-compose.selfhost.yml restart backend`. On restart, the backend prints which provider it picked and the negotiated TLS mode (`EmailService: SMTP relay <host>:<port> (starttls|implicit-tls) from=…` / `Resend API` / `DEV mode`) — credentials are never logged, so this line is safe to share when asking for help.
|
||||
|
||||
For more auth configuration (OAuth, signup allowlist) and the full SMTP variable reference, see [Auth setup](/auth-setup) and [Environment variables → Email](/environment-variables#email-configuration).
|
||||
|
||||
@@ -116,15 +116,6 @@ SMTP_TLS=implicit # 465 上可省略;非标准 SMTPS 端口上必
|
||||
RESEND_FROM_EMAIL=noreply@yourdomain.com
|
||||
```
|
||||
|
||||
对于**拒绝来自公网 IP 的默认 `localhost` 问候的严格公网 relay(例如 Google Workspace `smtp-relay.gmail.com`)**,把 `SMTP_EHLO_NAME` 设成 relay 期望的 FQDN——否则连接会被直接断开,并在后续某条命令上表现为一个不知所云的 `EOF`。它默认取容器主机名,而后者通常不是合法的 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
|
||||
```
|
||||
|
||||
之后重启:`docker compose -f docker-compose.selfhost.yml restart backend`。重启时 backend 会打印当前选择的 provider 和协商出的 TLS 模式(`EmailService: SMTP relay <host>:<port> (starttls|implicit-tls) from=…` / `Resend API` / `DEV mode`),密码不会被记录,所以这行截图给同事是安全的。
|
||||
|
||||
更多 auth 配置(OAuth、注册白名单)以及完整的 SMTP 变量说明见 [登录与注册配置](/auth-setup) 和 [环境变量](/environment-variables)。
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
---
|
||||
title: スキル
|
||||
description: エージェントに「ナレッジパック」を取り付けましょう — Anthropic Agent Skills オープン標準と互換性があります。
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
|
||||
スキルは[エージェント](/agents)のための**ナレッジパック**です — `SKILL.md` 1 つと、任意の補助ファイル(スクリプト、設定、参照テンプレート)で構成され、エージェントに「この種のタスクに出くわしたら、こう考えてこう動け」と伝えます。Multica は [Anthropic Agent Skills](https://agentskills.io) オープン標準を採用しているため、Anthropic 公式リポジトリ、ClawHub、skills.sh などから取得した、標準に準拠したどのスキルでも直接インポートできます。
|
||||
|
||||
## ワークスペーススキルとローカルスキル
|
||||
|
||||
Multica は 2 つのスキルソースに対応しています。
|
||||
|
||||
- **ワークスペーススキル** — Multica のクラウドに保存されます。エージェントに取り付けると、タスク実行時にあなたのデーモンへ同期されます。これが**チーム全体でスキルを共有する標準的な方法**です。
|
||||
- **ローカルスキル** — あなたのマシン上のディレクトリに存在します(各 AI コーディングツールには慣例的なデフォルトパスがあります。例: Claude Code の `~/.claude/skills/`)。あなたが要求すると、[デーモン](/daemon-runtimes)がマシンをスキャンし、どれをワークスペースに取り込むかを手動で選びます。
|
||||
|
||||
ほとんどの場合は**ワークスペーススキル**が望ましいでしょう。一度インポートすれば、すべてのチームメイトのエージェントが使えるからです。ローカルスキルは、まずローカルでテストしたい場合や、内容に機密性の高いローカル資料が含まれる場合に適しています。
|
||||
|
||||
## スキルをインポートする
|
||||
|
||||
ワークスペーススキルは 4 つのソースから取得します。
|
||||
|
||||
- **新規作成** — UI で `SKILL.md` と関連ファイルを直接作成します
|
||||
- **GitHub から** — リポジトリ URL を貼り付けると(例: `https://github.com/owner/repo/tree/main/skills/my-skill`)、Multica がそのディレクトリの `SKILL.md` とすべてのファイルを取得します
|
||||
- **ClawHub から** — [ClawHub](https://clawhub.io) 公開マーケットプレイスで検索し、バージョンを選択してインポートします
|
||||
- **ローカルから** — デーモンがあなたのマシンのスキルディレクトリをスキャンし、ワークスペースに取り込むものを選びます
|
||||
|
||||
個々のファイルもスキルパック全体も容量の上限があります(GitHub からインポートする際の単一ファイルの上限は約 1 MB)。正確なルールはインポートダイアログに表示され、上限を超えるとエラーが返されます。
|
||||
|
||||
## エージェントに取り付ける
|
||||
|
||||
インポートしたスキルは、**特定のエージェントに取り付けて**初めて効果を発揮します。1 つのエージェントに複数のスキルを取り付けられ、1 つのスキルを複数のエージェントに取り付けることもできます。
|
||||
|
||||
取り付けた後は、エージェントが次にタスクを開始するときにスキルを取り込みます — 各 AI コーディングツールには固有のスキル探索パスがあり(Claude Code は `.claude/skills/`、Cursor は `.cursor/skills/`、Antigravity は `.agents/skills/` などを使用)、Multica が正しい場所にファイルを自動的に配置します。**ただし、3 つのツール(Gemini、Hermes、OpenClaw)は現在、汎用のフォールバックパス `.agent_context/skills/` を使用しており、これらのツールが実際にそのパスからスキルを読み込むかどうかはツール自体に依存します。** 完全なパスマッピングと、ネイティブ探索とフォールバックの区別は [AI コーディングツール比較 → スキルファイルが置かれる場所](/providers#where-skill-files-go)にあります。
|
||||
|
||||
スキルの内容を編集した後は、**新しく作成されたタスクだけが新しいバージョンを取り込みます** — すでに実行中のタスクは以前のスキルをそのまま使い続けます。
|
||||
|
||||
## サードパーティスキルの安全性
|
||||
|
||||
GitHub や ClawHub からインポートしたスキルには、スクリプトや実行可能なコンテンツが含まれることがあります。Multica 自体はそれらを**署名も、監査も、サンドボックス化もしません** — スキルの内容はそのまま対応する AI コーディングツールに渡され、ツールがそれを実行可能なものとして扱うかどうかはツール次第です。
|
||||
|
||||
<Callout type="warning">
|
||||
**サードパーティスキルをインポートする前に、`SKILL.md` と同梱されるすべてのファイルを確認してください。**
|
||||
|
||||
2026 年 2 月に発生した「ClawHavoc」事件では、人気のスキルパックに仕込まれた悪意ある指示が、影響を受けたユーザーの API キーを盗み出しました。ClawHub はその後 VirusTotal スキャンを追加しましたが、**自動スキャンはあなた自身の確認の代わりにはなりません。**
|
||||
|
||||
**信頼できるソースからのみインポートしてください。** 機密データが関わるプロジェクトでは、自分で書いたローカルスキルだけを使うことを検討してください。
|
||||
</Callout>
|
||||
|
||||
## スキルと MCP
|
||||
|
||||
どちらもエージェントができることを拡張しますが、方向が異なります。
|
||||
|
||||
- **スキル** = 構造化された**ナレッジパック**(静的なコンテンツ + 指示)。エージェントはスキルを読んで「問題 X を見たら、こう考えてこう行動する」を学びます。
|
||||
- **MCP**(Model Context Protocol)= **ツールチャネル**。エージェントは MCP を使って外部サービス(データベース、ファイルシステム、サードパーティ API)に接続し、それらを**呼び出します**。
|
||||
|
||||
この 2 つは相互補完的です。現在の Multica では、MCP のサポートを**実際に使うのは Claude Code だけ**です — 他のツールは MCP 設定を受け取りはしますが、実際には使いません。MCP 専用のセクションは今後のリリースで追加される予定です。
|
||||
|
||||
---
|
||||
|
||||
これで、エージェントとは何か、どう作るか、スキルをどう取り付けるかが分かりました。次の問いはこれです。**エージェントは実際にどこで実行され、なぜ自分のエージェントはときどき止まってしまうのか?** 次の章では実行アーキテクチャ — デーモン、ランタイム、そしてタスクがどう連携するか — を扱います。
|
||||
|
||||
## 次のステップ
|
||||
|
||||
- [デーモンとランタイム](/daemon-runtimes) — エージェントが実際に実行される場所、そしてオンラインとオフラインの見分け方
|
||||
- [タスクの実行](/tasks) — 1 回の「エージェント作業セッション」の全ライフサイクル
|
||||
- [AI コーディングツール比較](/providers) — 12 ツール全体の比較(各ツールのスキル注入パスを含む)
|
||||
@@ -1,136 +0,0 @@
|
||||
---
|
||||
title: スクワッド
|
||||
description: "スクワッドは、1 人の指定されたリーダーエージェントが率いるエージェント(そして任意で人間のメンバー)のグループです。スクワッドにイシューを割り当てると、リーダーが誰が引き受けるかを決定します。"
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
|
||||
スクワッドは、1 人の指定された**リーダーエージェント**を擁する、**[エージェント](/agents)と人間の[メンバー](/members-roles)の名前付きグループ**です。スクワッド自体が一級の担当者です。どの **Assignee** ピッカーからでもスクワッドを選ぶと、リーダーがトリガーを受け取り、イシューを読んだ上で、その作業に最も適したスクワッドメンバーを `@` でメンションします。スクワッドを使えば、専門家を一度組み立てておいて、**名前ではなくトピックで**作業を割り振れます — チームが大きくなっても、ルーティングはそのまま維持されます。
|
||||
|
||||
## スクワッドの動作原理
|
||||
|
||||
- **リーダー 1 人、メンバー多数。** リーダーは必ずエージェントでなければならず、メンバーはエージェントでも人間のメンバーでも構いません。リーダーだけのスクワッドも許可されます(リーダーブリーフィングに「no other members」と表示されます)。同じエージェントが複数のスクワッドに所属することもできます。
|
||||
- **人を選べるあらゆる場所で割り当て可能。** スクワッドは Assignee ピッカー、@メンションピッカー、クイック作成モーダルに表示されます — エージェントやメンバーを選べる場所ならどこでも、スクワッドを選べます。
|
||||
- **アーカイブによるソフト削除。** スクワッドをアーカイブすると、ピッカーや一覧から消えます。現在そのスクワッドに割り当てられているイシューはすべて**リーダーエージェントに移管され**、作業が途切れないようにします。アーカイブされたスクワッドに新しいイシューを割り当てることはできません。
|
||||
|
||||
## スクワッドと単一エージェントのどちらを使うか
|
||||
|
||||
| スクワッドを選ぶ場合… | 単一エージェントを選ぶ場合… |
|
||||
|---|---|
|
||||
| 複数の専門家がいるが、このイシューに誰が合うか事前にわからないとき | 作業の範囲が 1 つの専門分野に明確で、誰がやるべきかわかっているとき |
|
||||
| 実際の応答者はイシューごとに変わっても、担当者(スクワッド)は安定して保ちたいとき | イシューにエージェントの名前を残し、明確な個人の説明責任を持たせたいとき |
|
||||
| コメントで `@FrontendTeam` のようなルーティング先がほしいとき | 一対一の `@agent-name` だけで十分なとき |
|
||||
|
||||
スクワッドは能力を加えません。**ルーティング**を加えます。メンバーは依然として普通のエージェントであり、リーダーの唯一の役割は適切な人を選ぶことです。
|
||||
|
||||
## 権限
|
||||
|
||||
| 操作 | 実行できる人 |
|
||||
|---|---|
|
||||
| スクワッドの作成 / 更新 / アーカイブ | ワークスペースの **owner** または **admin** |
|
||||
| メンバーの追加・削除、ロールの変更 | ワークスペースの **owner** または **admin** |
|
||||
| スクワッドにイシューを割り当て | すべてのワークスペースメンバー(エージェントへの割り当てと同じ) |
|
||||
| コメントでスクワッドを `@` でメンション | すべてのワークスペースメンバー |
|
||||
| スクワッドリーダーの評価を記録 | スクワッドリーダーエージェントのみ(CLI 経由) |
|
||||
|
||||
完全なロールマトリクスは[メンバーとロール](/members-roles)にあります。
|
||||
|
||||
## スクワッドを作成する
|
||||
|
||||
サイドバーで **スクワッド → 新しいスクワッド** を開き、次を入力してください。
|
||||
|
||||
- **名前(Name)** — 例: `Frontend Team`、`Bug Triage`。ワークスペース内で一意である必要はありません。
|
||||
- **説明(Description、任意)** — スクワッドカードと詳細ページに表示される短い紹介文。
|
||||
- **リーダー(Leader)** — 既存のエージェントを選びます。リーダーは `leader` ロールで自動的にスクワッドに追加されます。
|
||||
|
||||
作成後、スクワッドの詳細ページを開いて次を行えます。
|
||||
|
||||
- **メンバーを追加** — エージェントや人間のメンバーを選び、任意で各自に短いロールの説明(例: 「owns the migrations」、「reviewer of last resort」)を付けます。リーダーは誰に委任するかを決めるときにこれらのロールを使います。
|
||||
- **指示を書く** — リーダーが実行のたびに見るスクワッドレベルのガイダンスです(詳しくは後述)。
|
||||
- **アバターを設定** — エージェントに使うのと同じピッカーから選びます。
|
||||
|
||||
CLI での同等のコマンド:
|
||||
|
||||
```bash
|
||||
multica squad create --name "Frontend Team" --leader frontend-lead-agent
|
||||
multica squad member add <squad-id> --member-id <agent-or-user-uuid> --type agent --role "Owns Tailwind / shadcn surface"
|
||||
```
|
||||
|
||||
## スクワッドに割り当てられたイシューの実行方法
|
||||
|
||||
Backlog でないイシューがスクワッドに割り当てられると、Multica はただちに**リーダーエージェント**のための `task` をキューに入れます(すべてのメンバーのためではありません)。その後のフローは次のようになります。
|
||||
|
||||
1. **リーダーがタスクを引き受けます。** エージェントランタイムが次のポーリングでタスクを引き受けます。これは他のエージェント割り当てと同じです。
|
||||
2. **リーダーがブリーフィングを受けます。** 引き受けた時点で、Multica はリーダーのシステムプロンプトに 3 つのセクションを追記します — 下記の[リーダーが毎ターン見る内容](#what-the-leader-sees-on-every-turn)を参照してください。
|
||||
3. **リーダーが 1 つの委任コメントを投稿します。** そのコメントは、名簿(roster)にある正確なメンションマークダウンを使って、選んだメンバーを `@` でメンションします。このメンションが、メンションされた各エージェントのための新しい `task` をトリガーします。
|
||||
4. **リーダーが評価を記録します** — `multica squad activity <issue-id> action --reason "..."` を通じて記録します。これはイシューのアクティビティタイムラインにエントリを書き込み、リーダーが実際にトリガーを評価したことを人が確認できるようにします。
|
||||
5. **リーダーが停止します。** リーダーは実装作業を自分では行いません。委任されたメンバーが返信を投稿すると、リーダーが再びトリガーされて更新を読み、次のステップを委任するか、エスカレーションするか、沈黙を保ちます。
|
||||
|
||||
イシューが **Backlog** の状態であれば、リーダーはトリガーされません — Backlog は駐車場であり、エージェントへ直接割り当てる場合と同じルールが適用されます。
|
||||
|
||||
### リーダーが毎ターン見る内容
|
||||
|
||||
スクワッドリーダーが実行されるたびに、3 つのブロックがリーダーの指示に追記されます。
|
||||
|
||||
- **Squad Operating Protocol** — ハードコードされたルール集です: イシューを読み、`@` メンションで委任し、簡潔に(イシュー本文を繰り返さない — 担当者が自分で読めます)書き、毎ターン評価を記録し、**ディスパッチ後に停止する**。このプロトコルはシステムが管理しており、編集できません。
|
||||
- **Squad Roster** — リーダー自身の行と、アーカイブされていないメンバーごとに 1 行ずつで構成されます。各行には、リーダーが貼り付けるべき正確なメンションマークダウン(`[@Name](mention://agent/<uuid>)` または `[@Name](mention://member/<uuid>)`)が含まれています — 単なるテキストの `@name` を入力しても誰もトリガーされません。
|
||||
- **Squad Instructions** — このスクワッドのためのカスタムガイダンスです(スクワッドの詳細ページで設定するか、`multica squad update --instructions` で設定)。ルーティングルール(「DB 作業は Alice に、フロントエンドは Bob に」)、エスカレーションポリシー、その他イシュー自体にはない、リーダーが知っておくべき事柄を書くのに使ってください。
|
||||
|
||||
## リーダーが再トリガーされる場合
|
||||
|
||||
最初のディスパッチの後、リーダーはイシューの**ほとんどの後続コメント**によって自動的に起こされます。正確なルールは次のとおりです。
|
||||
|
||||
| イベント | リーダーがトリガーされるか? |
|
||||
|---|---|
|
||||
| 非メンバー(人間のレポーター、外部エージェント)がコメントを投稿 | **はい** |
|
||||
| スクワッドメンバーが `@mention` なしで進捗の更新を投稿 | **はい** — リーダーが次のステップが必要かどうかを再評価します |
|
||||
| 誰かが別のエージェント / メンバー / スクワッド / `@all` を明示的に `@` メンションするコメントを投稿 | **いいえ** — 明示的な `@` がルーティングシグナルであり、リーダーは身を引きます |
|
||||
| リーダー自身のコメント(自己トリガー) | **いいえ** — ループを防ぐためにガードされています |
|
||||
| イシューの相互参照(`[MUL-123](mention://issue/...)`)のみを含むコメント | **はい** — イシュー参照はルーティングではありません |
|
||||
|
||||
これらのルールの上に重複排除が適用されます。リーダーがこのイシューにすでに `queued` または `dispatched` 状態のタスクを持っている場合、新しいトリガーが重複したタスクをキューに入れることはありません。
|
||||
|
||||
<Callout type="info">
|
||||
**メンバーが `@` メンションを投稿したときにリーダーがトリガーされない理由。** スクワッドメンバーが誰かを直接 `@` したら、そのコメントは意図的な引き継ぎです — リーダーを起こしてルーティングを「観察」させても、何もしないターンを生み出してタイムラインを散らかすだけです。エージェントが書いたコメントは例外です。あるエージェントが別のエージェントを `@` する結果を投稿すると、リーダーは依然として起き上がり、スレッドを調整できます。
|
||||
</Callout>
|
||||
|
||||
## コメントでスクワッドを `@` でメンションする
|
||||
|
||||
スクワッドはメンバーやエージェントと並んで `@` ピッカーに表示されます。スクワッドをメンションすると `[@SquadName](mention://squad/<uuid>)` が挿入され、イシューをスクワッドに割り当てたかのように**スクワッドリーダー**をトリガーします — ただし担当者やステータスは変わりません。現在の所有者をそのまま保ちながら、スクワッドに質問やサブタスクを担当する人を選ばせたいときに使ってください。
|
||||
|
||||
同じアンチループのルールが適用されます。リーダーは自分自身をスキップし、同じコメント内に明示的なメンバーの `@` メンションがあれば、そのメンバーに直接ルーティングされます。
|
||||
|
||||
## スクワッドの再割り当てまたはアーカイブ
|
||||
|
||||
**イシューをスクワッドから別の担当者に再割り当てする**のは、他のあらゆる担当者変更と同じように動作します。イシューのアクティブなタスク(リーダーのものを含む)がすべてキャンセルされ、新しい担当者(エージェント、メンバー、または別のスクワッド)がキューに入ります。「担当者を変えずにスクワッドだけを外す」という別個の操作はありません。別の担当者を選んでください。
|
||||
|
||||
**スクワッドのアーカイブ**(`multica squad delete <id>`、または詳細ページの Archive ボタン):
|
||||
|
||||
1. **現在スクワッドに割り当てられているイシューをリーダーエージェントに移管し**、作業が途切れる代わりに具体的なエージェントを相手に継続するようにします。
|
||||
2. スクワッドに `archived_at` / `archived_by` を記録します。行は保存されるため過去のアクティビティエントリは引き続き解決されますが、スクワッドは一覧、ピッカー、@メンションのドロップダウンから消えます。
|
||||
3. このスクワッドへの**今後の割り当てを拒否**し、`cannot assign to an archived squad` を返します。
|
||||
|
||||
現在アーカイブ解除のコマンドはありません。ルーティングを復活させる必要がある場合は、新しいスクワッドを作成してください。
|
||||
|
||||
## CLI からのスクワッド操作
|
||||
|
||||
| コマンド | 用途 |
|
||||
|---|---|
|
||||
| `multica squad list` | ワークスペースのスクワッド一覧を表示 |
|
||||
| `multica squad get <id>` | 1 つのスクワッドの名前、リーダー、説明、指示を表示 |
|
||||
| `multica squad create --name "..." --leader <agent>` | スクワッドを作成(owner / admin) |
|
||||
| `multica squad update <id> [--name X] [--description X] [--instructions X] [--leader Y] [--avatar-url Z]` | 1 つ以上のフィールドを更新 |
|
||||
| `multica squad delete <id>` | アーカイブ(ソフト削除) — 割り当てられたイシューをリーダーに移管 |
|
||||
| `multica squad member list <id>` | スクワッドのメンバー一覧を表示 |
|
||||
| `multica squad member add <id> --member-id <uuid> --type agent\|member [--role "..."]` | メンバーを追加(owner / admin) |
|
||||
| `multica squad member remove <id> --member-id <uuid> --type agent\|member` | メンバーを削除(リーダーは削除できません — 先にリーダーを変更してください) |
|
||||
| `multica squad activity <issue-id> <action\|no_action\|failed> --reason "..."` | リーダーエージェントが毎ターン終了時に記録 |
|
||||
|
||||
`--leader` はエージェント名または UUID を受け付けます。それ以外の ID は `multica agent list --output json`、`multica workspace member list --output json`、`multica squad list --output json` から取得します。
|
||||
|
||||
## 次に
|
||||
|
||||
- [エージェントにイシューを割り当てる](/assigning-issues) — 同じフローで、スクワッド担当者にも適用されます
|
||||
- [コメントでエージェントを `@` でメンションする](/mentioning-agents) — `@` ピッカーにはスクワッドも表示されます
|
||||
- [エージェント](/agents) — エージェントとは何か、すべてのスクワッドの構成要素
|
||||
- [メンバーとロール](/members-roles) — owner / admin / member の完全な権限マトリクス
|
||||
@@ -123,7 +123,6 @@ multica squad member add <squad-id> --member-id <agent-or-user-uuid> --type agen
|
||||
| `multica squad delete <id>` | 보관(소프트 삭제) — 할당된 이슈를 리더에게 이전 |
|
||||
| `multica squad member list <id>` | 스쿼드의 멤버 목록 표시 |
|
||||
| `multica squad member add <id> --member-id <uuid> --type agent\|member [--role "..."]` | 멤버 추가(owner / admin) |
|
||||
| `multica squad member set-role <id> --member-id <uuid> --member-type agent\|member --role "..."` | 멤버를 제거하지 않고 역할 변경 |
|
||||
| `multica squad member remove <id> --member-id <uuid> --type agent\|member` | 멤버 제거(리더는 제거할 수 없습니다 — 먼저 리더를 변경하세요) |
|
||||
| `multica squad activity <issue-id> <action\|no_action\|failed> --reason "..."` | 리더 에이전트가 매 턴 종료 시 기록 |
|
||||
|
||||
|
||||
@@ -123,7 +123,6 @@ There is currently no unarchive command; create a new squad if you need the rout
|
||||
| `multica squad delete <id>` | Archive (soft-delete) — transfers assigned issues to the leader |
|
||||
| `multica squad member list <id>` | List a squad's members |
|
||||
| `multica squad member add <id> --member-id <uuid> --type agent\|member [--role "..."]` | Add a member (owner / admin) |
|
||||
| `multica squad member set-role <id> --member-id <uuid> --member-type agent\|member --role "..."` | Change a member's role without removing it |
|
||||
| `multica squad member remove <id> --member-id <uuid> --type agent\|member` | Remove a member (the leader cannot be removed — change leader first) |
|
||||
| `multica squad activity <issue-id> <action\|no_action\|failed> --reason "..."` | Recorded by the leader agent at the end of every turn |
|
||||
|
||||
|
||||
@@ -123,7 +123,6 @@ multica squad member add <squad-id> --member-id <agent-or-user-uuid> --type agen
|
||||
| `multica squad delete <id>` | 归档(软删除)——同时把当前分配给小队的 issue 转给队长 |
|
||||
| `multica squad member list <id>` | 列出小队成员 |
|
||||
| `multica squad member add <id> --member-id <uuid> --type agent\|member [--role "..."]` | 加成员(owner / admin)|
|
||||
| `multica squad member set-role <id> --member-id <uuid> --member-type agent\|member --role "..."` | 不移除成员,直接修改 role |
|
||||
| `multica squad member remove <id> --member-id <uuid> --type agent\|member` | 移除成员(**不能移除队长**——先换队长)|
|
||||
| `multica squad activity <issue-id> <action\|no_action\|failed> --reason "..."` | 队长每次结束前由它自己调用 |
|
||||
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
---
|
||||
title: タスク
|
||||
description: すべてのエージェント実行の作業単位であり、明確なステートマシン、タイムアウト、リトライルールを備えています。
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
import { Mermaid } from "@/components/mermaid";
|
||||
|
||||
**タスク**はすべての[エージェント](/agents)実行の単位です — [エージェントへのイシューの割り当て](/assigning-issues)、[コメントでのエージェントの @-メンション](/mentioning-agents)、[チャット](/chat)でのメッセージ送信、または[オートパイロット](/autopilots)が予定時刻に発火することは、いずれもタスクを生成します。Multica はそれをキューに入れ、[デーモン](/daemon-runtimes)が取得して対応する [AI コーディングツール](/providers)に引き渡し、完了するとその結果をサーバーに書き戻します。
|
||||
|
||||
タスクと[イシュー](/issues)は異なる 2 つのオブジェクトです。1 つのイシューは何度も割り当てられたり、@-メンションされたり、手動で再実行されたりでき — そのたびに**新しい**タスクが生成されます。
|
||||
|
||||
## タスクが経るステート
|
||||
|
||||
<Mermaid chart={`
|
||||
graph LR
|
||||
Q["Queued<br/>queued"] -->|daemon picks up| D["Dispatched<br/>dispatched"]
|
||||
D -->|agent starts| R["Running<br/>running"]
|
||||
R -->|success| C["Completed<br/>completed"]
|
||||
R -->|error or timeout| F["Failed<br/>failed"]
|
||||
Q -->|user cancels| X["Cancelled<br/>cancelled"]
|
||||
D -->|user cancels| X
|
||||
R -->|user cancels| X
|
||||
F -.retryable reason.-> Q
|
||||
`} />
|
||||
|
||||
- **Queued(キュー待ち)** — タスクが作成されたばかりで、デーモンが取得するのを待っている状態
|
||||
- **Dispatched(ディスパッチ済み)** — デーモンがタスクを占有し、AI コーディングツールを起動中
|
||||
- **Running(実行中)** — AI コーディングツールが実際に作業を実行中
|
||||
- **Completed(完了)** — 正常に終了し、成果物(コメント、コードコミット、ステータス変更)がサーバーに書き戻されます
|
||||
- **Failed(失敗)** — エラーまたはタイムアウトで中断。失敗理由がリトライ可能な場合、タスクは自動的に `queued` 状態に戻り、再試行されます
|
||||
- **Cancelled(キャンセル済み)** — ユーザーがキャンセルした場合
|
||||
|
||||
## タスクがタイムアウトしたときに起きること
|
||||
|
||||
Multica サーバーは 30 秒ごとにスキャンします。2 種類のタイムアウトが失敗を引き起こします。
|
||||
|
||||
| 状況 | タイムアウト |
|
||||
|---|---|
|
||||
| ディスパッチされたが開始されない(デーモンが取得したが AI ツールを起動しなかった) | **5 分** |
|
||||
| 実行が長すぎる | **2.5 時間** |
|
||||
|
||||
どちらのタイムアウトも失敗理由として `timeout` を使用し、**自動的にリトライされます**(次のセクション)。関連するランタイム欠落チェックについては、[デーモンとランタイム → ランタイムがオフラインとマークされるタイミング](/daemon-runtimes#when-a-runtime-is-marked-offline)を参照してください。
|
||||
|
||||
## どの失敗が自動的にリトライされ、どの失敗がされないか
|
||||
|
||||
失敗は 2 つのカテゴリに分かれます: **リトライ可能**と**リトライ不可**です。
|
||||
|
||||
**リトライ可能**(Multica が自動的に再キューイング):
|
||||
|
||||
- `runtime_offline` — タスクがディスパッチされた後にデーモンがいなくなった
|
||||
- `runtime_recovery` — デーモンがクラッシュして再起動し、終わらせなかったタスクを回収した
|
||||
- `timeout` — ランタイムまたはディスパッチのタイムアウト
|
||||
|
||||
**リトライ不可**(タスクは失敗状態のまま):
|
||||
|
||||
- `agent_error` — AI コーディングツール自体がエラーを報告した(API エラー、クォータ超過、内部バグ)。根本的な問題はリトライされません — 無限ループになってしまうためです。
|
||||
|
||||
自動リトライにはさらに 2 つの追加条件もあります。
|
||||
|
||||
1. **最大 2 回の試行** — オリジナル 1 回 + リトライ 1 回。リトライも失敗した場合は、理由がリトライ可能であってもそれ以上リトライしません。
|
||||
2. **イシューおよびチャットでトリガーされたタスクのみ** — オートパイロットでトリガーされたタスクは自動的にリトライ**されません**。
|
||||
|
||||
<Callout type="warning">
|
||||
**オートパイロットのタスクは自動的にリトライされません** — 意図された設計です。オートパイロットは独自の発火サイクル(例: 毎日)を持っており、失敗時に自動リトライが起きると次の予定実行と重なってしまいます。失敗後すぐに再実行が必要なら、手動の再実行を使用してください(次のセクション)。
|
||||
|
||||
**オートパイロットのタスクが失敗したことを知る方法**: [インボックス](/inbox)に通知が届き、関連するイシューのステータスが `in_progress` から `todo` に戻ります。[オートパイロット](/autopilots)ページでも、オートパイロットごとの最新の実行結果を確認できます。
|
||||
</Callout>
|
||||
|
||||
## 手動の再実行 vs. 自動リトライ
|
||||
|
||||
**手動の再実行**は、CLI または API(`POST /api/issues/{id}/rerun`)から自分でトリガーするものです。
|
||||
|
||||
```bash
|
||||
multica issue rerun <issue-id>
|
||||
```
|
||||
|
||||
動作:
|
||||
|
||||
- デフォルトでは、イシューの**現在のエージェント担当者**を対象とします — 以前のタスクを誰が実行したかに関わらず、再実行が現在の割り当てに従うようにしたいときに便利です。
|
||||
- 実行ログの特定の行にあるリトライボタンは、その行のタスク ID も一緒に送信するため、再実行は**現在の担当者ではなく、まさにそのタスクを実行したエージェント**を対象とします。これにより、スクワッドワーカー、並列の @-メンションエージェント、または再割り当てによってエージェントが入れ替わった行に対しても、行単位のリトライが意味を持つようになります。
|
||||
- このイシューに対する対象エージェントのキュー待ちまたは実行中のタスクを**キャンセルします**(ある場合)。同じイシューで他のエージェントが所有するタスク(例: 並列の @-メンション実行)はそのまま残します。
|
||||
- **まったく新しい**タスクを作成します — オリジナルのタスクが試行回数の上限に達していても、試行回数は 1 にリセットされます。
|
||||
- **新しいエージェントセッション**を開始します — 以前のセッション ID は**継承されません**。手動の再実行は、以前の成果物が悪かったと判断したことを意味するため、同じ会話を続けると汚染された状態をそのまま再生してしまいます。(一方、自動リトライはセッションを継承します — そのパスは悪い成果物ではなくインフラ障害のためのものです。)
|
||||
|
||||
比較:
|
||||
|
||||
| 項目 | 自動リトライ | 手動の再実行 |
|
||||
|---|---|---|
|
||||
| トリガー | システム、失敗理由に基づく | ユーザー、手動 |
|
||||
| 上限 | 2 回の試行 | 制限なし |
|
||||
| 適用できるソース | イシュー、チャット | エージェント担当者があるイシュー |
|
||||
| 選択されるエージェント | 失敗したタスクと同じエージェント | ソースタスクのエージェント(UI 行単位リトライ)またはイシューの現在の担当者(CLI / task_id なし) |
|
||||
| セッション継承 | あり(以前のセッションを再開) | なし(新しいセッション) |
|
||||
|
||||
## 失敗したタスクがイシューのステータスに与える影響
|
||||
|
||||
イシューがエージェントに割り当てられていてトリガーされたタスクが失敗すると(そして自動リトライが成功しないと)、**イシューのステータスが `in_progress` から `todo` に自動的に戻ります** — そのため、ボードを開くと「これはもう一度見る必要がある」とすぐに分かります。[イシューとプロジェクト](/issues)を参照してください。
|
||||
|
||||
## タスクが以前のコンテキストから続行できるか
|
||||
|
||||
できます — AI コーディングツールがセッション再開をサポートしている限りは。
|
||||
|
||||
Multica はタスク中にセッション ID を**2 回**固定します: 開始時に 1 回(AI ツールが最初のシステムメッセージを返したとき)、終了時に 1 回(完了または失敗時)。1 回目はデーモンが実行の途中でクラッシュしても復旧できるようにし、2 回目は次の**自動リトライ**のために予約され、その ID を渡し返すことでエージェントが以前の会話とファイル状態を引き継げるようにします。**手動の再実行は意図的にこのステップをスキップし**、新しいセッションを開始します — [手動の再実行 vs. 自動リトライ](#manual-rerun-vs-automatic-retry)を参照してください。
|
||||
|
||||
ただし、**実際にどの AI コーディングツールがこれをサポートするか**は大きく異なります。
|
||||
|
||||
- ✅ **実際にサポート** — Antigravity, Claude Code, Copilot, Hermes, Kimi, Kiro CLI, OpenCode, OpenClaw, Pi
|
||||
- ⚠️ **コードはあるが使用不可** — Codex, Cursor
|
||||
- ❌ **サポートなし** — Gemini
|
||||
|
||||
[プロバイダー対応表 → セッション再開](/providers#session-resumption-who-really-supports-it)を参照してください。
|
||||
|
||||
## 次へ
|
||||
|
||||
- [プロバイダー対応表](/providers) — 12 個の AI コーディングツール間の機能差(正確なセッション再開の状態を含む)
|
||||
- [エージェントへのイシューの割り当て](/assigning-issues) / [コメントでのエージェントの @-メンション](/mentioning-agents) / [チャット](/chat) / [オートパイロット](/autopilots) — タスクをトリガーする 4 つの方法
|
||||
@@ -1,279 +0,0 @@
|
||||
---
|
||||
title: トラブルシューティング
|
||||
description: Multica をセルフホストする際によく遭遇する問題 — 症状、原因、診断方法、解決方法。
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
|
||||
症状から問題を探してください。各項目では**症状 / 考えられる原因 / 診断方法 / 解決方法**を提供します。お使いの状況が一覧にない場合は、[GitHub](https://github.com/multica-ai/multica/issues) にイシューを登録してください。
|
||||
|
||||
## デーモンがサーバーに接続できない
|
||||
|
||||
**症状**: [`multica daemon`](/cli) の `status` コマンドが `offline` または `connection refused` を表示します。サーバーログに `/api/daemon/register` や `/api/daemon/heartbeat` のリクエストが見当たりません。デーモンの仕組みについては[デーモンとランタイム](/daemon-runtimes)を参照してください。
|
||||
|
||||
**考えられる原因**:
|
||||
|
||||
1. **`MULTICA_SERVER_URL` が誤ったアドレスを指している** — デフォルト値は `ws://localhost:8080/ws` で、セルフホスト時は自分のサーバーアドレスに変更する必要があります
|
||||
2. **ネットワーク / ファイアウォールによるブロック** — デーモンとサーバーが同じネットワークにいない、またはアウトバウンドトラフィックがブロックされている
|
||||
3. **トークンが期限切れまたは無効** — `multica login` を一度も実行していない、または PAT が取り消された
|
||||
4. **サーバーが登録を拒否した** — ログインしたアカウントが対象のワークスペースに所属していない(register が 403 を返す)
|
||||
5. **DNS 解決の失敗** — デーモンのマシンでホスト名が解決されない
|
||||
|
||||
**診断方法**:
|
||||
|
||||
```bash
|
||||
multica daemon logs --lines 100 # look for daemon-side errors
|
||||
echo $MULTICA_SERVER_URL # confirm the address is set
|
||||
curl -i http://<server-host>:8080/health # hit the server directly
|
||||
curl -i http://<server-host>:8080/readyz # include DB + migration readiness
|
||||
cat ~/.multica/config.json # verify api_token exists
|
||||
multica workspace list # confirm you're a member of the target workspace
|
||||
```
|
||||
|
||||
**解決方法**: 上記の各原因を 1 つずつ対処してください。最もよくある 2 つの解決策は、**`MULTICA_SERVER_URL` を変更してデーモンを再起動する**こと(`multica daemon restart`)と、**ログインし直す**こと(`multica logout && multica login`)です。
|
||||
|
||||
## タスクが `queued` で止まる
|
||||
|
||||
**症状**: エージェントにイシューを割り当てた後、イシューの状態はすぐに `in_progress` に変わりますが、長時間経ってもページにエージェント実行の兆候が見えません。`multica daemon status` はデーモンを `online` と表示しています。
|
||||
|
||||
**考えられる原因**(頻度順):
|
||||
|
||||
1. **エージェントの同時実行上限に到達** — このエージェントの `max_concurrent_tasks`(デフォルト 6)が、他の実行中タスクですでに埋まっている
|
||||
2. **同じイシューで同じエージェントの別タスクがまだ実行中** — 同じエージェント × 同じイシューは順次実行が強制されます(重複実行の防止)
|
||||
3. **エージェントがアーカイブされている** — アーカイブ後も新しいタスクはキューに入りますが、クレームできず、5 分後にタイムアウトします(code-issue G-01)
|
||||
4. **デーモンが現在のワークスペースにこのランタイムを登録していない** — デーモンを再起動するか、UI でランタイムを選択し直してください
|
||||
5. **デーモンの接続が切れた** — 直近 45 秒間ハートビートがありません。`daemon status` が `online` と表示されるのは、ごく最近切断された状態を反映している可能性があります
|
||||
|
||||
**診断方法**:
|
||||
|
||||
```bash
|
||||
multica daemon status --output json # runtime list + last_seen_at
|
||||
multica agent list # check agent archived state
|
||||
multica issue show <issue-id> # inspect task history
|
||||
```
|
||||
|
||||
サーバー側(セルフホスト)では、`"no_tasks"` / `"no_capacity"` を grep してクレームの結果を確認できます。
|
||||
|
||||
**解決方法**:
|
||||
|
||||
- 同時実行が満杯 → 実行中のタスクが終わるのを待つか、`multica agent update <id> --max-concurrent-tasks 10` で上限を引き上げてください
|
||||
- 同一イシューの順次実行 → 前のタスクが終わるのを待つか、別のエージェントに割り当て直してください
|
||||
- エージェントがアーカイブされている → `multica agent restore <id>`
|
||||
- ランタイム未登録 → `multica daemon restart` するとデーモンが再登録します
|
||||
|
||||
## WebSocket が接続できない
|
||||
|
||||
**症状**: ブラウザのコンソールに `WebSocket is closed` が記録されます。ページにリアルタイム更新(タスクの進捗、コメント、インボックス)が表示されず、再読み込みしないと見えません。バックエンドのタスクは引き続き実行されます。
|
||||
|
||||
**考えられる原因**:
|
||||
|
||||
1. **Origin チェックの失敗** — フロントエンドのドメインがサーバーの CORS 許可リストにありません。デフォルトの許可リストには `localhost:3000/5173/5174` のみが含まれ、公開インターネットでセルフホストするには `FRONTEND_ORIGIN` が必要です
|
||||
2. **プロトコルの不一致** — `https://` のフロントエンドには `wss://` が必要で、HTTP は `ws://` を使います
|
||||
3. **リバースプロキシが WebSocket アップグレードを有効にしていない** — Nginx / Envoy / HAProxy はデフォルトでは `Upgrade` ヘッダーを転送しません
|
||||
4. **JWT クッキーの期限切れまたは欠落** — 30 日の有効期限後にログインし直していない
|
||||
|
||||
**診断方法**:
|
||||
|
||||
- ブラウザの DevTools → Network → 「WS」でフィルタリングし、接続状態とステータスコードを確認してください
|
||||
- サーバーログで `"rejected origin"` / `"websocket"` を grep してください — origin の問題であれば明示的に表示されます
|
||||
- `curl -i http://<server-host>:8080/ws` は(`Upgrade` ヘッダー付きで)`101 Switching Protocols` を返すはずです
|
||||
|
||||
**解決方法**:
|
||||
|
||||
- Origin エラー → サーバーの `.env` に `FRONTEND_ORIGIN=https://multica.yourdomain.com` を設定(またはカンマ区切りの `CORS_ALLOWED_ORIGINS`)し、サーバーを再起動してください
|
||||
- プロトコルの不一致 → `FRONTEND_ORIGIN` のプロトコルがフロントエンドと一致しているか確認してください
|
||||
- リバースプロキシ → Nginx に `proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade";` を追加してください
|
||||
- クッキーの期限切れ → ページを再読み込みしてログインし直してください
|
||||
|
||||
## メールが届かない
|
||||
|
||||
**症状**: ログインまたは招待の受諾中にメールアドレスを送信したのに、インボックスにもスパムフォルダにも認証コードがありません。
|
||||
|
||||
**まず、サーバーがどのプロバイダーをアクティブと認識しているかを確認してください。** 起動時にバックエンドは次のいずれかを出力します。
|
||||
|
||||
- `EmailService: SMTP relay <host>:<port> from=<addr>` — SMTP を使用(`SMTP_HOST` が空でなければ Resend より優先)
|
||||
- `EmailService: Resend API from=<addr>` — Resend を使用
|
||||
- `EmailService: DEV mode — codes printed to stdout …` — プロバイダーが構成されていない
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.selfhost.yml logs backend | grep "EmailService:"
|
||||
```
|
||||
|
||||
期待していた行が見当たらない場合は、環境変数がプロセスに届いていません。`.env` と `docker compose -f docker-compose.selfhost.yml exec backend env | grep -E 'RESEND_|SMTP_'` を確認してください。この起動ログの行には認証情報は一切記録されません。
|
||||
|
||||
### Resend がアクティブなプロバイダーの場合
|
||||
|
||||
**考えられる原因**:
|
||||
|
||||
1. **`RESEND_API_KEY` が設定されていない** — サーバーは静かにフォールバックし、エラーを出さずに**コードを自身の stdout に書き込みます**。プロダクションで陥りやすい落とし穴です
|
||||
2. **Resend API キーが無効、またはクォータ超過** — サーバーログに `"failed to send verification code"` が表示されます
|
||||
3. **`RESEND_FROM_EMAIL` のドメインが Resend で検証されていない** — Resend が送信を拒否します
|
||||
4. **メールは送信されたが受信者の ISP にスパムと判定された** — Resend ダッシュボードとスパムフォルダを確認してください
|
||||
|
||||
**診断方法**:
|
||||
|
||||
- サーバーログで `"[DEV] Verification code for"` を grep してください — これがある場合、Resend が構成されておらず、コードが stdout に書き込まれたことを意味します
|
||||
- [Resend ダッシュボード](https://resend.com/) → Emails で送信履歴を確認してください
|
||||
- `RESEND_FROM_EMAIL` のドメインが Resend コンソールの「Verified Domains」リストに表示されるか確認してください
|
||||
|
||||
**解決方法**:
|
||||
|
||||
- API キーの欠落 → [サインインとサインアップの構成 → メールの仕組み](/auth-setup#how-email--verification-code-sign-in-works)に従って構成し、サーバーを再起動してください
|
||||
- ドメイン未検証 → Resend コンソールで DNS 検証フローを実行してください(SPF / DKIM レコードを追加)
|
||||
- 緊急時(内部テスト)→ サーバーログの `[DEV]` の下に出力されたコードをコピーしてください
|
||||
|
||||
### SMTP がアクティブなプロバイダーの場合
|
||||
|
||||
SMTP の経路はすべての失敗を失敗した段階とともにラップするため、サーバーログがすでにどの段階で relay がセッションを拒否したかを教えてくれます。`"failed to send verification email"` / `"failed to send invitation email"` を grep し、ラップされたエラーを確認してください。
|
||||
|
||||
| 記録されたエラー | 意味 | 解決方法 |
|
||||
|---|---|---|
|
||||
| `smtp dial <host>:<port>: dial tcp …: connect: connection refused` / `i/o timeout` | バックエンドコンテナが relay に到達できない — host が誤っている、port が誤っている、ファイアウォール、または relay が待ち受けていない | コンテナ内部から `SMTP_HOST` / `SMTP_PORT` が解決されるか確認してください(`docker compose -f docker-compose.selfhost.yml exec backend nslookup <host>` および `nc -vz <host> <port>`)。Multica を実行するホストから relay へのファイアウォールを開放してください |
|
||||
| `smtp starttls: x509: certificate signed by unknown authority`(または `certificate is not valid for any names`) | relay がプライベート CA / 自己署名証明書を使用しており、コンテナの信頼ストアがそれを拒否している | CA をコンテナにインストールするか、relay が信頼できるネットワークセグメント上で到達可能であることを確認したうえでのみ `SMTP_TLS_INSECURE=true` を設定してください |
|
||||
| `smtp auth: 535 5.7.8 Authentication credentials invalid`(または `534`/`530`) | `SMTP_USERNAME` / `SMTP_PASSWORD` が誤っている、または relay が `PLAIN` 以外の認証方式を要求している | メール管理者にサービスアカウントの認証情報を再確認してください。Exchange の匿名内部 relay の場合は両方を空のままにします(`SMTP_USERNAME=`、`SMTP_PASSWORD=`) |
|
||||
| `smtp MAIL FROM: 550 5.7.1 Client does not have permissions to send as this sender` | relay が `RESEND_FROM_EMAIL` をエンベロープ送信者として受け入れない — 典型的な Exchange の「anonymous users not allowed」または DMARC アラインメントの問題 | `RESEND_FROM_EMAIL` を relay が受け入れるドメインに設定してください。Exchange では receive connector で送信元 IP に `ms-Exch-SMTP-Accept-Any-Sender` を付与してください |
|
||||
| `smtp RCPT TO <addr>: 550 5.7.1 Unable to relay` | relay の receive connector が、あなたのサブネットから外部の受信者への中継を許可していない(外部ドメインと通信する匿名内部 relay で最も多い) | 招待を内部の受信者に制限するか、Multica ホストのサブネットを Exchange の「Anonymous Users → Relay」権限リストに追加してください |
|
||||
| `smtp DATA` / `smtp write body` / `smtp end data` | セッションは受け入れられたが relay が本文を破棄した — 通常はメッセージサイズ制限、コンテンツフィルタリング、または送信途中の接続リセットが原因 | relay のログで同じ `Message-ID`(ログには `<unixnano>@<host>` 形式)を確認してください。必要であればメッセージサイズの上限を引き上げてください |
|
||||
|
||||
`MAIL FROM`、`RCPT TO`、`DATA` のエラーは常に relay の応答コードとともに記録されるため、反対側の Exchange / Postfix のログと突き合わせることができます。認証コードと招待トークンは、ラップされたエラーに**決して**含まれません。
|
||||
|
||||
**診断方法**:
|
||||
|
||||
- 起動時に `"EmailService: SMTP relay"` を一度 grep し、ランタイムの失敗については `"failed to send"` を grep してください
|
||||
- バックエンドコンテナ内部から接続性を点検してください: `docker compose -f docker-compose.selfhost.yml exec backend sh -c 'nc -vz $SMTP_HOST $SMTP_PORT'`
|
||||
- 環境変数がプロセスに届いたか確認してください: `docker compose -f docker-compose.selfhost.yml exec backend env | grep SMTP_`(出力にパスワードが含まれるため、信頼できるシェルでのみ実行してください)
|
||||
|
||||
**解決方法**:
|
||||
|
||||
- host / port の誤り → `SMTP_HOST` / `SMTP_PORT` を調整してバックエンドを再起動してください。サポートされる relay モードは[認証設定 → Option B: SMTP relay](/auth-setup)を参照してください
|
||||
- 証明書の不一致 → relay の CA をコンテナにインストールするか、信頼できるネットワークセグメントで一時的に `SMTP_TLS_INSECURE=true` を設定してください
|
||||
- 認証の失敗 → 認証情報を再確認してください。匿名内部 relay の場合は `SMTP_USERNAME` と `SMTP_PASSWORD` を空のままにしてください
|
||||
- `Unable to relay` → 内部の受信者に制限するか、Exchange の receive connector で Multica ホストの IP に中継権限を付与してください
|
||||
|
||||
## 固定のローカルテストコードが動作しない
|
||||
|
||||
**症状**: セルフホストのインスタンスで `888888` のような固定のローカルテストコードでログインしようとしたところ、`invalid or expired code` で拒否されます。
|
||||
|
||||
**考えられる原因**(相互に排他的):
|
||||
|
||||
1. **`MULTICA_DEV_VERIFICATION_CODE` が空** — 固定コードはデフォルトで無効です
|
||||
2. **`APP_ENV=production`** — これは**正しい**プロダクション構成です。固定のローカルテストコードはプロダクションでは無視されます
|
||||
3. **構成されたコードが 6 桁でない** — このショートカットは 6 桁の値のみを受け付けます
|
||||
|
||||
**診断方法**:
|
||||
|
||||
```bash
|
||||
cat .env | grep -E 'APP_ENV|MULTICA_DEV_VERIFICATION_CODE'
|
||||
docker exec <container> env | grep -E 'APP_ENV|MULTICA_DEV_VERIFICATION_CODE'
|
||||
```
|
||||
|
||||
インボックス(スパムを含む)で実際の認証コードを確認してください。
|
||||
|
||||
**解決方法**:
|
||||
|
||||
- プロダクションでは `MULTICA_DEV_VERIFICATION_CODE` を空のままにし、Resend を構成して実際のコードを使用してください
|
||||
- ローカル開発や内部テストの場合は、サーバーログから生成されたコードをコピーするか、`APP_ENV=development` と `MULTICA_DEV_VERIFICATION_CODE=888888` を設定してください — 公開インスタンスでは固定コードを絶対に有効にしないでください(詳細は[サインインとサインアップの構成 → 固定のローカルテストコード](/auth-setup#fixed-local-testing-codes)を参照)
|
||||
|
||||
## 使用量ダッシュボードが 0 のままになる
|
||||
|
||||
**症状**: エージェントはタスクを完了し、生のトークン使用量はデータベースに記録されていますが、**設定 → 使用量**と**設定 → ランタイム**で入力 / 出力 / コストがすべて 0 と表示されます。これは静かに発生する現象で、バックエンドログにエラーはありません。
|
||||
|
||||
**考えられる原因**:
|
||||
|
||||
1. **`rollup_task_usage_hourly()` が一切スケジュールされていない** — 使用量 / ランタイムのダッシュボードは派生テーブル `task_usage_hourly` から読み取り、このテーブルはその関数によって埋められます。同梱の `pgvector/pgvector:pg17` イメージには `pg_cron` が含まれておらず、バックエンドもプロセス内で rollup を実行しません。外部スケジューラのない新規セルフホストインストールでは、これがデフォルトの状態です。
|
||||
2. **`pg_cron` はインストールされているが誤ったデータベースを指している** — `pg_cron.database_name` のデフォルト値は `postgres` です。Multica のデータベース名が異なる場合、スケジュールされたジョブは `rollup_task_usage_hourly()` を一切見つけられません。
|
||||
3. **スケジューラは動作しているが rollup が静かにエラーを出している** — 例えば cron エントリ内部の DB ロール / search_path が誤っている。
|
||||
|
||||
**診断方法**:
|
||||
|
||||
```sql
|
||||
-- Confirm raw events exist but the hourly table is empty.
|
||||
SELECT count(*) AS raw_rows FROM task_usage;
|
||||
SELECT count(*) AS hourly_rows FROM task_usage_hourly;
|
||||
|
||||
-- Confirm pg_cron is (or isn't) available.
|
||||
SELECT * FROM pg_available_extensions WHERE name = 'pg_cron';
|
||||
SHOW shared_preload_libraries;
|
||||
|
||||
-- If pg_cron is installed, check the schedule + last run.
|
||||
SELECT jobname, schedule, database, active FROM cron.job;
|
||||
SELECT jobname, status, return_message, start_time, end_time
|
||||
FROM cron.job_run_details ORDER BY start_time DESC LIMIT 10;
|
||||
|
||||
-- Watermark — if this is 1970-01-01, the rollup has never run.
|
||||
SELECT watermark_at FROM task_usage_hourly_rollup_state;
|
||||
```
|
||||
|
||||
**解決方法**:
|
||||
|
||||
- rollup を手動で一度呼び出して動作するか確認してください: `SELECT rollup_task_usage_hourly();` — ダッシュボードを再読み込みしてください。数値が表示されれば、欠けているのはスケジューラだけです。
|
||||
- [セルフホストクイックスタート → 使用量 rollup のスケジューリング](/self-host-quickstart#7-schedule-the-usage-rollup-required-for-the-usage-dashboard)からサポートされる方式のいずれかを選んでください: 外部 cron / systemd-timer / Kubernetes CronJob、または Postgres を `pg_cron` を含むイメージに置き換える。
|
||||
- スケジュール設定より前の履歴がすでにある場合は、バックエンドコンテナ内部で `backfill_task_usage_hourly` を実行し、ウォーターマーク以前のバケットを埋めてください。
|
||||
|
||||
## マイグレーション `103` が `refusing to drop legacy daily rollups` で失敗する
|
||||
|
||||
**症状**: `v0.3.4` から `v0.3.5+` にアップグレードする際、バックエンドコンテナが起動しない(または `migrate up` が中断する)とともに、次のエラーが発生します。
|
||||
|
||||
```text
|
||||
ERROR: refusing to drop legacy daily rollups:
|
||||
task_usage_hourly_rollup_state.watermark_at (1970-01-01 ...) trails
|
||||
task_usage latest event (...) by more than 01:00:00 — backfill is
|
||||
incomplete or pg_cron is not running. Run cmd/backfill_task_usage_hourly
|
||||
(and let pg_cron catch up) before re-running migrate
|
||||
```
|
||||
|
||||
**考えられる原因**: これはマイグレーション `103` の fail-closed ガードです。`task_usage_hourly` が生の `task_usage` に追いつくまで、レガシーの daily rollup の削除を拒否します。既存の行が存在し、rollup のウォーターマークがまだ epoch に留まっているとき — つまり、まだどの履歴も hourly テーブルに rollup されていないとき — にこのガードが発動します。
|
||||
|
||||
**解決方法**:
|
||||
|
||||
1. 同じデータベースに対して backfill を実行してください(冪等であり、中断しても安全で、再実行しても安全です):
|
||||
|
||||
```bash
|
||||
# Docker Compose
|
||||
docker compose -f docker-compose.selfhost.yml exec backend \
|
||||
./backfill_task_usage_hourly --sleep-between-slices=2s
|
||||
|
||||
# Kubernetes
|
||||
kubectl -n multica exec deploy/multica-backend -- \
|
||||
./backfill_task_usage_hourly --sleep-between-slices=2s
|
||||
```
|
||||
|
||||
2. アップグレードを再実行してください — バックエンドコンテナを再起動するだけで十分で、マイグレーションは起動時に実行されます。これでガードが最新のウォーターマークを確認し、`103` の適用を許可します。
|
||||
3. ウォーターマークが進み続けるように、継続的な rollup スケジュール(cron / `pg_cron`)を設定してください — [セルフホストクイックスタート → 使用量 rollup のスケジューリング](/self-host-quickstart#7-schedule-the-usage-rollup-required-for-the-usage-dashboard)を参照してください。
|
||||
|
||||
`--sleep-between-slices=2s` は、数年分の履歴を持つプロダクションデータベースにとって控えめなデフォルト値です。直近 N か月のみを保持し、それより古いバケットを永久に放棄してもかまわない場合は `--months-back N --force-partial` を使用してください。
|
||||
|
||||
## ポートの競合
|
||||
|
||||
**症状**: `multica server` や `multica daemon start` が `address already in use` で失敗します。
|
||||
|
||||
**考えられる原因**:
|
||||
|
||||
1. **サーバーポートが使用中**(デフォルト `8080`)
|
||||
2. **デーモンの health ポートが使用中**(デフォルト `19514`、プロファイルごとにハッシュでオフセット)
|
||||
3. **Web 開発サーバーのポート競合**(`3000` / `5173`)
|
||||
4. **ポートに対する権限不足**(`< 1024` の特権ポートのバインドには sudo が必要)
|
||||
|
||||
**診断方法**:
|
||||
|
||||
```bash
|
||||
lsof -i :8080 # macOS / Linux
|
||||
netstat -ano | findstr :8080 # Windows
|
||||
```
|
||||
|
||||
**解決方法**:
|
||||
|
||||
- 競合しているプロセスを終了する(`kill -9 <PID>`)か、`PORT=9000` でポートを変更してください
|
||||
- 80 / 443 を使うには → 直接バインドせず、前段にリバースプロキシ(Nginx / Caddy)を置いて高位ポートへ転送してください
|
||||
|
||||
## ログの場所
|
||||
|
||||
| 構成要素 | 場所 | コマンド |
|
||||
|---|---|---|
|
||||
| **デーモン** | `~/.multica/daemon.log`(バックグラウンドモード)またはフォアグラウンドの stdout | `multica daemon logs -f --lines 100` |
|
||||
| **サーバー(Docker)** | コンテナの stdout | `docker logs -f <container>` |
|
||||
| **サーバー(systemd)** | journal | `journalctl -u multica-server -f` |
|
||||
| **フロントエンド(dev)** | `pnpm dev` を実行中のターミナル | 直接確認 |
|
||||
| **フロントエンド(ブラウザ)** | DevTools → Console | `F12` を押す |
|
||||
|
||||
より詳細なデーモンログが必要な場合は、デーモンをバックグラウンドからフォアグラウンドに移してください: `multica daemon stop && multica daemon start --foreground`。
|
||||
@@ -1,56 +0,0 @@
|
||||
---
|
||||
title: ワークスペース
|
||||
description: ワークスペースはグループが協働する独立した空間で、すべてのイシュー、メンバー、コメント、エージェントが 1 つのワークスペースに属します。
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
|
||||
ワークスペースは **Multica でグループが協働する独立した空間**で、すべての[イシュー](/issues)、[メンバー](/members-roles)、[コメント](/comments)、[エージェント](/agents)が 1 つのワークスペースに属します。ログイン後に表示されるイシュー一覧、メンバー名簿、エージェント設定はすべて現在のワークスペースに限定されており、**ワークスペースを切り替えると画面全体が入れ替わります**。
|
||||
|
||||
## ワークスペースの作成
|
||||
|
||||
ワークスペースを作成するとき、3 つのことが決まります。
|
||||
|
||||
- **ワークスペース名** — メンバーに表示される表示名です。空白や非 ASCII 文字を使用できます。後から変更できます。
|
||||
- **Slug** — ワークスペース URL に使われる文字列です。小文字と数字のみが使用でき(`-` で連結)、**作成後は変更できないため**、慎重に選んでください。slug がすでに使用中だったり、システム予約語と重なったりする場合、作成画面で別の値を選ぶよう求められます。
|
||||
- **イシュー接頭辞** — ワークスペース内のすべてのイシュー番号の接頭辞です(`MUL-123` の `MUL`)。大文字と数字のみが使用でき、最大 10 文字です。
|
||||
|
||||
<Callout type="warning">
|
||||
**イシュー接頭辞は変更しないでください。** イシュー番号は現在の接頭辞でレンダリングされるため、接頭辞を変更すると `MUL-5` がただちに `NEW-5` になります。すべての外部リンク、Slack のメンション、コメント内の過去の参照が古い番号と合わなくなります。イシュー接頭辞は「作成時に決め、決して触らない」値として扱ってください。
|
||||
</Callout>
|
||||
|
||||
ワークスペースは Web UI から作成することも、コマンドラインから作成することもできます。
|
||||
|
||||
```bash
|
||||
multica workspace create
|
||||
```
|
||||
|
||||
## イシュー番号
|
||||
|
||||
ワークスペースで作成されるすべてのイシューには、`<接頭辞>-<数字>` 形式の番号が自動的に割り当てられます — `MUL-1`、`MUL-2`、`MUL-3`。いくつかの特性は次のとおりです。
|
||||
|
||||
- **ワークスペース内で連番かつ一意** — 各ワークスペースは独自のカウンターを保持し、ワークスペース同士は互いに干渉しません。
|
||||
- **手動で指定できない** — イシューを作成するときはタイトルのみを入力し、番号はシステムが割り当てます。
|
||||
- **削除しても再利用されない** — `MUL-5` を削除しても、次の新しいイシューは `MUL-5` ではなく `MUL-6` です。
|
||||
|
||||
## ワークスペースの削除
|
||||
|
||||
ワークスペースの owner のみが、ワークスペース全体を削除できます。削除は**取り消せません**。
|
||||
|
||||
<Callout type="warning">
|
||||
ワークスペースを削除すると、次の項目が一度にすべて消去されます。
|
||||
|
||||
- すべてのイシュー、プロジェクト、コメント、リアクション
|
||||
- すべての添付ファイル
|
||||
- すべてのメンバーシップと保留中の招待
|
||||
- すべてのエージェント設定とそのタスク履歴
|
||||
|
||||
**データは復旧できません。** 削除する前に、保管しておきたい項目をエクスポートしてください。
|
||||
</Callout>
|
||||
|
||||
ワークスペースの最後の owner であり、そのワークスペースから手を引きたい場合は、まず owner の役割を別のメンバーに移譲したうえで、新しい owner(または本人)が削除するかどうかを決定するようにしてください。[メンバーと役割](/members-roles)を参照してください。
|
||||
|
||||
## 次へ
|
||||
|
||||
- [メンバーと役割](/members-roles) — ワークスペースに人を追加する方法と、3 つの役割がそれぞれ何をできるか
|
||||
- [イシューとプロジェクト](/issues) — ワークスペース内部の中核となる作業オブジェクト
|
||||
@@ -1,11 +1,11 @@
|
||||
import { defineI18n } from "fumadocs-core/i18n";
|
||||
|
||||
// English is the default; Chinese (/zh/), Korean (/ko/), and Japanese (/ja/)
|
||||
// are available. hideLocale: 'default-locale' keeps English URLs prefix-free
|
||||
// English is the default; Chinese (/zh/) and Korean (/ko/) are available.
|
||||
// hideLocale: 'default-locale' keeps English URLs prefix-free
|
||||
// (`/docs/`) while translated locales live under `/docs/<lang>/...`.
|
||||
// parser: 'dot' picks up `page.zh.mdx` / `page.ko.mdx` / `page.ja.mdx` and `meta.<lang>.json`.
|
||||
// parser: 'dot' picks up `page.zh.mdx` / `page.ko.mdx` and `meta.<lang>.json`.
|
||||
export const i18n = defineI18n({
|
||||
languages: ["en", "zh", "ko", "ja"],
|
||||
languages: ["en", "zh", "ko"],
|
||||
defaultLanguage: "en",
|
||||
hideLocale: "default-locale",
|
||||
parser: "dot",
|
||||
|
||||
@@ -5,7 +5,6 @@ describe("prefixLocale", () => {
|
||||
it("prefixes root-relative paths with the active non-default locale", () => {
|
||||
expect(prefixLocale("/workspaces", "zh")).toBe("/zh/workspaces");
|
||||
expect(prefixLocale("/workspaces", "ko")).toBe("/ko/workspaces");
|
||||
expect(prefixLocale("/workspaces", "ja")).toBe("/ja/workspaces");
|
||||
expect(prefixLocale("/agents-create", "zh")).toBe("/zh/agents-create");
|
||||
});
|
||||
|
||||
@@ -31,7 +30,6 @@ describe("prefixLocale", () => {
|
||||
expect(prefixLocale("/zh/workspaces", "zh")).toBe("/zh/workspaces");
|
||||
expect(prefixLocale("/en/workspaces", "zh")).toBe("/en/workspaces");
|
||||
expect(prefixLocale("/ko/workspaces", "zh")).toBe("/ko/workspaces");
|
||||
expect(prefixLocale("/ja/workspaces", "zh")).toBe("/ja/workspaces");
|
||||
});
|
||||
|
||||
it("leaves external URLs alone", () => {
|
||||
|
||||
@@ -13,11 +13,9 @@ const pages = new Map<string, { url: string }>([
|
||||
["en:", { url: "/" }],
|
||||
["zh:", { url: "/zh" }],
|
||||
["ko:", { url: "/ko" }],
|
||||
["ja:", { url: "/ja" }],
|
||||
["en:agents", { url: "/agents" }],
|
||||
["zh:agents", { url: "/zh/agents" }],
|
||||
["ko:agents", { url: "/ko/agents" }],
|
||||
["ja:agents", { url: "/ja/agents" }],
|
||||
]);
|
||||
|
||||
vi.mock("@/lib/source", () => ({
|
||||
@@ -71,21 +69,6 @@ describe("docsAlternates", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("includes Japanese hreflang when a real *.ja.mdx page exists", async () => {
|
||||
existingDocs.add("agents.ja.mdx");
|
||||
const { docsAlternates } = await import("./site");
|
||||
|
||||
expect(docsAlternates(["agents"])).toEqual({
|
||||
canonical: "https://www.multica.ai/docs/agents",
|
||||
languages: {
|
||||
en: "https://www.multica.ai/docs/agents",
|
||||
zh: "https://www.multica.ai/docs/zh/agents",
|
||||
ja: "https://www.multica.ai/docs/ja/agents",
|
||||
"x-default": "https://www.multica.ai/docs/agents",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps the locale root alternates limited to real localized MDX pages", async () => {
|
||||
const { docsAlternates } = await import("./site");
|
||||
|
||||
|
||||
@@ -19,8 +19,6 @@ describe("docsSlugStaticParams", () => {
|
||||
{ lang: "zh", slug: ["agents"] },
|
||||
{ lang: "ko", slug: ["agents"] },
|
||||
{ lang: "ko", slug: ["cli", "reference"] },
|
||||
{ lang: "ja", slug: ["agents"] },
|
||||
{ lang: "ja", slug: ["cli", "reference"] },
|
||||
];
|
||||
|
||||
expect(docsSlugStaticParams(params)).toEqual([
|
||||
@@ -29,8 +27,6 @@ describe("docsSlugStaticParams", () => {
|
||||
{ lang: "zh", slug: ["agents"] },
|
||||
{ lang: "ko", slug: ["agents"] },
|
||||
{ lang: "ko", slug: ["cli", "reference"] },
|
||||
{ lang: "ja", slug: ["agents"] },
|
||||
{ lang: "ja", slug: ["cli", "reference"] },
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -28,18 +28,6 @@ export const uiTranslations: Partial<Record<Lang, Partial<Translations>>> = {
|
||||
chooseTheme: "테마 변경",
|
||||
editOnGithub: "GitHub에서 편집",
|
||||
},
|
||||
ja: {
|
||||
search: "検索",
|
||||
searchNoResult: "結果が見つかりません",
|
||||
toc: "このページの内容",
|
||||
tocNoHeadings: "見出しなし",
|
||||
lastUpdate: "最終更新",
|
||||
chooseLanguage: "言語を選択",
|
||||
nextPage: "次のページ",
|
||||
previousPage: "前のページ",
|
||||
chooseTheme: "テーマを変更",
|
||||
editOnGithub: "GitHub で編集",
|
||||
},
|
||||
};
|
||||
|
||||
// Display name shown in the LanguageToggle dropdown.
|
||||
@@ -47,7 +35,6 @@ export const localeLabels: Record<Lang, string> = {
|
||||
en: "English",
|
||||
zh: "简体中文",
|
||||
ko: "한국어",
|
||||
ja: "日本語",
|
||||
};
|
||||
|
||||
// Copy for the welcome page (Hero + Byline). Pages are translated as MDX;
|
||||
@@ -71,10 +58,4 @@ export const homeCopy = {
|
||||
titleAccent: "한곳에서.",
|
||||
byline: ["시작하기", "2026년 4월 업데이트", "약 6분 읽기"],
|
||||
},
|
||||
ja: {
|
||||
eyebrow: "Multica ドキュメント",
|
||||
titleLead: "人とエージェントが、",
|
||||
titleAccent: "一つの場所に。",
|
||||
byline: ["はじめに", "2026年4月更新", "約6分で読めます"],
|
||||
},
|
||||
} as const satisfies Record<Lang, unknown>;
|
||||
|
||||
@@ -17,7 +17,6 @@ import type {
|
||||
IssueStatus,
|
||||
IssuePriority,
|
||||
} from "@multica/core/types";
|
||||
import { formatDateOnly } from "@multica/core/issues/date";
|
||||
import { Text } from "@/components/ui/text";
|
||||
import { StatusIcon } from "@/components/ui/status-icon";
|
||||
import { PriorityIcon } from "@/components/ui/priority-icon";
|
||||
@@ -65,9 +64,12 @@ const TYPE_LABEL: Record<InboxItemType, string> = {
|
||||
quick_create_failed: "Quick-create failed",
|
||||
};
|
||||
|
||||
// due_date is a calendar day — format timezone-safely (no offset day shift).
|
||||
function shortDate(dateStr: string): string {
|
||||
return formatDateOnly(dateStr, { month: "short", day: "numeric" }, "en-US");
|
||||
if (!dateStr) return "";
|
||||
return new Date(dateStr).toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
function singleLine(value: string | null | undefined): string {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user